一.啥是fishhook
fishhook是一個(gè)運(yùn)行在IOS的模擬器和真機(jī)環(huán)境挺份,在MACH-O文件中能夠動(dòng)態(tài)重新綁定符號(hào)的簡(jiǎn)單的庫(kù)。
二.咋用呢
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSLog(@"");//先執(zhí)行一次系統(tǒng)函數(shù)杀捻,共享函數(shù)庫(kù)里才會(huì)加載進(jìn)這個(gè)方法 -> 待確定
// struct rebinding {
// const char *name; //需要hook的函數(shù)名稱,c字符串
// void *replacement; //新函數(shù)的地址
// void **replaced; //原函數(shù)的地址
// };
struct rebinding logRebinding;
logRebinding.name = "NSLog";
logRebinding.replacement = myLog;
logRebinding.replaced = (void *)&sys_nslog;
struct rebinding rebs[1] = {logRebinding};
//rebs: 存放rebinding結(jié)構(gòu)體的數(shù)組
//1: 數(shù)組的長(zhǎng)度
rebind_symbols(rebs, 1);
NSLog(@"originLog");
return YES;
}
static void (*sys_nslog)(NSString *format, ...);
void myLog(NSString *format, ...) {
format = @"newLog";
sys_nslog(format);
}
//2022-05-17 14:31:10.058392+0800 fishhookdemo[2753:1073295] newLog
當(dāng)我們打印NSLog(@"originLog");
時(shí),輸出了newLog
,很神奇
三.為啥這么神奇?
道理很簡(jiǎn)單,就是找到NSlog
的位置嫉髓,將其替換成我們自己的函數(shù)地址就行观腊,只是說(shuō)這個(gè)找尋過(guò)程有點(diǎn)麻煩而已。
這就需要我們具備一定的鋪墊知識(shí)算行。
3.1 動(dòng)態(tài)庫(kù)共享緩存(dyld shared cache)
在iOS系統(tǒng)中梧油,每個(gè)程序依賴的動(dòng)態(tài)庫(kù)都需要通過(guò)dyld(位于/usr/lib/dyld)一個(gè)一個(gè)加載到內(nèi)存,然而如果在每個(gè)程序運(yùn)行的時(shí)候都重復(fù)的去加載一次州邢,勢(shì)必造成運(yùn)行緩慢儡陨,為了優(yōu)化啟動(dòng)速度和提高程序性能,共享緩存機(jī)制就應(yīng)運(yùn)而生量淌。所有默認(rèn)的動(dòng)態(tài)鏈接庫(kù)被合并成一個(gè)大的緩存文件骗村,放到/System/Library/Caches/com.apple.dyld/目錄下,按不同的架構(gòu)保存著.
NSlog
就在共享動(dòng)態(tài)庫(kù)里面
3.2 懶加載符號(hào)表
- 我們把項(xiàng)目里的
NSLog
函數(shù)都注釋掉
- 打開(kāi)
NSLog
函數(shù)呀枢,使用machoview看下胚股,發(fā)現(xiàn)NSLog
函數(shù)是存放在Lazy Symbol Pointers
即懶加載段里面, 偏移量為00008010
在NSLog
之前打一個(gè)斷點(diǎn),然后使用image list
拿到基地址0x0000000100814000
- 基地址+偏移量按理說(shuō)應(yīng)該可以拿到
NSLog
函數(shù)的內(nèi)存地址
16進(jìn)制打印
- 查看
NSLog
函數(shù)的指針,反匯編一下,發(fā)現(xiàn)并不是NSLog
函數(shù)
- 跳過(guò)
NSLog
這個(gè)斷點(diǎn),重新看下0x000000010081c018
地址存放的數(shù)據(jù)裙秋,發(fā)現(xiàn)地址變了
- 反編譯下
震驚琅拌,發(fā)現(xiàn)該地址是NSLog
函數(shù)
- 結(jié)論: 懶加載表中的函數(shù),是在函數(shù)運(yùn)行一次后才會(huì)確定函數(shù)的地址
所以使用fishhook
需要先調(diào)用下要hook的函數(shù)
3.3 符號(hào)綁定& 重綁定符號(hào)
NSLog
函數(shù)屬于共享緩存庫(kù)的函數(shù)摘刑,所以在編譯期間进宝,是不能確定它的地址的。只有在運(yùn)行時(shí)枷恕,dyld
才會(huì)從共享緩存庫(kù)拿到NSLog
函數(shù)并且確定它的地址,寫入到懶加載符號(hào)表里
四.咋實(shí)現(xiàn)的呢
4.1 記錄下簡(jiǎn)單的步驟
- 找到字符串表
symtab_cmd->stroff
symtab_segment - 找到符號(hào)表
LC_SYMTAB
segment - 找到動(dòng)態(tài)符號(hào)表
LC_DYSYMTAB
segment -
indirect_symtab = LC_DYSYMTAB->indirectsymoff
拿到間接符號(hào)表indirect_symtab
党晋,間接符號(hào)表其實(shí)就是動(dòng)態(tài)庫(kù)的符號(hào)表 - 拿到懶加載section
__la_symbol_ptr
,這里面就有NSLog
函數(shù)的地址 -
indirect_symbol_indices = indirect_symtab + __la_symbol_ptr.reserved1
得到__la_symbol_ptr
模塊在indirect_symtab
表的起始位置 7 8 9 - for循環(huán)取出
LC_SYMTAB.7.n_un.n_strx
拿到其在字符串表symtab_segment
中的位置, 取出字符串NSLog
- 判斷要替換的字符串和取出的字符串是否一樣徐块,一樣的話將
__la_symbol_ptr
替換成我們的函數(shù)
4.2 搞個(gè)超簡(jiǎn)版demo未玻,來(lái)替換NSLog
調(diào)用下面的函數(shù),你會(huì)發(fā)現(xiàn)不管你NSLog什么,打印出來(lái)都是"替換NSLog成功啦"
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSLog(@"");//先執(zhí)行一次系統(tǒng)函數(shù)胡控,共享函數(shù)庫(kù)里才會(huì)加載進(jìn)這個(gè)方法 -> 因?yàn)槭菓?
//驗(yàn)證
replaceNSLog(youNSLog);
NSLog(@"");
return YES;
}
static void youNSLogFuc() {
printf("替換NSLog成功啦");
}
void (*youNSLog)(void) = youNSLogFuc;
fishcook.h
#ifndef fishcook_h
#define fishcook_h
#include <stdio.h>
#ifdef __cplusplus
extern "C" {
#endif //__cplusplus
//替換成你自己的函數(shù)
int replaceNSLog(void *youNSLog);
#ifdef __cplusplus
}
#endif //__cplusplus
#endif /* fishcook_h */
fishcook.m
//
// fishcook.c
// fishhookdemo
//
// Created by liyongkai on 2022/7/21.
//
#include "fishcook.h"
#include <mach-o/dyld.h>
#include <mach/mach.h>
#include <mach-o/loader.h>
#include <mach-o/nlist.h>
void *_youNSLog;
/// 替換函數(shù)
/// @param symtab 符號(hào)表
/// @param strtab 字符串表
/// @param section __la_symbol_ptr
/// @param indirect_symtab 間接符號(hào)表
static void replaceImp(intptr_t vmaddr_slide,
struct nlist_64 *symtab,
char *strtab,
struct section_64 *section,
uint32_t *indirect_symtab) {
//拿到__la_symbol_ptr section里面的內(nèi)容在間接表中的起始位置
uint32_t *indirect_symtab_indices = indirect_symtab + section->reserved1;
for (uint i = 0; i < section->size / sizeof(void *); i ++) {
uint32_t tabIndex = indirect_symtab_indices[i];
//通過(guò)符號(hào)表拿到nslog在字符串表中的位置
uint32_t strtab_offset = symtab[tabIndex].n_un.n_strx;
//拿到字符串
char *symbol_name = strtab + strtab_offset;
//如果一樣就替換
if (strcmp(&symbol_name[1], _youNSLog)) {
void **la_symbol_prt_address = (void **)(vmaddr_slide + section->addr);
la_symbol_prt_address[i] = _youNSLog;
return;
}
}
}
static void allImages(const struct mach_header* header, intptr_t vmaddr_slide) {
//1.能到符號(hào)表segment和動(dòng)態(tài)符號(hào)表segment
//記錄當(dāng)前的segmnet
struct segment_command_64 *cur_seg_cmd;
//符號(hào)表LC_SYMTAB
struct symtab_command* symtab_cmd = NULL;
//動(dòng)態(tài)符號(hào)表LC_DYSYMTAB
struct dysymtab_command* dysymtab_cmd = NULL;
//第一個(gè)segment的地址
uintptr_t cur = (uintptr_t )header + sizeof(struct mach_header_64);
//循環(huán)遍歷,找到 符號(hào)表LC_SYMTAB, 動(dòng)態(tài)符號(hào)表LC_DYSYMTAB
for (uint i = 0; i < header->ncmds; i ++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (struct segment_command_64 *)cur;
if (cur_seg_cmd->cmd == LC_SYMTAB) {
symtab_cmd = (struct symtab_command *)cur_seg_cmd;
} else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
dysymtab_cmd = (struct dysymtab_command *)cur_seg_cmd;
}
}
if (!symtab_cmd || !dysymtab_cmd) {
// 符號(hào)表或者動(dòng)態(tài)符號(hào)表的segment為空,說(shuō)明沒(méi)有動(dòng)態(tài)庫(kù),return
return;
}
//2.通過(guò)segment拿到符號(hào)表section, string table, 間接符號(hào)表
uintptr_t mh_addr = (uintptr_t)_dyld_get_image_header(0);
//拿到符號(hào)表section
struct nlist_64 *symtab = (struct nlist_64 *) (mh_addr +symtab_cmd->symoff);
//拿到string table
char *strtab = (char *)(mh_addr + symtab_cmd->stroff);
//拿到間接符號(hào)表, 也就是動(dòng)態(tài)庫(kù)的符號(hào)表
uint32_t *indirect_symtab = (uint32_t *)(mh_addr + dysymtab_cmd->indirectsymoff);
//找到__got section 和 la_symbol_ptr section, 他們都在__DATA segment里面
cur = (uintptr_t)header + sizeof(struct mach_header_64);
for (uint i = 0; i < header->ncmds; i ++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (struct segment_command_64 *)cur;
if (cur_seg_cmd->cmd == LC_SEGMENT_64) {
if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
strcmp(cur_seg_cmd->segname, "__DATA_CONST") != 0) {
continue;
}
for (uint j = 0; j < cur_seg_cmd->nsects; j ++) {
struct section_64 *sect = (struct section_64 *)(cur + sizeof(struct segment_command_64)) + j;
if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
// 懶加載符號(hào)表
replaceImp(vmaddr_slide, symtab, strtab, sect, indirect_symtab);
return;
}
}
}
}
}
int replaceNSLog(void *youNSLog) {
_youNSLog = youNSLog;
//監(jiān)聽(tīng)各個(gè)image
_dyld_register_func_for_add_image(allImages);
return 0;
}
五.總結(jié)
說(shuō)白了扳剿,fishhook代碼看似復(fù)雜,主要mach-O內(nèi)部結(jié)構(gòu)體復(fù)雜铜犬,找來(lái)找去很是麻煩。但如果有一天你對(duì)mach-O了如執(zhí)掌轻庆,那么fishhook也就不攻自破了癣猾。
- fishhook只能hook動(dòng)態(tài)庫(kù)里面的函數(shù),包含__got和la_symbol_ptr段
- 使用fishhook需要先調(diào)用下要hook的函數(shù)
- 將hook代碼盡量提前
六.Q
總結(jié)哪些函數(shù)屬于OC函數(shù)余爆,哪些屬于共享緩存庫(kù)