最近本身心血來潮,想研究下是否能夠完美攔截到 WKWebView
的全部網絡請求,因此就去看下了 WebKit
的源碼,發現源碼基本都是用 c++
去實現的,忽然就想去研究下可否 hook 私有庫裏面c++
中的函數。因而就開始了一段學習之旅。html
一切研究起於搜索,若是有人已經研究出來了,那就不用花費不少時間了,從 Google 到 stackOverflow,再到 gitHub,搜索了 hook
、 c++
相關的關鍵詞,基本沒有找到什麼資料,沒人能清晰的告訴我,在 iOS 中究竟能不能 hook c++ 方法。ios
在搜索沒有找到有用資料時,我是有點懵逼的,由於不知如何下手(以前對 Mach-O 的文件格式基本沒深刻了解)。以前知道 fishhook 能夠 hook c 函數,所以就想能不能也用 fishhook 來 hook 私有庫裏面 c++ 函數(體現了我對 fishhook 實現原理無知),當時的嘗試是失敗了。後來在一個研究逆向的同事的幫助下,瞭解到了可使用 hookzz 這個庫去 hook c/c++ 函數。具體 hookzz 的原理尚未去了解,使用方法以下所示:c++
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。git
那麼,如何尋找一個函數的函數指針呢?這裏就須要瞭解下 iOS 的 dyld 的文件格式 -- Mach-O。在 iOS 系統中,全部的 dyld 都 Mach-O 格式(具體什麼是 Mach-O,能夠上網搜索下,網上有不少大神發了不少解析文章),在 Mach-O 中,有一個符號表(Symbol Table)是專門存儲代碼的中全部符號和符號對應地址。而函數名稱也是符號一種,因此也能夠在符號表中直接找到。咱們直接用 MachOView 工具,能夠查看 dyld 文件。github
/System/Library/Frameworks
中能夠找到,以下圖:上圖右邊的第一紅框標出的,就是 c++ 函數的符號,會發現和咱們平時接觸到的 c++
函數的定義不太同樣,這是由於相比於 c 函數, c++ 的實體定義較爲複雜,因此區分不一樣的實體,編譯器會對 c++
實體進行 mangle 操做,從而保證了程序實體名稱的惟一性。咱們能夠經過 c++filt
工具進行 demangle 操做 (GCC and MSVC C++ Demangler 這個網站忽然打不開了,該網站也支持 demangle c++ 函數)以下圖所示緩存
能夠看到,將符號 __ZNK7WebCore30MediaDevicesEnumerationRequest23userMediaDocumentOriginEv
進行 demangle 操做後,能到獲取到 WebCore::MediaDevicesEnumerationRequest::userMediaDocumentOrigin() const
函數名稱。markdown
上面咱們已經分析瞭如何獲取到函數函數地址,接下來就是如何用代碼獲取到符號表,這裏須要對 Mach-O 文件格式有必定的瞭解網絡
- (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 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)); } 複製代碼
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 成功,具體緣由我也不清楚。ssh
當我覺得本身已經快要成功時,現實潑我一桶冷水。因爲以前測試都是在模擬器,因此在能夠打印出 WebKit 鏡像中全部函數的符號和其對應的地址,以下圖所示:ide
可是當我在真機上運行的時候,一臉懵逼,獲取到的符號大部分是 <redacted>
,只有部分地址解析出來了,而解析出來部分的符號對應的地址是 0x0
。以下圖所示:
通過分析後,發如今真機中,編譯器應該作了下面的優化處理(純屬我的猜想)
通過本身的研究後,發如今真機中,可能真的沒有什麼方法能夠 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 上的並無什麼區別。
通過研究後發現,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