PyPy 的沙盒功能

警告

这描述的是旧的、未维护的版本。新版本正在 sandbox-2py3.6-sandbox-2 分支以及 sandboxlib 仓库中开发。请参阅此处对其的描述:https://mail.python.org/pipermail/pypy-dev/2019-August/015797.html

另请注意,python 3.7+ 需要 _thread 模块,这可能是逃离沙盒的考虑因素。

介绍

PyPy 提供的沙盒功能与操作系统级沙盒(例如 Linux 上的 SECCOMP)类似,但以完全可移植的方式实现。要使用它,(常规的、可信的)程序会启动一个子进程,该子进程是 PyPy 的特殊沙盒版本。该子进程可以运行任意不受信任的 Python 代码,但其所有输入/输出都被序列化到 stdin/stdout 管道,而不是直接执行。外部进程读取管道并决定哪些命令允许或不允许(沙盒),甚至以不同的方式重新解释它们(虚拟化)。潜在的攻击者可以在子进程中运行任意代码,但实际上无法执行不受外部进程控制的任何输入/输出。还设置了额外的障碍来限制使用的 RAM 和 CPU 时间。

请注意,这与 Python 语言级别的沙盒截然不同,即对攻击者允许运行的 Python 代码类型进行限制(为什么?阅读有关 pysandbox 的内容)。

另一个比较点:如果我们尝试将 CPython 插入到一个特殊的虚拟化 C 库中,我们将得到一个不仅特定于操作系统,而且不安全的結果,因为 CPython 可以被 segfault(以多种方式,所有这些方式都非常非常模糊)。如果付出足够的努力,攻击者可以将几乎任何 segfault 变成漏洞。PyPy 生成的 C 代码不可 segfault,只要我们的代码生成器是正确的 - 这就是需要信任的代码行数更少。对于偏执的人来说,使用沙盒功能翻译的 PyPy 还包含系统运行时检查(例如针对缓冲区溢出),这些检查通常只存在于调试版本中。

警告

PyPy 方面已经完成了艰苦的工作 - 你得到了一个完全安全的版本。唯一实验性和未打磨的是从常规 Python 解释器(CPython 或未沙盒化的 PyPy)使用此沙盒化 PyPy 的库。欢迎贡献。

警告

已在 PyPy2 上测试。可能无法在 PyPy3 上开箱即用。

概述

PyPy 的翻译方面之一是沙盒功能。它指的是“完全虚拟化”中的“沙盒”,但是在普通的 C 中完成的,没有任何操作系统支持。它是一个双进程模型:我们可以将 PyPy 翻译成一个特殊的“pypy-c-sandbox”可执行文件,该文件是安全的,因为它不执行任何库或系统调用 - 相反,每当它想要执行此类操作时,它会将操作名称和参数编组到其 stdout 并等待其 stdin 上的编组结果。此 pypy-c-sandbox 进程旨在由一个外部“控制器”程序运行,该程序回答这些操作请求。

pypy-c-sandbox 程序是通过在翻译过程中添加一个转换获得的,该转换将所有 RPython 级别的外部函数调用转换为执行编组/等待/解编组的存根。试图逃离沙盒的攻击者被困在一个 C 程序中,该程序除了写入 stdout 和从 stdin 读取之外,不包含任何外部函数调用。(从理论上讲,它仍然可以被攻击,例如通过利用类似 segfault 的情况,但正如引言中所解释的,我们认为 PyPy 相对安全,可以抵御此类攻击。)

外部控制器是一个简单的 Python 程序,可以在 CPython 或普通的 PyPy 中运行。它可以通过向子进程提供任何自定义的世界视图来执行它喜欢的任何虚拟化。例如,虽然子进程认为它正在使用文件句柄,但实际上这些数字是由控制器进程创建的,因此它们根本不需要(并且可能不应该)是真正的操作系统级文件句柄。在我的演示控制器中,我实现了一个简单的数字到类文件对象的映射。控制器通过将请求的路径转换为虚拟和完全自定义的目录层次结构中的某个文件或类文件对象来响应“os_open”操作。类文件对象被放入映射中,任何未使用的数字 >= 3 作为键,后者被返回给子进程。“os_read”操作通过将子进程给出的伪文件句柄映射回控制器中的类文件对象,并从类文件对象中读取来工作。

使用沙箱功能翻译 RPython 程序也会使用一个特殊的标志,该标志会启用针对越界访问的所有 C 级断言。

顺便说一句,正如你应该已经意识到的,这与我们正在翻译的 PyPy 实际上是独立的。任何 RPython 程序都可以。我已经在 JS 解释器上成功尝试过。控制器之所以被称为“pypy_interact”,是因为它模拟了一个让 pypy-c-sandbox 愉快的文件层次结构——它包含(只读)虚拟目录,如 /bin/lib/pypy1.2/lib-python 和 /bin/lib/pypy1.2/lib_pypy,并且它假装可执行文件是 /bin/pypy-c。

操作指南

获取 pypy 仓库 的副本。在 pypy/goal 目录中,运行

../../rpython/bin/rpython -O2 --sandbox targetpypystandalone.py

如果你没有安装普通的 PyPy,你应该安装,因为它翻译起来更快;但你也可以在前面加上 python 来运行相同的命令。

要运行它,请使用 pypy/sandbox 目录中的工具

./pypy_interact.py /some/path/pypy-c-sandbox [args...]

就像 pypy-c 一样,如果你不传递任何参数,你就会得到交互式提示。理论上,从这个提示中做任何坏事或读取机器上的随机文件是不可能的。要将脚本作为参数传递,你需要将它与所有依赖项一起放在一个目录中,并要求 pypy_interact 使用 --tmp=DIR 选项将此目录(只读)导出到子进程的虚拟 /tmp 目录。示例

mkdir myexported
cp script.py myexported/
./pypy_interact.py --tmp=myexported /some/path/pypy-c-sandbox /tmp/script.py

即使 script.py 来自某个随机的不可信来源,例如,它是通过 HTTP 服务器完成的,这样做也是安全的。

要限制使用的堆大小,请使用 --heapsize=N 选项传递给 pypy_interact.py。你也可以使用 --timeout=N 选项来限制 CPU 时间(实际时间)。

并非所有操作都受支持;例如,如果你键入 os.readlink('…'),控制器会因异常而崩溃,子进程会被杀死。其他操作会导致子进程直接以“致命 RPython 错误”而死亡。这些都不是安全漏洞。更重要的是,大多数其他内置模块都没有启用。在抱怨此事之前,请仔细阅读本页中的所有警告。欢迎贡献。