添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

这是 探求 fishhook 原理 系列的第一篇。主要讲述了 Facebook 开源库 fishhook 的源码实现细节。需要具备 Mach-O 相关知识。可以先阅读 Mach-O 文件格式探索 一文。

关于符号表的基本知识

Lazy Binding 过程

在之前的 Mach-O 文件格式探索 一文中提及到了 __DATA.__la_symbol_ptr __DATA.__nl_symbol_ptr 这两个指针表,分别为 lazy binding指针表 non lazy binding指针表 。并且这两个指针表,保存着与字符串标对应的函数指针。

而 Mach-O 文件文件中通过 dyld 加载的 Lazy Binding 表并没有在加载过程中直接确定地址列表,而是在第一次调用该函数的时候,通过 PLT(Procedure Linkage Table) 来进行一次 Lazy Binding。来使用 printf 方法验证一下:

#include <stdio.h> int main ( int argc , const char * argv []) { printf ( "%s \n " , "hello world" ); printf ( "%s \n " , "hello desgard" ); return 0 ;

拖到 Hopper 中查看汇编:

_main: push rbp mov rbp , rsp sub rsp , 0x10 lea rdi , qword [ 0x100000f8e ] ; "hello world \\ n" , argument "format" for method imp___stubs__printf mov dword [ rbp + var_4 ], 0x0 mov al , 0x0 call imp___stubs__printf lea rdi , qword [ 0x100000f9b ] ; "hello desgard \\ n" , argument "format" for method imp___stubs__printf mov dword [ rbp + var_8 ], eax mov al , 0x0 call imp___stubs__printf xor ecx , ecx mov dword [ rbp + var_C ], eax mov eax , ecx add rsp , 0x10 pop rbp

发现在使用 printf 的时候会触发 call imp__stubs__printf 这条指令。点击进入 imp__stubs__printf 的存储位置查看:

imp___stubs__printf: jmp qword [ _printf_ptr ] ; _printf , CODE XREF = _main + 24 , _main + 41 ; ================================ ; 继续跟踪 .... 0x100001010 dq _printf ; DATA XREF = imp___stubs__printf

在两个 printf 方法前加上断点,使用 lldb 对其进行调试:

为什么这里要观测 0x10001010 这个地址?因为 imp___stubs__printf 这个指针指向了它。这说明在 __la_symbol_ptr 表中对其进行了记录。使用 MachOView 对其进行验证:

发现在 __DATA.__la_symbol_ptr 中的记录值与 lldb x 0x10001010 命令输出的记录值均为 01 00 00 0f 84 。这个值对应的地址就是 imp___stubs__printf 这个桩位置。

0x100000f84 push 0x0 0x100000f89 jmp 0x100000f74 ; Section __stub_helper 0x100000f74 lea r11 , qword [ dyld_stub_binder_100001000 + 8 ] ; CODE XREF = 0x100000f89 0x100000f7b push r11 0x100000f7d jmp qword [ dyld_stub_binder_100001000 ] ; dyld_stub_binder 0x100000f83 db 0x90 0x100000f84 push 0x0 0x100000f89 jmp 0x100000f74

这个位置是不是似曾相识?是的,在 Mach-O 文件格式探索 一文中的验证试验,已经试验过了 Stub 机制。说道这里,我们在 MachOView 中的 __TEXT.__stubs 这个 Section 对其进行验证:

与预想中的结果是相同的。这个在 __TEXT.__stub_helper 中解析出的汇编代码和 Hopper 中完全一致。在这里通过 _stub_helper 来调用 dyld_stu_binder 方法计算 printf 函数的真实地址。其具体信息可以看出, jmpq 0x100000f74 就是在 pushq 参数 $0x0 (link 过程中的标记值)后跳转到这个 section 的头部,并调用 binder 方法。

binder 方法的作用简单来讲就是计算对应的函数地址进行绑定,之后进而调用对应函数。

在第二次输出 0x10001010 的值的时候,发现与第一次的值不相同了。变成了 7f ff 76 cc 98 c4 。这个地址其实就是 printf 的真实地址。通过 x 命令及方法名的方式进行验证:

也就是说 __DATA.__la_symbol_ptr 中指向 printf 地址的值已经发生了改变,并真正的指向了 printf 指令。

Dynamic Symbol Table

Dynamic Symbol Table 动态符号表是用来加载动态库的时候导出的符号表,该表在 dyld 时期使用,并且在对象被加载的时候映射到进程的地址空间。所以我们可以说 DST 是符号表的子集。对于 ELF 来讲,DST 是有其定义的;但是对于 Mach-O 来说,我们可以理解为 DST 就是 Symbol Stubs 。DST 和 Mach-O 中的 Indirect Addressing 有很大的关联,这里给出 Apple 对于 Indirect Addressing 官方文档

这个文档的主要内容是介绍了 Non-lazy symbol references Lazy symbol reference 的特点和区别。其中 Lazy symbol reference 是指在方法在第一次调用的时候,会根据 dyld 过程时加载的映射地址进行处理,完成绑定操作。

【通过 MachOView 我们可以查看 Dynamic Symbol Table 中的所有 Indirect Symbol

理解 fishhook

根据上面的实战内容,我们理解了 Lazy Binding 这个过程。受此启发,我们是不是可以重新绑定 Mach-O 的 Symbol 从而 hook 一些 C 中的库函数呢?这个思路在 fishhook 中已经实现。

为了简化分析过程,我们引入了一个例子,之后的分析都会围绕着这个例子来进行。实例代码很简单,就是修改库函数 strlen ,让其始终返回 666

#include <stdio.h> #include "fishhook.h" static int ( * original_strlen )( const char * _s ); int new_strlen ( const char * _s ) { return 666 ; int main ( int argc , const char * argv []) { struct rebinding strlen_rebinding = { "strlen" , new_strlen , ( void * ) & original_strlen }; rebind_symbols (( struct rebinding [ 1 ]){ strlen_rebinding }, 1 ); char * str = "hellolazy" ; printf ( "%d \n " , strlen ( str )); return 0 ;

main 方法中,我们构造了一个 rebinding 结构体实例,使用 原方法名 新方法的指针 以及 一个二阶指针 (后面我们可以了解到这里存储了 __bss 段中原方法的地址)。

struct rebinding { const char * name ; // 原方法名 void * replacement ; // 新方法的代码段首地址 void ** replaced ; // 旧方法的指针

在跟踪代码之前,我先使用 nm 命令对生成的执行文件查看 File 中的符号信息( -n 参数是根据已知地址进行排序):

$ nm - n TestObjcProj U ___memcpy_chk U __dyld_get_image_header U __dyld_get_image_vmaddr_slide U __dyld_image_count U __dyld_register_func_for_add_image U _dladdr U _free U _malloc U _printf U _strcmp U _strlen U _strnlen U dyld_stub_binder 0000000100000000 T __mh_execute_header 00000001000006 90 t _rebind_symbols_image 00000001000006 f0 t _prepend_rebindings 00000001000007 d0 t _rebind_symbols_for_image 0000000100000 b00 t _rebind_symbols 0000000100000 bc0 t __rebind_symbols_for_image 0000000100000 bf0 t _perform_rebinding_with_section 0000000100000 e10 T _new_strlen 0000000100000 e20 T _main 00000001000010 90 b __rebindings_head 00000001000010 98 b _original_strlen

这里每一行代表一个符号,三列值分别代表 地址 符号说明 符号名 。其中符号说明为以下规则:

  • 小写代表作用域为 Local ,大写代表符号是 Global(external) 的。
  • A - 符号值是绝对的,在链接过程中不允许对其改变,这个符号常常出现在 中断向量表 中。
  • B - 符号值出现在内存 BSS 段。例如在某一个文件中定义全局的 static 方法 static void test ,则符号 test 的类型为 b ,切存储于 BSS 中。其值为该符号在 BSS 中的偏移。一般来说, BSS 分配在 RAM 中。
  • C - 称为 Common Symbol 一般符号 ,是为初始化的数据段。该符号不包含于普通的 Section 中。只有在链接过程才会进行分配。符号的值为所需要的字节数。
  • D - 称之为 Data Symbol 。位于初始化数据段中。一般分配到 Data Section 中。例如全局的 int table[5] = {233, 123, 321, 132, 231}; ,会分配到初始化数据段中。
  • T - 该符号位于代码区 TS 中。
  • U - 说明 当前文件中该符号是未定义的,该符号的定义在别的文件中
  • 我们找到这几个与 strlen 先关的符号,先记录下来:

    U _strlen # 系统库方法,dyld 动态加载,所以未定义 0000000100000 e10 T _new_strlen # 自定义方法,位于 Text Section 00000001000010 98 b _original_strlen #

    另外我们还需要了解的一些基地址信息。例如: Lazy Symbol Pointer Table ( __DATA.__la_symbol_ptr )、 Indirect Symbol Table Symbol Table 。这些均能从 MachOView 中获得到。

    获取到 __DATA.__la_symbol_ptr 基地址为 0x100001010

    这里获取到在 Indirect Symbols 中, _strlen 符号的地址为 0x1000025f0

    这里可以获取到在 Symbol Table 中对应的符号位置 0x100002560 ,而且其中还能检索到在 String Table 中对应的符号 _strlen 的符号名称。

    在进行 hook 的代码逻辑中,在声明一个 strlen_rebinding 实例之后,需要手动调用方法 rebind_symbols ,其中需要传入一个 rebinding 数组的头地址,以及其 size 。下面来看一下 rebind_symbols 方法的实现:

    #define __SIZE_TYPE__ long unsigned int typedef __SIZE_TYPE__ size_t ; * rebind_symbols * struct rebinding rebindings[] - rebinding 结构体数组 * size_t rebindings_nel - 数组长度 int rebind_symbols ( struct rebinding rebindings [], size_t rebindings_nel ) { // 维护一个 rebindings_entry 的结构 // rebinding 的多个实例组织成一个链表 int retval = prepend_rebindings ( & _rebindings_head , rebindings , rebindings_nel ); // 判断是否 malloc 失败,失败会返回 - 1 if ( retval < 0 ) { return retval ; // _rebindings_head -> next 是第一次调用的标志符, NULL 则代表第一次调用 if ( ! _rebindings_head -> next ) { // 第一次调用,将 _rebind_symbols_for_image 注册为回调 _dyld_register_func_for_add_image ( _rebind_symbols_for_image ); } else { // 先获取 dyld 镜像数量 uint32_t c = _dyld_image_count (); for ( uint32_t i = 0 ; i < c ; i ++ ) { // 根据下标依次进行重绑定过程 _rebind_symbols_for_image ( _dyld_get_image_header ( i ), _dyld_get_image_vmaddr_slide ( i )); // 返回状态值 return retval ;

    为了 将多次绑定时的多个符号 组织成一个链式结构, fishhook 自定义了一个链表结构来组织这个逻辑,其中每个节点的数据结构如下:

    struct rebindings_entry { struct rebinding * rebindings ; // rebinding 数组实例 size_t rebindings_nel ; // 元素数量 struct rebindings_entry * next ; // 链表索引 // 全局量,直接拿出表头 static struct rebindings_entry * _rebindings_head ;

    prepend_rebindings 方法中, fishhook 会维护这个结构:

    * prepend_rebindings 用于 rebindings_entry 结构的维护 * struct rebindings_entry **rebindings_head - 对应的是 static 的 _rebindings_head * struct rebinding rebindings[] - 传入的方法符号数组 * size_t nel - 数组对应的元素数量 static int prepend_rebindings ( struct rebindings_entry ** rebindings_head , struct rebinding rebindings [], size_t nel ) { // 声明 rebindings_entry 一个指针,并为其分配空间 struct rebindings_entry * new_entry = ( struct rebindings_entry * ) malloc ( sizeof ( struct rebindings_entry )); // 分配空间失败的容错处理 if ( ! new_entry ) { return - 1 ; // 为链表中元素的 rebindings 实例分配指定空间 new_entry -> rebindings = ( struct rebinding * ) malloc ( sizeof ( struct rebinding ) * nel ); // 分配空间失败的容错处理 if ( ! new_entry -> rebindings ) { free ( new_entry ); return - 1 ; // rebindings 数组中 copy new_entry -> rebingdings 成员中 memcpy ( new_entry -> rebindings , rebindings , sizeof ( struct rebinding ) * nel ); // new_entry -> rebindings_nel 赋值 new_entry -> rebindings_nel = nel ; // new_entry -> newx 赋值,维护链表结构 new_entry -> next = * rebindings_head ; // 移动 head 指针,指向表头 * rebindings_head = new_entry ; return 0 ;

    之后这一部分的结构将会持续被维护,用图示来说明一下:

    在执行完链表的初始化及结构维护的 prepend_rebindings 方法,继续执行。由于我们的 strlen 是 dyld 加载的系统库方法,所以 _rebindings_head -> next 在第一次调用的时候为空,因为没有做过替换符号,所以会调用 _dyld_register_func_for_add_image 来注册 _rebind_symbols_for_image 方法,之后程序每次加载动态库的时候,都会去调用该方法。如果不是第一次替换符号,则 遍历已经加载的动态库

    这一块的流程也是 fishhook 代码的精髓部分,逐一来分析这些地方。

    _dyld_register_func_for_add_image 是什么?

    这里笔者查看了 Apple 官方的 dyld.h 头文件,其中对于 _dyld_register_func_for_add_image 有较为详细的说明( dyld.h ):

    The following functions allow you to install callbacks which will be called by dyld whenever an image is loaded or unloaded. During a call to _dyld_register_func_for_add_image() the callback func is called for every existing image. Later, it is called as each new image is loaded and bound (but initializers not yet run). The callback registered with _dyld_register_func_for_remove_image() is called after any terminators in an image are run and before the image is un-memory-mapped.

    _dyld_register_func_for_add_image 这个方法当镜像 Image load 或是 unload 的时候都会由 dyld 主动调用。当该方法被触发时,会为每个镜像触发其回调方法。之后则将其镜像与其回电函数进行绑定(但是未进行初始化)。使用 _dyld_register_func_for_add_image 注册的回调将在镜像中的 terminators 启动后被调用。

    为什么传入的 _rebind_symbols_for_image 作为回调函数呢?

    extern void _dyld_register_func_for_add_image ( void ( * func )( const struct mach_header * mh , intptr_t vmaddr_slide )) __OSX_AVAILABLE_STARTING ( __MAC_10_1 , __IPHONE_2_0 ); extern void _dyld_register_func_for_remove_image ( void ( * func )( const struct mach_header * mh , intptr_t vmaddr_slide )) __OSX_AVAILABLE_STARTING ( __MAC_10_1 , __IPHONE_2_0 );

    extern 关键字来告知编译器 当调用方法的时候,请在其他模块中寻找该方法定义 。并且在这两个方法的声明中,所注册方法的参数列表一定是 (const struct mach_header* mh, intptr_t vmaddr_slide)

    * _rebind_symbols_for_image 是 rebind_symbols_for_image 的一个入口方法 * 这个入口方法存在的意义是满足 _dyld_register_func_for_add_image 传入回调方法的格式 * header - Mach-O 头 * slide - intptr_t 持有指针 static void _rebind_symbols_for_image ( const struct mach_header * header , intptr_t slide ) { // 外层是一个入口函数,意在调用有效的方法 rebind_symbols_for_image rebind_symbols_for_image ( _rebindings_head , header , slide );

    看到 _rebind_symbols_for_image 是个入口,我们的问题也就迎刃而解了。添加这个入口方法其实是为了适应回调函数的参数格式,但是真正调用的 rebind_symbols_for_image 所需要的参数不满足其方法的描述。

    intptr_t 是什么类型?

    至于 intptr_t ,笔者在之前阅读 Understanding and using C pointers 时见过。下面为引用:

    // / /usr/in clude / stdint . h /* Types for `void *' pointers. */ #if __WORDSIZE == 64 # ifndef __intptr_t_defined typedef long int intptr_t ; # define __intptr_t_defined # endif typedef unsigned long int uintptr_t ; #else # ifndef __intptr_t_defined typedef int intptr_t ; # define __intptr_t_defined # endif typedef unsigned int uintptr_t ; #endif

    intptr_t uintptr_t 这两种类型用于存储指针地址。从字面上看, intptr_t 像是一个整型指针类型,而且在 stdint.h 的注释中也可以看到 Types for void *pointers 的描述,但我们看到实际上 intptr_t 就是一个整数,在 64 位平台中定义为 long int ,否则定义为 int 。我们知道 C 语言的指针实际上就是变量的地址,在 64 位平台上指针为 8 字节,在 32 位平台上指针为 4 字节,而 intptr_t 刚好是和这个字节数对应,使用它可以安全地进行整数与指针的转换运算,也就是说当需要将指针作为整数运算时,将它转换成 intptr_t 进行运算才是安全的。 使用 int 时也可以使用 intptr_t 来保证平台的通用性,它在不同的平台上编译时长度不同,但都是标准的平台字长

    简而言之,这是一个标准字长的存储量,用来代表方法地址的偏移量。

    重绑定寻址 - 基址准备过程

    rebind_symbols_for_image 方法描述的也就是整个 fishhook 精华所在 - 重绑定符号过程 。继续跟踪代码来理解每一行的意图:

    static void rebind_symbols_for_image ( struct rebindings_entry * rebindings , const struct mach_header * header , intptr_t slide ) { Dl_info info ; if ( dladdr ( header , & info ) == 0 ) { return ; // 声明几个查找量 : // linkedit_segment , symtab_command , dysymtab_command segment_command_t * cur_seg_cmd ; segment_command_t * linkedit_segment = NULL ; struct symtab_command * symtab_cmd = NULL ; struct dysymtab_command * dysymtab_cmd = NULL ; // 初始化游标 // header = 0x100000000 - 二进制文件基址默认偏移 // sizeof ( mach_header_t ) = 0x20 - Mach - O Header 部分 // 首先需要跳过 Mach - O Header uintptr_t cur = ( uintptr_t ) header + sizeof ( mach_header_t ); // 遍历每一个 Load Command ,游标每一次偏移每个命令的 Command Size 大小 // header -> ncmds: Load Command 加载命令数量 // cur_seg_cmd -> cmdsize: Load 大小 for ( uint i = 0 ; i < header -> ncmds ; i ++ , cur += cur_seg_cmd -> cmdsize ) { // 取出当前的 Load Command cur_seg_cmd = ( segment_command_t * ) cur ; // Load Command 的类型是 LC_SEGMENT if ( cur_seg_cmd -> cmd == LC_SEGMENT_ARCH_DEPENDENT ) { // 比对一下 Load Command name 是否为 __LINKEDIT if ( strcmp ( cur_seg_cmd -> segname , SEG_LINKEDIT ) == 0 ) { // 检索到 __LINKEDIT linkedit_segment = cur_seg_cmd ; // 判断当前 Load Command 是否是 LC_SYMTAB 类型 // LC_SEGMENT - 代表当前区域链接器信息 else if ( cur_seg_cmd -> cmd == LC_SYMTAB ) { // 检索到 LC_SYMTAB symtab_cmd = ( struct symtab_command * ) cur_seg_cmd ; // 判断当前 Load Command 是否是 LC_DYSYMTAB 类型 // LC_DYSYMTAB - 代表动态链接器信息区域 else if ( cur_seg_cmd -> cmd == LC_DYSYMTAB ) { // 检索到 LC_DYSYMTAB dysymtab_cmd = ( struct dysymtab_command * ) cur_seg_cmd ; // 容错处理 if ( ! symtab_cmd || ! dysymtab_cmd || ! linkedit_segment || ! dysymtab_cmd -> nindirectsyms ) { return ; // slide: ASLR 偏移量 // vmaddr: SEG_LINKEDIT 的虚拟地址 // fileoff: SEG_LINKEDIT 地址偏移 // 式①: base = SEG_LINKEDIT 真实地址 - SEG_LINKEDIT 地址偏移 // 式②: SEG_LINKEDIT 真实地址 = SEG_LINKEDIT 虚拟地址 + ASLR 偏移量 // 将②代入①: Base = SEG_LINKEDIT 虚拟地址 + ASLR 偏移量 - SEG_LINKEDIT 地址偏移 uintptr_t linkedit_base = ( uintptr_t ) slide + linkedit_segment -> vmaddr - linkedit_segment -> fileoff ; // 通过 base + symtab 的偏移量 计算 symtab 表的首地址,并获取 nlist_t 结构体实例 nlist_t * symtab = ( nlist_t * )( linkedit_base + symtab_cmd -> symoff ); // 通过 base + stroff 字符表偏移量计算字符表中的首地址,获取字符串表 char * strtab = ( char * )( linkedit_base + symtab_cmd -> stroff ); // 通过 base + indirectsymoff 偏移量来计算动态符号表的首地址 uint32_t * indirect_symtab = ( uint32_t * )( linkedit_base + dysymtab_cmd -> indirectsymoff ); // 归零游标,复用 cur = ( uintptr_t ) header + sizeof ( mach_header_t ); // 再次遍历 Load Commands for ( uint i = 0 ; i < header -> ncmds ; i ++ , cur += cur_seg_cmd -> cmdsize ) { cur_seg_cmd = ( segment_command_t * ) cur ; // Load Command 的类型是 LC_SEGMENT if ( cur_seg_cmd -> cmd == LC_SEGMENT_ARCH_DEPENDENT ) { // 查询 Segment Name 过滤出 __DATA 或者 __DATA_CONST if ( strcmp ( cur_seg_cmd -> segname , SEG_DATA ) != 0 && strcmp ( cur_seg_cmd -> segname , SEG_DATA_CONST ) != 0 ) { continue ; // 遍历 Segment 中的 Section for ( uint j = 0 ; j < cur_seg_cmd -> nsects ; j ++ ) { // 取出 Section section_t * sect = ( section_t * )( cur + sizeof ( segment_command_t )) + j ; // flags & SECTION_TYPE 通过 SECTION_TYPE 掩码获取 flags 记录类型的 8 bit // 如果 section 的类型为 S_LAZY_SYMBOL_POINTERS // 这个类型代表 lazy symbol 指针 Section if (( sect -> flags & SECTION_TYPE ) == S_LAZY_SYMBOL_POINTERS ) { // 进行 rebinding 重写操作 perform_rebinding_with_section ( rebindings , sect , slide , symtab , strtab , indirect_symtab ); // 这个类型代表 non - lazy symbol 指针 Section if (( sect -> flags & SECTION_TYPE ) == S_NON_LAZY_SYMBOL_POINTERS ) { perform_rebinding_with_section ( rebindings , sect , slide , symtab , strtab , indirect_symtab );

    rebind_symbols_for_image 方法展示了冲绑定过程中,所有计算地址的流程。浏览过代码之后思考一个问题: 为了完成重绑定的操作,我们需要获取哪些地址信息呢?

    1. 获取 Linkedit Base Addr

    fishhook 绝大多数重要的地址计算都要使用到或是间接使用到 Linkedit Base Addr 。要如何理解这个基地址呢?这里我们可以理解成: Linkedit Segment 在文件中的首地址

    为什么 Linkedit Segment 首地址信息十分重要,因为在 Load Command 中, LC_SYMTAB LC_DYSYMTAB 的中所记录的 Offset 都是基于 __LINKEDIT 段的。而 LC_SYMTAB 中通过偏移量可以拿到 symtab 符号表首地址 strtab 符号名称字符表首地址 以及 indirect_symtab 跳转表首地址

    我们拿到 Indirect Symbols 的首地址 indirect_symtab 再加上 LC_SEGMENT.__DATA 中任何一个 Section 信息的 reverved1 字段就可以获取到对应的 Indirect Address 信息。在这之后我们可以遍历每一个 Indirect Symbols ,并以索引方式获取到每一个 nlist 结构的符号,从符号中获取到符号名字符串在字符表中的偏移量,进而继续获取符号名。

    2. 二次遍历 Load Command 的目的

    第一次遍历的目的在第一个疑问中已经解释的很清楚了,遍历目的有三个,为了找出 LC_SEGMENT(__LINKEDIT) LC_SYMTAB LC_DYSYMTAB 三个 Load Command ,从而我们可以计算出 Base Address Symbol Table Dynamic Symbol String Table 的位置。

    而在第二次遍历中,我们需要获取的是 LC_SEGMENT(__DATA) __nl_symbol_ptr __la_symbol_ptr 这两个 Section 。其目的是为了确定 lazy binding指针表 non lazy binding指针表 Dynamic Symbol 中对应的位置,方法就是获取到 reserved1 字段。这个在后面我们会做一个验证实现。

    在一切基址准备好之后,开始进行重绑定 perform_rebinding_with_section 方法:

    static void perform_rebinding_with_section ( struct rebindings_entry * rebindings , section_t * section , intptr_t slide , nlist_t * symtab , char * strtab , uint32_t * indirect_symtab ) { // Indirect Symbol 表中检索到对应位置 uint32_t * indirect_symbol_indices = indirect_symtab + section -> reserved1 ; // 获取 _DATA . __nl_symbol_ptr ( __la_symbol_ptr ) Section // 已知其 value 是一个指针类型,整段区域用二阶指针来获取 void ** indirect_symbol_bindings = ( void ** )(( uintptr_t ) slide + section -> addr ); // size / 一阶指针来计算个数,遍历整个 Section for ( uint i = 0 ; i < section -> size / sizeof ( void * ); i ++ ) { // 通过下标来获取每一个 Indirect Address Value // 这个 Value 也是外层寻址时需要的下标 uint32_t symtab_index = indirect_symbol_indices [ i ]; if ( symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL || symtab_index == ( INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS )) { continue ; // 获取符号名在字符表中的偏移地址 uint32_t strtab_offset = symtab [ symtab_index ]. n_un . n_strx ; // 获取符号名 char * symbol_name = strtab + strtab_offset ; // 过滤掉符号名小于 4 位的符号 if ( strnlen ( symbol_name , 2 ) < 2 ) { continue ; // 取出 rebindings 结构体实例数组,开始遍历链表 struct rebindings_entry * cur = rebindings ; while ( cur ) { // 对于链表中每一个 rebindings 数组的每一个 rebinding 实例 // 依次在 String Table 匹配符号名 for ( uint j = 0 ; j < cur -> rebindings_nel ; j ++ ) { // 符号名与方法名匹配 if ( strcmp ( & symbol_name [ 1 ], cur -> rebindings [ j ]. name ) == 0 ) { // 如果是第一次对跳转地址进行重写 if ( cur -> rebindings [ j ]. replaced != NULL && indirect_symbol_bindings [ i ] != cur -> rebindings [ j ]. replacement ) { // 记录原始跳转地址 * ( cur -> rebindings [ j ]. replaced ) = indirect_symbol_bindings [ i ]; // 重写跳转地址 indirect_symbol_bindings [ i ] = cur -> rebindings [ j ]. replacement ; // 完成后不再对当前 Indirect Symbol 处理 // 继续迭代到下一个 Indirect Symbol goto symbol_loop ; // 链表遍历 cur = cur -> next ; symbol_loop :;

    这段方法主要描述了替换 __DATA.__la_symbol_ptr __DATA.__la_symbol_ptr Indirect Pointer 主要过程。从 reserved1 字段获取到 Indirect Symbols 对应的位置。从中我们可以获取到指定符号的偏移量,这个偏移量主要用来在 String Table 中检索出符号名称字符串。之后我们找到 __DATA.__la_symbol_ptr __DATA.__la_symbol_ptr 这两个 Section。这两个表中,都是由 Indirect Pointer 构成的指针数组,但是 其中的元素决定了我们调用的方法应该以哪个代码段的方法来执行 。我们遍历这个指针数组中每一个指针,在每一层遍历中取出其符号名称,与我们的 rebindings 链表中每一个元素进行比对,当名称匹配的时候,重写其指向地址。

    以上就是 fishhook 的全部源码实现。本文完成了对于 fishhook 全部运行时流程进行了详尽的分析。在 验证试验 - 探求 fishhook 原理(二) 中,将会对 fishhook 的全部流程以计算的方式进行模拟实践。并对 fishhook 代码中的一些处理和技巧做出一一剖析。尽请期待。

  • Linux Tools Quick Tutorial
  • Hook 原理之 fishhook 源码解析
  • 感谢 AloneMonkey jmpews 两位大牛对我的一些帮助。