神刀安全网

从零开始学Fuzzing系列:带领nduja突破Grinder的壁垒

文章作者:walkerfuz

0×00 写在前面

站在巨人的肩膀上,才能看的更远,开发项目亦是如此。

四年前开源的Grinder项目,和借助于它运行的nduja,着实让浏览器漏洞挖掘飞入了寻常百姓家。但随着时间的考验,Grinder也遇到了让人爱恨交加的尴尬:明明产生了Crash,可就是无法重现。有多少人和我一样,从初识Grinder的激动,到分离时的落寞,也见证了一代怀揣梦想的挖洞人的足迹。

在现有项目的基础上,对其进行改进,乃是一种进步,于是出现了Morph项目。

Morph起初的定位就是解决Grinder架构中存在的本质问题:样本无法稳定重现,所以才有了前面《 浏览器挖掘框架Morph诞生记 》中讲述的“静态随机数组”的尝试,但随之带来的并发症却是:样本难以精简。

本文正是为解决这个问题而产生的,笔者采用一种Grinder log静态化的方式,将原本Grinder中采用DLL注入截获log语句的方式改进为提前生成静态精简样本的方式,从根本上解决样本无法稳定重现和难以精简两大难题,为浏览器漏洞挖掘工作提供新的思路。

0×01 我们需要什么格式的样本?

前面《 浏览器挖掘框架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 X function 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 = 1000 c.height = 1000 document.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>

上述样本采用二分法即可确定是哪些语句造成了浏览器崩溃。总之, 像上述两种没有循环,“一条大路走到底”格式的样本,才是我们期望得到的

0×02 Grinder log静态化的尝试

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的逻辑是什么样的呢?

0×03 nduja的逻辑

nduja的重点放在对Html文档中DOM元素的新建、修改与删除和对其属性、样式的随机修改上,主要执行逻辑如下:

从零开始学Fuzzing系列:带领nduja突破Grinder的壁垒

其中BuildElementTree函数主要逻辑是:

随机创建一系列DOM元素

随机将这些元素添加到文档树中的某个子节点位置

随机为某个元素的某些动作创建监听事件

随机修改元素的属性和样式等

执行逻辑如下:

从零开始学Fuzzing系列:带领nduja突破Grinder的壁垒

之后的Initialize函数逻辑最为简单,只涉及到随机为某些元素的某些动作添加监听事件:

从零开始学Fuzzing系列:带领nduja突破Grinder的壁垒

最后Boom函数主要是从之前BuildElementTree函数生成的DOM元素树中,选择具有某些特征的元素组成的对象集合,下图中的NodeIterator对象、TreeWalker对象、TagAggregation、ElemRange对象、TxtRange对象都是采用不同的策略组成的DOM元素集合,然后通过调用AlterRange、MoveIterator、MoveTreeWalker、TagCrawler等方法随机修改、删除集合中的某些元素,以测试浏览器的解析情况:

从零开始学Fuzzing系列:带领nduja突破Grinder的壁垒

上图中的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元素集合,然后随机修改、删除或添加子树:

从零开始学Fuzzing系列:带领nduja突破Grinder的壁垒

从上面整个nduja的逻辑流程可以发现,它主要针对DOM元素,随机进行创建、修改和删除操作,所以能够发现很多释放后重用漏洞也就不足为奇了。仔细想想,这类漏洞在2010年左右逐渐兴起,而nduja的作者是在2012年前后开发的这款工具。不难猜测,nduja的作者肯定是当年在分析释放后重用漏洞时,发现了这样一种浏览器释放后重用漏洞的测试逻辑,所以才有了nduja的诞生。

《白帽子讲浏览器安全》中有一段关于nduja现状的描述:

这个框架(nduja)默认的Fuzz效果可能已经不明显了,虽然可以产生显著多的崩溃,但是其中几乎没有可利用的。笔者曾经进行了测试,结果表明,使用默认代码运行七天过程中,产生了数万个崩溃,经程序分类筛选后发现没有可以利用的,在修改框架之后即发现了可用漏洞,所以在现有框架上进行修改甚至于手动定义是很有必要的。

只要详细了解上述nduja的逻辑,然后在现有流程的基础上,加上自己的改进,相信必定有所收获。

0×04 Morph的架构与使用

目前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上传的样本结果

主要设计逻辑如下:

从零开始学Fuzzing系列:带领nduja突破Grinder的壁垒

关于该框架的使用

假设存储漏洞结果的服务器为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 ]展示收集的漏洞样本结果列表:

从零开始学Fuzzing系列:带领nduja突破Grinder的壁垒

2、然后将node目录拷贝至 192.168.1.20 客户端,运行Morph:

morph -b IE -m nduja_try -p 7890 -s 192.168.1.10:8080

从零开始学Fuzzing系列:带领nduja突破Grinder的壁垒

当然客户端和服务端也可以同为一台机器,得到的结果存储在server下的upload目录。

关于modules的开发

目前可用的modules包括 nduja_rand、nduja_try、WebAPIs等。 自定义Fuzzing逻辑只需编写 对外提供可以生成静态样本的gen函数接口 的Python脚本即可。格式如下:

#! /user/bin/python # coding:UTF-8 class 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 + body  def gen():     js = JSTemplater()     return js.generate()

关于PyDbgEng3进程监控

这是一款专门针对Fuzzing测试优化的进程监控器, 项目 Github 地址:

https://github.com/walkerfuz/PyDbgEng3

主要特点包括:

可以得到目标进程异常时的crash详细信息

内置 !exploitable插件,能够判断漏洞是否可以利用

使用方法:

from PyDbgEng3 import Debugger proc_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”即可。

0×05 总结

本文主要讲述了如何解决浏览器Fuzzing过程中遇到的样本难以精简这一问题。可能读者看到最终的解决方法会豁然开朗,但笔者解决这个问题的过程却是十分坎坷的。希望能通过本文,和大家分享我解决问题的心路历程。

这里也讲述了nduja使用的Fuzzing策略,可以说,Fuzzing策略的研究对当前浏览器漏洞挖掘工作的开展十分必要。接下来准备和大家专门探讨一些笔者所用的浏览器Fuzzing策略,希望能抛砖引玉,吸引更多的人加入到讨论中来。

*原创作者:walkerfuz,本文属FreeBuf原创奖励计划文章,未经许可禁止转载

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » 从零开始学Fuzzing系列:带领nduja突破Grinder的壁垒

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址