幾年前,我參加了40小時的逆向工程基礎課程。在教我們使用IDAPro的過程中,講師相當迅速地演示瞭如何將ASM中的某些變量標記為結構的成員,基本上等同於C /中的老式 struct
C ++,並在代碼的其餘部分中將它們視為可見的地方。這對我來說似乎很有用。
但是,他沒有介紹的是如何識別結構。您如何知道一組變量實際上是一個結構而不只是一組相關變量?您如何確定作者在那裡使用了 struct
(或類似名稱)?
幾年前,我參加了40小時的逆向工程基礎課程。在教我們使用IDAPro的過程中,講師相當迅速地演示瞭如何將ASM中的某些變量標記為結構的成員,基本上等同於C /中的老式 struct
C ++,並在代碼的其餘部分中將它們視為可見的地方。這對我來說似乎很有用。
但是,他沒有介紹的是如何識別結構。您如何知道一組變量實際上是一個結構而不只是一組相關變量?您如何確定作者在那裡使用了 struct
(或類似名稱)?
不能。在C中,為C程序的讀者提供了結構,它們在程序映像中的使用是可選的。完全有可能在原始程序中,一個瘋狂的混蛋決定使用大小合適的char *緩衝區來做所有事情,並適當地進行強制轉換和添加,而您永遠不會知道其中的區別。
“結構”標籤完全是為了您作為代碼查看器的利益。很有可能是您應用於程序的結構標籤實際上是兩個始終彼此相鄰存儲的變量。只要這不會導致您對程序執行錯誤的結論,這將無關緊要。
在代碼中可以找到表示結構用法的非常常見的模式。
如果您的指針在某個非零偏移量處被取消引用,您可能正在處理結構。尋找類似這樣的模式:
mov eax,[ebp-8];將局部變量加載到eaxmov ecx [eax + 8]中; **在eax + 8處取消對雙字的引用**
在此示例中,我們有一個包含指針的變量,但是我們關心內存內容在某個特定偏移量指針之前。這正是使用結構的方式:我們得到一個指向該結構的指針,然後取消對該指針的引用以及一些偏移量以訪問特定成員。在C語言中,其語法為: pMyStruct->member_at_offset_8
。
旁注:不要混淆在某些變量的偏移量處的引用與在位置處的引用堆棧指針或幀指針的偏移量( esp
或 ebp
)。當然,您可以將局部變量和函數參數視為一個大結構,但是在C語言中,它們並未明確定義為此類。
實際上,您不需要取消引用即可檢測結構成員。例如:
mov eax,[ebp-8];將局部變量加載到eaxpush 30h中; num = 30hpush aSampleString; src =“樣本字符串”添加eax,0Chpush eax; dst = eax + 0xCcall strncpy
在此示例中,我們將最多0x30個字符從某個源字符串複製到 eax + 0xC
(請參閱 strncpy)。這告訴我們 eax
可能指向一個偏移量為0xC的字符串緩衝區(至少0x30字節)的結構。例如,該結構可能類似於:
struct _MYSTRUCT {DWORD a; // + 0x0 DWORD b; // + 0x4 DWORD c; // + 0x8 CHAR d [0x30]; // + 0xC ...}
在這種情況下,示例代碼如下:
strncpy(&pMyStruct->d,“示例字符串”,sizeof(pMyStruct->d));
> 旁注::可能(儘管不太可能)我們可以將文件複製到偏移量為+ 0xC的大字符串緩衝區中,但是您可以通過上下文確定這一點。例如,如果offset + 0x8是一個整數,那麼它絕對是一個結構。但是,如果我們將固定長度為0xC的字符串複製到地址 eax
,然後將另一個字符串複製到地址 eax + 0xC
,則可能是一個巨大的字符串。
比方說,您有一個結構體( not 指向該結構體的指針)作為堆棧的局部變量。大多數時候,IDA不知道堆棧上的結構或一堆單獨的局部變量之間的區別。但是,要處理結構的一個巨大提示是,如果您僅從變量中讀取而不寫入數據,或者(如果不是這樣,則)僅在不讀取變量的情況下寫入變量。這是每個示例:
lea eax,[ebp + var_58];將局部變量的地址加載到eaxpush eaxcall some_functionmov eax,[ebp + var_54]中;假設我們從未接觸過var_54,而是... test eax,eax; ...但是我們正在檢查它的值!jz在某處...
在此示例中,我們從 var_54
中讀取數據,而沒有向其寫入任何內容(在此函數內)。 可能表示它是從其他函數調用訪問的結構的成員。在此示例中,暗示 var_58
可能是該結構的開始,因為其地址被推入 some_function
的參數中。您可以通過遵循 some_function
的邏輯並檢查其參數是否已在偏移量+ 0x4處取消引用(並修改)來驗證這一點。當然,這不一定必須在 some_function
中發生-它可以在其子功能之一或其子功能之一中發生,等等。
存在用於編寫的類似示例:
xor eax,eaxmov [ebp + var_28],eax;假設這是var_28被觸摸的唯一時間,[ebp + var_30]按下eaxcall some_other_function ...
當您看到設置了局部變量然後再也沒有引用它時,您不能僅僅忘記它們,因為它們很可能是傳遞給另一個函數的結構的成員。此示例暗示在將結構的地址傳遞給 some_other_function
之前,將結構(從 var_30
開始)寫入偏移量+ 0x8。
這兩個用C編寫的示例都可能像這樣:
some_function(&myStruct); if(myStruct.member_at_offset_4)...
and
myStruct.member_at_offset_8 = 0; some_other_function(&myStruct);
側面說明:儘管這些示例中的每個示例都使用局部變量,但相同邏輯適用於全局變量。
這可能很明顯,IDA幾乎總是在為您處理此問題,但是,知道代碼中何時具有結構的一種簡單方法是,如果調用需要某種結構的文檔化函數。例如, CreateProcessW
需要一個指向 STARTUPINFOW
結構的指針。
我要說的最後一點是在所有這些情況下,從技術上講,是的,程序的作者可以編寫其代碼而無需使用結構。他們還可以通過將每個函數定義為帶有大內聯 __ asm
的 __ declspec(naked)
來編寫其代碼。您將永遠無法分辨。但是可以說,這並不重要。如果有邏輯值組連續存儲在內存中並從一個函數傳遞到另一個函數,則將它們註釋為結構仍然有意義。幾乎所有時候,這都是作者編寫代碼的方式。
如果您需要我詳細介紹任何內容,請告訴我。
查找結構很棘手,但是可以幫助您更好地理解代碼。正如安德魯所說,結構只是C的抽象,在彙編中,它只是內存的一滴而已,沒有可靠的方法來識別結構。但是,對於較簡單的程序,一些啟發式方法可能會有所幫助。例如,“小”大小的數組比大數組更可能是結構。例如,看到從循環讀取的整數將使其看起來像是一個數組,而以恆定的偏移量讀取幾個整數將使其看起來更像一個結構。另一種方法是看到同一組取消引用發生在代碼的不同區域。如果兩個不同的函數將某個指針用作參數,並且都嘗試遵循偏移量0x10、0x18、0x14之類的值,則可能是結構的代碼設置字段。同樣,從單個輸入的指針解引用的對不同大小數據的任何訪問都是一個很好的指示。
了解何時處理結構的最簡單方法是,代碼在調用已知(或文檔狀態)將結構作為參數的函數時。
例如 inet_ntoa 函數的code> in_addr 結構。
鑑於IDA最初並未弄清楚這一點。
我嘗試尋找一種情況,其中將指向數據塊的指針傳遞到函數中,然後在該函數中使用它時,與其不同的偏移量將被視為不同的數據類型。這向我表明1)它不是單一數據類型的數組,2)它是從基址中引用而不是作為單獨的參數傳遞的。當您看到它被動態分配(malloc或new)然後被各種類型的數據填充時,它也有幫助。
在IDA中,我總是建議我的學生繼續為他們懷疑是結構的事物創建IDA結構,並在他們看到被引用的元素時填充它們,即使後來他們發現它們也是如此。是錯的。這是一個反复的過程,隨著您對程序的了解以及在使用過程中如何使用這些數據,您將對其進行更詳細的填充,因此標記出要找出的內容很重要。
您可以使用多種方法(大多數方法已由先前的答案提示),但出於完整性考慮,我將在此處列出一些方法。
查找電話到需要結構的庫。除非可執行文件確實執行了它實際上不應該做的事情,並且將其轉換為指針,然後將其提供給庫函數並希望它不會崩潰(不太可能),那麼您可以在這裡很好地了解哪些內存部分是結構。
查看如何從函數傳遞/返回部分內存。一些編譯器/調用約定/平台與其他類型的數據不同,傳遞和返回小的結構(可以容納幾個寄存器)。例如,如果您看到數據同時在 eax
和 edx
中返回,那麼您可能正在處理一個小的結構。
查看內存的尋址方式。如果似乎內存的一部分已通過指針修改為超過一個寄存器的大小,則很可能是(同樣,除非它們做了異常的事情)數組或結構。另外,就像Yifan所說的那樣,如果有很多通過常量偏移量進行的訪問,則您可能正在查看一個結構,或者一個以類似於該結構的方式使用的小數組(例如 unsigned char rgb [3]
)。而且,如果您要查看的是不同大小的數據塊,那麼它幾乎肯定是一個結構。
但是,在任何假設情況下,只要使用數據與您的模型一致,在原始代碼中它是否只是字節數組,並帶有關於行進方向或成熟結構的約定,這無關緊要。使用任何可以幫助您推理代碼的模型。
要解決有關在不同代碼分支中具有不同內存佈局的結構的聯合問題,我將介紹兩種最常見的聯合用法(以我的經驗):
類型校正:在這種情況下,您甚至可能在編譯後的代碼中看不到它。儘管它在技術上是行為未定義的AFAIK,但大多數編譯器會將其視為 reinterpret_cast<>()
代數類型:通常,此標記帶有一個易於識別。如果您在多個位置看到代碼,這些代碼檢查某個整數,然後根據該值將內存視為不同,那麼您可以猜測這是怎麼回事。如果某些函數不檢查標記,那麼這還會告訴您一些有關函數的信息-假設作者不是很懶,並且想破壞他們的代碼,他們可能認為該函數只會在分支中被調用是不變的。已經知道它是某種類型。
正如其他人所說,我通常會在傳遞引用的情況下尋找函數調用-但我還會尋找返回malloc的緩衝區的實例(這是您知道/確認結構大小的方式)-並設置/初始化了“緩衝區”的各種成員。