通过lldb来说符号表绑定与fishhook

fishhook,facebook开源的一个可以动态绑定Mach-O符号表的库。在程序启动时与运行时会通过dyld来绑定符号表(这里有非懒加载与懒加载之分),而fishhook可以修改符号表的绑定。


验证

  • 非懒加载绑定
  • 懒加载绑定
非懒加载绑定

先来说说非懒加载绑定,我们最熟悉的objc_msgSend就是非懒加载绑定。新建一个空的命令行项目:

int main(int argc, const char * argv[]) {
    NSLog(@"Hello, World!");
    return 0;
}

NSLog(@"Hello, World!")处打上断点,并在Debug Workflow中选中Always Show Disassembly,运行程序后执行如下操作:

image.png

可以看到slide的值为0,似乎命令行项目与iOS不同,并没有使用ASLR技术。

image.png

通过MachOView可以看到,objc_msgSend在MachO中的偏移量为0x2008,而由于slide为0,所以这里的虚拟地址0x100002008就是程序运行后的真实地址,在控制台中继续执行如下操作:

image.png
  1. 通过image lookup -a可以看到0x100002008地址对应的确实是objc_msgSend,并且得到函数指针0x00007fff6b167040,它存储在Test.__DATA_CONST.__got
  2. 通过image lookup -n得到objc_msgSend所在动态库libobjc.A.dylib及偏移量0x0000000000006040
  3. 通过image list -o -f得到libobjc.A.dylib动态库首地址0x00007fff6b161000
  4. libobjc.A.dylib动态库首地址加上objc_msgSend偏移量得到objc_msgSend存储地址0x00007fff6b167040

这与用image lookup -a命令看到的地址完全吻合。

懒加载绑定
image.png

NSLog的偏移量为0x3000,同理,由于slide为0,这里的虚拟地址0x100003000就是真实地址。

image.png

可以看到NSLog符号存储在Test.__DATA.__la_symbol_ptr中,但是这里得到的地址0x0000000100001eac与真实的NSLog存储地址0x00007fff37702332并不相同,显然在0x0000000100001eac处必定进行了绑定操作。

0x0000000100001eac处打上断点,继续执行程序,得到如下所示汇编代码:

image.png

这里会跳转到0x100001e9c处,继续加断点跟进:

image.png

这里调用了dyld_stub_binderNSLog的符号绑定就是通过这个函数完成的,继续跟进并在dyld_stub_binder对应的汇编代码末尾处打上断点,继续运行程序后做如下操作:

image.png
  1. 此时r11存储的值为0x00007fff37702332
  2. 通过image lookup -a得到这个值就是NSLog的存储地址
  3. 再次查看0x100003000处信息,此时已经变成NSLog(之前为(void *)0x0000000100001eac

以上就是符号的非懒加载绑定与懒加载绑定,顺便说一句,懒加载绑定后已经存在符号到函数的映射信息,再次调用相同函数时不存在这个绑定过程,而是直接调用。

fishhook

  • fishhook的简单使用
  • fishhook源码解析
fishhook的简单使用

接着上文的NSLog继续说:

void (*sys_log)(NSString *format, ...);

void my_log(NSString *format, ...) {
    sys_log([format stringByAppendingFormat:@"---hello fishhook---"]);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"before");
        
        struct rebinding context;
        context.name = "NSLog";
        context.replacement = &my_log;
        context.replaced = (void *)&sys_log;
        struct rebinding contexts[1] = {context};
        rebind_symbols(contexts, 1);
        
        NSLog(@"after");
    }
    return 0;
}

运行程序后输出如下:

image.png

显然已经成功的hook了NSLog函数,下面来看fishhook是怎么做到的。

rebind_symbols(contexts, 1)NSLog(@"after")处打上断点,运行程序并作如下操作:

image.png

由于在此之前已经调用过NSLog,所以懒加载符号绑定已经完成,此时0x100003000对应的就是NSLog的函数指针。继续运行程序执行rebind_symbols,在此查看0x100003000,此时此时绑定的地址变成了自定义的my_log函数,所以当再次调用NSLog时,通过符号表找到的是my_log,从而实现对NSLog的hook

fishhook源码解析

先了解一下数据结构:

struct rebinding {
  const char *name;    
  void *replacement;
  void **replaced;
};

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

static struct rebindings_entry *_rebindings_head;

接着看Demo中调用的关键函数rebind_symbols

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 this was the first call, register callback for image additions (which is also invoked for
  // existing images, otherwise, just run on existing images
  if (!_rebindings_head->next) {
    _dyld_register_func_for_add_image(_rebind_symbols_for_image);
  } else {
    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;
}

这里首先调用了prepend_rebindings,这个函数是整个数据结构的关键:

prepend_rebindings
static int prepend_rebindings(struct rebindings_entry **rebindings_head,
                              struct rebinding rebindings[],
                              size_t nel) {
  struct rebindings_entry *new_entry = (struct rebindings_entry *) malloc(sizeof(struct rebindings_entry));
  if (!new_entry) {
    return -1;
  }
  new_entry->rebindings = (struct rebinding *) 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;
}

这里生成一个新的结构体指针new_entry,将rebindingsnel扔到结构体对应的成员变量中,并将next指针指向上文提到的_rebindings_head处,而此时的_rebindings_head值为NULL,最后又将new_entry赋值给_rebindings_head

显然,如果rebind_symbols函数调用多次,最终_rebindings_head是个单链表,prepend_rebindings函数的作用就是将新的结构体指针添加到链表头部,而这个单链表的尾部是NULL

当然,这里涉及到一些内存申请操作,如果申请失败返回-1

继续看prepend_rebindings函数之后的代码:

  if (!_rebindings_head->next) {
    _dyld_register_func_for_add_image(_rebind_symbols_for_image);
  } else {
    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));
    }
  }
 

如果_rebindings_head->nextNULL也就是说rebind_symbols函数是首次调用,此时注册image的回调函数_rebind_symbols_for_image

_dyld_register_func_for_add_image

这个函数是dyld提供给我们的镜像加载回调函数,对应注释如下:

/*
* 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及有新镜像加载时,这个注册的回调函数会被每个已加载的镜像回调。

_rebind_symbols_for_image
static void _rebind_symbols_for_image(const struct mach_header *header,
                                      intptr_t slide) {
    rebind_symbols_for_image(_rebindings_head, header, slide);
}

回调函数直接调用rebind_symbols_for_image

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

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

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

  ...
}

先通过dladdr函数查看是否可以加载到mach_header *对应的info,如果加载不到,直接return。

这里说一下Dl_info的数据结构:

typedef struct dl_info {
        const char      *dli_fname;     /* Pathname of shared object */
        void            *dli_fbase;     /* Base address of shared object */
        const char      *dli_sname;     /* Name of nearest symbol */
        void            *dli_saddr;     /* Address of nearest symbol */
} Dl_info;

比如,Demo对应的info信息如下:

image.png

接着定义了4个结构体指针,以64位架构为例:

  1. segment_command_t就是segment_command_64
  2. symtab_commandLoad CommandsLC_SYMTAB对应的结构体
  3. dysymtab_commandLoad CommandsLC_DYSYMTAB对应的结构体

然后跳过MachO Header,开始遍历Load Commands,将对应的LC分别放到linkedit_segmentsymtab_cmddysymtab_cmd这三个结构体指针中。

完成循环后对结构体指针判空,如果有满足为空条件的,直接return。

static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
                                     const struct mach_header *header,
                                     intptr_t slide) {
  ...

  // Find base symbol/string table addresses
  uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
  nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
  char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);

  // Get indirect symbol table (array of uint32_t indices into 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) {
      if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
          strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
        continue;
      }
      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);
        }
      }
    }
  }
}
  1. 获取当前镜像SEG_LINKEDITASLR偏移后的首地址
  2. 获取符号表地址
  3. 获取字符串表地址
  4. 获取动态符号表地址
  5. 再次遍历Load Commands找到__DATA__DATA_CONST中对应的__nl_symbol_ptr__la_symbol_ptr进行重新绑定

perform_rebinding_with_section的功能就是,遍历当前符号表是否有与链表中每个结构体指针对应的name字段匹配,如果有则重新绑定。至此。fishhook的rebind就完成了。

再回到rebind_symbols,来看对应的else部分代码:

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 this was the first call, register callback for image additions (which is also invoked for
  // existing images, otherwise, just run on existing images
  if (!_rebindings_head->next) {
    _dyld_register_func_for_add_image(_rebind_symbols_for_image);
  } else {
    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;
}

上文说过,_dyld_register_func_for_add_image只有在调用或者有新镜像加载时才会调用注册的回调函数。对于同一个镜像来说,当rebind_symbols被多次调用时,注册的回调函数_rebind_symbols_for_image已经不会再被调用,此时先获取当前已加载镜像,然后逐个手动调用_rebind_symbols_for_image即可。


Have fun!

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

推荐阅读更多精彩内容

  • 这是我在简书的第32篇文章 文/微辣 01 今天是继六周肚皮舞课之后,最震撼的一天。因为,我们迎来了肚皮舞进修班体...
    四间房阅读 367评论 0 1
  • 每天都要看到你 在我的世界里看到你 这样我才会发现生活的意义 每天都要看到你 在我们的小小世界里 看着就满心欢喜
    王不烦阅读 174评论 0 0
  • 本文准备讲解1个简单的算法编程问题, 这个算法编程问题来自LintCode平台。不了解.LintCode平台的读者...
    billliu_0d62阅读 124评论 2 0
  • 电影于2016年年初开拍,因一些原因,剧本做了大的变动,于近期开拍。 之前拍摄导演拍摄组演员,各界朋友都付出了很多...
    龚雪丽阅读 1,343评论 0 2