在正式开启对fishhook的讲解之前,我先对之前的dyld的内容(应用程序的加载——dyld动态链接器的工作流程)做个回顾。
如上图所示,MachO是可执行文件,其结构分为三大块:
DYLD动态链接器的工作过程:
1,程序的执行是从_dyld_start函数开始
2,_dyld_start函数里面调用了dyld::_main函数
2.1,配置环境变量
2.2,加载共享缓存
2.3,实例化主程序:加载所有需要的Mach-O镜像文件
2.4,加载所有插入的动态库
2.5,链接主程序
2.6,链接插入的库(动态库)
2.7,主程序的初始化
2.8,调用应用程序的入口函数main函数
其中,最关键的步骤是2.7主程序的初始化:
_objc_init
中注册并保存的,而在上面第2步notifySingle函数之后,会调用doInitialization函数,经过一系列函数的调用,最终会调到doModInitFunctions函数,然后依次进行libsystem、libdispatch和libobjc的初始化,而libobjc的初始化调用的就是_objc_init函数。这就形成了一个闭环。
Hook概述
Hook,中文译为“挂钩”或者“钩子”,在iOS的逆向中是指改变程序运行流程的一种技术。通过Hook,可以让别人的程序执行自己写的代码,在逆向中经常会使用到这种技术,所以在学习过程中,需要重点了解其原理,这样能够对恶意代码进行有效的防护。
通过上图我们可以很直观地感受到Hook的威力,它可以改变程序的执行路径、移除程序的校验等等。
iOS中Hook技术主要有3种方式:
fishhook的简单使用
首先,将fishhook代码下载下来:
git clone https://github.com/facebook/fishhook.git
然后将fishhook.c和fishhook.h这两个文件拖入工程:
然后来看一下其使用,这里我hook了系统的NSLog:
运行打印结果如下:
2021-03-30 16:47:49.867281+0800 Test[49189:683319] 我叫李拉维***勾住了
可以看到,成功Hook了系统的NSlog!
接下来再看一个例子:
这里我自定义了一个testFunc函数,然后我使用fishhook来hook该函数,执行之后,最终的结果表明,new_testFunc中的内容并没有被执行,也就是说,testFunc函数并没有被Hook住,这是为什么呢?
共享缓存机制
上面?我回顾的dyld的加载流程中,在dyld::_main函数中做的第二步就是加载共享缓存库。共享缓存库是什么呢?
我们知道,苹果不允许我们开发者上架动态库,我们不管是组件化也好,还是其他的一些第三方工具类也好,都是封装成静态库。
但是苹果的很多系统库基本都是动态库,比如UIKit、Foundation,为什么苹果要将这些系统库做成动态库呢?因为很多APP都会用到这些库,比如UIkit库,几乎没有APP没用到这个库吧。如果每一个App都重新加载一遍UIKit,那么势必会浪费很多的内存空间,所以苹果的系统库基本都是动态库。这些动态库不会像静态库那样在编译的时候就加载进应用程序的内存,而是在应用程序启动的时候去动态链接。这些动态库就是放在共享缓存中的。
现在我们知道了,共享缓存库里面放的是使用到的系统库,比如UIKit、Foundation等。
我们编译完代码,生成一个machO之后,是通过DYLD将其加载进内存的。machO里面写的有动态库(比如UIKit)的相关代码,但是此时该machO可执行文件里面没有UIKit的代码,所以就需要去找UIKit这个框架的实现。这个框架的实现是在哪里呢?machO是不知道的。谁知道呢?DYLD知道。machO告诉DYLD,我现在需要UIKit这个框架里面的内容,DYLD就会将共享缓存库里面的UIKit的地址告诉这个machO。也就是说,DYLD会将machO与动态库链接起来,生成一个最终的完整的可执行程序。
我们的Mac电脑上也是有共享缓存库的,路径为/private/var/db/dyld,如下:
fishhook原理探究
现在刨除一切其他的想法,我们就来考虑一个问题,为什么OC可以Hook?相信作为高级开发的你一定能够脱口而出,因为OC是一门动态语言,它会在运行的时候根据SEL去查找对应的IMP,因此我们可以调用RuntimeAPI去动态改变SEL与IMP的对应关系。
接下来看一下C函数的调用:
调成汇编展示:
可以看到,NSLog这个函数在底层会通过callq 0x10482e5bc 的方式执行,这里的0x10482e5bc就是一个地址。C是一门静态语言,它的各个函数在编译的时候就被加载进内存中,并且在编译的时候就确切地知道函数的地址了,调用的时候直接通过地址进行函数的调用。
此时,如果我们想要HookC函数的话,就只能改变C函数的地址,而C函数的地址在编译期间就已经确定了,这些地址被存储在machO二进制可执行文件中,而MachO是不会更改的,因此C函数是没有办法被Hook的。
但是!fishhook就是用来Hook C函数的啊!前面的例子已经演示了,fishhook确实Hook到了NSlog这个函数啊!!!这不是前后矛盾了吗?
哼哼?没有矛盾,接下来就来说明一下为什么fishhook可以hookC函数。
我们前面说到,C语言函数在编译的时候就确定了其地址。现在我们回到OC程序中,毫无疑问NSLog是一个C函数,但是在编译这个OC程序的时候,是不能确定其地址的。因为NSLog是在Foundation框架下,而Foundation是一个系统库,其存放于共享缓存库中,而共享缓存库在内存中的地址我在编译OC程序的时候是不知道的。只有在程序运行的时候,通过DYLD动态链接到共享缓存库,此时才可以知道具体函数在动态缓存库中的地址。
而C语言函数在编译的时候是必须要确定一个地址的,而我们的iOS程序在编译的时候又不能知道系统库中C函数的具体地址,这个时候就进行不下去了啊,程序没法编译了啊。
此时就有一门新技术产生了,这个技术叫做PIC(Position Independent Cod ,位置独立代码)。
我们编写的所有的代码,最终都会生成MachO文件,而在MachO文件的数据段(Data)中,会开辟一块内存区域,这块内存区域中会存储一系列的指针,这些指针就是用来专门指向外界的函数,我们将这些指针称之为“符号”。所以在编译的时候,对于外界动态库中的函数,它对应的那个地址实际上就是MachO的Data段中这块特殊区域里面存储的对应的指针,也就是符号。
一开始在编译的时候,MachO的Data段中这块特殊区域里面存储的指针(符号)是没有指向任何地方的,而在程序启动的时候,DYLD链接共享缓存库中的对应的动态库的时候,会对MachO的Data段中这块特殊区域里面存储的指针(符号)的指向进行一一赋值,这也就是所谓的“符号绑定”,这样的话,它们就能知道自己指向共享缓存库中的哪一个地址上面了。
这里讲到了符号和符号绑定,说句题外话,我们在Bugly或者阿里云Crash分析这样的平台上上传的符号表,实际上其记录的就是MachO的Data数据段的特殊区域里面存储的用于记录外界函数的指向的指针与共享缓存库中对应的函数地址的对应关系。
我们说fishhook可以hookC函数,那么它是在上面的那一个步骤进行hook的呢?
在程序中定义一个my_nslog函数和一个sys_nslog函数,这两个函数会被编译进machO里面,所以我们是可以获取到其地址的。在编译的时候让MachO的Data段中的NSLog对应的符号指向my_nslog;在DYLD动态链接的时候,会进行符号绑定,此时将共享缓存库中的地址绑定到sys_nslog函数地址上面。这样的话,我就可以在自定义的my_nslog函数函数中去做对应的操作,并且调用sys_nslog函数会回调真正的NSlog函数了。
所以说,fishhook的本质就是符号的重绑定,也正因为如此,用于交换的那个函数的命名是rebind_symbols:
经过上面的fishhook原理分析,我们再回到文章一开始的第二个例子,为什么我自己定义的testFunc函数不能被Hook到呢?
原因就在于testFunc函数没有使用PIC技术,testFunc函数是写在自己的程序中的,它在一开始编译的时候其地址就确定了,它在MachO的Data端里面没有所谓的符号,所以也就没有接下来的符号重绑定,因此我们自定义的testFunc函数不能被Hook。
其实这里也总结出一个结论,fishhook只能Hook住系统动态库中的C函数,对于直接写在程序中的或者静态库中的函数,fishhook也无能为力。
以上。