当前位置: 首页 > 科技观察

黑客与防御:缓冲区溢出攻击与堆栈保护

时间:2023-03-22 00:05:44 科技观察

大家好,我是小风哥。在上篇文章《??进程切换的本质是什么???》中举了一个例子,就是这段代码:exit(-1);}voidfuncB(){long*p=NULL;p=(long*)&p;*(p+2)=(long)funcC;}voidfuncA(){funcB();}intmain(){funcA();return0;}有同学在微信群里问不能在自己机器上复现,给出了编译好的机器指令:00000000004005ee:4005ee:55push%rbp4005ef:4889e5mov%rsp,%rbp4005f2:4883ec10sub$0x10,%rsp4005f6:64488b04252800mov%fs:0x28,%rax4005fd:00004005ff:45f8mov%rax,-0x8(%rbp)400603:31c0xor%eax,%eax400605:48c745f0000000movq$0x0,-0x10(%rbp)40060c:0040060d:488d45f0lea-0x10(%rbp),%rax400611:488945f0mov%rax,-0x10(%rbp)400615:488b45f0mov-0x10(%rbp),%rax400619:4883c010添加$0x10,%rax40061d:bad6054000mov$0x4005d6,%edx400622:488910mov%rdx,(%rax)400625:90nop400626:488b45f8mov-0x8(%rbp),%rax40062a:64483304252800xor%fs:0x28,%rax400631:0000400633:7405je40063aB400635:e866feffffcallq4004a0<__stack_chk_fail@plt>40063a:c9leaveq40063b:c3retq仔细看这段代码,有这样一条可疑指令:mov%fs:0x28,%raxmov%rax,-0x8(%rbp)这两行指令将fs:[0x28](段寻址方式)处的值压入调用栈(%rbp偏移8字节),函数即将返回时再次检查值是否被修改:mov-0x8(%rbp),%raxxor%fs:0x28,%rax接下来,如果栈中保存的值不等于fs:[0x28]处的值(用于比较的异或指令)则跳转到__stack_chk_fail函数,我们的问题是为什么会有这样的检查?本质上,我们一开始给出的代码是相对于缓冲区溢出攻击而言的。方法是修改之前栈帧的返回地址,改成某个具体地址(黑客想跳转到的地方);在开篇代码中,funcA函数调用funcB后需要返回funcA,但在我们“精心”设计下,调用funcB后跳转到funcC。我们有什么办法可以防止这种攻击吗?答案是肯定的,这种攻击方式可以追溯到很久很久以前。在上世纪初,采煤是一项非常危险的工作,因为煤矿中的有毒气体通常极难被人类察觉,给矿工的生命带来了极大的威胁,而金丝雀对有毒气体非常敏感,因此矿工可以使用金丝雀监测矿区,及早发现危险。这里也是一样,我们可以在栈区放一个“金丝雀”(fs:[0x28]处的值):当函数返回时,我们将取fs:[0x28]处的值和栈上的值再用“金丝雀”做对比,一旦发现这两个值不一样,就可以认为当前栈已经被销毁,因为栈上的数据已经不可信了,所以必须尽快撤离矿区尽可能的,也就是提前调用__stack_chk_fail函数终止进程。canary也是fs:[0x28]是随机生成的(每次程序运行都不一样),所以攻击者很难提前知道这个值是多少。当然我们也可以看到,增加栈保护功能需要增加额外的机器指令,对性能也会有轻微的影响,代价就是需要多执行一部分机器指令。这就是编译器的栈保护功能。当然这个功能也可以去掉。编译的时候加入-fno-stack-protector编译选项,这样就可以关闭栈保护功能,生成的代码可以重现上一篇文章《??进程切换的本质是什么???》中提到的效果就没有了。怎么样,要成为黑客也不是那么容易的,就像只有真正了解法律才能钻空子,真正了解计算机的工作原理才能破解一样。当然,了解计算机并不足以成为顶级黑客。你还需要想象力。