browser::forensics

forensics dfir history · cookies · creds
Profile rootsChromium/Edge FirefoxSafari + mobile DecryptionTimestamps Deleted recoveryElectron / chat apps Tools
RULE 0 — COPY WITH SIDE-CARS → SQLite browser DBs are WAL-mode. Always collect the DB with its -wal and -shm files — the newest (and many deleted) rows live only in the unmerged WAL. Opening a live DB checkpoints it and destroys that evidence. Work on copies, read-only.
01Where the Data Lives (default profile roots)
Profile locations
BrowserWindowsmacOSLinux
Chrome%LOCALAPPDATA%\Google\Chrome\User Data\Default~/Library/Application Support/Google/Chrome/Default~/.config/google-chrome/Default
Edge%LOCALAPPDATA%\Microsoft\Edge\User Data\Default~/Library/Application Support/Microsoft Edge/Default~/.config/microsoft-edge/Default
Brave…\BraveSoftware\Brave-Browser\User Data\Default~/Library/Application Support/BraveSoftware/…~/.config/BraveSoftware/…
Firefox%APPDATA%\Mozilla\Firefox\Profiles\<rand>.default-release~/Library/Application Support/Firefox/Profiles/<…>~/.mozilla/firefox/<…>
Safari—~/Library/Safari/ (sandbox: ~/Library/Containers/com.apple.Safari/…)—
ENUMERATE EVERY PROFILE → Chromium: Default, Profile 1, Profile 2… (one Local State key decrypts all). Firefox: read profiles.ini — modern default is *.default-release, legacy is *.default. Firefox splits Windows storage: DBs in Roaming (%APPDATA%), cache2 in Local (%LOCALAPPDATA%).
02Chromium (Chrome / Edge / Brave / Opera)
Artifacts (all SQLite unless noted; no file extension)
FileKey tables / content
Historyurls(url,title,visit_count,last_visit_time) · visits(visit_time,from_visit,transition) · downloads(target_path,tab_url,start/end_time,bytes) · keyword_search_terms(term)
Network/Cookiescookies(host_key,name,encrypted_value,creation/expires/last_access_utc). Moved to Network\Cookies in v96+ — check both.
Login Datalogins(origin_url,username_value,password_value[enc],date_created,times_used,blacklisted_by_user)
Web Dataautofill(name,value) · credit_cards(card_number_encrypted) · keywords
BookmarksJSON — date_added is WebKit µs as a string. Bookmarks.bak = prior copy (deleted-bookmark recovery)
Top Sites / Shortcutsmost-visited tiles · omni_box_shortcuts = what the user typed in the omnibox + hit counts
Faviconsicon_mapping(page_url) — proves visited pages even after History is cleared
Sessions / TabsSNSS format (magic SNSS). Current/Last Session & Tabs = open tabs + per-tab back/forward stack
Local StateJSON, in User Data root (above profile). Holds os_crypt.encrypted_key — see §05
Local Storage / IndexedDBLevelDB (leveldb/). App tokens, drafts, cached messages/mail. Deleted values recoverable — see §07
Cache / Code CacheSimple Cache (data_# blocks + f_######). Cached URLs, headers, bodies
Quick queries (work on a copy)
# recent history, WebKit time → human
sqlite3 -readonly History "SELECT
 datetime(last_visit_time/1000000-11644473600,
 'unixepoch') t,url,title FROM urls
 ORDER BY last_visit_time DESC LIMIT 50;"

# search terms typed by the user
sqlite3 History "SELECT term FROM keyword_search_terms;"

# downloads
sqlite3 History "SELECT target_path,tab_url,
 received_bytes FROM downloads;"
"They cleared history" — still recoverable
# Cleared History does NOT clear these:
- Top Sites, Favicons (icon_mapping)
- Shortcuts (typed omnibox text)
- Sessions / SNSS (open + last tabs)
- Cache_Data (viewed URLs)
- Local Storage / IndexedDB LevelDB
- the -wal + freelist of History itself (§07)
03Firefox / Gecko (+ Tor Browser)
Artifacts (profile root unless noted)
FileKey tables / content
places.sqlitemoz_places(url,title,visit_count,last_visit_date) + moz_historyvisits(place_id,visit_date,visit_type) + moz_bookmarks + moz_annos (downloads live here post-Fx21)
cookies.sqlitemoz_cookies(host,name,value[plaintext],expiry,lastAccessed,creationTime)
formhistory.sqlitemoz_formhistory(fieldname,value,firstUsed,lastUsed) — typed search/form terms
logins.json + key4.dbsaved credentials (3DES-CBC), decrypted via key4.db NSS store — see §05
sessionstore.jsonlz4mozLz4-compressed. + sessionstore-backups/{recovery,previous}.jsonlz4 = open/closed tabs + per-tab URL history
favicons.sqlitemoz_icons — corroborates visited sites after history trim
permissions.sqlitemoz_perms — geo/cam/mic/popup grants per origin
cache2/ (in Local)cached responses; entry trailer holds original URL + headers
prefs.js / extensions.jsonhomepage, download dir, proxy, sync hints · installed add-ons (dates flag sideloaded)
History + session
sqlite3 places.sqlite "SELECT
 datetime(h.visit_date/1000000,'unixepoch') t,
 p.url,p.title FROM moz_places p
 JOIN moz_historyvisits h ON p.id=h.place_id
 ORDER BY t;"
# PRTime = µs since 1970 (NOT 1601!)

# decompress recovered tabs (mozLz4)
dejsonlz4 sessionstore-backups/recovery.jsonlz4 out.json
Epoch trap in ONE table
# cookies.sqlite mixes epochs:
expiry        = Unix SECONDS
creationTime  = PRTime MICROSECONDS
lastAccessed  = PRTime MICROSECONDS
# divide expiry by 1e6 → year 56000. Don't.
04Safari (macOS/iOS) + Mobile
Safari (macOS)
# ~/Library/Safari/  (or the sandboxed Container copy)
History.db          # SQLite: history_items + history_visits
                    # visit_time = Cocoa epoch (sec since 2001)
Cookies.binarycookies  # ~/Library/Cookies/ — NOT SQLite!
Downloads.plist     # bplist: source URL, path, bytes
LastSession.plist   # open tabs at last close
Bookmarks.plist     # bookmarks + Reading List (saved, maybe unvisited)
CloudTabs.db        # tabs open on OTHER iCloud devices
Mobile
# iOS Safari (needs FFS/backup)
/private/var/mobile/Library/Safari/History.db
/private/var/mobile/Library/Cookies/Cookies.binarycookies
# backups: HomeDomain, filename = SHA1(domain-path) → use iLEAPP

# Android (root / full FS extraction)
/data/data/com.android.chrome/app_chrome/Default/   # Chromium schema
/data/data/org.mozilla.firefox/files/places.sqlite   # Fenix
/data/data/<app>/app_webview/Default/   # in-app WebView (no history DB)
Parse the non-SQLite bits
plutil -p Bookmarks.plist            # macOS native
python3 -c "import plistlib;print(
 plistlib.load(open('Downloads.plist','rb')))"
python3 BinaryCookieReader.py Cookies.binarycookies
ileapp -t fs -i <extraction> -o out  # iOS: Safari+Chrome+WebView
mac_apt.py -o out <image> SAFARI
05Decryption
Chromium cookies / passwords
# Prefix tells the scheme:
#   (none) pre-v80 = raw DPAPI
#   v10/v11 = AES-256-GCM under the Local State key
#   v20     = App-Bound Enc (Chrome 127+): needs SYSTEM/app ctx

# 1) recover the AES key (live, user context):
key = CryptUnprotectData(
   b64decode(local_state['os_crypt']['encrypted_key'])[5:])
#   [5:] strips the 'DPAPI' prefix → 32-byte AES key

# 2) decrypt a v10/v11 blob: 3B prefix|12B nonce|ct|16B tag
AES.new(key, AES.MODE_GCM, nonce=blob[3:15]) \
   .decrypt_and_verify(blob[15:-16], blob[-16:])

# offline (disk image): need the user's DPAPI masterkey
# %APPDATA%\Microsoft\Protect\<SID>\ + SID + logon pw
impacket dpapi.py masterkey -file <MK> -sid <SID> -password <pw>
# macOS: Keychain 'Chrome Safe Storage' (AES-128-CBC, salt 'saltysalt')
# Linux: gnome-keyring/kwallet, else hardcoded 'peanuts'
Firefox logins (NSS)
# key4.db 'metaData' = global salt + password-check;
# 'nssPrivate' = wrapped 3DES key. No Primary Password
# (the default) → decrypts with the EMPTY string.
python3 firepwd.py -d <profile_dir>
python3 firefox_decrypt.py <profile_dir>
# → plaintext hostname / username / password

# Firefox COOKIES are plaintext — don't try to decrypt them.
06Timestamps — the #1 error
Every browser uses a different epoch
EpochUnit / origin→ UnixUsed by
WebKit/Chromeµs since 1601-01-01v/1e6 − 11644473600Chrome/Edge/Brave (+Android Chrome) History, Cookies, downloads
Firefox PRTimeµs since 1970-01-01v/1e6places, formhistory, cookie creation/lastAccessed
Firefox expirySECONDS since 1970vcookies.sqlite expiry only (mixed table!)
Cocoa/CFAbsolutesec since 2001-01-01v + 978307200Safari History.db, binarycookies (macOS/iOS)
JS / LevelDBms since 1970v/1000Electron app values, IndexedDB (Date.now())
FILETIME100ns since 1601v/1e7 − 11644473600DPAPI blobs, $MFT, registry
Python one-liners
# WebKit (Chrome)  python3 -c "import datetime as d;print(d.datetime(1601,1,1)+d.timedelta(microseconds=WEBKIT))"
# PRTime (Firefox)  python3 -c "import datetime as d;print(d.datetime(1970,1,1)+d.timedelta(microseconds=PRTIME))"
# Cocoa (Safari)   python3 -c "import datetime as d;print(d.datetime(2001,1,1)+d.timedelta(seconds=COCOA))"
07Deleted-Record Recovery
SQLite (history / cookies)
# priority order of survival:
1. -wal frames    # newest + just-deleted rows
2. freelist pages # undark / bring2lite / walitean
3. unallocated / slack inside DB pages
4. carve from the volume image

undark -i History --freespace      # dump deleted rows
bring2lite History                 # freelist + WAL parser

# WARNING: Firefox runs VACUUM ('places maintenance')
# which PERMANENTLY wipes freelist — recover before touching.
LevelDB (Local Storage / IndexedDB)
# Not one file — parse the whole leveldb/ dir:
#   CURRENT, MANIFEST-*, *.log, *.ldb
# Overwritten/deleted values survive in unmerged .log
# and stale .ldb sstables until compaction.
strings *.log *.ldb | grep -i token   # quick hunt
# structured: parse sstables directly (ccl_chromium_reader)
# values may be lz-string / JSON encoded → decode after.
Incognito / Private
# Almost nothing hits the profile. Pivot to volatile:
pagefile.sys · hiberfil.sys · swap · RAM capture
bulk_extractor -o out pagefile.sys   # url/email/domain
vol -f mem.raw windows.pslist        # + yarascan 'v10'/'SQLite'
# DNS cache + $MFT/USN also place sites / session timing.
08Electron / Chat Apps
Same engines as Chromium — same techniques
# Discord, Slack, new Teams, Signal, WhatsApp are Chromium/Electron:
# SQLite (Cookies) + LevelDB (Local Storage/IndexedDB) + Simple Cache.
# Same parsers, same epochs (watch JS ms), same deleted-recovery.
Windows default paths + what's recoverable
AppPath (Windows)Recover
Discord%APPDATA%\discord\messages/cache; Local Storage\leveldb holds the session token
Slack%APPDATA%\Slack\IndexedDB messages; xoxc- token + workspace state in LevelDB/Cookies
Teams (classic)%APPDATA%\Microsoft\Teams\IndexedDB\…teams.microsoft.com…leveldb = messages. Retired 2024 but still on images
Teams (new)%LOCALAPPDATA%\Packages\MSTeams_8wekyb3d8bbwe\…\EBWebView\WebView2 — standard Chromium artifacts under EBWebView (confirm package: MSTeams_* work vs consumer)
Signal%APPDATA%\Signal\sql\db.sqlite is SQLCipher; key wrapped in config.json via safeStorage (DPAPI) → unwrap in user ctx, then PRAGMA key
Parse app LevelDB
# forensicsim — Teams/Slack/Discord IndexedDB → messages
python dump_leveldb.py -i <...indexeddb.leveldb> -o out.json
# same simple-cache format as browsers:
ChromeCacheView.exe /CacheFolder "...\Cache\Cache_Data" /scomma c.csv
Note
# Tokens in LevelDB are recovery artifacts of an
# authenticated session — treat as session/credential
# recovery for the investigation. Full server-side
# message history usually needs a provider/legal request.
09Tools
Chromium
hindsight.py -i "User Data/Default" -o report -f xlsx
#   history+cookies+downloads+local storage timeline
hindsight.py -i <prof> -b Firefox -o report   # -b Chrome|Firefox
ChromeCacheView / ChromeCookiesView / BrowsingHistoryView  # NirSoft
Firefox / Safari / mobile
python3 dumpzilla.py <profile> --All   # FF one-shot dump
firefox_decrypt.py <profile>            # saved logins
ileapp / aleapp -t fs -i <extraction> -o out  # iOS / Android
mac_apt.py -o out <image> SAFARI          # macOS/iOS
Collection + URL analysis
kape.exe --tsource C: --target Chrome,Edge,FireFox --tdest out
#   add Slack/Teams/Discord targets for chat data
unfurl "https://www.google.com/search?q=...&ei=..."
#   breaks a URL into search terms / embedded timestamps / IDs
WORKFLOW →  ① Enumerate every profile (Chromium Profile N, Firefox profiles.ini)  ② Copy each DB with -wal/-shm, work read-only  ③ Timeline with hindsight / dumpzilla — mind the epoch per browser  ④ Creds/cookies → Local State key (§05) or firefox_decrypt  ⑤ "Cleared"? → Sessions/SNSS, Favicons, Cache, LevelDB, WAL/freelist  ⑥ Chat app? → same engines, parse LevelDB/IndexedDB (§08)  ⑦ Incognito? → memory/pagefile/hiberfil (§07)