はじめに
普段は学生団体の運営などをしているランタンです。
セキュリティに関してはまだまだで勉強中ですが、ありがたいことにこの度セキュリティ・キャンプの脅威解析クラスに参加できることになりました。
そこで今回は、復習を兼ねてセキュリティ・キャンプの応募課題を晒してみようと思います。
文章は拙いところも多いかと思いますが、温かい目で見ていただけると嬉しいです...
内容に入る前に、この晒しについて
個人的なところは省いています。
この記事は「完璧な解答例」ではなく、自分なりに悩みながら取り組んだ過程と、その記録として読んでもらえたら嬉しいです。
課題
以下の問1から問7について、それぞれ回答してください。ただし問1~3はそれぞれ2000文字以内で回答してください。
なお、正解がある設問については、"正解していること"よりも"正解にたどり着くまでのプロセスや熱意"を重要視しています。答えにたどり着くまでの試行錯誤や自分なりの工夫等を書いて、精一杯アピールしてください。
問4
最近多くの組織がセキュリティ対策のためにEDR(Endpoint Detection and Response)を導入していますが、それを迂回する手法も研究されています。組織のセキュリティ担当として、その手法を分析した上で、どのような追加の対策を経営層に提案するべきか考えてください。
近年、多くの組織がサイバー攻撃対策の一環としてEDRを導入しています。EDRはエンドポイント上の不審な挙動を検知し、迅速なインシデント対応を可能にする重要なツールです。しかし、攻撃者もまた、EDRの検知を回避するための高度な手法を積極的に研究・活用しており、EDR単体では防ぎきれないリスクが存在することを認識する必要があります。
そもそもEDRが完全な可視化を提供できない理由として、以下のような技術的な制約があります。EDRの多くはユーザーモードで動作しており、カーネルやハードウェアレベルの不正な動作を直接監視できない、リアルタイムで全API呼び出しや全プロセス挙動を監視することは、性能への影響が大きく、常に精度とパフォーマンスのトレードオフを伴うこと、EDRの検知ロジック自体がブラックボックスであることが多く、逆に攻撃者にとっては観察対象となり、回避されやすい構造になっているなどがあります。
こうした構造的な限界を踏まえた上で、EDRを補完し、組織の防御力を多層的に強化する施策を検討する必要があります。提案する追加対策として1つ目にEDRのカーネルモード対応の見直しがあります。一部のEDRは、ユーザーモードに限らずカーネルレベルでの監視やシステムコールのフックに対応しています。導入済みEDRの機能を精査し、カーネルベースでの脅威検出機能があるかを確認するとともに、必要に応じて製品の見直しを検討すべきです。2つ目にメモリフォレンジックとEDRログの統合活用があります。プロセスインジェクションやファイルレス攻撃など、EDRが見逃しやすいメモリ上の活動に対しては、Volatilityなどのメモリ解析ツールを用いた定期的な監査が有効です。EDRの検知ログと組み合わせることで、検出困難な痕跡を補完できると考えます。3つ目にEDRバイパスの模倣攻撃による耐性評価などレッドチーム演習があります。EDRを含むセキュリティ体制全体の有効性を定期的に評価するため、EDR回避技術を用いた模擬攻撃を実施します。これにより、防御の抜け道や運用上の課題を具体的に洗い出し、現実に即した改善につなげることができます。4つ目に、EDR任せにしない多層防御の徹底が大切だと思います。EDRがカバーしきれない範囲を補完するため、以下のような多層的な防御の構成を提案します。アプリケーション制御による許可リスト運用やPowerShellなどスクリプト実行の監査強化とAMSI有効化、ネットワークセグメントの分離と、横展開防止策の徹底、EDR管理プロセスの自己防衛の設定確認があります。最後に、インシデント対応フローの実効性の見直しがあります。EDRのアラートから迅速に対処へつなげる体制整備も不可欠です。インシデント発生時の初動対応、封じ込め、影響範囲の特定、復旧の各フェーズについて、実際のEDRログを用いた訓練やシナリオベース演習を通じて実効性を高めることが求められます。
以上のように、EDRの限界を正しく理解し、それを補う仕組みと運用体制を構築することが、組織のセキュリティ成熟度を一段階高めることとなります。EDRは強力な防御ツールですが、それだけに依存するのではなく、EDRを機能させ続ける仕組みこそが今後のセキュリティ投資の焦点であると考えます。
組織に所属してEDRを触ったことがある訳では無いので、机上の空論で考えうることをまとめた気がします。
問5
以下の質問にそれぞれ500文字以内で答えてください
1.次のGitHubアカウント (https://github.com/clwomkv) から分かることを全て調べて簡潔にまとめてください (例: 他のアカウント, domain, ...)。
ユーザ名:clwomkv
Github作成日:March 17,2025
フォロワー:2人(のってぃ[notthei · malware]、_p0ty1103[p0ty1103])
6 contributions
パブリックリポジトリ:3つ
・pikax
TODO
・panda
概要:アーキテクチャに依存しない動的解析プラットフォーム
使用言語:C
・kitesield
概要:Linux上のx86-64 ELFバイナリ用のパッカー/プロテクター
使用言語:C
2.それぞれの情報をどのように調べたのかを簡潔に説明してください。
まず、GitHubのユーザープロフィールページから、ユーザー名やフォロワー数、フォロー数、公開リポジトリ数などの基本的な情報を確認しました。次に、フォローしているユーザーやフォロワーのプロフィールも確認し、そのユーザーがどのような人物や技術分野に関心を持っているかを把握するようにしました。
続いて、公開されている各リポジトリを確認し、まずはREADME.mdファイルに記載された内容を読み取り、そのリポジトリの概要や目的を把握しました。もしREADME.mdに十分な情報が記載されていなかった場合は、リポジトリ内のディレクトリ構成やコードファイルを直接確認し、実際にどのような機能が実装されているのか、どのような技術が用いられているのかを判断しました。
3.調査したときに、気をつけたこととその理由を述べてください。
見落としを防ぐために、まずはユーザーの基本情報を丁寧に確認することを意識しました。特にフォロー・フォロワーといった周辺の関係性を調べることで、そのユーザーの技術的関心やコミュニティとのつながりが推察できると考えたためです。
次に、公開されているリポジトリについては、まずREADME.mdを読み、そこから概要や意図を素早く把握するようにしました。README.mdが不十分な場合には、内容を誤解しないようにリポジトリ内のファイル構成や実装されているコードを丁寧に確認し、より正確な理解を得ることを心がけました。
このように、情報を正しく把握するために段階的かつ体系的に確認を行い、偏りや見落としがないように調査を進めました。
今思うと全然調査が足りてないですね...もっと調べましょう(自戒)
問6
ファイルをダウンロードし、以下のポイントについて記述してください。
https://drive.google.com/file/d/1QkYslq5IA7uVTtiiiH7PWXp1I6gvGZQD/view?usp=sharing
注意点: デバッグを行う場合はdata.txtを別ディレクトリにコピーしたほうがいいです。ファイルの中身はC3講義のタイトルと概要(英訳)をコピーして貼り付けているのみなので、忘れていた場合はサイトから持ってきて作り直して下さい。
6-1
data.txtを暗号化するために必要な作業について説明して下さい
プログラムを実行すると「WE ARE HACKER」というGUIが表示されるのみで、暗号化は実行されませんでした。Ghidraで解析を開始したもののmain関数が見つからなかったため、"data.txt"という文字列を起点に、その参照元をXrefs(相互参照)機能で追跡しました。その結果、CreateFileAやWriteFileといったファイル操作関数に行き着き、ここからさらに処理を遡って解析を進めました。ファイル操作関数を呼び出す上位の処理(番地 14000198a)を解析したところ、GetSystemTime関数でPCのシステム日付を取得し、特定の日付と比較するロジックが発見できました。比較対象の日付は「2025年8月11日」で、プログラムは実行環境のシステム日付がこの特定の日付と一致する場合にのみ、暗号化処理に進むように設計されていました。
以上の解析から、data.txtを暗号化するには、OSの日付と時刻の設定を手動に変更し、システム日付を「2025年8月11日」に設定した上で、対象プログラムを実行する必要があります。
特定の日付にしてうまく実行できたときは脳汁が溢れてました。
6-2
data.txtを暗号化する暗号関数・暗号フローについて説明して下さい
data.txtの暗号化処理には、Windowsの標準暗号化APIである「Cryptography API: Next Generation」が用いられており、その暗号アルゴリズムはAES-256-CBCです。Ghidraによる解析で判明したCNG APIの呼び出しフローは以下の通りです。
BCryptOpenAlgorithmProvider関数がL"AES"を引数に呼び出され、AES暗号を使用することが宣言されます。
BCryptSetProperty関数により、チェイニングモードとしてL"ChainingModeCBC"が設定されます。これにより、ブロック暗号の動作モードがCBC (Cipher Block Chaining)モードに確定します。
BCryptGenerateSymmetricKey関数が鍵を生成します。この関数の第6引数(鍵の元となるデータの長さ)に0x20(32バイト)が指定されています。鍵長が32バイト(256ビット)であることから、暗号アルゴリズムがAES-256であることが特定できます。鍵の元となるデータは、プログラム内部の別の関数(FUN_140001450)で動的に生成され、DAT_140007040に格納されます。
BCryptEncrypt関数が実際の暗号化を行います。この際、第5引数として初期化ベクトル (IV)が格納されたDAT_140007060へのポインタが渡されます。IVはCBCモードで必須の要素であり、16バイトのデータが使用されます。
以上のAPIフローから、暗号化はAES-256アルゴリズムとCBCモードを組み合わせた「AES-256-CBC」で実行されます。
6-3
data.txtを復号し、利用した復号プログラム・復号結果の記載ともし完全に復号できない場合、その理由について説明して下さい
ファイルの復号を試みましたが、完全な復号には至りませんでした。
復号に必要な要素の特定ではAES-256-CBCで暗号化されたデータを復号するには、「32バイトの暗号鍵」と「16バイトの初期化ベクトル (IV)」が必要です。
Ghidraの静的解析により、鍵を生成する関数 FUN_140001450 を特定しました。この関数は、プログラム内にハードコードされた2種類のデータ (DAT_140003000, DAT_140003080) を元に、XOR演算とビットシフトを組み合わせて32バイトの鍵を生成します。このロジックを以下のPythonプログラムで再現し、鍵データを導出しました。
def generate_key():
xor_key_seed = [0x3a, 0x7f, 0xc2, 0x9b]
encrypted_base_data = [
0xd6, 0x01, 0x00, 0x00, 0x37, 0x01, 0x00, 0x00, 0x0c, 0x01, 0x00, 0x00, 0xb9, 0x01, 0x00, 0x00,
0x3a, 0x01, 0x00, 0x00, 0x19, 0x01, 0x00, 0x00, 0x86, 0x01, 0x00, 0x00, 0x61, 0x01, 0x00, 0x00,
0xe2, 0x01, 0x00, 0x00, 0x55, 0x01, 0x00, 0x00, 0xca, 0x01, 0x00, 0x00, 0xf5, 0x01, 0x00, 0x00,
0x76, 0x01, 0x00, 0x00, 0x9d, 0x01, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, 0xbd, 0x01, 0x00, 0x00,
0x3e, 0x01, 0x00, 0x00, 0x05, 0x01, 0x00, 0x00, 0x9a, 0x01, 0x00, 0x00, 0x71, 0x01, 0x00, 0x00,
0xf2, 0x01, 0x00, 0x00, 0x51, 0x01, 0x00, 0x00, 0xce, 0x01, 0x00, 0x00, 0xf9, 0x01, 0x00, 0x00,
0x7a, 0x01, 0x00, 0x00, 0x99, 0x01, 0x00, 0x00, 0x06, 0x01, 0x00, 0x00, 0xa1, 0x01, 0x00, 0x00,
0x22, 0x01, 0x00, 0x00, 0x15, 0x01, 0x00, 0x00, 0x8a, 0x01, 0x00, 0x00, 0x79, 0x00, 0x00, 0x00
]
decoded_component = [byte ^ 0xaa for byte in xor_key_seed]
final_key = []
for i in range(32):
byte1 = decoded_component[i % 4]
byte2 = encrypted_base_data[i * 4]
result_byte = (byte1 ^ byte2) >> 1
final_key.append(result_byte)
return bytes(final_key)
aes_key = generate_key()
初期化ベクトル (IV) の特定では、IVはFUN_1400014f7関数で生成されます。この関数は、_time64で取得した現在時刻をsrandのシードとして用い、rand()を16回呼び出してIVを生成します。IVは実行ごとに変わるため、静的解析では特定できません。そこで、動的解析ツール「x64dbg」を使い、BCryptEncrypt呼び出し時のメモリをダンプしてIV (6B 32 16 ED 51 1E C0 EC 9F 94 7A A7 F0 57 50 0E) を取得しました。
復号結果と失敗理由の考察では、上記で特定した鍵とIVを使い、Pythonのpycryptodomeライブラリで復号を試みましたが、パディングエラー等が発生し、元の平文は得られませんでした。失敗の最も有力な原因は、鍵生成ロジックの解釈ミスにあると考えられます。鍵を生成する計算 result_byte = (byte1 ^ byte2) >> 1 に含まれる右ビットシフト (>> 1) は、最下位ビットの情報を失わせる不可逆な演算です。Ghidraのデコンパイル結果が必ずしも正確とは限らず、このビットシフトが本来のロジックと異なっている可能性があります。失われたビット情報を復元できないため、正しい鍵を導出できず、結果として復号に失敗したと結論付けました。その他、動的解析時に取得したIVのタイミングがずれていた可能性も考えられました。
復号できている人のものを見るとまだまだだなぁと実感しています...
問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 で動作すること
■アーカイブファイル配布URL
https://bit.ly/42akdJH
イメージファイル(questions.dd)のSHA256ハッシュ値:
87dd1e19f97d700a914bb4127d1432788e4fcaa0def61eaa5b2281f2f6ad8186
問7-1
イメージファイルquestions.ddに存在するinode番号14のファイルについて、次の機能を実現するスクリプト:
①ファイル名を出力する
②ファイルのデータが存在する物理アドレスの始点を出力する
③ファイルのデータを取り出し、イメージファイルと同じディレクトリに別のファイルとして保存する
※出力したファイルのハッシュ値が以下の値であること
fdf7c291905cf475f22a434f1f1c188a13496eff7429e6b15dd2887845c3558f
import struct
import hashlib
IMAGE_PATH = "questions.dd"
INODE_NUM = 14
SUPERBLOCK_OFFSET = 1024 # これより前はboot block
SUPERBLOCK_SIZE = 1024
# Ans
file_name = None
file_data_offset = None
extracted_sha256 = None
OUTPUT_FILE = None
# ブロックサイズを取得する
def read_block_size(f):
f.seek(SUPERBLOCK_OFFSET + 24)
log_block_size = struct.unpack("<I", f.read(4))[0]
block_size = 1024 << log_block_size
return block_size
# スーパーブロックを取得する
def read_superblock(f):
f.seek(SUPERBLOCK_OFFSET)
sb_data = f.read(SUPERBLOCK_SIZE) # スーパーブロック全体を読む(1024バイト)
# s_inodes_per_group (offset 40, 4 bytes)
s_inodes_per_group = struct.unpack_from("<I", sb_data, 40)[0]
# s_blocks_per_group (offset 32, 4 bytes)
s_blocks_per_group = struct.unpack_from("<I", sb_data, 32)[0]
# s_inode_size (offset 88, 2 bytes)
s_inode_size = struct.unpack_from("<H", sb_data, 88)[0]
# s_first_ino (offset 84, 4 bytes)
s_first_ino = struct.unpack_from("<I", sb_data, 84)[0]
# スーパーブロックの情報を辞書で返す
info = {
"inodes_per_group": s_inodes_per_group,
"blocks_per_group": s_blocks_per_group,
"inode_size": s_inode_size,
"first_inode": s_first_ino,
}
return info
# グループディスクリプタを取得する
# グループディスクリプタはスーパーブロックの次にある
def read_group_descriptor(f, block_size):
gd_offset = block_size
f.seek(gd_offset)
gd_data = f.read(32)
inode_table_block = struct.unpack_from("<I", gd_data, 8)[0]
return inode_table_block
# iノードの情報を取得する
def read_inode(f, inode_offset, inode_size, block_size):
global file_data_offset # 物理オフセットの答えを出力するためのグローバル変数
f.seek(inode_offset)
inode_data = f.read(inode_size)
i_mode = struct.unpack_from("<H", inode_data, 0)[0]
i_size = struct.unpack_from("<I", inode_data, 4)[0]
i_block_raw = inode_data[40:100]
# extents形式かどうか判定(ダイレクトブロック方式かextents方式か)
eh_magic = struct.unpack_from("<H", i_block_raw, 0)[0]
if eh_magic == 0xf30a: # extents形式のマジックナンバー
ee_start_lo = struct.unpack_from("<I", i_block_raw, 20)[0]
blocks = (ee_start_lo,)
else:
blocks = struct.unpack_from("<15I", inode_data, 40)
info = {
"mode": i_mode,
"size": i_size,
"blocks": blocks
}
# 物理オフセットを計算
file_data_offset = blocks[0] * block_size
return info
# ファイルのデータを抽出して保存する関数
def extract_and_save_file(f, block_size, blocks, file_size, output_path):
extracted_text = ""
remaining = file_size
# ブロック番号をループしてデータを読み込む
for block_num in blocks:
if block_num == 0 or remaining <= 0:
break
f.seek(block_num * block_size)
data = f.read(min(block_size, remaining))
text = data.decode("utf-8", errors="replace")
extracted_text += text
remaining -= len(data)
# ファイルをUTF-8で書き込み
with open(output_path, "wb") as out:
remaining = file_size
for block_num in blocks:
if block_num == 0 or remaining <= 0:
break
f.seek(block_num * block_size)
data = f.read(min(block_size, remaining))
out.write(data)
remaining -= len(data)
# SHA256ハッシュを計算する関数
def compute_sha256(path):
global extracted_sha256
with open(path, "rb") as f:
data = f.read()
extracted_sha256 = hashlib.sha256(data).hexdigest()
# file名探索
def read_directory_entries(f, inode_info, block_size):
entries = []
for block_num in inode_info['blocks']:
if block_num == 0:
continue
block_offset = block_num * block_size
f.seek(block_offset)
block_data = f.read(block_size)
offset = 0
while offset < block_size:
entry_data = block_data[offset:offset + 8]
if len(entry_data) < 8:
break
inode, rec_len, name_len, file_type = struct.unpack_from("<IHBb", entry_data)
if inode == 0 or rec_len == 0:
break # 不正 or 終端
name_data = block_data[offset + 8:offset + 8 + name_len]
try:
name = name_data.decode("utf-8")
except UnicodeDecodeError:
name = "<invalid>"
entries.append({
"inode": inode,
"rec_len": rec_len,
"name_len": name_len,
"file_type": file_type,
"name": name
})
print(f"Offset: {offset}, Inode: {inode}, Rec_len: {rec_len}, Name_len: {name_len}, File_type: {file_type}, Name: {name}")
offset += rec_len
return entries
# ディレクトリエントリを読み取る関数
def read_directory_entries(f, block_num, block_size):
entries = []
f.seek(block_num * block_size)
data = f.read(block_size)
offset = 0
while offset < block_size:
inode, rec_len, name_len, file_type = struct.unpack_from("<IHBb", data, offset)
if inode == 0:
# 無効なエントリ。ファイルシステムの終端か空領域の可能性あり
break
name = data[offset+8 : offset+8+name_len].decode('utf-8', errors='replace')
entries.append({
"inode": inode,
"rec_len": rec_len,
"name_len": name_len,
"file_type": file_type,
"name": name
})
offset += rec_len
if rec_len == 0:
break # 無効なエントリ
return entries
# iノード番号からファイル名を探索する関数
def find_path_by_inode(f, target_inode, current_inode, path_so_far, inode_table_block, inode_size, block_size, visited=None):
if visited is None:
visited = set()
if current_inode in visited:
return None
visited.add(current_inode)
inode_offset = inode_table_block * block_size + (current_inode - 1) * inode_size
inode_info = read_inode(f, inode_offset, inode_size, block_size)
for block in inode_info["blocks"]:
if block == 0:
continue
entries = read_directory_entries(f, block, block_size)
for entry in entries:
name = entry["name"]
inode = entry["inode"]
ftype = entry["file_type"]
if inode == 0:
continue
if inode == target_inode:
return name
if ftype == 2 and name not in (".", ".."):
subpath = find_path_by_inode(
f, target_inode, inode, f"{path_so_far}/{name}",
inode_table_block, inode_size, block_size, visited
)
if subpath:
return subpath
return None
# ===============================
# 実行用メイン関数
# ===============================
def main():
global file_name, extracted_sha256, file_data_offset
with open(IMAGE_PATH, "rb") as f:
block_size = read_block_size(f)
sb_info = read_superblock(f)
inode_table_block = read_group_descriptor(f, block_size)
inode_size = sb_info["inode_size"]
# iノード14番の情報を取得
inode_index = INODE_NUM - 1
inode_offset = inode_table_block * block_size + inode_index * inode_size
inode_info = read_inode(f, inode_offset, inode_size, block_size)
# ファイル名探索
path = find_path_by_inode(
f, INODE_NUM, 2, "", inode_table_block, inode_size, block_size
)
if path:
file_name = path
output_file = f"extracted_{file_name}"
else:
file_name = "<not found>"
output_file = "output_q1.txt"
extract_and_save_file(
f,
block_size,
inode_info["blocks"],
inode_info["size"],
output_file
)
compute_sha256(output_file)
# 結果を表示
print("=======================")
print(f"抽出したファイル名: {file_name}")
print("=======================")
print(f"ファイルデータ開始の物理オフセット: {file_data_offset} バイト")
print("=======================")
print(f"抽出したファイルのSHA256: {extracted_sha256}")
print("=======================")
if __name__ == "__main__":
main()
問7-2
イメージファイルquestions.ddに存在するinode番号16のファイルについて、次の機能を実現するスクリプト:
①ファイル名を出力する
②ファイルのデータが存在する物理アドレスの始点を出力する
③ファイルのデータを取り出し、イメージファイルと同じディレクトリに別のファイルとして保存する
※出力したファイルのハッシュ値が以下の値であること
9f964b89facdc3671569c812ba8b3fadd20f9dfcf1873b06fe672761392e7523
import struct
import hashlib
IMAGE_PATH = "questions.dd"
INODE_NUM = 16
SUPERBLOCK_OFFSET = 1024
SUPERBLOCK_SIZE = 1024
# Ans
file_name = None
file_data_offset = None
extracted_sha256 = None
OUTPUT_FILE = None
# ブロックサイズを取得する
def read_block_size(f):
f.seek(SUPERBLOCK_OFFSET + 24)
log_block_size = struct.unpack("<I", f.read(4))[0]
block_size = 1024 << log_block_size
return block_size
# スーパーブロックを取得する
def read_superblock(f):
f.seek(SUPERBLOCK_OFFSET)
sb_data = f.read(SUPERBLOCK_SIZE)
s_inodes_per_group = struct.unpack_from("<I", sb_data, 40)[0]
s_blocks_per_group = struct.unpack_from("<I", sb_data, 32)[0]
s_inode_size = struct.unpack_from("<H", sb_data, 88)[0]
s_first_ino = struct.unpack_from("<I", sb_data, 84)[0]
info = {
"inodes_per_group": s_inodes_per_group,
"blocks_per_group": s_blocks_per_group,
"inode_size": s_inode_size,
"first_inode": s_first_ino,
}
return info
# グループディスクリプタを取得する
def read_group_descriptor(f, block_size):
gd_offset = block_size
f.seek(gd_offset)
gd_data = f.read(32)
inode_table_block = struct.unpack_from("<I", gd_data, 8)[0]
return inode_table_block
# iノードを読み取る関数
def read_inode(f, inode_offset, inode_size, block_size):
global file_data_offset
f.seek(inode_offset)
inode_data = f.read(inode_size)
i_mode = struct.unpack_from("<H", inode_data, 0)[0]
i_size = struct.unpack_from("<I", inode_data, 4)[0]
i_block_raw = inode_data[40:100]
eh_magic = struct.unpack_from("<H", i_block_raw, 0)[0] # ext4ファイルシステムのマジックナンバー
blocks = []
if eh_magic == 0xf30a:
# ext4 extent形式
eh_entries = struct.unpack_from("<H", i_block_raw, 4)[0]
for i in range(eh_entries):
base = 12 + i * 12
if base + 12 > len(i_block_raw):
break
# extent構造体: ee_block, ee_len, ee_start_hi, ee_start_lo
ee_block, ee_len, ee_start_hi, ee_start_lo = struct.unpack_from("<IHHI", i_block_raw[base:base+12])
start_block = (ee_start_hi << 32) | ee_start_lo
for j in range(ee_len):
blocks.append(start_block + j)
else:
# 旧来の直接/間接ブロック
blocks = list(struct.unpack_from("<15I", inode_data, 40))
# 最初のデータブロックの物理オフセット
if blocks:
file_data_offset = blocks[0] * block_size
info = {
"mode": i_mode,
"size": i_size,
"blocks": blocks
}
return info
# ファイルを抽出して保存する関数
def extract_and_save_file(f, block_size, blocks, file_size, output_path):
remaining = file_size
with open(output_path, "wb") as out:
for block_num in blocks:
if block_num == 0 or remaining <= 0:
break
f.seek(block_num * block_size)
data = f.read(min(block_size, remaining))
out.write(data)
remaining -= len(data)
# SHA256を計算する関数
def compute_sha256(path):
global extracted_sha256
with open(path, "rb") as f:
data = f.read()
extracted_sha256 = hashlib.sha256(data).hexdigest()
# file名探索
def read_directory_entries(f, inode_info, block_size):
entries = []
# ディレクトリの直接ブロックのみ使用(簡易版)
for block_num in inode_info['blocks']:
if block_num == 0:
continue
block_offset = block_num * block_size
f.seek(block_offset)
block_data = f.read(block_size)
offset = 0
while offset < block_size:
# ディレクトリエントリのヘッダ部分を読み取る(8バイト)
entry_data = block_data[offset:offset + 8]
if len(entry_data) < 8:
break
inode, rec_len, name_len, file_type = struct.unpack_from("<IHBb", entry_data)
if inode == 0 or rec_len == 0:
break # 不正 or 終端
name_data = block_data[offset + 8:offset + 8 + name_len]
try:
name = name_data.decode("utf-8")
except UnicodeDecodeError:
name = "<invalid>"
entries.append({
"inode": inode,
"rec_len": rec_len,
"name_len": name_len,
"file_type": file_type,
"name": name
})
print(f"Offset: {offset}, Inode: {inode}, Rec_len: {rec_len}, Name_len: {name_len}, File_type: {file_type}, Name: {name}")
offset += rec_len
return entries
# ディレクトリエントリを読み取る関数
def read_directory_entries(f, block_num, block_size):
entries = []
f.seek(block_num * block_size)
data = f.read(block_size)
offset = 0
while offset < block_size:
inode, rec_len, name_len, file_type = struct.unpack_from("<IHBb", data, offset)
if inode == 0:
# 無効なエントリ。ファイルシステムの終端か空領域の可能性あり
break
name = data[offset+8 : offset+8+name_len].decode('utf-8', errors='replace')
entries.append({
"inode": inode,
"rec_len": rec_len,
"name_len": name_len,
"file_type": file_type,
"name": name
})
offset += rec_len
if rec_len == 0:
break # 無効なエントリ
return entries
# iノード番号からファイル名を探索する関数
def find_path_by_inode(f, target_inode, current_inode, path_so_far, inode_table_block, inode_size, block_size, visited=None):
if visited is None:
visited = set()
if current_inode in visited:
return None
visited.add(current_inode)
inode_offset = inode_table_block * block_size + (current_inode - 1) * inode_size
inode_info = read_inode(f, inode_offset, inode_size, block_size)
for block in inode_info["blocks"]:
if block == 0:
continue
entries = read_directory_entries(f, block, block_size)
for entry in entries:
name = entry["name"]
inode = entry["inode"]
ftype = entry["file_type"]
if inode == 0:
continue
if inode == target_inode:
return name
if ftype == 2 and name not in (".", ".."):
subpath = find_path_by_inode(
f, target_inode, inode, f"{path_so_far}/{name}",
inode_table_block, inode_size, block_size, visited
)
if subpath:
return subpath
return None
# ===============================
# 実行用メイン関数
# ===============================
def main():
global file_name, extracted_sha256, file_data_offset
with open(IMAGE_PATH, "rb") as f:
block_size = read_block_size(f)
sb_info = read_superblock(f)
inode_table_block = read_group_descriptor(f, block_size)
inode_size = sb_info["inode_size"]
# iノード14番の情報を取得
inode_index = INODE_NUM - 1
inode_offset = inode_table_block * block_size + inode_index * inode_size
inode_info = read_inode(f, inode_offset, inode_size, block_size)
# ファイル名探索
path = find_path_by_inode(
f, INODE_NUM, 2, "", inode_table_block, inode_size, block_size
)
if path:
file_name = path
output_file = f"extracted_{file_name}"
else:
file_name = "<not found>"
output_file = "output_q1.txt"
extract_and_save_file(
f,
block_size,
inode_info["blocks"],
inode_info["size"],
output_file
)
compute_sha256(output_file)
# 結果を表示
print("=======================")
print(f"抽出したファイル名: {file_name}")
print("=======================")
print(f"ファイルデータ開始の物理オフセット: {file_data_offset} バイト")
print("=======================")
print(f"抽出したファイルのSHA256: {extracted_sha256}")
print("=======================")
if __name__ == "__main__":
main()
問7-3
イメージファイルquestions.ddに存在するinode番号18のファイルについて、次の機能を実現するスクリプト:
①ファイル名を出力する
②ファイルのデータが存在する物理アドレスの始点を出力する
③ファイルのデータを取り出し、イメージファイルと同じディレクトリに別のファイルとして保存する
※出力したファイルのハッシュ値が以下の値であること
31055b7421279896ad6e0b8d2f6993ee219c2ba88d758917ac2f662e39dba32e
取り出しを失敗しているので参考にならないコードです...
import struct
import hashlib
IMAGE_PATH = "questions.dd"
INODE_NUM = 18
SUPERBLOCK_OFFSET = 1024
SUPERBLOCK_SIZE = 1024
# Ans
file_name = None
file_data_offset = None
extracted_sha256 = None
OUTPUT_FILE = None
def read_block_size(f):
f.seek(SUPERBLOCK_OFFSET + 24)
log_block_size = struct.unpack("<I", f.read(4))[0]
block_size = 1024 << log_block_size
return block_size
def read_superblock(f):
f.seek(SUPERBLOCK_OFFSET)
sb_data = f.read(SUPERBLOCK_SIZE)
s_inodes_per_group = struct.unpack_from("<I", sb_data, 40)[0]
s_blocks_per_group = struct.unpack_from("<I", sb_data, 32)[0]
s_inode_size = struct.unpack_from("<H", sb_data, 88)[0]
s_first_ino = struct.unpack_from("<I", sb_data, 84)[0]
info = {
"inodes_per_group": s_inodes_per_group,
"blocks_per_group": s_blocks_per_group,
"inode_size": s_inode_size,
"first_inode": s_first_ino,
}
return info
def read_group_descriptor(f, block_size):
gd_offset = block_size
f.seek(gd_offset)
gd_data = f.read(32)
inode_table_block = struct.unpack_from("<I", gd_data, 8)[0]
return inode_table_block
def parse_extents(f, block_size, extent_block_data):
if len(extent_block_data) < 12:
# ヘッダすらない場合は空リストを返す
return []
eh_magic = struct.unpack_from("<H", extent_block_data, 0)[0]
eh_entries = struct.unpack_from("<H", extent_block_data, 4)[0]
eh_depth = struct.unpack_from("<H", extent_block_data, 6)[0]
blocks = []
if eh_magic != 0xf30a:
return blocks
if eh_depth == 0:
# リーフノード(実データextent)
for i in range(eh_entries):
base = 12 + i * 12
if base + 12 > len(extent_block_data):
break
ee_block, ee_len, ee_start_hi, ee_start_lo = struct.unpack_from("<IHHI", extent_block_data[base:base+12])
start_block = (ee_start_hi << 32) | ee_start_lo
for j in range(ee_len):
blocks.append(start_block + j)
else:
# インデックスノード(子ノードをたどる)
for i in range(eh_entries):
base = 12 + i * 12
if base + 12 > len(extent_block_data):
break
ei_block, ei_leaf_lo, ei_leaf_hi, _ = struct.unpack_from("<IHIH", extent_block_data[base:base+12])
child_block = (ei_leaf_hi << 32) | ei_leaf_lo
f.seek(child_block * block_size)
child_data = f.read(block_size)
if len(child_data) == 0:
continue # 空ブロックはスキップ
blocks.extend(parse_extents(f, block_size, child_data))
return blocks
def read_inode(f, inode_offset, inode_size, block_size):
global file_data_offset
f.seek(inode_offset)
inode_data = f.read(inode_size)
i_mode = struct.unpack_from("<H", inode_data, 0)[0]
i_size = struct.unpack_from("<I", inode_data, 4)[0]
i_block_raw = inode_data[40:100]
eh_magic = struct.unpack_from("<H", i_block_raw, 0)[0]
blocks = []
if eh_magic == 0xf30a:
# extent tree対応
blocks = parse_extents(f, block_size, i_block_raw)
else:
blocks = list(struct.unpack_from("<15I", inode_data, 40))
# 最初のデータブロックの物理オフセット
if blocks:
file_data_offset = blocks[0] * block_size
info = {
"mode": i_mode,
"size": i_size,
"blocks": blocks
}
return info
def extract_and_save_file(f, block_size, blocks, file_size, output_path):
remaining = file_size
with open(output_path, "wb") as out:
for block_num in blocks:
if block_num == 0 or remaining <= 0:
break
f.seek(block_num * block_size)
data = f.read(min(block_size, remaining))
out.write(data)
remaining -= len(data)
def compute_sha256(path):
global extracted_sha256
with open(path, "rb") as f:
data = f.read()
extracted_sha256 = hashlib.sha256(data).hexdigest()
# file名探索
def read_directory_entries(f, inode_info, block_size):
entries = []
# ディレクトリの直接ブロックのみ使用(簡易版)
for block_num in inode_info['blocks']:
if block_num == 0:
continue
block_offset = block_num * block_size
f.seek(block_offset)
block_data = f.read(block_size)
offset = 0
while offset < block_size:
# ディレクトリエントリのヘッダ部分を読み取る(8バイト)
entry_data = block_data[offset:offset + 8]
if len(entry_data) < 8:
break
inode, rec_len, name_len, file_type = struct.unpack_from("<IHBb", entry_data)
if inode == 0 or rec_len == 0:
break # 不正 or 終端
name_data = block_data[offset + 8:offset + 8 + name_len]
try:
name = name_data.decode("utf-8")
except UnicodeDecodeError:
name = "<invalid>"
entries.append({
"inode": inode,
"rec_len": rec_len,
"name_len": name_len,
"file_type": file_type,
"name": name
})
print(f"Offset: {offset}, Inode: {inode}, Rec_len: {rec_len}, Name_len: {name_len}, File_type: {file_type}, Name: {name}")
offset += rec_len
return entries
# ディレクトリエントリを読み取る関数
def read_directory_entries(f, block_num, block_size):
entries = []
f.seek(block_num * block_size)
data = f.read(block_size)
offset = 0
while offset < block_size:
inode, rec_len, name_len, file_type = struct.unpack_from("<IHBb", data, offset)
if inode == 0:
# 無効なエントリ。ファイルシステムの終端か空領域の可能性あり
break
name = data[offset+8 : offset+8+name_len].decode('utf-8', errors='replace')
entries.append({
"inode": inode,
"rec_len": rec_len,
"name_len": name_len,
"file_type": file_type,
"name": name
})
offset += rec_len
if rec_len == 0:
break # 無効なエントリ
return entries
# iノード番号からファイル名を探索する関数
def find_path_by_inode(f, target_inode, current_inode, path_so_far, inode_table_block, inode_size, block_size, visited=None):
if visited is None:
visited = set()
if current_inode in visited:
return None
visited.add(current_inode)
inode_offset = inode_table_block * block_size + (current_inode - 1) * inode_size
inode_info = read_inode(f, inode_offset, inode_size, block_size)
for block in inode_info["blocks"]:
if block == 0:
continue
entries = read_directory_entries(f, block, block_size)
for entry in entries:
name = entry["name"]
inode = entry["inode"]
ftype = entry["file_type"]
if inode == 0:
continue
if inode == target_inode:
return name
if ftype == 2 and name not in (".", ".."):
subpath = find_path_by_inode(
f, target_inode, inode, f"{path_so_far}/{name}",
inode_table_block, inode_size, block_size, visited
)
if subpath:
return subpath
return None
# ===============================
# 実行用メイン関数
# ===============================
def main():
global file_name, extracted_sha256, file_data_offset
with open(IMAGE_PATH, "rb") as f:
block_size = read_block_size(f)
sb_info = read_superblock(f)
inode_table_block = read_group_descriptor(f, block_size)
inode_size = sb_info["inode_size"]
# iノード14番の情報を取得
inode_index = INODE_NUM - 1
inode_offset = inode_table_block * block_size + inode_index * inode_size
inode_info = read_inode(f, inode_offset, inode_size, block_size)
# ファイル名探索
path = find_path_by_inode(
f, INODE_NUM, 2, "", inode_table_block, inode_size, block_size
)
if path:
file_name = path
output_file = f"extracted_{file_name}"
else:
file_name = "<not found>"
output_file = "output_q1.txt"
extract_and_save_file(
f,
block_size,
inode_info["blocks"],
inode_info["size"],
output_file
)
compute_sha256(output_file)
# 結果を表示
print("=======================")
print(f"抽出したファイル名: {file_name}")
print("=======================")
print(f"ファイルデータ開始の物理オフセット: {file_data_offset} バイト")
print("=======================")
print(f"抽出したファイルのSHA256: {extracted_sha256}")
print("=======================")
if __name__ == "__main__":
main()
問7-4
問7-2について、問7-1のファイルと異なる点や機能を実現する上で工夫した点を400字以内で記載してください。
問7-2の実装では、問7-1と比較してより汎用的なファイル抽出が可能となるよう工夫しました。具体的には、read_inode関数においてext4のextents形式(eh_magic == 0xf30a)に対応し、ファイルが複数のブロックにまたがっている場合にも正しく読み出せるようにしています。各extentについて、eh_entriesの数だけループ処理を行い、ee_len分のブロック番号をリストに追加することで、全ブロックのデータを取得可能にしました。一方、問7-1では、extents形式であってもee_start_loからの1ブロックしか対応しておらず、長いファイルには対応できませんでした。これらの改良により、問7-2は実際のext4ファイルシステムの構造に近い形式の解析が可能となっています。
問7-5
問7-3について、問7-1や問7-2のファイルと異なる点や機能を実現する上で工夫した点を400字以内で記載してください。
問7-3では、問7-1や問7-2と異なり、対象ファイルが非常に大きく、複数の間接ブロックやextentsが絡む複雑な構造となっていました。そのため、従来の直接ブロックや単純なextents処理では全データを正しく抽出できませんでした。特に、ext4の多段間接参照や巨大ファイルのブロック管理に対応する必要がありましたが、既存コードは直接・一次間接ブロックや単純なextentsしか考慮していませんでした。このため、抽出結果が不完全となり、正しくファイルを出力できませんでした。
問7-6
スクリプトの実装に際して生成AIや類似システムを使った場合、どのような工夫をして利用したのか400字以内で記載してください(全く利用していない場合はその理由を記載)。
スクリプトの作成にあたっては、生成AIを積極的に活用しました。ext4ファイルシステムに関する知識がほとんどない状態からの挑戦だったため、一から調べるだけでは理解が追いつかない場面も多く、AIを先生のような存在として活用しました。具体的には、わからない概念や構造、データの意味について都度質問しながら、リファレンスと併用して知識を深めていきました。単なるコードの生成に頼るのではなく、自分の理解を補強するための対話的なツールとしてAIを用いることで、着実に理解を積み重ねながらスクリプトを完成させることができました。
まとめ
今見ても圧倒的に技術不足だなと感じますね。
技術<<<<やる気で選んでもらえた感じが高いのでキャンプまでに技術をどうにかしたい所存です。
ぜひ来年度以降の参考になればと思います。