QuietPolyfill, en NPM-dropper som gjenoppsto

QuietPolyfill: En NPM-dropper som gjenoppsto

TL; DR

Tjue npm-pakker publisert over to dager i slutten av juli og begynnelsen av august 2026 presenterer seg som interne UI-komponentbiblioteker og delte backend-verktøy. Ingen av dem implementerer funksjonaliteten de annonserer. Hver pakke leverer en stub-klasse, en README-fil som beskriver pakken som intern, og en fil kalt _polyfill.js.

Det interessante er hvor filen hentes fra. Det er ikke en installasjonshook — manifestene deklarerer ingen preinstall, install eller postinstall i det hele tatt. I stedet er den siste linjen av index.js lyder: try { require("./_polyfill"); } catch (_) {}Nyttelasten kjører første gang programkoden importerer pakken. Installerer med --ignore-scriptsden standard råd for å redusere eksponering for forsyningskjeden under installasjon, hindrer det ikke.

Hva _polyfill.js Det som gjøres er å hente en plattformspesifikk kjørbar fil fra en av tre Cloudflare Workers-verter, skrive den til en midlertidig katalog under et tilfeldig navn, merke den som kjørbar og starte den løsrevet fra den overordnede prosessen. Den kjørbare filen er i seg selv en laster: den lister opp endepunktsikkerhetsprodukter etter prosessnavn, sjekker gjeldende brukernavn mot kjente analysemiljøkontoer, oppdaterer AMSI- og ETW-telemetrigrensesnittene i minnet, installerer persistens under navnet til en Windows-lydkomponent og henter en tredje binærfil på flere megabyte. Det tredje trinnet ankommer kryptert og er dekryptert og utført i minnet i stedet for skrevet til disk.

Den 1. august, omtrent fire timer etter at pakkene ble bekreftet skadelige ved manuell gjennomgang, ble nitten av de tjue navnene fjernet fra npm av sin egen utgiver i løpet av en sekvens på tjuefem sekunder. Den 2. august ble et av disse navnene registrert på nytt – av en annen konto, med samme kode. Leveringsinfrastrukturen tjente alle tre stadiene da dette ble skrevet.

Nettsidepakker 20 npm-navn, ~74 publiserte versjoner
Navngiving av temaerbeaver-ui-* (13) accounts-* (3), pluss afisha-storybook-default, bcore-bravo-eslint-config, arbocrate-sla-prober-arbocrate-sla-prober-core, a.poltoradnev-package-c
AvtrekkerModulimport (require), ikke et npm-livssyklusskript
Stage 1_polyfill.js — henting og oppskyting
Stage 2Statisk koblet laster, UPX-pakket på Linux; opplisting av endepunktsikkerhet, AMSI/ETW-patching, persistens
Stage 3beacon_linux.bin / beacon_mac.bin / update_win.exe, 4–13 MB; levert kryptert, utført i minnet
PlattformerLinux x64 og arm64, macOS universal, Windows x64
UtgivereTo kontoer, én kodebase
status19 av 20 navn er ikke publisert av operatøren; 1 publisert på nytt og live; leveringsinfrastruktur live

Angrepsanatomi

Fase 1 – pakken

Hvert medlem sender den samme tre-filsformen. index.js eksporterer en klasse med en init() som setter en boolsk verdi, en versjon()Og konfigurer() som slår sammen et alternativobjekt. Ingenting annet. README.md gir en installasjonslinje, en krever linje, og setningen «Intern pakke. Se CONTRIBUTING.md for retningslinjer» — BIDRAG.md er ikke i tarballen.

_polyfill.js er litt under 5 KB og leses som vanlig infrastrukturkode. Den åpnes med tre miljøkontroller:

if (process.env.DISABLE_TELEMETRY ||
    process.env.ANALYTICS_OPT_OUT ||
    process.env.DO_NOT_TRACK)
  return;

Disse variablene er konvensjonelle personvernkontroller. Her bestemmer de om en binærfil lastes ned og kjøres. En maskin som setter IKKE_SPOR blir hoppet over helt.

Deretter kommer et stempel som kjøres én gang: en fil på /tmp/.analytics_stateeller analysestatus etter % TEMP% på Windows, der modifikasjonstiden sammenlignes med et vindu på 20 357 sekunder. Innenfor dette vinduet returnerer modulen umiddelbart, så gjentatt import under en bygging produserer ett henteforsøk i stedet for dusinvis.

Destinasjonsvertene finnes ikke som literaler. De settes sammen under kjøring fra fragmenter:

["oob-worker.cf102-","baf.workers.d","ev"].join("")

Tre verter er bygget på denne måten og omstokket før bruk, så rekkefølgen varierer per utførelse. Henting er en HTTPS GET festet til IPv4 med en User-Agent of node-fetch/2.6 — en plausibel verdi for en Node-prosess å sende ut. Et svar under 1,000 byte forkastes, og neste vert forsøkes.

Hvis alle tre feiler, går modulen tilbake til DNS. Den sender en TXT-forespørsel for c. for å få et antall deler, deretter ber om 0. gjennom n. I grupper på ti, sammenkobler svarene og base64-dekoder resultatet. Denne banen fører den samme kjørbare filen over DNS TXT-poster, som overlever miljøer der utgående HTTP filtreres, men DNS-oppløsning ikke.

De hentede bytene skrives til /var/tmp/.cache_<8-heks> på Unix, eller %TEMP%\dotnet_diag_<8-hex>.exe på Windows – et filnavn som leses som en .NET-diagnostikkartefakt. På Unix får filen modus 0755. Oppstart er spawn med løsrevet: sant, stdio: «ignorer»og unref(), slik at barnet overlever Node-prosessen som startet det; på Windows legger kallet til windowsSkjul: sant.

To detaljer fullfører bildet. En hjelper definert som const _l = (…a) => prosess.stderr.write(“”) kalles ved hvert trinn i hentingen – den skriver en tom streng og forkaster argumentene, og opptar posisjonen der loggingen ville vært. Og hver pakke sender en 81 KB-fil kl. lib/telemetri.js hvis headerkommentar beskriver en «Analytics SDK» med «HTTPS-transport med endepunktrotasjon og nytt forsøk» og «DNS-basert tjenesteoppdagelse for failover». I den nåværende versjonen er det ingenting som krever det.

Trinn 2 – lasteren

Linux x64-binærfilen er en statisk lenket ELF komprimert med UPX 3.96. macOS-bygget er en universell binærfil som dekker x86_64 og arm64; Windows-bygget er en strippet PE32+ konsollkjørbar fil. Alle fire ble hentet og undersøkt statisk; ingen ble kjørt.

Importene og strengtabellen i Windows-bygget beskriver oppførselen tydelig. Den løser ErDebuggerPresent, CheckRemoteDebuggerPresentog NtQueryInformasjonsprosess, og leser SYSTEM\CurrentControlSet\Services\Disk\Enum — en registersti hvis verdier identifiserer virtuelle diskkontrollere. Den kaller GetDiskFreeSpaceExW og GlobalminnestatusEksempel, i samsvar med å sjekke om vertens ressurser samsvarer med en kortvarig analyseinstans.

Den inneholder en liste over brukernavn: Nåværende bruker, IT-administrator, Hong Lee, TESTBOKS, hapubws, milozs, Timmy, admin, malware, sample, virus, miljøFlere av disse er gjenstander fra offentlig dokumenterte analysemiljøer.

Den inneholder også en liste over produkter for endepunktsikkerhet og prosessnavnene deres, oppregnet gjennom CreateToolhelp32Snapshot og Prosess32FørstW/Prosess32NesteW:

  • CrowdStrike Falcon (csfalconservice, CSFalconContainer)
  • Carbon Black (cb.exe)
  • Cylance (cylancesvc.exe)
  • Bitdefender (bdagent.exe)
  • Kaspersky (avp.exe, kavfs.exe)
  • Trend Micro og dyp sikkerhet (coreServiceShell, ds_agent.exe)
  • FireEye/Trellix (xagt.exe)
  • Tanium (taniumclient.exe)
  • HitmanPro (hmpalert.exe)
  • Elastisk (elastic-agent.exe, elastic-endpoint, filebeat.exe, winlogbeat.exe)
  • Wazuh og OSSEC (wazuh-agent.exe, ossec-agent.exe)
  • Check Point
  • Windows Defender

To ytterligere symboler er til stede: AmsiScanBuffer, inngangspunktet Windows eksponerer for skanning av innhold i minnet, og EtwEventWrite, funksjonen som Event Tracing for Windows bruker til å registrere hendelser. Begge er vanlige mål for modifikasjoner i prosessen som undertrykker telemetrien disse grensesnittene ellers ville produsert.

Persistens vises to ganger i Windows. Binærfilen kopieres til %APPDATA%\Microsoft\Windows\Themes\SystemSoundsService.exe, og to mekanismer refererer til den: en verdi under PROGRAMVARE\Microsoft\Windows\Gjeldende versjon\Kjør, og en planlagt oppgave registrert som

schtasks /Create /F /SC ONLOGON
  /TN "\Microsoft\Windows\Multimedia\SystemSoundsService"
  /TR "\"%s\""
  /RL HIGHEST

Oppgavebanen, oppgavenavnet og navnet på den kjørbare filen speiler alle en ekte Windows-lydkomponent. /RL HØYESTE ber om det høyeste tilgjengelige privilegienivået.

macOS-bygget følger samme mønster med plattformtilpassede navn: den skriver en egenskapsliste til ~/Bibliotek/LaunchAgents/com.apple.windowserver.helper.plist og plasserer den kjørbare på ~/.local/share/runtime/com.apple.runtimeDen sjekker etter /Programmer/VMware Fusion.app og /Bibliotek/Programstøtte/VMware-verktøy.

Nettverkskapasiteten spenner over tre transporter: WININET.dll for HTTPS med en https://%s:%d%s formater streng, WS2_32.dll stikkontakter, og DNSAPI.dll med DnsQuery_A ved siden av a c.%s.%s formatstreng – den samme chunk-count-spørringsformen som den første fasen bruker.

Beskrivelsen ovenfor er hva binærfilens import, strenger og registerstier viser ved inspeksjon. Offentlig flermotoranalyse av det samme Windows-eksemplet legger til detaljer som en statisk lesning av en importtabell ikke kan fastslå på egenhånd, og er tilskrevet her snarere enn gjengitt som førstehåndsobservasjon. Denne analysen rapporterer at AMSI- og ETW-referansene ikke bare er løst, men oppdatert – AmsiScanBuffer in amsi.dll og EtwEventWrite in ntdll.dll — som undertrykker skanning av innhold i minnet og hendelsessporing for prosessen. Den rapporterer antianalysesjekkene som tidsbaserte i tillegg til CheckRemoteDebuggerPresent, og identifiserer GetThreadContext kall som en maskinvareavbruddskontroll som leser feilsøkingsregistrene. Den rapporterer også en enkeltforekomst av mutex, Global\{7A3B9E1F-C2D4-4F8A-B5E6-D7A1F0C3E2B9}, og navn SystemSoundsService som Run-key-verdien. Denne mutex-verdien var ikke blant strengene som ble gjenopprettet her – utvinningsfilteret som ble brukt ekskluderte avstivede store bokstaver med heksadesimale tokens – og den er registrert i indikatortabellen basert på den offentlige analysen snarere enn direkte observasjon.

Stage 3

Lasteren refererer til et ytterligere sett med stier på de samme vertene. Alle var tilgjengelige under analysen: beacon_linux.bin på 5.3 MB, beacon_mac.bin på 13.3 MB, update_win.exe på 10.3 MB, og arm.bin på 4.3 MB. Disse ble ikke hentet, og oppførselen deres er ukjent.

For Windows-banen beskriver offentlig analyse av lasteren hvordan det tredje trinnet håndteres: det hentes over WinINet, ankommer kryptert, og dekrypteres i minnet og utføres reflekterende – minne allokert, importer løst, relokaliseringer behandlet og kontroll sendt til en ny tråd via OpprettTrådPå grunn av dette blir det tredje trinnet aldri skrevet til filsystemet, så filbasert deteksjon og disketterforskning etter hendelser har ingenting å gjenopprette fra det. Det betyr også at henting av bytene fra leveringsverten vil gi kryptert tekst, ikke en analyserbar kjørbar fil.

 

Tidslinje

Alle tidsstempler er UTC og kommer fra npm-registermetadata, som registrerer pakkeoppretting og -avpublisering uavhengig av noen observatør.

NårHva
2026-07-31 09:25:05 → 09:29:56Ti navn opprettet, hvert med start på versjon 0.0.1, omtrent tretten sekunder fra hverandre.
2026-08-01 09:38 → 11:29Eksisterende navn klatrer gjennom oppblåste versjonsnumre, 6.1.9 gjennom 6.1.12, 9.9.9 gjennom 9.9.12, 8.9.3 gjennom 8.9.6, 5.4.6 gjennom 5.4.9.
2026-08-01 13:04:14 → 13:04:18Ni ytterligere beaver-ui-* navn opprettet i en fire sekunders serie, hvert publisert direkte i en 12.x-versjon uten tidligere historikk.
2026-08-01 ~13:00–14:30Versjoner klassifisert som skadelige ved manuell gjennomgang.
2026-08-01 17:39:49 → 17:40:14Nitten av de tjue navnene ble ikke publisert i løpet av tjuefem sekunder, sekvensielt, med omtrent 1.2 sekunders mellomrom.
2026-08-02 18:58:41a.poltoradnev-package-c opprettet på nytt under en annen vedlikeholderkonto, versjon 0.0.2.
2026-08-02 19:20:28Versjon 33.9.5 utgitt med samme navn.
2026-08-02 20:14Alle tre leveringsvertene betjener trinn to og tre; DNS-reserve svarer med chunk-antall.

To av disse innleggene er verdt å dvele ved.

13:04-utbruddet la ni pakker på registeret på fire sekunder, hver med én enkelt versjon i tolverklassen og ingen 0.0.1 under den. Versjonsnumre på denne skalaen, på navn som er én dag gamle, beskriver ikke en utgivelseshistorikk.

Avpubliseringssekvensen er like mekanisk: nitten navn, tjuefem sekunder, jevn avstand. npm registrerer disse som vanlige avpubliseringshendelser i stedet for plassholderen en registerinitiert fjerning etterlater seg, noe som plasserer handlingen hos publiseringskontoen i stedet for hos registeret.

 

Indikatorer for kompromiss

Verter og stier er defanget. Ikke løs eller hent dem fra et produksjonssystem.

typenVerdi
Leveringsvertoob-worker[.]cf99-9b3[.]workers[.]dev
Leveringsvertoob-worker[.]cf102-baf[.]workers[.]dev
Leveringsvertoob-worker[.]cf103-070[.]workers[.]dev
DNS-transporttin[.]dl[.]well1[.]site (Linux x64), tina[.] (arm64), ldr[.] (Mac os), win[.] (Windows) — antall deler ved c.<domain>, data på <n>.<domain>
Trinn 2-stier/pkg/package, /pkg/package-arm64, /pkg/loader_mac, /pkg/package.exe
Trinn 3-stier/pkg/beacon_linux.bin, /pkg/beacon_mac.bin, /pkg/update_win.exe, /pkg/arm.bin
Slippbane (Unix)/var/tmp/.cache_<8-hex>
Slippbane (Windows)%TEMP%\dotnet_diag_<8-hex>.exe
Persistens (Windows)%APPDATA%\Microsoft\Windows\Themes\SystemSoundsService.exeoppgave \Microsoft\Windows\Multimedia\SystemSoundsService; Kjørnøkkelverdi SOFTWARE\Microsoft\Windows\CurrentVersion\Run\SystemSoundsService
Mutex (Windows)Global\{7A3B9E1F-C2D4-4F8A-B5E6-D7A1F0C3E2B9}
Persistens (macOS)~/Library/LaunchAgents/com.apple.windowserver.helper.plist; binær på ~/.local/share/runtime/com.apple.runtime
Kjør én gang-stempel/tmp/.analytics_state, %TEMP%\analytics_state (20 357 sekunders vindu)
Be om signaturHTTPS GET, IPv4-festet, brukeragent: node-fetch/2.6
Fase 1-fil_polyfill.js, SHA-256 392dc9ec85042ce1796bfa9b8c7bf6e05c7710579aab8b7b1eb73788683883af
Fil uten referanselib/telemetry.js, SHA-256 2266c9b8e199daebe31a9afcb61d9b0d76706b63c92312fc8223d22de42e63c7
Trinn 2 (Linux x64)SHA-256 7e486657f30594afda379b97030252a09a19fe8055e25c9e371544f59bd8e9e3
Fase 2 (Linux arm64)SHA-256 aafc37589c0d21fb729761a1579ca3b278b95bce152aacfa29243ffd27f5eeb7
Trinn 2 (macOS)SHA-256 c214746c74cae8ece8bdaf69aa05da4db6ce013f9e77452d1eed1a002fd9ba00
Trinn 2 (Windows)SHA-256 d632c2c0c7801c3da57cf849fe4b4ca8351d9a7fe73c31050bf5dbcc96a16791

Pakkenavn

beaver-ui-skuffe beaver-ui-handlinger-knapp beaver-ui-kort-stor beaver-ui-datointervallvelger bever-ui-skjema beaver-ui-form-modal beaver-ui-form-objekt beaver-ui-grid beaver-ui-header bever-ui-hooks beaver-ui-ikonlås beaver-ui-elementer-med-mer beaver-ui-layout regnskap-endelig-skjema kontoer-lastestatus kontotidslinje afisha-storybook-standard bcore-bravo-eslint-config arbocrate-sla-prober-arbocrate-sla-prober-core a.poltoradnev-pakke-c

Attribusjon og observert atferd

To npm-kontoer er involvert. De nitten upubliserte navnene ble publisert av én konto; a.poltoradnev-pakke-c ble registrert på nytt 2. august under et annet kontonavn og et annet e-postdomene. Koden er den samme avstamningen i begge, og den andre registreringen har de samme leveringsvertene, de samme DNS-reservedomenene, samme drop-path-konstruksjon og samme kjør én gang-stempel.

Det gjenutgitte paret viser også hvordan det ble satt sammen. 0.0.2 og 33.9.5 er byte-identiske bortsett fra versjonsstrengen i pakke.json, og index.js sendt som 0.0.2 erklærer konstant VERSJON = “33.9.5”Begge artefaktene ble kuttet fra ett kildetre og nummerert etterpå. Det finnes ingen 0.0.2 utviklingshistorien for 0.0.2 utgivelse for å representere.

Versjonsnummereringen på tvers av hele settet følger samme logikk. Pakkenavn i denne kampanjen leses som ting som ville eksistert i en organisasjon i stedet for i et offentlig register: et UI-komponentsett, en ESLint-konfigurasjon, en SLA-prober, en pakke oppkalt etter en person. Når en build løser en avhengighet etter navn og konsulterer et offentlig register sammen med et privat register, er det det høyeste versjonsnummeret som vinner. Et navn opprettet på 0.0.1 på mandag og bærer 12.9.6 på tirsdag er posisjonert for den sammenligningen.

De to publikasjonsbølgene er også forskjellige på en måte som antyder iterasjon. Den tidligere koden ga nyttelastfilen navnet setup.js og listet opp fem leveringsverter; den gjeldende filen heter _polyfill.js og viser tre. Den tidligere versjonen fjernet den slettede filen fem sekunder etter oppstart, slik at prosessen kjørte fra en slettet inode; den nåværende versjonen gjør ikke det. Den urefererte lib/telemetri.js forblir i tar-filen på tvers av begge, fortsatt sendt, ikke lenger koblet til.

Én observasjon om kill switchene er verdt å nevne på forhånd.cisely, fordi det bryter med en vanlig lesning. DEAKTIVER_TELEMETRI, ANALYSE_OPT_OUTog IKKE_SPOR blir respektert, og de blir sjekket før noe annet skjer. En forsker hvis analysemiljø setter noen av dem, observerer en pakke som ikke gjør noe i det hele tatt.

Innvirkning og hva forsvarere kan gjøre

Den praktiske konsekvensen av importtidsutløseren er smal og verdt å nevne i seg selv. npm install –ignore-scripts forhindrer at livssyklusskript kjører, og det er et godt råd. Det har ingen effekt her, fordi denne koden ikke bruker et livssyklusskript. Nyttelasten kjører når applikasjonskode, en testkjøring, en bundler eller en linter først importerer modulen. På en CI-runner som installeres og deretter umiddelbart bygger, er de to sekunder fra hverandre.

For jakt er de varige signalene strukturelle snarere enn bokstavelige. Vertene, filnavnet og slippstiene vil endres; disse formene er vanskeligere å variere:

  • Et modulstartpunkt hvis endelige setning er en prøv { require(“./x”); } catch (_) {} rundt en fil som ikke er en del av det eksporterte API-et. En pakke med en ekte polyfill importerer den øverst og bruker resultatet. Å svelge alle feil fra en import nederst i filen er ikke feilhåndtering.
  • Vertstrenger satt sammen av .bli med(“”) fra en rekke fragmenter. Legitim kode har ingen grunn til å dele et vertsnavn på tvers av array-elementer.
  • En pakke hvis deklarerte avhengigheter er tomme, men som strekker seg etter https, dnsog barneprosess ved importtidspunktet. Henting pluss utførelse i en pakke som annonserer en UI-komponent er hele funnet.
  • Versjonsnumre er ikke i samsvar med pakkens alder. Et navn som først ble publisert for dager siden i en tosifret hovedversjon, uten mellomliggende utgivelser, beskriver noe annet enn utviklingen.
  • Skriver til / Var / tmp or % TEMP% etterfulgt av a chmod til 0755 og en løsrevet gyte. Sekvensen er kort, ordnet og sjelden godartet sammen.

På Windows-verter finnes det to billige sjekker med høy tillit. Persistensartefaktene er faste strenger, og ingen av dem hører hjemme på et rent system: en planlagt oppgave på \Microsoft\Windows\Multimedia\SystemSoundsService, en Run-nøkkelverdi med navnet SystemSoundsService, og en kjørbar fil med samme navn under %APPDATA%\Microsoft\Windows\Temaer\Den ekte Windows-lydtjenesten finnes ikke på noen av disse stedene. For det andre bruker lasteren en fast mutex for én instans, Global\{7A3B9E1F-C2D4-4F8A-B5E6-D7A1F0C3E2B9}; dens tilstedeværelse på en vert er en indikator, og fordi lasteren behandler en eksisterende mutex som "allerede kjørende", er det en levedyktig kortsiktig inokulering å forhåndsopprette den mens utbedring er planlagt.

Deteksjon på det tredje stadiet er et vanskeligere problem og verdt å være tydelig på. På grunnlag av den offentlige analysen blir den dekryptert og kartlagt i minnet i stedet for å skrives til disk, så filskanning og diskforensikk vil ikke se den. Det som fortsatt er observerbart er lasterens eget fotavtrykk – persistensoppføringene, mutex og utgående forespørsler til leveringsvertene eller DNS-TXT-domenene.

For miljøer som løser avhengigheter fra både et privat register og et offentlig register, forblir avbøtende tiltak uendret av noe her: interne navn på omfanget slik at et offentlig navn ikke kan tilfredsstille dem, og oppløsning festes til det interne registeret for disse omfangene. Versjonsinflasjonsatferden beskrevet ovenfor fungerer bare der en høyere offentlig versjon kan vinne et navn.

Til slutt er det verdt å lese nøye om avpubliseringen, snarere enn som en avslutning. Nitten navn forlot registeret på tjuefem sekunder, og ett kom tilbake en dag senere under en ny konto, med leveringsinfrastrukturen urørt gjennomgående. Fjerning av pakker er ikke fjerning av operasjonen; vertene fortsatte å betjene hvert trinn mens dette ble skrevet. Ethvert avhengighetstre som løste et av disse navnene mellom 31. juli og 1. august, bør behandles som om de hadde hatt muligheten til å utføre trinn én, enten navnet fortsatt eksisterer eller ikke.

Referanser

sca-tools-programvare-verktøy for komposisjonsanalyse
Prioriter, utbedre og sikre programvarerisikoene dine
Få din gratis konto.
Ingen kredittkort kreves.

Sikre programvareutviklingen og -leveringen din

med Xygeni-produktpakken