基于桥的全量方法 Hook 方案(3)- TrampolineHook

本来以为是双休日,结果五一调休本周末只休一天,懵逼。不过还算完成了承诺,赶了出来。

开源地址:https://github.com/SatanWoo/TrampolineHook

TrampolineHook 是什么

之前杨萧玉在看到我《基于桥的全量方法 Hook 方案(2) - 全新升级》 后就问我这个和直接用 method_exchangeImplementation 之类的 runtime 方法交换 IMP 性能对比咋样?

所以这篇文章开头先占用大家宝贵的两分钟,简要说明下。

TrampolineHook 本质上不是用来 Swizzling 的框架,取 Hook 这个名字只是为了读起来顺口。它实际上是一个中心重定向框架。 换句话说,你可以认为它是为了通过一个函数替换/拦截所有你想要函数的框架。

其实这个中心重定向的思想并不新潮,很多人(包括我自己)在内就曾经利用重载 objc_msgForward 干过这样的事。

但是这个方式我在之前的文章里也提到过对应的缺点,比如:

  • 性能慢
  • 不能替换/拦截同一个继承链上的多个类。

所以可以认为 TrampolineHook 是一个让你不用关注底层架构Calling Convention(因为涉及到汇编),不用关心上下文信息保存、恢复,不用担心引入传统 Swizzle 方案在大型项目中有奇奇怪怪 Crash 问题的中心重定向框架。

TrampolineHook 技术原理

整个技术原理其实可以分为三部分:

  • vm_remap 技术。

  • 流程设计。

  • 汇编实现。

vm_remap 的价值

通俗意义上,我们访问的内存都是按照页来组织。而在程序加载后分配的页之中,会对应有不同的权限,比如代码占用的页,就是可读且可执行,但是一般不具备可写的权限;而存放数据的页呢,就对应是可读且可写,但不能拥有可执行权限。

在绝大多数情况下,当我们编写完一个程序运行的时候,动态分配的页都是用来做数据保存、访问的,不太会有涉及执行权限。

而要做到可以将动态分配出来的内存页具备可执行权限,就需要利用 vm_remap。 它的定义是这样的:

On Darwin, vm_remap() provides support for mapping an existing code page at new address, while retaining the existing page protections; using vm_remap(), we can create multiple copies of existing, executable code, placed at arbitrary addresses.

从定义中我们可以知道两点信息:

  • vm_remap 可以让内存页具备被 map 的页的特性,如果是可执行页被 map,那新创建的页自然而然页具备了这个权限。

  • vm_remap 也不是肆无忌惮的创建任何可执行的页,通俗理解,它只是一个 copy 映射。

上述图片引用自Implementing imp_implementationWithBlock()

因此,我们可以通过在编写代码的过程中,精心构造、预留在程序二进制的代码页,在运行时不断“复制映射”,来完成特殊的使命。

在我们的定义中,我们是构造了连续的两个页

流程设计

要构造特殊的程序二进制代码,首先还是要梳理我们的目的,我们的诉求是所有的函数都能先进入我们的一个中心重定向函数,执行自定义的操作,然后返回原函数,同时这个调用栈不能乱。

  • 把一个我们要替换的原方法 IMP A 取出来,保存起来。
  • 给这个原方法塞一个动态分配的可执行地址 B。
  • 当执行这个原方法的时候,会跳转到 可执行地址 B。
  • 这个 B 经过一段简短的运算操作,可以获取到原先保存的 IMP A。
  • 在跳转回 IMP A 之前,统一拦截函数先做些事情,比如检查是不是主线程调用之类的。

【注意】:在整个过程中,我们要保证参数寄存器、返回地址等不能错乱。

汇编实现

既然 vm_remap 是按页的维度来映射,我们要构造的代码自然而然要页对齐在 arm64 中,一页是 0x4000,也就是 16KB,所以首先就是 .align 14 来确保。

然后上一下最关键部分的代码,感兴趣的还是去 Github 上阅读完整的代码吧。

_th_entry:

// 不要小看这五行汇编
nop
nop
nop
nop
nop

sub x12, lr,   #0x8
sub x12, x12,  #0x4000
mov lr,  x13

ldr x10, [x12]

stp q0,  q1,   [sp, #-32]!
stp q2,  q3,   [sp, #-32]!
stp q4,  q5,   [sp, #-32]!
stp q6,  q7,   [sp, #-32]!

stp lr,  x10,  [sp, #-16]!
stp x0,  x1,   [sp, #-16]!
stp x2,  x3,   [sp, #-16]!
stp x4,  x5,   [sp, #-16]!
stp x6,  x7,   [sp, #-16]!
str x8,        [sp, #-16]!

// 加载自定义的拦截器,并跳转过去。
ldr x8,  interceptor
blr x8

ldr x8,        [sp], #16
ldp x6,  x7,   [sp], #16
ldp x4,  x5,   [sp], #16
ldp x2,  x3,   [sp], #16
ldp x0,  x1,   [sp], #16
ldp lr,  x10,  [sp], #16

ldp q6,  q7,   [sp], #32
ldp q4,  q5,   [sp], #32
ldp q2,  q3,   [sp], #32
ldp q0,  q1,   [sp], #32

br  x10

.rept 2032
mov x13, lr
bl _th_entry;
.endr

整段汇编可以分为几个部分:

  • 设计一大堆的动态可执行地址,即:

    .rept 2032
    mov x13, lr
    bl _th_entry;
    .endr
    

    这里最早我的实现是复制粘贴一大堆重复性代码,在 HookZZ 作者的指导下,我优化成了上述这样。

  • 执行统一的运行过程,通过偏移计算等方式获取保留的原始 IMP。

  • 要注意特定的寄存器用处,x8-x18是临时寄存器,里面的值在函数调用后可能被修改,这些寄存器为caller-saved。所以在我们自身函数可以用,但是要在调用别的函数之前保存好。

  • 要特别注意对 LR 寄存器的处理,没处理好,调用栈就回不去了。

  • 保存对应的参数、浮点参数等寄存器,避免上下文被我们自己的处理函数破坏。

  • b / bl 的跳转范围非常有限,由于我们是动态地址分配,不能保证拦截函数的范围偏移,所以要采用 blr 的方式。

TrampolineHook 用处

和传统的 Swizzle 需要提供对应的替换后的函数实现不同,中心化重定向思想可以帮助你实现很多有意思的事情:

  • 比如网上很常见的 hook objc_msgSend,可以帮你查看任意被 Hook 二进制中的函数耗时和调用链路。

  • 比如 Bang / AnyMethodLog 这样的重定向 Log 日志框架等等。

苹果著名的 MainThreadChecker 也用了类似的技术。由于我才疏学浅,只是大致完成了对其实现的逆向,通过 TrampolineHook 进行了重写。 因为效果还不错,所以也开源了出来,地址是:https://github.com/SatanWoo/TrampolineHook/tree/master/Example/MainThreadChecker

这次在重写 MainThreadChecker 的过程中,我也对比了下和 2017 年苹果实现的差异。在整体流程上没有比较大的差异,但是还是有一些细节可以分享分享:

  • iOS 10 的时候对应的二进制是 UIKit,到了 iOS 12/13 成了 UIKitCore,所以原先获取二进制的逻辑失效了,为了避免后续版本的变更干扰,我采用了苹果自身的守候,通过 class_getImageName([UIResponder class]) 来保证获取的就是我们理解上的 UIKit 动态库。

当然 TrampolineHook 的作用不止于此,争取过段时间把我的一些想法做完善再和大家交流。

后续思考

本质上 Trampolinevm_remap 技术不是新的技术,很早就有人应用了,构造 Trampoline 实际上在苹果自身关于 Block 的实现中就有。业界也有 SwiftTrace 也是用了对应的技术。

真正的关键在于你用 Trampoline 做什么?用途的不同也决定了效果的不同,这也是我把之前的代码重写 TrampolineHook 中所收获的,而且随着 TrampolineHook 相对我自身之前实现的优化,我发现眼前豁然开朗,能玩的事情还有很多,哈哈。

对了,如果有朋友对 arm64 的汇编比较熟悉,同时对函数调用也比较了解的话,会很快的发现我上述提供的汇编代码存在一个漏洞(虽然这个漏洞绝大多数人用不到),感兴趣的朋友可以微信交流下。

开源地址:https://github.com/SatanWoo/TrampolineHook 如果大家有什么想法或者遇到了自身项目中的 Bug,欢迎 issue。