domato101
fa1lr4in Lv2

Domato101

简介

Domato是googleprojectzero团队成员Ivan Fratric使用python开发的一款基于生成的DOM引擎fuzzer。

基于生成的fuzzer最困难的点之一在于创建的样本的语法或其他结构,作者尝试过手动创建以及从 Web 浏览器代码中自动提取的语法。

fuzzer由几个部分组成:

  • 一个给定语法结构可以生成样本的主引擎
  • 一组用于生成 HTML、CSS 和 JavaScript 代码的语法规定。

domato的使用非常简单

1
2
3
4
5
6
7
8
9
10
# domato目前使用python3 
# https://github.com/googleprojectzero/domato
# 查看使用信息
python3 generator.py -h

# 生成单个文件
python3 generator.py -f <output file>

# 生成多个文件
python generator.py -o <output directory> -n <number of output files>

生成的html文件都比较大,目的可能是为了增加代码覆盖率,从而增加触发crash的概率。生成的样本因为会指定目录下,所以命名为 fuzz-.html,例如 fuzz-00001.html、fuzz-00002.html 等。生成多个样本,只输入的语法文件需要加载和解析一次。

使用bugid配合domato进行fuzz测试

下载bugid并开启页堆

1
2
3
4
5
6
7
8
9
# BugId使用python2,如果python2和python3共存,可以将里面的可执行文件复制,并改名为python[2|3]
# https://github.com/SkyLined/BugId

# 开启对应应用程序的页堆:可以使用visual studio附带的gflags,也可以使用bugid带的脚本PageHeap.cmd。四大浏览器开启页堆命令如下。
# 使用PageHeap开启命令需要管理员权限
PageHeap.cmd edge ON
PageHeap.cmd chrome ON
PageHeap.cmd firefox ON
PageHeap.cmd msie ON

本人暂时未搭建其他浏览器的环境,至测试了IE环境,下面的图片来自blog

![#2 PageHeap](../../../study/【1】fa1lr4in_article/【2】FUZZ【进行中】/【1】FUZZ基础概念/【2】源码分析/domato/%232 PageHeap.png)

检测环境是否就绪

1
2
3
python -c "print 'ok!'"
python.exe "domato-master\generator.py"
BugId\BugId.cmd "%WinDir%\system32\cmd.exe" --cBugId.bEnsurePageHeap=false -- /C ECHO ok!

image-20211223215213610

有关下面两段代码的解释可以参考here

  • fuzz.cmd的主要功能就是调用了domato去生成了一些样本,并通过bugid进行逐个分析。
  • index.html则主要是测试浏览器对每个标签页的响应速度,一般测试为2s,根据自己的实际情况进行测试。

fuzz.cmd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
@ECHO OFF
SET BASE_FOLDER=C:\Fuzzing
SET PYTHON_EXE=C:\Python27\python.exe

:: What browser do we want to fuzz? ("chrome" | "edge" | "firefox" | "msie")
SET TARGET_BROWSER=msie
:: How many HTML files shall we teach during each loop?
SET NUMBER_OF_FILES=100
:: How long does it take BugId to start the browser and load an HTML file?
SET BROWSER_LOAD_TIMEOUT_IN_SECONDS=30
:: How long does it take the browser to render each HTML file?
SET AVERAGE_PAGE_LOAD_TIME_IN_SECONDS=2

:: Optionally configurable
SET BUGID_FOLDER=%BASE_FOLDER%\BugId
SET DOMATO_FOLDER=%BASE_FOLDER%\domato-master
SET TESTS_FOLDER=%BASE_FOLDER%\Tests
SET REPORT_FOLDER=%BASE_FOLDER%\Report
SET RESULT_FOLDER=%BASE_FOLDER%\Results
:: Store our results in a folder named after the target:
IF NOT EXIST "%RESULT_FOLDER%\%TARGET_BROWSER%" MKDIR "%RESULT_FOLDER%\%TARGET_BROWSER%"

::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Repeatedly generate tests and run them in the browser.
:LOOP
CALL :GENERATE
IF ERRORLEVEL 1 EXIT /B 1
CALL :TEST
IF ERRORLEVEL 1 EXIT /B 1
GOTO :LOOP

::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Generate test HTML files
:GENERATE
REM Delete old files.
DEL "%TESTS_FOLDER%\fuzz-*.html" /Q >nul 2>nul
REM Generate new HTML files.
"%PYTHON_EXE%" "%DOMATO_FOLDER%\generator.py" --output_dir "%TESTS_FOLDER%" --no_of_files %NUMBER_OF_FILES%
IF ERRORLEVEL 1 EXIT /B 1
EXIT /B 0

::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Run browser in BugId and load test HTML files
:TEST
REM Delete old report if any.
IF NOT EXIST "%REPORT_FOLDER%" (
MKDIR "%REPORT_FOLDER%"
) ELSE (
DEL "%REPORT_FOLDER%\*.html" /Q >nul 2>nul
)
REM Guess how long the browser needs to run to process all tests.
REM This is used by BugId to terminate the browser in case it survives all tests.
SET /A MAX_BROWSER_RUN_TIME=%BROWSER_LOAD_TIMEOUT_IN_SECONDS% + %AVERAGE_PAGE_LOAD_TIME_IN_SECONDS% * %NUMBER_OF_FILES%
REM Start browser in BugId...
"%PYTHON_EXE%" "%BUGID_FOLDER%\BugId.py" "%TARGET_BROWSER%" "--sReportFolderPath=\"%REPORT_FOLDER:\=\\%\"" --nApplicationMaxRunTimeInSeconds=%MAX_BROWSER_RUN_TIME% -- "file://%TESTS_FOLDER%\index.html"

IF ERRORLEVEL 2 (
ECHO - ERROR %ERRORLEVEL%.
REM ERRORLEVEL 2+ means something went wrong.
ECHO Please fix the issue before continuing...
EXIT /B 1
) ELSE IF NOT ERRORLEVEL 1 (
EXIT /B 0
)
ECHO Crash detected!

REM Create results sub-folder based on report file name and copy test files
REM and report.
FOR %%I IN ("%REPORT_FOLDER%\*.html") DO (
CALL :COPY_TO_UNIQUE_CRASH_FOLDER "%RESULT_FOLDER%\%%~nxI"
EXIT /B 0
)
ECHO BugId reported finding a crash, but not report file could be found!?
EXIT /B 1

:COPY_TO_UNIQUE_CRASH_FOLDER
SET REPORT_FILE=%~nx1
REM We want to remove the ".html" extension from the report file name to get
REM a unique folder name:
SET UNIQUE_CRASH_FOLDER=%RESULT_FOLDER%\%TARGET_BROWSER%\%REPORT_FILE:~0,-5%
IF EXIST "%UNIQUE_CRASH_FOLDER%" (
ECHO Repro and report already saved after previous test detected the same issue.
EXIT /B 0
)
ECHO Copying report and repro to %UNIQUE_CRASH_FOLDER% folder...
REM Move report to unique folder
MKDIR "%UNIQUE_CRASH_FOLDER%"
MOVE "%REPORT_FOLDER%\%REPORT_FILE%" "%UNIQUE_CRASH_FOLDER%\report.html"
REM Copy repro
MKDIR "%UNIQUE_CRASH_FOLDER%\Repro"
COPY "%TESTS_FOLDER%\*.html" "%UNIQUE_CRASH_FOLDER%\Repro"
ECHO Report and repro copied to %UNIQUE_CRASH_FOLDER% folder.
EXIT /B 0

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!doctype html>
<!-- saved from url=(0014)about:internet -->
<html>
<head>
<script>
var oIFrameElement = document.getElementById("IFrame"),
nPageLoadTimeoutInSeconds = 5,
uIndex = 0;
onload = function fLoadNext() {
// Show progress in title bar.
var sIndex = "" + uIndex++;
while (sIndex.length < 5) sIndex = "0" + sIndex;
var sTestURL = "fuzz-" + sIndex + ".html";
document.title = "Loading test " + sTestURL + "...";
// Add iframe element that loads the next test case.
var oIFrame = document.body.appendChild(document.createElement("iframe")),
bFinished = false;
oIFrame.setAttribute("sandbox", "allow-scripts");
oIFrame.setAttribute("src", sTestURL);
// Hook load event handler and add timeout to remove the iframe when the test is finished.
try {
oIFrame.contentWindow.addEventListener("load", fCleanupAndLoadNext);
} catch (e) {
// This may cause an exception because some browsers treat different files loaded from the
// local file system as comming from different origins.
};
var xTimeout = setTimeout(fCleanupAndLoadNext, nPageLoadTimeoutInSeconds * 1000);
function fCleanupAndLoadNext() {
// Both the load event and the timeout can call this function; make sure we only execute once:
if (!bFinished) {
bFinished = true;
console.log("Finished test " + sTestURL + "...");
// Let's give the page another 5 seconds to render animations etc.
setTimeout(function() {
// Remove the iframe from the document to delete the test.
try {
document.body.removeChild(oIFrame);
} catch (e) {};
}, 5000);
fLoadNext();
};
};
};
</script>
</head>
<body>
</body>
</html>

运行截图

image-20211223215010429

模块分析

generator.py

是主脚本,使用了grammar.py 作为库,并包含fuzzer的额外代码。

grammar.py

包含大多数与应用程序无关的生成引擎,因此可以在其他(即非DOM)基于生成的模糊器中使用。

.txt 文件包含语法定义。有 3 个主要文件,html.txt、css.txt 和 js.txt,分别包含 HTML、CSS 和 JavaScript 语法。这些根语法文件可能包含来自其他文件的内容。

该文件可以当作库进行使用,当我们需要使用自定义语法时,可以:

1
2
3
4
5
from grammar import Grammar

my_grammar = Grammar()
my_grammar.parse_from_file('input_file.txt')
result_string = my_grammar.generate_symbol('symbol_name')

扩展

基本语法

Domato 基于一个引擎,该引擎以下面指定的简单格式给出上下文无关的文法,然后从该文法生成样本。

语法被描述为具有以下基本格式的一组规则:

1
<symbol> = a mix of constants and <other_symbol>s

每个语法规则都包含由等号左侧和右侧。左侧包含一个符号,而右侧包含该符号的实现。

考虑以下 CSS 语法部分的简化示例:

1
2
3
4
<cssrule> = <selector> { <declaration> }
<selector> = a
<selector> = b
<declaration> = width:100%

使用上面的语法可以生成如下的代码

1
a { width:100% }

或者

1
b { width:100% }

也可以对selector进行加权

1
2
<selector p=0.9> = a
<selector p=0.1> = b

这样字符串 ‘a’ 将比 ‘b’ 更频繁地输出。

生成编程语言代码

要生成编程语言代码,可以使用类似的语法,但存在一些差异。编程语言语法的每一行都将对应于输出行。正因为如此,文法语法将更加自由,以允许用各种编程语言表达结构。其次,当生成一行时,除了输出该行之外,还可以创建一个或多个变量,这些变量可以在生成其他行时重复使用。同样,让我们看一下简化的示例:

1
2
3
4
5
6
7
!varformat fuzzvar%05d
!lineguard try { <line> } catch(e) {}

!begin lines
<new element> = document.getElementById("<string min=97 max=122>");
<element>.doSomething();
!end lines

如果引擎生成 5 行,我们可能会得到如下结果:

1
2
3
4
5
try { var00001 = document.getElementById("hw"); } catch(e) {}
try { var00001.doSomething(); } catch(e) {}
try { var00002 = document.getElementById("feezcqbndf"); } catch(e) {}
try { var00002.doSomething(); } catch(e) {}
try { var00001.doSomething(); } catch(e) {}

对上面的代码进行解释

  • 编程语言被限制在 ‘!begin lines’ 和 ‘!end lines’ 之间。这样语法解析器更容易识别各个范围。
  • <new element><element>的区别在于: <new element>表示变量赋值,而<element>表示了变量属性调用。
  • <string> 是内置符号之一,无需定义。
  • [可选] !varformat 定义要使用变量命名的格式。
  • [可选] !lineguard 定义在每一行周围插入的附加代码,以便捕获异常或执行其他任务。这样您就无需为每一行单独编写它。
  • 除了 ‘!begin lines’ 和 ‘!end lines’ 之外,您还可以使用 ‘!begin helperlines’ 和 ‘!end helperlines’ 定义辅助输出行()

注释

该行第一个“#”字符之后的所有内容都被视为注释,例如:

1
# This is a comment

防止无限递归

语法中有种方法可以告诉fuzzer哪些规则是非递归的,即使达到了最大递归级别也可以安全使用。这是通过“非递归”属性完成的。下面给出一个例子。

1
2
3
4
!max_recursion 10
<test root=true> = <foobar>
<foobar> = foo<foobar>
<foobar nonrecursive> = bar

可选选项 ‘!max_recursion’ 语句定义了最大递归深度级别(默认为 50)。第三行使用了递归调用,如果达到最大递归层数,生成器将强制结束递归语句的生成。

include与import

在 Domato 中,includeimport是不同的

include比较简单,如下代码所示:将 other.txt 中的rules包含到当前rules中。

1
!include other.txt

import的工作方式略有不同:

1
!import other.txt

它会创建一个新的 Grammar() 对象,然后可以使用特殊<import>符号从当前语法中引用该对象,例如:

1
<cssrule> = <import from=css.txt symbol=rule>

namespace的角度考虑includeimportinclude 会将包含的语法放入单个namespace中,而 import 将创建一个新的namespace,然后可以使用<import>符号和通过 from 属性让其他namespace进行访问.

使用 Python 代码

当我们想在语法中调用自定义 Python 代码。例如,假设您想使用引擎生成 http 响应,并且您希望正文长度与“大小”标头相匹配。正常情况下普通语法规则很难实现,但我们可以通过自定义 Python 代码来轻松它,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
!begin function savesize
context['size'] = ret_val
!end function

!begin function createbody
n = int(context['size'])
ret_val = 'a' * n
!end function

<foo root> = <header><cr><lf><body>
<header> = Size: <int min=1 max=20 beforeoutput=savesize>
<body> = <call function=createbody>

python 函数定义在 ‘!begin function ‘ 和 ‘!end function’ 命令之间。我们可以通过使用beforeoutput属性和使用<call>符号两种方式调用这些函数。

使用 beforeoutput 时,会在外部给调用的函数一个初始值,而这个初始值会被传递给ret_val

使用<call>时,会将函数的结果保存在ret_val中,如果不给ret_val赋值默认为空。

您的 Python 代码可以访问以下变量:

  • context- 相当于一个字典,上面的例子中savesize函数使用了key对应了size,而value对应了ret_val。实际上就是通过context定义了变量。
  • attributes- 相当于函数参数的传递。例如,类似于<call function=func foo=bar>,则foo的缺省值为”bar”。
  • ret_val- 函数的返回值。使用<call>时默认为空,

其他

考虑另一个生成 html 样本的示例:

1
2
3
<html> = <lt>html<gt><head><body><lt>/html<gt>
<head> = <lt>head<gt>...<lt>/head<gt>
<body> = <lt>body<gt>...<lt>/body<gt>

由于 ‘<’ 和 ‘>’ 在语法文件中会用到,因此我们在这里使用<lt>and<gt>代替。这些符号是内置的,不需要用户定义。所有内置符号的列表如下:

内置符号

以下符号具有特殊含义,用户不应重新定义:

  • <lt> - ‘<’ 字符
  • <gt> - ‘>’ 字符
  • <hash> - ‘#’ 特点
  • <cr> - CR 字符
  • <lf> - LF 字符
  • <space> - 空格字符
  • <tab> - 制表符
  • <ex>- ‘!特点
  • <char>- 可用于使用 ‘code’ 属性生成任意 ASCII 字符。例如<char code=97>对应于’a’。如果未指定,则生成随机字符。支持“min”和“max”属性。
  • <hex> - 生成一个随机的十六进制数字。
  • <int>, <int 8>, <uint8>, <int16>, <uint16>, <int32>, <uint32>, <int64>, <uint64>- 可用于生成随机整数。支持 ‘min’ 和 ‘max’ 属性,可用于限制将生成的整数范围。支持 ‘b’ 和 ‘be’ 属性,这使得输出二进制文件为小/大端格式,而不是文本输出。
  • <float>, <double>- 生成一个随机浮点数。支持 ‘min’ 和 ‘max’ 属性(如果未指定,则为 0 和 1)。支持使输出二进制的“b”属性。
  • <string>- 生成一个随机字符串。支持控制生成的最小和最大字符代码的“min”和“max”属性以及控制字符串长度的“minlength”和“maxlength”属性。
  • <htmlsafestring>- 与<string>除了 HTML 元字符将被转义外相同,从而可以安全地将字符串作为 HTML 文本或属性值的一部分嵌入。
  • <lines>- 输出给定数量(通过 ‘count’ 属性)的代码行。例如,请参阅有关生成编程语言代码的部分。
  • <import> - 从另一个语法导入符号,请参阅包含外部语法的部分了解详细信息。
  • <call>- 调用与函数属性相对应的用户定义函数。有关详细信息,请参阅有关在语法中包含 Python 代码的部分。
符号属性

支持以下属性:

  • root - 将符号标记为语法的根符号。唯一支持的值为“true”。调用 GenerateSymbol() 时,如果未指定参数,将生成根符号。
  • nonrecursive - 向生成器提示此规则不包含递归循环并用于防止无限递归。唯一支持的值为“true”。
  • new - 在生成编程语言时用于表示此处创建了一个新变量,而不是像往常一样扩展符号。唯一支持的值为“true”。
  • from, 符号 - 从其他语法导入符号时使用,请参阅“包括外部语法”部分。
  • count - 用于行符号以指定要创建的行数。
  • id - 用于标记多个符号应该共享相同的值。例如,在规则中,‘doSomething(<int id=1>, <int id=1>)’两个整数最终将具有相同的值。实际上只有第一个实例被扩展,第二个只是从第一个实例复制而来。
  • min, max - 用于生成数字类型以指定最小值和最大值。也用于限制字符串中生成的字符集。
  • b, be - 在数字类型中用于指定二进制小端 (‘b’) 或大端 (‘be’) 输出。
  • code - 用于字符符号以指定要由其代码输出的确切字符。
  • minlength, maxlength - 在生成字符串时用于指定最小和最大长度。
  • up - 在十六进制符号中用于指定大写输出(小写是默认值)。
  • function - 在<call>符号中使用,有关更多信息,请参阅“包括 Python 代码”部分。
  • beforeoutput - 用于调用用户指定的函数,请参阅“包括 Python”。

待改进的点

  1. 最好结合目前主流的基于覆盖引导的模糊测试技术来增加发现crash的概率(作者在他的博客上po上了相关的尝试,但是效果不太理想),作者的思路是目前比较主流的基于覆盖引导的fuzzer常用的思路,没有发现新的崩溃可能是由于对变异策略等方向的实现出现了问题(也就是作者结合的可能不够好,这块一定是一个可搞的点,不过有个问题就是很多的基于覆盖引导的fuzzer在开始fuzz前会给fuzzer提供语料库,并对其进行精简,而domato生成的样本又比较大,如何平衡这种代码覆盖率和效率的问题是非常关键的一点)。
  2. 目前官方工具里面没有对主流浏览器进行自动化测试的工具(ps:虽然这个功能比较简单)

源码分析

generator.py

首先判断参数,决定生成单个文件还是多个文件,都是调用generate_samples来生成文件,区别在于生成多少个。生成文件的命名格式,如果是多个,则按照fuzz-xxxxx.html递增。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if args.file:
generate_samples(template, [args.file]) # 可见generate_samples函数为生成样本的核心函数

elif args.output_dir:
if not args.no_of_files:
print("Please use switch -n to specify the number of files")
else:
...
outfiles = []
for i in range(nsamples):
outfiles.append(os.path.join(out_dir, 'fuzz-' + str(i).zfill(5) + '.html'))
generate_samples(template, outfiles)
else:
parser.print_help()

在generate_samples,调用了Grammar.py的库,解析了三个txt文件作为文法,并把css的文法namespace导入到html和js的文法namespace中。这里的parse_from_file函数后续要跟入Grammar.py去查看实现。最后调用了generate_new_sample去完成实际的html文件生成的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def generate_samples(template, outfiles):
"""该函数主要为了生成样本和写文件
Args:
template: 生成样本的模板,传入generate_new_sample函数中使用。
outfiles: 输出文件名的列表
"""

grammar_dir = os.path.join(os.path.dirname(__file__), 'rules')
htmlgrammar = Grammar()

err = htmlgrammar.parse_from_file(os.path.join(grammar_dir, 'html.txt'))
# CheckGrammar(htmlgrammar)
cssgrammar = Grammar()
err = cssgrammar.parse_from_file(os.path.join(grammar_dir, 'css.txt'))
# CheckGrammar(cssgrammar)
jsgrammar = Grammar()
err = jsgrammar.parse_from_file(os.path.join(grammar_dir, 'js.txt'))
# CheckGrammar(jsgrammar)

# JS and HTML grammar need access to CSS grammar.
# Add it as import
htmlgrammar.add_import('cssgrammar', cssgrammar)
jsgrammar.add_import('cssgrammar', cssgrammar)

for outfile in outfiles:
result = generate_new_sample(template, htmlgrammar, cssgrammar, jsgrammar)
#writefile

这里需要注意的是generate_symbol函数,还有_generate_code函数,_generate_code在generate_function_body函数中,用来生成js代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# generate_symbol 函数
# 先生成了html和css的结果,定义了template文件,方便将随机化生成的html,css,js放入特定的模板中进行排布,
def generate_new_sample(template, htmlgrammar, cssgrammar, jsgrammar):
"""从字符串中解析grammar规则
Args:
template: 是一个html文件,利用随机生成的html,js,css替换模板文件中的uzzer>,<jsfuzzer>,<cssfuzzer>
htmlgrammar: grammar生成的html代码
cssgrammar: grammar生成的css代码
jsgrammar: grammar生成的js代码
Returns:
最终的样本代码,html格式
"""

result = template

css = cssgrammar.generate_symbol('rules')
html = htmlgrammar.generate_symbol('bodyelements')

htmlctx = {
'htmlvars': [],
'htmlvarctr': 0,
'svgvarctr': 0,
'htmlvargen': ''
}
html = re.sub(
r'<[a-zA-Z0-9_-]+ ', # 从html_tags.py与svg_tags.py文件去匹配match与tag的对应关系
lambda match: add_html_ids(match, htmlctx), # lambda 匿名函数表达式,可以直接作为函数使用,也更方便了函数的嵌套
html
)

generate_html_elements(htmlctx, _N_ADDITIONAL_HTMLVARS) # 默认为htmlctx加了5个随机的_HTML_TYPES,但没改变变量html

result = result.replace('<cssfuzzer>', css)
result = result.replace('<htmlfuzzer>', html)

handlers = False
while '<jsfuzzer>' in result:
numlines = _N_MAIN_LINES
if handlers:
numlines = _N_EVENTHANDLER_LINES
else:
handlers = True
result = result.replace(
'<jsfuzzer>',
generate_function_body(jsgrammar, htmlctx, numlines), # 通过生成js代码并替换result来实现最后的替换。
1
)

return result

add_html_ids函数实现,该函数会判断随机生成的标签属于html还是svg,之后会对html_ctx赋值。目的是为了统计之前生成的html标签和svg标签计数。并将id加到标签中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def add_html_ids(matchobj, ctx):
tagname = matchobj.group(0)[1:-1]
if tagname in _HTML_TYPES:
ctx['htmlvarctr'] += 1
varname = 'htmlvar%05d' % ctx['htmlvarctr']
ctx['htmlvars'].append({'name': varname, 'type': _HTML_TYPES[tagname]})
ctx['htmlvargen'] += '/* newvar{' + varname + ':' + _HTML_TYPES[
tagname] + '} */ var ' + varname + ' = document.getElementById(\"' + varname + '\"); //' + _HTML_TYPES[
tagname] + '\n'
return matchobj.group(0) + 'id=\"' + varname + '\" '
elif tagname in _SVG_TYPES:
ctx['svgvarctr'] += 1
varname = 'svgvar%05d' % ctx['svgvarctr']
ctx['htmlvars'].append({'name': varname, 'type': _SVG_TYPES[tagname]})
ctx['htmlvargen'] += '/* newvar{' + varname + ':' + _SVG_TYPES[
tagname] + '} */ var ' + varname + ' = document.getElementById(\"' + varname + '\"); //' + _SVG_TYPES[
tagname] + '\n'
return matchobj.group(0) + 'id=\"' + varname + '\" '
else:
return matchobj.group(0)

generate_html_elements这个函数比较简单,添加了n行html元素到html_ctx中,默认为5,可以通过修改代码进行更改

1
2
3
4
5
6
7
8
9
def generate_html_elements(ctx, n):
for i in range(n):
tag = random.choice(list(_HTML_TYPES))
tagtype = _HTML_TYPES[tag]
ctx['htmlvarctr'] += 1
varname = 'htmlvar%05d' % ctx['htmlvarctr']
ctx['htmlvars'].append({'name': varname, 'type': tagtype})
ctx[
'htmlvargen'] += '/* newvar{' + varname + ':' + tagtype + '} */ var ' + varname + ' = document.createElement(\"' + tag + '\"); //' + tagtype + '\n'

generate_function_body这个函数用来生成js代码,前面是一段模板式的代码,后面调用_generate_code生成随机的js代码,可以指定行数。这个函数上面也说过,最关键的在于_generate_code函数的逻辑。

1
2
3
4
5
6
7
8
9
10
def generate_function_body(jsgrammar, htmlctx, num_lines):
js = ''
js += 'var fuzzervars = {};\n\n'
js += "SetVariable(fuzzervars, window, 'Window');\nSetVariable(fuzzervars, document, 'Document');\nSetVariable(fuzzervars, document.body.firstChild, 'Element');\n\n"
js += '//beginjs\n'
js += htmlctx['htmlvargen']
js += jsgrammar._generate_code(num_lines, htmlctx['htmlvars'])
js += '\n//endjs\n'
js += 'var fuzzervars = {};\nfreememory()\n'
return js

generate_new_sample函数调试跟踪。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# html变量某次结果
<iframe longdesc="^r{=g$3-&#x27;f&gt;}N7" is="x-rect" srcdoc="!m!VM)u" frameborder="1" srcdoc="gPd80b6D0g" style="scroll-snap-points-y: inherit; mso-font-kerning: 6pt; border-bottom-left-radius: -1px; border-right-style: hidden; overflow-y: auto" download="^&lt;" formnovalidate="formnovalidate" srcset="AJZ=1&#x27;btpN&#x27;=" formmethod="get">t;3qg</iframe>
<canvas dir="rtl" style="clip-path: url(#htmlvar00006); grid-template-areas: 'a'; overflow-wrap: break-word; width: 1pt; tab-size: 0" style="stroke-dashoffset: 30px; word-spacing: 0ex; stroke-linecap: round; stroke-opacity: 1; column-break-before: always" style="-webkit-margin-collapse: collapse separate; font-feature-settings: 'liga' 1; hyphens: auto; border-radius: -1 0 0px 42; border-bottom-right-radius: 0px" name="\uJItGlTNA" seed="0" behavior="scroll" autoload="autoload" face="Arial,helvetica" marginwidth="0">|Z l</canvas>
...


# add_html_ids处理后后htmlctx的结果
{'htmlvars': [{'name': 'htmlvar00001', 'type': 'HTMLIFrameElement'}, {'name': 'htmlvar00002', 'type': 'HTMLCanvasElement'}...],
'htmlvarctr': 20,
'svgvarctr': 0,
'htmlvargen': '/* newvar{htmlvar00001:HTMLIFrameElement} */ var htmlvar00001 = document.getElementById("htmlvar00001"); //HTMLIFrameElement\n/* newvar{htmlvar00002:HTMLCanvasElement} */ var htmlvar00002 = document.getElementById("htmlvar00002"); //HTMLCanvasElement\n ...'}

# htm的结果,在标签后面加上了id
<iframe id="htmlvar00001" longdesc="^r{=g$3-&#x27;f&gt;}N7" is="x-rect" srcdoc="!m!VM)u" frameborder="1" srcdoc="gPd80b6D0g" style="scroll-snap-points-y: inherit; mso-font-kerning: 6pt; border-bottom-left-radius: -1px; border-right-style: hidden; overflow-y: auto" download="^&lt;" formnovalidate="formnovalidate" srcset="AJZ=1&#x27;btpN&#x27;=" formmethod="get">t;3qg</iframe>
<canvas id="htmlvar00002" dir="rtl" style="clip-path: url(#htmlvar00006); grid-template-areas: 'a'; overflow-wrap: break-word; width: 1pt; tab-size: 0" style="stroke-dashoffset: 30px; word-spacing: 0ex; stroke-linecap: round; stroke-opacity: 1; column-break-before: always" style="-webkit-margin-collapse: collapse separate; font-feature-settings: 'liga' 1; hyphens: auto; border-radius: -1 0 0px 42; border-bottom-right-radius: 0px" name="\uJItGlTNA" seed="0" behavior="scroll" autoload="autoload" face="Arial,helvetica" marginwidth="0">|Z l</canvas>
...

# generate_html_elements处理后htmlctx的结果:htmlvars和htmlvargen后面都又增加了5个元素。但是该函数没有更新html变量。
{'htmlvars': [{'name': 'htmlvar00001', 'type': 'HTMLIFrameElement'}, {'name': 'htmlvar00002', 'type': 'HTMLCanvasElement'}...],
'htmlvarctr': 25,
'svgvarctr': 0,
'htmlvargen': '/* newvar{htmlvar00001:HTMLIFrameElement} */ var htmlvar00001 = document.getElementById("htmlvar00001"); //HTMLIFrameElement\n/* newvar{htmlvar00002:HTMLCanvasElement} */ var htmlvar00002 = document.getElementById("htmlvar00002"); //HTMLCanvasElement\n ...'}

经过上面的过程,可以将我们给定的模板进行替换,模板大致如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<!-- saved from url=(0014)about:internet -->
<html>
<head>
<style>
/*begincss*/
<cssfuzzer>
/*endcss*/
</style>
<script>

function freememory() {
try { CollectGarbage(); } catch(err) { }
try { FuzzingFunctions.garbageCollect(); } catch(err) { }
try { FuzzingFunctions.cycleCollect(); } catch(err) { }
try { window.gc(); } catch(err) { }
}

var runcount = {'jsfuzzer':0, 'eventhandler1':0, 'eventhandler2':0, 'eventhandler3':0, 'eventhandler4':0, 'eventhandler5':0}

function GetVariable(fuzzervars, var_type) { if(fuzzervars[var_type]) { return fuzzervars[var_type]; } else { return null; }}

function SetVariable(fuzzervars, var_name, var_type) { fuzzervars[var_type] = var_name; }

function jsfuzzer() {

runcount["jsfuzzer"]++; if(runcount["jsfuzzer"] > 2) { return; }

<jsfuzzer>

}

function eventhandler1() {

runcount["eventhandler1"]++; if(runcount["eventhandler1"] > 2) { return; }

<jsfuzzer>

}

function eventhandler2() {

runcount["eventhandler2"]++; if(runcount["eventhandler2"] > 2) { return; }

<jsfuzzer>

}

function eventhandler3() {

runcount["eventhandler3"]++; if(runcount["eventhandler3"] > 2) { return; }

<jsfuzzer>

}

function eventhandler4() {

runcount["eventhandler4"]++; if(runcount["eventhandler4"] > 2) { return; }

<jsfuzzer>

}

function eventhandler5() {

runcount["eventhandler5"]++; if(runcount["eventhandler5"] > 2) { return; }

<jsfuzzer>

}

</script>
</head>
<body onload=jsfuzzer()>
<!--beginhtml--><htmlfuzzer><!--endhtml-->
</body>
</html>

分别将在对应的位置进行替换,只要保证模板语法正确,生成的html,css,js语法正确,并可以通过js成功与DOM元素进行交互,在对变量的边界值做个界定,这样一个生成各个html样本的生成器就完成了,当然generator.py这个文件只是个壳子,真正的功能点在grammar.py实现,还有我们自定义的语法文件,我们可以根据自己的需要定制化规则和代码。

关键函数的流程大致如下,

image-20220117201908982

grammar.py

上面我们有几块代码没有跟,分别是generate_samples函数调用的parse_from_file,generate_new_sample函数调用的generate_symbol还有generate_function_body函数调用的_generate_code。在上面对代码分析时也已经大概了解了这三个函数相关的功能了

parse_from_file:从文件中解析语法并生成一个操作”类”,可以使用该”类”进行一些函数操作。

generate_symbol:使用上面的操作”类”生成随机的html和css代码。

_generate_code:使用上面的操作”类”生成随机的js代码。

最开始还有一个类的定义,Grammar(),我们先看下Grammar类的代码

Grammar

这个类接近1000行代码,就不贴了,其实grammar.py文件主要的逻辑都在里面,所以我们研究那三个函数即可。这里贴出定义结构体的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class Grammar(object):
# 接近1000行
def __init__(self):
self._root = ''
self._creators = {}
self._nonrecursive_creators = {}
self._all_rules = []
self._interesting_lines = {}
self._all_nonhelper_lines = []

self._creator_cdfs = {}
self._nonrecursivecreator_cdfs = {}

self._var_format = 'var%05d'

self._definitions_dir = '.'

self._imports = {}

self._functions = {}

self._line_guard = ''

self._recursion_max = 50
self._var_reuse_prob = 0.75
self._interesting_line_prob = 0.9
self._max_vars_of_same_type = 5

self._inheritance = {}

self._cssgrammar = None

# Helper dictionaries for creating built-in types.
self._constant_types = {
'lt': '<',
'gt': '>',
'hash': '#',
'cr': chr(13),
'lf': chr(10),
'space': ' ',
'tab': chr(9),
'ex': '!'
}

self._built_in_types = {
'int': self._generate_int,
'int32': self._generate_int,
'uint32': self._generate_int,
'int8': self._generate_int,
'uint8': self._generate_int,
'int16': self._generate_int,
'uint16': self._generate_int,
'int64': self._generate_int,
'uint64': self._generate_int,
'float': self._generate_float,
'double': self._generate_float,
'char': self._generate_char,
'string': self._generate_string,
'htmlsafestring': self._generate_html_string,
'hex': self._generate_hex,
'import': self._generate_import,
'lines': self._generate_lines
}

self._command_handlers = {
'varformat': self._set_variable_format,
'include': self._include_from_file,
'import': self._import_grammar,
'lineguard': self._set_line_guard,
'max_recursion': self._set_recursion_depth,
'var_reuse_prob': self._set_var_reuse_probability,
'extends': self._set_extends
...

parse_from_file

parse_from_file函数并没有做太多的工作,读取了传入的语法文件,并将内容放入content中,之后调用parse_from_string

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  def parse_from_file(self, filename):
"""从文件中解析语法

打开一个txt文件,解析并加载语法规则,语法详情查看readme。
import可以使语法文件包含其他语法文件。

Args:
filename: 语法文件绝对路径

Returns:
错误号,便于排查错误。
"""
...
return self.parse_from_string(content) # 使用content变量将文件内容存储起来。
parse_from_string

该函数相当于解析的主函数。

使用了_include_from_string将文件中的语法放入其对应的grammar中,这个grammar在generator.py中为htmlgrammar,cssgrammar,jsgrammar。

_normalize_probabilities会统计语法中出现的概率,放入self中的_creator_cdfs_nonrecursivecreator_cdfs字段中。

_compute_interesting_indices会统计js代码中“有趣”的行,并在之后生成js随机代码中出现更高的频率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def parse_from_string(self, grammar_str):
"""从字符串中解析语法

将字符串按行解析并加载语法规则。语法详情查看readme。

Args:
grammar_str: 从与反文件中读取的语法,以字符串格式进行存储。

Returns:
错误号,便于排查错误。
"""
errors = self._include_from_string(grammar_str) # 将语法从grammar_str放入self中
if errors:
return errors

self._normalize_probabilities() # 统计语法中出现的概率,放入self中的某个字段中
self._compute_interesting_indices() # 在传入文件为js.txt时第一次起作用。该函数会判断每行是否有<line>,而js.txt中存在
# !lineguard try { <line> } catch(e) { }
# 会让js.txt每行都尝试加入一个<line>,从而触发该函数

return 0
_include_from_string

跟入函数,该函数为语法存储的关键函数。首先识别每行是否为注释,如果为注释则不处理,之后匹配是否为预定义命令,比如!include等等,如果识别到则交由对应的函数进行处理。否则就使用_parse_grammar_line处理每行代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
def _include_from_string(self, grammar_str):
in_code = False
helper_lines = False
in_function = False
num_errors = 0
lines = grammar_str.split('\n')
for line in lines:

if not in_function:
cleanline = self._remove_comments(line) # 删除每行的注释
if not cleanline:
continue
else:
cleanline = line

# Process special commands
match = re.match(r'^!([a-z_]+)\s*(.*)$', cleanline)
if match: # 当匹配到预定义命令。 eg: '!include common.txt'
command = match.group(1) # eg: include
params = match.group(2) # eg: common.txt
"""
self._command_handlers = {
'varformat': self._set_variable_format,
'include': self._include_from_file,
'import': self._import_grammar,
'lineguard': self._set_line_guard,
'max_recursion': self._set_recursion_depth,
'var_reuse_prob': self._set_var_reuse_probability,
'extends': self._set_extends
}"""
if command in self._command_handlers: # 匹配到相关的关键词时调用对应的函数。
self._command_handlers[command](params) # eg: 'include': self._include_from_file,
elif command == 'begin' and params == 'lines':
in_code = True
helper_lines = False
elif command == 'begin' and params == 'helperlines':
in_code = True
helper_lines = True
elif command == 'end' and params in ('lines', 'helperlines'):
if in_code:
in_code = False
elif command == 'begin' and params.startswith('function'):
match = re.match(r'^function\s*([a-zA-Z._0-9]+)$', params)
if match and not in_function:
function_name = match.group(1)
function_body = ''
in_function = True
else:
print('Error parsing line ' + line)
num_errors += 1
elif command == 'end' and params == 'function':
if in_function:
in_function = False
self._save_function(function_name, function_body)
else:
print('Unknown command: ' + command)
num_errors += 1
continue

try:
if in_function:
function_body += cleanline + '\n'
elif in_code:
self._parse_code_line(cleanline, helper_lines) # 如果是code则交由该函数处理
else:
self._parse_grammar_line(cleanline) # 每行语法一般都交由该函数进行处理
except GrammarError:
print('Error parsing line ' + line)
num_errors += 1

return num_errors
_parse_grammar_line

可以看出代码逻辑主要将传入的line入self._all_rules中。也会在self._creators的对应tag_name中放入对应的元素。该函数的作用主要是将传入的一行语法录入到self的属性中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def _parse_grammar_line(self, line):
"""Parses a grammar rule."""
# Check if the line matches grammar rule pattern (<tagname> = ...).
...

# Parse the line to create a grammar rule.
...

# Splits the line into constant parts and tags. For example
# "foo<bar>baz" would be split into three parts, "foo", "bar" and "baz"
# Every other part is going to be constant and every other part
# is going to be a tag, always starting with a constant. Empty
# spaces between tags/beginning/end are not a problem because
# then empty strings will be returned in corresponding places,
# for example "<foo><bar>" gets split into "", "foo", "", "bar", ""
...

# Store the rule in appropriate sets.
create_tag_name = rule['creates']['tagname']
if create_tag_name in self._creators:
self._creators[create_tag_name].append(rule)
else:
self._creators[create_tag_name] = [rule]
if 'nonrecursive' in rule['creates']:
if create_tag_name in self._nonrecursive_creators:
self._nonrecursive_creators[create_tag_name].append(rule)
else:
self._nonrecursive_creators[create_tag_name] = [rule]
self._all_rules.append(rule)
if 'root' in rule['creates']:
self._root = create_tag_name

_parse_code_line

可以看到44-47行,会对 self._creators[‘line’]添加一些元素,这会对后面_compute_interesting_indices起作用,我们可以说如果在self._creators['line']中的元素可能是“有趣”的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def _parse_code_line(self, line, helper_lines=False):
"""Parses a rule for generating code."""
rule = {
'type': 'code',
'parts': [],
'creates': []
}
# Splits the line into constant parts and tags. For example
# "foo<bar>baz" would be split into three parts, "foo", "bar" and "baz"
# Every other part is going to be constant and every other part
# is going to be a tag, always starting with a constant. Empty
# spaces between tags/beginning/end are not a problem because
# then empty strings will be returned in corresponding places,
# for example "<foo><bar>" gets split into "", "foo", "", "bar", ""
rule_parts = re.split(r'<([^>)]*)>', line)
for i in range(0, len(rule_parts)):
if i % 2 == 0:
if rule_parts[i]:
rule['parts'].append({
'type': 'text',
'text': rule_parts[i]
})
else:
parsedtag = self._parse_tag_and_attributes(rule_parts[i])
rule['parts'].append(parsedtag)
if 'new' in parsedtag:
rule['creates'].append(parsedtag)

for tag in rule['creates']:
tag_name = tag['tagname']
if tag_name in _NONINTERESTING_TYPES:
continue
if tag_name in self._creators:
self._creators[tag_name].append(rule)
else:
self._creators[tag_name] = [rule]
if 'nonrecursive' in tag:
if tag_name in self._nonrecursive_creators:
self._nonrecursive_creators[tag_name].append(rule)
else:
self._nonrecursive_creators[tag_name] = [rule]

if not helper_lines:
if 'line' in self._creators:
self._creators['line'].append(rule)
else:
self._creators['line'] = [rule]

self._all_rules.append(rule)
_normalize_probabilities

主要就是遍历了下self._creatorsself._nonrecursive_creators,而它们在之前调用_include_from_string函数时被赋值了。遍历后调用_get_cdf来统计哪些tag使用了概率,并放入_creator_cdfs字典中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def _normalize_probabilities(self):
"""Preprocessess probabilities for production rules.

Creates CDFs (cumulative distribution functions) and normalizes
probabilities in the [0,1] range for all creators. This is a
preprocessing function that makes subsequent creator selection
based on probability easier.
"""
for symbol, creators in self._creators.items():
cdf = self._get_cdf(symbol, creators)
self._creator_cdfs[symbol] = cdf # 维护了一个字典_creator_cdfs,分别表示每个tag对应的概率分布。

for symbol, creators in self._nonrecursive_creators.items():
cdf = self._get_cdf(symbol, creators)
self._nonrecursivecreator_cdfs[symbol] = cdf

""" eg: 处理common.txt
_creator_cdfs = {'newline': [], 'interestingint': [], 'fuzzint': [], 'short': [1.0], 'boolean': [], 'percentage': [], 'elementid': [], 'svgelementid': [], 'class': [], 'color': [], 'tagname': [], 'svgtagname': [], 'imgsrc': [], 'videosrc': [], 'audiosrc': []}
_nonrecursivecreator_cdfs = {}
"""
_compute_interesting_indices

主要判断每个line是否有一些不”有趣“的特征,比如type不为texttagname不在_NONINTERESTING_TYPES中等等,这块通过黑名单的方式进行实现,后续可以通过再添加黑名单进一步优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def _compute_interesting_indices(self):
# select interesting lines for each variable type

if 'line' not in self._creators:
return

for i in range(len(self._creators['line'])):
self._all_nonhelper_lines.append(i)
rule = self._creators['line'][i]
for part in rule['parts']:
if part['type'] == 'text': # not intersting
continue
tagname = part['tagname']
if tagname in _NONINTERESTING_TYPES: # not intersting
continue
if 'new' in part: # not intersting
continue
if tagname not in self._interesting_lines:
self._interesting_lines[tagname] = []
self._interesting_lines[tagname].append(i)

image-20220118102602184

generate_symbol

generate_symbol函数之前我们猜测是随机生成html和css代码的函数。我们跟入该函数去验证我们的猜想。

函数逻辑基本定义了个结构体之后调用了_generate函数,跟入

1
2
3
4
5
6
7
8
9
def generate_symbol(self, name):
"""Expands a symbol whose name is given as an argument."""
context = {
'lastvar': 0,
'lines': [],
'variables': {},
'force_var_reuse': False
}
return self._generate(name, context, 0)
_generate

首先检查我们是否已经有给定类型的变量(对应30行位置处的if判断)。而这段代码只有在生成js代码时才会调用,所以我们后续讨论。

之后调用了_select_creator_expand_rule函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def _generate(self, symbol, context, recursion_depth=0, force_nonrecursive=False):
"""生成用户定义的symbol.

为给定符号选择规则并解析规则的右侧。

Args:
symbol: 要被解析的符号名
context: 字典包括:
'lastvar': 创建的最后一个变量的索引。
'lines': 生成的代码行。(用于编程语言生成)。
'variables': 包含迄今为止创建的所有变量名称的字典。
recursion_depth:当前递归深度。
force_nonrecursive: 是否强制只使用非递归规则。

Returns:
包含符号扩展的字符串。

Raises:
GrammarError: 如果语法描述不正确导致某些规则无法解析
RecursionError: 如果达到最大递归级别。
"""

# print symbol

# print 'Expanding ' + symbol + ' in depth ' + str(recursion_depth)

force_var_reuse = context['force_var_reuse'] # 强制变量重用

# Check if we already have a variable of the given type.
if (symbol in context['variables'] and
symbol not in _NONINTERESTING_TYPES):
# print symbol + ':' + str(len(context['variables'][symbol])) + ':' + str(force_var_reuse)
if (force_var_reuse or
random.random() < self._var_reuse_prob or
len(context['variables'][symbol]) > self._max_vars_of_same_type):
# print 'reusing existing var of type ' + symbol
context['force_var_reuse'] = False
variables = context['variables'][symbol]
return variables[random.randint(0, len(variables) - 1)]
# print 'Not reusing existing var of type ' + symbol

creator = self._select_creator(
symbol,
recursion_depth,
force_nonrecursive
)
return self._expand_rule(
symbol,
creator,
context,
recursion_depth,
force_nonrecursive
)

首先看下_select_creator的代码逻辑。

_select_creator

前面做了一些简单的判断,传入的symbol一定要被识别为已知类型,否则程序将无法生成代码(第20行);判断了递归深度不能溢出;之后根据force_nonrecursive判断是否强制非递归,并添加到不同的数组中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def _select_creator(self, symbol, recursion_depth, force_nonrecursive):
"""选择给定符号的creator。

creator基于语法中指定的概率,或者如果未指定概率则均匀分布。

Args:
symbol: 要获取其creator规则的符号的名称
recursion_depth: 当前递归深度
force_nonrecursive: 如果为 True,则仅使用标记为“非递归”的creator(如果可用)

Returns:
描述可以创建给定符号的规则的字典。

Raises:
RecursionError: 如果达到最大递归级别。
GrammarError: 如果没有创建给定类型的规则。
"""

# Do we even know how to create this type?
if symbol not in self._creators:
raise GrammarError('No creators for type ' + symbol)

if recursion_depth >= self._recursion_max:
raise RecursionError(
'Maximum recursion level reached while creating '
'object of type' + symbol
)
elif force_nonrecursive and symbol in self._nonrecursive_creators:
creators = self._nonrecursive_creators[symbol]
cdf = self._nonrecursivecreator_cdfs[symbol]
else:
creators = self._creators[symbol]
cdf = self._creator_cdfs[symbol]

if not cdf:
# Uniform distribution, faster
return creators[random.randint(0, len(creators) - 1)]

# Select a creator according to the cdf
idx = bisect.bisect_left(cdf, random.random(), 0, len(cdf))
return creators[idx]
_expand_rule

由于语法规则中有些右侧元素索引着其他元素,那么需要使用这个函数将其扩展,这样做的目的是为了语法简便。

在生成html和css代码时其实不会执行到最后的代码段,核心点在于调用_generate函数,而_generate函数也调用了该_expand_rule函数,二者存递归调用,将右侧的元素完全展开。

随机生成的结果通过ret_parts保存,最后拼接得到最终结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def _expand_rule(self, symbol, rule, context,
recursion_depth, force_nonrecursive):
"""扩展给定的规则。

遍历规则右侧的所有元素,将它们替换为它们的字符串表示或递归调用 _Generate() 以获取其他非终结符号。

参数:
符号:正在解析的符号的名称。
规则:将用于扩展符号的生产规则。
上下文:字典包括:
'lastvar':创建的最后一个变量的索引。
'lines':生成的代码行
(用于编程语言生成)。
'variables':包含所有名称的字典
到目前为止创建的变量。
recursion_depth:当前递归深度
force_nonrecursive:是否强制只使用非递归规则。

回报:
包含符号扩展的字符串。

提高:
GrammarError:如果语法描述不正确导致某些规则无法解析
RecursionError:如果达到最大递归级别。
"""
...
else:
try:
expanded = self._generate(
part['tagname'],
context,
recursion_depth + 1,
force_nonrecursive
)
except RecursionError as e:
if not force_nonrecursive:
expanded = self._generate(
part['tagname'],
context,
recursion_depth + 1,
True
)
else:
raise RecursionError(e)

if 'id' in part:
variable_ids[part['id']] = expanded

if 'beforeoutput' in part:
expanded = self._exec_function(
part['beforeoutput'],
part,
context,
expanded
)

ret_parts.append(expanded)

# Add all newly created variables to the context
additional_lines = []
for v in new_vars:
if v['type'] not in _NONINTERESTING_TYPES:
self._add_variable(v['name'], v['type'], context)
additional_lines.append("if (!" + v['name'] + ") { " + v['name'] + " = GetVariable(fuzzervars, '" + v['type'] + "'); } else { " + self._get_variable_setters(v['name'], v['type']) + " }")

# Return the result.
# In case of 'ordinary' grammar rules, return the filled rule.
# In case of code, return just the variable name
# and update the context
filed_rule = ''.join(ret_parts)
if rule['type'] == 'grammar':
return filed_rule
else:
context['lines'].append(filed_rule)
context['lines'].extend(additional_lines)
if symbol == 'line':
return filed_rule
else:
return ret_vars[random.randint(0, len(ret_vars) - 1)]

image-20220119112246373

_generate_code

_generate_code

调用了_add_variable_expand_rule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def _generate_code(self, num_lines, initial_variables=[], last_var=0):
"""Generates a given number of lines of code."""

context = {
'lastvar': last_var,
'lines': [],
'variables': {},
'interesting_lines': [],
'force_var_reuse': False
}

for v in initial_variables:
self._add_variable(v['name'], v['type'], context)
self._add_variable('document', 'Document', context)
self._add_variable('window', 'Window', context)

while len(context['lines']) < num_lines:
tmp_context = context.copy()
try:
if (random.random() < self._interesting_line_prob) and (len(tmp_context['interesting_lines']) > 0):
tmp_context['force_var_reuse'] = True
lineno = random.choice(tmp_context['interesting_lines'])
else:
lineno = random.choice(self._all_nonhelper_lines)
creator = self._creators['line'][lineno]
self._expand_rule('line', creator, tmp_context, 0, False)
context = tmp_context
except RecursionError as e:
print('Warning: ' + str(e))
if not self._line_guard:
guarded_lines = context['lines']
else:
guarded_lines = []
for line in context['lines']:
guarded_lines.append(self._line_guard.replace('<line>', line))
return '\n'.join(guarded_lines)
_add_variable

更新_interesting_lines,更新context[‘variables’][var_type],递归处理self._inheritance[var_type]的元素。

1
2
3
4
5
6
7
8
9
10
11
12
def _add_variable(self, var_name, var_type, context):
if var_type not in context['variables']:
context['variables'][var_type] = []
if var_type in self._interesting_lines:
set1 = set(context['interesting_lines'])
set2 = set(self._interesting_lines[var_type])
new_interesting = set2 - set1
context['interesting_lines'] += list(new_interesting)
context['variables'][var_type].append(var_name)
if var_type in self._inheritance:
for parent_type in self._inheritance[var_type]:
self._add_variable(var_name, parent_type, context)
_expand_rule

该函数上面提到过,不多说了。

image-20220119171957132

总结

总的来说,domato是一python开发的基于生成的DOM fuzzer。其核心在于给定语法规则生成随机的html文件。严格来说它并没有实现喂数据并检测崩溃的功能,可以把它当作一个生成器。

domato有两个主要的功能py文件 generator.py 和 grammar.py ,generator.py相当于外壳文件,用于初始化模板文件,并将生成的数据进行处理,最终生成一个样本文件;grammar.py 则相当于一个随机生成前端代码的库,通过读取特定的语法规则,来随机生成html,css,js代码,而这个语法文件通过txt文件进行维护,我们可以随时向语法文件添加新的特性来支持工具生成最新语法特性的代码。

该工具简单易用,可以方便对pdf js引擎,浏览器,js语法引擎等等场景进行fuzz,在问世之初便拿到很多CVE.

但是该工具最大的弊端在于没有与基于代码覆盖率引导的技术相结合,导致数据只是漫无目的生成,发现漏洞的随机性比较高,但是和代码覆盖引导结合有些问题

  1. 由于大的样本会对fuzz效率产生比较大的影响,所以在传统的基于覆盖引导技术的fuzz过程中一般会对样本进行精简(也就是语料库蒸馏),一般domato生成的html文件比较大,而拿afl-tmin来说处理一个大文件的时间是很长的,而成百上千的大文件语料库蒸馏就是一个比较大的问题,如何在保证语料库蒸馏效果的基础上提高语料库蒸馏的效率。而不至于语料库蒸馏的时间比真正fuzz的时间还要更长。
  2. 由于domato只实现了生成语料库的功能,我们需要自行实现自动化漏洞类型识别,挂机重启,数据处理,日志处理等等问题。
  3. 上面提到了语料库蒸馏的问题,还有关键的问题在于基于覆盖引导的dom fuzzer怎样实现,作者之前实现了该功能,但是效果并不理想,可见需要正确的编码和思想才可以充分利用基于覆盖引导的优势,否则只会适得其反。

参考链接

  1. 官方github
  2. 官方blog
  3. damato配合bugid自动化进行fuzz与分类
  4. 使用domato对pdf解析阅读器进行fuzz
  5. 将 AFL 布隆过滤器添加到 Domato 以获得更多崩溃
  6. Domato Fuzzer 的生成引擎内部结构
  7. 使用Domato去fuzz php解析引擎

TODO

  1. 将AFL布隆过滤器与domato结合
 Comments