Arya

Fuzzing: Breaking Things with Random Inputs(一)
01/15/2019[+]在和樱花探讨了一下人类的本质之后,我觉得我应该是个偏执狂吧(滑稽[+]周二的教练总是会放...
扫描右侧二维码阅读全文
16
2019/01

Fuzzing: Breaking Things with Random Inputs(一)

01/15/2019
[+]在和樱花探讨了一下人类的本质之后,我觉得我应该是个偏执狂吧(滑稽
[+]周二的教练总是会放日不落作为健身的背景音乐,我建议下一次换成大爷版的,更加洗脑
[+]这种天气还有什么比窝在被窝里一边看破产姐妹,一遍剥着坚果吃更爽的时光呢,当然没有~( ̄▽ ̄~)~
[+]明天一定要买板栗吃!(拍桌

第二章主要说的是通过随机输入的方式,来进行检测是否存在漏洞的模糊测试,就是常说的fuzzing,也就是web中常说的漏洞测试,也可以理解为代码审计

作者介绍了Fuzzing的历史,是在1988年的一个风雨交加的夜晚,Barton Miller教授在通过1200baud的电话线连接他的大学的计算机的时候,由于雷雨引起了噪声,直接导致两端的unix命令得到了错误的输入,并导致了频繁的崩溃。
(可以参考一下计算机网络里物理层相关知识,推荐计算机网络(自顶向下方法)能够帮助理解为什么噪声会导致输入出错)

当然,像我这样的人遇到这种事情,除了气的拍桌以外也不会别的想法吧(滑稽,但是这个时候就体现出了科学家之所以是科学家的原因,比如我被苹果砸到永远也不会想出万有引力,Barton教授为了明白是什么原因导致的,程序是否应该比噪声更加健壮?后来他就起草了一个程序测试项目,并找了他的学生来一起研究引起崩溃的原因,这就是第一个fuzz的诞生

0x01 一个简单的fuzzer

项目的内容是构建一个fuzz工具,需要实现以下的功能:
1、这个程序可以输出随机的字符流
2、通过这个fuzz工具攻击尽可能多的unix程序,并试图破坏这些程序

当然学习最重要的是思想,这个实验抓住了模糊测试的本质:
通过对一个程序进行随机输入,来检测这个程序是否存在漏洞

作者给出了一个简单的fuzz程序

import fuzzingbook_utils
import random
def fuzzer(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(0, max_length + 1)#生成一个
    out = ""
    for i in range(0, string_length):
        out += chr(random.randrange(char_start, char_start + char_range))
    return out
   
 print(fuzzer())

返回的是一段随机长度的字符
``
'!7#%"*#0=)$;%6*;>638:*>80"=</>(/*:-(2<4 !:5*6856&?""11<7+%<%7,4.8,*+&,,$,."'``

有个雷坑,他自己写的这个库fuzzingbook_utils限制了随机输入的多种情况,直接使用他的程序,获得的值永远和他的结果一样,(还好读了一遍代码发现是库的原因,差点又要怪我的电脑了~( ̄▽ ̄~)~)

如果自己写代码的话,可以删去

import fuzzingbook_utils

就可以获得随机值

作者随即又抛出了一个问题,如果输入的格式是确定的,我们又要怎么去满足他的输入格式限制?
例如:填写电话号码一栏必须是11位数字,那么我们在fuzzing时的输入则必须满足输入为11位数字这个条件;
输入的时候要求我们必须以';'或是'/'之类的符号进行分割时,我们又要怎么去满足他的输入条件呢?

0x02 利用脚本进行fuzzing

作者以第二个例子为问题,创建一个目录,同时在目录中创建了一个input.txt文本,并将fuzzer()函数中随机输入的数据写入input.txt

import os
import tempfile
basename = "input.txt"
tempdir = tempfile.mkdtemp() #随机创建一个文件夹
FILE = os.path.join(tempdir, basename) #在文件夹下创建一个文件
print(FILE) #打印出文件夹以及文件名

以上代码执行的话结果为
C:\Users\sakura\AppData\Local\Temp\tmp2jbf85eg\input.txt

接下来就是向input.txt文件中写入刚才生成的随机字符

data = fuzzer()with open(FILE, "w") as f:
    f.write(data)

Image2-1.png

可以看到写入了123=1=;/ *?**--1/</4:2 #$6;$,'-.'8:1*,$99=/1 #0(%9 2/0559?2,)00$(<1(>>56%,99.)/"+)$>1().,'13 .

当然你也可以直接通过代码读出

接下来作者写了一个程序,调用了linux系统里的bc计算机,计算了2+2的结果

import os
import subprocess

program = "bc"with open(FILE, "w") as f:
    f.write("2 + 2\n")result = subprocess.run([program, FILE],
                        stdin=subprocess.DEVNULL,
                        stdout=subprocess.PIPE,
                        stderr=subprocess.PIPE,
                        universal_newlines=True)  # Will be "text" in Python 3.7,除了3.5以上的版本以外,都没有run这个属性

最后会在文件夹里生成一个input.txt文档,写着2+2
Image2-2.png
然后增加代码

print(result.stdout)

4
这行代码使用bc对2+2的结果计算并输出结果4

Image2-3.png

我们也恶意检查一下运行的状态是否正确,并打印输出

print(result.returncode)

0表示命令的状态正确无误
42b016c5128ec3efdcb07846aff92aee.png

如果有错误的消息,则可以使用

print(result.stderr)

输出为' '

当然,我们可以使用这种和方式去调用任何一个程序,只要你用的是虚拟机,你甚至可以自己攻击自己(滑稽
我们也可以自己写一个模糊测试的文件删除程序,看一下模糊测试可以产生多少有效的文件名(一脸坏笑

既然我们已经有了可以创建一个文件夹以及文件的程序,那么现在可以开始尝试创建多个文件,并且写入fuzzer()生成的字符串,调用bc计算器来计算结果

#这里我对代码做了一些细微的调整,我实在不想所有的文件都写到tmp文件夹中,我删起来都麻烦,所以查了一下
tempfile.mkdtemp()的用法,手动操作改了文件夹存放的位置

#代码

import os
import tempfile
import subprocess
import random

basename = "input.txt"
tempdir = tempfile.mkdtemp(suffix='',prefix='/home/sakura/test/',dir='')#存放在/home/sakura/test/下面 
FILE = os.path.join(tempdir, basename) 
print(FILE) 

trials = 100
program = "bc"

runs = []

for i in range(trials):
    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))

通过调用sum函数检查生成了几个状态成功的元组

sum(1 for (data, result) in runs if result.stderr == "")

巧的是我这里也是4

打印并查看

Image2-3.png

那么知道了状态成功的
[+]此处补充一个我认为是知识点的知识点
sum(1 for (data, result) in runs if result.stderr == "")这一行代码的意思

#这一行代码一眼看过去比较难懂,实际上可以理解为
 sum( x for x in range(0,101))
# 这种代码的变形
 
 >>>a=[(2,3),(4,5),('a','b')]
 >>>sum(1 for i in a)
 3

#可以理解为令i=1,每读取一个a中的项,便用sum=sum+i
#换句话就是,统计列表a中的项数
#所以可以得出,下面这行代码的意思是,统计状态返回为""的元组项数
sum(1 for (data, result) in runs if result.stderr == "")
 

现在来看看失败的输入是为什么
增加代码,来检查第一条错误信息

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)

当然了,我们输入的大部分字符都是无效的,无法计算出来结果,这里我放上我的运行结果

Image2-6.png

所以可以看出我们输入的第一行随机生成的字符串中存在的各种错误

那么问题来了,如果滤过这些常见错误之后,哪还有哪些其他的错误呢,又是什么呢?

继续完善代码

anw=[result.stderr for (data, result) in runs if
 result.stderr != ""
 and "illegal character" not in result.stderr
 and "parse error" not in result.stderr
 and "syntax error" not in result.stderr]
 #排除了“非法字符”,“解析错误”,“语法错误”
 
 print(anw)
 

在这里作者给出的原本代码是不会输出的,所以我也做了一些细微的小完善

最后输出为
[]

Image2-7.png

那么我们现在再来检测是否存在可以计算的代数式的字符串
添加代码

print(sum(1 for (data, result) in runs if result.returncode != 0))

然而现实是残酷的
返回为
0

也就是说,这种fuzz下生成的字符串,基本都是不可计算的

0x03 fuzzer发现的bug

让我们把目光重新放回Miller教授身上,他带着学生写的fuzzer中1/3是存在问题的,那么教授发现了什么问题呢,这里作者吐槽了一句,看起来1990年程序员犯的错和现在犯的错误是没有任何变化的
[+]行吧,我暂且同意人的本质是复读机这一条(滑稽

而教授发现的这个问题也就是我们日常都能听见的缓冲区溢出
比如说C语言,输入与输出的时候都是有最大的位数限制,int型为4个字节,也就是32bit,以前打算法比赛的时候,每次写到大数计算问题,第一个最大的坑就是缓冲区溢出问题(气的拍桌

(可能我喜欢pwn的一个原因就是它让我觉得,我的c语言没有白学,哈哈哈哈

作者抛出了一个常见的栈溢出的例子

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

此时如果你输入的weekday[9]='Wednesday'(9个字符),编译器一定会报错,因为定义时候的weekday为9个字符,而这9个字符中,有一个'0'作为终止符,也就是你实际上可以输入的字符为8个

所以我们在进行模糊测试的时候,很容易就超出了可输入范围

作者以"Thursday"这个字符串长度为最大长度,去测试我们之前写的fuzzer()生成的字符串中,是否存在超出最大长度限制的代码

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

批量检测并抛出问题

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

通过作者自己写的ExpectError库,抛出问题
Traceback (most recent call last):
File "<ipython-input-22-f83db3d59a06>", line 5, in <module>

``crash_if_too_long(s)``

File "<ipython-input-20-928c2d2de976>", line 4, in crash_if_too_long
raise ValueError
ValueError (expected)

同时作者这里也告诉我们,这个库里是一些常见的错误捕获,而非那种“意外”的错误。

那么问题来了,什么样的错误是“意外”的错误呢?

在一开始接触C语言中,我第一个学习的循环就是do-while循环,这个循环在学到字符这一章的是候,一定会写一个常见的函数,就是一直输入数据,直到输入' ''为止,也就是将空格作为终止循环的标志。

假设程序员现在需要调用一个函数,不断读取非空格的字符
这里作者给了一段常见的代码

char read_next_nonspace() {
    char lastc;

    do {
        lastc = getchar();
    } while (lastc != ' ');

    return (lastc);}

这段代码在输入为' '时,终止循环,返回' '

那么将这段C代码改写为python代码为

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

那么现在还有一个问题,假设我需要读取的是这样一行字符串没有空格怎么办呢,那岂不是永远在循环里出不来了?

于是我们引入作者的ExpectTimeout模块,并生成的代码检测是否存在永不跳出循环的现象

from ExpectError import ExpectTimeout

trials = 100with ExpectTimeout(2):#2s之后仍未结束运行便抛出错误
    for i in range(trials):
        s = fuzzer()
        hang_if_no_space(s)

果然报错

Traceback (most recent call last):
File "<ipython-input-25-8e40f7d62a1b>", line 5, in <module>

``hang_if_no_space(s)``

File "<ipython-input-23-5f437edacff4>", line 7, in hang_if_no_space
i += 1
File "<ipython-input-23-5f437edacff4>", line 7, in hang_if_no_space
i += 1
`` File "<string>", line 16, in check_time
TimeoutError (expected)``

然后就没有然后了,没错,作者这里并没给出解决的思路,但是我有一个思路,可以先计算出整个字符串的长度,然后遍历字符串是判断是否输入结束,输入结束跳出循环即可,代码比较简单,不贴出来了

接下里作者介绍的是一种“流氓数”,实质上就是过大的输入,因为在C中,可以使用malloc给输入的字符动态分配缓冲区,

char *read_input() {
    size_t size = read_buffer_size();
    char *buffer = (char *)malloc(size);
    // fill buffer
    return (buffer);}

但是如果需要为字符串分配的缓冲区非常大,以至于超出了内存怎么办?如果大小为负数又怎么办?

这里给出了判断需要过大内存的的python代码

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

此时我们调用fuzzer()并将所有输入的字符转为acsl形式

long_number = fuzzer(100, ord('0'), 10)
print(long_number)

结果发现生成了一串巨长的数字
7056414967099541967374507745748918952640135045
运行判断数值大小的代码

with ExpectError():
    collapse_if_too_large(long_number)

果然抛出错误

当然,系统是不可能为你分配这么大的内存的,唯一的办法就是报错然后终止运行(比如我以前写代码,经常就是内存过大,xxx.exe终运行,C语言总是会让你怀疑人生不是吗,哈哈哈)

所以真的遇到了这种为了分配内存导致系统崩溃的情况,唯一的办法之后重启(发出了肆无忌惮的笑声

Last modification:January 17th, 2019 at 04:03 pm
If you think my article is useful to you, please feel free to appreciate

Leave a Comment