如何利用Ptrace攔截和模擬Linux系統呼叫

IT168GB發表於2018-07-10

寫在前面的話

ptrace(2)這個系統呼叫一般都跟除錯離不開關係,它不僅是類Unix系統中本地偵錯程式監控實現的主要機制,而且它還是strace系統呼叫常用的實現方法。ptrace()系統呼叫函式提供了一個程式(the “tracer”)監察和控制另一個程式(the “tracee”)的方法,它不僅可以監控系統呼叫,而且還能夠檢查和改變“tracee”程式的記憶體和暫存器裡的資料,甚至它還可以攔截系統呼叫。

a.png這裡的“攔截”我指的是tracer能夠改變系統呼叫引數,改變系統呼叫的返回值,甚至遮蔽特定的系統呼叫。這也就意味著,一個tracer將能夠完全實現自己的系統呼叫,這就非常有趣了,也就是說,一個tracer將可以模擬出一整套作業系統機制,而且這一切都不需要核心提供任何其他幫助。

但問題在於,一個程式一次只能夠繫結一個tracer,因此我們無法在除錯程式(GDB)的過程中模擬出一套外部作業系統,而另一個問題就是模擬系統呼叫將耗費更多的資源開銷。

在這篇文章中,我將主要討論x86-64架構下的Linux Ptrace,並且我還會使用到一些特定的Linux擴充套件。除此之外,我可能會忽略錯誤檢查,但最終釋出的完整原始碼將會解決這些問題。

本文涉及到的可執行程式碼樣本可以從【】獲取。

strace

在開始之前,我們先看一看strace的實現骨架。Ptrace一直都沒有相應的使用標準,但在不同的作業系統中它的介面都是類似的,尤其是它的核心功能,但多多少少都會有一些細微的差別。Ptrace(2)的原型類似如下:

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

pid是tracee的程式ID,一個tracee一次只能繫結一個tracer,但一個tracer可以繫結多個tracee。

request域負責選擇一個指定的Ptrace函式,例如ioctl(2)介面。對於strace來說,只有下面是必須的:

PTRACE_TRACEME:它的父程式必須跟蹤這個程式。

PTRACE_SYSCALL:繼續執行,但是會在下一個系統呼叫入口暫停執行。

PTRACE_GETREGS:獲取tracee的暫存器備份。

另外兩個資料域,即addr和data,它們負責給選定的Ptrace函式提供引數,一般這兩個資料都可以忽略,這裡我選擇傳入0。

strace介面本質上是其他命令的字首:

$strace [strace options] program [arguments]

我的最小化配置不包含任何引數,所以要做的第一件事就是假設它至少包含一個引數(fork(2)),透過argv傳遞。在載入目標程式之前,新的程式會告知核心它的父程式將會對它進行跟蹤監視,tracee將會被這個Ptrace系統呼叫掛起:


pid_tpid = fork(); switch(pid) {
    case -1: /* error */         FATAL("%s", strerror(errno));
    case 0/* child */         ptrace(PTRACE_TRACEME, 0, 0, 0);
        execvp(argv[1], argv + 1);
        FATAL("%s", strerror(errno));
} 


父程式將使用wait(2)來等待子程式的PTRACE_TRACEME,當wait(2)返回值之後,子程式將會被掛起:

wait pid(pid,0, 0);

在允許子程式繼續執行之前,我們將告訴作業系統tracee應該跟它的父程式一起終止。真實場景下的strace實現還需要設定其他的引數,例如PTRACE_O_TRACEFORK:

ptrace(PTRACE_SETOPTIONS,pid, 0, PTRACE_O_EXITKILL);

捕捉系統呼叫的迴圈步驟如下:

1.   等待程式進入下一次系統呼叫。

2.   列印系統呼叫資訊。

3.   允許系統呼叫執行,並等待返回結果。

4.   列印系統呼叫的返回值。

PTRACE_SYSCALL請求可以完成等待下一個系統呼叫以及等待系統呼叫結束這兩個任務,跟之前一樣,這裡也需要使用wait(2)來等待tracee進入特定狀態。


ptrace(PTRACE_SYSCALL,pid, 0, 0); waitpid(pid,0, 0); 


wait(2)返回後,執行緒暫存器中將儲存有系統呼叫號和相應引數。下一步就是收集系統呼叫資訊,在不同的系統架構中這一步的實現方式也不同。在x86-64中,系統呼叫號是透過rax傳遞的,引數(最大為6)將傳遞給rdi、rsi、rdx、r10、r8和r9。讀取暫存器還需要其他的Ptrace呼叫,但這裡就不需要wait(2)了,因為tracee並不會改變狀態。


struct user_regs_struct regs;
ptrace(PTRACE_GETREGS,pid, 0, &regs);
longsyscall = regs.orig_rax;
  fprintf(stderr,"%ld(%ld, %ld, %ld, %ld, %ld, %ld)",
        syscall,
        (long)regs.rdi, (long)regs.rsi,(long)regs.rdx,
        (long)regs.r10, (long)regs.r8,  (long)regs.r9); 


接下來就是另一個PTRACE_SYSCALL和wait(2),然後利用PTRACE_GETREGS獲取結果,結果將儲存在rax中:


ptrace(PTRACE_GETREGS,pid, 0, &regs); fprintf(stderr," = %ld\n", (long)regs.rax); 


這個樣本程式的輸出結果還是比較簡陋的,其中沒有包含系統呼叫的符號名,並且每一個引數都是按數字形式列印的,不過這已經足夠奠定系統呼叫攔截的基礎了。

系統呼叫攔截

假設我們想利用Ptrace去實現一個類似OpenBSD的pledge(2)這樣的東西。基本思路如下:很多程式一般都有一個初始化過程,這個過程需要涉及到很多系統訪問許可權,例如開啟檔案和繫結套接字等等。初始化完成之後,它們會進入主迴圈,並處理輸入資料,這裡只需要使用到少量系統呼叫。

在進入主迴圈之前,程式可以限制自身只進行少量操作,如果程式存在漏洞的話,pledge還可以限制漏洞利用程式碼所能完成的事情。當然了,我們不僅可以篡改系統呼叫引數,而且還可以修改系統呼叫號,並將其轉換成一個不存在的系統呼叫,然後在errno中報告一個EPERM錯誤資訊:


for(;;) {
    /* Enter next system call */     ptrace(PTRACE_SYSCALL, pid, 0, 0);
    waitpid(pid, 0, 0);
    struct user_regs_struct regs;
    ptrace(PTRACE_GETREGS, pid, 0, &regs);
    /* Is this system call permitted? */     int blocked = 0;
    if (is_syscall_blocked(regs.orig_rax)) {
        blocked = 1;
        regs.orig_rax = -1; // set to invalidsyscall         ptrace(PTRACE_SETREGS, pid, 0,&regs);
    }
 
    /* Run system call and stop on exit */     ptrace(PTRACE_SYSCALL, pid, 0, 0);
    waitpid(pid, 0, 0);
    if (blocked) {
        /* errno = EPERM */         regs.rax = -EPERM; // Operation notpermitted         ptrace(PTRACE_SETREGS, pid, 0,&regs);
    }
} 


建立自定義的系統呼叫

我將我新建立的模仿pledge的系統呼叫稱為xpledge(),我選擇的系統呼叫號是10000:

#define SYS_xpledge 10000

下面是這個針對tracee的系統呼叫完整介面實現:


#define_GNU_SOURCE #include<unistd.h> #defineXPLEDGE_RDWR  (1 << 0) #defineXPLEDGE_OPEN  (1 << 1) #definexpledge(arg) syscall(SYS_xpledge, arg) 


如果傳遞的引數為0,則只允許執行一些基本的系統呼叫,包括記憶體分配等。PLEDGE_RDWR指定的是各種讀寫操作,如read(2)、readv(2)、pread(2)和preadv(2)等。

在xpledge tracer中,我只需要檢測這個系統呼叫:


/*Handle entrance */ switch(regs.orig_rax) {
    case SYS_pledge:
        register_pledge(regs.rdi);
        break;
} 


作業系統將返回ENOSYS,因為它不是一個真正的系統呼叫,所以我們需要用success(0)重寫返回結果:


/*Handle exit */ switch(regs.orig_rax) {
    case SYS_pledge:
        ptrace(PTRACE_POKEUSER, pid, RAX * 8,0);
        break;
} 


樣例程式的輸出結果如下:


$./example
fread("/dev/urandom")[1]= 0xcd2508c7 XPledging...
XPledgefailed: Function not implemented
fread("/dev/urandom")[2]= 0x0be4a986 fread("/dev/urandom")[1]= 0x03147604 


在tracer下執行的結果如下:


$./xpledge ./example
fread("/dev/urandom")[1]= 0xb2ac39c4 XPledging...
fopen("/dev/urandom")[2]:Operation not permitted
fread("/dev/urandom")[1]= 0x2e1bd1c4 


外部系統模擬

Linux下的Ptrace中有一個非常實用的函式:PTRACE_SYSMU,我們可以利用這個函式來實現系統模擬:


for(;;) {
    ptrace(PTRACE_SYSEMU, pid, 0, 0);
    waitpid(pid, 0, 0);
    struct user_regs_struct regs;
    ptrace(PTRACE_GETREGS, pid, 0, &regs);
    switch (regs.orig_rax) {
        case OS_read:
            /* ... */         case OS_write:
            /* ... */         case OS_open:
            /* ... */         case OS_exit:
            /* ... */         /* ... and so on ... */     }
} 


此程式碼框架在相同系統架構中的測試結果都是能夠穩定執行的,大家可以根據自己的需要來修改程式碼。感謝大家的閱讀,希望大家喜歡。

本文轉載自,原文由FB小編Alpha_h4ck編譯

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31510736/viewspace-2157627/,如需轉載,請註明出處,否則將追究法律責任。

相關文章