標籤:
源碼分析
hijack.c
這個檔案實現了一個注入工具,可以向 -p 參數指定的進程注入一個so。
要實現這個效果,首先,需要得到目標進程若干函數如dlopen函數的地址,其次,需要能影響目標進程的正常執行流,讓其中間某個時候執行dlopen載入指定的庫,最後,還要能用動態載入的so裡的函數覆蓋原有記憶體裡的函數。
下面開始研究,如何得到目標進程指定函數的地址,首先要得到的是dlopen函數的地址,adbi是這麼做的:
void *ldl = dlopen("libdl.so", RTLD_LAZY); if (ldl) { dlopenaddr = (unsigned long)dlsym(ldl, "dlopen");//dlopenaddr 存放本進程的dlopen函數地址 dlclose(ldl); } unsigned long int lkaddr; unsigned long int lkaddr2; find_linker(getpid(), &lkaddr); find_linker(pid, &lkaddr2); dlopenaddr = lkaddr2 + (dlopenaddr - lkaddr); // dlopenaddr 存放目標進程的dlopen函數的地址
上述代碼是為了得到目標進程的dlopen函數地址。
首先,dlopen載入libdl.so,由於進程啟動後libdl.so肯定會先載入好,所以這裡返回已經載入好的libdl.so映射在本進程的起始地址空間,然後調用dlsym返回本進程的dlopen函數地址。
接著,find_linker函數利用 /proc/pid/maps 檔案可以得到進程pid的地址空間進而得到libdl.so映射到記憶體的起始地址,其中,注入進程的libdl.so映射的初始地址是 lkaddr, 目標進程是lkaddr2
最後,再利用dlopen函數在libdl.so動態庫的代碼的位移是固定的(注入進程和被注入進程使用的是同一個libdl.so),dlopenaddr - lkaddr 先算出這個位移值,lkaddr2 再上上述位移值即得到目標進程的 dlopen 函數的地址
maps檔案在linux和android上的地址塊命名有些區別,一般linux上libdl.so映射的地址是這樣的
7f6a96672000-7f6a96695000 r-xp 00000000 08:01 397502 /lib/x86_64-linux-gnu/ld-2.19.so
android 裡的命名叫 linker
find_linker 函數調用了 load_memmap函數和 find_linker_mem函數,
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 函數基本流程:開啟maps檔案,按照maps檔案的格式解析成一個數組,每一項存放一個動態庫的名稱以及其映射到記憶體裡的起始和結束位址
static intload_memmap(pid_t pid, struct mm *mm, int *nmmp){ char raw[80000]; // this depends on the number of libraries an executable uses 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; } /* 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); 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); continue; } if (strstr(name, "stack") != 0) { stack_start = start; stack_end = end; } /* 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函數的流程:遍曆上述數組,根據動態庫名稱匹配,即可擷取libdl.so對應的數組元素,從而得到libdl.so在進程內的起始和終止地址,代碼這裡就不貼了。
以上,是擷取目標進程某個動態庫內的函數在目標進程的真真實位址的方法。那麼目標進程,非動態庫函數的地址怎麼擷取呢?
=== ==
接下去研究第二個問題,如何影響目標進程的執行流,這裡必須介紹ptrace函數了。
ptrace
SYNOPSIS #include <sys/ptrace.h> long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);DESCRIPTION The ptrace() system call provides a means by which one process (the "tracer") may observe and control the execution of another process (the "tracee"), and examine and change the tracee‘s memory and registers. It is primarily used to implement breakpoint debugging and system call tracing.
動態庫注入技術一般都依賴於ptrace機制,ptrace是linux kernel 為了支援應用程式層debug功能而實現的系統調用,這個系統調用提供了“讓A進程關聯到B進程,並動態修改B進程的記憶體和寄存器”的機制,由於A進程可以動態修改B進程的記憶體空間和寄存器的值,所有可以影響B進程的執行序列,比如在正常的執行序列裡插入某段代碼,讓B進程載入一個動態庫。
下面研究adbi是怎麼使用ptrace達到目的的:
首先,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_GETREGS, pid, 0, ®s);
接著,構造新的寄存器值,這一步是關鍵。
數組sc存放初始化的指令
unsigned int sc[] = {0xe59f0040, // ldr r0, [pc, #64] ; 48 <.text+0x48>0xe3a01000, // mov r1, #0 ; 0x00xe1a0e00f, // mov lr, pc0xe59ff038, // ldr pc, [pc, #56] ; 4c <.text+0x4c>0xe59fd02c, // ldr sp, [pc, #44] ; 44 <.text+0x44>0xe59f0010, // ldr r0, [pc, #20] ; 30 <.text+0x30>0xe59f1010, // ldr r1, [pc, #20] ; 34 <.text+0x34>0xe59f2010, // ldr r2, [pc, #20] ; 38 <.text+0x38>0xe59f3010, // ldr r3, [pc, #20] ; 3c <.text+0x3c>0xe59fe010, // ldr lr, [pc, #20] ; 40 <.text+0x40>0xe59ff010, // ldr pc, [pc, #20] ; 44 <.text+0x44>0xe1a00000, // nop r00xe1a00000, // nop r1 0xe1a00000, // nop r2 0xe1a00000, // nop r3 0xe1a00000, // nop lr 0xe1a00000, // nop pc0xe1a00000, // nop sp0xe1a00000, // nop addr of libname0xe1a00000, // nop dlopenaddr};
下面使用ptrace擷取的寄存器值填充到sc數組的11到17,
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;
然後用前面擷取到的目標進程的dlopen函數的地址填充到第19位置,18位置存放動態庫的名字字串的地址,然後調用wirte_mem函數將動態庫的名字字串寫到libaddr地址指定的記憶體區。
sc[19] = dlopenaddr; // push library name to stack libaddr = regs.ARM_sp - n*4 - sizeof(sc); sc[18] = libaddr; // 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); }
其中,n是這麼算的,動態庫(如 /data/local/tmp/libexample.so)的位元組數+1,然後除以4,如果有餘數,結果加1. 其實,得到的n就是以‘4位元組’為單位的數值,這麼算主要是 write_mem函數的實現,下面會看到。結合上面的代碼,這個字串會被寫入 libaddr 對應的記憶體,這個記憶體位址是這麼算的:
regs.ARM_sp - n*4 - sizeof(sc); 即原來的棧頂指標往低地址移動 “sc 數組大小+動態庫字串位元組長度”
case ‘l‘: n = strlen(optarg)+1; n = n/4 + (n%4 ? 1 : 0); arg = malloc(n*sizeof(unsigned long)); memcpy(arg, optarg, n*4); break;
下面看write_mem是怎麼將一塊資料寫入目標進程的記憶體位址的:
static intwrite_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;}
pid是目標進程標識,buf是要寫入目標進程記憶體的資料區塊,nlong是‘4位元組’為單位的長度,pos是要寫入的地址。 由於資料buf是 long 型數組,所以迴圈一次即寫入4位元組的資料。最終是調用 ptrace 函數,另第一個參數為 PTRACE_POKETEXT 實現寫入的。
接下去,寫入新的指令資料(即sc數組)到目標進程:
// write code to stack 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); }// calc stack pointer
可以看到,方法跟上述寫動態庫名字字串類似,要寫入的目標地址 = 棧頂指標 - Sc 數組長度,然後調用write_mem函數將數組Sc寫入
接下去,移動棧頂指標為新的棧頂(往低地址移動”sc數組長度+動態庫名字字串長度“),接下去,根據是否有 mprotect 調用,會有兩種執行流:如果沒有mprotect,則將PC寄存器的值變成sc數組開始的位置,即接下去直接執行Sc數組的指令。否則,pc寄存器的值設定為 mprotect 函數,然後將lr寄存器設定為sc數組。並將r0,r1,r2參數設定為 mprotect調用的參數,這樣,首先執行 mprotect 函數將 r0,r1指定的記憶體範圍設定為 r2指定的許可權,在這個例子是,是將目標記憶體設定為 rwx, 執行完mprotect後再執行 sc 數組的指令。
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‘ }
經過上述設定後,新的寄存器值如:
結合上面的圖,新的指令流執行如下(下面轉自”參考1“)
一點需要說明一下,對於ARM處理器來說,pc寄存器的值,指向的不是當前正在執行指令的地址,而是往下第二條指令的地址。
好,我們正式開始分析代碼的含義,指令將從codeaddr指示的位置從低到高依次執行。
第一條指令將pc寄存器的值加上64,讀出那個地方的內容(4個位元組),然後放到寄存器r0中。剛才說過了,pc寄存器值指向的是當前指令位置加8個位元組,也就是說這條指令實際讀出的是當前指令位置向下72個位元組。由於sc數組是int型的,就是數組當前元素位置向下18個元素處。數一數,剛好是libaddr的位置。所以這條指令是為了讓r0寄存器指向.so共用庫路徑名字串。
第二條指令很簡單,是將0賦值給寄存器r1。
第三條指令用來將pc寄存器值儲存到lr寄存器中,這樣做的目的是為了調用dlopen()函數返回後,跳轉到指令“ldr sp, [pc, #56]”處。
第四條指令是將pc加上56處的數值載入到pc中,pc+56處是哪?當前指令位置往下64位元組,16個元素,剛好是dlopen()函數的調用地址。所以,這條指令其實就是調用dlopen()函數,傳入的參數一個是r0寄存器指向的共用庫路徑名,另一個是r1寄存器中的0。
調用dlopen()返回後將繼續執行下面的所有指令,我就不一一分析了,作用就是恢複目標進程原來寄存器的值。先是sp,然後是r0、r1、r2、r3和lr,最後恢複原來pc的值,繼續執行被暫停之前的指令,就像什麼都沒發生過一樣。
=====
最後,使用ptrace設定新的寄存器值進入目標記憶體,hook開始生效
ptrace(PTRACE_SETREGS, pid, 0, ®s); ptrace(PTRACE_DETACH, pid, 0, (void *)SIGCONT);
最後,如果參數有 -s ,還會執行下述流程:
if (appname) { if (ptrace(PTRACE_SETOPTIONS, pid, (void*)1, (void*)(PTRACE_O_TRACEFORK))) { printf("FATAL ERROR: ptrace(PTRACE_SETOPTIONS, ...)"); return -1; } ptrace(PTRACE_CONT, pid, (void*)1, 0); int t; int stat; int child_pid = 0; for (;;) { t = waitpid(-1, &stat, __WALL|WUNTRACED); if (t != 0 && t == child_pid) {char fname[256]; sprintf(fname, "/proc/%d/cmdline", child_pid); int fp = open(fname, O_RDONLY); if (fp < 0) { ptrace(PTRACE_SYSCALL, child_pid, 0, 0); continue; } read(fp, fname, sizeof(fname)); close(fp); if (strcmp(fname, appname) == 0) { // detach from zygote ptrace(PTRACE_DETACH, pid, 0, (void *)SIGCONT); // now perform on new process pid = child_pid; break; } else { ptrace(PTRACE_SYSCALL, child_pid, 0, 0); continue; } } if (WIFSTOPPED(stat) && (WSTOPSIG(stat) == SIGTRAP)) { if ((stat >> 16) & PTRACE_EVENT_FORK) { if (debug > 1) printf("fork\n"); int b = t; // save parent pid ptrace(PTRACE_GETEVENTMSG, t, 0, &child_pid); t = child_pid; ptrace(PTRACE_CONT, b, (void*)1, 0); ptrace(PTRACE_SYSCALL, child_pid, 0, 0); } } } } if (zygote) { int i = 0; for (i = 0; i < zygote; i++) { // -- zygote fix --- // we have to wait until the syscall is completed, IMPORTANT! ptrace(PTRACE_SYSCALL, pid, 0, 0); if (debug > 1) printf("/"); waitpid(pid, NULL, 0); ptrace(PTRACE_GETREGS, pid, 0, ®s); if (regs.ARM_ip != 0) { if (debug > 1) printf("not a syscall entry, wait for entry\n"); ptrace(PTRACE_SYSCALL, pid, 0, 0); waitpid(pid, NULL, 0); } ptrace(PTRACE_SYSCALL, pid, 0, 0); if (debug > 1) printf("\\"); waitpid(pid, NULL, 0); } }
參考
http://blog.csdn.net/roland_sun/article/details/34109569
android 5 HOOK 技術研究之 ADBI 項目 02