1.什么是HOOK
HOOK,中文译为“挂钩”或“钩子”。在iOS逆向开发中是指改变程序运行流程的一种技术。通过hook可以让别人的程序执行自己所写的代码。在逆向中经常使用这种技术,所以在学习过程中,要重点了解其原理,这样能够对恶意代码进行有效的防护。
fishhook源码 密码:eqsv
2.iOS中HOOK技术的几种方式
2.1 Method Swizzle
利用OC的Runtime特性,动态改变SEL(方法编号)和IMP(方法实现)的对应关系,达到OC方法调用流程改变的目的。主要应用于OC方法。
2.2 fishhook
它是Facebook提供的一个动态修改链接mach-O文件的工具。利用MachO文件加载原理,通过修改懒加载和非懒加载两个表的指针达到C函数HOOK的目的。
2.3 Cydia Substrate
Cydia Substrate原名为Mobile Substrate,它的主要作用是针对OC方法、C函数以及函数地址进行HOOK操作,它不仅仅是针对iOS而设计的,安卓一样也可以用。
总结:Method Swizzle与Cydia Substrate之所以能够hookOC方法,是利用了的OC动态特性(不是直接调用方法实现的地址),而我们都知道C语言是一种静态语言,其函数调用是直接通过地址来访问的,那么我们该如何对C函数进行hook呢?
3 fishhook的使用以及原理探究
3.1 fishhook的主要使用的函数介绍
//结构体rebinding
struct rebinding {
const char *name; //需要HOOK的函数名称,C字符串
void *replacement; //新函数的地址
void **replaced; //原始函数地址的指针!
};
/// 通过传入的rebinding类型数组并对其中的方法进行hook
/// @param rebindings rebinding类型数组
/// @param rebindings_nel hook的函数数量
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);
/// 指定一个镜像文件进行hook
/// @param header 指向hook的镜像文件的指针
/// @param slide ASLR值
/// @param rebindings rebinding类型数组
/// @param rebindings_nel hook的函数数量
int rebind_symbols_image(void *header,
intptr_t slide,
struct rebinding rebindings[],
size_t rebindings_nel);
3.2 使用fishhook hook 系统库中的NSLog函数。
首先创建一个fishhookDemo的工程文件,并拖入fishhook源码文件。
#import "ViewController.h"
#import "fishhook.h"
@interface ViewController ()
@end
@implementation ViewController
void func(const char *str) {
NSLog(@"%s", str);
}
- (void)viewDidLoad {
[super viewDidLoad];
//创建一个结构体变量nslogRebinding
struct rebinding nslogRebinding;
nslogRebinding.name = "NSLog"; //设置要hook的函数名
nslogRebinding.replacement = New_NSLog; //设置新函数的地址
nslogRebinding.replaced = (void *)&Old_NSLog; //传入指向NSLog函数的指针的地址,因为我们需要获取系统函数NSLog的地址,因此需要传入指针的地址。
struct rebinding rebindings[] = {nslogRebinding};
//保留原始地址
//设置新地址
//Hook的函数名字
rebind_symbols(rebindings, 1);
}
//原始函数的指针
static void (*Old_NSLog)(NSString *format, ...);
void New_NSLog(NSString *format, ...) {
format = [format stringByAppendingString:@" Hook到了"];
//调用原始函数
Old_NSLog(format);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"hookNSLog");
}
@end
运行程序,点击手机屏幕,执行结果如下:
3.2 使用fishhook hook 自定义的函数。
首先创建一个fishhookDemo的工程文件,并拖入fishhook源码文件。
#import "ViewController.h"
#import "fishhook.h"
@interface ViewController ()
@end
@implementation ViewController
void func(const char *str) {
NSLog(@"%s", str);
}
- (void)viewDidLoad {
[super viewDidLoad];
struct rebinding funcRebinding;
funcRebinding.name = "func";
funcRebinding.replacement = new_func;
funcRebinding.replaced = (void *)&old_func;
struct rebinding rebs[] = {funcRebinding};
rebind_symbols(rebs, 1);
}
void (*old_func)(const char *str);
void new_func(const char *str) {
NSLog(@"Hook了");
old_func(str);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
func("hello");
}
@end
运行程序,点击手机屏幕,执行结果如下:
3.3 比较这两次程序运行的结果我们发现自定义的函数hook不成功,这是为什么呢?
首先我们先来探讨一下,在我们的应用程序编译的时候,知不知道NSLog的真实地址?在我们的应用程序运行之前,是存在在磁盘中的,而应用程序使用的一些系统库中的函数,有可能还未加载到内存中的共享缓存中,我们又知道,C函数的调用是静态调用,在编译的时候需要确定其地址,但是这个函数及包含这个函数的库有可能还未加载到内存中,而且每个库每次加载到内存中的时候由于ASLR(随机地址偏移)机制的存在,它在内存中的地址都是不确定的,那该如何解决这个问题呢?
在应用程序的MachO中,text(代码段)是只读可执行的,Data(数据段)是可读可写的,这些聪明的程序员就利用这个特点想出了PIC(Position Independent Code)技术(位置无关代码技术)。这种技术的原理是这样的。假设代码中使用了系统库中的某个函数,在对应的汇编代码中就需要执行bl(bl 函数地址,bl是跳转指令,跳转到后面所示地址继续执行汇编指令)指令,因为text不可修改,因此这个bl指令后的地址就不允许被修改,但是由于Data段是可读可写的,就在Data段中放一个占位符(8字节大小,初始值是0x00000000),先将bl指令后的地址在编译的时候写成这个占位符在Data段中的地址,而dyld在将应用程序加载到内存中的时候,就将获取到的bl后要跳转的函数的真实地址通过一些方法写入到这个Data段占位符所在的内存地址中,这个占位符相当于一个变量,也叫做符号,dyld将符号中的代码地址进行修改就叫做符号绑定。
3.4 PIC(位置无关代码)技术原理
首先在应用程序中viewDidLoad方法中编写如下代码,并在这两次NSLog函数调用的时候打上断点如下图所示:
在XCode运行之前设置如下选项:
然后运行程序,如下图所示:
我们看到在首次执行到NSLog函数的时候是bl 0x102ef64ac这个地址的,那这个地址指向的是哪里呢?首先使用image list查看当前应用程序加载到内存中的地址,如下图所示:
图中红色方框圈起来的就是当前应用程序在内存中的虚拟地址,去掉0x0000000100000000(pageZero大小,4G也就是2的32次方)后就是应用程序在内存中经过ASLR(随机地址偏移)后的地址偏移量,将bl后面的地址值0x102ef64ac 减去0x102ef0000得到64ac就是这次调用的地址在MachO文件中的偏移量,接着使用MachOView来查看一下这个地址所在的位置,如下图所示:
我们看到这个地址指向的是MachO中的代码段也就是右边红框所示位置,bl指令后面跟的是地址,那么系统就要调转到bl后面的地址继续执行指令,也就是说这个地方其实是一段要执行代码,那刚刚我们说的是指向Data段地址不就错了吗,实际上苹果的开发者在设计这个技术的时候并不是这么简单,那么我们就来研究一下这一段要执行的汇编代码是什么。
首先我们发现在MachO中这段二进制代码的值是1F2003D590DA025800021FD6,那么这个值代表什么呢?
在XCode中的调试窗口使用(x 地址 )命令查看内存中的数据段,如下所示:
我们发现与图中红框所示数据一致,因此我们断定,这段数据就是即将要执行的代码段,我们再在XCode中的调试窗口使用dis -s命令查看这段代码段,如下图所示:
再在XCode中跳转到这段汇编代码段,如下图所示:
接着查看这段汇编代码执行到br x16时x16寄存器的值是什么,如下图所示:
再计算出x16寄存器中的值在MachO中的位置(使用0x102ef6560 - 0x102ef0000)为6560,而6560这个地址数据在MachO中是这样的:
那么它是如何知道要执行6560这个地址中的代码的呢?其实是通过查懒加载符号表得到的,如下图所示:
懒加载符号表中第一个符号NSLog的值就是6560,也就是说,0x102ef64ac那三行要执行的汇编代码的意义就是查找懒加载符号表中对应的符号的值并跳转执行,在MachO文件中Section64(_TEXT,__stubs)这个段记录的就是各个外部符号的1桩,每次系统执行到应用程序这些系统库的函数时,就会去执行这个段中对于函数的符号的桩中的代码,也就是说执行外部符号的流程是这样的,执行某个函数----->执行这个函数对于符号的桩中的代码----->跳转到懒加载符号表中对应符号中的值继续执行代码,上面已经看到了6560所指向的二进制代码块,我们静态分析一下这段代码块的作用:
再在XCode中动态调试查看这段代码的是否如此执行的,如下图所示:
单步调试继续查看:
我们可以清楚的看到与MachO中的二进制代码是一致的,接下来执行的代码就是要调用红框所示dyld_stub_binder这个方法执行了,通过以上探究我们发现,懒加载符号表中的各个符号的初始值都是要先跳转到dyld_stub_binder这个方法去执行绑定符号的操作的。
我们再来分析一下dyld_stub_binder这个函数,首先,这是一个外部符号,实际上它跟NSLog一样都是外部符号,那么既然这样,系统又是如何知道dyld_stub_binder这个函数的地址的呢?,如下图所示,在XCode中可以通过dyld_stub_binder的地址清楚查看到这个函数的二进制代码的。
那么这又是如何做到的呢?我们首先查看以下dyld_stub_binder这个函数地址是如何得到的,我们回到MachO文件查看一下是如何找到的,如下图所示:
那么MachO中0x10008000地址的数据是什么内容呢?我们再来查看一下,如下图所示:
结果就发现这个地址存储的是非懒加载符号的符号dyld_stub_binder的地址。这个非懒加载符号表中的dyld_stub_binder地址数据初始值是0,那么系统是什么时候将它的真实地址写入进去的呢?其实是在dyld链接主程序之后,绑定非懒加载符号表的时候,主程序main函数未运行之前由dyld强制绑定的,在主程序的main函数运行之前真实地址就存在了,那么我们如何验证呢?
首先我们查看一下这个非懒加载符号表中dyld_stub_binder符号的地址中的数据,dyld_stub_binder的地址就等于主程序首地址(0x102ef0000)加上0x8000为0x102EF8000,使用x命令查看此地址内存中的数据,以及这个数据指向的地址的汇编代码,如下图所示:
正好与前面我们发现的dyld_stub_binder的地址一致。
我们使用调试命令x来看看未执行dyld_stub_binder函数前懒加载符号表中NSLog符号中的数据:
如上图所示,初始值为0x102ef6560
再来看看执行完dyld_stub_binder之后这个地址中的数据,如下图所示:
那么我们再次调用NSLog函数,函数的执行流程是怎样的呢,与第一次调用NSLog函数相比,也是同样的去执行桩里面对应的符号的代码,然后找到懒加载符号表对应符号地址,也是跳转到这个地址中所存储的数据继续执行指令,只不过第二次调用的时候,其中NSLog的真实地址就已经存在了,就直接调用NSLog的地址了,不需要再通过dyld_stub_binder函数绑定符号了。
4.总结
fishhook的原理就是通过修改符号与地址的对应关系来进行hook,所以并不是所有C函数的调用都是静态的,外部函数的调用就是动态的,它可以绑定也可以重绑定,方法名、变量名、函数名以及类名等都是符号,会生成一张符号表。
补充说明:
(内部符号:项目内的所有符号,又分为本地符号以及全局符号)
(外部符号(间接符号):外部函数以及方法,本machO以外的)