Android Hook框架adbi原始碼淺析(一)
adbi(The Android Dynamic Binary Instrumentation Toolkit)是一個Android平臺通用hook框架,基於動態庫注入與inline hook技術實現。
該框架由兩個主要模組構成,1.hijack負責將動態庫注入到目標程序;2.libbase提供動態庫本身,它實現了通用的hook功能。
而example則是一個使用adbi進行epoll_wait hook的demo。
[email protected]:~/Android/adbi-master$ tree . ├── build.sh ├── clean.sh ├── hijack │ ├── hijack.c │ └── jni │ └── Android.mk ├── instruments │ ├── base │ │ ├── base.c │ │ ├── base.h │ │ ├── hook.c │ │ ├── hook.h │ │ ├── jni │ │ │ ├── Android.mk │ │ │ └── Application.mk │ │ ├── util.c │ │ └── util.h │ └── example │ ├── epoll_arm.c │ ├── epoll.c │ └── jni │ └── Android.mk └── README.md 7 directories, 16 files[email protected]:~/Android/adbi-master$
一、hijack
hijack實現程序注入功能,通過在目標程序插入dlopen()呼叫序列,載入指定SO動態庫檔案。要實現這個功能,主要做兩件事情:1.獲得目標程序中dlopen()地址;2.在目標程序的棧空間上構造一處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); //printf("own linker: 0x%x\n", lkaddr); //printf("offset %x\n", dlopenaddr - lkaddr); find_linker(pid, &lkaddr2); //printf("tgt linker: %x\n", lkaddr2); //printf("tgt dlopen : %x\n", lkaddr2 + (dlopenaddr - lkaddr)); dlopenaddr = lkaddr2 + (dlopenaddr - lkaddr);
首先呼叫 void *ldl = dlopen(“libdl.so”, RTLD_LAZY); 返回動態庫libdl.so地址,我們的目標函式dlopen()就在這個庫中實現。但是libdl.so是動態載入的,在每個程序中地址並不固定。看一下adbi如何解決這個問題:
static int find_linker(pid_t pid, unsigned long *addr) { struct mm mm[1000]; unsigned long libcaddr; int nmm; char libc[256]; symtab_t s; if (0 > load_memmap(pid, mm, &nmm)) { printf("cannot read memory map\n"); return -1; } if (0 > find_linker_mem(libc, sizeof(libc), &libcaddr, mm, nmm)) { printf("cannot find libc\n"); return -1; } *addr = libcaddr; return 1; }
主要呼叫了load_memmap和find_linker_mem兩個函式。
首先分析load_memmap函式,這個函式分3個步驟:
static int load_memmap(pid_t pid, struct mm *mm, int *nmmp) { char raw[80000]; // increase this if needed for larger "maps" char name[MAX_NAME_LEN]; char *p; unsigned long start, end; struct mm *m; int nmm = 0; int fd, rv; int i; sprintf(raw, "/proc/%d/maps", pid); fd = open(raw, O_RDONLY); if (0 > fd) { //printf("Can't open %s for reading\n", raw); return -1; }
(1)首先通過/proc//maps讀取目標程序的記憶體對映資訊,其格式大致如下:
2a002000-2a003000 r–p 00001000 1f:00 933 /system/bin/app_process
2a003000-2a1df000 rw-p 2a003000 00:00 0 [heap]
40000000-4000f000 r-xp 00000000 1f:00 984 /system/bin/linker
接下來一行行讀取檔案內容並解析:
/* (2)讀檔案內容 */ /* Zero to ensure data is null terminated */ memset(raw, 0, sizeof(raw)); p = raw; while (1) { rv = read(fd, p, sizeof(raw)-(p-raw)); if (0 > rv) { //perror("read"); return -1; } if (0 == rv) break; p += rv; if (p-raw >= sizeof(raw)) { //printf("Too many memory mapping\n"); return -1; } } close(fd);
/* (3)解析之 */ p = strtok(raw, "\n"); m = mm; while (p) { /* parse current map line */ rv = sscanf(p, "%08lx-%08lx %*s %*s %*s %*s %s\n", &start, &end, name); /* 分割每行內容 */ p = strtok(NULL, "\n"); if (rv == 2) { m = &mm[nmm++]; m->start = start; m->end = end; strcpy(m->name, MEMORY_ONLY); /* 40012000-40014000 r–p 40012000 00:00 0為空的情況 */ continue; } /* search backward for other mapping with same name */ for (i = nmm-1; i >= 0; i--) { m = &mm[i]; if (!strcmp(m->name, name)) break; } if (i >= 0) { /* 對名稱相同行進行合併 */ if (start < m->start) m->start = start; if (end > m->end) m->end = end; } else { /* new entry */ m = &mm[nmm++]; m->start = start; m->end = end; strcpy(m->name, name); /* 取每行最後的名稱段 */ } } *nmmp = nmm; return 0; }
繼續看find_linker_mem()功能:
static int find_linker_mem(char *name, int len, unsigned long *start, struct mm *mm, int nmm) { int i; struct mm *m; char *p; for (i = 0, m = mm; i < nmm; i++, m++) { //printf("name = %s\n", m->name); //printf("start = %x\n", m->start); if (!strcmp(m->name, MEMORY_ONLY)) continue; p = strrchr(m->name, '/'); if (!p) continue; p++; if (strncmp("linker", p, 6)) continue; break; // 'libc.so' or 'libc-[0-9]' */ if (!strncmp(".so", p, 3) || (p[0] == '-' && isdigit(p[1]))) break; } /* 獲取/system/bin/linker載入地址 */ if (i >= nmm) /* not found */ return -1; *start = m->start; strncpy(name, m->name, len); if (strlen(m->name) >= len) name[len-1] = '\0'; return 0; }
這段程式碼的作用是獲取/system/bin/linker在目標程序的載入地址。
linker是android提供的動態連結器,被各程序間共用。dlopen()函式就是在linker裡面定義,所以其內部的dlopen()函式相對於linker頭的偏移量是固定的,這樣計算其它程序內dlopen()函式的地址就非常簡單了,先在本程序內計算出dlopen()相對於linker頭的偏移量,再加上目標程序中linker的載入地址。
而linker的載入地址,就是上面通過/proc/<?>/maps讀到的40000000-4000f000 r-xp 00000000 1f:00 984 /system/bin/linker開始地址。
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對其進行修改了,如獲取暫存器值:
ptrace(PTRACE_GETREGS, pid, 0, ®s);
接下來要做的就是修改暫存器的值,在目標程序的棧空間上構造一處dlopen()呼叫,關鍵在於一個sc陣列:
unsigned int sc[] = { 0xe59f0040, // ldr r0, [pc, #64] ; 48 <.text+0x48> 0xe3a01000, // mov r1, #0 ; 0x0 0xe1a0e00f, // mov lr, pc 0xe59ff038, // ldr pc, [pc, #56] ; 4c <.text+0x4c> 0xe59fd02c, // ldr sp, [pc, #44] ; 44 <.text+0x44> 0xe59f0010, // ldr r0, [pc, #16] ; 30 <.text+0x30> 0xe59f1010, // ldr r1, [pc, #16] ; 34 <.text+0x34> 0xe59f2010, // ldr r2, [pc, #16] ; 38 <.text+0x38> 0xe59f3010, // ldr r3, [pc, #16] ; 3c <.text+0x3c> 0xe59fe010, // ldr lr, [pc, #16] ; 40 <.text+0x40> 0xe59ff010, // ldr pc, [pc, #16] ; 44 <.text+0x44> 0xe1a00000, // nop r0 0xe1a00000, // nop r1 0xe1a00000, // nop r2 0xe1a00000, // nop r3 0xe1a00000, // nop lr 0xe1a00000, // nop pc 0xe1a00000, // nop sp 0xe1a00000, // nop addr of libname 0xe1a00000, // nop dlopenaddr };
可以發現,這裡使用了上文獲取到的暫存器值,初始化了部分陣列元素:
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實現了什麼樣的功能。
1.首先指令從2處開始執行,ldr r0,[pc,#64] 將pc+64指向地址的內容存入r0暫存器,即圖中libaddr(.so地址)項,對其取值則r0指向.SO庫路徑名字串。(說明:對ARM指令集而言,PC總是指向當前指令的下兩條指令的地址,即PC的值為當前指令的地址值加8個位元組。所以[pc,#64]指向第(64+8)/4=18個元素處)
2.mov r1,#0 將0賦值給r1暫存器。
3.ldr pc,[pc,#56] 呼叫dlopen()函式,第一個入參為r0:so庫路徑名字串,第二個引數為r1:0。
4.函式執行完後,通過設定PC回到1處繼續執行,依次恢復pc/sp/r0/r1/r2/r3暫存器。
下面就可以將我們精心構造好的shellcode寫入到目標程序棧空間上:
// write library name to stack if (0 > write_mem(pid, (unsigned long*)arg, n, libaddr)) { printf("cannot write library name (%s) to stack, error!\n", arg); exit(1); } // write code to stack codeaddr = regs.ARM_sp - sizeof(sc); if (0 > write_mem(pid, (unsigned long*)&amp;amp;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並不能執行,因為當前linux都開啟了棧執行保護的功能。可以檢視棧屬性進行印證,沒有x位:
beeaf000-beec4000 rw-p befeb000 00:00 0 [stack]
但我們可以通過mprotect()函式,來修改棧記憶體的可執行許可權:
// calc stack pointer regs.ARM_sp = regs.ARM_sp - n*4 - sizeof(sc); // call mprotect() to make stack executable regs.ARM_r0 = stack_start; // want to make stack executable //printf("r0 %x\n", regs.ARM_r0); regs.ARM_r1 = stack_end - stack_start; // stack size //printf("mprotect(%x, %d, ALL)\n", regs.ARM_r0, regs.ARM_r1); regs.ARM_r2 = PROT_READ|PROT_WRITE|PROT_EXEC; // protections // normal mode, first call mprotect if (nomprotect == 0) { if (debug) printf("calling mprotect\n"); regs.ARM_lr = codeaddr; // points to loading and fixing code regs.ARM_pc = mprotectaddr; // execute mprotect() } // no need to execute mprotect on old Android versions else { regs.ARM_pc = codeaddr; // just execute the 'shellcode' }
這段程式碼首先計算棧頂位置,接著將 棧起始地址/棧大小/許可權位 3個引數壓棧,然後呼叫mprotect()設定程式碼所在棧區的可執行許可權,最後將lr暫存器設定為棧上程式碼的起始地址,這樣當呼叫mprotect()函式返回後就可以正常執行棧上程式碼了。
最後,恢復目標程序的暫存器值,並恢復被ptrace()暫停的程序:
// detach and continue ptrace(PTRACE_SETREGS, pid, 0, &amp;amp;regs); ptrace(PTRACE_DETACH, pid, 0, (void *)SIGCONT); if (debug) printf("library injection completed!\n");
到目前為止,我們已經能夠在指定程序載入任意SO庫了!