Skip to content

Commit 70663f1

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 70663f1

File tree

1 file changed

+131
-49
lines changed

1 file changed

+131
-49
lines changed

pwnlib/gdb.py

+131-49
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,12 +343,30 @@ 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 = [path or str(pid).encode("utf-8")]
269370

270371
if not which:
271372
log.error("Must specify which.")
@@ -285,8 +386,6 @@ def _gdbserver_args(pid=None, path=None, args=None, which=None, env=None):
285386
if not gdbserver:
286387
log.error("gdbserver is not installed")
287388

288-
orig_args = args
289-
290389
gdbserver_args = [gdbserver, '--multi']
291390
if context.aslr:
292391
gdbserver_args += ['--no-disable-randomization']
@@ -296,7 +395,24 @@ def _gdbserver_args(pid=None, path=None, args=None, which=None, env=None):
296395
if pid:
297396
gdbserver_args += ['--once', '--attach']
298397

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

365481
@LocalContext
366-
def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api=False, force_args=False, **kwargs):
482+
def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api=False, **kwargs):
367483
r"""
368484
Launch a GDB server with the specified command line,
369485
and launches GDB to attach to it.
@@ -435,6 +551,14 @@ def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api=
435551
>>> io.interactive() # doctest: +SKIP
436552
>>> io.close()
437553
554+
Create a new process with empty argv[0]
555+
556+
>>> io = gdb.debug([''], exe="/bin/bash")
557+
>>> io.sendline(b"echo $0")
558+
>>> io.recvline()
559+
b'\n'
560+
>>> io.close()
561+
438562
Create a new process, and stop it at '_start'
439563
440564
>>> io = gdb.debug('bash', '''
@@ -533,50 +657,8 @@ def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api=
533657
log.warn_once("Skipping debugger since context.noptrace==True")
534658
return runner(args, executable=exe, env=env)
535659

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-
578660
elif ssh or context.native or (context.os == 'android'):
579-
args = _gdbserver_args(args=args, which=which, env=env)
661+
args = _gdbserver_args(args=args, path=exe, which=which, env=env)
580662
else:
581663
qemu_port = random.randint(1024, 65535)
582664
qemu_user = qemu.user_path()

0 commit comments

Comments
 (0)