从零开始学Fuzzing系列:浏览器挖掘框架Morph诞生记

来源:互联网 发布:kenzo知乎 编辑:程序博客网 时间:2024/06/01 19:09

0×00 写在前面

欢迎来到《从零开始学Fuzzing系列》。

软件漏洞领域涉及漏洞分析和漏洞挖掘两个方向,目前安全站点关于各种漏洞分析的文章层出不穷,从2013年几天甚至几周才出来的分析文章,到现在几乎与漏洞预警同时发布的报告,也反映出这几年来漏洞分析技术的发展之迅速。

但不可否认的是,与之相辅相成的漏洞挖掘方向(目前大家普遍用的是Fuzzing,高校及实验室研究较多的是BitBlaze和S2E等二进制分析的高端玩意儿),虽然也出现了很多优秀的开源工具,但通俗易懂的学习资料,总感觉少了许多。

优秀的Peach Fuzzer几乎没有公开的Peach Pits,甚至连Pits的编写语法都需要去研究官网上苦涩的文档;优秀的Grinder,公开的Fuzzer模板只有nduja和fileja等少部分经典,针对最新浏览器的兼容性也或多或少有些问题。当然,瑕不掩瑜,这些工具都值得借鉴和学习。

通过这个专题,写写自己在漏洞Fuzzing方向的学习收获,希望能以一个初学者的力量,向国内的Hacker展示漏洞挖掘领域中Fuzzing方向的进展,彼此收获。

鉴于目前浏览器Fuzz的经久不衰,首先从这方面入手。此文是浏览器Fuzz的第一篇。

即将讲述的Morph框架是本文作者基于Puzzor大神《 如何进行浏览器Fuzz》思路编写的一款轻量级浏览器Fuzz框架,以个人理解展开了原文中未深入讲述的样本生成、异常捕获、Crash重现等部分的具体实现。

Morph的核心思想是,利用基于预置Random数组的静态样本生成策略,初步解决了Grinder无法重现某些Crash的缺点(使用过Grinder的深有体会),并借鉴Grinder框架思想,将Fuzzer作为插件进行集成,使得Morph能够很好地支持现有的nduja、cross_fuzz等fuzz工具。

0×01 浏览器Fuzz需要考虑的问题

如何设计一个浏览器Fuzz工具呢?最简单的思路就是,随机生成很多html文档,让浏览器一个个去打开,当打开某个样本导致浏览器崩溃时,那么就可以认为挖到了一个Crash。如下图:

这个过程会涉及几个核心问题:

1.如何随机生成html文档2.如何监控浏览器发生了Crash3.如何记录导致浏览器Crash的那个样本

这三个问题的解决方式,代表着一款浏览器Fuzz框架的特点。下面详细叙述一下开发Morph过程中对这三个问题的理解与实现。

0×02 样本生成:如何随机生成html文档

样本生成的好坏,直接决定了该工具能否挖掘出有价值的漏洞。

目前纯Fuzz方法中,比较流行的就是根据目标文件格式生成针对性样本,效果也比较理想。以HTML文档为例,诞生过很多随机生成html/DOM元素的Browser Fuzzer。

曾经盛名一时的nduja,其大致思路就是,利用Javascript随机创建DOM元素、随机调用DOM处理函数、随机删除DOM元素等操作来完成每一次Fuzz的。该工具曾经找到了很多UAF类型的漏洞,可以说它是2012年前后浏览器漏洞挖掘的一个里程碑思想。

Nduja的主体是一个HTML文件模板,摘抄部分经典Javascript代码:

// 随机数function rand( x ){    return Math.floor( Math.random() * x );}//生成包含随机DOM元素的Range块function createRange(){    range = document.createRange();    rstart = rand(document.all.length);    range.setStart(document.all[rstart], 0);    rend = rand(document.all.length);    range.setEnd(document.all[rend], 0);    return range;}//随机执行Range块处理函数function alterRange(range){    try{        rb = rand(document.all.length);        range_functions[rand(range_functions.length)](range, document.all[rb]);    }catch(exception){ }}//支持的所有Range块处理函数range_functions = [    function(range, elem){    range.deleteContents();    },    function(range, elem){    range.detach();    },    function(range, elem){    range.extractContents();    },    ......];//Fuzz函数function fuzz(){    ......    range1 = createRange();    alterRange(range1);    ......}

另外该fuzzer中还包括随机添加DOM事件addElementListener等方式,有兴趣的可以深入下载研究。

可以发现nduja采取的策略是,借助于rand随机函数,随机对DOM元素进行处理,以测试某些方式的DOM操作能否存在漏洞。

假设某次随机操作会导致浏览器产生Crash,那如何将这次的样本保存呢?不难理解,只要将本次rand函数生成的所有随机数全部保存下来即可。

著名的Grinder框架就是基于这种思路,采用DLL注入的方式,劫持了Javascript的parseFloat函数,用以记录某次样本需要记录的相关参数。比如上面的alterRange函数:

function alterRange(range){    try{        rb = rand(document.all.length);        rx = rand(range_functions.length);        range_functions[rx](range, document.all[rb]);        logger.log('range_functions[‘+ rx +’](‘+ range +’, document.all[‘+ rb +’]);;','nduja',1);    }catch(exception){    }}//备注:logger.log最终会调用Javascript的parseFloat函数

可以看出Grinder提供的logging.js可以直接记录某次执行过的所有Javascript语句,这样对样本重现和样本精简是非常有帮助的。当然,也可以选择直接记录rb、rx等随机值,只是后期的样本精简将会比较复杂。

所以,在nduja中添加适当的logger.log函数,即可在Grinder框架中进行Fuzz工作,原作者也是在这个框架下测试应用的。

Grinder的核心思想如下图:

从理论上来说,Grinder采取的方式比较完美,一方面解决了随机数的记录,一方面又能精简样本。但在实际使用Grinder过程中,为何出现无法重现部分漏洞的情况呢?仔细思考Grinder采用的DLL注入方式,可以发现有以下弊端:

1.并不能保证DLL注入能够成功。必须有symbols文件,它才能正确找到parseFloat函数在浏览器进程的正确位置。而浏览器最新版本一般不提供symbols文件或发布symbols文件比较慢,这无疑限制了Grinder对最新浏览器的测试能力。
2.再者无法保证logger.log记录函数能够把所有的log信息都记录下来。大部分编写Fuzzer插件的开发者并不能100%精确使用Grinder的logger记录每一条html的javascript语句,即使记录了所有语句,也无法保证Logger能够成功将所有语句写入到log文件而不受某次Fuzz的影响。
3.深层次考虑的话,也有可能是多个html共同导致了某个Crash。而Grinder的logger日志只记录一个html中的执行的log信息,这显然是无法重现这类漏洞的。

这些都是Grinder的底层架构带来的种种不足。Grinder要实现的最终目的,就是把每次生成的所有random值或该Random值后执行的Javascript语句记录下来。从本质上看,只要记录下所有random值,那么这一次执行的样本也就能确定下来。

既然DLL注入这种“秋后算账”式记录方法总会导致不确定地丢失,那在每个html中提前生成一堆random值,让javascript去取怎么样?请看nduja中最初的random函数:

// 随机数function rand( x ){    return Math.floor( Math.random() * x );}

我们改写为如下模板方式:

var random_int_array = %RANDOM_INT_ARRAY% ;var array_index = 0 ;function rand( x ){    if(array_index >= %RANDOM_ARRAY_LENGTH%){        array_index = 0 ;    }    return random_int_array[array_index++] % x ;}

每次生成样本,采用Python脚本提前将模板中的%RANDOM_INT_ARRAY%替换为随机生成的数组:

var random_int_array = [5,100,45,67,88,9,101,34,25,……] ;var array_index = 0 ;function rand( x ){    if(array_index >= 500){        array_index = 0 ;    }    return random_int_array[array_index++] % x ;}

每个文档的末尾采用Window.location.href将多个样本链接起来以便能依次打开它们。整个html文档格式如下图所示:

其基本原理如下图所示:

这样就将原本动态记录样本的方式变为静态生成样本的方式,完美解决了Grinder在样本恢复上的种种不足。

当然,这样做也有一定的弊端,Crash样本没有精简,所有的Crash不同的只是array数组的不同,后续的漏洞分析将是一场噩梦。因此,在后续样本分析之前,还需要提前精简样本。

0×03异常监控:如何监控浏览器发生了Crash

在对浏览器的异常检测上,通常有Pydbg和Windbg等方式,可以说各有千秋吧。但pydbg对python3和x64系统软件支持都不是特别好,另外windbg具有很吸引人的!exploitable插件,虽然某些时候给出的结论并不准确,但不失为一种对漏洞可利用性做出预判的方法,故而在Morph的开发过程中,采用windbg作为异常监控器。

在Windows下某个进程崩溃时,通常会弹出WerFault.exe异常提示,可以根据进程列表中有无WerFault.exe作为监控浏览器是否发生Crash的依据。

更好的方法是,将Windbg的命令行版cdb.exe设置为默认即时调试器:

>cdb.exe -iaec "-y -logo c:/log.txt -c \"!load msec.dll;!exploitable -v;\""

参数解释:

-iae将CDB安装为即时调试器,该参数不能和其他参数一起使用。该命令并不实际启动CDB。
-log{o|a} LogFile将日志记录到日志文件中。如果指定文件已存在,使用-logo 时会被覆盖,使用-loga 时会将新内容添加到后面。
-c "command"指定启动时运行的初始调试器命令。该命令必须用引号括起来,多条命令可以使用分号来分隔。

当系统某个进程出现异常时,会调用cdb.exe并执行!load msec.dll;!exploitable指令,判断该异常是否是可利用的,最终log信息会存放在log.txt文件中。

下面是Python脚本判断cdb.exe进程的核心代码:

def Watch():  config.MONITOR_RUNNING = True  # 1.循环检测是否出现崩溃进程  monitor_crash_proc()  if config.MONITOR_RUNNING is True:      # 2.检测到异常后保存当前样本    save_crash_and_log ()def monitor_crash_proc( ):  while True:    if LAST_COMPLETE_VECTOR >= (VECTORS_NUM-1):      MONITOR_RUNNING = False    if MONITOR_RUNNING is False or psutil.exist(‘cdb.exe’):      return

利用Windbg的!exploitable插件得到的log信息大致如下:

Description: Read Access Violation near NULLShort Description: ReadAVNearNullExploitability Classification: PROBABLY_NOT_EXPLOITABLERecommended Bug Title: Read Access Violation near NULL starting at MSHTML!CDoc::AFunC+0x058a (Hash=0x5789fdff.0x12345d4e)This is a user mode read access violation near null, and is probably not exploitable.

可以提取出Short Description、Exploitability Classification、Hash等关键词作为Crash文件名。主要脚本如下:

def GetCrashHash(logPath):    content = file.ReadFromFile(logPath)    try:         crash_exploitable = re.search("Explo..: \w+", content).group().replace("Exploitabi..: ", "", 1)        crash_type = re.search("Short Description: \w+", content).group().replace("Short Description: ", "", 1)        crash_hash = re.search("Hash=0x\w+\.0x\w+", content).group().replace("Hash=", "", 1)        return ("%s_%s_%s" % (crash_exploitable, crash_type, crash_hash))    except:        return False

分析得到的文件名如下:

PROBABLY_NOT_EXPLOITABLE_ ReadAVNearNull_0x5789fdff.0x12345d4e.html

其中Windbg中采用的!Exploitable插件最新版是v1.6.0。

0×04 样本保存:如何记录导致浏览器崩溃的那个样本

根据前面Random数组的方式,很容易生成很多静态html样本,在每个html结尾添加window.location.href指向下一个html文档。

假设生成了N个html文档,则打开第一个文档,浏览器即可依次执行序号为1,2,3,…N的html文档:

但这个过程中如果浏览器出现崩溃,却无法判断是这N个html中的哪一个导致的崩溃。这种情况该如何解决呢?

既然问题的关键在于,如何确定发生崩溃时正在执行的样本序号,那有没有一种方法可以让html文档在被打开时,直接告诉监控器自身的序号呢?

分析到这里,容易想到,可以让每个html文档执行fuzz函数之前,提前调用一个函数告诉监控器自身的序号,如果浏览器出现崩溃,监控器读取当前获得的序号即可。

在Javascript中可以向服务端发送消息并返回的手段包括XMLHttpRequest和WebSocket。鉴于HTML5是WEB前端的潮流,这里采用WebSocket方式。

在每个HTML文档Javascript中的Fuzz函数之前,添加一条WebSocket.Send语句,由网页自身发起Socket通信,告诉监控器的WebSocket Server端关于自身的序号。当该网页导致浏览器崩溃时,监控器很容易就能知道是哪个网页引起的了。

每个html网页模板添加WebSocket.Send后的格式类似于:

在编程实现上,为了保证漏洞重现时,减少对WebSocket的依赖,对html模板的顺序调整如下:

所有网页被浏览器打开后依次执行的原理如下图:

具体在html模板页面中JavaScript增加的代码如下:

function morph_fuzz(){    ……}function morph_notify_href(){    var socket  ;    socket = new WebSocket('ws://127.0.0.1:8080/');    socket.onopen = function(event) {        socket.send('1');    }    socket.onmessage = function(event) {        window.location.href = 2.html';    }}function morph_main(){   morph_fuzz();   morph_notify_href();}  ......<body onload="morph_main()"></body>

Server端负责记录最近被加载的html文档的序号:

def websocketHandle(websocket, path):    while True:        if not websocket.open:            return        msg = yield from websocket.recv()        if msg is None:            continue        # 记录最近被加载的VECTOR样本序号        LAST_COMPLETE_VECTOR = int(msg)        yield from websocket.send('RUN')

当监控器监控到浏览器进程出现崩溃时,可以断定LAST_COMPLETE_VECTOR+1即为导致崩溃的样本序号,将其对应的html文档保存即可:

def save_crash_and_log ( ):    # 得到当前Crash序号    crash_num = config.LAST_COMPLETE_VECOTR + 1        ……    # 保存当前样本和当前log    file.SaveFileFromSrcToDst(vectorCrashPath, dstCrashPath)    file.SaveFileFromSrcToDst(debuggerLogPath, dstLogPath)    ……    config.LAST_COMPLETE_VECOTR += 1

在保存该Crash样本后,LAST_COMPLETE_VECTOR自动+1,表示该样本已经被处理完成。

0×05 浏览器Fuzz需要注意的几个问题

在设计浏览器Fuzz框架过程中,会遇到很多看似很小但很重要的问题,下面列举和大家一起分享交流。

1.浏览器打开样本是采用file:///协议还是http://协议?

浏览器常用的是http和https协议,另外还支持file、data、gopher等协议。

在实际Fuzz中发现,部分网页使用http协议打开没事,但通过file协议打开就会导致Crash,猜测可能是由于file协议和http协议的权限不同造成的,也有可能是浏览器对file协议的解析不当(漏洞原因还未分析)。

目前在编程时,采用file协议打开样本,后续版本准备增加http等其它协议。

2.Firefox浏览器如何关闭Safemode安全模式?

默认情况下,采用第三方进程强制结束Firefox进程几次后,会弹出是否进入Firefox安全模式的提示,影响了正常Fuzz过程。需要在Firefox地址栏输入about:config,查找toolkit.startup.max_resumed_crashes,将其设置为-1即可关闭安全模式。

0×06 关于Morph框架的使用说明

上述就是Morph框架的核心思想,目前该工具仍在开发中,Github项目地址:

https://github.com/walkerfuz/morph

里面公开了nduja fuzzer插件,如果有兴趣的,可以根据Simple.html和nduja.html增加自定义插件。

关于Morph框架的安装和使用:

1. 安装Windbgx86 or Windbgx64。

下载MSECExtensions插件放在Windbg的winext文件夹,并打开Windbg测试load msec.dll是否成功,若出现Can't Load Library的错误,则需要安装Visual C++ Redistributable for Visual Studio 2008/2012。

注意:MSECExtensions1.6.0需要VC++2012运行时环境支持。

2. 将Windbg程序文件夹下的cdb.exe设置为默认JIT即时调试器:

>cdb.exe -iaec "-logo c:/log.txt -c \"!load msec.dll;!exploitable -v;\""

注意:c:/log.txt与config.py中的Debugger中的log参数必须一致

3. 如果Fuzz目标是Firefox,则需要关闭安全模式:

在firefox进入about:config找到toolkit.startup.max_resumed_crashes(默认是3),将其设置为-1即可。另外需要在Firefox选项–>隐私–>选择'不记录历史' 关闭Firefox的历史记录功能。

4.采用Windbg目录下自带的gflags.exe开启目标进程的页堆调试功能,比IE浏览器:

>gflags.exe /i iexplore.exe +hpa

5.下载Morph并配置Config.py中的默认参数。下面是几个比较常用的参数:

# configs which Can be modifiedMOR_FUZZERS_FOLDER = "fuzzer"MOR_CRASHES_FOLDER = "crash"MOR_VECTORS_FOLDER = "vector"MOR_FUZZER_SUFFIX = ".html"MOR_DBGLOG_SUFFIX = ".log"MOR_PRE_VECTORS_NUM = 50MOR_RANDOM_ARRAY_LENGTH = 10000MOR_MAX_RANDOM_NUMBER = 1000MOR_WEBSOCKET_SERVER = "127.0.0.1:8080"MOR_BROWSERS = {    "IE": {        'proc': 'iexplore.exe',        'args': "",        'fault': "WerFault.exe",        'path': "C:/Program Files/Internet Explorer/iexplore.exe",    },    "FF": {        'proc': 'firefox.exe',        'args': "",        'fault': "WerFault.exe",        'path': "C:/Program Files (x86)/Mozilla Firefox/firefox.exe",    },    "CM": {        'proc': 'chrome.exe',        'args': "--no-sandbox",        'fault': "WerFault.exe",        'path': "C:/Program Files (x86)/Google/Chrome/Application/chrome.exe",    },}MOR_DEBUGGER = {    "Windows": {        'proc': "cdb.exe",        'args': "",        'path': "C:/Program Files (x86)/Debugging Tools for Windows (x86)/cdb.exe",        'log': "C:/log.txt",    }}

请确保上述参数与实际环境对应起来。

6.运行:

morph.py --browser=IE --fuzzer=nduja.html

另附运行截图:

0×07 Fuzzer插件的开发方法

Morph采用插件式Fuzzer集成方法,目前提供了一个Simple.html模板和改进的nduja.html模板,请从Morph/Fuzzer目录中查看源代码,其中Simple.html如下:

<!DOCTYPE html><html><head><title>Morph Simple Fuzzer</title><script type='text/javascript'>var random_int_array = %MOR_RANDOM_INT_ARRAY% ;var array_index = 0 ;// Pick a random number between 0 and Xfunction morph_rand( x ){   if(array_index >= %MOR_RANDOM_ARRAY_LENGTH%){      array_index = 0 ;   }   return random_int_array[array_index++] % x ;   }function rand_item( arr ){   return arr[morph_rand(arr.length)];}elements = ["a","abbr","input","ins","isindex","b","base","basefont","bdi","bdo","big","blockquote","body","br","button",   "canvas","caption","center","cite","code","col","colgroup","command",   "datalist","dd","del","details","dir","div","dfn","dialog","dl","dt",   "h1","h2","h3","h4","h5","h6","head","header","hr","html",];MAX_ELEMENT_NUM = 200;function morph_fuzz(){   elementTree = [];     for(k=0;k<morph_rand(MAX_ELEMENT_NUM);k++){      r = rand_item(elements);      elementTree[k] = document.createElement(r);      elementTree[k].id = "Element" + k;      rb = morph_rand(document.all.length);      document.all[rb].appendChild(elementTree[k]);     }}function morph_notify_href(){   var socket  ;   socket = new WebSocket('ws://%MOR_WEBSOCKET_SERVER%/');   socket.onopen = function(event) {      socket.send('%MOR_CURRENT_HREF%');   }   socket.onmessage = function(event) {      window.location.href = '%MOR_NEXT_HREF%';              }}function morph_main(){   morph_fuzz();   morph_notify_href();}  </script></head><body onload="morph_main()"></body></html>

在每次生成静态样本时,利用Python脚本,将%MOR_RANDOM_INT_ARRAY%、%MOR_RANDOM_ARRAY_LENGTH%替换为random数组,将%MOR_WEBSOCKET_SERVER%替换为config.py中设置的WebSocket服务器,将%MOR_CURRENT_HREF%和%MOR_NEXT_HREF%替换为前后连续的两个序号。

只要按照上面的插件编写逻辑,即可轻松实现Fuzzer插件的开发,有兴趣的童鞋可以将网上公开的fileja、cross_fuzz改写为Morph的插件,仍旧会发现很多漏洞滴。

0×08 总结

虽然目前纯粹做Fuzz已经不适应最新技术潮流了,但开发Morph时也学到了很多东西,同时对Fuzz的核心有了更深入的理解。

Morph在Fuzz效率(生成所有样本文件太耗时)和针对Linux、Andriod等兼容上还需要完善,自定义插件只编写了nduja,后面也会陆续发布比较经典的其它几种插件,让我们一起期待morph v0.3吧!

0 0