远程调试附加协议

此协议使得外部工具能够附加到正在运行的 CPython 进程并远程执行 Python 代码。

附加到另一个 Python 进程可能需要额外的权限或配置,这取决于具体平台。

禁用远程调试

要禁用远程调试支持,请使用下列方式之一:

权限需求

附加到正在运行的 Python 进程用于远程调试在大多数平台上都需要特殊的配置。 具体的要求和故障排除步骤取决于你的操作系统:

Linux

总的来说,你可以调试你所拥有的进程,但有几种常见配置可能会禁用此功能。 某些 Linux 发行版会启用 ptrace 限制,或称 "Yama" 作为一种系统加固方式。 最新版本的 setpriv 命令(util-linux 2.41,2025 年 6 月发布)让你可以基于每个进程放松 ptrace 限制:

setpriv --ptracer any python3

(这是在 正被调试的 进程上配置的。)。 你也可以使用以下命令为所有进程关闭 ptrace 限制直到重启系统:

echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope

这也可以被持久化配置,通常是在 /etc/sysctl.d 中。

备注

禁用 ptrace_scope 会降低系统加固程度,因此仅应在低安全级别环境中执行。

ptrace 系统调用也可能因安全过滤器而被禁用。尤其常见于某些容器软件的旧版本。Docker 19.03 或更高版本(发布于 2019 年)以及 containerd 1.6.7 或更高版本(发布于 2022 年)在基于 Linux 内核 4.8 或更高版本运行时,将自动允许在容器内使用 ptrace 系统调用。如果您无法升级到这些版本,可以通过使用诸如 --security-opt seccomp=unconfined 之类的选项创建容器,以禁用该容器的系统调用安全过滤器。这会削弱容器的隔离性,因此应仅在低安全级别环境中使用此方法。

如果您需要跟踪一个*并非*由您所有进程的进程,则需要超级用户权限或同等权限。这也适用于已更改安全凭据的进程,例如 set-user-ID 或 set-group-ID 进程(尽管在 Python 中这种情况并不常见)。请尝试使用 sudo -E 运行调试命令。

备注

CAP_SYS_PTRACE 功能等同于超级用户权限,因为它允许对*任意*进程进行调试,而不仅仅是自己的进程。您可能会在网络上看到一些建议,建议利用该功能来绕过 ptrace 的限制或系统调用过滤器。这种做法在实践中或许可行,sudo 同样如此;但这样一来,调试进程将获得远超其所需权限的访问权限,因此应仅在低安全级别环境中使用。

最后请注意,一个进程一次只能使用一个追踪器。如果您已经通过 strace、gdb 等标识符连接到某个 Python 进程,就无法同时使用远程调试功能。(超级用户权限也无法绕过这一限制。)

macOS

默认情况下,macOS 会禁用对其他进程的调试功能。

您可以通过为 Python 二进制文件提供一个**临时代码签名**,并设置一项允许对其进行调试的**权限**,从而将其修改为接受调试。(临时“签名”仅是一种配置,不包含任何实际的加密签名,也不需要证书或其他类似材料,例如 Apple 开发者计划会员资格。)

以下命令将创建一个具有必要权限的文件 get-task-allow.plist,并将其添加到 Python 二进制文件中:

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

其中 path/to/bin/python3 是 Python 二进制文件的路径;您可以通过例如运行 which python3 或在 Python REPL 中评估 sys._base_executable 来找到该路径。(这些说明适用于非框架构建的 Python;框架构建可能需要采用不同的配置方式。)

之后,您应该就能对使用该二进制文件启动的自有 Python 进程进行调试了。

或者,与 Linux 类似,具有超级用户权限的进程(例如 sudo)不受此检查的限制,因此可以调试系统中任何用户的进程(不过,由于系统完整性保护的存在,针对特定二进制文件——例如操作系统提供的命令——还会进行额外的检查)。

Windows

要附加到另一个进程,您通常需要以管理员权限运行调试工具:以管理员身份运行命令提示符或者终端。

使用管理员权限时,除非启用 SeDebugPrivilege 权限,否则有的进程仍可能无法被访问。

要解决文件或文件夹访问的问题,请调整安全权限:

  1. 右键文件或文件夹并选择 属性。

  2. 在 安全 选项卡中查看有访问权限的用户和用户组。

  3. 点击 编辑 以调整权限。

  4. 选择您的用户账户。

  5. 在 权限 中,按需勾选 读取 或者 完全控制。

  6. 在点击 应用 后点击 确定。

备注

在继续前,请确保您已满足所有 权限需求。

本节描述了低级协议,该协议使外部工具能够在运行的 CPython 进程中注入和执行 Python 脚本。

该机制构成了 sys.remote_exec() 函数的基础,该函数用于指示远程 Python 进程执行指定的 .py 文件。但本节并不记录该函数的具体用法,而是详细阐述其底层协议的工作原理——该协议以目标 Python 进程的 pid 和待执行的 Python 源文件路径作为输入。这些信息支持协议的独立重新实现,且不受编程语言限制。

警告

注入脚本的执行依赖于解释器到达安全的求值点。因此,实际执行时机可能会因目标进程的运行时状态而产生延迟。

一旦注入,脚本将在解释器下一次达到安全求值点时在目标进程中执行。这种方法能够在不修改运行中 Python 应用的行为或结构的情况下实现远程执行功能。

后续各节提供了该协议的逐步描述,包括定位内存中解释器结构的技术、安全访问内部字段以及触发代码执行的方法。适用的情况下会注明平台特定的变体,并包含示例实现以澄清每个操作。

定位 PyRuntime 结构

CPython 将 PyRuntime 结构放置在一个专用的二进制节中,以帮助外部工具在运行时找到它。该节的名称和格式因平台而异。例如,在 ELF 系统上使用 .PyRuntime,在 macOS 上使用 __DATA,__PyRuntime。工具可以通过检查磁盘上的二进制文件来找到该结构的偏移量。

PyRuntime 结构包含 CPython 的全局解释器状态,并提供对其他内部数据的访问,包括解释器列表、线程状态和调试器支持字段。

要处理远程 Python 进程,调试器首先必须在目标进程中找到 PyRuntime 结构的内存地址。这个地址不能硬编码或通过符号名计算,因为它取决于操作系统加载二进制文件的位置。

查找 PyRuntime 的方法取决于平台,但一般步骤是相同的:

  1. 找到 Python 二进制文件或共享库在目标进程中加载的基址。

  2. 使用磁盘上的二进制文件定位 .PyRuntime 段的偏移。

  3. 将段偏移加到基址上,计算出内存中的地址。

以下部分将说明在每个受支持平台上如何进行此操作,并包括示例代码。

Linux (ELF)

在 Linux 上查找 PyRuntime 结构:

  1. 读取进程的内存映射 (例如,/proc/<pid>/maps) 以找到 Python 可执行文件或 libpython 加载的地址。

  2. 解析二进制文件中的 ELF 段头,获取 .PyRuntime 段的偏移。

  3. 将此偏移加到步骤 1 中的基址上,得到 PyRuntime 的内存地址。

以下是一个示例实现:

def find_py_runtime_linux(pid: int) -> int:
    # 步骤 1:尝试在内存中找到 Python 可执行文件
    binary_path, base_address = find_mapped_binary(
        pid, name_contains="python"
    )

    # 步骤 2:如果找不到可执行文件,则回退到共享库
    if binary_path is None:
        binary_path, base_address = find_mapped_binary(
            pid, name_contains="libpython"
        )

    # 步骤 3:解析 ELF 头以获取.PyRuntime 节的偏移量
    section_offset = parse_elf_section_offset(
        binary_path, ".PyRuntime"
    )

    # 步骤 4:计算内存中的 PyRuntime 地址
    return base_address + section_offset

在 Linux 系统上,有两种主要方法读取另一个进程的内存。第一种是通过 /proc 文件系统,具体来说是通过读取 /proc/[pid]/mem ,它提供了对进程内存的直接访问。这需要适当的权限——要么是与目标进程相同的用户,要么拥有 root 权限。第二种方法是使用 process_vm_readv() 系统调用,它提供了在进程间复制内存的更高效方式。虽然 ptrace 的 PTRACE_PEEKTEXT 操作也可以用来读取内存,但它显著较慢,因为它一次只读取一个字,并且需要在跟踪器和被跟踪进程之间进行多次上下文切换。

为了解析 ELF 节,过程包括从磁盘上的二进制文件中读取和解释 ELF 文件格式结构。ELF 头部包含一个指向节头表的指针。每个节头包含有关节的元数据,包括其名称(存储在单独的字符串表中)、偏移量和大小。要查找特定节(如.PyRuntime),需要遍历这些头部并匹配节名称。节头然后提供该节在文件中存在的偏移量,这可以用来计算二进制文件加载到内存时的运行时地址。

你可以在 ELF 规范 中了解更多关于 ELF 文件格式的信息。

macOS (Mach-O)

在 macOS 上查找 PyRuntime 结构:

  1. 调用 task_for_pid() 以获取目标进程的 mach_port_t 任务端口。此句柄用于通过 mach_vm_read_overwrite 和 mach_vm_region 等 API 读取内存。

  2. 扫描内存区域,找到包含 Python 可执行文件或 libpython 的区域。

  3. 从磁盘加载二进制文件并解析 Mach-O 头部,以在 __DATA 段中找到名为 PyRuntime 的节。在 macOS 上,符号名称自动以一个下划线为前缀,因此 PyRuntime 符号在符号表中显示为 _PyRuntime,但节名称不受影响。

以下是一个示例实现:

def find_py_runtime_macos(pid: int) -> int:
    # 步骤 1:访问进程的内存
    handle = get_memory_access_handle(pid)

    # 步骤 2:尝试在内存中找到 Python 可执行文件
    binary_path, base_address = find_mapped_binary(
        handle, name_contains="python"
    )

    # 步骤 3:如果找不到可执行文件,则回退到 libpython
    if binary_path is None:
        binary_path, base_address = find_mapped_binary(
            handle, name_contains="libpython"
        )

    # 步骤 4:解析 Mach-O 头以获取__DATA,__PyRuntime 段的偏移量
    section_offset = parse_macho_section_offset(
        binary_path, "__DATA", "__PyRuntime"
    )

    # 步骤 5:计算内存中的 PyRuntime 地址
    return base_address + section_offset

在 macOS 上,访问另一个进程的内存需要使用 Mach-O 特定的 API 和文件格式。第一步是通过 task_for_pid() 获取 task_port 句柄,这提供了对目标进程内存空间的访问。此句柄通过 mach_vm_read_overwrite() 等 API 启用内存操作。

可以使用 mach_vm_region() 检查进程内存,以扫描虚拟内存空间,而 proc_regionfilename() 帮助识别每个内存区域加载了哪些二进制文件。当找到 Python 二进制文件或库时,需要解析其 Mach-O 头部以定位 PyRuntime 结构。

Mach-O 格式将代码和数据组织到段和节中。PyRuntime 结构位于 __DATA 段中的名为 __PyRuntime 的节内。实际的运行时地址计算涉及找到作为二进制文件基址的 __TEXT 段,然后定位包含目标节的 __DATA 段。最终地址是通过将基址与 Mach-O 头部中的适当节偏移量组合来计算的。

请注意,在 macOS 上访问另一个进程的内存通常需要提升权限——要么是 root 访问权限,要么是授予调试进程的特殊安全权限。

Windows (PE)

在 Windows 上查找 PyRuntime 结构:

  1. 使用 ToolHelp API 枚举目标进程中加载的所有模块。这通过使用如 CreateToolhelp32Snapshot, Module32First, 和 Module32Next 等函数来完成。

  2. 识别对应于 python.exe 或 pythonXY.dll 的模块,其中 X 和 Y 是 Python 版本的主次版本号,并记录其基址。

  3. 定位 PyRuntim 节。由于 PE 格式对节名称有 8 个字符的限制 (定义为 IMAGE_SIZEOF_SHORT_NAME),原始名称 PyRuntime 被截断。 此节包含 PyRuntime 结构。

  4. 检索节的相对虚拟地址(RVA),并将其添加到模块的基址。

以下是一个示例实现:

def find_py_runtime_windows(pid: int) -> int:
    # 步骤 1:尝试在内存中找到 Python 可执行文件
    binary_path, base_address = find_loaded_module(
        pid, name_contains="python"
    )

    # 步骤 2:如果可执行文件未找到,则回退到共享的 pythonXY.dll
    if binary_path is None:
        binary_path, base_address = find_loaded_module(
            pid, name_contains="python3"
        )

    # 步骤 3:解析 PE 节头以获取 PyRuntime 节的相对虚拟地址(RVA)。
    # 由于 PE 格式(IMAGE_SIZEOF_SHORT_NAME)规定的 8 字符限制,
    # 该节的名称显示为"PyRuntim"。
    section_rva = parse_pe_section_offset(binary_path, "PyRuntim")

    # 步骤 4:计算内存中的 PyRuntime 地址
    return base_address + section_rva

在 Windows 上,访问另一个进程的内存需要使用 Windows API 函数,如 CreateToolhelp32Snapshot() 和 Module32First()/Module32Next() 来枚举已加载的模块。OpenProcess() 函数提供了一个句柄,用于访问目标进程的内存空间,通过 ReadProcessMemory() 实现内存操作。

可以通过枚举已加载的模块来检查进程内存,以找到 Python 二进制文件或 DLL。找到后,需要解析其 PE 头以定位 PyRuntime 结构。

PE 格式将代码和数据组织到节中。PyRuntime 结构位于名为 "PyRuntim" 的节中(由于 PE 的 8 字符名称限制,从 "PyRuntime" 截断)。实际的运行时地址计算涉及从模块入口找到模块的基址,然后在 PE 头中定位目标节。最终地址是通过将基址与 PE 节头中的节的虚拟地址组合来计算的。

请注意,在 Windows 上访问另一个进程的内存通常需要适当的权限——要么是管理员访问权限,要么是授予调试进程的 SeDebugPrivilege 权限。

读取_Py_DebugOffsets

一旦确定了 PyRuntime 结构的地址,下一步就是读取位于 PyRuntime 块开头的 _Py_DebugOffsets 结构。

该结构提供了特定版本的字段偏移量,这些偏移量用于安全地读取解释器和线程状态内存。这些偏移量在 CPython 版本之间有所变化,必须在使用前进行检查以确保它们是兼容的。

要读取和检查调试偏移量,请按照以下步骤操作:

  1. 从目标进程的 PyRuntime 地址开始读取内存,覆盖的字节数与 _Py_DebugOffsets 结构相同。该结构位于 PyRuntime 内存块的起始位置。其布局在 CPython 的内部头文件中定义,并在给定的小版本中保持不变,但在大版本中可能会发生变化。

  2. 检查该结构是否包含有效数据:

    • cookie 字段必须与预期的调试标记匹配。

    • version 字段必须与调试器使用的 Python 解释器版本匹配。

    • 如果调试器或目标进程使用的是预发布版本(例如,alpha、beta 或发布候选版本),则版本必须完全匹配。

    • free_threaded 字段在调试器和目标进程中必须具有相同的值。

  3. 如果结构体有效,其中包含的偏移量可以用于定位内存中的字段。如果任何检查失败,调试器应停止操作,以避免以错误格式读取内存。

以下是一个读取和检查 _Py_DebugOffsets 的示例实现:

def read_debug_offsets(pid: int, py_runtime_addr: int) -> DebugOffsets:
    # 步骤 1:从目标进程中读取 PyRuntime 地址处的内存
    data = read_process_memory(
        pid, address=py_runtime_addr, size=DEBUG_OFFSETS_SIZE
    )

    # 步骤 2:将原始字节反序列化为_Py_DebugOffsets 结构体
    debug_offsets = parse_debug_offsets(data)

    # 步骤 3:验证结构体的内容
    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

警告

建议挂起进程

为避免竞态条件并确保内存一致性,在执行任何读取或写入解释器内部状态的操作前,强烈建议先挂起目标进程。Python 运行时可能在正常执行期间并发修改解释器数据结构(例如创建或销毁线程),这可能导致无效的内存读写操作。

调试器可以通过使用 ptrace 附加到进程或发送 SIGSTOP 信号来挂起执行。只有在调试器端的内存操作完成后,才应恢复执行。

备注

一些工具,如性能分析器或基于采样的调试器,可以在不挂起运行进程的情况下操作。在这种情况下,工具必须明确设计以处理部分更新或不一致的内存。对于大多数调试器实现来说,挂起进程仍然是最安全、最稳健的方法。

定位解释器和线程状态

在远程 Python 进程中注入并执行代码前,调试器必须选定一个目标线程来调度执行。这是因为用于远程代码注入的控制字段位于 _PyRemoteDebuggerSupport 结构体中,而该结构体又嵌入在 PyThreadState 对象内。调试器通过修改这些字段来请求执行已注入的脚本。

PyThreadState 结构体表示在 Python 解释器内运行的线程。它维护线程的求值上下文,并包含调试器协调所需的字段。因此,定位一个有效的 PyThreadState 是触发远程执行的关键前提。

通常基于线程的角色或 ID 来选择线程。在大多数情况下,使用主线程,但一些工具可能通过其本地线程 ID 定位特定线程。一旦选择了目标线程,调试器必须在内存中定位解释器和相关的线程状态结构。

相关内部结构体定义如下:

  • PyInterpreterState 表示一个隔离的 Python 解释器实例。每个解释器维护其自己的导入模块集、内置状态和线程状态列表。尽管大多数 Python 应用程序使用单个解释器,但 CPython 支持在同一进程中使用多个解释器。

  • PyThreadState 表示在解释器内运行的线程。它包含执行状态和调试器使用的控制字段。

要定位一个线程:

  1. 使用偏移量 runtime_state.interpreters_head 获取 PyRuntime 结构体中第一个解释器的地址。这是活动解释器链表的入口点。

  2. 使用偏移量 interpreter_state.threads_main 访问与选定解释器相关联的主线程状态。这通常是目标的最可靠线程。

  3. 可选地,使用偏移量 interpreter_state.threads_head 遍历所有线程状态的链表。每个 PyThreadState 结构体包含一个 native_thread_id 字段,可以将其与目标线程 ID 进行比较以找到特定线程。

  4. 一旦找到有效的 PyThreadState,其地址可以在协议的后续步骤中使用,例如写入调试器控制字段和调度执行。

以下是一个定位主线程状态的示例实现:

def find_main_thread_state(
    pid: int, py_runtime_addr: int, debug_offsets: DebugOffsets,
) -> int:
    # 步骤 1:从 PyRuntime 中读取 interpreters_head
    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("在目标进程中没有找到解释器")

    # 步骤 2:从解释器中读取 threads_main 指针
    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("主线程状态不可用")

    return thread_state_addr

以下示例演示了如何通过其本地线程 ID 定位线程:

def find_thread_by_id(
    pid: int,
    interp_addr: int,
    debug_offsets: DebugOffsets,
    target_tid: int,
) -> int:
    # 从 threads_head 开始遍历链表
    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("没有找到给定ID的线程")

一旦定位到有效的线程状态,调试器可以继续修改其控制字段并调度执行,如下一节所述。

写入控制信息

一旦识别出有效的 PyThreadState 结构体,调试器可以修改其中的控制字段以调度指定 Python 脚本的执行。这些控制字段由解释器定期检查,当正确设置时,它们会在求值循环的安全点触发远程代码的执行。

每个 PyThreadState 包含一个 _PyRemoteDebuggerSupport 结构体,用于调试器和解释器之间的通信。其字段的位置由 _Py_DebugOffsets 结构体定义,包括以下内容:

  • debugger_script_path: 一个固定大小的缓冲区,用于存储 Python 源文件 (.py) 的完整路径。当触发执行时,目标进程必须能够访问并读取该文件。

  • debugger_pending_call: 一个整数型旗标。 将其设为 1 表示告知解释器已有脚本准备就绪等待执行。

  • eval_breaker: 解释器在执行过程中会检查的字段。设置该字段的第 5 位 (_PY_EVAL_PLEASE_STOP_BIT,值为 1U << 5) 将使解释器暂停并检查调试器活动。

要完成注入,调试器必须执行以下步骤:

  1. 将完整脚本路径写入 debugger_script_path 缓冲区。

  2. 将 debugger_pending_call 设置为 1。

  3. 读取 eval_breaker 的当前值,设置位 5 (_PY_EVAL_PLEASE_STOP_BIT),并将更新后的值写回。这会指示解释器检查调试器活动。

以下是一个示例实现:

def inject_script(
    pid: int,
    thread_state_addr: int,
    debug_offsets: DebugOffsets,
    script_path: str
) -> None:
    # 计算 _PyRemoteDebuggerSupport 的基准偏移量
    support_base = (
        thread_state_addr +
        debug_offsets.debugger_support.remote_debugger_support
    )

    # 步骤 1:将脚本路径写入 debugger_script_path
    script_path_ptr = (
        support_base +
        debug_offsets.debugger_support.debugger_script_path
    )
    write_string(pid, script_path_ptr, script_path)

    # 步骤 2:将 debugger_pending_call 设置为 1
    pending_ptr = (
        support_base +
        debug_offsets.debugger_support.debugger_pending_call
    )
    write_int(pid, pending_ptr, 1)

    # 步骤 3:在 eval_breaker 中设置 _PY_EVAL_PLEASE_STOP_BIT
    # (第 5 位,值为 1 << 5)
    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)

设置这些字段后,调试器可以恢复进程(如果它被挂起)。解释器将在下一个安全求值点处理请求,从磁盘加载脚本并执行它。

调试器有责任确保脚本文件在执行期间对目标进程保持存在和可访问。

备注

脚本执行是异步的。注入脚本后不能立即删除脚本文件。调试器应等待注入脚本产生可观察的效果后再删除文件。这个效果取决于脚本的设计目的。例如,调试器可能会等待远程进程连接回套接字后再删除脚本。一旦观察到此类效果,可以安全地假设文件不再需要。

总结

要在远程进程中注入并执行 Python 脚本:

  1. 在目标进程的内存中定位 PyRuntime 结构体。

  2. 读取并验证 PyRuntime 开头的 _Py_DebugOffsets 结构体。

  3. 使用该偏移量来定位一个有效的 PyThreadState。

  4. 将一个 Python 脚本的路径写入到 debugger_script_path。

  5. 将 debugger_pending_call 旗标设为 1。

  6. 设置 eval_breaker 字段中的 _PY_EVAL_PLEASE_STOP_BIT。

  7. 恢复进程(如已挂起)。脚本将在下一个安全求值点开始执行。

安全与威胁模型Security and threat model

远程调试协议所依赖的操作系统原语和原生调试器如 GDB 和 LLDB 所使用的相同。 附加到进程需要与原生调试器所需要的 相同权限,例如 Linux 上的 ptrace / Yama LSM,macOS 上的 task_for_pid,以及 Windows 上的 SeDebugPrivilege。 Python 没有引入任何新的权限提升路径;如果一个攻击者已经拥有附加到进程所需的权限,他同样能够使用 GDB 来读取内存或注入代码。

下列原则定义了在此特征中什么将被视为,以及什么将不被视为是安全漏洞:

执行附件操作需要操作系统级权限

在所有受支持的平台上,操作系统都会通过权限检查 (CAP_SYS_PTRACE、root 或管理员权限) 来管控跨进程内存访问。 一份仅在获得这些权限之后才暴露问题的报告 并不 构成 CPython 中的漏洞,因为此时操作系统安全边界已经被突破。

读取受入侵进程时发生的崩溃或内存错误不属于漏洞。

一种从目标进程读取内部解释器状态的工具,必须信任该内存处于正确状态。如果目标进程已被破坏,或受到攻击者的控制,调试器或性能分析器可能会崩溃、产生垃圾输出,或表现出不可预测的行为。这是所有基于 ptrace 的调试器所接受的风险。此类漏洞(缓冲区溢出、段错误,或由于读取损坏状态而引发的未定义行为)**不**被视为安全问题,但欢迎通过改进鲁棒性来修复这些问题。

目标进程中的漏洞不在本次范围内

如果正在被调试的 Python 进程已经遭到入侵,攻击者就已经能够控制该进程的执行。从这一起点进一步证明其影响,并不构成远程调试协议中的漏洞。

何时使用 PYTHON_DISABLE_REMOTE_DEBUG

环境变量 PYTHON_DISABLE_REMOTE_DEBUG (以及对应的 -X disable-remote-debug 标志) 允许操作员禁用协议的内联处理端,作为一项 纵深防御 措施。 在经过加固或处于沙箱化的部署环境中,由于预计不会对该进程进行调试或性能分析,且降低攻击面是优先事项,因此该功能可能非常有用;尽管操作系统级别的权限检查已经能够防止未授权访问。

设置此变量 不会 影响其他 OS 层级的调试接口 (ptrace, /proc, task_for_pid 等等),它们将根据它们各自的权限模型保持可用。