之前介绍了Android平台上3种常见的hook方法,而hook的前提是进程注入,通过进程注入我们可以将模块或代码注入到目标进程中以便对其空间内的数据进行操作,本篇文章介绍基于ptrace函数的注入技术。
对ptrace函数不熟悉的朋友可以参考我之前写的linux ptrace I和linux ptrace II,跟hook相比,在熟悉了ptrace函数的使用方式后注入过程并不复杂,但在细节的处理上要多加留意,稍有不慎就会造成目标进程发生崩溃。
注入流程如下:
- 附加目标进程
- 保存寄存器环境
- 远程调用mmap函数分配空间
- 远程调用dlopen函数注入模块
- 远程调用注入模块中的函数
- 远程调用munmap函数释放空间
- 恢复寄存器环境
- 脱离目标进程
整个注入过程都是围绕ptrace函数进行,所以我们需要对ptrace函数进行封装以便实现特定的功能。
1、进程附加
#define CODE_CHECK(code) do { \
if ((code) != ) { \
return -; \
} \
} while() int ptrace_attach(pid_t pid)
{
int status = ;
CODE_CHECK(ptrace(PTRACE_ATTACH, pid, NULL, NULL));
while(waitpid(pid, &status, WUNTRACED) == -) {
if (errno == EINTR) {
continue;
} else {
return -;
}
}
return ;
}
2、脱离进程
int ptrace_detach(pid_t pid)
{
CODE_CHECK(ptrace(PTRACE_DETACH, pid, NULL, NULL));
return ;
}
3、恢复进程运行状态
int ptrace_continue(pid_t pid)
{
CODE_CHECK(ptrace(PTRACE_CONT, pid, NULL, NULL));
return ;
}
4、获取寄存器信息
int ptrace_getregs(pid_t pid, struct pt_regs *regs)
{
CODE_CHECK(ptrace(PTRACE_GETREGS, pid, NULL, regs));
return ;
}
5、设置寄存器信息
int ptrace_setregs(pid_t pid, const struct pt_regs *regs)
{
CODE_CHECK(ptrace(PTRACE_SETREGS, pid, NULL, regs));
return ;
}
6、向目标进程写入数据
int ptrace_writedata(pid_t pid, const void *addr, const void *data, int size)
{
int write_count = size / sizeof(long);
int remain_size = size % sizeof(long);
long write_buffer;
for (int i = ; i < write_count; ++i) {
memcpy(&write_buffer, data, sizeof(long));
CODE_CHECK(ptrace(PTRACE_POKETEXT, pid, addr, write_buffer));
data = ((long*)data) + ;
addr = ((long*)addr) + ;
}
if (remain_size > ) {
write_buffer = ptrace(PTRACE_PEEKTEXT, pid, addr, NULL);
memcpy(&write_buffer, data, remain_size);
CODE_CHECK(ptrace(PTRACE_POKETEXT, pid, addr, write_buffer));
}
return ;
}
7、调用目标进程函数
int ptrace_call(pid_t pid, const void* addr, const long *parameters, int num, struct pt_regs *regs)
{
int i;
//根据函数调用约定,前4个参数分别放入r0、r1、r3、r4寄存器,其余放入栈中。
//如果需要传入字符串等信息需要提前将数据写入目标进程。
for (i = ; i < num && i < ; ++i) {
regs->uregs[i] = parameters[i];
}
if (i < num) {
LOG_INFO("write %d parameters to stack", num - i);
regs->ARM_sp -= (num - i) * sizeof(long);
CODE_CHECK(ptrace_writedata(pid, (void*)regs->ARM_sp,
¶meters[i], (num - i) * sizeof(long)));
}
//设置pc寄存器
regs->ARM_pc = (long)addr;
//根据pc寄存器的第0bit位判断目标地址指令集
if (regs->ARM_pc & ) {
//for thumb
regs->ARM_pc &= (~1u);
regs->ARM_cpsr |= CPSR_T_MASK;
} else {
regs->ARM_cpsr &= ~CPSR_T_MASK;
}
//设置lr寄存器值为0,当函数返回时进程会接收到异常信号而停止运行。
regs->ARM_lr = ;
//设置寄存器信息
CODE_CHECK(ptrace_setregs(pid, regs));
//恢复目标进行运行
CODE_CHECK(ptrace_continue(pid));
LOG_INFO("wait for stopping...");
int stat = ;
while(waitpid(pid, &stat, WUNTRACED) == -) {
if (errno == EINTR) {
continue;
} else {
return -;
}
}
if(!WIFSTOPPED(stat)) {
LOG_INFO("status is invalid: %d", stat);
return -;
}
CODE_CHECK(ptrace_getregs(pid, regs));
//平衡因传递参数而使用的栈
regs->ARM_sp += (num - i) * sizeof(long);
return ;
}
8、获取目标进程函数地址
void* get_module_addr(pid_t pid, const char *module_name)
{
FILE *fp;
char file_path[MAX_PATH];
char file_line[MAX_LINE];
if (pid < ) {
snprintf(file_path, sizeof(file_path), "/proc/self/maps");
} else {
snprintf(file_path, sizeof(file_path), "/proc/%d/maps", pid);
}
fp = fopen(file_path, "r");
if (fp == NULL) {
return NULL;
}
unsigned long addr_start = , addr_end = ;
while (fgets(file_line, sizeof(file_line), fp)) {
if (strstr(file_line, module_name)) {
if ( == sscanf(file_line, "%8lx-%8lx", &addr_start, &addr_end)) {
break;
}
}
}
fclose(fp);
LOG_INFO("library :%s %lx-%lx, pid : %d", module_name, addr_start, addr_end, pid);
return (void*)addr_start;
} void* get_remote_func_addr(pid_t pid, const char *module_name, const void *func_local_addr)
{
void *module_local_addr, *module_remote_addr, *func_remote_addr;
module_remote_addr = get_module_addr(pid, module_name);
module_local_addr = get_module_addr(-, module_name);
if (module_remote_addr == NULL || module_local_addr == NULL) {
return NULL;
}
return (void*)((unsigned long)func_local_addr - (unsigned long)module_local_addr + (unsigned long)module_remote_addr);
}
有了这些封装后的函数,我们便可以着手对注入流程进行实现了。
首先附加目标进程,并获取一些必要函数在目标进程中的地址。
//附加目标进程
pid_t pid = 目标进程号;
CODE_CHECK(ptrace_attach(pid));
//备份目标进程寄存器
struct pt_regs current_regs, origin_regs;
CODE_CHECK(ptrace_getregs(pid, &origin_regs));
memcpy(¤t_regs, &origin_regs, sizeof(struct pt_regs));
//获取远程函数地址
void *addr_mmap, *addr_munmap, *addr_dlopen, *addr_dlsym;
addr_mmap = get_remote_func_addr(pid, "/system/lib/libc.so", (void*)mmap);
addr_munmap = get_remote_func_addr(pid, "/system/lib/libc.so", (void*)munmap);
addr_dlopen = get_remote_func_addr(pid, "/system/bin/linker", (void *)dlopen);
ddr_dlsym = get_remote_func_addr(pid, "/system/bin/linker", (void *)dlsym);
接着远程调用目标进程mmap函数分配空间,因为我们在调用目标进程的dlopen等函数时,需要传递字符串信息,所以需要开辟一块空间写入字符串。
long remote_addr, remote_handle, remote_func, remote_ret;
//void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offsize);
long remote_params[];
remote_params[] = ;
remote_params[] = MMAP_SIZE;
remote_params[] = PROT_READ | PROT_WRITE | PROT_EXEC;
remote_params[] = MAP_ANONYMOUS | MAP_PRIVATE;
remote_params[] = ;
remote_params[] = ;
CODE_CHECK(ptrace_call(pid, addr_mmap, remote_params, , ¤t_regs));
//函数返回值在r0寄存器
remote_addr = current_regs.ARM_r0;
之后我们需要远程调用dlopen函数使目标进程加载我们的模块,但在调用之前需要先将模块路径先写入目标进程。
//将模块路径写入目标进程
char *lib_path = "模块路径";
CODE_CHECK(ptrace_writedata(pid, (void*)remote_addr, lib_path, strlen(lib_path) + ));
//void *dlopen(const char *filename, int flag);
remote_params[] = remote_addr;
remote_params[] = RTLD_NOW| RTLD_GLOBAL;
LOG_INFO("call remote dlopen");
CODE_CHECK(ptrace_call(pid, addr_dlopen, remote_params, , ¤t_regs));
remote_handle = current_regs.ARM_r0;
此时已成功将模块注入目标进程,需要远程调用dlsym函数获取模块在加载到目标进程后其中的函数地址,同之前一样需要先将函数名写入目标进程空间。
char *func_name = "函数名";
CODE_CHECK(ptrace_writedata(pid, (void*)remote_addr, func_name, strlen(func_name) + ));
//void *dlsym(void *handle, const char *symbol);
remote_params[] = remote_handle;
remote_params[] = remote_addr;
CODE_CHECK(ptrace_call(pid, addr_dlsym, remote_params, , ¤t_regs));
remote_func = current_regs.ARM_r0;
在获取函数地址后,直接对其进行远程调用,这里假设该函数不需要参数。
//int func();
CODE_CHECK(ptrace_call(pid, (void*)remote_func, NULL, , ¤t_regs));
remote_ret = current_regs.ARM_r0;
这时我们注入的代码已成功执行,可以恢复目标进程的运行状态了。
//释放之前在目标进程分配的空间
//int munmap(void *start, size_t length);
remote_params[] = remote_addr;
remote_params[] = MMAP_SIZE;
CODE_CHECK(ptrace_call(pid, addr_munmap, remote_params, , ¤t_regs));
//恢复寄存器信息
CODE_CHECK(ptrace_setregs(pid, &origin_regs));
//脱离目标进程
CODE_CHECK(ptrace_detach(pid));
一些需要包含的头文件:
#include <sys/ptrace.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <asm/ptrace.h>
#include <asm/mman.h>
#include <stdlib.h>
#include <unistd.h>
#include <dlfcn.h>
#include <stdio.h>
#include <string.h>