CVE-2018-8174是2018年4月份由360团队在一起APT攻击中捕获的0day,实际攻击样本中该漏洞结合CVE-2017-0199(一个关于office ole相关的逻辑漏洞)实现远程代码访问执行,360将该漏洞命名为“双杀”漏洞。该漏洞存在于VBsCript引擎中,VbsCript在释放相关对象时对引用计数问题处理不善,当我们构造特定的对象引用即有可能借助该漏洞实现释放内存空间的访问,即UAF利用。漏洞影响范围覆盖当前绝大多数系统版本,详情参见官方公告。此次调试环境我们选用win7 SP1+IE10,相关poc源码来源CVE-2018-8174:从UAF到任意地址读写 。作为漏洞研究新手,笔者参阅了各大论团上绝大多数关于该漏洞详细的分析报告,对卡巴斯基 Boris Larin写的一篇名为“Delving deep into VBScript Analysis of CVE-2018-8174 exploitation”的帖子(链接)尤为感兴趣,该帖子着重于深入讲解该漏洞的利用技巧并且对VBS虚拟机脚本解释机制进行了一定程度的刺探。所以本篇帖子中,我将尽可能地去还原Boris Larin报告中分享的内容。身为漏洞学习者,分析之前明确分析目的:1、uaf漏洞本身的成因2、CVE-2018-8174漏洞的成因3、猜测并验证vbs引擎如何去修复该漏洞4、简单探测vbs虚拟机机制、更深层次的去理解vbs对象,学习更细粒度的调试技巧,学习总结该漏洞Exp构造方法。5、学习针对vbs引擎的windbg插件扩展6、尝试还原完整利用。
UAF,即Use-After-Free,释放后使用。UAF漏洞是一种内存破坏漏洞,简单的说,漏洞的原因是使用了悬垂指针。对于UAF漏洞调试新手可以参考一下以下c代码弄懂uaf原理以及最简单的利用模型。
调试poc之前先开启浏览器页堆保护以便能够触发崩溃进行回溯。命令行工具进入windbg目录,执行下图中命令即可。双击poc,IE10出现“允许阻止的内容”的警告,打开windbg附加当前poc所在进程(我们在附加时候可以看到两个或更多的IE进程,最先启动的进程一般为IE框架进程,我们附加最后启动的那个进程就可以)。调试器g,刷新浏览器界面,单击 允许阻止的内容 ,触发crash,原因为对[eax]处数据的违规访问,使用堆操作命令(!heap -p -a 0xXXXXXXXX(此命令用于查看目标地址处的详细数据分配)),查看异常地址信息,如下图所示。从上图可以看到0x08cd7fd0是一块已经被释放的堆空间地址,在windbg窗口中记录了对该地址访问的代码段回溯情况。其实,结合下面贴出的poc代码,我们也大概能猜测出该漏洞位置应该位于VBS引擎对象释放处理过程。(这里插入一个自己调试过程中非常SB的疑问:在C/C++写码过程中这种UAF漏洞经常出现,原因是程序员自己疏忽大意留下的bug,这锅只能自己背;那么在vbs中,这种UAF同样是因为编码人员一些骚气的代码造成了,为什么要让虚拟机背锅?后来算是想明白了,vbs脚本交由虚拟机解释执行,其本身作为一种高级语言是无法拥有C/C++级别的操作粒度的,虚拟机封装了全部底层接口为用户提供全套服务,代码执行的可靠性及安全性都是虚拟机的职责。所以虽然该脚本语言经过较好封装及系列安全机制却依然留给程序员UAF的机会,这锅只能虚拟机背。既然这锅甩出去了,那么现在我们的问题就是类对象释放是怎么处理的,什么时候应该加入相关的安全校验?)
在上述脚本中通过重载Trigger类的析构函数把将要释放对象的地址保存在一个全局数组中,那么重载的析构函数是如何被调用的呢?我们定位到sub class_Terminate()即可回溯定位到类对象释放处理过程。上述截图中,自vbscipt!VbsErase函数开始即为释放处理过程,vbscript模块负责对象指针的拆除,OLEAUT32模块负责相关数据的清理。对照IDA可快速定位到TerminateClass函数。vbsccriptClass结构在内存中为0x30的一块空间,偏移4位置是对象引用计数。跟踪Trigger实例的释放过程如下。vbs引擎对类实例的处理逻辑为:当调用Release释放对象时,先将该实例对象引用计数减一,若结果为零,调用Terminate函数结束该对象(内部会判断该函数是否重载,若存在重载则调用用户定义的重载函数),最后调用类虚函数表中的析构函数进行最后释放处理。实际跟踪中,运行时模块及其他模块会因为同步、拷贝等问题多次修改计数,这一点可以忽略。比较Trigger实例释放前后的堆状态如下。调试至此,抛出一个新的问题:上述的Release函数逻辑上并没什么问题,从C/C++开发的角度看,我们较难去控制重载的析构函数做了哪些危险的操作。所以可以思考一下该漏洞应如何去修复?要弄清楚这个问题,我们得跟踪重载的Terinate函数的调用流程。结合动态调试,其执行流程如下:1、在VBScriptClass::Release函数中,处理相关引用计数问题,然后进入VBScriptClass::TerminateClass。2、VBScriptClass::TerminateClass内部通过传入的this指针解析Trigger对象结构,检测到TerminateClass函数已被用户重载。3、解析类结构中用户自定义的一些信息,封装成相应的接口类型,然后调用CScriptEntryPoint::Call函数。4、CScriptEntryPoint::Call内部初始化运行时环境及传入的参数信息,开始解释执行用户重载的TerminateClass函数。VBScriptClass::TerminateClass函数的执行逻辑:CScriptEntryPoint::Call函数的执行逻辑:在具体调试分析过程中,笔者将Boris Larin在其帖子中提到的VBscriptClass结构扩充如下(仅针对笔者当前调试环境,请读者自行调试验证):
调试至此,我虽然花了较大力气去逆向、调试相关的释放过程,但依然没有想到一个较好的思路去检测当前的UAF利用,因为vbs在对象释放处理部分都是都过对象引用计数是否为零作为释放的判断条件。既然如此,只有通过补丁去分析修复逻辑了。官方补丁下载分别跟踪补丁前后版本,发现更新补丁后,在最后crash代码“array_b(0) = 0”执行时,array_b数组中保存的Trigger对象数据已被清零。此时可以猜测该漏洞大致的修复思路:应该是在重载的Terminate函数执行时机附件进行相关的检测工作,若发现重新引用了将要被释放的Trigger对象,将阻断相关的赋值操作。我们通过修改原始代码定位到Terminate函数内部,查看当前的数组状况,然后对array_b数组下内存访问断点向前回溯。继续运行之后在VBScript!AssignVar函数触发了内存访问断点,简单逆向该函数此处的功能。该函数的作用是变体解构赋值传递,对于补丁之前的环境,该函数将按照上述注释顺利的将求值栈中保存的array_a(1)VARIANT结构拷贝到array_b(0)。重新使用补丁之后环境测试,此处的执行流程发生了改变,该函数在求值栈中获取的vt类型为0,导致函数内部进入CSession::RecordHr分支进而结束拷贝过程并清理掉Trigger对象。结合进入Terminate函数时array_a、array_b数组的内存结构,可以发现当进入Termanate函数时,求职栈中的array_a(1)->vt已经变为0(补丁之前为9、即VBS class类型)。由此可以判断在执行重载的Terminate函数之前,VBS引擎就已经做出了阻断。我们在补丁之后的环境测试何时修改了array_a(1)的vt类型,下断时机选择为执行“erase array_a”之前。如上所示,在Trriger对象的释放过程中,位于OLEAUT32.DLL模块的VariantClear函数将回去修改array_a(1)->vt。结合IDA逆向VariantClear函数内部逻辑如下。单步跟踪该函数,代码进入偏移为0x49E5的分支之后,将会清理掉求值栈的vt为0。栈回溯即可清晰的看到本报告前面逆向的析构逻辑。借助Bindiff插件可直观看到补丁代码。查看位于模块偏移为0x49E5的汇编代码正是如此。总结该漏洞的修复逻辑为:在执行erase(对象释放)代码时,vbs会检测将要被释放的对象是否发生新的引用,如有、则会在引用代码执行时将求值栈中的该对象的类型修改为0,继而0类型的变体结构中的值域指针将会被vbscript!VbsCriptClass::Release函数所释放,我们既定的变量也就无法获取被释放对象的内存访问。
简单分析参考帖子中的利用poc,这是一个简单的任意地址读取的利用模型,整个构造思路同CVE-2014-6332有较多相似之处。相关脚本及调试笔记在下面贴出。1、初始化全局数组array(40),填充堆空间碎片。2、UAF触发。先用array_a(1)记录Trigger对象地址,然后在重载的Terminate函数中,执行array_b(index)=array_a(1),用array_b(6)数组保存释放的Trigger对象地址。3、用myclass2对象占位刚被释放的Trigger对象空间。即array_b(6)和myclass2同时指向一块0x30的内存空间。4、调用myclass2.setprop(myconf)函数,传入的参数myconf是一个confusion对象,myconf在初始化时将调用默认的属性函数p。5、执行public default property get p时,先清除array_b数组,即释放前面占位的myclass2对象;然后用myclass1占位,将自定义的字符串变量FAKESAFEARRAY赋给myclass1.mem;用myclass1填充array_b数组;将一个特定的浮点数174088534690791e-324即“00000005 000005dd 00000000 0000200c”,该数据将返回给调用者myclass2对象的mem成员,并且由于myclass1.mem和myclass2.mem内存中的特定错位关系,该操作将会把myclass1.mem的类型有字符串修改为数组。6、此时myclass2.mem指向的是一个byte szBuffer[80000000]数组,可通过myclass2.mem实现任意地址访问。注意:调试之前关闭浏览器页堆保护。
总的来说,上述实现任意地址读取的重点在于最后使用myclass1占位空间时,vbs引擎并未清理掉之前myclass2的成员变量mem,而该变量地址刚好与myclass1.mem相差0xC(为什么???),以至于后续可以实现类型混淆构造出任意地址访问的数组结构。所以接下来我们重点讨论类对象成员变量的空间布局。
vbs引擎执行脚本的大致流程是先将脚本内容解析成相应的opcode字节码(该字节码可以解析成P-code(类似于二进制PE文件的反汇编)便于调试人员静态分析其执行流程),然后虚拟机中的CScriptRuntime类将会接管这些字节码并基于特定的堆栈规则开始解释执行。通过研究相应的P-code代码,可以让我们更直观地去理解vbs虚拟机的一些机制。借用Boris Larin帖子中的python脚本(kl_vbs_disasm_windbg.py),在vbscript!CScriptRuntime::RunNoEH函数执行时,加载执行该脚本即可还原出Vbs脚本在解释执行时编译成的中间代码并将其反编译成伪代码(P-code)。注意安装配置pykd环境。笔者当前调试环境需要修改原脚本信息,将脚本中的ecx改为edi,如下所示(IDA回溯一层即可发现当前环境是用edi寄存器传递一个struct var*的变量结构指针)。反汇编后的pcode代码如下所示。当前分析的重点是理解类对象成员的内存布局。我们主要分析两个过程:常规类的初始化过程和类默认属性过程property get。分析Class2解析过程如下。
confusion类的解析过程。
上述静态分析pcode的目的主要有两点:1、理解类结构解析过程中各成员的初始化顺序。2、VBS中特有的默认属性构造函数default property get function的执行逻辑。现在我们回到前面提到的一个非常重要的结构。按照上述结构可以根据类对象0x30的结构解析出对象中各成员(成员函数和成员变量)的具体位置。动态验证过程中不难发现在实际成员初始化的过程中,各成员会以VARIANT的形式按照既定的初始化顺序存储在一段连续的内存空间中。总结类成员在内存分配方法如下图所示。根据所有成员按需且遵循前面提到的初始化顺序申请一块连续的内存空间,各成员VVAL1,VVAL2,VVAL3...均以一个0x10的VARIANT结构呈现,相邻成员变量之间偏移等于一个固定的内存空间0x32+前一个成员名称(UNICODE字符串)的长度。结合上述的调试笔记来看:1、初始占位内存的地址为0x0235f4bc、即类对象第一个初始化的成员变量的VARIANT结构地址。2、用myclass2占位时,myclass2.mem的VARIANT地址= 0x0235f4bc + 0x32 +len("p") +0x32 + len("setprop") = 0x0235f530。其VARIANT结构:00000008 00000000 002aa5e4 00000000。3、用myclass1占位前,myclass1.mem的VARIANT地址= 0x0235f4bc + 0x32 +len("p0123456789") +0x32 + len("p01") = 0x0235f53c。其VARIANT结构:00000008 00000000 002aa5e4 00000000。4、调用myclass2.setprop(myconf),myconf类的default property get p将浮点数174088534690791e-324(内存结构为:00000005 000005dd 00000000 0000200c)赋给myclass2.mem。由于myclass1.mem与myclass2.mem的VARIANT结构偏移0xc,将会覆盖上一步中的“0008”为“200c”,如下图所示。此处的类型混淆正是该漏洞利用的关键,对于此类类对象占位利用的UAF漏洞,类成员变量及成员变量名称都是可以灵活控制的,理解这里的混淆原理之后,我们即可准备着手去实现针对VBS中UAF漏洞利用的通用构造方法。
上述分析中我们已经可以在内存空间构造一个任意读写的数组空间byte szBuff[80000000],类似于CVE-2014-6332中提到的、借助 Lenb函数获取指定的对象地址。在此基础上,有两种Exploit构造方法。
在获得内存任意地址读写之后、当前还需解决的一个问题是如何绕过相关的DEP保护。关于绕过保护机制我们得结合当前测试环境寻找办法,可以参考构造思路。参照上述构造思路我花了较长时间去尝试还原利用,在一些细节问题上非常痛苦,既然如此就摊牌了不装B了,尴尬(毕竟厚积薄发、不服不行!)。推荐一篇看雪论坛上的帖子,来自博主“来杯柠檬红茶”的CVE-2018-8174 “双杀”0day 从UAF到Exploit,该帖子中较完整的还原了实际攻击中的构造思路,代码简洁、可读性较高、便于分析。该部分旨在去理解其构造思路。该EXP构造流程如下:简要概括其流程如下:1、触发漏洞,通过类型混淆构造可读写的自定义数组空间。2、通过函数指针赋值时的特性获取CSEntrypoint对象地址,进而获取其虚表地址。3、通过vftable获取PE文件Vbscript.dll的加载基址。4、通过PE文件解析INT获取msvcrt.dll、kernelbase.dll、ntdll.dll模块基址,进而获取Virtualprotect、NtContinue函数地址。5、精心构造一段ROP链,将其写入前面准备的可控数组空间。6、借助特定代码时机攻击NtContinue函数劫持eip,获得shellcode执行。上述流程虽然前面的具体实现较为繁琐,但是思路非常清晰。难点是最后两步,而且对于初次调试该EXP一直会有个疑问:当前构造的shellcode是在什么时机获得执行权限的?这也是该EXP分析的重点。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
来杯柠檬红茶 666
mb_wlnwjirj 为什么我在win7 里双击poc,弹出microsoft vbscript compilation error '800a0400'这个窗口
Am0rf4tx 估计是语法错误吧,检查一下代码,代码注释使用标准的英文单引号。 不行的话,IE打开F12选择脚本调试看看哪里报的错误。
mb_wlnwjirj Break instruction exception - code 80000003 (first chance) *** ERROR: Symbol file could not be fou ...
wx_冀F vbscriptclass类的结构有没有什么办法通过调试可以得到具体结构,而不是通过其他的文章得知?