マルウェア解析チュートリアル<マルウェア解析のはじめかた編>
今回はいつもと趣向を変えて、マルウェア解析の方法に焦点を当てていきたいと思います。
このチュートリアルはマルウェア解析の初心者やこれから学ぼうとしている方を対象としており、出来るだけ余計な付随情報には触れず必要最低限のポイントのみをおさえました。丁寧な解説を心がけたので冗長な部分も多少あるかと思いますが、一通り読んで手を動かしていただくと簡単な解析を始めるための手助けになるかと思います。
初心者におけるマルウェア解析の習得において筆者が最も近道だと思うのは、いきなりマルウェアを触るのではなく、自分で作成したプログラムを解析することです。プログラムを開発する側の目線がわかれば、マルウェア作者がなぜこうした処理を入れたのか(なぜこうした処理が必要なのか)などが自ずと見えてきます。また、自身が作成したプログラムなので処理の流れや全体の動きが最初から理解できているため、解析の敷居が幾分下がる上、WindowsのAPIの使い方も自然と覚えていけます。そうした観点では解析と開発は背中合わせともいえるでしょう。逆に言えば、プログラム開発の経験なしにマルウェア解析を行うのはかなりハードルが高いと言えます。ただし、開発のプロを目指すわけではないのでガチガチにコーディングを身に着ける必要は全くなく、解析の学習という観点に絞れば雑にでも動くものを作れるかどうかがまずは大事だと感じます。実際、マルウェア開発者のコーディングは細かい観点では雑なものが多く、あまり初めからその辺りを気にする必要はないでしょう。
以降では、最もベーシックなアンチデバッグ(デバッグ検知)のプログラムを自分で作成し、それを解析する方法について手順を踏まえながら詳細に解説していきます。
(なお、当然ですが本記事は不正プログラムの作成や著作権侵害を助長する目的ではありません。)
以降の内容は、C/C++がなんとなく読める程度の前提知識はあるとベターですが、前提知識が無くても何らかのプログラミング言語を少しでも触った経験があれば把握できるかと思います。
アンチデバッグプログラムの作成
はじめに、アンチデバッグプログラムの作成からスタートしましょう。
以下のようなアンチデバッグプログラムを作成した後、後半で解析の解説へ入っていきます。
今回作成するのは、デバッガで解析されている場合にのみ「Debugger Detected !」というメッセージボックスを表示する単純なプログラムです(下図)。
図 1 今回のチュートリアルで作成するアンチデバッグプログラム
まずは、無償のVisual Studio Community(またはライセンスを持っている場合はVisual Studio Professional等)をMicrosoftの公式ページからダウンロードしてインストールします。
インストールの際、以下の図のように「C++によるデスクトップ開発」にチェックを入れて「インストール」をクリックします(下図)。
図 2 Visual Studio Community 2019でのインストール画面
インストールが完了しVisual Studioを起動させると次のようなメニューが表示されます。以下の図の通り、「新しいプロジェクトの作成」→「空のプロジェクト(C++)」を選択し、任意のプロジェクト名をつけて「作成」ボタンを押します。
図 3 Visual StudioでC++の空のプロジェクトを作成
すると何もない状態のVisual Studioが開くので、右の「ソリューションエクスプローラー」のリストの中の「ソースファイル」を右クリックし、「追加」→「新しい項目」を選択します(下図)。
開いたウインドウで「C++ファイル(.cpp)」を選択して「追加」ボタンをクリックします(下図)。
これでソースコードが書ける状態になりました。
開いた何も書かれていないソースファイルに以下のプログラムコードを一旦貼り付けます。
(できれば1行ずつ意味を調べながら自分の手で写していくとなお良いです)
上のプログラムコードの解説は下図となりますが、C/C++に慣れていない場合も細かい部分は気にする必要はなく、文字の意味から推測できそうな部分を把握して読んでいくだけでもまずは良いでしょう。(コピペで貼り付けた場合も内容を読んで意味を把握する事が必要です)
2行目に「WinMain」という文字が見えますが、Windowsのプログラムは通常、WinMainという名前のメインとなる関数の中に処理を記述していきます。
「MessageBox」は文字通りメッセージボックスを表示させるWindowsのAPI(関数)で、""で囲っている文字を表示させます。
重要なポイントとして、コードの中にIf文があることがわかりますが、その中に何かの条件が書かれています。If文の条件には「IsDebuggerPresent()」と記載されており、なんらかの関数であることはなんとなく推測できますが、どういった関数でしょうか。
Windowsの関数(API)は覚え切れないほどあり、わからないAPIに遭遇した場合は常にインターネットで調べてみることが重要です。Googleで「IsDebuggerPresent」と検索するとMicrosoftの解説ページが出てきます(下図)。
該当ページを見ると「もしデバッガで動かされていなければ0を返す」と記載があります。つまり、IsDebuggerPresentという関数はプログラムがデバッガで動かされているかどうかを判断するためのWindows APIであり、0以外(実際は1)が返ってくるとデバッガで動かされている(言い換えると、デバッガで解析されている)と判断できる便利な関数であることがわかります。
図 7 IsDebuggerPresentをインターネットで調べてみる
IsDebuggerPresentというAPIを使用するテクニックは、アンチデバッグ(解析妨害)の中でも最もベーシックな部類になり、アンチデバッグの解析を初めて学ぶには非常に適した題材の一つです。
では、さっそくEXEファイルを作成(ビルド)していきますが、その前にいくつか事前準備を行います。
まず、Visual Studioの上のメニューから「Release」を選択します(下図)。
デフォルトでは「Debug」が選択されていますが、そのままでは余計な情報がEXEに入ってしまうため、最初に「Release」にしておきます(以降の複数の設定を行う前に設定しておきます)。
続いて、メニューの「プロジェクト」→「〜のプロパティ」を選択(〜にはプロジェクト名が入ります)し、以下の図のように「構成プロパティ」→「C/C++」→「コード生成」にある「ランタイムライブラリ」を「マルチスレッド(/MT)」にします(下図)。
デフォルト設定ではEXEの動作に必要なDLLを外部から読み込みますが、この設定をしておくことでEXEファイルにDLLが埋め込まれた状態で作成できるため、どの環境でも動くようにしたい場合は必ずこの設定をしておきます。
次に、「リンカー」→「システム」の「サブシステム」を「Windows(/SUBSYSTEM:WINDOWS)」に設定します(下図)。この設定を行わないとWinMain関数が認識されずコンパイルエラーが発生するため、WinMain関数を持つプログラムを作成する場合は必ずこの値に設定します。
図 10 サブシステムをWindows(/SUBSYSTEM:WINDOWS)に設定
さらに、「リンカー」→「詳細設定」から「ランダム化されたベースアドレス」を「いいえ(/DYNAMICBASE:NO)」に設定します(下図)。
これはプログラムがロードされる際のメモリアドレスをランダムな配置にするASLRと呼ばれる機能の設定であり、ASLRが有効だとアドレスが常にランダムになってしまうことから解析の邪魔になるため、無効にしておきます。
最後に、「リンカー」→「デバッグ」にある「デバッグ情報の生成」を「いいえ」に設定します(下図)。
「デバッグ情報の生成」を「いいえ」に設定する背景ですが、デフォルトだと以下の図のようにEXEファイルのバイナリ内に、ユーザ名や開発時のフォルダ名が判別できるデバッグ情報(pdbファイル)へのファイルパスが埋め込まれてしまいます。そのため、他者に提供するようなプログラムの場合はこの設定を「いいえ」に設定しておきます。
図 13 デバッグ情報の生成が「はい」のままだとEXEにPDBのパスが埋め込まれる
以上で事前準備が完了したので、さっそくEXEファイルを作成(ビルド)してみましょう。
EXEファイルをビルドするには、「ビルド」→「ソリューションのビルド」を選択します(下図)。
通常、ビルドしたEXEファイルは以下のフォルダに作成されます。
C:¥Users¥<ユーザ名> ¥source¥repos¥<プロジェクト名>¥Release
該当フォルダに行き、作成したEXEファイルをそのままダブルクリックしてみます。
すると、「Debugger NO Detected . . . 」というメッセージボックスが表示されました(下図)。
つまり、このプログラムは現在自分自身がデバッグ(解析)されていないことを判断できているということを意味します。
動くプログラムができたので、では自身が作成したプログラムをデバッガで解析していきましょう。
自身が作成したプログラムの解析
動かしながら解析を行う場合「デバッガ」というツールを使用して解析を行います。
デバッガには現在いくつかメジャーなものがあり、どのデバッガでも基本的な使い方に違いはありませんが、今回の解析ではその中でも古株であるOllyDbgというフリーのデバッガを使用していきます。
(筆者はOllyDbgが見やすく優秀な面がまだあるという観点から主に使用していますが、一方でOllyDbg v1.10には軽微なバグや機能が足りない面も少しあるので、そうした際に他のデバッガも併せて使用しています。本記事の内容はどのデバッガでも同じように進められますので、使いやすいと感じたものを使用するか、1つのデバッガに拘らず状況に合わせて適宜使い分けると良いでしょう)
OllyDbgの公式サイトから「OllyDbg 1.10」をダウンロードしておきます。
OllyDbgを起動し、上記で自分が作成したプログラムを「File」→「Open」から読み込ませてみましょう。以下のような分割画面が表示されますが、それぞれの画面が役割を持っています。
どのデバッガも大体同じ画面構成になっており、今回主に使用するのは「逆アセンブル画面」と「レジスタ画面」の2つです(下図)。
OllyDbgはデフォルト状態では少し見づらいため、以下の設定をおすすめします。
まず「Options」→「Appearance」→「Fonts」→「Change」で「MSゴシック/太字/サイズ12」(またはお好みのフォント)に設定します(下図)。
続いて、「Options」メニュー→「Debugging options」→「Addresses」の以下の2つにチェックを入れます(下図)。
・Show name of local module
・Highlight symbolic names
この2つの設定を行うことで、以下の図のようにWindows APIが赤く表示されるようになり、また、それらのWindows APIがどのシステムDLLで定義されているかを表示してくれます。例えば、以下の図の例では「IsDebuggerPresent」が「KERNEL32(Kernel32.dll)」で定義されていることが把握できます。同じWindows APIの名前でも提供しているシステムDLLが異なるケースがあり、後々重要になってくる時が来るため設定しておきましょう。
また、逆アセンブル画面(OllyDbgの左上画面)の任意の場所で右クリックし、メニューから「Appearance」→「Highlighting」→「Chrismas tree」にすると、アセンブリ言語のコードがカラーでハイライトされ見やくなります(下図)。
(補足:なおこの辺りの設定は、例えば比較的新しいx64dbgというデバッガではデフォルトで同様の設定がはじめから施されています。)
話を戻して、デバッガで解析対象のプログラムを開いた直後は「モジュールエントリーポイント」という場所で停止しています。
エントリーポイントとはプログラムが動き出すコードの開始位置を指しますが、今回作成したプログラムのようにエントリーポイントがプログラム開発者の作成したコードの場所ではなく、ビルド作業の過程でVisual Studioに自動的に埋め込まれた初期化コードの場所になっている場合があります。
プログラム開発者が作成したメインの処理は前述の通り(ソースコードに自身で書いたように)WinMainにあることが今回わかっていますので、WinMainの場所へ移動します。
WinMainの場所へ辿り着くにはいくつかの方法がありますが、そのうちの一つをご紹介します。
まずメニューの「View」→「Memory」を選択します(下図)。
図 20 デバッガで開いた直後はエントリーポイントで停止する
そうすると[Memory map]というウインドウが表示されます(下図)。
この画面では、メモリに展開されたプロセスの構造(厳密にはプロセスに当てがわれた仮想アドレス空間の情報)が表示されます。
今回のプログラムではこの画面からWinMainの場所へ辿り着くことができます。
まず「Owner」の列から自身がつけたプログラム名になっている領域を探します。
そして「Section」の列が「.text」であるところを確認しましょう。
通常のEXEでは「.text」というセクション(一塊の構造の呼び名)が「コードセクション」と呼ばれ、実行可能なコード(今回で言えばメインの処理)がその場所に配置されます。
「Contains」の列はそうした役割を教えてくれており、「.text」を見ると「code」と表記されていることがわかります。
(なお、「.text」というセクション名は偽装できたり、他のセクションにコードを含めたりすることもできますが、本記事はベーシックな理解を目的としているので応用的な情報は解説しません。あくまで基本的なEXEの構造で理解しておくことが必要です。)
[Memory map]の画面はメモリの構造を表示しているだけなので、実際のコードを表示するには、「.text」の行を選択して右クリック→「View in Disassembler」を選択します。
(補足:なお、x64dbgのメモリマップ画面では以下のように「実行可能コード」と表示されます。)
図 22 メモリマップを別のデバッガ「x64dbg」で見た様子
話を戻して、OllyDbgのメモリマップから「View in Disassembler」を選択するとアセンブリ画面に戻り、以下の図の通りコードセクションの場所(0x00401000)が表示され、IsDebuggerPresentの文字がさっそく見えています(下図)。
(前述の通りVisual Studioの設定でASLRをオフせずにビルドした場合は0x00401000などのアドレスの数値が環境によって異なる値になります。)
少し下にMessageBoxなどの文字もある通り、見覚えのあるAPIが並んでいることがわかります。自分で作成したプログラムなので、ここがメインの処理であることが一目瞭然です。
なお、ここで注意しておいてほしい点として、実行中のコードの処理がこの位置(WinMainの場所)に移動したわけではなく、あくまでこの場所を表示しただけです。そのため、処理が止まっているコードの場所はまだEXEファイルを開いた時と変わらずエントリーポイントであることに注意してください(よく初めに混乱するポイントかと思います)。
ではそのままデバッガでプログラムを一度動作させてみましょう。
デバッガで動きを開始するにはキーボードの[F9]キーを一回押します。
※「F9」を押すと、特に停止させる処理がない限り最後までプログラムをデバッガ上で動かすことができ、最もよく使用するショートカットキーの一つです。
するとコードが動き、以下のように「Debugger Detected !」というメッセージボックスが表示されました(下図)。
図 24 作成したプログラムをそのままデバッガ上で動かした様子
つまり、このプログラムは正しくデバッガで解析されていることを判断できていることがわかります。これがもしマルウェアであれば、メッセージボックスではなくここに自身を終了する処理を入れることで、デバッガで解析されていると判断すると強制終了するといったアンチデバッグが実現できることになります。
アンチデバッグの回避
では、上記IsDebuggerPresentを使用したアンチデバッグ(デバッグ検知)を回避するにはどうしたら良いでしょうか。つまりデバッガで解析しても「Debugger NO Detected . . . 」と表示させられれば回避したことになります。
ここからは少しだけアセンブリ言語を理解する必要があります(といっても、最小限の解説に抑えています)。
最低限知っておくべき前提知識として、アセンブリ言語とC/C++コードの比較を以下の図に示しました。
まず、以下の図(上側)のように関数Aの結果が0かどうかをIF文でチェックして、0でない場合に関数Bを実行するというC/C++コードがあったとします。それをアセンブリ言語にしたものが図(下側)の内容になります。図(下側)に赤字で書いているものがアセンブリ言語における「命令」となりますが、今回の記事では以下の3つの命令だけ理解しておけば大丈夫です。
- call命令は実行する処理で、例えば「call <関数Aのアドレス>」と記述すると関数Aのアドレスにあるコードを実行する処理になります。実行された後の戻り値(関数の結果)はEAXという名前の変数に入るということを特に覚えておきます。
- test命令はその右側に置かれた2つの値にAND(論理積)という計算をし、計算結果が0であった場合にZF(ゼロフラグ)という値を1にします。ここでは同じ値同士のANDの場合、1同士であれば結果が1、0同士であれば0になるとだけ理解しておくと良いでしょう。
- jnz命令はjneとも表記しますが、ZFが0であれば指定先へジャンプするという命令です。
つまり、下図の3行のアセンブリ言語のコードを解説すると、call命令で関数Aが実行され、戻り値がEAXに入った後、test命令でEAX同士をAND計算しその計算結果によってZFを1または0にし、最後にjnz命令でZ Fの結果によってジャンプする・しないを決定する、という処理の流れになります。
戻り値を0かどうか比較するC/C++のIF文は基本的にだいたい以下の形になります。
ではそうした前提知識のもと、改めて、デバッガを眺めてみましょう。
(デバッガを起動し直すか、メニューの[Debug]→[Restart]を押してもう一度プログラムを読み込み直し、WinMainの場所へ再度移動しておきます)
先頭の開始位置に先ほど解説したアセンブリ言語の形が見えてきます(下図)。つまり、先ほどの知識があれば、ここがIF文であることがわかるわけです。
図 26 WinMain(.textセクションの先頭)の最初の3行を眺めてみる
ちなみに、前述のEAXやZFといった値をデバッガで確認したい場合は、レジスタ画面(右上)を見ることで把握できます。以下の図はOllyDbgとx64dbgのそれぞれの見方です。(レジスタとはCPUの中の小さな記憶装置であり、CPUが処理する命令やデータが配置されます。)
ここでIsDebuggerPresentがどんな関数だったかもう一度振り返りましょう(下図)。デバッガで動かされていなければ0を返す、つまりデバッガで動かされていれば0以外(実際は1)を返す関数でした。
図 28 もう一度IsDebuggerPresentというWindows APIを振り返る
再び、デバッガの画面を眺め、先頭に見える3行のコードをデバッガで動かした場合にどう遷移するかを考えてみます(下図)。
ここまでの理解で、以下の図の①②③の順に処理が進んでいくことがわかります。
以下の①②③の処理が終わった後、0x00401022へジャンプしていますが、細かい点がわからなくとも「Debugger Detected !」のメッセージボックスを表示させる処理の塊の中に入っていくことがなんとなく想像できれば大丈夫です。
デバッガを使用してアンチデバッグを回避する流れ
以上までの解説で、デバッガでの見方やデバッガで動かした場合の動作が一通り簡単にわかったので、実際にアンチデバッグを回避する作業に入っていきましょう。
回避方法は様々ありますが、今回はIsDebuggerPresentがデバッガで解析されている際に戻り値として「1」を戻すという性質に着目し、逆に「0」を戻すように改変することで回避してみます。
ここから先に進む上で一つだけ覚えておくべき非常に重要なものに「ブレークポイント」があります。ブレークポイントとは設定した場所でデバッガがプログラムの動きをストップしてくれる非常に重要な機能です。例えば、以下の図のように命令が並んでいた場合、矢印の位置にブレークポイントを設定して開始ポイント(AAAAAAの場所)から一気に実行すると、矢印の位置の処理にきた瞬間にプログラムが停止します。当然、上から順に1命令ずつ進ませても良いですが、膨大なコードの場合は一つずつ動かしていくと途方もない時間がかかってしまいます。ブレークポイントを活用すれば調べたいコードの場所まで一気にプログラムを進ませることができます(下図)。
デバッガでブレークポイントを設定するには、以下の流れで行います。
まず、アセンブラ画面(左上画面)の任意の場所をクリックした後、キーボードの[CTRL+G](CTRLキーを押しながらGキーを押す)を押すと表示されるウインドウでコードの場所を自由に移動できるので、ブレークポイントを設定したい場所へ一旦移動して表示させます(下図)。
今回はIsDebuggerPresentというAPIがキーポイントであることがすでにわかっているので、IsDebuggerPresentというAPIにブレークポイントを設定してみましょう。
入力欄に「IsDebuggerPresent」(またはKernel32.IsDebuggerPresent)と入力してOKを押します。
そうすると、逆アセンブラ画面が変化し、以下の図のようにIsDebuggerPresentのAPIが実装されている場所に移動できます。ここにブレークポイントを設定するには[F2]キーを押します(下図)。
冒頭のOllyDbgの設定でモジュール名を表示するようにしているため、IsDebuggerPresentが「Kernel32」というシステムDLLの中で実装されていることが把握できます。(なお、現在のコードがどのモジュールの中を見ているのかという情報はデバッガウインドウのタイトルバーを見ることでも把握できます(下図)。)
上記はブレークポイントを設定しただけですので、まだ実行中のコード地点はエントリーポイントの先頭にあります。(先ほどの作業はCTRL+GでKernel32の中のIsDebuggerPresentの位置を表示させただけであり、実行中のコード地点がそこに遷移したわけではない点に再度注意)
ではプログラムを動かしていきます。[F9]キーを押すとブレークポイントが設定されている場所まで、エントリーポイントから一気にコードが実行されていきます。すると先ほどブレークポイントを設定したIsDebuggerPresentの位置でストップすることがわかります(下図)。これは、Kernel32.dllの中のIsDebuggerPresentが呼び出された瞬間にプログラムが停止したことを意味します。
つまり、プログラム開発者がIsDebuggePresentをC/C++コードに書くと、内部的にはEXEにロードされたKernel32.dllの中のIsDebuggerPresentが呼び出されていることがわかります。
今現在、IsDebuggerPresentの呼び出し先頭に実行コード場所が来ていますが、今回はIsDebuggerPresentの戻り値を改変したいので、戻り値をリターンする(戻す)直前(つまりIsDebuggerPresentの関数の最後の位置)までもう少しだけ動かしてみましょう。
そのためには、[CTRL+F9]キーを押すことで、現在の位置からリターンする直前の場所まで実行することができます(下図)。
復習となりますが、なぜリターンまで実行するのかというと、IsDebuggerPresentはデバッガの有無で0か1かを返す関数でした。つまり今回は戻り値が重要なので、戻り値を返す直前の状態で止めて、戻り値を改変したいので、リターンまで実行するという背景があります。
CTRL+F9を押すと以下のような画面でストップしますが、retnと表示があるようにリターン処理の直前で止まっていることがわかります(下図)。リターンする戻り値はEAXに入るので、ここでレジスタ画面のEAXの値をみてみると「1」になっています。つまり、IsDebuggerPresentが「1」という結果を戻そうとしていることがわかります。1はデバッガで動作しているという意味でした。デバッガで動作していないと判断させるにはどうすれば良いでしょうか。IsDebuggerPresentが「0」を戻すように改変すれば良いことに気づきます。
図 35 リターンまで実行した直後の様子(レジスタのEAXをチェックする)
戻り値を改変するにはレジスタ画面のEAXを選択して右クリックし、「Modify」を選択します(下図)。
「Modify EAX」というウインドウが表示されるので、現在1になっている数値を0に書き換えます。同様の作業でレジスタの値は任意の値に変更することが可能です。
戻り値を改変した後で、コードを進めてみましょう。
[F7]または[F8]キーを押してコードを1行ずつ進めてみると(F7キーはF8キーよりもより細かくコードを実行していきますが今回はどちらでも構いません)、JNZの処理の時点でEAXが0になっており、先ほどはジャンプしていた「0x004010022」のアドレスへジャンプしなくなりました。その後、何事もなくJNZの下のコードへ遷移していくことがわかります(下図)。
図 37 戻り値を変えた後にコードを1つずつ進めてみると処理の流れが変わった事がわかる
さらに進めていくと以下のようにデバッガで動かしているのにも関わらず「Debugger NO Detected . . . 」というメッセージボックスが表示されました(下図)。つまり、これでアンチデバッグの回避が行えたことになります。もしIsDebuggerPresentを使用するマルウェアだった場合、この後の処理の解析に進むことができます。
(補足:なお、上記の作業でアンチデバッグは回避できていますが、MessageBox の直前でEAXを改変させたことでその直後に引数でEAXを使うMessageBoxの辻褄が合わなくなるため、「Debugger NO Detected . . . 」のメッセージボックスを表示させるには正確にはもう一箇所修正が必要となりますが、本質ではないのでここでは解説を省略しています。詳しくは末尾の動画の方で解説していますのでご覧ください)
バイナリを改変してアンチデバッグを回避する方法
なお、上記で解説したIsDebuggerPresentの戻り値を動的に改変する回避方法ではデバッガでの作業が毎回必要になります。動的に変更するのではなく、そもそもEXEファイルのバイナリを改変しておけば、そうした手間がなくなります。
ここでEXEファイルのバイナリを改変することでアンチデバッグを回避する方法も一つ解説しておきましょう。
一般のバイナリエディタでも良いですが、「PE-bear」というツールを使用すると手っ取り早く行えます(下図)。「PE-bear」で作成したプログラムを読み込ませた後、左から「.text」セクションを選択すると、WinMainのコードが右下の逆アセンブル画面に表示されるので、これまでの解説の通りI F文の分岐だったJNZ(JNE)命令の部分をクリックします。
そうすると、上のバイナリ画面で「75 18」という領域がハイライトされるので、ダブルクリックして編集状態にし「75」を「74」に変更します。あとはこのバイナリを保存すれば、たった1バイトを変更しただけでアンチデバッグを回避するバイナリに変化します。
上記の作業を解説すると、条件分岐であるJNZ(JNE)を反対の意味であるJEに変更することで、「デバッグされていた場合ジャンプする」という処理が「デバッグされていない場合にジャンプする」という処理に変化します。JNZ(JNE)という命令はバイナリでは0x75で表され、JEは0x74で表されるため、バイナリ上で75を74に変更すれば、対応する命令がJNZ(JNE)からJEに変化するわけです(条件分岐が逆になる)。
このように改変したバイナリを実行してみた結果が以下の様子です。
これまでと反対に、そのままフォルダから実行すると「Debugger Detected !」が表示され、デバッガーで実行すると「Debugger NO Detected . . . 」が表示されるようになりました。つまり、IF文の条件分岐が反対になっていることがわかります(下図)。
条件分岐系のアンチデバッグに対してはこのようにジャンプ命令を逆にするか、何もしないという命令であるNOP(バイナリでは0x90)に変更すると回避できます。
図 40 バイナリを書き換えたEXEを実行すると逆の動きになる様子
こうしてバイナリを直接書き換える事で、アンチデバッグにより動作しない環境でもマルウェアを動作させられるようになるという利点もあります(今回はデバッガ検知を取り上げていますが、サンドボックス検知などの処理だった場合、より有用でしょう)。
その他の補足
ここまであえて触れてきませんでしたが、IDA Proなどの逆アセンブラを使用すれば、より早く今回のような条件分岐が把握できます。以下はIDA Proで作成したプログラムを開いた様子ですが、WinMainの場所が自動的に開始位置として表示され、条件分岐が一目瞭然となります。実際にデバッガで解析する際はこのように逆アセンブラと並行して確認していくことでデバッガによる解析作業を効率的に進めることが可能となります。
また、x64dbgというデバッガの場合、デフォルトで付属しているグラフ機能を使えばIDA proのようなグラフィカルな表示でコード遷移を簡易的に表示することもできます(下図)。グラフを表示させるには、x64dbgの逆アセンブル画面上の確認したい場所で右クリックメニューから「グラフ」を選択します。
最後に、その他のデバッガのデフォルト状態で同じコードを見た場合の見え方の違いも簡単にご紹介します。Immunity DebuggerというデバッガはOllyDbgとほとんど同じ情報量ですが、x64dbgはデフォルトではAPIの引数がどのコードに対応しているかなどの紐付けが若干見づらくなっています。ただし、x64dbgの場合も「xAnalyzer」というPluginを使用すればそうした点をカバーできます。このようにどのデバッガもPluginを導入していくことでそれぞれが使いやすいように改善していけます(下図)。
図 43 メジャーなデバッガのそれぞれの見え方の違いとプラグインでの拡張
メインの解析チュートリアルは以上ですが、さらに深い部分に興味を持った方や実際のマルウェア解析で使える知識を知りたい方は以下の付録をご覧ください。
(付録)IsDebuggerPresentの内部構造を紐解く
IsDebuggerPresentを利用するアンチデバッグは上記で解説した通り非常にシンプルで広く知られている方法のためすぐに回避されてしまうことから、実際のマルウェアではそのままでは使用されないケースが多いです。より実際のマルウェア解析で使える知識となる情報を以降で解説していきます。
IsDebuggerPresentというWindows APIはどのように内部で実装されているのでしょうか。まずはその理解が必要です。
上記で作成したプログラムをOllyDbgで読み込んだ後、逆アセンブラ画面においてCTRL+Gを押し「KernelBase.IsDebuggerPresent」(Kernel32.IsDebuggerPresentではない点に注意)と入力してOKを押すとIsDebuggerPresentの実装コードを閲覧することができます。
実は上記の解説の中で所々登場してきたKernel32.dllというシステムDLLですが、様々なWindows APIが昔はKernel32.dllの中に実装されていたものの、近年のWindowsではKernel32.dllは多くのAPIを単純にKernelBase.dllに転送するだけの役割になっており、実質的な実体はKernelBase.dllという別のDLLに集約されています。そうした背景があり、実装部分を知るために上記のように明示的に「KernelBase.IsDebuggerPresent」とKernelBase.dllを明確に指定したのです。
以下の図を見て分かる通り、IsDebuggerPresentというWindows APIはたった3行のアセンブリ言語のコードで実装されていることがわかります(下図)。
ではこの3行が何をしているのか、紐解いていきましょう。
ここからの理解で必要になる前提知識として「PEB」と「TEB」というものについて先に解説しておきます。EXEファイルが実行され、プロセスとしてメモリに展開される際、そのデータの中に「PEB」と「TEB」という各種情報を格納した構造体(かたまり)が入っています。PEBはプロセスに関する情報が格納されており、TEBにはスレッドに関する情報が格納されています。名前の通り、PEBはプロセスに一つ、TEBはスレッドことに複数あります。
そして、このPEBという構造体の中に、プロセスが現在デバッグされているかどうかを示す「BeingDebugged」という特殊な項目があります(下図)。
実は先に結論を言ってしまうと、IsDebuggerPresentというWindows APIはPEBの中の「BeingDebugged」という値を取得して返しているだけなのです。そのあたりの話を以下で詳しく解説していきます。
プロセスがメモリに配置された際のPEBとTEBの関係性をシンプルに図示すると下図のようになります。TEBは(32bitプロセスの場合)常にFSレジスタという名前のレジスタに格納され、その先頭アドレスをfs:[0]として指し示す(参照する)ことができます。そして、TEBの先頭アドレスから、0x30だけ移動した場所(fs:[30]として参照できる)にPEBの先頭アドレスが記載されています。さらに、PEBの先頭アドレスから0x02だけ移動した場所に「BeingDebugged」という項目が存在します。
少しややこしいですが、常にこうした位置関係になるようにWindowsで決まっているので、TEBの先頭位置から順に辿っていくと必ず「BeingDebugged」の値に辿り着けます。
BeingDebuggedは1か0の値を持っており、文字通りBeingDebuggedの値が0であればデバッグされていない、1であればデバッグされていることを示します。IsDebuggerPresentというWindows APIはまさにこの値を取得して返しているだけなのです。
ではここまでの知識を持って、もう一度IsDebuggerPresentの実装を眺めてみましょう(下図)。
先ほどの位置関係に出てきた値と同じ30(0x30)と2(0x02)という2つの数字が見えてきます。
図 47 IsDebuggerPresentの実装をもう一度眺めてみる
上図の赤の点線で囲った部分の処理を一つずつ解説すると、下図のような処理を行なっています。EAXという変数を上手く使いながら、BeingDebuggedの値へ辿り着き、その結果を戻り値としてリターンしていることがわかります。(MOVとMOVZXという命令がありますが、ここでは右側の値を左側の値へ代入すると理解しておけば十分です)
図 48 IsDebuggerPresentの実装を一つずつ解説
これでIsDebuggerPresentがたった3行で実装できることがわかりました。
では実際にこれを自身のプログラムで(IsDebuggerPresentを使わずに)実装してみましょう。
ソースコードは以下となります。コピペ等でVisual Studioに貼り付けてそのままビルドしてみましょう。「__asm{ }」という文字列で囲うとアセンブリ言語を直接ソースに記述できるようになります。
上記のソースコードの解説を下図に示しましたが、前述のIsDebuggerPresentを使用したソースコードから変更した点は以下の3点のみです。Manual_IsDebuggerPresent(名前はなんでも良い)という独自の関数を作成し、白点線の部分(下図の変更箇所③)に配置しています。アセンブリコードを書いていますが上記で解説した範囲が理解できていれば十分です。
加えた独自の関数を使用できるようにするために、プロトタイプ宣言というものを追加しています(下図の変更箇所①)。そして、IF文の条件の括弧内を前述のIsDebuggerPresentから自身で作成した関数に置き換えています(下図の変更箇所②)。
複雑に思えるかもしれませんが、現実のマルウェアはIsDebuggerPresentではなくこのように独自実装している場合が少なくありません。
図 49 IsDebuggerPresentを自分で実装してみる
では独自実装してビルドしたプログラムのアンチデバッグ部分を、以前のIsDebuggerPresentのバージョンと比較してみてみましょう(下図)。
IsDebuggerPresentを使用している場合(下図の上)は簡単に処理が判別できますが、独自実装した場合(下図の下)はAPIが隠れてしまうため、コードを見慣れていないと一見わからなくなり、APIの呼び出しの解析のみに頼ってしまうと見逃してしまうことになります。またAPIを使用しないため、APIフックをするようなツールなどでも検知が出来なくなります。
図 50 独自実装したプログラムの処理をデバッガで比較してみる
たった3行のアセンブリコードでも独自実装することで得られる効果がとても大きいことがここからわかります。
ついでに独自実装のコードを解析してみましょう。WinMainの場所から[F7]または[F8]を押してコードを1行ずつ進め、0x00401011の箇所まで進めてみるとPEB+0x2の位置を取得する処理が見えます。左下のメモリ画面をみると取得したアドレスに「01」という値があることがわかります(下図)。
図 51 それぞれの値がどうなるか確認しながらデバッガで1行ずつ進めてみる
また、初めから直接デバッガでPEB(およびそこから把握できるBeingDebugged)の位置を知るには、次のような方法があります。CTRL+Gを押し、fs:[30]と入力するとPEBの場所が簡単に表示できます(下図)。
加えて、うさみみハリケーンなど、PEB/TEBの構造をよりわかりやすく把握できるツールもあり、これらを利用するとBeingDebuggedの値がすぐにわかります(下図)。PEB/TEBのその他の項目も下図のように名前とアドレスと値を並べて閲覧する事ができるので、その辺りをより深く知りたい場合の学習に適しています。
最後に、これもあえてここまで紹介しませんでしたが、デバッガのプラグインを使用すれば今回紹介したアンチデバッグは一瞬で無効にできます。OllyDbgであれば「Olly Advanced」や「ScyllaHide」などのプラグインがあります(下図)。ただし、そうした便利なプラグインをはじめから闇雲に使うのではなく、どのようにアンチデバッグが実現されどうやって回避しているのかという原理を理解するマインドがまずは大切です。
付録すらだいぶ長くなってしまいましたが、IsDebuggerPresentというマルウェア解析において非常にベーシックなネタひとつをとっても、丁寧に掘り下げるとなかなか骨が折れます。
今回のように自身で作成したプログラムであれば、処理の流れがあらかじめ把握できているので比較的敷居を下げた状態でスムーズに解析が進められたと思います。もしこれがマルウェア作者など他者の作成したプログラムだった場合、同じように頭からWinMainに処理があると決め付けて飛び込んでいくことは大きな遠回りとなる場合もあるので、例えば、「Debugger Detected !」というメッセージボックスを呼び出しているアドレスを先に特定しておき、そこから逆に辿っていくことで条件分岐を探すといった別の視点からのアプローチが必要となるなど、応用していくべきことはたくさんありますが、それらを踏まえても、今回のようなアプローチはそうしたマルウェア解析の基礎を作る近道になると感じます。
最後に
多くの情報が溢れる中、どこからどのようにしてマルウェア解析を始めていけばいいのか、という悩みがある方がもしかしたらいるかもしれません。マルウェア解析の書籍や講座などはいきなりアセンブリ言語などから解説するようなものも少なくなく、また淡々とした堅い内容が多かったりツールの紹介で終始したりするようなものもあり途中で挫折してしまう印象がありますが、ゴール(目標)を持って体系立てられた具体的な手順があることでいくらかわかりやすくなるのではないかと思います。
マルウェア解析における肝の一つはアンチデバッグ(耐解析)をいかに回避するかという点があることからも今回題材として最も基礎的なテクニックを取り上げましたが、アンチデバッグには数え切れないほど多くの、そして考えもつかないようなアイデアの複雑なテクニックが既に色々と考えられています。それらを本記事と同じように一つずつ作って解析を繰り返していけば(とても大変ではあるものの)、より深い理解を蓄積していけるでしょう。
(好評でしたら今後もシリーズとしてまた書いていくかもしれません。)
動画資料
本記事で解説した一連の流れについて、実際に動かしている作業の様子があるとわかりやすいかと思うので以下の動画としてアップしました。以下の動画はブログの補足という観点で作成しているため、動画だけでは情報が足りないと思いますので、先に本記事をご覧いただき、その後に本記事における細かい操作の確認などに適宜ご参考ください。(ブログ記事で紹介していない回避方法も動画最後に入れています)