FFRIエンジニアブログ

株式会社FFRIセキュリティのエンジニアが執筆する技術者向けブログです

Emotetの再帰的ローダーを解析する

はじめに

この記事では、昨年(2019 年)12 月に、FFRI yarai により検知され、エンドユーザー様からFFRIに報告された Emotet の複数の検体について、追加的な解析を行った結果を共有します。

Emotetに関して

Emotet は、昨年 11 月 JPCERT/CC から注意喚起がされていることもあり、FFRIとしても、過去に検知実績の記事を掲載致しました。

今回の記事では、Emotet の全体的な挙動の中でごく一部の、既存の解析記事では、あまり解説されていない Emotet の PE ローダーの機能に焦点を当てて解説します。

解析対象の検体のハッシュ値(sha256)は以下です。

  • e39de9f508d54d37a44d30497e8b027722cceed7c28117f8f4c8ff8e7861e2a4
  • b6fac2fc306b00ca44639f920d084c3414a150588aeefa2f837a5d0e20e9b4b5
  • b0370ac9c4e3e44b97af1dccdf6a9346734f9bdae76594b0ee4ef9b1c227a3bf

なお、これらの検体は、国内の複数の金融機関や会計事務所で観測され、FFRI yarai で検知された時点での実行ファイルのハッシュ値を示しています。

EmotetのPEローダー

Emotet は、実行ファイルの表層情報が頻繁に更新され、それらのハッシュ値も変化するため、検体のハッシュ値で検知することを難しくしています。また、検体により大量の junk コードが存在する為、単純にファイルを静的解析しても共通した特徴を抽出することは難しいです。 今回解析した検体も表層情報は大きく異なっていますが、当然共通する処理があります。ある点に着目すると、その共通する処理の入口部分を上手く捉えることができます。

この 3 検体には、検体自体の内部に含まれる PE ファイルを、独自の PE ローダーでロードし、実行するという処理が含まれていました(3 検体とも、kernel32!VirtualAllocExNuma という関数で PE ローダーのメモリを確保した後、ロード処理が行われていました)。

以下の 3 つの構成要素に分解して比較結果を表 1 にまとめてみます。

  1. 外側の PE ファイル(以下、外側 PE と記す)
  2. 内側の PE ファイルのローダー(以下、PE ローダーと記す)
  3. 内側の PE ファイル(以下、内側 PE と記す)

表 1 外側 PE・PE ローダー・内側 PE の比較結果

外側 PE とは、Emotet の実行ファイルそれ自体のことです。内側 PE とは、Emotet 自体の内部に存在するもう 1 つの実行ファイルを指します。PE ローダーとは、外側 PE に内包され、内側 PE をメモリ上に展開するコードを指します。

  1. 外側 PE が開始
  2. PE ローダーと内側 PE がヒープ上にコピーされる。
  3. PE ローダーが実行される。
  4. PE ローダーが内側 PE をロードし内側 PE が(同一プロセス内で)実行される。

ヒープ上でコードを実行するマルウェアは数多くありますが、Emotet の特徴として、目的となるコードを実行する為に、PE ローダーが介在するという点が挙げられます。さらに、PE ローダーが汎用的な PE をロードできるようになっている為、以下の様な効果をもたらします。

  • C&C サーバーからの PE 実行ファイルを同一プロセス内(インメモリ)で実行可能
  • 内部 PE のハッシュ検知からの保護
  • 内部 PE の静的解析のコストの増大

これらの特徴が Emotet が Malware-as-a-Service として機能している理由の一部である可能性があります。

今回は、PE ローダーに着目して、その機能を追ってみます。

PE ローダーのバイナリを分析してみると、内側の PE に対して、以下の処理が行われていることがわかります。

  1. Header フォーマットの確認
  2. ImageHeader のロード
  3. セクションから参照される Rawdata のロード
  4. ImportDirectoryEntry から参照される ImageImportDescriptor 内の WindowsAPI のアドレス解決
  5. BaseRelocationTable 内の動的リロケーション処理
  6. 各セクションのメモリアクセス属性の変更
  7. エントリーポイントの実行

また、Header フォーマットの解析以前に、ローダー内で使用する WindowsAPI のアドレス解決処理が含まれており、ローダー自体が、メモリ上のアドレスの位置によらず実行可能である様に設計されています。

Windowsのローダーとの比較

ここまで、Emotet の内側 PE を起動するコードを PE ローダーという表現を用いて説明しましたが、ここでは Windows のローダーと Emotet の PE ローダーを比較してみます。

表 2 Windows のローダーと Emotet の PE ローダーの比較結果

ntdll 内の LdrInitializeThunk 関数は、通常の開発ではあまり使わない関数ですが、ntdll のエントリーポイント上の関数であり、APC(Asynchronous Procedure Call)として、プロセスが開始される前に、カーネル側から毎回実行されます。処理自体は上に挙げた処理の他にも様々な処理があり、ntdll!LdrInitializeThunk から先のコードを静的解析することで理解することができます。

Windows のローダーと比較した特徴として、Emotet の PE ローダーは、

  • 実行対象を別プロセスとして実行する訳ではないので、カーネル内で行われる処理をする必要がある。
  • 動的リロケーションを必要とするプログラムの実行も可能とする。

当然、TLSCallback の呼び出しや例外エントリーの登録等、ntdll 内のローダーには存在するが、Emotet のローダーに存在しないものは、処理が行われません。また、通常起動時に確認が行われる OptionalHeader 内の多くのパラメーターも Emotet のローダーは無視します。例えば、Emotet が C&C サーバーと通信することで獲得する検体に、DLL 形式のものがありますが、このローダーの場合、DLL 形式の PE ファイルであっても通常の EXE 形式の PE ファイルと同じ様に起動されます。

再帰的PEローダーに関して

このロード手法は、Reflective(再帰的) PE Loader と呼ばれ、Emotet 以外にも Gandcrab、Trickbot、その他多数のマルウェアで使用された手法のようです。 また、この手法は、任意の PE を実行できるという意味で、(圧縮されることはありませんが、)パック手法の一部として分類されることもあるようです。

この手法についての紹介資料の参照を以下に添付します。

[1] ReflectivePELoader の BlackHat2016 発表資料, https://www.blackhat.com/docs/us-16/materials/us-16-Nipravsky-Certificate-Bypass-Hiding-And-Executing-Malware-From-A-Digitally-Signed-Executable-wp.pdf, 閲覧日:2020/03/04

[2] ReflectivePELoader のコードのまとめ, https://github.com/BenjaminSoelberg/ReflectivePELoader , 閲覧日:2020/03/04

まとめ

この記事では、Emotet の PE ローダーについて、まとめました。Emotet の PE ローダーの詳細な解説に興味のある方は、このブログの付録 1 に添付してある、今回の検体の PE ローダー部分のコードをアセンブリのコードとして切り出し、コメントを加えたものをご確認下さい。

今回は、ローダーのみの解説となりましたが、Emotet の PE ローダーが C&C サーバーとの通信により取得する新しいバージョンの自身のマルウェアや、別のマルウェアの実行に関与していると考えられます。

また、今回は、報告のあった 3 検体を中心に解析しましたが、Emotet には他にも様々な亜種が存在し、様々な角度から、包括的に調査をすることで、新たな発見があると思われます。

付録1<ローダーのアセンブリコードとコメント>

今回分析した検体に共通して見られた PE ローダーのコードにコメントを加えて下記に掲載しました。なお、プログラム自体はそれ自体は悪性のものではなく、ダウンロード、(NASM による)コンパイルも可能です。

付録2<動的リロケーション>

Emotet の PE ローダーの動的リロケーション処理とそれを解析に応用した事例を紹介します。なお、説明の対象として、命令セットアーキテクチャが x86 の場合を対象としており、x86-64 の場合は対象外です。

Visual Studio 等の開発環境で、マイクロソフト製のコンパイラ、リンカーを用いて開発されたプログラムの場合、既定では、ImageOptionalHeader の ImageDirectoryEntry に、BaseRelocationTable が存在し、その仮想アドレスが.reloc セクションの Rawdata をメモリ上参照します。このテーブルは、図 1 のように、単一または複数のブロックで構成され、1 つのブロックには、仮想アドレスとブロックのサイズ、そして、ブロックのサイズにあった個数のタイプとオフセットのペアで構成されています。テーブルに、複数のブロックが存在する場合は、リロケーション対象の範囲が 0x1000 以上の場合です。オフセットが 3byte のみしか表現できない為、任意のアドレスを表現するために、0x1000 毎にベースとなるアドレスを設ける必要があり、ブロックは、ベースアドレスを共有する単位となります。タイプにより個々の処理は少しづつ異なりますが、ここでは、最も一般的なケース(タイプ == IMAGE_REL_BASED_HIGHLOW)の場合を紹介します。

図 1 BaseRelocationTableの構造

処理としては、以下のように、外側のループで個々のブロックを巡回し、個々のタイプとオフセットを巡回するという二重ループになります。

  1. 内側のループ内で、ロード先の先頭アドレスに、個々のブロックに定められた仮想アドレスと個々のオフセットを足したアドレスの値を参照します。
  2. そのアドレスの値に、プログラムが本来ロードされる予定であったアドレスと実際のアドレスの差異を足します。
  3. その値を 1 でアクセスした同一のアドレスに格納します。

アセンブリ言語で表現すると以下の様になります。

eax : 書き換えを行うコードが存在する先頭アドレス(0x1000単位)
edi : オフセット

ebp : 実際にプログラムがロードされるアドレス - OptionalHeader上のImageBase 

add [eax+edi], ebp

この様な処理が必要な理由は以下が挙げられます。

  • 静的リンカーが定めた仮想アドレス上にプログラムが必ずしもロードされるとは限らない

  • 仮想アドレスを即値指定する命令が多数存在する(x86 の場合)

例えば、ASLR(Address Space Layout Randomization)の影響により、ImageOptionalHeader に 0x40000 と記されているが、実際には、0x60000 にロードされたプログラムがあると仮定します。このまま何もせず実行すると、そのプログラム内の即値指定を行う命令(例えば、静的変数の値を読み取る命令)が仮想アドレスの開始が 0x40000 であると仮定して書かれている為、即値指定の命令はアクセス違反や意図したアドレスとは異なるアドレスにアクセスしてしまうことになります。その様なことを防ぐ為、動的ローダーが、静的に定められた仮想アドレスと、実際のアドレスが異なる場合、その差分を即値指定のアドレスに適用するという処理を行います。

Emotet の PE ローダーの場合、今回解析した検体では、外側の PE には、ASLR が適用されない様になっていました。また、内側 PE も実際に新しいプロセス上のメモリに展開される訳ではないので、ASLR の影響は受けません。しかし、内側の実行ファイルを起動する際には、仮想アドレスがヒープ上に展開される為、内側の実行ファイルで静的に定められた仮想アドレスと異なるアドレスに、展開される可能性が非常に高いです。その場合、内側の PE の即値を必要とする命令が正しく機能する為に、本来 ASLR の処理で用いられる動的リロケーションの処理をローダーが実装しています。この様な処理は Emotet の技巧的な処理の 1 つの例といえるでしょう。

次に、この仕組みを利用した解析事例を以下に紹介します。

この PE ローダー以降の内部 PE の処理を追ってみるとわかりますが、内部 PE が Windows 標準で用意されている DLL 内の API を呼ぶ際に、全ての API の解決が動的に行われていることに気が付きます。その為、内側の PE のメモリ領域を切り出して静的に解析しても、呼び出している API を知ることは非常に手間がかかります(例えば、IDA Pro では、API 呼び出しが既に解決済みの場合のみ、WindowsAPI の呼び出しを静的に把握することができます)。実行が行われる処理に関しては動的に解析できる為、問題はありませんが、C&C サーバーとの通信等の処理次第では、呼ばれない API は動的に解析することはできません。ここで、このリロケーションの仕組みに着目します。実は、この検体の場合、API が動的に解決される際に、その呼び元のアドレスがこの動的リロケーションテーブルのエントリーを確認することで全てを確認することができます。 リロケーションが必要なアドレスは、アドレスを即値指定する命令に含まれると記載しましたが、具体的には、それらの命令の中で、即値アドレスのロードを行って、そのアドレスに存在する命令に実行を移す call 命令(0xff 0x15 + 即値 4byte)に着目します。即値 4byte が.data 等、実行ファイルの ImageBase によって変化するアドレスを指している場合、このオペランド部分がリロケーションエントリーに存在します。

デバッガーで、PE ローダーに対して、リロケーションを行う処理の書き換えを行うことで、ロードされる内部 PE のメモリ上で、「0xff,0x15 + 即値アドレス」の命令のアドレス、その即値の値を記録します。

具体的には、上に添付した PE ローダーのアセンブリの

;; ******************************************************************
_29d:
    and     ecx,0FFFh
    ;; Type == 3(IMAGE_REL_BASED_HIGHLOW)の時、ebpを足し合わせ
    add     dword [ecx+esi],ebp
;; *******************************************************************

で囲まれた部分を以降で配置する命令への call 命令と call 以降の残りのバイトを nop 命令に書き換えます。

そして、以下のバイナリを PE ローダーの終了後のコード領域でコピー元の領域の内、コピー済みの不要な領域に貼り付けます。

00000000 <_payload_start>:
   0:   81 e1 ff 0f 00 00       and    $0xfff,%ecx
   6:   01 2c 31                add    %ebp,(%ecx,%esi,1)
   9:   66 81 7c 31 fe ff 15    cmpw   $0x15ff,-0x2(%ecx,%esi,1)
  10:   74 01                   je     13 <_b01>
  12:   c3                      ret

00000013 <_b01>:
  13:   01 2c 31                add    %ebp,(%ecx,%esi,1)
  16:   50                      push   %eax
  17:   53                      push   %ebx
  18:   b8 fc 07 13 02          mov    $0x21307fc,%eax
  1d:   83 00 10                addl   $0x10,(%eax)
  20:   8b 00                   mov    (%eax),%eax
  22:   89 f3                   mov    %esi,%ebx
  24:   01 cb                   add    %ecx,%ebx
  26:   89 18                   mov    %ebx,(%eax)
  28:   8b 1b                   mov    (%ebx),%ebx
  2a:   89 58 04                mov    %ebx,0x4(%eax)
  2d:   5b                      pop    %ebx
  2e:   58                      pop    %eax
  2f:   c3                      ret

ポイントは、cmpw $0x15ff,-0x2(%ecx,%esi,1)命令で、リロケーションが必要な即値アドレス(オペランド)のオペコードが、0xff 0x15 で始まる call 命令であるかどうかを確認している部分です。その場合には、命令のアドレスをその即値アドレスとともに、空いている領域に格納します。

b6fac2fc306b00ca44639f920d084c3414a150588aeefa2f837a5d0e20e9b4b5 の検体で確認した所、合計 223 の API 呼び出しが存在することが確認できました。

なお、即値アドレスの中身は内部 PE の.data セクションを指しますが、この時点では、.data セクションには何も入っていません。 .data セクションに使用する API のアドレスは内部 PE の処理中に、DLL 毎に行われている為、その処理が終わった後に、再度、上で処理をしたアドレスの一覧を確認すると、呼び出されていない API も含めて、動的に解決される API を取得することができます。

以下、WinDbg を使用した場合の API を取得する例です。

r $t0 = 先程確保した調査したいAPIのあるアドレス;
u poi(poi(@$t0));
r $t0 = @$t0 + 0x8;

この手法は、内部 PE を解析する際に、動的アドレスを一度に調べることができるので、解析を効率化できます。 取得した全ての API に関してはこちらに掲載しませんが、C&C サーバーとのプロトコルを理解する為の CryptoAPI や通信する為の API の位置を特定することができました。