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 17c1a2071..9c2a91d1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ The table below shows which release corresponds to each branch, and what date th | ---------------- | -------- | ---------------------- | | [4.13.0](#4130-dev) | `dev` | | [4.12.0](#4120-beta) | `beta` | -| [4.11.0](#4110-stable) | `stable` | Sep 15, 2023 +| [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 @@ -69,9 +70,14 @@ The table below shows which release corresponds to each branch, and what date th ## 4.13.0 (`dev`) +- [#2281][2281] FIX: Getting right amount of data for search fix +- [#2293][2293] Add x86 CET status to checksec output +[2281]: https://github.com/Gallopsled/pwntools/pull/2281 +[2293]: https://github.com/Gallopsled/pwntools/pull/2293 ## 4.12.0 (`beta`) + - [#2202][2202] Fix `remote` and `listen` in sagemath - [#2117][2117] Add -p (--prefix) and -s (--separator) arguments to `hex` command - [#2221][2221] Add shellcraft.sleep template wrapping SYS_nanosleep @@ -88,7 +94,21 @@ The table below shows which release corresponds to each branch, and what date th [2257]: https://github.com/Gallopsled/pwntools/pull/2257 [2225]: https://github.com/Gallopsled/pwntools/pull/2225 -## 4.11.0 (`stable`) +## 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 + +[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()` 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/elf/elf.py b/pwnlib/elf/elf.py index ebdf2d68f..5c41f0f12 100644 --- a/pwnlib/elf/elf.py +++ b/pwnlib/elf/elf.py @@ -53,6 +53,7 @@ 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, RelrRelocationSection from elftools.elf.sections import SymbolTableSection @@ -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 @@ -1211,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: @@ -2075,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. @@ -2132,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/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/ssh.py b/pwnlib/tubes/ssh.py index 2216da0af..9337b27c3 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) @@ -2144,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.read('/proc/cpuinfo') + 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)) @@ -2180,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 0b7fbf456..f0ee62d96 100644 --- a/pwnlib/util/misc.py +++ b/pwnlib/util/misc.py @@ -386,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/setup.py b/setup.py index ee37b6735..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