Android Native Hook技術(一)
原理分析
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, ®s);
接下來要做的就是修改寄存器的值,在目標進程的棧空間上構造一處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, ®s);
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技術(一)