深度剖析堆棧中ShellCode的利用原理
VSole2022-08-17 06:56:54
前言
在進入正題之前,需要先了解一下函數調用的堆棧變化。
//代碼內容:一個簡單的函數調用//環境:vs2022 debug x86#include <iostream>
void fun() {
}
int main() { fun(); return 0;}
;fun函數調用的匯編代碼;在執行該匯編語句時,會將該代碼的下一個指令的地址進行壓棧(0x006C1886);目的是為了回跳,繼續執行后續代碼006C1881 call 006C10F0006C1886 xor eax,eax
在執行完call指令之后,正式進入函數內部,進行一系列初始化及環境保存操作。
;提升堆棧006C1800 push ebp 006C1801 mov ebp,esp 006C1803 sub esp,0C0h ;堆棧默認提升0xc0大小;保存函數調用前的寄存器狀態006C1809 push ebx 006C180A push esi 006C180B push edi ;初始化提升的新堆棧006C180C mov edi,ebp 006C180E xor ecx,ecx 006C1810 mov eax,0CCCCCCCCh 006C1815 rep stos dword ptr es:[edi] 006C1817 mov ecx,6CC066h 006C181C call 006C1311 ;一般功能代碼在中間附近出現 ;結束功能代碼后,恢復現場006C1821 pop edi 006C1822 pop esi 006C1823 pop ebx 006C1824 add esp,0C0h 006C182A cmp ebp,esp 006C182C call 006C123A 006C1831 mov esp,ebp ;恢復調用前的堆棧006C1833 pop ebp ;跳轉回地址0x006C1886處006C1834 ret
函數調用中的堆棧情況如下:

正文
- 對上述兩個堆棧狀態有個印象,接下來繼續進行代碼分析。
- 在用vs環境進行代碼編寫的時候,遇見類似strcpy,strcmp等函數調用時,如果不將設置中的安全檢查取消,那么將判定上述函數為危險函數。其危險的根本就是沒有對拷貝、比較等操作限制字符個數,如果無法預見“\0”字符或比較出差值,那么將一直執行下去。
#報錯如下D:\Code\CPP\CPP控制臺程序\CPP控制臺程序\main.cpp(7,2): error C4996: 'strcpy': This function or variable may be unsafe. Consider using strcpy_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.
//代碼內容:一個簡單的函數調用//環境:vs2022 debug x86#include <iostream>
void fun(const char * c ) { char arr[4]; strcpy(arr, c);}
int main() { char c[1024] = { 0 }; scanf("%s"); fun(c); return 0;}/*程序開始后輸入:11111111111111111111*/
;提升堆棧00141EC0 | 55 | push ebp | main.cpp:500141EC1 | 8BEC | mov ebp,esp |;sub esp,CC這句表示提升棧空間0xCC大小,十進制也就是204字節00141EC3 | 81EC CC000000 | sub esp,CC |;保存現場00141EC9 | 53 | push ebx |00141ECA | 56 | push esi | esi:__enc$textbss$end+2300141ECB | 57 | push edi |;初始化新堆棧00141ECC | 8D7D F4 | lea edi,dword ptr ss:[ebp-C] |00141ECF | B9 03000000 | mov ecx,3 |00141ED4 | B8 CCCCCCCC | mov eax,CCCCCCCC | eax:"11111111111111111111"00141ED9 | F3:AB | rep stosd |00141EDB | B9 66C01400 | mov ecx,_A0FF1F1C_CPP | main.cpp:1573248000141EE0 | E8 2CF4FFFF | call cpp控制臺程序.141311 |;拷貝功能代碼的匯編形式00141EE5 | 8B45 08 | mov eax,dword ptr ss:[ebp+8] | main.cpp:7, [ebp+8]:"11111111111111111111"00141EE8 | 50 | push eax | eax:"11111111111111111111"00141EE9 | 8D4D F8 | lea ecx,dword ptr ss:[ebp-8] |00141EEC | 51 | push ecx |00141EED | E8 C9F4FFFF | call cpp控制臺程序.1413BB | strcpy;結束函數調用,進行堆棧平衡00141EF2 | 83C4 08 | add esp,8 |00141EF5 | 52 | push edx | main.cpp:800141EF6 | 8BCD | mov ecx,ebp |00141EF8 | 50 | push eax | eax:"11111111111111111111"00141EF9 | 8D15 1C1F1400 | lea edx,dword ptr ds:[<>] |00141EFF | E8 D7F2FFFF | call cpp控制臺程序.1411DB |;恢復現場00141F04 | 58 | pop eax | eax:"11111111111111111111"00141F05 | 5A | pop edx |00141F06 | 5F | pop edi |00141F07 | 5E | pop esi | esi:__enc$textbss$end+2300141F08 | 5B | pop ebx |00141F09 | 81C4 CC000000 | add esp,CC |00141F0F | 3BEC | cmp ebp,esp |00141F11 | E8 24F3FFFF | call cpp控制臺程序.14123A |00141F16 | 8BE5 | mov esp,ebp |00141F18 | 5D | pop ebp |;結束當前函數調用00141F19 | C3 | ret |
下圖是對fun函數調用時,堆棧棧底值含義的分析

開始分析功能代碼部分
;將輸入的字符串的地址入棧00141EE5 | 8B45 08 | mov eax,dword ptr ss:[ebp+8] | main.cpp:7, [ebp+8]:"11111111111111111111"00141EE8 | 50 | push eax | eax:"11111111111111111111";將目標內存地址入棧,根據ebp-8我們可以清楚地知道,char arr[4]的空間位置00141EE9 | 8D4D F8 | lea ecx,dword ptr ss:[ebp-8] |00141EEC | 51 | push ecx |;調用strcpy函數00141EED | E8 C9F4FFFF | call cpp控制臺程序.1413BB | strcpy

執行strcpy函數后,堆棧圖如下:

我們可以清楚地看見,一個strcpy函數的執行,直接將原來的堆棧環境打亂
- 舊棧底地址值被更改
- 函數結束的回跳地址被更改(改成shellCOde的起始地址)
- 更改地址的堆棧也可以被隨意更改(改寫成shellCode具體內容
利用
- 思考:如果我們輸入的內容,恰好將函數的回跳地址重新覆蓋,覆蓋成我們shellCode地址的開始地址,那么接下來繼續執行的代碼就是我們的惡意代碼
- 問題:程序每次執行或操作系統重啟,地址都會發生改變,如果將回跳地址寫死,那么費力編寫的惡意字符串也就變成了無用的字符串
- 輸入惡意拼接的字符串,就從左側的正常堆棧變成右側被shellCode填充的堆棧

根據上邊的問題,我們需要一個動態定位ShellCode地址的方法。讓我們詳細分析下函數調用恢復的過程。
;恢復寄存器現場00141F04 | 58 | pop eax | eax:"11111111111111111111"00141F05 | 5A | pop edx |00141F06 | 5F | pop edi | edi:"111111111111"00141F07 | 5E | pop esi | esi:__enc$textbss$end+23;將舊棧底重新賦給棧底指針,但是舊棧底會在輸入惡意字符串時被破壞00141F08 | 5B | pop ebx |;esp棧頂寄存器向高位移動,棧提升使用了sub esp,cc,棧頂恢復與此相反;重點1:此時的esp指向了老ebp的存儲空間00141F09 | 81C4 CC000000 | add esp,CC |00141F0F | 3BEC | cmp ebp,esp |00141F11 | E8 24F3FFFF | call cpp控制臺程序.14123A |00141F16 | 8BE5 | mov esp,ebp |00141F18 | 5D | pop ebp |;最后利用ret將回跳地址pop并跳轉;重點2:此時ebp指向了shellCode的首地址00141F19 | C3 | ret |
在考慮更改eip寄存器的常用指令:
- ret
- jmp系列指令

經過兩次跳轉,成功的將eip指向了shellCode位置,進行繼續執行shellCode
利用跳轉的原因
- 動態定位shellCode
- shellCode復用
總結
- 盡量尋找系統文件dll中的jmp esp。因為DLL文件有個默認的載入地址,在沒有其他dll占用著位置的時候,dll就會裝入默認的位置,否則就會裝載到隨機的位置,并重新計算函數地址。而操作系統的dll是先于其他應用加載的,因此地址更變得概率不大,成功率高。
- 相同的系統,shellCode才有通用的可能。因為版本不同,dll加載位置可能不同,最終尋找到的jmp命令位置也不同
- 全文的操作前提是,能夠將棧中的數據當成了代碼來進行執行。如果不限制權限,那么就可以利用ret + jmp esp來進行跳轉,達到更改eip的效果,進而執行shellcode
VSole
網絡安全專家