GeezerVisonThe World Wide Web, restored to 800 pixels of glory. |
| Home|About|Gaming cabinet|Archives|How it works|Starter sites |
GeezerVison is showing docs.python.org — Remote debugging attachment protocol Remote debugging attachment protocol¶This protocol enables external tools to attach to a running CPython process and execute Python code remotely. Attaching to another Python process may require additional permissions or configuration, depending on the platform. Disabling remote debugging¶To disable remote debugging support, use any of the following:
Permission requirements¶Attaching to a running Python process for remote debugging requires special configuration on most platforms. The specific requirements and troubleshooting steps depend on your operating system: Linux In general, you can debug your own processes, but there are several common
configurations that may disable this. Some Linux distributions enable ptrace
restrictions, aka “Yama,” as a form of system hardening. Recent versions of
the
(This is configured on the process being debugged.) You can also turn off ptrace restrictions for all processes until reboot with:
This can also be configured persistently, usually in Note Disabling It is also possible that the If you need to trace a process that you do not own, you will need superuser
access or equivalent. This also applies to processes that have changed their
security credentials, e.g., set-user-ID or set-group-ID processes (though this
is unusual for Python). Try running the debugging command with Note The Finally, note that a process can only have one tracer at a time. If you have
already attached to a Python process under macOS By default, macOS disables the ability to debug other processes. You can modify your Python binary to opt in to being debugged by giving it an ad-hoc code signature with an entitlement enabling it to be debugged. (An ad-hoc “signature” is just a configuration without any actual cryptographic signature or a need for a certificate or anything else such as an Apple developer program membership.) The following commands will create a file echo '{"com.apple.security.get-task-allow": true}' | plutil -convert xml1 -o get-task-allow.plist - codesign --sign - --entitlements get-task-allow.plist path/to/bin/python3 where You should then be able to debug your own Python processes started with that binary. Alternatively, much as with Linux, processes with superuser privileges e.g. Windows To attach to another process, you usually need to run your debugging tool with administrative privileges. Start the command prompt or terminal as Administrator. Some processes may still be inaccessible even with Administrator rights,
unless you have the To resolve file or folder access issues, adjust the security permissions:
Note Ensure you’ve satisfied all Permission requirements before proceeding. This section describes the low-level protocol that enables external tools to inject and execute a Python script within a running CPython process. This mechanism forms the basis of the Warning The execution of the injected script depends on the interpreter reaching a safe evaluation point. As a result, execution may be delayed depending on the runtime state of the target process. Once injected, the script is executed by the interpreter within the target process the next time a safe evaluation point is reached. This approach enables remote execution capabilities without modifying the behavior or structure of the running Python application. Subsequent sections provide a step-by-step description of the protocol, including techniques for locating interpreter structures in memory, safely accessing internal fields, and triggering code execution. Platform-specific variations are noted where applicable, and example implementations are included to clarify each operation. Locating the PyRuntime structure¶CPython places the The To work with a remote Python process, a debugger must first find the memory
address of the The method for finding
The sections below explain how to do this on each supported platform and include example code. Linux (ELF) To find the
The following is an example implementation: def find_py_runtime_linux(pid: int) -> int: # Step 1: Try to find the Python executable in memory binary_path, base_address = find_mapped_binary( pid, name_contains="python" ) # Step 2: Fallback to shared library if executable is not found if binary_path is None: binary_path, base_address = find_mapped_binary( pid, name_contains="libpython" ) # Step 3: Parse ELF headers to get .PyRuntime section offset section_offset = parse_elf_section_offset( binary_path, ".PyRuntime" ) # Step 4: Compute PyRuntime address in memory return base_address + section_offset On Linux systems, there are two main approaches to read memory from another
process. The first is through the For parsing ELF sections, the process involves reading and interpreting the ELF file format structures from the binary file on disk. The ELF header contains a pointer to the section header table. Each section header contains metadata about a section including its name (stored in a separate string table), offset, and size. To find a specific section like .PyRuntime, you need to walk through these headers and match the section name. The section header then provides the offset where that section exists in the file, which can be used to calculate its runtime address when the binary is loaded into memory. You can read more about the ELF file format in the ELF specification. macOS (Mach-O) To find the
The following is an example implementation: def find_py_runtime_macos(pid: int) -> int: # Step 1: Get access to the process's memory handle = get_memory_access_handle(pid) # Step 2: Try to find the Python executable in memory binary_path, base_address = find_mapped_binary( handle, name_contains="python" ) # Step 3: Fallback to libpython if the executable is not found if binary_path is None: binary_path, base_address = find_mapped_binary( handle, name_contains="libpython" ) # Step 4: Parse Mach-O headers to get __DATA,__PyRuntime section offset section_offset = parse_macho_section_offset( binary_path, "__DATA", "__PyRuntime" ) # Step 5: Compute the PyRuntime address in memory return base_address + section_offset On macOS, accessing another process’s memory requires using Mach-O specific APIs
and file formats. The first step is obtaining a The process memory can be examined using The Mach-O format organizes code and data into segments and sections. The
Note that accessing another process’s memory on macOS typically requires elevated privileges - either root access or special security entitlements granted to the debugging process. Windows (PE) To find the
The following is an example implementation: def find_py_runtime_windows(pid: int) -> int: # Step 1: Try to find the Python executable in memory binary_path, base_address = find_loaded_module( pid, name_contains="python" ) # Step 2: Fallback to shared pythonXY.dll if the executable is not # found if binary_path is None: binary_path, base_address = find_loaded_module( pid, name_contains="python3" ) # Step 3: Parse PE section headers to get the RVA of the PyRuntime # section. The section name appears as "PyRuntim" due to the # 8-character limit defined by the PE format (IMAGE_SIZEOF_SHORT_NAME). section_rva = parse_pe_section_offset(binary_path, "PyRuntim") # Step 4: Compute PyRuntime address in memory return base_address + section_rva On Windows, accessing another process’s memory requires using the Windows API
functions like The process memory can be examined by enumerating loaded modules to find the
Python binary or DLL. When found, its PE headers need to be parsed to locate the
The PE format organizes code and data into sections. The Note that accessing another process’s memory on Windows typically requires
appropriate privileges - either administrative access or the Reading _Py_DebugOffsets¶Once the address of the This structure provides version-specific field offsets that are needed to safely read interpreter and thread state memory. These offsets vary between CPython versions and must be checked before use to ensure they are compatible. To read and check the debug offsets, follow these steps:
The following is an example implementation that reads and checks
def read_debug_offsets(pid: int, py_runtime_addr: int) -> DebugOffsets: # Step 1: Read memory from the target process at the PyRuntime address data = read_process_memory( pid, address=py_runtime_addr, size=DEBUG_OFFSETS_SIZE ) # Step 2: Deserialize the raw bytes into a _Py_DebugOffsets structure debug_offsets = parse_debug_offsets(data) # Step 3: Validate the contents of the structure if debug_offsets.cookie != EXPECTED_COOKIE: raise RuntimeError("Invalid or missing debug cookie") if debug_offsets.version != LOCAL_PYTHON_VERSION: raise RuntimeError( "Mismatch between caller and target Python versions" ) if debug_offsets.free_threaded != LOCAL_FREE_THREADED: raise RuntimeError("Mismatch in free-threaded configuration") return debug_offsets Warning Process suspension recommended To avoid race conditions and ensure memory consistency, it is strongly recommended that the target process be suspended before performing any operations that read or write internal interpreter state. The Python runtime may concurrently mutate interpreter data structures—such as creating or destroying threads—during normal execution. This can result in invalid memory reads or writes. A debugger may suspend execution by attaching to the process with Note Some tools, such as profilers or sampling-based debuggers, may operate on a running process without suspension. In such cases, tools must be explicitly designed to handle partially updated or inconsistent memory. For most debugger implementations, suspending the process remains the safest and most robust approach. Locating the interpreter and thread state¶Before code can be injected and executed in a remote Python process, the
debugger must choose a thread in which to schedule execution. This is necessary
because the control fields used to perform remote code injection are located in
the The A thread is typically selected based on its role or ID. In most cases, the main thread is used, but some tools may target a specific thread by its native thread ID. Once the target thread is chosen, the debugger must locate both the interpreter and the associated thread state structures in memory. The relevant internal structures are defined as follows:
To locate a thread:
The following is an example implementation that locates the main thread state: def find_main_thread_state( pid: int, py_runtime_addr: int, debug_offsets: DebugOffsets, ) -> int: # Step 1: Read interpreters_head from PyRuntime interp_head_ptr = ( py_runtime_addr + debug_offsets.runtime_state.interpreters_head ) interp_addr = read_pointer(pid, interp_head_ptr) if interp_addr == 0: raise RuntimeError("No interpreter found in the target process") # Step 2: Read the threads_main pointer from the interpreter threads_main_ptr = ( interp_addr + debug_offsets.interpreter_state.threads_main ) thread_state_addr = read_pointer(pid, threads_main_ptr) if thread_state_addr == 0: raise RuntimeError("Main thread state is not available") return thread_state_addr The following example demonstrates how to locate a thread by its native thread ID: def find_thread_by_id( pid: int, interp_addr: int, debug_offsets: DebugOffsets, target_tid: int, ) -> int: # Start at threads_head and walk the linked list thread_ptr = read_pointer( pid, interp_addr + debug_offsets.interpreter_state.threads_head ) while thread_ptr: native_tid_ptr = ( thread_ptr + debug_offsets.thread_state.native_thread_id ) native_tid = read_int(pid, native_tid_ptr) if native_tid == target_tid: return thread_ptr thread_ptr = read_pointer( pid, thread_ptr + debug_offsets.thread_state.next ) raise RuntimeError("Thread with the given ID was not found") Once a valid thread state has been located, the debugger can proceed with modifying its control fields and scheduling execution, as described in the next section. Writing control information¶Once a valid Each
To complete the injection, the debugger must perform the following steps:
The following is an example implementation: def inject_script( pid: int, thread_state_addr: int, debug_offsets: DebugOffsets, script_path: str ) -> None: # Compute the base offset of _PyRemoteDebuggerSupport support_base = ( thread_state_addr + debug_offsets.debugger_support.remote_debugger_support ) # Step 1: Write the script path into debugger_script_path script_path_ptr = ( support_base + debug_offsets.debugger_support.debugger_script_path ) write_string(pid, script_path_ptr, script_path) # Step 2: Set debugger_pending_call to 1 pending_ptr = ( support_base + debug_offsets.debugger_support.debugger_pending_call ) write_int(pid, pending_ptr, 1) # Step 3: Set _PY_EVAL_PLEASE_STOP_BIT (bit 5, value 1 << 5) in # eval_breaker eval_breaker_ptr = ( thread_state_addr + debug_offsets.debugger_support.eval_breaker ) breaker = read_int(pid, eval_breaker_ptr) breaker |= (1 << 5) write_int(pid, eval_breaker_ptr, breaker) Once these fields are set, the debugger may resume the process (if it was suspended). The interpreter will process the request at the next safe evaluation point, load the script from disk, and execute it. It is the responsibility of the debugger to ensure that the script file remains present and accessible to the target process during execution. Note Script execution is asynchronous. The script file cannot be deleted immediately after injection. The debugger should wait until the injected script has produced an observable effect before removing the file. This effect depends on what the script is designed to do. For example, a debugger might wait until the remote process connects back to a socket before removing the script. Once such an effect is observed, it is safe to assume the file is no longer needed. Summary¶To inject and execute a Python script in a remote process:
Security and threat model¶The remote debugging protocol relies on the same operating system primitives
used by native debuggers such as GDB and LLDB. Attaching to a process
requires the same privileges that those debuggers require, for example
The following principles define what is, and is not, considered a security vulnerability in this feature:
When to use
|
| You are visitor 01402503 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required. |