一
背景
二
介绍
-static-pie
的 Bochs 直到其启动菜单。自那之后的几个月里,我们取得了很多进展。我们现在已经实现了快照、代码覆盖反馈以及更多的 Linux 仿真逻辑,现在我们实际上可以开始模糊测试了!因此,在这篇文章中,我们将回顾一些代码库中新添加的主要功能,并提供一些关于如何设置模糊测试器进行模糊测试的示例。三
快照
1.Bochs 映像本身的任何可写的 PT_LOAD 内存段
2.Bochs 的文件表
3.Bochs 的动态内存,如堆分配
4.Bochs 的扩展 CPU 状态:AVX 寄存器、浮点单元等
5.Bochs 的寄存器
mmap
的调用。因此,我们可以很容易地通过这种方式对 MMU 状态进行快照。这也适用于文件表,因为我们以相同的方式控制所有文件 I/O。不过目前,我还没有实现文件表快照,因为在我用于开发的模糊测试工具中,Bochs 不会触及任何文件。当前,我的解决办法是将文件标记为“脏”,如果我们在模糊测试时访问了这些文件,我会在那时让系统崩溃。以后,我们应该能够像处理 MMU 一样处理文件快照。/proc
中为类似 Checkpoint Restore 提供的工具,如d0c s4vage讨论的那样。memcpy
操作,这将非常耗费成本。因此,我需要找到一种避免这种情况的方法。Bochs 确实有内存访问钩子,因此我可以通过这种方式跟踪来宾中的脏内存。如果我们发现当前的实现成为性能瓶颈,这可能是未来的一个实现方向。|---------------------------------------------|
| Non-Writable ELF Segments |
|---------------------------------------------| <-- Start of memory that we need to record and restore
| Writable ELF Segments |
|---------------------------------------------|
mmap
和MAP_FIXED
将其直接映射到最后一个可写的 ELF 段旁边。现在我们的连续内存块也包含了扩展状态。|---------------------------------------------|
| Non-Writable ELF Segments |
|---------------------------------------------| <-- Start of memory that we need to record and restore
| Writable ELF Segments |
|---------------------------------------------|
| Bochs Extended CPU State |
|---------------------------------------------|
| Bochs MMU/Brk Pool |
|---------------------------------------------| <-- End of memory that we need to record and restore
memcpy
。我们的解决方案要么使用差异性重置(即只重置脏页),要么找到一种新的方法进行整体恢复,因为memcpy
并不足够。memcpy
。我们可以使用 Linux 的共享内存对象来缓存模糊测试期间的快照内容,这些对象是通过libc::shm_open
分配的。这基本上就像打开了一个由共享内存支持的文件,因此在每次快照恢复时,我们实际上不会触发任何磁盘读取或昂贵的文件 I/O。mmap
覆盖在脏的连续内存块上。它们大小相同,对吧?而且我们控制连续内存块的位置,所以这使得重置脏内存变得非常容易!代码基本上就是这样:// This function will take the saved data in the shm object and just mmap it
// overtop of the writable memory block to restore the memory contents
#[inline]
fn restore_memory_block(base: usize, length: usize, fd: i32) ->
Result<(), LucidErr> {
// mmap the saved memory contents overtop of the dirty contents
let result = unsafe {
libc::mmap(
base as *mut libc::c_void,
length,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_PRIVATE | libc::MAP_FIXED,
fd,
0
)
};if result == libc::MAP_FAILED || result != base as *mut libc::c_void {
return Err(LucidErr::from("Failed to mmap restore snapshot"));
}Ok(())
}
mmap
覆盖了但不再需要的页面时会花费大量时间,如果我们未来扩大模糊测试规模,这可能会成为瓶颈。时间会证明一切。目前,我非常喜欢这种方法的简单和易用性。特别感谢 Dominik Maier 和模糊测试 Discord 社区的其他人帮助我完善了这个想法。memcpy
操作而减慢速度。以我当前的设置(分配了 64MB 的来宾内存)来看,这种共享内存加mmap
的方法比单纯的巨型memcpy
快了大约 10 倍。我们从在快照恢复代码中花费 13% 的 CPU 时间提升到使用memcpy
时的 96%。因此,当前对我们来说效果很好。// Copy the contents of an existing MMU, used for snapshot restore
pub fn restore(&mut self, mmu: &Mmu) {
self.map_base = mmu.map_base;
self.map_length = mmu.map_length;
self.brk_base = mmu.brk_base;
self.brk_size = mmu.brk_size;
self.curr_brk = mmu.curr_brk;
self.mmap_base = mmu.mmap_base;
self.mmap_size = mmu.mmap_size;
self.curr_mmap = mmu.curr_mmap;
self.next_mmap = mmu.next_mmap;
}
66:87C9 | xchg cx,cx | 1000011111 001 001 -> 1
66:87D2 | xchg dx,dx | 1000011111 010 010 -> 2
66:87DB | xchg bx,bx | 1000011111 011 011 -> 3
66:87E4 | xchg sp,sp | 1000011111 100 100 -> 4
66:87ED | xchg bp,bp | 1000011111 101 101 -> 5
66:87F6 | xchg si,si | 1000011111 110 110 -> 6
66:87FF | xchg di,di | 1000011111 111 111 -> 7
bochs/cpu/data_xfer16.cc
文件中。bxInstruction_c
结构体有处理这类操作的字段,跟踪源寄存器和目标寄存器。如果它们相同,它会根据指令编码中的二进制表示进行检查。例如,xchg dx, dx
表示i->src()
和i->dst()
都等于 2。data_xfer16.cc
添加了一个额外的 include,然后像下面这样加入我的逻辑:#if BX_SNAPSHOT
// Check for take snapshot instruction `xchg dx, dx`
if ((i->src() == i->dst()) && (i->src() == 2)) {
BX_COMMIT_INSTRUCTION(i);
if (BX_CPU_THIS_PTR async_event)
return;
++i;
char save_dir[] = "/tmp/lucid_snapshot";
mkdir(save_dir, 0777);
printf("Saving Lucid snapshot to '%s'...\n", save_dir);
if (SIM->save_state(save_dir)) {
printf("Successfully saved snapshot\n");
sleep(2);
exit(0);
}
else {
printf("Failed to save snapshot\n");
}
BX_EXECUTE_INSTRUCTION(i);
}
#endif
BX_SNAPSHOT
,当 Bochs 遇到xchg dx, dx
指令时,它应该能够将其状态保存到磁盘上,就像终端用户在我们的 harness 中完美时刻按下了暂停按钮一样。cpu/cpu.cc
中加入一些代码实现的,如下所示:jmp_buf BX_CPU_C::jmp_buf_env; void BX_CPU_C::cpu_loop(void)
{
#if BX_SUPPORT_HANDLERS_CHAINING_SPEEDUPS
volatile Bit8u stack_anchor = 0;BX_CPU_THIS_PTR cpuloop_stack_anchor = &stack_anchor;
#endif#if BX_DEBUGGER
BX_CPU_THIS_PTR break_point = 0;
BX_CPU_THIS_PTR magic_break = 0;
BX_CPU_THIS_PTR stop_reason = STOP_NO_REASON;
#endif// Place the Lucid snapshot taking code here above potential long jump returns
#if BX_LUCID
lucid_take_snapshot();
#endifif (setjmp(BX_CPU_THIS_PTR jmp_buf_env)) {
// can get here only from exception function or VMEXIT
BX_CPU_THIS_PTR icount++;
BX_SYNC_TIME_IF_SINGLE_PROCESSOR(0);
#if BX_DEBUGGER || BX_GDBSTUB
if (dbg_instruction_epilog()) return;
#endif
#if BX_GDBSTUB
if (bx_dbg.gdbstub_enabled) return;
#endif
}
BX_LUCID
),我们将在开始仿真指令之前或通过longjmp
或类似逻辑返回异常之前调用take_snapshot
函数。take_snapshot
代码的逻辑非常简单,我们只需在全局执行上下文中设置一些变量,让 Lucid 知道我们为什么退出虚拟机以及应该对此做些什么:// Call into Lucid to take snapshot of current Bochs state
__attribute__((optimize(0))) void lucid_take_snapshot(void) {
if (!g_lucid_ctx)
return;// Set execution mode to Bochs
g_lucid_ctx->mode = BOCHS;// Set the exit reason
g_lucid_ctx->exit_reason = TAKE_SNAPSHOT;// Inline assembly to switch context back to fuzzer
__asm__ (
"push %%r15\n\t" // Save r15 register
"mov %0, %%r15\n\t" // Move context pointer into r15
"call *(%%r15)\n\t" // Call context_switch
"pop %%r15" // Restore r15 register
: // No output
: "r" (g_lucid_ctx) // Input
: "memory" // Clobber
);return;
}
xchg dx, dx
指令,非常酷!在一个 fuzzcase 结束时,当我们重置快照状态并希望从快照状态重新开始执行 Bochs 时,我们只需通过上下文切换调用此函数,以一个简单的ret
指令结束。这将表现得就像 Bochs 正常从调用lucid_take_snapshot
函数返回一样:// Restore Bochs' state from the snapshot
fn restore_bochs_execution(contextp: *mut LucidContext) {
// Set the mode to Bochs
let context = LucidContext::from_ptr_mut(contextp);
context.mode = ExecMode::Bochs;// Get the pointer to the snapshot regs
let snap_regsp = context.snapshot_regs_ptr();// Restore the extended state
context.restore_xstate();// Move that pointer into R14 and restore our GPRs
unsafe {
asm!(
"mov r14, {0}",
"mov rax, [r14 + 0x0]",
"mov rbx, [r14 + 0x8]",
"mov rcx, [r14 + 0x10]",
"mov rdx, [r14 + 0x18]",
"mov rsi, [r14 + 0x20]",
"mov rdi, [r14 + 0x28]",
"mov rbp, [r14 + 0x30]",
"mov rsp, [r14 + 0x38]",
"mov r8, [r14 + 0x40]",
"mov r9, [r14 + 0x48]",
"mov r10, [r14 + 0x50]",
"mov r11, [r14 + 0x58]",
"mov r12, [r14 + 0x60]",
"mov r13, [r14 + 0x68]",
"mov r15, [r14 + 0x78]",
"mov r14, [r14 + 0x70]",
"sub rsp, 0x8", // Recover saved CPU flags
"popfq",
"ret",
in(reg) snap_regsp,
);
}
}
四
代码覆盖反馈
0x1337
的指令是jmp 0x13371337
,那么我们将跟踪一个由0x1337
和0x13371337
组成的边缘对。基本上,我们跟踪当前的 PC 和跳转到的 PC。这同样适用于未采取分支的情况,因为我们会跳过分支指令并转到新指令,这本质上也是一种分支。instrument/stubs/instrument.cc
暴露的存根编译 Bochs 进行插桩。一些存根对我们特别有用,因为它们为分支指令插桩。因此,如果你在编译 Bochs 时定义了BX_INSTRUMENTATION
,这些存根将被编译到来宾的分支指令处理程序中。它们的原型记录了当前的 PC 和目标 PC。我不得不对条件分支未采取的插桩存根的签名做了一些修改,因为它没有跟踪可能被采取的 PC,而我们需要该信息来形成边缘对。以下是修改前后的存根逻辑:void bx_instr_cnear_branch_taken(unsigned cpu, bx_address branch_eip, bx_address new_eip) {}
void bx_instr_cnear_branch_not_taken(unsigned cpu, bx_address branch_eip) {}
void bx_instr_cnear_branch_taken(unsigned cpu, bx_address branch_eip, bx_address new_eip) {}
void bx_instr_cnear_branch_not_taken(unsigned cpu, bx_address branch_eip, bx_address new_eip) {}
bx_instr_cnear_branch_not_taken
计算一个新的taken
PC,这虽然有点麻烦,但对于在别人的项目上做修改来说,还是很简单的。以下是 Bochs 补丁文件中我在调用点所做的更改的示例,你可以看到我必须计算一个新的变量bx_address taken
来获取一个对:- BX_INSTR_CNEAR_BRANCH_NOT_TAKEN(BX_CPU_ID, PREV_RIP);
+ bx_address taken = PREV_RIP + i->ilen();
+ BX_INSTR_CNEAR_BRANCH_NOT_TAKEN(BX_CPU_ID, PREV_RIP, taken);
const COVERAGE_MAP_SIZE: usize = 65536; #[derive(Clone)]
#[repr(C)]
pub struct CoverageMap {
pub curr_map: Vec<u8>, // The hit count map updated by Bochs
history_map: Vec<u8>, // The map from the previous run
curr_map_addr: *const u8, // Address of the curr_map used by Bochs
}
u8
值的长数组,其中每个索引代表我们命中的一个边缘对。我们将该数组的地址传递给 Bochs,以便它可以在跟踪的边缘对对应的数组位置设置值。因此,Bochs 遇到分支指令时,它将具有当前 PC 和跳转到的 PC,并会生成一个有意义的值,将该值转换为u8
数组覆盖率映射的索引。在该索引处,它会递增u8
值。这个过程通过对两个边缘地址进行哈希运算,然后执行一个逻辑 AND 操作来完成,以屏蔽掉不属于覆盖率映射索引的位。这意味着我们可能会有碰撞,某些边缘对可能会产生与另一个不同边缘对相同的哈希值。但这是这种策略的一个缺陷,我们必须接受。还有其他方法可以避免边缘对的碰撞,但这需要每次遇到分支指令时进行哈希查找。这可能会比较昂贵,但考虑到我们的目标代码是由一个非常慢的模拟器运行的,未来我们可能会切换到这种范式,拭目以待。dbj2_hash
,这是一种奇特的小哈希算法,速度快,而且据说提供了不错的分布(低碰撞率)。总的来说,我们执行以下操作:1.通过插桩的分支指令遇到一个边缘对。
2.使用dbj2_hash
哈希两个边缘地址。
3.缩短哈希值,使其不超过coverage_map.len()。
4.增加coverage_map[hash]
中的u8
值。
static inline uint32_t dbj2_hash(uint64_t src, uint64_t dst) {
if (!g_lucid_ctx)
return 0;uint32_t hash = 5381;
hash = ((hash << 5) + hash) + (uint32_t)(src);
hash = ((hash << 5) + hash) + (uint32_t)(dst);
return hash & (g_lucid_ctx->coverage_map_size - 1);
}static inline void update_coverage_map(uint64_t hash) {
// Get the address of the coverage map
if (!g_lucid_ctx)
return;uint8_t *map_addr = g_lucid_ctx->coverage_map_addr;
// Mark this as hit
map_addr[hash]++;// If it's been rolled-over to zero, make it one
if (map_addr[hash] == 0) {
map_addr[hash] = 1;
}
}void bx_instr_cnear_branch_taken(unsigned cpu, bx_address branch_eip, bx_address new_eip) {
uint64_t hash = dbj2_hash(branch_eip, new_eip);
update_coverage_map(hash);
//printf("CNEAR TAKEN: (0x%lx, 0x%lx) Hash: 0x%lx\n", branch_eip, new_eip, hash);
}
void bx_instr_cnear_branch_not_taken(unsigned cpu, bx_address branch_eip, bx_address new_eip) {
uint64_t hash = dbj2_hash(branch_eip, new_eip);
update_coverage_map(hash);
//printf("CNEAR NOT TAKEN: (0x%lx, 0x%lx) Hash: 0x%lx\n", branch_eip, new_eip, hash);
}
u8
值的数组,在每次模糊测试迭代后进行评估。Lucid 端需要做以下几件事:u8
分类到所谓的“桶”中,桶只是边缘对命中的一个范围。例如,命中边缘对 100 次与命中 101 次没有太大区别,所以我们将这两种类型的覆盖数据逻辑上归为一类。就我们而言,它们是相同的。我们真正想要的是显著的差异。因此,如果我们看到一个边缘对命中 1 次与 1000 次,我们希望知道这种差异。我直接从 AFL++ 中借用了归类逻辑,AFL++ 已经通过经验测试了最佳的归类策略,以便为大多数目标提供最有价值的反馈。u8
值高于旧的覆盖率映射(历史映射记录了每个索引的历史最高值),那么我们就有了我们感兴趣的新覆盖结果!// Roughly sort ranges of hitcounts into buckets, based on AFL++ logic
#[inline(always)]
fn bucket(hitcount: u8) -> u8 {
match hitcount {
0 => 0,
1 => 1,
2 => 2,
3 => 4,
4..=7 => 8,
8..=15 => 16,
16..=31 => 32,
32..=127 => 64,
128..=255 => 128,
}
}// Walk the coverage map in tandem with the history map looking for new
// bucket thresholds for hitcounts or brand new coverage
//
// Note: normally I like to write things as naively as possible, but we're
// using chained iterator BS because the compiler spits out faster code
pub fn update(&mut self) -> (bool, usize) {
let mut new_coverage = false;
let mut edge_count = 0;// Iterate over the current map that was updated by Bochs during fc
self.curr_map.iter_mut()// Use zip to add history map to the iterator, now we get tuple back
.zip(self.history_map.iter_mut())// For the tuple pair
.for_each(|(curr, hist)| {// If we got a hitcount of at least 1
if *curr > 0 {// Convert hitcount into bucket count
let bucket = CoverageMap::bucket(*curr);// If the old record for this edge pair is lower, update
if *hist < bucket {
*hist = bucket;
new_coverage = true;
}// Zero out the current map for next fuzzing iteration
*curr = 0;
}
});// If we have new coverage, take the time to walk the map again and
// count the number of edges we've hit
if new_coverage {
self.history_map.iter().for_each(|&hist| {
if hist > 0 {
edge_count += 1;
}
});
}(new_coverage, edge_count)
}
五
环境/目标设置
// Crash the kernel
void __crash(void)
{
asm volatile("xchgw %sp, %sp");
*(int *)0 = 0;
}// Check to see if the input matches our criteria
void inspect_input(char *input, size_t data_len) {
// Make sure we have enough data
if (data_len < 6)
return;if (input[0] == 'f')
if (input[1] == 'u')
if (input[2] == 'z')
if (input[3] == 'z')
if (input[4] == 'm')
if (input[5] == 'e')
__crash();return;
}SYSCALL_DEFINE2(fuzzme, void __user *, data, size_t, data_len)
{
char kernel_copy[1024] = { 0 };
printk("Inside fuzzme syscall\n");// Make sure we don't overflow stack buffer
if (data_len > 1024)
data_len = 1024;// Copy the user data over
if (copy_from_user(kernel_copy, data, data_len))
{
return -EFAULT;
}// Inspect contents to try and crash
inspect_input(kernel_copy, data_len);return 0;
}
fuzzme
的新系统调用,它的系统调用号是 451,然后我编译了一个 harness 并将其放入 iso 文件的/usr/bin/harness
目录中。我还没有尝试找到一种通用的方法将崩溃连接到 Lucid,我只是将特殊的 NOP 指令放入__crash
函数中以指示崩溃。不过,使用像 KASAN 这样的工具,我相信将来会有某个切入点可以用作捕获所有崩溃的通用方法。奇怪的是,从 Bochs 主机层面检测崩溃并不如内核向你的程序发送信号时那么简单(显然,如果你以这种方式构建它,某些内核 oops 会向你的 harness 发送信号)。#include <stdio.h>
#include <sys/syscall.h>
#include <string.h>#define __NR_fuzzme 451
#define LUCID_SIGNATURE { 0x13, 0x37, 0x13, 0x37, 0x13, 0x37, 0x13, 0x37, \
0x13, 0x38, 0x13, 0x38, 0x13, 0x38, 0x13, 0x38 }#define MAX_INPUT_SIZE 1024UL
struct fuzz_input {
unsigned char signature[16];
size_t input_len;
char input[MAX_INPUT_SIZE];
};int main(int argc, char *argv[])
{
struct fuzz_input fi = {
.signature = LUCID_SIGNATURE,
.input_len = 8,
};
memset(&fi.input[0], 'A', 8);// Create snapshot
asm volatile("xchgw %dx, %dx");// Call syscall we're fuzzing
long ret = syscall(__NR_fuzzme, fi.input, *(size_t *)&fi.input_len);// Restore snapshot
asm volatile("xchgw %bx, %bx");if (ret != 0) {
perror("Syscall failed");
} else {
printf("Syscall success\n");
}return 0;
}
/usr/bin/harness
,然后我可以从带有GUI的普通Bochs运行它,以在我们想要进行模糊测试快照的点保存Bochs状态到磁盘。kernel/sys.c
文件的底部,并且我根据教程将harness添加到了initramfs中的/usr/bin/harness
。当我创建ISO时,我的文件层次结构如下:iso_files
- boot
- bzImage
- initramfs.cpio.gz
- grub
- grub.cfg
bzImage
是编译后的内核映像,initramfs.cpio.gz
是我们希望在虚拟机中使用的压缩initramfs文件系统,你可以通过导航到其根目录并执行类似find . | cpio -o -H newc | gzip > /path/to/iso_files/boot/initramfs.cpio.gz
的命令来创建它。grub.cfg
文件的内容如下:set default=0
set timeout=10
menuentry 'Lucid Linux' --class os {
insmod gzio
insmod part_msdos
linux /boot/bzImage
initrd /boot/initramfs.cpio.gz
}
grub-mkrescue
指向iso_files
目录,它会输出我们想要在Bochs中运行的ISO:grub-mkrescue -o lucid_linux.iso iso_files/
。devbox:~/git_bochs/Bochs/bochs]$ /tmp/gui_bochs -f bochsrc_gui.txt
========================================================================
Bochs x86 Emulator 2.8.devel
Built from GitHub snapshot after release 2.8
Compiled on Jun 21 2024 at 14:42:29
========================================================================
00000000000i[ ] BXSHARE not set. using compile time default '/usr/local/share/bochs'
00000000000i[ ] reading configuration from bochsrc_gui.txt
------------------------------
Bochs Configuration: Main Menu
------------------------------This is the Bochs Configuration Interface, where you can describe the
machine that you want to simulate. Bochs has already searched for a
configuration file (typically called bochsrc.txt) and loaded it if it
could be found. When you are satisfied with the configuration, go
ahead and start the simulation.You can also start bochs with the -q option to skip these menus.
1. Restore factory default configuration
2. Read options from...
3. Edit options
4. Save options to...
5. Restore the Bochs state from...
6. Begin simulation
7. Quit nowPlease choose one: [6]
harness
,因为它已经自动在我的$PATH
中,然后将Bochs状态保存到磁盘!Please choose one: [6] 6
00000000000i[ ] installing sdl2 module as the Bochs GUI
00000000000i[SDL2 ] maximum host resolution: x=1704 y=1439
00000000000i[ ] using log file bochsout.txt
Saving Lucid snapshot to '/tmp/lucid_snapshot'...
Successfully saved snapshot
/tmp/lucid_snapshot
拥有在Lucid的Bochs中恢复这个已保存Bochs状态所需的所有信息。我们只需要注释掉/tmp/lucid_snapshot/config
中的显示库行,如下所示:# configuration file generated by Bochs
plugin_ctrl: unmapped=true, biosdev=true, speaker=true, extfpuirq=true, parallel=true, serial=true, e1000=false
config_interface: textconfig
#display_library: sdl2
./lucid --input-signature 0x13371337133713371338133813381338 --verbose --bochs-image /tmp/lucid_bochs --bochs-args -f /home/h0mbre/git_bochs/Bochs/bochs/bochsrc_nogui.txt -q -r /tmp/lucid_snapshot
romimage: file="/home/h0mbre/git_bochs/Bochs/bochs/bios/BIOS-bochs-latest"
vgaromimage: file="/home/h0mbre/git_bochs/Bochs/bochs/bios/VGABIOS-lgpl-latest"
pci: enabled=1, chipset=i440fx
boot: cdrom
ata0-master: type=cdrom, path="/home/h0mbre/custom_linux/lucid_linux.iso", status=inserted
log: bochsout.txt
clock: sync=realtime, time0=local
cpu: model=corei7_skylake_x
cpu: count=1, ips=750000000, reset_on_triple_fault=1, ignore_bad_msrs=1
cpu: cpuid_limit_winnt=0
memory: guest=64, host=64
#display_library: sdl2
六
结论
看雪ID:pureGavin
https://bbs.kanxue.com/user-home-777502.htm
# 往期推荐
2、恶意木马历险记
球分享
球点赞
球在看
点击阅读原文查看更多