【QTA】Android动态注入原理分析

2019-04-28  阿呆 

一、前言

Android的UI自动化测试可以通过注入式和非注入式分别实现,通过注入式可以更加方便地与应用进行交互。 QTA团队提供的Android UI自动化测试框架QT4A , 是通过动态注入的方式来获取被测应用的控件树信息等,从而达到自动化测试的目的。 本文主要介绍该动态注入的原理。


二、Android动态注入概述

QT4A中的动态注入是借助ptrace函数,该函数常用于断点调试或系统调用跟踪,由于其动态附着到远程进程的特性,我们可以在Android UI自动化测试中加以利用。 QT4A框架中将测试桩so动态库链接到被测应用进程空间,使得so中的函数在被测进程有对应地址,通过该地址即可在被测进程中调用QT4A的函数,与被测应用进行交互。


三、Android动态注入条件限制

需要注意的是,通过ptrace函数虽然可以跟踪进程,修改被跟踪进程的内存和寄存器值,但正因其强大的能力,它也需在以下任一条件下满足才能成功执行:

  • 设备已越狱(root)

  • 设备未root情况下,只能注入具有相同uid的进程。在Android中,可以通过如下两种方法达到:

         1、重打包QT4A so到被测apk包中实现;

         2、部分支持run-as命令的Android设备,也可以通过该命令切换到被测应用uid下再进行注入,该命令可用情况下则无需重打包。

QT4A结合了这两种方案实现非root下的动态注入。


四、Android动态注入整体流程

为了方便看结果,我们以注入一个简单的so(hello.so)为例,而不以QT4A真正的so为例。hello.so主要包括了一个入口函数,主要代码如下:

int hook_entry(char * a){
    LOGD("Hook success, pid = %d\n", getpid());
    LOGD("Hello %s\n", a);
    return 0;
}
我们目标是将其注入到被测Android应用进程中,预期结果是在被测应用中输出上述日志内容。 整体注入流程图如下:


首先通过PTRACE_ATTACH附着到远程进程:

ptrace(PTRACE_ATTACH, pid, NULL, 0)

在开始加载我们的so之前,我们先把远程进程的现场进行保护:

ptrace_getregs( target_pid, ®s )
如上,获取远程进程(进程id为target_pid)的寄存器,然后将其保存到original_regs中:
memcpy( &original_regs, ®s, sizeof(regs) );
加载hello.so后可以恢复现场并解除进程跟踪:
ptrace_setregs( target_pid, &original_regs );
ptrace_detach( target_pid );
接下来重点介绍如何加载hello.so.


五、获取远程函数地址

由于hello.so不在远程进程中,在远程进程中并没有hello.so相关的地址,要在远程进程加载hello.so,首先需要分配内存空间写入so,我们可以在远程进程中调用mmap函数为hello.so分配内存空间,但只有知道了函数地址才能开始调用,如何获取远程进程中的mmap函数地址呢?本节以获取mmap远程函数地址为例说明如何获取远程函数地址


5.1 mmap远程函数地址获取公式

同一系统库(例如mmap所在的系统库libc.so)的mmap地址与libc.so基地址的偏移量,在当前进程和远程进程(Android应用)中是相同的,所以,只要获取到当前进程的libc基地址(假设用变量local_handle表示)、当前进程mmap地址(local_addr)、远程进程libc.so基地址(remote_handle),即可根据如下公式获取远程mmap地址(remote_addr):


如上图,可获得公式:

local_addr - local_handle + = remote_addr - remote_handle式子可转化为remote_addr = local_addr + remote_handle - local_handle

接下来首先获取libc.so基地址(local_handle/remote_handle)和当前进程mmap函数地址(local_addr)。


5.2 获取libc.so基地址

获取进程中libc.so模块基地址(local_handle/remote_handle)的方法为:



即在/proc/{pid}/maps路径中找到模块名,其中pid替换为目标进程的进程id,对应的行首地址即为模块的基地址。如果在当前进程中读取当前进程的模块基地址,可读取/proc/self/maps路径下的模块地址即可。通过该方法可求得local_handle/remote_handle的值。


5.3 获取当前进程mmap函数地址

获取当前进程的mmap函数地址,有两种方法:

方法一:通过dlopen/dlsym的方式获取,如下图:


方法二:根据elf文件内容格式获取符号相对基地址的偏移量,加上当前进程中libc.so基地址,即可求得当前进程函数地址。实现在get_symbol_offset函数中,后续可详细见开源后的源码。

两种方法可以结合调用,更为可靠,整体调用代码如下:

/*
* 获取当前进程中的函数地址
* 调用:void* local_mmap_addr = get_func_addr(libc_path, "mmap");
*/
void* get_func_addr(const char* module_path, const char* func_name) {
    void* handle = dlopen(module_path, RTLD_NOW);
    if(handle != NULL){
        void* addr = dlsym(handle, func_name); 
        if(addr != NULL) return addr;
    }
    uint32_t addr = get_symbol_offset(module_path, func_name);
    if(addr == 0) return NULL;
    return get_module_base(-1, module_path) + addr;
}

其中get_symbol_offset读取到了函数偏移值,get_module_base获取了libc.so基地址(详细见《获取libc.so基地址》一节),两者相加即为当前进程mmap函数地址(local_addr)。

至目前为止,根据公式remote_addr = local_addr + remote_handle - local_handle,我们知道了local_handle/remote_handle/local_addr三个变量的值,从而可求得远程mmap地址(remote_addr)。

类似的,其他远程函数地址的获取方法类似上述过程,区别在于函数所在的库不同、函数名不同而已,后续不再赘述。


六、 远程进程函数调用

6.1 调用远程函数mmap分配内存空间

通过上一节分析,可知远程进程函数的地址获取方法,然后开始调用远程进程函数mmap分配内存空间,需要借助ptrace函数进行调用:

void* ret = ptrace_call( pid, remote_mmap_addr, parameters, 6, regs );
如上,传入所需的参数,可调用mmap函数分配内存空间并返回分配的内存地址。 ptrace_call函数首先会将调用的函数(mmap)所需的参数(parameters)从右到左压入堆栈,同时写入返回地址到对应寄存器中,并同步修改栈顶指针。 请注意,不同的CPU架构所用的寄存器和数据压入方式有一定的差异,请按不同的CPU架构对应处理,这里总结了部分的差异:

将堆栈和寄存器值都设置完毕后,通过调用ptrace函数,并传入参数PTRACE_CONT使mmap函数得以执行。


6.2 往mmap分配的内存空间写入hello.so路径和参数

int ptrace_writedata( pid_t pid, uint8_t *dest, uint8_t *data, size_t size )
该函数实现往地址中写入字符串的功能,其中了利用ptrace函数提供的写内存空间的方法,通过传入参数PTRACE_POKETEXT及其他所需参数进行写入,我们首先将hello.so路径写入mmap分配的内存空间中(remote_memory)。 同理,hello.so中的入口函数(hook_entry)   如果需要传入参数,也可通过这种方法写入remote_memory中。 更多细节请参考后续开源出来的源码。 而对应的,如果需要进行读操作,则传入参数PTRACE_PEEKTEXT及其他参数。


6.3 远程进程中调用hello.so的函数

目前为止,我们已经在远程进程中分配了内存,写入了hello.so和其函数hook_entry的参数。而我们又可以通过《获取远程函数地址》一节的方法,获取hello.so的函数地址,用变量remote_func_addr表示,接下来可以调用hook_entry函数:

ret = (long)ptrace_call( target_pid, remote_func_addr, parameters, param_size, ®s );

上述ptrace_call的函数的详细过程参考《6.1 调用远程函数mmap分配内存空间》一节。


调用结果如下图:

可以看到,hello.so中的 hook_entry 函数中的日志(Hook success……)在目标进程(2422)中打印出来了,证明我们的注入已成功。


七、总结

本文分析了QT4A所涉及的Android动态注入过程,QT4A利用该过程注入QT4A测试桩到被测Android应用进程中,达到与应用通信的目的。整个注入过程比较关键的是获取远程函数地址和调用远程函数。调用远程函数需要首先通过mmap分配内存写入待注入so(hello.so)和其函数所需参数,同时需要维护寄存器和堆栈状态,不同CPU架构有所差别。

感兴趣的同学可以加入QQ群交流



67°|674 人阅读|0 条评论
登录 后发表评论