前言
最近自己心血來潮,想研究下是否可以完美攔截到 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 鏡像中所有函數的符號和其對應的地址,如下圖所示:
但是當我在真機上運行的時候,一臉懵逼,獲取到的符號大部分是 <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