iOS Hook C++ 嘗試

前言

最近自己心血來(lái)潮贰锁,想研究下是否可以完美攔截到 WKWebView 的所有網(wǎng)絡(luò)請(qǐng)求各谚,所以就去看下了 WebKit 的源碼玫坛,發(fā)現(xiàn)源碼基本都是用 c++ 去實(shí)現(xiàn)的,突然就想去研究下能否 hook 私有庫(kù)里面c++ 中的函數(shù)反惕。于是就開(kāi)始了一段學(xué)習(xí)之旅尝艘。

搜索

一切研究起于搜索,如果有人已經(jīng)研究出來(lái)了姿染,那就不用花費(fèi)很多時(shí)間了背亥,從 Google 到 stackOverflow,再到 gitHub悬赏,搜索了 hook狡汉、 c++ 相關(guān)的關(guān)鍵詞,基本沒(méi)有找到什么資料闽颇,沒(méi)人能清晰的告訴我盾戴,在 iOS 中究竟能不能 hook c++ 方法。

探索

方案尋找

在搜索沒(méi)有找到有用資料時(shí)进萄,我是有點(diǎn)懵逼的捻脖,因?yàn)椴恢绾蜗率郑ㄖ皩?duì) Mach-O 的文件格式基本沒(méi)深入了解)锐峭。之前知道 fishhook 可以 hook c 函數(shù)中鼠,因此就想能不能也用 fishhook 來(lái) hook 私有庫(kù)里面 c++ 函數(shù),當(dāng)時(shí)的嘗試是失敗了沿癞。后來(lái)在一個(gè)研究逆向的同事的幫助下援雇,了解到了可以使用 hookzz 這個(gè)庫(kù)去 hook c/c++ 函數(shù)。具體 hookzz 的原理還沒(méi)有去了解椎扬,使用方法如下所示:

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 個(gè)參數(shù)惫搏,第一個(gè)是被 hook 函數(shù)的函數(shù)地址,第二個(gè)參數(shù)是用來(lái)替代原函數(shù)的函數(shù)地址蚕涤,第三參數(shù)是函數(shù)指針的指針筐赔,用于存儲(chǔ)原函數(shù)的函數(shù)指針。
由于第二個(gè)和第三個(gè)參數(shù)都只自己創(chuàng)建的揖铜,所以現(xiàn)在的問(wèn)題是茴丰,如何找到 hook 函數(shù)的函數(shù)地址。只要可以找到函數(shù)地址天吓,就能夠用 hookzz 進(jìn)行 hook贿肩。

被 hook 函數(shù)地址尋找

那么,如何尋找一個(gè)函數(shù)的函數(shù)指針呢龄寞?這里就需要了解下 iOS 的 dyld 的文件格式 -- Mach-O汰规。在 iOS 系統(tǒng)中,所有的 dyld 都 Mach-O 格式(具體什么是 Mach-O物邑,可以上網(wǎng)搜索下溜哮,網(wǎng)上有很多大神發(fā)了很多解析文章)滔金,在 Mach-O 中,有一個(gè)符號(hào)表(Symbol Table)是專(zhuān)門(mén)存儲(chǔ)代碼的中所有符號(hào)和符號(hào)對(duì)應(yīng)地址茬射。而函數(shù)名稱(chēng)也是符號(hào)一種鹦蠕,所以也可以在符號(hào)表中直接找到。我們直接用 MachOView 工具在抛,可以查看 dyld 文件钟病。

  1. 獲取 WebKit 的 dyld 文件,為了方便刚梭,我們直接拿 mac 系統(tǒng)中的 WebKit 庫(kù)肠阱,在文件目錄 /System/Library/Frameworks 中可以找到,如下圖:
WX20190909-110612.png
  1. 直接用 MachOView 工具打開(kāi) WebKit framework 中的 WebKit 文件朴读,直接將左邊的滾動(dòng)欄拉到最下面屹徘,就可以看到 Symbol Table,如下圖所示:
符號(hào)表演示.png

上圖右邊的第一紅框標(biāo)出的衅金,就是 c++ 函數(shù)的符號(hào)噪伊,會(huì)發(fā)現(xiàn)和我們平時(shí)接觸到的 c++ 函數(shù)的定義不太一樣,這是因?yàn)橄啾扔?c 函數(shù)氮唯, c++ 的實(shí)體定義較為復(fù)雜鉴吹,所以區(qū)分不同的實(shí)體,編譯器會(huì)對(duì) c++ 實(shí)體進(jìn)行 mangle 操作惩琉,從而保證了程序?qū)嶓w名稱(chēng)的唯一性豆励。我們可以通過(guò) c++filt 工具進(jìn)行 demangle 操作 (GCC and MSVC C++ Demangler
這個(gè)網(wǎng)站突然打不開(kāi)了,該網(wǎng)站也支持 demangle c++ 函數(shù))如下圖所示

c__filt工具使用.png

可以看到瞒渠,將符號(hào) __ZNK7WebCore30MediaDevicesEnumerationRequest23userMediaDocumentOriginEv 進(jìn)行 demangle 操作后良蒸,能到獲取到 WebCore::MediaDevicesEnumerationRequest::userMediaDocumentOrigin() const 函數(shù)名稱(chēng)。

代碼實(shí)現(xiàn)

上面我們已經(jīng)分析了如何獲取到函數(shù)函數(shù)地址伍玖,接下來(lái)就是如何用代碼獲取到符號(hào)表嫩痰,這里需要對(duì) 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 窍箍,找到符號(hào)表對(duì)應(yīng)的 segment串纺,同時(shí)也一起獲取到 _TEXT 和 _LINKEDIT 的 segment
// 遍歷鏡像里面的所有 segment
void _enumerate_segment(const mach_header *header, std::function<bool(struct load_command *)> func) {
    // 這里我們只考慮64位應(yīng)用。第一個(gè)command從header的下一位開(kāi)始
    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,  和 符號(hào)表的 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. 計(jì)算符號(hào)表和字符表的位置

    // 獲取到 _LINKEDIT segment 的首地址
    uintptr_t linkedit_addr = (uintptr_t)seg_linkedit->vmaddr -(uintptr_t)seg_text->vmaddr - (uintptr_t)seg_linkedit->fileoff;
    // 獲取到符號(hào)表的首地址
    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. 遍歷符號(hào)表
// 遍歷打印出所有的符號(hào)
    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++ 符號(hào)
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 是會(huì)返回 status = 4五辽,也就是格式不符合,經(jīng)過(guò)試驗(yàn)后外恕,發(fā)現(xiàn)在 iOS 系統(tǒng)上杆逗,只要將字符中開(kāi)頭的 __Z 修改為 _Z 后乡翅,便可以 demangle 成功,具體原因我也不清楚罪郊。

當(dāng)我以為自己已經(jīng)快要成功時(shí)蠕蚜,現(xiàn)實(shí)潑我一桶冷水。由于之前測(cè)試都是在模擬器悔橄,所以在可以打印出 WebKit 鏡像中所有函數(shù)的符號(hào)和其對(duì)應(yīng)的地址靶累,如下圖所示:

符號(hào)表模擬器運(yùn)行結(jié)構(gòu).png

但是當(dāng)我在真機(jī)上運(yùn)行的時(shí)候,一臉懵逼癣疟,獲取到的符號(hào)大部分是 <redacted>挣柬,只有部分地址解析出來(lái)了,而解析出來(lái)部分的符號(hào)對(duì)應(yīng)的地址是 0x0睛挚。如下圖所示:

真機(jī)獲取符號(hào)表.png

經(jīng)過(guò)分析后邪蛔,發(fā)現(xiàn)在真機(jī)中,編譯器應(yīng)該做了下面的優(yōu)化處理(純屬個(gè)人猜測(cè))

  1. 對(duì)于 dyld 中的內(nèi)部函數(shù)對(duì)應(yīng)的符號(hào)扎狱,都可以地址化(去符號(hào)化)侧到,因?yàn)榉?hào)是給人閱讀的,對(duì)于機(jī)器來(lái)說(shuō)一個(gè)二進(jìn)制地址就夠了淤击。而且也可以有效的減少內(nèi)存中 dyld 的體積匠抗。
  2. 對(duì)于 dyld 中暴露出來(lái)的函數(shù),可以在符號(hào)表中獲取到符號(hào)和在 dyld 中的偏移值遭贸,因?yàn)檫@些函數(shù)需要給外部調(diào)用戈咳,所以不能地址化心软。
  3. 對(duì)于 dyld 中引用的第三方庫(kù)中的函數(shù)壕吹,不會(huì)被地址化,但是由于是外部符號(hào)删铃,所以需要進(jìn)行重定向才能獲取到真正的地址耳贬。

總結(jié)

經(jīng)過(guò)自己的研究后,發(fā)現(xiàn)在真機(jī)中猎唁,可能真的沒(méi)有什么方法可以 hook c++ 中的私有方法咒劲。如果只是調(diào)試使用,我們可以直接在 mac 上用 MachOView 或 Hooper 來(lái)獲取到私有函數(shù)的在對(duì)應(yīng) dyld 中的偏移值诫隅,然后直接在代碼中用偏移中進(jìn)行 hook 操作腐魂。但是想在應(yīng)用中直接通過(guò)函數(shù)名稱(chēng)去 hook dyld 中內(nèi)部私有方法應(yīng)該是沒(méi)有辦法的(至少我現(xiàn)在想不出來(lái))。

如果想 hook 私有庫(kù)中的共有方法逐纬,應(yīng)該是可以實(shí)現(xiàn)的蛔屹。可以直接修改 fishhook 的源碼豁生,在外部符號(hào)匹配時(shí)兔毒,對(duì)從 dyld 符號(hào)表取到的符號(hào)進(jìn)行 demangle 操作漫贞,然后再進(jìn)行比較,因?yàn)?c 和 c++ 的唯一區(qū)別育叁,就是存儲(chǔ)在符號(hào)表中的符號(hào)有沒(méi)有經(jīng)過(guò)一層 demangle 操作迅脐。所以只要去除這個(gè)區(qū)別,可以把 c++ 的 hook 和 c 等同起來(lái)豪嗽。

ps: 相同的代碼谴蔑,在 iOS 真機(jī)上獲取到的內(nèi)部函數(shù)都是 <redacted>,但是在 Mac 或 iOS 模擬器上可以解析出來(lái)龟梦。在這個(gè)過(guò)程中树碱,為了探索是否是 iOS 中內(nèi)置的 dyld 和 Mac 中的不一致,我也從一臺(tái)越獄手機(jī)中拉取了 iOS 中的共享緩存 dyld_shared_cache_arm64变秦,從共享緩存中抽出 WebKit 庫(kù)后成榜,發(fā)現(xiàn)和 Mac 上的并沒(méi)有什么區(qū)別。

2019 年 10 月 14 日修改

經(jīng)過(guò)研究后發(fā)現(xiàn)蹦玫,hookzz 是無(wú)法用于 inline hook 的赎婚,所以在非越獄機(jī)器上,暫時(shí)沒(méi)有方法 hook C++ 函數(shù)
使用 HookZz 替換 mach_msg 方法程序崩潰

嘗試使用 fishhook 來(lái) hook 系統(tǒng)的 mach_msg樱溉,從而接管整個(gè)進(jìn)程的實(shí)驗(yàn)也失敗了挣输。
原因是:由于 fishhook 雖然只能 hook 到部分 mach_msg,對(duì)于 WebKit 中被調(diào)用的 mach_msg福贞,無(wú)法 hook 撩嚼,具體原因可以查看下 iOSer 上的討論鏈接 Fishhook 是否無(wú)法 hook 到所有的 mach_msg

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市挖帘,隨后出現(xiàn)的幾起案子完丽,更是在濱河造成了極大的恐慌,老刑警劉巖拇舀,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件逻族,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡骄崩,警方通過(guò)查閱死者的電腦和手機(jī)聘鳞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)要拂,“玉大人抠璃,你說(shuō)我怎么就攤上這事⊥讯瑁” “怎么了搏嗡?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)枪芒。 經(jīng)常有香客問(wèn)我彻况,道長(zhǎng)谁尸,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任纽甘,我火速辦了婚禮良蛮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘悍赢。我一直安慰自己决瞳,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布左权。 她就那樣靜靜地躺著皮胡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪赏迟。 梳的紋絲不亂的頭發(fā)上屡贺,一...
    開(kāi)封第一講書(shū)人閱讀 52,457評(píng)論 1 311
  • 那天,我揣著相機(jī)與錄音锌杀,去河邊找鬼甩栈。 笑死,一個(gè)胖子當(dāng)著我的面吹牛糕再,可吹牛的內(nèi)容都是我干的量没。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼突想,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼殴蹄!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起猾担,我...
    開(kāi)封第一講書(shū)人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤袭灯,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后垒探,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體妓蛮,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡怠李,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年圾叼,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片捺癞。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡夷蚊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出髓介,到底是詐尸還是另有隱情惕鼓,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布唐础,位于F島的核電站箱歧,受9級(jí)特大地震影響矾飞,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜呀邢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一洒沦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧价淌,春花似錦申眼、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至病毡,卻和暖如春濒翻,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背啦膜。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工肴焊, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人功戚。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓娶眷,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親啸臀。 傳聞我的和親對(duì)象是個(gè)殘疾皇子届宠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

推薦閱讀更多精彩內(nèi)容