Scheduled tasks in the registry: the RegRipper taskcache plugin
8 min read
Scheduled tasks are one of the oldest persistence mechanisms on Windows and still one of the most reliable for attackers, because everyone is conditioned to ignore them. The Task Scheduler runs hundreds of legitimate tasks; one more in the list is camouflage. The RegRipper taskcache plugin exists because the registry keeps its own copy of the scheduled-task inventory under TaskCache, and that copy is exactly what you need to detect tampering: a task that exists in the registry but not on disk, or on disk but not in the registry, is a red flag that the friendly Task Scheduler GUI will never show you.
Where it lives
The TaskCache tree sits in the SOFTWARE hive under Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache. Two subtrees do the work:
...\TaskCache\Tasks\<GUID>— one subkey per task, keyed by a GUID. This is where the metadata lives....\TaskCache\Tree\<task path>— the human-readable namespace. The folder structure here mirrors the Task Scheduler folder tree (\Microsoft\Windows\...), and the leaf key for each task carries anIdvalue containing the GUID that points back intoTasks.
So the registry stores the task in two halves. Tree is the index a human reads (what the task is called and where it sits in the folder hierarchy). Tasks is the record keyed by GUID (what the task does and when it ran). You resolve one to the other through the GUID. RegRipper's taskcache plugin and the parser on this site both key off the Tasks subkeys; the GUID is carried through as a column so you can pivot back to the Tree entry by hand.
The plugin reads from ...\Schedule\TaskCache\Tasks, enumerates every GUID subkey, and emits four columns: Task path, GUID, Registered (UTC), and Last run (UTC). The task path comes from the Path value inside the Tasks\<GUID> key (falling back to the raw GUID if no Path is present), and the two timestamps are decoded out of the DynamicInfo blob. More on that below.
What is in a Tasks\<GUID> key
Each Tasks\<GUID> subkey holds a handful of values. The ones that matter for an investigation:
Path— the task's logical path, e.g.\Microsoft\Windows\UpdateOrchestrator\Reboot. This is the same path you would see underTree, stored here so a tool reading only theTasksside can still label the row.DynamicInfo— a fixed binary blob holding version and timestamp fields. This is where the FILETIMEs live.Hash— a hash (SHA-256 in modern builds) of the task's XML definition. Windows uses this for integrity; for you it is a tamper signal, because if the on-disk XML changes without the legitimate scheduler machinery updating the registry, the stored hash and the recomputed hash diverge.SD— the security descriptor for the task, in binary form. Tells you who can read, run, or modify the task.
The action — the command line the task executes — is not in the registry. It lives in the task's XML definition on disk. The plugin note says this explicitly: "the task's action (exec command) lives in the on-disk XML, not the registry." The registry tells you a task exists, when it was registered, and when it last ran. It does not tell you the payload. For that you read C:\Windows\System32\Tasks\<task path>.
Decoding DynamicInfo
DynamicInfo is the interesting blob. Based on how the parser reads it, the layout it relies on is:
- Offset
0x00(4 bytes): a version field. - Offset
0x04(8 bytes): a FILETIME — the task registration / creation time. - Offset
0x0C(8 bytes): a FILETIME — the last run time.
That is what the code decodes: registered at 0x04, last-run at 0x0C, both rendered as UTC. Those two offsets are the ones I would trust, because they are the ones this implementation actually parses and the ones RegRipper has historically pulled.
DynamicInfo on newer Windows builds is longer than the bytes those two fields occupy, and the trailing region has been documented elsewhere as carrying a last-completed / last-success FILETIME and status fields. This parser does not surface a last-completed timestamp, so I will not put a hard offset on it — if you need it, dump the raw DynamicInfo value and inspect the bytes past 0x14 yourself, then corroborate against the on-disk XML before you rely on it. Registered and last-run are the two you can lean on from the registry alone.
A decoded row looks like this:
Task path: \Microsoft\Windows\PowerShell\ScheduledJobs\UpdateCheck
GUID: {B8A3F1E2-...-9C44}
Registered (UTC): 2026-06-09 03:11:48
Last run (UTC): 2026-06-16 02:00:02
A FILETIME of all 0x00 bytes decodes to an empty value, which is normal for a task that has never run.
The Tree-versus-Tasks cross-check
This is the reason TaskCache earns a place in your workflow over just dumping the XML directory. You have three places a scheduled task can be represented:
- The on-disk XML in
C:\Windows\System32\Tasks\<path>. - The
TaskCache\Tree\<path>entry (with itsId→ GUID). - The
TaskCache\Tasks\<GUID>entry (withPath,DynamicInfo,Hash,SD).
In a healthy system these three agree. Every Tree leaf has an Id that resolves to a Tasks key, and every task has a matching XML file on disk. The interesting cases are the disagreements:
- Tree/Tasks entry with no on-disk XML. Something registered a task in the registry, then the XML was deleted (or never written by the normal path). Anti-forensic cleanup that removes the file but misses the registry leaves exactly this pattern.
- On-disk XML with no Tree/Tasks entry. A task file dropped directly into
System32\Taskswithout going through the scheduler API. Attackers who write the XML manually to evade API-level monitoring create this mismatch. - Hash mismatch. The
Hashvalue in the Tasks key does not match a recomputed hash of the current XML. The task was edited out-of-band after registration.
The Task Scheduler GUI and schtasks /query read the live, reconciled view and will happily hide these inconsistencies from you. The registry TaskCache is the ground-truth inventory that lets you catch them. This is the same logic that makes the services plugin worth running: the registry records persistence whether or not the friendly management tool chooses to surface it.
Why this maps to T1053.005
Scheduled tasks are MITRE ATT&CK technique T1053.005, and they are popular for good reasons. A task survives reboots. It can run as SYSTEM. It can be triggered on logon, on a schedule, on an event, or on idle. It does not need a foreground process. And — critically for the attacker — none of the GUI-driven execution artifacts capture it. As covered in the UserAssist writeup, UserAssist records shell launches and nothing a scheduled task does. Run keys are noisy and obvious; a scheduled task hides in a crowd of legitimate Microsoft tasks. If you are hunting persistence, TaskCache belongs in the same pass as the Run keys and IFEO check.
Enumerating hidden tasks
The attacker move that TaskCache is best at exposing is the hidden scheduled task. There are a couple of variants:
- The
SD-stripped task. If you delete theSD(security descriptor) value from aTasks\<GUID>key, the task can become invisible toschtasksand the GUI while still firing. The task is fully functional but the management tools will not list it. RegRipper'staskcache, reading theTaskssubkeys directly, sees the GUID and itsDynamicInforegardless of whetherSDis present. ATaskskey with noSDvalue is itself suspicious. - The manually planted XML. As above, an XML file dropped into
System32\Taskswithout a corresponding registry entry. This one you catch by diffing the on-disk directory listing against the GUIDs you enumerate from TaskCache.
So the enumeration recipe is: dump every Tasks\<GUID> from the registry, dump every Tree leaf and its Id, list C:\Windows\System32\Tasks on disk, and reconcile all three sets. Anything that does not appear in all three, or whose Hash does not verify, goes on the review list — then read its XML to get the command line, because the registry will not give you that.
Tools
- RegRipper's
taskcacheplugin. ReadsMicrosoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tasks, resolves thePath, and decodes theDynamicInfoFILETIMEs. Output is one line per task. Read the plugin source to see exactly which offsets it trusts on the build you are analyzing. - Eric Zimmerman's RECmd with a TaskCache batch file produces the same data in CSV, including the GUID so you can pivot to
Tree. - The parser on this site can analyze a SOFTWARE hive in your browser and surfaces TaskCache as Task path, GUID, registered, and last-run, with the GUID carried through so you can cross-reference the
Treeside and the on-disk XML.
For the broader set of registry persistence checks this fits into, see the RegRipper plugins reference.
What TaskCache will not tell you
The registry gives you existence, identity, and timing. It does not give you the payload. Always finish the job by reading the XML in C:\Windows\System32\Tasks for the task's <Actions> block — that is where the command line, arguments, and run-as account live. Pair the registration timestamp from DynamicInfo against the XML file's MFT timestamps; if they disagree, you are looking at a planted or modified task. And as with every volatile artifact, pull the SOFTWARE hive once at a known time and treat that as your reference state, because the DynamicInfo last-run FILETIME moves every time the task fires.
Combined with the on-disk XML, the MFT, and your service and Run-key checks, TaskCache answers "what persistence is registered, when, and is it hiding" with a confidence the live management tools cannot match — precisely because it is the layer they paper over.