- [二进制漏洞]栈(Stack)溢出漏洞 Linux篇
- 前言
- 堆栈
- 堆栈(Stack)概念
- 堆栈数据存储方式
- 函数调用
- 函数调用C语言代码
- 函数调用过程GDB调试
- 函数Call返回原理
- 函数栈帧
- 函数栈帧描述
- 函数栈帧调试
- 栈溢出漏洞实战
- pwndbg调试
- 开始Hack
- Pwn菜鸡小分队
我们在学习栈溢出漏洞之前,最好都要懂一些开发,还有一些汇编知识,因为不管是安全还是逆向,这些都是基于开发的,有了开发扎实的基础在后续中才会突破瓶颈。
堆栈推荐大家可以先去看看《王爽汇编》,或者直接看Bilibili的堆栈是个啥?
堆栈(Stack)概念首先来了解下什么是堆栈?我们得从CPU开始说起,CPU中有个模块叫ALU
,专门用来处理数据运算。
学过汇编的小伙伴们都知道,CPU中有多个寄存器,不过是固定的,比如eax、ebx、ecx、edx、ebp、esp、edi、esi、eip
等,当处理的数据过多或者过大时候,寄存器都不够用了,这时候怎么办?
增加CPU的寄存器吗?不行那样成本太大了,所以就需要找另外的地方存数据,那么硬件中读取速度除了CPU,也就内存条速度最快了。
所以CPU招募了内存条,来用来存储数据,在内存条中还专门找了个区域用来存数据即:堆栈(stack)
,说白了堆栈就是一块内存
。
我简单的画了一个堆栈示意图,堆栈是一个自高地址向下增长的内存空间
,从图中可以看到我们的高地址,也就是栈底,而低地址大概4个空格的位置是栈顶。
也就是记住地址越低是栈顶,而且堆栈中要添加数据,地址要往跟低的地址移动。
接下来我们继续来看看,如何在堆栈中存取和读取数据,他既然是块内存,那么我们关注的肯定是存取和读取,首先堆栈中存入数据叫push
,读取数据叫pop
。
堆栈管理数据的方式是先进后出
,即存取进去的数据,会在堆底,最后存取进去的数据会在栈顶,所以最先拿出来的数据也是最后放进去的即栈顶。
这里有个需要注意的地方,就是很多人以为pop数据后,堆栈里面的数据就清空了,其实并不是。
之前说过堆栈其实就是一块内存,当我们pop后,其实知识把栈顶往下移了而已,内存里面的数据还是在的,并没有被清除掉,只是对于堆栈而言,那数据被弹出。
当要push新数据,push很多个数据,或者pop很多个数据,都按照图示以此类推。
函数调用 函数调用C语言代码学习堆栈最重要的应该就是函数调用了,当我们调用完一个函数后,代码都会往下继续执行下一句代码,那么这一步在底层是如何实现的呢?CPU怎么知道接下来要执行你函数调用完后的下一句代码?
这一部分其实稍微学过汇编的都应该知道。
当我们调用个函数的时候,在汇编层是叫Call myfunction
,而调用函数的时候就会用到堆栈,传入的参数即:push
。
#include <stdio.h>
/*自己的函数*/
void myfunction(int a,int b)
{
int c = a+b;
printf("%d\n",c);
}
int main()
{
myfunction(1,2);
printf("函数调用完毕!");
return 0;
}
函数调用过程GDB调试
接着我们将上面代码编译出来,并且关闭stack保护,编译成32位,命令gcc test.c -m32 -fno-stack-protector -o test
。
接下来用pwndbg
进行调试,详细的看下,函数调用与堆栈中的关系。gdb test , b main ,r
。
断点断到如上的位置,然后再单步n执行到call myfunction
处,此时注意观察堆栈,可以看到堆栈中压入了数据2
,1
。
而我们代码是 myfunction(1,2);
第一个参数是1
,第而个参数是2
,因为堆栈的先进后出的特性,所以先把最后的数据入栈。
接着最重要的一步,需要注意!
目前我们处在call myfunction
函数上,我们先记一下call myfunction的下一句汇编地址是多少,我这里是0x565555ac
,然后接着我们输入si
,单步步入进行调试,跳转到myfunction
函数的内部,然后此时注意观察你的堆栈有什么变化!
此时我们观察堆栈发现,之前我们call的下一句地址0x565555ac
被压栈了。
当我们一直单步步过myfunction
函数中的汇编代码,直到他的最后一句这里,发现汇编代码是一句ret
,ret的汇编代码其实就是pop eip
,
也就是将堆栈中的数据弹出到eip
,eip我们都知道是汇编中的PC指针,修改eip,那么当前CPU就会指向那地方开始执行代码。
而当前的堆栈数据就是我们调用myfunction
函数时压入的下一条指令的地址
,所以将其弹到eip,CPU就会指向那地方执行代码。
所以底层利用这种call 函数
时将下一条指令地址压栈的方式,然后执行完函数后再弹栈到eip
的方式跳过到调用完函数后的下一条代码。
栈帧也叫过程活动记录
,是编译器用来实现过程/函数调用
的一种数据结构
。
当一个函数在运行时,需要为它在堆栈
中创建一个栈帧
(stack frame)用来记录运行时产生的相关信息
,因此每个函数在执行前都会创建一个栈帧,在它返回时会销毁该栈帧。
所以说函数栈帧
就是一种数据结构,也是块内存里的数据。
我们继续用之前的代码做例子,然后用pwndbg
调试来详细的分析函数栈帧。
如上图,当我们准备调用call myfunction
的时候,其实在C语言中是当我们执行myfunction(1,2)
的时候就会生成一个栈帧,那么在汇编层具体是什么时候创建呢?
然后当我们进入到myfunction函数内部,然后看到第一条汇编语句是push ebp
,将ebp
寄存器压入堆栈。
EBP寄存器又被称为帧指针(Frame Pointer) 【指向当前栈帧的底部】
ESP寄存器又被称为栈指针(Stack Pointer) 【永远指向栈帧的顶部】
然后接着的一句汇编代码是mov ebp,esp
,这一句汇编指向完后,才开始真正的创建栈帧。
此时栈帧的数据结构差不多是这样: (现在我们就可以用ebp来进行寻址了,当我们要用到第一个参数那么用ebp+8即可,第二个参数ebp+0xC)
[ebp+0] -----> 栈帧底 ,也是当前的栈顶 【ebp】【esp】
[ebp+4] --> 调用完Call函数后下一条指令地址
[ebp+8] --> 1(参数1)
[ebp+0xC] --> 2(参数2)
在我们代码中myfunction
里面还有计算a+b的值赋值给c的代码
,我们继续调试看汇编且关注栈帧中对数据的处理。
当执行完栈帧创建后的汇编代码后,第一句的汇编代码是sub esp,0x10
,我们之前讲过esp永远为栈顶,当esp-16代表的是,esp要向上移动16字节,用来存放数据。
一般来说这种sub esp,xxx
或者add esp,-xxx
,都是用来创建临时变量 ,存放临时变量数据的。我们这里的临时变量就一个那就是int c
,那么int
占用4个字节,这里开辟了
16字节空间,可能是gcc的优化为了对齐什么的吧,Windows的话多少个临时变量空间就开辟多少空间。
那么此时的栈帧结构如下所示:
[ebp-0x10] 栈顶 [esp]
[ebp-0xC]
[ebp-8]
[ebp-4]
[ebp+0] -----> ebp 栈帧底 ,之前栈顶
[ebp+4] --> 调用完Call函数后下一条指令地址
[ebp+8] --> 1(参数1)
[ebp+0xC] --> 2(参数2)
【可以看到我们可以利用ebp这种方式来进行对临时变量的一个定位,因为ebp永远是栈底,所以可以用来寻找不同的数据,当ebp-代表的是临时变量,ebp+代表的是函数参数】
当我们继续单步执行代码,执行到如下图所示的地方,可以看到果然是利用【ebp+偏移】进行函数参数的定位,然后利用【ebp-偏移】进行临时变量的定位。
OK很好,到这里我们基本已经了解了函数调用栈帧
的一个详细原理了,这里再考考大家,那我在这个myfunction函数里要怎么知道返回后下一条代码的地址呢?
这个在之前说过了,当执行到ret汇编代码的时候,会把堆栈里面数据弹给eip。
那么现在我们用了函数调用帧的概念,是不是就很好懂了,当我们执行到ret的时候,这时候栈帧也就全部结束了,所以此时堆栈中的数据就是返回地址了。
也可以用[ebp+4]来代表返回地址。
最后从其他文章里面偷来的图片,方便理解函数栈帧概念。
栈溢出漏洞实战要求实现栈溢出来执行没有被调用的hack
函数。
要求:不允许使用pwntools工具
#include <stdio.h>
void hack()
{
printf("Hack Success!!!!\n");
}
int main()
{
printf("Hello,Please Start Hack!\n");
char buf[20];
scanf("%s",buf);
return 0;
}
首先我们执行程序,然后输入>=20字节,程序会崩溃(缓冲区溢出)!
pwndbg调试接下来老规矩,pwndbg
开始调试。
首先来找到返回地址,正常情况下[ebp+4]就是ret的返回地址,但是main函数可能不太一样。
调试下来发现,[ebp+20]才是返回地址,这个实际情况还是以ret语句时候堆栈里面的数据为准。
在这里我们可以手动用命令set *地址=值
来把return地址改成其他的,这里我们改成hack
函数。
OK上面我没通过调试器修改数值,直接将堆栈的值改成了hack函数的地址,让他在return的时候直接返回到hack函数,从而成功输出Hack Success!!!!
。
接下来我们用溢出来构造流程,让程序执行hack函数。
思路:
char buf[20]; 是20个字节的空间,因为他是个临时变量,所以他应该是用ebp-xxx来定位。
假设 [ebp-xxx] = buf地址
那么我们需要覆盖到的是返回地址,一般是在[ebp+4]
而这里strcpy允许我们任意的输入任何长度的字符串(造成漏洞的原因)
我们这里只要把[ebp+4]给覆盖了就行,所以我们在输入20个字符串后,再继续输入4个字符串会把[ebp+0]覆盖掉,因为溢出。
接着继续输入4个字符串,(28个字符串),就会把[ebp+4]也给覆盖掉,就覆盖到返回地址了。
程序ret的时候,就能跳到我们28个字符串中最后4个字符串构造的地址中去了。
因为我们这里调试出来是[ebp+20]
才是返回地址,而且这里buf是[ebp-0x1c]
,0x1c=28
,所以28字节刚好覆盖到ebp,那么再加20就覆盖到返回地址,所以长度是28+20=48
。
覆盖前
溢出覆盖后,溢出字符串1111111111111111111111111111111111111111111111112222
。
哈哈哈,一开始我还以为开心的结束了能hack到了,结果狗日的...有坑啊这玩意。
;这里把[ebp=8]地址设为栈顶,调试发现[ebp-8],刚好是char [20]字节后的数据,也就是溢出后的第一个字节地址。
0x565555e2 <main+74> lea esp, [ebp - 8]
;然后这里把栈顶弹给ecx寄存器
0x565555e5 <main+77> pop ecx
0x565555e6 <main+78> pop ebx
0x565555e7 <main+79> pop ebp
;这里又把[ecx-4],也就是[ebp-8]栈顶-4位置堆栈里面的 值 ,设置为新的esp,然后ret返回。
0x565555e8 <main+80> lea esp, [ecx - 4]
0x565555eb <main+83> ret
所以这里的思路是,我们可以来控制ecx寄存器,因为ecx寄存器是由[ebp-8]地址的值赋值过去的,这里刚好是我们溢出覆盖到的最开始4个字节,所以我们可以控制这个地址,然后让这个地址指向偏移-4位置,然后这位置里面的值是hack函数地址,即可hack成功!
哈哈,因为我自己出的题目,要求不能用pwntools工具,所以只能用ASCII码
来构造,构造来构造去发现ecx的堆栈地址是0xFF这种开头的,这种ASCII码对不上,超过能显示正常字符的ASCII码了,所以最后放弃了,我重新把题目代码改了下,改成了下面的样子。
题目要求:不能使用pwntools,让程序执行hack函数。
#include <stdio.h>
int _a = 1;
int _b = 2;
int _c = 3;
int _d = 4;
int _e = 5;
int _f = 6;
int _g = 7;
int _h = 0x5655556d;
int _i = 8;
int _j = 9;
void hack()
{
asm("mov esp,0xffffd57c\n");
printf("Hack Success!!!!\n");
asm("mov ebx,0\n");
asm("mov eax,1\n");
asm("int 0x80\n");
}
int main()
{
printf("Hello,Please Start Hack!\n");
char buf[20];
scanf("%s",buf);
return 0;
}
解题思路:
这题目不同电脑可能运行效果不一样,因为我把地址写死了,我这里把hack函数地址写到了全局变量,而且故意是第8个全局变量,因为这位置刚好是 .data段中地址是 可以用ASCII码来显示的,然后我在hack函数开头用了一个汇编设置了栈顶,因为不设置的话调用printf函数会失败,最后用汇编调用int 80(中断),功能号1 exit来强制退出程序,让其能显示出Hack Suucess字符串。
因为构造中是要[ecx-4]
才是返回地址,所以我们要填入的地址是0x56557028
,字符串是VUp(
因为内存中是大端存储,我们要反过来,改成(pUV
。
最后加上20个字符串用来做溢出,payload如下。
Payload:
11111111111111111111(pUV
调试图:
Pwn菜鸡小分队最后感谢大家的阅读,本菜鸡也是刚学,文章中如有错误请及时指出。
大家也可以来群里骂我哈哈哈,群里有PWN、RE、WEB大佬,欢迎交流