被测试代码:
[代码]java代码:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 |
#include<stdio.h> #include<stdlib.h> int count = 0; void print() { printf("hello,%d\n",count); sleep(1); } int main(int argc, char const *argv[]) { while(1){ print(); count++; } return 0; }</stdlib.h></stdio.h> |
注入方法都是通过ptrace实现的.
本文代码在github.
调用系统so库中的函数
目标函数是libc.so中的sleep函数.
正常情况是每输出一次暂停一秒,现在我们让它暂停10秒.
总体思路
§ 获取目标进程sleep函数地址
§ 在目标进程内执行sleep函数
如何获取函数地址
§ 已知条件: 本进程的基址、目标进程的基址、本进程中sleep函数的地址(当然,这些已知条件也是需要获得的 :p)
/proc/<pid>/maps文件中存储的是进程内存映射详情,我们可以在这个文件中查询进程中so的基址;
sleep函数在本进程中的地址直接可以获得(void*)
§ 求解: 目标进程中sleep函数地址
§ 计算: 本进程sleep地址 - 本进程基址 + 目标进程基址
获取so库的加载基址
打开/proc/<pid>/maps文件找到基址.
[代码]java代码:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | void* get_module_base(int pid, const char* module_name) { FILE *f; //文件指针 long addr = 0; //模块地址 char filename[32]; //maps路径 char *pch; char line[1024]; //每行 if(pid == 0){ snprintf(filename, sizeof(filename), "/proc/self/maps"); } else { snprintf(filename, sizeof(filename), "/proc/%d/maps", pid); } f = fopen(filename, "r"); if(f != NULL){ while(fgets(line,sizeof(line),f)){ if(strstr(line, module_name)) { //找到该行是否含有module_name pch = strtok(line,"-"); //分割出基址字符串 addr = strtoul(pch,NULL,0x10); //转换为16进制数 if(addr == 0x8000) //32位linux程序中默认的text加载地址为0x08408000,64位的改为0x00400000,此时计算base地址就没什么用了 addr = 0; break; } } fclose(f); } return (void*)addr; } |
计算目标进程中sleep函数地址
[代码]java代码:
01 02 03 04 05 06 07 08 09 10 | long get_remote_addr(int target_pid, const char* module_name, void* local_addr) { void* local_handle = get_module_base(0,module_name); void* remote_handle = get_module_base(target_pid,module_name); printf("local_handle:%p remote_handle:%p\n", local_handle, remote_handle); //计算公式 long remote_addr = (long)((uint32_t)local_addr - (uint32_t)local_handle + (uint32_t)remote_handle); printf("remote_addr:%p\n", remote_addr); return remote_addr; } |
如何执行sleep函数
§ 设置函数参数,如果参数个数小于等于4,参数按顺序放入R0~R4寄存器中;如果参数个数大于4,多余的部分需要入栈.
§ 设置pc寄存器的值,设置当前指令集标志位.
§ 应用以上寄存器的修改使之生效.
§ 等待函数执行.
[代码]java代码:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | //目标进程id,参数地址,参数个数,寄存器地址 int ptrace_call(int pid, long addr, long *params, uint32_t params_num, struct pt_regs* regs) { uint32_t i; for (i = 0; i < params_num && i < 4; i++) { //设置少于4个的参数 regs->uregs[i] = params[i]; } //设置多于4个的参数 if (i < params_num) { regs->ARM_sp -= (params_num - i) * long_size; //抬高栈顶指针(分配空间) writeData(pid, (long)regs->ARM_sp, (char*)¶ms[i], (params_num - i) * long_size); //写入 } regs->ARM_pc = addr; //设置pc if (regs->ARM_pc & 1) { //判断是否是Thumb指令 regs->ARM_pc &= (~1u); //Thumb的pc最后一位总是0 regs->ARM_cpsr |= CPSR_T_MASK; //T标志位为1 } else { //arm regs->ARM_cpsr &= ~CPSR_T_MASK; //T标志位为0 } regs->ARM_lr = 0; //为了使sleep函数执行完毕后产生“内存访问错误”,这样我们就知道什么时候执行完了 if(ptrace_setregs(pid,regs)==-1 || ptrace_continue(pid)==-1){ //目标进程继续执行 return -1; } int stat = 0; //WUNTRACED表示如果pid进程进入暂停状态,那么waitpid函数立即返回 waitpid(pid,&stat,WUNTRACED); //等待sleep函数执行,等待过程中本进程暂停执行 printf("%d\n", stat); while (stat != 0xb7f) { //0xb7f表示目标进程进入暂停状态 printf("%d\n", stat); if (ptrace_continue(pid) == -1) { return -1; } waitpid(pid,&stat,WUNTRACED); } return 0; } |
如何注入
§ 保存寄存器的值
§ 获得sleep函数地址
§ 执行sleep函数
§ 恢复寄存器的值
[代码]java代码:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | void inject(int pid) { struct pt_regs old_regs,regs; long sleep_addr; //保存寄存器 ptrace(PTRACE_GETREGS, pid, NULL, &old_regs); memcpy(®s, &old_regs, sizeof(regs)); long parameters[1]; parameters[0] = 10; sleep_addr = get_remote_addr(pid, "libc.so", (void*)sleep); ptrace_call(pid,sleep_addr,parameters,1,®s); //恢复寄存器 ptrace(PTRACE_SETREGS, pid, NULL, &old_regs); } |
主函数
[代码]java代码:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #include<stdio.h> #include<string.h> // strstr,strtok #include<stdlib.h> //strtoul #include<stdint.h> //uint32_t #include<unistd.h> //sleep #include<sys ptrace.h=""> #include<linux wait.h=""> // WUNTRACED #include<time.h> int main(int argc, char* argv[]) { if(argc != 2){ printf("usage: %s <pid to="" be="" traced="">\n",argv[0]); return 1; } int pid = atoi(argv[1]); if(0 != ptrace(PTRACE_ATTACH, pid, NULL, NULL)){ printf("attach failed."); return 1; } inject(pid); ptrace(PTRACE_DETACH, pid, NULL, NULL); return 0; }</pid></time.h></linux></sys></unistd.h></stdint.h></stdlib.h></string.h></stdio.h> |
番外
/proc/<pid> 文件夹
文件 | 内容 |
cmdline | 命令行全名(加参数变量)和 ps 命令中的command 列结果一样 |
cwd | 进程的工作目录 和 (pwdx PID) 结果相同 |
environ | 进程的环境变量 |
exe | 一般是/bin/ 的链接 |
fd | 进程打开的文件描述fu .用ls -l 可以查看具体的文件 (可以用lsof -p PID) |
status | 进程的相关状态 |
task | 该目录下是进程所包含的线程(note: ps 可以查看线程) |
mounts | 进程挂载点 |
maps | 进程内存映射详情 |
关于pc寄存器
arm.pdf 中的A2.4.3 Register 15 and the program counter有这样一段话:
是关于指令集在pc寄存器上的表现的.
Reading the program counter
When an instruction reads the PC, the value read depends on which instruction set it comes from:
• For an ARM instruction, the value read is the address of the instruction plus 8 bytes. Bits [1:0] of this
value are always zero, because ARM instructions are always word-aligned.
• For a Thumb instruction, the value read is the address of the instruction plus 4 bytes. Bit [0] of this
value is always zero, because Thumb instructions are always halfword-aligned.
关于CPSR寄存器
31 | 30 | 29 | 28 | 27 | 26 25 | 24 | 23 20 | 19 16 | 15 10 | 9 | 8 | 7 | 6 | 5 | 4 0 |
N | Z | C | V | Q | Res | J | RESERVED | GE[3:0] | RESERVED | E | A | I | F | T | M[4:0] |
其中J和T标记位代表当前指令集:
J | T | Instruction set |
0 | 0 | ARM |
0 | 1 | Thumb |
1 | 0 | Jazelle |
1 | 1 | RESERVED |
关于waitpid
详细介绍可看官方文档.
参数status
wait函数调用过后,status指针指向可以被宏解析的值,这些宏在ndk目录下platforms/android-21/arch-arm/usr/include/sys/wait.h文件中定义.
高2字节用于表示导致子进程的退出或暂停状态信号值(WTERMSIG),低2字节表示子进程是退出(0x0)还是暂停(0x7f)状态(WEXITSTATUS)。
如:0xb7f就表示子进程为暂停状态,导致它暂停的信号量为11即sigsegv错误。
关于错误代码的文档可看这里,
定义在ndk目录下platforms/android-21/arch-arm/usr/include/asm/signal.h中.
其中两个宏:
WEXITSTATUS(*statusPtr):
if the child process terminates normally, this macro evaluates to the lower 8 bits of the value passed to the exit or _exit function or returned from main.
WTERMSIG(*statusPtr)
if the child process ends by a signal that was not caught, this macro evaluates to the number of that signal.
参数options
指定了waitpid的额外行动.选项有:
WNOHANG:
告诉waitpid不等程序中止立即返回status信息.
正常情况是当主进程对子进程使用了waitpid,主进程就会阻塞直到waitpid返回status信息;如果指定了WNOHANG选项,主进程就不会阻塞了.
如果还没有可用的status信息,waitpid返回0.
WUNTRACED:
告诉waitpid,如果子进程进入暂停状态或者已经终止,那么就立即返回status信息,正常情况是紫禁城终止的时候才返回.
如果是被ptrace的子进程,那么即使不提供WUNTRACED参数,也会在子进程进入暂停状态的时候立即返回。
对于使用ptrace_cont运行的子进程,它会在3种情况下进入暂停状态:①下一次系统调用;②子进程退出;③子进程的执行发生错误。
总结
程序中的0xb7f就表示子进程进入了暂停状态,且发送的错误信号为11(SIGSEGV),它表示试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据。
当子进程执行完注入的函数后,由于我们在前面设置了regs->ARM_lr = 0,它就会返回到0地址处继续执行,这样就会产生SIGSEGV了.
调用自定义so库中的函数
§ 保存当前寄存器的状态
§ 获取目标程序的mmap, dlopen, dlsym, dlclose函数地址
§ 调用mmap分配空间保存参数信息
§ 调用dlopen加载so库
§ 调用dlsym找到目标函数地址
§ 执行目标函数
§ 调用dlclose卸载so库
§ 恢复寄存器的状态
保存当前寄存器的状态
[代码]java代码:
1 2 3 | struct pt_regs old_regs,regs; ptrace(PTRACE_GETREGS, pid, NULL, &old_regs); memcpy(®s,&old_regs,sizeof(regs)); |
获取目标程序的mmap, dlopen, dlsym, dlclose函数地址
[代码]java代码:
1 2 3 4 5 | long mmap_addr,dlopen_addr,dlsym_addr,dlclose_addr; mmap_addr = get_remote_addr(pid, libc_path, (void*)mmap); dlopen_addr = get_remote_addr(pid, libc_path, (void*)dlopen); dlsym_addr = get_remote_addr(pid, libc_path, (void*)dlsym); dlclose_addr = get_remote_addr(pid, libc_path, (void*)dlclose); |
调用mmap分配空间保存参数信息
mmap的原型如下:
[代码]java代码:
1 | void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); |
参数 | 描述 |
addr | 映射的起始地址,为0表示由系统决定映射的起始地址 |
length | 映射的长度 |
prot | 映射的内存保存属性,不能与文件的打开模式冲突 |
flags | 指定映射对象的类型,映射选项和映射页是否可以共享 |
fd | 有效的文件描述符,一般是由open()函数返回;其值也可以设置为-1,此时需要指定flags参数中的MAP_ANON,表明进行的是匿名映射 |
offset | 被映射对象内容的起点 |
这里我们需要的调用语句是mmap(0,0x4000,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_ANONYMOUS|MAP_PRIVATE,0,0),
PROT_EXEC表示可执行.
PROT_READ表示可读.
PROT_WRITE表示可写.
MAP_PRIVATE表示建.立一个写入时拷贝的私有映射.内存区域的写入不会影响到原文件.这个标志和以上标志是互斥的,只能使用其中一个.
MAP_ANONYMOUS表示匿名映射,映射区不与任何文件关联.
则:
[代码]java代码:
01 02 03 04 05 06 07 08 09 10 11 | long parameters[10]; parameters[0] = 0; //构造参数 parameters[1] = 0x4000; parameters[2] = PROT_READ | PROT_WRITE | PROT_EXEC; parameters[3] = MAP_ANONYMOUS | MAP_PRIVATE; parameters[4] = 0; parameters[5] = 0; ptrace_call(pid,mmap_addr,parameters,6,®s); //调用结束后获得r0中保存的返回值 ptrace(PTRACE_GETREGS,pid,NULL,®s); long mapping_base = regs.ARM_r0; |
调用dlopen加载so库
原型:
[代码]java代码:
1 2 |
void *dlopen(const char *filename, int flags); |
参数 | 描述 |
filename | so库名 |
flags | 打开方式 |
这里我们需要的调用语句是dlopen(so_path,RTLD_NOW | RTLD_GLOBAL),
RTLD_NOW表示需要在dlopen返回前,解析出所有未定义符号,如果解析不出来在dlopen会返回NULL;
RTLD_GLOBAL表示动态库中定义的符号可被其后打开的其它库解析.
则:
[代码]java代码:
1 2 3 4 5 6 | writeData(pid, mapping_base, so_path, strlen(so_path)+1); //将库名字符串放入目标进程空间 parameters[0] = mapping_base; parameters[1] = RTLD_NOW | RTLD_GLOBAL; ptrace_call(pid, dlopen_addr, parameters, 2, ®s); ptrace(PTRACE_GETREGS,pid,NULL,®s); //调用结束后获得r0中保存的返回值 long handle = regs.ARM_r0; |
调用dlsym找到目标函数地址
原型:
[代码]java代码:
1 | void *dlsym(void *handle, const char *symbol); |
参数 | 描述 |
handle | so库的基址 |
symbol | 函数名地址 |
这里我们需要的调用语句是dlsym(handle, function_name),则:
[代码]java代码:
1 2 3 4 5 6 | writeData(pid, mapping_base, function_name, strlen(function_name)+1); parameters[0] = handle; parameters[1] = mapping_base; ptrace_call(pid, dlsym_addr, parameters, 2, ®s); ptrace(PTRACE_GETREGS,pid,NULL,®s); //调用结束后获得r0中保存的返回值 long function_addr = regs.ARM_r0; |
执行目标函数
先写段c程序编译为so文件:
[代码]java代码:
1 2 3 4 5 | #include<stdio.h> void hello(char *str) { printf("hello %s\n",str); }</stdio.h> |
则:
[代码]java代码:
1 2 3 | writeData(pid, mapping_base, function_parameters, strlen(function_parameters)+1); parameters[0] = mapping_base; ptrace_call(pid, function_addr, parameters, 1, ®s); |
调用dlclose卸载so库
原型:
[代码]java代码:
1 | int dlclose(void *handle); |
则:
[代码]java代码:
1 2 | parameters[0] = handle; ptrace_call(pid,dlclose_addr,parameters,1,®s); |
恢复寄存器的状态
[代码]java代码:
1 | ptrace(PTRACE_SETREGS,pid,NULL,&old_regs); |
共同学习,写下你的评论
评论加载中...
作者其他优质文章