Linux 系統調用 Ptrace 詳解

 

From:https://blog.csdn.net/u012417380/article/details/60470075

Ptrace 詳解:https://www.cnblogs.com/tangr206/articles/3094358.html

ptrace運行原理及使用詳解:https://blog.csdn.net/edonlii/article/details/8717029

 

 

一、系統調用

 

操作系統提供一系列 系統調用 函數 來爲應用程序提供服務。關於系統調用的詳細相關知識,可以查看 《程序員的自我修養》 第十二章。對於 x86 操作系統來說,用中斷命令 "int 0x80" 來進行系統調用,系統調用前,需要將系統調用號放入到 %EAX 寄存器中,將系統的參數依次放入到寄存器 %ebx、%ecx、%edx 以及 %esi 和 %edi 中。

write 系統調用 爲例:

write(2,"Hello",5);

在 32位系統中會轉換成:

movl $1,%eax
movl $2,%ebx
movl $hello,%ecx
movl $5,%edx
int $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 59
user1@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
總用量 28K
SYS_write call return 14
SYS_write call with 1, 140179049189376, 51
-rw-rw-r-- 1 user1 user1  534  2月 26 18:02 ex1.c
SYS_write call return 51
SYS_write call with 1, 140179049189376, 52
-rw-rw-r-- 1 user1 user1  534  2月 26 18:02 ex1.c~
SYS_write call return 52
SYS_write call with 1, 140179049189376, 53
-rw-rw-r-- 1 user1 user1 1.1K  3月  2 13:02 hook2.c
SYS_write call return 53
SYS_write call with 1, 140179049189376, 54
-rw-rw-r-- 1 user1 user1 1.1K  3月  2 13:02 hook2.c~
SYS_write call return 54
SYS_write call with 1, 140179049189376, 53
-rwxrwxr-x 1 user1 user1 8.6K  3月  2 13:02 hook2.o
SYS_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_PEEKDATA、PTRACE_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.o
o.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 0
Hello,ptrace! [pid:14384]! num is 1
Hello,ptrace! [pid:14384]! num is 2
Hello,ptrace! [pid:14384]! num is 3
......

再另打開一個shell運行attach.o文件

$  ./.attach.o  14384 //pid

此時tracee.o執行到int 3斷點指令停止,attach1,o輸出:

tracee:RIP:0x7f48b0394f20 INST: 0x3173fffff0013d48
tracee:RIP:0x7f48b0394f23 INST: 0x8348c33100000000
Press Enter to continue  tracee process

按任意鍵tracee.o恢復執行

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

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章