Windows on Arm 下的 Inline Hook 简单实现
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,有需要自行去下载。