Fuzz-01-linux黑盒测试覆盖率

2023-03-24
#fuzz

在白盒测试中往往会用覆盖率作为fuzz的指标,来评判输入是否有价值。但黑盒测试中有时我们也想要获得覆盖率以更加精准的fuzz。可以采用二进制逆向工具,在程序的每个基本块头部插入软中断(int3)指令,运行时通过ptrace捕获SIGTRAP信号来统计代码覆盖率,再将原始的字节恢复并继续执行。

项目地址:ri-char/trace_tool

实现

Patch程序

在程序的基本块中插入int3指令,如一个程序原本的控制流如下

2023-03-24_20-21

图中共有8个基本块,需要将每个基本块的首字节改成int3,由于IDA Python难以批量处理大量文件,因此选用了radare2工具。它可以通过命令行交互,也提供了多种语言的接口可以很方便的使用代码交互。

radare2加载文件后,先通过aa命令让它自动分析所有的函数以及函数中的基本块。afl命令可以列出所有分析得到的函数,aflj可以指定以json格式输出,便于代码处理。agf命令可以可视化的展现函数的基本块,如上图所示,同样agfj也可以将基本块信息以json格式输出,如下图。

2023-03-24_20-29

找到所有的基本块后,首先需要将每个基本块的原始内容记录下来,以便运行时动态恢复。可以使用pB 1命令输出一字节的内容;使用wx cc将当前字节修改为0xcc(为int3指令的机器码)。当需要修改文件时,radare2需要以读写方式启动,加上-w参数。

2023-03-24_20-38

以上演示了手动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来设置需要跟踪cloneforkvforkexec

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

运行效果

对于上图中的程序进行测试

2023-03-24_21-04

局限性

  1. 待测试的二进制文件中不能有混淆
  2. 待测试的程序中不能有进程被调试(进程主动被调试是一种主要的反调试手段)