fishhook(二):基本使用 && 源码分析

fishhook 基本使用

  • fishhook 简介

    fishhook 是一个非常简单的库,它可以对运行在 iOS 模拟器和设备上的 MachO 二进制文件的部分符号进行动态重绑定。fishhook 提供了类似于在 macOS 上使用 DYLD_INTERPOSE 的功能。fishhook 可以用来 hook 系统的 C 函数以进行调试和跟踪(比如解决文件描述符重复回收等问题)

    GitHub 地址

  • fishhook 基本使用
    fishhook 简单使用

fishhook 源码分析

  • 符号地址重绑定的入口函数:rebind_symbols

    rebind_symbols 函数是 fishhook 中用于符号地址重绑定的入口函数,它需要传入 2 个参数:

    1. 重绑定信息数组的首地址 struct rebinding rebindings[]
    2. 重绑定信息数组的长度 size_t rebindings_nel

    其中,重绑定信息的定义如下:
    00 struct rebinding
    rebind_symbols 函数的实现如下:
    01 rebind_symbols

  • 维护用于存储多次重绑定信息的链表:prepend_rebindings

    为了将多次调用 rebind_symbols 函数所传递的多个重绑定信息数组(struct rebinding rebindings[])组织成一个链式结构,fishhook 自定义了一个结构体链表来支持这个逻辑:
    02 struct rebindings_entry
    prepend_rebindings 函数用于维护这个结构体链表:
    03 prepend_rebindings

  • dyld 加载镜像的回调:_rebind_symbols_for_image

    rebind_symbols 函数在执行完维护结构体链表的函数 prepend_rebindings 之后,继续执行

    如果是第一次调用 rebind_symbols 函数,则 _rebindings_head -> next == NULL,表示还没做过符号地址的替换(即表示 _rebind_symbols_for_image 函数还未注册为 dyld 加载镜像的回调),此时会调用 _dyld_register_func_for_add_image 函数将 _rebind_symbols_for_image 函数注册为 dyld 加载镜像的回调。在调用 _dyld_register_func_for_add_image 向 dyld 注册回调函数期间,会对每个已加载的镜像调用注册的回调函数 _rebind_symbols_for_image 进行符号地址的重绑定。之后,每当 dyld 加载新镜像时,也会调用注册的回调函数 _rebind_symbols_for_image 进行符号地址的重绑定

    如果不是第一次调用 rebind_symbols 函数,则 _rebindings_head -> next != NULL,表示已经做过符号地址的替换(即表示 _rebind_symbols_for_image 函数已经注册为 dyld 加载镜像的回调),此时会遍历所有已加载的镜像并调用 _rebind_symbols_for_image 函数进行符号地址的重绑定

    向 dyld 注册镜像加载的回调的函数的声明为:

    // 在调用 _dyld_register_func_for_add_image() 向 dyld 注册回调函数期间,会对每个已加载的镜像调用注册的回调函数 func
    // 之后,每当加载和绑定新镜像时(此时该镜像还未执行初始化),也会调用注册的回调函数 func
    extern void _dyld_register_func_for_add_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide));
    

    其规定了所注册的回调函数的参数列表一定是 (const struct mach_header* mh, intptr_t vmaddr_slide)

    但是,如果要进行符号地址重绑定
    不仅需要 const struct mach_header* mhintptr_t vmaddr_slide
    还需要 static struct rebindings_entry* _rebindings_head

    因此,_rebind_symbols_for_image 函数存在的作用是:
    04 _rebind_symbols_for_image

  • 准备符号地址重绑定过程所需的基地址:rebind_symbols_for_image
    05 rebind_symbols_for_image
    第一次遍历当前镜像 Load Commands 区域的目的:
    为了找出 LC_SEGMENT_64(__LINKEDIT)LC_SYMTABLC_DYSYMTAB 这 3 个加载指令
    从而计算出当前镜像的基地址、当前镜像中符号表的基地址、当前镜像中字符串表的基地址、当前镜像中间接符号表的基地址

    第二次遍历当前镜像 Load Commands 区域的目的:
    获取当前镜像的 LC_SEGMENT_64(__DATA)LC_SEGMENT_64(__DTAT_CONST) 这两个段加载指令中的 __nl_symbol_ptr__la_symbol_ptr 这两个节
    从而通过 Section->reserved1 字段,获取懒绑定指针表(Lazy Binding Symbol Pointers)和非懒绑定指针表(Non Lazy Binding Symbol Pointers)在间接符号表(Dynamic Symbol Table)中的起始位置

  • 进行真正的符号地址重绑定:perform_rebinding_with_section
    06 perform_rebinding_with_section

  • 其他函数

    get_protection 函数用于查询给定节(Section)的内存保护权限:
    07 get_protection
    rebind_symbols_image 函数用于对给定镜像进行符号地址重绑定:
    08 rebind_symbols_image

验证实验:通过 MachOView 验证 fishhook 准备基地址的过程

继续使用上文中 fishhook 基本使用 这一小节的代码示例:
fishhook 简单使用
真机运行之后,将其生成的 MachO 文件拖入 MachOView 中进行解析

注意:
因为 MachOView 进行的是静态分析,MachO 文件并未真正地在内存中运行,所以 ASLR 的偏移量 slide = 0

  • 计算当前镜像在虚拟内存中的起始地址(linkedit_base)

    从 MachO 文件的 Load Commands 区域的 LC_SEGMENT_64 (__LINKEDIT) 加载指令中可以得到:
    linkedit_segment->vmaddr = 0x00000001 00010000
    linkedit_segment->fileoff = 0x00000000 00010000
    linkedit_base = slide + linkedit_segment->vmaddr - linkedit_segment->fileoff = slide + 0x00000001 00000000
    LC_SEGMENT_64 (__LINKEDIT)

  • 计算当前镜像的符号表在虚拟内存中的起始地址(symtab)

    从 MachO 文件的 Load Commands 区域的 LC_SYMTAB 加载指令中可以得到:
    symtab_cmd->symoff = 0x00010650
    symtab = linkedit_base + symtab_cmd->symoff = slide + 0x00000001 00000000 + 0x00010650 = 0x100010650
    LC_SYMTAB 00
    通过 MachOView 我们可以看到,当前 MachO 文件中符号表的起始地址确实是 0x100010650
    Symbol Table

  • 计算当前镜像的字符串表在虚拟内存中的起始地址(strtab)

    从 MachO 文件的 Load Commands 区域的 LC_SYMTAB 加载指令中可以得到:
    symtab_cmd->stroff = 0x000117F8
    strtab = linkedit_base + symtab_cmd->stroff = slide + 0x00000001 0000000 + 0x000117F8 = 0x1000117F8
    LC_SYMTAB 01
    通过 MachOView 我们可以看到,当前 MachO 文件中字符串表的起始地址确实是 0x1000117F8
    String Table

  • 计算当前镜像的间接符号表在虚拟内存中的起始地址(indirect_symtab)

    从 MachO 文件的 Load Commands 区域的 LC_DYSYMTAB 加载指令中可以得到:
    dysymtab_cmd->indirectsymoff = 0x00011720
    indirect_symtab = linkedit_base + dysymtab_cmd->indirectsymoff = slide + 0x00000001 0000000 + 0x00011720 = 0x100011720
    LC_DYSYMTAB
    通过 MachOView 我们可以看到,当前 MachO 文件中间接符号表的起始地址确实是 0x100011720
    Dynamic Symbol Table

  • 计算非懒绑定指针表在间接符号表中的起始位置

    从 MachO 文件的 Load Commands 区域的 LC_SEGMENT_64 (__DATA_CONST) 加载指令中的 Section64 Header (__got) 节可以得到:
    (非懒绑定指针表的第 0 个元素)对应(间接符号表的第 25 个元素)
    LC_SEGMENT_64 (__DATA_CONST).Section64 Header (__got)
    因为,间接符号表的起始地址为 0x100011720,每个元素占 4 个字节
    Dynamic Symbol Table
    所以,(非懒绑定指针表的第 0 个元素)在(间接符号表)中的起始地址为:0x100011720 + 4 * 25 = 0x100011784
    Non-Lazy Symbol Pointers
    Dynamic Symbol Table

  • 计算懒绑定指针表在间接符号表中的起始位置

    从 MachO 文件的 Load Commands 区域的 LC_SEGMENT_64 (__DATA) 加载指令中的 Section64 Header (__la_symbol_ptr) 节可以得到:
    (懒绑定指针表的第 0 个元素)对应(间接符号表的第 28 个元素)
    LC_SEGMENT_64 (__DATA).Section64 Header (__la_symbol_ptr)
    因为,间接符号表的起始地址为 0x100011720,每个元素占 4 个字节
    Dynamic Symbol Table
    所以,(懒绑定指针表的第 0 个元素)在(间接符号表)中的起始地址为:0x100011720 + 4 * 28 = 0x100011790
    Lazy Symbol Pointers
    Dynamic Symbol Table

  • 通过间接符号表所存储的符号表索引值,获取符号表中对应的符号,进而获取该符号存储在字符串表中的名称

    由上面的分析过程可知:(懒绑定指针表的第 0 个元素)对应(间接符号表的第 28 个元素)

    因为(NSLog 的懒绑定指针)正好是(懒绑定指针表的第 0 个元素)
    所以(NSLog 的间接符号)也正好是(间接符号表的第 28 个元素)

    因为,间接符号表中存储的是符号表的索引值
    所以,NSLog 的符号在符号表中的索引为 0x000000E8(232)
    NSLog 在间接符号表中对应的元素
    因为,符号表的起始地址为 0x100010650,每个元素占 16 个字节
    Symbol Table
    所以,NSLog 的符号在符号表中的起始地址为:0x100010650 + 16 * 232 = 0x1000114D0
    NSLog 在符号表中对应的元素
    NSLog 在符号表中的符号可知:NSLog 的符号名称在字符串表中的偏移量为 0x000000CE(206)
    因为,字符串表的起始地址为 0x1000117F8,每个字符占 1 个字节
    String Table
    所以,NSLog 的符号名称在字符串表中的起始地址为:0x1000117F8 + 1 * 206 = 0x1000118C6
    NSLog 在字符串表中对应的元素

  • fishhook 官方原理图

    经过了上述的源码分析和验证实验,相信你对 fishhook 的实现原理一定有了较为清晰的理解了。在此附上一张 fishhook 的官方图解作为总结:
    fishhook 官方原理图

拓展延伸:fishhook 为何能在第一次调用外部 C 函数之前,获取到外部 C 函数的真实地址

  • Question

    请看下面的代码示例以及输出结果:
    ViewController.m
    思考一个问题:

    根据前面介绍的 MachO 文件的懒绑定机制,在第一次调用 NSLog 函数之前,NSLog 函数的懒绑定指针存储的是指向 stub_helper 节的引用。当第一次调用 NSLog 函数时,会通过 stub_helper 节走到 dyld_stub_binder,然后进行一次真正的符号地址绑定,并将 NSLog 函数的懒绑定指针从指向 stub_helper 节修改为指向 NSLog 函数的真实地址,然后再调用 NSLog 函数的真正实现

    在上面的示例代码中,在第一次调用 NSLog 函数之前,我们使用 fishhook 对 NSLog 函数进行了符号地址的重绑定。那么在对 NSLog 函数进行符号地址重绑定之后,第一次调用 NSLog 函数之前,NSLog 函数的懒绑定指针存储的应该是指向 swizzled_NSLog 函数的引用,并且 original_NSLog 函数指针存储的应该是指向 stub_helper 节的引用

    在上面的示例代码中,当我们第一次调用 NSLog 函数时,会走到 swizzled_NSLog 的函数实现中。而在 swizzled_NSLog 的函数实现中又会去调用 original_NSLog 函数指针。而 original_NSLog 函数指针存储的是指向 stub_helper 节的引用,此时调用 original_NSLog 函数指针会触发 NSLog 函数的懒绑定流程(通过 stub_helper 节走到 dyld_stub_binder,然后进行一次真正的符号地址绑定,并将 NSLog 函数的懒绑定指针从指向 swizzled_NSLog 函数的实现修改为指向 NSLog 的真实地址,然后再调用 NSLog 函数的真正实现)。此时控制台会打印输出 hook = First

    在上面的示例代码中,在调用完 NSLog(@"First"); 之后,NSLog 函数的懒绑定指针应该会被重新修改为指向 NSLog 函数的真正实现。那么在调用 NSLog(@"Second"); 时,控制台打印输出的应该是 second,但是实际的输出结果却是 hook = Second。即,此时 NSLog 函数的懒绑定指针还处于 hook 状态(即,仍然指向 swizzled_NSLog 函数的实现),这又是为什么呢?

  • Analyze

    让我们回想起 fishhook 源码中向 dyld 注册镜像加载的回调:

    /*
    以下函数允许你向 dyld 注册回调函数,并且每当 dyld 加载镜像时,dyld 将会调用注册的回调函数
    在调用 _dyld_register_func_for_add_image() 向 dyld 注册回调函数期间,会对每个已加载的镜像调用注册的回调函数 func
    之后,每当加载和绑定新镜像时(此时该镜像还未执行初始化),也会调用注册的回调函数 func
    */
    extern void _dyld_register_func_for_add_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide));
    

    也就是说,在调用 fishhook 的 rebind_symbols 函数对 NSLog 进行符号地址重绑定时,fishhook 不是只修改了主程序 MachO 镜像中 NSLog 函数的懒绑定指针,而是修改了 App 进程空间中所包含的全部 MachO 镜像的 NSLog 函数的懒绑定指针

    在 App 的进程空间中,第 0 个镜像为主程序的 MachO,其余镜像为主程序所依赖的动态库。而在这些主程序所依赖的动态库的 MachO 镜像里面,其懒绑定指针表和非懒绑定指针表已经被 dyld 修复为指向真实的符号地址。即,包含在动态库镜像中的 NSLog 函数的懒绑定指针,也被 dyld 修复为指向 NSLog 真实的实现地址

    可以查看 fishhook 源码中 perform_rebinding_with_section 函数的下面部分,红框处的判断代码是笔者添加的
    当断点进入到红框判断后的括号里面时,第 0 个是主程序的 MachO 镜像,此时 original_NSLog 的值确实是指向 stub_helper 节的地址
    但是这个判断并非只进入一次,App 在启动时会加载大量的动态库(通常数量会达到数百个),其中有很多都会进入到这个逻辑,比如:Foundation.frameworkCoreFoundation.framework 等等,这些动态库里面的非懒加载指针已经是真实的 NSLog 的地址,所以 original_NSLog 又被替换成了真实的 NSLog 的地址

    debug 时发现这个逻辑存在大量的重复调用,本身其实只要 original_NSLog 的值不会发生改变就没必要进入下方的赋值操作。因此,笔者加了一处判断,只有当 original_NSLog 的值会发生改变时才进入括号后的赋值操作
    红框判断

  • 再扩展…

    当只针对主程序 MachO 的镜像进行符号地址重绑定时,代码示例以及输出结果如下所示:
    ViewController.m

巩固练习:通过 lldb 恢复被 fishhook 替换掉的符号地址

为了加强对 MachO 文件懒绑定机制与 fishhook 底层原理的理解,接下来我们来做一个巩固练习

  • 代码示例 && 断点
    ViewController
    在调试时显示汇编代码:Debug - Debug Workflow - Always Show Disassembly

  • 在命中第一个断点时,获取 NSLog 懒绑定指针所存储的 stub_helper 节的地址
    Break Point 1

    // 查看 NSLog 对应的桩代码,位于 Section64(__TEXT, __stubs)
    (lldb) dis -a 0x1024a6388
    FishhookDemo`NSLog:
        0x1024a6388 <+0>: nop    
        0x1024a638c <+4>: ldr    x16, #0x5c74              ; (void *)0x00000001024a64cc
        0x1024a6390 <+8>: br     x16
    // 查看 NSLog 对应的懒绑定指针所存储的内容,位于 Section64(__DATA, __la_symbol_ptr)
    (lldb) memory read/2 (0x1024a638c+0x5c74)
    0x1024ac000: 0x024a64cc 0x00000001
    // 查看 NSLog 对应的懒绑定指针所指向的 Section64(__TEXT, __stub_helper)
    (lldb) dis -s 0x00000001024a64cc
        0x1024a64cc: ldr    w16, 0x1024a64d4
        0x1024a64d0: b      0x1024a64b4
        0x1024a64d4: udf    #0x0
    // 查看 Section64(__TEXT, __stub_helper) 头部的汇编代码
    (lldb) dis -s 0x1024a64b4
        0x1024a64b4: adr    x17, #0x7094              ; _dyld_private
        0x1024a64b8: nop    
        0x1024a64bc: stp    x16, x17, [sp, #-0x10]!
        0x1024a64c0: nop    
        0x1024a64c4: ldr    x16, #0x1b4c              ; (void *)0x00000001a395e08c: dyld_stub_binder
        0x1024a64c8: br     x16
    // 查看 dyld_stub_binder 函数的汇编代码
    (lldb) dis -s 0x00000001a395e08c
    libdyld.dylib`dyld_stub_binder:
        0x1a395e08c <+0>:  stp    x29, x30, [sp, #-0x10]!
        0x1a395e090 <+4>:  mov    x29, sp
        0x1a395e094 <+8>:  sub    sp, sp, #0xf0             ; =0xf0 
        0x1a395e098 <+12>: stp    x0, x1, [x29, #-0x10]
        0x1a395e09c <+16>: stp    x2, x3, [x29, #-0x20]
        0x1a395e0a0 <+20>: stp    x4, x5, [x29, #-0x30]
        0x1a395e0a4 <+24>: stp    x6, x7, [x29, #-0x40]
        0x1a395e0a8 <+28>: stp    x8, x9, [x29, #-0x50]
    // 过掉第一个断点时,控制台打印输出如下:
    2021-09-06 15:55:32.300740+0800 FishhookDemo[34769:7041863] First
    
  • 在命中第二个断点时,查看控制台打印输出结果
    Break Point 2

    // 过掉第二个断点时,控制台打印输出如下:
    2021-09-06 15:56:30.811207+0800 FishhookDemo[34769:7041863] hook = Second
    
  • 在命中第三个断点时,设置 NSLog 懒绑定指针所存储的地址为 stub_helper 节
    Break Point 3

    // 查看 NSLog 对应的桩代码,位于 Section64(__TEXT, __stubs)
    (lldb) dis -a 0x1024a6388
    FishhookDemo`NSLog:
        0x1024a6388 <+0>: nop    
        0x1024a638c <+4>: ldr    x16, #0x5c74              ; (void *)0x00000001024a5454: swizzled_NSLog at /Users/Airths/Desktop/Training/FishhookDemo/FishhookDemo/ViewController.m:39
        0x1024a6390 <+8>: br     x16
    // 查看 NSLog 对应的懒绑定指针所指向的内容,为 swizzled_NSLog 函数的汇编代码
    (lldb) dis -s 0x00000001024a5454
    FishhookDemo`swizzled_NSLog:
        0x1024a5454 <+0>:  sub    sp, sp, #0x50             ; =0x50 
        0x1024a5458 <+4>:  stp    x29, x30, [sp, #0x40]
        0x1024a545c <+8>:  add    x29, sp, #0x40            ; =0x40 
        0x1024a5460 <+12>: sub    x8, x29, #0x8             ; =0x8 
        0x1024a5464 <+16>: mov    x9, #0x0
        0x1024a5468 <+20>: stur   x9, [x29, #-0x8]
        0x1024a546c <+24>: stur   x0, [x29, #-0x18]
        0x1024a5470 <+28>: mov    x0, x8
    // 将 NSLog 的懒绑定指针设置为指向原来的 Section64(__TEXT, __stub_helper)
    (lldb) memory write -s 8 (0x1024a638c+0x5c74) 0x00000001024a64cc
    // 再次查看 NSLog 对应的桩代码,位于 Section64(__TEXT, __stubs)
    // 发现 NSLog 对应的懒绑定指针所存储的内容,已经由指向 swizzled_NSLog 变为指向 Section64(__TEXT, __stub_helper)
    (lldb) dis -a 0x1024a6388
    FishhookDemo`NSLog:
        0x1024a6388 <+0>: nop    
        0x1024a638c <+4>: ldr    x16, #0x5c74              ; (void *)0x00000001024a64cc
        0x1024a6390 <+8>: br     x16
    // 过掉第三个断点时,控制台打印输出如下:
    2021-09-06 15:59:49.997304+0800 FishhookDemo[34769:7041863] Third
    

使用 fishhook 进行简单的 hook 防护

  • iOS hook 流程简介

    1. 获取目标 App 的 IPA 包,并对目标 App 主程序的 MachO 文件进行脱壳
    2. 对已脱壳的目标 App 进行动态调试与静态分析,还原指定功能的实现过程,寻找 App 中的注入点
    3. 编写要注入的动态库(.framework.dylib),并在要注入的动态库中
      利用 runtime 的相关函数对指定 Objective-C 方法 进行 hook
      利用 fishhook 的相关函数对指定 外部函数 进行 hook
    4. 把要注入的动态库集成到目标 App 中。修改主程序的 MachO 文件,让其加载要注入的动态库(例如,使用 yololib)
    5. 对内容发生改变的目标 App 进行重签名(主程序 MachO 文件被修改、frameworks 目录中加入了要注入的动态库)
    6. 运行目标 App 进行调试与测试
  • Objective-C 中 +load 方法调用顺序大总结

    在不同镜像中

    ① dyld 会根据 App 主程序中对动态库的编译顺序来初始化动态库的镜像(先编译先初始化,后编译后初始化)
    ② dyld 会优先初始化动态库的镜像,然后再初始化 App 主程序的镜像(App 主程序的镜像最后初始化)
    ③ 在同一个镜像内,Objective-C 的 +load 方法 会比 C++ 的 __attribute__((constructor) 函数 先调用
    ④ 所有镜像(包括 App 主程序的镜像)中的 +load 方法__attribute__((constructor) 函数 都会比 主程序的 main 函数 先调用

    在相同镜像中

    当 dyld 初始化镜像时,会通过 RunTime 的 load_images 函数(准备和执行)镜像中所有 Class 和 Category 的 +load 方法。不管程序中有没有用到这些 Class 和 Category,只要镜像被初始化,这些 Class 和 Category 就都会被加载进内存并调用 +load 方法

    ⑤ 系统自动调用 +load 方法的方式为通过函数地址直接调用(IMP):

    1. 先调用所有类对象的 +load 方法,按照编译顺序调用(先编译先调用,后编译后调用)。并且在调用子类对象的 +load 方法之前会先调用父类对象的 +load 方法
    2. 再调用所有分类对象的 +load 方法,按照编译顺序调用(先编译先调用,后编译后调用)
      注意:分类对象与宿主类对象同名的普通方法是(后编译先调用)
      注意:分类对象 +load 方法的调用顺序只与分类对象的编译顺序有关,而与分类对象的继承关系无关
    3. 系统只会自动调用实现了 +load 方法的 Class 与 Category 中的 +load 方法
      系统不会自动调用没有实现 +load 方法的 Class 与 Category 中的 +load 方法
    4. 系统自动调用 +load 方法时,Class 与 Category 中的 +load 方法不会产生覆盖,都会被调用

    ⑥ 除非开发者手动调用,否则每个 Class 和 Category 的 +load 方法在程序运行期间只会被调用 1 次

    ⑦ 开发者手动调用 +load 方法的方式为通过消息机制间接调用(objc_msgSend):

    1. 如果子类未实现 +load 方法,则会调用父类的 +load 方法
    2. 如果宿主类与分类同时实现了 +load 方法,则会调用分类的 +load 方法
    3. 如果一个宿主类的多个分类同时实现了 +load 方法,则会调用最后参与编译的分类的 +load 方法
  • 使用 yololib 注入的动态库执行的优先级最低

    使用 yololib 注入动态库之前,MachO 文件为:
    before
    使用 yololib 向 MachO 文件分别注入 .dylib.framework 格式的动态库:

    # 输出当前目录下所包含的动态库与 MachO 文件
    ~/Desktop/YololibDemo > ls -l
    total 512
    drwxr-xr-x  7 Airths  staff    224  8 14 14:55 ATool.framework
    drwxr-xr-x  7 Airths  staff    224  8 14 14:55 BTool.framework
    -rwxr-xr-x@ 1 Airths  staff  80000  8 14 14:59 TargetMachO
    -rwxr-xr-x  1 Airths  staff  86880  8 14 14:55 libCTool.dylib
    -rwxr-xr-x  1 Airths  staff  86880  8 14 14:55 libDTool.dylib
    
    # 1.向 TargetMachO 注入 libCTool.dylib
    ~/Desktop/YololibDemo > yololib TargetMachO libCTool.dylib
    
    # 2.向 TargetMachO 注入 libDTool.dylib
    ~/Desktop/YololibDemo > yololib TargetMachO libDTool.dylib
    
    # 3.向 TargetMachO 注入 ATool.framework/ATool
    ~/Desktop/YololibDemo > yololib TargetMachO ATool.framework/ATool
    
    # 4.向 TargetMachO 注入 BTool.framework/BTool
    ~/Desktop/YololibDemo > yololib TargetMachO BTool.framework/BTool
    

    使用 yololib 注入动态库之后,MachO 文件为:
    after
    after
    由此可见:
    使用 yololib 注入的动态库会按注入顺序以 LC_LOAD_DYLIB 命令的形式拼接在原有 MachO 文件 Load Commands 区域的末尾

  • 使用 fishhook 进行简单的 hook 防护(Objective-C)

    ① 根据 Objective-C 中 +load 方法调用顺序大总结+load 方法的调用顺序的介绍可知:
    如果直接将防护代码写在主程序中,则进攻代码会优先于防护代码执行(即先进行 hook,再进行反 hook),从而导致 hook 防护失败

    ② 根据 使用 yololib 注入的动态库执行的优先级最低 对注入动态库的执行顺序的介绍可知:
    (主程序中所依赖的动态库)都优先于(使用 yololib 注入的动态库)加载

    综上 ① ② 两点所述:
    为了使防护代码生效,需要将防护代码写在主程序所依赖的动态库中

    此外,Objective-C 的 RunTime 中能进行 hook 的函数都需要进行防护:
    method_exchangeImplementationsclass_replaceMethodmethod_getImplementationmethod_setImplementation
    Hook 防护

  • 攻克防护的思路

    ① 修改主程序 MachO,将注入的动态库的执行顺序提前到防护的动态库之前

    ② 因为 fishhook 重绑定的是进程中所有镜像的懒绑定指针表(Lazy Symbol Pointers)和非懒绑定指针表(Non-Lazy Symbol Pointers
    又因为在 RunTime 函数所属的动态库 libobjc.A.dylib 中,RunTime 函数属于导出给外部使用的符号,不存储在懒绑定指针表和非懒绑定指针表中
    换句话说,位于 libobjc.A.dylib 中的 method_exchangeImplementationsclass_replaceMethodmethod_getImplementationmethod_setImplementation 等用于 hook 的函数的实现地址并没有被 fishhook 所替换
    所以,可以在注入的动态库中,手动定位到位于 libobjc.A.dylib 中的 method_exchangeImplementationsclass_replaceMethodmethod_getImplementationmethod_setImplementation 等用于 hook 的函数的实现地址,并进行调用

  • 相关 Demo

    使用 fishhook 进行简单的 hook 防护(Demo)

注意

  • 关于 stub

    我觉得 stub(桩)其实应该翻译成存根会更好一点
    因为在 MachO 文件中,stub 的作用就是一小段用于获取懒绑定指针(Lazy Symbol Pointer)或者非懒绑定指针(Non-Lazy Symbol Pointer)的代码。而在现实生活中,存根的作用也是用于获取存放在某处的东西

  • 关于基地址

    在 MachO 文件中,符号表(Symbol Table)、字符串表(String Table)、间接符号表(Dynamic Symbol Table)的偏移量都是相对于 MachO 文件的基地址(Base Address),而不是相对于链接信息段(LinkEdit Segment)的基地址

  • 关于懒绑定指针表(Lazy Symbol Pointers)和非懒绑定指针表(Non-Lazy Symbol Pointers)

    MachO 文件中采用地址无关代码技术(Position-Independent Code)对外部符号进行调用。当 MachO 文件调用 dyld_stub_binderNSLogobjc_release 等外部符号时,调用指令并没有与外部符号的实现地址进行绑定,而是会通过一小段 stub 桩指令,获取位于数据段中的外部符号的实现地址,进而间接地调用外部符号

    如果外部符号是 MachO 加载和初始化过程中所必须的(比如 dyld_stub_binder 函数),那么在 dyld 初始化 MachO 镜像的阶段就会立即对该外部符号进行地址绑定,这种外部符号的地址存储在 MachO 文件数据段中的非懒绑定指针表(Non-Lazy Symbol Pointers)里面

    如果外部符号不是 MachO 加载和初始化过程中所必须的(比如 NSLog 函数、objc_release 函数),那么在 dyld 初始化 MachO 镜像的阶段则不会对该外部符号进行地址绑定,这种外部符号的地址存储在 MachO 文件数据段中的懒绑定指针表(Lazy Symbol Pointers)里面。懒绑定指针表(Lazy Symbol Pointers)中存储的函数指针在未进行符号地址绑定时,默认都会指向 stub_helper 节,而 stub_helper 节作为辅助节,最终都会指向位于非懒绑定指针表(Non-Lazy Symbol Pointers)中的 dyld_stub_binder。程序在运行过程中,当第一次调用外部符号时,会走到 dyld_stub_binder,然后进行一次真正的符号地址绑定,并将懒绑定指针表(Lazy Symbol Pointers)中对应的函数指针从指向 stub_helper 修改为符号的真实地址,从而实现懒绑定。当后续再调用该外部符号时,就会经由 stub 桩指令,拿到位于懒绑定指针表(Lazy Symbol Pointers)中的该符号的真实地址,不需要再走到 dyld_stub_binder

    总而言之,不管是懒绑定指针表(Lazy Symbol Pointers)还是非懒绑定指针表(Non-Lazy Symbol Pointers),其本质都是一个用于存储外部符号地址的函数指针表,里面的元素都是一个个的函数指针。这些函数指针决定了当 MachO 文件调用外部函数时,应该跳转到哪个代码段的哪个位置去执行

  • fishhook 只能 hook 外部的 C 函数,不能 hook 内部的 C 函数

    fishhook 是通过读取和修改当前 MachO 文件中的懒绑定指针表(Lazy Symbol Pointers)和非懒绑定指针表(Non-Lazy Symbol Pointers)的方式来达到 hook 当前 MachO 文件的外部 C 函数的目的

    当前 MachO 文件的内部 C 函数在编译时已经进行了符号地址绑定,虽然当前 MachO 文件在调用内部 C 函数时仍然会采用地址无关代码技术,但是当前 MachO 文件在调用内部 C 函数时采取的是直接调用的方式。而不是将内部 C 函数的地址存储在懒绑定指针表或者非懒绑定指针表中,并使用 stub 桩代码进行间接调用

    因此,fishhook 只能 hook 外部的 C 函数,不能 hook 内部的 C 函数

  • 打印进程中所有镜像的信息

    -(void)printImagesInfo {
        uint32_t count = _dyld_image_count();
        for (uint32_t index = 0; index < count; index++) {
            const char * name = _dyld_get_image_name(index);
            intptr_t slide = _dyld_get_image_vmaddr_slide(index);
            const struct mach_header * header = _dyld_get_image_header(index);
            NSLog(@"index = %03d", index);
            NSLog(@"name = %s", name);
            NSLog(@"slide = 0x%lx", slide);
            NSLog(@"magic = 0x%x, cputype = 0x%x, cpusubtype = 0x%x, filetype = 0x%x, ncmds = %u, sizeofcmds = %u, flags = 0x%x",
                  header->magic, header->cputype, header->cpusubtype, header->filetype, header->ncmds, header->sizeofcmds, header->flags);
            NSLog(@"");
        }
    }
    
  • 向 dyld 注册镜像加载和卸载的回调

    -(void)dyldRegisterCallback {
        _dyld_register_func_for_add_image(dyld_add_image_callback);
        _dyld_register_func_for_remove_image(dyld_remove_image_callback);
    }
    
    static void dyld_add_image_callback (const struct mach_header* header,
                                         intptr_t slide) {
        static int addIndex = 0;
        NSLog(@"addIndex = %03d", addIndex++);
        NSLog(@"magic = 0x%x, cputype = 0x%x, cpusubtype = 0x%x, filetype = 0x%x, ncmds = %u, sizeofcmds = %u, flags = 0x%x",
              header->magic, header->cputype, header->cpusubtype, header->filetype, header->ncmds, header->sizeofcmds, header->flags);
        NSLog(@"slide = 0x%lx", slide);
        NSLog(@"");
    }
    
    static void dyld_remove_image_callback (const struct mach_header* header,
                                            intptr_t slide) {
        static int removeIndex = 0;
        NSLog(@"removeIndex = %03d", removeIndex++);
        NSLog(@"magic = 0x%x, cputype = 0x%x, cpusubtype = 0x%x, filetype = 0x%x, ncmds = %u, sizeofcmds = %u, flags = 0x%x",
              header->magic, header->cputype, header->cpusubtype, header->filetype, header->ncmds, header->sizeofcmds, header->flags);
        NSLog(@"slide = 0x%lx", slide);
        NSLog(@"");
    }
    
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
引用\[1\]中的代码展示了如何使用fishhook库来hook open和fopen函数。在main函数中,通过rebind_symbols函数将fopen函数重新绑定到my_fopen函数上,然后在my_fopen函数中可以对打开文件的操作进行个性化的判断和记录。这样就可以实现对open和fopen函数的hook操作。 引用\[2\]中提到,通过hook open函数可以打印系统中所有调用open函数打开文件的文件路径和使用open函数的进程PID以及进程名。在hook_fun()函数中可以对open函数的调用进行个性化的判断,例如禁止打开某些文件、篡改打开文件的路径或记录打开文件的行为等。 在Linux系统下,文件的创建、读取、追加、写入和删除等操作涉及到以下系统调用:open、fopen、read、write、lseek、close和unlink。通过hook这些系统调用,可以对文件操作进行监控和控制。 所以,通过使用fishhook库和hook技术,可以实现对open和fopen函数的hook操作,从而对文件操作进行个性化的判断和记录。 #### 引用[.reference_title] - *1* [使用fishhook hook函数fopen](https://blog.csdn.net/pureszgd/article/details/105282990)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [简述API HOOK技术及原理](https://blog.csdn.net/bing1564/article/details/130457403)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [【Android】Frida Hook 文件读写操作](https://blog.csdn.net/xiru9972/article/details/131182528)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值