在上一篇文章中,我们创建了一个简单但有效的,现在我们将进一步提高其可用性和实用性。 在本文中,我们将对此功能进行必要的扩展。
通用寄存器+增量状态
到目前为止我们还没有讨论的一件事是将其他通用寄存器也设置为随机值。 入口代码在其工作中确实使用了一些通用寄存器,如果我们确实在某个地方遇到问题,它很可能会因随机值而崩溃。
我们可能还想找到更微妙的漏洞,虽然不会完全使内核崩溃,但可能会将内核地址泄漏到用户空间以前从未见过的寄存器中。 检查内核是否正确并保存寄存器/标志等的一种方法是从内核模式返回后写出寄存器的状态。 这并不难实现,因为我们可以将所有(或至少大多数)寄存器的值放入固定地址中(例如,在我们已经用于其他目的的数据页中)。 这里的困难在于如何将其与在子进程中运行多个条目尝试/系统调用结合起来,这可能非常麻烦,因为健全性检查需要与条目尝试交织在一起。
最大限度地减少崩溃的可能性
正如我们在第二篇文章中提到的,崩溃子进程的成本非常高,因为这意味着启动一个全新的子进程。 因此,尽可能避免崩溃(并在同一子进程中运行尽可能多的进入尝试)可能是提高性能的可行策略。 这包括两个主要部分:
·保存/恢复行内所需的状态,例如,您要保存并恢复%rsp,以便后续的pushf/popf指令可以继续工作。
·从信号处理程序中恢复(例如通过安装处理程序)可以将进程恢复到已知的良好状态。
检查生成的汇编代码
尽管在生成汇编代码时代码很容易出错,但人们很难注意到,因为程序崩溃了并且您无法判断是否得到了意外结果。 我也遇到过类似的问题,但已经有2年没有注意到了:我在编码ljmp操作数的地址时不小心使用了错误的字节顺序,所以在32位兼容模式下,它实际上没有任何东西可以在上面运行!
检查汇编代码的一种简单方法是使用像这样的反汇编库,然后手动验证生成的代码。
#
...
ud_t u;
(&u);
(&u, );
(&u,64);
(&u, () mem);
(&u, (char *) mem, (char *) out - (char *) mem);
(&u, );
而((&u))
(, " lx %s\n", (&u), (&u));
(,“\n”);
KVM/Xen/Intel/AMD 交互
在一种情况下,我们看到与 KVM 的交互,其中启动任何 KVM 实例都会破坏 GDTR(GDT 寄存器)的大小,并因使用大于 GDT 预期大小的段而导致崩溃。 事实证明,该漏洞是可利用的,可以获得ring 0执行权限。 在另一种情况下,我们看到交互在硬件加速的嵌套客户端(客户端中的客户端)中运行。
通常,KVM 需要模拟底层硬件的某些特性,这会增加相当大的复杂性。 KVM 或 Xen 等虚拟机管理程序中很可能会发现漏洞,因此在不同的裸机 CPU 和多个虚拟机管理程序下运行是有价值的。
要以编程方式创建 KVM 实例,请参阅 Serge 的几行代码中的 KVM 主机。
一个相关的有趣实验可能是编译在 x86 或其他操作系统上运行,并查看它们的性能如何。 我只是在 WSL(针对 Linux)上测试了 Linux 二进制文件,没有发生任何问题。
配置/启动选项
配置/启动选项影响入口代码的具体操作。 以下是我在最新内核中找到的相关选项:
$ grep -o '[A-Z0-9_]*' arch/x86/entry/*.S | 排序| 独特的
氮
右
转移
事实上,还有更多选项,它们隐藏在头文件中。 使用这些选项的不同组合构建多个核心可以帮助揭示那些被破坏的组合,也许仅在由它们触发的边缘情况下。
您还可以通过查看/admin-guide/-.txt 找到一些可能影响输入代码的选项。 下面是一个生成配置选项随机组合的脚本,这对于使用 KVM 传递内核命令行非常有用:
标志=“”“nopti =关闭
黑貂=关 l1tf=关 mds=关 =关
kvm.=关闭
nosmt nosmt =关闭
nomce nopat -smp 鼻子
nosmp """.split()
print(' '.join(.(flags, 5)), "=%u" % (.(2), ))
启用后,一些代码将插入到入口代码中,例如用于系统调用和跟踪。 这可能也值得测试,因此我建议在运行之前调整这些文件(位于 /sys// 中):
我们已经看到,改变系统调用进入/退出的处理方式(因为需要停止进程并通知跟踪器),最好在 () 下运行部分进入尝试。 当跟踪进程停止时,尝试调整跟踪进程的一些/所有寄存器也可能很有趣。 完全正确地完成这个任务是非常困难的,所以这里不再赘述。
.sh
当我在虚拟机中进行测试时,我更喜欢将程序绑定并作为 init(pid1) 运行,这样就不需要将其复制到文件系统映像。 您可以使用这样的脚本:
#!/bin/bash
设置-e
设置-x
rm-rf/
MKDIR/
g++ - -Wall -std=c++14 -O2 -g -o /init -lm
(cd / && (查找 | cpio -o -H newc)) \
| gzip -c \
> .entry-fuzz.gz
如果您使用 Qemu/KVM,只需传入 - .entry-fuzz.gz,它将在启动时立即运行。
污点检查
如果我们确实遇到某种内核崩溃或漏洞,确保我们不会错过它们是很有用的。 我个人喜欢在内核命令行上使用参数ops==-1,并将-no-传递给Qemu/KVM; 这将确保任何警告都会导致 Qemu 立即退出(在终端上留下任何诊断信息)。 如果你运行的是专用裸机(例如使用上面的方法),你可以设置panic=0,这只会挂掉机器。
如果您在常规工作站上进行测试并且不想关闭整个机器,您可以检查内核是否被污染(每次出现警告或错误时都会被污染),然后直接退出:
int = open("/proc/sys//", );
如果(== -1)
错误(,错误号,“打开()”);
字符[16];
= 预读(, , (), 0);
如果(== -1)
错误(,errno,“pread()”);
而(1){
// + 运行测试用例
...
字符[16];
= 预读(, , (), 0);
如果(== -1)
错误(,errno,“pread()”);
如果 ( != || (, , )) {
(, " , .\n");
// TODO: 转储十六进制字节或
出口();
博客
如果内核崩溃并且不清楚问题出在哪里,则将尝试到网络的所有内容记录下来会非常有用。 我给出一个UDP日志的简单框架:
int 主函数(...)
整数 = (, , 0);
如果(== -1)
错误(, errno, "(, , 0)");
= {};
。 = ;
。 = htons(21000);
(、“10.5.0.1”、&...);
if ((, (const *) &, ()) == -1)
错误(,错误号,“()”);
...
然后,在为每个入口/出口生成代码后,您可以简单地将其转储到此套接字上:
write(, (char *) mem, out - ( *) mem);
我们希望日志服务器最后收到的数据(这里是10.5.0.1:21000)将包含导致崩溃的汇编代码。 根据具体的用例,有时需要添加某种框架,以便可以轻松确定测试用例具体从哪里开始和结束。
检查是否可以捕获已知漏洞
多年来,在入口代码中发现了许多漏洞。 因此,我们可以构建一些旧的、易受攻击的内核,并在它们之上运行它们,以确保它确实捕获这些已知的漏洞。 我们还可以通过发现漏洞所需的时间来衡量效率,但是,我们必须小心,不要过度优化到只发现这些漏洞的程度。