iOS逆向之深入解析Hook的原理方法和安全防護(hù)
Hook 簡(jiǎn)介
Hook就是一種改變程序執(zhí)行流程的一種技術(shù)的統(tǒng)稱(chēng);
一段程序的執(zhí)行流程是 A --> B --> C,現(xiàn)在我們?cè)?A 和 B 之間插入一段代碼或者直接改變 B ,這樣程序原有的執(zhí)行流程就發(fā)生了改變。如下圖所示:
Hook的方式:Method Swizzle,fishhook,Cydia Substrate;
Hook 原理
利用OC的Runtime特性,動(dòng)態(tài)去改變SEL(方法編號(hào))和IMP(方法實(shí)現(xiàn))的對(duì)應(yīng)關(guān)系,達(dá)到OC方法調(diào)用流程改變的目的主要用于OC方法。
Hook中主要用到的方法(參數(shù): Class、SEL、IMP、Method):
// 1、方法交換 OBJC_EXPORT void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); // 2、替換方法 OBJC_EXPORT IMP _Nullable class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); // 3、setIMP & getIMP OBJC_EXPORT IMP _Nonnull method_setImplementation(Method _Nonnull m, IMP _Nonnull imp) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); OBJC_EXPORT IMP _Nullable class_getMethodImplementation(Class _Nullable cls, SEL _Nonnull name) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Class 一個(gè) objc_class 類(lèi)型的結(jié)構(gòu)體指針,用于說(shuō)明對(duì)象是哪個(gè)類(lèi);
Method 一個(gè) objc_method 類(lèi)型的結(jié)構(gòu)體指針,用于定義一個(gè)方法,在objc源碼中定義如下:
struct objc_method { SEL _Nonnull method_name OBJC2_UNAVAILABLE; char * _Nullable method_types OBJC2_UNAVAILABLE; IMP _Nonnull method_imp OBJC2_UNAVAILABLE; } OBJC2_UNAVAILABLE;
1
2
3
4
5
SEL 可以發(fā)現(xiàn)在 Method 的定義中,也能看見(jiàn)SEL。在蘋(píng)果官方文檔中定義這種類(lèi)型:
typedef struct objc_selector *SEL;
1
IMP 同樣在 Method 的定義中也能看到,在蘋(píng)果官方文檔的定義如下:
id (IMP *)(id, SEL, ...)
1
SEL是一個(gè)C String,用于表示一個(gè)方法的名稱(chēng),IMP是一個(gè)方法實(shí)現(xiàn)首地址,默認(rèn)有兩個(gè)參數(shù) self 和 _cmd。其實(shí)SEL和IMP的關(guān)系可以類(lèi)比一本書(shū)的目錄,SEL就是目錄中的內(nèi)容標(biāo)題,IMP是后面的頁(yè)碼。一個(gè)方法調(diào)用時(shí),通過(guò)SEL找到對(duì)應(yīng)的IMP,進(jìn)而找到方法的實(shí)現(xiàn)。如下圖:
在Hook一個(gè)OC方法時(shí),只需要改變其SEL所指向的IMP時(shí),就可以實(shí)現(xiàn)方法的交換的目的,或者使用class_replaceMethod 和 method_setImplementation 改變一個(gè)類(lèi)原有方法的實(shí)現(xiàn),原理如下圖:
逆向中,Hook一個(gè)OC方法其實(shí)就是改變其SEL所指向的IMP,從而找到另一個(gè)實(shí)現(xiàn)地址,執(zhí)行另一個(gè)方法實(shí)現(xiàn)。但這種方法的局限在于,其只能針對(duì)OC方法進(jìn)行Hook,對(duì)于C函數(shù)則無(wú)法Hook。
fishhook主要利用了共享緩存功能和PIC技術(shù)來(lái)實(shí)現(xiàn)hook功能。
fishHook是由faceBook開(kāi)發(fā)的,是一個(gè)動(dòng)態(tài)修改MachO文件的工具,主要是通過(guò)修改懶加載和非懶加載表里的指針的指向來(lái)達(dá)到hook的目的,因此一般用它來(lái)hook系統(tǒng)的C函數(shù)。
DATA區(qū)有兩個(gè)section和動(dòng)態(tài)符號(hào)鏈接相關(guān):__nl_symbol_ptr 、__la_symbol_ptr;__nl_symbol_ptr為一個(gè)指針數(shù)組,直接對(duì)應(yīng)non-lazy綁定數(shù)據(jù)。__la_symbol_ptr也是一個(gè)指針數(shù)組,通過(guò)dyld_stub_binder輔助鏈接。
fishhook實(shí)現(xiàn):根據(jù)符號(hào)(字符)獲取系統(tǒng)函數(shù)地址;替換符號(hào)指向的地址為用戶(hù)聲明的函數(shù)地址(符號(hào)綁定);對(duì)外部聲明的指針進(jìn)行系統(tǒng)函數(shù)地址賦值。
示例說(shuō)明
static void (* old_log)(NSString* str); void newLog(NSString * str){ str = [str stringByAppendingString:@"\n 勾住了"]; old_log(str); } // hook rebind_symbols((struct rebinding [1]){{"NSLog",newLog,(void *)&old_log}}, 1);
1
2
3
4
5
6
7
rebind_symbols的簡(jiǎn)單邏輯
假設(shè):
A -> 原方法 ,B -> 新方法 ,Temp -> 中間變量
Temp = A;
A = B;
hook后需要調(diào)用原方法就調(diào)用Temp
具體如下:
1?? 首先,MachO文件是被dyld(dynamic load)動(dòng)態(tài)加載的,也就是說(shuō),MachO文件被加載到內(nèi)存的時(shí)候是不固定的,它有一個(gè)ASLR隨機(jī)值;
2?? 同樣,依賴(lài)的系統(tǒng)庫(kù)的加載也是隨機(jī)的,因此,當(dāng)MachO文件加載到內(nèi)存之前根本不知道系統(tǒng)庫(kù)在哪;
3?? 因此 ,蘋(píng)果采用了PIC技術(shù)(位置代碼獨(dú)立,位置和代碼無(wú)關(guān));
假設(shè)要調(diào)用NSLog函數(shù):首先會(huì)在映射表中增加一個(gè)間接指針,指向外部的NSLog函數(shù);然后dyld會(huì)動(dòng)態(tài)的去綁定,將指針指向外部的函數(shù)地址。
machO符號(hào)表中有懶加載表(_la_symbol_ptr)和非懶加載表(_nl_symbol_ptr)的_data段,在表中存放著與外部綁定的函數(shù)指針,在懶加載端有offset地址,如下:
右邊紅色方框中的都是我們熟悉的名稱(chēng),這些函數(shù)的使用是需要進(jìn)行懶加載綁定的;
左邊的offset提供了相對(duì)于MachO起始地址的偏移量offset,實(shí)際地址即是系統(tǒng)函數(shù)所在的內(nèi)存地址;
我們觀察NSLog函數(shù),記住對(duì)應(yīng)的offset=0x8018這個(gè)偏移量;
使用fishHook的rebind_symbols庫(kù)函數(shù)交換系統(tǒng)NSLog函數(shù):
#pragma mark - 交換NSLog - (void)exchangeLog { // 加載出來(lái)NSLog函數(shù),生成內(nèi)存地址(因?yàn)镹SLog是Lazy Symbol Pointers) NSLog(@"我來(lái)了"); // hook NSLog函數(shù) struct rebinding imp; imp.name = "NSLog"; imp.replacement = my_NSLog; imp.replaced = &sys_nslog; // 存放rebinding結(jié)構(gòu)體數(shù)組,一次可以交換多個(gè)函數(shù) struct rebinding rebs[1] = {imp}; rebind_symbols(rebs, 1); NSLog(@"點(diǎn)擊了屏幕"); } // 實(shí)現(xiàn)一個(gè)函數(shù)來(lái)替換原有函數(shù)-函數(shù)名稱(chēng)即是函數(shù)的指針 void my_NSLog(NSString *format, ...) { printf("攔截打印\n"); sys_nslog(format); } // 定義指針來(lái)接收原始函數(shù)的指針 static void (*sys_nslog)(NSString *format,...);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
然后運(yùn)行輸出,點(diǎn)擊屏幕,查看控制臺(tái)打印輸出,可以看到方法已被攔截,說(shuō)明NSLog函數(shù)已被替換;
所有的函數(shù)地址在編譯后都是確定,在OC中能夠交換方法是因?yàn)橛衧el和imp的連接過(guò)程,函數(shù)與用戶(hù)之間有一個(gè)中間者,那么fishhook應(yīng)該也是如此,修改了中間者的imp指向,否則直接調(diào)用函數(shù),就沒(méi)有交換的可能,在MachO中這個(gè)中間者叫符號(hào)。
在rebind_symbols(rebs, 1);調(diào)用前打斷點(diǎn),在控制臺(tái)執(zhí)行image list獲取模塊列表(image就是一個(gè)個(gè)模塊,一個(gè)個(gè)MachO文件),如下:
上面的紅色標(biāo)志就是應(yīng)用程序加載的起始地址(在程序運(yùn)行期間是固定,重新啟動(dòng)該地址則會(huì)隨機(jī)變化),然后根據(jù)這個(gè)地址去找NSLog函數(shù)的地址(偏移offset=0x8018),即在控制臺(tái)繼續(xù)輸入:x 0x000000010453c000+0x8018
上圖中的74 39 59 84 01 00 00 00即為NSLog函數(shù)所對(duì)應(yīng)的值,由于CPU的小端模式,因此需要從右往左取值: 0x0184593074就是NSLog的內(nèi)存地址;然后dis -s 0x0184593074:
繼續(xù)讓斷點(diǎn)往下多執(zhí)行一步,重復(fù)上面的操作,就可以看到fishhook對(duì)系統(tǒng)NSLog函數(shù)的替換:
fishHook官方說(shuō)明示意圖
回到MachO文件,來(lái)看看Lazy Symbol、Dynamic Symbol Table、Symbol Table、String Table表關(guān)系:
① Lazy Symbol和Dynamic Symbol Tabel一一對(duì)應(yīng)(在數(shù)組的下標(biāo)一致)這兩個(gè)表包含了所有與動(dòng)態(tài)庫(kù)相關(guān)的符號(hào);
② Dynamic Symbol Tabel和Symbol Table關(guān)聯(lián),Dynamic Symbol Table中的Data字段是Symbol Table數(shù)組的下標(biāo);
③ Symbol Table中的data字段地址 + String Table表的起始地址,就是目標(biāo)函數(shù)對(duì)應(yīng)字符的位置。
首先在Lazy Symbol Pointers中找到NSLog函數(shù),它處于表中第一個(gè)元素;
找到indirect Symbols表中的NSLog項(xiàng),它也處于表中第一個(gè)元素;
在上圖中找到NSLog的Data地址0x94,等于十進(jìn)制148,即為Symbol Table表中NSLog對(duì)應(yīng)的下標(biāo),如下:
通過(guò)下標(biāo)找到了對(duì)應(yīng)的符號(hào),主要上面的標(biāo)注0xBA為String Table中的偏移量,表的起始地址加上偏移量即是函數(shù)名所在的位置;
通過(guò)首地址獲取最終函數(shù)名,0xBA + 0x6180 = 0x623A,找到0x623A地址處,如下:
最終找到系統(tǒng)的函數(shù)NSLog。
Cydia Substrate 原名為 Mobile Substrate ,它的主要作用是針對(duì)OC方法、C函數(shù)以及函數(shù)地址進(jìn)行HOOK操作。
跟method Swizzle類(lèi)似,也是用的OC的動(dòng)態(tài)性,知識(shí)Method Swizzle是用的方法交換,Cydia Substrate 是用的method_getImplementation和 method_setImplementation這兩個(gè)方法,獲取方法實(shí)現(xiàn)和設(shè)置方法實(shí)現(xiàn)。
Cydia Substrate主要由3部分組成:
MobileHooker
它定義一系列的宏和函數(shù),底層調(diào)用objc的runtime和fishhook來(lái)替換系統(tǒng)或者目標(biāo)應(yīng)用的函數(shù)。主要有兩個(gè)函數(shù):MSHookMessageEx 主要作用于Objective-C方法,MSHookFunction 主要作用于C和C++函數(shù);
MSHookMessageEx函數(shù)的作用對(duì)象是Objective-C函數(shù),其原理是調(diào)用Objective-C中高等級(jí)的運(yùn)行時(shí)函數(shù)API:class_getInstanceMethod、method_setImplementation、method_exchangeImplementations等來(lái)替換原函數(shù)的邏輯,其實(shí)MobileHooker就是對(duì)fishhook和runtime做了封裝,就是和AFNetworking和NSURLSession的關(guān)系是一樣的;
// MSHookMessageEx void MSHookMessageEx(Class class, SEL selector, IMP replacement, IMP result) // MSHookFunction void MSHookFunction(voidfunction,void* replacement,void** p_original)
1
2
3
4
5
6
MobileLoader
MobileLoader的作用就是去加載第三方dylib,在ios啟動(dòng)的時(shí)候,會(huì)由launchd將MobileLoader載入內(nèi)存,然后MobileLoader會(huì)根據(jù)同名的plist文件指定的作用范圍,有選擇地在不同的進(jìn)程當(dāng)中去通過(guò)dlopen函數(shù)打開(kāi)目錄/Library/MobileSubstrate/DynamicLibraries/下的所有的dylib。
safe mode
因?yàn)锳PP程序質(zhì)量參差不齊崩潰再所難免,破解程序本質(zhì)是dylib,寄生在別人進(jìn)程里。 一旦出錯(cuò),就可能導(dǎo)致整個(gè)進(jìn)程崩潰,而一旦崩潰的是SpringBoard等系統(tǒng)進(jìn)程,崩潰后就會(huì)造成iOS癱瘓。所以CydiaSubstrate引入了安全模式,在安全模式下所有基于CydiaSubstratede 的三方dylib都會(huì)被禁用,便于查錯(cuò)與修復(fù)。
Hook 流程(以O(shè)bjective-C為例)
在 Objective-C 中,所有的 [receiver message] 都會(huì)轉(zhuǎn)換為 objc_msgSend(receiver, @selector(message))。
在當(dāng)前 class 的方法緩存里尋找(cache methodLists)
找到了跳到對(duì)應(yīng)的方法實(shí)現(xiàn),沒(méi)找到繼續(xù)往下執(zhí)行;
從當(dāng)前 class 的 方法列表里查找(methodLists),找到了添加到緩存列表里,然后跳轉(zhuǎn)到對(duì)應(yīng)的方法實(shí)現(xiàn);沒(méi)找到繼續(xù)往下執(zhí)行;
從 superClass 的緩存列表和方法列表里查找,直到找到基類(lèi)為止;
以上步驟還找不到 IMP,則用_objc_msgForward函數(shù)指針代替 IMP ,最后執(zhí)行這個(gè) IMP;
// objc-runtime-new.mm 文件里與 _objc_msgForward 有關(guān)的三個(gè)函數(shù)使用偽代碼展示,這也是 obj_msgSend 的實(shí)現(xiàn)過(guò)程 id objc_msgSend(id self, SEL op, ...) { if (!self) return nil; IMP imp = class_getMethodImplementation(self->isa, SEL op); // 調(diào)用這個(gè)函數(shù),偽代碼... imp(self, op, ...); } //查找IMP IMP class_getMethodImplementation(Class cls, SEL sel) { if (!cls || !sel) return nil; IMP imp = lookUpImpOrNil(cls, sel); // _objc_msgForward 用于消息轉(zhuǎn)發(fā) if (!imp) return _objc_msgForward; return imp; } IMP lookUpImpOrNil(Class cls, SEL sel) { if (!cls->initialize()) { _class_initialize(cls); } Class curClass = cls; IMP imp = nil; // 先查緩存,緩存沒(méi)有時(shí)重建,仍舊沒(méi)有則向父類(lèi)查詢(xún) do { if (!curClass) break; if (!curClass->cache) fill_cache(cls, curClass); imp = cache_getImp(curClass, sel); if (imp) break; } while (curClass = curClass->superclass); return imp; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
第一個(gè)階段的具體方法是+(BOOL)resolveInstanceMethod:(SEL)sel 和+(BOOL)resolveClassMethod:(SEL)sel,當(dāng)方法是實(shí)例方法時(shí)調(diào)用前者,當(dāng)方法為類(lèi)方法時(shí),調(diào)用后者。這個(gè)方法是為了給類(lèi)利用 class_addMethod 添加方法的機(jī)會(huì);
第二個(gè)階段是備援接收者階段,對(duì)象的具體方法是-(id)forwardingTargetForSelector:(SEL)aSelector ,此時(shí),運(yùn)行時(shí)詢(xún)問(wèn)能否把消息轉(zhuǎn)給其他接收者處理,也就是此時(shí)系統(tǒng)給了個(gè)將這個(gè) SEL 轉(zhuǎn)給其他對(duì)象的機(jī)會(huì);
第三個(gè)階段是完整消息轉(zhuǎn)發(fā)階段,對(duì)應(yīng)方法-(void)forwardInvocation:(NSInvocation *)anInvocation,這是消息轉(zhuǎn)發(fā)流程的最后一個(gè)環(huán)節(jié)(hook方案的核心)。
直接替換原方法的實(shí)現(xiàn)為_(kāi)objc_msgForward。當(dāng)原來(lái)的函數(shù)被調(diào)用時(shí),就不會(huì)在類(lèi)方法,父類(lèi)方法列表里查找實(shí)現(xiàn)了,直接表示找不到,進(jìn)入轉(zhuǎn)發(fā)流程。用_objc_msgForward來(lái)代替原來(lái)的函數(shù),代碼如下:
class_replaceMethod(class, selector, _objc_msgForward, method_getTypeEncoding(targetMethod));
1
替換forwardInvocation:的實(shí)現(xiàn),當(dāng)進(jìn)入轉(zhuǎn)發(fā)流程時(shí),階段一和階段二都不接手,在階段三forwardInvocation:里會(huì)接手;這里會(huì)替換forwardInvocation:的實(shí)現(xiàn),用newForwardInvocation代替,這樣就可以hook,完成自己的邏輯后,還要調(diào)用被hook的函數(shù)原來(lái)的邏輯。
id newForwardInvocation = ^(id self, NSInvocation *invocation) { // hook時(shí)要添加的代碼 if (originalForwardInvocation == NULL) { [self doesNotRecognizeSelector:invocation.selector]; } else { originalForwardInvocation(self, forwardInvocationSEL, invocation); } }; class_replaceMethod(class, @selector(forwardInvocation:), imp_implementationWithBlock(newForwardInvocation), "v@:@");
1
2
3
4
5
6
7
8
9
10
11
簡(jiǎn)單防護(hù)
我們知道Method Swizzle原理是方法交換,那么可以使用fishHook 修改method_exchangeImplementations函數(shù)的指向,這個(gè)修改指向要在我們的方法交換之后進(jìn)行(保證自己能改,別人不能改)。
我們的方法交換要在別人hook之前執(zhí)行,這個(gè)地方就需要將我們的方法封裝到靜態(tài)庫(kù)中,靜態(tài)庫(kù)中的load方法會(huì)先加載;
它的原理是修改imp的set和get方法,因此我們也可以通過(guò)fishHook修改method_getImplementation和method_setImplementation方法。
示例
+ (void)load { // 先交換,防護(hù)之前要將所有的交換都寫(xiě)完 Method oldOne = class_getInstanceMethod(objc_getClass("ViewController"), @selector(btnClickOne:)); Method newOne = class_getInstanceMethod(self, @selector(ClickOneHook:)); method_exchangeImplementations(oldOne, newOne); // 基本防護(hù) Method Swizzle struct rebinding bd ; // 原函數(shù)名(字符串) A函數(shù) bd.name = "method_exchangeImplementations"; // 交換后的函數(shù) B函數(shù) bd.replacement = myExchange; // 暫存原函數(shù)的地址 中間變量temp函數(shù) bd.replaced = (void *)&old_exchage; // 升級(jí)防護(hù),用于防護(hù)logos(cydia substrate) // method_setImplementation struct rebinding bd1 ; bd1.name = "method_getImplementation"; bd1.replacement = myExchange; bd1.replaced = (void *)&getImp; struct rebinding bd2 ; bd2.name = "method_setImplementation"; bd2.replacement = myExchange; bd2.replaced = (void *)&setImp; // fishhook方法交換 struct rebinding rebind[] = {bd,bd1,bd2}; /* arg1:數(shù)組,元素必須是rebinding這個(gè)結(jié)構(gòu)體 arg2:數(shù)組個(gè)數(shù) */ rebind_symbols(rebind, 3); } // 用于存放method_exchangeImplementations涵數(shù) 也就是Temp函數(shù) void (* old_exchage)(Class _Nullable cls, SEL _Nonnull name); // 用于存放method_getImplementation涵數(shù) IMP _Nonnull(*getImp)(Method _Nonnull m) ; // 用于存放method_setImplementation涵數(shù) IMP _Nonnull(*setImp)(Method _Nonnull m, IMP _Nonnull imp) ; // 新的交換函數(shù) B函數(shù) void myExchange(Class _Nullable cls, SEL _Nonnull name){ NSLog(@"檢測(cè)到hook"); exit(1); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
iOS 數(shù)據(jù)結(jié)構(gòu)
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶(hù)投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。