iOS hook C++ 嘗試

前言

最近本身心血來潮,想研究下是否能夠完美攔截到 WKWebView 的全部網絡請求,因此就去看下了 WebKit 的源碼,發現源碼基本都是用 c++ 去實現的,忽然就想去研究下可否 hook 私有庫裏面c++ 中的函數。因而就開始了一段學習之旅。html

搜索

一切研究起於搜索,若是有人已經研究出來了,那就不用花費不少時間了,從 Google 到 stackOverflow,再到 gitHub,搜索了 hookc++ 相關的關鍵詞,基本沒有找到什麼資料,沒人能清晰的告訴我,在 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

被 hook 函數地址尋找

那麼,如何尋找一個函數的函數指針呢?這裏就須要瞭解下 iOS 的 dyld 的文件格式 -- Mach-O。在 iOS 系統中,全部的 dyld 都 Mach-O 格式(具體什麼是 Mach-O,能夠上網搜索下,網上有不少大神發了不少解析文章),在 Mach-O 中,有一個符號表(Symbol Table)是專門存儲代碼的中全部符號和符號對應地址。而函數名稱也是符號一種,因此也能夠在符號表中直接找到。咱們直接用 MachOView 工具,能夠查看 dyld 文件。github

  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 函數名稱。markdown

代碼實現

上面咱們已經分析瞭如何獲取到函數函數地址,接下來就是如何用代碼獲取到符號表,這裏須要對 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 成功,具體緣由我也不清楚。ssh

當我覺得本身已經快要成功時,現實潑我一桶冷水。因爲以前測試都是在模擬器,因此在能夠打印出 WebKit 鏡像中全部函數的符號和其對應的地址,以下圖所示:ide

符號表模擬器運行結構.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

參考資料

相關文章
相關標籤/搜索