This post is also available in: English (英語)
概要
筆者がAutoItでコンパイルされたマルウェアのサンプルを分析していると、「Exe2Aut逆コンパイラを使用するとサンプルが実行される可能性がある」という内容のメッセージボックスがポップアップ表示されました。これを見て、逆コンパイラの仕組みやそもそもAutoItスクリプトがどのようにコンパイルされているのるかに興味を持ちました。そこで本稿では、最もよく利用されている2つのAutoIt逆コンパイラ、Exe2AutとmyAut2Exeの動作原理と、これらの逆コンパイラが本物のスクリプトではなくおとりのスクリプトを逆コンパイルさせられてしまう仕組みについて解説したいと思います。
図1 Exe2Autに表示された免責事項。
「改変された、もしくはAutoIt以外で作成されたファイルをドロップしてExe2Autに変換した場合、ドロップされたコードが実行される可能性があります。セキュリティを高めるため、仮想環境を利用するか、サンドボックスツール(Sandboxieなど)を利用されることをおすすめします」と表示されている
「コンパイル済み」AutoIt実行可能ファイルとは
「コンパイル済みAutoIt実行可能ファイル」とはようするに「スタンドアロンのAutoItインタープリタ」と「PEファイルにリソースとして存在するコンパイルされたスクリプトのバイトコード」という2つの部分から構成されるファイルです。AutoItの作成者は、簡単には逆コンパイルできないよういくつか対策を講じており、バイトコードにある種の圧縮や暗号化を施しています。コンパイル済みAutoIt実行可能ファイルは、まず圧縮されたバイトコードを展開し、それをインタープリタに与えて実行します。
Exe2Aut逆コンパイラを分析してみる
逆コンパイルの実行中にExe2Autを動的に分析してみると次のことがわかります。
- .tmp ファイルが %TEMP% フォルダに書き出される
- 生成されたバイナリはExe2Autの子プロセスとしてロードされる
- .tmp ファイルは生成されたバイナリにインジェクトされる
- 生成されたバイナリは逆コンパイル済みAutoItスクリプトをその時点の作業ディレクトリに書き込む
以上より、Exe2Autは、埋め込みインタープリタにスクリプトバイトコードを復号・展開させていること、その結果できたスクリプトバイトコードを抽出する処理を、生成されたバイナリにダイナミックリンクライブラリ(DLL)をインジェクトすることによって実現していることが言えます。ここでは、バイトコードを実行する関数をフックし、バイトコードを復号して関数名に戻す、という動的なアプローチをとっていることになります。つまり、インジェクションを検出するコードを追加すれば、その動作を変更させられるということになります。そうすれば、Exe2Autを騙し、アプリケーションの実行時に実行される実際のスクリプトではなく、おとりのスクリプトを逆コンパイルさせることが可能になります。
図2 Exe2Autのインジェクトされたモジュール
MyAut2Exeの場合はどうか
Exe2Autとは異なり、MyAut2Exeは組み込みインタープリタに頼らずにバイトコードリソースの抽出、アンパック、展開を行います。つまりMyAut2Exeは完全に静的な逆コンパイラなので、誤って別のものを実行するというリスクはありません。
MyAut2ExeはExe2Autよりも高機能な逆コンパイラで、AutoItやAutoHotkeyのコンパイル済みスクリプトを複数バージョンサポートしています。ですからその分、コンパイル済みスクリプトコードの抽出・アンパック関連設定が多くなっています。これらの設定には、正確に行う手間を省くための「自動化」機能が提供されていて、この機能を使うと、スクリプトがうまく逆コンパイルされるまで逆コンパイラの設定をブルートフォース的に適用していきます。「自動化」機能を使用した場合、MyAut2Exeは実行可能ファイルをパースしてAutoItのマジックバイトコードシグネチャを探し、シグネチャが見つかったところでコードを抽出して逆コンパイルを行います。つまり、マジックバイトコードのシーケンスが最初に出現した時点でパースと逆コンパイルは停止するということですから、本物のコンパイル済みスクリプトリソースより手前のオフセット位置におとりスクリプトを配置しておきさえすれば、MyAut2Exeにおとりの方を逆コンパイルさせることができてしまいます。つまり、MyAut2Exeについても騙すのは簡単です。
実証してみる
理論上はうまくいきそうですが、サイバーセキュリティの世界では理論よりも概念実証(PoC)のほうがずっと重んじられているものです。
そこで実証実験では、3つの異なるバイトコードをもつコンパイル済みAutoIt実行可能ファイルを用意することにしました。これをExe2AutないしMyAut2Exeで逆コンパイルすれば、本物のコードではなくおとりスクリプトのどれか1つが逆コンパイルされるはずです。
先に説明したとおり、MyAut2Exe用のおとりスクリプトは本物のバイトコードより手前のオフセット位置に配置しておきます。Exe2Autについては、おとり用・本物用のスクリプトリソース名をランタイムに変更することで、おとりコードの方を逆コンパイルさせることにします。
ということで、3つの異なるAutoItスクリプトをコンパイルし、それらをリソースとして.rsrcセクションに追加しました。うち2つはおとりスクリプトで、3つ目が本物のスクリプトです。その後、PE (Portable Executable) ヘッダの.rsrcセクションのパーミッション設定を読み取り/書き込みにします。
図3 本物とおとりのスクリプトリソース
次に筆者は、アセンブラで書いた短いシェルコードを作成し、それでPEB(プロセス環境ブロック)のPEB_LDR_DATA構造体を走査して、Exe2AutがインジェクトしたDLLの存在を確認することにしました。DLLはインジェクト前にランダムなファイル名でWindowsの%TEMP%ディレクトリに配置されますので、ディスク上の(UPXパックされた)DLLを検索することでも同じ確認は行えますが、ここで前者のアプローチをとった理由は、そのほうが信頼性が高く検出が難しいためです。PEB内の全ロードジュールを走査してセクション名.UPX0をもつロードモジュールの有無を確認するのは、Exe2Autがインジェクトしたモジュールを識別する上でよりエレガントな方法といえます。というのも、ふつう他のDLLがUPXでパックされていることはないからです。さらにこの方法なら実証で使用したバージョンだけでなく、場合によってはカスタム逆コンパイラを含め、多数のExe2Autバージョンを検出できる可能性があります。
さて、シェルコードを作成したら、今度は準備した実行可能ファイル内のどこにこのシェルコードをインジェクトするかを決める必要があります。.textセクションの最後に約210バイト分の「コードの洞窟」(code cave: プロセス内のメモリ領域のうちnullが連続している領域)が見つかりました。今回のシェルコードはここに容易に収まります。シェルコードを実行するにあたっては、IsDebuggerPresentの呼び出し直後にシェルコードにジャンプし、実行が完了したら通常の実行フローに戻すことにしました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
saveRegisters: push ecx push ebx push edx checkInjected: mov ebx, fs:[0x30] ; Get PEB address mov ebx, [ebx+0xC] ; Get LDR Table address mov ebx, [ebx+0x14] ; first entry of LDR table. (the first entry is the that of the executable) mov edx, [ebx+0x10] ; Store the offset in edx ; I need this later to calculate the offsets ; of the resource names nextModule: mov ebx, [ebx] ; Get address of next LDR entry mov ecx, dword ptr ds:[ebx+0x28] ; Pointer of the module name test ecx, ecx ; If the pointer is OxO it means we have reached ; the end of our LDR table and we want to je restoreRegisers ; continue normal execution mov ecx,dword ptr ds:[ebx+0x10] ; Get the modules base offset mov ecx, dword ptr ds:[ecx+0x178] ; load a dword from the modules base offset+0x178 cmp ecx,0x30585055 ; and check if it is "UPX0" je swapResouces jmp nextModule swapResouces: mov byte ptr ds:[edx+ 0xc7656], 0x49 ; Replace the '1' for a 'I' in "SCR1PT" mov byte ptr ds:[edx+ 0xc765e] , 0x35 ; Replace the 'S' for a '5' in "SCRIPT" restoreRegisers: pop edx pop ebx pop ecx test eax, eax ; Restore the instructions that were overwritten by jnz debugerIsPresent ; the jump to the codecave jmp debugerNotPresent ; Return to the normal program flow |
図3 アセンブラで書いたシェルコード(70 バイト)
図4 実証ビデオ
結論
この概念実証から学べることは、「自分が使っているツールの出力内容を盲信してはいけない」ということでしょう。リバースエンジニリングを行う皆さんは、自分のツールがどのように機能するのかや、それらがどのように騙され、誤った結果を返してしまう可能性があるのかについて認識しておく必要があります。なお、ここで紹介した手法は2つの逆コンパイラに誤った結果を出力させる可能性はありますが、それがサンドボックスによる動的分析の結果に影響することはありません。
脅威防止プラットフォームTraps、Cortex XDR™、WildFireの動的分析では、上に説明したような良性のスクリプトからの悪意のある振る舞いを検出し、実行を防止することができます。