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閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容