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

==================================================================================================

by Pradeep Padala

Created 2002-11-01 02:00

翻译: Magic.D E-mail: adamgic@163.com

译者序:在开发Hust Online Judge的过程中,查阅了不少资料,关于调试器技术的资料在网上是很少,即便是UNIX编程巨著《UNIX环境高级编程》中,相关内容也不多,直到我在http://www.linuxjournal.com上找到这篇文章,如获至宝,特翻译之,作为鄙人翻译技术文档的第一次尝试,必定会有不少蹩脚之处,各位就将就一下吧,欢迎大力拍砖。

你想过怎么实现对系统调用的拦截吗?你尝试过通过改变系统调用的参数来愚弄你的系统kernel吗?你想过调试器是如何使运行中的进程暂停并且控制它吗?

你可能会开始考虑怎么使用复杂的kernel编程来达到目的,那么,你错了。实际上Linux提供了一种优雅的机制来完成这些:ptrace系统函数。 ptrace提供了一种使父进程得以监视和控制其它进程的方式,它还能够改变子进程中的寄存器和内核映像,因而可以实现断点调试和系统调用的跟踪。

使用ptrace,你可以在用户层拦截和修改系统调用(sys call)

在这篇文章中,我们将学习如何拦截一个系统调用,然后修改它的参数。在本文的第二部分我们将学习更先进的技术:设置断点,插入代码到一个正在运行的程序中;我们将潜入到机器内部,偷窥和纂改进程的寄存器和数据段。

基本知识

操作系统提供了一种标准的服务来让程序员实现对底层硬件和服务的控制(比如文件系统),叫做系统调用(system calls)。当一个程序需要作系统调用的时候,它将相关参数放进系统调用相关的寄存器,然后调用软中断0x80,这个中断就像一个让程序得以接触到内核模式的窗口,程序将参数和系统调用号交给内核,内核来完成系统调用的执行。

在i386体系中(本文中所有的代码都是面向i386体系),系统调用号将放入%eax,它的参数则依次放入%ebx, %ecx, %edx, %esi 和 %edi。 比如,在以下的调用

Write(2, “Hello”, 5)的汇编形式大概是这样的

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

这里的$hello指向的是标准字符串”Hello”。

那么,ptrace会在什么时候出现呢?在执行系统调用之前,内核会先检查当前进程是否处于被“跟踪”(traced)的状态。如果是的话,内核暂停当前进程并将控制权交给跟踪进程,使跟踪进程得以察看或者修改被跟踪进程的寄存器。

让我们来看一个例子,演示这个跟踪程序的过程

  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. {
  9. pid_t child;
  10. long orig_eax;
  11. child = fork();
  12. if(child == 0) {
  13. ptrace(PTRACE_TRACEME, 0, NULL, NULL);
  14. execl("/bin/ls", "ls", NULL);
  15. }
  16. else {
  17. wait(NULL);
  18. orig_eax = ptrace(PTRACE_PEEKUSER,
  19. child, 4 * ORIG_EAX,
  20. NULL);
  21. printf("The child made a "
  22. "system call %ld ", orig_eax);
  23. ptrace(PTRACE_CONT, child, NULL, NULL);
  24. }
  25. return 0;
  26. }

运行这个程序,将会在输出ls命令的结果的同时,输出:

The child made a system call 11

说明:11是execve的系统调用号,这是该程序调用的第一个系统调用。想知道系统调用号的详细内容,察看 /usr/include/asm/unistd.h。

在以上的示例中,父进程fork出了一个子进程,然后跟踪它。在调用exec函数之前,子进程用PTRACE_TRACEME作为第一个参数调用了ptrace函数,它告诉内核:让别人跟踪我吧!然后,在子进程调用了execve()之后,它将控制权交还给父进程。当时父进程正使用wait()函数来等待来自内核的通知,现在它得到了通知,于是它可以开始察看子进程都作了些什么,比如看看寄存器的值之类。

出现系统调用之后,内核会将eax中的值(此时存的是系统调用号)保存起来,我们可以使用PTRACE_PEEKUSER作为ptrace的第一个参数来读到这个值。

我们察看完系统调用的信息后,可以使用PTRACE_CONT作为ptrace的第一个参数,调用ptrace使子进程继续系统调用的过程。

ptrace函数的参数

Ptrace有四个参数

long ptrace(enum __ptrace_request request,pid_t pid,void *addr,void *data);

第一个参数决定了ptrace的行为与其它参数的使用方法,可取的值有:

PTRACE_ME

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

这个程序的输出是这样的

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命令的执行将产生三个write系统调用。使用PTRACE_SYSCALL作为ptrace的第一个参数,使内核在子进程做出系统调用或者准备退出的时候暂停它。这种行为与使用PTRACE_CONT,然后在下一个系统调用/进程退出时暂停它是等价的。

在前一个例子中,我们用PTRACE_PEEKUSER来察看write系统调用的参数。系统调用的返回值会被放入%eax。

wait函数使用status变量来检查子进程是否已退出。它是用来判断子进程是被ptrace暂停掉还是已经运行结束并退出。有一组宏可以通过status的值来判断进程的状态,比如WIFEXITED等,详情可以察看wait(2) man。

如果你想在系统调用或者进程终止的时候读取它的寄存器,使用前面那个例子的方法是可以的,但是这是笨拙的方法。使用PRACE_GETREGS作为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>
  7. int main()
  8. {
  9. pid_t child;
  10. long orig_eax, eax;
  11. long params[3];
  12. int status;
  13. int insyscall = 0;
  14. struct user_regs_struct regs;
  15. child = fork();
  16. if(child == 0) {
  17. ptrace(PTRACE_TRACEME, 0, NULL, NULL);
  18. execl("/bin/ls", "ls", NULL);
  19. }
  20. else {
  21. while(1) {
  22. wait(&status);
  23. if(WIFEXITED(status))
  24. break;
  25. orig_eax = ptrace(PTRACE_PEEKUSER,
  26. child, 4 * ORIG_EAX,
  27. NULL);
  28. if(orig_eax == SYS_write) {
  29. if(insyscall == 0) {
  30. /* Syscall entry */
  31. insyscall = 1;
  32. ptrace(PTRACE_GETREGS, child,
  33. NULL, &regs);
  34. printf("Write called with "
  35. "%ld, %ld, %ld ",
  36. regs.ebx, regs.ecx,
  37. regs.edx);
  38. }
  39. else { /* Syscall exit */
  40. eax = ptrace(PTRACE_PEEKUSER,
  41. child, 4 * EAX,
  42. NULL);
  43. printf("Write returned "
  44. "with %ld ", eax);
  45. insyscall = 0;
  46. }
  47. }
  48. ptrace(PTRACE_SYSCALL, child,
  49. NULL, NULL);
  50. }
  51. }
  52. return 0;
  53. }

这段代码与前面的例子是比较相似的,不同的是它使用了PTRACE_GETREGS。 其中的user_regs_struct结构是在<linux/user.h>中定义的。

来点好玩的

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

输出是这样的:

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_POKEDATA作为第一个参数,以此来改变子进程中的变量值。它以与PTRACE_PEEKDATA相似的方式工作,当然,它不只是偷窥变量的值了,它可以修改它们。

单步

ptrace提供了对子进程进行单步的功能。 ptrace(PTRACE_SINGLESTEP, …) 会使内核在子进程的每一条指令执行前先将其阻塞,然后将控制权交给父进程。下面的例子可以查出子进程当前将要执行的指令。为了便于理解,我用汇编写了这个受控程序,而不是让你为c的库函数到底会作那些系统调用而头痛。

以下是被控程序的代码 dummy1.s,使用gcc  –o dummy1 dummy1.s来编译

.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

以下的程序则用来完成单步:

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

程序的输出是这样的:

你可能需要察看Intel的用户手册来了解这些指令代码的意思。

更复杂的单步,比如设置断点,则需要很仔细的设计和更复杂的代码才可以实现。

在第二部分,我们将会看到如何在程序中加入断点,以及将代码插入到已经在运行的程序中。

在第一部分中我们已经看到ptrace怎么获取子进程的系统调用以及改变系统调用的参数。在这篇文章中,我们将要研究如何在子进程中设置断点和往运行中的程序里插入代码。实际上调试器就是用这种方法来设置断点和执行调试句柄。与前面一样,这里的所有代码都是针对i386平台的。

附着在进程上

在第一部分钟,我们使用ptrace(PTRACE_TRACEME, …)来跟踪一个子进程,如果你只是想要看进程是怎么进行系统调用和跟踪程序的,这个做法是不错的。但如果你要对运行中的进程进行调试,则需要使用 ptrace(PTRACE_ATTACH, ….)

当 ptrace( PTRACE_ATTACH, …)在被调用的时候传入了子进程的pid时, 它大体是与ptrace(PTRACE_TRACEME, …)的行为相同的,它会向子进程发送SIGSTOP信号,于是我们可以察看和修改子进程,然后使用 ptrace( PTRACE_DETACH, …)来使子进程继续运行下去。

下面是调试程序的一个简单例子

  1. int main()
  2. {
  3. int i;
  4. for(i = 0;i < 10; ++i) {
  5. printf("My counter: %d ", i);
  6. sleep(2);
  7. }
  8. return 0;
  9. }

将上面的代码保存为dummy2.c。按下面的方法编译运行:

gcc -o dummy2 dummy2.c
./dummy2 &

现在我们可以用下面的代码来附着到dummy2上。

  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 user_regs_struct
  6. etc. */
  7. int main(int argc, char *argv[])
  8. {
  9. pid_t traced_process;
  10. struct user_regs_struct regs;
  11. long ins;
  12. if(argc != 2) {
  13. printf("Usage: %s <pid to be traced> ",
  14. argv[0], argv[1]);
  15. exit(1);
  16. }
  17. traced_process = atoi(argv[1]);
  18. ptrace(PTRACE_ATTACH, traced_process,
  19. NULL, NULL);
  20. wait(NULL);
  21. ptrace(PTRACE_GETREGS, traced_process,
  22. NULL, &regs);
  23. ins = ptrace(PTRACE_PEEKTEXT, traced_process,
  24. regs.eip, NULL);
  25. printf("EIP: %lx Instruction executed: %lx ",
  26. regs.eip, ins);
  27. ptrace(PTRACE_DETACH, traced_process,
  28. NULL, NULL);
  29. return 0;
  30. }

上面的程序仅仅是附着在子进程上,等待它结束,并测量它的eip( 指令指针)然后释放子进程。

调试器是怎么设置断点的呢?通常是将当前将要执行的指令替换成trap指令,于是被调试的程序就会在这里停滞,这时调试器就可以察看被调试程序的信息了。被调试程序恢复运行以后调试器会把原指令再放回来。这里是一个例子:

  1. <span style="font-weight: normal;">#include <sys/ptrace.h>
  2. #include <sys/types.h>
  3. #include <sys/wait.h>
  4. #include <unistd.h>
  5. #include <linux/user.h>
  6. const int long_size = sizeof(long);
  7. void getdata(pid_t child, long addr,
  8. char *str, int len)
  9. {
  10. char *laddr;
  11. int i, j;
  12. union u {
  13. long val;
  14. char chars[long_size];
  15. }data;
  16. i = 0;
  17. j = len / long_size;
  18. laddr = str;
  19. while(i < j) {
  20. data.val = ptrace(PTRACE_PEEKDATA, child,
  21. addr + i * 4, NULL);
  22. memcpy(laddr, data.chars, long_size);
  23. ++i;
  24. laddr += long_size;
  25. }
  26. j = len % long_size;
  27. if(j != 0) {
  28. data.val = ptrace(PTRACE_PEEKDATA, child,
  29. addr + i * 4, NULL);
  30. memcpy(laddr, data.chars, j);
  31. }
  32. str[len] = '';
  33. }
  34. void putdata(pid_t child, long addr,
  35. char *str, int len)
  36. {
  37. char *laddr;
  38. int i, j;
  39. union u {
  40. long val;
  41. char chars[long_size];
  42. }data;
  43. i = 0;
  44. j = len / long_size;
  45. laddr = str;
  46. while(i < j) {
  47. memcpy(data.chars, laddr, long_size);
  48. ptrace(PTRACE_POKEDATA, child,
  49. addr + i * 4, data.val);
  50. ++i;
  51. laddr += long_size;
  52. }
  53. j = len % long_size;
  54. if(j != 0) {
  55. memcpy(data.chars, laddr, j);
  56. ptrace(PTRACE_POKEDATA, child,
  57. addr + i * 4, data.val);
  58. }
  59. }
  60. int main(int argc, char *argv[])
  61. {
  62. pid_t traced_process;
  63. struct user_regs_struct regs, newregs;
  64. long ins;
  65. /* int 0x80, int3 */
  66. char code[] = {0xcd,0x80,0xcc,0};
  67. char backup[4];
  68. if(argc != 2) {
  69. printf("Usage: %s <pid to be traced> ",
  70. argv[0], argv[1]);
  71. exit(1);
  72. }
  73. traced_process = atoi(argv[1]);
  74. ptrace(PTRACE_ATTACH, traced_process,
  75. NULL, NULL);
  76. wait(NULL);
  77. ptrace(PTRACE_GETREGS, traced_process,
  78. NULL, &regs);
  79. /* Copy instructions into a backup variable */
  80. getdata(traced_process, regs.eip, backup, 3);
  81. /* Put the breakpoint */
  82. putdata(traced_process, regs.eip, code, 3);
  83. /* Let the process continue and execute
  84. the int 3 instruction */
  85. ptrace(PTRACE_CONT, traced_process, NULL, NULL);
  86. wait(NULL);
  87. printf("The process stopped, putting back "
  88. "the original instructions ");
  89. printf("Press <enter> to continue ");
  90. getchar();
  91. putdata(traced_process, regs.eip, backup, 3);
  92. /* Setting the eip back to the original
  93. instruction to let the process continue */
  94. ptrace(PTRACE_SETREGS, traced_process,
  95. NULL, &regs);
  96. ptrace(PTRACE_DETACH, traced_process,
  97. NULL, NULL);
  98. return 0;
  99. }</span>

上面的程序将把三个byte的内容进行替换以执行trap指令,等被调试进程停滞以后,我们把原指令再替换回来并把eip修改为原来的值。下面的图中演示了指令的执行过程

1. 进程停滞后

2. 替换入trap指令

3.断点成功,控制权交给了调试器

4. 继续运行,将原指令替换回来并将eip复原

在了解了断点的机制以后,往运行中的程序里面添加指令也不再是难事了,下面的代码会使原程序多出一个”hello world”的输出

这时一个简单的”hello world”程序,当然为了我们的特殊需要作了点修改:

  1. void main()
  2. {
  3. __asm__("
  4. jmp forward
  5. backward:
  6. popl   %esi      # Get the address of
  7. # hello world string
  8. movl   $4, %eax  # Do write system call
  9. movl   $2, %ebx
  10. movl   %esi, %ecx
  11. movl   $12, %edx
  12. int    $0x80
  13. int3             # Breakpoint. Here the
  14. # program will stop and
  15. # give control back to
  16. # the parent
  17. forward:
  18. call   backward
  19. .string "Hello World/n""
  20. );
  21. }

使用 gcc –o hello hello.c来编译它。

在backward和forward之间的跳转是为了使程序能够找到”hello world” 字符串的地址。

使用GDB我们可以得到上面那段程序的机器码。启动GDB,然后对程序进行反汇编:

(gdb) disassemble main
Dump of assembler code for function main:
0x80483e0 <main>:       push   %ebp
0x80483e1 <main+1>:     mov    %esp,%ebp
0x80483e3 <main+3>:     jmp    0x80483fa <forward>
End of assembler dump.
(gdb) disassemble forward
Dump of assembler code for function forward:
0x80483fa <forward>:    call   0x80483e5 <backward>
0x80483ff <forward+5>:  dec    %eax
0x8048400 <forward+6>:  gs
0x8048401 <forward+7>:  insb   (%dx),%es:(%edi)
0x8048402 <forward+8>:  insb   (%dx),%es:(%edi)
0x8048403 <forward+9>:  outsl  %ds:(%esi),(%dx)
0x8048404 <forward+10>: and    %dl,0x6f(%edi)
0x8048407 <forward+13>: jb     0x8048475
0x8048409 <forward+15>: or     %fs:(%eax),%al
0x804840c <forward+18>: mov    %ebp,%esp
0x804840e <forward+20>: pop    %ebp
0x804840f <forward+21>: ret
End of assembler dump.
(gdb) disassemble backward
Dump of assembler code for function backward:
0x80483e5 <backward>:   pop    %esi
0x80483e6 <backward+1>: mov    $0x4,%eax
0x80483eb <backward+6>: mov    $0x2,%ebx
0x80483f0 <backward+11>:        mov    %esi,%ecx
0x80483f2 <backward+13>:        mov    $0xc,%edx
0x80483f7 <backward+18>:        int    $0x80
0x80483f9 <backward+20>:        int3
End of assembler dump.

我们需要使用从man+3到backward+20之间的字节码,总共41字节。使用GDB中的x命令来察看机器码。

(gdb) x/40bx main+3

<main+3>: eb 15 5e b8 04 00 00 00
<backward+6>: bb 02 00 00 00 89 f1 ba
<backward+14>: 0c 00 00 00 cd 80 cc
<forward+1>: e6 ff ff ff 48 65 6c 6c
<forward+9>: 6f 20 57 6f 72 6c 64 0a

已经有了我们想要执行的指令,还等什么呢?只管把它们根前面那个例子一样插入到被调试程序中去!

代码:

  1. int main(int argc, char *argv[])
  2. {
  3. pid_t traced_process;
  4. struct user_regs_struct regs, newregs;
  5. long ins;
  6. int len = 41;
  7. char insertcode[] =
  8. "/xeb/x15/x5e/xb8/x04/x00"
  9. "/x00/x00/xbb/x02/x00/x00/x00/x89/xf1/xba"
  10. "/x0c/x00/x00/x00/xcd/x80/xcc/xe8/xe6/xff"
  11. "/xff/xff/x48/x65/x6c/x6c/x6f/x20/x57/x6f"
  12. "/x72/x6c/x64/x0a/x00";
  13. char backup[len];
  14. if(argc != 2) {
  15. printf("Usage: %s <pid to be traced> ",
  16. argv[0], argv[1]);
  17. exit(1);
  18. }
  19. traced_process = atoi(argv[1]);
  20. ptrace(PTRACE_ATTACH, traced_process,
  21. NULL, NULL);
  22. wait(NULL);
  23. ptrace(PTRACE_GETREGS, traced_process,
  24. NULL, &regs);
  25. getdata(traced_process, regs.eip, backup, len);
  26. putdata(traced_process, regs.eip,
  27. insertcode, len);
  28. ptrace(PTRACE_SETREGS, traced_process,
  29. NULL, &regs);
  30. ptrace(PTRACE_CONT, traced_process,
  31. NULL, NULL);
  32. wait(NULL);
  33. printf("The process stopped, Putting back "
  34. "the original instructions ");
  35. putdata(traced_process, regs.eip, backup, len);
  36. ptrace(PTRACE_SETREGS, traced_process,
  37. NULL, &regs);
  38. printf("Letting it continue with "
  39. "original flow ");
  40. ptrace(PTRACE_DETACH, traced_process,
  41. NULL, NULL);
  42. return 0;
  43. }

在前面的例子中我们将代码直接插入到了正在执行的指令流中,然而,调试器可能会被这种行为弄糊涂,所以我们决定把指令插入到进程中的自由空间中去。通过察看/proc/pid/maps可以知道这个进程中自由空间的分布。接下来这个函数可以找到这个内存映射的起始点:

  1. <span style="font-weight: normal;">long freespaceaddr(pid_t pid)
  2. {
  3. FILE *fp;
  4. char filename[30];
  5. char line[85];
  6. long addr;
  7. char str[20];
  8. sprintf(filename, "/proc/%d/maps", pid);
  9. fp = fopen(filename, "r");
  10. if(fp == NULL)
  11. exit(1);
  12. while(fgets(line, 85, fp) != NULL) {
  13. sscanf(line, "%lx-%*lx %*s %*s %s", &addr,
  14. str, str, str, str);
  15. if(strcmp(str, "00:00") == 0)
  16. break;
  17. }
  18. fclose(fp);
  19. return addr;
  20. }</span>

在/proc/pid/maps中的每一行都对应了进程中一段内存区域。主函数的代码如下:

  1. int main(int argc, char *argv[])
  2. {
  3. pid_t traced_process;
  4. struct user_regs_struct oldregs, regs;
  5. long ins;
  6. int len = 41;
  7. char insertcode[] =
  8. "/xeb/x15/x5e/xb8/x04/x00"
  9. "/x00/x00/xbb/x02/x00/x00/x00/x89/xf1/xba"
  10. "/x0c/x00/x00/x00/xcd/x80/xcc/xe8/xe6/xff"
  11. "/xff/xff/x48/x65/x6c/x6c/x6f/x20/x57/x6f"
  12. "/x72/x6c/x64/x0a/x00";
  13. char backup[len];
  14. long addr;
  15. if(argc != 2) {
  16. printf("Usage: %s <pid to be traced> ",
  17. argv[0], argv[1]);
  18. exit(1);
  19. }
  20. traced_process = atoi(argv[1]);
  21. ptrace(PTRACE_ATTACH, traced_process,
  22. NULL, NULL);
  23. wait(NULL);
  24. ptrace(PTRACE_GETREGS, traced_process,
  25. NULL, &regs);
  26. addr = freespaceaddr(traced_process);
  27. getdata(traced_process, addr, backup, len);
  28. putdata(traced_process, addr, insertcode, len);
  29. memcpy(&oldregs, &regs, sizeof(regs));
  30. regs.eip = addr;
  31. ptrace(PTRACE_SETREGS, traced_process,
  32. NULL, &regs);
  33. ptrace(PTRACE_CONT, traced_process,
  34. NULL, NULL);
  35. wait(NULL);
  36. printf("The process stopped, Putting back "
  37. "the original instructions ");
  38. putdata(traced_process, addr, backup, len);
  39. ptrace(PTRACE_SETREGS, traced_process,
  40. NULL, &oldregs);
  41. printf("Letting it continue with "
  42. "original flow ");
  43. ptrace(PTRACE_DETACH, traced_process,
  44. NULL, NULL);
  45. return 0;
  46. }

那么,在使用ptrace的时候,内核里发生了声么呢?这里有一段简要的说明:

当一个进程调用了 ptrace( PTRACE_TRACEME, …)之后,内核为该进程设置了一个标记,注明该进程将被跟踪。内核中的相关原代码如下:

Source: arch/i386/kernel/ptrace.c
if (request == PTRACE_TRACEME) {
    /* are we already being traced? */
    if (current->ptrace & PT_PTRACED)
        goto out;
    /* set the ptrace bit in the process flags. */
    current->ptrace |= PT_PTRACED;
    ret = 0;
    goto out;
}

一次系统调用完成之后,内核察看那个标记,然后执行trace系统调用(如果这个进程正处于被跟踪状态的话)。其汇编的细节可以在 arh/i386/kernel/entry.S中找到。

现在让我们来看看这个sys_trace()函数(位于 arch/i386/kernel/ptrace.c )。它停止子进程,然后发送一个信号给父进程,告诉它子进程已经停滞,这个信号会激活正处于等待状态的父进程,让父进程进行相关处理。父进程在完成相关操作以后就调用ptrace( PTRACE_CONT, …)或者 ptrace( PTRACE_SYSCALL, …), 这将唤醒子进程,内核此时所作的是调用一个叫wake_up_process() 的进程调度函数。其他的一些系统架构可能会通过发送SIGCHLD给子进程来达到这个目的。

小结:

ptrace函数可能会让人们觉得很奇特,因为它居然可以检测和修改一个运行中的程序。这种技术主要是在调试器和系统调用跟踪程序中使用。它使程序员可以在用户级别做更多有意思的事情。已经有过很多在用户级别下扩展操作系统得尝试,比如UFO,一个用户级别的文件系统扩展,它使用ptrace来实现一些安全机制。

作者:

Pradeep Padala,

p_padala@yahoo.com

http://www.cise.ufl.edu/~ppadala

玩转ptrace(转)的更多相关文章

  1. linux下 玩转ptrace

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

  2. [译] 玩转ptrace (一)

    [本文翻译自这里: http://www.linuxjournal.com/article/6100?page=0,0,作者:Pradeep Padaia] 你是否曾经想过怎样才能拦截系统调用?你是否 ...

  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. 玩转spring boot——快速开始

    开发环境: IED环境:Eclipse JDK版本:1.8 maven版本:3.3.9 一.创建一个spring boot的mcv web应用程序 打开Eclipse,新建Maven项目 选择quic ...

  7. [C#] 软硬结合第二篇——酷我音乐盒的逆天玩法

    1.灵感来源: LZ是纯宅男,一天从早上8:00起一直要呆在电脑旁到晚上12:00左右吧~平时也没人来闲聊几句,刷空间暑假也没啥动态,听音乐吧...~有些确实不好听,于是就不得不打断手头的工作去点击下 ...

  8. [C#] 了解过入口函数 Main() 吗?带你用批处理玩转 Main 函数

    了解过入口函数 Main() 吗?带你用批处理玩转 Main 函数 目录 简介 特点 方法的参数 方法的返回值 与批处理交互的一个示例 简介 我们知道,新建一个控制台应用程序的时候,IDE 会同时创建 ...

  9. 玩转spring boot——开篇

    很久没写博客了,而这一转眼就是7年.这段时间并不是我没学习东西,而是园友们的技术提高的非常快,这反而让我不知道该写些什么.我做程序已经有十几年之久了,可以说是彻彻底底的“程序老炮”,至于技术怎么样?我 ...

随机推荐

  1. php银行卡校验

    前言银行金卡,维萨和万事达.银联品牌,如果是贷记卡或准贷记卡,一定为16位卡号.而借记卡可以16-19位不等.美国运通卡则不论金卡或是白金卡.普通卡,都是15位卡号.16-19 位卡号校验位采用 Lu ...

  2. Spring Mvc + Maven + yuicompressor 使用 profile 来压缩 javascript ,css 文件; (十)

    profile相关知识点: 在开发项目时,设想有以下场景: 你的Maven项目存放在一个远程代码库中(比如github),该项目需要访问数据库,你有两台电脑,一台是Linux,一台是Mac OS X, ...

  3. 20155227 2016-2017-2 《Java程序设计》第八周学习总结

    20155227 2016-2017-2 <Java程序设计>第八周学习总结 教材学习内容总结 NIO与NIO2 NIO即New IO.java从JDK1.4开始提供了NIO,在JAVA ...

  4. 仿QQ聊天图文混排流程图【适用于XMPP】

      图文混排流程图.graffle4.8 KB   下面附上图片素材: 表情.zip692.5 KB     下面是字符串与图片的详细对应关系:                 "[呲牙]& ...

  5. B - C Looooops POJ - 2115 (扩展欧几里得)

    题目链接:https://cn.vjudge.net/contest/276376#problem/B 题目大意:for( int  i= A ; i != B; i+ = c ),然后给你A,B,C ...

  6. AF_INET域与AF_UNIX域socket通信原理对比

    原文 1.  AF_INET域socket通信过程 典型的TCP/IP四层模型的通信过程. 发送方.接收方依赖IP:Port来标识,即将本地的socket绑定到对应的IP端口上,发送数据时,指定对方的 ...

  7. Linux信号(signal)机制【转】

    转自:http://gityuan.com/2015/12/20/signal/ 信号(signal)是一种软中断,信号机制是进程间通信的一种方式,采用异步通信方式 一.信号类型 Linux系统共定义 ...

  8. oracle日期、转换函数

    转换函数 日期类型转换成字符类型 select to_char(sysdate) s1, --14-3月 -16        to_char(sysdate, 'yyyy-mm-dd') s2, - ...

  9. Linux下配置MySQL需要注意的几点

    1.为mysql加上连接数,linux下最大能允许8000个mysql连接. 经验下,设置为3000 [mysqld] max_connections=3000

  10. winform解析json

    在使用C#开发爬虫程序时,会遇到需要解析json字符串的情况.对于json字符串可以使用正则表达式的形式进行解析,更为方便的方法是使用Newtonsoft.Json来实现. Nuget添加应用包 在工 ...