0x00 概述
在本文中,我们将介绍利用CVE-2019-12750的更为复杂的一种方式,这是影响Symantec Endpoint Protection(以下简称SEP)的一个本地提权漏洞(LPE)。在本文撰写时,我们所使用的漏洞利用方式依然适用于所有版本的Windows系统。
漏洞利用过程如下图所示:
图1. 漏洞利用过程
受影响的SEP版本如下:
Symantec Endpoint Protection v14.x < v14.2 (RU1)
Symantec Endpoint Protection v12.x < 12.1 (RU6 MP10)
Symantec Endpoint Protection Small Business Edition v12.x < 12.1 (RU6 MP10c)
0x01 前言
在前一篇文章中,我们介绍了如何在Windows 10 v1803之前的系统中来利用SEP中存在的一个漏洞,这里我们将以最新版的Windows v1909系统来探讨该漏洞的利用方法。
这里我们需要研究利用该漏洞的更为复杂的一种方法。在Windows 10 v1809及后续系统版本中,Windows为内核模式池分配引入了Low Fragmentation Heap(LFH,低碎片堆)机制,导致我们最初的利用方法无法使用。
因此我们需要寻找新方法,在内核模式中执行代码,绕过其他漏洞利用缓解机制(如SMEP及KASLR)。
0x02 LFH
在最新版的Windows 10中,由于部署了内核模式LFH,导致我们无法通过该漏洞获取令牌对象(Token Object)。因此我们需要找到另一种方法,才能成功在受影响的主机上实现本地权限提升。
在Windows 10 v1803之前的系统版本中,泄露出的内核内存页面通常会包含各种大小的内核对象。在新版本系统中,内核池分配器会将相同大小的对象尽可能放在一个内存页面中,以便降低内存碎片率。当我们处理分配的小空间时(比如我们这里使用的0x30
字节的chunk,如图2所示),会发现基本上大小相同的已分配的空间都处于同一个内存页面中。
图2. 相同大小的chunk会被归到同一个组中
如上图所示,泄露出的内核池内存页面中只会包含该大小的chunk,因此我们无法使用前一篇文章中提到的方法来获取令牌对象。当然,我们还是有可能在一些填充缝隙中泄露出某些信息,但我们决定寻找其他利用方法。
0x03 初步分析
根据现有情况可知,已分配的相同大小的空间实际上属于不同的对象。此时我们需要仔细研究这些pool chunk,澄清我们可以在用户态中获取哪类信息、控制哪种数据。
在特定条件下,该软件中的许多驱动会分配大小为0x30
字节的chunk,因此我们先以这些专有对象为研究点。
我们发现标记为B2d2
的对象比较有趣,这些对象中包含指向另一个驱动(BHDrvx64.sys
)的一个指针。此外,其中还包含指向函数指针表的一个指针,因此更值得研究。
图3. B2d2
chunk(BHDrvx64.sys
)
这里我们需要关注两个要点。
首先,我们拿到了与漏洞相关chunk同样大小的一个对象,这意味着该对象可以存放到同一个内存页面中。
其次,我们拿到了指向目标软件另一个驱动的函数表的一个指针,这意味着我们同样获取了必要的信息,可以在后续利用过程中找到对应的内核模式基址,寻找可以执行的一组指令gadget。
这是不错的开头,但任务还未结束,我们需要澄清对这类对象的控制程度,具体要澄清如下3个问题:
1、对象的分配时机;
2、对象的释放时机;
3、函数表指针的使用场景。
0x04 分析B2d2对象
我们可以简单搜索特定池标志,找到BHDrvx64.sys
中负责分配此类对象的函数。
图4. B2d2
对象分配空间
完成对象分配后,SEP会将函数表指针写入该数据缓冲区的起始处,然后将剩余空间置零。
图5. 初始化B2d2
对象
如果观察分配该对象时的调用栈,我们可以获取到一些相关信息,比如调用该函数的执行路径以及触发对象创建的具体事件类型。
我们发现有各种回调函数可能会触发该对象分配操作,其中包括:
1、注册表键值(NtCreateKey
、NtSetValueKey
等);
2、进程句柄(NtOpenProcess
);
3、进程终止(NtTerminateProcess
);
4、可执行映像加载(exe、dll等);
5、创建文件(NtCreateFile
);
6、关闭句柄(NtClose
)。
这里我们将重点关注关闭文件句柄,这是根源所在,后面我们将简要解释这一点。
图6. B2d2
对象创建
根据前文描述,此时对象前8
个字节为函数指针表的地址,剩余数据则填充为0。但这里还有个细节对漏洞利用至关重要,实际上SEP还会在该对象中添加一些额外的信息。
图7. B2d2
对象附加信息
我们可以通过硬件断点来监控SEP对剩余对象数据的访问,检查调用栈后,我们发现在剩余数据被添加到对象内部时,nt!RtlAppendUnicodeStringToString
函数会被调用。这个信息可以帮助我们回溯执行流,最终澄清这部分剩余数据为UNICODE_STRING
结构体的长度(USHORT Length
)及最大长度字段(USHORT MaximumLength
),后面跟着指向实际Unicode字符串缓冲区的一个指针(PWSTR Buffer
)。
因此我们创建了一个测试应用,该程序用来创建文件,然后关闭文件句柄。由于我们知道完整的路径长度(以字节为单位),因此我们可以使用条件断点,以便在正确的时间触发。
图8. B2d2
对象附加信息
需要注意的是,驱动预分配的B2d3
缓冲区最多可容纳0x802
字节。这意味着当文件路径信息写入B2d2
对象中时,MaximumLength
字段值会被设置为该值,因为现在指针引用的是另一个缓冲区。
图9. 最大字段值更新
如果完整路径超过0x800
字节,剩余字符就会被丢弃,Length
字段会被设置为该值。
我们可以使用较长的路径名来创建文件,使Length
字段值被设置为0x800
,MaximumLength
字段值被设置为0x802
。通过这种方式,我们可以将我们自己的B2d2
对象与通过其他进程创建的对象区分开来。
大家可能会好奇为什么我们一定要澄清这些附加数据的具体含义。
在第一篇文章中,我们提到过该漏洞允许每个进程获取分页池内核数据的一个内存页面,但每次提取到的并不一定都是不同的页面。由于泄露页面中的对象可能与任何正在运行的进程有关,因此我们需要找到一种方法来确定利用过程中应以哪些B2d2
对象为目标。
显然,如果破坏我们无法控制的某个对象的函数表指针,那么当另一个进程尝试访问该对象时,将导致主机崩溃。
来总结一下最新的发现:
1、进程创建文件;
2、进程关闭文件句柄,创建B2d2
对象;
3、B2d2
对象中添加函数表指针;
4、B2d2
对象中添加文件路径长度字段、最大长度字段以及指向字符串缓冲区的一个指针。
换而言之,如果我们的进程创建1个文件,然后关闭对应的文件句柄,那么只要进程处于运行状态,就能得到与该文件关联的一个B2d2
对象。当进程终止时,驱动会使用来自该对象的函数表指针来调用匹配的清理函数。
掌握这些信息后,现在我们可以开展下一阶段工作。
0x05 接管执行流
我们已经掌握劫持执行流的所需信息,因此可以创建一个简单的PoC,手动修改我们B2d2
对象中的函数表指针。
图10. 修改指针
图11. 控制执行流的Poc
我们将原始指针替换为虚假指针(0xF8F8F8F8F8F8F8F8
),然后结束进程,以便确认在对象被释放时,我们的确能控制执行流。
0x06 处理KASLR
我们通过已分配的PfFk
空间获得指向内核模块的指针,这些chunk与漏洞相关的pool chunk大小相同(0x30
字节),因此我们经常能看到这些chunk共享同一个内存页面。
图12. PfFk
chunk
泄露出的内核模块指针如下:
图13. 泄露的NtosKrnl
指针
此时有几个注意事项。比如这些空间中并不一定包含该指针,有可能是任意的内核模式地址。然而我们可以通过一种方法筛选出感兴趣的chunk。
nt!PfGlobals
符号所处的地址始终按16字节对齐,且该指针所处位置为nt!PfGlobals + 0x299
,这意味着该指针值始终以数字9
结尾。我们查看了不同版本Windows中的实现(Windows 10 v1809到v1909),不管有没有打最新补丁,都满足这种情况。
获取该地址后,我们可以轻松找到nt!PfGlobals
的地址,但这里存在一个“陷阱”。
ntoskrnl
并没有导出该符号,并且由于相对虚拟地址(RVA)在不同系统版本中有所不同,因此我们需要找到方法确定该地址,以计算内核模块映像基址。
这里我们不能使用常见的LoadLibraryEx
/GetProcAddress
函数组合来获取该符号的RVA,可以采用别的办法。
通过IDA Pro查看内核模块后,我们在ntoskrnl
的PAGE
段中找到了一些代码,其中多次引用了这个符号。
图14. 引用PfGlobals
的Ntoskrnl
代码
我们需要在用户空间中使用LoadLibraryEx
来加载ntoskrnl
,解析PE头获取PAGE
段信息,然后搜索这个代码模式。然后我们可以提取PfGlobals
的地址(x64系统中LEA
指令可以获取相对引用信息),减去用户模式映像基址,得到该符号的RVA。一旦得到该符号的RVA,我们就可以将其内核模式地址(通过内存泄露获得)减去该值,最终算出ntoskrnl
的映像基址。
在漏洞利用下一阶段,我们需要利用该信息来计算用来禁用SMEP的gadget的内核模式地址。
0x07 禁用SMEP
回到图10及图11,我们可以看到用来控制执行流的指针地址位于B2d2
chunk + 0x10
处,这也是RCX
寄存器指向的地址。
然而在SMEP启用状态下,我们无法使用该指针直接跳转到我们在用户模式下的payload,因此我们需要将其作为内核模式代码的重定向器来使用。
由于我们能完全控制该对象的内容,因此可以使用接下来的8个字节来存储指向一组指令的指针,以便进一步重定向执行流,临时禁用SMEP。
原始指针位于对象地址 + 0x10
处,指向的是存储在BHDriver64.sys
中的一个函数指针表(如图3所示)。由于我们有引用该地址的一条指令(如图5所示),因此可以使用相同的方法来计算该模块的内核模式映像基址。
接下来我们找到了一组指令集,可以进一步重定向执行流。
图15. BHDriverx64
Gadget
因此,对象中第一个被劫持的指针(offset 0x10
)用来将执行流引导这组指令(图15),提取0ffset 0x18
处的指针(RCX
已经指向对象地址 + 0x10
处),解析该指针以获取一个函数指针。由于我们只是读取该指针,因此可以使用用户模式中的指针(offset 0x18
)来读取并获得待跳转的内核地址。该指针既可以用来读取,也可以用来覆盖CR4,以便禁用SMEP。
在依托VMWare环境的Windows 10 v1909虚拟机中,CR4的值为0x3506f8
,而在Windows 10 v1809中,该值为0x1506f8
。
需要注意的是,虽然在v1909系统中SMAP
bit同样处于设置状态,但经过EFLAGS.AC
标志(对齐检查标志)运算后,该bit实际上并没有使用。
CR4寄存器的第20个bit及第21个bit用来控制SMEP及SMAP。
由于我们需要将寄存器的值改为0x506f8
,以便禁用SMEP(同时将SMAP bit复位),我们可以在0x50000
处分配一个用户模式页面,在0x506f8
处设置SMEP禁用gadget,这样当解析RCX,跳转到SMEP禁用gadget(图16)时,寄存器将被设置成我们需要的值,成功禁用SMEP,同时保持其余bit不变。
图16. NtosKrnl
SMEP启用/禁用Gadget
0x08 Payload执行
解决掉SMEP后,我们可以使用相同的方法来获取并修改另一个B2d2
对象,直接跳转到用户空间中的payload,该payload将使用前面获取的所有信息来提升漏洞利用进程的权限。
这里我们将使用PsLookupProcessByProcessId
来获取system
进程(PID 4)对象的地址,然后使用PsReferencePrimaryToken
来获取对应的Primary
令牌的地址。
接下来我们使用PsLookupProcessByProcessId
来获得漏洞利用进程对象的地址,然后使用系统进程使用的令牌指针来覆盖该进程对应的令牌指针。
最后,payload会跳转回我们在内核模式地址空间中的SMEP gadget,恢复原始值,使执行流恢复正常。
0x09 总结
在本文中,我们主要以Windows 10 v1909系统作为研究对象,澄清内核池LFH机制对我们漏洞利用的影响,此时内存泄露无法保障我们完成任务,因此我们不得不寻找其他的利用方式。虽然这个过程比较艰辛,涉及到方方面面,但最终我们还是成功搞定了有效的利用方法。
0x0A 时间线
2019年4月:发现漏洞
2019年4月18日:通知厂商
2019年4月19日:厂商确认漏洞
2019年4月19日:厂商请求延长漏洞处理时间
2019年7月31日:厂商发布安全公告
2019年12月12日:本文发布