diff options
author | Timo Röhling <roehling@debian.org> | 2023-09-16 11:08:56 +0200 |
---|---|---|
committer | Timo Röhling <roehling@debian.org> | 2023-09-16 11:08:56 +0200 |
commit | e66b79b9ce2cecc89dcae92efcd93110d0704798 (patch) | |
tree | d5ea2f5532d948f2c5cf126efa4cfed020efed4f | |
parent | d2a3b5f42b8e380a1ac87084a9fef002655d0555 (diff) |
New upstream version 4.11.0
41 files changed, 973 insertions, 303 deletions
diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index b6ee935..1a5f6aa 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -11,17 +11,17 @@ jobs: runs-on: ${{ matrix.os }} timeout-minutes: 30 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Cache for pip - uses: actions/cache@v1 + uses: actions/cache@v3 id: cache-pip with: path: ~/.cache/pip key: ${{ matrix.os }}-cache-pip - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} @@ -33,14 +33,13 @@ jobs: qemu-user-static \ binutils-aarch64-linux-gnu \ binutils-arm-linux-gnueabihf \ - libc6-dbg \ - openjdk-8-jre-headless + libc6-dbg - name: Install Android AVD run: | - USER=travis source travis/setup_avd.sh + source travis/setup_avd_fast.sh sed -i 's/skip_android = True/skip_android = False/' docs/source/conf.py - set | egrep '^(ANDROID|PATH)' >.android.env + set | grep ^PATH >.android.env - name: Install dependencies run: | diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 491504e..2550110 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -1,5 +1,9 @@ name: Changelog -on: [pull_request] +on: + pull_request: + paths: + - pwnlib/** + - pwn/** env: GITHUB_BASE: origin/${{ github.event.pull_request.base.ref }} @@ -11,7 +15,7 @@ jobs: runs-on: ${{ matrix.os }} timeout-minutes: 5 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32c7142..321457a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,12 +5,12 @@ jobs: test: strategy: matrix: - python-version: [2.7, 3.8] + python_version: [2.7, 3.8] os: [ubuntu-latest] runs-on: ${{ matrix.os }} timeout-minutes: 30 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 @@ -20,16 +20,29 @@ jobs: git log --oneline --graph -10 - name: Cache for pip - uses: actions/cache@v1 + uses: actions/cache@v3 id: cache-pip with: path: ~/.cache/pip key: ${{ matrix.os }}-cache-pip - - name: Set up Python ${{ matrix.python-version }} + - name: Set up Python ${{ matrix.python_version }} + if: matrix.python_version != '2.7' uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python-version }} + python-version: ${{ matrix.python_version }} + + - name: Set up Python 2.7 + if: matrix.python_version == '2.7' + run: | + sudo apt-get update + sudo apt-get install -y \ + python2.7 python2.7-dev python2-pip-whl + sudo ln -sf python2.7 /usr/bin/python + export PYTHONPATH=`echo /usr/share/python-wheels/pip-*py2*.whl` + sudo --preserve-env=PYTHONPATH python -m pip install --upgrade pip setuptools wheel + sudo chown -R $USER /usr/local/lib/python2.7 + - name: Verify tag against version if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') @@ -49,7 +62,8 @@ jobs: sudo apt-get update sudo apt-get install -y --no-install-recommends -o Acquire::Retries=3 \ ash bash-static dash ksh mksh zsh \ - pandoc gdb gdbserver socat sshpass \ + python3-rpyc \ + gdb gdbserver socat \ binutils-multiarch qemu-user-static \ binutils-aarch64-linux-gnu \ binutils-arm-linux-gnueabihf \ @@ -62,11 +76,6 @@ jobs: libc6-dbg \ elfutils - - name: Install RPyC for GDB - run: | - sudo apt-get install -y python3-pip - /usr/bin/python3 -m pip install rpyc - - name: Testing Corefiles run: | ulimit -a @@ -82,9 +91,8 @@ jobs: - name: Install dependencies run: | pip install --upgrade pip - pip install --upgrade wheel + pip install --upgrade wheel build pip install --upgrade flake8 appdirs - python setup.py egg_info pip install --upgrade --editable . - name: Sanity checks @@ -94,7 +102,8 @@ jobs: run: pip install -r docs/requirements.txt - name: Manually install non-broken Unicorn - run: pip install unicorn==1.0.2rc3 + if: matrix.python_version == '2.7' + run: pip install unicorn==2.0.0rc7 - name: Disable yama ptrace_scope run: | @@ -163,9 +172,9 @@ jobs: pwn libcdb hash b229d1da1e161f95e839cf90cded5f719e5de308 - name: Build source and wheel distributions + if: matrix.python_version > '2.7' run: | - python setup.py sdist - python setup.py bdist_wheel --universal + python -m build - uses: actions/upload-artifact@v2 with: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 221bac0..ac7f9ca 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,16 +10,16 @@ jobs: runs-on: ${{ matrix.os }} timeout-minutes: 30 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Cache for pip - uses: actions/cache@v1 + uses: actions/cache@v3 id: cache-pip with: path: ~/.cache/pip key: ${{ matrix.os }}-cache-pip - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/merge-conflict.yml b/.github/workflows/merge-conflict.yml index cf1ac73..984c438 100644 --- a/.github/workflows/merge-conflict.yml +++ b/.github/workflows/merge-conflict.yml @@ -1,5 +1,7 @@ name: Check for merge markers -on: [pull_request] +on: + pull_request: + types: [synchronize] env: GITHUB_BASE: origin/${{ github.event.pull_request.base.ref }} @@ -11,7 +13,7 @@ jobs: runs-on: ${{ matrix.os }} timeout-minutes: 5 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index fbe46f4..9e95784 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -10,16 +10,16 @@ jobs: runs-on: ${{ matrix.os }} timeout-minutes: 30 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Cache for pip - uses: actions/cache@v1 + uses: actions/cache@v3 id: cache-pip with: path: ~/.cache/pip key: ${{ matrix.os }}-cache-pip - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} @@ -27,7 +27,7 @@ jobs: run: | set -x pip install pylint - pip install --upgrade -r requirements.txt + pip install --upgrade -e . pylint --exit-zero --errors-only pwnlib -f parseable | cut -d ' ' -f2- > current.txt git fetch origin git checkout origin/"$GITHUB_BASE_REF" diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..5b502e3 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,12 @@ +# https://docs.readthedocs.io/en/stable/config-file/v2.html + +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3" + +python: + install: + - requirements: docs/requirements.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 03bfd99..17c1a20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,10 @@ The table below shows which release corresponds to each branch, and what date th | Version | Branch | Release Date | | ---------------- | -------- | ---------------------- | -| [4.12.0](#4120) | `dev` | -| [4.11.0](#4110) | `beta` | -| [4.10.0](#4100) | `stable` | May 21, 2023 +| [4.13.0](#4130-dev) | `dev` | +| [4.12.0](#4120-beta) | `beta` | +| [4.11.0](#4110-stable) | `stable` | Sep 15, 2023 +| [4.10.0](#4100) | | May 21, 2023 | [4.9.0](#490) | | Dec 29, 2022 | [4.8.0](#480) | | Apr 21, 2022 | [4.7.1](#471) | | Apr 20, 2022 @@ -66,35 +67,48 @@ The table below shows which release corresponds to each branch, and what date th | [3.0.0](#300) | | Aug 20, 2016 | [2.2.0](#220) | | Jan 5, 2015 -## 4.12.0 (`dev`) +## 4.13.0 (`dev`) -## 4.11.0 (`beta`) +## 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 +- [#2219][2219] Fix passing arguments on the stack in shellcraft syscall template +- [#2212][2212] Add `--libc libc.so` argument to `pwn template` command +- [#2257][2257] Allow creation of custom templates for `pwn template` command +- [#2225][2225] Allow empty argv in ssh.process() + +[2202]: https://github.com/Gallopsled/pwntools/pull/2202 +[2117]: https://github.com/Gallopsled/pwntools/pull/2117 +[2221]: https://github.com/Gallopsled/pwntools/pull/2221 +[2219]: https://github.com/Gallopsled/pwntools/pull/2219 +[2212]: https://github.com/Gallopsled/pwntools/pull/2212 +[2257]: https://github.com/Gallopsled/pwntools/pull/2257 +[2225]: https://github.com/Gallopsled/pwntools/pull/2225 + +## 4.11.0 (`stable`) - [#2185][2185] make fmtstr module able to create payload without $ notation -- [#2062][2062] make pwn cyclic -l work with entry larger than 4 bytes -- [#2092][2092] shellcraft: dup() is now called dupio() consistently across all supported arches -- [#2093][2093] setresuid() in shellcraft uses current euid by default - [#2103][2103] Add search for libc binary by leaked function addresses `libcdb.search_by_symbol_offsets()` -- [#2125][2125] Allow tube.recvregex to return capture groups -- [#2144][2144] Removes `p2align 2` `asm()` headers from `x86-32`, `x86-64` and `mips` architectures to avoid inconsistent instruction length when patching binaries - [#2177][2177] Support for RISC-V 64-bit architecture - [#2186][2186] Enhance `ELF.nx` and `ELF.execstack` - [#2129][2129] Handle `context.newline` correctly when typing in `tube.interactive()` +- [#2214][2214] Fix bug at ssh.py:`download` and `download_file` with relative paths +- [#2241][2241] Fix ssh.process not setting ssh_process.cwd attribute +- [#2261][2261] Fix corefile module after pyelftools update [2185]: https://github.com/Gallopsled/pwntools/pull/2185 -[2062]: https://github.com/Gallopsled/pwntools/pull/2062 -[2092]: https://github.com/Gallopsled/pwntools/pull/2092 -[2093]: https://github.com/Gallopsled/pwntools/pull/2093 [2103]: https://github.com/Gallopsled/pwntools/pull/2103 -[2125]: https://github.com/Gallopsled/pwntools/pull/2125 -[2144]: https://github.com/Gallopsled/pwntools/pull/2144 [2177]: https://github.com/Gallopsled/pwntools/pull/2177 [2186]: https://github.com/Gallopsled/pwntools/pull/2186 [2129]: https://github.com/Gallopsled/pwntools/pull/2129 +[2214]: https://github.com/Gallopsled/pwntools/pull/2214 +[2241]: https://github.com/Gallopsled/pwntools/pull/2241 +[2261]: https://github.com/Gallopsled/pwntools/pull/2261 -## 4.10.0 (`stable`) +## 4.10.0 In memoriam — [Zach Riggle][zach] — long time contributor and maintainer of Pwntools. diff --git a/MANIFEST.in b/MANIFEST.in index dcf6b23..8f001ea 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,7 @@ +graft build graft examples graft extra +graft travis 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 diff --git a/docs/requirements.txt b/docs/requirements.txt index b54a40f..d44363c 100755 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,9 @@ capstone coveralls +python-dateutil doc2dash docutils<0.18 +intervaltree isort mako>=1.0.0 paramiko>=1.15.2 @@ -10,6 +12,8 @@ pyelftools>=0.2.3 pygments>=2.0 pypandoc pyserial>=2.7 +pysocks +psutil requests>=2.5.1 ropgadget>=5.3 sphinx==1.8.6; python_version<'3' diff --git a/docs/source/shellcraft/amd64.rst b/docs/source/shellcraft/amd64.rst index 03dc8cd..8aced2c 100644 --- a/docs/source/shellcraft/amd64.rst +++ b/docs/source/shellcraft/amd64.rst @@ -17,3 +17,9 @@ .. automodule:: pwnlib.shellcraft.amd64.linux :members: + +:mod:`pwnlib.shellcraft.amd64.windows` +--------------------------------------- + +.. automodule:: pwnlib.shellcraft.amd64.windows + :members: diff --git a/pwn/toplevel.py b/pwn/toplevel.py index f0658a7..92b01ad 100644 --- a/pwn/toplevel.py +++ b/pwn/toplevel.py @@ -16,7 +16,6 @@ import tempfile import threading import time -import colored_traceback from pprint import pprint import pwnlib @@ -85,7 +84,12 @@ info = log.info debug = log.debug success = log.success -colored_traceback.add_hook() +try: + import colored_traceback +except ImportError: + pass +else: + colored_traceback.add_hook() # Equivalence with the default behavior of "from import *" # __all__ = [x for x in tuple(globals()) if not x.startswith('_')] diff --git a/pwnlib/adb/adb.py b/pwnlib/adb/adb.py index 14299b8..789546f 100644 --- a/pwnlib/adb/adb.py +++ b/pwnlib/adb/adb.py @@ -1370,8 +1370,12 @@ def compile(source): ndk_build = misc.which('ndk-build') if not ndk_build: # Ensure that we can find the NDK. - ndk = os.environ.get('NDK', None) - if ndk is None: + for envvar in ('NDK', 'ANDROID_NDK', 'ANDROID_NDK_ROOT', + 'ANDROID_NDK_HOME', 'ANDROID_NDK_LATEST_HOME'): + ndk = os.environ.get(envvar) + if ndk is not None: + break + else: log.error('$NDK must be set to the Android NDK directory') ndk_build = os.path.join(ndk, 'ndk-build') diff --git a/pwnlib/asm.py b/pwnlib/asm.py index 745ac74..b37d39c 100644 --- a/pwnlib/asm.py +++ b/pwnlib/asm.py @@ -185,6 +185,8 @@ def which_binutils(util, check_version=False): 'mips64': ['mips'], 'powerpc64': ['powerpc'], 'sparc64': ['sparc'], + 'riscv32': ['riscv32', 'riscv64', 'riscv'], + 'riscv64': ['riscv64', 'riscv32', 'riscv'], }.get(arch, []) # If one of the candidate architectures matches the native @@ -262,7 +264,11 @@ def _assembler(): 'powerpc64': [gas, '-m%s' % context.endianness, '-mppc%s' % context.bits], # ia64 only accepts -mbe or -mle - 'ia64': [gas, '-m%ce' % context.endianness[0]] + 'ia64': [gas, '-m%ce' % context.endianness[0]], + + # riscv64-unknown-elf-as supports riscv32 as well as riscv64 + 'riscv32': [gas, '-march=rv32gc', '-mabi=ilp32'], + 'riscv64': [gas, '-march=rv64gc', '-mabi=lp64'], } assembler = assemblers.get(context.arch, [gas]) @@ -362,7 +368,8 @@ def _bfdname(): 'msp430' : 'elf32-msp430', 'powerpc' : 'elf32-powerpc', 'powerpc64' : 'elf64-powerpc', - 'riscv' : 'elf%d-%sriscv' % (context.bits, E), + 'riscv32' : 'elf%d-%sriscv' % (context.bits, E), + 'riscv64' : 'elf%d-%sriscv' % (context.bits, E), 'vax' : 'elf32-vax', 's390' : 'elf%d-s390' % context.bits, 'sparc' : 'elf32-sparc', @@ -385,6 +392,8 @@ def _bfdarch(): 'powerpc64': 'powerpc', 'sparc64': 'sparc', 'thumb': 'arm', + 'riscv32': 'riscv', + 'riscv64': 'riscv', } if arch in convert: diff --git a/pwnlib/context/__init__.py b/pwnlib/context/__init__.py index dfeacdd..10a7a4f 100644 --- a/pwnlib/context/__init__.py +++ b/pwnlib/context/__init__.py @@ -404,7 +404,8 @@ class ContextType(object): 'msp430': little_16, 'powerpc': big_32, 'powerpc64': big_64, - 'riscv': little_32, + 'riscv32': little_32, + 'riscv64': little_64, 's390': big_32, 'sparc': big_32, 'sparc64': big_64, @@ -775,7 +776,9 @@ class ContextType(object): ('i686', 'i386'), ('armv7l', 'arm'), ('armeabi', 'arm'), - ('arm64', 'aarch64')] + ('arm64', 'aarch64'), + ('rv32', 'riscv32'), + ('rv64', 'riscv64')] for k, v in transform: if arch.startswith(k): arch = v @@ -1447,7 +1450,7 @@ class ContextType(object): from pwnlib.tubes.ssh import ssh if not isinstance(shell, ssh): - raise AttributeError("context.ssh_session must be an ssh tube") + raise AttributeError("context.ssh_session must be an ssh tube") return shell diff --git a/pwnlib/elf/corefile.py b/pwnlib/elf/corefile.py index 9d81995..b5a248a 100644 --- a/pwnlib/elf/corefile.py +++ b/pwnlib/elf/corefile.py @@ -76,7 +76,6 @@ import tempfile from io import BytesIO, StringIO import elftools -from elftools.common.py3compat import bytes2str from elftools.common.utils import roundup from elftools.common.utils import struct_parse from elftools.construct import CString @@ -94,6 +93,7 @@ from pwnlib.util.fiddling import enhex 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 @@ -134,12 +134,13 @@ def iter_notes(self): self.stream.seek(offset) # n_namesz is 4-byte aligned. disk_namesz = roundup(note['n_namesz'], 2) - note['n_name'] = bytes2str( - CString('').parse(self.stream.read(disk_namesz))) - offset += disk_namesz + with context.local(encoding='latin-1'): + note['n_name'] = _decode( + CString('').parse(self.stream.read(disk_namesz))) + offset += disk_namesz - desc_data = bytes2str(self.stream.read(note['n_descsz'])) - note['n_desc'] = desc_data + 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 diff --git a/pwnlib/elf/elf.py b/pwnlib/elf/elf.py index 9053a1a..8bbf0b8 100644 --- a/pwnlib/elf/elf.py +++ b/pwnlib/elf/elf.py @@ -49,7 +49,6 @@ from six import BytesIO from collections import namedtuple -from elftools.elf.constants import E_FLAGS from elftools.elf.constants import P_FLAGS from elftools.elf.constants import SHN_INDICES from elftools.elf.descriptions import describe_e_type @@ -227,20 +226,13 @@ class ELF(ELFFile): #: :class:`str`: Path to the file self.path = packing._need_text(os.path.abspath(path)) - #: :class:`str`: Architecture of the file (e.g. ``'i386'``, ``'arm'``). - #: - #: See: :attr:`.ContextType.arch` - self.arch = self.get_machine_arch() - if isinstance(self.arch, (bytes, six.text_type)): - self.arch = self.arch.lower() - #: :class:`dotdict` of ``name`` to ``address`` for all symbols in the ELF self.symbols = dotdict() #: :class:`dotdict` of ``name`` to ``address`` for all Global Offset Table (GOT) entries self.got = dotdict() - #: :class:`dotdict` of ``name`` to ``address`` for all Procedure Linkate Table (PLT) entries + #: :class:`dotdict` of ``name`` to ``address`` for all Procedure Linkage Table (PLT) entries self.plt = dotdict() #: :class:`dotdict` of ``name`` to :class:`.Function` for each function in the ELF @@ -268,17 +260,12 @@ class ELF(ELFFile): #: :class:`int`: Pointer width, in bytes self.bytes = self.bits // 8 - if self.arch == 'mips': - mask = lambda a, b: a & b == b - flags = self.header['e_flags'] - - if mask(flags, E_FLAGS.EF_MIPS_ARCH_32) \ - or mask(flags, E_FLAGS.EF_MIPS_ARCH_32R2): - pass - elif mask(flags, E_FLAGS.EF_MIPS_ARCH_64) \ - or mask(flags, E_FLAGS.EF_MIPS_ARCH_64R2): - self.arch = 'mips64' - self.bits = 64 + #: :class:`str`: Architecture of the file (e.g. ``'i386'``, ``'arm'``). + #: + #: See: :attr:`.ContextType.arch` + self.arch = self.get_machine_arch() + if isinstance(self.arch, (bytes, six.text_type)): + self.arch = self.arch.lower() self._sections = None self._segments = None @@ -471,18 +458,21 @@ class ELF(ELFFile): def get_machine_arch(self): return { - 'EM_X86_64': 'amd64', - 'EM_386' :'i386', - 'EM_486': 'i386', - 'EM_ARM': 'arm', - 'EM_AARCH64': 'aarch64', - 'EM_MIPS': 'mips', - 'EM_PPC': 'powerpc', - 'EM_PPC64': 'powerpc64', - 'EM_SPARC32PLUS': 'sparc', - 'EM_SPARCV9': 'sparc64', - 'EM_IA_64': 'ia64' - }.get(self['e_machine'], self['e_machine']) + ('EM_X86_64', 64): 'amd64', + ('EM_386', 32): 'i386', + ('EM_486', 32): 'i386', + ('EM_ARM', 32): 'arm', + ('EM_AARCH64', 64): 'aarch64', + ('EM_MIPS', 32): 'mips', + ('EM_MIPS', 64): 'mips64', + ('EM_PPC', 32): 'powerpc', + ('EM_PPC64', 64): 'powerpc64', + ('EM_SPARC32PLUS', 32): 'sparc', + ('EM_SPARCV9', 64): 'sparc64', + ('EM_IA_64', 64): 'ia64', + ('EM_RISCV', 32): 'riscv32', + ('EM_RISCV', 64): 'riscv64', + }.get((self['e_machine'], self.bits), self['e_machine']) @property def entry(self): @@ -1061,10 +1051,10 @@ class ELF(ELFFile): return banner = self.string(self.symbols.linux_banner) - + # convert banner into a utf-8 string since re.search does not accept bytes anymore banner = banner.decode('utf-8') - + # 'Linux version 3.18.31-gd0846ecc regex = r'Linux version (\S+)' match = re.search(regex, banner) @@ -1114,7 +1104,7 @@ class ELF(ELFFile): else: log.error('Unsupported architecture %s in ELF.libc_start_main_return', self.arch) return 0 - + lines = self.functions['__libc_start_main'].disasm().split('\n') exit_addr = hex(self.symbols['exit']) calls = [(index, line) for index, line in enumerate(lines) if set(line.split()) & call_instructions] @@ -1128,7 +1118,7 @@ class ELF(ELFFile): return_from_main = lines[call_to_main[0] + call_return_offset].lstrip() return_from_main = int(return_from_main[ : return_from_main.index(':') ], 16) return return_from_main - + # Starting with glibc-2.34 calling `main` is split out into `__libc_start_call_main` ret_addr = find_ret_main_addr(lines, calls) # Pre glibc-2.34 case - `main` is called directly @@ -1142,7 +1132,7 @@ class ELF(ELFFile): match = direct_call_pattern.search(line[1]) if not match: continue - + target_addr = int(match.group(1), 0) # `__libc_start_call_main` is usually smaller than `__libc_start_main`, so # we might disassemble a bit too much, but it's a good dynamic estimate. @@ -1685,61 +1675,244 @@ class ELF(ELFFile): Specifically, we are checking for ``READ_IMPLIES_EXEC`` being set by the kernel, as a result of honoring ``PT_GNU_STACK`` in the kernel. - The **Linux kernel** directly honors ``PT_GNU_STACK`` to `mark the - stack as executable.`__ + ``READ_IMPLIES_EXEC`` is set, according to a set of architecture specific + rules, that depend on the CPU features, and the presence of ``PT_GNU_STACK``. - .. __: https://github.com/torvalds/linux/blob/v4.9/fs/binfmt_elf.c#L784-L789 + Unfortunately, :class:`ELF` is not context-aware, so it's not always possible + to determine whether the process of a binary that's missing ``PT_GNU_STACK`` + will have NX or not. + + The rules are as follows: + + +-----------+--------------+---------------------------+------------------------------------------------+----------+ + | ELF arch | linux | GNU_STACK | other | NX | + +===========+==============+===========================+================================================+==========+ + | i386 | < 5.8 | non-exec | | enabled | + | | [#x86_5.7]_ +---------------------------+------------------------------------------------+----------+ + | | | exec / missing | | disabled | + | +--------------+---------------------------+------------------------------------------------+----------+ + | | >= 5.8 | exec / non-exec | | enabled | + | | [#x86_5.8]_ +---------------------------+------------------------------------------------+----------+ + | | | missing | | disabled | + +-----------+--------------+---------------------------+------------------------------------------------+----------+ + | amd64 | < 5.8 | non-exec | | enabled | + | | [#x86_5.7]_ +---------------------------+------------------------------------------------+----------+ + | | | exec / missing | | disabled | + | +--------------+---------------------------+------------------------------------------------+----------+ + | | >= 5.8 | exec / non-exec / missing | | enabled | + | | [#x86_5.8]_ | | | | + +-----------+--------------+---------------------------+------------------------------------------------+----------+ + | arm | < 5.8 | non-exec* | | enabled | + | | [#arm_5.7]_ +---------------------------+------------------------------------------------+----------+ + | | | exec / missing | | disabled | + | +--------------+---------------------------+------------------------------------------------+----------+ + | | >= 5.8 | exec / non-exec* | | enabled | + | | [#arm_5.8]_ +---------------------------+------------------------------------------------+----------+ + | | | missing | | disabled | + +-----------+--------------+---------------------------+------------------------------------------------+----------+ + | mips | < 5.18 | non-exec* | | enabled | + | | [#mips_5.17]_+---------------------------+------------------------------------------------+----------+ + | | | exec / missing | | disabled | + | +--------------+---------------------------+------------------------------------------------+----------+ + | | >= 5.18 | exec / non-exec* | | enabled | + | | [#mips_5.18]_+---------------------------+------------------------------------------------+----------+ + | | | missing | | disabled | + +-----------+--------------+---------------------------+------------------------------------------------+----------+ + | powerpc | [#powerpc]_ | non-exec / exec | | enabled | + | | +---------------------------+------------------------------------------------+----------+ + | | | missing | | disabled | + +-----------+--------------+---------------------------+------------------------------------------------+----------+ + | powerpc64 | [#powerpc]_ | exec / non-exec / missing | | enabled | + +-----------+--------------+---------------------------+------------------------------------------------+----------+ + | ia64 | [#ia64]_ | non-exec | | enabled | + | | +---------------------------+------------------------------------------------+----------+ + | | | exec / missing | e_flags & EF_IA_64_LINUX_EXECUTABLE_STACK == 0 | enabled | + | | + +------------------------------------------------+----------+ + | | | | e_flags & EF_IA_64_LINUX_EXECUTABLE_STACK != 0 | disabled | + +-----------+--------------+---------------------------+------------------------------------------------+----------+ + | the rest | [#the_rest]_ | exec / non-exec / missing | | enabled | + +-----------+--------------+---------------------------+------------------------------------------------+----------+ + + \\* Hardware limitations are ignored. + + If ``READ_IMPLIES_EXEC`` is set, then `all readable pages are executable`__. + .. __: https://github.com/torvalds/linux/blob/v6.3/fs/binfmt_elf.c#L1008-L1009 + .. code-block:: c + + if (elf_read_implies_exec(loc->elf_ex, executable_stack)) + current->personality |= READ_IMPLIES_EXEC; + + .. [#x86_5.7] + `source <https://github.com/torvalds/linux/blob/v5.7/arch/x86/include/asm/elf.h#L285-L286>`__ + + .. code-block:: c + + #define elf_read_implies_exec(ex, executable_stack) \\ + (executable_stack != EXSTACK_DISABLE_X) + + .. [#x86_5.8] + `source <https://github.com/torvalds/linux/blob/v5.8/arch/x86/include/asm/elf.h#L305-L306>`__ + + .. code-block:: c + + #define elf_read_implies_exec(ex, executable_stack) \\ + (mmap_is_ia32() && executable_stack == EXSTACK_DEFAULT) + + `mmap_is_ia32()`__: + .. __: https://github.com/torvalds/linux/blob/v5.8/arch/x86/include/asm/elf.h#L318-L321 + .. code-block:: c + + /* + * True on X86_32 or when emulating IA32 on X86_64 + */ + static inline int mmap_is_ia32(void) + + .. [#arm_5.7] + `source <https://github.com/torvalds/linux/blob/v5.7/arch/arm/kernel/elf.c#L85-L92>`__ + + .. code-block:: c + + int arm_elf_read_implies_exec(int executable_stack) + { + if (executable_stack != EXSTACK_DISABLE_X) + return 1; + if (cpu_architecture() < CPU_ARCH_ARMv6) + return 1; + return 0; + } + + .. [#arm_5.8] + `source <https://github.com/torvalds/linux/blob/v5.8/arch/arm/kernel/elf.c#L104-L111>`__ + + .. code-block:: c + + int arm_elf_read_implies_exec(int executable_stack) + { + if (executable_stack == EXSTACK_DEFAULT) + return 1; + if (cpu_architecture() < CPU_ARCH_ARMv6) + return 1; + return 0; + } + + .. [#mips_5.17] + `source <https://github.com/torvalds/linux/blob/v5.17/arch/mips/kernel/elf.c#L329-L342>`__ + + .. code-block:: c + + int mips_elf_read_implies_exec(void *elf_ex, int exstack) + { + if (exstack != EXSTACK_DISABLE_X) { + /* The binary doesn't request a non-executable stack */ + return 1; + } + if (!cpu_has_rixi) { + /* The CPU doesn't support non-executable memory */ + return 1; + } + return 0; + } + + .. [#mips_5.18] + `source <https://github.com/torvalds/linux/blob/v5.18/arch/mips/kernel/elf.c#L329-L336>`__ + + .. code-block:: c + + int mips_elf_read_implies_exec(void *elf_ex, int exstack) + { + /* + * Set READ_IMPLIES_EXEC only on non-NX systems that + * do not request a specific state via PT_GNU_STACK. + */ + return (!cpu_has_rixi && exstack == EXSTACK_DEFAULT); + } - .. code-block:: c + .. [#powerpc] + `source <https://github.com/torvalds/linux/blob/v6.3/arch/powerpc/include/asm/elf.h#L82-L108>`__ - case PT_GNU_STACK: - if (elf_ppnt->p_flags & PF_X) - executable_stack = EXSTACK_ENABLE_X; - else - executable_stack = EXSTACK_DISABLE_X; - break; + .. code-block:: c - Additionally, it then sets ``read_implies_exec``, so that `all readable pages - are executable`__. + #ifdef __powerpc64__ + /* stripped */ + # define elf_read_implies_exec(ex, exec_stk) (is_32bit_task() ? \\ + (exec_stk == EXSTACK_DEFAULT) : 0) + #else + # define elf_read_implies_exec(ex, exec_stk) (exec_stk == EXSTACK_DEFAULT) + #endif /* __powerpc64__ */ - .. __: https://github.com/torvalds/linux/blob/v4.9/fs/binfmt_elf.c#L849-L850 + .. [#ia64] + `source <https://github.com/torvalds/linux/blob/v6.3/arch/ia64/include/asm/elf.h#L203-L204>`__ - .. code-block:: c + .. code-block:: c + + #define elf_read_implies_exec(ex, executable_stack) \\ + ((executable_stack!=EXSTACK_DISABLE_X) && ((ex).e_flags & EF_IA_64_LINUX_EXECUTABLE_STACK) != 0) + + EF_IA_64_LINUX_EXECUTABLE_STACK__: + .. __: https://github.com/torvalds/linux/blob/v6.3/arch/ia64/include/asm/elf.h#L33 + + .. code-block:: c + + #define EF_IA_64_LINUX_EXECUTABLE_STACK 0x1 /* is stack (& heap) executable by default? */ + + .. [#the_rest] + `source <https://github.com/torvalds/linux/blob/v6.3/include/linux/elf.h#L13>`__ + + .. code-block:: c - if (elf_read_implies_exec(loc->elf_ex, executable_stack)) - current->personality |= READ_IMPLIES_EXEC; + # define elf_read_implies_exec(ex, have_pt_gnu_stack) 0 """ if not self.executable: return True - + + exec_bit = None for seg in self.iter_segments_by_type('GNU_STACK'): - return not bool(seg.header.p_flags & P_FLAGS.PF_X) + exec_bit = bool(seg.header.p_flags & P_FLAGS.PF_X) - # If you NULL out the PT_GNU_STACK section via ELF.disable_nx(), - # everything is executable. - return False + non_exec = exec_bit is False + missing = exec_bit is None + EF_IA_64_LINUX_EXECUTABLE_STACK = 1 + + if self.arch in ['i386', 'arm', 'aarch64', 'mips', 'mips64']: + if non_exec: + return True + elif missing: + return False + return None + elif self.arch == 'amd64': + return True if non_exec else None + elif self.arch == 'powerpc': + return not missing + elif self.arch == 'powerpc64': + return True + elif self.arch == 'ia64': + if non_exec: + return True + return not bool(self['e_flags'] & EF_IA_64_LINUX_EXECUTABLE_STACK) + + return True @property def execstack(self): - """:class:`bool`: Whether the current binary uses an executable stack. + """:class:`bool`: Whether dynamically loading the current binary will make the stack executable. - This is based on the presence of a program header PT_GNU_STACK_ - being present, and its setting. + This is based on the presence of a program header ``PT_GNU_STACK``, + its setting, and the default stack permissions for the architecture. - ``PT_GNU_STACK`` + If ``PT_GNU_STACK`` is persent, the stack permissions are `set according to it`__: - The p_flags member specifies the permissions on the segment - containing the stack and is used to indicate whether the stack - should be executable. The absense of this header indicates - that the stack will be executable. + .. __: https://github.com/bminor/glibc/blob/glibc-2.37/elf/dl-load.c#L1218-L1220 - In particular, if the header is missing the stack is executable. - If the header is present, it may **explicitly** mark that the stack is - executable. + .. code-block:: c + + case PT_GNU_STACK: + stack_flags = ph->p_flags; + break; + + Else, the stack permissions are set according to the architecture defaults + as `defined by`__ ``DEFAULT_STACK_PERMS``: - This is only somewhat accurate. When using the GNU Linker, it usees - DEFAULT_STACK_PERMS_ to decide whether a lack of ``PT_GNU_STACK`` - should mark the stack as executable: + .. __: https://github.com/bminor/glibc/blob/glibc-2.37/elf/dl-load.c#L1093-L1096 .. code-block:: c @@ -1754,29 +1927,25 @@ class ELF(ELFFile): :: $ git grep '#define DEFAULT_STACK_PERMS' | grep -v PF_X - sysdeps/aarch64/stackinfo.h:31:#define DEFAULT_STACK_PERMS (PF_R|PF_W) - sysdeps/nios2/stackinfo.h:31:#define DEFAULT_STACK_PERMS (PF_R|PF_W) - sysdeps/tile/stackinfo.h:31:#define DEFAULT_STACK_PERMS (PF_R|PF_W) - - .. _PT_GNU_STACK: https://refspecs.linuxbase.org/LSB_3.0.0/LSB-PDA/LSB-PDA/progheader.html - .. _DEFAULT_STACK_PERMS: https://github.com/bminor/glibc/blob/glibc-2.25/elf/dl-load.c#L1036-L1038 + sysdeps/aarch64/stackinfo.h: #define DEFAULT_STACK_PERMS (PF_R|PF_W) + sysdeps/arc/stackinfo.h: #define DEFAULT_STACK_PERMS (PF_R|PF_W) + sysdeps/csky/stackinfo.h: #define DEFAULT_STACK_PERMS (PF_R|PF_W) + sysdeps/ia64/stackinfo.h: #define DEFAULT_STACK_PERMS (PF_R|PF_W) + sysdeps/loongarch/stackinfo.h: #define DEFAULT_STACK_PERMS (PF_R | PF_W) + sysdeps/nios2/stackinfo.h: #define DEFAULT_STACK_PERMS (PF_R|PF_W) + sysdeps/riscv/stackinfo.h: #define DEFAULT_STACK_PERMS (PF_R | PF_W) """ - # Dynamic objects do not have the ability to change the executable state of the stack. if not self.executable: return False - # If NX is completely off for the process, the stack is executable. - if not self.nx: - return True - + # If the ``PT_GNU_STACK`` program header is preset, use it's premissions. + for seg in self.iter_segments_by_type('GNU_STACK'): + return bool(seg.header.p_flags & P_FLAGS.PF_X) + # If the ``PT_GNU_STACK`` program header is missing, then use the - # default rules. Only AArch64 gets a non-executable stack by default. - for _ in self.iter_segments_by_type('GNU_STACK'): - break - else: - return self.arch != 'aarch64' - - return False + # default rules. Out of the supported architectures, only AArch64, + # IA-64, and RISC-V get a non-executable stack by default. + return self.arch not in ['aarch64', 'ia64', 'riscv32', 'riscv64'] @property def canary(self): @@ -1851,6 +2020,7 @@ class ELF(ELFFile): "NX:".ljust(10) + { True: green("NX enabled"), False: red("NX disabled"), + None: yellow("NX unknown - GNU_STACK missing"), }[self.nx], "PIE:".ljust(10) + { True: green("PIE enabled"), @@ -1859,7 +2029,7 @@ class ELF(ELFFile): ]) # Execstack may be a thing, even with NX enabled, because of glibc - if self.execstack and self.nx: + if self.execstack and self.nx is not False: res.append("Stack:".ljust(10) + red("Executable")) # Are there any RWX areas in the binary? @@ -2071,6 +2241,8 @@ class ELF(ELFFile): if self.mmap[offset:offset+4] == PT_GNU_STACK: self.mmap[offset:offset+4] = b'\x00' * 4 self.save() + # Invalidate the cached segments, ``PT_GNU_STACK`` was removed. + self._segments = None return log.error("Could not find PT_GNU_STACK, stack should already be executable") diff --git a/pwnlib/elf/plt.py b/pwnlib/elf/plt.py index 34ff142..ff8153d 100644 --- a/pwnlib/elf/plt.py +++ b/pwnlib/elf/plt.py @@ -64,8 +64,11 @@ def prepare_unicorn_and_context(elf, got, address, data): 'arm': U.UC_ARCH_ARM, 'i386': U.UC_ARCH_X86, 'mips': U.UC_ARCH_MIPS, + 'mips64': U.UC_ARCH_MIPS, # 'powerpc': U.UC_ARCH_PPC, <-- Not actually supported 'thumb': U.UC_ARCH_ARM, + 'riscv32': U.UC_ARCH_RISCV, + 'riscv64': U.UC_ARCH_RISCV, }.get(elf.arch, None) if arch is None: @@ -119,18 +122,6 @@ def prepare_unicorn_and_context(elf, got, address, data): uc.mem_write(got, p_magic) - # Separately, Unicorn is apparently unable to hook unmapped memory - # accesses on MIPS. So we also have to map the page that contains - # the magic address. - start = magic_addr & (~0xfff) - try: - uc.mem_map(start, 0x1000) - except Exception: - # Ignore double-mapping - pass - trap = packing.p32(0x34000000, endian=elf.endian) - uc.mem_write(magic_addr, trap) - return uc, uc.context_save() diff --git a/pwnlib/fmtstr.py b/pwnlib/fmtstr.py index c4b7682..8dddc21 100644 --- a/pwnlib/fmtstr.py +++ b/pwnlib/fmtstr.py @@ -675,7 +675,7 @@ def sort_atoms(atoms, numbwritten): return out -def make_payload_dollar(data_offset, atoms, numbwritten=0, countersize=4): +def make_payload_dollar(data_offset, atoms, numbwritten=0, countersize=4, no_dollars=False): r''' Makes a format-string payload using glibc's dollar syntax to access the arguments. @@ -688,6 +688,7 @@ def make_payload_dollar(data_offset, atoms, numbwritten=0, countersize=4): atoms(list): list of atoms to execute numbwritten(int): number of byte already written by the printf function countersize(int): size in bytes of the format string counter (usually 4) + no_dollars(bool) : flag to generete the payload with or w/o $ notation Examples: >>> pwnlib.fmtstr.make_payload_dollar(1, [pwnlib.fmtstr.AtomWrite(0x0, 0x1, 0xff)]) @@ -697,6 +698,13 @@ def make_payload_dollar(data_offset, atoms, numbwritten=0, countersize=4): fmt = "" counter = numbwritten + + if no_dollars: + # since we can't dynamically offset, we have to increment manually the parameter index, use %c, so the number of bytes written is predictable + fmt += "%c" * (data_offset - 1) + # every %c write a byte, so we need to keep track of that to have the right pad + counter += data_offset - 1 + for idx, atom in enumerate(atoms): # set format string counter to correct value padding = atom.compute_padding(counter) @@ -707,9 +715,54 @@ def make_payload_dollar(data_offset, atoms, numbwritten=0, countersize=4): log.warn("padding is negative, this will not work on glibc") # perform write - if padding: + # if the padding is less than 3, it is more convenient to write it : [ len("cc") < len("%2c") ] , this could help save some bytes, if it is 3 it will take the same amout of bytes + # we also add ( context.bytes * no_dollars ) because , "%nccccccccc%n...ptr1ptr2" is more convenient than %"n%8c%n...ptr1ccccccccptr2" + if padding < 4 + context.bytes * no_dollars: + fmt += "c" * padding + ## if do not padded with %{n}c do not need to add something in data to use as argument, since we are not using a printf argument + else: fmt += "%" + str(padding) + "c" - fmt += "%" + str(data_offset + idx) + "$" + SPECIFIER[atom.size] + + if no_dollars: + data += b'c' * context.bytes + ''' + [ @murph12F was here ] + + the data += b'c' * context.bytes , is used to keey the arguments aligned when a %c is performed, so it wont use the actual address to write at + examplea stack and payload: + + fmtsr = %44c%hhn%66c%hhn + + --------- + | addr2 | + --------- + | 0x000 | + --------- + | addr1 | + --------- + | 0x000 | <-- (rsp) + --------- + + in this case the the first %44c will use the current arugument used pointed by rsp ( 0 ), and increment rsp + + --------- + | addr2 | + --------- + | 0X000 | + --------- + | addr1 | <-- (rsp) + --------- + | 0x000 | + --------- + + now it will perform the %hhn, and it will correctly use the addr1 argument + ''' + + if no_dollars: + fmt += "%" + SPECIFIER[atom.size] + else: + fmt += "%" + str(data_offset + idx) + "$" + SPECIFIER[atom.size] + data += pack(atom.start) return fmt.encode(), data @@ -762,7 +815,7 @@ def fmtstr_split(offset, writes, numbwritten=0, write_size='byte', write_size_ma return make_payload_dollar(offset, atoms, numbwritten) -def fmtstr_payload(offset, writes, numbwritten=0, write_size='byte', write_size_max='long', overflows=16, strategy="small", badbytes=frozenset(), offset_bytes=0): +def fmtstr_payload(offset, writes, numbwritten=0, write_size='byte', write_size_max='long', overflows=16, strategy="small", badbytes=frozenset(), offset_bytes=0, no_dollars=False): r"""fmtstr_payload(offset, writes, numbwritten=0, write_size='byte') -> str Makes payload with given parameter. @@ -779,6 +832,7 @@ def fmtstr_payload(offset, writes, numbwritten=0, write_size='byte', write_size_ write_size(str): must be ``byte``, ``short`` or ``int``. Tells if you want to write byte by byte, short by short or int by int (hhn, hn or n) overflows(int): how many extra overflows (at size sz) to tolerate to reduce the length of the format string strategy(str): either 'fast' or 'small' ('small' is default, 'fast' can be used if there are many writes) + no_dollars(bool) : flag to generete the payload with or w/o $ notation Returns: The payload in order to do needed writes @@ -798,9 +852,15 @@ def fmtstr_payload(offset, writes, numbwritten=0, write_size='byte', write_size_ >>> fmtstr_payload(1, {0x0: 0x1337babe}, write_size='byte') b'%19c%12$hhn%36c%13$hhn%131c%14$hhn%4c%15$hhn\x03\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00' >>> fmtstr_payload(1, {0x0: 0x00000001}, write_size='byte') - b'%1c%3$na\x00\x00\x00\x00' + b'c%3$naaa\x00\x00\x00\x00' >>> fmtstr_payload(1, {0x0: b"\xff\xff\x04\x11\x00\x00\x00\x00"}, write_size='short') b'%327679c%7$lln%18c%8$hhn\x00\x00\x00\x00\x03\x00\x00\x00' + >>> fmtstr_payload(10, {0x404048 : 0xbadc0ffe, 0x40403c : 0xdeadbeef}, no_dollars=True) + b'%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%125c%hhn%17c%hhn%32c%hhn%17c%hhn%203c%hhn%34c%hhn%3618c%hnacccc>@@\x00cccc=@@\x00cccc?@@\x00cccc<@@\x00ccccK@@\x00ccccJ@@\x00ccccH@@\x00' + >>> fmtstr_payload(6, {0x404048 : 0xbadbad00}, no_dollars=True) + b'%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%229c%hhn%173c%hhn%13c%hhn%33c%hhnccccH@@\x00ccccI@@\x00ccccK@@\x00ccccJ@@\x00' + >>> fmtstr_payload(6, {0x4040 : 0xbadbad00, 0x4060: 0xbadbad02}, no_dollars=True) + b'%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%212c%hhn%173c%hhn%13c%hhn%33c%hhn%39c%hhn%171c%hhn%13c%hhn%33c%hhnacccc@@\x00\x00ccccA@\x00\x00ccccC@\x00\x00ccccB@\x00\x00cccc`@\x00\x00cccca@\x00\x00ccccc@\x00\x00ccccb@\x00\x00' """ sz = WRITE_SIZE[write_size] szmax = WRITE_SIZE[write_size_max] @@ -809,7 +869,7 @@ def fmtstr_payload(offset, writes, numbwritten=0, write_size='byte', write_size_ fmt = b"" for _ in range(1000000): data_offset = (offset_bytes + len(fmt)) // context.bytes - fmt, data = make_payload_dollar(offset + data_offset, all_atoms, numbwritten=numbwritten) + fmt, data = make_payload_dollar(offset + data_offset, all_atoms, numbwritten=numbwritten, no_dollars=no_dollars) fmt = fmt + cyclic((-len(fmt)-offset_bytes) % context.bytes) if len(fmt) + offset_bytes == data_offset * context.bytes: diff --git a/pwnlib/gdb.py b/pwnlib/gdb.py index 049035a..753955e 100644 --- a/pwnlib/gdb.py +++ b/pwnlib/gdb.py @@ -599,7 +599,9 @@ def get_gdb_arch(): 'powerpc64': 'powerpc:common64', 'mips64': 'mips:isa64', 'thumb': 'arm', - 'sparc64': 'sparc:v9' + 'sparc64': 'sparc:v9', + 'riscv32': 'riscv:rv32', + 'riscv64': 'riscv:rv64', }.get(context.arch, context.arch) def binary(): diff --git a/pwnlib/libcdb.py b/pwnlib/libcdb.py index 98f51ad..932f295 100644 --- a/pwnlib/libcdb.py +++ b/pwnlib/libcdb.py @@ -77,13 +77,15 @@ def provider_libc_rip(hex_encoded_id, hash_type): data = b"" try: result = requests.post(url, json=params, timeout=20) - if result.status_code != 200 or len(result.json()) == 0: + result.raise_for_status() + libc_match = result.json() + if not libc_match: log.warn_once("Could not find libc for %s %s on libc.rip", hash_type, hex_encoded_id) - log.debug("Error: %s", result.text) return None - libc_match = result.json() - assert len(libc_match) == 1, 'Invalid libc.rip response.' + if len(libc_match) > 1: + log.debug("Received multiple matches. Choosing the first match and discarding the others.") + log.debug("%r", libc_match) url = libc_match[0]['download_url'] log.debug("Downloading data from libc.rip: %s", url) @@ -217,7 +219,7 @@ def unstrip_libc(filename): >>> libc = ELF(filename) >>> hex(libc.symbols.main_arena) '0x219c80' - >>> unstrip_libc(which('python')) + >>> unstrip_libc(pwnlib.data.elf.get('test-x86')) False >>> filename = search_by_build_id('d1704d25fbbb72fa95d517b883131828c0883fe9', unstrip=True) >>> 'main_arena' in ELF(filename).symbols @@ -253,6 +255,98 @@ def unstrip_libc(filename): return True +def _handle_multiple_matching_libcs(matching_libcs): + from pwnlib.term import text + from pwnlib.ui import options + log.info('Multiple matching libc libraries for requested symbols:') + for idx, libc in enumerate(matching_libcs): + log.info('%d. %s', idx+1, text.red(libc['id'])) + log.indented('\t%-20s %s', text.green('BuildID:'), libc['buildid']) + log.indented('\t%-20s %s', text.green('MD5:'), libc['md5']) + log.indented('\t%-20s %s', text.green('SHA1:'), libc['sha1']) + log.indented('\t%-20s %s', text.green('SHA256:'), libc['sha256']) + log.indented('\t%s', text.green('Symbols:')) + for symbol, address in libc['symbols'].items(): + log.indented('\t%25s = %s', symbol, address) + + selected_index = options("Select the libc version to use:", [libc['id'] for libc in matching_libcs]) + return matching_libcs[selected_index] + +def search_by_symbol_offsets(symbols, select_index=None, unstrip=True, return_as_list=False): + """ + Lookup possible matching libc versions based on leaked function addresses. + + The leaked function addresses have to be provided as a dict mapping the + function name to the leaked value. Only the lower 3 nibbles are relevant + for the lookup. + + If there are multiple matches you are presented with a list to select one + interactively, unless the ``select_index`` or ``return_as_list`` arguments + are used. + + Arguments: + symbols(dict): + Dictionary mapping symbol names to their addresses. + select_index(int): + The libc to select if there are multiple matches (starting at 1). + unstrip(bool): + Try to fetch debug info for the libc and apply it to the downloaded file. + return_as_list(bool): + Return a list of build ids of all matching libc versions + instead of a path to a downloaded file. + + Returns: + Path to the downloaded library on disk, or :const:`None`. + If the ``return_as_list`` argument is :const:`True`, a list of build ids + is returned instead. + + Examples: + >>> filename = search_by_symbol_offsets({'puts': 0x420, 'printf': 0xc90}, select_index=1) + >>> libc = ELF(filename) + >>> libc.sym.system == 0x52290 + True + >>> matched_libcs = search_by_symbol_offsets({'__libc_start_main_ret': '7f89ad926550'}, return_as_list=True) + >>> len(matched_libcs) > 1 + True + >>> for buildid in matched_libcs: # doctest +SKIP + ... libc = ELF(search_by_build_id(buildid)) # doctest +SKIP + """ + import requests + for symbol, address in symbols.items(): + if isinstance(address, int): + symbols[symbol] = hex(address) + try: + params = {'symbols': symbols} + url = "https://libc.rip/api/find" + log.debug('Request: %s', params) + result = requests.post(url, json=params, timeout=20) + result.raise_for_status() + + matching_libcs = result.json() + log.debug('Result: %s', matching_libcs) + if len(matching_libcs) == 0: + log.warn_once("No matching libc for symbols %r on libc.rip", symbols) + return None + + if return_as_list: + return [libc['buildid'] for libc in matching_libcs] + + if len(matching_libcs) == 1: + return search_by_build_id(matching_libcs[0]['buildid'], unstrip=unstrip) + + if select_index is not None: + if select_index > 0 and select_index <= len(matching_libcs): + return search_by_build_id(matching_libcs[select_index - 1]['buildid'], unstrip=unstrip) + else: + log.error('Invalid selected libc index. %d is not in the range of 1-%d.', select_index, len(matching_libcs)) + return None + + selected_libc = _handle_multiple_matching_libcs(matching_libcs) + return search_by_build_id(selected_libc['buildid'], unstrip=unstrip) + except requests.RequestException as e: + log.warn_once("Failed to lookup libc for symbols %r from libc.rip: %s", symbols, e) + return None + def search_by_build_id(hex_encoded_id, unstrip=True): """ Given a hex-encoded Build ID, attempt to download a matching libc from libcdb. @@ -409,4 +503,4 @@ def get_build_id_offsets(): }.get(context.arch, []) -__all__ = ['get_build_id_offsets', 'search_by_build_id', 'search_by_sha1', 'search_by_sha256', 'search_by_md5', 'unstrip_libc'] +__all__ = ['get_build_id_offsets', 'search_by_build_id', 'search_by_sha1', 'search_by_sha256', 'search_by_md5', 'unstrip_libc', 'search_by_symbol_offsets'] diff --git a/pwnlib/log.py b/pwnlib/log.py index df01a52..4e04997 100644 --- a/pwnlib/log.py +++ b/pwnlib/log.py @@ -84,7 +84,7 @@ result in a message being emitted but rather an animated progress line (with a spinner!) being created. Note that other handlers will still see a meaningful log record. -The custom handler will only handle log records whith a level of at least +The custom handler will only handle log records with a level of at least :data:`context.log_level`. Thus if e.g. the level for the ``'pwnlib.tubes.ssh'`` is set to ``'DEBUG'`` no additional output will show up unless :data:`context.log_level` is also set to ``'DEBUG'``. Other handlers diff --git a/pwnlib/rop/rop.py b/pwnlib/rop/rop.py index 0756ad5..2c99282 100644 --- a/pwnlib/rop/rop.py +++ b/pwnlib/rop/rop.py @@ -1544,23 +1544,18 @@ class ROP(object): # Prioritise non-PIE binaries so we can use _fini exes = (elf for elf in self.elfs if not elf.library and elf.bits == 64) - nonpie = csu = None + csu = None for elf in exes: - if not elf.pie: - if '__libc_csu_init' in elf.symbols: - break - nonpie = elf - elif '__libc_csu_init' in elf.symbols: + if '__libc_csu_init' in elf.symbols: csu = elf + if not elf.pie: + break + + if csu: + elf = csu else: log.error('No non-library binaries in [elfs]') - if elf.pie: - if nonpie: - elf = nonpie - elif csu: - elf = csu - from .ret2csu import ret2csu ret2csu(self, elf, edi, rsi, rdx, rbx, rbp, r12, r13, r14, r15, call) diff --git a/pwnlib/shellcraft/templates/amd64/windows/__doc__ b/pwnlib/shellcraft/templates/amd64/windows/__doc__ new file mode 100644 index 0000000..8e8d7f8 --- /dev/null +++ b/pwnlib/shellcraft/templates/amd64/windows/__doc__ @@ -0,0 +1 @@ +Shellcraft module containing Intel x86_64 shellcodes for Windows. diff --git a/pwnlib/shellcraft/templates/amd64/windows/cmd.asm b/pwnlib/shellcraft/templates/amd64/windows/cmd.asm new file mode 100644 index 0000000..4d44995 --- /dev/null +++ b/pwnlib/shellcraft/templates/amd64/windows/cmd.asm @@ -0,0 +1,9 @@ +<% + from pwnlib.shellcraft import amd64 +%> +<%docstring>Execute cmd.exe and keep the parent process +in an infinite loop. +</%docstring> + + ${amd64.windows.winexec(b'cmd.exe')} + ${amd64.infloop()} diff --git a/pwnlib/shellcraft/templates/amd64/windows/getexport.asm b/pwnlib/shellcraft/templates/amd64/windows/getexport.asm new file mode 100644 index 0000000..76f8543 --- /dev/null +++ b/pwnlib/shellcraft/templates/amd64/windows/getexport.asm @@ -0,0 +1,55 @@ +<% + from pwnlib.shellcraft import amd64, pretty + from pwnlib.util.packing import u64, _need_bytes +%> +<%docstring>Find the address of an exported function in a dll +by manually iterating through the PE export table. + +Args: + function_name (str): The name of the function to find. + dll(str): The name of the DLL to find the function in. + dest (str): The register to load the function address into. +</%docstring> +<%page args="function_name,dll='kernel32.dll',dest='rax'"/> +<% +function_name = _need_bytes(function_name) +dll = _need_bytes(dll) +if len(function_name) > 8: + raise ValueError('function_name must be <= 8 bytes') +assert dll == b'kernel32.dll' +%> + ${amd64.windows.kernel32base('rbx')} /* rbx = kernel32.dll PE base */ + mov r8d, [rbx + 0x3c] + mov rdx, r8 + add rdx, rbx + ${amd64.mov('r9', 0x88)} + add rdx, r9 + mov r8d, [rdx] + add r8, rbx /* r8 = export table */ + mov esi, [r8 + 0x20] + add rsi, rbx /* rsi = names table */ + xor rcx, rcx +% if len(function_name) <= 8: + mov r9, ${pretty(u64(function_name.ljust(8, b'\x00')))} +% else: + ${amd64.pushstr(function_name)} + mov r9, rsp +% endif + + /* Loop through the names table */ + FindFunction: + inc rcx + mov eax, [rsi + rcx * 4] + add rax, rbx + ## TODO: implement strcmp properly for function names > 8 bytes + cmp qword ptr [rax], r9 + jnz FindFunction + + mov esi, [r8 + 0x24] + add rsi, rbx /* rsi = ordinals table */ + mov cx, [rsi + rcx * 2] + mov esi, [r8 + 0x1c] + add rsi, rbx /* rsi = address table */ + mov eax, [rsi + rcx * 4] + add rax, rbx /* rax = function address */ + mov ${dest}, rax diff --git a/pwnlib/shellcraft/templates/amd64/windows/getprocaddress.asm b/pwnlib/shellcraft/templates/amd64/windows/getprocaddress.asm new file mode 100644 index 0000000..6067a76 --- /dev/null +++ b/pwnlib/shellcraft/templates/amd64/windows/getprocaddress.asm @@ -0,0 +1,28 @@ +<% + from pwnlib.shellcraft import amd64, pretty + from pwnlib.util.packing import _need_bytes + from pwnlib.util.misc import align +%> +<%docstring>Find the address of an exported function +by calling kernel32::GetProcAddress. + +Args: + function_name(str): The name of the function to find. + dll(str): The name of the DLL to find the function in. + dest (str): The register to load the function address into. +</%docstring> +<%page args="function_name,dll='kernel32.dll',dest='rax'"/> +<% +function_name = _need_bytes(function_name) +dll = _need_bytes(dll) +assert dll == b'kernel32.dll' +%> + + ${amd64.windows.getexport(b'GetProcA', b'kernel32.dll', dest='rdi')} + ${amd64.pushstr(function_name)} + mov rdx, rsp + ${amd64.windows.kernel32base(dest='rcx')} + sub rsp, 0x30 + call rdi + add rsp, ${pretty(0x30+align(8, len(function_name)))} + mov ${dest}, rax diff --git a/pwnlib/shellcraft/templates/amd64/windows/kernel32base.asm b/pwnlib/shellcraft/templates/amd64/windows/kernel32base.asm new file mode 100644 index 0000000..71d1201 --- /dev/null +++ b/pwnlib/shellcraft/templates/amd64/windows/kernel32base.asm @@ -0,0 +1,18 @@ +<% from pwnlib.shellcraft import amd64 %> +<%docstring>Find the base address of kernel32.dll in memory. + +Args: + dest (str): The register to load the kernel32.dll base address into. +</%docstring> +<%page args="dest='rax'"/> +## The loaded list of modules always starts with +## 1. the executable itself +## 2. ntdll.dll +## 3. kernel32.dll + ${amd64.windows.peb(dest)} + mov ${dest}, [${dest} + 0x18] /* PEB->Ldr */ + mov rsi, [${dest} + 0x20] /* PEB->Ldr.InMemOrder LIST_ENTRY */ + lodsq + xchg rax, rsi + lodsq + mov ${dest}, [rax + 0x20] /* LDR_DATA_TABLE_ENTRY->DllBase */
\ No newline at end of file diff --git a/pwnlib/shellcraft/templates/amd64/windows/ntdllbase.asm b/pwnlib/shellcraft/templates/amd64/windows/ntdllbase.asm new file mode 100644 index 0000000..454f295 --- /dev/null +++ b/pwnlib/shellcraft/templates/amd64/windows/ntdllbase.asm @@ -0,0 +1,16 @@ +<% from pwnlib.shellcraft import amd64 %> +<%docstring>Find the base address of ntdll.dll in memory. + +Args: + dest (str): The register to load the ntdll.dll base address into. +</%docstring> +<%page args="dest='rax'"/> +## The loaded list of modules always starts with: +## 1. the executable itself +## 2. ntdll.dll +## 3. kernel32.dll + ${amd64.windows.peb(dest)} + mov ${dest}, [${dest} + 0x18] /* PEB->Ldr */ + mov rsi, [${dest} + 0x20] /* PEB->Ldr.InMemOrder LIST_ENTRY */ + lodsq + mov ${dest}, [rax + 0x20] /* LDR_DATA_TABLE_ENTRY->DllBase */ diff --git a/pwnlib/shellcraft/templates/amd64/windows/peb.asm b/pwnlib/shellcraft/templates/amd64/windows/peb.asm new file mode 100644 index 0000000..ee11e21 --- /dev/null +++ b/pwnlib/shellcraft/templates/amd64/windows/peb.asm @@ -0,0 +1,8 @@ +<% from pwnlib.shellcraft import amd64 %> +<%docstring>Loads the Process Environment Block (PEB) into the target register. + +Args: + dest (str): The register to load the PEB into. +</%docstring> +<%page args="dest='rax'"/> + ${amd64.windows.teb(dest, 0x60)} diff --git a/pwnlib/shellcraft/templates/amd64/windows/teb.asm b/pwnlib/shellcraft/templates/amd64/windows/teb.asm new file mode 100644 index 0000000..6ce201e --- /dev/null +++ b/pwnlib/shellcraft/templates/amd64/windows/teb.asm @@ -0,0 +1,21 @@ +<% + from pwnlib.log import getLogger + from pwnlib.shellcraft import pretty + from pwnlib.shellcraft.registers import get_register, is_register + log = getLogger('pwnlib.shellcraft.amd64.teb') +%> +<%page args="dest='rax', offset=0"/> +<%docstring>Loads the Thread Environment Block (TEB) into the target register. + +Args: + dest (str): The register to load the TEB into. + offset (int): The offset from the TEB to load. +</%docstring> +<% +if not is_register(dest): + log.error('%r is not a register' % dest) + +dest = get_register(dest) +%> + xor esi, esi + mov ${dest}, qword ptr gs:[rsi+${pretty(offset)}] diff --git a/pwnlib/shellcraft/templates/amd64/windows/winexec.asm b/pwnlib/shellcraft/templates/amd64/windows/winexec.asm new file mode 100644 index 0000000..d680520 --- /dev/null +++ b/pwnlib/shellcraft/templates/amd64/windows/winexec.asm @@ -0,0 +1,21 @@ +<% + from pwnlib.shellcraft import amd64, pretty + from pwnlib.util.packing import _need_bytes + from pwnlib.util.misc import align +%> +<%docstring>Execute a program using WinExec. + +Args: + cmd (str): The program to execute. +</%docstring> +<%page args="cmd"/> +<% +cmd = _need_bytes(cmd) +%> + + ${amd64.windows.getprocaddress(b'WinExec', b'kernel32.dll', 'rsi')} + ${amd64.pushstr(cmd)} + mov rcx, rsp + sub rsp, 0x30 + call rsi + add rsp, ${pretty(0x30+align(8, len(cmd)))} diff --git a/pwnlib/term/readline.py b/pwnlib/term/readline.py index 6891f12..b60b656 100644 --- a/pwnlib/term/readline.py +++ b/pwnlib/term/readline.py @@ -404,7 +404,7 @@ def readline(_size=-1, prompt='', float=True, priority=10): buffer = (buffer_left + buffer_right) if buffer: history.insert(0, buffer) - return force_to_bytes(buffer) + b'\n' + return force_to_bytes(buffer) except KeyboardInterrupt: control_c() finally: diff --git a/pwnlib/tubes/ssh.py b/pwnlib/tubes/ssh.py index cd06f34..adda422 100644 --- a/pwnlib/tubes/ssh.py +++ b/pwnlib/tubes/ssh.py @@ -57,7 +57,7 @@ class ssh_channel(sock): #: Command specified for the constructor process = None - def __init__(self, parent, process = None, tty = False, wd = None, env = None, raw = True, *args, **kwargs): + def __init__(self, parent, process = None, tty = False, cwd = None, env = None, raw = True, *args, **kwargs): super(ssh_channel, self).__init__(*args, **kwargs) # keep the parent from being garbage collected in some cases @@ -68,9 +68,9 @@ class ssh_channel(sock): self.tty = tty self.env = env self.process = process - self.cwd = wd or '.' - if isinstance(wd, six.text_type): - wd = packing._need_bytes(wd, 2, 0x80) + self.cwd = cwd or '.' + if isinstance(cwd, six.text_type): + cwd = packing._need_bytes(cwd, 2, 0x80) env = env or {} msg = 'Opening new channel: %r' % (process or 'shell') @@ -80,8 +80,8 @@ class ssh_channel(sock): if isinstance(process, six.text_type): process = packing._need_bytes(process, 2, 0x80) - if process and wd: - process = b'cd ' + sh_string(wd) + b' >/dev/null 2>&1; ' + process + if process and cwd: + process = b'cd ' + sh_string(cwd) + b' >/dev/null 2>&1; ' + process if process and env: for name, value in env.items(): @@ -841,8 +841,11 @@ class ssh(Timeout, Logger): >>> sh = s.process(executable='/bin/sh') >>> str(sh.pid).encode() in s.pidof('sh') # doctest: +SKIP True - >>> s.process(['pwd'], cwd='/tmp').recvall() + >>> io = s.process(['pwd'], cwd='/tmp') + >>> io.recvall() b'/tmp\n' + >>> io.cwd + '/tmp' >>> p = s.process(['python','-c','import os; os.write(1, os.read(2, 1024))'], stderr=0) >>> p.send(b'hello') >>> p.recv() @@ -1068,7 +1071,7 @@ os.execve(exe, argv, env) script = 'echo PWNTOOLS; for py in python3 python2.7 python2 python; do test -x "$(which $py 2>&1)" && echo $py && exec $py -c %s check; done; echo 2' % sh_string(script) with context.quiet: - python = ssh_process(self, script, tty=True, raw=True, level=self.level, timeout=timeout) + python = ssh_process(self, script, tty=True, cwd=cwd, raw=True, level=self.level, timeout=timeout) try: python.recvline_contains(b'PWNTOOLS') # Magic flag so that any sh/bash initialization errors are swallowed @@ -1151,14 +1154,19 @@ os.execve(exe, argv, env) Examples: >>> s = ssh(host='example.pwnme') - >>> py = s.run('python -i') + >>> py = s.system('python3 -i') >>> _ = py.recvuntil(b'>>> ') >>> py.sendline(b'print(2+2)') - >>> py.sendline(b'exit') + >>> py.sendline(b'exit()') >>> print(repr(py.recvline())) b'4\n' >>> s.system('env | grep -a AAAA', env={'AAAA': b'\x90'}).recvall() b'AAAA=\x90\n' + >>> io = s.system('pwd', wd='/tmp') + >>> io.recvall() + b'/tmp\n' + >>> io.cwd + '/tmp' """ if wd is None: @@ -1377,17 +1385,10 @@ from ctypes import *; libc = CDLL('libc.so.6'); print(libc.getenv(%r)) if status != 0: return None - # OpenSSL outputs in the format of... - # (stdin)= e3b0c4429... - data = data.replace(b'(stdin)= ',b'') - - # sha256 and sha256sum outputs in the format of... - # e3b0c442... - - data = data.replace(b'-',b'').strip() - if not isinstance(data, str): data = data.decode('ascii') + data = re.search("([a-fA-F0-9]{64})",data).group() return data def _get_cachefile(self, fingerprint): @@ -1474,7 +1475,7 @@ from ctypes import *; libc = CDLL('libc.so.6'); print(libc.getenv(%r)) self._download_raw(remote, local, p) if not self._verify_local_fingerprint(fingerprint): - p.error('Could not download file %r' % remote) + self.error('Could not download file %r', remote) return local @@ -1509,17 +1510,25 @@ from ctypes import *; libc = CDLL('libc.so.6'); print(libc.getenv(%r)) calling the function twice has little overhead. Arguments: - remote(str): The remote filename to download + remote(str/bytes): The remote filename to download local(str): The local filename to save it to. Default is to infer it from the remote filename. + + Examples: + >>> with open('/tmp/foobar','w+') as f: + ... _ = f.write('Hello, world') + >>> s = ssh(host='example.pwnme', + ... cache=False) + >>> _ = s.set_working_directory(wd='/tmp') + >>> _ = s.download_file('foobar', 'barfoo') + >>> with open('barfoo','r') as f: + ... print(f.read()) + Hello, world """ if not local: local = os.path.basename(os.path.normpath(remote)) - if os.path.basename(remote) == remote: - remote = os.path.join(self.cwd, remote) - with self.progress('Downloading %r to %r' % (remote, local)) as p: local_tmp = self._download_to_cache(remote, p) @@ -1527,7 +1536,7 @@ from ctypes import *; libc = CDLL('libc.so.6'); print(libc.getenv(%r)) if not os.path.exists(local) or hashes.sha256filehex(local_tmp) != hashes.sha256filehex(local): shutil.copy2(local_tmp, local) - def download_dir(self, remote=None, local=None): + def download_dir(self, remote=None, local=None, ignore_failed_read=False): """Recursively downloads a directory from the remote server Arguments: @@ -1540,17 +1549,22 @@ from ctypes import *; libc = CDLL('libc.so.6'); print(libc.getenv(%r)) remote = packing._encode(self.sftp.normalize(remote)) else: with context.local(log_level='error'): - remote = self.system(b'readlink -f ' + sh_string(remote)) + remote = self.system(b'readlink -f ' + sh_string(remote)).recvall().strip() local = local or '.' local = os.path.expanduser(local) self.info("Downloading %r to %r" % (remote, local)) + if ignore_failed_read: + opts = b" --ignore-failed-read" + else: + opts = b"" with context.local(log_level='error'): remote_tar = self.mktemp() - cmd = b'tar -C %s -czf %s .' % \ - (sh_string(remote), + cmd = b'tar %s -C %s -czf %s .' % \ + (opts, + sh_string(remote), sh_string(remote_tar)) tar = self.system(cmd) @@ -1560,6 +1574,11 @@ from ctypes import *; libc = CDLL('libc.so.6'); print(libc.getenv(%r)) local_tar = tempfile.NamedTemporaryFile(suffix='.tar.gz') self.download_file(remote_tar, local_tar.name) + # Delete temporary tarfile from remote host + if self.sftp: + self.unlink(remote_tar) + else: + self.system(b'rm ' + sh_string(remote_tar)).wait() tar = tarfile.open(local_tar.name) tar.extractall(local) @@ -1691,6 +1710,18 @@ from ctypes import *; libc = CDLL('libc.so.6'); print(libc.getenv(%r)) file_or_directory(str): Path to the file or directory to download. local(str): Local path to store the data. By default, uses the current directory. + + + Examples: + >>> with open('/tmp/foobar','w+') as f: + ... _ = f.write('Hello, world') + >>> s = ssh(host='example.pwnme', + ... cache=False) + >>> _ = s.set_working_directory('/tmp') + >>> _ = s.download('foobar', 'barfoo') + >>> with open('barfoo','r') as f: + ... print(f.read()) + Hello, world """ file_or_directory = packing._encode(file_or_directory) with self.system(b'test -d ' + sh_string(file_or_directory)) as io: @@ -1766,7 +1797,7 @@ from ctypes import *; libc = CDLL('libc.so.6'); print(libc.getenv(%r)) if self.cwd != '.': cmd = 'cd ' + sh_string(self.cwd) - s.sendline(cmd) + s.sendline(packing._need_bytes(cmd, 2, 0x80)) s.interactive() s.close() @@ -1824,6 +1855,13 @@ from ctypes import *; libc = CDLL('libc.so.6'); print(libc.getenv(%r)) >>> _=s.set_working_directory(symlink=symlink) >>> assert b'foo' in s.ls().split(), s.ls().split() >>> assert homedir != s.pwd() + + >>> _=s.set_working_directory() + >>> io = s.system('pwd') + >>> io.recvallS().strip() == io.cwd + True + >>> io.cwd == s.cwd + True """ status = 0 diff --git a/pwnlib/tubes/tube.py b/pwnlib/tubes/tube.py index 91ca4f2..39a27d8 100644 --- a/pwnlib/tubes/tube.py +++ b/pwnlib/tubes/tube.py @@ -4,6 +4,7 @@ from __future__ import division import abc import logging +import os import re import six import string @@ -745,9 +746,9 @@ class tube(Timeout, Logger): return self.buffer.get() def recvall(self, timeout=Timeout.forever): - """recvall() -> bytes + """recvall(timeout=Timeout.forever) -> bytes - Receives data until EOF is reached. + Receives data until EOF is reached and closes the tube. """ with self.waitfor('Receiving all data') as h: @@ -867,7 +868,7 @@ class tube(Timeout, Logger): is much more usable, since we are using :mod:`pwnlib.term` to print a floating prompt. - Thus it only works in while in :data:`pwnlib.term.term_mode`. + Thus it only works while in :data:`pwnlib.term.term_mode`. """ self.info('Switching to interactive mode') @@ -892,13 +893,44 @@ class tube(Timeout, Logger): t.daemon = True t.start() + from pwnlib.args import term_mode try: + os_linesep = os.linesep.encode() + to_skip = b'' while not go.isSet(): if term.term_mode: data = term.readline.readline(prompt = prompt, float = True) + if data: + data += self.newline else: stdin = getattr(sys.stdin, 'buffer', sys.stdin) data = stdin.read(1) + # Keep OS's line separator if NOTERM is set and + # the user did not specify a custom newline + # even if stdin is a tty. + if sys.stdin.isatty() and ( + term_mode + or context.newline != b"\n" + or self._newline is not None + ): + if to_skip: + if to_skip[:1] != data: + data = os_linesep[: -len(to_skip)] + data + else: + to_skip = to_skip[1:] + if to_skip: + continue + data = self.newline + # If we observe a prefix of the line separator in a tty, + # assume we'll see the rest of it immediately after. + # This could stall until the next character is seen if + # the line separator is started but never finished, but + # that is unlikely to happen in a dynamic tty. + elif data and os_linesep.startswith(data): + if len(os_linesep) > 1: + to_skip = os_linesep[1:] + continue + data = self.newline if data: try: @@ -1329,7 +1361,7 @@ class tube(Timeout, Logger): Should not be called directly. Sends data to the tube. Should return ``exceptions.EOFError``, if it is unable to send any - more, because of a close tube. + more, because of a closed tube. """ raise EOFError('Not implemented') @@ -1345,9 +1377,8 @@ class tube(Timeout, Logger): def timeout_change(self): """ - Informs the raw layer of the tube that the timeout has changed. + Should not be called directly. Informs the raw layer of the tube that the timeout has changed. - Should not be called directly. Inherited from :class:`Timeout`. """ diff --git a/pwnlib/util/packing.py b/pwnlib/util/packing.py index 1565116..15dcddc 100644 --- a/pwnlib/util/packing.py +++ b/pwnlib/util/packing.py @@ -168,7 +168,7 @@ def pack(number, word_size = None, endianness = None, sign = None, **kwargs): def unpack(data, word_size = None): r"""unpack(data, word_size = None, endianness = None, sign = None, **kwargs) -> int - Packs arbitrary-sized integer. + Unpacks arbitrary-sized integer. Word-size, endianness and signedness is done according to context. @@ -239,7 +239,7 @@ def unpack(data, word_size = None): @LocalNoarchContext def unpack_many(data, word_size = None): - """unpack(data, word_size = None, endianness = None, sign = None) -> int list + """unpack_many(data, word_size = None, endianness = None, sign = None) -> int list Splits `data` into groups of ``word_size//8`` bytes and calls :func:`unpack` on each group. Returns a list of the results. @@ -1065,6 +1065,9 @@ def _encode(s): return s.encode(context.encoding) def _decode(b): + if isinstance(b, (str, six.text_type)): + return b # already text + if context.encoding == 'auto': try: return b.decode('utf-8') diff --git a/pwnlib/version.py b/pwnlib/version.py index 8e85738..6c387e4 100644 --- a/pwnlib/version.py +++ b/pwnlib/version.py @@ -1 +1 @@ -__version__ = '4.10.0' +__version__ = '4.11.0' diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..78d315e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,71 @@ +[build-system] +requires = ["setuptools>=44.0", "wheel", "toml; python_version<'3.4'"] +build-backend = "setuptools.build_meta" + +[project] +dynamic = ["version", "scripts"] + +name = "pwntools" +description = "Pwntools CTF framework and exploit development library." +license = {text = "Mostly MIT, some GPL/BSD, see LICENSE-pwntools.txt"} +readme = "README.md" +authors = [{name = "Gallopsled et al.", email = "pwntools-users@googlegroups.com"}] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Topic :: Security", + "Topic :: Software Development :: Assemblers", + "Topic :: Software Development :: Debuggers", + "Topic :: Software Development :: Disassemblers", + "Topic :: Software Development :: Embedded Systems", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: System :: System Shells", + "Topic :: Utilities", +] +keywords = ["pwntools", "exploit", "ctf", "capture", "the", "flag", "binary", "wargame", "overflow", "stack", "heap", "defcon"] + +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'", + "capstone>=3.0.5rc2", # see Gallopsled/pwntools#971, Gallopsled/pwntools#1160 + "ropgadget>=5.3", + "pyserial>=2.7", + "requests>=2.0", + "pip>=6.0.8", + "pygments>=2.0", + "pysocks", + "python-dateutil", + "packaging", + "psutil>=3.3.0", + "intervaltree>=3.0", + "sortedcontainers", + "unicorn>=1.0.2rc1", # see unicorn-engine/unicorn#1100 and #1170 + "six>=1.12.0", + "rpyc", + "colored_traceback", + "pathlib2; python_version < '3.4'", +] + +[project.urls] +homepage = "https://pwntools.com" +download = "https://github.com/Gallopsled/pwntools/releases" + +[tool.distutils.bdist_wheel] +universal = 1 + +[tool.setuptools] +include-package-data = false + +[tool.setuptools.packages.find] +namespaces = false diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 49d4929..0000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -# This requirements.txt file simply hits the setup.py file and -# installs the 'install_requires' python modules, the magic command -# to do that is simply '-e .': so here it is - -# Amazing command --e . @@ -11,7 +11,6 @@ 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 @@ -45,30 +44,15 @@ for filename in glob.glob('pwnlib/commandline/*'): if not flag: console_scripts.append(script) -install_requires = ['paramiko>=1.15.2', - 'mako>=1.0.0', - 'pyelftools>=0.2.4', - 'capstone>=3.0.5rc2', # See Gallopsled/pwntools#971, Gallopsled/pwntools#1160 - 'ropgadget>=5.3', - 'pyserial>=2.7', - 'requests>=2.0', - 'pip>=6.0.8', - 'pygments>=2.0', - 'pysocks', - 'python-dateutil', - 'packaging', - 'psutil>=3.3.0', - 'intervaltree>=3.0', - 'sortedcontainers', - # see unicorn-engine/unicorn#1100 and #1170 - 'unicorn>=1.0.2rc1', - 'six>=1.12.0', - 'rpyc', - 'colored_traceback', -] +compat = {} +if sys.version_info < (3, 4): + import toml + project = toml.load('pyproject.toml')['project'] + compat['install_requires'] = project['dependencies'] + compat['name'] = project['name'] + if '--user' in sys.argv: + sys.argv.remove('--user') -if platform.python_version_tuple()[0] == '2': - install_requires += ['pathlib2'] # Check that the user has installed the Python development headers PythonH = os.path.join(get_python_inc(), 'Python.h') @@ -77,19 +61,8 @@ if not os.path.exists(PythonH): print("$ apt-get install python-dev", file=sys.stderr) sys.exit(-1) -# Convert README.md to reStructuredText for PyPI -long_description = '' -try: - long_description = subprocess.check_output(['pandoc', 'README.md', '--to=rst'], universal_newlines=True) -except Exception as e: - print("Failed to convert README.md through pandoc, proceeding anyway", file=sys.stderr) - traceback.print_exc() - setup( - name = 'pwntools', - python_requires = '>=2.7', - packages = find_packages(), - version = '4.10.0', + version = '4.11.0', data_files = [('pwntools-doc', glob.glob('*.md') + glob.glob('*.txt')), ], @@ -105,33 +78,5 @@ setup( }, entry_points = {'console_scripts': console_scripts}, scripts = glob.glob("bin/*"), - description = "Pwntools CTF framework and exploit development library.", - long_description = long_description, - author = "Gallopsled et al.", - author_email = "pwntools-users@googlegroups.com", - url = 'https://pwntools.com', - download_url = "https://github.com/Gallopsled/pwntools/releases", - install_requires = install_requires, - license = "Mostly MIT, some GPL/BSD, see LICENSE-pwntools.txt", - keywords = 'pwntools exploit ctf capture the flag binary wargame overflow stack heap defcon', - classifiers = [ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'Intended Audience :: Science/Research', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: MIT License', - 'Natural Language :: English', - 'Operating System :: POSIX :: Linux', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Topic :: Security', - 'Topic :: Software Development :: Assemblers', - 'Topic :: Software Development :: Debuggers', - 'Topic :: Software Development :: Disassemblers', - 'Topic :: Software Development :: Embedded Systems', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: System :: System Shells', - 'Topic :: Utilities', - ] + **compat ) diff --git a/travis/setup_avd_fast.sh b/travis/setup_avd_fast.sh new file mode 100644 index 0000000..577ddc2 --- /dev/null +++ b/travis/setup_avd_fast.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +set -ex + +# Grab prerequisites +# Valid ABIs: +# - armeabi-v7a +# - arm64-v8a +# - x86 +# - x86_64 +ANDROID_ABI='armeabi-v7a' +ANDROIDV=android-24 + +# Create our emulator Android Virtual Device (AVD) +# --snapshot flag is deprecated, see bitrise-steplib/steps-create-android-emulator#18 +export PATH=$PATH:"$ANDROID_HOME"/cmdline-tools/latest/bin:"$ANDROID_HOME"/platform-tools +yes | sdkmanager --sdk_root="$ANDROID_HOME" --install "system-images;$ANDROIDV;default;$ANDROID_ABI" +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 & +adb wait-for-device +adb shell id +adb shell getprop |