Windows Registry Analysis – Today’s Episode: Tasks

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.

Two instances of the Windows Registry Editory. The upper one shows the "Simple Task" in the Tree subkey, the lower one the task definition in the Tasks subkey of the TaskCache registry key.
References from Tasks subkey to Tree subkey in the TaskCache registry key.

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.

SubkeyDescription
BootReferences to tasks which ought to be triggered at boot time.
LogonReferences to tasks which ought to be triggered when a user logs on.
MaintenanceReferences to tasks which ought to be executed during automatic system maintenance.
PlainReferences to all tasks which are neither a boot, logon, nor maintenance task.
TasksRhe actions, settings, triggers, etc of all task data in the system organized by task id.
TreeReferences to and Security Descriptors for all tasks, organized in a tree-like structure.
Overview of the different subkeys in the TaskCache registry key.

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.

ValueDescription
IdThe UUID identifying the Task.
IndexThe type of Task (Boot Task (1), Logon Task (2), Plain Task (3), Maintenance Task (4))
SDA SECURITY_DESCRIPTOR describing the permission of the task folder (“who is allowed to see this task or the directory tree”).
The Values which can may present in the subkeys of Tree key.

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.

ValueDescription
ActionsThe actions which are to be executed when the task is triggered (e.g. “execute an application”); see below
AuthorThe author of the Task. This may be a specific string but can also be a reference to a resource DLL
DataAdditional data associated with the Task
DateThe date and time the Task was registered at
DescriptionThe description of the Task. This may be a specific string but can also be a reference to a resource DLL
DocumentationThe documentation of the Task. This may be a specific string but can also be a reference to a resource DLL
DynamicInfoDynamic information about the task; see below
HashA CRC32 or SHA256 hash of the Task XML file (in C:\windows\system32\tasks\…)
PathReference 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.
SchemaThe 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).
SecurityDescriptorThe SDDL which defines who is allowed to do what on or with a task
SourceThe source of the task. This may be a specific string but can also be a reference to a resource DLL
TriggerThe triggers of the task; see below
URISpecifies where the task is placed in the task folder hierarchy
VersionThe minimum version of the Task Scheduler Remoting Protocol compatible with this task
The list of Values which may be present in the task definitions in the Tasks subkey.

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 magic differs between all actions and the 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 seconds
  • idle_wait_timeout_seconds: “wait for idle for” converted to seconds
  • execution_time_limit_seconds: “stop the task if it’s running longer than” converted to seconds
  • delete_expired_task_after: “if the task is not scheduled to run again, delete it after” converted to seconds
  • priority: the process priority value the task scheduler assigns to the task process
  • restart_on_failure_delay: “if the task fails, restart every” converted to seconds
  • restart_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> days
  • mode 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;
}

Closing Words

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.

What the Pack(er)?

Lately, I broke one of the taboos of malware analysis: looking into the packer stub of a couple of malware samples. Fortunately, I must say. Because I discovered something I was really surprised by. But first, a little detour.

Historically, Emotet has been observed to assemble infected systems into three botnets dubbed Epoch 1, Epoch 2, and Epoch 3. After the takedown and the later resurrection, there seems to only be two botnets which have subsequently been dubbed Epoch 4 and Epoch 5. The differences between the old and the new core of the botnets are significant on the technical side – however, the old Epochs 1 through 3 shared the same core and so do the recent Epoch 4 and Epoch 5. The only noticeable difference between Epochs 1 through 3 was the config which was embedded into the Emotet core before a sample was rolled out to the victims. The same also applies to the more recent Epochs 4 and 5.

However, there is a significant difference in the operation carried out by the botnets between what happened before the disruption and what was observed since the rebirth. In the past, observations showed that Emotet bots used to drop whatever their operator’s customers paid them for. Brad Duncan alone already observed Emotet dropping QakBot/QBot, Trickbot, and Gootkit. Of these, the Trickbot group seemed to be their best and longest-running customer based on the numerous observations of Trickbot being dropped by Emotet. But after the resurrection, there were no longer observations of additional malware being dropped by Emotet. Instead, starting in December 2021, researchers observed a CobaltStrike beacon being dropped onto an infected machine without any evidence that there was another malware involved. Emotet has since been reportedly and repeatedly seen to deploy CobaltStrike beacons to infected machines, so this was definitely not a one-off drop and drew the attention of our researchers.

With the context of this analysis being setup properly, we can finally come back to the actual topic of this blog post: breaking taboos by analyzing packing stubs. Enjoy!

Poking (in) Packing Stubs

For the first period of time after the resurrection, the Emotet core seems to have used XOR encryption to hide their bot from static analysis. It can easily be seen that the algorithms appear to be (almost) identical between Epoch 4 (left) and Epoch 5 (right) – disregarding a few compiler optimizations due to different key lengths:

Emotet XOR Decrypt for Payload – Epoch 4 (left) vs Epoch 5 (right)

At some point, the authors changed the encryption scheme to use RC4 instead of plain XOR. Although the code applying the RC4 algorithm looks different thanks to a substantial amount of superfluous API calls, there are obvious similarities between Epoch 4 on the left and Epoch 5 on the right:

The surprising discovery we made during the week preceeding the publication of this post is related to the CobaltStrike drops. Assuming from what was observed for Epochs 1 through 3, thoughts were that some other party paid the Emotet operators to drop CobaltStrike as their desired payload. Having a closer look at the samples reveals an interesting observation: all of the CobaltStrike drops used packing stubs which looked extremely familiar. The drops referred to in the following were received on March 11th, however, these specific packing stubs were already observed earlier for Emotet drops. Unfortunately, we did not see the connection until a couple of days ago. But have a look for yourself:

As it can be seen in both examples, Drop A used the packer which was observed in the early days after the rebirth while Drop B used the same packer as the Emotet core itself at the time of writing this post.

Conclusion or (Educated) Guessing

Prior to the rebirth, drops were not bound to the operation of Emotet  – the botnet was known to drop whatever their operator’s customers paid them for; but since the resurrection, this seems to have shifted towards drops which are very tightly-bound to the Emotet core and thus the operation as well. Considering that Trickbot was used to revive the Emotet botnet back in november 2021 and the observation that Emotet since then only dropped CobaltStrike beacons to infected machines, one thought may arise: have the Trickbot operators perhaps invited their old friends from Emotet over to work for the Conti group as well? It has long been said that the Emotet operators are closely related to the Trickbot group because of their long-running partnership. The thought is also supported by information from the Conti playbook leak in 2021 where it can be seen that Conti makes heavy use of CobaltStrike as a reconnaissance tools before deploying their ransomware. AdvIntel also suspected that Emotet arose as part of the Conti group. The now-discovered use of identical packers for both the Emotet core and the CobaltStrike drops supports the claim in a fascinating way.

Alternatively, or additionally, the resurrection of Emotet may have been the final step in replacing Trickbot as the initial foothold of the Conti group in their victim’s networks by putting their remaining Trickbot bots to a last use. It cannot be denied that Emotet was a surprisingly efficient malware so the Conti operators may have gone for using both Emotet and BazarLoader to access their victim’s networks: with the Trickbot developers focusing solely on BazarLoader and the Emotet operators back into the business, this leaves the Conti group with two independent and powerful tools to access infected machines.

Remarks

Of course, at the same time the author made the aforementioned discovery, researchers observed another drop being delivered by Emotet: SystemBC. It remains to be seen whether this was a one-time delivery in the sense of a test or if researcher will see this drop more often in the future.

Reference Samples

c7574aac7583a5bdc446f813b8e347a768a9f4af858404371eae82ad2d136a01 – old Emotet Epoch 4 sample (2021-11-15)

1c9f611ce78ab0efd09337c06fd8c65b926ebe932bc91b272e97c6b268ab13a1 – old Emotet Epoch 5 sample (2021-11-18)

8494831bbfab5beb6a58d1370ac82a4b3caa1f655b78678c57ef93713c476f9c – recent Emotet Epoch 4 sample (2022-03-14)

31f7e5398c41d7eb8d033dbc7d3b90a2daf54995e20b5ab4a72956b41c8e1455 – recent Emotet Epoch 5 sample (2022-03-15)

cf7a53b0e07f4a1fabc40a5e711cf423d18db685ed4b3c6c87550fcbc5d1a036 – CobaltStrike Drop A (2022-03-11)

73aba991054b1dc419e35520c2ce41dc263ff402bcbbdcbe1d9f31e50937a88e – CobaltStrike Drop B (2022-03-11)

Guess who’s back

tl;dr: Emotet

The (slighty) longer story:
On Sunday, November 14, at around 9:26pm UTC we observed on several of our Trickbot trackers that the bot tried to download a DLL to the system. According to internal processing, these DLLs have been identified as Emotet. However, since the botnet was taken down earlier this year, we were suspicious about the findings and conducted an initial manual verification. Please find first results and IOCs below. Currently, we have high confidence that the samples indeed seem to be a re-incarnation of the infamous Emotet.

We are still conducting more in-depth analyses to raise the confidence even further. New information will be provided as they become available.

Initial Analysis

Sunday, November 14, 9:26pm: first occurence of the URLs being dropped; the URL we received was hxxp://141.94.176.124/Loader_90563_1.dll (SHA256 of the drop: c7574aac7583a5bdc446f813b8e347a768a9f4af858404371eae82ad2d136a01). Internal processing detected Emotet when executing the sample in our sandbox systems. Notably, the sample seems to have been compiled just before the deployment via several Trickbot botnets was observed: Timestamp : 6191769A (Sun Nov 14 20:50:34 2021)

The network traffic originating from the sample closely resembles what has been observed previously (e.g. as described by Kaspersky): the URL contains a random resource path and the bot transfers the request payload in a cookie (see image below). However, the encryption used to hide the data seems different from what has been observed in the past. Additionally, the sample now uses HTTPS with a self-signed server certificate to secure the network traffic.

Network Traffic originating from the DLL

A notable characteristic of the last Emotet samples was the heavy use of control-flow flattening to obfuscate the code. The current sample also contains flattened control flows. To illustrate the similarity in the style of the obfuscation, find two arbitrary code snippets below. Left side is a sample from 2020, on the right is a snippet from the current sample:

Conclusion (so far)

As per the famous duck-typing, we conclude so far: smells like Emotet, looks like Emotet, behaves like Emotet – seems to be Emotet.

We are currently updating our internal tooling for the new sample to provide more indicators to strengthen the claim that Emotet seems to be back.

IOCs

URLs:
hxxp://141.94.176.124/Loader_90563_1.dll

Hashes:
c7574aac7583a5bdc446f813b8e347a768a9f4af858404371eae82ad2d136a01 - Loader_90563_1.dll

Server List:
81.0.236.93:443
94.177.248.64:443
66.42.55.5:7080
103.8.26.103:8080
185.184.25.237:8080
45.76.176.10:8080
188.93.125.116:8080
103.8.26.102:8080
178.79.147.66:8080
58.227.42.236:80
45.118.135.203:7080
103.75.201.2:443
195.154.133.20:443
45.142.114.231:8080
212.237.5.209:443
207.38.84.195:8080
104.251.214.46:8080
138.185.72.26:8080
51.68.175.8:8080
210.57.217.132:8080

String List:
SOFTWARE\Microsoft\Windows\CurrentVersion\Run
POST
%s\rundll32.exe "%s",Control_RunDLL
Control_RunDLL
%s\%s
%s\%s
%s\%s%x
%s%s.exe
%s\%s
SHA256
HASH
AES
Microsoft Primitive Provider
ObjectLength
KeyDataBlob
%s\rundll32.exe "%s\%s",%s
Content-Type: multipart/form-data; boundary=%s

RNG
%s%s.dll
%s\rundll32.exe "%s",Control_RunDLL
%s%s.dll
%s\regsvr32.exe -s "%s"
%s\%s
%s%s.exe
SOFTWARE\Microsoft\Windows\CurrentVersion\Run
%s\rundll32.exe "%s\%s",%s
ECCPUBLICBLOB
ECDH_P256
Microsoft Primitive Provider
ECCPUBLICBLOB
Cookie: %s=%s

%s\rundll32.exe "%s\%s",%s
%s:Zone.Identifier
%u.%u.%u.%u
%s\%s
%s\*
%s\%s
WinSta0\Default
%s\rundll32.exe "%s",Control_RunDLL %s
%s%s.dll
ECCPUBLICBLOB
ECDSA_P256
Microsoft Primitive Provider
%s\%s
SHA256
Microsoft Primitive Provider
ObjectLength

Trickbot rdpscanDll – Transforming Candidate Credentials for Brute-Forcing RDP Servers

After some weeks of not seeing the RDP scanner module of Trickbot, I recently observed that the module was again distributed among the bots in our tracking lab. Since Bitdefender already published a report on the module in March 2020, I focused on checking whether or not the command-and-control (C2) communication of the module remained more or less the same or if there was anything groundbreakingly new. Short answer: there wasn’t. There may be some under-the-hood fixes or improvements but I (as of yet) did not stumble upon anything significant that wasn’t already found by Bitdefender: the module still receives its mode of action, target servers, usernames, and password candidates from the C2 server and then does what the mode tells it to do. But while I was checking that, I also had a look at the actual data that we received from the C2 server.

Password List

My intuition on the password list was that it is just a dictionary of words to try. This is also suggested by the URL which is used to retrieve the password list: hxxps://%c2%/%gtag%/%bot_id%/rdp/dict. Thus I did not have a closer look at the password list at that time, because everything looked the way Bitdefender described it and I had no reason to look at it in detail. But one or two days later, I re-requested the list of passwords to see whether the list changed in the meantime – and it did indeed. Because of that I had a quick look at what changed and then I noticed that I overlooked something right from the start (literally, duh!). On the left side of the picture you see what I had a quick look at after retrieving the password list from the C2 server with curl (and thus seeing only the last lines of the output). On the right side there is the very same password list, just seen from the start.

trickbot_passwordlist

To the keen eye it seems that they may be using some kind of templating mechanism to adjust the list of passwords and use more specific credential candidates. With that thought in mind I spun up my analysis environment and started digging into the module to see what the Trickbot gang is actually doing there (spoiler: yes, they do some kind of templating – but not just the find-and-replace kind).

Transforming them P@ssw0rds

As mentioned before, this is not a simple find-and-replace but instead they can change the credential candidates to better fit the attacked host. In that sense, I decided to call those things transforms instead of templates because they are not just templates that are filled out but a little bit more dynamic. Example:

  • %username%123 → myuser123 (lowercase)
  • %Username%123 → Myuser123 (lowercase but first char uppercase)
  • %UsErNaMe%123 → MyUsEr123 (alternating case, starting with uppercase char)
  • %EMANRESU%123 → RESUYM123 (uppercase and reversed)

And that is essentially how the markers in the password list work. I was able to extract all 91 transformations that are currently available to the rdpscanDll (as of 2020-08-14). Please find the list with all transforms with an example and a description for each of them at the end of this blog post.

Some of the transforms can even be parameterized to a certain degree: %OriginalUsername%, %OriginalDomain%, and %domain% can be prepended or appended with an (N) to indicate whether the first N or last N characters of the element should be used (or everything if no parameters are present).

Reconnaissance

After finding the list of transforms, I decided to ask my favorite internet search engine whether these names for the transforms are known related to RDP. And I indeed found a RDP brute force tool by a certain z668 which seemingly makes use of some of the transforms that are used in the rdpscanDll. Although this tool seems to be a standalone application, the names of the transforms and the context of their use could suggest a connection between z668 and the Trickbot gang – at least to a certain degree. Sure, the connection may not be really strong because the Trickbot module is written in C++ and the RDP tool seems to be written in C#. But given the fact that C# can load and use native DLLs and considering that z668 forked the FreeRDP project on Github, the actual scanner may indeed be written in C/C++ (and probably using FreeRDP). Thus it is possible that the Trickbot gang may have obtained the source code from z668 to integrate the RDP scanner into their module framework and to use their C2 communication protocol. But: this is just guessing based on some more or less loose facts – I could easily be completely wrong with that.

Transform List

Transform Identifier Example Description
EmptyPass tries an empty password
GetHost fills in the hostname of the currently attacked IP (ex: myhost)
IP the currently attacked IP address (ex: 234.234.234.234)
Port fills in the currently attacked port (ex: 3389)
IpReplaceDot 234.234.234.234 → 234234234234 remove the dots of the IP address
RemoveNumerics us3rn4me → usrnme removes all number from the username
RemoveLetters us3rn4m3 → 343 removes all letters from the username
RemoveOtherSymbols usern@m3 → usernm3 removes all non-alphanumeric characters from the username
OriginalUsernameLettersBeginInverse 123admin456 → 123654nimda keeps all non-letters (i.e. digits, special chars) at the beginning of the username and reverses the rest (“invert [from where] letters begin
OriginalUsernameLettersBeginSwap 123admin456 → admin456123 swaps all non-letters (i.e. digits, special chars) at the beginning of the username with the rest (“swap [where] letters begin”)
OriginalUsernameLettersEndInverse admin123root → admintoor321 keeps all letters at the beginning of the username and reverses the rest (“invert [where] letters end”)
OriginalUsernameLettersEndSwap admin123root → 123rootadmin swaps all letters at the beginning of the username with the rest (“swap [where] letters end”)
OriginalUsernameNumsBeginInverse admin123root → admintoor321 keeps all non-digits at the beginning of the username and reverses the rest (“invert [from where] nums begin
OriginalUsernameNumsBeginSwap admin123root → admintoor321 swaps all non-digits at the beginning of the username with the rest (“swap [where] nums begin”)
OriginalUsernameNumsEndInverse 123admin → 123nimda keeps all digits at the beginning of the username and reverses the rest (“invert [where] nums end”)
OriginalUsernameNumsEndSwap 123admin456 → admin456123 swaps all digits at the beginning of the username with the rest (“swap [where] nums end”)
OriginalUsernameInsert %OriginalUsernameInsert%(N)SOMESTRING → SOMEusernameSTRING (ex: N = 4) insert username after Nth character of SOMESTRING
OriginalUsername use the username as password
OnlyName Firstname Lastname → Firstname uses only the first name (everything left of the first space) of the username as password
OnlySurname Firstname Lastname → Lastname uses only the last name (everything right of the first space) of the username as password
username Admin → admin username in lowercase
Username AdMin → Admin username lowercase but first char upper
UsErNaMe Admin → AdMiN username in alternating case, starting with uppercase
uSeRnAmE Admin → aDmIn username in alternating case, starting with lowercase
USERNAME Admin → ADMIN username in uppercase
EMANRESU Admin → NIMDA username in uppercase and reversed
EmanresuLowercase AdMin → Nimda username reversed and lowercase, first char uppercase
Emanresu AdMin → NiMdA username reversed, first char upper
emanresuLowercase AdMin → nimda username reversed and lowercase
emanresuUppercase AdMin → NIMDA username reversed and uppercase
emanresu Admin → nimda username reversed and lowercase
ReplaceFirst_X-x administrator → @dministrator (ex: %ReplaceFirst_a-@%) replaces the first occurrence of X with x in the username (needle and replacement can be more than 1 char)
ReplaceFirstI_X-x Administrator → @dministrator (ex: %ReplaceFirstI_a-@%) case insensitively replaces the first occurrence of X with x in the username (needle and replacement can be more than 1 char)
ReplaceLast_X-x administrator → @dministrator (ex: %ReplaceLast_a-@%) replaces the last occurrence of X with x in the username (needle and replacement can be more than 1 char)
ReplaceLastI_X-x Administrator → @dministrator (ex: %ReplaceLastI_a-@%) case insensitively replaces the last occurrence of X with x in the username (needle and replacement can be more than 1 char)
ReplaceAll_X-x administrator → @dministrator (ex: %ReplaceAll_a-@%) replaces all occurrences of X with x in the username (needle and replacement can be more than 1 char)
ReplaceAllI_X-x Administrator → @dministrator (ex: %ReplaceAllI_a-@%) case insensitively replaces all occurrences of X with x in the username (needle and replacement can be more than 1 char)
DomainRemoveNumerics test-123.com → test-.com removes all digits from the domain
DomainRemoveLetters test-123.com → -123. removes all letters from the domain
DomainRemoveOtherSymbols test-123.com → test123com removes all non-alphanum chars from the domain
OriginaldomainInsert %OriginaldomainInsert%(N)SOMESTRING → SOMEdomainSTRING (ex: N = 4) insert domain after Nth character of SOMESTRING
OriginaldomainPart test-123.com → 123com (ex: %OriginaldomainPart%(6)) takes the last N chars of the domain name (ignoring any dots)
OriginaldomainNumsBeginInverse test-123.com → test-moc.321 keeps all non-digits at the beginning of the domain and reverses the rest (“invert [from where] nums begin
OriginaldomainNumsBeginSwap test-123.com → 123.comtest- swaps all non-digits at the beginning of the domain with the rest (“swap [where] nums begin”)
OriginaldomainNumsEndInverse 123-test.com → 123moc.tset- keeps all digits at the beginning of the domain and reverses the rest (“invert [where] nums end”)
OriginaldomainNumsEndSwap 123-test.com → -test.com123 swaps all digits at the beginning of the domain with the rest (“swap [where] nums end”)
OriginaldomainLettersBeginInverse test-123.com → test-moc.321 keeps all non-letters (i.e. digits, special chars) at the beginning of the domain and reverses the rest (“invert [from where] letters begin
OriginaldomainLettersBeginSwap 123-test.com → test.com123- swaps all non-letters (i.e. digits, special chars) at the beginning of the domain with the rest (“swap [where] letters begin”)
OriginaldomainLettersEndInverse test-123.com → testmoc.321- keeps all letters at the beginning of the domain and reverses the rest (“invert [where] letters end”)
OriginaldomainLettersEndSwap test-123.com → -123.comtest swaps all letters at the beginning of the domain with the rest (“swap [where] letters end”)
Originaldomainleft test-123.com → test-123 takes the left part of the domain (everything left of the first dot) and lowercases the first character
OriginalDomainleft test-123.com → Test-123 takes the left part of the domain (everything left of the first dot) and capitalizes the first character
Originaldomainright test-123.com → test-123 takes the right part of the domain (everything right of the first dot) and lowercases the first character
OriginalDomainright test-123.com → Test-123 takes the right part of the domain (everything right of the first dot) and capitalizes the first character
Originaldomain uses the plain domain name
OriginalDomain test-123.com → Test-123.com uses the domain name and capitalizes the first character
NiamodLowercase abc%NiamodLowercase%123 abc123
niamodLowercase test-123.com → Moc.321-tset reverses and lowercases the domain name, first character capitalized
niamodUppercase test-123.com → mOC.312-TSET reverses and capitalizes the domain name, first char lowercase
domainleftHyphen test-123.com → test takes everything left of the first hyphen
DOMAINLEFTHYPHEN test-123.com → TEST takes everything left of the first hyphen, capitalized
DomainleftHyphen test-123.com → Test takes everything left of the first hyphen, first char capitalized
domainrightHyphen test-123.com → 123.com takes everything right of the first hyphen
DOMAINRIGHTHYPHEN test-123.com → 123.COM takes everything right of the first hyphen, capitalized
DomainrightHyphen test-abc.com → Abc.com takes everything right of the first hyphen, first char capitalized
domainleftUnderscore test_123.com → test takes everything left of the first underscore
DOMAINLEFTUNDERSCORE test_123.com → TEST takes everything left of the first underscore, capitalized
DomainleftUnderscore test_123.com → Test takes everything left of the first underscore, first char capitalized
domainrightUnderscore test_abc.com → abc.com takes everything right of the first underscore
DOMAINRIGHTUNDERSCORE test_123.com → 123.COM takes everything right of the first underscore, capitalized
DomainrightUnderscore test_abc.com → Abc.com takes everything right of the first underscore, first char capitalized
DomainReplaceFirst_X-x EXAMPLE-attack.com → EXAMPLE-@ttack.com (ex: %DomainReplaceFirst_a-@%) replaces the first occurrence of X with x in the domain (needle and replacement can be more than 1 char)
DomainReplaceFirstI_X-x EXAMPLE-attack.com → EX@MPLE-attack.com (ex: %DomainReplaceFirstI_a-@%) case insensitively replaces the first occurrence of X with x in the domain (needle and replacement can be more than 1 char)
DomainReplaceLast_X-x EXAMPLE-attack.com → EXAMPLE-att@ck.com (ex: %DomainReplaceLast_a-@%) replaces the last occurrence of X with x in the domain (needle and replacement can be more than 1 char)
DomainReplaceLastI_X-x EXAMPLE-attack.com → EXAMPLE-att@ck.com (ex: %DomainReplaceLastI_a-@%) case insensitively replaces the last occurrence of X with x in the domain (needle and replacement can be more than 1 char)
DomainReplaceAll_X-x EXAMPLE-attack.com → EXAMPLE-@tt@ck.com (ex: %DomainReplaceAll_a-@%) replaces all occurrences of X with x in the domain (needle and replacement can be more than 1 char)
DomainReplaceAllI_X-x EXAMPLE-attack.com → EX@MPLE-@tt@ck.com (ex: %DomainReplaceAllI_a-@%) case insensitively replaces all occurrences of X with x in the domain (needle and replacement can be more than 1 char)
niamod test-123.com → moc.321-tset reverses the domain name
Niamod test-123.com → Moc.321-tset reverses the domain name, first char capitalized
domainleft TEST-123.com → test-123 everything left of the first dot, lowercased
DOMAINLEFT Test-123.com → TEST-123 everything left of the first dor, capitalized
Domainleft test-123.com → Test-123 everything left of the first dot, lowercased but first char capitalized
domainright TEST-123.com → com everything right of the first dot, lowercased
DOMAINRIGHT Test-123.com → COM everything right of the first dor, capitalized
Domainright test-123.com → Com everything right of the first dot, lowercased but first char capitalized
domain TEST-123.com → test-123.com domain name, lowercase
Domain TEST-123.com domain name lowercased, first char capitalized
DoMaIn test-123.com → TeSt-123.cOm domain name in alternating case, starting with uppercase
dOmAiN test-123.com → tEsT-123.CoM domain name in alternating case, starting with lowercase
DOMAIN test-123.com → TEST-123.COM domain name capitalized
NIAMOD test-123.com → MOC.321-TSET domain name reversed and capitalized

One framework to build them all, one framework to name them, and in their IDBs to bind them

Authors: Luca Ebach, Tilman Frosch

Rejoice everyone, today we pushed bindifflib to our Github! Bindifflib is a framework to build a set of libraries with a set of different compilers, currently the compilers of Visual Studio 2010, 2013, 2015, and 2017 – both 32 bit and 64 bit. After compilation, bindifflib will import all DLLs into IDA Pro and will use the Program Database (PDB) files to properly name (almost) all functions in the IDB.

We have created bindifflib out of the need speed up our understanding of binary code dropped at our doorstep, mostly malware. Occasionally, we encounter larger binaries that smell of statically linked libraries. Figuring out which part of the binary is a library, which library specifically, and which part is actually relevant, often takes some time that could be spent better and is also not very exciting. Fortunately there is BinDiff! Having some functions already identified allows us to spend the time to understand the purpose of the code and the authors‘ intentions instead of first digging through the basic capabilities as provided by knowing that a certain library was used in general. So let‘s just create some targets to diff a unknown binary against, so that we can focus our reversing efforts on the non-generic parts of the binary. Having just one library compiled by one compiler didn’t really cut it, so it was time to automate things to have a selection of likely or frequently used libraries compiled with a set of popular compilers. Naturally, one could also replace „likely“ and „frequently“ by „vulnerable“ and head into a completely different direction that bindifflib can help with.

We consider the code more a proof of concept than a software product. Use at your own risk, we love feedback!

Find the code here: https://github.com/GDATAAdvancedAnalytics/bindifflib

Zeus Panda: Down To The Roots

Some time ago, we analyzed Panda’s webinjects to get an insight in how they actually work and to understand their communication with the ATS servers (read it here: part 1, part 2).

In the last few weeks, we drilled down on the binary itself and had a closer look on this side of the Zeus.Panda malware. In the resulting whitepaper, we present a more in-depth analysis of the malware executable, detailing the malware’s actions on the victim’s PC beyond and in addition to infecting browsers to enable fraudulent banking transactions.

Find the whitepaper here (pdf).