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 = §ions[j];
NSLog(@"section name %s",lazySection->sectname);
}
if ((mSection.flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
// 非懶加載到section header
nonLazySection = §ions[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)》