你是否曾尝试介入系统调用的执行,是否曾尝试通过改变系统调用的参数来欺骗内核,是否曾经想过debugger是怎么停止一个正在执行的进程,并且让你控制一个进程的。
如果你在想通过负责的内核编程来完成这个工作,请三思。其实Linux已经提供了一个完成这些工作的一个优雅的方式,就是ptrace系统调用。ptrace提供了父进程观察和控制其他进程的机制。它能够检查并且改变其他进程的image和寄存器,通常ptrace被用来实现断点调试和系统调用追踪。
在这篇文章中,我们学习如何介入系统调用的执行,并且改变它的参数。在第二部分,我们会学习一些高级的技术,设定断点和在正在运行的程序中插入代码,我们会窥视紫禁城的寄存器和数据段,并且改变它的内容,我们还会描述一个通过插入代码而使得程序停止或者执行任意指令的方式。
basics
操作系统通过系统调用这个标准的机制来提供服务。系统调用提供了一个标准的API用来访问底层硬件和底层的服务,如文件系统。当一个进程想要调用系统调用,它会把要传递给系统调用的参数放在寄存器中,并且调用软中断0X80,这个软中断就像一个到内核代码的门,然后内核在检查参数之后会执行系统调用。
在i386体系结构中(本文中的代码是针对i386的),系统调用参数号放在寄存器%eax中,要传给这个系统调用的参数被存在%ebx,%ecx, %edx, %esi和%edi中,并按照这个顺序存放。例如下面的调用:
write(2, "Hello", 5)
通常会翻译成
movl $4, %eax
movl $2, %ebx
movl $hello,%ecx
movl $5, %edx
int $0x80
$hello指向字符串Hello。
然而ptrace是在哪里出现的?其实在执行系统调用之前,内核会检查是否这个进程被traced,如果是的话,内核停止进程的执行,并且把控制权给tracking进程,这样这个tracking进程就能够查看和修改被trace的进程的寄存器。
让我们通过一个进程执行的例子来展示:
#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;
}
在运行时,程序输出:
The child made a system call 11
同时也包括ls的输出。系统调用号11是execve,并且它是child执行的第一个系统调用,如果想查看系统调用号的定义,可以查看
/usr/include/asm/unistd.h
正如例子中可以看到的,一个进程fork一个子进程,而子进程是我们要trace的进程。在执行ecec之前,子进程调用ptrace,第一个参数是PTRACE_TRACEME,这个参数告诉内核这个进程是被traced。并且当子进程执行execve系统调用时,内核会吧控制权交给parent。parent进程通过调用wait,等待内核的通知,通知到达之后父进程检查系统调用的参数或者做其他的事情,例如查看寄存器。
当系统调用发生时,内核保存原有的eax寄存器的值,这包含了系统调用号,我们可以通过调用ptrace,并传递给一个参数PTRACE_PEEKUSER来通过子进程的USER段来读取这个系统调用号。
当我们检查完了系统调用,子进程可以继续执行,这是通过传递给Ptrace调用PTRACE_CONT调用。
ptrace参数
long ptrace(enum __ptrace_request request,
pid_t pid,
void *addr,
void *data);
第一个参数决定Ptrace的行为与其他参数是如何使用的。这个参数的数值有可能有很多。
每个参数的具体意义会在文章接下来的部分详细解释。
读取系统调用参数
通过用PTRACE_PEEKUSER调用ptrace,我们可以查看USER区域的内容,寄存器的内容和其他信息都存储在这个USER区域。内核在这个区域存储寄存器的内容,从而使得父进程可以通过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产生了三个write系统调用,当在调用ptrace时传递参数为PTRACE_SYSCALL时,会让子进程在每次调用系统调用或者退出时停止子进程的执行。
在之前的例子中,我们使用PTRACE_PEEKUSER来查看write系统调用的参数。当一个系统调用返回的时候,返回值会被放在%eax中,然后读取它的方法可以参看例子。
wait调用中的status变量被用来检查是否子进程退出了,这是一个典型的方法来检查是否子进程被ptrace停止了或者能够退出。如果想知道关于WIFEXITED这个宏的贡多细节,可以参看wait(2)的man手册。
读取寄存器的值
#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传递参数为PTRACE_GETREGS,这里我们已经利用了在<linux/user.h>中定义的user_regs_struct来读取寄存器的数值。
做一些有趣的事情
#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的工作方式如PTRACE_PEEKDATA相同,除了在子进程传递参数给系统调用时,这里会做读和写的操作,而PEEKDATA只会做读数据操作。
单步调试
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
单步调试上面代码的程序是:
#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手册来搞懂这些指令的含义,对更复杂的进程进行单步调试,例如设置断点,会需要更仔细的设计和更复杂的代码。