Android平台下hook框架adbi的研究(上)

对于Android系统来说,底层本质上来说还是一个Linux系统,所以过往在Linux上常用的技巧,在Android平台上都可以实现。

比如,可以利用ptrace()函数来attach到一个进程上,从而可以修改对应该进程的内存内容和寄存器的值。

但是,有了这些功能,要想真正做到随意hook一个进程中的指定函数,还是有很多工作要做。而adbi(The Android Dynamic Binary Instrumentation Toolkit)就是一个通用的框架,使得hook变得异常简单。可以从这里获得其源代码:https://github.com/crmulliner/adbi。

它的基本实现原理是利用ptrace()函数attach到一个进程上,然后在其调用序列中插入一个调用dlopen()函数的步骤,将一个实现预备好的.so文件加载到要hook的进程中,最终由这个加载的.so文件在初始化函数中hook指定的函数。

整个adbi工具集由两个主要模块组成,分别是用于.so文件注入的进程劫持工具(hijack tool)和一个修改函数入口的基础库。接下来,我们还是通过阅读代码来分别了解这两个模块的实现原理,本篇我们先将重点放在劫持工具的实现上。

前面的介绍中已经提到过了,这个模块的作用是将一个调动dlopen()函数的步骤插入到指定进程的调用序列中。

要做到这点,需要解决几个问题,一是如何获得目标进程中dlopen()函数的调用地址,二是如何插入一个调用dlopen()函数的步骤。

好,我们先来看看第一个问题是如何解决的。在adbi中,具体是通过以下几行代码来找到目标进程中dlopen()函数的起始地址的:

void *ldl = dlopen("libdl.so", RTLD_LAZY);if (ldl) {dlopenaddr = 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);首先用dlopen()在当前进程中加载libdl.so动态库,接着用dlsym()函数获得dlopen()函数的调用地址。感觉有点先有鸡还是先有蛋的问题了,这里稍微说明一下,libdl.so库肯定早就已经加载到进程中了,这里再加载一次其实并不会真的把这个动态库再在内存中的另一个位置加载一次,而是返回已经加载过的地址。

但是,获得了dlopen()函数在当前进程中的地址后并没有用,因为libdl.so是动态库,每次会被动态的加载到任意的一个地址上去,并不会固定。所以,当前进程中dlopen()函数的地址,一般情况下并不等于别的进程中dlopen()函数的地址。那要如何才能获得指定进程中的dlopen()函数的地址呢?代码中调用到了find_linker()函数,我们接下来看看它的实现:

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()函数,这个函数主要由三个步骤组成:

static intload_memmap(pid_t pid, struct mm *mm, int *nmmp){char raw[80000]; // this depends on the number of libraries an executable useschar 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;}……

首先,会打开一个内存文件,获得其句柄。该文件路径是“/proc/<进程号>/maps”,作用就是读出指定进程的内存映射信息,其格式大概如下:

40096000-40098000 r-xp 00000000 b3:16 109/system/bin/app_process40098000-40099000 r–p 00001000 b3:16 109/system/bin/app_process40099000-4009a000 rw-p 00000000 00:00 0 4009a000-400a9000 r-xp 00000000 b3:16 176/system/bin/linker400a9000-400aa000 r–p 0000e000 b3:16 176/system/bin/linker400aa000-400ab000 rw-p 0000f000 b3:16 176/system/bin/linker400ab000-400ae000 rw-p 00000000 00:00 0 400ae000-400b0000 r–p 00000000 00:00 0 400b0000-400b9000 r-xp 00000000 b3:16 855/system/lib/libcutils.so400b9000-400ba000 r–p 00008000 b3:16 855/system/lib/libcutils.so400ba000-400bb000 rw-p 00009000 b3:16 855/system/lib/libcutils.so400bb000-400be000 r-xp 00000000 b3:16 955/system/lib/liblog.so400be000-400bf000 r–p 00002000 b3:16 955/system/lib/liblog.so400bf000-400c0000 rw-p 00003000 b3:16 955/system/lib/liblog.so400c0000-40107000 r-xp 00000000 b3:16 832/system/lib/libc.so40107000-40108000 —p 00000000 00:00 0 好了,打开句柄之后,那接下来就是一行一行读取内存映射信息文件的内容: ……/* 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) {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;}先是按照格式解析出每行的起始地址,结束地址,和名称。如果没有解析出名称,就会用一个自定义的名称补上(“[memory]”)#define MEMORY_ONLY "[memory]"

如果名字是“stack”,表明这段内存用于栈。从前面的格式中可以看出,会有几行都叫一个名字的情况,接下来的代码会将这些连续的并且名字相同的内存段合并一下。

好了,现在就已经得到了指定进程的内存映射情况,包括起始地址、结束地址和名称。

轻轻的风,吹开你紧锁的眉头,

Android平台下hook框架adbi的研究(上)

相关文章:

你感兴趣的文章:

标签云: