使用 pyrasite-ng 劫持 Python 实现任意代码注入

在日常开发和运维中,我们常常遇到这样的场景:一个 Python 服务已经在运行,但我们想临时在进程里执行调试代码;程序出现死循环或者内存暴涨,想不重启就直接在进程里分析;甚至有些安全研究场景,需要注入恶意代码测试目标。通常情况下,Python 进程一旦启动,就无法轻易插入新的逻辑。但 pyrasite-ng 这个工具为我们提供了可能 —— 它允许我们 在不重启进程的前提下,把任意 Python 代码注入目标进程执行。


介绍

pyrasite-ng 是一个 Python 代码注入工具,原版叫 pyrasite,由 Luke Macken 在 2011 年开源,后来由社区维护更新。 user202729 为其添加了对Python3现代化支持并发布 pyrasite-ng

它的主要功能包括:

  • 向运行中的 Python 进程注入自定义脚本;
  • 提供一套标准 payload,例如:
    • helloworld.py(输出 Hello World)
    • dump_stacks.py(打印所有线程堆栈)
    • reverse_shell.py(建立远程反弹 shell)
    • dump_memory.py(导出内存对象信息)
  • 可以通过 交互式 Shell 直接和进程对话。

适用场景:

  • 生产环境在线调试(排查问题、观察内存)
  • 安全研究(测试代码注入、反弹 shell)
  • 教学实验(理解 Python 嵌入式运行原理)

pyrasite-ng 原理详解:为什么能注入?

要理解 pyrasite-ng 的注入原理,关键在于三个问题:

  • 通信层面:注入之后如何和目标进程交互?
  • 操作系统层面:为什么可以往别的进程里“写代码”?
  • Python 解释器层面:注入的代码是如何被执行的?

1. 操作系统层面:ptrace + gdb

ptrace 权限

Linux 下调试器(如 gdbstrace)能附加到其他进程,本质依赖的是内核提供的 ptrace 系统调用

  • ptrace 允许调试器读取和修改被调试进程的寄存器和内存。
  • 当我们运行: gdb -p <pid> 就相当于告诉内核:把这个进程当成“被调试对象”,允许我往它的上下文里执行指令。

gdb 的 call 命令

gdb 除了单步调试,还可以直接调用目标进程里的函数。例如:

gdb
(gdb) call printf("Hello from gdb\n")

这会在目标进程里执行 printf,并输出结果。

只要我们知道目标进程中有某个函数存在(如 Python C API 函数),我们就能通过 gdb call 触发它!

2. Python 解释器层面:PyRun_SimpleString

Python 进程的内部结构

当我们运行 python app.py 时,实际上进程里已经加载了 Python 解释器核心(libpython):

  • 里面包含大量的 C API 函数,如 PyRun_SimpleStringPy_InitializePyImport_ImportModule 等。
  • 这些函数平时是给 C 程序嵌入 Python 时用的,但它们始终驻留在 Python 进程的内存中。

为什么能用?

因为 pyrasite 确定目标进程是 Python 解释器,它必然有 PyRun_SimpleString 符号。
于是通过 gdb,就可以执行:

gdb
call (int) PyRun_SimpleString("exec(open('payload.py').read())")

结果就是:目标进程会像执行正常脚本一样,去读取并执行我们指定的 Python 代码。

这就是注入的关键:

  • 我们没有“注入机器码”,只是让 Python 自己去执行一段字符串。
  • 相当于我们远程控制了 Python 的 exec

3. GIL 问题:为什么不会崩?

Python 有 GIL(全局解释器锁)。在多线程环境下,必须先拿到 GIL 才能安全地执行 Python 代码。

pyrasite-ng 在注入前,会先调用:

Python
PyGILState_Ensure()

确保当前线程拿到 GIL。执行完后,再:

Python
PyGILState_Release()

释放锁。

这样避免了和原本 Python 线程的竞争,保证注入代码安全运行。

4. 通信机制:反向连接

单纯执行一段代码还不够,我们希望能持续交互。
这就是 payload + socket 通信的设计。

过程:

  1. pyrasite 在本地随机开一个端口监听。
  2. 注入时,把 reverse.py 动态改写成:
Python
host = "127.0.0.1" 
port = <随机端口> 
ReversePythonConnection().start()
  1. 目标进程运行后,会主动反连控制端 socket。
  2. 这样就建立了一个“隧道”,控制端能不断发送命令,目标进程执行并返回 stdout/stderr。

一句话总结
pyrasite-ng 能注入的根本原因是:

  • 操作系统的调试机制(ptrace + gdb)允许我们调用目标进程的任意函数;
  • Python 进程内部已经有现成的执行接口(PyRun_SimpleString);
  • 我们通过 payload 把 socket 通道建立起来,从而实现持久交互。
Bash
┌─────────────────────────────┐
         操作系统层           
                             
   (1) 用户执行 pyrasite     
                            
                            
   gdb attach 到目标进程      
                            
   利用 ptrace 调用函数       
└───────┬─────────────────────┘
          call PyRun_SimpleString("exec(open(payload).read())")
        
┌─────────────────────────────┐
     Python 解释器层          
                             
   (2) PyRun_SimpleString    
       在目标进程内执行代码     
                            
                          
   载入 payload.py 脚本       
                          
   运行 ReverseConnection     
   (反向连接/执行命令类)        
└───────┬─────────────────────┘
          socket.connect(host, port)
        
┌─────────────────────────────┐
         通信层               
                             
   (3) payload 反连控制端      
                          
   控制端(PyrasiteIPC)监听端口  
                            
   双向通信建立成功             
                            
   发送命令  exec 执行        
   返回结果  stdout/stderr    
└─────────────────────────────┘

实现

下面演示一个完整的流程。

1. 安装 pyrasite-ng

Bash
pip install pyrasite-ng

需要确保本机安装了 gdb,并且允许调试其他进程(Linux 下 /proc/sys/kernel/yama/ptrace_scope 要设置为 0)。

Bash
apt install gdb
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope

2. 启动一个目标进程

新开一个终端,运行一个简单的 受控端Python 程序:

Bash
(base) root@ubuntu:~# python3
Python 3.12.9 | packaged by Anaconda, Inc. | (main, Feb  6 2025, 18:56:27) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> x = 1
>>> print(x)
1
>>>

设置x变量等于1

Bash
pgrep -a python

记下它的 PID(例如 371001)。

3. 注入 Hello World

方法1:一次性注入

编写要注入的代码,例如:

Python
x=2
print("inject success!")
print(x)

保存为injectTest.py 在另一终端执行:

Python
pyrasite 371001 injectTest.py

我们打上日志,来看看程序/gdb执行了什么:

Bash
gdb -p 371001 -batch -eval-command='call ((int (*)())PyGILState_Ensure)()' -eval-command='call ((int (*)(const char *))PyRun_SimpleString)("import sys; sys.path.insert(0, \"/root\"); sys.path.insert(0, \"/workspace/miniconda3/lib/python3.12/site-packages\"); exec(open(\"/root/hello.py\").read())")' -eval-command='call ((void (*) (int) )PyGILState_Release)($1)'
====== gdb stdout: ======
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
0x00007f31d3ddf63d in __GI___select (nfds=1, readfds=readfds@entry=0x7ffe6dbf7d88, writefds=writefds@entry=0x0, exceptfds=exceptfds@entry=0x0, timeout=timeout@entry=0x0) at ../sysdeps/unix/sysv/linux/select.c:69
$1 = 1
$2 = 0
[Inferior 1 (process 371001) detached]

====== gdb stderr: ======
69      ../sysdeps/unix/sysv/linux/select.c: No such file or directory.

======

pyrasite 实际执行的 gdb 命令

pyrasite 会拼出一串 gdb 命令,去附加目标进程:

Bash
gdb -p 371001 -batch \
  -eval-command='call ((int (*)())PyGILState_Ensure)()' \
  -eval-command='call ((int (*)(const char *))PyRun_SimpleString)("import sys; sys.path.insert(0, \"/root\"); sys.path.insert(0, \"/workspace/miniconda3/lib/python3.12/site-packages\"); exec(open(\"/root/hello.py\").read())")' \
  -eval-command='call ((void (*) (int) )PyGILState_Release)($1)'

拆解一下:

  1. -p 371001 → 附加到进程 371001。
  2. -batch → 非交互模式,执行完命令就退出。
  3. -eval-command=... 依次做三件事:
    • PyGILState_Ensure() → 获取 GIL,保证线程安全。
    • PyRun_SimpleString("...exec(open(\"hello.py\").read())") → 在目标 Python 里运行这段代码,等于执行 hello.py。
    • PyGILState_Release($1) → 释放 GIL。

gdb stdout 输出

Bash
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
0x00007f31d3ddf63d in __GI___select (...) at ../sysdeps/unix/sysv/linux/select.c:69
$1 = 1
$2 = 0
[Inferior 1 (process 371001) detached]

逐行解释:

  • [Thread debugging using libthread_db enabled]
    gdb 启用了 libthread_db 库,可以调试多线程。
  • Using host libthread_db library ...
    告诉你它用了宿主机上的线程调试库。
  • 0x00007f31d3ddf63d in __GI___select (...) at ...select.c:69
    表示 gdb 调用函数时,进程当时正阻塞在 select() 系统调用里(典型的 Python 等待 I/O 或 sleep 场景)。
    这个和注入没冲突,说明进程当时没在跑用户态 Python,而是挂在 select。
  • $1 = 1
    表示第一次 call 的返回值(即 PyGILState_Ensure() 的结果),值 1 代表获取 GIL 成功。
  • $2 = 0
    表示第二次 call 的返回值(即 PyRun_SimpleString(...) 的返回值),0 表示执行成功。
    (如果出错会返回 -1,并设置 Python 内部异常。)
  • [Inferior 1 (process 371001) detached]
    gdb 已经从目标进程分离,进程继续运行。

gdb stderr 输出

Bash
69      ../sysdeps/unix/sysv/linux/select.c: No such file or directory.

意思是:

  • gdb 想显示 select.c 的源代码第 69 行。
  • 但你的系统没有安装 glibc 的源码包,所以报 “No such file or directory”
  • 这不是错误,只是 gdb 找不到源码文件,对注入没有影响

执行结果

在刚才的受控端Python程序中,会立即打印出inject success!,然后观测到x变量已经变为2:

Python
(base) root@ubuntu:~# python3
Python 3.12.9 | packaged by Anaconda, Inc. | (main, Feb  6 2025, 18:56:27) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> x = 1
>>> print(x)
1
>>> inject success!
2

方法2:shell交互式注入

使用pyrasite-shell 输入目标的进程<PID>,设置x = 3

Bash
(base) root@ubuntu:~# pyrasite-shell  371001
Pyrasite Shell 2.0
Connected to 'python3'
Python 3.12.9 | packaged by Anaconda, Inc. | (main, Feb  6 2025, 18:56:27) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(DistantInteractiveConsole)

>>> x = 3

回到受控端Python程序,可以看到x的值已经被改成3

Bash
(base) root@ubuntu:~# python3
Python 3.12.9 | packaged by Anaconda, Inc. | (main, Feb  6 2025, 18:56:27) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> x = 1
>>> print(x)
1
>>> inject success!
2
>>> print(x)
3
>>>

防御

既然已经理解了 pyrasite-ng 注入原理(依赖 ptrace + gdb → 调用 PyRun_SimpleString → 载入 payload),那防御思路就是:

(1) 限制 ptrace

  • Linux 默认允许同一用户互相 ptrace,攻击者只要知道 PID 就能 attach。
  • 建议配置: echo 1 | sudo tee /proc/sys/kernel/yama/ptrace_scope
    • 0 → 允许任意用户附加同用户进程(风险大)
    • 1 → 只允许调试自己启动的子进程(推荐)
    • 2 → 完全禁止 ptrace(除非 root)
    • 3 → 完全禁止 ptrace(即便 root 也需要 CAP_SYS_PTRACE)

如果要长期生效,可写入 /etc/sysctl.d/10-ptrace.conf

Bash
kernel.yama.ptrace_scope = 1

(2) Python 层面防御

移除调试符号

  • pyrasite 注入依赖 gdb 能识别 Python 解释器里的符号(如 PyRun_SimpleString)。
  • 如果在部署时使用 strip 去掉符号表,注入难度会加大。

内嵌防御逻辑

  • 在应用里定期检查 ptrace/gdb attach 痕迹,例如:
    • 读取 /proc/self/status 里的 TracerPid 字段,如果不为 0,说明被调试。
    • 一旦发现被调试,可以报警或直接退出。

总结

pyrasite-ng 展示了 Python 进程注入的完整链路:

  • 利用 gdb 和 PyRun_SimpleString 注入任意代码;
  • 通过 socket IPC 与进程通信;
  • 执行 payload 实现调试、分析、甚至远程控制。

它既是 一款强大的调试工具,也是 一个潜在的安全风险

  • 如果攻击者能访问你的系统并附加到 Python 进程,就能直接劫持它。
  • 因此在生产环境中,应注意限制调试权限(ptrace、SELinux)、做好进程隔离和安全防护。

pyrasite-ng 就像一把“双刃剑”,在开发者手里是利器,在攻击者手里是武器。理解其原理,不仅能帮助我们调试 Python 应用,也能提醒我们加强安全防护。

Linux 虚拟机根目录扩容指南(使用 GParted 和 LVM) 在 Linux 中挂载新虚拟化硬盘的完整指南 基于Docker的Windows容器化实践
View Comments
There are currently no comments.