之前一直都有听说过fishhook是用来hook系统自带的C函数的,也大概知道原理是重新绑定符号来达到hook的目的,一直没有深入的去读一下fishhook的源码。这几天好好的坐下来读了一下fishhook的源码,在这个过程中也对C/C++指针以及结构体的内存布局都有了进一步认识,其实之前不愿意读fishhook的源码有很大一部分原因是因为结构体和指针以及C/C++基础知识的欠缺让我打了退堂鼓。
在阅读源码的过程中也查阅了很多的资料、博客来加深认识和理解,但是发现大多数人的博客基本上都是千篇一律的拿machO文件来作说明,很少有人从fishhook的代码的角度去细致的推导出hook的具体流程,我写这篇文章的目的就是基于此,给那些想知道为什么代码这么写并且这么写能够达到什么一个什么目的人,也是我在这个过程中最让我头痛的问题,如果有什么不对的地方,欢迎指正。(我使用的代码是戴铭老师GCDFetchFeed项目的代码,和fishhook其实是一样的)
1:首先,介绍几个C/C++相关的知识,在下面会用得上。
结构体的内存布局。结构体的所占字节数=结构体各个元素所占字节数之和(假如结构体元素的类型不一致,那么所占字节是最大元素的倍数,自己可以用一个int和一个NSString测试,最终是占用16个字节而不是12个字节);并且一个结构体指针p加上1表示这个指针指向的地址往后移动这个结构体大小个字节。也就是说,假如我结构体占16个字节,有一个指针p指向一个结构体变量test的地址,那么p+1就等于test的地址+16,如下图-1。每一个NSString占用8个字节,三个NSString类型变量总共占用24个字节;指针+1后面再讲;arm64下指针是占有8个字节,结构体指针是指向结构体的首地址。
2:开始绑定是调用intrebind_symbols这个函数,然后调用prepend_rebindings这个函数,江数据由结构体rebinding准换成结构体rebindings_entry。然后程序继续执行会调用_dyld_register_func_for_add_image这个函数,这个函数的作用是当dyld加载我们的程序的时候,无论是添加还是删除image(模块),都会调用这个方法注册的回调。这个涉及到dyld方面的知识,我说一下我的理解:在我们点击app到main函数之前,是由dyld来负责加载我们的应用程序到内存,也包括很多的image,在装载每一个image进入内存的时候,都会调用这个回调,在调试这个代码的过程中,我发现这个回调会执行很多次,这个也是合理的,因为我们的app会有很多个image要载入内存,我们具体不知道哪一个image里面有我们想要hook的函数,所以每一个image都会去执行同一套绑定的代码,一旦命中了我们需要hook的符号,那么就能够达到我们的目的。_dyld_register_func_for_add_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide))这个方法的参数是一个函数指针,返回的参数一个是machO文件的指针mh,一个偏移量(在dyld加载我们的程序是,会使用ASLR生成一个随机的偏移量,这个是dyld的知识不细说),mh指向的就是当前载入的这个文件的首地址。
3:接着调用了这个函数rebind_symbols_for_image,是我们要分析的第一个很重要的函数。首先我先说一下这个函数的目的,这里就不过多介绍machO文件的知识,这个machO文件加载进内存后,地址都是连续的,目前我们的mh指针指向的是这个machO文件的首地址,如图2。
4:我们要从这个machO文件的找到dysymtab_command和symtab_command这两个load_commond,找这两个是为了求出需要hook的函数的偏移地址(后面再分析怎么求),还要找一个linkedit_segment这个commod,从后面的代码来找这个commond的目的就是求出一个0x0000000100000000这样一个地址,这个是段虚拟内存的起始地址,也就是说虚拟内存的起始地址不是从0开始的,arm64是从0x0000000100000000这个值开始的。我们还需要找到系统函数的符号表所在的load_commond对应的section。至此我们要找到4个值,分别是代码里面的3个加上下面代码的一个sect,图-3。这个sect待会再说,这几个变量分别对应的是machO文件的位置如图-4。
5:我们说过,mh目前指向的是machO文件的首地址,machO文件最开始的元素是mach_header,这个主要记录着当前这个machO文件的一些信息。可以具体看mach_header这个结构体。并且machO文件的内存布局是连续的,那么要找得到load_commod的首地址,我们需要将mh指针向下移动sizeof(mach_header)个字节大小。如图-5.
图-6是根据上面的表达式验证的过程,在arm64下,mach_header_t是mach_header_64的别名,结构体mach_header_64有8个变量,每一个变量都是占用4个字节,那么就是占用32个字节,就是16进制的0x20.
6:接下来的代码如图-7,就是求得我们在步骤4里面说的三个变量,具体的做法就是遍历machO里面所有的load_commond元素,cur就是指向当前这个load_commond的首地址。因为在内存中的布局是连续的,所以下一个commod的首地址=cur+上一个commod的大小,就是for循环里面的cur+=cur_seg_cmd->cmdSize,也可以在machO文件里面验证,看图-8,图-9
图8的Text段的file offset就是起在machO文件的起始偏移地址位0,大小是0xA8000,下面的DATA段的起始地址就是0+0xA8000=0xA8000这个地址,这个只是偏移地址,并不是实际地址,内存中的实际地址是偏移地址+ASLR+0x0000000100000000才是真实地址,但是偏移地址和最终的fileSize都是一样的不会变的。这也能说明machO文件在内存中的布局就是连续的,commond连着commond。
7:要求的的三个变量linkedit_segment,symtab_cmd,dysymtab_cmd分别是三种不同类型的load_commond。具体为什么是这三种可以找找资料,这里只分析这个结果怎么计算来的,不分析为什么取这个类型。别问,问就是machO的结构就是这样,因为我们后面要找Symbol Table的起始地址能根据symtab_cmd的symoff这个字段得到,也能得到String Table的起始地址,看图-10,图11。这个String Table就是我们要找的符号表。现在只有起始地址,没有我们要hook的函数具体在哪里,继续往下看。
我们发现LC_SYMTAB的SymBol Table Offset这个字段刚好存的就是图11的SymTable的起始地址。同样String Table OffSet也是对应的String Table的偏移地址。dysymtab_cmd的indirectsymoff这个字段就是我们要找的Dynamic Symbol Table的起始偏移地址。
8.接着继续看代码,图-12,下面4句代码就是根据上面遍历得到load_commond来获得我们后面需要用到的几个变量。linkedit_segment->vmaddr- linkedit_segment->fileoff第一行这一部分的意思就是要得到0x0000000100000000这个虚拟内存起始的地址,可以对着machO文件计算一下这个值,我觉得这个值不一定要从这个linkedit_segment来取,Text段的的VM Address不就是这个值么。然后加上slide表示的是程序在内存中的偏移地址。
这里小结一下,a)linkedit_base这个值是在内存中偏移的地址,就是ASLR算出来的那一部分+空出来的部分,只需要这个值加上每一个部分的偏移地址就是实际的内存地址;b)symtab就是Symbol Table的偏移地址;c)strtab就是String Table的偏移地址,这个也是我们最终的需要hook的符号位置;d:)indirect_symtab这个是Dynamic_symbol_Table的地址。我们已经找到的元素在machO文件的位置看图-13。我们还缺点什么,怎么具体定位到我们需要hook的函数,我们只是有了需要用到的元素的起始地址。
9:这个函数还有剩下的一部分,如图-14.这一部分也是重新遍历所有的load_commond段,找到__DATA段或者__DATA_CONST段,别问为什么找这两个段,因为这是代码告诉我的,问就是这两个段能够帮助我们找到需要hook的函数的位置。当是满足是SEG_DATA和SEG_DATA_CONST的时候,就遍历这个load_commond,取到type为S_LAZY_SYMBOL_POINTERS和N_NON_LAZY_SYMBOL_POINTERS这两种类型的section,因为系统的C函数会在lz_symbol_ptr里面。对应machO文件的__got和__la_symbol_ptr段,如图-15.
反正我打印这个sectname,发现也是需要从__got这个类型里面找的。这个sect是为了找到section为la_symbol_ptr的偏移地址,图16。至此这一阶段所有需要查找的偏移地址都已经找到了,在图-13和图16里面,使我们后面会用上的地址。看到这个地方的你还好嘛。
10:接着看下一个重量级的函数perform_rebinding_with_section,这个函数需要的参数就是我们上面列出来的4个加上一个ASLR的slide和一个我们传进来的需要替换的函数的指针结构体rebindings,总共六个参数。这里摆出一个很多博客都说的结论,就是Dynamic Symbol Table符号表里面符号出现的下标和Lazy_symbol_ptr里面出现的是一样的。大概的意思就是说假如我需要hook的NSLog,假如NSLog在Symbol里面出现的下标是100,那么在Dynamic Symbol Table出现的下标也是100,我们可以证明一下这个结论.看下图-17,18,19,20,21
可以看到,图17是起始地址,图18是NSLog的地址,两者偏移为图21的(0xA8520-0xa8240)/8=92个字节,因为每一个是8个字节,看图17能够看出来是8个字节的增长。然后看图19的起始地址是0x1258B0+92*4=0x125a20(每一个占用4个字节,图19可以看出来),刚好就是NSLog所在的偏移地址。至此下标的问题得以证明。接着继续看。
11:图-22看代码,我字节看的时候做了很多笔记,方便理解。这个函数的参数就是从前面那一个函数求得到的。红框标出来的那一个值我看了很久不知道是什么意思,但是最终我通过取值和对比地址,发现刚好就是在indirect_symbol_tab偏移了部分TEXT段,得到的第一个DATA段,看图-23,24
可以看到图-23通过实际的地址-slide-虚拟空出来的地址,就是偏移地址,而这个地址就是图-24_CATransform3dIdentity的偏移地址,接着往下面运行你会发现char*symbol_name = strtab + strtab_offset; 这个符号symbol_name刚好就是等于_CATransform3dIdentity,就是说遍历符号没有从头开始,而是从某一个DATA开始。剩下的循环的代码和之前的也是差不多的。具体分析一下。之前就说过获取这section就是为了从addr里面拿到indirect_symbol_table的偏移地址,和slide相加就是内存中的实际地址.下面那一个for循环的意思就是通过section的数量来遍历。第4行的indirect_symbol_indices[i]表示的是指针往后移动,也就是一个一个的indirect_symbol遍历,取到下标,然后从symtab里面取到具体的符号偏移地址,然后加上strtab_offset就是得到了这个符号的地址,下面的代码就是比较两个符号是否相同,相同就就hook处理,不相同就不处理。
13:根据machO文件来说,indirect Symbols的Data字段就是要算出下标,和SymbolTable的下标相对应,然后Symbol Table的DATA字段是对应的StringTable的偏移值,通过遍历section得到下标symtab_index,然后SymbolTab去下标为symtab_index的元素的n_strx字段得到StringTable的符号地址,然后比较符号是否和我们要hook的相同。其实最终的目的就是要取符号表和我们要hook的函数做比较。
if(strcmp(&symbol_name[1], cur->rebindings[j].name) ==0) 在比较两个符号是否相等使用了这个表达式symbol_name[1]这个函数不是很懂,取第一个字符,也就是元素下标为1的首地址?然后取值?没太明白
至此,fishhook的代码就分析完毕。还有几个疑问,图-25里面那个section->reserved1这个值是否是我猜的那样?如果是我猜的那样,那么图-20那张图的NSLog对象的_TEXT段又怎么解释,或者说为什么两个NSLog取了第一个?如有错误欢迎指正,如有疑问欢迎和我讨论。