android 5 HOOK 技术研究之 ADBI 项目
简介
adbi 是一个android平台的二进制注入框架,源码开放在github上 : ADBI 项目 ,从hook技术的分类来说,其属于so注入+inline hook, 这种方式的套路是:基于linux系统的ptrace机制,attach一个目标进程,注入一个动态链接库进入目标进程的地址空间,然后用so里边的函数地址替换目标进程地址空间里原有的函数地址(老的函数地址一般也需要保存起来)。
源码目录
hijack: 可执行程序,用于注入一个so到目标进程
libbase: 注入库,提供hook和unhook函数,一般编译为一个静态库
example: 一个hook例子(hook了系统函数 epoll_wait),使用libbase,编译成一个动态库,由 hijack 注入目标进程
编译运行
默认从github clone下面的编译文件 Android.mk 里目标体系架构是 arm ,所以要运行测试例子需要准备一个 Arm 模拟器,另外, README.md 给出的编译方式是 linux 系统的shell脚本,在Windows下不能直接使用,需要改成相应的批处理脚本。这里我直接手动进入相关目录执行ndk-build.
1. 下载ndk, 资源站 下载 ndk
2. 解压到某一目录,将ndk的路径设置进 PATH 环境变量
3. git clone adbi 项目
4. 进入hijack/jni, instruments/base/jni, instruments/base/example/jni 分别执行 ndk-build
5. 启动模拟器
我使用android studio avd 管理器创建了一个模拟器,参数如下
使用android studio 创建一个android项目(默认即可),启动模拟器
6. adb push 刚刚编译好的文件到模拟器,adb shell 进入模拟器并执行注入
源码分析
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 int load_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 ; 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, #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 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 };
下面使用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 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; }
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);
}
}
参考