運(yùn)行時(shí)Hook所有Block方法調(diào)用的技術(shù)實(shí)現(xiàn)

本技術(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)層次結(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)可能需要其他一些能力:

具體完整的代碼可以訪問(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ū)地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容