版权所有 © 转载时必须以链接形式注明作者和原始出处!
Playing with ptrace, Part II
by Pradeep Padala p_padala@yahoo.com
http://www.cise.ufl.edu/~ppadala
Created 2002-11-01 02:00
翻译: Magic.D E-mail: adamgic@163.com
在第一部分中我们已经看到ptrace怎么获取子进程的系统调用以及改变系统调用的参数。在这篇文章中,我们将要研究如何在子进程中设置断点和往运行中的程序里插入代码。实际上调试器就是用这种方法来设置断点和执行调试句柄。与前面一样,这里的所有代码都是针对i386平台的。
附着在进程上
在第一部分钟,我们使用ptrace(PTRACE_TRACEME, …)来跟踪一个子进程,如果你只是想要看进程是怎么进行系统调用和跟踪程序的,这个做法是不错的。但如果你要对运行中的进程进行调试,则需要使用 ptrace( PTRACE_ATTACH, ….)
当 ptrace( PTRACE_ATTACH, …)在被调用的时候传入了子进程的pid时, 它大体是与ptrace( PTRACE_TRACEME, …)的行为相同的,它会向子进程发送SIGSTOP信号,于是我们可以察看和修改子进程,然后使用 ptrace( PTRACE_DETACH, …)来使子进程继续运行下去。
下面是调试程序的一个简单例子
1 2 3 4 5 6 7 8 9 |
|
将上面的代码保存为dummy2.c。按下面的方法编译运行:
gcc -o dummy2 dummy2.c
./dummy2 &
现在我们可以用下面的代码来附着到dummy2上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
上面的程序仅仅是附着在子进程上,等待它结束,并测量它的eip( 指令指针)然后释放子进程。 设置断点
调试器是怎么设置断点的呢?通常是将当前将要执行的指令替换成trap指令,于是被调试的程序就会在这里停滞,这时调试器就可以察看被调试程序的信息了。被调试程序恢复运行以后调试器会把原指令再放回来。这里是一个例子:
1 2 3 4 5 6 7 8 9 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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
|
上面的程序将把三个byte的内容进行替换以执行trap指令,等被调试进程停滞以后,我们把原指令再替换回来并把eip修改为原来的值。下面的图中演示了指令的执行过程
1. 进程停滞后
2. 替换入trap指令
3.断点成功,控制权交给了调试器
4. 继续运行,将原指令替换回来并将eip复原
在了解了断点的机制以后,往运行中的程序里面添加指令也不再是难事了,下面的代码会使原程序多出一个”hello world”的输出
这时一个简单的”hello world”程序,当然为了我们的特殊需要作了点修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
使用
gcc -o hello hello.c
来编译它。
在backward和forward之间的跳转是为了使程序能够找到”hello world” 字符串的地址。
使用GDB我们可以得到上面那段程序的机器码。启动GDB,然后对程序进行反汇编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
我们需要使用从man+3到backward+20之间的字节码,总共41字节。使用GDB中的x命令来察看机器码。
1 2 3 4 5 6 |
|
已经有了我们想要执行的指令,还等什么呢?只管把它们根前面那个例子一样插入到被调试程序中去!
代码:
1 2 3 4 5 6 7 8 9 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 36 37 38 39 40 41 |
|
将代码插入到自由空间
在前面的例子中我们将代码直接插入到了正在执行的指令流中,然而,调试器可能会被这种行为弄糊涂,所以我们决定把指令插入到进程中的自由空间中去。通过察看/proc/pid/maps可以知道这个进程中自由空间的分布。接下来这个函数可以找到这个内存映射的起始点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
在/proc/pid/maps中的每一行都对应了进程中一段内存区域。主函数的代码如下:
1 2 3 4 5 6 7 8 9 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 36 37 38 39 40 41 42 43 44 |
|
ptrace的幕后工作
那么,在使用ptrace的时候,内核里发生了声么呢?这里有一段简要的说明:当一个进程调用了 ptrace( PTRACE_TRACEME, …)之后,内核为该进程设置了一个标记,注明该进程将被跟踪。内核中的相关原代码如下:
1 2 3 4 5 6 7 8 9 10 |
|
一次系统调用完成之后,内核察看那个标记,然后执行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来实现一些安全机制。