0x01.runtime和Method Swizzle
runtime是Objective C 語(yǔ)言的特殊機(jī)制颜曾。總的來(lái)說(shuō)铐刘,OC通過(guò)runtime聯(lián)系上c和c++夜只,其底層由c語(yǔ)言編寫
根據(jù)OC語(yǔ)言的特性,有很多類和成員變量在編譯時(shí)是系統(tǒng)不知道的闸天,而在運(yùn)行時(shí)暖呕,我們所編寫的代碼才會(huì)轉(zhuǎn)換成完整的確定的代碼去運(yùn)行(運(yùn)行時(shí)鏈接)。因此苞氮,僅僅只有一套編譯器是不夠的湾揽,還需要一套運(yùn)行時(shí)系統(tǒng)(Runtime System)來(lái)處理編譯后的工作,runtime就是這樣一個(gè)系統(tǒng)笼吟。根據(jù)這個(gè)原理钝腺,在運(yùn)行時(shí)交換原有方法和我們自己編寫的新方法的imp指針,可以達(dá)到hook函數(shù)的目的赞厕,是為Method Swizzle。
0x02.簡(jiǎn)單實(shí)現(xiàn)
下面簡(jiǎn)單實(shí)現(xiàn)一個(gè)普普通通的demo
這些調(diào)用的方法都是簡(jiǎn)單的輸出log皿桑,反正是拿來(lái)湊數(shù)的代碼。不重要
編寫一下Method Swizzle的代碼蔬啡。
這里把trycheck6函數(shù)和自己編寫的gogo函數(shù)做了交換诲侮,關(guān)鍵的交換函數(shù)就是method_exchangeImplementations函數(shù),用于方法指針的交換箱蟆。
運(yùn)行效果就不看了沟绪,就是該執(zhí)行trycheck6時(shí)打印了gogogogo
這樣方法交換就完成了。相當(dāng)于實(shí)現(xiàn)了對(duì)old函數(shù)也就是trycheck6這個(gè)函數(shù)的hook空猜,改變了它的功能和邏輯绽慈。但其實(shí)它的名字和外皮并沒有改變。
0x03.觀察
以runtime的特性辈毯,我們?cè)谒\(yùn)行的時(shí)候獲取一個(gè)類下所有方法的imp指針坝疼。達(dá)到遍歷所有方法的目的。
之前沒接觸過(guò)Objective C谆沃,就隨便寫寫钝凶。代碼如下:
+(NSArray*)getAllmethods
{
? ? unsigned?int?methodCount =0;
? ? const?char* lastname;
? ? Method* methodList =class_copyMethodList([self?class], &methodCount);
? ? NSMutableArray*methodsArray = [NSMutableArrayarrayWithCapacity:methodCount];
? ? Methodtemp = methodList[i];
? ? IMP imp = method_getImplementation(temp);
? ? const ?char* name_s =sel_getName(method_getName(temp));
? ? //SEL name_f = method_getName(temp);
? ? lastname = name_s;
? ? //int arguments = method_getNumberOfArguments(temp);
? ? //const char* encoding = method_getTypeEncoding(temp);
? ? //NSLog(@"方法名:%@,參數(shù)個(gè)數(shù):%d,編碼方式:%@",[NSString stringWithUTF8String:name_s],arguments,[NSString stringWithUTF8String:encoding]);
? ? ? ? [methods ArrayaddObject:[NSString stringWithUTF8String:name_s]];
? ? }
? ? free(methodList);
? ? return?methodsArray;
}
隨便復(fù)制一手,最后返回的函數(shù)列表
這只是某一個(gè)類中所獲取到的函數(shù)唁影。要獲取所有的函數(shù)需要遍歷類耕陷。
可以看到掂名,就算是方法交換之后返回的函數(shù)列表,trycheck6也還是trycheck6哟沫。他的名字并沒有變成gogo饺蔑。可見這還是有點(diǎn)欺騙性的南用。
0x04.天馬行空的想象
我們?nèi)绾稳z測(cè)一個(gè)函數(shù)是否被hook呢膀钠?
其實(shí)在調(diào)試中是可以看見不一樣的
這里imp指針的值可以看見,就算我把我的new方法改成了trycheck6同名裹虫,前面也會(huì)帶上MSHook肿嘲。人工一眼就能看見是被hook了。
那從antihook的角度來(lái)講筑公,是不是獲取imp里面的值做比較就行了呢雳窟?有兩個(gè)問題:
第一,imp是指針匣屡,指向的是函數(shù)的地址封救。Xcode只是便于讓開發(fā)者具體的看才轉(zhuǎn)化成了這個(gè)樣子,其實(shí)imp的值只是一串十六進(jìn)制數(shù)而已
第二捣作,就算第一條能實(shí)現(xiàn)誉结。要檢測(cè)同名的替換函數(shù),勢(shì)必要檢測(cè)類名券躁,一個(gè)真正的工程有很多類名惩坑,要遍歷類還要遍歷每個(gè)類下的函數(shù),太耗時(shí)了也拜。
綜上直接去比較imp指針是行不通的以舒。但是要檢測(cè)一個(gè)函數(shù)是否被hook也肯定離不開imp指針的。
我們來(lái)看一下慢哈,old和new在內(nèi)存上的位置
用于方法交換的new的地址是比old的地址高的。這里只是我的demo的情況卵贱。在真正的項(xiàng)目中滥沫,注入動(dòng)態(tài)庫(kù)來(lái)進(jìn)行方法交換的話,動(dòng)態(tài)庫(kù)會(huì)被注入到Load_command段末尾而不是像我一樣直接寫在項(xiàng)目中键俱,可能會(huì)有old和new地址偏移量大的情況佣谐,這個(gè)留待后面驗(yàn)證。
基于這個(gè)思路方妖,既然是交換了指向方法的imp指針狭魂,imp指針的值肯定有變化。同一個(gè)類中函數(shù)的地址是由低到高線性變化的。
從沒實(shí)現(xiàn)Method Swizzle時(shí)上面打印的log可以發(fā)現(xiàn)雌澄,同一個(gè)類中函數(shù)的地址是由低到高線性變化的斋泄,而且若函數(shù)體一樣大偏移也是一樣的。
那么我們實(shí)現(xiàn)了Method Swizzle之后呢镐牺,我們可以看看
很驚訝炫掐,trycheck7的地址要比trycheck6的地址低,所以偏移出現(xiàn)了負(fù)數(shù)睬涧。這很好理解募胃,因?yàn)閚ew的地址比old的地址高,交換過(guò)來(lái)trycheck6的地址也變高了畦浓。
那么這樣可以不可以作為一個(gè)判斷trycheck6被hook的依據(jù)呢痹束?我覺得是可以的,但是我自己寫的demo只有幾十kb大小讶请,函數(shù)調(diào)用也并不夠復(fù)雜祷嘶。
我弄了幾個(gè)更大的app工程,發(fā)現(xiàn)machO文件越大夺溢,new和old的距離是越大的论巍,單個(gè)函數(shù)體越復(fù)雜越大,函數(shù)直接的偏移也是越大的风响,在實(shí)際工程中應(yīng)該可以取一個(gè)閥值嘉汰,當(dāng)兩個(gè)在內(nèi)存中線性的函數(shù)偏移量絕對(duì)值大于了這個(gè)閾值,那么一般是可以認(rèn)定為有問題的状勤≈O郑或者對(duì)于小的工程,也可以按比例取閾值荧降,這個(gè)就有點(diǎn)天馬行空的想象了。