Fuzz-01-linux黑盒测试覆盖率
在白盒测试中往往会用覆盖率作为fuzz的指标,来评判输入是否有价值。但黑盒测试中有时我们也想要获得覆盖率以更加精准的fuzz。可以采用二进制逆向工具,在程序的每个基本块头部插入软中断(int3
)指令,运行时通过ptrace捕获SIGTRAP信号来统计代码覆盖率,再将原始的字节恢复并继续执行。
项目地址:ri-char/trace_tool
实现
Patch程序
在程序的基本块中插入int3
指令,如一个程序原本的控制流如下
图中共有8个基本块,需要将每个基本块的首字节改成int3
,由于IDA Python难以批量处理大量文件,因此选用了radare2
工具。它可以通过命令行交互,也提供了多种语言的接口可以很方便的使用代码交互。
radare2
加载文件后,先通过aa
命令让它自动分析所有的函数以及函数中的基本块。afl
命令可以列出所有分析得到的函数,aflj
可以指定以json格式输出,便于代码处理。agf
命令可以可视化的展现函数的基本块,如上图所示,同样agfj
也可以将基本块信息以json格式输出,如下图。
找到所有的基本块后,首先需要将每个基本块的原始内容记录下来,以便运行时动态恢复。可以使用pB 1
命令输出一字节的内容;使用wx cc
将当前字节修改为0xcc
(为int3
指令的机器码)。当需要修改文件时,radare2
需要以读写方式启动,加上-w
参数。
以上演示了手动patch的过程,代码则是将以上过程自动化,完整代码参考ri-char /trace_tool/src/patch.rs。
统计覆盖率
首先需要通过ptrace
调试上目标进程,可以在子进程上调用PTRACE_TRACEME
来主动让父进程调试自己,无需root权限,代码如下
match unsafe { fork() }? {
ForkResult::Parent { child } => {
let exit_code=debug(child, &initial_byte_map, args.output)?;
exit(exit_code);
},
ForkResult::Child => {
ptrace::traceme()?;
execv(cstr_args[0].as_c_str(), cstr_args.as_slice())?;
Ok(())
}
}
由于大多数程序都是多线程或者多进程的,所以需要调用PTRACE_SETOPTIONS
来设置需要跟踪clone
、fork
、vfork
和exec
ptrace::setoptions(
child,
ptrace::Options::PTRACE_O_TRACECLONE
| ptrace::Options::PTRACE_O_TRACEEXEC
| ptrace::Options::PTRACE_O_TRACEFORK
| ptrace::Options::PTRACE_O_TRACEVFORK,
)?;
在调试的过程中,需要一个主循环来处理被调试进程的所有事件并进行处理。其中最重要的事件就是接收到了SIGTRAP信号
let mut reg = ptrace::getregs(pid)?;
// 此处获得的rip是int3后一条指令的地址,因此需要减1获得int3的地址
reg.rip -= 1;
if let Some((module, offset)) = get_module_and_offset(pid, reg.rip)? {
if let Some(initial_byte) = get_initial_byte(&module, offset, initial_byte_map) {
let aligned_ip = reg.rip & !7u64;
// 获取该地址中原始内容
let mut code_bytes: [u8; 8] =
ptrace::read(pid, aligned_ip as AddressType)?.to_ne_bytes();
// 将0xcc修改成程序原始的值
code_bytes[(reg.rip - aligned_ip) as usize] = initial_byte;
let new_bytes = u64::from_ne_bytes(code_bytes);
// 写入
unsafe {
ptrace::write(
pid,
aligned_ip as AddressType,
new_bytes as *mut std::ffi::c_void,
)
}?;
// 将被调试程序rip-1
ptrace::setregs(pid, reg)?;
// 记录
if let Some(trigger_set) = record_map.get_mut(&module) {
trigger_set.insert(offset);
} else {
let mut trigger_set = HashSet::new();
trigger_set.insert(offset);
record_map.insert(module.clone(), trigger_set);
}
}
}
// 让被调试程序继续运行
ptrace::cont(pid, None)?;
完整代码参考ri-char /trace_tool/src/run.rs。
运行效果
对于上图中的程序进行测试
局限性
- 待测试的二进制文件中不能有混淆
- 待测试的程序中不能有进程被调试(进程主动被调试是一种主要的反调试手段)