玩转ptrace:【Playing with ptrace, Part I】

你是否曾尝试介入系统调用的执行,是否曾尝试通过改变系统调用的参数来欺骗内核,是否曾经想过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手册。


读取寄存器的值

如果你想要在系统调用进入或者退出的时候读取寄存器的值,上面所展示的过程可能很麻烦,把PTRACE_GETREGS作为第一个参数来调用ptrace会通过一个调用完成这些工作。
具体代码如下:
#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, &regs);
                 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只会做读数据操作。

单步调试

ptrace提供了单步运行子进程代码的功能。通过调用ptrace(PTRACE_SINGLESTEP,...)会告诉内核在每一个指令停止子进程,并使父进程来控制,下面的例子展示了一个在系统调用执行时阅读到底那条指令正在执行的方法。我创建了一些拙劣的可执行文件来帮助你来理解到底发生了什么而不是对libc的调用进行分析。

下面是一个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

单步调试上面代码的程序是:

#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, &regs);
            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手册来搞懂这些指令的含义,对更复杂的进程进行单步调试,例如设置断点,会需要更仔细的设计和更复杂的代码。

在第二部分中,我们会看到如何插入断点,并且如何在一个正在执行的程序中插入代码。



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值