fishhook實現(xiàn)原理分析

fishhook 是FaceBook開源的可以用來重綁定Mach-O格式的外部動態(tài)庫中符號的一個庫,這里一定要理解為什么hook的是動態(tài)庫肺孤,想要真正搞清楚這個庫的原理可以閱讀《程序員的自我修養(yǎng)》這本書尤仍,首先要理解什么是靜態(tài)庫,什么是動態(tài)庫绷耍。這篇文章比較偏重對整個庫實現(xiàn)過程的分析,實現(xiàn)代碼的理解

使用

static void (*sys_NSLog)(NSString *format,...);
static void hook_nslog(NSString *format, ...){
    // 修改打印的內(nèi)容
    format = [format stringByAppendingFormat:@" haha"];
    // 調(diào)用hook的函數(shù)
    sys_NSLog(format);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSLog(@"Hook before !");
        // hook Foundation框架中的NSLog函數(shù)
        struct rebinding rebindSymbol;
        rebindSymbol.name = "NSLog";
        rebindSymbol.replacement = (void *)hook_nslog;
        // 將原來函數(shù)的地址保持在sys_NSLog中
        rebindSymbol.replaced = (void **)&sys_NSLog;
        
        struct rebinding rebs[] = {rebindSymbol};
        rebind_symbols(rebs,1);
        
        NSLog(@"Hook after !");
        
    }
    return 0;
}

打印結(jié)果

Hook before !
Hook after ! haha

通過前后兩個打印信息,可以看到我們hook了NSLog函數(shù)隐锭,并且打印了自定義的信息渐扮,同時要注意在我們替換的函數(shù)中調(diào)用保存的函數(shù)论悴,這樣才會調(diào)用到原來函數(shù)中

實現(xiàn)原理分析

dyld通過更新Mach-O二進制文件的__Data段的特定部分的指針來綁定所謂的 lazy 和 non-lazy 符號。fishhook通過rebind_symbols函數(shù)傳入的需要替換的符號名稱來定位它的位置墓律,然后執(zhí)行替換來實現(xiàn)重綁定符號的過程

在一個Mach-O文件中膀估,__Data段可能會包含動態(tài)綁定符號相關(guān)的section:__nl_symbol_ptr 和 __la_symbol_ptr ,__nl_symbol_ptr是非懶加載的一組指針數(shù)組(可以理解為函數(shù)地址耻讽,這些地址在程序載入的時候綁定)察纯,__la_symbol_ptr也是指向?qū)牒瘮?shù)的指針數(shù)組,通常在第一次調(diào)用該符號時由dyld_stub_binder函數(shù)填充针肥,為了能在相應(yīng)的sections中找到特定位置的符號的名稱饼记,需要跳過幾個間接層。對于這兩個相關(guān)的sections慰枕,對應(yīng)的section header (定義在<mach-o/loader.h>頭文件中)中的reserved1字段提供了他們相關(guān)的符號在間接符號表中的起始位置具则,間接符號表可以通過__LINKEDIT段來定位,它是在符號表中的一組index數(shù)組具帮,其順序與懶加載和非懶加載部分中指針的順序相同博肋,所以對于struct section nl_symbol_ptr,它在符號表中第一個符號的index可以通過這樣來獲取indirect_symbol_table[nl_symbol_ptr->reserved1]蜂厅,符號表是為struct nlist的數(shù)組匪凡,每一個nlist中對應(yīng)的在字符表中的index,字符表也可以通過__LINKEDIT段來定位掘猿,字符表存儲的就是符號名稱的字符數(shù)組锹雏。所以最后我們就可以通過字符表和需要hook的符號名稱比較來找到符號的位置,然后可以將函數(shù)指針替換术奖。

上面是對官方說明文檔的一些翻譯理解礁遵,總結(jié)一下整個過程就是首先要明確我們要替換的數(shù)據(jù)是在數(shù)據(jù)區(qū)轻绞,當(dāng)然代碼區(qū)的數(shù)據(jù)我們也無法修改。動態(tài)庫的符號又分為所謂的:懶加載符號和非懶加載符號佣耐,非懶加載符號在程序加載階段就必須要完成綁定政勃,綁定就是dyld去查找對應(yīng)的符號對應(yīng)的函數(shù)地址,然后將地址寫入到非懶加載的數(shù)據(jù)區(qū)兼砖。懶加載符號會在第一次調(diào)用這個函數(shù)時奸远,程序會通過懶加載符號的數(shù)據(jù)區(qū)找對應(yīng)的函數(shù)地址,而此時這個函數(shù)地址指向的是__stud_helper代碼段的一段固定代碼讽挟,這段 代碼又會跳轉(zhuǎn)到dyld_stub_binder這個函數(shù)處懒叛,然后通過dyld_stub_binder去查找外部符號地址,找到后將地址寫入到相應(yīng)的數(shù)據(jù)區(qū)耽梅。

實現(xiàn)過程

整個實現(xiàn)過程就像是一個文件的解析薛窥,如果有解析過mp4,flv這種類似的文件眼姐,可能會更好理解整個過程诅迷。

第一步:找到當(dāng)前可執(zhí)行文件的image文件

    // 獲取加載的image文件個數(shù)
    int count = _dyld_image_count();
    int executeIndex = -1;
    for (int i = 0; i<count; i++) {
        // 獲取image的mach_header
        const struct mach_header* machHeader = _dyld_get_image_header(i);
        if (machHeader->filetype == MH_EXECUTE) { // 查找主程序的image的index
            executeIndex = i;
            break;
        }
    }

先通過dyld提供的函數(shù)_dyld_image_count獲取當(dāng)前程序加載的image文件個數(shù),然后遍歷查找主程序image所在的index众旗。

第二步:查找符號命令罢杉,動態(tài)符號命令,鏈接命令

#ifdef __LP64__
typedef struct mach_header_64 mach_header_t;
typedef struct segment_command_64 segment_command_t;
typedef struct section_64 section_t;
typedef struct nlist_64 nlist_t;
#define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT_64
#else
typedef struct mach_header mach_header_t;
typedef struct segment_command segment_command_t;
typedef struct section section_t;
typedef struct nlist nlist_t;
#define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT
#endif

    const struct mach_header* machHeader = _dyld_get_image_header(executeIndex);
    
    uintptr_t cur = (uintptr_t)machHeader + sizeof(mach_header_t);
    
    // 符號表命令
    struct symtab_command *symCommand = NULL;
    // 動態(tài)符號表命令
    struct dysymtab_command *dysymCommand = NULL;
    // 鏈接命令
    segment_command_t *linked_cmd = NULL;
    
        for (int i = 0; i<machHeader->ncmds; i++) {
        
        struct load_command *command = (struct load_command *)cur;
        
        // 鏈接命令屬于segment_command類型
        if (command->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
            segment_command_t *segmentCmd = (segment_command_t *)command;
            
            // 鏈接命令
            if (strcmp(segmentCmd->segname, SEG_LINKEDIT) == 0) {
                linked_cmd = segmentCmd;
            }
        }
        
        // 符號表
        if (command->cmd == LC_SYMTAB) {
            symCommand = (struct symtab_command *)command;
        }
        
        // 動態(tài)符號表
        if (command->cmd == LC_DYSYMTAB) {
            dysymCommand = (struct dysymtab_command *)command;
        }
        
        cur += command->cmdsize;
    }
    

通過上面的代碼就可以找到符號命令贡歧,動態(tài)符號命令滩租,鏈接符號命令。

第三步:獲取懶加載符號函數(shù)地址和非懶加載符號函數(shù)地址的section Hearder

非懶加載符號對應(yīng)的函數(shù)指針數(shù)組在數(shù)據(jù)區(qū)的__got節(jié)利朵,懶加載符號對應(yīng)的函數(shù)指針數(shù)組在數(shù)據(jù)區(qū)的__la_symbol_ptr節(jié)持际,首先需要查找到對應(yīng)到Section header,這兩個section header在segment_command為SEG_DATA和SEG_DATA_CONST的command中哗咆,,__got section header在SEG_DATA_CONST的segment_command中益眉,__la_symbol_ptr section header在SEG_DATA的segment_command中

if (strcmp(segmentCmd->segname, SEG_DATA) == 0 || strcmp(segmentCmd->segname, SEG_DATA_CONST) == 0) {
                section_t *sections = (section_t *)((uintptr_t)segmentCmd + sizeof(segment_command_t));
                for (int j = 0; j<segmentCmd->nsects; j++) {
                    section_t mSection = sections[j];
                    if ((mSection.flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS ) {
                        // 懶加載section header
                        lazySection = &sections[j];
                        NSLog(@"section name %s",lazySection->sectname);
                    }
                    
                    if ((mSection.flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
                        // 非懶加載到section header
                        nonLazySection = &sections[j];
                        int index = nonLazySection->reserved1;
                        NSLog(@"section name %s",nonLazySection->sectname);
                    }
                }
            }

第四步:理解ASLR晌柬,計算符號信息,間接符號信息郭脂,字符信息在內(nèi)存中實際地址

  • ASLR:通俗的說就是在app每次啟動的時候會隨機給一個地址偏移量,由于現(xiàn)代計算機都使用的是虛擬內(nèi)存年碘,會導(dǎo)致程序加載到內(nèi)存中可能每次都是固定的一個地址,這樣會有安全問題展鸡,通過每次程序啟動時給程序加載的地址添加一個隨機的偏移值屿衅,就是所謂的ASLR
  • 計算程序加載的基地址:通過鏈接段的vmaddr和fileoff字段計算出沒有ASLR的情況下程序加載的基地址,然后將這個地址加上ASLR的值就可以得到程序?qū)嶋H的基地址
  • 計算符號信息地址:通過 base(上面計算得到的基地址) + symtab 的偏移量 計算 symtab 表的首地址莹弊,并獲取 nlist_t 結(jié)構(gòu)體實例
  • 計算間接符號信息地址:通過 base + indirectsymoff 偏移量來計算動態(tài)符號表的首地址
  • 計算字符信息地址:/通過 base + stroff 字符表偏移量計算字符表中的首地址涤久,獲取字符串表
    // 得到當(dāng)前程序ASLR的值
    intptr_t slide = _dyld_get_image_vmaddr_slide(executeIndex);
    
    // 計算實際加載的基地址
    uintptr_t linked_base_address = linked_cmd->vmaddr-linked_cmd->fileoff+slide;
    
    // 計算符號表所在地址
    nlist_t *symbolList = (nlist_t *)(linked_base_address+symCommand->symoff);
    // 計算間接符號表所在位置
    uint32_t *dysmList = (uint32_t *)(linked_base_address+dysymCommand->indirectsymoff);
    // 計算字符表所在位置
    char *strList = (char *)(linked_base_address+symCommand->stroff);

上面就是計算的方法涡尘,其中symCommand和dysymCommand通過上面步驟二獲取

第五步:遍歷__got段和__la_symbol_ptr段

最后一步就是遍歷數(shù)據(jù)區(qū)的__got段(非懶加載符號的函數(shù)地址數(shù)組)和__la_symbol_ptr (懶加載符號的函數(shù)地址數(shù)組)比對要查找的符號名稱,找到要替換的符號位置

    // 遍歷__got段
    int gotSymbolNum = nonLazySection->size/(sizeof(void*));
    void **gotSymbolValue = (void **)((uintptr_t)slide + nonLazySection->addr);
    for (int i = 0; i<gotSymbolNum; i++) {
        // 在間接符號表中的index
        int dysm_index = nonLazySection->reserved1+i;
        // 在符號表中的index
        uint32_t symtab_index = dysmList[dysm_index];
        if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
            symtab_index == (INDIRECT_SYMBOL_LOCAL   | INDIRECT_SYMBOL_ABS)) {
          continue;
        }
        // 找到對應(yīng)的符號
        nlist_t findSymbol = symbolList[symtab_index];
        char *mSymbolName = strList+findSymbol.n_un.n_strx;
        bool symbol_name_longer_than_1 = mSymbolName[0] && mSymbolName[1];
        if (symbol_name_longer_than_1) {
            // 由于c語言在編譯時將符號名前面加上_,所以這里需要從index為1處開始比較
            if (strcmp(&mSymbolName[1],symbolName) == 0) {
                NSLog(@"Find symbolName : %s",symbolName);
                //break;
                // 替換函數(shù)實現(xiàn)
                gotSymbolValue[i] = replaceFunc;
            }
        }
    }
    // 遍歷__la_symbol_ptr段
    int lazySymbolNum = lazySection->size/sizeof(void*);
    void **laSymbolValue = (void **)((uintptr_t)slide + lazySection->addr);
    for (int i = 0; i<lazySymbolNum; i++) {
        // 在間接符號表中的index
        int dysm_index = lazySection->reserved1+i;
        // 在符號表中的index
        uint32_t symtab_index = dysmList[dysm_index];
        if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
            symtab_index == (INDIRECT_SYMBOL_LOCAL   | INDIRECT_SYMBOL_ABS)) {
          continue;
        }
        // 在字符表中的index
        int str_offset = symbolList[symtab_index].n_un.n_strx;
        char *symbolStr = strList+str_offset;
        bool symbol_name_loger_than_1 = symbolStr[0] && symbolStr[1];
        if (symbol_name_loger_than_1) {
            if (strcmp(&symbolStr[1], symbolName) == 0) {
                NSLog(@"Find symbol : %s",symbolName);
                // 替換函數(shù)實現(xiàn)
                laSymbolValue[i] = replaceFunc;
            }
        }
    }

總結(jié)

以上主要分析了查找符號的整個流程响迂,具體實現(xiàn)可根據(jù)fishhook源碼比較分析.

參考資料

《程序員的自我修養(yǎng)》

《深入理解Mac OSX & iOS操作系統(tǒng)》

探究Mach-O文件

iOS程序員的自我修養(yǎng)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末考抄,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子蔗彤,更是在濱河造成了極大的恐慌川梅,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件然遏,死亡現(xiàn)場離奇詭異贫途,居然都是意外死亡,警方通過查閱死者的電腦和手機待侵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門丢早,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人诫给,你說我怎么就攤上這事香拉。” “怎么了中狂?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵凫碌,是天一觀的道長。 經(jīng)常有香客問我胃榕,道長盛险,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任勋又,我火速辦了婚禮苦掘,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘楔壤。我一直安慰自己鹤啡,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布蹲嚣。 她就那樣靜靜地躺著递瑰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪隙畜。 梳的紋絲不亂的頭發(fā)上抖部,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音议惰,去河邊找鬼慎颗。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的俯萎。 我是一名探鬼主播傲宜,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼讯屈!你這毒婦竟也來了蛋哭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤涮母,失蹤者是張志新(化名)和其女友劉穎谆趾,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體叛本,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡沪蓬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了来候。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片跷叉。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖营搅,靈堂內(nèi)的尸體忽然破棺而出云挟,到底是詐尸還是另有隱情,我是刑警寧澤转质,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布园欣,位于F島的核電站,受9級特大地震影響休蟹,放射性物質(zhì)發(fā)生泄漏沸枯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一赂弓、第九天 我趴在偏房一處隱蔽的房頂上張望绑榴。 院中可真熱鬧,春花似錦盈魁、人聲如沸翔怎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赤套。三九已至,卻和暖如春按脚,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背敦冬。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工辅搬, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓堪遂,卻偏偏與公主長得像介蛉,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子溶褪,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

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

  • 上一篇分析了fishHook原理,本文在fishHook原理基礎(chǔ)上進行fishHook源碼分析币旧。從fishHook...
    king_jensen閱讀 833評論 0 0
  • 前言 雖然寫 fishhook 原理的文章有很多,但是總覺得不夠簡單直觀猿妈。大部分都是羅列大堆源碼進行講解吹菱,看得人云...
    微微笑的蝸牛閱讀 1,856評論 6 7
  • 13.1 Objective-C消息傳遞(Messaging) 對于C/C++這類靜態(tài)語言,調(diào)用一個方法其實就是跳...
    泰克2008閱讀 1,975評論 1 6
  • fishhook fishhook是Facebook提供的用于hook系統(tǒng)c函數(shù)的庫彭则。它能動態(tài)重新綁定運行在iOS...
    lattr閱讀 1,073評論 0 1
  • 上一篇說到源碼經(jīng)過預(yù)處理鳍刷、編譯、匯編之后生成目標(biāo)文件俯抖,這一章介紹一下iOS输瓜、Mac OS中目標(biāo)文件的格式Mach-...
    Tenloy閱讀 2,008評論 2 9