首页 > 代码库 > linux ptrace II

linux ptrace II

第一篇 linux ptrace I

在之前的文章中我们用ptrace函数实现了查看系统调用参数的功能。在这篇文章中,我们会用ptrace函数实现设置断点,跟代码注入功能。

参考资料

Playing with ptrace, Part I

Playing with ptrace, Part II

英文好的推荐直接看老外的文章。但是其代码是运行在x86系统上的,本文中将其移植到了x86_64系统。

 

进程附加

在之前的文章中,我们都是trace自己程序fork出来的子进程,现在我们来看一下如何trace一个正在运行的进程。

trace一个正在运行的进程称为进程附加(attach)。使用的是ptrace函数的PTRACE_ATTACH参数。当一个进程成功附加到一个正在运行的进程时,此进程会成为被附加进程的父进程,同时向被附加的进程发送一个SIGSTOP信号,让其停止,这时我们就可以对其进行操纵。当我们完成对tracee的操作后就可以使用ptrace的PTRACE_DETACH参数停止附加。

我们用一个循环来模拟一个正在运行的进程,下边称此程序为hello

int main()
{   int i;
    for(i = 0;i < 10; ++i) {
        printf("My counter: %d\n", i);
        sleep(2);
    }
    return 0;
}

本文之后所有的程序都以此程序当作被附加的进程。在其运行之后我们可以使用 ps -h 命令查看其进程号(pid),以便我们通过进程号对其附加。

接下来看一个简单的进程附加的例子

#include <sys/types.h>
#include <sys/reg.h>
#include <sys/user.h>
#include <sys/wait.h>
#include <sys/ptrace.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    pid_t traced_process;
    struct user_regs_struct regs;
    long ins;
    if(argc != 2)
    {
        puts("no pid input");
        exit(1);
    }
    traced_process = atoi(argv[1]);
    printf("try to trace pid :%u\n",traced_process);
    if(ptrace(PTRACE_ATTACH,traced_process,NULL,NULL)==-1)
    {
        perror("trace error:");
    }
    wait(NULL);
    if(ptrace(PTRACE_GETREGS,traced_process,NULL,&regs)==-1)
    {
        perror("trace error:");
    }
    ins = ptrace(PTRACE_PEEKTEXT,traced_process,regs.rip,NULL);
    if(ins == -1)
    {
        perror("trace error:");
    }
    printf("EIP:%llx Instruction executed: %lx\n",regs.rip,ins);
    if(ptrace(PTRACE_DETACH,traced_process,NULL,NULL)==-1)
    {
        perror("trace error:");
    }
    return 0;
}

上边的程序对hello进行了附加,等其停下来以后,读取hello要运行的下一条指令的内容(地址存在rip中)。读取之后停止附加,让hello继续运行。

 

设置断点

下面要实现的是许多调试器都拥有的设置断点功能。

设置断点的原理:假如要在A地址处设置断点,可以把A地址处的指令替换为一条trap指令,就是说当tracee运行完这条被替换的指令后会自动停止,然后告知tracer自己已停止。

代码如下:

#include <sys/ptrace.h>
#include <sys/reg.h>
#include <sys/user.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define LONG_SIZE 8

void getdata(pid_t child, long addr,char *str,int len)
{
    char *laddr = str;
    int i = 0,j = len/LONG_SIZE;
    union u{
        long val;
        char chars[LONG_SIZE];
    } word;
    while(i<j)
    {
        word.val = ptrace(PTRACE_PEEKDATA,child,addr + i*LONG_SIZE,NULL);
        if(word.val == -1)
            perror("trace error");
        memcpy(laddr,word.chars,LONG_SIZE);
        ++i;
        laddr += 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 error");
    }
    str[len] = \0;
}


void putdata(pid_t child,long addr,char *str,int len)
{
    char *laddr = str;
    int i = 0, j = len/LONG_SIZE;
    union u{
        long val;
        char chars[LONG_SIZE];
    }word;
    while(i<j)
    {
        memcpy(word.chars,laddr,LONG_SIZE);
        if(ptrace(PTRACE_POKEDATA,child,addr+i*LONG_SIZE,word.val) == -1)
            perror("trace error");
        ++i;
        laddr += LONG_SIZE;
    }
    j = len % LONG_SIZE;
    if(j != 0)
    {
        word.val = 0;
        memcpy(word.chars,laddr,j);
        if(ptrace(PTRACE_POKEDATA,child,addr+i*LONG_SIZE,word.val) == -1)
            perror("trace error");
    }
}


void printBytes(const char* tip,char* codes,int len)
{
    int i;
    printf("%s :",tip);
    for(i = 0;i<len;++i)
    {
        printf("%02x ",(unsigned char)codes[i]);
    }
    puts("");
}

#define CODE_SIZE 8

int main(int argc ,char *argv[])
{
    if(argc != 2)
    {
        puts("no pid input");
        exit(1);
    }
    pid_t traced_process;
    struct user_regs_struct regs;
    long ins;
    char code[LONG_SIZE] = {0xcc};
    char backup[LONG_SIZE];
    traced_process = atoi(argv[1]);
    printf("try to attach pid:%u\n",traced_process);
    if(ptrace(PTRACE_ATTACH,traced_process,NULL,NULL) == -1)
    {
        perror("trace attach error");
    }
    wait(NULL);
    if(ptrace(PTRACE_GETREGS,traced_process,NULL,&regs) == -1)
    {
        perror("trace get regs error");
    }
    //copy instructions into backup variable
    getdata(traced_process,regs.rip,backup,CODE_SIZE);
    printBytes("get tracee instuction",backup,LONG_SIZE);
    puts("try to set breakpoint");
    printBytes("set breakpoint instruction",code,LONG_SIZE);
    putdata(traced_process,regs.rip,code,CODE_SIZE);
    if(ptrace(PTRACE_CONT,traced_process,NULL,NULL) == -1)
    {
        perror("trace continue error");
    }
    wait(NULL);
    puts("the process stopped Press <Enter> to continue");
    getchar();
    printBytes("place breakpoint instruction with tracee instruction",backup,LONG_SIZE);
    putdata(traced_process,regs.rip,backup,CODE_SIZE);
    ptrace(PTRACE_SETREGS,traced_process,NULL,&regs);
    ptrace(PTRACE_DETACH,traced_process,NULL,NULL);
    return 0;

}

这里在hello程序停下来后将rip指向的指令备份在了backup数组中,同时把rip指向的指令替换为code数组中的指令。

code数组中0xcc对应为汇编指令的

int3 ;软中断指令,执行后程序会陷入中断停止,同时发送signal,可被tracer接收

当tracer接收到软中断指令发送的signal时,从等待(wait)中被唤醒,把被替换的指令还原回去,同时也将寄存器还原,最后停止附加,让hello继续执行。

示意图如下

技术分享

代码注入

接下来我们实现最后一个功能,进行代码注入,让hello程序打印出一行 “hello world"

要进行代码注入,首先我们要知道被注入代码的机器指令是什么。

先写出要进行注入代码的汇编代码

section .text

global main

main:
    jmp forward
backward:
    pop rsi
    mov rax, 1
    mov rdi, 1
    mov rdx, 13
    syscall
    int3
forward:
    call backward
db "Hello world",0xa

注意,这里为什么要跳来跳去呢,因为我们要定位字符串的地址,在执行 call backward 这条指令时,会把其下一条指令,也就是字符串的地址压入栈中。然后 pop rsi 将其取出。

使用nasm工具对其进行汇编

nasm -f  elf64 inject.asm

再使用objdump工具查看对应的机器码

objdump -d inject.o

现在就可以将其注入到hello的进程里了。方法同设置断点一样,这里只贴出主函数代码

#define CODE_SIZE 48
int
main(int argc ,char *argv[]) { if(argc<2) { puts("no pid input"); exit(1); } pid_t tracee = atoi(argv[1]); char code_inject[CODE_SIZE] ={0xeb,0x13,0x5e,0xb8,0x01,0x00,0x00,0x00,0xbf,0x01,0x00,0x00,0x00,0xba,0x0d,0x00,0x00,0x00,0x0f,0x05,0xcc,0xe8,0xe8,0xff,0xff,0xff,0x48,0x65,0x6c,0x6c,0x6f,0x20,0x77,0x6f,0x72,0x6c,0x64,0xa}; char code_backup[CODE_SIZE]; struct user_regs_struct oldregs,regs; long ins; if(ptrace(PTRACE_ATTACH,tracee,NULL,NULL) == -1) { perror("attach error"); } wait(NULL); puts("attach success"); ptrace(PTRACE_GETREGS,tracee,NULL,&regs); long addr = freeSpaceAddr(tracee); // long addr = regs.rip; printf("find free addr %lx\n",addr); getdata(tracee,addr,code_backup,CODE_SIZE); putdata(tracee,addr,code_inject,CODE_SIZE); memcpy(&oldregs,&regs,sizeof(regs)); regs.rip = addr; printf("new rip :%llx\n",regs.rip); if(ptrace(PTRACE_SETREGS,tracee,NULL,&regs) == -1) { perror("set regs error"); } puts("replace instructions success, continue tracee"); if(ptrace(PTRACE_CONT,tracee,NULL,NULL) == -1) { perror("continue tracee error"); } wait(NULL); ptrace(PTRACE_GETREGS,tracee,NULL,&regs); printf("tracee end at rip: %llx\n",regs.rip); puts("tracee has stopped, putting back original instructions"); putdata(tracee,addr,code_backup,CODE_SIZE); if(ptrace(PTRACE_SETREGS,tracee,NULL,&oldregs) == -1) { perror("put original instuctions error"); } ptrace(PTRACE_DETACH,tracee,NULL,NULL); return 0; }

 

以上我们完成了设置断点和代码注入的功能。在老外文章 playing with ptrace 2 中还有一小节。是讲我们之前的代码注入是将代码注入到tracee的代码空间里,这样做不是很好。可以通过 搜索/proc/pid/maps文件找到一块空闲空间进行代码注入。不过老外没有讲具体的搜索空闲内存空间的原理,按照他的代码我没有成功。有成功的朋友欢迎补充。

 

linux ptrace II