一、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
中close
index
为1061
。 - 在
Indirect Symbol Table
1061
对应的角标为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
。
SEG_LINKEDIT
获取和LC_SYMTAB
与LC_DYSYMTAB
不同是因为在Load Commands
中本来就不同,我们解析其它字段也要做类似操作。具体如下:
- 根据
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_bindings
就是存放lazy
和non-lazy
表中的data
数组:
- 遍历懒加载/非懒加载符号表。
- 读取
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
开始,这也就对应上了。这也就验证了懒加载和非懒加载符号都在间接符号表中能对应上。
demo
总结
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