Heim > Artikel > Web-Frontend > 从零开始学Fuzzing系列:带领nduja突破Grinder的壁垒_html/css_WEB-ITnose
文章作者:walkerfuz
站在巨人的肩膀上,才能看的更远,开发项目亦是如此。
四年前开源的Grinder项目,和借助于它运行的nduja,着实让浏览器漏洞挖掘飞入了寻常百姓家。但随着时间的考验,Grinder也遇到了让人爱恨交加的尴尬:明明产生了Crash,可就是无法重现。有多少人和我一样,从初识Grinder的激动,到分离时的落寞,也见证了一代怀揣梦想的挖洞人的足迹。
在现有项目的基础上,对其进行改进,乃是一种进步,于是出现了Morph项目。
Morph起初的定位就是解决Grinder架构中存在的本质问题:样本无法稳定重现,所以才有了前面《 浏览器挖掘框架Morph诞生记 》中讲述的“静态随机数组”的尝试,但随之带来的并发症却是:样本难以精简。
本文正是为解决这个问题而产生的,笔者采用一种Grinder log静态化的方式,将原本Grinder中采用DLL注入截获log语句的方式改进为提前生成静态精简样本的方式,从根本上解决样本无法稳定重现和难以精简两大难题,为浏览器漏洞挖掘工作提供新的思路。
前面《 浏览器挖掘框架Morph诞生记 》中介绍过一种“静态随机数组”方法,用来解决Grinder log记录容易出现样本无法稳定重现的问题。笔者在开发出Morph v0.2.5之后进行了大规模部署测试,也得到了一些可以稳定重现的Crash结果,但拿到样本想进一步分析时,却遇到了难题。得到的Crash样本是如下形式的HTML文档:
<html><head><script type='text/javascript'>var mor_array = [675, 142, 861, 226, 112, 157, 667, ...... 147, 368, 10, 1];//元素个数有可能上万个var mor_index = 0 ;// Pick a random number between 0 and Xfunction rand( x ){ index = (mor_index++) % (mor_array.length); return mor_array[index] % x;}function R(mod) { return rand(10000) % mod;}......function tweakattributes(elem,i){ for( var p in elem){//这里的循环要依次调用上面的mor_array数组中的元素 try { r=rand_item(interesting_vals); elem.setAttribute(p,r); } catch (exception) {} }}......function buildElementsTree(){ elementTree=[]; for (k=0;k<200;k++){//这里的循环要依次调用上面的mor_array数组中的元素 r=rand_item( elements ); elementTree[k]=document.createElement(r); elementTree[k].id="el"+k; rb=R(document.all.length); document.all[rb].appendChild(elementTree[k]); tweakattributes(elementTree[k],k); } }}function morph_fuzz(){ buildElementsTree(); ...... }</script></head><body onload="morph_fuzz()"></body></html>
要想在上述样本中定位到是哪个js语句最终导致crash,必须依次读取静态数组,一步步调试执行buildElementsTree和tweakattributes函数中的for循环,拆解得到相关js语句。而且该语句 有可能 与之前循环的某个语句还有联系,必须将两者或更多的语句都定位出来才能得到完整的POC样本。
显而易见,如此分析起来,是极其繁琐的。用一句话形容这些样本: 食之无味,弃之可惜 。
那我们到底需要什么格式的样本呢?搞过浏览器漏洞分析的人员都知道,平常分析的POC都是这样的:
<html><head><script> function exploit(){ var e0 = null; var e1 = null; var e2 = null; try { e0 = document.getElementById("a"); e1 = document.createElement("div"); e2 = document.createElement("q"); e1.applyElement(e2); e1.appendChild(document.createElement('button')); e1.applyElement(e0); e2.innerHTML = ""; e2.appendChild(document.createElement('body')); }catch(e){ } CollectGarbage(); } </script> </head><body onload="exploit()"><form id="a"></form></body></html>
稍微跟踪调试即可确定crash产生的原因。另外,用Grinder成功重现出Crash样本的童鞋也知道,得到的POC样本通常都是这种格式:
<html><body></body><script>var createdObjects={}var c = document.createElement("CANVAS")c.width = 1000c.height = 1000document.body.appendChild(c)var img = new Image()img.src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAANlBMVEX///+6v8a2u8PKztPAxcu7wMf4+fm0ucH =="try{ ctx = c.getContext("2d")} catch(e){}try{ HTML0= document.createElement("MENU")} catch(e){}try{ createdObjects["HTML0"]=HTML0} catch(e){}try{ document.body.appendChild(HTML0)} catch(e){}try{ P0= new Path2D()} catch(e){}try{ createdObjects["P0"]=P0} catch(e){}try{ ctx.height="36191.05884594913180334528604"} catch(e){}try{ delete createdObjects['HTML0']} catch(e){}try{ ctx.translate(-0.9872812044341117,0)} catch(e){}try{ ctx.getLineDash()} catch(e){}try{ CollectGarbage()} catch(e){}try{ ctx.scale(-1435178373,-58)} catch(e){}try{window.location.reload(true);}catch(e){}</script></html>
上述样本采用二分法即可确定是哪些语句造成了浏览器崩溃。总之, 像上述两种没有循环,“一条大路走到底”格式的样本,才是我们期望得到的 。
nduja本质上是一种Fuzzing策略,它制定了一系列新建、修改、删除DOM元素的规则。通过生成随机数的方式,产生不同的样本来测试浏览器是否产生异常。而Grinder则是提供了启动并监控浏览器进程、打开或记录异常样本等功能,将nduja承载起来的Fuzzing平台。
Grinder log的精髓在于,它能够通过Dll注入的方式,将在时间上顺序执行的js语句记录下来,但由于涉及到EventListener函数的调用,因此testcase.py脚本的自动化重现,在某些时候是不可行的。
既然上述Grinder log动态化记录js语句的方式是不可靠的,那如果按照log记录的模式,以同样的逻辑,提前生成静态样本再传递给浏览器进程加载测试,是否会解决样本难以精简的问题呢?
这种思路,我们称之为Grinder log静态化。简单来说,之前在grinder平台中使用的nduja样本,增加log语句后是这样的:
<html><head><script type='text/javascript'>......function tweakattributes(elem,i){ for( var p in elem){ try { r=rand_item(interesting_vals); logger.log("elementTree["+i+"]."+p+"="+r+";", "ndujaL", 1); elem.setAttribute(p,r); }catch(exception) {} }}......function buildElementsTree(){ elementTree=[]; for (k=0;k<200;k++){ r=rand_item( elements ); logger.log("elementTree["+k+"]=document.createElement('"+r+"');","ndujaL",1); elementTree[k]=document.createElement(r); logger.log("elementTree["+k+"].id='el"+k+"';","ndujaL",1); elementTree[k].id="el"+k; rb=R(document.all.length); logger.log( "document.all["+rb+"].appendChild(elementTree["+k+"]);", "ndujaL", 1 ); document.all[rb].appendChild(elementTree[k]); tweakattributes(elementTree[k],k); } }}function morph_fuzz(){ buildElementsTree(); ...... }</script></head><body onload="morph_fuzz()"></body></html>
只有在动态执行过程中,才能通过logger.log语句将后续执行的js语句记录下来,最后通过testcase.py将其恢复的html样本(补充完整后)类似于以下形式:
<html><head><script type='text/javascript'>elementTree=[];......try{elementTree[3]=document.createElement("button");}catch(exception) {}try{ elementTree[3].id="el"+"3"; }catch(exception) {}try{ document.all[5].appendChild(elementTree[k]); }catch(exception) {}......elementTree[3].setAttribute ("edition","first");elementTree[3].setAttribute("title", 0x41414141414141);......</script></head></html>
Grinder log静态化就是,提前采用编程语言(Python)静态生成上述逻辑的样本:
class JsGenCls(): # adjust def trys(self, case): return "try{%s}catch(e){}\n" % case # Random def randb(self): return r.choice(["true", "false"]) def create_element_append_child(self): ret = "" ret += self.trys("%s = document.createElement('%s');" % (self.newElem(), self.randTag())) ret += self.trys("%s.id = '%s';" % (self.newElem(), self.newElem())) ret += self.trys("%s.appendChild(%s);" % (self.randDoc(), self.newElem())) self.elements.append(self.newElem()) return ret def tweak_attributes(self, element): ret = "" for attribute in g.HTMLAttributes: ret += self.trys("%s.setAttribute('%s',%s);" % (element, attribute, self.randInteresting())) return ret def fuzz_nduja(self): ret = "" # 1. build element treee for nduja # create element and append child for i in range(g.MAX_ELEM): ret += self.create_element_append_child() # tweak attributes ret += self.tweak_attributes(self.lastElem()) # boom return ret def generate(self): script = self.fuzz_nduja() script += self.window_reload() script = self.gen_tags("script", script) head = "<title>nduja_fuzzer</title>\n" body = self.gen_tags("body", script) return head + body
只要调用JsGenCls.generate函数即可生成一段Html文档的字符串,生成最终效果如下:
<title>nduja_fuzzer</title><body><script>try{Element0 = document.createElement('body');}catch(e){}try{Element0.id = 'Element0';}catch(e){}try{document.all[2].appendChild(Element0);}catch(e){}try{Element0.addEventListener('chargingchange', func0, false);}catch(e){}try{Element0.setAttribute('accesskey',true);}catch(e){}try{Element0.setAttribute('action','no');}catch(e){}try{Element0.setAttribute('aria-checked','controls');}catch(e){}try{Element0.setAttribute('aria-colcount',-7e6);}catch(e){}try{Element0.setAttribute('aria-colspan',0x80000000);}catch(e){}try{Element0.setAttribute('aria-flowto','ab');}catch(e){}try{Element0.ownerDocument();}catch(e){}try{Element0.document='ltr';}catch(e){}try{Element0.cloneNode='controls';}catch(e){}try{Element0.open=null;}catch(e){}try{Element0.close(-7e6);}catch(e){}</script></body>
将 上述 生成的精简样本传递给浏览器进程,让其加载测试即可。可以看出, Grinder log静态化的关键在于,将nduja的逻辑采用编程语言静态生成出来 。那nduja的逻辑是什么样的呢?
nduja的重点放在对Html文档中DOM元素的新建、修改与删除和对其属性、样式的随机修改上,主要执行逻辑如下:
其中BuildElementTree函数主要逻辑是:
随机创建一系列DOM元素
随机将这些元素添加到文档树中的某个子节点位置
随机为某个元素的某些动作创建监听事件
随机修改元素的属性和样式等
执行逻辑如下:
之后的Initialize函数逻辑最为简单,只涉及到随机为某些元素的某些动作添加监听事件:
最后Boom函数主要是从之前BuildElementTree函数生成的DOM元素树中,选择具有某些特征的元素组成的对象集合,下图中的NodeIterator对象、TreeWalker对象、TagAggregation、ElemRange对象、TxtRange对象都是采用不同的策略组成的DOM元素集合,然后通过调用AlterRange、MoveIterator、MoveTreeWalker、TagCrawler等方法随机修改、删除集合中的某些元素,以测试浏览器的解析情况:
上图中的Spray函数实现了数据的内存填充:
function spray(){ for(S="\u4545",k=[],y=0;y++<65;) y<20?S+=S:k[y]=[S.substr(22)+"\u4545\u4545"].join(""); }
在nduja逻辑前两个函数中,AddEventListener监听事件指向了一个ModifyDOM自定义函数,它的逻辑主要是在某些Event事件信号产生时,随机创建DOM元素集合,然后随机修改、删除或添加子树:
从上面整个nduja的逻辑流程可以发现,它主要针对DOM元素,随机进行创建、修改和删除操作,所以能够发现很多释放后重用漏洞也就不足为奇了。仔细想想,这类漏洞在2010年左右逐渐兴起,而nduja的作者是在2012年前后开发的这款工具。不难猜测,nduja的作者肯定是当年在分析释放后重用漏洞时,发现了这样一种浏览器释放后重用漏洞的测试逻辑,所以才有了nduja的诞生。
《白帽子讲浏览器安全》中有一段关于nduja现状的描述:
这个框架(nduja)默认的Fuzz效果可能已经不明显了,虽然可以产生显著多的崩溃,但是其中几乎没有可利用的。笔者曾经进行了测试,结果表明,使用默认代码运行七天过程中,产生了数万个崩溃,经程序分类筛选后发现没有可以利用的,在修改框架之后即发现了可用漏洞,所以在现有框架上进行修改甚至于手动定义是很有必要的。
只要详细了解上述nduja的逻辑,然后在现有流程的基础上,加上自己的改进,相信必定有所收获。
目前Morph工具已经开发至v0.3.*版本,将nduja等Fuzzing逻辑作为modules模块的方式添加到工程当中。项目Github地址:
https://github.com/walkerfuz/morph
该工具的架构已经演变成morph.py、web.py和server.py三个松耦合模块:
morph.py:负责启动WEB服务器web.py、通过PyDbgEng3启动并监控浏览器进程、上传经过二次确认的异常样本等
web.py:结合modules模块负责生成静态样本并提供给浏览器进程
server.py:保存morph.py上传的样本结果
主要设计逻辑如下:
关于该框架的使用
假设存储漏洞结果的服务器为192.168.1.10,运行Morph漏洞挖掘任务的客户端为192.168.1.20。
1、首先将server目录拷贝至 192.168.1.10 服务器上,启动:
server -p 8080
浏览器访问[ http://192.168.1.10:8080/upload ]展示收集的漏洞样本结果列表:
2、然后将node目录拷贝至 192.168.1.20 客户端,运行Morph:
morph -b IE -m nduja_try -p 7890 -s 192.168.1.10:8080
当然客户端和服务端也可以同为一台机器,得到的结果存储在server下的upload目录。
关于modules的开发
目前可用的modules包括 nduja_rand、nduja_try、WebAPIs等。 自定义Fuzzing逻辑只需编写 对外提供可以生成静态样本的gen函数接口 的Python脚本即可。格式如下:
#! /user/bin/python# coding:UTF-8class JSTemplater(): def generate(self): script = self.fuzz_nduja() script += self.window_reload() script = self.gen_tags("script", script) head = "<title>nduja_fuzzer</title>\n" body = self.gen_tags("body", script) return head + bodydef gen(): js = JSTemplater() return js.generate()
关于PyDbgEng3进程监控 器
这是一款专门针对Fuzzing测试优化的进程监控器, 项目 Github 地址:
https://github.com/walkerfuz/PyDbgEng3
主要特点包括:
可以得到目标进程异常时的crash详细信息
内置 !exploitable插件,能够判断漏洞是否可以利用
使用方法:
from PyDbgEng3 import Debuggerproc_args = b"C:/Program Files/Internet Explorer/iexplore.exe"crashInfo = Debugger.Run(proc_args, minorHash=True, mode=“M”/"S", trace=None)
该工具还针对某些多进程浏览器进行了优化设计,比如 Chrome浏览器。当多进程浏览器的某个子标签进程出现异常时,PyDbgEng3能够准确记录Crash现场的详细信息,并正确结束整个进程树。只需要将 参数 mode设置为“M”即可。
本文主要讲述了如何解决浏览器Fuzzing过程中遇到的样本难以精简这一问题。可能读者看到最终的解决方法会豁然开朗,但笔者解决这个问题的过程却是十分坎坷的。希望能通过本文,和大家分享我解决问题的心路历程。
这里也讲述了nduja使用的Fuzzing策略,可以说,Fuzzing策略的研究对当前浏览器漏洞挖掘工作的开展十分必要。接下来准备和大家专门探讨一些笔者所用的浏览器Fuzzing策略,希望能抛砖引玉,吸引更多的人加入到讨论中来。
*原创作者:walkerfuz,本文属FreeBuf原创奖励计划文章,未经许可禁止转载