Showing posts with label leveldb. Show all posts
Showing posts with label leveldb. Show all posts

Sunday, May 10, 2026

Reading the CURRENT — LevelDB Forensics with crush

Moin! 👋

LevelDB is everywhere — you just don't see it. If your workflow starts and ends with SQLite, you're walking past a surprising amount of data on every acquisition. Chrome, Electron-based desktop apps, iOS, Android — LevelDB shows up constantly in real cases, often unnoticed, and often holding data that other tools quietly skip over.

With the current release, the crush LevelDB viewer has grown significantly. This post walks through what it can do — and more importantly, what it means for your investigation.

What Is LevelDB — and Why Should I Care?

LevelDB is an on-disk key-value store developed by Google. Unlike SQLite, it does not present itself as a single file with a recognisable header — instead, a LevelDB database occupies an entire folder, containing a mix of files with .log, .ldb, and metadata files named MANIFEST-######, CURRENT, and LOG. If you have ever opened an acquisition and seen a folder full of files like that and moved on, there is a good chance you left data on the table.

The forensic value of LevelDB depends on what the application chose to store there — and that varies enormously. In Chrome and Chromium-based browsers you will find browsing history fragments, sync metadata, IndexedDB entries from web applications, session state, and local storage data. Electron apps like Signal Desktop, Discord, or WhatsApp Desktop use LevelDB for app configuration, cached user data, and UI state.

On top of that, there is a structural property worth understanding: deleted records are not immediately gone. Every write to LevelDB — including deletions — is appended as a new entry with a sequence number. A deletion creates a "tombstone" entry: the key remains, but the value is empty. In .ldb files, superseded or deleted records are eventually cleaned out during compaction — but until that happens, they can still be read. In .log files, the active write log, everything is there. A tombstone without a value is not nothing: the key alone can tell you that something existed and was deliberately removed.

Alex Caithness from CCL wrote the definitive primer on this in 2020 — if you have not read it yet, it is required reading before going further:

The open-source Python library that came out of that research — ccl_chrome_indexeddb — is also the foundation that the crush LevelDB viewer builds on. Full credit to Alex and the CCL team for that work.

Where to Find LevelDB

LevelDB shows up more often than most people expect — and the pattern is consistent enough that you can learn to spot it without memorising paths.

On iOS and Android, look for any Chromium-based browser (Chrome, Edge, Brave) and any app built on a Chromium WebView or Electron runtime. The LevelDB stores typically live under the app's data container, in subdirectories named IndexedDB, Local Storage/leveldb, or Sync Data/LevelDB. The folder name is usually your first hint.

On desktop systems — Windows, Linux, macOS — the same logic applies, but the surface area is larger than most people assume. Every Chromium-based browser and every Electron app on the system has its own LevelDB stores. On Linux alone, a standard workstation running Chrome, Signal Desktop, Discord, and Slack will have dozens of LevelDB databases under ~/.config/. It is easy to treat LevelDB as a mobile-only concern. It is not.

The reliable indicator is always the folder structure: a directory containing a CURRENT file, one or more MANIFEST-###### files, and a mix of .log and .ldb or .sst files is a LevelDB database — regardless of what the folder itself is named.

Example LevelDB folder content


For this post, I am using the Spotify Client App from an iPhone acquisition (Public iOS 17 Image from Josh Hickman) as the example throughout. The path to the LevelDB looks like this:

/private/var/mobile/Containers/Data/Application/E81161FB-2689-4DF6-B98B-C8F95B056440/Library/Application Support/PersistentCache/Users/7jlf2hrgerxu6n0pp8l35r5h0-user/primary.ldb/

Opening a LevelDB Database in crush

LevelDB databases don't announce themselves — there is no single file to click on. A LevelDB database is a plain directory, typically named LevelDB (or sometimes leveldb), containing a mix of .log, .ldb, .sst, and metadata files. There is no special label or icon that distinguishes it from any other folder in the file panel. The .sst (Sorted String Table) extension is functionally identical to .ldb — it is an older naming convention still used by some non-Chrome LevelDB implementations. crush handles both.

To open it in crush, right-click the folder in the file panel and select Open. A single left-click only selects the folder — the right-click menu is what triggers crush to read the directory structure and open the LevelDB viewer. That is the step that is easy to miss the first time.

No extraction needed — crush reads directly from inside ZIP or TAR archives, or from a folder on disk. 

File panel showing right-click context menu on a LevelDB folder, with


The Overview Tab

The first tab is the Overview. Before looking at any records, this is where I orient myself.

It shows the MANIFEST metadata for the database: the comparator in use, the last sequence number, the log number, and the files that make up the database grouped by compaction level. crush parses all MANIFEST-* files present in the directory — not just the current one. The active MANIFEST is labelled (current); older ones expose compaction history from before the last recovery, potentially referencing file numbers that are no longer on disk. The CURRENT pointer itself is shown as a separate entry, and where present, the prev_log_number field identifies the WAL log file that preceded a recovery — useful context when you are trying to reconstruct the write history of a database that has been through a crash or a clean shutdown cycle.

The last sequence number tells you how many write operations have been performed on this database across its lifetime — including updates and deletions, not just inserts. A low number (like 0 or close to it) means the database is fresh or barely touched. A high number is a signal that there has been a lot of activity — and potentially a lot of deleted data worth looking at.

The compaction level of the files tells you how mature the database is. Fresh data lives in .log files and level-0 .ldb files; as the database ages and compaction runs, data moves to higher levels and old or deleted records get cleaned out. A database where everything is still at level 0 has had very little compaction — which is good for us, because deleted records are more likely to still be present.


Overview tab showing MANIFEST metadata, all MANIFEST files, and file list by level


The Files Tab

The Files tab gives you a per-file breakdown: filename, type (Log or Ldb), compaction level, size in bytes, and counts of live, deleted, and unknown records per file. For .ldb and .sst table files, the tab also shows the Smallest Key and Largest Key boundaries sourced directly from the MANIFEST VersionEdit entries — decoded as UTF-8 where possible, hex otherwise. At a glance, you can see which files cover which key ranges without opening a single record.

Files containing deleted records are colour-coded red. That is your first triage signal — if a file is red, it is worth a closer look. A file with a high deleted record count relative to its live records tells its own story.

The distinction between Live, Deleted, and Unknown is worth understanding:

  • Live — the record is the current version of a key and has a value.
  • Deleted — a tombstone entry. The key is present, the value is empty. Something was here.
  • Unknown — records in .ldb files where the state cannot be determined without reading the full compaction context. These are worth inspecting manually.
Files tab showing per-file record counts, size, key ranges, with red-coded files containing deleted records

The Records Tab — Live and Deleted in Context

The Records tab shows all records from all files in a single table. This is where the examination happens.

The filter toolbar lets you narrow down to All / Live / Deleted / Unknown records. Deleted records appear inline in red, directly alongside the live data — not in a separate view. That matters: seeing what is gone in the context of what is still there is often where the interesting questions start.

Each row shows:

  • The sequence number — the order in which this record was written. Lower sequence numbers are older writes.
  • The offset — the byte position within the source file where this record begins, displayed in hex. This is your anchor for cross-referencing in a hex editor and for court testimony: if you need to point to exactly where in the evidence file a record lives, this is how you do it. The offset is included in CSV exports.
  • A UTF-8 text preview and a hex preview of the key.
  • A UTF-8 text preview and a hex preview of the value.
  • The state — Live, Deleted, or Unknown.

Selecting any row feeds the full key and value bytes into a split hex pane below the table — separate Key and Value tabs, so you can inspect each independently without them running together. For records from .ldb or .sst table files, a third Internal Key tab is also available, showing the complete LevelDB internal key: the user key plus its 8-byte sequence number and type suffix. This is the raw on-disk representation — useful when you need to verify exactly what LevelDB stored, or when the type byte tells you something about how the record was written.

Records tab showing mixed live and deleted records, with the split hex pane below


Search and Filter

The Search box filters rows case-insensitively across all columns — key text, value text, state. It combines with the state filter buttons, so you can ask questions like "show me only deleted records whose key contains this prefix" in two clicks. For databases with thousands of records, this is how you find signal in the noise quickly.

The LOG Files

LevelDB writes its own operational log to files named LOG and LOG.old — plain text, not to be confused with the binary .log write-ahead log files. crush surfaces these in dedicated tabs, one per file that exists, with the full content displayed in a monospace view and a Find toolbar for searching within the log. This is where LevelDB records its own compaction events, file opens, and error conditions — useful background when you are trying to understand what happened to a database and when.

BLOB Inspector — When the Value Is Protobuf

In the LevelDB used as the example here, roughly 95% of the values are Protobuf-encoded binary data. That is typical for LevelDBs — the apps use Protobuf extensively for serialising entity data before it goes into the LevelDB store.

Right-clicking any row in the Records table offers Inspect Key…, Inspect Value…, and for table-file records, Inspect Internal Key… — all opening the shared BLOB Inspector dialog.

In Auto mode, the inspector tries to detect the format automatically. For valid Protobuf data it will decode it and display the wire-decoded fields in protoc --decode_raw style — field numbers, wire types, and values, rendered as formatted text. PNG, JPEG, GIF, binary plists, XML, Android Binary XML, and JSON are also detected automatically in Auto mode via magic bytes or content sniffing.


BLOB Inspector open on a Protobuf value, showing decoded fields


If auto-detection does not find a match, you can manually select from the available decode modes: Hex, UTF-8, Latin-1, Base64, Plist, XML, JSON, Protobuf (schema-less), Android Binary XML, or Image. This covers the formats you are most likely to encounter in LevelDB values across Chrome, Electron apps, and Android databases.

The inspector opens as a non-modal window — the rest of the UI stays fully interactive while it is open, and you can have multiple inspectors open at the same time. That matters when you are comparing values across records without constantly opening and closing the dialog.

The BLOB Inspector is shared across all viewers in crush — SQLite, LevelDB, Realm. Any improvement to the inspector (a new decode mode, a UI fix) benefits all three at once. If you have been following the SQLite and Realm deep dives, the workflow here is identical: right-click, inspect, decode.

Further Reading

Happy examining. 🐢

Saturday, April 25, 2026

Introducing crush: A DFIR Workbench for Surfing Through Data Formats

Moin! 👋

Today I want to share something a little different from the usual artifact analysis posts — I am releasing crush-forensics v0.5.0, a digital forensic analysis workbench I have been building for a while.

You can find it on GitHub: github.com/kalink0/crush-forensics

The Problem

When I work with acquisitions — especially mobile ones — I often find myself wanting to quickly look at a specific file without firing up a full forensic platform. Maybe I want to check a PLIST, peek into a SQLite database, or just confirm what a binary blob actually contains. Opening the whole acquisition in a heavy tool just to answer a quick question felt like overkill.

At the same time, reaching for a hex editor or writing a one-off script every time also gets old fast. I wanted something in the middle: a lightweight, dedicated workbench that knows about the file formats we actually work with in DFIR — and that lets me navigate directly inside ZIP and TAR archives without extracting anything to disk first.

That is what crush is.

Why "crush"?

I am a big Finding Nemo fan. Crush is the laid-back sea turtle who surfs the East Australian Current — and that is exactly the vibe I wanted for this tool: just riding through data formats, going with the flow. 🐢

The idea of surfing through file formats felt like the right metaphor — you open an archive, navigate the structure, and glide from a PLIST to a SQLite DB to a hex view without fighting the current. Dude.

What Can It Do?

crush is a Python-based GUI application (built with PySide6). It supports:

  • Opening and navigating inside ZIP and TAR archives directly — no extraction to disk needed
  • Opening single files and folders
  • Export files/folders and open them directly in external software
  • Hex Viewer
  • SQLite Viewer
  • Text Viewer with syntax highlighting and encoding detection
  • JSON Viewer (collapsible tree)
  • XML Viewer (collapsible tree)
  • PLIST and BPLIST Viewer
  • SEGB v1 and v2 Viewer
  • ABX Viewer (Android Binary XML)
  • LevelDB Viewer (Chrome LevelDB / Android app databases)
  • Image Viewer
  • Media Viewer (audio and video)
  • Multi-Log Studio — multi-source log analysis with format auto-detection, including Apple Unified Log / .tracev3 / .logarchive, syslog, and more (note: Unified Log support is currently alpha — decoding can be slow, this is actively being worked on)
  • Protobuf Viewer — schema-less, with optional schema decoding
  • PDF text extraction
  • Realm Database Viewer — header, schema/class extraction, top-ref comparison, table/column data decoding

Android ABX viewer in crush (Linux)



iOS SQLite viewer in crush (Windows)


Built-in Data Format Database

One feature I am particularly happy with is the built-in data format database. Crush identifies forensically relevant formats by magic bytes and extension, and surfaces the information directly in the UI for every selected file — including formats that do not have a dedicated viewer yet.

For each format it shows:

  • Full name and abbreviation
  • Category (database, configuration, log, ...)
  • Forensic relevance — what an investigator is likely to find here
  • Relevant platforms (iOS, Android, macOS, Windows, ...)
  • Magic bytes with offset and description
  • Links to format specs and relevant forensic research

Format Reference — the built-in data format database in crush


So for example, when you open an ABX file, crush identifies it via its magic bytes, opens it in the ABX viewer, and right there in the UI you can see that this is an Android Binary XML file used for system and app settings — plus links to the AOSP source and relevant research from CCL Solutions. No more alt-tabbing to a browser to remember what a format is.


Integrity Mode

Crush also has an optional integrity mode for auditability. When enabled:

  • Records SHA-256 hashes when files are opened or exported
  • Hashes ZIP/TAR/file sources on open (folders are not hashed)
  • Writes hashes to the log
  • Creates a crush-export-hashes.txt file next to exported data

You can toggle it via the status badge in the bottom right of the UI. It can also be turned off for faster opening of large ZIP/TAR sources. A small but useful addition for anyone who needs to demonstrate that what they examined is what they got.


Integrity Mode dialog in crush

Installation

v0.5.0 ships with pre-built binaries for Windows, Linux, and macOS — available directly from the GitHub releases page. No Python environment needed, no dependency wrangling — just download and run.

One honest caveat: the macOS binary is currently untested as I do not have a macOS system available. If you are on macOS and give it a try, please let me know how it goes via GitHub issues or direct message — that feedback would be really valuable.

If you prefer to run from source (e.g. for development), the README has full instructions, including the platform-specific system dependencies and how to download the Unified Log parser binaries.

Credits

Crush builds on some excellent work from the DFIR community. Bundled third-party modules include:

  • ccl_bplist by CCL Solutions Group — binary plist parsing
  • ccl_segb by CCL Solutions Group — SEGB parsing
  • ccl_leveldb by CCL Solutions Group — LevelDB / Chrome LevelDB parsing
  • macos-UnifiedLogs by Mandiant — Apple Unified Log parsing

Thank you to everyone in the community whose research and open source work made this possible.

Nightly Builds

If you want to live on the edge — every night a fresh build is automatically created from the latest main branch, including the most recent features and fixes. Changes are tracked in the changelog.md in the repo. Not recommended for production casework, but great if you want to try out what is coming next or help with testing.

What's Next?

There is still a lot I want to add — more decoders, better search, maybe some lightweight timeline capabilities. I will keep building as I run into things I wish it could do during actual casework.

If you try it out, I would love to hear what features would be most useful to you. Open an issue on GitHub, or reach out to me on Mastodon, Bluesky, or LinkedIn.

Until next time — happy forensicating! 🔍

GitHub: github.com/kalink0/crush-forensics