第1部分。QInst:最好先输掉一天,然后在五分钟内飞起来(编写文书非常琐碎)

上一部分中,我粗略地描述了如何从ELF文件加载eBPF函数。 现在是时候从幻想转变为苏联动画片了,在花费了一定的精力之后,遵循明智的建议,制作了通用的仪器工具 (或者,简而言之,UII !!!) 。 为此,我将利用Golden Hammer反图案设计,并从相对熟悉的QEMU中构建一个工具。 作为奖励,我们获得了跨体系结构的检测以及整个虚拟计算机级别的检测。 检测的形式为“带有eBPF的小型本机so-file +小型.o-file”。 在这种情况下,在优化和代码生成之前,将在QEMU内部表示的相应指令之前替换eBPF函数。


结果, 在代码生成过程添加的工具本身(即,不计算正常系统运行时的几千字节)看起来像这样,这不是伪代码:


#include <stdint.h> extern uint8_t *__afl_area_ptr; extern uint64_t prev; void inst_qemu_brcond_i64(uint64_t tag, uint64_t x, uint64_t y, uint64_t z, uint64_t u) { __afl_area_ptr[((prev >> 1) ^ tag) & 0xFFFF] += 1; prev = tag; } void inst_qemu_brcond_i32(uint64_t tag, uint64_t x, uint64_t y, uint64_t z, uint64_t u) { __afl_area_ptr[((prev >> 1) ^ tag) & 0xFFFF] += 1; prev = tag; } 

好了,是时候将我们的小精灵加载到矩阵了。 好吧,如何下载,而不是 ack 喷雾。


正如有关QEMU.js文章中已经提到的那样 ,QEMU操作模式之一是从来宾JIT生成主机代码(可能对于完全不同的体系结构)。 如果是我上一次实现代码生成后端,那么这次我将通过在优化器前面进行楔入来处理内部表示。 这是一个任意决定吗? 不行 希望优化器可以消除多余的角落,抛出不必要的变量等。 据我了解,他实际上做了简单而又迅速的事情:推送常量,抛出“ x:= x + 0”之类的表达式并删除无法访问的代码。 我们可以得到相当数量的东西。


汇编脚本配置


首先,让我们将源文件: tcg/bpf-loader.ctcg/instrument.c到Makefiles中。 一般而言,希望有朝一日将其推送到上游,因此您最终需要明智地进行此操作,但是现在我将无条件地将这些文件添加到程序集中。 我将通过环境变量采用AFL最佳传统中的参数。 顺便说一下,我将在AFL的仪器上再次进行测试。


只需查找提到的“邻居”-使用grep -Roptimize.c文件,我们将找不到任何东西。 因为有必要搜索optimize.o


 --- a/Makefile.target +++ b/Makefile.target @@ -110,7 +110,7 @@ obj-y += trace/ obj-y += exec.o obj-y += accel/ obj-$(CONFIG_TCG) += tcg/tcg.o tcg/tcg-op.o tcg/tcg-op-vec.o tcg/tcg-op-gvec.o -obj-$(CONFIG_TCG) += tcg/tcg-common.o tcg/optimize.o +obj-$(CONFIG_TCG) += tcg/tcg-common.o tcg/optimize.o tcg/instrument.o tcg/bpf-loader.o obj-$(CONFIG_TCG_INTERPRETER) += tcg/tci.o obj-$(CONFIG_TCG_INTERPRETER) += disas/tci.o obj-$(CONFIG_TCG) += fpu/softfloat.o 

因此,您在这里,用C进行元编程...


首先,让我们从最后一个系列中添加bpf-loader.c ,其中包含提取与QEMU操作相对应的入口点的代码。 神秘的tcg-opc.h文件将帮助我们解决这个问题。 看起来像这样:


 /* * DEF(name, oargs, iargs, cargs, flags) */ /* predefined ops */ DEF(discard, 1, 0, 0, TCG_OPF_NOT_PRESENT) DEF(set_label, 0, 0, 1, TCG_OPF_BB_END | TCG_OPF_NOT_PRESENT) /* variable number of parameters */ DEF(call, 0, 0, 3, TCG_OPF_CALL_CLOBBER | TCG_OPF_NOT_PRESENT) DEF(br, 0, 0, 1, TCG_OPF_BB_END) // ... 

什么废话 事实是,它没有连接到源头中-您需要定义DEF宏,包括此文件,然后立即删除该宏。 看,他什至没有警卫。


 static const char *inst_function_names[] = { #define DEF(name, a, b, c, d) stringify(inst_qemu_##name), #include "tcg-opc.h" #undef DEF NULL }; 

结果,我们得到了一个整齐的目标函数名称数组,由操作码索引并以NULL结尾,我们可以为文件中的每个字符运行该函数。 我了解这并不有效。 鉴于此操作具有一次性性质,因此很简单,这很重要。 接下来,我们只跳过所有字符


 ELF64_ST_BIND(sym->st_info) == STB_LOCAL || ELF64_ST_TYPE(sym->st_info) != STT_FUNC 

其余的将根据列表进行检查。


我们依附于执行流程


现在,您需要了解代码生成机制的流程,并等到感兴趣的指令通过为止。 但是首先,您需要在tcg/tcg.h定义函数instrumentation_inittcg_instrumentinstrumentation_shutdown并写下它们的调用:初始化-初始化后端之后, tcg_optimize -在tcg_optimize调用之前。 似乎instrumentation_shutdown可以挂在atexit上的instrumentation_init ,而不会飙升。 我也这么认为,它很可能会在完整的系统仿真模式下工作,但是在用户模式仿真模式下,QEMU转换exit_group系统调用,有时exit_exit函数调用,该调用会忽略所有这些atexit处理程序,因此,我们将在linux-user/syscall.c搜索它, linux-user/syscall.c调用linux-user/syscall.c我们前面的代码中。


解释字节码


因此,是时候阅读编译器为我们生成的内容了。 使用带有-x选项的llvm-objdump或更方便地-d -t -r可以方便地完成此操作。


输出例子
 $ ./compile-bpf.sh test-bpf.o: file format ELF64-BPF Disassembly of section .text: 0000000000000000 inst_brcond_i64: 0: 18 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r2 = 0 ll 0000000000000000: R_BPF_64_64 prev 2: 79 23 00 00 00 00 00 00 r3 = *(u64 *)(r2 + 0) 3: 77 03 00 00 01 00 00 00 r3 >>= 1 4: 7b 32 00 00 00 00 00 00 *(u64 *)(r2 + 0) = r3 5: af 13 00 00 00 00 00 00 r3 ^= r1 6: 57 03 00 00 ff ff 00 00 r3 &= 65535 7: 18 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r4 = 0 ll 0000000000000038: R_BPF_64_64 __afl_area_ptr 9: 79 44 00 00 00 00 00 00 r4 = *(u64 *)(r4 + 0) 10: 0f 34 00 00 00 00 00 00 r4 += r3 11: 71 43 00 00 00 00 00 00 r3 = *(u8 *)(r4 + 0) 12: 07 03 00 00 01 00 00 00 r3 += 1 13: 73 34 00 00 00 00 00 00 *(u8 *)(r4 + 0) = r3 14: 7b 12 00 00 00 00 00 00 *(u64 *)(r2 + 0) = r1 15: 95 00 00 00 00 00 00 00 exit 0000000000000080 inst_brcond_i32: 16: 18 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r2 = 0 ll 0000000000000080: R_BPF_64_64 prev 18: 79 23 00 00 00 00 00 00 r3 = *(u64 *)(r2 + 0) 19: 77 03 00 00 01 00 00 00 r3 >>= 1 20: 7b 32 00 00 00 00 00 00 *(u64 *)(r2 + 0) = r3 21: af 13 00 00 00 00 00 00 r3 ^= r1 22: 57 03 00 00 ff ff 00 00 r3 &= 65535 23: 18 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r4 = 0 ll 00000000000000b8: R_BPF_64_64 __afl_area_ptr 25: 79 44 00 00 00 00 00 00 r4 = *(u64 *)(r4 + 0) 26: 0f 34 00 00 00 00 00 00 r4 += r3 27: 71 43 00 00 00 00 00 00 r3 = *(u8 *)(r4 + 0) 28: 07 03 00 00 01 00 00 00 r3 += 1 29: 73 34 00 00 00 00 00 00 *(u8 *)(r4 + 0) = r3 30: 7b 12 00 00 00 00 00 00 *(u64 *)(r2 + 0) = r1 31: 95 00 00 00 00 00 00 00 exit SYMBOL TABLE: 0000000000000000 l df *ABS* 00000000 test-bpf.c 0000000000000000 ld .text 00000000 .text 0000000000000000 *UND* 00000000 __afl_area_ptr 0000000000000080 g F .text 00000080 inst_brcond_i32 0000000000000000 g F .text 00000080 inst_brcond_i64 0000000000000008 g O *COM* 00000008 prev 

如果您尝试查找eBPF操作码的描述,则会发现在明显的地方(Linux内核的源代码和手册页)中有关于如何使用它,如何编译等的描述。 然后,您会看到iovisor工具团队页面,其中包含方便的非官方eBPF参考。


该指令占用一个64位字(大约两个),格式为


 struct { uint8_t opcode; uint8_t dst:4; uint8_t src:4; uint16_t offset; uint32_t imm; }; 

占据两个单词的变量仅由具有所有逻辑的第一条指令和具有32个立即数的立即数组成的“预告片”组成,它们在objdump反汇编程序中非常清晰可见。


操作码本身也具有规则的结构:最低三位是操作类:32位ALU,64位ALU,加载/存储,条件跳转。 因此,按照QEMU的最佳传统在宏上实现它们非常方便。 我不会在代码库上进行详细说明 我们不在代码审查中 我最好告诉您有关的陷阱。


我的第一个问题是,我以QEMU- local_temp的形式制作了一个惰性eBPF寄存器分配器,并开始不加思索地将此函数的调用转移到宏。 事实证明,这就像一个著名的模因:“我们将抽象插入到抽象中,以便在生成指令的同时可以生成指令。” 事后,我已经不太了解当时发生了什么,但是生成的指令的顺序显然发生了奇怪的事情。 之后,我制作了tcg_gen_...函数的类似物,将新指令推入列表的中间,以操作数作为函数的参数,并且顺序自动变为应有的顺序(因为参数完全在调用之前被完全计算过一次)。


第二个问题是在查看eBPF中的立即数操作数时,试图将TCG常量推为任意指令的操作数。 提示已经提到的tcg-opc.h ,该操作的参数列表的组成严格固定: n输入参数, m输出和k常数。 顺便说一下,调试此类代码时,它有助于通过命令行参数-d op,op_opt甚至-d op,op_opt,out_asm传递QEMU。


可能的论点
 $ ./x86_64-linux-user/qemu-x86_64 -d help Log items (comma separated): out_asm show generated host assembly code for each compiled TB in_asm show target assembly code for each compiled TB op show micro ops for each compiled TB op_opt show micro ops after optimization op_ind show micro ops before indirect lowering int show interrupts/exceptions in short format exec show trace before each executed TB (lots of logs) cpu show CPU registers before entering a TB (lots of logs) fpu include FPU registers in the 'cpu' logging mmu log MMU-related activities pcall x86 only: show protected mode far calls/returns/exceptions cpu_reset show CPU state before CPU resets unimp log unimplemented functionality guest_errors log when the guest OS does something invalid (eg accessing a non-existent register) page dump pages at beginning of user mode emulation nochain do not chain compiled TBs so that "exec" and "cpu" show complete traces trace:PATTERN enable trace events Use "-d trace:help" to get a list of trace events. 

好吧,不要重复我的错误:内部指令反汇编程序非常先进,如果您在其中看到类似add_i64 loc15,loc15,$554412123213 ,则美元符号后面的东西不是指针。 更确切地说,这当然是一个指针,但可能挂有标志,并且具有操作数字面值的作用,而不是指针。 当然,所有这一切都适用,如果您知道应该有一些特定的数字,例如$0$ff ,则根本不必担心指针。 :)如何movi这个问题-您只需要创建一个返回新temp的函数,就可以通过movi将所需的常量放入其中。


顺便说一句,如果您在tcg/tcg.c #define USE_TCG_OPTIMIZATIONS tcg/tcg.c #define USE_TCG_OPTIMIZATIONS tcg/tcg.c ,那么突然间,优化将关闭,并且更容易分析代码转换。


对于sim卡,我将向有兴趣将QEMU纳入文档的读者,甚至是正式的读者! 其余部分,我将演示AFL承诺的仪器。


和兔子一样


对于运行时的全文,我再次将读者发送到存储库,因为它(文本)没有艺术价值,并且从AFL交付中的qemu_mode诚实地得到了强化,并且通常是一段常规的C代码。 :


 #include <stdint.h> extern uint8_t *__afl_area_ptr; extern uint64_t prev; void inst_qemu_brcond_i64(uint64_t tag, uint64_t x, uint64_t y, uint64_t z, uint64_t u) { __afl_area_ptr[((prev >> 1) ^ tag) & 0xFFFF] += 1; prev = tag; } void inst_qemu_brcond_i32(uint64_t tag, uint64_t x, uint64_t y, uint64_t z, uint64_t u) { __afl_area_ptr[((prev >> 1) ^ tag) & 0xFFFF] += 1; prev = tag; } 

对于相应的QEMU操作,挂钩函数具有与iargs一样多的参数很重要。 标头中的两个extern将在重定位过程中链接到运行时。 原则上, prev可以在此处定义,但随后需要将其定义为static ,否则它将属于我不支持的COMMON部分。 实际上,实际上,我们只是简单地重写了文档中的伪代码,但这在这里是机器可读的!


要检查,请创建bug.c文件:


 #include <stdio.h> #include <unistd.h> #include <stdlib.h> int main(int argc, char *argv[]) { char buf[16]; int res = read(0, buf, 4); if (buf[0] == 'T' && buf[1] == 'E' && buf[2] == 'S' && buf[3] == 'T') abort(); return res * 0; } 

还有forksrv文件,可方便地输入AFL:


 #!/bin/bash export NATIVE_INST=./instrumentation-examples/afl/afl-native.so export BPF_INST=./instrumentation-examples/afl/afl-bpf.co exec ./x86_64-linux-user/qemu-x86_64 ./instrumentation-examples/afl/bug 

并进行模糊测试:


 AFL_SKIP_BIN_CHECK=1 afl-fuzz -i ../input -o ../output -m none -- ./forksrv 

美国模糊圈
 1234 T234 TE34 TES4 TEST <-     crashes,    2200   

到目前为止,速度还不是很热,但是作为一个借口,我现在要说的是,这里( qemu_mode不使用原始qemu_mode的重要功能:将可执行代码的地址发送到fork服务器。 但是,现在QEMU代码库中没有AFL,并且希望这种通用化的工具有一天会挤在上游。


GitHub项目

Source: https://habr.com/ru/post/zh-CN452608/


All Articles