ランサムウェア暗号化メカニズムの分析 は何をしますか?
使用するタイミング
- ランサムウェア感染が発生し、復旧のために使用された暗号化方式の理解が必要な場合
- 身代金を支払わずに復号が可能かどうかを評価する場合(実装上の欠陥、既知の復号ツールの有無)
- ランサムウェアをリバースエンジニアリングし、暗号化アルゴリズム、鍵導出、鍵保存メカニズムを特定する場合
- ランサムウェアの暗号実装に脆弱性が見つかり、復号ツールを開発する場合
- ランサムウェアサンプルをその暗号化アプローチに基づいて分類し、既知のファミリーに帰属させる場合
注意: 実際のデータ復旧作業では、暗号化されたファイルのテストコピーで復号方法を確認するまでは使用しないでください。
前提条件
- GhidraまたはIDA Pro(ランサムウェアバイナリのリバースエンジニアリング用)
- Python 3.8以上と
pycryptodomeライブラリ(暗号化/復号ルーチンのテスト用) - 暗号化されたサンプルファイルと対応する平文のオリジナルファイル(既知平文ペア)
- ランサムウェアバイナリへのアクセス(可能であればアンパック済み)
- 対称暗号(AES、ChaCha20)および非対称暗号(RSA)に関する知識
- NoMoreRansom.orgデータベース(既存の無料復号ツールの確認用)
ワークフロー
ステップ 1: 暗号化アルゴリズムの特定
ランサムウェアが使用する暗号アルゴリズムを特定します。
# インポートからWindows Crypto APIの使用を確認
import pefile
pe = pefile.PE("ransomware.exe")
crypto_apis = {
"CryptAcquireContextA": "Windows CryptoAPI",
"CryptAcquireContextW": "Windows CryptoAPI",
"CryptGenKey": "Windows CryptoAPI 鍵生成",
"CryptEncrypt": "Windows CryptoAPI 暗号化",
"CryptImportKey": "Windows CryptoAPI 鍵インポート",
"BCryptOpenAlgorithmProvider": "Windows CNG (最新暗号化)",
"BCryptEncrypt": "Windows CNG 暗号化",
"BCryptGenerateKeyPair": "Windows CNG 非対称鍵生成",
}
print("Crypto API インポート:")
for entry in pe.DIRECTORY_ENTRY_IMPORT:
for imp in entry.imports:
if imp.name and imp.name.decode() in crypto_apis:
print(f" {entry.dll.decode()} -> {imp.name.decode()}: {crypto_apis[imp.name.decode()]}")
一般的なランサムウェアの暗号化方式:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
AES-256-CBC + RSA-2048: 最も一般的なハイブリッド方式 (LockBit, REvil, Conti)
AES-256-CTR + RSA-4096: ストリーム暗号モードの亜種 (BlackCat/ALPHV)
ChaCha20 + RSA-4096: 最新のストリーム暗号 (Hive, Royal)
Salsa20 + ECDH: Curve25519 鍵交換 (Babuk)
AES-128-ECB: 脆弱なモード - 既知平文攻撃による復号の可能性
XORのみ: 単純な暗号化 - 常に復元可能
カスタムアルゴリズム: 実装上の欠陥が存在することが多い
ステップ 2: 鍵生成と管理の分析
暗号化鍵の生成方法と保存方法をリバースエンジニアリングします。
ランサムウェアにおける鍵管理パターン:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 強固 (鍵なしでは復元不可能):
- CryptGenRandomでファイルごとにAES鍵を生成
- 埋め込まれたRSA公開鍵でAES鍵を暗号化
- 暗号化された鍵を各ファイルに追加、または別途保存
- RSA秘密鍵は攻撃者のC2サーバのみが保持
2. 脆弱 (復元の可能性あり):
- 予測可能なシード(タイムスタンプ、PID)からAES鍵を導出
- すべてのファイルに同一のAES鍵を使用(単一鍵の漏洩で全復号可能)
- 暗号化開始前に鍵をC2に送信(PCAPに鍵が含まれている可能性)
- 短い繰り返し鍵によるXOR(総当たり攻撃が可能)
- GetTickCountやtime()をシードとするPRNG(鍵空間が限定的)
3. 実装の欠陥:
- ECBモード(平文のパターンが保存される)
- 初期化ベクター(IV)のファイル間での再利用
- 鍵がメモリ内に平文で保存される(メモリダンプから復元可能)
- 部分的な暗号化(ファイルの最初のNバイトのみ)
ステップ 3: ファイル暗号化ルーチンの調査
ファイル処理ロジックをリバースエンジニアリングします。
// 典型的なランサムウェアのファイル暗号化フロー (Ghidraによる逆コンパイル疑似コード):
void encrypt_file(char *filepath) {
// 1. ファイル拡張子が対象リストに含まれるか確認
if (!is_target_extension(filepath)) return;
// 2. ファイルごとのAES鍵を生成 (AES-256の場合は32バイト)
BYTE aes_key[32];
CryptGenRandom(hProv, 32, aes_key);
// 3. ランダムなIVを生成 (16バイト)
BYTE iv[16];
CryptGenRandom(hProv, 16, iv);
// 4. ファイル内容を読み込み
HANDLE hFile = CreateFile(filepath, GENERIC_READ, ...);
BYTE *plaintext = read_entire_file(hFile);
// 5. AES-256-CBCで暗号化
aes_cbc_encrypt(plaintext, file_size, aes_key, iv);
// 6. AES鍵をRSA公開鍵で暗号化
BYTE encrypted_key[256]; // RSA-2048 出力
rsa_encrypt(aes_key, 32, rsa_pubkey, encrypted_key);
// 7. ファイルに書き込み: 暗号化データ + 暗号化鍵 + IV
write_file(filepath, encrypted_data, encrypted_key, iv);
// 8. ファイル名をランサムウェアの拡張子に変更
rename_file(filepath, strcat(filepath, ".locked"));
}
ステップ 4: 暗号の脆弱性の確認
悪用可能な実装上の欠陥をテストします。
from Crypto.Cipher import AES
import os
import struct
# テスト1: 複数のファイルで同じ鍵が使用されているか確認
# 既知のファイルの暗号化バージョンを比較
def check_key_reuse(file1_enc, file2_enc):
with open(file1_enc, "rb") as f:
data1 = f.read()
with open(file2_enc, "rb") as f:
data2 = f.read()
# IVを抽出 (場所はランサムウェアファミリーに依存)
# もしIVが同じでファイルが暗号化ブロックを共有している場合 -> 同じ鍵
iv1 = data1[-16:] # 例: IVが末尾にある場合
iv2 = data2[-16:]
if iv1 == iv2:
print("[!] 同じIVを検出 - 鍵の再利用の可能性")
# テスト2: 予測可能な鍵導出の確認
# 鍵がタイムスタンプから導出される場合、可能な値を総当たり
def brute_force_timestamp_key(encrypted_file, known_header, timestamp_range):
with open(encrypted_file, "rb") as f:
encrypted_data = f.read()
for ts in timestamp_range:
# ランサムウェアと同じ方法で鍵を導出
import hashlib
key = hashlib.sha256(str(ts).encode()).digest()
iv = encrypted_data[-16:]
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(encrypted_data[:16])
if decrypted[:len(known_header)] == known_header:
print(f"[!] 鍵を発見! タイムスタンプ: {ts}")
return key
return None
# テスト3: ECBモードの確認 (パターン保存)
def check_ecb_mode(encrypted_file):
with open(encrypted_file, "rb") as f:
data = f.read()
# ECBモードでは、同一の平文ブロックから同一の暗号文ブロックが生成される
blocks = [data[i:i+16] for i in range(0, len(data), 16)]
unique = len(set(blocks))
total = len(blocks)
if unique < total * 0.95:
print(f"[!] ECBモードの可能性: {total-unique}個の重複ブロック (全{total}個中)")
ステップ 5: 鍵の復元を試行
特定された脆弱性を利用して鍵を復元します。
# 復元方法1: メモリダンプから鍵を抽出
# Volatilityプラグインを使用してAES鍵スケジュールをスキャン
# vol3 -f memory.dmp windows.yarascan --yara-rule "aes_key_schedule"
# 復元方法2: 既知平文攻撃 (脆弱なアルゴリズム)
def xor_key_recovery(encrypted_file, known_plaintext):
"""既知平文-暗号文ペアからXOR鍵を復元"""
with open(encrypted_file, "rb") as f:
ciphertext = f.read()
key = bytes(c ^ p for c, p in zip(ciphertext, known_plaintext))
# 繰り返し鍵の長さを特定
for key_len in range(1, 256):
candidate = key[:key_len]
if all(key[i] == candidate[i % key_len] for i in range(min(len(key), key_len * 4))):
print(f"XOR鍵 (長さ {key_len}): {candidate.hex()}")
return candidate
return None
# 復元方法3: NoMoreRansomで既存の復号ツールを確認
# https://www.nomoreransom.org/en/decryption-tools.html
ステップ 6: 暗号分析の文書化
調査結果を構造化されたレポートにまとめます。
分析では以下を文書化する必要があります:
- 特定されたアルゴリズム (AES, RSA, ChaCha20, カスタム)
- 鍵サイズと動作モード (CBC, CTR, ECB, GCM)
- 鍵生成方法 (CSPRNG, 予測可能なシード, 静的鍵)
- 鍵の保存場所 (ファイルに追加, レジストリ, C2への送信)
- ファイル変更パターン (完全暗号化, 部分暗号化, ヘッダーのみ)
- 対象となるファイル拡張子
- ランサムノートの形式と支払いインフラ
- 復号の実現可能性評価 (可能/不可能/部分的)
- 推奨される復旧方法
主要な概念
| 用語 | 定義 |
|---|---|
| ハイブリッド暗号化 | 高速なファイル暗号化に対称暗号(AES)を、安全な鍵ラッピングに非対称暗号(RSA)を組み合わせる手法。ランサムウェアの標準的なアプローチ。 |
| 鍵ラッピング | ファイルごとの対称鍵を攻撃者のRSA公開鍵で暗号化し、攻撃者の秘密鍵のみが復号できるようにする手法。 |
| ECBモード | 電子コードブックモード。各ブロックを独立して暗号化するため、平文のパターンが保存される。部分的な復元を可能にする重大な脆弱性。 |
| 既知平文攻撃 | 既知のオリジナルファイルとその暗号化バージョンを使用して暗号鍵を導出する攻撃。XORや脆弱なストリーム暗号に対して有効。 |
| 鍵スケジュール | AES鍵がメモリ上で展開された形式。メモリダンプ内をスキャンすることで、鍵が消去される前に復元できる可能性がある。 |
| CSPRNG | 暗号論的に安全な疑似乱数生成器。CryptGenRandomを使用するランサムウェアは予測不可能な鍵を生成する。 |
| 部分暗号化 | 一部のランサムウェアは速度向上のため、ファイルの最初のNバイトまたはNブロックごとにのみ暗号化する。暗号化されていない部分は復元に役立つ可能性がある。 |
ツールとシステム
- Ghidra: アセンブリレベルでランサムウェアの暗号化ルーチンを解析するためのリバースエンジニアリングスイート
- PyCryptodome: 復号ルーチンの実装とテストのためのPython暗号ライブラリ
- NoMoreRansom.org: 既知のランサムウェアファミリー向けに、Europolとセキュリティベンダーが提供する無料復号ツールのリポジトリ
- Volatility: 感染システムのRAMダンプから暗号鍵を抽出するためのメモリフォレンジックフレームワーク
- CryptoTester: 定数やコードパターンに基づいて暗号アルゴリズムを特定するツール
一般的なシナリオ
シナリオ: ランサムウェアインシデントにおける復号可能性の評価
コンテキスト: ある組織がランサムウェアに感染し、ファイルサーバーが暗号化された。管理者は、身代金を支払わずに復号が可能かどうかを、復旧判断の前に知る必要がある。
アプローチ:
- ランサムノート、ファイル拡張子、サンプルハッシュからランサムウェアファミリーを特定(ID Ransomwareで確認)
- NoMoreRansom.orgで、このファミリー向けの既存の無料復号ツールを確認
- Ghidraで暗号化ルーチンをリバースエンジニアリングし、アルゴリズムと鍵管理を特定
- 実装上の弱点(鍵の再利用、予測可能なシード、ECBモード)をテスト
- インシデントのPCAPがC2への鍵送信をキャプチャしているか確認(暗号化前に鍵が送信された場合)
- 影響を受けたマシンのメモリダンプをスキャンし、RAMに残っているAES鍵スケジュールを確認
- 結果を報告: 復号可能/不可能とその技術的根拠
落とし穴:
- 暗号化されたファイルの唯一のコピーで復号方法をテストする(常にコピーで作業すること)
- すべてのファイルが同じ鍵を使用していると確認せずに前提とする(ファイルごとの鍵を使用するランサムウェアもある)
- ボリュームシャドウコピー(vssadmin)を確認しない(ランサムウェアが削除に失敗している可能性がある)
- レポートでファイル暗号化アルゴリズムと鍵ラッピングアルゴリズムを混同する
出力形式
ランサムウェア暗号化分析
================================
サンプル: lockbit3.exe
ファミリー: LockBit 3.0 / LockBit Black
SHA-256: abc123def456...
暗号化方式
ファイル暗号: AES-256-CTR (ファイルごとのユニークな鍵)
鍵ラッピング: RSA-2048 (バイナリに埋め込まれた公開鍵)
鍵生成: CryptGenRandom (CSPRNG - 予測不可能)
IV生成: ファイルごとにランダム16バイト
ファイル構造: [暗号化データ][RSA暗号化鍵(256B)][IV(16B)][マジック(8B)]
対象拡張子
合計: 412の拡張子が対象
カテゴリ: ドキュメント(.doc, .xls, .pdf), データベース(.sql, .mdb),
アーカイブ(.zip, .7z), ソースコード(.py, .java, .cs)
除外: .exe, .dll, .sys, .lnk (システムファイルは保持)
実装分析
鍵の強度: 強固 - ファイルごとのランダム鍵、再利用なし
モードの安全性: 強固 - ユニークなナンスを使用したCTRモード
鍵の保存: RSA暗号化された鍵を各ファイルに追加
シャドウコピー: vssadminとWMIを使用して削除
復号の実現可能性
鍵なしの場合: 不可能
- 特定された実装上の欠陥なし
- RSA-2048鍵ラッピングがブルートフォースを防止
- CSPRNGによる鍵予測を防止
- 既存の無料復号ツールなし
復旧オプション
1. オフラインバックアップから復元(推奨)
2. ボリュームシャドウコピーの確認(可能性は低い - ランサムウェアが削除する)
3. マシンが再起動されていない場合のメモリフォレンジック(鍵がRAMに残存している可能性)
4. 攻撃者との交渉(最後の手段 - 復号の保証なし)