最近本身心血来潮,想研究下是否能够完美拦截到 WKWebView
的全部网络请求,因此就去看下了 WebKit
的源码,发现源码基本都是用 c++
去实现的,忽然就想去研究下可否 hook 私有库里面c++
中的函数。因而就开始了一段学习之旅。html
一切研究起于搜索,若是有人已经研究出来了,那就不用花费不少时间了,从 Google 到 stackOverflow,再到 gitHub,搜索了 hook
、 c++
相关的关键词,基本没有找到什么资料,没人能清晰的告诉我,在 iOS 中究竟能不能 hook c++ 方法。ios
在搜索没有找到有用资料时,我是有点懵逼的,由于不知如何下手(以前对 Mach-O 的文件格式基本没深刻了解)。以前知道 fishhook 能够 hook c 函数,所以就想能不能也用 fishhook 来 hook 私有库里面 c++ 函数(体现了我对 fishhook 实现原理无知),当时的尝试是失败了。后来在一个研究逆向的同事的帮助下,了解到了可使用 hookzz 这个库去 hook c/c++ 函数。具体 hookzz 的原理尚未去了解,使用方法以下所示:c++
extern "C" { extern int ZzReplace(void *function_address, void *replace_call, void **origin_call); } size_t (*origin_fread)(void * ptr, size_t size, size_t nitems, FILE * stream); size_t (fake_fread)(void * ptr, size_t size, size_t nitems, FILE * stream) { // Do What you Want. return origin_fread(ptr, size, nitems, stream); } void hook_fread() { ZzReplace((void *)fread, (void *)fake_fread, (void **)&origin_fread); } 复制代码
ZzReplace
的一共须要传入 3 个参数,第一个是被 hook 函数的函数地址,第二个参数是用来替代原函数的函数地址,第三参数是函数指针的指针,用于存储原函数的函数指针。 因为第二个和第三个参数都只本身建立的,因此如今的问题是,如何找到 hook 函数的函数地址。只要能够找到函数地址,就可以用 hookzz 进行 hook。git
那么,如何寻找一个函数的函数指针呢?这里就须要了解下 iOS 的 dyld 的文件格式 -- Mach-O。在 iOS 系统中,全部的 dyld 都 Mach-O 格式(具体什么是 Mach-O,能够上网搜索下,网上有不少大神发了不少解析文章),在 Mach-O 中,有一个符号表(Symbol Table)是专门存储代码的中全部符号和符号对应地址。而函数名称也是符号一种,因此也能够在符号表中直接找到。咱们直接用 MachOView 工具,能够查看 dyld 文件。github
/System/Library/Frameworks
中能够找到,以下图:上图右边的第一红框标出的,就是 c++ 函数的符号,会发现和咱们平时接触到的 c++
函数的定义不太同样,这是由于相比于 c 函数, c++ 的实体定义较为复杂,因此区分不一样的实体,编译器会对 c++
实体进行 mangle 操做,从而保证了程序实体名称的惟一性。咱们能够经过 c++filt
工具进行 demangle 操做 (GCC and MSVC C++ Demangler 这个网站忽然打不开了,该网站也支持 demangle c++ 函数)以下图所示缓存
能够看到,将符号 __ZNK7WebCore30MediaDevicesEnumerationRequest23userMediaDocumentOriginEv
进行 demangle 操做后,能到获取到 WebCore::MediaDevicesEnumerationRequest::userMediaDocumentOrigin() const
函数名称。markdown
上面咱们已经分析了如何获取到函数函数地址,接下来就是如何用代码获取到符号表,这里须要对 Mach-O 文件格式有必定的了解网络
- (void*)findDyldImageWithName:(NSString *)targetName { int count = _dyld_image_count(); for (int i = 0; i < count; i++) { const char* name = _dyld_get_image_name(i); if(strstr(name, [targetName cStringUsingEncoding:NSUTF8StringEncoding]) > 0) { return (void*)_dyld_get_image_header(i); } } return NULL; } 复制代码
// 遍历镜像里面的全部 segment void _enumerate_segment(const mach_header *header, std::function<bool(struct load_command *)> func) { // 这里咱们只考虑64位应用。第一个command从header的下一位开始 struct load_command *baseCommand = (struct load_command *)((struct mach_header_64 *)header + 1); if (baseCommand == nullptr) return; struct load_command *command = baseCommand; for (int i = 0; i < header->ncmds; i++) { if (func(command)) { return; } command = (struct load_command *)((uintptr_t)command + command->cmdsize); } } void _log_dyld_all_symbol(char *dyld_name) { const struct mach_header *header = NULL; uint64_t slide; int count = _dyld_image_count(); // 获取到 WebKit 镜像的 header 和 slide 大小 for (int i = 0; i < count; i++) { const char* name = _dyld_get_image_name(i); if(strstr(name, dyld_name) > (char *)0) { header = _dyld_get_image_header(i); slide = _dyld_get_image_vmaddr_slide(i); break; } } segment_command_64 *seg_linkedit = NULL; segment_command_64 *seg_text = NULL; struct symtab_command *symtab_command = NULL; // 遍历 load_command,获取到 _LINKEDIT segment,_TEXT segment, 和 符号表的 load_commond _enumerate_segment(header, [&](struct load_command *command) { if (command->cmd == LC_SEGMENT_64) { struct segment_command_64 *segCmd = (struct segment_command_64 *)command; if (0 == strcmp((segCmd)->segname, SEG_LINKEDIT)) seg_linkedit = segCmd; else if (0 == strcmp((segCmd)->segname, SEG_TEXT)) seg_text = segCmd; } else if (command->cmd == LC_SYMTAB) { symtab_command = (struct symtab_command *)command; } return false; }); //......... } 复制代码
// 获取到 _LINKEDIT segment 的首地址 uintptr_t linkedit_addr = (uintptr_t)seg_linkedit->vmaddr -(uintptr_t)seg_text->vmaddr - (uintptr_t)seg_linkedit->fileoff; // 获取到符号表的首地址 struct nlist_64 *nlist = (struct nlist_64 *)((uintptr_t)header + (uintptr_t)symtab_command->symoff + linkedit_addr); // 获取到字符表的首地址 intptr_t string_table = (intptr_t)header + ((uintptr_t)symtab_command->stroff + (uintptr_t)linkedit_addr); 复制代码
// 遍历打印出全部的符号 for (int i = 0; i < symtab_command->nsyms ; i++) { char * symbol_name = (char *)(string_table + nlist->n_un.n_strx); char * demangle_symbol = _demangle_symbol(symbol_name); printf("symbol name: %s\n", demangle_symbol); nlist = (struct nlist_64 *)((uintptr_t)nlist + sizeof(struct nlist_64)); } 复制代码
char * _demangle_symbol(char* mangle_symbol) { size_t str_len = strlen(mangle_symbol); if (str_len < 3) { return mangle_symbol; } if (PLATFORM_IOS) { if (strstr(mangle_symbol, "__Z") == mangle_symbol) { char *new_mangle_symbol = mangle_symbol + 1; int status; char *demangle_symbol = abi::__cxa_demangle (new_mangle_symbol, nullptr, 0, &status); return status == 0 ? demangle_symbol : mangle_symbol; } } else { int status; char *demangle_symbol = abi::__cxa_demangle (mangle_symbol, nullptr, 0, &status); return status == 0 ? demangle_symbol : mangle_symbol; } return mangle_symbol; } 复制代码
这里的 demangle 须要区分下 iOS 系统和 MacOS 系统,在 iOS 系统中,直接 demangle 是会返回 status = 4,也就是格式不符合,通过试验后,发如今 iOS 系统上,只要将字符中开头的 __Z
修改成 _Z
后,即可以 demangle 成功,具体缘由我也不清楚。ssh
当我觉得本身已经快要成功时,现实泼我一桶冷水。因为以前测试都是在模拟器,因此在能够打印出 WebKit 镜像中全部函数的符号和其对应的地址,以下图所示:ide
可是当我在真机上运行的时候,一脸懵逼,获取到的符号大部分是 <redacted>
,只有部分地址解析出来了,而解析出来部分的符号对应的地址是 0x0
。以下图所示:
通过分析后,发如今真机中,编译器应该作了下面的优化处理(纯属我的猜想)
通过本身的研究后,发如今真机中,可能真的没有什么方法能够 hook c++ 中的私有方法。若是只是调试使用,咱们能够直接在 mac 上用 MachOView 或 Hooper 来获取到私有函数的在对应 dyld 中的偏移值,而后直接在代码中用偏移中进行 hook 操做。可是想在应用中直接经过函数名称去 hook dyld 中内部私有方法应该是没有办法的(至少我如今想不出来)。
若是想 hook 私有库中的公有方法,应该是能够实现的。能够直接修改 fishhook 的源码,在外部符号匹配时,对从 dyld 符号表取到的符号进行 demangle 操做,而后再进行比较,由于 c 和 c++ 的惟一区别,就是存储在符号表中的符号有没有通过一层 demangle 操做。因此只要去除这个区别,能够把 c++ 的 hook 和 c 等同起来。
ps: 相同的代码,在 iOS 真机上获取到的内部函数都是 ,可是在 Mac 或 iOS 模拟器上能够解析出来。在这个过程当中,为了探索是不是 iOS 中内置的 dyld 和 Mac 中的不一致,我也从一台越狱手机中拉取了 iOS 中的共享缓存 dyld_shared_cache_arm64,从共享缓存中抽出 WebKit 库后,发现和 Mac 上的并无什么区别。
通过研究后发现,hookzz 是没法用于 inline hook 的,因此在非越狱机器上,暂时没有方法 hook C++ 函数 使用 HookZz 替换 mach_msg 方法程序崩溃
尝试使用 fishhook 来 hook 系统的 mach_msg,从而接管整个进程通信的实验也失败了。 缘由是:因为 fishhook 虽然只能 hook 到部分 mach_msg
,对于 WebKit 中被调用的 mach_msg
,没法 hook ,具体缘由能够查看下 iOSer 上的讨论连接 Fishhook 是否没法 hook 到全部的 mach_msg