greenhouse_fuzz

记录一下greenhouse工具 fuzzing的实现方法

greenhouse进行fuzzing的流程

build_fuzz_img.py

一般使用的命令

1
python3 build_fuzz_img.py -f <path-to-rehosted-greenhouse-image.tar.gz> 

调用构造函数builder.build。涉及以下五步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def build(self):

        self._get_info() # 提取架构,从dockerfile提取命令行

        self._extract_dict() # 生成字典文件,保存在img_dir/dictionaries下

        # 生成fuzz.sh, postauth.sh, finish.sh, minify.sh文件
        # 均从templates/文件夹下构造
        self._assemble_fuzz_script()

        # 设置入口点为fuzz.sh并开始fuzz
        self._assemble_dockerfile()

        # 复制固件文件系统并调用docker build
        self._build_docker()

其中_get_info_extract_dict比较简单,不详细说明。

_assemble_fuzz_script

使用fuzz.sh.j2作为模板并在其中添加以下参数。

  • ARCH: 目标架构
  • CMD:目标进程启动的命令行
  • DRYRUN_TIMEOUT:timeout设置
  • bg_block:后台进程启动脚本
    下面是fuzz.sh.j2
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
# 创建一系列文件,文件夹
/fuzz_bins/utils/mkdir -p /scratch
/fuzz_bins/utils/cp -a /fuzz/* /scratch
cd /scratch
/fuzz_bins/utils/cp /qemu-$ARCH-static /qemu-static

# 执行后台程序
echo "[Fuzz] Launch background scripts..."
{{bg_block}}


# 执行程序一次并观察是否能单进程模拟成功。下面用到了$CMD代表目标应用启动参数
echo "[Fuzz] Dry run the server..."
output=$(/fuzz_bins/utils/timeout -s SIGTERM $DRYRUN_TIMEOUT /fuzz_bins/ghup_bins/unshare_pid /qemu-static -hackbind -hackproc -execve "/qemu-static -hackbind -hackproc" -- $CMD 2>&1 )

# 单进程模拟成功的标志:目标进程成功bind某个端口
result=$(ls / | /fuzz_bins/utils/grep 'GH_SUCCESSFUL_BIND')
echo $result
if [ -z "$result" ]
then
# 没有成功单进程模拟,或者说没有bind成功
    echo "[GH_ERROR] Fail to launch the server normally!!!"
    echo $output

    # 和上一次fuzzing不同的是,这一次没有使用unshare_pid binary
    # 使用unshare_pid为了创建一个新的沙箱环境,在这个环境中目标进程的挂载点和pid都是初始化内容,从而使已知的
    echo "[Fuzz] Trying without unshare"
    output=$(/fuzz_bins/utils/timeout -s SIGKILL $DRYRUN_TIMEOUT /qemu-static -hackbind -hackproc -execve "/qemu-static -hackbind -hackproc" -- $CMD 2>&1  && cleanup)

# 如果还是无法运行,则退出
    result=$(ls / | /fuzz_bins/utils/grep 'GH_SUCCESSFUL_BIND')
    echo $result
    if [ -z "$result" ]
    then
        echo "[GH_ERROR] Fail to launch the server normally!!!"
        echo $output
        echo "[GH_ERROR] Giving up"
        exit
    fi
fi

这一步之后我们可以确保目标进程被模拟成功。接下来尝试获取目标进程accept返回时的地址,用于后续加速fuzzing。

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
# 获取accept地址
echo "[Fuzz] Dry run the server again to obtain the address for forkserver..."

# 这里相比上面的dry run添加了两处。1. GH_DRYRUN=1, 2.hookhack
# 这里获取了accept之后的吓一跳执行的用户态指令的地址,例如
# 0x40a004: call accept
# 0x40a008: mov s0, a0 <--- 返回内容"return addr: 0x40a008"
# 除此之外,在accept中还设置了虚假远程连接IP,使得在调用accept()的时候能够立即填充sin buffer
output=$(GH_DRYRUN=1 /fuzz_bins/utils/timeout -s SIGTERM $DRYRUN_TIMEOUT /fuzz_bins/ghup_bins/unshare_pid /usr/bin/afl-qemu-trace -hookhack -hackbind -hackproc -execve "/qemu-static -hackbind -hackproc" -- $CMD 2>&1)
# 提取返回内容"return addr: 0x40a008"中的0x40a008
addr_str=$(echo "$output" | /fuzz_bins/utils/timeout -s SIGKILL $DRYRUN_TIMEOUT /fuzz_bins/utils/grep --line-buffered 'return addr')

echo $addr_str

# 和上面一样,如果出现错误,进一步处理
if [ -z "$addr_str" ]
then
    echo "[GH_ERROR] something wrong with afl+GH!!!"
    echo $output
    echo "[Fuzz] Trying without unshare"
    output=$(GH_DRYRUN=1 /fuzz_bins/utils/timeout -s SIGKILL $DRYRUN_TIMEOUT /usr/bin/afl-qemu-trace -hookhack -hackbind -hackproc -execve "/qemu-static -hackbind -hackproc" -- $CMD 2>&1 && cleanup)
    addr_str=$(echo "$output" | /fuzz_bins/utils/timeout -s SIGKILL $DRYRUN_TIMEOUT /fuzz_bins/utils/grep --line-buffered 'return addr')
    echo $addr_str
    if [ -z "$addr_str" ]
    then
        echo "[GH_ERROR] something wrong with afl+GH!!!"
        echo $output
        echo "[GH_ERROR] Giving up"
        exit
    fi
fi
addr=$(echo $addr_str | /fuzz_bins/utils/cut -d' ' -f3)

# 上述得到的地址作为AFL开始fork的位置,这样可以节约环境检查的开销
if [ -z "$addr" ]
then
    echo "[GH_ERROR] failed to extract the fork address from QEMU's output!!!"
    exit
fi



# 备份地址
echo $addr > /scratch/addr


# 开始fuzzing
echo "[Fuzz] Start Fuzzing..."
export AFL_ENTRYPOINT=$addr # <---- 在这里设置了二进制程序入口,上述得到的地址
export LD_BIND_LAZY=1
export AFL_NO_AFFINITY=1
exec /fuzz_bins/bin/afl-fuzz -t 1000 -Q -x /scratch/dictionary -i /scratch/seeds -o /scratch/output -- $CMD

postauth_fuzz.sh

最重要的内容已经完成了,接下来作者还创建了一个postauth_fuzz.sh,为了绕过身份验证。这个binary应该是经过加密了,不能直接反编译找到相关字符串。猜测应该是使用默认口令?

构建docker

在上面两步之后,开始构建dockerfile,这是在docker中运行的进程的fuzzing入口。如下

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
def _assemble_dockerfile(self):
        with open(os.path.join(self.img_dir, "Dockerfile")) as f:
            lines = f.read().splitlines()
        with open(os.path.join(self.img_dir, "Dockerfile"), 'w') as f:
            for line in lines:
                if line.startswith("FROM"): # prologue
                    f.write("FROM scratch\n")
                elif line.startswith("ENTRYPOINT"): # skip
                    continue
                elif line.startswith("CMD"): # epilogue
                    f.write("COPY config.json /config.json\n")
                    f.write("COPY fuzz_bins /fuzz_bins\n")
                    f.write("COPY seeds /fuzz/seeds\n")
                    f.write("COPY dictionary /fuzz/dictionary\n")
                    f.write("COPY fuzz.sh /fuzz.sh\n")
                    f.write("COPY postauth_fuzz.sh /postauth_fuzz.sh\n")
                    f.write("COPY finish.sh /finish.sh\n")
                    f.write("COPY minify.sh /minify.sh\n")
                    f.write(f'RUN ["/fuzz_bins/utils/cp", "/fuzz_bins/qemu/afl-qemu-trace-{self._arch}", "/usr/bin/afl-qemu-trace"]\n')
                    f.write("WORKDIR /scratch\n")
                   # 注意这里默认是fuzz.sh,而不是postauth_fuzz.sh
                    f.write("CMD /fuzz.sh\n")
                   continue
                else:
                    f.write(line+"\n")

crash调试

这里困扰了我很久。之前一直在greenhouse自带的启动脚本中加入-g参数辅助gdb远程调试。但是总发现地址存在一些问题。后来才知道这样仅仅是调试/bin/sh执行目标启动脚本的过程。如果得到了crash需要调试。需要单独把这个进程写入原本命令中/bin/sh xxx.sh的位置,之后使用-g远程调试即可。注意依然需要chroot。
例如下面是qemu_run.sh

1
2
3
cd www

/usr/sbin/httpd

那么需要在www/下面写一个run_debug.sh

1
2
3
4
5
# 原先内容
# chroot /fs /qemu-arm-static -pconly -hackbind -hackproc -hacksysinfo -D /trace.log0 -strace -execve "/qemu-arm-static -pconly -hackbind -hackproc -hacksysinfo -strace -D /trace.log" -E LD_PRELOAD="libnvram-faker.so" /bin/sh qemu_run.sh > /fs/GREENHOUSE_STDLOG 2>&1

# 现在修改为
chroot /fs /qemu-arm-static -g 12234 -pconly -hackbind -hackproc -hacksysinfo -D /trace.log0 -strace -execve "/qemu-arm-static -pconly -hackbind -hackproc -hacksysinfo -strace -D /trace.log" -E LD_PRELOAD="libnvram-faker.so" /usr/sbin/httpd > /fs/GREENHOUSE_STDLOG 2>&1

这样就可以远程调试了。

文章目录
  1. 1. build_fuzz_img.py
    1. 1.1. _assemble_fuzz_script
    2. 1.2. postauth_fuzz.sh
    3. 1.3. 构建docker
  2. 2. crash调试
|