1. 程式人生 > 實用技巧 >簡單緩衝區溢位逆向分析筆記

簡單緩衝區溢位逆向分析筆記

(一)

1.ctrl+x 開啟IDA的交叉引用視窗,檢視當前語句是從哪一個語句跳轉過來的。

call fun() 呼叫函式fun() call語句會先把call的下一條語句入棧,然後jmp到call函式的位置,下條語句的這個地址就稱為返回地址


2.OD

F8 執行下條語句 call語句的話F8是直接執行CALL的內容

F7是跟進去,進入CALL裡看這個CALL 的具體程式碼

F9 執行到斷點位置

棧的話,從棧底到棧頂是高地址到低地址 ,棧中儲存函式額返回地址後,還會儲存父函式的ebp的地址,然後為區域性變數分配空間 (debug格式下分配 空間會大於實際需要的大小)。分配完之後,區域性變數空間的值會全部變成CCCCC,程式的容錯性保持自身的健壯性,使用CCC填充滿這個區域。int 三斷點(INT3斷點是斷點的一種,在諸如Ollydbg中的快捷鍵是F2,是一種很常用的斷點型別。)

strcpy(name,"hello") 先入棧他的右邊的引數,再入棧左邊的引數,

name入棧存的是分配的區域性變數區的首址。


下面用一張圖來說明下: wKiom1eXb7yhAdASAAB28M7Sv-4394.png

我是初學小白,有什麼地方寫錯了還請各位指正~~~持續更新中...........

(二)

1.EIP暫存器裡儲存的是CPU下次要執行的指令的地址。
2.EBP暫存器裡儲存的是是棧的棧底指標,通常叫棧基址,這個是一開始進行fun()函式呼叫之前,由ESP傳遞給EBP的。(在函式呼叫前你可以這麼理解:ESP儲存的是棧頂地址,也是棧底地址。)
3.ESP暫存器裡儲存的是在呼叫函式fun()之後,棧的棧頂。並且始終指向棧頂。 堆疊是一種簡單的資料結構,是一種只允許在其一端進行插入或刪除的線性表。

允許插入或刪除操作的一端稱為棧頂,另一端稱為棧底,對堆疊的插入和刪除操作被稱入棧和出棧。
有一組CPU指令可以實現對程序的記憶體實現堆疊訪問。其中,POP指令實現出棧操作,PUSH指令實現入棧操作。
CPU的ESP暫存器存放當前執行緒的棧頂指標,
EBP暫存器中儲存當前執行緒的棧底指標。
CPU的EIP暫存器存放下一個CPU指令存放的記憶體地址,當CPU執行完當前的指令後,從EIP暫存器中讀取下一條指令的記憶體地址,然後繼續執行。


  1. EIP暫存器裡儲存的是CPU下次要執行的指令的地址。
    也就是呼叫完fun函式後,讓CPU知道應該執行main函式中的printf("函式呼叫結束")語句了。

    2.EBP暫存器裡儲存的是是棧的棧底指標,通常叫棧基址,這個是一開始進行fun()函式呼叫之前,由ESP傳遞給EBP的。(在函式呼叫前你可以這麼理解:ESP儲存的是棧頂地址,也是棧底地址。)
    3.ESP暫存器裡儲存的是在呼叫函式fun()之後,棧的棧頂。並且始終指向棧頂。 堆疊是一種簡單的資料結構,是一種只允許在其一端進行插入或刪除的線性表。
    允許插入或刪除操作的一端稱為棧頂,另一端稱為棧底,對堆疊的插入和刪除操作被稱入棧和出棧。
    有一組CPU指令可以實現對程序的記憶體實現堆疊訪問。其中,POP指令實現出棧操作,PUSH指令實現入棧操作。
    CPU的ESP暫存器存放當前執行緒的棧頂指標,
    EBP暫存器中儲存當前執行緒的棧底指標。
    CPU的EIP暫存器存放下一個CPU指令存放的記憶體地址,當CPU執行完當前的指令後,從EIP暫存器中讀取下一條指令的記憶體地址,然後繼續執行。 esp和ebp區別
    問題: push ebp



(轉)gdb反彙編小結

如果在Linux平臺可以用gdb進行反彙編和除錯。(轉)

2. 最簡C程式碼分析

為簡化問題,來分析一下最簡的c程式碼生成的彙編程式碼:
# vi test1.c

int main()
{
return 0;
}

編譯該程式,產生二進位制檔案:
# gcc test1.c -o test1
# file test1
test1: ELF 32-bit LSB executable 80386 Version 1, dynamically linked, not stripped

test1是一個ELF格式32位小端(Little Endian)的可執行檔案,動態連結並且符號表沒有去除。
這正是Unix/Linux平臺典型的可執行檔案格式。
用mdb反彙編可以觀察生成的彙編程式碼:

# mdb test1
Loading modules: [ libc.so.1 ]
> main::dis ; 反彙編main函式,mdb的命令一般格式為 <地址>::dis
main: pushl %ebp ; ebp暫存器內容壓棧,即儲存main函式的上級呼叫函式的棧基地址
main+1: movl %esp,%ebp ; esp值賦給ebp,設定main函式的棧基址
main+3: subl $8,%esp
main+6: andl $0xf0,%esp
main+9: movl $0,%eax
main+0xe: subl %eax,%esp
main+0x10: movl $0,%eax ; 設定函式返回值0
main+0x15: leave ; 將ebp值賦給esp,pop先前棧內的上級函式棧的基地址給ebp,恢復原棧基址
main+0x16: ret ; main函式返回,回到上級呼叫
>

注:這裡得到的組合語言語法格式與Intel的手冊有很大不同,Unix/Linux採用AT&T彙編格式作為組合語言的語法格式
如果想了解AT&T彙編可以參考文章:Linux AT&T 組合語言開發指南

問題:誰呼叫了 main函式?

在C語言的層面來看,main函式是一個程式的起始入口點,而實際上,ELF可執行檔案的入口點並不是main而是_start。
mdb也可以反彙編_start:

> _start::dis ;從_start 的地址開始反彙編
_start: pushl $0
_start+2: pushl $0
_start+4: movl %esp,%ebp
_start+6: pushl %edx
_start+7: movl $0x80504b0,%eax
_start+0xc: testl %eax,%eax
_start+0xe: je +0xf <_start+0x1d>
_start+0x10: pushl $0x80504b0
_start+0x15: call -0x75 <atexit>
_start+0x1a: addl $4,%esp
_start+0x1d: movl $0x8060710,%eax
_start+0x22: testl %eax,%eax
_start+0x24: je +7 <_start+0x2b>
_start+0x26: call -0x86 <atexit>
_start+0x2b: pushl $0x80506cd
_start+0x30: call -0x90 <atexit>
_start+0x35: movl +8(%ebp),%eax
_start+0x38: leal +0x10(%ebp,%eax,4),%edx
_start+0x3c: movl %edx,0x8060804
_start+0x42: andl $0xf0,%esp
_start+0x45: subl $4,%esp
_start+0x48: pushl %edx
_start+0x49: leal +0xc(%ebp),%edx
_start+0x4c: pushl %edx
_start+0x4d: pushl %eax
_start+0x4e: call +0x152 <_init>
_start+0x53: call -0xa3 <__fpstart>
_start+0x58: call +0xfb <main> ;在這裡呼叫了main函式
_start+0x5d: addl $0xc,%esp
_start+0x60: pushl %eax
_start+0x61: call -0xa1 <exit>
_start+0x66: pushl $0
_start+0x68: movl $1,%eax
_start+0x6d: lcall $7,$0
_start+0x74: hlt
>

問題:為什麼用EAX暫存器儲存函式返回值?
實際上IA32並沒有規定用哪個暫存器來儲存返回值。但如果反彙編Solaris/Linux的二進位制檔案,就會發現,都用EAX儲存函式返回值。
這不是偶然現象,是作業系統的ABI(Application Binary Interface)來決定的。
Solaris/Linux作業系統的ABI就是Sytem V ABI。


概念:SFP (Stack Frame Pointer) 棧框架指標

正確理解SFP必須瞭解:
IA32 的棧的概念
CPU 中32位暫存器ESP/EBP的作用
PUSH/POP 指令是如何影響棧的
CALL/RET/LEAVE 等指令是如何影響棧的

如我們所知:
1)IA32的棧是用來存放臨時資料,而且是LIFO,即後進先出的。棧的增長方向是從高地址向低地址增長,按位元組為單位編址。
2) EBP是棧基址的指標,永遠指向棧底(高地址),ESP是棧指標,永遠指向棧頂(低地址)。
3) PUSH一個long型資料時,以位元組為單位將資料壓入棧,從高到低按位元組依次將資料存入ESP-1、ESP-2、ESP-3、ESP-4的地址單元。
4) POP一個long型資料,過程與PUSH相反,依次將ESP-4、ESP-3、ESP-2、ESP-1從棧內彈出,放入一個32位暫存器。
5) CALL指令用來呼叫一個函式或過程,此時,下一條指令地址會被壓入堆疊,以備返回時能恢復執行下條指令。
6) RET指令用來從一個函式或過程返回,之前CALL儲存的下條指令地址會從棧內彈出到EIP暫存器中,程式轉到CALL之前下條指令處執行
7) ENTER是建立當前函式的棧框架,即相當於以下兩條指令:
pushl %ebp
movl %esp,%ebp
8) LEAVE是釋放當前函式或者過程的棧框架,即相當於以下兩條指令:
movl ebp esp
popl ebp

如果反彙編一個函式,很多時候會在函式進入和返回處,發現有類似如下形式的彙編語句:

pushl %ebp ; ebp暫存器內容壓棧,即儲存main函式的上級呼叫函式的棧基地址
movl %esp,%ebp ; esp值賦給ebp,設定 main函式的棧基址
........... ; 以上兩條指令相當於 enter 0,0
...........
leave ; 將ebp值賦給esp,pop先前棧內的上級函式棧的基地址給ebp,恢復原棧基址
ret ; main函式返回,回到上級呼叫

這些語句就是用來建立和釋放一個函式或者過程的棧框架的。
原來編譯器會自動在函式入口和出口處插入建立和釋放棧框架的語句。
函式被呼叫時:
1) EIP/EBP成為新函式棧的邊界
函式被呼叫時,返回時的EIP首先被壓入堆疊;建立棧框架時,上級函式棧的EBP被壓入堆疊,與EIP一道行成新函式棧框架的邊界
2) EBP成為棧框架指標SFP,用來指示新函式棧的邊界
棧框架建立後,EBP指向的棧的內容就是上一級函式棧的EBP,可以想象,通過EBP就可以把層層呼叫函式的棧都回朔遍歷一遍,偵錯程式就是利用這個特性實現 backtrace功能的
3) ESP總是作為棧指標指向棧頂,用來分配棧空間
棧分配空間給函式區域性變數時的語句通常就是給ESP減去一個常數值,例如,分配一個整型資料就是 ESP-4
4) 函式的引數傳遞和區域性變數訪問可以通過SFP即EBP來實現
由於棧框架指標永遠指向當前函式的棧基地址,引數和區域性變數訪問通常為如下形式:
+8+xx(%ebp) ; 函式入口引數的的訪問
-xx(%ebp) ; 函式區域性變數訪問

假如函式A呼叫函式B,函式B呼叫函式C ,則函式棧框架及呼叫關係如下圖所示:

	+-------------------------+---->高地址
	|EIP(上級函式返回地址)|
	+-------------------------+
+-->|EBP(上級函式的EBP)|--+<------當前函式A的EBP(即SFP框架指標)
|	+-------------------------++-->偏移量A
|	|LocalVariables||
|	|..........|--+<------ESP指向函式A新分配的區域性變數,區域性變數可以通過A的ebp-偏移量A訪問
|f	+-------------------------+
|r	|Argn(函式B的第n個引數)|
|a	+-------------------------+
|m	|Arg.(函式B的第.個引數)|
|e	+-------------------------+
|	|Arg1(函式B的第1個引數)|
|o	+-------------------------+
|f	|Arg0(函式B的第0個引數)|--+<------B函式的引數可以由B的ebp+偏移量B訪問
|	+-------------------------++-->偏移量B
|A	|EIP(A函式的返回地址)||
|	+-------------------------+--+
+---	|EBP(A函式的EBP)|<--+<------當前函式B的EBP(即SFP框架指標)
	+-------------------------+|
	|LocalVariables||
	|..........||<------ESP指向函式B新分配的區域性變數
	+-------------------------+|
	|Argn(函式C的第n個引數)||
	+-------------------------+|
	|Arg.(函式C的第.個引數)||
	+-------------------------++-->frameofB
	|Arg1(函式C的第1個引數)||
	+-------------------------+|
	|Arg0(函式C的第0個引數)||
	+-------------------------+|
	|EIP(B函式的返回地址)||
	+-------------------------+|
+-->|EBP(B函式的EBP)|--+<------當前函式C的EBP(即SFP框架指標)
|+-------------------------+
|	|LocalVariables|
|	|..........|<------ESP指向函式C新分配的區域性變數
|	+-------------------------+---->低地址
frameofC
	
		圖1-1


再分析test1反彙編結果中剩餘部分語句的含義:

# mdb test1
Loading modules: [ libc.so.1 ]
> main::dis ; 反彙編main函式
main: pushl %ebp
main+1: movl %esp,%ebp ; 建立Stack Frame(棧框架)
main+3: subl $8,%esp ; 通過ESP-8來分配8位元組堆疊空間
main+6: andl $0xf0,%esp ; 使棧地址16位元組對齊
main+9: movl $0,%eax ; 無意義
main+0xe: subl %eax,%esp ; 無意義
main+0x10: movl $0,%eax ; 設定main函式返回值
main+0x15: leave ; 撤銷Stack Frame(棧框架)
main+0x16: ret ; main 函式返回
>

以下兩句似乎是沒有意義的,果真是這樣嗎?
movl $0,%eax
subl %eax,%esp

用gcc的O2級優化來重新編譯test1.c:
# gcc -O2 test1.c -o test1
# mdb test1
> main::dis
main: pushl %ebp
main+1: movl %esp,%ebp
main+3: subl $8,%esp
main+6: andl $0xf0,%esp
main+9: xorl %eax,%eax ; 設定main返回值,使用xorl異或指令來使eax為0
main+0xb: leave
main+0xc: ret
>
新的反彙編結果比最初的結果要簡潔一些,果然之前被認為無用的語句被優化掉了,進一步驗證了之前的猜測。
提示:編譯器產生的某些語句可能在程式實際語義上沒有用處,可以用優化選項去掉這些語句。

問題:為什麼用xorl來設定eax的值?
注意到優化後的程式碼中,eax返回值的設定由 movl $0,%eax 變為 xorl %eax,%eax ,這是因為IA32指令中,xorl比movl有更高的執行速度。

概念:Stack aligned 棧對齊
那麼,以下語句到底是和作用呢?
subl $8,%esp
andl $0xf0,%esp ; 通過andl使低4位為0,保證棧地址16位元組對齊

表面來看,這條語句最直接的後果是使ESP的地址後4位為0,即16位元組對齊,那麼為什麼這麼做呢?
原來,IA32 系列CPU的一些指令分別在4、8、16位元組對齊時會有更快的執行速度,因此gcc編譯器為提高生成程式碼在IA32上的執行速度,預設對產生的程式碼進行16位元組對齊

andl $0xf0,%esp 的意義很明顯,那麼 subl $8,%esp 呢,是必須的嗎?
這裡假設在進入main函式之前,棧是16位元組對齊的話,那麼,進入main函式後,EIP和EBP被壓入堆疊後,棧地址最末4位二進位制位必定是1000,esp -8則恰好使後4位地址二進位制位為0000。看來,這也是為保證棧16位元組對齊的。

如果查一下gcc的手冊,就會發現關於棧對齊的引數設定:
-mpreferred-stack-boundary=n ; 希望棧按照2的n次的位元組邊界對齊, n的取值範圍是2-12

預設情況下,n是等於4的,也就是說,預設情況下,gcc是16位元組對齊,以適應IA32大多數指令的要求。

讓我們利用-mpreferred-stack-boundary=2來去除棧對齊指令:

# gcc -mpreferred-stack-boundary=2 test1.c -o test1

> main::dis
main: pushl %ebp
main+1: movl %esp,%ebp
main+3: movl $0,%eax
main+8: leave
main+9: ret
>

可以看到,棧對齊指令沒有了,因為,IA32的棧本身就是4位元組對齊的,不需要用額外指令進行對齊。
那麼,棧框架指標SFP是不是必須的呢?
# gcc -mpreferred-stack-boundary=2 -fomit-frame-pointer test1.c -o test
> main::dis
main: movl $0,%eax
main+5: ret
>

由此可知,-fomit-frame-pointer 可以去除SFP。

問題:去除SFP後有什麼缺點呢?

1)增加調式難度
由於SFP在偵錯程式backtrace的指令中被使用到,因此沒有SFP該除錯指令就無法使用。
2)降低彙編程式碼可讀性
函式引數和區域性變數的訪問,在沒有ebp的情況下,都只能通過+xx(esp)的方式訪問,而很難區分兩種方式,降低了程式的可讀性。

問題:去除SFP有什麼優點呢?

1)節省棧空間
2)減少建立和撤銷棧框架的指令後,簡化了程式碼
3)使ebp空閒出來,使之作為通用暫存器使用,增加通用暫存器的數量
4)以上3點使得程式執行速度更快

概念:Calling Convention 呼叫約定和 ABI (Application Binary Interface) 應用程式二進位制介面

函式如何找到它的引數?
函式如何返回結果?
函式在哪裡存放區域性變數?
那一個硬體暫存器是起始空間?
那一個硬體暫存器必須預先保留?

Calling Convention 呼叫約定對以上問題作出了規定。Calling Convention也是ABI的一部分。
因此,遵守相同ABI規範的作業系統,使其相互間實現二進位制程式碼的互操作成為了可能。
例如:由於Solaris、Linux都遵守System V的ABI,Solaris 10就提供了直接執行Linux二進位制程式的功能。
詳見文章:
關注: Solaris 10的10大新變化

3. 小結
本文通過最簡的C程式,引入以下概念:
SFP 棧框架指標
Stack aligned 棧對齊
Calling Convention 呼叫約定 和 ABI (Application Binary Interface) 應用程式二進位制介面
今後,將通過進一步的實驗,來深入瞭解這些概念。通過掌握這些概念,使在彙編級除錯程式產生的core dump、掌握C語言高階除錯技巧成為了可能。


轉載於:https://blog.51cto.com/aurorasec/1830429