本文基於win-xp-SP3,實際內存地址與環境有差異,需按實際調試結果爲準。
基礎概念
VA = Image Base + RVA
- File Offset:文件偏移,PE文件中數據相對於文件開頭的偏移。
- Image Base:裝載地址,PE裝入內存的地址。默認EXE基地址是0x00400000,DLL是0x10000000。
- VA:Virtual Address,虛擬內存地址。PE中的指令被裝入內存後的地址。
- RVA:Relative Virtual Address,相對虛擬地址。內存地址相對於映射基地址的偏移量。
PE文件是按磁盤數據標準存放,以0x200字節(512)爲基本單位組織。當數據節不足0x200字節時,不足的地方被0x00填充,超過0x200,下一個0x200塊將分配給這個節。
裝入內存後,按內存標準存放,並以0x1000字節(4096)爲單位組織。
節(section) | 相對虛擬地址RVA | 文件偏移量 |
---|---|---|
.text | 0x00001000 | 0x0400 |
.data | 0x00009000 | 0x7400 |
這種差異引起的節基址差稱爲節偏移:
.text節偏移 = 0x1000 - 0x400 = 0xc00
.data節偏移 = 0x9000 - 0x7400 = 0x1C00
調試時基於內存地址,要想確認對應的文件偏移,公式如下:
文件偏移地址 = RVA - 節偏移
LordPE可以查看PE文件的節信息,確認節的虛擬地址、裝載地址、文件偏移,就可以確認其中一個內存指令對應的文件地址了。
棧幀介紹
下面以一段代碼示例來說明函數調用時的棧幀情況。
- PUSH:爲棧增加一個元素
- POP:從棧中取出一個元素
- ESP:該寄存器指向棧幀的棧頂
- EBP:該寄存器指向棧幀的底部
- EIP:指令寄存器,指向下一條等待執行的指令地址。
int func_B(int arg_B1, int arg_B2)
{
int var_B1, var_B2;
var_B1=arg_B1+arg_B2;
var_B2=arg_B1-arg_B2;
return var_B1*var_B2;
}
int func_A(int arg_A1, int arg_A2)
{
int var_A;
var_A = func_B(arg_A1,arg_A2) + arg_A1 ;
return var_A;
}
int main(int argc, char **argv, char **envp)
{
int var_main;
var_main=func_A(4,3);
return var_main;
}
棧從高地址往低地址發展,堆從低地址往高地址發展。
VC6.0默認使用__stacall調用方式:參數入棧從右到左,函數返回時堆棧平衡操作在子函數中進行。
函數調用步驟:
- 參數入棧:將參數從右向左依次壓入棧中
- 返回地址入棧:將當前調用指令的下一條指令地址入棧,供函數返回時繼續執行。
- 跳轉:處理器從當前代碼區跳轉到被調用函數的入口
- 棧幀調整:保存當前棧幀狀態值,EBP入棧;切換到新棧幀,ESP值裝入EBP;給新棧分配空間,把ESP減去所需空間,擡高棧頂。
例如函數A調用函數B:
;A調用前,函數B有3個參數
push 參數 3
push 參數 2
push 參數 1
call 函數B地址;call 指令將同時完成兩項工作: a)向棧中壓入當前指令的下一條在內存中的位置,即保存返回地址。
; b)跳轉到所調用函數的入口地址函數入口處
......
;函數B入口代碼
push ebp ;保存舊棧幀的底部
mov ebp, esp ;設置新棧幀的底部(棧幀切換)
sub esp, xxx ;設置新棧幀的頂部(擡高棧頂,爲新棧幀開闢空間)
函數返回步驟:
- 保存返回值:通常將函數的返回值保存在EAX中
- 彈出當前棧幀,恢復上一個棧幀:給ESP加上棧幀的大小,降低棧頂,回收棧空間;棧上保存的EBP值彈入EBP寄存器,恢復上一個棧幀;將返回地址彈給EIP寄存器。
add esp, xxx ;降低棧頂,回收當前的棧幀
pop ebp;將上一個棧幀底部位置恢復到 ebp,
retn;這條指令有兩個功能: a)彈出當前棧頂元素,即彈出棧幀中的返回地址。至此,棧幀恢復工作完成。
;b)讓處理器跳轉到彈出的返回地址,恢復調用前的代碼區
下圖需要牢記心中,返回地址與漏洞利用有密切關係。
實踐:利用棧溢出漏洞,彈出MessageBox
使用如下代碼演示利用過程,從password.txt讀取內容拷貝到buffer[44]模擬棧溢出。
使用VC6.0編譯,默認編譯選項,編譯成debug版本。在password.txt中植入二進制機器碼,運行後彈出一個消息框顯示"hacktest"。
步驟:
(1)分析調試漏洞程序,獲得淹沒返回地址的偏移
(2)獲得buffer的起始地址,並將其寫入password.txt相應的偏移處,用來沖刷返回地址
(3)向password.txt寫入可執行的機器碼,用來調用API彈出一個消息框
#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);//over flowed here!
return authenticated;
}
main()
{
int valid_flag=0;
char password[1024];
FILE * fp;
LoadLibrary("user32.dll");//prepare for messagebox
if(!(fp=fopen("password.txt","rw+")))
{
exit(0);
}
fscanf(fp,"%s",password);
valid_flag = verify_password(password);
if(valid_flag)
{
printf("incorrect password!\n");
}
else
{
printf("Congratulation! You have passed the verification!\n");
}
fclose(fp);
}
1.分析調試漏洞程序,獲得淹沒返回地址的偏移
如果在 password.txt 中寫入恰好 44 個字符,那麼第 45 個隱藏的截斷符 null 將沖掉authenticated 低字節中的 1,從而突破密碼驗證的限制。
出於字節對齊、容易辨認的目的,我們把“ 4321”作爲一個輸入單元。buffer[44]共需要 11 個這樣的單元。
第 12 個輸入單元將 authenticated 覆蓋;第 13 個輸入單元將前棧幀 EBP 值覆蓋;第 14 個
輸入單元將返回地址覆蓋。
首先在password.txt寫入11組“4321”,使用OllyDbg加載,通過View-->source file定位到strcpy(buffer,password);下斷點,單步調試觀察棧內容。
EBP:0x0012FB20位置的上方是"4321"和authenticated ,下方是返回地址。buffer[44]的內存地址是0x0012FAF0。53~56個字符的ASCII碼值將寫入棧幀的返回地址,成爲函數返回後執行的指令地址。
也就是說將buffer[44]的起始地址0x0012FAF0寫入password.txt文件中的第53~56個字節,verify_password 函數返回時會跳轉到我們輸入的字串開始執行。
2.獲得buffer的起始地址,並將其寫入password.txt相應的偏移處,用來沖刷返回地址
調用MessageBoxA(NULL, "hacktest", "hacktest", NULL)即可彈出一個對話框,MessageBoxA在user32.dll中,使用Dependency Walker可確認其入口地址爲0x77D507EA。
調用MessageBoxA彙編指令對應機器碼如下:
首先在棧上創建"hacktest"字符串,然後壓入參數,最後CALL對應函數。
PUSH 74657374在內存中由低-->高:74 65 73 74
PUSH 0x44332211在內存中由低-->高:11 22 33 44
最後使用010 Editor編輯Hex碼:
首先將上述調用MessageBoxA的機器碼寫入,第53~56字節覆蓋返回地址爲buffer起始地址0x0012FAF0,其餘字節用nop 0x90填充。
3.向password.txt寫入可執行的機器碼,用來調用API彈出一個消息框
運行程序,成功彈出"hacktest"消息框。
參考
王清《0day安全:軟件漏洞分析技術》