【轉】逆向分析技巧

 

逆向分析技巧

第一章 軟件逆向的基礎

  本章首先論述了二進制可執行代碼的做用和格式,以後介紹了軟件逆向所須要瞭解的基本知識,最後論述了軟件逆向經常使用工具及其原理。程序員

1.1 二進制可執行代碼

  二進制可執行代碼是由高級語言編寫的源代碼通過編譯器編譯的生成結果,或者低級語言代碼通過彙編器的生成結果,在這個階段,連接器將多個對象文件合併成一個包含了多個模塊代碼的可執行文件,表現爲二進制可執行代碼。運行時,系統可執行文件加載器將文件加載到內存執行。算法

1.1.1 分析二進制可執行代碼的必要性

  最早進的程序分析工具最適合分析源代碼,它相比於二進制可執行代碼級別能夠獲得更多的高級信息。然而二進制可執行代碼仍然值得研究,軟件中全部的信息都會存在於二進制可執行代碼中。不少商業軟件(特別是Windows平臺的軟件)以及惡意軟件(如病毒,木馬,間諜軟件)都是以二進制格式發行,而分析這類二進制可執行代碼極其重要。和源代碼相比,二進制可執行代碼還有另外一個明顯的優點——執行很方便可是難於閱讀,這個特性在軟件公司發行軟件的隱私保護是頗有利的。
  對於防止系統被攻擊角度來講,分析二進制可執行代碼也是十分必要的,它能在不須要源代碼的狀況下提供安全保障且能避免一大堆法律上的問題。另外破解技術帶動了一個流行的研究方向,即軟件保護,它的做用是防止軟件被逆向。破解和保護是一種無盡的博弈,相對於軟件破解,軟件保護更加須要對二進制可執行代碼進行分析和理解,軟件破解僅僅須要理解代碼的邏輯,找到代碼中敏感信息處以後禁用或者修改便可,而軟件保護須要在理解二進制可執行代碼以後創建起防護系統,將其植入代碼敏感信息處,而且設法阻止原始二進制可執行代碼和防護系統被逆向。同時,惡意軟件會威脅使用者的系統和硬件安全,病毒和木馬都是以二進制形式傳播,而且隱藏在宿主文件二進制可執行代碼中。惡意代碼會隨着宿主文件的啓動而執行,以後又會感染更多文件。所以,對二進制格式代碼的分析是頗有必要的。express

1.1.2 二進制對象文件格式

  對象文件格式是一種計算機用於存儲目標代碼和關聯數據的文件格式。各類類型的操做系統和編譯器都有本身的對象文件格式,公認的格式有COFF、ELF和OMF。通常對象文件會包含多種數據塊,每一個數據塊都存儲一種類型的數據:頭部、可執行代碼段、靜態數據段、未初始化數據段、連接引用、重定位信息、動態連接信息、調試信息。根據對象文件的使用狀況,能夠分紅下面3種類型及他們的組合:編程

  • 可連接對象文件:是連接器或連接加載器的輸入文件。包含了大量符號和重定位信息,目標代碼一般分紅許多小的邏輯段,連接器每次會分別按不一樣方式處理這些段。
  • 可執行對象文件:加載到內存且做爲程序執行的文件。包含目標代碼,爲了使整個文件映射到地址空間,通常是頁對齊的,一般不須要符號和重定位信息。該種文件格式是最多見的可執行文件,一般能夠直接在目標操做系統環境中運行。
  • 可加載對象文件:能夠做爲庫和其餘程序一塊兒加載到內存中。由許多純目標代碼構成,包含全部符號和重定位信息,以便根據不一樣系統運行環境進行運行時符號連接。可執行文件是一種具體的對象格式,在不一樣的系統和編譯器中,可執行有各類不一樣的格式。現今最流行的包括LINUX/UNIX下的ELF文件和Windows下的 PE格式。

  PE格式是Windows系統下的可執行文件格式的一種(NE和LE格式是早期Windows使用的可執行文件格式) 。PE表明Portable Executable,意爲可移植可執行。現今大部分Windows下的32位或者64位可執行文件都是PE文件格式,包括DLL(動態連接庫)/EXE/OCX(控件)/LIB(靜態連接庫)/SYS(驅動)等後綴形式的文件。PE格式以舊DOS文件頭信息開始,後接MS-DOS實模式存根程序,用來顯示實模式DOS下的信息,該程序以後是PE文件頭,該結構大小爲18h字節,用於描述文件基本特徵,包含PE\x0\x0標記。緊接在PE文件頭以後是可選頭,用於詳細說明頁面映像結構(加載基址、內存映像大小、對齊方式等)。可選頭中最重要的結構之一是DATA_DIRECTORY結構,包含了導入導出表、調試信息、可重定位表等信息。接在可選文件頭以後,是各類不一樣類型的節,節以後是程序附加數據,通常打包程序可能用到該區域。windows

1.2 相關專業術語

1.2.1 反彙編

  反彙編是將機器語言代碼翻譯成彙編語言代碼的過程,按執行方式能夠分爲靜態反彙編和動態反彙編。靜態反彙編是指不執行的方式的翻譯過程,動態反彙編則須要跟蹤執行目標文件,具體反映出程序運行狀況。靜態反彙編可以一次對整個可執行文件進行處理,而動態反彙編只能處理被執行到的部分,同時反彙編的時間與文件的長度成正比,而動態反彙編時間與被執行到的指令數成正比。一般前者時間消耗比後者少,靜態反彙編效率比動態反彙編效率高。本文主要討論靜態反彙編的狀況。設計模式

1.2.2 類佈局和RTTI

  C++語言在底層實現中借用了許多C語言中的優秀實現手段。爲了實現跨平臺,C/C++編譯器生產廠商遵循簡單內存佈局原則,成員變量按照聲明的順序對其排列在內存中。在MSVC8及以上的版本中,可使用編譯程序cl.exe獲取生成的類佈局信息,按以下格式使用:api

cl -d1 reportSingleClassLayout [classname] filename	查看單個類佈局
cl -d1 reportAllClassLayout filename 查看該文件全部類及結構體佈局

  運行時類型信息(Run-Time Type Information)是一種由編譯器生成的信息,用於支持dynamic_case<>和typeid()這樣的操做符。RTTI是爲帶有虛函數的類(多態類)設計的。MSVC編譯器會在虛函數表指針以前放置指針指向 「完整對象定位符(COL)」結構體,編譯器能夠經過該結構體根據虛函數表指針找到一個完整對象的位置。RTTI對於逆向分析函數類提供有價值的信息,能夠從中恢復出類名和繼承關係,甚至能夠恢復出類佈局。數組

2.2.3 虛函數和虛表

  在C++語言中,虛函數是程序運行時刻才肯定的函數,它會自動調用指針指向的真正對象對應的函數。調用的函數地址在編譯時是沒法肯定的,只有在調用即將執行前肯定,所以虛函數的調用經過間接調用實現。全部的虛函數地址都會存放在一個專用常全局數組——虛函數表中,而由帶有虛函數的類實例化出的對象,老是帶有虛函數表指針。非派生對象僅有一個虛函數表指針,而多重繼承可能有多個,同一個類實例化的對象之間共用虛表。純虛函數是在抽象類中使用的,必須通過繼承類重載後纔可調用。
  對於基類,虛函數地址按照虛函數聲明順序排序在虛表中,而派生類的重載函數會替換相應基類虛函數地址。一般虛函數表就是一個普通的數組。然而某些編譯器是以鏈表組織的,虛函數表中每一個元素含有指向下一個元素的指針,元素之間並非緊密排列而是分散在文件中,這種狀況較爲少見。對於多重繼承的派生類,虛函數表項可能存在指向轉換程序(Thunk)的地址,該程序修改this指針以從父類虛函數表中調用虛函數,使其指向「替換函數」對象實例。這種技術是由C++語言開發者Bjarne Stroustrup提供的,他借用了Algol-60的早期實現形式,在Algol中修正this指針的代碼稱爲形實轉換程序(thunk),而調用自己稱爲「經過形實轉換程序進行的調用」,這些術語至今在描述C++時使用。sass

2.2.4 API

  API是Application Programming Interface應用程序接口的縮寫形式,是構築Windows程序的基石,下層是操做系統內核,上層是用戶應用程序,應用程序甚至操做系統自己都要經過API完成特定的功能,Windows上幾乎全部有實際功能的程序最終都會直接或間接地調用API,所以熟悉API調用甚至API彙編代碼對於逆向分析和調試代碼十分必要。Windows API按函數功能能夠劃分爲:硬件與系統函數、控件與消息函數、菜單函數、進程和線程函數、繪圖函數、打印函數、網絡函數、文件處理函數、加密函數等。API按照系統層次可劃分爲用戶級API和系統級API
安全

2.2.5 Windows消息機制

  Windows是消息機制驅動的系統,消息提供了應用程序之間、應用程序與系統之間的通訊手段,應用程序實現的功能靠消息觸發,並可以對該事件進行響應和處理,這些處理函數稱爲回調函數。Windows窗口程序主要是是事件驅動而非過程驅動,所以瞭解Windows消息機制極其必要。WWindows系統中有兩種消息隊列,系統消息隊列和應用程序消息隊列。Windows爲每一個程序(嚴格的說是每一個線程)維護一個消息隊列,Windows檢查系統消息隊列消息的發生位置,若是位於某個應用程序窗口範圍則將該消息派遣到應用程序消息隊列中。若是應用程序沒有來取消息則消息就暫時保留在隊列中,當程序中的消息執行到GetMessage時,控制權轉移到GetMessage所在的USER32.DLL中,USER32.DLL從消息隊列中取出一條消息並把這條消息返回應用程序。應用程序處理這條消息時,因爲可能存在多個窗口,所以並非直接調用本身的回調函數窗口過程,而是調用DispatchMessage函數經過系統找到給合適的窗口過程進行調用。窗口過程處理完畢後控制權返回到DispatchMessage,繼續消息循環。應用程序之間或自己也能夠發送消息,PostMessage將消息投遞到某程序的消息隊列中,而SendMessage則越過消息隊列直接調用目標程序窗口過程,過程處理完畢後才返回。

2.2.6 MSVC和GCC

  在本文中,MSVC是指微軟Visual Studio中的C++編譯器,它是Windows程序設計中使用最多的編譯器,所以熟悉該編譯器內部運做機制對Windows下的逆向分析十分必要,可以識別編譯器自動生成的代碼不但有助於快速定位程序員編寫的用戶代碼,並且對於恢復軟件層次架構也頗有幫助。而GCC則是Unix/Linux系統的默認編譯器,已經移植到Windows平臺上。

2.3 在軟件逆向工程中使用工具

2.3.1 在逆向工程中使用分析工具的必要性

  逆向分析中最重要的工具包括靜態分析工具、動態分析工具和其餘輔助工具。動態分析工具主要功能是調試,以便高效分析軟件的行爲並驗證靜態分析結果,甚至找出軟件缺陷和漏洞。因爲操做系統提供了完善的調試API,所以利用各類調試工具能夠很是方便的觀察和控制目標軟件,在調試過程當中,使用者能夠隨意修改指令、寄存器、內存、設置運行斷點,使程序一邊運行一邊分析。而靜態分析則是相對於動態分析而言的,在不少場合下不適合直接運行程序,例如軟件的某一模塊(沒法單獨運行)、病毒木馬蠕蟲程序、平臺設備不兼容等。此時須要使用靜態分析經常使用的反彙編軟件將二進制可執行代碼轉換爲彙編語言進行分析。
  逆向工具的做用,也是其產生的目的,就是將逆向工做者從庫函數分析、異常處理、反彙編這些重複的勞動中結果出來,而把主要精力放在程序的數據結構、算法、功能的分析中去,可是現今的逆向軟件因爲功能不夠完善同時因爲編譯器的多樣性和高度優化性,所以不少步驟仍需人工干預,交互式完成。

2.3.2 常見軟件逆向工具簡介

  對於反彙編器,目前已經有不少比較好的反彙編軟件產品,如國內的MC-Z80、DBJ-Z80系統軟件,但它們主要是針對某種型號的產品而開發的,通用性和可移植性較差。國外著名的反彙編軟件主要有C32Asm、W32Dasm、Hopper Disassmbler和IDA Pro等。C32Asm集反彙編、16進制工具、Hiew修改功能於一體。W32Dasm支持靜態智能反彙編、代碼查找及轉移功能,還具備動態分析功能,速度快,可是它只能對80x86系列指令集程序進行操做。Hopper Disassmbler是一款專業的32位和64位可執行文件的反彙編、反編譯和調試軟件。IDA Pro支持的指令集最多,交互性最強,且提供了反彙編程序流程分析和流程圖顯示功能,內部支持自編寫IDC腳本和用戶插件。
  對於調試器,目前比較優秀的調試器有OllyDbg、MDebug、RORDbg、WinDbg、SoftIce等。OllyDbg是一款出色的調試器,是當今最爲流行的用戶模式調試器,功能繁多,可編寫插件進行功能擴展。RORDbg是一個虛擬機技術實現的簡易調試器,主要用於外殼分析和脫殼,目前只能運行exe主線程和dll入口函數,因爲採用虛擬方式執行指令所以能夠做爲分析外殼輔助手段,速度較慢。OllyDbg、PEBrowse Professional均爲用戶模式調試器,WinDbg和SoftIce則爲內核模式調試器,WinDbg因爲和Windows操做系統緊密結合,能夠方便的調試DLL初始化代碼和內核,同時在調試過程當中下載對應符號信息,方便理解程序。
  在軟件逆向中一般還須要其餘多種類型的輔助工具,包括日誌記錄器、代碼可視化分析、設計模式恢復、文檔和圖表生成工具等。

2.3.3 Intel指令結構

  反彙編器是解析機器指令的,以x86平臺Intel指令集Opcode爲例,Intel指令手冊中描述的指令由6部分構成,如表 2.1所示。前綴最多有4個,每一個前綴1字節,不容許一個前綴重複2次,前綴分爲:普通前綴(Prefixed)、指示性前綴(Maandatory Prefix)和64位擴展前綴(REX Predix)。前綴有4中,包括鎖前綴(F0H)和重複前綴(REP系)、寄存器和地址超越前綴(66H,67H)、64位擴展前綴(REX)。

Instruction Prefixes Opcode Mode-REG-R/M SIB/Dispacement Immediate
指令前綴(可選) 指令操做碼 操做數類型(可選) 輔助Mode R/M,計算偏移地址(可選) 當即數(可選)
每一個1字節 1,2,3字節 1字節 1,2,4字節 1,2,4字節
  • Opcode爲機器碼中的操做符部分,用來講明指令語句執行什麼操做,如某條彙編語句是MOV,JMP仍是CALL。Opcode是彙編指令的主要部分,必不可少,對Opcode的解析也是反彙編引擎的主要工做。彙編指令助記符與Opcode大致上一一對應。
  • Mode-REG-R/M:是輔助Opcode解釋彙編指令助記符後的操做數類型,R表示寄存器,M表示內存單元
  • SIB:輔助Mode-REG-R/M計算地址偏移,SIB的尋址方式爲基址+變址,SIB佔1字節大小,0,1,2位用於指定做爲基址的寄存器,3,4,5位用於指定做爲變址的寄存器,第6,7位用於指定乘數,因爲只有2位所以能夠表示4種乘數:1,2,4,8。如MOV EAX,DWORD PTR DS:[EBX+ECX*2]。
  • Displacement:輔助SIB,如MOV EAX,DWORD PTR DS:[EBX+ECX*2+3]這條指令的「+3」是由Displacement指定。
  • 當即數,用於解釋指令語句中操做數爲常量值的狀況。

2.3.4 反彙編器工做原理

  瞭解了指令集以後,須要對指令集機器碼進行二進制到彙編語言的解析,解析所用到最著名的2種算法稱爲線性掃描和回溯遍歷,是全部調試器和反彙編器的基礎。線性掃描算法會按順序逐個讀取二進制字節並嘗試匹配指令,流程以下Procedure LinearDisasm(addr):

  • 1)判斷addr是否在起始地址和結束地址範圍內,若是不在則退出,不然轉2。
  • 2)將該地址處字節翻譯成指令I,並加入結果指令集合中,同時返回指令長度length,轉3。
  • 3)將addr向後推動length個字節,轉1進入序列中下一條指令。

  該算法的優勢是:因爲掃描的是整個代碼區域,所以可以在很大程度上識別每條指令。然而該算法不能區分數據和代碼。因爲算法會順序獲取字節轉換成指令,所以代碼中嵌入的數據也會被當作指令字節,遇到這種狀況反彙編器沒法翻譯成正確指令且不斷產生錯誤指令直到遇到沒法與任何指令進行匹配的字節。此外,沒法讓反彙編器得知開始產生持續錯誤的位置。GNU實用程序objdump和許多連接時優化工具均採用該算法
  線性掃描算法的主要缺點是沒有利用二進制文件的控制流程信息。所以它沒法避免地將代碼中嵌入的數據錯誤解釋爲數據,產生錯誤指令。這樣不只會致使嵌入代碼中的當前數據的翻譯錯誤,後面接續的數據也會受到影響。爲了不誤把數據解釋爲指令產生了回溯遍歷算法,該算法採起了以下的控制流程Procedure RecursiveDisasm(int addr):

  • 1)判斷addr是否在起始地址和結束地址範圍內,若是不在則退出,不然轉2。
  • 2)若是addr已經訪問過則退出,不然轉3。
  • 3)將該地址處字節翻譯成指令I並加入結果指令集合中,返回指令長度length,同時該地址處標記爲已訪問,轉4。
  • 4)若是I是一條分支指令則轉5,不然轉6。
  • 5)對於I的每條分支,執行RecursiveDisasm(addr)轉6回溯執行分支。
  • 6)將addr向後推動length個字節,轉1進入序列中的下一條指令。

  不管反編譯器什麼時候遇到分支指令都會嘗試從全部可能的分支地址處解析。理想的狀況是,若是反編譯器知道每一個分支的準確目的地址,根據控制流程,反彙編器就能夠遍歷全部可能在運行時執行的代碼,這樣代碼段會被換分紅多個不內嵌數據的代碼塊。但若是目標地址在運行時是動態變化的,即間接分支指令的目標地址具備歧義性,此時反編譯器沒法經過靜態方法獲取該地址,也會致使翻譯錯誤。所以,一個反彙編器可能會遺漏實際指令,若是猜想的地址有誤,也會產生翻譯錯誤。大量的二進制翻譯和優化系統都使用該算法,例如UQBT翻譯系統,在控制流圖解析的研究中也採用了該方法,主流逆向工具反彙編算法如表 2.2所示。

Debugger Disasm
OllyDbg 回溯遍歷
SoftIce 線性掃描
WinDbg 線性掃描
IDA Pro 回溯遍歷
PEBrowse 回溯遍歷

2.3.5 利用靜態工具進行分析

  代碼分析工具在進行軟件分析是經過提取軟件信息完成的。軟件分析一般分爲三類:靜態分析、動態分析和歷史分析。靜態分析用於不須要執行的軟件,動態分析用於分析執行痕跡或捕獲運行行爲,歷史分析用於分析版本系統變化引發的軟件變化。
  優秀的靜態分析工具能支持多種編程語言元素特性、多種編譯器內部處理機制、多種操做系統特性和平臺不兼容代碼,這意味着即便不能在當前平臺進行運行、調試和測試,這種工具仍能進行代碼分析,這類工具如TXL和SrcML;靜態分析工具能解析出有價值的信息,有時並不須要構建一個完整的抽象語法樹AST(Abstract Syntax Tree),而使用特殊的逆向工程方式獲取這些信息,這類工具如Bauhaus和Columbus;靜態分析工具可以提取程序語義,該功能經過多重逆向手段解析出更多的AST信息,這類工具如CodeSurfer。靜態工具速度較快,然而在處理指針、多態和動態類型等情形時靜態分析會難於進行,另外對於用戶交互和對象之間數據交換的分析也是靜態分析所不擅長的,而動態分析卻適合處理這些狀況。

第二章 靜態逆向分析模型

  逆向分析的目標是理解一個系統中的軟件以便更容易地進行加強功能、更正、增長文檔、再設計或者用其餘的程序設計語言再編碼。逆向工程工具應支持產生程序的高層抽象,使維護者更容易理解程序,重用舊代碼以及準確加入新功能,避免死碼的產生。
  在必定規則下進行逆向分析會提升逆向分析的效率,下面就來介紹這樣的規則。本章提出的逆向框架其中涉及的每一部分都在會在本章中和以後的章節中進行詳細介紹。

3.1 使用IDA進行靜態逆向分析的各個階段

  該模型分爲預處理模塊、函數識別模塊、類識別模塊、異常處理識別模塊、綜合分析模塊。預處理模塊對二進制可執行代碼進行反彙編和初步分析,去除軟件保護機制,初步識別PE文件格式、資源及導入導出表、程序入口等信息。函數識別模塊用於識別系統庫函數和用戶函數,包括對變量、表達式、語句、函數傳參調用和函數執行流程的分析。類識別模塊用於識別存在於二進制文件中的類佈局、對象結構、RTTI信息、this指針的使用,從而分析出成員變量和成員函數,推導出原始的類結構。異常處理模塊用於識別二進制可執行代碼中存在的異常處理信息,包括SEH和C++異常。綜合分析模塊對上述三個模塊的處理結果進行綜合分析,推導出二進制可執行代碼對應的源代碼、軟件架構、算法、設計模式和文檔

3.2 分析前處理

3.2.1 去除保護機制

  現代軟件傾向於打包(添加保護機制),通過打包之後實際執行的代碼會被加密和壓縮保護,下降了彙編代碼可讀性。
  爲了便於分析試劑執行代碼,去除程序保護,須要先用外殼探測程序得到目標程序所用保護類型,而後針對該類型使用解包器、破解、脫殼、調試、內存轉儲等多種手段和工具將實際執行的代碼剝離出來,對於打包器或加殼工具對原程序資源和導入表形成的破壞還應使用相關恢復工具進行恢復。另外,分析前處理的另外一個重要做用是識別程序的編寫語言,這有助於對程序內用到的編程語言相關的庫函數的簽名識別。
  通常地手工分析程序編寫語言能夠經過剖析入口點特徵,這個步驟和查殼工具原理相似,能快速定位編寫語言甚至編寫庫但並不通用,由於無保護機制的程序入口點也有多是隻是一層語言外殼,更通用的方式是使用IDA等專業工具的庫函數簽名機制。

3.2.2 分析程序中用到的函數

  在Windows中,代碼共享是進程通訊的核心思想,用戶程序不能直接控制硬件,也不能直接和Windows內核通訊,Windows提供了各類功能的dll(動態連接庫),這些dll的輸出函數能夠爲用戶程序提供內核服務,用戶進程會常常調用API實現特定功能,所以瞭解API是必要的。一般使用PE導入表查看靜態加載的API,字符串常量域結合動態加載函數API(GetProcAddress)以獲得動態加載的API。

3.2.3 分析程序中的資源

  PE格式常見的資源包括位圖、加速鍵、光標、對話框、圖標、菜單、字符串表、工具條和自定義資源,分析PE資源能夠經過調試技術快速定位到程序執行流程的關鍵點,其中字符串所包含的信息較爲敏感,經過觀察字符串和程序執行邏輯的變化,有助於快速定位到關鍵代碼。對於有界面框架的程序,對話框和菜單有着特殊的重要性。例如MFC工程中,根據菜單和對話框操做相關API,以及對話框中子控件資源屬性(主要是ID)和菜單子項ID,能夠很容易地分析出當前代碼所產生的行爲。導入和導出函數能夠用於方便地定位程序主幹代碼,便於理清執行邏輯。

3.2.4 分析入口函數

  程序的入口點是最早執行的代碼位置,不少初始化工做都會在其中進行。程序入口分爲真正入口和用戶入口,以MSVC爲例,應用程序的真正入口點並非main/WinMain/DllMain及其寬字符形式(w-)(這部分屬於用戶入口),這些函數僅僅是真正入口點(如start)所執行的一個可由用戶重載和自定義的函數而已。對入口函數啓動部分流程和庫函數使用的分析,能夠進一步肯定程序相關的編程語言信息和使用庫函數信息。函數入口在開始執行用戶入口函數以前所作的操做有:獲取平臺版本、初始化堆空間、初始化命令行參數、初始化環境變量、初始化全局數據和浮點寄存器等,能夠經過該流程和用戶入口函數的參數類型,找到用戶入口函數。同時,在全部用戶函數執行完以後,程序並無結束運行,而是繼續執行一些清理工做,例如exit和atexit函數,最後調用API終結進程,這部分也是在庫函數代碼中實現的。

3.2.5 識別庫函數

  現代程序中早已融入模塊設計概念,程序中充斥着各類系統庫函數、第三方函數和組件,接口設計方法普遍應用於軟件領域,所以對於程序的分析不可避免會遇到庫函數,並且一般這些庫函數代碼量會比用戶實際編寫代碼量多出幾倍(平均起來庫函數在程序代碼中所佔的比重爲50%到90%,特別是利用了可視化開發環境自動生成代碼功能),因爲數據量巨大,分析時間漫長,且庫函數經常比簡單的程序代碼更加複雜而難以理解,所以不適合直接手動分析,於是程序分析軟件應該能提供一種較爲智能的方式自動識別這些函數,而把逆向分析者主要精力快速集中在用戶代碼的分析上。做爲逆向工做者,也應該熟悉經常使用庫函數的彙編級模板,這樣就能夠在代碼分析工具因爲庫函數升級或高度優化致使沒法正常識別的庫函數的狀況下不影響逆向速度。
  IDA使用了一種高效的方式,使用二叉樹形式組織的標準庫檢索字節序列,這種搜索方式的時間複雜度是O(logn),對於大多數狀況使用函數開頭的32字節足以準確識別。在識別操做正確性方面,許多函數結束位置處於二叉檢索樹相同的葉節點,這會致使識別過程出現衝突或二義性,這也是在製做IDA的sig(特徵標誌庫)文件常常發生衝突的緣由。爲了減小錯誤,IDA經過啓動代碼識別編譯器程序並加載相應的庫文件,同時容許用戶手工加載這些特徵標誌庫文件。在程序連接時編譯器常以用戶OBJ模塊與函數庫的列表順序分配函數,因此不少狀況下代碼區中的庫函數段和用戶代碼段之間會有明顯的分隔。
  不少函數庫包含了開發商信息和庫版本的版權內容,這爲識別編譯器類型和版本帶來了很大便利,只須要找到相應文本字串片斷便可。特殊函數庫帶有某些特徵也能夠用來識別編譯器,例如調用的Windows API種類(用於文件、內存、圖形、網絡、加解密、硬件等)、數學函數通常含有豐富的協處理器指令等。另外能夠利用參數和常量等信息進行推斷,如函數接受浮點參數,那麼極有可能來自於某個數學函數庫。最後,算法的識別也會有助於識別庫函數。

第三章 C語言元素分析

  本章對常見數據類型、表達式、語句、函數結構等基本的C語言元素進行語法分析和底層實現分析;對函數棧結構的函數序言和函數結語結構進行剖析;根據內存變量使用狀況提出了一種識別變量生命週期的方法;基於函數棧幀原理提出了一種切實可行的函數邊界檢測方法,該方法能夠準肯定位特定高級語言函數的機器碼範圍。

4.1 識別變量

4.1.1 識別棧變量

  在高級語言代碼中若是顯式聲明過自動類型變量,則一般都會在該函數棧中開闢對應空間劃分變量空間,一種劃分方式是將指令sub esp,xxx放於函數入口附近,而相對的add esp,xxx放於函數結束處附近。而在使用這些變量時,也相應會用ebp作間接尋址。MSVC中常使用ebp的正偏移作棧變量尋址,而Borland和其餘編譯器則經常使用使用負偏移。做爲參數的變量因爲傳遞以前會被壓棧,所以只要在函數體內計算出當前棧頂指針和棧底指針位置,就能夠選擇一個棧寄存器進行間接尋址獲取參數。在存在函數序言的狀況下,因爲調用函數時的call func指令和函數內push指令的2次壓棧操做,所以第一個參數是從ebp+8位置開始的,後面的參數按4字節大小向高地址遞推,能夠看出參數尋址的相對偏移量爲正,與此相反,函數內棧變量則是以ebp負偏移量進行尋址。特殊狀況下,編譯器可能對棧變量進行優化,而將一些無用的參數棧位置做爲棧變量使用。根據這些特性和函數中引用局部變量和參數的指令能夠恢復相應的函數棧。函數內部常常會對棧指針進行調整,這時參數和變量偏移位置就須要常常從新分析。

4.1.2 識別堆變量

  堆變量是全部變量中最容易識別的一種類型。在C\C++中一般使用malloc和new操做符實現堆空間的申請,返回的數據是堆地址,該地址對於整個進程有效。相應的使用free和delete釋放該地址處申請空間。在Windows下申請堆空間在程序結束前都須要調用釋放堆空間的API函數,不然會形成內存泄露。

4.1.3 寄存器變量和臨時變量

  爲了使訪問內存頻率儘量低,編譯器的高級優化功能會把使用頻率最多的局部變量存在通用寄存器中。C/C++語言中,register關鍵字用來向編譯器請求分配在寄存器中,由編譯器選擇最佳代碼處理方式。寄存器變量能夠由PUSH指令臨時存放在棧中,並由POP指令彈出堆,寄存器變量不會經過EBP寄存器進行尋址。臨時變量的產生是編譯器根據實際須要產生的變量,臨時變量產生的緣由有下面三個可能:

  • 對於複雜語句表達式,在上個指令完成以後存儲操做結果,並用於下面的指令,該操做結果可能存儲爲臨時變量。
  • 在移動數據時產生臨時變量。因爲80x86處理器不支持直接從內存到內存的數據傳輸,所以內存間的變量賦值須要藉助於臨時寄存器變量。
  • 做爲存儲函數返回值。絕大多數高級語言(包括C/C++)容許將函數調用做爲表達式的一部分。

4.1.4 識別全局變量和靜態變量

  分析程序的算法的關鍵一步就須要透徹地分析整個反彙編代碼,並搜索出全部的交叉引用,因爲全局變量經過直接尋址,所以在高級語言中識別全局變量相對容易。IDA中能夠對符號進行交叉引用檢索,這個特色大大提升了分析代碼的效率,然而在一些狀況下IDA並不能很好地識別這些符號,所以須要以手工方式進行交叉引用的重建。靜態變量和全局變量類似,只是做用域不一樣,靜態變量在編譯級別只能在定義的做用域內使用,在彙編級別的表現是集中於某個函數代碼域,而全局變量能夠在多處使用。全局變量須要在主函數執行前初始化,在程序退出前執行清理工做(析構),而和靜態變量在首次使用時初始化,並設置某個標誌位,註冊退出函數,在下次執行到該處時跳過初始化,在程序退出時執行清理工做。MSVC經過生成初始化函數段實現這種初始化功能,這些初始化例程地址會存入一個表中,在程序啓動後由運行庫函數_cinit進行處理。該表常位於.data段起始處。

4.2 識別特殊類型

4.2.1 識別字符串

  IDA能夠識別多種格式的字符串,包括c型(結束符’\0’)、dos型(結束符’$’)、pascal型(長度域1字節)、寬pascal型(長度域2字節)、delphi型(長度域4字節)、unicode等類型。Windows程序常見的類型是c型和unicode,所以識別正常的字符串並無困難,可是若是字符串採用了加密技術轉換成不明確的數字,狀況就會變得複雜。這時候首先應該使用交叉引用功能查看數據段裏的數據被哪些代碼所引用,若是被引用則對該處代碼進行分析,進行算法逆向還原出字符串。對於自動檢測程序中的字符串有許多的識別字符串的算法可用,都是基於以下三點:

  • 字符串是有限字符集和,字符是指數字、字母、符號和諸如列表框和回車符之類的控制字符。
  • 字符串至少包含2個以上的字符。
  • 肯定字符串類型,不一樣類型字符串邊界計算方式不一樣。
      MSVC編譯器在初始化棧上字符串變量時,一般按機器字長爲步長對數據和棧區進行劃分,將數據分別拷貝到目的棧區。

4.2.2 識別結構體

  若是在彙編代碼中發現對某處內存附近進行連續讀寫且讀寫指令相距較近,則該處可能爲結構體實例一部分。存在於堆空間的結構體大小通常能夠從動態分配空間大小推斷出來;存在於棧空間的結構體識別,則須要結合兩種分析方式。第一種方式是採用累積法,若是發現內存連續賦值且這種行爲在多處反彙編代碼中出現就將該偏移處元素其加入結構體。第二種方式是反推法,對於棧結構體,分析當前整個函數棧大小和全部其餘棧變量邊界範圍,反推出該結構體範圍。若是從API調用參數或者從其餘分析過的有關聯的函數中已經得知該結構體類型,則能夠簡化分析。

4.3 識別語句

4.3.1 識別表達式

  普通表達式會包括算數運算、邏輯運算、關係運算、位運算等形式的語言元素。對於複雜表達式,編譯器首先會對複合條件根據內定的計算順序進行語法分析和表達式分解,拆分紅多個體現基本操做之間相互關係的簡單條件做爲中間形式,以後使用goto語句替換條件語句。編譯器在分析時採用邏輯二叉樹結構表示複雜條件的分解過程。在邏輯樹分支較多時,會對邏輯樹作修剪操做,優化邏輯樹結構,經過對條件進行取反而剪除多餘樹枝並刪除子樹全部標號,經過合併和修剪枝幹的分支優化理清邏輯關係。

4.3.2 識別循環語句

  循環語句是一種常見程序設計邏輯,C++循環語句主要包括for循環、while循環、do-while循環三種形式,每種循環有着不一樣的執行流程:do循環先執行循環體後比較判斷,while先比較判斷後執行循環體,for先初始化再比較判斷最後執行循環體。

for循環

  for循環語句能夠抽象成下面的通常語法形式:for(statement1;condition;statement2) {statement3;},編譯以後能夠轉化爲以下彙編代碼形式:

call statement1;
jmp judge
change:
call statement2;
judge:
call condition;
jz end;
call statement3;
jmp change;
end:…
while循環

  while循環語句能夠抽象成通常語法形式:while(condition) {statement;},編譯之後能夠轉化爲以下彙編代碼形式,能夠看出形式上較for循環要簡單:

judge:
call condition;
jz end;
call statement;
jmp judge;
end:…
do循環

  do循環語句能夠抽象成通常語法形式:do{statement;} while(condition);,編譯結果能夠轉換爲以下僞代碼,能夠看出形式上較while循環簡單:

begin:
call statement;
call condition;
jnz begin;

  一般編譯器在優化的時候,while和for循環會近似優化爲效率更高的do循環。對於continue語句,continue執行後當即將控制權傳遞給檢查條件代碼,通常地在帶有前置條件的循環中,該語句會編譯成一條向上方定位的無條件跳轉指令;而在後置條件循環中,該語句則被編譯成一條向下方定位的無條件跳轉指令,continue以後的當前域語句不可執行。

4.3.3 識別分支語句

  C語言分支語句包括if-else語句、if-else if-else語句、switch-case-default語句,分支是任何程序設計語言的核心內容,所以正確識別它們是極其重要的。

if語句

  if語句能夠抽象成通常語法形式:if(condition) then{statement1;statementN;} else{statementl1;statementlN;},編譯器的任務是將這條語句編譯成若是condition成立則執行statement1與statementN指令序列,若是不成立則執行statementl1與statementlN指令序列。絕大多數編譯器(即便不具備優化功能)都對條件值進行取反從而將語句if(condition) then{statement1;statementN;}轉換成以下的僞代碼:

if(not condition) then continue
	statement1;
	…
	statementN;
continue:

  可見要重建程序的源代碼,必須對條件值進行取反,從而使語句塊{statement1;statementN;}一定繼起於then關鍵字。 而對於整個語句的if-then-else,僞代碼以下:

if(not condition) then else
//執行if分支語句
statement1;
…
statementN;
goto continue;
else:
//執行else分支語句
statementl1;
…
statementlN;
continue:…
switch語句

  在Windows程序設計中常常在多信息碼的狀況下會用到switch語句,例如消息回調、錯誤處理、網絡狀態、驅動派遣等實現代碼,是比較經常使用的多分支結構,效率上也高於if分支結構。這種分支語句若是不經優化,表示成邏輯樹因爲分支數較多、深度較大、效率較低,表現爲「一邊倒」,所以一般會在編譯時經過分叉算法進行優化和平衡,在這種算法中編譯器會根據須要改變case分支語句的處理順序,進行壓縮處理,下降邏輯樹深度,加快索引速度。編譯器須要找到合適的值使每一個節點的左右子樹深度達到基本平衡,通過平衡邏輯二叉樹,最大比較深度從o(n)降爲o(logn)。通過編譯器優化的switch語句提升了執行性能,也提升了逆向分析的複雜度。在逆向分析switch代碼時只須要將相等判斷的語句提取出來便可恢復switch語句。
  在switch分支數小於4的狀況下,MSVC採用模擬if-else if 的方法,而當分支數大於4且case斷定值存在明顯線性關係組合時,編譯器會採用語句塊地址表或語句塊索引表進行優化。若case斷定值無明顯線性關係則編譯器會採用相似上面二叉判斷樹的方式實現。整體上說,編譯器處理case有幾步:首先對全部case值按大小排序,而後劃分出近似線性段和相對非線性段,對每一個線性段分配靜態索引地址數組創建跳轉表以加快索引速度,對於非線性段則使用原始if-else型判斷加以翻譯。

4.4 識別用戶函數

  函數和堆棧是密不可分的:參數經過棧傳遞給函數;函數內部經過劃分棧區分配棧變量。

4.4.1 函數序言(prolog)和函數結語(epilog)

32位的狀況

  對於32位程序下,若是未經編譯器優化,即可能生成函數序言和函數結語,它們的做用分別是保護現場和恢復現場。函數序言部分通常出如今函數的開始,32位函數中標準的函數序言代碼以下:

push ebp;保存ebp寄存器
mov ebp,esp;設置棧幀指針
sub esp,localbytes;在棧內存中分配局部變量空間
push <registers>;保存寄存器

  其中localbytes變量表示局部變量棧上所須要分配的字節數,變量表示要保存在棧上的寄存器列表,這些寄存器壓入棧後,即可以在函數中使用。函數結語部分通常出如今函數的結尾,一般只有一個函數序言,而函數結語可能有多個,32位函數中標準的函數結語代碼以下:

pop <registers>;恢復寄存器
mov esp,ebp;恢復棧指針
pop ebp;恢復ebp
ret;函數返回

  在MSVC中若是使用naked關鍵字修飾函數,編譯器會省略函數序言和函數結語部分。

64位的狀況

  64位程序中有兩種函數類型,須要棧幀的函數稱爲幀函數,不須要的稱爲葉函數。在幀函數中任何須要分配棧空間、調用了其餘函數、保存非易失性寄存器或者使用了異常處理的函數都必須有函數序言和函數結語,此外,幀函數還須要一個函數表項。函數序言所作的操做有:必要時將參數寄存器保存在內部棧中、將非易失性寄存器入棧、爲局部變量和臨時變量分配固定的棧空間、設置棧指針。對於棧中分配的固定空間超過一頁(大於4096字節)的狀況,棧空間的分配範圍可能超過一個虛擬內存頁,所以實際分配前須要檢查分配狀況。編譯器會爲此提供一個特殊的例程用於保護參數寄存器,供函數序言調用。64位程序函數序言的典型代碼爲:

mov [rsp+8],rcx;存儲rcx
push r15;保存非易失性寄存器
push r14;
push r13;
sub rsp,fixed-allocation-size;爲局部變量分配固定大小的棧空間
lea r13,128[rsp];創建棧指針

4.4.2 識別函數參數棧

  函數傳遞參數的方式有3種:堆棧方式、寄存器方式和同時使用堆棧與寄存器的方式,通常來講傳參的類型,不管是普通類型變量、結構體、類,都會拆分紅固定長度(4字節)壓棧,而浮點數則能夠經過寄存器拆分式壓棧也能夠經過浮點寄存器壓棧傳參。

32位程序常見調用約定
  • _cdecl:C/C++函數默認調用約定,棧由調用者清理,所以函數參數個數能夠是變參,同時因爲每次調用結束,調用者都要平衡堆棧,所以調用時產生的代碼量教_stdcall多。
  • _stdcall:Windows API函數使用的調用約定,棧由被調用者清理,所以沒法使用變參,因爲恢復棧的代碼存在於API實現中,所以調用時產生的代碼量較_cdecl少
  • _fastcall:該調用約定指定傳遞的參數儘量使用寄存器,僅適用於x86體系結構。因爲使用了寄存器,所以這種調用約定函數執行效率較高。沒法使用變參
  • _thiscall:該約定是類成員函數的默認調用約定,不能使用變參,由被調用者清理堆棧,this指針一般做爲隱藏參數傳遞。
64位程序調用約定

  64位平臺下編譯器只使用新型_fastcall調用約定,其主要特性以下:

  • 前四個整形或指針類型參數由4個通用寄存器R8, R9, RCX, RDX依次傳遞,前四個浮點類型參數由4個浮點寄存器XMM0,XMM1,XMM2,XMM3傳遞。
  • 除前四個參數之外的參數經過棧來傳遞,從右至左依次入棧。
  • 由調用函數負責清理調用棧。
  • 小於等於64位的整形或指針類型返回值由RAX寄存器傳遞。

4.4.3 識別函數棧變量

  棧變量老是在某個函數的範圍內起做用,一個函數從生命週期開始到結束,整個函數內不管在哪一個做用域申請的局部非靜態變量,均是棧變量,一般狀況下全部棧變量空間的總和均在函數頭部(可能位於函數序言)一次分配。esp寄存器爲棧頂寄存器,指向當前棧的起始地址,壓棧操做會改變該寄存器值,ebp寄存器爲棧底寄存器,指向當前棧的結束地址,用來保存和恢復函數棧幀。esp和ebp經常使用來取得棧變量,在一個函數中,棧頂常常會發生變化,而棧底相對不變。當發生函數調用時,控制權從一個函數進入另外一個函數,就會針對該函數開闢出所需棧空間;當一個函數結束時,須要清除使用的棧空間,關閉棧幀,這一過程稱爲棧平衡,在MSVC的調試版程序中,會有庫函數  __chkesp專門檢測函數調用以後的棧平衡。棧幀中能夠尋址的數據有局部變量、函數返回地址、函數參數等。
  分析棧變量生命週期:高級語言中,變量均有做用域,所謂做用域是指變量在源碼中能夠被訪問到的範圍。全局變量屬於進程做用域,在整個進程中都可訪問,靜態變量屬於文件做用域,在當前文件中能夠訪問。局部變量屬於代碼塊做用域,從定義開始的代碼塊範圍內能夠訪問,該代碼塊能夠是整個函數、循環體中、甚至花括號內。因爲全部局部變量均處於函數棧中,而彙編級別不存在高級語言的做用域,而在整個函數範圍內都可訪問,所以若是須要還原局部變量在高級語言的範圍,能夠分析引用該變量的代碼塊,例如僅僅在循環體中引用到該變量而其他代碼未涉及,則可認爲該變量做用域爲該循環體。以上討論的是非靜態局部變量的狀況,對於靜態局部變量,其做用域也處於代碼塊中,然而在彙編級別,因爲須要保持相對不變,所以位於全局變量區,而不存在棧中。所以靜態局部變量的查找方式和做用域相似全局變量。

4.4.4 識別函數返回值類型

  對於普通類型返回,編譯器會根據函數返回類型大小不一樣進行不一樣的操做。

返回長度 返回方式 返回類型
1字節 AL寄存器 按值返回
2字節 AX寄存器 按值返回
4字節 EAX寄存器 按值返回
8字節 EDX:EAX寄存器 按值返回
浮點型 協處理器堆棧或者EAX寄存器 按引用/值返回
雙精度型 協處理器堆棧或者EDX:EAX寄存器 按引用/值返回
近指針 EAX寄存器 按值返回
3字節/5字節/6字節/7字節或多於8字節 引用方式的隱含參數 按引用返回

4.4.5 斷定函數邊界

  在一些時候IDA採用反彙編算法會產生函數範圍誤判、沒法識別甚至和數據混淆的狀況,對於這種狀況須要進行人工干預。對於函數起始位置和終止位置的判斷能夠採用如下方式進行試探。

  • 對於起始位置,若出現下面狀況之一,則須要試探該處是否爲新函數起始位置:
    • 該位置處爲函數序言。
    • 該位置處爲某個遠跳轉jmp指令或call指令的操做數。
    • 該位置鄰接於2個函數之間的未識別區域。
    • 該位置處於代碼區未識別區域且以前爲對齊字節。 *對於結束位置,若出現下面狀況之一,則須要試探該處是否爲當前函數的結束位置:
    • 該位置處爲函數結語且以後不存在函數結語。
    • 該位置以後臨接某函數起始位置。

第四章 分析程序中的C++語言元素

  C++機制是在C語言基礎上構建的,因此在實現時藉助了C語言的實現方法。下面兩個等式從本質上形象地描述了C++語言的主要元素構成方式:

  • 類=數據結構+方法
  • 對象=分配的內存+數據+方法

  本章介紹了C++區別於C語言的高級語言特性和實現原理,並提出了切實可行的恢復方法,其中包括對new和delete操做符的實現原理進行了分析;對通常類類型的對象內存佈局原理進行總結;基於內存對象佈局理論提出了一種恢復類結構(包括成員變量和成員函數)的方法,該方法能夠用於重建類和識別程序中建立的對象;分析了SEH實現機制和32/64位Windows程序異常處理結構;基於SEH實現機制、C++異常處理底層實現原理和RTTI設計原理提出了Windows程序異常處理語句恢復方法,該方法支持32/64位Windows程序中異常處理語句的恢復。

5.1 識別new和delete操做符

  MSVC的new操做符在編譯時是以庫函數和類構造函數實現的,內部調用operator new函數,該函數接受一個參數,爲申請的空間大小(對象大小),operator new函數會調用malloc函數,而malloc函數調用Windows API函數HeapAlloc,返回分配指針。在代碼中放置了new函數之後,爲防止內存分配失敗,二進制代碼中首先會檢測該地址是否爲空,若爲空則直接返回空對象,不然使用該地址做爲this指針,傳遞給類構造函數執行,若構造函數執行成功,則會將地址返回。編譯器在遇到new和delelte操做符時,會將它們轉化成函數調用。
  相似的,delete操做符是以庫函數和類析構函數實現的,編譯代碼首先使用this指針執行類析構函數以後執行delete函數。delete函數接受一個地址參數,最終調用Windows API函數HeapFree實現分配內存釋放。

5.2 識別類

  類是C++面向對象機制的基礎,所以瞭解編譯器對類的處理機制十分重要。類佈局由虛函數表(簡稱虛表)、虛基類表(簡稱虛基表)、成員變量構成。

5.2.1 編譯器對類及類實例的處理行爲

  類成員變量一般按照聲明順序在內存中分配,和類的成員變量域相同的結構體僅僅是沒有成員函數。下面以包含2個成員變量a1,a2的基類A爲例說明編譯器一般在實現中採用的類佈局。
  簡單繼承中,派生類的成員變量在內存中的位置位於基類成員變量以後,這是大多數知名C++廠商採用的內存安排,這樣的好處是,派生類獲取基類指針時不須要計算偏移量,由於派生類對象地址同時基類指針。在單繼承類層次下,每一個新派生類都簡單地把成員變量添加到基類成員變量以後,若是派生類既不重寫也不增長新的虛函數,那麼父類虛表能夠重用。以類B爲例,B繼承於A且有一個b3成員變量。
  大多數狀況下簡單繼承對於編程已經足夠,然而C++爲特殊緣由也支持多重繼承,若是當前對象同時兼有多個互斥對象的特性,須要對多個基類作交集,這時候要使用多重繼承。內存中的佈局是基類在先,派生類在後,與單繼承相同的是,類C拷貝了類A和類B的全部數據,不一樣的是,類C的指針和類A相同和類B不一樣。以類C爲例,C依次繼承於A和B,且有一個c4成員變量。
  在多重繼承中,若派生類繼承的基類也繼承於同一個原始基類,若是該原始類成員較多,通過拷貝後每一個基類都會含有相同的成員,而派生類進行繼承就會產生較大資源浪費和內存開銷,同時實例中原本相同的成員能夠分別進行修改而不是共享關係形成數據不一致,爲了解決這個問題出現了虛繼承,在虛繼承中繼承的相同成員變量是共享關係,只有一份實例。虛繼承中,虛基類的相對位置是不固定的,可能會根據派生類而不一樣。
  編譯器須要跟蹤每一個繼承的虛基類的基址偏移,這部分在MSVC經過生成虛基類表vbtable實現從而實現間接計算虛基類位置的目的,該表存儲的是相對該類的每一個虛基類表指針與虛基類之間的偏移量,而GCC作法也較爲類似,它會將該偏移存放在虛函數表(vftable)中,也就是說MSVC中的虛函數和虛基類使用的是不一樣的表(虛表和虛基表),而GCC中則是都寫在虛函數表中的。以類D和類E爲例,D虛繼承於A,且有一個成員d5,E依次虛繼承於A、繼承於B,且有一個成員e6。

MSVC GCC
const D::vbtable
dd 0//類D基址偏移
dd 8//類A基址偏移
const D::vftable
dd 8
dd 0
dd offset//類D typeinfo結構偏移
dd 0
const E::vbtable
dd 0//類E基址偏移
dd 0CH//A基址偏移
const E::vftable
dd 0CH
dd 0
dd offset//E的typeinfo結構偏移
dd 0

  若虛繼承的類自己是虛繼承的,則類佈局中會有多個虛基類表指針,對於虛繼承以及繼承的基類是虛繼承的狀況,下面的類佈局順序在MSVC系列編譯器中成立:

  • 首先排列非虛繼承的基類實例,若是該基類存在重複繼承的成分會將這些成員捨去。
  • 有虛基類時,爲每一個基類增長一個隱藏的vbptr,除非已經從非虛繼承的類那裏繼承了一個vbptr。
  • 排列派生類的新數據成員。
  • 最後排列每一個虛基類的一個實例。

  菱形繼承是另外一類較爲複雜的對象結構,會將單一繼承和多重繼承進行組合,所以菱形繼承能夠很好地用來觀察類佈局。假設類A爲基類,有成員a1,x,類B和類C分別虛繼承於類A,同時類B和類C各有成員b1,x和c1,類D依次繼承於類B和類C,且有成員d1,x。

5.2.2 識別RTTI信息

  RTTI存儲了豐富的類型,能夠給逆向分析C++的類結構帶來極大幫助,同時若是使用了面向對象的異常處理機制,編譯器也會產生相應的RTTI信息。可見了解RTTI結構十分必要。對於有虛函數的類,其佈局中會產生虛表,而虛表所在地址以前的一個機器字長大小的元素,存放着該類的一種稱爲「RTTI徹底對象定位定位符」(RTTI Complete Object Locator)的結構體指針。它是一種用於描述類繼承關係的結構體。該結構包含兩個指針,該結構以下:

+0x00  ULONG signature;//結構標誌
+0x04  ULONG offset;//對象內存中該類偏移
+0x08  ULONG cdOffset;//RTTI類型描述符(RTTI Type Descriptor)指針
+0x18  ULONG pTypeDescriptor;//RTTI類繼承描述符指針(RTTI Class Hierarchy Descriptor)
其中RTTI類型描述符在C++程序中以type_info類實現,該結構以下:
+0x00  ULONG _vfptr;//type_info類虛表指針
+0x04  ULONG spare;
+0x08  CHAR  name;//通過名稱粉碎和重修飾的類名
而RTTI類繼承描述符記錄了類的繼承信息,其結構以下:
+0x00  ULONG signature;//結構標誌
+0x04  ULONG attributes;//繼承類型,虛繼承或多重繼承
+0x08  ULONG numBaseClasses;//基類個數
+0x0C  ULONG pBaseClassArray;

  其中pBaseClassArray是指向基類描述符數組,該數組每一個元素指向每一個基類的RTTI基類描述符結構體,該結構體中一個成員指向該基類的type_info結構,從這些結構很容易肯定出全部類之間的關係。

5.2.3 識別不一樣類型的對象

  對象也有做用域,不一樣做用域的對象生命週期不一樣,所以構造函數被調用的時機也不一樣,若是能夠從二進制代碼中分析出對象構造函數和析構函數的調用時機,那麼就能夠推知該對象的做用域類型以及生命週期。對象按做用域類型能夠分爲下面幾種: 局部對象:和棧變量相似,棧變量均在函數入口處統一分配空間,對象也相同,然而對象的構造函數是在做用域(塊)開始位置調用的,析構函數是在做用域(塊)結束位置調用的。識別局部對象的構造函數的必要條件有兩個:該函數是這個對象調用的第一個函數;該函數返回this指針。

  • 堆對象:和堆變量相似,要點在於堆空間的申請、使用和釋放。C++中對象的堆空間申請使用new操做符,在Windows環境下,編譯器解析new操做符時先對所要new的對象進行sizeof操做獲得對應退化的結構體大小,以後將該參數傳遞給operator new函數執行,該函數接受一個表示申請空間大小的整形參數,new函數中會創建一些結構體以方便動態內存的管理,最終會調用Windows API函數HeapAlloc申請系統堆空間,在此時HeapAlloc接受的申請大小已再也不是new函數所傳入的大小,而是通過附加結構體和進行內存對齊以後的新大小,所以識別庫函數new十分關鍵。相應地,堆對象的釋放使用delete操做符,最終使用Windows API函數HeapFree釋放申請空間。
  • 參數對象:對於對象做爲參數傳遞的狀況,是一種局部對象的特殊狀況,編譯器會在默認狀況下調用拷貝構造函數(該拷貝構造函數接受一個參數爲對象引用)構造出一個做用域爲子函數的新對象傳參,同時將該新對象析構函數調用設置在子函數結束處,由於構造形式產生的類進行傳遞從語法上沒法供父函數其它地方使用。在傳遞時,對象會退化爲結構體方式傳遞,該結構體如前所述,主要包含虛表指針和成員變量域,函數體內所引用的this指針就是每一個結構體在棧上的首字節位置。
  • 返回對象:對於函數返回對象的狀況,也是局部對象的特殊狀況,能夠看作返回結構體的操做,若是該對象在函數體內聲明並返回,那麼在子函數結束處會執行析構函數回收對象,此時該對象在不該該在函數外使用。而對於經過構造函數直接返回對象或返回對象引用的狀況,在這種狀況下的返回對象在父函數引用,一樣會調用拷貝構造函數構造出一個做用域爲父函數的臨時對象,所以編譯器會在父函數結束前調用前析構函數。
  • 全局對象和靜態對象:兩者構造時機相同,程序中全部全局對象在同一處統一初始化,對於MSVC,此位置位於_cinit的_initterm這個運行庫函數中。可見只要找到該函數所要處理的構造函數地址,就能夠找到全局對象和靜態對象。類似地,全局對象和靜態對象的析構函數處理位於atexit函數中。

5.2.4 識別類構造函數和析構函數

  構造函數和析構函數是類的重要組成部分。類構造函數和析構函數都是可選的。構造函數在類實例化對象時分配空間以後自動調用,是對象第一個被調用的函數,用來初始化類,在高級語言語法中,構造函數是禁止設置返回類型的,然而在彙編級別的實現中,老是返回傳進來的this指針,並能夠接受多個參數。根據C++標準,構造函數不自動激活異常,即便對象內存分配失敗。大多數編譯器在調用構造函數以前會放置檢查空指針的代碼,內存分配成功後,纔會執行構造函數,而對象的其它函數即便在內存分配不成功的狀況下也會被調用,而若是此時this指針爲空,那麼對象首次調用的非構造函數將可能觸發一個異常。根據上述原理可知,以檢查空指針代碼做爲函數結尾的函數多是構造函數。在最壞狀況下,構造函數會依次執行以下操做:

  • 對於最終派生類,初始化vbptr成員變量,調用虛基類構造函數(遞歸過程)
  • 調用非虛基類構造函數
  • 若存在虛函數則初始化虛表
  • 調用成員變量的構造函數
  • 執行初始化的一系列操做,包括虛函數表成員變量
  • 執行構造函數的列表初始化元素
  • 執行構造函數體和用戶初始化代碼

  在編譯器執行代碼優化後,上面的步驟可能順序會被打亂,而且有些函數進行了內聯操做(例如構造函數和析構函數)。 對於全局對象,其構造過程在啓動代碼中實現。通常的方式是使用編譯器生成的函數表調用構造函數,構造函數的內存存於數據段,在這個步驟中編譯器會設法在程序結束前調用類析構函數,MSVC會將析構函數添加到atexit()回調中,而GCC則會使用一種析構函數表(全局對象)完成該操做。對象數組的構造,對象數組的每一個元素會分別建立,若是任何元素的構造函數拋出異常,全部前面構造的元素都會析構,數組析構時,每一個元素都要正確釋放,即便數組大小不能肯定也必須成功完成該操做。在這個過程當中MSVC使用了一種稱爲向量構造迭代器(vector constructor iterator)的輔助函數完成該操做。析構函數和構造函數類似,可是是無參函數,C++規定只在內存分配成功而且建立了對象的狀況下才調用析構函數,所以析構函數代碼中也會放置檢查空指針代碼。MSVC編譯器會自動生成異常處理結構以保證異常發生時對象能夠被銷燬。與構造函數不一樣的是,類只能有一個析構函數,而可能有多個重載的構造函數,而析構函數通常設置爲虛函數以實現資源自動回收釋放機制,所以構造函數不會出如今虛表中,而析構函數每每出如今虛表中。析構函數執行的操做和構造函數恰好相反,在最壞狀況下析構函數會依次執行以下操做(若是有虛函數則初始化虛虛函數表指針及成員變量(這樣操做之後函數體裏的虛函數調用會使用當前類的方法):

  • 執行析構函數體中,程序定義的其餘析構代碼
  • 調用成員變量的析構函數(與構造順序相反)
  • 調用直接非虛基類的析構函數(與構造順序相反)
  • 對於最終派生類,調用虛基類析構函數(與構造順序相反)

  因爲簡單的析構函數可能會在編譯器優化期間內聯,所以常常能夠在彙編代碼中見到虛表指針在一個函數屢次加載的狀況。在MSVC中有虛基類的類構造函數接受一個隱藏的「最終派生類」標誌決定虛基類是否須要初始化。MSVC採用分層析構模型,在析構代碼中加入了一個隱藏的析構函數用於析構包含虛基類的類(對於「最終派生類」而言);代碼中再加入另外一個虛構函數用於析構不包含虛基類的類同時前者調用後者。
  對於不一樣繼承類,虛析構函數可能結構不一樣,編譯器須要保證在不知道指針類型的狀況下進行正確的操做,所以MSVC使用了一種輔助函數(deleting析構函數)存放在虛表中替代實際析構函數,它會調用實際的析構函數,而後執行delete操做。而GCC則使用了多重析構函數(in-charge、not-in-charge和incharge-deleting函數),並經過調用相應的多重析構函數進行操做。 通常在沒有顯式定義構造函數時,在下面兩種狀況下編譯器會提供默認構造函數:

  • 1)本類、本類中定義的成員對象或者父類中有虛函數存在。因爲要初始化虛表,所以編譯器須要追加默認構造函數以完成虛表的隱式初始化操做。
  • 2)父類或本類中定義的成員對象有構造函數。因爲要先構造父類後構造自身,而調用父類構造函數的行爲須要默認構造函數完成。 在構造函數和析構函數較爲簡單時,編譯器一般會直接之內聯函數對待。

5.2.5 識別建立對象實例行爲

  C++面向對象思想和高級特性是以C爲藍本實現的,對象從本質上講就是含有屬性(成員變量)、事件和方法(成員函數)的動態結構體,而類則是包含函數、靜態數組(包含虛函數表、虛基類表、成員變量域)的具備保護屬性(如public,private,protected,friend)的混合體。保護屬性只在編譯級別由編譯器語法檢查來維護,而在底層類佈局中,基類全部成員不管保護屬性如何都會被派生類繼承。對象實例和結構體實例最大的不一樣在於對象實例會使用this指針。經過this指針能夠分析出對象大小。 全局(靜態)對象在編譯期間被分配到數據段中,所以通常不會出現內存分配失敗。爲了實現構造函數只能調用一次的條件,一般編譯器會使用一個初值false的全局變量標誌,在首次調用構造函數時將該值置true。在類對象被使用時,先判斷該標誌是否爲false,若是不爲false就跳過構造函數語句。全局析構函數一般在_atexit之類的運行庫函數內順序進行註冊,並在程序結束前由doexit函數倒序進行調用。

5.2.6 識別類成員函數

  識別非虛函數:在調用類成員函數調用以前,一般會先獲取到對象實例的this指針以便函數在須要的時候使用。所以若是在彙編級流程中已經分析出某函數中使用了this指針就能夠利用該信息分析和使用到該變量的代碼,經過函數調用識別出某個類成員。 識別虛函數和成員變量:若是找到了類構造函數就能夠找到該類的虛函數表地址,通常來講虛函數表與靜態變量和全局變量存放在數據段的不一樣地方,虛函數表中存在的虛函數和成員變量便容易分析出來。純虛函數在函數表中是以指向庫函數__purecall的指針代替。編譯期間會作出語法限制,純虛函數要求繼承後纔可調用,通過繼承虛函數表中的__purecall被替換成相應的繼承類虛函數地址,所以通常不會生成對純虛函數的調用,若是運行時遇到了純虛函數調用,程序會出現一個異常並終止運行;在發行版中,純虛函數一般會被優化掉。通常地,能夠經過下面特徵識別虛函數:

  • 1)類中隱式定義了一個數據成員(虛表指針),該成員位於對象首地址處
  • 2)構造函數將該數據成員初始化爲某個數組首地址,該地址位於常量數據區,數組內每一個元素都是函數指針。
  • 3)數組函數被調用時第一個參數爲this指針,函數內部會對this指針作間接引用。

  所以,對於類虛函數識別,最終歸結於對構造函數和析構函數的識別,如前所述。在這兩個函數中,均會對虛表進行初始化。另外對於this指針的識別,若是發現某個寄存器在父函數調用子函數以前被賦值,而在子函數中該寄存器未經初始化直接引用的,則應判斷該處是否存儲了this指針。若是找到了初始化this指針的行爲,那麼就能夠找到該類的繼承關係。

5.2.7 重建類

  重建類主要是重建類佈局,手工重建類佈局包括兩方面,一方面是經過this指針的引用狀況識別出類成員變量,另外一方面,經過this指針找到構造函數並根據構造函數中設置this指針的行爲找到該類全部虛函數。另外,對於引用this了指針卻不在虛表中的函數,能夠歸併到類的普通成員函數中。因爲對象內存大致上是虛表和類成員,且虛表中的虛函數與類成員均與聲明順序相同,這樣就能夠近似模擬一個和原始高級語言類功能基本相同的類。

5.3 識別異常處理

  異常是對程序運過程當中發生的異常狀況的一種響應。異常能夠是硬件產生的,也能夠是軟件產生的。當異常發生時,系統將程序控制權轉交給異常處理代碼,例如在32位Windows系統中,FS寄存器的零偏移處存儲着線程相關的結構體,經過該結構體能夠獲得異常處理函數地址。異常流程一般包括三個部分:引起異常、捕獲異常、處理異常,經常使用於建立對象、文件I/O操做中。異常處理機制因爲其執行順序的複雜性常常用於軟件保護和代碼混淆機制中,所以對異常機制的分析是必要的。這裏以MSVC爲例說明編譯器如何利用Windows下SEH機制產生異常代碼,MSVC支持三種類型的異常處理,包括C++異常處理、結構化異常處理、MFC異常處理。

5.3.1 C++異常處理

  C++標準規定了異常處理的語法,各編譯廠商都要遵循這些語法,可是因爲C++標準沒有規定異常處理的實現過程所以不一樣廠商編譯器產生的異常處理代碼不一樣,是C++程序經常使用的類型安全的處理方式,用來確保函數結束的棧解退(stack unwinding)過程當中對象析構函數被正常調用。棧解退是這樣一種過程:函數因爲出現異常而非因返回而終止,則程序會釋放函數棧內存,可是不會釋放到當前函數返回地址(正常函數調用指令會將CALL指令的下一條指令地址壓棧)而結束,而是繼續釋放多級函數棧直到找到一個位於try塊中的返回地址,以後控制權轉到該異常處理程序。和函數返回同樣,該操做會調用類的析構函數,然而函數返回僅處理當前函數在棧中的對象,而異常處理語句則處理try塊和throw之間整個函數調用序列中存在的對象。
  C++異常中使用的關鍵字有try,catch和throw。try塊用於監視異常,一個try塊後能夠跟隨多個catch塊,每一個catch塊用於捕獲一種異常,catch異常聲明語句是省略號表明捕獲判斷以前未捕獲的任何類型的異常,包括C類型異常和系統和程序產生的異常。throw表達式用來拋出各類表達式形式的異常。一般在捕獲時能夠指定標準庫中定義的std::exception類及其派生的類做爲異常捕獲類型,同時,C++還容許從該類派生自定義類。
  MSVC的異常處理機制創建於SEH機制之上,在處理C++異常時會在具備異常處理的函數入口處註冊一個異常回調函數,該函數將一種異常信息結構體(FuncInfo)壓棧並調用庫函數__CxxFrameHandler處理該異常。拋出異常採用庫函數__CxxThrowException完成,該函數接受的兩個參數分別是產生異常的對象指針和異常信息結構體(ThrowInfo)指針。異常回調函數在得到執行權後會獲得這兩個參數以及FuncInfo表結構地址,根據異常類型進行try塊匹配操做,若是匹配失敗則析構異常對象並返回繼續搜索的信號;若是找到對應try塊則經過ThrowInfo表結構的類型遍歷查找匹配catch塊,以後進行棧解退和析構對象操做直到到達try所在函數,進而執行catch塊。C++異常處理機制經過下面的指令序列完成當前函數中SEH鏈的構造和異常回調處理例程的註冊:

push ebp; 
push trylevel;__try的層數
push handler_address;異常處理函數地址
push large fs:0;當前SEH鏈地址入棧
mov large fs:0,esp;SEH鏈增長新元素

  異常處理函數中會先將FuncInfo壓棧,該結構體偏移0x10處指向一種TryBlockMapEntry結構體,結構以下:

+0x00  DWORD tryLow;try塊的最小狀態索引,用於範圍檢測
+0x04  DWORD tryHigh;try塊的最大狀態索引,用於範圍檢測
+0x08  DWORD catchHight;catch塊的最高狀態索引,用於範圍檢測
+0x0C  DWORD dwCatchCount;catch塊的個數
+0x10  _msRttiDscr* pCatchHandlerArray;catch塊描述

  _msRttiDscr結構體中存儲了每一個catch語句所捕獲類型的RTTI描述,以及catch塊的首地址,根據這些信息即可以恢復出C++異常部分的源代碼。在C++程序中,應該使用C++異常處理而應避免使用結構化異常處理,雖然SEH能夠用於多種語言,然而使用C++自己的異常處理更靈活,能夠處理任何異常類型,使程序更好地移植。

5.3.2 32位程序 SEH結構化異常處理

  Windows系統特有的異常處理機制,適合在C語言程序設計中使用。當進程沒法從硬件和軟件異常中恢復時,結構化異常處理機制會顯示出相應錯誤信息並記錄下進程內部狀態用於診斷軟件缺陷。這個機制對於不可複製的缺陷極爲有用,在Windows程序設計中也很常見。編譯器的SEH機制是創建在操做系統SEH機制之上的,在SEH中有三種形式的異常處理方式,包括異常處理(Exception handler)、終止處理(Termination Handler)和向量化異常處理(vectored exception handler)。其中異常處理是使用try-except語句;終止處理是使用try-finally語句;向量化異常處理是使用API調用AddVectoredExceptionHandler註冊處理函數,RemoveVectoredExceptionHandler註銷處理函數。

try-except語句語法
__try compound-statement
__except (expression) compound-statement

  __try語句用於監視異常,__except用於異常處理,觸發過程爲:首先執行try塊語句,若是過程當中無異常發生,則執行__except語句以後的語句。若是執行過程當中發生了異常或者被監視語句調用的任何子程序中發生了異常,程序會計算expression表達式決定如何處理異常.

try-finally語句語法爲:
__try compound-statement
(__leave)
__finally compound-statement

  __try語句用於監視異常,__finally語句用做在監視代碼退出後執行的特定操做,該操做不管監視代碼是否因異常而退出都會在執行。觸發過程爲:首先執行__try語句,若產生異常則控制權轉到__finally,若是未發生異常則監視語句執行完畢後控制權轉到__finally語句中(不管如何都會進入__finally,即便使用了goto語句),__finally中的複合語句執行完畢後執行以後的語句。__leave關鍵字在try-finally語句塊中是合法的,用來跳出try塊直接執行__finally語句。Windows爲註冊異常回調函數定義了一種特殊結構體EXCEPTION_REGISTRATION,該結構體以鏈表形式相互鏈接成SEH鏈,結構以下:

+0x00  struct EXCEPTION_REGISTRATION* Prev//前一個結構指針
+0x04  DWORD							Handler;//異常處理例程地址
+0x08  struct SCOPETABLE_ENTRY*		scopetable;//異常處理做用域表
+0x0C  int								trylevel;//try層數
+0x10  int								_ebp;//函數序言ebp 
+0x14  PEXCEPTION_POINTERS			xpointers;

  相應地,在32位Windows程序中,能夠經過下面的指令序列完成當前函數中SEH鏈的構造和異常回調處理例程的註冊:

push ebp;若是使用了SEH那麼必定會有函數序言部分且被添加到函數序言
push trylevel;__try的層數
push scopetable;指向scopetable表的指針,描述異常處理的做用域
push handler_address;異常處理函數地址
push large fs:0;當前SEH鏈地址入棧
mov large fs:0,esp;SEH鏈增長新元素

  32位Windows程序中,每一個線程都有本身的異常回調函數,TEB結構體記載了線程的全部信息,該結構體能夠經過FS:找到,其第一個成員爲指向EXCEPTION_REGISTRATION結構體的指針,結構如上所述。其中Handler成員指向一個運行庫函數,該函數用來從SEH鏈向前索引獲得做用域適合該異常的第一個異常回調。其中存儲於SCOPETABLE_ENTRY結構體的HandlerFunc域爲用戶異常處理代碼,而FilterFunc域用作異常類型過濾函數,一般SCOPETABLE_ENTRY結構隨Handler變化。
  當statement1的代碼塊中產生異常後,系統查找當前線程TEB結構,從中讀取出異常結構EXCEPTION_REGISTRATION,使用結構中的運行庫函數根據SEH鏈搜索適合處理該做用域的第一個異常處理,若是搜到就進行過濾函數的判斷以及執行相應的用戶代碼。當含有異常處理的代碼所在函數退出時,會將棧上的原始FS:(這一步包含在構建新SEH鏈中)覆蓋現有FS:,從而完成異常處理回調例程的註銷。
  相綜上所述,對於32位Windows程序,恢復SEH代碼的步驟爲:首先查找函數序言的代碼,若是有對fs:處的操做就是在引用SEH,須要查看以前的壓棧操做,根據上述結構找到用戶異常處理代碼塊,包括__except的過濾函數和執行塊以及__finally執行塊。對於__try塊的肯定,每次該塊代碼執行以前,一般會將以前壓棧的trylevel設置爲0,而在執行以後會當即將該值設置爲-1;trylevel的做用極其重要,能夠用來分析多重異常佈局狀況下的異常語句恢復。

5.3.3 64位程序SEH結構化異常處理

  32位程序異常處理的實現須要藉助函數棧,這種方式存在兩個弱點,首先異常信息存儲於棧上,容易被棧緩衝區溢出利用。其次,異常狀況通常出現的次數並很少,可是每次執行函數都須要爲使用SEH而初始化相關變量和棧,形成指令冗餘。64位程序異常處理提供基於表的SEH(32位程序是基於棧幀的SEH)以解決上述兩個問題,在源碼編譯成可執行代碼後,編譯器會爲該PE文件生成一種表存放於PE頭部用於異常處理,該表存儲了全部描述模塊異常代碼的信息。異常發生時,Windows系統會解析該表,根據執行函數找到合適的異常處理函數。
  Windows 64位程序採用PE32+文件格式,該格式是PE格式的一種改進形式。該格式中的.pdata段的ExceptionDir目錄結構中存在一種異常表,存儲了全部具備異常檢測功能的函數信息,該結構存儲了大量RUNTIME_FUNCTION結構體數組,該結構體結構以下:

+0x00  ULONG BeginAddress;//異常處理所在函數起始地址
+0x04  ULONG EndAddress;//異常處理所在函數結束地址
+0x08  ULONG UnwindData;//異常結構信息

  其中UnwindData是指向異常結構信息的地址,該地址處爲包含一個UNWIND_INFO信息頭以及若干數量的UNWIND_CODE結構體,該數量由UNWIND_INFO的CountOfCodes決定,該結構體存儲了該函數中的異常處理信息,包括try塊所包圍的代碼起始和終止位置。
  和32位SEH同樣,編譯器會提供一個庫函數用於處理異常,這個函數(__C_specific_handler)的地址存放於UNWIND_INFO的handler_address中,而variable域,在try_finally語句下,會生成flag=11的結構體,該結構體分別存放:try塊指令起始地址偏移、try塊指令結束地址偏移、finally語句指令偏移。而try_except語句下則會生成flag=9的結構體,該結構體分別存放:try塊指令起始地址偏移、except語句指令偏移、過濾函數地址偏移。根據這些信息足以定位異常代碼。相應地,對於C++類型異常,會產生flag=3的結構體,處理異常的庫函數一樣爲__CxxFrameHandler,該結構體較爲複雜,其中包含每一個catch所捕獲類型的RTTI信息(爲前述type_info結構體)、每一個catch的函數序言起始指令偏移及中間體代碼(除去函數序言和函數結語部分)起始指令偏移及函數結語起始指令偏移。根據這些信息就能夠還原出原始異常捕獲代碼。
  在PE32+文件中異常表RUNTIME_FUNCTION數組是根據函數起始地址排序的,當異常發生時,當前線程的全部現場信息都會由操做系統存儲在一種context記錄中,以後系統觸發異常派遣功能,重複執行如下步驟:

  • 1)使用存放在context記錄中的RIP搜索符合當前執行函數RUNTIME_FUNCTION表項使其知足(BedinAddress<RIP<EndAddress)。
  • 2)若是未發現表項則說明處於葉函數中,此時直接返回RSP上存儲的函數返回指針,同時爲了模擬函數返回過程,RSP會相應加8,以後重複第1步。
  • 3)若是找到函數表項,此時RIP可能位於函數序言、函數結語或中間代碼部分。若位於前二者中,則因爲沒法處理異常而重複執行第1步。若處於中間代碼部分且表項分配了合法的異常處理函數,則會調用該函數,若是該函數沒法處理該異常,則直接進行棧解退。
  • 4)異常處理函數已處理結果,則程序使用原始context記錄繼續執行。若是異常處理函數返回「繼續搜索」狀態,則程序須要進行棧解退到context記錄處於上級函數調用者爲止。

第五章 逆向模型的測試

  本章經過實例演示瞭如何利用文中提出的逆向分析模型,藉助分析工具,對Windows下通常應用軟件進行逆向分析的整個流程,輸入二進制可執行代碼,經過分析文件類型、查找程序入口、分析C/C++語言元素、分析函數功能、分析算法,輸出軟件設計流程、算法和文檔,在這個過程當中合理協調軟件與人工分析共同完成軟件逆向工做,例證了該模型的正確性和可用性。

6.1 使用代碼分析工具輔助分析

  IDA是用來作手工分析的輔助工具。IDA反彙編的時間與程序代碼段大小有關,分爲兩個階段,第一階段將代碼與數據分開,標記各個函數和符號並分析參數調用、參數棧、局部變量棧、跳轉等指令關係,並分析數據結構,自動生成流程圖和模塊調用關係。第二階段識別出文件編譯類型信息並加載對應特徵庫,這部分主要經過FLIRT技術(Fast Library Identification and Recognition Technology)實現,該技術能夠經過對比特徵碼自動找出庫函數調用;除此以外IDA的插件擴展性和交互性極強,較好的插件包括C語言僞代碼分析工具Hex-Rays;支持多平臺調試;支持IDC腳本。
  查看PE信息:在去除了程序保護之後,首先須要瞭解文件的類型,這種類型包括兩個方面,一個是該文件的編程語言(C++/C/Delphi/VB/ASM等)和編譯器類型(Delphi、VB、Borland C++等),另外一個是該文件的用途,IDA通常能夠自動根據PE格式獲取文件類型是可執行程序、動態連接庫、靜態連接庫、驅動程序等,而用戶也能夠根據經驗經過查看入口函數部分指令序列判斷程序類型。其次,須要分析文件中使用了哪些API調用或者導出了哪些符號,這部分能夠經過函數導入表和導出表獲得。對於。或者導出IDA能夠列出輸入函數、輸出函數、PE節、字符串表。
  分析程序結構:分離各個PE節,分離用戶代碼和庫函數代碼,IDA提供了PE文件佈局圖,對於PE文件各個段數據,以及庫函數代碼和用戶代碼都作出了區分。紅色的部分是未識別出的函數,須要使用者手工肯定該處數據類型是函數或數據。
  識別庫函數:在這一階段,IDA將使用FLIRT技術識別庫函數,加載並標記特徵庫,並加載相關的數據結構模板。
  識別數據結構:通常來講IDA能夠根據API函數和庫函數的相關信息推導出與之關聯的變量類型,可是對於用戶自定義函數則無能爲力,所以IDA提供了自定義數據結構的方法,包括簡單數據類型、浮點類型、結構體、枚舉類型等。用戶能夠經過直接修改數據類型、自定義結構體、從C語言頭文件導入結構體、從選擇的結構體操做代碼區域推導結構體。對於代碼區隱藏的數據和數據區的結構體,通過上面的操做,能夠以與源代碼所定義數據結構形式最接近的方式呈獻給用戶。用戶能夠手動加載頭文件添加數據結構,同時也能夠自定義數據結構。對於能夠,IDA支持C類型的數據結構,包括簡單數據類型和結構體。
  代碼分析: IDA能夠以反彙編指令、16進制數據、C語言僞代碼、函數關係結構圖四種形式展現代碼分析結果。因爲代碼分析過程會產生錯誤,所以IDA容許用戶手動調整,自定義指令序列或數據部分開始的位置。
  函數識別: 加載PE文件後,IDA會自動分析其中的庫函數和API函數,並將識別出的函數名替換到函數列表中以便查閱。對於未能識別或識別錯誤的代碼,須要進行手工分析其參數類型、參數個數、起始位置、結束位置、調用方式等,並對IDA的相應參數進行修改,必要時還需作堆棧平衡分析和修改。在瞭解函數功能後最好進行註釋,並取符合功能的名稱做爲函數名。

6.2 使用該模型分析C++程序實例

  本文研究已在普通PC機上進行了成功測試,操做系統平臺爲Windows 7 x64 Ultimate,測試工具爲PEID、IDA,測試對象爲Nisoft的AltStreamDump,該軟件用於查找指定目錄下的全部NTFS文件數據流。本文處於研究目的,版權歸原做者全部。

6.2.1 實施逆向模型操做流程

  • 去除軟件保護:使用PEID查看文件信息,發現未加保護,採用MSVC2005編寫。 肯定軟件編寫語言和編譯器:C/C++,和MSVC2005。
  • 分析類型:查看輸入表,發現使用了msvcrt.dll,而由後面分析的入口可知爲使用了C運行時庫的命令行應用層程序。
  • 識別庫函數:因爲使用了運行時庫,所以IDA自動加載的庫函數爲Microsoft Visual C 2-10/net runtime;使用的類型庫爲mssdk和vc6win,無需手工建立和加載它庫。
  • 查找程序入口:使用IDA加載,找到IDA輸出表中的系統入口函數start,接着找到用戶入口,該函數出如今環境變量初始化以後,退出函數以前,能夠發現.text:00401D76 call sub_401914符合要求,該函數調用以前使用了三個壓棧指令,說明須要三個參數,再根據前面_wgetmainargs獲取這三個參數能夠推斷出爲寬字符版本的main函數,其符號名和參數類型爲: int wmain(int argc,wchar_t* argv,wchar_t* envp)
  • 從新標註符號:將前面的sub_401914改成wmain。
  • 分析函數屬性:以wmain函數爲例,其餘函數以此類推。父函數調用該函數前進行了三次壓棧操做,同時在調用後進行了堆棧平衡,所以是_cdecl調用方式。因爲IDA已經識別出該處代碼,函數結尾以後的指令段已經處於其餘函數範圍所以終止位置正確。
  • 分析局部變量:以wmain函數爲例,函數序言部分有指令sub esp,454h,可見局部棧變量使用了少於454h字節的空間,所以須要從函數頭部開始分析棧使用狀況判斷使用了哪些變量及這些變量的做用域,分析結果如圖 6.2所示。
  • 分析函數功能:以分析出的.text:00401004 sub_401004爲例,該函數通過功能識別之後命名爲GetPrivilege,使用的API調用序列爲:
    GetCurrentProcess
    LoadLibraryW 「advapi32.dll」
    OpenProcessToken
    LookupPrivilegeValueW
    AdjustTokenPrivileges
    CloseHandle

      查詢MSDN並進行詳細分析可知該組調用序列爲Windows系統提權的典型功能代碼。

  • 分析函數算法:以分析出的類成員函數RecurseFind爲例。該函數採用回溯法遍歷當前目錄及其全部子目錄,並分析和顯示其中文件的數據流信息,因爲分析過程較繁瑣,流程分析結果如圖 6.3所示。
  • 分析類:以wmain函數(父函數)中的類爲例。在分析多個函數中的代碼時,發現調用不少函數以前都會設置esi寄存器,同時在被調用函數中也在未初始化狀態下直接使用該寄存器,所以能夠假設該寄存器存放的是this指針。以此爲前提,在父函數中能夠發現申請了很大的局部變量空間,對於類成員函數調用的線索,能夠看到父函數最後在.text:00401AC0處有一條指令爲lea esi,[esp+460h+var_448],緊接着調用了子函數sub_401848,並且在該子函數中直接使用了esi,所以認定這個函數爲成員函數(全部使用了this指針的函數均可看作成員函數),同時關注以前esi和var_448的操做狀況,將var_448做爲棧上存儲類的對象空間開始處,對於其結束位置,須要從兩個方面推測,一方面是父函數中該棧上該類空間起始位置以後的第一個其餘變量所在位置,該對象在棧上的結束位置不可能超過這個值,另外根據esi是this指針這個信息從函數列表中全部的子函數中進行搜索,查看對於this指針的最大偏移數,因爲對象並無賦予虛表指針的狀況,所以最大偏移數就能夠假定是臨近最後的成員變量位置。

  根據這種方式綜合分析,獲得程序中的兩個自命名類,一個是用於控制顯示部分的MainClass,另外一個用於文件查找的FindFileClass。這樣就根據this指針肯定了全部成員函數和類成員。另外調用第一個成員函數以前存在對對象成員變量域的賦值操做,這種操做極有多是類構造函數採用了優化而內聯的形式存在於父函數之中,對因而否爲構造函數,能夠經過該類每次出現於內存中是否都執行了構造函數這個本質進行驗證,若是不是則認爲它是通常成員函數,對於析構函數同理。在分析了各個成員變量做用域以及搜索到全部關聯的成員函數後能夠獲得下面的兩個簡單類結構:

  • 修正函數屬性:將全部手工分析出的函數,對於IDA分析其參數類型、參數個數、起始位置、終止位置、調用方式產生錯誤的,在函數列表窗口選擇該函數並右鍵選擇菜單中的修改函數選項。
  • 分析異常處理:因爲在用戶函數中沒有發現中存在fs:相關操做或異常處理相關函數,所以不予考慮。
  • 分析RTTI信息:因爲進行了優化,找不到相應RTTI信息。

6.2.2 重建開發文檔

AltStreamDump v1.05 Copyright©2011-2012 Nir Sofer
系統配置:支持Windows 2000直到Windows7的系統
使用說明:AltStreamDump不須要任何安裝過程或附加dll文件,打開命令提示符窗口就能夠運行該程序。AltSrtreamDump默認顯示當前目錄的文件數據流,您能夠經過使用-f和-d命令行參數查看其餘文件夾的文件數據流。
命令行選項:
-h用於顯示命令行幫助;
-f [Folder Path]用於指定要搜索的目錄;
-d [Subfolders Depth]用於指定要搜索的父目錄深度(0=不搜索子目錄 1=搜索一級子目錄,以此類推)
例子:AltStreamDump.exe –f 「c:\myfolder」 –d 3

  通過逆向分析後,可知該程序是經過調用ntdll.dll中的API函數NtQueryInformationFile獲得文件數據流的。將源代碼採用MSVC6從新編譯,生成的AltStreamDump.exe運。

相關文章
相關標籤/搜索