本文主要是介绍从零开始学Fuzzing系列:浏览器挖掘框架Morph诞生记,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
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.如何监控浏览器发生了Crash 3.如何记录导致浏览器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 = Falseif MONITOR_RUNNING is False or psutil.exist(‘cdb.exe’):return
利用Windbg的!exploitable插件得到的log信息大致如下:
Description: Read Access Violation near NULL
Short Description: ReadAVNearNull
Exploitability Classification: PROBABLY_NOT_EXPLOITABLE
Recommended 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 οnlοad="morph_main()"></body>
Server端负责记录最近被加载的html文档的序号:
def websocketHandle(websocket, path):while True:if not websocket.open:returnmsg = 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……# 保存当前样本和当前logfile.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 modified MOR_FUZZERS_FOLDER = "fuzzer" MOR_CRASHES_FOLDER = "crash" MOR_VECTORS_FOLDER = "vector" MOR_FUZZER_SUFFIX = ".html" MOR_DBGLOG_SUFFIX = ".log" MOR_PRE_VECTORS_NUM = 50 MOR_RANDOM_ARRAY_LENGTH = 10000 MOR_MAX_RANDOM_NUMBER = 1000 MOR_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 X function 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吧!
这篇关于从零开始学Fuzzing系列:浏览器挖掘框架Morph诞生记的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!