目录导航
背景
内存和存储资源有限的嵌入式设备可能会利用BusyBox等工具,该工具被称为嵌入式 Linux 的瑞士军刀。BusyBox 是一个包含许多有用的 Unix 实用程序(称为小程序)的软件套件,这些实用程序打包为单个可执行文件。在BusyBox的,你可以找到一个完整的shell,DHCP客户端/服务器,以及小工具,如cp
,ls
,grep
,等。您可能会发现许多运行 BusyBox 的 OT 和 IoT 设备,包括流行的可编程逻辑控制器 (PLC)、人机界面 (HMI) 和远程终端单元 (RTU)——其中许多现在在 Linux 上运行。
作为我们致力于提高开源软件安全性的一部分, Claroty 的 Team82 和 JFrog 合作开展了一个检查 BusyBox 的漏洞研究项目。使用静态和动态技术,Claroty 的 Team82 和 JFrog 发现了影响最新版本 BusyBox 的 14 个漏洞。BusyBox 在8 月 19 日发布的1.34.0 版本中私下披露并修复了所有漏洞。
在大多数情况下,这些问题的预期影响是拒绝服务 (DoS)。但是,在极少数情况下,这些问题也可能导致信息泄漏和可能的远程代码执行。
在这份报告中,我们提供了我们发现的漏洞的详细信息,详细说明了受影响的人,讨论了我们的研究方法,深入解释了其中一个漏洞,并针对这些问题提出了修复和解决方法。
除了披露漏洞之外,Team82 还开源了我们的自定义 AFL 模糊测试工具,这些工具负责触发许多提到的漏洞。希望这将有助于其他研究人员发现和披露更多问题。
漏洞
CVE ID | 描述 | 受影响的小程序 | 受影响的版本(含) | 影响 | CVSS v3.1 |
---|---|---|---|---|---|
CVE-2021-42373 | 当提供了节名称但没有给出页面参数时,空指针取消引用会导致拒绝服务man | man | 1.33.0-1.33.1 | 拒绝服务 | 5.1 |
CVE-2021-42374 | 当精心制作的 LZMA 压缩输入被解压缩时,越界堆读入会导致信息泄漏和拒绝服务。这可以由任何内部支持 LZMA 压缩的小程序/格式触发。unlzma | lzma /及更多(见下文)unlzma | 1.27.0 – 1.33.1 | DoS 和信息泄露 | 6.5 |
CVE-2021-42375 | 由于 shell 将特定字符误认为保留字符,因此在处理精心制作的 shell 命令时,对特殊元素的错误处理会导致拒绝服务。这可用于在过滤命令输入的罕见情况下的 DoS。ash | ash | 1.33.1 | 拒绝服务 | 4.1 |
CVE-2021-42376 | 由于在\x03分隔符之后缺少验证,在处理精心制作的 shell 命令时,空指针取消引用会导致拒绝服务。这可用于在非常罕见的过滤命令输入条件下的 DoS。hush | hush | 1.16-1.31.1 | 拒绝服务 | 4.1 |
CVE-2021-42377 | 由于 shell 对 &&& 字符串处理不当,因此在处理精心设计的 shell 命令时,攻击者控制的指针 free in会导致拒绝服务和可能的代码执行。这可用于在过滤命令输入的罕见条件下进行远程代码执行。hush | hush | 1.33.0-1.33.1 | DoS 和可能的 RCE | 6.4 |
CVE-2021-42378 | 在函数中处理精心设计的模式时,awk 中的use-after-free会导致拒绝服务和可能的代码执行awk getvar_i | awk | 1.16-1.33.1 | DoS 和可能的 RCE | 6.6 |
CVE-2021-42379 | 在函数中处理精心设计的 awk 模式时,释放后使用会导致拒绝服务和可能的代码执行awk next_input_file | awk | 1.18-1.33.1 | DoS 和可能的 RCE | 6.6 |
CVE-2021-42380 | 在函数中处理精心设计的 awk 模式时,释放后使用会导致拒绝服务和可能的代码执行awk clrvar | awk | 1.28-1.33.1 | DoS 和可能的 RCE | 6.6 |
CVE-2021-42381 | 在函数中处理精心设计的 awk 模式时,释放后使用会导致拒绝服务和可能的代码执行awk hash_init | awk | 1.21-1.33.1 | DoS 和可能的 RCE | 6.6 |
CVE-2021-42382 | 在函数中处理精心设计的 awk 模式时,释放后使用会导致拒绝服务和可能的代码执行awk getvar_s | awk | 1.26-1.33.1 | DoS 和可能的 RCE | 6.6 |
CVE-2021-42383 | 在函数中处理精心设计的 awk 模式时,释放后使用会导致拒绝服务和可能的代码执行awk evaluate | awk | 1.33.1 | DoS 和可能的 RCE | 6.6 |
CVE-2021-42384 | 在函数中处理精心设计的 awk 模式时,释放后使用会导致拒绝服务和可能的代码执行awk handle_special | awk | 1.18-1.33.1 | DoS 和可能的 RCE | 6.6 |
CVE-2021-42385 | 在函数中处理精心设计的 awk 模式时,释放后使用会导致拒绝服务和可能的代码执行awk evaluate | awk | 1.16-1.33.1 | DoS 和可能的 RCE | 6.6 |
CVE-2021-42386 | 在函数中处理精心设计的 awk 模式时,释放后使用会导致拒绝服务和可能的代码执行awk nvalloc | awk | 1.16-1.33.1 | DoS 和可能的 RCE | 6.6 |
触发漏洞的条件
由于受影响的小程序不是守护程序,因此只有当易受攻击的小程序被提供不受信任的数据(通常通过命令行参数)时,每个漏洞才能被利用。具体来说,这些是触发每个漏洞必须发生的条件:
CVE-2021-42373 – 如果攻击者可以控制传递给man
.
man
由默认的 BusyBox 配置构建,但未随 Ubuntu 的默认 BusyBox 二进制文件一起提供。
CVE-2021-42374 – 如果攻击者可以提供精心制作的压缩文件,将使用unlzma
.
请注意,即使unlzma小程序不可用,但CONFIG_FEATURE_SEAMLESS_LZMA
(默认启用)启用,其他applet如tar
,unzip
,rpm
,dpkg,lzma
以及man
处理与文件时也可以达到漏洞代码.lzma
的文件名后缀。
unlzma
由默认的 BusyBox 配置构建,并随 Ubuntu 的默认 BusyBox 二进制文件一起提供。
CVE-2021-42375 – 如果攻击者可以提供ash
包含特殊字符 $,{,},#的命令行,则适用。
ash
由默认的 BusyBox 配置构建,并随 Ubuntu 的默认 BusyBox 二进制文件一起提供。
CVE-2021-42376 – 如果攻击者可以提供hush
包含特殊字符 \x03(分隔符)的命令行,则适用。
hush
由默认的 BusyBox 配置构建,但未随 Ubuntu 的默认 BusyBox 二进制文件一起提供。
CVE-2021-42377 – 如果攻击者可以提供hush
包含特殊字符 &的命令行,则适用。
CVE-2021-42378 – CVE-2021-42386 – 如果攻击者可以提供任意模式awk
(该模式是此小程序采用的第一个位置参数),则适用。
awkƒ
由默认的 BusyBox 配置构建,并随 Ubuntu 的默认 BusyBox 二进制文件一起提供。
研究方法论
为了研究 BusyBox,我们使用了静态和动态分析方法。
首先,以自上而下的方式对 BusyBox 源代码进行人工审查(根据用户输入直到特定的小程序处理)。我们还寻找明显的逻辑/内存损坏漏洞。
下一个方法是模糊测试。我们用ASan编译了 BusyBox,并为每个 BusyBox 小程序实现了一个AFL工具。随后通过删除不必要的代码部分、在同一进程(持久模式)上运行多个模糊测试循环以及并行运行多个模糊测试实例来优化每个线束。
我们从模糊测试所有守护程序小程序开始,包括 HTTP、Telnet、DNS、DHCP、NTP 等。为了有效地模糊基于网络的输入,需要进行许多代码更改。例如,我们执行的主要修改是用来自 STDIN 的输入替换所有 recv 函数,以支持模糊输入。当我们对非服务器小程序进行模糊测试时,也进行了类似的更改。
我们为每个小程序准备了几个示例,并在几天内运行了数百个经过模糊测试的 BusyBox 实例。这给了我们数以万计的崩溃来评估。我们必须创建具有相同根本原因的崩溃类别,以帮助减少样本集中的崩溃数量。后来,我们最小化了每个组代表,以便处理一小部分独特的崩溃输入。
为了完成这些任务,我们开发了自动工具来消化所有崩溃数据并根据崩溃分析报告进行分类,主要包括相关代码区域的崩溃堆栈跟踪、寄存器和汇编代码。例如,我们合并了具有类似崩溃堆栈跟踪的案例,因为它们通常具有相同的问题根本原因。
最后,我们研究了每个独特的崩溃并最小化其输入向量以了解根本原因,这使我们能够创建一个利用导致崩溃的漏洞的概念验证 (PoC)。此外,我们针对多个 BusyBox 版本测试了我们的 PoC,以了解何时将错误引入源代码。
总之,以下是我们在研究中采取的步骤:
- 代码审查
- 模糊测试
- 减少和最小化
- 分流
- 概念验证
- 测试多个版本
- 披露
BusyBox 模糊测试指南和资源
作为我们对开源安全和安全社区的承诺的一部分,我们创建了一个简单的入门指南,详细介绍了如何对 BusyBox 进行模糊测试。该指南与我们编写的所有模糊测试工具一起发布,作为我们模糊测试工作的一部分。我们希望社区能够进一步改进这些模糊测试工具,以便发现和修复 BusyBox 中的更多错误。
所有材料都可以在Claroty 的 GitHub 页面上找到。
威胁分析
为了评估这些漏洞造成的威胁级别,我们检查了 JFrog 的数据库,其中包含 10,000 多个嵌入式固件映像(仅由公开可用的固件映像组成,而不是上传到 JFrog Artifactory 的固件映像)。我们发现其中 40% 包含一个 BusyBox 可执行文件,该文件与其中一个受影响的小程序相关联,这使得这些问题在基于 Linux 的嵌入式固件中极为普遍。
但是,我们认为这些问题目前不会构成严重的安全威胁,因为:
- DoS 漏洞很容易被利用,但由于小程序几乎总是作为单独的分叉进程运行,因此通常可以减轻其影响。
- 信息泄漏漏洞很容易被利用(参见下一节)。
- 释放后使用漏洞可能可用于远程代码执行,但目前我们没有尝试为它们创建武器化漏洞。此外,
awk
从外部输入处理模式非常罕见(并且本质上是不安全的)。
深入了解 CVE-2021-42374 – LZMA OOB 阅读
Lempel-Ziv-Markov 链算法 (LZMA) 和范围编码
LZMA是一种使用字典压缩的压缩算法,并使用范围编码器对其输出进行编码。字典压缩器使用复杂的字典数据结构查找匹配项,并生成一系列文字符号和短语引用,范围编码器一次一位地对其进行编码,使用复杂的模型对每一位进行概率预测。
压缩算法使用自适应二进制范围编码器将压缩流编码为比特流。数据被分成多个数据包,其中每个数据包描述一个字节或一个 LZ77 序列,其长度和距离被隐式或显式编码。
.lzma 格式包含一个 13 字节的标头,后跟 LZMA 压缩数据。这是一个使用以下方法压缩字符串“abc”的小例子:

为了输出解压缩的流,LZMA 实现使用内存缓冲区,该缓冲区以用户提供的字典大小(LZMA 标头的一部分)的大小进行初始化。一旦该缓冲区被填满,它会自动输出迄今为止的数据,刷新缓冲区并再次开始填满它。
漏洞
该漏洞是由于unpack_lzma_stream
状态 >= 时函数(在 decompress_unlzma.c 中)中的大小检查不足引起的LZMA_NUM_LIT_STATES
:
while (global_pos + buffer_pos < header.dst_size) {
...
uint32_t pos;
pos = buffer_pos - rep0;
if ((int32_t)pos < 0) // Insufficient check
pos += header.dict_size; // dict_size is user-controlled
match_byte = buffer[pos]; // Read OOB may occur here
do {
int bit;
match_byte <<= 1;
bit = match_byte & 0x100;
...
为了触发漏洞并控制泄漏数据的起始偏移量,我们需要确保满足以下条件:
buffer_pos = 0
and
rep0 = offset + dict_size
这样,pos
将等于-(offset + dict_size)
。添加dict_size
, pos
will be之后, -offset
我们可以通过 match_byte 从我们想要的偏移量泄漏内存。泄漏的内存很可能包含可以进一步帮助攻击者进行利用活动的指针(例如,通过促进 ASLR 绕过)。
导致越界访问
利用此漏洞的大体思路是准备一个特制的 LZMA 编码流,这样在解码时,两个条件都会被填充并且pos
等于负数-offset
。最终,解压后的流将包含泄漏的内存,这些内存将被写入输出流。
为了满足第一个条件buffer_pos = 0
,我们需要确保state >= LZMA_NUM_LIT_STATES
在刷新当前解压缩的缓冲区流之后立即到达我们的代码流 ( ),因此缓冲区指针位置将为 0。我们可以通过到达当前匹配的最后一次迭代来实现这一点。 :
buffer[buffer_pos++] = previous_byte;
if (buffer_pos == header.dict_size) {
buffer_pos = 0;
global_pos += header.dict_size;
if (transformer_write(xstate, buffer, header.dict_size) != (ssize_t)header.dict_size)
goto bad;
IF_DESKTOP(total_written += header.dict_size;)
}
len--;
} while (len != 0 && buffer_pos < header.dst_size); // match_last_iteration will end with buffer_pos = 0;
第二个条件更难满足,需要深入了解 LZMA 算法的工作原理。一般的想法是在 LZMA 比特流中编码一个特殊的长度,以便在解码时它会被rep0
变量使用。
总而言之,为了达到 OOB 条件,我们需要写入一些字节,然后使用匹配来填充缓冲区以header.dict_size and
更改rep0
为我们想要的值。因此, pos
将是相等的-offset
,我们可以从偏移量泄漏字节作为对缓冲区指针的引用。
从越界内存泄漏位
阅读完之后match_byte
,我们将进入这个流程:
do {
int bit;
match_byte <<= 1;
bit = match_byte & 0x100;
bit ^= (rc_get_bit(rc, prob + 0x100 + bit + mi, &mi) << 8); /* 0x100 or 0 */
if (bit)
break;
} while (mi < 0x100);
while (mi < 0x100) {
rc_get_bit(rc, prob + mi, &mi);
}
只要位匹配我们的 match_byte(泄漏的字节),它就会在循环中从 读取概率,但是一旦有一位不匹配,它就会从头 读取。我们可以检测第一个不匹配的位是什么,通过检查是否通过写入更多文字字节而改变,或者概率是否改变并且我们得到不同的输出。最后,泄漏的位将被刷新到解压缩的缓冲区中。prob + 0x100 + bit + miprob + miprob + mi
武器化 ZIP 文件
尽管该漏洞是在 LZMA 解压算法中发现的,但我们发现许多小程序支持 LZMA 压缩,并且默认情况下会尝试解压编码的 LZMA 流(有关管理此行为的配置标志,请参阅“修复和解决方法”部分)。例如,无处不在的 ZIP 格式支持 LZMA 压缩作为“类型 14”压缩。
从攻击者的角度来看,ZIP 是一个更好的攻击媒介,因为:
unzip
调用比直接调用unlzma
.- 使用此攻击向量,对要解压缩的文件名没有限制(与需要
.lzma
后缀的 tar 攻击向量不同)。 - 泄露的数据可以被提取出来并保存到文件中,以后可以远程读取。例如,这可能发生在允许上传带有媒体资源的 zip 文件的嵌入式 Web 服务中,这些文件将被提取到可访问的位置。从那里,攻击者可以读取泄漏的内存数据。
为了测试这一点,我们构建了一个小的 PoC 脚本,该脚本生成一个武器化的 ZIP,其中一个文件使用 LZMA 进行压缩:

修复和解决方法
BusyBox 1.34.0(直接下载链接)中已修复所有14个漏洞,并敦促用户升级。
如果无法升级 BusyBox(由于特定版本兼容性需要),则可以在没有易受攻击的功能(小程序)的情况下编译 BusyBox 1.33.1 和更早版本作为变通方法。
make defconfig
在 BusyBox 的源目录中运行后(或者如果重用以前的配置),编辑.config
文件如下:
man
– 注释掉CONFIG_MAN=y
lzma
– 注释掉CONFIG_UNLZMA=y
,CONFIG_FEATURE_SEAMLESS_LZMA=y
和CONFIG_FEATURE_UNZIP_LZMA=y
ash
– 注释掉CONFIG_ASH=y
hush
– 注释掉CONFIG_HUSH=y
awk
– 注释掉CONFIG_AWK=y
致谢
我们要感谢 BusyBox 开发团队的 Denys Vlasenko 以快速的方式验证和修复了1.34.0 版的所有上述问题。