一系列日益複雜的程式演示了 64 位元 Windows 上的函數掛鉤。
我在自學函數掛鉤如何運作的同時編寫了這些程式。它們可能對其他嘗試做同樣事情的人(或在我忘記這一切如何再次運作後的未來我)有幫助。它們旨在按順序查看。第一次在程式中使用函數時,它將包含在該特定範例程式的 .cpp 檔案中,其名稱帶有下劃線前綴。使用相同函數的任何後續範例都將使用 hooking_common.h 中包含的該函數的副本,以最大限度地減少程式碼重複並保持後面的範例程式足夠小以便仍然易於閱讀。
我已經做了一些工作來清理它們,但是後面的範例仍然有點混亂。需要注意的是,此儲存庫中的最終結果不足以編寫功能齊全的掛鉤庫,但足以開始沿著這條路走。
在運行時,某些專案可能依賴正在建置的其他專案(注入dll需要已經建置了dll),或同時執行(掛鉤目標程式需要它已經在執行)。
所有範例均使用 Visual Studio 2019 (v142) 和 Windows SDK 10.0.17763.0 建置。我不認為這裡有任何與 VS 或 SDK 版本相關的東西,但我在這裡列出它以防萬一。幾乎可以肯定,有些東西是 MSVC 特有的。
最後,最後一個彈跳床範例在 mspaint 中安裝了一個鉤子。我假設在將來的某個時候,對 mspaint 的更新將導致此範例崩潰。在撰寫本文時,mspaint 的當前版本是 1909(作業系統內部版本 18363.1016)。
這些範例分為兩類:使用彈跳床的範例和不使用彈跳床的範例。非蹦床範例僅用於演示在不同情況下將程式流從一個函數重定向到另一個函數。建立彈翻床很複雜,當我試圖弄清楚函數掛鉤如何運作時,首先建立非彈跳床範例非常有幫助。此外,還有 4 個“目標程式”,用於演示如何在不同(已運行)進程中安裝掛鉤的範例。
這些範例中的大多數都會洩漏與掛鉤相關的記憶體。我真的不在乎,因為這些範例只是為了演示掛鉤概念,而且因為這些「洩漏」的分配無論如何都需要存在直到程式終止。
雖然函數掛鉤技術的標準術語似乎沒有太多,但此儲存庫中的程式碼(和自述文件)使用以下術語:
由於這些範例在安裝鉤子時不會建立彈跳床,因此我認為這些函數展示了「破壞性」鉤子,因為原始函數在被鉤子後完全無法使用。
一個使用跳躍指令覆蓋函數起始位元組的小範例,該跳轉指令將程式流程重定向到同一程式中的不同函數。由於沒有建造蹦床,因此此操作具有破壞性,並且原始函數不再可調用。這是儲存庫中唯一的 32 位元範例。
上一個範例的 64 位元版本。在 64 位元應用程式中,函數可能位於記憶體中足夠遠的位置,無法透過 32 位元相對跳轉指令存取。由於沒有 64 位元相對跳轉指令,程式首先建立一個「中繼」函數,其中包含可以到達記憶體中任何位置的絕對 jmp 指令的位元組(並跳到有效負載函數)。安裝在目標函數中的 32 位元跳到此中繼函數,而不是立即跳到有效負載。
提供一個使用先前專案中的技術來掛鉤成員函數而不是自由函數的範例。
與前面的範例略有不同,此程式示範如何透過物件的 vtable 取得該函數的位址來將掛鉤安裝到虛擬成員函數中。沒有其他範例涉及虛擬函數,但我認為它很有趣,可以包含在這裡。
將掛鉤安裝到另一個正在運行的進程中最簡單的範例。此範例使用 DbgHelp 函式庫透過字串名稱在目標進程(A - 具有自由函數的目標)中尋找函數。這是唯一可能的,因為目標程式是在啟用調試符號的情況下建立的。雖然簡單,但此範例比以前的程式稍長一些,因為它引入了大量新函數(用於定位和操作遠端進程)。
此範例示範如何掛鉤另一個程序從 dll 匯入的函數。由於 ASLR 的工作原理,如何取得遠端進程中 dll 函數的位址存在一些細微差別,如此處所示。除此之外,此範例與前一個範例幾乎相同。
此範例示範如何在未由 dll 匯入且不在符號表中的函數中安裝掛鉤(可能是因為遠端進程沒有偵錯符號)。這意味著沒有(簡單)方法可以透過字串名稱查找目標函數。相反,此範例假設您已使用 x64dbg 等反組譯器來取得要掛鉤的函數的相對虛擬位址 (RVA)。該程式使用該 RVA 來安裝鉤子。
與上方類似,不同之處在於此範例使用 dll 注入來安裝有效負載函數,而不是編寫原始機器碼位元組。這更容易使用,因為您的有效負載可以再次用 C++ 編寫。此範例的有效負載包含在專案 08B-DLL-Payload 中。
下面的範例在hook時安裝了trampolines,這表示在安裝hook後程式仍然可以執行目標函數中的邏輯。由於安裝鉤子至少會覆蓋目標函數中的前5個位元組,因此這5個位元組中包含的指令將會被移到trampoline函數中。因此,呼叫trampoline函數有效地執行了目標函數的原始邏輯。
相當於範例#2 的彈跳床安裝。這個例子有點奇怪,因為我想示範如何創建一個彈跳床而不需要使用反彙編引擎。在本例中,目標函數被建立為在開頭具有已知的 5 個位元組指令,因此我們只需將函數的前 5 個位元組複製到彈翻床函數即可。這意味著創建彈跳床非常容易,因為我們知道它的確切大小並且它不使用任何需要修復的相對尋址。如果您正在為一個非常具體的用例編寫一個彈跳床,那麼您可能只需對此進行一些變體即可擺脫困境。
此範例顯示了與上一個範例類似的場景,只不過這次我使用反組譯器(capstone)來取得我們需要從目標函數中竊取的位元組。這使得掛鉤程式碼可以用於任何函數,而不僅僅是我們知道將是簡單情況的函數。實際上,這個範例中發生了很多事情,因為它從目標掛鉤(如前一個)跳到建立通用掛鉤函數。彈翻床必須將相對呼叫/跳躍轉換為使用絕對位址的指令,這使得事情變得更加複雜。這也不是一個 100% 完美的通用掛鉤範例,它會因循環指令而失敗,如果您嘗試使用少於 5 個位元組的指令來掛鉤函數。
基本上與上面相同,只是此範例包含在安裝掛鉤時暫停所有正在執行的執行緒的程式碼。這不能保證在所有情況下都是線程安全的,但它絕對比什麼都不做要安全得多。
這擴展了前兩個範例中使用的掛鉤/彈跳床程式碼,以支援將多個函數重定向到相同有效負載,並允許有效負載函數呼叫安裝在其中的掛鉤的其他函數。
這是第一個在不同進程中安裝掛鉤的彈跳床範例(在本例中為目標應用程式 B - 具有來自 DLL 的免費函數的目標)。所有掛鉤邏輯都包含在 dll 有效負載 13B - Trampoline Imported Func DLL Payload 中。這裡沒有太多新內容,這個範例只是將已經完成的彈跳床掛鉤內容與先前展示的用於掛鉤從 dll 匯入的函數的技術結合。
回購皇冠上的寶石。此範例將 dll 有效負載(14B - Trampoline Hook MSPaint Payload)注入到正在運行的 mspaint 實例中(在運行此實例之前,您必須自行啟動 mspaint)。無論您在 MSPaint 中實際選擇什麼顏色,安裝的掛鉤都會導致畫筆繪製為紅色。老實說,前面的範例中沒有顯示任何內容,看到它在非人為的程式上運行真是太酷了。
在循環中呼叫自由函數的簡單目標應用程式。編譯時包含偵錯資訊。
目標應用程式在循環中呼叫從 dll (B2 - GetNum-DLL) 匯入的自由函數。
在循環中呼叫非虛擬成員函數的目標應用程式。
在循環中呼叫虛擬成員函數的目標應用程式。