今天要和大家分享的是Facebook提供的一个动态修改链接mach-O文件的工具fishhook。它利用MachO文件加载原理,通过修改懒加载和非懒加载两个表的指针达到C函数HOOK的目的。
这个工具本身代码才两百多行,如果你对MachO有一定的了解推荐读者阅读其源码.本文将从fishhook的使用切入,带着大家探索其原理。废话不多说,我们上代码。
fishhook简单使用它所提供的接口拿到fishhook之后你会发现,它真的非常简单,这家伙只有两个文件“fishhook.h”和“fishhook.c”。它提供的接口仅有一个结构体和两个函数:
rebinding结构体用来确定你要HOOK的函数和要交换的函数地址。
structnrebinding{nnconstcharn*name;n//需要HOOK的函数名称,C字符串nnvoidn*replacement;n//新函数的地址nnvoidn**replaced;n//原始函数地址的指针!nn};
注意replaced:在使用该结构体时,由于函数内部要修改外部指针变量所保存的值,所以这里是指针的指针(二级指针)。
rebind_symbols和rebind_symbols_image函数用来HOOK的两个方法。只不过后者是指定某个镜像文件的时候使用。所以一般我们直接使用前者。
镜像文件:比如NSLog函数是在Fundation框架中,那么Fundation在内存中就是一个镜像文件。
intnrebind_symbols(nstructnrebindingrebindings[],nsize_tnrebindings_nel);nnnnintnrebind_symbols_image(nvoidn*header,nnintptr_tnslide,nnstructnrebindingrebindings[],nnsize_tnrebindings_nel);
多说无益,玩一下最重要!
HOOK一下NSLog我们新建一个SingleView的项目。在ViewDidLoad中对系统的NSLog函数进行HOOK。
//函数指针nnstaticvoidn(*sys_nslog)(nNSStringn*format,...);nnnn//定义一个新的函数。HOOK成功后NSLog调用时,会来到这里nnvoidnmyNSLog(nNSStringn*format,...){nnformat=[formatstringByAppendingString:@n"n上钩了!nn"n];nnsys_nslog(format);n//调用系统的NSLog,HOOK成功后sys_nslog指针保存的是Fundation中NSLog的地址nn}nnnn-(nvoidn)viewDidLoad{nn[nsupernviewDidLoad];nnnn//准备rebinding结构体nnstructnrebindingnslog;nnnslog.name=n"NSLog"n;n//需要HOOK的函数名称nnnslog.replacement=myNSLog;n//新函数的地址nnnslog.replaced=(nvoidn*)&sys_nslog;n//原始函数指针nnnn//准备数组,将一个或多个rebinding结构体放进去。nnstructnrebindingrebs[n1n]={nslog};nn/**nnarg1:存放rebinding结构体的数组nnarg2:数组的长度nn*/nnrebind_symbols(rebs,n1n);nn}nnnn-(nvoidn)touchesBegan:(nNSSetn<nUITouchn*>*)toucheswithEvent:(nUIEventn*)neventnn{nnNSLogn(@n"点击了屏幕!"n);nn}
运行之后,可以发现NSLog的函数调用已经被HOOK成功了!几行代码轻松搞定,看起来很厉害的样子。(PS:但是剧情如果这样发展,太过简单了。我预感,马上会有一个转折...)接下来迫不及待的,我们HOOK一下自定义的函数,结果你会发现怎么都HOOK不到。姿势和HOOK系统的NSLog一样(代码我就不贴了,不信你自己去~~浪费时间~~尝试)。那为什么系统函数能HOOK成功,自定义的却HOOK不到呢?so~,往下看...
fishHook原理分析MachO首先我们要了解一个东西。我们写好的代码,生成的iOS程序其实是一个可执行文件。这个文件格式是MachO格式,所以一般我们称其为MachO文件。在刚才的APP包里面,它长这样:
这个文件里面包含的就是数据和指令。比如你定义的类、方法、全局变量、方法实现等等。我们为什么要讨论MachO?因为结合上面的疑问我们思考一个问题:自定义函数和系统函数,在文件位置上有什么区别?
自定义函数在本MachO文件中,在运行时刻进入内存,自定义函数在本镜像文件中。系统函数在系统框架中,在运行时刻进入内存,系统函数在系统的动态库中,比如NSLog在Fundation这个镜像文件中。那既然如此我们就可以得到这样的结论:自定义的函数,在编译时刻,编译器就可以确定函数的实现地址(在MachO文件中的偏移地址)。但是系统函数是没办法知道的。那么在CPU执行我们的代码的时候,我们是如何告诉CPU,我们需要调用系统函数。以及如何知道系统函数的地址的呢?这里就要提到PIC技术了。
PIC(PositionIndependcode)技术PIC翻译过来就是位置独立代码。
说人话:比如当你的程序要调用一个MachO外部函数的时候,编译器是没办法知道该函数的地址的。所以它在MachO文件里面生成一个列表,列表里面放指针。让当前的系统函数调用指向这个列表里面对应的指针。等到我们的MachO文件加载进入内存时,再将系统函数的真实地址,一个一个的赋值给列表中的指针。
那么这个列表,我们称为符号表。这里面的指针,我们称为符号。给里面的指针赋值的过程,我们称为符号绑定。那么说到这里,我想很多童鞋已经猜到了。fishhook之所以HOOK不了自定义的函数,就是因为自定义的函数没有通过符号寻找地址这个过程。而系统函数是通过符号去绑定实现地址的。fishhook就是利用这一点,去修改了系统函数的符号达到HOOK的目的。其实我们通过fishhook的函数名称就不难看出来rebind_symbols符号重绑定。
这篇文章到这里其实已经差不多了。不过理论总归是理论,很多人总想眼见为实的探索。如果你想探索,那么你准备酒,我准备肉MachOView,我们往下走...
fishHook原理探索观察符号绑定和重绑定过程MachO文件内部存有代码和数据,代码放在_TEXT段(代码段)中,数据放在_DATA段(数据段)中。数据段除了全局变量、常量、自定义类还有很多东西,比如我们刚才提到的符号表。接下来,我们要借助一个图形化工具,去分析我们的MachO文件。这个工具就是MachOView,它长这样:
我们将刚才的MachO文件放入到MachOView里面分析一下。可以看到下图。
在这里面我们就可以清晰的看到符号表在文件中长啥样了。并且最重要的是能够看出符号在文件中的偏移位置。
有了这个偏移值,我们可以在项目运行的时候,通过LLDB,观察的符号的绑定和重绑定过程。
动态调试1、来到我们原来的代码,在这几行打上断点并运行。
2、第一次断点触发时,我们利用LLDB查看符号中的值。
首先查看MachO文件的地址:通过imagelist指令,查看所有镜像文件(MachO文件)的地址。
然后通过memoryread指令读取内存。因为符号是指针,所以读取8个字节的数据。
但是这里面存放的到底是不是NSLog的真实地址呢?我们如何查看。
我们可以利用dis-s查看反汇编。由于iOS的小端模式,所以读数据从右往左读。比如:图中数据d0eae10201000000那么读取出来的地址是0x0102e1ead0
然后我们通过汇编发现,这里并不是NSLog的地址。如果是,这里会显示Fundation框架中的NSLog函数(等下会看到现象)。其原因是因为NSLog是懒加载符号,此时是第一次调用,还没有绑定符号。此时如果跟汇编,我们最终会看到dyld_stub_binder函数的调用。所以,我们过掉断点。
3、来到第二个断点处
我们重复上面的步骤,查看符号的内存数据,然后取出里面的值,通过dis反汇编查看。你会发现此刻已经绑定了符号。可以清晰的看到里面是NSLog的实现地址了。(这个过程就是符号绑定!)
接下来,我们来看看rebind_symbols之后,我们的符号表里面保存的地址变成了啥?
4、来到第三个断点处(第42行)
我们重复上面的步骤,查看符号的内存数据,然后取出里面的值,通过dis反汇编查看。你会发现此刻符号内的数据已经替换成了自定义的函数地址了。(符号重绑定成功!)
经过一系列动态调试,我们可以观察到符号绑定和重绑定的全部过程。但是,fishhook是怎么通过我们传给它的一个字符就找到了我们NSLog的符号的呢?我们往下看...
通过符号找到字符串我们还是来到MachOView里面,注意,我们数一下,NSLog这个符号,在懒加载符号表里面是第几个?很明显第一个。
所以,与懒加载表对应的另外一张表就出来了。indirectSymbols。懒加载表里面NSLog是第一个,那么它在indirectSymbols表里面也就是第一个。
接下来,将indirectSymbols里面对应的Data值换算成为10进制。
0x81的十进制是129.为什么要转换这个数据,因为它又是另外一个列表的角标。
这个列表就是Symbols,我们查看一下。
那么到了Symbols里面就接近我们最终的字符常量了。注意在Symbols里面有StringTable的Index值。这里是0xC9。这就说明,在StringTable里面,偏移0xC9的地址就是我们NSLog字符。
我们来到StringTable里面查看一下。我们通过起始位置加上偏移便可以定位到NSLog字符了。
函数名称在字符表里面都是5F也就是_开始然后00也就是.结束!这也就是为什么你给fishhook一个系统函数名称,它能够帮你顺利HOOK到系统函数的原理了。通过上面一顿分析,我相信官方的图你也能看懂了。官方是以close函数为例:
刚才我们通过动态调试加MachOView的分析梳理了整个符号绑定以及重绑定的过程。这个过程也是fishhook能够HOOK系统函数的原理。了解了其原理接下来你可以分析一下它的源码了。Facebook那帮家伙是怎么通过代码实现刚才的过程?好在fishhook的源码并不多,但是如果你不了解MachO的相关API,这百来行代码也会让你怀疑人生。
还有一个问题,fishhook这个工具我们可以用它来干啥?我例举个实际的运用场景:比如最近抖音团队分享的二进制重排启动优化。在这个优化的过程中,我们如何定位到启动时所有的OC方法?我想你已经猜到了,通过objc_msgSend函数的HOOK。
原文作者:逻辑iOS技术号(Hank)
公众号:
显示全部
收起
最新更新 | 文章排行 | 滇ICP备2023006777号 | 网站地图