0

0

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

WBOY

WBOY

发布时间:2024-02-09 20:57:24

|

2610人浏览过

|

来源于良许Linux教程网

转载

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的长度。百度可以搜到很多不负责的帖子说返回一个字节的数据是不对的!

动软商城系统
动软商城系统

动软商城系统是一款优秀的网上商城系统,经营者只需要轻松的后台操作,就可以马上拥有功能强的网上销售系统,同时动软商城系统提供多样的营销手段帮助您成功打开网上销售市场。动软的模版界面机制,可以轻松的搭建出风格各异的界面,最大限度的满足经营者的要求,还拥有专业SEO优化系统,大大提高网页被搜索引擎抓取收录的几率。动软商城系统先进的流程控制技术全面促进进、销、存等系统的协同,支持企业数据整合和网络资源信息

下载

读取系统调用参数

通过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,®s);
                    printf("write called with %llu, %llu, %llu\n",regs.rdi,regs.rsi,regs.rdx);
                }
                else
                {
                    ptrace(PTRACE_GETREGS,child,NULL,®s);
                    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(®s,0,sizeof(struct user_regs_struct));
            if(ptrace(PTRACE_GETREGS,child,NULL,®s) == -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,®s);
            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 的强大和灵活。

相关专题

更多
golang结构体相关大全
golang结构体相关大全

本专题整合了golang结构体相关大全,想了解更多内容,请阅读专题下面的文章。

193

2025.06.09

golang结构体方法
golang结构体方法

本专题整合了golang结构体相关内容,请阅读专题下面的文章了解更多。

185

2025.07.04

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

469

2023.08.10

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

469

2023.08.10

磁盘配额是什么
磁盘配额是什么

磁盘配额是计算机中指定磁盘的储存限制,就是管理员可以为用户所能使用的磁盘空间进行配额限制,每一用户只能使用最大配额范围内的磁盘空间。php中文网为大家提供各种磁盘配额相关的内容,教程,供大家免费下载安装。

1345

2023.06.21

如何安装LINUX
如何安装LINUX

本站专题提供如何安装LINUX的相关教程文章,还有相关的下载、课程,大家可以免费体验。

698

2023.06.29

linux find
linux find

find是linux命令,它将档案系统内符合 expression 的档案列出来。可以指要档案的名称、类别、时间、大小、权限等不同资讯的组合,只有完全相符的才会被列出来。find根据下列规则判断 path 和 expression,在命令列上第一个 - ( ) , ! 之前的部分为 path,之后的是 expression。还有指DOS 命令 find,Excel 函数 find等。本站专题提供linux find相关教程文章,还有相关

293

2023.06.30

linux修改文件名
linux修改文件名

本专题为大家提供linux修改文件名相关的文章,这些文章可以帮助用户快速轻松地完成文件名的修改工作,大家可以免费体验。

773

2023.07.05

桌面文件位置介绍
桌面文件位置介绍

本专题整合了桌面文件相关教程,阅读专题下面的文章了解更多内容。

0

2025.12.30

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

相关下载

更多

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PostgreSQL 教程
PostgreSQL 教程

共48课时 | 6.2万人学习

Git 教程
Git 教程

共21课时 | 2.3万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号