目录
摘要
在 SafeDep,我们构建并运营着一个大规模的开源包监控和静态代码分析基础设施。该基础设施的目标是持续分析发布在 npm、pypi、rubygems、Go Proxy 等包注册表中的开源包,并发现恶意包。该服务为 vet 等工具提供支持,为开发者和 CI/CD 流程提供针对恶意开源包的防护。要了解更多关于此服务的信息,请参阅恶意包分析文档。
当前静态代码分析工作流程包括
- YARA Forge 规则
- 静态代码分析以识别代码中的危险能力(例如文件系统、网络、进程操作)
- 基于 LLM 的代码分析以识别恶意代码块
- OSS 项目和包元数据
- 基于 LLM 的分类,结合我们的知识(提示规则)和包特定上下文
虽然我们不断扩展这个系统,改进基于代码分析的工具并消除已知的误报,但我们相信静态分析总会存在误报和漏报。虽然静态代码分析方法的优势在于关注代码——这一真相的来源,但它受到停机问题的限制,在我们的用例中,还受到赖斯定理的限制。对我们来说,安全研究与工程问题在于做出正确的权衡,使系统在现实生活中有效运行,同时对人工程序(手动分析)的需求随时间减少。
我们开始探索构建一个补充系统的想法,该系统可以验证和关联静态分析结果。这就是动态分析的用武之地,即在受监控环境中运行开源包并根据运行时的真实行为确定其安全状态的能力。对我们来说,这个系统的设计目标是:
- 在沙箱环境中运行开源包
- 观察所有运行时行为
- 与静态分析结果关联,并根据运行时证据将其分类为恶意(或非恶意)
我们最终利用开源工具和自定义平台工具构建了一个解决方案,该方案可以
- 在沙箱中执行开源包安装
- 使用 eBPF 通过系统调用追踪观察容器的运行时行为
- 收集追踪器生成的事件,对其进行过滤,并使用关联 ID 将其存储(日志)到数据库
- 查询事件日志以识别被静态分析系统标记为恶意的包的运行时行为
- 确定自动化关联和分析的方法,以补充我们基于静态分析的检测
关键挑战
- 你如何运行一个本质上是一个库的开源包?
- 什么是可用于识别异常的基线运行时行为?
- 创建运行时行为(规则)库以对应 MITRE ATT&CK
- 在观察和收集数据的同时保证包执行的隔离和 containment
- 实现分析发布到支持生态系统(例如
npm)的每个包所需的规模 - 在规模和准确性之间进行权衡
技术分析
作为第一步,我们的目标是构建一个与现有静态代码分析基础设施并行运行的系统,并执行以下操作
- 运行开源包
- 收集运行时行为数据(事件)
- 将运行时行为数据持久化到数据库
- 研究并识别基于运行时行为数据对恶意包进行分类的系统
在这个阶段,我们感觉我们的技术需求与 Google 的(或 OpenSSF 的?)Package Analysis 项目正在解决的问题类似。我们确实从这个系统中获得了灵感,但不幸的是决定构建我们自己的,因为
- 它与 Google 的内部基础设施耦合
- 它已经结合了静态分析,我们需要能够关联我们的静态分析结果
- 避免依赖系统工具如
strace,而是直接挂钩到内核的系统调用接口 - 支持不同的执行器环境,特别是用于检测容器环境的复杂恶意负载
- 模拟系统调用,甚至机器指令(例如模拟
sleep或伪造rdtsc)
然而,我们从这个项目中的一些解决方案中获得了灵感,特别是在 dynamicanalysis 包中实现的运行开源包的功能。
在开始实现之前,我们必须解决以下问题
- 如何运行开源包
- 使用什么沙箱技术?
- 如何监控运行时行为
运行开源包
我们决定从简单的安装命令开始,即执行 npm install .. 或 pip install ..。然而,未来的工作包括尝试加载包文件并在尽力而为的基础上执行导出的函数。这里存在几个挑战:
- 在代码中查找导出的函数是非常特定于生态系统的
- 识别有效的函数参数只能尽力而为
- 执行每个导出的函数将无法扩展(对我们来说)
- 避免执行函数中的休眠和 IO 等待
- 处理错误和重试
一个无法通过简单运行 npm install ... 进行分析的恶意包示例在 恶意 NPM 包 Express Cookie Parser 中描述。
解决方案架构
我们的设计目标之一是让动态分析系统最初与现有静态分析系统并行部署。这是为了使我们能够执行必要的研究和开发,手动观察已知恶意包的运行时行为,并为非恶意的 npm、python 和其他包在安装时的行为建立基线。
为确保设计的简单性和松耦合,我们引入了
- 一个执行器,从我们的 NATS(作为另一个消费组)读取包分析消息
- 创建一个新的沙箱并在沙箱中执行包安装命令
- 沙箱是在使用 eBPF 监控的 VM 中创建的
- 监控生成的事件由单独的事件处理器处理,该处理器执行过滤,作为未来与静态分析结果关联的占位符
- 事件数据库用于存储所有捕获的事件
沙箱
沙箱的目的是限制爆炸半径,因为我们正在运行不受信任且"预期"为恶意的包及其未知负载。虽然在 VMM 受到利用的时代,只有物理隔离才能提供真正的沙箱。我们在"合理"隔离方面做出了实际选择,避免不必要的复杂性,同时保持添加额外保护层以防范复杂攻击(如容器逃逸、内核漏洞利用等)的灵活性。
威胁模型
作为沙箱设计的一部分,我们考虑了以下威胁,因为我们正在基础设施中执行不受信任和恶意的代码
- 执行恶意代码生成反弹 shell 以在沙箱中获得交互式访问
- 利用漏洞进行权限提升和容器逃逸
- 利用操作系统内核中的漏洞
- 利用本地可访问网络中的网络服务
- 利用虚拟机监控程序(VMM)中的漏洞以突破虚拟化
实现
我们决定采用基于 Docker 容器的沙箱配合 DIND。选择主要基于
- 我们的平台运行在 Kubernetes 上,但我们希望将其视为另一个工作负载,而不是低级服务(例如 daemonset)
- 将 DIND 作为 Executor 的 sidecar,并可随 Executor 水平扩展
- 保持将"完整堆栈"沙箱(Executor + DIND)部署在独立 VPC 中的选项开放
- 通过本地 unix 套接字限制通信,以便能够使用 NSP 完全限制 DIND pods
- 利用熟悉的 Docker 客户端 API 来创建容器、执行命令和销毁容器
虽然沙箱实现选择主要是为了工程简单性和可扩展性,但我们确实将威胁作为基础设施的一部分加以考虑。
| 威胁 | 缓解措施 |
|---|---|
| 执行恶意代码生成反弹 shell 以在沙箱中获得交互式访问 | 无缓解措施。我们希望这种情况发生。容器是临时的,有严格的执行期限以避免长期持久化。专用节点池和有计划的 VPC 隔离保护其余基础设施 |
| 利用漏洞进行权限提升和容器逃逸 | DIND 使用 Linux 内核命名空间提供额外的隔离层。需要两次容器逃逸才能从沙箱突破到底层 Kubernetes 节点。我们计划在未来的增强中与 VPC 隔离一起实现 gVisor |
| 利用操作系统内核中的漏洞 | 使用带有 taints 和 tolerations 的专用 Kubernetes 节点池,以保证只有 executor pods 在节点池中运行。没有密钥挂载到这些 pods 中。利用 Kubelet 令牌进行集群内横向移动将是一个值得观察的有趣攻击 |
| 利用本地可访问网络中的网络服务 | Kubernetes Network Policy 阻止对 Pod 的本地网络 CIDR 的出口。Google Cloud 防火墙规则在 VM 级别仅允许访问 Kubernetes API Server 和容器注册表(只读),Kubelet 需要这些来在节点中拉取镜像 |
| 利用虚拟机监控程序(VMM)中的漏洞以突破虚拟化 | 无缓解措施。我们的整个基础设施将因 VMM 逃逸漏洞而被攻陷。详细低级信息可参见论文 Large Scale Cluster Management with Borg |
运行时监控
运行时监控解决方案的目的是在系统调用级别观察包执行,使用标准模式生成事件,使用规则对"有趣"事件进行分类,并将事件存储在事件日志中以进行手动或自动分析。运行时监控解决方案与系统其余部分完全解耦,单独负责执行系统监控。然而,我们确实考虑了以下目标
- 必须支持 Linux 内核和容器监控(平台要求)
- 在 OS 级别进行监控以减少环境足迹(例如避免依赖
strace) - 为了性能和隐蔽性而权衡应用上下文
我们决定使用 Falco 作为我们的可观测性技术。选择的依据是
- Falco
- 构建我们自己的基于 eBPF 的解决方案
对我们来说,选择 Falco 是相当明显的,因为我们正在推出一个专注于研发的重点实验服务。在达到当前可用选项的局限性之前,投资构建和生产化基于 eBPF 的解决方案是没有意义的。在任何情况下,Falco 是一个 CNCF 毕业项目,表明该解决方案的成熟度,并满足我们当前的所有要求,特别是编写自定义规则以匹配各种系统调用参数的能力。
- Falco 和事件处理器部署为 Kubernetes DaemonSet
- Falco 和事件处理器共享
emptyDir卷以实现 Falco gRPC 套接字 - 事件处理器是一个 gRPC 客户端,从 unix 套接字读取 Falco 事件
- 事件处理器通过将事件持久化到数据库来集成系统与平台其余部分
- 未来的迭代将使事件处理器将事件流式传输到 NATS 以进行分析和关联
观察结果
让我们从快速统计数据和挑战开始
- 在生产环境中一小时内有 100,000+ 个事件持久化到数据库
- 在撰写本文时,数据库中有 12,491,123 个事件,生产时间不到两周
- Falco 不允许基于
ip != X进行过滤,因为X在规则解析时解析 - 每个包管理器都有自己的初始化足迹,但跨版本的行为差异使得基线化变得棘手
性能
*n2d-standard-4*用作动态分析的节点类型,每个节点由 4 个 CPU 和 16 GB RAM 组成- Falco 和事件处理器容器部署时 CPU 和内存限制均为
100m(1 个 CPU 的 10%)和 256Mb - Executor(NATS 监听器)部署时 CPU 和内存限制为
100m和 256Mb - DIND 容器部署时 CPU 和内存限制为
500m和 512Mb
Executor,包含 NATS 监听器和 DIND 容器,CPU 使用率较低,表明资源可能过度配置。
另一方面,Falco 和事件处理器 pods(DaemonSet)显示 CPU 峰值。这几乎可以肯定是由于 Falco 容器因规则匹配系统调用而产生较高的 CPU 负载。
Executor(NATS 监听器和 DIND)内存使用相当稳定,偶尔有峰值。此时我们很想知道是哪些包导致了这些峰值。我们猜测它们是由安装期间需要平台特定编译的包引起的。
然而,Falco 容器显示出内存泄漏的迹象。这可能与一个已知问题有关。我们基于 Go 的事件处理器也可能由于高吞吐量事件处理和 Go GC 的工作方式而加剧。需要进行更深入的分析以确定这种内存使用模式的根本原因。
下一步是什么?
我们为运行时分析构建和部署的技术堆栈针对规模进行了调整。根据最初的设计目标,我们已准备好必要的基础设施来开始将运行时行为与我们的静态分析结果进行关联。我们现在可以独立地对 OSS 包安装时行为进行大规模研究。然而,我们相信,对用户和大型社区的核心价值在于
- 消除噪音
- 通过其运行时行为识别真正的恶意包
- 与静态分析基础设施关联并减少误报
在对我们观察到的运行时行为进行分类方面存在多个挑战,例如
- 命令执行在安装阶段并不罕见,例如
esbuild、node-gyp、make和其他构建工具,Python、Node 或其他生态系统包需要这些工具来构建平台特定的二进制文件 - 包管理器有自己的初始化足迹,由于跨版本的差异,使得基线化变得棘手
- 构建遥测在不使用深度数据包检测的情况下大规模运行时,与恶意 C2 行为无法区分
对我们来说,下一步是识别启发式方法,并创建一种方法来基线化生态系统特定的包安装行为以识别异常。未来的工作还包括
- 识别在安装时不触发的复杂恶意包,例如 https://safedep.io/malicious-npm-package-express-cookie-parser/
- 提高代码执行覆盖率
- 最小化环境足迹以避免恶意代码检测分析环境
- 改进自定义规则以更好地检测恶意负载
参考
-
dynamic-analysis
-
oss
-
malware
-
security
SafeDep 博客最新动态
关注以获取开源安全与工程方面的最新更新和见解