前言
最近自己心血来潮,想研究下是否可以完美拦截到 WKWebView
的所有网络请求,所以就去看下了 WebKit
的源码,发现源码基本都是用 c++
去实现的,突然就想去研究下能否 hook 私有库里面c++
中的函数。于是就开始了一段学习之旅。
搜索
一切研究起于搜索,如果有人已经研究出来了,那就不用花费很多时间了,从 Google 到 stackOverflow,再到 gitHub,搜索了 hook
、 c++
相关的关键词,基本没有找到什么资料,没人能清晰的告诉我,在 iOS 中究竟能不能 hook c++ 方法。
探索
方案寻找
在搜索没有找到有用资料时,我是有点懵逼的,因为不知如何下手(之前对 Mach-O 的文件格式基本没深入了解)。之前知道 fishhook 可以 hook c 函数,因此就想能不能也用 fishhook 来 hook 私有库里面 c++ 函数,当时的尝试是失败了。后来在一个研究逆向的同事的帮助下,了解到了可以使用 hookzz 这个库去 hook c/c++ 函数。具体 hookzz 的原理还没有去了解,使用方法如下所示:
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。
被 hook 函数地址寻找
那么,如何寻找一个函数的函数指针呢?这里就需要了解下 iOS 的 dyld 的文件格式 -- Mach-O。在 iOS 系统中,所有的 dyld 都 Mach-O 格式(具体什么是 Mach-O,可以上网搜索下,网上有很多大神发了很多解析文章),在 Mach-O 中,有一个符号表(Symbol Table)是专门存储代码的中所有符号和符号对应地址。而函数名称也是符号一种,所以也可以在符号表中直接找到。我们直接用 MachOView 工具,可以查看 dyld 文件。
- 获取 WebKit 的 dyld 文件,为了方便,我们直接拿 mac 系统中的 WebKit 库,在文件目录
/System/Library/Frameworks
中可以找到,如下图:
- 直接用 MachOView 工具打开 WebKit framework 中的 WebKit 文件,直接将左边的滚动栏拉到最下面,就可以看到 Symbol Table,如下图所示:
上图右边的第一红框标出的,就是 c++ 函数的符号,会发现和我们平时接触到的 c++
函数的定义不太一样,这是因为相比于 c 函数, c++ 的实体定义较为复杂,所以区分不同的实体,编译器会对 c++
实体进行 mangle 操作,从而保证了程序实体名称的唯一性。我们可以通过 c++filt
工具进行 demangle 操作 (GCC and MSVC C++ Demangler
这个网站突然打不开了,该网站也支持 demangle c++ 函数)如下图所示
可以看到,将符号 __ZNK7WebCore30MediaDevicesEnumerationRequest23userMediaDocumentOriginEv
进行 demangle 操作后,能到获取到 WebCore::MediaDevicesEnumerationRequest::userMediaDocumentOrigin() const
函数名称。
代码实现
上面我们已经分析了如何获取到函数函数地址,接下来就是如何用代码获取到符号表,这里需要对 Mach-O 文件格式有一定的了解
- 获取到 WebKit dyld 的镜像地址,代码如下:
- (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 ,找到符号表对应的 segment,同时也一起获取到 _TEXT 和 _LINKEDIT 的 segment
// 遍历镜像里面的所有 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));
}
- demangle c++ 符号
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 成功,具体原因我也不清楚。
当我以为自己已经快要成功时,现实泼我一桶冷水。由于之前测试都是在模拟器,所以在可以打印出 WebKit 镜像中所有函数的符号和其对应的地址,如下图所示:
符号表模拟器运行结构.png但是当我在真机上运行的时候,一脸懵逼,获取到的符号大部分是 <redacted>
,只有部分地址解析出来了,而解析出来部分的符号对应的地址是 0x0
。如下图所示:
经过分析后,发现在真机中,编译器应该做了下面的优化处理(纯属个人猜测)
- 对于 dyld 中的内部函数对应的符号,都可以地址化(去符号化),因为符号是给人阅读的,对于机器来说一个二进制地址就够了。而且也可以有效的减少内存中 dyld 的体积。
- 对于 dyld 中暴露出来的函数,可以在符号表中获取到符号和在 dyld 中的偏移值,因为这些函数需要给外部调用,所以不能地址化。
- 对于 dyld 中引用的第三方库中的函数,不会被地址化,但是由于是外部符号,所以需要进行重定向才能获取到真正的地址。
总结
经过自己的研究后,发现在真机中,可能真的没有什么方法可以 hook c++ 中的私有方法。如果只是调试使用,我们可以直接在 mac 上用 MachOView 或 Hooper 来获取到私有函数的在对应 dyld 中的偏移值,然后直接在代码中用偏移中进行 hook 操作。但是想在应用中直接通过函数名称去 hook dyld 中内部私有方法应该是没有办法的(至少我现在想不出来)。
如果想 hook 私有库中的共有方法,应该是可以实现的。可以直接修改 fishhook 的源码,在外部符号匹配时,对从 dyld 符号表取到的符号进行 demangle 操作,然后再进行比较,因为 c 和 c++ 的唯一区别,就是存储在符号表中的符号有没有经过一层 demangle 操作。所以只要去除这个区别,可以把 c++ 的 hook 和 c 等同起来。
ps: 相同的代码,在 iOS 真机上获取到的内部函数都是 <redacted>,但是在 Mac 或 iOS 模拟器上可以解析出来。在这个过程中,为了探索是否是 iOS 中内置的 dyld 和 Mac 中的不一致,我也从一台越狱手机中拉取了 iOS 中的共享缓存 dyld_shared_cache_arm64,从共享缓存中抽出 WebKit 库后,发现和 Mac 上的并没有什么区别。
2019 年 10 月 14 日修改
经过研究后发现,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
网友评论