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 中运行通过。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏go语言核心编程技术

go语言调度器源代码情景分析之五:汇编指令

汇编语言是每位后端程序员都应该掌握的一门语言,因为学会了汇编语言,不管是对我们调试程序还是研究与理解计算机底层的一些运行原理都具有非常重要的作用,所以建议有兴趣...

15120
来自专栏知识分享

3-ESP8266 SDK开发基础入门篇--点亮一个灯

https://www.cnblogs.com/yangfengwu/p/11072834.html

10130
来自专栏go语言核心编程技术

go语言调度器源代码情景分析之六:go汇编语言

go语言runtime(包括调度器)源代码中有部分代码是用汇编语言编写的,不过这些汇编代码并非针对特定体系结构的汇编代码,而是go语言引入的一种伪汇编,它同样也...

14130
来自专栏go语言核心编程技术

go语言调度器源代码情景分析之三:内存

内存由大量内存单元组成,内存单元大小为1个字节(1字节包含8个二进制位), 每个内存单元都有一个编号,更专业的说法是每一个内存单元都有一个地址,我们在编写汇编代...

10230
来自专栏go语言核心编程技术

go语言调度器源代码情景分析之八:系统调用

我们将在最后一章讨论有关系统调用方面的抢占调度,所以这里有必要对系统调用有个基本的了解。

11940
来自专栏知识分享

2-STM32+W5500+GPRS物联网开发基础篇-基础篇学习的内容

https://www.cnblogs.com/yangfengwu/p/10936553.html

13440
来自专栏go语言核心编程技术

go语言调度器源代码情景分析之二:CPU寄存器

寄存器是CPU内部的存储单元,用于存放从内存读取而来的数据(包括指令)和CPU运算的中间结果,之所以要使用寄存器来临时存放数据而不是直接操作内存,一是因为CPU...

12140
来自专栏go语言核心编程技术

go语言调度器源代码情景分析之四:函数调用栈

栈是一种“后进先出”的数据结构,它相当于一个容器,当需要往容器里面添加元素时只能放在最上面的一个元素之上,需要取出元素时也只能从最上面开始取,通常我们称添加元素...

11910
来自专栏go语言核心编程技术

Go语言调度器源代码情景分析之十:线程本地存储

本文是《go调度器源代码情景分析》系列 第一章 预备知识的第十小节,也是预备知识的最后一小节。

13850
来自专栏go语言核心编程技术

go调度器源代码情景分析之九:操作系统线程及线程调度

要深入理解goroutine的调度器,就需要对操作系统线程有个大致的了解,因为go的调度系统是建立在操作系统线程之上的,所以接下来我们对其做一个简单的介绍。

13020

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励