Working with SQLite
- Working with SQLite
SQLite is the most widely deployed database engine in existence. Unlike client-server databases, it stores the entire database in a single cross-platform file. This makes it ideal for embedded use — and it is embedded everywhere: every browser, every smartphone, most desktop applications, and a large fraction of operating system subsystems all use SQLite to store structured data. For forensic investigators and network analysts, fluency with SQLite is essential: browser history, cookies, saved credentials, chat logs, call records, and network tool output are all routinely stored in SQLite databases.
Installation
SQLite’s command-line shell (sqlite3) is available on all major platforms.
# Debian / Ubuntu / Kali
sudo apt install sqlite3
# macOS (already installed; or via Homebrew)
brew install sqlite
# Windows (via winget)
winget install SQLite.SQLite
# or download the precompiled shell from https://www.sqlite.org/download.html
Verify:
sqlite3 --version
# 3.45.1 2024-01-30 ...
The sqlite3 Shell
Open a database file (created if it does not exist):
sqlite3 path/to/database.db
For forensic work, open read-only to avoid accidental modification:
sqlite3 -readonly path/to/database.db
Essential dot-commands
Dot-commands are shell directives, not SQL. They are not terminated with a semicolon.
| Command | Effect |
|---|---|
.help |
List all dot-commands |
.tables |
List tables in the database |
.schema [table] |
Show CREATE statements |
.headers on |
Show column names above query results |
.mode column |
Fixed-width column output |
.mode csv |
CSV output |
.mode table |
Markdown-style table output |
.output file.csv |
Redirect output to a file |
.output stdout |
Return output to the terminal |
.once file.csv |
Redirect next query only |
.read script.sql |
Execute SQL from a file |
.quit |
Exit the shell |
Quick inspection workflow
sqlite3 -readonly unknown.db
.headers on
.mode column
.tables
.schema
SELECT * FROM some_table LIMIT 5;
.quit
Schema Inspection
sqlite_master / sqlite_schema
Every SQLite database has a built-in catalog table that lists all objects:
SELECT type, name, tbl_name, sql
FROM sqlite_master
WHERE type IN ('table', 'index', 'view', 'trigger')
ORDER BY type, name;
sqlite_schema is an alias for sqlite_master introduced in SQLite 3.33.0.
PRAGMA statements
PRAGMA commands query or set internal database properties.
-- List all columns in a table
PRAGMA table_info(urls);
-- List indexes on a table
PRAGMA index_list(urls);
-- List all databases attached to this connection
PRAGMA database_list;
-- Check database integrity
PRAGMA integrity_check;
-- Show journal mode (delete, wal, memory, off)
PRAGMA journal_mode;
-- Show page size and page count
PRAGMA page_size;
PRAGMA page_count;
PRAGMA table_info(t) returns one row per column: cid, name, type, notnull, dflt_value, pk.
Core SQL
SQLite supports standard SQL with minor differences from PostgreSQL/MySQL.
Selecting and filtering
SELECT url, title, last_visit_time
FROM urls
WHERE url LIKE '%example.com%'
ORDER BY last_visit_time DESC
LIMIT 20;
Aggregation
-- Count visits per domain
SELECT
rtrim(ltrim(url, 'https://'), '/') AS domain,
COUNT(*) AS visits
FROM urls
GROUP BY domain
ORDER BY visits DESC
LIMIT 10;
Joins
-- Chrome: combine urls and visits tables
SELECT
u.url,
u.title,
v.visit_time,
v.visit_duration
FROM visits v
JOIN urls u ON v.url = u.id
ORDER BY v.visit_time DESC
LIMIT 25;
Subqueries and CTEs
Common Table Expressions (CTEs) make complex queries readable:
WITH top_sites AS (
SELECT url, COUNT(*) AS n
FROM visits v JOIN urls u ON v.url = u.id
GROUP BY url
ORDER BY n DESC
LIMIT 20
)
SELECT * FROM top_sites;
Useful built-in functions
| Function | Purpose |
|---|---|
datetime(t, 'unixepoch') |
Convert Unix timestamp to datetime string |
strftime('%Y-%m-%d', t, 'unixepoch') |
Format a timestamp |
hex(blob_col) |
Render a BLOB as hex |
length(col) |
String or BLOB length |
COALESCE(a, b) |
First non-null value |
CAST(col AS TEXT) |
Type conversion |
LIKE, GLOB |
Pattern matching (GLOB is case-sensitive) |
Timestamps
SQLite has no native date type. Applications store timestamps as integers, reals, or strings — the epoch and unit vary per application.
| Application | Epoch | Unit |
|---|---|---|
| Unix (standard) | 1970-01-01 | seconds |
| Chrome / Chromium | 1601-01-01 | microseconds |
| Firefox | 1970-01-01 | microseconds |
| macOS Core Data | 2001-01-01 | seconds |
| WebKit | 2001-01-01 | seconds |
Converting Chrome timestamps
Chrome stores timestamps as microseconds since 1601-01-01 (the Windows FILETIME epoch):
-- Subtract the offset between 1601 and 1970 (11644473600 seconds)
SELECT
url,
title,
datetime(last_visit_time / 1000000 - 11644473600, 'unixepoch', 'localtime') AS visited
FROM urls
ORDER BY last_visit_time DESC
LIMIT 20;
Converting Firefox timestamps
Firefox stores timestamps as microseconds since the Unix epoch:
SELECT
p.url,
p.title,
datetime(h.visit_date / 1000000, 'unixepoch', 'localtime') AS visited
FROM moz_historyvisits h
JOIN moz_places p ON h.place_id = p.id
ORDER BY h.visit_date DESC
LIMIT 20;
Forensic Artifacts
Browser databases
Browsers lock their databases while running. Copy the file out of the profile directory before opening it, or use a live imaging tool.
Chrome / Chromium
Default profile locations:
| OS | Path |
|---|---|
| Linux | ~/.config/google-chrome/Default/ |
| macOS | ~/Library/Application Support/Google/Chrome/Default/ |
| Windows | %LOCALAPPDATA%\Google\Chrome\User Data\Default\ |
Key databases and tables:
| File | Key tables | Contents |
|---|---|---|
History |
urls, visits, downloads |
Browsing history, download records |
Cookies |
cookies |
Cookie name, value, host, expiry, creation time |
Login Data |
logins |
Saved usernames and encrypted passwords |
Web Data |
autofill, credit_cards |
Autofill entries and encrypted card data |
Favicons |
favicons, icon_mapping |
Site icons (useful for confirming visits) |
Worked example — recent downloads:
sqlite3 -readonly History
.headers on
.mode column
SELECT
datetime(start_time / 1000000 - 11644473600, 'unixepoch', 'localtime') AS started,
tab_url,
target_path,
received_bytes,
total_bytes,
state -- 0=in progress, 1=complete, 2=cancelled, 4=interrupted
FROM downloads
ORDER BY start_time DESC
LIMIT 20;
Firefox
Default profile locations:
| OS | Path |
|---|---|
| Linux | ~/.mozilla/firefox/<profile>/ |
| macOS | ~/Library/Application Support/Firefox/Profiles/<profile>/ |
| Windows | %APPDATA%\Mozilla\Firefox\Profiles\<profile>\ |
Key databases and tables:
| File | Key tables | Contents |
|---|---|---|
places.sqlite |
moz_places, moz_historyvisits, moz_bookmarks |
History, bookmarks, annotations |
cookies.sqlite |
moz_cookies |
Cookie store |
formhistory.sqlite |
moz_formhistory |
Form autofill values |
favicons.sqlite |
moz_icons, moz_pages_w_icons |
Site icons |
Worked example — bookmark tree:
sqlite3 -readonly places.sqlite
.headers on
.mode column
SELECT
b.title,
p.url,
datetime(b.dateAdded / 1000000, 'unixepoch', 'localtime') AS added,
datetime(b.lastModified / 1000000, 'unixepoch', 'localtime') AS modified
FROM moz_bookmarks b
JOIN moz_places p ON b.fk = p.id
WHERE b.type = 1 -- 1=bookmark, 2=folder, 3=separator
ORDER BY b.dateAdded DESC;
macOS artifacts
| Path | Contents |
|---|---|
/private/var/db/com.apple.xpc.roleaccountd.staging/ |
XPC service registration |
/Library/Application Support/com.apple.TCC/TCC.db |
Privacy/TCC permission grants (which apps can access camera, contacts, etc.) |
~/Library/Application Support/com.apple.TCC/TCC.db |
Per-user TCC grants |
/private/var/folders/.../com.apple.LaunchServices-*.csstore |
Launch Services database |
Worked example — TCC permission grants:
sqlite3 -readonly /Library/Application\ Support/com.apple.TCC/TCC.db
.headers on
.mode column
SELECT
client,
service,
CASE auth_value
WHEN 0 THEN 'denied'
WHEN 1 THEN 'unknown'
WHEN 2 THEN 'allowed'
WHEN 3 THEN 'limited'
ELSE CAST(auth_value AS TEXT)
END AS permission,
datetime(last_modified, 'unixepoch', 'localtime') AS modified
FROM access
ORDER BY service, client;
iOS / iPadOS (via backup or extraction)
iTunes/Finder backups and forensic extractions expose many SQLite databases:
| Database | Contents |
|---|---|
sms.db |
SMS and iMessage content, attachments |
AddressBook.sqlitedb |
Contacts |
call_history.db |
Call logs |
consolidated.db / CellLocation.sqlite |
Location history (older iOS) |
PhotoData/Photos.sqlite |
Photo metadata, albums, locations |
Android
| Path | Contents |
|---|---|
/data/data/com.android.providers.contacts/databases/contacts2.db |
Contacts |
/data/data/com.android.providers.telephony/databases/mmssms.db |
SMS/MMS |
/data/data/com.android.providers.calendar/databases/calendar.db |
Calendar |
/data/data/<app_package>/databases/*.db |
Per-application databases |
Root or a forensic extraction tool (Cellebrite, AXIOM, etc.) is required to read most of these from a live device.
WAL Mode and Forensic Implications
SQLite’s default journal mode is rollback journal (producing a -journal file). Many modern applications — including Chrome, Firefox, and most iOS databases — explicitly configure Write-Ahead Log (WAL) mode instead, which produces -wal and -shm companion files.
In WAL mode:
- Writes go to the
.walfile first; reads may come from either the main database or the WAL. - The WAL is periodically checkpointed (merged back into the main file) and then reset.
- Forensically: the
.walfile may contain records not yet checkpointed — always collect it alongside the main database. - If the
.walis missing and the database is in WAL mode, the main file may be in an inconsistent state.
# Check the journal mode of a database
sqlite3 -readonly database.db "PRAGMA journal_mode;"
# Collect all associated files
cp database.db database.db-wal database.db-shm /evidence/
Recovering Deleted Rows
SQLite marks deleted rows as free space but does not immediately zero them. Until the pages are reused or the database is vacuumed, the original data may still be present in the file.
VACUUM and freelist pages
-- Check whether the database has been vacuumed recently
PRAGMA freelist_count;
-- High freelist_count means many deleted-but-not-reclaimed pages exist
Carving with external tools
sqlite_forensics/ undark — parse the raw file to extract rows from free pagessqlparse/sqliteparser— Python-based SQLite page parserbulk_extractor— carves SQLite records from disk images or memory dumps- Autopsy / FTK / Cellebrite — commercial tools with built-in SQLite recovery
Manual carving: SQLite records begin with a variable-length integer (varint) encoding the payload length. The SQLite file format specification at https://www.sqlite.org/fileformat2.html documents the on-disk layout in detail.
Exporting Data
To CSV
sqlite3 -readonly History \
-cmd ".headers on" \
-cmd ".mode csv" \
-cmd ".output urls.csv" \
"SELECT url, title, last_visit_time FROM urls;"
Or from within the shell:
.headers on
.mode csv
.output report.csv
SELECT * FROM urls ORDER BY last_visit_time DESC;
.output stdout
To SQL dump
sqlite3 database.db .dump > backup.sql
Comparing two databases
# Show rows in db_a but not in db_b (by URL)
sqlite3 -readonly db_a.db "ATTACH 'db_b.db' AS b;
SELECT url FROM urls
EXCEPT
SELECT url FROM b.urls;"
Python Integration
Python’s standard library includes sqlite3 — no third-party packages needed.
import sqlite3
from datetime import datetime, timezone, timedelta
CHROME_EPOCH_OFFSET = 11644473600 # seconds between 1601-01-01 and 1970-01-01
def chrome_ts(micros: int) -> datetime:
return datetime.fromtimestamp(micros / 1_000_000 - CHROME_EPOCH_OFFSET, tz=timezone.utc)
con = sqlite3.connect("file:History?mode=ro", uri=True) # read-only
con.row_factory = sqlite3.Row # enables column access by name
with con:
rows = con.execute(
"SELECT url, title, last_visit_time FROM urls ORDER BY last_visit_time DESC LIMIT 50"
).fetchall()
for row in rows:
print(chrome_ts(row["last_visit_time"]), row["url"])
con.close()
Key points:
uri=Truewith?mode=roopens the file read-only at the OS level.row_factory = sqlite3.Rowallowsrow["column_name"]access.- Always use parameterized queries (
?placeholders) when inserting user-supplied values to prevent SQL injection.
# Safe: parameterized
cur.execute("SELECT * FROM urls WHERE url LIKE ?", (f"%{domain}%",))
# Unsafe: string formatting
cur.execute(f"SELECT * FROM urls WHERE url LIKE '%{domain}%'") # never do this
Quick Reference
Common investigative queries
-- Most visited sites
SELECT url, visit_count FROM urls ORDER BY visit_count DESC LIMIT 20;
-- Activity in a specific time window (Chrome)
SELECT
url,
datetime(last_visit_time/1000000 - 11644473600, 'unixepoch', 'localtime') AS ts
FROM urls
WHERE last_visit_time/1000000 - 11644473600
BETWEEN strftime('%s','2025-03-01') AND strftime('%s','2025-03-31')
ORDER BY last_visit_time;
-- Search terms extracted from Google URLs
SELECT
url,
datetime(last_visit_time/1000000 - 11644473600, 'unixepoch', 'localtime') AS ts
FROM urls
WHERE url LIKE '%google.%/search?%q=%'
ORDER BY last_visit_time DESC;
-- Downloads with suspicious extensions
SELECT
target_path,
tab_url,
datetime(start_time/1000000 - 11644473600, 'unixepoch', 'localtime') AS ts
FROM downloads
WHERE target_path LIKE '%.exe'
OR target_path LIKE '%.ps1'
OR target_path LIKE '%.bat'
OR target_path LIKE '%.vbs'
ORDER BY start_time DESC;
References
- SQLite Documentation — official reference
- SQLite File Format — on-disk structure for forensic carving
- SQLite PRAGMA statements — full PRAGMA reference
- SQLite date and time functions — timestamp conversion functions
- Chrome source: history schema — authoritative table definitions
- Firefox places.sqlite schema — Mozilla wiki on the Places database
- Kristinn Gudjonsson, Mastering Python Forensics — chapter on SQLite forensics
- Cory Altheide & Harlan Carvey, Digital Forensics with Open Source Tools — SQLite artifact analysis