iOS hook C++ 尝试

4,933 阅读8分钟

前言

最近自己心血来潮,想研究下是否可以完美拦截到 WKWebView 的所有网络请求,所以就去看下了 WebKit 的源码,发现源码基本都是用 c++ 去实现的,突然就想去研究下能否 hook 私有库里面c++ 中的函数。于是就开始了一段学习之旅。

搜索

一切研究起于搜索,如果有人已经研究出来了,那就不用花费很多时间了,从 Google 到 stackOverflow,再到 gitHub,搜索了 hookc++ 相关的关键词,基本没有找到什么资料,没人能清晰的告诉我,在 iOS 中究竟能不能 hook c++ 方法。

探索

方案寻找

在搜索没有找到有用资料时,我是有点懵逼的,因为不知如何下手(之前对 Mach-O 的文件格式基本没深入了解)。之前知道 fishhook 可以 hook c 函数,因此就想能不能也用 fishhook 来 hook 私有库里面 c++ 函数(体现了我对 fishhook 实现原理无知),当时的尝试是失败了。后来在一个研究逆向的同事的帮助下,了解到了可以使用 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 文件。

  1. 获取 WebKit 的 dyld 文件,为了方便,我们直接拿 mac 系统中的 WebKit 库,在文件目录 /System/Library/Frameworks 中可以找到,如下图:

WX20190909-110612.png

  1. 直接用 MachOView 工具打开 WebKit framework 中的 WebKit 文件,直接将左边的滚动栏拉到最下面,就可以看到 Symbol Table,如下图所示:

符号表演示.png

上图右边的第一红框标出的,就是 c++ 函数的符号,会发现和我们平时接触到的 c++ 函数的定义不太一样,这是因为相比于 c 函数, c++ 的实体定义较为复杂,所以区分不同的实体,编译器会对 c++ 实体进行 mangle 操作,从而保证了程序实体名称的唯一性。我们可以通过 c++filt 工具进行 demangle 操作 (GCC and MSVC C++ Demangler 这个网站突然打不开了,该网站也支持 demangle c++ 函数)如下图所示

c__filt工具使用.png

可以看到,将符号 __ZNK7WebCore30MediaDevicesEnumerationRequest23userMediaDocumentOriginEv 进行 demangle 操作后,能到获取到 WebCore::MediaDevicesEnumerationRequest::userMediaDocumentOrigin() const 函数名称。

代码实现

上面我们已经分析了如何获取到函数函数地址,接下来就是如何用代码获取到符号表,这里需要对 Mach-O 文件格式有一定的了解

  1. 获取到 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;
}
  1. 遍历镜像中的 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;
    });
    
    //.........
    
}

  1. 计算符号表和字符表的位置

    // 获取到 _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);

  1. 遍历符号表
// 遍历打印出所有的符号
    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));
    }
    
  1. 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。如下图所示:

真机获取符号表.png

经过分析后,发现在真机中,编译器应该做了下面的优化处理(纯属个人猜测)

  1. 对于 dyld 中的内部函数对应的符号,都可以地址化(去符号化),因为符号是给人阅读的,对于机器来说一个二进制地址就够了。而且也可以有效的减少内存中 dyld 的体积。
  2. 对于 dyld 中暴露出来的函数,可以在符号表中获取到符号和在 dyld 中的偏移值,因为这些函数需要给外部调用,所以不能地址化。
  3. 对于 dyld 中引用的第三方库中的函数,不会被地址化,但是由于是外部符号,所以需要进行重定向才能获取到真正的地址。

总结

经过自己的研究后,发现在真机中,可能真的没有什么方法可以 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 上的并没有什么区别。

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

参考资料