puzzor@home:~$

如何进行浏览器fuzz

如何进行浏览器Fuzz

这篇文章描述了一个浏览器fuzzer的必要组成部分,当然也仅仅如此。生成模版部分每个人有每个人的想法,只要能出货的模版都是好模版。

Fuzzing测试

Fuzzing测试的基本思想是将一些随机数据发送给目标程序,当程序对这些数据进行处理时对其进行观察。如果在处理过程中程序出现异常则需要将这个输入数据保留下来进行进一步的分析以确定该数据是否会导致安全问题。

对于浏览器来说,其处理的数据主要是html文档。因此在对浏览器进行模糊测试时数据的生成主要以html作为程序所处理的数据对象。

浏览器的Fuzzing工具设计

1. 如何生成case 2. 如何捕获异常 3. 如何去重 4. 是否需要考虑复现问题 5. 其它

首先来说为什么要写自己的Fuzzer,最初的想法是要解决grinder中存在的一些feature不能复现的问题,所以就着手去重新写一个静态的Fuzzer。这样就解决了动态模糊测试过程中所面临的样本重现问题。

目前感觉一部分人在用grinder,当然我自己也用过。只不过一段时间后感觉有些问题,除了上面说的重现问题外,精简可能也不是很好(至少我用的时候存在这样的问题);另一部分人在用着自己写的工具去跑,不过我没有见过,只是听说。

在编写过程中要明确整个框架工作机制:Fuzzer要能够自动化的对浏览器进行模糊测试并对异常进行监控。其中这句话包括两个关键部分,1自动测试 2异常监测。而自动测试具体又包含了生成case与自动调用程序打开case。

考虑到要进行静态的测试,因此Fuzzer的整个步骤是:首先生成静态html,然后通过函数调用启动浏览器进程,用调试器挂载浏览器进程,最后让浏览器处理html就可以了。

浏览器Fuzzing工具实现

首先对于之前的问题1,我们在这里定义一个generate()函数,函数中将会在遵循html的语法规范的基础上生成一些随机的内容并写入到html文件中。注意到MAX_HTML_COUNT参数为生成html文件的数量。函数如下:

def generate():
    for i in range(0,MAX_HTML_COUNT):#每轮MAX_HTML_COUNT个case
        html="\n\t\n\t\t\n"
        html+="\t\t\n"
        html+="\t\n"
        html+="\t"+random_str(8)+"\n"
        html+="\t\n\t"        f=open(os.path.abspath(os.path.dirname(__file__))+'\\Data\\Cases\\%d.html'%i,'w')
        f.write(html)
        f.close()

函数中两个注意的地方:

1. 在脚本中加入了”for(var start = Date.now(); Date.now() - start <= 500; ) { }”,这句是为了后面在附加调试器时js能够有一定的延时以便调试器成功挂载IE进程。
2. "window.location.href ='%d.html';"%(i+1)+"\n" 这里能够将MAX_HTML_COUNT个html串起来,执行完一个html后继续载入下一个html 其次对于问题2,我们可以利用pydbg。Pydbg是基于python开发的调试器,能够实现调试器的基本功能。关于pydbg的安装以及使用请自行搜索。利用pydbg我们可以对进程进行attach并且在AV发生时候能够对异常进行处理。接下来我们定义start_debugger函数并将EXCEPTION_ACCESS_VIOLATION类型异常的回调函数设置为av函数。start_debugger函数实现如下: ~~~python def start_debugger (debugger, pid):
try:
    debugger.set_callback(EXCEPTION_ACCESS_VIOLATION, av)
    debugger.attach(pid)
    debugger.debug_event_loop()
except Exception as err:
    print err ~~~ start_debugger函数会附加到进程号为pid的进程上并设置回调函数av。 av函数实现如下: ~~~python def av(dbg):
if dbg.dbg.u.Exception.dwFirstChance:
    return DBG_EXCEPTION_NOT_HANDLED
h=hashlib.md5()
h.update(dbg.disasm(dbg.context.Eip))#计算EIP的hash值
if(os.path.exists(os.path.abspath(os.path.dirname(__file__))+"\\Data\\Crash\\"+h.hexdigest())):#如果已经存在该hash则返回
    dbg.terminate_process()
    return DBG_EXCEPTION_NOT_HANDLED
shutil.copytree(os.path.abspath(os.path.dirname(__file__))+"\\Data\\Cases",os.path.abspath(os.path.dirname(__file__))+"\\Data\\Crash\\"+h.hexdigest())#将此轮测试例拷贝 ~~~ av函数即为异常发生时候的处理函数。这里我们会计算异常发生时的EIP处指令的hash值,计算出hash值后判断是不是已经发生过相同的hash,如果没有就把当前测试的html拷贝一份。当然这里以EIP指令的hash作为区分是否完全合适值得讨论。这个问题也是一开始说的问题3。有以下几种考虑:第一,不同地址的重复指令肯定会有很多,仅仅以指令作为hash肯定会有问题。第二,即便是同一条指令引起的hash,调用栈的不同的也有可能。第三,等等。

最后对于问题4,我们这里采用静态生成并进行fuzz的时候不需要考虑复现问题。当然只是目前不需要,因为我们没有加入资源竞争的处理,如果这一部分加上的话复现这一块就会变得复杂 最后放上整体的代码

#coding=utf-8
__author__ = 'Puzzor'
import os,win32com.client,thread,time,random
from pydbg import *
import hashlib,shutil
from pydbg.defines import *

MAX_HTML_COUNT=100
WMI = win32com.client.GetObject('winmgmts:')

def random_str(randomlength=8):
    strValue = ''
    chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789~!@#$%^&*()'
    length = len(chars) - 1
    for i in range(randomlength):
        strValue+=chars[random.randint(0, length)]
    return strValue

def buildDir():
    try:
        os.mkdir(os.path.abspath(os.path.dirname(__file__))+"\\Data")
    except:
        pass
    try:
        os.mkdir(os.path.abspath(os.path.dirname(__file__))+"\\Data\\Cases")
    except:
        pass
    try:
        os.mkdir(os.path.abspath(os.path.dirname(__file__))+"\\Data\\Crash")
    except:
        pass


def av(dbg):
    if dbg.dbg.u.Exception.dwFirstChance:
        return DBG_EXCEPTION_NOT_HANDLED
    h=hashlib.md5()
    h.update(dbg.disasm(dbg.context.Eip))#计算EIP的hash值
    if(os.path.exists(os.path.abspath(os.path.dirname(__file__))+"\\Data\\Crash\\"+h.hexdigest())):#如果已经存在该hash则返回
        dbg.terminate_process()
        return DBG_EXCEPTION_NOT_HANDLED
    shutil.copytree(os.path.abspath(os.path.dirname(__file__))+"\\Data\\Cases",os.path.abspath(os.path.dirname(__file__))+"\\Data\\Crash\\"+h.hexdigest())#将此轮测试例拷贝

def start_debugger (debugger, pid):
    try:
        debugger.set_callback(EXCEPTION_ACCESS_VIOLATION, av)
        debugger.attach(pid)
        debugger.debug_event_loop()
    except Exception as err:
        print err

def EnumerateProcesses(processName):
    processList = WMI.ExecQuery("SELECT * FROM Win32_Process where name = '%s'"%processName)
    return processList

def fuzz(crash_wait_time):
    kernel32 = windll.kernel32
    while True:
        os.popen('taskkill.exe /im:iexplore.exe /f')
        os.popen('taskkill.exe /im:WerFault.exe /f')#错误报告进程
        buildDir()
        generate()#生成测试例
        db=pydbg()
        kernel32.WinExec("C:\Program Files (x86)\Internet Explorer\iexplore.exe "+os.path.abspath(os.path.dirname(__file__))+"\\Data\\Cases\\0.html",6)
for process in EnumerateProcesses("iexplore.exe"):#枚举进程
            thread.start_new_thread(start_debugger, (db,int(process.Handle)))#尝试附加进程
        time.sleep(crash_wait_time)

if __name__=="__main__":
    crash_wait_time=10#等待时间,主要是等待执行100个html执行完成,自行调整
fuzz(crash_wait_time)