CVE-2018-15982漏洞详细分析

好久没分析漏洞了,趁着最近Flash 0day的热度再来玩一波。

0x1 提取内嵌swf

拿到的样本是个docx文档,解压后发现内嵌有一个activeX控件,根据控件的classid能够判断出是一个Flash动画相关:

activeX

分析activeX1.bin文件,在其偏移0xa08位置发现了flash动画文件的标志头:

fws

直接将该位置到文件尾部的数据截取下来,然后用010edit的swf文件模版修正一下格式去掉多余数据后可以提取出被插入的原swf文件:

swf提取

0x2 样本攻击流程分析

攻击样本是个docx文档,一开始可能会以为是利用的office漏洞,其实从上文的分析也能看出实际是利用flash漏洞,文档打开并不会自动运行,需要诱导选择播放flash漏洞后才能触发。

打开文档

仔细分析后才知道作者是在页眉处插入了上文提取出来的swf文件:

页眉

这里啰嗦一下如何在word文档中插入这类动画,首先在开发工具中点击插入其他类型的控件,然后选择“Shockwave Flash Object”类型:

插入

插入控件后右键设置一下属性,主要将其中“EmbedMovie”(内嵌影像)设置为True,并指定一下要插入的swf动画文件路径保存即可。

设置swf

然后只要受害者打开word文档后选择播放动画就会运行swf漏洞文件,之后的任务主要由该文件独立来完成,当然该文件最终的功能是执行两条CMD命令来运行外部程序(攻击时打包在同一个压缩包里):

进程

其实Flash漏洞与浏览器组合更经典,直接打开url链接就能中招:

——html

0x3 Flash调试环境配置

近些年来Flash漏洞频繁爆出,主要和其产品Flash Player有关,swf文件正是该产品使用的动画载体文件,和PE文件类似,这种文件也是将动画脚本(Action Script,一种类似java的脚本语言,目前发展到版本3,简称AS3)编译成二进制,然后供Flash Player引擎解析执行。既然是编译的,也就能够反汇编和反编译,本文直接使用一款工具“AS3 Sorcerer”对swf漏洞文件进行反编译,可以看到该swf程序没有进行代码混淆,反编译出来的代码和源码非常接近,这使得分析工作相对轻松了不少。

反编译

既然伪源码都出来了,那我们当然希望能够直接通过该代码来从源码级别分析这个漏洞,因此需要先搭建一下Flash的编译和调试环境,本小节主要介绍一下大概的安装配置步骤,已熟悉的可直接跳过到下一章节。

首先,贴出Flash开发环境的官方链接。

引导页:https://www.adobe.com/devnet/air/articles/getting_started_air_as.html

产品:https://helpx.adobe.com/cn/download-install/kb/creative-cloud-apps-download.html

Flash Player:https://helpx.adobe.com/flash-player/kb/archived-flash-player-versions.html

Air SDK下载:https://helpx.adobe.com/air/kb/archived-air-sdk-version.html

AS文档:https://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/index.html

编译Flash动画一般使用两种软件:Adobe Flash Builder或Adobe Flash Professional,本文主要介绍的是第一种。从上述产品链接中找到Flash Builder下载安装,windows平台分32位和64位版本。

安装

安装过程需要注册,具体可自行网上找按照教程,嘿嘿~

安装完成后,主要还需要搭配一下Flash Player和Air SDK的版本,让IDE可对指定版本Flash文件进行调试,也能支持编译指定版本的Flash文件。

先说Air SDK吧,下载指定版本的SDK(SDK and Compiler)后,分别在安装目录下的“sdks\4.6.0”(最新)和“\eclipse\plugins\com.adobe.flash.compiler_4.7.0.349722\AIRSDK”这两个目录解压覆盖SDK包即可。

sdk

再来说说Flash Player的版本,依然是下载指定版本的FP包,比如本漏洞环境要求版本为31.0.0.153及之前版本(太早的也不行,某些包库不支持),就下载个31.0.0.108的包为例吧,一般其中会包含Debug和Release两种版本的环境包,并且每种版本还分位多种运行环境的程序,我们进行编译和调试主要使用Debug中的ax和sa两种程序,将之放置到Flash Builder安装目录的对应位置如下,其中ax需要运行安装:

fp

sa版本其实本文用不到,其路径设置也可以直接在IDE中指定:

sa

还有IDE也可以设置Flash项目调试使用的浏览器:

浏览器

一切配置好后就可以开始进行调试分析了,以下为IDE调试配合OD观察对象内存,从IDE中可以看到的对象地址为该对象的实际内存地址加1:

_debug

0x4 漏洞成因分析

开始进入本文的主角——Flash漏洞的分析,下文展示代码是对反编译出来的伪源码分析理解后重新命名了变量、函数等符号的结果,也是为增加可读性方便理解。

首先分配和释放多次内存,为后续控制内存分配做好准备,此为漏洞利用的常用步骤。

准备

其中触发垃圾回收的函数“gc_clean()”,内部实现如下,实现原理是连续两次connect同一个连接将引发异常,自动运行垃圾回收器,将未被引用的空闲内存进行回收。

gc

接着就到达引发本次漏洞的关键代码了,主要发生对象是Metadata。这种对象有点类似python中的字典,用于存储一系列的“键——值”对。漏洞则发生在该对象增加键值的方法函数“SetObject”中,该函数在存储“键”对象的时候,没有对该对象进行所谓的“DRCWB”,这里可以先简单理解为没有给“键”对象加个引用计数(如下代码中“键”对象引用计数为0),导致攻击者可以利用此点通过执行“gc_clean()”来强制回收“键”对象的内存,从而获得垂悬指针,进而可以进行“UAF”漏洞利用。

漏洞

具体我们还是先观察一下垂悬指针的形成过程吧,在IDE中设置断点运行到上图map对象存储完键值后、执行“gc_clean()”之前,然后OD附加浏览器进程,通过IDE中的map对象地址减一(如0x7913191-1)来观察OD内存窗口中的map对象,可以看到“键”对象(String)被依次存进一个数组中:

然而执行“gc_clean()”之后,key数组中保存的String指针没有动,但是指针对应的String对象却被释放了,于是key数组就保留了指向原string对象(如第20个)的垂悬指针,将能够越权访问到其对应地址后续被重新分配给的其他对象结构:

string_free

调试过程中从IDE观察keySet数组也可以观察到漏洞触发前后,垂悬指针(从第10项开始是因为前10个单数字String对象是Flash维护,引用计数不为0不会被释放)指向内存重新分配给程序其他对象后读取到了“异常”数据:

UAF

回过头再看看漏洞函数“SetObject”的实现过程吧,由于每次添加新项都会往上述key数组中依次写入一个新key的对象地址,故只需在IDE中单步跟踪while循环里的“SetObject”时候,在OD中对key数组区域下个内存写入断点就能定位到“SetObject”的native实现函数,跟踪过程没有发现针对新key对象的引用计数之类的保护操作,认为是造成这次漏洞的最主要原因!-_-!。

_save_string_to_key_array

0x5 漏洞利用分析

经过上述的分析,知道漏洞的根本原因是Metadata对象的SetObject方法在存储新键时没有对新“键”对象做相关的引用计数保护,导致攻击者可以通过强制GC获得对新“键”对象的垂悬指针,从而可以接下去进行UAF漏洞利用。本节对利用代码进行分析,漏洞利用的目标是通过实现任意内存读写来劫持程序最终得以执行CMD命令。

从强制GC获得垂悬指针后继续吧,攻击者接着马上就分配多个对象来抢占刚回收的内存地址,分配的对象我们称之为Mark(标记类),将其保存在一个vector数组里。

_alloc_mark

此时就会出现上节IDE中keySet数组里观察到的“异常”数据了,其实从第“10”项开始的垂悬指针指向的新对象就是这里的Mark对象了,该对象与原String对象正好重合(对象大小一样,均为0x18,@银雁冰,多谢纠正),大致内存结构可参考下图

所以此时通过keySet的垂悬指针来访问对象,就出现了所谓的类型混淆,访问String对象的len字段其实真正拿到的值是混淆后Mark对象的tag1字段值,也就是24。所以攻击者利用这点来判断混淆后的内存状态,结合指针特征来判定当前系统环境的机器位数,从而在下面能够选择进入32位或者64位两种系统的漏洞利用流程:

_check_bit

本文主要介绍32位的利用代码,64位的利用其实原理一样,实现上大同小异,主要在于多用一个自定义类(本文命名为Qword)来处理64位数据。先看看整体利用的大致框架吧,如下图所示,32位和64位的实现用的是同一种套路。

framework

接着开始看看漏洞利用的姿势,小目标是实现内存任意读写。实现小目标的第一步依然是类型混淆,这里攻击者又设计了另一个自定义类Mix(混淆类)来和上述的自定义类Mark进行混淆,然后互相配合之下可以达成目的。自定义混淆类Mix同样包含两个成员字段,故对象size和Mark对象相同可以进行二次UAF,进而再次造成类型混淆。

_mix

混淆之后,这两种对象再次重合,对象的两个字段偏移一致。大致内存结构如下图所示,注意此时若通过Mark对象的tag1成员来读取Mix的对应值,读出来的将是一个对象地址(+1),所以下面将会发现利用代码中直接将某些对象直接赋值给Mix对象的ctrl_byte_array成员,然后直接读取Mark对象的tag1值就能得到那些对象的内存地址了,最爱这种小机巧。

这对基友还真是天作之合,不过为了更好地配合,最好还是做一下“同步”操作,搞清楚谁是谁,即需要先确定一下混淆后r_mark数组中第几项是Mix对象,以及该项对应r_mix数组中的第几项。

_high_mix_mark

一旦对应位置都找到,就可以实现我们的小目标了。不过为了理解小目标实现的脑洞,最好还是要观察一下Mix对象中over_data的内存格式,然后就能理解代码中为何要对参数addr(要读取/写入的内存地址)减去16了:

_read_write_func

OK,小目标达成,目前就拥有了稳定的任意内存读写功能,距离最终目标执行shellcode也就不远了。据说后面的利用方式是出自Hacking Team的Flash 0day,然而搜索姿势不行并没有找到参考文章,所以还是老老实实自己动手吧。首先,定位相关的模块地址和API函数地址,定位方式是通过读取对象(ctrl_byte_array)虚表指针来获得Flash ocx解析模块的地址,进而解析PE导入表来获取kernel32的基址,最后再解析其导出表来获得VirtualProtect函数的地址,解析过程就不贴了。

find_kernel32

搜索VirtualProtect函数的地址是为了后面调用它来获得一块可执行的内存来存储shellcode,那么就先来找找shellcode吧。从反编译的伪源码中找一下就会发现看不到长得像shellcode的地方,不过解析下原swf文件却能够看到确实存在两段shellcode二进制数据:

_defineBinarayData

但是这些shellcode如何在AS中使用呢,其实很简单,为此作者定义两个类分别叫Shellcode32和Shellcode64,关键是这两个类均继承自“ByteArrayAsset”类,并绑定了外部二进制文件“shellcodeXX.bin”,然后只需要new一下该对象就能直接获得Shellcode数据了。

_bytearrayasset

既然必要的数据都准备好了,那就着手开始劫持程序,先获得一块可写可执行的内存,然后再写入shellcode数据最后执行。这一步的代码比较长,核心还是在于对程序eip的劫持,本利用代码采用的还是劫持对象指针。具体采用什么对象的什么指针呢,还是先来看看Main类定义的一个方法吧:

exec_shellcode

在Flash AS3中,函数也是一种对象,故攻击者先定义了这么一个特别的空函数,然后准备对其调用过程进行劫持。首先获得该函数对象的内存地址,方法和前述技巧一致:

get_obj_addr_exec_shellcode

接着就是根据该对象地址去定位要劫持的位置了,目标位置根据环境版本的不同攻击者做了一些判断调整,主要是一处偏移有所偏差:

check_offset

确定了此处偏移,就可以定位待劫持的目标地址了,定位完后先保存一下原函数的地址等数据待后面恢复时使用。

localtion——hijack

可能是为了不影响原有调用过程的数据,并且在劫持过程中能够顺利调用到相关的子函数地址,攻击者直接将劫持对象的一些函数指针表拷贝到自己控制的一块内存区域“addr_buf_vec_array”:

copy_func_table

这块“addr_buf_vec_array”内存其实是攻击者为了最终存储shellcode准备的,也就是即将被VirtualProtect更改可执行属性的内存位置,其位置是处于一个整型vec数组的缓冲区域,该缓冲区就是为了存放劫持对象的函数指针表(开头)和shellcode(后面):

r_vec

Ok,万事俱备,来看看如何劫持eip调用VirtualProtect吧,后面真正执行shellcode时也是使用该方式来进行劫持调用的,即先将要劫持的目标eip写入指定对象结构地址(+28),然后将其引用地址再写入上述存储劫持目标的地址中即可,当然如果需要带调用参数还需另外修改其他位置的内容:

set_eip

单看这静态的代码可能还是比较难理解攻击者为什么这样写,没关系,只需在OD跟踪一下劫持过程的native函数便能揭晓,直接先在IDE中执行exec_shellcode.call.apply时下个断点,然后在OD中对VirtualProtect的保存地址“addr_buf_vec_array + 8 + 128 + 28”(参见上图)下个内存访问断点,然后执行后就能定位到native函数,仔细分析一下该函数就能明白攻击者定位劫持对象的偏移以及劫持时写入的这些地址原来都是基于此函数得出来的:

__call_apply

上图函数包括前劫持函数带的前两个参数的来源,而VirtualProtect第三个参数(NewProtect即0x40)则是从上层调用(up_call)而来,从下图可见其将传给exec_shellcode函数第二个参数的array对象的长度字段0x41(65)进行了减1操作,随后传给call.apply当作第三个参数使用了。至于更深层次的探究,这里就不继续了。

_arg3

到这里基本已经能够理解攻击者劫持程序的过程了,最后就剩拷贝shellcode执行,贴一下拷贝过程结束对漏洞利用的分析吧。

copy_shellcode

0x6 漏洞补丁分析

分析完漏洞成因和漏洞利用,再来看看最新版本Flash Player修补这个漏洞的情况。上述Flash Player的链接里没找到最新版(32.0.0.101)的归档包,要安装Debug版的最新版Flash Player ActiveX可从“https://www.flash.cn/support/debug-downloads”下载在线安装包,安装前先卸载老版本。

还是先在最新调试环境下观察下map对象存储的键数据,发现原来在+0x20处位置的key数组指针不再存在,转而保存到+0x1c处的一个Array对象中,键数组指针列表的每一项都被“加2”存放,然后再次强制GC后这些指针所对应的String对象却不会再被释放了。

_patch_map_obj

再次跟踪一下SetObject的实现过程,发现不再是直接保存键对象指针,而是在保存前还会对该指针进行各种检查:

check_addr

往回跟一层可见在计算键指针保存到键数组中的具体位置,其中第i项(EDI)加2后再乘以4的计算方式可解释键数组指针列表为啥从+8处为第一项。

patch_save_array_pos

再往回跟两层,还能看到获取第i项键地址后进行了加2编码,然后还接着对该地址进行了一些未知的操作,可见新版Flash Player针对该漏洞进行了大量的修补,单是对要保存的键对象就至少进行了两轮保护,所以不会再出现键对象指针被悬空的现象。

_patch_op_addr

0x7 总结

回想这个漏洞发生的原因,字典对象的“键”在保存过于简单,没有考虑到对键对象进行保护,导致可以被攻击者加以利用造成悬空指针的出现。看样子作为开发者需要注意的安全编程还真不少,此事项就属于比较容易被忽略的一种吧,Mark!然后是漏洞利用部分,也是令人叹服,精心构造出稳定的任意内存读写功能后,又逆向了函数对象的调用过程来实现比较通用和稳定的调用劫持,比之前自己玩CVE-2015-0311时用ROP链来运行shellcode的方式高级了很多。最后再说补丁,感觉是永远打不完的~

 

0x8 参考链接

  1. ““毒针”行动 – 针对“俄罗斯总统办所属医疗机构”发起的0day攻击”:http://blogs.360.cn/post/PoisonNeedles_CVE-2018-15982.html
  2. “Flash 0day + Hacking Team远控:利用最新Flash 0day漏洞的攻击活动与关联分析”:https://www.anquanke.com/post/id/167334
(完)