本技術(shù)實(shí)現(xiàn)在YSBlockHook中。
1.方法調(diào)用的幾種Hook機(jī)制
iOS系統(tǒng)中一共有:C函數(shù)、Block、OC類(lèi)方法三種形式的方法調(diào)用。Hook一個(gè)方法調(diào)用的目的一般是為了監(jiān)控?cái)r截或者統(tǒng)計(jì)一些系統(tǒng)的行為。Hook的機(jī)制有很多種,通常良好的Hook方法都是以AOP的形式來(lái)實(shí)現(xiàn)的。
當(dāng)我們想Hook一個(gè)OC類(lèi)的某些具體的方法時(shí)可以通過(guò)Method Swizzling技術(shù)來(lái)實(shí)現(xiàn)、當(dāng)我們想Hook動(dòng)態(tài)庫(kù)中導(dǎo)出的某個(gè)C函數(shù)時(shí)可以通過(guò)修改導(dǎo)入函數(shù)地址表中的信息來(lái)實(shí)現(xiàn)(可以使用開(kāi)源庫(kù)fishhook來(lái)完成)、當(dāng)我們想Hook所有OC類(lèi)的方法時(shí)則可以通過(guò)替換objc_msgSend系列函數(shù)來(lái)實(shí)現(xiàn)。。。
那么對(duì)于Block方法呢而言呢?
2.Block的內(nèi)部實(shí)現(xiàn)原理和實(shí)現(xiàn)機(jī)制簡(jiǎn)介
這里假定你對(duì)Block內(nèi)部實(shí)現(xiàn)原理和運(yùn)行機(jī)制有所了解,如果不了解則請(qǐng)參考文章《深入解構(gòu)iOS的block閉包實(shí)現(xiàn)原理》或者自行通過(guò)搜索引擎搜索。
源程序中定義的每個(gè)Block在編譯時(shí)都會(huì)轉(zhuǎn)化為一個(gè)和OC類(lèi)對(duì)象布局相似的對(duì)象,每個(gè)Block也存在著isa這個(gè)數(shù)據(jù)成員,根據(jù)isa指向的不同,Block分為_(kāi)_NSStackBlock、__NSMallocBlock、__NSGlobalBlock 三種類(lèi)型。也就是說(shuō)從某種程度上Block對(duì)象也是一種OC對(duì)象。下面的類(lèi)圖描述了Block類(lèi)的層次結(jié)構(gòu)。

Block類(lèi)以及其派生類(lèi)在CoreFoundation.framework中被定義和實(shí)現(xiàn),并且沒(méi)有對(duì)外公開(kāi)。
每個(gè)Block對(duì)象在內(nèi)存中的布局,也就是Block對(duì)象的存儲(chǔ)結(jié)構(gòu)被定義如下(代碼出自蘋(píng)果開(kāi)源出來(lái)的庫(kù)實(shí)現(xiàn)libclosure中的文件Block_private.h):
//需要注意的是下面兩個(gè)只是模板,具體的每個(gè)Block定義時(shí)總是按這個(gè)模板來(lái)定義的。
//Block描述,每個(gè)Block一個(gè)描述并定義在全局?jǐn)?shù)據(jù)段
struct Block_descriptor_1 {
uintptr_t reserved; //記住這個(gè)變量和結(jié)構(gòu)體,它很重要?。? uintptr_t size;
};
//Block對(duì)象的內(nèi)存布局
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
uintptr_t invoke; //Block對(duì)象的實(shí)現(xiàn)函數(shù)
struct Block_descriptor_1 *descriptor;
// imported variables,這里是每個(gè)block對(duì)象的特定數(shù)據(jù)成員區(qū)域
};
這里要關(guān)注一下struct Block_descriptor_1中的reserved這個(gè)數(shù)據(jù)成員,雖然系統(tǒng)沒(méi)有用到它,但是下面就會(huì)用到它而且很重要!
在了解了Block對(duì)象的類(lèi)型以及Block對(duì)象的內(nèi)存布局后,再來(lái)考察一下一個(gè)Block從定義到調(diào)用是如何實(shí)現(xiàn)的。就以下面的源代碼為例:
int main(int argc, char *argv[])
{
//定義
int a = 10;
void (^testblock)(void) = ^(){
NSLog(@"Hello world!%d", a);
};
//執(zhí)行
testblock();
return 0;
}
在將OC代碼翻譯為C語(yǔ)言代碼后每個(gè)Block的定義和調(diào)用將變成如下的偽代碼:
//testblock的描述信息
struct Block_descriptor_1_fortestblock {
uintptr_t reserved;
uintptr_t size;
};
//testblock的布局存儲(chǔ)結(jié)構(gòu)體
struct Block_layout_fortestblock {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
uintptr_t invoke; //Block對(duì)象的實(shí)現(xiàn)函數(shù)
struct Block_descriptor_1_fortestblock *descriptor;
int m_a; //外部的傳遞進(jìn)來(lái)的數(shù)據(jù)。
};
//testblock函數(shù)的實(shí)現(xiàn)。
void main_invoke_fortestblock(struct Block_layout_fortestblock *cself)
{
NSLog(@"Hello world!%d", cself->m_a);
}
//testblock對(duì)象描述的實(shí)例,存儲(chǔ)在全局內(nèi)存區(qū)
struct Block_descriptor_1_fortestblock _testblockdesc = {0, sizeof(struct Block_layout_fortestblock)};
int main(int argc, char *argv[])
{
//定義部分
int a = 10;
struct Block_layout_fortestblock testblock = {
.isa = __NSConcreteStackBlock,
.flags =0,
.reserved = 0,
.invoke = main_invoke_fortestblock,
.descriptor = & _testblockdesc,
.m_a = a
};
//調(diào)用部分
testblock.invoke();
return 0;
}
可以看出Block對(duì)象的生成和調(diào)用都是在編譯期間就已經(jīng)固定在代碼中了,它不像其他OC對(duì)象調(diào)用方法時(shí)需要通過(guò)runtime來(lái)執(zhí)行間接調(diào)用。并且線上程序中所有關(guān)于Block的符號(hào)信息都會(huì)被strip掉。所以上述的所介紹的幾種Hook方法都無(wú)法Hook住一個(gè)Block對(duì)象的函數(shù)調(diào)用。
如果想要Hook住系統(tǒng)的所有Block調(diào)用,需要解決如下幾個(gè)問(wèn)題:
a. 如何在運(yùn)行時(shí)將所有的Block的invoke函數(shù)替換為一個(gè)統(tǒng)一的Hook函數(shù)。
b. 這個(gè)統(tǒng)一的Hook函數(shù)如何調(diào)用原始Block的invoke函數(shù)。
c. 如何構(gòu)建這個(gè)統(tǒng)一的Hook函數(shù)。
3.實(shí)現(xiàn)Block對(duì)象Hook的方法和原理
一個(gè)OC類(lèi)對(duì)象的實(shí)例通過(guò)引用計(jì)數(shù)來(lái)管理對(duì)象的生命周期。在MRC時(shí)代當(dāng)對(duì)象進(jìn)行賦值和拷貝時(shí)需要通過(guò)調(diào)用retain方法來(lái)實(shí)現(xiàn)引用計(jì)數(shù)的增加,而在ARC時(shí)代對(duì)象進(jìn)行賦值和拷貝時(shí)就不再需要顯示調(diào)用retain方法了,而是系統(tǒng)內(nèi)部在編譯時(shí)會(huì)自動(dòng)插入相應(yīng)的代碼來(lái)實(shí)現(xiàn)引用計(jì)數(shù)的添加和減少。不管如何只要是對(duì)OC對(duì)象執(zhí)行賦值拷貝操作,最終內(nèi)部都會(huì)調(diào)用retain方法。
Block對(duì)象也是一種OC對(duì)象??!
每當(dāng)一個(gè)Block對(duì)象在需要進(jìn)行賦值或者拷貝操作時(shí),也會(huì)激發(fā)對(duì)retain方法的調(diào)用。因?yàn)锽lock對(duì)象賦值操作一般是發(fā)生在Block方法執(zhí)行之前,因此我們可以通過(guò)Method Swizzling的機(jī)制來(lái)Hook 類(lèi)的retain方法,然后在重寫(xiě)的retain方法內(nèi)部將Block對(duì)象的invoke數(shù)據(jù)成員替換為一個(gè)統(tǒng)一的Hook函數(shù)!
通過(guò)考察__NSStackBlock、__NSMallocBlock、__NSGlobalBlock 三個(gè)類(lèi)的實(shí)現(xiàn)發(fā)現(xiàn)這三個(gè)類(lèi)都重載了NSObject的retain方法,這樣在執(zhí)行Method Swizzling時(shí)就不需要對(duì)NSObject的retain方法執(zhí)行替換,而只要對(duì)上述三個(gè)類(lèi)的retain執(zhí)行替換即可。
你可以說(shuō)出為什么這三個(gè)派生類(lèi)都會(huì)對(duì)retain方法進(jìn)行重載嗎?答案可以從這三種Block的類(lèi)型定義以及所表示的意義中去尋找。
Block技術(shù)不僅可以用在OC語(yǔ)言中,LLVM對(duì)C語(yǔ)言進(jìn)行的擴(kuò)展也能使用Block,比如gcd庫(kù)中大量的使用了Block。在C語(yǔ)言中如果對(duì)一個(gè)Block進(jìn)行賦值或者拷貝系統(tǒng)需要通過(guò)C庫(kù)函數(shù):
//函數(shù)聲明在Block.h頭文件匯總
// Create a heap based copy of a Block or simply add a reference to an existing one.
// This must be paired with Block_release to recover memory, even when running
// under Objective-C Garbage Collection.
BLOCK_EXPORT void *_Block_copy(const void *aBlock)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
來(lái)實(shí)現(xiàn),這個(gè)函數(shù)定義在libsystem_blocks.dylib庫(kù)中,并且?guī)鞂?shí)現(xiàn)已經(jīng)開(kāi)源:libclosure。因此可以借助fishhook庫(kù)來(lái)對(duì)__Block_copy這個(gè)函數(shù)進(jìn)行替換處理,然后在替換的函數(shù)函數(shù)中將一個(gè)Block的原始的invoke函數(shù)替換為統(tǒng)一的Hook函數(shù)。
另外一個(gè)C語(yǔ)言函數(shù)objc_retainBlock,也是實(shí)現(xiàn)了對(duì)Block進(jìn)行賦值時(shí)的引用計(jì)數(shù)增加,這個(gè)函數(shù)內(nèi)部就是簡(jiǎn)單的調(diào)用__Block_copy方法。因此我們也可以添加對(duì)objc_retainBlock的替換處理。
解決了第一個(gè)問(wèn)題后,接下來(lái)再解決第二個(gè)問(wèn)題。還記得上面提到過(guò)的struct Block_descriptor_1中的reserved這個(gè)數(shù)據(jù)成員嗎? 當(dāng)我們通過(guò)上述的方法對(duì)所有Block對(duì)象的invoke成員替換為一個(gè)統(tǒng)一的Hook函數(shù)前,可以將Block對(duì)象的原始invoke函數(shù)保存到這個(gè)保留字段中去。然后就可以在統(tǒng)一的Hook函數(shù)內(nèi)部讀取這個(gè)保留字段中的保存的原始invoke函數(shù)來(lái)執(zhí)行真實(shí)的方法調(diào)用了。
因?yàn)橐粋€(gè)Block對(duì)象函數(shù)的第一個(gè)參數(shù)其實(shí)是一個(gè)隱藏的參數(shù),這個(gè)隱藏的參數(shù)就是Block對(duì)象本身,因此很容易就可以從隱藏的參數(shù)中來(lái)獲取到對(duì)應(yīng)的保留字段。
下面的代碼將展示通過(guò)方法交換來(lái)實(shí)現(xiàn)Hook處理的偽代碼
struct Block_descriptor {
void *reserved;
uintptr_t size;
};
struct Block_layout {
void *isa;
int32_t flags; // contains ref count
int32_t reserved;
void *invoke;
struct Block_descriptor *descriptor;
};
//統(tǒng)一的Hook函數(shù),這里以偽代碼的形式提供
void blockhook(void *obj, ...)
{
struct Block_layout *layout = (struct Block_layout*) obj;
//調(diào)用原始的invoke函數(shù)
layout->descriptor->reserved(...);
}
//模擬器下如果返回類(lèi)型是結(jié)構(gòu)體并且大于16字節(jié)那么第一個(gè)參數(shù)是返回值保存的內(nèi)存地址,block對(duì)象變?yōu)榈诙€(gè)參數(shù)
void blockhook_stret(void *pret, void *obj, ...)
{
struct Block_layout *layout = (struct Block_layout*) obj;
//調(diào)用原始的invoke函數(shù)
layout->descriptor->reserved(...);
}
//執(zhí)行Block對(duì)象的方法替換處理
void replaceBlockInvokeFunction(const void *blockObj)
{
struct Block_layout *layout = (struct Block_layout*)blockObj;
if (layout != NULL && layout->descriptor != NULL){
int32_t BLOCK_USE_STRET = (1 << 29); //如果模擬器下返回的類(lèi)型是一個(gè)大于16字節(jié)的結(jié)構(gòu)體,那么block的第一個(gè)參數(shù)為返回的指針,而不是block對(duì)象。
void *hookfunc = ((layout->flags & BLOCK_USE_STRET) == BLOCK_USE_STRET) ? blockhook_stret : blockhook;
if (layout->invoke != hookfunc){
layout->descriptor->reserved = layout->invoke;
layout->invoke = hookfunc;
}
}
}
void *(*__NSStackBlock_retain_old)(void *obj, SEL cmd) = NULL;
void *__NSStackBlock_retain_new(void *obj, SEL cmd)
{
replaceBlockInvokeFunction(obj);
return __NSStackBlock_retain_old(obj, cmd);
}
void *(*__NSMallocBlock_retain_old)(void *obj, SEL cmd) = NULL;
void *__NSMallocBlock_retain_new(void *obj, SEL cmd)
{
replaceBlockInvokeFunction(obj);
return __NSMallocBlock_retain_old(obj, cmd);
}
void *(*__NSGlobalBlock_retain_old)(void *obj, SEL cmd) = NULL;
void *__NSGlobalBlock_retain_new(void *obj, SEL cmd)
{
replaceBlockInvokeFunction(obj);
return __NSGlobalBlock_retain_old(obj, cmd);
}
int main(int argc, char *argv[])
{
//因?yàn)轭?lèi)名和方法名都不能直接使用,所以這里都以字符串的形式來(lái)轉(zhuǎn)換獲取。
__NSStackBlock_retain_old = (void *(*)(void*,SEL))class_replaceMethod(NSClassFromString(@"__NSStackBlock"), sel_registerName("retain"), (IMP)__NSStackBlock_retain_new, nil);
__NSMallocBlock_retain_old = (void *(*)(void*,SEL))class_replaceMethod(NSClassFromString(@"__NSMallocBlock"), sel_registerName("retain"), (IMP)__NSMallocBlock_retain_new, nil);
__NSGlobalBlock_retain_old = (void *(*)(void*,SEL))class_replaceMethod(NSClassFromString(@"__NSGlobalBlock"), sel_registerName("retain"), (IMP)__NSGlobalBlock_retain_new, nil);
return 0;
}
解決了第二個(gè)問(wèn)題后,就需要解決第三個(gè)問(wèn)題。上面的統(tǒng)一Hook函數(shù)blockhook和block_stret只是偽代碼實(shí)現(xiàn),因?yàn)槿魏我粋€(gè)Block中的函數(shù)的參數(shù)類(lèi)型和個(gè)數(shù)是不一樣的,而且統(tǒng)一Hook函數(shù)也需要在適當(dāng)?shù)臅r(shí)候調(diào)用原始的默認(rèn)Block函數(shù)實(shí)現(xiàn),并且不能破壞參數(shù)信息。為了解決這些問(wèn)題就使得這個(gè)統(tǒng)一的Hook函數(shù)不能用高級(jí)語(yǔ)言來(lái)實(shí)現(xiàn),而只能用匯編語(yǔ)言來(lái)實(shí)現(xiàn)。下面就是在arm64位體系下的實(shí)現(xiàn)代碼:
.text
.align 5
.private_extern _blockhook
_blockhook:
//為了不破壞原有參數(shù),這里將所有參數(shù)壓入棧中
stp q6, q7, [sp, #-0x20]!
stp q4, q5, [sp, #-0x20]!
stp q2, q3, [sp, #-0x20]!
stp q0, q1, [sp, #-0x20]!
stp x6, x7, [sp, #-0x10]!
stp x4, x5, [sp, #-0x10]!
stp x2, x3, [sp, #-0x10]!
stp x0, x1, [sp, #-0x10]!
stp x8, x30, [sp, #-0x10]!
//這里可以添加任意邏輯來(lái)進(jìn)行hook處理。
//這里將所有參數(shù)還原
ldp x8, x30, [sp], #0x10
ldp x0, x1, [sp], #0x10
ldp x2, x3, [sp], #0x10
ldp x4, x5, [sp], #0x10
ldp x6, x7, [sp], #0x10
ldp q0, q1, [sp], #0x20
ldp q2, q3, [sp], #0x20
ldp q4, q5, [sp], #0x20
ldp q6, q7, [sp], #0x20
ldr x16, [x0, #0x18] //將block對(duì)象的descriptor數(shù)據(jù)成員取出
ldr x16, [x16] //獲取descriptor中的reserved成員
br x16 //執(zhí)行reserved中保存的原始函數(shù)指針。
LExit_blockhook:
對(duì)于x86_64/arm32位系統(tǒng)來(lái)說(shuō),如果block函數(shù)的返回是一個(gè)結(jié)構(gòu)體并且長(zhǎng)度超過(guò)16字節(jié)(arm32是8字節(jié))。那么block對(duì)象里面的flags屬性就會(huì)設(shè)置為BLOCK_USE_STRET。而x86_64/arm32位系統(tǒng)對(duì)于這種返回類(lèi)型的函數(shù)就會(huì)將返回值存放到第一個(gè)參數(shù)所指向的內(nèi)存中,同時(shí)會(huì)把原本的block對(duì)象變化為第二個(gè)參數(shù),因此需要對(duì)這種情況進(jìn)行特殊處理。
關(guān)于在運(yùn)行時(shí)Hook所有Block方法調(diào)用的技術(shù)實(shí)現(xiàn)原理就介紹到這里了。當(dāng)然一個(gè)完整的系統(tǒng)可能需要其他一些能力:
- 如果你只想Hook可執(zhí)行程序中定義的Block,那么請(qǐng)參考我的文章:深入iOS系統(tǒng)底層之映像操作API介紹 中的內(nèi)容來(lái)實(shí)現(xiàn)Hook函數(shù)的過(guò)濾處理。
- 如果你不想借助Block_descriptor中的reserved來(lái)保存原始的invoke函數(shù),那么可以參考我的文章:Thunk程序的實(shí)現(xiàn)原理以及在iOS中的應(yīng)用(二)中介紹的技術(shù)來(lái)實(shí)現(xiàn)統(tǒng)一Hook函數(shù)以及完成對(duì)原始invoke函數(shù)的調(diào)用技術(shù)。
具體完整的代碼可以訪問(wèn)我的github中的項(xiàng)目:YSBlockHook。這個(gè)項(xiàng)目以AOP的形式實(shí)現(xiàn)了真機(jī)arm64位模式下對(duì)可執(zhí)行程序中所有定義的Block進(jìn)行Hook的方法,Hook所做的事情就是在所有Block調(diào)用前,打印出這個(gè)Block的符號(hào)信息。
歡迎大家訪問(wèn)歐陽(yáng)大哥2013的github地址和簡(jiǎn)書(shū)地址