Skip to content
Back to Blog

Windows 10/11 ShimCache `10ts`: the 1-byte trap that silently returns zero records

u
unJaena Team
May 1, 20268 min read
Windows 10/11 ShimCache `10ts`: the 1-byte trap that silently returns zero records

Windows 10/11 ShimCache 10ts: the 1-byte trap that silently returns zero records#

TL;DR#

If your forensic pipeline parses the AppCompatCache registry value with a 3-byte b"sts" signature search, it returns zero records on every Windows 10 1607+ image. The actual magic is the 4-byte b"10ts" (0x31 0x30 0x74 0x73). We caught this against four real Windows E01 images during real-data differential testing — synthetic fixtures had been passing for months. The fix is six lines and one comment. The diagnostic — five minutes with xxd and Eric Zimmerman's AppCompatCacheParser.

This post walks through (1) what ShimCache is and why this fails so silently, (2) the exact byte-layout mistake, (3) the second smaller mistake right next to it (path_size in BYTES, not WCHAR count), (4) how to verify your own parser, and (5) a methodology argument for testing parsers against real images, not synthetic blobs.

What ShimCache is, and why a silent failure is dangerous#

ShimCache (formally the Application Compatibility Cache, stored in the registry under SYSTEM\ControlSet00x\Control\Session Manager\AppCompatCache) is one of the most useful execution-evidence sources Windows leaves behind:

  • It records the full path and $STANDARD_INFORMATION last-modified timestamp of executables the shim engine has examined.
  • An entry can persist even after the binary is deleted — making ShimCache a primary "yes, this thing ran on this machine" signal during incident response and insider-threat work.
  • It survives reboots, gets flushed to the registry on shutdown, and is parseable offline from a SYSTEM hive copy.

The canonical reference for the binary layout is Mandiant's whitepaper Leveraging the AppCompatCache for Forensic Analysis, updated by community work from Eric Zimmerman (AppCompatCacheParser) and Andrew Davis. The format has changed several times — XP/2003, Win7/Server 2008R2, Win8/8.1, Win10 RTM, and then again in Windows 10 1607 (Anniversary Update, 2016) which is the layout that has been stable ever since.

The dangerous thing about a parser bug here is that it fails silently. Your code:

  1. Opens the hive ✓
  2. Finds the AppCompatCache value ✓
  3. Reads the bytes ✓
  4. Iterates the entry loop, finds zero matching signatures ✓
  5. Returns []

To downstream consumers (timeline, IR pivot tables, MITRE ATT&CK T1518.001 detections), this looks identical to "the box has no execution evidence" — i.e., to a clean machine. You ship a report saying "no program-execution evidence recovered." There is no exception, no log line, no coverage gap warning.

The bug, in 8 bytes#

Here is the entry header for a single Win10 1607+ ShimCache record (per Mandiant + Zimmerman):

OffsetBytesField
+0x0031 30 74 73DWORD signature ("10ts" ASCII)
+0x04xx xx xx xxDWORD unknown / padding
+0x08xx xx xx xxDWORD entryDataSize
+0x0Cxx xxWORD pathSize (BYTES, not WCHAR)
+0x0E<pathSize bytes>path (UTF-16LE, no null terminator)
+...xx xx xx xx xx xx xx xxFILETIME lastModified
+...xx xx xx xxDWORD dataSize
+...<dataSize bytes>data (last DWORD == 1 → executed)

The signature is b"10ts" — bytes 0x31 0x30 0x74 0x73.

The bug: the original code searched for the 3-byte substring b"sts" (bytes 0x73 0x74 0x73). Note that b"sts" does not appear inside b"10ts" — there is only one s byte in 10ts, and it is at the end. So the search walks the entire blob and never matches. Entry loop exits with entries = []. Game over.

How does a 3-byte sts end up in a Win10 parser at all? Most likely it was copied from an older reference that abbreviated the trailing-3-bytes-of-some-magic in a Python bytes literal. The result compiles, lints, type-checks, and unit-tests against any fixture that does not actually contain real Windows 10 magic bytes. Nothing complains until you put a real hive in front of it.

The fix is straightforward:

python
# 4-byte magic per Mandiant / Zimmerman AppCompatCacheParser:
# Win10 1607+ uses ASCII "10ts" (0x31 0x30 0x74 0x73)
sig = data[offset:offset+4]
if sig != b'10ts':
    next_sig = data.find(b'10ts', offset)
    if next_sig == -1:
        break
    offset = next_sig

Two things to call out: (a) the comparison is 4 bytes, not 3; (b) when there is no signature at the current offset, we use bytes.find to jump to the next candidate rather than incrementing by 1 — this absorbs any post-entry padding without rescanning byte-by-byte.

The second bug, hidden in path_size#

Right after the magic-byte fix, we found a second issue. The path_size field at offset +0x0C is a WORD — 2 bytes, little-endian. In some older references it is documented as "path length in WCHARs" — i.e., character count, multiply by 2 to get byte length. In the actual Win10 1607+ layout, path_size is the byte length already. Multiplying by 2 walks past the end of the entry into the next record (or past the buffer) and triggers a UnicodeDecodeError or an offset overrun, which the entry loop catches and skips — silently.

Mandiant's whitepaper is unambiguous on this — the field is documented as bytes — but if you have ever cross-referenced two write-ups that disagree on units, you know how easy it is to ship the wrong one.

python
# WORD path_size, in BYTES — do NOT multiply by 2 even though
# the path is UTF-16LE. Cross-checked against Eric Zimmerman's
# AppCompatCacheParser source.
path_len = struct.unpack('<H', data[offset:offset+2])[0]
offset += 2
path = data[offset:offset+path_len].decode('utf-16le', errors='ignore').rstrip('\x00')
offset += path_len

How we caught it: real-image differential testing#

We had been validating ShimCache against synthetic fixtures (deterministic blobs we hand-craft to match the documented layout). Synthetic fixtures pass. They will always pass — we wrote them to match the parser, not the real OS.

The bug surfaced when we ran our parser against four real Windows E01 images side-by-side with Plaso (the reference parser suite maintained by the Google DFIR team). Image sizes ranged from 13 GB to 23 GB, all Windows 10 1607+ or Windows 11. All four had a populated AppCompatCache value. Plaso returned records on three of the four (the fourth was empty, which Plaso and our parser agreed on). Our parser returned zero records on all four, including the three where Plaso clearly found data.

The diagnostic took five minutes once we had the data. Dump the registry value bytes and look for the magic:

text
# illustrative hexdump of an AppCompatCache value, header 0x34
00000000: 3400 0000 0000 0000 0000 0000 0000 0000  4...............
00000010: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000030: 0000 0000 3130 7473 b2c8 7e88 e0e0 0000  ....10ts..~.....

Bytes 31 30 74 73 ("10ts") at offset 0x34. That is the magic. It is not "sts".

Once the magic-byte and path_size fixes landed, the three populated images parsed cleanly. The fourth (legitimately empty) returned zero records, agreeing with Plaso — the Plaso differential test verdict was MATCH ("both tools produced zero events — consistent"), which is what you want from a Daubert-defensible "we agree with the industry reference" stance.

We also wrote a synthetic regression that builds a Win10 entry exactly per the Mandiant + Zimmerman layout (4-byte 10ts magic, BYTES path_size, FILETIME, optional data segment with execution flag). It now produces 3/3 entries with the right paths and the right executed boolean — a guard against regressing back to a broken signature.

Verifying your own parser in five minutes#

If you have any in-house ShimCache code (or you have forked an older OSS parser), here is a five-minute self-check:

  1. Take a SYSTEM hive from any Windows 10 1607+ box you legally own (your own laptop is fine).
  2. Extract the AppCompatCache registry value bytes with reglookup, python-registry, or regf-tools.
  3. xxd it. Look at offset 0x30 or 0x34. If you see 31 30 74 73 ("10ts"), your hive uses the modern layout.
  4. Run your parser against the same hive. If you get zero records but step 3 found 10ts, your signature search is broken.
  5. Run Eric Zimmerman's AppCompatCacheParser against the same hive. Your record count should match within a few entries (some parsers handle the very last truncated entry differently — small differences are fine, order-of-magnitude differences are not).

If you ship forensic tooling and you have never run step 5 against a real hive, you have homework.

The methodology argument#

We caught six schema-drift bugs in one round of real-image testing. ShimCache 10ts was one of them; the others were spread across modern Android (a WhatsApp chat → jid join that silently dropped 230 messages, a Telegram messages → messages_v2 table rename, a Contacts schema split across raw_contacts, and an Android 11+ packages.xml ABX-format change) and modern iOS (an iOS 17 Safari hi.title → hv.title column move). Every one of these passed against synthetic fixtures. Every one of these would have shipped to production undetected if we had skipped real-image testing.

The recurring failure mode is the same: real OS schemas drift between releases, fixture writers do not get the memo, the parser keeps "passing" the only tests that exist, and the silent failure looks identical to "no evidence on this device." This is the worst kind of bug for forensics work — wrong answer with high confidence.

The take-away is not "do not write synthetic fixtures." Synthetic fixtures are great for unit-testing the parser against the layout you think exists. But they have to be paired with real-image differential testing against an industry-reference tool — Plaso for the cross-platform set, Eric Zimmerman's tooling for Windows registry, the relevant mobile reference for iOS/Android. If your build does not include a real-image diff lane, you are flying without the second instrument.

Where this lives#

This fix shipped in unjaena-collector v2.4.0 along with the matching server-side parser. The collector is AGPL-3.0; the relevant before/after diff is documented inline in the comments above. If you are tracking similar bugs, Mandiant's Leveraging the AppCompatCache for Forensic Analysis whitepaper and Zimmerman's repo remain the two most reliable references for the binary layout.

If you find another schema drift in an artifact we cover, open an issue — we would rather hear about it than ship another silent zero.

Share

Get AI forensics insights

Receive posts on AI traces, AI coding traces, browser artifacts, and artifact analysis.

Subscribe to Insights