iOS Hook C++ 尝试

前言

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

搜索

一切研究起于搜索,如果有人已经研究出来了,那就不用花费很多时间了,从 Google 到 stackOverflow,再到 gitHub,搜索了 hookc++ 相关的关键词,基本没有找到什么资料,没人能清晰的告诉我,在 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 文件。

  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 真机上获取到的内部函数都是 <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

参考资料

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

推荐阅读更多精彩内容