HOOK概述
HOOK
,中文译为“挂钩”或“钩子”。在iOS
逆向中是指改变程序运行流程的一种技术。通过HOOK
可以让别人的程序执行自己所写的代码。在逆向中经常使用这种技术。所以在学习过程中,我们重点要了解其原理,这样能够对恶意代码进行有效的防护
HOOK
示意图
iOS
中HOOK
技术的几种方式:
Method Swizzle
:主要用于OC
方法,利用OC
的Runtime
特性,动态改变SEL
(方法编号)和IMP
(方法实现)的对应关系,达到OC
方法调用流程改变的目的fishhook
:MachO
文件的工具。利用MachO
文件加载原理,通过修改懒加载和非懒加载两个表的指针,达到对C函数
进行HOOK
的目的Cydia Substrate
:原名为Mobile Substrate
,它的主要作用是针对OC
方法、C函数
以及函数地址进行HOOK
操作。当然它并不是仅仅针对iOS
而设计的,安卓一样可以使用。官方地址
Method Swizzle
利用OC
的Runtime
特性,动态改变SEL
(方法编号)和IMP
(方法实现)的对应关系,达到OC
方法调用流程改变的目的。主要用于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
(方法欺骗)
多种
HOOK
方式
class_addMethod
方式:让原始方法可以被调用,不至于因为找不到SEL
而崩溃class_replaceMethod
方式:直接给原始的方法替换IMP
method_setImplementation
方式:直接重新赋值新的IMP
Cydia Substrate
MobileHooker
顾名思义用于
HOOK
。它定义一系列的宏和函数,底层调用objc
的runtime
和fishhook
来替换系统或者目标应用的函数其中有两个函数:
MSHookMessageEx
:主要作用于Objective-C
方法void MSHookMessageEx(Class class, SEL selector, IMP replacement, IMP result)
MSHookFunction
:主要作用于C
和C++
函数,Logos
语法的%hook
就是对以下函数做了一层封装void MSHookFunction(void function, void* replacement, void** p_original)
MobileLoader
MobileLoader
用于加载第三方dylib
在运行的应用程序中。启动时MobileLoader
会根据规则把指定目录的第三方的动态库加载进去,第三方的动态库也就是我们写的破解程序
safe mode
破解程序本质是
dylib
,寄生在别人进程里。 系统进程一旦出错,可能导致整个进程崩溃,崩溃后就会造成iOS
瘫痪。所以Cydia Substrate
引入了安全模式,在安全模式下所有基于Cydia Substratede
的三方dylib
都会被禁用,便于查错与修复
fishHook
获取代码
git clone https://github.com/facebook/fishhook.git
关键函数
FISHHOOK_VISIBILITY int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);
rebindings
:存放rebinding
结构体的数组,可以同时交换多个函数rebindings_nel
:数组的长度
rebinding
结构体struct rebinding { const char *name; void *replacement; void **replaced; };
name
:需要HOOK
的函数名称,C
字符串replacement
:新函数的地址replaced
:原始函数地址的指针
案例1:
HOOK
系统NSLog
函数打开
ViewController.m
文件,定义新函数void my_NSLog(NSString *format, ...) { format = [format stringByAppendingString:@"~~~🍺🍺🍺🍺🍺"]; sys_NSLog(format); }
定义函数指针,用于保存
NSLog
系统函数地址static void (*sys_NSLog)(NSString *format, ...);
viewDidLoad
方法中,写入以下代码:- (void)viewDidLoad { [super viewDidLoad]; struct rebinding reb; reb.name="NSLog"; reb.replacement=my_NSLog; reb.replaced=(void *)&sys_NSLog; struct rebinding rebs[] = { reb }; rebind_symbols(rebs, 1); }
添加
touchesBegan
方法-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { NSLog(@"hahaha"); }
真机运行项目,点击屏幕,输出以下内容:
fishhookDemo[2072:399007] hahaha~~~🍺🍺🍺🍺🍺
fishHook
可以HOOK
我们的C函数
,但是我们知道函数是静态的,也就是说在编译的时候,编译器就知道了它的实现地址,这也是为什么C函数
只写函数声明调用时会报错。那么为什么fishHook
还能够改变C函数
的调用呢?难道函数也有动态的特性存在?
案例2:
HOOK
自定义函数打开
ViewController.m
文件,定义自定义函数void test(NSString *format, ...){ NSLog(@"hahaha"); }
定义新函数
void my_test(NSString *format, ...){ format = [format stringByAppendingString:@"~~~🍺🍺🍺🍺🍺"]; sys_test(format); }
定义函数指针,用于保存
test
自定义函数地址static void (*sys_test)(NSString *format, ...);
viewDidLoad
方法中,写入以下代码:- (void)viewDidLoad { [super viewDidLoad]; struct rebinding reb; reb.name="test"; reb.replacement=my_test; reb.replaced=(void *)&sys_test; struct rebinding rebs[] = { reb }; rebind_symbols(rebs, 1); }
添加
touchesBegan
方法-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { test(@"hahaha"); }
真机运行项目,点击屏幕,输出以下内容:
fishhookDemo[2117:410009] hahaha
在
案例2
中,HOOK
自定义test
函数并没有效果。NSLog
是系统函数,而test
是当前MachO
中的函数,它们之间有什么区别?
fishHook原理探究
一般来说,静态语言通过地址直接访问,例如
案例2
中的test
函数。但对于NSLog
系统函数来说,它属于外部调用函数,编译时并不能找到它的真实地址
NSLog
存储在Fundation
框架中,App
运行在不同系统版本的不同设备上,每个设备中NSLog
的函数地址都各不相同再有,当
App
启动时,会涉及ASLR
(地址空间配置随机加载),导致程序运行它的虚拟内存地址都不一样这些原因,都会造成外部函数的真实地址,在编译时期是无法确定的
那外部函数是如何被调用的?
当
App
启动时,dyld
读取主程序MachO
文件,会加载共享缓存中的系统库,将用到的函数真实地址告诉MachO
对于
MachO
中的代码段来说,它是只读的。在运行时,无法直接修改外部函数的真实地址对于上述情况,苹果采用
PIC
技术(位置独立代码),在MachO
调用外部函数时,在可读可写的数据段,定义符号,占8字节
,用来存放外部函数的地址。编译时,暂存占位地址。运行时,dyld
将符号绑定真实函数地址。对于代码段来说,并没有任何改变
故此,外部调用函数,并不是直接地址访问,而是通过符号找到地址。这跟
OC
中SEL
与IMP
的对应关系非常相似。这种机制,可以让开发者动态HOOK
外部调用函数在
OC
中动态改变SEL
与IMP
的对应关系,对于外部调用函数,动态改变的是符号和地址的对应关系,上述操作称为:符号表重绑定
案例1
查看
NSLog
加载前的占位地址查看
MachO
文件
NSLog
函数,存储在懒加载符号表中。dyld
加载MachO
时,绑定非懒加载符号和弱引用符号,而懒加载符号,则是在首次使用时动态绑定打开
ViewController.m
文件,修改viewDidLoad
代码- (void)viewDidLoad { [super viewDidLoad]; NSLog(@"begin"); struct rebinding reb; reb.name="NSLog"; reb.replacement=my_NSLog; reb.replaced=(void *)&sys_NSLog; struct rebinding rebs[] = { reb }; rebind_symbols(rebs, 1); NSLog(@"end"); }
在
begin
和end
设置两处断点,真机运行项目
使用
image list
命令,找到程序虚拟内存的首地址
- 首地址:
0x102c48000
ASLR
:0x2c48000
查看
NSLog
在MachO
中的偏移地址和占位地址
- 偏移地址:
0xC000
- 占位地址:
0x1000064C0
在
lldb
中,使用程序首地址 + 偏移地址
,找到NSLog
加载前的占位地址
- 在
lldb
中,获取的NSLog
占位地址为0x102c4e4c0
,和MachO
中看到的0x1000064C0
并不一样因为
MachO
中的占位地址,还要加上程序启动时的ASLR
随机偏移地址
占位地址 + ASLR = 0x102c4e4c0
,和lldb
中读取出的地址一致
案例2
查看
NSLog
加载后的真实地址承接
案例1
,断点向下执行
在
lldb
中,使用程序首地址 + 偏移地址
,找到NSLog
加载后的真实地址
- 之前的占位地址
0x102c4e4c0
,被真实地址0x19d9327f0
替换使用
dis -s 0x19d9327f0
命令,读取地址中的代码dis -s 0x000000019d9327f0 ------------------------- Foundation`NSLog: 0x19d9327f0 <+0>: sub sp, sp, #0x20 ; =0x20 0x19d9327f4 <+4>: stp x29, x30, [sp, #0x10] 0x19d9327f8 <+8>: add x29, sp, #0x10 ; =0x10 0x19d9327fc <+12>: adrp x8, 311278 0x19d932800 <+16>: ldr x8, [x8, #0xb70] 0x19d932804 <+20>: ldr x8, [x8] 0x19d932808 <+24>: str x8, [sp, #0x8] 0x19d93280c <+28>: add x8, x29, #0x10 ; =0x10
- 指向
NSLog
代码
案例3
查看交换后的函数地址
承接
案例2
,断点向下执行
在
lldb
中,使用程序首地址 + 偏移地址
,找到NSLog
交换后的函数地址
- 原本
NSLog
的真实地址为0x19d9327f0
,交换后变为0x102c4d57c
使用
dis -s 0x102c4d57c
命令,读取地址中的代码dis -s 0x102c4d57c ------------------------- fishhookDemo`my_NSLog: 0x102c4d57c <+0>: sub sp, sp, #0x30 ; =0x30 0x102c4d580 <+4>: stp x29, x30, [sp, #0x20] 0x102c4d584 <+8>: add x29, sp, #0x20 ; =0x20 0x102c4d588 <+12>: sub x8, x29, #0x8 ; =0x8 0x102c4d58c <+16>: mov x9, #0x0 0x102c4d590 <+20>: stur x9, [x29, #-0x8] 0x102c4d594 <+24>: str x0, [sp, #0x10] 0x102c4d598 <+28>: mov x0, x8
指向
my_NSLog
代码由此可见,
HOOK
外部的C函数
,本质就是在修改符号和地址的对应关系
符号绑定的过程
Symbol Table
:符号表,⽤来保存符号String Table
:字符串表,⽤来保存符号的名称Indirect Symbol Table
:间接符号表,保存使⽤的外部符号。更准确⼀点就是使⽤的外部动态库的符号。是Symbol Table
的子集代码中的函数名、变量名、方法名,在项目编译后,都会生成一张符号表。符号之间也有差别,分为内部符号和外部符号
内部符号是当前
MachO
中的符号,而外部符号又称为间接符号,例如:系统库、动态库中的符号
符号按可见性划分,分为全局符号和本地符号
- 全局符号对整个项目可见
- 本地符号仅对当前文件可见
案例1:
符号绑定的过程
打开
ViewController.m
文件,写入以下代码:#import "ViewController.h" @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; NSLog(@"第一次外部函数的调用!"); NSLog(@"第二次外部函数的调用!"); } @end
真机运行项目,来到
viewDidLoad
方法:
- 两次
NSLog
的指令都是bl 0x1022ba4d8
使用
image list
命令,找到程序虚拟内存的首地址
- 首地址:
0x1022b4000
使用
NSLog地址 - 首地址
计算偏移地址
- 偏移地址:
0x64d8
打开
MachO
文件,找到偏移地址为0x64d8
的位置
- 处于
__TEXT,__stubs
中__stubs
:符号桩,本质上就是一段代码,用于跳转到懒加载符号表中,找到对应符号的值0x64d8
即是NSLog
的桩在
fishHook
原理探究中,多次提到跳转到占位地址,这种说法并不准确。真实的情况是,会跳转到外部符号的桩,本质上就是一段代码
案例2:
查看桩对应的代码
查看
MachO
文件,找到桩对应的值
1F2003D530D9025800021FD6
:以二进制指令的形式存储的代码单步调试,来到
NSLog
函数
使用
x 0x1022ba4d8
命令,读取地址中的值,以二进制形式显示
1F2003D530D9025800021FD6
,和MachO
中对应的指令一致有此可见,
1F2003D530D9025800021FD6
对应的就是上述三句汇编代码
案例3:
三句汇编代码的含义?
断点执行到
br x16
指令
br
指令:跳转到x16
寄存器存储的地址查看
x16
寄存器存储的地址,再减去ASLR
偏移地址
- 得到地址:
0x10000658c
查看
MachO
文件,找到0x10000658c
地址
- 指向懒加载符号表中
NSLog
符号的值三句汇编代码的含义:找到懒加载符号表中的地址去执行
案例4:
懒加载符号表中的地址,指向的代码是什么?
在
MachO
中,找到偏移地址0x658c
,指向__TEXT,__stubs_helper
中的代码
b
指令,跳转到偏移地址为0x6574
的位置执行代码在
MachO
中,找到偏移地址为0x6574
的位置
x16
寄存器,偏移地址为0x8000
br
指令,跳转到偏移地址为0x8000
位置执行代码在
MachO
中,找到偏移地址为0x8000
的位置
- 处于非懒加载符号表中
- 执行
dyld_stub_binder
函数,用于符号绑定懒加载符号表中的地址,指向寻找并执行
dyld_stub_binder
函数的代码
案例5:
dyld_stub_binder
也是外部函数,它的地址是如何找到的?在
MachO
中,可以看到dyld_stub_binder
函数的偏移地址为0x8000
,但全是0
,说明此时还没有值
dyld_stub_binder
函数,同样是外部函数,存储在非懒加载表中当
dyld
加载主程序时,会绑定非懒加载符号和弱引用符号,所以dyld_stub_binder
函数的值,在程序启动时被dyld
直接绑定真机运行项目,读取
首地址 + 0x8000
地址中的值
使用
dis -s 0x19c2e6f94
命令,读取地址中的代码libdyld.dylib`dyld_stub_binder: 0x19c2e6f94 <+0>: stp x29, x30, [sp, #-0x10]! 0x19c2e6f98 <+4>: mov x29, sp 0x19c2e6f9c <+8>: sub sp, sp, #0xf0 ; =0xf0 0x19c2e6fa0 <+12>: stp x0, x1, [x29, #-0x10] 0x19c2e6fa4 <+16>: stp x2, x3, [x29, #-0x20] 0x19c2e6fa8 <+20>: stp x4, x5, [x29, #-0x30] 0x19c2e6fac <+24>: stp x6, x7, [x29, #-0x40] 0x19c2e6fb0 <+28>: stp x8, x9, [x29, #-0x50]
案例6:
NSLog
函数,首次加载和非首次加载的区别真机运行项目,首次加载
NSLog
函数
- 先找到桩里面的代码
- 找到懒加载符号表中的地址去执行
- 指向
__stubs_helper
中的代码- 寻找并执行
dyld_stub_binder
函数- 符号表重绑定
再次加载
NSLog
函数
- 找到桩里面的代码
- 找到懒加载符号表中的地址去执行
- 首次加载
NSLog
函数,懒加载符号表中的地址,因重绑定而修改- 修改后的值,直接指向
NSLog
函数的真实地址
通过符号找到字符串
fishHook
提供的rebinding
结构体,其中name
为需要HOOK
的函数名称作用:当找到相应的符号,再通过符号找到字符串,然后和
name
进行字符串比较,如果匹配成功,则替换函数指针
案例1:
通过懒加载符号表中的符号,找到间接符号表中的相同符号,再找到符号对应的值
__la_symbol_ptr
懒加载符号表中的符号和顺序,跟间接符号表一致
在
Dynamic Symbol Table
中,找到NSLog
的值
案例2:
间接符号表中
NSLog
的值为0xB8
,转为10进制
为184
184
对应的是此符号在符号总表中的角标在
Symbol Table
中,通过角标找到符号,再找到String Table Index
的值
案例3:
符号表中
NSLog
的String Table Index
值为0xCE
,表示在字符串表中的偏移地址在
String Table
中,找到首地址
首地址
:0x11230
通过
首地址 + 偏移值 = 0x112FE
,找到对应字符串
_
是函数的开始,.
是分隔符 。5F
从_
开始,往后读取_NSLog
,遇到分隔符结束
总结
HOOK
- 钩子,改变程序执行流程的一种技术
Method Swizzle
利用OC
运行时的特性,修改SEL
和IMP
的对应关系,达到对OC
方法HOOK
的目的
IMP
,本质上是函数指针method_exchangeImplementations
:交互两个IMP
class_replaceMethod
:替换某个SEL
的IMP
,如果没有该方法,使用class_addMethod
添加method_getImplementation
、method_setImplementation
:获取和设置某个方法的IMP
,很多三方框架都使用这种方式
fishHook
MachO
文件的加载原理,动态修改懒加载和非懒加载两张符号表- 可以
HOOK
系统函数,但是无法HOOK
自定义函数
fishHook
原理解析
- 共享缓存:
iOS
系统有一块特殊的位置,存放公用动态库。即:动态库共享缓存(dyld shared cache
)PIC
技术:由于外部函数调用,在编译时期无法确定内存地址。苹果采用PIC
技术(位置独立代码),在MachO
文件的DATA
段,建立懒加载和非懒加载两张符号表,里面存放执行外部函数的指针符号绑定过程
- 外部函数调用,先找到桩里面的代码,
__TEXT,__stubs
- 找到懒加载符号表中的地址去执行
外部函数,首次加载
- 懒加载符号表中的地址,指向
__TEXT,__stubs_helper
中的代码- 通过代码寻找并执行
dyld_stub_binder
函数- 作用:符号表重绑定
外部函数,非首次加载
- 懒加载符号表中的地址,因重绑定而修改
- 修改后的值,直接指向外部函数的真实地址
dyld_stub_binder
函数
- 存储在非懒加载符号表中
- 当
dyld
加载主程序时,符号被dyld
直接绑定通过符号找到字符串
fishHook
利用Lazy Symbol Table
→Indirect Symbol Table
→Symbol Table
→String Table
,通过重绑定修改指针的值达到HOOK
目的
网友评论