From 888a2aa344009850338a7800bd577fa613f3e994 Mon Sep 17 00:00:00 2001 From: Gilad Reti Date: Thu, 2 Oct 2025 23:46:26 +0300 Subject: [PATCH 1/2] support preexec_fn in debug() Previously, preexec_fn was mistakengly run is gdbserver's context. Run it inside the debuggee instead to allow run/debug workflows. --- pwnlib/gdb.py | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/pwnlib/gdb.py b/pwnlib/gdb.py index fe959ad4a..9551ce79d 100644 --- a/pwnlib/gdb.py +++ b/pwnlib/gdb.py @@ -245,8 +245,8 @@ def debug_shellcode(data, gdbscript=None, vma=None, api=False): return debug(tmp_elf, gdbscript=gdbscript, arch=context.arch, api=api) -def _execve_script(argv, executable, env, ssh): - """_execve_script(argv, executable, env, ssh) -> str +def _execve_script(argv, executable, env, ssh, preexec_fn, preexec_args): + """_execve_script(argv, executable, env, ssh, preexec_fn, preexec_args) -> str Returns the filename of a python script that calls execve the specified program with the specified arguments. @@ -258,6 +258,8 @@ def _execve_script(argv, executable, env, ssh): 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 + preexec_fn(callable): Callable to invoke before exec() + preexec_args(tuple): Args to pass to callable Returns: The filename of the created script. @@ -269,7 +271,8 @@ def _execve_script(argv, executable, env, ssh): # ssh.process with run=false creates the script for us return ssh.process(argv, executable=executable, env=env, run=False) - script = misc._create_execve_script(argv=argv, executable=executable, env=env, log=log) + script = misc._create_execve_script(argv=argv, executable=executable, env=env, log=log, preexec_fn=preexec_fn, + preexec_args=preexec_args) script = script.strip() # Create a temporary file to hold the script tmp = tempfile.NamedTemporaryFile(mode="w+t",prefix='pwnlib-execve-', suffix='.py', delete=False) @@ -417,7 +420,8 @@ def _get_runner(ssh=None): else: return tubes.process.process @LocalContext -def debug(args, gdbscript=None, gdb_args=None, exe=None, ssh=None, env=None, port=0, gdbserver_args=None, sysroot=None, api=False, **kwargs): +def debug(args, gdbscript=None, gdb_args=None, exe=None, ssh=None, env=None, port=0, gdbserver_args=None, sysroot=None, api=False, + preexec_fn=None, preexec_args=(), **kwargs): r""" Launch a GDB server with the specified command line, and launches GDB to attach to it. @@ -436,6 +440,14 @@ def debug(args, gdbscript=None, gdb_args=None, exe=None, ssh=None, env=None, por gdb to load a local version of binaries/libraries instead of downloading them from the gdbserver, which is faster api(bool): Enable access to GDB Python API. + preexec_fn(callable): + Function which is executed on the remote side before execve(). + This **MUST** be a self-contained function -- it must perform + all of its own imports, and cannot refer to variables outside + its scope. + preexec_args(object): + Argument passed to ``preexec_fn``. + This **MUST** only consist of native Python objects. Returns: :class:`.process` or :class:`.ssh_channel`: A tube connected to the target process. @@ -537,6 +549,17 @@ def debug(args, gdbscript=None, gdb_args=None, exe=None, ssh=None, env=None, por >>> io.close() >>> os.remove("./local-libc.so") # cleanup + Use preexec_fn + + >>> def dup2(from_, to): + ... import os + ... os.dup2(from_, to) + >>> p = gdb.debug(['python','-c','import os; print(os.read(2,1024).decode())'], + ... preexec_fn=dup2, preexec_args=(0,2)) + >>> p.sendline(b'hello') + >>> p.recvline() + b'hello\n' + Using SSH: @@ -649,7 +672,7 @@ def debug(args, gdbscript=None, gdb_args=None, exe=None, ssh=None, env=None, por return runner(args, executable=exe, env=env) if ssh or context.native or (context.os == 'android'): - if len(args) > 0 and which(packing._decode(args[0])) == packing._decode(exe): + if len(args) > 0 and which(packing._decode(args[0])) == packing._decode(exe) and preexec_fn is None: args = _gdbserver_args(gdbserver_args=gdbserver_args, args=args, port=port, which=which, env=env) else: @@ -657,7 +680,7 @@ def debug(args, gdbscript=None, gdb_args=None, exe=None, ssh=None, env=None, por # 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) + script = _execve_script(args, executable=exe, env=env, ssh=ssh, preexec_fn=preexec_fn, preexec_args=preexec_args) args = _gdbserver_args(gdbserver_args=gdbserver_args, args=args, port=port, which=which, env=env, python_wrapper_script=script) else: qemu_port = port if port != 0 else random.randint(1024, 65535) From 8429eb24633468d9ea0999e5ea287270c012a706 Mon Sep 17 00:00:00 2001 From: Gilad Reti Date: Thu, 2 Oct 2025 21:30:46 +0000 Subject: [PATCH 2/2] update Changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 375e53f27..810aec3b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,7 @@ The table below shows which release corresponds to each branch, and what date th - [#2611][2611] Cleanup `pwnlib.lexer` exports and imports - [#2610][2610] Fix `log.progress` ignoring `context.log_console` - [#2615][2615] tube/process: Fix redirecting stderr to stdout on Windows +- [#2630][2630] support `preexec_fn` in `debug()` [2598]: https://github.com/Gallopsled/pwntools/pull/2598 [2419]: https://github.com/Gallopsled/pwntools/pull/2419 @@ -128,6 +129,7 @@ The table below shows which release corresponds to each branch, and what date th [2611]: https://github.com/Gallopsled/pwntools/pull/2611 [2610]: https://github.com/Gallopsled/pwntools/pull/2610 [2615]: https://github.com/Gallopsled/pwntools/pull/2615 +[2630]: https://github.com/Gallopsled/pwntools/pull/2630 ## 4.15.0 (`beta`)