在上上篇文章iOS应用安全4 -- 代码注入,窃取微信登陆密码中咱们知道了如何hook App中的OC方法,即便用OC的运行时机制,在运行期间替换相应方法的实现。
那么有没有想过咱们如何才能hook C语言写的函数呢?
这就要使用到咱们今天所要讲的fishhook了,下载地址。git
fishhook是facebook提供的一个动态修改连接mach-O文件的工具。利用MachO文件加载原理,经过修改懒加载和非懒加载两个表的指针达到C函数hook的目的。github
话很少说,先把它下载下来添加到工程。 算法
// rebinding--->从新绑定
struct rebinding {
const char *name; // 要hook的函数名称,char* 是C语言的字符串
void *replacement; // 用来 替换原始函数 的函数的地址
void **replaced; // 被替换掉的原始函数的地址,注意是void**
};
复制代码
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);
复制代码
很明显,第二个函数是对第一个函数的扩展,那么就先来分析一下第一个函数如何使用吧。数组
为了简单起见,就再也不重签名一个App再测试了,直接在新建的项目的ViewController
类中hook
系统的C函数NSLog
。代码不多,就下面这些xcode
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// 就以hook系统的NSLog函数为例
// 建立一个结构体变量
struct rebinding rebind;
rebind.name = "NSLog"; // 注意是C语言的字符串不用加@
rebind.replacement = cus_log; // C语言函数名 即 函数指针
// 这里传的是函数地址的地址,为的是在rebind_symbols函数内部附上NSLog的地址
rebind.replaced = (void *)&sys_log;
// 定义rebinding数组,里面只有一个元素。固然,也能够存多个
struct rebinding rbs[] = {rebind};
rebind_symbols(rbs, 1);
}
// 定义函数指针用来`接收`系统的NSLog函数的地址
static void (*sys_log)(NSString *format, ...);
// 自定义打印函数,用来替换NSLog
void cus_log(NSString *format, ...) {
// 给打印的字符串后面添加一个标记
format = [format stringByAppendingString:@"-------->cus_log的标记"];
// 调用系统NSLog函数
sys_log(format);
}
// 测试
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"点击屏幕了😂😂😂");
}
复制代码
我认为上面的代码注释的很清晰了,这里再也不重复解释。但有一点仍是须要说明一下,rebinding
结构体的replaced
变量是void**
类型,若是对此不太了解的能够本身搜索一下C语言传值和传地址的区别,也能够看我前几天写的文章数据结构与算法2 -- 链表的最下面总结部分,有对这一块的详细解释。缓存
运行效果以下: 安全
上面咱们实现了使用fishhook替换系统C函数NSLog的实现,那么在App中由开发者本身写的C函数能不能使用fishhook替换呢?
试试就知道了,测试代码以下:微信
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
struct rebinding rb;
rb.name = "printHelloWorld";
rb.replacement = hook_printHelloWorld;
rb.replaced = (void *)&app_printHelloWorld;
struct rebinding rbs[] = {rb};
rebind_symbols(rbs, 1);
}
// 一个简单的C语言函数, 假设这个函数是别人App中实现的C语言函数
void printHelloWorld() {
NSLog(@"Hello World!");
}
// 接收printHelloWorld函数的实现地址
static void (*app_printHelloWorld)(void);
// 替换printHelloWorld函数的函数
void hook_printHelloWorld() {
NSLog(@"在Hello World!以前打印一句话,表明了hook成功");
// 调用原始的printHelloWorld函数
app_printHelloWorld();
}
// 测试
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
printHelloWorld();
}
复制代码
运行效果以下,能够看到hook失败了。
由于在Hello World!以前打印一句话,表明了hook成功
这句话并无在Hello World!
打印前打印。 数据结构
P一、为何会失败呢?为何不能hook这个自定义的C函数呢?app
仔细想一想咱们会发现如下两件事!
Method Swizzling
等方法来实现hook OC的方法。P二、为何fishhook能够hook NSLog函数呢?NSLog难道不是C函数吗?
首先说明NSLog
确实是C语言函数,这一点没问题。
之因此fishhook可以hook NSLog函数,这个我默认你已经看过上一篇文章iOS应用安全5 -- main函数调用以前作了些什么?。
上一篇文章主要介绍了MachO文件
和dyld是如何加载应用程序的?
这两部分,那么固然不会无缘无故的来说这两块内容,确定是有用的。
在dyld加载应用程序的过程当中,有一个步骤叫加载共享缓存,相信你们应该都还记得这一步是作什么的?
咱们都知道NSLog是系统库Foundation框架中的一个C函数,而这个Foundation框架自己就是一个真正意义上的动态库,即它具备在多个进程中共享的特性。而咱们平时说的共享缓存就是指这一类系统库。
系统定义的C函数,因为具体的函数实现是在系统共享库中,所以在程序编译期间是没法获取到这个C函数的实现地址,只能经过一种叫符号绑定的方法动态连接到函数名。
这样直接说有点抽象,下面我列举个场景:
一、新建一个工程,在
ViewController.m
的viewDidLoad
方法中写一句NSLog(@"哈哈哈");
二、而后command+B
编译程序,那么在编译到NSLog(@"哈哈哈");
这一句代码时,xcode会判断NSLog
函数是否是系统库里面的函数。
三、发现NSLog
是Foundation
框架里的函数,那么就会将NSLog(@"哈哈哈");
这句代码和符号表中的一个char*
类型的C字符串"NSLog"
进行绑定。
四、当command+R
运行应用程序时,就会在dyld
加载应用程序时将iOS
系统共享缓存区
的Foundation
库加载到内存中。
五、程序运行过程当中,当代码第一次执行到NSLog
函数调用时,就会到Foundation
库的MachO文件中查询NSLog
函数的实现地址。而后将函数的实现地址和符号表中的"NSLog"
字符串进行绑定,这样后面再调用到NSLog
函数时就可以直接找到函数的实现地址。(有点懒加载的意思,因此这种方法也被称为懒绑定)
到这,想必不用我说,也都知道了自定义的C函数是怎么回事了吧?
自定义的C函数,因为函数的实现和函数的调用是在同一个MachO文件(App自己的MachO文件)中,所以在编译连接期间,xcode就直接将函数调用语句和函数的实现地址进行了连接,也就不会有系统C函数的那些步骤了。
理解了上面系统C函数的工做原理,咱们应该也能猜到一些fishhook hook系统C函数的原理了。下面仍然以NSLog函数为例:
从上面咱们知道,当第一次调用
NSLog
函数的时候,系统就会到Foundation
框架中找NSLog
的函数实现地址,与App的MachO文件中的符号表中的"NSLog"
字符串进行绑定。
fishhook就是利用这一点,回过头看fishhook
中的那个函数名rebind_symbols
,翻译为从新绑定符号,再看看那个rebinding
结构体里面有哪些东西?
// rebinding--->从新绑定
struct rebinding {
const char *name; // 要hook的函数名称,char* 是C语言的字符串
void *replacement; // 用来 替换原始函数 的函数的地址
void **replaced; // 被替换掉的原始函数的地址,注意是void**
};
复制代码
是否是一切都是那么的恰到好处?
咱们给rebind_symbols
函数传进去一个rebinding
结构体,包括了符号表中对应NSLog
函数的符号"NSLog"
,而后rebind_symbols
函数经过符号"NSLog"
找到Foundation
框架中NSLog
函数的实现地址,将这个函数地址赋值给replaced
变量返回给咱们,再将符号"NSLog"
绑定的函数地址替换为咱们传进去的函数地址replacement
。(注意:fishhook为了保证返回给咱们的replaced
是正确有值的,必要时它会在rebind_symbols
函数内部先默认调用一次对应的函数)
说了那么大一堆东西,谁知道是真的仍是框个人?
rebind_symbols
函数调用以前手动先调用一次NSLog
函数。如图,添加一句NSLog(@"123456");
,并在调用前打上断点。
lldb
的image list
命令查看主程序MachO在内存中的偏移值(即ASLR)。
符号NSLog
当前绑定的地址在内存中的真实地址。lldb
命令memory read
查看咱们计算出来的那块内存。
符号NSLog
绑定的内存地址保存的值,这个值是一个指针,由于iOS默认是小端模式,高位字节保存在低位地址中,因此这个指针指向的真实地址应该是0x00000001005b6904。lldb
的dis -s
指令反汇编这个地址。能够看到这个地址如今就是一块无心义的地址,也就是符号NSLog
默认绑定的地址。
NSLog(@"123456");
断点停在rebind_symbols
函数以前。
lldb
命令memory read
查看内存,能够发现,同一块内存地址,里面保存的地址已经发生了改变。
lldb
的dis -s
指令反汇编这个地址(注意小端)。
符号NSLog
绑定的那个指针保存的地址已经由最开始的那个无心义的地址变成了Foundation
框架里面的NSLog函数的地址
了。rebind_symbols
函数执行。再次使用lldb
命令memory read
查看那块内存。
符号NSLog
绑定指针保存的Foundation里面的NSLog函数
地址已经被fishhook修改为了fishhook-test里面的cus_log函数
。整个流程就是这样,没有框你吧?
从上面咱们知道了如何经过MachO文件中懒加载符号表中的符号找到函数在内存中的地址,进而hook这个函数。而这整个过程都有一个前提,那就是要找到符号表中的NSLog,那么如何找呢?
别急,仔细看fishhook的rebinding结构体,咱们出来要传入hook函数的地址以外还须要提供一个和系统C函数同名的C字符串。那么确定就是经过这个C字符串来找对应函数的符号了。
String Table
,听名字就知道这是一个字符串表,既然要经过字符串"NSLog"
找函数符号,那么固然就要先从字符串表开始了。String Table
以下:
.
进行分割,使用_
表示后面的是一个函数,仔细看上图中的"._NSLog"
对应的二进制值,能够发现二进制值其实就是每一个字符的ascii
码。"NSLog"
中的字符取出来,转换成对应的ascii
码,再到MachO
的String Table
表中查询,就能够找到"NSLog"
字符串在String Table
表中的偏移量。
"_NSLog"
的偏移量而不是"NSLog"
的偏移量(因此上图计算的结果应该是0x00010F5B,而非0x00010F5C)。"_NSLog"
相对于MachO文件的偏移量 减去 String Table
相对于MachO文件的偏移量便可获得"_NSLog"
相对于String Table
的偏移量。Symbol Table
能够发现,每个符号都对应的有一个String Table Index
的东西,这个东西就是上一步计算出来的。
String Table Index
等于0x9B的,发现它对应的字符串值Value
就是_NSLog
,说明没有找错。"_NSLog"
在符号中对应的位置是105,换算成16进制是0x69。再换到Dynamic Symbol Table
目录下,查找值是0x69的行。
0x69
对应的Symbol
符号也正是_NSLog
。再次印证了没错。而且关键点就在于下面的那个Indirect Address
行,0x10000C000。Load Commands
里面的_PAGEZERO
的大小,这个大小实际上是一个虚拟大小
Indirect Address
真正的偏移量是0xC000,这个0xC000有没有以为很熟悉?好,再看懒加载符号表--->Lazy Symbol Pointers
_NSLog
了,至此终于和上一个标题接上了。经过这篇文章,咱们学到了如下几点:
以前就讲过,hook OC的方法,能够经过3种方式交换方法的实现地址,使得App调用方法1的时候执行交换后的方法2,咱们再调用方法2的时候执行咱们添加的代码和方法1本来的实现。具体能够回头再看看iOS应用安全4 -- 代码注入,窃取微信登陆密码这篇文章。
那么面对这种破解,咱们做为开发者要如何防御呢?
很简单,能够利用本篇文章讲述的fishhook,把runtime
里面的那几个方法交换的C函数所有替换成没法使用的C函数,这样,别人再使用方法交换方法的时候就会调用到咱们替换以后的没法使用的C函数。哈哈😄。
可是这样就彻底能够保证安全了吗?
还差得远呢,别人仍然可使用其余方法来破坏或者绕过你的保护。
攻防永远是对立的,没有绝对的安全,只有相对的安全。黑客若真的铁了心的要破解你的App,你再怎么防御也是没用的。咱们要作的就是要让黑客花费很长很长的时间才能破解出来,让他知难而退便可。