├── .gitignore ├── LICENSE ├── README.md ├── assembler.py ├── elf_to_shellcode.py ├── elf_to_stelf.py ├── test_elfs ├── Makefile ├── glibc-static-pie.c ├── glibc-static.c ├── linux_syscall_support.h ├── rust_helloworld.rs └── syscall-static-pie.c └── testall.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # test artefacts 132 | out.sh 133 | 134 | # build artefacts 135 | test_elfs/glibc-static 136 | test_elfs/glibc-static-pie 137 | test_elfs/rust_helloworld 138 | test_elfs/syscall-static-pie -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 David Buchanan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | __ __ __ _ _ __ _ _ __ __ __ 3 | \ \ \ \ \ \ ___| |_ ___| |/ _| | | ___ __ _ __| | ___ _ __ / / / / / / 4 | \ \ \ \ \ \ / __| __/ _ \ | |_ | |/ _ \ / _` |/ _` |/ _ \ '__| / / / / / / 5 | / / / / / / \__ \ || __/ | _| | | (_) | (_| | (_| | __/ | \ \ \ \ \ \ 6 | /_/ /_/ /_/ |___/\__\___|_|_| |_|\___/ \__,_|\__,_|\___|_| \_\ \_\ \_\ 7 | ``` 8 | A stealthy ELF loader - no files, no execve, no RWX (currently only supporting x86_64) 9 | 10 | See also: [arget13/DDexec](https://github.com/arget13/DDexec), which is a similar idea to this but probably more flexible - I wrote most of `stelf-loader` before I realised that `DDexec` existed (oops!) 11 | 12 | Here's a simple "hello world" ELF (`test_elfs/syscall-static-pie.c`), packed using stelf-loader: 13 | 14 | ```sh 15 | $ python3 elf_to_stelf.py test_elfs/syscall-static-pie - -r 16 | #!/bin/sh 17 | 18 | read a /proc/self/mem 4</dev/null 20 | A 21 | EOF 22 | cat /dev/fd/4 >&5 23 | tail -c+$(($(echo $a|cut -d\ -f9)+1)) <&3 2>&5 24 | base64 -d</dev/fd/4 & 25 | H4sIAMd7+mMC/+3ZP0vDQBjH8SdtEkqX9B30dG6pHQQH/y1KBhdfQkkjBgKKCYiTr6DQd+Bb6Fg6 26 | qODgu3Bz0L2z5pqeXASLi6jw/Qx5eH7ccwe3HenfT5rhaGceiNxKS2TmiEjg9cv4qW1iz44feyYW 27 | O57smbi+iF9uivExAAAAAAD4bdcHR4c1Rz/6S3XZlUXXKvv9ZR6qjyVFtiV+8XWL1XqtK18bO9Vq 28 | 9tWtZ/Wf67pUqz3n2vspWc2a8+31G9XalWo1czUzZ47rrT6usazHz/nQl+9rCAAAAAAAP2eqH8Z3 29 | +jPr6Gf+aPO1rf/dT7eLrv8WeA/cEQAAAAAAAAAAAAAAf10Yp+lZR2X5IE+i7nkSqzg9UZdJfqqG 30 | yUUc5Sq7yqJBmmZrTW4LAAAAAID/6R2QbjDaAFAAAA== 31 | EOF 32 | base64 -d<&3 33 | McCwCTH/vgBQAAAx0rIDTTHSQbIiTTHASffQTTHJDwVJicdJifZIifJIicYx/0C3BDHADwVIhcB4 34 | 90gBxkgpwnXvMcCwCkyJ/0yJ9jHSsgUPBUH/5w== 35 | EOF 36 | ``` 37 | 38 | Or, as a more copy-pasteable one-liner: 39 | 40 | ```sh 41 | echo H4sIAPoum2QC/1VSXY+iQBB851fwsDEashm+lcQ1AVEBvVMEBMm94DCDg6h8iUL2x5/rXS63/VJJVXVXJ90limI6oscgL68QVCjDoGorGGUZhR4I0sLkP+WMzrQ4Hs/Wc1qagBg14HJ7GlXqyVAwqukXh2MgTnoSVUcko98h89bvv/URPF7pt+gT3mr6Pf5F0+9YGTDcYDDuCTT/Ze8fogrJ4lN9JXwmt0tH8sHk38weZYiVqW6utzNvTwEjhBu20W0tNUZOXM9ORaBoAhsv5NywbQO03FI7xtg+nVItWS6ne+KWsm7HjHaQ54lMFWs9uQlax674TsoLFKjOisjpHdgPCRlDCO9IMNAFu1J5jBYQkZ+PpcO7ZzJLEwU2YuFzpBNFjNq9T62lYXU+EIbMlRtpZg/1Vbp4iKGxFSG37YDsCEGYeUGwbBol3MpF2u7mNdkxo8BfcUFRL7nRYa3sCkqp/ToEPpaHuVd0woVvqtyHfDRiBG6hBFHmXznYeV0oDsvSlW9V1BkduNgZo5TTP8nqhkeFMQoFikk3pbzATLRZcyUGsTtEq7IZ+UgFcGar3wpz7D5nsnDLB5I5Y8jwcnKKLlG8MLZwUSyo1vOg5BTDtrC08zksXV9cvTpNHchb3j6keqTOv4iPj9dTfL/ppCdQP+D0PnUN0CTaV/qDLU3ddQ3HPpjkiaqDsf1ES7/vLAJji+DQJNgyCdw/ADsVNN1Qn5p5hJpIKWyiPU5Jfr8EzWvwqbUA21pKajhV4m00zwDS/e8uvwGGMkYO6wIAAA==|base64 -d|gunzip|/bin/sh 42 | ``` 43 | 44 | You could put this in a file and execute it, but that would slightly defeat the 45 | purpose (of being file-less). The intended usage scenario is to paste it directly into a terminal, 46 | or perhaps even `curl | sh` or `nc | sh`. The aim is to be as portable as possible between different shell implementations. So far I've tested it against `bash`, `zsh`, `dash`, and `busybox ash`. 47 | 48 | This implementation currently relies on some hand-written x86-64 shellcode, but the 49 | general approach should be applicable cross-architecture. 50 | 51 | It works on both `static` and `static-pie` ELFs. However, dynamic ELFs are out of scope for this project. 52 | 53 | `static-pie` ELFs are recommended, to ensure that the address space does not collide with that of the loader process. 54 | 55 | ## Shell-based self-injecting shellcode loader 56 | 57 | This technique has a slightly complicated history, with some "multiple discovery" going on - multiple people working independently had similar ideas, with improvements over time. I will try to present the concepts in chronological order, with attribution where possible. 58 | 59 | Before ASLR/PIE was widely implemented, somebod{y,ies} realised that the `dd` binary could be used to inject code into its own address space, by writing to `/proc/self/mem`, making use of the `seek` argument to seek to the correct offset within the `mem` file to point into some executable code, which would subsequently be executed. 60 | 61 | An example of this can be seen in brainsmoke's [tweet](https://web.archive.org/web/20220907035407/https://twitter.com/brainsmoke/status/399558997994668033) here: 62 | 63 | ```sh 64 | base64 -d<</*` entries, and use that to know where to inject the code, thus "bypassing" ASLR. `dd` does not have permission to open the parent shell's `mem` file, and so clever fd redirection was used to get the shell itself to open the fd, to be passed into `dd`. 70 | 71 | Example ([source](https://web.archive.org/web/20220904095110/https://twitter.com/brainsmoke/status/1258875830014480386)) 72 | 73 | ```sh 74 | cd /proc/$$;exec 3>mem;(base64 -d<<&3 75 | ``` 76 | 77 | ### Parallel idea 2: 78 | 79 | Independently, `arget13` had the idea of using `dd` to do something similar to the base technique which he called [DDexec](https://github.com/arget13/DDexec). However, not realising that `mem` allows writing to read-only pages, he came up with a wonderfully complex mechanism for injecting a ROP payload, all implemented through shell scripting (Edit: I might have been getting my wires crossed here, thinking of [Cexigua](https://github.com/AonCyberLabs/Cexigua) - I need to revisit this). 80 | 81 | ### Parallel idea 3: 82 | 83 | I was vaguely aware of `brainsmoke`'s technique, having seen it mentioned on IRC several years prior. However, I couldn't remember how it worked exactly, and so I tried to reinvent it myself. I came up with [this](https://web.archive.org/web/20220811155804/https://twitter.com/David3141593/status/1386438123647868930) 84 | 85 | ```sh 86 | dd of=/proc/$$/mem bs=1 seek=$(($(cut -d" " -f9/syscall` and parsed out the value of the program counter register - which is inevitably pointing into some executable code. 90 | 91 | As-is, this technique was suboptimal, since it requires `dd` to be allowed to open the `mem` file of the parent process (which only happens if `dd` is run as root, or Yama is disabled). 92 | 93 | ### Merging of the streams 94 | 95 | After I posted about my variant of the technique, brainsmoke pointed out his fd redirection trick, which I was able to incorporate back into my technique, giving the "best of both worlds" - no need for parsing `objdump` output, and no need for root or disabled Yama. 96 | 97 | `arget13` saw the conversation, and incorporated the `syscall` technique into his `DDexec` tool. He also ditched his complex ROP generation code in favour of directly overwriting executable code. 98 | 99 | I wasn't aware of `DDexec` at the time, and I started writing my own tool along the same lines, which I called `stelf-loader` (this tool!) - but at this point in time I hadn't published it. 100 | 101 | After writing most of `stelf-loader`, I found `DDexec` and facepalmed - it was basically everything I set out to do with `stelf-loader`, and more. However, `stelf-loader` still does something that `DDexec` doesn't - `stelf-loader` is written in Python as opposed to shell scripting, and instead *generates* shell scripts (or one-liners) as output. The generated shell scripts are potentially very compact and can be pasted directly into a terminal session, whereas `DDexec` requires the (relatively) large ddexec shell script to be dropped onto the system first (you could work around this, but it would still take up more space). 102 | 103 | Around 12/12/2022, `arget13` discovered that several other common utilities other than `dd` could be used to seek the file descriptor, one of those alternatives being `tail`. `tail` is of course more commonly installed and executed than `dd`, making the overall technique more portable and less eyebrow-raising. 104 | 105 | After noticing this change to `DDexec`, I updated `stelf-loader` to also use `tail` for seeking. 106 | 107 | In its current form, `stelf-loader` combines: 108 | 109 | - "The" `/proc//mem` shellcode injection. 110 | - brainsmoke's fd redirection technique. 111 | - My own `/proc//syscall` program counter "leak" technique. 112 | - arget13's `tail`-seeking technique (as a sneakier alternative to `dd`) 113 | 114 | ## `elf_to_shellcode.py` 115 | 116 | This is usable as a standalone script. It flattens a static 117 | ELF into a piece of shellcode. This is very similar to what [gamozolabs/elfloader](https://github.com/gamozolabs/elfloader) does, 118 | except it also generates a shellcode stub that sets up the correct page permissions. (If I was rewriting this project today, I'd use `elfloader` to do the ELF parsing, but writing my own was an interesting educational exercise.) 119 | This avoids needing RWX mappings. 120 | 121 | ## `elf_to_stelf.py` 122 | 123 | This script ties together all the aforementioned tricks. It takes a static-pie ELF as input, and outputs a (potentially extremely long) sequence of shell commands. 124 | 125 | ## TODO 126 | 127 | - argv passthru 128 | - mmap and pivot to a fresh stack 129 | - add more options to `elf_to_shellcode.py` 130 | -------------------------------------------------------------------------------- /assembler.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | 4 | def nasm(source): 5 | asm_source_file = tempfile.NamedTemporaryFile("w+") 6 | asm_source_file.write(source) 7 | asm_source_file.flush() 8 | 9 | out_file = tempfile.NamedTemporaryFile("rb+") 10 | os.system(f"nasm {asm_source_file.name} -o {out_file.name}") 11 | return out_file.read() 12 | 13 | ASM_HEADER = """\ 14 | BITS 64 15 | 16 | sys_mprotect equ 10 17 | 18 | PROT_NONE equ 0x0 19 | PROT_READ equ 0x1 20 | PROT_WRITE equ 0x2 21 | PROT_EXEC equ 0x4 22 | 23 | MAP_FIXED equ 0x10 ;/* Interpret addr exactly */ 24 | MAP_ANONYMOUS equ 0x20 ;/* don't use a file */ 25 | MAP_PRIVATE equ 0x02 26 | 27 | AT_NULL equ 0 ;/* end of vector */ 28 | AT_PHDR equ 3 ;/* program headers for program */ 29 | AT_PHENT equ 4 ;/* size of program header entry */ 30 | AT_PHNUM equ 5 ;/* number of program headers */ 31 | AT_ENTRY equ 9 ;/* entry point of program */ 32 | AT_RANDOM equ 25 ;/* address of 16 random bytes */ 33 | 34 | 35 | global _start 36 | section .text 37 | """ 38 | -------------------------------------------------------------------------------- /elf_to_shellcode.py: -------------------------------------------------------------------------------- 1 | from elftools.elf.elffile import ELFFile 2 | from assembler import nasm, ASM_HEADER 3 | from functools import reduce 4 | import operator 5 | import io 6 | import tempfile 7 | import os 8 | 9 | 10 | class Mapper(): 11 | def __init__(self, base=0, align=0x1000): 12 | self.mem = bytearray() 13 | self.flags = [] 14 | self.align = align 15 | self.base = base 16 | 17 | def map(self, start, data, flags): 18 | start -= self.base 19 | 20 | # pre-extend the storage arrays, if necessary 21 | if start + len(data) > len(self.mem): 22 | extra_needed = start + len(data) - len(self.mem) 23 | self.mem += bytearray(extra_needed) 24 | self.flags += [0] * extra_needed 25 | 26 | # map the data 27 | self.mem[start:start+len(data)] = data 28 | self.flags[start:start+len(data)] = [flags] * len(data) 29 | 30 | # pad up to page boundary 31 | pad_len = -len(self.mem) % self.align 32 | self.mem += bytearray(pad_len) 33 | self.flags += [0] * (pad_len) 34 | 35 | def get_page_maps(self): # returns iterator of (start, length, perms) 36 | map_start = 0 37 | prev_flags = None 38 | 39 | for page_start in range(0, len(self.mem), self.align): 40 | page_flags = reduce( 41 | operator.or_, 42 | self.flags[page_start:page_start+self.align] 43 | ) 44 | 45 | if prev_flags is not None and page_flags != prev_flags: 46 | map_len = page_start - map_start 47 | yield (map_start + self.base, map_len, prev_flags) 48 | map_start = page_start 49 | 50 | prev_flags = page_flags 51 | 52 | if prev_flags is not None: 53 | yield (map_start + self.base, len(self.mem) - map_start, prev_flags) 54 | 55 | 56 | def flags_to_string(flags): 57 | return ('r' if flags & 4 else '-') \ 58 | + ('w' if flags & 2 else '-') \ 59 | + ('x' if flags & 1 else '-') 60 | 61 | def flags_to_prot(flags): 62 | words = [] 63 | if flags & 4: words.append("PROT_READ") 64 | if flags & 2: words.append("PROT_WRITE") 65 | if flags & 1: words.append("PROT_EXEC") 66 | 67 | if not words: 68 | return "PROT_NONE" 69 | 70 | return " | ".join(words) 71 | 72 | def elf_to_shellcode(elf_file, argv=[b"X"], raw_entry=False, verbose=True): 73 | argv_buf = b"" 74 | argv_offsets = [] 75 | for arg in argv: 76 | argv_offsets.append(len(argv_buf)) 77 | argv_buf += arg + b"\0" 78 | 79 | elf = ELFFile(elf_file) 80 | asm_source = io.StringIO() 81 | asm_source.write(ASM_HEADER + "\n_start:\n") 82 | 83 | image_base = min(seg.header.p_vaddr for seg in elf.iter_segments() if seg.header.p_type == "PT_LOAD") 84 | 85 | if verbose: 86 | print("[+] ELF Image base: ", hex(image_base)) 87 | print("[+] ELF Entry: ", hex(elf.header.e_entry)) 88 | print() 89 | print("[+] ELF segments:") 90 | print("\tType Offset VAddr FileSize MemSize Prot") 91 | 92 | mapper = Mapper(base=image_base) 93 | 94 | for seg in elf.iter_segments(): 95 | if verbose: 96 | print(f"\t{seg.header.p_type:<16} "\ 97 | f"0x{seg.header.p_offset:08x} "\ 98 | f"0x{seg.header.p_vaddr:08x} "\ 99 | f"0x{seg.header.p_filesz:08x} "\ 100 | f"0x{seg.header.p_memsz:08x} "\ 101 | f"{flags_to_string(seg.header.p_flags)}") 102 | 103 | # We only actually care about PT_LOAD segments 104 | if seg.header.p_type != "PT_LOAD": 105 | continue 106 | 107 | padded_data = seg.data() + bytes(seg.header.p_memsz - seg.header.p_filesz) 108 | mapper.map(seg.header.p_vaddr, padded_data, seg.header.p_flags) 109 | 110 | if verbose: 111 | print() 112 | print("[+] Page mappings:") 113 | print("\tStart Length Prot") 114 | 115 | for start, length, flags in mapper.get_page_maps(): 116 | if verbose: 117 | print(f"\t0x{start:08x} 0x{length:08x} {flags_to_string(flags)}") 118 | 119 | # TODO: have the loop in ASM, reading from a table? 120 | asm_source.write(f""" 121 | xor eax, eax 122 | mov al, sys_mprotect 123 | lea rdi, [rel imagebase + 0x{start - image_base:x}] 124 | mov rsi, 0x{length:x} 125 | mov rdx, {flags_to_prot(flags)} 126 | syscall 127 | """) 128 | 129 | image_file = tempfile.NamedTemporaryFile("wb+") 130 | image_file.write(mapper.mem) 131 | image_file.flush() 132 | 133 | if not raw_entry: 134 | asm_source.write(f""" 135 | 136 | ;push 0 ; possibly unnecessary stack alignment 137 | ;push 0x41 ; argv[0] 138 | ;mov r15, rsp 139 | push 0 ;alignment 140 | 141 | push 0 142 | push AT_NULL 143 | 144 | push 0x{elf.header.e_phentsize:x} 145 | push AT_PHENT 146 | 147 | lea rax, [rel imagebase] ; ideally this should point to some random bytes... TODO: use rdrand 148 | push rax 149 | push AT_RANDOM 150 | 151 | lea rax, [rel imagebase + 0x{elf.header.e_entry - image_base:x}] 152 | push rax 153 | push AT_ENTRY 154 | 155 | push 0x{elf.header.e_phnum:x} 156 | push AT_PHNUM 157 | 158 | lea rax, [rel imagebase + 0x{elf.header.e_phoff:x}] 159 | push rax 160 | push AT_PHDR 161 | 162 | push 0 ; end envp 163 | push 0 ; end argv 164 | """) 165 | if argv: 166 | asm_source.write(f""" 167 | lea rax, [rel argv + {argv_offsets[-1]}] 168 | push rax 169 | """) 170 | for a, b in zip(argv_offsets[-2::-1], argv_offsets[-1::-1]): 171 | asm_source.write(f""" 172 | sub rax, {b - a} 173 | push rax 174 | """) 175 | asm_source.write(f""" 176 | push {len(argv)} ; argc 177 | 178 | xor eax, eax 179 | xor edi, edi 180 | xor esi, esi 181 | xor edx, edx 182 | """) 183 | 184 | asm_source.write(f""" 185 | 186 | jmp imagebase + 0x{elf.header.e_entry - image_base:x} 187 | 188 | """) 189 | 190 | if argv: 191 | asm_source.write(f""" 192 | argv: 193 | db {', '.join(str(x) for x in argv_buf)} 194 | 195 | """) 196 | 197 | asm_source.write(f""" 198 | align 0x1000 199 | imagebase: 200 | incbin "{image_file.name}" 201 | """) 202 | 203 | if verbose: 204 | print() 205 | print("[+] Shellcode asm source:") 206 | print() 207 | print(asm_source.getvalue()) 208 | 209 | image = nasm(asm_source.getvalue()) 210 | 211 | # useful for testing 212 | #os.system(f"nasm {asm_source_file.name} -f elf64 -o shellcode.o && gcc shellcode.o -o shellcode.elf -nostdlib -static-pie") 213 | 214 | whole_image_base = image_base + len(mapper.mem) - len(image) if image_base else None 215 | 216 | return image, whole_image_base 217 | 218 | 219 | if __name__ == "__main__": 220 | import argparse 221 | 222 | parser = argparse.ArgumentParser("elf_to_shellcode") 223 | parser.add_argument("elf") 224 | parser.add_argument("dest") 225 | parser.add_argument("-r", "--raw_entry", action="store_true", help="Do not put argv/env/auxv on the stack before calling the entrypoint") 226 | parser.add_argument("-v", "--verbose", action="store_true") 227 | parser.add_argument("-a", "--argv", action="append", default=[], help="args to be passed to argv (can be repeated)") 228 | 229 | args = parser.parse_args() 230 | 231 | with open(args.elf, "rb") as elf: 232 | image, image_base = elf_to_shellcode(elf, argv=[x.encode() for x in args.argv], raw_entry=args.raw_entry, verbose=args.verbose) 233 | with open(args.dest, "wb") as out_file: 234 | out_file.write(image) 235 | -------------------------------------------------------------------------------- /elf_to_stelf.py: -------------------------------------------------------------------------------- 1 | from elf_to_shellcode import elf_to_shellcode 2 | from assembler import nasm, ASM_HEADER 3 | import base64 4 | import gzip 5 | import io 6 | 7 | 8 | def multiline_b64(data): 9 | return base64.encodebytes(data).decode() 10 | 11 | def b64(data): 12 | return base64.b64encode(data).decode() 13 | 14 | def elf_to_stelf(elf_file, out_file, argv=[b"X"], oneliner=False, raw_entry=False, verbose=True): 15 | image, image_base = elf_to_shellcode(elf_file, argv=argv, raw_entry=raw_entry, verbose=verbose) 16 | 17 | primary_shellcode_src = ASM_HEADER + f""" 18 | _start: 19 | xor eax, eax 20 | mov al, 0x9 ; mmap( 21 | {f"mov rdi, 0x{image_base:x}" if image_base else "xor edi, edi ; NULL,"} 22 | mov rsi, 0x{len(image):x}, 23 | xor edx, edx 24 | mov dl, PROT_READ | PROT_WRITE 25 | xor r10, r10 26 | mov r10b, MAP_ANONYMOUS | MAP_PRIVATE{" | MAP_FIXED" if image_base else ""} 27 | xor r8, r8 28 | not r8, ; fd=-1, 29 | xor r9, r9 ; offset=0 ) 30 | syscall 31 | 32 | mov r15, rax 33 | mov r14, rsi 34 | mov rdx, rsi 35 | mov rsi, rax 36 | 37 | xor edi, edi 38 | mov dil, 4 ; fd=4 39 | 40 | readloop: 41 | xor eax, eax ; read(rdx=4, rsi=buf, rdx=len) 42 | syscall 43 | 44 | test rax, rax 45 | js readloop 46 | 47 | add rsi, rax 48 | sub rdx, rax 49 | jnz readloop 50 | 51 | readloop_done: 52 | xor eax, eax 53 | mov al, sys_mprotect 54 | mov rdi, r15 55 | mov rsi, r14 56 | xor edx, edx 57 | mov dl, PROT_READ | PROT_EXEC 58 | syscall 59 | 60 | jmp r15 61 | """ 62 | 63 | #open("tmp.asm", "w").write(primary_shellcode_src) 64 | primary_shellcode = nasm(primary_shellcode_src) 65 | compressed_secondary_shellcode = gzip.compress(image) 66 | 67 | result = f"""\ 68 | #!/bin/sh 69 | 70 | read a /proc/self/mem 4</dev/null 72 | A 73 | EOF 74 | cat /dev/fd/4>&5 75 | tail -c+$(($(echo $a|cut -d\ -f9)+1))<&3 2>&5 76 | (base64 -d</dev/fd/4& 77 | {multiline_b64(compressed_secondary_shellcode)}EOF 78 | base64 -d<&3 79 | {multiline_b64(primary_shellcode)}EOF 80 | """ 81 | 82 | if oneliner: 83 | stripped = "\n".join(result.split("\n")[2:]) # strip hashbang 84 | result = f"echo {b64(gzip.compress(stripped.encode()))}|base64 -d|gunzip|/bin/sh\n" 85 | 86 | #print(result) 87 | 88 | out_file.write(result) 89 | 90 | if __name__ == "__main__": 91 | import argparse 92 | 93 | parser = argparse.ArgumentParser("elf_to_stelf") 94 | parser.add_argument("elf") 95 | parser.add_argument("dest") 96 | parser.add_argument("-r", "--raw_entry", action="store_true") 97 | parser.add_argument("-o", "--oneliner", action="store_true", help="wrap everything as a oneliner") 98 | parser.add_argument("-v", "--verbose", action="store_true") 99 | parser.add_argument("-a", "--argv", action="append", default=[], help="args to be passed to argv (can be repeated)") 100 | 101 | args = parser.parse_args() 102 | 103 | with open(args.elf, "rb") as elf: 104 | with open("/dev/stdout" if args.dest == "-" else args.dest, "w") as dest: 105 | elf_to_stelf(elf, dest, argv=[x.encode() for x in args.argv], oneliner=args.oneliner, raw_entry=args.raw_entry, verbose=args.verbose) 106 | -------------------------------------------------------------------------------- /test_elfs/Makefile: -------------------------------------------------------------------------------- 1 | all: glibc-static-pie glibc-static syscall-static-pie rust_helloworld 2 | 3 | glibc-static-pie: glibc-static-pie.c 4 | gcc glibc-static-pie.c -o glibc-static-pie -static-pie -s 5 | 6 | glibc-static: glibc-static.c 7 | gcc glibc-static.c -o glibc-static -static -s 8 | 9 | syscall-static-pie: syscall-static-pie.c 10 | gcc syscall-static-pie.c -o syscall-static-pie -nostdlib -static-pie -s -Os -fcf-protection=none -Wl,--build-id=none -fno-exceptions -fno-unwind-tables -fno-asynchronous-unwind-tables -fno-ident 11 | strip \ 12 | --remove-section=.gnu.hash \ 13 | --remove-section=.dynsym \ 14 | --remove-section=.dynstr \ 15 | --remove-section=.dynamic \ 16 | --remove-section=.eh_frame \ 17 | syscall-static-pie 18 | 19 | 20 | rust_helloworld: rust_helloworld.rs 21 | rustc --target x86_64-unknown-linux-musl rust_helloworld.rs 22 | -------------------------------------------------------------------------------- /test_elfs/glibc-static-pie.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main(int argc, char *argv[], char *envp[]) 4 | { 5 | printf("Hello, glibc static-pie ELF!\n"); 6 | 7 | printf("I have %d args:\n", argc); 8 | for (int i=0; i 2 | 3 | int main() 4 | { 5 | printf("Hello, glibc static ELF!\n"); 6 | } 7 | -------------------------------------------------------------------------------- /test_elfs/rust_helloworld.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello Rust world!"); 3 | } 4 | -------------------------------------------------------------------------------- /test_elfs/syscall-static-pie.c: -------------------------------------------------------------------------------- 1 | static int _errno; 2 | #define SYS_ERRNO _errno 3 | #include "linux_syscall_support.h" 4 | 5 | static char msg[] = "Hello, static-pie elf with direct syscalls!\n"; 6 | 7 | void _start(void) 8 | { 9 | sys_write(1, msg, sizeof(msg) - 1); 10 | sys__exit(0); 11 | } 12 | -------------------------------------------------------------------------------- /testall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | echo 6 | echo 7 | echo === syscall-static-pie === 8 | python3 elf_to_stelf.py test_elfs/syscall-static-pie out.sh -r 9 | echo 10 | 11 | echo sh: 12 | sh ./out.sh 13 | echo 14 | echo ash: 15 | ash ./out.sh 16 | echo 17 | echo bash: 18 | bash ./out.sh 19 | echo 20 | echo dash: 21 | dash ./out.sh 22 | echo 23 | echo zsh: 24 | zsh ./out.sh 25 | echo 26 | 27 | echo 28 | echo === glibc-static-pie === 29 | python3 elf_to_stelf.py test_elfs/glibc-static-pie out.sh -a hello -a world 30 | echo 31 | 32 | echo sh: 33 | sh ./out.sh 34 | echo 35 | echo ash: 36 | ash ./out.sh 37 | echo 38 | echo bash: 39 | bash ./out.sh 40 | echo 41 | echo dash: 42 | dash ./out.sh 43 | echo 44 | echo zsh: 45 | zsh ./out.sh 46 | echo 47 | 48 | echo 49 | echo === oneliner === 50 | python3 elf_to_stelf.py test_elfs/syscall-static-pie out.sh -r -o 51 | echo 52 | wc -c out.sh 53 | echo 54 | sh ./out.sh 55 | echo --------------------------------------------------------------------------------