原文地址:https://www.freebuf.com/vuls/172983.html
0x00 背景
2018年5月9日,360发表Blog “Analysis of CVE-2018-8174 VBScript 0day and APT actor related toOffice targeted attack” 揭露了利用“双杀”0day发起的APT攻击,其中使用的漏洞就是IE vbscript 0day:CVE-2018-8174,不久该样本就在互联网被公布。由于360的Blog并没有对漏洞原理和任意地址读写的利用方法详细介绍,且原始样本混淆严重,笔者对样本进行了简化,重点说明该漏洞的原理和如何利用该漏洞实现任意地址读写,希望帮助大家更好的理解这个漏洞利用程序。
身为漏洞学习者,分析之前明确分析目的:
1、uaf漏洞本身的成因
2、CVE-2018-8174漏洞的成因
3、猜测并验证vbs引擎如何去修复该漏洞
4、简单探测vbs虚拟机机制、更深层次的去理解vbs对象,学习更细粒度的调试技巧,学习总结该漏洞Exp构造方法。
5、学习针对vbs引擎的windbg插件扩展
6、尝试还原完整利用。
//关键词:UAF 任意地址写 攻击虚表 ZwContinue 改变上下文
0x01 漏洞原理
POC:
1 | <html lang="en"> |
创建了一个Trigger类的实例赋值给数组array_a (1),并通过Erase array_a清空array_a中的元素,触发脚本中Class_Terminate的调用,其中增加了一个array_b(0)对Trigger实例的引用(Trigger实例引用计数+1),再通过array_a (1)= 1删除array_a (1) 对Trigger实例的引用(Trigger实例引用计数-1)来平衡引用计数,Trigger实例会被释放,但是array_b(0)仍然保留了这个Trigger实例的引用,即为悬挂指针,array_b(0) = 0访问未分配内存触发漏洞。开启hpa和ust观察漏洞现场: (iexplore.exe!!!!!!!!!!!!!!!!!!!!!!!!!! not iexplorer.exe!!!!)
1 | 0:000> g |
05f4ffd0是一块已经释放的内存,windbg记录了对该地址访问的代码回溯情况。
vbscript!VbsErase即对应了脚本中的Erase,而eax正是被VBScriptClass::Release函数释放的VBScriptClass对象也就是脚本中的Trigger实例。这里看下VBScriptClass::Release的逻辑:
1 | LONG __stdcall VBScriptClass::Release(VBScriptClass *this) |
Release函数中,当 VBScriptClass::TerminateClass(v1)执行时(内部会判断该函数是否重载,若存在重载则调用用户定义的重载函数),最后调用类虚函数表中的析构函数进行最后释放处理。尽管释放了内存,但是,留下了悬挂指针array_b(0)。
Kanxue “输出全靠吼”师傅的文章”探索CVE-2018-8174中,提出了下面的问题并进行了研究:
Release函数逻辑上并没什么问题,从C/C++开发的角度看,我们较难去控制重载的析构函数做了哪些危险的操作。所以可以思考一下该漏洞应如何去修复?要弄清楚这个问题,我们得跟踪重载的Terinate函数的调用流程。
我们来看看TerminateClass函数的逆向结果:
1 | int __thiscall VBScriptClass::TerminateClass(VBScriptClass *this) |
Vbscript!CsciptEntryPoint::Call逆向结果:
1 | signed int __thiscall CScriptEntryPoint::Call(CScriptEntryPoint *this, VARIANTARG *a2, int a3, struct VAR *a4, VARIANTARG *pvargSrc, int a6) |
可以看到,整个的释放过程基本都是通过引用计数来判断。在上面作者的文章中,通过补丁分析,可以看到漏洞修复逻辑。
0x02 漏洞利用
UAF在之前的HEVD学习中,也大致学习了一下,而利用的关键,则是要使用这个悬挂指针操作内存。
下来,我们看下POC:(测试版本,其中MsgBox和IsEmpty为测试需要,可适当增减)
1 | <html lang="en"> |
先创建18个空的empty_class对象,保存在数组aa(0)-aa(17);接着创建19个class_setprop_a类对象,保存在aa(20)-aa(38),即初始化aa数组
循环7次,ab数组保存class_ter_a类对象,清除ab数组,导致重载class_ter_a::Class_Terminate函数,而在重载函数中,使用wild_ref_arr_a数组保存ab(1),即class_ter_a类对象。循环结束,即构造了7个wild_ref_arr_a悬挂指针。
setprop_a_1 = New class_setprop_a,定义对象setprop_a_1 占位之前释放的对象空间,此时setprop_a_1 和 wild_ref_arr_a(6)中各元素均指向同一块内存空间(setprop_a_1 对象)。
再循环7次,ab数组保存class_ter_b类对象,清除a数组,导致重载class_ter_b::Class_Terminate函数,而在这个重载函数中,使用wild_ref_arr_b数组保存ab(1),即class_ter_b类对象。循环结束,即同样又构造了7个wild_ref_arr_b悬挂指针。
Set setprop_a_2 = New class_setprop_a,定义对象setprop_a_2 占位之前释放的对象空间,此时setprop_a_2 和winld_ref_arr_b(6)中个元素均指向同一内存空间(setprop_a_2 对象)。
setprop_a_1对象 有个SetProp方法,将class_setprop_a类中的mem成员赋值为class_getp_a类对象getp_a,而getp_a对象默认初始化时,进入属性p,其中再次清空7个wild_ref_arr_a悬挂指针,然后构造7个fun赋值给7个wild_ref_arr_a悬挂指针,class_setprop_a和class_fun_a类具有相同偏移量的mem成员。这里class_fun_a类的mem成员是预先定义好的fake_array数组。一个特定的浮点数174088534690791e-324即“00000005 000005dd 00000000 0000200c”,该数据将返回给调用者class_setprop_a对象的mem成员
同样的,
setprop_a_2对象 也有SetProp方法,将class_setprop_a类中的mem成员赋值为class_getp_b类对象getp_b,而getp_b对象默认初始化时,进入属性p,其中再次清空7个wild_ref_arr_b悬挂指针,然后构造7个fun赋值给7个wild_ref_arr_a悬挂指针。这里class_fun_a类的mem成员是预先定义好的fake_str数组。一个特定的浮点数636598737289582e-328,该数据将返回给调用者class_setprop_a对象的mem成员
1 | fake_array = Unescape("%u0001%u0880%u0001%u0000%u0000%u0000%u0000%u0000%uffff%u7fff%u0000%u0000") |
由于7个wild_ref_arr_a悬挂指针后有1个class_setprop_a,7个wild_ref_arr_a悬挂指针是7个class_fun_a,那么最后P属性的返回值必定在class_setprop_a和class_fun_a的错位处混肴,可以证明class_setprop_a指针与class_fun_a重叠即指针地址相同,利用P的值精心构造内存结构将class_fun_a类mem类型从BSTR混肴成Array类型,从而实现任意内存读取。
同样的,7个wild_ref_arr_b悬挂指针后有1个class_setprop_a,7个wild_ref_arr_b悬挂指针是7个class_fun_a,那么最后P属性的返回值必定在class_setprop_a和class_fun_a的错位处混肴,可以证明,class_setprop_a指针与class_fun_a重叠即指针地址相同,利用P的值精心构造内存结构将class_fun_a类mem类型从BSTR混肴成Array类型,从而实现任意内存读取。
当实现混肴后,可以通过class_setprop_a->mem也就是class_fun_a->mem之泄露的地址mem,因为对于VARIANT类型类型对于的SAFEARRAY结构首部有2个USHORT,加起来长度是4,然后将mem赋值成8即string类型,这个string类型长度->cbElements的指针地址赋值为addr+4,这样再通过GetAddrValue获取这个string的长度就等于读取addr的指针指向的值,从而实现任意地址读取,最后获取vbsscript.dll等一系列dll导出函数的基址后,通过清空wild_ref_arr_a悬挂指针指针时触发vbscript!VAR::Clear导致shellcode执行
//注意调试之前需要关闭Page Heap选项
调试结果
1 | 0:013> sxe ld:Vbscript |
//看到MZ,确定,这就是模块基地址而我们的CScriptEntryPoint实在Vbscript.dll模块中,因此我们获得了后者的基地址
1 | 0:005> g |
后面通过Vbscript.dll基地址获得msvcrt.dll、kernleBase.dll、ntdll.dll模块基地址(通过导入表遍历)
1 | 0:005> g |
获取KernelBase.dll的VirtualProtect函数地址(访问导出表获取)
1 | 0:005> g |
获取Ntdll.dll的NtContinue函数(访问导出表获取)
精心构造一段ROP链,将其写入前面准备的可控数组空间。
借助特定代码时机攻击NtContinue函数劫持eip,获得shellcode执行。
总结类成员在内存分配方法如下图所示。根据所有成员按需且遵循前面提到的初始化顺序申请一块连续的内存空间,各成员VVAL1,VVAL2,VVAL3…均以一个0x10的VARIANT结构呈现,相邻成员变量之间偏移等于一个固定的内存空间0x32+前一个成员名称(UNICODE字符串)的长度。
如何获取到的CScriptEntryPoint对象的地址,可以参考Heavenml 师傅的文章,其中介绍到了函数指针向变量赋值时的特性,
下面的小poc:
1 | <SCRIPT LANGUAGE="VBScript"> |
调试数据如下:
1 | 0:005> bp vbscript!VbsIsEmpty |
在执行i=testaa的时候,VBS使用CScriptEntryPoint对象去检查这条语句是不合法的,随之产生一个错误,再CScriptEntryPoint被写入了CSession对象中,之后i = null 时,没有清除掉CScriptEntryPoint,导致CScriptEntryPoint被构造进了VARIANT结构。
通过如上的数据我们可以看出,i的类型虽然是vbNull,但是通过赋值函数地址,其实i的值域已经保存了CScriptEntryPoint的地址。于是就存在如下的关系
1 | *(*(CScriptEntryPoint+8)+16) = COleScriptObject |
//方法一 修改SafeMode标志 上帝模式
//方法二
VARIANT的类型被修改为4d,在后续执行“ memClassA.mem(address + 8) = 0”时,Vbs将检测该VARIANT结构并根据其类型进行相应的处理。在类型修改为4d之后,对该地址下访问断点即可跟踪到后续的处理逻辑。
弹出trigger_var_clear窗口,下断点进入AssignVar函数,单步调试进入Clear
可以看到,将类型修改为4d,该类型标识的是一个指向vfTable的用于安全释放处理的结构。
访问我们构造的虚表中的ZwContinue函数。
1 | //构建攻击所需虚表 |
这里调试可以看到,内存如下所示
1 | bytes = bytes &addr2str(addr)//shellcode地址续在bytes后面 |
这里判断vt类型,进入loc_6E50089c,最后调用 *(*(VARIANT结构中+8处成员)+8),即如下所示
1 | 0:005> dd esi |
此函数第一参数
1 | typedef struct _CONTEXT { |
ZwContinue函数的功能。在调用相应的异常处理回调函数时通过该函数恢复异常发生的环境信息进而进行处理。借助该函数通过传入伪造的CONTEXT参数,如上所示,直接实现了EIP的劫持。
当eip指向我们构造的VirtualProtect函数调用时,实现了非可执行内存,可读可写可执行。
1 | KERNELBASE!VirtualProtect: |
最终实现shellcode执行。