From 63820d299f0ffbc55e7c6b9b5cbdeef888895660 Mon Sep 17 00:00:00 2001 From: Disconnect3d Date: Sun, 29 Jul 2018 21:21:52 +0200 Subject: [PATCH] Merge dev to beta (#506) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixes `u` command `module object is not callable` (#311) pwndbg> u 0x404030 'u': Starting at the specified address, disassemble N instructions (default 5). Traceback (most recent call last): File "/home/dc/installed/pwndbg/pwndbg/commands/__init__.py", line 99, in __call__ return self.function(*args, **kwargs) File "/home/dc/installed/pwndbg/pwndbg/commands/__init__.py", line 191, in _OnlyWhenRunning return function(*a, **kw) File "/home/dc/installed/pwndbg/pwndbg/commands/windbg.py", line 292, in u pwndbg.commands.nearpc(where, n) TypeError: 'module' object is not callable * refactored wrapper (#280) * added command got to display status of the got table Signed-off-by: degrigis * return when checksec is not available and added decorator OnlyWhenRunning Signed-off-by: degrigis * removed duplicated code for pie and not pie binaries Signed-off-by: degrigis * inserted support function to get checksec output and performed all requirements check initially Signed-off-by: degrigis * corrected typo Signed-off-by: degrigis * reorganized the command got splitting the code in library routines and moved the checksec internal function in a separate module Signed-off-by: degrigis * handled exception directly inside functions and enhanced code Signed-off-by: degrigis * extracted only column in readelf output and enhanced exception handling Signed-off-by: degrigis * fix exception handling returning subprocess error Signed-off-by: degrigis * removed unused import and reordered Signed-off-by: degrigis * reordered imports Signed-off-by: degrigis * added wrappers module and refactored some code Signed-off-by: degrigis * removed not useful comment Signed-off-by: degrigis * removed unused import Signed-off-by: degrigis * moved comments in docstring Signed-off-by: degrigis * refactored code to use partial functions, simplified code Signed-off-by: degrigis * simplified a loc Signed-off-by: degrigis * capslock char fixed Signed-off-by: degrigis * removed unuseful pwndbg.arch.ptrsize check Signed-off-by: degrigis * refactored code and added the new module wrapper that contains every new wrapper module Signed-off-by: degrigis * used class style decorator for wrapper and improved code style Signed-off-by: degrigis * changed return with print for errors Signed-off-by: degrigis * removed prints debug and statically linked check moved at the top of the got function Signed-off-by: degrigis * refactored OnlyWithCommand decorator Signed-off-by: degrigis * wrappers are OnlyWithFile now Signed-off-by: degrigis * redirected stderr to stdout in subprocess.check_output and memoized the wrappers for readelf/file/checksec Signed-off-by: degrigis * reordered an import Signed-off-by: degrigis * removed pdb Signed-off-by: degrigis * fixed format string and removed desc from got command Signed-off-by: degrigis * consolidated decorators Signed-off-by: degrigis * merging Signed-off-by: degrigis * reordered import for travis Signed-off-by: degrigis * refactored some code Signed-off-by: degrigis * resolve travis complains Signed-off-by: degrigis * docstring for _extract_jumps Signed-off-by: degrigis * fixed isort Signed-off-by: degrigis * f*** isort Signed-off-by: degrigis * Enhance canary command Canary command: * Displays telescope result of places where canaries are located * Moved to its own file (`pwndbg/commands/canary.py`) * Moved to `ArgparsedCommand` (as discussed in https://github.com/pwndbg/pwndbg/issues/244) * update for ida_script.py to handle ida 7.0 (#308) * fix for ida 7.0 * using idaapi.save_database instead, change version cmp from == to >= * Fix the current year (#319) This triggered me * checksec: cache output of command (#317) * checksec: cache output of command * checksec: use get_raw_out() for derived functions * cp fixes from stable: malloc chunk names, remote target search bug (#323) * Fix malloc chunk names (#318) * heap: respect rename of malloc_chunk fields newer glibc uses different names for the fields of malloc_chunk * move value_from_type to typeinfo and rename to read_gdbvalue * add comment about renaming of `[prev_]size` * Workaround for gdb remote target search bug described in #321 (#322) * Move vmmap to ArgparsedCommand; add sloppy_gdb_parse (#285) * Migrate vmmap command to ArgparsedCommand * vmmap command: better msg for no mappings * WIP: vmmap * Review fixes * isort fix * Add nextret command (#301) * Py version check: use pwndbg.compat.py* instead of sys.version (#327) * Dumpargs add --force to show all possible register arguments (#326) * Added --all flag to dumpargs command This gives possibility to dump all register argument even if we failed to resolve arguments from metadatas. * Display info when dumpargs not resolved call args * Dumpargs: changed --all to --force * Revert telescope changes as it fails when we are not on call instruction. * Fix isort * Fix IDA Pro decompiled code not being displayed (#328) * Fix withHexrays decorator not returning wrapper function * IDA xmlrpc: add cfuncptr_t marshaller & better errors * IDA xmlrpc server: add shutdown() which can be used for dev * Small refactor of context.py * Fix context Hexrays decompiled code display * Fix hard error when something else (not IDA) listens on IDA's port (#330) * Fix hard error when something else (not IDA) listens on IDA's port The default IDA port is 8888 and it can happen that some other program (such as a jupyter notebook) is listening on that address. This made pwndbg unusable, because it would crash trying to connect to IDA. * add timeout to ida connect * Fix exception if there is an indirect jump (#329) This is a simple typo, but the error message that GDB gave was interesting: Previously, if you stopped on an instruction that does an indirect jump, like this: ``` jmp [ecx*4 + 0xdeadbeef] ``` then pwndbg would the following exception: ``` gdb.error: evaluation of this expression requires the program to have a function "malloc". ``` The reason is that the code used `memory_sz` and passed that to gdb.Value, thus creating a string value. When casting the string to a pointer later, GDB tries to allocate a string in the inferior which failed since malloc is not available. The fix is, of course, to use the correct function (`memory`) that returns an int and not a string. * Fixes issue when we try to display context while selected thread is running #299 (#331) (#333) * ArgparsedCommands: config, configfile, theme, themefile (#335) * Fix Python<=2.7.6 "TypeError: Struct() argument 1 must be string, not unicode" (#336) * Fix Python<=2.7.6 "TypeError: Struct() argument 1 must be string, not unicode" Additional information is available here: http://python-future.org/stdlib_incompatibilities.html#struct-pack * Completely remove libheap, as it is not ever referenced * Expose IDA Pro commands, even when IDA is not available. (#337) Closes #225 * Removed duplicate requirement (#339) 2 Lines stated "capstone" * Closes #338: Fix 'This command is not documented' (#341) * Reduce the number of times we check to see if running Android * Do not populate the main exe symbols on Android, it's unnecessary * Add nextproginstr command (#360) * Add nextproginstr command * Fix isort * Update next.py * Update next.py * Update next.py * Add possibility to prevent skipping repeating telescope values (#359) * Merge stable to dev (#365) * Fixes `u` command `module object is not callable` (#310) pwndbg> u 0x404030 'u': Starting at the specified address, disassemble N instructions (default 5). Traceback (most recent call last): File "/home/dc/installed/pwndbg/pwndbg/commands/__init__.py", line 99, in __call__ return self.function(*args, **kwargs) File "/home/dc/installed/pwndbg/pwndbg/commands/__init__.py", line 191, in _OnlyWhenRunning return function(*a, **kw) File "/home/dc/installed/pwndbg/pwndbg/commands/windbg.py", line 292, in u pwndbg.commands.nearpc(where, n) TypeError: 'module' object is not callable * Fix malloc chunk names (#318) * heap: respect rename of malloc_chunk fields newer glibc uses different names for the fields of malloc_chunk * move value_from_type to typeinfo and rename to read_gdbvalue * add comment about renaming of `[prev_]size` * Workaround for gdb remote target search bug described in #321 (#322) * Fixes issue when we try to display context while selected thread is running #299 (#331) * Fix tag_release (#348) * Fix "dt" offsets which are sometimes floating-point (#355) * Fixes #362 - broken entry command (#363) * fix ds and da for gdb 7.11 (#364) * fix ds and da for gdb 7.11 * add max argument to da and ds * Support bare metal environment (#369) * Add elf.find_elf_magic() and remove duplicate code * Add pwndbg.abi.LinuxOnly decorator * Support bare metal environment Use @pwndbg.abi.LinuxOnly and pwndbg.abi.linux to disable several util functions which search the memory to find the AUXV, the ELF header, or the page bound. * Add xinfo command for extended offset information (#376) This commit adds a `xinfo` command that calculates the offset of a specified address to other interesting locations within the address space: * In the most general case, simply the offset of the pointer into the current mapping is displayed. * If the address specified is a stack adress, the offsets to the top and the bottom of the stack, as well as to the current stack pointer, frame pointer and stack canary are displayed. * If the address points into a memory mapped file, the command additionally shows the offset to the beginning of the file in memory and on disk. * Fail on two commands with the same name (#372) * More badges in README Add "Python 2&3" and "freenode: #pwndbg" badges created with https://shields.io/ * Fix Python 2&3 badge in README * Update README badges links * Add dereference-limit and heap-dereference-limit parameters (#367) * Add dereference-limit and heap-dereference-limit parameters This allows setting the number of pointers dereferenced during 'telescope' and in the register context. Separately, the number of heap bins which are dereferenced can be set. * Cast LIMIT to an integer, and address off-by-one * Adds $rebase(offset) function (#374) Adds `$rebase(offset)` gdbfunction that can be used to set up a breakpoint over an offset from program image base. Also changed a bit the pwndbg banner displayed at startup. * ArgparsedCommand: pass parser or description; move some cmds to ArgparsedC~ (#373) * Fix upper stack boundary (#377) * Fix upper_stack_boundary not working introduced in 31f468e The `upper_stack_boundary` we returned wasn't matching the one from `vmmap`. Previously we determined upper address by having a memory read failure. Recent changes made it so we got a `None` instead of the address in such situation. This adds a parameter to `find_elf_magic` which lets us get a result when gdb.MemoryError occurs. * Small refactor: add missing newlines * Add capstone, unicorn versions to version command (#379) * Merge stable to dev (#381) * Fixes `u` command `module object is not callable` (#310) pwndbg> u 0x404030 'u': Starting at the specified address, disassemble N instructions (default 5). Traceback (most recent call last): File "/home/dc/installed/pwndbg/pwndbg/commands/__init__.py", line 99, in __call__ return self.function(*args, **kwargs) File "/home/dc/installed/pwndbg/pwndbg/commands/__init__.py", line 191, in _OnlyWhenRunning return function(*a, **kw) File "/home/dc/installed/pwndbg/pwndbg/commands/windbg.py", line 292, in u pwndbg.commands.nearpc(where, n) TypeError: 'module' object is not callable * Fix malloc chunk names (#318) * heap: respect rename of malloc_chunk fields newer glibc uses different names for the fields of malloc_chunk * move value_from_type to typeinfo and rename to read_gdbvalue * add comment about renaming of `[prev_]size` * Workaround for gdb remote target search bug described in #321 (#322) * Fixes issue when we try to display context while selected thread is running #299 (#331) * Fix tag_release (#348) * Fix "dt" offsets which are sometimes floating-point (#355) * Fixes #362 - broken entry command (#363) * Fix #373 for python2 env (#384) Since the python2 use `from __future__ import unicode_literals`, so the string literals will be `unicode` type in python2. Use `six.string_types` in `isinstance()` instead of using `str` type. * Fix Endianess issue and Memory error on GDB (#386) * Fix py2 import error (shlex.quotes vs pipes.quotes) (#389) * Avoid enhancements when dereference limit is zero (#380) * Avoid enhancements when dereference limit is zero * Replace last element in chain with enhancements * make everything themeable (#392) * theme: make everything themable by avoiding explicite colors This makes it posssible to theme everything logically grouped by message types. This will also make it easier for future features to keep a consistent way of coloring plus make every non-specific coloring themeable automatically. Direct explicit usage of colors should be avoided in future commits. * theme: make banner fully customizable including positions * fixup: fix wrong import during refactoring (#394) * Fix inthook for enums in Python 3 (#393) Fixes the problem that can be observed below: ``` pwndbg> py import re; flags = 1 | re.MULTILINE Traceback (most recent call last): File "", line 1, in File "/usr/lib/python3.6/enum.py", line 798, in __or__ result = self.__class__(self._value_ | self.__class__(other)._value_) File "/usr/lib/python3.6/enum.py", line 291, in __call__ return cls.__new__(cls, value) File "/usr/lib/python3.6/enum.py", line 533, in __new__ return cls._missing_(value) File "/usr/lib/python3.6/enum.py", line 762, in _missing_ new_member = cls._create_pseudo_member_(value) File "/usr/lib/python3.6/enum.py", line 788, in _create_pseudo_member_ pseudo_member._name_ = None AttributeError: 'int' object has no attribute '_name_' ``` * Implement asm&source syntax highlight (#390) * Syntax-highlight: Add asm lexer in color/lexer.py * Syntax-highlight: Add pygments to requirements.txt * Syntax-highlight: Update lexer for supporting ARM Support symbol, constant, comments * Syntax-highlight: Enable asm syntax highlight * Syntax-highlight: Add source highlight utils in commnads/context.py * Syntax-highlight: Add disasm highlight utils in color/disasm.py * Syntax-highlight: Implement Source code highlighting in commands/context.py * Syntax-highlight: Add syntax_highlight() in color/syntax_highlight.py * Fix texts * Add color theme and prefix config for context code * Add missed utf8 magic comment * Fix isort * context: bring back args section (default off) (#397) This allows to use args section via the context-sections config setting (default off). Additionally introduce a nearpc-show-args config value making it possible to disable showing it trice while using the args section. * config: validate context-sections and show all available values (#396) When setting an illegal value, fall back to the default sections. * Make de-reference only works on known pages in bare metal mode and add commands to manually add pages (#385) * Make chain.get() to check vmmap first in bare metal mode Make chain.get() limit to de-reference within the known page in bare metal mode. Since the address are all valid when mmu is not enable and all the value are valid physical address. It will be de-referenced even these addresses are not used and actually, it is data in the most of case. Ex. 0x1 often means the value 1, not the address 0x1. Also, for issue #371, some addresses may be the MMIO registers. The read operation on these address will break the state. It is better to limit the de-reference address range. This patch will also fix it, hopefully. * Add custom vmmap add/del API in vmmap.py In some cases, ex. bare metal, the pages information can not be detected automatically. Also, the most of pwndbg feature rely on page information such as highlighting. User may want to create page information manually and maintain it by himself. This commit add python APIs to manually add/del page information and they are isolated. * Fix stack page detection in bare metal mode We can not detect the stack page size in bare metal mode by 1. finding the ELF location after the stack page 2. page fault A simple workaround is returning the current $sp page and assume it is the stack page. * Add vmmap control command to add/del customized vmmap In some cases, ex. bare metal, the pages information can not be detected automatically. Also, the most of pwndbg feature rely on page information such as highlighting. User may want to create page information manually and maintain it by himself. I add few commands to make user can add/del pages and load page information from ELF sections. * Fix the command amount for auto test to pass CI * Add warning message * Fix descriptions * Fix cache issue and use bisect in insert API * Keep LinuxOnly in find_elf_magic * remove XXX * improve repeat functionality of commands (#395) * hexdump: adjust shown offset from src while repeating * nearpc: make command repeatable to show further instructions The pc gets adjusted to the last instructions address making it visually easy to follow where to continue reading the assembly. This also forwards repeating of emulate() and pdisass() * telescope: make command repeatable with adjusted offset from src This also forwards stack() to be repeatable. * [WIP] Stop highlight and prefix display when repeating nearpc command (#399) Fix nearpc command repeat: highlight, prefix and instruction display * color: make lrjust() work with multiple chars (#401) This fixes the issue if ljust with multiple characters like the banner separator char * syntax: highlight code for chain format during enhance (#400) * Fix missing enum in Python 2 (#403) * Add developer notes (#405) * Update DEVELOPING.md * Better detection of extended-remote types * Bring back possibility of empty context (#409) * Change README about GEF / GDBINIT / PWNDBG (#413) * Ban isort==4.3.0 (#417) See timothycrosley/isort#652 for more information. tl;dr is `pip install isort==4.3.0` fails, which is what `pip install -Ur requirements.txt` will attempt to do. Ban this specific version as it causes issues. * display the frame pointer register (x29) in aarch64 context (#418) * ensure length padding works with py2 by enforcing unicode awareness (#416) This works around the issue of python2 not being unicode aware and the config classes not properly returning instance of decoded raw strings. This leads to length operations being performed on bytes rather then logical characters. We check for python2 and enfore decoding if not a text_type. Fixes #412 * Profiling and performance improvements (#421) * Add scripts for benchmarking and profiling pwndbg commands * Fix performance issue in emulator.py Register to unicorn enum lookup was really ineffective. Replaced with parsing (consts) on initialization time, and only dict lookup on hot path. * Fix performance issue in syntax_highlight. Current code initialized pygments on each syntax_highlight(), which apparently took some time. * Minor performance improvements in syntax_highlight * Memoize IDA availability. Not sure it this is a valid solution, I have never used pwndbg with IDA. However, we should not try to connect to ida on each context(), as this takes 25% of current exec time. * Explicitly source gdbinit in benchmark scripts. * Refactor variable names in nearpc (#422) * Try to connect to IDA on every debugger stop. (#423) Add option to disable IDA integration completly. * Fix Parameter config class (#404) * Avoid to use 'type' as varialbe name * Fix utf8 issue of Parameter.value in python2 * Fix Parameter member funcs * Operator overwrite of Parameter * Remove all workaround of Parameter * Use regex to unwrap the string * Remove impossible cases in commands/context.py after Parameter class update * Revert "ensure length padding works with py2 by enforcing unicode awareness (#416)" This reverts commit 8ecaa670436841a68893051b5635c161c77f2200. * Fix #429 - osabi check for non-English GDB version (#430) Detailed info is within the issue, but TLDR: ``` (gdb) show osabi El actual SO ABI es «auto» (actualmente «GNU/Linux»). El SO ABI predeterminado es «GNU/Linux». ``` * add basic rust support (#431) When a rust binary is loaded gdb will not find the usual c types. * Fixes #428 - pwndbg.memory.write encoding error (#432) It seems that pwndbg.memory.write fix for Py2 introduced in 433bf231 wasn't tested properly on Py3. In Py2 by default the `bytes` is just `str` and so doesn't accept the encoding argument. Because of that a `from builtins import bytes` has been added. Some more info on `builtins` module can be found here: http://python-future.org/imports.html#imports-of-builtins * Fixes #427 - readelf parsing error on old readelf versions (#433) Here is `readelf --program-headers ` output for different readelf versions (The `//` are commented lines; the output is truncated so it contains only useful data): ``` Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align // GNU readelf (GNU Binutils for Debian) 2.25 (2014): LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x000000000001bad4 0x000000000001bad4 R E 200000 // GNU readelf (GNU Binutils) 2.29.1 (2017): LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x000000000001e050 0x000000000001e050 R E 0x200000 ``` Our parser parsed the line after the one containing `LOAD` and expected that `Align` column value will be always prefixed with `0x`. As we can see this is not always the case... * fix find_fake_chunk #410 (#435) * Fixes #436 - memory write regression (#437) * Fix regression made in #432 *This situation pushes me more and more to work on tests engine* * Fix eX memory write on Python 2 As string literal is unicode, in Py2 the code below would fail if `bytestr` is just a `str`, due to `'0'` being unocide literal: ``` bytestr.rjust(size*2, '0') ``` * Tests framework (#375) * Add prototype of unit tests for pwndbg * Add test for pwndbg [filter] * Fix isort, e2e tests, add pytest requirement * Add comment about not handling exceptions for unittests * Fixes after rebase * Fix test_loads_without_crashing * e2e tests: no colors & loading pwndbg tests * Fix isort * Add example of no file loaded test * Move tests to unit_tests, add binary, add memory tests * Isort fixes * Move from e2e/unit tests to tests * Add info about tests to DEVELOPING.md * Fix tests * review fixes * commands filtering test: check for contents, not for equality * Add tests launcher bash script * Change tests launcher name from unittests to pytests * Cleanup; better test file paths * Add theme param to disable colors * Better test_loads * Skip some tests locally that can run on travis * Fix test_loads according to travis * Fix travis tests * Don't check for IDA Pro if it is dissabled (#439) * Improve behavior without IDA Pro (#442) * Improve behavior without IDA Pro * Fix import order * Improved IDA Pro behaviour more * Added only_after_first_prompt decorator * Removed newline after import * Added documentation * Improved docstring * Implement support for ptmalloc's tcache in heap/ (#420) * Implement support for ptmalloc's tcache in heap/ (#387) Glibc 2.26 added per-thread cache of free chunks. This implements new "tcache" and "tcachebins" commands for displaying information about this cache. Note this works well only if pthread is linked in the debugged program. Otherwise gdb cannot access thread-local variables, so it cannot find address of tcache main struct. One can though find the address, ex. by stepping through malloc code, and pass it to the new commands. * Another round of review fixes. * handle gracefully older libc, without tcache * use aligned size for consistency with other bins * Support hex data prefixed with 0x when using eX windbg command (#444) When using `eX` commands and setting data to hex value prefixed with `0x`, we get an exception: ``` pwndbg> ed 0xffb21ae4 0x55616740 Traceback (most recent call last): File "/usr/lib/python3.6/encodings/hex_codec.py", line 19, in hex_decode return (binascii.a2b_hex(input), len(input)) binascii.Error: Non-hexadecimal digit found The above exception was the direct cause of the following exception: Traceback (most recent call last): File "/home/dc/installed/pwndbg/pwndbg/commands/__init__.py", line 109, in __call__ return self.function(*args, **kwargs) File "/home/dc/installed/pwndbg/pwndbg/commands/__init__.py", line 200, in _OnlyWhenRunning return function(*a, **kw) File "/home/dc/installed/pwndbg/pwndbg/commands/windbg.py", line 141, in ed return eX(4, address, data) File "/home/dc/installed/pwndbg/pwndbg/commands/windbg.py", line 180, in eX data = codecs.decode(bytestr, 'hex') binascii.Error: decoding with 'hex' codec failed (Error: Non-hexadecimal digit found) ``` This commit fixes this problem so that if the data input has prefix, it is stripped. * Adds stepret command and nextret docstring (#448) So that now we can step-until-return-like-instruction like a boss! :) * Add stepsyscall and rename next_syscall to nextsyscall (#447) * Add stepsyscall (and stepsc) command So that one can break at a syscall which is e.g. inside a call. * Rename next_syscall into nextsyscall * Display context on next/stepsyscall only if process is alive * Fix bins command 'There is no member named tcache_bins' (#449) Bins command fails on a libc that doesn't use tcache at all, e.g.: ``` GNU C Library (Ubuntu GLIBC 2.23-0ubuntu10) stable release version 2.23, by Roland McGrath et al. ``` Here is the output: ``` pwndbg> bins Traceback (most recent call last): File "/root/pwndbg/pwndbg/commands/__init__.py", line 109, in __call__ return self.function(*args, **kwargs) File "/root/pwndbg/pwndbg/commands/__init__.py", line 200, in _OnlyWhenRunning return function(*a, **kw) File "/root/pwndbg/pwndbg/commands/heap.py", line 255, in bins if pwndbg.heap.current.has_tcache(): File "/root/pwndbg/pwndbg/heap/ptmalloc.py", line 47, in has_tcache return (self.mp and self.mp['tcache_bins']) gdb.error: There is no member named tcache_bins. ``` This commit fixes this issue by checking whether `tcache_bins` field is present in the `malloc_par` structure. * Fixes bins command (#424) (#450) The problem was that after some of the recent changes to chain/get to prevent dereferencing too much addresses and having better display when dereferencing limit is 0 (used for bare metal debugging) the bins command displayed wrong results for everything except fastbins. This was due to the fact we are adding the dereference start address to the list. This fixes the `bins` command by adding `include_start=True` keyword argument to the `chain.get` function. The `bins` simply uses `include_start=False`. * Fixes #391 - kills compat.py module (#452) * Kill compat.py completely (#453) * Fix IDA 7 unhandled DecompilationFailure (#455) * version command: show IDA Pro versions (#456) * version with IDA: proper hexrays detection (#457) * Fix emulate command crash (#459) After we added `repeat` functionality for some commands, the emulate stopped to work: ``` pwndbg> emulate Traceback (most recent call last): File "/home/dc/installed/pwndbg/pwndbg/commands/__init__.py", line 109, in __call__ return self.function(*args, **kwargs) File "/home/dc/installed/pwndbg/pwndbg/commands/__init__.py", line 200, in _OnlyWhenRunning return function(*a, **kw) File "/home/dc/installed/pwndbg/pwndbg/commands/nearpc.py", line 180, in emulate nearpc.repeat = emulate.repeat AttributeError: 'bool' object has no attribute 'repeat' ``` This is due to the fact the command has the same name as argument which is a bool. * Add filtering for config and theme commands (#458) * Add filtering for config and theme commands * Fix isort * Change IDA xmlrpc default port (#462) * Fixes #460 - getting SP reg on threaded apps (#463) * Fixes #460 - getting SP reg on threaded apps As the issue described: as we cache registers, we might get their values wrong as we don't invalidate cache when thread is changed. This leads to showing wrong context stack values in threaded apps. This commit/PR adds a new memoization solution: `reset_on_prompt` which resets cache on `gdb.events.before_prompt` event. * Fix isort * Fix before_prompt event on old GDB versions (#464) * Fix before_prompt event on old GDB versions This adds an `EventWrapper` class which behaves similar to gdb events but lets us: * check whether event is a real gdb event or not * call event callbacks if it is not a real gdb event * Better comment * Fix pwndbg.disasm.near with disabled caching (#465) Before this changes `context_disasm` produced different display based on memoization settings. The bug can be seen below: ``` [dc@dc:pwndbg|dev $%]$ gdb ~/test/a.out pwndbg: loaded 166 commands. Type pwndbg [filter] for a list. pwndbg: created $rebase, $ida gdb functions (can be used with print/break) Reading symbols from /home/dc/test/a.out...(no debugging symbols found)...done. pwndbg> set context-sections disasm Set which context sections are displayed (controls order) to 'disasm' pwndbg> entry Temporary breakpoint 1 at 0x400080 Temporary breakpoint 1, 0x0000000000400080 in _start () LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ───────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────── ► 0x400080 <_start> jmp _start <0x400080> ↓ ► 0x400080 <_start> jmp _start <0x400080> Breakpoint *0x400080 pwndbg> python import pwndbg; pwndbg.memoize.memoize.caching=False LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ───────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────── ► 0x400080 <_start> jmp _start <0x400080> ↓ ► 0x400080 <_start> jmp _start <0x400080> ↓ ► 0x400080 <_start> jmp _start <0x400080> ↓ ► 0x400080 <_start> jmp _start <0x400080> ↓ ► 0x400080 <_start> jmp _start <0x400080> ↓ ► 0x400080 <_start> jmp _start <0x400080> ↓ ► 0x400080 <_start> jmp _start <0x400080> ↓ ► 0x400080 <_start> jmp _start <0x400080> ↓ ► 0x400080 <_start> jmp _start <0x400080> ↓ ► 0x400080 <_start> jmp _start <0x400080> ↓ ► 0x400080 <_start> jmp _start <0x400080> Breakpoint *0x400080 pwndbg> ``` The tested binary can be reproduced with this assembly code: ```asm global _start _start: jmp $ ``` Compiled as `nasm -f elf64 code.asm && ld code.o`. --- About the bug: The check for multiple identical loops or rets is done using `set(insns[-3:])`. Before this hapens the `insns` is filled with the results of `one(address)`. This calls `get_one_instruction(address)` which is cached until `reset_on_cont`. As a result, when caching is enabled the `get_one_instruction` returns the same `capstone.CsInsn` instances for given address. When it is disabled, we return other instances which are identical. The problem was that `set(insns[-3:])` creates a set based on `capstone.CsInsn` instances and not on the instruction addresses. The fix changes this behavior so that we compare last 3 instruction addresses. * Fix shell commands descriptions (#466) Before that change all shell commands had a description of `None`. This was because we used string formatting for a docstring and if one does so, the string isn't a docstring anymore. This also fixes `r2` command description. * Use gdb.VERSION instead of parsing gdb.execute result (#467) Yay, this is finally there: ``` >>> gdb.VERSION '8.1.0.20180409-git' ``` * Fix NameError: global name 'abi' is not defined (#469) * typeinfo: skip failed compile attempts (#470) * typeinfo: skip failed compile attempts This fixes the problem that single header files that are not compilable do not abort the compilation loop. Errors about the failure are printed anyway be check_output we just avoid abortion. * Remove shell=True in subprocess.check_output * Improve dumpargs command edge cases (#471) * Adds `-f` alias for `--force` option * More informative docstrings * Fix the up and down commands when provided with an argument. (#473) * Command for calculating PIE offsets (#474) * PIE command * Kill compat in piebase command * Improve piebase command * Improve piebase command exe name introspection * No longer rely on executeable segment for piebase * Fix isort (#475) * Fixes wrong pc/ip display in context introduced in 9fd5d35 (#477) Before this PR we could get wrong RIP (like off by one) when single stepping through the code: ``` [...] RIP 0x555555559850 ◂— xor ebp, ebp ───────────────────────[ DISASM ]────────────────────── 0x555555559850 xor ebp, ebp ► 0x555555559852 mov r9, rdx <0x7ffff7de59a0> [...] pwndbg> i r rip rip 0x555555559852 0x555555559852 ``` The patch fixes the issue by reassigning GDB stop signal handler to getting register values. * Fixes 476 - segfault handling when using rr project (#478) * Fixes #476 - segfault handling when using rr project * Fix isort * bug fix: tcache bin (#482) * Fix and enhance xinfo command (#480) * Instead of unstable parsing of readelf output, use the elftools ELF wrapper for parsing PT_LOAD segments * Fix #434 xinfo command doesn't show File(Disk) info on non-PIE binaries Also remove some trailing whitespace Also fix another bug in xinfo; now it can show the disk offset of all mmap files, not just the primary executable * New xinfo feature: Print containing ELF sections for file-backed addresses * Only print header for ELF sections if at least 1 section contains the address * Fix bug in section offset calculation when printing containing ELF sections * Refactor ELF file parsing helpers for cleaner separtion of ELF metadata parsing and enrichment, and a specific use scenario (getting a list of segments/sections containing a given virtual addr). Also makes implementing caching parse results easier Adjust xinfo command to these API changes * Fix bug: Reference mem_end instead of file_end * Don't use underscore variable names; change decorator to reset_on_objfile * Update xinfo.py * ptmalloc multiple heaps per non-main arena support, related fixes (#479) * Multiple ptmalloc enhancements: * Adds support for multiple heaps per arena for the `arenas` command. * Names every heap objfile to enable proper coloring in vmmap - fixes 451. * Refactors the `heap` command to address issue 443. * Adds comment for HEAP_MAX_SIZE * Refactors Arena and HeapInfo into classes * Adds additional comment * Objfile event dispatching fix (#486) * Fixes objfile caching bug. * Disables vmmap exploration when the target isn't alive. * Resets the objfile cache to the proper type on exit. (#487) * isort: fix import order to make travis pass (#490) * Heap: allocator initialization check & global_max_fast bug fix (#485) * Bug fix: global_max_fast symbol contains the actual value not the address * heap: return from find_fake_fast if allocator is not initialized * Bug fix: address method should return the symbol address if it's an intergral symbols * Revert commit c35152df1c2c601b67b0157e3dfe51809a8b9249 * add OnlyWhenHeapIsInitialized decorator * Update heap.py * Refactors heap.get_region, adds special case for get_heap_boundaries. (#489) Occasionally, the [heap] vm region and the actual start of the heap are different, e.g. [heap] starts at 0x61f000 but mp_.sbrk_base is 0x620000. Return an adjusted Page object if this is the case. Also changes the callers of these functions where appropriate. * Leak offset probing tool (#492) * PIE command * Kill compat in piebase command * Improve piebase command * Improve piebase command exe name introspection * No longer rely on executeable segment for piebase * Leak probing tool * Fix description for probeleak * Update probeleak.py Changed `%x` to `0x%x` in edge case scenario print/reporting. * Reorder imports * Improve probeleak printing * Fix isort (#493) * Fixes #488: wrong regs display on threaded targets (#495) Please see https://github.com/pwndbg/pwndbg/issues/488#issuecomment-403213457 for explanation. * add vis_heap_chunks (#496) * add vis_heap_chunks * Add top_chunk suffix only when needed * use ArgparsedCommand and pwndbg.arch.unpack + better formatting * Minor improvements, fix isort * Run each test in a separate GDB session (#498) * it would be cool to have tests that run within GDB so that we don't have to parse GDB output and deal with weird problems * we can't run all tests in one GDB session as `file x; entry; ; file y; entry; ;` may have different results - it seems either us or GDB fails to cleanup everything properly * Fix nearpc following jumps when used w/o emulation (#499) * Tests launcher: show passed and failed count * Build nearpc, emulate, u, pdisass test binaries * Add tests for emulate, nearpc, pdisass, u * Refactored disasm and emulator * Fix nearpc following jumps w/o emulation * Prevent tests from calling start_binary twice * Add test for emulate_disasm_loop * Fix isort * Add nasm to travis install * Add --eval-command quit to tests invocation This should prevent travis from staying in gdb/stalled build when something fails in weird way (like a file is missing) ``` [+] Building 'emulate_disasm.o' make: nasm: Command not found make: *** [emulate_disasm.o] Error 127 gdbinit.py: No such file or directory. pytests_collect.py: No such file or directory. No output has been received in the last 10m0s, this potentially indicates a stalled build or something wrong with the build itself. Check the details on how to adjust your build configuration on: https://docs.travis-ci.com/user/common-build-problems/#Build-times-out-because-no-output-was-received ``` * Add test binaries * Inform about `exception-debugger` on exceptions (#501) Instead of hiding this feature just for devs who reads our dev guide or just knows that this exists lets make pwndbg development great again and show this command to the world! * Fixes piebase and breakrva on remote debugging (#500) Three things here: 1. This fixes `piebase` and `breakrva` commands - a bug with remote targets mentioned in https://github.com/pwndbg/pwndbg/issues/488#issuecomment-403213457. 2. It also adds a check if result address is still in the memory pages belonging to the given module. This works now as: ``` pwndbg> breakrva main Offset 0x555555554601 rebased to module /home/dc/pwndbg_bug/a.out as 0xaaaaaaaa8601 is beyond module's memory pages: 0x555555554000 0x555555555000 r-xp 1000 0 /home/dc/pwndbg_bug/a.out 0x555555754000 0x555555755000 r--p 1000 0 /home/dc/pwndbg_bug/a.out 0x555555755000 0x555555756000 rw-p 1000 1000 /home/dc/pwndbg_bug/a.out ``` 3. It gives a better output for `piebase`: ``` pwndbg> piebase 1 Calculated VA from /home/dc/pwndbg_bug/a.out = 0x555555554001 ``` --- To reproduce the fixed bug, launch any binary on a gdbserver: ``` gdbserver 127.0.0.1:4444 ./a.out ``` Then start a debugging session: ``` gdb -q -ex 'target remote 127.0.0.1:4444' ./a.out ``` and fire e.g. `breakrva 123`. --- Below you can see the bug case and explanation why it occured: ``` pwndbg> breakrva 1 There are no mappings for specified address or module. 'breakrva': Break at RVA from PIE base. Traceback (most recent call last): File "/home/dc/pwndbg/pwndbg/commands/__init__.py", line 109, in __call__ return self.function(*args, **kwargs) File "/home/dc/pwndbg/pwndbg/commands/__init__.py", line 200, in _OnlyWhenRunning return function(*a, **kw) File "/home/dc/pwndbg/pwndbg/commands/pie.py", line 61, in breakrva spec = "*%#x" % (addr) TypeError: %x format: an integer is required, not NoneType ``` So what is the issue here? 1. We have the same logic in both `piebase` and `breakrva` - if the user doesn't specify second argument - a module name - we retrieve it with `get_exe_name`: ```python def breakrva(offset=None, module=None): offset = int(offset) if not module: module = get_exe_name() addr = translate_addr(offset, module) spec = "*%#x" % (addr) # [ ... - some more code, not important here ] ``` 2. The `get_exe_name` returns just `pwndbg.auxv.get().get('AT_EXECFN', pwndbg.proc.exe)`. The difference is important here. On the case shown above the `pwndbg.auxv.get()['AT_EXECFN']` returns `./a.out` while `pwndbg.proc.exe` returns the full path: `/home/dc/pwndbg_bug/a.out`. 3. This `module` is then passed to `translate_addr` as can be seen on the code above. 4. The `translate_addr` tries to retrieve memory page (`Page` instance) which belongs to the module: ```python def translate_addr(offset, module): mod_filter = lambda page: module in page.objfile pages = list(filter(mod_filter, pwndbg.vmmap.get())) if not pages: print('There are no mappings for specified address or module.') return # [ ... - some more code, not important here ] ``` 5. The `translate_addr` returns `None` because the `page.objfile` for e.g. binary objfile returns its full path as can be seen below: ``` (Pdb) pwndbg.vmmap.get()[0].objfile '/home/dc/pwndbg_bug/a.out' ``` 6. Because we returned `None`, the `spec = "*%#x" % (addr)` string formatting for breakrva or `print(hex(addr))` for piebase fails. * Fixes piebase and breakrva on remote debugging (#502) Fixes the issue caught by ecx86 in: https://github.com/pwndbg/pwndbg/pull/500#issuecomment-404332482 The commands broke when we debugged a remote target which was hosted on a remote gdbserver (NOT a local one). This is because we used `pwndbg.proc.exe` (changed in previous commit) which is a local path to the binary which was then used to filter out memory pages belonging to the binary. To fix the issue, the AUXV's AT_EXECFN is used first which was used before previous commit but the returned path is now normalized (as in previous version it didn't work because if it returned path './a.out' it couldn't match it with binary's Page.objfile which was e.g. '/blabla/a.out'). * Bump version (#505) --- .github/CONTRIBUTING.md | 2 +- .gitignore | 6 +- .travis.yml | 10 +- DEVELOPING.md | 39 + README.md | 11 +- docs/requirements.txt | 1 - docs/source/api/compat.rst | 5 - ida_script.py | 110 +- profiling/.gitignore | 3 + profiling/benchmark.sh | 13 + profiling/profile.sh | 18 + profiling/test.c | 3 + pwndbg/__init__.py | 25 +- pwndbg/abi.py | 61 + pwndbg/android.py | 7 +- pwndbg/arch.py | 6 +- pwndbg/arguments.py | 44 +- pwndbg/argv.py | 2 + pwndbg/auxv.py | 3 + pwndbg/chain.py | 49 +- pwndbg/color/__init__.py | 21 +- pwndbg/color/context.py | 12 + pwndbg/color/disasm.py | 15 +- pwndbg/color/lexer.py | 135 ++ pwndbg/color/message.py | 73 + pwndbg/color/syntax_highlight.py | 72 + pwndbg/commands/__init__.py | 68 +- pwndbg/commands/aslr.py | 6 +- pwndbg/commands/auxv.py | 8 +- pwndbg/commands/canary.py | 53 + pwndbg/commands/checksec.py | 13 +- pwndbg/commands/config.py | 57 +- pwndbg/commands/context.py | 199 +- pwndbg/commands/cpsr.py | 36 +- pwndbg/commands/defcon.py | 13 +- pwndbg/commands/dumpargs.py | 55 +- pwndbg/commands/elf.py | 23 +- pwndbg/commands/got.py | 60 +- pwndbg/commands/heap.py | 270 ++- pwndbg/commands/hexdump.py | 8 +- pwndbg/commands/ida.py | 142 +- pwndbg/commands/misc.py | 47 +- pwndbg/commands/nearpc.py | 74 +- pwndbg/commands/next.py | 68 +- pwndbg/commands/pie.py | 106 + pwndbg/commands/probeleak.py | 84 + pwndbg/commands/radare2.py | 4 +- pwndbg/commands/search.py | 8 +- pwndbg/commands/shell.py | 4 +- pwndbg/commands/stack.py | 8 +- pwndbg/commands/start.py | 11 +- pwndbg/commands/telescope.py | 39 +- pwndbg/commands/theme.py | 28 +- pwndbg/commands/version.py | 45 +- pwndbg/commands/vmmap.py | 137 +- pwndbg/commands/windbg.py | 42 +- pwndbg/commands/xinfo.py | 126 ++ pwndbg/compat.py | 22 - pwndbg/config.py | 119 +- pwndbg/decorators.py | 27 + pwndbg/disasm/__init__.py | 61 +- pwndbg/disasm/arch.py | 7 +- pwndbg/dt.py | 7 +- pwndbg/elf.py | 174 +- pwndbg/emu/emulator.py | 82 +- pwndbg/enhance.py | 4 + pwndbg/events.py | 98 +- pwndbg/exception.py | 22 +- pwndbg/gdbutils/__init__.py | 10 + pwndbg/gdbutils/functions.py | 54 + pwndbg/gitver.py | 28 - pwndbg/heap/__init__.py | 2 + pwndbg/heap/heap.py | 9 + pwndbg/heap/libheap.py | 1878 ----------------- pwndbg/heap/ptmalloc.py | 292 ++- pwndbg/hexdump.py | 11 +- pwndbg/ida.py | 80 +- pwndbg/inthook.py | 12 +- pwndbg/memoize.py | 13 + pwndbg/memory.py | 18 +- pwndbg/next.py | 41 + pwndbg/proc.py | 22 +- pwndbg/prompt.py | 43 +- pwndbg/regs.py | 4 +- pwndbg/remote.py | 14 +- pwndbg/stack.py | 22 +- pwndbg/stdio.py | 2 - pwndbg/symbol.py | 5 +- pwndbg/typeinfo.py | 22 +- pwndbg/ui.py | 32 +- pwndbg/version.py | 2 +- pwndbg/vmmap.py | 33 +- pwndbg/wrappers.py | 33 - pwndbg/wrappers/__init__.py | 35 + pwndbg/wrappers/checksec.py | 42 + pwndbg/wrappers/readelf.py | 36 + pytests_collect.py | 36 + pytests_launcher.py | 31 + requirements.txt | 5 +- tag_release.sh | 2 +- tests.sh | 33 + tests/__init__.py | 8 +- tests/binaries/__init__.py | 14 + tests/binaries/emulate_disasm.asm | 14 + tests/binaries/emulate_disasm.out | Bin 0 -> 736 bytes tests/binaries/emulate_disasm_loop.asm | 17 + tests/binaries/emulate_disasm_loop.out | Bin 0 -> 768 bytes tests/binaries/makefile | 50 + tests/binaries/old_bash/__init__.py | 11 + .../bash => binaries/old_bash}/binary | Bin .../bash => binaries/old_bash}/core | Bin tests/binaries/reference-binary.c | 8 + tests/binaries/reference-binary.out | Bin 0 -> 10840 bytes tests/common.py | 31 - tests/conftest.py | 30 + tests/testLoadsWithoutCrashing.py | 11 - tests/test_emulate.py | 95 + tests/test_loads.py | 129 ++ tests/test_memory.py | 33 + tests/test_misc.py | 33 + 120 files changed, 3830 insertions(+), 2767 deletions(-) create mode 100644 DEVELOPING.md delete mode 100644 docs/source/api/compat.rst create mode 100644 profiling/.gitignore create mode 100755 profiling/benchmark.sh create mode 100755 profiling/profile.sh create mode 100644 profiling/test.c create mode 100644 pwndbg/color/lexer.py create mode 100644 pwndbg/color/message.py create mode 100644 pwndbg/color/syntax_highlight.py create mode 100644 pwndbg/commands/canary.py create mode 100644 pwndbg/commands/pie.py create mode 100644 pwndbg/commands/probeleak.py create mode 100644 pwndbg/commands/xinfo.py delete mode 100644 pwndbg/compat.py create mode 100644 pwndbg/decorators.py create mode 100644 pwndbg/gdbutils/__init__.py create mode 100644 pwndbg/gdbutils/functions.py delete mode 100644 pwndbg/gitver.py delete mode 100644 pwndbg/heap/libheap.py delete mode 100644 pwndbg/wrappers.py create mode 100644 pwndbg/wrappers/__init__.py create mode 100644 pwndbg/wrappers/checksec.py create mode 100644 pwndbg/wrappers/readelf.py create mode 100644 pytests_collect.py create mode 100644 pytests_launcher.py create mode 100755 tests.sh create mode 100644 tests/binaries/__init__.py create mode 100644 tests/binaries/emulate_disasm.asm create mode 100755 tests/binaries/emulate_disasm.out create mode 100644 tests/binaries/emulate_disasm_loop.asm create mode 100755 tests/binaries/emulate_disasm_loop.out create mode 100644 tests/binaries/makefile create mode 100644 tests/binaries/old_bash/__init__.py rename tests/{corefiles/bash => binaries/old_bash}/binary (100%) rename tests/{corefiles/bash => binaries/old_bash}/core (100%) create mode 100644 tests/binaries/reference-binary.c create mode 100755 tests/binaries/reference-binary.out delete mode 100644 tests/common.py create mode 100644 tests/conftest.py delete mode 100644 tests/testLoadsWithoutCrashing.py create mode 100644 tests/test_emulate.py create mode 100644 tests/test_loads.py create mode 100644 tests/test_memory.py create mode 100644 tests/test_misc.py diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d6771698b8..3a96102d8b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,6 +1,6 @@ ### Contributing -Contributions to Pwndbg are always welcome! +Contributions to Pwndbg are always welcome! If you want to get more familiar with project idea/structure/whatever - [here are some developer notes](./DEVELOPING.md). If something is not clear, feel free to ask in a github issue! If you want to help, fork the project, hack your changes and create a pull request. diff --git a/.gitignore b/.gitignore index 4248ccbe61..b61b993c65 100644 --- a/.gitignore +++ b/.gitignore @@ -60,4 +60,8 @@ npm-debug.log .gdb_history # PyCharm project files -.idea/ \ No newline at end of file +.idea/ + +# PyTest files +.pytest_cache/ +tests/.pytest_cache/ diff --git a/.travis.yml b/.travis.yml index 99facd8aa3..7cb1554f26 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,14 +9,14 @@ cache: - capstone - unicorn install: - - sudo apt-get -y install gdb + - sudo apt-get -y install gdb nasm - lsb_release -a - pip install -r requirements.txt - sudo ./setup.sh script: - - futurize --all-imports --stage1 --print-function --write --unicode-literals pwndbg - - git diff-index --quiet HEAD -- pwndbg - - isort --check-only --diff --recursive pwndbg - - nosetests ./tests/ + - futurize --all-imports --stage1 --print-function --write --unicode-literals pwndbg tests + - git diff-index --quiet HEAD -- pwndbg tests + - isort --check-only --diff --recursive pwndbg tests + - PWNDBG_TRAVIS_TEST_RUN=1 ./tests.sh - python2.7 -m py_compile ida_script.py $(git ls-files 'pwndbg/*.py') - python3 -m py_compile $(git ls-files 'pwndbg/*.py') diff --git a/DEVELOPING.md b/DEVELOPING.md new file mode 100644 index 0000000000..a7923b703c --- /dev/null +++ b/DEVELOPING.md @@ -0,0 +1,39 @@ +# Random developer notes + +Feel free to update the list below! + +* If you want to play with pwndbg functions under GDB, you can always use GDB's `pi` which launches python interpreter or just `py `. + +* If there is possibility, don't use `gdb.execute` as this requires us to parse the string and so on; there are some cases in which there is no other choice. Most of the time we try to wrap GDB's API to our own/easier API. + +* We have our own `pwndbg.config.Parameter` (which extends `gdb.Parameter`) - all of our parameters can be seen using `config` or `theme` commands. If we want to do something when user changes config/theme - we can do it defining a function and decorating it with `pwndbg.config.Trigger`. + +* The dashboard/display/context we are displaying is done by `pwndbg/commands/context.py` which is invoked through GDB's prompt hook (which we defined in `pwndbg/prompt.py` as `prompt_hook_on_stop`). + +* All commands should be defined in `pwndbg/commands` - most of them lie in seperate files but some files contains many of them (e.g. commands corresponding to windbg debugger - in `windbg.py` or some misc commands in `misc.py`). We would also want to make all of them to use `ArgparsedCommand` (instead of `Command` or `ParsedCommand` decorators). + +* We change a bit GDB settings - this can be seen in `pwndbg/__init__.py` - there are also imports for all pwndbg submodules + +* We have a wrapper for GDB's events in `pwndbg/events.py` - thx to that we can e.g. invoke something based upon some event + +* We have a caching mechanism (["memoization"](https://en.wikipedia.org/wiki/Memoization)) which we use through Python's decorators - those are defined in `pwndbg/memoize.py` - just check its usages + +* To block a function before the first prompt was displayed use the `pwndbg.decorators.only_after_first_prompt` decorator. + +* Memory accesses should be done through `pwndbg/memory.py` functions + +* Process properties can be retrieved thx to `pwndbg/proc.py` - e.g. using `pwndbg.proc.pid` will give us current process pid + +* We have an inthook to make it easier to work with Python 2 and gdb.Value objects - see the docstring in `pwndbg/inthook.py` . Specifically, it makes it so that you can call `int()` on a `gdb.Value` instance and get what you want. + +* We have a wrapper for handling exceptions that are thrown by commands - defined in `pwndbg/exception.py` - current approach seems to work fine - by using `set exception-verbose on` - we get a stacktrace. If we want to debug stuff we can always do `set exception-debugger on`. + +* Some of pwndbg's functionality - e.g. memory fetching - require us to have an instance of proper `gdb.Type` - the problem with that is that there is no way to define our own types - we have to ask gdb if it detected particular type in this particular binary (that sucks). We do it in `pwndbg/typeinfo.py` and it works most of the time. The known bug with that is that it might not work properly for Golang binaries compiled with debugging symbols. + +* We would like to add proper tests for pwndbg - see tests framework PR if you want to help on that. + +# Testing + +Our tests are written using [pytest](https://docs.pytest.org/en/latest/). It uses some magic so that Python's `assert` can be used for asserting things in tests and it injects dependencies which are called fixtures, into test functions. + +The fixtures should be defined in [tests/conftest.py](tests/conftest.py). If you need help with writing tests, feel free to reach out on gitub issues/pr or on our irc channel on freenode. diff --git a/README.md b/README.md index a194be908e..6a3bfd8f24 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# pwndbg [![Build Status](https://travis-ci.org/pwndbg/pwndbg.svg?branch=master)](https://travis-ci.org/pwndbg/pwndbg) [![license](https://img.shields.io/github/license/mashape/apistatus.svg?maxAge=2592000)]() +# pwndbg [![Build Status](https://travis-ci.org/pwndbg/pwndbg.svg?branch=dev)](https://travis-ci.org/pwndbg/pwndbg) [![license](https://img.shields.io/github/license/mashape/apistatus.svg?maxAge=2592000)](https://github.com/pwndbg/pwndbg/blob/dev/LICENSE.md) [![Py2&3](https://img.shields.io/badge/Python-2%20%26%203-green.svg)]() [![IRC](https://img.shields.io/badge/freenode-%23pwndbg-red.svg)](https://webchat.freenode.net/?channels=#pwndbg) `pwndbg` (/poʊndbæg/) is a GDB plug-in that makes debugging with GDB suck less, with a focus on features needed by low-level software developers, hardware hackers, reverse-engineers and exploit developers. @@ -6,19 +6,24 @@ It has a boatload of features, see [FEATURES.md](FEATURES.md). ## Why? -Vanilla GDB is terrible to use for reverse engineering and exploit development. Typing `x/g30x $esp` is not fun, and does not confer much information. The year is 2016 and GDB still lacks a hexdump command. GDB's syntax is arcane and difficult to approach. Windbg users are completely lost when they occasionally need to bump into GDB. +Vanilla GDB is terrible to use for reverse engineering and exploit development. Typing `x/g30x $esp` is not fun, and does not confer much information. The year is 2017 and GDB still lacks a hexdump command. GDB's syntax is arcane and difficult to approach. Windbg users are completely lost when they occasionally need to bump into GDB. ## What? Pwndbg is a Python module which is loaded directly into GDB, and provides a suite of utilities and crutches to hack around all of the cruft that is GDB and smooth out the rough edges. -Many other projects from the past (e.g., [gdbinit][gdbinit], [PEDA][PEDA]) and present (e.g. [GEF][GEF]) exist to fill some these gaps. Unfortunately, they're all either unmaintained, unmaintainable, or not well suited to easily navigating the code to hack in new features (respectively). +Many other projects from the past (e.g., [gdbinit][gdbinit], [PEDA][PEDA]) and present (e.g. [GEF][GEF]) exist to fill some these gaps. Each provides an excellent experience and great features -- but they're difficult to extend (some are unmaintained, and all are a single [100KB][gdbinit2], [200KB][peda.py], or [300KB][gef.py] file (respectively)). Pwndbg exists not only to replace all of its predecessors, but also to have a clean implementation that runs quickly and is resilient against all the weird corner cases that come up. [gdbinit]: https://github.com/gdbinit/Gdbinit +[gdbinit2]: https://github.com/gdbinit/Gdbinit/blob/master/gdbinit + [PEDA]: https://github.com/longld/peda +[peda.py]: https://github.com/longld/peda/blob/master/peda.py + [GEF]: https://github.com/hugsy/gef +[gef.py]: https://github.com/hugsy/gef/blob/master/gef.py ## How? diff --git a/docs/requirements.txt b/docs/requirements.txt index df0255c1c4..b6a3f0b851 100755 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -20,4 +20,3 @@ python-ptrace>=0.8 six future unicorn>=1.0.0 -capstone diff --git a/docs/source/api/compat.rst b/docs/source/api/compat.rst deleted file mode 100644 index 8b390f1a5c..0000000000 --- a/docs/source/api/compat.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`pwndbg.compat` --- pwndbg.compat -============================================= - -.. automodule:: pwndbg.compat - :members: diff --git a/ida_script.py b/ida_script.py index 07de595252..fe7d3738e4 100644 --- a/ida_script.py +++ b/ida_script.py @@ -6,6 +6,7 @@ import threading import xmlrpclib from SimpleXMLRPCServer import SimpleXMLRPCServer +from xml.sax.saxutils import escape import idaapi import idautils @@ -20,13 +21,36 @@ dt = datetime.datetime.now().isoformat().replace(':', '-') # Save the database so nothing gets lost. -idc.SaveBase(idc.GetIdbPath() + '.' + dt) +if idaapi.IDA_SDK_VERSION >= 700: + idaapi.save_database(idc.GetIdbPath() + '.' + dt) +else: + idc.SaveBase(idc.GetIdbPath() + '.' + dt) -xmlrpclib.Marshaller.dispatch[type(0L)] = lambda _, v, w: w("%d" % v) -xmlrpclib.Marshaller.dispatch[type(0)] = lambda _, v, w: w("%d" % v) + +DEBUG_MARSHALLING = False + +def create_marshaller(use_format=None, just_to_str=False): + assert use_format or just_to_str, 'Either pass format to use or make it converting the value to str.' + + def wrapper(_marshaller, value, appender): + if use_format: + marshalled = use_format % value + elif just_to_str: + marshalled = '%s' % escape(str(value)) + + if DEBUG_MARSHALLING: + print("Marshalled: '%s'" % marshalled) + + appender(marshalled) + + return wrapper + +xmlrpclib.Marshaller.dispatch[type(0L)] = create_marshaller("%d") +xmlrpclib.Marshaller.dispatch[type(0)] = create_marshaller("%d") +xmlrpclib.Marshaller.dispatch[idaapi.cfuncptr_t] = create_marshaller(just_to_str=True) host = '127.0.0.1' -port = 8888 +port = 31337 orig_LineA = idc.LineA @@ -44,23 +68,30 @@ def LineA(*a, **kw): def wrap(f): def wrapper(*a, **kw): - try: - rv = [] - - def work(): - rv.append(f(*a, **kw)) - - with mutex: - flags = idaapi.MFF_WRITE - if f == idc.SetColor: - flags |= idaapi.MFF_NOWAIT - rv.append(None) - idaapi.execute_sync(work, flags) - return rv[0] - except: - import traceback - traceback.print_exc() - raise + rv = [] + error = [] + + def work(): + try: + result = f(*a, **kw) + rv.append(result) + except Exception as e: + error.append(e) + + with mutex: + flags = idaapi.MFF_WRITE + if f == idc.SetColor: + flags |= idaapi.MFF_NOWAIT + rv.append(None) + idaapi.execute_sync(work, flags) + + if error: + msg = 'Failed on calling {}.{} with args: {}, kwargs: {}\nException: {}' \ + .format(f.__module__, f.__name__, a, kw, str(error[0])) + print('[!!!] ERROR:', msg) + raise error[0] + + return rv[0] return wrapper @@ -71,15 +102,50 @@ def register_module(module): server.register_function(wrap(function), name) +def decompile(addr): + """ + Function that overwrites `idaapi.decompile` for xmlrpc so that instead + of throwing an exception on `idaapi.DecompilationFailure` it just returns `None`. + (so that we don't have to parse xmlrpc Fault's exception string on pwndbg side + as it differs between IDA versions). + """ + try: + return idaapi.decompile(addr) + except idaapi.DecompilationFailure: + return None + + +def versions(): + """Returns IDA & Python versions""" + import sys + return { + 'python': sys.version, + 'ida': idaapi.get_kernel_version(), + 'hexrays': idaapi.get_hexrays_version() if idaapi.init_hexrays_plugin() else None + } + + server = SimpleXMLRPCServer((host, port), logRequests=True, allow_none=True) register_module(idc) register_module(idautils) register_module(idaapi) server.register_function(lambda a: eval(a, globals(), locals()), 'eval') +server.register_function(decompile) # overwrites idaapi/ida_hexrays.decompie +server.register_function(versions) server.register_introspection_functions() -print('Ida Pro xmlrpc hosted on http://%s:%s' % (host, port)) +print('IDA Pro xmlrpc hosted on http://%s:%s' % (host, port)) +print('Call `shutdown()` to shutdown the IDA Pro xmlrpc server.') thread = threading.Thread(target=server.serve_forever) thread.daemon = True thread.start() + + +def shutdown(): + global server + global thread + server.shutdown() + server.server_close() + del server + del thread diff --git a/profiling/.gitignore b/profiling/.gitignore new file mode 100644 index 0000000000..84bc10df24 --- /dev/null +++ b/profiling/.gitignore @@ -0,0 +1,3 @@ +test +stats +stats.log diff --git a/profiling/benchmark.sh b/profiling/benchmark.sh new file mode 100755 index 0000000000..61301b6605 --- /dev/null +++ b/profiling/benchmark.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# Benchmark context command +make test > /dev/null +git log --abbrev-commit --pretty=oneline HEAD^..HEAD +gdb ./test \ + -ex "source ../gdbinit.py" \ + -ex "b main" -ex "r" \ + -ex "python import timeit; print(' 1ST RUN:', timeit.repeat('pwndbg.commands.context.context()', repeat=1, number=1, globals=globals())[0])" \ + -ex "si" \ + -ex "python import timeit; print(' 2ND RUN:', timeit.repeat('pwndbg.commands.context.context()', repeat=1, number=1, globals=globals())[0])" \ + -ex "si" \ + -ex "python import timeit; print('MULTIPLE RUNS:', timeit.repeat('pwndbg.commands.context.context()', repeat=1, number=10, globals=globals())[0] / 10)" \ + -ex "quit" | grep 'RUNS*:' diff --git a/profiling/profile.sh b/profiling/profile.sh new file mode 100755 index 0000000000..80fb7a1bc4 --- /dev/null +++ b/profiling/profile.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Quick and dirty script to profile pwndbg using cProfile. +make test > /dev/null +git log --abbrev-commit --pretty=oneline HEAD^..HEAD +# To profile first run, remove -ex "context". +gdb ./test \ + -ex "source ../gdbinit.py" \ + -ex "b main" -ex "r" \ + -ex "context" \ + -ex "python import cProfile; cProfile.run('pwndbg.commands.context.context()', 'stats')" \ + -ex "quit" + +python3 -c " +import pstats +p = pstats.Stats('stats') +p.strip_dirs().sort_stats('tottime').print_stats(20) +" +[ -x /usr/local/bin/pyprof2calltree ] && command -v kcachegrind >/dev/null 2>&1 && /usr/local/bin/pyprof2calltree -k -i stats diff --git a/profiling/test.c b/profiling/test.c new file mode 100644 index 0000000000..3d5939e31b --- /dev/null +++ b/profiling/test.c @@ -0,0 +1,3 @@ +int main() { + while(1); +} diff --git a/pwndbg/__init__.py b/pwndbg/__init__.py index dabd18be44..11f5a9eed0 100755 --- a/pwndbg/__init__.py +++ b/pwndbg/__init__.py @@ -18,6 +18,7 @@ import pwndbg.commands.argv import pwndbg.commands.aslr import pwndbg.commands.auxv +import pwndbg.commands.canary import pwndbg.commands.checksec import pwndbg.commands.config import pwndbg.commands.context @@ -33,6 +34,8 @@ import pwndbg.commands.misc import pwndbg.commands.next import pwndbg.commands.peda +import pwndbg.commands.pie +import pwndbg.commands.probeleak import pwndbg.commands.procinfo import pwndbg.commands.radare2 import pwndbg.commands.reload @@ -48,6 +51,7 @@ import pwndbg.commands.version import pwndbg.commands.vmmap import pwndbg.commands.windbg +import pwndbg.commands.xinfo import pwndbg.commands.xor import pwndbg.constants import pwndbg.disasm @@ -60,6 +64,7 @@ import pwndbg.dt import pwndbg.elf import pwndbg.exception +import pwndbg.gdbutils.functions import pwndbg.heap import pwndbg.inthook import pwndbg.memory @@ -73,6 +78,8 @@ import pwndbg.version import pwndbg.vmmap import pwndbg.wrappers +import pwndbg.wrappers.checksec +import pwndbg.wrappers.readelf __version__ = pwndbg.version.__version__ version = __version__ @@ -88,7 +95,6 @@ 'auxv', 'chain', 'color', -'compat', 'disasm', 'dt', 'elf', @@ -116,16 +122,11 @@ 'vmmap' ] -prompt = "pwndbg> " -prompt = "\x02" + prompt + "\x01" # STX + prompt + SOH -prompt = pwndbg.color.red(prompt) -prompt = pwndbg.color.bold(prompt) -prompt = "\x01" + prompt + "\x02" # SOH + prompt + STX +pwndbg.prompt.set_prompt() pre_commands = """ set confirm off set verbose off -set prompt %s set pagination off set height 0 set history expansion on @@ -140,7 +141,7 @@ handle SIGBUS stop print nopass handle SIGPIPE nostop print nopass handle SIGSEGV stop print nopass -""".strip() % (prompt, pwndbg.ui.get_window_size()[1]) +""".strip() % (pwndbg.ui.get_window_size()[1]) for line in pre_commands.strip().splitlines(): gdb.execute(line) @@ -151,6 +152,12 @@ except gdb.error: pass - # handle resize event to align width and completion signal.signal(signal.SIGWINCH, lambda signum, frame: gdb.execute("set width %i" % pwndbg.ui.get_window_size()[1])) + +# Workaround for gdb bug described in #321 ( https://github.com/pwndbg/pwndbg/issues/321 ) +# More info: https://sourceware.org/bugzilla/show_bug.cgi?id=21946 +# As stated on GDB's bugzilla that makes remote target search slower. +# After GDB gets the fix, we should disable this only for bugged GDB versions. +if 1: + gdb.execute('set remote search-memory-packet off') diff --git a/pwndbg/abi.py b/pwndbg/abi.py index 7da6846c2c..af822478d4 100644 --- a/pwndbg/abi.py +++ b/pwndbg/abi.py @@ -4,7 +4,13 @@ from __future__ import print_function from __future__ import unicode_literals +import functools +import re + +import gdb + import pwndbg.arch +import pwndbg.color.message as M class ABI(object): @@ -111,3 +117,58 @@ class SigreturnABI(SyscallABI): linux_i386_srop = ABI(['eax'], 4, 0) linux_amd64_srop = ABI(['rax'], 4, 0) linux_arm_srop = ABI(['r7'], 4, 0) + + +@pwndbg.events.start +def update(): + global abi + global linux + + # Detect current ABI of client side by 'show osabi' + # + # Examples of strings returned by `show osabi`: + # 'The current OS ABI is "auto" (currently "GNU/Linux").\nThe default OS ABI is "GNU/Linux".\n' + # 'The current OS ABI is "GNU/Linux".\nThe default OS ABI is "GNU/Linux".\n' + # 'El actual SO ABI es «auto» (actualmente «GNU/Linux»).\nEl SO ABI predeterminado es «GNU/Linux».\n' + # 'The current OS ABI is "auto" (currently "none")' + # + # As you can see, there might be GDBs with different language versions + # and so we have to support it there too. + # Lets assume and hope that `current osabi` is returned in first line in all languages... + abi = gdb.execute('show osabi', to_string=True).split('\n')[0] + + # Currently we support those osabis: + # 'GNU/Linux': linux + # 'none': bare metal + + linux = 'GNU/Linux' in abi + + if not linux: + msg = M.warn( + "The bare metal debugging is enabled since the gdb's osabi is '%s' which is not 'GNU/Linux'.\n" + "Ex. the page resolving and memory de-referencing ONLY works on known pages.\n" + "This option is based ib gdb client compile arguments (by default) and will be corrected if you load an ELF which has the '.note.ABI-tag' section.\n" + "If you are debuging a program that runs on Linux ABI, please select the correct gdb client." + % abi + ) + print(msg) + + +def LinuxOnly(default=None): + """Create a decorator that the function will be called when ABI is Linux. + Otherwise, return `default`. + """ + def decorator(func): + @functools.wraps(func) + def caller(*args, **kwargs): + if linux: + return func(*args, **kwargs) + else: + return default + return caller + + return decorator + + +# Update when starting the gdb to show warning message for non-Linux ABI user. +update() diff --git a/pwndbg/android.py b/pwndbg/android.py index f8299732f9..36f4ede46e 100644 --- a/pwndbg/android.py +++ b/pwndbg/android.py @@ -7,12 +7,15 @@ import gdb -import pwndbg.color +import pwndbg.color.message as message import pwndbg.events import pwndbg.file +import pwndbg.memoize import pwndbg.remote +@pwndbg.memoize.reset_on_start +@pwndbg.memoize.reset_on_exit def is_android(): try: if pwndbg.file.get('/system/etc/hosts'): @@ -29,7 +32,7 @@ def sysroot(): if gdb.parameter('sysroot') == 'target:': gdb.execute(cmd) else: - print(pwndbg.color.bold("sysroot is already set, skipping %r" % cmd)) + print(message.notice("sysroot is already set, skipping %r" % cmd)) KNOWN_AIDS = { 0: "AID_ROOT", diff --git a/pwndbg/arch.py b/pwndbg/arch.py index 7b3d84ea9c..15c6bc168e 100644 --- a/pwndbg/arch.py +++ b/pwndbg/arch.py @@ -48,7 +48,7 @@ def update(): m.ptrsize = pwndbg.typeinfo.ptrsize m.ptrmask = (1 << 8*pwndbg.typeinfo.ptrsize)-1 - if 'little' in gdb.execute('show endian', to_string=True): + if 'little' in gdb.execute('show endian', to_string=True).lower(): m.endian = 'little' else: m.endian = 'big' @@ -60,6 +60,10 @@ def update(): (8, 'big'): '>Q', }.get((m.ptrsize, m.endian)) + # Work around Python 2.7.6 struct.pack / unicode incompatibility + # See https://github.com/pwndbg/pwndbg/pull/336 for more information. + m.fmt = str(m.fmt) + # Attempt to detect the qemu-user binary name if m.current == 'arm' and m.endian == 'big': m.qemu = 'armeb' diff --git a/pwndbg/arguments.py b/pwndbg/arguments.py index dbe3bee954..a3a3603a05 100644 --- a/pwndbg/arguments.py +++ b/pwndbg/arguments.py @@ -15,6 +15,8 @@ import pwndbg.abi import pwndbg.arch +import pwndbg.chain +import pwndbg.color.nearpc as N import pwndbg.constants import pwndbg.disasm import pwndbg.funcparser @@ -52,8 +54,9 @@ '__userpurge': '', } + def get_syscall_name(instruction): - if not CS_GRP_INT in instruction.groups: + if CS_GRP_INT not in instruction.groups: return None try: @@ -65,6 +68,7 @@ def get_syscall_name(instruction): except: return None + def get(instruction): """ Returns an array containing the arguments to the current function, @@ -99,9 +103,6 @@ def get(instruction): # Get the syscall number and name abi = pwndbg.abi.ABI.syscall() - # print(abi) - # print(abi.register_arguments) - target = None syscall = getattr(pwndbg.regs, abi.syscall_register) name = pwndbg.constants.syscall(syscall) @@ -109,7 +110,6 @@ def get(instruction): return [] result = [] - args = [] name = name or '' sym = gdb.lookup_symbol(name) @@ -128,7 +128,6 @@ def get(instruction): except TypeError: pass - # Try to grab the data out of IDA if not func and target: typename = pwndbg.ida.GetType(target) @@ -139,17 +138,17 @@ def get(instruction): # GetType() does not include the name. typename = typename.replace('(', ' function_name(', 1) - for k,v in ida_replacements.items(): - typename = typename.replace(k,v) + for k, v in ida_replacements.items(): + typename = typename.replace(k, v) - func = pwndbg.funcparser.ExtractFuncDeclFromSource(typename + ';') + func = pwndbg.funcparser.ExtractFuncDeclFromSource(typename + ';') if func: args = func.args else: - args = [pwndbg.functions.Argument('int',0,argname(i, abi)) for i in range(n_args_default)] + args = [pwndbg.functions.Argument('int', 0, argname(i, abi)) for i in range(n_args_default)] - for i,arg in enumerate(args): + for i, arg in enumerate(args): result.append((arg, argument(i, abi))) return result @@ -164,10 +163,12 @@ def argname(n, abi=None): return 'arg[%i]' % n + def argument(n, abi=None): """ Returns the nth argument, as if $pc were a 'call' or 'bl' type instruction. + Works only for ABIs that use registers for arguments. """ abi = abi or pwndbg.abi.ABI.default() regs = abi.register_arguments @@ -180,3 +181,24 @@ def argument(n, abi=None): sp = pwndbg.regs.sp + (n * pwndbg.arch.ptrsize) return int(pwndbg.memory.poi(pwndbg.typeinfo.ppvoid, sp)) + + +def arguments(abi=None): + """ + Yields (arg_name, arg_value) tuples for arguments from a given ABI. + Works only for ABIs that use registers for arguments. + """ + abi = abi or pwndbg.abi.ABI.default() + regs = abi.register_arguments + + for i in range(len(regs)): + yield argname(i, abi), argument(i, abi) + + +def format_args(instruction): + result = [] + for arg, value in get(instruction): + code = arg.type != 'char' + pretty = pwndbg.chain.format(value, code=code) + result.append('%-10s %s' % (N.argument(arg.name) + ':', pretty)) + return result diff --git a/pwndbg/argv.py b/pwndbg/argv.py index 9ddfa30d64..12565a275a 100644 --- a/pwndbg/argv.py +++ b/pwndbg/argv.py @@ -7,6 +7,7 @@ import gdb +import pwndbg.abi import pwndbg.arch import pwndbg.events import pwndbg.memory @@ -25,6 +26,7 @@ envc = None @pwndbg.events.start +@pwndbg.abi.LinuxOnly() def update(): global argc global argv diff --git a/pwndbg/auxv.py b/pwndbg/auxv.py index f2a628e768..a36279a269 100644 --- a/pwndbg/auxv.py +++ b/pwndbg/auxv.py @@ -11,6 +11,7 @@ import gdb +import pwndbg.abi import pwndbg.arch import pwndbg.events import pwndbg.info @@ -150,6 +151,8 @@ def find_stack_boundary(addr): return addr def walk_stack(): + if not pwndbg.abi.linux: + return None if pwndbg.qemu.is_qemu_kernel(): return None diff --git a/pwndbg/chain.py b/pwndbg/chain.py index 8c0b85c0b6..4227323000 100755 --- a/pwndbg/chain.py +++ b/pwndbg/chain.py @@ -7,6 +7,7 @@ import gdb +import pwndbg.abi import pwndbg.color.chain as C import pwndbg.color.memory as M import pwndbg.color.theme as theme @@ -16,11 +17,11 @@ import pwndbg.typeinfo import pwndbg.vmmap -LIMIT = 5 +LIMIT = pwndbg.config.Parameter('dereference-limit', 5, 'max number of pointers to dereference in a chain') -def get(address, limit=LIMIT, offset=0, hard_stop=None, hard_end=0): +def get(address, limit=LIMIT, offset=0, hard_stop=None, hard_end=0, include_start=True): """ - Recursively dereferences an address. + Recursively dereferences an address. For bare metal, it will stop when the address is not in any of vmmap pages to avoid redundant dereference. Arguments: address(int): the first address to begin dereferencing @@ -28,12 +29,14 @@ def get(address, limit=LIMIT, offset=0, hard_stop=None, hard_end=0): offset(int): offset into the address to get the next pointer hard_stop(int): address to stop at hard_end: value to append when hard_stop is reached + include_start(bool): whether to include starting address or not Returns: A list representing pointers of each ```address``` and reference """ + limit = int(limit) - result = [] + result = [address] if include_start else [] for i in range(limit): # Don't follow cycles, except to stop at the second occurrence. if result.count(address) >= 2: @@ -43,10 +46,17 @@ def get(address, limit=LIMIT, offset=0, hard_stop=None, hard_end=0): result.append(hard_end) break - result.append(address) try: - address = int(pwndbg.memory.poi(pwndbg.typeinfo.ppvoid, address + offset)) + address = address + offset + + # Avoid redundant dereferences in bare metal mode by checking + # if address is in any of vmmap pages + if not pwndbg.abi.linux and not pwndbg.vmmap.find(address): + break + + address = int(pwndbg.memory.poi(pwndbg.typeinfo.ppvoid, address)) address &= pwndbg.arch.ptrmask + result.append(address) except gdb.MemoryError: break @@ -74,9 +84,10 @@ def format(value, limit=LIMIT, code=True, offset=0, hard_stop=None, hard_end=0): A string representing pointers of each address and reference Strings format: 0x0804a10 —▸ 0x08061000 ◂— 0x41414141 """ + limit = int(limit) # Allow results from get function to be passed to format - if type(value) == list: + if isinstance(value, list): chain = value else: chain = get(value, limit, offset, hard_stop, hard_end) @@ -84,6 +95,20 @@ def format(value, limit=LIMIT, code=True, offset=0, hard_stop=None, hard_end=0): arrow_left = C.arrow(' %s ' % config_arrow_left) arrow_right = C.arrow(' %s ' % config_arrow_right) + # Colorize the chain + rest = [] + for link in chain: + symbol = pwndbg.symbol.get(link) or None + if symbol: + symbol = '%#x (%s)' % (link, symbol) + rest.append(M.get(link, symbol)) + + # If the dereference limit is zero, skip any enhancements. + if limit == 0: + return rest[0] + # Otherwise replace last element with the enhanced information. + rest = rest[:-1] + # Enhance the last entry # If there are no pointers (e.g. eax = 0x41414141), then enhance # the only element there is. @@ -93,20 +118,12 @@ def format(value, limit=LIMIT, code=True, offset=0, hard_stop=None, hard_end=0): # Otherwise, the last element in the chain is the non-pointer value. # We want to enhance the last pointer value. If an offset was used # chain failed at that offset, so display that offset. - elif len(chain) < limit: + elif len(chain) < limit + 1: enhanced = pwndbg.enhance.enhance(chain[-2] + offset, code=code) else: enhanced = C.contiguous('%s' % config_contiguous) - # Colorize the rest - rest = [] - for link in chain[:-1]: - symbol = pwndbg.symbol.get(link) or None - if symbol: - symbol = '%#x (%s)' % (link, symbol) - rest.append(M.get(link, symbol)) - if len(chain) == 1: return enhanced diff --git a/pwndbg/color/__init__.py b/pwndbg/color/__init__.py index 0b1c55f58b..3b1ed0812e 100644 --- a/pwndbg/color/__init__.py +++ b/pwndbg/color/__init__.py @@ -5,10 +5,13 @@ from __future__ import print_function from __future__ import unicode_literals +import os import re import pwndbg.memoize +from . import theme as theme + NORMAL = "\x1b[0m" BLACK = "\x1b[30m" RED = "\x1b[31m" @@ -53,15 +56,24 @@ def bold(x): return colorize(x, BOLD) def underline(x): return colorize(x, UNDERLINE) def colorize(x, color): return color + terminateWith(str(x), color) + NORMAL + +disable_colors = theme.Parameter('disable-colors', bool(os.environ.get('PWNDBG_DISABLE_COLORS')), 'whether to color the output or not') + + @pwndbg.memoize.reset_on_stop def generateColorFunctionInner(old, new): def wrapper(text): return new(old(text)) + return wrapper def generateColorFunction(config): function = lambda x: x - for color in str(config).split(','): + + if disable_colors: + return function + + for color in config.split(','): function = generateColorFunctionInner(function, globals()[color.lower().replace('-', '_')]) return function @@ -72,4 +84,9 @@ def terminateWith(x, color): return re.sub('\x1b\\[0m', NORMAL + color, x) def ljust_colored(x, length, char=' '): - return x + (length - len(strip(x))) * char + remaining = length - len(strip(x)) + return x + ((remaining // len(char) + 1) * char)[:remaining] + +def rjust_colored(x, length, char=' '): + remaining = length - len(strip(x)) + return ((remaining // len(char) + 1) * char)[:remaining] + x diff --git a/pwndbg/color/context.py b/pwndbg/color/context.py index 6ef7e2b4e3..6483defed8 100644 --- a/pwndbg/color/context.py +++ b/pwndbg/color/context.py @@ -9,16 +9,22 @@ import pwndbg.config as config from pwndbg.color import generateColorFunction +config_prefix_color = theme.ColoredParameter('code-prefix-color', 'none', "color for 'context code' command (prefix marker)") config_highlight_color = theme.ColoredParameter('highlight-color', 'green,bold', 'color added to highlights like source/pc') config_register_color = theme.ColoredParameter('context-register-color', 'bold', 'color for registers label') config_flag_value_color = theme.ColoredParameter('context-flag-value-color', 'none', 'color for flags register (register value)') config_flag_bracket_color = theme.ColoredParameter('context-flag-bracket-color', 'none', 'color for flags register (bracket)') config_flag_set_color = theme.ColoredParameter('context-flag-set-color', 'green,bold', 'color for flags register (flag set)') config_flag_unset_color = theme.ColoredParameter('context-flag-unset-color', 'red', 'color for flags register (flag unset)') +config_flag_changed_color = theme.ColoredParameter('context-flag-changed-color', 'underline', 'color for flags register (flag changed)') config_banner_color = theme.ColoredParameter('banner-color', 'blue', 'color for banner line') +config_banner_title = theme.ColoredParameter('banner-title-color', 'none', 'color for banner title') config_register_changed_color = theme.ColoredParameter('context-register-changed-color', 'normal', 'color for registers label (change marker)') config_register_changed_marker = theme.Parameter('context-register-changed-marker', '*', 'change marker for registers label') +def prefix(x): + return generateColorFunction(config.code_prefix_color)(x) + def highlight(x): return generateColorFunction(config.highlight_color)(x) @@ -40,5 +46,11 @@ def flag_set(x): def flag_unset(x): return generateColorFunction(config.context_flag_unset_color)(x) +def flag_changed(x): + return generateColorFunction(config.context_flag_changed_color)(x) + def banner(x): return generateColorFunction(config.banner_color)(x) + +def banner_title(x): + return generateColorFunction(config.banner_title_color)(x) diff --git a/pwndbg/color/disasm.py b/pwndbg/color/disasm.py index af6c07dd80..df06de3918 100644 --- a/pwndbg/color/disasm.py +++ b/pwndbg/color/disasm.py @@ -10,13 +10,13 @@ import pwndbg.chain import pwndbg.color.context as C import pwndbg.color.memory as M +import pwndbg.color.syntax_highlight as H import pwndbg.color.theme as theme import pwndbg.config as config import pwndbg.disasm.jump from pwndbg.color import generateColorFunction -from pwndbg.color import green from pwndbg.color import ljust_colored -from pwndbg.color import red +from pwndbg.color.message import on capstone_branch_groups = set(( capstone.CS_GRP_CALL, @@ -28,8 +28,15 @@ def branch(x): return generateColorFunction(config.disasm_branch_color)(x) + +def syntax_highlight(ins): + return H.syntax_highlight(ins, filename='.asm') + + def instruction(ins): asm = '%-06s %s' % (ins.mnemonic, ins.op_str) + if pwndbg.config.syntax_highlight: + asm = syntax_highlight(asm) is_branch = set(ins.groups) & capstone_branch_groups # Highlight the current line if enabled @@ -76,11 +83,11 @@ def instruction(ins): if is_branch: asm = asm.replace(ins.mnemonic, branch(ins.mnemonic), 1) - # If we know the conditional is taken, mark it as green. + # If we know the conditional is taken, mark it as taken. if ins.condition is None: asm = ' ' + asm elif ins.condition: - asm = green('✔ ') + asm + asm = on('✔ ') + asm else: asm = ' ' + asm diff --git a/pwndbg/color/lexer.py b/pwndbg/color/lexer.py new file mode 100644 index 0000000000..7310278f8f --- /dev/null +++ b/pwndbg/color/lexer.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import re + +import six +from pygments.lexer import RegexLexer +from pygments.lexer import include +from pygments.token import Comment +from pygments.token import Name +from pygments.token import Number +from pygments.token import Operator +from pygments.token import Other +from pygments.token import Punctuation +from pygments.token import String +from pygments.token import Text + +__all__ = ['PwntoolsLexer'] + +class PwntoolsLexer(RegexLexer): + """ + Fork from pwntools + https://github.com/Gallopsled/pwntools/blob/7860eecf025135380b137dd9df85dd02a2fd1667/pwnlib/lexer.py + + Edit: + * Remove Objdump rules + * Merge pygments-arm (https://github.com/heia-fr/pygments-arm) + """ + name = 'PwntoolsLexer' + filenames = ['*.s', '*.S', '*.asm'] + + #: optional Comment or Whitespace + string = r'"(\\"|[^"])*"' + char = r'[\w$.@-]' + identifier = r'(?:[a-zA-Z$_]' + char + '*|\.' + char + '+|or)' + number = r'(?:0[xX][a-zA-Z0-9]+|\d+)' + memory = r'(?:[\]\[])' + + eol = r'[\r\n]+' + + tokens = { + 'root': [ + include('whitespace'), + + # Label + (identifier + ':', Name.Label), + (number + ':', Name.Label), + + # AT&T directive + (r'\.' + identifier, Name.Attribute, 'directive-args'), + (r'lock|rep(n?z)?|data\d+', Name.Attribute), + + # Instructions + (identifier, Name.Function, 'instruction-args'), + + (r'[\r\n]+', Text), + ], + 'directive-args': [ + (identifier, Name.Constant), + (string, String), + ('@' + identifier, Name.Attribute), + (number, Number.Integer), + + (eol, Text, '#pop'), + (r'#.*?$', Comment, '#pop'), + + include('punctuation'), + include('whitespace') + ], + 'instruction-args': [ + # Fun things + (r'([\]\[]|BYTE|DWORD|PTR|\+|\-|}|{|\^|>>|<<|&)', Text), + + # Address constants + (identifier, Name.Constant), + ('=' + identifier, Name.Constant), # ARM symbol + (number, Number.Integer), + + # Registers + ('%' + identifier, Name.Variable), + ('$' + identifier, Name.Variable), + + # Numeric constants + ('$' + number, Number.Integer), + ('#' + number, Number.Integer), + + # ARM predefined constants + ('#' + identifier, Name.Constant), + + (r"$'(.|\\')'", String.Char), + + (eol, Text, '#pop'), + + include('punctuation'), + include('whitespace') + ], + 'whitespace': [ + (r'\n', Text), + (r'\s+', Text), + + # Block comments + # /* */ (AT&T) + (r'/\*.*?\*/', Comment), + + # Line comments + # // (AArch64) + # # (AT&T) + # ; (NASM/intel, LLVM) + # @ (ARM) + (r'(//|[#;@]).*$', Comment.Single) + ], + 'punctuation': [ + (r'[-*,.():]+', Punctuation) + ] + } + +# Note: convert all unicode() to str() if in Python2.7 since unicode_literals is enabled +# The pygments<=2.2.0 (latest stable when commit) in Python2.7 use 'str' type in rules matching +# We must convert all unicode back to str() +if six.PY2: + def _to_str(obj): + type_ = type(obj) + if type_ in (tuple, list): + return type_(map(_to_str, obj)) + elif type_ is unicode: + return str(obj) + return obj + + PwntoolsLexer.tokens = { + _to_str(k): _to_str(v) + for k, v in PwntoolsLexer.tokens.iteritems() + } diff --git a/pwndbg/color/message.py b/pwndbg/color/message.py new file mode 100644 index 0000000000..8a50650771 --- /dev/null +++ b/pwndbg/color/message.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +from pwndbg import config +from pwndbg.color import generateColorFunction +from pwndbg.color import theme + +config_status_on_color = theme.ColoredParameter('message-status-on-color', 'green', 'color of on status messages') +config_status_off_color = theme.ColoredParameter('message-status-off-color', 'red', 'color of off status messages') + +config_notice_color = theme.ColoredParameter('message-notice-color', 'purple', 'color of notice messages') +config_hint_color = theme.ColoredParameter('message-hint-color', 'yellow', 'color of hint and marker messages') +config_success_color = theme.ColoredParameter('message-success-color', 'green', 'color of success messages') +config_warning_color = theme.ColoredParameter('message-warning-color', 'yellow', 'color of warning messages') +config_error_color = theme.ColoredParameter('message-error-color', 'red', 'color of error messages') +config_system_color = theme.ColoredParameter('message-system-color', 'light-red', 'color of system messages') + +config_exit_color = theme.ColoredParameter('message-exit-color', 'red', 'color of exit messages') +config_breakpoint_color = theme.ColoredParameter('message-breakpoint-color', 'yellow', 'color of breakpoint messages') +config_signal_color = theme.ColoredParameter('message-signal-color', 'bold,red', 'color of signal messages') + +config_prompt_color = theme.ColoredParameter('prompt-color', 'bold,red', 'prompt color') + + +def on(msg): + return generateColorFunction(config.message_status_on_color)(msg) + + +def off(msg): + return generateColorFunction(config.message_status_off_color)(msg) + + +def notice(msg): + return generateColorFunction(config.message_notice_color)(msg) + + +def hint(msg): + return generateColorFunction(config.message_hint_color)(msg) + + +def success(msg): + return generateColorFunction(config.message_success_color)(msg) + + +def warn(msg): + return generateColorFunction(config.message_warning_color)(msg) + + +def error(msg): + return generateColorFunction(config.message_error_color)(msg) + + +def system(msg): + return generateColorFunction(config.message_system_color)(msg) + + +def exit(msg): + return generateColorFunction(config.message_exit_color)(msg) + + +def breakpoint(msg): + return generateColorFunction(config.message_breakpoint_color)(msg) + + +def signal(msg): + return generateColorFunction(config.message_signal_color)(msg) + + +def prompt(msg): + return generateColorFunction(config.prompt_color)(msg) diff --git a/pwndbg/color/syntax_highlight.py b/pwndbg/color/syntax_highlight.py new file mode 100644 index 0000000000..878b39b81f --- /dev/null +++ b/pwndbg/color/syntax_highlight.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import os.path +import re + +import pwndbg.config +from pwndbg.color import disable_colors +from pwndbg.color import message +from pwndbg.color import theme + +try: + import pygments + import pygments.lexers + import pygments.formatters + from pwndbg.color.lexer import PwntoolsLexer +except ImportError: + pygments = None + +pwndbg.config.Parameter('syntax-highlight', True, 'Source code / assembly syntax highlight') +style = theme.Parameter('syntax-highlight-style', 'monokai', 'Source code / assembly syntax highlight stylename of pygments module') + +formatter = pygments.formatters.Terminal256Formatter(style=str(style)) +pwntools_lexer = PwntoolsLexer() +lexer_cache = {} + +@pwndbg.config.Trigger([style]) +def check_style(): + global formatter + try: + formatter = pygments.formatters.Terminal256Formatter( + style=str(style) + ) + except pygments.util.ClassNotFound: + print(message.warn("The pygment formatter style '%s' is not found, restore to default" % style)) + style.revert_default() + + +def syntax_highlight(code, filename='.asm'): + # No syntax highlight if pygment is not installed + if not pygments or disable_colors: + return code + + filename = os.path.basename(filename) + + lexer = lexer_cache.get(filename, None) + + # If source code is asm, use our customized lexer. + # Note: We can not register our Lexer to pygments and use their APIs, + # since the pygment only search the lexers installed via setuptools. + if not lexer: + for glob_pat in PwntoolsLexer.filenames: + pat = '^' + glob_pat.replace('.', r'\.').replace('*', r'.*') + '$' + if re.match(pat, filename): + lexer = pwntools_lexer + break + + if not lexer: + try: + lexer = pygments.lexers.guess_lexer_for_filename(filename, code) + except pygments.util.ClassNotFound: + # no lexer for this file or invalid style + pass + + if lexer: + lexer_cache[filename] = lexer + code = pygments.highlight(code, lexer, formatter).rstrip() + + return code diff --git a/pwndbg/commands/__init__.py b/pwndbg/commands/__init__.py index 2a586cf546..890a3efd07 100644 --- a/pwndbg/commands/__init__.py +++ b/pwndbg/commands/__init__.py @@ -5,9 +5,11 @@ from __future__ import print_function from __future__ import unicode_literals +import argparse import functools import gdb +import six import pwndbg.chain import pwndbg.color @@ -19,23 +21,31 @@ import pwndbg.symbol import pwndbg.ui +commands = [] + class Command(gdb.Command): """Generic command wrapper""" - count = 0 - commands = [] - history = {} + command_names = set() + history = {} + + def __init__(self, function, prefix=False): + command_name = function.__name__ - def __init__(self, function, inc=True, prefix=False): - super(Command, self).__init__(function.__name__, gdb.COMMAND_USER, gdb.COMPLETE_EXPRESSION, prefix=prefix) + super(Command, self).__init__(command_name, gdb.COMMAND_USER, gdb.COMPLETE_EXPRESSION, prefix=prefix) self.function = function - if inc: - self.commands.append(self) + if command_name in self.command_names: + raise Exception('Cannot add command %s: already exists.' % command_name) + + self.command_names.add(command_name) + commands.append(self) functools.update_wrapper(self, function) self.__doc__ = function.__doc__ + self.repeat = False + def split_args(self, argument): """Split a command-line string from the user into arguments. @@ -123,9 +133,8 @@ def fix(self, arg): class ParsedCommandPrefix(ParsedCommand): - def __init__(self, function, inc=True, prefix=True): - super(ParsedCommand, self).__init__(function, inc, prefix) - + def __init__(self, function, prefix=True): + super(ParsedCommand, self).__init__(function, prefix) def fix(arg, sloppy=False, quiet=True, reraise=False): @@ -193,6 +202,15 @@ def _OnlyWhenRunning(*a, **kw): print("%s: The program is not being run." % function.__name__) return _OnlyWhenRunning +def OnlyWhenHeapIsInitialized(function): + @functools.wraps(function) + def _OnlyWhenHeapIsInitialized(*a, **kw): + if pwndbg.heap.current.is_initialized(): + return function(*a, **kw) + else: + print("%s: Heap is not initialized yet." % function.__name__) + return _OnlyWhenHeapIsInitialized + class QuietSloppyParsedCommand(ParsedCommand): def __init__(self, *a, **kw): @@ -205,7 +223,7 @@ class _ArgparsedCommand(Command): def __init__(self, parser, function, *a, **kw): self.parser = parser self.parser.prog = function.__name__ - function.__doc__ = self.parser.description + self.__doc__ = function.__doc__ = self.parser.description super(_ArgparsedCommand, self).__init__(function, *a, **kw) def split_args(self, argument): @@ -215,12 +233,18 @@ def split_args(self, argument): class ArgparsedCommand(object): """Adds documentation and offloads parsing for a Command via argparse""" - def __init__(self, parser): - self.parser = parser + def __init__(self, parser_or_desc): + """ + :param parser_or_desc: `argparse.ArgumentParser` instance or `str` + """ + if isinstance(parser_or_desc, six.string_types): + self.parser = argparse.ArgumentParser(description=parser_or_desc) + else: + self.parser = parser_or_desc # We want to run all integer and otherwise-unspecified arguments # through fix() so that GDB parses it. - for action in parser._actions: + for action in self.parser._actions: if action.dest == 'help': continue if action.type in (int, None): @@ -230,3 +254,19 @@ def __init__(self, parser): def __call__(self, function): return _ArgparsedCommand(self.parser, function) + + +def sloppy_gdb_parse(s): + """ + This function should be used as ``argparse.ArgumentParser`` .add_argument method's `type` helper. + + This makes the type being parsed as gdb value and if that parsing fails, + a string is returned. + + :param s: String. + :return: Whatever gdb.parse_and_eval returns or string. + """ + try: + return gdb.parse_and_eval(s) + except (TypeError, gdb.error): + return s diff --git a/pwndbg/commands/aslr.py b/pwndbg/commands/aslr.py index 60d9a1c7c9..bd00920510 100644 --- a/pwndbg/commands/aslr.py +++ b/pwndbg/commands/aslr.py @@ -9,10 +9,10 @@ import gdb -import pwndbg.color import pwndbg.commands import pwndbg.proc import pwndbg.vmmap +from pwndbg.color import message options = {'on':'off', 'off':'on'} @@ -35,9 +35,9 @@ def aslr(state=None): print("Change will take effect when the process restarts") aslr = pwndbg.vmmap.check_aslr() - status = pwndbg.color.red('OFF') + status = message.off('OFF') if aslr: - status = pwndbg.color.green('ON') + status = message.on('ON') print("ASLR is %s" % status) diff --git a/pwndbg/commands/auxv.py b/pwndbg/commands/auxv.py index 21b956ac0b..e2192748ce 100644 --- a/pwndbg/commands/auxv.py +++ b/pwndbg/commands/auxv.py @@ -5,7 +5,6 @@ from __future__ import print_function from __future__ import unicode_literals -import gdb import six import pwndbg.auxv @@ -13,12 +12,9 @@ import pwndbg.commands -@pwndbg.commands.ParsedCommand +@pwndbg.commands.ArgparsedCommand('Print information from the Auxiliary ELF Vector.') @pwndbg.commands.OnlyWhenRunning def auxv(): - """ - Print information from the Auxiliary ELF Vector. - """ - for k,v in sorted(pwndbg.auxv.get().items()): + for k, v in sorted(pwndbg.auxv.get().items()): if v is not None: print(k.ljust(24), v if not isinstance(v, six.integer_types) else pwndbg.chain.format(v)) diff --git a/pwndbg/commands/canary.py b/pwndbg/commands/canary.py new file mode 100644 index 0000000000..ed07ee162c --- /dev/null +++ b/pwndbg/commands/canary.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import pwndbg.auxv +import pwndbg.commands +import pwndbg.commands.telescope +import pwndbg.memory +import pwndbg.regs +import pwndbg.search +from pwndbg.color import message + + +def canary_value(): + auxv = pwndbg.auxv.get() + at_random = auxv.get('AT_RANDOM', None) + if at_random is None: + return None, None + + global_canary = pwndbg.memory.pvoid(at_random) + + # masking canary value as canaries on the stack has last byte = 0 + global_canary &= (pwndbg.arch.ptrmask ^ 0xFF) + + return global_canary, at_random + + +@pwndbg.commands.ArgparsedCommand('Print out the current stack canary.') +@pwndbg.commands.OnlyWhenRunning +def canary(): + global_canary, at_random = canary_value() + + if global_canary is None or at_random is None: + print(message.error("Couldn't find AT_RANDOM - can't display canary.")) + return + + print(message.notice("AT_RANDOM = %#x # points to (not masked) global canary value" % at_random)) + print(message.notice("Canary = 0x%x" % global_canary)) + + stack_canaries = list( + pwndbg.search.search(pwndbg.arch.pack(global_canary), mappings=pwndbg.stack.stacks.values()) + ) + + if not stack_canaries: + print(message.warn('No valid canaries found on the stacks.')) + return + + print(message.success('Found valid canaries on the stacks:')) + for stack_canary in stack_canaries: + pwndbg.commands.telescope.telescope(address=stack_canary, count=1) diff --git a/pwndbg/commands/checksec.py b/pwndbg/commands/checksec.py index eb3b941351..eea868aa27 100755 --- a/pwndbg/commands/checksec.py +++ b/pwndbg/commands/checksec.py @@ -7,15 +7,10 @@ import pwndbg.commands import pwndbg.which -import pwndbg.wrappers +import pwndbg.wrappers.checksec -@pwndbg.commands.Command +@pwndbg.commands.ArgparsedCommand('Prints out the binary security settings using `checksec`.') @pwndbg.commands.OnlyWithFile -def checksec(file=None): - ''' - Prints out the binary security settings. Attempts to call the binjitsu - checksec first, and then falls back to checksec.sh. - ''' - local_path = file or pwndbg.file.get_file(pwndbg.proc.exe) - print(pwndbg.wrappers.checksec("--file",local_path)) +def checksec(): + print(pwndbg.wrappers.checksec.get_raw_out()) diff --git a/pwndbg/commands/config.py b/pwndbg/commands/config.py index f350494476..63c288549c 100644 --- a/pwndbg/commands/config.py +++ b/pwndbg/commands/config.py @@ -8,11 +8,14 @@ from __future__ import print_function from __future__ import unicode_literals +import argparse + import pwndbg.commands import pwndbg.config from pwndbg.color import light_yellow from pwndbg.color import ljust_colored from pwndbg.color import strip +from pwndbg.color.message import hint def print_row(name, value, default, docstring, ljust_optname, ljust_value, empty_space=6): @@ -30,12 +33,30 @@ def extend_value_with_default(value, default): return value -@pwndbg.commands.Command -def config(): - """Shows pwndbg-specific configuration points""" - +def get_config_parameters(scope, filter_pattern): values = [v for k, v in pwndbg.config.__dict__.items() - if isinstance(v, pwndbg.config.Parameter) and v.scope == 'config'] + if isinstance(v, pwndbg.config.Parameter) and v.scope == scope] + + if filter_pattern: + filter_pattern = filter_pattern.lower() + values = [v for v in values if filter_pattern in v.optname.lower() or filter_pattern in v.docstring.lower()] + + return values + + +parser = argparse.ArgumentParser(description='Shows pwndbg-specific config. The list can be filtered.') +parser.add_argument('filter_pattern', type=str, nargs='?', default=None, + help='Filter to apply to config parameters names/descriptions') + + +@pwndbg.commands.ArgparsedCommand(parser) +def config(filter_pattern): + values = get_config_parameters('config', filter_pattern) + + if not values: + print(hint('No config parameter found with filter "{}"'.format(filter_pattern))) + return + longest_optname = max(map(len, [v.optname for v in values])) longest_value = max(map(len, [extend_value_with_default(repr(v.value), repr(v.default)) for v in values])) @@ -45,20 +66,28 @@ def config(): for v in sorted(values): print_row(v.optname, repr(v.value), repr(v.default), v.docstring, longest_optname, longest_value) - print(light_yellow('You can set config variable with `set `')) - print(light_yellow('You can generate configuration file using `configfile` ' - '- then put it in your .gdbinit after initializing pwndbg')) + print(hint('You can set config variable with `set `')) + print(hint('You can generate configuration file using `configfile` ' + '- then put it in your .gdbinit after initializing pwndbg')) + +configfile_parser = argparse.ArgumentParser(description='Generates a configuration file for the current Pwndbg options') +configfile_parser.add_argument('--show-all', action='store_true', help='Force displaying of all configs.') -@pwndbg.commands.Command + +@pwndbg.commands.ArgparsedCommand(configfile_parser) def configfile(show_all=False): - """Generates a configuration file for the current Pwndbg options""" configfile_print_scope('config', show_all) -@pwndbg.commands.Command +themefile_parser = argparse.ArgumentParser( + description='Generates a configuration file for the current Pwndbg theme options' +) +themefile_parser.add_argument('--show-all', action='store_true', help='Force displaying of all theme options.') + + +@pwndbg.commands.ArgparsedCommand(themefile_parser) def themefile(show_all=False): - """Generates a configuration file for the current Pwndbg theme options""" configfile_print_scope('theme', show_all) @@ -70,11 +99,11 @@ def configfile_print_scope(scope, show_all=False): if params: if not show_all: - print(light_yellow('Showing only changed values:')) + print(hint('Showing only changed values:')) for p in params: print('# %s: %s' % (p.optname, p.docstring)) print('# default: %s' % p.native_default) print('set %s %s' % (p.optname, p.native_value)) print() else: - print(light_yellow('No changed values. To see current values use `%s`.' % scope)) + print(hint('No changed values. To see current values use `%s`.' % scope)) diff --git a/pwndbg/commands/context.py b/pwndbg/commands/context.py index a4dead5476..d6171bf27f 100644 --- a/pwndbg/commands/context.py +++ b/pwndbg/commands/context.py @@ -5,6 +5,8 @@ from __future__ import print_function from __future__ import unicode_literals +import ast +import codecs import sys import gdb @@ -15,7 +17,7 @@ import pwndbg.color.backtrace as B import pwndbg.color.context as C import pwndbg.color.memory as M -import pwndbg.color.theme as theme +import pwndbg.color.syntax_highlight as H import pwndbg.commands import pwndbg.commands.nearpc import pwndbg.commands.telescope @@ -27,6 +29,8 @@ import pwndbg.symbol import pwndbg.ui import pwndbg.vmmap +from pwndbg.color import message +from pwndbg.color import theme def clear_screen(): @@ -39,7 +43,26 @@ def clear_screen(): config_clear_screen = pwndbg.config.Parameter('context-clear-screen', False, 'whether to clear the screen before printing the context') config_context_sections = pwndbg.config.Parameter('context-sections', 'regs disasm code stack backtrace', - 'which context sections are displayed by default (also controls order)') + 'which context sections are displayed (controls order)') + + +@pwndbg.config.Trigger([config_context_sections]) +def validate_context_sections(): + valid_values = [context.__name__.replace('context_', '') for context in context_sections.values()] + + # If someone tries to set an empty string, we let to do that informing about possible values + # (so that it is possible to have no context at all) + if not config_context_sections.value or config_context_sections.value.lower() in ('none', 'empty'): + config_context_sections.value = '' + print(message.warn("Sections set to be empty. FYI valid values are: %s" % ', '.join(valid_values))) + return + + for section in config_context_sections.split(): + if section not in valid_values: + print(message.warn("Invalid section: %s, valid values: %s" % (section, ', '.join(valid_values)))) + print(message.warn("(setting none of them like '' will make sections not appear)")) + config_context_sections.revert_default() + return # @pwndbg.events.stop @@ -52,7 +75,7 @@ def context(*args): Accepts subcommands 'reg', 'disasm', 'code', 'stack', 'backtrace', and 'args'. """ if len(args) == 0: - args = str(config_context_sections).split() + args = config_context_sections.split() args = [a[0] for a in args] @@ -71,11 +94,10 @@ def context(*args): sys.stdout.write(line + '\n') sys.stdout.flush() + def context_regs(): - result = [] - result.append(pwndbg.ui.banner("registers")) - result.extend(get_regs()) - return result + return [pwndbg.ui.banner("registers")] + get_regs() + @pwndbg.commands.Command @pwndbg.commands.OnlyWhenRunning @@ -86,6 +108,7 @@ def regs(*regs): pwndbg.config.Parameter('show-flags', False, 'whether to show flags registers') pwndbg.config.Parameter('show-retaddr-reg', False, 'whether to show return address register') + def get_regs(*regs): result = [] @@ -104,7 +127,7 @@ def get_regs(*regs): continue if reg not in pwndbg.regs: - print("Unknown register: %r" % reg) + message.warn("Unknown register: %r" % reg) continue value = pwndbg.regs[reg] @@ -135,7 +158,7 @@ def get_regs(*regs): name = C.flag_unset(name) if value & bit != last & bit: - name = pwndbg.color.underline(name) + name = C.flag_changed(name) names.append(name) if names: @@ -163,39 +186,82 @@ def context_disasm(): return banner + result theme.Parameter('highlight-source', True, 'whether to highlight the closest source line') +source_code_lines = pwndbg.config.Parameter('context-source-code-lines', + 10, + 'number of source code lines to print by the context command') +theme.Parameter('code-prefix', '►', "prefix marker for 'context code' command") + +@pwndbg.memoize.reset_on_start +def get_highlight_source(filename): + # Notice that the code is cached + with open(filename) as f: + source = f.read() + + if pwndbg.config.syntax_highlight: + source = H.syntax_highlight(source, filename) + + source_lines = source.splitlines() + source_lines = tuple(line.rstrip() for line in source_lines) + return source_lines + def context_code(): try: + # Compute the closest pc and line number symtab = gdb.selected_frame().find_sal().symtab linetable = symtab.linetable() closest_pc = -1 closest_line = -1 for line in linetable: - if line.pc <= pwndbg.regs.pc and line.pc > closest_pc: + if closest_pc < line.pc <= pwndbg.regs.pc: closest_line = line.line closest_pc = line.pc if closest_line < 0: return [] - source = gdb.execute('list %i' % closest_line, from_tty=False, to_string=True) + # Get the full source code + filename = symtab.fullname() + source = get_highlight_source(filename) # If it starts on line 1, it's not really using the # correct source code. if not source or closest_line <= 1: return [] - # highlight the current code line - source_lines = source.splitlines() - if pwndbg.config.highlight_source: - for i in range(len(source_lines)): - if source_lines[i].startswith('%s\t' % closest_line): - source_lines[i] = C.highlight(source_lines[i]) - break - - banner = [pwndbg.ui.banner("source")] - banner.extend(source_lines) + n = int(source_code_lines) + + # Compute the line range + start = max(closest_line - 1 - n//2, 0) + end = min(closest_line - 1 + n//2 + 1, len(source)) + num_width = len(str(end)) + + # split the code + source = source[start:end] + + # Compute the prefix_sign length + prefix_sign = pwndbg.config.code_prefix + prefix_width = len(prefix_sign) + + # Format the output + formatted_source = [] + for line_number, code in enumerate(source, start=start + 1): + fmt = ' {prefix_sign:{prefix_width}} {line_number:>{num_width}} {code}' + if pwndbg.config.highlight_source and line_number == closest_line: + fmt = C.highlight(fmt) + + line = fmt.format( + prefix_sign=C.prefix(prefix_sign) if line_number == closest_line else '', + prefix_width=prefix_width, + line_number=line_number, + num_width=num_width, + code=code + ) + formatted_source.append(line) + + banner = [pwndbg.ui.banner("Source (code)")] + banner.extend(formatted_source) return banner except: pass @@ -203,21 +269,22 @@ def context_code(): if not pwndbg.ida.available(): return [] - try: - name = pwndbg.ida.GetFunctionName(pwndbg.regs.pc) - addr = pwndbg.ida.LocByName(name) - lines = pwndbg.ida.decompile(addr) - return lines.splitlines() - except: - pass + name = pwndbg.ida.GetFunctionName(pwndbg.regs.pc) + addr = pwndbg.ida.LocByName(name) + # May be None when decompilation failed or user loaded wrong binary in IDA + code = pwndbg.ida.decompile(addr) + + if code: + return [pwndbg.ui.banner("Hexrays pseudocode")] + code.splitlines() + else: + return [] - return [] stack_lines = pwndbg.config.Parameter('context-stack-lines', 8, 'number of lines to print in the stack context') + def context_stack(): - result = [] - result.append(pwndbg.ui.banner("stack")) + result = [pwndbg.ui.banner("stack")] telescope = pwndbg.commands.telescope.telescope(pwndbg.regs.sp, to_string=True, count=stack_lines) if telescope: result.extend(telescope) @@ -225,6 +292,7 @@ def context_stack(): backtrace_frame_label = theme.Parameter('backtrace-frame-label', 'f ', 'frame number label for backtrace') + def context_backtrace(frame_count=10, with_banner=True): result = [] @@ -273,25 +341,22 @@ def context_backtrace(frame_count=10, with_banner=True): i += 1 return result -def context_args(): - result = [] - ################################################## - # DISABLED FOR NOW, I LIKE INLINE DISPLAY BETTER - ################################################## - # # For call instructions, attempt to resolve the target and - # # determine the number of arguments. - # for arg, value in pwndbg.arguments.arguments(pwndbg.disasm.one()): - # code = False if arg.type == 'char' else True - # pretty = pwndbg.chain.format(value, code=code) - # result.append('%-10s %s' % (arg.name+':', pretty)) - # if not result: - # return [] - # result.insert(0, pwndbg.ui.banner("arguments")) - return result +def context_args(with_banner=True): + args = pwndbg.arguments.format_args(pwndbg.disasm.one()) + + # early exit to skip section if no arg found + if not args: + return [] + + if with_banner: + args.insert(0, pwndbg.ui.banner("arguments")) + + return args last_signal = [] + def save_signal(signal): global last_signal last_signal = result = [] @@ -299,28 +364,35 @@ def save_signal(signal): if isinstance(signal, gdb.ExitedEvent): # Booooo old gdb if hasattr(signal, 'exit_code'): - result.append(pwndbg.color.red('Exited: %r' % signal.exit_code)) + result.append(message.exit('Exited: %r' % signal.exit_code)) elif isinstance(signal, gdb.SignalEvent): msg = 'Program received signal %s' % signal.stop_signal + if signal.stop_signal == 'SIGSEGV': - try: - si_addr = gdb.parse_and_eval("$_siginfo._sifields._sigfault.si_addr") - msg += ' (fault address %#x)' % int(si_addr or 0) - except gdb.error: - pass - msg = pwndbg.color.red(msg) - msg = pwndbg.color.bold(msg) - result.append(msg) + + # When users use rr (https://rr-project.org or https://github.com/mozilla/rr) + # we can't access $_siginfo, so lets just show current pc + # see also issue 476 + if _is_rr_present(): + msg += ' (current pc: %#x)' % pwndbg.regs.pc + else: + try: + si_addr = gdb.parse_and_eval("$_siginfo._sifields._sigfault.si_addr") + msg += ' (fault address %#x)' % int(si_addr or 0) + except gdb.error: + pass + result.append(message.signal(msg)) elif isinstance(signal, gdb.BreakpointEvent): for bkpt in signal.breakpoints: - result.append(pwndbg.color.yellow('Breakpoint %s' % (bkpt.location))) + result.append(message.breakpoint('Breakpoint %s' % (bkpt.location))) gdb.events.cont.connect(save_signal) gdb.events.stop.connect(save_signal) gdb.events.exited.connect(save_signal) + def context_signal(): return last_signal @@ -328,7 +400,22 @@ def context_signal(): context_sections = { 'r': context_regs, 'd': context_disasm, + 'a': context_args, 'c': context_code, 's': context_stack, 'b': context_backtrace } + + +@pwndbg.memoize.forever +def _is_rr_present(): + """ + Checks whether rr project is present (so someone launched e.g. `rr replay `) + """ + + # this is ugly but I couldn't find a better way to do it + # feel free to refactor it + globals_list_literal_str = gdb.execute('python print(list(globals().keys()))', to_string=True) + interpreter_globals = ast.literal_eval(globals_list_literal_str) + + return 'RRCmd' in interpreter_globals and 'RRWhere' in interpreter_globals diff --git a/pwndbg/commands/cpsr.py b/pwndbg/commands/cpsr.py index 2c41e09bd2..3c9c29946c 100644 --- a/pwndbg/commands/cpsr.py +++ b/pwndbg/commands/cpsr.py @@ -5,37 +5,35 @@ from __future__ import print_function from __future__ import unicode_literals -import gdb - import pwndbg.arch -import pwndbg.color import pwndbg.commands import pwndbg.regs +from pwndbg.color import context +from pwndbg.color import message -@pwndbg.commands.Command +@pwndbg.commands.ArgparsedCommand('Print out ARM CPSR register') @pwndbg.commands.OnlyWhenRunning def cpsr(): - 'Print out the ARM CPSR register' if pwndbg.arch.current != 'arm': - print("This is only available on ARM") + print(message.warn("This is only available on ARM")) return cpsr = pwndbg.regs.cpsr - N = cpsr & (1<<31) - Z = cpsr & (1<<30) - C = cpsr & (1<<29) - V = cpsr & (1<<28) - T = cpsr & (1<<5) - - bold = pwndbg.color.bold + N = cpsr & (1 << 31) + Z = cpsr & (1 << 30) + C = cpsr & (1 << 29) + V = cpsr & (1 << 28) + T = cpsr & (1 << 5) result = [ - bold('N') if N else 'n', - bold('Z') if Z else 'z', - bold('C') if C else 'c', - bold('V') if V else 'v', - bold('T') if T else 't' + context.flag_set('N') if N else context.flag_unset('n'), + context.flag_set('Z') if Z else context.flag_unset('z'), + context.flag_set('C') if C else context.flag_unset('c'), + context.flag_set('V') if V else context.flag_unset('v'), + context.flag_set('T') if T else context.flag_unset('t') ] - print('cpsr %#x [ %s ]' % (cpsr, ' '.join(result))) + + print('CPSR %s %s %s %s' % (context.flag_value('%#x' % cpsr), + context.flag_bracket('['), ' '.join(result), context.flag_bracket(']'))) diff --git a/pwndbg/commands/defcon.py b/pwndbg/commands/defcon.py index 4ceaf80008..d796246996 100644 --- a/pwndbg/commands/defcon.py +++ b/pwndbg/commands/defcon.py @@ -11,10 +11,7 @@ import pwndbg.memory import pwndbg.symbol import pwndbg.vmmap -from pwndbg.color import blue -from pwndbg.color import bold -from pwndbg.color import green -from pwndbg.color import red +from pwndbg.color import message @pwndbg.commands.Command @@ -38,7 +35,7 @@ def defcon_heap(addr=0x2aaaaaad5000): def heap_freebins(addr=0x0602558): - print(bold('Linked List')) + print(message.notice('Linked List')) # addr = 0x0602558 # addr = 0x060E360 @@ -81,12 +78,12 @@ def heap_allocations(addr, free): size &= ~3 if size > 0x1000: - print(red(bold("FOUND CORRUPTION OR END OF DATA"))) + print(message.error("FOUND CORRUPTION OR END OF DATA")) data = '' if not in_use or addr in free: - print(blue(bold("%#016x - usersize=%#x - [FREE %i]" % (addr, size, flags)))) + print(message.hint("%#016x - usersize=%#x - [FREE %i]" % (addr, size, flags))) linkedlist = (addr + 8 + size - 0x10) & pwndbg.arch.ptrmask @@ -100,7 +97,7 @@ def heap_allocations(addr, free): print(" bk: %#x" % bk) print(" fd: %#x" % fd) else: - print(green(bold("%#016x - usersize=%#x" % (addr, size)))) + print(message.notice("%#016x - usersize=%#x" % (addr, size))) pwndbg.commands.hexdump.hexdump(addr+8, size) addr += size + 8 diff --git a/pwndbg/commands/dumpargs.py b/pwndbg/commands/dumpargs.py index 290ee3cb7f..efff5efe47 100644 --- a/pwndbg/commands/dumpargs.py +++ b/pwndbg/commands/dumpargs.py @@ -5,24 +5,63 @@ from __future__ import print_function from __future__ import unicode_literals +import argparse + import pwndbg.arguments +import pwndbg.chain import pwndbg.commands +import pwndbg.commands.telescope import pwndbg.disasm +parser = argparse.ArgumentParser( + description='Prints determined arguments for call instruction. Pass --all to see all possible arguments.' +) +parser.add_argument('-f', '--force', action='store_true', help='Force displaying of all arguments.') + -@pwndbg.commands.Command +@pwndbg.commands.ArgparsedCommand(parser) @pwndbg.commands.OnlyWhenRunning -def dumpargs(*a): +def dumpargs(force=False): + force_text = "Use `%s --force` to force the display." % dumpargs.__name__ + + if not pwndbg.disasm.is_call() and not force: + print("Cannot dump args as current instruction is not a call.\n" + force_text) + return + + args = all_args() if force else call_args() + + if args: + print('\n'.join(args)) + elif force: + print("Couldn't resolve call arguments from registers.") + print("Detected ABI: {} ({} bit) either doesn't pass arguments through registers or is not implemented. Maybe they are passed on the stack?".format(pwndbg.arch.current, pwndbg.arch.ptrsize*8)) + else: + print("Couldn't resolve call arguments. Maybe the function doesn\'t take any?\n" + force_text) + + +def call_args(): """ - If the current instruction is a call instruction, print that arguments. + Returns list of resolved call argument strings for display. + Attempts to resolve the target and determine the number of arguments. + Should be used only when being on a call instruction. """ - result = [] + results = [] - # For call instructions, attempt to resolve the target and - # determine the number of arguments. for arg, value in pwndbg.arguments.get(pwndbg.disasm.one()): code = False if arg.type == 'char' else True pretty = pwndbg.chain.format(value, code=code) - result.append('%8s%-10s %s' % ('',arg.name+':', pretty)) + results.append(' %-10s %s' % (arg.name+':', pretty)) + + return results + + +def all_args(): + """ + Returns list of all argument strings for display. + """ + results = [] + + for name, value in pwndbg.arguments.arguments(): + results.append('%4s = %s' % (name, pwndbg.chain.format(value))) - print('\n'.join(result)) + return results diff --git a/pwndbg/commands/elf.py b/pwndbg/commands/elf.py index e6a9b83071..51ba8227ee 100755 --- a/pwndbg/commands/elf.py +++ b/pwndbg/commands/elf.py @@ -8,14 +8,12 @@ from elftools.elf.elffile import ELFFile import pwndbg.commands +from pwndbg.color import message -@pwndbg.commands.Command +@pwndbg.commands.ArgparsedCommand('Prints the section mappings contained in the ELF header.') @pwndbg.commands.OnlyWithFile def elfheader(): - """ - Prints the section mappings contained in the ELF header. - """ local_path = pwndbg.file.get_file(pwndbg.proc.exe) with open(local_path, 'rb') as f: @@ -32,25 +30,20 @@ def elfheader(): sections.append((start, start + size, section.name)) sections.sort() + for start, end, name in sections: print('%#x - %#x ' % (start, end), name) -@pwndbg.commands.Command +@pwndbg.commands.ArgparsedCommand('Prints any symbols found in the .got.plt section if it exists.') @pwndbg.commands.OnlyWithFile def gotplt(): - """ - Prints any symbols found in the .got.plt section if it exists. - """ print_symbols_in_section('.got.plt', '@got.plt') -@pwndbg.commands.Command +@pwndbg.commands.ArgparsedCommand('Prints any symbols found in the .plt section if it exists.') @pwndbg.commands.OnlyWithFile def plt(): - """ - Prints any symbols found in the .plt section if it exists. - """ print_symbols_in_section('.plt', '@plt') @@ -74,10 +67,14 @@ def print_symbols_in_section(section_name, filter_text=''): start, end = get_section_bounds(section_name) if start is None: - print(pwndbg.color.red('Could not find section')) + print(message.error('Could not find section')) return symbols = get_symbols_in_region(start, end, filter_text) + + if not symbols: + print(message.error('No symbols found in section %s' % section_name)) + for symbol, addr in symbols: print(hex(int(addr)) + ': ' + symbol) diff --git a/pwndbg/commands/got.py b/pwndbg/commands/got.py index 408526eb69..92b1f43895 100644 --- a/pwndbg/commands/got.py +++ b/pwndbg/commands/got.py @@ -12,10 +12,9 @@ import pwndbg.enhance import pwndbg.file import pwndbg.which -import pwndbg.wrappers -from pwndbg.color import green -from pwndbg.color import light_yellow -from pwndbg.color import red +import pwndbg.wrappers.checksec +import pwndbg.wrappers.readelf +from pwndbg.color import message parser = argparse.ArgumentParser(description='Show the state of the Global Offset Table') parser.add_argument('name_filter', help='Filter results by passed name.', @@ -25,35 +24,25 @@ @pwndbg.commands.ArgparsedCommand(parser) @pwndbg.commands.OnlyWhenRunning def got(name_filter=''): - local_path = pwndbg.file.get_file(pwndbg.proc.exe) - cs_out = pwndbg.wrappers.checksec("--file", local_path) - - file_out = pwndbg.wrappers.file(local_path) - if "statically" in file_out: - print(red("Binary is statically linked.")) - return - - readelf_out = pwndbg.wrappers.readelf("--relocs", local_path) - - jmpslots = '\n'.join(filter(lambda l: _extract_jumps(l), - readelf_out.splitlines())) + relro_status = pwndbg.wrappers.checksec.relro_status() + pie_status = pwndbg.wrappers.checksec.pie_status() + jmpslots = list(pwndbg.wrappers.readelf.get_jmpslots()) if not len(jmpslots): - print(red("NO JUMP_SLOT entries available in the GOT")) + print(message.error("NO JUMP_SLOT entries available in the GOT")) return - if "PIE enabled" in cs_out: + if "PIE enabled" in pie_status: bin_text_base = pwndbg.memory.page_align(pwndbg.elf.entry()) - relro_status = "No RELRO" - if "Full RELRO" in cs_out: - relro_status = "Full RELRO" - elif "Partial RELRO" in cs_out: - relro_status = "Partial RELRO" + relro_color = message.off + if 'Partial' in relro_status: + relro_color = message.warn + elif 'Full' in relro_status: + relro_color = message.on + print("\nGOT protection: %s | GOT functions: %d\n " % (relro_color(relro_status), len(jmpslots))) - print("\nGOT protection: %s | GOT functions: %d\n " % (green(relro_status), len(jmpslots.splitlines()))) - - for line in jmpslots.splitlines(): + for line in jmpslots: address, info, rtype, value, name = line.split()[:5] if name_filter not in name: @@ -61,23 +50,8 @@ def got(name_filter=''): address_val = int(address, 16) - if "PIE enabled" in cs_out: # if PIE, address is only the offset from the binary base address + if "PIE enabled" in pie_status: # if PIE, address is only the offset from the binary base address address_val = bin_text_base + address_val got_address = pwndbg.memory.pvoid(address_val) - print("[%s] %s -> %s" % (address, light_yellow(name), pwndbg.chain.format(got_address))) - - -def _extract_jumps(l): - try: - # Checks for records in `readelf --relocs ` which has type e.g. `R_X86_64_JUMP_SLO` - # NOTE: Because of that we DO NOT display entries that are not writeable (due to FULL RELRO) - # as they have `R_X86_64_GLOB_DAT` type. - # - # Probably we should display them seperately. - if 'JUMP' in l.split()[2]: - return l - else: - return False - except IndexError: - return False + print("[0x%x] %s -> %s" % (address_val, message.hint(name), pwndbg.chain.format(got_address))) diff --git a/pwndbg/commands/heap.py b/pwndbg/commands/heap.py index 1cbc158c7c..978d982381 100755 --- a/pwndbg/commands/heap.py +++ b/pwndbg/commands/heap.py @@ -5,76 +5,85 @@ from __future__ import print_function from __future__ import unicode_literals +import argparse import struct import gdb import six +import pwndbg.color.context as C import pwndbg.color.memory as M import pwndbg.commands -from pwndbg.color import bold -from pwndbg.color import red -from pwndbg.color import underline -from pwndbg.color import yellow +import pwndbg.typeinfo +from pwndbg.color import generateColorFunction +from pwndbg.color import message -def value_from_type(type_name, addr): - gdb_type = pwndbg.typeinfo.load(type_name) - return gdb.Value(addr).cast(gdb_type.pointer()).dereference() +def read_chunk(addr): + # in old versions of glibc, `mchunk_[prev_]size` was simply called `[prev_]size` + # to support both versions, we change the new names to the old ones here so that + # the rest of the code can deal with uniform names + renames = { + "mchunk_size": "size", + "mchunk_prev_size": "prev_size", + } + val = pwndbg.typeinfo.read_gdbvalue("struct malloc_chunk", addr) + return dict({ renames.get(key, key): int(val[key]) for key in val.type.keys() }, value=val) -def format_bin(bins, verbose=False): + +def format_bin(bins, verbose=False, offset=None): main_heap = pwndbg.heap.current - fd_offset = main_heap.chunk_key_offset('fd') + if offset is None: + offset = main_heap.chunk_key_offset('fd') result = [] for size in bins: - chain = bins[size] + b = bins[size] + if isinstance(b, tuple): + chain, count = b + else: + chain = b + count = None - if not verbose and chain == [0]: + if not verbose and (chain == [0] and not count): continue - formatted_chain = pwndbg.chain.format(chain, offset=fd_offset) + formatted_chain = pwndbg.chain.format(chain[0], offset=offset) if isinstance(size, int): size = hex(size) - result.append((bold(size) + ': ').ljust(13) + formatted_chain) + if count is not None: + line = (message.hint(size) + message.hint(' [%3d]' % count) + ': ').ljust(13) + else: + line = (message.hint(size) + ': ').ljust(13) + line += formatted_chain + result.append(line) if not result: - result.append(bold('empty')) + result.append(message.hint('empty')) return result @pwndbg.commands.ParsedCommand @pwndbg.commands.OnlyWhenRunning +@pwndbg.commands.OnlyWhenHeapIsInitialized def heap(addr=None): """ - Prints out all chunks in the main_arena, or the arena specified by `addr`. + Prints out chunks starting from the address specified by `addr`. """ - - main_heap = pwndbg.heap.current - main_arena = main_heap.get_arena(addr) - + main_heap = pwndbg.heap.current + main_arena = main_heap.main_arena if main_arena is None: return - heap_region = main_heap.get_region(addr) - - if heap_region is None: - print(red('Could not find the heap')) - return - - top = main_arena['top'] - last_remainder = main_arena['last_remainder'] - - print(bold('Top Chunk: ') + M.get(top)) - print(bold('Last Remainder: ') + M.get(last_remainder)) - print() + page = main_heap.get_heap_boundaries(addr) + if addr is None: + addr = page.vaddr # Print out all chunks on the heap # TODO: Add an option to print out only free or allocated chunks - addr = heap_region.vaddr - while addr <= top: + while addr < page.vaddr + page.memsz: chunk = malloc_chunk(addr) size = int(chunk['size']) @@ -86,6 +95,7 @@ def heap(addr=None): @pwndbg.commands.ParsedCommand @pwndbg.commands.OnlyWhenRunning +@pwndbg.commands.OnlyWhenHeapIsInitialized def arena(addr=None): """ Prints out the main arena or the arena at the specified by address. @@ -101,32 +111,38 @@ def arena(addr=None): @pwndbg.commands.ParsedCommand @pwndbg.commands.OnlyWhenRunning +@pwndbg.commands.OnlyWhenHeapIsInitialized def arenas(): """ - Prints out allocated arenas + Prints out allocated arenas. """ + heap = pwndbg.heap.current + for ar in heap.arenas: + print(ar) - heap = pwndbg.heap.current - addr = None - arena = heap.get_arena(addr) - main_arena_addr = int(arena.address) - fmt = '[%%%ds]' % (pwndbg.arch.ptrsize *2) - while addr != main_arena_addr: - - h = heap.get_region(addr) - if not h: - print(red('Could not find the heap')) - return - hdr = bold(fmt%(hex(addr) if addr else 'main')) - print(hdr, M.heap(str(h))) - addr = int(arena['next']) - arena = heap.get_arena(addr) +@pwndbg.commands.ArgparsedCommand('Print malloc thread cache info.') +@pwndbg.commands.OnlyWhenRunning +@pwndbg.commands.OnlyWhenHeapIsInitialized +def tcache(addr=None): + """ + Prints out the thread cache. + + Glibc 2.26 malloc introduced per-thread chunk cache. This command prints + out per-thread control structure of the cache. + """ + main_heap = pwndbg.heap.current + tcache = main_heap.get_tcache(addr) + + if tcache is None: + return + + print(tcache) - @pwndbg.commands.ParsedCommand @pwndbg.commands.OnlyWhenRunning +@pwndbg.commands.OnlyWhenHeapIsInitialized def mp(): """ Prints out the mp_ structure from glibc @@ -137,6 +153,7 @@ def mp(): @pwndbg.commands.ParsedCommand @pwndbg.commands.OnlyWhenRunning +@pwndbg.commands.OnlyWhenHeapIsInitialized def top_chunk(addr=None): """ Prints out the address of the top chunk of the main arena, or of the arena @@ -146,9 +163,9 @@ def top_chunk(addr=None): main_arena = main_heap.get_arena(addr) if main_arena is None: - heap_region = main_heap.get_region() + heap_region = main_heap.get_heap_boundaries() if not heap_region: - print(red('Could not find the heap')) + print(message.error('Could not find the heap')) return heap_start = heap_region.vaddr @@ -159,7 +176,7 @@ def top_chunk(addr=None): last_addr = None addr = heap_start while addr < heap_end: - chunk = value_from_type('struct malloc_chunk', addr) + chunk = read_chunk(addr) size = int(chunk['size']) # Clear the bottom 3 bits @@ -176,7 +193,8 @@ def top_chunk(addr=None): @pwndbg.commands.ParsedCommand @pwndbg.commands.OnlyWhenRunning -def malloc_chunk(addr): +@pwndbg.commands.OnlyWhenHeapIsInitialized +def malloc_chunk(addr,fake=False): """ Prints out the malloc_chunk at the specified address. """ @@ -185,36 +203,41 @@ def malloc_chunk(addr): if not isinstance(addr, six.integer_types): addr = int(addr) - chunk = value_from_type('struct malloc_chunk', addr) + chunk = read_chunk(addr) size = int(chunk['size']) actual_size = size & ~7 prev_inuse, is_mmapped, non_main_arena = main_heap.chunk_flags(size) arena = None - if non_main_arena: + if not fake and non_main_arena: arena = main_heap.get_heap(addr)['ar_ptr'] - - fastbins = main_heap.fastbins(arena) + + fastbins = [] if fake else main_heap.fastbins(arena) header = M.get(addr) + if fake: + header += message.prompt(' FAKE') if prev_inuse: if actual_size in fastbins: - header += yellow(' FASTBIN') + header += message.hint(' FASTBIN') else: - header += yellow(' PREV_INUSE') + header += message.hint(' PREV_INUSE') if is_mmapped: - header += yellow(' IS_MMAPED') + header += message.hint(' IS_MMAPED') if non_main_arena: - header += yellow(' NON_MAIN_ARENA') - print(header, chunk) + header += message.hint(' NON_MAIN_ARENA') + print(header, chunk["value"]) return chunk @pwndbg.commands.ParsedCommand @pwndbg.commands.OnlyWhenRunning -def bins(addr=None): +@pwndbg.commands.OnlyWhenHeapIsInitialized +def bins(addr=None, tcache_addr=None): """ - Prints out the contents of the fastbins, unsortedbin, smallbins, and largebins from the + Prints out the contents of the tcachebins, fastbins, unsortedbin, smallbins, and largebins from the main_arena or the specified address. """ + if pwndbg.heap.current.has_tcache(): + tcachebins(tcache_addr) fastbins(addr) unsortedbin(addr) smallbins(addr) @@ -222,6 +245,7 @@ def bins(addr=None): @pwndbg.commands.ParsedCommand @pwndbg.commands.OnlyWhenRunning +@pwndbg.commands.OnlyWhenHeapIsInitialized def fastbins(addr=None, verbose=True): """ Prints out the contents of the fastbins of the main arena or the arena @@ -235,12 +259,13 @@ def fastbins(addr=None, verbose=True): formatted_bins = format_bin(fastbins, verbose) - print(underline(yellow('fastbins'))) + print(C.banner('fastbins')) for node in formatted_bins: print(node) @pwndbg.commands.ParsedCommand @pwndbg.commands.OnlyWhenRunning +@pwndbg.commands.OnlyWhenHeapIsInitialized def unsortedbin(addr=None, verbose=True): """ Prints out the contents of the unsorted bin of the main arena or the @@ -254,12 +279,13 @@ def unsortedbin(addr=None, verbose=True): formatted_bins = format_bin(unsortedbin, verbose) - print(underline(yellow('unsortedbin'))) + print(C.banner('unsortedbin')) for node in formatted_bins: print(node) @pwndbg.commands.ParsedCommand @pwndbg.commands.OnlyWhenRunning +@pwndbg.commands.OnlyWhenHeapIsInitialized def smallbins(addr=None, verbose=False): """ Prints out the contents of the small bin of the main arena or the arena @@ -273,12 +299,13 @@ def smallbins(addr=None, verbose=False): formatted_bins = format_bin(smallbins, verbose) - print(underline(yellow('smallbins'))) + print(C.banner('smallbins')) for node in formatted_bins: print(node) @pwndbg.commands.ParsedCommand @pwndbg.commands.OnlyWhenRunning +@pwndbg.commands.OnlyWhenHeapIsInitialized def largebins(addr=None, verbose=False): """ Prints out the contents of the large bin of the main arena or the arena @@ -292,21 +319,41 @@ def largebins(addr=None, verbose=False): formatted_bins = format_bin(largebins, verbose) - print(underline(yellow('largebins'))) + print(C.banner('largebins')) + for node in formatted_bins: + print(node) + +@pwndbg.commands.ParsedCommand +@pwndbg.commands.OnlyWhenRunning +@pwndbg.commands.OnlyWhenHeapIsInitialized +def tcachebins(addr=None, verbose=False): + """ + Prints out the contents of the bins in current thread tcache or in tcache + at the specified address. + """ + main_heap = pwndbg.heap.current + tcachebins = main_heap.tcachebins(addr) + + if tcachebins is None: + return + + formatted_bins = format_bin(tcachebins, verbose, offset = main_heap.tcache_next_offset) + + print(C.banner('tcachebins')) for node in formatted_bins: print(node) @pwndbg.commands.ParsedCommand @pwndbg.commands.OnlyWhenRunning +@pwndbg.commands.OnlyWhenHeapIsInitialized def find_fake_fast(addr, size): """ Finds candidate fake fast chunks that will overlap with the specified address. Used for fastbin dups and house of spirit """ main_heap = pwndbg.heap.current - - fastbin = main_heap.fastbin_index(int(size)) max_fast = main_heap.global_max_fast + fastbin = main_heap.fastbin_index(int(size)) start = int(addr) - int(max_fast) mem = pwndbg.memory.read(start, max_fast, partial=True) @@ -318,11 +365,88 @@ def find_fake_fast(addr, size): 8: 'Q' }[pwndbg.arch.ptrsize] - print(red("FAKE CHUNKS")) + print(C.banner("FAKE CHUNKS")) for offset in range(max_fast - pwndbg.arch.ptrsize): candidate = mem[offset:offset + pwndbg.arch.ptrsize] if len(candidate) == pwndbg.arch.ptrsize: value = struct.unpack(fmt, candidate)[0] if main_heap.fastbin_index(value) == fastbin: - malloc_chunk(start+offset-pwndbg.arch.ptrsize) + malloc_chunk(start+offset-pwndbg.arch.ptrsize,fake=True) + + +vis_heap_chunks_parser = argparse.ArgumentParser(description='Visualize heap chunks at the specified address') +vis_heap_chunks_parser.add_argument('address', help='Start address') +vis_heap_chunks_parser.add_argument('count', nargs='?', default=2, + help='Number of chunks to visualize') + +@pwndbg.commands.ArgparsedCommand(vis_heap_chunks_parser) +@pwndbg.commands.OnlyWhenRunning +@pwndbg.commands.OnlyWhenHeapIsInitialized +def vis_heap_chunks(address, count): + address = int(address) + main_heap = pwndbg.heap.current + main_arena = main_heap.get_arena() + top_chunk = int(main_arena['top']) + + unpack = pwndbg.arch.unpack + + cells_map = {} + chunk_id = 0 + ptr_size = pwndbg.arch.ptrsize + while chunk_id < count: + prev_size = unpack(pwndbg.memory.read(address, ptr_size)) + current_size = unpack(pwndbg.memory.read(address+ptr_size, ptr_size)) + real_size = current_size & ~main_heap.malloc_align_mask + prev_inuse = current_size & 1 + stop_addr = address + real_size + + while address < stop_addr: + assert address not in cells_map + cells_map[address] = chunk_id + address += ptr_size + + if prev_inuse: + cells_map[address - real_size] -= 1 + + chunk_id += 1 + + # we reached top chunk, add it's metadata and break + if address >= top_chunk: + cells_map[address] = chunk_id + cells_map[address+ptr_size] = chunk_id + break + + # TODO: maybe print free chunks in bold or underlined + color_funcs = [ + generateColorFunction("yellow"), + generateColorFunction("cyan"), + generateColorFunction("purple"), + generateColorFunction("green"), + generateColorFunction("blue"), + ] + + addrs = sorted(cells_map.keys()) + + printed = 0 + out = '' + + for addr in addrs: + if printed % 2 == 0: + out += "\n0x%x:" % addr + + cell = unpack(pwndbg.memory.read(addr, ptr_size)) + cell_hex = '\t0x{:0{n}x}'.format(cell, n=ptr_size*2) + + chunk_idx = cells_map[addr] + color_func_idx = chunk_idx % len(color_funcs) + color_func = color_funcs[color_func_idx] + + out += color_func(cell_hex) + + printed += 1 + + if top_chunk in addrs: + out += "\t <-- Top chunk" + + print(out) diff --git a/pwndbg/commands/hexdump.py b/pwndbg/commands/hexdump.py index 308ac32046..cbe6bcdc6e 100644 --- a/pwndbg/commands/hexdump.py +++ b/pwndbg/commands/hexdump.py @@ -29,12 +29,16 @@ parser.add_argument('count', nargs='?', default=pwndbg.config.hexdump_bytes, help='Number of bytes to dump') + @pwndbg.commands.ArgparsedCommand(parser) @pwndbg.commands.OnlyWhenRunning def hexdump(address=None, count=pwndbg.config.hexdump_bytes): if hexdump.repeat: address = hexdump.last_address + hexdump.offset += 1 + else: + hexdump.offset = 0 address = int(address) address &= pwndbg.arch.ptrmask @@ -51,7 +55,9 @@ def hexdump(address=None, count=pwndbg.config.hexdump_bytes): print(e) return - for line in pwndbg.hexdump.hexdump(data, address=address, width=width): + for i, line in enumerate(pwndbg.hexdump.hexdump(data, address=address, width=width, offset=hexdump.offset)): print(line) + hexdump.offset += i hexdump.last_address = 0 +hexdump.offset = 0 diff --git a/pwndbg/commands/ida.py b/pwndbg/commands/ida.py index 7bd3f3b2e6..0aece60848 100644 --- a/pwndbg/commands/ida.py +++ b/pwndbg/commands/ida.py @@ -15,6 +15,7 @@ import pwndbg.commands.context import pwndbg.ida import pwndbg.regs +from pwndbg.gdbutils.functions import GdbFunction @pwndbg.commands.ParsedCommand @@ -32,104 +33,99 @@ def j(*args): pass -if pwndbg.ida.available(): - @pwndbg.commands.Command - @pwndbg.commands.OnlyWhenRunning - def up(n=1): - """ - Select and print stack frame that called this one. - An argument says how many frames up to go. - """ - f = gdb.selected_frame() - for i in range(n): - o = f.older() - if o: - o.select() - - bt = pwndbg.commands.context.context_backtrace(with_banner=False) - print('\n'.join(bt)) - - j() +@pwndbg.commands.Command +@pwndbg.commands.OnlyWhenRunning +def up(n=1): + """ + Select and print stack frame that called this one. + An argument says how many frames up to go. + """ + f = gdb.selected_frame() + for i in range(int(n)): + if f.older(): + f = f.older() + f.select() - @pwndbg.commands.Command - @pwndbg.commands.OnlyWhenRunning - def down(n=1): - """ - Select and print stack frame called by this one. - An argument says how many frames down to go. - """ - f = gdb.selected_frame() + bt = pwndbg.commands.context.context_backtrace(with_banner=False) + print('\n'.join(bt)) - for i in range(n): - o = f.newer() - if o: - o.select() + j() - bt = pwndbg.commands.context.context_backtrace(with_banner=False) - print('\n'.join(bt)) - j() +@pwndbg.commands.Command +@pwndbg.commands.OnlyWhenRunning +def down(n=1): + """ + Select and print stack frame called by this one. + An argument says how many frames down to go. + """ + f = gdb.selected_frame() + for i in range(int(n)): + if f.newer(): + f = f.newer() + f.select() - @pwndbg.commands.Command - def save_ida(): - if not pwndbg.ida.available(): - return + bt = pwndbg.commands.context.context_backtrace(with_banner=False) + print('\n'.join(bt)) - path = pwndbg.ida.GetIdbPath() + j() - # Need to handle emulated paths for Wine - if path.startswith('Z:'): - path = path[2:].replace('\\', '/') - pwndbg.ida.SaveBase(path) - basename = os.path.basename(path) - dirname = os.path.dirname(path) - backups = os.path.join(dirname, 'ida-backup') +@pwndbg.commands.Command +@pwndbg.ida.withIDA +def save_ida(): + """Save the IDA database""" + if not pwndbg.ida.available(): + return - if not os.path.isdir(backups): - os.mkdir(backups) + path = pwndbg.ida.GetIdbPath() - basename, ext = os.path.splitext(basename) - basename += '-%s' % datetime.datetime.now().isoformat() - basename += ext + # Need to handle emulated paths for Wine + if path.startswith('Z:'): + path = path[2:].replace('\\', '/') + pwndbg.ida.SaveBase(path) - # Windows doesn't like colons in paths - basename = basename.replace(':', '_') + basename = os.path.basename(path) + dirname = os.path.dirname(path) + backups = os.path.join(dirname, 'ida-backup') - full_path = os.path.join(backups, basename) + if not os.path.isdir(backups): + os.mkdir(backups) - pwndbg.ida.SaveBase(full_path) + basename, ext = os.path.splitext(basename) + basename += '-%s' % datetime.datetime.now().isoformat() + basename += ext - data = open(full_path, 'rb').read() + # Windows doesn't like colons in paths + basename = basename.replace(':', '_') - # Compress! - full_path_compressed = full_path + '.bz2' - bz2.BZ2File(full_path_compressed, 'w').write(data) + full_path = os.path.join(backups, basename) - # Remove old version - os.unlink(full_path) + pwndbg.ida.SaveBase(full_path) - save_ida() + data = open(full_path, 'rb').read() + # Compress! + full_path_compressed = full_path + '.bz2' + bz2.BZ2File(full_path_compressed, 'w').write(data) -class ida(gdb.Function): - """Evaluate ida.LocByName() on the supplied value. - """ + # Remove old version + os.unlink(full_path) - def __init__(self): - super(ida, self).__init__('ida') +save_ida() - def invoke(self, name): - name = name.string() - result = pwndbg.ida.LocByName(name) - if 0xffffe000 <= result <= 0xffffffff or 0xffffffffffffe000 <= result <= 0xffffffffffffffff: - raise ValueError("ida.LocByName(%r) == BADADDR" % name) +@GdbFunction() +def ida(name): - return result + """Evaluate ida.LocByName() on the supplied value.""" + name = name.string() + result = pwndbg.ida.LocByName(name) + if 0xffffe000 <= result <= 0xffffffff or 0xffffffffffffe000 <= result <= 0xffffffffffffffff: + raise ValueError("ida.LocByName(%r) == BADADDR" % name) -ida() + return result diff --git a/pwndbg/commands/misc.py b/pwndbg/commands/misc.py index 89acec7e1a..463cb821ff 100644 --- a/pwndbg/commands/misc.py +++ b/pwndbg/commands/misc.py @@ -50,21 +50,8 @@ def errno(err): @_pwndbg.commands.ArgparsedCommand(parser) def pwndbg(filter_pattern): - sorted_commands = list(_pwndbg.commands.Command.commands) - sorted_commands.sort(key=lambda x: x.__name__) - - if filter_pattern: - filter_pattern = filter_pattern.lower() - - for c in sorted_commands: - name = c.__name__ - docs = c.__doc__ - - if docs: docs = docs.strip() - if docs: docs = docs.splitlines()[0] - - if not filter_pattern or filter_pattern in name.lower() or (docs and filter_pattern in docs.lower()): - print("%-20s %s" % (name, docs)) + for name, docs in list_and_filter_commands(filter_pattern): + print("%-20s %s" % (name, docs)) @_pwndbg.commands.ParsedCommand @@ -78,13 +65,23 @@ def distance(a, b): print("%#x->%#x is %#x bytes (%#x words)" % (a, b, distance, distance // _arch.ptrsize)) -@_pwndbg.commands.Command -@_pwndbg.commands.OnlyWhenRunning -def canary(): - """Print out the current stack canary""" - auxv = _pwndbg.auxv.get() - at_random = auxv.get('AT_RANDOM', None) - if at_random is not None: - print("AT_RANDOM=%#x" % at_random) - else: - print("Couldn't find AT_RANDOM") +def list_and_filter_commands(filter_str): + sorted_commands = list(_pwndbg.commands.commands) + sorted_commands.sort(key=lambda x: x.__name__) + + if filter_str: + filter_str = filter_str.lower() + + results = [] + + for c in sorted_commands: + name = c.__name__ + docs = c.__doc__ + + if docs: docs = docs.strip() + if docs: docs = docs.splitlines()[0] + + if not filter_str or filter_str in name.lower() or (docs and filter_str in docs.lower()): + results.append((name, docs)) + + return results diff --git a/pwndbg/commands/nearpc.py b/pwndbg/commands/nearpc.py index 386e8aa508..9f1c702873 100644 --- a/pwndbg/commands/nearpc.py +++ b/pwndbg/commands/nearpc.py @@ -25,6 +25,7 @@ import pwndbg.symbol import pwndbg.ui import pwndbg.vmmap +from pwndbg.color import message def ljust_padding(lst): @@ -37,6 +38,7 @@ def ljust_padding(lst): pwndbg.color.theme.Parameter('nearpc-prefix', '►', 'prefix marker for nearpc command') pwndbg.config.Parameter('left-pad-disasm', True, 'whether to left-pad disassembly') nearpc_lines = pwndbg.config.Parameter('nearpc-lines', 10, 'number of additional lines to print for the nearpc command') +show_args = pwndbg.config.Parameter('nearpc-show-args', True, 'show call arguments below instruction') @pwndbg.commands.ParsedCommand @pwndbg.commands.OnlyWhenRunning @@ -44,6 +46,12 @@ def nearpc(pc=None, lines=None, to_string=False, emulate=False): """ Disassemble near a specified address. """ + + # Repeating nearpc (pressing enter) makes it show next addresses + # (writing nearpc explicitly again will reset its state) + if nearpc.repeat: + pc = nearpc.next_pc + result = [] # Fix the case where we only have one argument, and @@ -63,7 +71,7 @@ def nearpc(pc=None, lines=None, to_string=False, emulate=False): # Check whether we can even read this address if not pwndbg.memory.peek(pc): - result.append(pwndbg.color.red('Invalid address %#x' % pc)) + result.append(message.error('Invalid address %#x' % pc)) # # Load source data if it's available # pc_to_linenos = collections.defaultdict(lambda: []) @@ -80,10 +88,10 @@ def nearpc(pc=None, lines=None, to_string=False, emulate=False): # for line in symtab.linetable(): # pc_to_linenos[line.pc].append(line.line) - instructions = pwndbg.disasm.near(pc, lines, emulate=emulate) + instructions = pwndbg.disasm.near(pc, lines, emulate=emulate, show_prev_insns=not nearpc.repeat) if pwndbg.memory.peek(pc) and not instructions: - result.append(pwndbg.color.red('Invalid instructions at %#x' % pc)) + result.append(message.error('Invalid instructions at %#x' % pc)) # In case $pc is in a new map we don't know about, # this will trigger an exploratory search. @@ -93,46 +101,47 @@ def nearpc(pc=None, lines=None, to_string=False, emulate=False): symbols = [pwndbg.symbol.get(i.address) for i in instructions] addresses = ['%#x' % i.address for i in instructions] + nearpc.next_pc = instructions[-1].address + instructions[-1].size if instructions else 0 + # Format the symbol name for each instruction symbols = ['<%s> ' % sym if sym else '' for sym in symbols] # Pad out all of the symbols and addresses - if pwndbg.config.left_pad_disasm: + if pwndbg.config.left_pad_disasm and not nearpc.repeat: symbols = ljust_padding(symbols) addresses = ljust_padding(addresses) prev = None # Print out each instruction - for address_str, s, i in zip(addresses, symbols, instructions): - asm = D.instruction(i) - value = pwndbg.config.nearpc_prefix.value - - if isinstance(value, bytes): - value = codecs.decode(value, 'utf-8') + for address_str, symbol, instr in zip(addresses, symbols, instructions): + asm = D.instruction(instr) + prefix_sign = pwndbg.config.nearpc_prefix - prefix = ' %s' % (pwndbg.config.nearpc_prefix if i.address == pc else ' ' * len(value)) + # Show prefix only on the specified address and don't show it while in repeat-mode + show_prefix = instr.address == pc and not nearpc.repeat + prefix = ' %s' % (prefix_sign if show_prefix else ' ' * len(prefix_sign)) prefix = N.prefix(prefix) - if pwndbg.config.highlight_pc: - prefix = C.highlight(prefix) - pre = pwndbg.ida.Anterior(i.address) + pre = pwndbg.ida.Anterior(instr.address) if pre: result.append(N.ida_anterior(pre)) # Colorize address and symbol if not highlighted - if i.address != pc or not pwndbg.config.highlight_pc: + # symbol is fetched from gdb and it can be e.g. '' + if instr.address != pc or not pwndbg.config.highlight_pc or nearpc.repeat: address_str = N.address(address_str) - s = N.symbol(s) + symbol = N.symbol(symbol) elif pwndbg.config.highlight_pc: + prefix = C.highlight(prefix) address_str = C.highlight(address_str) - s = C.highlight(s) + symbol = C.highlight(symbol) - line = ' '.join((prefix, address_str, s, asm)) + line = ' '.join((prefix, address_str, symbol, asm)) # If there was a branch before this instruction which was not # contiguous, put in some ellipses. - if prev and prev.address + prev.size != i.address: + if prev and prev.address + prev.size != instr.address: result.append(N.branch_marker('%s' % nearpc_branch_marker)) # Otherwise if it's a branch and it *is* contiguous, just put @@ -142,8 +151,8 @@ def nearpc(pc=None, lines=None, to_string=False, emulate=False): result.append('%s' % nearpc_branch_marker_contiguous) # For syscall instructions, put the name on the side - if i.address == pc: - syscall_name = pwndbg.arguments.get_syscall_name(i) + if instr.address == pc: + syscall_name = pwndbg.arguments.get_syscall_name(instr) if syscall_name: line += ' <%s>' % N.syscall_name(syscall_name) @@ -151,31 +160,38 @@ def nearpc(pc=None, lines=None, to_string=False, emulate=False): # For call instructions, attempt to resolve the target and # determine the number of arguments. - for arg, value in pwndbg.arguments.get(i): - code = False if arg.type == 'char' else True - pretty = pwndbg.chain.format(value, code=code) - result.append('%8s%-10s %s' % ('', N.argument(arg.name) + ':', pretty)) - - prev = i + if show_args: + result.extend(['%8s%s' % ('', arg) for arg in pwndbg.arguments.format_args(instruction=instr)]) + prev = instr if not to_string: print('\n'.join(result)) return result + @pwndbg.commands.ParsedCommand @pwndbg.commands.OnlyWhenRunning def emulate(pc=None, lines=None, to_string=False, emulate=True): """ Like nearpc, but will emulate instructions from the current $PC forward. """ + nearpc.repeat = emulate_command.repeat return nearpc(pc, lines, to_string, emulate) + +emulate_command = emulate + + @pwndbg.commands.ParsedCommand @pwndbg.commands.OnlyWhenRunning -def pdisass(pc=None, lines=None): +def pdisass(pc=None, lines=None, to_string=False): """ Compatibility layer for PEDA's pdisass command """ - return nearpc(pc, lines, False, False) + nearpc.repeat = pdisass.repeat + return nearpc(pc, lines, to_string, False) + + +nearpc.next_pc = 0 diff --git a/pwndbg/commands/next.py b/pwndbg/commands/next.py index a3f29af9b3..8e173a3ead 100644 --- a/pwndbg/commands/next.py +++ b/pwndbg/commands/next.py @@ -21,12 +21,14 @@ def nextjmp(*args): if pwndbg.next.break_next_branch(): pwndbg.commands.context.context() + @pwndbg.commands.Command @pwndbg.commands.OnlyWhenRunning def nextjump(*args): """Breaks at the next jump instruction""" nextjmp(*args) + @pwndbg.commands.Command @pwndbg.commands.OnlyWhenRunning def nextcall(*args): @@ -34,6 +36,37 @@ def nextcall(*args): if pwndbg.next.break_next_call(*args): pwndbg.commands.context.context() + +@pwndbg.commands.Command +@pwndbg.commands.OnlyWhenRunning +def nextret(*args): + """Breaks at next return-like instruction""" + if pwndbg.next.break_next_ret(): + pwndbg.commands.context.context() + + +@pwndbg.commands.Command +@pwndbg.commands.OnlyWhenRunning +def stepret(*args): + """Breaks at next return-like instruction by 'stepping' to it""" + while pwndbg.proc.alive and not pwndbg.next.break_next_ret() and pwndbg.next.break_next_branch(): + # Here we are e.g. on a CALL instruction (temporarily breakpointed by `break_next_branch`) + # We need to step so that we take this branch instead of ignoring it + gdb.execute('si') + continue + + if pwndbg.proc.alive: + pwndbg.commands.context.context() + + +@pwndbg.commands.Command +@pwndbg.commands.OnlyWhenRunning +def nextproginstr(*args): + """Breaks at the next instruction that belongs to the running program""" + if pwndbg.next.break_on_program_code(): + pwndbg.commands.context.context() + + @pwndbg.commands.Command @pwndbg.commands.OnlyWhenRunning def stepover(*args): @@ -47,21 +80,46 @@ def so(*args): """Alias for stepover""" stepover(*args) + @pwndbg.commands.Command @pwndbg.commands.OnlyWhenRunning -def next_syscall(*args): +def nextsyscall(*args): """ - Breaks at the next syscall. + Breaks at the next syscall not taking branches. """ while pwndbg.proc.alive and not pwndbg.next.break_next_interrupt() and pwndbg.next.break_next_branch(): continue - pwndbg.commands.context.context() + + if pwndbg.proc.alive: + pwndbg.commands.context.context() @pwndbg.commands.Command @pwndbg.commands.OnlyWhenRunning def nextsc(*args): """ - Breaks at the next syscall. + Breaks at the next syscall not taking branches. + """ + nextsyscall(*args) + + +@pwndbg.commands.Command +@pwndbg.commands.OnlyWhenRunning +def stepsyscall(*args): + """ + Breaks at the next syscall by taking branches. """ - next_syscall(*args) + while pwndbg.proc.alive and not pwndbg.next.break_next_interrupt() and pwndbg.next.break_next_branch(): + # Here we are e.g. on a CALL instruction (temporarily breakpointed by `break_next_branch`) + # We need to step so that we take this branch instead of ignoring it + gdb.execute('si') + continue + + if pwndbg.proc.alive: + pwndbg.commands.context.context() + + +@pwndbg.commands.Command +@pwndbg.commands.OnlyWhenRunning +def stepsc(*args): + stepsyscall(*args) diff --git a/pwndbg/commands/pie.py b/pwndbg/commands/pie.py new file mode 100644 index 0000000000..e4f2d16f7c --- /dev/null +++ b/pwndbg/commands/pie.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import argparse +import os + +import gdb + +import pwndbg.auxv +import pwndbg.commands +import pwndbg.vmmap + + +def get_exe_name(): + """ + Returns exe name, tries AUXV first which should work fine on both + local and remote (gdbserver, qemu gdbserver) targets. + + If the value is somehow not present in AUXV, we just fallback to + local exe filepath. + + NOTE: This might be wrong for remote targets. + """ + path = pwndbg.auxv.get().get('AT_EXECFN') + + if path is not None: + # We normalize the path as `AT_EXECFN` might contain e.g. './a.out' + # so matching it against Page.objfile later on will be wrong; + # We want just 'a.out' + return os.path.normpath(path) + + return pwndbg.proc.exe + + +def translate_addr(offset, module): + mod_filter = lambda page: module in page.objfile + pages = list(filter(mod_filter, pwndbg.vmmap.get())) + + if not pages: + print('There are no memory pages in `vmmap` ' + 'for specified address=0x%x and module=%s' % (offset, module)) + return + + first_page = min(pages, key=lambda page: page.vaddr) + + addr = first_page.vaddr + offset + + if not any(addr in p for p in pages): + print('Offset 0x%x rebased to module %s as 0x%x is beyond module\'s ' + 'memory pages:' % (offset, module, addr)) + for p in pages: + print(p) + return + + return addr + + +parser = argparse.ArgumentParser() +parser.description = 'Calculate VA of RVA from PIE base.' +parser.add_argument('offset', nargs='?', default=0, + help='Offset from PIE base.') +parser.add_argument('module', type=str, nargs='?', default='', + help='Module to choose as base. Defaults to the target executable.') + +@pwndbg.commands.ArgparsedCommand(parser) +@pwndbg.commands.OnlyWhenRunning +def piebase(offset=None, module=None): + offset = int(offset) + if not module: + module = get_exe_name() + + addr = translate_addr(offset, module) + + if addr is not None: + print('Calculated VA from %s = 0x%x' % (module, addr)) + + +parser = argparse.ArgumentParser() +parser.description = 'Break at RVA from PIE base.' +parser.add_argument('offset', nargs='?', default=0, + help='Offset to add.') +parser.add_argument('module', type=str, nargs='?', default='', + help='Module to choose as base. Defaults to the target executable.') + +@pwndbg.commands.ArgparsedCommand(parser) +@pwndbg.commands.OnlyWhenRunning +def breakrva(offset=None, module=None): + offset = int(offset) + if not module: + module = get_exe_name() + addr = translate_addr(offset, module) + + if addr is not None: + spec = "*%#x" % (addr) + gdb.Breakpoint(spec) + + +@pwndbg.commands.QuietSloppyParsedCommand +@pwndbg.commands.OnlyWhenRunning +def brva(map): + """Alias for breakrva.""" + return breakrva(map) diff --git a/pwndbg/commands/probeleak.py b/pwndbg/commands/probeleak.py new file mode 100644 index 0000000000..11da0eb8d8 --- /dev/null +++ b/pwndbg/commands/probeleak.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import argparse +import math + +import gdb + +import pwndbg.arch +import pwndbg.color.memory as M +import pwndbg.color.message as message +import pwndbg.commands +import pwndbg.elf +import pwndbg.vmmap + + +def find_module(addr): + mod_filter = lambda page: page.vaddr <= addr < page.vaddr + page.memsz + pages = list(filter(mod_filter, pwndbg.vmmap.get())) + + if not pages: + return None + + if len(pages) > 1: + print(message.warn('Warning: There is more than one page containing address 0x%x (wtf?)', addr)) + + return pages[0] + +parser = argparse.ArgumentParser() +parser.description = 'Pointer scan for possible offset leaks.' +parser.add_argument('address', nargs='?', default='$sp', + help='Leak memory address') +parser.add_argument('count', nargs='?', default=0x40, + help='Leak size in bytes') + +@pwndbg.commands.ArgparsedCommand(parser) +@pwndbg.commands.OnlyWhenRunning +def probeleak(address=None, count=0x40): + + address = int(address) + address &= pwndbg.arch.ptrmask + count = max(int(count), 0) + ptrsize = pwndbg.arch.ptrsize + off_zeros = int(math.ceil(math.log(count,2)/4)) + + if count > address > 0x10000: # in case someone puts in an end address and not a count (smh) + count -= address + + if count % ptrsize > 0: + newcount = count - (count % ptrsize) + print(message.warning("Warning: count 0x%x is not a multiple of 0x%x; truncating to 0x%x." % (count, ptrsize, newcount))) + count = newcount + + try: + data = pwndbg.memory.read(address, count, partial=True) + except gdb.error as e: + print(message.error(str(e))) + return + + if not data: + print(message.error("Couldn't read memory at 0x%x" % (address,))) + return + + found = False + for i in range(0, count, ptrsize): + p = pwndbg.arch.unpack(data[i:i+ptrsize]) + page = find_module(p) + if page: + if not found: + print(M.legend()) + found = True + + mod_name = page.objfile + if not mod_name: + mod_name = '[anon]' + fmt = '+0x{offset:0{n1}x}: 0x{ptr:0{n2}x} = {page}' + right_text = ('(%s) %s + 0x%x') % (page.permstr, mod_name, p - page.vaddr + page.offset) + print(fmt.format(n1=off_zeros, n2=ptrsize*2, offset=i, ptr=p, page=M.get(p, text=right_text))) + if not found: + print(message.hint('No leaks found at 0x{:x}-0x{:x} :('.format(address, address+count))) diff --git a/pwndbg/commands/radare2.py b/pwndbg/commands/radare2.py index 24b926ceb1..05815728e1 100644 --- a/pwndbg/commands/radare2.py +++ b/pwndbg/commands/radare2.py @@ -10,7 +10,7 @@ import pwndbg.commands -parser = argparse.ArgumentParser(description=".", +parser = argparse.ArgumentParser(description='Launches radare2', epilog="Example: r2 -- -S -AA") parser.add_argument('--no-seek', action='store_true', help='Do not seek to current pc') @@ -32,4 +32,4 @@ def r2(arguments, no_seek=False): try: subprocess.call(cmd) except Exception: - print("Could not run radare2. Please ensure it's installed and in $PATH.") + print("Could not run radare2. Please ensure it's installed and in $PATH.") diff --git a/pwndbg/commands/search.py b/pwndbg/commands/search.py index acb88988d7..1d128cf5a6 100644 --- a/pwndbg/commands/search.py +++ b/pwndbg/commands/search.py @@ -12,13 +12,13 @@ import struct import pwndbg.arch -import pwndbg.color import pwndbg.color.memory as M import pwndbg.commands import pwndbg.config import pwndbg.enhance import pwndbg.search import pwndbg.vmmap +from pwndbg.color import message saved = set() @@ -118,6 +118,10 @@ def search(type, hex, string, executable, writable, value, mapping_name, save, n 'qword': 'Q' }[type] + # Work around Python 2.7.6 struct.pack / unicode incompatibility + # See https://github.com/pwndbg/pwndbg/pull/336 for more information. + fmt = str(fmt) + try: value = struct.pack(fmt, value) except struct.error as e: @@ -136,7 +140,7 @@ def search(type, hex, string, executable, writable, value, mapping_name, save, n mappings = [m for m in mappings if mapping_name in m.objfile] if not mappings: - print(pwndbg.color.red("Could not find mapping %r" % mapping_name)) + print(message.error("Could not find mapping %r" % mapping_name)) return # Prep the saved set if necessary diff --git a/pwndbg/commands/shell.py b/pwndbg/commands/shell.py index 33c9e57eae..8543b7fb3f 100644 --- a/pwndbg/commands/shell.py +++ b/pwndbg/commands/shell.py @@ -76,11 +76,13 @@ def register_shell_function(cmd): def handler(*a): - """Invokes %s""" % cmd if os.fork() == 0: os.execvp(cmd, (cmd,) + a) os.wait() + handler.__name__ = str(cmd) + handler.__doc__ = 'Invokes {}'.format(cmd) + pwndbg.commands.Command(handler, False) for cmd in shellcmds: diff --git a/pwndbg/commands/stack.py b/pwndbg/commands/stack.py index 2a82246752..8483612f9b 100644 --- a/pwndbg/commands/stack.py +++ b/pwndbg/commands/stack.py @@ -3,8 +3,6 @@ from __future__ import print_function from __future__ import unicode_literals -import argparse - import gdb import pwndbg.arch @@ -14,11 +12,7 @@ import pwndbg.vmmap -p = argparse.ArgumentParser(description=''' -Print out the stack addresses that contain return addresses -''') - -@pwndbg.commands.ArgparsedCommand(p) +@pwndbg.commands.ArgparsedCommand('Print out the stack addresses that contain return addresses.') @pwndbg.commands.OnlyWhenRunning def retaddr(): sp = pwndbg.regs.sp diff --git a/pwndbg/commands/start.py b/pwndbg/commands/start.py index 7adc9b879c..49aa783614 100644 --- a/pwndbg/commands/start.py +++ b/pwndbg/commands/start.py @@ -16,6 +16,15 @@ import pwndbg.events import pwndbg.symbol +# Py 2 vs Py 3 +try: + from shlex import quote +except ImportError: + from pipes import quote + + + + break_on_first_instruction = False @@ -66,5 +75,5 @@ def entry(*a): """ global break_on_first_instruction break_on_first_instruction = True - run = 'run ' + ' '.join(a) + run = 'run ' + ' '.join(map(quote, a)) gdb.execute(run, from_tty=False) diff --git a/pwndbg/commands/telescope.py b/pwndbg/commands/telescope.py index ab3480e3bd..ac4c04c9da 100644 --- a/pwndbg/commands/telescope.py +++ b/pwndbg/commands/telescope.py @@ -24,12 +24,14 @@ import pwndbg.regs import pwndbg.typeinfo -telescope_lines = pwndbg.config.Parameter('telescope-lines', - 8, - 'number of lines to printed by the telescope command') +telescope_lines = pwndbg.config.Parameter('telescope-lines', 8, 'number of lines to printed by the telescope command') +skip_repeating_values = pwndbg.config.Parameter('telescope-skip-repeating-val', True, + 'whether to skip repeating values of the telescope command') + offset_separator = theme.Parameter('telescope-offset-separator', '│', 'offset separator of the telescope command') offset_delimiter = theme.Parameter('telescope-offset-delimiter', ':', 'offset delimiter of the telescope command') -repeating_maker = theme.Parameter('telescope-repeating-marker', '... ↓', 'repeating values marker of the telescope command') +repeating_marker = theme.Parameter('telescope-repeating-marker', '... ↓', + 'repeating values marker of the telescope command') @pwndbg.commands.ParsedCommand @@ -39,11 +41,17 @@ def telescope(address=None, count=telescope_lines, to_string=False): Recursively dereferences pointers starting at the specified address ($sp by default) """ + ptrsize = pwndbg.typeinfo.ptrsize + if telescope.repeat: + address = telescope.last_address + ptrsize + telescope.offset += 1 + else: + telescope.offset = 0 + address = int(address if address else pwndbg.regs.sp) & pwndbg.arch.ptrmask count = max(int(count), 1) & pwndbg.arch.ptrmask delimiter = T.delimiter(offset_delimiter) separator = T.separator(offset_separator) - ptrsize = pwndbg.typeinfo.ptrsize # Allow invocation of "telescope 20" to dump 20 bytes at the stack pointer if address < pwndbg.memory.MMAP_MIN_ADDR and not pwndbg.memory.peek(address): @@ -93,18 +101,21 @@ def telescope(address=None, count=telescope_lines, to_string=False): # Collapse repeating values. value = pwndbg.memory.pvoid(addr) - if last == value: + if skip_repeating_values and last == value: if not skip: - result.append(T.repeating_marker('%s' % repeating_maker)) + result.append(T.repeating_marker('%s' % repeating_marker)) skip = True continue last = value skip = False - line = ' '.join((T.offset("%02x%s%04x%s" % (i, delimiter, addr-start, separator)), + line = ' '.join((T.offset("%02x%s%04x%s" % (i + telescope.offset, delimiter, + addr - start + (telescope.offset * ptrsize), separator)), T.register(regs[addr].ljust(longest_regs)), pwndbg.chain.format(addr))) result.append(line) + telescope.offset += i + telescope.last_address = addr if not to_string: print('\n'.join(result)) @@ -112,16 +123,20 @@ def telescope(address=None, count=telescope_lines, to_string=False): return result -parser = argparse.ArgumentParser(description='dereferences on stack data with specified count and offset') +parser = argparse.ArgumentParser(description='dereferences on stack data with specified count and offset.') parser.add_argument('count', nargs='?', default=8, type=int, help='number of element to dump') parser.add_argument('offset', nargs='?', default=0, type=int, help='Element offset from $sp (support negative offset)') + + @pwndbg.commands.ArgparsedCommand(parser) @pwndbg.commands.OnlyWhenRunning def stack(count, offset): - """ - Recursively dereferences pointers on the stack - """ ptrsize = pwndbg.typeinfo.ptrsize + telescope.repeat = stack.repeat telescope(address=pwndbg.regs.sp + offset * ptrsize, count=count) + + +telescope.last_address = 0 +telescope.offset = 0 diff --git a/pwndbg/commands/theme.py b/pwndbg/commands/theme.py index a107ac969a..6582593471 100644 --- a/pwndbg/commands/theme.py +++ b/pwndbg/commands/theme.py @@ -8,20 +8,30 @@ from __future__ import print_function from __future__ import unicode_literals +import argparse + import pwndbg.color.theme import pwndbg.commands import pwndbg.config from pwndbg.color import generateColorFunction -from pwndbg.color import light_yellow +from pwndbg.color.message import hint from pwndbg.commands.config import extend_value_with_default +from pwndbg.commands.config import get_config_parameters from pwndbg.commands.config import print_row +parser = argparse.ArgumentParser(description='Shows pwndbg-specific theme config. The list can be filtered.') +parser.add_argument('filter_pattern', type=str, nargs='?', default=None, + help='Filter to apply to theme parameters names/descriptions') + + +@pwndbg.commands.ArgparsedCommand(parser) +def theme(filter_pattern): + values = get_config_parameters('theme', filter_pattern) + + if not values: + print(hint('No theme parameter found with filter "{}"'.format(filter_pattern))) + return -@pwndbg.commands.Command -def theme(): - """Shows pwndbg-specific theme configuration points""" - values = [v for k, v in pwndbg.config.__dict__.items() - if isinstance(v, pwndbg.config.Parameter) and v.scope == 'theme'] longest_optname = max(map(len, [v.optname for v in values])) longest_value = max(map(len, [extend_value_with_default(str(v.value), str(v.default)) for v in values])) @@ -39,6 +49,6 @@ def theme(): default = repr(v.default) print_row(v.optname, value, default, v.docstring, longest_optname, longest_value) - print(light_yellow('You can set theme variable with `set `')) - print(light_yellow('You can generate theme config file using `themefile` ' - '- then put it in your .gdbinit after initializing pwndbg')) + print(hint('You can set theme variable with `set `')) + print(hint('You can generate theme config file using `themefile` ' + '- then put it in your .gdbinit after initializing pwndbg')) diff --git a/pwndbg/commands/version.py b/pwndbg/commands/version.py index 9257b940df..137cee763c 100644 --- a/pwndbg/commands/version.py +++ b/pwndbg/commands/version.py @@ -14,25 +14,58 @@ import gdb import pwndbg -import pwndbg.color import pwndbg.commands +import pwndbg.ida +from pwndbg.color import message def _gdb_version(): - return gdb.execute('show version', to_string=True).split('\n')[0] + try: + return gdb.VERSION # GDB >= 8.1 (or earlier?) + except AttributeError: + return gdb.execute('show version', to_string=True).split('\n')[0] def _py_version(): return sys.version.replace('\n', ' ') +def capstone_version(): + try: + import capstone + return '.'.join(map(str, capstone.cs_version())) + except ImportError: + return 'not found' + + +def unicorn_version(): + try: + import unicorn + return unicorn.__version__ + except ImportError: + return 'not found' + + @pwndbg.commands.Command def version(): """ Displays gdb, python and pwndbg versions. """ - gdb_str = 'Gdb: %s' % _gdb_version() - py_str = 'Python: %s' % _py_version() - pwndbg_str = 'Pwndbg: %s' % pwndbg.__version__ + gdb_str = 'Gdb: %s' % _gdb_version() + py_str = 'Python: %s' % _py_version() + pwndbg_str = 'Pwndbg: %s' % pwndbg.__version__ + + capstone_str = 'Capstone: %s' % capstone_version() + unicorn_str = 'Unicorn: %s' % unicorn_version() + + all_versions = (gdb_str, py_str, pwndbg_str, capstone_str, unicorn_str) + + ida_versions = pwndbg.ida.get_ida_versions() + + if ida_versions is not None: + ida_version = 'IDA PRO: %s' % ida_versions['ida'] + ida_py_ver = 'IDA Py: %s' % ida_versions['python'] + ida_hr_ver = 'Hexrays: %s' % ida_versions['hexrays'] + all_versions += (ida_version, ida_py_ver, ida_hr_ver) - print('\n'.join(map(pwndbg.color.light_red, (gdb_str, py_str, pwndbg_str)))) + print('\n'.join(map(message.system, all_versions))) diff --git a/pwndbg/commands/vmmap.py b/pwndbg/commands/vmmap.py index 9646299639..811882fb02 100644 --- a/pwndbg/commands/vmmap.py +++ b/pwndbg/commands/vmmap.py @@ -1,43 +1,138 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ -Command to print the vitual memory map a la /proc/self/maps. +Command to print the virtual memory map a la /proc/self/maps. """ from __future__ import absolute_import from __future__ import division from __future__ import print_function from __future__ import unicode_literals +import argparse + import gdb import six +from elftools.elf.constants import SH_FLAGS +from elftools.elf.elffile import ELFFile import pwndbg.color.memory as M import pwndbg.commands -import pwndbg.compat +import pwndbg.elf import pwndbg.vmmap -@pwndbg.commands.QuietSloppyParsedCommand +def pages_filter(s): + gdbval_or_str = pwndbg.commands.sloppy_gdb_parse(s) + + # returns a module filter + if isinstance(gdbval_or_str, six.string_types): + module_name = gdbval_or_str + return lambda page: module_name in page.objfile + + # returns an address filter + elif isinstance(gdbval_or_str, six.integer_types + (gdb.Value,)): + addr = gdbval_or_str + return lambda page: addr in page + + else: + raise argparse.ArgumentTypeError('Unknown vmmap argument type.') + + +parser = argparse.ArgumentParser() +parser.description = 'Print virtual memory map pages. Results can be filtered by providing address/module name.' +parser.add_argument('pages_filter', type=pages_filter, nargs='?', default=None, + help='Address or module name.') + + +@pwndbg.commands.ArgparsedCommand(parser) @pwndbg.commands.OnlyWhenRunning -def vmmap(map=None): - """ - Print the virtal memory map, or the specific mapping for the - provided address / module name. - """ - int_map = None - str_map = None - - if isinstance(map, six.string_types): - str_map = map - elif isinstance(map, six.integer_types + (gdb.Value,)): - int_map = int(map) +def vmmap(pages_filter=None): + pages = list(filter(pages_filter, pwndbg.vmmap.get())) + + if not pages: + print('There are no mappings for specified address or module.') + return print(M.legend()) + for page in pages: + print(M.get(page.vaddr, text=str(page))) - for page in pwndbg.vmmap.get(): - if str_map and str_map not in page.objfile: - continue - if int_map and int_map not in page: - continue - print(M.get(page.vaddr, text=str(page))) +parser = argparse.ArgumentParser() +parser.description = 'Add Print virtual memory map page.' +parser.add_argument('start', help='Starting virtual address') +parser.add_argument('size', help='Size of the address space, in bytes') +parser.add_argument('flags', nargs='?', type=str, default='', help='Flags set by the ELF file, see PF_X, PF_R, PF_W') +parser.add_argument('offset', nargs='?', default=0, help='Offset into the original ELF file that the data is loaded from') + +@pwndbg.commands.ArgparsedCommand(parser) +def vmmap_add(start, size, flags, offset): + page_flags = { + 'r': pwndbg.elf.PF_R, + 'w': pwndbg.elf.PF_W, + 'x': pwndbg.elf.PF_X, + } + perm = 0 + for flag in flags: + flag_val = page_flags.get(flag, None) + if flag_val is None: + print('Invalid page flag "%s"', flag) + return + perm |= flag_val + + page = pwndbg.memory.Page(start, size, perm, offset) + pwndbg.vmmap.add_custom_page(page) + + print('%r added' % page) + + +@pwndbg.commands.ParsedCommand +def vmmap_clear(): + pwndbg.vmmap.clear_custom_page() + + +parser = argparse.ArgumentParser() +parser.description = 'Load virtual memory map pages from ELF file.' +parser.add_argument('filename', nargs='?', type=str, help='ELF filename, by default uses current loaded filename.') + +@pwndbg.commands.ArgparsedCommand(parser) +def vmmap_load(filename): + if filename is None: + filename = pwndbg.proc.exe + + print('Load "%s" ...' % filename) + + # TODO: Add an argument to let use to choose loading the page information from sections or segments + + # Use section information to recover the segment information. + # The entry point of bare metal enviroment is often at the first segment. + # For example, assume the entry point is at 0x8000. + # In most of case, link will create a segment and starts from 0x0. + # This cause all values less than 0x8000 be considered as a valid pointer. + pages = [] + with open(filename, 'rb') as f: + elffile = ELFFile(f) + + for section in elffile.iter_sections(): + vaddr = section['sh_addr'] + memsz = section['sh_size'] + sh_flags = section['sh_flags'] + offset = section['sh_offset'] + + # Don't add the sections that aren't mapped into memory + if not sh_flags & SH_FLAGS.SHF_ALLOC: + continue + + # Guess the segment flags from section flags + flags = pwndbg.elf.PF_R + if sh_flags & SH_FLAGS.SHF_WRITE: + flags |= pwndbg.elf.PF_W + if sh_flags & SH_FLAGS.SHF_EXECINSTR: + flags |= pwndbg.elf.PF_X + + page = pwndbg.memory.Page(vaddr, memsz, flags, offset, filename) + pages.append(page) + + for page in pages: + pwndbg.vmmap.add_custom_page(page) + print('%r added' % page) diff --git a/pwndbg/commands/windbg.py b/pwndbg/commands/windbg.py index fceb22b509..d17db38360 100644 --- a/pwndbg/commands/windbg.py +++ b/pwndbg/commands/windbg.py @@ -8,9 +8,11 @@ from __future__ import print_function from __future__ import unicode_literals +import argparse import codecs import math import sys +from builtins import str import gdb @@ -171,9 +173,15 @@ def eX(size, address, data, hex=True): if address is None: return - for i,bytestr in enumerate(data): + for i, bytestr in enumerate(data): if hex: + bytestr = str(bytestr) + + if bytestr.startswith('0x'): + bytestr = bytestr[2:] + bytestr = bytestr.rjust(size*2, '0') + data = codecs.decode(bytestr, 'hex') else: data = bytestr @@ -216,20 +224,28 @@ def dqs(*a): return pwndbg.commands.telescope.telescope(*a) -@pwndbg.commands.ParsedCommand +da_parser = argparse.ArgumentParser() +da_parser.description = 'Dump a string at the specified address.' +da_parser.add_argument('address', help='Address to dump') +da_parser.add_argument('max', type=int, nargs='?', default=256, + help='Maximum string length') +@pwndbg.commands.ArgparsedCommand(da_parser) @pwndbg.commands.OnlyWhenRunning -def da(address, max=256): - """ - Dump a string at the specified address. - """ +def da(address, max): + address = int(address) + address &= pwndbg.arch.ptrmask print("%x" % address, repr(pwndbg.strings.get(address, max))) -@pwndbg.commands.ParsedCommand +ds_parser = argparse.ArgumentParser() +ds_parser.description = 'Dump a string at the specified address.' +ds_parser.add_argument('address', help='Address to dump') +ds_parser.add_argument('max', type=int, nargs='?', default=256, + help='Maximum string length') +@pwndbg.commands.ArgparsedCommand(ds_parser) @pwndbg.commands.OnlyWhenRunning -def ds(address, max=256): - """ - Dump a string at the specified address. - """ +def ds(address, max): + address = int(address) + address &= pwndbg.arch.ptrmask print("%x" % address, repr(pwndbg.strings.get(address, max))) @pwndbg.commands.ParsedCommand @@ -282,14 +298,14 @@ def bp(where): @pwndbg.commands.ParsedCommand @pwndbg.commands.OnlyWhenRunning -def u(where=None, n=5): +def u(where=None, n=5, to_string=False): """ Starting at the specified address, disassemble N instructions (default 5). """ if where is None: where = pwndbg.regs.pc - pwndbg.commands.nearpc.nearpc(where, n) + return pwndbg.commands.nearpc.nearpc(where, n, to_string) @pwndbg.commands.Command @pwndbg.commands.OnlyWhenRunning diff --git a/pwndbg/commands/xinfo.py b/pwndbg/commands/xinfo.py new file mode 100644 index 0000000000..2533655ec8 --- /dev/null +++ b/pwndbg/commands/xinfo.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import argparse +import subprocess + +import gdb + +import pwndbg.arch +import pwndbg.color.memory as M +import pwndbg.commands +import pwndbg.config +import pwndbg.memory +import pwndbg.regs +import pwndbg.stack +import pwndbg.vmmap +import pwndbg.wrappers + +parser = argparse.ArgumentParser(description='Shows offsets of the specified address to useful other locations') +parser.add_argument('address', nargs='?', default='$pc', + help='Address to inspect') + +def print_line(name, addr, first, second, op, width = 20): + + print("{} {} = {} {} {:#x}".format(name.rjust(width), M.get(addr), + M.get(first) if not isinstance(first, str) else first.ljust(len(hex(addr).rstrip('L'))), + op, second,)) + +def xinfo_stack(page, addr): + # If it's a stack address, print offsets to top and bottom of stack, as + # well as offsets to current stack and base pointer (if used by debugee) + + sp = pwndbg.regs.sp + frame = pwndbg.regs[pwndbg.regs.frame] + frame_mapping = pwndbg.vmmap.find(frame) + + print_line("Stack Top", addr, page.vaddr, addr - page.vaddr, "+") + print_line("Stack End", addr, page.end, page.end - addr, "-") + print_line("Stack Pointer", addr, sp, addr - sp, "+") + + if frame_mapping and page.vaddr == frame_mapping.vaddr: + print_line("Frame Pointer", addr, frame, frame - addr, "-") + + canary_value = pwndbg.commands.canary.canary_value()[0] + + if canary_value is not None: + all_canaries = list( + pwndbg.search.search(pwndbg.arch.pack(canary_value), mappings=pwndbg.stack.stacks.values()) + ) + follow_canaries = sorted(filter(lambda a: a > addr, all_canaries)) + if follow_canaries is not None and len(follow_canaries) > 0: + nxt = follow_canaries[0] + print_line("Next Stack Canary", addr, nxt, nxt - addr, "-") + +def xinfo_mmap_file(page, addr): + # If it's an address pointing into a memory mapped file, print offsets + # to beginning of file in memory and on disk + + file_name = page.objfile + objpages = filter(lambda p: p.objfile == file_name, pwndbg.vmmap.get()) + first = sorted(objpages, key = lambda p: p.vaddr)[0] + + # print offset from ELF base load address + rva = addr - first.vaddr + print_line("File (Base)", addr, first.vaddr, rva, "+") + + # find possible LOAD segments that designate memory and file backings + containing_loads = [seg for seg in pwndbg.elf.get_containing_segments(file_name, first.vaddr, addr) + if seg['p_type'] == 'PT_LOAD'] + + for segment in containing_loads: + if segment['p_type'] == 'PT_LOAD' and addr < segment['x_vaddr_mem_end']: + offset = addr - segment['p_vaddr'] + print_line('File (Segment)', addr, segment['p_vaddr'], offset, '+') + break + + for segment in containing_loads: + if segment['p_type'] == 'PT_LOAD' and addr < segment['x_vaddr_file_end']: + file_offset = segment['p_offset'] + (addr - segment['p_vaddr']) + print_line("File (Disk)", addr, file_name, file_offset, "+") + break + else: + print('{} {} = [not file backed]'.format('File (Disk)'.rjust(20), M.get(addr))) + + containing_sections = pwndbg.elf.get_containing_sections(file_name, first.vaddr, addr) + if len(containing_sections) > 0: + print('\n Containing ELF sections:') + for sec in containing_sections: + print_line(sec['x_name'], addr, sec['sh_addr'], addr - sec['sh_addr'], '+') + + +def xinfo_default(page, addr): + # Just print the distance to the beginning of the mapping + print_line("Mapped Area", addr, page.vaddr, addr - page.vaddr, "+") + + +@pwndbg.commands.ArgparsedCommand(parser) +@pwndbg.commands.OnlyWhenRunning +def xinfo(address=None): + addr = int(address) + addr &= pwndbg.arch.ptrmask + + page = pwndbg.vmmap.find(addr) + + if page is None: + print("\n Virtual address {:#x} is not mapped.".format(addr)) + return + + print("Extended information for virtual address {}:".format(M.get(addr))) + + print("\n Containing mapping:") + print(M.get(address, text=str(page))) + + print("\n Offset information:") + + if page.is_stack: + xinfo_stack(page, addr) + else: + xinfo_default(page, addr) + + if page.is_memory_mapped_file: + xinfo_mmap_file(page, addr) diff --git a/pwndbg/compat.py b/pwndbg/compat.py deleted file mode 100644 index 1b112946f7..0000000000 --- a/pwndbg/compat.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Compatibility functionality, for determining whether we are -running under Python2 or Python3, and resolving any -inconsistencies which arise from this. -""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -import sys - -# Quickly determine which version is running -python2 = sys.version_info.major == 2 -python3 = sys.version_info.major == 3 - -if python3: - basestring = str -else: - basestring = basestring diff --git a/pwndbg/config.py b/pwndbg/config.py index eb460dd42e..85bf88ceb9 100644 --- a/pwndbg/config.py +++ b/pwndbg/config.py @@ -22,13 +22,18 @@ from __future__ import print_function from __future__ import unicode_literals +import codecs import collections +import re import sys import types +from functools import total_ordering import gdb import six +import pwndbg.decorators + TYPES = collections.OrderedDict() # The value is a plain boolean. @@ -37,15 +42,15 @@ # The value is an integer. # This is like PARAM_INTEGER, except 0 is interpreted as itself. -for type in six.integer_types: - TYPES[type] = gdb.PARAM_ZINTEGER +for typ in six.integer_types: + TYPES[typ] = gdb.PARAM_ZINTEGER # The value is a string. # When the user modifies the string, any escape sequences, # such as ‘\t’, ‘\f’, and octal escapes, are translated into # corresponding characters and encoded into the current host charset. -for type in six.string_types: - TYPES[type] = gdb.PARAM_STRING +for typ in six.string_types: + TYPES[typ] = gdb.PARAM_STRING triggers = collections.defaultdict(lambda: []) @@ -86,7 +91,25 @@ def value_to_gdb_native(value): return value +member_remap = { + 'value': '_value', + 'raw_value': 'value' +} +@total_ordering class Parameter(gdb.Parameter): + """ + For python2, we can not store unicode type in self.value since the implementation limitation of gdb python. + We use self._value as the converted cache and set __getattribute__() and __setattr__() to remap variables. + + Since GDB will set gdb.Parameter.value to user input and call get_set_string(), + we use self.raw_value to map back to gdb.Parameter.value + + That is, we remap + * Parameter.value -> gdb.Parameter._value (if it is string type, always keep unicode) + All getter return this + * Parameter.raw_value -> gdb.Parameter.value + Only used in get_set_string() + """ def __init__(self, name, default, docstring, scope='config'): self.docstring = docstring.strip() self.optname = name @@ -113,16 +136,56 @@ def native_default(self): def is_changed(self): return self.value != self.default + def __setattr__(self, name, value): + new_name = member_remap.get(name, name) + new_name = str(new_name) # Python2 only accept str type as key + return super(Parameter, self).__setattr__(new_name, value) + + def __getattribute__(self, name): + new_name = member_remap.get(name, name) + new_name = str(new_name) # Python2 only accept str type as key + return super(Parameter, self).__getattribute__(new_name) + def get_set_string(self): + value = self.raw_value + + # For string value, convert utf8 byte string to unicode. + if isinstance(value, six.binary_type): + value = codecs.decode(value, 'utf-8') + + # Remove surrounded ' and " characters + if isinstance(value, six.string_types): + # The first character must be ' or " and ends with the same character. + # See PR #404 for more information + pattern = r"^(?P[\"'])(?P.*?)(?P=quote)$" + + value = re.sub(pattern, r"\g", value) + + # Write back to self.value + self.value = value + for trigger in triggers[self.name]: trigger() - if isinstance(self.value, str): - self.value = self.value.replace("'", '').replace('"', '') + + if not pwndbg.decorators.first_prompt: + # Remove the newline that gdb adds automatically + return '\b' return 'Set %s to %r' % (self.docstring, self.value) def get_show_string(self, svalue): return 'Sets %s (currently: %r)' % (self.docstring, self.value) + def revert_default(self): + self.value = self.default + + # TODO: use __getattribute__ to remapping all member function to self.value's member? + # Then, we can use param.member() just like param.value.member() + + # The str type member function, used in color/__init__.py + def split(self, *args, **kargs): + return str(self).split(*args, **kargs) + + # Casting def __int__(self): return int(self.value) @@ -132,23 +195,46 @@ def __str__(self): def __bool__(self): return bool(self.value) + # Compare operators + # Ref: http://portingguide.readthedocs.io/en/latest/comparisons.html + # If other is Parameter, comparing by optname. Used in `sorted` in `config` command. + # Otherwise, compare `self.value` with `other` + def __eq__(self, other): + if isinstance(other, gdb.Parameter): + return self.optname == other.optname + else: + return self.value == other + def __lt__(self, other): - return self.optname <= other.optname + if isinstance(other, gdb.Parameter): + return self.optname < other.optname + else: + return self.value < other - def __div__(self, other): - return self.value / other + # Operators + def __add__(self, other): + return self.value + other - def __floordiv__(self, other): - return self.value // other + def __radd__(self, other): + return other + self.value + + def __sub__(self, other): + return self.value - other + + def __rsub__(self, other): + return other - self.value def __mul__(self, other): return self.value * other - def __sub__(self, other): - return self.value - other + def __rmul__(self, other): + return other * self.value - def __add__(self, other): - return self.value + other + def __div__(self, other): + return self.value / other + + def __floordiv__(self, other): + return self.value // other def __pow__(self, other): return self.value ** other @@ -156,6 +242,9 @@ def __pow__(self, other): def __mod__(self, other): return self.value % other + def __len__(self): + return len(self.value) + # Python2 compatibility __nonzero__ = __bool__ diff --git a/pwndbg/decorators.py b/pwndbg/decorators.py new file mode 100644 index 0000000000..ab1643bd8d --- /dev/null +++ b/pwndbg/decorators.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import functools + +first_prompt = False + + +def only_after_first_prompt(value_before=None): + """ + Decorator to prevent a function from running before the first prompt was displayed. + The 'value_before' parameter can be used to specify the value that is + returned if the function is called before the first prompt was displayed. + """ + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + if first_prompt: + return func(*args, **kwargs) + else: + return value_before + return wrapper + return decorator diff --git a/pwndbg/disasm/__init__.py b/pwndbg/disasm/__init__.py index 64686b461e..74d7ef094c 100644 --- a/pwndbg/disasm/__init__.py +++ b/pwndbg/disasm/__init__.py @@ -144,13 +144,20 @@ def get(address, instructions=1): capstone.CS_GRP_INVALID, capstone.CS_GRP_IRET, -# Note that we explicitly do not include the PRIVILEGE category, since -# we may be in kernel code, and privileged instructions are just fine -# in that case. -# capstone.CS_GRP_PRIVILEGE, + # Note that we explicitly do not include the PRIVILEGE category, since + # we may be in kernel code, and privileged instructions are just fine + # in that case. + #capstone.CS_GRP_PRIVILEGE, } -def near(address, instructions=1, emulate=False): + +def near(address, instructions=1, emulate=False, show_prev_insns=True): + """ + Disasms instructions near given `address`. Passing `emulate` makes use of + unicorn engine to emulate instructions to predict branches that will be taken. + `show_prev_insns` makes this show previously cached instructions + (this is mostly used by context's disasm display, so user see what was previously) + """ current = one(address) @@ -159,17 +166,18 @@ def near(address, instructions=1, emulate=False): if current is None or not pwndbg.memory.peek(address): return [] + insns = [] + # Try to go backward by seeing which instructions we've returned # before, which were followed by this one. - needle = address - insns = [] - cached = backward_cache[current.address] - insn = one(cached) if cached else None - while insn is not None and len(insns) < instructions: - insns.append(insn) - cached = backward_cache[insn.address] - insn = one(cached) if cached else None - insns.reverse() + if show_prev_insns: + cached = backward_cache[current.address] + insn = one(cached) if cached else None + while insn is not None and len(insns) < instructions: + insns.append(insn) + cached = backward_cache[insn.address] + insn = one(cached) if cached else None + insns.reverse() insns.append(current) @@ -192,8 +200,8 @@ def near(address, instructions=1, emulate=False): # # At this point, we've already added everything *BEFORE* the requested address, # and the instruction at 'address'. - insn = current - total_instructions = 1+(2*instructions) + insn = current + total_instructions = 1 + (2*instructions) while insn and len(insns) < total_instructions: target = insn.target @@ -201,26 +209,20 @@ def near(address, instructions=1, emulate=False): # Disable emulation if necessary if emulate and set(insn.groups) & DO_NOT_EMULATE: emulate = False - emu = None - - # Continue disassembling after a RET or JUMP, but don't follow through CALL. - if capstone.CS_GRP_CALL in insn.groups: - target = insn.next + emu = None # If we initialized the emulator and emulation is still enabled, we can use it # to figure out the next instruction. - elif emu: + if emu: target_candidate, size_candidate = emu.single_step() if None not in (target_candidate, size_candidate): target = target_candidate - size = size_candidate # Continue disassembling at the *next* instruction unless we have emulated # the path of execution. elif target != pc: - target = insn.next - + target = insn.address + insn.size insn = one(target) if insn: @@ -231,7 +233,14 @@ def near(address, instructions=1, emulate=False): # but any repeats after that are removed. # # This helps with infinite loops and RET sleds. - while insns and len(insns) > 2 and len(set(insns[-3:])) == 1: + while insns and len(insns) > 2 and insns[-3].address == insns[-2].address == insns[-1].address: del insns[-1] return insns + + +def is_call(address=None): + """ + Returns whether a given address contains call instruction. + """ + return capstone.CS_GRP_CALL in one(address).groups diff --git a/pwndbg/disasm/arch.py b/pwndbg/disasm/arch.py index 1422cb6c7c..47fd4ae06a 100644 --- a/pwndbg/disasm/arch.py +++ b/pwndbg/disasm/arch.py @@ -143,8 +143,11 @@ def next(self, instruction, call=False): addr &= pwndbg.arch.ptrmask if op.type == CS_OP_MEM: if addr is None: - addr = self.memory_sz(instruction, op) - addr = int(pwndbg.memory.poi(pwndbg.typeinfo.ppvoid, addr)) + addr = self.memory(instruction, op) + + # self.memory may return none, so we need to check it here again + if addr is not None: + addr = int(pwndbg.memory.poi(pwndbg.typeinfo.ppvoid, addr)) if op.type == CS_OP_REG: addr = self.register(instruction, op) diff --git a/pwndbg/dt.py b/pwndbg/dt.py index 71506183a4..408e6058b8 100644 --- a/pwndbg/dt.py +++ b/pwndbg/dt.py @@ -112,7 +112,8 @@ def dt(name='', addr=None, obj = None): for name, field in t.items(): # Offset into the parent structure - o = getattr(field, 'bitpos', 0)/8 + o = getattr(field, 'bitpos', 0) // 8 + b = getattr(field, 'bitpos', 0) % 8 extra = str(field.type) ftype = field.type.strip_typedefs() @@ -137,7 +138,9 @@ def dt(name='', addr=None, obj = None): else: extra_lines.append(35*' ' + line) extra = '\n'.join(extra_lines) - line = " +0x%04x %-20s : %s" % (o, name, extra) + bitpos = '' if not b else ('.%i' % b) + + line = " +0x%04x%s %-20s : %s" % (o, bitpos, name, extra) rv.append(line) return ('\n'.join(rv)) diff --git a/pwndbg/elf.py b/pwndbg/elf.py index 6539b51038..9fac6985b8 100644 --- a/pwndbg/elf.py +++ b/pwndbg/elf.py @@ -14,10 +14,15 @@ import ctypes import sys +from collections import namedtuple import gdb +from elftools.elf.constants import SH_FLAGS +from elftools.elf.elffile import ELFFile from six.moves import reload_module +import pwndbg.abi +import pwndbg.arch import pwndbg.auxv import pwndbg.elftypes import pwndbg.events @@ -35,6 +40,19 @@ module = sys.modules[__name__] +class ELFInfo(namedtuple('ELFInfo', 'header sections segments')): + """ + ELF metadata and structures. + """ + @property + def is_pic(self): + return self.header['e_type'] == 'ET_DYN' + + @property + def is_pie(self): + return self.is_pic + + @pwndbg.events.start @pwndbg.events.new_objfile def update(): @@ -65,6 +83,96 @@ def read(typ, address, blob=None): obj.type = typ return obj + +@pwndbg.memoize.reset_on_objfile +def get_elf_info(filepath): + """ + Parse and return ELFInfo. + + Adds various calculated properties to the ELF header, segments and sections. + Such added properties are those with prefix 'x_' in the returned dicts. + """ + local_path = pwndbg.file.get_file(filepath) + with open(local_path, 'rb') as f: + elffile = ELFFile(f) + header = dict(elffile.header) + segments = [] + for seg in elffile.iter_segments(): + s = dict(seg.header) + s['x_perms'] = [ + mnemonic for mask, mnemonic in [(PF_R, 'read'), (PF_W, 'write'), (PF_X, 'execute')] + if s['p_flags'] & mask != 0 + ] + # end of memory backing + s['x_vaddr_mem_end'] = s['p_vaddr'] + s['p_memsz'] + # end of file backing + s['x_vaddr_file_end'] = s['p_vaddr'] + s['p_filesz'] + segments.append(s) + sections = [] + for sec in elffile.iter_sections(): + s = dict(sec.header) + s['x_name'] = sec.name + s['x_addr_mem_end'] = s['x_addr_file_end'] = s['sh_addr'] + s['sh_size'] + sections.append(s) + return ELFInfo(header, sections, segments) + + +@pwndbg.memoize.reset_on_objfile +def get_elf_info_rebased(filepath, vaddr): + """ + Parse and return ELFInfo with all virtual addresses rebased to vaddr + """ + raw_info = get_elf_info(filepath) + # silently ignores "wrong" vaddr supplied for non-PIE ELF + load = vaddr if raw_info.is_pic else 0 + headers = dict(raw_info.header) + headers['e_entry'] += load + + segments = [] + for seg in raw_info.segments: + s = dict(seg) + for vaddr_attr in ['p_vaddr', 'x_vaddr_mem_end', 'x_vaddr_file_end']: + s[vaddr_attr] += load + segments.append(s) + + sections = [] + for sec in raw_info.sections: + s = dict(sec) + for vaddr_attr in ['sh_addr', 'x_addr_mem_end', 'x_addr_file_end']: + s[vaddr_attr] += load + sections.append(s) + + return ELFInfo(headers, sections, segments) + + +def get_containing_segments(elf_filepath, elf_loadaddr, vaddr): + elf = get_elf_info_rebased(elf_filepath, elf_loadaddr) + segments = [] + for seg in elf.segments: + # disregard non-LOAD segments that are not file-backed (typically STACK) + if 'LOAD' not in seg['p_type'] and seg['p_filesz'] == 0: + continue + # disregard segments not containing vaddr + if vaddr < seg['p_vaddr'] or vaddr >= seg['x_vaddr_mem_end']: + continue + segments.append(dict(seg)) + return segments + + +def get_containing_sections(elf_filepath, elf_loadaddr, vaddr): + elf = get_elf_info_rebased(elf_filepath, elf_loadaddr) + sections = [] + for sec in elf.sections: + # disregard sections not occupying memory + if sec['sh_flags'] & SH_FLAGS.SHF_ALLOC == 0: + continue + # disregard sections that do not contain vaddr + if vaddr < sec['sh_addr'] or vaddr >= sec['x_addr_mem_end']: + continue + sections.append(dict(sec)) + return sections + + @pwndbg.proc.OnlyWhenRunning @pwndbg.memoize.reset_on_start def exe(): @@ -76,6 +184,7 @@ def exe(): if e: return load(e) + @pwndbg.proc.OnlyWhenRunning @pwndbg.memoize.reset_on_start def entry(): @@ -115,11 +224,53 @@ def load(pointer): ehdr_type_loaded = 0 + @pwndbg.memoize.reset_on_start def reset_ehdr_type_loaded(): global ehdr_type_loaded ehdr_type_loaded = 0 + +@pwndbg.abi.LinuxOnly() +def find_elf_magic(pointer, max_pages=1024, search_down=False, ret_addr_anyway=False): + """Search the nearest page which contains the ELF headers + by comparing the ELF magic with first 4 bytes. + + Parameter: + search_down: change the search direction + to search over the lower address. + That is, decreasing the page pointer instead of increasing. + (default: False) + Returns: + An integer address of ELF page base + None if not found within the page limit + """ + addr = pwndbg.memory.page_align(pointer) + step = pwndbg.memory.PAGE_SIZE + if search_down: + step = -step + + max_addr = pwndbg.arch.ptrmask + + for i in range(max_pages): + # Make sure address within valid range or gdb will raise Overflow exception + if addr < 0 or addr > max_addr: + return None + + try: + data = pwndbg.memory.read(addr, 4) + except gdb.MemoryError: + return addr if ret_addr_anyway else None + + # Return the address if found ELF header + if data == b'\x7FELF': + return addr + + addr += step + + return addr if ret_addr_anyway else None + + def get_ehdr(pointer): """Returns an ehdr object for the ELF pointer points into. """ @@ -127,21 +278,12 @@ def get_ehdr(pointer): # the ELF header. base = pwndbg.memory.page_align(pointer) - try: - data = pwndbg.memory.read(base, 4) - - # Do not search more than 4MB of memory - for i in range(1024): - if data == b'\x7FELF': - break - - base -= pwndbg.memory.PAGE_SIZE - data = pwndbg.memory.read(base, 4) - - else: + # For non linux ABI, the ELF header may not be found in memory. + # This will hang the gdb when using the remote gdbserver to scan 1024 pages + base = find_elf_magic(pointer, search_down=True) + if base is None: + if pwndbg.abi.linux: print("ERROR: Could not find ELF base!") - return None, None - except gdb.MemoryError: return None, None # Determine whether it's 32- or 64-bit @@ -151,6 +293,7 @@ def get_ehdr(pointer): Elfhdr = read(Ehdr, base) return ei_class, Elfhdr + def get_phdrs(pointer): """ Returns a tuple containing (phnum, phentsize, gdb.Value), @@ -169,6 +312,7 @@ def get_phdrs(pointer): x = (phnum, phentsize, read(Phdr, Elfhdr.address + phoff)) return x + def iter_phdrs(ehdr): if not ehdr: raise StopIteration @@ -186,6 +330,7 @@ def iter_phdrs(ehdr): p_phdr = read(PhdrType, p_phdr) yield p_phdr + def map(pointer, objfile=''): """ Given a pointer into an ELF module, return a list of all loaded @@ -209,6 +354,7 @@ def map(pointer, objfile=''): ei_class, ehdr = get_ehdr(pointer) return map_inner(ei_class, ehdr, objfile) + def map_inner(ei_class, ehdr, objfile): if not ehdr: return [] diff --git a/pwndbg/emu/emulator.py b/pwndbg/emu/emulator.py index 54632cb0a3..2b12acdf22 100644 --- a/pwndbg/emu/emulator.py +++ b/pwndbg/emu/emulator.py @@ -9,7 +9,7 @@ from __future__ import unicode_literals import binascii -import inspect +import re import capstone as C import gdb @@ -21,6 +21,19 @@ import pwndbg.memory import pwndbg.regs + +def parse_consts(u_consts): + """ + Unicorn "consts" is a python module consisting of a variable definition + for each known entity. We repack it here as a dict for performance. + """ + consts = {} + for name in dir(u_consts): + if name.startswith('UC_'): + consts[name] = getattr(u_consts, name) + return consts + + # Map our internal architecture names onto Unicorn Engine's architecture types. arch_to_UC = { 'i386': U.UC_ARCH_X86, @@ -33,12 +46,12 @@ } arch_to_UC_consts = { - 'i386': U.x86_const, - 'x86-64': U.x86_const, - 'mips': U.mips_const, - 'sparc': U.sparc_const, - 'arm': U.arm_const, - 'aarch64': U.arm64_const, + 'i386': parse_consts(U.x86_const), + 'x86-64': parse_consts(U.x86_const), + 'mips': parse_consts(U.mips_const), + 'sparc': parse_consts(U.sparc_const), + 'arm': parse_consts(U.arm_const), + 'aarch64': parse_consts(U.arm64_const), } # Map our internal architecture names onto Unicorn Engine's architecture types. @@ -54,6 +67,7 @@ DEBUG = False + def debug(*a,**kw): if DEBUG: print(*a, **kw) @@ -98,6 +112,7 @@ def debug(*a,**kw): e.until_jump() ''' + class Emulator(object): def __init__(self): self.arch = pwndbg.arch.current @@ -107,6 +122,14 @@ def __init__(self): self.consts = arch_to_UC_consts[self.arch] + # Just registers, for faster lookup + self.const_regs = {} + r = re.compile(r'^UC_.*_REG_(.*)$') + for k,v in self.consts.items(): + m = r.match(k) + if m: + self.const_regs[m.group(1)] = v + self.uc_mode = self.get_uc_mode() debug("# Instantiating Unicorn for %s" % self.arch) debug("uc = U.Uc(%r, %r)" % (arch_to_UC[self.arch], self.uc_mode)) @@ -115,7 +138,7 @@ def __init__(self): # Jump tracking state self._prev = None - self._prevsize = None + self._prev_size = None self._curr = None # Initialize the register state @@ -156,7 +179,6 @@ def __init__(self): if DEBUG: self.hook_add(U.UC_HOOK_CODE, self.trace_hook) - def __getattr__(self, name): reg = self.get_reg_enum(name) @@ -247,11 +269,6 @@ def get_reg_enum(self, reg): Also supports general registers like 'sp' and 'pc'. """ - if 'fsbase' in reg: - # import pdb - # pdb.set_trace() - pass - if not self.regs: return None @@ -261,8 +278,9 @@ def get_reg_enum(self, reg): # 'eax' ==> enum # if reg in self.regs.all: - for reg_enum in (c for c in dir(self.consts) if c.endswith('_' + reg.upper())): - return getattr(self.consts, reg_enum) + e = self.const_regs.get(reg.upper(), None) + if e is not None: + return e # If we're looking for an abstract register which *is* accounted for, # we can also do an indirect lookup. @@ -308,9 +326,9 @@ def emulate_with_hook(self, hook, count=512): def mem_read(self, *a, **kw): debug("uc.mem_read(*%r, **%r)" % (a, kw)) - return self.uc.mem_read(*a,**kw) + return self.uc.mem_read(*a, **kw) - jump_types = set([C.CS_GRP_CALL, C.CS_GRP_JUMP, C.CS_GRP_RET]) + jump_types = {C.CS_GRP_CALL, C.CS_GRP_JUMP, C.CS_GRP_RET} def until_jump(self, pc=None): """ @@ -340,7 +358,7 @@ def until_jump(self, pc=None): # Set up the state. Resetting this each time means that we will not ever # stop on the *current* instruction. self._prev = None - self._prevsize = None + self._prev_size = None self._curr = None # Add the single-step hook, start emulating, and remove the hook. @@ -349,26 +367,26 @@ def until_jump(self, pc=None): # We're done emulating return self._prev, self._curr - def until_jump_hook_code(self, uc, address, size, user_data): + def until_jump_hook_code(self, _uc, address, instruction_size, _user_data): # We have not emulated any instructions yet. if self._prev is None: pass # We have moved forward one linear instruction, no branch or the # branch target was the next instruction. - elif self._prev + self._prevsize == address: + elif self._prev + self._prev_size == address: pass # We have branched! # The previous instruction does not immediately precede this one. else: self._curr = address - debug(hex(self._prev), hex(self._prevsize), '-->', hex(self._curr)) + debug(hex(self._prev), hex(self._prev_size), '-->', hex(self._curr)) self.emu_stop() return self._prev = address - self._prevsize = size + self._prev_size = instruction_size def until_call(self, pc=None): addr, target = self.until_jump(pc) @@ -401,7 +419,7 @@ def single_step(self, pc=None): A StopIteration is raised if a fault or syscall or call instruction is encountered. """ - self._singlestep = (None, None) + self._single_step = (None, None) pc = pc or self.pc insn = pwndbg.disasm.one(pc) @@ -409,16 +427,16 @@ def single_step(self, pc=None): # If we don't know how to disassemble, bail. if insn is None: debug("Can't disassemble instruction at %#x" % pc) - return self._singlestep + return self._single_step debug("# Single-stepping at %#x: %s %s" % (pc, insn.mnemonic, insn.op_str)) try: self.emulate_with_hook(self.single_step_hook_code, count=1) - except U.unicorn.UcError: - self._singlestep = (None, None) + except U.unicorn.UcError as e: + self._single_step = (None, None) - return self._singlestep + return self._single_step def single_step_iter(self, pc=None): a = self.single_step(pc) @@ -427,9 +445,9 @@ def single_step_iter(self, pc=None): yield a a = self.single_step(pc) - def single_step_hook_code(self, uc, address, size, user_data): + def single_step_hook_code(self, _uc, address, instruction_size, _user_data): debug("# single_step: %#-8x" % address) - self._singlestep = (address, size) + self._single_step = (address, instruction_size) def dumpregs(self): for reg in list(self.regs.misc) + list(self.regs.common) + list(self.regs.flags): @@ -443,6 +461,6 @@ def dumpregs(self): value = self.uc.reg_read(enum) debug("uc.reg_read(%(name)s) ==> %(value)x" % locals()) - def trace_hook(self, uc, address, size, user_data): - data = binascii.hexlify(self.mem_read(address, size)) + def trace_hook(self, _uc, address, instruction_size, _user_data): + data = binascii.hexlify(self.mem_read(address, instruction_size)) debug("# trace_hook: %#-8x %r" % (address, data)) diff --git a/pwndbg/enhance.py b/pwndbg/enhance.py index c554659ec6..77ea14e34a 100644 --- a/pwndbg/enhance.py +++ b/pwndbg/enhance.py @@ -20,12 +20,14 @@ import pwndbg.arch import pwndbg.color as color import pwndbg.color.enhance as E +import pwndbg.config import pwndbg.disasm import pwndbg.memoize import pwndbg.memory import pwndbg.strings import pwndbg.symbol import pwndbg.typeinfo +from pwndbg.color.syntax_highlight import syntax_highlight bad_instrs = [ '.byte', @@ -100,6 +102,8 @@ def enhance(value, code = True): instr = pwndbg.disasm.one(value) if instr: instr = "%-6s %s" % (instr.mnemonic, instr.op_str) + if pwndbg.config.syntax_highlight: + instr = syntax_highlight(instr) szval = pwndbg.strings.get(value) or None szval0 = szval diff --git a/pwndbg/events.py b/pwndbg/events.py index afdfaeeaff..c5c7eae873 100644 --- a/pwndbg/events.py +++ b/pwndbg/events.py @@ -10,8 +10,9 @@ from __future__ import print_function from __future__ import unicode_literals -import functools import sys +from functools import partial +from functools import wraps import gdb @@ -38,12 +39,15 @@ class StartEvent(object): def __init__(self): self.registered = list() self.running = False + def connect(self, function): if function not in self.registered: self.registered.append(function) + def disconnect(self, function): if function in self.registered: self.registered.remove(function) + def on_new_objfile(self): if self.running or not gdb.selected_thread(): return @@ -61,15 +65,56 @@ def on_exited(self): def on_stop(self): self.on_new_objfile() + gdb.events.start = StartEvent() + +class EventWrapper(object): + """ + Wraper for GDB events which may not exist on older GDB versions but we still can + fire them manually (to invoke them you have to call `invoke_callbacks`). + """ + def __init__(self, name): + self.name = name + + self._event = getattr(gdb.events, self.name, None) + self._is_real_event = self._event is not None + + def connect(self, func): + if self._event is not None: + self._event.connect(func) + + def disconnect(self, func): + if self._event is not None: + self._event.disconnect(func) + + @property + def is_real_event(self): + return self._is_real_event + + def invoke_callbacks(self): + """ + As an optimization please don't call this if your GDB has this event (check `.is_real_event`). + """ + for f in registered[self]: + f() + + +# Old GDBs doesn't have gdb.events.before_prompt, so we will emulate it using gdb.prompt_hook +before_prompt_event = EventWrapper('before_prompt') +gdb.events.before_prompt = before_prompt_event + + # In order to support reloading, we must be able to re-fire # all 'objfile' and 'stop' events. -registered = {gdb.events.exited: [], - gdb.events.cont: [], - gdb.events.new_objfile: [], - gdb.events.stop: [], - gdb.events.start: []} +registered = { + gdb.events.exited: [], + gdb.events.cont: [], + gdb.events.new_objfile: [], + gdb.events.stop: [], + gdb.events.start: [], + gdb.events.before_prompt: [] # The real event might not exist, but we wrap it +} # GDB 7.9 and above only try: @@ -78,39 +123,44 @@ def on_stop(self): except (NameError, AttributeError): pass + class Pause(object): def __enter__(self, *a, **kw): global pause pause += 1 + def __exit__(self, *a, **kw): global pause pause -= 1 + # When performing remote debugging, gdbserver is very noisy about which # objects are loaded. This greatly slows down the debugging session. # In order to combat this, we keep track of which objfiles have been loaded # this session, and only emit objfile events for each *new* file. -objfile_cache = set() +objfile_cache = dict() + def connect(func, event_handler, name=''): if debug: print("Connecting", func.__name__, event_handler) - @functools.wraps(func) + @wraps(func) def caller(*a): if debug: sys.stdout.write('%r %s.%s %r\n' % (name, func.__module__, func.__name__, a)) if a and isinstance(a[0], gdb.NewObjFileEvent): objfile = a[0].new_objfile + handler = '%s.%s' % (func.__module__, func.__name__) path = objfile.filename + dispatched = objfile_cache.get(path, set()) - if path in objfile_cache: + if handler in dispatched: return - # print(path, objfile.is_valid()) - - objfile_cache.add(path) + dispatched.add(handler) + objfile_cache[path] = dispatched if pause: return @@ -126,22 +176,31 @@ def caller(*a): event_handler.connect(caller) return func + def exit(func): return connect(func, gdb.events.exited, 'exit') def cont(func): return connect(func, gdb.events.cont, 'cont') def new_objfile(func): return connect(func, gdb.events.new_objfile, 'obj') def stop(func): return connect(func, gdb.events.stop, 'stop') def start(func): return connect(func, gdb.events.start, 'start') + + +before_prompt = partial(connect, event_handler=gdb.events.before_prompt, name='before_prompt') + + def reg_changed(func): try: return connect(func, gdb.events.register_changed, 'reg_changed') - except Exception: + except AttributeError: return func + + def mem_changed(func): try: return connect(func, gdb.events.memory_changed, 'mem_changed') - except Exception: + except AttributeError: return func + def log_objfiles(ofile=None): if not (debug and ofile): return @@ -151,8 +210,10 @@ def log_objfiles(ofile=None): print("objfile: %r" % name) gdb.execute('info sharedlibrary') + gdb.events.new_objfile.connect(log_objfiles) + def after_reload(start=True): if gdb.selected_inferior().pid: for f in registered[gdb.events.stop]: @@ -161,6 +222,9 @@ def after_reload(start=True): if start: f() for f in registered[gdb.events.new_objfile]: f() + for f in registered[gdb.events.before_prompt]: + f() + def on_reload(): for event, functions in registered.items(): @@ -168,19 +232,23 @@ def on_reload(): event.disconnect(function) registered[event] = [] + @new_objfile def _start_newobjfile(): gdb.events.start.on_new_objfile() + @exit def _start_exit(): gdb.events.start.on_exited() + @stop def _start_stop(): gdb.events.start.on_stop() + @exit def _reset_objfiles(): global objfile_cache - objfile_cache = set() + objfile_cache = dict() diff --git a/pwndbg/exception.py b/pwndbg/exception.py index c2928a7c18..4c593a8948 100644 --- a/pwndbg/exception.py +++ b/pwndbg/exception.py @@ -12,7 +12,7 @@ import gdb -import pwndbg.color +import pwndbg.color.message as message import pwndbg.config import pwndbg.memoize import pwndbg.stdio @@ -32,7 +32,7 @@ def inform_report_issue(exception_msg): Informs user that he can report an issue. The use of `memoize` makes it reporting only once for a given exception message. """ - print(pwndbg.color.purple( + print(message.notice( 'If that is an issue, you can report it on https://github.com/pwndbg/pwndbg/issues\n' "(Please don't forget to search if it hasn't been reported before)\n" "PS: Pull requests are welcome") @@ -47,6 +47,14 @@ def handle(name='Error'): - ``set exception-verbose on`` enables stack traces. - ``set exception-debugger on`` enables the post-mortem debugger. """ + + # This is for unit tests so they fail on exceptions instead of displaying them. + if getattr(sys, '_pwndbg_unittest_run', False) is True: + E, V, T = sys.exc_info() + e = E(V) + e.__traceback__ = T + raise e + # Display the error if debug or verbose: exception_msg = traceback.format_exc() @@ -56,11 +64,13 @@ def handle(name='Error'): else: exc_type, exc_value, exc_traceback = sys.exc_info() - print(pwndbg.color.red('Exception occured: {}: {} ({})'.format(name, exc_value, exc_type))) + print(message.error('Exception occured: {}: {} ({})'.format(name, exc_value, exc_type))) - print(pwndbg.color.purple('For more info invoke `') + - pwndbg.color.yellow('set exception-verbose on') + - pwndbg.color.purple('` and rerun the command')) + print(message.notice('For more info invoke `') + + message.hint('set exception-verbose on') + + message.notice('` and rerun the command\nor debug it by yourself with `') + + message.hint('set exception-debugger on') + + message.notice('`')) # Break into the interactive debugger if debug: diff --git a/pwndbg/gdbutils/__init__.py b/pwndbg/gdbutils/__init__.py new file mode 100644 index 0000000000..4b48c91a54 --- /dev/null +++ b/pwndbg/gdbutils/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +""" +Put all new things related to gdb in this module. +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import pwndbg.gdbutils.functions diff --git a/pwndbg/gdbutils/functions.py b/pwndbg/gdbutils/functions.py new file mode 100644 index 0000000000..48d04ed35c --- /dev/null +++ b/pwndbg/gdbutils/functions.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +""" +Put all functions defined for gdb in here. + +This file might be changed into a module in the future. +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import functools + +import gdb + +import pwndbg.proc + +functions = [] + + +def GdbFunction(only_when_running=False): + return functools.partial(_GdbFunction, only_when_running=only_when_running) + + +class _GdbFunction(gdb.Function): + def __init__(self, func, only_when_running): + self.name = func.__name__ + self.func = func + self.only_when_running = only_when_running + + functions.append(self) + + super(_GdbFunction, self).__init__(self.name) + + functools.update_wrapper(self, func) + self.__doc__ = func.__doc__ + + def invoke(self, *args): + if self.only_when_running and not pwndbg.proc.alive: + # Returning empty string is a workaround that we can't stop e.g. `break *$rebase(offset)` + # Thx to that, gdb will print out 'evaluation of this expression requires the target program to be active' + return '' + + return self.func(*args) + + def __call__(self, *args): + return self.invoke(*args) + + +@GdbFunction(only_when_running=True) +def rebase(addr): + """Return rebased address.""" + base = pwndbg.elf.exe().address + return base + int(addr) diff --git a/pwndbg/gitver.py b/pwndbg/gitver.py deleted file mode 100644 index 8e28b3152e..0000000000 --- a/pwndbg/gitver.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -import os -import subprocess - -file_path = os.path.dirname(__file__) -pwndbg_pwndbg = os.path.abspath(file_path) -pwndbg = os.path.dirname(pwndbg_pwndbg) -capstone = os.path.join(pwndbg, 'capstone') -unicorn = os.path.join(pwndbg, 'unicorn') - -def get_hash(directory): - argv = ['git', '-C', directory, 'describe', '--always'] - return subprocess.check_output(argv).strip() - -hashes = { - 'capstone': get_hash(capstone), - 'unicorn': get_hash(unicorn), - 'pwndbg': get_hash(pwndbg) -} - -if __name__ == '__main__': - print(hashes) diff --git a/pwndbg/heap/__init__.py b/pwndbg/heap/__init__.py index 63f9f50cc4..14476295c1 100644 --- a/pwndbg/heap/__init__.py +++ b/pwndbg/heap/__init__.py @@ -10,6 +10,8 @@ current = None +heap_chain_limit = pwndbg.config.Parameter('heap-dereference-limit', 8, 'number of bins to dereference') + @pwndbg.events.new_objfile def update(): import pwndbg.heap.dlmalloc diff --git a/pwndbg/heap/heap.py b/pwndbg/heap/heap.py index 89f02debef..240ff710a6 100644 --- a/pwndbg/heap/heap.py +++ b/pwndbg/heap/heap.py @@ -44,3 +44,12 @@ def containing(address): An integer. """ raise NotImplementedError() + + + def is_initialized(self): + """Returns whether the allocator is initialized or not. + + Returns: + A boolean. + """ + raise NotImplementedError() diff --git a/pwndbg/heap/libheap.py b/pwndbg/heap/libheap.py deleted file mode 100644 index 88d3459c68..0000000000 --- a/pwndbg/heap/libheap.py +++ /dev/null @@ -1,1878 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -The MIT License (MIT) - -Copyright (c) 2015 cloudburst - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -import struct -import sys -from os import uname - -import gdb - -# bash color support -color_support = True -if color_support: - c_red = "\033[31m" - c_red_b = "\033[01;31m" - c_green = "\033[32m" - c_green_b = "\033[01;32m" - c_yellow = "\033[33m" - c_yellow_b = "\033[01;33m" - c_blue = "\033[34m" - c_blue_b = "\033[01;34m" - c_purple = "\033[35m" - c_purple_b = "\033[01;35m" - c_teal = "\033[36m" - c_teal_b = "\033[01;36m" - c_none = "\033[0m" -else: - c_red = "" - c_red_b = "" - c_green = "" - c_green_b = "" - c_yellow = "" - c_yellow_b = "" - c_blue = "" - c_blue_b = "" - c_purple = "" - c_purple_b = "" - c_teal = "" - c_teal_b = "" - c_none = "" -c_error = c_red -c_title = c_green_b -c_header = c_yellow_b -c_value = c_blue_b - -################################################################################ -# MALLOC CONSTANTS AND MACROS -################################################################################ - -_machine = uname()[4] -if _machine == "x86_64": - SIZE_SZ = 8 -elif _machine in ("i386", "i686"): - SIZE_SZ = 4 - -MIN_CHUNK_SIZE = 4 * SIZE_SZ -MALLOC_ALIGNMENT = 2 * SIZE_SZ -MALLOC_ALIGN_MASK = MALLOC_ALIGNMENT - 1 -MINSIZE = (MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK - -def chunk2mem(p): - "conversion from malloc header to user pointer" - return (p.address + (2*SIZE_SZ)) - -def mem2chunk(mem): - "conversion from user pointer to malloc header" - return (mem - (2*SIZE_SZ)) - -def request2size(req): - "pad request bytes into a usable size" - - if (req + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE): - return MINSIZE - else: - return (int(req + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK) - -PREV_INUSE = 1 -IS_MMAPPED = 2 -NON_MAIN_ARENA = 4 -SIZE_BITS = (PREV_INUSE|IS_MMAPPED|NON_MAIN_ARENA) - -def prev_inuse(p): - "extract inuse bit of previous chunk" - return (p.size & PREV_INUSE) - -def chunk_is_mmapped(p): - "check for mmap()'ed chunk" - return (p.size & IS_MMAPPED) - -def chunk_non_main_arena(p): - "check for chunk from non-main arena" - return (p.size & NON_MAIN_ARENA) - -def chunksize(p): - "Get size, ignoring use bits" - return (p.size & ~SIZE_BITS) - -def next_chunk(p): - "Ptr to next physical malloc_chunk." - return (p.address + (p.size & ~SIZE_BITS)) - -def prev_chunk(p): - "Ptr to previous physical malloc_chunk" - return (p.address - p.prev_size) - -def chunk_at_offset(p, s): - "Treat space at ptr + offset as a chunk" - return malloc_chunk(p.address + s, inuse=False) - -def inuse(p): - "extract p's inuse bit" - return (malloc_chunk(p.address + \ - (p.size & ~SIZE_BITS), inuse=False).size & PREV_INUSE) - -def set_inuse(p): - "set chunk as being inuse without otherwise disturbing" - chunk = malloc_chunk((p.address + (p.size & ~SIZE_BITS)), inuse=False) - chunk.size |= PREV_INUSE - chunk.write() - -def clear_inuse(p): - "clear chunk as being inuse without otherwise disturbing" - chunk = malloc_chunk((p.address + (p.size & ~SIZE_BITS)), inuse=False) - chunk.size &= ~PREV_INUSE - chunk.write() - -def inuse_bit_at_offset(p, s): - "check inuse bits in known places" - return (malloc_chunk((p.address + s), inuse=False).size & PREV_INUSE) - -def set_inuse_bit_at_offset(p, s): - "set inuse bits in known places" - chunk = malloc_chunk((p.address + s), inuse=False) - chunk.size |= PREV_INUSE - chunk.write() - -def clear_inuse_bit_at_offset(p, s): - "clear inuse bits in known places" - chunk = malloc_chunk((p.address + s), inuse=False) - chunk.size &= ~PREV_INUSE - chunk.write() - -def bin_at(m, i): - "addressing -- note that bin_at(0) does not exist" - if SIZE_SZ == 4: - offsetof_fd = 0x8 - return (gdb.parse_and_eval("&main_arena.bins[%d]" % \ - ((i -1) * 2)).cast(gdb.lookup_type('unsigned int')) - offsetof_fd) - elif SIZE_SZ == 8: - offsetof_fd = 0x10 - return (gdb.parse_and_eval("&main_arena.bins[%d]" % \ - ((i -1) * 2)).cast(gdb.lookup_type('unsigned long')) - offsetof_fd) - -def next_bin(b): - return (b + 1) - -def first(b): - return b.fd - -def last(b): - return b.bk - -NBINS = 128 -NSMALLBINS = 64 -SMALLBIN_WIDTH = MALLOC_ALIGNMENT -MIN_LARGE_SIZE = (NSMALLBINS * SMALLBIN_WIDTH) - -def in_smallbin_range(sz): - "check if size is in smallbin range" - return (sz < MIN_LARGE_SIZE) - -def smallbin_index(sz): - "return the smallbin index" - - if SMALLBIN_WIDTH == 16: - return (sz >> 4) - else: - return (sz >> 3) - -def largebin_index_32(sz): - "return the 32bit largebin index" - - if (sz >> 6) <= 38: - return (56 + (sz >> 6)) - elif (sz >> 9) <= 20: - return (91 + (sz >> 9)) - elif (sz >> 12) <= 10: - return (110 + (sz >> 12)) - elif (sz >> 15) <= 4: - return (119 + (sz >> 15)) - elif (sz >> 18) <= 2: - return (124 + (sz >> 18)) - else: - return 126 - -def largebin_index_64(sz): - "return the 64bit largebin index" - - if (sz >> 6) <= 48: - return (48 + (sz >> 6)) - elif (sz >> 9) <= 20: - return (91 + (sz >> 9)) - elif (sz >> 12) <= 10: - return (110 + (sz >> 12)) - elif (sz >> 15) <= 4: - return (119 + (sz >> 15)) - elif (sz >> 18) <= 2: - return (124 + (sz >> 18)) - else: - return 126 - -def largebin_index(sz): - "return the largebin index" - - if SIZE_SZ == 8: - return largebin_index_64(sz) - else: - return largebin_index_32(sz) - -def bin_index(sz): - "return the bin index" - - if in_smallbin_range(sz): - return smallbin_index(sz) - else: - return largebin_index(sz) - -BINMAPSHIFT = 5 -BITSPERMAP = 1 << BINMAPSHIFT -BINMAPSIZE = (NBINS / BITSPERMAP) - -def fastbin(ar_ptr, idx): - return ar_ptr.fastbinsY[idx] - -def fastbin_index(sz): - "offset 2 to use otherwise unindexable first 2 bins" - if SIZE_SZ == 8: - return ((sz >> 4) - 2) - else: - return ((sz >> 3) - 2) - -MAX_FAST_SIZE = (80 * SIZE_SZ / 4) -NFASTBINS = (fastbin_index(request2size(MAX_FAST_SIZE)) + 1) - -FASTCHUNKS_BIT = 0x1 - -def have_fastchunks(M): - return ((M.flags & FASTCHUNKS_BIT) == 0) - -def clear_fastchunks(M, inferior=None): - if inferior == None: - inferior = get_inferior() - - M.flags |= FASTCHUNKS_BIT - inferior.write_memory(M.address, struct.pack(" ",c_value,"[ ",fd," ]",c_none), end=' ') - - if fd == 0: #fastbin is empty - print("") - else: - fb_size = ((MIN_CHUNK_SIZE) +(MALLOC_ALIGNMENT)*fb) - print("(%d)" % fb_size) - chunk = malloc_chunk(fd, inuse=False) - while chunk.fd != 0: - if chunk.fd is None: # could not read memory section - break - print("%s%26s0x%08lx%s%s(%d)" % (c_value,"[ ",chunk.fd," ] ",c_none, fb_size)) - chunk = malloc_chunk(chunk.fd, inuse=False) - - if fb_num != None: #only print one fastbin - return - - -################################################################################ -def print_smallbins(inferior, sb_base, sb_num): - "walk and print the small bins" - - print(c_title + "===================================", end=' ') - print("Smallbins ==================================\n" + c_none) - - for sb in range(2,NBINS+2,2): - if sb_num != None and sb_num!=0: - sb = sb_num*2 - - offset = sb_base + (sb-2)*SIZE_SZ - try: - mem = inferior.read_memory(offset, 2*SIZE_SZ) - if SIZE_SZ == 4: - fd,bk = struct.unpack(" ",c_value,"[ ", fd, " | ", bk, " ] ", \ - c_none)) - - while (1): - if fd == (offset-2*SIZE_SZ): - break - - chunk = malloc_chunk(fd, inuse=False) - print("%s%26s0x%08lx%s0x%08lx%s%s" % \ - (c_value,"[ ",chunk.fd," | ",chunk.bk," ] ",c_none), end=' ') - print("(%d)" % chunksize(chunk)) - - fd = chunk.fd - - if sb_num != None: #only print one smallbin - return - - -################################################################################ -def print_bins(inferior, fb_base, sb_base): - "walk and print the nonempty free bins, modified from jp" - - print(c_title + "==================================", end=' ') - print("Heap Dump ===================================\n" + c_none) - - for fb in range(0,NFASTBINS): - print_once = True - p = malloc_chunk(fb_base-(2*SIZE_SZ)+fb*SIZE_SZ, inuse=False) - - while (p.fd != 0): - if p.fd is None: - break - - if print_once: - print_once = False - print(c_header + " fast bin %d @ 0x%lx" % \ - (fb,p.fd) + c_none) - print(" free chunk @ " + c_value + "0x%lx" % p.fd + c_none + \ - " - size" + c_value, end=' ') - p = malloc_chunk(p.fd, inuse=False) - print("0x%lx" % chunksize(p) + c_none) - - for i in range(1, NBINS): - print_once = True - b = sb_base + i*2*SIZE_SZ - 4*SIZE_SZ - p = malloc_chunk(first(malloc_chunk(b, inuse=False)), inuse=False) - - while p.address != b: - if print_once: - print_once = False - if i==1: - try: - print(c_header + " unsorted bin @ 0x%lx" % \ - (b.cast(gdb.lookup_type("unsigned long")) \ - + 2*SIZE_SZ) + c_none) - except: - print(c_header + " unsorted bin @ 0x%lx" % \ - (b + 2*SIZE_SZ) + c_none) - else: - try: - print(c_header + " small bin %d @ 0x%lx" % \ - (i,b.cast(gdb.lookup_type("unsigned long")) \ - + 2*SIZE_SZ) + c_none) - except: - print(c_header + " small bin %d @ 0x%lx" % \ - (i,b + 2*SIZE_SZ) + c_none) - - print(c_none + " free_chunk @ " + c_value \ - + "0x%lx " % p.address + c_none \ - + "- size " + c_value + "0x%lx" % chunksize(p) + c_none) - - p = malloc_chunk(first(p), inuse=False) - - -################################################################################ -def print_flat_listing(ar_ptr, sbrk_base): - "print a flat listing of an arena, modified from jp and arena.c" - - print(c_title + "==================================", end=' ') - print("Heap Dump ===================================\n" + c_none) - print("%s%14s%17s%15s%s" % (c_header, "ADDR", "SIZE", "STATUS", c_none)) - print("sbrk_base " + c_value + "0x%lx" % sbrk_base) - - p = malloc_chunk(sbrk_base, inuse=True, read_data=False) - - while(1): - print("%schunk %s0x%-14lx 0x%-10lx%s" % \ - (c_none, c_value, p.address, chunksize(p), c_none), end=' ') - - if p.address == top(ar_ptr): - print("(top)") - break - elif p.size == (0|PREV_INUSE): - print("(fence)") - break - - if inuse(p): - print("%s" % "(inuse)") - else: - p = malloc_chunk(p.address, inuse=False) - print("(F) FD %s0x%lx%s BK %s0x%lx%s" % \ - (c_value, p.fd, c_none,c_value,p.bk,c_none), end=' ') - - if ((p.fd == ar_ptr.last_remainder) \ - and (p.bk == ar_ptr.last_remainder) \ - and (ar_ptr.last_remainder != 0)): - print("(LR)") - elif ((p.fd == p.bk) & ~inuse(p)): - print("(LC)") - else: - print("") - - p = malloc_chunk(next_chunk(p), inuse=True, read_data=False) - - print(c_none + "sbrk_end " + c_value \ - + "0x%lx" % (sbrk_base + ar_ptr.system_mem) + c_none) - - -################################################################################ -def print_compact_listing(ar_ptr, sbrk_base): - "print a compact layout of the heap, modified from jp" - - print(c_title + "==================================", end=' ') - print("Heap Dump ===================================" + c_none) - p = malloc_chunk(sbrk_base, inuse=True, read_data=False) - - while(1): - if p.address == top(ar_ptr): - sys.stdout.write("|T|\n") - break - - if inuse(p): - sys.stdout.write("|A|") - else: - p = malloc_chunk(p.address, inuse=False) - - if ((p.fd == ar_ptr.last_remainder) \ - and (p.bk == ar_ptr.last_remainder) \ - and (ar_ptr.last_remainder != 0)): - sys.stdout.write("|L|") - else: - sys.stdout.write("|%d|" % bin_index(p.size)) - - p = malloc_chunk(next_chunk(p), inuse=True, read_data=False) - - -################################################################################ -class print_bin_layout(gdb.Command): - "dump the layout of a free bin" - - def __init__(self): - super(print_bin_layout, self).__init__("print_bin_layout", - gdb.COMMAND_DATA, gdb.COMPLETE_NONE) - - def invoke(self, arg, from_tty): - "Specify an optional arena addr: print_bin_layout main_arena=0x12345" - - if len(arg) == 0: - sys.stdout.write(c_error) - print("Please specify the free bin to dump") - sys.stdout.write(c_none) - return - - try: - if arg.find("main_arena") == -1: - main_arena = gdb.selected_frame().read_var('main_arena') - main_arena_address = main_arena.address - else: - arg = arg.split() - for item in arg: - if item.find("main_arena") != -1: - if len(item) < 12: - sys.stdout.write(c_error) - print("Malformed main_arena parameter") - sys.stdout.write(c_none) - return - else: - main_arena_address = int(item[11:],16) - except RuntimeError: - sys.stdout.write(c_error) - print("No frame is currently selected.") - sys.stdout.write(c_none) - return - except ValueError: - sys.stdout.write(c_error) - print("Debug glibc was not found.") - sys.stdout.write(c_none) - return - - if main_arena_address == 0: - sys.stdout.write(c_error) - print("Invalid main_arena address (0)") - sys.stdout.write(c_none) - return - - ar_ptr = malloc_state(main_arena_address) - mutex_lock(ar_ptr) - - sys.stdout.write(c_title) - print("=================================", end=' ') - print("Bin Layout ===================================\n") - sys.stdout.write(c_none) - - b = bin_at(ar_ptr, int(arg)) - p = malloc_chunk(first(malloc_chunk(b, inuse=False)), inuse=False) - print_once = True - print_str = "" - count = 0 - - while p.address != b: - if print_once: - print_once=False - print_str += "--> " + c_value + "[bin %d]" % int(arg) + c_none - count += 1 - - print_str += " <--> " + c_value + "0x%lx" % p.address + c_none - count += 1 - #print_str += " <--> 0x%lx" % p.address - p = malloc_chunk(first(p), inuse=False) - - if len(print_str) != 0: - print_str += " <--" - print(print_str) - print("%s%s%s" % ("|"," " * (len(print_str) - 2 - count*12),"|")) - print("%s" % ("-" * (len(print_str) - count*12))) - else: - print("Bin %d empty." % int(arg)) - - mutex_unlock(ar_ptr) - - -################################################################################ -class check_house_of_mind(gdb.Command): - "print and help validate a house of mind layout" - - def __init__(self): - super(check_house_of_mind, self).__init__("check_house_of_mind", - gdb.COMMAND_DATA, gdb.COMPLETE_NONE) - - def invoke(self, arg, from_tty): - """ - Specify the house of mind method and chunk address (p=mem2chunk(mem)): - check_house_of_mind method=unsortedbin p=0x12345678 - check_house_of_mind method=fastbin p=0x12345678 - """ - - if arg.find("method") == -1: - print("Please specify the House of Mind method to use:") - print("house_of_mind method={unsortedbin, fastbin}") - return - elif arg.find("p") == -1: - print("Please specify the chunk address to use:") - print("house_of_mind p=0x12345678") - return - else: - arg = arg.split() - for item in arg: - if item.find("method") != -1: - if len(item) < 8: - sys.stdout.write(c_error) - print("Malformed method parameter") - print("Please specify the House of Mind method to use:") - print("house_of_mind method={unsortedbin, fastbin}") - sys.stdout.write(c_none) - return - else: - method = item[7:] - if item.find("p") != -1: - if len(item) < 11: - sys.stdout.write(c_error) - print("Malformed chunk parameter") - print("Please specify the chunk address to use:") - print("house_of_mind p=0x12345678") - sys.stdout.write(c_none) - return - else: - p = int(item[2:],16) - - sys.stdout.write(c_title) - print("===============================", end=' ') - print("House of Mind ==================================\n") - sys.stdout.write(c_none) - - if method.find("unsorted") != -1: - self.unsorted_bin_method(p) - elif method.find("fast") != -1: - self.fast_bin_method(p) - - def unsorted_bin_method(self, p): - p = malloc_chunk(addr=p, inuse=True, read_data=False) - - print(c_none + "Checking chunk p") - print(c_none + " [*] p = " + c_value + "0x%x" % p.address + c_none) - - if p.address < gdb.parse_and_eval("(unsigned int)%d" % -chunksize(p)): - print(" [*] size does not wrap") - else: - print(c_error + " [_] ERROR: p > -size" + c_none) - return - - if chunksize(p) >= MINSIZE: - print(" [*] size is > minimum chunk size") - else: - print(c_error + " [_] ERROR: chunksize(p) < MINSIZE" + c_none) - return - - if chunksize(p) > get_max_fast(): - print(" [*] size is not in fastbin range") - else: - print(c_error + " [_] ERROR: size is in fastbin range" + c_none) - return - - if not chunk_is_mmapped(p): - print(" [*] is_mmapped bit is not set") - else: - print(c_error + " [_] ERROR: IS_MMAPPED bit is set" + c_none) - return - - if prev_inuse(p): - print(" [*] prev_inuse bit is set") - else: - print(c_error + " [_] ERROR: PREV_INUSE bit is not set, this will", end=' ') - print("trigger backward consolidation" + c_none) - - if chunk_non_main_arena(p): - print(" [*] non_main_arena flag is set") - else: - print(c_error + " [_] ERROR: p's non_main_arena flag is NOT set") - return - - print(c_none + "\nChecking struct heap_info") - print(c_none + " [*] struct heap_info = " \ - + c_value + "0x%x" % heap_for_ptr(p.address)) - - inferior = get_inferior() - if inferior == -1: - return None - - try: - mem = inferior.read_memory(heap_for_ptr(p.address), SIZE_SZ) - if SIZE_SZ == 4: - ar_ptr = struct.unpack("mutex is zero") - else: - print(c_error + " [_] ERROR: av->mutex is not zero" + c_none) - return - - if p.address != av.top: - print(c_none + " [*] p is not the top chunk") - else: - print(c_error + " [_] ERROR: p is the top chunk" + c_none) - return - - if noncontiguous(av): - print(c_none + " [*] noncontiguous_bit is set") - elif contiguous(av): - print(c_error + \ - " [_] ERROR: noncontiguous_bit is NOT set in av->flags" + c_none) - return - - print(" [*] bck = &av->bins[0] = " + c_value + "0x%x" % (ar_ptr+0x38)) - - if SIZE_SZ == 4: - print(c_none + " [*] fwd = bck->fd = *(&av->bins[0] + 8) =", end=' ') - elif SIZE_SZ == 8: - print(c_none + " [*] fwd = bck->fd = *(&av->bins[0] + 16) =", end=' ') - - fwd = inferior.read_memory(ar_ptr + 0x38 + 2*SIZE_SZ, SIZE_SZ) - if SIZE_SZ == 4: - fwd = struct.unpack("bk (0x%x) != bck (0x%x)" % \ - (fwd, ar_ptr+0x38) + c_error) - print(" - ERROR: This will prevent this attack on glibc 2.11+", end=' ') - print(c_none) - - print(c_none + "\nChecking following chunks") - nextchunk = chunk_at_offset(p, chunksize(p)) - - if prev_inuse(nextchunk): - print(c_none + " [*] prev_inuse of the next chunk is set") - else: - print(c_error + " [_] PREV_INUSE bit of the next chunk is not set" \ - + c_none) - return - - if chunksize(nextchunk) > 2*SIZE_SZ: - print(c_none + " [*] nextchunk size is > minimum size") - else: - print(c_error + " [_] ERROR: nextchunk size (%d) < %d" % \ - (chunksize(nextchunk), 2*SIZE_SZ) + c_none) - return - - if chunksize(nextchunk) < av.system_mem: - print(c_none + " [*] nextchunk size is < av->system_mem") - else: - print(c_error + " [_] ERROR: nextchunk size (0x%x) >" % \ - chunksize(nextchunk), end=' ') - print("av->system_mem (0x%x)" % av.system_mem + c_none) - return - - if nextchunk.address != av.top: - print(c_none + " [*] nextchunk != av->top") - else: - print(c_error + " [_] ERROR: nextchunk is av->top (0x%x)" % av.top \ - + c_none) - return - - if inuse_bit_at_offset(nextchunk, chunksize(nextchunk)): - print(c_none + " [*] prev_inuse bit set on chunk after nextchunk") - else: - print(c_error + " [_] ERROR: PREV_INUSE bit of chunk after", end=' ') - print("nextchunk (0x%x) is not set" % \ - (nextchunk.address + chunksize(nextchunk)) + c_none) - return - - print(c_header + "\np (0x%x) will be written to fwd->bk (0x%x)" \ - % (p.address, fwd+0xC) + c_none) - - def fast_bin_method(self, p): - p = malloc_chunk(addr=p, inuse=True, read_data=False) - - print(c_none + "Checking chunk p") - print(c_none + " [*] p = " + c_value + "0x%x" % p.address + c_none) - - if p.address < gdb.parse_and_eval("(unsigned int)%d" % -chunksize(p)): - print(" [*] size does not wrap") - else: - print(c_error + " [_] ERROR: p > -size" + c_none) - return - - if chunksize(p) >= MINSIZE: - print(" [*] size is >= minimum chunk size") - else: - print(c_error + " [_] ERROR: chunksize(p) < MINSIZE" + c_none) - return - - if chunksize(p) < get_max_fast(): - print(" [*] size is in fastbin range") - else: - print(c_error + " [_] ERROR: size is not in fastbin range" + c_none) - return - - if chunk_non_main_arena(p): - print(" [*] non_main_arena flag is set") - else: - print(c_error + " [_] ERROR: p's non_main_arena flag is NOT set") - return - - if prev_inuse(p): - print(" [*] prev_inuse bit is set") - else: - print(c_error + " [_] ERROR: PREV_INUSE bit is not set, this will", end=' ') - print("trigger backward consolidation" + c_none) - - print(c_none + "\nChecking struct heap_info") - print(c_none + " [*] struct heap_info = " \ - + c_value + "0x%x" % heap_for_ptr(p.address)) - - inferior = get_inferior() - if inferior == -1: - return None - - try: - mem = inferior.read_memory(heap_for_ptr(p.address), SIZE_SZ) - if SIZE_SZ == 4: - ar_ptr = struct.unpack("mutex is zero") - else: - print(c_error + " [_] ERROR: av->mutex is not zero" + c_none) - return - - print(c_none + " [*] av->system_mem is 0x%x" % av.system_mem) - - print(c_none + "\nChecking following chunk") - nextchunk = chunk_at_offset(p, chunksize(p)) - print(" [*] nextchunk = " + c_value + "0x%x" % nextchunk.address) - - if nextchunk.size > 2*SIZE_SZ: - print(c_none + " [*] nextchunk size is > 2*SIZE_SZ") - else: - print(c_error + " [_] ERROR: nextchunk size is <= 2*SIZE_SZ" +c_none) - return - - if chunksize(nextchunk) < av.system_mem: - print(c_none + " [*] nextchunk size is < av->system_mem") - else: - print(c_error + " [_] ERROR: nextchunk size (0x%x) is >= " % \ - chunksize(nextchunk), end=' ') - print("av->system_mem (0x%x)" % (av.system_mem) + c_none) - return - - fb = ar_ptr + (2*SIZE_SZ) + (fastbin_index(p.size)*SIZE_SZ) - print(c_header + "\np (0x%x) will be written to fb (0x%x)" \ - % (p.address, fb) + c_none) - - -################################################################################ -# INITIALIZE CUSTOM GDB CODE -################################################################################ - -heap() -print_malloc_stats() -print_bin_layout() -check_house_of_mind() -gdb.pretty_printers.append(pretty_print_heap_lookup) diff --git a/pwndbg/heap/ptmalloc.py b/pwndbg/heap/ptmalloc.py index 5163300fbd..923b3be9ee 100644 --- a/pwndbg/heap/ptmalloc.py +++ b/pwndbg/heap/ptmalloc.py @@ -6,27 +6,64 @@ from __future__ import unicode_literals from collections import OrderedDict +from collections import namedtuple import gdb +import pwndbg.color.memory as M import pwndbg.events import pwndbg.typeinfo -from pwndbg.color import bold -from pwndbg.color import red +from pwndbg.color import message from pwndbg.constants import ptmalloc +from pwndbg.heap import heap_chain_limit + +# See https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/arena.c;h=37183cfb6ab5d0735cc82759626670aff3832cd0;hb=086ee48eaeaba871a2300daf85469671cc14c7e9#l30 +# and https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c;h=f8e7250f70f6f26b0acb5901bcc4f6e39a8a52b2;hb=086ee48eaeaba871a2300daf85469671cc14c7e9#l869 +# 1 Mb (x86) or 64 Mb (x64) +HEAP_MAX_SIZE = 1024 * 1024 if pwndbg.arch.ptrsize == 4 else 2 * 4 * 1024 * 1024 * 8 -HEAP_MAX_SIZE = 1024 * 1024 def heap_for_ptr(ptr): "find the heap and corresponding arena for a given ptr" return (ptr & ~(HEAP_MAX_SIZE-1)) +class Arena(object): + def __init__(self, addr, heaps): + self.addr = addr + self.heaps = heaps + + def __str__(self): + res = [] + prefix = '[%%%ds] ' % (pwndbg.arch.ptrsize * 2) + prefix_len = len(prefix % ('')) + arena_name = hex(self.addr) if self.addr != pwndbg.heap.current.main_arena.address else 'main' + res.append(message.hint(prefix % (arena_name)) + str(self.heaps[0])) + for h in self.heaps[1:]: + res.append(' ' * prefix_len + str(h)) + + return '\n'.join(res) + + +class HeapInfo(object): + def __init__(self, addr, first_chunk): + self.addr = addr + self.first_chunk = first_chunk + + def __str__(self): + fmt = '[%%%ds]' % (pwndbg.arch.ptrsize * 2) + return message.hint(fmt % (hex(self.first_chunk))) + M.heap(str(pwndbg.vmmap.find(self.addr))) + + class Heap(pwndbg.heap.heap.BaseHeap): def __init__(self): # Global ptmalloc objects self._main_arena = None self._mp = None + # List of arenas/heaps + self._arenas = None + # ptmalloc cache for current thread + self._thread_cache = None @property @@ -36,12 +73,94 @@ def main_arena(self): if main_arena_addr is not None: self._main_arena = pwndbg.memory.poi(self.malloc_state, main_arena_addr) else: - print(bold(red('Symbol \'main arena\' not found. Try installing libc ' - 'debugging symbols and try again.'))) + print(message.error('Symbol \'main_arena\' not found. Try installing libc ' + 'debugging symbols and try again.')) return self._main_arena + @property + @pwndbg.memoize.reset_on_stop + def arenas(self): + arena = self.main_arena + arenas = [] + arena_cnt = 0 + main_arena_addr = int(arena.address) + sbrk_page = self.get_heap_boundaries().vaddr + + # Create the main_arena with a fake HeapInfo + main_arena = Arena(main_arena_addr, [HeapInfo(sbrk_page, sbrk_page)]) + arenas.append(main_arena) + + # Iterate over all the non-main arenas + addr = int(arena['next']) + while addr != main_arena_addr: + heaps = [] + arena = self.get_arena(addr) + arena_cnt += 1 + + # Get the first and last element on the heap linked list of the arena + last_heap_addr = heap_for_ptr(int(arena['top'])) + first_heap_addr = heap_for_ptr(addr) + + heap = self.get_heap(last_heap_addr) + if not heap: + print(message.error('Could not find the heap for arena %s' % hex(addr))) + return + + # Iterate over the heaps of the arena + haddr = last_heap_addr + while haddr != 0: + if haddr == first_heap_addr: + # The first heap has a heap_info and a malloc_state before the actual chunks + chunks_offset = self.heap_info.sizeof + self.malloc_state.sizeof + else: + # The others just + chunks_offset = self.heap_info.sizeof + heaps.append(HeapInfo(haddr, haddr + chunks_offset)) + + # Name the heap mapping, so that it can be colored properly. Note that due to the way malloc is + # optimized, a vm mapping may contain two heaps, so the numbering will not be exact. + page = self.get_region(haddr) + page.objfile = '[heap %d:%d]' % (arena_cnt, len(heaps)) + heap = self.get_heap(haddr) + haddr = int(heap['prev']) + + # Add to the list of arenas and move on to the next one + arenas.append(Arena(addr, tuple(reversed(heaps)))) + addr = int(arena['next']) + + arenas = tuple(arenas) + self._arenas = arenas + return arenas + + + def has_tcache(self): + return (self.mp and 'tcache_bins' in self.mp.type.keys() and self.mp['tcache_bins']) + + + @property + def thread_cache(self): + tcache_addr = pwndbg.symbol.address('tcache') + + if tcache_addr is not None: + try: + self._thread_cache = pwndbg.memory.poi(self.tcache_perthread_struct, tcache_addr) + _ = self._thread_cache['entries'].fetch_lazy() + except Exception as e: + print(message.error('Error fetching tcache. GDB cannot access ' + 'thread-local variables unless you compile with -lpthread.')) + else: + if not self.has_tcache(): + print(message.warn('Your libc does not use thread cache')) + return None + + print(message.error('Symbol \'tcache\' not found. Try installing libc ' + 'debugging symbols and try again.')) + + return self._thread_cache + + @property def mp(self): mp_addr = pwndbg.symbol.address('mp_') @@ -54,7 +173,8 @@ def mp(self): @property def global_max_fast(self): - return pwndbg.symbol.address('global_max_fast') + addr = pwndbg.symbol.address('global_max_fast') + return pwndbg.memory.u(addr) @property @@ -62,7 +182,7 @@ def global_max_fast(self): def heap_info(self): return pwndbg.typeinfo.load('heap_info') - + @property @pwndbg.memoize.reset_on_objfile def malloc_chunk(self): @@ -75,6 +195,18 @@ def malloc_state(self): return pwndbg.typeinfo.load('struct malloc_state') + @property + @pwndbg.memoize.reset_on_objfile + def tcache_perthread_struct(self): + return pwndbg.typeinfo.load('struct tcache_perthread_struct') + + + @property + @pwndbg.memoize.reset_on_objfile + def tcache_entry(self): + return pwndbg.typeinfo.load('struct tcache_entry') + + @property @pwndbg.memoize.reset_on_objfile def mallinfo(self): @@ -90,14 +222,42 @@ def malloc_par(self): @property @pwndbg.memoize.reset_on_objfile def malloc_alignment(self): + """Corresponds to MALLOC_ALIGNMENT in glibc malloc.c""" return pwndbg.arch.ptrsize * 2 + @property + @pwndbg.memoize.reset_on_objfile + def size_sz(self): + """Corresponds to SIZE_SZ in glibc malloc.c""" + return pwndbg.arch.ptrsize + + + @property + @pwndbg.memoize.reset_on_objfile + def malloc_align_mask(self): + """Corresponds to MALLOC_ALIGN_MASK in glibc malloc.c""" + return self.malloc_alignment - 1 + + @property + @pwndbg.memoize.reset_on_objfile + def minsize(self): + """Corresponds to MINSIZE in glibc malloc.c""" + return self.min_chunk_size + + @property @pwndbg.memoize.reset_on_objfile def min_chunk_size(self): + """Corresponds to MIN_CHUNK_SIZE in glibc malloc.c""" return pwndbg.arch.ptrsize * 4 + def _request2size(self, req): + """Corresponds to request2size in glibc malloc.c""" + if req + self.size_sz + self.malloc_align_mask < self.minsize: + return self.minsize + return (req + self.size_sz + self.malloc_align_mask) & ~self.malloc_align_mask + def _spaces_table(self): spaces_table = [ pwndbg.arch.ptrsize * 2 ] * 64 \ @@ -148,9 +308,16 @@ def chunk_key_offset(self, key): except: return None + + @property + @pwndbg.memoize.reset_on_objfile + def tcache_next_offset(self): + return self.tcache_entry.keys().index('next') * pwndbg.arch.ptrsize + + def get_heap(self,addr): return pwndbg.memory.poi(self.heap_info,heap_for_ptr(addr)) - + def get_arena(self, arena_addr=None): if arena_addr is None: @@ -167,47 +334,64 @@ def get_arena_for_chunk(self,addr): else: r=self.main_arena return r - - def get_region(self,addr=None): + + + def get_tcache(self, tcache_addr=None): + if tcache_addr is None: + return self.thread_cache + + return pwndbg.memory.poi(self.tcache_perthread_struct, tcache_addr) + + + def get_heap_boundaries(self, addr=None): """ - Finds the memory region used for heap by using mp_ structure's sbrk_base property - and falls back to using /proc/self/maps (vmmap) which can be wrong - when .bss is very large - For non main-arena heaps use heap_info struct provieded at the beging of the page. + Get the boundaries of the heap containing `addr`. Returns the brk region for + adresses inside it or a fake Page for the containing heap for non-main arenas. """ + page = pwndbg.memory.Page(0, 0, 0, 0) + brk = self.get_region() + if addr is None or brk.vaddr < addr < brk.vaddr + brk.memsz: + # Occasionally, the [heap] vm region and the actual start of the heap are + # different, e.g. [heap] starts at 0x61f000 but mp_.sbrk_base is 0x620000. + # Return an adjusted Page object if this is the case. + sbrk_base = int(self.mp['sbrk_base']) + if sbrk_base != brk.vaddr: + page.vaddr = sbrk_base + page.memsz = brk.memsz - (sbrk_base - brk.vaddr) + return page + else: + return brk + else: + page.vaddr = heap_for_ptr(addr) + heap = self.get_heap(page.vaddr) + page.memsz = int(heap['size']) + return page + + def get_region(self, addr=None): + """ + Finds the memory map used for the heap at addr or the main heap by looking for a + mapping named [heap]. + """ if addr: - heap = self.get_heap(addr) - base = int(heap.address) + self.heap_info.sizeof + self.malloc_state.sizeof - page = pwndbg.vmmap.find(base) - ## trim whole page to look exactly like heap - page.size = int(heap['size']) - page.vaddr = base - - return page + return pwndbg.vmmap.find(addr) - page = None + # No address provided, find the vm region of the main heap. + brk = None for m in pwndbg.vmmap.get(): if m.objfile == '[heap]': - page = m + brk = m break - if page is not None: - mp = self.mp - if mp: - ## this can't fail right? - page.vaddr = int(mp['sbrk_base']) - return page - - return None + return brk - def fastbin_index(self, size): if pwndbg.arch.ptrsize == 8: return (size >> 4) - 2 else: return (size >> 3) - 2 + def fastbins(self, arena_addr=None): arena = self.get_arena(arena_addr) @@ -222,13 +406,38 @@ def fastbins(self, arena_addr=None): result = OrderedDict() for i in range(num_fastbins): size += pwndbg.arch.ptrsize * 2 - chain = pwndbg.chain.get(int(fastbinsY[i]), offset=fd_offset) + chain = pwndbg.chain.get(int(fastbinsY[i]), offset=fd_offset, limit=heap_chain_limit) result[size] = chain return result - + + def tcachebins(self, tcache_addr=None): + tcache = self.get_tcache(tcache_addr) + + if tcache is None: + return + + counts = tcache['counts'] + entries = tcache['entries'] + + num_tcachebins = entries.type.sizeof // entries.type.target().sizeof + + def tidx2usize(idx): + """Tcache bin index to chunk size, following tidx2usize macro in glibc malloc.c""" + return idx * self.malloc_alignment + self.minsize - self.size_sz + + result = OrderedDict() + for i in range(num_tcachebins): + size = self._request2size(tidx2usize(i)) + count = int(counts[i]) + chain = pwndbg.chain.get(int(entries[i]), offset=self.tcache_next_offset, limit=heap_chain_limit) + + result[size] = (chain, count) + + return result + def bin_at(self, index, arena_addr=None): """ @@ -256,7 +465,7 @@ def bin_at(self, index, arena_addr=None): front, back = normal_bins[index * 2], normal_bins[index * 2 + 1] fd_offset = self.chunk_key_offset('fd') - chain = pwndbg.chain.get(int(front), offset=fd_offset, hard_stop=current_base) + chain = pwndbg.chain.get(int(front), offset=fd_offset, hard_stop=current_base, limit=heap_chain_limit, include_start=False) return chain @@ -304,3 +513,14 @@ def largebins(self, arena_addr=None): result[size] = chain return result + + + def is_initialized(self): + """ + malloc state is initialized when a new arena is created. + https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c;h=96149549758dd424f5c08bed3b7ed1259d5d5664;hb=HEAD#l1807 + By default main_arena is partially initialized, and during the first usage of a glibc allocator function some other field are populated. + global_max_fast is one of them thus the call of set_max_fast() when initializing the main_arena, + making it one of the ways to check if the allocator is initialized or not. + """ + return self.global_max_fast != 0 diff --git a/pwndbg/hexdump.py b/pwndbg/hexdump.py index 03543c6163..bad32476bb 100644 --- a/pwndbg/hexdump.py +++ b/pwndbg/hexdump.py @@ -53,7 +53,7 @@ def load_color_scheme(): color_scheme[-1] = ' ' printable[-1] = ' ' -def hexdump(data, address = 0, width = 16, skip = True): +def hexdump(data, address = 0, width = 16, skip = True, offset = 0): if not color_scheme or not printable: load_color_scheme() data = list(bytearray(data)) @@ -73,9 +73,9 @@ def hexdump(data, address = 0, width = 16, skip = True): hexline = [] if address: - hexline.append(H.offset("+%04x " % (i*width))) + hexline.append(H.offset("+%04x " % ((i + offset) * width))) - hexline.append(H.address("%#08x " % (base + (i*width)))) + hexline.append(H.address("%#08x " % (base + (i * width)))) for group in groupby(line, 4): for char in group: @@ -89,9 +89,12 @@ def hexdump(data, address = 0, width = 16, skip = True): hexline.append(printable[char]) hexline.append(H.separator('%s' % config_separator)) - yield(''.join(hexline)) + # skip empty footer if we printed something + if last_line: + return + hexline = [] if address: diff --git a/pwndbg/ida.py b/pwndbg/ida.py index 61421b48ed..75a244dd66 100644 --- a/pwndbg/ida.py +++ b/pwndbg/ida.py @@ -13,19 +13,22 @@ import errno import functools import socket +import sys +import time import traceback import gdb +import six import pwndbg.arch -import pwndbg.color -import pwndbg.compat import pwndbg.config +import pwndbg.decorators import pwndbg.elf import pwndbg.events import pwndbg.memoize import pwndbg.memory import pwndbg.regs +from pwndbg.color import message try: import xmlrpc.client as xmlrpclib @@ -34,32 +37,74 @@ ida_rpc_host = pwndbg.config.Parameter('ida-rpc-host', '127.0.0.1', 'ida xmlrpc server address') -ida_rpc_port = pwndbg.config.Parameter('ida-rpc-port', 8888, 'ida xmlrpc server port') +ida_rpc_port = pwndbg.config.Parameter('ida-rpc-port', 31337, 'ida xmlrpc server port') +ida_enabled = pwndbg.config.Parameter('ida-enabled', True, 'whether to enable ida integration') +ida_timeout = pwndbg.config.Parameter('ida-timeout', 2, 'time to wait for ida xmlrpc in seconds') xmlrpclib.Marshaller.dispatch[int] = lambda _, v, w: w("%d" % v) -if pwndbg.compat.python2: +if six.PY2: xmlrpclib.Marshaller.dispatch[long] = lambda _, v, w: w("%d" % v) xmlrpclib.Marshaller.dispatch[type(0)] = lambda _, v, w: w("%d" % v) _ida = None +# to avoid printing the same exception multiple times, we store the last exception here +_ida_last_exception = None -@pwndbg.config.Trigger([ida_rpc_host, ida_rpc_port]) +# to avoid checking the connection multiple times with no delay, we store the last time we checked it +_ida_last_connection_check = 0 + + +@pwndbg.decorators.only_after_first_prompt() +@pwndbg.config.Trigger([ida_rpc_host, ida_rpc_port, ida_timeout]) def init_ida_rpc_client(): - global _ida + global _ida, _ida_last_exception, _ida_last_connection_check + + if not ida_enabled: + return + + now = time.time() + if _ida is None and (now - _ida_last_connection_check) < int(ida_timeout) + 5: + return + addr = 'http://{host}:{port}'.format(host=ida_rpc_host, port=ida_rpc_port) _ida = xmlrpclib.ServerProxy(addr) + socket.setdefaulttimeout(int(ida_timeout)) + exception = None # (type, value, traceback) try: _ida.here() - print(pwndbg.color.green("Pwndbg successfully connected to Ida Pro xmlrpc: %s" % addr)) + print(message.success("Pwndbg successfully connected to Ida Pro xmlrpc: %s" % addr)) except socket.error as e: if e.errno != errno.ECONNREFUSED: - traceback.print_exc() + exception = sys.exc_info() + _ida = None + except socket.timeout: + exception = sys.exc_info() _ida = None + except xmlrpclib.ProtocolError: + exception = sys.exc_info() + _ida = None + + if exception: + if not isinstance(_ida_last_exception, exception[0]) or _ida_last_exception.args != exception[1].args: + if hasattr(pwndbg.config, "exception_verbose") and pwndbg.config.exception_verbose: + print(message.error("[!] Ida Pro xmlrpc error")) + traceback.print_exception(*exception) + else: + exc_type, exc_value, _ = exception + print(message.error('Failed to connect to IDA Pro ({}: {})'.format(exc_type.__qualname__, exc_value))) + if exc_type is socket.timeout: + print(message.notice('To increase the time to wait for IDA Pro use `') + message.hint('set ida-timeout ') + message.notice('`')) + else: + print(message.notice('For more info invoke `') + message.hint('set exception-verbose on') + message.notice('`')) + print(message.notice('To disable IDA Pro integration invoke `') + message.hint('set ida-enabled off') + message.notice('`')) + + _ida_last_exception = exception and exception[1] + _ida_last_connection_check = now class withIDA(object): @@ -74,6 +119,7 @@ def __call__(self, *args, **kwargs): return self.fn(*args, **kwargs) return None + def withHexrays(f): @withIDA @functools.wraps(f) @@ -81,6 +127,9 @@ def wrapper(*a, **kw): if _ida.init_hexrays_plugin(): return f(*a, **kw) + return wrapper + + def takes_address(function): @functools.wraps(function) def wrapper(address, *args, **kwargs): @@ -97,8 +146,15 @@ def wrapper(*args, **kwargs): return wrapper -@withIDA +@pwndbg.memoize.reset_on_stop def available(): + if not ida_enabled: + return False + return can_connect() + + +@withIDA +def can_connect(): return True @@ -335,6 +391,12 @@ def decompile(addr): return _ida.decompile(addr) +@withIDA +@pwndbg.memoize.forever +def get_ida_versions(): + return _ida.versions() + + @withIDA @pwndbg.memoize.reset_on_stop def GetStrucQty(): diff --git a/pwndbg/inthook.py b/pwndbg/inthook.py index 81d87b717d..38b9191e14 100644 --- a/pwndbg/inthook.py +++ b/pwndbg/inthook.py @@ -10,8 +10,8 @@ from __future__ import print_function from __future__ import unicode_literals +import enum import os -import sys import gdb import six @@ -19,7 +19,7 @@ import pwndbg.typeinfo -if sys.version_info < (3, 0): +if six.PY2: import __builtin__ as builtins else: import builtins @@ -47,7 +47,10 @@ def __new__(cls, value, *a, **kw): if symbol.is_function: value = value.cast(pwndbg.typeinfo.ulong) - elif not isinstance(value, six.string_types) and not isinstance(value, six.integer_types): + elif not isinstance(value, (six.string_types, six.integer_types)) \ + or isinstance(cls, enum.EnumMeta): + # without check for EnumMeta math operations with enums were failing e.g.: + # pwndbg> py import re; flags = 1 | re.MULTILINE return _int.__new__(cls, value, *a, **kw) return _int(_int(value, *a, **kw)) @@ -56,7 +59,6 @@ def __new__(cls, value, *a, **kw): if os.environ.get('SPHINX', None) is None: builtins.int = xint globals()['int'] = xint - - if sys.version_info >= (3, 0): + if six.PY3: builtins.long = xint globals()['long'] = xint diff --git a/pwndbg/memoize.py b/pwndbg/memoize.py index 644b4419ef..59f857b5e6 100644 --- a/pwndbg/memoize.py +++ b/pwndbg/memoize.py @@ -96,6 +96,19 @@ def __reset_on_stop(): _reset = __reset_on_stop +class reset_on_prompt(memoize): + caches = [] + kind = 'prompt' + + @staticmethod + @pwndbg.events.before_prompt + def __reset_on_prompt(): + for obj in reset_on_prompt.caches: + obj.cache.clear() + + _reset = __reset_on_prompt + + class reset_on_exit(memoize): caches = [] kind = 'exit' diff --git a/pwndbg/memory.py b/pwndbg/memory.py index 62ce99d305..c65256cc11 100644 --- a/pwndbg/memory.py +++ b/pwndbg/memory.py @@ -9,11 +9,11 @@ from __future__ import unicode_literals import os +from builtins import bytes import gdb import pwndbg.arch -import pwndbg.compat import pwndbg.events import pwndbg.qemu import pwndbg.typeinfo @@ -68,9 +68,6 @@ def read(addr, count, partial=False): # Move down by another page stop_addr -= PAGE_SIZE - # if pwndbg.compat.python3: - # result = bytes(result) - return bytearray(result) @@ -99,7 +96,10 @@ def write(addr, data): addr(int): Address to write data(str,bytes,bytearray): Data to write """ - gdb.selected_inferior().write_memory(addr, bytes(data)) + if isinstance(data, str): + data = bytes(data, 'utf8') + + gdb.selected_inferior().write_memory(addr, data) def peek(address): @@ -399,6 +399,14 @@ def end(self): """ return self.vaddr + self.memsz + @property + def is_stack(self): + return self.objfile == '[stack]' + + @property + def is_memory_mapped_file(self): + return len(self.objfile) > 0 and self.objfile[0] != '[' + @property def read(self): return bool(self.flags & 4) diff --git a/pwndbg/next.py b/pwndbg/next.py index 35f85dd225..42856ce115 100644 --- a/pwndbg/next.py +++ b/pwndbg/next.py @@ -16,6 +16,7 @@ import pwndbg.disasm import pwndbg.regs +from pwndbg.color import message jumps = set(( capstone.CS_GRP_CALL, @@ -26,6 +27,7 @@ interrupts = set((capstone.CS_GRP_INT,)) + def next_int(address=None): """ If there is a syscall in the current basic black, @@ -49,6 +51,7 @@ def next_int(address=None): return None + def next_branch(address=None): if address is None: ins = pwndbg.disasm.one(pwndbg.regs.pc) @@ -64,6 +67,7 @@ def next_branch(address=None): return None + def break_next_branch(address=None): ins = next_branch(address) @@ -72,6 +76,7 @@ def break_next_branch(address=None): gdb.execute('continue', from_tty=False, to_string=True) return ins + def break_next_interrupt(address=None): ins = next_int(address) @@ -80,6 +85,7 @@ def break_next_interrupt(address=None): gdb.execute('continue', from_tty=False, to_string=True) return ins + def break_next_call(symbol_regex=None): while pwndbg.proc.alive: ins = break_next_branch() @@ -103,6 +109,41 @@ def break_next_call(symbol_regex=None): if ins.symbol and re.match('%s$' % symbol_regex, ins.symbol): return ins + +def break_next_ret(address=None): + while pwndbg.proc.alive: + ins = break_next_branch(address) + + if not ins: + break + + if capstone.CS_GRP_RET in ins.groups: + return ins + + +def break_on_program_code(): + """ + Breaks on next instruction that belongs to process' objfile code. + :return: True for success, False when process ended or when pc is at the code. + """ + mp = pwndbg.proc.mem_page + start = mp.start + end = mp.end + + if start <= pwndbg.regs.pc < end: + print(message.error('The pc is already at the binary objfile code. Not stepping.')) + return False + + while pwndbg.proc.alive: + gdb.execute('si', from_tty=False, to_string=False) + + addr = pwndbg.regs.pc + if start <= addr < end: + return True + + return False + + def break_on_next(address=None): address = address or pwndbg.regs.pc ins = pwndbg.disasm.one(address) diff --git a/pwndbg/proc.py b/pwndbg/proc.py index b568f10dcd..a6fcfa7991 100644 --- a/pwndbg/proc.py +++ b/pwndbg/proc.py @@ -2,7 +2,8 @@ # -*- coding: utf-8 -*- """ Provides values which would be available from /proc which -are not fulfilled by other modules. +are not fulfilled by other modules and some process/gdb flow +related information. """ from __future__ import absolute_import from __future__ import division @@ -47,6 +48,18 @@ def tid(self): def alive(self): return gdb.selected_thread() is not None + @property + def thread_is_stopped(self): + """ + This detects whether selected thread is stopped. + It is not stopped in situations when gdb is executing commands + that are attached to a breakpoint by `command` command. + + For more info see issue #229 ( https://github.com/pwndbg/pwndbg/issues/299 ) + :return: Whether gdb executes commands attached to bp with `command` command. + """ + return gdb.selected_thread().is_stopped() + @property def exe(self): for obj in gdb.objfiles(): @@ -56,13 +69,20 @@ def exe(self): if self.alive: auxv = pwndbg.auxv.get() return auxv['AT_EXECFN'] + + @property + def mem_page(self): + return next(p for p in pwndbg.vmmap.get() if p.objfile == self.exe) + def OnlyWhenRunning(self, func): @functools.wraps(func) def wrapper(*a, **kw): if self.alive: return func(*a, **kw) + return wrapper + # To prevent garbage collection tether = sys.modules[__name__] diff --git a/pwndbg/prompt.py b/pwndbg/prompt.py index 17bfe55a6b..7645310abe 100644 --- a/pwndbg/prompt.py +++ b/pwndbg/prompt.py @@ -7,24 +7,37 @@ import gdb +import pwndbg.decorators import pwndbg.events +import pwndbg.gdbutils import pwndbg.memoize +from pwndbg.color import disable_colors +from pwndbg.color import message -hint_msg = 'Loaded %i commands. Type pwndbg [filter] for a list.' % len(pwndbg.commands.Command.commands) +funcs_list_str = ', '.join(message.notice('$' + f.name) for f in pwndbg.gdbutils.functions.functions) + +hint_lines = ( + 'loaded %i commands. Type %s for a list.' % (len(pwndbg.commands.commands), message.notice('pwndbg [filter]')), + 'created %s gdb functions (can be used with print/break)' % funcs_list_str +) + +for line in hint_lines: + print(message.prompt('pwndbg: ') + message.system(line)) -print(pwndbg.color.red(hint_msg)) cur = (gdb.selected_inferior(), gdb.selected_thread()) def prompt_hook(*a): global cur + pwndbg.decorators.first_prompt = True + new = (gdb.selected_inferior(), gdb.selected_thread()) if cur != new: pwndbg.events.after_reload(start=False) cur = new - if pwndbg.proc.alive: + if pwndbg.proc.alive and pwndbg.proc.thread_is_stopped: prompt_hook_on_stop(*a) @@ -32,4 +45,26 @@ def prompt_hook(*a): def prompt_hook_on_stop(*a): pwndbg.commands.context.context() -gdb.prompt_hook = prompt_hook + +@pwndbg.config.Trigger([message.config_prompt_color, disable_colors]) +def set_prompt(): + prompt = "pwndbg> " + + if not disable_colors: + prompt = "\x02" + prompt + "\x01" # STX + prompt + SOH + prompt = message.prompt(prompt) + prompt = "\x01" + prompt + "\x02" # SOH + prompt + STX + + gdb.execute('set prompt %s' % prompt) + + +if pwndbg.events.before_prompt_event.is_real_event: + gdb.prompt_hook = prompt_hook + +else: + # Old GDBs doesn't have gdb.events.before_prompt, so we will emulate it using gdb.prompt_hook + def extended_prompt_hook(*a): + pwndbg.events.before_prompt_event.invoke_callbacks() + return prompt_hook(*a) + + gdb.prompt_hook = extended_prompt_hook diff --git a/pwndbg/regs.py b/pwndbg/regs.py index 9b2d731b53..9b3a6543d8 100644 --- a/pwndbg/regs.py +++ b/pwndbg/regs.py @@ -19,7 +19,6 @@ import six import pwndbg.arch -import pwndbg.compat import pwndbg.events import pwndbg.memoize import pwndbg.proc @@ -106,6 +105,7 @@ def __iter__(self): aarch64 = RegisterSet( retaddr = ('lr',), flags = {'cpsr':{}}, + frame = 'x29', gpr = tuple('x%i' % i for i in range(29)), misc = tuple('w%i' % i for i in range(29)), args = ('x0','x1','x2','x3'), @@ -267,6 +267,7 @@ class module(ModuleType): last = {} @pwndbg.memoize.reset_on_stop + @pwndbg.memoize.reset_on_prompt def __getattr__(self, attr): attr = attr.lstrip('$') try: @@ -422,6 +423,7 @@ def __repr__(self): @pwndbg.events.cont +@pwndbg.events.stop def update_last(): M = sys.modules[__name__] M.last = {k:M[k] for k in M.common} diff --git a/pwndbg/remote.py b/pwndbg/remote.py index 8e47fa3ee5..2a0269b5d1 100644 --- a/pwndbg/remote.py +++ b/pwndbg/remote.py @@ -17,5 +17,17 @@ def is_remote(): # https://sourceware.org/bugzilla/show_bug.cgi?id=18335 # # return 'serial line' in gdb.execute('info program',to_string=True,) + info_file = gdb.execute('info file',to_string=True,from_tty=False) - return 'Remote serial target' in gdb.execute('info file',to_string=True,from_tty=False) + # target remote + if 'Remote serial target' in info_file: + return True + + # target extended-remote + if 'Extended remote serial target' in info_file: + return True + + if 'Debugging a target over a serial line.' in info_file: + return True + + return False diff --git a/pwndbg/stack.py b/pwndbg/stack.py index 8aed6f4d99..f0cff21af6 100644 --- a/pwndbg/stack.py +++ b/pwndbg/stack.py @@ -14,6 +14,7 @@ import gdb +import pwndbg.elf import pwndbg.events import pwndbg.memoize import pwndbg.memory @@ -27,6 +28,7 @@ # This is updated automatically by is_executable. nx = False + def find(address): """ Returns a pwndbg.memory.Page object which corresponds to the @@ -39,17 +41,17 @@ def find(address): if address in stack: return stack + def find_upper_stack_boundary(addr, max_pages=1024): addr = pwndbg.memory.page_align(int(addr)) - try: - for i in range(max_pages): - data = pwndbg.memory.read(addr, 4) - if b'\x7fELF' == pwndbg.memory.read(addr, 4): - break - addr += pwndbg.memory.PAGE_SIZE - except gdb.MemoryError: - pass - return addr + + # We can't get the stack size from stack layout and page fault on bare metal mode, + # so we return current page as a walkaround. + if not pwndbg.abi.linux: + return addr + pwndbg.memory.PAGE_SIZE + + return pwndbg.elf.find_elf_magic(addr, max_pages=max_pages, ret_addr_anyway=True) + @pwndbg.events.stop @pwndbg.memoize.reset_on_stop @@ -104,6 +106,7 @@ def current(): """ return find(pwndbg.regs.sp) + @pwndbg.events.exit def clear(): """ @@ -115,6 +118,7 @@ def clear(): global nx nx = False + @pwndbg.events.stop @pwndbg.memoize.reset_on_exit def is_executable(): diff --git a/pwndbg/stdio.py b/pwndbg/stdio.py index 4106da366b..9ac414aa54 100644 --- a/pwndbg/stdio.py +++ b/pwndbg/stdio.py @@ -16,8 +16,6 @@ import gdb -import pwndbg.compat - class Stdio(object): queue = [] diff --git a/pwndbg/symbol.py b/pwndbg/symbol.py index 3d16a1221e..51dfd81735 100644 --- a/pwndbg/symbol.py +++ b/pwndbg/symbol.py @@ -205,7 +205,7 @@ def address(symbol): try: symbol_obj = gdb.lookup_symbol(symbol)[0] if symbol_obj: - return int(symbol_obj) + return int(symbol_obj.value().address) except Exception: pass @@ -229,6 +229,9 @@ def add_main_exe_to_symbols(): if not pwndbg.remote.is_remote(): return + if pwndbg.android.is_android(): + return + exe = pwndbg.elf.exe() if not exe: diff --git a/pwndbg/typeinfo.py b/pwndbg/typeinfo.py index 2cd48cf9f2..78e1894490 100644 --- a/pwndbg/typeinfo.py +++ b/pwndbg/typeinfo.py @@ -48,21 +48,21 @@ def lookup_types(*types): def update(): module.char = gdb.lookup_type('char') - module.ulong = lookup_types('unsigned long', 'uint') + module.ulong = lookup_types('unsigned long', 'uint', 'u32') module.long = lookup_types('long', 'int') module.uchar = lookup_types('unsigned char', 'ubyte') - module.ushort = lookup_types('unsigned short', 'ushort') + module.ushort = lookup_types('unsigned short', 'ushort', 'u16') module.uint = lookup_types('unsigned int', 'uint') - module.void = gdb.lookup_type('void') + module.void = lookup_types('void', '()') module.uint8 = module.uchar module.uint16 = module.ushort module.uint32 = module.uint - module.uint64 = lookup_types('unsigned long long', 'ulong') + module.uint64 = lookup_types('unsigned long long', 'ulong', 'u64') module.int8 = gdb.lookup_type('char') - module.int16 = gdb.lookup_type('short') + module.int16 = lookup_types('short', 'i16') module.int32 = gdb.lookup_type('int') - module.int64 = lookup_types('long long', 'long') + module.int64 = lookup_types('long long', 'long', 'i64') module.ssize_t = module.long module.size_t = module.ulong @@ -153,7 +153,10 @@ def compile(filename=None, address=0): if not os.path.exists(objectname): gcc = pwndbg.gcc.which() gcc += ['-w', '-c', '-g', filename, '-o', objectname] - subprocess.check_output(' '.join(gcc), shell=True) + try: + subprocess.check_output(gcc) + except subprocess.CalledProcessError as e: + return add_symbol_file(objectname, address) @@ -165,3 +168,8 @@ def add_symbol_file(filename=None, address=0): with pwndbg.events.Pause(): gdb.execute('add-symbol-file %s %s' % (filename, address), from_tty=False, to_string=True) + +def read_gdbvalue(type_name, addr): + """ Read the memory contents at addr and interpret them as a GDB value with the given type """ + gdb_type = pwndbg.typeinfo.load(type_name) + return gdb.Value(addr).cast(gdb_type.pointer()).dereference() diff --git a/pwndbg/ui.py b/pwndbg/ui.py index fd6536964f..2eadca147a 100644 --- a/pwndbg/ui.py +++ b/pwndbg/ui.py @@ -16,16 +16,40 @@ import pwndbg.arch import pwndbg.color.context as C -import pwndbg.color.theme as theme -import pwndbg.config as config +from pwndbg import config +from pwndbg.color import ljust_colored +from pwndbg.color import message +from pwndbg.color import rjust_colored +from pwndbg.color import strip +from pwndbg.color import theme theme.Parameter('banner-separator', '─', 'repeated banner separator character') +theme.Parameter('banner-title-surrounding-left', '[ ', 'banner title surrounding char (left side)') +theme.Parameter('banner-title-surrounding-right', ' ]', 'banner title surrounding char (right side)') +title_position = theme.Parameter('banner-title-position', 'center', 'banner title position') + + +@pwndbg.config.Trigger([title_position]) +def check_title_position(): + valid_values = ['center', 'left', 'right'] + if title_position not in valid_values: + print(message.warn('Invalid title position: %s, must be one of: %s' % + (title_position, ', '.join(valid_values)))) + title_position.revert_default() + def banner(title): title = title.upper() _height, width = get_window_size() - width -= 2 - return C.banner(("[{:%s^%ss}]" % (config.banner_separator, width)).format(title)) + title = '%s%s%s' % (config.banner_title_surrounding_left, C.banner_title(title), config.banner_title_surrounding_right) + if 'left' == title_position: + banner = ljust_colored(title, width, config.banner_separator) + elif 'right' == title_position: + banner = rjust_colored(title, width, config.banner_separator) + else: + banner = rjust_colored(title, (width + len(strip(title))) // 2, config.banner_separator) + banner = ljust_colored(banner, width, config.banner_separator) + return C.banner(banner) def addrsz(address): address = int(address) & pwndbg.arch.ptrmask diff --git a/pwndbg/version.py b/pwndbg/version.py index e757eee5d5..4d04ff8715 100644 --- a/pwndbg/version.py +++ b/pwndbg/version.py @@ -26,7 +26,7 @@ def build_id(): # CalledProcessError -> git return code != 0 return '' -__version__ = '1.0.0' +__version__ = '1.1.0' b_id = build_id() diff --git a/pwndbg/vmmap.py b/pwndbg/vmmap.py index 816603c788..2006461ea7 100644 --- a/pwndbg/vmmap.py +++ b/pwndbg/vmmap.py @@ -12,12 +12,14 @@ from __future__ import print_function from __future__ import unicode_literals +import bisect import os import sys import gdb +import six -import pwndbg.compat +import pwndbg.abi import pwndbg.elf import pwndbg.events import pwndbg.file @@ -34,9 +36,14 @@ # by analyzing the stack or register context. explored_pages = [] +# List of custom pages that can be managed manually by vmmap_* commands family +custom_pages = [] + @pwndbg.events.new_objfile @pwndbg.memoize.reset_on_stop def get(): + if not pwndbg.proc.alive: + return tuple() pages = [] pages.extend(proc_pid_maps()) @@ -49,6 +56,7 @@ def get(): pages.extend(pwndbg.stack.stacks.values()) pages.extend(explored_pages) + pages.extend(custom_pages) pages.sort() return tuple(pages) @@ -66,6 +74,7 @@ def find(address): return explore(address) +@pwndbg.abi.LinuxOnly() def explore(address_maybe): """ Given a potential address, check to see what permissions it has. @@ -112,6 +121,26 @@ def clear_explored_pages(): while explored_pages: explored_pages.pop() + +def add_custom_page(page): + bisect.insort(custom_pages, page) + + # Reset all the cache + # We can not reset get() only, since the result may be used by others. + # TODO: avoid flush all caches + pwndbg.memoize.reset() + + +def clear_custom_page(): + while custom_pages: + custom_pages.pop() + + # Reset all the cache + # We can not reset get() only, since the result may be used by others. + # TODO: avoid flush all caches + pwndbg.memoize.reset() + + @pwndbg.memoize.reset_on_stop def proc_pid_maps(): """ @@ -161,7 +190,7 @@ def proc_pid_maps(): else: return tuple() - if pwndbg.compat.python3: + if six.PY3: data = data.decode() pages = [] diff --git a/pwndbg/wrappers.py b/pwndbg/wrappers.py deleted file mode 100644 index f82796d911..0000000000 --- a/pwndbg/wrappers.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" - Wrappers to external utilities. -""" - -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -import functools -import subprocess - -import pwndbg.which - - -def call_program(progname, *args): - program = pwndbg.which.which(progname) - - if not program: - raise OSError('Could not find %s command in $PATH.' % progname) - - cmd = [progname] + list(args) - - try: - return subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode('utf-8') - except Exception as e: - raise OSError('Error during execution of %s command: %s' % (progname, e)) - -checksec = functools.partial(call_program, 'checksec') -readelf = functools.partial(call_program, 'readelf') -file = functools.partial(call_program, 'file') diff --git a/pwndbg/wrappers/__init__.py b/pwndbg/wrappers/__init__.py new file mode 100644 index 0000000000..e28d629df2 --- /dev/null +++ b/pwndbg/wrappers/__init__.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import functools +import subprocess +from subprocess import STDOUT + +import pwndbg.commands +import pwndbg.which + + +class OnlyWithCommand(object): + def __init__(self, command): + self.cmd_name = command + self.cmd_path = pwndbg.which.which(command) + + def __call__(self, function): + function.cmd_path = self.cmd_path + + @pwndbg.commands.OnlyWithFile + @functools.wraps(function) + def _OnlyWithCommand(*a,**kw): + if self.cmd_path: + return function(*a, **kw) + else: + raise OSError('Could not find command %s in $PATH' % self.cmd_name) + return _OnlyWithCommand + + +def call_cmd(cmd): + return subprocess.check_output(cmd, stderr=STDOUT).decode('utf-8') diff --git a/pwndbg/wrappers/checksec.py b/pwndbg/wrappers/checksec.py new file mode 100644 index 0000000000..9d499518c9 --- /dev/null +++ b/pwndbg/wrappers/checksec.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import pwndbg.commands +import pwndbg.memoize +import pwndbg.wrappers + +cmd_name = "checksec" + +@pwndbg.wrappers.OnlyWithCommand(cmd_name) +@pwndbg.memoize.reset_on_objfile +def get_raw_out(): + + local_path = pwndbg.file.get_file(pwndbg.proc.exe) + cmd = [get_raw_out.cmd_path, "--file", local_path] + return pwndbg.wrappers.call_cmd(cmd) + +@pwndbg.wrappers.OnlyWithCommand(cmd_name) +def relro_status(): + relro = "No RELRO" + out = get_raw_out() + + if "Full RELRO" in out: + relro = "Full RELRO" + elif "Partial RELRO" in out: + relro = "Partial RELRO" + + return relro + +@pwndbg.wrappers.OnlyWithCommand(cmd_name) +def pie_status(): + pie = "No PIE" + out = get_raw_out() + + if "PIE enabled" in out: + pie = "PIE enabled" + + return pie diff --git a/pwndbg/wrappers/readelf.py b/pwndbg/wrappers/readelf.py new file mode 100644 index 0000000000..343c7d2a14 --- /dev/null +++ b/pwndbg/wrappers/readelf.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import re + +import pwndbg.wrappers + +cmd_name = "readelf" + +@pwndbg.wrappers.OnlyWithCommand(cmd_name) +def get_jmpslots(): + local_path = pwndbg.file.get_file(pwndbg.proc.exe) + cmd = [get_jmpslots.cmd_path, "--relocs", local_path] + readelf_out = pwndbg.wrappers.call_cmd(cmd) + + return filter(_extract_jumps, readelf_out.splitlines()) + +def _extract_jumps(line): + ''' + Checks for records in `readelf --relocs ` which has type e.g. `R_X86_64_JUMP_SLO` + NOTE: Because of that we DO NOT display entries that are not writeable (due to FULL RELRO) + as they have `R_X86_64_GLOB_DAT` type. + + It might be good to display them seperately in the future. + ''' + try: + if "JUMP" in line.split()[2]: + return line + else: + return False + except IndexError: + return False diff --git a/pytests_collect.py b/pytests_collect.py new file mode 100644 index 0000000000..16ace05040 --- /dev/null +++ b/pytests_collect.py @@ -0,0 +1,36 @@ +from __future__ import print_function +from __future__ import division +from __future__ import absolute_import +from __future__ import unicode_literals + +import os +import sys + +import pytest + + +TESTS_PATH = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + 'tests' +) + + +class CollectTestFunctionNames: + """See https://github.com/pytest-dev/pytest/issues/2039#issuecomment-257753269""" + def __init__(self): + self.collected = [] + + def pytest_collection_modifyitems(self, items): + for item in items: + self.collected.append(item.nodeid) + + +collector = CollectTestFunctionNames() +pytest.main(['--collect-only', TESTS_PATH], plugins=[collector]) + +print('Listing collected tests:') +for nodeid in collector.collected: + print('Test:', nodeid) + +# easy way to exit GDB session +sys.exit(0) diff --git a/pytests_launcher.py b/pytests_launcher.py new file mode 100644 index 0000000000..d41cb54a4d --- /dev/null +++ b/pytests_launcher.py @@ -0,0 +1,31 @@ +from __future__ import print_function +from __future__ import division +from __future__ import absolute_import +from __future__ import unicode_literals + +import os +import pytest +import sys +print(sys.argv) + +sys._pwndbg_unittest_run = True + +CURRENT_DIR = os.path.dirname(os.path.realpath(__file__)) + +test = os.environ['PWNDBG_LAUNCH_TEST'] + +test = os.path.join(CURRENT_DIR, test) + +# If you want to debug tests locally, add '--pdb' here +args = [test, '-vvv', '-s', '--showlocals', '--color=yes'] + +print('Launching pytest with args: %s' % args) + +return_code = pytest.main(args) + +if return_code != 0: + print('-' * 80) + print('If you want to debug tests locally, modify tests_launcher.py and add --pdb to its args') + print('-' * 80) + +sys.exit(return_code) diff --git a/requirements.txt b/requirements.txt index e57676553f..4abd467038 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ future -isort +isort!=4.3.0 pip psutil>=3.1.0 pycparser @@ -8,4 +8,7 @@ python-ptrace>=0.8 ROPgadget six unicorn>=1.0.0 +pygments https://github.com/aquynh/capstone/archive/next.zip#subdirectory=bindings/python +enum34 +pytest diff --git a/tag_release.sh b/tag_release.sh index 0fc7092b08..c8ae5d6c75 100755 --- a/tag_release.sh +++ b/tag_release.sh @@ -8,7 +8,7 @@ git fetch --all TAG=$($DATE '+%Y.%m.%d') -if git tag -a $TAG -m "Add release $TAG" origin/master; then +if git tag -a $TAG -m "Add release $TAG" origin/stable; then git push origin $TAG fi diff --git a/tests.sh b/tests.sh new file mode 100755 index 0000000000..cfc4e08b0f --- /dev/null +++ b/tests.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +cd tests/binaries && make && cd ../.. + +# NOTE: We run tests under GDB sessions and because of some cleanup/tests dependencies problems +# we decided to run each test in a separate GDB session +TESTS_LIST=$(gdb --silent --nx --nh --command gdbinit.py --command pytests_collect.py --eval-command quit | grep -o "tests/.*::.*") + +tests_passed_or_skipped=0 +tests_failed=0 + +for test_case in ${TESTS_LIST}; do + PWNDBG_LAUNCH_TEST="${test_case}" PWNDBG_DISABLE_COLORS=1 gdb --silent --nx --nh --command gdbinit.py --command pytests_launcher.py --eval-command quit + + exit_status=$? + + if [ ${exit_status} -eq 0 ]; then + (( ++tests_passed_or_skipped )) + else + (( ++tests_failed )) + fi +done + +echo "" +echo "*********************************" +echo "********* TESTS SUMMARY *********" +echo "*********************************" +echo "Tests passed or skipped: ${tests_passed_or_skipped}" +echo "Tests failed: ${tests_failed}" + +if [ ${tests_failed} -ne 0 ]; then + exit 1 +fi diff --git a/tests/__init__.py b/tests/__init__.py index faa18be5bb..6837111bda 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,2 +1,6 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +from . import binaries diff --git a/tests/binaries/__init__.py b/tests/binaries/__init__.py new file mode 100644 index 0000000000..bdc06cc2f4 --- /dev/null +++ b/tests/binaries/__init__.py @@ -0,0 +1,14 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import os + +from . import old_bash + +path = os.path.dirname(__file__) + + +def get(x): + return os.path.join(path, x) diff --git a/tests/binaries/emulate_disasm.asm b/tests/binaries/emulate_disasm.asm new file mode 100644 index 0000000000..90ac60f0c3 --- /dev/null +++ b/tests/binaries/emulate_disasm.asm @@ -0,0 +1,14 @@ +global _start + +; This binary is there to test +; emulate vs nearpc/u/pdisas commands +; The emulate should show just jump and one nop +; The rest should show jump and two nops +; +; Motivated by https://github.com/pwndbg/pwndbg/issues/315 + +_start: +jmp label +nop +label: +nop diff --git a/tests/binaries/emulate_disasm.out b/tests/binaries/emulate_disasm.out new file mode 100755 index 0000000000000000000000000000000000000000..f9c9720d7a78cf500bd46dc2b278507c38a2791e GIT binary patch literal 736 zcmbV|u};G<5QhJxDJol$7!VA!OE)?&F;!ha47`JJ6icm?Fw{n(U0R6_9s3A83h%?j z6TsQNOVubWSc<>WNRtGzQcU302Gd~09r!pz=;V$vQ*@DqY&Rrjla9zO z+d^Q#PQ+KT-yaz*qC}n-WUp)++1t!aWJ=lCuWQ8rGBzaM{hww&kd(M=cxkP2}Hr*>%CpTpDT{P? zT(K<(blCAQ{Cwq;hA-QX2ASXQXXm|(%Ri}l(V)}io|16ajPUg%TJRtz0pv9CPl&W@ z=5*L!VN9&7Q=%?k^?U5ED|sa8!%W*m#Z#kAs%DGDeU}J~&8G8PC^a!g8LKl3mBiX= zf_V&d%~NXigwSRl?}XXp(Uv#*A3LIU_hvghM&5JD`jAneErikah854rHG3OI_h%#e kTCDdOcM08IwZ?8RRo_FgvR&vsVs-s*96TcpP0kjG^CIA2c literal 0 HcmV?d00001 diff --git a/tests/binaries/makefile b/tests/binaries/makefile new file mode 100644 index 0000000000..8b5e2c7b69 --- /dev/null +++ b/tests/binaries/makefile @@ -0,0 +1,50 @@ +CC = gcc +DEBUG = 1 +CFLAGS += -Wall +SOURCES = $(wildcard *.c) +COMPILED = $(SOURCES:.c=.o) +LINKED = $(SOURCES:.c=.out) + +NASM = nasm -f elf64 +LD = ld +SOURCES_ASM = $(wildcard *.asm) +COMPILED_ASM = $(SOURCES_ASM:.asm=.o) +LINKED_ASM = $(SOURCES_ASM:.asm=.out) +LDFLAGS = +EXTRA_FLAGS = +EXTRA_FLAGS_ASM = + +ifeq ($(TARGET), x86) +CFLAGS += -m32 +endif + +ifeq ($(DEBUG), 1) +CFLAGS += -DDEBUG=1 -ggdb -O0 +else +CFLAGS += -O1 +endif + + +.PHONY : all clean + +all: $(LINKED) $(LINKED_ASM) + + +%.out : %.c + @echo "[+] Building '$@'" + @$(CC) $(CFLAGS) $(EXTRA_FLAGS) -o $@ $? $(LDFLAGS) + +%.o : %.asm + @echo "[+] Building '$@'" + @$(NASM) $(EXTRA_FLAGS_ASM) -o $@ $? + +%.out : %.o + @echo "[+] Linking '$@'" + @$(LD) -o $@ $? + +clean : + @echo "[+] Cleaning stuff" + @rm -f $(COMPILED) $(LINKED) + + +reference-binary.out: EXTRA_FLAGS := -Dexample=1 diff --git a/tests/binaries/old_bash/__init__.py b/tests/binaries/old_bash/__init__.py new file mode 100644 index 0000000000..61861a3785 --- /dev/null +++ b/tests/binaries/old_bash/__init__.py @@ -0,0 +1,11 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import os + +path = os.path.dirname(__file__) + +def get(x): + return os.path.join(path, x) diff --git a/tests/corefiles/bash/binary b/tests/binaries/old_bash/binary similarity index 100% rename from tests/corefiles/bash/binary rename to tests/binaries/old_bash/binary diff --git a/tests/corefiles/bash/core b/tests/binaries/old_bash/core similarity index 100% rename from tests/corefiles/bash/core rename to tests/binaries/old_bash/core diff --git a/tests/binaries/reference-binary.c b/tests/binaries/reference-binary.c new file mode 100644 index 0000000000..4f53cd46d9 --- /dev/null +++ b/tests/binaries/reference-binary.c @@ -0,0 +1,8 @@ +#include + +void foo() {} + +int main() { + puts("Hello world"); + foo(); +} diff --git a/tests/binaries/reference-binary.out b/tests/binaries/reference-binary.out new file mode 100755 index 0000000000000000000000000000000000000000..73e937772e319d83adf5d9f3c374d3197a7539e6 GIT binary patch literal 10840 zcmeHNdvH|M89#R)$!0?~O9B``%Z);X+H4XYAwrRCcx`L|i@~B5F8jDiwrqBHcJD%B zJ0l}XO%NYycdzSS@6t5E#Vu|UB(->LO^2#D*{B$h4$&^EaJ^PE ziVDacsk7@lY=T<5e5hPA^(nm@l{OKPNF=*gl-(=Jj_Ifhj4Ahz`bNJV<mUE=xx~TTi*MEFJ*zWh%RjK6xU2$F zFN3azuC9!J-W2-Vq1Vgc`=`)Tf3Gcr7Z|5F4NIHI7OcE5jQC*G=uM`hspMTIM5;IP zRy1cB1JPt!Z0PRlTw^Q_EeA(Wd+j{jq>%%wo`SG7a=`lpXynEZBg1E`*$Cwh5)n$x z_OVmFq1t)(O8?tOD2fRg_DKRBSQe9R7F~vY6yF4~_jL>J_1VD1zM<)70Jqv#^d|TJ>AH!S27`p{+m2zmB zh~63=zSK2*p?lF8IakkLa-ZD*f06s%we;54xIf*)A9oL5SUdb)cx=Y&k$odtR~ENBqZP3vQuTHN$JqoUolMQDRTZSJ&6-$AW1NP61CUb!?j zb^`%aDUTOoKI#eI54=v;KyZDa;pW=vJ-)qS^_-QTSu$TfX2_o6-U*zKaH^2)o%qEN zyWBd$f#81EnwpAE1Oz?Re-*zj)Hi|(uD{c+C!a^~+lG3R>N^9$hh3e4hKJni0{Q_@ zXJFxj-bkQje?=tFzOQm)pd%M(4+mPpfrXs`9XQ}S13vkDISuo@@KbyrlFKXQ$_QK; zfh!|$Wd#2B5#YRx^Dwmyjptg^lY#4T{cGlnLjC$5cMYdkEus|K!)BtCZmh z)nC>JmCSJ;S2!fSvar8TDVcE>$L#)LT%`)znC$PU3JhDnlzH4omHd{f|47N)59R*< zM)>{H>2MZLgtOv5$eg!tShHq@zHn=-khTi?^3ak{OH*rGL5fQj*$VMl__F829<3E> zvKLO##$pVDJq=WUyC+99w|ipzabm16mIc1l1+saw)vwZ__QrR@ASV+Fuz8J75)B2 z10d@^O*KI&Sbn|AjN5V4ALOU;r>PIP_!@i5aHF%CvC#yCR;0alglxOi4;q_5qNi>JfpzeoD8VgO z0TotvN+WnG+L8L+Oh#yKz7zPdr5zF4bZtSPzS4aX)m`;i z$MZ#X^-ZWp=!*3ruv%!*G7&;mep(Rm)oboRbv4;)5UXm&9km1PSE9G8p2TWy2Hn!M zu6nYlX@s9xFa4rA=%Nu6=?O`m_}j0=Y^n$fk{h!uJbGo27dCY3NUy}fzU zlJ;phz&XxlBOl#uN<_Xtle2VsvgL7DWLUCxPcCVhM%DthHx=!}k&^Nmi{^1GG!V`0 zGIO>zXGRm!QJEehc-dYm6W=9ptkfslG2(@s5znLw18M1ZvC)bnCPLB?(`I9RSNA$2 zn<~J6HiJtZof)qXg>*jImo^i6ygzDpMcF6Qwz(Y~%&22*YIn$vzK|~D&BS8qQbtex zw1;8bIgN)rDl^jNAjZVVMiZ?zfgwptY_ilQ%S3a3X25Jt#GA8w(ur7Kvt{P3e6yVF zq?xySU*ev+YKJ8!ie{;8DUCmIMA1Q<)ya{M#&;F6h76w;1hjHm5XwLXtC29WCw2#S zTXcHI(@vFgwr%r?MFc0wl$p-Z`KZ38wbkBv^`-%*I-)Qbbf_^zgGE@PQ-E0b%=TRBWIt?5hw;iFkKGZ5{mp>=?s{k!7EtdY|AttyEtnI3Fz4 z`vvEXrTS{Y`E#ir8*G_8%PnezUREC%|DICnr*=FaFVzRdsM;w?_0!csRk0R30Zz6f zymJ&)xkdf>ca9=8@G}JG^`-im_|Bmht0eChvjpei#g%S>JxQJamg*Zsu&kb*@RN&M z@Y_adom=o*iBr#SBToG_r3xjv1!wmY)p)356}~?~zc{c3G)E>d?~` zM|ge@v%Lth9E>I=#(~UC8-4LyxRR*i$H44zSX&Mo-1e-ZUTthWZoeK(}=Eo$A~p#0D)65*ZW@N0#4 zW~wKkr}&-t*P+*y&th@``fH07il_InI=uZ)dalNER^BS~_9FS3_~K<)14f*_c_Gy> z5*ec}m5D`DMgm(}-iQ_kMLaW*O_`RN2rX~Bv8_CazP2TeXf78WLNa9KhD2{JI$#=! z!oa`~Oo}7}NUH=|CUY{3^;^Ol*BR?Jtu?TBmf9e#TYLMa@W!q+r8V*kA5_@Sjdc;_ zA+mOhFgA2=?hJPuo7b}ymDpdW(K}i z@(@w*Wxq|9ehn=RPlWPA1IU&@t(;B!nL`R_=CUG`Mt&0tcXl;d(LN>NT_6-IB=KQ7 znGhk8`lI=N5lRfD;m@YXDP_%WB+AK5xjNY6xtoF9Xq{)H6gY z3!sb~?vUB9)>3~0PFS){?L}oz1Vso7dH{*Fm6X6Weos$W$)| zgXiZSt6RYo+jAbtbUp(Wb=t3m{sPY6xP8tynR5KRojbS1O|Zw??s)vilq053s&?t| zOnqm2IuW398-92%9JlBEmFYbMDk{$L+krYdW90Zb&t*#a2o;W>aZGoij?OGu=6sl` zPR1zC_-(BacY&cXWqZz>nI2a_Zr|DedzF2s!f}4hl*fteXW{3xe+W7n6Se(~?-#sp zV8<^h&iNlkZL$6MK&gQB5^LHE?ZWh%5R2_~wo{bjWj8D{ecWNs`!CZUIVPOjWq;px z*z>tPQ$D|DytDsLDSMtj&M%lc+jH9g&|xocMruqY9BYX$Do%TOf`fW)pWjcI(gAC6 z;c;W#FTv0nr+bJB=au|_N=`h>OzB{^7C*M<_nqVFLkQ!@Hx-td{uw%2du-3|Mf`tY zqbh!0pKQnDO8GfK*q+~?j!{r35u1!(*`Db;s42D|&ofm6+zvZnJC;YmkdE8uye+5< zop{w9HW%l?k*;N2V>4_&-=SxFrk7FAd@*#XYTxPCiLHUcsUtrLW#7gfQ_h`O%IBRr zf*!_MCY1{|8NWKQ*H2-8aE5L2=D4NJIPI4xduKm0Gj046d}Am-Y+pm1IxT?xVkZ)|1l59>DKCWnDT6#oK2`Q@?z literal 0 HcmV?d00001 diff --git a/tests/common.py b/tests/common.py deleted file mode 100644 index 3340951029..0000000000 --- a/tests/common.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -import subprocess -import tempfile -import unittest - - -def pywrite(data): - return write(data, suffix='.py') - -def write(data, suffix=''): - t = tempfile.NamedTemporaryFile(delete=False, suffix=suffix) - t.write(data.encode('utf-8')) - return t - -def run_gdb_with_script(pybefore='', pyafter=''): - command = ['gdb','--silent','--nx','--nh'] - - if pybefore: - command += ['--command', pywrite(pybefore).name] - - command += ['--command', 'gdbinit.py'] - - if pyafter: - command += ['--command', pywrite(pyafter).name] - - command += ['--eval-command', 'quit'] - - return subprocess.check_output(command, stderr=subprocess.STDOUT) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000000..7d7ec427ee --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,30 @@ +""" +This file should consist of global test fixtures. +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import gdb +import pytest + +_start_binary_called = False + + +@pytest.fixture +def start_binary(): + """ + Returns function that launches given binary with 'start' command + """ + def _start_binary(path): + gdb.execute('file ' + path) + gdb.execute('start') + + global _start_binary_called + if _start_binary_called: + raise Exception('Starting more than one binary is not supported in pwndbg tests.') + + _start_binary_called = True + + yield _start_binary diff --git a/tests/testLoadsWithoutCrashing.py b/tests/testLoadsWithoutCrashing.py deleted file mode 100644 index 58552343ca..0000000000 --- a/tests/testLoadsWithoutCrashing.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import unicode_literals - -from . import common - - -def test_loads_wivout_crashing_bruv(): - output = common.run_gdb_with_script() - assert 'Type pwndbg [filter] for a list.' in output, output diff --git a/tests/test_emulate.py b/tests/test_emulate.py new file mode 100644 index 0000000000..3beaef408a --- /dev/null +++ b/tests/test_emulate.py @@ -0,0 +1,95 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import tests +from pwndbg.commands.nearpc import emulate +from pwndbg.commands.nearpc import nearpc +from pwndbg.commands.nearpc import pdisass +from pwndbg.commands.windbg import u + +EMULATE_DISASM_BINARY = tests.binaries.get('emulate_disasm.out') +EMULATE_DISASM_LOOP_BINARY = tests.binaries.get('emulate_disasm_loop.out') + + +def test_emulate_disasm(start_binary): + """ + Tests emulate command and its caching behavior + """ + start_binary(EMULATE_DISASM_BINARY) + + assert emulate(to_string=True) == [ + ' ► 0x400080 <_start> jmp label <0x400083>', + ' ↓', + ' 0x400083