HOOK
建議大家先對MachO有一定的了解,因為下面的內容會涉及到MachO里面的內容3、iOS強化 --- Mach-O 文件
HOOK,中文譯為“掛鉤”或“鉤子”。在iOS逆向中時指改變程序運行流程的一種技術。通過HOOK可以讓別人的程序執行自己所寫的代碼。在逆向中經常使用這種技術。
下面我們來簡單看一下HOOK的工作流程是怎樣的。
iOS中HOOK的幾種方式
- 1、
Method Swizzle
利用OC的Runtime
特性,動態改變SEL(方法編號)
和IMP(方法實現)
的對應關系,達到OC方法調用流程改變的目的。主要用于OC方法。這個我們在10、代碼的注入里面有詳細講過。 - 2、
fishHook
它是Facebook提供的一個動態修改鏈接Mach-O
文件的工具。利用Mach-O
文件加載原理,通過修改懶加載
和非懶加載
兩個表的指針達到C函數
HOOK的目的。 - 3、
Cydia Substrate
Cydia Substrate
原名Mobile Substrate
,它的主要作用是針對OC方法、C函數以及函數地址進行HOOK。當然它并不是針對iOS而設計的,Android一樣可以使用。官網地址
fishHook
這里我們簡單的使用一下fishHook
。fishHook地址
fishHook
只給我們提供了兩個函數和一個結構體
FISHHOOK_VISIBILITY
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);
// 參數一 : 存放rebinding結構體的數組(可以同時交換多個函數)
// 參數二 : rebinding數組長度
// 跟上面的函數比起來,這個函數可以指定image(鏡像文件)
FISHHOOK_VISIBILITY
int rebind_symbols_image(void *header,
intptr_t slide,
struct rebinding rebindings[],
size_t rebindings_nel);
struct rebinding {
const char *name;//需要HOOK的函數名稱,C字符串
void *replacement;//新函數的地址
void **replaced;//原始函數地址的指針!
};
下面我們就來使用一下fishHook
。
- 首先將
fishHook
文件拖進我們的工程,并且在要使用fishHook
的類里面,引用fishHook
:
引入fishHook - HOOK 系統函數
NSLog
HOOK NSLog
運行結果:
HOOK結果
可以看到,已經HOOK
成功了。這里有一點要跟大家講清楚,就是fishHook
在使用的過程中sys_NSLog
怎么就跟NSLog
聯系上了呢?
其實是這個樣子的,我們在HOOK
過程中:
i
:fishHook
將NSLog(系統的)
的真實地址,賦給了sys_NSLog
,注意sys_NSLog
只是一個函數指針。
ii
:緊接著將NSLog
指向我們自定義的my_NSLog
。
iii
:這樣在執行完my_NSLog
的代碼之后,我們可以利用sys_NSLog
回到原本的業務邏輯中,不影響原始業務的執行。
-
fishHook源碼探究
上面我們簡單的講了一下fishHook
的運行機制,那么我們接下來就來動態調試一下。
首先我們進入rebind_symbols
這個方法去看一下,它里面的實現:
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
//prepend_rebindings的函數會將整個 rebindings 數組添加到 _rebindings_head 這個鏈表的頭部
//Fishhook采用鏈表的方式來存儲每一次調用rebind_symbols傳入的參數,每次調用,就會在鏈表的頭部插入一個節點,鏈表的頭部是:_rebindings_head
int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
//根據上面的prepend_rebinding來做判斷,如果小于0的話,直接返回一個錯誤碼回去
if (retval < 0) {
return retval;
}
//根據_rebindings_head->next是否為空判斷是不是第一次調用。
if (!_rebindings_head->next) {
//第一次調用的話,調用_dyld_register_func_for_add_image注冊監聽方法.
//已經被dyld加載的image會立刻進入回調。
//之后的image會在dyld裝載的時候觸發回調。
_dyld_register_func_for_add_image(_rebind_symbols_for_image);
} else {
//遍歷已經加載的image,進行的hook
uint32_t c = _dyld_image_count();
for (uint32_t i = 0; i < c; i++) {
_rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));
}
}
return retval;
}
根據代碼,我們可以看到,首先會有一個retval
的判斷,那么我們來看看prepend_rebindings
這個方法
static int prepend_rebindings(struct rebindings_entry **rebindings_head,
struct rebinding rebindings[],
size_t nel) {
struct rebindings_entry *new_entry = (struct rebindings_entry *) malloc(sizeof(struct rebindings_entry));
if (!new_entry) {
return -1;
}
new_entry->rebindings = (struct rebinding *) malloc(sizeof(struct rebinding) * nel);
if (!new_entry->rebindings) {
free(new_entry);
return -1;
}
memcpy(new_entry->rebindings, rebindings, sizeof(struct rebinding) * nel);
new_entry->rebindings_nel = nel;
new_entry->next = *rebindings_head;
*rebindings_head = new_entry;
return 0;
}
可以看到*rebindings_head = new_entry;
,也就是我們之前傳入的&_rebindings_head
。我們再來看一下_rebindings_head
是什么:
struct rebindings_entry {
struct rebinding *rebindings;
size_t rebindings_nel;
struct rebindings_entry *next;
};
static struct rebindings_entry *_rebindings_head;
到這里不難看出_rebindings_head
其實就是一個鏈表的表頭。
下面我們繼續在rebind_symbols
方法里面往下走,這里跟大家說一下,我們在動態調試的時候,無論是第一次調用還是第N次調用,都會進入_rebind_symbols_for_image
,這里大家只要打斷點調試一下就可以看到。那么我們直接去_rebind_symbols_for_image
方法。
/*****************************************/
// _rebind_symbols_for_image
/*****************************************/
static void _rebind_symbols_for_image(const struct mach_header *header,
intptr_t slide) {
rebind_symbols_for_image(_rebindings_head, header, slide);
}
??
/*****************************************/
// rebind_symbols_for_image
/*****************************************/
//回調的最終就是這個函數! 三個參數:要交換的數組 、 image的頭 、 ASLR的偏移
static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
const struct mach_header *header,
intptr_t slide){}
rebind_symbols_for_image
方法中有一個函數,要先跟大家講清楚,那就是dladdr()
。
-
dladdr()
:用來確認指定的address
是否位于構成進程的地址空間中的其中一個加載模塊內(可執行庫或共享庫)。
○:如果某個地址位于在其上面映射加載模塊的基址,和為該加載模塊映射的最高虛擬地址之間(包括兩端),則認為該地址在加載模塊的范圍內。
○:如果某個加載模塊符合這個條件,則會搜索其動態符號表,以查找與指定的adress
最接近的符號。最接近的符號是指其值等于,或最為接近但小于指定的adress
的符號。
那么dladdr()
執行會有什么效果呢?拿下面代碼中的來說:
i
:如果指定的adress
不再其中一個加載模塊的范圍內,則返回0
;且不修改Dl_info
結構的內容。否則,返回一個非零值
,同時設置Dl_info
結構的字段。
ii
:如果指定的adress
在加載模塊的范圍內,找不到其值小于或等于adress
的符號,則dli_sname
、dli_saddr
、dli_size
字段將設置為0
;dli_bind
字段設置為STB_LOCAL
,dli_type
字段設置為STT_NOTYPE
。
//回調的最終就是這個函數! 三個參數:要交換的數組 、 image的頭 、 ASLR的偏移
static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
const struct mach_header *header,
intptr_t slide) {
//這個dladdr函數就是在程序里面找header
Dl_info info;
if (dladdr(header, &info) == 0) {
return;
}
//下面就是定義好幾個變量,準備從MachO里面去找!
segment_command_t *cur_seg_cmd;
segment_command_t *linkedit_segment = NULL;
struct symtab_command* symtab_cmd = NULL;
struct dysymtab_command* dysymtab_cmd = NULL;
//跳過header的大小,找loadCommand
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;
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
linkedit_segment = cur_seg_cmd;
}
} else 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 || !linkedit_segment ||
!dysymtab_cmd->nindirectsyms) {
return;
}
// Find base symbol/string table addresses
//鏈接時程序的基址 = __LINKEDIT.VM_Address -__LINKEDIT.File_Offset + silde的改變值
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
//符號表的地址 = 基址 + 符號表偏移量
nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
//字符串表的地址 = 基址 + 字符串表偏移量
char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
// Get indirect symbol table (array of uint32_t indices into symbol table)
//動態符號表地址 = 基址 + 動態符號表偏移量
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
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) {
//尋找到data段
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
。那我們就繼續跟進perform_rebinding_with_section
,這里面就有意思了,有我們想要的HOOK;我們上面提到的函數指針的替換,就在這里面:
static void perform_rebinding_with_section(struct rebindings_entry *rebindings,
section_t *section,
intptr_t slide,
nlist_t *symtab,
char *strtab,
uint32_t *indirect_symtab) {
//nl_symbol_ptr和la_symbol_ptrsection中的reserved1字段指明對應的indirect symbol table起始的index
uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
//slide+section->addr 就是符號對應的存放函數實現的數組也就是我相應的__nl_symbol_ptr和__la_symbol_ptr相應的函數指針都在這里面了,所以可以去尋找到函數的地址
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
//遍歷section里面的每一個符號
for (uint i = 0; i < section->size / sizeof(void *); i++) {
//找到符號在Indrect Symbol Table表中的值
//讀取indirect table中的數據
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;
}
//以symtab_index作為下標,訪問symbol table
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
//獲取到symbol_name
char *symbol_name = strtab + strtab_offset;
//判斷是否函數的名稱是否有兩個字符,為啥是兩個,因為函數前面有個_,所以方法的名稱最少要1個
bool symbol_name_longer_than_1 = symbol_name[0] && symbol_name[1];
//遍歷最初的鏈表,來進行hook
struct rebindings_entry *cur = rebindings;
while (cur) {
for (uint j = 0; j < cur->rebindings_nel; j++) {
//這里if的條件就是判斷從symbol_name[1]兩個函數的名字是否都是一致的,以及判斷兩個
if (symbol_name_longer_than_1 &&
strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
//判斷replaced的地址不為NULL以及我方法的實現和rebindings[j].replacement的方法不一致
if (cur->rebindings[j].replaced != NULL &&
indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
//讓rebindings[j].replaced保存indirect_symbol_bindings[i]的函數地址
*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
}
//將替換后的方法給原先的方法,也就是替換內容為自定義函數地址
indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
goto symbol_loop;
}
}
cur = cur->next;
}
symbol_loop:;
}
}
最關鍵的兩句代碼:
//讓rebindings[j].replaced保存indirect_symbol_bindings[i]的函數地址
*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
//將替換后的方法給原先的方法,也就是替換內容為自定義函數地址
indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
這就與我們之前分析的fishHook
的HOOK
過程對應起來了。
總結:
- 我們利用
fishHook
對系統方法NSLog
進行了HOOK
,這是利用了Runtime
的特性;這是因為間接符號邊在編譯的時候,并沒有確定函數的真實地址。只有在運行時在確定,所以我們可以HOOK
。 - 如果大家有興趣,可以自己定義一個
C函數
,看看能不能HOOK
。這里先告訴大家,答案是否定的。雖然NSLog
也是一個C函數
,但是它是外部符號,我們自己在工程中定義的C函數
屬于內部符號,在編譯鏈接的時候,會變成地址調用,也就是說會變成類似于這樣的:b 0x00000119890
。那么我們再利用fishHook
就無法HOOK
到了。這里大家有興趣可以試一下(這個之后我們會去探索inlinHook
,會去探究靜態函數的HOOK
)。 - 在上面的代碼注釋里面,有提到
slide
,這就是涉及到PIC
計數,也就是ASLR
。這個知識點大家要去了解一下,這個以后會經常提到。簡單講,我們的APP包里面的函數地址,或者說我們通過MachOView查看我們的可執行程序時,看到的地址都是虛擬地址,在APP運行在手機上的時候都是會變的。將虛擬地址和ASLR
地址結合得到我們要使用的真實地址。