新聞中心
前言

我們提供的服務(wù)有:成都網(wǎng)站制作、網(wǎng)站設(shè)計、微信公眾號開發(fā)、網(wǎng)站優(yōu)化、網(wǎng)站認證、郾城ssl等。為千余家企事業(yè)單位解決了網(wǎng)站和推廣的問題。提供周到的售前咨詢和貼心的售后服務(wù),是有科學管理、有技術(shù)的郾城網(wǎng)站制作公司
在日常分析使用某個軟件的過程中,如果我們想要去挖掘軟件的漏洞、或者是通過打補丁的方式給軟件增添一些新的功能,抑或是為了記錄下軟件運行過程中被調(diào)用的函數(shù)及其參數(shù),有時候我們需要劫持對某些DLL庫的調(diào)用過程。在一般情況下,如果我們是軟件的開發(fā)者或者該軟件提供源碼下載,那么剛才提到的問題只要對源碼進行一定的修改就可以了,簡直是小菜一碟。但是在更多情況下,我們無從獲取軟件或是庫的源碼,因為他們根本沒有采用源碼發(fā)行的方式。那這樣我們是否就一籌莫展了呢?通過閱讀這篇文章,我會告訴你最流行的“API鉤子”方法是什么,并且會以略微不同的方式展現(xiàn)給大家。
API鉤子
正如上文我們已經(jīng)提到的,劫持DLL最流行的方法被稱作“API鉤子”——一種將庫函數(shù)調(diào)用重定向到你的代碼的技術(shù)。最為流行的API鉤子庫非微軟的 Microsoft Detours (常用于游戲破解)莫屬,并且這個商業(yè)庫被打上的價值標簽已經(jīng)高達9999.95美元(約68999元人民幣)。再舉一個例子,在Dephi語言中有一個庫叫做 madCodeHook,他的商業(yè)價值約為349歐元(約2564元人民幣)。
下面就讓我們來看一看API鉤子的具體實現(xiàn)原理。
對于已經(jīng)加載的DLL庫及對應(yīng)函數(shù),通過在想要鉤取的函數(shù)頭部首字節(jié)打上一個補丁(也叫重寫,個人認為叫覆蓋最為貼切),補丁內(nèi)容為一個JMP指令,像是 JMP NEAR 這樣的形式,轉(zhuǎn)換成16進制就是 E9 xx xx xx xx。如下圖所示:
圖1:被鉤取的函數(shù)前后內(nèi)容示意
當控制權(quán)被傳遞到我們鉤取過的函數(shù)后,通常這時就可以執(zhí)行我們自己想要執(zhí)行的代碼了,執(zhí)行完畢后又會接著運行原函數(shù)然后返回到之前從DLL庫中調(diào)用該函數(shù)的代碼位置。
API鉤子其實會導致一些問題,而問題的來源就在于編譯過的軟件結(jié)構(gòu)和它本身的代碼結(jié)構(gòu)。當我們想要通過鉤子本身來調(diào)用原函數(shù)的時候(通常不加處理情況下會導致一個死循環(huán)),我們必須要創(chuàng)建一個特殊的代碼區(qū)塊來調(diào)用原函數(shù)代碼,這個代碼區(qū)塊有個別稱叫做“蹦床”(個人覺得在國內(nèi)更常被稱為跳板),這樣的話就不用管鉤子本身是否在要調(diào)用的函數(shù)體內(nèi)了。
另外需要說明的是,API鉤子技術(shù)不是萬能的,在受保護的DLL庫中幾乎不可能實現(xiàn)。說得詳細一點就是,比如存在CRC校驗保護的時候,無論是從硬盤上還是內(nèi)存中對庫DLL庫代碼的修改都是不可行的。
還有一點就是,經(jīng)典的API鉤子也不適用于DLL庫導出的“偽函數(shù)”,這里的偽函數(shù)是指導出的變量、類指針等等。因為在這種類型的“函數(shù)”條件下我們根本不可能在原函數(shù)和我們的代碼之間建立一個經(jīng)典的代碼鉤子(事實上根本就沒有函數(shù)可鉤取)。那是不是就無可奈何了呢?上面我們提到的方法是改寫原函數(shù)代碼,而下面要介紹的第二種常見方法就是修改PE導出表。只不過這種方法的局限性很大,遠不如前一種流行,而且只有很少的一部分鉤子庫支持它。
DLL轉(zhuǎn)發(fā)
一種更加有創(chuàng)意但是也更為麻煩的API鉤取方式叫做“DLL”轉(zhuǎn)發(fā),它通過Windows的內(nèi)部機制來實現(xiàn),基本原理就是轉(zhuǎn)發(fā)DLL調(diào)用至其他模塊。
DLL轉(zhuǎn)發(fā)技術(shù)基于“替換表“來實現(xiàn),所以也被稱為“DLL代理”,它可以導出所有的原始庫函數(shù),也可以傳遞所有對庫函數(shù)的調(diào)用——除了我們想要鉤取的那部分函數(shù)。而函數(shù)調(diào)用是被通過一些鮮為人知的Windows機制傳遞給原函數(shù)庫的,這樣我們就可以借此來調(diào)用其他庫函數(shù),裝作他們本來就是存儲在我們使用的API鉤子庫里一樣,但事實上這些代碼被存儲在其他的庫中。弄明白以上這些過程,我們也就不難得知為什么要叫做“DLL轉(zhuǎn)發(fā)”了。
函數(shù)調(diào)用慣例
函數(shù)調(diào)用慣例是一個低等級的用于傳遞函數(shù)參數(shù)和處理函數(shù)調(diào)用返回前的堆棧的方式。很大一部分情況下它取決于編譯時的設(shè)置,并且在大多數(shù)高級編程語言中可以任意選擇函數(shù)調(diào)用的方式,所以兩者任取其一均可。為了讓我們的API鉤子庫正常運行,它的鉤取函數(shù)也必須使用和已經(jīng)被鉤取的函數(shù)相同的調(diào)用慣例。他們只有在二進制情況下相互兼容才不會引發(fā)像堆棧破壞之類的異常。
表1. 函數(shù)調(diào)用慣例
調(diào)用慣例高度依賴于編譯器的默認設(shè)置,比如Delphi默認采用register調(diào)用慣例,C語言默認采用cdecl調(diào)用慣例。
WinAPI函數(shù)(Windows系統(tǒng)函數(shù))默認使用stdcall調(diào)用慣例,所以在調(diào)用之前,函數(shù)的參數(shù)都使用push指令存儲在棧中,然后call指令被執(zhí)行,執(zhí)行完畢后并沒有必要去修正棧指針ESP,因為在stdcall調(diào)用慣例中,棧在函數(shù)返回前是自動修正的。這里值得一提的是,一個很有趣的現(xiàn)象是WinAPI中的有些函數(shù)并不使用stdcall而是C語言的cdecl,cdecl并不將參數(shù)存儲于棧,但棧的修正會在調(diào)用完成后根據(jù)函數(shù)參數(shù)的數(shù)量被編譯器修正。舉一個例子,user32.dll中的一個函數(shù)wsprintfA()(它在C函數(shù)庫中的對應(yīng)是sprintf())就采用cdecl慣例,這種調(diào)用方式是備受推崇的,因為這樣除了編譯器之外沒有人知道究竟傳遞了多少個參數(shù)。
API鉤子實例
作為一個例子,我想讓它盡量簡單易懂一點,只會用到一個測試庫BlackBox.dll,它只導出兩個函數(shù)Sum()和Divide(),想必你已經(jīng)猜到了,第一個函數(shù)的作用是兩個數(shù)的求和,第二個函數(shù)是兩個數(shù)的除法。讓我們假設(shè)我們擁有一個完整的庫文檔,并且清楚地知道這兩個函數(shù)使用的調(diào)用慣例(假設(shè)我們有這個庫的頭文件),而且我們還知道它們各自都使用哪些參數(shù)。在其他情況下我們需要使用逆向工程來獲得這些底層信息。
代碼清單1:
- 6// 該函數(shù)將兩個數(shù)相加并將結(jié)果儲存于Result變量中
- // 成功返回TRUE,失敗返回ERROR
- BOOL __stdcall Sum(int Number1, int Number2, int * Result);
- // 該函數(shù)將兩個數(shù)相除并將結(jié)果儲存于Result變量中
- // 成功返回TRUE,失敗返回ERROR
- BOOL __stdcall Divide(int Number1, int Number2, int * Result);
在我們的樣例庫中,Divide()函數(shù)是有bug的,因為如果除0就會導致程序崩潰(假設(shè)我們的程序并沒有做異常處理),現(xiàn)在我們的目標就是來修補這個漏洞。
代理DLL
為了修補BlackBox.dll中的漏洞,我們接下來需要創(chuàng)建一個中間庫,能夠使Divide()函數(shù)得以有效應(yīng)用而不出現(xiàn)除0異常。該應(yīng)用采用FASM編譯器(波蘭的mr Tomasza Grysztar 創(chuàng)建)的32位匯編器。在下面你會看到帶有精確注釋的樣例庫模板。
代碼清單2:樣例庫的開頭
- -------------------------------------------------
- ; DLL 輸出文件格式
- ;-------------------------------------------------
- format PE GUI 4.0 DLL
- ; DLL 入口點函數(shù)名
- entry DllEntryPoint
- ; 導入的Windows函數(shù)和常數(shù)
- include '%fasm%\include\win32a.inc'
注意源代碼的開頭,你可以在找到輸出文件的類型聲明,并且在頭文件、DLL庫的函數(shù)入口點也可以放置這些代碼。
代碼清單3:未初始化的數(shù)據(jù)段
- ;-------------------------------------------------
- ; 未初始化的數(shù)據(jù)段
- ;-------------------------------------------------
- section '.bss' readable writeable
- ; uchwyt HMODULE oryginalnej biblioteki
- hLibOrgdd ?
可執(zhí)行文件和DLL庫被分割為一個個獨立的部分,他們其中之一是未初始化的數(shù)據(jù)段,這部分并不占用硬盤的空間,僅僅擁作于記錄程序所使用的未初始化變量的整體大小信息。可執(zhí)行文件的段名稱并不重要(它被限制為最多只有8個字符),通常它會被賦以公司合同的名稱。在這個段的聲明中還會定義訪問權(quán)限(如讀、寫、執(zhí)行),但是在FASM編譯器下.bss段的聲明還會為變量創(chuàng)建一個未初始化的段。
代碼清單4:數(shù)據(jù)段
- ;-------------------------------------------------
- ; 初始化的數(shù)據(jù)段
- ;-------------------------------------------------
- section '.data' data readable writeable
- ; 原始庫的名稱
- szDllOrgdb 'BlackBox_org.dll',0
因為原始庫已經(jīng)有了名稱了,所以這里我們重命名一個BlackBox_org.dll(它以ASCII形式存儲于源代碼中,以null結(jié)束),這個庫會在后面用到。
代碼清單5:帶有DLL入口點的代碼段
- ;-------------------------------------------------
- ; 庫的代碼段
- ;-------------------------------------------------
- section '.text' code readable executable
- ;-------------------------------------------------
- ; DLL庫入口點 (DllMain)
- ;-------------------------------------------------
- proc DllEntryPoint hinstDLL, fdwReason, lpvReserved
- moveax,[fdwReason]
- ; DLL library 加載完畢后立即傳遞事件
- cmpeax,DLL_PROCESS_ATTACH
- je_dll_attach
- jmp_dll_exit
- ; 庫已經(jīng)加載
- _dll_attach:
- ; 獲得原始 DLL 庫的句柄
- ; 如果想要調(diào)用原始函數(shù)就會使用
- pushszDllOrg
- call[GetModuleHandleA]
- mov[hLibOrg],eax
- ; 返回 1 說明庫初始化成功
- moveax,1
- _dll_exit:
- ret
代碼段包含所有庫函數(shù)和DLL入口點函數(shù)。這是一個特殊的函數(shù),它在庫加載以后被Windows系統(tǒng)函數(shù)調(diào)用。代碼段需要被標記上可執(zhí)行的標記,以此來告訴操作系統(tǒng)這段內(nèi)存區(qū)域包含可以執(zhí)行的代碼段。如果沒有這樣標記,那么任何想從這塊內(nèi)存區(qū)域執(zhí)行代碼的行為都會以觸發(fā)CPU處理器的DEP(Data Execution Prevention)內(nèi)存保護機制而告終。在初始化函數(shù)內(nèi)部(DllMain),接收到 DLL_PROCESS_ATTACH 事件后我們將使用原始DLL庫名稱來獲得他的句柄,也就是 HMODULE (這樣之后就可以被調(diào)用了)。
代碼清單6:過度優(yōu)化保護
- ; 調(diào)用任何原始庫
- ; BlackBox_org.dll 中的函數(shù), 沒有它FASM編譯器就會
- ; 移除對庫的引用并且不會被自動加載
- calldummy
我們自定義的庫會調(diào)用到原始庫,但是如果我們一點引用也不放在源代碼中,F(xiàn)ASM編譯器會移除所有對它的引用(優(yōu)化)而且原始庫并不會被自動加載,這就是為什么在ret指令后直接放了一個偽調(diào)用的緣故(這樣在任何時候都不會執(zhí)行)。
代碼清單7:有效的Divide()函數(shù)代碼
- ;-------------------------------------------------
- ; 我們修改后能夠處理除0錯誤的Divide() 函數(shù)
- ;-------------------------------------------------
- proc Divide Number1, Number2, Result
- ; 檢查除數(shù)是否為0
- ; 如果是的話返回ERROR代碼
- movecx,[Number2]
- testecx,ecx
- jeDivisionError
- ; 將第一個數(shù)字載入 EAX 處理器
- moveax,[Number1]
- ;擴展 EDX 寄存器來處理有符號數(shù)
- cdq
- ; 現(xiàn)在 EDX:EAX 寄存器對可以處理64位數(shù)據(jù)了
- ; EDX:EAX / ECX 除法的實現(xiàn), 除法在EDX:EAX寄存器對
- ; 上實現(xiàn),就像對待64位數(shù)據(jù)一樣, 除法的結(jié)果保存在EAX
- ; 寄存器中, 余數(shù)保存在EDX 寄存器中
- idiv ecx
- ; 檢查有效的指向結(jié)果的指針
- ; 如果沒有檢測到則返回error 代碼
- movedx,[Result]
- testedx,edx
- jeDivisionError
- ; 在受保護的地址存儲除法的結(jié)果
- mov[edx],eax
- ; 以 exit code TRUE (1) 返回
- moveax,1
- jmpDivisionExit
- ; 除法錯誤,返回FALSE (0)
- DivisionError:
- sub eax,eax
- DivisionExit:
- ; 從除法函數(shù)中返回
- ; 布爾型的exit 代碼被設(shè)置在 EAX 寄存器中
- ret
- endp
修改后的Divide()函數(shù)的實現(xiàn)增添了對除0錯誤的校驗,函數(shù)遇到錯誤會返回錯誤代碼FALSE,另外還額外做了對指向結(jié)果變量result的指針非空檢查,如果指針指向null也會報錯。另外請注意,修改后的函數(shù)的調(diào)用慣例與原函數(shù)是完全一致的,并且在我們的這個例子中使用的是stdcall慣例,所以函數(shù)參數(shù)被傳遞到棧中,函數(shù)返回值儲存于EAX寄存器,棧指針也被FASM編譯器自動修復,方法是根據(jù)源代碼中的ret聲明生成ret (number_of_parameters * 4)指令。
代碼清單8:庫的導入表
- ;-------------------------------------------------
- ; 我們的庫使用的函數(shù)段
- ;-------------------------------------------------
- section '.idata' import data readable writeable
- ; 在代碼中用到的庫的列表
- library kernel,'KERNEL32.DLL',\
- blackbox, 'BlackBox_org.dll'
- ; KERNEL32.dll庫的函數(shù)列表
- importkernel,\
- GetModuleHandleA, 'GetModuleHandleA'
- ; 聲明了原始庫的用途
- ; DLL 庫會被自動加載
- importblackbox,\
- dummy, 'Divide'
FASM編譯器允許我們手動地定義我們自己的庫調(diào)用到的庫和函數(shù),除了標準系統(tǒng)庫,我們需要在這里添加一個對 BlackBox.dll 的引用。多虧于此,當Windows加載我們的鉤子庫的同時也會根據(jù)地址空間加載原始庫,從而無需再手動調(diào)用 LoadLibraryA() 函數(shù)來加載它。 在某些情況下想要使用導入表來加載庫甚至是強制性要求使用 LoadLibraryA() 的,它需要使用多線程應(yīng)用程序中TLS(Thread Local Storage)機制的動態(tài)鏈接庫來支持。
代碼清單9:函數(shù)導出表
- ;-------------------------------------------------
- ; 導出表段包含我們的庫中導出的函數(shù)
- ; 這里我們也許要聲明原始庫中聲明的函數(shù)
- ;-------------------------------------------------
- section '.edata' export data readable
- ; 導出函數(shù)列表及其指針
- export'BlackBox.dll',\
- Sum, 'Sum',\
- Divide, 'Divide'
- ; 轉(zhuǎn)發(fā)表名稱, 首先目的庫被存儲 (無需.DLL擴展)
- ; 然后最終的函數(shù)名稱被存儲
- Sum db 'BlackBox_org.Sum',0
在這個段中我們必須聲明原始庫中的所有函數(shù),而且我們想要鉤取的函數(shù)必須在代碼中得以應(yīng)用,想要傳遞給原始庫的函數(shù)存儲在一個特殊的文本格式中:
DestinationDllLibrary.FunctionName
或
DestinationDllLibrary.#1
以此來順序?qū)牒瘮?shù)而非按照名稱的順序。該機制的所有內(nèi)部工作均交由Windows系統(tǒng)自身處理。以上為DLL轉(zhuǎn)發(fā)。
代碼清單10:重定位部分
- ;-------------------------------------------------
- ; 重定位部分
- ;-------------------------------------------------
- section '.reloc' fixups data discardable
我們的庫中最后一個段是重定位段,它保證了我們的庫能夠正常運行。這是因為動態(tài)鏈接庫被加載的基地址是非常多變的,而引起這個多變性的原因在于指針使用的絕對地址和匯編器的指令使用的絕對地址必須根據(jù)當前內(nèi)存中的基地址做出更新,而這個基地址的信息正是由編譯器在重定位段中生成的。
總結(jié)
這篇API鉤子介紹的方法可以被成功應(yīng)用于各種使用動態(tài)鏈接庫的場合,較傳統(tǒng)的經(jīng)典API鉤子方法而言各有利弊,但是在我看來本文的方法為實踐打開了更大的拓展空間,并提供了一種更加簡單的改變軟件完整功能性的方法。該方法同樣可以在高級語言中以適當?shù)膶С龊瘮?shù)定義文件(DEF)的方式實現(xiàn)。
文章名稱:實踐API鉤子攔截DLL庫調(diào)用
轉(zhuǎn)載源于:http://www.5511xx.com/article/dpsehej.html


咨詢
建站咨詢
