performing-sqlite-database-forensics
Perform forensic analysis of SQLite databases to recover deleted records from freelists and WAL files, decode encoded timestamps, and extract evidence from browser history, messaging apps, and mobile device databases.
What this skill does
# Performing SQLite Database Forensics
## Overview
SQLite is the most widely deployed database engine in the world, used by virtually every mobile application, web browser, and many desktop applications to store user data. In digital forensics, SQLite databases are critical evidence sources containing browser history, messaging records, call logs, GPS locations, application preferences, and cached content. Forensic analysis goes beyond simple SQL queries to examine the internal B-tree page structures, freelist pages containing deleted records, Write-Ahead Log (WAL) files preserving transaction history, and unallocated space within database pages where recoverable data may persist after deletion.
## When to Use
- When conducting security assessments that involve performing sqlite database forensics
- When following incident response procedures for related security events
- When performing scheduled security testing or auditing activities
- When validating security controls through hands-on testing
## Prerequisites
- DB Browser for SQLite (sqlitebrowser)
- SQLite command-line tools (sqlite3)
- Python 3.8+ with sqlite3 module
- Belkasoft Evidence Center or Axiom (commercial)
- Hex editor (HxD, 010 Editor) for manual page inspection
- Understanding of B-tree data structures
## SQLite Internal Structure
### Database Header (First 100 Bytes)
| Offset | Size | Description |
|--------|------|-------------|
| 0 | 16 | Magic string: "SQLite format 3\000" |
| 16 | 2 | Page size (512-65536 bytes) |
| 18 | 1 | File format write version |
| 19 | 1 | File format read version |
| 24 | 4 | File change counter |
| 28 | 4 | Database size in pages |
| 32 | 4 | First freelist trunk page number |
| 36 | 4 | Total freelist pages |
| 52 | 4 | Text encoding (1=UTF-8, 2=UTF-16le, 3=UTF-16be) |
| 96 | 4 | Version-valid-for number |
### Page Types
| Type | ID | Description |
|------|----|-------------|
| B-tree Interior | 0x05 | Internal table node |
| B-tree Leaf | 0x0D | Table leaf page containing actual records |
| Index Interior | 0x02 | Internal index node |
| Index Leaf | 0x0A | Index leaf page |
| Freelist Trunk | - | Tracks freed pages |
| Freelist Leaf | - | Freed page with recoverable data |
| Overflow | - | Continuation of large records |
## Deleted Record Recovery
### Method 1: Freelist Page Analysis
When records are deleted, SQLite may place their pages on the freelist rather than overwriting them immediately.
```python
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"])
```
### Method 2: WAL (Write-Ahead Log) Analysis
The WAL file contains pending transactions that have not yet been checkpointed back to the main database.
```python
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
```
### Method 3: Unallocated Space Within Pages
Deleted cells within active B-tree pages leave data in the unallocated region between the cell pointer array and the cell content area.
```python
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": unalRelated in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.