QuietPolyfill, en NPM-dropper som återuppstod

QuietPolyfill: En NPM-dropper som återuppstod

TL; DR

Tjugo npm-paket som publicerades under två dagar i slutet av juli och början av augusti 2026 presenterar sig som interna UI-komponentbibliotek och delade backend-verktyg. Inget av dem implementerar den funktionalitet de annonserar. Varje paket levererar en stub-klass, ett README-dokument som beskriver paketet som internt och en fil som heter _polyfill.js.

Det intressanta är varifrån filen anropas. Det är inte en installationshook — manifesten deklarerar ingen förinstallation, installation eller efterinstallation alls. Istället den sista raden av index.js lyder: try { require("./_polyfill"); } catch (_) {}Nyttolasten körs första gången programkoden importerar paketet. Installera med --ignore-scripts, den standard råd för att minska exponeringen i leveranskedjan vid installationstillfället, hindrar det inte.

Vad _polyfill.js Det som krävs är att hämta en plattformsspecifik körbar fil från en av tre Cloudflare Workers-värdar, skriva den till en tillfällig katalog under ett slumpmässigt namn, markera den som körbar och starta den fristående från föräldraprocessen. Den körbara filen är i sig en laddare: den räknar upp slutpunktssäkerhetsprodukter efter processnamn, kontrollerar det aktuella användarnamnet mot kända analysmiljökonton, patchar AMSI- och ETW-telemetrigränssnitten i minnet, installerar persistens under namnet på en Windows-ljudkomponent och hämtar en tredje binärfil som är flera megabyte stor. Det tredje steget anländer krypterat och är dekrypteras och körs i minnet snarare än skrivs till disk.

Den 1 augusti, ungefär fyra timmar efter att paketen bekräftats vara skadliga vid manuell granskning, togs nitton av de tjugo namnen bort från npm av deras egen utgivare i en tjugofemsekunderssekvens. Den 2 augusti registrerades ett av dessa namn igen – av ett annat konto, med samma kod. Leveransinfrastrukturen betjänade alla tre steg när detta skrevs.

Paket20 npm-namn, ~74 publicerade versioner
Namnge temanbeaver-ui-* (13), accounts-* (3), plus afisha-storybook-default, bcore-bravo-eslint-config, arbocrate-sla-prober-arbocrate-sla-prober-core, a.poltoradnev-package-c
TriggerModulimport (require), inte ett npm-livscykelskript
Steg 1_polyfill.js — hämtning och sjösättning
Steg 2Statiskt länkad laddare, UPX-packad på Linux; uppräkning av slutpunktssäkerhet, AMSI/ETW-patchning, persistens
Steg 3beacon_linux.bin / beacon_mac.bin / update_win.exe, 4–13 MB; levereras krypterad, exekverad i minnet
PlattformarLinux x64 och arm64, macOS universal, Windows x64
Utgivare Två konton, en kodbas
Status19 av 20 namn opublicerade av operatören; 1 publicerad på nytt och live; leveransinfrastruktur live

Attackanatomi

Steg 1 — paketet

Varje medlem levererar samma form med tre filer. index.js exporterar en klass med en i det() som sätter en booleskt värde, en version()Och en konfigurera() som sammanfogar ett optionsobjekt. Inget annat. README.md ger en installationsrad, en kräver rad och meningen ”Internt paket. Se CONTRIBUTING.md för riktlinjer” — BIDRAGANDE.md finns inte i tarballen.

_polyfill.js är strax under 5 KB och läses som vanlig infrastrukturkod. Den öppnas med tre miljökontroller:

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

Dessa variabler är konventionella integritetskontroller. Här styr de om en binärfil laddas ner och körs. En maskin som ställer in SPÅRA_INTE hoppas över helt och hållet.

Nästa kommer en stämpel som körs en gång: en fil på /tmp/.analytics_state, eller analysstatus under % TEMP% på Windows, vars modifieringstid jämförs med ett fönster på 20 357 sekunder. Inom det fönstret returnerar modulen omedelbart, så upprepade importer under en build producerar ett hämtningsförsök snarare än dussintals.

Destinationsvärdarna finns inte som literaler. De sätts ihop vid körning från fragment:

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

Tre värdar byggs på detta sätt och blandas före användning, så ordningen varierar per körning. Hämtning är en HTTPS GET-funktion fäst till IPv4 med en User-Agent of nod-hämta/2.6 — ett rimligt värde för en Node-process att generera. Ett svar under 1 000 byte ignoreras och nästa värd försöks.

Om alla tre misslyckas återgår modulen till DNS. Den utfärdar en TXT-fråga för c. för att få ett stycken antal, begär sedan 0. dig genom n. i omgångar om tio, sammanfogar svaren och avkodar resultatet med base64. Denna sökväg bär samma körbara fil över DNS TXT-poster, vilket överlever miljöer där utgående HTTP filtreras men DNS-upplösning inte.

De hämtade byten skrivs till /var/tmp/.cache_<8-hex> på Unix, eller %TEMP%\dotnet_diag_<8-hex>.exe på Windows — ett filnamn som läses som en .NET-diagnostikartefakt. På Unix ges filen läge 0755. Start är rom med fristående: sant, stdio: "ignorera"och unref(), så barnet överlever Node-processen som startade det; i Windows lägger anropet till fönsterDölj: sant.

Två detaljer kompletterar bilden. En hjälpare definierad som const _l = (…a) => process.stderr.write(“”) anropas vid varje steg i hämtningen — den skriver en tom sträng och ignorerar dess argument, och upptar positionen där loggningen skulle sitta. Och varje paket skickar en 81 KB-fil vid lib/telemetri.js vars rubrikkommentar beskriver ett "Analytics SDK" med "HTTPS-transport med slutpunktsrotation och återförsök" och "DNS-baserad tjänstidentifiering för redundans". I den aktuella versionen krävs det inte något.

Steg 2 — lastaren

Linux x64-binärfilen är en statiskt länkad ELF komprimerad med UPX 3.96. MacOS-versionen är en universell binärfil som täcker x86_64 och arm64; Windows-versionen är en avskalad PE32+-konsolkörbar fil. Alla fyra hämtades och undersöktes statiskt; ingen kördes.

Windows-byggets importer och strängtabell beskriver dess beteende tydligt. Den löser IsDebuggerPresent, CheckRemoteDebuggerPresentoch NtQueryInformationProcessoch läser SYSTEM\CurrentControlSet\Tjänster\Disk\Enum — en registersökväg vars värden identifierar virtuella diskkontroller. Den anropar GetDiskFreeSpaceExW och GlobalMemoryStatusEx, i linje med att kontrollera om värdens resurser matchar en kortlivad analysinstans.

Den innehåller en lista med användarnamn: Nuvarande användare, IT-administratör, Hong Lee, TESTBOX, hapubws, milozs, Timmy, administration, malware, prov, virus, miljöFlera av dessa är artefakter från offentligt dokumenterade analysmiljöer.

Den innehåller också en lista över produkter för slutpunktssäkerhet och deras processnamn, uppräknade genom CreateToolhelp32Snapshot och Process32FirstW/Process32NextW:

  • CrowdStrike Falcon (csfalconservice, CSFalconContainer)
  • Kimrök (cb.exe)
  • Cylance (cylancesvc.exe)
  • Bitdefender (bdagent.exe)
  • kaspersky (avp.exe, kavfs.exe)
  • Trend Micro och djupgående säkerhet (coreServiceShell, ds_agent.exe)
  • FireEye/Spaljé (xagt.exe)
  • Tanium (taniumclient.exe)
  • HitmanPro (hmpalert.exe)
  • Elastisk (elastic-agent.exe, elastic-endpoint, filebeat.exe, winlogbeat.exe)
  • Wazuh och OSSEC (wazuh-agent.exe, ossec-agent.exe)
  • Check Point
  • Windows Defender

Två ytterligare symboler finns: AmsiScanBuffer, startpunkten som Windows exponerar för skanning av innehåll i minnet, och EtwEventWrite, funktionen genom vilken Event Tracing för Windows registrerar händelser. Båda är vanliga mål för modifieringar i processen som undertrycker den telemetri som dessa gränssnitt annars skulle producera.

Persistens visas två gånger i Windows. Binärfilen kopieras till %APPDATA%\Microsoft\Windows\Themes\SystemSoundsService.exe, och två mekanismer refererar till det: ett värde under PROGRAMVARA\Microsoft\Windows\Aktuell version\Köroch en schemalagd uppgift registrerad som

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

Sökvägen, uppgiftsnamnet och namnet på den körbara filen speglar alla en äkta Windows-ljudkomponent. /RL HÖGSTA begär den högsta tillgängliga behörighetsnivån.

macOS-versionen följer samma mönster med plattformsanpassade namn: den skriver en egenskapslista till ~/Bibliotek/LaunchAgents/com.apple.windowserver.helper.plist och placerar dess körbara kod på ~/.local/share/runtime/com.apple.runtimeDen kontrollerar efter /Program/VMware Fusion.app och /Bibliotek/Programsupport/VMware-verktyg.

Nätverkskapaciteten omfattar tre transporter: WININET.dll för HTTPS med en https://%s:%d%s formatera sträng, WS2_32.dll uttag, och DNSAPI.dll med DnsQuery_A vid sidan av a c.%s.%s formatsträng — samma chunk-count-frågeform som det första steget använder.

Beskrivningen ovan visar vad binärfilens importer, strängar och registersökvägar visar vid inspektion. Publik flermotoranalys av samma Windows-exempel lägger till detaljer som en statisk läsning av en importtabell inte kan fastställa på egen hand, och tillskrivs här snarare än omarbetas som förstahandsobservation. Den analysen rapporterar att AMSI- och ETW-referenserna inte bara är lösta utan korrigerade — AmsiScanBuffer in amsi.dll och EtwEventWrite in ntdll.dll — vilket undertrycker skanning av innehåll i minnet och händelsespårning för processen. Den rapporterar antianalyskontrollerna som tidsbaserade utöver CheckRemoteDebuggerPresent, och identifierar GetThreadContext anrop som en hårdvarubrytpunktskontroll som läser felsökningsregistren. Den rapporterar också en mutex för en enda instans, Global\{7A3B9E1F-C2D4-4F8A-B5E6-D7A1F0C3E2B9}och namn Systemljudtjänst som Run-key-värdet. Det mutex-värdet fanns inte bland de strängar som återfanns här – extraktionsfiltret som användes exkluderade spetsade hexagonala versaler – och det registreras i indikatortabellen baserat på den offentliga analysen snarare än direkt observation.

Steg 3

Laddaren refererar till ytterligare en uppsättning sökvägar på samma värdar. Alla var nåbara under analysen: beacon_linux.bin vid 5.3 MB, beacon_mac.bin vid 13.3 MB, update_win.exe vid 10.3 MB, och arm.bin vid 4.3 MB. Dessa hämtades inte, och deras beteende är okänt.

För Windows-sökvägen beskriver den offentliga analysen av laddaren hur det tredje steget hanteras: det hämtas via WinINet, anländer krypterat och dekrypteras i minnet och exekveras reflektivt – minne allokeras, importer löses, omplaceringar bearbetas och kontrollen skickas till en ny tråd via Skapa trådAv den anledningen skrivs det tredje steget aldrig till filsystemet, så filbaserad detektering och diskforensik efter incidenter har inget att återställa från det. Det innebär också att hämtning av byten från leveransvärden skulle ge chiffertext, inte en analyserbar körbar fil.

 

tidslinje

Alla tidsstämplar är UTC och kommer från npm-registermetadata, som registrerar skapande och avpublicering av paket oberoende av någon observatör.

NärVad
2026-07-31 09:25:05 → 09:29:56Tio namn skapade, alla med början på version 0.0.1, ungefär tretton sekunders mellanrum.
2026-08-01 09:38 → 11:29Befintliga namn klättrar genom uppblåsta versionsnummer, 6.1.9 dig genom 6.1.12, 9.9.9 dig genom 9.9.12, 8.9.3 dig genom 8.9.6, 5.4.6 dig genom 5.4.9.
2026-08-01 13:04:14 → 13:04:18Nio ytterligare beaver-ui-* namn skapade i en fyra sekunders period, vart och ett publicerat direkt i en 12.x-version utan tidigare historik.
2026-08-01 ~13:00–14:30Versioner klassificerade som skadliga vid manuell granskning.
2026-08-01 17:39:49 → 17:40:14Nitton av de tjugo namnen publicerades inte inom loppet av tjugofem sekunder, i tur och ordning, med cirka 1.2 sekunders mellanrum.
2026-08-02 18:58:41a.poltoradnev-package-c skapad igen under ett annat underhållskonto, version 0.0.2.
2026-08-02 19:20:28Version 33.9.5 publicerad med samma namn.
2026-08-02 20:14Alla tre leveransvärdar som hanterar steg två och tre; DNS-reserv svarar med chunk-antal.

Två av dessa inlägg är värda att uppehålla sig vid.

13:04-utbrottet lade in nio paket i registret på fyra sekunder, vart och ett med en enda version i tolvtalen och ingen 0.0.1 under den. Versionsnummer på den här skalan, på namn som är en dag gamla, beskriver inte en utgivningshistorik.

Avpubliceringssekvensen är lika mekanisk: nitton namn, tjugofem sekunder, jämnt mellanrum. npm registrerar dessa som vanliga avpubliceringshändelser snarare än den platshållare som en registerinitierad borttagning lämnar efter sig, vilket placerar åtgärden hos publiceringskontot snarare än hos registret.

 

Indikatorer för kompromiss

Värdar och sökvägar är defangerade. Lös inte upp eller hämta dem från ett produktionssystem.

TypVärderar
Leveransvärdoob-worker[.]cf99-9b3[.]workers[.]dev
Leveransvärdoob-worker[.]cf102-baf[.]workers[.]dev
Leveransvärdoob-worker[.]cf103-070[.]workers[.]dev
DNS-transporttin[.]dl[.]well1[.]site (Linux x64), tina[.] (arm64), ldr[.] (Mac OS), win[.] (Windows) — antalet bitar vid c.<domain>, data på <n>.<domain>
Steg 2-vägar/pkg/package, /pkg/package-arm64, /pkg/loader_mac, /pkg/package.exe
Steg 3-vägar/pkg/beacon_linux.bin, /pkg/beacon_mac.bin, /pkg/update_win.exe, /pkg/arm.bin
Släppsökväg (Unix)/var/tmp/.cache_<8-hex>
Släppbana (Windows)%TEMP%\dotnet_diag_<8-hex>.exe
Persistens (Windows)%APPDATA%\Microsoft\Windows\Themes\SystemSoundsService.exeuppgift \Microsoft\Windows\Multimedia\SystemSoundsService; Környckelvärde 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 vid ~/.local/share/runtime/com.apple.runtime
Kör en gång-stämpel/tmp/.analytics_state, %TEMP%\analytics_state (20 357 sekunders fönster)
Begär underskriftHTTPS GET, IPv4-fästad, användaragent: node-fetch/2.6
Steg 1-fil_polyfill.js, SHA-256 392dc9ec85042ce1796bfa9b8c7bf6e05c7710579aab8b7b1eb73788683883af
Fil utan referanslib/telemetry.js, SHA-256 2266c9b8e199daebe31a9afcb61d9b0d76706b63c92312fc8223d22de42e63c7
Steg 2 (Linux x64)SHA-256 7e486657f30594afda379b97030252a09a19fe8055e25c9e371544f59bd8e9e3
Steg 2 (Linux arm64)SHA-256 aafc37589c0d21fb729761a1579ca3b278b95bce152aacfa29243ffd27f5eeb7
Steg 2 (macOS)SHA-256 c214746c74cae8ece8bdaf69aa05da4db6ce013f9e77452d1eed1a002fd9ba00
Steg 2 (Windows)SHA-256 d632c2c0c7801c3da57cf849fe4b4ca8351d9a7fe73c31050bf5dbcc96a16791

Paketnamn

beaver-ui-låda beaver-ui-actions-knapp beaver-ui-kort-stort beaver-ui-datumintervallväljare bäver-ui-formulär beaver-ui-form-modal beaver-ui-form-objekt beaver-ui-grid beaver-ui-header bäver-ui-hooks beaver-ui-ikonlås beaver-ui-föremål-med-mer beaver-ui-layout slutgiltigt räkenskapsformulär konton-laddningsstatus konton-tidslinje afisha-storybook-standard bcore-bravo-eslint-konfiguration arbocrate-sla-prober-arbocrate-sla-prober-core a.poltoradnev-paket-c

Attribuering och observerat beteende

Två npm-konton är inblandade. De nitton opublicerade namnen publicerades av ett konto; a.poltoradnev-paket-c omregistrerades den 2 augusti under ett annat kontonamn och en annan e-postdomän. Koden är samma härkomst i båda, och den andra registreringen har samma leveransvärdar, samma DNS-reservdomäner, samma drop-path-konstruktion och samma "kör en gång"-stämpel.

Det återpublicerade paret visar också hur det sattes ihop. 0.0.2 och 33.9.5 är byte-identiska förutom versionssträngen i package.json, Och den index.js skickas som 0.0.2 deklarerar konstant VERSION = “33.9.5”Båda artefakterna skars ut från ett källträd och numrerades efteråt. Det finns ingen 0.0.2 utvecklingshistorik för den 0.0.2 släpp att representera.

Versionsnumreringen för hela uppsättningen följer samma logik. Paketnamn i den här kampanjen läses som saker som skulle finnas inom en organisation snarare än i ett offentligt register: en UI-komponentuppsättning, en ESLint-konfiguration, en SLA-prober, ett paket uppkallat efter en person. När en build löser ett beroende med namn och konsulterar ett offentligt register tillsammans med ett privat, är det det högre versionsnumret som vinner. Ett namn som skapas vid 0.0.1 på måndag och bär 12.9.6 på tisdag är positionerad för den jämförelsen.

De två publikationsvågorna skiljer sig också åt på ett sätt som tyder på iteration. Den tidigare koden gav dess nyttolastfil namn. setup.js och listade fem leveransvärdar; den aktuella filen heter _polyfill.js och listar tre. Den tidigare versionen tog bort den tappade filen fem sekunder efter starten, vilket lämnade processen körandes från en borttagen inode; den nuvarande versionen gör det inte. Den orefererade lib/telemetri.js finns kvar i tarballen för båda, fortfarande levererad, inte längre inkopplad.

En observation om kill switcharna är värd att nämna i förväg.cisely, eftersom det strider mot en vanlig tolkning. DISABLE_TELEMETRY, ANALYSERA_AVSTÄLLNINGoch SPÅRA_INTE respekteras och de kontrolleras innan något annat händer. En forskare vars analysmiljö sätter någon av dem observerar ett paket som inte gör någonting alls.

Påverkan och vad försvarare kan göra

Den praktiska konsekvensen av importtidsutlösaren är smal och värd att ange i sig själv. npm-installation –ignore-scripts förhindrar att livscykelskript körs, och det är ett bra råd. Det har ingen effekt här, eftersom den här koden inte använder ett livscykelskript. Nyttolasten körs när applikationskod, en testkörning, en bundler eller en linter först importerar modulen. På en CI-körning som installeras och sedan omedelbart bygger, är de två sekunder ifrån varandra.

För jakt är de varaktiga signalerna strukturella snarare än bokstavliga. Värdarna, filnamnet och släppvägarna kommer att ändras; dessa former är svårare att variera:

  • En modulstartpunkt vars slutsats är en försök { require("./x"); } catch (_) {} runt en fil som inte är en del av det exporterade API:et. Ett paket med en äkta polyfill importerar den högst upp och använder dess resultat. Att svälja alla fel från en import längst ner i filen är inte felhantering.
  • Värdsträngar sammansatta av .ansluta sig till("") från en uppsättning fragment. Legitim kod har ingen anledning att dela upp ett värdnamn över arrayelement.
  • Ett paket vars deklarerade beroenden är tomma men som sträcker sig efter https, dnsoch barnprocess vid importtillfället. Hämtning plus exekvering i ett paket som annonserar en UI-komponent är hela fyndet.
  • Versionsnummer är inte i överensstämmelse med ett pakets ålder. Ett namn som först publicerades för några dagar sedan i en tvåsiffrig större version, utan mellanliggande utgåvor, beskriver något annat än dess utveckling.
  • Skriver till / Var / tmp or % TEMP% följt av en chmod till 0755 och en fristående spawn. Sekvensen är kort, ordnad och sällan godartad tillsammans.

På Windows-värdar finns det två billiga kontroller med hög tillförlitlighet. Persistensartefakterna är fasta strängar och ingen av dem hör hemma på ett rent system: en schemalagd uppgift på \Microsoft\Windows\Multimedia\Systemljudtjänst, ett Run-key-värde med namnet Systemljudtjänst, och en körbar fil med samma namn under %APPDATA%\Microsoft\Windows\Teman\Den äkta Windows-ljudtjänsten finns inte på någon av dessa platser. För det andra använder laddaren en fast mutex för en enda instans, Global\{7A3B9E1F-C2D4-4F8A-B5E6-D7A1F0C3E2B9}; dess närvaro på en värd är en indikator, och eftersom laddaren behandlar en befintlig mutex som "redan igång" är det en gångbar kortsiktig inokulering att förskapa den medan sanering är schemalagd.

Detektion i det tredje steget är ett svårare problem och värt att vara tydlig med. Enligt den redogörelse som ges av den offentliga analysen dekrypteras och mappas den i minnet snarare än skrivs till disk, så filskanning och diskforensik kommer inte att se den. Det som förblir observerbart är laddarens eget fotavtryck - persistensposterna, mutex och utgående förfrågningar till leveransvärdarna eller DNS-TXT-domänerna.

För miljöer som löser beroenden från både ett privat register och ett publikt register, ändras begränsningen inte av något här: interna namn för omfånget så att ett publikt namn inte kan uppfylla dem, och upplösningen fästs i det interna registret för dessa omfång. Versionsuppblåsningsbeteendet som beskrivs ovan fungerar bara där en högre publik version kan vinna ett namn.

Slutligen är avpubliceringen värd att läsa noggrant snarare än som ett slut. Nitton namn lämnade registret på tjugofem sekunder och ett återkom en dag senare under ett nytt konto, med leveransinfrastrukturen orörd rakt igenom. Borttagning av paket är inte borttagning av operationen; värdarna fortsatte att betjäna varje steg medan detta skrevs. Alla beroendeträd som löste ett av dessa namn mellan 31 juli och 1 augusti bör behandlas som att de haft möjlighet att genomföra steg ett, oavsett om namnet fortfarande existerar eller inte.

Referensprojekt

sca-tools-programvara-verktyg-för-kompositionsanalys
Prioritera, åtgärda och säkra dina programvarurisker
Skaffa ditt gratiskonto.
Inga kreditkort krävs.

Säkra din programvaruutveckling och leverans

med Xygeni-produktsviten