def binary(): """binary() -> str Returns: str: Path to the appropriate ``gdb`` binary to use. Example: >>> gdb.binary() # doctest: +SKIP '/usr/bin/gdb' """ gdb = misc.which('pwntools-gdb') or 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
def user_path(): """ Returns the path to the QEMU-user binary for the currently selected architecture. >>> pwnlib.qemu.user_path() 'qemu-i386-static' >>> pwnlib.qemu.user_path(arch='thumb') 'qemu-arm-static' """ arch = archname() system = 'qemu-system-' + arch normal = 'qemu-' + arch static = normal + '-static' if context.os == 'baremetal': if misc.which(system): return system else: if misc.which(static): return static if misc.which(normal): return normal log.warn_once("Neither %r nor %r are available" % (normal, static))
def __on_enoexec(self, exception): """We received an 'exec format' error (ENOEXEC) This implies that the user tried to execute e.g. an ARM binary on a non-ARM system, and does not have binfmt helpers installed for QEMU. """ # Get the ELF binary for the target executable with context.quiet: # XXX: Cyclic imports :( from pwnlib.elf import ELF binary = ELF(self.executable) # If we're on macOS, this will never work. Bail now. # if platform.mac_ver()[0]: # self.error("Cannot run ELF binaries on macOS") # Determine what architecture the binary is, and find the # appropriate qemu binary to run it. qemu = get_qemu_user(arch=binary.arch) qemu = which(qemu) if qemu: self._qemu = qemu args = [qemu] if self.argv: args += ['-0', self.argv[0]] args += ['--'] return [args, qemu] # If we get here, we couldn't run the binary directly, and # we don't have a qemu which can run it. self.exception(exception)
def __id_init__(self): # load up all library symbols; adds things like str_bin_sh uncaught by ELF() with open(self.libpath+'.symbols') as f: # Weird thing: this breaks if 'rb' is used. symbols = dict(l.split() for l in f.readlines()) for k in symbols: new_value = int(symbols[k],16) if k in self.symbols: # if we might have symbol conflicts... if self.symbols[k] == new_value: continue # no conflict here if new_value: # is not 0 if self.symbols[k]: # if BOTH aren't zero AND they're different log.debug('pwnscripts.libc: symbol "%s" has conflicting offsets in ' '"%s" (%s) vs "%s" (%s)' % (k, self.binary, hex(self.symbols[k]), self.libpath+'.symbols', hex(new_value))) else: pass # if new_value is non-zero while the original one is 0 # NOTE: Don't remove the above line; it's there for future implementation del self.symbols[k] # Remove the symbol (and have it set later in the loop) else: continue # ignore symbols[k] if it's zero and the original symbol isn't self.symbols[k] = int(symbols[k], 16) # load up one_gadget offsets in advance if which('one_gadget') is None: log.info('one_gadget does not appear to exist in PATH. ignoring.') self.one_gadget = None else: self.one_gadget = _one_gadget(self.libpath+'.so')
def version(program='gdb'): """Gets the current GDB version. Note: Requires that GDB version meets the following format: ``GNU gdb (GDB) 7.12`` Returns: tuple: A tuple containing the version numbers Example: >>> (7,0) <= gdb.version() <= (10,0) True """ program = misc.which(program) expr = br'([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(b'.')))
def version(program='gdb'): """Gets the current GDB version. Note: Requires that GDB version meets the following format: ``GNU gdb (GDB) 7.12`` Returns: tuple: A tuple containing the version numbers 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('.')))
def unstrip_libc(filename): """ Given a path to a libc binary, attempt to download matching debug info and add them back to the given binary. This modifies the given file. Arguments: filename(str): Path to the libc binary to unstrip. Returns: :const:`True` if binary was unstripped, :const:`False` otherwise. Examples: >>> filename = search_by_build_id('2d1c5e0b85cb06ff47fa6fa088ec22cb6e06074e', unstrip=False) >>> libc = ELF(filename) >>> hex(libc.symbols.read) '0xe56c0' >>> 'main_arena' in libc.symbols False >>> unstrip_libc(filename) True >>> libc = ELF(filename) >>> hex(libc.symbols.main_arena) '0x1d57a0' >>> unstrip_libc(which('python')) False >>> filename = search_by_build_id('06a8004be6e10c4aeabbe0db74423ace392a2d6b', unstrip=True) >>> 'main_arena' in ELF(filename).symbols True """ if not which('eu-unstrip'): log.warn_once('Couldn\'t find "eu-unstrip" in PATH. Install elfutils first.') return False libc = ELF(filename, checksec=False) if not libc.buildid: log.warn_once('Given libc does not have a buildid. Cannot look for debuginfo to unstrip.') return False for server_url in DEBUGINFOD_SERVERS: libc_dbg = _search_debuginfo_by_hash(server_url, enhex(libc.buildid)) if libc_dbg: break else: log.warn_once('Couldn\'t find debug info for libc with build_id %s on any debuginfod server.', enhex(libc.buildid)) return False # Add debug info to given libc binary inplace. p = process(['eu-unstrip', '-o', filename, filename, libc_dbg]) output = p.recvall() p.close() if output: log.error('Failed to unstrip libc binary: %s', output) return False return True
def compile(source): """Compile a source file or project with the Android NDK.""" ndk_build = misc.which('ndk-build') if not ndk_build: # Ensure that we can find the NDK. ndk = os.environ.get('NDK', None) if ndk is None: log.error('$NDK must be set to the Android NDK directory') ndk_build = os.path.join(ndk, 'ndk-build') # Determine whether the source is an NDK project or a single source file. project = find_ndk_project_root(source) if not project: # Realistically this should inherit from context.arch, but # this works for now. sdk = '21' abi = { 'aarch64': 'arm64-v8a', 'amd64': 'x86_64', 'arm': 'armeabi-v7a', 'i386': 'x86', 'mips': 'mips', 'mips64': 'mips64', }.get(context.arch, None) # If we have an attached device, use its settings. if context.device: abi = str(properties.ro.product.cpu.abi) sdk = str(properties.ro.build.version.sdk) if abi is None: log.error("Unknown CPU ABI") project = _generate_ndk_project(source, abi, sdk) # Remove any output files lib = os.path.join(project, 'libs') if os.path.exists(lib): shutil.rmtree(lib) # Build the project io = tubes.process.process(ndk_build, cwd=os.path.join(project, 'jni')) result = io.recvall() if 0 != io.poll(): log.error("Build failed:\n%s" % result) # Find all of the output files output = glob.glob(os.path.join(lib, '*', '*')) return output[0]
def binary(): """binary() -> str Returns: str: Path to the appropriate ``gdb`` binary to use. """ 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
def compile(source): """Compile a source file or project with the Android NDK.""" ndk_build = misc.which('ndk-build') if not ndk_build: # Ensure that we can find the NDK. ndk = os.environ.get('NDK', None) if ndk is None: log.error('$NDK must be set to the Android NDK directory') ndk_build = os.path.join(ndk, 'ndk-build') # Determine whether the source is an NDK project or a single source file. project = find_ndk_project_root(source) if not project: # Realistically this should inherit from context.arch, but # this works for now. sdk = '21' abi = { 'aarch64': 'arm64-v8a', 'amd64': 'x86_64', 'arm': 'armeabi-v7a', 'i386': 'x86', 'mips': 'mips', 'mips64': 'mips64', }.get(context.arch, None) # If we have an attached device, use its settings. if context.device: abi = getprop('ro.product.cpu.abi') sdk = getprop('ro.build.version.sdk') if abi is None: log.error("Unknown CPU ABI") project = _generate_ndk_project(source, abi, sdk) # Remove any output files lib = os.path.join(project, 'libs') if os.path.exists(lib): shutil.rmtree(lib) # Build the project io = tubes.process.process(ndk_build, cwd=os.path.join(project, 'jni')) result = io.recvall() if 0 != io.poll(): log.error("Build failed:\n%s" % result) # Find all of the output files output = glob.glob(os.path.join(lib, '*', '*')) return output[0]
def get_qemu_user(): """ Returns the path to the QEMU-user binary for the currently selected architecture. >>> get_qemu_user() 'qemu-i386-static' >>> get_qemu_user(arch='thumb') 'qemu-arm-static' """ arch = get_qemu_arch() normal = 'qemu-' + arch static = normal + '-static' if misc.which(static): return static if misc.which(normal): return normal log.warn_once("Neither %r nor %r are available" % (normal, static))
def binary(): """binary() -> str Returns: str: Path to the appropriate ``gdb`` binary to use. """ 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
def __id_init__(self): # load up all library symbols; adds things like str_bin_sh uncaught by ELF() with open( self.libpath + '.symbols') as f: # Weird thing: this breaks if 'rb' is used. symbols = dict(l.split() for l in f.readlines()) for k in symbols: self.symbols[k] = int(symbols[k], 16) # load up one_gadget offsets in advance if which('one_gadget') is None: log.info('one_gadget does not appear to exist in PATH. ignoring.') self.one_gadget = None else: self.one_gadget = _one_gadget(self.libpath + '.so')
def __on_enoexec(self, exception): """We received an 'exec format' error (ENOEXEC) This implies that the user tried to execute e.g. an ARM binary on a non-ARM system, and does not have binfmt helpers installed for QEMU. """ # Get the ELF binary for the target executable with context.quiet: # XXX: Cyclic imports :( from pwnlib.elf import ELF binary = ELF(self.executable) # If we're on macOS, this will never work. Bail now. # if platform.mac_ver()[0]: # self.error("Cannot run ELF binaries on macOS") # Determine what architecture the binary is, and find the # appropriate qemu binary to run it. qemu = get_qemu_user(arch=binary.arch) if not qemu: raise exception qemu = which(qemu) if qemu: self._qemu = qemu args = [qemu] if self.argv: args += ['-0', self.argv[0]] args += ['--'] return [args, qemu] # If we get here, we couldn't run the binary directly, and # we don't have a qemu which can run it. self.exception(exception)
def attach(target, gdbscript=None, exe=None, need_ptrace_scope=True, gdb_args=None, ssh=None): """attach(target, gdbscript = None, exe = None, arch = None, ssh = None) -> None 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. 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. .. code-block:: python # Attach directly to pid 1234 gdb.attach(1234) .. code-block:: python # Attach to the youngest "bash" process gdb.attach('bash') .. code-block:: python # Start a process bash = process('bash') # Attach the debugger gdb.attach(bash, ''' set follow-fork-mode child break execve continue ''') # Interact with the process bash.sendline('whoami') .. code-block:: python # Start a forking server server = process(['socat', 'tcp-listen:1234,fork,reuseaddr', 'exec:/bin/sh']) # Connect to the server io = remote('localhost', 1234) # Connect the debugger to the server-spawned process gdb.attach(io, ''' break exit continue ''') # Talk to the spawned 'sh' io.sendline('exit') .. code-block:: python # Connect to the SSH server shell = ssh('bandit0', 'bandit.labs.overthewire.org', password='******', port=2220) # Start a process on the server cat = shell.process(['cat']) # Attach a debugger to it gdb.attach(cat, ''' break exit continue ''') # Cause `cat` to exit cat.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 isinstance(gdbscript, file): with gdbscript: gdbscript = gdbscript.read() # enable gdb.attach(p, 'continue') if gdbscript and not gdbscript.endswith('\n'): gdbscript += '\n' # 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 context.os == 'android': pre += 'set gnutarget ' + _bfdname() + '\n' # let's see if we can find a pid to attach to pid = None if isinstance(target, (int, long)): # 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 = 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 = '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] cmd += [ 'gdb -q %r %s -x "%s"' % (target.executable, 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] elif isinstance(target, tubes.process.process): pid = proc.pidof(target)[0] elif isinstance(target, tuple) and len(target) == 2: host, port = target pre += 'target remote %s:%d\n' % (host, port) 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: log.error('could not find target process') cmd = binary() 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 ssh: ssh.download_file(exe) exe = os.path.basename(exe) if 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) gdbserver = runner(gdb_cmd) port = _gdbserver_port(gdbserver, None) host = context.adb_host pre += 'target remote %s:%i' % (context.adb_host, port) gdbscript = pre + (gdbscript or '') if gdbscript: tmp = tempfile.NamedTemporaryFile(prefix='pwn', suffix='.gdb', delete=False) 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) 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) return gdb_pid
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 test(original): r"""Tests the output provided by a shell interpreting a string >>> test('foobar') >>> test('foo bar') >>> test('foo bar\n') >>> test("foo'bar") >>> test("foo\\\\bar") >>> test("foo\\\\'bar") >>> test("foo\\x01'bar") >>> test('\n') >>> test('\xff') >>> test(os.urandom(16 * 1024).replace('\x00', '')) """ input = sh_string(original) cmdstr = '/bin/echo %s' % input SUPPORTED_SHELLS = [ ['ash', '-c', cmdstr], ['bash', '-c', cmdstr], ['bash', '-o', 'posix', '-c', cmdstr], ['ksh', '-c', cmdstr], ['busybox', 'ash', '-c', cmdstr], ['busybox', 'sh', '-c', cmdstr], ['zsh', '-c', cmdstr], ['posh', '-c', cmdstr], ['dash', '-c', cmdstr], ['mksh', '-c', cmdstr], ['sh', '-c', cmdstr], # ['adb', 'exec-out', cmdstr] ] for shell in SUPPORTED_SHELLS: binary = shell[0] if not which(binary): log.warn_once('Shell %r is not available' % binary) continue progress = log.progress('%s: %r' % (binary, original)) with context.quiet: with process(shell) as p: data = p.recvall(timeout=2) p.kill() # Remove exactly one trailing newline added by echo # We cannot assume "echo -n" exists. data = data[:-1] if data != original: for i,(a,b) in enumerate(zip(data, original)): if a == b: continue log.error(('Shell %r failed\n' + 'Expect %r\n' + 'Sent %r\n' + 'Output %r\n' + 'Mismatch @ %i: %r vs %r') \ % (binary, original, input, data, i, a, b)) progress.success()
def __init__(self, argv = None, shell = False, executable = None, cwd = None, env = None, stdin = PIPE, stdout = PTY, stderr = STDOUT, close_fds = True, preexec_fn = lambda: None, raw = True, aslr = None, setuid = None, where = 'local', display = None, alarm = None, *args, **kwargs ): super(process, self).__init__(*args,**kwargs) # Permit using context.binary if argv is None: if context.binary: argv = [context.binary.path] else: raise TypeError('Must provide argv or set context.binary') #: `subprocess.Popen` object self.proc = None if not shell: executable, argv, env = self._validate(cwd, executable, argv, env) # Permit invocation as process('sh') and process(['sh']) if isinstance(argv, (str, unicode)): argv = [argv] # Avoid the need to have to deal with the STDOUT magic value. if stderr is STDOUT: stderr = stdout # Determine which descriptors will be attached to a new PTY handles = (stdin, stdout, stderr) #: Which file descriptor is the controlling TTY self.pty = handles.index(PTY) if PTY in handles else None #: Whether the controlling TTY is set to raw mode self.raw = raw #: Whether ASLR should be left on self.aslr = aslr if aslr is not None else context.aslr #: Whether setuid is permitted self._setuid = setuid if setuid is None else bool(setuid) # Create the PTY if necessary stdin, stdout, stderr, master, slave = self._handles(*handles) #: Arguments passed on argv self.argv = argv #: Full path to the executable self.executable = executable if self.executable is None: if shell: self.executable = '/bin/sh' else: self.executable = which(self.argv[0]) #: Environment passed on envp self.env = os.environ if env is None else env self._cwd = os.path.realpath(cwd or os.path.curdir) #: Alarm timeout of the process self.alarm = alarm self.preexec_fn = preexec_fn self.display = display or self.program self._qemu = False self._corefile = None message = "Starting %s process %r" % (where, self.display) if self.isEnabledFor(logging.DEBUG): if self.argv != [self.executable]: message += ' argv=%r ' % self.argv if self.env != os.environ: message += ' env=%r ' % self.env with self.progress(message) as p: if not self.aslr: self.warn_once("ASLR is disabled!") # In the event the binary is a foreign architecture, # and binfmt is not installed (e.g. when running on # Travis CI), re-try with qemu-XXX if we get an # 'Exec format error'. prefixes = [([], executable)] executables = [executable] exception = None for prefix, executable in prefixes: try: self.proc = subprocess.Popen(args = prefix + argv, shell = shell, executable = executable, cwd = cwd, env = env, stdin = stdin, stdout = stdout, stderr = stderr, close_fds = close_fds, preexec_fn = self.__preexec_fn) break except OSError as exception: if exception.errno != errno.ENOEXEC: raise prefixes.append(self.__on_enoexec(exception)) p.success('pid %i' % self.pid) if self.pty is not None: if stdin is slave: self.proc.stdin = os.fdopen(os.dup(master), 'r+') if stdout is slave: self.proc.stdout = os.fdopen(os.dup(master), 'r+') if stderr is slave: self.proc.stderr = os.fdopen(os.dup(master), 'r+') os.close(master) os.close(slave) # Set in non-blocking mode so that a call to call recv(1000) will # return as soon as a the first byte is available fd = self.proc.stdout.fileno() fl = fcntl.fcntl(fd, fcntl.F_GETFL) fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) # Save off information about whether the binary is setuid / setgid self.uid = os.getuid() self.gid = os.getgid() self.suid = -1 self.sgid = -1 st = os.stat(self.executable) if self._setuid: if (st.st_mode & stat.S_ISUID): self.setuid = st.st_uid if (st.st_mode & stat.S_ISGID): self.setgid = st.st_gid
def attach(target, gdbscript='', exe=None, gdb_args=None, ssh=None, sysroot=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 directly to pid 1234 >>> gdb.attach(1234) # doctest: +SKIP >>> # Attach to the youngest "bash" process >>> gdb.attach('bash') # doctest: +SKIP >>> # Start a process >>> bash = process('bash') >>> # Attach the debugger >>> pid = gdb.attach(bash, ''' ... set follow-fork-mode child ... break execve ... continue ... ''') >>> # Interact with the process >>> bash.sendline("/bin/echo hello") >>> bash.recvline() b'hello\n' >>> bash.close() >>> # Start a forking server >>> server = process(['socat', 'tcp-listen:12345,fork,reuseaddr', 'exec:/bin/bash,nofork']) >>> sleep(1) >>> # Connect to the server >>> io = remote('127.0.0.1', 12345) >>> # Connect the debugger to the server-spawned process >>> pid = gdb.attach(io, ''' ... break exit ... continue ... ''', exe = '/bin/bash') >>> # Talk to the spawned 'bash' >>> io.sendline("echo hello") >>> io.recvline() b'hello\n' >>> io.sendline("exit") >>> io.close() >>> # Connect to the SSH server >>> shell = ssh('travis', 'example.pwnme', password='******') >>> # Start a process on the server >>> cat = shell.process(['cat']) >>> # Attach a debugger to it >>> gdb.attach(cat, ''' ... break exit ... continue ... ''') >>> cat.sendline("hello") >>> cat.recvline() b'hello\n' >>> # Cause `cat` to exit >>> cat.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' # 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()) waiting = 100 if exe: while waiting: waiting -= 1 for pid in pids: if proc.exe(pid) == exe: waiting = False break else: time.sleep(0.01) 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() 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) 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 compile(source): r"""Compile a source file or project with the Android NDK. Example: >>> temp = tempfile.mktemp('.c') >>> write(temp, ''' ... #include <stdio.h> ... static char buf[4096]; ... int main() { ... FILE *fp = fopen("/proc/self/maps", "r"); ... int n = fread(buf, 1, sizeof(buf), fp); ... fwrite(buf, 1, n, stdout); ... return 0; ... }''') >>> filename = adb.compile(temp) >>> sent = adb.push(filename, "/data/local/tmp") >>> adb.process(sent).recvall() # doctest: +ELLIPSIS b'... /system/bin/linker\n...' """ ndk_build = misc.which('ndk-build') if not ndk_build: # Ensure that we can find the NDK. ndk = os.environ.get('NDK', None) if ndk is None: log.error('$NDK must be set to the Android NDK directory') ndk_build = os.path.join(ndk, 'ndk-build') # Determine whether the source is an NDK project or a single source file. project = find_ndk_project_root(source) if not project: # Realistically this should inherit from context.arch, but # this works for now. sdk = '21' abi = { 'aarch64': 'arm64-v8a', 'amd64': 'x86_64', 'arm': 'armeabi-v7a', 'i386': 'x86', 'mips': 'mips', 'mips64': 'mips64', }.get(context.arch, None) # If we have an attached device, use its settings. if context.device: abi = getprop('ro.product.cpu.abi') sdk = getprop('ro.build.version.sdk') if abi is None: log.error("Unknown CPU ABI") project = _generate_ndk_project(source, abi, sdk) # Remove any output files lib = os.path.join(project, 'libs') if os.path.exists(lib): shutil.rmtree(lib) # Build the project io = tubes.process.process(ndk_build, cwd=os.path.join(project, 'jni')) result = io.recvall() if 0 != io.poll(): log.error("Build failed:\n%s", result) # Find all of the output files output = glob.glob(os.path.join(lib, '*', '*')) return output[0]
def _validate(self, cwd, executable, argv, env): """ Perform extended validation on the executable path, argv, and envp. Mostly to make Python happy, but also to prevent common pitfalls. """ cwd = cwd or os.path.curdir # # Validate argv # # - Must be a list/tuple of strings # - Each string must not contain '\x00' # if isinstance(argv, (bytes, six.text_type)): argv = [argv] if not all(isinstance(arg, (bytes, six.text_type)) for arg in argv): self.error("argv must be strings: %r" % argv) # Create a duplicate so we can modify it argv = list(argv or []) for i, oarg in enumerate(argv): if isinstance(oarg, six.text_type): arg = oarg.encode('utf-8') else: arg = oarg if b'\x00' in arg[:-1]: self.error('Inappropriate nulls in argv[%i]: %r' % (i, oarg)) argv[i] = arg.rstrip(b'\x00') # # Validate executable # # - Must be an absolute or relative path to the target executable # - If not, attempt to resolve the name in $PATH # if not executable: if not argv: self.error("Must specify argv or executable") executable = argv[0] if not isinstance(executable, str): executable = executable.decode('utf-8') # Do not change absolute paths to binaries if executable.startswith(os.path.sep): pass # If there's no path component, it's in $PATH or relative to the # target directory. # # For example, 'sh' elif os.path.sep not in executable and which(executable): executable = which(executable) # 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) self.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) if not os.path.isfile(executable): self.error("%r is not a file" % executable) if not os.access(executable, os.X_OK): self.error("%r is not marked as executable (+x)" % executable) # # Validate environment # # - Must be a dictionary of {string:string} # - No strings may contain '\x00' # # Create a duplicate so we can modify it safely env = os.environ if env is None else env env2 = {} for k,v in env.items(): if not isinstance(k, (bytes, six.text_type)): self.error('Environment keys must be strings: %r' % k) if not isinstance(k, (bytes, six.text_type)): self.error('Environment values must be strings: %r=%r' % (k,v)) if isinstance(k, six.text_type): k = k.encode('utf-8') if isinstance(v, six.text_type): v = v.encode('utf-8', 'surrogateescape') if b'\x00' in k[:-1]: self.error('Inappropriate nulls in env key: %r' % (k)) if b'\x00' in v[:-1]: self.error('Inappropriate nulls in env value: %r=%r' % (k, v)) env2[k.rstrip(b'\x00')] = v.rstrip(b'\x00') return executable, argv, env2
def __init__(self, argv = None, shell = False, executable = None, cwd = None, env = None, stdin = PIPE, stdout = PTY, stderr = STDOUT, close_fds = True, preexec_fn = lambda: None, raw = True, aslr = None, setuid = None, where = 'local', display = None, alarm = None, *args, **kwargs ): super(process, self).__init__(*args,**kwargs) # Permit using context.binary if argv is None: if context.binary: argv = [context.binary.path] else: raise TypeError('Must provide argv or set context.binary') #: :class:`subprocess.Popen` object that backs this process self.proc = None if not shell: executable, argv, env = self._validate(cwd, executable, argv, env) # Permit invocation as process('sh') and process(['sh']) if isinstance(argv, (bytes, six.text_type)): argv = [argv] # Avoid the need to have to deal with the STDOUT magic value. if stderr is STDOUT: stderr = stdout # Determine which descriptors will be attached to a new PTY handles = (stdin, stdout, stderr) #: Which file descriptor is the controlling TTY self.pty = handles.index(PTY) if PTY in handles else None #: Whether the controlling TTY is set to raw mode self.raw = raw #: Whether ASLR should be left on self.aslr = aslr if aslr is not None else context.aslr #: Whether setuid is permitted self._setuid = setuid if setuid is None else bool(setuid) # Create the PTY if necessary stdin, stdout, stderr, master, slave = self._handles(*handles) #: Arguments passed on argv self.argv = argv #: Full path to the executable self.executable = executable if self.executable is None: if shell: self.executable = '/bin/sh' else: self.executable = which(self.argv[0]) #: Environment passed on envp self.env = os.environ if env is None else env self._cwd = os.path.realpath(cwd or os.path.curdir) #: Alarm timeout of the process self.alarm = alarm self.preexec_fn = preexec_fn self.display = display or self.program self._qemu = False self._corefile = None message = "Starting %s process %r" % (where, self.display) if self.isEnabledFor(logging.DEBUG): if self.argv != [self.executable]: message += ' argv=%r ' % self.argv if self.env != os.environ: message += ' env=%r ' % self.env with self.progress(message) as p: if not self.aslr: self.warn_once("ASLR is disabled!") # In the event the binary is a foreign architecture, # and binfmt is not installed (e.g. when running on # Travis CI), re-try with qemu-XXX if we get an # 'Exec format error'. prefixes = [([], executable)] executables = [executable] exception = None for prefix, executable in prefixes: try: self.proc = subprocess.Popen(args = prefix + argv, shell = shell, executable = executable, cwd = cwd, env = env, stdin = stdin, stdout = stdout, stderr = stderr, close_fds = close_fds, preexec_fn = self.__preexec_fn) break except OSError as exception: if exception.errno != errno.ENOEXEC: raise prefixes.append(self.__on_enoexec(exception)) p.success('pid %i' % self.pid) if self.pty is not None: if stdin is slave: self.proc.stdin = os.fdopen(os.dup(master), 'r+b', 0) if stdout is slave: self.proc.stdout = os.fdopen(os.dup(master), 'r+b', 0) if stderr is slave: self.proc.stderr = os.fdopen(os.dup(master), 'r+b', 0) os.close(master) os.close(slave) # Set in non-blocking mode so that a call to call recv(1000) will # return as soon as a the first byte is available if self.proc.stdout: fd = self.proc.stdout.fileno() fl = fcntl.fcntl(fd, fcntl.F_GETFL) fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) # Save off information about whether the binary is setuid / setgid self.uid = os.getuid() self.gid = os.getgid() self.suid = -1 self.sgid = -1 st = os.stat(self.executable) if self._setuid: if (st.st_mode & stat.S_ISUID): self.setuid = st.st_uid if (st.st_mode & stat.S_ISGID): self.setgid = st.st_gid
def test(original): r"""Tests the output provided by a shell interpreting a string >>> test(b'foobar') >>> test(b'foo bar') >>> test(b'foo bar\n') >>> test(b"foo'bar") >>> test(b"foo\\\\bar") >>> test(b"foo\\\\'bar") >>> test(b"foo\\x01'bar") >>> test(b'\n') >>> test(b'\xff') >>> test(os.urandom(16 * 1024).replace(b'\x00', b'')) """ input = sh_string(original) if not isinstance(input, str): input = input.decode('latin1') cmdstr = six.b('/bin/echo %s' % input) SUPPORTED_SHELLS = [ ['ash', '-c', cmdstr], ['bash', '-c', cmdstr], ['bash', '-o', 'posix', '-c', cmdstr], ['ksh', '-c', cmdstr], ['busybox', 'ash', '-c', cmdstr], ['busybox', 'sh', '-c', cmdstr], ['zsh', '-c', cmdstr], ['posh', '-c', cmdstr], ['dash', '-c', cmdstr], ['mksh', '-c', cmdstr], ['sh', '-c', cmdstr], # ['adb', 'exec-out', cmdstr] ] for shell in SUPPORTED_SHELLS: binary = shell[0] if not which(binary): log.warn_once('Shell %r is not available' % binary) continue progress = log.progress('%s: %r' % (binary, original)) with context.quiet: with process(shell) as p: data = p.recvall(timeout=2) p.kill() # Remove exactly one trailing newline added by echo # We cannot assume "echo -n" exists. data = data[:-1] if data != original: for i, (a, b) in enumerate(zip(data, original)): if a == b: continue log.error(('Shell %r failed\n' + 'Expect %r\n' + 'Sent %r\n' + 'Output %r\n' + 'Mismatch @ %i: %r vs %r') \ % (binary, original, input, data, i, a, b)) progress.success()
def _validate(self, cwd, executable, argv, env): """ Perform extended validation on the executable path, argv, and envp. Mostly to make Python happy, but also to prevent common pitfalls. """ cwd = cwd or os.path.curdir # # Validate argv # # - Must be a list/tuple of strings # - Each string must not contain '\x00' # if isinstance(argv, (str, unicode)): argv = [argv] if not all(isinstance(arg, (str, unicode)) for arg in argv): self.error("argv must be strings: %r" % argv) # Create a duplicate so we can modify it argv = list(argv or []) for i, arg in enumerate(argv): if '\x00' in arg[:-1]: self.error('Inappropriate nulls in argv[%i]: %r' % (i, arg)) argv[i] = arg.rstrip('\x00') # # Validate executable # # - Must be an absolute or relative path to the target executable # - If not, attempt to resolve the name in $PATH # if not executable: if not argv: self.error("Must specify argv or executable") executable = argv[0] # Do not change absolute paths to binaries if executable.startswith(os.path.sep): pass # If there's no path component, it's in $PATH or relative to the # target directory. # # For example, 'sh' elif os.path.sep not in executable and which(executable): executable = which(executable) # 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) self.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) if not os.path.isfile(executable): self.error("%r is not a file" % executable) if not os.access(executable, os.X_OK): self.error("%r is not marked as executable (+x)" % executable) # # Validate environment # # - Must be a dictionary of {string:string} # - No strings may contain '\x00' # # Create a duplicate so we can modify it safely env = dict(os.environ if env is None else env) for k,v in env.items(): if not isinstance(k, (str, unicode)): self.error('Environment keys must be strings: %r' % k) if not isinstance(k, (str, unicode)): self.error('Environment values must be strings: %r=%r' % (k,v)) if '\x00' in k[:-1]: self.error('Inappropriate nulls in env key: %r' % (k)) if '\x00' in v[:-1]: self.error('Inappropriate nulls in env value: %r=%r' % (k, v)) env[k.rstrip('\x00')] = v.rstrip('\x00') return executable, argv, env
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 attach(target, gdbscript = None, exe = None, need_ptrace_scope = True, gdb_args = None, ssh = None): """attach(target, gdbscript = None, exe = None, arch = None, ssh = None) -> None 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. 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. .. code-block:: python # Attach directly to pid 1234 gdb.attach(1234) .. code-block:: python # Attach to the youngest "bash" process gdb.attach('bash') .. code-block:: python # Start a process bash = process('bash') # Attach the debugger gdb.attach(bash, ''' set follow-fork-mode child break execve continue ''') # Interact with the process bash.sendline('whoami') .. code-block:: python # Start a forking server server = process(['socat', 'tcp-listen:1234,fork,reuseaddr', 'exec:/bin/sh']) # Connect to the server io = remote('localhost', 1234) # Connect the debugger to the server-spawned process gdb.attach(io, ''' break exit continue ''') # Talk to the spawned 'sh' io.sendline('exit') .. code-block:: python # Connect to the SSH server shell = ssh('bandit0', 'bandit.labs.overthewire.org', password='******', port=2220) # Start a process on the server cat = shell.process(['cat']) # Attach a debugger to it gdb.attach(cat, ''' break exit continue ''') # Cause `cat` to exit cat.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 isinstance(gdbscript, file): with gdbscript: gdbscript = gdbscript.read() # enable gdb.attach(p, 'continue') if gdbscript and not gdbscript.endswith('\n'): gdbscript += '\n' # 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 context.os == 'android': pre += 'set gnutarget ' + _bfdname() + '\n' # let's see if we can find a pid to attach to pid = None if isinstance(target, (int, long)): # 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 = 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 = '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] cmd += ['gdb -q %r %s -x "%s"' % (target.executable, 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] elif isinstance(target, tubes.process.process): pid = proc.pidof(target)[0] elif isinstance(target, tuple) and len(target) == 2: host, port = target pre += 'target remote %s:%d\n' % (host, port) 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: log.error('could not find target process') cmd = binary() if gdb_args: cmd += ' ' cmd += ' '.join(gdb_args) cmd += ' -q ' if exe and context.native: if ssh: ssh.download_file(exe) exe = os.path.basename(exe) if 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) gdbserver = runner(gdb_cmd) port = _gdbserver_port(gdbserver, None) host = context.adb_host pre += 'target remote %s:%i' % (context.adb_host, port) gdbscript = pre + (gdbscript or '') if gdbscript: tmp = tempfile.NamedTemporaryFile(prefix = 'pwn', suffix = '.gdb', delete = False) 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) 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) return gdb_pid
def __init__(self, argv=None, shell=False, executable=None, cwd=None, env=None, stdin=PIPE, stdout=PTY, stderr=STDOUT, close_fds=True, preexec_fn=lambda: None, raw=True, aslr=None, setuid=None, where='local', display=None, alarm=None, prefer_dockerfile=True, baseimage=None, withgdb=True, gdbport=1234, reload=True, *args, **kwargs): super(dockerized, self).__init__(*args, **kwargs) # Permit using context.binary if argv is None: if context.binary: argv = [context.binary.path] else: raise TypeError('Must provide argv or set context.binary') #: :class:`subprocess.Popen` object that backs this process self.proc = None # We need to keep a copy of the un-_validated environment for printing original_env = env if shell: executable_val, argv_val, env_val = executable, argv, env else: executable_val, argv_val, env_val = self._validate( cwd, executable, argv, env) # Avoid the need to have to deal with the STDOUT magic value. if stderr is STDOUT: stderr = stdout # Determine which descriptors will be attached to a new PTY handles = (stdin, stdout, stderr) #: Which file descriptor is the controlling TTY self.pty = handles.index(PTY) if PTY in handles else None #: Whether the controlling TTY is set to raw mode self.raw = raw #: Whether ASLR should be left on self.aslr = aslr if aslr is not None else context.aslr #: Whether setuid is permitted self._setuid = setuid if setuid is None else bool(setuid) # Create the PTY if necessary stdin, stdout, stderr, master, slave = self._handles(*handles) #: Arguments passed on argv self.argv = argv_val #: Full path to the executable self.executable = executable_val if self.executable is None: if shell: self.executable = '/bin/sh' else: self.executable = which(self.argv[0]) #: Environment passed on envp self.env = os.environ if env is None else env_val self._cwd = os.path.realpath(cwd or os.path.curdir) #: Alarm timeout of the process self.alarm = alarm self.preexec_fn = preexec_fn self.display = display or self.program message = "Starting %s process %r" % (where, self.display) if self.isEnabledFor(logging.DEBUG): if argv != [self.executable]: message += ' argv=%r ' % self.argv if original_env not in (os.environ, None): message += ' env=%r ' % self.env ## Make new Dockerfile self.prefer_dockerfile = prefer_dockerfile self.baseimage = baseimage self.withgdb = withgdb self.gdbport = gdbport if (not os.path.isfile(os.path.join( self._cwd, 'Dockerfile'))) or (not self.prefer_dockerfile): dockerfile = f'''FROM {self.baseimage} RUN mkdir -p {self._cwd} WORKDIR {self._cwd} COPY ./ ./ RUN chmod +x {self.executable} ''' if self.withgdb: if baseimage == 'ubuntu:19.04': dockerfile += '''RUN sed -i -re 's/([a-z]{2}\.)?archive.ubuntu.com|security.ubuntu.com/old-releases.ubuntu.com/g' /etc/apt/sources.list ''' dockerfile += f'''RUN apt-get update && apt-get install -y gdb gdbserver && rm -rf /var/lib/apt/lists/* EXPOSE {self.gdbport} ''' dockerfile += '''CMD [ ''' dockerfile += ', '.join( map(lambda a: f'"{a.decode("utf-8")}"', self.argv)) dockerfile += ' ]\n' with open(os.path.join(self._cwd, 'Dockerfile'), 'w') as f: f.write(dockerfile) self.docker_image_tag = f'pwntools_{os.path.basename(self.executable)}' self.docker_container_name = self.docker_image_tag self.reload = reload client = docker.from_env() self.debug(f'Starting to build image with tag {self.docker_image_tag}') client.images.build(tag=self.docker_image_tag, path=self._cwd) self.success(f'Built image with tag {self.docker_image_tag}') try: container = client.containers.get(self.docker_container_name) if not self.reload: raise RuntimeError("Container with same name already exist. ") if container.attrs['Config']['Image'] != self.docker_image_tag: self.warn( f"Removing container with name {self.docker_container_name} and image {container.attrs['Config']['Image']}" ) container.remove(force=True) except docker.errors.NotFound: pass self.container = client.containers.run( self.docker_image_tag, name=self.docker_container_name, privileged=self.withgdb, ports={f'{self.gdbport}/tcp': ('127.0.0.1', self.gdbport)}, stdin_open=True, detach=True) self.container_sock_stdin_demuxed = dockerpty.io.Demuxer( self.container.attach_socket(params={ 'stdin': 1, 'stream': 1 })) self.container_sock_stdout_demuxed = dockerpty.io.Demuxer( self.container.attach_socket(params={ 'stdout': 1, 'stderr': 1, 'stream': 1, 'logs': 1 })) if self.withgdb: self.container.exec_run( ['gdbserver', '--attach', '0.0.0.0:1234', '1'], privileged=True, detach=True) self.gdbsock = ('127.0.0.1', self.gdbport)
def _validate(self, cwd, executable, argv, env): """ Perform extended validation on the executable path, argv, and envp. Mostly to make Python happy, but also to prevent common pitfalls. """ orig_cwd = cwd cwd = cwd or os.path.curdir argv, env = normalize_argv_env(argv, env, self, 4) if env: env = {bytes(k): bytes(v) for k, v in env} if argv: argv = list(map(bytes, argv)) # # Validate executable # # - Must be an absolute or relative path to the target executable # - If not, attempt to resolve the name in $PATH # if not executable: if not argv: self.error("Must specify argv or executable") executable = argv[0] if not isinstance(executable, str): executable = executable.decode('utf-8') path = env and env.get(b'PATH') if path: path = path.decode() else: path = os.environ.get('PATH') # Do not change absolute paths to binaries if executable.startswith(os.path.sep): pass # If there's no path component, it's in $PATH or relative to the # target directory. # # For example, 'sh' elif os.path.sep not in executable and which(executable, path=path): executable = which(executable, path=path) # 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) self.warn_once( "Could not find executable %r in $PATH, using %r instead" % (tmp, executable)) # There is a path component and user specified a working directory, # it must be relative to that directory. For example, 'bar/baz' with # cwd='foo' or './baz' with cwd='foo/bar' elif orig_cwd: executable = os.path.join(orig_cwd, executable) if not os.path.exists(executable): self.error("%r does not exist" % executable) if not os.path.isfile(executable): self.error("%r is not a file" % executable) if not os.access(executable, os.X_OK): self.error("%r is not marked as executable (+x)" % executable) return executable, argv, env