Moin! 👋
After the SQLite deep dive with crush, the next stop is Realm. This post walks through what the crush Realm viewer can do — and as before, what the features actually mean for an investigation. I am using the Android app de.formel1 — the German Formula 1 news app — as the example throughout. I enjoy watching F1, and it turns out the app stores some of its data in its Realm database.
With v0.7.0, the crush Realm viewer has been significantly extended and improved. This post walks through what it can do — and as before, what the features actually mean for an investigation.
What is Realm — and Why Should I Care?
Realm is an object-oriented database developed for mobile platforms. Unlike SQLite which stores data in tables with rows and columns, Realm stores data as objects — think classes with properties rather than spreadsheet rows. It was originally developed by Realm Inc., acquired by MongoDB in 2019, and has seen adoption in a range of Android and iOS apps.
Forensically, Realm matters because it shows up in real cases — and most tools either
support it poorly or not at all. If you have never encountered a .realm file
in an acquisition, you may simply not have noticed: the file has no extension-based
prominence and the format is far less documented than SQLite.
For a thorough introduction to Realm and its physical structure, Damien Attoe's The Realm Files series is the best starting point available:
Finding the Database
Realm databases use the .realm extension and are typically found under the
app's data directory. For the Formula 1 news app it is:
/data/data/de.formel1/files/
The extension alone is not always reliable — some apps store Realm files without it or
under an unexpected name. A more robust identifier is the magic bytes: at offset 16 in
the file, Realm stores the 4-byte value 54 2D 44 42 — the ASCII string
"T-DB". If you see that signature, you are looking at a Realm database
regardless of the filename.
In crush, .realm files are now labelled Realm in the VFS
tree panel — the same way SQLite files show their type at a glance. Previously they showed
no label at all, which made them easy to overlook when browsing a large acquisition.
The Header
The Header tab is my first stop when opening a Realm file — before looking at any data. It shows the fundamental structural information of the database: the file format version, the positions of both top-level references in the file, and which one is currently active.
This matters because everything else in the viewer builds on this foundation. The active Top Ref determines which snapshot is the current state of the database; the inactive one points to the previous commit. Knowing where both live in the file is the starting point for understanding what the database currently contains — and what it contained before.
The Schema Tab
The Schema tab shows every table (class in Realm terminology) as an expandable node
listing all column names with their Realm type: int, bool,
string, date, link, and so on.
This is important context before looking at data. Which tables exist? What properties do
they have? Are there link columns that reference other tables — and can I
join across them? Tables without decoded data still appear as leaf entries so nothing
is hidden.
In earlier versions of the crush Realm parser, every column was labelled
col_0, col_1, … regardless of the actual schema names.
That is now fixed — column names are read directly from the Realm spec node.
Top Refs — Deleted Data and Schema Diff
Before diving into tables, the Top Refs tab is worth checking first. This is specific to Realm's internal architecture and has direct forensic relevance.
Realm maintains two top-level references — Top Ref 0 and Top Ref 1 — as part of its copy-on-write architecture. At any point, one of them is active (the current state of the database) and the other represents the previous commit. The Top Refs tab shows both references, which one is currently active, and key settings and metadata for each.
Beyond the low-level header comparison, the tab now also shows a Diff — schema section. This is where it gets forensically useful: crush compares the schema of both Top Refs and lists tables that exist only in the active ref (added since the last snapshot), tables that exist only in the inactive ref (deleted since the last snapshot), and tables present in both but with a changed row count. A drop in row count between the inactive and active ref is a direct indicator that records were deleted between the two commits.
Freed Data — Inspecting Unallocated Space
In addition to active and previous snapshots, crush also exposes freed data directly. Unlike the Tables tab, this is not reconstructed into rows — it is a view into unallocated regions of the Realm file.
When objects are deleted in Realm, their space is marked as free but not immediately overwritten. These regions may still contain residual data from previously stored objects. The Freed Data tab surfaces these regions as individual entries with:
- Offset — where the free region is located in the file
- Size — the length of the region
- Source — whether the region originates from the active or inactive Top Ref
- Content — decoded where possible (currently it looks for null terminated strings)
Selecting an entry reveals the raw data, including a full hex view at the bottom of the tab. This keeps the representation close to the underlying file structure and avoids introducing assumptions about object boundaries or schema correctness.
In practice, these regions can still contain recognizable artefacts such as strings, timestamps, or fragments of previously stored objects. However, interpretation requires care: data may be partially overwritten, misaligned, or no longer internally consistent.
What this view provides is access to data that is not present in either the active or previous snapshot — and would otherwise remain invisible.
The Tables Tab — Active and Previous Snapshot
The Tables tab shows decoded table data from both Top Refs — not just the active one. This mirrors how the SQLite WAL viewer exposes superseded page versions: the idea is the same, Realm just implements it differently at the format level.
Tables from the inactive (previous) ref can be shown by activating the
Show diff to prev ref checkbox. In the temporary SQLite database
(more on this in the next section) the previous refs are stored as
_prev_<tablename>, which makes cross-snapshot comparisons straightforward.
The example data in the image is synthetically generated — I do not have real test data
for this scenario in the Formula 1 app.
Deleted = Only existent in the prev ref
Prev Version = In both refs - but different content
Added = Only in the active ref
Everything else = identical in both refs
A few things that are now decoded correctly that were previously broken:
- Timestamps — Realm timestamp columns (type 8) were previously fed
through the wrong decoder, producing incorrect Unix-epoch-1970 dates. They are now
decoded correctly and displayed as
YYYY-MM-DD HH:MM:SS UTC. - Strings — format 24 stores strings in fixed-width inline entries. The previous logic produced garbage values; strings now decode correctly.
- Booleans — nullable booleans (Realm's 2-bit encoding: 0=False,
1=True, ≥2=NULL) were previously returned as raw integers 0/1/2. They now show as
True,False, orNone. - NULL-only columns — columns with no payload were silently dropped.
They now appear in the table with all-
Nonevalues so the column is visible.
The SQL Bar and Cross-Snapshot Queries
When the Tables tab opens, crush loads the decoded Realm data — from both snapshots —
into a temporary in-memory SQLite database. The full SQL bar is available:
SELECT, WHERE, ORDER BY, aggregates, and more.
The temp database is deleted automatically when the tab is closed.
Because both the active and previous snapshot tables are present in the same database, cross-snapshot comparisons are possible directly in SQL. For example, to find rows that exist in the previous snapshot but not in the active one — i.e. deleted records:
SELECT p.* FROM _prev_class_Evidence p LEFT JOIN class_Evidence e ON p._objkey = e._objkey WHERE e._objkey IS NULL
Cross-Table JOINs via _objkey
Beyond cross-snapshot queries, Realm link columns store ObjKey values — the internal
identifiers Realm uses to reference objects across tables. Each table in the temporary
SQLite database receives a leading _objkey column containing the Realm
ObjKey for every row, making cross-table joins straightforward:
SELECT ae.dt_published [Published Date], ae.title [Title], a.share_link, e.last_name [Author] FROM class_Article a JOIN class_AuthorEDP e ON a.author = e._objkey JOIN class_ArticleEDP ae ON a.edp = ae._objkey
ObjKeys are not shown in the table grid — they are only present in the SQL database — so the visual view stays clean while the join capability is fully available.
Inspect Cell
As with the SQLite viewer, right-clicking any cell and selecting "Inspect cell…" opens the content in a dedicated window with auto-detection. If the content is a known format — a binary plist, JSON — it opens directly in the appropriate viewer. Otherwise the hex view is available as fallback.
Realm BLOB columns occasionally contain nested serialized data — Inspect Cell is the fastest way to find out what is in there without leaving crush.
Further Reading
- The Realm Files — Vol 1: Intro to RealmDB — Damien Attoe
- The Realm Files — Vol 2: Physical Structure — Damien Attoe
- The Realm Files — Vol 3: The Realm Header — Damien Attoe
- Realm Forensics Chapter — Mobile Forensics: The File Format Handbook — Springer
- Realm Studio — GUI viewer for Realm databases (not forensic, but useful for orientation)
Happy examining. 🐢