performing-sqlite-database-forensics について
ブラウザーやメッセージアプリなどのSQLiteデータベースを調べるデジタルフォレンジック用スキルです。SQLクエリに加え、B-treeページ、フリーリスト、WAL、未割り当て領域から削除データの痕跡を探します。
主な機能
- 01SQLiteのB-treeページ構造を調査
- 02フリーリスト内の削除データを分析
- 03WALトランザクションファイルを確認
- 04ページの未割り当て領域を検索
- 05アプリのタイムスタンプをデコード
活用シーン
- 01削除レコードの残存データを回収
- 02ブラウザーやメッセージのDBを調査
- 03WALデータとDB内容を照合
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mukul975/ anthropic-cybersecurity-skills --skill performing-sqlite-database-forensicsインストール後、Claude Code で/performing-sqlite-database-forensicsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
LICENSEassets/template.mdreferences/api-reference.mdstandards.mdworkflows.mdscripts/agent.pyprocess.py詳細
SQLite データベースのフォレンジック調査
概要
SQLite は世界で最も広く導入されているデータベースエンジンであり、ほぼすべてのモバイルアプリケーション、Web ブラウザー、多くのデスクトップアプリケーションでユーザーデータの保存に使用されています。デジタルフォレンジックにおいて、SQLite データベースは重要な証拠ソースです。ブラウザーの履歴、メッセージ記録、通話履歴、GPS の位置情報、アプリケーションの設定、キャッシュされたコンテンツなどが含まれています。フォレンジック分析では、単純な SQL クエリにとどまらず、内部の B ツリーページ構造、削除されたレコードを含むフリーリストページ、トランザクション履歴を保持する先行書き込みログ(WAL)ファイル、さらに削除後も復元可能なデータが残存している可能性のあるデータベースページ内の未割り当て領域を調査します。
使用する場面
- SQLite データベースのフォレンジック調査を伴うセキュリティ評価を実施する場合
- 関連するセキュリティイベントのインシデント対応手順に従う場合
- 定期的なセキュリティテストまたは監査活動を実施する場合
- 実地テストを通じてセキュリティ制御を検証する場合
前提条件
- DB Browser for SQLite(sqlitebrowser)
- SQLite コマンドラインツール(sqlite3)
- sqlite3 モジュールを含む Python 3.8 以降
- Belkasoft Evidence Center または Axiom(商用)
- 手動でページを調査するための 16 進エディター(HxD、010 Editor)
- B ツリーのデータ構造に関する理解
SQLite の内部構造
データベースヘッダー(先頭 100 バイト)
| オフセット | サイズ | 説明 |
|---|---|---|
| 0 | 16 | マジック文字列:"SQLite format 3\000" |
| 16 | 2 | ページサイズ(512~65536 バイト) |
| 18 | 1 | ファイル形式の書き込みバージョン |
| 19 | 1 | ファイル形式の読み取りバージョン |
| 24 | 4 | ファイル変更カウンター |
| 28 | 4 | ページ単位のデータベースサイズ |
| 32 | 4 | 最初のフリーリスト幹ページ番号 |
| 36 | 4 | フリーリストページの合計数 |
| 52 | 4 | テキストエンコーディング(1=UTF-8、2=UTF-16le、3=UTF-16be) |
| 96 | 4 | バージョン有効性番号 |
ページタイプ
| タイプ | ID | 説明 |
|---|---|---|
| B ツリー内部 | 0x05 | 内部テーブルノード |
| B ツリーリーフ | 0x0D | 実際のレコードを含むテーブルのリーフページ |
| インデックス内部 | 0x02 | 内部インデックスノード |
| インデックスリーフ | 0x0A | インデックスのリーフページ |
| フリーリスト幹 | - | 解放されたページを追跡 |
| フリーリストリーフ | - | 復元可能なデータを含む解放済みページ |
| オーバーフロー | - | 大きなレコードの継続部分 |
削除されたレコードの復元
方法 1:フリーリストページの分析
レコードが削除されると、SQLite はそれらのページをすぐに上書きせず、フリーリストに配置することがあります。
import struct
import sqlite3
import os
def analyze_freelist(db_path: str) -> dict:
"""Analyze SQLite freelist to identify pages containing deleted data."""
with open(db_path, "rb") as f:
# Read header
header = f.read(100)
page_size = struct.unpack(">H", header[16:18])[0]
if page_size == 1:
page_size = 65536
first_freelist_page = struct.unpack(">I", header[32:36])[0]
total_freelist_pages = struct.unpack(">I", header[36:40])[0]
freelist_info = {
"page_size": page_size,
"first_freelist_page": first_freelist_page,
"total_freelist_pages": total_freelist_pages,
"trunk_pages": [],
"leaf_pages": []
}
if first_freelist_page == 0:
return freelist_info
# Walk the freelist trunk chain
trunk_page = first_freelist_page
while trunk_page != 0:
offset = (trunk_page - 1) * page_size
f.seek(offset)
page_data = f.read(page_size)
next_trunk = struct.unpack(">I", page_data[0:4])[0]
leaf_count = struct.unpack(">I", page_data[4:8])[0]
leaves = []
for i in range(leaf_count):
leaf_page = struct.unpack(">I", page_data[8 + i * 4:12 + i * 4])[0]
leaves.append(leaf_page)
freelist_info["trunk_pages"].append({
"page_number": trunk_page,
"next_trunk": next_trunk,
"leaf_count": leaf_count,
"leaf_pages": leaves
})
freelist_info["leaf_pages"].extend(leaves)
trunk_page = next_trunk
return freelist_info
def extract_freelist_content(db_path: str, output_dir: str):
"""Extract raw content from freelist pages for analysis."""
info = analyze_freelist(db_path)
os.makedirs(output_dir, exist_ok=True)
with open(db_path, "rb") as f:
page_size = info["page_size"]
for page_num in info["leaf_pages"]:
offset = (page_num - 1) * page_size
f.seek(offset)
page_data = f.read(page_size)
output_file = os.path.join(output_dir, f"freelist_page_{page_num}.bin")
with open(output_file, "wb") as out:
out.write(page_data)
return len(info["leaf_pages"])
方法 2:WAL(先行書き込みログ)の分析
WAL ファイルには、メインデータベースにまだチェックポイントされていない保留中のトランザクションが含まれています。
def parse_wal_header(wal_path: str) -> dict:
"""Parse SQLite WAL file header and frame inventory."""
with open(wal_path, "rb") as f:
header = f.read(32)
magic = struct.unpack(">I", header[0:4])[0]
file_format = struct.unpack(">I", header[4:8])[0]
page_size = struct.unpack(">I", header[8:12])[0]
checkpoint_seq = struct.unpack(">I", header[12:16])[0]
salt1 = struct.unpack(">I", header[16:20])[0]
salt2 = struct.unpack(">I", header[20:24])[0]
wal_info = {
"magic": hex(magic),
"format": file_format,
"page_size": page_size,
"checkpoint_sequence": checkpoint_seq,
"frames": []
}
# Parse frames (24-byte header + page_size data each)
frame_offset = 32
frame_num = 0
file_size = os.path.getsize(wal_path)
while frame_offset + 24 + page_size <= file_size:
f.seek(frame_offset)
frame_header = f.read(24)
page_number = struct.unpack(">I", frame_header[0:4])[0]
db_size_after = struct.unpack(">I", frame_header[4:8])[0]
wal_info["frames"].append({
"frame_number": frame_num,
"page_number": page_number,
"db_size_pages": db_size_after,
"offset": frame_offset
})
frame_offset += 24 + page_size
frame_num += 1
return wal_info
方法 3:ページ内の未割り当て領域
アクティブな B ツリーページ内でセルが削除されると、セルポインター配列とセルコンテンツ領域の間にある未割り当て領域にデータが残ります。
def analyze_unallocated_space(db_path: str, page_number: int) -> dict:
"""Analyze unallocated space within a specific B-tree page."""
with open(db_path, "rb") as f:
header = f.read(100)
page_size = struct.unpack(">H", header[16:18])[0]
if page_size == 1:
page_size = 65536
offset = (page_number - 1) * page_size
f.seek(offset)
page_data = f.read(page_size)
# Parse page header (8 or 12 bytes depending on type)
page_type = page_data[0]
first_freeblock = struct.unpack(">H", page_data[1:3])[0]
cell_count = struct.unpack(">H", page_data[3:5])[0]
cell_content_offset = struct.unpack(">H", page_data[5:7])[0]
if cell_content_offset == 0:
cell_content_offset = 65536
header_size = 12 if page_type in (0x02, 0x05) else 8
cell_pointer_end = header_size + cell_count * 2
unallocated_start = cell_pointer_end
unallocated_end = cell_content_offset
unallocated_size = unallocated_end - unallocated_start
return {
"page_number": page_number,
"page_type": hex(page_type),
"cell_count": cell_count,
"unallocated_start": unallocated_start,
"unallocated_end": unallocated_end,
"unallocated_size": unallocated_size,
"unallocated_data": page_data[unallocated_start:unallocated_end].hex()
}
一般的なフォレンジック対象データベース
| アプリケーション | データベースファイル | 主なテーブル |
|---|---|---|
| Chrome | History | urls, visits, downloads, keyword_search_terms |
| Firefox | places.sqlite | moz_places, moz_historyvisits |
| Safari | History.db | history_items, history_visits |
| msgstore.db | messages, chat_list | |
| Signal | signal.sqlite | sms, mms |
| iMessage | sms.db | message, handle, chat |
| Android SMS | mmssms.db | sms, mms, threads |
| Skype | main.db | Messages, Conversations |
タイムスタンプのデコード
from datetime import datetime, timedelta
def decode_chrome_timestamp(chrome_ts: int) -> datetime:
"""Convert Chrome/WebKit timestamp to datetime (microseconds since 1601-01-01)."""
epoch_delta = 11644473600
return datetime.utcfromtimestamp((chrome_ts / 1000000) - epoch_delta)
def decode_unix_timestamp(unix_ts: int) -> datetime:
"""Convert Unix timestamp to datetime."""
return datetime.utcfromtimestamp(unix_ts)
def decode_mac_absolute_time(mac_ts: float) -> datetime:
"""Convert Mac Absolute Time (seconds since 2001-01-01)."""
mac_epoch = datetime(2001, 1, 1)
return mac_epoch + timedelta(seconds=mac_ts)
def decode_mozilla_timestamp(moz_ts: int) -> datetime:
"""Convert Mozilla PRTime (microseconds since Unix epoch)."""
return datetime.utcfromtimestamp(moz_ts / 1000000)
参考資料
- SQLite ファイル形式:https://www.sqlite.org/fileformat2.html
- Belkasoft の SQLite 分析:https://belkasoft.com/sqlite-analysis
- Spyder Forensics の SQLite トレーニング:https://www.spyderforensics.com/sqlite-forensic-fundamentals-2025/
- 破損した SQLite データベースのフォレンジック分析:https://www.forensicfocus.com/articles/forensic-analysis-of-damaged-sqlite-databases/
出力例
$ python3 sqlite_forensics.py --db /evidence/chrome/Default/History \
--wal /evidence/chrome/Default/History-wal \
--journal /evidence/chrome/Default/History-journal \
--output /analysis/sqlite_report
SQLite Database Forensic Analyzer v2.0
========================================
Database: /evidence/chrome/Default/History
Size: 48.2 MB
SQLite Ver: 3.39.5
Page Size: 4096 bytes
Total Pages: 12,345
Encoding: UTF-8
[+] Analyzing WAL (Write-Ahead Log)...
WAL file: History-wal (2.1 MB)
WAL frames: 512
Checkpointed: No (contains uncommitted data)
Recoverable rows from WAL: 234
[+] Analyzing journal file...
Journal file: History-journal (0 bytes - rolled back)
[+] Scanning for deleted records (freelist pages)...
Freelist pages: 456
Deleted records recovered: 1,892
[+] Analyzing table: urls
Active rows: 12,456
Deleted rows: 1,234 (recovered from freelist)
WAL-only rows: 89
--- Recovered Deleted URLs (Last 10) ---
Row ID | URL | Title | Visit Count | Last Visit (UTC)
-------|--------------------------------------------------|--------------------------|-------------|---------------------
89234 | https://mega.nz/folder/xYz123#key=AbCdEf | MEGA | 5 | 2024-01-16 03:20:00
89235 | https://transfer.sh/abc123/data.7z | transfer.sh | 1 | 2024-01-16 03:25:00
89240 | https://temp-mail.org/en/ | Temp Mail | 3 | 2024-01-15 13:00:00
89241 | https://browserleaks.com/ip | IP Leak Test | 1 | 2024-01-15 12:55:00
89245 | https://www.virustotal.com/gui/file/a1b2c3... | VirusTotal | 2 | 2024-01-15 14:30:00
89250 | https://github.com/gentilkiwi/mimikatz/releases | Mimikatz Releases | 1 | 2024-01-15 16:00:00
89260 | https://raw.githubusercontent.com/.../payload.ps1| GitHub Raw | 1 | 2024-01-15 14:34:00
89270 | https://pastebin.com/edit/kL9mN2pQ | Pastebin - Edit | 2 | 2024-01-15 14:42:00
89280 | https://duckduckgo.com/?q=clear+browser+history | DuckDuckGo | 1 | 2024-01-17 22:00:00
89285 | https://duckduckgo.com/?q=anti+forensics+tools | DuckDuckGo | 1 | 2024-01-17 22:05:00
[+] Analyzing table: downloads
Active rows: 234
Deleted rows: 12 (recovered)
--- Recovered Deleted Downloads ---
Row ID | Filename | URL | Size | Start Time (UTC)
-------|------------------------|----------------------------------------|-----------|---------------------
5012 | payload.ps1 | https://raw.githubusercontent.com/... | 4,096 | 2024-01-15 14:34:00
5015 | mimikatz_trunk.zip | https://github.com/.../releases/... | 1,892,352 | 2024-01-15 16:00:00
5018 | netscan_portable.zip | https://www.softperfect.com/... | 5,242,880 | 2024-01-15 15:05:00
[+] Slack space analysis...
Pages with slack space data: 234
Partial strings recovered: 67 fragments
Summary:
Total records analyzed: 14,578 (active) + 3,126 (deleted/WAL)
Evidence-relevant URLs: 23 (flagged)
Deleted downloads: 12 (3 tool-related)
Anti-forensics evidence: Browser history deletion detected
Report: /analysis/sqlite_report/sqlite_forensics.json
Recovered DB: /analysis/sqlite_report/History_recovered.db