diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 7474cc4e6..b9fe7774f 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -13,16 +13,16 @@ jobs: steps: # Required for subdirectories in Git context - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Build and push base image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 if: github.event_name == 'workflow_dispatch' with: context: "{{defaultContext}}:extra/docker/base" @@ -30,7 +30,7 @@ jobs: tags: pwntools/pwntools:base - name: Build and push stable image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/stable') with: context: "{{defaultContext}}:extra/docker/stable" @@ -38,7 +38,7 @@ jobs: tags: pwntools/pwntools:stable - name: Build and push beta image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/beta') with: context: "{{defaultContext}}:extra/docker/beta" @@ -46,7 +46,7 @@ jobs: tags: pwntools/pwntools:beta - name: Build and push dev image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/dev') with: context: "{{defaultContext}}:extra/docker/dev" @@ -56,7 +56,7 @@ jobs: pwntools/pwntools:latest - name: Build and push ci image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/dev') with: context: "{{defaultContext}}:travis/docker" diff --git a/CHANGELOG.md b/CHANGELOG.md index 21fa4aaeb..5a886d438 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,11 @@ The table below shows which release corresponds to each branch, and what date th | Version | Branch | Release Date | | ---------------- | -------- | ---------------------- | -| [4.12.0](#4120) | `dev` | -| [4.11.0](#4110) | `beta` | -| [4.10.0](#4100) | `stable` | May 21, 2023 +| [4.13.0](#4130-dev) | `dev` | +| [4.12.0](#4120-beta) | `beta` | +| [4.11.1](#4111-stable) | `stable` | Nov 14, 2023 +| [4.11.0](#4110) | | Sep 15, 2023 +| [4.10.0](#4100) | | May 21, 2023 | [4.9.0](#490) | | Dec 29, 2022 | [4.8.0](#480) | | Apr 21, 2022 | [4.7.1](#471) | | Apr 20, 2022 @@ -66,43 +68,68 @@ The table below shows which release corresponds to each branch, and what date th | [3.0.0](#300) | | Aug 20, 2016 | [2.2.0](#220) | | Jan 5, 2015 -## 4.12.0 (`dev`) +## 4.13.0 (`dev`) + +- [#2277][2277] elf: Resolve more relocations into GOT entries +- [#2281][2281] FIX: Getting right amount of data for search fix +- [#2293][2293] Add x86 CET status to checksec output +- [#1763][1763] Allow to add to the existing environment in `process` instead of replacing it + +[2277]: https://github.com/Gallopsled/pwntools/pull/2277 +[2281]: https://github.com/Gallopsled/pwntools/pull/2281 +[2293]: https://github.com/Gallopsled/pwntools/pull/2293 +[1763]: https://github.com/Gallopsled/pwntools/pull/1763 + +## 4.12.0 (`beta`) + - [#2202][2202] Fix `remote` and `listen` in sagemath - [#2117][2117] Add -p (--prefix) and -s (--separator) arguments to `hex` command +- [#2215][2215] Add pyinstaller hook to support bundling scripts using pwntools - [#2221][2221] Add shellcraft.sleep template wrapping SYS_nanosleep - [#2219][2219] Fix passing arguments on the stack in shellcraft syscall template - [#2212][2212] Add `--libc libc.so` argument to `pwn template` command - [#2257][2257] Allow creation of custom templates for `pwn template` command -- [#2215][2215] Add pyinstaller hook to support bundling scripts using pwntools +- [#2225][2225] Allow empty argv in ssh.process() [2202]: https://github.com/Gallopsled/pwntools/pull/2202 [2117]: https://github.com/Gallopsled/pwntools/pull/2117 +[2215]: https://github.com/Gallopsled/pwntools/pull/2215 [2221]: https://github.com/Gallopsled/pwntools/pull/2221 [2219]: https://github.com/Gallopsled/pwntools/pull/2219 [2212]: https://github.com/Gallopsled/pwntools/pull/2212 [2257]: https://github.com/Gallopsled/pwntools/pull/2257 -[2215]: https://github.com/Gallopsled/pwntools/pull/2215 +[2225]: https://github.com/Gallopsled/pwntools/pull/2225 + +## 4.11.1 (`stable`) + +- [#2271][2271] FIX: Generated shebang with path to python invalid if path contains spaces +- [#2272][2272] Fix `tube.clean_and_log` not logging buffered data +- [#2281][2281] FIX: Getting right amount of data for search fix +- [#2287][2287] Fix `_countdown_handler` not invoking `timeout_change` +- [#2294][2294] Fix atexit SEGV in aarch64 loader -## 4.11.0 (`beta`) +[2271]: https://github.com/Gallopsled/pwntools/pull/2271 +[2272]: https://github.com/Gallopsled/pwntools/pull/2272 +[2281]: https://github.com/Gallopsled/pwntools/pull/2281 +[2287]: https://github.com/Gallopsled/pwntools/pull/2287 +[2294]: https://github.com/Gallopsled/pwntools/pull/2294 + +## 4.11.0 - [#2185][2185] make fmtstr module able to create payload without $ notation - [#2103][2103] Add search for libc binary by leaked function addresses `libcdb.search_by_symbol_offsets()` - [#2177][2177] Support for RISC-V 64-bit architecture - [#2186][2186] Enhance `ELF.nx` and `ELF.execstack` - [#2129][2129] Handle `context.newline` correctly when typing in `tube.interactive()` +- [#2214][2214] Fix bug at ssh.py:`download` and `download_file` with relative paths +- [#2241][2241] Fix ssh.process not setting ssh_process.cwd attribute +- [#2261][2261] Fix corefile module after pyelftools update [2185]: https://github.com/Gallopsled/pwntools/pull/2185 [2103]: https://github.com/Gallopsled/pwntools/pull/2103 [2177]: https://github.com/Gallopsled/pwntools/pull/2177 [2186]: https://github.com/Gallopsled/pwntools/pull/2186 [2129]: https://github.com/Gallopsled/pwntools/pull/2129 - -## 4.10.1 (`stable`) - -- [#2214][2214] Fix bug at ssh.py:`download` and `download_file` with relative paths -- [#2241][2241] Fix ssh.process not setting ssh_process.cwd attribute -- [#2261][2261] Fix corefile module after pyelftools update - [2214]: https://github.com/Gallopsled/pwntools/pull/2214 [2241]: https://github.com/Gallopsled/pwntools/pull/2241 [2261]: https://github.com/Gallopsled/pwntools/pull/2261 diff --git a/MANIFEST.in b/MANIFEST.in index 8f001ea41..5327e1886 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,4 +6,4 @@ include *.md *.txt *.sh *.yml MANIFEST.in recursive-include docs *.rst *.png Makefile *.py *.txt recursive-include pwnlib *.py *.asm *.rst *.md *.txt *.sh __doc__ *.mako recursive-include pwn *.py *.asm *.rst *.md *.txt *.sh -recursive-exclude *.pyc +global-exclude *.pyc diff --git a/examples/clean_and_log.py b/examples/clean_and_log.py index a307d76a2..5e5a2493c 100644 --- a/examples/clean_and_log.py +++ b/examples/clean_and_log.py @@ -11,18 +11,24 @@ """ from pwn import * +from multiprocessing import Process -os.system('''(( -echo prefix sometext ; -echo prefix someothertext ; -echo here comes the flag ; -echo LostInTheInterTubes -) | nc -l 1337) & -''') +def submit_data(): + with context.quiet: + with listen(1337) as io: + io.wait_for_connection() + io.sendline(b'prefix sometext') + io.sendline(b'prefix someothertext') + io.sendline(b'here comes the flag') + io.sendline(b'LostInTheInterTubes') -r = remote('localhost', 1337) -atexit.register(r.clean_and_log) +if __name__ == '__main__': + p = Process(target=submit_data) + p.start() -while True: - line = r.recvline() - print(re.findall(r'^prefix (\S+)$', line)[0]) + r = remote('localhost', 1337) + atexit.register(r.clean_and_log) + + while True: + line = r.recvline() + print(re.findall(br'^prefix (\S+)$', line)[0]) diff --git a/extra/docker/beta/Dockerfile b/extra/docker/beta/Dockerfile index cbfd05632..5a83dd6fc 100644 --- a/extra/docker/beta/Dockerfile +++ b/extra/docker/beta/Dockerfile @@ -2,6 +2,6 @@ FROM pwntools/pwntools:stable USER root RUN python2.7 -m pip install --upgrade git+https://github.com/Gallopsled/pwntools@beta \ - && python3 -m pip install --upgrade git+https://github.com/Gallopsled/pwntools@beta + && python3 -m pip install --force-reinstall --upgrade git+https://github.com/Gallopsled/pwntools@beta RUN PWNLIB_NOTERM=1 pwn update USER pwntools diff --git a/extra/docker/dev/Dockerfile b/extra/docker/dev/Dockerfile index d5f7af8f5..77d04d331 100644 --- a/extra/docker/dev/Dockerfile +++ b/extra/docker/dev/Dockerfile @@ -2,6 +2,6 @@ FROM pwntools/pwntools:stable USER root RUN python2.7 -m pip install --upgrade git+https://github.com/Gallopsled/pwntools@dev \ - && python3 -m pip install --upgrade git+https://github.com/Gallopsled/pwntools@dev + && python3 -m pip install --force-reinstall --upgrade git+https://github.com/Gallopsled/pwntools@dev RUN PWNLIB_NOTERM=1 pwn update USER pwntools diff --git a/extra/docker/stable/Dockerfile b/extra/docker/stable/Dockerfile index 980ef3f7e..1535d4af1 100644 --- a/extra/docker/stable/Dockerfile +++ b/extra/docker/stable/Dockerfile @@ -2,6 +2,6 @@ FROM pwntools/pwntools:base USER root RUN python2.7 -m pip install --upgrade git+https://github.com/Gallopsled/pwntools@stable \ - && python3 -m pip install --upgrade git+https://github.com/Gallopsled/pwntools@stable + && python3 -m pip install --force-reinstall --upgrade git+https://github.com/Gallopsled/pwntools@stable RUN PWNLIB_NOTERM=1 pwn update USER pwntools diff --git a/pwnlib/commandline/__init__.py b/pwnlib/commandline/__init__.py index a0aeedac1..2c1b31aef 100644 --- a/pwnlib/commandline/__init__.py +++ b/pwnlib/commandline/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python2 __all__ = [ 'asm', 'checksec', diff --git a/pwnlib/commandline/asm.py b/pwnlib/commandline/asm.py index 8f1c39884..03c51a6a2 100644 --- a/pwnlib/commandline/asm.py +++ b/pwnlib/commandline/asm.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python2 from __future__ import absolute_import from __future__ import division diff --git a/pwnlib/commandline/checksec.py b/pwnlib/commandline/checksec.py index 1b7e74c3c..5dcea5e38 100644 --- a/pwnlib/commandline/checksec.py +++ b/pwnlib/commandline/checksec.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python2 from __future__ import absolute_import from __future__ import division diff --git a/pwnlib/commandline/constgrep.py b/pwnlib/commandline/constgrep.py old mode 100755 new mode 100644 index 5959b5155..bac138d72 --- a/pwnlib/commandline/constgrep.py +++ b/pwnlib/commandline/constgrep.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python2 from __future__ import absolute_import from __future__ import division diff --git a/pwnlib/commandline/cyclic.py b/pwnlib/commandline/cyclic.py index eeb55b9b0..ff012a359 100644 --- a/pwnlib/commandline/cyclic.py +++ b/pwnlib/commandline/cyclic.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python2 from __future__ import absolute_import from __future__ import division diff --git a/pwnlib/commandline/debug.py b/pwnlib/commandline/debug.py index 5c92af36d..fe5fca6f5 100644 --- a/pwnlib/commandline/debug.py +++ b/pwnlib/commandline/debug.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python2 from __future__ import absolute_import from __future__ import division diff --git a/pwnlib/commandline/disablenx.py b/pwnlib/commandline/disablenx.py index 9751f3b6a..29839c0f8 100644 --- a/pwnlib/commandline/disablenx.py +++ b/pwnlib/commandline/disablenx.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python2 from __future__ import absolute_import from __future__ import division diff --git a/pwnlib/commandline/disasm.py b/pwnlib/commandline/disasm.py index 78e69b904..4c4535594 100644 --- a/pwnlib/commandline/disasm.py +++ b/pwnlib/commandline/disasm.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python2 from __future__ import absolute_import from __future__ import division from __future__ import print_function diff --git a/pwnlib/commandline/elfdiff.py b/pwnlib/commandline/elfdiff.py index 60e5d8fbf..48afef09f 100644 --- a/pwnlib/commandline/elfdiff.py +++ b/pwnlib/commandline/elfdiff.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python2 from __future__ import absolute_import from __future__ import division diff --git a/pwnlib/commandline/elfpatch.py b/pwnlib/commandline/elfpatch.py index 7de0f2015..10a5adc24 100644 --- a/pwnlib/commandline/elfpatch.py +++ b/pwnlib/commandline/elfpatch.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python2 from __future__ import absolute_import from __future__ import division diff --git a/pwnlib/commandline/hex.py b/pwnlib/commandline/hex.py index 136106c67..d538af246 100644 --- a/pwnlib/commandline/hex.py +++ b/pwnlib/commandline/hex.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python2 from __future__ import absolute_import from __future__ import division diff --git a/pwnlib/commandline/libcdb.py b/pwnlib/commandline/libcdb.py index 1555db2a7..b32400d2c 100644 --- a/pwnlib/commandline/libcdb.py +++ b/pwnlib/commandline/libcdb.py @@ -221,7 +221,7 @@ def main(args): exe = ELF(file, checksec=False) log.info('%s', text.red(os.path.basename(file))) - libc_version = re.search(b'libc[ -](\d+\.\d+)', exe.data) + libc_version = re.search(br'libc[ -](\d+\.\d+)', exe.data) if libc_version: log.indented('%-20s %s', text.green('Version:'), libc_version.group(1).decode()) diff --git a/pwnlib/commandline/phd.py b/pwnlib/commandline/phd.py index 1ef1fd91a..7f3891e0f 100644 --- a/pwnlib/commandline/phd.py +++ b/pwnlib/commandline/phd.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python2 from __future__ import absolute_import from __future__ import division diff --git a/pwnlib/commandline/shellcraft.py b/pwnlib/commandline/shellcraft.py index 9d49c5608..9f5fe36ae 100644 --- a/pwnlib/commandline/shellcraft.py +++ b/pwnlib/commandline/shellcraft.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python2 from __future__ import absolute_import from __future__ import division diff --git a/pwnlib/commandline/template.py b/pwnlib/commandline/template.py old mode 100755 new mode 100644 index a8f480dfe..f68461cb6 --- a/pwnlib/commandline/template.py +++ b/pwnlib/commandline/template.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python2 from __future__ import absolute_import from __future__ import division diff --git a/pwnlib/commandline/unhex.py b/pwnlib/commandline/unhex.py index 048bb9224..a254e6b3f 100644 --- a/pwnlib/commandline/unhex.py +++ b/pwnlib/commandline/unhex.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python2 from __future__ import absolute_import from __future__ import division diff --git a/pwnlib/elf/elf.py b/pwnlib/elf/elf.py index 96244af8f..5c41f0f12 100644 --- a/pwnlib/elf/elf.py +++ b/pwnlib/elf/elf.py @@ -47,14 +47,15 @@ from six import BytesIO -from collections import namedtuple +from collections import namedtuple, defaultdict from elftools.elf.constants import P_FLAGS from elftools.elf.constants import SHN_INDICES from elftools.elf.descriptions import describe_e_type from elftools.elf.elffile import ELFFile +from elftools.elf.enums import ENUM_GNU_PROPERTY_X86_FEATURE_1_FLAGS from elftools.elf.gnuversions import GNUVerDefSection -from elftools.elf.relocation import RelocationSection +from elftools.elf.relocation import RelocationSection, RelrRelocationSection from elftools.elf.sections import SymbolTableSection from elftools.elf.segments import InterpSegment @@ -510,6 +511,29 @@ def iter_segments_by_type(self, t): if t == seg.header.p_type or t in str(seg.header.p_type): yield seg + def iter_notes(self): + """ + Yields: + All the notes in the PT_NOTE segments. Each result is a dictionary- + like object with ``n_name``, ``n_type``, and ``n_desc`` fields, amongst + others. + """ + for seg in self.iter_segments_by_type('PT_NOTE'): + for note in seg.iter_notes(): + yield note + + def iter_properties(self): + """ + Yields: + All the GNU properties in the PT_NOTE segments. Each result is a dictionary- + like object with ``pr_type``, ``pr_datasz``, and ``pr_data`` fields. + """ + for note in self.iter_notes(): + if note.n_type != 'NT_GNU_PROPERTY_TYPE_0': + continue + for prop in note.n_desc: + yield prop + def get_segment_for_address(self, address, size=1): """get_segment_for_address(address, size=1) -> Segment @@ -917,15 +941,25 @@ def _populate_synthetic_symbols(self): self.symbols['got.' + symbol] = address def _populate_got(self): - """Loads the symbols for all relocations""" + """Loads the symbols for all relocations. + + >>> libc = ELF(which('bash')).libc + >>> assert 'strchrnul' in libc.got + >>> assert 'memcpy' in libc.got + >>> assert libc.got.strchrnul != libc.got.memcpy + """ # Statically linked implies no relocations, since there is no linker # Could always be self-relocating like Android's linker *shrug* if self.statically_linked: return + revsymbols = defaultdict(list) + for name, addr in self.symbols.items(): + revsymbols[addr].append(name) + for section in self.sections: # We are only interested in relocations - if not isinstance(section, RelocationSection): + if not isinstance(section, (RelocationSection, RelrRelocationSection)): continue # Only get relocations which link to another section (for symbols) @@ -937,7 +971,13 @@ def _populate_got(self): for rel in section.iter_relocations(): sym_idx = rel.entry.r_info_sym - if not sym_idx: + if not sym_idx and rel.is_RELA(): + # TODO: actually resolve relocations + relocated = rel.entry.r_addend # sufficient for now + + symnames = revsymbols[relocated] + for symname in symnames: + self.got[symname] = rel.entry.r_offset continue symbol = symbols.get_symbol(sym_idx) @@ -1195,9 +1235,10 @@ def search(self, needle, writable = False, executable = False): for seg in segments: addr = seg.header.p_vaddr memsz = seg.header.p_memsz - zeroed = memsz - seg.header.p_filesz + filesz = seg.header.p_filesz + zeroed = memsz - filesz offset = seg.header.p_offset - data = self.mmap[offset:offset+memsz] + data = self.mmap[offset:offset+filesz] data += b'\x00' * zeroed offset = 0 while True: @@ -2059,6 +2100,12 @@ def checksec(self, banner=True, color=True): if self.ubsan: res.append("UBSAN:".ljust(10) + green("Enabled")) + + if self.shadowstack: + res.append("SHSTK:".ljust(10) + green("Enabled")) + + if self.ibt: + res.append("IBT:".ljust(10) + green("Enabled")) # Check for Linux configuration, it must contain more than # just the version. @@ -2116,6 +2163,31 @@ def ubsan(self): """:class:`bool`: Whether the current binary was built with Undefined Behavior Sanitizer (``UBSAN``).""" return any(s.startswith('__ubsan_') for s in self.symbols) + + @property + def shadowstack(self): + """:class:`bool`: Whether the current binary was built with + Shadow Stack (``SHSTK``)""" + if self.arch not in ['i386', 'amd64']: + return False + for prop in self.iter_properties(): + if prop.pr_type != 'GNU_PROPERTY_X86_FEATURE_1_AND': + continue + return prop.pr_data & ENUM_GNU_PROPERTY_X86_FEATURE_1_FLAGS['GNU_PROPERTY_X86_FEATURE_1_SHSTK'] > 0 + return False + + @property + def ibt(self): + """:class:`bool`: Whether the current binary was built with + Indirect Branch Tracking (``IBT``)""" + if self.arch not in ['i386', 'amd64']: + return False + for prop in self.iter_properties(): + if prop.pr_type != 'GNU_PROPERTY_X86_FEATURE_1_AND': + continue + return prop.pr_data & ENUM_GNU_PROPERTY_X86_FEATURE_1_FLAGS['GNU_PROPERTY_X86_FEATURE_1_IBT'] > 0 + return False + def _update_args(self, kw): kw.setdefault('arch', self.arch) diff --git a/pwnlib/rop/rop.py b/pwnlib/rop/rop.py index 09923730f..6a57a60d1 100644 --- a/pwnlib/rop/rop.py +++ b/pwnlib/rop/rop.py @@ -717,15 +717,20 @@ def setRegisters(self, registers): name = ",".join(goodregs) stack.append((gadget.address, gadget)) for r in gadget.regs: - moved += context.bytes - if r in registers: - stack.append((registers[r], r)) - else: - stack.append((Padding('' % r), r)) + if isinstance(r, str): + if r in registers: + stack.append((registers[r], r)) + else: + stack.append((Padding('' % r), r)) + moved += context.bytes + continue + + for slot in range(moved, moved + r, context.bytes): + left = gadget.move - slot + stack.append((Padding('' % left), 'stack padding')) + moved += context.bytes - for slot in range(moved, gadget.move, context.bytes): - left = gadget.move - slot - stack.append((Padding('' % left), 'stack padding')) + assert moved == gadget.move return stack @@ -1389,9 +1394,7 @@ def __getattr__(self, k): elif add.match(insn): arg = int(add.match(insn).group(1), 16) sp_move += arg - while arg >= context.bytes: - regs.append(hex(arg)) - arg -= context.bytes + regs.append(arg) elif ret.match(insn): sp_move += context.bytes elif leave.match(insn): diff --git a/pwnlib/shellcraft/templates/aarch64/linux/loader.asm b/pwnlib/shellcraft/templates/aarch64/linux/loader.asm index 7136aaedf..d6f23cd25 100644 --- a/pwnlib/shellcraft/templates/aarch64/linux/loader.asm +++ b/pwnlib/shellcraft/templates/aarch64/linux/loader.asm @@ -107,14 +107,14 @@ PT_LOAD = 1 mov x3, sp stp x2, x3, [sp, #-16]! - /* argc, argv[0], argv[1], envp */ + /* argc, argv[0], argv[1], envp; x0 must be zero! */ /* ideally these could all be empty, but unfortunately we have to keep the stack aligned. it's easier to just push an extra argument than care... */ stp x0, x1, [sp, #-16]! /* argv[1] = NULL, envp = NULL */ - mov x0, 1 - mov x1, sp - stp x0, x1, [sp, #-16]! /* argc = 1, argv[0] = "" */ + mov x2, 1 + mov x3, sp + stp x2, x3, [sp, #-16]! /* argc = 1, argv[0] = "" */ br x8 diff --git a/pwnlib/timeout.py b/pwnlib/timeout.py index a1a4859f8..8e21a2d09 100644 --- a/pwnlib/timeout.py +++ b/pwnlib/timeout.py @@ -30,9 +30,11 @@ def __enter__(self): self.obj._stop = min(self.obj._stop, self.old_stop) self.obj._timeout = self.timeout + self.obj.timeout_change() def __exit__(self, *a): self.obj._timeout = self.old_timeout self.obj._stop = self.old_stop + self.obj.timeout_change() class _local_handler(object): def __init__(self, obj, timeout): @@ -157,7 +159,7 @@ def _get_timeout_seconds(self, value): else: value = float(value) - if value is value < 0: + if value < 0: raise AttributeError("timeout: Timeout cannot be negative") if value > self.maximum: diff --git a/pwnlib/tubes/process.py b/pwnlib/tubes/process.py index 8770ade0c..8c44e7b7d 100644 --- a/pwnlib/tubes/process.py +++ b/pwnlib/tubes/process.py @@ -58,7 +58,9 @@ class process(tube): cwd(str): Working directory. Uses the current working directory by default. env(dict): - Environment variables. By default, inherits from Python's environment. + Environment variables to add to the environment. + ignore_environ(bool): + Ignore Python's environment. By default use Python's environment iff env not specified. stdin(int): File object or file descriptor number to use for ``stdin``. By default, a pipe is used. A pty can be used instead by setting @@ -224,6 +226,7 @@ def __init__(self, argv = None, executable = None, cwd = None, env = None, + ignore_environ = None, stdin = PIPE, stdout = PTY, stderr = STDOUT, @@ -255,7 +258,7 @@ def __init__(self, argv = None, original_env = env if shell: - executable_val, argv_val, env_val = executable, argv, env + executable_val, argv_val, env_val = executable or '/bin/sh', argv, env else: executable_val, argv_val, env_val = self._validate(cwd, executable, argv, env) @@ -287,14 +290,14 @@ def __init__(self, argv = None, #: Full path to the executable self.executable = executable_val + if ignore_environ is None: + ignore_environ = env is not None # compat + #: Environment passed on envp - self.env = os.environ if env is None else env_val + self.env = {} if ignore_environ else dict(getattr(os, "environb", os.environ)) - if self.executable is None: - if shell: - self.executable = '/bin/sh' - else: - self.executable = which(self.argv[0], path=self.env.get('PATH')) + # Add environment variables as needed + self.env.update(env_val or {}) self._cwd = os.path.realpath(cwd or os.path.curdir) diff --git a/pwnlib/tubes/ssh.py b/pwnlib/tubes/ssh.py index 33978cec3..5f020f4b0 100644 --- a/pwnlib/tubes/ssh.py +++ b/pwnlib/tubes/ssh.py @@ -17,6 +17,7 @@ from pwnlib import term from pwnlib.context import context, LocalContext +from pwnlib.exception import PwnlibException from pwnlib.log import Logger from pwnlib.log import getLogger from pwnlib.term import text @@ -613,6 +614,9 @@ def __init__(self, user=None, host=None, port=22, password=None, key=None, self._platform_info = {} self._aslr = None self._aslr_ulimit = None + self._cpuinfo_cache = None + self._user_shstk = None + self._ibt = None misc.mkdir_p(self._cachedir) @@ -882,6 +886,23 @@ def process(self, argv=None, executable=None, tty=True, cwd=None, env=None, time >>> io = s.process(['cat'], timeout=5) >>> io.recvline() b'' + + >>> # Testing that empty argv works + >>> io = s.process([], executable='sh') + >>> io.sendline(b'echo $0') + >>> io.recvline() + b'$ \n' + >>> # Make sure that we have a shell + >>> io.sendline(b'echo hello') + >>> io.recvline() + b'$ hello\n' + + >>> # Testing that empty argv[0] works + >>> io = s.process([''], executable='sh') + >>> io.sendline(b'echo $0') + >>> io.recvline() + b'$ \n' + """ if not argv and not executable: self.error("Must specify argv or executable") @@ -945,7 +966,7 @@ def func(): pass os.environ.clear() environ.update(env) else: - env = os.environ + env = environ def is_exe(path): return os.path.isfile(path) and os.access(path, os.X_OK) @@ -1034,8 +1055,35 @@ def is_exe(path): %(func_src)s %(func_name)s(*%(func_args)r) -os.execve(exe, argv, env) -""" % locals() # """ +""" % locals() + + if len(argv) > 0 and len(argv[0]) > 0: + script += r"os.execve(exe, argv, env) " + + # os.execve does not allow us to pass empty argv[0] + # Therefore we use ctypes to call execve directly + else: + script += r""" +# Transform envp from dict to list +env_list = [key + b"=" + value for key, value in env.items()] + +# ctypes helper to convert a python list to a NULL-terminated C array +def to_carray(py_list): + py_list += [None] # NULL-terminated + return (ctypes.c_char_p * len(py_list))(*py_list) + +c_argv = to_carray(argv) +c_env = to_carray(env_list) + +# Call execve +libc = ctypes.CDLL('libc.so.6') +libc.execve(exe, c_argv, c_env) + +# We should never get here, since we sanitized argv and env, +# but just in case, indicate that something went wrong. +libc.perror(b"execve") +raise OSError("execve failed") +""" % locals() script = script.strip() @@ -1054,7 +1102,7 @@ def is_exe(path): execve_repr = "execve(%r, %s, %s)" % (executable, argv, 'os.environ' - if (env in (None, os.environ)) + if (env in (None, getattr(os, "environb", os.environ))) else env) # Avoid spamming the screen if self.isEnabledFor(logging.DEBUG) and len(execve_repr) > 512: @@ -1458,14 +1506,14 @@ def update(has, total): with open(local, 'wb') as fd: fd.write(data) - def _download_to_cache(self, remote, p): + def _download_to_cache(self, remote, p, fingerprint=True): with context.local(log_level='error'): remote = self.readlink('-f',remote) if not hasattr(remote, 'encode'): remote = remote.decode('utf-8') - fingerprint = self._get_fingerprint(remote) + fingerprint = fingerprint and self._get_fingerprint(remote) or None if fingerprint is None: local = os.path.normpath(remote) local = os.path.basename(local) @@ -1487,7 +1535,7 @@ def _download_to_cache(self, remote, p): return local - def download_data(self, remote): + def download_data(self, remote, fingerprint=True): """Downloads a file from the remote server and returns it as a string. Arguments: @@ -1508,7 +1556,7 @@ def download_data(self, remote): """ with self.progress('Downloading %r' % remote) as p: - with open(self._download_to_cache(remote, p), 'rb') as fd: + with open(self._download_to_cache(remote, p, fingerprint), 'rb') as fd: return fd.read() def download_file(self, remote, local = None): @@ -2100,6 +2148,57 @@ def preexec(): return self._aslr_ulimit + def _cpuinfo(self): + if self._cpuinfo_cache is None: + with context.quiet: + try: + self._cpuinfo_cache = self.download_data('/proc/cpuinfo', fingerprint=False) + except PwnlibException: + self._cpuinfo_cache = b'' + return self._cpuinfo_cache + + @property + def user_shstk(self): + """:class:`bool`: Whether userspace shadow stack is supported on the system. + + Example: + + >>> s = ssh("travis", "example.pwnme") + >>> s.user_shstk + False + """ + if self._user_shstk is None: + if self.os != 'linux': + self.warn_once("Only Linux is supported for userspace shadow stack checks.") + self._user_shstk = False + + else: + cpuinfo = self._cpuinfo() + + self._user_shstk = b' user_shstk' in cpuinfo + return self._user_shstk + + @property + def ibt(self): + """:class:`bool`: Whether kernel indirect branch tracking is supported on the system. + + Example: + + >>> s = ssh("travis", "example.pwnme") + >>> s.ibt + False + """ + if self._ibt is None: + if self.os != 'linux': + self.warn_once("Only Linux is supported for kernel indirect branch tracking checks.") + self._ibt = False + + else: + cpuinfo = self._cpuinfo() + + self._ibt = b' ibt ' in cpuinfo or b' ibt\n' in cpuinfo + return self._ibt + def _checksec_cache(self, value=None): path = self._get_cachefile('%s-%s' % (self.host, self.port)) @@ -2136,7 +2235,15 @@ def checksec(self, banner=True): "ASLR:".ljust(10) + { True: green("Enabled"), False: red("Disabled") - }[self.aslr] + }[self.aslr], + "SHSTK:".ljust(10) + { + True: green("Enabled"), + False: red("Disabled") + }[self.user_shstk], + "IBT:".ljust(10) + { + True: green("Enabled"), + False: red("Disabled") + }[self.ibt], ] if self.aslr_ulimit: diff --git a/pwnlib/tubes/tube.py b/pwnlib/tubes/tube.py index 153112989..21a312f15 100644 --- a/pwnlib/tubes/tube.py +++ b/pwnlib/tubes/tube.py @@ -1034,8 +1034,13 @@ def clean_and_log(self, timeout = 0.05): b'hooray_data' >>> context.clear() """ + cached_data = self.buffer.get() + if cached_data and not self.isEnabledFor(logging.DEBUG): + with context.local(log_level='debug'): + self.debug('Received %#x bytes:' % len(cached_data)) + self.maybe_hexdump(cached_data, level=logging.DEBUG) with context.local(log_level='debug'): - return self.clean(timeout) + return cached_data + self.clean(timeout) def connect_input(self, other): """connect_input(other) diff --git a/pwnlib/util/misc.py b/pwnlib/util/misc.py index 7fbf479ca..f0ee62d96 100644 --- a/pwnlib/util/misc.py +++ b/pwnlib/util/misc.py @@ -229,6 +229,10 @@ def normalize_argv_env(argv, env, log, level=2): for k,v in env_items: if not isinstance(k, (bytes, six.text_type)): log.error('Environment keys must be strings: %r' % k) + # Check if = is in the key, Required check since we sometimes call ctypes.execve directly + # https://github.com/python/cpython/blob/025995feadaeebeef5d808f2564f0fd65b704ea5/Modules/posixmodule.c#L6476 + if b'=' in packing._encode(k): + log.error('Environment keys may not contain "=": %r' % (k)) if not isinstance(v, (bytes, six.text_type)): log.error('Environment values must be strings: %r=%r' % (k,v)) k = packing._need_bytes(k, level, 0x80) # ASCII text is okay @@ -382,7 +386,7 @@ def run_in_new_terminal(command, terminal=None, args=None, kill_at_exit=True, pr import os os.execve({argv0!r}, {argv!r}, os.environ) ''' - script = script.format(executable=sys.executable, + script = script.format(executable='/bin/env ' * (' ' in sys.executable) + sys.executable, argv=command, argv0=which(command[0])) script = script.lstrip() diff --git a/pwnlib/version.py b/pwnlib/version.py index 14e628018..94955abb8 100644 --- a/pwnlib/version.py +++ b/pwnlib/version.py @@ -1 +1 @@ -__version__ = '4.12.0dev' +__version__ = '4.13.0dev' diff --git a/pyproject.toml b/pyproject.toml index 57b695211..b65e73bf7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,8 +36,8 @@ requires-python = ">=2.7" dependencies = [ "paramiko>=1.15.2", "mako>=1.0.0", - "pyelftools>=0.24, <0.30; python_version < '3'", - "pyelftools>=0.24; python_version >= '3'", + "pyelftools>=0.29, <0.30; python_version < '3'", + "pyelftools>=0.29; python_version >= '3'", "capstone>=3.0.5rc2", # see Gallopsled/pwntools#971, Gallopsled/pwntools#1160 "ropgadget>=5.3", "pyserial>=2.7", diff --git a/setup.py b/setup.py index 8dde71f7a..11ecba6db 100755 --- a/setup.py +++ b/setup.py @@ -3,14 +3,12 @@ import glob import os -import platform -import subprocess import sys -import traceback from distutils.command.install import INSTALL_SCHEMES from distutils.sysconfig import get_python_inc from distutils.util import convert_path +from setuptools import find_packages from setuptools import setup # Get all template files @@ -50,6 +48,7 @@ import toml project = toml.load('pyproject.toml')['project'] + compat['packages'] = find_packages() compat['install_requires'] = project['dependencies'] compat['name'] = project['name'] # https://github.com/pypa/pip/issues/7953 @@ -64,7 +63,7 @@ sys.exit(-1) setup( - version = '4.12.0dev', + version = '4.13.0dev', data_files = [('pwntools-doc', glob.glob('*.md') + glob.glob('*.txt')), ],