记录一下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()
self._extract_dict()
self._assemble_fuzz_script()
self._assemble_dockerfile()
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"): f.write("FROM scratch\n") elif line.startswith("ENTRYPOINT"): continue elif line.startswith("CMD"): 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") 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
那么需要在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
|
这样就可以远程调试了。