完成静音:将漏洞串连起来,在 Outlook 上实施 RCE 攻击(第 2 部分)
简介
利用我们在 本系列博文第 1 部分中介绍的漏洞,我们得以在攻击目标上再次播放自定义声音文件,成功滥用 Outlook 的提醒声音功能。为了发挥这一能力的作用,并将其转换为完整的远程代码执行 (RCE) 攻击,我们开始在 Windows 上的声音文件解析中搜寻漏洞。
攻击面
将要通过 Outlook 播放的声音文件为 波形音频文件格式 (WAV)。它通过 PlaySound 功能播放,该功能可以接收声音文件路径。 PlaySound 将会加载文件、对其进行解析,然后调用 soundOpen 。此命令将调用不同的 wave 函数,例如 waveOutOpen。
WAV 文件可以用作多个音频编解码器的容器(也称为包装器)。编解码器是一种程序或代码,用于对数据流(例如图像、视频或音频)进行编码或解码。通常,编解码器将经过 脉冲编码调制 (PCM) 编码,这是一种表示采样模拟信号的简单方法。
我们可以在三个主要攻击面中搜寻漏洞:
WAV 格式解析
音频压缩管理器
不同的音频编解码器
WAV 格式解析
WAV 格式解析在 soundInitWavHdr 函数内完成,生成的文件为 winmm.dll(实施 Windows 多媒体 API 的库)。该处呈现的攻击面并不大,并且似乎已经过审查,我们并未找到任何漏洞。
什么是音频压缩管理器?
在某些情况下,WAV 文件中使用的编解码器并未使用简单的 PCM 编码,因此需要通过自定义解码器进行解码,而音频压缩管理器 (ACM) 就是负责处理此类情况的代码。这些解码器在扩展名为 .acm 的文件中执行。MP3 编解码器就是一个很常见的例子,它在 l3codeca.acm 中执行。每个编解码器都由一个驱动程序(它不同于内核模式驱动程序,但功能相似)进行处理,而该驱动程序则通过 ACM 进行寄存。
每次需要转换时,例如将 MP3 转换为 PCM(或反向转换),ACM 都会介入并管理此类转换。当我们使用 WAV 文件,但又无法使用 PCM 时,将会查询 ACM,确定文件本身所指定的编解码器是否存在且能够处理文件转换(图 1)。
每个 ACM 驱动程序都必须执行 acmdStreamSize 和 acmdStreamOpen等函数。第一个函数可以返回转换输出所需的文件字节数;第二个函数则会创建数据流结构体并设置合适的域,例如解码回调函数。
ACM 的攻击面也没有那么大。但是,我们仍然在该代码中找到了一个漏洞,如本文稍后所示。
不同的音频编解码器
在最后这个攻击面中,默认安装了一些不同的音频编解码器。编解码器使用两种不同的方式在 WAV 文件中指定:
FORMAT 块中的 wFormatTag
如果 wFormatTag 是 WAVE_FORMAT_EXTENSIBLE,则 SubFormat 将保留该音频编解码器的 GUID。
有关可用编解码器的列表,请参见 附录。
音频信号处理基础知识
在深入介绍代码之前,我们先来熟悉一下音频信号处理的一些基础知识。如果您对这些概念已经比较熟悉,则可以随时 跳到下一节。
当我们听到声音时,实际上听到的是通过传输介质传播的振动。声音就是接收到的振动波,由我们的耳朵接收,并被大脑所感知。
音频信号就是一个连续不断的波形。在进行数字化处理时,我们需要将其从模拟信号转换为数字信号(例如,使用 ADC 转换器)。这种转换就是对模拟信号的离散化。为完成这一转换,我们在均匀间隔的数据点(称为 样本)中对模拟信号进行了多次采样。该 采样率 (也称为采样频率)决定了每秒提取多少个样本。采样率越高,捕获的细节就越多,但也需要进行更多的存储和处理。
我们使用的并不是采样率,而是 样本大小,也就是每个样本有多少个数据位。同样,样本大小越大,其质量就越高(也就是越接近原始声音)。每个样本的大小通常为 16 位或 24 位。
音频编解码器基于心理声学模型。 心理声学 是一门对声觉和听觉进行研究的科学,它研究的是人类听觉系统如何感知各种声音。例如,人类能听到的声音频率范围是 20 Hz 到 20,000 Hz。因此,为了进一步缩减文件大小,音频编解码器可能会去除那些人类无法听到的 频率 。此外,如果信号的 音量 太小,人类耳朵无法听到,音频编解码器也会将其去除。以 20 Hz 的声音为例,如果其音量小于 60 分贝,则无法被人类听到。
心理声学模型的示例还有很多,例如:如果寂静信号的频率或时间接近某个响亮信号,则会将其屏蔽。
在执行信号的分析、解释和修改时,使用的是滤波器组,它们会将信号分为多个分波段。通过这些不同的分量波段,可以对信号的特定部分进行更为细致的检查和处理。较为常用的滤波器组是 DCT 和多相滤波器组。
掌握了这些基础知识之后,我们就可以深入了解不同的编解码器了。
第一次尝试:对样本缓冲区的越界写入
我们首先尝试的是 MP3,因为与其他编解码器相比,MP3 要复杂得多。其他大多数编解码器都只能执行比较简单的转换,而 MP3 则将解码过程分成了多个步骤(图 2)。
MP3 音频数据由一系列帧组成,每个帧都代表着一小段音频。在一个帧中,包含一个标头和音频数据。音频数据采用哈夫曼编码方式进行压缩。每一帧都正好代表着每个通道的 1,152 个频域样本(单声道/立体声)。它被分成两个称为区组的块,每个块包含 576 个样本。 每一帧还会保留与其解码相关的信息,称为辅助信息 (图 3)。
在 MP3 解码过程中执行的大多数操作都比较复杂,理论上来说,也是容易出现细微漏洞的地方。在现实情况下,许多操作(例如修改 DCT、多相滤波组和混叠消除)都在缓冲区上执行,而其中始终保留着 576 个样本的值。因此,在这里找到越界写入漏洞似乎不太可能。哈夫曼解码方法可能会吸引我们的注意,因为它本质上是作用于更具动态性的数据(与包含 576 个样本的缓冲区不同)。
MP3 哈夫曼解码
对区组(576 个样本)执行哈夫曼解码时,使用的是代码表(而不是二叉树)。总频率范围为 0 至 22,050(奈奎斯特频率),划分为五个区域(图 4):
区域 1、2 和 3 是三个“大值”区域,其中样本的值介于 -8,206 到 8,206 之间
区域 4 是“count1 区域”,这是值 -1、0 或 1 的四元组
区域 5 是“rzero 区域”。在此区域,较高频率的值被假定为波幅较低,因为不需要编码;这些值等于 0。
每个区域都有自己的哈夫曼表(0 区域除外),因此,不同频率的样本会经过不同的编码。
样本归类为哪些区域取决于以下变量:
big_values——指定大值区域总共包含多少个样本
region0_count 和 region1_count——将 big_values 划分为子区域;从 big_values 中减去它们之和,得到的就是 region2 中的样本数
part2_3length——指定将多少个数据位用于比例因子(第 2 部分),将多少个数据位用于哈夫曼编码(第 3 部分)
对 576 个样本进行解码的过程如下所示:
对 big_values 区域中的样本进行解码
对 count1 区域中的样本进行解码
如果处理的数据位数大于 part2_3length,则表示已实际解码所有输入数据,甚至包含过度读取的数据。 因此,需要从 total_samples_read 中减去 4(即去除这些输入位)
如果还有样本剩余,则将其填充为 0,从而形成 0 区域
这一逻辑在表 1 中显示为伪代码。
total_samples_read = 0;
// Decode big_values region
[redacted for brevity]
// Decode count1 region [1]
for (int i = 0; i < count1; i++) {
samples[total_samples_read++] = huff_decode(bitstream, count1_huff_table);
}
if (bits_processed > part2_3length) [2] {
// Overread. Throw last 4 samples
total_samples_read -= 4;
}
// Fill rzero region with zeros
for (int i = total_samples_read; i < 576; i++) {
samples[total_samples_read++] = 0; [3]
}
表 1:哈夫曼解码逻辑的伪代码
遗憾的是,该代码遗漏了一种特定的边缘情况,从而导致发生整数下溢。
大值区域的大小为 0
Count1 区域的大小为 0
Part2_3length 为 0
Bits_processed 大于 0
在此特定情况下,bits_processed 将大于 part2_3length(在比例因子解码期间,此值在解码过程之前达到非零值)。因此,该代码将会“丢弃”最后四个样本。由于代码未处理任何样本,因此 total_samples_read 为 0。我们将在此处遇到下溢,并且代码认为我们已经处理了 -4 个样本。现在,它将如下所示填充 0 区域:
将缓冲区指针设置为 &samples[total_samples_read]。此指针指向样本缓冲区 之前 的 16 个数据位。
将写入大小设置为 576 - total_samples_read = 576 - (-4) = 580 整数。
这样,紧靠样本缓冲区之前就有了一个越界写入,其值为零。很好!
那么,这个漏洞为什么没有遭受 CVE 攻击呢?样本缓冲区是某个结构体的一部分,而紧靠样本缓冲区之前的域则是比例因子阵列。此阵列中包含我们已经控制的域,因此我们并未在此处造成预期的影响。
当代码对样本执行反量化之后,也会出现同样的漏洞。它会再次填充 0 区域,但这次是填入已反量化的缓冲区。猜一猜反量化后的缓冲区前面是什么?经过哈夫曼解码的样本缓冲区。这与我们之前看到的样本缓冲区相同,同样也在我们的控制之中。所以,我们同样没有造成真正的影响。
这些越界写入仍然存在于 MP3 解码器之中(通过 WAV 和 .mp3 文件都能访问),并且 Microsoft 表示未来将予以修复。尽管在编解码器的反转期间并未找到任何具有影响力的漏洞,但我们相信,解码器执行的不同复杂操作中可能还隐藏着其他漏洞。
第二次尝试:IMA ADPCM 编解码器中的整数溢出
我们接下来将尝试利用 IMA ADPCM 编解码器,它在 imaadp32.acm 中执行。我们现在已经知道,ACM 将负责管理不同编解码器之间的来回转换。要寄存编解码器,代码就必须执行 ACM 函数。其中一个此类函数就是 acmStreamSize,它会返回目的地缓冲区所需的字节数。
IMA ADPCM 编解码器会计算目的地缓冲区大小,其依据为输入负载的大小 (cbSrcLength)、对齐 (nBlockAlign) 和每个块的样本数(wSamplesPerBlock;表 2)。
(cbSrcLength / pwfxSrc->nBlockAlign) *(pwfxSrc->wSamplesPerBlock * pwfxDst->nBlockAlign)
表 2:缓冲区大小计算
在执行乘法之前,代码会确保计算不会造成整数溢出(表 3)。
SrcNumberOfBlocks = cbSrcLength / pwfxSrc->WaveFormat.nBlockAlign;
v14 = pwfxSrc->wSamplesPerBlock * pwfxDst->nBlockAlign;
if ( 0xFFFFFFFF / v14 < SrcNumberOfBlocks )
return ERROR_OVERFLOW;
IsThereRemainder = cbSrcLength % pwfxSrc->WaveFormat.nBlockAlign;
if ( IsThereRemainder )
++SrcNumberOfBlocks;
DstBufferLengthInBytes = v14 * SrcNumberOfBlocks;
表 3:为防止整数溢出而执行的计算检查
很明显,这种检查还不足以防止溢出。如果除法算式 cbSrcLength/pwfxSrc->nBlockAlign的结果有余数,代码会增大 (cbSrcLength /pwfxSrc->nBlockAlign)的结果,并在乘法计算中使用该结果。 溢出检查并未涵盖这一结果增大。因此,我们仍然可以通过指定自定义值来使目的地缓冲区的长度出现溢出。
我们需要提供 cbSrcLength ,它在除以 pwfxSrc->nBlockAlign时出现了余数。
表 4 显示了一个导致整数溢出的值示例。
cbSrcLength = 0x71c71c72
pwfxSrc->nBlockAlign = 8
pwfxSrc->wSamplesPerBlock = 9
pwfxDst->nBlockAlign = 2
表 4:导致整数溢出的值示例
此情况会导致目的地缓冲区的大小为 0xE 字节,而它应该要大得多。
尽管这看上去像是一个有可能导致越界写入的整数溢出,但解码函数可以 正确地 确保不会在分配缓冲区之后发生写入,并且不会假设已分配了正确大小的缓冲区。
因此,尽管我们提供了多个样本,但目的地缓冲区实际上已满,代码将停止运行。在 AD PCM 编解码器(在 msadp32.acm中执行)中,表现出了同样的行为。
第三次尝试:ACM 中的整数溢出 (CVE-2023-36710)
最后,我们在 ACM 代码中发现了一个很好利用的漏洞。在播放 WAV 文件的过程中,将会调用 ACM 管理器中的 mapWavePrepareHeader 函数(在 msacm32.drv中执行)。
此函数存在一个整数溢出涵洞。它会调用 acmStreamSize,而后者则会调用驱动程序的回调。前面说过,此函数会返回所需的目的地缓冲区大小。收到此大小信息之后, mapWavePrepareHeader 将会增加 176 字节(将继续保持目的地缓冲区的数据流标头大小),并且不会执行溢出检查。增加这些字节之后,生成的代码将传递至 GlobalAlloc (图 6)。
这是一个可被利用的问题。我们可以使 GlobalAlloc 只分配一个极小的缓冲区,而不是让 acmStreamSize 返回一个介于 0xffffff50 至 0xffffffff 的值以分配较大的缓冲区。完成此分配之后,我们可以造成两种越界写入:
数据流标头值,例如结构体大小以及来源和目的地缓冲区指针和大小。这些值部分可控。
编解码器的已解码值。这些值完全可控。
为了触发该漏洞,我们需要提供一个 WAV 样本,其大小在解码之后应大于或等于 0xffffff50。尽管这听起来很容易做到,但我们在多次尝试之后发现,在某些编解码器中可能无法实现。以 MP3 编解码器为例,其计算过程中会乘以 1,152 或 576(这些是每帧的样本数)。计算结果永远达不到我们所需要的范围。
最后,我们成功地使用 IMA ADP 编解码器触发了该漏洞。文件大小达到约 1.8 GB。通过对计算执行数学限制操作,我们可以得出结论:使用 IMA ADP 编解码器时,可以达到的最小文件大小为 1 GB。
如果可以使用脚本引擎来发起动态入侵,利用此类漏洞将变得更为容易。由于 Windows Media Player 并没有此类引擎,因此入侵会变得更为困难。但这仍有可能做到(正如 Chris Evans 在其撰写的《更高明的入侵:针对 Linux 台式机的无脚本零日入侵》博文中展示的那样)。现在,还是有更多机会在 Outlook 应用程序环境(或其他即时通信应用程序)中成功地利用这一漏洞。
总结
本系列博文介绍了一些研究成果,从受到攻击者广泛利用的一个漏洞开始。(如果您还未阅读 第 1 部分,请立即阅读。)随后的研究过程是寻找绕过,并且最终找到了一个伴生漏洞,将其串连起来后,可实现零点击 RCE 攻击链。尽管这些漏洞已被修复,攻击者仍会继续寻找类似的攻击面和漏洞并加以远程利用。
到目前为止,我们在 Outlook 中发现的攻击面仍然存在,并且还能发现并利用新的漏洞。尽管 Microsoft 对 Exchange 进行了修补,可以放弃包含 PidLidReminderFileParameter 属性的邮件,但我们无法排除绕过这一抵御措施的可能性。
附录
本网站 列出了 Microsoft 的 Media Foundation 平台中提供的所有媒体类型和编解码器。
我们的测试表明,实际上只有以下编解码器可以通过 WAV 使用:
1—— PCM
2—— ADPCM
6—— A-LAW
7—— U-LAW
11—— IMA ADPCM
31—— GSM 6.10
55—— MPEG-1 Audio Layer III (MP3)
00000003_0000_0010_8000_00aa00389b71——IEEE Float
00000008-0000-0010-8000-00aa00389b71——DTS Audio
00000092-0000-0010-8000-00aa00389b71——Dolby Digital
00000164-0000-0010-8000-00aa00389b71——Microsoft WMA