![]() 嵌入式linux QQ交流群:175159209,欢迎爱好者加入交流技术问题! ptrace提供让一个进程来控制另一个进程的能力,包括检测,修改被控制进程的代码,数据,寄存器,进而实现设置断点,注入代码和跟踪系统调用的功能。 这里把使用ptrace函数的进程称为tracer,被控制的进程称为tracee。 使用ptrace函数来拦截系统调用(system call) 操作系统向上层提供标准的API来执行与底层硬件交互的操作,这些标准API称为系统调用,每个系统调用都有一个调用编号,可以在unistd.h中查询。当进程触发一个系统调用时它会把参数放入寄存器中,然后通过软中断进入内核模式,通过内核来执行这个系统调用的代码。 在X86_64体系中,系统调用号保存在rax,调用参数依次保存在rdi,rsi,rdx,rcx,r8和r9中;而在x86体系中,系统调用号保存在寄存器eax中,其余的参数依次保存在ebx,ecx,edx,esi中 例如控制台打印所执行的系统调用为 write(1,"Hello",5) 翻译为汇编代码为 mov rax, 1 mov rdi, message mov rdx, 5 syscall message: db "Hello" 在执行系统调用时,内核先检测一个进程是否为tracee,如果是的话内核就会暂停该进程,然后把控制权转交给tracer,之后tracer就可以查看或者修改tracee的寄存器了。 示例代码如下 #include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <sys/reg.h> #include <sys/user.h> #include <stdio.h> int main() { pid_t child; long orig_rax; child = fork(); if(child == 0) { ptrace(PTRACE_TRACEME,0,NULL,NULL); execl("/bin/ls","ls",NULL); } else { wait(NULL); orig_rax = ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL); printf("the child made a system call %ld\n",orig_rax); ptrace(PTRACE_CONT,child,NULL,NULL); } return 0; } //输出:the child made a system call 59 该程序通过fork创建出一个我们将要跟踪(trace)的子进程,在执行execl之前,子进程通过ptrace函数的PTRACE_TRACEME参数来告知内核自己将要被跟踪。 对于execl,这个函数实际上会触发execve这个系统调用,这时内核发现此进程为tracee,然后将其暂停,发送一个signal唤醒等待中的tracer(此程序中为主线程)。 当触发系统调用时,内核会将保存调用编号的rax寄存器的内容保存在orig_rax中,我们可以通过ptrace的PTRACE_PEEKUSER参数来读取。 ORIG_RAX为寄存器编号,保存在sys/reg.h中,而在64位系统中,每个寄存器有8个字节的大小,所以此处用8*ORIG_RAX来获取该寄存器地址。 当我们获取到系统调用编号以后,就可以通过ptrace的PTRACE_CONT参数来唤醒暂停中的子进程,让其继续执行。 ptrace参数 long ptrace(enum __ptrace_request request,pid_t pid,void addr, void *data); 参数request 控制ptrace函数的行为,定义在sys/ptrace.h中。 参数pid 指定tracee的进程号。 以上两个参数是必须的,之后两个参数分别为地址和数据,其含义由参数request控制。 具体request参数的取值及含义可查看帮助文档(控制台输入:man ptrace) 注意返回值,man手册上的说法是返回一个字的数据大小,在32位机器上是4个字节,在64位机器上是8个字节,都对应一个long的长度。百度可以搜到很多不负责的帖子说返回一个字节的数据是不对的! 读取系统调用参数 通过ptrace的PTRACE_PEEKUSER参数,我们可以查看USER区域的内容,例如查看寄存器的值。USER区域为一个结构体(定义在sys/user.h中的user结构体)。 内核将寄存器的值储存在该结构体中,便于tracer通过ptrace函数查看。 示例代码如下 #include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/reg.h> #include <sys/user.h> #include <sys/syscall.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { pid_t child; long orig_rax,rax; long params[3]={0}; int status; int insyscall = 0; child = fork(); if(child == 0) { ptrace(PTRACE_TRACEME,0,NULL,NULL); execl("/bin/ls","ls",NULL); } else { while(1) { wait(&status); if(WIFEXITED(status)) break; orig_rax = ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL); //printf("the child made a system call %ld\n",orig_rax); if(orig_rax == SYS_write) { if(insyscall == 0) { insyscall = 1; params[0] = ptrace(PTRACE_PEEKUSER,child,8*RDI,NULL); params[1] = ptrace(PTRACE_PEEKUSER,child,8*RSI,NULL); params[2] = ptrace(PTRACE_PEEKUSER,child,8*RDX,NULL); printf("write called with %ld, %ld, %ld\n",params[0],params[1],params[2]); } else { rax = ptrace(PTRACE_PEEKUSER,child,8*RAX,NULL); printf("write returned with %ld\n",rax); insyscall = 0; } } ptrace(PTRACE_SYSCALL,child,NULL,NULL); } } return 0; } /*** 输出: write called with 1, 25226320, 65 ptrace_1.c ptrace_2.c ptrace_3.C ptrace_4.C ptrace_5.c test.c write returned with 65 ***/ 以上代码中我们查看write系统调用(由ls命令向控制台打印文字触发)的参数。 为了追踪系统调用,我们使用ptrace的PTRACE_SYSCALL参数,它会使tracee在触发系统调用或者结束系统调用时暂停,同时向tracer发送signal。 在之前的例子中我们使用PTRACE_PEEKUSER参数来查看系统调用的参数,同样的,我们也可以查看保存在RAX寄存器中的系统调用返回值。 上边代码中的status变量时用来检测是否tracee已经执行结束,是否需要继续等待tracee执行。 读取所有寄存器的值 这个例子中演示一个获取寄存器值的简便方法 #include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/user.h> #include <sys/reg.h> #include <sys/syscall.h> #include <unistd.h> #include <stdio.h> int main() { pid_t child; long orig_rax ,rax; long params[3] = {0}; int status = 0; int insyscall = 0; struct user_regs_struct regs; child = fork(); if(child == 0) { ptrace(PTRACE_TRACEME,0,NULL,NULL); execl("/bin/ls","ls",NULL); } else { while(1) { wait(&status); if(WIFEXITED(status)) break; orig_rax = ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL); if(orig_rax == SYS_write) { if(insyscall == 0) { insyscall = 1; ptrace(PTRACE_GETREGS,child,NULL,®s); printf("write called with %llu, %llu, %llu\n",regs.rdi,regs.rsi,regs.rdx); } else { ptrace(PTRACE_GETREGS,child,NULL,®s); printf("write returned with %ld\n",regs.rax); insyscall = 0; } } ptrace(PTRACE_SYSCALL,child,NULL,NULL); } } return 0; } 这个例子中通过PTRACE_GETREGS参数获取了所有的寄存器值。结构体user_regs_struct定义在sys/user.h中。 修改系统调用的参数 现在我们已经知道如何拦截一个系统调用并查看其参数了,接下来我们来修改它 #include <sys/ptrace.h> #include <sys/user.h> #include <sys/reg.h> #include <sys/wait.h> #include <sys/syscall.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #define LONG_SIZE 8 //获取参数 char* getdata(pid_t child,unsigned long addr,unsigned long len) { char *str =(char*) malloc(len + 1); memset(str,0,len +1); union u{ long int val; char chars[LONG_SIZE]; }word; int i, j; for(i = 0,j = len/LONG_SIZE; i<j; ++i) { word.val = ptrace(PTRACE_PEEKDATA,child,addr + i*LONG_SIZE,NULL); if(word.val == -1) perror("trace get data error"); memcpy(str+i*LONG_SIZE,word.chars,LONG_SIZE); } j = len % LONG_SIZE; if(j != 0) { word.val = ptrace(PTRACE_PEEKDATA,child,addr + i*LONG_SIZE,NULL); if(word.val == -1) perror("trace get data error"); memcpy(str+i*LONG_SIZE,word.chars,j); } return str; } //提交参数 void putdata(pid_t child,unsigned long addr,unsigned long len, char *newstr) { union u { long val; char chars[LONG_SIZE]; }word; int i,j; for(i = 0, j = len/LONG_SIZE; i<j ; ++i) { memcpy(word.chars,newstr+i*LONG_SIZE,LONG_SIZE); if(ptrace(PTRACE_POKEDATA, child, addr+i*LONG_SIZE,word.val) == -1) perror("trace error"); } j = len % LONG_SIZE; if(j !=0 ) { memcpy(word.chars,newstr+i*LONG_SIZE,j); ptrace(PTRACE_POKEDATA, child, addr+i*LONG_SIZE,word.val); } } //修改参数 void reserve(char *str,unsigned int len) { int i,j; char tmp; for(i=0,j=len-2; i<=j; ++i,--j ) { tmp = str[i]; str[i] = str[j]; str[j] = tmp; } } int main() { pid_t child; child = fork(); if(child == 0) { ptrace(PTRACE_TRACEME,0,NULL,NULL); execl("/bin/ls","ls",NULL); } else { struct user_regs_struct regs; int status = 0; int toggle = 0; while(1) { wait(&status); if(WIFEXITED(status)) break; memset(®s,0,sizeof(struct user_regs_struct)); if(ptrace(PTRACE_GETREGS,child,NULL,®s) == -1) { perror("trace error"); } if(regs.orig_rax == SYS_write) { if(toggle == 0) { toggle = 1; //in x86_64 system call ,pass params with %rdi, %rsi, %rdx, %rcx, %r8, %r9 //no system call has over six params printf("make write call params %llu, %llu, %llu\n",regs.rdi,regs.rsi,regs.rdx); char *str = getdata(child,regs.rsi,regs.rdx); printf("old str,len %lu:\n%s",strlen(str),str); reserve(str,regs.rdx); printf("hook str,len %lu:\n%s",strlen(str),str); putdata(child,regs.rsi,regs.rdx,str); free(str); } else { toggle = 0; } } ptrace(PTRACE_SYSCALL,child,NULL,NULL); } } return 0; } /*** 输出: make write call params 1, 9493584, 66 old str,len 66: ptrace ptrace2 ptrace3 ptrace4 ptrace5 test test.s hook str,len 66: s.tset tset 5ecartp 4ecartp 3ecartp 2ecartp ecartp s.tset tset 5ecartp 4ecartp 3ecartp 2ecartp ecartp make write call params 1, 9493584, 65 old str,len 65: ptrace_1.c ptrace_2.c ptrace_3.C ptrace_4.C ptrace_5.c test.c hook str,len 65: c.tset c.5_ecartp C.4_ecartp C.3_ecartp c.2_ecartp c.1_ecartp c.tset c.5_ecartp C.4_ecartp C.3_ecartp c.2_ecartp c.1_ecartp ***/ 这个例子中,综合了以上我们提到的所有知识。进一步得,我们使用了ptrace的PTRACE_POKEDATA参数来修改系统调用的参数值。 这个参数和PTRACE_PEEKDATA参数的作用相反,它可以修改tracee指定地址的数据。 单步调试 接下来介绍一个调试器中常用的操作,单步调试,它就用到了ptrace的PTRACE_SINGLESTEP参数。 #include <sys/ptrace.h> #include <sys/types.h> #include <sys/user.h> #include <sys/reg.h> #include <sys/syscall.h> #include <sys/wait.h> #include <stdio.h> #include <unistd.h> #define LONG_SIZE 8 void main() { pid_t chid; chid = fork(); if(chid == 0) { ptrace(PTRACE_TRACEME,0,NULL,NULL); //这里的test是一个输出hello world的小程序 execl("./test","test",NULL); } else { int status = 0; struct user_regs_struct regs; int start = 0; long ins; while(1) { wait(&status); if(WIFEXITED(status)) break; ptrace(PTRACE_GETREGS,chid,NULL,®s); if(start == 1) { ins = ptrace(PTRACE_PEEKTEXT,chid,regs.rip,NULL); printf("EIP:%llx Instuction executed:%lx\n",regs.rip,ins); } if(regs.orig_rax == SYS_write) { start = 1; ptrace(PTRACE_SINGLESTEP,chid,NULL,NULL); }else{ ptrace(PTRACE_SYSCALL,chid,NULL,NULL); } } } } 通过rip寄存器的值来获取下一条要执行指令的地址,然后用PTRACE_PEEKDATA读取。 这样,就可以看到要执行的每条指令的机器码。 ![]() 点击下方“阅读原文”查看更多 ![]() ---------------------------------------------------------------------------------------------------------------------- 我们尊重原创,也注重分享,文章来源于微信公众号:嵌入式Linux中文站,建议关注公众号查看原文。如若侵权请联系qter@qter.org。 ---------------------------------------------------------------------------------------------------------------------- |