FuzzingBook学习 Part2-1

Fuzzing: Breaking Things with Random Inputs

主要关注字符串生成的模糊测试方法,将生成的字符串输入程序,来测试是否会出现crash

FuzzingBook库使用#

Fuzzer#

1
2
3
4
>>> from fuzzingbook.Fuzzer import RandomFuzzer
>>> random_fuzzer = RandomFuzzer()
>>> random_fuzzer.fuzz()
'%$<1&<%+=!"83?+)9:++9138 42/ "7;0-,)06 "1(2;6>?99$%7!!*#96=>2&-/(5*)=$;0$$+;<12"?30&'

RandomFuzzer是利用PythonRandom模块生成字符串的类

主要参数如下:

  • min_length:最小长度
  • max_length:最大长度
  • char_start:字符起点
  • char_range:字符范围

Produce strings of min_length to max_length characters in the range [char_start, char_start + char_range]

Runner#

有了Fuzzer生成的字符串,我们可以结合Runner进行模糊测试

  • PrintRunner:将字符串打印出来
1
2
3
4
5
6
>>> from fuzzingbook.Fuzzer import PrintRunner
>>> print_runner = PrintRunner()
>>> random_fuzzer.run(print_runner)
EQYGAXPTVPJGTYHXFJ

('EQYGAXPTVPJGTYHXFJ', 'UNRESOLVED')
  • ProgramRunner:将字符串喂给程序
1
2
3
4
>>> cat = ProgramRunner('cat')
>>> random_fuzzer.run(cat)
(CompletedProcess(args='cat', returncode=0, stdout='BZOQTXFBTEOVYX', stderr=''),
'PASS')

自建Fuzzer#

使用Python的Random库自己写一个Fuzzer

1
2
3
4
5
6
7
8
9
import random
def fuzzer(min_length=0, max_length=100, char_start=32, char_range=32):
"""A string of up to `max_length` characters
in the range [`char_start`, `char_start` + `char_range`]"""
string_length = random.randrange(min_length, max_length + 1)
out = ""
for i in range(0, string_length):
out += chr(random.randrange(char_start, char_start + char_range))
return out

Fuzz外部程序#

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
import os
import random
import tempfile
import subprocess

def fuzzer(min_length=0, max_length=100, char_start=32, char_range=32):
"""A string of up to `max_length` characters
in the range [`char_start`, `char_start` + `char_range`]"""
string_length = random.randrange(min_length, max_length + 1)
out = ""
for i in range(0, string_length):
out += chr(random.randrange(char_start, char_start + char_range))
return out

def tmpfile():
basename = "input.txt"
tempdir = tempfile.mkdtemp()
FILE = os.path.join(tempdir, basename)
print(FILE)
return FILE

def fuzz(num = 100):
data = fuzzer()
FILE = tmpfile()
with open(FILE, "w") as f:
f.write(data)
runs = []
program = "bc"
for i in range(num):
data = fuzzer()
with open(FILE, "w") as f:
f.write(data)
result = subprocess.run([program, FILE],
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
runs.append((data, result))
print(sum(1 for (data, result) in runs if result.stderr == ""))
print(sum(1 for (data, result) in runs if result.returncode != 0))
errors = [(data, result) for (data, result) in runs if result.stderr != ""]
(first_data, first_result) = errors[0]
print(repr(first_data))
print(first_result.stderr)

fuzz()

这里fuzz的是bc这个命令,可以看到只出现了各种parser error,但是return code都为0,因此并没有产生bug

bc 命令是任意精度计算器语言,通常在linux下当计算器用。

找漏洞示例#

strcpy#

C语言描述:

strcpy会导致内存问题

1
2
char weekday[9]; // 8 characters + trailing '\0' terminator
strcpy (weekday, input);

使用Python模拟

1
2
3
4
def crash_if_too_long(s):
buffer = "Thursday"
if len(s) > len(buffer):
raise ValueError

Fuzz:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from fuzzingbook.ExpectError import ExpectError
import random
def fuzzer(min_length=0, max_length=100, char_start=32, char_range=32):
"""A string of up to `max_length` characters
in the range [`char_start`, `char_start` + `char_range`]"""
string_length = random.randrange(min_length, max_length + 1)
out = ""
for i in range(0, string_length):
out += chr(random.randrange(char_start, char_start + char_range))
return out

def crash_if_too_long(s):
buffer = "Thursday"
if len(s) > len(buffer):
raise ValueError

trials = 100
with ExpectError():
for i in range(trials):
s = fuzzer()
crash_if_too_long(s)

getchar死循环#

C语言描述:

1
2
while (getchar() != ' ') {
}

当getchar一直不为空时,就会出现死循环

使用Python模拟

1
2
3
4
5
6
7
def hang_if_no_space(s):
i = 0
while True:
if i < len(s):
if s[i] == ' ':
break
i += 1

Fuzz:

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
from fuzzingbook.ExpectError import ExpectTimeout

import random
def fuzzer(min_length=0, max_length=100, char_start=32, char_range=32):
"""A string of up to `max_length` characters
in the range [`char_start`, `char_start` + `char_range`]"""
string_length = random.randrange(min_length, max_length + 1)
out = ""
for i in range(0, string_length):
out += chr(random.randrange(char_start, char_start + char_range))
return out

def hang_if_no_space(s):
i = 0
while True:
if i < len(s):
if s[i] == ' ':
break
i += 1

trials = 100
with ExpectTimeout(2):
for i in range(trials):
s = fuzzer()
hang_if_no_space(s)

奇异值#

C语言描述

当读入的值很大时,会出现问题

1
2
3
4
5
6
char *read_input() {
size_t size = read_buffer_size();
char *buffer = (char *)malloc(size);
// fill buffer
return (buffer);
}

Python模拟

1
2
3
def collapse_if_too_large(s):
if int(s) > 1000:
raise ValueError

Fuzz:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from fuzzingbook.ExpectError import ExpectError
import random
def fuzzer(min_length=0, max_length=100, char_start=32, char_range=32):
"""A string of up to `max_length` characters
in the range [`char_start`, `char_start` + `char_range`]"""
string_length = random.randrange(min_length, max_length + 1)
out = ""
for i in range(0, string_length):
out += chr(random.randrange(char_start, char_start + char_range))
return out

def collapse_if_too_large(s):
if int(s) > 1000:
raise ValueError

long_number = fuzzer(0,100, ord('0'), 10)
print(long_number)
with ExpectError():
collapse_if_too_large(long_number)

异常捕获#

在C和C++中,内存错误是十分常见的,同时也有一些工具帮助我们检测内存错误,例如clang提供的一系列sanitizer

内存相关检查#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
with open("program.c", "w") as f:
f.write("""
#include <stdlib.h>
#include <string.h>

int main(int argc, char** argv) {
/* Create an array with 100 bytes, initialized with 42 */
char *buf = malloc(100);
memset(buf, 42, 100);

/* Read the N-th element, with N being the first command-line argument */
int index = atoi(argv[1]);
char val = buf[index];

/* Clean up memory so we don't leak */
free(buf);
return val;
}
""")

from fuzzingbook.bookutils import print_file
print_file("program.c")

使用clang编译

1
clang -fsanitize=address -g -o program program.c

错误检测

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
./program 110
=================================================================
==51215==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60b0000000ae at pc 0x000109f7deef bp 0x7ffee5c854d0 sp 0x7ffee5c854c8
READ of size 1 at 0x60b0000000ae thread T0
#0 0x109f7deee in main program.c:12
#1 0x7fff204f6f3c in start+0x0 (libdyld.dylib:x86_64+0x15f3c)

0x60b0000000ae is located 10 bytes to the right of 100-byte region [0x60b000000040,0x60b0000000a4)
allocated by thread T0 here:
#0 0x109fd54c0 in wrap_malloc+0xa0 (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x484c0)
#1 0x109f7de3f in main program.c:7
#2 0x7fff204f6f3c in start+0x0 (libdyld.dylib:x86_64+0x15f3c)

SUMMARY: AddressSanitizer: heap-buffer-overflow program.c:12 in main
Shadow bytes around the buggy address:
0x1c15ffffffc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1c15ffffffd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1c15ffffffe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1c15fffffff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1c1600000000: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
=>0x1c1600000010: 00 00 00 00 04[fa]fa fa fa fa fa fa fa fa fa fa
0x1c1600000020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x1c1600000030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x1c1600000040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x1c1600000050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x1c1600000060: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==51215==ABORTING
[1] 51215 abort ./program 110

信息泄露检查#

信息泄露除了逻辑漏洞外,还表现为可操控内存中存在敏感信息,例如

1
2
3
secrets = ("<space for reply>" + fuzzer(100)
+ "<secret-certificate>" + fuzzer(100)
+ "<secret-key>" + fuzzer(100) + "<other-secrets>")

同时可以加入一些字符表示未初始化内存

1
2
3
uninitialized_memory_marker = "deadbeef"
while len(secrets) < 2048:
secrets += uninitialized_memory_marker

可以定义一个类似心脏滴血漏洞的服务如下,服务会根据参数返回相关信息

1
2
3
4
5
6
7
8
9
def heartbeat(reply, length, memory):
# Store reply in memory
memory = reply + memory[len(reply):]

# Send back heartbeat
s = ""
for i in range(length):
s += memory[i]
return s

效果如下:

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
import random
def fuzzer(min_length=0, max_length=100, char_start=32, char_range=32):
"""A string of up to `max_length` characters
in the range [`char_start`, `char_start` + `char_range`]"""
string_length = random.randrange(min_length, max_length + 1)
out = ""
for i in range(0, string_length):
out += chr(random.randrange(char_start, char_start + char_range))
return out

def heartbeat(reply, length, memory):
# Store reply in memory
memory = reply + memory[len(reply):]

# Send back heartbeat
s = ""
for i in range(length):
s += memory[i]
return s

secrets = ("<space for reply>" + fuzzer(0,100)
+ "<secret-certificate>" + fuzzer(0,100)
+ "<secret-key>" + fuzzer(0,100) + "<other-secrets>")
uninitialized_memory_marker = "deadbeef"
while len(secrets) < 2048:
secrets += uninitialized_memory_marker

print(heartbeat("potato", 6, memory=secrets))
print(heartbeat("bird", 4, memory=secrets))
print(heartbeat("hat", 500, memory=secrets))
'''
potato
bird
hatace for reply>9,,+3/=5($(5-$-:$(>/2+6-;/&.1=61%8.8)=+"5#:3>-/0339:9 90<77*%+:!&47;9 ":722<02-9<0*/4:$6)'92,, "<secret-certificate>'$678-+'>?<secret-key>'=26#:$45)'(9,$0;+:&!??4/5)16)03/<)$5:#3>=(:.=/.97:62/>'>3%;8?068#".#$4<other-secrets>deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdea
'''

对于这类信息泄露漏洞,我们可以这样检测它

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
from fuzzingbook.ExpectError import ExpectError
import random
def fuzzer(min_length=0, max_length=100, char_start=32, char_range=32):
"""A string of up to `max_length` characters
in the range [`char_start`, `char_start` + `char_range`]"""
string_length = random.randrange(min_length, max_length + 1)
out = ""
for i in range(0, string_length):
out += chr(random.randrange(char_start, char_start + char_range))
return out

def heartbeat(reply, length, memory):
# Store reply in memory
memory = reply + memory[len(reply):]

# Send back heartbeat
s = ""
for i in range(length):
s += memory[i]
return s

secrets = ("<space for reply>" + fuzzer(0,100)
+ "<secret-certificate>" + fuzzer(0,100)
+ "<secret-key>" + fuzzer(0,100) + "<other-secrets>")
uninitialized_memory_marker = "deadbeef"
while len(secrets) < 2048:
secrets += uninitialized_memory_marker

with ExpectError():
for i in range(10):
s = heartbeat(fuzzer(), random.randint(1, 500), memory=secrets)
assert not s.find(uninitialized_memory_marker)
assert not s.find("secret")
'''
Traceback (most recent call last):
File "exp.py", line 32, in <module>
assert not s.find(uninitialized_memory_marker)
AssertionError (expected)
'''

Fuzzing 架构#

Runner类#

首先我们需要的的Runner基类,它定义了程序的返回状态

  • PASS:运行结果正确
  • FAIL:运行结果失败
  • UNRESOLVED:既不是正确也不是失败,一般为验证出错的正常退出
1
2
3
4
5
6
7
8
9
10
11
12
13
class Runner(object):
# Test outcomes
PASS = "PASS"
FAIL = "FAIL"
UNRESOLVED = "UNRESOLVED"

def __init__(self):
"""Initialize"""
pass

def run(self, inp):
"""Run the runner with the given input"""
return (inp, Runner.UNRESOLVED)

后续的各种Runner类都可以在Runner这个基类的基础上进行扩展,例如一个只输出输入的PrintRunner如下:

1
2
3
4
5
6
7
8
class PrintRunner(Runner):
def run(self, inp):
"""Print the given input"""
print(inp)
return (inp, Runner.UNRESOLVED)

p = PrintRunner()
(result, outcome) = p.run("Some input")

对于一般的程序,我们通常将输入喂给程序并执行来进行Fuzz,因此我们可以实现ProgramRunner如下:

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
import subporcess
class ProgramRunner(Runner):
def __init__(self, program):
"""Initialize. `program` is a program spec as passed to `subprocess.run()`"""
self.program = program

def run_process(self, inp=""):
"""Run the program with `inp` as input. Return result of `subprocess.run()`."""
return subprocess.run(self.program,
input=inp,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)

def run(self, inp=""):
"""Run the program with `inp` as input. Return test outcome based on result of `subprocess.run()`."""
result = self.run_process(inp)

if result.returncode == 0:
outcome = self.PASS
elif result.returncode < 0:
outcome = self.FAIL
else:
outcome = self.UNRESOLVED

return (result, outcome)

如果输入是不可见字符,我们可以修改如下:

1
2
3
4
5
6
7
class BinaryProgramRunner(ProgramRunner):
def run_process(self, inp=""):
"""Run the program with `inp` as input. Return result of `subprocess.run()`."""
return subprocess.run(self.program,
input=inp.encode(),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)

Fuzzer类#

Fuzzer类主要用于生成输入的测试用例并执行测试,我们还是先定义一个Fuzzer基类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Fuzzer(object):
def __init__(self):
pass

def fuzz(self):
"""Return fuzz input"""
return ""

def run(self, runner=Runner()):
"""Run `runner` with fuzz input"""
return runner.run(self.fuzz())

def runs(self, runner=PrintRunner(), trials=10):
"""Run `runner` with fuzz input, `trials` times"""
# Note: the list comprehension below does not invoke self.run() for subclasses
# return [self.run(runner) for i in range(trials)]
outcomes = []
for i in range(trials):
outcomes.append(self.run(runner))
return outcomes

对于RandomFuzzer,只需要继承Fuzzer类,并是吸纳对应的fuzz方法即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class RandomFuzzer(Fuzzer):
def __init__(self, min_length=10, max_length=100,
char_start=32, char_range=32):
"""Produce strings of `min_length` to `max_length` characters
in the range [`char_start`, `char_start` + `char_range`]"""
self.min_length = min_length
self.max_length = max_length
self.char_start = char_start
self.char_range = char_range

def fuzz(self):
string_length = random.randrange(self.min_length, self.max_length + 1)
out = ""
for i in range(0, string_length):
out += chr(random.randrange(self.char_start,
self.char_start + self.char_range))
return out

使用如下:

1
2
3
random_fuzzer = RandomFuzzer(min_length=20, max_length=20)
for i in range(10):
print(random_fuzzer.fuzz())

Fuzz Cat#

结合Runner和Fuzzer,可以fuzz cat程序,如下:

1
random_fuzzer.run(cat)

评论