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

www.96kaifa.com | 2016-10-30 |

摘要:文章作者:walkerfuz
0×00 写在前面
站在巨人的肩膀上,才能看的更远,开发项目亦是如此。

四年前开源的Grinder项目,和借助于它运行的nduja,着实让浏览器漏洞挖掘飞入了寻常百姓家。但随着时间的考验,Grin…...

文章作者: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元素的新建、修改与删除和对其属性、样式的随机修改上,主要执行逻辑如下:

1.jpg

其中BuildElementTree函数主要逻辑是:

随机创建一系列DOM元素

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

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

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

执行逻辑如下:

2.png

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

3.png

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

4.png

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

5.png

从上面整个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上传的样本结果

主要设计逻辑如下:

6.png

关于该框架的使用

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

8.png

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

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

9.png

当然客户端和服务端也可以同为一台机器,得到的结果存储在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原创奖励计划文章,未经许可禁止转载