0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

セキュリティ・キャンプ 2025 応募課題晒し(C脅威解析クラス)

Last updated at Posted at 2025-08-17

はじめに

セキュリティ・キャンプ全国大会のCクラス(脅威解析クラス)に参加したので応募課題晒しです。自分自身CTFの経験すらなく、ついていけるかかなり心配でしたが、講義はめちゃくちゃ楽しかったです。非常にいいイベントだと思うので、対象の方はぜひ参加してみてください。。。雰囲気もピリピリしてるということはなく、和やかで楽しい雰囲気でした。

私の応募課題の回答については、誤りが含まれているかもしれないので、あまり信用しないようにしてください。例えば、Q6-3は動的解析をして、メモリからIVをひっこぬくのが想定解だったみたいです。問1~3は個人的な内容ですので、飛ばしています。あと個人的には問4がかなり怪しい…

問4

最近多くの組織がセキュリティ対策のためにEDR(Endpoint Detection and Response)を導入していますが、それを迂回する手法も研究されています。組織のセキュリティ担当として、その手法を分析した上で、どのような追加の対策を経営層に提案するべきか考えてください。

EDRは,エンドポイントの通信ログやスクリプトの実行などのログを収集して,調査や封じ込めを行うことができるセキュリティ技術です.EDRを迂回するツールとして,「EDRKillShifter」が挙げられます.例として,ランサムウェアである,「RamsomHub」(参考:https://news.sophos.com/ja-jp/2024/08/14/edr-kill-shifter-jp/ )は,暗号化されたプログラムをメモリ上にロードして,そのプログラムは難読化されている上,EDRキラーとよばれるプログラムを起動します.最終的にメモリ上に動的に展開されたEDRキラーは,脆弱性のあるドライバをインストールして,踏み台にし,カーネル空間でプログラムを実行します(BYOVD攻撃と呼ばれています).インストールされるドライバはあくまで脆弱性はありますが,正規なドライバなので,EDRからみると安全な操作として判断されることも少なくはありません.さらに,ドライバはカーネル空間で動作するので,EDR自体を無効化したりすることが可能となってしまいます.

このような攻撃に対する対策としては,まず脆弱性のあるドライバのインストールができないように,定義ファイルを最新の状態にアップデートすることが挙げられます.Windowsでは,「Vulnerable Driver Blocklist」があり,脆弱性のあるドライバは実行できないようにするしくみがあります.その他には,そもそも許可されたドライバのみをインストールするようにしたり,古い署名付きのドライバをインストールできないようにしたりする方法が考えられます.また,Windowsでは,Hypervisor-protected Code Integrity (HVCI)を利用すると,Hyper-Vの仮想マシンとしてカーネルが動作するようになり,カーネルでロードされるコードが署名付きかどうかを,仮想マシン外のレベルで,チェックするロジックが動作するようになります.これにより,ドライバと同じレベルで動くプログラムが不正なコードを実行しようとしても,それを署名ありか,改変されていないかを確認することが可能となります.さらに,確保されたメモリ領域は書き換え不可能となっていて,カーネル空間のプログラムからは基本的に改変することができないようになります.このように,ユーザー空間でのセキュリティだけでなく,カーネル空間でも不正なコードが実行されないように,セキュリティ定義ファイルを最新にアップデートすることや,HVCIの導入,さらにカーネル空間での動作を記録できるようなEDRがあれば導入することが必要です.

問5

以下の質問にそれぞれ500文字以内で答えてください

  1. 次のGitHubアカウント (https://github.com/clwomkv) から分かることを全て調べて簡潔にまとめてください (例: 他のアカウント, domain, ...)。
  2. それぞれの情報をどのように調べたのかを簡潔に説明してください。
  3. 調査したときに、気をつけたこととその理由を述べてください。

問5-1

「pikax」リポジトリのコミットログから,clwomkvによるコミットだけでなく,「Takeru Ito」なる人物によるコミットもあった.さらに,gitのコミットログのメールアドレスを参照すると,「clwomkv」のメールアドレスはc.seccamp@proton.meで,「Takeru Ito」のメールアドレスは,itotx1993+work@gmail.comであることがわかりました.さらに,pandaとkiteshieldのフォークに関しては,pikaxによるコミットが何も行われていません.しかし,これらの活動がいつ行われているのかを知ることはでき,pandaのフォークは,3月17日の13:27:30,kiteshieldのフォークは,3月17日の13:34:13に行われており,pikaxは3月17日の13:55:33に作成されている.さらに,pikaxのコミットは3月17日の14:22:04, 14:02:49, 13:55:34(すべて日本時間)に行われている.アカウント自体は3月17日の13:15:31に行われている.

問5-2

メールアドレスとコミットした時刻については,リポジトリをcloneして,git log --pretty=fullerコマンドを実行することにより取得しました.さらに,GitHubのアカウントの作成時刻やフォークした時刻については,GithubのAPIを用いて取得しました.pandaとkiteshieldのフォークは,GitHubの画面上でも,cloneしてlogを確かめても,特に独自な変更がコミットされている様子は無いことがわかり,さらにitotx1993+work@gmail.comというアドレスに関しても,「itotx1993」「clwomkv」というIDや,「Takeru Ito」でGoogle,Twitter,Instagram,Facebookで検索しましたが,見つけられませんでした.唯一得られている情報であるGmailから,GHuntというツールを使って検索しましたが,有用な情報を得ることはできませんでした.

問5-3

まず,GitHubのアカウントが与えられているので,Gitのメールアドレスを参照することが思いつき,無事メールアドレスを入手することはできました.そのユーザ名から,「Takeru Ito」が本名で,1993年が生年月日だと思われ,その情報をもとに検索しました.このとき,同姓同名の人物はヒットしましたが,安易に結びつけずに,生年月日が一致しているか,pandaやkiteshieldをフォークしている人物像と一致するかなどを意識しました.また,Googleだけでなく,DuckDuckGoや様々なSNSサービスで検索して,普通に検索しただけでは引っかからないような情報がないか注意深く探索しましたが,GitHubから得られる情報以外に,有用な情報を得ることはできませんでした.

問6

ファイルをダウンロードし、以下のポイントについて記述してください。
https://drive.google.com/file/d/1QkYslq5IA7uVTtiiiH7PWXp1I6gvGZQD/view?usp=sharing
パスワードは"infected"です。
リンクにアクセスすると不審なファイル・不正なファイルといった表示がされるかと思いますが、これは課題用ファイルのためマルウェアではありません。また、VTやその他のスキャンサービスにアップロードするのは禁止です。
注意点: デバッグを行う場合はdata.txtを別ディレクトリにコピーしたほうがいいです。ファイルの中身はC3講義のタイトルと概要(英訳)をコピーして貼り付けているのみなので、忘れていた場合はサイトから持ってきて作り直して下さい。

問6-1

data.txtを暗号化するために必要な作業について説明して下さい

Ghidraで解析してみると,現在の日時のチェックが行われ,3秒待ってからある関数が別スレッドで実行され,その終了を待っている箇所が確認できます

void FUN_1400018db(void)

{
  _SYSTEMTIME local_28;
  HANDLE local_10;
  
  GetSystemTime(&local_28);
  if (((local_28.wYear == 0x7e9) && (local_28.wMonth == 8)) && (local_28.wDay == 0xb)) {
    Sleep(3000);
    local_10 = CreateThread((LPSECURITY_ATTRIBUTES)0x0,0,(LPTHREAD_START_ROUTINE)&LAB_140001719,
                            (LPVOID)0x0,0,(LPDWORD)0x0);
    if (local_10 != (HANDLE)0x0) {
      WaitForSingleObject(local_10,0xffffffff);
      CloseHandle(local_10);
    }
  }
  return;
}

呼ばれている関数は次の通りとなっていて,data.txtを読み込んで,処理が行われ,データが書き込まれている事がわかります.

処理を順に追っていくと,GetModuleFileNameAにより実行ファイルのフルパスを取得し,そのパスをPathRemoveFileSpecAに渡すことにより実行ファイルの存在するディレクトリのパスを取得します.次に,そのパスとdata.txtPathCombineAで結合して,data.txtのフルパスを得ます.以上をまとめると,実行ファイルと同じディレクトリにあるdata.txtを対象として処理が行われることがわかります.

さらに,CreateFileAdata.txtを開き,GetFileSizeによりそのファイルサイズを得ます.ファイルサイズ分のメモリ領域がmallocで確保され,更にそのポインタがpUStack_28に入ります.さらにポインタはFUN_140001554に渡されています.

undefined8 UndefinedFunction_140001719(void)

{
  DWORD DStack_254;
  LPCVOID pvStack_250;
  CHAR aCStack_248 [272];
  CHAR aCStack_138 [272];
  PUCHAR pUStack_28;
  DWORD DStack_1c;
  HANDLE pvStack_18;
  char *pcStack_10;
  
  pcStack_10 = "data.txt";
  GetModuleFileNameA((HMODULE)0x0,aCStack_138,0x104);
  PathRemoveFileSpecA(aCStack_138);
  PathCombineA(aCStack_248,aCStack_138,pcStack_10);
  pvStack_18 = CreateFileA(aCStack_248,0xc0000000,0,(LPSECURITY_ATTRIBUTES)0x0,3,0x80,(HANDLE)0x0);
  if (pvStack_18 != (HANDLE)0xffffffffffffffff) {
    DStack_1c = GetFileSize(pvStack_18,(LPDWORD)0x0);
    if (DStack_1c != 0xffffffff) {
      pUStack_28 = (PUCHAR)malloc((ulonglong)(DStack_1c + 1));
      ReadFile(pvStack_18,pUStack_28,DStack_1c,&DStack_254,(LPOVERLAPPED)0x0);
      if (pUStack_28 != (PUCHAR)0x0) {
        FUN_140001554(pUStack_28,&pvStack_250,DStack_1c);
      }
      SetFilePointer(pvStack_18,0,(PLONG)0x0,0);
      WriteFile(pvStack_18,pvStack_250,DStack_1c,&DStack_254,(LPOVERLAPPED)0x0);
    }
  }
  return 0;
}

FUN_140001554の内部では,AES-CBCを使った暗号化が行われていることがわかります.

void FUN_140001554(PUCHAR param_1,undefined8 *param_2,ULONG param_3)

{
  void *pvVar1;
  ULONG local_28;
  uint local_24;
  BCRYPT_KEY_HANDLE local_20;
  BCRYPT_ALG_HANDLE local_18;
  PUCHAR local_10;
  
  local_18 = (BCRYPT_HANDLE)0x0;
  local_20 = (BCRYPT_KEY_HANDLE)0x0;
  local_24 = 0;
  local_28 = 0;
  FUN_140001450();
  FUN_1400014f7();
  BCryptOpenAlgorithmProvider(&local_18,L"AES",(LPCWSTR)0x0,0);
  BCryptSetProperty(local_18,L"ChainingMode",(PUCHAR)L"ChainingModeCBC",0x20,0);
  BCryptGetProperty(local_18,L"ObjectLength",(PUCHAR)&local_24,4,&local_28,0);
  local_10 = (PUCHAR)malloc((ulonglong)local_24);
  BCryptGenerateSymmetricKey(local_18,&local_20,local_10,local_24,&DAT_140007040,0x20,0);
  local_28 = 0;
  BCryptEncrypt(local_20,param_1,param_3,(void *)0x0,&DAT_140007060,0x10,(PUCHAR)0x0,0,&local_28,1);
  pvVar1 = malloc((ulonglong)local_28);
  *param_2 = pvVar1;
  BCryptEncrypt(local_20,param_1,param_3,(void *)0x0,&DAT_140007060,0x10,(PUCHAR)*param_2,local_28,
                &local_28,1);
  return;
}

以上より,GetSystemTimeのチェックが通れば暗号化が行われると考えられます.if文の条件より,2025年8月11日であればよい事がわかります.Windowsの日時設定を2025年8月11日に変更することによって,data.txtが暗号化されるようになります.ただし,GetSystemTimeはUTC時間を返す( https://learn.microsoft.com/ja-jp/windows/win32/api/sysinfoapi/nf-sysinfoapi-getsystemtime )ので,注意が必要です.

問6-2

data.txtを暗号化する暗号関数・暗号フローについて説明して下さい

暗号化が行われている関数を再掲します.

void FUN_140001554(PUCHAR param_1,undefined8 *param_2,ULONG param_3)

{
  void *pvVar1;
  ULONG local_28;
  uint local_24;
  BCRYPT_KEY_HANDLE local_20;
  BCRYPT_ALG_HANDLE local_18;
  PUCHAR local_10;
  
  local_18 = (BCRYPT_HANDLE)0x0;
  local_20 = (BCRYPT_KEY_HANDLE)0x0;
  local_24 = 0;
  local_28 = 0;
  FUN_140001450();
  FUN_1400014f7();
  BCryptOpenAlgorithmProvider(&local_18,L"AES",(LPCWSTR)0x0,0);
  BCryptSetProperty(local_18,L"ChainingMode",(PUCHAR)L"ChainingModeCBC",0x20,0);
  BCryptGetProperty(local_18,L"ObjectLength",(PUCHAR)&local_24,4,&local_28,0);
  local_10 = (PUCHAR)malloc((ulonglong)local_24);
  BCryptGenerateSymmetricKey(local_18,&local_20,local_10,local_24,&DAT_140007040,0x20,0);
  local_28 = 0;
  BCryptEncrypt(local_20,param_1,param_3,(void *)0x0,&DAT_140007060,0x10,(PUCHAR)0x0,0,&local_28,1);
  pvVar1 = malloc((ulonglong)local_28);
  *param_2 = pvVar1;
  BCryptEncrypt(local_20,param_1,param_3,(void *)0x0,&DAT_140007060,0x10,(PUCHAR)*param_2,local_28,
                &local_28,1);
  return;
}

BCryptOpenAlgorithmProviderにより,AESのプロバイダオブジェクトを取得します.プロバイダオブジェクトとは,暗号化のアルゴリズムなどを提供するオブジェクトです.これらの一連の暗号化処理には,WindowsのCNGというAPIが使われています.さらに,BCryptSetPropertyにより,CBCを使うことを指定します.その後,BCryptEncryptでデータが暗号化されます.

BCryptGenerateSymmetricKeyではDAT_140007040がAES鍵として,BCryptEncryptではIVとしてDAT_140007060が渡されています.AES鍵は32バイトの長さであることがわかります.その中身を参照すると,すべてが0で埋められている事がわかりました.とりあえず,鍵とIVを0としてopensslを使って復号を試みます.

$ openssl enc -aes-256-cbc -d -K 0000000000000000000000000000000000000000000000000000000000000000 -iv 00000000000000000000000000000000 -in data.txt -out dec.txt
bad decrypt
808BAB6B147F0000:error:1C80006B:Provider routines:ossl_cipher_generic_block_final:wrong final block length:../providers/implementations/ciphers/ciphercommon.c:443:

生成されたdec.txtの中身も人間が読める文字列になっておらず,うまくいきませんでした.そこで,もう一度ghidraの解析結果をみてみると,暗号処理の前にFUN_140001450FUN_1400014f7が呼ばれています.その中身を解析します.

まず,FUN_140001450の内部では,DAT_140003080のデータに対して0xaaとXOR演算を行って上書きした後に,DAT_140007040にデータが書き込まれている事がわかります.つまり,FUN_140001450ではAES鍵の導出を行っている関数であるといえます.

void FUN_140001450(void)

{
  int local_10;
  int local_c;
  
  for (local_c = 0; local_c < 4; local_c = local_c + 1) {
    (&DAT_140003080)[local_c] = (&DAT_140003080)[local_c] ^ 0xaa;
  }
  for (local_10 = 0; local_10 < 0x20; local_10 = local_10 + 1) {
    (&DAT_140007040)[local_10] =
         (byte)((&DAT_140003080)[local_10 % 4] ^
               (byte)*(undefined4 *)(&DAT_140003000 + (longlong)local_10 * 4)) >> 1;
  }
  return;
}

次に,FUN_1400014f7では,_time64によってUNIX時間を取得した後,その値をseedとしてrand関数を使って乱数を取得しています.したがって,UNIX時間より生成される疑似乱数をIVとしていることがわかります.

void FUN_1400014f7(void)

{
  int iVar1;
  __time64_t _Var2;
  int local_c;
  
  _Var2 = _time64((__time64_t *)0x0);
  srand((uint)_Var2);
  for (local_c = 0; local_c < 0x10; local_c = local_c + 1) {
    iVar1 = rand();
    (&DAT_140007060)[local_c] = (char)iVar1;
  }
  return;
}

以上をまとめると,このプログラムでは鍵長256ビットのAES-CBCが使われ,data.txtが暗号化されます.ただし,AES鍵はデータセクションの値から導出され,IVはUNIX時間をseedとした疑似乱数から導出されます.

問6-3

data.txtを復号し、利用した復号プログラム・復号結果の記載ともし完全に復号できない場合、その理由について説明して下さい

問6-2で解析した内容をプログラムに書き直します.まず,AES鍵の導出ロジックをPythonで書き直します.AES鍵の導出が行われている箇所のGhidraの解析結果を再掲します.

void FUN_140001450(void)

{
  int local_10;
  int local_c;
  
  for (local_c = 0; local_c < 4; local_c = local_c + 1) {
    (&DAT_140003080)[local_c] = (&DAT_140003080)[local_c] ^ 0xaa;
  }
  for (local_10 = 0; local_10 < 0x20; local_10 = local_10 + 1) {
    (&DAT_140007040)[local_10] =
         (byte)((&DAT_140003080)[local_10 % 4] ^
               (byte)*(undefined4 *)(&DAT_140003000 + (longlong)local_10 * 4)) >> 1;
  }
  return;
}

はじめのforループで,DAT_140003080の各要素に対して0xaaとXORして,上書きしています.次のforループでは,DAT_140007040へ,DAT_140003080DAT_140003000をもとにXORとシフト演算を行ってAES鍵を導出しています.これをPythonで書き直すために,まずは鍵のseedとなっているDAT_140003080DAT_140003000の箇所にどのようなデータが格納されているかを確かめます.

DAT_140003080には次のデータが格納されています.

       140003080 3a              ??         3Ah    :
       140003081 7f              ??         7Fh    
       140003082 c2              ??         C2h
       140003083 9b              ??         9Bh

DAT_140003000には次のデータが格納されています.

       140003000 d6              ??         D6h
       140003001 01              ??         01h
       140003002 00              ??         00h
       140003003 00              ??         00h
       140003004 37              ??         37h    7
       140003005 01              ??         01h
       140003006 00              ??         00h
       140003007 00              ??         00h
       140003008 0c              ??         0Ch
       140003009 01              ??         01h
       14000300a 00              ??         00h
       14000300b 00              ??         00h
       14000300c b9              ??         B9h
       14000300d 01              ??         01h
       14000300e 00              ??         00h
       14000300f 00              ??         00h
       140003010 3a              ??         3Ah    :
       140003011 01              ??         01h
       140003012 00              ??         00h
       140003013 00              ??         00h
       140003014 19              ??         19h
       140003015 01              ??         01h
       140003016 00              ??         00h
       140003017 00              ??         00h
       140003018 86              ??         86h
       140003019 01              ??         01h
       14000301a 00              ??         00h
       14000301b 00              ??         00h
       14000301c 61              ??         61h    a
       14000301d 01              ??         01h
       14000301e 00              ??         00h
       14000301f 00              ??         00h
       140003020 e2              ??         E2h
       140003021 01              ??         01h
       140003022 00              ??         00h
       140003023 00              ??         00h
       140003024 55              ??         55h    U
       140003025 01              ??         01h
       140003026 00              ??         00h
       140003027 00              ??         00h
       140003028 ca              ??         CAh
       140003029 01              ??         01h
       14000302a 00              ??         00h
       14000302b 00              ??         00h
       14000302c f5              ??         F5h
       14000302d 01              ??         01h
       14000302e 00              ??         00h
       14000302f 00              ??         00h
       140003030 76              ??         76h    v
       140003031 01              ??         01h
       140003032 00              ??         00h
       140003033 00              ??         00h
       140003034 9d              ??         9Dh
       140003035 01              ??         01h
       140003036 00              ??         00h
       140003037 00              ??         00h
       140003038 02              ??         02h
       140003039 01              ??         01h
       14000303a 00              ??         00h
       14000303b 00              ??         00h
       14000303c bd              ??         BDh
       14000303d 01              ??         01h
       14000303e 00              ??         00h
       14000303f 00              ??         00h
       140003040 3e              ??         3Eh    >
       140003041 01              ??         01h
       140003042 00              ??         00h
       140003043 00              ??         00h
       140003044 05              ??         05h
       140003045 01              ??         01h
       140003046 00              ??         00h
       140003047 00              ??         00h
       140003048 9a              ??         9Ah
       140003049 01              ??         01h
       14000304a 00              ??         00h
       14000304b 00              ??         00h
       14000304c 71              ??         71h    q
       14000304d 01              ??         01h
       14000304e 00              ??         00h
       14000304f 00              ??         00h
       140003050 f2              ??         F2h
       140003051 01              ??         01h
       140003052 00              ??         00h
       140003053 00              ??         00h
       140003054 51              ??         51h    Q
       140003055 01              ??         01h
       140003056 00              ??         00h
       140003057 00              ??         00h
       140003058 ce              ??         CEh
       140003059 01              ??         01h
       14000305a 00              ??         00h
       14000305b 00              ??         00h
       14000305c f9              ??         F9h
       14000305d 01              ??         01h
       14000305e 00              ??         00h
       14000305f 00              ??         00h
       140003060 7a              ??         7Ah    z
       140003061 01              ??         01h
       140003062 00              ??         00h
       140003063 00              ??         00h
       140003064 99              ??         99h
       140003065 01              ??         01h
       140003066 00              ??         00h
       140003067 00              ??         00h
       140003068 06              ??         06h
       140003069 01              ??         01h
       14000306a 00              ??         00h
       14000306b 00              ??         00h
       14000306c a1              ??         A1h
       14000306d 01              ??         01h
       14000306e 00              ??         00h
       14000306f 00              ??         00h
       140003070 22              ??         22h    "
       140003071 01              ??         01h
       140003072 00              ??         00h
       140003073 00              ??         00h
       140003074 15              ??         15h
       140003075 01              ??         01h
       140003076 00              ??         00h
       140003077 00              ??         00h
       140003078 8a              ??         8Ah
       140003079 01              ??         01h
       14000307a 00              ??         00h
       14000307b 00              ??         00h
       14000307c 79              ??         79h    y
       14000307d 00              ??         00h
       14000307e 00              ??         00h
       14000307f 00              ??         00h

まず,DAT_140003080に対するXORですが,これは各要素に対して0xaaをXORするだけです.したがって,Pythonで書き直すと次の通りになります.

    seed = [0x3A, 0x7F, 0xC2, 0x9B]
    for i in range(len(seed)):
        seed[i] ^= 0xAA

次のfor文ですが,ghidraの逆コンパイルの結果を見ると,4バイトずつ持ってきて,それをint型として読み込み,さらにそれをbyte型としてキャストして,その値と(&DAT_140003080)[local_10 % 4]をXORしています.そして1ビット分右シフトを行います.ここで注意するべきなのは,一度byteにキャストしているため,実質1バイト分しかAES鍵の生成には使われません.さらに,x86はリトルエンディアンで動くので,下位バイトから順に格納されていきます.したがって,XORされるのは,140003000140003004140003008,…の値です.以上に注意して,Pythonで書き直すと次のようになります.seed2は,DAT_140003080から0バイト目,4バイト目,8バイト目…を抜き出した配列です.

    key = []
    seed2 = [0xD6, 0x37, 0x0C, 0xB9, 0x3A, 0x19, 0x86, 0x61, 0xE2, 0x55, 0xCA, 0xF5, 0x76, 0x9D, 0x02, 0xBD, 0x3E, 0x05, 0x9A, 0x71, 0xF2, 0x51, 0xCE, 0xF9, 0x7A, 0x99, 0x06, 0xA1, 0x22, 0x15, 0x8A, 0x79]
    for i in range(0x20):
        key.append((seed[i % 4] ^ seed2[i]) >> 1)

以上をまとめると,AES鍵を導出するPythonプログラムは次の通りとなります.

def generate_aes_key() -> bytes:
    seed = [0x3A, 0x7F, 0xC2, 0x9B]
    seed2 = [0xD6, 0x37, 0x0C, 0xB9, 0x3A, 0x19, 0x86, 0x61, 0xE2, 0x55, 0xCA, 0xF5, 0x76, 0x9D, 0x02, 0xBD, 0x3E, 0x05, 0x9A, 0x71, 0xF2, 0x51, 0xCE, 0xF9, 0x7A, 0x99, 0x06, 0xA1, 0x22, 0x15, 0x8A, 0x79]
    key = []

    for i in range(len(seed)):
        seed[i] ^= 0xAA

    for i in range(0x20):
        key.append((seed[i % 4] ^ seed2[i]) >> 1)

    return bytes(key)

次は,IVの導出ロジックをPythonで書き直します.ghidraで解析した結果は次のとおりでした.

void FUN_1400014f7(void)

{
  int iVar1;
  __time64_t _Var2;
  int local_c;
  
  _Var2 = _time64((__time64_t *)0x0);
  srand((uint)_Var2);
  for (local_c = 0; local_c < 0x10; local_c = local_c + 1) {
    iVar1 = rand();
    (&DAT_140007060)[local_c] = (char)iVar1;
  }
  return;
}

_time64を呼び出して,その値をsrandに与えて乱数を生成しています.Pythonにも疑似乱数の生成関数はありますが,アルゴリズムが異なる可能性があるので,全く同じ関数を同じDLLから呼び出すのがよいと考えました.ghidraの解析結果によると,srandrandAPI-MS-WIN-CRT-UTILITY-L1-1-0.DLLから呼び出されていることがわかりました.それらは__cdeclで呼び出されています.__cdeclとは呼び出し規約のことで,Win32 APIでは__stdcallが使われ,スタックのクリーンアップ(スタックに積まれた関数の引数のクリーンアップ)を呼び出し元が行うか・呼び出し先が行うかなどが異なります.今回は__cdeclなので,Pythonからrandsrandを呼び出すときは注意する必要があります.Pythonで__cdeclが用いられている関数を呼び出すためには,ctypes.CDLLを使って実装する必要があります.以上により,与えられたUNIX時間をもとにIVを生成する関数は次の通りとなります.rand()の結果はchar型にキャストされています.したがって,下位1バイトのみが取り出されます.Pythonで同じことを実現するために,0xffとAND演算を行い,ivへappendしています.

import ctypes

ucrt_utility = ctypes.CDLL("API-MS-WIN-CRT-UTILITY-L1-1-0.DLL")

def get_iv(unix_time: int) -> bytes:
    iv = []
    ucrt_utility.srand(ctypes.c_uint(unix_time))
    for _ in range(0x10):
        iv.append(ucrt_utility.rand() & 0xFF)
    return bytes(iv)

AES鍵は必ず同じ値が算出される一方,IVはUNIX時間に依存しているため,実行時のUNIX時間が分かっていないと復号することができません.しかし,今回のプログラムでは,実行される日にちまでは確定しており(2025年8月11日),さらに暗号化される前のデータも既知です.そこで,2025年8月11日の24時間分のUNIX時間をすべて総当たりして,IVを求めることにしました.

具体的には,AESの最初のブロックのみを試しに復号してみて,その結果が平文と一致するかどうかをチェックします.AESでは16バイトのブロックごとに復号されるので,IVが正しければdata.txtの最初の16文字分(Decoding Ransomw)と一致するはずです.

以上を実装したPythonスクリプトは次のとおりです.AESの復号の機能を使うので,pycryptodomeをインストールする必要があります.カレントディレクトリの暗号化済みのdata.txtを読み込んで,鍵とIVと復号結果を標準出力へ出力します.

# Windowsでしか動かない
# python 3.12で動いてます

import ctypes
import datetime
from Crypto.Cipher import AES

ucrt_utility = ctypes.CDLL("API-MS-WIN-CRT-UTILITY-L1-1-0.DLL")


def generate_aes_key() -> bytes:
    seed = [0x3A, 0x7F, 0xC2, 0x9B]
    seed2 = [0xD6, 0x37, 0x0C, 0xB9, 0x3A, 0x19, 0x86, 0x61, 0xE2, 0x55, 0xCA, 0xF5, 0x76, 0x9D, 0x02, 0xBD, 0x3E, 0x05, 0x9A, 0x71, 0xF2, 0x51, 0xCE, 0xF9, 0x7A, 0x99, 0x06, 0xA1, 0x22, 0x15, 0x8A, 0x79]
    key = []

    for i in range(len(seed)):
        seed[i] ^= 0xAA

    for i in range(0x20):
        key.append((seed[i % 4] ^ seed2[i]) >> 1)

    return bytes(key)


def get_iv(unix_time: int) -> bytes:
    iv = []
    ucrt_utility.srand(ctypes.c_uint(unix_time))
    for _ in range(0x10):
        iv.append(ucrt_utility.rand() & 0xFF)
    return bytes(iv)


aes_key = generate_aes_key()

with open("data.txt", "rb") as f:
    cipher_text = f.read()
    first_block = cipher_text[:16]
    # bfa
    start_time = datetime.datetime(2025, 8, 11, tzinfo=datetime.timezone.utc)
    end_time = start_time + datetime.timedelta(days=1)
    for unix_time in range(int(start_time.timestamp()), int(end_time.timestamp())):
        iv = get_iv(unix_time)
        cipher = AES.new(aes_key, AES.MODE_CBC, iv)
        plain_text = cipher.decrypt(first_block).decode("ascii", errors="ignore")
        if plain_text == "Decoding Ransomw":
            break
    else:
        raise Exception("IV bfa failed")

    print(f"Key: {aes_key.hex()}")
    print(f"IV: {iv.hex()}")

    cipher = AES.new(aes_key, AES.MODE_CBC, iv)
    print(
        cipher.decrypt(cipher_text[: len(cipher_text) // 16 * 16]).decode(
            "utf-8", errors="ignore"
        )
    )

出力結果は次の通りとなり,末尾を除いて復号できている事がわかります.

Key: 2371324455667728394051627324354657687920314253647526374859607124
IV: 34dedba6f71dd0f8aa46ccdabd1c5b3a
Decoding Ransomware: Unraveling the Mind of Cybercriminals

Ransomware is malicious malware that encrypts data and demands a ransom to decrypt it. It has become a major risk for individuals and organizations. This lecture aims to provide students with an understanding of the basic principles of ransomware operation and infection routes. In addition to this, the course also touches on the psychology of cybercriminals who launch ransomware attacks, their motives, and attack strategies.

The lecture will begin with a basic overview of ransomware and the relationship between threat actors. Then, through static and dynamic analysis, students will learn basic analysis techniques using debuggers. The goal of this lecture is to understand why ransomware systems are created and why ransomware is prevalent after mastering basic ransomware analysi

AESでは16バイトのブロックごとに暗号化されるため,平文のデータサイズも16バイトの倍数である必要があります.しかし,今回与えられたdata.txtは16バイトの倍数にはなっていません.そこで,今回のプログラムでは与えられたデータに対してパディングが行われています.BCryptEncryptの引数の最後の値が1となっているため,PKCS#7によるパディングが行われていることがわかります.したがって,本来は暗号化したあとのデータは16バイトの倍数であるはずです.しかし,16バイトの倍数であるはずのデータは,UndefinedFunction_140001719の内部のWriteFile関数によって,元の平文ファイルのサイズ分だけしか書き込まれないため,最後のブロックが欠落した状態で保存されます.したがって,一番最後のブロックは復号することができません.

今回実装したプログラムでは,最後のブロックの部分をlen(cipher_text) // 16 * 16によって取り除くことで,最後のブロック以外のデータを復号しています.

問7

後述のアーカイブファイルに含まれるquestions.ddはext4のパーティションをddコマンドでコピーしたイメージファイルです。
このイメージファイルを対象に、問7-1, 7-2, 7-3の機能を実現するPythonスクリプトをそれぞれ作成して提出してください。スクリプトファイルは単一のファイルでも複数に分かれても構いません。なお、作成するスクリプトは次のA, B, Cの要件を満たす必要があります。
A. ext4ファイルシステムのメタデータの構造体を解析・参照することで各問で示された機能を実現すること(単に正解の値を保持して出力する、などの実装は禁止)。
B. スクリプト内の処理でdebugfs等のOSコマンドの出力結果を直接参照しないこと。ただし、スクリプトを作成するための調査においてdebugfs含む様々なコマンドを用いてイメージファイルを解析したり、実際に手元の端末にマウントしたりすることは可能。
C. Python 3.12 で動作すること
その後、問7-4, 7-5, 7-6に回答してください。
■アーカイブファイル配布URL
https://bit.ly/42akdJH
イメージファイル(questions.dd)のSHA256ハッシュ値:
87dd1e19f97d700a914bb4127d1432788e4fcaa0def61eaa5b2281f2f6ad8186

問7-1

イメージファイルquestions.ddに存在するinode番号14のファイルについて、次の機能を実現するスクリプト:
①ファイル名を出力する
②ファイルのデータが存在する物理アドレスの始点を出力する
③ファイルのデータを取り出し、イメージファイルと同じディレクトリに別のファイルとして保存する
※出力したファイルのハッシュ値が以下の値であること
fdf7c291905cf475f22a434f1f1c188a13496eff7429e6b15dd2887845c3558f

問7-2

イメージファイルquestions.ddに存在するinode番号16のファイルについて、次の機能を実現するスクリプト:
①ファイル名を出力する
②ファイルのデータが存在する物理アドレスの始点を出力する
③ファイルのデータを取り出し、イメージファイルと同じディレクトリに別のファイルとして保存する
※出力したファイルのハッシュ値が以下の値であること
9f964b89facdc3671569c812ba8b3fadd20f9dfcf1873b06fe672761392e7523

問7-3

イメージファイルquestions.ddに存在するinode番号18のファイルについて、次の機能を実現するスクリプト:
①ファイル名を出力する
②ファイルのデータが存在する物理アドレスの始点を出力する
③ファイルのデータを取り出し、イメージファイルと同じディレクトリに別のファイルとして保存する
※出力したファイルのハッシュ値が以下の値であること
31055b7421279896ad6e0b8d2f6993ee219c2ba88d758917ac2f662e39dba32e

問7-1~7-3は共通したPythonスクリプトに実装しました。target_inode_numを書き換えることで動作します。

import dataclasses
import math
import struct


@dataclasses.dataclass
class super_block:
    s_log_block_size: int
    s_inode_size: int
    s_inodes_per_group: int
    s_blocks_per_group: int
    s_desc_size: int
    s_blocks_count: int


@dataclasses.dataclass
class block_group_descriptors:
    bg_inode_table: int


@dataclasses.dataclass
class ext4_extent_header:
    eh_magic: int
    eh_entries: int
    eh_max: int
    eh_depth: int
    eh_generation: int


@dataclasses.dataclass
class ext4_extent_idx:
    ei_block: int
    ei_leaf_lo: int
    ei_leaf_hi: int


@dataclasses.dataclass
class ext4_extent:
    ee_block: int
    ee_len: int
    ee_start_hi: int
    ee_start_lo: int


@dataclasses.dataclass
class inode:
    i_flags: int
    i_size: int
    # i_block
    extent_header: ext4_extent_header
    extent_entries: list[ext4_extent | ext4_extent_idx]


@dataclasses.dataclass
class ext4_dir_entry_2:
    inode: int
    file_type: int
    name: str


def parse_super_block(data: bytes) -> super_block:
    # https://docs.kernel.org/filesystems/ext4/globals.html
    s_log_block_size = int.from_bytes(data[0x18 : 0x18 + 4], "little")
    s_inode_size = int.from_bytes(data[0x58 : 0x58 + 2], "little")
    s_inodes_per_group = int.from_bytes(data[0x28 : 0x28 + 4], "little")
    s_blocks_per_group = int.from_bytes(data[0x20 : 0x20 + 4], "little")
    s_desc_size = int.from_bytes(data[0xFE : 0xFE + 2], "little")
    # そこまで大きくないので下位32ビットで十分
    s_blocks_count_lo = int.from_bytes(data[0x04 : 0x04 + 4], "little")
    return super_block(
        s_log_block_size,
        s_inode_size,
        s_inodes_per_group,
        s_blocks_per_group,
        s_desc_size,
        s_blocks_count_lo,
    )


def parse_bgd(data: bytes) -> block_group_descriptors:
    bg_inode_table = int.from_bytes(data[0x08 : 0x08 + 4], "little")
    return block_group_descriptors(bg_inode_table)


def parse_extents_nodes(
    data: bytes,
) -> tuple[ext4_extent_header, list[ext4_extent | ext4_extent_idx]]:
    extent_header_bytes = data[:12]
    eh_magic, eh_entries, eh_max, eh_depth, eh_generation = struct.unpack_from(
        "<HHHHI", extent_header_bytes
    )

    all_extent_bytes = data[12:]
    if eh_magic != 0xF30A:
        raise ValueError("invalid magic number")
    extent_header = ext4_extent_header(
        eh_magic, eh_entries, eh_max, eh_depth, eh_generation
    )

    extent_entries: list[ext4_extent | ext4_extent_idx] = []
    for offset in range(0, eh_entries * 12, 12):
        extent_bytes = all_extent_bytes[offset : offset + 12]
        if extent_header.eh_depth == 0:
            ee_block, ee_len, ee_start_hi, ee_start_lo = struct.unpack_from(
                "<IHHI", extent_bytes
            )
            extent = ext4_extent(ee_block, ee_len, ee_start_hi, ee_start_lo)
            extent_entries.append(extent)
        else:
            ei_block, ei_leaf_lo, ei_leaf_hi = struct.unpack_from("<IIH", extent_bytes)
            extent_idx = ext4_extent_idx(ei_block, ei_leaf_lo, ei_leaf_hi)
            extent_entries.append(extent_idx)

    return (extent_header, extent_entries)


def parse_inode(data: bytes) -> inode:
    i_size = int.from_bytes(data[0x04 : 0x04 + 4], "little")
    i_flags = int.from_bytes(data[0x20 : 0x20 + 4], "little")

    # EXT4_EXTENTS_FL
    if i_flags & 0x80000:
        extent_header, extent_entries = parse_extents_nodes(data[0x28:])
        return inode(
            i_flags=i_flags,
            i_size=i_size,
            extent_header=extent_header,
            extent_entries=extent_entries,
        )
    else:
        raise ValueError("EXT4_EXTENTS_FL is not true")


def parse_ext4_dir_entry_2(data: bytes) -> list[ext4_dir_entry_2]:
    result: list[ext4_dir_entry_2] = []
    offset = 0
    while offset < len(data):
        inode_num, rec_len, name_len, file_type = struct.unpack_from(
            "<IHBB", data[offset : offset + 8]
        )
        if inode_num == 0:
            # https://elixir.bootlin.com/linux/v6.12.4/source/fs/ext4/ext4.h#L2346
            break
        if rec_len == 0:
            # 無限ループ回避
            break
        name = data[offset + 8 : offset + 8 + name_len].decode("utf-8")
        offset += rec_len
        result.append(ext4_dir_entry_2(inode_num, file_type, name))

    return result


def get_inode(
    inode_num: int, bgds: list[block_group_descriptors], sb: super_block, in_image
) -> inode:
    block_size = 1024 << sb.s_log_block_size
    target_bgd = bgds[(inode_num - 1) // sb.s_inodes_per_group]
    inode_offset = (inode_num - 1) % sb.s_inodes_per_group
    in_image.seek(
        target_bgd.bg_inode_table * block_size + sb.s_inode_size * inode_offset
    )
    inode_byte = in_image.read(sb.s_inode_size)
    return parse_inode(inode_byte)


padding_cache: bytes | None = None
is_start_address_shown: bool = False


def explore_nodes(
    header: ext4_extent_header,
    entries: list[ext4_extent | ext4_extent_idx],
    next_block_num: int,
    block_size: int,
    left_inode_size: int,
    in_image,
    out_file,
) -> tuple[int, int]:
    global padding_cache
    global is_start_address_shown

    if header.eh_depth == 0:
        for entry in entries:
            # スパースファイル
            if next_block_num != entry.ee_block:
                padding_block_num = entry.ee_block - next_block_num
                # 早くするためにキャッシュしておく
                if padding_cache is None:
                    padding_cache = b"\x00" * block_size
                # padding_cache * 1とするとメモリコピーが発生するらしい
                if padding_block_num == 1:
                    out_file.write(padding_cache)
                elif padding_block_num > 0:
                    out_file.write(padding_cache * (entry.ee_block - next_block_num))
                else:
                    raise ValueError("ee_block order is invalid")
                left_inode_size -= block_size
            
            if not is_start_address_shown:
                print(f"start address: {entry.ee_start_lo * block_size}")
                is_start_address_shown = True

            # 小さいので下位ビットしか見なくてOK
            in_image.seek(entry.ee_start_lo * block_size)
            size = min(entry.ee_len * block_size, left_inode_size)
            left_inode_size -= size
            next_block_num = entry.ee_block + entry.ee_len
            out_file.write(in_image.read(size))
    else:
        for entry in entries:
            in_image.seek(block_size * entry.ei_leaf_lo)
            block_bytes = in_image.read(block_size)
            header, entries = parse_extents_nodes(block_bytes)
            next_block_num, left_inode_size = explore_nodes(
                header,
                entries,
                next_block_num,
                block_size,
                left_inode_size,
                in_image,
                out_file,
            )

    return next_block_num, left_inode_size


def get_filename(
    parent_inode: inode,
    target_inode_num: int,
    block_size: int,
    bgds: list[block_group_descriptors],
    sb: super_block,
    in_image,
) -> str | None:
    if parent_inode.extent_header.eh_depth != 0:
        # ツリー構造になっているディレクトリは実装しない
        raise ValueError("too many directories or files")

    for entry in parent_inode.extent_entries:
        in_image.seek(entry.ee_start_lo * block_size)
        dir_bytes = in_image.read(entry.ee_len * block_size)
        dirs = parse_ext4_dir_entry_2(dir_bytes)
        for dir in dirs:
            # ディレクトリなら次のところへ探索する
            if dir.file_type == 0x2 and dir.name not in (".", ".."):
                child_inode = get_inode(dir.inode, bgds, sb, in_image)
                name = get_filename(
                    child_inode, target_inode_num, block_size, bgds, sb, in_image
                )
                if name is not None:
                    return name
            elif dir.inode == target_inode_num:
                return dir.name

    return None


image_file = "questions.dd"
target_inode_num = 14

with open(image_file, "rb") as f:
    # skip boot sector
    f.seek(1024)

    # super block
    super_block_bytes = f.read(1024)
    sb = parse_super_block(super_block_bytes)

    # block group descriptors

    block_size = 1024 << sb.s_log_block_size
    # s_log_block_size > 2を前提としている
    # 一番最初のブロックにboot sectorとsuper blockが収まっている前提
    f.seek(block_size)
    bgds: list[block_group_descriptors] = []
    groups_count = math.ceil(sb.s_blocks_count / sb.s_blocks_per_group)
    all_bgd_bytes = f.read(sb.s_desc_size * groups_count)
    for offset in range(0, len(all_bgd_bytes), sb.s_desc_size):
        bgd_bytes = all_bgd_bytes[offset : offset + sb.s_desc_size]
        bgd = parse_bgd(bgd_bytes)
        bgds.append(bgd)

    # ディレクトリの情報を取得
    dir_inode = get_inode(2, bgds, sb, f)
    filename = get_filename(dir_inode, target_inode_num, block_size, bgds, sb, f)
    print(f"filename: {filename}")

    target_inode = get_inode(target_inode_num, bgds, sb, f)

    with open(filename, "wb") as o:
        _, left_inode_size = explore_nodes(
            target_inode.extent_header,
            target_inode.extent_entries,
            0,
            block_size,
            target_inode.i_size,
            f,
            o,
        )
        if left_inode_size > 0:
            # 末尾がスパース
            o.write(b"\x00" * left_inode_size)
        elif left_inode_size < 0:
            raise ValueError("invalid filesize")

問7-4

問7-1との違いは,ファイルが格納されているブロックが2箇所に分かれていること,そしてそれぞれの箇所において,ブロックが連続して配置されていることが異なります.問7-1では,1つのブロックに収まるサイズのファイルなので,ファイル全体が1つのブロックに格納されていました.ext4では,ファイルが格納されているブロックを記述する際,そのブロックの位置と連続するブロックの数を指定することができます.そのため,実装する上では,inodeからブロックの位置と範囲を取得し,ブロックをまとめて取り出し,連結したうえでもとのファイルサイズ分だけ切り出してデータを出力するように実装しました.

問7-5

問7-2との違いは,inodeに記述されているブロックの情報が,木構造になっています.ext4では,ファイルサイズが大きくなると,すべてのブロックの情報をinodeに記述することが困難になるので,B木の構造で,ファイルが格納されているブロックを保持します.B木で表現することで,ファイルの中の必要な範囲のデータを効率的に取り出すことが可能です.実装するうえでは,B木を深さ優先探索して,順にデータブロックを結合するプログラムを作成しました.さらに,問7-2との違いは,このデータがスパースファイルであることで,データが書き込まれていない範囲(0で埋められる範囲)が飛ばされて格納されています.そこで,ブロック番号が飛ばされている場合は,欠落しているブロックのサイズ分だけ0を書き込むように実装しています.

問7-6

ext4の構造について知らなかったので,ChatGPTに構造や仕様について質問を重ねました.なんとなく全体像がわかってきたところで,詳しい仕様を確認するためにLinuxカーネルのドキュメントや,ソースコードを参照して,実装するための具体的な仕様について調べました.カーネル系の実装については,細かい仕様が誤っていると動かないので,ChatGPTにはソースコードを生成してもらうことは今回は行っていません.実装していくうえで,「この仕様はこういう解釈をしていたが,それは合っているか?」とか,「この理解だと,ここがこうなってしまうと思うが,その点は矛盾しないか?」など,常に自身で問いを立てることを意識しました.実装するうえで,ChatGPTにソースコードやドキュメントの参照元を提示するよう指示しましたが,関係のないソースを提示することが多く,実装には直接役立ちませんでした.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?