When it comes to persistence of common off-the-shelf malware, the most commonly observed persistence mechanisms are run keys, services, and scheduled tasks. For either of these, Windows or even the malware itself creates a set of registry keys to register the persistence mechanism with the operating system. Out of these mechanisms, this blog post specifically focuses on scheduled tasks. These are particularly interesting since they allow for much more versatile launch conditions and actions compared to services or run keys.All tasks currently registered to a Windows machine are represented by a set of registry keys and values in the HKLM\Software\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache
tree. To the keen eye, most of the registry keys and their values are recognizable as they have descriptive names or contain strings. However, there are some values which seemingly contain binary data – and those are what this blog post is (mainly) about.
But before we dive into the depths of the Windows registry and the task scheduler, let me provide an important disclaimer: all this research was conducted using Windows 10 1909 with the occasional claim being verified on a Windows 7 SP1. Yet, there is certainly information presented in this blog post which is not valid for operating systems older than Windows 10. From my experience during this research, these differences are regularly just elements in structures which were not yet present in, for example, Windows 7. Vice versa, there are elements in the structures which are no longer used in current versions of the Windows operating system. Although these fields might not be used nowadays, they are still present in Windows 10. I assume this is to ensure backwards compatibility so that after, for example, an upgrade of the operating system, the tasks from the older version still work as expected with the newer one.
To make parsing the raw registry data easier, I created a set of struct definitions using kaitai struct. I published the kaitai definitions and some tooling based on these definitions on Github. For more information regarding how to use either of it, please refer to the README in the repository: https://github.com/GDATAAdvancedAnalytics/winreg-tasks
With all that being said, let’s get into it.
The Happy Path – Creating a new Task and What Happens in the Registry
Assume we just created a new Task called “Simple Task” which starts calc.exe
whenever a user logs on. The task scheduler then creates a set of registry keys which hold the information entered in the task creation wizard. These registry keys are split into roughly two groups which reference each other. The first picture shows the task in the Tree
subkey, the second picture shows the values found in the respective Tasks
subkey (please note my exceptional GIMP skills!). The arrows denote the references from one group to the other. I discuss the different subkeys and their values in the next section.
Structural Overview
The TaskCache
key has several subkeys which contain and organize all tasks registered in the system. All elements in the Tasks
subkey reference a key in the Tree
subkey and vice versa. Boot
, Logon
, Maintenance
, and Plain
tasks only have an ID
value which references a key in the Tasks
subkey.
Subkey | Description |
---|---|
Boot | References to tasks which ought to be triggered at boot time. |
Logon | References to tasks which ought to be triggered when a user logs on. |
Maintenance | References to tasks which ought to be executed during automatic system maintenance. |
Plain | References to all tasks which are neither a boot, logon, nor maintenance task. |
Tasks | Rhe actions, settings, triggers, etc of all task data in the system organized by task id. |
Tree | References to and Security Descriptors for all tasks, organized in a tree-like structure. |
The subkeys in the Tree
key usually contain up to three values but only hold very limited information about a given task. If the subkey stands for a directory in the task scheduler snap-in and does not represent a task, there only is the SD
value present which denotes the access rights to this specific task folder (if there is no SD
value, the folder or task is hidden from the list of tasks for the user querying their task list). If a subkey references a task, there is at least the Id
value set in addition to the SD
value. Usually, the subkey then also contains an Index
value but the presence of this specific value does not seem to be mandatory.
Value | Description |
---|---|
Id | The UUID identifying the Task. |
Index | The type of Task (Boot Task (1), Logon Task (2), Plain Task (3), Maintenance Task (4)) |
SD | A SECURITY_DESCRIPTOR describing the permission of the task folder (“who is allowed to see this task or the directory tree”). |
Each subkey in Tasks
has several Values which define the task. Usually, only a subset of the values provided in the table below are set for a task. This is because many of these values are optional and are not saved in the registry if empty (e.g. Author
, Data
, Description
, Documentation
, Source
). In fact, Actions
and Triggers
seem to be the only required values – for the MMC snap-in to show the task at least. But while the snap-in is much more restrictive with regards to how the state of the registry is, the task scheduler service is much less so. Thus, even a task without any Actions
and Triggers
might still be considered valid by the task scheduler service albeit the task not being of any use.
Value | Description |
---|---|
Actions | The actions which are to be executed when the task is triggered (e.g. “execute an application”); see below |
Author | The author of the Task. This may be a specific string but can also be a reference to a resource DLL |
Data | Additional data associated with the Task |
Date | The date and time the Task was registered at |
Description | The description of the Task. This may be a specific string but can also be a reference to a resource DLL |
Documentation | The documentation of the Task. This may be a specific string but can also be a reference to a resource DLL |
DynamicInfo | Dynamic information about the task; see below |
Hash | A CRC32 or SHA256 hash of the Task XML file (in C:\windows\system32\tasks\…) |
Path | Reference to the corresponding entry in the Tree subkey and also the location of the task’s XML file on disk relative to the task directory. |
Schema | The version of the XML schema to apply when serializing the task data. This roughly translates to the minimal Windows version the task should be compatible to (e.g., schema 0x00010006 → Windows 10). |
SecurityDescriptor | The SDDL which defines who is allowed to do what on or with a task |
Source | The source of the task. This may be a specific string but can also be a reference to a resource DLL |
Trigger | The triggers of the task; see below |
URI | Specifies where the task is placed in the task folder hierarchy |
Version | The minimum version of the Task Scheduler Remoting Protocol compatible with this task |
Dynamic Information – The “When it Happened”
The DynamicInfo
Value contains three timestamps denoting when the task was created, its last execution time, and the last time it finished successfully. The structure also holds any error code which occurred during the latest execution of the task. Prior to (at least) Windows 7, the structure also contained the current state of the task. But this value seems to be no longer used in this specific place but needs to be kept for compatibility reasons, presumably.A pseudo-C structure for the DynamicInfo
Value could look like this:
struct DynamicInfo { DWORD magic; // currently 0x3 FILETIME ftCreate; FILETIME ftLastRun; DWORD dwTaskState; DWORD dwLastErrorCode; FILETIME ftLastSuccessfulRun; // this field may not be present on older Windows versions (e.g. Windows 7) };
Given our introductory example from above, the DynamicInfo
Value may contain this byte sequence:
03 00 00 00 e9 79 2d f1 31 1c d8 01 # Mon 7 February 2022 14:49:43 UTC 5b 8b 6b 73 34 1c d8 01 # Mon 7 February 2022 16:19:59 UTC 00 00 00 00 00 00 00 00 # ERROR_SUCCESS e4 70 d5 67 34 1c d8 01 # Mon 7 February 2022 15:07:21 UTC
If we changed the action of the task to execute a non-existent file, the data might look like this (note the non-zero error code):
03 00 00 00 e9 79 2d f1 31 1c d8 01 # Mon 7 February 2022 14:49:43 UTC 62 76 13 3b 33 1c d8 01 # Mon 7 February 2022 16:20:39 UTC 00 00 00 00 02 00 07 80 # 0x80070002 -> ERROR_FILE_NOT_FOUND 4c 30 75 3b 33 1c d8 01 # Mon 7 February 2022 16:20:39 UTC
Actions – The “What Should Happen”
Whenever a Task is triggered by the scheduler, it may execute a set of actions. PowerShell does not allow to create more than 32 actions for a task, however, technically there is almost no limit to how many actions the scheduling service can handle. The only limitation seems to be the amount of elements an STL container can hold.Once again referring to the introductory example, the Actions
value may contain a byte sequence similar to the following one. Since we only set the command to calc
and did not add any arguments or passed a working directory, the structure is relatively small.
03 00 # version 0c 00 00 00 41 00 75 00 74 00 68 00 6f 00 72 00 # context ("Author") 66 66 # magic 0x6666 (-> execution action) 00 00 00 00 # id 08 00 00 00 63 00 61 00 6c 00 63 00 # command ("calc") 00 00 00 00 # arguments 00 00 00 00 # working directory 00 00 # flags
If one decides to add more details to the actions, the structure can grow in size rapidly. The next example shows the byte sequence for the Actions
value when the action also passes arguments to the command and changes the working directory:
03 00 # version 0c 00 00 00 41 00 75 00 74 00 68 00 6f 00 72 00 # context ("Author") 66 66 # magic 0x6666 (-> execution action) 00 00 00 00 # id 08 00 00 00 63 00 61 00 6c 00 63 00 # command ("calc") 2c 00 00 00 61 00 72 00 67 00 31 00 20 00 # arguments ("arg1 arg2 verylongarg3") 61 00 72 00 67 00 32 00 20 00 76 00 65 00 # 72 00 79 00 6c 00 6f 00 6e 00 67 00 61 00 # 72 00 67 00 33 00 # 56 00 00 00 43 00 3a 00 5c 00 74 00 68 00 # working directory ("C:\this\is\a\very\long\path\to\a\directory\") 69 00 73 00 5c 00 69 00 73 00 5c 00 61 00 # 5c 00 76 00 65 00 72 00 79 00 5c 00 6c 00 # 6f 00 6e 00 67 00 5c 00 70 00 61 00 74 00 # 68 00 5c 00 74 00 6f 00 5c 00 61 00 5c 00 # 64 00 69 00 72 00 65 00 63 00 74 00 6f 00 # 72 00 79 00 5c 00 # 00 00 # flags
The Actions Structure
A pseudo-C representation of the byte sequences from above cannot be given as easily as for the DynamicInfo
. This is mainly because there is not only the execution action which we just looked at, but there is also email actions, COM handlers, and message box actions. We define these actions throughout the course of this section but for now we just assume they exist and “forward declare” them. The Actions
structure can then be laid out like this:
struct Action; struct ComHandlerAction : Action; struct EmailAction : Action; struct ExecutionAction : Action; struct MessageBoxAction : Action; struct Actions { WORD version; // 0x16 on Win10 1909 BSTR context; // the id of the principal which is used to run the actions; must match the principal_id of the JobBucket (see below) Action actions[]; // repeated until EOF };
Technically, the Actions
structure holds two properties: magic
and id
. But since the
differs between all actions and the magic
id
is specific to each individual action, I include these two fields into the definitions of the sub structures and not into the generic Action
structure. Either way, the magic
denotes which type of action comes next in the data stream and the id
may be used to assign to it an easily recognizable name. It seems that setting an id for an action is only supported when using the COM interface but not when using any other front-end tooling (Powershell, Task Scheduler Snap-In).
ComHandler Action
The ComHandler action is the smallest one to find in the registry. Besides the common magic
and id
, it only contains a CLSID and a string for (optional) additional data:
struct ComHandlerAction : Action { WORD magic = 0x7777; BSTR id; CLSID classId; BSTR data; };
Whenever one of these actions is triggered, the task scheduler service spawns a new taskhostw.exe
process which then loads and executes the COM class configured by the classId
property of the action. The COM class must implement the ITaskHandler interface and its Start
method is passed the value from the data
property of the action.When parsing the CLSID from the registry one must pay special attention to the byte ordering: since the data in this structure is memcpy’d into a buffer, the order order of bytes for data1
, data2
, and data3
is inverted. See the following listing for an example:
03 00 # version 14 00 00 00 4c 00 6f 00 63 00 61 00 6c 00 41 00 64 00 6d 00 69 00 6e 00 # context ("LocalAdmin") 77 77 # magic 00 00 00 00 # id c2 d0 d1 89 cf a3 0c 49 ab e3 b8 6c de 34 b0 47 # clsid {89d1d0c2-a3cf-490c-abe3-b86cde34b047} (ReAgentTaskHandler) 16 00 00 00 56 00 65 00 72 00 69 00 66 00 79 00 57 00 69 00 6e 00 52 00 45 00 # data ("VerifyWinRE")
Email Action
Although being obsolete and discontinued, tasks technically can still have Email actions as defined by the IEmailAction COM interface. However, the task scheduler is only able to parse the respective data from the registry but does not execute Email tasks anymore.
struct EmailAction : Action { WORD magic = 0x8888; BSTR id; BSTR from; BSTR to; BSTR cc; BSTR bcc; BSTR replyTo; BSTR server; BSTR subject; BSTR body; DWORD numAttachments; BSTR attachmentFilenames[numAttachments]; DWORD numHeaders; Pair<BSTR, BSTR> headers[numHeaders]; // "BSTR headerName; BSTR headerValue;" x numHeaders }
Execution Action
The execution action is the only action which can be created using the task scheduler MMC snap-in (technically, you can also create email and message box actions – you are just not allowed to save the task then). An execution task has three customizable properties (not counting the id
) which are defined in the IExecAction COM interface.
struct ExecutionAction : Action { WORD magic = 0x6666; BSTR id; BSTR command; BSTR arguments; BSTR workingDirectory; WORD flags; // only present if Actions.version >= 3 };
Example:
03 00 # version 0c 00 00 00 41 00 75 00 74 00 68 00 6f 00 72 00 # context ("Author") 66 66 # magic 00 00 00 00 # id 46 00 00 00 25 00 73 00 79 00 73 00 74 00 65 00 # command ("%systemroot%\system32\usoclient.exe") 6d 00 72 00 6f 00 6f 00 74 00 25 00 5c 00 73 00 79 00 73 00 74 00 65 00 6d 00 33 00 32 00 5c 00 75 00 73 00 6f 00 63 00 6c 00 69 00 65 00 6e 00 74 00 2e 00 65 00 78 00 65 00 18 00 00 00 53 00 74 00 61 00 72 00 74 00 49 00 # params ("StartInstall") 6e 00 73 00 74 00 61 00 6c 00 6c 00 00 00 00 00 # working directory 00 00 # flags
Message Box Action
Similarly to Email actions, the MessageBox actions (or ShowMessage action as per Microsoft terms) have been discontinued and can no longer be used with the task scheduler. The structure of the data is simple, though, as a user only needed to define the caption and the content of the message box to show upon task activation.
struct MessageboxAction : Action { WORD magic = 0x9999; BSTR id; BSTR caption; BSTR content; }
Triggers – The “When Should it Happen”
The second, but not less important, building block of Windows tasks is their triggers. Simply put, triggers define when a given task shall be executed. Windows offers a range of different triggers (e.g. calendar-based triggers, boot triggers, logon triggers) which additionally have a variety of customization options (e.g. execution delay, repetition timings). All triggers share the same set of options but can be configured individually. I describe the top-level structure first and then descend into the different sub structures.The most significant difference between Triggers
and Actions
is that the data in the Triggers
structure is aligned to 8-byte boundaries whereas the data in the Actions
is not aligned at all. This makes parsing the Triggers more tedious since one must pay special attention to data alignments and cannot just read a number of bytes and interpret them. Unfortunately, this also makes the structure definitions look bloated. To counteract that, I define an enhanced set of types which have an ALIGNED_
prefix which hints that the encapsulated data is padded a multiple of 8 bytes. The following example applies to WORD, DWORD
, and any other type analogously where necessary:
struct ALIGNED_BYTE { BYTE value; BYTE padding[7]; }
The Triggers Structure
The triggers structure is defined by only three major members: the Header
, the JobBucket
, and the collection of Triggers
.
struct Triggers { Header header; JobBucket bucket; Trigger triggers[]; // repeated until eof }
The Header
The header only denotes the version of the struct and the time and date when the task shall be activated at and deactivated at, respectively. On Windows 10 1909, the struct version is 0x17
; Windows 7 used 0x15
.
struct Header { ALIGNED_BYTE version; TSTIME startBoundary; // the earliest startBoundary of all triggers TSTIME endBoundary; // the latest endBoundary of all triggers }
The JobBucket
The JobBucket holds general information about the task, its triggers, and all of the options and settings which can be set for a task.
struct JobBucket { // see Github ("job_bucket_flags") for a list of identified values ALIGNED_DWORD flags; // the crc32 checksum of the task XML ALIGNED_DWORD crc32; // the id of the principal which is used to execute the task; only if header.version >= 0x16 ALIGNED_BSTR principal_id; // the DisplayName of the task principal; only if header.version >= 0x17 ALIGNED_BSTR display_name; // the task principal UserInfo user_info; // if set, this structure contains additional settings which are not mandatory when creating a task OptionalSettings optional_settings; }
The task scheduler allows running tasks as a different user than the one who created the task initially. The UserInfo
struct contains the information which are necessary to impersonate the task principal.
struct UserInfo { // if non-zero, the rest of the struct is omitted in the registry ALIGNED_BYTE skip_user; // only if skip_user == 0 ALIGNED_BYTE skip_sid; // any value of the SID_NAME_USE enum; only if skip_user == 0 and skip_sid == 0 ALIGNED_DWORD sid_type; // SID in binary form; only if skip_user == 0 and skip_sid == 0 ALIGNED_BUFFER sid; // only if skip_user == 0 ALIGNED_BSTR username; }
The OptionalSettings
contain the preferences and settings from the Conditions and Settings tabs in the Task Scheduler snap-in as well as additional settings which can (only) be set using the Task Scheduler COM interface (ITaskSettings, ITaskSettings2, ITaskSettings3).
struct OptionalSettings { ALIGNED_DWORD len; // if len == 0, the rest of the structure is omitted in the registry DWORD idle_duration_seconds; DWORD idle_wait_timeout_seconds; DWORD execution_time_limit_seconds; DWORD delete_expired_task_after; DWORD priority; DWORD restart_on_failure_delay; DWORD restart_on_failure_retries; GUID network_id; BYTE padding0[4]; // probably there because the previous struct members are part of another struct which is inlined here BYTE privileges; // only if len == 0x38 or len == 0x58 TSTIMEPERIOD periodicity; // only if len == 0x58 TSTIMEPERIOD deadline; // only if len == 0x58 BYTE exclusive; // only if len == 0x58 BYTE padding1[3]; // only if len == 0x58 }
Several of these settings have corresponding input fields in the task scheduler snap-in. To make these more easily recognizable, I copied over the labels to their respective setting in the struct where applicable:
idle_duration_seconds
: “start the task only if the computer is idle for” setting converted to secondsidle_wait_timeout_seconds
: “wait for idle for” converted to secondsexecution_time_limit_seconds
: “stop the task if it’s running longer than” converted to secondsdelete_expired_task_after
: “if the task is not scheduled to run again, delete it after” converted to secondspriority
: the process priority value the task scheduler assigns to the task processrestart_on_failure_delay
: “if the task fails, restart every” converted to secondsrestart_on_failure_retries
: “attempt to restart up to”network_id
: “start only if the following network connection is available”privileges
: a bitmap of Se* permissions (e.g., SeDebugPrivilege; see Github repository for full list) to grant to the task process when runningperiodicity
: the amount of time a task needs when executed during automatic system maintenance (only applies to maintenance tasks)deadline
: defines the amount of time which is allowed to pass before the task is executed during emergency maintenance if it failed to complete during normal maintenance (only applies to maintenance tasks)exclusive
: defines whether the task shall be run as an exclusive task with no other maintenance task running at the same time (only applies to maintenance tasks)
The Triggers
The list of trigger structures defines the triggers of a given task. There is (almost) no technical limit to the amount of triggers a task is allowed to have. Yet, there commonly are only 1 to 3 triggers with the occasional outlier in my testing system having up to 6 triggers.
Different to the Actions
described above, the triggers only share the common field magic
. However, I again include the magic
into the different trigger definitions so that is is more easily recognizable which magic a given trigger has (similarly to how I have done it with the Actions above).
All of the triggers below contain a GenericData
field (except for the TimeTrigger
which uses a JobSchedule
). I describe the structure below the trigger definitions.
The examples given for the different triggers may contain garbage or suspiciously-looking data in several fields. This mainly has two reasons. On the one hand, the task scheduler fills the buffer for the registry data with the character H
before serializing any data. This is where all the 0x48
come from. On the other hand, the data structures being memcpy
‘d into the buffer are usually not initialized. This has the side effect that there may be content from the stack (or heap, depending on where the source data structure resides) spoiled into the allocated buffer and eventually written into the registry. I have observed heap and stack pointers where only the least significant byte was overridden by a field of the structure, partial strings, and also “random” data which I could not identify.
WnfStateChangeTrigger
This trigger listens for notifications in the Windows Notification Framework (WNF). It appears that this trigger is intended to only be used internally in Windows, because the Microsoft documentation only provides the non-descriptive trigger type TASK_TRIGGER_CUSTOM_TRIGGER_01
for this trigger type. However, if one is well-versed with COM and how to work with “unknown” interfaces, it should be possible to create even these triggers.
struct WnfStateChangeTrigger { ALIGNED_DWORD magic = 0x6666; GenericData genericData; BYTE state_name[8]; ALIGNED_DWORD cbData; BYTE data[cbData]; }
Example:
17 00 00 00 00 00 00 00 # header.version (0x17) 00 7c 10 22 98 7c 10 22 00 00 00 00 00 00 00 00 # header.start_boundary (localized: no, filetime: 0) 00 7c 10 22 98 7c 10 22 ff ff ff ff ff ff ff ff # header.end_boundary (localized: no, filetime: 0xffffffffffffffff) 00 90 c0 42 48 48 48 48 # job_bucket.flags (0x42c09000) 27 82 bb 7f 48 48 48 48 # job_bucket.crc32 (0x7fbb8227) 0c 00 00 00 48 48 48 48 55 00 73 00 65 00 72 00 73 00 00 00 48 48 48 48 # job_bucket.principal_id ("Users") 00 00 00 00 48 48 48 48 # job_bucket.display_name ("") 00 48 48 48 48 48 48 48 # job_bucket.user_info.skip_user (0x00 -> user struct is present) 00 48 48 48 48 48 48 48 # job_bucket.user_info.skip_sid (0x00 -> sid is present) 05 00 00 00 48 48 48 48 # job_bucket.user_info.sid_type (0x05 -> SidTypeWellKnownGroup) 0c 00 00 00 48 48 48 48 01 01 00 00 00 00 00 05 04 00 00 00 48 48 48 48 # SID (S-1-5-4 "INTERACTIVE") 00 00 00 00 48 48 48 48 # job_bucket.user_info.username ("") 2c 00 00 00 48 48 48 48 # job_bucket.optional_settings.len (0x2c) 00 00 00 00 # job_bucket.optional_settings.idle_duration_seconds (0) ff ff ff ff # job_bucket.optional_settings.idle_wait_timeout_seconds (-1) 58 02 00 00 # job_bucket.optional_settings.execution_time_limit_seconds (600) ff ff ff ff # job_bucket.optional_settings.delete_expired_task_after (-1) 06 00 00 00 # job_bucket.optional_settings.priority (6) 00 00 00 00 # job_bucket.optional_settings.restart_on_failure_retries (0) 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # job_bucket.optional_settings.network_id 00 00 00 00 48 48 48 48 # job_bucket.optional_settings.padding0 66 66 00 00 00 00 00 00 # triggers[0].magic (0x6666) 00 7c 10 22 98 7c 10 22 00 00 00 00 00 00 00 00 # triggers[0].generic_data.start_boundary (localized: no, filetime: 0) 00 7c 10 22 98 7c 10 22 ff ff ff ff ff ff ff ff # triggers[0].generic_data.end_boundary (localized: no, filetime: 0xffffffffffffffff) 00 00 00 00 # triggers[0].generic_data.delay_seconds (0) ff ff ff ff # triggers[0].generic_data.timeout_seconds (-1) 00 00 00 00 # triggers[0].generic_data.repetition_interval_seconds (0) 00 00 00 00 # triggers[0].generic_data.repetition_duration_seconds (0) 00 00 00 00 # triggers[0].generic_data.repetition_duration_seconds_2 (0) 00 # triggers[0].generic_data.stop_at_duration_end (false) e3 87 00 # triggers[0].generic_data.padding 01 7e e8 fa cd 31 1c be # triggers[0].generic_data.enabled (true) 6f 8a 99 8f 84 0b 3e 42 # triggers[0].generic_data.unknown 00 00 00 00 # triggers[0].generic_data.trigger_id ("") 48 48 48 48 # triggers[0].generic_data.pad_to_block 75 78 bc a3 3a 07 80 08 # triggers[0].state_name ("7578bca33a078008", WNF_WIFI_TASK_TRIGGER) 00 00 00 00 00 00 00 00 # triggers[0].cbData (0)
SessionChangeTrigger
Triggers a task whenever the session state of the given user
changes (see JobBucket
for the definition of the UserInfo
structure). Session state changes can be console connect and disconnect, remote connect and disconnect, and session lock and unlock. The specific values for the session states can be found in the Github repository (“session_state” enum).
struct SessionChangeTrigger { ALIGNED_DWORD magic = 0x7777; GenericData genericData; ALIGNED_DWORD dwStateChange; UserInfo user; }
Example (only the trigger structure, full example at WnfStateChangeTrigger
):
77 77 00 00 00 00 00 00 # magic (0x7777) 00 67 10 22 80 67 10 22 00 00 00 00 00 00 00 00 # start_boundary (localized: no, filetime: 0) 00 67 10 22 80 67 10 22 ff ff ff ff ff ff ff ff # end_boundary (localized: no, filetime: 0xffffffffffffffff) 58 02 00 00 # delay_seconds (600) ff ff ff ff # timeout_seconds (0xffffffff) 00 00 00 00 # repetition_interval_seconds (0) 00 00 00 00 # repetition_duration_seconds (0) 00 00 00 00 # repetition_duration_seconds_2 (0) 00 # stop_at_duration_end (false) e3 87 00 # padding 00 00 49 00 6e 00 73 00 # enabled (false) 74 00 61 00 6c 00 6c 00 # unknown 34 00 00 00 4c 00 6f 00 63 00 61 00 6c 00 43 00 # trigger_id ("LocalConsoleConnectTrigger") 6f 00 6e 00 73 00 6f 00 6c 00 65 00 43 00 6f 00 6e 00 6e 00 65 00 63 00 74 00 54 00 72 00 69 00 67 00 67 00 65 00 72 00 01 00 00 00 # state_change ("ConsoleConnect") 65 00 00 00 # padding 01 48 48 48 48 48 48 48 # user_info.skip_user (true -> no user struct)
RegistrationTrigger
Triggers the task actions when the task registered or updated.
struct RegistrationTrigger { ALIGNED_DWORD magic = 0x8888; GenericData genericData; }
Example (only the trigger structure, full example at WnfStateChangeTrigger
):
88 88 00 00 00 00 00 00 # magic (0x8888) 00 59 10 22 70 59 10 22 00 00 00 00 00 00 00 00 # start_boundary (localized: no, filetime: 0) 00 59 10 22 70 59 10 22 ff ff ff ff ff ff ff ff # end_boundary (localized: no, filetime: 0xffffffffffffffff) 00 00 00 00 # delay_seconds (0) ff ff ff ff # timeout_seconds (0xffffffff) 00 00 00 00 # repetition_interval_seconds (0) 00 00 00 00 # repetition_duration_seconds (0) 00 00 00 00 # repetition_duration_seconds_2 (0) 00 # stop_at_duration_end (false) e3 87 00 # padding 01 00 00 00 00 00 00 00 # enabled (true) 4c 4d 45 4d 48 00 00 00 # unknown 00 00 00 00 # trigger_id 48 48 48 48 # block padding
LogonTrigger
Triggers a task whenever the given user
logs on (see JobBucket
for the definition of the UserInfo
structure).
struct LogonTrigger { ALIGNED_DWORD magic = 0xAAAA; GenericData genericData; UserInfo user; }
Example (only the trigger structure, full example at WnfStateChangeTrigger
):
aa aa 00 00 00 00 00 00 # magic (0xaaaa) 00 59 10 22 70 59 10 22 00 00 00 00 00 00 00 00 # start_boundary (localized: no, filetime: 0) 00 59 10 22 70 59 10 22 ff ff ff ff ff ff ff ff # end_boundary (localized: no, filetime: 0xffffffffffffffff) 00 00 00 00 # delay_seconds (0) ff ff ff ff # timeout_seconds (0xffffffff) 80 70 00 00 # repetition_interval_seconds (28800 "PT8H") 00 00 00 00 # repetition_duration_seconds (0) 00 00 00 00 # repetition_duration_seconds_2 (0) 00 # stop_at_duration_end (false) e3 87 00 # padding 01 a9 a7 1c 94 a9 a7 1c # enabled (true) 00 00 00 00 00 00 00 00 # unknown 00 00 00 00 # trigger_id 48 48 48 48 # block padding 01 48 48 48 48 48 48 48 # user_info.skip_user (true -> no user struct)
EventTrigger
Triggers a task based on events that appeared in the Windows logs. The subscription
field contains a list of XPATH queries which create the filters the task scheduler uses to check whether the conditions for the EventTrigger
are fulfilled.
struct EventTrigger { ALIGNED_DWORD magic = 0xCCCC; GenericData genericData; ALIGNED_BSTR_EXPANDSIZE subscription; DWORD unknown0; DWORD unknown1; ALIGNED_BSTR_EXPANDSIZE unknown2; ALIGNED_DWORD len_value_queries; Pair<ALIGNED_BSTR_EXPANDSIZE, ALIGNED_BSTR_EXPANDSIZE> value_queries; // "ALIGNED_BSTR_EXPANDSIZE name; ALIGNED_BSTR_EXPANDSIZE query;" x len_value_queries }
Example (only the trigger structure, full example at WnfStateChangeTrigger
):
cc cc 00 00 00 00 00 00 # magic (0xcccc) 00 59 10 22 70 59 10 22 00 00 00 00 00 00 00 00 # start_boundary (localized: no, filetime: 0) 00 59 10 22 70 59 10 22 ff ff ff ff ff ff ff ff # end_boundary (localized: no, filetime: 0xffffffffffffffff) dc 05 00 00 # delay_seconds (1500 "PT25M") 08 07 00 00 # timeout_seconds (1800 "PT30M") 10 0e 00 00 # repetition_interval_seconds (3600 "PT1H") 40 38 00 00 # repetition_duration_seconds (14400 "PT4H") 40 38 00 00 # repetition_duration_seconds_2 (14400 "PT4H") 00 # stop_at_duration_end (false) e3 87 00 # padding 01 00 00 00 00 00 00 00 # enabled (true) 0c 00 00 00 00 00 00 00 # unknown 00 00 00 00 # trigger_id 48 48 48 48 # block padding 05 01 00 00 00 00 00 00 3c 00 51 00 75 00 65 00 # subscription: <Select Path="Microsoft-Windows-User Device Registration/Admin">*[System[Provider[@Name='Microsoft-Windows-User Device Registration'] and EventID=300]]</Select></Query></QueryList> 72 00 79 00 4c 00 69 00 73 00 74 00 3e 00 3c 00 51 00 75 00 65 00 72 00 79 00 20 00 49 00 64 00 3d 00 22 00 30 00 22 00 20 00 50 00 61 00 74 00 68 00 3d 00 22 00 4d 00 69 00 63 00 72 00 6f 00 73 00 6f 00 66 00 74 00 2d 00 57 00 69 00 6e 00 64 00 6f 00 77 00 73 00 2d 00 55 00 73 00 65 00 72 00 20 00 44 00 65 00 76 00 69 00 63 00 65 00 20 00 52 00 65 00 67 00 69 00 73 00 74 00 72 00 61 00 74 00 69 00 6f 00 6e 00 2f 00 41 00 64 00 6d 00 69 00 6e 00 22 00 3e 00 3c 00 53 00 65 00 6c 00 65 00 63 00 74 00 20 00 50 00 61 00 74 00 68 00 3d 00 22 00 4d 00 69 00 63 00 72 00 6f 00 73 00 6f 00 66 00 74 00 2d 00 57 00 69 00 6e 00 64 00 6f 00 77 00 73 00 2d 00 55 00 73 00 65 00 72 00 20 00 44 00 65 00 76 00 69 00 63 00 65 00 20 00 52 00 65 00 67 00 69 00 73 00 74 00 72 00 61 00 74 00 69 00 6f 00 6e 00 2f 00 41 00 64 00 6d 00 69 00 6e 00 22 00 3e 00 2a 00 5b 00 53 00 79 00 73 00 74 00 65 00 6d 00 5b 00 50 00 72 00 6f 00 76 00 69 00 64 00 65 00 72 00 5b 00 40 00 4e 00 61 00 6d 00 65 00 3d 00 27 00 4d 00 69 00 63 00 72 00 6f 00 73 00 6f 00 66 00 74 00 2d 00 57 00 69 00 6e 00 64 00 6f 00 77 00 73 00 2d 00 55 00 73 00 65 00 72 00 20 00 44 00 65 00 76 00 69 00 63 00 65 00 20 00 52 00 65 00 67 00 69 00 73 00 74 00 72 00 61 00 74 00 69 00 6f 00 6e 00 27 00 5d 00 20 00 61 00 6e 00 64 00 20 00 45 00 76 00 65 00 6e 00 74 00 49 00 44 00 3d 00 33 00 30 00 30 00 5d 00 5d 00 3c 00 2f 00 53 00 65 00 6c 00 65 00 63 00 74 00 3e 00 3c 00 2f 00 51 00 75 00 65 00 72 00 79 00 3e 00 3c 00 2f 00 51 00 75 00 65 00 72 00 79 00 4c 00 69 00 73 00 74 00 3e 00 00 00 48 48 48 48 00 00 00 00 # unknown0 (0) 00 00 00 00 # unknown1 (0) 00 00 00 00 00 00 00 00 # unknown2 ("") 00 00 00 00 00 00 00 00 # len_value_queries (0)
TimeTrigger
Unlike all other triggers, a TimeTrigger
does not have a GenericData
field. This is, because time-based triggers allow for more fine-grained options as to when a task shall be run. The GenericData
does not have the required fields to hold all properties of these options and thus the JobSchedule
replaces it. The only significant difference compared to other tasks is that a JobSchedule
does not contain the trigger_id
field so the TimeTrigger
structure holds one instead. For all other structures, the trigger_id
is part of the GenericData
field (see below).
struct TimeTrigger { ALIGNED_DWORD magic = 0xDDDD; JobSchedule job_schedule; BSTR trigger_id; // only if header.version >= 0x16 BYTE padding[8 - (trigger_id.cbData + 4)) % 8]; // pad to multiple of 8 bytes; only if header.version >= 0x16 }
Comparing the JobSchedule
structure with the GenericData
structure, there is a significant overlap of fields (start_boundary
, end_boundary
, repetition_*
, execution_time_limit
, stop_tasks_at_duration_end
, is_enabled
, max_delay_seconds
). The most significant difference is the mode
and its data1
, data2
, and data3
fields. Depending on the value of mode, the data fields contain different bitmaps of values which, for example, represent the different days of a month:
mode 0
(ITimeTrigger): run at<start_boundary>
mode 1
(IDailyTrigger): run at<start_boundary>
and repeat every<data1>
daysmode 2
(IWeeklyTrigger): run on days of week<(data2 as day_of_week bitmap)>
every<data1>
weeks starting at<start_boundary>
mode 3
(IMonthlyTrigger): run in months<(data3 as months bitmap)>
on days<(data2:data1 as day in month bitmap)>
starting at<start_boundary>
mode 4
(IMonthlyDOWTrigger): run in months<(data3 as months bitmap)>
in weeks<(data2 as week bitmap)>
on days<(data1 as day_of_week bitmap)>
starting at<start_boundary>
struct JobSchedule { TSTIME start_boundary; TSTIME end_boundary; TSTIME unknown0; DWORD repetition_interval_seconds; DWORD repetition_duration_seconds; DWORD execution_time_limit_seconds; DWORD mode; // see above for possible values WORD data1; WORD data2; WORD data3; BYTE pad0[2]; BYTE stop_tasks_at_duration_end; BYTE is_enabled; BYTE pad1[2]; DWORD unknown1; DWORD max_delay_seconds; BYTE pad2[4]; }
Example (only the trigger structure, full example at WnfStateChangeTrigger
):
dd dd 00 00 00 00 00 00 # magic (0xdddd) 01 07 0b 00 00 00 09 00 00 78 18 25 ab 03 c7 01 # start_boundary (localized: yes, filetime: 0x01c703ab25187800; "Thu 9 November 2006 02:00:00 UTC") 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # end_boundary (localized: no, filetime: 0) 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # unknown0 00 00 00 00 # repetition_interval_seconds (0) 00 00 00 00 # repetition_duration_seconds (0) ff ff ff ff # execution_time_limit_seconds (0xffffffff) 01 00 00 00 # mode (1 -> daily) 01 00 # data1 (0x1 -> repeat every 1 day(s)) 00 00 # data2 (0) 00 00 # data3 (0) 00 00 # pad0 00 # stop_tasks_at_duration_end (false) 01 # is_enabled (true) 28 6a # pad1 01 00 00 00 # unknown1 10 0e 00 00 # max_delay_seconds (3600 "PT1H") 6f a8 28 6a # pad2 48 00 00 00 37 00 64 00 62 00 61 00 31 00 38 00 # trigger_id ("7dba1862-fdda-4030-83de-895375c111d4") 36 00 32 00 2d 00 66 00 64 00 64 00 61 00 2d 00 34 00 30 00 33 00 30 00 2d 00 38 00 33 00 64 00 65 00 2d 00 38 00 39 00 35 00 33 00 37 00 35 00 63 00 31 00 31 00 31 00 64 00 34 00 48 48 48 48 # block padding
IdleTrigger
The IdleTrigger
causes a task to be run whenever the system goes into idle mode. This trigger structure does not have additional fields besides the GenericData
.
struct IdleTrigger { ALIGNED_DWORD magic = 0xEEEE; GenericData genericData; }
For an example see RegistrationTrigger
.
BootTrigger
Tasks with BootTriggers are run by the scheduler when the system boots. These triggers structures do not have any additional fields besides the GenericData
.
struct BootTrigger { ALIGNED_DWORD magic = 0xFFFF; GenericData genericData; }
For an example see RegistrationTrigger
.
GenericData
The GenericData is a set of options which can be modified individually for each trigger. It contains, for example, the range of time in which the trigger is active (start_boundary
, end_boundary
), whether there should be a delay between activating the trigger and running the task (delay_seconds
), or for how long the task instance is allowed to run after being launched (timeout_seconds
). The structure also contains the repetition pattern for the task (“repeat task every x hours for the duration of y days”, for example) and a boolean indicating whether all task instances shall be stopped once the repetition_duration
has passed. Triggers can also be enabled
and may have assigned a trigger_id
to make them more recognizable.
struct GenericData { TSTIME start_boundary; TSTIME end_boundary; DWORD delay_seconds; DWORD timeout_seconds; DWORD repetition_interval_seconds; DWORD repetition_duration_seconds; DWORD repetition_duration_seconds_2; // seems to be always the same value as repetition_duration_seconds; probably a remnant of something no longer implemented, the XML serializer skips this value as well BYTE stop_at_duration_end; BYTE padding[3]; ALIGNED_BYTE enabled; BYTE unknown[8]; BSTR trigger_id; // only if header.version >= 0x16 BYTE pad_to_block[(8 - (trigger_id.len + 4)) % 8]; // only if header.version >= 0x16 }
For an example see WnfStateChangeTrigger
.
Auxiliary Structures
The struct definitions given above use of several other structs to define their content. This section provides the missing definitions in alphabetical order.
struct ALIGNED_BSTR { ALIGNED_DWORD cbString; WCHAR str[cbData/2]; BYTE padding[(8-(cbData%8))%8]; // pad to multiple of 8 bytes } struct ALIGNED_BSTR_EXPANDSIZE { ALIGNED_DWORD nChars; WCHAR str[nChars+1]; BYTE padding[(8-((nChars*2+2)%8))%8]; // pad to multiple of 8 bytes } struct ALIGNED_BUFFER { ALIGNED_DWORD cbData; BYTE data[cbData]; BYTE padding[(8-(cbData%8))%8]; // pad to multiple of 8 bytes } struct BSTR { DWORD cbData; WCHAR str[cbData/2]; } struct TSTIME { ALIGNED_BYTE isLocalized; FILETIME time; } struct TSTIMEPERIOD { WORD year; WORD month; WORD week; WORD day; // if used in conjunction with week this is "day of week" WORD hour; WORD minute; WORD second; }
Although there are still a few unanswered question (read: unknown elements in structures), the analysis presented in this blog post covers most of the data located in the TaskCache
in the Windows registry. However, this post mainly focuses on dissecting and describing the Actions
, Triggers
, and DynamicInfo
blobs as these contain serialized data in binary form. Understanding these binary blobs may be crucial in scenarios where, for example, a malicious party tampered with the data in the registry to hide a certain task from a user.
Definitions for all structures I identified during the analysis can be found on Github together with some tooling based on the code generated by the Kaitai struct compiler. I chose Kaitai as the struct description language so that parsers implemented in different languages can be generated easily. Please refer to the documentation located on Github for more information on how to use the provided structure definitions.