推薦一本書《C++反彙編與逆向分析技術揭祕》
業精於勤而荒於嬉,行成於思而毀於隨。
3.程序級的機器表示
計算機執行機器代碼,用字節序列編碼低級的操作
編譯器基於編程語言的規則、目標機器的指令集和操作系統遵循的慣例
經過一系列的階段生成機器代碼
GCC C語言編譯器以彙編代碼的形式產生輸出,彙編代碼是機器代碼的文本表示
然後GCC調用匯編器和鏈接器,根據彙編代碼生產可執行的機器代碼
高級語言(如Java語言 C語言 等),機器屏蔽了程序的細節,即機器級的實現
與此相反,早期程序員用匯編代碼編程時,必須指定程序用來執行計算的低級指令
高級語言提供的抽象級別比較高,在這種抽象級別上工作效率會更高,也更靠譜
用高級語言編寫的程序可以在很多不同的機器上編譯和執行
而彙編代碼則是與特定的機器密切相關
對程序員來說,能夠閱讀和理解會變代碼是一項重要的技能
能理解編譯器的優化能力,分析代碼中隱含的低效率
許多攻擊利用了系統程序中的漏洞重寫信息,瞭解這些漏洞如何出現,如何防禦
需要具備程序的機器表示的知識
源代碼與對應的彙編代碼的關係通常不太容易理解,這是一種逆向工程(reverse engineering)
通過研究系統和逆向工作,來試圖瞭解系統的穿件過程
32位機器只能使用大概4GB的隨機訪問處理器
64位機器能夠使用躲達256TB的內存空間,很容易就能擴展至16EB
3.1歷史觀點
Intel處理器系列,俗稱x86,經歷了一個長期,不斷變化的發展過程
8086,是一個重要的開始
3.2程序的編碼
GCC C編譯器,是Linux上面的默認編譯器
-Og告訴編譯器使用會生成符合原始C代碼整體結構的機器代碼的優化等級
使用較高級別優化產生的代碼會嚴重變形
實際上GCC命令調用了一整套的程序,將源代碼轉換成可執行代碼
首先,C預處理器擴展源代碼,插入所有用#include命令指定的文件
並擴展所有用#define聲明指定的宏
其次,編譯器產生兩個源文件的彙編代碼,p1.s p2.s
接下來,彙編器會將彙編代碼轉換成二進制目標代碼文件p1.o p2.o
目標代碼是機器代碼的一種形式,它包含所有指令的二進制表示,但還沒有填入全局值地址
最後,鏈接器將兩個目標文件與實現庫函數(如printf函數)的代碼合併
並最終生成可執行文件p (有命令行指示符 -o p 指定的)
展示一下Hello World 中間生成文件
環境:
.i 文件:
.s 文件:
.o 文件:
用objdump來查看
3.2.1機器級代碼
計算機使用了多種不同形式的抽象,利用更簡單的抽象模型來隱藏實現的細節
對於機器編程來說,其中兩種抽象非常重要
第一種是由指令集體系結構 或 指令集結構(ISA),來定義機器級程序的格式和行爲
它定義了處理器狀態,指令的格式,以及每條指令對狀態的影響
第二種抽象是,機器級程序使用的內存地址是虛擬地址,提供的內存模型是看上去非常大的字節數組
在整個編譯過程中,編譯器會完成大部分工作,將用C語言提供的相對比較抽象的執行模型表示的程序轉化成處理器執行的非常基本的指令
彙編代碼表示,非常接近機器代碼,與機器代碼的二進制格式相比
彙編代碼的主要特點是它用可讀性更好的文本格式表示
能夠理解彙編代碼以及它與原始C代碼的聯繫,是理解計算機如何執行程序的關鍵一步
X86-64 的機器代碼和原始的C代碼差別非常大,一些通常對C語言程序員隱藏的處理器狀態都是可見的:
程序計數器(通常稱爲 PC,在x86-64 中用%rip表示),給出將要執行的下一條指令在內存中的地址
程序內容包括:程序的可執行機器代碼,操作系統需要的一些信息,用來管理過程調用和返回運行時棧,以及用戶分配的內存塊
一條機器指令只執行一個非常基本的操作
如,存放在寄存器中的兩個數字相加,在存儲器和寄存器之間傳送數據
編譯器必須產生這些指令的序列,從而實現程序結構
3.2.2代碼示例
查看一下這個函數的彙編表示形式
環境: 爲了可觀,這裏我們用windows
Windows10 1511, vs2017
每句C語言都對應一些彙編語言,彙編指令的左邊顯示了機器代碼,16進制
這就是機器代碼和彙編代碼還有C代碼的對應關係
關於機器代碼和它的反彙編表示的特性值得注意:
假設文件demo.c有下面這樣的函數:
還有一個 mstore.c
生成可執行文件 demo
然後用objdump –d demo 查看反彙編,這裏我們保存爲demo.txt,打開查看
3.2.3關於格式註解
GCC 產生的彙編代碼不客觀,包含一些不需要關心的信息,它不提供任何程序的描述或它是如何工作的描述,例如利用如下代碼生成文件 mstore.s
Gcc –Og –S mstore.c
Mstore.s的完整內容如下:
所有以 . 開頭的行都是指導彙編器和鏈接器工作的僞指令
Intel彙編代碼格式:
3.3數據格式
由於是從16爲體系結構擴展成32位的,Intel用術語“字”表示16位數據類型
因此,稱32位數爲“雙字”(double words),稱64位爲“四字”(quad words)
3.4訪問信息
一個x86-64的中央處理單元包含一組16個存儲64位值的通用目的寄存器
用來存儲整數數據的指針
它們的名字都以%r開頭,後面還跟着一些不同的命名規則的名字
這是由於指令集歷史演化造成的
最開始8086中有8個16位的寄存器 %ax 到 %sp
擴展到IA32 架構時,這些寄存器也就擴展成32位寄存器, %eax 到 %ebp
擴展到x86-64後,原來8個寄存器擴展成64位, %rax 到 %rbp
還增加了9個新的寄存器,%r8 到 %r15
3.4.1操作數指示符
大多數指令有一個或多個操作數(operand),指示出執行一個操作中要使用的源數據值,以及放置結果的目的位置
指令編碼結構,參考英特爾開發手冊
X86-64支持多種操作數格式
源數據值可以以常數形式給出,或是從寄存器或內存中讀出
結果可以存放在寄存器或內存中
因此,各種不同的操作數的可能性被分爲三種類型:
第一種類型是立即數(immediate),用來表示常數值,在ATT格式的彙編代碼中書寫形式是‘$’後面跟一個用標準C表示法表示的整數,比如, $-577 $0x1F
不同指令允許的立即數範圍不同
第二種類型是寄存器(register),它表示某個寄存器的內容,參考上圖
第三類操作數是內存引用,它會根據計算出來的地址(有效地址)訪問某個內存地址,參考上圖
有多種不同的尋址模式,允許不同形式的內存引用,參考上圖
3.4.2數據傳送指令
Mov指令使用規則,參考英特爾開發手冊
Intel彙編操作數方向:Mov 目標操作數,源操作數
ATT彙編操作數方向相反
源操作數指定的值是一個立即數,存儲在寄存器或內存中
目標操作數指定一個位置,要麼是寄存器,要麼是一個內存地址
傳輸指令的兩個操作數都不能同時爲內存
兩個操作數的大小必須相同
3.4.3數據傳送示例
可看到生成的彙編,兩個賦值都是有3條彙編指令,中間藉助寄存器,而不是直接內存到內存
C語言中,所謂的指針,隨便你把它看作什麼,就是一串數據
(如果你能把指針玩得非常熟練的情況下)
3.4.4壓入和彈出棧數據
Push指令,把數據壓入到棧上,而pop指令是彈出數據
這些指令都只有一個操作數—壓入的數據源和彈出的數據目的
Push rax 等價於 sub rsp,8 mov [rsp],rax 先執行前面的,然後執行後面的
Pop rax 等價於 mov rax,[rsp] add rsp,8 同上
根據棧是從高地址向低地址擴展,push 和 pop 得按圖上這樣的順序壓入 彈出
3.5算數和邏輯操作
3.5.1加載有效地址
加載有效地址實際上是mov指令的變形
例如,lea eax,dword ptr ds:[xxx]
取的不是XXX內存地址的值,而是XXX的內存地址
3.5.5特殊的算術操作
條件碼
它們描述了最近的算數或邏輯操作的熟悉,可以檢測這些寄存器來執行條件分支指令
LEA 指令不改變任何條件碼,是用來進行地址計算的
訪問條件碼
條件碼通常不會直接讀取,常用的使用方法有三種
1.可以根據條件碼的某種組合將一個字節設置爲0或者1
2.可以條件跳轉到程序的某個其他部分
3.可以有條件地傳送數據
跳轉指令
關於分支
一個簡單的分支邏輯:
環境:windows10 1511, vs2017 Debug x86
//彙編代碼
//程序流程圖
各種判斷
JGE(大於或等於)判斷,判斷依據:SF OF 同時爲0 或 1
JLE(小於或等於)判斷,判斷依據:ZF == 1 | SF != OF
JNE(如果不等於)判斷,判斷依據:ZF == 0
JE(如果等於)判斷,判斷依據: ZF == 1
對於這些,我是死記硬背的
有一個結論,反彙編中的判斷和C語言的判斷,是相反的
多重IF…ELSE
環境:windows10 1511, vs2017 Debug x86
//彙編代碼
屏幕太小,不好截圖,諒解
//程序流程圖
紅色的線爲不成立,綠色的線爲成了,藍色的線爲每個分支執行完後跳轉到的地址
循環
While循環
//彙編代碼
//程序流程圖
紅色線爲不成立,綠色線爲成立,藍色線爲執行完後跳往哪,後邊就不重複了
可以看到,程序執行下來首先判斷,如果判斷不成立(反彙編層),就執行循環體
循環體執行完後又返回剛下來判斷的地方,以此類推
For循環
//彙編代碼
//程序流程圖
可以看到,程序下來就直接JMP到判斷的地方,如果不成立(紅線)就執行循環體
循環體執行完後(藍線)就跳轉到讓NUM局部變量自增1,隨後又跳轉到判斷的地方
以此類推
Do…while 循環
//彙編代碼
//程序流程圖
Do…while循環,不管判斷成不成立,至少會循環一次,由圖可見
JL 如果小於,SF != OF
Switch語句
首先看下小於4個的情況下
//彙編代碼
//程序流程圖
和IF…ELSE相比,可以看出,switch結構將所有條件跳轉都放置在一起
而IF…ELSE會在條件跳轉後邊緊跟着語句塊
由圖可以看出,Break;語句就直接跳轉到了結束處
下面來看下超過4個且連續的情況
//彙編代碼
屏幕分辨率小,無法展示
//程序流程圖
可以看出,生成了一張表
程序下來,用變量x和十進制10相減,得到的一個數值其實是這張表的索引
如果索引大於4,就跳轉到結尾處
Index * 4 + 表的首地址 , 就是該跳往的地址
注意我的編譯環境是: windows10 1511, vs2017 x86
過程
可理解爲函數
運行時棧
三種調用約定:
C/C++默認是 _cdecl
__stdcall 從右至左的順序將參數壓棧, 調用者平衡棧
__cdecl 從右至左的順序將參數壓棧,調用者平衡棧
__fastcall 使用ECX和EDX寄存器傳送前兩個DWORD或更小的參數,其他參數依次從右至左入棧,被調用者平衡棧
環境: windows7 64 vc6.0
//彙編代碼
//從MAIN開始,一行一行跟一下就行了
關於棧,後進先出,有借有還-再借不難
轉移控制
CALL 0x12345678
可以看成
Push 下一條指令的地址
Mov eip, 0x12345678 當然這樣式不能修改EIP的,但是相當於這樣
RET
Mov EIP,當前ESP的值
ESP 再加4
如果後邊跟操作數,那就讓esp+4+操作數
數組
環境: xp vc6
只是內存上一串連續的數據,定義的類型不同,分配的空間也就不同
結構體
詳情看書吧
關於緩衝區溢出
測試環境: XP vc6.0
當執行完arr[4] = (int)test 發現原來的返回地址被修改了
關於緩衝區溢出簡單的演示一遍,詳情看書吧
浮點代碼
…