Python依赖项沙箱化:保护代码安全的实用方案

本文详细介绍了如何使用secimport工具对Python第三方依赖进行沙箱隔离,通过dtrace技术监控系统调用,防止恶意代码执行,包含实际代码示例和实现原理,为Python应用安全提供生产级解决方案。

在代码中沙箱化Python依赖项 | Avi Lumelsky | 信息安全技术文章

运行来自不可信来源的代码仍然是一个未解决的问题,特别是在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

一些由min-DALLE为"secimport"创作的AI艺术。GitHub链接在底部。

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

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

我们信任开源社区。然而,我们使用的包的维护者是个人。当版本未用==锁定时存在很大风险;我们的包在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模块代替容器,用dtrace代替SELinux,探测内核。

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

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

在此示例中,我使用了"dtrace -n"将钩子传递给dtrace。

我将此dtrace命令扩展为dscript,这是一种存储这些钩子并编程这些探针以执行我们想要的操作的方法。

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

我使用dscript语言中称为关联数组的东西高效地实现了它。

我为脚本中想要的变量实现了一个Python包装器,并为dtrace文件内容创建了一个模板。

然后,我编写了"secimport"!

“安全导入"或"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
19
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这样的动态语言成为可能,我相信社区将能够用几行代码为其他语言实现检测。

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

使用eBPF保护PyTorch模型

本文不是由GPT生成的。

感谢你阅读到这里。

如果你喜欢这篇文章,我欢迎你查看我先前的一些发布:

  • 我如何发现AWS上数千个开放数据库
  • 10分钟内实现Google网络钓鱼的概念验证:ɢoogletranslate.com
  • 通过客户端端口扫描识别网站用户 - 使用WebAssembly和Go
  • Facebook知道你的饮食:逐步发现Facebook收集的关于你的全部数据
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计