Author:
Vladislav Lunin, Senior Specialist of the Positive Technologies Expert Security Center Sophisticated Threat Research Group
Takeaways:
- A new group was discovered targetting Russian government structures: TaxOff.
- TaxOff phished using legal and financial emails.
- TaxOff used the Trinper backdoor in its attacks.
- 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
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.
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.
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.
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.
One aspect of thread parallelism is data transfer between threads using global variables that can be divided into the following groups:
- Group 1 include a container for storing instances of the class communicating with C2 (CommHTTP_instance).
- Group 2 include a container for storing information about code injections (map_TaskInject).
- Group 3 include containers for storing running commands (vector_RunningTasks, deque_shared_RunningTasks, map_RunningTasks).
- 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
This backdoor pattern is used to create command subclasses that are inherited from the base class and redefine its methods and fields.
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.
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).
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.
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:
Member | Purpose | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
magic | Magic number 0xB0B1B201 | ||||||||||||||||||||||||||||||
VictimData |
|
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
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).
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.
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.
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:
Member | Purpose | ||||||||||||||||||||||||||||||||
sleep_time | Timeout for a CommHTTP class instance in the C2 communication loop | ||||||||||||||||||||||||||||||||
size | Sized of used buffer cache | ||||||||||||||||||||||||||||||||
UserAgent | User-Agent used to communicate with C2 servers | ||||||||||||||||||||||||||||||||
wstr_x86 | x86 wide string not used | ||||||||||||||||||||||||||||||||
wstr_x64 | X64 wide string not used | ||||||||||||||||||||||||||||||||
C2 | C2 addresses | ||||||||||||||||||||||||||||||||
public_key | Public key for encrypting information about the victim and session key | ||||||||||||||||||||||||||||||||
public_key_len | Length of the public key | ||||||||||||||||||||||||||||||||
Commands | Structure used to receive commands
| ||||||||||||||||||||||||||||||||
TaskResults | Structure used to send command operation results
|
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.
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.
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.
ID | Command | Description |
---|---|---|
0x1213C7 | Inject | Code injection into process |
0xF17E09 | WriteFile | Write to file |
0xF17ED0 | ReadFile | Read from file |
0xC033A4D | Cmd | Execute command using cmd.exe |
0x6E17A585 | GetRunningTasks | Receive commands running currently |
0xECEC | Exec | Reverse shell |
0xCD | Cd | Change of directory |
0x108 | JobConf | Add command in the background |
0xD1E | Die | Backdoor shutdown |
0x6177 | KillTask | End working command |
0xC04F | SetCommConfValue | Configuration 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
FILE | MD5 | SHA-1 | SHA-256 |
---|---|---|---|
Материалы.img | fdeb5b2771785dc412227904127e1cae | 6e7bf3ef4e53efea9a7b0446f498545e8dc517dc | dd3a609b7beb35fb2527e7ca1450ad40569b3ffbf67d84811fcf8ff09096d823 |
История поисков.html | e4da6bd811eb3b5adc4ec29fa859c08c | e810613df0dbb5d8634e7e5321f5b14c62ccfcf6 | 00f433c593204eaa1facb18d1a0dec4caee06915bbc8a51ad6bf47bf9e865fe8 |
BK_new2.2.EXE | 7815db832ef5124935d9b53445a72f49 | d45c3392011070e7e827dd3f8d6797725384b1b3 | f699c309f0d2547a85f6623dc74cc452a1471cd77af2360116447244043ee0dd |
DCIM.lnk | 468f4b71eac65391d3d59466e21ec379 | 9a083844696dd8ccce9a6f11d3a9f1227ea639ba | 93b07ba651fb6dbebaaadb39cf45ddfea7af9d3943458a5630aa588080dcf335 |
Trinper | |||
drive.google.com | 463d8f6e597fc7c2acdb3f5a3bae37b6 | 8dfecf3417b8f2ab96a3591c93223d6802690fe3 | 2a0c6a66774cc535f51e1a12d81ba6aa346934aa542291cee0c57f3bc9373a8e |
PhotoScreenSaver.scr | 19354fc1fb24d2eb08de0d46d464b16b | 62e27a7e392a48d6cf14040c6fe59dabb8df44a7 | 6d4fac9e4c36face9e0d0a7fdec1cc1403b3188ecf5c24f1ac6c32981f9c72b2 |
SearchApps.exe | 62739a86a227ad89fa6c57f5c2335220 | f5815561dfc63ad12f96a3e86e0f40cd39622373 | 7e82b3f1be69d34684a4aa4823ef0d5ae864db3501fae5a0c3697bcd28df5cef |
DotNet35.exe | f590d65dce86589b0e0d507cfeef9f68 | c3012a66acaea8801446ee61f8213a663eb7a76a | e93c1a0696b59a58e2444eb69ddf165eed71ad159624674a7fe6c91e9852443a |
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.002 | Phishing: Spearphishing Link | TaxOff used phishing emails with links to malicious files |
Execution | ||
T1204.002 | User Execution: Malicious File | TaxOff used bait files to run the Trinper backdoor |
Defense Evasion | ||
T1055.012 | Process Injection: Process Hollowing | TaxOff used the Trinper backdoor to inject code into processes |
Credential Access | ||
T1187 | Forced Authentication | TaxOff used a false authorization form |
T1056.001 | Input Capture: Keylogging | TaxOff used the Trinper backdoor to intercept keystrokes |
Discovery | ||
T1083 | File and Directory Discovery | TaxOff used the Trinper backdoor to collect file system information |
Collection | ||
T1115 | Clipboard Data | TaxOff used the Trinper backdoor to access the clipboard |
T1056.001 | Input Capture: Keylogging | TaxOff used the Trinper backdoor to intercept keystrokes |
Command And Control | ||
T1071 | Application Layer Protocol | TaxOff used http (https) to connect the Trinper backdoor to C2 |
T1132.001 | Data Encoding: Standard Encoding | TaxOff used the Trinper backdoor to encode received information using Base64 |
T1573.001 | Encrypted Channel: Symmetric Cryptography | TaxOff used the Trinper backdoor to encrypt sent information using AES-256 |
T1573.002 | Encrypted Channel: Asymmetric Cryptography: | TaxOff used the Trinper backdoor to encrypt sent information using RSA |
T1090.004 | Proxy: Domain Fronting | TaxOff used domain fronting to communicate with the Trinper backdoor |
Exfiltration | ||
T1020 | Automated Exfiltration | TaxOff used the Trinper backdoor to automatically exfiltrate results from executing commands |
T1041 | Exfiltration Over C2 Channel | TaxOff 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 |
Get in touch
will contact you shortly