-
-
[原创]针对 Exif 手写一个模糊测试器
-
发表于: 2024-8-28 23:09 4343
-
这其实我去年跟着视频做的一个学习笔记,今天刷看雪时看到篇文章才知道,这是来自NYU Fuzzing Talk。
我觉得是一个挺不错的fuzz入门教程。
虽然模糊测试经常发送随机或半随机数据,但完全随机的输入大多数情况下会被应用程序很早的阶段就判断为无效输入。通过对文件格式有深入的理解,我们可以确保至少部分输入是有效的,或接近有效,从而深入地测试程序的后续处理流程。
很多文件格式,例如PDF、PNG、ELF等,都有所谓的“magic值”或魔数这是文件开头的特定字节序列,用于标识文件的格式。没有正确的 magic 值程序可能会立即拒绝文件,不进一步处理。了解这些 magic 值对于构建有效的模糊输入是关键。
这就是为什么我们需要官方文档的原因
我们 fuzz 的时候需要先改好头和尾,不然可能程序都不执行
读取原始种子用于变异,然后将原始种子和变异种子拿给程序解析,去看有没有crash
这是原图
简易fuzz-读取框架
fuzz 当中首先需要读取种子内容,然后让目标程序去运行解析。首先我们需要有一个读取框架用于将目标文档用作于我们的有效输入样本。
模糊测试当中一个关键的步骤是进行样本的变异。样本变异简而言之,就是对已知的输入样本(例如一个文件或数据包)进行修改,产生新的、可能触发程序异常的输入。这是模糊测试的核心,因为它的目的就是尝试各种不同的输入,查找程序的潜在问题。
变异策略也有很多:翻转,插入,删除等
以翻转为例,我们可以选择随机翻转
然后我们对之前那个jpg进行变异得到一个新的图片,这就是我们变异样本
然后我们可以对比变异前后两张图片在二进制上的差别
magic num 就是指有些数字是特殊的,我们可以选择直接插入magic num 来更有效的进行变异
一些文件格式标志等
我们这里有一个对magic num 的初步选择
再来看看这个变异的效果
再看看二进制数据上的差别
我们可以看到数据在二进制格式上的变化比之前大得多 ,毕竟之前只有单 bit 翻转
其实正常来说,这应该是第一步
虽然收集和生成初始输入样本是必要的,但进行输入变异只是模糊测试的热身工作。真正的目的是对这些变异后的输入进行深入的测试,看看它们是否可以触发目标程序中的漏洞或错误。每当我们找到一个通过某种输入触发的问题时,我们就有了进一步了解目标程序的机会,也可能发现了一个潜在的安全风险
流程
我们选择的目标的是 Exif
Exif,全称"Exchangeable image file format”,是一种图像文件格式标准。它用于保存照片的元数据,如拍摄日期、相机型号、曝光时间、GPS位置等。
我们这里选择这个 Exif 项目
简单测试器功能:这个项目的程序就是对图片进行解析
有了目标二进制程序我们希望进一步深入自动化测试:
这一步其实实现起来的代码比较简单
在此之后我们还可以利用正则亲自尝试这些文件是否真的可以对 exif 造成 crash
有了这些可以让exif产生crash的样本后,我们就需要定位追踪到具体是那部分产生的crash,这时我们就可以 AddressSanitizer 工具
AddressSanitizer 工作原理
检测的错误类型
同时我们需要在编译时也采取些措施
我们此时用新编译出来的 exifsan 去执行我们有效变异的 jpg就可以看到触发报错的位置
最后我们就可以定位到出问题的那个文件或者gdb调试core看看是到底是出了什么问题
https://www.youtube.com/watch?v=SngK4W4tVc0
import
sys
# read bytes from our valid JPEG and return them in a mutable bytearray
def
get_bytes(filename):
f
=
open
(filename,
"rb"
).read()
return
bytearray(f)
if
len
(sys.argv) <
2
:
print
(
"Usage: JPEGfuzz.py <valid_jpg>"
)
else
:
filename
=
sys.argv[
1
]
data
=
get_bytes(filename)
counter
=
0
for
x
in
data:
if
counter <
10
:
print
(x)
counter
+
=
1
import
sys
# read bytes from our valid JPEG and return them in a mutable bytearray
def
get_bytes(filename):
f
=
open
(filename,
"rb"
).read()
return
bytearray(f)
if
len
(sys.argv) <
2
:
print
(
"Usage: JPEGfuzz.py <valid_jpg>"
)
else
:
filename
=
sys.argv[
1
]
data
=
get_bytes(filename)
counter
=
0
for
x
in
data:
if
counter <
10
:
print
(x)
counter
+
=
1
255
0xFF
11111111
随机翻转
11011111
0xDF
233
255
0xFF
11111111
随机翻转
11011111
0xDF
233
import
sys
import
random
# read bytes from our valid JPEG and return them in a mutable bytearray
def
get_bytes(filename):
f
=
open
(filename,
"rb"
).read()
return
bytearray(f)
def
bit_flip(data):
num_of_flips
=
int
((
len
(data)
-
4
)
*
0.01
)
indexes
=
range
(
4
, (
len
(data)
-
4
))
chosen_indexes
=
[]
# iterate selecting indexes until we've hit our num_of_flips number
counter
=
0
while
counter < num_of_flips:
chosen_indexes.append(random.choice(indexes))
counter
+
=
1
for
x
in
chosen_indexes:
current
=
data[x]
current
=
(
bin
(current).replace(
"0b"
,""))
current
=
"0"
*
(
8
-
len
(current))
+
current
indexes
=
range
(
0
,
8
)
picked_index
=
random.choice(indexes)
new_number
=
[]
# our new_number list now has all the digits, example: ['1', '0', '1', '0', '1', '0', '1', '0']
for
i
in
current:
new_number.append(i)
# if the number at our randomly selected index is a 1, make it a 0, and vice versa
if
new_number[picked_index]
=
=
"1"
:
new_number[picked_index]
=
"0"
else
:
new_number[picked_index]
=
"1"
# create our new binary string of our bit-flipped number
current
=
''
for
i
in
new_number:
current
+
=
i
# convert that string to an integer
current
=
int
(current,
2
)
# change the number in our byte array to our new number we just constructed
data[x]
=
current
return
data
# create new jpg with mutated data
def
create_new(data):
f
=
open
(
"mutated.jpg"
,
"wb+"
)
f.write(data)
f.close()
if
len
(sys.argv) <
2
:
print
(
"Usage: JPEGfuzz.py <valid_jpg>"
)
else
:
filename
=
sys.argv[
1
]
data
=
get_bytes(filename)
mutated_data
=
bit_flip(data)
create_new(mutated_data)
import
sys
import
random
# read bytes from our valid JPEG and return them in a mutable bytearray
def
get_bytes(filename):
f
=
open
(filename,
"rb"
).read()
return
bytearray(f)
def
bit_flip(data):
num_of_flips
=
int
((
len
(data)
-
4
)
*
0.01
)
indexes
=
range
(
4
, (
len
(data)
-
4
))
chosen_indexes
=
[]
# iterate selecting indexes until we've hit our num_of_flips number
counter
=
0
while
counter < num_of_flips:
chosen_indexes.append(random.choice(indexes))
counter
+
=
1
for
x
in
chosen_indexes:
current
=
data[x]
current
=
(
bin
(current).replace(
"0b"
,""))
current
=
"0"
*
(
8
-
len
(current))
+
current
indexes
=
range
(
0
,
8
)
picked_index
=
random.choice(indexes)
new_number
=
[]
# our new_number list now has all the digits, example: ['1', '0', '1', '0', '1', '0', '1', '0']
for
i
in
current:
new_number.append(i)
# if the number at our randomly selected index is a 1, make it a 0, and vice versa
if
new_number[picked_index]
=
=
"1"
:
new_number[picked_index]
=
"0"
else
:
new_number[picked_index]
=
"1"
# create our new binary string of our bit-flipped number
current
=
''
for
i
in
new_number:
current
+
=
i
# convert that string to an integer
current
=
int
(current,
2
)
# change the number in our byte array to our new number we just constructed
data[x]
=
current
return
data
# create new jpg with mutated data
def
create_new(data):
f
=
open
(
"mutated.jpg"
,
"wb+"
)
f.write(data)
f.close()
if
len
(sys.argv) <
2
:
print
(
"Usage: JPEGfuzz.py <valid_jpg>"
)
else
:
filename
=
sys.argv[
1
]
data
=
get_bytes(filename)
mutated_data
=
bit_flip(data)
create_new(mutated_data)
0xFF
0x7F
0x00
0xFFFF
0x0000
0xFFFFFFFF
0x00000000
0x80000000
<
-
-
minimum
32
-
bit
int
0x40000000
<
-
-
just half of that amount
Ox7FFFFFFF <
-
-
max
32
-
bit
int
0xFF
0x7F
0x00
0xFFFF
0x0000
0xFFFFFFFF
0x00000000
0x80000000
<
-
-
minimum
32
-
bit
int
0x40000000
<
-
-
just half of that amount
Ox7FFFFFFF <
-
-
max
32
-
bit
int
import
sys
import
random
# read bytes from our valid JPEG and return them in a mutable bytearray
def
get_bytes(filename):
f
=
open
(filename,
"rb"
).read()
return
bytearray(f)
def
bit_flip(data):
num_of_flips
=
int
((
len
(data)
-
4
)
*
0.01
)
indexes
=
range
(
4
, (
len
(data)
-
4
))
chosen_indexes
=
[]
# iterate selecting indexes until we've hit our num_of_flips number
counter
=
0
while
counter < num_of_flips:
chosen_indexes.append(random.choice(indexes))
counter
+
=
1
for
x
in
chosen_indexes:
current
=
data[x]
current
=
(
bin
(current).replace(
"0b"
,""))
current
=
"0"
*
(
8
-
len
(current))
+
current
indexes
=
range
(
0
,
8
)
picked_index
=
random.choice(indexes)
new_number
=
[]
# our new_number list now has all the digits, example: ['1', '0', '1', '0', '1', '0', '1', '0']
for
i
in
current:
new_number.append(i)
# if the number at our randomly selected index is a 1, make it a 0, and vice versa
if
new_number[picked_index]
=
=
"1"
:
new_number[picked_index]
=
"0"
else
:
new_number[picked_index]
=
"1"
# create our new binary string of our bit-flipped number
current
=
''
for
i
in
new_number:
current
+
=
i
# convert that string to an integer
current
=
int
(current,
2
)
# change the number in our byte array to our new number we just constructed
data[x]
=
current
return
data
def
magic(data):
magic_vals
=
[
(
1
,
255
),
(
1
,
255
),
(
1
,
127
),
(
1
,
0
),
(
2
,
255
),
(
2
,
0
),
(
4
,
255
),
(
4
,
0
),
(
4
,
128
),
(
4
,
64
),
(
4
,
127
)
]
picked_magic
=
random.choice(magic_vals)
length
=
len
(data)
-
8
index
=
range
(
0
, length)
picked_index
=
random.choice(index)
# here we are hardcoding all the byte overwrites for all of the tuples that begin (1, )
if
picked_magic[
0
]
=
=
1
:
if
picked_magic[
1
]
=
=
255
:
# 0xFF
data[picked_index]
=
255
elif
picked_magic[
1
]
=
=
127
:
# 0x7F
data[picked_index]
=
127
elif
picked_magic[
1
]
=
=
0
:
# 0x00
data[picked_index]
=
0
# here we are hardcoding all the byte overwrites for all of the tuples that begin (2, )
elif
picked_magic[
0
]
=
=
2
:
if
picked_magic[
1
]
=
=
255
:
# 0xFFFF
data[picked_index]
=
255
data[picked_index
+
1
]
=
255
elif
picked_magic[
1
]
=
=
0
:
# 0x0000
data[picked_index]
=
0
data[picked_index
+
1
]
=
0
# here we are hardcoding all of the byte overwrites for all of the tuples that being (4, )
elif
picked_magic[
0
]
=
=
4
:
if
picked_magic[
1
]
=
=
255
:
# 0xFFFFFFFF
data[picked_index]
=
255
data[picked_index
+
1
]
=
255
data[picked_index
+
2
]
=
255
data[picked_index
+
3
]
=
255
elif
picked_magic[
1
]
=
=
0
:
# 0x00000000
data[picked_index]
=
0
data[picked_index
+
1
]
=
0
data[picked_index
+
2
]
=
0
data[picked_index
+
3
]
=
0
elif
picked_magic[
1
]
=
=
128
:
# 0x80000000
data[picked_index]
=
128
data[picked_index
+
1
]
=
0
data[picked_index
+
2
]
=
0
data[picked_index
+
3
]
=
0
elif
picked_magic[
1
]
=
=
64
:
# 0x40000000
data[picked_index]
=
64
data[picked_index
+
1
]
=
0
data[picked_index
+
2
]
=
0
data[picked_index
+
3
]
=
0
elif
picked_magic[
1
]
=
=
127
:
# 0x7FFFFFFF
data[picked_index]
=
127
data[picked_index
+
1
]
=
255
data[picked_index
+
2
]
=
255
data[picked_index
+
3
]
=
255
return
data
# create new jpg with mutated data
def
create_new(data):
f
=
open
(
"mutated.jpg"
,
"wb+"
)
f.write(data)
f.close()
if
len
(sys.argv) <
2
:
print
(
"Usage: JPEGfuzz.py <valid_jpg>"
)
else
:
filename
=
sys.argv[
1
]
data
=
get_bytes(filename)
#mutated_data = bit_flip(data)
mutated_data
=
magic(data)
create_new(mutated_data)
import
sys
import
random
# read bytes from our valid JPEG and return them in a mutable bytearray
def
get_bytes(filename):
f
=
open
(filename,
"rb"
).read()
return
bytearray(f)
def
bit_flip(data):
num_of_flips
=
int
((
len
(data)
-
4
)
*
0.01
)
indexes
=
range
(
4
, (
len
(data)
-
4
))
chosen_indexes
=
[]
# iterate selecting indexes until we've hit our num_of_flips number
counter
=
0
while
counter < num_of_flips:
chosen_indexes.append(random.choice(indexes))
counter
+
=
1
for
x
in
chosen_indexes:
current
=
data[x]
current
=
(
bin
(current).replace(
"0b"
,""))
current
=
"0"
*
(
8
-
len
(current))
+
current
indexes
=
range
(
0
,
8
)
picked_index
=
random.choice(indexes)
new_number
=
[]
# our new_number list now has all the digits, example: ['1', '0', '1', '0', '1', '0', '1', '0']
for
i
in
current:
new_number.append(i)
# if the number at our randomly selected index is a 1, make it a 0, and vice versa
if
new_number[picked_index]
=
=
"1"
:
new_number[picked_index]
=
"0"
else
:
new_number[picked_index]
=
"1"
# create our new binary string of our bit-flipped number
current
=
''
for
i
in
new_number:
current
+
=
i
# convert that string to an integer
current
=
int
(current,
2
)
# change the number in our byte array to our new number we just constructed
data[x]
=
current
return
data
def
magic(data):
magic_vals
=
[
(
1
,
255
),
(
1
,
255
),
(
1
,
127
),
(
1
,
0
),
(
2
,
255
),
(
2
,
0
),
(
4
,
255
),
(
4
,
0
),
(
4
,
128
),
(
4
,
64
),
(
4
,
127
)
]
picked_magic
=
random.choice(magic_vals)
length
=
len
(data)
-
8
index
=
range
(
0
, length)
picked_index
=
random.choice(index)
# here we are hardcoding all the byte overwrites for all of the tuples that begin (1, )
if
picked_magic[
0
]
=
=
1
:
if
picked_magic[
1
]
=
=
255
:
# 0xFF
data[picked_index]
=
255
elif
picked_magic[
1
]
=
=
127
:
# 0x7F
data[picked_index]
=
127
elif
picked_magic[
1
]
=
=
0
:
# 0x00
data[picked_index]
=
0
# here we are hardcoding all the byte overwrites for all of the tuples that begin (2, )
elif
picked_magic[
0
]
=
=
2
:
if
picked_magic[
1
]
=
=
255
:
# 0xFFFF
data[picked_index]
=
255
data[picked_index
+
1
]
=
255
elif
picked_magic[
1
]
=
=
0
:
# 0x0000
data[picked_index]
=
0
data[picked_index
+
1
]
=
0
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!