1. 程式人生 > >Android Native Hook技術(一)

Android Native Hook技術(一)

數組 chef protect 獲取 通過 防止 example tile ofo

原理分析

ADBI是一個著名的安卓平臺hook框架,基於 動態庫註入inline hook 技術實現。該框架主要由2個模塊構成:1)hijack負責將so註入到目標進程空間,2)libbase是註入的so本身,提供了inline hook能力。

源碼目錄中的example則是一個使用ADBI進行hook epoll_wait的示例。

hijack

hijack實現動態庫註入功能,通過在目標進程插入dlopen()調用序列,加載指定so文件。要實現這個功能,主要做兩件事情:

  • 獲得目標進程中dlopen()地址
  • 在目標進程的棧空間上構造一處dlopen()調用

下面分別解決這兩個問題

1. 獲得目標進程中dlopen()地址

在ADBI中,通過下面代碼來獲得目標進程中dlopen()函數地址:

void *ldl = dlopen("libdl.so", RTLD_LAZY);
if (ldl) {
    dlopenaddr = (unsigned long)dlsym(ldl, "dlopen");
    dlclose(ldl);
}
unsigned long int lkaddr;
unsigned long int lkaddr2;

find_linker(getpid(), &lkaddr);
find_linker(pid, &lkaddr2);

dlopenaddr = lkaddr2 + (dlopenaddr - lkaddr);

其中find_linker()函數功能是獲取指定進程中linker的地址。

linker是Android提供的動態鏈接器,每個進程都會映射一份到自己的進程空間,而dlopen()函數就是在linker裏面定義,其相對於linker頭部偏移是固定的。

因此要計算某進程中dlopen()函數地址,只需分別取當前進程linker地址lkaddr和dlopen()地址dlopenaddr,並通過 /proc/pid_xxx/maps 讀取被註入進程linker地址lkaddr2。

dlopenaddr = lkaddr2 + (dlopenaddr - lkaddr) 即為目標進程中dlopen()地址。

2. 在目標進程的棧空間上構造dlopen()調用

要修改目標進程寄存器等信息,需使用到ptrace()函數,gdb等程序擁有查看、修改調試進程寄存器等的能力就是因為使用了ptrace()。

先將hijack attach到目標進程上去:

if (0 > ptrace(PTRACE_ATTACH, pid, 0, 0)) {
    printf("cannot attach to %d, error!\n", pid);
    exit(1);
}
waitpid(pid, NULL, 0);

這時目標進程暫停,就可以通過ptrace對其進行修改了,以下代碼獲取寄存器值保存在regs中:

ptrace(PTRACE_GETREGS, pid, 0, &regs);

接下來要做的就是修改寄存器的值,在目標進程的棧空間上構造一處dlopen()調用,關鍵在sc數組:


unsigned int sc[] = { 0xe59f0040, //0 ldr r0, [pc, #64] 0xe3a01000, //1 mov r1, #0 0xe1a0e00f, //2 mov lr, pc 0xe59ff038, //3 ldr pc, [pc, #56] 0xe59fd02c, //4 ldr sp, [pc, #44] 0xe59f0010, //5 ldr r0, [pc, #16] 0xe59f1010, //6 ldr r1, [pc, #16] 0xe59f2010, //7 ldr r2, [pc, #16] 0xe59f3010, //8 ldr r3, [pc, #16] 0xe59fe010, //9 ldr lr, [pc, #16] 0xe59ff010, //10 ldr pc, [pc, #16] 0xe1a00000, //11 nop r0 0xe1a00000, //12 nop r1 0xe1a00000, //13 nop r2 0xe1a00000, //14 nop r3 0xe1a00000, //15 nop lr 0xe1a00000, //16 nop pc 0xe1a00000, //17 nop sp 0xe1a00000, //18 nop addr of libname 0xe1a00000, //19 nop dlopenaddr };

接下來使用上文取到的寄存器值對sc數組進行初始化:


sc[11] = regs.ARM_r0; sc[12] = regs.ARM_r1; sc[13] = regs.ARM_r2; sc[14] = regs.ARM_r3; sc[15] = regs.ARM_lr; sc[16] = regs.ARM_pc; sc[17] = regs.ARM_sp; sc[19] = dlopenaddr; libaddr = regs.ARM_sp - n*4 - sizeof(sc); sc[18] = libaddr;

上面代碼數組內容就是我們要寫入到目標進程當前棧空間的指令,即一份shellcode,接下來看一下這段shellcode實現了什麽樣的功能:

ldr r0,[pc,#64]

將so路徑字符串地址存入r0

ARM指令集中PC寄存器總 指向當前指令的下兩條指令 地址處,這是為了加快指令執行速度,如下圖第一條指令執行時,第三條指令已經在讀取:

指令一 > 讀取    解析    執行
指令二 >         讀取    解析    執行
指令三 >                 讀取    解析    執行

因此PC+64實際指向sc[18]的位置,取其內容即為so路徑字符串的地址

mov r1,#0

將0賦值給r1寄存器。

ldr pc,[pc,#56]

調用dlopen()函數,第一個參數r0為so路徑符串地址,第二個參數r1為0。

ldr sp, [pc, #44] ldr r0, [pc, #16] ldr r1, [pc, #16] ldr r2, [pc, #16] ldr r3, [pc, #16] ldr lr, [pc, #16] ldr pc, [pc, #16]

函數執行完後,依次恢復保存的 sp/r0/r1/r2/r3/lr/pc 寄存器,並繼續執行。

接下來我們通過ptrace調用,將上面構造的shellcode以及so路徑字符串寫入到目標進程棧上:


// so name寫入棧 if (0 > write_mem(pid, (unsigned long*)arg, n, libaddr)) { printf("cannot write library name (%s) to stack, error!\n", arg); exit(1); } // shellcode 寫入棧 codeaddr = regs.ARM_sp - sizeof(sc); if (0 > write_mem(pid, (unsigned long*)&sc, sizeof(sc)/sizeof(long), codeaddr)) { printf("cannot write code, error!\n"); exit(1); }

/* Write NLONG 4 byte words from BUF into PID starting
   at address POS.  Calling process must be attached to PID. */
static int
write_mem(pid_t pid, unsigned long *buf, int nlong, unsigned long pos)
{
    unsigned long *p;
    int i;
 
    for (p = buf, i = 0; i < nlong; p++, i++)                                           
        if (0 > ptrace(PTRACE_POKETEXT, pid, (void *)(pos+(i*4)), (void *)*p))
            return -1;
    return 0;
}

寫入棧以後,shellcode並不能執行,因為當前Android都開啟了棧執行保護,需要先通過mprotect(),來修改棧的可執行權限:


// 計算棧頂指針 regs.ARM_sp = regs.ARM_sp - n*4 - sizeof(sc); // 調用mprotect()設置棧可執行 regs.ARM_r0 = stack_start; // 棧起始位置 regs.ARM_r1 = stack_end - stack_start; // 棧大小 regs.ARM_r2 = PROT_READ|PROT_WRITE|PROT_EXEC; // 權限 if (nomprotect == 0) { if (debug) printf("calling mprotect\n"); regs.ARM_lr = codeaddr; // lr指向shellcode,mprotect()後執行 regs.ARM_pc = mprotectaddr; } // 舊版本Android沒有棧保護,Android 2.3引入 else { regs.ARM_pc = codeaddr; }

這段代碼首先計算棧頂位置,接著將棧 起始地址/棧大小/權限位 3個參數壓棧,然後調用mprotect()函數設置棧的可執行權限,最後將lr寄存器設置為棧上代碼的起始地址,這樣當mprotect()函數返回後就可以正常執行棧上代碼了。

最後,恢復目標進程的寄存器值,並恢復被ptrace()暫停的進程:

ptrace(PTRACE_SETREGS, pid, 0, &regs);
ptrace(PTRACE_DETACH, pid, 0, (void *)SIGCONT);
 
if (debug)
    printf("library injection completed!\n");

到目前為止,我們已經能夠在指定進程加載任意so庫了!

libbase

其實so註入到目標進程中後,hook功能完全可以在init_array中實現,但ADBI為了方便我們使用,編寫了一個通用的hook框架libbase.so

libbase依然要解決2個問題:

  • 定位被 hook 函數位置
  • 進行 inline hook

關於獲取hook函數地址的方法這裏不再贅述。直接看inline hook部分,這部分功能在hook.c的hook()函數中實現,先看hook_t結構體:

struct hook_t {
    unsigned int jump[3];           // 跳轉指令(ARM)
    unsigned int store[3];          // 原指令(ARM)
    unsigned char jumpt[20];        // 跳轉指令(Thumb)
    unsigned char storet[20];       // 原指令(Thumb)
    unsigned int orig;              // 被hook函數地址
    unsigned int patch;             // 補丁地址
    unsigned char thumb;            // 補丁代碼指令集,1為Thumb,2為ARM
    unsigned char name[128];        // 被hook函數名
    void *data;
};

hook_t是一個標準inline hook結構體,保存了 跳轉指令/跳轉地址/指令集/被hook函數名 等信息。因為ARM使用了ARM和Thumb兩種指令集,所以代碼中需進行區分:


if (addr % 4 == 0) { /* ARM指令集 */ } else { /* Thumb指令集 */ }

這樣進行判斷的依據是,Thumb指令的地址最後一位固定為 1

接下來看一下ARM指令集分支的處理流程,這是該問題解決的核心部分:

if (addr % 4 == 0) {
    log("ARM using 0x%lx\n", (unsigned long)hook_arm)
    h->thumb = 0;
    h->patch = (unsigned int)hook_arm;
    h->orig = addr;
    h->jump[0] = 0xe59ff000; // LDR pc, [pc, #0]
    h->jump[1] = h->patch;
    h->jump[2] = h->patch;
    for (i = 0; i < 3; i++)                                  
                h->store[i] = ((int*)h->orig)[i];
    for (i = 0; i < 3; i++)                                  
                    ((int*)h->orig)[i] = h->jump[i];
}

首先填充hook_t結構體,第一個for循環保存了原地址處3條指令,共12字節。第二個for循環用新的跳轉指令進行覆寫,關鍵的三條指令分別保存在jump[0]-[2]中:

jump[0]賦值0xe59ff000,翻譯成ARM匯編為 ldr pc,[pc,#0] ,由於pc寄存器讀出的值是當前指令地址加8,因此這條指令實際是將jump[2]的值加載到pc寄存器。
jump[2]保存的是hook函數地址。jump[1]僅用來4字節占位。Thumb分支原理與ARM分支一致,不再分析。

接下來我們註意到,函數最後調用了一處hook_cacheflush()函數:


hook_cacheflush((unsigned int)h->orig, (unsigned int)h->orig+sizeof(h->jumpt));

我們知道,現代處理器都有指令緩存,用來提高執行效率。前面我們修改的是內存中的指令,為防止緩存的存在,使我們修改的指令執行不到,需進行緩存的刷新:

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

參考資料:

  • ADBI github源碼下載
  • [ARM Cache Flush on mmap’d Buffers with __clear_cache()]bb

Android Native Hook技術(一)