Eye Describe Anatomy

ShimCache: structure, contents and evidential limits

One registry value holding a serialised database. The compatibility cache Windows keeps to avoid re-examining binaries, what each field in a record actually says, and the three things it is routinely claimed to prove that it cannot.

What this page covers

  1. What ShimCache is
  2. The header, and every kind of record
  3. The header identifies which Windows wrote the cache
  4. One record, byte by byte
  5. Where the entries sit, and how they are reached
  6. What an entry supports, and what it does not
  7. How Crow-Eye reads the cache
  8. Getting the cache, and what each route gives
  9. How ShimCache changed across Windows
  10. Reference
  11. Anti-forensic handling, and what survives it

What ShimCache is

Windows carries decades of backwards compatibility. When a program starts, the Application Compatibility engine has to decide whether that binary needs a shim: a small patch that lies to the program about the Windows version, or redirects a path, or fakes an API that no longer behaves the way it did in 2001. Deciding that requires looking at the file, and looking at every file every time would be slow. So Windows keeps a cache of what it has already looked at.

That cache is the artifact. It exists to make compatibility checks fast. It was never designed to be evidence, which is exactly why it is such good evidence: nothing about it is defensive, and nothing about it is documented by Microsoft.

100,394bytes, all in one registry value
456entries in it, against a cap of 1,024
0x34header size, the Windows 10 and 11 marker
10tsrecord signature, once per entry

One value. HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\AppCompatCache holds all of it, and the registry knows nothing about what is inside: to the hive this is a single REG_BINARY blob of 100,394 bytes. Everything below is structure the registry does not describe, which is why ShimCache needs a parser of its own rather than being another key someone reads.

The header, and every kind of record

Read from the AppCompatCache value of the reference capture. The header is what tells a parser which format it is holding; the records are what it finds after it. Five views: the header, a file record, that record's trailing blob expanded into its 12-byte slots, a packaged (Store/UWP) record, and a record whose blob reports a 32-bit image.

Structure is real and measured. Paths that could identify the machine are replaced with a placeholder of exactly the same byte length, so every offset still adds up; stock Windows paths are left as they are.

Live Byte Selection

Full dissection reference

The same fields as a table. Both are drawn from one definition, so the map and the table cannot disagree with each other.

Table 1
OffsetSizeFieldWhat it is

Where these figures come from. The header, the records and the byte maps on this page are read from the AppCompatCache value of a live Windows 11 system - the reference capture. Paths that could identify the machine are replaced by placeholders of exactly the same byte length, so every offset still adds up; stock Windows paths are left as they are. Every offset, size and count is the real measurement, and all of it is regenerated by gen_shimcache_bytemap.py from one definition, so the maps, the hex dump and the tables cannot disagree with each other.

Figure 1

How the five views relate, and how a reader reaches each one

The map above offers five tabs, and they are not five things found in the cache. Two of them are one record read at two depths, three are one skeleton carrying different content, and all of them hang off the header. ShimCache has no pointers: a record is reached only because the one before it declared its own length. Hover a box or a line to read what it is; click a box to open that view in the map above.

The value: a header, then records end to end Header 52BHeader 52Bat 0 record 0record 0at 52 record 1record 1at 308 The header's first DWORD is its own size, 52, which is where record 0 Record 0's cell size is 244, and the field excludes the 12 bytes befor header size cell size + 12 One record, three shapes, one skeleton File record 154BFile record 154B154 bytes One of the three shapes a record can take. They are not different layo Packaged record 198BPackaged record 198B198 bytes One of the three shapes a record can take. They are not different layo 32-bit record 98B32-bit record 98B98 bytes One of the three shapes a record can take. They are not different layo a path no path, FILETIME 0 blob says 32-bit The same record, cut deeper Data slots 154BData slots 154B154 bytes - the same ones The same bytes, read at a different depth. Nothing is re-read from the same bytes What a reader is forced to do, in this order header size → where record 0 starts · signature +0x00 → is this a record at all · path length +0x0C → where the FILETIME, the data size and the blob begin data size → how many slots the blob holds, and only then what architecture and OS-binary flag the record carries Only the first five fields sit at fixed offsets. Everything after the path moves with it, which is why a reader that assumes a fixed record shape does not fail - it reads a timestamp out of the middle of somebody else's path and returns a plausible date. What Windows does with the value internally is not visible in the bytes, and this page does not guess at it. The order above is the one the FORMAT forces on any reader.

The first four bytes are the header length, and that number is the version marker. There is no version string anywhere; the size of the header is how a parser tells one layout from another.

Table 2
HeaderWindowsRecord shape
0x30Windows 7 and Server 2008 R2fixed width, 512 entries
0x34Windows 10 and 11variable width, 10ts signed, 1,024 entries

The reference system reports 0x34. Read a 0x34 cache with the Windows 7 layout and nothing raises: the fixed-width reader walks off into the middle of a path and returns entries that look like entries. That is the failure this format invites, and it is why the header check comes first.

Nothing in the header is the entry count

Two real captures settle it. On one, the dword at offset 4 reads 8,395 while walking the records finds 1,024; on the other it reads 0 while the cache holds 456. Every dword in the header was checked against both the record count and the value size on both captures, and none of them matched either.

So there is no count to trust, and a parser that invents one from the header is reporting a number the artifact never stated. The only honest count is the one obtained by walking the records - stepping by each record's own cell size, and stopping where the signatures stop.

One record, byte by byte

One entry from the reference capture, at offset 0xE956 of the value. The path is replaced with a placeholder of the same length, so every offset below is still the real one. These are the same bytes the map above is drawn from - one definition, so the two cannot disagree.

Click any byte, or any row of the table below, to read the field it belongs to.

Live Byte Selection

Where each field sits

Table 3
OffsetSizeFieldWhat it is
0x004Signature "10ts"10ts, little-endian 0x73743031
0x044Record id0x7A1AA073 - different on every record
0x084Cell size142, counted from offset 12, so the record is 12 + 142 = 154 bytes
0x0C2Path length44 bytes, which is 22 UTF-16 characters
0x0E44Path (UTF-16LE)UTF-16LE, no terminator
0x3A8Last modified (FILETIME)FILETIME, the file's mtime
0x424Data size84 bytes follow
0x4684Data7 slots of 12 bytes - tag, type, value

The arithmetic closes: 14 bytes of fixed fields, 44 of path, 8 of FILETIME, 4 of data length and 84 of data blob gives 154, and the cell size field says 142 counted from offset 12, which is 12 + 142 = 154. A layout that adds up is the cheapest verification a parser has, and one that does not is a layout somebody guessed.

Where the entries sit, and how they are reached

Figure 2

Where the entries sit in the value

Every one of the 456 entries on the reference capture, at its real offset and its real length. The header first, then the records end to end - there is no index between them and no padding.

The value, to scale the 52-byte header file at 52, 256 bytes file at 308, 242 bytes file at 550, 206 bytes packaged app at 756, 248 bytes packaged app at 1,004, 272 bytes file at 1,276, 342 bytes file at 1,618, 268 bytes file at 1,886, 158 bytes file at 2,044, 346 bytes file at 2,390, 178 bytes file at 2,568, 246 bytes packaged app at 2,814, 226 bytes file at 3,040, 188 bytes file at 3,228, 206 bytes file at 3,434, 360 bytes file at 3,794, 166 bytes file at 3,960, 262 bytes file at 4,222, 234 bytes file at 4,456, 262 bytes packaged app at 4,718, 238 bytes file at 4,956, 254 bytes packaged app at 5,210, 262 bytes file at 5,472, 160 bytes file at 5,632, 322 bytes file at 5,954, 310 bytes file at 6,264, 318 bytes file at 6,582, 318 bytes file at 6,900, 310 bytes file at 7,210, 328 bytes packaged app at 7,538, 274 bytes file at 7,812, 336 bytes packaged app at 8,148, 298 bytes file at 8,446, 250 bytes file at 8,696, 218 bytes file at 8,914, 222 bytes file at 9,136, 210 bytes file at 9,346, 214 bytes file at 9,560, 214 bytes file at 9,774, 288 bytes file at 10,062, 200 bytes file at 10,262, 162 bytes file at 10,424, 256 bytes file at 10,680, 164 bytes file at 10,844, 288 bytes file at 11,132, 304 bytes packaged app at 11,436, 236 bytes file at 11,672, 256 bytes packaged app at 11,928, 252 bytes file at 12,180, 292 bytes packaged app at 12,472, 276 bytes file at 12,748, 180 bytes file at 12,928, 282 bytes packaged app at 13,210, 200 bytes file at 13,410, 240 bytes packaged app at 13,650, 276 bytes file at 13,926, 240 bytes file at 14,166, 230 bytes packaged app at 14,396, 202 bytes file at 14,598, 228 bytes packaged app at 14,826, 226 bytes file at 15,052, 174 bytes file at 15,226, 320 bytes file at 15,546, 304 bytes file at 15,850, 182 bytes file at 16,032, 178 bytes file at 16,210, 166 bytes file at 16,376, 152 bytes file at 16,528, 238 bytes file at 16,766, 230 bytes file at 16,996, 192 bytes file at 17,188, 140 bytes file at 17,328, 246 bytes file at 17,574, 240 bytes file at 17,814, 204 bytes file at 18,018, 332 bytes file at 18,350, 304 bytes file at 18,654, 140 bytes file at 18,794, 188 bytes file at 18,982, 218 bytes packaged app at 19,200, 232 bytes packaged app at 19,432, 256 bytes packaged app at 19,688, 240 bytes file at 19,928, 112 bytes packaged app at 20,040, 264 bytes file at 20,304, 212 bytes packaged app at 20,516, 236 bytes packaged app at 20,752, 260 bytes packaged app at 21,012, 222 bytes file at 21,234, 194 bytes packaged app at 21,428, 246 bytes file at 21,674, 260 bytes file at 21,934, 294 bytes file at 22,228, 110 bytes file at 22,338, 288 bytes file at 22,626, 256 bytes file at 22,882, 254 bytes file at 23,136, 184 bytes file at 23,320, 174 bytes file at 23,494, 174 bytes file at 23,668, 238 bytes packaged app at 23,906, 232 bytes file at 24,138, 264 bytes file at 24,402, 236 bytes file at 24,638, 232 bytes packaged app at 24,870, 256 bytes file at 25,126, 264 bytes file at 25,390, 196 bytes file at 25,586, 124 bytes file at 25,710, 180 bytes file at 25,890, 202 bytes packaged app at 26,092, 244 bytes packaged app at 26,336, 226 bytes file at 26,562, 268 bytes file at 26,830, 290 bytes packaged app at 27,120, 262 bytes file at 27,382, 294 bytes file at 27,676, 260 bytes packaged app at 27,936, 308 bytes file at 28,244, 258 bytes packaged app at 28,502, 244 bytes file at 28,746, 274 bytes packaged app at 29,020, 268 bytes file at 29,288, 268 bytes file at 29,556, 280 bytes file at 29,836, 162 bytes file at 29,998, 160 bytes file at 30,158, 220 bytes file at 30,378, 164 bytes file at 30,542, 144 bytes file at 30,686, 162 bytes packaged app at 30,848, 266 bytes file at 31,114, 288 bytes packaged app at 31,402, 290 bytes file at 31,692, 324 bytes file at 32,016, 274 bytes file at 32,290, 306 bytes file at 32,596, 158 bytes file at 32,754, 236 bytes file at 32,990, 266 bytes file at 33,256, 236 bytes file at 33,492, 158 bytes file at 33,650, 200 bytes file at 33,850, 186 bytes file at 34,036, 342 bytes file at 34,378, 208 bytes file at 34,586, 184 bytes file at 34,770, 192 bytes file at 34,962, 360 bytes file at 35,322, 280 bytes file at 35,602, 160 bytes file at 35,762, 304 bytes file at 36,066, 218 bytes file at 36,284, 214 bytes file at 36,498, 202 bytes file at 36,700, 170 bytes file at 36,870, 258 bytes file at 37,128, 220 bytes file at 37,348, 172 bytes file at 37,520, 172 bytes file at 37,692, 106 bytes file at 37,798, 142 bytes file at 37,940, 152 bytes file at 38,092, 172 bytes file at 38,264, 160 bytes file at 38,424, 164 bytes file at 38,588, 228 bytes file at 38,816, 190 bytes file at 39,006, 162 bytes file at 39,168, 162 bytes packaged app at 39,330, 236 bytes packaged app at 39,566, 200 bytes file at 39,766, 174 bytes file at 39,940, 260 bytes file at 40,200, 266 bytes file at 40,466, 240 bytes file at 40,706, 154 bytes file at 40,860, 232 bytes file at 41,092, 204 bytes file at 41,296, 224 bytes file at 41,520, 178 bytes file at 41,698, 180 bytes file at 41,878, 226 bytes file at 42,104, 244 bytes file at 42,348, 168 bytes file at 42,516, 328 bytes file at 42,844, 176 bytes file at 43,020, 254 bytes file at 43,274, 224 bytes file at 43,498, 360 bytes file at 43,858, 162 bytes file at 44,020, 208 bytes file at 44,228, 220 bytes file at 44,448, 312 bytes file at 44,760, 202 bytes file at 44,962, 244 bytes file at 45,206, 240 bytes file at 45,446, 216 bytes file at 45,662, 326 bytes file at 45,988, 248 bytes file at 46,236, 362 bytes file at 46,598, 358 bytes file at 46,956, 226 bytes file at 47,182, 362 bytes file at 47,544, 246 bytes file at 47,790, 322 bytes file at 48,112, 246 bytes file at 48,358, 342 bytes file at 48,700, 282 bytes file at 48,982, 220 bytes file at 49,202, 302 bytes file at 49,504, 160 bytes file at 49,664, 160 bytes file at 49,824, 326 bytes file at 50,150, 338 bytes file at 50,488, 154 bytes file at 50,642, 316 bytes file at 50,958, 304 bytes file at 51,262, 344 bytes file at 51,606, 164 bytes file at 51,770, 178 bytes file at 51,948, 106 bytes packaged app at 52,054, 244 bytes packaged app at 52,298, 268 bytes packaged app at 52,566, 250 bytes packaged app at 52,816, 244 bytes packaged app at 53,060, 248 bytes packaged app at 53,308, 266 bytes packaged app at 53,574, 252 bytes packaged app at 53,826, 252 bytes packaged app at 54,078, 252 bytes packaged app at 54,330, 252 bytes packaged app at 54,582, 246 bytes file at 54,828, 162 bytes file at 54,990, 164 bytes file at 55,154, 168 bytes file at 55,322, 108 bytes file at 55,430, 114 bytes file at 55,544, 158 bytes file at 55,702, 332 bytes packaged app at 56,034, 220 bytes file at 56,254, 214 bytes file at 56,468, 200 bytes file at 56,668, 170 bytes file at 56,838, 176 bytes file at 57,014, 228 bytes file at 57,242, 160 bytes file at 57,402, 134 bytes file at 57,536, 220 bytes file at 57,756, 256 bytes file at 58,012, 252 bytes file at 58,264, 172 bytes file at 58,436, 172 bytes packaged app at 58,608, 248 bytes packaged app at 58,856, 284 bytes file at 59,140, 264 bytes file at 59,404, 268 bytes file at 59,672, 294 bytes file at 59,966, 272 bytes file at 60,238, 244 bytes packaged app at 60,482, 232 bytes packaged app at 60,714, 242 bytes packaged app at 60,956, 244 bytes packaged app at 61,200, 222 bytes packaged app at 61,422, 260 bytes file at 61,682, 198 bytes file at 61,880, 164 bytes file at 62,044, 256 bytes file at 62,300, 158 bytes file at 62,458, 160 bytes file at 62,618, 172 bytes file at 62,790, 162 bytes file at 62,952, 168 bytes file at 63,120, 206 bytes file at 63,326, 210 bytes file at 63,536, 214 bytes file at 63,750, 218 bytes file at 63,968, 166 bytes file at 64,134, 162 bytes file at 64,296, 160 bytes file at 64,456, 150 bytes file at 64,606, 162 bytes file at 64,768, 170 bytes file at 64,938, 170 bytes file at 65,108, 254 bytes file at 65,362, 162 bytes file at 65,524, 164 bytes file at 65,688, 252 bytes file at 65,940, 184 bytes file at 66,124, 228 bytes packaged app at 66,352, 262 bytes file at 66,614, 200 bytes file at 66,814, 158 bytes file at 66,972, 222 bytes file at 67,194, 162 bytes file at 67,356, 186 bytes file at 67,542, 190 bytes file at 67,732, 156 bytes file at 67,888, 160 bytes file at 68,048, 182 bytes file at 68,230, 164 bytes file at 68,394, 194 bytes file at 68,588, 192 bytes file at 68,780, 190 bytes file at 68,970, 180 bytes file at 69,150, 198 bytes file at 69,348, 168 bytes file at 69,516, 178 bytes file at 69,694, 168 bytes file at 69,862, 178 bytes file at 70,040, 94 bytes file at 70,134, 136 bytes file at 70,270, 230 bytes file at 70,500, 162 bytes file at 70,662, 254 bytes packaged app at 70,916, 268 bytes file at 71,184, 158 bytes file at 71,342, 168 bytes file at 71,510, 346 bytes file at 71,856, 100 bytes file at 71,956, 216 bytes packaged app at 72,172, 218 bytes packaged app at 72,390, 242 bytes file at 72,632, 160 bytes file at 72,792, 178 bytes file at 72,970, 184 bytes file at 73,154, 162 bytes file at 73,316, 170 bytes file at 73,486, 180 bytes file at 73,666, 156 bytes file at 73,822, 178 bytes file at 74,000, 126 bytes file at 74,126, 302 bytes file at 74,428, 176 bytes file at 74,604, 174 bytes file at 74,778, 180 bytes file at 74,958, 170 bytes file at 75,128, 158 bytes file at 75,286, 160 bytes file at 75,446, 162 bytes file at 75,608, 204 bytes file at 75,812, 176 bytes file at 75,988, 222 bytes file at 76,210, 174 bytes file at 76,384, 156 bytes packaged app at 76,540, 238 bytes file at 76,778, 238 bytes file at 77,016, 266 bytes file at 77,282, 250 bytes packaged app at 77,532, 242 bytes file at 77,774, 158 bytes packaged app at 77,932, 212 bytes file at 78,144, 218 bytes packaged app at 78,362, 236 bytes file at 78,598, 162 bytes file at 78,760, 172 bytes file at 78,932, 172 bytes file at 79,104, 174 bytes file at 79,278, 162 bytes file at 79,440, 212 bytes file at 79,652, 214 bytes file at 79,866, 216 bytes file at 80,082, 212 bytes file at 80,294, 210 bytes file at 80,504, 218 bytes file at 80,722, 222 bytes file at 80,944, 208 bytes file at 81,152, 210 bytes file at 81,362, 214 bytes file at 81,576, 212 bytes file at 81,788, 218 bytes file at 82,006, 214 bytes file at 82,220, 214 bytes file at 82,434, 218 bytes file at 82,652, 218 bytes file at 82,870, 208 bytes file at 83,078, 162 bytes file at 83,240, 170 bytes file at 83,410, 170 bytes file at 83,580, 172 bytes file at 83,752, 178 bytes file at 83,930, 162 bytes file at 84,092, 182 bytes file at 84,274, 174 bytes file at 84,448, 172 bytes file at 84,620, 172 bytes file at 84,792, 168 bytes file at 84,960, 170 bytes file at 85,130, 170 bytes file at 85,300, 168 bytes file at 85,468, 168 bytes file at 85,636, 170 bytes file at 85,806, 194 bytes file at 86,000, 178 bytes file at 86,178, 160 bytes file at 86,338, 254 bytes packaged app at 86,592, 246 bytes packaged app at 86,838, 270 bytes file at 87,108, 190 bytes file at 87,298, 160 bytes file at 87,458, 218 bytes file at 87,676, 226 bytes packaged app at 87,902, 306 bytes file at 88,208, 152 bytes file at 88,360, 248 bytes file at 88,608, 232 bytes file at 88,840, 216 bytes file at 89,056, 234 bytes file at 89,290, 122 bytes file at 89,412, 346 bytes file at 89,758, 180 bytes file at 89,938, 198 bytes packaged app at 90,136, 290 bytes file at 90,426, 164 bytes packaged app at 90,590, 226 bytes packaged app at 90,816, 260 bytes packaged app at 91,076, 232 bytes packaged app at 91,308, 250 bytes packaged app at 91,558, 266 bytes file at 91,824, 184 bytes file at 92,008, 178 bytes file at 92,186, 346 bytes file at 92,532, 170 bytes file at 92,702, 168 bytes file at 92,870, 172 bytes file at 93,042, 172 bytes file at 93,214, 186 bytes file at 93,400, 164 bytes file at 93,564, 200 bytes packaged app at 93,764, 266 bytes packaged app at 94,030, 286 bytes file at 94,316, 172 bytes file at 94,488, 236 bytes file at 94,724, 252 bytes file at 94,976, 244 bytes file at 95,220, 272 bytes file at 95,492, 238 bytes packaged app at 95,730, 226 bytes packaged app at 95,956, 236 bytes packaged app at 96,192, 260 bytes packaged app at 96,452, 268 bytes packaged app at 96,720, 240 bytes packaged app at 96,960, 264 bytes packaged app at 97,224, 202 bytes packaged app at 97,426, 232 bytes file at 97,658, 242 bytes file at 97,900, 196 bytes file at 98,096, 236 bytes file at 98,332, 230 bytes packaged app at 98,562, 246 bytes file at 98,808, 290 bytes packaged app at 99,098, 270 bytes file at 99,368, 152 bytes file at 99,520, 160 bytes file at 99,680, 230 bytes packaged app at 99,910, 230 bytes packaged app at 100,140, 254 bytes 0 100,394 456 entries, end to end, no gaps and no index - 372 of them file records and 84 packaged. Lengths run 94 to 362 bytes, median 218: an entry is as long as its path, which is why nothing here is a fixed stride.
file record - has a pathpackaged app - has nonethe 52-byte header
Table 4

What Crow-Eye stores, and where each column comes from

One row per entry in shimcache_entries. The From column says where the value was read from rather than who produced it: record +0x08 is a field at that offset in the byte map above; record, after the path is a field whose offset moves with the path length and therefore has no constant to give; the data slots is the trailing blob decoded as triples; the walk is where the reader had got to; and bookkeeping has no origin in the cache at all. Filled on is measured against the reference capture - several columns exist for only one of the two record shapes, and the table says so rather than implying every column is always there.

ColumnFromTypeWhat it meansFilled on
idbookkeeping-The database's own row number. It orders nothing and means nothing outside this table.every row
filenamefrom pathtextThe last component of the path. For a packaged app there is no path, so this carries the package identity instead - which is why it is filled on every row while path is not.every row
pathrecord +0x0EUTF-16LEThe full path of the binary, as the cache recorded it. Its length is the field immediately before it, and every field after it moves accordingly.372 of 456 rows
entry_typefrom pathfile / packaged appWhich of the two record shapes this was. A record with no path is a packaged (Store/UWP) application, and the distinction is what stops a packed package name being read as a file path.every row
package_family_namefrom raw_entrytextThe package family, split out of the packaged record's own payload. Left whole it sits in the path column and correlation against Prefetch or AmCache can never match the same program.84 of 456 rows
package_versionfrom raw_entrytextThe package version, from the same payload.84 of 456 rows
architecturethe data slotsx64 / x86Decoded from the machine-type slot in the record's trailing data. For a packaged app it is the package's declared architecture instead, which is not the same thing as the image's and is deliberately not overwritten.386 of 456 rows
raw_entryrecord +0x46textThe packaged record's payload, kept as the tab-separated fields it was read as. Present only on packaged records.84 of 456 rows
last_modifiedrecord, after the pathFILETIMEThe FILETIME the record carries - the binary's modification time, not a run time. It follows the path, so it has no fixed offset.372 of 456 rows
last_modified_readablefrom last_modifiedtextThe same timestamp rendered for display, and the literal string Unknown where there is none - which is why it is filled on every row while last_modified is not.every row
data_sizerecord, after the pathUInt32How many bytes of trailing data the record declares. It follows the FILETIME, so its offset moves too.every row
entry_sizerecord +0x08UInt32The cell size the record declares, plus the 12 bytes that field excludes - signature, record id and itself. This is the number the walk adds to reach the next record, and the only correct way to do so.every row
cache_entry_positionthe walkfile offsetWhere in the value this record began. It is the row's provenance: the claim can be checked by going back to that offset in the evidence.every row
cache_indexthe walkUInt32How many records the walk had already passed. ShimCache is ordered most-recent-first, so this is the only ordering the artifact carries - and it is a position, never a time.every row
record_idrecord +0x04UInt32A per-record identifier. Measured across a live cache it was unique on every single record, which is what makes it usable as an identity.every row
shim_flagsthe data slotstag=value listThe record's trailing data, decoded as 12-byte (tag, kind, value) triples and written out one part per slot. Two separate slots carry the machine type and agreed on every record measured.every row
entry_hashfrom path, raw_entry, last_modified, data_size and cache_entry_positionhashThe dedup key, and the reason packaged apps can be told apart at all: they share an empty path, so 209 of them would have collided under the old UNIQUE(path, last_modified).every row
parsed_atbookkeeping-When Crow-Eye read the cache. Bookkeeping - it belongs on no timeline and describes the examination, not the evidence.every row

What an entry supports, and what it does not

It is not an execution artifact

This is the most common misuse of ShimCache in a report. An entry means the compatibility engine looked at the file. That happens on execution, and it also happens when a file is enumerated - browsing a directory in Explorer can create entries for binaries nobody ran. The entry proves the file existed at that path and that Windows saw it. It does not prove anybody ran it.

The timestamp is the file's, not the event's

The FILETIME in a record is the file's last modification time from the file system. It is not when the file ran, and it is not when the entry was made. A binary whose timestamps were altered before it was dropped carries the altered time straight into the cache, and the cache will repeat it confidently.

It is written at shutdown, so a live machine's copy is stale

On Windows 10 and 11 the cache is held in memory and flushed to the registry when the system shuts down. Everything since the last clean shutdown exists only in RAM. A hive pulled from a running machine - which is the normal case in an incident - has a ShimCache that stops at the last reboot, and a machine that crashed may never have flushed at all.

What it is genuinely good for: the order. Entries sit in most-recently-examined order, so two adjacent entries were seen in the same window even if their recorded timestamps are years apart. Position carries information no timestamp in the record does, and it survives timestomping because an attacker who alters a file's mtime does not thereby move it in the cache.

How Crow-Eye reads the cache

ShimCache gets its own parser rather than being handled by the registry parser, and the reason is structural: this is not a key with values. It is one value holding a serialised database with its own header, its own version signature and its own record layout. The registry is only the envelope it arrived in. The same is true of its versioning - the record shape tracks the Windows release, not the hive format - so a change in Windows 11 breaks ShimCache parsing and nothing else.

Crow-Eye reads the cache two ways, and shares the format code between them. On a live machine it reads the value through the registry API; against a collected image or a hive file it opens SYSTEM directly, replaying the transaction logs first so it sees the recovered state rather than the flushed one. Only the source of the bytes differs - both hand the same blob to the same walk, so the two paths cannot drift into giving different answers. Both also read every control set present, not just CurrentControlSet: an older set can hold entries the current one has already aged out. The registry internals guide covers how that file is laid out and why a hive read from a running system may be mid-transaction.

Getting the cache, and what each route gives

ShimCache is one registry value, so acquiring it looks trivial and is not. What a reader ends up holding depends on how it was taken, and the routes do not return the same thing. The value is at HKLM\SYSTEM\{ControlSet}\Control\Session Manager\AppCompatCache, under every control set present rather than only the current one.

Table 5
RouteWhat it holdsWhat it misses
The live registry, through the API What was flushed at the last clean shutdown. Everything since. On a machine that has not shut down cleanly it can be empty - see below.
A SYSTEM hive from an image or a collection The same flushed state, plus whatever the transaction logs recover. Crow-Eye replays those logs before reading. The in-memory cache, which was lost with the machine.
Older control sets, and hives from shadow copies Entries the current set has already aged out. This is the cheapest way to reach past the 1,024-entry cap. Nothing the current set has that they do not - read both.
A memory image The complete cache on a running machine, including everything since the last shutdown. Crow-Eye does not read this. It is named here because on a live incident it is the only copy that is current.

An empty cache is not evidence of tampering

The page above says a live machine's copy is stale. It can also be empty, which is a different thing and invites a worse mistake. Read from this reference system while writing this section, the value came back as 52 bytes - a complete 0x34 header and not one record after it. The header is not blank either: the DWORD at 0x10 reads 84, which is one of the fields the reference above records as observed rather than explained.

Live Byte Selection

That is a well-formed cache containing nothing. It happens because the registry copy is written at shutdown and the in-memory cache had not been flushed to it. An examiner who finds a header and no records has found a machine whose cache has not been written since it was last cleared - not, on that evidence alone, a machine somebody wiped. The two look identical in the registry, and the difference has to come from somewhere else.

The database keeps what the cache forgets

ShimCache evicts. The cache holds at most 1,024 entries and drops the oldest to make room, so an entry seen in one collection may be absent from the next. Crow-Eye's table does not follow it down: each parse inserts only records whose entry_hash is new and leaves everything already stored in place, so the table accumulates across parses and can hold more than the cache currently does. Two collections a month apart merge rather than replace.

That is deliberate, and it is why the schema is extended with ALTER TABLE rather than rebuilt: dropping the table to gain a column would discard every entry that has since aged out of the cache, and those are exactly the ones no second collection can recover.

What settles a question ShimCache cannot

An entry says a file existed at a path and that Windows looked at it. Almost every question worth asking needs a second artifact, and which one depends on the question.

Table 6
The questionRead it withWhat the pair settles
Did it run? Prefetch, or event log 4688 ShimCache cannot answer this at all on Windows 10 or 11. Prefetch records a run and counts it; 4688 records a process creation with its parent.
Was the file catalogued, or only seen? AmCache AmCache carries a SHA-1 and an install association. A binary in both was seen twice by two different mechanisms; one in ShimCache alone was looked at and never inventoried.
Is the recorded timestamp true? The MFT The record carries the file's $STANDARD_INFORMATION modification time. A disagreement with $FILE_NAME is the classic timestomping indicator, and ShimCache repeats the altered value without complaint.
Is the file still there? The file system, or the USN journal A path in the cache with no file at the other end is common and is often the most interesting row on the table. The journal says whether it was deleted and when.
When, relative to what? ShimCache itself Position is the artifact's own answer. Entries sit in most-recently-examined order, so two adjacent entries were seen in one window whatever their recorded timestamps say - and an attacker who alters an mtime does not thereby move a record.

How ShimCache changed across Windows

Each row below records a change that alters what a reader may conclude, not only how the bytes are arranged. A parser written against one Windows release returns nothing against another, and does so without raising an error. Crow-Eye parses the Windows 10 and 11 format only. A cache whose header is one of the older magics below is identified by name and returns no entries - it is refused rather than guessed at, because handing an unknown layout to the Windows 10 walk returns whatever a byte scan happens to find. An empty ShimCache table from a Windows 7 image means the format is not supported, and the parser says so on the console rather than only in the count.

Table 7
WindowsWhat changedWhat it means for a reader
XPFixed-width records, magic 0xDEADBEEFFixed size, so the walk is trivial - and capped at 96 entries, a fraction of what a modern cache holds.
Vista and 7Magic 0xBADC0FFE (Vista) and 0xBADC0FEE (7), 1,024 entriesWindows 7 carries an execution flag. This is the build the 'ShimCache proves execution' claim came from, and it was true here.
8Per-record 00ts signature appearsRecords become variable length, with the path inside the record.
10 and 11Header 0x34, per-record 10tsThe execution flag is gone and nothing replaced it. Every modern citation of ShimCache as execution evidence is carrying a Windows 7 fact forward by a decade.

Reference

Header size is the version

Table 8
HeaderDecimalWindows
0x3452Windows 10 and 11 - the header in the map above
0x3048Windows 10, earlier builds
0x80128Windows 8 and 8.1
0xDEADBEEF-Windows XP - a magic, not a size; a different layout entirely
0xBADC0FFE-Windows Vista and Server 2008
0xBADC0FEE-Windows 7 and Server 2008 R2

What a record does and does not carry

Table 9
FieldPresentMeans
PathalwaysThe file the shim engine examined.
Last modifiedalwaysThe FILE's timestamp, not the cache's.
Execution flagWindows 7 onlyAbsent on Windows 8 and later.
Run countneverShimCache has never recorded one on any version.
Execution timeneverNothing in the record says when anything ran.

Anti-forensic handling, and what survives it

The preceding sections describe what an intact cache supports. This section describes the cache after an attempt to remove or evade it, which is the condition in which it is frequently encountered. In most rows the attempt leaves a trace of its own.

Table 10
TechniqueWhat is doneWhat survives
Clearing the valueAppCompatCache is deleted or zeroed.The value is rebuilt at the next shutdown, so an empty or very short cache on a long-running machine is itself the finding.
Shutdown timingPulling power rather than shutting down cleanly.Not anti-forensics but has the same effect: the cache is written at shutdown, so a machine seized running has a cache that stops at the last clean shutdown. Everything since is missing and nothing says so.
Renaming the binaryThe file is moved or renamed after running.The old path stays in the cache. A path here with no file at the other end is common and often the most interesting row on the table.
Cache pressureRunning many binaries to push older entries out.The cap is 1,024. Eviction is by position, so flooding the cache does evict history - and leaves 1,024 entries whose timestamps cluster in a way that normal use does not produce.