前言
2018年6月1号,360高级威胁应对团队捕获到一个在野flash 0day。上周,国外分析团队Unit 42公布了关于该次行动的进一步细节。随后,卡巴斯基在twitter指出此次攻击背后的APT团伙是FruityArmor APT。
在这篇博客中,我们将披露该漏洞利用的进一步细节。
漏洞利用
原始样本需要与云端交互触发,存在诸多不便,所以我们花了一些时间完整逆向了整套利用代码,以下分析中出现的代码片段为均为逆向后的代码。原始利用支持xp/win7/win8/win8.1/win10 x86/x64全平台。以下分析环境为windows 7 sp1 x86 + Flash 29.0.0.171。64位下的利用过程会在最后一小节简要提及。
1. 通过栈越界读写实现类型混淆
原样本中首先定义两个很相似的类class_5和class_7,并且class_7的第一个成员变量是一个class_5对象指针,如下:
紧接着调用replace方法尝试触发漏洞,可以看到在replace函数内定义了一个class_5对象和一个class_7对象,并将这两个对象作为参数交替传入trigger_vul函数()。
从下图可以看到,trigger_vul方法一共有256个参数,分别为交替出现的128个class_5对象和128个class_7对象。这是为了后面的类型混淆做准备。
在trigger_vul内部,首先创建一个class_6对象用于触发漏洞,
在class_6类内调用li(123456)触发RangeError,通过修改ByteCode后可以导致进入如下的catch逻辑(伪代码),可以看到在catch内越界交换了两个栈上的变量(local_448和local_449)。而攻击者通过精确布控jit栈,导致交换的两个栈变量恰好为先前压入的一个cls5对象指针和一个cls7对象指针。从而实现了类型混淆。
成功交换指针后,将修改完后的栈上数据(256个参数)分别回赋给一个cls5_vec对象和一个cls7_vec对象,最后返回cls5_vec对象,这时cls5_vec里面存在一个cls7对象,其余均为为cls5。
在windbg中看到上述过程如下:
根据着色分布可以看到栈上的一个cls5对象指针和一个cls7指针在漏洞触发后发生了互换:
返回到trigger_vul之后,遍历cls5_vec中的成员,找出m_p1不为0x11111111的cls_5对象,此对象即为被混淆的cls_7。随后保存有问题的“cls_5”对象和cls_7对象到静态成员。
trigger_vul返回之后,通过_cls5.m_p6成员是否为0来确定当前环境为x86还是x64,并借助两个混淆的对象(cls5和cls7)去初始化一个class_8对象,该对象用于实现任意地址读写。
2. 任意地址读写
class_8类是攻击者构造的一个工具类,用来实现任意地址读写,并在此基础上实现了x86/x64下的一系列读写功能函数。我们重点来看一下readDWORD32和writeDWORD32的实现。
由于cls7的第一个成员(var_114)是一个cls5对象,所以在cls5被混淆成cls7后,表面上对cls5.m_p1的修改实质是对cls7.var_114的修改。现在假设我们有一个需要读取的32位地址addr,只需要把addr-0x10的值赋值给cls5.m_p1,这样相当于把cls7.var_114设为了addr-0x10。然后去读取cls7.var_114.m_p1, 此语句会将cls7.var_114.m_p1处的值当做一个class_5对象,并读取它的第一个成员变量,也即将addr-0x10当作一个class_5对象,并读取addr-0x10+0x10处的四个字节。
下图解释了为什么32位下需要addr-0x10,由于继承关系,每一个as3对象的前16个字节结构是固定的(其中,“pvtbl”是C++虚表指针,“composite”、“ vtable”和“delegate”成员可以参考avmplus源码中的ScriptObject实现),一个类对象的第一个成员变量位于对象首地址+0x10处(64位下类推为addr-0x20):
图:从内存来看,混淆后,对cls5的操作实际上影响了cls7对应内存处的值,随后可以通过访问cls7.var_114.m_p1去读取任意addr处的值。
writeDWORD32原理和readDWORD32类似,此处不再赘述。
在clsss_8类中,攻击者在上述两个函数的基础上实现了一系列功能函数,全部如下:
3. 定位ByteArray相关成员偏移
虽然攻击者并未借助ByteArray来实现任意地址读写,但为方便利用编写,他必须知道当前Flash版本中ByteArray相关成员的内存偏移。为此,攻击者定义了一个class_15类,用来借助任意地址写实现对特定成员的偏移搜索并,保存。以供后面使用。
setOffset32的部分逻辑:
以下class_15的成员用来保存动态搜索到的内存偏移。
4. 1st shellcode
找到相关偏移后,攻击者立即开始构造shellcode并执行。 1阶段的shellcode为内置,但有7个DWORD32字段需要动态填充。而2阶段的shellcode通过一个ByteArray动态传入,即上面setOffset函数中的_bArr成员。由于并未得到攻击者的2阶段shellcode,我们使用的2阶段shellcode来自HackingTeam泄漏的代码,功能为弹一个计算器。
攻击者先借助ByteArray(ba)存储了一个1阶段shellcode模板,反汇编后如下,其中紫色区域是需要动态填充的字段,这些字段代表的含义如注释所示:
然后初始化一个新的ByteArray对象(ba2),将其的array区域的前16字节初始化如下:
5. Bypass ROP
为了构造ROP,攻击者专门定义了一个辅助类class_25,在里面实现了如下功能函数:
攻击者先借助flash模块的IAT找到User32.dll的GetDC地址,再借助User32.dll的IAT找到ntdll.dll的RtlUnWind地址,
随后从ntdll.dll的EAT的AddressOfFunctions数组中找到NtProtectVirtualMemory和NtPrivilegedServiceAuditAlarm的函数偏移并计算得到对应的函数地址。
攻击者这里的思路是取出NtProtectVirtualMemory的SSDT索引,和NtPrivilegedServiceAuditAlarm+0x5的地址,供后面使用。
后面会通过call NtPrivilegedServiceAuditAlarm+0x5并传入NtProtectVirtualMemory的SSDT索引的方式来Bypass ROP的检测。由于ROP检测并未Hook NtPrivilegedServiceAuditAlarm作为关键函数,所以并不会进入ROP检测逻辑中,因此绕过了ROP的所有检测。
随后搜索以下的ROP部件并保存,供后面使用
随后将上述信息返回给上层调用者:
随后部分值被填充到1st shellcode的前5个pattern。
6. Bypass CFG
这个样本在32位下通过覆盖jit栈的方式来绕过CFG,攻击者首先定义了两个相似的类class_26和class_27。两者都定义了一个方法叫做method_87。不同之处在于class_26.method_87只接受两个参数,而class_27.method_87接受256个参数,并会将传入的参数全部保存并返回给调用者。
攻击者首先初始化了一个class_26对象cls26和一个class_27对象cls27。然后借助任意地址读写能力将cls26.method_87的jit地址替换为cls26.method_87的jit地址,
然后第二次调用cls26.method_87,此时实际上调用的是cls27.method_87,由于cls26.method_87自身只会传入2个参数,导致泄漏了大量jit栈上的数据,攻击者随后利用泄漏的数据找到一个jit参数栈的地址,并第二次调用cls27.method_87,用以覆盖jit栈的一个返回地址,从而在对应的函数返回时控制eip。
在windbg中观察一下上述过程:
根据这篇文章,我们可以知道cls26对象的+0x08处是一个vTable对象指针,而vTable对象的+0x48处是一个MethodEnv对象指针,MethodEnv对象内又包含自身的_implGPR函数指针和一个MethodInfo对象指针,MethodInfo对象内也包含一份_implGPR函数指针,这些结构体间在内存中的寻址关系如下所示:
所以replace_jit_addr函数本质上是用cls27.method_87的jit地址替换了cls26.method_87的jit地址。但cls26.method_87的jit地址在好几个地方都有存储(如上图就有MethodEnv._implGPR和MethodEnv.MethodInfo._implGPR两个地方存储着cls26.method_87的地址),我们如何确定要覆盖的是哪一个地方?
这得从class_21$/executeShellcodeWithCfg32函数的jit汇编代码中寻找答案。如下是executeShellcodeWithCfg32的部分汇编代码。代码中红框圈出的两句代码清楚地指明了cls26.method_27函数第二次调用时的函数指针寻址过程,很明显,这里用的是MethodEnv._implGPR。
至于cls27.method_27的地址,任意找一个存储其jit地址的地方读取即可(这里也可以采用HackingTeam的代码中读取jit函数指针的方法,如下)。所以一共可以有三种方式。Exp代码中的两种,加上HackingTeam中的一种。但写入地址是唯一的。通过上述做法,成功实现了对jit地址的偷天换日。
在2016年的一篇总结Flash利用的文献中,作者曾介绍过用覆写MethodInfo._implGPR的方式来劫持eip。两种方式十分类似,但并不完全相同。
在第二次调用cls27.method_87时,攻击者传入的参数如下,其中的retn为上面寻找到的gadget03(addr_of_ret)。其余重要参数均在注释中进行说明。由于ba2_array的前12个字节分别为:第一阶段的shellcode地址(ba_array),0x1000,0。这些恰好对应NtProtectVirtualMemory所需的前3个参数。
我们具体看一下cls27.method_87内部的逻辑。可以看到若第一参数为0x85868788,则递归调用自身20次,这是为了布局jit栈,方便后面覆盖eip:
在最后一次调用中,cls27.method_87会借助前面泄漏的jit栈地址来找到将要覆盖的eip所在的栈地址pRetAddr,并保存原始返回地址。
随后,为了在触发漏洞后不造成crash,攻击者又传入原始返回地址第二次修改1st shellcode,将最后两个pattern处填写为正确的值,保证shellcode执行完后可以正常返回:
通过覆盖栈上的eip劫持控制流,成功避开了CFG的检测,从而Bypass CFG。
调试发现被覆盖的eip为jit栈上cls27.method_87递归调用自身20次中某次的返回地址
最后,在递归调用某次返回的过程中,eip被成功劫持至第一阶段的ROP,随后的整个过程在windbg中观察如下:
2nd shellcode执行完毕后,会继续从class_27.method的递归调用中返回。然后返回到flash的正常逻辑,此过程中不会造成crash和卡顿,整个利用方式非常稳定。
7. 64位下的利用分析
原利用代码也支持64位环境。64位下的漏洞触发代码和32位下并没有什么不同,只在Bypass CFG部分有所差异。原利用代码中出现了两种Bypass CFG的方法,下面分别介绍。
如果当前64位环境下的ntdll.dll中可以找到如下gadget,则走分支1。从注释的汇编代码中可以清楚地看到这部分gadget的作用:弹出栈顶部的4个值给x64调用约定下作为前4个参数的寄存器并返回。
随后找到kerner32!VirtualProtect函数地址,并和传入的shellcode一起传入下图所示的函数,在curruptJitStack函数借助jit地址覆盖去替换返回地址(此过程和32位下非常相似),并在jit函数返回时利用rop将shellcode所在地址设置为可执行。随后调用replaceJitApply64去调用执行shellcode。replaceJitApply64函数内借助了HackingTeam之前泄漏的方法去Bypass CFG,即覆盖FunctionObject.Apply()方法的虚表地址。其中replaceJitApply64方法会在分支2中分析。
假如在当前进程的ntdll.dll没有找到分支1所需的gadget,则进入分支2,分支2采用了覆盖FunctionObject.Apply()方法的虚表地址的方法。
我们来详细看一下replaceJitApply64,如果熟悉之前HackingTeam的利用代码,则很容易理解下述代码:
分支2会两次调用replaceJitApply64函数,第一次的目的是调用kernel32!VirtualProtect函数去设置shellcode的执行权限。函数内首先定义一个ByteArray对象ba,然后将shellcode放置在ba.array的首部。
随后将找到ExecMgr对象的虚表,将其虚表前的8个字节及虚表的前0xE4/8个虚函数地址拷贝到ba.array的len(shellcode)起始处(伪造虚表)。
随后覆盖伪造的ExecMgr虚表+0x30处的8个字节,这正是apply方法对应的虚函数地址。随后覆写ExecMgr首部的虚表指针,设置相关寄存器的值和相关对象偏移处的值,以构造VirtualProtect函数所需的4个参数,随后调用apply方法以调用VirtualProtect,调用完将之前覆盖的值都恢复原来的值,从而不造成crash。对这部分细节的详细描述可以参考这篇博客。下图的注释也写得比较清楚。
调用完后返回到上级函数,随后再次调用replaceJitApply64方法,用shellcode+0x8的地址去替换apply方法对应的虚函数地址。从而执行shellcode。执行完shellcode后回到Flash代码,整个过程也不会造成crash。
总结
CVE-2018-5002是一个位于avm2解释器内的非常严重的漏洞,漏洞质量高,影响范围极为广泛。从原始flash的编译日志可以观察到,整套利用框架早在2018.2.7日就已经完成编译。该套利用代码通用性强,稳定性好,整体水平较高。
References
https://recon.cx/2012/schedule/attachments/43_Inside_AVM_REcon2012.pdf