在上一篇文章中,我们创建了一个简单但有效的模糊器。接下来,我们将进一步提高fuzzer的易用性和实用性。在本文中,我们将对该模糊器的功能进行必要的扩展。通用寄存器+增量状态到目前为止我们还没有涉及的一件事是将其他通用寄存器也设置为随机值。入口代码在其工作中确实使用了一些通用寄存器,如果我们确实在某处遇到问题,它很可能会因随机值而崩溃。我们可能还想找到更微妙的错误——那些不会使内核彻底崩溃,但可能会将内核地址泄漏到用户空间从未见过的寄存器中的错误。检查内核是否正确、我们的寄存器/标志等是否已保存的一种方法是在从内核模式返回后写出寄存器的状态。这并不难实现,因为我们可以将所有(或至少大部分)寄存器值存储在固定地址(例如,在我们已经用于其他目的的数据页中)。这里棘手的部分是如何将其与在子进程中运行多次进入尝试/系统调用相结合,因为健全性检查需要与进入尝试交错进行,这可能非常麻烦。最小化崩溃的可能性我们已经在第二篇文章中提到,让一个子进程崩溃是非常昂贵的,因为这意味着启动一个全新的子进程。因此,尽可能避免崩溃(并在同一子进程中运行尽可能多的入口尝试)可能是提高模糊器性能的可行策略。这包括两个主要部分:在行内保存/恢复所需的状态,例如,您保存并恢复%rsp以便后续的pushf/popf指令继续工作。从信号处理程序中恢复,例如通过安装处理程序,可以将进程恢复到已知的良好状态。检查生成的汇编代码虽然在生成汇编代码时很容易出错,但很难注意到,因为程序崩溃并且您看不到您得到了意想不到的结果。我之前也有过类似的问题,但是2年没注意到:我在对ljmp操作数的地址进行编码时,不小心用错了字节顺序,所以在32位兼容模式下,它居然Neverrunanythingonit!检查汇编代码的一种简单方法是使用像udis86这样的反汇编库,然后手动验证生成的代码。#include...ud_tu;ud_init(&u);ud_set_vendor(&u,UD_VENDOR_INTEL);ud_set_mode(&u,64);ud_set_pc(&u,(uint64_t)mem);ud_set_input_buffer(&u,(unsignedchar*)mem,(char*)out-(char*)mem);ud_set_syntax(&u,UD_SYN_ATT);while(ud_disassemble(&u))fprintf(stderr,"%08lx%s\n",ud_insn_off(&u),ud_insn_asm(&u));fprintf(stderr,"\n");KVM/Xen/Intel/AMD交互在一个案例中,我们看到了与KVM的交互,其中启动任何KVM实例都会破坏GDTR(GDT寄存器)的大小,并允许模糊器通过使用GDT超过GDTR(GDT寄存器)的大小期望导致崩溃的段大小。事实证明,这个漏洞是可利用的,并赋予ring0执行权限。在另一种情况下,我们在硬件加速的嵌套客户端(客户端中的客户端)中运行时看到了交互。通常,KVM需要模拟底层硬件的某些特性,这增加了相当多的复杂性。模糊器很有可能在KVM或Xen等管理程序中发现漏洞,因此在不同的裸机CPU和多个管理程序下运行模糊器很有价值。要以编程方式创建KVM实例,请参阅SergeZaitsev编写的几行代码中的KVM主机。一个相关的有趣实验可能是为Windows或在x86上运行的其他操作系统编译模糊器,看看它们的工作情况。我在WSL(适用于Linux的Windows子系统)上简要测试了Linux二进制文件,没有发生任何不良情况。Configuration/StartupOptionsConfiguration/StartupOptions影响入口代码的具体操作。下面是我在最新的内核中发现的相关选项:$grep-o'CONFIG_[A-Z0-9_]*'arch/x86/entry/entry_64*.S|sort|uniqCONFIG_DEBUG_ENTRYCONFIG_IA32_EMULATIONCONFIG_PARAVIRTCONFIG_RETPOLINECONFIG_STACKPROTECTORCONFIG_X86_5LEVELCONFIG_X86_ESPFIX64CONFIG_X86_L1_CACHE_SHIFTCONFIG_XEN_PV其实,还有更多的选项,whichareallhiddenintheheaderfile.Buildingmultiplekernelswithdifferentcombinationsoftheseoptionscanhelprevealcombinationsthatarebroken,perhapsonlyinedgecasestriggeredbythefuzzer.BylookingatDocumentation/admin-guide/kernel-parameters.txt,youcanalsofindsomeoptionsthatmayaffecttheentrypointcode.这里有一个Python脚本,它可以生成随机的配置选项组合,这对于用KVM传递内核命令行非常有用:importrandomflags="""noptinospectre_v1nospectre_v2spectre_v2_user=offspec_store_bypass_disable=offl1tf=offmds=offtsx_async_abort=offkvm.nx_huge_pages=offnoapicnoclflushnosmapnosmepnoexec32nofxsrnohugeiomapnosmtnosmtnoxsavenoxsaveoptnoxsavesintremap=offnolapicnomcenopatnopcidnorandmapsnoreplace-smpnordrandnosepnosmpnox2apic""".split()print(''.join(random.sample(flags,5)),"nmi_watchdog=%u"%(random.randrange(2),))Whenftraceisenabled,itwillbeintheentrycodeInsertsomecode,e.g.forsyscallsandirqflagstracing.这可能也很值得测试,所以我建议在运行模糊器之前调整这些文件(位于/sys/kernel/tracing):PTRACE_SYSCALL我们已经看到ptrace改变了系统调用进入/退出的处理方式(因为进程需要被停止并通知跟踪器),因此最好使用ptrace_syscall在ptrace()下运行部分入口尝试。当被ptrace停止时,尝试调整被跟踪进程的一些/所有寄存器可能也很有趣。完全正确地完成这个任务是非常困难的,这里就不介绍了。mkinitrd.sh当我在VM中进行测试时,我更喜欢将程序绑定到initrd中并将其作为init(pid1)运行,这样就不需要将其复制到文件系统映像中。您可以使用这样的脚本:#!/bin/bashset-eset-xrm-rfinitrd/mkdirinitrd/g++-static-Wall-std=c++14-O2-g-oinitrd/initmain.cc-lm(cdinitrd/&&(find|cpio-o-Hnewc))\|gzip-c\>initrd.entry-fuzz.gz如果你使用的是Qemu/KVM,只需传入-initrdinitrd.entry-fuzz.gz,它就会运行启动后立即使用模糊器。污点检查如果模糊器确实遇到某种内核崩溃或错误,确保我们不会错过它们是很有用的。我个人喜欢在内核命令行中使用参数ops=panicpanic_on_warnpanic=-1,并且将-no-reboot传递给Qemu/KVM;这将确保任何警告都会导致Qemu立即退出(在终端上留下任何诊断信息)。如果你在专用裸机上运行模糊器(例如,使用上面的initrd方法),你可以设置panic=0,这只会挂机。如果您在常规工作站上进行测试并且不希望整个机器挂起,您可以检查受污染的内核(只要有警告或错误就会被污染)然后退出:inttainted_fd=open("/proc/sys/kernel/tainted",O_RDONLY);if(tainted_fd==-1)error(EXIT_FAILURE,errno,"open()");chartainted_orig_buf[16];ssize_ttainted_orig_len=pread(tainted_fd,tainted_orig_buf,sizeof(tainted_orig_buf),0);if(tainted_orig_len==-1)error(EXIT_FAILURE,errno,"pread()");while(1){//generate+runtestcase...chartainted_buf[16];ssize_ttainted_len=pread(tainted_fd,tainted_buf,sizeof(tainted_buf),0);if(tainted_len==-1)error(EXIT_FAILURE,errno,"pread()");if(tainted_len!=tainted_orig_len||memcmp(tainted_buf,tainted_orig_buf,tainted_len)){fprintf(stderr,"Kernelbecametainted,stopping.\n");//TODO:dumphexbytesordisassemblyexit(EXIT_FAILURE);}}Networklog如果内核崩溃并且不清楚是什么问题,记录你试图对网络做的一切是非常有用的。我将给出UDP日志的简单框架:intmain(...){intudp_socket=socket(AF_INET,SOCK_DGRAM,0);if(udp_socket==-1)error(EXIT_FAILURE,errno,"socket(AF_INET,SOCK_DGRAM,0)");structsockaddr_inremote_addr={};remote_addr.sin_family=AF_INET;remote_addr.sin_port=htons(21000);inet_pton(AF_INET,"10.5.0.1",&remote_addr.sin_addr.s_addr);if(connect(udp_socket,(conststructs*)&remote_addr,sizeof(remote_addr))==-1)error(EXIT_FAILURE,errno,"connect()");...}然后,生成每次进入/退出的代码后,可以简单的把它转储在这个套接字上:write(udp_socket,(char*)mem,out-(uint8_t*)mem);我们希望日志服务器(此处为10.5.0.1:21000)接收到的最后数据将包含生成的Crashing汇编代码。根据具体的用例,有时需要添加某种框架,以便很容易判断测试用例的确切开始和结束位置。检查模糊器是否捕获已知漏洞多年来,人们在入口代码中发现了许多错误。所以我们可以构建一些旧的、易受攻击的内核并在它们上运行模糊器以确保它确实捕获了这些已知的错误。我们还可以根据查找错误所需的时间来衡量模糊器的效率,但是,我们必须注意不要过度优化以防止它们只找到这些错误。代码覆盖率/仪器反馈仪器化像AFL和syzkaller这样的模糊器如此有效的原因之一是它们使用代码覆盖率来非常精确地测量调整测试用例的各个位的效果。这通常是通过使用特殊的编译器标志编译C代码来实现的,该编译器标志会发出额外的代码来收集覆盖率数据。对于汇编代码,尤其是入口代码,这是一个非常棘手的问题,因为如果不手动检查代码的每条指令,我们无法准确知道CPU处于什么状态(以及我们可以破坏哪些寄存器/状态)。然而,如果我们真的想提高代码覆盖率,有一个方法可以做到:x86指令集包含一条指令,它既接受立即数又接受立即地址,并且不影响任何其他状态(例如标志):movb$值,(地址)。我们唯一需要注意的是:确保addr是一个编译时常量地址,它总是映射到一些物理内存,并在页表中标记为存在,这样我们就不会出现页面错误当我们访问它时。幸运的是,Linux已经提供了一种机制:fixmaps,也就是“编译时虚拟内存分配”。这样,我们可以静态分配一个编译时常量虚拟地址,该地址指向所有任务和上下文的相同底层物理页面。由于是任务间共享的,所以我们在进程间切换时必须清除或以其他方式保存/恢复这些值。通过结合使用C宏和汇编程序宏,我们可以获得侵入性极低的覆盖原语,您可以将其放在入口代码中的任何位置,以记录所采用的代码路径。我已经编写了一个补丁,但仍有一些边缘情况需要解决(例如,启用SMAP时,它不能完全工作)。另外,我怀疑x86维护者是否愿意在入口代码中包含这些覆盖注释。在模糊器方面,使仪器反馈复杂化的一件事是您需要一个完整的系统来跟踪测试用例、它们的结果,以及(可能)您对每个测试用例应用了哪些突变。因此,我暂时选择忽略代码覆盖率;无论如何,这是一个广泛的模糊测试主题,与x86或特别是入口代码没有太大关系。性能计数器/硬件反馈收集代码覆盖率的一种完全不同的方法是使用性能计数器。我知道最近有两个项目这样做:·ResmackFuzzTest·kAFL这里最大的好处显然是不需要检测(内核修改)。最大的缺点是性能计数器不是完全确定的(可能是由于硬件中断等外部因素)。也许它对入口代码也不起作用,因为在汇编代码上只花费了很短的时间。无论如何,这里有几个链接供进一步参考:https://man7.org/linux/man-pages/man2/perf_event_open.2.htmlhttp://www.brendangregg.com/perf.html这篇文章翻译自:https://blogs.oracle.com/linux/fuzzing-the-linux-kernel-x86-entry-code%2c-part-3-of-3如有转载请注明出处。
