上一篇 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" ); }
- #define __ARM_NR_BASE (__NR_SYSCALL_BASE+0x0f0000)
- #define __ARM_NR_breakpoint (__ARM_NR_BASE+1)
- #define __ARM_NR_cacheflush (__ARM_NR_BASE+2)
- #define __ARM_NR_usr26 (__ARM_NR_BASE+3)
- #define __ARM_NR_usr32 (__ARM_NR_BASE+4)
- #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));
