Skip to content

Bugfix gdb.debug: exe parameter now respected #2227

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,13 @@ The table below shows which release corresponds to each branch, and what date th
- [#2117][2117] Add -p (--prefix) and -s (--separator) arguments to `hex` command
- [#2221][2221] Add shellcraft.sleep template wrapping SYS_nanosleep
- [#2219][2219] Fix passing arguments on the stack in shellcraft syscall template
- [#2227][2227] Fix gdb.debug: exe parameter now respected

[2202]: https://github.com/Gallopsled/pwntools/pull/2202
[2117]: https://github.com/Gallopsled/pwntools/pull/2117
[2221]: https://github.com/Gallopsled/pwntools/pull/2221
[2219]: https://github.com/Gallopsled/pwntools/pull/2219
[2227]: https://github.com/Gallopsled/pwntools/pull/2227

## 4.11.0 (`beta`)

Expand Down
100 changes: 97 additions & 3 deletions pwnlib/gdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@

from contextlib import contextmanager
import os
import sys
import platform
import psutil
import random
Expand Down Expand Up @@ -249,7 +250,65 @@ def debug_shellcode(data, gdbscript=None, vma=None, api=False):

return debug(tmp_elf, gdbscript=gdbscript, arch=context.arch, api=api)

def _gdbserver_args(pid=None, path=None, args=None, which=None, env=None):
def _execve_script(argv, executable, env, ssh):
"""_execve_script(argv, executable, env, ssh) -> str

Returns the filename of a python script that calls
execve the specified program with the specified arguments.
This script is suitable to call with gdbservers ``--wrapper`` option,
so we have more control over the environment of the debugged process.

Arguments:
argv(list): List of arguments to pass to the program
executable(bytes): Path to the program to run
env(dict): Environment variables to pass to the program
ssh(ssh): SSH connection to use if we are debugging a remote process

Returns:
The filename of the created script.
"""
# Make sure args are bytes, not str or bytearray.
argv = [bytes(packing._encode(arg)) for arg in argv]
executable = packing._encode(executable)
if ssh:
# ssh.process creates the script for us
return ssh.process(argv, executable=executable, env=env, run=False)

# Convert environment to a list of key=value strings
if env is None:
env_list = []
else:
env_list = [bytes(key) + b"=" + bytes(value) for key, value in env.items()]

# Create a python script that calls execve
# This script uses ctypes to call execve directly, instead of using os.execve
# since os.execve doesn't allow an empty argv[0]
exe = sys.executable
script = """
#!{exe!s}
import ctypes

# ctypes helper to convert a python list to a NULL-terminated C array
def to_carray(py_list):
py_list += [None] # NULL-terminated
return (ctypes.c_char_p * len(py_list))(*py_list)
c_argv = to_carray({argv!r})
c_env = to_carray({env_list!r})
# Call execve
execve = ctypes.CDLL(None).execve
execve({executable!r}, c_argv, c_env)""".format(**locals())
script = script.strip()
# Create a temporary file to hold the script
tmp = tempfile.NamedTemporaryFile(mode="w+t",prefix='pwn', suffix='.py', delete=False)
tmp.write(script)
# Make script executable
os.fchmod(tmp.fileno(), 0o755)
log.debug("Created execve wrapper script %s:\n%s", tmp.name, script)

return tmp.name


def _gdbserver_args(pid=None, path=None, args=None, which=None, env=None, python_wrapper_script=None):
"""_gdbserver_args(pid=None, path=None, args=None, which=None, env=None) -> list

Sets up a listening gdbserver, to either connect to the specified
Expand All @@ -260,6 +319,8 @@ def _gdbserver_args(pid=None, path=None, args=None, which=None, env=None):
path(str): Process to launch
args(list): List of arguments to provide on the debugger command line
which(callaable): Function to find the path of a binary.
env(dict): Environment variables to pass to the program
python_wrapper_script(str): Path to a python script to use with ``--wrapper``

Returns:
A list of arguments to invoke gdbserver.
Expand Down Expand Up @@ -296,7 +357,10 @@ def _gdbserver_args(pid=None, path=None, args=None, which=None, env=None):
if pid:
gdbserver_args += ['--once', '--attach']

if env is not None:
if python_wrapper_script:
gdbserver_args += ['--wrapper', python_wrapper_script, '--']

elif env is not None:
env_args = []
for key in tuple(env):
if key.startswith(b'LD_'): # LD_PRELOAD / LD_LIBRARY_PATH etc.
Expand Down Expand Up @@ -458,6 +522,13 @@ def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api=

>>> io.interactive() # doctest: +SKIP
>>> io.close()

Start a new process with modified argv[0]
>>> io = gdb.debug("/bin/sh", 'continue', argv=[b'\xde\xad\xbe\xef'])
>>> io.sendline(b"echo $0")
>>> io.recvline()
b'$ \xde\xad\xbe\xef\n'
>>> io.close()

Using GDB Python API:

Expand Down Expand Up @@ -509,6 +580,20 @@ def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api=
Interact with the process
>>> io.interactive() # doctest: +SKIP
>>> io.close()

Using a modified argv[0] on a remote process
>>> io = gdb.debug("/bin/sh", 'continue', argv=[b'\xde\xad\xbe\xef'], ssh=shell)
>>> io.sendline(b"echo $0")
>>> io.recvline()
b'$ \xde\xad\xbe\xef\n'
>>> io.close()

Using an empty argv[0] on a remote process
>>> io = gdb.debug("/bin/sh", 'continue', argv=[], ssh=shell)
>>> io.sendline(b"echo $0")
>>> io.recvline()
b'$ \n'
>>> io.close()
"""
if isinstance(args, six.integer_types + (tubes.process.process, tubes.ssh.ssh_channel)):
log.error("Use gdb.attach() to debug a running process")
Expand All @@ -529,12 +614,21 @@ def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api=
if env:
env = {bytes(k): bytes(v) for k, v in env}

exe = which(packing._decode(exe or args[0]))
if not exe:
log.error("Could not find executable %r" % exe)

if context.noptrace:
log.warn_once("Skipping debugger since context.noptrace==True")
return runner(args, executable=exe, env=env)

if ssh or context.native or (context.os == 'android'):
args = _gdbserver_args(args=args, which=which, env=env)
# GDBServer is limited in it's ability to manipulate argv[0]
# but can use the ``--wrapper`` option to execute commands and catches
# ``execve`` calls.
# Therefore, we use a wrapper script to execute the target binary
script = _execve_script(args, executable=exe, env=env, ssh=ssh)
args = _gdbserver_args(args=args, which=which, env=env, python_wrapper_script=script)
else:
qemu_port = random.randint(1024, 65535)
qemu_user = qemu.user_path()
Expand Down
1 change: 1 addition & 0 deletions pwnlib/tubes/ssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,7 @@ def is_exe(path):
self.chmod('+x', tmpfile)

self.info("Uploading execve script to %r" % tmpfile)
script = packing._encode(script)
self.upload_data(script, tmpfile)
return tmpfile

Expand Down