[本文翻译自这里: http://www.linuxjournal.com/article/6100?page=0,0,作者:Pradeep Padaia]

你是否曾经想过怎样才能拦截系统调用?你是否曾经想过通过修改一下系统调用的参数来耍一把内核?你是否想过调试器是怎样把一个进程停下来,然后把控制权转移给你的?如果你以为这些都是通过复杂的内核编程来实现的,那你就错了,事实上,Linux 提供了一种很优雅的方式来实现上述所有行为:ptrace 系统调用。ptrace 提供了一种机制使得父进程可以观察和控制子进程的执行过程,ptrace 还可以检查和修改该子进程的可执行文件在内存中的镜像及该子进程所使用的寄存器中的值。这种用法通常来说,主要用于实现对进程插入断点和跟踪子进程的系统调用。

在本篇文章中,我们将学习怎么去拦截一个系统调用并且修改该系统调用的参数,在后续一篇文章中,我们将继续探讨 ptrace 的一些更深入的技术,如设置断点,在运行的子进程中插入代码等。我们将会查看进程的寄存器和数据段,并去修改其中的内容。我们还会介绍一种方式来在进程中插入代码,使得该进程能停下来,并执行我们插入的任意代码。

基础

操作系统通过一个叫做“系统调用”的标准机制来对上层提供服务,他们提供了一系列标准的API来让上层应用程序获取底层的硬件和服务,比如文件系统。当一个进程想要进行一个系统调用的时候,它会把该系统调用所需要用到的参数放到寄存器里,然后执行软中断指令0x80. 这个软中断就像是一个门,通过它就能进入内核模式,进入内核模式后,内核将会检查系统调用的参数,然后执行该系统调用。

在 i386 平台下(本文所有代码都基于 i386), 系统调用的编号会被放在寄存器 %eax 中,而系统调用的参数会被依次放到 %ebx,%ecx,%edx,%exi 和 %edi中,比如说,对于下面的系统调用:

  1. write(2, "Hello", 5)

编译后,它最后大概会被转化成下面这样子:

  1. movl $, %eax
  2. movl $, %ebx
  3. movl $hello,%ecx
  4. movl $, %edx
  5. int $0x80

其中 $hello 指向字符串 "Hello"。

看完上面简单的例子,现在我们来看看 ptrace 又是怎样执行的。首先,我们假设进程 A 要 ptrace 进程 B。在 ptrace 系统调用真正开始前,内核会检查一下我们将要 trace 的进程 B 是否当前已经正在被 traced 了,如果是,内核就会把该进程 B 停下来,并把控制权交给调用进程 A (任何时候,子进程只能被父进程这唯一一个进程所trace),这使得进程A有机会去检查和修改进程B的寄存器的值。

下面我们用一个例子来说明:

  1. #include <sys/ptrace.h>
  2. #include <sys/types.h>
  3. #include <sys/wait.h>
  4. #include <unistd.h>
  5. #include <linux/user.h> /* For constants
  6. ORIG_EAX etc */
  7. int main()
  8. { pid_t child;
  9. long orig_eax;
  10. child = fork();
  11. if(child == ) {
  12. ptrace(PTRACE_TRACEME, , NULL, NULL);
  13. execl("/bin/ls", "ls", NULL);
  14. }
  15. else {
  16. wait(NULL);
  17. orig_eax = ptrace(PTRACE_PEEKUSER,
  18. child, * ORIG_EAX,
  19. NULL);
  20. printf("The child made a "
  21. "system call %ld\n", orig_eax);
  22. ptrace(PTRACE_CONT, child, NULL, NULL);
  23. }
  24. return ;
  25. }

当把上面这段代码编译执行后,终端上除了命令 ls 的输出外,还会输出下面一行:

  1. The child made a system call

根据上面的输出,我们知道,在执行 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 个参数:

  1. long ptrace(enum __ptrace_request request,
  2. pid_t pid,
  3. void *addr,
  4. void *data);

其中第一个参数决定ptrace的行为也决定了接下来其它3个参数是怎样被使用的,第1个参数可以取以下任意一个值:

  1. PTRACE_TRACEME,
  2. PTRACE_PEEKTEXT,
  3. PTRACE_PEEKDATA,
  4. PTRACE_PEEKUSER,
  5. PTRACE_POKETEXT,
  6. PTRACE_POKEDATA,
  7. PTRACE_POKEUSER,
  8. PTRACE_GETREGS,
  9. PTRACE_GETFPREGS,
  10. PTRACE_SETREGS,
  11. PTRACE_SETFPREGS,
  12. PTRACE_CONT,
  13. PTRACE_SYSCALL,
  14. PTRACE_SINGLESTEP,
  15. PTRACE_DETACH

本文接下来会解释这些参数有什么不同的地方。

读取系统调用的参数

通过调用ptrace并传入PTRACE_PEEKUSER作为第一个参数,我们可以检查子进程中,保存了该进程的寄存器的内容(及其它一些内容)的用户态内存区域(USER area)。内核把寄存器的内容保存到这块区域,就是为了能够让父进程通过ptrace来读取,下面举一个例子来说明一下:

  1. #include <sys/ptrace.h>
  2. #include <sys/types.h>
  3. #include <sys/wait.h>
  4. #include <unistd.h>
  5. #include <linux/user.h>
  6. #include <sys/syscall.h> /* For SYS_write etc */
  7. int main()
  8. { pid_t child;
  9. long orig_eax, eax;
  10. long params[];
  11. int status;
  12. int insyscall = ;
  13. child = fork();
  14. if(child == ) {
  15. ptrace(PTRACE_TRACEME, , NULL, NULL);
  16. execl("/bin/ls", "ls", NULL);
  17. }
  18. else {
  19. while() {
  20. wait(&status);
  21. if(WIFEXITED(status))
  22. break;
  23. orig_eax = ptrace(PTRACE_PEEKUSER,
  24. child, * ORIG_EAX, NULL);
  25. if(orig_eax == SYS_write) {
  26. if(insyscall == ) {
  27. /* Syscall entry */
  28. insyscall = ;
  29. params[] = ptrace(PTRACE_PEEKUSER,
  30. child, * EBX,
  31. NULL);
  32. params[] = ptrace(PTRACE_PEEKUSER,
  33. child, * ECX,
  34. NULL);
  35. params[] = ptrace(PTRACE_PEEKUSER,
  36. child, * EDX,
  37. NULL);
  38. printf("Write called with "
  39. "%ld, %ld, %ld\n",
  40. params[], params[],
  41. params[]);
  42. }
  43. else { /* Syscall exit */
  44. eax = ptrace(PTRACE_PEEKUSER,
  45. child, * EAX, NULL);
  46. printf("Write returned "
  47. "with %ld\n", eax);
  48. insyscall = ;
  49. }
  50. }
  51. ptrace(PTRACE_SYSCALL,
  52. child, NULL, NULL);
  53. }
  54. }
  55. return ;
  56. }

编译执行上面的代码,得到的输出和前一个例子的输出有些类似:

  1. ppadala@linux:~/ptrace > ls
  2. a.out dummy.s ptrace.txt
  3. libgpm.html registers.c syscallparams.c
  4. dummy ptrace.html simple.c
  5. ppadala@linux:~/ptrace > ./a.out
  6. Write called with , ,
  7. a.out dummy.s ptrace.txt
  8. Write returned with
  9. Write called with , ,
  10. libgpm.html registers.c syscallparams.c
  11. Write returned with
  12. Write called with , ,
  13. dummy ptrace.html simple.c
  14. Write returned with

在这个例子中,我们追踪了 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的作用。参看下面的例子:

  1. #include <sys/ptrace.h>
  2. #include <sys/types.h>
  3. #include <sys/wait.h>
  4. #include <unistd.h>
  5. #include <linux/user.h>
  6. #include <sys/syscall.h>
  7. int main()
  8. { pid_t child;
  9. long orig_eax, eax;
  10. long params[];
  11. int status;
  12. int insyscall = ;
  13. struct user_regs_struct regs;
  14. child = fork();
  15. if(child == ) {
  16. ptrace(PTRACE_TRACEME, , NULL, NULL);
  17. execl("/bin/ls", "ls", NULL);
  18. }
  19. else {
  20. while() {
  21. wait(&status);
  22. if(WIFEXITED(status))
  23. break;
  24. orig_eax = ptrace(PTRACE_PEEKUSER,
  25. child, * ORIG_EAX,
  26. NULL);
  27. if(orig_eax == SYS_write) {
  28. if(insyscall == ) {
  29. /* Syscall entry */
  30. insyscall = ;
  31. ptrace(PTRACE_GETREGS, child,
  32. NULL, &regs);
  33. printf("Write called with "
  34. "%ld, %ld, %ld\n",
  35. regs.ebx, regs.ecx,
  36. regs.edx);
  37. }
  38. else { /* Syscall exit */
  39. eax = ptrace(PTRACE_PEEKUSER,
  40. child, * EAX,
  41. NULL);
  42. printf("Write returned "
  43. "with %ld\n", eax);
  44. insyscall = ;
  45. }
  46. }
  47. ptrace(PTRACE_SYSCALL, child,
  48. NULL, NULL);
  49. }
  50. }
  51. return ;
  52. }

这个例子和前面一个例子几乎是一模一样的,除了读取寄存器的地方换成了PTRACE_GETREGS.在这里我们用到了user_regs_struct这个结构体,它被定义在<linux/user.h>中。

做点有趣的事情

好,有了前面的基础,现在我们可以来尝试做些有趣的事情了。下面我们将把子进程调用 write 时,传给 write() 的参数都给反转过来,看看会得到怎样的结果。

  1. #include <sys/ptrace.h>
  2. #include <sys/types.h>
  3. #include <sys/wait.h>
  4. #include <unistd.h>
  5. #include <linux/user.h>
  6. #include <sys/syscall.h>
  7. const int long_size = sizeof(long);
  8. void reverse(char *str)
  9. { int i, j;
  10. char temp;
  11. for(i = , j = strlen(str) - ;
  12. i <= j; ++i, --j) {
  13. temp = str[i];
  14. str[i] = str[j];
  15. str[j] = temp;
  16. }
  17. }
  18. void getdata(pid_t child, long addr,
  19. char *str, int len)
  20. { char *laddr;
  21. int i, j;
  22. union u {
  23. long val;
  24. char chars[long_size];
  25. }data;
  26. i = ;
  27. j = len / long_size;
  28. laddr = str;
  29. while(i < j) {
  30. data.val = ptrace(PTRACE_PEEKDATA,
  31. child, addr + i * ,
  32. NULL);
  33. memcpy(laddr, data.chars, long_size);
  34. ++i;
  35. laddr += long_size;
  36. }
  37. j = len % long_size;
  38. if(j != ) {
  39. data.val = ptrace(PTRACE_PEEKDATA,
  40. child, addr + i * ,
  41. NULL);
  42. memcpy(laddr, data.chars, j);
  43. }
  44. str[len] = '\0';
  45. }
  46. void putdata(pid_t child, long addr,
  47. char *str, int len)
  48. { char *laddr;
  49. int i, j;
  50. union u {
  51. long val;
  52. char chars[long_size];
  53. }data;
  54. i = ;
  55. j = len / long_size;
  56. laddr = str;
  57. while(i < j) {
  58. memcpy(data.chars, laddr, long_size);
  59. ptrace(PTRACE_POKEDATA, child,
  60. addr + i * , data.val);
  61. ++i;
  62. laddr += long_size;
  63. }
  64. j = len % long_size;
  65. if(j != ) {
  66. memcpy(data.chars, laddr, j);
  67. ptrace(PTRACE_POKEDATA, child,
  68. addr + i * , data.val);
  69. }
  70. }
  71. int main()
  72. {
  73. pid_t child;
  74. child = fork();
  75. if(child == ) {
  76. ptrace(PTRACE_TRACEME, , NULL, NULL);
  77. execl("/bin/ls", "ls", NULL);
  78. }
  79. else {
  80. long orig_eax;
  81. long params[];
  82. int status;
  83. char *str, *laddr;
  84. int toggle = ;
  85. while() {
  86. wait(&status);
  87. if(WIFEXITED(status))
  88. break;
  89. orig_eax = ptrace(PTRACE_PEEKUSER,
  90. child, * ORIG_EAX,
  91. NULL);
  92. if(orig_eax == SYS_write) {
  93. if(toggle == ) {
  94. toggle = ;
  95. params[] = ptrace(PTRACE_PEEKUSER,
  96. child, * EBX,
  97. NULL);
  98. params[] = ptrace(PTRACE_PEEKUSER,
  99. child, * ECX,
  100. NULL);
  101. params[] = ptrace(PTRACE_PEEKUSER,
  102. child, * EDX,
  103. NULL);
  104. str = (char *)calloc((params[]+)
  105. * sizeof(char));
  106. getdata(child, params[], str,
  107. params[]);
  108. reverse(str);
  109. putdata(child, params[], str,
  110. params[]);
  111. }
  112. else {
  113. toggle = ;
  114. }
  115. }
  116. ptrace(PTRACE_SYSCALL, child, NULL, NULL);
  117. }
  118. }
  119. return ;
  120. }

上面的代码编译运行后,将得到这样的类似下面的结果:

  1. ppadala@linux:~/ptrace > ls
  2. a.out dummy.s ptrace.txt
  3. libgpm.html registers.c syscallparams.c
  4. dummy ptrace.html simple.c
  5. ppadala@linux:~/ptrace > ./a.out
  6. txt.ecartp s.ymmud tuo.a
  7. c.sretsiger lmth.mpgbil c.llacys_egnahc
  8. c.elpmis lmth.ecartp ymmud

有趣吧!这个例子使用到了我们前面提到过的所有概念。在这当中,我们通过在 ptrace 中使用 PTRACE_POKEDATA 参数来改变子进程中的数据。这个 PTRACE_POKEDATA 用起来和 PTRACE_PEEKDATA 是一样的,不同之处只在于 PTRACE_POKEDATA 不仅可以读数据,还能往子进程里写数据。

单步执行

ptrace 提供了一种手段使得我们可以像 debugger 一样单步执行子进程的代码,很酷?调用一下 ptrace(PTRACE_SINGLESTEP) 就能完成这样的事情,这个调用会告诉内核,在子进程每执行完一条子令之后,就停一下。

下面的代码演示了怎么读取子进程中当前正在被执行的子令,为了让读者更好的理解发生了什么事情,我自己写了一个很简单的dummy程序来方便大家理解。

下面是一小段汇编代码:

  1. .data
  2. hello:
  3. .string "hello world\n"
  4. .globl main
  5. main:
  6. movl $, %eax
  7. movl $, %ebx
  8. movl $hello, %ecx
  9. movl $, %edx
  10. int $0x80
  11. movl $, %eax
  12. xorl %ebx, %ebx
  13. int $0x80
  14. ret

我们用命令把它编译成可执行文件:

  1. gcc -o dummy1 dummy1.s

然后我们将单步执行这个程序:

  1. #include <sys/ptrace.h>
  2. #include <sys/types.h>
  3. #include <sys/wait.h>
  4. #include <unistd.h>
  5. #include <linux/user.h>
  6. #include <sys/syscall.h>
  7. int main()
  8. { pid_t child;
  9. const int long_size = sizeof(long);
  10. child = fork();
  11. if(child == ) {
  12. ptrace(PTRACE_TRACEME, , NULL, NULL);
  13. execl("./dummy1", "dummy1", NULL);
  14. }
  15. else {
  16. int status;
  17. union u {
  18. long val;
  19. char chars[long_size];
  20. }data;
  21. struct user_regs_struct regs;
  22. int start = ;
  23. long ins;
  24. while() {
  25. wait(&status);
  26. if(WIFEXITED(status))
  27. break;
  28. ptrace(PTRACE_GETREGS,
  29. child, NULL, &regs);
  30. if(start == ) {
  31. ins = ptrace(PTRACE_PEEKTEXT,
  32. child, regs.eip,
  33. NULL);
  34. printf("EIP: %lx Instruction "
  35. "executed: %lx\n",
  36. regs.eip, ins);
  37. }
  38. if(regs.orig_eax == SYS_write) {
  39. start = ;
  40. ptrace(PTRACE_SINGLESTEP, child,
  41. NULL, NULL);
  42. }
  43. else
  44. ptrace(PTRACE_SYSCALL, child,
  45. NULL, NULL);
  46. }
  47. }
  48. return ;
  49. }

编译运行上面的代码,输出的结果是:

  1. hello world
  2. EIP: Instruction executed: 80cddb31
  3. EIP: 804947c Instruction executed: c3

想要看明白这里做了什么事情,你可能需要先查一下 Intel 的手册,弄明白那些指令是干什么的。对程序执行更复杂的单步操作,如加入断点等,我们还需要写一些更细致更复杂的代码,在下一篇文章中,我们会展示一下怎么对程序加入断点。

[译] 玩转ptrace (一)的更多相关文章

  1. linux下 玩转ptrace

    译者序:在开发Hust Online Judge的过程中,查阅了不少资料,关于调试器技术的资料在网上是很少,即便是UNIX编程巨著<UNIX环境高级编程>中,相关内容也不多,直到我在 ht ...

  2. 玩转ptrace(转)

    下面是转帖的内容,写的很详细.但是不同的linux发行版中头文件的路径和名称并不相同.如在某些发行版中<linux/user.h>就不存在,其中定义的变量出现在<asm/ptrace ...

  3. 玩转ptrace (一)

    转自http://www.cnblogs.com/catch/p/3476280.html [本文翻译自这里: http://www.linuxjournal.com/article/6100?pag ...

  4. linux ptrace学习

    ptrace提供了一种使父进程得以监视和控制其它进程的方式,它还能够改变子进程中的寄存器和内核映像,因而可以实现断点调试和系统调用的跟踪.学习linux的ptrace是为学习android adbi框 ...

  5. 安卓动态调试七种武器之离别钩 – Hooking(上)

    安卓动态调试七种武器之离别钩 – Hooking(上) 作者:蒸米@阿里聚安全 0x00 序 随着移动安全越来越火,各种调试工具也都层出不穷,但因为环境和需求的不同,并没有工具是万能的.另外工具是死的 ...

  6. 每日一译系列-模块化css怎么玩(译文)

    原文链接:How Css Modules Work 原文作者是Preact的作者 这是一篇关于如何使用Css Modules的快速介绍,使用到的工具是Webpack吊炸的css-loader 首先,我 ...

  7. [译]SSAS下玩转PowerShell(三)

    在第一篇中简单介绍了PowerShell,包含基本的一些命令,以及如何打开PowerShell,并且导航到SSAS对象.第二篇中学习了如何使用变量根据当前日期创建SSAS备份,以及如何运行MDX和XM ...

  8. [译]SSAS下玩转PowerShell(二)

    上一篇中简单的介绍了SSAS下的PowerShell,这一篇会演示更多的操作,比如根据当前时间创建备份,使用变量去指定处理哪一个分区,以及用XMLA脚本去创建分区,和在PowerShell中调用Pow ...

  9. [译]SSAS下玩转PowerShell

    操作SSAS数据库的方法有很多,是否有一种可以方法可以通过脚本自动去做这些事呢,比如处理分区,创建备份以及监视SSAS的运行状况. 原文地址: http://www.mssqltips.com/sql ...

随机推荐

  1. [C#.Net]KeyDown(KeyUp)和KeyPress的区别

    在keyDown事件里使用keyValue:在keyPress事件里使用keyChar. keyValue转换keyChar:(char)keyValue 验证只有数字和backSpace e.han ...

  2. 网上流行的linux内核漫画

  3. 买铅笔(NOIP2016)

    先给题目链接:买铅笔 这题非常水,没啥可分析的,先给代码: #include<bits/stdc++.h> //1 using namespace std; int main(){ int ...

  4. Java语法基础课 原码 反码 补码

    原码就是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示值. 反码的表示方法是:正数的反码是其本身:负数的反码是在其原码的基础上, 符号位不变,其余各个位取反. 补码的表示方法是在反码的基础 ...

  5. 2018.11.24 poj1743Musical Theme(二分答案+后缀数组)

    传送门 代码: 二分答案. 然后对于预处理的heightheightheight数组分成几段. 保证每一段中都是连续的几个heightheightheight并且这些heightheightheigh ...

  6. D. Three Pieces(dp,array用法,棋盘模型)

    https://codeforces.com/contest/1065/problem/D 题意 给你一个方阵,里面的数字从1~nn,你需要从标号为1的格子依次走到标号为nn,在每一个格子你有两个决策 ...

  7. s4-2 ALOHA 协议

    多路访问协议  随机访问协议(Random Access) 特点:站点争用信道,可能出现站点之间的冲突 典型的随机访问协议 • ALOHA协议 • CSMA协议 • CSMA/CD协议(以太网采 ...

  8. BZOJ 4407 于神之怒加强版 (莫比乌斯反演 + 分块)

    4407: 于神之怒加强版 Time Limit: 80 Sec  Memory Limit: 512 MBSubmit: 1067  Solved: 494[Submit][Status][Disc ...

  9. Redis和RabbitMQ在项目中的使用

    一:Redis的使用 1.先引入pom.xml的依赖 <dependency> <groupId>redis.clients</groupId> <artif ...

  10. 1, 2, and 4 symbols per clock中数据排列

    图片来自High-De€nitionMultimedia Interface (HDMI) IP Core User Guide 在自己处理的过程中很多细节的东西必须要清楚. 今天想自己从RGB数据中 ...