ptraceを弄ぶ(一)


ptraceを弄ぶ(一)
http://blog.chinaunix.net/u/19651/showart_362901. 
by Pradeep Padala Created 2002-11-01 02:00翻訳:Magic.D E-mail:[email protected] Online Jundgeを開発する過程で、多くの資料を調べました。調合器の技術に関する資料はネット上では少ないです。UNIXプログラミングの大作『UNIX環境高級プログラミング』でも、その内容は多くないです。私がいるまでhttp://www.linuxjournal.com 上にこの文章を見つけました。もし至宝を得たら、特に翻訳します。拙者の技術文書を翻訳する初めての試みとして、きっと不手際が多いと思います。皆さん、我慢してください。
システムの呼び出しに対するブロッキングはどうやって実現したいですか?システムの呼び出しのパラメータを変えて、あなたのシステムを翻弄してみましたか?どのようにして運転中のプロセスを停止させ、コントロールすると思いますか?どうやって複雑なケネルプログラミングを使って目的を達成するかを考え始めるかもしれません。実際にLinuxは、これらを達成するための優雅なメカニズムを提供しています。ptraceは、親プロセスを他のプロセスを監視し制御する方法を提供し、また、サブプロセスのレジスタとカーネルイメージを変更することができ、したがって、ブレークポイント調整とシステム呼び出しの追跡を実現することができる。ptraceを使って、ユーザー層でブロックしたり、システム呼び出しを変更したりすることができます。この記事では、システム呼び出しをブロックする方法を学び、パラメータを変更します。本稿の第二部分では、より先進的な技術を学びます。ブレークポイントを設定し、コードを実行中のプログラムに挿入します。機械の中に潜り込んで、プロセスのレジスターとデータの部分をのぞき見てみます。  基本的な知識オペレーティングシステムは、システムコール(system cals)と呼ばれる、最下層のハードウェアおよびサービスに対するコントロールをプログラマに実現させる標準的なサービスを提供する。プログラムがシステム起動を必要とする場合、関連パラメータをシステム起動関連レジスタに入れ、ソフト中断0 x 80を呼び出します。この中断は、プログラムがカーネルモードに接触するウィンドウのように、プログラムがパラメータとシステム呼び出し番号をカーネルに渡し、カーネルがシステム呼び出しの実行を完了します。  i 386システムでは、システム番号は%eaxに入れられます。そのパラメータは%ebx,%ecx,%edx,%esi,%ediを順に入れます。例えば、以下の呼び出しで         Write(2,「ハロー」,5)  のアセンブリ形式は大体このようです。
    movl $4, %eax
    movl $2, %ebx
    movl $hello, %ecx
    movl $5, %edx
    int $0x80
 
ここのハローは標準文字列「ハロー」を指します。  では、ptraceはいつ現れますか?システム呼び出しを実行する前に、カーネルは現在のプロセスが「追跡」された状態にあるかどうかを確認します。そうであれば、カーネルは現在のプロセスを停止し、追跡プロセスに制御権を与えて、追跡プロセスを観察または修正することができるようにします。  例を見てみましょう。この追跡プログラムの過程を示します。
  # include < sys/ ptrace. h>
 # include < sys/ types. h>
 # include < sys/ wait. h>
 # include < unistd. h>
  # include < linux/ user. h> /* For constants
                                    ORIG_EAX etc */

 int main( )
  {
    pid_t child;
     long orig_eax;
     child = fork( ) ;
      if ( child = = 0) {
         ptrace( PTRACE_TRACEME, 0, NULL , NULL ) ;
         execl( "/bin/ls" , "ls" , NULL ) ;
     }
      else {
         wait( NULL ) ;
         orig_eax = ptrace( PTRACE_PEEKUSER,
                           child, 4 * ORIG_EAX,
                           NULL ) ;
         printf ( "The child made a "
                "system call %ld " , orig_eax) ;
         ptrace( PTRACE_CONT, child, NULL , NULL ) ;
     }
     return 0;
 }
このプログラムを実行すると、lsコマンドの結果を出力しながら出力されます。  The child made a system call 11  説明:11はexecveのシステム呼び出し番号で、このプログラムが呼び出された最初のシステム呼び出しです。システム呼び出し番号の詳細を知りたいです。/usr/include/asm/unistd.hを見てください。  以上の例では、親プロセスforkは、サブプロセスを作って追跡します。exec関数を呼び出す前に、サブプロセスはPTRACE_uを使います。TRACEMEは最初のパラメータとしてptrace関数を呼び出しました。カーネルに教えます。他の人に追跡させてください。その後、子プロセスがexecve()を呼び出した後、親プロセスにコントロールを返す。当時父のプロセスはwait()関数を使ってカーネルからの通知を待っていましたが、今は通知されました。それで、子プロセスが何をしているかを確認し始めます。レジスターの値を見たりします。  システムの呼び出しが発生したら、カーネルはeaxの値を保存します。PTRACE_を使ってもいいです。PEEKUSERはptraceの最初のパラメータとしてこの値を読みます。システムの呼び出し情報を確認してからPTRACE_を使用できます。CONTは、ptraceの最初のパラメータとして、ptraceを呼び出して、サブシステム呼び出しのプロセスを継続させる。  ptrace関数のパラメータ  Ptraceには4つのパラメータlong ptraceがあります。            pidut pid、            void*addr            void*data;;  最初のパラメータはptraceの挙動と他のパラメータの使用方法を決定しました。望ましい値はPTRACE_です。ME PTRACE_PEEKTEXT PTRACE_PEEKDATA PTRACE_PEEKUSER PTRACE_POKETEXT PTRACE_POKEDATA PTRACE_POKEUSER PTRACE_GETREGS PTRACE_GETFREGS,PTRACE_SETREGS PTRACE_SETFREGS PTRACE_CONT PTRACE_SYSSCALL,PTRACE_SINGLESTEP PTRACE_DETACHは、これらの定数の使用法を以下に説明する。  システム呼び出しのパラメータを読みだします。  PTRACE_を通じてPEEKUSERは、ptraceの最初のパラメータとして呼び出され、サブプロセスに関するレジスタ値を取得することができます。  まず次の例を見ます。# include < sys/ ptrace. h>
 # include < sys/ types. h>
 # include < sys/ wait. h>
 # include < unistd. h>
 # include < linux/ user. h>
  # include < sys/ syscall. h> /* For SYS_write etc */
 
 int main( )
  {
     pid_t child;
     long orig_eax, eax;
     long params[ 3] ;
     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_eax = ptrace( PTRACE_PEEKUSER,
                      child, 4 * ORIG_EAX, NULL ) ;
            if ( orig_eax = = SYS_write) {
               if ( insyscall = = 0) {
                  /* Syscall entry */
                 insyscall = 1;
                 params[ 0] = ptrace( PTRACE_PEEKUSER,
                                    child, 4 * EBX,
                                    NULL ) ;
                 params[ 1] = ptrace( PTRACE_PEEKUSER,
                                    child, 4 * ECX,
                                    NULL ) ;
                 params[ 2] = ptrace( PTRACE_PEEKUSER,
                                    child, 4 * EDX,
                                    NULL ) ;
                 printf ( "Write called with "
                        "%ld, %ld, %ld " ,
                        params[ 0] , params[ 1] ,
                        params[ 2] ) ;
                 }
            else { /* Syscall exit */
                 eax = ptrace( PTRACE_PEEKUSER,
                              child, 4 * EAX, NULL ) ;
                     printf ( "Write returned "
                            "with %ld " , eax) ;
                     insyscall = 0;
                 }
             }
             ptrace( PTRACE_SYSCALL,
                    child, NULL , NULL ) ;
         }
     }
     return 0;
 }


 
ppadala@linux: ~ / ptrace > ls
a. out dummy. s ptrace. txt
libgpm. html registers. c syscallparams. c
dummy ptrace. html simple. c
ppadala@linux: ~ / ptrace > . / a. out
Write called with 1, 1075154944, 48
a. out dummy. s ptrace. txt
Write returned with 48
Write called with 1, 1075154944, 59
libgpm. html registers. c syscallparams. c
Write returned with 59
Write called with 1, 1075154944, 30
dummy ptrace. html simple. c
Write returned with 30
 
上記の例では、writeシステムの呼び出しを追跡しましたが、lsコマンドの実行には三つのwriteシステムの呼び出しが発生します。PTRACE_を使用しますSYSSCALLはptraceの最初のパラメータとして、カーネルをサブシステム起動または終了の準備中に停止させます。この挙動はPTRACE_を使用しています。CONTは、その後、次のシステムの起動/プロセス終了時に停止することが等価である。  前の例ではPTRACE_を使っています。PEEKUSERは、システムの呼び出しパラメータを確認します。システム呼び出しの戻り値は%eaxに入れられます。  wait関数はstatus変数を使ってサブプロセスが終了したかどうかを確認します。サブプロセスがptraceによって停止されたか、それともすでに運行が終了したかを判断するために使用されます。一組のマクロはstatusの値でプロセスの状態を判断することができます。例えばWIFEXITEDなど、詳細はwait(2)manを観察することができます。  レジスタの値を読み出します。  システムが起動したり、プロセスが終了した時にレジスターを読み出したいなら、前の例の方法を使ってもいいですが、これは不器用な方法です。PRACE_を使用するGETREGSは、ptraceの最初のパラメータとして呼び出され、一度の関数呼び出しですべての関連レジスタ値を取得することができます。取得レジスタの価値例は以下の通りである。
  # include < sys/ ptrace. h>
 # include < sys/ types. h>
 # include < sys/ wait. h>
 # include < unistd. h>
 # include < linux/ user. h>
 # include < sys/ syscall. h>
 
 int main( )
  {
     pid_t child;
     long orig_eax, eax;
     long params[ 3] ;
     int status;
     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_eax = ptrace( PTRACE_PEEKUSER,
                             child, 4 * ORIG_EAX,
                             NULL ) ;
            if ( orig_eax = = SYS_write) {
                if ( insyscall = = 0) {
                   /* Syscall entry */
                  insyscall = 1;
                  ptrace( PTRACE_GETREGS, child,
                         NULL , & regs) ;
                  printf ( "Write called with "
                         "%ld, %ld, %ld " ,
                         regs. ebx, regs. ecx,
                         regs. edx) ;
              }
               else { /* Syscall exit */
                  eax = ptrace( PTRACE_PEEKUSER,
                               child, 4 * EAX,
                               NULL ) ;
                  printf ( "Write returned "
                         "with %ld " , eax) ;
                  insyscall = 0;
              }
           }
           ptrace( PTRACE_SYSCALL, child,
                  NULL , NULL ) ;
        }
    }
    return 0;
 }
このコードは前の例と比較して似ていますが、PTRACE_を使っています。GETREGSその中のuser_regs_struct構造は<linux/user.h>で定義されている。    面白いものをください  今はちょっと面白いことをします。私たちはwriteシステムに送って呼び出した文字列を反転させます。
  # include < sys/ ptrace. h>
 # include < sys/ types. h>
 # include < sys/ wait. h>
 # include < unistd. h>
 # include < linux/ user. h>
 # include < sys/ syscall. h>
 
 const int 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 * 4,
                           NULL ) ;
         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 * 4,
                           NULL ) ;
         memcpy ( laddr, data. chars, j) ;
     }
     str[ len] = '' ;
 }
 
 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 * 4, data. val) ;
         + + i;
         laddr + = long_size;
     }
     j = len % long_size;
      if ( j ! = 0) {
         memcpy ( data. chars, laddr, j) ;
         ptrace( PTRACE_POKEDATA, child,
                addr + i * 4, data. val) ;
     }
 }
 
 int main( )
  {
    pid_t child;
    child = fork( ) ;
     if ( child = = 0) {
       ptrace( PTRACE_TRACEME, 0, NULL , NULL ) ;
       execl( "/bin/ls" , "ls" , NULL ) ;
    }
     else {
       long orig_eax;
       long params[ 3] ;
       int status;
       char * str, * laddr;
       int toggle = 0;
        while ( 1) {
          wait( & status) ;
          if ( WIFEXITED( status) )
              break ;
          orig_eax = ptrace( PTRACE_PEEKUSER,
                            child, 4 * ORIG_EAX,
                            NULL ) ;
           if ( orig_eax = = SYS_write) {
              if ( toggle = = 0) {
                toggle = 1;
                params[ 0] = ptrace( PTRACE_PEEKUSER,
                                   child, 4 * EBX,
                                   NULL ) ;
                params[ 1] = ptrace( PTRACE_PEEKUSER,
                                   child, 4 * ECX,
                                   NULL ) ;
                params[ 2] = ptrace( PTRACE_PEEKUSER,
                                   child, 4 * EDX,
                                   NULL ) ;
                str = ( char * ) calloc ( ( params[ 2] + 1)
                                  * sizeof ( char ) ) ;
                getdata( child, params[ 1] , str,
                        params[ 2] ) ;
                reverse ( str) ;
                putdata( child, params[ 1] , str,
                        params[ 2] ) ;
             }
              else {
                toggle = 0;
             }
          }
       ptrace( PTRACE_SYSCALL, child, NULL , NULL ) ;
       }
    }
    return 0;
 }

 
ppadala@linux: ~ / ptrace > ls
a. out dummy. s ptrace. txt
libgpm. html registers. c syscallparams. c
dummy ptrace. html simple. c
ppadala@linux: ~ / ptrace > . / a. out
txt. ecartp s. ymmud tuo. a
c. sretsiger lmth. mpgbil c. llacys_egnahc
c. elpmis lmth. ecartp ymmud
この例では前に論じたすべての知識点が含まれています。もちろん新しい内容もあります。ここはPTRACEを使います。POKEDATAは最初のパラメータとしてサブプロセスの変数値を変更します。PTRACE_と一緒にしますPEEKDATAのような働き方はもちろん、変数の値をのぞき込むだけでなく、それらを修正することができます。  ステップ  ptraceはサブプロセスをシングルステップで行う機能を提供しています。ptraceは、サブプロセスの各コマンド実行前にカーネルをブロックし、親プロセスにコントロールを渡すことができます。次の例では、サブプロセスが現在実行されている命令を検出することができます。理解しやすくするために、私はアセンブリでこの制御されたプログラムを書きました。あなたがcのライブラリ関数のために一体それらのシステムの呼び出しをするのではなく、頭が痛いです。  以下は被制御プログラムのコードdummy 1.sで、gccを使用します。  –o dummy 1 dummy 1.sをコンパイルします。
  . data
hello:
    . string "hello world/n"
. globl main
main:
    movl $4, %eax
    movl $2, %ebx
    movl $hello, %ecx
    movl $12, %edx
    int $0x80
    movl $1, %eax
    xorl %ebx , %ebx
    int $0x80
    ret
以下のプログラムはシングルステップを完了するために使用されます。
  # include < sys/ ptrace. h>
 # include < sys/ types. h>
 # include < sys/ wait. h>
 # include < unistd. h>
 # include < linux/ user. h>
 # include < sys/ syscall. h>
 int main( )
  {
     pid_t child;
     const int long_size = sizeof ( long ) ;
     child = fork( ) ;
      if ( child = = 0) {
         ptrace( PTRACE_TRACEME, 0, NULL , NULL ) ;
         execl( "./dummy1" , "dummy1" , NULL ) ;
     }
      else {
         int status;
          union u {
             long val;
             char chars[ long_size] ;
         } data;
         struct user_regs_struct regs;
         int start = 0;
         long ins;
          while ( 1) {
             wait( & status) ;
             if ( WIFEXITED( status) )
                 break ;
             ptrace( PTRACE_GETREGS,
                    child, NULL , & regs) ;
              if ( start = = 1) {
                 ins = ptrace( PTRACE_PEEKTEXT,
                              child, regs. eip,
                              NULL ) ;
                 printf ( "EIP: %lx Instruction "
                        "executed: %lx " ,
                        regs. eip, ins) ;
             }
              if ( regs. orig_eax = = SYS_write) {
                 start = 1;
                 ptrace( PTRACE_SINGLESTEP, child,
                        NULL , NULL ) ;
             }
             else
                 ptrace( PTRACE_SYSCALL, child,
                        NULL , NULL ) ;
         }
     }
     return 0;
 }
プログラムの出力はこうです。
これらの命令コードの意味を理解するために、Intelのユーザーマニュアルを確認する必要があります。
より複雑なシングルステップ、たとえばブレークポイントを設定すると、綿密な設計とより複雑なコードが必要です。
 
 
第二の部分では、プログラムにブレークポイントを入れる方法と、実行中のプログラムにコードを挿入する方法を見ます。