在这份趋势科技漏洞研究服务漏洞报告的摘录中,趋势科技研究团队的Guy Lederfein和Jason McFadyen详细说明了微软Windows中最近修补的一个远程代码执行漏洞。这个漏洞最初是由微软攻击性研究与安全工程团队发现的。成功利用可能导致使用易受攻击库的应用程序上下文中的任意代码执行。以下是他们关于CVE-2024-20697的报告的一部分,只做了一些最小的修改。
在微软Windows中包含的Libarchive库存在一个整数溢出漏洞。这个漏洞是由于对RARVM过滤器的块长度进行不充分的边界检查导致的,RARVM过滤器用于RAR归档中压缩数据的Intel E8预处理。
远程攻击者可以通过诱使目标用户提取一个精心制作的RAR归档来利用这个漏洞。成功利用可能导致使用易受攻击库的应用程序上下文中的任意代码执行。
漏洞
RAR文件格式支持数据压缩、错误恢复和多卷跨越。RAR格式有几个版本:RAR1.3、RAR1.5、RAR2、RAR3和最新版本RAR5。不同的RAR版本使用不同的压缩和解压缩算法。
以下描述了1.5、2.x和3.x版本使用的RAR格式。RAR归档由一系列可变长度块组成。
每个块以一个头开始。下面的表格是一个RAR块头的通用结构:
偏移量 大小 名称 描述
------ -------- ------------ -------------------------------------------------------------
0x00 2 HeadCRC 块头的CRC
0x02 1 HeaderType 块类型:
RarBlock标记 (0x72)
ArcHeader (0x73)
FileHeader(0x74)
旧风格注释头(0x75)
旧风格真实性信息 (0x76)
旧风格子块 (0x77)
旧风格恢复记录 (0x78)
旧风格真实性信息 (0x79)
SubBlock (0x7a)
EndBlock (0x7b)
0x03 2 Flags 块标志:
SKIP_IF_UNKNOWN (0x4000)
LONG_BLOCK (0x8000)
0x05 2 HeadSize 头大小
RarBlock Marker是RAR归档的第一个块,作为RAR格式文件的签名:
偏移量 大小 名称 描述
-------- -------- ------------ --------------------------------------------
0x00 2 HeadCRC 块头的CRC = 0x6152
0x02 1 HeaderType 块类型 = 0x72
0x03 2 Flags 块标志 = 0x1A21
0x05 2 HeadSize 头大小 = 0x0007
这个块在每个RAR文件的开头总是包含以下字节序列:
0x52 0x61 0x72 0x21 0x1A 0x07 0x00 (ASCII: "Rar!x1Ax07x00")
ArcHeader是RAR文件中的第二个块,具有以下结构:
偏移量 大小 名称 描述
-------- -------- ------------ ---------------------------------
0x00 2 HeadCRC 块头的CRC
0x02 1 HeaderType 块类型 = 0x73
0x03 2 Flags 块标志:
MHD_VOLUME (0x0001)
MHD_COMMENT (0x0002)
MHD_LOCK (0x0004)
MHD_SOLID (0x0008)
MHD_NEWNUMBERING (0x0010)
MHD_AV (0x0020)
MHD_PROTECT (0x0040)
MHD_PASSWORD (0x0080)
MHD_FIRSTVOLUME (0x0100)
0x05 2 HeadSize 块大小 = 0x000D
0x07 2 HighPosAV
0x09 4 PosAV
ArcHeader块后面跟着一个或多个FileHeader块。这些块具有以下结构:
偏移量 大小 名称 描述
-------- -------- ------------ ---------------------------------
0x00 2 HeadCRC 块头的CRC
0x02 1 HeaderType 块类型 = 0x74
0x03 2 Flags 块标志:
LHD_SPLIT_BEFORE (0x0001)
LHD_SPLIT_AFTER (0x0002)
LHD_PASSWORD (0x0004)
LHD_COMMENT (0x0008)
LHD_SOLID (0x0010)
LHD_WINDOWMASK (0x00E0)
LHD_LARGE (0x0100)
LHD_UNICODE (0x0200)
LHD_SALT (0x0400)
LHD_VERSION (0x0800)
LHD_EXTTIME (0x1000)
0x05 2 HeadSize 完整头大小,不包括文件数据
0x07 4 PackSize 压缩文件大小 (P)
0x0B 4 UnpSize 未压缩文件大小
0x0F 1 HostOS 用于归档的操作系统:
HOST_MSDOS (0x00)
HOST_OS2 (0x01)
HOST_WIN32 (0x02)
HOST_UNIX (0x03)
HOST_MACOS (0x04)
HOST_BEOS (0x05)
0x10 4 FileHash 文件的CRC32哈希
0x14 4 FileTime 日期和时间以MS-DOS格式
0x18 1 UnpVer 需要的RAR版本以解压文件
0x19 1 Method 打包方法:
存储 (0x30)
最快 (0x31)
快 (0x32)
普通 (0x33)
好 (0x34)
最好 (0x35)
0x1A 2 NameSize 文件名大小 (N)
0x1C 4 FileAttr 文件属性 (操作系统依赖)
0x20 4 HighPackSize 压缩文件大小的高4字节64位值
可选 - 仅当LHD_LARGE标志设置时出现
0x24 4 HighUnpSize 未压缩文件大小的高4字节64位值
可选 - 仅当LHD_LARGE标志设置时出现
0x28 N FileName 文件名
0x28+N 8 Salt 盐值
可选 - 仅当LHD_SALT标志设置时出现
0x30+N. M ExtTime mtime、ctime、atime和arctime的精确时间
可选 - 仅当LHD_EXTTIME标志设置时出现
0x30+N+M P Data 文件数据
如果Method > 0x30,数据将被压缩
请注意,上述偏移量是相对于可选字段的存在而言的。
EndBlock 块表示RAR归档的结束。这个块具有以下结构:
偏移量 大小 名称 描述
-------- -------- ------------ ---------------------------------
0x00 2 HeadCRC 块头的CRC
0x02 1 HeaderType 块类型 = 0x7B
0x03 2 Flags 块标志:
EARC_NEXT_VOLUME (0x0001)
EARC_DATACRC (0x0002)
EARC_REVSPACE (0x0004)
EARC_VOLNUMBER (0x0008)
0x05 2 HeadSize 头大小
0x07 4 ArcDataCRC RAR归档的CRC32哈希
可选 - 仅当EARC_DATACRC标志设置时出现
0x0B 2 VolNumber 卷号
可选 - 仅当EARC_VOLNUMBER标志设置时出现
对于RAR归档中的每个 FileHeader 块,如果 Method 字段没有设置为 "Store" (0x30
),那么 Data 字段将包含压缩的文件数据。解压缩的方法取决于用于压缩数据的RAR版本。需要解压压缩数据的RAR版本记录在 FileHeader 块的 UnpVer 字段中。
与本报告相关的是RAR格式版本2.9(即RAR4)使用的RAR提取方法,当 UnpVer 字段设置为29时使用。压缩数据可能使用Lempel-Ziv(LZ)算法压缩,或使用预测部分匹配(PPM)压缩。本报告不会详细描述提取算法,而只会总结与理解漏洞相关的部分。有关提取算法的参考实现,请参见UnRAR 源代码中的 Unpack::Unpack29()
函数。
当libarchive库尝试从RAR归档中提取文件内容时,如果文件数据被压缩(即 Method 字段没有设置为 "Store"),将调用 read_data_compressed()
函数来提取压缩数据。压缩数据由多个块组成,每个块可以分别使用LZ算法(通过将块的第一个位设置为0表示)或使用PPM压缩(通过将块的第一个位设置为1表示)。最初,将调用 parse_codes()
函数来解码提取文件数据所需的表。如果遇到使用LZ算法压缩的数据块,将调用 expand()
函数来解压缩数据。在 expand()
函数中,将通过循环调用 read_next_symbol()
从压缩数据中读取符号。在 read_next_symbol()
函数中,将根据 parse_codes()
函数解码的哈夫曼表解码符号。
如果解码出的符号是257,将调用 read_filter()
函数来读取RARVM过滤器,它具有以下结构:
偏移量 大小 名称 描述
-------- -------- ------------ ---------------------------------
0x00 1 Flags 过滤器标志:
LENGTH (0x07)
READ_GLOBAL_DATA (0x08)
READ_REGISTERS (0x10)
READ_BLOCK_LENGTH (0x20)
ADD_258_BLOCK_START (0x40)
READ_FILTER_NUM (0x80)
0x01 1 LengthExt1 长度扩展 #1
可选 - 仅当LENGTH设置为6或7时出现
0x02 1 LengthExt2 长度扩展 #2
可选 - 仅当长度设置为7时出现
0x03 var Code 过滤器代码
请注意,上述偏移量是相对于可选字段的存在而言的。
Code 字段大小的计算如下:如果标志字段的最低3位(将称为 LENGTH)小于6,则代码大小为 (LENGTH + 1)。如果 LENGTH 设置为6,则代码大小为 (LengthExt1 + 7)。如果 LENGTH 设置为7,则代码大小为 (LengthExt1 << 8) | LengthExt2
。在计算出代码长度并将代码本身复制到缓冲区后,将代码、其长度和过滤器标志发送到 parse_filter()
函数以解析代码段。
在代码段内,通过调用函数 membr_next_rarvm_number()
解析数字。该函数读取2位,并根据其值确定要读取多少位以解析值。如果前2位是0,则读取4位值位;如果是1,则读取8位值位;如果是2,则读取16位值位;如果是3,则读取32位值位。
parse_filter()
函数将解析代码段,其具有以下结构:
偏移量 大小 名称 描述
-------- -------- ---------------- --------------------------------------------------------
0x00 var FilterNum 过滤器编号
可选 - 仅当READ_FILTER_NUM标志设置时出现
var var BlockStart 块开始位置
如果设置了ADD_258_BLOCK_START标志,
将在此值上加258
var var BlockLength 可选 - 仅当READ_BLOCK_LENGTH标志设置时出现
var var Registers 包含长度为7位的寄存器位掩码
后跟最多7个寄存器值
可选 - 仅当READ_REGISTERS标志设置时出现
var var ByteCodeLen (L) 下述ByteCode字段的长度(以字节为单位)
var L ByteCode 要编译的字节码
请注意,如果未设置 READ_REGISTERS 标志,寄存器将被初始化,使得第5个寄存器设置为块长度,该长度要么从代码段中读取(如果设置了 READ_BLOCK_LENGTH 标志),要么从前一个过滤器的块长度中继承。
在 parse_filter()
中解析完这些字段后,ByteCode 字段及其长度将被发送到 compile_program()
函数。在这个函数中,字节码的第一个字节将被验证,以确保它等于字节码中所有其他字节的异或值。如果为真,它将设置 rar_program_code
结构体的 fingerprint 字段为 CRC-32 算法在完整字节码上运行的结果,与左移 32 位的字节码长度组合。
回到 parse_filter()
函数,在为过滤器计算完所有字段后,将通过调用 create_filter()
并使用包含 fingerprint 字段和计算出的寄存器值的 rar_program_code
结构体来初始化 rar_filter
结构体。这些值将分别设置到 rar_filter
结构体的 prog 字段和 initialregisters 字段。
一旦完成过滤器的处理,将调用 run_filters()
函数来运行解析后的过滤器。这个函数通过调用 execute_filter()
来执行每个过滤器,初始化 rar_filters
结构体的 vm 字段为类型为 rar_virtual_machine
的结构体。这个结构体包含一个 registers 字段,这是一个包含 8 个整数的数组,以及一个大小为 0x40004
的内存字段。如果与执行的过滤器相关联的 rar_program_code
结构体的 fingerprint 字段等于 0x35AD576887
或 0x393CD7E57E
,则将调用 execute_filter_e8()
函数。这个函数从 initialregisters 数组的第五个字段读取块长度。然后,运行一个循环来替换 VM 内存中的 0xE8
和/或 0xE9
实例,使用块长度作为循环退出条件。
Libarchive 库中存在一个整数溢出漏洞,该库包含在微软 Windows 中。这个漏洞是由于对 RAR 归档中包含的用于 Intel E8 预处理的 RARVM 过滤器的块长度进行不充分的边界检查所致。具体来说,如果归档包含一个 RARVM 过滤器,其 fingerprint 字段被计算为 0x35AD576887
或 0x393CD7E57E
,则将通过调用 execute_filter_e8()
来执行它。如果过滤器的第五个寄存器设置为块长度 4,则此函数中的循环条件(设置为块长度减去 5)将溢出到 0xFFFFFFFF
。由于 VM 内存的大小为 0x40004
,这将导致超出基于堆的缓冲区表示的 VM 内存的边界内存访问。
远程攻击者可以通过诱使目标用户提取一个精心制作的 RAR 归档来利用这个漏洞,该归档包含一个 RARVM 过滤器,其第五个寄存器设置为 4。成功利用可能导致使用易受攻击库的应用程序上下文中的任意代码执行。
注意事项:
• 所有多字节整数都是小端字节序。• 除非另有说明,否则所有偏移量和大小都是以字节为单位。• 由于没有 RAR4 格式的官方文档,描述基于 UnRAR 和 libarchive 源代码。字段名称要么从源代码复制,要么根据功能给出。
检测指导
为了检测利用此漏洞的攻击,检测设备必须监视并解析常见的端口上的流量,其中可能会发送 RAR 归档,例如 FTP、HTTP、SMTP、IMAP、SMB 和 POP3。
检测设备必须查找 RAR 文件的传输,并能够解析 RAR 文件格式。目前,没有 RAR 文件格式的官方文档。这个检测指导基于 UnRAR 程序和 libarchive 库提供的提取 RAR 归档的源代码。
上面详细描述了 RAR 块头的通用结构。检测设备首先必须查找 RarBlock 标记,这是 RAR 归档的第一个块,也是 RAR 格式文件的签名:
偏移量 大小 名称 描述
-------- -------- ------------ --------------------------------------------
0x00 2 HeadCRC 块头的CRC = 0x6152
0x02 1 HeaderType 块类型 = 0x72
0x03 2 Flags 块标志 = 0x1A21
0x05 2 HeadSize 头大小 = 0x0007
检测设备可以通过查找以下字节序列来识别此块:
0x52 0x61 0x72 0x21 0x1A 0x07 0x00 ("Rar!x1Ax07x00")
如果找到,设备随后必须识别 ArcHeader,这是 RAR 文件中的第二个块,如上所述。ArcHeader 块后面跟着一个或多个 FileHeader 块,其结构也在上文中详细描述。请注意,上述偏移量是相对于可选字段的存在而言的。
检测设备必须解析每个 FileHeader 块,并检查其 Method 字段。如果 Method 字段的值大于 0x30
,则检测设备必须检查 FileHeader 块的 Data 字段,其中包含压缩的文件数据。压缩数据可能使用 Lempel-Ziv (LZ) 算法压缩,或使用预测部分匹配 (PPM) 压缩。这个检测指导不会详细描述提取算法。有关提取算法的参考实现,请参见 UnRAR 源代码中的 Unpack::Unpack29() 函数。
压缩数据由多个块组成,每个块都可以使用 LZ 算法压缩(通过将块的第一个位设置为 0 表示)或使用 PPM 压缩(通过将块的第一个位设置为 1 表示)。检测设备必须根据用于压缩它的算法提取每个块。如果遇到使用 LZ 算法压缩的块,检测设备必须从压缩数据的开头解码哈夫曼表。然后,检测设备必须迭代遍历其余的压缩数据,并根据生成的哈夫曼表解码每个符号。如果遇到符号 257,则必须将以下数据解析为 RARVM 过滤器,其结构如下:
Offset Size Name Description
-------- -------- ------------ ---------------------------------
0x00 1 Flags Filter flags:
LENGTH (0x07)
READ_GLOBAL_DATA (0x08)
READ_REGISTERS (0x10)
READ_BLOCK_LENGTH (0x20)
ADD_258_BLOCK_START (0x40)
READ_FILTER_NUM (0x80)
0x01 1 LengthExt1 Length Extension #1
Optional - only present if LENGTH is set to 6 or 7
0x02 1 LengthExt2 Length Extension #2
Optional - only present if Length is set to 7
0x03 var Code Filter code
请注意,上述偏移量是相对于可选字段的存在而言的。
检测设备接下来必须计算 Code 字段的大小。Code 字段大小的计算方法如下:如果标志字段(将被称为 LENGTH)的最低三位小于 6,则代码大小是 (LENGTH + 1)。如果 LENGTH 设置为 6,则代码大小是 (LengthExt1 + 7)。如果 LENGTH 设置为 7,则代码大小是 (LengthExt1 << 8) | LengthExt2
。计算出 Code 字段的大小后,必须根据以下结构解析 Code 字段:
偏移量 大小 名称 描述
-------- -------- ---------------- --------------------------------------------------------
0x00 var FilterNum 过滤器编号
可选 - 仅当 READ_FILTER_NUM 标志设置时出现
var var BlockStart 块开始位置
如果设置了 ADD_258_BLOCK_START 标志,
则在此值上加 258
var var BlockLength 可选 - 仅当 READ_BLOCK_LENGTH 标志设置时出现
var var Registers 包含长度为 7 位的寄存器位掩码
后跟最多 7 个寄存器值
可选 - 仅当 READ_REGISTERS 标志设置时出现
var var ByteCodeLen (L) 下述 ByteCode 字段的长度(以字节为单位)
var L ByteCode 要编译的字节码
此结构中的所有数值字段(FilterNum、BlockStart、BlockLength、寄存器值和 ByteCodeLen)必须根据 UnRAR 源代码中 RarVM::ReadData()
函数实现的算法读取。该算法读取数据的 2 位,表示包含数值的位数。请注意,此结构中的某些字段是可选的,并且取决于 RARVM 过滤器结构的 Flags 字段中设置的标志。
提取完所有必要的字段后,检测设备必须检查以下条件:
• ByteCode 字段的 CRC-32 校验和为 0xAD576887
且 ByteCodeLen 字段为 0x35
,或者 ByteCode 字段的 CRC-32 校验和为 0x3CD7E57E
且 ByteCodeLen 字段为 0x39
。• 设置了 READ_REGISTERS 标志并且 Registers 字段的第 5 个寄存器的值设置为 4
,或者设置了 READ_BLOCK_LENGTH 标志并且 BlockLength 字段的值设置为 4
。如果同时满足这两个条件,流量应被视为可疑。可能正在进行利用此漏洞的攻击。
注意事项:
• 所有多字节整数都是小端字节序。• 除非另有说明,否则所有偏移量和大小都以字节为单位。
结论
微软在 2024 年 1 月修补了此漏洞,并分配了 CVE-2024-20697。尽管他们没有推荐任何缓解因素,但您可以采取一些额外措施来帮助保护免受此错误的利用。这包括不要从不可信的来源提取 RAR 归档文件,并使用本文“检测指导”部分提供的指导过滤流量。尽管如此,建议应用供应商补丁以完全解决此问题。
原文始发于微信公众号(3072):CVE-2024-20697 RAR归档RCE 漏洞分析与检测
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论