diff --git a/.travis.yml b/.travis.yml index fc845131f..fe8853555 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,7 @@ addons: - mksh - zsh - pandoc + - gdb cache: - pip - directories: diff --git a/docs/source/gdb.rst b/docs/source/gdb.rst index 5cd137be2..044db1f48 100644 --- a/docs/source/gdb.rst +++ b/docs/source/gdb.rst @@ -1,3 +1,7 @@ +.. testsetup:: * + + from pwn import * + :mod:`pwnlib.gdb` --- Working with GDB ====================================== diff --git a/pwnlib/elf/elf.py b/pwnlib/elf/elf.py index 8f6f38ed6..d2edcc124 100644 --- a/pwnlib/elf/elf.py +++ b/pwnlib/elf/elf.py @@ -708,7 +708,7 @@ def bss(self, offset=0): return curr_bss + offset def __repr__(self): - return "ELF(%r)" % self.path + return "%s(%r)" % (self.__class__.__name__, self.path) def dynamic_by_tag(self, tag): dt = None diff --git a/pwnlib/gdb.py b/pwnlib/gdb.py index f00951b1e..a9d42f86b 100644 --- a/pwnlib/gdb.py +++ b/pwnlib/gdb.py @@ -5,6 +5,7 @@ import re import shlex import tempfile +import time from pwnlib import adb from pwnlib import atexit @@ -247,9 +248,25 @@ def get_gdb_arch(): 'thumb': 'arm' }.get(context.arch, context.arch) +def binary(): + gdb = misc.which('gdb') + + if not context.native: + multiarch = misc.which('gdb-multiarch') + + if multiarch: + return multiarch + log.warn_once('Cross-architecture debugging usually requires gdb-multiarch\n' \ + '$ apt-get install gdb-multiarch') + + if not gdb: + log.error('GDB is not installed\n' + '$ apt-get install gdb') + + return gdb @LocalContext -def attach(target, execute = None, exe = None, need_ptrace_scope = True): +def attach(target, execute = None, exe = None, need_ptrace_scope = True, gdb_args = None): """attach(target, execute = None, exe = None, arch = None) -> None Start GDB in a new terminal and attach to `target`. @@ -266,15 +283,16 @@ def attach(target, execute = None, exe = None, need_ptrace_scope = True): If `gdb-multiarch` is installed we use that or 'gdb' otherwise. Arguments: - target: The target to attach to. - execute (str or file): GDB script to run after attaching. - exe (str): The path of the target binary. - arch (str): Architechture of the target binary. If `exe` known GDB will - detect the architechture automatically (if it is supported). + target: The target to attach to. + execute (str or file): GDB script to run after attaching. + exe(str): The path of the target binary. + arch(str): Architechture of the target binary. If `exe` known GDB will + detect the architechture automatically (if it is supported). + gdb_args(list): List of arguments to pass to GDB. Returns: - :const:`None` -""" + PID of the GDB process, or the window which it is running in. + """ if context.noptrace: log.warn_once("Skipping debug attach since context.noptrace==True") return @@ -294,28 +312,11 @@ def attach(target, execute = None, exe = None, need_ptrace_scope = True): # gdb script to run before `execute` pre = '' if not context.native: - if not misc.which('gdb-multiarch'): - log.warn_once('Cross-architecture debugging usually requires gdb-multiarch\n' \ - '$ apt-get install gdb-multiarch') pre += 'set endian %s\n' % context.endian pre += 'set architecture %s\n' % get_gdb_arch() if context.os == 'android': pre += 'set gnutarget ' + _bfdname() + '\n' - else: - # If ptrace_scope is set and we're not root, we cannot attach to a - # running process. - # We assume that we do not need this to be set if we are debugging on - # a different architecture (e.g. under qemu-user). - try: - ptrace_scope = open('/proc/sys/kernel/yama/ptrace_scope').read().strip() - if need_ptrace_scope and ptrace_scope != '0' and os.geteuid() != 0: - msg = 'Disable ptrace_scope to attach to running processes.\n' - msg += 'More info: https://askubuntu.com/q/41629' - log.warning(msg) - return - except IOError: - pass # let's see if we can find a pid to attach to pid = None @@ -392,13 +393,11 @@ def findexe(): if not pid and not exe: log.error('could not find target process') - cmd = None - for p in ('gdb-multiarch', 'gdb'): - if misc.which(p): - cmd = p - break - else: - log.error('no gdb installed') + cmd = binary() + + if gdb_args: + cmd += ' ' + cmd += ' '.join(gdb_args) cmd += ' -q ' @@ -432,10 +431,13 @@ def findexe(): cmd += ' -x "%s"' % (tmp.name) log.info('running in new terminal: %s' % cmd) - misc.run_in_new_terminal(cmd) + + gdb_pid = misc.run_in_new_terminal(cmd) + if pid and context.native: proc.wait_for_debugger(pid) - return pid + + return gdb_pid def ssh_gdb(ssh, process, execute = None, arch = None, **kwargs): if isinstance(process, (list, tuple)): @@ -591,3 +593,70 @@ def find_module_addresses(binary, ssh=None, ulimit=False): rv.append(lib) return rv + +def corefile(process): + r"""Drops a core file for the process. + + Arguments: + process: Process to dump + + Returns: + A ``pwnlib.elf.corefile.Core`` object. + """ + + if context.noptrace: + log.warn_once("Skipping corefile since context.noptrace==True") + return + + temp = tempfile.NamedTemporaryFile(prefix='pwn-corefile-') + + # Due to https://sourceware.org/bugzilla/show_bug.cgi?id=16092 + # will disregard coredump_filter, and will not dump private mappings. + if version() < (7,11): + log.warn_once('The installed GDB (%s) does not emit core-dumps which ' + 'contain all of the data in the process.\n' + 'Upgrade to GDB >= 7.11 for better core-dumps.' % binary()) + + # This is effectively the same as what the 'gcore' binary does + gdb_args = ['-batch', + '-q', + '--nx', + '-ex', '"set pagination off"', + '-ex', '"set height 0"', + '-ex', '"set width 0"', + '-ex', '"set use-coredump-filter on"', + '-ex', '"generate-core-file %s"' % temp.name, + '-ex', 'detach'] + + with context.local(terminal = ['sh', '-c']): + with context.quiet: + pid = attach(process, gdb_args=gdb_args) + os.waitpid(pid, 0) + + return elf.corefile.Core(temp.name) + +def version(program='gdb'): + """Gets the current GDB version. + + Note: + Requires that GDB version meets the following format: + + ``GNU gdb (GDB) 7.12`` + + Returns: + A tuple + + Example: + + >>> (7,0) <= gdb.version() <= (8,0) + True + """ + program = misc.which(program) + expr = r'([0-9]+\.?)+' + + with tubes.process.process([program, '--version'], level='error') as gdb: + version = gdb.recvline() + + versions = re.search(expr, version).group() + + return tuple(map(int, versions.split('.'))) diff --git a/pwnlib/tubes/process.py b/pwnlib/tubes/process.py index 9f5ee4861..92c03c4b0 100644 --- a/pwnlib/tubes/process.py +++ b/pwnlib/tubes/process.py @@ -278,6 +278,7 @@ def __init__(self, argv = None, self.preexec_fn = preexec_fn self.display = display or self.program + self.__qemu = False message = "Starting %s process %r" % (where, self.display) @@ -350,7 +351,7 @@ def __preexec_fn(self): ctypes.CDLL('libc.so.6').personality(ADDR_NO_RANDOMIZE) resource.setrlimit(resource.RLIMIT_STACK, (-1, -1)) - except: + except Exception: self.exception("Could not disable ASLR") # Assume that the user would prefer to have core dumps. @@ -367,9 +368,18 @@ def __preexec_fn(self): try: PR_SET_NO_NEW_PRIVS = 38 ctypes.CDLL('libc.so.6').prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) - except: + except Exception: pass + # Avoid issues with attaching to processes when yama-ptrace is set + try: + PR_SET_PTRACER = 0x59616d61 + PR_SET_PTRACER_ANY = -1 + ctypes.CDLL('libc.so.6').prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0) + except Exception: + pass + + if self.alarm is not None: signal.alarm(self.alarm) @@ -400,6 +410,7 @@ def __on_enoexec(self, exception): if self.argv: args += ['-0', self.argv[0]] args += ['--'] + self.__qemu = True return [args, qemu] # If we get here, we couldn't run the binary directly, and @@ -466,7 +477,9 @@ def _validate(self, cwd, executable, argv, env): # Either there is a path component, or the binary is not in $PATH # For example, 'foo/bar' or 'bar' with cwd=='foo' elif os.path.sep not in executable: + tmp = executable executable = os.path.join(cwd, executable) + log.warn_once("Could not find executable %r in $PATH, using %r instead" % (tmp, executable)) if not os.path.exists(executable): self.error("%r does not exist" % executable) @@ -543,7 +556,6 @@ def kill(self): Kills the process. """ - self.close() def poll(self, block = False): @@ -786,20 +798,43 @@ def libc(self): e.address = address return e + @property + def elf(self): + """elf() -> pwnlib.elf.elf.ELF + + Returns an ELF file for the executable that launched the process. + """ + import pwnlib.elf + return pwnlib.elf.elf.ELF(self.executable) + @property def corefile(self): - filename = 'core.%i' % (self.pid) - process(['gcore', '-o', 'core', str(self.pid)]).wait() + """corefile() -> pwnlib.elf.elf.Core - import pwnlib.elf.corefile - return pwnlib.elf.corefile.Core(filename) + Returns a corefile for the process. + + Example: + + >>> proc = process('bash') + >>> isinstance(proc.corefile, pwnlib.elf.corefile.Core) + True + """ + import pwnlib.gdb + return pwnlib.gdb.corefile(self) def leak(self, address, count=1): - """Leaks memory within the process at the specified address. + r"""Leaks memory within the process at the specified address. Arguments: address(int): Address to leak memory at count(int): Number of bytes to leak at that address. + + Example: + + >>> e = ELF('/bin/sh') + >>> p = process(e.path) + >>> p.leak(e.address, 4) + '\x7fELF' """ # If it's running under qemu-user, don't leak anything. if 'qemu-' in os.path.realpath('/proc/%i/exe' % self.pid): diff --git a/pwnlib/tubes/ssh.py b/pwnlib/tubes/ssh.py index 7a34f0d26..d0398c627 100644 --- a/pwnlib/tubes/ssh.py +++ b/pwnlib/tubes/ssh.py @@ -852,6 +852,13 @@ def is_exe(path): sys.stdout.write("Could not disable setuid: prctl(PR_SET_NO_NEW_PRIVS) failed") sys.exit(-1) +try: + PR_SET_PTRACER = 0x59616d61 + PR_SET_PTRACER_ANY = -1 + ctypes.CDLL('libc.so.6').prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0) +except Exception: + pass + if sys.argv[-1] == 'check': sys.stdout.write("1\n") sys.stdout.write(str(os.getpid()) + "\n") diff --git a/pwnlib/util/misc.py b/pwnlib/util/misc.py index 6d9a94717..98c615e25 100644 --- a/pwnlib/util/misc.py +++ b/pwnlib/util/misc.py @@ -174,27 +174,35 @@ def run_in_new_terminal(command, terminal = None, args = None): Run a command in a new terminal. - When `terminal` is not set: - - If `context.terminal` is set it will be used. If it is an iterable then - `context.terminal[1:]` are default arguments. - - If X11 is detected (by the presence of the ``DISPLAY`` environment - variable), ``x-terminal-emulator`` is used. - - If tmux is detected (by the presence of the ``TMUX`` environment - variable), a new pane will be opened. + When ``terminal`` is not set: + - If ``context.terminal`` is set it will be used. + If it is an iterable then ``context.terminal[1:]`` are default arguments. + - If a ``pwntools-terminal`` command exists in ``$PATH``, it is used + - If ``$TERM_PROGRAM`` is set, that is used. + - If X11 is detected (by the presence of the ``$DISPLAY`` environment + variable), ``x-terminal-emulator`` is used. + - If tmux is detected (by the presence of the ``$TMUX`` environment + variable), a new pane will be opened. Arguments: - command (str): The command to run. - terminal (str): Which terminal to use. - args (list): Arguments to pass to the terminal + command (str): The command to run. + terminal (str): Which terminal to use. + args (list): Arguments to pass to the terminal + + Note: + The command is opened with ``/dev/null`` for stdin, stdout, stderr. Returns: - None + PID of the new terminal process """ if not terminal: if context.terminal: terminal = context.terminal[0] args = context.terminal[1:] + elif which('pwntools-terminal'): + terminal = 'pwntools-terminal' + args = [] elif 'DISPLAY' in os.environ: terminal = 'x-terminal-emulator' args = ['-e'] @@ -223,15 +231,20 @@ def run_in_new_terminal(command, terminal = None, args = None): log.debug("Launching a new terminal: %r" % argv) - if os.fork() == 0: + pid = os.fork() + + if pid == 0: # Closing the file descriptors makes everything fail under tmux on OSX. if platform.system() != 'Darwin': - os.close(0) - os.close(1) - os.close(2) + devnull = open(os.devnull, 'rwb') + os.dup2(devnull.fileno(), 0) + os.dup2(devnull.fileno(), 1) + os.dup2(devnull.fileno(), 2) os.execv(argv[0], argv) os._exit(1) + return pid + def parse_ldd_output(output): """Parses the output from a run of 'ldd' on a binary. Returns a dictionary of {path: address} for