fishhook 源碼分析

引用

前言

fishhook 是什么?

fishhook 是用于 Mach-O 的符號(hào)動(dòng)態(tài)綁定的 facebook 開源維護(hù)的的第三方庫(kù)棚愤。當(dāng) Mach-O 加載第三方庫(kù)的時(shí)候宛畦,可以用 fishhook 進(jìn)行hook揍移。

具體的參考 fishhook 在 GitHub 上的 readme .

原理

readme 上已經(jīng)說的很清楚了那伐。但是想要搞清楚原理,還有很多信息需要補(bǔ)充畅形。

一句話概括就是:fishhook 找到目標(biāo)函數(shù)地址诉探,然后替換成自己的函數(shù)地址肾胯,達(dá)到 hook 的目的。

Mach-O

讓我們來看看 Mach-O 的文件格式宪潮。Mach-O 是Mac OS X 上程序的標(biāo)準(zhǔn)格式滚粟。

這里有張弄慰,來表示 Mach-O 的基本格式。

Figure 1 Mach-O file format basic structure

Mach-O 文件包含三個(gè)主要區(qū)域:

  • header structure
  • load commands
  • segment

其中斋日,segment 是一段一段的墓陈。

每個(gè) segment 包含 0 個(gè)或 多個(gè) section贡必。每個(gè) section 包含 code 或者 特殊類型的 data。每個(gè) segment 定義了 一片虛擬內(nèi)存 衫樊,其中動(dòng)態(tài)鏈接器映射到程序的地址空間科侈。

Mach-O 在內(nèi)存中的布局結(jié)構(gòu):

Mach-O 內(nèi)存布局

fishhook 要做的事情就是找到 segment: DATA 臀栈,然后找到 section:__la_symbol_ptr挠乳。最后睡扬,找到其中的目標(biāo)函數(shù)地址,替換目標(biāo)函數(shù)地址為自己的函數(shù)地址屎开。

具體分為2個(gè)步驟:

  • 找到目標(biāo)函數(shù)地址
  • 找到函數(shù)名马靠,一一比對(duì)虑粥。成功后,替換目標(biāo)函數(shù)地址為自己的函數(shù)地址第晰。

其中,找到函數(shù)名的過程甜熔,有點(diǎn)曲折突倍。string table 是一個(gè)數(shù)組,里面包含了函數(shù)名焊虏。根據(jù)偏移 offset 能找到需要的函數(shù)名诵闭。

分析

進(jìn)行符號(hào)綁定前,我們能獲得的信息有:

  • mach_header *header瘟芝,header 結(jié)構(gòu) 指針
  • intptr_t slide模狭,ALSR 偏移

接著 header 的是 load commands踩衩。 load commans 里面包含了 LG_SEGMENT驱富。 LG_SEGMENT 有4種:

  • __PAGEZERO
  • __TEXT
  • __DATA
  • __LINKEDIT

PAGEZERO 是可執(zhí)行程序的第一個(gè)段∠呓牛總是位于虛擬內(nèi)存中的最開始的位置浑侥。它的大小是根據(jù)架構(gòu)類型來的晰绎。在X64里面的荞下,大小是 0x0000000100000000 。

TEXT 里面存儲(chǔ)的是 code 和 只讀數(shù)據(jù)仰税。我們這里用不到陨簇。

DATA 里面包含了可讀寫數(shù)據(jù)迹淌。里面的 section header 正是我們需要的信息。最重要的是找到 section header 里的 __nl_symbol_ptr__la_symbol_ptr

LINKEDIT 里面記錄了動(dòng)態(tài)連接器的一些信息荷鼠,fishhook 主要通過它找到基地址榔幸,一般得到的地址就是 PAGEZERO 之后的地址削咆,也就是 0x0000000100000000蠢笋。

LG_SEGMENT(__DATA)LG_SEGMENT(__TEXT) 里面都包含了 section header昨寞。我們主要用到 DATA 里的 section header ( 主要是 __nl_symbol_ptr__la_symbol_ptr )援岩。

LG_SEGMENT(__DATA) 包含如下 section header :

  • __nl_symbol_ptr
  • __got
  • __la_symbol_ptr
  • __objc_imageinfo
  • __bss

__nl_symbol_ptr is an array of pointers to non-lazily bound data (these are bound at the time a library is loaded) and __la_symbol_ptr is an array of pointers to imported functions that is generally filled by a routine called dyld_stub_binder during the first call to that symbol (it's also possible to tell dyld to bind these at launch)

我們先來看看如何調(diào)用 fishhook

static void (*orig_foo)(int);

void my_foo(int x) {
printf("real func: %d\n",x);
}

int main(int argc, const char * argv[]) {
@autoreleasepool {
rebind_symbols((struct rebinding[1]){"foo",my_foo,(void *)&orig_foo}, 1);
foo(20);
}
return 0;
}

函數(shù)內(nèi)部調(diào)用:

  • rebind_symbols_image
  • _rebind_symbols_for_image
  • rebind_symbols_for_image
  • perform_rebinding_with_section

rebind_symbols 里大體上做了如下操作:

  • 開辟鏈表 rebindings_entry 空間享怀,并填入信息
  • 判斷是不是第一次調(diào)用添瓷。第一次調(diào)用的話,對(duì)添加的 image 注冊(cè)回調(diào):_dyld_register_func_for_add_image(_rebind_symbols_for_image);
  • 非第一次的話坯汤,在已有的 image 里調(diào)用 _rebind_symbols_for_image

_rebind_symbols_for_image 里只是調(diào)用: rebind_symbols_for_image 玫霎。

rebind_symbols_for_image

rebind_symbols_for_image 大體上做如下動(dòng)作:

  • 在 load commands 里找到 linkedit_segment庶近、symtab_cmd眷蚓、dysymtab_cmd
  • 通過 linkedit_segment 和 symtab_cmd 找到 symtab沙热、strtab
  • 在 load commands 里找到 __DATA,__la_symbol_ptr__DATA,__nl_symbol_ptr
  • 調(diào)用 perform_rebinding_with_section

查找 linkedit_segment、symtab_cmd投队、dysymtab_cmd敷鸦。在 MachOView 里扒披,查看事例程序 TestMacOS(參見下面鏈接)的內(nèi)存布局:

TestMacOs MachOView 查看結(jié)構(gòu)

mach_header_64

struct mach_header_64
{
uint32_t magic;
cpu_type_t cputype;
cpu_subtype_t cpusubtype;
uint32_t filetype;
uint32_t ncmds;
uint32_t sizeofcmds;
uint32_t flags;
uint32_t reserved;
};
  • ncmds
  • load commands 的數(shù)量

segment_command_64

struct segment_command_64
{
uint32_t cmd;
uint32_t cmdsize;
char segname[16];
uint64_t vmaddr;
uint64_t vmsize;
uint64_t fileoff;
uint64_t filesize;
vm_prot_t maxprot;
vm_prot_t initprot;
uint32_t nsects;
uint32_t flags;
};
  • cmd
  • 數(shù)字從0x1開始碟案,代表不同的 load command
  • cmdsize
  • 當(dāng)前 cmd 的大小
  • segname
  • segment 的名字,大寫的辆亏。如:__TEXT__DATA
  • vmaddr
  • 段在虛擬內(nèi)存的開始地址
  • fileoff
  • vmaddr 的偏移
#define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT_64

//獲得 load commands 的起始地址
uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (segment_command_t *)cur;
//查找LG_SEGMENT_64->__LINKEDIT
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
linkedit_segment = cur_seg_cmd;
}
}
//查找 LG_SYMTAB
else if (cur_seg_cmd->cmd == LC_SYMTAB) {
symtab_cmd = (struct symtab_command*)cur_seg_cmd;
}
//查找 LG_DYSYMTAB
else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
}
}

找到 linkedit_base

// Find base symbol/string table addresses
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;

找到 symtab、strtab疑苔、dysymtab_command:

symtab_command

struct symtab_command
{
uint_32 cmd;
uint_32 cmdsize;
uint_32 symoff;
uint_32 nsyms;
uint_32 stroff;
uint_32 strsize;
};
  • symoff
  • symbol table 的偏移
  • stroff
  • string table 的偏移

dysymtab_command

struct dysymtab_command
{
uint32_t cmd;
uint32_t cmdsize;
uint32_t ilocalsym;
uint32_t nlocalsym;
uint32_t iextdefsym;
uint32_t nextdefsym;
uint32_t iundefsym;
uint32_t nundefsym;
uint32_t tocoff;
uint32_t ntoc;
uint32_t modtaboff;
uint32_t nmodtab;
uint32_t extrefsymoff;
uint32_t nextrefsyms;
uint32_t indirectsymoff;
uint32_t nindirectsyms;
uint32_t extreloff;
uint32_t nextrel;
uint32_t locreloff;
uint32_t nlocrel;
};
  • indirectsymoff
  • indirect symbol table 的偏移

nlist_64

struct nlist_64
{
union {
uint32_t n_strx;
} n_un;
uint8_t n_type;
uint8_t n_sect;
uint16_t n_desc;
uint64_t n_value;
};

  • n_un
  • 函數(shù)名在 string table 的 index
//注意:這里的偏移都是基于基地址( linkedit_base )的偏移
nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);

獲取 section header兵迅,并調(diào)用 perform_rebinding_with_section:

cur = (uintptr_t)header + sizeof(mach_header_t);
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (segment_command_t *)cur;
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
continue;
}
for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
section_t *sect =
(section_t *)(cur + sizeof(segment_command_t)) + j;
if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
}
}
}

perform_rebinding_with_section

section_64

struct section_64
{
char sectname[16];
char segname[16];
uint64_t addr;
uint64_t size;
uint32_t offset;
uint32_t align;
uint32_t reloff;
uint32_t nreloc;
uint32_t flags;
uint32_t reserved1;
uint32_t reserved2;
};
  • addr
  • section 的虛擬內(nèi)存地址,類型是 integer 的
  • reserved1
  • 對(duì)于 symbol pointer sections 和 stubs sections 來說瞧省,reserved1 表示 indirect table 數(shù)組的 index。用來索引 section's entries.
uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
//找到庫(kù)函數(shù)的地址: section->addr <==> section(__DATA,__la_symbol_ptr)
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
for (uint i = 0; i < section->size / sizeof(void *); i++) {
//查找indirect_symbol_indices數(shù)組鞍匾,獲取其中的內(nèi)容,得到 symtab_index 
uint32_t symtab_index = indirect_symbol_indices[i];
if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
symtab_index == (INDIRECT_SYMBOL_LOCAL   | INDIRECT_SYMBOL_ABS)) {
continue;
}
//找到函數(shù)名位于 string table 的偏移
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
//找到 函數(shù)名
char *symbol_name = strtab + strtab_offset;

if (strnlen(symbol_name, 2) < 2) {
continue;
}
struct rebindings_entry *cur = rebindings;
while (cur) {
for (uint j = 0; j < cur->rebindings_nel; j++) {
if (strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
if (cur->rebindings[j].replaced != NULL &&
indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
//把函數(shù)地址保存到 rebindings.replaced 里
*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
}
//替換內(nèi)容為自定義函數(shù)地址
indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
goto symbol_loop;
}
}
cur = cur->next;
}
symbol_loop:;
}

這里有張圖交洗,仿照著 fishhook 的 readme 畫的。

查找 string table 的函數(shù)名

附錄Demo

Demo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末橡淑,一起剝皮案震驚了整個(gè)濱河市构拳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖置森,帶你破解...
    沈念sama閱讀 216,919評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件斗埂,死亡現(xiàn)場(chǎng)離奇詭異凫海,居然都是意外死亡呛凶,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門行贪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來漾稀,“玉大人,你說我怎么就攤上這事瓮顽∠睾茫” “怎么了围橡?”我有些...
    開封第一講書人閱讀 163,316評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵暖混,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我翁授,道長(zhǎng)拣播,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,294評(píng)論 1 292
  • 正文 為了忘掉前任收擦,我火速辦了婚禮贮配,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘塞赂。我一直安慰自己泪勒,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,318評(píng)論 6 390
  • 文/花漫 我一把揭開白布宴猾。 她就那樣靜靜地躺著圆存,像睡著了一般。 火紅的嫁衣襯著肌膚如雪仇哆。 梳的紋絲不亂的頭發(fā)上沦辙,一...
    開封第一講書人閱讀 51,245評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音讹剔,去河邊找鬼油讯。 笑死,一個(gè)胖子當(dāng)著我的面吹牛延欠,可吹牛的內(nèi)容都是我干的陌兑。 我是一名探鬼主播,決...
    沈念sama閱讀 40,120評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼由捎,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼兔综!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,964評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤邻奠,失蹤者是張志新(化名)和其女友劉穎笤喳,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體碌宴,經(jīng)...
    沈念sama閱讀 45,376評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡杀狡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,592評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了贰镣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片呜象。...
    茶點(diǎn)故事閱讀 39,764評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖碑隆,靈堂內(nèi)的尸體忽然破棺而出恭陡,到底是詐尸還是另有隱情,我是刑警寧澤上煤,帶...
    沈念sama閱讀 35,460評(píng)論 5 344
  • 正文 年R本政府宣布休玩,位于F島的核電站,受9級(jí)特大地震影響劫狠,放射性物質(zhì)發(fā)生泄漏拴疤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,070評(píng)論 3 327
  • 文/蒙蒙 一独泞、第九天 我趴在偏房一處隱蔽的房頂上張望呐矾。 院中可真熱鬧,春花似錦懦砂、人聲如沸蜒犯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽罚随。三九已至,卻和暖如春衫画,著一層夾襖步出監(jiān)牢的瞬間毫炉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工削罩, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瞄勾,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,819評(píng)論 2 370
  • 正文 我出身青樓弥激,卻偏偏與公主長(zhǎng)得像进陡,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子微服,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,665評(píng)論 2 354

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

  • 最早了解到fishhook是看了下面兩篇文章之后趾疚,頓時(shí)讓我覺得這是一個(gè)非常好的東西。總共210行代碼糙麦,收獲了150...
    阿呆少爺閱讀 6,610評(píng)論 1 8
  • 關(guān)注倉(cāng)庫(kù)辛孵,及時(shí)獲得更新:iOS-Source-Code-AnalyzeFollow: Draveness · Gi...
    Draveness閱讀 6,034評(píng)論 5 33
  • fishhook 用于替換 iOS 程序中動(dòng)態(tài)庫(kù)的符號(hào),常被用來 hook 系統(tǒng)中的 C 函數(shù)赡磅。 fishhook...
    gbupup閱讀 1,057評(píng)論 0 10
  • 關(guān)鍵時(shí)刻魄缚,第一時(shí)間送達(dá)! 問題種類 時(shí)間復(fù)雜度 在集合里數(shù)據(jù)量小的情況下時(shí)間復(fù)雜度對(duì)于性能的影響看起來微乎其微焚廊。但...
    C9090閱讀 893評(píng)論 0 1
  • 模擬登陸 以模擬登錄兄弟連猿代碼為例 四個(gè)步驟無需贅述了:初始化冶匹,配置,發(fā)送請(qǐng)求獲取數(shù)據(jù)咆瘟,關(guān)閉連接嚼隘。 配置的部分為...
    王寶花閱讀 4,297評(píng)論 0 1