玩轉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手冊來搞懂這些指令的含義,對更復雜的進程進行單步調試,例如設置斷點,會需要更仔細的設計和更復雜的代碼。

在第二部分中,我們會看到如何插入斷點,並且如何在一個正在執行的程序中插入代碼。



發佈了91 篇原創文章 · 獲贊 13 · 訪問量 45萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章