了解栈溢出攻击与保护

栈溢出攻击是个老话题了,本质上就是通过合法的方式输入不符合规的数据来破坏栈上的数据,从而执行恶意代码

以下内容以x86程序来说明,x64大同小异。

0x01 栈的内存布局

要了解如何攻击,就要先掌握一个函数的栈空间里是如何摆放数据的

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,而这个地址不存在,引发了一个内存错误

了解栈溢出攻击与保护_第1张图片

0x02 微软的爱,缓冲区溢出检查

为了减轻开发者的负担,VC编译器增加了一个栈保护功能:/GS 开关

了解栈溢出攻击与保护_第2张图片

这个选项默认是开启的,我们看看函数变成什么样子了

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,如果被覆盖了就报错。

了解栈溢出攻击与保护_第3张图片

这个__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

0x03 更安全的函数

对于像strcpy、memcpy等危险的函数,在标准上已经有安全的版本了,比如strcpy_s、memcpy_s等,更多相关函数可以看手册:https://docs.microsoft.com/en-us/cpp/c-runtime-library/security-enhanced-versions-of-crt-functions

0x04 JMP ESP

通过第一节已经了解了栈溢出攻击的原理,但是有个问题,胡乱覆盖数据只会导致程序崩溃而已,似乎并没有太多作用。

攻击的目的一般来说是要执行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技术,在栈上执行代码这条路基本走不通了。

了解栈溢出攻击与保护_第4张图片

简单地说:

ASLR(随机基址):开启后,PE镜像每次载入内存后的地址是不固定的,攻击者无法对固定的地址攻击。

DEP(数据执行保护):开启后,堆栈区域将无法执行代码,这样就杜绝了ShellCode。

当然有些大牛提供了绕过的思路,网上可查,这里不展开。

你可能感兴趣的:(Windows编程)