モジュール名のハッシュ値がどのように取得されているかが分かったら、シェルコードの続きを読み進めましょう。
24: mov eax, [edx+3Ch] ; IMAGE_DOS_HEADER.e_lfanewをeaxに代入 25: add eax, edx ; RVAをVAに変換 26: mov eax, [eax+78h] ; IMAGE_OPTIONAL_HEADER32.DataDirectory[0].VirtualAddressをeaxに代入 27: test eax, eax 28: jz short loc_89 ; エクスポート関数に関する情報がなければloc_89にジャンプ 29: add eax, edx ; RVAをVAに変換 30: push eax 31: mov ecx, [eax+18h] ; NumberOfNamesの値をecxに代入 32: mov ebx, [eax+20h] ; AddressOfNames(RVA)を代入 33: add ebx, edx ; AddressOfNamesのRVAをVAに変換 34: loc_4A: 35: jecxz short loc_88 ; ecxが0ならloc_88にジャンプ(全エクスポート関数名のチェックが終わるとecxは0になる) 36: dec ecx ; ecxを1減算 37: mov esi, [ebx+ecx*4]; エクスポート関数名の文字列へのポインタ(RVA)をesiに代入 38: add esi, edx ; RVAをVAに変換 39: xor edi, edi ; ediを0に初期化 ……(省略)…… 66: loc_88: 67: pop eax 68: loc_89: 69: pop edi 70: pop edx 71: mov edx, [edx] ; 次のモジュールのLDR_MODULEのアドレスを取得 72: jmp short loc_15 ; loc_15にジャンプし、次のモジュールに対して処理を実施
この部分の処理は、前回解説した内容を思い出せばすぐに理解できるでしょう。PEフォーマットを解釈してエクスポート関数に関する情報にたどり着いたら、AddressOfNamesを順番にたどってエクスポートしている関数名を読み込む、という処理です。
モジュールにエクスポート関数がない場合や、モジュールの全エクスポート関数に対して処理が終わった場合には、LDR_MODULEのLIST_ENTRYのFlinkを読み込み、次のモジュールに対して処理を実施するようになっています。
ちなみに、67行目のpop命令は30行目のpush命令に対応しています。これは、IDAでスタックポインタを表示する設定にしておけばすぐに確認できます。67行目のpop命令では、スタックポインタは0x2Cとなっています。
IDAの逆アセンブリ画面をみると、31行目からスタックポインタが0x2Cになっているのが分かります。この直前のpush命令で値がスタックに積まれたからです。従って、67行目のpop命令は30行目のpush命令で積まれた値を取り出している、と読み取ることができるのです(図1)。
それでは、残りの部分を読み解いてみましょう。ここから先も、大半は前回の内容の復習です。
40: loc_54: 41: xor eax, eax ; eaxを0に初期化 42: lodsb ; eaxにエクスポート関数名をeaxに1バイト読み込む 43: ror edi, 0Dh ; ediの値を13ビット右にローテート 44: add edi, eax ; ediの値にeaxを加える 45: cmp al, ah 46: jnz short loc_54 ; 文字列のすべての文字を処理するまでloc_54を繰り返す 47: add edi, [ebp-8] ; API名のハッシュ値(edi)にモジュール名のハッシュ値([ebp-8])を加える 48: cmp edi, [ebp+24h] ; 呼び出したいAPIのハッシュ値と比較 49: jnz short loc_4A ; 違っていたらloc_4Aに戻り、別のAPI名のハッシュ値を求める 50: pop eax ; IMAGE_EXPORT_DIRECTORYのアドレスをスタックから取り出してeaxに代入 51: mov ebx, [eax+24h] ; 序数テーブルのRVAをebxに代入 52: add ebx, edx ; RVAをVAに変換 53: mov cx, [ebx+ecx*2] ; 対象となるAPIの序数をcxに代入 54: mov ebx, [eax+1Ch] ; エクスポートアドレステーブルのRVAをebxに代入 55: add ebx, edx ; RVAをVAに変換 56: mov eax, [ebx+ecx*4] ; 序数を基にエクスポートアドレステーブルから対象となるAPIのアドレス(RVA)を取得 57: add eax, edx ; RVAをVAに変換 58: mov [esp+28h+var_4], eax ; APIのエントリポイントのアドレスをスタック上(後のpopaを実行したときにeaxに代入されるような位置)に書き込み 59: pop ebx ; スタックポインタの巻き戻し 60: pop ebx ; スタックポインタの巻き戻し 61: popa ; スタックに積んであった汎用レジスタの値を戻す 62: pop ecx ; 関数呼び出し時にスタックに積んだreturn addressをecxに取り出す 63: pop edx ; 関数呼び出し時に引数として積んでいたAPIのハッシュ値を取り出す 64: push ecx ; return addressをもう一度スタックに戻す 65: jmp eax ; 目的のAPIを呼び出す
41行目から46行目は、モジュール名のハッシュ値を求めたとき(11行目〜20行目)と同じように、ハッシュ値を格納するレジスタであるediを13ビット右にローテートし、文字列のデータを1バイトずつ取り出してediに加える、という処理を繰り返しています。ハッシュ値を取得する対象がモジュール名からAPI名に変わっただけです。ただ、API名はワイド文字列ではないことに注意しておいてください。
47行目では、先に求めておいたモジュール名のハッシュ値とAPI名から求めたハッシュ値の和を計算しています。この2つのハッシュ値の和が、この関数の引数として設定されているハッシュ値と一致する場合、IMAGE_EXPORT_DIRECTORYの序数テーブルとエクスポートアドレステーブルをたどり、呼び出したいAPIのエントリポイントのアドレスを取得します(50行目〜57行目)。IMAGE_EXPORT_DIRECTORYをたどる部分については前回解説したのと同じ方法です。
最終的には65行目のjmp eaxでAPIを呼び出すのですが、その前に、この関数で操作してきたスタックを巻き戻しています(59行目〜61行目)。
なお、popa命令を実行したときにAPIのエントリポイントのアドレスがeaxに代入されるように、あらかじめスタック上にAPIのアドレスを配置しています(58行目)。また、APIの呼び出し前に、この関数の引数としてスタックに積んであったAPIのハッシュ値を取り除き、あらかじめスタックに積んでおいたAPI自体が使う引数を使えるように処理しています(62行目〜64行目)。
ここまでで、どのようにしてハッシュ値が計算されるのかは理解できたかと思います。
解析を進めるに当たり、まずは先ほど読み解いたハッシュ値計算アルゴリズムを基に、ハッシュ値とAPI名を対応させたC言語形式の列挙型データを作りましょう。IDAでは、C言語のヘッダファイルを読み込ませることで解析を行いやすくできます。
列挙型データを定義したファイルを作成するPythonのサンプルスクリプト“get_hash.py”はこちらです。このサンプルスクリプトでは、シェルコードで比較的使われることが多いkernel32.dll、ws2_32.dll、urlmon.dllのエクスポート関数を確認し、ハッシュ値を計算します。計算した結果は、C言語のヘッダ形式のファイルとして出力します。
もし、これ以外のDLLがエクスポートしているAPIのハッシュ値が必要な場合は、いったんサンプルスクリプトで取得したAPIのハッシュ値を使ってシェルコードの解析を進め、LoadLibrary関数の呼び出し個所で引数を確認し、追加で必要となるDLLを把握しましょう。その後、必要なDLLについてもハッシュ値を取得すれば問題ありません。
このサンプルスクリプトを実行すると、次のように“hash_(モジュール名)_(API名)”=“ハッシュ値”というデータで構成される列挙型データの定義が作成されます。出力結果は後で使うので、リダイレクトしてファイルに保存しておきます。
C:\work\python get_hash.py enum API_HASH{ hash_kernel32_AcquireSRWLockExclusive=0xFD8452C6, hash_kernel32_AcquireSRWLockShared=0x74FE289C, ……中略…… hash_urlmon_WriteHitLogging=0x40924BA1, hash_urlmon_ZonesReInit=0xE6ECC7FF };
この出力結果をIDAに読み込ませるには、メニューから[File]→[Load file]→[Parse C header file…]とたどり、先ほど保存しておいたファイルを選びます。このとき、「Please setup the compiler options in Option->Compiler first」という警告が表示されたら、指示されたとおり、[Option]→[Compiler]とたどってコンパイラの設定をします。CompilerがUnknownになっていると思いますので、今回はとりあえずVisual C++を選んでおきます。
サンプルスクリプトで作成したヘッダファイルを正しく読み込ませることができたら、このデータを解析に活用できるよう、IDAの列挙型データ(enum)として登録します。
登録の方法ですが、まず、Enumsタブを選び、Insertキーを押すか、左上の「Define a new enumeration type」ボタンをクリックします。そうすると「Add enum type」というウィンドウが出てくるので、ここで「Add standard enum by enum name」ボタンをクリックし、「Please chose a enum」というウィンドウを表示させます。ここに、先ほど読み込ませたヘッダファイルに記載してあったenumのエントリがありますので、選んで「OK」を押しましょう(図5)。
IDAのenumへの登録が成功すれば、図6のように、モジュール/API名に対応するハッシュ値の一覧がEnumsウィンドウで確認できるはずです(登録されたenumデータが折りたたまれて表示されている場合がありますが、そのときはenumのエントリを選んで「+」キーを押すと展開されます)。
それでは、これらの値を逆アセンブリウィンドウでの解析に活用しましょう。
まずは、APIのハッシュ値を選択して、右クリックからコンテキストメニューを表示します。ハッシュ値と合致する値がenumにあれば、コンテキストメニューの[Symbolic constant]の先に、ハッシュ値に該当する[hash_(モジュール名)_(API名)]という文字列が現れます。これを選ぶことで、逆アセンブリウィンドウ中のハッシュ値が「hash_(モジュール名)_(API名)」という名前に変わります(図7)。
すべてのハッシュ値についてenumから読み込んだ名前に変更しておけば、これまで逆アセンブリの画面を見ていただけでは分からなかった、呼び出そうとしているAPIを容易に把握できるようになります。
今回は、ハッシュ値を使ってAPIを呼び出す方法を解説しました。ですが、shell_bind_tcpのシェルコードが全体としてどのような動作をするのかについては説明していません。
第1回から今回までの内容をしっかりと身に着けることができれば、きっとこのshell_bind_tcpというシェルコードの動作も理解できるはずです。ぜひ、shell_bind_tcpがどのような動作をするのかを自分の手で確かめてみてください。
次回もお楽しみに!
青木 一史(あおき かずふみ)
2006年 日本電信電話株式会社入社。セキュアプラットフォーム研究所(SC研)所属。入社以来、ハニーポット技術やマルウェア解析技術の研究開発に従事。最近はマルウェアの動的解析技術に関する研究を行っている。「アナライジング・マルウェア」の著者。
Copyright © ITmedia, Inc. All Rights Reserved.