linux ptrace I


這幾天通過《游戲安全——手游安全技術入門這本書》了解到linux系統中ptrace()這個函數可以實現外掛功能,於是在ubuntu 16.04 x86_64系統上對這個函數進行了學習。

參考資料:

Playing with ptrace, Part I

Playing with ptrace, Part II

這兩篇文章里的代碼都是在x86平台上運行的,本文中將其移植到了x86_64平台。

ptrace提供讓一個進程來控制另一個進程的能力,包括檢測,修改被控制進程的代碼,數據,寄存器,進而實現設置斷點,注入代碼和跟蹤系統調用的功能。

這里把使用ptrace函數的進程稱為tracer,被控制的進程稱為tracee。

 

使用ptrace函數來攔截系統調用(system call)

操作系統向上層提供標准的API來執行與底層硬件交互的操作,這些標准API稱為系統調用,每個系統調用都有一個調用編號,可以在unistd.h中查詢。當進程觸發一個系統調用時它會把參數放入寄存器中,然后通過軟中斷進入內核模式,通過內核來執行這個系統調用的代碼。

在X86_64體系中,系統調用號保存在rax,調用參數依次保存在rdi,rsi,rdx,rcx,r8和r9中;而在x86體系中,系統調用號保存在寄存器eax中,其余的參數依次保存在ebx,ecx,edx,esi中

例如控制台打印所執行的系統調用為

write(1,"Hello",5)

翻譯為匯編代碼為

mov rax, 1
mov rdi, message
mov rdx, 5
syscall
message:
db "Hello"

在執行系統調用時,內核先檢測一個進程是否為tracee,如果是的話內核就會暫停該進程,然后把控制權轉交給tracer,之后tracer就可以查看或者修改tracee的寄存器了。

示例代碼如下

#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/reg.h>
#include <sys/user.h>
#include <stdio.h>

int main()
{
    pid_t child;
    long orig_rax;
    child = fork();
    if(child == 0)
    {
        ptrace(PTRACE_TRACEME,0,NULL,NULL);
        execl("/bin/ls","ls",NULL);
    }
    else
    {
        wait(NULL);
        orig_rax = ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);
        printf("the child made a system call %ld\n",orig_rax);
        ptrace(PTRACE_CONT,child,NULL,NULL);
    }
    return 0;
}

//輸出:the child made a system call 59

該程序通過fork創建出一個我們將要跟蹤(trace)的子進程,在執行execl之前,子進程通過ptrace函數的PTRACE_TRACEME參數來告知內核自己將要被跟蹤。

對於execl,這個函數實際上會觸發execve這個系統調用,這時內核發現此進程為tracee,然后將其暫停,發送一個signal喚醒等待中的tracer(此程序中為主線程)。

當觸發系統調用時,內核會將保存調用編號的rax寄存器的內容保存在orig_rax中,我們可以通過ptrace的PTRACE_PEEKUSER參數來讀取。

ORIG_RAX為寄存器編號,保存在sys/reg.h中,而在64位系統中,每個寄存器有8個字節的大小,所以此處用8*ORIG_RAX來獲取該寄存器地址。

當我們獲取到系統調用編號以后,就可以通過ptrace的PTRACE_CONT參數來喚醒暫停中的子進程,讓其繼續執行。

 

ptrace參數

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

參數request 控制ptrace函數的行為,定義在sys/ptrace.h中。

參數pid 指定tracee的進程號。

以上兩個參數是必須的,之后兩個參數分別為地址和數據,其含義由參數request控制。

具體request參數的取值及含義可查看幫助文檔(控制台輸入: man ptrace)

注意返回值,man手冊上的說法是返回一個字的數據大小,在32位機器上是4個字節,在64位機器上是8個字節,都對應一個long的長度。百度可以搜到很多不負責的帖子說返回一個字節的數據是不對的!

 

讀取系統調用參數

通過ptrace的PTRACE_PEEKUSER參數,我們可以查看USER區域的內容,例如查看寄存器的值。USER區域為一個結構體(定義在sys/user.h中的user結構體)。

內核將寄存器的值儲存在該結構體中,便於tracer通過ptrace函數查看。

示例代碼如下

#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/reg.h>
#include <sys/user.h>
#include <sys/syscall.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    pid_t child;
    long orig_rax,rax;
    long params[3]={0};
    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_rax = ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);
            //printf("the child made a system call %ld\n",orig_rax);
            if(orig_rax == SYS_write)
            {
                if(insyscall == 0)
                {
                    insyscall = 1;
                    params[0] = ptrace(PTRACE_PEEKUSER,child,8*RDI,NULL);
                    params[1] = ptrace(PTRACE_PEEKUSER,child,8*RSI,NULL);
                    params[2] = ptrace(PTRACE_PEEKUSER,child,8*RDX,NULL);
                    printf("write called with %ld, %ld, %ld\n",params[0],params[1],params[2]);
                }
                else
                {
                    rax = ptrace(PTRACE_PEEKUSER,child,8*RAX,NULL);
                    printf("write returned with %ld\n",rax);
                    insyscall = 0;
                }
            }
            ptrace(PTRACE_SYSCALL,child,NULL,NULL);
        }
    }
    return 0;

}
/***
輸出:
write called with 1, 25226320, 65
ptrace_1.c  ptrace_2.c    ptrace_3.C  ptrace_4.C    ptrace_5.c  test.c
write returned with 65
***/


以上代碼中我們查看write系統調用(由ls命令向控制台打印文字觸發)的參數。

為了追蹤系統調用,我們使用ptrace的PTRACE_SYSCALL參數,它會使tracee在觸發系統調用或者結束系統調用時暫停,同時向tracer發送signal。

在之前的例子中我們使用PTRACE_PEEKUSER參數來查看系統調用的參數,同樣的,我們也可以查看保存在RAX寄存器中的系統調用返回值。

上邊代碼中的status變量時用來檢測是否tracee已經執行結束,是否需要繼續等待tracee執行。

 

讀取所有寄存器的值

這個例子中演示一個獲取寄存器值的簡便方法

#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <sys/reg.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdio.h>

int main()
{
    pid_t child;
    long orig_rax ,rax;
    long params[3] = {0};
    int status = 0;
    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_rax = ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);
            if(orig_rax == SYS_write)
            {
                if(insyscall == 0)
                {
                    insyscall = 1;
                    ptrace(PTRACE_GETREGS,child,NULL,&regs);
                    printf("write called with %llu, %llu, %llu\n",regs.rdi,regs.rsi,regs.rdx);
                }
                else
                {
                    ptrace(PTRACE_GETREGS,child,NULL,&regs);
                    printf("write returned with %ld\n",regs.rax);
                    insyscall = 0;
                }
            }
            ptrace(PTRACE_SYSCALL,child,NULL,NULL);
        }
    }
    return 0;
}

 

這個例子中通過PTRACE_GETREGS參數獲取了所有的寄存器值。結構體user_regs_struct定義在sys/user.h中。

修改系統調用的參數

現在我們已經知道如何攔截一個系統調用並查看其參數了,接下來我們來修改它

#include <sys/ptrace.h>
#include <sys/user.h>
#include <sys/reg.h>
#include <sys/wait.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


#define LONG_SIZE 8
//獲取參數
char* getdata(pid_t child,unsigned long addr,unsigned long len)
{
    char *str =(char*) malloc(len + 1);
    memset(str,0,len +1);
    union u{
        long int val;
        char chars[LONG_SIZE];
    }word;
    int i, j;    
    for(i = 0,j = len/LONG_SIZE; i<j; ++i)
    {
        word.val = ptrace(PTRACE_PEEKDATA,child,addr + i*LONG_SIZE,NULL);
        if(word.val == -1)
            perror("trace get data error");
        memcpy(str+i*LONG_SIZE,word.chars,LONG_SIZE);
    }
    j = len % LONG_SIZE;
    if(j != 0)
    {
        word.val = ptrace(PTRACE_PEEKDATA,child,addr + i*LONG_SIZE,NULL);
        if(word.val == -1)
            perror("trace get data error");
        memcpy(str+i*LONG_SIZE,word.chars,j);
    }
    return str;
}
//提交參數
void putdata(pid_t child,unsigned long  addr,unsigned long len, char *newstr)
{
    union u
    {
        long val;
        char chars[LONG_SIZE];
    }word;
    int i,j;
    for(i = 0, j = len/LONG_SIZE; i<j ; ++i)
    {
        memcpy(word.chars,newstr+i*LONG_SIZE,LONG_SIZE);
        if(ptrace(PTRACE_POKEDATA, child, addr+i*LONG_SIZE,word.val) == -1)
            perror("trace error");

    }
    j = len % LONG_SIZE;
    if(j !=0 )
    {
        memcpy(word.chars,newstr+i*LONG_SIZE,j);
        ptrace(PTRACE_POKEDATA, child, addr+i*LONG_SIZE,word.val);
    }
}

//修改參數
void reserve(char *str,unsigned int len) { int i,j; char tmp; for(i=0,j=len-2; i<=j; ++i,--j ) { tmp = str[i]; str[i] = str[j]; str[j] = tmp; } } int main() { pid_t child; child = fork(); if(child == 0) { ptrace(PTRACE_TRACEME,0,NULL,NULL); execl("/bin/ls","ls",NULL); } else { struct user_regs_struct regs; int status = 0; int toggle = 0; while(1) { wait(&status); if(WIFEXITED(status)) break; memset(&regs,0,sizeof(struct user_regs_struct)); if(ptrace(PTRACE_GETREGS,child,NULL,&regs) == -1) { perror("trace error"); } if(regs.orig_rax == SYS_write) { if(toggle == 0) { toggle = 1; //in x86_64 system call ,pass params with %rdi, %rsi, %rdx, %rcx, %r8, %r9 //no system call has over six params printf("make write call params %llu, %llu, %llu\n",regs.rdi,regs.rsi,regs.rdx); char *str = getdata(child,regs.rsi,regs.rdx); printf("old str,len %lu:\n%s",strlen(str),str); reserve(str,regs.rdx); printf("hook str,len %lu:\n%s",strlen(str),str); putdata(child,regs.rsi,regs.rdx,str); free(str); } else { toggle = 0; } } ptrace(PTRACE_SYSCALL,child,NULL,NULL); } } return 0; }
/***
輸出:
make write call params 1, 9493584, 66
old str,len 66:
ptrace        ptrace2    ptrace3     ptrace4    ptrace5     test    test.s
hook str,len 66:
s.tset    tset     5ecartp    4ecartp     3ecartp    2ecartp        ecartp
s.tset    tset     5ecartp    4ecartp     3ecartp    2ecartp        ecartp
make write call params 1, 9493584, 65
old str,len 65:
ptrace_1.c  ptrace_2.c    ptrace_3.C  ptrace_4.C    ptrace_5.c  test.c
hook str,len 65:
c.tset  c.5_ecartp    C.4_ecartp  C.3_ecartp    c.2_ecartp  c.1_ecartp
c.tset  c.5_ecartp    C.4_ecartp  C.3_ecartp    c.2_ecartp  c.1_ecartp
***/

 這個例子中,綜合了以上我們提到的所有知識。進一步得,我們使用了ptrace的PTRACE_POKEDATA參數來修改系統調用的參數值。

這個參數和PTRACE_PEEKDATA參數的作用相反,它可以修改tracee指定地址的數據。

 

單步調試

接下來介紹一個調試器中常用的操作,單步調試,它就用到了ptrace的PTRACE_SINGLESTEP參數。

#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/user.h>
#include <sys/reg.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>

#define LONG_SIZE 8

void main()
{
    pid_t chid;
    chid = fork();
    if(chid == 0)
    {
        ptrace(PTRACE_TRACEME,0,NULL,NULL);
     //這里的test是一個輸出hello world的小程序 execl(
"./test","test",NULL); } else { int status = 0; struct user_regs_struct regs; int start = 0; long ins; while(1) { wait(&status); if(WIFEXITED(status)) break; ptrace(PTRACE_GETREGS,chid,NULL,&regs); if(start == 1) { ins = ptrace(PTRACE_PEEKTEXT,chid,regs.rip,NULL); printf("EIP:%llx Instuction executed:%lx\n",regs.rip,ins); } if(regs.orig_rax == SYS_write) { start = 1; ptrace(PTRACE_SINGLESTEP,chid,NULL,NULL); }else{ ptrace(PTRACE_SYSCALL,chid,NULL,NULL); } } } }

通過rip寄存器的值來獲取下一條要執行指令的地址,然后用PTRACE_PEEKDATA讀取。

這樣,就可以看到要執行的每條指令的機器碼。

 

注:本文對開頭文章參考資料進行了一些翻譯,所有代碼均在ubuntu 16.04 64bit 中運行通過。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM