Banshee Rust Rewrite?
Infostealers targeting macOS are evolving rapidly, making continuous monitoring essential, which our team is always on the lookout for. Many infostealers share similar behaviors aimed at exfiltrating data from compromised systems. In fact, these similarities can make it difficult to distinguish between different infostealers without a deep understanding of what to look for.
Recently, the Objective-C source code for the infostealer "Banshee" was leaked, offering insight into its inner workings. On January 15, 2025, our team identified a new infostealer written in Rust on VirusTotal. This infostealer exhibits many of the same behaviors and targets, (such as browsers, wallets, and extensions,) found in the leaked Banshee code. Interestingly, the Rust-based application transmits captured files to localhost, suggesting it may still be in the testing or development phase.
In this article, we will examine the behavior of this Rust-based application and compare it to the leaked Objective-C code to provide insights into reverse-engineering Rust malware.
File Analysis
Before we begin, I want to first highlight the function names seen in this application that give us hints as to the behavior we should expect as we continue our analysis.
Comparing these names with the leaked Banshee Objective-C source code, we see many similarities which further supports the theory of this being a rewrite of Banshee in Rust. Also, Rust adds a unique identifier to function names when compiling so we will be removing them from the function names for readability. Let’s dive into some of them and compare what we see in the leaked Banshee source code to this Rust binary, starting with the main() function.
mac_os_stealer::main()
The main() function in Rust gives you the address of where the actual start of the application is.
At the start of this function named mac_os_stealer::main()
, there is a call to std::env::args()
, which returns the launch arguments of the current application. This is used in a check for a specific string “run_controller” that is expected to be passed as a launch argument.
Given this check, we can see that this application can be launched with this launch argument and would initialize a Controller object. It also has the ability to launch itself by passing the string “run_controller” along with the string “5Hi/42mU6An00CRnEQ7BmqDv92He3xofcXsrq9u/6OEb+o+fBgtuFHTpW03Nlvnm”. This is similar to launch arguments that have been seen in previous infostealers. Next, we have another call to launch a new process.
The “killall” string is passed to the Command::new()
function and the “Terminal” string is passed to the Command::arg()
function, which is then spawned as a process.
We can see similar code in the Banshee source code including a check for launch arguments and then the command to terminate the Terminal.
After the killall command executes, there is a branch to a function which checks if the application is running in a Virtual Machine that we cover next.
mac_os_stealer::antivm::check_vm()
This function begins by calling mac_os_stealer::tools::exec()
, which accepts a command string and its size.
Given the lack of null bytes in Rust strings, we need to use the size of the string (passed along with the reference to the string object) to understand which strings are used. We can read the bytes of the command string by using bv.read() in Binary Ninja by passing the address of the string referenced and the size of the string.
>>> bv.read(0x100051818, 0x3c)
b"system_profiler SPHardwareDataType | grep 'Model Identifier'"
This command is commonly used by infostealers to query for system information and determine if they are executing in a Virtual Machine. Following the output of this command, there is a check for the string “Virtual” using the function core::str::pattern::StrSearcher::new()
.
Let’s now focus on the mac_os_stealer::tools::exec()
function as it is used more than once by this application.
mac_os_stealer::tools::exec()
As covered above, this function is passed a string for the command to execute. We can see how this string is used by taking a look at this function.
We can see the use of the Process module using new() for the command “sh”, and two arg() calls to set up “-c” and the command passed to this function. This function is used several times in this application, so now we can tell what it is for and what we should expect to be passed as arguments.
Debugger Check
Going back to the main() function, we see a call to sysctl() which in this case is used to check if there is a debugger attached.
This is similar to the Banshee source code for checking if there is a debugger attached as seen below.
Both of these checks, result in a call to a print function std::io::stdio::_print()
to print out information about the system including:
VM Check Result:
Debugger Check Result:
We will continue with the next function called mac_os_stealer::system_info::get_system_info()
used to obtain information about the system.
mac_os_stealer::system_info::get_system_info()
At the start of this function, we see three calls to mac_osstealer::tools::exec()
. We already covered the tools::exec()
function above, so we know that these three commands will be executed. Let’s see how they appear in the decompilation below.
These three commands will be executed to obtain information about the system including the OS version, type of machine, and memory size. The information obtained is returned to the main() function and printed along with “System Info:”. The next function creates a temporary directory, however we will not cover this one here. After the creation of the temporary directory we have a call to mac_os_stealer::sender::send_data()
, which prepares for network connections.
mac_os_stealer::sender::send_data()
At the beginning of this function, we can see functions related to encryption. We are not going to dive into these specifically but we want to highlight some of the values seen being used by these functions.
The first string we see above in the XOR related instructions is the same key we saw in the beginning of the main() function, which was used for a check of a launch argument. Following that string, we see 4 other XOR related instructions and a check at the end which will print out the size of the encryption after completion: “Data size after encryption:”
Towards the middle of the function, we can see evidence of this application potentially being in development similar to our previous Purrglar blog post with the use of localhost as the destination and in this case 3030 as the port.
The std::net::tcp::TcpStream::connect_timeout()
function is then used to create a TCP connection to the address seen above which is 127.0.0.1:3030.
Next, we will focus more on the stealer-related behavior, starting with the functions related to browsers.
mac_os_stealer::browsers::Browsers::new()
At the beginning of this function, we can see the setup for browsers Infostealers commonly targeted including Chrome, Firefox, Microsoft Edge, Yandex, etc.
Along with these browsers and their expected file paths, we can also see extension IDs for commonly targeted wallets, however, the decompilation of this function seems to affect how these extension IDs are parsed.
We can compare these to the leaked source code and see many of the same extension IDs, which is expected and used by other infostealers as well.
After this setup function returns, another function named mac_os_stealer::browsers::Browsers::collect_all_data()
is called to capture files from these browsers by specifying the file paths, using a stat()
function to check if the file exists, and creating a directory named “Browsers” to store the collected files. We won’t cover this behavior as it matches what has already been reported on for known infostealers. We will instead cover the function used for capturing wallet-related information from the system next.
mac_os_stealer::wallets::Wallets::collect_wallet_data()
This will be the last function we cover as we wrap up the comparisons in this Rust application to the leaked Banshee Objective-C source code. To start, we can see the setup for the wallets expected to be targeted by Infostealers towards the beginning of the function.
As mentioned previously, Rust strings do not carry null termination so we need to parse them using the size. We can see the start of the referenced string blob added to an array and the size of each path at +0x8 offset of each member. If we compare this to the leaked source code, we can see the same paths used which further emphasizes the potential of this being a rust implementation of Banshee.
These wallet paths are later used for functions related to the collection of these files to copy them to a folder Wallets similar to the other files collected and covered above. We can expect that these folders containing the captured files will then be sent to the configured command and control server.
Conclusion
Based on our analysis of the leaked Banshee source code, we can confidently assert that this Rust-based infostealer is a rewrite of Banshee. This is supported by several factors: the application uses similar function names, prints the same error and status strings, and targets identical files. The use of localhost as the destination further suggests that this may be a test application or a prototype created using the leaked code.
Our investigation highlights the importance of staying vigilant as macOS malware continues to adapt and evolve.
IOCs
Sha256 Hash:
dea72cdd7c9dfc49f0a19581086c8e6e99b000dc33f461ece8b9f37c1bd7068d
Network Communications:
127.0.0.1:3030
See Kandji in Action
Experience Apple device management and security that actually gives you back your time.
See Kandji in Action
Experience Apple device management and security that actually gives you back your time.