引用
- Mac OS X ABI Mach-O File Format Reference
前言
fishhook 是什么?
fishhook 是用于 Mach-O 的符号动态绑定的 facebook 开源维护的的第三方库。当 Mach-O 加载第三方库的时候,可以用 fishhook 进行hook。
具体的参考 fishhook 在 GitHub 上的 readme .
原理
readme 上已经说的很清楚了。但是想要搞清楚原理,还有很多信息需要补充。
一句话概括就是:fishhook 找到目标函数地址,然后替换成自己的函数地址,达到 hook 的目的。
Mach-O
让我们来看看 Mach-O 的文件格式。Mach-O 是Mac OS X 上程序的标准格式。
这里有张图,来表示 Mach-O 的基本格式。
Mach-O 文件包含三个主要区域:
- header structure
- load commands
- segment
其中,segment 是一段一段的。
每个 segment 包含 0 个或 多个 section。每个 section 包含 code 或者 特殊类型的 data。每个 segment 定义了 一片虚拟内存 ,其中动态链接器映射到程序的地址空间。
Mach-O 在内存中的布局结构:
fishhook 要做的事情就是找到 segment: DATA ,然后找到 section:__la_symbol_ptr。最后,找到其中的目标函数地址,替换目标函数地址为自己的函数地址。
具体分为2个步骤:
- 找到目标函数地址
- 找到函数名,一一比对。成功后,替换目标函数地址为自己的函数地址。
其中,找到函数名的过程,有点曲折。string table 是一个数组,里面包含了函数名。根据偏移 offset 能找到需要的函数名。
分析
进行符号绑定前,我们能获得的信息有:
- mach_header *header,header 结构 指针
- intptr_t slide,ALSR 偏移
接着 header 的是 load commands。 load commans 里面包含了 LG_SEGMENT。 LG_SEGMENT 有4种:
- __PAGEZERO
- __TEXT
- __DATA
- __LINKEDIT
PAGEZERO
是可执行程序的第一个段。总是位于虚拟内存中的最开始的位置。它的大小是根据架构类型来的。在X64里面的,大小是 0x0000000100000000 。
TEXT
里面存储的是 code 和 只读数据。我们这里用不到。
DATA
里面包含了可读写数据。里面的 section header 正是我们需要的信息。最重要的是找到 section header 里的 __nl_symbol_ptr
和 __la_symbol_ptr
LINKEDIT
里面记录了动态连接器的一些信息,fishhook 主要通过它找到基地址,一般得到的地址就是 PAGEZERO
之后的地址,也就是 0x0000000100000000。
LG_SEGMENT(__DATA)
和 LG_SEGMENT(__TEXT)
里面都包含了 section header。我们主要用到 DATA 里的 section header ( 主要是 __nl_symbol_ptr
和 __la_symbol_ptr
)。
LG_SEGMENT(__DATA)
包含如下 section header :
__nl_symbol_ptr
__got
__la_symbol_ptr
__objc_imageinfo
__bss
__nl_symbol_ptr
is an array of pointers to non-lazily bound data (these are bound at the time a library is loaded) and__la_symbol_ptr
is an array of pointers to imported functions that is generally filled by a routine called dyld_stub_binder during the first call to that symbol (it's also possible to tell dyld to bind these at launch)
我们先来看看如何调用 fishhook
static void (*orig_foo)(int);
void my_foo(int x) {
printf("real func: %d\n",x);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
rebind_symbols((struct rebinding[1]){"foo",my_foo,(void *)&orig_foo}, 1);
foo(20);
}
return 0;
}
函数内部调用:
- rebind_symbols_image
- _rebind_symbols_for_image
- rebind_symbols_for_image
- perform_rebinding_with_section
rebind_symbols
里大体上做了如下操作:
- 开辟链表
rebindings_entry
空间,并填入信息 - 判断是不是第一次调用。第一次调用的话,对添加的 image 注册回调:
_dyld_register_func_for_add_image(_rebind_symbols_for_image);
- 非第一次的话,在已有的 image 里调用
_rebind_symbols_for_image
_rebind_symbols_for_image
里只是调用: rebind_symbols_for_image
。
rebind_symbols_for_image
rebind_symbols_for_image 大体上做如下动作:
- 在 load commands 里找到 linkedit_segment、symtab_cmd、dysymtab_cmd
- 通过 linkedit_segment 和 symtab_cmd 找到 symtab、strtab
- 在 load commands 里找到
__DATA,__la_symbol_ptr
和__DATA,__nl_symbol_ptr
- 调用 perform_rebinding_with_section
查找 linkedit_segment、symtab_cmd、dysymtab_cmd。在 MachOView 里,查看事例程序 TestMacOS(参见下面链接)的内存布局:
mach_header_64
struct mach_header_64
{
uint32_t magic;
cpu_type_t cputype;
cpu_subtype_t cpusubtype;
uint32_t filetype;
uint32_t ncmds;
uint32_t sizeofcmds;
uint32_t flags;
uint32_t reserved;
};
- ncmds
- load commands 的数量
segment_command_64
struct segment_command_64
{
uint32_t cmd;
uint32_t cmdsize;
char segname[16];
uint64_t vmaddr;
uint64_t vmsize;
uint64_t fileoff;
uint64_t filesize;
vm_prot_t maxprot;
vm_prot_t initprot;
uint32_t nsects;
uint32_t flags;
};
- cmd
- 数字从0x1开始,代表不同的 load command
- cmdsize
- 当前 cmd 的大小
- segname
- segment 的名字,大写的。如:
__TEXT
和__DATA
- vmaddr
- 段在虚拟内存的开始地址
- fileoff
- vmaddr 的偏移
#define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT_64
//获得 load commands 的起始地址
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;
//查找LG_SEGMENT_64->__LINKEDIT
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
linkedit_segment = cur_seg_cmd;
}
}
//查找 LG_SYMTAB
else if (cur_seg_cmd->cmd == LC_SYMTAB) {
symtab_cmd = (struct symtab_command*)cur_seg_cmd;
}
//查找 LG_DYSYMTAB
else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
}
}
找到 linkedit_base
// Find base symbol/string table addresses
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
找到 symtab、strtab、dysymtab_command:
symtab_command
struct symtab_command
{
uint_32 cmd;
uint_32 cmdsize;
uint_32 symoff;
uint_32 nsyms;
uint_32 stroff;
uint_32 strsize;
};
- symoff
- symbol table 的偏移
- stroff
- string table 的偏移
dysymtab_command
struct dysymtab_command
{
uint32_t cmd;
uint32_t cmdsize;
uint32_t ilocalsym;
uint32_t nlocalsym;
uint32_t iextdefsym;
uint32_t nextdefsym;
uint32_t iundefsym;
uint32_t nundefsym;
uint32_t tocoff;
uint32_t ntoc;
uint32_t modtaboff;
uint32_t nmodtab;
uint32_t extrefsymoff;
uint32_t nextrefsyms;
uint32_t indirectsymoff;
uint32_t nindirectsyms;
uint32_t extreloff;
uint32_t nextrel;
uint32_t locreloff;
uint32_t nlocrel;
};
- indirectsymoff
- indirect symbol table 的偏移
nlist_64
struct nlist_64
{
union {
uint32_t n_strx;
} n_un;
uint8_t n_type;
uint8_t n_sect;
uint16_t n_desc;
uint64_t n_value;
};
- n_un
- 函数名在 string table 的 index
//注意:这里的偏移都是基于基地址( linkedit_base )的偏移
nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
获取 section header,并调用 perform_rebinding_with_section:
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);
}
}
}
}
perform_rebinding_with_section
section_64
struct section_64
{
char sectname[16];
char segname[16];
uint64_t addr;
uint64_t size;
uint32_t offset;
uint32_t align;
uint32_t reloff;
uint32_t nreloc;
uint32_t flags;
uint32_t reserved1;
uint32_t reserved2;
};
- addr
- section 的虚拟内存地址,类型是 integer 的
- reserved1
- 对于 symbol pointer sections 和 stubs sections 来说,reserved1 表示 indirect table 数组的 index。用来索引 section's entries.
uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
//找到库函数的地址: section->addr <==> section(__DATA,__la_symbol_ptr)
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
for (uint i = 0; i < section->size / sizeof(void *); i++) {
//查找indirect_symbol_indices数组,获取其中的内容,得到 symtab_index
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;
}
//找到函数名位于 string table 的偏移
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
//找到 函数名
char *symbol_name = strtab + strtab_offset;
if (strnlen(symbol_name, 2) < 2) {
continue;
}
struct rebindings_entry *cur = rebindings;
while (cur) {
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) {
//把函数地址保存到 rebindings.replaced 里
*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
}
//替换内容为自定义函数地址
indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
goto symbol_loop;
}
}
cur = cur->next;
}
symbol_loop:;
}
这里有张图,仿照着 fishhook 的 readme 画的。
附录Demo
Demo