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/CHANGELOG.md b/CHANGELOG.md index c69981884..bf8ed4ce4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,18 +70,26 @@ The table below shows which release corresponds to each branch, and what date th ## 4.13.0 (`dev`) +- [#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 - [#2310][2310] Add support to start a process on Windows +[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 [2310]: https://github.com/Gallopsled/pwntools/pull/2310 ## 4.12.0 (`beta`) 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/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/template.py b/pwnlib/commandline/template.py index f68461cb6..6e5f74655 100644 --- a/pwnlib/commandline/template.py +++ b/pwnlib/commandline/template.py @@ -9,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): @@ -44,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/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/data/templates/pwnup.mako b/pwnlib/data/templates/pwnup.mako index e47217036..28c670869 100644 --- a/pwnlib/data/templates/pwnup.mako +++ b/pwnlib/data/templates/pwnup.mako @@ -44,7 +44,7 @@ from pwn import * %if not quiet: # Set up pwntools for the correct architecture %endif -%if ctx.binary: +%if ctx.binary or not host: exe = context.binary = ELF(args.EXE or ${binary_repr}) <% binary_repr = 'exe.path' %> %else: @@ -99,11 +99,7 @@ else: %endif library_path = libcdb.download_libraries(${libc_repr}) if library_path: - %if ctx.binary: exe = context.binary = ELF.patch_custom_libraries(${binary_repr}, library_path) - %else: - exe = ELF.patch_custom_libraries(exe, library_path) - %endif libc = exe.libc else: libc = ELF(${libc_repr}) 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 5c41f0f12..4751975b3 100644 --- a/pwnlib/elf/elf.py +++ b/pwnlib/elf/elf.py @@ -460,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', diff --git a/pwnlib/gdb.py b/pwnlib/gdb.py index 753955ea4..df2599669 100644 --- a/pwnlib/gdb.py +++ b/pwnlib/gdb.py @@ -315,7 +315,14 @@ def _gdbserver_port(gdbserver, ssh): # Process /bin/bash created; pid = 14366 # Listening on port 34816 - process_created = gdbserver.recvline() + process_created = gdbserver.recvline(timeout=3) + + if not process_created: + log.error( + 'No output from gdbserver after 3 seconds. Try setting the SHELL=/bin/sh ' + 'environment variable or using the env={} argument if you are affected by ' + 'https://sourceware.org/bugzilla/show_bug.cgi?id=26116' + ) if process_created.startswith(b'ERROR:'): raise ValueError( 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/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/tubes/ssh.py b/pwnlib/tubes/ssh.py index 01e6eed36..1d2a2e48e 100644 --- a/pwnlib/tubes/ssh.py +++ b/pwnlib/tubes/ssh.py @@ -124,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 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 ed230988a..2ca9f31a5 100644 --- a/pwnlib/util/misc.py +++ b/pwnlib/util/misc.py @@ -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 c241db72f..ce59233f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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/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