Go-Sandbox 开发实录 (2):特洛伊木马 —— 如何“黑”进 Python 进程?

在上一篇中,我们确定了“进程级沙箱”的路线。但摆在面前的第一个技术难题是:如何在 Python 进程启动的那一刻,强行插入我们的安全代码?

我们需要在用户代码执行之前,完成 Chroot 和 Seccomp 的设置。如果等到用户代码开始跑了再限制,黄花菜都凉了。

尝试一:直接修改解释器源码?

最硬核的办法是下载 CPython 源码,在 main 函数里加几行代码,重新编译一个 safe-python

但这太蠢了。

  1. 维护噩梦:每次 Python 发新版,我们都得重新打补丁、编译。
  2. 不通用:那 Node.js 怎么办?Java 怎么办?难道都要改源码?

我们需要一种非侵入式的方案。

尝试二:LD_PRELOAD?

Linux 有个黑魔法叫 LD_PRELOAD,可以在程序运行前预加载动态库。我们试着写了个 C 的 .so,劫持 __libc_start_main

但这在 Go 里实现起来很麻烦,而且容易跟 Python 自身的初始化逻辑冲突。我们希望核心逻辑还是用 Go 写,毕竟 Go 处理系统调用比 C 舒服多了。

最终方案:Python 的“内鬼” —— ctypes

后来我们灵光一闪:Python 不是自带 ctypes 吗?它能加载 C 的动态库。

如果我们把 Go 代码编译成一个 C 兼容的动态库 (.so),然后写一个极简的 Python 脚本(我们叫它 prescript.py),让这个脚本先加载 .so,执行安全锁定,然后再执行用户代码,岂不是完美?

第一步:把 Go 伪装成 C

Go 的 c-shared 构建模式简直是神器。我们只需要在 Go 函数前加上 //export,它就能被外部调用。

看看我们的 cmd/lib/python/main.go,它长得一点都不像普通的 Go 程序:

package main

import "C" // 必须引入这个,不然 CGO 不工作
import (
    "github.com/leiguorui/go-sandbox/internal/core/lib/python"
)

//export SandboxSeccomp
// 这一行注释告诉编译器:把这个函数暴露给 C!
func SandboxSeccomp(uid int, gid int, enable_network bool) {
    // 这里面是真正的 Go 逻辑:Chroot, Seccomp 等
    python.InitSeccomp(uid, gid, enable_network)
}

func main() {} // 必须有个空的 main

编译一下:

go build -o python.so -buildmode=c-shared cmd/lib/python/main.go

现在,我们有了一个 python.so,在 Python 眼里,它就是个普通的 C 库。

第二步:编写引导脚本

接下来,我们需要一个 Python 脚本来充当“内鬼”。这就是 prescript.py 的由来。

我们在开发时遇到的一个坑是:如何把用户代码传进去?
直接写文件?不安全,容易被用户读取。
最后我们决定:直接把加密后的用户代码硬编码在脚本里

看看这个脚本的雏形(为了方便理解,我简化了部分逻辑):

import ctypes
from base64 import b64decode

# 1. 召唤“特洛伊木马”
# 加载我们刚才编译的 python.so
lib = ctypes.CDLL("./python.so")

# 2. 告诉 ctypes 这个函数的参数类型
# 这一步很关键,搞错了类型会导致内存崩溃
lib.SandboxSeccomp.argtypes = [ctypes.c_uint32, ctypes.c_uint32, ctypes.c_bool]

# 3. 【高光时刻】执行锁定!
# 这行代码执行完,当前进程就被关进笼子了
# 之后再想访问 /etc/passwd?没门!
lib.SandboxSeccomp(uid, gid, enable_network)

# 4. 图穷匕见:执行用户代码
# 此时环境已经安全,我们可以放心地解密并运行用户的代码了
user_code = b64decode("{{code}}") # 这里会被 Go Server 替换成真正的代码
exec(user_code)

总结

通过 ctypes + c-shared,我们成功地在 Python 解释器内部植入了一个 Go 编写的“安全栓”。

这种方法的妙处在于:

  1. 零修改:直接使用系统自带的 Python。
  2. 跨语言:同样的思路,Node.js 可以用 ffi-napi 做,Java 可以用 JNI 做。

解决了“怎么进”的问题,下一篇我们来聊聊“进了之后做什么”:如何用 Seccomp 编织一张逃不出去的网。