Concerned about recent PAN-OS and other firewall/VPN CVEs? Take advantage of Zscaler’s special offer today

Zscaler Blog

Get the latest Zscaler blog updates in your inbox

Security Research

Return of the Evilnum APT with updated TTPs and new targets

June 27, 2022 - 15 min read


Since the beginning of 2022, ThreatLabz has been closely monitoring the activities of the Evilnum APT group. We identified several instances of their low-volume targeted attack campaigns launched against our customers in the UK and Europe region.

The new instances of the campaign use updated tactics, techniques, and procedures. In earlier campaigns observed in 2021, the main distribution vector used by this threat group was Windows Shortcut files (LNK) sent inside malicious archive files (ZIP) as email attachments in spear phishing emails to the victims.

In the most recent instances, the threat actor has started using MS Office Word documents, leveraging document template injection to deliver the malicious payload to the victims’ machines. In this blog, we present the technical details of all components involved in the end-to-end attack chain. At the time of writing, to the best of our knowledge, the complete attack chain of this new instance of Evilnum APT group is not publicly documented anywhere.

ThreatLabz has identified several domains associated with Evilnum APT group which have not been previously detected by security vendors. This discovery indicates that the Evilnum APT group has been successful at flying under the radar and has remained undetected for a long time.


Key points

  • The key targets of the Evilnum APT group have predominantly been in the FinTech (Financial services) sector, specifically companies dealing with trading and compliance in the UK and Europe.
  • In March 2022, we observed a significant update in the choice of targets of Evilnum APT group. They targeted an Intergovernmental organization which deals with international migration services.
  • The timeline of the attack and the nature of the chosen target coincided with Russia-Ukraine conflict.
  • Macro-based documents used in the template injection stage leveraged VBA code stomping technique to bypass static analysis and also to deter reverse engineering.
  • A heavily obfuscated JavaScript was used to decrypt and drop the payloads on the endpoint. The JavaScript configured a scheduled task to run the dropped binary. This JavaScript has significant improvements in the obfuscation technique compared to the previous versions used by EvilNum APT group.
  • The names of all the file system artifacts created during the course of execution were chosen carefully by the threat actor to spoof legitimate Windows and other legitimate third party binaries' names.
  • In each new instance of the campaign, the APT group registered multiple domain names using specific keywords related to the industry vertical targeted.


Attack flow


Figure 1: Attack chain


Technical Analysis

For the purpose of technical analysis we will consider the document with MD5: 0b4f0ead0482582f7a98362dbf18c219 

Stage 1: Malicious document

The stage 1 malicious document is delivered via spear phishing email. Once the user downloads and opens the malicious document, it fetches the stage 2 macro template from the attacker-hosted domain and displays the decoy content as shown in Figure 2 asking the user to enable the macro content.



Figure 2: Decoy content


Stage 2: Macro template [VBA code stomping technique]

The stage 2 template contains the main malicious macro code. It makes use of VBA code stomping technique which is fairly uncommon in the wild. This technique destroys the original source code and only a compiled version of the VBA macro code (also known as p-code) is stored in the document.

As a result, this technique prevents static analysis tools such as olevba from extracting the decompiled VBA code.

Using the technique mentioned by the Walmart team here, we were able to extract the full macro code.

All the strings in the macro code are decrypted using the string decryption function shown in Figure 3.


Figure 3: String decryption function used in the VBA macro code

Below are the key functionalities of the macro.

1. The document file has two text boxes with encrypted contents. These textboxes will be decrypted at run time by the VBA macro code.

a) Textbox 1 - msform_ct.TextBox1.Text. This will be decrypted and contents will be written to %appdata%\"ThirdPartyNotices.txt"

b) Textbox 2 - msform_ct.TextBox2.Text - This will be decrypted and contents will be written to "%appdata%\Redist.txt"

2. Copies the legitimate Windows binary Wscript.exe to a file with the name "msdcat.exe". Such file copy operations are done by malwares as a way to bypass endpoint security products.

3. The file - Redist.txt contains the obfuscated JavaScript which will be executed with the following command line:

msdcat.exe" /E:jscRipt "%appdata%\Redist.txt" dg ThirdPartyNotices.txt

Note: "dg" is a hard coded command line parameter present inside the VBA macro code.

4. During the course of execution of VBA macro code, there are multiple calls to doc.Shapes.AddPicture() which fetches a JPG image from the attacker-controlled server. We believe this was done by the attacker to trace and log the execution of code on the endpoint.

One such example is shown in Figure 4. There is a call to doc.Shapes.AddPicture() between the building of the command line and the execution of the command line.


Figure 4: VBA code fetches image from attacker’s server to log actions on endpoint


Stage 3: Dropped JavaScript [Deobfuscation and analysis]

The original JavaScript dropped by the VBA-based macro code is heavily obfuscated. We will highlight some of the unique obfuscation techniques used which are rarely observed in obfuscated JavaScripts.

There are two parameters passed to this JavaScript at the time of execution with following command line:

msdcat.exe" /E:jscRipt "C:\Users\user\AppData\Roaming\Redist.txt" dg ThirdPartyNotices.txt

parameter 1: "dg". This string is later used in the string decryption function in JavaScript.

parameter 2: The file "ThirdPartyNotices.txt" contains the encrypted code which will be decrypted and dropped by the JavaScript on the filesystem with binary name - SerenadeDACplApp.exe

Most obfuscation techniques involve a large array of encrypted and encoded strings which are referenced throughout the code using indexes. A common approach to deobfuscate this requires multiple "search and replace" operations where you replace the reference with the actual decrypted and decoded string.

JavaScript in this case used an interesting technique where the original array of strings was shuffled, and would be unshuffled in memory at the time of execution. So, any attempt to dereference the strings without unshuffling the array would result in an error. Such a method can be used to deter reverse engineering and also bypass some tools which try to automate the process of deobfuscation.

Let's look at it in more detail.

Figure 5 below shows the huge array of strings defined at the beginning of the JavaScript. This array is wrapped inside a function as an extra layer of obfuscation.


Figure 5: array of encoded and encrypted strings

The next step is to unshuffle the array. Figure 6 below shows the relevant JavaScript code which uses a brute force approach to unshuffle the array. It has a predefined seed of value "0x6467a". Upon each iteration, the function computes a seed using various mathematical operations and compares it with the predefined seed "0x6467a". The function continues to shift the contents of the array by one position to the right until this condition is satisfied.

Relevant comments are included in the code to illustrate the unshuffling logic.


Figure 6: JavaScript function which unshuffles the array

Other techniques used for obfuscation involve control flow flattening techniques which leverage switch-case obfuscation. Figure 7 shows one of the string decryption functions which uses such an obfuscation technique.


Figure 7: Control flow flattening using switch-case obfuscation

The sequence of steps of decryption are shuffled using switch-case and the order is followed according to the following sequence:


It means, "case 15" is executed followed by "case 12" and so on. The final “case 11” returns the decrypted string.

We can re-write the string decryption function as shown in Figure 8 which is easier to analyze.


Figure 8: re-written string decryption function

We have included a list of interesting decrypted strings extracted from the JavaScript in Appendix I.

[+] Persistence

The threat actor achieves persistence via Scheduled task. During JavaScript execution, a scheduled task with the name "UpdateModel Task'' will be created to execute the dropped loader binary with required command line arguments.

Task details:



"OUM3NjBDNjAtRkNDQi00Q0FDLUE5NEMtNzY0RTc5MDNDN0Mw" "devZUQVD.tmp" "NzkzMTA3" "Ni4xLjc2MDE%3D" 0 "E4A6450B" "NTk1NDQxWwpaWhlhdmVbB1tf" Z




Stage 4: Dropped binary (Loader)

As described in previous section, the JavaScript drops two files:

a) An executable file (SerenadeDACplApp.exe) - It turns out to be a loader

b) A binary file (devZUQVD.tmp) - This is the file loaded during runtime by the loader

The loader is executed by the scheduled task along with the required arguments. During the course of its execution it performs following operations:

1. Performs command-line checks and extracts the file name for the binary to be loaded

The loader checks if the command-line ends with ("). If true then it will terminate the process else it will parse the arguments to extract the file name for the binary file to be loaded. 

# There are two code logics to extract the file name

  • If the first argument has the format (--[char]=[char]*) then the loader will remove the first 5 characters from this argument string, prepend "dev" and append ".tmp" to it. The resulting string is used as the file name for the dropped binary.


         Argument string: --E=nThisIsUsedInFileName

         Extracted file name: devThisIsUsedInFileName.tmp

  • The second argument string is used as the file name for the dropped binary

2. Generate full path for the dropped binary file in DOS format

The loader first extracts the full path of the currently executing binary and prepends it with “\\??\\” and then overwrites the file name of the currently executing binary with the file name extracted in Step-1.

3. Using the Heaven's gate technique calls the NtOpenFile API to create a file handle

4. Allocates memory for reading the file content using RtlAllocateHeap API

5. Using the Heaven's gate technique calls the NtReadFile API to read the file content to allocated memory

6. Decrypt the file content

# Encrypted content format

XOR key length (1 byte) + XOR key + encrypted content size (4 bytes) + encrypted content



Figure 9: Encrypted content format

The decrypted content turns out to be a PE file that uses a custom format for storing the PE header and section header information.

# Decrypted content format

Custom PE header (+ Custom Section header + Section data)*Number of sections

# PE header format

Start of decrypted content as well as PE header (1 byte - 00) + Image Base (4 bytes) + Size of Image (4 bytes) + Entry Point (4 bytes) + Number of sections (4 bytes) + Offset to first section information from start of decrypted content (4 bytes) + Size of decrypted content (4 bytes)

# Section header format

Section number marker (1 byte) + Section RVA (4 bytes) + Section VirtualSize (4 bytes) + Unknown (4 bytes)



Figure 10: PE header, Section header and Section data

7. Using the Heaven's gate technique calls the NtAllocateVirtualMemory API to allocate memory for the PE file to be mapped. 

Note: The size is taken from the PE header format described above.

8. Map the PE file in memory.

9. Using Heaven's gate technique calls the NtCreateThreadEx API to create a thread pointing to the entry point of mapped PE.

Note: The loader uses Heaven's gate technique to evade endpoint security products as well as syscall or API monitoring applications. It uses a custom header format to thwart memory scanning for PE header or section header patterns and also makes it difficult to dump and analyze the PE file as a standalone executable.


Stage 5: Mapped PE (Main backdoor)

The mapped PE is the main backdoor of the attack chain. On execution it performs the following operations:

1. Decrypts  the backdoor configuration which includes :

a) C2 domains

b) User Agent strings

c) Network paths

d) Referrer strings

e) Cookies type strings

f) Request methods + Library names (These will be loaded during further execution) + Network communication key generation seed bytes + Mutex name

All the information inside the configuration is encrypted using XOR key. The XOR key is different for each data item within the configuration.

# Encrypted data item format

Encrypted data size (2 bytes) + Encrypted data + XOR Key length (2 bytes) + XOR Key



Figure 11: Encrypted data item format

2. Resolves API addresses for the libraries retrieved from the configuration

3. Performs mutex check

4. Builds data exfiltration string to be sent as part of the beacon request

# String format




5. Encrypt and Base64 encode the generated string


String encryption and encoding

Figure 12: Steps performed for encrypting and encoding the exfiltrated data

Note: You can find the decryption code under Appendix III section

6. Embed the encoded string inside the cookie header field by selecting one of the cookie type strings from the configuration.


[+] Network communication

Once all the above operations are done. The backdoor selects one of the C2 domains and a path string from the configuration and sends the beacon network request.

If the beacon request is successful, the backdoor will query the server for available content and download it.

Based on the content size two different operations are performed:

1. If the content size is 4 then the backdoor checks if the downloaded data is equal to "01". If true, it takes the machine snapshot and sends it to the C2 server via POST request. The snapshot data is exfiltrated in encrypted form with the cookie header containing additional information.

# Format of cookie header string


"u":"{first_arg-user_id}", "sc":1, "dt"="{snapshot_date_time}"


2. If the content size is greater than 4, then the backdoor decrypts the downloaded data and executes it.


Zscaler Sandbox report

# Template payload



Indicators of compromise

[+] Hashes






proof of ownership.docx



tradersway compliance.docx



vantagemarkets documents.docx



vantagefx compliance.docx



calliber docs (2).docx



complaince tfglobaltrading.docx














Encrypted binary



[+] C2 Domains






































[+] Unique URI paths

# Below URI paths are appended to the domain names by the malware while sending POST requests












[+] Scheduled task names

UpdateModel Task


Schedule Defrag


Appendix I

# Unique strings extracted from the deobfuscated JavaScript



SELECT UUID FROM Win32_ComputerSystemProduct

SELECT Version FROM Win32_OperatingSystem




UpdateModel Task

/c start /min "" powershell -inputformat none -outputformat none -windowstyle hidden -c





%appdata%\Mael Horz\HxD Hex Editor\Logs\nvapiu.exe

%appdata%\Mael Horz\HxD Hex Editor\Logs

Schedule Defrag



WsSwap AssessmentTask


cmd /c ""ping -n 5 -w 10000 > nul & del /q





Western Digital\WD Backup\Storage







Appendix II

# Runtime strings present inside the unpacked backdoor binary






























Connection: keep-Alive

Content-Type: text/plain

Accept-Language: en-US,en;q=0.8

Accept: */*














Appendix III

# Decryption code for network communication

#include <Windows.h>

#include <stdio.h>

#define SEED_SIZE 32

VOID DeriveKey(BYTE seed[], BYTE key[]) {


    BYTE swapByte = 0;

    BYTE seedIndex = 0;

    BYTE calKeyIndex = 0;

    /* Initialize the key array */

    for (int i = 0; i < 256; i++) {

        key[i] = i;


    /* Calculate XOR key */

    for (int currKeyIndex = 0; currKeyIndex < 256; currKeyIndex++) {

        calKeyIndex = seed[currKeyIndex % SEED_SIZE] + key[currKeyIndex] + calKeyIndex;

        swapByte = key[currKeyIndex];

        key[currKeyIndex] = key[calKeyIndex];

        key[calKeyIndex] = swapByte;


    /* Print the derived XOR key */

    for (int k = 0; k < 256; k++) {

        printf("%02x ", key[k]);



VOID Decrypt(BYTE data[], BYTE key[]) {

    BYTE XORKeySize = data[0];

    BYTE *XORKey = (BYTE*)data + sizeof(BYTE);

    UINT encryptedDataSize = data[sizeof(BYTE) + XORKeySize];

    BYTE *encryptedData = (BYTE*)data + (sizeof(BYTE) + XORKeySize + sizeof(UINT));

    BYTE *layer1DecryptedData = (BYTE*)malloc(encryptedDataSize);

    for (UINT dataIndex = 0; dataIndex < encryptedDataSize; dataIndex++) {

        layer1DecryptedData[dataIndex] = encryptedData[dataIndex] ^ XORKey[dataIndex % XORKeySize];


    BYTE swapByte = 0;

    BYTE calKeyIndex = 0;

    BYTE finalKeyIndex = 0;

    for (UINT index = 1; index <= encryptedDataSize; index++) {

        calKeyIndex = key[index] + calKeyIndex;

        swapByte = key[index];

        key[index] = key[calKeyIndex];

        key[calKeyIndex] = swapByte;

        finalKeyIndex = key[index] + key[calKeyIndex];

        printf("%c ", layer1DecryptedData[index - 1] ^ key[finalKeyIndex]);



int main() {

    BYTE key[256];

    BYTE seed[SEED_SIZE] = {  // Taken from configuration

        0xBD, 0xDE, 0x96, 0xD2, 0x9C, 0x68, 0xEE, 0x06, 0x49,

        0x64, 0xD1, 0xE5, 0x8A, 0x86, 0x05, 0x12, 0xB0, 0x9A,

        0x50, 0x00, 0x4E, 0xF2, 0xE4, 0x92, 0x5C, 0x76, 0xAB,

        0xFC, 0x90, 0x23, 0xDF, 0xC6


    BYTE data[] = {  

    // Put Base64 decoded encrypted data here in HEX format


    DeriveKey(seed, key);


    Decrypt(data, key);

    return 0;


form submtited
Thank you for reading

Was this post useful?

dots pattern

Get the latest Zscaler blog updates in your inbox

By submitting the form, you are agreeing to our privacy policy.