Skip to content

Commit ad5ce74

Browse files
committed
Issue Gallopsled#1273, make gdb.debug() respect exe param
This feature currently only works for context.native, i.e for LOCAL debugging on 'i386' or 'amd64' Implementation Challenges and solution: 1. `gdbserver` has no immediate support for manipulating argv[0] * Use the `--wrapper` flag, to execve the binary * Note that it must be an execve and not an fork+execve, since otherwise gdbserver doesn't attach 2. In python3 `os.execve` doesn't allow empty argv[0]. * Use `ctypes` to effectively call the execve function from Libc via python * `_generate_execve_script` generates this script and stores in a temp file * `gdbserver --wrapper python script.py -- dummy` to call it
1 parent e02568c commit ad5ce74

File tree

1 file changed

+132
-53
lines changed

1 file changed

+132
-53
lines changed

pwnlib/gdb.py

+132-53
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,89 @@ def debug_shellcode(data, gdbscript=None, vma=None, api=False):
249249

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

252+
def _find_python(which):
253+
"""Finds the path to a Python interpreter."""
254+
for py in ["python2.7", "python2", "python", "python3"]:
255+
found = which(py)
256+
257+
if found is not None:
258+
return found
259+
260+
return None
261+
262+
def _generate_execve_script(exe, args, env):
263+
"""Generates a python script to execve() a binary.
264+
This method uses `ctypes` to call `execve()` directly, since in
265+
python3 os.execve() doesn't allow us to specify argv[0]=NULL or argv[0]=b''
266+
267+
This script might want to be called by `python -c`, to ensure it's safety,
268+
we add typechecks to cast all user-controlled input into hexstrings.
269+
270+
Arguments:
271+
exe(str): Path to the binary to execute
272+
argv(list): List of arguments to pass to the binary
273+
env(dict): Environment variables to pass to the binary
274+
275+
Returns:
276+
The generated script as a string
277+
"""
278+
if args is None:
279+
args = []
280+
if env is None:
281+
env = {}
282+
283+
# Type checks
284+
if type(exe) not in [bytes, bytearray, str]:
285+
log.error("exe must be a string or bytes")
286+
287+
if isinstance(args, list):
288+
for arg in args:
289+
if type(arg) not in [bytes, bytearray, str]:
290+
log.error("args must be a list of strings or bytes")
291+
else:
292+
log.error("args must be a list of strings or bytes")
293+
294+
if isinstance(env, dict):
295+
for key, value in env.items():
296+
if type(key) not in [bytes, bytearray, str]:
297+
log.error("env keys must be strings or bytes")
298+
if type(value) not in [bytes, bytearray, str]:
299+
log.error("env values must be strings or bytes")
300+
else:
301+
log.error("env must be a dictionary of strings or bytes")
302+
303+
# This script calls execve() directly using ctypes
304+
script = """
305+
import ctypes
306+
307+
exe = bytes.fromhex({executable!r})
308+
argv = [bytes.fromhex(x) for x in {formatted_args!r}]
309+
envp = [bytes.fromhex(x) for x in {formatted_env!r}]
310+
311+
def get_string_list(string_list):
312+
#Transform a list of bytes into a ctypes array of char pointers
313+
char_p_array = (ctypes.c_char_p * len(string_list))()
314+
for i, string in enumerate(string_list):
315+
char_p_array[i] = ctypes.c_char_p(string)
316+
317+
return char_p_array
318+
319+
c_exe = ctypes.c_char_p(exe)
320+
c_argv = get_string_list(argv)
321+
c_envp = get_string_list(envp)
322+
323+
# Call execve
324+
libc = ctypes.CDLL(None)
325+
libc.execve(c_exe, c_argv, c_envp)
326+
"""
327+
script = script.format(executable=(packing._encode(exe)).hex(),
328+
formatted_args=[(packing._encode(arg)).hex() for arg in args],
329+
formatted_env=[((packing._encode(k)) + b'=' + bytes(packing._encode(v))).hex() for k, v in env.items()])
330+
331+
# log.debug("Generated execve script:\n%s", script)
332+
333+
return script
334+
252335
def _gdbserver_args(pid=None, path=None, args=None, which=None, env=None):
253336
"""_gdbserver_args(pid=None, path=None, args=None, which=None, env=None) -> list
254337
@@ -260,21 +343,36 @@ def _gdbserver_args(pid=None, path=None, args=None, which=None, env=None):
260343
path(str): Process to launch
261344
args(list): List of arguments to provide on the debugger command line
262345
which(callaable): Function to find the path of a binary.
346+
env(dict): Dictionary containing the debugged process environment variables.
263347
264348
Returns:
265349
A list of arguments to invoke gdbserver.
266350
"""
267-
if [pid, path, args].count(None) != 2:
268-
log.error("Must specify exactly one of pid, path, or args")
351+
if [path, args, pid].count(None) == 3:
352+
log.error("Must specify at least one of pid, path, or args")
353+
354+
if pid is not None:
355+
if [path, args].count(None) != 2:
356+
log.error("Cannot specify both pid and path or args")
357+
358+
elif path is None:
359+
if args:
360+
# Local which needs str not bytes
361+
path = which(packing._decode(args[0]))
362+
else:
363+
log.error("Must specify at least one of pid, path, or args")
364+
365+
366+
path = packing._need_bytes(path, min_wrong=0x80)
367+
368+
if args is None:
369+
args = []
269370

270371
if not which:
271372
log.error("Must specify which.")
272373

273374
gdbserver = ''
274375

275-
if not args:
276-
args = [str(path or pid)]
277-
278376
# Android targets have a distinct gdbserver
279377
if context.bits == 64:
280378
gdbserver = which('gdbserver64')
@@ -285,8 +383,6 @@ def _gdbserver_args(pid=None, path=None, args=None, which=None, env=None):
285383
if not gdbserver:
286384
log.error("gdbserver is not installed")
287385

288-
orig_args = args
289-
290386
gdbserver_args = [gdbserver, '--multi']
291387
if context.aslr:
292388
gdbserver_args += ['--no-disable-randomization']
@@ -296,7 +392,24 @@ def _gdbserver_args(pid=None, path=None, args=None, which=None, env=None):
296392
if pid:
297393
gdbserver_args += ['--once', '--attach']
298394

299-
if env is not None:
395+
# gdbserver does not support passing argv[0] to the executable
396+
# To work around this, we use the --wrapper option to start a python
397+
# script which calls execve() directly
398+
# https://sourceware.org/pipermail/gdb/2013-May/043021.html
399+
if context.native:
400+
script = _generate_execve_script(path, args, env)
401+
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=".py") as tmp:
402+
tmp.write(script)
403+
404+
log.debug("Wrote execve wrapper script to %s", tmp.name)
405+
python = _find_python(which)
406+
gdbserver_args += ["--wrapper", python, tmp.name, "--"]
407+
408+
# Gdbserver needs to start with args, therefore we add a dummy arg if none are specified
409+
if args is None or len(args) == 0 or len(args[0].strip()) == 0:
410+
args = ['dummy']
411+
412+
elif env is not None:
300413
env_args = []
301414
for key in tuple(env):
302415
if key.startswith(b'LD_'): # LD_PRELOAD / LD_LIBRARY_PATH etc.
@@ -363,7 +476,7 @@ def _get_runner(ssh=None):
363476
else: return tubes.process.process
364477

365478
@LocalContext
366-
def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api=False, force_args=False, **kwargs):
479+
def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api=False, **kwargs):
367480
r"""
368481
Launch a GDB server with the specified command line,
369482
and launches GDB to attach to it.
@@ -435,6 +548,14 @@ def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api=
435548
>>> io.interactive() # doctest: +SKIP
436549
>>> io.close()
437550
551+
Create a new process with empty argv[0]
552+
553+
>>> io = gdb.debug([''], exe="/bin/bash")
554+
>>> io.sendline(b"echo $0")
555+
>>> io.recvline()
556+
b'\n'
557+
>>> io.close()
558+
438559
Create a new process, and stop it at '_start'
439560
440561
>>> io = gdb.debug('bash', '''
@@ -533,50 +654,8 @@ def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api=
533654
log.warn_once("Skipping debugger since context.noptrace==True")
534655
return runner(args, executable=exe, env=env)
535656

536-
if force_args:
537-
# gdbserver does not support passing argv[0] to the executable
538-
# To work around this, we use the --wrapper option
539-
# https://sourceware.org/pipermail/gdb/2013-May/043021.html
540-
541-
# Here we create a wrapper that calls execve with the correct argv[0]
542-
src = """
543-
#include <unistd.h>
544-
int main(){
545-
char** argv = {0};
546-
execv("EXE", argv, envp);
547-
}
548-
"""
549-
exe = exe.encode()
550-
src = src.replace("EXE", str(exe)[2:-1])
551-
print(src)
552-
# And a execve binary
553-
outfile = tempfile.NamedTemporaryFile()
554-
outfile.close()
555-
with tempfile.NamedTemporaryFile(suffix=".c") as srcfile:
556-
print(srcfile.name, outfile.name)
557-
srcfile.write(src.encode())
558-
srcfile.flush()
559-
os.system("gcc -o {} {}".format(outfile.name, srcfile.name))
560-
os.system("echo WIN")
561-
562-
563-
# # Transform env to list if env:
564-
# env_list = [k + b'=' + v for k, v in env.items()]
565-
# else:
566-
# env_list = 0
567-
568-
# # Transform args to list
569-
# args = [bytes(a) for a in args]
570-
571-
572-
# Create gdbserver args
573-
args = ['gdbserver', '--no-disable-randomization', '--wrapper', outfile.name, '--']
574-
args += ["localhost:0", "dummy"]
575-
576-
577-
578-
elif ssh or context.native or (context.os == 'android'):
579-
args = _gdbserver_args(args=args, which=which, env=env)
657+
if ssh or context.native or (context.os == 'android'):
658+
args = _gdbserver_args(args=args, path=exe, which=which, env=env)
580659
else:
581660
qemu_port = random.randint(1024, 65535)
582661
qemu_user = qemu.user_path()

0 commit comments

Comments
 (0)