ELF64檔案逆向分析知識—[1]64位逆向基礎知識
第一次用IDA開啟ELF64可執行檔案進行分析,有種剛學逆向的錯覺,各種之前不認識的暫存器,函式呼叫完全找不到函式引數是怎麼傳遞的。看來有必要惡補一下x64位CPU的和逆向分析相關的知識。
首先先來科普一下64位CPU的混亂的術語:
術語 說明
AMD64 AMD研製的64位CPU(直接向下相容x86).
EM64T Intel研製的相容AMD64的CPU.
Intel64 EM64T的新名字.
IA-64 Intel和HP合作開發的64位CPU(可以通過模擬器間接相容x86).
x86 Intel的IA-32、IA-16、IA-8系列的CPU.
x64 AMD64&Intel64.
接下來就讓我們一起看下x64中新增或變更的知識點(這裡只介紹和逆向分析相關的,更詳細內容可以參考Intel使用者手冊)。
64位
記憶體地址變成64位,當然程式使用的指標也相應的程式設計64位的指標。所以含有絕對地址(VA)的指令大小比原來增加了4位元組。同樣,暫存器的大小和棧的基本單元也變成64位。
記憶體
虛擬記憶體的實際大小為16TB(核心和使用者空間各佔8TB)。
通用暫存器
大小擴大到64(8位元組),個數增加到18個(新增了R8~R15暫存器)。x64系統下的所有通用暫存器的名稱均以字母”R”開頭(x86以字母”E”開頭),為了向下相容,支援訪問暫存器的8位、16位、32位(eg:AL、AX、EAX)。
**Note:**64位本地模式中不使用段暫存器:CS、DS、ES、SS、FS、GS,他們僅用於向下相容32位程式。
CALL/JMP指令
仍延續x86相同的指令,eg,CALL XXXXXXXX –> FF15XXXXXXXX,其中XXXXXXXX“絕對地址”指向IAT區域的某個位置。但對地址解析方法不同了。具體解析方式暫時先不深入。
函式呼叫約定
Windows平臺
整數和浮點數引數
32位:cdecl、stdcall、fastcall等幾種,但64位統一為一種變形的fastcall。64位的fastcall中最多可以把函式的4個引數儲存到暫存器中傳遞:
引數 整數型 浮點數型
1st RCX XMM0
2st RDX XMM1
3st R8 XMM2
4st R9 XMM2
超過4個引數,使用棧來傳遞,傳遞順序依照“從右向左”。此外,函式返回時傳遞的引數過程中所用的棧由呼叫者清理。看上去64位的fastcall就像32位下的cdcel和fastcall的結合。函式的前4個引數雖然使用暫存器傳遞,但在棧中仍為這4個引數預留了空間(32個位元組)。
Note:當整數和浮點數引數混合出現時,eg:
void func(float a, int b, double c, int d);
a放入XMM0中,b放入RDX,c放入XMM2,d放入R9。
這個存放的順序很怪異,其實這是嚴格按照表2.1中整數和浮點數的4個引數一一對應,需要為沒用的引數預留空間,eg,c引數沒有放到XMM1中,XMM1被預留位置了。
指標引數
指標引數的傳遞遵循整數引數傳遞的方式。
結構體引數
結構體引數比較特殊,如果結構體長度小於64bit,則使用整數引數的傳遞規則。但如果是一個很大的結構體,那麼應該還是要在堆疊中申請臨時空間的(但ddk沒有明說這一點,參考x86的規則應該如此)。
未宣告函式呼叫
func1();
func2()
{
func1(2, 1.0, 7);
}
在這種情況下,func1()的引數表其實不明確,那麼引數的傳遞要怎樣進行?這裡採用了一個比較保守的規則,就是:整數引數還是按照暫存器對映關係放入對應的暫存器中,浮點數在按照對映關係放入XMM暫存器後,還需要按照整數引數的暫存器對映關係放入整數暫存器中一次,這就是“比較保守的規則”的意思。就現在這個例子而言,結果如下:
2在RCX中,1.0在RDX和XMM1中,7在R8中。
1
Linux平臺
和Windows平臺的編譯器一樣,在Linux下的GCC編譯器編譯的函式,預設也是採用fastcall呼叫約定,但引數傳遞的方式卻和Windows平臺下截然不同,最多會把8個引數儲存到暫存器中傳遞:
引數 整數型 浮點數型
1st RDI XMM0
2st RSI XMM1
3st RDX XMM2
4st RCX XMM3
5st R8 XMM4
6st R9 XMM5
7st 棧 XMM6
8st 棧 XMM7
這裡當引數都是整數時大於6個引數時使用棧傳遞,浮點數型引數大於8個時,使用棧傳遞,傳遞順序依照“從右向左”。
Note:
當整數和浮點數引數混合出現時,eg:
void func(float a, int b, double c, int d);
a放在XMM0,b放在RDI,c放在XMM1,d放在RSI。
可以看到Linux的GCC下正浮混合時沒有為沒用到得引數預留空間。
RAX、RCX、RDX、RSI、RDI、R8、R9都是易失暫存器(它們的值經常被改變),所以被呼叫函式不必恢復它們的值,可以看到上述幾個暫存器大多被用於函式傳參,值被修改了也無妨。其他的暫存器是非易失暫存器(RBX、RBP、RSP、R10~R15),通常在函式中使用時需要儲存原值,並在函式返回時恢復它們的值。
從Windows和Linux的64位函式呼叫約定不同可以總結出兩點:
隨著x64位引入更多的暫存器後,傳遞引數也相應的更多使用暫存器。也可以說有錢更任性。
兩個平臺的程式碼移植問題很不樂觀。想了一下好像和逆向分析無關(偷著樂!!)。
棧&棧幀
Windows平臺
64位的作業系統中使用的棧與棧幀方式也發生了變化。簡言之,棧的大小比函式實際需要的大小要大很多。呼叫子函式時不在使用PUSH命令來傳遞引數,而是通過MOV指令操作暫存器與預定的棧(fastcall)來傳遞。使用VC++建立的x64程式程式碼幾乎看不到PUSH/POP指令(替換成MOV了,我暈!!)。
建立棧幀時也不再使用RBP暫存器,而是直接使用RSP暫存器來實現(這點讓我們分析棧幀更加困難,幸虧還有IDA)。之前可以在32位下的編譯器選項中開啟優化功能。
讓我們看下具體的變化:
看不到以前的棧幀的身影,現在上來就:
sub rsp,48h
...
add rsp,48h
Ret
1
2
3
4
5
在函式中基本上看不到PUSH/POP指令了,即使是呼叫子函式時超出4個引數部分也是使用MOV向棧中存入資料(好彆扭!)
多餘4個引數的函式,超出部分的引數使用棧傳遞,但設定順序那叫一個亂啊,根本不是按順序來的。且從第五個引數發現並不是存在當前的棧頂,而是[RSP+20h],也就是前面說的預留的32個位元組。
Linux平臺
由於Linux平臺逆向分析很少,能總結的也不多。由於Linux平臺下的GCC比Windows平臺的VC使用更多的暫存器(達到6~8個),所以很少看到使用棧傳遞的情況。這裡以一個玩具程式碼為例(GCC沒有開啟優化):
int func(int a , int b , int c , int d , int e , int f , int g , int h , int i)
{
return a + b + c + d + e + f + g + h + i;
}
int main(int argc, char* argv[])
{
int ret;
ret = func(1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9);
printf("ret : %d" , ret);
return 0;
}
我們再來看下,反彙編後:
可以看到多餘6個引數使用MOV指令向棧記憶體[RSP+XXXh]位置壓入引數,並且看到引數的傳遞順序,和Windows一樣木有PUSH指令壓入引數了。接下來再來看一下棧幀:
這裡我編譯時使用了“-g”選項,同時保留了符號表和重定位資訊(沒有使用“-s”),這裡很尷尬IDA顯示的func函式的呼叫約定是cdecl,可能64位的呼叫約定很像32位的cdecl和fastcall的結合體,所以IDA顯示為cdecl。也可以看到清晰的棧幀結構:
但有時,可以看到就不那麼幸運的看到清晰的棧幀了,比如下面的反彙編程式碼:
這裡我去掉“-g”選項並使用“-s”選項,編譯後:
已經找不到func的身影,我們使用外掛反編譯一下:
現,反編譯外掛顯示的是fastcall呼叫約定。這可能是IDA還沒能跟上編譯器的變化,無法做出正確的呼叫約定匹配。
惡補完x64位的知識後,現在回過頭再看下IDA中的彙編程式碼,順眼很多了。
接下來,利用IDA的靜態庫SIG更進一步增加反彙編的可閱讀行。
---------------------
作者:杏林小軒
來源:CSDN
原文:https://blog.csdn.net/txx_683/article/details/53454307
版權宣告:本文為博主原創文章,轉載請附上博文連結!