ホームページ >システムチュートリアル >Linux >Linux での ptrace: 強力なプロセス追跡および制御メカニズム

Linux での ptrace: 強力なプロセス追跡および制御メカニズム

WBOY
WBOY転載
2024-02-09 20:57:24858ブラウズ

ptrace は、Linux システムの特別なシステム コールで、あるプロセスが別のプロセスの実行 (メモリ、レジスタ、信号などの読み書きを含む) を追跡および制御できるようにします。 ptrace は非常に強力で、デバッガー、トレーサー、インジェクターなどのツールの実装に使用できます。しかし、ptrace がどのように機能するかを本当に理解していますか? Linux で ptrace を使用する方法を知っていますか? ptrace のメリットとデメリットをご存知ですか?この記事では、Linux での ptrace の関連知識を詳しく紹介し、Linux でのこの強力なプロセス追跡および制御メカニズムをよりよく使用および理解できるようにします。

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

ptrace は、あるプロセスが別のプロセスを制御する機能を提供します。これには、制御されるプロセスのコード、データ、レジスタの検出と変更が含まれ、それによってブレークポイントの設定、コードの挿入、およびシステム コールの追跡の機能が有効になります。

ここでは、ptrace関数を使用するプロセスをtracer、制御対象のプロセスをtraceeと呼びます。

ptrace 関数を使用してシステム コールをインターセプトする

オペレーティング システムは、基礎となるハードウェアと対話する操作を実行するために、上位層に標準 API を提供します。これらの標準 API はシステム コールと呼ばれます。各システム コールにはコール番号があり、unistd.h で照会できます。プロセスがシステム コールをトリガーすると、パラメータをレジスタに入れ、ソフト割り込みを通じてカーネル モードに入り、カーネルを通じてシステム コールのコードを実行します。

パラメータはebx、ecx、edx、esiに順番に保存されます

たとえば、コンソール印刷で実行されるシステムコールは

です。 リーリー

はアセンブリ コードに

として変換されます。 リーリー

システム コールを実行するとき、カーネルは最初にプロセスがtraceeであるかどうかを検出します。そうである場合、カーネルはプロセスを一時停止し、制御をトレーサに渡します。その後、トレーサはtraceeのレジスタを表示または変更できるようになります。

サンプルコードは以下のとおりです

リーリー

このプログラムは、fork を通じてトレース (trace) するサブプロセスを作成します。execl を実行する前に、サブプロセスは、ptrace 関数の PTRACE_TRACEME パラメータを通じてトレースされることをカーネルに通知します。

execl の場合、この関数は実際に execve システム コールをトリガーします。このとき、カーネルはプロセスがトレース中であることを検出し、それを一時停止し、待機中のトレーサ (このプログラムのメイン スレッド) を起動する信号を送信します。 )。

システム コールがトリガーされると、カーネルは呼び出し番号を保持する rax レジスタの内容を orig_rax に保存します。これは、ptrace の PTRACE_PEEKUSER パラメータを通じて読み取ることができます。

ORIG_RAX は、sys/reg.h に保存されているレジスタ番号です。64 ビット システムでは、各レジスタのサイズは 8 バイトなので、ここでは 8*ORIG_RAX を使用してレジスタ アドレスを取得します。

システム コール番号を取得した後、ptrace の PTRACE_CONT パラメータを通じて一時停止された子プロセスを起動し、実行を継続させることができます。

ptrace パラメータ

リーリー

パラメータ要求は ptrace 関数の動作を制御し、sys/ptrace.h で定義されます。

パラメータ pid はトレース先のプロセス番号を指定します。

上記の 2 つのパラメータは必須であり、次の 2 つのパラメータはそれぞれアドレスとデータであり、それらの意味はパラメータ要求によって制御されます。

特定のリクエストパラメータの値と意味については、ヘルプドキュメントを参照してください (コンソール入力: man ptrace)

戻り値に注意してください。マニュアルには、1 ワードのデータ サイズが返されると記載されており、32 ビット マシンでは 4 バイト、64 ビット マシンでは 8 バイトです。これらはすべて長さに対応しています。長いもの。 Baidu には、1 バイトのデータを返すのは間違っているという無責任な投稿がたくさんあります。

システムコールパラメータの読み取り

ptrace の PTRACE_PEEKUSER パラメータを通じて、レジスタの値などの USER 領域の内容を表示できます。 USER 領域は構造体 (sys/user.h で定義されたユーザー構造体) です。

カーネルはレジスタ値をこの構造体に保存します。これは、トレーサが ptrace 関数を通じて表示するのに便利です。

サンプルコードは以下のとおりです

リーリー

上記のコードでは、write システム コールのパラメーターをチェックします (テキストをコンソールに出力する ls コマンドによってトリガーされます)。

システム コールを追跡するために、ptrace の PTRACE_SYSCALL パラメータを使用します。これにより、システム コールがトリガーされるか終了すると、tracee が一時停止し、同時にトレーサにシグナルが送信されます。

前の例では、PTRACE_PEEKUSER パラメータを使用してシステム コールのパラメータを表示しましたが、同様に、RAX レジスタに格納されているシステム コールの戻り値を表示することもできます。

上記のコードのステータス変数は、tracee の実行が終了したかどうか、また、tracee の実行を待ち続ける必要があるかどうかを検出するために使用されます。

すべてのレジスタの値を読み取ります

この例は、レジスタ値を取得する簡単な方法を示しています

リーリー

この例では、すべてのレジスタ値は 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 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はlxlinux.netで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。