Browser Forensics on Windows: Chrome, Edge and Firefox
Investigate Chrome, Edge and Firefox on Windows: History, downloads, cookies, sessions, extensions, WebKit and PRTime timestamps, WAL and deleted records.
This guide shows you how to reconstruct what a Windows user did in Chrome, Edge and Firefox: which pages they visited and how they got there, what they downloaded and where it landed, which accounts they were signed into and which tabs were open. You will learn where each browser keeps its data, how to convert its timestamps, and how to recover records the user tried to clear.
Why browsers deserve their own workflow
In most intrusions and insider cases the browser is where the story starts or ends: the phishing link, the malicious download, the webmail session, the upload to a file-sharing site. Browser data is also some of the most detailed user activity on the system, with per-visit timestamps and referrer chains that registry artifacts never give you. The browser artifacts cheat sheet lists the files; this guide is about working them as an investigation.
Two families cover almost everything you will meet on Windows:
- Chromium: Google Chrome, Microsoft Edge (since 2020), Brave, Opera, Vivaldi. Same file layout, same SQLite schemas, same timestamp format.
- Firefox (Gecko): its own SQLite databases, JSON files compressed with a Mozilla-specific LZ4 container, and a different epoch.
Profile locations
| Browser | Profile root | Notes |
|---|---|---|
| Chrome | %LOCALAPPDATA%\Google\Chrome\User Data\<Profile>\ | Default, Profile 1, Profile 2...; Local State sits one level up |
| Edge | %LOCALAPPDATA%\Microsoft\Edge\User Data\<Profile>\ | Same layout as Chrome, plus Edge features such as Collections |
| Brave | %LOCALAPPDATA%\BraveSoftware\Brave-Browser\User Data\<Profile>\ | Chromium layout |
| Firefox | %APPDATA%\Mozilla\Firefox\Profiles\<random>.<name>\ | Databases in Roaming; profiles.ini in %APPDATA%\Mozilla\Firefox\ lists profiles |
| Firefox cache | %LOCALAPPDATA%\Mozilla\Firefox\Profiles\<random>.<name>\cache2\ | Cache lives in Local, not Roaming |
Enumerate every profile of every installed browser for every user. Chrome's Local State file has a profile.info_cache section that maps profile folders to the display name and the signed-in Google account, which matters when a user keeps a separate "personal" profile.
Collecting without losing data
Browsers keep their databases open, and recent changes often live only in the SQLite write-ahead log or rollback journal. Close the browser if you control the system, or copy through a shadow copy. Always collect the companion files (-wal, -shm, -journal) with each database, and work on copies: opening the original in a SQLite browser can checkpoint the WAL and destroy the very records you want.
# Whole Chromium and Firefox profiles, including companion files
robocopy "C:\Users\jdoe\AppData\Local\Google\Chrome\User Data" E:\case\chrome /E /COPY:DAT /XD "Cache" "Code Cache" "GPUCache"
robocopy "C:\Users\jdoe\AppData\Local\Microsoft\Edge\User Data" E:\case\edge /E /COPY:DAT /XD "Cache" "Code Cache" "GPUCache"
robocopy "C:\Users\jdoe\AppData\Roaming\Mozilla\Firefox" E:\case\firefox /E /COPY:DAT
Drop the /XD exclusions when the cache matters to the case. KAPE's WebBrowsers and BrowserCache targets do the same across all users (see acquisition and triage).
Chromium: the History database
History is the core of any Chromium examination. The tables you will use most:
| Table | What it holds |
|---|---|
urls | One row per URL: url, title, visit_count, typed_count, last_visit_time, hidden |
visits | One row per visit: url (id into urls), visit_time, from_visit, transition, visit_duration |
visit_source | Flags visits that were synced from another device or imported, rather than browsed locally |
downloads | target_path, start_time, end_time, received_bytes, total_bytes, state, danger_type, opened, referrer, tab_url, mime_type |
downloads_url_chains | Every URL in the redirect chain of each download |
keyword_search_terms | Terms typed into the omnibox search, linked to the result URL |
Timestamps: WebKit time
Chromium stores times as WebKit time: microseconds since 1601-01-01 UTC. Multiply by 10 to get a FILETIME, or convert directly in SQLite:
SELECT datetime(v.visit_time/1000000-11644473600,'unixepoch') AS visit_utc,
u.url, u.title,
v.transition & 0xFF AS core_transition,
v.from_visit, v.visit_duration/1000000.0 AS seconds
FROM visits v JOIN urls u ON u.id = v.url
ORDER BY v.visit_time;
The low byte of transition is the core type: 0 link, 1 typed, 2 auto bookmark, 3 auto subframe, 5 generated (omnibox suggestion), 7 form submit, 8 reload, and so on. The upper bits are qualifiers such as redirect markers. A typed transition on a phishing domain tells a different story than a link click from webmail, and following from_visit backwards rebuilds the exact path the user took.
Downloads
SELECT datetime(start_time/1000000-11644473600,'unixepoch') AS start_utc,
datetime(end_time/1000000-11644473600,'unixepoch') AS end_utc,
target_path, received_bytes, total_bytes, state, danger_type, opened,
tab_url, referrer
FROM downloads ORDER BY start_time;
Join downloads_url_chains on id to see every hop; the last URL in the chain is the actual file source, often a CDN or storage bucket rather than the page the user saw. opened = 1 means the file was opened from the browser's download UI, not that it never ran otherwise.
Synced visits
With browser sync enabled, visits made on a phone or another PC land in History with the same structure as local ones. Check visit_source and correlate with a local artifact (a file on disk, SRUM bytes for the browser, DNS or proxy logs) before you place the suspect at this keyboard for a given visit.
Chromium: the rest of the profile
| File | Evidence | Notes |
|---|---|---|
Network\Cookies | Hosts, cookie names, creation and last access (WebKit time) | In the Network subfolder since Chrome 96; values are encrypted |
Login Data | origin_url, username_value, date_created, date_last_used, times_used | Passwords encrypted; user names in clear |
Web Data | Autofill entries, addresses, search engines | Form values typed by the user |
Top Sites | Most visited sites | Can outlive a history clear |
Favicons | icon_mapping.page_url for pages whose icon was stored | Often retains page URLs after history was deleted |
Shortcuts | Omnibox text the user typed and the URL chosen | Captures partial typing |
Sessions\ | Session_<time> and Tabs_<time> files (SNSS format) | Open tabs and their back/forward history |
Preferences, Secure Preferences | Settings, extensions with install time, download directory | JSON |
Extensions\<id>\<version>\ | Installed extension code and manifest.json | Compare IDs with the store and known-bad lists |
Cache\Cache_Data\ | Cached pages, images, scripts | Parse with ChromeCacheView or Hindsight |
Cookies, Login Data and encryption
Chromium on Windows encrypts cookie values and saved passwords with an AES key stored in Local State under os_crypt.encrypted_key, itself protected with the user's DPAPI master key. Values prefixed v10 follow that scheme. From Chrome 127, Google added App-Bound Encryption: values prefixed v20 depend on a key that is also wrapped by a SYSTEM-level service, so the user's DPAPI key alone is no longer enough offline. Edge and other Chromium browsers have adopted this at different times and to different degrees, so check the prefixes in your data rather than assuming. Decryption, where it is authorised and feasible, is covered in credential access forensics.
For most cases you do not need the values. host_key, name, creation_utc and last_access_utc already show that a session existed with a webmail provider, a cloud drive or an admin portal, and when it was last used.
Extensions
Malicious or abused extensions are a real initial access and data theft vector. List every folder under Extensions, read each manifest.json for permissions and host_permissions, and pull install times and install source from the extensions.settings block of Preferences or Secure Preferences. An extension installed outside the store, or with broad host permissions installed the same day as a suspicious download, deserves a closer look.
Edge-specific data
Edge adds its own stores on top of the Chromium layout. Collections\collectionsSQLite holds items the user saved to Collections (URL, title, creation time), which is deliberate curation rather than passive browsing. Edge also keeps IE mode data and, on older or upgraded systems, you may still find the legacy WebCacheV01.dat ESE database of Internet Explorer and EdgeHTML Edge.
Firefox
| File | Tables or content |
|---|---|
places.sqlite | moz_places (URL, title, visit_count, typed, last_visit_date), moz_historyvisits (visit_date, visit_type, from_visit), moz_bookmarks, moz_annos (downloads), moz_inputhistory |
formhistory.sqlite | moz_formhistory: field name, value, timesUsed, firstUsed, lastUsed |
cookies.sqlite | moz_cookies: host, name, value, creation and last access times |
favicons.sqlite | Icons and the page URLs they belong to |
sessionstore.jsonlz4, sessionstore-backups\ | Open windows and tabs, closed tabs, form data (recovery.jsonlz4, previous.jsonlz4) |
logins.json, key4.db | Saved logins, encrypted with keys in key4.db (and the primary password if set) |
extensions.json, extensions\ | Installed add-ons |
Firefox uses PRTime: microseconds since 1970-01-01 UTC. visit_type values include 1 link, 2 typed, 3 bookmark, 5 and 6 permanent and temporary redirects, 7 download and 9 reload.
SELECT datetime(h.visit_date/1000000,'unixepoch') AS visit_utc,
p.url, p.title, h.visit_type, h.from_visit
FROM moz_historyvisits h JOIN moz_places p ON p.id = h.place_id
ORDER BY h.visit_date;
Downloads are recorded as annotations in moz_annos (downloads/destinationFileURI and downloads/metaData) attached to the source URL in moz_places.
Decompressing sessionstore
The .jsonlz4 files are JSON wrapped in Mozilla's mozLz40 container: an 8-byte magic, a 4-byte decompressed size, then an LZ4 block. A few lines of Python unpack them:
import json, lz4.block, sys
data = open(sys.argv[1], "rb").read()
assert data[:8] == b"mozLz40\0"
session = json.loads(lz4.block.decompress(data[8:]))
for win in session.get("windows", []):
for tab in win.get("tabs", []):
for entry in tab.get("entries", []):
print(entry.get("url"), entry.get("title"))
Session files are gold after a history clear or when the user browsed with history disabled: they reflect what was open, including closed tabs that can still be restored.
Recovering deleted and cleared records
"Clear browsing data" and single-entry deletions remove rows, but SQLite rarely wipes the bytes immediately. Look in this order:
- WAL and journal: the
-walfile can hold previous versions of pages, including rows deleted since the last checkpoint. Parse it before merging it. - Free pages and freeblocks inside the database: carve them with a SQLite-aware tool such as FQLite or the carving features of commercial suites (file carving).
- Side stores:
Favicons,Top Sites,Shortcuts, the cache and session files keep URLs independently ofHistory. - Volume shadow copies: older copies of the whole profile.
- Unallocated space, the pagefile and memory for URL strings.
A profile whose History is nearly empty while Favicons, cookies and cache are full is a clear sign of selective clearing, which is itself a finding for the anti-forensics section of your report.
Private browsing traces
Incognito and private windows write no history, cookies or session data to the profile. What remains:
- Downloaded files, with a Zone.Identifier stream. Chromium has been reported to omit the
HostUrlfor downloads from Incognito, so expect a bareZoneId=3in that case. - SRUM network usage for the browser process, per hour (SRUM forensics).
- URLs and page fragments in RAM,
pagefile.sysandhiberfil.sys(memory forensics). - The DNS client cache on a live system (
ipconfig /displaydns, collected during live response). - LNK files and Jump Lists if the user opened the downloaded files.
Tying downloads to files on disk
For each download row, check the file at target_path and read its Mark of the Web, an alternate data stream:
Get-Content -Path "C:\Users\jdoe\Downloads\invoice.iso" -Stream Zone.Identifier
# [ZoneTransfer]
# ZoneId=3
# ReferrerUrl=https://mail.example.com/
# HostUrl=https://files.example.net/dl/invoice.iso
Matching HostUrl with the last entry of downloads_url_chains proves provenance even when history was cleared. From there, follow the file into execution artifacts and archives, as described in phishing initial access.
Tools
- Hindsight: parses a whole Chromium profile (history, downloads, cookies, autofill, preferences, extensions, cache, LevelDB) into a timeline.
- SQLECmd with its maps for Chromium and Firefox databases; DB Browser for SQLite for manual queries on copies.
- NirSoft BrowsingHistoryView, ChromeCacheView and MozillaCacheView for quick reviews.
- Plaso for folding browser events into a super timeline.
- For one-off files, the free Browser Forensics parser opens History, places.sqlite and related databases client-side in the browser.
hindsight.exe -i "E:\case\chrome\Default" -o E:\out\chrome_default -f xlsx
SQLECmd.exe -d E:\case --csv E:\out\sqlite
Pitfalls
- Wrong epoch. WebKit counts from 1601, PRTime from 1970, and both are microseconds. A date in 1970 or in the far future means the wrong conversion.
- Local vs UTC. All browser times are UTC; confirm what your tool displays (see UTC and local time).
last_visit_timeis not every visit. Usevisitsormoz_historyvisitsfor the full sequence.- A visit is not a person reading a page. Redirects, pop-ups, prefetching, frames and sync all create entries. Check
transition,visit_durationand focus evidence. - Opening the original database. It can checkpoint and discard the WAL. Always work on copies.
- Checking only
Default. Multiple profiles and multiple browsers are the norm. - Assuming 90 days is the limit. Chromium expires history after roughly 90 days, but downloads, cookies, Favicons and shadow copies often reach further back.
Workflow checklist
- List users, installed browsers and every profile (
Local State,profiles.ini). - Collect each profile with
-wal,-shmand-journalfiles, plusLocal State, from a shadow copy or a closed browser. - Parse with Hindsight or SQLECmd; query
visits,downloadsandmoz_historyvisitsdirectly for the key window. - Rebuild referral chains with
from_visitandtransition/visit_type. - Match downloads to files on disk and their Zone.Identifier streams.
- Review cookies, Login Data user names, sessions, extensions and Edge Collections for accounts and intent.
- Recover cleared data from WAL, free pages, side stores and shadow copies.
- Correlate with SRUM, LNK files, Jump Lists and user activity artifacts, and put every event on one UTC timeline.
Area 06
User Activity
The artifact cheat sheets behind this guide: location by Windows version, what each one proves and the tools to parse it.