API名のハッシュ化テクニックを理解せよ!:リバースエンジニアリング入門(6)(2/3 ページ)
コンピュータウイルスの解析などに欠かせないリバースエンジニアリング技術ですが、何だか難しそうだな、という印象を抱いている人も多いのではないでしょうか。この連載では、「シェルコード」を例に、実践形式でその基礎を紹介していきます。(編集部)
新しい解析素材の取得
それでは本題に移りましょう。まずは新しい解析素材の取得です。第1回の記事を振り返りながら、シェルコードを取得します。なお、今回は「shell_bind_tcp」というシェルコードを解析します。
シェルコードの取得方法は第1回の記事で解説していますので、これを振り返りながら進めていきましょう。
スタートメニューから「Metasploit Framework」内の「Metasploit Console」を起動し、(1)「use」コマンドで利用するペイロード(シェルコード)をセットします。ここで、(2)「show options」コマンドで、設定できるオプションの一覧を確認します。設定が必須となっている項目(Requiredがyesとなっている項目)に値を設定しないとペイロードの出力ができないので、ここで確認しておきましょう。今回使うshell_bind_tcpのシェルコードの場合、設定が必要なオプションには初期値が設定してあるので、特に設定しなくても問題ありません。最後に、(3)「generate」コマンドでシェルコードをファイルに書き出します。
msf > use windows/shell_bind_tcp ……(1) msf payload(shell_bind_tcp) > show options ……(2) Module options (payload/windows/shell_bind_tcp): Name Current Setting Required Description ---- --------------- -------- ----------- EXITFUNC process yes Exit technique: seh, thread, process, none LPORT 4444 yes The listen port RHOST no The target address msf payload(shell_bind_tcp) > generate -t raw -f shell_bind_tcp.bin ……(3) [*] Writing 341 bytes to shell_bind_tcp.bin... msf payload(shell_bind_tcp) >
解析のヒントになりそうな文字列が見つからない!?
次に、取り出したシェルコードを、これまでの解析作業と同様にIDAに読み込ませましょう。「やり方を忘れてしまった!」という場合は第3回の記事を振り返ってみてください。
前回まで解析してきたdownload_execというシェルコードの場合は、先頭にデコーダが挿入されていましたが、shell_bind_tcpのシェルコードにはデコーダはありません。ですので、逆アセンブリの画面でコードを表示させるためにキーボードの「c」押してみると、全体がすんなりとコード表示に切り替わったかと思います。コードとして表示されない場所があったら、「c」を押してコードに変換してみてください。
download_execのシェルコードでは、デコードした後に「GetProcAddress」や「LoadLibraryA」といった文字列が確認できました。どのようなAPIが呼び出されるのかを見れば、やろうとしていることを大まかに把握できるので、こういった文字列は解析を行う際のヒントになります。ところが、shell_bind_tcpの場合はそういったヒントになりそうな文字列は見当たりません。
shell_bind_tcpの中身をもう少し見てみると、何やら“call ebp”の直前には、0x0726774Cや0x006B8029などの何らかの数値データが毎回pushされているようです。これはどういった意味があるのでしょうか?(図3)
文字列のハッシュ値を介したAPI呼び出しテクニック
“call ebp”の直前にpushされている4バイトのデータは、実は呼び出したいAPI名とそのAPIをエクスポートしているDLL名に基づくハッシュ値で、call ebpで呼び出される関数内で利用されています。ここでebpに格納されているアドレスは、シェルコードでおなじみのcall/popの命令の組み合わせを使って設定されており、0x00000006から始まる関数のエントリポイントになっています。
API名のハッシュ値を利用する関数は0x00000006から0x0000008Dまでです。もしこの関数を、IDAが関数として認識していない場合には、先に説明したように、0x00000006のところにカーソルを合わせて、キーボードの「p」を押してみましょう。
それでは、この関数を読み解きます。今回も前回と同様、説明の都合上、行番号を追加しています。前回までに解説してきた内容と類似している個所も多いので、復習しながら読んでみてください。
1: pusha ; 汎用レジスタの値をスタックに積む 2: mov ebp, esp ; espの値をebpに代入 3: xor edx, edx ; edxを0に初期化 4: mov edx, fs:[edx+30h] ; PEBのアドレスを取得 5: mov edx, [edx+0Ch] ; LoaderDataのアドレスを取得 6: mov edx, [edx+14h] ; InMemoryOrderModuleListのアドレスを取得 7: loc_15: 8: mov esi, [edx+28h] ; LDR_MODULEエントリ内のBaseDllName(UNICODE_STRING)のBufferを取得 9: movzx ecx, word ptr [edx+26h] ; LDR_MODULEエントリ内のBaseDllName(UNICODE_STRING)のMaximumLengthを取得 10: xor edi, edi ; ediを0に初期化 11: loc_1E: 12: xor eax, eax ; eaxを0に初期化 13: lodsb ; モジュール名(ワイド文字列)から1バイト読み込み、eaxに代入 14: cmp al, 61h ; ; 読み込んだ値を0x61と比較 15: jl short loc_27 ; al < 0x61ならloc_27にジャンプ 16: sub al, 20h ; ; al >= 0x61なら0x20を引く 17: loc_27: 18: ror edi, 0Dh ; ediの値を13ビット右ローテート 19: add edi, eax ; ediの値にeaxの値を加える 20: loop loc_1E ; BaseDllNameのMaximumLengthの回数分loc_1Eからの処理を繰り返す 21: push edx ; edx(InMemoryOrderModuleListのアドレス)をスタックに積む 22: push edi ; edi(モジュール名のハッシュ値)をスタックに積む 23: mov edx, [edx+10h] ; edxにモジュールのベースアドレスを代入
1行目でpusha命令が実行されています。この命令は、全汎用レジスタの値をスタックに積むための命令です。pusha命令とは逆に、スタックから値を取り出して全汎用レジスタに代入するpopa命令という命令もあります。関数の最初にpusha命令を実行し、関数の最後にpopa命令を実行すれば、関数内の処理で汎用レジスタの値を操作しても、関数から戻るときには汎用レジスタの値を関数実行前の状態に戻すことができます(popa命令を実行するときのespの値がpusha命令を実行した直後のespと同じ値であることが前提です)。
なお、pusha命令はeax、ecx、edx、ebx、esp、esi、ediの順にスタックに値を積みます。popa命令はちょうどpusha命令と逆の順番で、edi、esi、esp、ebx、edx、ecx、eaxの順に値を取り出します。
4行目から9行目は、PEB(Process Environment Block)を利用してモジュール情報を読み出すための処理で、第4回で解説しています。ただし、第4回の解説ではInInitializationOrderModuleList(初期化順のモジュール情報)のアドレスからLDR_MODULEのエントリを探してBaseAddress(モジュールがロードされているベースアドレス)を取得していましたが、今回の処理では、InMemoryOrderModuleList(メモリアドレス順のモジュール情報)を使っています(6行目)。BaseAddressの取得にどのモジュールリストを使うかによって、BaseAddressへのオフセット値が異なるので注意が必要です。
8行目、9行目では、LDR_MODULEのBaseDllNameという変数内の値を取得しています。ここで、LDR_MODULEの定義を再度確認しましょう。
なお、前述のとおり、今回はInMemoryOrderModuleListを使っているので、定義の左側にはInMemoryOrderModuleListからのオフセット値を記載しています。また、BaseDllNameの型はUNICODE_STRINGなので、こちらの定義も確認しておきましょう。
typedef struct _LDR_MODULE { LIST_ENTRY InLoadOrderModuleList; +00h LIST_ENTRY InMemoryOrderModuleList; +08h LIST_ENTRY InInitializationOrderModuleList; +10h PVOID BaseAddress; +14h PVOID EntryPoint; +18h ULONG SizeOfImage; +1Ch UNICODE_STRING FullDllName; +24h UNICODE_STRING BaseDllName; +2Ch ULONG Flags; ……(省略)…… } LDR_MODULE, *PLDR_MODULE;
typedef struct _UNICODE_STRING { +00h USHORT Length; +02h USHORT MaximumLength; +04h PWSTR Buffer; } UNICODE_STRING *PUNICODE_STRING;
UNICODE_STRINGの定義を踏まえると、8行目ではBaseDllNameのワイド文字列のポインタであるBuffer、9行目ではBaseDllNameのバッファ長であるMaximumLengthをそれぞれesi、ecxに代入していることになります。
11行目から16行目の処理は、モジュール名を1バイトずつ取り出し、小文字の場合は大文字に変換する、という処理になっています。ASCIIコードで“a”から“z”は0x61から0x7a、“A”から“Z”は0x41から0x5aです。そのため、取り出した値が小文字であれば0x61以上の値になり、大文字であれば0x61未満の値になるので、0x61との比較で大文字か小文字かを判別できます。さらに、大文字と小文字のASCIIコードの差は0x20ですから、小文字であれば0x20を引けば大文字に変換できます。
17行目から20行目では、ハッシュ値を格納するレジスタであるediを13ビット右にローテートし、ワイド文字列のデータを1バイト取り出してediに加える、という処理を繰り返しています。処理を大まかに図示すると、図4のようになります。この処理を行うことで、モジュール名を4バイトの値に変換します。
Copyright © ITmedia, Inc. All Rights Reserved.