一、Hook概述
HOOK中文译为挂钩或钩子。在iOS逆向中是指改变程序运行流程的一种技术。通过hook可以让别人的程序执行自己所写的代码。在逆向中经常使用这种技术。只有了解其原理才能够对恶意代码进行有效的防护。
比如很久之前的微信自动抢红包插件:

1.1Hook的几种方式
iOS中HOOK技术的大致上分为5种:Method Swizzle、fishhook、Cydia Substrate、libffi、inlinehook。
1.1.1 Method Swizzle (OC)
利用OC的Runtime特性,动态改变SEL(方法编号)和IMP(方法实现)的对应关系,达到OC方法调用流程改变的目的。主要用于OC方法。
可以将SEL 和 IMP 之间的关系理解为一本书的目录。SEL 就像标题,IMP就像页码。他们是一一对应的关系。(书的目录不一定一一对应,可能页码相同,理解就行。)。

Runtime提供了交换两个SEL和IMP对应关系的函数:
OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
通过这个函数交换两个SEL和IMP对应关系的技术,称之为Method Swizzle(方法欺骗)

runtime中有3种方式实现方法交换:
-
method_exchangeImplementations:在分类中直接交换就可以了,如果不在分类需要配合class_addMethod实现原方法的回调。 -
class_replaceMethod:直接替换原方法。 -
method_setImplementation:重新赋值原方法,通过getImp和setImp配合。
runtime都比较熟悉,就不多介绍了,不是很了解使用的可以参考:第4部分代码注入
1.1.2 fishhook (外部函数)
是Facebook提供的一个动态修改链接mach-O文件的工具。利用MachO文件加载原理,通过修改懒加载和非懒加载两个表的指针达到C(系统C函数)函数HOOK的目的。fishhook
总结下来是:dyld 更新 Mach-O 二进制的 __DATA segment 的 __la_symbol_str 中的指针,使用 rebind_symbol方法更新两个符号位置来进行符号的重新绑定。
1.1.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都会被禁用,便于查错与修复。
1.1.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。AOP库 Stinger和BlockHook就是使用libbfi做的。
1.1.5 inlinehook 内联钩子 (静态)
Inline Hook 就是在运行的流程中插入跳转指令来抢夺运行流程的一个方法。大体分为三步:
- 将原函数的前
N个字节搬运到Hook函数的前N个字节; - 然后将原函数的前
N个字节填充跳转到Hook函数的跳转指令; - 在
Hook函数末尾几个字节填充跳转回原函数+N的跳转指令;


MSHookFunction就是inline hook。
基于 Dobby 的 Inline Hook。Dobby 是通过插入 __zDATA 段和__zTEXT 段到 Mach-O 中。
-
__zDATA用来记录Hook信息(Hook数量、每个Hook方法的地址)、每个Hook方法的信息(函数地址、跳转指令地址、写Hook函数的接口地址)、每个Hook的接口(指针)。 -
__zText用来记录每个Hook函数的跳转指令。
dobby
Dobby(原名:HOOKZz)是一个全平台的inlineHook框架,它用起来就和fishhook一样。
Dobby 通过 mmap 把整个 Mach-O 文件映射到用户的内存空间,写入完成保存本地。所以 Dobby 并不是在原 Mach-O 上进行操作,而是重新生成并替换。
Doddy
二 fishHook
2.1 fishhook的使用
在fishhook源码.h文件中只提供了两个函数和一个结构体rebinding。
rebind_symbols、rebind_symbols_image
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);
-
rebindings[]:存放rebinding结构体的数组(可以同时交换多个函数)。 -
rebindings_nel:rebindings数组的长度。 -
slide:ASLR。 -
header:image的Header。
只有两个函数重新绑定符号,两个函数的区别是一个指定image一个不指定。按照我们一般的理解放在前面的接口更常用,参数少的更简单。
rebinding
struct rebinding {
const char *name;//需要HOOK的函数名称,C字符串
void *replacement;//新函数的地址
void **replaced;//原始函数地址的指针!
};
-
name:要HOOK的函数名称,C字符串。 -
replacement:新函数的地址。(函数指针,也就是函数名称)。 -
replaced:原始函数地址的指针。(二级指针)。
2.1.1 Hook NSLog
现在有个需求,Hook系统的NSLog函数。
Hook代码:
- (void)hook_NSLog {
struct rebinding rebindNSLog;
rebindNSLog.name = "NSLog";
rebindNSLog.replacement = HP_NSLog;
rebindNSLog.replaced = (void *)&sys_NSLog;
struct rebinding rebinds[] = {rebindNSLog};
rebind_symbols(rebinds, 1);
}
//原函数,函数指针
static void (*sys_NSLog)(NSString *format, ...);
//新函数
void HP_NSLog(NSString *format, ...) {
format = [format stringByAppendingFormat:@"\n Hook"];
//调用系统NSLog
sys_NSLog(format);
}
调用:
[self hook_NSLog];
NSLog(@"hook_NSLog");
输出:
hook_NSLog
Hook
这个时候就已经Hook住NSLog,走到了HP_NSLog中。
Hook代码调用完毕,sys_NSLog保存系统NSLog原地址,NSLog指向HP_NSLog。
2.1.2 Hook 自定义 C 函数
Hook一下自己的C函数:
void func(const char * str) {
NSLog(@"%s",str);
}
- (void)hook_func {
struct rebinding rebindFunc;
rebindFunc.name = "func";
rebindFunc.replacement = HP_func;
rebindFunc.replaced = (void *)&original_func;
struct rebinding rebinds[] = {rebindFunc};
rebind_symbols(rebinds, 1);
}
//原函数,函数指针
static void (*original_func)(const char * str);
//新函数
void HP_func(const char * str) {
NSLog(@"Hook func");
original_func(str);
}
调用:
[self hook_func];
func("HotPotCat");
输出:
HotPotCat
这个时候可以看到没有Hook的func。
结论:自定义的函数fishhook hook 不了,系统的可以hook。
2.2 fishhook原理
fishHOOK可以HOOK C函数,但是我们知道函数是静态的,也就是说在编译的时候,编译器就知道了它的实现地址,这也是为什么C函数只写函数声明调用时会报错。那么为什么fishhook还能够改变C函数的调用呢?难道函数也有动态的特性存在?
是否意味着C Hook就必须修改调用地址?那意味着要修改二进制。(原理上使用汇编可以实现。fishhook不是这么处理的)
那么系统函数和本地函数区别到底在哪里?
2.2.1 符号 & 符号绑定 & 符号表 & 重绑定符号
NSLog函数的地址在编译的那一刻并不知道NSLog的真实地址。NSLog在Foundation框架中。在运行时NSLog的地址在 共享缓存 中。在整个手机中只有dyld知道NSLog的真实地址。
在LLVM编译器生成MachO文件时,如果让我们做就先空着系统函数的地址,等运行起来再替换。我们知道MachO中分为Text(只读)和Data(可读可写),那么显然这种方式行不通。
那么可行的方案是在Data段放一个 占位符(8字节)让代码编译的时候直接bl 占位符。在运行的时候dyld加载应用的时候将Data段的地址修改为NSLog真实地址,代码bl 占位符没有变 。这个技术就叫做 PIC(position independent code`)位置无关代码。(实际不是这么简单)
- 占位符 就叫做 符号。
-
dyld将data段符号进行修改的这个过程叫做 符号绑定。 - 一个又一个的符号放在一起形成了一个列表,叫做 符号表。
对于外部的C函数通过 符号 找 地址 也就给了我们机会动态的Hook外部C函数。OC是修改SEL与IMP对应的关系,符号 也是修改符号所对应的地址。这个动作叫做 重新绑定符号表。这也就是fishhook``hook的原理。
2.2.2验证
在Hook NSLog前后分别调用NSLog:
NSLog(@"before");
[self hook_NSLog];
NSLog(@"after");

在
MachO中我们能看到懒加载和非懒加载符号表,dyld绑定过程中绑定的是非懒加载符号和弱符号的。NSLog是懒加载符号,只有调用的时候才去绑定。
在MachO中可以看到_NSLog的Data(值)是0000000100006960。offset为:0x8010
在第一个NSLog处打个断点 运行查看:
主程序开始0x0000000100b24000,ASLR是0xb24000:
0x0000000100b24000 + 0x8010中存储的内容为0x0100b2a960。
0000000100006960 + 0xb24000 (ASLR) = 0x100B2A960。
所以这里就对应上了。0x0100b2a960这个地址就是(符号表中的值其实是一个代码的地址,指向本地代码。)。

执行完第一个NSLog后(hook前):

符号表指向了
NSLog。执行完
hook后:
符号表指向了
HP_NSLog。
这也就是fishhook能够Hook的真正原因(修改懒加载符号表)。
2.3 符号绑定过程(间接)
刚才在上面NSLog第一次执行之前我们拿到的地址0x0100b2a960实际上指向一段本地代码,加上ASLR后执行对应地址的代码然后就修改了懒加载符号表。
那么这个过程究竟是怎么做的呢?
先说明一些符号的情况:
- 本地符号:只能本
MachO用。 - 全局符号:暴露给外面用。
- 间接符号:当我们要调用外部函数/方法时,在编译时期地址是不知道的。比如系统的
NSLog。
间接符号专门有个符号表Indirect Symbols:

Symbols包含了所有的符号。
有以下代码:
NSLog(@"外部函数第一次调用");
NSLog(@"外部函数第二次调用");
断点断到第一个NSLog,可以看到两次调用NSLog是同一个地址0x100e12998:

比首地址大
0x0000000100e0c000,所以这个地址在本MachO中。0x100e12998 - 0x0000000100e0c000 = 0x6998。
6998在MachO的Symbol Stubs中:

这个就是
NSLog的桩(外部符号的桩),值为1F2003D570B3005800021FD6(代码),这个代码是:
这个时候就对应上了:

执行桩中的代码:

这段代码的意思是执行桩中的代码找到符号表中代码跳转执行(0000000100006A28)。
6A28这段代码在__stub_helper中:

这里执行的是符号绑定。
继续动态调试:

这块是刚好对应上。
继续进去:

继续进去:

对应上了。实际上执行的是
dyld_stub_binder。也就是说懒加载符号表里面的初始值都是执行符号绑定的函数。
dyld_stub_binder是外部函数,那么怎么得到的dyld_stub_binder函数呢?

在MachO中x16是0x100008000:

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

所以
dyld_stub_binder是通过去非懒加载表中查找。验证 :

验证确认,
No-Lazy Symbol Pointers表中默认值是0。
符号绑定过程:
- 程序一运行,先绑定
No-Lazy Symbol Pointers表中dyld_stub_binder的值。 - 调用
NSLog先找桩,执行桩中的代码。桩中的代码是找懒加载符号表中的代码去执行。 - 懒加载符号表中的初始值是本地的源代码,这个代码去
NoLazy表中找绑定函数地址。 - 进入
dyld的binder函数进行绑定。
binder函数执行完毕后就调用第一次的NSLog了。这个时候再看一下懒加载符号表中的符号:

符号已经变了。这个时候符号就已经绑定成功了。
接着执行第二次NSLog,这个时候依然是去找桩中的代码执行:

这个继续执行就执行到Foundation框架的NSLog了(已经绑定过了,不需要继续绑定):

这个时候通过桩直接跳到了真实地址(还是虚拟的)。这个做的原因是符号表中保存地址执行代码,代码是保存在代码段的(桩)。

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

找的是
0x00008008这个地址,在Lazy Symbol中NSLog排在第一个。
在Indirect Symbols中可以看到顺序和Lazy Symbols中相同,也就是要找Lazy Symbols中的符号,只要找到Indirect Symbols中对应的第几个就可以了。

那么怎么确认Indirect Symbols中的第几个呢?
在Indirect Symbols中data对应值(十六进制)这里NSLog是101,这个代表着NSLog在总的符号表(Symbols)中的角标:

在这里我们可以看到NSLog在String Table中偏移为0x98(十六进制)。

通过偏移值计算得到
0xCC38就确认到了_NSLog(长度+首地址)。
这样我们就从Lazy Symbols -> Indirect Symbols -> Symbols - > String Table 通过符号找到了字符串。那么fishhook的过程就是这么处理的,通过遍历所有符号和要hook的数组中的字符串做对比。
在fishhook中有一张图说明这个关系:

这里是通过符号查找
close字符串。
-
Lazy Symbol Pointer Table中closeindex为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中设置:

-
Deployment Postprocessing:设置为YES则在编译阶段去符号,否则在打包阶段去符号。 -
Strip Style:All Symbols去掉所有符号(间接除外),Non-Global Symbols去掉除全局符号外的符号。Debugging Symbols去掉调试符号。
设置Deployment Postprocessing为YES,Strip Style为All Symbols。编译查看多了一个.bcsymbolmap文件,这个文件就是bitcode。

这个时候的MachO文件中Symbols就只剩下间接符号表中的符号了:

其中
value为函数的实现地址(imp)。间接符号不会找到符号表中地址执行,是找Lazy Symbol Table中的地址。
代码中打断点就断不住了:

要断住NSLog就要打符号断点了:

bt看下调用栈:

发现自定义方法全是
unnamed,这个很明显就是去掉符号的。这种情况下就不好分析代码了。如果是
oc方法调用则直接读取x0,x1就能获取self和cmd:
在这里我们就要下断点在方法调用之前,可以通过下地址断点。

先计算出偏移值,下次直接
ASLR+偏移值直接断点。这个也就是动态调试常用的方法。
2.5.2 恢复符号
前面动态调试下断点比较麻烦,如果能恢复符号的话就方便很多了。
在上面的例子中去掉所有符号后Symbol Table中只有间接符号了。虽然符号表中没有了,但是类列表和方法列表中依然存在。

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

这个时候就恢复了,查看
MachO(恢复的符号在Symbol Table后面):
这个时候就可以重签名后进行动态调试了。
restore-symbol地址
2.6 fishhook源码解析
rebind_symbols
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函数。
image list验证:

_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链表地址。
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);
//如果指定image就直接调用了 rebind_symbols_for_image,没有遍历了。
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都调用到了rebind_symbols_for_image,由于给定了image所以不需要循环遍历。
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;//拿到image的信息
//dladdr函数就是在程序里面找header
if (dladdr(header, &info) == 0) {
return;
}
//准备从MachO里面去找!
segment_command_t *cur_seg_cmd;//临时变量
//这里与MachOView中看到的对应
segment_command_t *linkedit_segment = NULL;//SEG_LINKEDIT
struct symtab_command* symtab_cmd = NULL;//LC_SYMTAB 符号表地址
struct dysymtab_command* dysymtab_cmd = NULL;//LC_DYSYMTAB 动态符号表地址
//cur为了跳过header的大小,找loadCommands cur = 首地址 + mach_header大小
uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
//循环load commands找对应的 SEG_LINKEDIT LC_SYMTAB LC_DYSYMTAB
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (segment_command_t *)cur;
//这里`SEG_LINKEDIT`获取和`LC_SYMTAB`与`LC_DYSYMTAB`不同是因为在`MachO`中分别对应`LC_SEGMENT_64(__LINKEDIT)`、`LC_SYMTAB`、`LC_DYSYMTAB`
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;
}
}
//有任何一项为空就直接返回,nindirectsyms表示间接符号表中符号数量,没有则直接返回
if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
!dysymtab_cmd->nindirectsyms) {
return;
}
// Find base symbol/string table addresses
//符号表和字符串表都属于data段中的linkedit,所以以linkedit基址+偏移量去获取地址(这里的偏移量不是整个macho的偏移量,是相对基址的偏移量)
//链接时程序的基址 = __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) {
//寻找到load command 中的data【LC_SEGEMNT_64(__DATA)】,相当于拿到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;
//找懒加载表(lazy symbol table)
if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
//找到直接调用函数 perform_rebinding_with_section,这里4张表就都已经找到了。传入要hook的数组、ASLR、以及4张表
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
//非懒加载表(Non-Lazy symbol table)
if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
}
}
}
}
- 找到
SEG_LINKEDIT、LC_SYMTAB、LC_DYSYMTAB的load commans。
- 根据
linkedit和偏移值分别找到符号表的地址和字符串表的地址以及间接符号表地址。 - 遍历
load commands和data段找到懒加载符号表和非懒加载符号表。 - 找到表的同时就直接调用
perform_rebinding_with_section进行hook替换函数符号。
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。 - 将懒加载/非懒加载符号表的
data放入indirect_symbol_bindings数组中。
- 遍历懒加载/非懒加载符号表。
- 读取
indirect_symbol_indices找到符号在Indrect Symbol Table表中的值放入symtab_index。 - 以
symtab_index作为下标,访问symbol table,拿到string table的偏移值。 - 根据
strtab_offset偏移值获取字符地址symbol_name,也就相当于字符名称。 - 循环遍历
rebindings也就是链表(自定义的Hook数据) - 判断
&symbol_name[1]和rebindings[j].name两个函数的名字是否都是一致的,以及判断字符长度是否大于1。 - 相同则先保存原地址到自定义函数指针(如果
replaced传值的话,没有传则不保存)。并且用要Hook的目标函数replacement替换indirect_symbol_bindings,这里就完成了Hook。
- 读取
-
reserved1确认了懒加载和非懒加载符号在间接符号表中的index值。
疑问点:懒加载和非懒加载怎么和间接符号表index对应的呢?
直接Hook dyld_stub_binder以及NSLog看下index对应的值:

在间接符号表中非懒加载符号从
20开始供两个,懒加载从22开始,这也就对应上了。这也就验证了懒加载和非懒加载符号都在间接符号表中能对应上。
总结

libffi
inlinehook
https://www.jianshu.com/p/7954e6cde245
越狱和防护相关内容
https://www.desgard.com/2020/08/05/why-hook-msg_objc-can-use-asm-2.html
https://www.die.lu/core/index.php/2020/05/30/299/
dobby










