一、前言
在正题开始之前,我们先来聊聊iOS中的hook技术。一谈到hook,很多人首先想到的是runtime,runtime确实强大,但是它存在很多局限性:
1)、侵入性:一旦hook了某个类的方法,那么只能这个类的所有对象的方法都会被hook。
2)、语言上的局限性:runtime 的hook 只能作用于OC方法。
开源框架Aspects很巧妙的解决了第一个问题,Aspects通过动态创建子类的方式将对当前类的hook转换为对当前类动态生成的子类的hook,以此避免对当前类其他对象的代码侵入,这与KVO的实现思路是一致的。而fishhook能从一定程度上辅助runtime解决hook对语言局限性的问题。
二、浅谈fishhook
fishhook是Facebook开源的一个C语言的hook工具,我们可以使用fishhook来hook动态链接的C函数。为什么在这里要强调动态链接呢?因为fishhook只能hook iOS系统的C函数,你自己编写的C函数是无法hook的。
fishhook使用起来很简单,在这里就不谈了,先来简单介绍下fishhook的实现原理。由于动态库并不参与前期的静态编译链接,所以在程序的可执行文件中,代码段并不包含动态库相关函数的汇编后的指令。那么系统是如何根据函数的调用符号找到真实的函数地址呢?在Mach-O文件中存在符号表和动态符号表以及字符串表,字符串表中存储了所有的字符信息,比如代码int a = 100;这个变量a的名字即存在字符串表中。符号表则存储了所有符号位于字符串表中的位置信息,动态符号表存储了动态库符号位于符号表中的偏移信息。动态库的section中的reserved1存储了该section的偏移量X,动态符号表偏移X后即是该section的符号表索引数组Indices的首地址,以Indices数组中的值为索引,可以在符号表中获取到当前的符号在字符串表中的偏移,从而获取到符号字符串。通过该section的addr字段可以获取到该section的符号绑定表,表中记录着动态符号如:printf所对应的函数地址,修改符号绑定表的内容为指定函数地址即实现了hook。fishhook看起来非常的绕,这是由于动态链接存在复杂的索引关系,在这里就不过多介绍了,有兴趣的可以搜索下有关fishhook的博文,优秀博文非常多。
fishhook很厉害,但是在刚接触时我有两个疑问:1、fishhook能hook C++函数吗?在我的前篇文章中也提出了hook C++函数的问题,但是在留言中貌似没有得到有效的答案。2、fishhook 为什么不能hook自己写的C函数呢?下面我们来一一解答这两个问题。
三、C++的符号修饰
在程序员还是使用纸带写代码的时候,人们约定在指定的某几位代表指令,不同的0、1组合代表不同的指令。如:中,0100代表跳转指令,后面的0000代表目标地址。由于汇编的出现,0100被用jump来代替,这就是最早的符号,符号表能映根据符号映射到一个指令。C语言也是与此类似,实际上我们也是通过一个符号来代表一个函数的地址,但是随着程序的不断变大,符号冲突的概率逐渐增加。一个程序员在一个.c文件实现了hello函数,可能另一个程序员在另一个.c文件中也实现了一个同名的hello函数,在这两个文件进行编译和汇编后,会在各自的目标文件中形成同名的强符号,导致最终链接时报错。这是由于在C语言中,函数和初始化后的全局变量默认都是强符号,如果你想改为弱符号,那么可以使用__attribute__((weak))修饰。在这里提一下最近58客户端发现的一个有意思的事情。在iOS 8.11.1版本以后,我们发现buggly上崩溃日志都会携带一个来自RN的函数调用栈RCTFBQuickPerformanceLoggerConfigureHooks,在RN中它的声明如下,
但是在源码中,这个函数没有任何实现,完全是一个空函数。看名字这个函数是hook使用的,那么它是怎么实现hook的呢?将RCT__EXTERN 展开后为__attribute__((visibility("default"))),其作用为将RCTFBQuickPerformanceLoggerConfigureHooks向外界暴露,如果外界存在同名函数,那么RCTFBQuickPerformanceLoggerConfigureHooks会报符号冲突的错误。那么如何做到即能暴露符号,又不造成符号冲突呢?这就利用了__attribute__((weak)),将RCTFBQuickPerformanceLoggerConfigureHooks生命为弱符号,当外界有同名函数时,SDK内部调用外届的函数,否则调用内部空函数。
为了防止出现函数名冲突,在UNIX的C环境下,所有的函数会被加上”_”前缀,也就是说void hello ( ),符号实际上为”_hello”,这种机制能够避免与系统函数的冲突。C++为了解决符号冲突的问题,表现的更为彻底。与C相比,C++有命名空间的限制,可以极大地避免函数的冲突,除了命名空间外,C++还存在构造和析构函数,函数重载等特征。这就导致C++的函数符号要比C函数更复杂。同样的一个函数,在C和C++中,函数符号是完全不一样的。假设有函数
在C环境下,它的符号为”_cleanup”,而在C++环境下它的符号为“__Z7cleanupPv”,这就表明,同样一个函数在C和C++中,修饰机制是不一样的。为了避免由于符号不同导致的问题,很多开源代码会加上extern "C” {}来限定函数在C环境。但是在C环境中并不识别extern "C”标识,因此你会看到很多的开源代码中存在以下代码
其意图在于如果在C++环境中则限定为C环境。那么究竟C++的修饰机制是怎样的呢?我们看到一个C++函数,如何推断出它的符号呢?很遗憾,我没有找到明确的关于C++函数符号修饰的介绍,不同的编译器不同的平台签名有所不同。不过没有关系,办法还是有的,假设我想知道JavaScriptCore中某个C++函数的符号,那么我们可以创建一个cpp文件,将C++函数名复制过去,
然后通过gcc -c将文件编译成目标文件WBIMC++.o,然后调用命令nm WBIMC++.o即可查看相应的符号。
能获取到C++的符号,是不是也就意味着hook C++函数是可行的。我们在fishhook的符号中随便传入一个JavaScriptCore的C++函数符号”_ZNK3JSC11SlotVisitor18containsOpaqueRootEPv“,通过代码断点调试发现,fishhook能够正确获取和替换函数指针
因此hook C++是可行的。
四、Mach-O文件简介
在接触Mach-O之前,我有两个疑问,第一个是之前提出的问题,fishhook为什么不能hook自己写的C函数。第二个问题是跟58正在做的技术项目相关,如何动态调用static 函数。弄清楚这两个问题必须要对Mach-O有较为透彻的了解。
什么是Mach-O,按我的理解就是遵循特定结构的文件。一般比较常见的文件有:应用程序、目标文件、动态库、链接器等,其中应用程序、目标文件.o是尤为重要的。Mach-O可以分为三个部分:
1)、Header
Header是文件的头部信息,包括CPU信息、文件类型、Command条数及Size信息。总体来说,作为开发者Header使用的较少,比较常用的是(uintptr_t)&_mh_execute_header获取header地址进行计算用。
2)、Commands
Commands描述的是文件的加载信息,加载信息有很多,加载的段、符号表、动态库信息等都在Commands中取到。这个部分信息还是比较有用的,我们可以从这里获取到符号表和字符串表的偏移量,下文中会有详细的解释。
首先来说下段(Segment),上图中可以看出共加载了4个段,__PAGEZERO是一个空段,它位于文件起始段的位置。__TEXT和__DATA分别是文本段和数据段,分别存储了代码信息和数据信息。__LINKEDIT是链接信息段,可以通过__LINKEDIT进行地址计算。段又可以细分为section,每个Segment可以包含多个section。
3)、数据区
除了Header和Commands外所有的原始数据。Commands是对数据的汇总提示,而数据区则是真实的数据。Commands与数据区的关系就像size和char*的关系。
接下来先介绍几个比较重要的模块:
1)、(__TEXT,__text)
这里存放的是汇编后的代码,当我们进行编译时,每个.m文件会经过预编译->编译->汇编形成.o文件,称之为目标文件。汇编后,所有的代码会形成汇编指令存储在.o文件的(__TEXT,__text)区((__DATA,__data)也是类似)。链接后,所有的.o文件会合并成一个文件,所有.o文件的(__TEXT,__text)数据都会按链接顺序存放到应用文件的(__TEXT,__text)中。
2)、(__DATA,__data)
存储数据的section,static在进行非零赋值后会存储在这里,如果static 变量没有赋值或者赋值为0,那么它会存储在(__DATA,__bss)中。
3)、Symbol Table
符号表,这个是重点中的重点,符号表是将地址和符号联系起来的桥梁。符号表并不能直接存储符号,而是存储符号位于字符串表的位置。
4)、String Table
字符串表所有的变量名、函数名等,都以字符串的形式存储在字符串表中。
5)、动态符号表
动态符号表存储的是动态库函数位于符号表的偏移信息。(__DATA,__la_symbol_ptr) section 可以从动态符号表中获取到该section位于符号表的索引数组。
动态符号表并不存储符号信息,而是存储其位于符号表的偏移信息。Fishhook源码看起来比较复杂主要是因为hook的是动态链接的函数,索引和链接关系比较绕。但是我们自己编写的C函数不是动态链接的,而是在编译链接后代码指令就存储在文件内部的函数,因此不会用到动态符号表。接下来我们以static 函数为例,看看如何动态的查找自己编写的函数地址。
五、动态调用static函数
在58iOS客户端中大量存在static函数,这些static函数该如何动态调用呢?能否通过脚本来调用static函数呢?在调研Mach-O之前,我们是一愁莫展,尝试使用dlsym函数获取静态函数,但是实践发现dlsym并不能获取到函数地址。在了解Mach-O后,我们发现Mach-O文件中存放了所有编译过的的函数指令,static 函数也一定在文件中。假设在文件中下面函数
在Mach-O文件中,搜索代码段,可以发现静态函数存放在代码段中,其地址为0x1000010C0
那么我们如何通过函数的名称获取到函数地址呢?所谓的函数名实际上就是函数符号,因此函数地址与函数名强关联。
符号表实际上是个结构体数组,结构体nlist_64中包括该符号位于字符串表的偏移,section索引,以及对应的地址信息。在符号表中,实际上不能直接获取到其对应的符号,在图中我们能看到符号为”_s_cleanup”,这实际上是工具帮我们获取好后展示出来的,实际上我们在代码中只能拿到其位于字符串表中的索引,在_s_cleanup的符号表中其索引为0x594,也就是说字符串表+0x594即为_s_cleanup字符串符号。
从上图中可以看出字符串表的起始地址为0x6004,0x6004+0x594 = 0x6598。
获取到字符串符号后,我们可以知道这个符号是不是我们想要的符号,如果是我们想要的符号,那么获取其函数地址。到这里,应该说通过Mach-O文件获取静态链接函数地址已经完美解决了。需要注意的是,这个函数地址并不是真实的地址,需要计算出其相对于真实地址的偏移,再加上真实文件地址即为真实函数地址。
那么如何获取函数符号表、字符串表呢?实际上segment和符号表在Commands中是顺序存放的,_mh_execute_header.ncmds可以根据索引遍历所有的Command。
找到LC_SYMTAB后,LC_SYMTAB会告诉我们符号表位于可执行文件的偏移以及字符串表位于可执行文件的偏移。地址计算后即可得到符号表和字符串表
最终效果如下:
细心的同学可能会发现一个bug,static 函数在不同的文件中是可以同名的,参数只有一个函数符号的话如何确定是哪个文件中函数?实际上在符号表中,是可以存在相同符号的,即如果两个文件中都存在s_cleanup函数,那么符号表中会存在两个_s_cleanup,只不过他们的函数地址不同。那么如何区分同名静态函数呢?实际上在链接时,各个段可以理解为按文件的顺序存放的,也就是说符号表实际上也是存在文件顺序的。
符号表的type可以区分出这个符号是否是文件相关信息,type == 0x64则是文件相关信息,因此在遍历符号表时可以判断出当前正在遍历哪个文件的符号。能判断出正在遍历哪个文件,那么bug就迎刃而解。
另外,如果static 函数只是在代码中实现了,但是并没有任何调用的地方,那么在编译时,编译器会将static函数优化掉,不会生成相关指令。因此符号表中不会存储static函数相关信息,也就无法实现动态调用。如果想要做到static 数据存取,那么方式与此类似,只不过获取到的地址不是函数地址,而是数据存储地址,如果static 是函数内的局部变量,那么其符号需要加上函数符号,比如
那么它的静态变量s_iData符号为"_application:didFinishLaunchingWithOptions:.s_iData”。通过memset即可修改变量的值。
关于Mach-O还有个比较有意思的是,我们可以自定义section,将数据和函数指令放入我们指定的section中,
编译链接后,其文件中多了个(__TEXT,__mysection),并且函数还能正常运行。这为我们进行代码混淆又提供了一个手段。
在了解Mach-O之前,我们无法动态调用内联函数,动态调用任意C函数首先需要尝试是否能够通过dlsym函数获取到指针,如果获取不到函数指针则可能说明是内联函数,因此需要根据if-else来判断是哪个内联函数。但是现在我们可以通过Mach-O发现,所谓的内联函数在iOS代码中都是以static inline 修饰的,那么在编译时内联函数的函数符号会被写入当前目标文件的符号表,函数实现会被当做指令写入代码段,如同普通函数一样。在AppDelegate.m中调用CGSizeMake后,查看AppDelegate的目标文件符号表,可以看出符号表中包含内联函数的符号,如同普通函数一样。
六、总结
在iOS领域hook的方式有很多种,在不是必须的情况下还是少用为妙,hook之后出现问题非常难排查。本文主要介绍了如何根据Mach-O文件,获取静态链接的函数地址,动态链接的函数可以参考fishhook。