Back in April, researchers at JAMF detailed a sophisticated APT campaign targeting macOS users with multi-stage malware that culminated in a Rust backdoor capable of downloading and executing further malware on infected devices. ‘RustBucket’, as they labeled it, was attributed with strong confidence to the BlueNoroff APT, generally assumed to be a subsidiary of the wider DPRK cyber attack group known as Lazarus.
RustBucket is noteworthy for the range and type of anti-evasion and anti-analysis measures seen in various stages of the malware. In this post, we review the multiple malware payloads used in the campaign and highlight the novel techniques RustBucket deploys to evade analysis and detection.
RustBucket Stage 1 | AppleScript Dropper
The attack begins with an Applet that masquerades as a PDF Viewer app. An Applet is simply a compiled AppleScript that is saved in a
.app format. Unlike regular macOS applications, Applets typically lack a user interface and function merely as a convenient way for developers to distribute AppleScripts to users.
The threat actors chose not to save the script as run-only, which allows us to easily decompile the script with the built-on
osadecompile tool (this is, effectively, what Apple’s GUI Script Editor runs in the background when viewing compiled scripts).
The script contains three do shell script commands, which serve to download and execute the next stage. In the variant described by JAMF, this was a barebones PDF viewer called
Internal PDF Viewer. We will forgo the details here as researchers have previously described this in detail.
Stage 1 writes the second stage to the
/Users/Shared/ folder, which does not require permissions and is accessible to malware without having to circumvent TCC. The Stage 1 variant described by Elastic differs in that it writes the second stage as a hidden file to
The Stage 1 is easily the least sophisticated and easily detected part of the attack chain. The arguments of the
do shell script commands should appear in the Mac’s unified logs and as output from command line tools such as the
Success of the Stage 1 relies heavily on how well the threat actor employs social engineering tactics. In the case described by JAMF, the threat actors used an elaborate ruse of requiring an “internal” PDF reader to read a supposedly confidential or ‘protected’ document. Victims were required to execute the Stage 1 believing it to be capable of reading the PDF they had received. In fact, the Stage 1 was only a dropper, designed to protect the Stage 2 should anyone without the malicious PDF stumble on it.
RustBucket Stage 2 | Payloads Written in Swift and Objective-C
We have found a number of different Stage 2 payloads, some written in Swift, some in Objective-C, and both compiled for Intel and Apple silicon architectures (see IoCs at the end of the post). The sizes and code artifacts of the Stage 2 samples vary. The universal ‘fat’ binaries vary between 160Kb and 210Kb.
Across the samples, various username strings can be found. Those we have observed in Stage 2 binaries so far include:
/Users/carey/ /Users/eric/ /Users/henrypatel/ /Users/hero/
Despite the differences in size and code artifacts, the Stage 2 payloads have in common the task of retrieving the Stage 3 from the command and control server. The Stage 2 payload requires a specially-crafted PDF to unlock the code which would lead to the downloading of the Stage 3 and provide an XOR’d key to decode the obfuscated C2 appended to the end of the PDF.
In some variants, this data is executed in the
downAndExecute function as described by previous researchers; in others, we note that download of the next stage is performed in the aptly-named
down_update_run function. This function itself varies across samples. In
b02922869e86ad06ff6380e8ec0be8db38f5002b, for example, it runs a hardcoded command via
However, the same function in other samples, (e.g.,
d5971e8a3e8577dbb6f5a9aad248c842a33e7a26) use NSURL APIs and entirely different logic.
Researchers at Elastic noted, further, that in one newer variant of Stage 2 written in Swift, the User-Agent string is all lowercase, whereas in the earlier Objective-C samples they are not.
Although User-Agent strings are not inherently case sensitive, if this was a deliberate change it is possible the threat actors are parsing the User-Agent strings on the server side to weed out unwanted calls to the C2. That said, sloppiness around case-sensitivity is seen elsewhere in RustBucket samples (e.g., “/users/shared” in Stage 1), and the case variance may be no more than a product of different developers with different standards of rigor.
In the most recent samples, the payload retrieved by Stage 2 is written to disk as“ErrorCheck.zip” in
$TMPDIR typically at
/var/folders/…/../T/) before being executed on the victim’s device.
RustBucket Stage 3 | New Variant Drops Persistence LaunchAgent
The Stage 3 payload has so far been seen in two distinct variants:
- A: 182760cbe11fa0316abfb8b7b00b63f83159f5aa Stage3
- B: b74702c9b82f23ebf76805f1853bc72236bee57c ErrorCheck, System Update
Both variants are Mach-O universal binaries compiled from Rust source code. Variant A is considerably larger than B, with the universal binary of the former weighing in at 11.84MB versus 8.12MB for variant B. The slimmed-down newer variant imports far fewer crates and makes less use of the sysinfo crate found in both. Notably, variant B does away with the
webT class seen in variant A for gathering environmental information and checking for execution in a virtual machine via querying the
SPHardwareDataType value of
However, variant B has not scrubbed all
webT artifacts from the code and reference to the missing module can still be found in the strings.
18070 0x0032bdf4 0x10032bdf4 136 137 ascii /Users/carey/Dev/MAC_DATA/MAC/Trojan/webT/target/x86_64-apple-darwin/release/deps/updator-7a0e7515c124fac6.updator.ab9d0eaa-cgu.0.rcgu.o
The substring “Trojan”, which does not appear in earlier variants, is also found in the file path referenced by the same string.
Importantly, variant B contains a persistence mechanism that was not present in the earlier versions of RustBucket. This takes the form of a hardcoded LaunchAgent, which is written to disk at
ErrorCheck file also writes a copy of itself to
~/Library/Metadata/System Update and serves as the target executable of the LaunchAgent.
Since the Stage 3 requires a URL as a launch parameter this is provided in the property list as a Program Argument. Curiously, the URL passed to ErrorCheck on launch is appended to this hardcoded URL in the LaunchAgent plist.
Appending the supplied
<url> value to the hardcoded URL can be clearly seen in the code, though whether this is an error or accounted for in the way the string is parsed by the binary we have yet to determine.
Much of the malware functionality found in variant A’s
webT methods is, in variant B, now buried in the massive
sym.updator::main function. This is responsible for surveilling the environment and parsing the arguments received at launch, processing commands, gathering disk information and more. This massive function is over 22Kb and contains 501 basic blocks. Our analysis of this is ongoing but aside from the functions previously described by Elastic, this function also gathers disk information, including whether the host device’s disk is SSD or the older, rotational platter type.
After gathering environmental information, the malware calls
sym.updator::send_request to post the data to the C2 using the following User-Agent string (this time not in lowercase):
Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)
The malware compares the response against two hardcoded values,
In the sample analyzed by Elastic, the researchers reported that
0x31 causes the malware to self-terminate while
0x30 allows the operator to drop a further payload in the
The choice of Rust and the complexity of the Stage 3 binaries suggest the threat actor was willing to invest considerable effort to thwart analysis of the payload. As the known C2s were unresponsive by the time we conducted our analysis, we were unable to obtain a sample of the next stage of the malware, but already at this point in the operation the malware has gathered a great deal of host information, enabled persistence and opened up a backdoor for further malicious activity.
SentinelOne Protects Against RustBucket Malware
SentinelOne Singularity protects customers from known components of the RustBucket malware. Attempts to install persistence mechanisms on macOS devices are also dynamically detected and blocked by the agent.
The RustBucket campaign highlights that the threat actor, whom previous researchers have confidently attributed to DPRK’s BlueNoroff APT, has invested considerable resources in multi-stage malware aimed specifically at macOS users and is evolving its attempts to thwart analysis by security researchers.
The extensive effort made to evade analysis and detection in itself shows the threat actor is aware of the growing adoption of security software by organizations with macOS devices in their fleets, as security teams have increasingly begun to see the need for better protection than provided out-of-the-box. SentinelOne continues to track the RustBucket campaign and our analysis of the known payloads is ongoing.
Indicators of Compromise
Stage 2 Mach-Os
Stage 3 Version A Mach-Os
Stage 3 Version B Mach-Os
|469236d0054a270e117a2621f70f2a494e7fb823||DOJ Report on Bizlato Investigation.pdf|
|7f8f43326f1ce505a8cd9f469a2ded81fa5c81be||Jump Crypto Investment Agreement.pdf|
|be234cb6819039d6a1d3b1a205b9f74b6935bbcc||DOJ Report on Bizlato Investigation_asistant.pdf|
Stage 1 Applications (.zip)
$TMPDIR/ErrorCheck.zip /Users/Shared/1.zip /Users/Shared/Internal PDF Viewer.app /Users/Shared/.pd ~/Library/Metadata/System Update ~/Library/LaunchAgents/com.apple.systemupdate.plist