TaxOff: um, you've got a backdoor...

Author: 

Vladislav Lunin, Senior Specialist of the Positive Technologies Expert Security Center Sophisticated Threat Research Group

Takeaways:

  1. A new group was discovered targetting Russian government structures: TaxOff.
  2. TaxOff phished using legal and financial emails.
  3. TaxOff used the Trinper backdoor in its attacks.
  4. Trinper is a multithreaded backdoor written in C++ with flexible configuration using the template method as a design pattern, STL containers, and a buffer cache to improve performance. It has numerous malicious capabilities.

Introduction

In Q3 2024, the Positive Technologies Expert Security Center (PT ESC) TI Department discovered a series of attacks on Russian government agencies. We were unable to establish any connection with known groups using the same techniques. The main goal was espionage and gaining a foothold to follow through on further attacks. We dubbed the group TaxOff because of their legal and finance-related phishing emails leading to a backdoor written in at least C++17, which we named Trinper after the artifact used to communicate with C2.

Initial infection vector

TaxOff uses phishing emails. We found several of them, including one with a link to Yandex Disk with malicious content for 1C and another with a fake installer for special software used by government employees to submit annual income and expense reports. This software is updated every year and targeted by attackers who distribute malware pretending to be updates.

Materials.img

One email had a link to Yandex Disk with the file Materials.img containing the following:

  • DCIM.lnk — a shortcut used to start the Trinper backdoor
  • drive.google.com — the Trinper backdoor
  • Encrypteddata — merged encrypted RAR archives with trimmed headers 
  • История поисков.html — a phishing form like the image below
Figure 1.png
Figure 1. Phishing form

Spravki BK

The other vector contained the Spravki BK software used by government employees in Russia to submit income and expense reports. This software has also been targeted by group to spread the Konni backdoor as the renamed WEXTRACT.EXE.MUI file usually responsible for extracting compressed CAB files. In our case, it contains two executable files instead: bk.exe (Figure 2, Spravki BK) and DotNet35.exe, the Trinper backdoor.

Figure 2.png
Figure 2. Information about the bk.exe file

Similar to CAB files, the RCData resource section contains attributes of the execution sequence of the files it stores. The first attribute, RUNPROGRAM, contains instructions to execute a specific program or command at the start and launches bk.exe.

Figure 3. RUNPROGRAM attribute contents
Figure 3. RUNPROGRAM attribute contents

The second attribute, POSTRUNPROGRAM, contains instructions to launch the executable file after RUNPROGRAM has been executed. So after bk.exe is run, DotNet35.exe is launched.

Figure 4. POSTRUNPROGRAM attribute contents
Figure 4. POSTRUNPROGRAM attribute contents

Trinper backdoor

To improve general understanding of how the backdoor works, the sections below include an explanation of its architecture, STL, design pattern, custom serialization, and buffer cache as a preface to its functional description.

Architecture

Like any other multithreaded application, Trinper is built on a parallel programming paradigm, specifically thread parallelism. Tasks are broken down into sequential steps as shown on the diagram below, each of which can be performed in parallel with others.

Figure 5. Trinper's architecture
Figure 5. Trinper's architecture

One aspect of thread parallelism is data transfer between threads using global variables that can be divided into the following groups:

  1. Group 1 include a container for storing instances of the class communicating with C2 (CommHTTP_instance).
  2. Group 2 include a container for storing information about code injections (map_TaskInject).
  3. Group 3 include containers for storing running commands (vector_RunningTasks, deque_shared_RunningTasks, map_RunningTasks).
  4. Group 4 include containers for storing the operation of background commands (map_shared_ptr_BgJobs, deque_BgJobKeylogger, unordered_map_BgJobKeylogger).

Use of STL

The Standard Template Library provides a set of common generic algorithms, containers, means of accessing their contents, and various functions in C++ used by the backdoor. The main sign of STL runtime is the error messages for various containers.

Figure 6.png
Figure 6. Error strings related to STL container runtime

std::string and std::wstring

Strings are objects represented as a sequence of characters. Symbols can either have ASCII or wide encoding, which makes these containers clearly distinguishable. In std::string, the maximum length of the predefined buffer can't exceed 15 bytes, otherwise a heap buffer will be allocated. As for std::wstring, the length of the predefined buffer can't exceed 7 bytes. This runtime is for comparing the length of the stored string and possible subsequent allocation of a heap buffer, which allows one of the containers used to be determined precisely.

Figure 7.png
Figure 7. Recognizing the std::string and std::wstring runtime

std::vector<T>

Vectors are containers for array-like sequences that can vary in size. One way to recognize the runtime of a vector is to compare successive memory addresses and then assign a new value to one of them. If the pointer to the first vector element is equal to the pointer to the last element (for example, when adding a new element), then the vector will have to change its current size, allocate additional memory before adding it, and redefine the pointer to the last element.

Figure 8. Recognizing the std::vector<T> runtime
Figure 8. Recognizing the std::vector<T> runtime

std::list<T>

Lists are sequence containers that allow constant-time insertion and erasure of elements anywhere in the sequence, as well as iteration in both directions. One way to recognize the runtime of a doubly linked list is to compare the selected buffer with the subsequent one to see if the end has been reached when iterating the elements.

Figure 9. Recognizing the std::list<T> runtime
Figure 9. Recognizing the std::list<T> runtime

std::map<K, T>

Maps are associative containers that store elements formed by a combination of a key and mapped value in a specific order. One way to recognize map runtime is that the map needs to know if there's already an element with the provided key before inserting a new key-value pair or returning a value based on the key.

Figure 10. Recognizing std::map<K, T> runtime
Figure 10. Recognizing std::map<K, T> runtime

std::unordered_map<K, T>

Unordered maps are associative containers that store elements formed by a combination of a key and mapped value, which allows individual elements to be quickly found by their keys. One of the ways to recognize the runtime of an unordered map is how its elements are stored in hash tables, as the hash sum will always be calculated for any element index regardless of the operation.

Figure 11. Recognizing the std::unordered_map<K, T> runtime
Figure 11. Recognizing the std::unordered_map<K, T> runtime

std::deque<T>

Deques are dynamically sized sequence containers that can expand or contract at the front (head) or back (tail). One method of recognizing the runtime of a deque is to access elements in the blocks. Their size is always a multiple of two, so access uses bitwise operations to split the index into a block and an offset.

Figure 12. Recognizing the std::deque<T> runtime
Figure 12. Recognizing the std::deque<T> runtime

std::shared_ptr<T>

Smart pointers control pointer storage and provide limited trash collection, potentially sharing this control with other objects. One way to recognize the runtime of a smart pointer is with atomic operations. If the number of shared pointers to an object decreases to zero, then the control block is deleted.

Figure 13. Recognizing the std::shared_ptr<T> runtime
Figure 13. Recognizing the std::shared_ptr<T> runtime

std::filesystem

std::filesystem provides means for performing operations on file systems and their components, including paths, regular files, and directories. One of the ways to recognize its runtime is the presence of functions with the _std_fs_* prefix, which indicates operations with the file system.

Figure 14. Recognizing the std::filesystem runtime
Figure 14. Recognizing the std::filesystem runtime

Including header files

To avoid creating structures manually, we need to include header files. To determine their location, we run the x86/x64 Native Tools Command Prompt for VS 20XX (depending on the bit depth of the executable file) and enter the echo %INCLUDE% command. Then we copy all the paths and paste them in Options > Compiler > Include directories. We also specify -target x86_64-pc-win32/i386-pc-win32 -x c++ as arguments to include C++ header files.

Figure 15. Including header files
Figure 15. Including header files

In most cases, the element type of containers is set at compile time, so you can't just include a vector header file (for example) and expect all the element types to be included. Instead, you need to create a separate header file where the container element type will be defined explicitly.

Figure 16. Vector structure
Figure 16. Vector structure

Design pattern used

A design pattern in software engineering is a recurring architectural construct offering a solution to a design problem within a frequently occurring context. This is a rare find in malicious code and indicates that the programmer who wrote the backdoor is an experienced professional. The backdoor uses the template method, which is a behavioral design pattern that defines the skeleton of an algorithm and defers certain steps to subclasses. The pattern allows subclasses to redefine the algorithm's steps without changing its overall structure.

Figure 17. The template method
Figure 17. The template method

This backdoor pattern is used to create command subclasses that are inherited from the base class and redefine its methods and fields.

Figure 18. Template method in use
Figure 18. Template method in use

Custom serialization used

In addition to encryption, the backdoor uses custom serialization to store the configuration and increase flexibility, as it allows fields to have multiple values in the same token.

Figure 19. Serialized configuration
Figure 19. Serialized configuration

For example, if a container token is negative, then it has another container in its value that may also only be part of a sequential nesting of containers or unequivocally determine the value of the token (for example, store a string).

Figure 20. Deserialization of the configuration
Figure 20. Deserialization of the configuration

 

Buffer cache use

A buffer cache is a data structure designed for the temporary storage of data to speed up access to it. The Trinper backdoor uses caching to reduce access time to frequently used data, minimize latency, and improve overall program performance.

Figure 21. Use of buffer cache
Figure 21. Use of buffer cache

Initialization and execution of main class instances

At the start, the backdoor deserializes the configuration and gets the name it should have. If it's different, execution is stopped, but if the names match, the backdoor continues initialization and calls a function to obtain information about the victim's computer and collect it with the following type of VictimInfo structure:

struct struct_VictimInfo
{
	DWORD magic;
	struct_VictimData VictimData;
};

struct struct_VictimData
{
	GUID guid;
	BYTE pbSecret[16];
	BYTE UserNameW[64];
	BYTE hostname[32];
	BYTE disks[32];
	BYTE h_addrs[20];
	DWORD KeyboardLayout;
	BYTE dwOemId;
	BYTE val_64;
	BYTE dwMajorVersion;
	BYTE dwMinorVersion;
	BYTE Authority;
	BYTE FileNameW[64];
	BYTE AdaptersAddresses[6];
};

The fields of the VictimInfo structure have the following purposes:

MemberPurpose
magicMagic number 0xB0B1B201
VictimData
MemberPurpose
guidGenerated GUID
pbSecretSession key for AES-128-CBC
UserNameWUsername
hostnameHost name
disksDisk names
h_addrsHost address list
KeyboardLayoutSystem language used
dwOemIdInformation about the architecture
val_64Constant value 64
dwMajorVersionMajor version of the system
dwMinorVersionMinor version of the system
AuthorityLevel of integrity
FileNameWFile path
AdaptersAddressesAddresses of network adapters

After filling in the VictimInfo structure, the backdoor creates and runs these class instances for execution in different threads:

  • CommHTTP — the class for the thread for communication with C2 servers
  • BgJobFileCapture — the class for the thread that monitors the file system
  • BgJobKeylogger — the class for the thread where keystrokes will be intercepted

In its thread, the CommHTTP class parses the deserialized configuration it will use to communicate with C2, generates a session key for AES-128-CBC (with the initialization vector equal to zero), imports the public RSA key, and enters a communication loop with C2 servers where:

  • Commands are received
  • Results about command operations are received and sent
Figure 22. CommHTTP class instance execution cycle
Figure 22. CommHTTP class instance execution cycle

An instance of the BgJobFileCapture class monitors the file system in its thread, loops through all connected disks, and searches recursively for .doc, .xls, .ppt, .rtf, and .pdf files stored on disks. It also stores execution results in a map with a key (file name) and value (the structure containing information about the file, including its contents).

Figure 23. Receiving information about the file system
Figure 23. Receiving information about the file system

An instance of the BgJobKeylogger class intercepts keystrokes in its thread and stores them in a deque, with data from the clipboard stored in an unordered map.

Figure 24. Installing the keylogger
Figure 24. Installing the keylogger

Configuration

The configuration is encrypted and stored in the .data section, and decryption is carried out with a one-byte key for a regular Xor operation.

Figure 25. Decrypting the configuration
Figure 25. Decrypting the configuration

Here's what the decrypted and deserialized configuration structure looks like:

struct struct_Config
{
	DWORD sleep_time;
	DWORD size;
	std::wstring UserAgent;
	std::wstring wstr_x86;
	std::wstring wstr_x64;
	std::vector<std::wstring> C2;
	QWORD *public_key;
	QWORD public_key_len;
	struct_Commands Commands;
	struct_TaskResults TaskResults;
};

struct struct_Commands
{
	std::wstring Uri;
	std::vector<std::string> Headers;
	struct_CommandsResponse CommandsResponse;
	struct_CommandsHeaders CommandsHeaders;
	QWORD HelloMessage;
	QWORD HelloMessageLen;
};

struct struct_CommandsResponse
{
	std::string TagOpen;
	std::string Encoder;
	std::string TagClose;
};

struct struct_CommandsHeaders
{
	std::string Header;
	std::string TagOpen;
	std::string Encoder;
	std::string TagClose;
};

struct struct_TaskResults
{
	std::wstring Uri;
	std::vector<std::string> Headers;
	struct_TaskResultsData TaskResultsData;
	struct_TaskResultsHeaders TaskResultsHeaders;
};

struct struct_TaskResultsData
{
	std::string TagOpen;
	std::string Encoder;
	std::string TagClose;
};

struct struct_TaskResultsHeaders
{
	std::string Header;
	std::string TagOpen;
	std::string Encoder;
	std::string TagClose;
};

The fields of the Config structure have the following purposes:

MemberPurpose
sleep_timeTimeout for a CommHTTP class instance in the C2 communication loop
sizeSized of used buffer cache
UserAgentUser-Agent used to communicate with C2 servers
wstr_x86x86 wide string not used
wstr_x64X64 wide string not used
C2C2 addresses
public_keyPublic key for encrypting information about the victim and session key
public_key_lenLength of the public key
Commands

Structure used to receive commands

MemberPurpose
UriCommand request path
HeadersCustom headers
CommandsResponse
MemberPurpose
TagOpenStart of command mask
EncoderCommand encoding algorithm
TagCloseEnd of command mask
CommandsHeaders
MemberPurpose
HeaderHeader storing information about the victim
TagOpenStart of the header mask
EncoderEncoding algorithm
TagCloseEnd of header mask
HelloMessageCommand request string
HelloMessageCommand request string length
TaskResults

Structure used to send command operation results

MemberPurpose
UriPath of command operation results
HeadersCustom headers
TaskResultsData
MemberPurpose
TagOpenStart of task results mask
EncoderAlgorithm for encoding task results
TagCloseEnd of task results mask
TaskResultsHeaders
MemberPurpose
HeaderHeader storing information about the victim
TagOpenStart of the header mask
EncoderEncoding algorithm
TagCloseEnd of header mask

Communication protocol with C2

All communication with C2 is carried out by an CommHTTP class instance using calls to WININET.DLL library network functions. Information about the victim's computer and session key is encrypted with the public RSA key, encoded using Base64, and sent to C2 in the Config.Commands.CommandsHeaders header with Config.Commands.HelloMessage in the request data. The commands received from C2 in response are enclosed between the Config.Commands.CommandsResponse.TagOpen and Config.Commands.CommandsResponse.TagClose markers and encoded using Base64. The task results are encrypted with the AES-128-CBC session key, encoded using Base64, and enclosed between the Config.TaskResults.TaskResultsData.TagOpen and Config.TaskResults.TaskResultsData.TagClose markers in the request data to C2.

Figure 26. POST request to receive commands
Figure 26. POST request to receive commands

For example, below is a backdoor request to C2 to receive commands. The greeting message in the data is mid=76&mod=TRINP, and you can see that the User-Agent header doesn't display the content received from Config.UserAgent correctly. This is due to an error passing the header value to InternetOpenW. The problem is that InternetOpenW tries to convert the string for User-Agent from wide to ASCII encoding, but does so incorrectly because the pointer to the values from the configuration is passed incorrectly, leading to an undisplayable string generated at the output.

Figure 27. Request packet to receive commands
Figure 27. Request packet to receive commands

Commands

As mentioned earlier, commands are invoked not by calling a specific function, but by instantiating classes and adding them to a smart pointer wrapper to be added to a deque for execution, and then retrieved from there and called in the main thread loop. The table below includes descriptions of the commands.

IDCommandDescription
0x1213C7InjectCode injection into process
0xF17E09WriteFileWrite to file
0xF17ED0ReadFileRead from file
0xC033A4DCmdExecute command using cmd.exe
0x6E17A585GetRunningTasksReceive commands running currently
0xECECExecReverse shell
0xCDCdChange of directory
0x108JobConfAdd command in the background
0xD1EDieBackdoor shutdown
0x6177KillTaskEnd working command
0xC04FSetCommConfValueConfiguration update

Insights

The TaxOff group tricks users by baiting them with time-sensitive material they're expecting at work and attack using a sophisticated multithreaded backdoor called Trinper. After establishing persistent access to compromised systems, they can effectively manage multiple tasks simultaneously and follow through on various malicious actions without significantly impacting system performance. Multithreading provides a high degree of parallelism to hide the backdoor while retaining the ability to collect and exfiltrate data, install additional modules, and maintain communications with C2. This combination of convincing bait and a sophisticated multithreaded backdoor makes TaxOff's attacks particularly dangerous and difficult to detect and prevent, highlighting the need for continuous user awareness of cyberthreats and the implementation of multi-layered security measures for protection.

IoC

File indicators

FILEMD5SHA-1SHA-256
Материалы.imgfdeb5b2771785dc412227904127e1cae6e7bf3ef4e53efea9a7b0446f498545e8dc517dcdd3a609b7beb35fb2527e7ca1450ad40569b3ffbf67d84811fcf8ff09096d823
История поисков.htmle4da6bd811eb3b5adc4ec29fa859c08ce810613df0dbb5d8634e7e5321f5b14c62ccfcf600f433c593204eaa1facb18d1a0dec4caee06915bbc8a51ad6bf47bf9e865fe8
BK_new2.2.EXE7815db832ef5124935d9b53445a72f49d45c3392011070e7e827dd3f8d6797725384b1b3f699c309f0d2547a85f6623dc74cc452a1471cd77af2360116447244043ee0dd
DCIM.lnk468f4b71eac65391d3d59466e21ec3799a083844696dd8ccce9a6f11d3a9f1227ea639ba93b07ba651fb6dbebaaadb39cf45ddfea7af9d3943458a5630aa588080dcf335
Trinper
drive.google.com463d8f6e597fc7c2acdb3f5a3bae37b68dfecf3417b8f2ab96a3591c93223d6802690fe32a0c6a66774cc535f51e1a12d81ba6aa346934aa542291cee0c57f3bc9373a8e
PhotoScreenSaver.scr19354fc1fb24d2eb08de0d46d464b16b62e27a7e392a48d6cf14040c6fe59dabb8df44a76d4fac9e4c36face9e0d0a7fdec1cc1403b3188ecf5c24f1ac6c32981f9c72b2
SearchApps.exe62739a86a227ad89fa6c57f5c2335220f5815561dfc63ad12f96a3e86e0f40cd396223737e82b3f1be69d34684a4aa4823ef0d5ae864db3501fae5a0c3697bcd28df5cef
DotNet35.exef590d65dce86589b0e0d507cfeef9f68c3012a66acaea8801446ee61f8213a663eb7a76ae93c1a0696b59a58e2444eb69ddf165eed71ad159624674a7fe6c91e9852443a

Network indicators

185.158.248.91
193.37.215.111
server.1cscan.net
usfna.global.ssl.fastly.net
usfnb.global.ssl.fastly.net
usfnc.global.ssl.fastly.net
cfn.global.ssl.fastly.net
fna.global.ssl.fastly.net
fnb.global.ssl.fastly.net
consult-asset-feed.global.ssl.fastly.net
consult-vendor-free.global.ssl.fastly.net
consult-zero-ads.global.ssl.fastly.net

File signatures

rule PTESC_apt_win_ZZ_TaxOff__Backdoor__Trinper{
    strings:
        $s1 = "Task"
        $s2 = "TaskCd"
        $s3 = "TaskCmd"
        $s4 = "TaskExec"
        $s5 = "TaskGetRunningTasks"
        $s6 = "TaskInject"
        $s7 = "TaskDie"
        $s8 = "TaskJobConf"
        $s9 = "TaskKillTask"
        $s10 = "TaskReadFile"
        $code1 = {E8 ?? ?? ?? ?? 44 38 60 ?? 75 ?? 66 39 58 ?? 72 ?? 48 8B 40 ?? 8B 08 EB ??}
        $code2 = {48 89 4C 24 ?? 48 8B 44 24 ?? 0F B6 40 ?? 85 C0 75 ?? 48 8B 44 24 ?? 0F B7 40 ?? 83 F8 ?? 7D ?? 33 C0 EB ?? 48 8B 44 24 ?? 48 8B 40 ?? 8B 00 C3}
    condition:
        ((uint16(0) == 0x5a4d) and (all of($s*)) and (any of($code*)))
}

MITRE TTPs

Initial Access
T1566.002Phishing: Spearphishing LinkTaxOff used phishing emails with links to malicious files
Execution
T1204.002User Execution: Malicious FileTaxOff used bait files to run the Trinper backdoor
Defense Evasion
T1055.012Process Injection: Process HollowingTaxOff used the Trinper backdoor to inject code into processes
Credential Access
T1187Forced AuthenticationTaxOff used a false authorization form
T1056.001Input Capture: KeyloggingTaxOff used the Trinper backdoor to intercept keystrokes
Discovery
T1083File and Directory DiscoveryTaxOff used the Trinper backdoor to collect file system information
Collection
T1115Clipboard DataTaxOff used the Trinper backdoor to access the clipboard
T1056.001Input Capture: KeyloggingTaxOff used the Trinper backdoor to intercept keystrokes
Command And Control
T1071Application Layer ProtocolTaxOff used http (https) to connect the Trinper backdoor to C2
T1132.001Data Encoding: Standard EncodingTaxOff used the Trinper backdoor to encode received information using Base64
T1573.001Encrypted Channel: Symmetric CryptographyTaxOff used the Trinper backdoor to encrypt sent information using AES-256
T1573.002Encrypted Channel: Asymmetric Cryptography:TaxOff used the Trinper backdoor to encrypt sent information using RSA
T1090.004Proxy: Domain FrontingTaxOff used domain fronting to communicate with the Trinper backdoor
Exfiltration
T1020Automated ExfiltrationTaxOff used the Trinper backdoor to automatically exfiltrate results from executing commands
T1041Exfiltration Over C2 ChannelTaxOff used the Trinper backdoor to exfiltrate data to C2

Positive Technologies product verdicts

PT Sandbox

apt_win_ZZ_TaxOff__Backdoor__Trinper

MaxPatrol SIEM

Suspicious_Connection
RunAs_System_or_External_tools
Run_Executable_File_without_Meta
Suspicious_Directory_For_Process

PT NAD

BACKDOOR [PTsecurity] Trinper (APT TaxOff) sid: 10012123
SUSPICIOUS [PTsecurity] Suspicious HTTP header Trinper (APT TaxOff) sid: 10012124, 10012125
Share this article:

Get in touch

Fill in the form and our specialists
will contact you shortly