Linux Ptrace 详解

来源:互联网 发布:怎么js调用方法 编辑:程序博客网 时间:2024/05/20 15:12

一、系统调用

操作系统提供一系列系统调用函数来为应用程序提供服务。关于系统调用的详细相关知识,可以查看<<程序员的自我修养》第十二章。
对于x86操作系统来说,用中断命令“int 0x80”来进行系统调用,系统调用前,需要将系统调用号放入到%EAX寄存器中,将系统的参数依次放入到寄存器%ebx%ecx%edx以及%esi%edi中。

以write系统调用为例:

write(2,"Hello",5);

在32位系统中会转换成:

movl $1,%eaxmovl $2,%ebxmovl $hello,%ecxmovl $5,%edxint $0x80

其中1为write的系统调用号,所有的系统调用号定义在unistd.h文件中,$hello 表示字符串“Hello”的地址;32位Linux系统通过0x80中断来进行系统调用。

64位系统用户应用层用整数寄存器%rdi ,%rsi,%rdx,%rcx, %r8以及 %r9来传参。而内核接口用%rdi ,%rsi,%rdx,%r10,&r8以及%r10来传参,并且用syscall指令而不是80中断进行系统调用。
x86和x64都用寄存器rax来保存调用号和返回值。

二、ptrace 函数简介

#include <sys/ptrace.h>long ptrace(enum _ptrace_request request,pid_t pid,void * addr ,void *data);

ptrace()系统调用函数提供了一个进程(the “tracer”)监察和控制另一个进程(the “tracee”)的方法。并且可以检查和改变“tracee”进程的内存和寄存器里的数据。它可以用来实现断点调试和系统调用跟踪。

tracee首先需要被附着到tracer。在多线程进程中,每个线程都可以被附着到一个tracer。ptrace命令总是以ptrace(PTARCE_foo,pid,..)的形式发送到tracee进程。pid是tracee线程ID。

当一个进程可以开始跟踪进程通过调用fork函数创建子进程并让子进程执行PTRACE_TRACEME,然后子进程再调用execve()(如果当前进程被ptrace,execve()成功执行后 SIGTRAP信号量会被发送到该进程)。一个进程也可以使用”PTRACE_ATTACH”或者”PTRACE_SEIZE”来跟踪另一个进程。

当进程被跟踪后,每当信号量传来,甚至信号量会被忽略时,tracee会暂停。tracer会在下次调用waitpid(wstatus)(或者其它wait系统调用)处被通知。该调用会返回一个包含tracee暂停原因信息的状态码。当tracee暂停后,tracer可以使用一系列ptrace请求来查看和修改tracee中的信息。tracer接着可以让tracee继续执行。tracee传递给tracer中的信号量通常被忽略。
当PTRACE_O_TRACEEXEC项未起作用时,所有成功执行execve()的tracee进程会被发送一个 SIGTRAP信号量后暂停,在新程序执行之前,父进程将会取得该进程的控制权。

当tracer结束跟踪后,可以通过调用PTRACE_DETACH继续让tracee执行。

prace更多相关信息可以查看http://man7.org/linux/man-pages/man2/ptrace.2.html官方文档。

三、示例

1.ptrace追踪子进程执行exec()

#include <stdio.h>#include <unistd.h>#include <sys/ptrace.h>#include <sys/types.h>#include <sys/wait.h>#include <sys/reg.h>   /* For constants ORIG_RAX etc */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);   }}

编译后输出:

The child made a system call 59user1@user-virtual-machine:~/hookTest$ a.out    attach.c~  ex1.c   ex1.o  ex2.c~  ex3.c   ex3.o  ex4.c~    victim.c~attach.c  attach.o   ex1.c~  ex2.c  ex2.o   ex3.c~  ex4.c  victim.c  victim.o

execl()函数对应的系统调用为__NR_execve,系统调用值为59。父进程通过调用fork()来创建子进程。在子进程中,先运行patrce().请求参数设为PTRACE_TRACE,来告诉内核当前进程被父进程trace,每当有信号量传递到当前进程,该进程会暂停,提醒父进程在wait()调用处继续执行。然后再调用execl()。当execl()函数成功执行后,新程序运行之前,SIGTRAP信号量会被发送到该进程,让子进程停止,这时父进程会在wait相关调用处被通知,获取子进程的控制权,可以查看子进程内存和寄存器相关信息。

当进程进行系统调用时,int会在内核栈中依次压入用户态的寄存器SS、ESP、EFLAGS、CS、EIP.中断处理程序的SAVE_ALL宏会将 依次将EAX、EBP、EDI、ESI、EDX、ECX、EBX寄存器值压入内核栈。调用ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL) 获取USER area信息时<sys/reg.h>文件定义了与内核栈寄存器数组顺序相同的下标:

#ifndef _SYS_REG_H#define _SYS_REG_H  1#ifdef __x86_64__/* Index into an array of 8 byte longs returned from ptrace for   location of the users' stored general purpose registers.  */# define R15    0# define R14    1# define R13    2# define R12    3# define RBP    4# define RBX    5# define R11    6# define R10    7# define R9 8# define R8 9# define RAX    10# define RCX    11# define RDX    12# define RSI    13# define RDI    14# define ORIG_RAX 15# define RIP    16# define CS 17# define EFLAGS 18# define RSP    19# define SS 20# define FS_BASE 21# define GS_BASE 22# define DS 23# define ES 24# define FS 25# define GS 26#else/* Index into an array of 4 byte integers returned from ptrace for * location of the users' stored general purpose registers. */# define EBX 0# define ECX 1# define EDX 2# define ESI 3# define EDI 4# define EBP 5# define EAX 6# define DS 7# define ES 8# define FS 9# define GS 10# define ORIG_EAX 11# define EIP 12# define CS  13# define EFL 14# define UESP 15# define SS   16#endif

这样8*ORIG_RAX就找到USER area 中 ORIG_RAX 寄存器值的保存地址。ORIG_RAX保存了系统调用号。

当检查完系统调用之后,可以调用ptrace并设置参数PTRACE_CONT让子进程继续进行。

2.读取子进程系统调用参数

//64位下乌班图程序#include <sys/ptrace.h>#include <sys/wait.h>#include <sys/reg.h>#include <sys/user.h>#include <sys/syscall.h>#include <stdio.h>int main(){    pid_t child;    long orig_rax;    int status;    int iscalling=0;    struct user_regs_struct regs;    child = fork();        if(child==0){          ptrace(PTRACE_TRACEME,0,NULL,NULL);      execl("/bin/ls","ls","-l","-h",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){                    ptrace(PTRACE_GETREGS,child,NULL,&regs);            if(!iscalling){                iscalling =1;                printf("SYS_write call with %lld, %lld, %lld\n",regs.rdi,regs.rsi,regs.rdx);                        } else{                               printf("SYS_write call return %lld\n",regs.rax);                               iscalling = 0;                        }                                                  }            ptrace(PTRACE_SYSCALL,child,NULL,NULL);          }       }          return 0;}

编译后输出:

SYS_write call with 1, 140179049189376, 14总用量 28KSYS_write call return 14SYS_write call with 1, 140179049189376, 51-rw-rw-r-- 1 user1 user1  534  226 18:02 ex1.cSYS_write call return 51SYS_write call with 1, 140179049189376, 52-rw-rw-r-- 1 user1 user1  534  226 18:02 ex1.c~SYS_write call return 52SYS_write call with 1, 140179049189376, 53-rw-rw-r-- 1 user1 user1 1.1K  32 13:02 hook2.cSYS_write call return 53SYS_write call with 1, 140179049189376, 54-rw-rw-r-- 1 user1 user1 1.1K  32 13:02 hook2.c~SYS_write call return 54SYS_write call with 1, 140179049189376, 53-rwxrwxr-x 1 user1 user1 8.6K  32 13:02 hook2.oSYS_write call return 53

可以看到ls -l -h 执行了六次SYS_write系统调用。
读取寄存器中的参数时,可以使用PTRACE_PEEKUSER一个字一个字读取,也可以使用PTRACE_GETREGS参数直接将寄存器的值读取到结构体user_regs_struct 中,该结构体定义在sys/user.h

对于PTRACE_STSCALL参数,该参数会像PTRACE_CONT一样使暂停的子进程继续执行,并在子进程下次进行系统调用前或系统调后,向子进程发送SINTRAP信号量,让子进程暂停。

WIFEXITED函数(宏)函数用来检查子进程是暂停还准备退出。

3.修改子进程系统调用参数

val = ptrace(PTRACE_PEEKDATA,child,addr,NULL)

PTRACE_PEEKDATAPTRACE_PEEKTEXT参数是在tracee内存的addr地址处读取一个字(sizeof(long))的数据,反回值是long 型的,可多次读取addr
+i*sizeof(long)然后再合并得到最终字符串的内容。

现在,我们对系统调用write 输出的字符串参数进行反转:

#include <sys/ptrace.h>#include <sys/wait.h>#include <sys/reg.h>#include <sys/syscall.h>#include <sys/user.h>#include <stdio.h>#include <string.h>#include <errno.h>#include <stdlib.h>#define long_size sizeof(long)void reverse(char * str){    int i,j;    char temp;    for(i=0,j=strlen(str)-2;i<=j;++i,--j){          temp=str[i];          str[i]=str[j];          str[j]=temp;    }}void getdata(pid_t child,long addr,char * str,int len){  char * laddr;  int i,j;  union u{    long val;        char chars[long_size];  } data;  i=0;  j=len/long_size;  laddr=str;  while(i<j){   data.val=ptrace(PTRACE_PEEKDATA,child,addr+i*long_size,NULL);   if(data.val == -1){     if(errno){        printf("READ error: %s\n",strerror(errno));     }   }     memcpy(laddr,data.chars,long_size);     ++i;     laddr +=long_size;  };  j=len % long_size;  if(j!=0){     data.val=ptrace(PTRACE_PEEKDATA,child,addr+i*long_size,NULL);     memcpy(laddr,data.chars,j);  }   str[len]='\0';}void putdata(pid_t child,long addr,char * str,int len){    char * laddr;        int i,j;        union u{          long val;          char chars[long_size];       } data;       i=0;       j=len /long_size;       laddr=str;       while(i<j){           memcpy(data.chars,laddr,long_size);           ptrace(PTRACE_POKEDATA,child,addr +i*long_size,data.val);           ++i;           laddr+=long_size;     }     j=len%long_size;     if(j!=0){              //注意:由于写入时也是按字写入的,所以正确的做法是先将该字的高地址数据读出保存在data的高地址上 ,然后将该字再写入           memcpy(data.chars,laddr,j);           ptrace(PTRACE_POKEDATA,child,addr +i*long_size,data.val);     }}int main(){    pid_t child;        int status;        struct user_regs_struct regs;        child =fork();        if(child ==0){          ptrace(PTRACE_TRACEME,0,NULL,NULL);          execl("/bin/ls","ls",NULL);       }else{           long orig_eax;           char *str,*laddr;           int toggle =0;           while(1){             wait(&status);             if(WIFEXITED(status))                 break;             orig_eax = ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);             if(orig_eax == SYS_write){               if(toggle == 0){                  toggle =1;                 ptrace(PTRACE_GETREGS,child,NULL,&regs);                 str=(char * )calloc((regs.rdx+1),sizeof(char));                 getdata(child,regs.rsi,str,regs.rdx);                 reverse(str);                 putdata(child,regs.rsi,str,regs.rdx);              }else{              toggle =0;               }            }         ptrace(PTRACE_SYSCALL,child,NULL,NULL);          }       }      return 0;}

输出:

user1@user-virtual-machine:~/hookTest$ ./hook3.oo.3kooh  ~c.3kooh  c.3kooh  o.2kooh  ~c.2kooh c.2kooh  ~c.1xe  c.1xe

4.向其它程序注入指令

我们追踪其它独立运行的进程时,需要使用下面的命令:

ptrace(PTRACE_ATTACH, pid, NULL, NULL)

使pid进程成为被追踪的tracee进程。tracee进程会被发送一个SIGTOP信号量,tracee进程不会立即停止,直到完成本次系统调用。如果要结束追踪,则调用PTRACE_DETACH即可。

debug 设置断点的功能可以通过ptrace实现。原理是ATTACH正在运行的进程使其停止。然后读取该进程的指令寄存器IR(32位x86为EIP,64w的是RIP)内容所指向的指令,备份后替换成目标指令,再使其继续执行,此时被追踪进程就会执行我们替换的指令,运行完注入的指令之后,我们再恢复原进程的IR
,从而达到改变原程序运行逻辑的目的。

tracee进程代码:

stdio.h>int main(){        int i=0;    while(1){            printf("Hello,ptrace! [pid:%d]! num is %d\n",getpid(),i++);                sleep(2);      }      return 0;}

tracer进程代码

#include<sys/ptrace.h>#include<sys/reg.h>#include<sys/wait.h>#include<sys/user.h>#include<stdlib.h>#include<errno.h>#include<string.h>#include<stdio.h>#define long_size sizeof(long)void getdata(pid_t child, long addr ,char * str,int len){    char * laddr =str;    int i,j;    union u{          long  val;          char   chars [long_size] ;        } data;    i=0;    j=len/long_size;     while(i<j){           data.val=ptrace(PTRACE_PEEKDATA,child,addr + long_size*i,NULL);           if(data.val==-1){        if(errno){                  printf("READ error: %s\n",strerror(errno));                }           }           memcpy(laddr,data.chars,long_size);            ++i;            laddr=laddr+long_size;        }    j= len %long_size;    if(j!=0){      data.val=ptrace(PTRACE_PEEKDATA,child,addr + long_size*i,NULL);      if(data.val==-1){        if(errno){                  printf("READ error: %s\n",strerror(errno));                }           }      memcpy(laddr,data.chars,j);    }    str[len]='\0';}void putdata(pid_t child , long addr,char * str,int len){    char * laddr =str;    int i,j;    j=len/long_size;    i=0;    union u{           long val;           char chars [long_size]  ;    } data;    while(i<j){         memcpy(data.chars,laddr,long_size);         ptrace(PTRACE_POKEDATA,child,addr + long_size*i,data.val);         ++i;         laddr=laddr+long_size;    }    j=len%long_size;    if(j!=0){        data.val= ptrace(PTRACE_PEEKDATA,child,addr + long_size*i,NULL);        if(data.val==-1){           if(errno){                  printf("READ error: %s\n",strerror(errno));                }        }         memcpy(data.chars,laddr,j);         ptrace(PTRACE_POKEDATA,child,addr + long_size*i,data.val);        }     }int main(int argc,char * argv[]){     if(argc!=2){         printf("Usage: %s pid\n",argv[0]);     }     pid_t tracee = atoi(argv[1]);     struct user_regs_struct regs;     /*int 80(系统调用) int 3(断点)*/     unsigned char code[]={0xcd,0x80,0xcc,0x00,0,0,0,0}; //八个字节,等于long 型的长度     char backup[8]; //备份读取的指令     ptrace(PTRACE_ATTACH,tracee,NULL,NULL);     long inst;  //用于保存指令寄存器所指向的下一条将要执行的指令的内存地址       wait(NULL);      ptrace(PTRACE_GETREGS,tracee,NULL,&regs);     inst  =ptrace(PTRACE_PEEKTEXT,tracee,regs.rip,NULL);      printf("tracee:RIP:0x%llx INST: 0x%lx\n",regs.rip,inst);     //读取子进程将要执行的 7 bytes指令并备份     getdata(tracee,regs.rip,backup,7);     //设置断点     putdata(tracee,regs.rip,code,7);     //让子进程继续执行并执行“int 3”断点指令停止     ptrace(PTRACE_CONT,tracee,NULL,NULL);     wait(NULL);     long rip=ptrace(PTRACE_PEEKUSER,tracee,8*RIP,NULL);//获取子进程停止时,rip的值     long inst2=ptrace(PTRACE_PEEKTEXT,tracee,rip,NULL);     printf("tracee:RIP:0x%lx INST: 0x%lx\n",rip,inst2);     printf("Press Enter to continue  tracee process\n");     getchar();     putdata(tracee,regs.rip,backup,7); //重新将备份的指令写回寄存器     ptrace(PTRACE_SETREGS,tracee,NULL,&regs);//设置会原来的寄存器值      ptrace(PTRACE_CONT,tracee,NULL,NULL);     ptrace(PTRACE_DETACH,tracee,NULL,NULL);     return 0;}

先运行tracee.o 文件

$  ./tracee.o

此时tracee.o输出:

Hello,ptrace! [pid:14384]! num is 0Hello,ptrace! [pid:14384]! num is 1Hello,ptrace! [pid:14384]! num is 2Hello,ptrace! [pid:14384]! num is 3......

再另打开一个shell运行attach.o文件

$  ./.attach.o  14384 //pid

此时tracee.o执行到int 3断点指令停止,attach1,o输出:

tracee:RIP:0x7f48b0394f20 INST: 0x3173fffff0013d48tracee:RIP:0x7f48b0394f23 INST: 0x8348c33100000000Press Enter to continue  tracee process

按任意键tracee.o恢复执行

参考:

http://www.cnblogs.com/pannengzhi/p/5203467.html

0 0
原创粉丝点击