Swift Hook 新思路 -- 虚函数表

开发 前端
本文的技术方案仅针对通过虚函数表调用的函数进行 Hook,不涉及直接地址调用和objc_msgSend 的调用的情况。

[[390109]]

摘要:业界对Swift 的 Hook 大多都需要依靠 OC 的消息转发特性来实现,本文从修改 Swift 的虚函数表的角度,介绍了一种新的 Hook 思路。并以此为主线,重点介绍 Swift 的详细结构以及应用。

1. 前言

由于历史包袱的原因,目前主流的大型APP基本都是以 Objective-C 为主要开发语言。

但是敏锐的同学应该能发现,从 Swift 的 ABI 稳定以后,各个大厂开始陆续加大对 Swift 的投入。

虽然在短期内 Swift 还难以取代 Objective-C,但是其与 Objective-C 并驾齐驱的趋势是越来越明显,从招聘的角度就即可管中窥豹。

在过去一年的招聘过程中我们总结发现,有相当数量的候选人只掌握 Swift 开发,对Objective-C 开发并不熟悉,而且这部分候选人大多数比较年轻。

另外,以 RealityKit 等新框架为例,其只支持 Swift 不支持 Objective-C。上述种种现象意味着随着时间的推移,如果项目不能很好的支持 Swift 开发,那么招聘成本以及应用创新等一系列问题将会凸显出来。

因此,58 同城在 2020 年 Q4 的时候在集团内发起了跨部门协同项目,从各个层面打造 Objective-C 与 Swift 的混编生态环境——项目代号 ”混天“。

一旦混编生态构建完善,那么很多问题将迎刃而解。

2. 原理简述

本文的技术方案仅针对通过虚函数表调用的函数进行 Hook,不涉及直接地址调用和objc_msgSend 的调用的情况。

另外需要注意的是,Swift Compiler 设置为 Optimize for speed(Release默认)则TypeContext 的 VTable 的函数地址会清空。

设置为 Optimize for size 则 Swfit 可能会转变为直接地址调用。

以上两种配置都会造成方案失效。因此本文重点在介绍技术细节而非方案推广。

如果 Swift 通过虚函数表跳表的方式来实现方法调用,那么可以借助修改虚函数表来实现方法替换。即将特定虚函数表的函数地址修改为要替换的函数地址。但是由于虚函数表不包含地址与符号的映射,我们不能像 Objective-C 那样根据函数的名字获取到对应的函数地址,因此修改 Swift 的虚函数是依靠函数索引来实现的。

简单理解就是将虚函数表理解为数组,假设有一个 FuncTable[],我们修改函数地址只能通过索引值来实现,就像 FuncTable[index] = replaceIMP 。但是这也涉及到一个问题,在版本迭代过程中我们不能保证代码是一层不变的,因此这个版本的第 index 个函数可能是函数 A,下个版本可能第 index 个函数就变成了函数 B。显然这对函数的替换会产生重大影响。

为此,我们通过 Swift 的 OverrideTable 来解决索引变更的问题。在 Swift 的OverrideTable 中,每个节点都记录了当前这个函数重写了哪个类的哪个函数,以及重写后函数的函数指针。

因此只要我们能获取到 OverrideTable 也就意味着能获取被重写的函数指针 IMP0 以及重写后的函数指针 IMP1。只要在 FuncTable[] 中找到 IMP0 并替换成 IMP1 即可完成方法替换。

接下来将详细介绍Swift的函数调用、TypeContext、Metadata、VTable、OverrideTable 等细节,以及他们彼此之间有何种关联。为了方便阅读和理解,本文所有代码及运行结果,都是基于 arm64 架构

3. Swift 的函数调用

首先我们需要了解 Swift 的函数如何调用的。与 Objective-C 不同,Swift 的函数调用存在三种方式,分别是:基于 Objective-C 的消息机制、基于虚函数表的访问、以及直接地址调用。

▐ 3.1 Objective-C 的消息机制

首先我们需要了解在什么情况下 Swift 的函数调用是借助 Objective-C 的消息机制。如果方法通过 @objc dynamic 修饰,那么在编译后将通过 objc_msgSend 的来调用函数。

假设有如下代码

  1. class MyTestClass :NSObject { 
  2.     @objc dynamic func helloWorld() { 
  3.         print("call helloWorld() in MyTestClass"
  4.     } 
  5.  
  6. let myTest = MyTestClass.init() 
  7. myTest.helloWorld() 

编译后其对应的汇编为

  1. 0x1042b8824 <+120>: bl     0x1042b9578               ; type metadata accessor for SwiftDemo.MyTestClass at <compiler-generated> 
  2. 0x1042b8828 <+124>: mov    x20, x0 
  3. 0x1042b882c <+128>: bl     0x1042b8998               ; SwiftDemo.MyTestClass.__allocating_init() -> SwiftDemo.MyTestClass at ViewController.swift:22 
  4. 0x1042b8830 <+132>: stur   x0, [x29, #-0x30] 
  5. 0x1042b8834 <+136>: adrp   x8, 13 
  6. 0x1042b8838 <+140>: ldr    x9, [x8, #0x320] 
  7. 0x1042b883c <+144>: stur   x0, [x29, #-0x58] 
  8. 0x1042b8840 <+148>: mov    x1, x9 
  9. 0x1042b8844 <+152>: str    x8, [sp, #0x60] 
  10. 0x1042b8848 <+156>: bl     0x1042bce88               ; symbol stub for: objc_msgSend 
  11. 0x1042b884c <+160>: mov    w11, #0x1 
  12. 0x1042b8850 <+164>: mov    x0, x11 
  13. 0x1042b8854 <+168>: ldur   x1, [x29, #-0x48] 
  14. 0x1042b8858 <+172>: bl     0x1042bcd5c               ; symbol stub for

从上面的汇编代码中我们很容易看出调用了地址为0x1042bce88的objc_msgSend 函数。

▐ 3.2 虚函数表的访问

虚函数表的访问也是动态调用的一种形式,只不过是通过访问虚函数表的方式进行调用。

假设还是上述代码,我们将 @objc dynamic 去掉之后,并且不再继承自 NSObject。

  1. class MyTestClass { 
  2.     func helloWorld() { 
  3.         print("call helloWorld() in MyTestClass"
  4.     } 
  5.  
  6. let myTest = MyTestClass.init() 
  7. myTest.helloWorld() 

汇编代码变成了下面这样👇

  1. 0x1026207ec <+120>: bl     0x102621548               ; type metadata accessor for SwiftDemo.MyTestClass at <compiler-generated> 
  2. 0x1026207f0 <+124>: mov    x20, x0 
  3. 0x1026207f4 <+128>: bl     0x102620984               ; SwiftDemo.MyTestClass.__allocating_init() -> SwiftDemo.MyTestClass at ViewController.swift:22 
  4. 0x1026207f8 <+132>: stur   x0, [x29, #-0x30] 
  5. 0x1026207fc <+136>: ldr    x8, [x0] 
  6. 0x102620800 <+140>: adrp   x9, 8 
  7. 0x102620804 <+144>: ldr    x9, [x9, #0x40] 
  8. 0x102620808 <+148>: ldr    x10, [x9] 
  9. 0x10262080c <+152>: and    x8, x8, x10 
  10. 0x102620810 <+156>: ldr    x8, [x8, #0x50] 
  11. 0x102620814 <+160>: mov    x20, x0 
  12. 0x102620818 <+164>: stur   x0, [x29, #-0x58] 
  13. 0x10262081c <+168>: str    x9, [sp, #0x60] 
  14. 0x102620820 <+172>: blr    x8 
  15. 0x102620824 <+176>: mov    w11, #0x1 
  16. 0x102620828 <+180>: mov    x0, x11 

从上面汇编代码可以看出,经过编译后最终是通过 blr 指令调用了 x8 寄存器中存储的函数。至于 x8 寄存器中的数据从哪里来的,留到后面的章节阐述。

▐ 3.3 直接地址调用

假设还是上述代码,我们再将 Build Setting 中Swift Compiler - Code Generaation -> Optimization Level 修改为 Optimize for Size[-Osize],汇编代码变成了下面这样👇

  1. 0x1048c2114 <+40>:  bl     0x1048c24b8               ; type metadata accessor for SwiftDemo.MyTestClass at <compiler-generated> 
  2. 0x1048c2118 <+44>:  add    x1, sp, #0x10             ; =0x10  
  3. 0x1048c211c <+48>:  bl     0x1048c5174               ; symbol stub for: swift_initStackObject 
  4. 0x1048c2120 <+52>:  bl     0x1048c2388               ; SwiftDemo.MyTestClass.helloWorld() -> () at ViewController.swift:23 
  5. 0x1048c2124 <+56>:  adr    x0, #0xc70c               ; demangling cache variable for type metadata for Swift._ContiguousArrayStorage<Any

这是大家就会发现bl 指令后跟着的是一个常量地址,并且是 SwiftDemo.MyTestClass.helloWorld() 的函数地址。

4. 思考

既然基于虚函数表的派发形式也是一种动态调用,那么是不是以为着只要我们修改了虚函数表中的函数地址,就实现了函数的替换?

5. 基于 TypeContext 的方法交换

在往期文章《从 Mach-O 角度谈谈 Swift 和 OC 的存储差异》我们可以了解到在Mach-O 文件中,可以通过 __swift5_types 查找到每个 Class 的ClassContextDescriptor,并且可以通过 ClassContextDescriptor 找到当前类对应的虚函数表,并动态调用表中的函数。

注意:(在 Swift 中,Class/Struct/Enum 统称为 Type,为了方便起见,我们在文中提到的TypeContext 和 ClassContextDescriptor 都指的是 ClassContextDescriptor)。

首先我们来回顾下 Swift 的类的结构描述,结构体 ClassContextDescriptor 是 Swift 类在Section64(__TEXT,__const) 中的存储结构。

  1. struct ClassContextDescriptor{ 
  2.     uint32_t Flag; 
  3.     uint32_t Parent; 
  4.     int32_t  Name
  5.     int32_t  AccessFunction; 
  6.     int32_t  FieldDescriptor; 
  7.     int32_t  SuperclassType; 
  8.     uint32_t MetadataNegativeSizeInWords; 
  9.     uint32_t MetadataPositiveSizeInWords; 
  10.     uint32_t NumImmediateMembers; 
  11.     uint32_t NumFields; 
  12.     uint32_t FieldOffsetVectorOffset; 
  13.     <泛型签名> //字节数与泛型的参数和约束数量有关 
  14.     <MaybeAddResilientSuperclass>//有则添加4字节 
  15.     <MaybeAddMetadataInitialization>//有则添加4*3字节 
  16.     VTableList[]//先用4字节存储offset/pointerSize,再用4字节描述数量,随后N个4+4字节描述函数类型及函数地址。 
  17.     OverrideTableList[]//先用4字节描述数量,随后N个4+4+4字节描述当前被重写的类、被重写的函数描述、当前重写函数地址。 

从上述结构可以看出,ClassContextDescriptor 的长度是不固定的,不同的类 ClassContextDescriptor 的长度可能不同。那么如何才能知道当前这个类是不是泛型?以及是否有 ResilientSuperclass、MetadataInitialization 特征?其实在前一篇文章《从Mach-O 角度谈谈 Swift 和 OC 的存储差异》中已经做了说明,我们可以通过 Flag 的标记位来获取相关信息。

例如,如果 Flag 的 generic 标记位为 1,则说明是泛型。

  1. |  TypeFlag(16bit)  |  version(8bit) | generic(1bit) | unique(1bit) | unknow (1bi) | Kind(5bit) | 
  2. //判断泛型 
  3. (Flag & 0x80) == 0x80 

那么泛型签名到底能占多少字节呢?Swift 的 GenMeta.cpp 文件中对泛型的存储做了解释,整理总结如下:

  1. 假设有泛型有paramsCount个参数,有requeireCount个约束 
  2.  
  3. /** 
  4.      16B  =  4B + 4B + 2B + 2B + 2B + 2B 
  5.      addMetadataInstantiationCache -> 4B 
  6.      addMetadataInstantiationPattern -> 4B 
  7.      GenericParamCount -> 2B 
  8.      GenericRequirementCount -> 2B 
  9.      GenericKeyArgumentCount -> 2B 
  10.      GenericExtraArgumentCount -> 2B 
  11.  */ 
  12.  short pandding = (unsigned)-paramsCount & 3; 
  13.  泛型签名字节数 = (16 + paramsCount + pandding + 3 * 4 * (requeireCount) + 4); 

因此只要明确了 Flag 各个标记位的含义以及泛型的存储长度规律,那么就能计算出虚函数表 VTable 的位置以及各个函数的字节位置。

了解了泛型的布局以及 VTable 的位置,是不是就意味着能实现函数指针的修改了呢?答案当然是否定的,因为 VTable 存储在 __TEXT 段,__TEXT 是只读段,我们没办法直接进行修改。不过最终我们通过 remap 的方式修改代码段,将 VTable 中的函数地址进行了修改,然而发现在运行时函数并没有被替换为我们修改的函数。那到底是怎么一回事呢?

6. 基于 Metadata 的方法交换

上述实验的失败当然是我们的不严谨导致的。在项目一开始我们先研究的是类型存储描述 TypeContext,主要是类的存储描述 ClassContextDescriptor。在找到 VTable 后我们想当然的认为运行时 Swift 是通过访问 ClassContextDescriptor 中的 VTable 进行函数调用的。但是事实并不是这样。

7. VTable 函数调用

接下来我们将回答下 Swift的函数调用 章节中提的问题,x8 寄存器的函数地址是从哪里来的。还是前文中的 Demo,我们在 helloWorld() 函数调用前打断点

  1. let myTest = MyTestClass.init() 
  2. ->  myTest.helloWorld() 

断点停留在 0x100230ab0 处👇

  1. 0x100230aac <+132>: stur   x0, [x29, #-0x30] 
  2. 0x100230ab0 <+136>: ldr    x8, [x0] 
  3. 0x100230ab4 <+140>: ldr    x8, [x8, #0x50] 
  4. 0x100230ab8 <+144>: mov    x20, x0 
  5. 0x100230abc <+148>: str    x0, [sp, #0x58] 
  6. 0x100230ac0 <+152>: blr    x8 

此时 x0 寄存器中存储的是 myTest 的地址 x0 = 0x0000000280d08ef0,ldr x8, [x0] 则是将 0x280d08ef0 处存储的数据放入 x8(注意,这里是只将 *myTest 存入 x8,而不是将 0x280d08ef0 存入 x8)。单步执行后,通过 re read 查看各个寄存器的数据后会发现 x8 存储的是 type metadata 的地址,而不是 TypeContext 的地址。

  1. x0 = 0x0000000280d08ef0 
  2. x1 = 0x0000000280d00234 
  3. x2 = 0x0000000000000000 
  4. x3 = 0x00000000000008fd 
  5. x4 = 0x0000000000000010 
  6. x5 = 0x000000016fbd188f 
  7. x6 = 0x00000002801645d0 
  8. x7 = 0x0000000000000000 
  9. x8 = 0x000000010023e708  type metadata for SwiftDemo.MyTestClass 
  10. x9 = 0x0000000000000003 
  11. x10= 0x0000000280d08ef0 
  12. x11= 0x0000000079c00000 

经过上步单步执行后,当前程序要做的是 ldr x8, [x8, #0x50],即将 type metadata + 0x50 处的数据存储到 x8。这一步就是跳表,也就是说经过这一步后,x8 寄存器中存储的就是 helloWorld() 的地址。

  1.     0x100230aac <+132>: stur   x0, [x29, #-0x30] 
  2.     0x100230ab0 <+136>: ldr    x8, [x0] 
  3. ->  0x100230ab4 <+140>: ldr    x8, [x8, #0x50] 
  4.     0x100230ab8 <+144>: mov    x20, x0 
  5.     0x100230abc <+148>: str    x0, [sp, #0x58] 
  6.     0x100230ac0 <+152>: blr    x8 

那是否真的是这样呢?ldr x8, [x8, #0x50] 执行后,我们再次查看 x8,看看寄存器中是否为函数地址👇

  1. x0 = 0x0000000280d08ef0 
  2. x1 = 0x0000000280d00234 
  3. x2 = 0x0000000000000000 
  4. x3 = 0x00000000000008fd 
  5. x4 = 0x0000000000000010 
  6. x5 = 0x000000016fbd188f 
  7. x6 = 0x00000002801645d0 
  8. x7 = 0x0000000000000000 
  9. x8 = 0x0000000100231090  SwiftDemo`SwiftDemo.MyTestClass.helloWorld() -> () at ViewController.swift:23 
  10. x9 = 0x0000000000000003 

结果表明 x8 存储的确实是 helloWorld() 的函数地址。上述实验表明经过跳转0x50 位置后,程序找到了 helloWorld() 函数地址。类的 Metadata 位于__DATA 段,是可读写的。其结构如下:

  1. struct SwiftClass { 
  2.     NSInteger kind; 
  3.     id superclass; 
  4.     NSInteger reserveword1; 
  5.     NSInteger reserveword2; 
  6.     NSUInteger rodataPointer; 
  7.     UInt32 classFlags; 
  8.     UInt32 instanceAddressPoint; 
  9.     UInt32 instanceSize; 
  10.     UInt16 instanceAlignmentMask; 
  11.     UInt16 runtimeReservedField; 
  12.     UInt32 classObjectSize; 
  13.     UInt32 classObjectAddressPoint; 
  14.     NSInteger nominalTypeDescriptor; 
  15.     NSInteger ivarDestroyer; 
  16.     //func[0] 
  17.     //func[1] 
  18.     //func[2] 
  19.     //func[3] 
  20.     //func[4] 
  21.     //func[5] 
  22.     //func[6] 
  23.     .... 
  24. }; 

上面的代码在经过0x50 字节的偏移后正好位于 func[0] 的位置。因此要想动态修改函数需要修改Metadata中的数据。

经过试验后发现修改后函数确实是在运行后发生了改变。但是这并没有结束,因 为虚函数表与消息发送有所不同,虚函数表中并没有任何函数名和函数地址的映射,我们只能通过偏移来修改函数地址。

比如,我想修改第1个函数,那么我要找到 Meatadata,并修改 0x50 处的 8 字节数据。同理,想要修改第 2 个函数,那么我要修改 0x58 处的 8 字节数据。这就带来一个问题,一旦函数数量或者顺序发生了变更,那么都需要重新进行修正偏移索引。

举例说明下,假设当前 1.0 版本的代码为

  1. class MyTestClass { 
  2.     func helloWorld() { 
  3.         print("call helloWorld() in MyTestClass"
  4.     } 

此时我们对 0x50 处的函数指针进行了修改。当 2.0 版本变更为如下代码时,此时我们的偏移应该修改为 0x58,否则我们的函数替换就发生了错误。

  1. class MyTestClass { 
  2.     func sayhi() { 
  3.         print("call sayhi() in MyTestClass"
  4.     } 
  5.  
  6.     func helloWorld() { 
  7.         print("call helloWorld() in MyTestClass"
  8.     } 

为了解决虚函数变更的问题,我们需要了解下 TypeContext 与 Metadata 的关系。

8. TypeContext 与 Metadata 的关系

Metadata 结构中的 nominalTypeDescriptor 指向了 TypeContext,也就是说当我们获取到 Metadata 地址后,偏移 0x40 字节就能获取到当前这个类对应的 TypeContext地址。那么如何通过 TypeContext 找到 Metadata 呢?

我们还是看刚才的那个 Demo,此时我们将断点打到 init() 函数上,我们想了解下 MyTestClass 的 Metadata 到底是哪里来的。

  1. ->  let myTest = MyTestClass.init() 
  2. myTest.helloWorld() 

此时展开为汇编我们会发现,程序准备调用一个函数。

  1. ->  0x1040f0aa0 <+120>: bl     0x1040f16a8               ; type metadata accessor for SwiftDemo.MyTestClass at <compiler-generated> 
  2.     0x1040f0aa4 <+124>: mov    x20, x0 
  3.     0x1040f0aa8 <+128>: bl     0x1040f0c18               ; SwiftDemo.MyTestClass.__al 

在执行 bl 0x1040f16a8 指令之前,x0 寄存器为 0。

  1. x0 = 0x0000000000000000 

此时通过 si 单步调试就会发现跳转到了函数 0x1040f16a8 处,其函数指令较少,如下所示👇

  1. SwiftDemo`type metadata accessor for MyTestClass: 
  2. ->  0x1040f16a8 <+0>:  stp    x29, x30, [sp, #-0x10]! 
  3.     0x1040f16ac <+4>:  adrp   x8, 13 
  4.     0x1040f16b0 <+8>:  add    x8, x8, #0x6f8            ; =0x6f8  
  5.     0x1040f16b4 <+12>: add    x8, x8, #0x10             ; =0x10  
  6.     0x1040f16b8 <+16>: mov    x0, x8 
  7.     0x1040f16bc <+20>: bl     0x1040f4e68               ; symbol stub for: objc_opt_self 
  8.     0x1040f16c0 <+24>: mov    x8, #0x0 
  9.     0x1040f16c4 <+28>: mov    x1, x8 
  10.     0x1040f16c8 <+32>: ldp    x29, x30, [sp], #0x10 
  11.     0x1040f16cc <+36>: ret 

在执行 0x1040f16a8 函数执行完后,x0 寄存器就存储了 MyTestClass 的 Metadata 地址。

  1. x0 = 0x00000001047e6708  type metadata for SwiftDemo.MyTestClass 

那么这个被标记为 type metadata accessor for SwiftDemo.MyTestClass at 的函数到底是什么?

在上文介绍的 struct ClassContextDescriptor 貌似有个成员是 AccessFunction,那这个 ClassContextDescriptor 中的 AccessFunction 是不是 Metadata 的访问函数呢?这个其实很容易验证。

我们再次运行 Demo,此时metadata accessor 为 0x1047d96a8,继续执行后Metadata地址为 0x1047e6708。

  1. x0 = 0x00000001047e6708  type metadata for SwiftDemo.MyTestClass 

查看 0x1047e6708,继续偏移 0x40 字节后可以得到 Metadata 结构中的 nominalTypeDescriptor 地址 0x1047e6708 + 0x40 = 0x1047e6748。

查看 0x1047e6748 存储的数据为 0x1047df4a0。

  1. (lldb) x 0x1047e6748 
  2. 0x1047e6748: a0 f4 7d 04 01 00 00 00 00 00 00 00 00 00 00 00  ..}............. 
  3. 0x1047e6758: 90 90 7d 04 01 00 00 00 18 8c 7d 04 01 00 00 00  ..}.......}..... 

ClassContextDescriptor 中的 AccessFunction 在第 12 字节处,因此对 0x1047df4a0 + 12 可知 AccessFunction 的位置为 0x1047df4ac。继续查看 0x1047df4ac 存储的数据为

  1. (lldb) x 0x1047df4ac 
  2. 0x1047df4ac: fc a1 ff ff 70 04 00 00 00 00 00 00 02 00 00 00  ....p........... 
  3. 0x1047df4bc: 0c 00 00 00 02 00 00 00 00 00 00 00 0a 00 00 00  ................ 

由于在 ClassContextDescriptor 中,AccessFunction 为相对地址,因此我们做一次地址计算 0x1047df4ac + 0xffffa1fc - 0x10000000 = 0x1047d96a8,与 metadata accessor 0x1047d96a8 相同,这就说明 TypeContext 是通过 AccessFunction 来获取对应的Metadata的地址的。

当然,实际上也会有例外,有时编译器会直接使用缓存的 cache Metadata 的地址,而不再通过 AccessFunction 来获取类的 Metadata。

9. 基于 TypeContext 和 Metadata 的方法交换

在了解了 TypeContext 和 Metadata 的关系后,我们就能做一些设想了。在 Metadata中虽然存储了函数的地址,但是我们并不知道函数的类型。这里的函数类型指的是函数是普通函数、初始化函数、getter、setter 等。

在 TypeContext 的 VTable 中,method 存储一共是 8 字节,第一个4字节存储的函数的 Flag,第二个4字节存储的函数的相对地址。

  1. struct SwiftMethod { 
  2.     uint32_t Flag; 
  3.     uint32_t Offset; 
  4. }; 

通过 Flag 我们很容易知道是否是动态,是否是实例方法,以及函数类型 Kind。

  1. |  ExtraDiscriminator(16bit)  |... | Dynamic(1bit) | instanceMethod(1bit) | Kind(4bit) | 

Kind 枚举如下👇

  1. typedef NS_ENUM(NSInteger, SwiftMethodKind) { 
  2.     SwiftMethodKindMethod             = 0,     // method 
  3.     SwiftMethodKindInit               = 1,     //init 
  4.     SwiftMethodKindGetter             = 2,     // get 
  5.     SwiftMethodKindSetter             = 3,     // set 
  6.     SwiftMethodKindModify             = 4,     // modify 
  7.     SwiftMethodKindRead               = 5,     // read 
  8. }; 

从 Swift 的源码中可以很明显的看到,类重写的函数是单独存储的,也就是有单独的OverrideTable。

并且 OverrideTable 是存储在 VTable 之后。与 VTable 中的 method 结构不同,OverrideTable 中的函数需要 3 个 4 字节描述:

  1. struct SwiftOverrideMethod { 
  2.     uint32_t OverrideClass;//记录是重写哪个类的函数,指向TypeContext 
  3.     uint32_t OverrideMethod;//记录重写哪个函数,指向SwiftMethod 
  4.     uint32_t Method;//函数相对地址 
  5. }; 

也就是说 SwiftOverrideMethod 中能够包含两个函数的绑定关系,这种关系与函数的编译顺序和数量无关。

如果 Method 记录用于 Hook 的函数地址,OverrideMethod 作为被Hook的函数,那是不是就意味着无论如何改变虚函数表的顺序及数量,只要 Swift 还是通过跳表的方式进行函数调用,那么我们就无需关注函数变化了。

为了验证可行性,我们写 Demo 测试一下:

  1. class MyTestClass { 
  2.     func helloWorld() { 
  3.         print("call helloWorld() in MyTestClass"
  4.     } 
  5. }//作为被Hook类及函数 
  6.  
  7. <---------------------------------------------------> 
  8.  
  9. class HookTestClass: MyTestClass  { 
  10.     override func helloWorld() { 
  11.         print("\n********** call helloWorld() in HookTestClass **********"
  12.         super.helloWorld() 
  13.         print("********** call helloWorld() in HookTestClass end **********\n"
  14.     } 
  15. }//通过继承和重写的方式进行Hook 
  16.  
  17. <---------------------------------------------------> 
  18.    
  19. let myTest = MyTestClass.init() 
  20.  myTest.helloWorld() 
  21.  
  22.  //do hook 
  23.  print("\n------ replace MyTestClass.helloWorld() with   HookTestClass.helloWorld() -------\n"
  24.  
  25.  WBOCTest.replace(HookTestClass.self); 
  26.  
  27.  //hook 生效 
  28.  myTest.helloWorld() 

运行后,可以看出 helloWorld() 已经被替换成功👇

  1. 2021-03-09 17:25:36.321318+0800 SwiftDemo[59714:5168073] _mh_execute_header = 4368482304 
  2. call helloWorld() in MyTestClass 
  3.  
  4. ------ replace MyTestClass.helloWorld() with HookTestClass.helloWorld() ------- 
  5.  
  6.  
  7. ********** call helloWorld() in HookTestClass ********** 
  8. call helloWorld() in MyTestClass 
  9. ********** call helloWorld() in HookTestClass end ********** 

10. 总结

本文通过介绍 Swift 的虚函数表 Hook 思路,介绍了 Swift Mach-O 的存储结构以及运行时的一些调试技巧。Swift 的 Hook 方案一直是从 Objective-C 转向 Swift 开发的同学比较感兴趣的事情。我们想通过本文向大家介绍关于 Swift 更深层的一些内容,至于方案本身也许并不是最重要的,重要的是我们希望是否能够从中 Swift 的二进制中找到更多的应用场景。比如,Swift 的调用并不会存储到 classref 中,那如何通过静态扫描知道哪些 Swift 的类或 Struct 被调用了?其实解决方案也是隐含在本文中。

 

责任编辑:姜华 来源: Swift 社区
相关推荐

2022-07-18 15:32:37

C++虚函数表

2010-01-18 17:38:54

C++虚函数表

2011-09-01 11:12:02

Restaurant 美食应用餐饮应用

2009-12-03 10:32:21

2017-01-23 11:18:16

戴尔

2022-05-23 09:18:55

RocketMQ存储中间件

2015-05-07 14:24:36

everRun

2016-05-31 10:11:51

2022-05-26 08:53:47

Go函数代码

2010-12-03 10:49:11

Virtuozzo

2013-10-12 13:40:09

2013-08-08 10:06:07

CA TechnoloCA Expo

2009-01-11 10:27:00

小型办公室网络组建

2013-01-16 10:07:30

加密解密破解Android软件

2023-12-07 13:14:54

2017-12-14 09:03:24

租赁数据中心设备

2009-12-30 14:19:50

城域网接入技术

2022-08-05 23:16:29

元宇宙科技虚拟交互

2017-01-10 14:28:01

数据管理大数据SAP

2009-11-26 10:38:08

网关准入控制内网安全
点赞
收藏

51CTO技术栈公众号