Evolution of UNC4990: Uncovering USB Malware's Hidden Depths
Mandiant
Written by: Diana Ion, Jae Young Kim, Muhammad Umair, Panagiotis Antoniou, Yash Gupta
Mandiant Managed Defense has been tracking UNC4990, an actor who heavily uses USB devices for initial infection. UNC4990 primarily targets users based in Italy and is likely motivated by financial gain. Our research shows this campaign has been ongoing since at least 2020.
Despite relying on the age-old tactic of weaponizing USB drives, UNC4990 continues to evolve their tools, tactics and procedures (TTPs). The actor has moved from using seemingly benign encoded text files to hosting payloads on popular websites such as Ars Technica, GitHub, GitLab, and Vimeo.
The legitimate services abused by UNC4990 (including Ars Technica, GitHub, GitLab, and Vimeo) didn’t involve exploiting any known or unknown vulnerabilities in these sites, nor did any of these organizations have anything misconfigured to allow for this abuse. Additionally, the content hosted on these services posed no direct risk for the everyday users of these services, as the content hosted in isolation was completely benign. Anyone who may have inadvertently clicked or viewed this content in the past was not at risk of being compromised.
Mandiant has observed UNC4990 leverage EMPTYSPACE (also known as VETTA Loader and BrokerLoader), a downloader that can execute any payload served by the command and control (C2) server, and QUIETBOARD, which is a backdoor that was delivered using EMPTYSPACE.
Infection Lifecycle
Figure 1: Infection chain
Initial Compromise: USB LNK
In all instances of the infection which Mandiant Managed Defense responded to, the infection began with the victim double-clicking a malicious LNK shortcut file on a removable USB device. The naming convention for the LNK file typically consisted of the vendor of the USB device and the storage size in brackets, for example: KINGSTON (32GB).lnk
. Mandiant also observed instances where, instead of the vendor name, the drive label was used, for example: D (32GB).lnk
.
In addition to this, the icon of the LNK file was set to the Microsoft Windows default icon for drives. This was likely done to entice unsuspecting users to double click the file, ultimately triggering the functionality embedded in the LNK file.
Figure 2: Malicious LNK file
Upon double clicking, the PowerShell script explorer.ps1
is executed via the following LNK shortcut target:
C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe -windowstyle
hidden -NoProfile -nologo -ExecutionPolicy ByPass -File explorer.ps1
Explorer.ps1
From the investigations conducted by Mandiant Managed Defense, Mandiant identified multiple iterations of a malicious PowerShell script called explorer.ps1
. This is an encoded PowerShell script that ultimately downloads and decodes an additional payload, which, in the cases investigated by Mandiant, has been the EMPTYSPACE downloader.
Mandiant suspects the earlier versions of explorer.ps1
were not encoded; however, more recent variants were loaded into memory as a reverse Base64 encoded string, similar to the one shown in Figure 3.
Figure 3: explorer.ps1 (SHA256: 6fb4945bb73ac3f447fb7af6bd2937395a067a6e0c0900886095436114a17443)
The earliest version of explorer.ps1
which we identified (SHA256: 72f1ba6309c98cd52ffc99dd15c45698dfca2d6ce1ef0bf262433b5dfff084be
) checks whether a Hangul Filler Unicode
character (E3 85 A4
in UTF-8) labeled directory exists at the current path and only continues with the execution of the following sequence in the case the condition is true (Hangul Filler is a special Unicode character (U+3164) used in the Korean writing system, Hangul. It is typically not possible to use a whitespace as a file or directory name in Windows. However, using the Hangul Filler character, which is rendered as a whitespace, this restriction can be bypassed):
- Triggers the default action associated with the item pointed to by Hangul Filler named directory.
- Some newer instances of the script contain a unique UUID value, different for each infection. The identifier is saved to a file named from_machine_uuid.dat in the APPDATA directory. Mandiant determined that this UUID variable was not present in the script from the beginning of the campaign and only added later on as a new capability to track infected hosts.
- The script fetches a resource from a URL stored in the script,
hxxps://lucaespo.altervista[.]org/updater.php?from=USB1
, and saves it asRuntime Broker.exe
a.k.a. EMPTYSPACE in the TEMP directory. - In later versions (such as
SHA256: 99d9dfd8f1c11d055e515a02c1476bd9036c788493063f08b82bb5f34e19dfd6
), the script was updated with an intermediary stage hosted at the URL:hxxps://eldi8.github[.]io/src.txt
. Thesrc.txt
(SHA256: b38dbaea648ef7da1c639f4fdaac0d88f03306ea42f0edc9af512c613dbdb7e1
) file contains a pattern of three characters: TAB:09
, Space:02
and Line Feed:0A
. In a traditional text editor,src.txt
would appear as a blank file. Mandiant observed the same src.txt had been previously hosted on GitLab:hxxps://evh001.gitlab[.]io/src.txt
.
Figure 4: src.txt as viewed in a HEX editor
- A custom decoding scheme is then applied to the src.txt file, consisting of the following sequence of operations:
- Character replacement
- Spaces are replaced with 1s
- Tab characters are replaced with 0s
- New line characters are replaced with spaces
- This transformation changes the original string into a new format that resembles a binary string (composed of 1s and 0s).
- The transformed string is then split into an array of substrings. Each substring represents a sequence of 1s and 0s.
- Each substring is converted from a binary representation to its corresponding character.
- The resulting characters are joined back together into a single string.
- Character replacement
-
The newly constructed string is the final URL from where the executable
Runtime Broker.exe
is downloaded:hxxps://wjecpujpanmwm[.]tk/updater.php?from=USB1
. This URL was serving EMPTYSPACE from at least early 2022 through to July 2023, as Mandiant has also observed it in updated versions ofexplorer.ps1
. -
Once EMPTYSPACE has been downloaded, the script continuously checks for the existence of the file
pythonw.exe
, under the directory%ProgramFiles%\Winsoft Update Service\
and will proceed to execute the newly downloaded malware every second only if thepythonw.exe
file is not present at the specified path.
Use of Third-Party Websites for Payload Hosting
Starting in 2023, the use of GitHub was replaced by a new payload hosted on Vimeo, a video sharing website, with the new URL being also hard-coded in explorer.ps1
as hxxps://vimeo[.]com/api/v2/video/804838895.json
. The encoded payload was inserted into the description of a Pink Floyd-related video uploaded to Vimeo on March 5, 2023. At the time of publishing this post, the video was removed from Vimeo.
Figure 5: Vimeo video description
Figure 6: JSON object pulled from the hard-coded Vimeo URL
This change from the previously discussed version has been coupled with an upgrade from the custom decoding scheme to the use of AES in CBC mode and Base64 encoding.
The script fetches the Vimeo JSON blob which contains the attacker payload between the delimiter characters ::??
and ?:?:
. The payload is then Base64 decoded and decrypted with a hard-coded AES-256-CBC key shown as follows:
92 f7 6b 7d 6a e7 3f 41 b5 8f 41 e5 14 fb 68 de c8
e8 4a 2d c1 6f a2 71 f3 f3 1d 9f 3c 99 b7 4d
From November 27, 2023, Mandiant Managed Defense observed yet another shift in TTPs with regard to third-party websites used as C2. As the Vimeo video was taken down, the threat actor switched to using a well-known news forum, Ars Technica. In the updated instances of explorer.ps1
, Mandiant observed the following hard-coded URL: hxxps://arstechnica[.]com/civis/members/frncbf22.1062014/about/
.
As with the previous payload hosted on Vimeo, the Ars Technica URL employs the exact same technique down to even the same delimiter characters and encryption key, so there would be no other changes required in the explorer.ps1
script besides the URL. However, now the encoded blob was appended to the image URL contained in the About section of the user frncbf2
. This user became a member on the Ars Technica forum on November 23, 2023.
Figure 7: User profile of frncbf2 on the Ars Technica forum
Figure 8: Payload appended to image URL
As of mid December 2023, the photo hosted on Ars Technica was removed together with the intermediary payload.
From mid-2023, the threat actor also updated the URL serving EMPTYSPACE. Recent infections revealed the new URL to be hxxps://evinfeoptasw.dedyn[.]io/updater.php?from=USB1
. The final URL is formed by appending the string "&user=<uuid>
", UUID being the unique identifier mentioned previously.
The different versions of explorer.ps1
that Mandiant encountered during the research process showed how the script was incrementally changed. Initially, the script only focused on downloading EMPTYSPACE from an encoded URL. It then added an intermediary stage for constructing the final URL using payloads hosted on third party websites. Later on, the capability to track infections was added. Mandiant observed the presence of a variable storing a unique identifier (UUID) which is appended to the URL from where EMPTYSPACE is downloaded. Figure 9 shows the major changes undergone by the script.
Figure 9: Evolution of explorer.ps1
EMPTYSPACE
EMPTYSPACE is a downloader that communicates with its C2 server over HTTP. It downloads and executes an executable payload served by the C2 server. The EMPTYSPACE beacon response is parsed as JSON containing a list of tasks, each of which specify a file to download to disk and execute.
Mandiant has identified multiple variants of EMPTYSPACE, typically named Runtime Broker
. These variants have been written in Node.js, .NET and Python. Yoroi noted an additional Go variant in their research.
NODE.JS version
This version of Runtime Broker.exe
(SHA256: a4f20b60a50345ddf3ac71b6e8c5ebcb9d069721b0b0edc822ed2e7569a0bb40
) is a downloader compiled with nexe, a utility that bundles a Node.js app into a single executable. The executable consists of Node.js runtime executable version 12.9.1 and the following items in the file overlay section:
- SHA256
4814393285c2afcd671dbdd53b3b2021963c32a09745f83ed894e5ae4e2764b8
at file offset0x16B6E00
: JavaScript, the initialization component ofnexe
. - SHA256:
461d580a16cf1fa67b4ac751dfe9d36b2de3f13c97670b3b12641f20246ce4b3
at file offset0x16BAC3A
: DLL referred to as drivelist.node. Its sole purpose appears to be to produce a drive listing for use in the JavaScript payload. - SHA256
fae6192a0648a892c845d9498002ca79497ea58e5315d277f65f7b243f7110e4
at file offset0x171683A
: Main JavaScript payload referred to asindex.js
; bundled by webpack.
Runtime Broker.exe
will execute "net session
" to determine whether the current process has elevated permissions. If running as an elevated process, the sample uses the named pipe \\?\pipe\installSrvUniqID
to ensure that only a single instance of the executable is running.
The sample extracts and drops the overlay DLL (SHA256: 461d580a16cf1fa67b4ac751dfe9d36b2de3f13c97670b3b12641f20246ce4b3
) to <current_directory>/build/Release/drivelist.node
. The sample invokes the drivelist.node
module to produce a drive listing. The sample iterates this listing to search for a removable and readable/writeable drive whose mount point path contains the Hangul Filler
character (E3 85 A4
in UTF-8). This is possibly to determine whether the instance of the malware is the initial infection from a USB. The sample only proceeds with the remaining functionality if such a path is found.
If not running as an elevated process, the sample sets the registry value HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\\Node_Run
to the executable path and attempts to run as an elevated process. If already running as an elevated process, the sample deletes the aforementioned registry value and sends an HTTP POST request to hxxps[:]//bobsmith[.]apiworld[.]cf/license.php
with a Base64-encoded beacon containing basic host information such as hostname, username, and localtime. The sample refers to itself as CINSTALLER1
in this beacon. The beacon response is parsed as JSON containing a list of tasks, each of which specify a file to download to disk and execute.
The malware may additionally drop two batch files, execute.bat
and command.bat, to %TEMP%
during the process of attempting to run the sample as an elevated process.
.NET version
This variant (SHA256: 8a492973b12f84f49c52216d8c29755597f0b92a02311286b1f75ef5c265c30d
) is an obfuscated .NET based downloader. The malware can download and execute payloads from the C2 server, restart itself with elevated privileges, delete downloaded payloads, and communicate system information to the C2 server. The malware, when executed, optionally expects the following command line argument:
elevated_true
If the argument is provided, the malware will attempt to restart itself with elevated privileges. Otherwise, the malware creates and checks for the following mutex to prevent multiple executions:
cinstaller_2022
The malware then checks if a removable drive is mounted, and proceeds to find a directory labeled with the Hangul Filler
character (E3 85 A4
). At this point, the execution only proceeds if the directory exists. The next step is a download loop for which a JSON object with the following structure is generated:
{
"from": "CINSTALLER1",
"path": "Malware path",
"username": "<current user's Windows username>",
"cwd": "<current working directory>",
"time": "<number of seconds since Unix epoch (January 1, 1970)>",
"temp": "Temporary path",
"programs": "Program Files path"
}
The malware will then base64 encode the generated JSON and send it in a POST request to the C2 server. The configured C2 server for this sample is as follows:
hxxps://bobsmith.apiworld[.]cf/license.php
The base64 string is prepended with "AA" such that the POST data looks as follows:
"AA"
<base64 encoded JSON>"=="
The malware expects the C2 server to return a collection of objects. For each object in the collection, it extracts specific data: a link, a path, a command (cmd), arguments for the command, and a deletion flag (delete).
For each object in the received collection, if a URL and path are provided, the program attempts to download a file from the URL to the specified path. This download is retried indefinitely every 5 seconds in case of failure. If a command (cmd) is specified, the program attempts to execute it with the provided arguments, running it in a hidden window and without creating a new window. If the deletion flag (delete) is set and both the URL and path are provided, the program attempts to delete the downloaded file after execution.
In at least one investigation, Mandiant has observed this version of EMPTYSPACE relaying on additional resources on the host, most likely dropped during the initial infection. These are bootstrap.pyc, which is a Python compiled version of EMPTYSPACE with a similar capability of communicating with a list of embedded C2 domains and the QUIETBOARD backdoor. EMPTYSPACE interacts with these files via an intermediary executable, a Python wrapper named "RuntimeBroker .exe
" (vs "Runtime Broker.exe
") located in C:\Windows.
Python Version (Bootstrap.pyc)
One of the versions analyzed by Mandiant Managed Defense for bootstrap.pyc
is shown in Figure 10 (decompiled code). The BOOTSTRAP_VERSION is set to ‘PYBOOTSTRAP4’, suggesting the existence of other versions. With this in mind, more open source research revealed three other versions, each containing essentially the same code but with different domains.
Figure 10: bootstrap.pyc V4
The code continuously attempts to reach a specific URL (hxxp://google[.]com/generate_204
) with a 30-second timeout. If unsuccessful, it retries after 2 seconds. This check likely verifies internet connectivity before proceeding.
Next, it creates a variable request_data
containing encoded information:
- Encodes a JSON dictionary with user details, including username and path of the executable file.
- Adds "AA" and "==" at the beginning and end of the encoded data, exactly the same way the .NET version of EMPTYSPACE is formatting the data before sending it to the C2 server.
Once data is prepared, the code iterates over a list of URLs, attempting to send POST requests containing the request_data
.
Upon successful communication, it attempts to decode the server's response using Base64 and then deserialize it using the marshal.loads function. It executes the deserialized data using the exec function.
QUIETBOARD (Program.pyz)
QUIETBOARD is a Python based pre-compiled multi-component backdoor capable of arbitrary command execution, clipboard content manipulation for crypto currency theft, USB/removable drive infection, screenshotting, system information gathering, and communication with the C2 server. Additionally, the backdoor has the capability of modular expansion and running independent Python based code/modules. All these capabilities are provided and managed via its various components: start, coronausb, cboard, runservice, executer, info and connection.
The aforementioned modules are initiated via the primary component start, which creates multiple threads to manage each of these components in parallel. Following is a breakdown of what each component entails.
start
The start module in the malware framework serves as an orchestrator or initializer for the other components. During its execution, the module:
- Checks for the existence of a lock file (program.lock) in the current directory. If this lock file exists, it's deleted. If it cannot be deleted, the script exits immediately, which is likely a mechanism to prevent multiple instances of the malware from running simultaneously.
- Checks if a file named overload exists in the current directory. If it does, the script reads the content of this file, decodes it from Base64, unmarshals it, and executes it using the executer module.
- Checks if a directory named runs exists; if not, it creates one. If it exists, the script iterates through all files contained in this directory. Each file is treated as a script: it's read, decoded, unmarshalled, and executed similarly to the overload file. This directory could be used for executing multiple scripts, possibly allowing for modular expansion of the malware’s capabilities.
Starts the coronausb, cboard, and runservice modules in separate threads and begins the operation of the connection module synchronously.
coronausb
This component monitors and infects removable drives. It creates a hidden folder in the attached removable drives, moves existing data into the newly created folder, and creates a deceptive LNK shortcut that is made to look like the default Microsoft Windows drive icon. The name of the shortcut can be either of two patterns depending on whether a volume label is present or not:
<volume_label> (<total_size_in_gb>GB).lnk
<drive_letter> (<total_size_in_gb>GB).lnk
This shortcut is linked to a PowerShell script that is written inside the USB drive and is named explorer.ps1.
The hidden folder is created as follows:
This empty_character mechanism results in the generation of the Hangul Filler
character (E3 85 A4
) which visually shows up as a whitespace making the directory path appear as "D:\
" (assuming D to be the removable drive).
There is also a mechanism which checks if an older version of explorer.ps1
already exists on any of detected USB drives and removes it, replacing it with a new version. This ensures the “update” of older infected removable drives.
cboard
This component acts as a crypto stealer by continuously monitoring and altering the clipboard content. It tries to detect known patterns for crypto wallet addresses and replace them with its own wallet addresses with the intention of stealing crypto from any transaction the victim might conduct.
The following table lists a general breakdown of the patterns matched, and the replaced wallet addresses:
Table 2: Wallet addresses embedded in the cboard module
Additionally, Mandiant has observed the Bitcoin address bc1qk55vk7wjgzg3pmxlh59rv5dlgewd9jem5nrt4w
being injected in the HTML code of multiple Italian websites, mainly connected to Italian universities, substantiating the financial motives behind the threat actors’ actions.
The Ethereum and Doge addresses had their first transaction on the same date, within one hour of each other, on January 10, 2022. The Bitcoin address was first used towards the end of 2022, on December 11.
runservice
This component is primarily meant to dynamically fetch and execute additional Python code from the C2 server. The malware generates the following JSON based on information gathered by the info module:
The JSON is Base64 encoded and AES encrypted with the following key in CBC mode:
Key: 4lZYQ/POapYTZka0gVM/rg==
The malware then proceeds to send the encrypted JSON in a POST request to the following C2 server.
hxxps://luke.compeyson.eu[.]org/runservice/api/public.php
The malware expects to receive Python code in response, which it executes and communicates back the result of to the following URL in a post request.
hxxps://luke.compeyson.eu[.]org/runservice/api/public_result.php
This fetch and execute operation continues indefinitely until the server responds with data containing a "continue" flag set to False.
executer
This component contains the functionality to dynamically execute Python code and is used by the runservice module to execute the received Python payloads.
info
The info component of the malware is designed to gather and assemble various pieces of information about the infected computer. It then structures this information into a JSON object, which is later used in the runservice component, and is also communicated back to the C2 server by the connection component.
The module compiles the following host information:
- Generates a unique id for the system and stores it in a file named "
cUuid.dat
" (if one already does not exist). - Attempts to read the installation date from a file named "
instDate.dat
", and creates and writes to it if one is not already available. - Retrieves system specifications by executing the following WMI queries:
- Select * from Win32_OperatingSystem
- Select * from Win32_ComputerSystem
- Select * from Win32_Processor
- Select * from Win32_VideoController
- Retrieves WIFI SSIDs by running the command "
netsh wlan show interfaces
" - Retrieves BSSID information by running the command "
netsh wlan show networks mode=bssid
" - Attempts to geo locate the infected computer by querying the URL:
hxxps://www.googleapis[.]com/geolocation/v1/geolocate?key=AIzaSyBOti4mM-6x9WDnZIjIeyEU21OpBXqWBgw
- Attempts to read a UUID from a file named "
from_machine_uuid.dat
" which from context might contain the UUID of the source infection machine.
connection
The connection component communicates back all the gathered information from the victim system (generated by the info module) to the C2 server, optionally including a screenshot of the system. It can further keep operating in a loop with a sleep time specified by the C2 server, and which is by default set to 0.1s. Moreover, the connection module can execute arbitrary Python code received from the C2 server in the same loop using the executer module, similar to runservice. However, this module has the added functionality of either executing the received code synchronously or asynchronously, in a newly generated thread, based on the setting received from the configured C2 server:
hxxps://eu1.microtunnel[.]it/c0s1ta/index.php
Mandiant identified multiple versions of QUIETBOARD as well. One earlier version contained only the coronausb module, while another had all the modules previously described except for runservice. This might suggest the order in which the threat actor has developed each module, starting with the capability of infecting USB drives and adding more functionality on top of it. Having the runservice module as a last addition is telling of how the threat actor evolved, gained confidence and updated the code with a C2 capability.
In one particular infection, after months of just beaconing activity, QUIETBOARD dropped an open-source coinminer, further supporting the financial gain angle for the threat actor.
Threat Actor Spotlight: UNC4990
Mandiant has collected intelligence surrounding a campaign and additional likely related activity conducted by UNC4990 targeting organizations located in Italy, but based in Europe and the U.S,. across multiple industries, including health, transportation, construction, and logistics. Italian organizations appear to be primarily impacted by this activity.
Mandiant assesses with medium confidence that UNC4990 is a financially motivated threat actor operational since at least 2020. Based on the extensive use of Italian infrastructure throughout UNC4990 operations, including using Italian blogging platforms for C2, we believe this actor to be operating out of Italy.
Though the group’s TTPs have evolved over time, UNC4990 operations generally involve widespread USB infection followed by the deployment of the EMPTYSPACE downloader. During these operations, the cluster relies on third-party websites such as GitHub, Vimeo, and Ars Technica to host encoded additional stages, which it downloads and decodes via PowerShell early in the execution chain.
It is unclear whether UNC4990 is responsible only for initial access and foothold. In at least one investigation, Mandiant has observed the deployment of a Coinminer following months of inactivity, leaving the end goal for UNC4990 operations open.
Conclusion
Mandiant observed a clear evolution of the TTPs from the early stages of the campaign to its current form.
Starting off with the initial payload served in explorer.ps1, where a custom decoding scheme was developed to the point where it got replaced with asymmetric encryption and the addition of the capability to track infected devices.
Furthermore, the analysis of both EMPTYSPACE and QUIETBOARD suggests how the threat actors took a modular approach in developing their toolset. QUIETBOARD started by only having one module and then more functionality was incrementally added. Similarly, the Python variant of EMPTYSPACE shows clear signs of versioning. The use of multiple programming languages to create different versions of the EMPTYSPACE downloader and the URL change when the Vimeo video was taken down show a predisposition for experimentation and adaptability on the threat actors’ side.
Detection Opportunities
YARA-L Rules
Indicators of Compromise
Host-Based IOCs
Network-Based IOCs
Mandiant Security Validation Actions
Organizations can validate their security controls using the following actions with Mandiant Security Validation.
Acknowledgement
Blas Kojusner, Dimiter Andonov, Elvis Miezitis, Mike Hunhoff, Moritz Raabe, Mustafa Nasser, Nikolay Marinov