以错误的方式进行二进制覆盖
正确的方法 回到恐龙时代,如果你写了一个程序并想测试它是否正确,你通常只有两种选择:要么手动构造错误的输入并依次尝试,要么连接一个程序随机生成输入并自动进行测试。这些程序称为模糊测试器,或有时称为生成器,这被称为“模糊测试”。而且在很长时间内,它的效果并不好:生成器对被测程序没有任何反馈,除了“这个随机输入是否崩溃”。虽然这种反馈有用,但并不能让模糊测试器知道它是否在相对于上一个随机输入取得进展,或者是否做得很好,因此你的模糊测试器会反复击打同样的浅层代码。你可能会让它运行一周,当你回来时发现它从未生成能够触及程序有趣部分的输入,所有输入都立即被丢弃,或者从未向前推进到 if(packet_header == 0xdeadbeef) 检查处。美国模糊测试器(American Fuzzy Lop)通过引入基于覆盖率的模糊测试,彻底革新了这一领域。模糊测试器的唯一反馈不再仅仅是输入是否崩溃,而是记录了在程序执行过程中命中代码覆盖的情况;如果一个输入执行了之前未达到的程序部分,那么它就命中了新代码,并可能能够测试程序的一些新有趣表面区域,这些区域可能存在一些bug。这比过去的“愚蠢模糊测试”有效得多。 AFL 通过使用自定义的 Clang 编译器优化程序对被测程序进行编译来收集程序的覆盖率,该优化程序在控制流图(CFG)边上添加了覆盖反馈:如果你命中了代码中的新基本块,你就会获得一个新的覆盖位图项。如果在某个输入上运行程序的结果生成了一个相比之前设置了新位的位图,那就说明它命中了新代码;该输入可以保存为将来执行时变异的种子输入。你也可以为不具备源代码的黑箱二进制文件进行基于反馈的模糊测试。例如,AFL++ 具有 qemu_mode 后端:它使用 qemu-user 执行被测程序,该程序将你的 x86 指令转换为 QEMU 的 TCG 中间表示(IR),然后再通过其即时编译器将其转换回 x86,现在可以安全地在其二型虚拟机环境下执行。AFL++ 有一个 QEMU 的派生版本,该版本增加了一个自定义 TCG 钩子,以向中间表示中的区块添加覆盖插桩;每个区块的 IR 被扩展为 TCG 操作,这些操作以与 AFL 拓展 Clang IR 额外 LLVM 操作相同的方式写入覆盖位图项。尽管听起来像是很多工作,但这个 qemu_mode 运行的速度仅比本地程序慢 ~3-5 倍。黑箱二进制覆盖的黄金标准是让硬件为你完成这个任务。与需要在运行模糊测试用例时向被测程序添加额外操作不同,你可以使用 Intel PT。启用某些性能计数器配置寄存器将开始处理器跟踪:你的 CPU 将自动记录元数据作为其执行管道的一部分,而模糊测试器可以查看结果元数据以提取其执行的基本块的跟踪。这非常有效,基于 Intel PT 的跟踪仅比正常本地执行慢 ~10%。 当然,问题是,如果你像我一样,你的笔记本电脑配备的是 AMD 处理器而非 Intel。AMD 有自己神奇的性能计数功能,包括一个记录正在执行的代码的分支跟踪缓冲区,但它的 1)文档支持较差 2)性能远不及 Intel PT 3)无法以精确的方式使用——也就是说,计数器要么是基于采样的,因此只能给你 1/n 的事件,要么可能会掉落事件,如果在你能够清空之前元数据缓冲区填满。在这两种情况下,这使得它不适合进行模糊测试(你从技术上讲可以在仅依赖随机覆盖的基础上构建一个模糊测试器,这甚至可能不会那么糟。但实际上,还没有人这样做,也许没有价值。)这引出了我的坏主意。我想为一个副项目编写自己的模糊测试器,因此我希望收集准确的黑箱二进制覆盖。但我不想再编写自己的动态重编译引擎,或者购买一台新笔记本电脑。软件工程师该怎么办?我们可以通过用 INT3 指令替换所有分支来对客代码进行插桩。等等,等我解释。为了收集精确的覆盖率,我们需要程序在每次遇到分支时执行某些操作。我们无法在每个块中插入新行为,因为在每个块中已为其正常指令使用了所有字节:尝试调整块的大小需要修复代码指针,然后你就回到了“差劲的 QEMU”状态。分支指令的最小长度为两个字节:Jcc 有一种形式,仅由一个操作码字节和一个...
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡