我的编程空间,编程开发者的网络收藏夹
学习永远不晚

如何测试Linux内核入口代码

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

如何测试Linux内核入口代码

本篇内容介绍了“如何测试Linux内核入口代码”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

通用寄存器+delta状态

到目前为止,我们尚未涉及的一件事是将其他通用寄存器也设置为随机值。入口代码在工作过程中确实会用到某些通用寄存器,如果我们真的在某个地方遇到了问题,那么它很可能因随机值而崩溃。

我们可能还想找出更细微的漏洞——虽然这些漏洞不会使得内核彻底崩溃,但可能会将内核地址泄漏到用户空间从未见过的某个寄存器中。一种检查内核是否正确,是否保存了我们的寄存器/标志等的方法是在从内核模式返回后写出寄存器的状态。这并不难实现,因为我们可以将所有(或者至少是大部分)寄存器的值存放到固定的地址中(例如,在我们已经用于其他用途的数据页中)。这里的难点在于如何将其与在一个子进程中运行多个进入尝试(entry attempts)/系统调用结合起来,因为需要将健全性检查与进入尝试交织在一起,这可能会非常麻烦。

最大限度地降低崩溃的概率

我们在第二篇文章中已经提到,令子进程崩溃的代价相当高,因为这意味着要启动一个全新的子进程。因此,尽可能避免崩溃(并在同一个子进程中运行尽可能多的进入尝试)可能是提高fuzzer性能的可行策略。这包括两个主要部分:

· 保存/恢复行内所需的状态,例如,你要保存和恢复%rsp,以便后续的pushf/popf指令能够继续工作。

· 从信号处理程序中恢复,例如通过安装处理程序,可以将进程恢复到已知的良好状态。

检查生成的汇编代码

虽然代码很容易在生成汇编代码的时候出错,但人们却很难注意到,因为程序都崩溃了,你也看不出你得到的是一个意外的结果。我曾经遇到过类似的问题,但是在2年的时间里一直没有觉察到:我在编码ljmp操作数的地址时,不小心用错了字节顺序,所以在32位兼容模式下,它实际上从来没有运行过任何东西!

一种检查汇编代码的简便方法是使用像udis86这样的反汇编库,然后通过手动方式验证生成的代码。

#include   ...   ud_t u; 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, (unsigned char *) 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寄存器)的大小,并允许fuzzer通过使用超出GDT预期大小的段而导致崩溃。事实证明,这个漏洞是可利用的,并能获得ring 0的执行权限。在另一个案例中,我们看到了在硬件加速的嵌套式客户机(客户机中的客户机)中运行时的交互。

通常,KVM需要模拟底层硬件的某些特性,这增加了相当多的复杂性。fuzzer很有可能在KVM或Xen等管理程序中发现漏洞,因此在不同的裸机CPU和多种管理程序下运行fuzzer是很有价值的。

要想以编程方式创建KVM实例,请参阅Serge Zaitsev撰写的KVM host in a few lines of code一文。

一个相关的有趣实验可能是为运行在x86上的Windows或其他操作系统编译fuzzer,看看它们的效果如何。我在WSL(Windows Subsystem for Linux)上简单地测试了Linux二进制文件,没有发生什么不良情况。

配置/启动选项

配置/启动选项会影响入口代码的具体操作。下面是我在最新的内核中发现的相关选项:

$ grep -o 'CONFIG_[A-Z0-9_]*' arch/x86/entry/entry_64*.S | sort | uniq CONFIG_DEBUG_ENTRY CONFIG_IA32_EMULATION CONFIG_PARAVIRT CONFIG_RETPOLINE CONFIG_STACKPROTECTOR CONFIG_X86_5LEVEL CONFIG_X86_ESPFIX64 CONFIG_X86_L1_CACHE_SHIFT CONFIG_XEN_PV

其实,还有更多的选项,它们都隐藏在头文件中。通过这些选项的不同组合来构建多个内核,可以帮助揭示那些被破坏的组合,也许只有在由fuzzer触发的边缘情况下才会出现。

通过查看Documentation/admin-guide/kernel-parameters.txt,你还可以找到一些可能影响入口代码的选项。这里有一个Python脚本,它可以生成随机的配置选项组合,这对于用KVM传递内核命令行非常有用:

import random   flags = """nopti nospectre_v1 nospectre_v2 spectre_v2_user=off spec_store_bypass_disable=off l1tf=off mds=off tsx_async_abort=off kvm.nx_huge_pages=off noapic noclflush nosmap nosmep noexec32 nofxsr nohugeiomap nosmt nosmt noxsave noxsaveopt noxsaves intremap=off nolapic nomce nopat nopcid norandmaps noreplace-smp nordrand nosep nosmp nox2apic""".split()   print(' '.join(random.sample(flags, 5)), "nmi_watchdog=%u" % (random.randrange(2), ))

ftrace

Ftrace启用时,会在入口代码中插入一些代码,例如用于系统调用和irqflags跟踪。这可能也非常值得进行测试,所以我建议在运行fuzzer之前,不妨调整一下这些文件(位于/sys/kernel/tracing路径下):

如何测试Linux内核入口代码

PTRACE_SYSCALL

我们已经看到,ptrace改变了处理系统调用进入/退出的方式(因为需要停止进程并通知跟踪器),所以最好在ptrace()下使用ptrace_syscall运行一部分进入尝试。当被ptrace停止时,尝试调整被跟踪的进程的一些/所有寄存器也可能很有趣。要完全正确地完成这个任务是非常困难的,所以这里就不多介绍了。

mkinitrd.sh

当我在VM中进行测试时,我更喜欢将程序绑定在initrd中,并以init(pid1)的形式运行,这样就不需要将其复制到文件系统映像上。您可以使用如下所示的脚本:

#! /bin/bash   set -e set -x   rm -rf initrd/ mkdir initrd/ g++ -static -Wall -std=c++14 -O2 -g -o initrd/init main.cc -lm   (cd initrd/ && (find | cpio -o -H newc)) \     | gzip -c \     > initrd.entry-fuzz.gz

如果你使用的是Qemu/KVM,只要传入-initrd initrd.entry-fuzz.gz,它就会在开机后立即运行fuzzer。

污点检查

如果fuzzer真的遇到了某种内核崩溃或漏洞,那么确保我们不会遗漏它们是很有用的。我个人喜欢在内核命令行中使用参数ops=panic panic_on_warn panic=-1,并将-no-reboot传递给Qemu/KVM;这将确保任何警告都会立即导致Qemu退出(将任何诊断程序留在终端上)。如果你正在使用专门的裸机运行fuzzer(例如,使用上面的initrd方法),可以令panic=0,这样只会挂起机器。

如果你在普通的工作站上进行测试,并且不想让整台机器挂掉,则可以检查内核是否被污染(每当出现警告或漏洞时都会被污染),然后直接地退出:

int tainted_fd = open("/proc/sys/kernel/tainted", O_RDONLY); if (tainted_fd == -1)     error(EXIT_FAILURE, errno, "open()");   char tainted_orig_buf[16]; ssize_t tainted_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 + run test case       ...       char tainted_buf[16];     ssize_t tainted_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, "Kernel became tainted, stopping.\n");         // TODO: dump hex bytes or disassembly         exit(EXIT_FAILURE);     } }

网络日志

如果内核崩溃了,并且不清楚问题出在哪里,那么将所有正在尝试的内容记录到网络中是非常有用的。我将给出一个UDP日志的简单框架:

int main(...) {     int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);     if (udp_socket == -1)         error(EXIT_FAILURE, errno, "socket(AF_INET, SOCK_DGRAM, 0)");       struct sockaddr_in remote_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, (const struct sockaddr *) &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)会包含导致崩溃的汇编代码。根据具体的用例,有时需要添加某种框架,以便可以轻松地判断出测试用例的具体开始和结束位置。

检查fuzzer是否能捕捉到已知的漏洞

多年来,人们已经在入口代码中找到了许多漏洞。因此,我们可以构建一些旧的、有漏洞的内核,并在它们上面运行fuzzer,以确保它确实能捕捉到这些已知的漏洞。我们也可以用寻找漏洞所花费的时间来衡量fuzzer的效率,但是,我们必须小心,不要过度优化,以防止它们只找到这些漏洞。

代码覆盖率/插桩技术反馈

插桩技术

AFL和syzkaller这样的fuzzer如此有效的原因之一是,它们使用代码复盖率来非常精确地衡量调整测试用例的各个二进制位的效果。这通常是通过使用一个特殊的编译器标志编译C代码来实现的,该标志发出额外的代码来收集覆盖率数据。对于汇编代码,尤其是入口代码,这是一个非常棘手的问题,因为如果不手动检查代码的每个指令,我们就无法知道CPU到底处于什么状态(以及我们可以破坏哪些寄存器/状态)。

但是,如果我们真的想要提高代码覆盖率,有一种方法可以做到:x86指令集包含一条指令,该指令同时接受一个立即数和一个立即数地址,并且不影响任何其他状态(例如标志):movb$value,(addr)。我们唯一需要注意的是:确保addr是一个编译时常量地址,它总是映射到某个物理内存,并在页表中标记为present,这样我们在访问它时就不会出现页面错误。幸运的是,Linux已经提供了一种机制:fixmaps,也就是“编译时虚拟内存分配”。这样,我们就可以静态地分配一个编译时常量虚拟地址,该地址指向所有任务和上下文的相同底层物理页面。由于它是在任务之间共享的,因此当在进程之间切换时,我们必须清除或以其他方式保存/恢复这些值。

通过组合使用C宏和汇编器宏,我们可以得到一个侵入性非常低的覆盖原语,你可以在入口代码中的任何地方加入这个原语,来记录所采用的代码路径。我已经编写了一个补丁,但还有一些边缘情况需要解决(例如,当SMAP被启用时,它并不完全有效)。此外,我怀疑x86的维护者是否会喜欢在入口代码中掺杂这些覆盖率注释。

在fuzzer方面,有一件事让插桩技术反馈变得更加复杂,那就是你需要一个完整的系统来跟踪测试用例、结果以及(可能的)你对每个测试用例应用了哪些突变。正因为如此,我选择暂时忽略代码覆盖率;无论如何,这都是一个宽泛的fuzzing话题,与x86或特别是入口代码没有太大关系。

性能计数器/硬件反馈

收集代码覆盖率的一种完全不同的方法是使用性能计数器。我知道最近有两个项目就是这样做的:

· Resmack Fuzz Test

· kAFL

这里最大的好处显然是不需要进行检测(修改内核)。最大的缺点在于性能计数器不是完全确定的(可能是由于硬件中断等外部因素所致)。也许它对入口代码也不起作用,因为在汇编代码上只花费了很短的时间。

“如何测试Linux内核入口代码”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注编程网网站,小编将为大家输出更多高质量的实用文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

如何测试Linux内核入口代码

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Linux系统如何查看内核代码

要查看Linux系统的内核代码,可以按照以下步骤进行操作:1. 下载内核源代码:- 可以从官方网站上下载最新的内核源代码,网址为 https://www.kernel.org/- 也可以通过Git命令克隆Linux内核的源代码仓库,命令如下
2023-10-18

如何让Linux内核源码规范

如何让Linux内核源码规范,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。从编码风格错误开始曾经在开发Linux内核驱动的时候,创建了一个补丁文件,但是在把补丁
2023-06-15

如何分析Linux内核源码do_fork

本篇文章为大家展示了如何分析Linux内核源码do_fork,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。我们都知道进程是Linux内核中最为重要的一个抽象概念,那么我们平时在fork一个进程时,该
2023-06-16

如何进行linux内核模块调试

这篇文章将为大家详细讲解有关如何进行linux内核模块调试,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。1. 开启虚拟机,虚拟机运行到 kgdb: Waiting for connectio
2023-06-16

Linux如何测试udp端口连接

在Linux上,你可以使用`netcat`命令来测试UDP端口的连接。以下是测试UDP端口连接的步骤:1. 打开终端(命令行界面)。2. 输入以下命令:```shellnc -u ```替换``和``为要测试的UDP端口的IP地址和端口号
2023-10-11

如何调试和测试SQL拼接代码

调试和测试SQL拼接代码的步骤如下:使用日志输出:在拼接SQL语句的过程中,可以使用日志输出来打印拼接的结果,以便查看拼接是否正确,可以使用System.out.println()或者日志库来输出拼接的SQL语句。使用IDE的调试功能:在I
如何调试和测试SQL拼接代码
2024-04-29

openSUSE如何给内核源代码打补丁

小编给大家分享一下openSUSE如何给内核源代码打补丁,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!内核源码升级可能使某个补丁失效,所以并不是一个补丁可以"补"
2023-06-16

探究一个LED如何入门Linux内核

目录前言led trigger开始探索LED 设备注册leds 目录触类旁通class 目录的产生start_kernel()Starting kernel …uboot附完整调用关系人生切入点 前言 最近项目上需要用到 LED 子系统,在
2022-06-04

PHP如何优化单元测试代码

这篇文章将为大家详细讲解有关PHP如何优化单元测试代码,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。1、单元测试通过实现单一责任原则(我们的代码应该只关注功能的单个部分),我们将确保在测试期间,我们只会同
2023-06-15

Linux块层多队列中如何引入内核

本篇内容主要讲解“Linux块层多队列中如何引入内核”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Linux块层多队列中如何引入内核”吧!首先过目一下多队列架构:以读IO为例,单队列和多队列相同
2023-06-15

Golang 函数测试中如何实现代码覆盖率测试?

回答: 在 golang 函数测试中实现代码覆盖率测试的步骤如下:步骤:安装覆盖率包:go get golang.org/x/tools/cmd/cover。导入覆盖率包并设置覆盖模式。定义被测函数。使用覆盖率命令运行测试文件。查看 cov
Golang 函数测试中如何实现代码覆盖率测试?
2024-04-16

Linux中如何测试端口的连通性

这篇文章给大家分享的是有关Linux中如何测试端口的连通性的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。Linux系统有时候需要测试某个端口的连通性,用户可以参考如下方法来测试。方法一、telnet法 telne
2023-06-12

Linux下如何调试c++代码

这篇文章主要为大家展示了“Linux下如何调试c++代码”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“Linux下如何调试c++代码”这篇文章吧。Linux下调试C++代码具体方法1.编写好代码
2023-06-28

如何进行C++代码的性能测试?

如何进行C++代码的性能测试?概述:在软件开发过程中,性能测试是一项非常重要的任务。对于C++代码来说,性能测试可以帮助开发人员了解代码的执行效率,找到性能瓶颈,并对其进行优化。本文将介绍一些常用的C++代码性能测试方法和工具,帮助开发人员
如何进行C++代码的性能测试?
2023-11-02

golang函数的测试代码如何组织?

组织 golang 测试代码的最佳实践:文件结构:每个包的测试代码应放置在以 _test.go 后缀结尾的独立文件中。测试函数命名:使用 func test_() 命名测试函数,描述其测试的内容。测试表:使用测试表组织涉及多个输入/输出值的
golang函数的测试代码如何组织?
2024-04-28

编程热搜

目录