转自http://www.cnblogs.com/catch/p/3476280.htmlhtml
[本文翻译自这里: http://www.linuxjournal.com/article/6100?page=0,0,做者:Pradeep Padaia]linux
你是否曾经想过怎样才能拦截系统调用?你是否曾经想过经过修改一下系统调用的参数来耍一把内核?你是否想过调试器是怎样把一个进程停下来,而后把控制权转移给你的?若是你觉得这些都是经过复杂的内核编程来实现的,那你就错了,事实上,Linux 提供了一种很优雅的方式来实现上述全部行为:ptrace 系统调用。ptrace 提供了一种机制使得父进程能够观察和控制子进程的执行过程,ptrace 还能够检查和修改该子进程的可执行文件在内存中的镜像及该子进程所使用的寄存器中的值。这种用法一般来讲,主要用于实现对进程插入断点和跟踪子进程的系统调用。编程
在本篇文章中,咱们将学习怎么去拦截一个系统调用而且修改该系统调用的参数,在后续一篇文章中,咱们将继续探讨 ptrace 的一些更深刻的技术,如设置断点,在运行的子进程中插入代码等。咱们将会查看进程的寄存器和数据段,并去修改其中的内容。咱们还会介绍一种方式来在进程中插入代码,使得该进程能停下来,并执行咱们插入的任意代码。学习
基础spa
操做系统经过一个叫作“系统调用”的标准机制来对上层提供服务,他们提供了一系列标准的API来让上层应用程序获取底层的硬件和服务,好比文件系统。当一个进程想要进行一个系统调用的时候,它会把该系统调用所须要用到的参数放到寄存器里,而后执行软中断指令0x80. 这个软中断就像是一个门,经过它就能进入内核模式,进入内核模式后,内核将会检查系统调用的参数,而后执行该系统调用。操作系统
在 i386 平台下(本文全部代码都基于 i386), 系统调用的编号会被放在寄存器 %eax 中,而系统调用的参数会被依次放到 %ebx,%ecx,%edx,%exi 和 %edi中,好比说,对于下面的系统调用:翻译
write(2,
"Hello"
, 5)
|
编译后,它最后大概会被转化成下面这样子:debug
movl $4, %eax movl $2, %ebx movl $hello,%ecx movl $5, %edx int $0x80
其中 $hello 指向字符串 "Hello"。调试
看完上面简单的例子,如今咱们来看看 ptrace 又是怎样执行的。首先,咱们假设进程 A 要 ptrace 进程 B。在 ptrace 系统调用真正开始前,内核会检查一下咱们将要 trace 的进程 B 是否当前已经正在被 traced 了,若是是,内核就会把该进程 B 停下来,并把控制权交给调用进程 A (任什么时候候,子进程只能被父进程这惟一一个进程所trace),这使得进程A有机会去检查和修改进程B的寄存器的值。code
下面咱们用一个例子来讲明:
#include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <linux/user.h> /* For constants ORIG_EAX etc */ int main() { pid_t child; long orig_eax; child = fork(); if(child == 0) { ptrace(PTRACE_TRACEME, 0, NULL, NULL); execl("/bin/ls", "ls", NULL); } else { wait(NULL); orig_eax = ptrace(PTRACE_PEEKUSER, child, 4 * ORIG_EAX, NULL); printf("The child made a " "system call %ld\n", orig_eax); ptrace(PTRACE_CONT, child, NULL, NULL); } return 0; }
当把上面这段代码编译执行后,终端上除了命令 ls 的输出外,还会输出下面一行:
The child made a system call 11
根据上面的输出,咱们知道,在执行 ls 命令的时候,第11号系统调用被执行了,它是子进程中执行的第一个系统调用。若是想查看一下各个系统调用编号对应的名字,能够参考头文件:/usr/include/asm/unistd.h.
正如你在上面的例子中所看到,ptrace 的使用流程通常是这样的:父进程 fork() 出子进程,子进程中执行咱们所想要 trace 的程序,在子进程调用 exec() 以前,子进程须要先调用一次 ptrace,以 PTRACE_TRACEME 为参数。这个调用是为了告诉内核,当前进程已经正在被 traced,当子进程执行 execve() 以后,子进程会进入暂停状态,把控制权转给它的父进程(SIG_CHLD信号), 而父进程在fork()以后,就调用 wait() 等子进程停下来,当 wait() 返回后,父进程就能够去查看子进程的寄存器或者对子进程作其它的事情了。
当系统调用发生时,内核会把当前的%eax中的内容(即系统调用的编号)保存到子进程的用户态代码段中(USER SEGMENT or USER CODE),咱们能够像上面的例子那样经过调用Ptrace(传入PTRACE_PEEKUSER做为第一个参数)来读取这个%eax的值,当咱们作完这些检查数据的事情以后,经过调用ptrace(PTRACE_CONT),可让子进程从新恢复运行。
ptrace的参数
ptrace 总共有 4 个参数:
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
其中第一个参数决定ptrace的行为也决定了接下来其它3个参数是怎样被使用的,第1个参数能够取如下任意一个值:
PTRACE_TRACEME, PTRACE_PEEKTEXT, PTRACE_PEEKDATA, PTRACE_PEEKUSER, PTRACE_POKETEXT, PTRACE_POKEDATA, PTRACE_POKEUSER, PTRACE_GETREGS, PTRACE_GETFPREGS, PTRACE_SETREGS, PTRACE_SETFPREGS, PTRACE_CONT, PTRACE_SYSCALL, PTRACE_SINGLESTEP, PTRACE_DETACH
本文接下来会解释这些参数有什么不一样的地方。
读取系统调用的参数
经过调用ptrace并传入PTRACE_PEEKUSER做为第一个参数,咱们能够检查子进程中,保存了该进程的寄存器的内容(及其它一些内容)的用户态内存区域(USER area)。内核把寄存器的内容保存到这块区域,就是为了可以让父进程经过ptrace来读取,下面举一个例子来讲明一下:
#include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <linux/user.h> #include <sys/syscall.h> /* For SYS_write etc */ int main() { pid_t child; long orig_eax, eax; long params[3]; 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_eax = ptrace(PTRACE_PEEKUSER, child, 4 * ORIG_EAX, NULL); if(orig_eax == SYS_write) { if(insyscall == 0) { /* Syscall entry */ insyscall = 1; params[0] = ptrace(PTRACE_PEEKUSER, child, 4 * EBX, NULL); params[1] = ptrace(PTRACE_PEEKUSER, child, 4 * ECX, NULL); params[2] = ptrace(PTRACE_PEEKUSER, child, 4 * EDX, NULL); printf("Write called with " "%ld, %ld, %ld\n", params[0], params[1], params[2]); } else { /* Syscall exit */ eax = ptrace(PTRACE_PEEKUSER, child, 4 * EAX, NULL); printf("Write returned " "with %ld\n", eax); insyscall = 0; } } ptrace(PTRACE_SYSCALL, child, NULL, NULL); } } return 0; }
编译执行上面的代码,获得的输出和前一个例子的输出有些相似:
ppadala@linux:~/ptrace > ls a.out dummy.s ptrace.txt libgpm.html registers.c syscallparams.c dummy ptrace.html simple.c ppadala@linux:~/ptrace > ./a.out Write called with 1, 1075154944, 48 a.out dummy.s ptrace.txt Write returned with 48 Write called with 1, 1075154944, 59 libgpm.html registers.c syscallparams.c Write returned with 59 Write called with 1, 1075154944, 30 dummy ptrace.html simple.c Write returned with 30
在这个例子中,咱们追踪了 write() 这个系统调用,由上面的输出咱们能够看出,ls这个程序总共调用了3次 write().
调用 ptrace 并传入参数:PTRACE_SYSCALL, 会使得子进程在每次进行系统调用及结束一次系统调用时都会被内核停下来,这一个过程就至关于作了一个ptrace(PTRACE_CONT) 调用,而后在每次系统调用前和系统调用后就停下来。在前面一个例子中,咱们用 PTRACE_PEEKUSER 来读取系统调用的参数,当系统调用结束后,该调用的返回值会被放在%eax中,像上面的例子展现的那样,这个值也是能够被读取的。
至于上面的例子中出现的调用:wait(&status),这是个典型的用于判断子进程是被 ptrace 停住仍是已经运行结束了的用法,变量 status 用于标记子进程是否已经结束退出,关于这个 wait() 和 WIFEXITED 的更多细节,读者能够自行查看一下manual(man 2).
读取寄存器的值
若是你想在系统调用开始前或结束后读取多个寄存器的值,上面的代码实现起来会比较麻烦,ptrace提供了另外一种方式来一次性读取全部的寄存器的内容,这就是参数:PTRACE_GETREGS的做用。参看下面的例子:
#include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <linux/user.h> #include <sys/syscall.h> int main() { pid_t child; long orig_eax, eax; long params[3]; int status; 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_eax = ptrace(PTRACE_PEEKUSER, child, 4 * ORIG_EAX, NULL); if(orig_eax == SYS_write) { if(insyscall == 0) { /* Syscall entry */ insyscall = 1; ptrace(PTRACE_GETREGS, child, NULL, ®s); printf("Write called with " "%ld, %ld, %ld\n", regs.ebx, regs.ecx, regs.edx); } else { /* Syscall exit */ eax = ptrace(PTRACE_PEEKUSER, child, 4 * EAX, NULL); printf("Write returned " "with %ld\n", eax); insyscall = 0; } } ptrace(PTRACE_SYSCALL, child, NULL, NULL); } } return 0; }
这个例子和前面一个例子几乎是如出一辙的,除了读取寄存器的地方换成了PTRACE_GETREGS.在这里咱们用到了user_regs_struct这个结构体,它被定义在<linux/user.h>中。
作点有趣的事情
好,有了前面的基础,如今咱们能够来尝试作些有趣的事情了。下面咱们将把子进程调用 write 时,传给 write() 的参数都给反转过来,看看会获得怎样的结果。
#include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <linux/user.h> #include <sys/syscall.h> const int long_size = sizeof(long); void reverse(char *str) { int i, j; char temp; for(i = 0, j = strlen(str) - 2; i <= j; ++i, --j) { temp = str[i]; str[i] = str[j]; str[j] = temp; } } void getdata(pid_t child, long addr, char *str, int len) { char *laddr; int i, j; union u { long val; char chars[long_size]; }data; i = 0; j = len / long_size; laddr = str; while(i < j) { data.val = ptrace(PTRACE_PEEKDATA, child, addr + i * 4, NULL); memcpy(laddr, data.chars, long_size); ++i; laddr += long_size; } j = len % long_size; if(j != 0) { data.val = ptrace(PTRACE_PEEKDATA, child, addr + i * 4, NULL); memcpy(laddr, data.chars, j); } str[len] = '\0'; } void putdata(pid_t child, long addr, char *str, int len) { char *laddr; int i, j; union u { long val; char chars[long_size]; }data; i = 0; j = len / long_size; laddr = str; while(i < j) { memcpy(data.chars, laddr, long_size); ptrace(PTRACE_POKEDATA, child, addr + i * 4, data.val); ++i; laddr += long_size; } j = len % long_size; if(j != 0) { memcpy(data.chars, laddr, j); ptrace(PTRACE_POKEDATA, child, addr + i * 4, data.val); } } int main() { pid_t child; child = fork(); if(child == 0) { ptrace(PTRACE_TRACEME, 0, NULL, NULL); execl("/bin/ls", "ls", NULL); } else { long orig_eax; long params[3]; int status; char *str, *laddr; int toggle = 0; while(1) { wait(&status); if(WIFEXITED(status)) break; orig_eax = ptrace(PTRACE_PEEKUSER, child, 4 * ORIG_EAX, NULL); if(orig_eax == SYS_write) { if(toggle == 0) { toggle = 1; params[0] = ptrace(PTRACE_PEEKUSER, child, 4 * EBX, NULL); params[1] = ptrace(PTRACE_PEEKUSER, child, 4 * ECX, NULL); params[2] = ptrace(PTRACE_PEEKUSER, child, 4 * EDX, NULL); str = (char *)calloc((params[2]+1) * sizeof(char)); getdata(child, params[1], str, params[2]); reverse(str); putdata(child, params[1], str, params[2]); } else { toggle = 0; } } ptrace(PTRACE_SYSCALL, child, NULL, NULL); } } return 0; }
上面的代码编译运行后,将获得这样的相似下面的结果:
ppadala@linux:~/ptrace > ls a.out dummy.s ptrace.txt libgpm.html registers.c syscallparams.c dummy ptrace.html simple.c ppadala@linux:~/ptrace > ./a.out txt.ecartp s.ymmud tuo.a c.sretsiger lmth.mpgbil c.llacys_egnahc c.elpmis lmth.ecartp ymmud
有趣吧!这个例子使用到了咱们前面提到过的全部概念。在这当中,咱们经过在 ptrace 中使用 PTRACE_POKEDATA 参数来改变子进程中的数据。这个 PTRACE_POKEDATA 用起来和 PTRACE_PEEKDATA 是同样的,不一样之处只在于 PTRACE_POKEDATA 不只能够读数据,还能往子进程里写数据。
单步执行
ptrace 提供了一种手段使得咱们能够像 debugger 同样单步执行子进程的代码,很酷?调用一下 ptrace(PTRACE_SINGLESTEP) 就能完成这样的事情,这个调用会告诉内核,在子进程每执行完一条子令以后,就停一下。
下面的代码演示了怎么读取子进程中当前正在被执行的子令,为了让读者更好的理解发生了什么事情,我本身写了一个很简单的dummy程序来方便你们理解。
下面是一小段汇编代码:
.data hello: .string "hello world\n" .globl main main: movl $4, %eax movl $2, %ebx movl $hello, %ecx movl $12, %edx int $0x80 movl $1, %eax xorl %ebx, %ebx int $0x80 ret
咱们用命令把它编译成可执行文件:
gcc -o dummy1 dummy1.s
而后咱们将单步执行这个程序:
#include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <linux/user.h> #include <sys/syscall.h> int main() { pid_t child; const int long_size = sizeof(long); child = fork(); if(child == 0) { ptrace(PTRACE_TRACEME, 0, NULL, NULL); execl("./dummy1", "dummy1", NULL); } else { int status; union u { long val; char chars[long_size]; }data; struct user_regs_struct regs; int start = 0; long ins; while(1) { wait(&status); if(WIFEXITED(status)) break; ptrace(PTRACE_GETREGS, child, NULL, ®s); if(start == 1) { ins = ptrace(PTRACE_PEEKTEXT, child, regs.eip, NULL); printf("EIP: %lx Instruction " "executed: %lx\n", regs.eip, ins); } if(regs.orig_eax == SYS_write) { start = 1; ptrace(PTRACE_SINGLESTEP, child, NULL, NULL); } else ptrace(PTRACE_SYSCALL, child, NULL, NULL); } } return 0; }
编译运行上面的代码,输出的结果是:
hello world EIP: 8049478 Instruction executed: 80cddb31 EIP: 804947c Instruction executed: c3
想要看明白这里作了什么事情,你可能须要先查一下 Intel 的手册,弄明白那些指令是干什么的。对程序执行更复杂的单步操做,如加入断点等,咱们还须要写一些更细致更复杂的代码,在下一篇文章中,咱们会展现一下怎么对程序加入断点。