Building a Jump List Timeline When Someone Tried to Hide It
Most write-ups treat a Jump List like a sorted log you read top to bottom. It is not. The DestList stream is a most-recently-used structure, and the moment you assume the order in the file is the order things happened, your timeline is wrong and you do not know it. This post is about getting the order right, and about what the artifact looks like after someone has been in there trying to make a problem go away.
If you have not parsed one of these before, start with the DFIR walkthrough and the automaticDestinations file format breakdown. This assumes you already know what a DestList row contains.
The ordering trap
Every DestList entry carries one FILETIME: the last time the shell recorded that target being opened. One value. It overwrites on each access. So a single entry tells you the most recent open and nothing about the five opens before it. That is the first thing people get wrong. They see "last access 14:07" and write "user opened the file once at 14:07" into the report. The file may have been opened forty times. You only ever see the last one.
The second thing they get wrong is sequence. The numbered LNK streams (1, 2, 3, and so on) are allocated in roughly creation order, but they are not your timeline either, because Windows reuses and rewrites them. What you actually want is two independent orderings, sorted and then compared:
- The per-entry last-access FILETIME, sorted ascending. This is your event order.
- The DestList entry-ID values. The header keeps a counter that the shell increments every time it issues a new entry. Each row stores the ID it was given. That counter only ever goes up, so entry IDs are a monotonic record of the order in which targets first entered the list.
When those two orderings agree, you have a clean MRU and you can trust the timeline. When they disagree, something interesting happened, and disagreement is exactly the signal the rest of this post is about.
Reconstructing the sequence
Parse to CSV and pull three columns: entry ID, last-access time, and target path. With JLECmd:
JLECmd.exe -d "C:\case\AutomaticDestinations" --csv "C:\case\out"
Then sort by LastModified (JLECmd's name for the DestList FILETIME) and keep EntryNumber alongside it. In a normal list the entry IDs will trend the same direction as the timestamps. A file that was opened recently sits near the top of the MRU and carries a high entry ID and a recent timestamp. Old, untouched targets carry low IDs and old timestamps and are drifting toward eviction.
Now look at the header counter against your row count. If the DestList header says the last issued entry number is 61 and you are holding nine rows, fifty-some entries left this list at some point. Before you write "evidence was deleted," understand the boring explanation first: the automatic Jump List is capped. The cap is tied to the "number of recent items to display" shell setting, and when the list is full the least-recently-used entry is evicted to make room. Normal churn produces exactly this gap. A workstation that opens documents all day will show a high counter and a short surviving list and that is just Tuesday.
What is not normal is gaps in the surviving IDs that break the MRU logic, or a header counter that does not move while access times keep advancing. Hold that thought.
What a cleared Jump List actually looks like
Users and attackers reach for the same blunt instruments. The Settings toggle ("show recently opened items in Start, Jump Lists, and File Explorer"), deleting the per-AppID files, or running a privacy cleaner. The clear or delete guide covers the mechanics. Here is the forensic residue each one leaves.
Delete the .automaticDestinations-ms file outright and Windows does not mourn it. The next interactive use of that application recreates the file from scratch. So absence is rarely absence. What you find is a file whose $STANDARD_INFORMATION and $FILE_NAME creation times in the MFT are suspiciously recent, holding a handful of entries whose access timestamps all cluster inside a narrow window after that creation time. A Notepad Jump List that claims the user's entire history with Notepad began last Thursday afternoon, on a machine provisioned two years ago, is telling you when the old one died, not when the user started using Notepad.
The toggle is sneakier because it truncates rather than destroys, and the file keeps its original creation time. The tell there is a DestList that is short and flat: few entries, all clustered recent, an entry-ID counter that resets the story. Cross-check the toggle's own footprint in the registry (HKCU\...\Explorer\Advanced\Start_TrackDocs) and the registry parser will date the change for you through the key's last-write time.
The deletion itself is recorded somewhere the person almost never thinks to clean. Writes to the Jump List file pass through the NTFS change journal. Pull the USN journal and you will see the DataOverwrite, DataExtend, and frequently FileDelete/FileCreate reason flags against the .automaticDestinations-ms name, timestamped. The Jump List can be gone and the journal still says when it went.
Surgical tampering, and why it is easy to catch
Wholesale deletion is loud. The quieter move is opening the compound file and removing one inconvenient entry while leaving the rest intact. This is where knowing the container format pays off.
The .automaticDestinations-ms is an OLE compound file (CFBF, the old structured-storage format). It has a FAT, a directory of named streams, and sector slack like any allocation structure. Two ways the surgery goes wrong:
A DIY editor removes the numbered LNK stream but does not touch DestList. Now you have a DestList row whose entry references a stream number that is not present in the directory. A dangling pointer. Parse the streams, parse the DestList, diff the two sets. Every DestList entry should resolve to a stream; every stream (except DestList) should be claimed by an entry. Anything orphaned on either side is a structural inconsistency that healthy shell-maintained files do not produce.
The other failure mode is editing the DestList bytes directly. Each entry carries an integrity value, and hand-edited rows from scripts that do not recompute it will not match. Windows is lax about validating this in practice, so the forged file still loads, which is the whole point for the attacker, but the bad checksum survives as a flag for you. Treat a checksum mismatch as suspicious, not conclusive, because parser bugs and version skew also produce them.
And then there is recovery. Because it is a compound file with a FAT, a removed stream's sectors go to unallocated, not to zero. They sit there until reallocated. Carving the CFBF free sector chain can pull back a dropped LNK stream, or enough of one to recover the target path and the embedded volume GUID. It is not guaranteed, the sectors get reused, but I have recovered a full UNC path out of compound-file slack on a file the user believed they had cleaned.
The shadow copy almost always wins
All of the above is interesting and most of the time you do not need it, because Volume Shadow Copy quietly snapshotted the Jump List before anyone touched it. Mount the shadow copies, pull the same .automaticDestinations-ms from each, and diff the DestLists across snapshots. The entry that exists in Tuesday's snapshot and is missing from Friday's tells you both what was removed and the window in which it happened. Selective deletion on the live volume is defeated entirely by a copy the user did not know existed and cannot reach.
This is the first thing to check, not the last. I lead with the compound-file internals because you should understand them, but operationally the order is shadow copies, then USN journal, then carving, then byte-level analysis of what survived.
A timeline you can defend
Reconstruction is only worth anything if it holds up, so two habits.
Pin your clock. The DestList FILETIME is UTC and the LNK header times are UTC, but the system you are reconstructing may have moved its clock: NTP corrections, a manual change, a VM reverted to an older snapshot. When entry-ID order and timestamp order disagree, clock movement is a more common and more honest explanation than forgery, and you check it before you allege tampering. Look for the time-change events in System.evtx (event ID 4616 in Security for "system time was changed") through the EVTX parser, and reconcile.
Do not lean on one artifact. A Jump List places a file in front of a user at a time. UserAssist confirms the application actually launched in a GUI session. Prefetch confirms the process executed. The USN journal confirms the target file existed and was touched. SRUM confirms the app was running and pushing bytes. The Jump List is a strong thread, and it is one thread. The cases that fall apart on cross-examination are the ones built on a single artifact read literally.
For one-off review without moving the file off the workstation, drop it on the homepage and it decodes the streams and the DestList client-side. When you need batch output and timeline events, JLECmd and Plaso are still the tools. The tools comparison has the rundown.
Further reading
- Yogesh Khatri and Mike Stevens, "Jumping into the Past" - the DestList reverse engineering this all rests on.
- Eric Zimmerman, JLECmd and JumpListExplorer.
- Joachim Metz, libolecf - reference for the OLE compound file container if you are carving by hand.
- MITRE ATT&CK T1070 Indicator Removal for where cleared Jump Lists sit in the larger picture.