前言
最近自己心血來潮,想研究下是否可以完美攔截到 WKWebView 的所有網(wǎng)絡(luò)請求,所以就去看下了 WebKit 的源碼,發(fā)現(xiàn)源碼基本都是用 c++ 去實現(xiàn)的,突然就想去研究下能否 hook 私有庫里面c++ 中的函數(shù)。于是就開始了一段學(xué)習(xí)之旅。
搜索
一切研究起于搜索,如果有人已經(jīng)研究出來了,那就不用花費很多時間了,從 Google 到 stackOverflow,再到 gitHub,搜索了 hook、 c++ 相關(guān)的關(guān)鍵詞,基本沒有找到什么資料,沒人能清晰的告訴我,在 iOS 中究竟能不能 hook c++ 方法。
探索
方案尋找
在搜索沒有找到有用資料時,我是有點懵逼的,因為不知如何下手(之前對 Mach-O 的文件格式基本沒深入了解)。之前知道 fishhook 可以 hook c 函數(shù),因此就想能不能也用 fishhook 來 hook 私有庫里面 c++ 函數(shù),當(dāng)時的嘗試是失敗了。后來在一個研究逆向的同事的幫助下,了解到了可以使用 hookzz 這個庫去 hook c/c++ 函數(shù)。具體 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 個參數(shù),第一個是被 hook 函數(shù)的函數(shù)地址,第二個參數(shù)是用來替代原函數(shù)的函數(shù)地址,第三參數(shù)是函數(shù)指針的指針,用于存儲原函數(shù)的函數(shù)指針。
由于第二個和第三個參數(shù)都只自己創(chuàng)建的,所以現(xiàn)在的問題是,如何找到 hook 函數(shù)的函數(shù)地址。只要可以找到函數(shù)地址,就能夠用 hookzz 進(jìn)行 hook。
被 hook 函數(shù)地址尋找
那么,如何尋找一個函數(shù)的函數(shù)指針呢?這里就需要了解下 iOS 的 dyld 的文件格式 -- Mach-O。在 iOS 系統(tǒng)中,所有的 dyld 都 Mach-O 格式(具體什么是 Mach-O,可以上網(wǎng)搜索下,網(wǎng)上有很多大神發(fā)了很多解析文章),在 Mach-O 中,有一個符號表(Symbol Table)是專門存儲代碼的中所有符號和符號對應(yīng)地址。而函數(shù)名稱也是符號一種,所以也可以在符號表中直接找到。我們直接用 MachOView 工具,可以查看 dyld 文件。
- 獲取 WebKit 的 dyld 文件,為了方便,我們直接拿 mac 系統(tǒng)中的 WebKit 庫,在文件目錄
/System/Library/Frameworks中可以找到,如下圖:

- 直接用 MachOView 工具打開 WebKit framework 中的 WebKit 文件,直接將左邊的滾動欄拉到最下面,就可以看到 Symbol Table,如下圖所示:

上圖右邊的第一紅框標(biāo)出的,就是 c++ 函數(shù)的符號,會發(fā)現(xiàn)和我們平時接觸到的 c++ 函數(shù)的定義不太一樣,這是因為相比于 c 函數(shù), c++ 的實體定義較為復(fù)雜,所以區(qū)分不同的實體,編譯器會對 c++ 實體進(jìn)行 mangle 操作,從而保證了程序?qū)嶓w名稱的唯一性。我們可以通過 c++filt 工具進(jìn)行 demangle 操作 (GCC and MSVC C++ Demangler
這個網(wǎng)站突然打不開了,該網(wǎng)站也支持 demangle c++ 函數(shù))如下圖所示

可以看到,將符號 __ZNK7WebCore30MediaDevicesEnumerationRequest23userMediaDocumentOriginEv 進(jìn)行 demangle 操作后,能到獲取到 WebCore::MediaDevicesEnumerationRequest::userMediaDocumentOrigin() const 函數(shù)名稱。
代碼實現(xiàn)
上面我們已經(jīng)分析了如何獲取到函數(shù)函數(shù)地址,接下來就是如何用代碼獲取到符號表,這里需要對 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 ,找到符號表對應(yīng)的 segment,同時也一起獲取到 _TEXT 和 _LINKEDIT 的 segment
// 遍歷鏡像里面的所有 segment
void _enumerate_segment(const mach_header *header, std::function<bool(struct load_command *)> func) {
// 這里我們只考慮64位應(yīng)用。第一個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 需要區(qū)分下 iOS 系統(tǒng)和 MacOS 系統(tǒng),在 iOS 系統(tǒng)中,直接 demangle 是會返回 status = 4,也就是格式不符合,經(jīng)過試驗后,發(fā)現(xiàn)在 iOS 系統(tǒng)上,只要將字符中開頭的 __Z 修改為 _Z 后,便可以 demangle 成功,具體原因我也不清楚。
當(dāng)我以為自己已經(jīng)快要成功時,現(xiàn)實潑我一桶冷水。由于之前測試都是在模擬器,所以在可以打印出 WebKit 鏡像中所有函數(shù)的符號和其對應(yīng)的地址,如下圖所示:

但是當(dāng)我在真機上運行的時候,一臉懵逼,獲取到的符號大部分是 <redacted>,只有部分地址解析出來了,而解析出來部分的符號對應(yīng)的地址是 0x0。如下圖所示:

經(jīng)過分析后,發(fā)現(xiàn)在真機中,編譯器應(yīng)該做了下面的優(yōu)化處理(純屬個人猜測)
- 對于 dyld 中的內(nèi)部函數(shù)對應(yīng)的符號,都可以地址化(去符號化),因為符號是給人閱讀的,對于機器來說一個二進(jìn)制地址就夠了。而且也可以有效的減少內(nèi)存中 dyld 的體積。
- 對于 dyld 中暴露出來的函數(shù),可以在符號表中獲取到符號和在 dyld 中的偏移值,因為這些函數(shù)需要給外部調(diào)用,所以不能地址化。
- 對于 dyld 中引用的第三方庫中的函數(shù),不會被地址化,但是由于是外部符號,所以需要進(jìn)行重定向才能獲取到真正的地址。
總結(jié)
經(jīng)過自己的研究后,發(fā)現(xiàn)在真機中,可能真的沒有什么方法可以 hook c++ 中的私有方法。如果只是調(diào)試使用,我們可以直接在 mac 上用 MachOView 或 Hooper 來獲取到私有函數(shù)的在對應(yīng) dyld 中的偏移值,然后直接在代碼中用偏移中進(jìn)行 hook 操作。但是想在應(yīng)用中直接通過函數(shù)名稱去 hook dyld 中內(nèi)部私有方法應(yīng)該是沒有辦法的(至少我現(xiàn)在想不出來)。
如果想 hook 私有庫中的共有方法,應(yīng)該是可以實現(xiàn)的??梢灾苯有薷?fishhook 的源碼,在外部符號匹配時,對從 dyld 符號表取到的符號進(jìn)行 demangle 操作,然后再進(jìn)行比較,因為 c 和 c++ 的唯一區(qū)別,就是存儲在符號表中的符號有沒有經(jīng)過一層 demangle 操作。所以只要去除這個區(qū)別,可以把 c++ 的 hook 和 c 等同起來。
ps: 相同的代碼,在 iOS 真機上獲取到的內(nèi)部函數(shù)都是 <redacted>,但是在 Mac 或 iOS 模擬器上可以解析出來。在這個過程中,為了探索是否是 iOS 中內(nèi)置的 dyld 和 Mac 中的不一致,我也從一臺越獄手機中拉取了 iOS 中的共享緩存 dyld_shared_cache_arm64,從共享緩存中抽出 WebKit 庫后,發(fā)現(xiàn)和 Mac 上的并沒有什么區(qū)別。
2019 年 10 月 14 日修改
經(jīng)過研究后發(fā)現(xiàn),hookzz 是無法用于 inline hook 的,所以在非越獄機器上,暫時沒有方法 hook C++ 函數(shù)
使用 HookZz 替換 mach_msg 方法程序崩潰
嘗試使用 fishhook 來 hook 系統(tǒng)的 mach_msg,從而接管整個進(jìn)程的實驗也失敗了。
原因是:由于 fishhook 雖然只能 hook 到部分 mach_msg,對于 WebKit 中被調(diào)用的 mach_msg,無法 hook ,具體原因可以查看下 iOSer 上的討論鏈接 Fishhook 是否無法 hook 到所有的 mach_msg
參考資料
- Mach-O 可執(zhí)行文件
- 探秘 Mach-O 文件
- iOS逆向基礎(chǔ)Mach-O文件(1)
- 趣探 Mach-O:文件格式分析
- 動態(tài)修改 C 語言函數(shù)的實現(xiàn)
- 巧用符號表 - 探求 fishhook 原理(一)
- Hook 原理之 fishhook 源碼解析
- HookZz
- dyld詳解
- iOS 逆向----一鍵砸殼工具frida-ios-dump
- monkeyDev
- iOS 逆向----從越獄的手機中提取App Store下載的APP
- frida-ios-dump
- iOS逆向----SSH連接越獄iPhone
- iproxy-通過USB使用SSH連接iOS設(shè)備