Android Hook框架adbi的分析(2)--- inline Hook的實現

本文博客地址:http://blog.csdn.net/qq1084283172/article/details/74452308

一、 Android Hook框架adbi源碼中inline Hook實現部分的代碼結構

Android Hook框架adbi源碼中inline Hook部分的實現代碼結構示意圖如下所示,hijack代碼部分是前面的博客中提到的root下Android跨進程注入so的注入工具,instruments\base代碼部分爲inline Hook的操作實現,instruments\example代碼部分則爲Android Hook框架adbi實現Hook系統調用函數epoll_wait的使用例子。

這裏寫圖片描述

二、 adbi源碼中inline Hook實現的詳細步驟分析

1 .inline Hook函數被調用的時機

在so庫文件加載的時候,會首先執行.init段的構造函數,因此在編寫注入到Android目標進程中的so庫文件時要定義該構造函數並實現在此處調用inline Hook。inline Hook實現就是在so庫文件注入到Android進程中被加載調用該構造函數時被執行的。Android Hook框架adbi基於模塊化的設計思想,該構造函數的編寫是放在自定義Hook函數的接口中來實現的,在這裏就是在Hook函數代碼示例instruments\example\epoll.c中定義和實現的。

這裏寫圖片描述

這裏寫圖片描述

2 .inline Hook操作的Hook函數實現

inline Hook操作的Hook函數是在adbi\instruments\base\hook.c中實現的,在Hook目標pid進程的目標函數時,定義了一個全局的靜態變量,保存被Hook目標函數相關的信息,用以對目標函數的Hook操作和函數還原,具體的結構定義如下:

struct hook_t {

    // arm指令模式的12字節Hook
    unsigned int jump[3];   /* 要修改的hook指令(Arm) */
    unsigned int store[3]; /* 被修改的原指令(Arm) */

    // thumb指令模式的20字節Hook
    unsigned char jumpt[20]; /* 要修改的hook指令(Thumb) */
    unsigned char storet[20]; /* 被修改的源指令(Thumb) */

    unsigned int orig; /* 被hook的目標函數地址 */
    unsigned int patch; /* hook的自定義函數地址 */

    unsigned char thumb; /* 表明被hook函數使用的指令集,1爲Thumb,0爲Arm */
    unsigned char name[128]; /* 被hook的函數名 */

    // 用於存放其他的數據(未使用)
    void *data;
};

在對目標進程的目標函數進行Hook之前,使用hijack注入工具中查找mprotect函數調用地址的方法,獲取被Hook目標函數的調用地址,具體就是通過解析目標函數所在的so庫文件中的“.symtab”或者“.dynsym”節,獲取到庫中所有的符號信息,查找得到目標函數的調用地址的RVA,加上目標函數所在so庫文件的加載基地址就是目標函數的調用地址VA了。

// 對目標pid進程的指定函數進行Hook處理

// h爲記錄Hook信息的靜態變量的指針,pid爲被Hook的目標進程的pid,libname爲被Hook函數所在的so庫文件名稱,
// funcname爲被Hook的目標函數,hook_arm爲被Hook的函數的arm指令模式的替換函數,hook_thumb爲被Hook的函數的thumb指令模式的替換函數
int hook(struct hook_t *h, int pid, char *libname, char *funcname, void *hook_arm, void *hook_thumb)
{
    unsigned long int addr;
    int i;

    // 在指定pid進程的指定so庫中查找將被Hook的目標函數funcname的調用地址VA即addr
    if (find_name(pid, funcname, libname, &addr) < 0) {

        log("can't find funcname: %s\n", funcname)
        return 0;
    }

    log("hooking:   %s = 0x%lx ", funcname, addr)
    // 保存被Hook的目標函數的名稱
    strncpy(h->name, funcname, sizeof(h->name)-1);

Arm處理器支持兩種指令集,一是基本的Arm指令集,二是Thumb指令集。因此,爲了正確的Hook目標函數,不至於導致被Hook的Android進程崩潰,在Hook目標進程的目標函數之前還需要判斷進程當前所處的arm指令模式。判斷的方法是看函數跳轉地址的最後兩位是不是全0,如果是,那就是Arm模式的指令,如果最後兩位不全爲0,那就是Thumb模式的指令。由於Hook目標函數時的跳轉指令需要4字節對齊,所以對目標函數調用地址進行4字節取模來判斷執行的指令集。


Arm與Thumb之間的狀態切換是通過專用的跳轉交換指令BX來實現。BX指令以通用寄存器(R0~R15)爲操作數,通過拷貝Rn到PC實現絕對跳轉。BX利用Rn寄存器中目的地址值的最後一位判斷跳轉後的狀態,如果爲“1”表示跳轉到Thumb指令集的函數中,如果爲“0”表示跳轉到Arm指令集的函數中。而Arm指令集的每條指令是32位,即4個字節,也就是說Arm指令的地址肯定是4的倍數,最後兩位必定爲“00”。所以,直接就可以將從符號表中獲得的調用地址模4,看是否爲0來判斷要修改的函數是用Arm指令集還是Thumb指令集。


上面這段解釋說明引用自博主Roland_Sun的博文Android平臺下hook框架adbi的研究(下) ,特地摘抄過來幫助分析和理解。

    // 通過判斷函數跳轉地址的最後兩位是不是全0,來判斷指令的運行模式,
    // 如果後兩位全是的0,那就一定是用Arm指令,如果後兩位不全爲0,那一定是用Thumb指令集

    if (addr % 4 == 0) 
    {
        // Arm指令模式的HooK目標函數的處理
        ······
    }
    else 
    {
        // Thumb指令模式的Hook目標函數的處理
        ······
    }

Arm指令模式HooK目標函數的處理是通過12字節指令覆蓋來完成的,簡單的來說就是將目標函數調用地址處的前12字節的指令先保存起來,然後使用12字節的Hook跳轉指令進行覆蓋。


Arm指令模式下Hook目標函數的處理,先將自定義hook函數和要被hook目標函數的地址保存起來。然後生成hook的代碼指令,只有3個4字節就是12個字節,第一個dword字節是代碼指令“LDR pc, [pc, #0]”,由於pc寄存器讀出的值實際上是當前指令地址加8,所以這裏是把jump[2]的值加載進pc寄存器中,而jump[2]處保存的是自定義hook函數的地址。因此,jump[0~3]實際上保存的是跳轉到自定義hook函數的代碼指令。再下面,將被hook函數的前3個4字節保存下來,方便後面函數的恢復。最後,將跳轉指令寫到被hook目標函數的前12字節。這樣以後,當要調用被hook函數的時候,實際執行的指令就是跳轉到自定義hook函數處。


    // Arm指令模式的HooK目標函數的處理
    if (addr % 4 == 0) {

        log("ARM using 0x%lx\n", (unsigned long)hook_arm)

        // arm指令模式
        h->thumb = 0;
        // 自己實現的Hook函數地址
        h->patch = (unsigned int)hook_arm;
        // 被Hook目標函數的原函數地址
        h->orig = addr;

        // 用於Hook目標函數的調用地址爲新地址hook_arm
        h->jump[0] = 0xe59ff000; // LDR pc, [pc, #0]
        h->jump[1] = h->patch;
        // pc寄存器讀出的值實際上是當前指令地址加8
        // 把jump[2]的值加載進pc寄存器
        h->jump[2] = h->patch;

        // 保存原目標函數的12字節指令,用於函數的恢復
        for (i = 0; i < 3; i++)
            h->store[i] = ((int*)h->orig)[i];

        // 覆蓋目標函數的12字節指令爲Hook函數指令,實現對目標函數的Hook
        for (i = 0; i < 3; i++)
            ((int*)h->orig)[i] = h->jump[i];
    }

Thumb指令模式下Hook目標函數的處理方式和arm模式下的Hook處理一樣,但是基於thumb指令的長度不同,在對目標函數代碼指令的覆蓋上有所不同,Thumb指令模式下Hook目標函數需要20字節的Hook指令,Hook目標函數的操作是先保存目標函數的前20字節的指令,然後使用20個字節的Hook指令對目標函數進行覆蓋處理。

    // Thumb指令模式的Hook目標函數的處理
    else {

        // 對自定義Hook函數的調用地址進行指令模式的判斷
        if ((unsigned long int)hook_thumb % 4 == 0)
            log("warning hook is not thumb 0x%lx\n", (unsigned long)hook_thumb)

        // thumb指令模式
        h->thumb = 1;
        log("THUMB using 0x%lx\n", (unsigned long)hook_thumb)

        // 保存用於Hook目標函數的調用地址爲新地址hook_thumb
        h->patch = (unsigned int)hook_thumb;
        // 保存被Hook目標函數的原函數地址
        h->orig = addr; 

        // 保存寄存器r5,r6的值用於恢復環境(r6在高地址,r5在地址)
        h->jumpt[1] = 0xb4;
        h->jumpt[0] = 0x60; // push {r5,r6}
// 將PC寄存器的值加上12賦值給r5。加上的立即數必須是4的倍數,而加上8又不夠,只能加12。
// 這樣的話,讀出的PC寄存器的值是當前指令地址加上4,再加上12的話,那麼可以算出來r5寄存器的值實際指向的是jumpt[18],而不是jumpt[16]了。
// 這裏還有一點需要注意,對於Thumb的“Add Rd, Rp, #expr”指令來說,如果Rp是PC寄存器的話,那麼PC寄存器讀出的值應該是(當前指令地址+4)& 0xFFFFFFFC,
// 也就是去掉最後兩位,算下來正好可以減去2。但這裏也有個假設,就是被hook函數的起始地址必須是4字節對齊的,哪怕被hook函數是使用Thumb指令集編寫的。
        h->jumpt[3] = 0xa5;
        h->jumpt[2] = 0x03; // add r5, pc, #12 (比較難理解)
        // 將保存在jumpt[16]處的hook函數地址加載到r5寄存器中
        h->jumpt[5] = 0x68;
        h->jumpt[4] = 0x2d; // ldr r5, [r5]
        // 降低棧頂,恢復到初始的狀態,釋放內存空間
        h->jumpt[7] = 0xb0;
        h->jumpt[6] = 0x02; // add sp,sp,#8
        // 用保存的自定義hook函數地址覆蓋原來壓入的r6的值,r5的值暫時不受影響
        h->jumpt[9] = 0xb4;
        h->jumpt[8] = 0x20; // push {r5}
        // 擡高棧頂,r5的值被保護
        h->jumpt[11] = 0xb0;
        h->jumpt[10] = 0x81; // sub sp,sp,#4
        // 進行出棧操作,pc寄存器得到自定義的Hook函數的地址,r5的值還是原來的
        h->jumpt[13] = 0xbd;
        h->jumpt[12] = 0x20; // pop {r5, pc}
        // 僅僅用於4字節對齊的填充,只是因爲前面的add指令只能加4的倍數
        h->jumpt[15] = 0x46;
        h->jumpt[14] = 0xaf; // mov pc, r5 ; just to pad to 4 byte boundary

        // 用於存放自定義Hook函數的調用地址(4字節)
        memcpy(&h->jumpt[16], (unsigned char*)&h->patch, sizeof(unsigned int));
        // sub 1 to get real address,獲取到thumb指令模式下函數的真實調用地址
        unsigned int orig = addr - 1; 
        // 保存被Hook目標函數的原始thumb指令
        for (i = 0; i < 20; i++) {

            h->storet[i] = ((unsigned char*)orig)[i];
            //log("%0.2x ", h->storet[i])
        }
        //log("\n")

        // 覆蓋被Hook目標函數的指令爲自定義的Hook函數指令
        for (i = 0; i < 20; i++) {

            ((unsigned char*)orig)[i] = h->jumpt[i];
            //log("%0.2x ", ((unsigned char*)orig)[i])
        }

    }

Thumb指令模式下Hook目標函數的Hook指令比較難理解,當初也是思考了好久纔想明白了一些,主要參考的也是博主Roland_Sun的解釋和分析。知道自己很多地方說不清楚,因此有關Thumb指令模式下Hook指令的理解就借用博主Roland_Sun的理解,在此分析基礎上進行修改幫助理解。


和對Arm指令集的處理非常相似,只不過跳轉指令換成了Thumb。和Arm的處理不同,這裏是通過pop指令來修改PC寄存器的值實現函數的Hook跳轉操作。
1.首先,入棧r6和r5寄存器的值,並在arm指令操作中寄存器編號大在棧的高地址編號小在棧的低地址,將r5壓棧是因爲後面的指令執行修改了r5寄存器的值,壓棧後方便以後恢復,而將r6寄存器壓棧純粹是爲了要保留一個位置。
2.接着,將PC寄存器的值加上12賦值給r5,加上的立即數必須是4的倍數,而加上8又不夠,只能加12。這樣的話,讀出的PC寄存器的值是當前指令地址加上4,再加上12的話,那麼可以算出來r5寄存器的值實際指向的是jumpt[18],而不是jumpt[16]了。
3.這裏還有一點需要注意,對於Thumb模式下的“Add Rd, Rp, #expr”指令來說,如果Rp是PC寄存器的話,那麼PC寄存器讀出的值應該是(當前指令地址+4)& 0xFFFFFFFC,也就是去掉最後兩位,算下來正好可以減去2。但這裏也有個假設,就是被hook函數的起始地址必須是4字節對齊的,哪怕被hook函數使用Thumb指令集編寫的。
4.再下面的指令目的就是將保存在jumpt[16]處的自定義hook函數地址覆蓋r6寄存器在棧中的值,棧中r5寄存器的值不受影響,僅僅用於後面寄存器環境的恢復。所以,下面的“pop {r5, pc}”指令剛好可以完成恢復r5寄存器並且修改PC寄存器的值,從而實現跳轉到自定義hook函數地址處執行。
5.接下來的指令(從jumpt[14])完全是多餘的了,完全不會執行到,只是因爲前面的add指令只能加4字節的倍數。最後,還有一點不同的是,因爲被hook函數是Thumb指令集,所以其真正的內存映射地址是其符號地址減去1。


Hook操作覆蓋目標函數的代碼指令以後還需要刷新指令緩存。現代的處理器都有指令緩存,用來提高代碼指令的執行效率,ARM處理器也一樣也有指令緩存機制。雖然目標進程內存中被Hook目標函數的代碼指令已經改變,但是cache中的代碼指令可能仍爲原有的代碼指令,再進行代碼指令執行時還是優先執行緩存中的代碼指令,使得被Hook目標函數修改的指令得不到執行,所以需要手動刷新cache中的代碼指令,解決的方法是觸發Android系統隱藏刷新cache的系統調用。

// 調用Android系統的私有系統調用__ARM_NR_cacheflush實現緩存指令的刷新
void inline hook_cacheflush(unsigned int begin, unsigned int end)
{   
    const int syscall = 0xf0002;

    // 禁止編譯器對彙編指令進行指令優化
    __asm __volatile (
        "mov     r0, %0\n"          
        "mov     r1, %1\n"
        "mov     r7, %2\n"
        "mov     r2, #0x0\n"
        "svc     0x00000000\n"
        :
        :   "r" (begin), "r" (end), "r" (syscall) // 輸入列表
        :   "r0", "r1", "r7"                      // 修改寄存器列表
        );
}

這裏寫圖片描述

對目標函數進行Hook操作的時候還需要考慮對目標函數Hook的恢復還原和再次對目標函數進行Hook操作的處理。adbi的源碼文件adbi\instruments\base\hook.c中,hook_precall函數就是對目標函數進行Hook後的恢復還原,hook_postcall函數就是對目標函數進行恢復還原之後的再次Hook操作。

// 進行thumb或者arm模式被Hook目標函數指令的恢復即實現函數Hook的恢復
void hook_precall(struct hook_t *h)
{
    int i;

    // thumb指令模式被Hook目標函數的指令的恢復
    if (h->thumb) {

        // 獲取被Hook目標函數的真實調用地址
        unsigned int orig = h->orig - 1;
        // 進行thumb指令模式被Hook指令的恢復
        for (i = 0; i < 20; i++) {

            ((unsigned char*)orig)[i] = h->storet[i];
        }

    } else {

        // 進行arm指令模式被Hook指令的恢復
        for (i = 0; i < 3; i++){

            ((int*)h->orig)[i] = h->store[i];
        }
    }   

    // 刷新指令緩存
    hook_cacheflush((unsigned int)h->orig, (unsigned int)h->orig+sizeof(h->jumpt));
}
// 進行thumb或者arm指令模式Hook目標函數的指令覆蓋即實現函數的Hook
void hook_postcall(struct hook_t *h)
{
    int i;

    if (h->thumb) {

        // 獲取thumb指令模式函數真實的調用地址
        unsigned int orig = h->orig - 1;
        // 進行thumb指令模式Hook目標函數指令的覆蓋
        for (i = 0; i < 20; i++)
            ((unsigned char*)orig)[i] = h->jumpt[i];

    } else {

        // 進行arm指令模式Hook目標函數指令的覆蓋
        for (i = 0; i < 3; i++)
            ((int*)h->orig)[i] = h->jump[i];
    }

    // 刷新指令緩存
    hook_cacheflush((unsigned int)h->orig, (unsigned int)h->orig+sizeof(h->jumpt)); 
}

3 .自定義Hook函數Thumb模式和Arm模式的實現

很顯然,在上面的分析中提到的Hook目標函數實現操作中需要提供Thumb模式和Arm模式的自定義Hook函數的實現。在我們進行Hook目標函數的操作中並不知道要被Hook的目標函數是那種模式的指令集,只能通過被Hook目標函數的調用地址來判斷,因此需要提供Thumb模式和Arm模式的自定義Hook函數的實現。那麼,如何控制將代碼編譯成Arm指令集還是是Thumb指令集呢?


Android NDK默認情況下將C代碼編譯成Thumb指令,如果想將C代碼編譯成Arm指令集,有兩種方法:
1.在Android.mk文件中添加上“LOCAL_ARM_MODE := arm”,這樣會默認將所有的C代碼編譯成Arm指令集。
2.前面的方法只能將所有代碼全部編譯成Arm指令集,如果想一部分代碼編譯成Arm,一部分編譯成Thumb就力不從心了。想要達到這個目的,可以將那些你想編譯成Arm指令集的C代碼文件名字後面加上一個“.arm”後綴。而其它的沒有加上“.arm”後綴的C文件將使用“LOCAL_ARM_MODE”指定的指令集編譯,默認情況下是Thumb。注意,這裏只是在“LOCAL_SRC_FILES”裏列出的C文件名後加上“.arm”後綴就可以了,不要真的去改那個要編譯的C文件名。


adbi\instruments\example目錄下的實例是用第二種方法指定“epoll.c”編譯成Thumb指令,而“epoll_arm.c”編譯成Arm指令集,同時連接通過base編譯出的靜態庫。

這裏寫圖片描述

三、 adbi源碼中inline Hook實現的流程總結

  1. 在so庫文件加載注入到Android目標進程中調用so庫文件的構造函數時,調用inline Hook操作Hook目標進程的目標函數;
  2. 通過遍歷目標進程的內存佈局信息,獲取到被Hook目標函數所在的so庫文件的內存加載基地址以及解析該so庫文件的“.symtab”或者“.dynsym”節獲取被Hook目標函數的RVA,進而獲取到被Hook目標函數的調用地址;
  3. 通過判斷被Hook目標函數調用地址的最後兩位是不是全0,來判斷被Hook目標函數的指令運行模式是Thumb模式還是Arm模式;
  4. 如果是Arm指令集模式,先保存被Hook目標函數的前12個字節的代碼指令,然後使用12字節的Hook代碼指令覆蓋被Hook目標函數的前12個字節;
  5. 如果是Thumb指令集模式,先保存被Hook目標函數的前20個字節的代碼指令,然後使用20字節的Hook代碼指令覆蓋被Hook目標函數的前20個字節;
  6. 被Hook目標函數的代碼指令被Hook修改以後,調用Android系統的隱藏系統調用cacheflush刷新指令緩存,使inline Hook操作生效,待到下一次被Hook目標函數被調用就是調用的我們自定義的Hook函數。

本篇博文中使用到帶有註釋分析的Android Hook框架adbi的源碼下載地址:http://download.csdn.net/detail/qq1084283172/9893002


參考鏈接:
Android平臺下hook框架adbi的研究(下)
Android Arm Inline Hook

發佈了144 篇原創文章 · 獲贊 183 · 訪問量 71萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章