Python依赖项沙盒化:保护代码安全的创新方案

本文详细介绍如何使用secimport工具为Python依赖项创建沙盒环境,防止第三方包执行危险操作。通过实际代码示例展示如何限制网络访问和shell执行,并深入分析基于dtrace的技术实现原理。

在代码中沙盒化Python依赖项

运行来自不可信来源的代码仍然是一个未解决的问题,特别是在Python和Javascript这样的动态语言中。

我将从两个未解决的问题开始:

  • 如果你导入requests用于HTTP,为什么requests能够打开终端并切换到sudo?
  • 如果你导入logging,为什么它能够在只需要向特定目录写入文件的情况下进行网络(或像Log4Shell中的LDAP)操作?

这就是我如何为Python导入编写沙盒的故事:创建一个生产就绪的解决方案并针对不同用例进行测试。

简要说明

解决方案如下所示。GitHub链接在底部。

第三方包中pickle如何被利用?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>> import pickle
>>> class Demo:
...     def __reduce__(self):
...         return (eval, ("__import__('os').system('echo Exploited!')",))
... 
>>> pickle.dumps(Demo())
b"\x80\x04\x95F\x00\x00\x00\x00\x00\x00\x00\x8c\x08builtins\x94\x8c\x04eval\x94\x93\x94\x8c*__import__('os').system('echo Exploited!')\x94\x85\x94R\x94."
>>> pickle.loads(b"\x80\x04\x95F\x00\x00\x00\x00\x00\x00\x00\x8c\x08builtins\x94\x8c\x04eval\x94\x93\x94\x8c*__import__('os').system('echo Exploited!')\x94\x85\x94R\x94.")
Exploited!
0

使用secimport,你可以控制此类操作:

1
2
3
4
In [1]: import secimport
In [2]: pickle = secimport.secure_import("pickle")
In [3]: pickle.loads(b"\x80\x04\x95F\x00\x00\x00\x00\x00\x00\x00\x8c\x08builtins\x94\x8c\x04eval\x94\x93\x94\x8c*__import__('os').system('echo Exploited!')\x94\x85\x94R\x94.")
[1]    28027 killed     ipython

向代码添加新库可能具有挑战性的原因

  • 开发人员无法准确知道对包的期望
  • 你导入它,但包内部可以在你不知情的情况下对你的环境执行任何操作
  • 很难确定仅针对你的用例运行该库所需的最低要求
  • 应该允许哪组系统调用,使其正常运行且仅此而已?

我们信任开源社区。然而,我们使用的包的维护者是个人。当版本没有用==锁定时存在很大风险;我们的包在CI中静默更新,新代码在这些包内部运行而我们不知道。

每天都有有人匿名向PyPI上传恶意wheel包。有时这是你已经使用的包。

当前解决方案

今天的解决方案大多是带外且每个进程的。

如果给定模块包含漏洞、恶意逻辑或为非常小的任务提供大型代码库,我们必须以某种方式限制它。

主机(计算机)应保持不受你的应用程序或其第三方应用程序的影响。

那么今天我能做什么?

  1. 为整个应用程序创建安全计算(seccomp)配置文件

    • 如果你的程序以意外方式行为,无论是由于第三方库还是你自己的代码,它将被记录或进程将立即被杀死
    • 例如:RedHat、SE-Linux、SECCOMP
  2. 在CI/CD或IDE中进行静态代码分析/安全扫描

    • 你寻找过时的包或分析代码
    • 例如:Snyk、CheckMarx、Clair
  3. 在该代码库中完全不使用开源或第三方软件

    • 这在规模上不可行
  4. WASI沙盒化

    • WebAssembly很棒!但与Rust/Go/.Net不同,Python不编译为WebAssembly,因此此解决方案目前与我们无关
  5. 在VM或Hypervisor中运行软件

    • Google开发了名为gVisor的容器沙盒
    • gVisor是一种VM,可以翻译应用程序中的每个系统调用
    • Google用Go从头开始实现了Linux

限制Python进程中的模块

我不是为应用程序提供统一的配置文件,而是希望使开发人员能够在导入/编译时使用给定范围限制代码中的任何包。

就像SELinux在执行时在Linux内核中为进程授予白名单范围一样,我希望使开发人员能够在生产环境中在特定约束下控制代码中的任何包。

限制Python模块 - MVP

我想要一个可以记录每个Python调用和每个系统调用的工具。

实现此类工具可以使用以下技术:

  • eBPF
  • DTrace
  • 任何其他.*trace工具

我理解eBPF如今很常见,但我们需要跨平台的东西,为时间提供更多价值,学习曲线更快,设置简单以便实践评估。

阅读足够多的博客并尝试不同的工具后,我理解dtrace是此用例的正确起点。

DTrace堆栈概述

与eBPF不同,dtrace不需要以特定方式编译内核(并非每个Linux都内置)。dtrace在Mac和Windows上工作,使任何基于dtrace的解决方案可供更多用户使用。dtrace也是破坏性的,意味着它可以从监视python进程的dscript探针杀死进程。这正是我想要的。

查看以下图像;我们不是容器而是Python模块,我们不是SELinux而是dtrace,探测内核。

  1. 运行Python进程
  2. 在后台运行dtrace进程
  3. 运行任何你想要覆盖的内容
  4. dtrace输出

太棒了!我们可以看到posix_spawn系统调用被调用(第4行)。

在此示例中,我使用了"dtrace -n"将钩子传递给dtrace。我将此dtrace命令扩展为dscript,这是一种存储这些钩子并编程这些探针以执行我们想要的操作的方式。

经过示例后,我编写了一个dscript程序(脚本文件),当特定Python模块调用spawn系统调用时杀死进程。

我使用dscript语言中称为关联数组的东西高效地实现了它。我为脚本中想要的变量实现了一个Python包装器,并为dtrace文件内容创建了一个模板。

然后,我编写了"secimport"!

“secure import"或"secimport"的MVP版本示例

secimport是一个Python包,可用于:

  • 在生产环境中限制/约束特定的Python模块
  • 来自不可信来源的开源、第三方
  • 在用户空间/操作系统/内核级别审计Python应用程序的流程
  • 在统一配置下运行整个Python应用程序

有点像Python模块的seccomp。跨平台。

网络示例

1
2
3
4
5
6
7
8
9
>>> import requests
>>> requests.get('https://google.com')
<Response [200]>  
>>> from secimport import secure_import
>>> requests = secure_import('requests', allow_networking=False)
# 下一个调用应该杀死进程,
# 因为我们不允许requests模块进行网络连接
>>> requests.get('https://google.com')
[1]    86664 killed

Shell示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Python 3.10.0 (default, May  2 2022, 21:43:20) [Clang 13.0.0 (clang-1300.0.27.3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
# 让我们导入subprocess模块
>>> import secimport
>>> subprocess = secimport.secure_import("subprocess", allow_shells=False)
# 让我们导入os 
>>> import os
>>> os.system("ps")
  PID TTY           TIME CMD
 2022 ttys000    0:00.61 /bin/zsh -l
50092 ttys001    0:04.66 /bin/zsh -l
75860 ttys001    0:00.13 python
0
# 它按预期工作,返回退出代码0
# 现在,让我们尝试使用不同的模块"subprocess"调用相同的逻辑,该模块是使用"secure_import"导入的:
>>> subprocess.check_call('ps')
[1]    75860 killed     python
# 太酷了!

该模块的dtrace配置文件保存在:/tmp/.secimport/sandbox_subprocess.d

日志文件:/tmp/.secimport/sandbox_subprocess.log

结论

安全社区似乎需要一个能够限制代码中特定模块同时将其保持在同一进程中的沙盒。

我提出了一种处理代码库中第三方代码的方法。

源代码:https://github.com/avilum/secimport

示例:https://github.com/avilum/secimport/blob/master/docs/EXAMPLES.md

如果我使像python这样的动态语言成为可能,我相信社区将能够用几行代码实现其他语言的检测。

第二部分:使用eBPF保护PyTorch模型

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计