diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 76cae56a5..6cbbb7e3b 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -21,7 +21,7 @@ jobs: key: ${{ matrix.os }}-cache-pip - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -37,6 +37,7 @@ jobs: - name: Install Android AVD run: | + sudo usermod -aG kvm $USER source travis/setup_avd_fast.sh sed -i 's/skip_android = True/skip_android = False/' docs/source/conf.py set | grep ^PATH >.android.env diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3011b9427..120b171be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: - name: Set up Python ${{ matrix.python_version }} if: matrix.python_version != '2.7' - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python_version }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 52508ebae..9cc95fd00 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,7 +19,7 @@ jobs: key: ${{ matrix.os }}-cache-pip - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index b70becc94..a76794b0a 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -19,7 +19,7 @@ jobs: key: ${{ matrix.os }}-cache-pip - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 5b502e342..ea7cd7250 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -7,6 +7,9 @@ build: tools: python: "3" +sphinx: + configuration: docs/source/conf.py + python: install: - requirements: docs/requirements.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 2996706eb..06d16837c 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 @@ -68,11 +69,29 @@ The table below shows which release corresponds to each branch, and what date th | [2.2.0](#220) | | Jan 5, 2015 ## 4.13.0 (`dev`) -- [#2279][2279] Make pwn template always set context.binary +- [#2242][2242] Term module revamp: activating special handling of terminal only when necessary +- [#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 +- [#2307][2307] Fix `pwn libcdb file` crashing if "/bin/sh" string was not found +- [#2309][2309] Detect challenge binary and libc in `pwn template` +- [#2308][2308] Fix WinExec shellcraft to make sure it's 16 byte aligned +- [#2279][2279] Make `pwn template` always set context.binary + +[2242]: https://github.com/Gallopsled/pwntools/pull/2242 +[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 +[2307]: https://github.com/Gallopsled/pwntools/pull/2307 +[2309]: https://github.com/Gallopsled/pwntools/pull/2309 +[2308]: https://github.com/Gallopsled/pwntools/pull/2308 [2279]: https://github.com/Gallopsled/pwntools/pull/2279 ## 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 @@ -89,7 +108,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/docs/source/conf.py b/docs/source/conf.py index 8ce3ccad7..ee537d0db 100755 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -169,7 +169,7 @@ def __setattr__(self, name, value): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -265,8 +265,8 @@ def __setattr__(self, name, value): u'2016, Gallopsled et al.', 'manual'), ] -intersphinx_mapping = {'python': ('https://docs.python.org/3.8', None), - 'paramiko': ('https://paramiko-docs.readthedocs.org/en/2.1/', None)} +intersphinx_mapping = {'python': ('https://docs.python.org/3/', None), + 'paramiko': ('https://docs.paramiko.org/en/2.1/', None)} # The name of an image file (relative to this directory) to place at the top of # the title page. @@ -382,8 +382,6 @@ def linkcode_resolve(domain, info): html_theme_path = [alabaster.get_path()] html_theme_options = { 'nosidebar' : True } - # otherwise, readthedocs.org uses their theme by default, so no need to specify it - # -- Customization to Sphinx autodoc generation -------------------------------------------- import sphinx.ext.autodoc 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/examples/options.py b/examples/options.py index 84e3879ae..a30243f14 100644 --- a/examples/options.py +++ b/examples/options.py @@ -4,5 +4,5 @@ from pwn import * -opts = [string.letters[x] for x in range(10)] +opts = [string.ascii_letters[x] for x in range(12)] print('You choose "%s"' % opts[options('Pick one:', opts)]) 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/adb/adb.py b/pwnlib/adb/adb.py index e5d0434c7..84bb89213 100644 --- a/pwnlib/adb/adb.py +++ b/pwnlib/adb/adb.py @@ -66,6 +66,7 @@ from pwnlib.context import LocalContext from pwnlib.context import context from pwnlib.device import Device +from pwnlib.exception import PwnlibException from pwnlib.log import getLogger from pwnlib.protocols.adb import AdbClient from pwnlib.util.packing import _decode @@ -122,7 +123,7 @@ def current_device(any=False): >>> device = adb.current_device(any=True) >>> device # doctest: +ELLIPSIS - AdbDevice(serial='emulator-5554', type='device', port='emulator', product='sdk_...phone_armv7', model='sdk ...phone armv7', device='generic') + AdbDevice(serial='emulator-5554', type='device', port='emulator', product='sdk_...phone_...', model='...', device='generic...') >>> device.port 'emulator' """ @@ -252,13 +253,13 @@ class AdbDevice(Device): >>> device = adb.wait_for_device() >>> device.arch - 'arm' + 'amd64' >>> device.bits - 32 + 64 >>> device.os 'android' >>> device.product # doctest: +ELLIPSIS - 'sdk_...phone_armv7' + 'sdk_...phone_...' >>> device.serial 'emulator-5554' """ @@ -1364,7 +1365,7 @@ def compile(source): >>> filename = adb.compile(temp) >>> sent = adb.push(filename, "/data/local/tmp") >>> adb.process(sent).recvall() # doctest: +ELLIPSIS - b'... /system/bin/linker\n...' + b'... /system/lib64/libc.so\n...' """ ndk_build = misc.which('ndk-build') @@ -1490,8 +1491,9 @@ class Partitions(object): @context.quietfunc def by_name_dir(self): try: - return next(find('/dev/block/platform','by-name')) - except StopIteration: + with context.local(log_level=logging.FATAL): + return next(find('/dev/block/platform','by-name')) + except (StopIteration, PwnlibException): return '/dev/block' @context.quietfunc 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..30ee47f07 100644 --- a/pwnlib/commandline/libcdb.py +++ b/pwnlib/commandline/libcdb.py @@ -176,9 +176,13 @@ def translate_offset(offs, args, exe): return offs def collect_synthetic_symbols(exe): - available_symbols = ['str_bin_sh'] - exe.symbols['str_bin_sh'] = next(exe.search(b'/bin/sh\x00')) - + available_symbols = [] + try: + exe.symbols['str_bin_sh'] = next(exe.search(b'/bin/sh\x00')) + available_symbols.append('str_bin_sh') + except StopIteration: + pass + libc_start_main_return = exe.libc_start_main_return if libc_start_main_return > 0: exe.symbols['__libc_start_main_ret'] = libc_start_main_return @@ -221,7 +225,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..6e5f74655 --- 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 @@ -10,24 +9,54 @@ parser = common.parser_commands.add_parser( 'template', help = 'Generate an exploit template', - description = 'Generate an exploit template' + description = 'Generate an exploit template. If no arguments are given, ' + 'the current directory is searched for an executable binary and ' + 'libc. If only one binary is found, it is assumed to be the ' + 'challenge binary.' ) # change path to hardcoded one when building the documentation printable_data_path = "pwnlib/data" if 'sphinx' in sys.modules else pwnlib.data.path -parser.add_argument('exe', nargs='?', help='Target binary') +parser.add_argument('exe', nargs='?', help='Target binary. If not given, the current directory is searched for an executable binary.') parser.add_argument('--host', help='Remote host / SSH server') parser.add_argument('--port', help='Remote port / SSH port', type=int) parser.add_argument('--user', help='SSH Username') parser.add_argument('--pass', '--password', help='SSH Password', dest='password') -parser.add_argument('--libc', help='Path to libc binary to use') +parser.add_argument('--libc', help='Path to libc binary to use. If not given, the current directory is searched for a libc binary.') parser.add_argument('--path', help='Remote path of file on SSH server') parser.add_argument('--quiet', help='Less verbose template comments', action='store_true') parser.add_argument('--color', help='Print the output in color', choices=['never', 'always', 'auto'], default='auto') parser.add_argument('--template', help='Path to a custom template. Tries to use \'~/.config/pwntools/templates/pwnup.mako\', if it exists. ' 'Check \'%s\' for the default template shipped with pwntools.' % os.path.join(printable_data_path, "templates", "pwnup.mako")) +parser.add_argument('--no-auto', help='Do not automatically detect missing binaries', action='store_false', dest='auto') + +def detect_missing_binaries(args): + log.info("Automatically detecting challenge binaries...") + # look for challenge binary, libc, and ld in current directory + exe, libc, ld = args.exe, args.libc, None + other_files = [] + for filename in os.listdir(): + if not os.path.isfile(filename): + continue + if not libc and ('libc-' in filename or 'libc.' in filename): + libc = filename + elif not ld and 'ld-' in filename: + ld = filename + else: + if os.access(filename, os.X_OK): + other_files.append(filename) + if len(other_files) == 1: + exe = other_files[0] + elif len(other_files) > 1: + log.warning("Failed to find challenge binary. There are multiple binaries in the current directory: %s", other_files) + + if exe != args.exe: + log.success("Found challenge binary %r", exe) + if libc != args.libc: + log.success("Found libc binary %r", libc) + return exe, libc def main(args): @@ -45,19 +74,20 @@ def main(args): if not (args.path or args.exe): log.error("Must specify --path or a exe") - s = ssh(args.user, args.host, args.port or 22, args.password or None) - - try: - remote_file = args.path or args.exe - s.download(remote_file) - except Exception: - log.warning("Could not download file %r, opening a shell", remote_file) - s.interactive() - return + with ssh(args.user, args.host, args.port or 22, args.password or None) as s: + try: + remote_file = args.path or args.exe + s.download(remote_file) + except Exception: + log.warning("Could not download file %r, opening a shell", remote_file) + s.interactive() + return if not args.exe: args.exe = os.path.basename(args.path) + if args.auto and (args.exe is None or args.libc is None): + args.exe, args.libc = detect_missing_binaries(args) if args.template: template = Template(filename=args.template) # Failing on invalid file is ok 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/context/__init__.py b/pwnlib/context/__init__.py index 10a7a4f9c..507403e8b 100644 --- a/pwnlib/context/__init__.py +++ b/pwnlib/context/__init__.py @@ -787,7 +787,7 @@ def arch(self, arch): try: defaults = self.architectures[arch] except KeyError: - raise AttributeError('AttributeError: arch must be one of %r' % sorted(self.architectures)) + raise AttributeError('AttributeError: arch (%r) must be one of %r' % (arch, sorted(self.architectures))) for k,v in defaults.items(): if k not in self._tls: diff --git a/pwnlib/data/elf/test-riscv64 b/pwnlib/data/elf/test-riscv64 new file mode 100755 index 000000000..2f54f95f0 Binary files /dev/null and b/pwnlib/data/elf/test-riscv64 differ diff --git a/pwnlib/data/elf/test-riscv64-pie b/pwnlib/data/elf/test-riscv64-pie new file mode 100755 index 000000000..d9c3217cb Binary files /dev/null and b/pwnlib/data/elf/test-riscv64-pie differ diff --git a/pwnlib/data/elf/test-riscv64-relro b/pwnlib/data/elf/test-riscv64-relro new file mode 100755 index 000000000..8b624fe7b Binary files /dev/null and b/pwnlib/data/elf/test-riscv64-relro differ diff --git a/pwnlib/data/elf/test-riscv64-relro-pie b/pwnlib/data/elf/test-riscv64-relro-pie new file mode 100755 index 000000000..074be1ab0 Binary files /dev/null and b/pwnlib/data/elf/test-riscv64-relro-pie differ diff --git a/pwnlib/data/elf/test-x32 b/pwnlib/data/elf/test-x32 new file mode 100644 index 000000000..7c07e4df9 Binary files /dev/null and b/pwnlib/data/elf/test-x32 differ diff --git a/pwnlib/data/elf/test-x32-pie b/pwnlib/data/elf/test-x32-pie new file mode 100644 index 000000000..454b21236 Binary files /dev/null and b/pwnlib/data/elf/test-x32-pie differ diff --git a/pwnlib/data/elf/test-x32-relro b/pwnlib/data/elf/test-x32-relro new file mode 100644 index 000000000..d8de7430d Binary files /dev/null and b/pwnlib/data/elf/test-x32-relro differ diff --git a/pwnlib/data/elf/test-x32-relro-pie b/pwnlib/data/elf/test-x32-relro-pie new file mode 100644 index 000000000..67bd00e72 Binary files /dev/null and b/pwnlib/data/elf/test-x32-relro-pie differ diff --git a/pwnlib/elf/corefile.py b/pwnlib/elf/corefile.py index b4d0e9dc7..02ac36ebf 100644 --- a/pwnlib/elf/corefile.py +++ b/pwnlib/elf/corefile.py @@ -93,7 +93,6 @@ from pwnlib.util.fiddling import unhex from pwnlib.util.misc import read from pwnlib.util.misc import write -from pwnlib.util.packing import _decode from pwnlib.util.packing import pack from pwnlib.util.packing import unpack_many @@ -106,44 +105,11 @@ 'aarch64': elf_prstatus_aarch64 } -prpsinfo_types = { - 32: elf_prpsinfo_32, - 64: elf_prpsinfo_64, -} - siginfo_types = { 32: elf_siginfo_32, 64: elf_siginfo_64 } -# Slightly modified copy of the pyelftools version of the same function, -# until they fix this issue: -# https://github.com/eliben/pyelftools/issues/93 -def iter_notes(self): - """ Iterates the list of notes in the segment. - """ - offset = self['p_offset'] - end = self['p_offset'] + self['p_filesz'] - while offset < end: - note = struct_parse( - self.elffile.structs.Elf_Nhdr, - self.stream, - stream_pos=offset) - note['n_offset'] = offset - offset += self.elffile.structs.Elf_Nhdr.sizeof() - self.stream.seek(offset) - # n_namesz is 4-byte aligned. - disk_namesz = roundup(note['n_namesz'], 2) - with context.local(encoding='latin-1'): - note['n_name'] = _decode( - CString('').parse(self.stream.read(disk_namesz))) - offset += disk_namesz - - desc_data = _decode(self.stream.read(note['n_descsz'])) - note['n_desc'] = desc_data - offset += roundup(note['n_descsz'], 2) - note['n_size'] = offset - note['n_offset'] - yield note class Mapping(object): """Encapsulates information about a memory mapping in a :class:`Corefile`. @@ -605,7 +571,6 @@ def __init__(self, *a, **kw): log.warn_once("%s does not use a supported corefile architecture, registers are unavailable" % self.file.name) prstatus_type = prstatus_types.get(self.arch) - prpsinfo_type = prpsinfo_types.get(self.bits) siginfo_type = siginfo_types.get(self.bits) with log.waitfor("Parsing corefile...") as w: @@ -616,39 +581,30 @@ def __init__(self, *a, **kw): continue - # Note that older versions of pyelftools (<=0.24) are missing enum values - # for NT_PRSTATUS, NT_PRPSINFO, NT_AUXV, etc. - # For this reason, we have to check if note.n_type is any of several values. - for note in iter_notes(segment): - if not isinstance(note.n_desc, bytes): - note['n_desc'] = note.n_desc.encode('latin1') + for note in segment.iter_notes(): # Try to find NT_PRSTATUS. - if prstatus_type and \ - note.n_descsz == ctypes.sizeof(prstatus_type) and \ - note.n_type in ('NT_GNU_ABI_TAG', 'NT_PRSTATUS'): + if note.n_type == 'NT_PRSTATUS': self.NT_PRSTATUS = note self.prstatus = prstatus_type.from_buffer_copy(note.n_desc) # Try to find NT_PRPSINFO - if prpsinfo_type and \ - note.n_descsz == ctypes.sizeof(prpsinfo_type) and \ - note.n_type in ('NT_GNU_ABI_TAG', 'NT_PRPSINFO'): + if note.n_type == 'NT_PRPSINFO': self.NT_PRPSINFO = note - self.prpsinfo = prpsinfo_type.from_buffer_copy(note.n_desc) + self.prpsinfo = note.n_desc # Try to find NT_SIGINFO so we can see the fault - if note.n_type in (0x53494749, 'NT_SIGINFO'): + if note.n_type == 'NT_SIGINFO': self.NT_SIGINFO = note self.siginfo = siginfo_type.from_buffer_copy(note.n_desc) # Try to find the list of mapped files - if note.n_type in (constants.NT_FILE, 'NT_FILE'): + if note.n_type == 'NT_FILE': with context.local(bytes=self.bytes): self._parse_nt_file(note) # Try to find the auxiliary vector, which will tell us # where the top of the stack is. - if note.n_type in (constants.NT_AUXV, 'NT_AUXV'): + if note.n_type == 'NT_AUXV': self.NT_AUXV = note with context.local(bytes=self.bytes): self._parse_auxv(note) @@ -684,31 +640,16 @@ def __init__(self, *a, **kw): self._describe_core() def _parse_nt_file(self, note): - t = tube() - t.unrecv(note.n_desc) - - count = t.unpack() - page_size = t.unpack() - starts = [] addresses = {} - for i in range(count): - start = t.unpack() - end = t.unpack() - offset = t.unpack() - starts.append((start, offset)) - - for i in range(count): - filename = t.recvuntil(b'\x00', drop=True) + for vma, filename in zip(note.n_desc.Elf_Nt_File_Entry, note.n_desc.filename): if not isinstance(filename, str): - filename = filename.decode('utf-8') - (start, offset) = starts[i] - + filename = filename.decode('utf-8', 'surrogateescape') for mapping in self.mappings: - if mapping.start == start: + if mapping.start == vma.vm_start: mapping.name = filename - mapping.page_offset = offset + mapping.page_offset = vma.page_offset self.mappings = sorted(self.mappings, key=lambda m: m.start) diff --git a/pwnlib/elf/datatypes.py b/pwnlib/elf/datatypes.py index f3989e48d..0dffaf7f5 100644 --- a/pwnlib/elf/datatypes.py +++ b/pwnlib/elf/datatypes.py @@ -631,29 +631,6 @@ class Elf64_auxv_t(ctypes.Structure): _fields_ = [('a_type', ctypes.c_uint64), ('a_val', ctypes.c_uint64),] -def generate_prpsinfo(long): - return [ - ('pr_state', byte), - ('pr_sname', char), - ('pr_zomb', byte), - ('pr_nice', byte), - ('pr_flag', long), - ('pr_uid', ctypes.c_ushort), - ('pr_gid', ctypes.c_ushort), - ('pr_pid', ctypes.c_int), - ('pr_ppid', ctypes.c_int), - ('pr_pgrp', ctypes.c_int), - ('pr_sid', ctypes.c_int), - ('pr_fname', char * 16), - ('pr_psargs', char * 80) - ] - -class elf_prpsinfo_32(ctypes.Structure): - _fields_ = generate_prpsinfo(Elf32_Addr) - -class elf_prpsinfo_64(ctypes.Structure): - _fields_ = generate_prpsinfo(Elf64_Addr) - def generate_siginfo(int_t, long_t): class siginfo_t(ctypes.Structure): _fields_ = [('si_signo', int_t), diff --git a/pwnlib/elf/elf.py b/pwnlib/elf/elf.py index 96244af8f..4751975b3 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 @@ -459,6 +460,7 @@ def _describe(self, *a, **kw): def get_machine_arch(self): return { ('EM_X86_64', 64): 'amd64', + ('EM_X86_64', 32): 'amd64', # x32 ABI ('EM_386', 32): 'i386', ('EM_486', 32): 'i386', ('EM_ARM', 32): 'arm', @@ -510,6 +512,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 +942,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 +972,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 +1236,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 +2101,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 +2164,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/log.py b/pwnlib/log.py index 4e04997ff..317dde44a 100644 --- a/pwnlib/log.py +++ b/pwnlib/log.py @@ -560,7 +560,7 @@ def emit(self, record): # we enrich the `Progress` object to keep track of the spinner if not hasattr(progress, '_spinner_handle'): - spinner_handle = term.output('') + spinner_handle = term.output('[x] ') msg_handle = term.output(msg) stop = threading.Event() def spin(): diff --git a/pwnlib/py2compat.py b/pwnlib/py2compat.py new file mode 100644 index 000000000..8584adf43 --- /dev/null +++ b/pwnlib/py2compat.py @@ -0,0 +1,94 @@ +""" +Compatibility layer with python 2, allowing us to write normal code. +Beware, some monkey-patching is done. +""" + +import os +import shutil +import sys +try: + import fcntl + import termios +except ImportError: + pass + +from collections import namedtuple +from struct import Struct + +def py2_monkey_patch(module): + def decorator(f): + if sys.version_info < (3,): + f.__module__ = module.__name__ + setattr(module, f.__name__, f) + return decorator + +# python3 -c 'import shutil,inspect; print(inspect.getsource(shutil.get_terminal_size))' +@py2_monkey_patch(shutil) +def get_terminal_size(fallback=(80, 24)): + """Get the size of the terminal window. + + For each of the two dimensions, the environment variable, COLUMNS + and LINES respectively, is checked. If the variable is defined and + the value is a positive integer, it is used. + + When COLUMNS or LINES is not defined, which is the common case, + the terminal connected to sys.__stdout__ is queried + by invoking os.get_terminal_size. + + If the terminal size cannot be successfully queried, either because + the system doesn't support querying, or because we are not + connected to a terminal, the value given in fallback parameter + is used. Fallback defaults to (80, 24) which is the default + size used by many terminal emulators. + + The value returned is a named tuple of type os.terminal_size. + """ + # columns, lines are the working values + try: + columns = int(os.environ['COLUMNS']) + except (KeyError, ValueError): + columns = 0 + + try: + lines = int(os.environ['LINES']) + except (KeyError, ValueError): + lines = 0 + + # only query if necessary + if columns <= 0 or lines <= 0: + try: + size = os.get_terminal_size(sys.__stdout__.fileno()) + except (AttributeError, ValueError, IOError): + # stdout is None, closed, detached, or not a terminal, or + # os.get_terminal_size() is unsupported + size = os.terminal_size(fallback) + if columns <= 0: + columns = size.columns + if lines <= 0: + lines = size.lines + + return os.terminal_size((columns, lines)) + +@py2_monkey_patch(os) +class terminal_size(tuple): + @property + def columns(self): + return self[0] + + @property + def lines(self): + return self[1] + + def __repr__(self): + return 'os.terminal_size(columns=%r, lines=%r)' % self + +terminal_size = namedtuple('terminal_size', 'columns lines') + +termsize = Struct('HHHH') + +@py2_monkey_patch(os) +def get_terminal_size(fd): # pylint: disable=function-redefined + arr = b'\0' * termsize.size + arr = fcntl.ioctl(fd, termios.TIOCGWINSZ, arr) + lines, columns, xpixel, ypixel = termsize.unpack(arr) + return os.terminal_size((columns, lines)) 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/shellcraft/templates/amd64/windows/winexec.asm b/pwnlib/shellcraft/templates/amd64/windows/winexec.asm index d6805201b..eb82eb433 100644 --- a/pwnlib/shellcraft/templates/amd64/windows/winexec.asm +++ b/pwnlib/shellcraft/templates/amd64/windows/winexec.asm @@ -7,15 +7,24 @@ Args: cmd (str): The program to execute. + cmd_show (int): nCmdShow parameter. -<%page args="cmd"/> +<%page args="cmd, cmd_show = 0"/> <% cmd = _need_bytes(cmd) +stack_frame = 0x30 + align(8, len(cmd)+1) +stack_frame_align = 8 & ~stack_frame %> ${amd64.windows.getprocaddress(b'WinExec', b'kernel32.dll', 'rsi')} ${amd64.pushstr(cmd)} mov rcx, rsp - sub rsp, 0x30 + sub rsp, ${pretty(0x30 + stack_frame_align)} + ${amd64.mov('rdx', cmd_show)} call rsi - add rsp, ${pretty(0x30+align(8, len(cmd)))} +% if stack_frame + stack_frame_align < 0x80: + add rsp, ${pretty(stack_frame + stack_frame_align)} +% else: + ${amd64.mov('rcx', stack_frame + stack_frame_align)} + add rsp, rcx +% endif diff --git a/pwnlib/term/__init__.py b/pwnlib/term/__init__.py index 366773583..b6be1fe45 100644 --- a/pwnlib/term/__init__.py +++ b/pwnlib/term/__init__.py @@ -12,9 +12,10 @@ from pwnlib.term import text # Re-exports (XXX: Are these needed?) -output = term.output -width = term.width +term.update_geometry() +width = term.width height = term.height +output = term.output getkey = key.get Keymap = keymap.Keymap diff --git a/pwnlib/term/key.py b/pwnlib/term/key.py index 211eb9363..e21477a46 100644 --- a/pwnlib/term/key.py +++ b/pwnlib/term/key.py @@ -10,6 +10,7 @@ from pwnlib.term import keyconsts as kc from pwnlib.term import termcap +from pwnlib.term import term __all__ = ['getch', 'getraw', 'get', 'unget'] @@ -21,11 +22,15 @@ except Exception: _fd = os.open(os.devnull, os.O_RDONLY) def getch(timeout = 0): + term.setupterm() while True: try: rfds, _wfds, _xfds = select.select([_fd], [], [], timeout) if rfds: - c = os.read(_fd, 1) + with term.rlock: + rfds, _wfds, _xfds = select.select([_fd], [], [], 0) + if not rfds: continue + c = os.read(_fd, 1) return ord(c) if c else None else: return None diff --git a/pwnlib/term/readline.py b/pwnlib/term/readline.py index b60b656de..d47b178a4 100644 --- a/pwnlib/term/readline.py +++ b/pwnlib/term/readline.py @@ -3,6 +3,7 @@ from __future__ import division from __future__ import print_function +import io import six import sys @@ -406,17 +407,20 @@ def readline(_size=-1, prompt='', float=True, priority=10): history.insert(0, buffer) return force_to_bytes(buffer) except KeyboardInterrupt: - control_c() + do_raise = False + try: + control_c() + except KeyboardInterrupt: + do_raise = True + if do_raise: + raise finally: line = buffer_left + buffer_right + '\n' buffer_handle.update(line) - buffer_handle.freeze() buffer_handle = None if prompt_handle: - prompt_handle.freeze() prompt_handle = None if suggest_handle: - suggest_handle.freeze() suggest_handle = None if shutdown_hook: shutdown_hook() @@ -484,7 +488,10 @@ class Wrapper: def __init__(self, fd): self._fd = fd def readline(self, size = None): - return readline(size) + r = readline(size) + if isinstance(self._fd, io.TextIOWrapper): + r = r.decode(encoding=self._fd.encoding, errors=self._fd.errors) + return r def __getattr__(self, k): return getattr(self._fd, k) sys.stdin = Wrapper(sys.stdin) diff --git a/pwnlib/term/term.py b/pwnlib/term/term.py index 5ed4fe07d..d8062bfa5 100644 --- a/pwnlib/term/term.py +++ b/pwnlib/term/term.py @@ -1,42 +1,69 @@ from __future__ import absolute_import from __future__ import division +from __future__ import unicode_literals import atexit import errno import os import re +import shutil import signal -import six import struct import sys import threading import traceback +import weakref if sys.platform != 'win32': import fcntl import termios -from pwnlib.context import ContextType -from pwnlib.term import termcap +from ..context import ContextType +from . import termcap +from .. import py2compat __all__ = ['output', 'init'] # we assume no terminal can display more lines than this MAX_TERM_HEIGHT = 200 -# default values -width = 80 -height = 25 - # list of callbacks triggered on SIGWINCH on_winch = [] - - +cached_pos = None settings = None -_graphics_mode = False +setup_done = False +epoch = 0 fd = sys.stdout +winchretry = False +rlock = threading.RLock() + +class WinchLock(object): + def __init__(self): + self.guard = threading.RLock() + self.lock = threading.Lock() + + @property + def acquire(self): + return self.lock.acquire + + @property + def release(self): + return self.lock.release + + def __enter__(self): + self.guard.acquire() + return self.lock.__enter__() + def __exit__(self, tp, val, tb): + try: + return self.lock.__exit__(tp, val, tb) + finally: + if winchretry: + handler_sigwinch(signal.SIGWINCH, None) + self.guard.release() + +wlock = WinchLock() def show_cursor(): do('cnorm') @@ -46,102 +73,74 @@ def hide_cursor(): def update_geometry(): global width, height - hw = fcntl.ioctl(fd.fileno(), termios.TIOCGWINSZ, '1234') - h, w = struct.unpack('hh', hw) - # if the window shrunk and theres still free space at the bottom move - # everything down - if h < height and scroll == 0: - if cells and cells[-1].end[0] < 0: - delta = min(height - h, 1 - cells[-1].end[0]) - for cell in cells: - cell.end = (cell.end[0] + delta, cell.end[1]) - cell.start = (cell.start[0] + delta, cell.start[1]) - height, width = h, w + width, height = shutil.get_terminal_size() def handler_sigwinch(signum, stack): - if hasattr(signal, 'pthread_sigmask'): - signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGWINCH}) - update_geometry() - redraw() - for cb in on_winch: - cb() - if hasattr(signal, 'pthread_sigmask'): - signal.pthread_sigmask(signal.SIG_UNBLOCK, {signal.SIGWINCH}) + global cached_pos, winchretry + with wlock.guard: + while True: + if not wlock.acquire(False): + winchretry = True + return + + winchretry = False + update_geometry() + for cb in on_winch: + cb() + wlock.release() + if not winchretry: break + def handler_sigstop(signum, stack): resetterm() - os.kill(os.getpid(), signal.SIGSTOP) + os.kill(0, signal.SIGSTOP) def handler_sigcont(signum, stack): - setupterm() - redraw() + global epoch, cached_pos, setup_done + epoch += 1 + cached_pos = None + setup_done = False def setupterm(): - global settings - update_geometry() + global settings, setup_done + if setup_done: + return + setup_done = True hide_cursor() + update_geometry() do('smkx') # keypad mode + mode = termios.tcgetattr(fd) + IFLAG, OFLAG, CFLAG, LFLAG, ISPEED, OSPEED, CC = range(7) if not settings: - settings = termios.tcgetattr(fd.fileno()) - mode = termios.tcgetattr(fd.fileno()) - IFLAG = 0 - OFLAG = 1 - CFLAG = 2 - LFLAG = 3 - ISPEED = 4 - OSPEED = 5 - CC = 6 - mode[LFLAG] = mode[LFLAG] & ~(termios.ECHO | termios.ICANON | termios.IEXTEN) + settings = mode[:] + settings[CC] = settings[CC][:] + mode[LFLAG] &= ~(termios.ECHO | termios.ICANON | termios.IEXTEN) mode[CC][termios.VMIN] = 1 mode[CC][termios.VTIME] = 0 - termios.tcsetattr(fd, termios.TCSAFLUSH, mode) + termios.tcsetattr(fd, termios.TCSADRAIN, mode) + fd.flush() def resetterm(): + global settings, setup_done if settings: - termios.tcsetattr(fd.fileno(), termios.TCSADRAIN, settings) - show_cursor() - do('rmkx') - fd.write(' \x08') # XXX: i don't know why this is needed... - # only necessary when suspending the process + termios.tcsetattr(fd, termios.TCSADRAIN, settings) + settings = None + if setup_done: + setup_done = False + show_cursor() + do('rmkx') + fd.flush() def init(): atexit.register(resetterm) - setupterm() signal.signal(signal.SIGWINCH, handler_sigwinch) signal.signal(signal.SIGTSTP, handler_sigstop) signal.signal(signal.SIGCONT, handler_sigcont) - # we start with one empty cell at the current cursor position - put('\x1b[6n') - fd.flush() - s = '' - while True: - try: - c = os.read(fd.fileno(), 1) - except OSError as e: - if e.errno != errno.EINTR: - raise - continue - if not isinstance(c, six.string_types): - c = c.decode('utf-8') - s += c - if c == 'R': - break - row, col = re.findall('\x1b' + r'\[(\d*);(\d*)R', s)[0] - row = int(row) - height - col = int(col) - 1 - cell = Cell() - cell.start = (row, col) - cell.end = (row, col) - cell.content = [] - cell.frozen = True - cell.float = 0 - cell.indent = 0 - cells.append(cell) class Wrapper: def __init__(self, fd): self._fd = fd def write(self, s): - output(s, frozen = True) + return output(s, frozen=True) def __getattr__(self, k): return getattr(self._fd, k) if sys.stdout.isatty(): @@ -156,408 +155,272 @@ def __getattr__(self, k): # freeze all cells if an exception is thrown orig_hook = sys.excepthook def hook(*args): + sys.stderr = sys.__stderr__ resetterm() - for c in cells: - c.frozen = True - c.float = 0 + cells.clear() if orig_hook: orig_hook(*args) else: traceback.print_exception(*args) - # this is a bit esoteric - # look here for details: https://stackoverflow.com/questions/12790328/how-to-silence-sys-excepthook-is-missing-error - if fd.fileno() == 2: - os.close(fd.fileno()) sys.excepthook = hook +tmap = {c: '\\x{:02x}'.format(c) for c in set(range(0x20)) - {0x09, 0x0a, 0x0d, 0x1b} | {0x7f}} + def put(s): - if not isinstance(s, six.string_types): - s = s.decode('utf-8') - fd.write(s) + global cached_pos, epoch + s = s.translate(tmap) + if cached_pos: + it = iter(s.replace('\n', '\r\n')) + sanit_s = '' + for c in it: + if c == '\r': + cached_pos[1] = 0 + elif c == '\n': + cached_pos[0] += 1 + elif c == '\t': + cached_pos[1] = (cached_pos[1] + 8) & -8 + elif c in '\x1b\u009b': # ESC or CSI + seq = c + for c in it: + seq += c + if c not in '[0123456789;': + break + else: + # unterminated ctrl seq, just print it visually + c = seq.replace('\x1b', r'\x1b').replace('\u009b', r'\u009b') + cached_pos[1] += len(c) -def flush(): fd.flush() + # if '\e[123;123;123;123m' then nothing + if c == 'm': + c = seq + else: + # undefined ctrl seq, just print it visually + c = seq.replace('\x1b', r'\x1b').replace('\u009b', r'\u009b') + cached_pos[1] += len(c) + elif c < ' ': + assert False, 'impossible ctrl char' + else: + # normal character, nothing to see here + cached_pos[1] += 1 + sanit_s += c + else: + s = sanit_s.replace('\r\n', '\n') + return fd.write(s) def do(c, *args): s = termcap.get(c, *args) if s: - put(s) + fd.write(s.decode('utf-8')) + +def goto(rc): + global cached_pos + r, c = rc + nowr, nowc = cached_pos or (None, None) + cached_pos = [r, c] + # common cases: we can just go up/down a couple rows + if c == 0: + if r == nowr + 1: + fd.write('\n') + return + if c != nowc: + fd.write('\r') + elif c != nowc: + do('hpa', c) -def goto(r, c): - do('cup', r - scroll + height - 1, c) + if r == nowr - 1: + do('cuu1') + elif r < nowr: + do('cuu', nowr - r) + elif r > nowr: + do('cud', r - nowr) -cells = [] -scroll = 0 class Cell(object): - pass - -class Handle: - def __init__(self, cell, is_floating): - self.h = id(cell) - self.is_floating = is_floating - def update(self, s): - update(self.h, s) - def freeze(self): - freeze(self.h) - def delete(self): - delete(self.h) - -STR, CSI, LF, BS, CR, SOH, STX, OOB = range(8) -def parse_csi(buf, offset): - i = offset - while i < len(buf): - c = buf[i] - if c >= 0x40 and c < 0x80: - break - i += 1 - if i >= len(buf): - return - end = i - cmd = [c, None, None] - i = offset - in_num = False - args = [] - if buf[i] >= ord('<') and buf[i] <= ord('?'): - cmd[1] = buf[i] - i += 1 - while i < end: - c = buf[i] - if c >= ord('0') and c <= ord('9'): - if not in_num: - args.append(c - ord('0')) - in_num = True - else: - args[-1] = args[-1] * 10 + c - ord('0') - elif c == ord(';'): - if not in_num: - args.append(None) - in_num = False - if len(args) > 16: - break - elif c >= 0x20 and c <= 0x2f: - cmd[2] = c - break - i += 1 - return cmd, args, end + 1 - -def parse_utf8(buf, offset): - c0 = buf[offset] - n = 0 - if c0 & 0b11100000 == 0b11000000: - n = 2 - elif c0 & 0b11110000 == 0b11100000: - n = 3 - elif c0 & 0b11111000 == 0b11110000: - n = 4 - elif c0 & 0b11111100 == 0b11111000: - n = 5 - elif c0 & 0b11111110 == 0b11111100: - n = 6 - if n: - return offset + n - -def parse(s): - global _graphics_mode - if isinstance(s, six.text_type): - s = s.encode('utf8') - out = [] - buf = bytearray(s) - i = 0 - while i < len(buf): - x = None - c = buf[i] - if c >= 0x20 and c <= 0x7e: - x = (STR, [six.int2byte(c)]) - i += 1 - elif c & 0xc0: - j = parse_utf8(buf, i) - if j: - x = (STR, [b''.join(map(six.int2byte, buf[i : j]))]) - i = j - elif c == 0x1b and len(buf) > i + 1: - c1 = buf[i + 1] - if c1 == ord('['): - ret = parse_csi(buf, i + 2) - if ret: - cmd, args, j = ret - x = (CSI, (cmd, args, b''.join(map(six.int2byte, buf[i : j])))) - i = j - elif c1 == ord(']'): - # XXX: this is a dirty hack: - # we still need to do our homework on this one, but what we do - # here is supporting setting the terminal title and updating - # the color map. we promise to do it properly in the next - # iteration of this terminal emulation/compatibility layer - # related: https://unix.stackexchange.com/questions/5936/can-i-set-my-local-machines-terminal-colors-to-use-those-of-the-machine-i-ssh-i - try: - j = s.index('\x07', i) - except Exception: - try: - j = s.index('\x1b\\', i) - except Exception: - j = 1 - x = (OOB, s[i:j + 1]) - i = j + 1 - elif c1 in map(ord, '()'): # select G0 or G1 - i += 3 - continue - elif c1 in map(ord, '>='): # set numeric/application keypad mode - i += 2 - continue - elif c1 == ord('P'): - _graphics_mode = True - i += 2 - continue - elif c1 == ord('\\'): - _graphics_mode = False - i += 2 - continue - elif c == 0x01: - x = (SOH, None) - i += 1 - elif c == 0x02: - x = (STX, None) - i += 1 - elif c == 0x08: - x = (BS, None) - i += 1 - elif c == 0x09: - x = (STR, [b' ']) # who the **** uses tabs anyway? - i += 1 - elif c == 0x0a: - x = (LF, None) - i += 1 - elif c == 0x0d: - x = (CR, None) - i += 1 - - if x is None: - x = (STR, [six.int2byte(c) for c in bytearray(b'\\x%02x' % c)]) - i += 1 - - if _graphics_mode: - continue - - if x[0] == STR and out and out[-1][0] == STR: - out[-1][1].extend(x[1]) - else: - out.append(x) - return out - -saved_cursor = None -# XXX: render cells that is half-way on the screen -def render_cell(cell, clear_after = False): - global scroll, saved_cursor - row, col = cell.start - row = row - scroll + height - 1 - if row < 0: - return - indent = min(cell.indent, width - 1) - for t, x in cell.content: - if t == STR: - i = 0 - while i < len(x): - if col >= width: - col = 0 - row += 1 - if col < indent: - put(' ' * (indent - col)) - col = indent - c = x[i] - if not hasattr(c, 'encode'): - c = c.decode('utf-8', 'backslashreplace') - put(c) - col += 1 - i += 1 - elif t == CSI: - cmd, args, c = x - put(c) - # figure out if the cursor moved (XXX: here probably be bugs) - if cmd[1] is None and cmd[2] is None: - c = cmd[0] - if len(args) >= 1: - n = args[0] - else: - n = None - if len(args) >= 2: - m = args[1] - else: - m = None - if c == ord('A'): - n = n or 1 - row = max(0, row - n) - elif c == ord('B'): - n = n or 1 - row = min(height - 1, row + n) - elif c == ord('C'): - n = n or 1 - col = min(width - 1, col + n) - elif c == ord('D'): - n = n or 1 - col = max(0, col - n) - elif c == ord('E'): - n = n or 1 - row = min(height - 1, row + n) - col = 0 - elif c == ord('F'): - n = n or 1 - row = max(0, row - n) - col = 0 - elif c == ord('G'): - n = n or 1 - col = min(width - 1, n - 1) - elif c == ord('H') or c == ord('f'): - n = n or 1 - m = m or 1 - row = min(height - 1, n - 1) - col = min(width - 1, m - 1) - elif c == ord('S'): - n = n or 1 - scroll += n - row = max(0, row - n) - elif c == ord('T'): - n = n or 1 - scroll -= n - row = min(height - 1, row + n) - elif c == ord('s'): - saved_cursor = row, col - elif c == ord('u'): - if saved_cursor: - row, col = saved_cursor - elif t == LF: - if clear_after and col <= width - 1: - put('\x1b[K') # clear line - put('\n') - col = 0 - row += 1 - elif t == BS: - if col > 0: - put('\x08') - col -= 1 - elif t == CR: - put('\r') - col = 0 - elif t == SOH: - put('\x01') - elif t == STX: - put('\x02') - elif t == OOB: - put(x) - if row >= height: - d = row - height + 1 - scroll += d - row -= d - row = row + scroll - height + 1 - cell.end = (row, col) - -def render_from(i, force = False, clear_after = False): - e = None - # `i` should always be a valid cell, but in case i f***ed up somewhere, I'll - # check it and just do nothing if something went wrong. - if i < 0 or i >= len(cells): - return - goto(*cells[i].start) - for c in cells[i:]: - if not force and c.start == e: - goto(*cells[-1].end) - break - elif e: - c.start = e - render_cell(c, clear_after = clear_after) - e = c.end - if clear_after and (e[0] < scroll or e[1] < width - 1): - put('\x1b[J') - flush() - -def redraw(): - for i in reversed(range(len(cells))): - row = cells[i].start[0] - if row - scroll + height <= 0: - # XXX: remove this line when render_cell is fixed - i += 1 - break - else: - if not cells: + def __init__(self, value, float): + self.value = value + self.float = float + + def draw(self): + self.pos = get_position() + self.born = epoch + put(self.value) + self.pos_after = get_position() + + def update(self, value): + if isinstance(value, bytes): + value = value.decode('utf-8', 'backslashreplace') + with wlock: + want_erase_line = False + if '\n' in value: + if len(value) < len(self.value): + want_erase_line = True + elif '\n' not in self.value: # not really supported + for cell in cells.iter_after(self): + if cell.value: + want_erase_line = True + break + self.value = value + self.update_locked(erase_line=want_erase_line) + fd.flush() + + def prepare_redraw(self): + global epoch + if self.born != epoch: + return None + saved = get_position() + if saved < self.pos or saved == (1, 1): + epoch += 1 + return None + goto(self.pos) + return saved + + def update_locked(self, erase_line=False): + prev_pos = self.prepare_redraw() + if prev_pos is None: + for cell in cells: + cell.draw() return - render_from(i, force = True, clear_after = True) - -lock = threading.Lock() -def output(s = '', float = False, priority = 10, frozen = False, - indent = 0, before = None, after = None): - with lock: - rel = before or after - if rel: - i, _ = find_cell(rel.h) - is_floating = rel.is_floating - float = cells[i].float - if before: - i -= 1 - elif float and priority: - is_floating = True - float = priority - for i in reversed(range(len(cells))): - if cells[i].float <= float: + erased_line = None + if erase_line: + do('el') + erased_line = self.pos[0] + put(self.value) + pos = get_position() + if pos == self.pos_after: + goto(prev_pos) + return + if pos < self.pos_after: + do('el') + erased_line = self.pos[0] + old_after = self.pos_after + self.pos_after = pos + + cell = self # in case there are no more cells + for cell in cells.iter_after(self): + if old_after != cell.pos: + # do not merge gaps + break + pos = get_position() + if erased_line != pos[0]: + if pos[0] < cell.pos[0]: + # the cell moved up, erase its line + do('el') + erased_line = pos[0] + elif cell.pos == pos: + # cell got neither moved nor erased break + + if pos[1] < cell.pos[1]: + # the cell moved left, it must be same line as self; erase if not yet erased + if not erase_line and erased_line != pos[0]: + do('el') + erased_line = pos[0] + + old_after = cell.pos_after + cell.draw() + if cell.pos_after == old_after and erased_line != old_after[0]: + break else: - is_floating = False - i = len(cells) - 1 - while i > 0 and cells[i].float: - i -= 1 - # put('xx %d\n' % i) - cell = Cell() - cell.content = parse(s) - cell.frozen = frozen - cell.float = float - cell.indent = indent - cell.start = cells[i].end - i += 1 - cells.insert(i, cell) - h = Handle(cell, is_floating) - if not s: - cell.end = cell.start - return h - # the invariant is that the cursor is placed after the last cell - if i == len(cells) - 1: - render_cell(cell, clear_after = True) - flush() - else: - render_from(i, clear_after = True) - return h - -def find_cell(h): - for i, c in enumerate(cells): - if id(c) == h: - return i, c - raise KeyError - -def discard_frozen(): - # we assume that no cell will shrink very much and that noone has space - # for more than MAX_TERM_HEIGHT lines in their terminal - while len(cells) > 1 and scroll - cells[0].end[0] > MAX_TERM_HEIGHT: - c = cells.pop(0) - del c # trigger GC maybe, kthxbai - -def update(h, s): - with lock: - try: - i, c = find_cell(h) - except KeyError: - return - if not c.frozen and c.content != s: - c.content = parse(s) - render_from(i, clear_after = True) - -def freeze(h): - try: - i, c = find_cell(h) - c.frozen = True - c.float = 0 - if c.content == []: - cells.pop(i) - discard_frozen() - except KeyError: - return + if cell.float: + # erase all screen after last float + do('ed') + if prev_pos > get_position(): + goto(prev_pos) + + def __repr__(self): + return '{}({!r}, float={}, pos={})'.format(self.__class__.__name__, self.value, self.float, self.pos) + + +class WeakCellList(object): + def __init__(self): + self._cells = [] + self._floats = [] + self._lists = self._cells, self._floats + + @property + def cells(self): + return self.iter_field(self._cells) + + @property + def floats(self): + return self.iter_field(self._floats) + + def iter_field(self, *Ls): + for L in Ls: + for iref in L[:]: + i = iref() + if i is None: + L.remove(iref) + else: + yield i + + def __iter__(self): + return self.iter_field(*self._lists) + + def iter_after(self, v): + it = iter(self) + for cell in it: + if cell == v: + break + return it + + def clear(self): + for c in self: + c.float = False + for L in self._lists: + del L[:] + + def insert(self, v, before): + L = self._lists[v.float] + for i, e in enumerate(self.iter_field(L)): + if e == before: + L.insert(i, weakref.ref(v)) + return + raise IndexError('output before dead cell') + + def append(self, v): + L = self._lists[v.float] + L.append(weakref.ref(v)) + + +cells = WeakCellList() + -def delete(h): - update(h, '') - freeze(h) +def get_position(): + global cached_pos + if not cached_pos: + cached_pos = [0, 0] + return tuple(cached_pos) + + +def output(s='', float=False, priority=10, frozen=False, indent=0, before=None): + with wlock: + if before: + float = before.float + + if isinstance(s, bytes): + s = s.decode('utf-8', 'backslashreplace') + if frozen: + for f in cells.floats: + f.prepare_redraw() + do('ed') # we could do it only when necessary + break + ret = put(s) + for f in cells.floats: + f.draw() + for f in cells.floats: + fd.flush() + break + return ret + + c = Cell(s, float) + if before is None: + cells.append(c) + c.draw() + else: + before.prepare_redraw() + cells.insert(c, before) + c.draw() + for f in cells.iter_after(c): + f.draw() + return c diff --git a/pwnlib/term/unix_termcap.py b/pwnlib/term/unix_termcap.py index f850ddb41..c6025e40b 100644 --- a/pwnlib/term/unix_termcap.py +++ b/pwnlib/term/unix_termcap.py @@ -32,7 +32,7 @@ def get(cap, *args, **kwargs): s = curses.tigetflag(cap) if s == -1: # default to empty string so tparm doesn't fail - s = '' + s = b'' else: s = bool(s) cache[cap] = s 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 2216da0af..1d2a2e48e 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 @@ -123,7 +124,7 @@ def resizer(): pass self.resizer = resizer - term.term.on_winch.append(self.resizer) + term.term.on_winch.append(self.resizer) # XXX memory leak else: self.resizer = None @@ -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) @@ -756,7 +760,7 @@ def shell(self, shell = None, tty = True, timeout = Timeout.default): """ return self.run(shell, tty, timeout = timeout) - def process(self, argv=None, executable=None, tty=True, cwd=None, env=None, timeout=Timeout.default, run=True, + def process(self, argv=None, executable=None, tty=True, cwd=None, env=None, ignore_environ=None, timeout=Timeout.default, run=True, stdin=0, stdout=1, stderr=2, preexec_fn=None, preexec_args=(), raw=True, aslr=None, setuid=None, shell=False): r""" @@ -784,8 +788,9 @@ def process(self, argv=None, executable=None, tty=True, cwd=None, env=None, time Working directory. If :const:`None`, uses the working directory specified on :attr:`cwd` or set via :meth:`set_working_directory`. env(dict): - Environment variables to set in the child. If :const:`None`, inherits the - default environment. + Environment variables to add to the environment. + ignore_environ(bool): + Ignore default environment. By default use default environment iff env not specified. timeout(int): Timeout to set on the `tube` created to interact with the process. run(bool): @@ -905,6 +910,9 @@ def process(self, argv=None, executable=None, tty=True, cwd=None, env=None, time aslr = aslr if aslr is not None else context.aslr + if ignore_environ is None: + ignore_environ = env is not None # compat + argv, env = misc.normalize_argv_env(argv, env, self) if shell: @@ -955,11 +963,12 @@ def func(): pass os.chdir(%(cwd)r) +if %(ignore_environ)r: + os.environ.clear() environ = getattr(os, 'environb', os.environ) if env is not None: env = OrderedDict((bytes(k), bytes(v)) for k,v in env) - os.environ.clear() environ.update(env) else: env = environ @@ -1502,14 +1511,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) @@ -1531,7 +1540,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: @@ -1552,7 +1561,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): @@ -2144,6 +2153,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)) @@ -2180,7 +2240,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/ui.py b/pwnlib/ui.py index b0dc96096..f03491cdd 100644 --- a/pwnlib/ui.py +++ b/pwnlib/ui.py @@ -42,15 +42,11 @@ def ehook(*args): if "coverage" in sys.modules: cmd = "import coverage; coverage.process_startup()\n" + cmd env.setdefault("COVERAGE_PROCESS_START", ".coveragerc") + env['COLUMNS'] = '80' + env['ROWS'] = '24' p = process([sys.executable, "-c", cmd], env=env, stderr=subprocess.PIPE) - try: - p.recvuntil(b"\33[6n") - except EOFError: - raise EOFError("process terminated with code: %r (%r)" % (p.poll(True), p.stderr.read())) # late initialization can lead to EINTR in many places - fcntl.ioctl(p.stdout.fileno(), termios.TIOCSWINSZ, struct.pack("hh", 80, 80)) - p.stdout.write(b"\x1b[1;1R") - time.sleep(0.5) + fcntl.ioctl(p.stdout.fileno(), termios.TIOCSWINSZ, struct.pack('HH', 24, 80)) return p def yesno(prompt, default=None): @@ -107,9 +103,9 @@ def yesno(prompt, default=None): yesfocus, yes = term.text.bold('Yes'), 'yes' nofocus, no = term.text.bold('No'), 'no' hy = term.output(yesfocus if default is True else yes) - term.output('/') + hs = term.output('/') hn = term.output(nofocus if default is False else no) - term.output(']\n') + he = term.output(']\n') cur = default while True: k = term.key.get() @@ -210,9 +206,9 @@ def options(prompt, opts, default = None): for i, opt in enumerate(opts): h = term.output(arrow if i == cur else space, frozen = False) num = numfmt % (i + 1) - term.output(num) - term.output(opt + '\n', indent = len(num) + len(space)) - hs.append(h) + h1 = term.output(num) + h2 = term.output(opt + '\n', indent = len(num) + len(space)) + hs.append((h, h1, h2)) ds = '' while True: prev = cur @@ -249,11 +245,11 @@ def options(prompt, opts, default = None): if prev != cur: if prev is not None: - hs[prev].update(space) + hs[prev][0].update(space) if was_digit: - hs[cur].update(term.text.bold_green('%5s> ' % ds)) + hs[cur][0].update(term.text.bold_green('%5s> ' % ds)) else: - hs[cur].update(arrow) + hs[cur][0].update(arrow) else: linefmt = ' %' + str(len(str(len(opts)))) + 'd) %s' if default is not None: @@ -361,6 +357,5 @@ def more(text): print(l) if i + step < len(lines): term.key.get() - h.delete() else: print(text) diff --git a/pwnlib/util/misc.py b/pwnlib/util/misc.py index 0b7fbf456..7977f7a77 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() @@ -410,7 +410,12 @@ def run_in_new_terminal(command, terminal=None, args=None, kill_at_exit=True, pr if terminal == 'tmux': out, _ = p.communicate() - pid = int(out) + try: + pid = int(out) + except ValueError: + pid = None + if pid is None: + log.error("Could not parse PID from tmux output (%r). Start tmux first.", out) elif terminal == 'qdbus': with subprocess.Popen((qdbus, konsole_dbus_service, '/Sessions/{}'.format(last_konsole_session), 'org.kde.konsole.Session.processId'), stdout=subprocess.PIPE) as proc: diff --git a/pyproject.toml b/pyproject.toml index 517553053..ce59233f9 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", @@ -50,7 +50,7 @@ dependencies = [ "psutil>=3.3.0", "intervaltree>=3.0", "sortedcontainers", - "unicorn>=1.0.2rc1", # see unicorn-engine/unicorn#1100 and #1170 + "unicorn>=2.0.1", "six>=1.12.0", "rpyc", "colored_traceback", 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 diff --git a/travis/setup_avd_fast.sh b/travis/setup_avd_fast.sh index 577ddc287..f55ea59d8 100644 --- a/travis/setup_avd_fast.sh +++ b/travis/setup_avd_fast.sh @@ -8,7 +8,7 @@ set -ex # - arm64-v8a # - x86 # - x86_64 -ANDROID_ABI='armeabi-v7a' +ANDROID_ABI='x86_64' ANDROIDV=android-24 # Create our emulator Android Virtual Device (AVD) @@ -18,7 +18,7 @@ yes | sdkmanager --sdk_root="$ANDROID_HOME" --install "system-images;$ANDROIDV;d yes | sdkmanager --sdk_root="$ANDROID_HOME" --licenses echo no | avdmanager --silent create avd --name android-$ANDROID_ABI --force --package "system-images;$ANDROIDV;default;$ANDROID_ABI" -"$ANDROID_HOME"/emulator/emulator -avd android-$ANDROID_ABI -no-window -no-boot-anim -read-only -no-audio -no-window -no-snapshot & +"$ANDROID_HOME"/emulator/emulator -avd android-$ANDROID_ABI -no-window -no-boot-anim -read-only -no-audio -no-window -no-snapshot -gpu off -accel off & adb wait-for-device adb shell id adb shell getprop