首頁 >系統教程 >Linux >Linux 下的 ptrace:強大的進程追蹤與控制機制

Linux 下的 ptrace:強大的進程追蹤與控制機制

WBOY
WBOY轉載
2024-02-09 20:57:24860瀏覽

ptrace 是 Linux 系統中一種特殊的系統調用,它可以讓一個進程追蹤和控制另一個進程的執行,包括讀寫其記憶體、暫存器、訊號等。 ptrace 的功能非常強大,它可以用來實現諸如調試器、追蹤器、注入器等工具。但是,你真的了解 ptrace 的工作原理嗎?你知道如何在 Linux 下使用 ptrace 嗎?你知道 ptrace 的優點和缺點嗎?本文將為你詳細介紹 Linux 下的 ptrace 的相關知識,讓你在 Linux 下更好地使用和理解這個強大的進程追蹤和控制機制。

Linux 下的 ptrace:一种强大的进程跟踪和控制机制

#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 
#include 
#include 
#include 
#include 
#include 
#include 

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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


#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; iif(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; iif(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; imain()
{
    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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#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读取。

这样,就可以看到要执行的每条指令的机器码。

通过本文,你应该对 Linux 下的 ptrace 有了一个深入的了解,知道了它的定义、原理、用法和优点。你也应该明白了 ptrace 的适用场景和注意事项,以及如何在 Linux 下正确地使用 ptrace。我们建议你在需要跟踪和控制进程的执行时,使用 ptrace 来实现你的目标。同时,我们也提醒你在使用 ptrace 时要注意一些潜在的风险和问题,如权限、安全、兼容性等。希望本文能够帮助你更好地使用 Linux 系统,让你在 Linux 下体验 ptrace 的强大和灵活。

以上是Linux 下的 ptrace:強大的進程追蹤與控制機制的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:lxlinux.net。如有侵權,請聯絡admin@php.cn刪除