swift 混淆原理探究
前提
由于swift是一门静态语言, 其派发机制与OC不同,两者在构建时其MachO文件是有区别的.适用于OC代码层面的混淆方案在swift中并不适用.要探究swift的混淆方案,我们要搞清楚swift在构建时和OC的区别.对于面向对象的编程来说MachO文件可以简单看做是保存了所有类信息的文件,我们需要搞清楚这些类的信息是如何保存的.
MachO是如何存储OC中Class信息的
首先看一下MachO文件的整体结构, 我们将从__objc_classlist
入手,探究OC类的存储方式.MachO文件结构如下

__objc_classlist
中存储的类表,其实就是一系列地址,每个地址对应一个类.

其中红色0xd408
指向_objc_data
分区中的某个地址,结构如下
arduino
struct class64
{
unsigned long long isa;
unsigned long long superClass;
unsigned long long cache;
unsigned long long vtable;
unsigned long long data;
};

其中红色0xc048
为data指针 指向_objc_data
分区中的某个地址,结构如下
arduino
struct class64Info
{
unsigned int flags;//objc-runtime-new.h line:379~460
unsigned int instanceStart;
unsigned int instanceSize;
unsigned int reserved;
unsigned long long instanceVarLayout;
unsigned long long name;
unsigned long long baseMethods;
unsigned long long baseProtocols;
unsigned long long instanceVariables;
unsigned long long weakInstanceVariables;
unsigned long long baseProperties;
};
根据0xc048
往下数32个字节能找到name
地址0x7368
, 可以在__objc_classname
中找到该类名为ViewController
. \x56\x69\x65\x77\x43\x6f\x6e\x74\x72\x6f\x6c\x6c\x65\x72
是其Unicode编码


同理可知 0x6420
为其方法列表地址,其结构如下,首先是method64_list_t
随后跟着是relative_method_t
arduino
struct method64_list_t
{
unsigned int entsize; //hopper显示为flag
unsigned int count;
};
struct relative_method_t {
int32_t nameOffset; // SEL*
int32_t typesOffset; // const char *
int32_t impOffset; // IMP
};

0x6f78
表示方法名偏移.0x0fb0
为方法类型偏移. 0xfffffc60为
Imp偏移.
0x6428 + 0x6f78 = 0xd3a0
,0xd3a0
在 _objc_selrefs
分区

里面存放一个字符串的指针 再找到0x6500
位于 __objct_methodname
分区

可以看到方法名是viewDidLoad
. 同样的方式可以找到方法实现 00006430 + fffffc60 = 100006090
为Imp地址.

以上就是OC中Class的大致存储方式.总结一下
__objc_classlist
存放着Class
指针指向__objc_data
中的地址_objc_data
存放着class64
结构体数据其中data
指向了class64Info
的地址class64Info
中name
指向了__objc_classname
中的字符串,表示类名class64Info
中baseMethods
指向了__objc_methodlist
中的结构体method64_list_t
,其中存放着方法信息method64_list_t
中count
表明方法数量,在其随后的地址中以relative_method_t
表示relative_method_t
中nameOffset
可以计算得出方法名的地址, 这一地址位于__objc_methodname
分区.relative_method_t
中impOffset
可以计算得出方法对应的函数地址,位于__text
分区.
类名和方法名都已字符串的方式存储在对应的分区__objc_classname
和__objc_methodname
中了. 这和我们的直觉是一致的,OC作为一门动态语言方法名一定会以某种形式保存在MachO文件中.
基于这个原因对OC代码的混淆方案中会对方法名和类名进行替换从而达到混淆的效果. 但是我们知道swift作为一门静态语言,其方法的派发机制和OC是不同的. 那么问题来了, 对swift进行方法名称的替换有一样有效吗? 下文将探究该问题.
Swift的派发机制
swift拥有三种不同的派发机制
-
静态派发(直接派发)
-
函数表派发
-
动态派发
我们需要搞清楚,这些派发方式是否有方法名的参与.
他们的区别总结如下

sql
这张图出自[Static vs Dynamic Dispatch in Swift: A decisive choice](https://medium.com/@bakshioye/static-vs-dynamic-dispatch-in-swift-a-decisive-choice-cece1e872d) 这篇文章, 不过在实际的测试中,有一些细微的差别. Release模式下编译器尽可能的将函数表派发优化为静态派发甚至直接优化为内联函数, 因此为了观察派发的具体指令需要在Debug下进行.
- 静态派发会在编译或者静态链接时直接确定函数的地址,肯定没有符号的参与.
- 动态派发就是oc的消息机制, 需要方法名的参与,不赘述了.
下面探寻一下 函数表派发的细节
测试代码


其中 bl , blr 为跳转指令. 其后是地址或者寄存器. 这里iOS加载MachO时存在随机地址偏移(ASLR), 我们需要对地址进行转换得到其在MachO文件中的地址,
image list 打印内存中所有image , 找到SwiftConfuse这个Image,查看其地址 0x0000000102058000, 我们计算地址要减去这个数,来得到文件的偏移地址进行分析.
下面逐行分析这些指令做了啥
bl 0x10205db54
: 还原地址 0x10205db54 - 0x012058000 = 0x5b54
, Hopper跳转到地址0x5b54
,

这是一个读取swift metadata的程序, 获取Foo的matedata的地址并返回.
mov x20 ,x0
: 将x0复制到x20, 前一条指令返回的地址保存到x0中,
bl 0x10205d664
: 还原地址 0x10205d664 - 0x012058000 = 0x5664
, Hopper跳转查看,该程序用来初始化Foo对象并返回对象地址.
可以通过x0地址来验证
成功调用了Foo对象的bar方法.
mov x20 ,x0
: 将x0复制到x20
str x20, [sp, #0x10]
: 把寄存器x20的值保存到栈内存[sp + #0x10]
, 对象的赋值操作,我们不关心.
ldr x8 [x20]
: 这里读取的是x20地址开始的8个字节的数据. x20中存储的是Foo对象的地址. 根据Swift对象地址布局. 对象的第一个8字节存储的就是对象metadata的地址, 对的,其实就是
isa
指针😏. 所以这里获取就是Foo的metadata信息
metadata 地址为 0x1020720c8
. 还原地址 0x1020720c8 - 0x012058000 = 0x1a0c8
这个结构看着是不是很熟悉, 其实就是上文探究OC类存储时的
class64
, 说明了OC和swift类的存储方式是一致的. 也可以解释为啥OC类的结构体中有vtable.
ldr x8 [x8, #0x50]
: 读取x8偏移50字节后地址的8个直接的数据 即 0x1a0c8 + 50 = 0x1a118
中的值,

这里得到的就是0x10000053f4
就是bar方法的地址,通过hopper可以验证.
blr x8
: 执行bar方法.
注意: 这里有2次获取matedata, 第一次是通过matedata accessor获取的, 第二次是通过对象地址获取的. 这是因为在存在对象的情况下,通过对象获取效率更高.
由上述流程可以看出Swift函数表派发寻址原理, swift class matedata 中存储了vtable, 函数表派发会先找到swift类的matedata, 再通过偏移量找到vtable中的地址. 这一过程并没有也不需要函数名的参与.
同OC根据matedata也可以迅速定位出swift类名的位置, matedata->data->name
当然这个和OC有点区别, OC是保存在
__objc_classname
分区的, swift是保存在__cstring
分区的.
结论
对于swift而言
- 由于其大部分的方法是静态派发或者函数表派发,machO文件中不保存方法名称.对方法名进行混淆只对
@objc dynamic
修饰的方法起作用, 大部分情况下是无用功, 市面上一些对swift进行方法名替换混淆的工具从原理上来说就是行不通的. - 纯swift类 类名和属性名方法名, 包括方法名的字符串不都储存. 继承OC的类的类名和存储属性的字符串存储在 Text段的 __cstring 分区, @objc修饰的方法名 和 继承自OC类的方法名会储存