*本文中涉及到的相关漏洞已报送厂商并得到修复,本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担。
一、背景介绍
VLC 是一款自由、开源的跨平台多媒体播放器及框架。它支持众多音频与视频解码器及文件格式和各类流式协议,也能作为流式服务器在 IPv4 或 IPv6 的高速网络连接下使用。
本文主要分析 VLC Media Player 2.2.8 版本的 MKV 格式视音频分离器 (demuxer) 中存在的一个 UAF 漏洞,实验环境为最新版本的 Win10 64 位, 主要从漏洞细节和漏洞的利用两大部分对漏洞进行分析。
在漏洞细节部分,主要分为以下三个方面:
1. 如何找到漏洞触发点?
2. 如何确定 UAF 对象?
3. 分析漏洞成因。
在漏洞利用部分,下面这些问题都将得到说明:
1. 如何通过精确的 Heap Spary 布置 shellcode?
2. 如何重新利用 UAF 对象?
3. 如何 bypass DEP&ASLR?
MKV
MKV 格式属于 Matroska 开源多媒体容器标准中的一种。它是建立在 EBML 语言的基础上,这是一种类似于 XML 格式的可扩展二进制元语言,使用可变长度的整数存储,以节省空间。EBML 元素都有自己的级别,每一个高一级的元素由若干次一级的元素组成。整个 MKV 文件 从整体看可分为 2 大部分:EBMLHeader 和 Segment。前者包含了文件的版本、文档类型等相关信息;后者则保存了媒体文件的视频和音频的实际数据,又可以再分为 SeekHead、Chapters、Attachments、Cluster 等若干子元素。
1.1 漏洞描述
天融信阿尔法实验室经过深入分析关于该漏洞主要是源于 VLC 播放器用于处理 MKV 格式的视音频分离器模块——libmkv_plugin.dll 中, 该模块的一个 matroska_segment_c 对象在释放后被重引用。可以通过构造大量和该对象大小相同的数据块,最终使这块内存重新被分配,后面再引用该对象时,便可控制程序指针,最后通过精确的 Heap Spray 布置 shellcode、ROP 绕过 DEP,造成任意代码执行。
1.2 受影响的系统版本
VLC media player <= 2.2.8
1.3 漏洞编号
CVE-2018-11529
二、漏洞细节
由于 VLC Media Player 的 release 版本没有调试信息,所以我们可以在官网下载源码自行编译带有调试信息的程序。另外要注意的的一点是,官方推荐使用 MinGW 在 Linux 环境下交叉编译 VLCMedia Player。而 MinGW 使用的是 DWARF 格式调试信息而非 PDB 格式,所以在 Windbg 中调试时,依然无法看到符号信息,但这依然方便了我们使用 IDA 静态分析。
2.1 漏洞触发点
首先我们需要找到漏洞触发的位置,使用 Windbg 自带工具 gflag 开启页堆:
将 vlc.exe 附加到 Windbg 上,打开 poc.mkv,在此处触发了异常,可以确定漏洞存在于 libmkv_plugin.dll 模块,再结合 IDA 和源码分析,得知具体位置是在函数 int Control(demux_t_0 *p_demux, int i_query, va_list args) 中
现在关闭页堆,重新打开 vlc.exe,并在刚才异常的位置下断点,再次打开 poc.mkv,可以看到 [ebp+28h] 处的值为 0x22000020,
查看 ebp 处的内容,发现正是 PoC 中构造的 UAF_object 的内容,因此可以基本确定,ebp 中保存的是指向 UAF 对象指针。
当然这里并不是漏洞的触发点,UAF 漏洞的触发一般都是要再次引用该对象的某个方法。所以我们可以在 [ebp+28h] 处设置一个内存访问断点(也就是上图中 0x22000020 的位置):ba r4 0f4069e8
继续运行程序,断在了函数 matroska_segment_c::UnSelect() 中:
继续往后执行几步便会触发漏洞,弹出计算器。
这里的 call 指令调用了一个位于 vlc.exe 模块中的地址,结合 PoC 来看也是 ROP 链第一个 gadget 的地址。
到这里已经可以初步推测漏洞成因,在对象 matroska_segment_c 被释放后,接着申请与其大小相同内存去「占坑」,待后面重引用该对象中某个函数或成员时,就可以控制程序执行流程。
2.2 释放重引用的位置
接下来的问题是——matroska_segment_c 对象是在哪里被释放,又是在哪里被重新分配呢?尝试使用!heap -p -a 查看分配此内存时的栈回溯信息,发现无法却显示。
这是因为这里编译器采用了一种称为 FPO(Frame Pointer Omission)的优化方式,它压缩或者省略了在栈上为函数创建栈帧指针的过程。这样可以加速函数调用,但是在这种情况下,调试器可能不能基于 EBP 展开栈帧(除非它有启用了 FPO 模块相匹配的符号文件,而前面提到过,我们没有办法生成用于 Windbg 调试的 PDB 文件)。
在哪里分配?
又因为程序每次加载时堆的地址都是不固定的,也无法靠下断点来进行分析,那么我们只能结合 IDA 和源码来分析了。通过交叉引用找到调用 matroska_segment_c::UnSelect() 的 Close() 函数,观察 matroska_segment_c *是从哪取出的,过程整理如下:
① 将 p_this 强制转换为 demux_t*型并赋值给 p_demux
② 从 p_demux 偏移 0x48 处取出 demux_sys_t*型的结构赋值给 p_sys
③ 再从 p_sys 偏移 0x6C 处取出 virtual_segment_c *型结构赋值给 p_vsegment
④ 调用 virtual_segment_c 的 CurrentSegment() 方法得到一个 matroska_segment_c *型指针
⑤ 最后调用 matroska_segment_c 的 Unselect() 方法触发漏洞
再来看看定义该函数的 vlc/modules/demux/mkv/mkv.cpp 文件
文件中定义了 Open()、Close() 和 Control() 等函数,通过函数名和注释可以推测,程序在处理 mkv 文件时,首先执行 Open() 函数。而 Open() 和 Close() 都有相同的参数——p_this,在上面的分析中,UAF 对象的指针正是从该结构体中取出的。刚进入 Open() 函数时,对象的初始化应该都没有完成,我们只需监视 p_demux 结构在上述内存偏移中的变化,就能找到分配 UAF 对象的地方。
在 IDA 找到 Open() 函数起始地址,在 Windbg 中下断点,然后找到该函数参数 p_this 这个结构的位置,在其相应偏移处设置内存访问断点,观察这些位置内存的变化。
中断到这里时,UAF 对象处于已经被创建了。
在 IDA 中查看该地址,定位在 demux_sys_t::PreloadLinked(demux_sys_t*const this) 函数中,而这个函数又只在函数 Open(vlc_object_t *) 中被引用,最后很快就能在源码中找到分配该内存的位置:
在哪里释放?
描述一个堆块的数据结构是 HEAP_ENTRY,它位于一个堆块起始处前 8 字节,而一个堆块的状态由占用状态变为空闲状态时,对于已释放的堆块,堆管理器定义了 HEAP_FREE_ENTRY 结构来描述,相比 HEAP_ENTRY,它多了 8 个字节的 LIST_ENTRY 结构用于存放空闲链表(Free List)的节点。
那么要定位到该堆块释放的位置就是十分容易了,我们只需要在 HEAP_ENTRY 处设置内存访问断点,当堆块释放时 HEAP_ENTRY 中某些标志位发生改变就能立刻断下。
按 F5 继续运行,不出所料的断在了 ntdll!RtFreeHeap 中,只需要 Step Out 几次,就能来到 libmkv_plugin.dll 模块中释放这块内存地方了。
此时,堆块状态已经由 busy 变为了 free:
同样的,结合 IDA 在源码中找到释放堆块的位置。
在哪里重用?
继续追踪堆块的变化,和上面的方法类似,就不再赘述了。
2.3 漏洞成因
在 libmkv_plugin.dll 模块的 static intOpen(vlc_object_t * p_this) 函数中,通过调用 ProloadLinked() 函数预加载所有链接段,并创建了一个 virtual_segment_c 对象实例,在该对象的构造函数中,matroska_segment_c 对象被初始化。
但后面的 FreeUnused() 函数中却又释放掉了 matroska_segment_c*指向的内存。
而在 static void Close(vlc_object_t *p_this) 函数中,p_vsegment 调用 CurrentSegment() 方法返回了一个指向 matroska_segment_c 的指针赋值给 p_segment,但该指针指向的对象 matroska_segment_c 已经被释放,p_segment 早已是一个悬空指针。
三、漏洞利用
3.1 Heap Spray部署 shellcode
一个 MKV 文件所有的音视频帧数据都存放在 Cluster 这个结构中。每个 Cluster 内可能有很多个 BlockGroup 组成,BlockGroup 内又由若干个 Block 组成。这些 Block 内就是音视频的帧数据。在 PoC 就是构造了数十个大小为 0xfff000 的 Cluster,并将喷射数据放入每个 Block 中。
当堆块进行大量分配的时候,堆地址的低位不会发生变化,仅有几个高字节是改变的,查看堆喷的内存块,可以看到每次打开 PoC 堆喷的地址总是以「018」结尾的,变化的总是前面的高位地址,所以我们只需以将每个 Block 的大小控制为 0x1000,就能够准确得到喷射的位置。以下图为例,0x22000020 所在堆块的 UserPtr 是 0x211e5020。(0x22000020 – 0x211e5020) / 0x1000 = 0xE1B(3611) 刚好能被整除,也就是 0x22000020 的内容能被我们精确控制,因为它永远是一个 Block 的起始地址。
3.2 构造 UAF 对象
MKV 的 Attachment 部分主要是用于支持在文件中附加任何类型的文件,包括图片、网页、程序等。通过构造 500 个大小与之前被释放的 matroska_segment_c 大小相同的 attachments,使释放掉的内存被重新分配。
结合源码可以看到,最后触发漏洞的函数并不是 UAF 对象的方法,而是从 UAF 对象中又取出的其他,所以在上面堆喷的数据中不仅布置了 shellcode,还有 demux_sys_t、demux_t 和 es_out_t 这些结构。最终触发漏洞的 es_out_Del 函数的指针就定义在 es_out_t 这个结构中,回过头看看堆喷数据中,该指针已被布置为了 ROP chains 的起始地址。
3.3 bypass DEP&ASLR
使用 Immunity debugger 的插件 mona,我们可以查看程序所有运行的模块。这里可以看到主程序 vlc.exe 的 Rebase 是 False, 即使有 ASLR,程序每次打开其加载地址仍然和以前一样的不会改变,所以我们可以在 vlc.exe 模块中寻找可用的 gadget,构造 ROP chains。
完整的 ROP 链如下:
不同于普通的栈溢出,此时我们能控制的只有几个寄存器的值,而当前程序的栈空间是不可控的,所以在 ROP 链的开始利用 Stack Pivot, 将栈指针 esp 指向堆喷的内存中,相当于将堆作为栈来使用。
将 edi 的值放入 edx, 剩下的三条指令没有实际作用,在栈上填充无用的数据来补偿被三个 pop 指令弹出的值。
剩下的几条 gadget 在为后面布置栈的内容做准备。
将上面寄存器中的值压入栈中。
设置 VirtualProtectStub 的参数。
转跳到 VirtualProtectStub 函数中,修改喷射内存的的执行权限。
完成后转跳到 shellcode 继续执行。
要实现漏洞的利用,只需简单的修改 PoC 中的 shellcode 部分就行,下面是经过修改的用于 MSF 的漏洞利用模块。
四、修复建议
目前官方已修复该漏洞,可从官网下载最新版本。
*本文作者:alphalab,转载请注明来自Freebuf.COM
来源:freebuf.com 2018-08-08 08:30:09 by: alphalab
请登录后发表评论
注册