def unix_connect(path): """ Creates a socket connection to the given host and port. :param path: the path to the unix domain socket :returns: an RPyC connection exposing ``SlaveService`` """ return factory.unix_connect(path, SlaveService)
def discover(self, service): from rpyc.utils.factory import unix_connect, connect if self.discoverer is None or self.discoverer.closed: try: if self.path: self.discoverer = unix_connect(self.path) else: self.discoverer = connect(self.host, self.port) except: raise DiscoveryError('Discovery service not found.') info = dict(self.discoverer.root.discover(service)) if info is not None: if 'socket_path' in info: return unix_connect(info.get('socket_path')) else: return connect(info.get('hostname'), info.get('port')) else: raise DiscoveryError('Service discovery failed.')
def unix_connect(path): """ Creates a socket connection to the given host and port. :param path: the path to the unix domain socket :returns: an RPyC connection exposing ``SlaveService`` """ return factory.unix_connect(path, SlaveService)
def discover(self, service): from rpyc.utils.factory import unix_connect, connect if self.discoverer is None or self.discoverer.closed: try: if self.path: self.discoverer = unix_connect(self.path) else: self.discoverer = connect(self.host, self.port) except: raise DiscoveryError('Discovery service not found.') info = dict(self.discoverer.root.discover(service)) if info is not None: if 'socket_path' in info: return unix_connect(info.get('socket_path')) else: return connect(info.get('hostname'), info.get('port')) else: raise DiscoveryError('Service discovery failed.')
def client(socket_path, first, second, *, wait=False): while True: try: conn = unix_connect(socket_path) except (ConnectionRefusedError, FileNotFoundError): if wait: continue raise else: break print(conn.root.similarity(version, first, second))
def from_unix_socket(cls, path: Path) -> "Glue": """This establishes a connection to the server using a path that uses unix sockets. Args: path: A path that uses unix sockets. Returns: A "glue" object with a connection established from a unix socket. """ conn = unix_connect(str(path)) return cls(conn)
def kill_server(socket_path): try: conn = unix_connect(socket_path) except (ConnectionRefusedError, FileNotFoundError): print("Could not connect to server", file=sys.stderr) sys.exit(1) try: conn.root.kill() except EOFError: # Expected, if we kill the server. pass
def gdb_api_client(address, machine, *, gdb_args=None, expose_extra=None): raise Exception("This is currently broken") if gdb_args is None: gdb_args = [] if expose_extra is None: expose_extra = [] socket_dir_path = Path(mkdtemp()) socket_path = socket_dir_path / "socket" gdb_api_bridge_path = Path(__file__).parent / "gdb_api_bridge.py" host, port = address args = [ "gdb", *("-ex", f"python socket_path = {repr(str(socket_path))}"), *("-ex", f"python expose_extra = {str(expose_extra)}"), *("-ex", f"source {gdb_api_bridge_path}"), *("-ex", f"target remote {host}:{port}"), *gdb_args, machine.argv[0], ] process = Popen(args, stdin=PIPE, stdout=DEVNULL, stderr=DEVNULL) for _ in range(100): if socket_path.exists(): break sleep(0.1) conn = unix_connect(str(socket_path)) socket_path.unlink() socket_dir_path.rmdir() BgServingThread(conn, callback=lambda: None) return GDB(conn, extra=expose_extra)
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)
def _fork_and_run(func, *, timeout, socket_path, no_start=False): """ Connect to server and execute `func` remotely. Start server if not already running. Args: timeout (int): seconds until server shuts down, use None to run forever socket_path (str): desired path of socket for backend communication no_start (bool): don't start the background server Returns: return value of `func` """ starting_path = os.getcwd() # if server is running, connect to it try: conn = unix_connect( socket_path, config={ 'allow_all_attrs': True, 'allow_setattr': True, 'allow_getattr': True, }, ) return func(conn) except (FileNotFoundError, ConnectionRefusedError) as e: # if no_start provided and server not running, do nothing if no_start: raise e # handle ConnectionRefusedError - clean up old socket if os.path.exists(socket_path): if stat.S_ISSOCK(os.stat(socket_path).st_mode): os.remove(socket_path) else: log.warning('Encountered regular file at {}'.format(socket_path)) # otherwise, fork server, then connect to it pid = os.fork() # parent process, run server as unix daemon if pid == 0: try: with daemon.DaemonContext(): os.chdir(starting_path) server = MyServer( MyService, socket_path=socket_path, protocol_config={ 'allow_all_attrs': True, 'allow_setattr': True, 'allow_getattr': True, }, # initial timeout before any client connects listener_timeout=timeout ) server.start() except Exception: import traceback open('/tmp/pykeepass_server_exception', 'w').write(traceback.format_exc()) # child process else: # FIXME: is there a more robust way to start the client after the server? while not os.path.exists(socket_path): time.sleep(0.05) conn = unix_connect( socket_path, config={ 'allow_all_attrs': True, 'allow_setattr': True, 'allow_getattr': True, }, ) return func(conn)