Android系统采用的是Linux内核,很多Linux系统上的技术都可以应用在Android系统上,Android系统上ptrace注入远程进程的技术就是其中一种。本章节将对ptrace注入的完整流程进行介绍。
一、ptrace函数介绍
ptrace注入技术的核心就是ptrace函数,在ptrace注入过程中,将多次调用ptrace函数。Linux的man文档(超链接至: http://man7.org/linux/man-pages/man2/ptrace.2.html)中提到,ptrace函数为一个进程提供了监视和控制其他进程的方法,在注入进程后,父进程还可以读取和修改子进程的内存空间以及寄存器值。
ptrace函数的原型如下所示,其中request参数为一个联合体,该参数决定了ptrace函数的行为,pid参数为远程进程的ID,addr参数与data参数在不同的request参数取值下表示不同的含义。
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
request参数取值较多,由于篇幅所限这里仅介绍一部分ptrace注入进程的过程中需要使用到的request参数。
PTRACE_ATTACH,表示附加到指定远程进程;
PTRACE_DETACH,表示从指定远程进程分离
PTRACE_GETREGS,表示读取远程进程当前寄存器环境
PTRACE_SETREGS,表示设置远程进程的寄存器环境
PTRACE_CONT,表示使远程进程继续运行
PTRACE_PEEKTEXT,从远程进程指定内存地址读取一个word大小的数据
PTRACE_POKETEXT,往远程进程指定内存地址写入一个word大小的数据
二、ptrace注入进程流程
2.1 ptrace注入综述
Ptrace注入的目的是为了将外部的模块注入到游戏进程中,然后执行被注入模块的代码,可以对游戏进程的代码和数据进行修改。目前有两种实现ptrace注入模块到远程进程的方法,其中一种是使用ptrace将shellcode注入到远程进程的内存空间中,然后通过执行shellcode加载远程进程模块;另外一种方法是直接远程调用dlopen\dlsym等函数加载被注入模块并执行指定的代码。两种方法的实现方式稍有差异,但总体来说都是通过ptrace函数在远程进程空间执行代码,本章仅就第二种方式进行详细介绍,附件中包含两种注入方法实现的代码,有兴趣的读者可以了解。
如下图1-1所示,为ptrace注入远程进程的整体流程,整个注入过程并不复杂,但其中有一些细节需要注意,稍有不慎就会造成远程进程崩溃。下面小节将详细介绍流程中的每个步骤。
2.2 Attach到远程进程
ptrace注入的第一个步骤是先附加到远程进程上,如下所示,附加到远程进程是通过调用request参数为PTRACE_ATTACH的ptrace函数,pid为对应需要附加的远程进程的ID,addr参数和data参数为NULL。
ptrace(PTRACE_ATTACH, pid, NULL, NULL);
在附加到远程进程后,远程进程的执行会被中断,此时父进程可以通过调用waitpid函数来判断子进程是否进入暂停状态。waitpid的函数原型如下所示,其中当options参数为WUNTRACED ,表示若对应pid的远程进程进入暂停状态,则马上返回,可用于等待远程进程进入暂停状态。
pid_t waitpid(pid_t pid,int * status,int options);
2.3 读取和写入寄存器值
在通过ptrace改变远程进程执行流程前,需要先读取远程进程的所有寄存器值进行保存,在detach时向远程进程写入保存的原寄存器值用于恢复远程进程原有的执行流程。
如下所示,为读取和写入寄存器值的ptrace调用,request参数分别为PTRACE_GETREGS和PTRACE_SETREGS,pid为对应进程的ID。
ptrace(PTRACE_GETREGS, pid, NULL, regs);
ptrace(PTRACE_SETREGS, pid, NULL, regs);
在ARM处理器下,data参数的regs为pt_regs结构的指针,从远程进程获取的寄存器值将存储到该结构中,pt_regs结构的定义如下所示,其中ARM_r0成员用于存储R0寄存器的值,函数调用后的返回值会存储在R0寄存器中,ARM_pc成员存储当前执行地址,ARM_sp成员存储当前栈顶地址,ARM_lr成员存储返回地址,ARM_cpsr成员存储状态寄存器的值。
struct pt_regs {
long uregs[18];
};
#define ARM_cpsr uregs[16]
#define ARM_pc uregs[15]
#define ARM_lr uregs[14]
#define ARM_sp uregs[13]
#define ARM_ip uregs[12]
#define ARM_fp uregs[11]
#define ARM_r10 uregs[10]
#define ARM_r9 uregs[9]
#define ARM_r8 uregs[8]
#define ARM_r7 uregs[7]
#define ARM_r6 uregs[6]
#define ARM_r5 uregs[5]
#define ARM_r4 uregs[4]
#define ARM_r3 uregs[3]
#define ARM_r2 uregs[2]
#define ARM_r1 uregs[1]
#define ARM_r0 uregs[0]
#define ARM_ORIG_r0 uregs[17]
2.4 远程进程内存读取和写入数据
调用request参数为PTRACE_PEEKTEXT的ptrace函数可以从远程进程的内存空间中读取数据,一次读取一个word大小的数据。如下所示,其中addr参数为需读取数据的远程进程内存地址,返回值为读取出的数据。
ptrace(PTRACE_PEEKTEXT, pid, pCurSrcBuf, 0);
ptrace(PTRACE_POKETEXT, pid, pCurDestBuf, lTmpBuf) ;
调用request参数为PTRACE_POKETEXT的ptrace函数可以将数据写入到远程进程的内存空间中,同样一次写入一个word大小的数据,ptrace函数的addr参数为要写入数据的远程进程内存地址,data参数为要写入的数据。
写入数据时需要注意,若写入数据长度不是一个word大小的倍数,写入最后一个不足word大小的数据时,要先保存原地址处的高位数据。
如下代码所示,首先通过request参数为PTRACE_PEEKTEXT的ptrace函数读取原内存中的一个word大小的数据,然后将要写入的数据复制到读取出的数据的低位,然后调用ptrace函数将修改后的数据写入远程进程的内存地址处。
lTmpBuf = ptrace(PTRACE_PEEKTEXT, pid, pCurDestBuf, NULL);
memcpy((void *)(&lTmpBuf), pCurSrcBuf, nRemainCount);
if (ptrace(PTRACE_POKETEXT, pid, pCurDestBuf, lTmpBuf) < 0)
{
LOGD("Write Remote Memory error, MemoryAddr:0x%lx", (long)pCurDestBuf);
return -1;
}
2.5 远程调用函数
在ARM处理器中,函数调用的前四个参数通过R0-R3寄存器来传递,剩余参数按从右到左的顺序压入栈中进行传递。如下代码所示,在远程调用函数前,需要先判断函数调用的参数个数,如果小于4个,则将参数按顺序分别写入R0-R3寄存器中,若大于4个,则首先调整SP寄存器在栈中分配空间,然后通过调用ptrace函数将剩余参数写入到栈中。
for (i = 0; i < num_params && i < 4; i ++) {
regs->uregs[i] = parameters[i];
}
if (i < num_params) {
regs->ARM_sp -= (num_params - i) * sizeof(long) ;
if (ptrace_writedata(pid, (void *)regs->ARM_sp, (uint8_t *)¶meters[i], (num_params - i) * sizeof(long)) == -1)
return -1;
}
在写入函数的参数后,修改进程的PC寄存器为需要执行的函数地址。这里有一点需要注意,在ARM架构下有ARM和Thumb两种指令,因此在调用函数前需要判断函数被解析成哪种指令,如下所示的代码就是通过地址的最低位是否为1来判断调用地址处指令为ARM或Thumb,若为Thumb指令,则需要将最低位重新设置为0,并且将CPSR寄存器的T标志位置位,若为ARM指令,则将CPSR寄存器的T标志位复位。
if (regs->ARM_pc & 1) { /* thumb */
regs->ARM_pc &= (~1u);
regs->ARM_cpsr |= CPSR_T_MASK;
} else { /* arm */
regs->ARM_cpsr &= ~CPSR_T_MASK;
}
在使远程进程恢复运行前,还需要设置远程进程的LR寄存器值为0,并且在在本地进程调用options参数为WUNTRACED的waitpid函数等待远程进程重新进入暂停状态。远程进程的函数调用结束后,会跳转到LR寄存器存储的地址处,但由于LR寄存器被设置为0,会导致远程进程执行出错,此时进程会进入暂停状态,本地进程等待结束,通过读取远程进程的R0寄存器可以获取远程函数调用的返回结果,以上就是一次完整地调用远程函数的过程。
在ptrace注入流程中需要多次调用函数,除了调用被注入模块的函数外,还需要调用mmap函数在远程进程地址空间内分配内存,调用dlopen函数来远程加载被注入模块,调用dlsym函数来获取被注入模块对应函数的地址,调用dlclose函数来关闭加载的模块。这些函数的原型如下所示,
void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
void * dlopen( const char * pathname, int mode);
voiddlsym(voidhandle,constchar*symbol);
int dlclose (void *handle);
在调用这些函数前,需要首先获取到这些系统函数在远程进程中的地址,mmap函数是在”/system/lib/libc.so”模块中,dlopen、dlsym与dlclose函数均是在”/system/bin/linker”模块中。
读取”/proc/pid/maps”可以获取到系统模块在本地进程和远程进程的加载基地址,要获取远程进程内存空间中mmap等函数的虚拟地址,可通过计算本地进程中mmap等函数相对于模块的地址偏移,然后使用此地址偏移加上远程进程对应模块的基地址,这个地址就是远程进程内存空间中对应函数的虚拟地址。
2.6 恢复寄存器值
在从远程进程detach前,需要将远程进程的原寄存器环境恢复,保证远程进程原有的执行流程不被破坏,如果不恢复寄存器值,detach时会导致远程进程的崩溃。
2.7 Detach进程
从远程进程脱离是ptrace注入的最后一个步骤,在detach后被注入进程将继续运行。如下所示,从远程进程detach是调用request参数为PTRACE_DETACH的ptrace函数。
ptrace(PTRACE_DETACH, pid, NULL, 0);