Evolution RAT:未公開のHVNCセッション乗っ取りプラットフォームをリバースエンジニアリングする | AIネイティブなアクティブディフェンス

TL;DR

  • 直接配信される.vbsファイルをurlscan.ioで調査したところ、世界中でわずか3件という結果が得られました。そのうち回収に十分な時間オンライン状態を保っていたのは1件のみで、未公開のx86-64ペイロードに至る完全なチェーンを内包した3.3MBのスクリプトでした
  • この5段階のチェーンは、難読化されたVBScriptから始まり、動的生成されたPowerShell、AES-256-ECB、独自のBase85アルファベット、リフレクションでロードされる.NETインジェクター、そしてAppLaunch.exeへのプロセスホローイングへと進みます。永続化のために、ドロッパーは自身のソースを逆順にしたものを偽の「Windows Security」XHTML文書内に隠します
  • 入れ子になったPE内のPDBパスは、Overlord HVNC DLL Loadedというバナー文字列によって独立に裏付けられ、このファミリーをEvolution RATと特定できます。ビルダーがパッチを当てる.cfgマーカーはホストに依存しないファイルフィンガープリントを提供し、固定された平文のhelloフレームはビルドに依存しないネットワークシグネチャを提供します
  • このペイロードは、HVNC、リモート入力、ファイル・プロセス制御、キーロギング、インジェクション、接続の強制終了、リバースプロキシなど、対話的アクセスのための59個のコマンドを公開しています。収集時点では配布サーバーとC2は同一のコロンビアのIPアドレスを共有していました
  • この脅威の本質的な能力はセッションIDの窃取にあります。Evolution RATは稼働中のブラウザプロファイルを複製し、その単一インスタンス用ロックを解除して、隠しデスクトップ上でクローンを起動し、Chromeがフィンガープリンティングスクリプトに公開するのとまったく同じ形式で35のホスト属性を整形します。リバースプロキシは、盗まれたCookieおよびデバイスIDに被害者のネットワーク発信元を付加します
  • Caronteはスクリプトチェーン全体を自動的に展開し、入れ子になったPDBメタデータと.cfgのC2情報を抽出し、最終ペイロードをマッピングしました。アナリストの役割は、得られた証拠の検証と解釈にとどまります

主な調査結果の概要

カテゴリ 詳細
調査の意義 入れ子になったPE内のPDBパスから命名され、Overlord HVNC DLL Loadedバナーによって独立に裏付けられた、Evolution RATに関する初の公開技術文書
配布チェーン 3.3MBのVBS → 生成されたPowerShell → AES-256-ECBローダー → 独自Base85 → リフレクション型.NETインジェクター → AppLaunch.exeにホロー化されたx86-64ペイロード
永続化 逆順にしたVBScriptを偽のXHTML「Windows Security」文書に埋め込み、XORで難読化されたStartupローダーが復元・実行。もう一つの経路ではStartupにWindowsDefender.exeをインストール
セッション乗っ取り 稼働中のブラウザプロファイルの複製、ロックファイルの削除、MinHookによるパスリダイレクト、Chrome App-Bound Encryptionへの対応、そして35個のブラウザ互換フィンガープリントフィールド
コマンド&コントロール 暗号化されていないフレーム形式のTCPプロトロコル上で59個のコマンド。serversniperxx.duckdns.org:4577は収集時点で配布ホストとIPアドレスを共有していた
持続的な検知手法 .cfgセクション内のビルダーマーカー##CFG_START## / ##CFG_END####、および固定のクライアントhello AD DE 02 00 10 02 00 00
公開検知状況 収集時点:.NETインジェクターは公開ソース4件中0件、x86-64ペイロードは4件中1件で検出

攻撃チェーンの概要

このチェーンは非常に手が込んでいますが、その本当の目的は最終段階になって初めて見えてきます。各段階はそれぞれ異なる検知面を取り除いていき、ペイロードが実行されるとすべての主要な機能が、再利用可能なブラウザセッションを構成する一部分を担うようになります。すなわち、認証状態、起動可能なプロファイル、それに合致するデバイスフィンガープリント、そして被害者自身のネットワーク発信元です。

発見の経緯:世界でわずか3件の結果

1つのクエリが3つのURLを返しました。翌朝には、そのうち2つは応答しなくなっていました。残る1つを開くと、5段階の実行プロセス、隠しデスクトップ、59個のコマンド、そしてこれまで公開の報告に一度も登場したことがない内部名を持つマルウェアファミリーが姿を現しました。

このサンプルは、投稿や報告から得られたものではありません。単純な観察に基づいたハンティングから見つかったものです。すなわち、ランディングページもリダイレクトチェーンもなく、スクリプトペイロードを直接配信するURLは非常に稀であり、世界規模で列挙できるほどだという観察です。

urlscan.ioのクエリは単純なものです:

task.url:".vbs" AND date:>now-24h

この期間、プラットフォーム全体でこのクエリが返した結果は3件でした:

http://asegurar2026.duckdns.org/sostener2.vbs
http://172.245.214.91/fridaexploit1.vbs
http://64.89.160.17/System1.vbs

この程度の件数であれば、通常のトリアージの課題は逆転します。管理すべきシグナル対ノイズの比率がそもそも存在しません。ノイズがほぼ皆無だからです。各結果は、通常フィルターを書くのに費やす時間で個別に取得・調査できます。

3件のうち2件は1日以内に消えてしまいました。172.245.214.91と64.89.160.17は、いずれの特徴も把握できないまま翌朝には応答しなくなっており、本レポートではこの2件について何も述べていません。それがこの種のハンティングの現実的な姿です。クエリ自体は安価に実行できますが、調査可能な時間の窓は非常に狭く、見つかったものの大半は調べ終わる前に消えてしまいます。稼働し続けていたのはasegurar2026.duckdns.orgのみであり、本レポートが扱うのはこの1件だけです。

このホストは同一のペイロードを少なくとも2つの名前、sostener2.vbsとenvifa.vbsで配信しており、両ファイルはバイト単位で同一です(SHA256はc8f229c7843f5fec053c04fb78b2ed045c029671412d74a07ce40608eeef1797)。素のIPアドレスも同様に応答します。http://181.237.42.61/envifa.vbsは200 OKを返し、ハッシュ値も同一です。つまりDuckDNSの名前は必須要件ではなく単なる利便性のためのものであり、このダイナミックDNSエントリのみをテイクダウンしてもペイロードの除去にはなりません。

注目に値するのはサーバーバナーです:

Server: Apache/2.4.58 (Win64) OpenSSL/3.1.3 PHP/8.0.30

Windows上のApacheで、PHP 8.0を使い、コロンビアのアドレスのドキュメントルートから.vbsを配信しています。この組み合わせは一般的な商用ホスティングとしては異例で、WAMP環境のマシンがインターネットに公開され、配布拠点として転用されたと考えるのが自然です。ただし、このサーバーの所有者については入手可能な証拠からは特定できませんでした。

命名はすべてスペイン語です。asegurar(確保する、保証する)、sostener(保持する、支える)。コロンビアのアドレスと合わせて考えると、これはLATAM(中南米)関連の運用背景を示唆していますが、被害者の地理的分布や運用者の意図した標的を確定するものではありません。

ドロッパー:1つのBase64ストリームを343個に分割

sostener2.vbsのサイズは3,346,929バイトですが、その中身がどこに使われているかを正確に把握しておく価値があります。直感的な答えは誤っているからです。

このファイルの大部分はパディングではありません。大部分がペイロードそのものです。単一のBase64ストリーム、文字数にして3,308,332文字、つまりファイル全体の98.9%が、インラインで運ばれるAES暗号化された次段のペイロードです。それ以外の部分、すなわちロジック、永続化処理、おとりの関数などはすべて残りの38KBに収まっています。ダウンロードを行うドロッパーは存在しません。x86-64バイナリに至るまでのチェーン全体が、この1つのテキストファイルに同梱されているのです。

このストリームの上には、3つの難読化技術が重ねられています:

  • 文字列に対するXOR 0x99。意味のあるリテラルはすべて、10進バイト値をパイプ区切りで並べた配列として保存され、単一バイトでXORをかけたうえで実行時に1つのヘルパー関数によって再構成されます。このファイルに対する静的な文字列抽出では使える情報は何も得られません。API名もパスもURLも、読み取れる形では一切存在しません。
  • 343個のリテラルを、順序を崩して再構成。Base64ストリームは1つのリテラルではありません。343個のDim代入に分割され、343個の別々のステートメントによってv0x86fadに連結されますが、その順序は宣言順と343カ所中0カ所しか一致しません。ファイルを上から下に読み、リテラルを出現順に結合しても、デコード結果は何も得られません。ペイロードを復元するには、単にBase64をgrepするだけでなく、連結順序を解決する必要があります。
  • おとりコード。34個の関数・サブルーチン定義があり、その大半は値を計算して破棄するだけのものです。これに、If 0 Then Randomize Timerという形の170行が織り交ぜられています。この条件は常にfalseであり、このステートメントが実行されることはありません。

このおとりコードは、しばしば単なる容量稼ぎのパディングだと説明されますが、ここではそうではありません。この170行は合計でおよそ4KB、ファイル全体の0.12%にすぎません。スキャナーのバイト予算を使い切らせるものではないのです。これが実現しているのは、人間の目と単純なパーサーを欺くことです。制御フローを読み解こうとする人間は、目に見えるステートメントの9割を読み飛ばさなければならず、関数定義をインデックス化するツールは、どこにも繋がらない34個の名前を得るだけです。

このファイルがスキャナーに課す負荷は、パディングではなくペイロード自体に由来します。おとりの行をすべて取り除いても、3.34MBのスクリプトが残ります。このサイズこそが、このマルウェアの正体なのです。

永続化:マルウェアを逆順に格納した偽のXHTMLページ

永続化の仕組みは、このドロッパーの中でもっとも特徴的な部分であり、特定の製品ではなく検知手法のカテゴリそのものを打ち破るものであるため、詳しく説明する価値があります。

スクリプトはまず、自身がすでにStartupフォルダから実行されているかどうかを確認し、そうでない場合にのみ永続化をインストールします。実行されていない場合、WScript.ScriptFullNameで自身のソースを読み込み、StrReverseで文字単位で逆順にし、その逆順テキストをXHTMLとして提示される文書の中に埋め込みます:

<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Windows Security</title></head>
<body>
<div id="ms-2EDe7CMv96" data-key="8MxUjqEtBpLuwtsg">
<!--2EDe7CMv96-->
[reversed VBScript source]
<!--/2EDe7CMv96-->
</div></body></html>

このファイルは2箇所に書き込まれます:

%APPDATA%\Microsoft\manifest_ckCGj9.xhtml
C:\Users\Public\manifest_ckCGj9.xhtml

また別のローダーが、CfgShieldckCGj9.vbsとしてStartupフォルダに設置されます。ファイルの先頭には、人間の閲覧者を意識した3行のコメントが付けられています:

' Windows SmartScreen Filter Config
' Version 4.8.494 - Microsoft Corporation
' (c) 2026 Microsoft. All rights reserved.

このローダーは自身の文字列に対してXOR 0xCDを用いており、これはドロッパーの0x99とは異なるキーです。そのため、一方用に書かれたデコーダーではもう一方を読み解くことができません。

デコードされると、このルーチンは%APPDATA%を展開し、\Microsoft\manifest_ckCGj9.xhtmlを探し、見つからなければC:\Users\Public\manifest_ckCGj9.xhtmlにフォールバックします。data-key="8MxUjqEtBpLuwtsg"というリテラルでファイルの妥当性を検証し、2つの2EDe7CMv96コメントマーカーの間のテキストを抽出して逆順にすることで元のソースを復元し、先頭のOption Explicitを取り除いたうえでExecuteGlobalで実行します。

さらに3つのデコードされた文字列、Microsoft.Update.Session、Win32_OperatingSystem、root\SecurityCenter2がローダー内に宣言されていますが、一度も使われることはありません。これらはドロッパーの死んだ関数と同種のおとりであり、急いで解析するアナリストがこれを環境チェックだと読み違えるように配置されています。

この仕組みが検知に与える影響は直接的です。再起動を生き延びるアーティファクトは、スクリプトとして認識できる姿をしていません。拡張子は.xhtmlで、<title>タグはWindowsのセキュリティコンポーネントを名乗り、本文には逆順に書かれたVBScriptが含まれています。

元のドロッパー向けのシグネチャは、この本文には一致しません。すべての文字列、API名、キーワードが逆順になっているためです。認識可能なスクリプト構文を持つ唯一の永続化ファイルは小さなStartupローダーのみで、こちらの文字列はXORで暗号化されています。

同じ6文字のトークンckCGj9がXHTMLのファイル名とローダーのファイル名の両方に出現し、トリアージ時にこの2つのアーティファクトを同一ホスト上で紐付ける手がかりになります。

第2〜第5段階:AES、独自Base85、そしてホローイング

ドロッパー自身は復号処理を一切行いません。代わりに第2のスクリプトを生成し、その処理をPowerShellに委ねます。

具体的には、このVBScriptはメモリ上に短いPowerShellプログラムを構築し、ADODB.Streamを通じてBOMなしUTF-8として%TEMP%\<random hex>.ps1に書き込み、それを実行します:

C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -ExecutionPolicy Bypass -File "<%TEMP%\...ps1>"

生成される第2段階は小規模です。その役割は、343個のリテラルからなるBlobをBase64デコードし、AES-256のECBモード、PKCS7パディングで復号し、平文をディスクに書き込むことだけです。

すべての型名・メソッド名は文字列の断片から組み立てられ('System'+'.Con'+'vert'、('From'+'Base64S'+'trin'+'g')、('Create'+'Decryp'+'tor'))、リフレクションによって呼び出されるため、スキャナーが照合できる完全な.NET識別子は残りません。唯一隠されていないリテラルが鍵です:

Be1OluQXuNlV3c/vviqtY1xEW70mAk13+uxHyLqeTZ8=

これは32バイトで、05ed4e96e417b8d955ddcfefbe2aad635c445bbd26024d77faec47c8ba9e4d9fです。

2,481,247バイトの平文自体もPowerShellスクリプトです。これは%TEMP%\<random hex><random hex>\ClJdZcnG.ps1に書き込まれ、2度目のpowershell.exe起動を経ることなく、コール演算子で呼び出されます。そのため第3段階は、それを復号したプロセスの内部で実行され、独立したコマンドラインとしては一切現れません。

3秒後、生成されたスクリプトはそのファイルとそのディレクトリを削除し、続けてVBScriptが第2段階を削除します。このチェーンは中間のアーティファクトを消去していき、残るのはXHTMLの永続化と最終ペイロードが書き込むものだけです。

この再構成は正確です。343個の連結を解決し、結果をBase64デコードし、上記の鍵で復号すると、SHA256がa49c01a5cc1c06be5f2e6cbd0f63554c7755e45f3931c08541da643014c8c9e5と一致する2,481,247バイトがバイト単位で得られます。

第3段階は各バイナリをBase85エンコードされたBlobとして運びますが、標準のBase85ではありません。使われているアルファベットは独自のものです:

0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~

数字が先頭に来て、大文字、小文字と続き、両方の引用符とバックスラッシュを除いた記号の並びで締めくくられています。これによってBlobをエスケープなしでPowerShellのヒアストリング内に収めることができます。グループサイズ、基数、短グループのパディング方式は標準的ですが、文字と値のマッピングだけが標準的ではありません。Ascii85にもRFC 1924のZ85セットにも一致しません。これだけで十分な難読化効果があります。標準のデコーダーはゴミデータを生成し、既知のエンコード方式を順に試すアンパッカーも一致を見出せません。運用者側が負担するコストは、スクリプト自体に組み込まれたおよそ40行のデコードロジックにすぎませんが、アナリスト側が負担するコストは、このBlobが既製のツールでは何一つ処理できないという点です。

デコードするとx86-64のペイロードが得られます。もう一つの小さなPEも、同じスクリプト内に単純なBase64文字列として同梱されています:

ファイル サイズ 役割
.NETインジェクター 9,728バイト スクリプト内にBase64として格納され、Reflection.Assembly::Load経由でメモリ内にロードされる。型はyMOxml.nRCN06Y、メソッドはC3FrcKQp
x86-64ペイロード 1,972,736バイト 独自Base85のBlobであり、Evolution RATそのもの

このインジェクターのエントリメソッドが受け取る引数はちょうど2つで、ペイロードのバイト列とターゲットプロセスのパスです。設定もC2もオプションも一切ありません。このメソッドが行う処理は1つ、プロセスホローイングをAppLaunch.exeに対して実行することであり、そのパスはポインタ幅によって選択されます:

C:\Windows\Microsoft.NET\Framework64\v4.0.30319\AppLaunch.exe   (64-bit host)
C:\Windows\Microsoft.NET\Framework\v4.0.30319\AppLaunch.exe     (32-bit host)

AppLaunch.exeは.NET Frameworkに同梱されるMicrosoft署名済みのバイナリです。これはほぼすべてのWindows環境に存在する正規のホストプロセスであり、これが動作していること自体は表面上異常には見えません。

このインジェクターについてはもう一段落分の価値があります。そのインポート面が意図的に無内容にされているためです。宣言されているP/InvokeはGetModuleHandleA、GetProcAddress、VirtualAlloc、CreateProcessA、CloseHandleのみで、すべてkernel32由来です。メタデータの中にインジェクションを示唆するAPI名はどこにもありません。実際のAPIセットは0x2FでXORされた文字列として格納されており、実行時にntdll.dllから解決されます。このntdll.dllという名前自体も同じ方式でエンコードされています:

a[zABN_yFJX`I|JL[F@A       ->  NtUnmapViewOfSection
a[nCC@LN[JyF][ZNCbJB@]V    ->  NtAllocateVirtualMemory
a[x]F[JyF][ZNCbJB@]V       ->  NtWriteVirtualMemory
a[hJ[l@A[JW[{G]JNK         ->  NtGetContextThread
a[|J[l@A[JW[{G]JNK         ->  NtSetContextThread
a[}J\ZBJ{G]JNK             ->  NtResumeThread
41 5B 4B 43 43 01 4B 43 43 ->  ntdll.dll

それぞれがMarshal.GetDelegateForFunctionPointerで呼び出し可能な形に変換されます。これはWin32の呼び出し順序ではなく、ネイティブAPIによるホローイングです。VirtualAllocExもWriteProcessMemoryもSetThreadContextも使われません。Win32側のAPI名に依存する検知ロジック、インポートテーブルであれその階層に配置されたフック集合であれ、そうしたロジックからは、このアセンブリがプロセスを1つ生成しただけのようにしか見えません。

ファミリー名の特定:入れ子のPE内に残されたPDBパス

x86-64ペイロード自体にはPDBパスがありません。そのデバッグディレクトリに含まれるのは、Microsoftのリンカーが出力するプロファイルガイド最適化のセクションレイアウトを示すPOGOレコード1件のみで、それ以外はありません。外側のバイナリのデバッグ情報を読んだだけでは、このファミリーは無名のままです。

その名前はさらに一段深いところにあります。ペイロードのリソースセクション内のオフセット1810200には別の完全なPEファイルが存在し、こちらはデバッグパスが除去されずにビルドされていました:

C:\Users\USER\Desktop\Evolution RAT\Overlord-Server\dist-clients\HVNCInjection.pdb
GUID 59965EFB-F602-42DC-94D0-FCF15841EFC1   age=1

このパスからは3つのことが読み取れます。プロジェクト名はEvolution RATです。サーバー側のコンポーネントはOverlord-Serverと呼ばれ、クライアントのバイナリはdist-clientsディレクトリに出力されます。これは、共通のサーバープロジェクトから各運用者向けのクライアントをコンパイルするビルダーの構成です。そして入れ子になったモジュール名はHVNCInjectionであり、これは関数を1つも逆コンパイルするまでもなく、このペイロードの主要な機能を物語っています。

このパスはC:\Users\USER\Desktopから始まっており、このプロジェクトがUSERという名前のアカウントのデスクトップディレクトリからビルドされたことを示しています。デバッグパスは入れ子のモジュールには残っていましたが外側のバイナリには残っておらず、これがトップレベルのPEしか読み取らないツールにはこのファミリー名が見えない理由です。

入れ子のモジュールは、このオフセットから156,160バイトのところでセクションテーブルが終わる64ビットDLLであり、そのRSDSデバッグレコードは外側のペイロードのファイルオフセット1944736に位置しています。このモジュール自身の文字列は、PDBとは別の形でこの名前を裏付けています。すなわちこのモジュールはOverlord HVNC DLL Loadedというバナーを%TEMP%\crashlogovd.logに書き込みます。ビルドの異なる部分から生成された2つのアーティファクトが、いずれもOverlordという名前で一致しているのです。

Evolution RAT、Overlord-Server、SniperLogs、HVNCInjectionで検索しても、公開されている文書は一切見つかりません。ベンダーレポートも、サンドボックスの解析記事も、マルウェアリポジトリの登録も、フォーラムへの投稿もありません。検索でヒットするのはたまたまHVNCを実装している無関係なファミリーだけです。この技術セット自体はすでに確立されたものですが、この具体的な実装については公開の記録にはどこにも記述がありません。

C2は平文であり、これは設計上の判断

このペイロードは、設定情報を.cfgという専用のPEセクションに保存しています。仮想サイズは163バイトで、RVAは0x1A0000、ファイルオフセットは0x193600、暗号化されておらず、固定のレイアウトを持っています。このセクション全体をバイト単位で示すと以下のとおりです:

offset  len  contents
  0     13   "##CFG_START##"
 13      3   zero padding, aligning the next field to 16
 16    128   host, null-padded: "serversniperxx.duckdns.org"
144      2   port, uint16 LE: E1 11  =  4577  (0x11E1)
146      1   zero padding
147     13   "##CFG_END####"
160      3   zero padding

この平文での保存は見落としではありません。マーカー自体がその理由を説明しています。##CFG_START##と##CFG_END####は、コンパイル済みバイナリの中で運用者のホストとポートをパッチするためにビルダーが検索する目印です。このセクションが平文かつ固定長でなければならないのは、コンパイラへのアクセス権を持たないビルドツールが後からここに書き込みを行うためです。128バイトのnullパディングされたホストフィールドも同じ制約の産物であり、ファイルのサイズやレイアウトを変えられないその場書き換えです。各フィールドを囲むアライメントパディングによって、パッチを当てるツールは構文解析ではなく固定オフセットへのシークだけで済むようになっています。

これによりこれらのマーカーはファミリーレベルのフィンガープリントとなります。運用者がどのC2にパッチを当てようとも、このツールキットが生成するあらゆるビルドを通じて不変です。これが、後述のYARAルールがserversniperxx.duckdns.orgではなくこれらのマーカーに照準を合わせている理由です。ホストとポートは変わりますが、このセクション構造こそがこの製品そのものです。

serversniperxx.duckdns.orgは181.237.42.61に解決され、これはsostener2.vbsとenvifa.vbsを配信しているのと同一のアドレスです。配布はWindows上のApacheインスタンスを通じてポート80で行われ、コマンド&コントロールは同一マシンのポート4577で稼働しています。ポート4577はTCP接続を受け付けますが、HTTPには応答しません。これは本レポートで後述する、クライアントが実装しているフレーム形式のバイナリプロトコルと整合しています。

内部の命名はツールキット全体で一貫しています。C2のホスト名はserversniperxx、被害者ごとの識別子の形式はSniperX_%d、そしてキーログのディレクトリはC:\ProgramData\SniperLogsです。運用者側のインフラから被害者のディスク上のディレクトリ名に至るまで、同じ語根が貫かれています。

このセクションの内容はすべて、ペイロードのバイナリから静的に確認されたものです。

この脅威の中核モジュールはHVNCです。modules\hvnc_injection.dllはReflectiveLoaderのエクスポート関数を通じてリフレクションでロードされ、ディスク上のロード可能なイメージとして触れられることは一切ありません。HVNC(Hidden Virtual Network Computing)は、被害者からは見えない第2のWindowsデスクトップを作成し、攻撃者がそれを対話的に操作します。そのデスクトップ上で起動されたアプリケーションは、被害者自身のセッション、被害者自身のトークン、被害者自身のネットワーク上の立ち位置で動作する一方、見える方のデスクトップには何も表示されません。

  • GhostStandby。SetWindowDisplayAffinity(0x11)で作成された透明なウィンドウで、これは画面キャプチャからウィンドウを除外するフラグの組み合わせです。運用者がその背後に配置したものは、スクリーンショット、画面録画、リモートサポートセッションのいずれからも一切見えません。これは正規のDRMやパスワードマネージャーがキャプチャからコンテンツを隠すために使うのと同じAPIを、運用者の活動を隠すために転用したものです。
  • winlogon.exeへのなりすまし。SeDebugPrivilegeを取得したうえで、winlogon.exeに対してDuplicateTokenExとImpersonateLoggedOnUserを実行し、SYSTEMレベルのセキュリティコンテキストを得ます。
  • アンチ解析。IsDebuggerPresent、SetUnhandledExceptionFilter、そしてNtSuspendProcess/NtResumeProcessのペア。
  • ブラウザスレッドの一時停止。SQLiteの認証情報データベースを読み取る前にブラウザのスレッドを一時停止し、読み取り後に再開します。これにより、データベースを一貫した状態で、かつブラウザ自身のファイルハンドルと競合することなく読み取れます。
  • Defenderの除外設定。powershell -WindowStyle Hidden -Command "Add-MpPreference -ExclusionPath 'というコマンドのプレフィックスと、対応する-ExclusionProcess版がいずれも文字列定数として存在し、サービス自体を無効化しようとするのではなく、自身のパスとプロセス名をDefenderの除外リストに追加します。
  • 2つ目の永続化経路。このペイロードは、3段階前に確立されたVBScriptの永続化とは独立して、自身をStart Menu\Programs\StartupにWindowsDefender.exeとしてコピーします。また自身をsvchost.exeとしてもコピーします。
  • キャプチャ。画面キャプチャはBitBltとGDI+でJPEGにエンコードされ、ウェブカメラとマイクにはMedia Foundation(MF.dll、MFPlat.DLL、MFReadWrite.dll)が使われます。
  • キーロギング。WH_KEYBOARD_LLとWH_MOUSE_LLのフックがC:\ProgramData\SniperLogs\keylog_%04d%02d%02d_%02d%02d%02d.txtに書き込みを行い、C2チャネル経由で使える小さなログ管理コマンド群、CONTENT|、DELETE、DELETED、EMPTYを備えています。
  • 同期プリミティブ。ミューテックスhvnc:ready、hvnc:launch_ok、hvnc:launch_fail、hvnc_watchdog:stuck、Local\hvnc_rdi_、SingletonSocket。stuck状態を持つウォッチドッグが存在することから、HVNCセッションは一度きりのものではなく、長時間稼働し監視されることが想定されていると分かります。

ネットワーク制御

このペイロードのネットワーク機能のうち3つは、単なる情報窃取ツールに必要な範囲を超えています:

  • 任意のTCP接続の強制終了をSetTcpEntry経由で行い、コマンド0xE3として公開されています。コマンドのペイロードは12バイトで、ローカルIP、ローカルポート、リモートIP、リモートポートから成ります。運用者はホスト上の任意の単一接続を切断できます。
  • 接続の列挙で、コマンド0xE0はJSON形式で報告され、接続ごとに1つのオブジェクト{"pid":%u,"process":"%s","local_ip":"%s","local_port":%u,"remote_ip":"%s","remote_port":%u,"state":"%s"}が返されます。
  • システムプロキシの乗っ取りで、Software\Microsoft\Windows\CurrentVersion\Internet Settingsに127.0.0.1:1を書き込みます。ループバックのポート1はプロキシではなく、そこには何もリッスンしていません。これを設定すると、システムプロキシに従うすべてのアプリケーションについて、被害者のインターネットアクセスが完全に断たれます。

これら3つをまとめて見ると、封じ込め用のツールキットが浮かび上がります。接続の列挙によって、どのプロセスが何と通信しているかを把握できます。SetTcpEntryで特定のセッションを終了できます。デッドプロキシの設定によってホストを丸ごと遮断できます。すでに情報を窃取し終えた情報窃取ツールにとって、これらは何の役にも立ちません。一方、自分自身のチャネルを維持したまま特定の接続を切断する必要がある、対話的なライブセッションを操作する運用者にとっては役立つ機能です。

4つ目のネットワーク機能である、コマンド0xB0のリバースプロキシについては、後述のC2コマンドセットの項で扱います。

認証情報の窃取:20種のブラウザと、Chromeの最新の防御機能

このペイロードは、Chromiumエコシステムのほぼ全体とFirefoxを含む約20種のブラウザを標的とします。Chrome、Edge、Brave、Opera、OperaGX、Vivaldi、Chromium、Yandex、CentBrowser、Iridium、CocCoc、Torch、Comodo Dragon、Epic Privacy、Slimjet、URBrowser、7Star、Amigo、Sputnik、Firefoxです。

使われているクエリは標準的なものです:

SELECT origin_url, username_value, password_value FROM logins;
SELECT host_key, name, encrypted_value, path, is_secure, is_httponly, expires_utc FROM cookies;

履歴はGeckoのプロファイルではmoz_placesから、Chromiumのプロファイルではurlsから読み取られます。

これをコモディティ実装から区別する点が2つあります。

1つ目は、このペイロードがapp_bound_encrypted_keyを処理する点です。Chrome 127で導入されたApp-Bound Encryptionは、Cookieとパスワードの暗号化キーを特定のアプリケーションに紐付けるものであり、これによりLocal Stateファイルをコピーして別プロセスからCryptUnprotectDataを呼び出しても、もはや鍵は得られなくなりました。このペイロードはApp-Bound形式の鍵の経路を明示的に処理しており、Chrome 127以前の方式ではなく、現行のChromeに対して機能することを意味します。

2つ目は、キー名"Google Chromekey1"を指定してNCryptDecryptを呼び出している点です。これはChromeのApp-Bound鍵フローで用いられるCNGキー識別子です。これは汎用的なDPAPIラッパーではなく、以前存在した方式ではなく現行の防御スキームに対して書かれたコードなのです。

転換点:パスワードの窃取ではなくセッションの複製

ここまでの内容であれば、Evolution RATは異例に高機能な認証情報窃取ツールだと解釈することもできます。しかし1つの関数がその解釈を覆します。CloneProfileです。その目的は別の秘密情報を抽出することではなく、被害者の認証済みブラウザを別の場所でも使えるようにすることにあります。

その手順は以下のとおりです:

  1. ブラウザのユーザーデータディレクトリを特定し、攻撃者が制御するパスの下にあるhvnc_%08Xにプロファイルをコピーします。このコピー処理は2段階で行われ、まず重要なファイルを、続いて残りをコピーし、60秒のタイムアウトとC2チャネル経由の進捗報告を備えています。liteモードでは、セッション状態を含まない大容量ディレクトリ、すなわちcache、code cache、service worker、blob_storage、拡張機能ストア、テレメトリのデータベースをスキップします。
  2. コピーからロックファイルを削除します。SingletonLock、SingletonCookie、parent.lock、.parentlockです。
  3. 隠されたHVNCデスクトップ内で、クローンされたプロファイルに対してブラウザを起動します。

手順1には、実装が真剣に対処している厄介な問題があります。稼働中のブラウザは、もっとも興味深いファイルを開いたまま保持しており、単純なコピーはそこで失敗します。このコードはこのケースを明示的に処理しています。SeDebugPrivilegeを有効化し、NtQuerySystemInformationでシステムのハンドルを列挙してロックされたパスをどのプロセスが保持しているかを見つけ出し、ロックしているプロセスからハンドルを複製することで、それでもファイルを読み取ります。ステータス文字列(Hijack: %d locking PIDs for %s、Hijack: %llu handles, searching...、Hijack OK: %s (%lld bytes))は、その意図を明確に物語っています。被害者がプロファイルを奪われるために、ブラウザを閉じる必要はないのです。

手順2は、そのコピーを使用可能にする仕組みです。この4つのファイルは、ChromiumとGeckoがプロファイルディレクトリごとに単一のブラウザインスタンスのみを許可するための仕組みです。これらのファイルが存在すると、同じプロファイルに対する2回目の起動は拒否されるか、稼働中のインスタンスに処理が引き渡されます。コピーからこれらを削除することで、被害者自身のブラウザが元のプロファイルでまだ稼働している間でも、クローンを独立して起動できるようになります。

起動されるのは、認証情報の入力を求めるブラウザではありません。すでにログイン済みのブラウザであり、そのプロファイル内にセッションCookieが存在していたすべてのサイトに対してログイン状態にあります。セッションCookieは、その性質上、認証がすでに完了したこと、しかも第2要素認証を含めて完了したことの証明そのものです。埋めるべきパスワードプロンプトも、傍受すべきワンタイムパスワードも、被害者が承認すべきプッシュ通知も存在しません。なぜなら、アプリケーション側の視点では新たな認証は一切発生していないからです。第2要素認証が破られたわけではありません。被害者自身がそれを満たしており、そのCookieは言わばその領収書なのです。

ブラウザをクローン側に留め置くために、このペイロードはファイルAPI面全体にMinHookのフックをインストールします:

NtCreateFile              NtQueryAttributesFile
NtOpenFile                NtQueryFullAttributesFile
NtDeleteFile              NtQueryDirectoryFile
NtSetInformationFile      NtQueryDirectoryFileEx
CreateProcessW

ブラウザプロセスがファイルを開く、検査する、列挙する、削除するために使うあらゆる経路が横取りされ、さらにCreateProcessWもフックされているため、ブラウザが生成する子プロセスもこのリダイレクトを引き継ぎます。ブラウザは自分が通常のプロファイルを使っていると信じ込んでいますが、実際にはコピーの方を使わされています。

フィンガープリントセット:35個のフィールドと、その用途

このクライアントは接続時にこれらのフィールドを送信しません。要求に応じて収集される仕組みになっており、コマンド0x35によって収集処理全体(ブラウザからの窃取処理を含む)が起動され、結果はオペコード0x64でJSON文書として返されます。フィールドは35個です:

os_name            os_version          os_platform         machine_name
user_name          domain              processor_count     system_dir
screen_width       screen_height       screen_depth        timezone
timezone_offset    timezone_display    language            language_display
languages          cpu_name            cpu_id              cpu_cores
cpu_threads        gpu_name            gpu_driver          gpu_ram
ram_gb             device_memory       mac_address         disk_serial
fonts              fonts_count         browsers            chrome_version
user_agent         webgl_vendor        webgl_renderer

この種のマルウェアにおいて、ホストプロファイルは標準的な機能です。被害者を国、権限レベル、そのマシンに再び戻ってくる価値があるかどうかで振り分けるには十分なフィールド数です。クライアントは接続時にすでに1つ、528バイトの固定フィールド構造(後述のプロトコルの項で扱います)を送信しています。今回のものはそれとは別の、はるかに大規模な収集であり、その過剰な部分は特定のサブセットに集中しています:

webgl_vendor · webgl_renderer · fonts · fonts_count · device_memory · languages · user_agent

これはシステムインベントリではありません。これは項目単位で見ても、まさにブラウザフィンガープリンティングの入力セットです。これは、正当な認証情報を提示しているセッションが本当にそれが主張するデバイスから来ているのかを判断するために不正検知プラットフォームが用いる技術です。WebGLのvendorとrendererの文字列は、ブラウザが報告する形式でGPUとドライバーを識別します。インストール済みフォントの一覧とその数は、フィンガープリンティングスクリプトが利用できる中でも特にエントロピーの高いシグナルの一つです。device_memoryとlanguagesは、OSがその名前で公開しているものではなく、JavaScriptから読み取れるnavigatorプロパティに対応しています。

値は単に収集されるだけでなく、整形されている

決定的な点は、どのフィールドが収集されているかではありません。それらを送信する際にコードが何を行っているかです。

これらの値はいずれもネイティブプロセスがブラウザから直接読み取れるものではないため、このペイロードはWMI、レジストリ、GDIといったシステム由来の情報源からこれらを構築します。そのうえで、それぞれの値を、ブラウザが実際に生成するであろう文字列とまったく同じ形式に整形します。フォーマット文字列はバイナリ内に、対応するフィールド名のすぐ隣に存在します:

user_agent      Mozilla/5.0 (Windows NT %lu.%lu; %s) AppleWebKit/537.36
                (KHTML, like Gecko) Chrome/%s Safari/537.36
webgl_vendor    "Google Inc. (" + <adapter vendor> + ")"
webgl_renderer  "ANGLE (" + <adapter name> + ", Direct3D11 vs_5_0 ps_5_0, D3D11)"
device_memory   %.0f            (integer gigabytes, as navigator.deviceMemory reports)
ram_gb          %.1f            (the same quantity, one decimal, for the operator)
languages       <UI language> + ",en-US"

GPUの文字列はWin32_VideoControllerから取得され、Chromeが用いるANGLEの表記形式で包まれます。ChromeのバージョンはSoftware\Google\Chrome\BLBeaconから読み取られ、ユーザーエージェントのテンプレートに組み込まれます。レジストリの値が存在しない場合のフォールバックには131.0.6778.86がハードコードされています。プラットフォームトークンにはWin64; x64が代入されます。物理メモリの容量は1回取得され、2回出力されます。ram_gbでは小数点以下1桁の精度で、device_memoryでは整数に丸められます。これは2つの異なる用途があるためです。すなわち、被害者を精査する人間向けと、navigator.deviceMemoryが返す値と一致することが期待されるフィンガープリント比較向けです。

この最後のペアが、意図に関する疑問に決着をつけています。RAMの容量を2つの異なる精度で、2つの異なる名前の下に、2回収集するというのは、システムインベントリが行うことではありません。一方が自分自身のためのもので、もう片方はウェブサイトが見たものと一致させる必要がある場合にこそ行うことです。

率直に言えば、これは偶然フィンガープリンティングと重なっているだけの機器インベントリではありません。これは、ホストの実際のハードウェアから、そのホストのブラウザが報告するであろう値を、報告側が用いる構文のまま組み立て、別の場所から再生される準備を整えた、フィンガープリント偽造キットなのです。

この3つの機能を組み合わせると、設計の意図が明らかになります:

  1. CloneProfileは運用者に被害者の認証済みセッションを与えます。
  2. Cookieの窃取は、運用者にそのセッションが依拠しているトークンを与えます。
  3. 35フィールドのフィンガープリントセットは、運用者に、不正検知エンジンがそれらのセッションに付随していると期待するデバイスIDを与えます。

その結果生まれるのは、リスクエンジンがクライアント側から評価する軸において一致してしまう再生セッションです。正しいCookie、正しいユーザーエージェント、正しい言語とタイムゾーンの組み合わせ、正しい申告メモリ、正しいGPUのvendorとrenderer、正しいフォントの一覧。通常であればアカウント乗っ取り、未認識のデバイス、アカウントの履歴と一致しないロケール、既知プロファイルでの新規ブラウザフィンガープリントといった形で警告されるはずのシグナルこそが、この収集処理が満たすためにまさに組み立てられているシグナルなのです。

これが防御側にとって重要な分析上の要点です。すなわち、狙われているのは認証情報そのものではなく、セッションIDだということです。この感染後にパスワードをローテーションしても、すでに複製されたセッションについては何も変わりません。パスワードのリセットよりも、MFAの再登録の方がはるかに重要です。

ここで実際に新しい点は何か

セッションの窃取自体は新しい発見ではなく、本レポートもそれを新発見だと主張するものではありません。Cookieの窃取はもう何年も前からコモディティ化していますし、AiTM(中間者)型のフィッシングキットは、それよりもさらに長い期間にわたって稼働中のセッションをMFAの脇からリレーし続けてきました。プロファイルクローニングも、複数のコモディティ情報窃取ツールにすでに登場しています。Chrome 127で導入されたApp-Bound Encryptionは、まさに「Local Stateをコピーする」手法を打ち破るために実装されたものであり、それはこの種の圧力がすでに十分に理解されていたからこそ存在するものです。

本サンプルには、その一線を越えた点が3つあります。

1つ目は、この収集処理がフォーマットを意識している点です。ハードウェアインベントリを取得する情報窃取ツールは数多くあります。しかし本サンプルは、受信側がどのような形式を期待しているかを把握したうえでそれを生成します。これは、その値がブラウザの報告値との比較に使われることを前提としない限り意味を成しません。

2つ目は、この3つの機能が1箇所に集約され、組み合わされている点です。Cookie、起動可能なプロファイルのクローン、ブラウザの構文に沿ったデバイスフィンガープリント、そしてネットワーク発信元を偽装するリバースプロキシは、通常であれば情報窃取ツール、ローダー、プロキシウェアファミリー、そして購入者が後付けするもの、といった具合に別々のツールに分散しています。ここでは、それらすべてが単一のバイナリの中に、単一の運用者によって、単一のチャネル上で動作するよう統合されています。

3つ目は、このクローンが、ロックしているプロセスからハンドルを複製することによって、被害者のブラウザを停止させることなく取得される点です。ブラウザを閉じる必要のあるコピーは、機会があれば使える程度の機能にとどまります。稼働中のブラウザに対しても機能するコピーは、実運用向けの機能です。

これらのいずれも、新奇な技術を必要とするものではありません。必要とされたのは、狙う製品はパスワードではなくセッションであると決断し、そのうえですべてのコンポーネントをその仕様に沿って構築することだけです。

C2プロトコル:8バイトのヘッダーと、暗号化なし

この機能セットは、運用者が何を再現できるかを説明するものです。プロトコルは、その運用のあらゆる部分がどのように制御されているかを説明するとともに、その過程で2つの持続的な検知手法を明らかにしてくれます。

以下のプロトコルは、ペイロードのバイナリからGhidraで再構成されたものです。C2との間で通信を行ったわけではなく、サンプルを実行したわけでもなく、パケットキャプチャも一切行っていません。ここに記載されているすべては、フレームを構築するコードと、それを検証するコードから得られたものです。

フレーミング

すべてのメッセージは、固定8バイトのヘッダーの後に生のペイロードが続く構造です:

オフセット サイズ フィールド 備考
0 2 マジック 0xDEAD、リトルエンディアン。ワイヤー上ではAD DE
2 2 オペコード リトルエンディアン
4 4 長さ リトルエンディアン、0x1000000(16MiB)を上限

その後にちょうどlengthバイト分のペイロードが続きます。終端マーカーもチェックサムも暗号化層も一切ありません。送信側はFUN_140039620、受信側は同じ構造をFUN_140017050で検証します。

マジックナンバーは命令ストリーム中で読み取れます。0x140039634のバイト列はB8 AD DE 00 00であり、これはmov eax, 0xDEADに逆アセンブルされます。これは、フレームを送信する前にヘッダーにロードされる定数です。

通信は暗号化されておらず、ChaCha20はソケットに一切触れない

双方向とも端から端まで追跡しました。送信側では、FUN_140039620がヘッダーを構築し、送信全体を担うループであるFUN_140039380にバッファを渡します。受信側では、FUN_140017050がヘッダーを検証し、受信全体を担うループであるFUN_140038590を通じて本体を読み取ります。いずれの経路にも暗号プリミティブは一切存在せず、バッファはソケットに到達する際も出ていく際も無傷のままです。

これは正確に述べておく価値があります。というのも、このバイナリには最新の暗号技術が実際に含まれているからです。FUN_140010710はRFC 8439で規定されるChaCha20です。その唯一の呼び出し元はFUN_140019040、ChaCha20-Poly1305のAEADラッパーであり、さらにそれを唯一呼び出しているのがFUN_1400197a0で、これがChromeのApp-Bound Encryptionの鍵を復号します。.dataにハードコードされた3つの鍵は:

B31C6E241AC846728DA9C1FAC4936651
E98F37D7F4E1FA433D19304DC2258042
CCF8A1CEC56605B8517552BA1A2D061C

この経路に属するもので、認証情報窃取用の素材であり、通信用の鍵ではありません。

もう一つ、当初は誤った方向を示唆していたアーティファクトがあります。ChaCha20のシグマ定数expand 32-byte kは、0x14014E248の.rdataに、定数スキャナーが期待する位置にちょうど存在しており、その存在は、このプロトコルがストリーム暗号でラップされていることを示唆しているように見えます。

しかしこれには参照が一切ありません。.text内のRIP相対のディスプレースメントでこのアドレスを指すものは一つもなく、このファイル内に絶対ポインタも存在しません。これはChaCha20の実装が残した死んだデータであり、実際に稼働している暗号処理はChromeの鍵の経路にある別のコピーから自身の状態を初期化しています。したがって、参照グラフを伴わない暗号定数だけを根拠にすると、通信経路に関する誤った評価につながってしまいます。

これが一般的な失敗のパターンです。暗号定数やインポートだけからなされた評価は、バイナリを逆向きに読んでしまい、通信チャネルが保護されていると結論付けてしまいます。それを正すのは参照グラフだけです。コマンド&コントロールは平文で稼働しており、通信を監視できる位置に置かれたセンサーであれば、その中のあらゆるフレームを解析できます。

ハンドシェイク

  1. getaddrinfo、続いてsocket、そしてconnect。
  2. 何かを送信する前にソケットオプションが設定されます。TCP_NODELAY、SO_SNDBUFは0x80000、SO_RCVBUFは0x40000、タイムアウト30000ミリ秒、そしてSIO_KEEPALIVE_VALSでアイドル400000ミリ秒、プローブ間隔5000ミリ秒。
  3. クライアントが最初のフレームAD DE 02 00 10 02 00 00を送信します。すなわちマジック、オペコード0x02、長さ0x210です。続く528バイトは、FUN_14000e760が構築する固定フィールド構造です:
オフセット サイズ 内容
0x000 0x40 HWID:ComputerNameとボリュームシリアル番号
0x040 0x40 ComputerName
0x080 0x40 UserName
0x0C0 0x40 RtlGetVersionによるOSバージョン
0x100 0x10 アーキテクチャ
0x110 0x40 未特定
0x150 0x80 フォアグラウンドウィンドウのタイトル
0x1D0 0x40 予約領域
  1. クライアントはその後、次に進む前にオペコード0x03のフレームを待ちます。
  2. アクティブウィンドウのタイトルを運ぶビーコンをオペコード0x90で開始します。最初は即座に、その後は変化のたびに、250ミリ秒間隔で送信します。
  3. メインループに入ります:ヘッダーの受信、検証、本体の受信、ディスパッチという流れです。

前節で説明した35フィールドのJSONは、この一連の流れには含まれません。接続時のプロファイルは528バイトの固定フィールド構造であり、フィンガープリント文書は運用者がコマンド0x35で要求したときにのみ生成されます。

59個のコマンド

ディスパッチャーはFUN_140019c20です。opcode > 0xF9となる値はすべて拒否し、0x14001B694にあるジャンプテーブルを参照します。59個のオペコードにハンドラが存在します。主流の規則としては、応答用のオペコードはコマンドのオペコードに1を加えたものです。0x00には0x01で応答があり、0x20は画面ストリームを開始し、そのフレームは0x21として返ってきます。

その中でも重要なものは以下のとおりです:

Op 機能
0x00 Ping、0x01で応答
0x20 画面ストリーム開始、フレームはオペコード0x21で送信
0x23 リモートマウス:動作、x、y
0x24 リモートキーボード:押下/解放、仮想キー
0x33 ファイルのステージングと自己更新
0x35 フル収集:ブラウザからの窃取処理を含む35フィールドのJSONがオペコード0x64で返される
0x38 プロセス一覧
0x3A PIDによるプロセス強制終了
0x42 リモートシェルの標準入力への書き込み
0x44 ファイルマネージャーおよびドライブ一覧
0x4A LockWorkStation、画面解像度の変更、ExitProcess
0x4C ファイルまたはディレクトリの再帰的削除
0x4D ShellExecuteによる実行
0x80 マイクキャプチャ、44.1kHz、4バッファ
0xA8 ストリームの品質とフレームレート、10〜100に制限
0xA9 クリップボードへの書き込み
0xB0 リバースプロキシ:任意のホストへの接続
0xC7 / 0xC8 BlockInputのオン・オフ
0xCB HKCUのRunキーによる永続化、設定または削除
0xD0 PEインジェクションおよびホローイング
0xE0 TCP接続テーブル
0xE2 プロセス名によるプロセス強制終了
0xE3 SetTcpEntryによるTCP接続の強制終了、12バイトのペイロード
0xF7 キーログファイルの取得

このコマンドセットは、単なる情報窃取ツールのコマンドリストではなく、完全な対話型リモートアクセス機能を構成しています。画面、マウス、キーボード、入力ブロックはセッションを、ファイルマネージャー、プロセス制御、ShellExecute、PEインジェクションはホストを、レジストリによる永続化と自己更新は足がかりの維持を、それぞれ担っています。

0xB0:出口ノードとしての被害者

1つのオペコードは別途注目に値します。0xB0は運用者が指定したホストへの接続を開き、それを介して中継を行います。これはリバースプロキシであり、この表の中の他のすべての機能とは異なる種類の機能です。ホストからデータを読み取ることもなければ、ホストを制御することもありません。運用者にとってのネットワーク出口点として、そのホストを機能させるものです。

これは、本レポートがこれまで説明してきたパターンを完成させるものでもあります。クローンされたプロファイルは認証済みセッションを提供します。35フィールドの収集は、そのセッションが提示すると想定されるデバイスIDを提供します。0xB0は、リスクエンジンがクライアント側から評価できる3つ目の軸、すなわちネットワークの発信元を提供します。これを通じて送られるトラフィックは、運用者自身のアドレスではなく被害者自身のアドレスから発信されるためです。Cookie、デバイス、ネットワーク——単一のバイナリの中に、それぞれ異なるチェック項目に対応する3つの機能が備わっているのです。

プロトコルからは分からないこと

ごまかさずに述べておくべき不明点が3つあります:

  • 0x03の確認応答のペイロードはソケットから読み取られますが、そのバッファを利用する処理は特定できませんでした。この時点でサーバーが何を返送しているのかは確認できていません。
  • ハンドラは特定できたものの、機能が未解決のオペコードが9個あります:0x30、0x31、0x48、0x49、0xA0、0xA1、0xC0、0xC4、0xF5です。
  • hello構造体のオフセット0x110にある0x40バイトのフィールドは特定できていません。

ネットワークシグネチャ

クライアントが送信する最初の8バイトは固定です。マジック、オペコード0x02、長さ0x210です。ビルダーが.cfgセクションにパッチするホストやポートがどう変わろうと、このバイトは変化しません。したがってこのhelloは、##CFG_START##マーカーがファイルレベルで果たしているのと同じ意味で、ビルドに依存しないネットワークアーティファクトです。ここから直接、2つのルールが導かれます:

alert tcp $HOME_NET any -> $EXTERNAL_NET any (
  msg:"Evolution RAT C2 hello (magic 0xDEAD, opcode 0x02, 528B sysinfo)";
  flow:established,to_server;
  content:"|AD DE 02 00 10 02 00 00|"; depth:8; offset:0;
  threshold:type limit,track by_src,count 1,seconds 300;
  classtype:trojan-activity; sid:1000001; rev:1;)
alert tcp $HOME_NET any -> $EXTERNAL_NET any (
  msg:"Evolution RAT active-window beacon (opcode 0x90)";
  flow:established,to_server;
  content:"|AD DE 90 00|"; depth:4; offset:0;
  classtype:trojan-activity; sid:1000002; rev:1;)

1つ目のルールは、送信元ごとに5分間に1回、最初のチェックイン時に発火します。2つ目のルールは、すでに確立済みの感染を捕捉するものです。ウィンドウタイトルのビーコンはセッションが続く限り送信され続け、その先頭4バイトも同様に固定されているためです。

副次的なシグナルとして、キープアライブの設定は特徴的です。400秒のアイドルと5秒のプローブ間隔という組み合わせは、数分間沈黙した後に短いプローブのバーストを発するという、長期間持続するTCPセッションを生み出します。標準的でないポートへの持続的な接続において、この時間パターンは、内容とは無関係に単独でも表面化させる価値があります。

防御上の優先事項

Evolution RATは4つの異なる層で持続的なシグナルを露呈しています。防御側は、このツールキットを検知し、その目的を封じ込めるために、このキャンペーンの現行のDuckDNSの名前を必要としません。

  • ファイルおよび永続化の検知。大きな逆順テキスト領域を含むXHTMLファイルと、それに付随する小さなStartup VBSローダーを併せて探索してください。ckCGj9というペアのファイル名、2EDe7CMv96のコメントマーカー、data-key="8MxUjqEtBpLuwtsg"はこのキャンペーン固有のものです。##CFG_START##と##CFG_END####は、構成にかかわらずこのペイロードのビルダーを特定します。
  • 実行のテレメトリ。wscript.exeが-ExecutionPolicy Bypassを伴ってPowerShellを起動する動き、ランダムな名前の%TEMP%ディレクトリ配下に一時的に現れる.ps1ファイル、想定外のAppLaunch.exeの子プロセスやホロー化されたイメージを相関させてください。後になってユーザーのStartupフォルダに現れるWindowsDefender.exeは、信頼性の高い第2のシグナルです。
  • ネットワークの検査。この平文プロトコルによって、内容ベースの検知が現実的なものになっています。固定のクライアントhello AD DE 02 00 10 02 00 00、アクティブウィンドウのビーコンAD DE 90 00、そして400秒/5秒のキープアライブパターンは、C2のホスト名やポートが変わっても有効なままです。
  • ブラウザとIDに関する対応。プロファイルの複製が行われるため、修復作業はマルウェアの除去とパスワードのローテーションにとどめてはなりません。サーバー側でアクティブなアプリケーションセッションを終了し、影響を受けたデバイスとネットワークからのアカウント活動を確認し、必要に応じてMFAを再登録し、ブラウザのCookieとデバイスフィンガープリントのデータが漏洩したものと見なしてください。
  • 封じ込めの順序。運用者はTCP接続を列挙・終了でき、システムプロキシを変更でき、入力をブロックでき、HVNCによるアクセスを維持できます。侵害されたデスクトップ内部で実行される操作だけに頼るのではなく、ネットワークの制御プレーンでエンドポイントを隔離してください。

収集時点での公開検知状況

2つのPE段階について、OTX、MalwareBazaar、ThreatFox、URLhausと照合しました:

サンプル 検出のあった公開ソース
.NETインジェクター(9,728バイト) 4件中0件
x86-64ペイロード(1,972,736バイト) 4件中1件(OTXのみ)

Caronteは、このペイロードを、評価情報に一切頼ることなく振る舞いのみから、infostealer with hVNCとして高評価で分類しました。

収集時点で、181.237.42.61はポート80でドロッパー群を配信し、4577での接続も受け付けていました。いずれの観測も2026年8月13日時点のものであり、ここに記載した内容をこのホストの現在の状態に関する記述として読むべきではありません。同じurlscanの調査期間に見つかった他の2つの姉妹URLは1日以内にオフラインになっており、この1件もダイナミックDNS上にあります。

侵害指標(IOC)

Stage hashes (SHA256):
  Stage 1  VBScript dropper            3,346,929 B
           c8f229c7843f5fec053c04fb78b2ed045c029671412d74a07ce40608eeef1797
           served as sostener2.vbs and envifa.vbs, byte-identical
  Stage 3  PowerShell loader           2,481,247 B
           a49c01a5cc1c06be5f2e6cbd0f63554c7755e45f3931c08541da643014c8c9e5
           = AES-256-ECB(base64_decode(343-literal blob)), PKCS7 stripped
  Stage 4  .NET injector                   9,728 B
           5b5f3f49769493f6ed3de978d41bc4bb8dd286467becb4d25036f037462cb013
  Stage 5  x86-64 payload (Evolution RAT) 1,972,736 B
           7a3c619827557de9a3687daa137f772f40e1bba1a4ca32bac1b06557f42ce522
  Nested   HVNCInjection module carved from stage 5 offset 1810200 to EOF
           6f6b10e5cce18005231da32a4344e764d7c1e4f22bcc1d78f264561966a1b46d
           (162,536 B as carved; the module's section table ends at 156,160 B,
            so this hash depends on the carve boundary, not on the module alone)
  Stage 2 is generated at runtime by stage 1 and has no fixed hash.
Dropper:
  Names:    sostener2.vbs, envifa.vbs  (byte-identical)
  SHA256:   c8f229c7843f5fec053c04fb78b2ed045c029671412d74a07ce40608eeef1797
  Size:     3,346,929 bytes (98.9% of it is the base64 payload literal)
  Type:     Obfuscated VBScript. XOR 0x99 strings; payload split across 343
            string literals concatenated in non-declaration order; 34 decoy
            functions; 170 constant-false padding lines (~4 KB total)
Dropped / staged files:
  %TEMP%\<hex>.ps1                       stage 2, generated AES decryptor
                                         (UTF-8 no BOM, deleted by stage 1)
  %TEMP%\<hex><hex>\ClJdZcnG.ps1         stage 3, self-deleted after 3 s
                                         (fixed basename "ClJdZcnG")
  Launch: powershell.exe -NoProfile -ExecutionPolicy Bypass -File "<stage 2>"
          stage 3 is then invoked in-process with the call operator, so it
          never appears as a separate command line
Distribution:
  Host:     asegurar2026.duckdns.org
  IP:       181.237.42.61  (Colombia)
  URLs:     http://asegurar2026.duckdns.org/sostener2.vbs
            http://asegurar2026.duckdns.org/envifa.vbs
            http://181.237.42.61/envifa.vbs
  Server:   Apache/2.4.58 (Win64) OpenSSL/3.1.3 PHP/8.0.30
            (Apache on Windows; consistent with a compromised host)
C2:
  Host:     serversniperxx.duckdns.org
  Port:     4577  (0x11E1)
  IP:       181.237.42.61   <-- same machine as distribution
  Protocol: framed binary over raw TCP, cleartext (does not speak HTTP)
  Stored in PE section .cfg, RVA 0x1A0000, file offset 0x193600,
  virtual size 163 bytes, cleartext:
    +0    13  "##CFG_START##"
    +13    3  zero padding
    +16  128  host, null-padded
    +144   2  port, uint16 LE  (E1 11 = 4577)
    +146   1  zero padding
    +147  13  "##CFG_END####"
    +160   3  zero padding
C2 protocol:
  Frame:      [magic uint16 LE 0xDEAD][opcode uint16 LE][length uint32 LE][payload]
              no closing marker, no checksum, no encryption; length cap 0x1000000
  Functions:  FUN_140039620 (TX framing) -> FUN_140039380 (send_all)
              FUN_140017050 (RX validation) -> FUN_140038590 (recv_all)
              magic at 0x140039634: B8 AD DE 00 00  (mov eax, 0xDEAD)
  Hello:      AD DE 02 00 10 02 00 00  + 528-byte fixed-field struct (FUN_14000e760)
                0x000  0x40  HWID (ComputerName + volume serial)
                0x040  0x40  ComputerName
                0x080  0x40  UserName
                0x0C0  0x40  OS version (RtlGetVersion)
                0x100  0x10  architecture
                0x110  0x40  unidentified
                0x150  0x80  foreground window title
                0x1D0  0x40  reserved
  Server ACK: opcode 0x03 (client waits for it before proceeding)
  Beacon:     opcode 0x90, active window title, immediately then on change (250 ms)
  Socket:     TCP_NODELAY, SO_SNDBUF 0x80000, SO_RCVBUF 0x40000, timeout 30000 ms
              SIO_KEEPALIVE_VALS idle 400000 ms, interval 5000 ms
  Dispatcher: FUN_140019c20, guard opcode > 0xF9, jump table 0x14001B694, 59 opcodes
              response opcode = command opcode + 1 (dominant convention)
  Notable opcodes:
    0x00 ping (reply 0x01)        0x20 screen stream (frames 0x21)
    0x23 remote mouse             0x24 remote keyboard
    0x33 file staging/self-update 0x35 full collection (JSON via 0x64)
    0x38 process list             0x3A kill process by PID
    0x42 shell stdin write        0x44 file manager / drive listing
    0x4A LockWorkStation, resolution change, ExitProcess
    0x4C delete file/dir          0x4D ShellExecute
    0x80 microphone (44.1 kHz)    0xA8 stream quality/FPS (clamp 10..100)
    0xA9 clipboard write          0xB0 reverse proxy to arbitrary host
    0xC7 / 0xC8 BlockInput on/off 0xCB HKCU Run persistence (set/delete)
    0xD0 PE injection/hollowing   0xE0 TCP connection table
    0xE2 kill process by name     0xE3 SetTcpEntry kill (12-byte payload)
    0xF7 keylog file retrieval
  Handler identified, capability unresolved:
    0x30 0x31 0x48 0x49 0xA0 0xA1 0xC0 0xC4 0xF5
Malware identity:
  Family:   Evolution RAT
  PDB:      C:\Users\USER\Desktop\Evolution RAT\Overlord-Server\dist-clients\HVNCInjection.pdb
  PDB GUID: 59965EFB-F602-42DC-94D0-FCF15841EFC1  age=1
  Note:     PDB is in a PE nested at offset 1810200 of the payload (inside the
            .rsrc range, raw 0x193800..0x1E0200). RSDS record at offset 1944736.
            The outer payload's debug directory has a single POGO record.
  Second confirmation: "Overlord HVNC DLL Loaded" written to
            %TEMP%\crashlogovd.log by the nested module
  Client ID format: SniperX_%d
Crypto:
  Stage 2:  AES-256-ECB / PKCS7  (decrypts stage 3)
  Key:      Be1OluQXuNlV3c/vviqtY1xEW70mAk13+uxHyLqeTZ8=   (base64)
            = 05ed4e96e417b8d955ddcfefbe2aad635c445bbd26024d77faec47c8ba9e4d9f
  Stage 3:  Base85 with non-standard alphabet (carries stage 5);
            stage 4 travels in the same script as plain base64
            0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ
            abcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~
  Stage 4:  API name strings XOR 0x2F, resolved from ntdll.dll at runtime
  Payload:  ChaCha20-Poly1305 (RFC 8439) at FUN_140010710 / FUN_140019040,
            reached only from FUN_1400197a0 (Chrome App-Bound Encryption key
            decryption). Not used for C2 traffic.
  Decoy:    "expand 32-byte k" at 0x14014E248 in .rdata has ZERO xrefs.
            Dead data. Do not infer an encrypted C2 channel from it.
  Hardcoded keys in .data:
            B31C6E241AC846728DA9C1FAC4936651
            E98F37D7F4E1FA433D19304DC2258042
            CCF8A1CEC56605B8517552BA1A2D061C
Persistence:
  %APPDATA%\Microsoft\manifest_ckCGj9.xhtml
  C:\Users\Public\manifest_ckCGj9.xhtml
    (fake XHTML: <title>Windows Security</title>,
     container <div id="ms-2EDe7CMv96" data-key="8MxUjqEtBpLuwtsg">,
     markers <!--2EDe7CMv96--> ... <!--/2EDe7CMv96-->,
     body = StrReverse of the original VBScript)
  Startup\CfgShieldckCGj9.vbs
    (header: "Windows SmartScreen Filter Config" /
             "Version 4.8.494 - Microsoft Corporation" /
             "(c) 2026 Microsoft. All rights reserved."
     XOR 0xCD strings, ExecuteGlobal;
     unused decoy strings: Microsoft.Update.Session,
     Win32_OperatingSystem, root\SecurityCenter2)
  Installed only when the running script is not already in Startup
  Start Menu\Programs\Startup\WindowsDefender.exe   (payload self-copy)
  Payload also copies itself as svchost.exe
Injection:
  .NET injector type/method:  yMOxml.nRCN06Y :: C3FrcKQp
  Loaded via:                 Reflection.Assembly::Load
  Entry args:                 (payload bytes, target process path)
  Hollowing target:           C:\Windows\Microsoft.NET\Framework64\v4.0.30319\AppLaunch.exe
                              (Framework\ instead of Framework64\ on 32-bit)
  Declared P/Invokes (kernel32, all the metadata reveals):
        GetModuleHandleA, GetProcAddress, VirtualAlloc,
        CreateProcessA, CloseHandle
  Real API set, strings XOR 0x2F, resolved from ntdll.dll at runtime:
        NtUnmapViewOfSection, NtAllocateVirtualMemory, NtWriteVirtualMemory,
        NtGetContextThread, NtSetContextThread, NtResumeThread
        (bound via Marshal.GetDelegateForFunctionPointer)
  NOTE: the Win32 names VirtualAllocEx / WriteProcessMemory /
        SetThreadContext / ResumeThread do NOT appear in this injector.
Filesystem (payload):
  C:\ProgramData\SniperLogs\
  C:\ProgramData\SniperLogs\keylog_%04d%02d%02d_%02d%02d%02d.txt
  modules\hvnc_injection.dll   (reflectively loaded, ReflectiveLoader export)
Mutexes / synchronization:
  hvnc:ready          hvnc:launch_ok      hvnc:launch_fail
  hvnc_watchdog:stuck Local\hvnc_rdi_     SingletonSocket
Registry:
  Software\Microsoft\Windows\CurrentVersion\Internet Settings
    ProxyServer set to 127.0.0.1:1  (dead proxy; severs the host's internet access)
Command execution:
  powershell.exe -NoProfile -ExecutionPolicy Bypass -File "<%TEMP%\<hex>.ps1>"
  powershell -WindowStyle Hidden -Command "Add-MpPreference -ExclusionPath '
  powershell -WindowStyle Hidden -Command "Add-MpPreference -ExclusionProcess '
  cmd.exe /Q                      (remote shell host)
  taskkill /IM <browser> /F       (before profile clone)
Hooked APIs (MinHook, browser profile redirection):
  NtCreateFile   NtOpenFile   NtDeleteFile   NtSetInformationFile
  NtQueryAttributesFile   NtQueryFullAttributesFile
  NtQueryDirectoryFile    NtQueryDirectoryFileEx
  CreateProcessW
Browser targets (~20):
  Chrome, Edge, Brave, Opera, OperaGX, Vivaldi, Chromium, Yandex,
  CentBrowser, Iridium, CocCoc, Torch, Comodo Dragon, Epic Privacy,
  Slimjet, URBrowser, 7Star, Amigo, Sputnik, Firefox
Profile-clone artifacts:
  Clone directory name:  hvnc_%08X
  Deleted from the copy: SingletonLock  SingletonCookie
                         parent.lock    .parentlock
  Locked files are read by duplicating handles out of the locking process
  (SeDebugPrivilege + NtQuerySystemInformation handle enumeration).
  Log strings: "Hijack: %d locking PIDs for %s",
               "Hijack: %llu handles, searching...",
               "Hijack OK: %s (%lld bytes)",
               "CloneProfile: TIMEOUT after 60s"
  lite mode skips: cache, code cache, service worker, blob_storage,
                   jumplisterrors, extensions, extension state/rules/scripts,
                   local/sync extension settings, managed extension storage,
                   segmentation_platform, commerce_local_db,
                   optimization_guide_prediction_model_downloads
Chrome App-Bound Encryption:
  app_bound_encrypted_key handled
  NCryptDecrypt with key name "Google Chromekey1"
Fingerprint fields (35), returned with opcode 0x64 on command 0x35:
  os_name os_version os_platform machine_name user_name domain
  processor_count system_dir screen_width screen_height screen_depth
  timezone timezone_offset timezone_display language language_display
  languages cpu_name cpu_id cpu_cores cpu_threads gpu_name gpu_driver
  gpu_ram ram_gb device_memory mac_address disk_serial fonts fonts_count
  browsers chrome_version user_agent webgl_vendor webgl_renderer
Browser-facing values are synthesized into Chrome's own formats:
  user_agent      Mozilla/5.0 (Windows NT %lu.%lu; %s) AppleWebKit/537.36
                  (KHTML, like Gecko) Chrome/%s Safari/537.36
                  platform token "Win64; x64"
  chrome_version  from HKCU Software\Google\Chrome\BLBeacon "version",
                  fallback literal 131.0.6778.86
  webgl_vendor    "Google Inc. (" + <Win32_VideoController vendor> + ")"
  webgl_renderer  "ANGLE (" + <adapter> + ", Direct3D11 vs_5_0 ps_5_0, D3D11)"
  device_memory   %.0f   from Win32_OperatingSystem TotalVisibleMemorySize
  ram_gb          %.1f   same source, operator-facing precision
  languages       <UI language> + ",en-US"
rule Evolution_RAT_VBS_Dropper_XHTML_Persistence {
    meta:
        description = "VBScript dropper for Evolution RAT: stores its own reversed source inside a fake XHTML file and reloads it from a Startup loader"
        author      = "Beelzebub Research"
        date        = "2026-08-13"
        reference   = "https://beelzebub.ai/blog/evolution-rat-hvnc-browser-session-hijacking"
    strings:
        $marker   = "2EDe7CMv96" ascii
        $manifest = "manifest_ckCGj9" ascii
        $loader   = "CfgShieldckCGj9" ascii
        $datakey  = "8MxUjqEtBpLuwtsg" ascii
        $title    = "<title>Windows Security</title>" ascii
        $hdr      = "Windows SmartScreen Filter Config" ascii
        $f1       = "StrReverse" ascii nocase
        $f2       = "ExecuteGlobal" ascii nocase
        $pad      = "If 0 Then Randomize Timer" ascii nocase
    condition:
        (any of ($marker, $manifest, $loader, $datakey))
        and 2 of ($title, $hdr, $f1, $f2, $pad)
}
rule Evolution_RAT_PowerShell_Stage {
    meta:
        description = "Evolution RAT stage 3/4: AES-256-ECB decrypted PowerShell loader carrying a Base85 blob, reflective .NET injector load and AppLaunch.exe hollowing"
        author      = "Beelzebub Research"
        date        = "2026-08-13"
        reference   = "https://beelzebub.ai/blog/evolution-rat-hvnc-browser-session-hijacking"
    strings:
        $key    = "Be1OluQXuNlV3c/vviqtY1xEW70mAk13+uxHyLqeTZ8=" ascii wide
        $b85    = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~" ascii wide
        $dec    = "Decode-Base85" ascii wide
        $tgt    = "AppLaunch.exe" ascii wide nocase
        $refl   = "Reflection.Assembly" ascii wide
        $type   = "yMOxml.nRCN06Y" ascii wide
        $meth   = "C3FrcKQp" ascii wide
        $mode   = "ECB" ascii wide
        $pad    = "PKCS7" ascii wide
        $ps     = "-ExecutionPolicy Bypass" ascii wide nocase
    condition:
        // $b85 is the strongest anchor: the alphabet is a toolkit constant and
        // survives rebuilds, while $key and the type/method names may not.
        $b85
        or $key
        or (3 of ($dec, $tgt, $refl, $type, $meth, $mode, $pad, $ps)
            and any of ($type, $meth))
}
rule Evolution_RAT_DotNet_Injector_XOR_NT_API {
    meta:
        description = "Evolution RAT stage 4: 9,728-byte .NET injector that resolves native NT hollowing APIs from XOR 0x2F encoded strings, so the Win32 names never appear"
        author      = "Beelzebub Research"
        date        = "2026-08-13"
        reference   = "https://beelzebub.ai/blog/evolution-rat-hvnc-browser-session-hijacking"
    strings:
        // API names as stored: plaintext XOR 0x2F
        $x_unmap  = "a[zABN_yFJX`I|JL[F@A" ascii              // NtUnmapViewOfSection
        $x_alloc  = "a[nCC@LN[JyF][ZNCbJB@]V" ascii           // NtAllocateVirtualMemory
        $x_write  = "a[x]F[JyF][ZNCbJB@]V" ascii              // NtWriteVirtualMemory
        $x_getctx = "a[hJ[l@A[JW[{G]JNK" ascii                // NtGetContextThread
        $x_setctx = "a[|J[l@A[JW[{G]JNK" ascii                // NtSetContextThread
        $x_resume = "a[}J\\ZBJ{G]JNK" ascii                   // NtResumeThread
        $x_ntdll  = { 41 5B 4B 43 43 01 4B 43 43 }            // ntdll.dll
        $type     = "yMOxml" ascii
        $meth     = "C3FrcKQp" ascii
    condition:
        uint16(0) == 0x5A4D
        and (
            3 of ($x_*)
            or ($x_ntdll and any of ($type, $meth))
        )
}
rule Evolution_RAT_Payload_Config_Section {
    meta:
        description = "Evolution RAT x86-64 payload: builder config markers in the .cfg PE section plus HVNC, GhostStandby and SniperLogs artifacts. The ##CFG_START## markers are build-invariant and match any operator's host/port."
        author      = "Beelzebub Research"
        date        = "2026-08-13"
        reference   = "https://beelzebub.ai/blog/evolution-rat-hvnc-browser-session-hijacking"
    strings:
        $cfg_start = "##CFG_START##" ascii
        $cfg_end   = "##CFG_END####" ascii
        $h1        = "hvnc:ready" ascii
        $h2        = "hvnc:launch_ok" ascii
        $h3        = "hvnc:launch_fail" ascii
        $h4        = "hvnc_watchdog:stuck" ascii
        $h5        = "hvnc_injection.dll" ascii
        $g1        = "GhostStandby" wide
        $s1        = "SniperLogs" ascii
        $s2        = "SniperX_" ascii
        $s3        = "CloneProfile" ascii
        $s4        = "Google Chromekey1" wide
        $ov        = "Overlord HVNC DLL Loaded" ascii
        $wgl       = "ANGLE (" ascii
        $ua        = "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36" ascii
    condition:
        uint16(0) == 0x5A4D
        and (
            ($cfg_start and $cfg_end)
            or $ov
            or 3 of ($h1, $h2, $h3, $h4, $h5, $g1, $s1, $s2, $s3, $s4, $wgl, $ua)
        )
}

結論

この5段階のチェーンは擬装であって、この物語の到達点ではありません。逆順ソースによる永続化、XOR文字列、AES、独自のBase85アルファベット、リフレクションによるロード、そしてプロセスホローイング。それぞれの段階が、ペイロードがブラウザに到達できるようになるまで、異なる検知面を1つずつ取り除いていきます。そこで何が起きるかこそが、Evolution RATをコモディティな情報窃取ツール群から区別する点です。

この作戦は4つの要素から構成されています。Cookieが認証済みの状態を提供します。CloneProfileは、被害者のブラウザを止めることなくその状態を起動可能にします。35フィールドの収集は、不正検知システムが期待するデバイスIDを、ChromeのANGLE (...)レンダラー構文に至るまで再現します。オペコード0xB0は、被害者のネットワークを通じてトラフィックを送出できるようにします。これらが揃うことで、盗まれたセッションは、同じデバイスと同じ接続から戻ってきた正規のユーザーのように見えてしまいます。Chrome向けにGPU名を整形すること自体が、単なるホストのインベントリのために行われることはありません。

もっとも有効な検知手法もまた、1つのキャンペーンのインフラを超えたところに存在します。serversniperxx.duckdns.orgとポート4577はある1つのビルドを特定するにすぎませんが、ビルダーの##CFG_START##/##CFG_END####マーカーはこのツールキット自体を特定します。固定されたAD DE 02 00 10 02 00 00のhelloも、ネットワーク上で同じ役割を果たします。ホストは変わっても、製品としての制約は変わりません。

Caronteがこれをどう可能にしたか

この調査は、1本の不審なVBScriptから始まりました。Caronteは5段階のチェーン全体を自律的に処理し、逆順ソースによる永続化を再構成し、343個のリテラルの連結を解決し、AES鍵を復元し、PowerShellローダーを復号し、独自のBase85のBlobをデコードし、.NETインジェクターとx86-64ペイロードを抽出しました。続けて入れ子になったPEを追跡し、埋め込まれたHVNCモジュールを独立した解析対象としてキューに入れ、すべての段階を元のサンプルにひも付けました。

本セクション以降の内容はすべて、Caronteの自律的な処理結果の上に構築された検証と探索です。アナリストの役割は、抽出されたアーティファクトを検証し、その振る舞いを解釈し、技術的な証拠をセッション乗っ取りという目的に結びつけることでした。

Caronteによるペイロード解析では、GhostStandbyとSetWindowDisplayAffinity(0x11)による画面キャプチャの除外、winlogon.exeのトークンなりすまし経路、キー名Google Chromekey1を用いたNCryptDecrypt呼び出し、Defenderの除外コマンド、SQLiteの読み取りに先立つブラウザスレッドの一時停止、hvnc:*ミューテックスの全セット、SniperLogs、そしてプロセスホローイングのシーケンスとそれを実装する具体的な関数アドレスが特定されました。infostealer with hVNCという「高」評価の分類は、評価情報の照会からではなく、こうした振る舞いに基づく証拠から導き出されたものです。

また、このファミリーを特定する2つの識別子も、コードについて推論するのではなく、決定的な抽出処理によって導き出されました。

C2アドレスは、逆コンパイルで辿れるどの命令からも参照されていません。それはコンパイル後にビルダーによって書き込まれる、.cfgという非標準のPEセクションの中に存在します。すべてのセクションを列挙し、サイズが異常なものをダンプすれば、一手でこれを見つけられます。前述の要約はserversniperxx.duckdns.orgを特定し、末尾の0x11e1をポート4577として読み取っており、これは##CFG_START##と##CFG_END####マーカーの間の設定Blobからそのまま得られたものです。

PDBパスも、ペイロードのデバッグディレクトリにはありません。そこにはPOGOレコードが1件あるだけです。これは、リソースセクションの生の範囲内、オフセット1810200に埋め込まれた別のPEの中に存在します。逆コンパイルされたコードについて推論するだけではここには辿り着けません。ファイルを走査し、MZヘッダーの背後に有効なPEシグネチャを持つものをすべて切り出し、それぞれのデバッグディレクトリを解析することで、初めて辿り着けます。この切り出し処理によって、pdb_pathと、ビルドGUIDである59965EFB-F602-42DC-94D0-FCF15841EFC1が、構造化された指標として得られました。

切り出されたモジュールを構造的な文脈(オフセット、サイズ、セクションごとのエントロピー、3つのDLLからなるインポート一覧)として与えたうえで、この処理はさらにそのモジュールを抽出し、独立した解析対象としてキューに入れました:

このモジュール自体のスコアは0であり、ネットワークや永続化のロジックは一切持っていません。これは純粋にローダーとファイルAPIのフック集合であり、実際の機能はこれが注入する第2段階の側に存在します。このモジュール自体の要約は、%TEMP%\crashlogovd.logに書き込まれるOverlord HVNC DLL Loadedというバナーを名指ししており、これがPDBパスに続く、このファミリー名の2つ目の独立した裏付けとなっています。

その結果得られたのは、エンドツーエンドの解析パイプラインでした。Caronteはスクリプトチェーンを展開し、抽出された各段階を紐付け、最終ペイロードをマッピングし、ファミリーの識別子を自動的に浮かび上がらせました。人間のアナリストが担ったのは、手作業での抽出ではなく、検証と解釈でした。この圧縮こそが、すなわち重度に難読化されたVBScriptから、完全な脅威グラフと、裏付けの取れたセッション乗っ取りの評価に至るまでの圧縮こそが、このプラットフォームをこの種のキャンペーンに対して有用なものにしているのです。

Caronteの仕組みを見る

翻訳元: https://beelzebub.ai/blog/evolution-rat-hvnc-browser-session-hijacking/

本記事は beelzebub.ai の記事を翻訳・要約したものです。