在日常开发和运维中,我们常常遇到这样的场景:一个 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 下调试器(如 gdb
、strace
)能附加到其他进程,本质依赖的是内核提供的 ptrace 系统调用。
- ptrace 允许调试器读取和修改被调试进程的寄存器和内存。
- 当我们运行:
gdb -p <pid>
就相当于告诉内核:把这个进程当成“被调试对象”,允许我往它的上下文里执行指令。
gdb 的 call
命令
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_SimpleString
、Py_Initialize
、PyImport_ImportModule
等。 - 这些函数平时是给 C 程序嵌入 Python 时用的,但它们始终驻留在 Python 进程的内存中。
为什么能用?
因为 pyrasite
确定目标进程是 Python 解释器,它必然有 PyRun_SimpleString
符号。
于是通过 gdb,就可以执行:
call (int) PyRun_SimpleString("exec(open('payload.py').read())")
结果就是:目标进程会像执行正常脚本一样,去读取并执行我们指定的 Python 代码。
这就是注入的关键:
- 我们没有“注入机器码”,只是让 Python 自己去执行一段字符串。
- 相当于我们远程控制了 Python 的
exec
。
3. GIL 问题:为什么不会崩?
Python 有 GIL(全局解释器锁)。在多线程环境下,必须先拿到 GIL 才能安全地执行 Python 代码。
pyrasite-ng 在注入前,会先调用:
PyGILState_Ensure()
确保当前线程拿到 GIL。执行完后,再:
PyGILState_Release()
释放锁。
这样避免了和原本 Python 线程的竞争,保证注入代码安全运行。
4. 通信机制:反向连接
单纯执行一段代码还不够,我们希望能持续交互。
这就是 payload + socket 通信的设计。
过程:
- pyrasite 在本地随机开一个端口监听。
- 注入时,把
reverse.py
动态改写成:
host = "127.0.0.1"
port = <随机端口>
ReversePythonConnection().start()
- 目标进程运行后,会主动反连控制端 socket。
- 这样就建立了一个“隧道”,控制端能不断发送命令,目标进程执行并返回 stdout/stderr。
一句话总结:pyrasite-ng
能注入的根本原因是:
- 操作系统的调试机制(ptrace + gdb)允许我们调用目标进程的任意函数;
- Python 进程内部已经有现成的执行接口(
PyRun_SimpleString
); - 我们通过 payload 把 socket 通道建立起来,从而实现持久交互。
┌─────────────────────────────┐
│ 操作系统层
│
│ (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
pip install pyrasite-ng
需要确保本机安装了 gdb
,并且允许调试其他进程(Linux 下 /proc/sys/kernel/yama/ptrace_scope
要设置为 0)。
apt install gdb
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
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
>>>
设置x变量等于1
pgrep -a python
记下它的 PID(例如 371001)。
3. 注入 Hello World
方法1:一次性注入
编写要注入的代码,例如:
x=2
print("inject success!")
print(x)
保存为injectTest.py 在另一终端执行:
pyrasite 371001 injectTest.py
我们打上日志,来看看程序/gdb执行了什么:
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 命令,去附加目标进程:
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)'
拆解一下:
-p 371001
→ 附加到进程 371001。-batch
→ 非交互模式,执行完命令就退出。-eval-command=...
依次做三件事:PyGILState_Ensure()
→ 获取 GIL,保证线程安全。PyRun_SimpleString("...exec(open(\"hello.py\").read())")
→ 在目标 Python 里运行这段代码,等于执行 hello.py。PyGILState_Release($1)
→ 释放 GIL。
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 (...) 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 输出
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:
(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
(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
(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
:
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 应用,也能提醒我们加强安全防护。