栈溢出攻击是个老话题了,本质上就是通过合法的方式输入不符合规的数据来破坏栈上的数据,从而执行恶意代码。
以下内容以x86程序来说明,x64大同小异。
要了解如何攻击,就要先掌握一个函数的栈空间里是如何摆放数据的
void test()
{
int a = 0x11111111;
int b = 0x22222222;
}
汇编代码如下
push ebp
mov ebp,esp
sub esp,48
push ebx
push esi
push edi
mov dword ptr ss:[ebp-4],11111111
mov dword ptr ss:[ebp-8],22222222
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
建议通过像od或x64dbg这样的调试器来查看比较方便
当我们执行到ret时,栈空间如下
010FFDC8 22222222 // var b
010FFDCC 11111111 // var a
010FFDD0 010FFE24 // ebp
010FFDD4 00D9107E // 返回地址
栈空间排放顺从栈顶到栈底
1.局部变量
2.子过程入口压入的ebp值
3.子过程返回地址
攻击者的工作就是找出软件中进行了数据输入且有内存操作的地方,比较典型的就是strcpy、memcpy等函数。
来看看一个问题代码
void test()
{
char s[8];
strcpy(s, "11112222");
}
子过程返回时栈的情况
0053FD38 31313131 // 1111
0053FD3C 32323232 // 2222
0053FD40 0053FD00 // ebp
0053FD44 0089107E // 返回地址
目前因为输入的数据长度没有超过变量允许的长度。但如果我输入的数据超过8个字节将会覆盖掉ebp和返回地址
void test()
{
char s[8];
strcpy(s, "1111222233334444");
}
栈空间
012FFA74 31313131
012FFA78 32323232
012FFA7C 33333333 // 原本ebp值被覆盖
012FFA80 34343434 // 原本函数返回地址被覆盖
如果攻击者提前在内存里写入了一段ShellCode代码,然后在这里覆盖掉返回地址为ShellCode的入口,情况可想而知。。。
所以黑客要成功攻击程序的话,有个前提条件就是开发者的“配合”:用了不安全的函数或直接对指针操作而不检查数据最大合法长度。
上面的子过程会执行ret 0x34343434,而这个地址不存在,引发了一个内存错误
为了减轻开发者的负担,VC编译器增加了一个栈保护功能:/GS 开关
这个选项默认是开启的,我们看看函数变成什么样子了
push ebp
mov ebp,esp
sub esp,4C
mov eax,dword ptr ds:[<___security_cookie>] // 取了一个全局变量cookie
xor eax,ebp
mov dword ptr ss:[ebp-4],eax // cookie入栈
push ebx
push esi
push edi
push dddd.ED2088
lea eax,dword ptr ss:[ebp-C]
push eax
call
add esp,8
pop edi
pop esi
pop ebx
mov ecx,dword ptr ss:[ebp-4]
xor ecx,ebp
call // 检查cookie是否被修改
mov esp,ebp
pop ebp
ret
手段也很简单,/gs打开后,c runtime会在程序启动时产生一个随机数,称为cookie,然后编译器会在每个子过程中开头将这个cookie压入栈中,让它处于ebp和返回地址的中间,如果攻击者还通过第一节中的方法来覆盖返回地址,势必会将这个cookie覆盖掉,而过程最后会通过一个嵌入的__security_check_cookie函数来检查cookie,如果被覆盖了就报错。
这个__security_cookie是导出的,我们可以获得它
extern UINT_PTR __security_cookie;
int main()
{
printf("__security_cookie = 0x%x\n", __security_cookie);
getchar();
return 0;
}
它是怎么初始化的呢,可以看下这篇文章:http://blog.sina.com.cn/s/blog_4e0987310101ie77.html
关于/GS开关:https://docs.microsoft.com/en-us/cpp/build/reference/gs-buffer-security-check
关于Cookie初始化函数:https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/security-init-cookie
对于像strcpy、memcpy等危险的函数,在标准上已经有安全的版本了,比如strcpy_s、memcpy_s等,更多相关函数可以看手册:https://docs.microsoft.com/en-us/cpp/c-runtime-library/security-enhanced-versions-of-crt-functions
通过第一节已经了解了栈溢出攻击的原理,但是有个问题,胡乱覆盖数据只会导致程序崩溃而已,似乎并没有太多作用。
攻击的目的一般来说是要执行ShellCode代码,如何能把ShellCode输入进去并且执行呢?这就是JMP ESP解决的问题。
所谓JMP ESP就是将返回地址指向含有"jmp esp"汇编语句的地址
0x11111110 ret 0x88888888 // 跳向jmp esp语句的地方
0x11111114 ... // ShellCode代码
...
0x88888888 jmp esp // 此时esp正好等于0x11111114
因为ret执行后esp正好+4指向了后面的指令,然后jmp esp跳转到ShellCode开始执行。
不过可惜的是,这个技术已经很古老了,因为微软后来引入了ASLR和DEP技术,在栈上执行代码这条路基本走不通了。
简单地说:
ASLR(随机基址):开启后,PE镜像每次载入内存后的地址是不固定的,攻击者无法对固定的地址攻击。
DEP(数据执行保护):开启后,堆栈区域将无法执行代码,这样就杜绝了ShellCode。
当然有些大牛提供了绕过的思路,网上可查,这里不展开。