Windows on Arm 下的 Inline Hook 簡單實現
因為一些原因好久沒寫什麼東西了,簡單水一篇,順便蹭一下 X Elite 和 Windows on Arm 的熱度。
之前有分析過 x86 和 x64 的 Inline Hook 基本實現方法,由於手裡有 WoA 裝置(8cx Gen3),今年初我順便研究了一下在 Arm64 架構下的 Hook 方法。這些東西對於研究安卓的人來說可能比較熟悉了,因為安卓在這塊已經是比較成熟了,同樣在 Linux 也是如此,但是 Windows 也算是近幾年才算是有比較大的進展吧。雖然從現在的視角看 X Elite 並不是吹的那麼好,AI PC 還是噱頭大於實際。
和 x86 的 CISC 不同,Arm 架構屬於 RISC 架構,指令都是定長的,儘管現在 CPU 實際上到底是複雜指令集還是精簡指令集已經比較模糊了,但是指令的差別還是受歷史影響比較明顯的。Arm 架構主要有 AArch32 執行態和 AArch64 執行態,其中 AArch32 態還支援 2 位元組的 Thumb 指令,但是我在這裡討論的 WoA 使用 arm64 架構,只執行在 AArch64 態,其 A64 指令集的指令都是固定 4 位元組長的。
在 A64 的跳轉指令中,用的比較多的是 B 和 BR 指令,其中 B 的範圍是前後 128MB,BR 則是任意位置。由於 A64 不像 x86 那樣提供了棧操作的指令,這導致了遠距離無條件跳轉只能藉助暫存器進行了。好在 X16 暫存器就是專門幹這個的,我們透過 LDR 指令從當前位置載入要跳轉的地址到 X16,然後使用 BR X16
就可以跳轉了。兩條指令加上一個地址,一共佔用 16 個位元組的空間。
對於 Hook 系統 API 的情況,從使用者地址空間跳到系統地址空間的距離顯然不是一個 B 指令可以完成的了,所以必須要用 BR。使用這個方法要修改被 Hook 地址的前 4 條指令,這樣明顯不好,我們希望只改一條指令使其跳轉。
和 x64 的情況一樣,我們希望能短跳到最近的一片空白記憶體,然後再二次跳轉到實際的地址。好在 B 指令有 128MB 的跳轉範圍足夠使用了,除非這個 DLL 或者 EXE 的體積超過了 128MB。具體實現思路和方法可以看我之前寫的 Hook 有關 x64 部分的說明。
和 x86、x64 一樣,有可能被 Hook 的地址頭部就是一條跳轉指令,所以我們需要解析這條指令並找到真正的入口,這樣才可以在 Hook 函式中呼叫原始的函式功能。在 AArch64 中,主要有 B 跳轉和 ADRP + LDR + BR 跳轉這兩種。所以需要判斷第一條是 ADRP 的情況下後面是不是跟著 LDR 指令,從而計算實際的地址並直接跳轉過去。
但是 ADRP 開頭的程式碼後面不一定跟 LDR,所以還需要額外判斷,如果 ADRP 只是用來計算一個地址的話,那還需要把這句指令改一下,因為 ADRP 計算的地址是和 PC 相關的。因為我們直接載入用的是絕對地址,所以改成 LDR 指令,具體則是修改成如下的模式:
LDR Xn, .L_Addr
B .L_Next
.L_Addr:
; 8位元組的地址
.L_Next:
; 接下來的指令
這裡 LDR 指令的偏移是 2,B 指令的偏移是 3,實際的機器碼是 0x58000040
和 0x14000003
,其中 LDR 指令的低 5 位表示暫存器,需要設定。
對於程式碼有興趣可以去看看我的 tinyhook,在 GitHub:https://github.com/DrPeaboss/tinyhook。當然我的程式碼實現很簡陋,而且還有很多沒覆蓋到的情況,只處理了幾種常見的情況。
如果要了解 Windows 下各種 CPU 架構 Inline Hook 的成熟程式碼,可以去看看微軟官方的 Detours 庫:https://github.com/microsoft/Detours,為了實現完整的功能,其程式碼是非常複雜且繁瑣的。
參考資料主要是 Arm 的架構手冊,Arm Architecture Reference Manual for A-profile architecture,有需要自行去下載。