def wait_for_debugger(pid, debugger_pid=None): """wait_for_debugger(pid, debugger_pid=None) -> None Sleeps until the process with PID `pid` is being traced. If debugger_pid is set and debugger exits, raises an error. Arguments: pid (int): PID of the process. Returns: None """ t = Timeout() with t.countdown(timeout=15): with log.waitfor('Waiting for debugger') as l: if debugger_pid: debugger = psutil.Process(debugger_pid) while t.timeout and tracer(pid) is None: try: debugger.wait(0.01) except psutil.TimeoutExpired: pass else: l.failure( "debugger exited! (maybe check /proc/sys/kernel/yama/ptrace_scope)" ) else: while t.timeout and tracer(pid) is None: time.sleep(0.01) if tracer(pid): l.success() else: l.failure('Debugger did not attach to pid %d within 15 seconds', pid)
def corefile(self): """corefile() -> pwnlib.elf.elf.Core Returns a corefile for the process. If the process is alive, attempts to create a coredump with GDB. If the process is dead, attempts to locate the coredump created by the kernel. """ # If the process is still alive, try using GDB import pwnlib.elf.corefile import pwnlib.gdb try: if self.poll() is None: corefile = pwnlib.gdb.corefile(self) if corefile is None: self.error("Could not create corefile with GDB for %s", self.executable) return corefile # Handle race condition against the kernel or QEMU to write the corefile # by waiting up to 5 seconds for it to be written. t = Timeout() finder = None with t.countdown(5): while t.timeout and (finder is None or not finder.core_path): finder = pwnlib.elf.corefile.CorefileFinder(self) time.sleep(0.5) if not finder.core_path: self.error("Could not find core file for pid %i" % self.pid) core_hash = sha256file(finder.core_path) if self._corefile and self._corefile._hash == core_hash: return self._corefile self._corefile = pwnlib.elf.corefile.Corefile(finder.core_path) except AttributeError as e: raise RuntimeError( e ) # AttributeError would route through __getattr__, losing original message self._corefile._hash = core_hash return self._corefile
def attach(target, gdbscript='', exe=None, gdb_args=None, ssh=None, sysroot=None, r2cmd=None): r""" Start GDB in a new terminal and attach to `target`. Arguments: target: The target to attach to. gdbscript(:obj:`str` or :obj:`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 additional arguments to pass to GDB. sysroot(str): Foreign-architecture sysroot, used for QEMU-emulated binaries and Android targets. Returns: PID of the GDB process (or the window which it is running in). Notes: The ``target`` argument is very robust, and can be any of the following: :obj:`int` PID of a process :obj:`str` Process name. The youngest process is selected. :obj:`tuple` Host, port pair of a listening ``gdbserver`` :class:`.process` Process to connect to :class:`.sock` Connected socket. The executable on the other end of the connection is attached to. Can be any socket type, including :class:`.listen` or :class:`.remote`. :class:`.ssh_channel` Remote process spawned via :meth:`.ssh.process`. This will use the GDB installed on the remote machine. If a password is required to connect, the ``sshpass`` program must be installed. Examples: Attach to a process by PID >>> pid = gdb.attach(1234) # doctest: +SKIP Attach to the youngest process by name >>> pid = gdb.attach('bash') # doctest: +SKIP Attach a debugger to a :class:`.process` tube and automate interaction >>> io = process('bash') >>> pid = gdb.attach(io, gdbscript=''' ... call puts("Hello from process debugger!") ... detach ... quit ... ''') >>> io.recvline() b'Hello from process debugger!\n' >>> io.sendline('echo Hello from bash && exit') >>> io.recvall() b'Hello from bash\n' Attach to the remote process from a :class:`.remote` or :class:`.listen` tube, as long as it is running on the same machine. >>> server = process(['socat', 'tcp-listen:12345,reuseaddr,fork', 'exec:/bin/bash,nofork']) >>> sleep(1) # Wait for socat to start >>> io = remote('127.0.0.1', 12345) >>> sleep(1) # Wait for process to fork >>> pid = gdb.attach(io, gdbscript=''' ... call puts("Hello from remote debugger!") ... detach ... quit ... ''') >>> io.recvline() b'Hello from remote debugger!\n' >>> io.sendline('echo Hello from bash && exit') >>> io.recvall() b'Hello from bash\n' Attach to processes running on a remote machine via an SSH :class:`.ssh` process >>> shell = ssh('travis', 'example.pwnme', password='******') >>> io = shell.process(['cat']) >>> pid = gdb.attach(io, gdbscript=''' ... call sleep(5) ... call puts("Hello from ssh debugger!") ... detach ... quit ... ''') >>> io.recvline(timeout=5) # doctest: +SKIP b'Hello from ssh debugger!\n' >>> io.sendline('This will be echoed back') >>> io.recvline() b'This will be echoed back\n' >>> io.close() """ if context.noptrace: log.warn_once("Skipping debug attach since context.noptrace==True") return # if gdbscript is a file object, then read it; we probably need to run some # more gdb script anyway if hasattr(gdbscript, 'read'): with gdbscript: gdbscript = gdbscript.read() # enable gdb.attach(p, 'continue') if gdbscript and not gdbscript.endswith('\n'): gdbscript += '\n' # Use a sane default sysroot for Android if not sysroot and context.os == 'android': sysroot = 'remote:/' # gdb script to run before `gdbscript` pre = '' if not context.native: pre += 'set endian %s\n' % context.endian pre += 'set architecture %s\n' % get_gdb_arch() if sysroot: pre += 'set sysroot %s\n' % sysroot if context.os == 'android': pre += 'set gnutarget ' + _bfdname() + '\n' if exe: pre += 'file %s\n' % exe # let's see if we can find a pid to attach to pid = None if isinstance(target, six.integer_types): # target is a pid, easy peasy pid = target elif isinstance(target, str): # pidof picks the youngest process pidof = proc.pidof if context.os == 'android': pidof = adb.pidof pids = list(pidof(target)) if not pids: log.error('No such process: %s' % target) pid = pids[0] log.info('Attaching to youngest process "%s" (PID = %d)' % (target, pid)) elif isinstance(target, tubes.ssh.ssh_channel): if not target.pid: log.error("PID unknown for channel") shell = target.parent tmpfile = shell.mktemp() if six.PY3: tmpfile = tmpfile.decode() gdbscript = 'shell rm %s\n%s' % (tmpfile, gdbscript) shell.upload_data(gdbscript or '', tmpfile) cmd = [ 'ssh', '-C', '-t', '-p', str(shell.port), '-l', shell.user, shell.host ] if shell.password: if not misc.which('sshpass'): log.error("sshpass must be installed to debug ssh processes") cmd = ['sshpass', '-p', shell.password] + cmd if shell.keyfile: cmd += ['-i', shell.keyfile] exefile = target.executable cmd += ['gdb -q %s %s -x "%s"' % (exefile, target.pid, tmpfile)] misc.run_in_new_terminal(' '.join(cmd)) return elif isinstance(target, tubes.sock.sock): pids = proc.pidof(target) if not pids: log.error('Could not find remote process (%s:%d) on this machine' % target.sock.getpeername()) pid = pids[0] # Specifically check for socat, since it has an intermediary process # if you do not specify "nofork" to the EXEC: argument # python(2640)───socat(2642)───socat(2643)───bash(2644) if proc.exe(pid).endswith('/socat') and time.sleep( 0.1) and proc.children(pid): pid = proc.children(pid)[0] # We may attach to the remote process after the fork but before it performs an exec. # If an exe is provided, wait until the process is actually running the expected exe # before we attach the debugger. t = Timeout() with t.countdown(2): while exe and os.realpath( proc.exe(pid)) != os.realpath(exe) and t.timeout: time.sleep(0.1) elif isinstance(target, tubes.process.process): pid = proc.pidof(target)[0] exe = exe or target.executable elif isinstance(target, tuple) and len(target) == 2: host, port = target if context.os != 'android': pre += 'target remote %s:%d\n' % (host, port) else: # Android debugging is done over gdbserver, which can't follow # new inferiors (tldr; follow-fork-mode child) unless it is run # in extended-remote mode. pre += 'target extended-remote %s:%d\n' % (host, port) pre += 'set detach-on-fork off\n' def findexe(): for spid in proc.pidof(target): sexe = proc.exe(spid) name = os.path.basename(sexe) # XXX: parse cmdline if name.startswith('qemu-') or name.startswith('gdbserver'): exe = proc.cmdline(spid)[-1] return os.path.join(proc.cwd(spid), exe) exe = exe or findexe() elif isinstance(target, elf.corefile.Corefile): pre += 'target core %s\n' % target.path else: log.error("don't know how to attach to target: %r" % target) # if we have a pid but no exe, just look it up in /proc/ if pid and not exe: exe_fn = proc.exe if context.os == 'android': exe_fn = adb.proc_exe exe = exe_fn(pid) if not pid and not exe and not ssh: log.error('could not find target process') cmd = binary() cmd = "" if gdb_args: cmd += ' ' cmd += ' '.join(gdb_args) if context.gdbinit: cmd += ' -nh ' # ignore ~/.gdbinit cmd += ' -x %s ' % context.gdbinit # load custom gdbinit cmd += ' -q ' if exe and context.native: if not ssh and not os.path.isfile(exe): log.error('No such file: %s' % exe) cmd += ' "%s"' % exe if pid and not context.os == 'android': cmd += ' %d' % pid if context.os == 'android' and pid: runner = _get_runner() which = _get_which() gdb_cmd = _gdbserver_args(pid=pid, which=which, env=env) gdbserver = runner(gdb_cmd) port = _gdbserver_port(gdbserver, None) host = context.adb_host pre += 'target extended-remote %s:%i\n' % (context.adb_host, port) # gdbserver on Android sets 'detach-on-fork on' which breaks things # when you're trying to debug anything that forks. pre += 'set detach-on-fork off\n' gdbscript = pre + (gdbscript or '') if gdbscript: tmp = tempfile.NamedTemporaryFile(prefix='pwn', suffix='.gdb', delete=False, mode='w+') log.debug('Wrote gdb script to %r\n%s' % (tmp.name, gdbscript)) gdbscript = 'shell rm %s\n%s' % (tmp.name, gdbscript) tmp.write(gdbscript) tmp.close() cmd += ' -x %s' % (tmp.name) #cmd = "r2 -d {}".format(pid) if r2cmd != None: cmd = "r2 -c '{}' -d {}".format(r2cmd, pid) else: cmd = "r2 -d {}".format(pid) print("Command: {}".format(cmd)) log.info('running in new terminal: %s' % cmd) gdb_pid = misc.run_in_new_terminal(cmd) if pid and context.native: proc.wait_for_debugger(pid, gdb_pid) return gdb_pid
def attach(target, gdbscript='', exe=None, gdb_args=None, ssh=None, sysroot=None, api=False): r""" Start GDB in a new terminal and attach to `target`. Arguments: target: The target to attach to. gdbscript(:obj:`str` or :obj:`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 additional arguments to pass to GDB. sysroot(str): Foreign-architecture sysroot, used for QEMU-emulated binaries and Android targets. api(bool): Enable access to GDB Python API. Returns: PID of the GDB process (or the window which it is running in). When ``api=True``, a (PID, :class:`Gdb`) tuple. Notes: The ``target`` argument is very robust, and can be any of the following: :obj:`int` PID of a process :obj:`str` Process name. The youngest process is selected. :obj:`tuple` Host, port pair of a listening ``gdbserver`` :class:`.process` Process to connect to :class:`.sock` Connected socket. The executable on the other end of the connection is attached to. Can be any socket type, including :class:`.listen` or :class:`.remote`. :class:`.ssh_channel` Remote process spawned via :meth:`.ssh.process`. This will use the GDB installed on the remote machine. If a password is required to connect, the ``sshpass`` program must be installed. Examples: Attach to a process by PID >>> pid = gdb.attach(1234) # doctest: +SKIP Attach to the youngest process by name >>> pid = gdb.attach('bash') # doctest: +SKIP Attach a debugger to a :class:`.process` tube and automate interaction >>> io = process('bash') >>> pid = gdb.attach(io, gdbscript=''' ... call puts("Hello from process debugger!") ... detach ... quit ... ''') >>> io.recvline() b'Hello from process debugger!\n' >>> io.sendline(b'echo Hello from bash && exit') >>> io.recvall() b'Hello from bash\n' Using GDB Python API: .. doctest :skipif: six.PY2 >>> io = process('bash') Attach a debugger >>> pid, io_gdb = gdb.attach(io, api=True) Force the program to write something it normally wouldn't >>> io_gdb.execute('call puts("Hello from process debugger!")') Resume the program >>> io_gdb.continue_nowait() Observe the forced line >>> io.recvline() b'Hello from process debugger!\n' Interact with the program in a regular way >>> io.sendline(b'echo Hello from bash && exit') Observe the results >>> io.recvall() b'Hello from bash\n' Attach to the remote process from a :class:`.remote` or :class:`.listen` tube, as long as it is running on the same machine. >>> server = process(['socat', 'tcp-listen:12345,reuseaddr,fork', 'exec:/bin/bash,nofork']) >>> sleep(1) # Wait for socat to start >>> io = remote('127.0.0.1', 12345) >>> sleep(1) # Wait for process to fork >>> pid = gdb.attach(io, gdbscript=''' ... call puts("Hello from remote debugger!") ... detach ... quit ... ''') >>> io.recvline() b'Hello from remote debugger!\n' >>> io.sendline(b'echo Hello from bash && exit') >>> io.recvall() b'Hello from bash\n' Attach to processes running on a remote machine via an SSH :class:`.ssh` process >>> shell = ssh('travis', 'example.pwnme', password='******') >>> io = shell.process(['cat']) >>> pid = gdb.attach(io, gdbscript=''' ... call sleep(5) ... call puts("Hello from ssh debugger!") ... detach ... quit ... ''') >>> io.recvline(timeout=5) # doctest: +SKIP b'Hello from ssh debugger!\n' >>> io.sendline(b'This will be echoed back') >>> io.recvline() b'This will be echoed back\n' >>> io.close() """ if context.noptrace: log.warn_once("Skipping debug attach since context.noptrace==True") return # if gdbscript is a file object, then read it; we probably need to run some # more gdb script anyway if hasattr(gdbscript, 'read'): with gdbscript: gdbscript = gdbscript.read() # enable gdb.attach(p, 'continue') if gdbscript and not gdbscript.endswith('\n'): gdbscript += '\n' # Use a sane default sysroot for Android if not sysroot and context.os == 'android': sysroot = 'remote:/' # gdb script to run before `gdbscript` pre = '' if not context.native: pre += 'set endian %s\n' % context.endian pre += 'set architecture %s\n' % get_gdb_arch() if sysroot: pre += 'set sysroot %s\n' % sysroot if context.os == 'android': pre += 'set gnutarget ' + _bfdname() + '\n' if exe and context.os != 'baremetal': pre += 'file "%s"\n' % exe # let's see if we can find a pid to attach to pid = None if isinstance(target, six.integer_types): # target is a pid, easy peasy pid = target elif isinstance(target, str): # pidof picks the youngest process pidof = proc.pidof if context.os == 'android': pidof = adb.pidof pids = list(pidof(target)) if not pids: log.error('No such process: %s', target) pid = pids[0] log.info('Attaching to youngest process "%s" (PID = %d)' % (target, pid)) elif isinstance(target, tubes.ssh.ssh_channel): if not target.pid: log.error("PID unknown for channel") shell = target.parent tmpfile = shell.mktemp() gdbscript = b'shell rm %s\n%s' % ( tmpfile, packing._need_bytes(gdbscript, 2, 0x80)) shell.upload_data(gdbscript or b'', tmpfile) cmd = [ 'ssh', '-C', '-t', '-p', str(shell.port), '-l', shell.user, shell.host ] if shell.password: if not misc.which('sshpass'): log.error("sshpass must be installed to debug ssh processes") cmd = ['sshpass', '-p', shell.password] + cmd if shell.keyfile: cmd += ['-i', shell.keyfile] cmd += ['gdb', '-q', target.executable, str(target.pid), '-x', tmpfile] misc.run_in_new_terminal(cmd) return elif isinstance(target, tubes.sock.sock): pids = proc.pidof(target) if not pids: log.error('Could not find remote process (%s:%d) on this machine' % target.sock.getpeername()) pid = pids[0] # Specifically check for socat, since it has an intermediary process # if you do not specify "nofork" to the EXEC: argument # python(2640)───socat(2642)───socat(2643)───bash(2644) if proc.exe(pid).endswith('/socat') and time.sleep( 0.1) and proc.children(pid): pid = proc.children(pid)[0] # We may attach to the remote process after the fork but before it performs an exec. # If an exe is provided, wait until the process is actually running the expected exe # before we attach the debugger. t = Timeout() with t.countdown(2): while exe and os.path.realpath( proc.exe(pid)) != os.path.realpath(exe) and t.timeout: time.sleep(0.1) elif isinstance(target, tubes.process.process): pid = proc.pidof(target)[0] exe = exe or target.executable elif isinstance(target, tuple) and len(target) == 2: host, port = target if context.os != 'android': pre += 'target remote %s:%d\n' % (host, port) else: # Android debugging is done over gdbserver, which can't follow # new inferiors (tldr; follow-fork-mode child) unless it is run # in extended-remote mode. pre += 'target extended-remote %s:%d\n' % (host, port) pre += 'set detach-on-fork off\n' def findexe(): for spid in proc.pidof(target): sexe = proc.exe(spid) name = os.path.basename(sexe) # XXX: parse cmdline if name.startswith('qemu-') or name.startswith('gdbserver'): exe = proc.cmdline(spid)[-1] return os.path.join(proc.cwd(spid), exe) exe = exe or findexe() elif isinstance(target, elf.corefile.Corefile): pre += 'target core "%s"\n' % target.path else: log.error("don't know how to attach to target: %r", target) # if we have a pid but no exe, just look it up in /proc/ if pid and not exe: exe_fn = proc.exe if context.os == 'android': exe_fn = adb.proc_exe exe = exe_fn(pid) if not pid and not exe and not ssh: log.error('could not find target process') gdb_binary = binary() cmd = [gdb_binary] if gdb_args: cmd += gdb_args if context.gdbinit: cmd += ['-nh'] # ignore ~/.gdbinit cmd += ['-x', context.gdbinit] # load custom gdbinit cmd += ['-q'] if exe and context.native: if not ssh and not os.path.isfile(exe): log.error('No such file: %s', exe) cmd += [exe] if pid and not context.os == 'android': cmd += [str(pid)] if context.os == 'android' and pid: runner = _get_runner() which = _get_which() gdb_cmd = _gdbserver_args(pid=pid, which=which) gdbserver = runner(gdb_cmd) port = _gdbserver_port(gdbserver, None) host = context.adb_host pre += 'target extended-remote %s:%i\n' % (context.adb_host, port) # gdbserver on Android sets 'detach-on-fork on' which breaks things # when you're trying to debug anything that forks. pre += 'set detach-on-fork off\n' if api: # create a UNIX socket for talking to GDB socket_dir = tempfile.mkdtemp() socket_path = os.path.join(socket_dir, 'socket') bridge = os.path.join(os.path.dirname(__file__), 'gdb_api_bridge.py') # inject the socket path and the GDB Python API bridge pre = 'python socket_path = ' + repr(socket_path) + '\n' + \ 'source ' + bridge + '\n' + \ pre gdbscript = pre + (gdbscript or '') if gdbscript: tmp = tempfile.NamedTemporaryFile(prefix='pwn', suffix='.gdb', delete=False, mode='w+') log.debug('Wrote gdb script to %r\n%s', tmp.name, gdbscript) gdbscript = 'shell rm %s\n%s' % (tmp.name, gdbscript) tmp.write(gdbscript) tmp.close() cmd += ['-x', tmp.name] log.info('running in new terminal: %s', cmd) if api: # prevent gdb_faketerminal.py from messing up api doctests def preexec_fn(): os.environ['GDB_FAKETERMINAL'] = '0' else: preexec_fn = None gdb_pid = misc.run_in_new_terminal(cmd, preexec_fn=preexec_fn) if pid and context.native: proc.wait_for_debugger(pid, gdb_pid) if not api: return gdb_pid # connect to the GDB Python API bridge from rpyc import BgServingThread from rpyc.utils.factory import unix_connect if six.PY2: retriable = socket.error else: retriable = ConnectionRefusedError, FileNotFoundError t = Timeout() with t.countdown(10): while t.timeout: try: conn = unix_connect(socket_path) break except retriable: time.sleep(0.1) else: # Check to see if RPyC is installed at all in GDB rpyc_check = [ gdb_binary, '--nx', '-batch', '-ex', 'python import rpyc; import sys; sys.exit(123)' ] if 123 != tubes.process.process(rpyc_check).poll(block=True): log.error('Failed to connect to GDB: rpyc is not installed') # Check to see if the socket ever got created if not os.path.exists(socket_path): log.error( 'Failed to connect to GDB: Unix socket %s was never created', socket_path) # Check to see if the remote RPyC client is a compatible version version_check = [ gdb_binary, '--nx', '-batch', '-ex', 'python import platform; print(platform.python_version())' ] gdb_python_version = tubes.process.process( version_check).recvall().strip() python_version = str(platform.python_version()) if gdb_python_version != python_version: log.error( 'Failed to connect to GDB: Version mismatch (%s vs %s)', gdb_python_version, python_version) # Don't know what happened log.error('Failed to connect to GDB: Unknown error') # now that connection is up, remove the socket from the filesystem os.unlink(socket_path) os.rmdir(socket_dir) # create a thread for receiving breakpoint notifications BgServingThread(conn, callback=lambda: None) return gdb_pid, Gdb(conn)