Пример #1
0
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
Пример #2
0
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))
Пример #3
0
    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)
Пример #4
0
    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')
Пример #5
0
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'.')))
Пример #6
0
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('.')))
Пример #7
0
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
Пример #8
0
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]
Пример #9
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
Пример #10
0
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]
Пример #11
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))
Пример #12
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
Пример #13
0
    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')
Пример #14
0
    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)
Пример #15
0
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
Пример #16
0
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
Пример #17
0
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()
Пример #18
0
    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
Пример #19
0
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
Пример #20
0
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]
Пример #21
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
Пример #22
0
    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
Пример #23
0
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()
Пример #24
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, (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
Пример #25
0
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)
Пример #26
0
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)
Пример #28
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.
        """

        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