前言
本篇文章开始给大家分享下Hook(钩子)的原理,包括iOS系统原生的Method Swizzle,还有很有名的Hook第三方框架,例如fishHook、Cydia Substrate以及inlineHook等,然后会重点介绍下fishHook的底层处理流程,希望大家能够跟着实操一遍。
一、Hook概述
Hook中文译为挂钩或钩子。在iOS逆向中是指改变程序运行流程的一种技术。通过Hook可以让别人的程序执行自己所写的代码。在逆向中经常使用这种技术。只有了解其原理才能够对恶意代码进行有效的防护。
比如很久之前的微信自动抢红包插件👇

1.1Hook的几种方式
iOS中Hook技术的大致上分为5种:Method Swizzle、fishhook、Cydia Substrate、libffi、inlinehook。
1. Method Swizzle (OC)
利用OC的Runtime特性,动态改变SEL(方法编号)和IMP(方法实现)的对应关系,达到OC方法调用流程改变的目的👇 (主要用于OC方法)

可以将SEL 和 IMP 之间的关系理解为一本书的目录。SEL 就像标题,IMP就像页码。他们是一一对应的关系👇

方法交换的实现方式
主要有3种👇
-
method_exchangeImplementations👉 在分类中直接交换就可以了,如果不在分类,需要配合class_addMethod实现跳回到原方法。 -
class_replaceMethod👉 直接替换原方法。 -
method_setImplementation👉 重新赋值原方法,通过getImp和setImp配合。
具体使用案例可以参考我之前写的文章 👉 11-代码注入(⚠️注意:拉到最后面😁)
2. fishhook
是Facebook提供的一个动态修改链接MachO文件的工具。利用MachO文件加载原理,通过修改懒加载和非懒加载两个表的指针,达到C函数(系统C函数)HOOK的目的。
大概流程 👉 dyld 更新 Mach-O 二进制的 __DATA segment的__la_symbol_str 中的指针,使用 rebind_symbol方法更新两个符号位置来进行符号的重新绑定。后面我会详细的分析底层的流程。
3. Cydia Substrate
Cydia Substrate 原名为 Mobile Substrate ,主要作用是针对OC方法、C函数以及函数地址进行HOOK操作。并不仅仅针对iOS而设计,安卓一样可以用。
Cydia Substrate结构
Cydia Substrate主要分为3部分:Mobile Hooker、MobileLoader、safe mode。
- Mobile Hooker
它定义了一系列的宏和函数,底层调用objc的runtime和fishhook来替换系统或者目标应用的函数。其中有两个函数:
-
MSHookMessageEx:主要作用于OC方法 MSHookMessageExvoid MSHookMessageEx(Class class, SEL selector, IMP replacement, IMP result) -
MSHookFunction:(inline hook)主要作用于C和C++函数 MSHookFunction。Logos语法的%hook就是对这个函数做了一层封装。void MSHookFunction(voidfunction,void* replacement,void** p_original)
MobileLoader
MobileLoader用于加载第三方dylib在运行的应用程序。启动时MobileLoader会根据规则把指定目录的第三方的动态库加载进去,第三方的动态库也就是我们写的破解程序。safe mode
破解程序本质是dylib寄生在别人进程里。 系统进程一旦出错,可能导致整个进程崩溃,崩溃后就会造成iOS瘫痪。所以CydiaSubstrate引入了安全模式,在安全模式下所有基于CydiaSubstratede的三方dylib都会被禁用,便于查错与修复。
4. libffi
基于libbfi动态调用C函数。使用libffi中的ffi_closure_alloc构造与原方法参数一致的"函数" (stingerIMP),以替换原方法函数指针;此外,生成了原方法和Block的调用的参数模板cif和blockCif。方法调用时,最终会调用到void _st_ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata), 在该函数内,可获取到方法调用的所有参数、返回值位置,主要通过ffi_call根据cif调用原方法实现和切面block。
5. inlinehook
Inline Hook 就是在运行的流程中插入跳转指令来抢夺运行流程的一个方法。大体分为三步👇
- 将
原函数的前 N 个字节搬运到Hook 函数的前 N 个字节; - 然后将
原函数的前 N 个字节填充跳转到Hook 函数的跳转指令; - 在
Hook 函数末尾几个字节填充跳转回原函数+N 的跳转指令;
大致流程如下图👇


其中, Cydia Substrate框架中的MSHookFunction就是使用的inlinehook原理。
Dobby
Dobby(原名:HOOKZz)是一个全平台的inlineHook框架,它用起来就和fishhook一样。
Dobby 通过 mmap 把整个 Mach-O 文件映射到用户的内存空间,写入完成保存本地。所以 Dobby 并不是在原 Mach-O 上进行操作,而是重新生成并替换。
Dobby 是通过插入 __zDATA 段和 __zTEXT 段到 Mach-O 中。
-
__zDATA👉 记录 Hook 信息(Hook 数量、每个 Hook 方法的地址)、每个 Hook 方法的信息(函数地址、跳转指令地址、写 Hook 函数的接口地址)、每个 Hook 的接口(指针)。
*__zText👉 记录每个 Hook 函数的跳转指令。
二、fishHook
2.1 fishhook的使用
首先我们看看fishhook是如何使用的 👉 当然看.h头文件👇
/*
* A structure representing a particular intended rebinding from a symbol
* name to its replacement
*/
struct rebinding {
const char *name;//需要HOOK的函数名称,C字符串
void *replacement;//新函数的地址
void **replaced;//原始函数地址的指针!
};
/*
* For each rebinding in rebindings, rebinds references to external, indirect
* symbols with the specified name to instead point at replacement for each
* image in the calling process as well as for all future images that are loaded
* by the process. If rebind_functions is called more than once, the symbols to
* rebind are added to the existing list of rebindings, and if a given symbol
* is rebound more than once, the later rebinding will take precedence.
*/
FISHHOOK_VISIBILITY
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);
/*
* Rebinds as above, but only in the specified image. The header should point
* to the mach-o header, the slide should be the slide offset. Others as above.
*/
FISHHOOK_VISIBILITY
int rebind_symbols_image(void *header,
intptr_t slide,
struct rebinding rebindings[],
size_t rebindings_nel);
很简单,只提供了一个结构体rebinding和两个函数。
rebinding
struct rebinding {
const char *name;//需要HOOK的函数名称,C字符串
void *replacement;//新函数的地址
void **replaced;//原始函数地址的指针!
};
-
name👉 要HOOK的函数名称,C字符串。 -
replacement👉 新函数的地址。(函数指针,也就是函数名称)。 -
replaced👉 原始函数地址的指针。(二级指针)。
2个函数
FISHHOOK_VISIBILITY
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);
FISHHOOK_VISIBILITY
int rebind_symbols_image(void *header,
intptr_t slide,
struct rebinding rebindings[],
size_t rebindings_nel);
-
header👉 image的Header -
slide👉 ASLR -
rebindings[]👉 存放rebinding结构体的数组(可以同时交换多个函数) -
rebindings_nel👉 rebindings数组的长度
示例演示
示例一:HOOK NSLog
现在我们使用fishHook hook一下系统的NSLog函数,代码👇
- (void)hook_NSLog {
struct rebinding rebindNSLog;
rebindNSLog.name = "NSLog";
rebindNSLog.replacement = LG_NSLog;
rebindNSLog.replaced = (void *)&sys_NSLog;
struct rebinding rebinds[] = {rebindNSLog};
rebind_symbols(rebinds, 1);
}
//原函数,函数指针
static void (*sys_NSLog)(NSString *format, ...);
//新函数
void LG_NSLog(NSString *format, ...) {
format = [format stringByAppendingFormat:@"被 Hook了!!!"];
//调用系统NSLog
sys_NSLog(format);
}
调用代码👇
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"hello");
}
run👇

此时就已经Hook住NSLog,走到了LG_NSLog中。
Hook代码调用完毕,sys_NSLog保存系统NSLog原地址,NSLog就指向LG_NSLog。
示例二:HOOK 自定义C函数
接下来我们来Hook一下自定义的C函数👇
void func(const char * str) {
NSLog(@"%s",str);
}
- (void)hook_func {
struct rebinding rebindFunc;
rebindFunc.name = "func";
rebindFunc.replacement = LG_func;
rebindFunc.replaced = (void *)&original_func;
struct rebinding rebinds[] = {rebindFunc};
rebind_symbols(rebinds, 1);
}
//原函数,函数指针
static void (*original_func)(const char * str);
//新函数
void LG_func(const char * str) {
NSLog(@"Hook func");
original_func(str);
}
调用代码👇
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self hook_func];
func("hello");
}
运行👇

我们发现,此时是没有Hook到func函数的。由此得出👇
自定义的函数fishhook不能hook,系统的函数fishhook可以hook。
2.2 fishhook原理
fishhook可以HOOK C函数,但是我们知道函数是静态的,也就是说在编译的时候就确定了实现地址,这也是C函数只写函数声明,在调用时会报错的原因。那么为什么fishhook还能够改变C函数的调用呢?是否像Method Swizzle一样改变了函数实现的地址?带着这些问题,我们继续往下看。
首先我们得弄清楚 👉 系统函数和本地函数有什么区别?
2.2.1 符号 & 符号绑定 & 符号表 & 重绑定符号
NSLog函数的地址在编译的那一刻,我们的App程序并不知道NSLog函数实现的真实地址 👉 因为NSLog在Foundation框架中,在运行时NSLog函数的实现地址在 共享缓存 中。只有系统的dyld知道这个真实地址。
在LLVM编译器生成MachO文件时,我们知道MachO中分为Text(只读)和Data(可读可写),如果先空着系统函数的地址,等运行起来再替换系统函数的地址,显然这种方式行不通,因为你不知道要空多少空间,而且也比较浪费空间。
可行的方案 👉 在Data段放一个 占位符(8字节),让代码编译的时候直接bl 占位符。在运行的时候(即dyld加载应用的时候),将Data段的地址修改为NSLog真实地址,代码bl 占位符此时不变 ,这样就能保证运行NSLog时执行的是真实的实现代码。这个技术就叫做 PIC(position independent code) 位置无关代码。(当然实际的实现并不是这么简单)
-
占位符就叫做符号 -
dyld将data段符号进行修改的这个过程叫做符号绑定 - 一个又一个的符号放在一起形成了一个列表,叫做
符号表
所以,外部的C函数是通过符号 找 地址, 那么,我们就有机会动态的Hook外部C函数。OC的Method Swizzle是修改SEL与IMP对应的关系,对于符号, 当然也能修改符号所对应的地址。这个动作叫做 重新绑定符号表。这也就是fishhook hook的原理。
2.2.2 示例验证
首先在Hook NSLog前后分别调用NSLog👇
NSLog(@"Hook 前");
[self hook_NSLog];
NSLog(@"Hook 后");
接着编译,查看Mach-O的懒加载和非懒加载符号表👇

我们在懒加载表中找到NSLog,说明NSLog是懒加载符号 👉 只有调用的时候才去绑定。
在MachO中可以看到_NSLog的Data(值)是10000064EC,offset值为0x8010。
绑定前的地址
然后我们在 NSLog(@"Hook 前");打上断点,lldb调试如下👇

我们通过image list指令,查看程序的起始地址是0x0000000100624000,其中ASLR的值是0x624000。接着我们打开汇编调试👇

然后进入NSLog👇

最终,我们得到NSLog在内存中的地址是0x00000001043464ec。
回到Mach-O中,NSLog的Data值是0x10000064EC + ASLR值0x4340000 = 0x00000001043464ec。那么我们由此可以推断出👇
Mach-O中记录的NSLog的Data值是没有ASLR(虚拟地址偏移)的。
绑定后的地址
继续运行断点到绑定后的NSLog,同理,查看地址👇

程序起始地址0x0000000104340000 + NSLog的偏移地址0x8010得到了NSLog的真实地址,然后通过lldb的x指令查看起始的8字节中存储的值是地址0x0104345650,再通过dis -s查看改地址对应的汇编代码,发现就是LG_NSLog方法。由此可见👇
懒加载符号表里面绑定的地址已经改变了。
2.3 符号绑定过程
接下来,我们来分析一下,上面懒加载符号表中,绑定的地址发生变化的过程,也就是符号绑定的过程。
- iOS中函数名、变量名、方法名、编译完成后会生成一张
符号表 - 符号有2种类型 👉
内部符号&外部符号
2.3.1 内部符号:内部函数,方法名称
如ViewDidLoad。内部符号又分为👇
-
本地符号👉 自己内部使用的 -
全局符号👉 外部也可以使用
示例演示
新建工程symbolTest,定义一个全局函数代码👇
//全局符号,可以暴露给外界
void test(){
}
本地函数👇
//本地符号 作用域相当于本文件
static void test1(){
NSLog(@"test1");
}

⚠️注意:App在上架时会
去符号,去的是本地符号。
我们可以通过dump指令查看Mach-O中的所有符号👇
objdump --macho -t
xxx(你的MachO文件名称)


再使用MachOView查看👇

符号表
Symbols包含所有的符号👉 本地符号,全局符号,间接符号。
2.3.2 外部符号(间接符号表)
MachO文件中调用外部方法名称,如NSLog,LLVM编译时期并不知道外部(MachO文件以外)方法的地址。
间接符号有个专门的符号表Indirect Symbols,用到的外部符号例如NSStringFromClass,编译时会生成一个符号👇

2.3.3 符号绑定过程
接着回到正题,看看符号绑定过程。首先,有以下代码👇
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSLog(@"外部函数第一次调用");
NSLog(@"外部函数第二次调用");
}
断点断到第一个NSLog,查看汇编👇

可以看到两次调用NSLog是同一个地址0x102c06524,并且通过image list得到程序的起始地址是0x0000000102c00000,那么0x1049ee524 - 0x00000001049e8000 = 0x6524,而0x6524是👇

0x6524在MachO的Symbol Stubs中。这个就是NSLog的桩(外部符号的桩),值为1F2003D510D7005800021FD6(是代码),这个代码是👇

查看第一句汇编的地址0x1049ee524存储的值👇

就是NSLog桩的值!!!
继续执行完NSLog代码👇

上图可知,执行完NSLog汇编后,通过读取x16寄存器的值(即返回值)可知👇
执行
Symbol Stubs桩中的代码来找到Symbol Stubs符号中的代码。
至此,我们定位到了地址00000001000065CC,而65CC地址在__stub_helper中👇

其中,绿框中执行的b 0x1000065b4就是符号绑定的过程,我们继续执行汇编代码👇

上图的第一句汇编在MachO中其实对应的就是adr x17,12204,因为0x1049ee5b4 - 0x00000001049e8000(程序起始地址) = 0x65B4。
继续执行,进去👇

而MachO中的dyld_stub_binder是👇

和上面的汇编就一一对应上了!!!🍺🍺🍺🍺🍺🍺
实际上执行的是dyld_stub_binder,综上可以得出结论👇
懒加载符号表里面的初始值都是执行符号绑定的函数。
但是dyld_stub_binder也是外部符号,那接下来的问题就是 👉 怎么找到dyld_stub_binder这个符号呢?
继续执行汇编代码,走到0x1049ee5c8: br x16这句👇

上图我们通过lldb指令读取x16寄存器的地址是0x0000000181041474,该地址正是dyld_stub_binder的实现地址,那么接下来就是该值是如何计算出的呢?依旧看MachO👇

这个符号在非懒加载表中(一运行就绑定)👇

综上所述,第一次符号绑定的过程👇
- 程序一运行,先绑定
No-Lazy Symbol Pointers表中dyld_stub_binder函数的值。 - 调用
NSLog时,先找Symbol Stubs桩,执行桩中的代码,桩中的代码是对应找到懒加载符号表中的代码去执行。 - 懒加载符号表中的初始值是
本地的源代码,这个代码去NoLazy表中找绑定函数地址。 - 最后就执行
dyld_stub_binder函数进行符号绑定。
继续执行NSLog

第2次执行NSLog的时候,通过桩直接跳到了真实地址,因为符号表中已经保存了地址执行代码。
小结
符号绑定的整个流程图如下👇

-
外部函数调用时执行桩(__TEXT,__stubs)中的代码 - 桩中的代码去
懒加载符号表(__DATA,__la_symbo_ptrl)中找地址执行- 绑定过 👉 要么直接调用绑定的函数地址
- 未绑定 👉去
__TEXT,__stubhelper中找绑定函数dyld_stub_binder进行绑定。 -
懒加载符号表中默认保存的是寻找binder的代码
- 懒加载中的代码去
__TEXT,__stubhelper中执行绑定代码(binder函数)。 -
dyld_stub_binder在非懒加载符号表中(__DATA._got),程序运行就绑定好了。
2.4 通过符号找字符串
我们使用fishhook的时候我们是通过rebindNSLog.name = "NSLog";来hook NSLog。那么fishhook通过NSLog字符串怎么找到的NSLog函数符号的呢?
根据上面分析的符号绑定过程,我们知道,在绑定的时候是去Lazy Symbol中去找的NSLog对应的绑定代码👇

0x00008008这个地址,在Lazy Symbol中NSLog排在第一个。在Indirect Symbols间接符号表中可以看到顺序和Lazy Symbols中相同👇

所以反过来,要找Lazy Symbols中的符号,只要找到Indirect Symbols中对应的索引值就可以了,那么接下来就是确定索引值了。
我们注意到,在上图的间接符号表中,NSLog对应的Data值是000000BD(十六进制),转换成十进制是189,这个189就是代表着NSLog在总符号表(Symbols)中的角标👇

注意到Data中保存的是000000D4(十六机制),这是NSLog在String Table中偏移值👇

通过偏移值计算得到0xD334,就找到了_NSLog(长度+首地址)。
⚠️注意:
.表示分隔符,函数名前面有_
至此,我们就从Lazy Symbols -> Indirect Symbols -> Symbols - > String Table通过符号找到了字符串。fishhook找符号的过程就是这么处理的,通过遍历所有符号和要hook的数组中的字符串做对比。
在fishhookgitHub网址中有一张图说明这个关系👇

上图是通过符号查找close字符串的过程👇
-
Lazy Symbol Pointer Table中close index为1061 - 在
Indirect Symbol Table1061 对应的角标为0X00003fd7(十进制16343) - 在
Symbol Table找角标16343对应的字符串表中的偏移值70026 - 在
String Table中找首地址+偏移值(70026)就找到了close字符串
反过来,那么通过字符串找符号过程👇
- 在
String Table中找到字符串,计算偏移值 - 通过
偏移值在Symbols中找到角标 - 通过
角标在Indirect Symbols中找到对应的符号,也能取到这个符号的index - 通过找到的
index在Lazy Symbols中找到对应index的符号。
2.5 去掉符号 & 恢复符号
符号本身在MachO文件中,占用包体积大小 ,在我们分析别人的App时符号是去掉的。
2.5.1 去掉符号
- 对于
App来说,会去掉所有符号(间接符号除外) - 对于
动态库来说要保留全局符号(外部要调用)
脱符号的设置
去掉符号在Build setting中设置👇

Strip Style说明👇
- All Symbols去掉
所有符号(间接除外) - Non-Global Symbols去掉
除全局符号外的符号 - Debugging Symbols去掉
调试符号
⚠️注意:
Deployment Postprocessing👉 设置为YES则在编译阶段去符号,否则在打包阶段去符号。

All Symbols
设置Deployment Postprocessing为YES,Strip Style为All Symbols,然后编译,打开包所在的位置👇

查看多了一个.bcsymbolmap文件,这个文件就是bitcode。接着我们查看MachO文件中Symbols总符号表👇

上图中,我们看到NSLog的Value段存储的地址是0000000000000000,value为函数的实现地址(imp),所以代码中打断点就断不住了👇

直接跑完了。要断住NSLog就要打符号断点👇

再运行👇

bt指令查看调用栈,发现👇
frame #0: 0x0000000182762ba8 Foundation`NSLog
frame #1: 0x0000000104e51fc4 symbolTest`___lldb_unnamed_symbol2$$symbolTest + 72
说明自定义的方法test1是unnamed,这个很明显就是去掉符号的。这种情况下就不好分析代码了。
之前学习汇编的时候,可知道,oc方法调用则直接读取x0,x1就能获取self和cmd,例如👇

接着,我们可以下地址断点,再通过image list指令,结合ASLR值,计算出偏移值👇

后面,就能ASLR+偏移值直接下断点,找到方法的imp地址,这就是动态调试。
2.5.2 恢复符号
动态调试下断点,使用起来还是比较麻烦,需要计算,如果能恢复符号的话就方便很多了。
我明知道,在上面的例子中去掉所有符号后Symbol Table中只有间接符号了,虽然符号表中没有了,但是类列表和方法列表中依然存在。

这也就为我们提供了恢复Symbol Table的机会。
恢复指令
可以通过restore-symbol工具恢复符号(只能恢复oc的,runtime机制导致)👇
./restore-symbol
原始Macho文件-o恢复后文件

查看恢复后的machO👇

这个时候就可以重签名后进行动态调试了。
restore-symbol工具链接
2.6 fishhook源码解析
最后,也是本篇文章的重点,就是fishhook源码解析,废话不多说,直接上源码。
2.6.1 rebind_symbols
//第一次是拿dyld的回调,之后是手动拿到所有image去调用。这里因为没有指定image所以需要拿到所有的。
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
//prepend_rebindings的函数会将整个 rebindings 数组添加到 _rebindings_head 这个链表的头部
//Fishhook采用链表的方式来存储每一次调用rebind_symbols传入的参数,每次调用,就会在链表的头部插入一个节点,链表的头部是:_rebindings_head
int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
//根据上面的prepend_rebinding来做判断,如果小于0的话,直接返回一个错误码回去
if (retval < 0) {
return retval;
}
//根据_rebindings_head->next是否为空判断是不是第一次调用。
if (!_rebindings_head->next) {
//第一次调用的话,调用_dyld_register_func_for_add_image注册监听方法.
//已经被dyld加载的image会立刻进入回调。之后的image会在dyld装载的时候触发回调。这里相当于注册了一个回调到 _rebind_symbols_for_image 函数。
_dyld_register_func_for_add_image(_rebind_symbols_for_image);
} else {
//不是第一次调用,遍历已经加载的image,进行的hook
uint32_t c = _dyld_image_count();//这个相当于 image list count
for (uint32_t i = 0; i < c; i++) {
//遍历重新绑定image header aslr
_rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));
}
}
return retval;
}
- 首先通过
prepend_rebindings函数生成链表,存放所有要Hook的函数。 - 根据
_rebindings_head->next是否为空判断是不是第一次调用,第一次调用走系统的回调,第二次则自己获取所有的image list进行遍历。 - 最后都会走
_rebind_symbols_for_image函数。
rebindings_entry链表
其中,_rebindings_head是指向链表rebindings_entry结构体的指针👇
struct rebindings_entry {
struct rebinding *rebindings; // HOOK的相关信息
size_t rebindings_nel; // 所占空间大小
struct rebindings_entry *next; // 链表的next指针
};
static struct rebindings_entry *_rebindings_head;
rebind_symbols_image
int rebind_symbols_image(void *header,
intptr_t slide,
struct rebinding rebindings[],
size_t rebindings_nel) {
struct rebindings_entry *rebindings_head = NULL;
int retval = prepend_rebindings(&rebindings_head, rebindings, rebindings_nel);
rebind_symbols_for_image(rebindings_head, (const struct mach_header *) header, slide);
if (rebindings_head) {
free(rebindings_head->rebindings);
}
free(rebindings_head);
return retval;
}
rebind_symbols_image流程比rebind_symbols简单很多,直接调用rebind_symbols_for_image,因为指定了void *header,不需要遍历所有的image。
2.6.2 _rebind_symbols_for_image
//两个参数 header 和 ASLR
static void _rebind_symbols_for_image(const struct mach_header *header,
intptr_t slide) {
//_rebindings_head 参数是要交换的数据,head的头
rebind_symbols_for_image(_rebindings_head, header, slide);
}
直接调用rebind_symbols_for_image,传递了head链表地址。
2.6.4 rebind_symbols_for_image
//回调的最终就是这个函数! 三个参数:要交换的数组 、 image的头 、 ASLR的偏移
static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
const struct mach_header *header,
intptr_t slide) {
/*dladdr() 可确定指定的address 是否位于构成进程的进址空间的其中一个加载模块(可执行库或共享库)内,如果某个地址位于在其上面映射加载模块的基址和为该加载模块映射的最高虚拟地址之间(包括两端),则认为该地址在加载模块的范围内。如果某个加载模块符合这个条件,则会搜索其动态符号表,以查找与指定的address 最接近的符号。最接近的符号是指其值等于,或最为接近但小于指定的address 的符号。
*/
/*
如果指定的address 不在其中一个加载模块的范围内,则返回0 ;且不修改Dl_info 结构的内容。否则,将返回一个非零值,同时设置Dl_info 结构的字段。
如果在包含address 的加载模块内,找不到其值小于或等于address 的符号,则dli_sname 、dli_saddr 和dli_size字段将设置为0 ; dli_bind 字段设置为STB_LOCAL , dli_type 字段设置为STT_NOTYPE 。
*/
// typedef struct dl_info {
// const char *dli_fname; //image 镜像路径
// void *dli_fbase; //镜像基地址
// const char *dli_sname; //函数名字
// void *dli_saddr; //函数地址
// } Dl_info;
Dl_info info;
//这个dladdr函数就是在程序里面找header
if (dladdr(header, &info) == 0) {
return;
}
//下面就是定义好几个变量,准备从MachO里面去找!
segment_command_t *cur_seg_cmd;
segment_command_t *linkedit_segment = NULL;
struct symtab_command* symtab_cmd = NULL;
struct dysymtab_command* dysymtab_cmd = NULL;
//跳过header的大小,找loadCommand
uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (segment_command_t *)cur;
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
linkedit_segment = cur_seg_cmd;
}
} else if (cur_seg_cmd->cmd == LC_SYMTAB) {
symtab_cmd = (struct symtab_command*)cur_seg_cmd;
} else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
}
}
//如果刚才获取的,有一项为空就直接返回
if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
!dysymtab_cmd->nindirectsyms) {
return;
}
// Find base symbol/string table addresses
//链接时程序的基址 = __LINKEDIT.VM_Address -__LINKEDIT.File_Offset + silde的改变值
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
// printf("地址:%p\n",linkedit_base);
//符号表的地址 = 基址 + 符号表偏移量
nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
//字符串表的地址 = 基址 + 字符串表偏移量
char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
// Get indirect symbol table (array of uint32_t indices into symbol table)
//动态符号表地址 = 基址 + 动态符号表偏移量
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
cur = (uintptr_t)header + sizeof(mach_header_t);
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (segment_command_t *)cur;
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
//寻找到data段
if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
continue;
}
for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
section_t *sect =
(section_t *)(cur + sizeof(segment_command_t)) + j;
//找懒加载表
if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
//非懒加载表
if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
}
}
}
}
核心的步骤有👇
- 根据
linkedit和偏移值分别找到符号表的地址和字符串表的地址以及间接符号表地址。 - 遍历
load commands和data段找到懒加载符号表和非懒加载符号表。 - 找到表的同时就直接调用
perform_rebinding_with_section进行hook替换函数符号。
2.6.5 perform_rebinding_with_section
//rebindings:要hook的函数链表,可以理解为数组
//section:懒加载/非懒加载符号表地址
//slide:ASLR
//symtab:符号表地址
//strtab:字符串标地址
//indirect_symtab:动态(间接)符号表地址
static void perform_rebinding_with_section(struct rebindings_entry *rebindings,
section_t *section,
intptr_t slide,
nlist_t *symtab,
char *strtab,
uint32_t *indirect_symtab) {
//nl_symbol_ptr和la_symbol_ptrsection中的reserved1字段指明对应的indirect symbol table起始的index。也就是第几个这里是和间接符号表中相对应的
//这里就拿到了index
uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
//slide+section->addr 就是符号对应的存放函数实现的数组也就是我相应的__nl_symbol_ptr和__la_symbol_ptr相应的函数指针都在这里面了,所以可以去寻找到函数的地址。
//indirect_symbol_bindings中是数组,数组中是函数指针。相当于lazy和non-lazy中的data
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
//遍历section里面的每一个符号(懒加载/非懒加载)
for (uint i = 0; i < section->size / sizeof(void *); i++) {
//找到符号在Indrect Symbol Table表中的值
//读取indirect table中的数据
uint32_t symtab_index = indirect_symbol_indices[i];
if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) {
continue;
}
//以symtab_index作为下标,访问symbol table,拿到string table 的偏移值
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
//获取到symbol_name 首地址 + 偏移值。(char* 字符的地址)
char *symbol_name = strtab + strtab_offset;
//判断是否函数的名称是否有两个字符,因为函数前面有个_,所以方法的名称最少要1个
bool symbol_name_longer_than_1 = symbol_name[0] && symbol_name[1];
//遍历最初的链表,来判断名字进行hook
struct rebindings_entry *cur = rebindings;
while (cur) {
for (uint j = 0; j < cur->rebindings_nel; j++) {
//这里if的条件就是判断从symbol_name[1]两个函数的名字是否都是一致的,以及判断字符长度是否大于1
if (symbol_name_longer_than_1 &&
strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
//判断replaced的地址不为NULL 要替换的自己实现的方法和rebindings[j].replacement的方法不一致。
if (cur->rebindings[j].replaced != NULL &&
indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
//让rebindings[j].replaced保存indirect_symbol_bindings[i]的函数地址,相当于将原函数地址给到你定义的指针的指针。
*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
}
//替换内容为自己自定义函数地址,这里就相当于替换了内存中的地址,下次桩直接找到lazy/non-lazy表的时候直接就走这个替换的地址了。
indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
//替换完成跳转外层循环,到(懒加载/非懒加载)数组的下一个数据。
goto symbol_loop;
}
}
//没有找到就找自己要替换的函数数组的下一个函数。
cur = cur->next;
}
symbol_loop:;
}
}
核心步骤👇
- 首先通过
懒加载/非懒加载符号表和间接符号表找到所有的index角标,reserved1确认了懒加载和非懒加载符号在间接符号表中的index值。

- 将
懒加载/非懒加载符号表的Data值放入indirect_symbol_bindings数组中。

- 遍历
懒加载/非懒加载符号表👇
- 读取
indirect_symbol_indices找到符号在Indrect Symbol Table表中的值放入symtab_index。 - 以
symtab_index作为下标,访问symbol table,拿到string table的strtab_offset偏移值。 - 根据
strtab_offset偏移值获取字符地址symbol_name字符名。 - 循环遍历
rebindings链表(即自定义的Hook数据) - 判断
&symbol_name[1]和rebindings[j].name两个函数的名字是否都是一致的,以及判断字符长度是否大于1心步骤👇。 -
相同👉 先保存(replaced时,没有replaced则不保存)原地址到自定义函数指针。并且用要Hook的目标函数replacement替换indirect_symbol_bindings,这里就完成了Hook。
总结
