Cisco Japan Blog

Qilin EDR キラー Infection Chain

2 min read



 

Cisco Talos の武田です。

こちらのブログでは、「2025 年の日本におけるランサムウェア被害と、Qilin 事例から学ぶ早期検知の重要性・ルールの紹介」のブログで触れた Qilin のアフィリエイトが使用する EDR を無効化するマルウェアについて深く解析した結果を紹介いたします。

 

この記事は、Cisco Talos の Security Research Engineer である Takahiro Takeda、Holger Unterbrinkによるブログ 「Qilin EDR Killer Infection Chain 」(2026/4/2)の翻訳です。

 

 

  • エンドポイントでの検知と対応( EDR )ツールは広く導入されており、従来のアンチウイルスよりもはるかに高い能力を備えています。その結果、攻撃者はそれらを無効化またはバイパス(回避)するために EDR キラーを使用します。
  • テレメトリ収集(プロセス、メモリ、ネットワークアクティビティ)を無効化することで、防御側が監視および分析できる範囲が制限されます。
  • 防御側が振る舞い検知を向上させるにつれて、攻撃者は初期アクセスや実行の初期段階の一部として、防御レイヤー自体を標的にすることが増えています。
  • 本ブログ記事では、Qilin ランサムウェアの攻撃で使用される悪意のある「 dll 」の詳細な分析を提供します。これは EDR システムを標的とした多段階の感染チェーンです。市場のほぼすべてのベンダーにわたる 300 種類以上の異なる EDR ドライバーを終了させることができます。
  • SEH / VEH ベースの難読化、カーネルオブジェクトの操作、およびさまざまな API やシステムコールのバイパス手法など、マルウェアが EDR ソリューションを回避し、最終的に無効化するために使用する複数の技術(テクニック)を紹介します。

 

このブログ記事では、Cisco Talos が Qilin ランサムウェアの攻撃で展開されているのを確認した、悪意のあるダイナミックリンクライブラリ( DLL )「 msimg32.dll 」の詳細な技術分析を提供します。より広範な Qilin の活動と攻撃については、こちらのブログ記事で紹介および解説されています。

 

この DLL は、侵害されたシステム上に存在するローカルのエンドポイントでの検知と対応( EDR )ソリューションを無効化するように設計された、洗練された多段階感染チェーンの初期段階にあたるものです。図1 は、この感染チェーンの全体的な実行フローを示す概要図です。

 

図1. 感染チェーンの流れ

 

第一段階は、EDR キラーコンポーネントの実行環境を準備する役割を担うPE ローダーで構成されています。この二次ペイロードは、暗号化された形式でローダー内に埋め込まれています。

このローダーは、高度なEDR回避技術を実装しています。HaloGateと同様のアプローチを活用することで、ユーザーモードのフックを無効化し、実行時にETW(  Event Tracing for Windows )イベントの生成を抑制します。さらに、構造化例外処理(SEH)とベクトル化例外処理(VEH)を広範囲に使用して制御フローを曖昧にし、API 呼び出しパターンを隠蔽します。これにより、ローカルの EDRソリューションによる検知をトリガーすることなく、EDR キラーペイロードをメモリ上で完全に復号、ロード、実行することが可能になります。

EDR キラーコンポーネントは、アクティブになると2つのヘルパードライバーをロードします。最初のドライバー(「 wdrv.sys 」)はシステムの物理メモリへのアクセスを提供し、2 番目のドライバー(「 hlpdrv.sys )は EDR プロセスの終了に使用されます。これに先立ち、EDR キラーコンポーネントは EDR によって確立された監視コールバックを登録解除し、プロセス終了が妨害されることなく進行するようにします。全体として、このマルウェアは、広範なベンダーにわたる 300 種類以上の異なる EDR ドライバーを無効化する能力を持っています。このキャンペーンについては、以前に Sophos ESET などがより高いレベルで報告していますが、本分析では、感染チェーンのこれまで文書化されていなかった技術的な詳細に焦点を当てています。(例えば、SEH/ VEHのトリックや特定のカーネルオブジェクトの上書きなどが挙げられます。)

 

PE Loader section

 

この悪意のある DLL は、msimg32.dll から関数をインポートする正規のアプリケーションによってサイドローディングされている可能性が高いです。

本来の機能を維持するため、元の API 呼び出しは C:\Windows\System32 にある正規のライブラリに転送されます。

Qilin 固有の msimg32.dll は、その DllMain 関数内から悪意のあるロジックをトリガーします。その結果、正規のアプリケーションがこの DLL をロードするとすぐにペイロードが実行される仕組みになっています。

 

図2. ” msimg32.dll “の悪性バージョン

 

Sophos 社も以前のブログ記事で、このローダーに関する技術的および歴史的な洞察(詳細)を提供しており、同社はこれを「 Shanya crypter 」と呼んでいます。

 

PE Loader (msimg32.dll) Initialization Phase

 

初期化中に、ローダーはプロセスメモリ内にスロットポリシーテーブルとして機能するヒープバッファを割り当てます。

 

図3a. スロットポリシーテーブル用のバッファ割り当て

 

このバッファのサイズは、 Ntdll.dll の OptionalHeader.SizeOfCode を 16 で割った値( SizeOfCode >> 4 として実装)として計算されます。その結果、 OptionalHeader.SizeOfCode で定義されるコード領域(通常は .text  領域)をカバーする、16 バイトのコードスロットごとに 1 バイトが割り当てられます。テーブルの各エントリは、BaseOfCode からの相対的な固定の 16 バイトブロックに対応します。

次に、ローダーは ntdll.dll のエクスポートテーブルを反復処理します。名前が ” Nt ” で始まる各エクスポート関数について、対応するシステムコールスタブの仮想アドレスが解決(取得)されます。このアドレスから、スロットインデックスが次のように計算されます:

slot_idx = (FuncVA – BaseOfCode) / 16

このインデックスは、スロットポリシーテーブル内の対応するエントリをマークするために使用されます。すべての Nt* スタブにはデフォルトのポリシーが割り当てられますが、選択された一部の関数には明示的に特別なポリシーがマークされます。これには以下が含まれます:

  • NtTraceEvent
  • NtTraceControl
  • NtAlpcSendWaitReceivePort

その結果、ntdll.dll の実行可能コードを変更することなく、関連するシステムコールスタブのデータ駆動型の分類が行われます。最終的なスロットポリシーテーブルは以下のようになります:

 

図3b. スロットポリシーテーブル

 

実際のローダー関数ははるかに複雑であり、実行時におけるハッシュベースの API 解決といった、追加の難読化技術(手法)を組み込んでいます。

 

図4. 「 Nt 」システムコールスタブ関数に基づくスロットポリシーテーブルの作成

 

テーブルを構築した後、動的に ntdll!LdrProtectMrdata を解決(取得)します(これについては後で詳しく説明します。)続いてこのルーチンを呼び出し、.mrdata セクションの保護属性を書き込み可能に変更します。このセクションには、例外ディスパッチャーのコールバックポインターや、その他の重要なランタイムデータが含まれています。

セクションが書き込み可能になると、ローダーはディスパッチャースロットを独自のカスタム例外ハンドラーで上書きします。その結果、例外がトリガーされるたびに、この(ローダーの)ルーチンが実行されるようになります。

 

図5. 例外ハンドラディスパッチャスロットの上書き

 

Runtime exception handling (hook_function_ExceptionCallback)

 

この関数は主に、ブレークポイント例外の処理とシングルステップ例外の処理という2つのタスクを実行します。

ブレークポイント例外( 0xCC )の処理は比較的単純です。単に INT3( 0xCC )の直後にある命令から実行を再開するだけです。なぜこのアプローチが実装されたのか、確かな理由は分かっていません。脆弱な分析システムに対する軽量なアンチエミュレーション、アンチ解析やアンチサンドボックスのメカニズムとして機能しているか、より高度なアンチデバッグ技術の基盤となっているか、あるいはステージ2 や 3 で観察される VEH ベースのロジックと同様の、将来的な制御フロー操作の準備として機能している可能性があります。

 

図6. hook_function_ExceptionCallback 関数のブレークポイントロジック

 

関数のシングルステップ処理部分ははるかに複雑であり、ここで先ほど紹介したスロットポリシーテーブルが利用されます。ctx->ntstub_class_map は、初期化中に割り当てられたマップバッファを指しています。

 

図7. hook_function_ExceptionCallback 関数のシングルステップロジック

 

初期化関数とディスパッチ関数のロジックを簡略化すると、擬似コードでは以下のようになります。InitCtxAndPatchNtdllMrdataDispatch は初期化関数であり、hook_function_ExceptionCallback は前述のディスパッチ関数を指します。

 

図8. 簡略化されたシングルステップ SEH ロジック

 

上の「“ hook_function_ExceptionCallback ” 関数のシングルステップロジック」のスクリーンショットに示されている find_syscall ルーチンは、HaloGate / TartarusGate スタイルのシステムコール復元技術を実装しています。詳細は以下の画像で確認できます。このルーチンは、ntdll 内を前方および後方にスキャンして、無傷の(改ざんされていない)システムコールスタブを検索し、再利用可能な隣接するシステムコールを特定します。

 

簡略化されたロジックは以下の通りです:

  1. 前方および後方をスキャンすることにより、ターゲットとなるシステムコール番号を間接的に決定する。
  2. クリーンな(フックされていない)隣接するスタブを見つける。
  3. 正しいシステムコール IDを EAX レジスタに手動でロードする。
  4. syscall 命令を使用してカーネルモードに直接移行する。言い換えると、クリーンな隣接するスタブ内に配置された syscall 命令を使用する。

 

隣接するシステムコールスタブを再利用して目的のシステムコールを呼び出すことで、ローダーはフックされたコード自体を変更することなく、EDR によってフックされたシステムコールをバイパスします。Windows カーネルは EAX 内のシステムコール ID のみを評価し、どのエクスポートされた API 関数が呼び出しを開始したかは検証しません。

 

Figure 9. Halo’s Gate: find_syscall function.

 

前述の通り、マルウェアの実際のコードはより複雑であり、例えば前述した ntdll!LdrProtectMrdata の実行時解決などが挙げられます。

 

図10. 実行時における ntdll!LdrProtectMrdata の解決

 

ローダーは、ntdll!LdrProtectMrdata 関数をステルス性の高い方法で解決(取得)します。LdrProtectMrdata を名前(またはハッシュ)で解決する代わりに、ローダーは以下の手順を実行します:

  1. ntdll イメージ内の .mrdata セクションを見つける。
  2. 現在のディスパッチャースロットポインター( dispatch_slot )が .mrdata 内にあるかどうかを確認する。
  3. そうである場合、既知のエクスポートされた ntdll 関数(ハッシュ経由で特定された RtlDeleteFunctionTable )をアンカー(起点)として使用する。
  4. そのアンカーから CALL rel32 命令( 0xE8 )をスキャンし、そのターゲットアドレスを抽出する。
  5. その呼び出し先を LdrProtectMrdata として扱い、ctx->LdrProtectMrdata に格納する。

先に説明した初期化ルーチンには、いくつかの基本的なアンチデバッグ対策も組み込まれています。例えば、KiUserExceptionDispatcher にブレークポイントが設定されているかどうかを検証します。そのようなブレークポイントが検知された場合、プロセスを意図的にクラッシュさせます。このチェックはディスパッチャーが上書きされる前に実行されるため、その結果発生する例外は、元のデフォルトの例外ハンドラーによって処理されることになります。

 

図11. KiUserExceptionDispatcher のブレークポイントチェック

 

ローダーはジオフェンシング(地理的制限)も実装しています。旧ソビエト連邦諸国で一般的に使用されている言語に設定されたシステムを除外します。このチェックは初期段階で実行され、除外リストに該当するロケール(地域・言語設定)が検出された場合、ローダーは処理を終了します。

 

図12. ジオフェンシング関数

 

図13. ジオフェンシングの除外対象国リスト

 

ステージ1 の初期化後、ローダーは後続のステージのアンパック(展開)に進みます。ページングファイルにバックアップされたセクションを作成し、このセクションの 2つのビューをプロセスのアドレス空間にマッピングします。この側面については深く分析されていませんが、同じセクションの 2つのビューを作成することは、読み取り/書き込み/実行可能( READ-WRITE-EXECUTABLE )なメモリ領域を隠蔽するために使用される一般的なマルウェアの手法です。通常、一方のビューは書き込み( WRITE )アクセスのみで構成され、基盤となるセクションの実際の実行可能権限を隠蔽(マスク)します。この共有メモリ領域には、アンパック後に後続のステージが含まれることになります。これには、分析中にメモリのダンプがより困難になるという副次的な効果もあります。仮想メモリページが現在 RAM 上に存在しない( Present ビットがクリアされている)場合、そこにアクセスするとページフォールトがトリガーされます。その後、カーネルは、例えばページファイルから物理メモリにページをロードすることによって、このフォールトを解決します。

 

図14. CreateFileMappingA リゾルバ関数、Returnハンドル 0x174

 

図15. 最初の「書き込み専用」ビュー、FILE_MAP_WRITE ( 0x2 )。

 

図16. 2番目の「R-W-X」ビュー、0x18 = FILE_MAP_WRITE ( 0x2 ) | FILE_MAP_READ ( 0x4 ) | FILE_MAP_EXECUTE ( 0x20 )。

 

ビューを作成した後、このバッファにバイトデータをコピーしてデコード(復号)します。緑色でハイライトされた基本ブロックはこのルーチンの開始を示しており、一方で赤色の基本ブロックはデコードされたペイロードへの最終的な制御の移行(次の図を参照)を表しています。黄色の基本ブロックには、実行がいつ赤色の基本ブロックへ移行するかを決定する判定ロジックが含まれています。

 

図17. ステージ2 のデコードルーチン

 

赤い基本ブロックの内部では、ステージ2 としてデコードされたバイト列への最終的なジャンプが行われています。

 

図18. 赤いブロック内のステージ2 への呼び出し

 

Stage 2

 

ステージ2( 0x2470000 )は、実行をステージ3 へ移行するための、ステルス性の高い移行メカニズムとしてのみ機能します。予想される通り、0x2470000 など、これ以降に参照されるすべてのアドレスは実行時に動的に割り当てられるため、ローダーの実行ごとに異なる可能性があります。

ステージ2の最初の部分は単純です。メモリセクションに保存されたデータをデコード(復号)し、以前にマッピングされたビューのマップを解除(アンマップ)します。それに続く関数呼び出しが、重要なステップとなります:ctx->FuncPtrHookIAT((ULONGLONG)ctx->hooking_func);

 

図19. ステージ2

 

図20. IAT hooking関数

 

この IAT フックルーチンは、メインプロセス(悪意のある msimg32.dll をロードしたプロセス)のインポートアドレステーブル( IAT )内の ExitProcess エントリを上書きします。

 

図21. 0x140017138 における ExitProcess への上書きされた IAT ポインタ

 

図18 に示されているように、実行はステージ2 から正常に戻り、DllMain は異常なく完了します。悪意のあるロジックは後ほど、プロセス終了時に exit_or_terminate_process によって ExitProcess が呼び出された際にトリガーされます。プロセスを終了する代わりに、実行はステージ3に対応する関数 0x2471000 へとリダイレクトされます。

 

Stage 3

 

ステージ3は主に、悪意のある msimg32.dll 内に元々埋め込まれていた PE イメージをメモリから展開してロードします。まず、後続のコードセクションやそれに続くデコードルーチンで使用されるシステムコールスタブを解決することから始まります。

 

図22. システムコールの解決および特定の関数の実行

 

複数のデコード復号および準備ステップを経て、PE イメージがメモリから展開されます。

 

図23. 以前にアンパックされた圧縮バッファ

 

図24. 展開されたバッファ

 

PE イメージが展開された後、PE の準備、ロード、そして最終的な実行を担う最終ルーチンは、今回の実行では 0x24A2CE7 に確認できます。

 

図25. 埋め込まれた PE の最終的なロードおよび実行

 

fix_and_load_PE_set_VEH 関数は、まず NtCreateFile、NtCreateSection、MapViewOfFile を使用して、shell32.dll をプロセスのアドレス空間にマッピングすることから開始します。その後、事前に読み込まれていた PE イメージで、メモリ上の shell32.dll の内容を上書きします。

 

図26. shell32.dllのメモリへのロード

 

埋め込まれデコードされた PE イメージをメモリにコピーした後、コードはベース再配置されます。

 

図27. PE再配置

 

PE をメモリ内で実行するための準備が完了した後、ローダーはステージ2 と同様の手法をとりますが、今回はベクトル化例外ハンドラー( VEH )を活用します。VEH を登録した後、ntdll!NtOpenSection にハードウェアブレークポイントを設定することでハンドラーをトリガーします。NtOpenSection を間接的に呼び出すために、ローダーは続いて  LdrLoadDll API を呼び出して偽の DLL をロードします。マルウェアの作者は、おそらく挑発的な意図から、著名なセキュリティ研究者にちなんだ名前を意図的に選んだものと思われます。

 

図28. LdrLoadDll への呼び出し

 

いくつかの中間ステップを経て、その結果 NtOpenSection が呼び出されます。これにより、事前に設定されていたハードウェアブレークポイントがトリガーされ、結果として VEH(ベクトル化例外ハンドラー)が呼び出されます。NtOpenSection でVEH が初めてトリガーされると、以下のコードが実行されます。

 

図29. 悪意のあるVEH、パート1:NtOpenSection ハンドラ

 

メモリ上の「shell32.dll」という名前を「 hasherezade_[redacted].dll 」に変更し、コンテキストレコード内の RIP を NtOpenSection スタブ内の次のret命令( 0xC3 )を指すように調整します。また、NtMapViewOfSection に新しいハードウェアブレークポイントを設定します。さらに、スタックポインタを LdrpMinimalMapModule+offset を参照するように更新します。ここで、このオフセットは LdrpMinimalMapModule 内の NtOpenSection への呼び出しの直後の命令に対応しています。その後、 NtContinue を呼び出し、コンテキストレコードに格納されたRIP値(つまりret命令)で実行を再開します。この ret 命令は、スタック上に準備されたアドレス、すなわち LdrpMinimalMapModule+offset へ制御を移します。

cr_1->rsp = LdrpMinimalMapModule+offset
cr_1->rip = ntdll!NtOpenSection+0x14 = ret ; 実行時に<rsp>へジャンプする

 

図30. NtOpenSection 呼び出し後のジャンプ先

 

LdrpMinimalMapModule の実行中に NtMapViewOfSection への呼び出しが行われ、これにより前のルーチンで設定されたハードウェアブレークポイントがトリガーされます。この際、図31 に示す VEH は以下のコードが実行されます。

 

図31. 悪意のある VEH、パート2:NtMapViewOfSection ハンドラ

 

すべてのハードウェアブレークポイントを削除した後、スタックポインターを LdrMinimalMapModule+offset 内のアドレスを指すように設定します。これは NtMapViewOfSection への呼び出しの直後にあたります。

 

ctx->rsp -> ntdll!LdrpMinimalMapModule+0x23b 
ctx->rip -> ntdll!NtMapViewOfSection+0x14 = ret  

 

ret 命令に到達すると、スタックポインタ( rsp )に格納されているアドレスへジャンプします。

 

図32. NtMapViewOfSection 呼び出し後のジャンプ先

 

LdrpMinimalMapModule 内の後続のコードは、以前に復元された PE イメージをプロセスのアドレス空間にマッピングし、実行の準備を行います。最後に、制御は 0x24A3C1E に戻ります。これは、最初のハードウェアブレークポイントをトリガーした呼び出しの直後の命令です。

 

図33. LdrLoadDll の呼び出し後の命令

 

さらにいくつかの追加の調整を経た後、ローダーはステージ4、すなわちロードされた PE イメージへと実行を移行します。

 

図34. ロードされた PE への最終的なジャンプ

 

この PE ファイルは、幅広い商用ソリューションにわたる 300 種類以上の異なるEDR ドライバーを無効化する能力を持つ EDR キラーです。このコンポーネントの詳細な分析については、次のセクションで説明します。

 

図35. EDR ドライバーリストからの抜粋

 

PE loader summary

 

このバイナリの最初の 3 つのステージは、巧妙に構築された SEH(構造化例外処理)および VEH(ベクトル化例外処理)の手法を通じてユーザーモードのフックを回避することで、一般的な EDRソリューションをバイパスできる、洗練された複雑なPEローダーを実装しています。これらの手法は完全に新しいものではありませんが、依然として有効であり、適切に実装された EDR ソリューションであれば検知できるはずです。

ローダーは、埋め込まれた PE ペイロードをメモリ上で復号して実行します。このキャンペーンにおいて、そのペイロードは 300 種類以上の異なる EDR 製品を無効化する能力を持つ EDR キラーです。このコンポーネントについては、次のセクションで詳細に分析します。

 

EDR Killer section

Stage 4 – Extracted EDR Killer PE file

 

初期化処理とは別に、ステージ3 から抽出された PE が最初に行うことは、ロケール(地域設定)が旧ソビエト連邦諸国のものであるかを再度確認し、該当する場合はクラッシュさせます。これは、ステージ1 とステージ2 が、攻撃者の望む任意の PE をロードできる単なるカスタム PE ローダーに過ぎないことを示すもう 1 つの指標となります。そうでなければ、同じチェックを 2 度行うことは理にかなっていません。

図36. マルウェアのジオフェンシング関数

 

図37. ブロック対象国リスト

 

このマルウェアはその後、権限昇格を試み、補助ドライバーをロードします。これは、そのプロセスが管理者権限で実行される必要があることも示しています。

図38. 権限昇格およびヘルパードライバーのロード

 

rwdrv.sys ドライバーは、元々 TechPowerUp LLC によって配布され、有効なデジタル証明書で署名された ThrottleStop.sys の名前を変更したバージョンです。これは、GPU-Z や ThrottleStop などのツールで正規に使用されています。このドライバーの悪用が確認されたのは今回が初めてではなく、以前にもマルウェアキャンペーンで利用されていました。

その無害な出自にもかかわらず、このドライバーは非常に強力な機能を公開しており、任意のユーザーモードアプリケーションからロードすることが可能です。決定的な問題として、意味のあるセキュリティチェックを強制することなくこれらの機能を実装しているため、悪用目的で特に魅力的なものとなっています。

このドライバーは、IOCTL を通じてユーザーモードに低レベルのハードウェアアクセスインターフェースを公開します。これにより、ユーザーモードアプリケーションがシステムハードウェアや CPU レジスタと直接対話できるようになります。 このドライバーは、以下の機能を提供する IOCTL ハンドラーを実装しています:

  • I/Oポートアクセス
    • ハードウェアポートからの読み取り( inb / inw / ind )
    • ハードウェアポートへの書き込み( outb / outw / outd )
  • CPUのモデル固有レジスタ(MSR)アクセス
    • MSRの読み取り( __readmsr )
    • 重要なsyscall/sysenterレジスタの変更に対する限定的な保護を伴うMSRの書き込み(__writemsr)
  • 物理メモリ / MMIO(メモリマップトI/O)アクセス
    • MmMapIoSpace を使用して、任意の物理メモリをカーネル空間にマッピングする
    • MmMapLockedPagesSpecifyCache を使用して、同じメモリのユーザーモードマッピングを作成する
    • ドライバーインスタンスごとに最大 256 個のアクティブなマッピングを維持する
    • これらのマッピングを解放/アンマップするための IOCTL を提供する
  • 直接的な物理メモリアクセス
    • 物理メモリの値の読み取り
    • 物理メモリの値の書き込み
  • PCIコンフィギュレーション空間アクセス
    • PCI コンフィギュレーションレジスタの読み取り( HalGetBusDataByOffset )
    • PCI コンフィギュレーションレジスタの書き込み( HalSetBusDataByOffset )

さらに、このドライバーは開いているハンドルの数を追跡し、メモリマッピングを呼び出し元のプロセス ID と関連付けます。

全体として、このドライバーは汎用的なカーネルモードのハードウェアアクセスレイヤーとして機能し、ポートI/O、MSR アクセス、物理メモリマッピング、および PCI コンフィギュレーション操作のためのプリミティブ(基本機能)を公開します。このような機能は通常、ハードウェア診断ツール、ファームウェアユーティリティ、または低レベルのシステムユーティリティによって使用されますが、特権のないユーザーモードからアクセス可能な場合、悪用される可能性のある強力なプリミティブも提供します。

このマルウェアによって頻繁に使用される 2 つの重要な機能は、物理メモリの読み取りと書き込みを行う機能です。

 

図39. 物理メモリ読み取りIOCTL

 

図40. 物理メモリ書き込み IOCTL

 

ドライバーをロードした後、マルウェアは Windows のバージョンを特定する処理に進みます。そのために、まず PEB ベースの検索ルーチンを使用して必要な API 関数を解決します。これは、このマルウェア全体を通じて一貫して用いられている手法です。

 

図41. DLL 解決

 

図42.  API 解決

 

その実装は、プロセス環境ブロック( PEB )を解析し、モジュール名のハッシュを検索することでターゲットモジュールを特定します。次に、 ResolveExportByHash  関数が、先ほど見つかった DLL からモジュールのベースアドレスを取得し、その PE ヘッダーを解析して関数ハッシュに対応する関数を見つけ出します。この関数は、API 関数のアドレスを PE オフセット、または仮想アドレスのいずれかとして提供することができます。

いくつかの初期化とチェックを行った後、rwdrv.sys のハンドルを取得し、続いてマルウェアの EDR 関連の部分、すなわち EDR の回避、無力化、および無効化を担うカーネルトリックへと進みます。

 

図43. 「 rwdrv.sys 」のドライバーハンドルの取得

 

図44. EDRキラー部分の概要

 

ここでその詳細を簡単に見てみましょう。処理はまず、物理メモリページのベクター(配列)を構築することから始まります。このベクターは、後続のメソッドで使用されます。

 

図45. ページフレーム番号 ( PFN ) メタデータリストの初期化ロジック

 

上記の if 文にある SetMemLayoutPointer 関数は、NtQuerySystemInformation API 関数を利用して、物理メモリページに関する Superfetch 情報を収集します。この情報は、グローバル変数( mem_layout_v1_ptr または mem_layout_v2_ptr )にポインタとして格納されます。どちらが使用されるかは、関数に渡される引数である version 変数によって決まります。今回のケースでは、v1 は関数の初回呼び出し用、v2 は 2 回目用です。言い換えれば、実行中の Windows システムで動作するバージョンを総当たり(ブルートフォース)で試行していることになります。

 

図46.  Superfetch 構造体および NtQuerySystemInformation の呼び出し

 

BuildSuperfetchPfnMetadataList 関数は非常に大規模で複雑です。簡略化して説明すると、まず mem_layout ポインターを使用して総ページ数を計算することから始まります。

 

図47. 総ページカウントアルゴリズム

 

その後、最後に再び NtQuerySystemInformation を使用して物理ページとそのメタデータを取得し、この情報をグローバルベクター( g_PfnVector )に格納して処理を終了します。

 

図48. Superfetch 構造体

 

図49. グローバル物理メモリリストベクトルの構築

 

先ほどのブロックに戻りますが、次のステップでは、特定の操作(プロセス作成、スレッド作成、イメージロードイベントなど)に対するコールバックを削除することにより、EDR を無力化(ブラインド化)します。

 

図50. EDR コールバックの削除

 

unregister_callbacks 関数は、マルウェア内に保存されている 300 以上のドライバー名のリストを順に処理します。

 

図51. EDRドライバー名リスト

 

図52. unregister_callbacks 関数

 

これはまた、他のいくつかの関数でも使用されている、マルウェアの全体的な実装(手法)を示しています。特定の API 関数を使用して、実際に使用している関数またはオブジェクト(この場合はカーネルコールバックである cng!CngCreateProcessNotifyRoutine )へのオフセットを計算します。また、プロセスの仮想アドレス空間内にあるこのオブジェクトには触れません。以前にロードされたドライバー(「 rwdrv.sys 」)を使用して、その物理メモリアドレスを取得します。このロジックおよびドライバーとの通信は read_phy_bytes 関数に実装されており、メモリの上書きについても同様です。ドライバーとの通信を処理するためには write_to_phy_mem 関数が使用されます。ドライバーと通信する DeviceIoControlImplementation 関数は、write_to_phy_mem 内に実装されています。

図53. write_to_phy_mem 内で呼び出される DeviceIoControlImplementation

 

図44 に示されている他のコールバック関連の関数も、先ほど説明したものと同様に機能します。これらは、EDRミニフィルタードライバーによって設定された他のEDR 固有のコールバックを上書き、または登録解除します。

EDR キラーの最終部分は、別のドライバー(「 hlpdrv.sys 」)をロードすることから始まります。

 

図54. hlpdrv.sys のロードと使用

 

マルウェアは、IOCTLコード 0x2222008 を使用してドライバーを操作し、システム上で実行されている EDR プロセスを終了させます。これにより、プロセスの保護解除と終了を担う、ドライバー内の関数が実行されます。

 

図55. hlpdrv.sys における保護プロセス終了関数

 

図56に示されているように、一度終了させられると、Windows Defender などの EDR プロセスは実行されなくなります。

 

図56. Windows Defender プロセスの終了

 

さらに、CiValidateImageHeader コールバックを復元します。RestoreCiValidateImageHeaderCallback 関数は図57 に示されています。

 

図57. コード整合性チェックの復元

 

これは、図52 で以前に確認したのと同じ概念(手法)を使用します:

 

  • 既知の API 関数を解決する。
  • この関数をアンカー(起点)として使用し、そのコード内の特定の命令を特定する。
  • この命令は、そのオペランドの 1 つに、目的のオブジェクトまたはその近くを指すポインターを含んでいる。
  • その命令内からターゲットオブジェクトへのポインターを特定する。
  • オペランドに対して符号拡張(サインエクステンション)を実行する。
  • 追加のオフセットを加算して、検索対象のオブジェクト(この場合は CiValidateImageHeader コールバック)の最終的なアドレスを計算する。
  • CiValidateImageHeader への元の関数ポインターを復元する。

 

マルウェアは以前、CiValidateImageHeader へのコールバックを、常に true を返す関数である ArbPreprocessEntry のアドレスで上書きしていたことに注意してください。言い換えると、これにより元のコード整合性( Code Integrity )チェックが復元されたことになります。

 

Summary

 

このブログ記事では、Qilin ランサムウェアの攻撃で確認された悪意のある「 msimg32.dll 」に隠された感染チェーンについて、技術的な観点から深く掘り下げました。侵害されたシステム上で最新の EDR 保護機能を回避、あるいは完全に無効化するために、マルウェアがどのような巧妙な手口を用いているかを示しています。

現代のマルウェアがいかに多くのハードルを越えなければならないかを知ることは、防御側にとって心強いことです。しかし同時に、最先端の防御メカニズムであっても、執念深い攻撃者によって突破される可能性があるという事実も浮き彫りにしています。防御側は、保護を単一の製品に決して依存するべきではありません。その代わりに、Talos は多層的なセキュリティアプローチ(多層防御)を強く推奨します。これにより、攻撃者が仮に 1 つの防衛線を突破できたとしても、検知されずに潜伏し続けることは極めて困難になります。

 

Coverage

 

以下の ClamAV シグネチャは、この脅威を検知し、ブロックします。

  • Trojan.EDRKiller download attempt

以下の Snort ルール( SID )は、この脅威を検知し、ブロックします。

  • Snort2: 66181, 66180
  • Snort3: 301456

Indicators of compromise (IOCs) 

 

msimg32.dll 

MD5: 89ee7235906f7d12737679860264feaf
SHA1: 01d00d3dd8bc8fd92dae9e04d0f076cb3158dc9c
SHA256: 7787da25451f5538766240f4a8a2846d0a589c59391e15f188aa077e8b888497  

 

EDRKiller.exe (non-fixed memory dump with overlay)  

1305e8b0f9c459d5ed85e7e474fbebb1
84e2d2084fe08262c2c378a377963a1482b35ac5
12fcde06ddadf1b48a61b12596e6286316fd33e850687fe4153dfd9383f0a4a0 

Time stamp:  0x684d33f0 (14. June 2025, 08:33:52 UTC)
ImpHash   : 05aa031a007e2f51e3f48ae2ed1e1fcb
TLSH: T1B4647C01B7E50CF9EE77C638C9614A06EA72BC425761DADF43A04A964F237D09E3DB12 

 

rwdrv.sys (dump)  

MD5: 6bc8e3505d9f51368ddf323acb6abc49
SHA1: 82ed942a52cdcf120a8919730e00ba37619661a3
SHA256: 16f83f056177c4ec24c7e99d01ca9d9d6713bd0497eeedb777a3ffefa99c97f0 

 

hlpdrv.sys (dump)  

MD5: 3bb14eb610885692bd1e4e9d268d8c8d
SHA1: c748ad6828f8f401ebfa859a3fec40ce47a1e30d
SHA256: 99325e2ee68a1c22d39bd508e6d216e06177817be325b131848a23ca786a2c54 

 

Authors

Takahiro Takeda

Security Research Engineer

Talos, TI&I, Outreach team

コメントを書く