android hook 框架 ADBI 如何實現so函數掛鈎


上一篇 android 5 HOOK 技術研究之 ADBI 項目 02 分析了hijack.c, 這個文件編譯為一個可執行程序 hijack, 該程序實現了向目標進程注入一個動態庫的功能。這一篇繼續研究 adbi 項目其他源碼,解決真正替換目標進程函數的問題。

 

在開始之前,先看看 adbi 給出的一個例子,這個例子替換了目標進程epoll_wait函數的實現為自定義的實現:

首先,給出例子的epoll_wait自定義實現,共2個:

int my_epoll_wait_arm(int epfd, struct epoll_event *events, int maxevents, int timeout)
{
        return my_epoll_wait(epfd, events, maxevents, timeout);
}
int my_epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
{
        int (*orig_epoll_wait)(int epfd, struct epoll_event *events, int maxevents, int timeout);
        orig_epoll_wait = (void*)eph.orig;

        hook_precall(&eph);
        int res = orig_epoll_wait(epfd, events, maxevents, timeout);
        if (counter) {
                hook_postcall(&eph);
                log("epoll_wait() called\n");
                counter--;
                if (!counter)
                        log("removing hook for epoll_wait()\n");
        }

        return res;
}
hook(&eph, getpid(), "libc.", "epoll_wait", my_epoll_wait_arm, my_epoll_wait);
my_epoll_wait_arm 是 arm 格式指令下的 hook 函數,
my_epoll_wait 是 thumb 格式指令下的 hook 函數,
hook 函數將 libc 庫的 epoll_wait 函數替換成上述函數中的一個,並將一些關鍵信息存放在 eph 結構里。
查看 my_epoll_wait 這個函數主要執行3步: hook_precall, orig_epoll_wait, hook_postcall。 
hook_precall 將 eph 結構保存的原始 epoll_wait 地址重新賦值回去,orig_epoll_wait 調用原始 epoll_wait, 最后hook_postcall再用自定義的my_epoll_wait 賦值為原始epoll_wait. 一般inline hook 都是這種套路,這樣做可以在hook函數內部調用原始函數,以實現在原始函數之上做一些事情。
其中,eph 是 struct hook_t 結構,用於保存一次hook的相關信息,如下:
struct hook_t {
        unsigned int jump[3]; // 對應arm,12個字節,執行這部分指令可以調用 hook 函數
        unsigned int store[3]; // 對應arm, 12 個字節,執行這部分指令可以讓目標函數恢復到原始狀態
        unsigned char jumpt[20]; //對應 thumb,20個字節,執行這部分指令可以調用 hook 函數
        unsigned char storet[20];//對應 thumb, 20個字節,執行這部分指令可以讓目標函數恢復到原始狀態
        unsigned int orig; // 目標函數地址,真正發揮作用的指令,它的值可能是上述4種其一
        unsigned int patch; // hook函數地址
        unsigned char thumb; // 取1 表示指令格式是 thumb, 取 0 表示是 arm
        unsigned char name[128];// 被hook的目標函數名
        void *data;
};

 

下面逐步分析目標進程函數的劫持過程,首先第一個問題,需要找到目標函數在目標進程內的地址。

int hook(struct hook_t *h, int pid, char *libname, char *funcname, void *hook_arm, void *hook_thumb)
{ unsigned
long int addr; int i; if (find_name(pid, funcname, libname, &addr) < 0) { log("can't find: %s\n", funcname) return 0; }     。。。 }

這個問題,在  android 5 HOOK 技術研究之 ADBI 項目 02  分析向目標進程注入so的過程時已經有涉及(需要找到目標進程內的 dlopen 函數, mprotect 函數)。 在 hijack.c 的實現里,是通過解析本進程和目標進程的 maps 文件,得到本進程和目標進程 libdl.so 庫加載的起始地址,然后用dlsym獲取本進程內 dlopen 函數的地址,算出dlopen函數到 libdl.so 加載初始地址的 offset, 然后目標進程 libdl.so 的起始地址加上剛剛算出的 offset 即得到目標進程內部 dlopen的真實地址,這里,利用的是tracer進程和 tracee 進程加載的是同一個 libdl.so ,因而同一個符號在該動態庫里的offset是固定的。

 

那么,如果要hook的目標函數不是在一個動態庫里,而是tracee進程靜態鏈接的一個函數,上述方式就失效了,這時候,如何獲取目標函數地址呢?

這里,adbi項目的作者給出了另外一種方式,可以解決這個問題,find_name 函數仍然是獲取動態庫里的函數的地址,但這個實現改一下也可以用於獲取靜態鏈接的函數的地址:

 

int find_name(pid_t pid, char *name, char *libn, unsigned long *addr)
{
        struct mm mm[1000];
        unsigned long libcaddr;
        int nmm;
        char libc[1024];
        symtab_t s;

        if (0 > load_memmap(pid, mm, &nmm)) { // load_memap 函數加載 maps 文件並將內存塊解析成一個數組
                log("cannot read memory map\n")
                return -1;
        }
        if (0 > find_libname(libn, libc, sizeof(libc), &libcaddr, mm, nmm)) { // 在上述數組里使用so名字尋找對應的起                                              // 始地址
                log("cannot find lib: %s\n", libn)
                return -1;
        }
        //log("lib: >%s<\n", libc)
        s = load_symtab(libc); // 加載so文件,解析后獲取符號表
        if (!s) {
                log("cannot read symbol table\n");
                return -1;
        }
        if (0 > lookup_func_sym(s, name, addr)) { // 獲取目標函數在符號表里的地址值,這個值其實就是該函數相對於so起始地
                              // 址的offset log(
"cannot find function: %s\n", name); return -1; } *addr += libcaddr; // so 在maps文件里的起始地址 + 函數在so符號表里的值 = 函數在目標進程的地址 return 0; }
load_symtab, lookup_func_sym 這兩個函數在  android 5 HOOK 技術研究之 ADBI 項目 02  沒有出現過, load_symtab 的作用是使用 elf 格式解析 so 文件, (linux平台下, 可執行程序,動態庫,.o 文件,都是elf格式的文件, android 底層是linux), 獲取該elf文件的符號表。 lookup_func_sym 函數使用函數名遍歷符號表,找到該符號(函數)對應的地址,對與 so 文件來說,這時候找到的地址是一個以起始地址為0計算的地址,即本質是一個offset, 用這個值加上目標進程 maps 文件里 so 加載的起始地址,即得到了目標進程里函數的地址。

如果要hook的地址不是so的函數呢,則使用 load_symtab 加載目標進程的可執行程序並獲取符號表, lookup_func_sym 得到的函數地址就是真實地址。

如果目標進程對應的可執行程序,或者加載的so,符號被 strip 了(這時候其elf文件里沒有符號表),如果獲取函數地址 ?

 ==========

 找到要hook的函數的地址后,開始填充 hook_t 結構,並覆蓋原始地址:

int hook(struct hook_t *h, int pid, char *libname, char *funcname, void *hook_arm, void *hook_thumb)
{
    。。。
     log("hooking:   %s = 0x%x ", funcname, addr)
        strncpy(h->name, funcname, sizeof(h->name)-1); // 被hook的函數名

        if (addr % 4 == 0) { // ARM 格式
                log("ARM using 0x%x\n", hook_arm)
                h->thumb = 0;
                h->patch = (unsigned int)hook_arm; // hook 函數先保存在 h->patch 字段
                h->orig = addr; // 目標進程的被hook函數原始地址保存在 h->orig 字段
                h->jump[0] = 0xe59ff000; // LDR pc, [pc, #0] // h->jump 填充hook指令
                h->jump[1] = h->patch; // 新的hook函數地址放在 hook指令的第4到12個字節
                h->jump[2] = h->patch;
                for (i = 0; i < 3; i++)
                        h->store[i] = ((int*)h->orig)[i]; // 由於hook需要12個指令,這里先把原始12個字節的指令保存在 h-                                  //>store, unhook 時用到
                for (i = 0; i < 3; i++)
                        ((int*)h->orig)[i] = h->jump[i]; // 新的指令12個字節覆蓋老的指令
        }
        else {
                if ((unsigned long int)hook_thumb % 4 == 0) //thumb格式
                        log("warning hook is not thumb 0x%x\n", hook_thumb)
                h->thumb = 1;
                log("THUMB using 0x%x\n", hook_thumb)
                h->patch = (unsigned int)hook_thumb; // hook函數地址保存在 h->patch
                h->orig = addr; // 原始地址保存在 h->orig
                h->jumpt[1] = 0xb4; // jumpt 保存實現 hook 的指令,20字節
                h->jumpt[0] = 0x30; // push {r4,r5}
                h->jumpt[3] = 0xa5;
                h->jumpt[2] = 0x03; // add r5, pc, #12
                h->jumpt[5] = 0x68;
                h->jumpt[4] = 0x2d; // ldr r5, [r5]
                h->jumpt[7] = 0xb0;
                h->jumpt[6] = 0x02; // add sp,sp,#8
                h->jumpt[9] = 0xb4;
                h->jumpt[8] = 0x20; // push {r5}
                h->jumpt[11] = 0xb0;
                h->jumpt[10] = 0x81; // sub sp,sp,#4
                h->jumpt[13] = 0xbd;
                h->jumpt[12] = 0x20; // pop {r5, pc}
                h->jumpt[15] = 0x46;
                h->jumpt[14] = 0xaf; // mov pc, r5 ; just to pad to 4 byte boundary
                memcpy(&h->jumpt[16], (unsigned char*)&h->patch, sizeof(unsigned int));// hook函數地址賦值給 jump                                    //t 指令的第 16字節開始的位置
                unsigned int orig = addr - 1; // sub 1 to get real address
                for (i = 0; i < 20; i++) {
                        h->storet[i] = ((unsigned char*)orig)[i]; //同等數量(20字節)的原始指令保存在 h->storet 里
                        //log("%0.2x ", h->storet[i])
                }
                //log("\n")
                for (i = 0; i < 20; i++) {
                        ((unsigned char*)orig)[i] = h->jumpt[i]; // 新的指令覆蓋老的指令
                        //log("%0.2x ", ((unsigned char*)orig)[i])
                }
        }
        hook_cacheflush((unsigned int)h->orig, (unsigned int)h->orig+sizeof(h->jumpt));//刷新緩存
        return 1;
}

 上述注釋可以看到,hook函數需要分 arm 指令和 thumb 指令實現兩個分支的hook, 其中 , arm 格式需要替換 12 個字節的指令,thumb需要替換 20個字節,將原始的指令存放在 hook_t結構的 store/storet 字段,將新的指令構造好后存放在 hook_t結構的 jump/jumpt字段,hook_t結構的 orig 字段存放原始函數的地址,當執行 hook 調用時,用 jump/jumpt 指令覆蓋 orig地址對應長度的內存,這時候執行 orig 函數,就會執行到 jump/jumpt指令,也就是執行到hook函數,當執行 unhook 調用時,就用 store/storet 指令覆蓋 orig 地址對應長度的內存,這時候的這塊指令等於原始執行,相當於執行原始函數。

void unhook(struct hook_t *h)
{
        log("unhooking %s = %x  hook = %x ", h->name, h->orig, h->patch)
        hook_precall(h);
}

unhook函數,直接調用 hook_precall, 前面分析 my_epoll_wait 函數時發現,hook函數里也調用了 hook_precall, hook_postcall ,以實現在hook函數內可以調用原始函數,下面是這兩個函數的實現:

void hook_precall(struct hook_t *h)
{
        int i;

        if (h->thumb) {
                unsigned int orig = h->orig - 1;
                for (i = 0; i < 20; i++) {
                        ((unsigned char*)orig)[i] = h->storet[i];
                }
        }
        else {
                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));
}

void hook_postcall(struct hook_t *h)
{
        int i;

        if (h->thumb) {
                unsigned int orig = h->orig - 1;
                for (i = 0; i < 20; i++)
                        ((unsigned char*)orig)[i] = h->jumpt[i];
        }
        else {
                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));
}

實現比較簡單,參見上面的講述,這里不再贅述。

 

上述hook方式本質上是一個內存的賦值操作(((unsigned char*)orig)[i] = h->jumpt[i];)賦值操作的左值orig是代碼區的地址,但不意味着這個賦值操作完成后,對orig代碼區的調用就馬上會用到剛剛賦值的指令,這是因為,CPU執行指令時,是從緩存(cache)獲取的指令,內存的指令同步到緩存需要時間,所以,adbi 在每次對目標地址的指令做內存賦值操作后,都添加了一個刷新緩存的操作,人為觸發一次指令同步操作,

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"
                );
}

 

  1. #define __ARM_NR_BASE (__NR_SYSCALL_BASE+0x0f0000)   
  2. #define __ARM_NR_breakpoint (__ARM_NR_BASE+1)   
  3. #define __ARM_NR_cacheflush (__ARM_NR_BASE+2)   
  4. #define __ARM_NR_usr26 (__ARM_NR_BASE+3)   
  5. #define __ARM_NR_usr32 (__ARM_NR_BASE+4)   
  6. #define __ARM_NR_set_tls (__ARM_NR_BASE+5)

實現方式是會匯編調用一個系統調用  __ARM_NR_cacheflush (0xf0002 )

參考:http://blog.csdn.net/roland_sun/article/details/36049307 

 

Arm格式的hook指令: 12個字節,

前面4個字節是:  LDR pc, [pc, #0],pc寄存器讀出的值是當前值+8,所以這一句是把jump[2]對應的地址加載到 pc 寄存器,這一句指令執行后,程序計數器執行 jump[2], 即 h->patch ,即hook函數。  jump[1] 的值是用來填充空間的,可以任意
 h->jump[0] = 0xe59ff000; // LDR pc, [pc, #0] // h->jump 填充hook指令
                h->jump[1] = h->patch; // 新的hook函數地址放在 hook指令的第4到12個字節
                h->jump[2] = h->patch;

 

Thumb格式的hook指令:20個字節,

首先,hook函數的地址被存放在 jumpt數組(char型)的第16,20這4個字節處。

                h->jumpt[1] = 0xb4; 
                h->jumpt[0] = 0x30; // push {r4,r5}
                h->jumpt[3] = 0xa5;
                h->jumpt[2] = 0x03; // add r5, pc, #12
                h->jumpt[5] = 0x68;
                h->jumpt[4] = 0x2d; // ldr r5, [r5]
                h->jumpt[7] = 0xb0;
                h->jumpt[6] = 0x02; // add sp,sp,#8
                h->jumpt[9] = 0xb4;
                h->jumpt[8] = 0x20; // push {r5}
                h->jumpt[11] = 0xb0;
                h->jumpt[10] = 0x81; // sub sp,sp,#4
                h->jumpt[13] = 0xbd;
                h->jumpt[12] = 0x20; // pop {r5, pc}
                h->jumpt[15] = 0x46;
                h->jumpt[14] = 0xaf; // mov pc, r5 ; just to pad to 4 byte boundary
                memcpy(&h->jumpt[16], (unsigned char*)&h->patch, sizeof(unsigned int));

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM