距離上個文章已經有段時間了,雖然沒多少人閱讀但是好在自娛自樂,前段時間出去受虐一波,所以又開始發憤圖強,開始向以前比較常用的一些厲害的開源庫學習學習.閒話不多說,fishhook是facebook出的一款可以修改外鏈的C方法(非自己所寫的方法,一般存在於app啟動時使用dyld載入的動態庫中)的一個庫,整個檔案只有200多行程式碼.
用法
假如你在使用一個三方的framework的時候它裡面無時無刻的列印著一些無用的資訊而且市面上沒有很好的去替代它的產品,這時候就可以把fishhook
請出來將對應的列印函式給hook掉,比如
static void (*orig_printf)(char *format, ...);
int main(int argc, const char * argv[]) {
printf("abcd");
return 0;
}
static void my_printf(const char * s, ...){
//這裡可以寫你想替換的程式碼
//比如 orig_printf("dcba");
}
__attribute((constructor)) void injected_function(){
rebind_symbols((struct rebinding[1]){{"printf", my_printf, (void *)&orig_printf}},1);
}
複製程式碼
這樣就成功的在main函式呼叫之前就將printf
函式給替換了,可以遠離煩人的列印.
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel)
的第一個引數是個結構體陣列
struct rebinding {
const char *name;
void *replacement;
void **replaced;
};
複製程式碼
name
是想要hook的的函式名稱,replacement
是替換後的函式指標,replaced
是傳入一個指向函式指標的指標(如果將函式成功的替換後,會將原函式的值給放入其中)
當然在很多情況中我們會在逆向中用到fishhook,並且fishhook只能去替換連結外部動態庫中的程式碼,自己寫的C函式不能去替換
Mach-O
在去閱讀fishhook
的時候我個人認為是必須對Mach-O
有一些瞭解。Mach-O
是iOS/MacOS下面的可執行檔案,在iOS工程下使用command+b
編譯後會在Products
目錄下生成一個.app
檔案,..app
中一個同名檔案就是我們的Mach-O
檔案了,它裡面包含了我們app的類,方法,以及編譯期間就確定常量等內容。
- Header:儲存了一些Mach-O的基本資訊,比如是32位/64位,LoadCommand的個數等
- LoadCommand: 這一段跟著Header排布,載入Mach-O時會從這裡面獲取到對應的資料來確定記憶體分佈
- Data: 這裡面儲存了具體的資料,裡面細分為多個segment,segment在分為多個section,這裡麵包含了具體的程式碼與資料等資訊
我們實際用工具去看下Mach-O,新建一個iOS工程,什麼程式碼都不加,直接編譯一下,然後使用MachOView
檢視 因為本文關注的是fishhook
,我們主要關注__Data
段中的__la_symbol_ptr
這個section(這個section表示的是懶載入的符號表,如果我們自己寫的函式會在編譯時確定地址寫入macho,而系統的例如printf這些編譯期間是不確認地址的),然後與之相關的還需要關注Symbol Table
、Dynamic Symbol Table
與__LINKEDIT
段
因為MachO裡面的東西太多就不過多的去描述,大概有些瞭解知道它是什麼東西就可以閱讀fishhook原始碼,如果想要深入的瞭解MachO可以看這篇部落格
閱讀原始碼
下面就開始進入正題了,開始去閱讀fishhook的原始碼。我們直接從我們的呼叫函式看int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel)
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
if (retval < 0) {
return retval;
}
// If this was the first call, register callback for image additions (which is also invoked for
// existing images, otherwise, just run on existing images
if (!_rebindings_head->next) {
_dyld_register_func_for_add_image(_rebind_symbols_for_image);
} else {
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;
}
複製程式碼
首先呼叫了prepend_rebindings
函式,傳入了三個引數,第一個引數是指向私有結構體static struct rebindings_entry *_rebindings_head
的指標,第二個引數struct rebinding
結構體陣列,第三個值是結構體陣列的長度
static int prepend_rebindings(struct rebindings_entry **rebindings_head,
struct rebinding rebindings[],
size_t nel) {
struct rebindings_entry *new_entry = malloc(sizeof(struct rebindings_entry));
if (!new_entry) {
return -1;
}
new_entry->rebindings = 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;
}
複製程式碼
這裡面的程式碼比較簡單
- 初始化一個
struct rebindings_entry
結構體 - 在將結構體中陣列初始化
- 將我們傳入的結構體陣列的值copy到剛初始化的陣列中
- 在將新初始化的結構體放在這個連結串列的最前面
在往下走看到以連結串列的next指標判斷該方法是否第一次呼叫,如果第一次呼叫則呼叫
_dyld_register_func_for_add_image
方法,並傳入_rebind_symbols_for_image
函式指標
_dyld_register_func_for_add_image 註冊自定義的回撥函式,同時也會為所有已經載入的動態庫或可執行檔案執行回撥
每個動態庫都會回撥_rebind_symbols_for_image
這個方法,然後這個方法只是對rebind_symbols_for_image
的一個封裝,rebind_symbols_for_image
實現程式碼比較長我們可以分成兩個部分去看
static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
const struct mach_header *header,
intptr_t slide) {
Dl_info info;
if (dladdr(header, &info) == 0) {
return;
}
segment_command_t *cur_seg_cmd;
segment_command_t *linkedit_segment = NULL;
struct symtab_command* symtab_cmd = NULL;
struct dysymtab_command* dysymtab_cmd = NULL;
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;
}
}
...
}
複製程式碼
這段程式碼主要是為了獲取對應的Symbol Table
、Dynamic Symbol Table
與__LINKEDIT
段對應的結構體
- 因為LoadCommand是緊跟著mac_header的 所以
uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
獲取到第一個LoadCommand的位置 - 之後遍歷根據cmd的值去取得對應的結構體
在往下看
static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
const struct mach_header *header,
intptr_t slide) {
...
// Find base symbol/string table addresses
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) {
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);
}
}
}
}
}
複製程式碼
首先是可以根據上面程式碼得到個結論,程式的基地址 = sild + __LINKEDIT->vmaddr - __LINKEDIT->fileoff
,這裡面的__LINKEDIT->vmaddr
是__LINKEDIT
在記憶體中的地址,fileoff
是__LINKEDIT
在mach-o檔案中的偏移量,那麼silde
是什麼?其實silde
就是ASLR
,那ASLR
又是什麼?ASLR:Address space layout randomization
,通俗的說就是在app每次啟動的時候會隨機給一個地址偏移量,然後我們真正的記憶體地址就是Mach-O
中的地址加上這個偏移量。得到程式的基地址後在根據符號表中的偏移值得到符號表中的資料,之後在遍歷一遍LoadCommand,尋找__DATA
和__DATA_CONST
的section,並對對__nl_symbol_ptr
以及__la_symbol_ptr
進行重新繫結。
接下來呼叫了perform_rebinding_with_section
函式
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) {
uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
for (uint i = 0; i < section->size / sizeof(void *); i++) {
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;
}
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
char *symbol_name = strtab + strtab_offset;
bool symbol_name_longer_than_1 = symbol_name[0] && symbol_name[1];
struct rebindings_entry *cur = rebindings;
while (cur) {
for (uint j = 0; j < cur->rebindings_nel; j++) {
if (symbol_name_longer_than_1 &&
strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
if (cur->rebindings[j].replaced != NULL &&
indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
}
indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
goto symbol_loop;
}
}
cur = cur->next;
}
symbol_loop:;
}
}
複製程式碼
這段函式看起來稍微有點長,但是邏輯是很好去理解的,首先先根據動態符號表中地址 + 在符號表中的index 獲得在該段在動態符號表中的位置(reserved1
的值表示偏移量),進行一個for迴圈在每次迴圈中獲取到對應方法的方法名,然後在遍歷私有結構體連結串列struct rebindings_entry *
,把連結串列中的每個結構體中的陣列中的方法名與當前的表中的方法名比較,如果相同就將符號表中的指標資訊儲存給外面呼叫時傳入的指向函式指標的指標中,在表中的指標替換成我們傳入的函式指標,就這樣就完成了一次偷天換日的過程。
最後借官方的圖