Python依赖沙箱化:保护代码免受第三方包威胁

本文详细介绍了如何使用secimport工具对Python第三方依赖进行沙箱化隔离,通过DTrace监控系统调用,防止恶意代码执行,包括网络访问、shell操作等限制,并提供实际代码示例和实现原理。

代码中的Python依赖沙箱化

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

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

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

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

TL;DR

解决方案如下所示。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创作的一些AI艺术,代表"secimport"。GitHub链接在底部。

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

  • 开发人员无法确切知道包需要什么
  • 你导入它,但包内部可以在你不知道的情况下对你的环境做任何想做的事情
  • 很难确定运行该库所需的最低要求,仅针对你的用例
  • 应该允许哪些系统调用集合,使其正常运行,但不多于所需?

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

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

某天早上,一个拥有全球使用pip包的人醒来后讨厌俄罗斯。为了抗议,他在安装他的python包时通过检查IP并在导入时运行os.system(或类似操作)来删除俄罗斯和白俄罗斯安装者的硬盘。历史会重演,所以这只是时间问题…这太容易了。

值得一提的是,在添加新的开源代码时,你无法审查全部代码(但Google和Apple可以)。

当前的解决方案大多是带外和每个进程的

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

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

那么我现在能做什么?

  1. 为整个应用程序创建sec-comp(Linux安全计算)配置文件。如果你的程序以意外方式行为,无论是由于第三方库还是你自己的代码,它将被记录或进程将立即被杀死。 例如:RedHad、SE-Linux、SECCOMP等优秀工具。

  2. 在你的CI/CD或IDE中进行静态代码分析/安全扫描器。 你查找过时的包或分析代码。 例如:Snyk、CheckMarx、Clair。

  3. 根本不在该代码库中使用开源或第三方软件。 这在规模上不可行…但我知道一些小型初创公司根本不在代码中使用开源。这很硬核——想象一下从头开始实现任何逻辑。

  4. WASI沙箱化——WebAssembly很棒!但是,与Rust/Go/.Net不同,Python不编译为WebAssembly,所以这个解决方案目前与我们无关(有人用rust-python吗?)。

  5. 在VM或Hypervisor中运行软件(与容器不同)。 Google开发了一个用于容器的沙箱,称为gVisor。 gVisor是一种VM,可以转换应用程序中的每个系统调用。 对于这个项目,Google用Go从头开始实现了Linux。 沙箱二进制文件(运行容器)大约为~17MB。 每个系统调用都被转换为gVisor,然后转换为Host,同时强制执行高级策略。相当令人印象深刻,对吧?

然而,在VM内运行应用程序通常会导致性能下降。我个人经历过(优秀的)沙箱使应用程序速度降低高达50%。

Google使用gVisor在Google App Engine中隔离容器。

就像Google一样,我认为开源包将始终是你应用程序的攻击面。但这并不意味着你应该改变编码方式。每个模块都应该有自己的限制,由开发人员定义或从某些模板中选择。

如何限制我们python进程中的模块

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

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

Log4Shell有人记得吗?为什么Log4J默认能够打开LDAP连接?

限制Python模块——MVP

我想要一个可以记录每个python调用和每个系统调用的工具。 我假设我们和内核之间没有恶意的东西可以影响可见性。听起来很难,保持相同的性能听起来更难。

追踪/沙箱化到这个级别通常会影响运行时性能。

实现这样的工具可以使用以下技术:

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

我知道eBPF现在很常见,但我们需要跨平台的东西,能够以更快的学习曲线和易于设置的实践评估为时间提供更多价值。

我们不能假设每个python开发人员都会知道eBPF的C语言。我假设在MVP之后可以轻松地用eBPF替换(eBPF也被称为"DTrace 2.0")。

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模块调用spawn系统调用时杀死python进程。

我使用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
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生成的

infosecwriteups.com

感谢你阅读到这里。

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

  • 我如何发现AWS上的数千个开放数据库

  • 关于发现和报告包含财富500强公司、医院、加密平台敏感数据的数据库的旅程

  • 10分钟内实现Google网络钓鱼的POC:ɢoogletranslate.com

  • 早在2016年,我遇到了一篇关于有人购买ɢoogle.com的帖子。它被用于网络钓鱼目的(注意第一个字符…)

  • 通过客户端端口扫描识别网站用户——使用WebAssembly和Go

  • 网站倾向于从浏览器扫描用户的开放端口,以更好地识别新/返回用户。能否滥用’localhost’…

  • Facebook知道你吃什么:逐步发现Facebook收集的关于你的所有数据。

  • 关于我如何以编程方式探索https://facebook.com/dyi的故事。

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