一、ptrace函数介绍
ptrace函数的原型如下所示,其中request为行为参数,该参数决定了ptrace函数的行为,pid参数为远程进程的ID,addr参数与data参数在不同的request参数取值下表示不同的含义。
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
部分ptrace注入进程的过程中需要使用到的request参数
PTRACE_ATTACH,表示附加到指定远程进程;
PTRACE_DETACH,表示从指定远程进程分离
PTRACE_GETREGS,表示读取远程进程当前寄存器环境
PTRACE_SETREGS,表示设置远程进程的寄存器环境
PTRACE_CONT,表示使远程进程继续运行
PTRACE_PEEKTEXT,从远程进程指定内存地址读取一个word大小的数据
PTRACE_POKETEXT,往远程进程指定内存地址写入一个word大小的数据
二、ptrace注入进程流程
目前有两种实现ptrace注入模块到远程进程的方法
1、直接远程调用dlopen\dlsym等函数加载被注入模块并执行指定的代码
(1)Attach到远程进程
int ptrace_attach(pid_t pid)
{
if (ptrace(PTRACE_ATTACH, pid, NULL, 0) < 0) {
perror("ptrace_attach");
return -1;
}
int status = 0;
waitpid(pid, &status , WUNTRACED);
return 0;
}
附加到远程进程是通过调用request参数为PTRACE_ATTACH的ptrace函数,在附加到远程进程后,远程进程的执行会被中断,此时父进程可以通过调用waitpid函数来判断子进程是否进入暂停状态。
(2)读取和写入寄存器值
在通过ptrace改变远程进程执行流程前,需要先读取远程进程的所有寄存器值进行保存,在detach时向远程进程写入保存的原寄存器值用于恢复远程进程原有的执行流程。
int ptrace_getregs(pid_t pid, struct pt_regs * regs)
{
if (ptrace(PTRACE_GETREGS, pid, NULL, regs) < 0) {
perror("ptrace_getregs: Can not get register values");
return -1;
}
return 0;
}
int ptrace_setregs(pid_t pid, struct pt_regs * regs)
{
if (ptrace(PTRACE_SETREGS, pid, NULL, regs) < 0) {
perror("ptrace_setregs: Can not set register values");
return -1;
}
return 0;
}
读取和写入寄存器值的ptrace调用,request参数分别为PTRACE_GETREGS和PTRACE_SETREGS,pid为对应进程的ID。
(3)远程进程内存读取和写入数据
int ptrace_readdata(pid_t pid, uint8_t *src, uint8_t *buf, size_t size)
{
uint32_t i, j, remain;
uint8_t *laddr;
//联合体
union u{
long val;
char chars[sizeof(long)];
} d;
//4字节的整数倍
j = size / 4;
//剩余的字节数
remain = size % 4;
//src为要读取数据的目标进程的内存地址
//buf保存读取到目标进程中的数据
laddr = buf;
//在目标进程中读取4字节的整数倍的数据
for (i = 0; i < j; i++)
{
//在目标进程中读取数据
d.val = ptrace(PTRACE_PEEKTEXT, pid, src, 0);
//拷贝读取的数据到临时缓冲区中
memcpy(laddr, d.chars, 4);
src += 4;
laddr += 4;
}
//在目标进程中读取剩余的数据
if (remain > 0)
{
//在目标进程中读取数据
d.val = ptrace(PTRACE_PEEKTEXT, pid, src, 0);
//拷贝读取的数据到临时缓冲区中
memcpy(laddr, d.chars, remain);
}
return 0;
}
调用request参数为PTRACE_PEEKTEXT的ptrace函数可以从远程进程的内存空间中读取数据,一次读取一个word大小的数据。
int ptrace_writedata(pid_t pid, uint8_t *dest, uint8_t *data, size_t size)
{
uint32_t i, j, remain;
uint8_t *laddr;
//联合体
union u {
long val;
char chars[sizeof(long)];
} d;
//4字节整数倍
j = size / 4;
//剩余的字节数
remain = size % 4;
//data中存放的是要写入目标进程的数据
laddr = data;
//向目标进程中写入4字节的整数倍的数据
for (i = 0; i < j; i ++)
{
memcpy(d.chars, laddr, 4);
//向目标中写入1个字的数据
ptrace(PTRACE_POKETEXT, pid, dest, d.val);
dest += 4;
laddr += 4;
}
//向目标进程中写入剩余的数据
if (remain > 0)
{
//d.val = ptrace(PTRACE_PEEKTEXT, pid, dest, 0); //原来的代码中有,感觉是多余的
for ( i = 0; i < remain; i++)
{
d.chars[i] = *laddr++;
}
//向目标进程中写入剩余的数据
ptrace(PTRACE_POKETEXT, pid, dest, d.val);
}
return 0;
}
调用request参数为PTRACE_POKETEXT的ptrace函数可以将数据写入到远程进程的内存空间中,同样一次写入一个word大小的数据
(4)远程调用函数
在ARM处理器中,函数调用的前四个参数通过R0-R3寄存器来传递,剩余参数按从右到左的顺序压入栈中进行传递。
// 参数说明:
// pid_t pid 远程进程pid
// uint32_t addr 函数地址
// long *params 函数参数
// uint32_t num_params 参数个数
// struct pt_regs* regs 寄存器
#if defined(__arm__)
int ptrace_call(pid_t pid, uint32_t addr, long *params, uint32_t num_params, struct pt_regs* regs)
{
uint32_t i;
for (i = 0; i < num_params && i < 4; i ++) {
regs->uregs[i] = params[i];
}
if (i < num_params) {
regs->ARM_sp -= (num_params - i) * sizeof(long) ;
ptrace_writedata(pid, (void *)regs->ARM_sp, (uint8_t *)¶ms[i], (num_params - i) * sizeof(long));
}
//设置远程目标进程的的PC寄存器的值
regs->ARM_pc = addr;
if (regs->ARM_pc & 1) {
/* thumb */
regs->ARM_pc &= (~1u);
regs->ARM_cpsr |= CPSR_T_MASK;
} else {
/* arm */
regs->ARM_cpsr &= ~CPSR_T_MASK;
}
regs->ARM_lr = 0;
if (ptrace_setregs(pid, regs) == -1
|| ptrace_continue(pid) == -1) {
printf("error\n");
return -1;
}
int stat = 0;
waitpid(pid, &stat, WUNTRACED);
while (stat != 0xb7f) {
if (ptrace_continue(pid) == -1) {
printf("error\n");
return -1;
}
waitpid(pid, &stat, WUNTRACED);
}
return 0;
}
#elif defined(__i386__)
long ptrace_call(pid_t pid, uint32_t addr, long *params, uint32_t num_params, struct user_regs_struct * regs)
{
regs->esp -= (num_params) * sizeof(long) ;
ptrace_writedata(pid, (void *)regs->esp, (uint8_t *)params, (num_params) * sizeof(long));
long tmp_addr = 0x00;
regs->esp -= sizeof(long);
ptrace_writedata(pid, regs->esp, (char *)&tmp_addr, sizeof(tmp_addr));
regs->eip = addr;
if (ptrace_setregs(pid, regs) == -1
|| ptrace_continue( pid) == -1) {
printf("error\n");
return -1;
}
int stat = 0;
waitpid(pid, &stat, WUNTRACED);
while (stat != 0xb7f) {
if (ptrace_continue(pid) == -1) {
printf("error\n");
return -1;
}
waitpid(pid, &stat, WUNTRACED);
}
return 0;
}
#else
#error "Not supported"
#endif
在远程调用函数前,需要先判断函数调用的参数个数,如果小于4个,则将参数按顺序分别写入R0-R3寄存器中,若大于4个,则首先通过调整SP寄存器来在栈中进行空间分配,然后通过调用ptrace函数将剩余参数写入到栈中。
在写入函数的参数后,修改进程的PC寄存器为需要执行的函数地址。这里有一点需要注意,在ARM架构下有ARM和Thumb两种指令,因此在调用函数前需要判断函数被解析成哪种指令,如下所示的代码就是通过地址的最低位是否为1来判断调用地址处指令为ARM或Thumb,若为Thumb指令,则需要将最低位重新设置为0,并且将CPSR寄存器的T标志位置位,若为ARM指令,则将CPSR寄存器的T标志位复位。
调用request参数为PTRACE_CONT的ptrace函数是进程继续运行,这样进执行到了我们指定的函数。
int ptrace_continue(pid_t pid)
{
if (ptrace(PTRACE_CONT, pid, NULL, 0) < 0) {
perror("ptrace_cont");
return -1;
}
return 0;
}
另外,对于x86的函数调用会有所区别,这里做了区分调用。
在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);
void*dlsym(void*handle,constchar*symbol);
int dlclose (void *handle);
从ptrace_call这个函数的参数可以看到,如果想要调用这些函数,需要首先获取到这些系统函数在远程进程中的地址。那么如何获取到远程进程空间中某个函数的地址呢?
mmap函数是在”/system/lib/libc.so”模块中,dlopen、dlsym与dlclose函数均是在”/system/bin/linker”模块中。
// 函数说明:
// pid_t pid 进程pid
// const char* module_name 函数所在模块的名称,例如:/system/lib/libc.so
void* get_module_base(pid_t pid, const char* module_name)
{
FILE *fp;
long addr = 0;
char *pch;
char filename[32];
char line[1024];
if (pid < 0) {
/* self process */
snprintf(filename, sizeof(filename), "/proc/self/maps", pid);
} else {
snprintf(filename, sizeof(filename), "/proc/%d/maps", pid);
}
fp = fopen(filename, "r");
if (fp != NULL) {
while (fgets(line, sizeof(line), fp)) {
if (strstr(line, module_name)) {
pch = strtok( line, "-" );
addr = strtoul( pch, NULL, 16 );
if (addr == 0x8000)
addr = 0;
break;
}
}
fclose(fp) ;
}
return (void *)addr;
}
读取”/proc/pid/maps”可以获取到系统模块在本地进程和远程进程的加载基地址
// 参数说明:
// pid_t target_pid 目标进程的pid
// const char* module_name
// void* local_addr
void* get_remote_addr(pid_t target_pid, const char* module_name, void* local_addr)
{
void* local_handle, *remote_handle;
local_handle = get_module_base(-1, module_name);
remote_handle = get_module_base(target_pid, module_name);
DEBUG_PRINT("[+] get_remote_addr: local[%x], remote[%x]\n", local_handle, remote_handle);
void * ret_addr = (void *)((uint32_t)local_addr + (uint32_t)remote_handle - (uint32_t)local_handle);
#if defined(__i386__)
if (!strcmp(module_name, libc_path)) {
ret_addr += 2;
}
#endif
return ret_addr;
}
要获取远程进程内存空间中mmap等函数的虚拟地址,可通过计算本地进程中mmap等函数相对于模块的地址偏移,然后使用此地址偏移加上远程进程对应模块的基地址,这个地址就是远程进程内存空间中对应函数的虚拟地址。
(5)Detach进程
int ptrace_detach(pid_t pid)
{
if (ptrace(PTRACE_DETACH, pid, NULL, 0) < 0) {
perror("ptrace_detach");
return -1;
}
return 0;
}
假设我们需要注入到远程的/system/bin/surfaceflinger进程,并且在该进程中注入/data/libhello.so模块,执行该模块的hook_entry方法。
注入的基本步骤为:
// 1、获取待注入进程的pid,即获取/system/bin/surfaceflinger进程的pid
pid_t target_pid;
target_pid = find_pid_of("/system/bin/surfaceflinger");
// 2、attach到远程进程
if (ptrace_attach(target_pid) == -1)
goto exit;
// 3、保存寄存器环境
struct pt_regs regs, original_regs;
if (ptrace_getregs(target_pid, ®s) == -1)
goto exit;
/* save original registers */
memcpy(&original_regs, ®s, sizeof(regs));
// 4、远程调用mmap来远程分配内存空间
// 4.1 获取到远程mmap函数的地址
const char *libc_path = "/system/lib/libc.so";
mmap_addr = get_remote_addr(target_pid, libc_path, (void *)mmap);
// 4.2 构造mmap参数,并远程调用mmap函数
parameters[0] = 0; // addr
parameters[1] = 0x4000; // size
parameters[2] = PROT_READ | PROT_WRITE | PROT_EXEC; // prot
parameters[3] = MAP_ANONYMOUS | MAP_PRIVATE; // flags
parameters[4] = 0; //fd
parameters[5] = 0; //offset
if (ptrace_call(target_pid, (uint32_t)mmap_addr, parameters, 6, regs) == -1)
return -1;
if (ptrace_getregs(target_pid, regs) == -1)
return -1;
// 4.3、得到分配的内存空间的初始地址,并将注入模块写入该内存
lchar *library_path = "/data/libhello.so";
map_base = ptrace_retval(®s);
ptrace_writedata(target_pid, map_base, library_path, strlen(library_path) + 1);
// 5、远程打开/data/libhello.so模块
// 5.1 获取到远程dlopen函数的地址
const char *linker_path = "/system/bin/linker";
dlopen_addr = get_remote_addr( target_pid, linker_path, (void *)dlopen );
// 5.2、构造dlopen参数,并远程调用dlopen函数
parameters[0] = map_base;
parameters[1] = RTLD_NOW| RTLD_GLOBAL;
if (ptrace_call(target_pid, (uint32_t)dlopen_addr, parameters, 2, regs) == -1)
return -1;
void * sohandle = ptrace_retval(®s);
// 6、远程调用dlsym函数来实现对注入的libhello.so模块的hook_entry方法的调用
const char * func_name = "hook_entry";
#define FUNCTION_NAME_ADDR_OFFSET 0x100
ptrace_writedata(target_pid, map_base + FUNCTION_NAME_ADDR_OFFSET, func_name, strlen(func_name) + 1);
parameters[0] = sohandle;
parameters[1] = map_base + FUNCTION_NAME_ADDR_OFFSET;
dlsym_addr = get_remote_addr( target_pid, linker_path, (void *)dlsym );
if (ptrace_call(target_pid, (uint32_t)dlsym_addr, parameters, param_num, regs) == -1)
return -1;
void * hook_entry_addr = ptrace_retval(®s);
// 7、关闭远程调用,恢复寄存器环境并detach进程
parameters[0] = sohandle;
dlclose_addr = get_remote_addr( target_pid, linker_path, (void *)dlclose );
if (ptrace_call(target_pid, (uint32_t)dlclose_addr, parameters, 1, regs) == -1)
return -1;
/* restore */
ptrace_setregs(target_pid, &original_regs);
ptrace_detach(target_pid);
完整代码参考:
Android中的so注入(inject)和挂钩(hook) - For both x86 and arm
2、使用ptrace将shellcode注入到远程进程的内存空间中,然后通过执行shellcode加载远程进程模块
(1)在目标进程中分配内存,用来写shellcode和参数
(2)往目标进程中写入shellcode, shellcode会调用dlopen来载入我们的library
(3)运行目标进程中的shellcode
具体代码参考: 发个Android平台上的注入代码
参考文献:
https://github.com/crmulliner/adbi
https://github.com/matrixhawk/Poison
ptrace注入游戏介绍
Android的so库注入
Android的so注入( inject)和函数Hook(基于got表) - 支持arm和x86
Android进程的so注入–Poison(稳定注入版)
Android注入完全剖析
android hook 框架 libinject2 如何实现so注入
进击的Android注入术《二》
android hook 框架 ADBI 如何实现so注入