HOOK,中文譯爲「掛鉤」或「鉤子」。在iOS逆向中是指改變程序運行流程的一種技術。 例如,一個正常的程序運行流程是A->B->C,經過hook技術可讓程序的執行變成A->咱們本身的代碼->B->C。在這個過程當中,咱們的代碼能夠獲取到A傳遞B的數據,對其進行修改或利用再傳遞給B,而A,B是不會感知到這個過程的。因此,經過hook可讓別人的程序執行本身所寫的代碼。在逆向中常用這種技術。在學習過程當中,咱們重點要了解其原理,這樣可以對惡意代碼進行有效的防禦。在iOS系統中有如下三種方式能夠實現hook,這篇文章主要講究fishhook的使用及其原理。git
利用OC的runtime api,動態改變SEL和IMP的對應關係,達到方法調用流程改變的目的。主要用於OC方法github
它是Facebook開源的一個動態修改連接Mach-O文件的工具。利用dyld加載Mach-O文件的原理,經過修改懶加載和非懶加載兩個表的指針達到hook系統動態庫函數的目的。主要用於系統庫的C函數。api
Cydia Substrate原名爲Mobile Substrate,它的主要做用是針對OC方法,C函數以及函數地址進行Hook操做。而且它並非僅僅針對iOS設計的,在安卓平臺同樣可使用。官方地址www.cydiasubstrate.com數組
不少文章裏介紹說是使用Method Swizzling來進行方法交換的,但其實吧,這兩單詞翻譯過來就是方法交換的意思。代入原句就很彆扭了,明明就是使用runtime提供的api來達到Mehtod Swizzling方法交換的目的。其實runtime裏面除了使用method_exchangeImplementations()
來實現方法的交換之外,還可使用class_replaceMethod()
方法替換實現,也可使用method_getImplementation()
和method_setImplementation
搭配使用實現。這個部分的內容在我前面的文章代碼注入中已經講過了,感興趣的讀者能夠自行前往查看。緩存
方法交換在正向開發中可用於埋點,數據監控統計,防止崩潰等,在iOS逆向工程中能夠經過對某個方法進行攔截和修改達到修改邏輯和數據的目的,在後面的實戰中會大量使用該技術。安全
fishhook利用了dyld加載Mach-O文件的原理,dyld從iOS13開始,從dyld2更新到了dyld3,目前看來,對fishhook的影響不是很大,依然能夠正常使用。這裏貼上github對這個問題的討論 fishhook with dyld 3.0 #43markdown
從github下載fishhook源碼,能夠看到fishhook源碼就一個.c和.h加起來不到300行代碼。新建一個工程,並添加如下代碼: 這裏須要講一下fishhook提供的api就兩個,都是c語言的函數。rebing_symbols
函數須要兩個參數,第一個參數是一個rebinding
結構體的數組,第二個參數是數組的個數。架構
rebinding
結構體是fishhook提供的
ide
name
字段表示須要hook的函數的名稱。replaced
字段這裏須要傳入一個函數指針,用來保存被hook的函數的原始實現。replacement
傳入我們本身實現,用來替換的的函數。點擊屏幕,發現咱們的輸出帶上了後綴,表示hook成功了!!! 函數
沒有交換成功,爲何fishhook能夠交換系統的C函數,而沒法交換咱們自定義的C函數呢,請看下面的原理
iOS工程師們常常會聽到說Objective-C是一門動態語言,而C是一門靜態語言,這裏說的動態和靜態,具體是指什麼呢?主要區別在於編譯時肯定,仍是運行時肯定。那麼這個肯定,是指肯定什麼呢,好比變量的具體類型,函數的具體實現等...下面舉個例子,在工程中聲明一個OC方法,不寫定義代碼,和聲明一個C方法,一樣不寫定義代碼。編譯一下,查看編譯器是否經過? 雖然Xcode給出了一個警告⚠️test
方法定義未找到,但仍是編譯經過了。 而C語言聲明的一個函數func
,Xcode提示編譯未經過,報錯Undefined symbol:_func
未定義的符號_func
,這裏系統自動給func
加上下劃線。Objective-C的動態特性使咱們可使用runtime來hook,而C的靜態特性決定了它的函數實如今編譯期就肯定了,是沒法進行hook的,那麼爲何咱們iOS系統的C函數可以被fishhook交換呢?
iOS中使用了共享緩存技術,每一個APP進程都會用到的系統庫,好比UIKit,Foundation...都會被放到共享緩存庫中,在個人上篇文章dyld中講到過,感興趣的同窗能夠前往查看
由於C函數是靜態的,在編譯的時刻,就須要有一個C函數的實現地址。而iOS因爲有了共享緩存機制,使得咱們APP內調用的系統函數,經過dyld加載進內存的時候,纔會綁定系統函數在共享緩存中的地址。這裏存在一個矛盾,編譯器在編譯C函數的時候,必需要一個地址,但共享緩存的存在,讓咱們實際的地址只有在運行的時刻才能知道。因此蘋果使用PIC技術。
根據當前APP中調用到了的系統庫函數的符號(好比NSLog),在Mach-O的Data段(Data段可讀寫)創建了了懶加載表和非懶加載表,在編譯的時候,就使用對應的符號地址,這個時候的地址是內存中的隨機值,僅僅是爲了經過編譯。在程序啓動dyld執行完綁定時,這個時候纔將共享緩存中真正的實現地址找到並賦值給咱們的符號。
理論講了那麼多,怎麼驗證咱們講的是否是對的?
根據經驗咱們知道NSLog符號是懶加載的,那咱們就以NSLog符號舉例。咱們能夠新建一個嶄新的工程,什麼代碼也不寫,直接查看Mach-O文件的懶加載符號指針以下圖,是找不到NSLog的。 而後咱們在任何位置,添加一句NSLog打印代碼: 以後再次查看Mach-O文件的懶加載符號指針: 發現多了一個地址,這個就是咱們NSLog符號在咱們Mach-O文件中的地址。這裏證實了系統確實是根據咱們項目裏面用到的庫函數,來創建的符號表的。
咱們回到交換NSLog函數的代碼,在調用fishhook重綁定前,添加一行NSLog輸出代碼,再分別在NSLog打印前,打印後,和fishhook重綁定後打上三個斷點: 再次運行,到第一個斷點,使用LLDB的image list
命令查看咱們當前程序加載的鏡像以及地址,咱們只須要第一個鏡像,也就是咱們當前APP本身的Mach-O的內存地址 再打開Mach-O文件查看NSLog符號的地址 須要注意的是,Mach-O文件中NSLog符號的地址是至關於當前文件的偏移,再加上當前Mach-O文件在內存中的地址,就是上一步獲得的。相加的值纔是NSLog符號在內存中的地址。使用LLDB命令memory read 地址
能夠查看內存中的值,我這裏加起來是0x1024F0000
,再使用dis -s 地址
查看反彙編,發現如今啥也不是。 這個時候,放掉第一個斷點,來到第二個斷點,此時NSLog就已經打印一次了,那麼咱們NSLog符號的地址裏面存放的應該就是Foundation中NSLog的實現。驗證一下: 沒有問題,咱們來到最後一個斷點,這個時候fishhook執行了重綁定代碼,那麼咱們看看如今NSLog符號是否是指向了咱們的實現:
能夠看到,fishhook其實就是修改懶加載符號表,非懶加載符號表中符號指向的地址,從而達到hook的目的。
fishhook的實現代碼不過200多行,分析這200多行代碼,須要對Mach-O文件有必定的理解。若是有興趣的能夠查看我以前的文章Mach-O文件,若是不瞭解Mach-O文件的話,那這200多行的代碼就有點像天書...
先看一下fishhook的頭文件 頭文件很是簡單,一個rebinding
結構體,兩個C函數,都是用來重綁定的,其中一個不須要指定鏡像和ASLR的偏移,另外一個須要。通常推薦仍是使用不須要指定鏡像和偏移的,畢竟這兩個參數咱們也不太好弄到...(可使用dyld提供的一些api獲取,但不是很方便,dyld的api在mach-o/loader.h,使用Xcode快捷鍵command + shift + o打開),並且我在使用指定鏡像的這個api的時候,會出現只綁定成功一次的狀況...
再來看實現代碼,這裏就只分析int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel)
這個不須要指定鏡像的。 首先調用prepend_rebindings()
來作一些準備工做,rebind_symbols
使用鏈表來存儲每一次調用本身時,傳入的參數,每次調用的時候和參數一塊兒構成新的一張表,新表的next指向舊的表,這樣每次調用的參數都保存下來了... 根據_rebindings_head->next
是否有值,就能夠判斷出是不是第一次調用rebind_symbols
,若是是第一次調用,就調用註冊監聽函數_dyld_register_func_for_add_image()
,已經被dyld加載的鏡像會馬上執行回調,以後加載的鏡像,會在dyld加載的時候觸發回調。若是不是第一次調用了,就不須要重複註冊回調了,直接遍歷全部鏡像進行重綁定。 回調函數_rebind_symbols_for_image
裏面調用本身的重綁定函數。
接下來看rebind_symbols_for_image
的實現
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;
uintptr_t size = sizeof(mach_header_t);
uintptr_t cur = (uintptr_t)header + size;
// 循環遍歷Mach-O文件的Load Commands,找到上面3個須要的Load Command
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;
}
}
//若是剛纔獲取的,有一項爲空就直接返回,dysymtab_cmd->nindirectsyms意思LC_DYSYMTAB加載命令中 間接符號表個數的意思 小於0意思沒有就不執行後面的代碼了
if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment || !dysymtab_cmd->nindirectsyms) {
return;
}
複製代碼
這一步就是從Mach-O文件中的Load Commands中找到想要的加載命令,分別是LC_SYMTAB,LC_DYSYMTAB和__LINKEDIT段,下一步根據這幾個Load Command分別找到符號表,字符串表和間接符號表的地址
// 鏡像文件頭在內存中的地址簡稱基址 = __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);
//動態符號表地址 = 基址 + 動態符號表偏移量
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
複製代碼
接着,又是遍歷一遍Load Commands,找到咱們的非懶加載符號表,和懶加載符號表,這個過程判斷比較多,由於非懶加載符號表和懶加載符號表在__DATA_CONST段的__got節和__DATA段的__la_symbol_ptr節中
cur = (uintptr_t)header + sizeof(mach_header_t);
// 又是遍歷一遍Load Commands,若是是LC_SEGMENT_64或LC_SEGMENT加載命令,那麼找到名字爲__DATA_CONST或__DATA的segment
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_CONST或__DATA的segment
if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 && strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
continue;
}
// 找到section爲S_LAZY_SYMBOL_POINTERS或者S_NON_LAZY_SYMBOL_POINTERS的section
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);
}
}
}
}
複製代碼
LC_SEGMENT_ARCH_DEPENDENT
是一個針對不一樣架構的宏,對應的是普通的段或者64位架構的段
#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
複製代碼
找到了懶加載表或者非懶加載表以後,就開始執行真正的重綁定邏輯了perform_rebinding_with_section()
//__got和__la_symbol_ptr section中的reserved1字段指明對應的indirect_symbol table起始的index
uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
//slide + section->addr 就是符號對應的存放函數實現的數組
//也就是我相應的__got和__la_symbol_ptr相應的函數指針都在這裏面了,因此能夠去尋找到函數的地址
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
複製代碼
上面兩個變量,一個用來尋找符號,一個用來尋找符號對應的函數實現地址,而後是遍歷兩個表裏面的每個符號,每個符號都跟鏈表裏面的咱們傳入的name
匹配,若是一致就說明找到了要hook的符號,而後將符號對應的原始函數實現地址,賦值給咱們用來保存的變量replaced
,再將咱們自定義函數的地址賦值給符號保存;這樣,咱們APP代碼調用符號函數的時候,就先來到了咱們自定義函數的邏輯,若是咱們在自定義的函數邏輯裏,調用保存的原始函數實現,就實現了hook,代碼以下
//遍歷section裏面的每個符號
unsigned long long count = section->size / sizeof(void *);
for (uint i = 0; i < count; 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;
//判斷是否函數的名稱是否有兩個字符,爲啥是兩個,由於C函數前面有個_,因此函數的名稱最少要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++) {
struct rebinding one = cur->rebindings[j];
//這裏if的條件就是判斷從symbol_name[1],從1開始去掉了_,兩個函數的名字是否都是一致的
if (symbol_name_longer_than_1 && strcmp(&symbol_name[1], one.name) == 0) {
//判斷replaced的地址不爲NULL以及原始方法的實現和rebindings[j].replacement的方法不一致
if (one.replaced != NULL && indirect_symbol_bindings[i] != one.replacement) {
//讓rebindings[j].replaced保存indirect_symbol_bindings[i]的函數地址
*(one.replaced) = indirect_symbol_bindings[i];
}
//將替換後的方法給原先的方法,也就是替換內容爲自定義函數地址,
indirect_symbol_bindings[i] = one.replacement;
goto symbol_loop;
}
}
cur = cur->next;
}
symbol_loop:;
}
複製代碼
顧名思義用於HOOK。它定義一系列的宏和函數,底層調用objc的runtime和fishhook來替換系統或者目標應用的函數。其中有兩個函數:
MSHookMessageEx
主要做用於Objective-C方法MSHookFunction
主要做用於C和C++函數,Logos語法的%hook就是對此函數作了一層封裝。MobileLoader用於加載第三方dylib在運行的應用程序中。啓動時MobileLoader會根據規則把指定目錄的第三方的動態庫加載進去,第三方的動態庫也就是咱們寫的破解程序。
破解程序本質是dylib,寄生在別人進程裏。系統進程一旦出錯,可能致使整個進程崩潰,崩潰後就會形成iOS癱瘓。因此CydiaSubstrate引入了安全模式,在安全模式下全部基於Cydia Substratede的三方dylib都會被禁用,便於查錯與修復。
利用fishhook修改runtime的相關api,好比上面所講的method_exchangeImplementations
等等,但須要最早加載,不然無效,放在工程的Framework中最好,這樣別人沒法使用第三方Framework插入的方式進行代碼注入了,使用過yololib工具注入的同窗會發現,插入的Framework只能放在Load Commands的最後一條,那樣咱們本身的Framework確定在前面,這樣就能夠屏蔽惡意代碼注入了。