对于网络服务程序的模糊测试,当前的解决方案主要有:hook libc socket调用、修改afl、修改网络程序等,其中无论哪种方式,几乎都需要修改网络服务程序。
AFLplusplus的开发者在仓库中推荐使用hook socket 的方案,这种方案灵感来源于preeny。基于 LD_PRELOAD
hook libc中socket相关的函数将网络流转换为标准输入的IO,但是这种方案在一些复杂的网络程序中并不一定通用。
以nginx为例,在nginx启动后,程序会持续运行并监听对应的网络端口,如果没有异常并不会主动退出。而AFL需要每次重新启动程序,在这种场景下进行测试时就需要修改nginx的源代码。
另一个较为自然的方案是直接修改AFL传递输入的方式。这种方式中比较有代表性的是Monash University的研究者提出的aflnet。aflnet在afl的基础上,将标准输入修改为网络发包的方式,并加入了网络传输的功能,可以较为高效的测试网络服务程序。
但是afl的分支众多,有着不同的优劣势,也有着不同的应用场景。基于修改AFL的方案在移植其它优化策略时需要较大的工作量,另外aflnet同样需要对待测程序进行一定的修改以适应测试。
效率最高的方案是直接修改网络程序,调用对应的解析函数来进行测试,以bind9为例,其代码中就专门提供了用于Fuzz的部分。这种方式直接获取标准输入,传入核心函数中进行测试。这种方式的缺陷在于需要较为了解程序,且需要对目标程序进行定制开发。
那么能不能找到一种相对简单的方案,能够在不对AFL或者目标程序进行修改的基础上,较为简单的测试网络服务程序呢?
考虑到AFL读取覆盖率是通过共享内存的方式,一个解决思路是,并不直接通过AFL启动程序,而是AFL启动辅助程序,AFL将标准输入传输辅助程序,辅助程序和网络程序进行交互。
具体来说,AFL启动辅助程序,辅助程序检查网络程序是否启动,若未启动,则启动待测的网络服务程序。此时 __AFL_SHM_ID
环境变量将传输待测网络服务程序中,基于AFL插桩的网络服务程序在测试时同样会记录覆盖率信息到当前的共享内存中。
每次进行新的测试时,辅助程序重复读取输入、将输入通过网络发送至目标网络服务程序的流程。而网络服务程序则不需要在启动流程中浪费运行时间,达到类似 persistent mode
的效果。
另外当网络服务程序失去响应时,辅助程序主动crash,使得AFL记录对应的crash输入。
程序的运行流程如下图所示:
最后,基于这种思路,完成了一个简单的实现,可以在这里查看。