前言
本篇文章开始给大家分享下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的目的。
fishhook官方链接
大概流程 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结构
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
。
AOP
库 Stinger和BlockHook就是使用libbfi
做的。
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 函数的跳转指令。
Dobby gitHub链接
二、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 *)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 *)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 Symbo
l中去找的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的数组中的字符串做对比。
在fishhook
gitHub网址中有一张图说明这个关系
上图是通过符号
查找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
中设置
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工具链接
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
。