棧溢出攻擊是個老話題了,本質上就是通過合法的方式輸入不符合規的數據來破壞棧上的數據,從而執行惡意代碼。
以下內容以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,而這個地址不存在,引發了一個內存錯誤
0x02 微軟的愛,緩衝區溢出檢查
爲了減輕開發者的負擔,VC編譯器增加了一個棧保護功能:/GS 開關
這個選項默認是開啓的,我們看看函數變成什麼樣子了
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 <dddd._strcpy>
add esp,8
pop edi
pop esi
pop ebx
mov ecx,dword ptr ss:[ebp-4]
xor ecx,ebp
call <dddd.@__security_check_cookie@4> // 檢查cookie是否被修改
mov esp,ebp
pop ebp
ret
手段也很簡單,/gs打開後,c runtime會在程序啓動時產生一個隨機數,稱爲cookie,然後編譯器會在每個子過程中開頭將這個cookie壓入棧中,讓它處於ebp和返回地址的中間,如果攻擊者還通過第一節中的方法來覆蓋返回地址,勢必會將這個cookie覆蓋掉,而過程最後會通過一個嵌入的__security_check_cookie函數來檢查cookie,如果被覆蓋了就報錯。
這個__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技術,在棧上執行代碼這條路基本走不通了。
簡單地說:
ASLR(隨機基址):開啓後,PE鏡像每次載入內存後的地址是不固定的,攻擊者無法對固定的地址攻擊。
DEP(數據執行保護):開啓後,堆棧區域將無法執行代碼,這樣就杜絕了ShellCode。
當然有些大牛提供了繞過的思路,網上可查,這裏不展開。