QuietPolyfill, um dropper NPM que ressuscitou

QuietPolyfill: Um dropper NPM que ressuscitou

TL, DR

Vinte pacotes npm publicados em dois dias, entre o final de julho e o início de agosto de 2026, se apresentam como bibliotecas internas de componentes de interface do usuário e utilitários de backend compartilhados. Nenhum deles implementa a funcionalidade anunciada. Cada um inclui uma classe stub, um arquivo README que descreve o pacote como interno e um arquivo chamado _polyfill.js.

O interessante é saber de onde esse arquivo é invocado. Não é um gancho de instalação — os manifestos não declaram nenhuma pré-instalação, instalação ou pós-instalação. Em vez disso, a última linha de index.js lê-se: try { require("./_polyfill"); } catch (_) {}O payload é executado na primeira vez que o código do aplicativo importa o pacote. Instalando com --ignore-scripts, standard Recomendações para reduzir a exposição da cadeia de suprimentos no momento da instalação. não o impede.

O quê _polyfill.js O processo consiste em recuperar um executável específico da plataforma de um dos três hosts do Cloudflare Workers, gravá-lo em um diretório temporário com um nome aleatório, marcá-lo como executável e executá-lo separadamente do processo pai. Esse executável é, em si, um carregador: ele enumera os Produtos de segurança de endpoint pelo nome do processo, verifica o nome de usuário atual em relação às contas conhecidas do ambiente de análise, aplica patches nas interfaces de telemetria AMSI e ETW na memória, instala a persistência com o nome de um componente de áudio do Windows e recupera um terceiro binário com vários megabytes. O terceiro estágio chega criptografado e é descriptografado e executado na memória em vez de gravado em disco..

Em 1º de agosto, aproximadamente quatro horas após a confirmação de que os pacotes eram maliciosos por meio de revisão manual, dezenove dos vinte nomes foram removidos do npm pelo próprio editor em uma sequência de vinte e cinco segundos. Em 2 de agosto, um desses nomes foi registrado novamente — por uma conta diferente, com o mesmo código. A infraestrutura de entrega atendia a todas as três etapas quando este texto foi escrito.

PACOTES20 nomes npm, ~74 versões publicadas
Temas de nomenclaturabeaver-ui-* (13) accounts-* (3), mais afisha-storybook-default, bcore-bravo-eslint-config, arbocrate-sla-prober-arbocrate-sla-prober-core, a.poltoradnev-package-c
GatilhoImportação de módulo (require), não um script de ciclo de vida do npm
Estágio 1_polyfill.js — recuperação e lançamento
Estágio 2Carregador vinculado estaticamente, compactado em UPX no Linux; enumeração de segurança de endpoint, aplicação de patches AMSI/ETW, persistência.
Estágio 3beacon_linux.bin / beacon_mac.bin / update_win.exe, 4–13 MB; entregue criptografado, executado na memória
DesafiosLinux x64 e arm64, macOS universal, Windows x64
Divulgadores Duas contas, uma base de código
Status19 de 20 nomes não publicados pelo operador; 1 republicado e ao vivo; infraestrutura de entrega ao vivo

Anatomia do ataque

Etapa 1 — o pacote

Cada membro envia o mesmo arquivo com formato de três linhas. index.js exporta uma classe com um iniciar() que define um valor booleano, um versão()e um configurar() que mescla um objeto de opções. Nada mais. README.md fornece uma linha de instalação, uma requerer linha e a frase “Pacote interno. Consulte CONTRIBUTING.md para obter diretrizes” — CONTRIBUINDO.md Não está no arquivo tar.

_polyfill.js Tem pouco menos de 5 KB e se lê como código de infraestrutura comum. Ele inicia com três verificações de ambiente:

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

Essas variáveis ​​são controles de privacidade convencionais. Aqui, elas controlam se um arquivo binário pode ser baixado e executado. Uma máquina que define NÃO_RASTREAR é completamente ignorado.

Em seguida, vem um carimbo de execução única: um arquivo em /tmp/.analytics_state, ou estado_analítico para % TEMP% No Windows, cujo tempo de modificação é comparado a uma janela de 20,357 segundos. Dentro dessa janela, o módulo retorna imediatamente, de modo que importações repetidas durante uma compilação produzem apenas uma tentativa de recuperação, em vez de dezenas.

Os hosts de destino não estão presentes como valores literais. Eles são montados em tempo de execução a partir de fragmentos:

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

Três hosts são construídos dessa forma e embaralhados antes do uso, portanto a ordem varia a cada execução. A recuperação é feita por meio de uma solicitação HTTPS GET vinculada ao IPv4 com um User-Agent of node-fetch/2.6 — um valor plausível para um processo Node emitir. Uma resposta com menos de 1,000 bytes é descartada e o próximo host é testado.

Se todas as três opções falharem, o módulo recorre ao DNS. Ele emite uma consulta TXT para c. para obter uma contagem de blocos, em seguida, solicita 0. através de n. Em lotes de dez, concatena as respostas e decodifica o resultado em base64. Este caminho carrega o mesmo executável por meio de registros TXT de DNS, que sobrevive em ambientes onde o HTTP de saída é filtrado, mas a resolução de DNS não.

Os bytes recuperados são gravados em /var/tmp/.cache_<8-hex> no Unix, ou %TEMP%\dotnet_diag_<8-hex>.exe No Windows, o nome do arquivo é interpretado como um artefato de diagnóstico do .NET. No Unix, o arquivo recebe a permissão 0755. O comando de inicialização é ovas com as separado: verdadeiro, stdio: “ignorar” e não referenciado(), portanto, o processo filho sobrevive ao processo Node que o iniciou; no Windows, a invocação adiciona windowsHide: true.

Dois detalhes completam o quadro. Um ajudante definido como const _l = (…a) => process.stderr.write(“”) é chamada em cada etapa da recuperação — ela escreve uma string vazia e descarta seus argumentos, ocupando a posição onde o registro de logs estaria. E cada pacote envia um arquivo de 81 KB em lib/telemetria.js cujo comentário no cabeçalho descreve um "SDK de Análise" com "transporte HTTPS com rotação e repetição de endpoints" e "descoberta de serviços baseada em DNS para failover". Na versão atual, nada o requer.

Etapa 2 — o carregador

O binário Linux x64 é um arquivo ELF estaticamente vinculado e compactado com UPX 3.96. A versão para macOS é um binário universal que abrange x86_64 e arm64; a versão para Windows é um executável de console PE32+ simplificado. Todos os quatro foram obtidos e examinados estaticamente; nenhum foi executado.

As importações e a tabela de strings da versão para Windows descrevem seu comportamento claramente. Ela resolve IsDebuggerPresent, CheckRemoteDebuggerPresent e Processo de Informação de Consulta Nte lê SISTEMA\ConjuntoDeControlesAtual\Serviços\Disco\Enum — um caminho de registro cujos valores identificam controladores de disco virtuais. Ele chama GetDiskFreeSpaceExW e StatusEx de Memória Global, o que é consistente com a verificação se os recursos do host correspondem a uma instância de análise de curta duração.

Contém uma lista de nomes de usuário: Usuário atual, Administrador de TI, Hong Lee, TESTBOX, hapubws, milozs, timmy, admin, malwares, amostra, vírus, meio AmbienteVários desses são artefatos de ambientes de análise documentados publicamente.

Também inclui uma lista de produtos de segurança de endpoints e seus respectivos nomes de processo, enumerados através de CreateToolhelp32Snapshot e Process32FirstW/Process32PróximoW:

  • Falcão CrowdStrike (csfalconservice, CSFalconContainer)
  • Negro de Carbono (cb.exe)
  • Cylance (cylancesvc.exe)
  • Bitdefender (bdagent.exe)
  • Kaspersky (avp.exe, kavfs.exe)
  • Trend Micro e Deep Security (coreServiceShell, ds_agent.exe)
  • FireEye/Trellix (xagt.exe)
  • Tânio (taniumclient.exe)
  • HitmanPro (hmpalert.exe)
  • Elástico (elastic-agent.exe, elastic-endpoint, filebeat.exe, winlogbeat.exe)
  • Wazuh e OSSEC (wazuh-agent.exe, ossec-agent.exe)
  • Check Point
  • Windows Defender

Estão presentes mais dois símbolos: AmsiScanBuffer, o ponto de entrada que o Windows expõe para a verificação de conteúdo na memória, e EtwEventWrite, a função através da qual o Event Tracing para Windows registra eventos. Ambos são alvos comuns de modificação em processo que suprime a telemetria que essas interfaces produziriam de outra forma.

A persistência aparece duas vezes no Windows. O binário é copiado para %APPDATA%\Microsoft\Windows\Themes\SystemSoundsService.exee dois mecanismos fazem referência a ele: um valor em SOFTWARE\Microsoft\Windows\VersãoAtual\Executare uma tarefa agendada registrada como

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

O caminho da tarefa, o nome da tarefa e o nome do executável são idênticos aos de um componente de áudio genuíno do Windows. /RL MAIS ALTO Solicita o nível de privilégio mais alto disponível.

A versão para macOS segue o mesmo padrão com nomes apropriados para a plataforma: ela grava uma lista de propriedades em ~/Library/LaunchAgents/com.apple.windowserver.helper.plist e coloca seu executável em ~/.local/share/runtime/com.apple.runtimeEle verifica se... /Aplicações/VMware Fusion.app e /Biblioteca/Suporte a Aplicativos/Ferramentas VMware.

A capacidade da rede abrange três meios de transporte: WININET.dll para HTTPS com um https://%s:%d%s string de formatação, WS2_32.dll soquetes e DNSAPI.dll com as DNSQuery_A ao lado de um c.%s.%s string de formato — o mesmo formato de consulta de contagem de blocos usado na primeira etapa.

A descrição acima corresponde ao que as importações, strings e caminhos de registro do binário mostram após inspeção. Uma análise pública com múltiplos mecanismos da mesma amostra do Windows adiciona detalhes que uma leitura estática de uma tabela de importações não consegue estabelecer por si só, e é atribuída aqui em vez de ser repetida como observação direta. Essa análise relata que as referências AMSI e ETW não são apenas resolvidas, mas corrigidas. AmsiScanBuffer in amsi.dll e EtwEventWrite in ntdll.dll — que suprime a varredura de conteúdo na memória e o rastreamento de eventos para o processo. Ele relata as verificações anti-análise como baseadas em tempo, além de CheckRemoteDebuggerPresente identifica o GetThreadContext A chamada funciona como uma verificação de ponto de interrupção de hardware, lendo os registros de depuração. Ela também reporta um mutex de instância única. Global\{7A3B9E1F-C2D4-4F8A-B5E6-D7A1F0C3E2B9}e nomes Serviço de sons do sistema como o valor da chave de execução. Esse valor de mutex não estava entre as strings recuperadas aqui — o filtro de extração usado excluiu tokens hexadecimais maiúsculos entre chaves — e está registrado na tabela de indicadores com base nessa análise pública, e não em observação direta.

Estágio 3

O carregador faz referência a um conjunto adicional de caminhos nos mesmos hosts. Todos estavam acessíveis durante a análise: beacon_linux.bin com 5.3 MB, beacon_mac.bin com 13.3 MB, atualizar_win.exe com 10.3 MB, e braço.bin com 4.3 MB. Estes não foram recuperados e seu comportamento é desconhecido.

Para o caminho do Windows, a análise pública do carregador descreve como esse terceiro estágio é tratado: ele é buscado via WinINet, chega criptografado e é descriptografado na memória e executado de forma reflexiva — memória alocada, importações resolvidas, realocações processadas e o controle passado para uma nova thread via Criar tópicoPor esse motivo, o terceiro estágio nunca é gravado no sistema de arquivos, portanto, a detecção baseada em arquivos e a análise forense de disco pós-incidente não têm nada a recuperar a partir dele. Isso também significa que a recuperação dos bytes do host de entrega resultaria em texto cifrado, e não em um executável analisável.

 

Timeline

Todos os registros de data e hora estão em UTC e provêm dos metadados do registro npm, que registra a criação e a remoção de pacotes independentemente de qualquer observador.

Ao O quê
2026-07-31 09:25:05 → 09:29:56 Foram criados dez nomes, cada um começando na versão. 0.0.1, com aproximadamente treze segundos de intervalo.
2026-08-01 09:38 → 11:29 Os nomes existentes sobem através de números de versão inflacionados, 6.1.9 através de 6.1.12, 9.9.9 através de 9.9.12, 8.9.3 através de 8.9.6, 5.4.6 através de 5.4.9.
2026-08-01 13:04:14 → 13:04:18 Nove outros beaver-ui-* Nomes criados em uma sequência de quatro segundos, cada um publicado diretamente em uma versão 12.x sem histórico anterior.
2026-08-01 ~13:00–14:30 Versões classificadas como maliciosas após análise manual.
2026-08-01 17:39:49 → 17:40:14 Dezenove dos vinte nomes não foram publicados em vinte e cinco segundos, sequencialmente, com cerca de 1.2 segundos de intervalo entre eles.
2026-08-02 18:58:41 a.poltoradnev-package-c criado novamente sob uma conta de manutenção diferente, versão 0.0.2.
2026-08-02 19:20:28 Versão 33.9.5 Publicado com o mesmo nome.
2026-08-02 20:14 Todos os três servidores de entrega atendem aos estágios dois e três; resposta de fallback de DNS com contagens de blocos.

Duas dessas entradas merecem ser analisadas com mais atenção.

A sequência de 13:04 colocou nove pacotes no registro em quatro segundos, cada um com uma única versão na casa dos doze e nenhuma outra. 0.0.1 abaixo. Números de versão nessa escala, com nomes de um dia atrás, não descrevem um histórico de lançamentos.

A sequência de despublicação é igualmente mecânica: dezenove nomes, vinte e cinco segundos, espaçamento uniforme. O npm registra isso como eventos comuns de despublicação, em vez do marcador deixado por uma remoção iniciada pelo registro, que atribui a ação à conta de publicação em vez do registro.

 

Indicadores de compromisso

Os hosts e caminhos foram desativados. Não os resolva nem os recupere de um sistema de produção.

Formato Valor
Anfitrião de entrega oob-worker[.]cf99-9b3[.]workers[.]dev
Anfitrião de entrega oob-worker[.]cf102-baf[.]workers[.]dev
Anfitrião de entrega oob-worker[.]cf103-070[.]workers[.]dev
Transporte DNS tin[.]dl[.]well1[.]site (Linux x64), tina[.] (arm64), ldr[.] (Mac OS), win[.] (Windows) — contagem de blocos em c.<domain>, dados em <n>.<domain>
Caminhos da Etapa 2 /pkg/package, /pkg/package-arm64, /pkg/loader_mac, /pkg/package.exe
Caminhos da Etapa 3 /pkg/beacon_linux.bin, /pkg/beacon_mac.bin, /pkg/update_win.exe, /pkg/arm.bin
Caminho de arrastar (Unix) /var/tmp/.cache_<8-hex>
Arrastar caminho (Windows) %TEMP%\dotnet_diag_<8-hex>.exe
Persistência (Windows) %APPDATA%\Microsoft\Windows\Themes\SystemSoundsService.exe; tarefa \Microsoft\Windows\Multimedia\SystemSoundsService; Valor da chave de execução SOFTWARE\Microsoft\Windows\CurrentVersion\Run\SystemSoundsService
Mutex (Windows) Global\{7A3B9E1F-C2D4-4F8A-B5E6-D7A1F0C3E2B9}
Persistência (macOS) ~/Library/LaunchAgents/com.apple.windowserver.helper.plist; binário em ~/.local/share/runtime/com.apple.runtime
Carimbo de uso único /tmp/.analytics_state, %TEMP%\analytics_state (janela de 20,357 s)
Solicitar assinatura HTTPS GET, IPv4 fixado, User-Agent: node-fetch/2.6
Arquivo de Estágio 1 _polyfill.js, SHA-256 392dc9ec85042ce1796bfa9b8c7bf6e05c7710579aab8b7b1eb73788683883af
Arquivo sem referência lib/telemetry.js, SHA-256 2266c9b8e199daebe31a9afcb61d9b0d76706b63c92312fc8223d22de42e63c7
Etapa 2 (Linux x64) SHA-256 7e486657f30594afda379b97030252a09a19fe8055e25c9e371544f59bd8e9e3
Etapa 2 (Linux arm64) SHA-256 aafc37589c0d21fb729761a1579ca3b278b95bce152aacfa29243ffd27f5eeb7
Etapa 2 (macOS) SHA-256 c214746c74cae8ece8bdaf69aa05da4db6ce013f9e77452d1eed1a002fd9ba00
Etapa 2 (Windows) SHA-256 d632c2c0c7801c3da57cf849fe4b4ca8351d9a7fe73c31050bf5dbcc96a16791

Nomes de pacotes

beaver-ui-drawer botão-de-ações-da-interface-do-beaver cartão-ui-grande beaver-ui-date-range-picker formulário beaver-ui beaver-ui-form-modal objeto de formulário beaver-ui grade beaver-ui cabeçalho-ui-do-beaver beaver-ui-hooks ícone-ui-beaver-lock beaver-ui-itens-com-mais layout-ui-beaver formulário-final-de-contas estado de carregamento de contas conta-cronologia afisha-storybook-padrão bcore-bravo-eslint-config arbocrate-sla-prober-arbocrate-sla-prober-core a.poltoradnev-package-c

Atribuição e comportamento observado

Duas contas do npm estão envolvidas. Os dezenove nomes não publicados foram publicados por uma das contas; a.poltoradnev-package-c O registro foi refeito em 2 de agosto com um nome de conta e um domínio de e-mail diferentes. O código é da mesma linhagem em ambos os casos, e o segundo registro possui os mesmos servidores de entrega, os mesmos domínios de fallback de DNS, a mesma construção de drop-path e o mesmo carimbo de execução única.

A edição republicada também mostra como foi montada. Versões. 0.0.2 e 33.9.5 são idênticos em nível de byte, exceto pela string de versão em package.json, e a index.js enviado como 0.0.2 declara const VERSION = “33.9.5”Ambos os artefatos foram cortados de uma mesma árvore e numerados posteriormente. Não há 0.0.2 histórico de desenvolvimento para o 0.0.2 liberação para representar.

A numeração das versões em todo o conjunto segue a mesma lógica. Os nomes dos pacotes nesta campanha são interpretados como elementos que existiriam dentro de uma organização, e não em um registro público: um conjunto de componentes de interface do usuário, uma configuração do ESLint, um verificador de SLA, um pacote com o nome de uma pessoa. Quando uma compilação resolve uma dependência pelo nome e consulta um registro público juntamente com um privado, o número de versão mais alto é o que prevalece. Um nome criado em 0.0.1 na segunda-feira e carregando 12.9.6 Na terça-feira, estará em posição para essa comparação.

As duas ondas de publicação também diferem de uma forma que sugere iteração. O código anterior nomeou seu arquivo de carga útil. configuração.js e listou cinco servidores de entrega; o arquivo atual é nomeado _polyfill.js e lista três. A versão anterior removia o arquivo descartado cinco segundos após a inicialização, deixando o processo em execução a partir de um inode excluído; a versão atual não faz isso. O não referenciado lib/telemetria.js permanece no pacote de alcatrão em ambos os lados, ainda transportado, não mais conectado.

Uma observação sobre os interruptores de segurança merece ser mencionada antes de prosseguirmos.cisSim, porque contraria uma leitura comum. DESATIVAR_TELEMETRIA, DESATIVAÇÃO DE ANÁLISES e NÃO_RASTREAR são respeitadas e verificadas antes de qualquer outra coisa. Um pesquisador cujo ambiente de análise define qualquer uma delas observa um pacote que não faz absolutamente nada.

Impacto e o que os defensores podem fazer

A consequência prática do gatilho no momento da importação é restrita e merece ser mencionada separadamente. npm install --ignore-scripts Impede a execução de scripts de ciclo de vida, e é uma recomendação sensata. Não tem efeito aqui, porque este código não usa um script de ciclo de vida. O payload é executado quando o código do aplicativo, uma execução de teste, um bundler ou um linter importa o módulo pela primeira vez. Em um executor de CI que instala e compila imediatamente, os dois eventos ocorrem com segundos de diferença.

Para fins de busca, os sinais duradouros são estruturais, e não literais. Os hosts, os nomes dos arquivos e os caminhos de destino mudam; essas formas são mais difíceis de alterar:

  • Um ponto de entrada de módulo cuja declaração final é um tente { require(“./x”); } catch (_) {} em torno de um arquivo que não faz parte da API exportada. Um pacote com um polyfill genuíno o importa no início do arquivo e utiliza seu resultado. Ignorar todos os erros de uma importação feita no final do arquivo não é tratamento de erros.
  • Cadeias de host montadas por .juntar("") a partir de uma série de fragmentos. Um código legítimo não tem motivo para dividir um nome de host entre elementos de um array.
  • Um pacote cujas dependências declaradas estão vazias, mas que tenta obter https, dns e processo_filho no momento da importação. A descoberta principal consiste na recuperação e execução, dentro de um pacote que anuncia um componente de interface do usuário.
  • Números de versão inconsistentes com a idade do pacote. Um nome publicado pela primeira vez há poucos dias em uma versão principal de dois dígitos, sem lançamentos intermediários, descreve algo diferente de seu desenvolvimento.
  • Escreve para / var / tmp or % TEMP% seguido por um chmod até 07:55 e um spawn separado. A sequência é curta, ordenada e raramente benigna em conjunto.

Em hosts Windows, existem duas verificações baratas e de alta confiabilidade. Os artefatos de persistência são strings fixas e nenhum deles pertence a um sistema limpo: uma tarefa agendada em \Microsoft\Windows\Multimedia\SystemSoundsService, um valor de chave de execução chamado Serviço de sons do sistemae um executável com esse nome em %APPDATA%\Microsoft\Windows\Temas\O serviço de áudio genuíno do Windows não reside em nenhum desses locais. Em segundo lugar, o carregador usa um mutex fixo de instância única. Global\{7A3B9E1F-C2D4-4F8A-B5E6-D7A1F0C3E2B9}A sua presença num host é um indicador e, como o carregador trata um mutex existente como "já em execução", a sua pré-criação é uma medida de segurança viável a curto prazo enquanto se aguarda a resolução do problema.

A detecção na terceira etapa é um problema mais complexo e merece clareza. De acordo com análises públicas, os dados são descriptografados e mapeados na memória, em vez de gravados em disco, portanto, varreduras de arquivos e análises forenses de disco não os detectarão. O que permanece observável é o rastro do próprio carregador — as entradas de persistência, o mutex e as requisições de saída para os hosts de entrega ou os domínios DNS-TXT.

Para ambientes que resolvem dependências tanto de um registro privado quanto de um público, a mitigação permanece inalterada por qualquer coisa aqui descrita: defina o escopo dos nomes internos para que um nome público não possa satisfazê-los e fixe a resolução ao registro interno para esses escopos. O comportamento de inflação de versão descrito acima só funciona onde uma versão pública mais recente pode obter um nome.

Por fim, vale a pena ler atentamente a despublicação, e não como um ponto final. Dezenove nomes deixaram o registro em vinte e cinco segundos e um retornou um dia depois sob uma nova conta, com a infraestrutura de entrega intacta durante todo o processo. A remoção de pacotes não significa a remoção da operação; os servidores continuaram atendendo a todas as etapas enquanto este texto era escrito. Qualquer árvore de dependências que tenha resolvido um desses nomes entre 31 de julho e 1º de agosto deve ser considerada como tendo tido a oportunidade de executar a primeira etapa, independentemente de o nome ainda existir ou não.

Referências

sca-tools-software-composição-análise-ferramentas
Priorize, corrija e proteja seus riscos de software
Crie sua conta gratuita.
Nenhum cartão de crédito é necessário.

Proteja seu desenvolvimento e entrega de software

com o Suíte de Produtos da Xygeni