zoukankan      html  css  js  c++  java
  • linux ptrace I【转】

    转自:https://www.cnblogs.com/mmmmar/p/6040325.html

    这几天通过《游戏安全——手游安全技术入门这本书》了解到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
    ",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
    ",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
    ",params[0],params[1],params[2]);
                    }
                    else
                    {
                        rax = ptrace(PTRACE_PEEKUSER,child,8*RAX,NULL);
                        printf("write returned with %ld
    ",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
    ",regs.rdi,regs.rsi,regs.rdx);
                    }
                    else
                    {
                        ptrace(PTRACE_GETREGS,child,NULL,&regs);
                        printf("write returned with %ld
    ",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 ",regs.rdi,regs.rsi,regs.rdx); char *str = getdata(child,regs.rsi,regs.rdx); printf("old str,len %lu: %s",strlen(str),str); reserve(str,regs.rdx); printf("hook str,len %lu: %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 ",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 中运行通过。

  • 相关阅读:
    立方体的形成
    三维变换
    实现任意元素居中
    多个transform 属性案例
    旋转轴心案例
    codeforces 706B B. Interesting drink(二分)
    codeforces 706A A. Beru-taxi(水题)
    hdu-5831 Rikka with Parenthesis II(贪心)
    hdu-5826 physics(数学)
    hdu-5813 Elegant Construction(贪心)
  • 原文地址:https://www.cnblogs.com/sky-heaven/p/11113812.html
Copyright © 2011-2022 走看看