diff --git a/CHANGELOG.md b/CHANGELOG.md index 777fcab5b..96e4c0c1d 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 | | ---------------- | -------- | ---------------------- | -| [3.11.0](#3110) | `dev` | Dec 2, 2017 (planned) -| [3.10.0](#3100) | `beta` | Oct 21, 2017 (planned) -| [3.9.2](#392) | `stable` | Oct 5, 2017 +| [3.12.0](#3120) | `dev` | Jan 13, 2018 (planned) +| [3.11.0](#3110) | `beta` | Dec 2, 2017 (planned) +| [3.10.0](#3100) | `stable` | Oct 25, 2017 +| [3.9.2](#392) | | Oct 5, 2017 | [3.9.1](#391) | | Sep 28, 2017 | [3.9.0](#390) | | Sep 11, 2017 | [3.8.0](#380) | | Jul 29, 2017 @@ -39,6 +40,10 @@ 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 +## 3.12.0 + +To be released on Jan 13, 2018. + ## 3.11.0 To be released on Dec 2, 2017. @@ -58,12 +63,12 @@ To be released on Dec 2, 2017. ## 3.10.0 -To be released on Oct 21, 2017. - +- [#1007][1007] Add support for setting a `gdbinit` file in the context - [#1055][1055] Fixes for `Corefile` stack parsing, speed up `ELF.string()` - [#1057][1057] Fix a variable name typo in `DynELF` logging which results in an exception being thrown - [#1058][1058] Fix an edge case in `ssh_process.exe` +[1007]: https://github.com/Gallopsled/pwntools/pull/1007 [1055]: https://github.com/Gallopsled/pwntools/pull/1055 [1057]: https://github.com/Gallopsled/pwntools/pull/1057 [1058]: https://github.com/Gallopsled/pwntools/pull/1058 diff --git a/README.md b/README.md index 4082e81f6..2486a971f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![pwntools logo](https://github.com/Gallopsled/pwntools/blob/stable/docs/source/logo.png?raw=true) [![Docs](https://readthedocs.org/projects/pwntools/badge/?version=stable)](https://docs.pwntools.com/) -[![PyPI](https://img.shields.io/badge/pypi-v3.9.2-green.svg?style=flat)](https://pypi.python.org/pypi/pwntools/) +[![PyPI](https://img.shields.io/badge/pypi-v3.10.0-green.svg?style=flat)](https://pypi.python.org/pypi/pwntools/) [![Travis](https://travis-ci.org/Gallopsled/pwntools.svg)](https://travis-ci.org/Gallopsled/pwntools) [![Coveralls](https://img.shields.io/coveralls/Gallopsled/pwntools/dev.svg)](https://coveralls.io/github/Gallopsled/pwntools?branch=dev) [![Twitter](https://img.shields.io/badge/twitter-pwntools-4099FF.svg?style=flat)](https://twitter.com/pwntools) diff --git a/pwnlib/dynelf.py b/pwnlib/dynelf.py index b3f502647..83714496e 100644 --- a/pwnlib/dynelf.py +++ b/pwnlib/dynelf.py @@ -447,7 +447,7 @@ def _find_linkmap(self, pltgot=None, debug=None): if debug: w.status("r_debug.linkmap") linkmap = self.leak.field(debug, r_debug.r_map) - w.status("r_debug.linkmap %#x" % result) + w.status("r_debug.linkmap %#x" % linkmap) if not linkmap: w.failure("Could not find DT_PLTGOT or DT_DEBUG") diff --git a/pwnlib/elf/corefile.py b/pwnlib/elf/corefile.py index 12b771e3d..96e8446db 100644 --- a/pwnlib/elf/corefile.py +++ b/pwnlib/elf/corefile.py @@ -389,7 +389,7 @@ class Corefile(ELF): >>> core.vdso.data[:4] '\x7fELF' - >>> core.libc # docteset: +ELLIPSIS + >>> core.libc # doctest: +ELLIPSIS Mapping('/lib/x86_64-linux-gnu/libc-...', ...) The corefile also contains a :attr:`.Corefile.stack` property, which gives @@ -421,7 +421,7 @@ class Corefile(ELF): >>> s = ssh('travis', 'example.pwnme') >>> _ = s.set_working_directory() - >>> elf = ELF.from_assembly('int3') + >>> elf = ELF.from_assembly(shellcraft.trap()) >>> path = s.upload(elf.path) >>> _ =s.chmod('+x', path) >>> io = s.process(path) @@ -438,6 +438,46 @@ class Corefile(ELF): >>> io.wait() >>> io.corefile.fault_addr 1234 + + Tests: + + These are extra tests not meant to serve as examples. + + Corefile.getenv() works correctly, even if the environment variable's + value contains embedded '='. Corefile is able to find the stack, even + if the stack pointer doesn't point at the stack. + + >>> elf = ELF.from_assembly(shellcraft.crash()) + >>> io = elf.process(env={'FOO': 'BAR=BAZ'}) + >>> io.wait() + >>> core = io.corefile + >>> core.getenv('FOO') + 'BAR=BAZ' + >>> core.sp == 0 + True + >>> core.sp in core.stack + False + + Corefile gracefully handles the stack being filled with garbage, including + argc / argv / envp being overwritten. + + >>> context.clear(arch='i386') + >>> assembly = ''' + ... LOOP: + ... mov dword ptr [esp], 0x41414141 + ... pop eax + ... jmp LOOP + ... ''' + >>> elf = ELF.from_assembly(assembly) + >>> io = elf.process() + >>> io.wait() + >>> core = io.corefile + >>> core.argc, core.argv, core.env + (0, [], {}) + >>> core.stack.data.endswith('AAAA') + True + >>> core.fault_addr == core.sp + True """ _fill_gaps = False @@ -463,6 +503,9 @@ def __init__(self, *a, **kw): #: variable. #: #: Note: Use with the :meth:`.ELF.string` method to extract them. + #: + #: Note: If FOO=BAR is in the environment, self.env['FOO'] is the + #: address of the string "BAR\x00". self.env = {} #: :class:`list`: List of addresses of arguments on the stack. @@ -673,7 +716,22 @@ def ppid(self): @property def signal(self): - """:class:`int`: Signal which caused the core to be dumped.""" + """:class:`int`: Signal which caused the core to be dumped. + + Example: + + >>> elf = ELF.from_assembly(shellcraft.trap()) + >>> io = elf.process() + >>> io.wait() + >>> io.corefile.signal == signal.SIGTRAP + True + + >>> elf = ELF.from_assembly(shellcraft.crash()) + >>> io = elf.process() + >>> io.wait() + >>> io.corefile.signal == signal.SIGSEGV + True + """ if self.siginfo: return int(self.siginfo.si_signo) if self.prstatus: @@ -684,9 +742,18 @@ def fault_addr(self): """:class:`int`: Address which generated the fault, for the signals SIGILL, SIGFPE, SIGSEGV, SIGBUS. This is only available in native core dumps created by the kernel. If the information is unavailable, - this returns the address of the instruction pointer.""" - fault_addr = 0 + this returns the address of the instruction pointer. + + Example: + + >>> elf = ELF.from_assembly('mov eax, 0xdeadbeef; jmp eax', arch='i386') + >>> io = elf.process() + >>> io.wait() + >>> io.corefile.fault_addr == io.corefile.eax == 0xdeadbeef + True + """ + fault_addr = 0 if self.siginfo: fault_addr = int(self.siginfo.sigfault_addr) @@ -878,11 +945,31 @@ def _parse_stack(self): # Now we can fill in the environment env_pointer_data = stack[start_of_envp:p_last_env_addr+self.bytes] for pointer in unpack_many(env_pointer_data): - end = stack.find('=', last_env_addr) - if pointer in stack and end in stack: - name = stack[pointer:end] - self.env[name] = pointer + # If the stack is corrupted, the pointer will be outside of + # the stack. + if pointer not in stack: + continue + + try: + name_value = self.string(pointer) + except Exception: + continue + + name, value = name_value.split('=', 1) + + # "end" points at the byte after the null terminator + end = pointer + len(name_value) + 1 + + # Do not mark things as environment variables if they point + # outside of the stack itself, or we had to cross into a different + # mapping (after the stack) to read it. + # This may occur when the entire stack is filled with non-NUL bytes, + # and we NULL-terminate on a read failure in .string(). + if end not in stack: + continue + + self.env[name] = pointer + len(name) + len('=') # May as well grab the arguments off the stack as well. # argc comes immediately before argv[0] on the stack, but @@ -934,15 +1021,32 @@ def getenv(self, name): Returns: :class:`str`: The contents of the environment variable. + + Example: + + >>> elf = ELF.from_assembly(shellcraft.trap()) + >>> io = elf.process(env={'GREETING': 'Hello!'}) + >>> io.wait() + >>> io.corefile.getenv('GREETING') + 'Hello!' """ if name not in self.env: log.error("Environment variable %r not set" % name) - return self.string(self.env[name]).split('=',1)[-1] + return self.string(self.env[name]) @property def registers(self): - """:class:`dict`: All available registers in the coredump.""" + """:class:`dict`: All available registers in the coredump. + + Example: + + >>> elf = ELF.from_assembly('mov eax, 0xdeadbeef;' + shellcraft.trap(), arch='i386') + >>> io = elf.process() + >>> io.wait() + >>> io.corefile.registers['eax'] == 0xdeadbeef + True + """ if not self.prstatus: return {} @@ -1050,6 +1154,7 @@ def __init__(self, proc): new_path = 'core.%i' % core_pid if core_pid > 0 and new_path != self.core_path: write(new_path, self.read(self.core_path)) + self.unlink(self.core_path) self.core_path = new_path # Check the PID diff --git a/pwnlib/elf/elf.py b/pwnlib/elf/elf.py index 8dab341e8..60fea2c9f 100644 --- a/pwnlib/elf/elf.py +++ b/pwnlib/elf/elf.py @@ -1692,16 +1692,33 @@ def unpack(self, address, *a, **kw): return packing.unpack(self.read(address, context.bytes), *a, **kw) def string(self, address): - """Reads a null-terminated string from the specified ``address``""" + """string(address) -> str + + Reads a null-terminated string from the specified ``address`` + + Returns: + A ``str`` with the string contents (NUL terminator is omitted), + or an empty string if no NUL terminator could be found. + """ data = '' while True: - c = self.read(address, 1) + read_size = 0x1000 + partial_page = address & 0xfff + + if partial_page: + read_size -= partial_page + + c = self.read(address, read_size) + if not c: return '' - if c == '\x00': - return data + data += c - address += 1 + + if '\x00' in c: + return data[:data.index('\x00')] + + address += len(c) def flat(self, address, *a, **kw): """Writes a full array of values to the specified address. diff --git a/pwnlib/tubes/ssh.py b/pwnlib/tubes/ssh.py index cd150f30d..75bc892a1 100644 --- a/pwnlib/tubes/ssh.py +++ b/pwnlib/tubes/ssh.py @@ -341,7 +341,8 @@ def elf(self): libs = self.parent.libs(self.executable) for lib in libs: - if self.executable in lib: + # Cannot just check "executable in lib", see issue #1047 + if lib.endswith(self.executable): return pwnlib.elf.elf.ELF(lib) diff --git a/pwnlib/version.py b/pwnlib/version.py index 83d5fae5f..130b10efa 100644 --- a/pwnlib/version.py +++ b/pwnlib/version.py @@ -1 +1 @@ -__version__ = '3.11.0dev' +__version__ = '3.11.0beta0' diff --git a/setup.py b/setup.py index d95b5a968..99a603bb8 100644 --- a/setup.py +++ b/setup.py @@ -86,7 +86,7 @@ setup( name = 'pwntools', packages = find_packages(), - version = '3.11.0dev', + version = '3.11.0beta0', data_files = [('', glob.glob('*.md') + glob.glob('*.txt')), ], diff --git a/travis/install.sh b/travis/install.sh index 812a86dab..adff191c4 100644 --- a/travis/install.sh +++ b/travis/install.sh @@ -90,7 +90,7 @@ setup_android_emulator() elif [[ -n "$TRAVIS_TAG" ]]; then echo "TRAVIS_TAG ($TRAVIS_TAG) indicates a new relase" echo "Forcing Android Emulator installation" - elif (git log --stat "$TRAVIS_COMMIT_RANGE" | grep -iE "android|adb"); then + elif (git log --stat "$TRAVIS_COMMIT_RANGE" | grep -iE "android|adb" | grep -v "commit "); then echo "Found Android-related commits, forcing Android Emulator installation" else # In order to avoid running the doctests that require the Android