iOS - hook msg_send

本篇是iOS开发高手课读书笔记第一篇

fishoook

fishoook是Facebook 开源的一个库,可以动态地重新绑定在 iOS 上运行的 Mach-O 二进制文件的符号

fishhook 实现的大致思路是,通过重新绑定符号,可以实现对 c 方法的 hook。dyld 是通过更新 Mach-O 二进制的 __DATA segment 特定的部分中的指针来绑定 lazy 和 non-lazy 符号,通过确认传递给 rebind_symbol 里每个符号名称更新的位置,就可以找出对应替换来重新绑定这些符号。

简单实例

官方给的使用例子
#import <UIKit/UIKit.h>
#import "AppDelegate.h"

#import <dlfcn.h>
#import <fishhook/fishhook.h>

static int (*orig_close)(int);
static int (*orig_open)(const char *, int, ...);
 
int my_close(int fd) {
  printf("Calling real close(%d)\n", fd);
    
  return orig_close(fd);
}
 
int my_open(const char *path, int oflag, ...) {
  va_list ap = {0};
  mode_t mode = 0;
 
  if ((oflag & O_CREAT) != 0) {
    // mode only applies to O_CREAT
    va_start(ap, oflag);
    mode = va_arg(ap, int);
    va_end(ap);
    printf("Calling real open('%s', %d, %d)\n", path, oflag, mode);
    return orig_open(path, oflag, mode);
  } else {
    printf("Calling real open('%s', %d)\n", path, oflag);
    return orig_open(path, oflag, mode);
  }
}

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        
//        hook close 和 open 函数
        rebind_symbols((struct rebinding[2]){{"close", my_close, (void *)&orig_close}, {"open", my_open, (void *)&orig_open}}, 2);
           // 打开我们自己的二进制文件并打印出前4个字节(这对于给定架构上的所有Mach-O二进制文件都是相同的)
            int fd = open(argv[0], O_RDONLY);
            uint32_t magic_number = 0;
            read(fd, &magic_number, 4);
            printf("Mach-O Magic Number: %x \n", magic_number);
            close(fd);
        
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

运行效果

Calling real open('/Users/geneqiao/Library/Developer/CoreSimulator/Devices/B1724970-4E12-4FA1-961F-5987B32AD8C7/data/Containers/Bundle/Application/6BCC65C2-A74F-406D-BC15-4CB90FD67864/hook_msgsend.app/hook_msgsend', 0)
Calling real close(5)
hook NSLog 函数
- (void)viewDidLoad {
    [super viewDidLoad];
    struct rebinding nslog;
    nslog.name = "NSLog";
    nslog.replacement = nNSLog;
    nslog.replaced = (void *)&oNSLog;
    
    struct rebinding rebinds[1] = {nslog};
    
    rebind_symbols(rebinds, 1);
    
    
}
static void (* oNSLog)(NSString *format, ...);

void nNSLog(NSString *format, ...) {
    oNSLog(@"%@",[format stringByAppendingString:@"被HOOK了"]);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"测试");
}

打印结果

2020-12-18 16:56:05.908524+0800 hook_msgsend[57695:13206866] 测试被HOOK了

对于我们自己的C函数,fishHook并没有hook成功,还是执行原来的方法。

fishHook原理

1.共享缓存库
在iOS or Mac系统中,几乎所有的程序都会用到动态库,而动态库在加载的时候都需要用dyld(位于/usr/lib/dyld)程序进行链接。
很多系统库几乎每个程序都要用到,如果在程序运行的时候一个一个将这些动态库加载进来,不仅耗费内存,而且耗时。为了降低内存,提高性能,苹果引入了共享缓存库,用来存储系统的库。
Mac下的共享缓存库位置:/private/var/db/dyld/
iOS下的共享缓存库位置:/System/Library/Caches/com.apple.dyld/
文件名都是以“dyld_shared_cache_”开头,再加上这个dyld缓存文件所支持的指令集。

2.PIC技术(位置独立代码)
C语言是静态的,也就是说,在编译的时候就已经确定了函数的地址。而系统的函数由于共享缓存库的存在,必须是dyld加载的时候(运行时)才能确定,这明显存在矛盾。
为了解决这个问题,苹果针对Mach-O文件提供了一种PIC技术,即在Match-O的_Data段中添加懒加载表(Lazy Symbol Pointers)和非懒加载表(Non-Lazy Symbol Pointers)两个表,让系统的函数在编译的时候先指向懒加载表(Lazy Symbol Pointers)或非懒加载表(Non-Lazy Symbol Pointers)中的符号地址,
这两个表中符号地址的指向在编译的时候并没有指向任何地方,app启动,被dyld加载到内存,就进行链接, 给这2个表赋值动态缓存库的地址进行符号绑定

使用 lldb 调试

使用 image list lldb 命令,可以得到 ASLR 地址偏移

(lldb) image list

[  0] FEC53598-931B-3F55-A692-AB7462FC5755 0x000000010260d000 /Users/geneqiao/Library/Developer/Xcode/DerivedData/hook_msgsend-cdykumyojctkvuabnasqgaskibpl/Build/Products/Debug-iphonesimulator/hook_msgsend.app/hook_msgsend 

使用MatchOView查看MatchO懒加载表中的NSLog符号在文件中的偏移值

image.png

调用NSLog前

(lldb) x 0x00000001025ec000+0x8000
x 0x00000001025ec000+0x8000
0x1025f4000: aa e3 5e 02 01 00 00 00 44 17 7f 20 ff 7f 00 00  ..^.....D.. ....
0x1025f4010: 57 5c 69 24 ff 7f 00 00 0e e4 5e 02 01 00 00 00  W\i$......^.....
0x1025f4000: aa e3 5e 02 01 00 00 00 44 17 7f 20 ff 7f 00 00  ..^.....D.. ....
0x1025f4010: 57 5c 69 24 ff 7f 00 00 0e e4 5e 02 01 00 00 00  W\i$......^.....
(lldb) dis -s 01025ee3aa
    0x1025ee3aa: pushq  $0x0
    0x1025ee3af: jmp    0x1025ee390
    0x1025ee3b4: pushq  $0xd
    0x1025ee3b9: jmp    0x1025ee390
    0x1025ee3be: pushq  $0x65
    0x1025ee3c3: jmp    0x1025ee390

调用NSLog后

(lldb) x 0x00000001025ec000+0x8000
0x1025f4000: 0d 4d 80 20 ff 7f 00 00 44 17 7f 20 ff 7f 00 00  .M. ....D.. ....
0x1025f4010: 57 5c 69 24 ff 7f 00 00 0e e4 5e 02 01 00 00 00  W\i$......^.....
(lldb) dis -s 7fff20804d0d
Foundation`NSLog:
    0x7fff20804d0d <+0>:  pushq  %rbp
    0x7fff20804d0e <+1>:  movq   %rsp, %rbp
    0x7fff20804d11 <+4>:  subq   $0xd0, %rsp
    0x7fff20804d18 <+11>: testb  %al, %al
    0x7fff20804d1a <+13>: je     0x7fff20804d42            ; <+53>
    0x7fff20804d1c <+15>: movaps %xmm0, -0xa0(%rbp)
    0x7fff20804d23 <+22>: movaps %xmm1, -0x90(%rbp)

Hook NSLog后

(lldb) x 0x00000001025ec000+0x8000
0x1025f4000: c0 d9 5e 02 01 00 00 00 44 17 7f 20 ff 7f 00 00  ..^.....D.. ....
0x1025f4010: 57 5c 69 24 ff 7f 00 00 0e e4 5e 02 01 00 00 00  W\i$......^.....
(lldb) dis -s 01025ed9c0
hook_msgsend`nNSLog:
    0x1025ed9c0 <+0>:  pushq  %rbp
    0x1025ed9c1 <+1>:  movq   %rsp, %rbp
    0x1025ed9c4 <+4>:  subq   $0x30, %rsp
    0x1025ed9c8 <+8>:  movq   $0x0, -0x8(%rbp)
    0x1025ed9d0 <+16>: leaq   -0x8(%rbp), %rax
    0x1025ed9d4 <+20>: movq   %rdi, -0x10(%rbp)
    0x1025ed9d8 <+24>: movq   %rax, %rdi
    0x1025ed9db <+27>: movq   -0x10(%rbp), %rsi

查看NSLog执行之前,执行了NSLog,被Hook以后这三个时刻,懒加载表中NSLog符号指向的地址。得到结论:
fishHook其实就是修改懒加载表(Lazy Symbol Pointers)、非懒加载表(Non-Lazy Symbol Pointers)中的符号地址的指向,从而达到hook的目的。

将指向系统方法(外部函数)的指针重新进行绑定指向内部函数/自定义 C 函数。
将内部函数的指针在动态链接时指向系统方法的地址。

也就是说内部/自定义的 C 函数 fishhook 也 HOOK 不了,它只能HOOK Mach-O 外部(共享缓存库中)的函数

fishhook 是如何根据字符串对应在符号表中的指针,找到其在共享库的函数实现的?
官方给出的图

这张图在描述如何由一个字符串(比如 "NSLog"),跟着它在 MachO 文件的懒加载表中对应的指针,一步步的找到该指针指向的函数实现地址,大致步骤如下:
1.根据 LoadCommand 字段,获取 lazy_symbol_ptr Section地址。
遍历 lazy_symbol_ptr Section,根据当前是第几项,加上LoadCommand上,Indirect SymIndex的值,查找当前符号在间接表( Dynamic Symbols Table -> Indirect Symbols)中的信息。

image.png
image.png

从间接表中获取到符号表下标。


image.png

从符号表中获取到字符串表下标。


image.png

得到符号字符串,与待替换函数名称进行比较。若相等,则更新 got 表中的值。

image.png

fishhook 使用场景

防止第三方注入

基本思路:

  1. 在基础的动态调试逆向中,最常见的就是定位到目标方法后,通过 runtime 中的几种方法交换方式,实现代码注入的目的。iOS代码注入+HOOK微信登录
  2. 既然 fishhook 可以拦截系统外部的 C 函数,那自然就可以 HOOK 到 runtime 库中的所有方法。
  3. 那我们就将所有可能用来篡改我们 OC 方法实现的 runtime API,都用 fishhook 拦截掉,使其无法用代码注入的方式成功 HOOK。


    image.png

这时我们程序跑起来就可以看到如下输出:


image.png

为了阻止其完成方法交换,我们要 hook method_exchangeImplementations 方法,拖入 fishhook 源文件,再添加一个分类并写好 hook method_exchangeImplementations 的代码

image.png

你会发现 method_exchangeImplementations 并没有 HOOK 成功, viewDidAppear: 依然被篡改了实现.
项目里参与编译的文件顺序就是其编译后被加载时的载入顺序,即此时 ViewController+HOOKTest 的 load 方法会早于 ViewController+FishHook 的 load 调用,所以 method_exchangeImplementations 的实现被我们 HOOK 发生在 viewDidAppear: 被别人交换之后,从而导致防护的失败:


image.png

如上图所示,在调整了编译文件的顺序之后成功 HOOK 到了 method_exchangeImplementations 的调用,但实际开发中我们不可能采用这么笨的方法,也不可能通过这种方式决定文件的载入顺序,因此我们要想办法保证 fishhook 的代码必须最先执行才行。

那如何做到呢?由此前的 dyld背后的故事&源码分析 可以得知,本地的Framework中的类一定会早于后注入的库(动态库例外,非越狱设备是没有插入动态库的权限的)和可执行文件中的类进行初始化。所以我们将 fishhook 的 HOOK 操作代码移到自建的 Framework 中即可:

image.png

至此,我们已经知道了 fishhook 反调试的基本思路,实际开发中,像method_getImplementation、method_setImplementation等函数都需要用同样的方式HOOK,同时,如果自己的项目中已经用到了这些函数,还需要设计相应的白名单方案,并且在检测到是被三方非法 HOOK 时通常直接调用 exit(0) 这类接口终止掉进程。

fishhook 源码分析

在使用 fishhook 时,需要声明一个 rebinding 类型的结构体变量.

/*
一种结构,表示从符号名到它的替换物之间重新绑定的特定意图
 */
struct rebinding {
  const char *name;// 需要hook的函数名
  void *replacement;//新函数的地址
  void **replaced;//指向原函数实现的指针
};

创建结构体后, 再把这些结构体放到一个数组中,然后调用重绑定符号函数 int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);(如果绑定成功返回 0,否则返回 -1),并将结构体数组长度作为参数传入:
rebind_symbols

int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
  int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
  if (retval < 0) {
    return retval;
  }
    // 第一次调用,添加回调,对库的装载完成进行监听和回调:
  if (!_rebindings_head->next) {
    _dyld_register_func_for_add_image(_rebind_symbols_for_image);
  } else {
      // 遍历所有 image
    uint32_t c = _dyld_image_count();
    for (uint32_t i = 0; i < c; i++) {
    //已经载入的镜像文件(也就是库)逐一查找目标符号进行 hook。
      _rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));
    }
  }
  return retval;
}

prepend_rebindings

struct rebindings_entry {
  struct rebinding *rebindings;
  size_t rebindings_nel;
  struct rebindings_entry *next;
};

static struct rebindings_entry *_rebindings_head;

static int prepend_rebindings(struct rebindings_entry **rebindings_head,
                              struct rebinding rebindings[],
                              size_t nel) {
    
    // 创建结构体
  struct rebindings_entry *new_entry = malloc(sizeof(struct rebindings_entry));
  if (!new_entry) {
    return -1;
  }
    // 赋值结构体
  new_entry->rebindings = malloc(sizeof(struct rebinding) * nel);
    // 失败
  if (!new_entry->rebindings) {
    free(new_entry);
    return -1;
  }
    // 内存拷贝
  memcpy(new_entry->rebindings, rebindings, sizeof(struct rebinding) * nel);
    //
  new_entry->rebindings_nel = nel;
  new_entry->next = *rebindings_head;
  *rebindings_head = new_entry;
  return 0;
}

看源码:_rebindings_head 被声明为一个指向 rebindings_entry 结构体的静态指针变量,那 &_rebindings_head 就是取出这个指针的地址.
而 ** rebindings_entry ** 是一个链表,存储有 rebinding数组,数组元素的数量(用于开辟对应大小的空间),和下一个rebindings_entry结构体
_rebindings_head 就是指向该链表的指针。
为了加深理解,我为你画了一张 prepend_rebindings 函数的作用示意图:

prepend_rebindings 函数的目的:将新加入的 rebindings 数组不断的添加到 _rebindings_head 这个链表的头部成为新的头节点。

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);

对已经载入的镜像文件(也就是库)逐一查找目标符号进行 hook。但 fishhook 的代码执行时间非常早,所以第一次执行时,要 hook 的库可能还没完成装载,因此如果是第一次调用,会通过一个函数对库的装载完成进行监听和回调:

static void _rebind_symbols_for_image(const struct mach_header *header,
                                      intptr_t slide) {
//  当回调到 _rebind_symbols_for_image 时,会将存着待绑定函数信息的链表作为参数传入,用于符号查找和函数指针的交换,第二个参数 header是 当前 image 的头信息,第三个参数 slide是 ASLR 的偏移
    rebind_symbols_for_image(_rebindings_head, header, slide);
}
// 无非就是按照规则计算各种表的地址和指针在表中的偏移量。
static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
                                     const struct mach_header *header,
                                     intptr_t slide) {
  Dl_info info;
// 加载当前 mach-O 的所有header
  if (dladdr(header, &info) == 0) {
    return;
  }

  segment_command_t *cur_seg_cmd;
  segment_command_t *linkedit_segment = NULL;
  struct symtab_command* symtab_cmd = NULL;
  struct dysymtab_command* dysymtab_cmd = NULL;

    // 找到符号表相关的 command,包括 linkedit segment command、symtab command 和 dysymtab command
// 跳过header的大小,直接寻找loadcommand
  uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
  for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
    cur_seg_cmd = (segment_command_t *)cur;
    if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
      if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
        linkedit_segment = cur_seg_cmd;
      }
    } else if (cur_seg_cmd->cmd == LC_SYMTAB) {
      symtab_cmd = (struct symtab_command*)cur_seg_cmd;
    } else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
      dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
    }
  }

  if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
      !dysymtab_cmd->nindirectsyms) {
    return;
  }    
   //获得 base symbol 和 indirect (string table) 符号表
  // 链接时程序的基础地址 = slide的偏移值 +  _ _LINKEDIT.VM_Address - _ _LINKEDIT.File_Offset
  uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
  // Symbol Table Offset 符号表的地址 = 基址 + 符号表偏移量
  nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
  // String Table  字符串表的地址 = 基址 + 字符串偏移量
  char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);

  // Dynamic Symbol Table 动态符号表的地址 = 基址 + 动态符号表偏移量
  uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);

  cur = (uintptr_t)header + sizeof(mach_header_t);
// 重新遍历
  for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
    cur_seg_cmd = (segment_command_t *)cur;
    if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
      //找到DATA和DATA_CONST segment
      if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
          strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
        continue;
      }
      // 遍历 number of sections in segment
      for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
        section_t *sect =
          (section_t *)(cur + sizeof(segment_command_t)) + j;
        // 找懒加载表
        if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
          perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
        }
        // 非懒加载表
        if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
          perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
        }
      }
    }
  }
}

最后根据算好的符号表地址和偏移量,找到共享库目标函数的指针,然后将该指针的值(即目标函数的地址)赋值给 *replaced,之后修改该指针的值为我们的 replacement(新的函数地址),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) {
//  section的reserved1 + Dynamic Symbol Table 动态符号表的地址 =  indirect_symbol 的地址
  uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
// slider + section->addr = 符号对应存储函数实现的数组,用来寻找到函数的地址
  void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
// 遍历section 的每一个符号
  for (uint i = 0; i < section->size / sizeof(void *); i++) {
// 找到符号在Symbol Table表中的索引
    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;
    }
// 以symtab_index 作为下标,访问symbol table中对应的函数信息
    uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
// 获取symbol_name
    char *symbol_name = strtab + strtab_offset;
// 遍历 rebindings
    struct rebindings_entry *cur = rebindings;
    while (cur) {
      for (uint j = 0; j < cur->rebindings_nel; j++) {
        // 这里 strcmp是判断symbol_name与rebindings 中对应的函数名是否相等,相等即为目标hook函数
        if (strlen(symbol_name) > 1 &&
            strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
          // 判断replace.地址不为NULL以及还没有hook过,避免重复交换和空指针
          if (cur->rebindings[j].replaced != NULL &&
              indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
            // 让rebindings[j].repalced 保存 indirect_symbol_bindings[i]的函数地址
            *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
          }
          // 将替换后的方法给原先的方法,也就是替换内容为自定义函数地址
          indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
          //遍历下一个符号
          goto symbol_loop;
        }
      }
      cur = cur->next;
    }
  symbol_loop:;
  }
}

hook msg_send

objc_msgSend底层使用汇编实现的: objc_msgSend 源码跟踪,

函数定义如下

objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
  OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

看了函数定义,hook的难点是不定参数。既然是不定参数,其实我们可以根据不定参数的原理,在 va_list 来解析数组,就可以获取到所有参数了。

虽然使用 va_list 是行的通的,但是我们需要考虑 va_list 在不同平台上的数据结构差异。

但既然都已经对不同平台做差异化处理了,那么干脆就直接使用内联汇编来实现传参逻辑就可以了

因此需要使用一些汇编代码,
先实现两个方法 pushCallRecord 和 popCallRecord,来分别记录 objc_msgSend 方法调用前后的时间,然后相减就能够得到方法的执行耗时。
针对 arm64 架构: arm64 有 31 个 64 bit 的整数型寄存器,分别用 x0 到 x30 表示。
主要的实现思路是:

  1. 入栈参数,参数寄存器是 x0~ x7。对于 objc_msgSend 方法来说,x0 第一个参数是传入对象,x1 第二个参数是选择器 _cmd。syscall 的 number 会放到 x8 里。
  2. 交换寄存器中保存的参数,将用于返回的寄存器 lr 中的数据移到 x1 里。
  3. 使用 bl label 语法调用 pushCallRecord 函数。
  4. 执行原始的 objc_msgSend,保存返回值。
  5. 使用 bl label 语法调用 popCallRecord 函数。

开始hook,记得设置hook的深度

void smCallTraceStart() {
    _call_record_enabled = true;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        /*
         线程私有数据采用了一键多值的技术,即一个键对应多个值。访问数据时都是通过键值来访问,好像是对一个变量进行访问,其实是在访问不同的数据。
         一键多值靠的是一个关键数据结构数组即TSD池,创建一个TSD就相当于将结构数组中的某一项设置为“in_use”,并将其索引返回给*key,然后设置清理函数。

         第一个参数为指向一个键值的指针,key一旦被创建,所有线程都可以访问它,但各线程可根据自己的需要往key中填入不同的值,这就相当于提供了一个同名而不同值的全局变量,一键多值。
         第二个参数指明了一个destructor函数,如果这个参数不为空,那么当每个线程结束时,系统将调用这个函数来释放绑定在这个键上的内存块。
         */
        pthread_key_create(&_thread_key, &release_thread_call_stack);
        // 使用fishhook hook `objc_msgSend`函数
        fish_rebind_symbols((struct rebinding[6]){
            {"objc_msgSend", (void *)hook_Objc_msgSend, (void **)&orig_objc_msgSend},
        }, 1);
    });
}
// 停止hook
void smCallTraceStop() {
    _call_record_enabled = false;
}

// 最小时间
void smCallConfigMinTime(uint64_t us) {
    _min_time_cost = us;
}
// 最大深度
void smCallConfigMaxDepth(int depth) {
    _max_call_depth = depth;
}

// 获取?
smCallRecord *smGetCallRecords(int *num) {
    if (num) {
        *num = _smRecordNum;
    }
    return _smCallRecords;
}

// 清除
void smClearCallRecords() {
    if (_smCallRecords) {
        free(_smCallRecords);
        _smCallRecords = NULL;
    }
    _smRecordNum = 0;
}

hook_Objc_msgSend
结构体的定义

typedef struct {
    __unsafe_unretained Class cls;
    SEL sel;
    uint64_t time; // us (1/1000 ms)
    int depth;
} smCallRecord;

// CallRecord 记录调用方法详细信息,包括 obj 和 SEL 等;
typedef struct {
    id self; //通过 object_getClass 能够得到 Class 再通过 NSStringFromClass 能够得到类名
    Class cls;
    SEL cmd; //通过 NSStringFromSelector 方法能够得到方法名
    uint64_t time; //us
    uintptr_t lr; // link register
} thread_call_record;
// ThreadCallStack 里面,需要用 index 记录当前调用方法树的深度。
typedef struct {
    thread_call_record *stack;
    int allocated_length;
    int index; //index 记录当前调用方法树的深度
    bool is_main_thread;
} thread_call_stack;

// 获取 thread_call_stack
static inline thread_call_stack * get_thread_call_stack() {
    thread_call_stack *cs = (thread_call_stack *)pthread_getspecific(_thread_key);
    if (cs == NULL) {
        cs = (thread_call_stack *)malloc(sizeof(thread_call_stack));
        cs->stack = (thread_call_record *)calloc(128, sizeof(thread_call_record));
        cs->allocated_length = 64;
        cs->index = -1;
        cs->is_main_thread = pthread_main_np();
        pthread_setspecific(_thread_key, cs);
    }
    return cs;
}
// 释放 thread_call_stack
static void release_thread_call_stack(void *ptr) {
    thread_call_stack *cs = (thread_call_stack *)ptr;
    if (!cs) return;
    if (cs->stack) free(cs->stack);
    free(cs);
}


typedef struct {
    __unsafe_unretained Class cls;
    SEL sel;
    uint64_t time; // us (1/1000 ms)
    int depth;
} smCallRecord;

static smCallRecord *_smCallRecords;
/*
即所谓的“裸函数”,对于这种函数,编译器不会生成任何函数入口代码和退出代码。这种函数一般应用在与操作系统内核相关的代码中,如中断处理函数、钩子函数等。
因为编译器不会生成入口代码和退出代码,所以写naked函数的时候要分外小心。进入函数代码时,父函数仅仅会将参数和返回地址压栈,亦即只有esp寄存器和eip寄存器会发生变化。
一般来说,使用naked函数时需要注意以下问题
1、函数必须显式返回。
一般通过__asm ret的内嵌汇编指令返回,否则的话,就等着int 3吧^^
2、不可以通过任何方式使用局部变量。
若声明一个局部变量,并在代码中为其赋值,则会更改父函数中相应位置的局部函数的值。举例:
*/
__attribute__((__naked__))
static void hook_Objc_msgSend() {
    // 保存参数
    save()
    //  交换寄存器中保存的参数,将用于返回的寄存器 lr 中的数据移到 x1 里。
    __asm volatile ("mov x2, lr\n");
    __asm volatile ("mov x3, x4\n");
    // 调用  before_objc_msgSend函数
    call(blr, &before_objc_msgSend)
    // 加载参数
    load()
    // 调用原始的objc_msgSend方法
    call(blr, orig_objc_msgSend)
    // 保存原始的返回值
    save()
    // 调用 after_objc_msgSend 函数
    call(blr, &after_objc_msgSend)
    // 还原lr
    __asm volatile ("mov lr, x0\n");
    // 加载原始的 返回值
    load()
    // 返回
    ret()
}

参数保存 save()

/*
入栈参数,参数寄存器是 x0~ x7。对于 objc_msgSend 方法来说,x0 第一个参数是传入对象,x1 第二个参数是选择器 _cmd。syscall 的 number 会放到 x8 里。
*/
#define save() \
__asm volatile ( \
"stp x8, x9, [sp, #-16]!\n" \
"stp x6, x7, [sp, #-16]!\n" \
"stp x4, x5, [sp, #-16]!\n" \
"stp x2, x3, [sp, #-16]!\n" \
"stp x0, x1, [sp, #-16]!\n");
// 参数还原
#define load() \
__asm volatile ( \
"ldp x0, x1, [sp], #16\n" \
"ldp x2, x3, [sp], #16\n" \
"ldp x4, x5, [sp], #16\n" \
"ldp x6, x7, [sp], #16\n" \
"ldp x8, x9, [sp], #16\n" );

调用

#define call(b, value) \
__asm volatile ("stp x8, x9, [sp, #-16]!\n"); \
__asm volatile ("mov x12, %0\n" :: "r"(value)); \ 
__asm volatile ("ldp x8, x9, [sp], #16\n"); \
__asm volatile (#b " x12\n");

返回

#define ret() __asm volatile ("ret\n");

before and after

void before_objc_msgSend(id self, SEL _cmd, uintptr_t lr) {
    push_call_record(self, object_getClass(self), _cmd, lr);
}

uintptr_t after_objc_msgSend() {
    return pop_call_record();
}

static inline void push_call_record(id _self, Class _cls, SEL _cmd, uintptr_t lr) {
    thread_call_stack *cs = get_thread_call_stack();
    if (cs) {
        int nextIndex = (++cs->index);
        if (nextIndex >= cs->allocated_length) {
            cs->allocated_length += 64;
            cs->stack = (thread_call_record *)realloc(cs->stack, cs->allocated_length * sizeof(thread_call_record));
        }
        thread_call_record *newRecord = &cs->stack[nextIndex];
        newRecord->self = _self;
        newRecord->cls = _cls;
        newRecord->cmd = _cmd;
        newRecord->lr = lr;
        if (cs->is_main_thread && _call_record_enabled) {
            记录当前时间
            struct timeval now;
            gettimeofday(&now, NULL);
            newRecord->time = (now.tv_sec % 100) * 1000000 + now.tv_usec;
        }
    }
}

static int _smRecordAlloc;

static inline uintptr_t pop_call_record() {
    thread_call_stack *cs = get_thread_call_stack();
    int curIndex = cs->index;
    int nextIndex = cs->index--;
    thread_call_record *pRecord = &cs->stack[nextIndex];
    
    if (cs->is_main_thread && _call_record_enabled) {
        struct timeval now;
        gettimeofday(&now, NULL);
        // 计算耗时
        uint64_t time = (now.tv_sec % 100) * 1000000 + now.tv_usec;
        if (time < pRecord->time) {
            time += 100 * 1000000;
        }
        // 耗时
        uint64_t cost = time - pRecord->time;
        // 大于最小耗时.且深度小于最大深度
        //static int _smRecordNum;
        if (cost > _min_time_cost && cs->index < _max_call_depth) {
            if (!_smCallRecords) {
                _smRecordAlloc = 1024;
                _smCallRecords = malloc(sizeof(smCallRecord) * _smRecordAlloc);
            }
            _smRecordNum++;
            if (_smRecordNum >= _smRecordAlloc) {
                _smRecordAlloc += 1024;
                _smCallRecords = realloc(_smCallRecords, sizeof(smCallRecord) * _smRecordAlloc);
            }
           // 存储相关信息
            smCallRecord *log = &_smCallRecords[_smRecordNum - 1];
            log->cls = pRecord->cls;
            log->depth = curIndex;
            log->sel = pRecord->cmd;
            log->time = cost;
        }
    }
    return pRecord->lr;
}

调用代码

#pragma mark - Trace
#pragma mark - OC Interface
+ (void)start {
    smCallTraceStart();
}
+ (void)startWithMaxDepth:(int)depth {
    smCallConfigMaxDepth(depth);
    [SMCallTrace start];
}
+ (void)startWithMinCost:(double)ms {
    smCallConfigMinTime(ms * 1000);
    [SMCallTrace start];
}
+ (void)startWithMaxDepth:(int)depth minCost:(double)ms {
    smCallConfigMaxDepth(depth);
    smCallConfigMinTime(ms * 1000);
    [SMCallTrace start];
}
+ (void)stop {
    smCallTraceStop();
}
+ (void)save {
    NSMutableString *mStr = [NSMutableString new];
    NSArray<SMCallTraceTimeCostModel *> *arr = [self loadRecords];
    for (SMCallTraceTimeCostModel *model in arr) {
        //记录方法路径
        model.path = [NSString stringWithFormat:@"[%@ %@]",model.className,model.methodName];
        [self appendRecord:model to:mStr];
    }
//    NSLog(@"%@",mStr);
}
+ (void)stopSaveAndClean {
    [SMCallTrace stop];
    [SMCallTrace save];
    smClearCallRecords();
}
+ (void)appendRecord:(SMCallTraceTimeCostModel *)cost to:(NSMutableString *)mStr {
//    [mStr appendFormat:@"%@\n path%@\n",[cost des],cost.path];
    if (cost.subCosts.count < 1) {
        cost.lastCall = YES;
        //记录到数据库中
        [[SMLagDB shareInstance] addWithClsCallModel:cost];
    } else {
        for (SMCallTraceTimeCostModel *model in cost.subCosts) {
            if ([model.className isEqualToString:@"SMCallTrace"]) {
                break;
            }
            //记录方法的子方法的路径
            model.path = [NSString stringWithFormat:@"%@ - [%@ %@]",cost.path,model.className,model.methodName];
            [self appendRecord:model to:mStr];
        }
    }
    
}
+ (NSArray<SMCallTraceTimeCostModel *>*)loadRecords {
    NSMutableArray<SMCallTraceTimeCostModel *> *arr = [NSMutableArray new];
    int num = 0;
    smCallRecord *records = smGetCallRecords(&num);
    for (int i = 0; i < num; i++) {
        smCallRecord *rd = &records[i];
        SMCallTraceTimeCostModel *model = [SMCallTraceTimeCostModel new];
        model.className = NSStringFromClass(rd->cls);
        model.methodName = NSStringFromSelector(rd->sel);
        model.isClassMethod = class_isMetaClass(rd->cls);
        model.timeCost = (double)rd->time / 1000000.0;
        model.callDepth = rd->depth;
        [arr addObject:model];
    }
    NSUInteger count = arr.count;
    for (NSUInteger i = 0; i < count; i++) {
        SMCallTraceTimeCostModel *model = arr[i];
        if (model.callDepth > 0) {
            [arr removeObjectAtIndex:i];
            //Todo:不需要循环,直接设置下一个,然后判断好边界就行
            for (NSUInteger j = i; j < count - 1; j++) {
                //下一个深度小的话就开始将后面的递归的往 sub array 里添加
                if (arr[j].callDepth + 1 == model.callDepth) {
                    NSMutableArray *sub = (NSMutableArray *)arr[j].subCosts;
                    if (!sub) {
                        sub = [NSMutableArray new];
                        arr[j].subCosts = sub;
                    }
                    [sub insertObject:model atIndex:0];
                }
            }
            i--;
            count--;
        }
    }
    return arr;
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容

  • HOOK,中文译为“挂钩”或“钩子”。在iOS逆向中是指改变程序运行流程的一种技术。通过hook可以让别人的程序执...
    鼬殿阅读 1,436评论 0 2
  • HOOK HOOK,中文译为“挂钩”或“钩子”。在iOS逆向中是指改变程序运行流程的一种技术。通过hook可以让别...
    looha阅读 720评论 0 1
  • Hook 1. 概述:Hook,中文译为“挂钩”或“钩子”。在iOS逆向中是指改变程序运行流程的一种技术。通过ho...
    Nice_cheo阅读 1,294评论 0 0
  • C 静态: 系统的C函数存在着动态的部分!!,本来C语言是静态的所以fishHook是无法Hook的,但是由于系统...
    Code_人生阅读 820评论 0 5
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,515评论 16 22