Defeating VMProtect’s Latest Tricks

A colleague of mine recently came across a SystemBC sample that is protected with VMProtect 3.6 or higher. VMProtect is a commercial packer that comes with advanced anti-debugging and VM detection capabilities. It also employs code virtualization – a technique where normal machine code is translated into a proprietary bytecode language that is interpreted at runtime – which makes it very hard to determine the exact logic implemented by the code. ScyllaHide, our anti-anti-debug tool of choice, was not up to the task of hiding the debugger from the packer, so we dove into the unexpectedly deep rabbit hole of figuring out what is going on.

Kernel mode tooling such as TitanHide/HyperHide would probably have been up to the task of defeating most of the checks, but we prefer user mode tooling, since it is much less complicated to use and easier to debug.

Debugger checks

On the face of it, VMProtect’s debugger checks don’t use any exotic techniques. We have seen the following checks, all of which Scylla has long since had support for:

  • PEB.BeingDebugged
  • ProcessDebugPort
  • ProcessDebugObjectHandle
  • NtSetInformationThread ThreadHideFromDebugger
  • CloseHandle with invalid handle value
  • Non-zero debug registers in CONTEXT when catching exceptions

If you’d like to get a deeper understanding of these techniques, please refer to Check Point Research’s excellent Anti-Debug Tricks page.

The first problem we encountered is that when debugging a 32-bit executable on a 64-bit system, it is possible for the executable to hide code from the debugger by doing a far jump into the 64-bit segment 0x33 (also dubbed “Heaven’s Gate“). In particular, most of the anti-debug checks will be executed in 64-bit mode. x32dbg/x64dbg can only debug their respective bitness, but not both at once. The only debugger we know of that can achieve such a feat is WinDbg, but its interface is rather unpleasant to deal with. So we switched to a native 32-bit system for this sample.

The next problem is that VMProtect has built-in syscall tables for the most common Windows builds. If it finds that it runs on a known system version, it uses the sysenter instruction to directly call into kernel mode for its checks, bypassing any user mode hooks (there is ProcessInstrumentationCallback, but it has limits – the callback is triggered after a syscall has executed but before it returns, which means we cannot prevent changes such as setting the ThreadHideFromDebugger flag). The obvious approach to deal with this is to change the build number to a fake number everywhere, so that the packer has no choice but to call the APIs the regular way.

Enter the myriad ways of obtaining the Windows build number these days.

ScyllaHide already supports patching the OsBuildNumber field in the PEB (Process Environment Block), which is also what APIs such as GetVersion/GetVersionEx read:

        if (flags & PEB_PATCH_OsBuildNumber)

Doing a simple increment comes with a little caveat. What if you happen to be on a recent Windows 10 build such as 19043 or 19044? 19044/19045 are both valid builds. Oops.

In a ScyllaHide issue, Mattiwatti, who is one of the maintainers of ScyllaHide, already outlined the next technique that VMProtect uses on modern Windows versions. KUSER_SHARED_DATA is a read-only page that is mapped into every Windows process. It allows quick access to many commonly needed values, such as versions, current tick count, current time, and much more. APIs such as GetTickCount access this page in order to avoid a context switch to the kernel. Since Windows 10 the structure also contains the build number. This is unfortunate because ScyllaHide cannot modify this value. But it is possible to set an access hardware breakpoint and change the value in the register after VMProtect has read it. Alternatively, Windows 8.1 or older can be used.

If these ways have proven unfruitful for obtaining a known build number, VMProtect will suspect that we’re up to no good and starts inspecting system library versions. It will parse NTDLL’s version resource, which contains plenty occurrences of the build number. ScyllaHide patches one of them (the FileVersion string), which apparently was sufficient at some point in the past. Not anymore. Nowadays, VMProtect inspects all four build numbers (two in binary form, two in strings). So we adjusted ScyllaHide to set all of them to a fake version.

Memory breakpoints on other libraries’ resource sections have not been hit, so that should suffice for fooling the packer, right? Right?! Nope. You can imagine that at this point we were quite confused how it still managed to find the correct version.

After some more tracing, we found the call sequence NtQueryVirtualMemory (to get NTDLL’s full path on disk) → NtOpenFileNtCreateSectionNtMapViewOfSection. This maps a fresh copy of the NTDLL image into memory. Oh, well. They call APIs, we hook APIs. One somewhat mean detail is that NtCreateSection is called with the flag SEC_IMAGE_NO_EXECUTE. This prevents image load notify routines and debugger events from being raised when the image is loaded, however the flag is only supported since Windows 8. As a result, anything packed with this VMProtect version will not run on Windows beta builds from before Windows 8, and incidentally this also comes to bite us when faking the version on a Windows 7 system – VMProtect knows the usual build numbers of Windows 7 (7600/7601) and it would ordinarily never take this code path. Since we’re hooking anyway, we change this value to SEC_IMAGE when detecting an older OS and everyone is happy.

If you thought we’re finally rid of the direct syscalls now, think again. VMProtect has one final trick up its sleeve: it tries to extract syscall numbers from the library code. We expected this all along, but it makes sense that it only happens on the fresh mapping from disk. This way, the packer can avoid any hooks and other code patches placed on the regular NTDLL image in memory. That is, until we come in and deliberately destroy some API entrypoints in the mapping (smile). The packer expects the first instruction to be mov eax, CallNumber, and if it cannot find that, it finally gives up and calls the regular NTDLL API export.

VM Checks

Overview of VM checks:

  • cpuid hypervisor bit & hypervisor vendor
  • Trap Flag tricks in combination with forced VM exit via rdtsc/cpuid
  • NtQuerySystemInformation with SystemFirmwareTableInformation, TableIDs FIRM and RSMB
  • (presence of sbiedll.dll in process, for Sandboxie detection)

For further reading about these checks, please refer to Check Point Research’s Evasion techniques page (particularly, the “CPU” and “Firmware tables” sections).

The first is relatively easy to mitigate by disabling paravirtualization, which will remove any hypervisor information from cpuid.

The second trick is somewhat mean and took us a while to figure out. Consider the following code block:

<prepare flags value with TF bit (0x100) on stack>
popfd    ; apply flag change
cpuid    ; force VM exit
nop      ; filler for EIP check
push ebx ; next regular instruction

The Trap Flag provides single stepping functionality for debuggers. If you set it, the processor will raise an interrupt after executing the following instruction. So we expect the instruction pointer in the exception that the OS gives us to be at the nop. As it turns out, older VirtualBox versions will rat you out, because they have a bug that causes EIP to be at the push instead. This is fixed in version 7.0.4, which was pretty recent at the time of writing.

Finally, VMProtect will inspect some firmware bits. The RSMB provider is used to obtain raw SMBIOS values such as BIOS vendor, BIOS version, system family, system UUID, etc. VirtualBox also has custom OEM fields for “VBoxRev” and “VBoxVer”. It is possible to change all of these through VM configuration changes (VBoxManage setextradata). The FIRM provider is a different story. It allows reading 128K at physical addresses 0xC0000 and 0xE0000, respectively. These ranges contain BIOS Option ROM code, which may contain some strings that give away that virtualization is in use. This cannot be helped without modifying and recompiling the VM software.

What we can do is hook NtQuerySystemInformation and return empty data. Since ScyllaHide hooks that API anyway, we simply integrated a code path for the SystemFirmwareTableInformation class.


With the aforementioned counter-measures (or rather, counter-measures for the counter-measures) in place, we can finally debug and unpack the sample. Fun fact: The sample is packed twice, so after VMProtect has finished initialization, another layer of packer code runs and unpacks the final SystemBC malware. This is not very smart, because this nullifies the effects of VMProtect’s import protection, making it trivial to obtain a fully functional dump. Malware packers often lack the sophistication of commercial grade packers.

We’ve committed all ScyllaHide code changes to GitHub and are currently waiting for them to be accepted upstream.


e21f50a1794acd0a585c86a157e8f70b044adcc860d6d0648d874deccd7ba653 (SystemBC sample)

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:

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.

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.

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.

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


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:

    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

    // 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.


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];
    BYTE data[cbData];


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)


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)


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


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)


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;
    DWORD unknown0;
    DWORD unknown1;
    ALIGNED_DWORD 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)


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


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.


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.


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.

    ALIGNED_DWORD cbString;
    WCHAR str[cbData/2];
    BYTE padding[(8-(cbData%8))%8]; // pad to multiple of 8 bytes

    WCHAR str[nChars+1];
    BYTE padding[(8-((nChars*2+2)%8))%8]; // pad to multiple of 8 bytes

    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;

    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.


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:// (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.



c7574aac7583a5bdc446f813b8e347a768a9f4af858404371eae82ad2d136a01 - Loader_90563_1.dll

Server List:

String List:
%s\rundll32.exe "%s",Control_RunDLL
Microsoft Primitive Provider
%s\rundll32.exe "%s\%s",%s
Content-Type: multipart/form-data; boundary=%s

%s\rundll32.exe "%s",Control_RunDLL
%s\regsvr32.exe -s "%s"
%s\rundll32.exe "%s\%s",%s
Microsoft Primitive Provider
Cookie: %s=%s

%s\rundll32.exe "%s\%s",%s
%s\rundll32.exe "%s",Control_RunDLL %s
Microsoft Primitive Provider
Microsoft Primitive Provider

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.


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).


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:
Port fills in the currently attacked port (ex: 3389)
IpReplaceDot → 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 → removes all digits from the domain
DomainRemoveLetters → -123. removes all letters from the domain
DomainRemoveOtherSymbols → 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 → 123com (ex: %OriginaldomainPart%(6)) takes the last N chars of the domain name (ignoring any dots)
OriginaldomainNumsBeginInverse → test-moc.321 keeps all non-digits at the beginning of the domain and reverses the rest (“invert [from where] nums begin
OriginaldomainNumsBeginSwap → 123.comtest- swaps all non-digits at the beginning of the domain with the rest (“swap [where] nums begin”)
OriginaldomainNumsEndInverse → 123moc.tset- keeps all digits at the beginning of the domain and reverses the rest (“invert [where] nums end”)
OriginaldomainNumsEndSwap → -test.com123 swaps all digits at the beginning of the domain with the rest (“swap [where] nums end”)
OriginaldomainLettersBeginInverse → 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 → 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 → testmoc.321- keeps all letters at the beginning of the domain and reverses the rest (“invert [where] letters end”)
OriginaldomainLettersEndSwap → -123.comtest swaps all letters at the beginning of the domain with the rest (“swap [where] letters end”)
Originaldomainleft → test-123 takes the left part of the domain (everything left of the first dot) and lowercases the first character
OriginalDomainleft → Test-123 takes the left part of the domain (everything left of the first dot) and capitalizes the first character
Originaldomainright → test-123 takes the right part of the domain (everything right of the first dot) and lowercases the first character
OriginalDomainright → 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 → uses the domain name and capitalizes the first character
NiamodLowercase abc%NiamodLowercase%123 abc123
niamodLowercase → Moc.321-tset reverses and lowercases the domain name, first character capitalized
niamodUppercase → mOC.312-TSET reverses and capitalizes the domain name, first char lowercase
domainleftHyphen → test takes everything left of the first hyphen
DOMAINLEFTHYPHEN → TEST takes everything left of the first hyphen, capitalized
DomainleftHyphen → Test takes everything left of the first hyphen, first char capitalized
domainrightHyphen → takes everything right of the first hyphen
DOMAINRIGHTHYPHEN → 123.COM takes everything right of the first hyphen, capitalized
DomainrightHyphen → takes everything right of the first hyphen, first char capitalized
domainleftUnderscore → test takes everything left of the first underscore
DOMAINLEFTUNDERSCORE → TEST takes everything left of the first underscore, capitalized
DomainleftUnderscore → Test takes everything left of the first underscore, first char capitalized
domainrightUnderscore → takes everything right of the first underscore
DOMAINRIGHTUNDERSCORE → 123.COM takes everything right of the first underscore, capitalized
DomainrightUnderscore → takes everything right of the first underscore, first char capitalized
DomainReplaceFirst_X-x → (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 → (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 → (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 → (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 → (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 → (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 → moc.321-tset reverses the domain name
Niamod → Moc.321-tset reverses the domain name, first char capitalized
domainleft → test-123 everything left of the first dot, lowercased
DOMAINLEFT → TEST-123 everything left of the first dor, capitalized
Domainleft → Test-123 everything left of the first dot, lowercased but first char capitalized
domainright → com everything right of the first dot, lowercased
DOMAINRIGHT → COM everything right of the first dor, capitalized
Domainright → Com everything right of the first dot, lowercased but first char capitalized
domain → domain name, lowercase
Domain domain name lowercased, first char capitalized
DoMaIn → TeSt-123.cOm domain name in alternating case, starting with uppercase
dOmAiN → tEsT-123.CoM domain name in alternating case, starting with lowercase
DOMAIN → TEST-123.COM domain name capitalized
NIAMOD → MOC.321-TSET domain name reversed and capitalized

Dissecting Olympic Destroyer – a walk-through


After a destructive cyber attack had hit this year’s olympics, the malware was quickly dubbed Olympic Destroyer.  Talos were fast to provide initial coverage. A malware explicitly designed to sabotage the computer systems of the Olympic opening ceremony sounded very interesting, but other duties were more pressing at that time, so analysis for pure curiosity had to wait.  A few weeks later I had some free evenings on my hands and decided to combine a few interests of mine: Listening to music, consuming high quality whisky and analyzing malware – regrettably one of those things is frowned upon at work, and it’s not malware analysis. 😉

I had most of the binaries reversed and already written up a few pages, when Kaspersky released an article with some more details than previously publicly known. Having finished my work and focusing on the technical aspects of Olympic Destroyer, I think I can add several technical details about the malware. In the following expect plain and straight-forward binary analysis and reverse engineering in the form of a walk-through.

Olympic Destroyer comes in two types. The first one is a little bit simpler. It was discovered by Talos, who published it in their comprehensive blog post. One example of this type has the Sha256 of edb1ff2521fb4bf748111f92786d260d40407a2e8463dcd24bb09f908ee13eb9.

The second type of the binary has, to the best of my knowledge, not yet been explicitly named, but it was implicitly analyzed by Kaspersky in their also very comprehensive blog post. One example has the Sha256 sum of e8349cfcc422310c259688b0226cb14f5196a6daad77b622405282aeac89ab06.

In the following blog post I will mainly describe the first type of Olympic Destroyer. At the end I will discuss the main differences between the two types, which revolve around the usage or non-usage of the well-known tool PsExec.

The Orchestrator

In this part we will cover the innermost functionality of the Olympic Destroyer. As orientation point we will use the main() function, from where on we will cover the single function calls step by step. Luckily Olympic Destroyer runs single threaded – except for the spreading functionality – which makes it easier to follow the execution one call after another.

The analyzed orchestrator has a Sha256 of edb1ff2521fb4bf748111f92786d260d40407a2e8463dcd24bb09f908ee13eb9 and is 0x1C6800 (~1.7MB) in size. A lot of this size is made up of five resources, whose role will be explained later on. IDA detects 756 functions of which not even ten were automatically identified by IDA FLIRT in version 6.9, which made the analysis more time consuming. IDA version 6.95 seems to have a newer FLIRT database and a lot of functions are identified automatically, as I realized way too late.

Configuration – or not

The main() function is located at 0x004071E0. It creates a structure on the local stack, which I called “config” when starting to reverse the binary. Over the time I discovered that it is merely a state or a singleton data container – nonetheless I kept the name “config” for reasons of consistency. This structure is carried throughout many of the subsequent function calls, most of the time in the form of thiscalls in the ecx register. You can find the whole structure in the appendix section below.

It contains different type of data, like simple integers, which for example describe the bitness of the OS with either the value 32 or 64 by dynamically resolving and calling IsWow64Process. Yes, the author(s) actually use full integers instead of encoding this information in a simple bit ¯\_(ツ)_/¯.

Detecting the bitness of the system

More interesting are probably the different paths of files dropped to the filesystem during runtime, which are also stored in this structure. I will describe them when writing about the resources.

Additionally, we find some security related variables, like the security token information of the current user, which is gathered by calling GetTokenInformation(TokenUser) and comparing the result against “S-1-5-18” (Local System), “S-1-5-19” (NT Authority Local Service), and “S-1-5-20” (NT Authority Network Service).

Most important is probably the vector of objects, which contains domain names and domain credentials, that are used to spread laterally through the network. More about this later on when I will cover the lateral movement.

After the config structure is initialized in 0x00406390 by nulling its members, it is dynamically filled with its respective values in the subsequent call to 0x00406500, where most of the previously mentioned values and information is generated. From then on, the config is ready for use and most values are only read instead of written – except for the file paths, which are generated more or less randomly on the fly when used and of course the credential vector, which gets expanded a few calls later.

Magical Code Injections

With a call to 0x004066C0 Olympic Destroyer checks for the existence of two files, which it uses to mark and avoid multiple runs of itself:

  1. C:\<MD5(Computer Name)>
  2. %SystemDrive%\Users\Public\<MD5(Computer Name\User Name)>

If one of those files is found, the function which I called “selfDeleteInjectBinary” at 0x00405DD0 is executed. Most of this function is already described in Endgame’s blogpost at, but somehow they either misinterpreted the feature, or missed the main point of the shellcode. I’m not sure what their intention was, but their blog post somehow does not say what the shellcode actually does ¯\_(ツ)_/¯.

Olympic Destroyer starts an invisible “notepad.exe” by using the flags CREATE_NO_WINDOW in dwCreationFlags as well as STARTF_USESHOWWINDOW in StartupInfo.dwFlags and SW_HIDE in StartupInfo.wShowWindow before calling CreateProcessW.

Starting an invisible Notepad

Then it injects two blocks of data/code into the running notepad by calling VirtualAllocEx and WriteProcessMemory. The first block contains addresses of APIs and the path to the current executable, the second one is a shellcode which uses the addresses of the first block. By calling CreateRemoteThread the execution of the shellcode within notepad is started.

After this injection, the main process exits by calling ExitProcess while the execution of the injected thread runs in the process space of notepad. But all the shellcode does, is a simple delayed self-deletion mechanism:
First it sleeps a configurable number of seconds. In our case it is five seconds. After that, the shellcode looks for the original path of Olympic Destroyer, which was passed by the first injected memory block by checking GetFileAttributesW != INVALID_FILE_ATTRIBUTES. Then it tries to open the file with CreateFileW, gets the file size by calling GetFileSize and then loops over the file size and calls WriteFile with always one zero byte until the whole file is overwritten with zeros. After closing the file handle, the file is finally deleted with DeleteFile and the shellcode calls ExitProcess to end its execution.

To sum it up: The code injection is simply a nulling and deletion mechanism to hide the traces of the main binary.

Setting the markers for self-deletion

The two files, which mark multiple runs of Olympic Destroyer, mentioned in the previous paragraph, are then created:

Create infection markers

Depending on the rights under which the binary is running, the markers in C:\ and %COMMON_DOCUMENTS% are created. From then on, a second run of Olympic Destroyer will wipe the original executable.

Stealing credentials

In the following call to 0x004065A0, we will dive into a very important feature of Olympic Destroyer: The stealing of credentials from the current system, which are later used for lateral movement.

Olympic Destroyer contains five resources of type “BIN”. All of those resources are encrypted with AES. The calculation of the key is hard coded in the binary and can be described as a trivial MD5 hash of the string “123”. This hash is then concatenated twice in order to reach a key length of 256 bits for the AES algorithm. The evaluation of whether those shenanigans of symmetric cryptography with a hard coded key makes sense is left as an exercise for the reader. 🙂

When stealing the credentials, at first the resource 101 is decrypted, written to a more or less randomly generated filename in %tmp%. “More or less” because the algorithm is based on calls to GetTickCount() with Sleeps in between the calls.

After writing the decrypted resource to disk, a proper random string is generated by calling CoCreateGuid. The GUID is then used as the name for a named pipe in the form \\.\pipe\, which is created by calling CreateNamedPipeW and then used as inter process communication mechanism with the process, which is then started from the file written to %tmp%.

Resource 101

When resource 101 is started, it also gets the name of the pipe to communicate with its parent process as well as the password “123” as arguments. The main task of resource 101 is to use the password to decrypt and execute another resource of type “BMP” embedded in the file of resource 101 and send a buffer with stolen credentials to its parent process. So, it’s a simple loader which transfers a buffer via IPC.

The BMP resource is a DLL called “BrowserPwd.dll”. This DLL is not written to disk but parsed and loaded in memory. It seems that its only purpose is to steal credentials from the browsers Internet Explorer, Firefox and Chrome. In order to work with Firefox and Chrome, an SQLite library is compiled into the DLL, which makes up most of the DLL’s code.

  • For Internet Explorer it uses COM to iterate over the browsers history and then reads all autocomplete passwords from the registry in Software\Microsoft\Internet Explorer\IntelliForms\Storage2 and decrypts them using the WinAPI CryptUnprotectData.
  • For Firefox, the credentials are stolen from sqlite and logins.json. The nss3.dll from Firefox is used to decrypt the protected passwords.
  • For Chrome, the user’s database in […]\Application Data\Google\Chrome\User Data\Default\Login Data is copied temporarily and then the credentials are read and decrypted by calling the WinAPI CryptUnprotectData.

All stolen credentials are returned in a buffer which uses a special style of separating the single items. This buffer is constructed within the DLL and returned to its original loader, which is resource 101:

Stolen credentials are formatted in a certain way

This buffer is then sent from the loader via the named pipe to its parent process:

The loader of resource 101 uses the names pipe to transfer the buffer with the stolen credentials

Resource 102 and 103

After resource 101 was executed, a second attempt to steal credentials is started, in case the current process could acquire debug privileges during initialization of the config object. In case it has those right, depending on the architecture of the operating system, either resource 102 (x86) or 103 (x64) is started. Both executables have the same logic as resource 101 – decrypt and load a DLL in memory, execute the DLL and return its buffer via IPC –  only the payload in form of their internal DLL, the resource of type “BMP”, is different. Everything else stays the same.

So, the question is, what are the DLLs in the resources of resource 102 and 103? For 103, the x64 version, I did not look into it in order to save some time, but I assume it’s the very same payload as in 102, only for x64 systems. For 102, which is an x86 binary, the loaded internal DLL seems to be a custom version of the well-known penetration testing tool Mimikatz, which, besides other nifty features, can dump credentials from a Windows system. I did not spend too much time in the analysis of this DLL, but a swift look (as in “1-2 hours”), compared with several matching functions, structures and strings from the original code of Mimikatz are strong indicators that this DLL has actually Mimikatz’ credential dumping capability. This assumption was also verified by dynamic analysis, where the binary was actually stealing the credentials of my analysis machine. Additionally, the author(s) of Olympic Destroyer named this DLL “kiwi86.dll”, which is a reference to the nickname “gentilkiwi”, who is the author of Mimikatz.

After receiving the stolen credentials via the named pipes, Olympic Destroyer parses the received buffers and saves the credentials in its config structure. Then it returns its control flow to the main function.

Saving the Credentials – Or how to build a network worm

Back in the main function, right after stealing credentials from browsers and by the power of Mimikatz, Olympic Destroyer creates a copy of itself in the %tmp% folder in 0x00404040. If this copy succeeds, the copied file is modified in the next function call to  0x00401FB0. Here the whole file is read into a buffer in the process’ memory. Then this buffer is searched for the byte marker 9E EC 87 D4 89 16 42 09 55 E2 74 E4 79 0B 42 4C. Those bytes mark the beginning of the serialized credentials vector as an array:

Hex dump of Olympic Destroyer

I tried to mark the single elements of the array in different colors to describe them, but it turns out my MS Paint skills are really bad. So, you’ll just get a two pseudo structs defining what you can see around the red marked bytes:

struct credentials
byte marker[16];
WORD numberOfElements;
CREDENTIAL credentialArray[numberOfElements];
WORD lengthOfUsername;
WORD lengthOfPassword;
char userName[lengthOfUsername];
char password[lengthOfPassword];

In our case there are 0x2C stolen credentials. The first block of credentials has a username\domain string of 0x1B bytes length and has a password of 0x0C bytes length. Then the second block of credentials follows, and so on.

Once Olympic Destroyer has located the array in its buffer, the array is written over with the serialized version of the current credentials vector of the config object. Then the executable modified in memory is written back to disk in the %tmp% directory.

In other words: The list of credentials, which was present when Olympic Destroyer was executed first, is now updated with all credentials stolen during runtime.

Resource 104 and 105 – Preparing the next steps

After updating a copy of itself with all stolen credentials, the execution flow returns to the main function where two consecutive calls to 0x00403F30 prepare the network spreading algorithm and the destructive parts. Both calls take a resource name as a first parameter for input and return a string with a path to a file. In this function Olympic Destroyer takes the same decryption algorithm as previously described and decrypts the resources 104 and 105. Both files are not yet executed but written to disk with a random filename in the %tmp% folder.

Resource 104 is a simple copy of the well-known tool “PsExec” which can be used to execute commands and files on remote computers. It will come into play when I describe the lateral movement.

Resource 105 though is the actual “Destroyer” of Olympic Destroyer.

Starting the Destroyer – Fulfilling the real purpose

After writing resource 104 and 105 to %tmp%, the function at 0x00404220 is called with the path to resource 105 as an argument. Here nothing magical happens. The file from the resource is simply executed without a visible window/console and the function returns:

Starting an invisible process

From here on the destroyer from resource 105 is running. It has its own chapter later on.

Lateral Movement

Once the destroyer part of Olympic Destroyer has been started in its own process, the main function calls 0x00406ED0 to start the network spreading routine.

At first two sanity checks are made by calling GetFileAttributesA in order to ensure that PsExec from resource 104 and the copy of Olympic Destroyer with the updated credentials list in the %tmp% folder exist. If both checks pass, a list of potential targets within the local network is built:
With a call to 0x00406DD0 Olympic Destroyer utilizes the GetIpNetTable API to enumerate all IPv4 addresses of the current ARP cache, thus getting all IP addresses the local machine had access to – considering ARP cache timeouts which can remove older entries, of course.

The list of IPv4 addresses is then passed to the function at 0x004054E0, along with a pointer to the config object as well as the path to PsExec and the updated copy of Olympic Destroyer in the %tmp% folder. I think it is noteworthy that passing both paths to the files in %tmp% is completely superfluous, since they are already a part of the config object, which is also passed as argument.

The function at 0x004054E0 is the heart of the spreading algorithm:
First, it reads the updated copy of Olympic Destroyer into memory. Then it initializes a new structure with all information passed as arguments as well as some additional information, which is somehow not really used later on. After that it calls 0x00407680, where the spreading in the network begins:
For each IP address, a new thread is spawned, which starts at 0x00407D40. This thread then loops over all credentials of the config object, trying to use WMI via COM objects in order to infect remote computers:

Remote command execution

The first important function for that is at 0x004045D0 (called executeRemoteCmdline), which gets one IP and one pair of credentials as input, as well as one command line to execute on the target machine – outPtr is used to transport the return value. The whole function is a mess of COM calls, but I’ll try to explain their meaning anyways. Words in italic are quotes from the binary:
This function creates a COM object of CLSID {4590F811-1D3A-11D0-891F-00AA004B2E24} and IID {dc12a687-737f-11cf-884d-00aa004b2e24} in order to remotely execute WMI commands. Then a connection to \\\root\CIMV2 is created and the credentials are applied by calling CoSetProxyBlanket. With the class Win32_Process and the function Create a Commandline is executed on the remote computer. With “Select * From Win32_ProcessStopTrace” the event for the termination of the remote process is registered in order to read its ExitStatus code afterwards.
The executed command line is rather simple:

“cmd.exe /c (ping > nul) && if exist %programdata%\\evtchk.txt (exit 5) else ( type nul > %programdata%\\evtchk.txt)”

With the execution of ping a short delay is introduced, since the execution waits for ping to fail four times to ping the address Then, in case the file %programdata%\evtchk.txt exists on the target system, the execution returns the exit code five. Otherwise said file is created and the execution finishes with its standard error code of zero.

The return value of the remote command line is then read and is returned via outPtr as a function argument from 0x004045D0. Interestingly the outPtr is only written to in case of a successful remote execution. All error cases leave the outPtr untouched. As the memory address of the target of outPtr is initialized with zeros, the caller of 0x004045D0 is unable to distinguish between an error during the remote code execution (e.g. because of false credentials or an unavailable IP) and the successful write of %programdata%\evtchk.txt file on the remote machine. ¯\_(ツ)_/¯

At 0x00404C30 the second interesting function (called writeFileToRemoteRegistryAndExecuteCommandlineVbs) is located. It takes the target IP address as well as the credentials as input. It is very similar to the function 0x004045D0 described previously. The main difference is that by using the StdRegProv class and the function SetBinaryValue a registry key in HKEY_CURRENT_USER\Environment with the name Data is created on the remote computer. The value of the registry key an executable file, but interestingly it is not the copy of Olympic Destroyer with the updated credential list in %tmp%, as I would have expected, but it is the binary which is currently executed and thus does not contain any of the current system’s credentials:

The remote spreading algorithm spreads the wrong binary

After the binary is written to the remote registry, the function at 0x00404C30 calls the function at 0x004044B0. Here the function Create of the COM class Win32_Process is used to remotely execute another command line. This command line is already known from the Talos blog post. For readability I pretty-printed the commands:

cmd.exe /c
echo strPath = Wscript.ScriptFullName
& echo.Set FSO = CreateObject^(\”Scripting.FileSystemObject\”^)
& echo.FSO.DeleteFile strPath, 1
& echo.Set oReg = GetObject^(\”winmgmts:{impersonationLevel=impersonate}!\\\\.\\root\\default:StdRegProv\”^)
& echo.oReg.GetBinaryValue ^&H80000001, \”Environment\”, \”Data\”, arrBytes
& echo.Set writer = FSO.OpenTextFile^(\”%ProgramData%\\%COMPUTERNAME%.exe\”, 2, True^)
& echo.For i = LBound^(arrBytes^) to UBound^(arrBytes^)
& echo.s = s ^& Chr^(arrBytes^(i^)^)
& echo.Next
& echo.writer.write s
& echo.writer.close
) > %ProgramData%\\_wfrcmd.vbs && cscript.exe %ProgramData%\\_wfrcmd.vbs && %ProgramData%\\%COMPUTERNAME%.exe

The first set of echos outputs parts of a VB script, which are then written to %ProgramData%\_wfrcmd.vbs by using the redirect operator “>”. Afterwards this file is executed via the cscript interpreter before the executable %ProgramData%\%COMPUTERNAME%.exe is executed. This executable is created during the runtime of the newly created VB script, which basically just reads the executable stored in HKEY_CURRENT_USER\Environment\Data and writes it to %ProgramData%\%COMPUTERNAME%.exe.

Back in 0x00405170, the function at 0x004045D0 (executeRemoteCmdline) is called a second time. This time it removes the file %programdata%\evtchk.txt, which was previously checked or created on the remote computer by executing the command line “del %programdata%\evtchk.txt”.

To state the obvious, in case it got lost in all the text: %programdata%\evtchk.txt is intended as a mutex object on the remote computer, which marks that a remote infection is currently ongoing. This avoids that two computers running Olympic Destroyer’s infection routine infect the same target at the very same time. Yet, as this file is deleted right after the infection, it does not avoid multiple infections of the same target in general, but only in parallel.

While all previously mentioned remote infection threads are running, the main thread waits for their termination by calling WaitForMultipleObjects, where it waits for all spawned threads to finish.

Once all threads are finished and back in the function 0x00406ED0, the control flow enters a loop, which iterates over all credentials and passes them to the function at 0x00406780. This function also has the purpose of enumerating network targets. Once again COM objects are involved:
One main part of this function is the call to NetGetDCName, which gets the name of the primary domain controller. This name is formatted into the string “%s\\root\\directory\\LDAP” in order to use it with the same COM objects as before during the remote code execution (CLSID {4590F811-1D3A-11D0-891F-00AA004B2E24} and IID {dc12a687-737f-11cf-884d-00aa004b2e24}) by using the credentials, which are passed as function arguments. If everything works so far, the statement “SELECT ds_cn FROM ds_computer” is executed in order to get all computer names from the current domain. Then, for each computer, by calling GetAddrInfoW and ntohl the domain names are resolved to IPs. A vector of IPs is returned from 0x00406780. The IPs are then passed to the already known function at 0x004054E0 in order to infect those computers remotely.

When this IP enumeration and remote infection loop is finished, some objects and memory is cleaned up before the control flow returns back to the main function.

Self-Deletion – Or how to hide your traces, well, at least one of the many…

The last step in the main function, before freeing the remaining objects and memory, is the call to the already described function “selfDeleteInjectBinary” at 0x00405DD0. This time the sleep interval is only three instead of five seconds. So the spawned process tries to wipe the binary of the parent process every three seconds until it succeeds. The control flow of Olympic Destroyer then leaves the main function and the process exits, which will make the wiping of the binary possible.

I think it is noteworthy that none of the other dropped files are deleted. Everything in %tmp% remains and also all infection markers described previously are still there.

The Destroyer

A big part of this component’s functionality can be described in one picture by looking at the main function:

Destroyer main function

After giving itself the SeShutdownPrivilege and bluntly ignoring all potential erroneous API calls, the Destroyer calls the function at 0x00401000 (“execProcAndWaitForTerminate”) five times in a row in order to:

  1. Delete all shadow copies without prompt to avoid restoring the system
  2. Silently delete all backups created by the tool wbadmin
  3. Ignore all failures during boot and avoid starting the recovery mode
  4. Clear system logs
  5. Clear security logs

Then the function at 0x004012E8 (“deactivateAllActiveServices”) is called. The name in the screenshot is already a spoiler of the actual functionality: All services of the local computer are disabled. This is done by iterating over all possible types of services by calling EnumServicesStatusW with the dwServiceType parameter set to 0x13F and dwServiceState to SERVICE_STATE_ALL, and then calling ChangeServiceConfigW(SERVICE_DISABLED) for each service.
In combination with the previously disabled recovery mode and deleted backups, this bricks the local system on the next boot.

Back in the main function a thread is spawned which executes the function 0x004016BF (“wiperThread”). The main thread then sleeps for a fixed single hour before shutting down the system – no matter what the wiper thread did or didn’t do in the meantime. Note that this might also interrupt the spreading routine of Olympic Destroyer, which might still run.

The first thing the wiper thread does is setting its own thread priority to THREAD_PRIORITY_TIME_CRITICAL in order to get as much CPU cycles as possible. Then it recursively iterates over all available network resources with the APIs WNetOpenEnumW and WNetEnumResourceW. Each available resource is temporarily mounted by calling WNetAddConnection2W(CONNECT_TEMPORARY), yet the parameters for the username and password are set to zero, thus the current user’s credentials are used. It is important to note that the stolen credentials are not used here. This decouples the Destroyer logically from its parent process.
For each successfully mounted resource the function at 0x00401441 is called.
This function is also best described with a screenshot:

Remote wiping functionality

This function simply iterates recursively over all folders, starting at the mountpoint which is provided as an argument, and then destroys each single file that it finds:

  1. Files equal or smaller to 1MB in size are completely written over with zeros
  2. For files bigger than 1MB only the first 4096 bytes are nulled. Yet for most files this should be enough to render them useless

The wiper thread does not communicate with the main thread and there is no synchronization in any way. No matter if the wiping already finished or not, the system is shut down after one hour.
It might be a simple mistake to shut the system down after a fixed time: The wiping may not have wiped everything it can reach, or it could have already finished and the local computer is still useable until the shutdown. Additionally the remote spreading could still be ongoing.

Yet, I think it is more likely that this feature is a well-planned and sophisticated time bomb: Imagine Olympic Destroyer spreading through a network, wiping all it could wipe for one hour, when suddenly one system after another shuts down and is unable to boot.

Different types of Olympic Destroyer

As mentioned in the introduction, I found two different types of Olympic Destroyer. The simpler type was described previously. The second type has the very same functionality, it only adds a few more functions. Those additional functions have the purpose of extending the spreading functionality of Olympic Destroyer by leveraging PsExec, which was written to %tmp% but then ignored by the simpler version.

Using PsExec

The additional function call is placed right after writing/checking the file %programdata%\evtchk.txt and before the spreading function which uses COM objects and spreads the version of Olympic Destroyer which was not updated with the stolen credentials. This bugged behavior of spreading the wrong binary over COM exists in both versions.

The additional call to PsExec is done in the following way:

Format string for calling PsExec

PsExec is started with several parameters:

  • The first three parameter identify the target computer and the credentials which are applied
  • Then the dialogue to confirm the EULA of PsExec is skipped with “accepteula”
  • “-d” runs PsExec in a non-interactive way, which means that the caller does not wait for PsExec to terminate
  • With “-s” the remote process is started with System rights (in case the credentials allow that)
  • “-c” and “-f” specify that the actually executed file is copied to the target computer and overwritten in case it already exists
  • The last parameter is the remotely executed file, which is obviously Olympic Destroyer

This time the remotely executed binary is the copy of Olympic Destroyer in %tmp%, which was updated with the credentials stolen during the current run.

The output buffer returned from PsExec is parsed for the string “started”, which indicates to Olympic Destroyer that its call was successful. A successful remote infection using PsExec breaks the loop which iterates over the credentials for a fixed target computer. Thus the target is only infected once and the bugged COM infection is avoided.

A crippled worm and a capable worm

The simple version of Olympic Destroyer has some spreading functionality, although it is broken in the sense that the wrong binary is spread through the network. By not spreading the updated version of Olympic Destroyer, which contains the credentials stolen during the run, it loses a crucial part of its spreading capability:

Assume we have a computer “A” with a logged in user who has the rights which allow remote spreading of Olympic Destroyer. And a computer “B”, which is in reach of A, but where no user is logged in. A third computer “C” is only reachable over B but not over A.
If the simple version of Olympic Destroyer is executed on computer A, it will use the stolen credentials to infect computer B. But on computer B there are no credentials to steal, so it won’t be able to infect computer C.

In other words: The simple version of Olympic Destroyer can only spread to computers which are “one hop” in distance.
Yet, in most cases this should still be enough to infect a whole network, since a central Domain Controller is usually connected to most computers in the network.

Spreading the more advanced version including the stolen credentials gives Olympic Destroyer even better worming capabilities, since it gathers more and more credentials as it spreads further and further through a network.
In the previous example computer C could be infected from computer B by using the credentials stolen on computer A.

Crunching some numbers

In order to verify my findings with the two versions of Olympic Destroyer, I grabbed 36 different samples which are identified as Olympic Destroyer and compared their sets of stolen credentials. One sample had and empty list of credentials, so I discarded it.

It turns out that 23 of those samples are from the simple version type. All of them contained the same set of credentials, which were already described by Talos. They are for the domains g18.internal and All of the samples contained additional credentials stolen from various sandbox systems and virtual machines of researchers, who probably uploaded the files from the %tmp% folder to Virus Total during their analysis.

I could not find a single sample which contained only a subset of the credentials stolen from the g18.internal and domains. If you strip the credentials from sandboxes and researchers, all 23 samples contain the same set of crendetials. This supports the findings that the simple version of Olympic Destroyer has a broken spreading algorithm.

In contrast to that, 12 samples of the total 36 are from the ATOS network with the domain ww930, as partially described by Kaspersky. Apparently the more capable version of Olympic Destroyer was spreading here, thus the differences in the list of credentials is bigger. The first pair of credentials in this set can be found in all 12 samples. But the rest of the credentials is a mix stolen from different computers in the same network. We can see that the worm took different paths when spreading though the network, acquiring the credentials of at least five different computers.

After removing the credentials from researchers and sandboxes, we are left with five unique sets of credentials. If one subset of credentials is one letter, the sets can be described as A, AB, AC, AD and ADE. This shows that the more capable version of Olympic Destroyer actually inherits its list of stolen credentials to the infected systems.
The samples in question are:


It is likely that there are more samples out there which give a better picture of the way Olympic Destroyer wormed itself through the ATOS network.

PE timestamps

As the blog article of Kasperky has already shown, the author(s) of Olympic Destroyer had quite the fun in planting false flags. So, the compilation time stamps of the PE files should be taken with a grain of salt, as they can be easily forged.
Nonetheless they provide an interesting picture.

Simple version of Olympic Destroyer, PE time stamps ordered ascending:

Name Compilation Time Stamp Description
Resource 104 2016-06-28 18:43:09 Copy of PsExec
Resource 105 2017-12-27 09:03:48 Destroyer
DLL in Resource 101 2017-12-27 11:44:17 Browser Password Stealer
DLL in Resource 102 2017-12-27 11:44:21 Windows Account Password Stealer
Resource 101 2017-12-27 11:44:30 Loader for internal DLL
Resource 103 2017-12-27 11:44:35 Loader for internal x64 DLL
Resource 102 2017-12-27 11:44:40 Loader for internal DLL
Main binary 2017-12-27 11:44:47 Olympic Destroyer

(Note that I did not extract the time stamp for the DLL in the resource of resource 103)

The PE time stamps of the more complex version in ascending order:

Name Compilation Time Stamp Description
Resource 104 2016-06-28 18:43:09 Copy of PsExec
Resource 105 2017-12-27 09:03:48 Destroyer
DLL in Resource 101 2017-12-27 11:38:53 Browser Password Stealer
DLL in Resource 102 2017-12-27 11:38:58 Windows Account Password Stealer
Resource 101 2017-12-27 11:39:06 Loader for internal DLL
Resource 103 2017-12-27 11:39:11 Loader for internal x64 DLL
Resource 102 2017-12-27 11:39:17 Loader for internal DLL
Main binary 2017-12-27 11:39:22 Olympic Destroyer

Some of those values actually make sense, although they might have been crafted in order to do so. The DLLs which are resources of resource 101 and 102 have to be compiled before they can be embedded as resources, so their time stamps come first. The same goes for all resource which are embedded in them main binary of Olympic Destroyer.
PsExec in resource 104 is the original copy of PsExec, thus has the original time stamp.
A time difference of four to nine seconds for each binary sounds realistic, given only a few dependencies on external libraries. Unfortunately, the compilation with the biggest external dependencies, the DLL in resource 101 where SQLite is used, seems to be the first binary in the build chain. This is where I would have expected to see the biggest gaps in between the time stamps. But as it is the start of the build chain, we cannot compare it to any binary built before it.
By looking at the gaps, we can also see that everything except the destroyer part seem to be compiled in one block.
Also the more complex version of Olympic Destroyer seems to be compiled five minutes before the simpler version. Most probably the attacker(s) just compiled the first set of Olympic Destroyer, before commenting out the one function using PsExec (implicitly removing all the used sub-functions), and then recompiled the whole set.

It is noteworthy to point out that both versions of Olympic Destroyer use the very same copy of the destroyer component. Not only the compilation time stamps are the same, but also their hash sums.



This article has shown the innermost working of the malware called Olympic Destroyer. We have seen that by pure reverse engineering of the malware samples, a plethora of information can be obtained and deduced.

The analysis indicates that Olympic Destroyer consists of two completely independent parts: The first one is a framework for network spreading using resource 101 to 104 in order to spread as fast and as far as possible in the local network. The second one is the destructive component. Both parts work completely independent from each other. Resource 101 to 103 have a strong logical dependency on the main binary by receiving the decryption key as well as the name of the named pipe as arguments. And the main binary depends on the information returned from the resources 101 to 103 formatted in a certain way. In contrast to that, the destroyer in resource 105 is only dropped and executed in a fire and forget manor. No arguments, return values or status codes are exchanged.
So I think it is correct to state that everything except the destroyer is merely a vehicle in form of a spreading framework to deliver a payload. And the delivered payload is the destroyer. In theory every other payload could be delivered by simply exchanging the resource 105.

We have also seen that Olympic Destroyer comes in two different versions, which have been spread in two different networks. The spreading algorithm differs in the way that credentials stolen on one system are not carried on to the next infected system in one version.
It is unknown to me why the differences exist. Reading the Kasperky article indicates that the attackers already had a strong foothold in the g18.internal and network. So it might have been enough to spread only one hop from the initial infected machine.  This decision could also be influenced by the defensive mechanisms employed at the targeted network. A proper network monitoring tool should mark the execution of PsExec as red flag, which might have been the reason to remove this part of the spreading algorithm.
The analysis of stolen credentials in the network of ATOS indicates that the attackers had a weaker foothold in the network, since, juding by the samples I looked at, only two sets of credentials were stolen on the initial infection (compared to 44 in the simpler variant). All other credentials were added during the spreading in the network. This weak foothold might have been the cause to go with a more aggressive spreading algorithm.


Config structure as used in Olympic Destroyer:

struct config
DWORD credentialsVectorStart;
DWORD credentialsVectorEnd;
DWORD credentialsVectorMaxSize;
WSADATA wsadata;
char ressourceHpath[1024];
char randomTempPath[1024];
char ressourceIpath[1024];
char selfModulePath[1024];
char domainName[256];
char accountName[256];
char domainAndAccountName[256];
char v13[256];
DWORD bitness;
DWORD bVersionGreaterEqualVista;
DWORD bVersionSmallerEqualXP;
DWORD bHasSelfDebugPrivs;
DWORD bIsServiceOrAdmin;
DWORD bIsUserAccount;

Hashes used for analysis:


Emotet drops ZeuS Panda targeting German and Austrian online banking users

Emotet is currently one of the prevalent threats on the Internet. The former banking trojan is now known to steal passwords and to drop other malware like Dridex on its infected machines. We recently found Emotet spreading Zeus Panda, which presented us with an opportunity to link some of our research on Emotet with our analysis of ZeuS Panda.  The Zeus Panda sample used in this wave is rolled out through Emotet in german-speaking countries and targets online banking users in Germany and Austria.

The Emotet C2 server drops additional malware to infected system. Whether a system receives such a package seems to be based on the geographical location of the infected system in question. After the additional malware is downloaded from the C2 server, it is written to a file in %ALLUSERSPROFILE% (C:\ProgramData in recent Windows versions) with a random name of 4 to 19 characters length and the file extension “.exe”. Emotet is capable of executing this binary in two different ways, either of which is chosen by the C2 server. The first mode executes the malware in the same context that Emotet is running in, the second mode executes the malware in the context of the currently logged-on user.

As stated above, the current wave downloads and executes the well-known ZeuS Panda banking trojan. To know which banking sites it should attack and how to modify the site’s content, the trojan needs so-called webinjects. From the URL masks of the webinjects this sample uses, we can tell that it currently targets online banking customers in Germany and Austria. All injects write a single script reference into the targeted websites. When the targeted site is loaded, the browser loads the referenced script, which is then executed in the context of the banking website. The only difference between the webinjects is the last number in the URL of the script source. This number seems to define the targeted website, which allows the server to deliver a target-specific script. The script actually downloaded is obfuscated by a simple string encryption. The actual script is part of an Automated Transfer System (ATS) which tries to persuade the user into transferring money to an account the attacker specifies.

scriptSchutzanalageRetoureThe above screenshots show an exemplary representation on the modification of the banking websites. They show two different attack scenarios: The first script tries to trick the user into performing an transaction in the guise of a security check. The attackers “inform” the customer of newly installed security measures on the banking website, coercing the user to complete a training using a demo account, before they are able to access their account again. During this training, a real transaction is made in the background to an account that the attacker specifies. The phrasing in the text is lousy and should raise suspicion with most customers.


The second script tries to persuade the user that an erroneous transfer was made to their account. It suggests to go to a bank branch or make the return transfer online. Additionally, the script blocks access to the banking account until the return transfer has been completed. The phrasing in the text is better than in the first script and may not raise suspicion at first glance.

The first script resembles word by word the webinject Kaspersky identified during their analysis of Emotet in 2015. At this time Emotet contained its own banking trojan capability and delivered the webinjects directly into the browser. As ZeuS Panda uses the same webinject format as the old Emotet, we can speculate about the reasons:

  • The webinject is acquired from the same creator
  • The group behind Emotet has dropped developing their own banking trojan and acquires such trojans from other malware authors
  • The group behind Emotet developed multiple banking trojans for its own use and for sale

It seems Emotet is not only used to sell distribution of malware, but also used by its owners. It is also possible that the group behind Emotet uses the slim downloader as an entry point for targeted attacks. In this case the group can spread Emotet worldwide and distribute specific malware to each target. As the real malicious payload is only downloaded after some time and only to specific targets, analysts can not directly draw conclusions on the real intention of an infection.






ZeuS Panda:




Emotet harvests Microsoft Outlook

The original German blog post can be found on the G DATA Blog.

Emotet has been known as a trojan for years. Former versions focused on attacking online banking users, however the current Emotet was  transformed into a downloader and information stealer. The first reports of this new variant were published by CERT Polska in April 2017. Since then, Emotet has been spreading through spam phishing mails containing a link to a Microsoft Word document that acts as dropper for the Emotet binary.

Recently, CERT-Bund again warned about the spam mails which spread Emotet. The sender address of these emails is spoofed to appear as a sender known to the recipient. This strengthens the trust in the mail and increases the probability that the recipient opens the attachment or link without further consideration.

For this to work, the entities spreading Emotet need to have at least superficial knowledge of the social network a target interacts with via email. Acting opportunistically Emotet delivers a specific module to infected systems to harvest all emails in Microsoft Outlook accounts of the current user, allowing it to extract the relations between sender and receiver.



To obtain the information from Outlook, the module takes advantage of the standardized interface MAPI. The picture above shows the loading of the MAPI-DLL and the retrieval of the needed functions. Utilizing this interface, the module iterates through all Outlook profiles it can access on the computer. It extracts all E-Mail-Account Names and E-Mail-Addresses from each profile. Afterwards it searches for emails recursively in each folder in the profile. From each mail found it extracts the sender (displayed name and mail address) and all recipients (displayed names and mail addresses) inclusive the recipients in the CC- and BCC-fields and saves them in relation to each other. The picture below shows the extracted fields from the emails. In case a field only contains a reference to an address book entry, the module extracts the name and email address from the address book. In this process only the mail header is evaluated, the content of the mails is not analyzed.


After the Emotet module has searched all profiles, folders, and emails, it writes the data it has retrieved in a temporary file in the directory %PROGRAMDATA%. The email addresses are sorted descending by how often they occur. Each address is extended with all contacts, that are in relation to it. However, two cases are distinguished:

  • if the referenced contact is the sender of the mail, it is extended with all recipients
  • if the referenced contact is the recipient of the mail, it is only extended with the sender

Example (Mailbox of A):mail

Mail 1: A sends to B and C

Mail 2: D sends to A

Mail 3: C sends to A , D, and E

A is referenced three times and therefore is placed on top of the list. A has a relation to B and C through mail 1, thus B and C get connected with A. Mail 2 shows a connection from D to A, thus D gets connected with A too. The relation from C to A in mail 3 is ignored, because it is already captured in mail 1 (A→C). Mail 3 contains the additional relations C→D and C→E. As no relations between C↔D and C↔E are already in the list, the contacts D and E get assigned to the contact C and are appended to the list.

The complete list, which gets transferred to the attacker, looks like this:

A<>; B<>; C<>; D<>
C<>; D<>; E<>

Afterwards the module encrypts the file, transfers it to the attacker and removes it from disk.

This  allows the attacker to get a condensed but comprehensive overview of the social network graph behind a victims email communications. With such a list, an attacker has knowledge of the relation between persons and can send spam mails with suitable sender header without great afford. Additionally, an attacker learns relations between contacts whose computers are not yet infected.

To deliver the spam mails to the suitable recipients, the attacker needs valid E-Mail accounts. For this task, they use an additional module that is able to extract the credentials from mail programs and transfer them to the attackers. To extract the credentials from all common mail programs, such as Microsoft Outlook, Mozilla Thunderbird, and Windows Mail, this module utilizes an integrated copy of the application Mail PassView from the company NirSoft. It writes this information to a temporary file, which is then encrypted and transfered to the attacker. Once transfered the temporary file is deleted.

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).


Zeus Panda Webinjects: Don’t trust your eyes

In our last blog article Zeus Panda Webinjects: a case study, we described the functionality of current Zeus Panda webinject stages and gave some insight into the corresponding administration panel. As we only scratched the surface of the target specific second webinject attack stage (in the following we reference this as 2nd attack stage), we would like to share more details about this part.

Basically, the 2nd attack stage already includes the complete code needed for the attack. The different code branches are triggered by setting status variables, especially the branch variable already introduced in our previous article on that topic. Last time we also introduced the send() function, which is used to exfiltrate data. send() isn’t entirely unidirectional: the HTTP response of this request includes further code that is evaluated as JavaScript. Thereby the backend is able to set the different status variables to trigger the existing code branches of the 2nd attack stage. Let’s dive into the details of this communication protocol:

Communication protocol and status variables

Figure 1: Communication protocol

Figure 1 illustrates the communication protocol between the 2nd attack stage and the backend server. We see the different steps of the communication, the branches triggered, and the website on which the step occurs. Before going into details, the concept behind the communication is the following:

  1. The current attack state is sent from the client to the backend server.
  2. The backend checks for the current attack state and sets the right response parameters to initiate the next attack stage.
  3. The backend response contains variables to notify the 2nd attack stage (client), which attack branch should be executed next.
  4. The 2nd attack stage evaluates the response variables and triggers the next branch.
  5. This procedure is repeated until the final state of the protocol is reached.

Time to branch

Let’s take a detailed look into the different branches now.

1 The SL branch is triggered at the beginning of the attack, when an infected victim accesses the login page of the targeted online banking, inserts the login credentials and clicks on the submit button. (NOTE: The low level Trojan functions need to trigger an the initial webinject (generic loader) on that website and therefore the URL of the online banking website has to be listed in the trojan config file). The submitted login credentials are intercepted, exfiltrated to the backend (see previous blog post), then the 2nd stage code calls the original login function of the banking or payment website. The backend now registers the new victim, identified by the botid. It returns an empty response to the webinject.
2 At this point, the victim has successfully logged in and has been redirected to the account balance overview page. This triggers the 2nd branch: CP. The CP branch is called multiple times during the attack and transmits general status information of the victim to the attacker. The response of the backend contains status flags to trigger the next step of the attack. At this point here, the backend signals to initiate the attack.
3 The attack signal triggers the 3rd step shown in Figure 1: The TL branch. This branch is used to collect details from all available accounts by using the grabber module. Furthermore, a flag is set to indicate a page reload after the response of the send function has been received. The collected data is then exfiltrated again. The botid is used to correlate transmitted data to existing victim entries in the backend and therefore works as unique identifier for the victim. The server response is empty, but the previously set reload flag now triggers the CP branch again.
4 The CP branch now sends the some information to the backend as  described in Step 2. As the backend has stored a different state for the botid already, the response is different now. It signals the 2nd attack stage that the grabber module has finished and the ats module should start now. This module is used to manipulate account details like the account balance or transaction details. Also some status flags are set to trigger the next branch.
5 The GD branch: This branch is used to collect and exfiltrate account details of the victim. As already described in step 3, the reload flag is used to trigger the CP branch again.
6 The CP branch again submits status information, and the backend now triggers the next step of the attack. Besides some status flags, details about the target account and some fake data is provided. The data is used by the CP branch to display a fake overlay with a message and/or images, to trick the victim into starting a transaction. To that end, the fake overlay is used like in a normal phishing attack. We could observe different kinds of messages, which could be categorized into different modi operandi. (see below).

If the victim fell for the scam, the previously provided data is used to pre-fill the transaction form. Naturally, this data contains a target account for the transaction. This account will be controlled by the attacker somehow, i.e., it most likely belongs to a money-mule.

Additionally, the response from the backend contains fake information to be displayed. Depending on the modus operandi, this information is used to display different transaction details to the victim, then the ones used for the transaction in background.

7 Now the victim is redirected to the overview page for a successful transaction. In combination with the current flag state, this page visit triggers the TL branch of the 2nd stage code. The TL branch is used to collect details from the transaction overview page and exfiltrates them to the backend. This indicates a successful transaction to the attacker. The backend response is empty. The webinject transits into the next state, without the need for further communication with the backend.
8 The last triggered branch is called CG. It creates a copy of the complete DOM of the successful transaction overview page and exfiltrates it to the backend. There is no indication that this data is displayed in the admin panel, thus we assume it is transmitted for debug purposes only.

Modi Operandi

In the following we detail two different exemplary modi operandi, which we could observe during our analysis. The real visible appearance is different, as the webinject makes heavy use of the style-sheets provided by the target website. This is a very straight-forward way to properly brand fraudulent content to match the corporate design of target banks or payment providers. We focus on the content shipped to banking customers.

Charity Fraud: SOS-Kinder


The victim is asked to donate 1€ to an non-profit organization, in this case for SOS children. This mimics the well know internationally active “SOS-Kinderdorf” organization. The German text is well written and does not contain the obvious indications for phishing that we all love and know from the occasional phishing mail, like contorted grammar and a more than flowery vocabulary. No Google Translate in sight, here. To leverage this scam vector, the webinject makes use of the data provided by the backed in Step 6 as detailed above. Using an overlay, the victim is made believe he/she is transferring 1€, but under the hood the amount is change to a much higher value.

The attackers follow a very classic social engineering approach for our part of the world and appeal to the victims helpfulness: Who doesn’t want to help children in need by spending 1€? We refer to this kind of attack as charity fraud.

Refund Fraud: Finanzpolizei


The overlay presents a message to the victim, indicating a transaction has been made to their account. As the victim sees a manipulated version of his account balance, he really believes the transaction has had happen. Furthermore the text indicates a preliminary investigation by the “Finanzpolizei” against the initiator of the transaction. If the victim is not transferring the money back, the text threatens with prosecution by law enforcement for participating in a money laundering scheme.

Finally, all the Google Translate and contextual cluelessness we came to love in the scams out there! Regrettably for the attacker, not all German-speaking countries are actually Germany. (We tried that once, partially, and it was a horrible idea.) An institution called “Finanzpolizei” does indeed exist — but not in Germany. The valid target audience for this scam is thus supposedly to be found in Austria, however, the scam is also actively used in Germany. The German text includes some mistakes and is not as well written as the first modus operandi we have shown above.

In the case at hand, the attackers try to make the victim follow through with a classic refund scam, by threatening legal consequences. As the story works without the need to manipulate the transferred amount under the hood, the fake data needed in the first described modus operandi is not used in this kind of attack. Nevertheless the attack is kind enough to prefill the transaction form with the correct details to ease the transaction for the victim.

Return of the victim

Now let’s assume the victim has been tricked into initiating a transaction by themselves to send their money to the attacker. What happens, if the victim takes a look into his online banking account some time later? As expected, the 2nd attack stage is also prepared for that case: The user is presented the “temporarily unavailable” notification (see Figures 1 and 2 from our previous post) and the login function of the target website is disabled. As long as the status variables are set to the finale state of the described communication protocol, the victim is thus unable to access their account again as long as the backend server is reachable. Even when disabling this blocking functionality, account information like transaction details and total balance are still manipulated. As this manipulations use the originally provided style cheets (CSS) from the target institute, a victim has no way to visibility distinguish between a fake entry and an original one.


Nowadays almost all financial institutes make use of two-factor authentication to protect their users from fraud. The modi operandi used by current banking trojan attacks successfully circumvent this by using social engineering techniques. The victim is tricked into initiating the transaction willingly and happily provides all information needed to confirm the transaction. This is achieved by visible modifications of the website that are indistinguishable from the original website content. The success rate of these attacks is still quite high.

By using a multi-layered attack, it’s also cumbersome for analysts to get an complete insight into the technical details. As soon as the backend server is not available anymore, only the 1st stage of a webinject is accessible on an infected machine. Without the backend server, most of the attack code is not available and therefore some pieces of the puzzle are missing.

These kind of multi-layered attacks have become more and more complex and sophisticated. However, beyond the visual appearance, the code of the original website is modified heavily to make this attacks work and these modifications necessarily leave a footprint. In our fraud detection solutions, we provide our customers with instant visibility into these modification symptoms so they can fare better at protecting their customers’ assets.

Authors: Manuel Körber-Bilgard and Karsten Tellmann