-
-
[翻译]fuzzer开发 1:一个新机器
-
发表于: 2024-9-4 09:40 4097
-
很长一段时间以来,我一直想在博客上利用周末和空闲时间开发一个模糊测试器,但由于种种原因,我从未真正构思出一个既有教育意义又能为模糊测试社区提供某种实用性的项目。最近,由于Linux内核漏洞利用的原因,我对 Nyx 产生了浓厚的兴趣。Nyx 是一个基于 KVM 的虚拟机模糊测试器,可以用来对传统上难以进行模糊测试的目标进行快照模糊测试。很多时候(大多数时候?),我们想要模糊测试的东西并不自然地适合传统的模糊测试方法。当面对模糊测试中的目标复杂性时(暂且不论输入生成和细微差别),通常有两种方法。
一种方法是对目标进行“脑叶切除”,以便你可以隔离出你认为“有趣”的一小部分目标,然后只对此进行模糊测试。这可以有许多形式,比如将内核子系统的一小部分从内核中剥离出来,并将其编译成可以用传统模糊测试工具进行测试的用户态应用程序。这也可以表现为从 Web 浏览器中提取一个输入解析例程,并仅对解析逻辑进行模糊测试。然而,这种方法有其局限性,在理想情况下,我们希望对任何可能接触到或受到这种“有趣”目标逻辑影响的东西进行模糊测试。这种“脑叶切除”方法极大地减少了我们可以探索的目标状态的数量。试想一下,假设解析例程成功生成了一个数据结构,而这个数据结构随后被其他目标逻辑所使用,并且实际上暴露了一个漏洞。这种模糊测试方法未能探索这种可能性。
另一种方法是有效地将目标沙盒化,以便你可以对其执行环境施加一些控制,并对整个目标进行模糊测试。这是像 Nyx 这样的模糊测试器所采用的方法。通过对整个虚拟机进行快照模糊测试,我们能够以更大范围探索状态的方式来测试复杂的目标,例如 Web 浏览器或内核。Nyx 为我们提供了一种对整个虚拟机/系统进行快照模糊测试的方法。在我看来,这是模糊测试的理想方式,因为你大大缩小了一个人为设计的模糊测试环境与目标应用程序在“现实世界”中存在的方式之间的差距。当然,这里存在权衡,其中之一就是模糊测试工具本身的复杂性。但是,我认为考虑到复杂的本地代码应用程序可能存在无限漏洞的倾向,为了提高我们模糊测试工作流程的漏洞发现潜力,这种手工劳动和复杂性是值得的。
因此,为了理解 Nyx 的工作原理,以便我可以在其基础上构建一个模糊测试器,我重新观看了 gamozolabs(Brandon Falk)在 Nyx 论文上做的流媒体纸质评论。那是一个很棒的直播,Nyx 的作者参与了 Twitch 聊天,因此有一些不错的讨论,这场直播真正突出了 Nyx 在模糊测试中作为一个惊人工具的价值。但在直播中,除了 Nyx 之外,还有其他一些东西引起了我的兴趣!在直播中,Gamozo 描述了他以前构建的一种模糊测试架构,该架构利用 Bochs 模拟器对复杂目标和整个系统进行快照模糊测试。这种架构对我来说非常有趣且聪明,巧合的是,它与我和朋友为模糊测试设计的沙盒工具也有几个共同点。
这种模糊测试架构似乎符合我在博客上进行模糊测试器开发项目时个人看重的几个标准:
综合考虑,这似乎是一个在博客上实施的理想项目,所以我联系了 Gamozo 以确保他没问题,我不想被认为是在追逐他的创意,他非常慷慨并鼓励我去做。所以非常感谢 Gamozo 分享了这么多内容,我们现在开始开发模糊测试器。
也非常感谢 @is_eqv 和 @ms_s3c,至少 Nyx 的两位作者,他们总是非常友好并慷慨地回答问题。这是社区中值得拥有的好人。
另一位要感谢的是 @Kharosx0,他帮助我理解 Bochs 并回答了我所有关于设计意图的问题,又是一位非常慷慨的人,他总是在 Fuzzing Discord 上帮助别人。
如果你发现任何编程错误或对代码有一些挑剔之处,请告诉我。我已经尽量对所有内容进行了详细注释,考虑到我在几个周末的时间里拼凑了这些内容,代码中可能存在一些问题。我也还没有真正确定仓库的结构,或者文件的名称,或者任何类似的内容,所以请对代码质量保持耐心。这主要是出于学习目的,目前它只是将 Bochs 加载到内存中的一个概念验证,用来解释架构的第一部分。
我决定暂时将这个项目命名为“Lucid”,作为对清醒梦的参考,因为我们的模糊测试目标在模拟器中执行时处于某种梦境状态。
Bochs 是什么?好问题。Bochs 是一个 x86 全系统模拟器,能够运行一个带有软件模拟硬件设备的完整操作系统。简而言之,它是一个没有 JIT(即时编译)的、更小、更简单的模拟工具,类似于 QEMU,但用例少得多,性能也低得多。与 QEMU 的“让我们模拟任何东西并以良好的性能完成它”方法不同,Bochs 采用了“让我们完全在软件中模拟整个 x86 系统,而不需要太担心性能”的方法。这种方法有其明显的缺点,但如果你只对运行 x86 系统感兴趣,Bochs 是一个很好的工具。我们将使用 Bochs 作为我们的模糊测试器中的目标执行引擎。我们的目标代码将在 Bochs 中运行。因此,如果我们正在模糊测试 Linux 内核,那么该内核将驻留并在 Bochs 中执行。Bochs 是用 C++ 编写的,并且显然仍在维护,但不要指望有太多代码更改或快速开发,最后一次发布是在两年多前。
这是我们讨论模糊测试器将如何根据 Gamozo 在直播中描述的信息进行设计的部分。简单来说,我们将创建一个“模糊测试器”进程,它将执行 Bochs,而 Bochs 又将执行我们的模糊测试目标。我们不会在每次模糊测试迭代中对目标进行快照和恢复,而是重置包含目标及其所有模拟状态的 Bochs。通过对 Bochs 进行快照和恢复,我们实际上在对目标进行快照和恢复。
进一步深入一点,这个设置要求我们将 Bochs 沙盒化并在我们的“模糊测试器”进程中运行它。为了将 Bochs 与用户的操作系统和内核隔离开来,我们将对 Bochs 进行沙盒化,以便它无法与我们的操作系统进行交互。这使我们能够实现一些目标,但最重要的是,这应该使 Bochs 变得确定性。正如 Gamozo 在直播中所解释的那样,将 Bochs 与操作系统隔离开来,可以防止 Bochs 访问任何随机/伪随机的数据源。这意味着我们将防止 Bochs 向内核发出系统调用,以及执行任何检索硬件源数据的指令,如 CPUID
或类似的东西。我实际上还没有考虑到后者,但我对系统调用有一个计划。随着 Bochs 与操作系统隔离开来,我们可以期望它在每次模糊测试迭代中以相同的方式运行。给定模糊测试输入 A,Bochs 应该在接下来的 1 万亿次迭代中完全以相同的方式执行。
其次,这还意味着 Bochs 的所有状态都会包含在我们的沙盒中,这应该使我们能够更轻松地重置 Bochs 的状态,而不是它作为一个远程进程。在一个正常情况下 Bochs 作为一个普通 Linux 进程执行的范例中,重置其状态并不简单,可能需要一种重手段,比如在每次模糊测试迭代中在内核中进行页表遍历,或者更糟的方式。
总的来说,这就是我们的模糊测试设置应该如何工作的概述:
为了提供一个沙盒环境,我们必须将一个可执行的Bochs镜像加载到我们自己的模糊测试器进程中。为此,我选择将Bochs构建为一个ELF文件,然后将该ELF文件加载到模糊测试器进程的内存中。让我们深入探讨一下迄今为止是如何实现这一点的。
为了尽可能简化将Bochs加载到内存中的过程,我选择将Bochs编译为-static-pie
ELF。这意味着生成的ELF对于其加载位置没有任何预期要求。在其_start
例程中,实际上包含了所有OS ELF加载器的正常逻辑,这些逻辑是执行自身重定位所必需的。这是不是很酷?但在我们深入探讨之前,第一个目标只是简单地构建并加载一个-static-pie
测试程序,并确保我们能正确地做到这一点。
为了确保我们已正确实现所有内容,我们将确保测试程序可以正确访问我们传递的任何命令行参数,并且能够执行和退出。
记住,在这一点上,我们还没有对加载的程序进行沙盒处理,我们此时只是尝试将其加载到fuzzer虚拟地址空间中并跳转到它,并确保堆栈和一切都正确设置。因此,如果我们直接跳转到执行Bochs,可能会遇到一些不是实际问题的问题。
编译测试程序并使用readelf -l
检查时,我们可以看到实际上存在一个DYNAMIC
段。这可能是因为在前述的_start
例程中需要执行的重定位。
那我们实际上需要关心这个ELF镜像的哪些部分用于加载目的呢?我们可能不需要大部分信息来简单地将ELF加载并运行。起初,我不知道需要什么,所以我只是解析了所有的ELF头。
考虑到这个ELF解析代码不需要健壮,因为我们只使用它来解析和加载我们自己的可执行文件,我只是确保在解析各种头文件时生成的可执行文件没有明显的问题。
我以前写过ELF解析代码,但不太记得它是如何工作的,所以我不得不重新学习一切,参考了维基百科:https://en.wikipedia.org/wiki/Executable_and_Linkable_Format。幸运的是,我们不是在尝试解析任意的ELF文件,只是解析我们自己构建的64位ELF文件。目标是从ELF头信息中创建一个数据结构,该结构为我们提供将ELF加载到内存中所需的数据。因此,我跳过了一些ELF头的值,但最终将ELF头解析成了以下数据结构:
我们真正关心的是这些结构成员中的几个。首先,我们肯定需要知道入口地址,这是你应该开始执行的地方。因此,最终我们的代码将跳转到这个地址以开始执行测试程序。我们还关心phoff
。这是我们可以找到程序头表基址的ELF偏移量。这只是一个程序头的数组。除了phoff
之外,我们还需要知道数组中条目的数量和每个条目的大小,以便我们可以解析它们。这就是phnum
和phentsize
分别派上用场的地方。给定数组索引0的偏移量、数组成员的数量以及每个成员的大小,我们可以解析程序头。
单个程序头,即数组中的单个条目,可以被综合成以下数据结构:
这些程序头描述了ELF镜像在内存中应该存在的段。特别是,我们关心的是具有LOAD类型的可加载段,因为这些段是我们在加载ELF镜像时必须考虑的段。以我们的readelf输出为例:
我们可以看到有4个可加载段。它们还有几个我们需要跟踪的属性:
对于我们自己创建的-static-pie
ELF的非常简化的用例,我们基本上可以忽略解析的ELF的其他部分。
现在我们已经成功解析出ELF文件的相关属性,我们可以在内存中创建一个可执行镜像。目前,我选择仅在Linux环境中实现所需的功能,但没有理由不能将这个ELF加载到我们的内存中,即使我们是一个Windows用户态进程。这也就是这个设计很酷的原因之一。某个时候,可能有人会想要添加Windows支持,我们就会加上它。
我们需要做的第一件事是,根据标记为LOAD
的段的组合大小计算我们需要的虚拟内存大小。我们还必须记住,在未对齐的段之后有一些填充,为此,我采用了以下逻辑:
我们遍历解析的ELF中的所有程序头,查看最大的“end_addr
”在哪里。这也考虑了段之间的页对齐填充。如你所见,我们还对最后一个段进行了页对齐,确保其大小四舍五入到最接近的页大小。此时我们知道需要多少内存来mmap
以容纳可加载的ELF段。我们在这里mmap
了一段连续的内存区域:
现在我们已经划出足够的内存来写入可加载的段。段数据当然是从文件中获取的,所以我们首先再次遍历程序头,提取我们需要的所有相关数据,以便将文件数据从内存中memcpy到我们刚创建的内存中。你可以在这里看到这个逻辑:
在提取了段的元数据后,我们可以复制内容,并调用mprotect在内存中的段上,使其权限完全匹配我们之前讨论的Flags段元数据。这个逻辑在这里:
在那之后,如果成功了,我们的ELF镜像基本上就完成了。我们可以跳转到它并开始执行!开玩笑的,我们首先需要为新“进程”设置一个堆栈,我了解到这是一个巨大的痛点。
我在这上面花了很多时间,实际上可能还有一些bug!我会说这是最难的部分,因为其他部分基本上都是直截了当的。为了完成这部分,我严重依赖于这个描述如何构建x86 32位应用程序堆栈的资源:https://articles.manugarg.com/aboutelfauxiliaryvectors。
以下是从上述链接资源中摘取的一个非常有用的描述32位堆栈的图表:
当我们在命令行上传递参数给进程时,比如ls / -laht
,Linux操作系统必须将ls
ELF加载到内存中并创建其环境。在这个例子中,我们还向进程传递了几个参数值,比如/
和-laht
。操作系统通过参数向量(即argv
的简写)将这些参数传递给进程,argv
是一个字符串指针的数组。参数的数量由参数计数(即argc
)表示。argv
的第一个成员通常是命令行上传递的可执行文件的名称,所以在我们的例子中它将是ls
。如你所见,堆栈上的第一件事,即堆栈的顶部,位于堆栈地址范围的下端,是argc,接下来是所有表示程序参数的字符串数据指针。重要的是要注意数组在末尾是以NULL
终止的。
在那之后,我们有一个类似的数据结构,即envp
数组,它是表示环境变量的字符串数据指针的数组。你可以通过在GDB下运行一个程序并使用命令show environment
自己检索这些数据,环境变量通常以“KEY=VALUE”的形式表示,例如在我的机器上,语言环境变量的键值对是“LANG=en_US.UTF-8
”。对于我们的目的,我们可以忽略环境变量。这个向量也是以NULL
终止的。
接下来是辅助向量(auxiliary vector),这对我们来说非常重要。此信息详细描述了程序的几个方面。向量中的这些辅助条目每个条目占用16字节。它们像我们的环境变量条目一样包含一个键和值,但这些基本上是u64值。对于测试程序,我们实际上可以通过在GDB中使用info aux
来转储辅助信息。
键在左边,值在右边。例如,在堆栈上我们可以预期AT_PHNUM
的值是0x5,这描述了程序头的数量,伴随的值是12
。我们可以转储堆栈并看到这一点。
在数据的末端,可以看到在0x7fffffffe2d8
处有键0x5
,而在0x7fffffffe2e0
处有值0xc
(即十六进制的12)。为了正确加载我们的ELF,我们需要其中的一些信息,因为ELF的_start
例程需要其中一些信息来正确设置环境。我在堆栈中包含了以下这些项,它们可能并非全都必要:
因此,在考虑了所有这些值后,我们现在知道了构建程序堆栈所需的所有数据。
首先,我们需要分配内存来容纳Bochs的堆栈,因为我们需要知道它映射的位置,以便制定我们的指针。我们将知道表示堆栈数据的向量中的偏移量,但除非提前知道堆栈在内存中的位置,否则我们无法知道绝对地址。分配堆栈非常简单,因为我只是像处理程序段一样使用了mmap
。现在我使用了1MB
的堆栈,这似乎足够大了。
在我的堆栈创建逻辑中,我从底部开始创建堆栈,然后将值插入到堆栈顶部。
因此,我们放置到堆栈的第一个值是图表中的“end-marker”,在Rust中它只是一个0u64
。
接下来,我们需要将所有需要的字符串放入堆栈,即我们的命令行参数。为了分隔fuzzer的命令行参数和Bochs的命令行参数,我创建了一个命令行参数--bochs-args
,它用来作为这两类参数之间的分界点。--bochs-args
之后的每个参数都是给Bochs的。我遍历所有提供的命令行参数,然后将它们放入堆栈中。我还记录了每个字符串参数的长度,这样以后在需要在argv
向量中放置指向字符串的指针时,我们可以计算它们的绝对地址。顺便说一句,我还确保在字符串推送过程中保持8字节对齐,这样我们就不必处理任何奇怪的指针值。这不是必需的,但使得我更容易推理堆栈状态。这是通过以下逻辑执行的:
推送字符串的操作如下:
然后我们添加一些填充和辅助向量成员:
然后,由于我们忽略了环境变量,我们只是在堆栈上推送了一个NULL
指针,并且还推送了终止argv
向量的NULL
指针:
这是我花了很多时间调试的地方。现在我们必须将指针添加到我们的参数中。为此,我首先计算了堆栈数据的总长度,因为我们已经知道了所有变量部分的长度,比如参数的数量和所有字符串的长度。我们已经知道了当前存在的堆栈长度,其中包括了字符串,并且我们知道还剩下多少指针和成员需要添加到堆栈中(参数数量和argc
)。既然我们知道这些信息,我们就可以在将argv
指针推入堆栈时计算字符串数据的绝对地址。计算长度的方法如下:
接下来,我们从堆栈的底部开始,创建一个可移动的偏移量,该偏移量将通过堆栈跟踪,直到每个字符串的开始位置,以便我们可以计算其绝对地址。偏移量表示从堆栈顶部到达的深度。一开始,偏移量是可以达到的最大值,因为它位于堆栈的底部(高内存地址)。我们从中减去值,以指向我们推入堆栈的每个argv
字符串的开头。因此,堆栈的底部看起来是这样的:
因此,凭借我们记录的参数及其长度,我们可以在每次迭代参数长度时调整偏移量,以指向字符串的开头。不过,有一个陷阱,在第一次迭代时,我们必须考虑到结束标记及其8字节。因此,逻辑如下:
这非常酷!而且看起来确实有效?最后,我们用argc
封装堆栈,并完成了在向量中填充所有堆栈数据的操作。接下来,我们要将数据复制到堆栈分配中,这很简单,所以这里没有代码片段。
我认为值得注意的最后一件事是,我创建了一个名为STACK_DATA_MAX
的常量,堆栈数据的长度不能超过这个可调节的值。我们使用这个值在将程序加载到内存中并开始执行时设置RSP
。RSP
被设置为绝对最低地址,即堆栈分配大小减去STACK_DATA_MAX
。这样,当堆栈增长时,我们为堆栈的增长留下了尽可能多的空间,因为堆栈在内存中是向下增长的。
此时,一切都应该在内存中正确设置好了,我们只需跳转到目标代码并开始执行。目前,我还没有完善上下文切换例程或其他任何东西,我们实际上只是跳转到程序并执行它,希望一切顺利。实现这一目标的代码非常简单:
我们清除RDX
的原因是,如果_start
例程看到RDX
中的值不为零,它将解释为我们试图注册一个位于RDX
地址的挂钩,以在程序退出时调用它。目前我们不需要运行这样的挂钩,所以暂时将其设置为NULL
。其他寄存器的值并不重要。我们将程序入口点移动到RAX
中,并将其用作长跳转目标,并提供我们手工制作的RSP
,以便程序有一个堆栈可以用来进行重定位并正确运行。
程序运行,解析我们的命令行参数,并退出而没有崩溃!所以看起来一切都已经准备就绪。通常这会是一个很好的停止点,但我出于病态的好奇……
我们必须看看,对吧?首先,我们必须将Bochs编译为-static-pie
ELF,这本身就是一个头痛的问题,但我最终弄明白了。
Bochs运行了!它无法理解我们胡乱编写的命令行参数,但是我们成功地加载并运行了它。
接下来的步骤和博客文章将是开发一个上下文切换例程,我们将使用它在Fuzzer执行和Bochs执行之间进行切换。这将涉及每次保存我们的状态,并且基本上以类似于正常用户到内核上下文切换的方式运行。
之后,我们必须非常熟悉Bochs,并尝试在普通的Bochs中运行一个目标。一旦我们做到这一点,我们将尝试在Fuzzer中运行它。
https://github.com/h0mbre/Lucid
本文使用chatGPT-4o翻译而成,如有错误之处,请斧正
原文链接:https://h0mbre.github.io/New_Fuzzer_Project/
#include <stdio.h>
#include <unistd.h>
int
main(
int
argc,
char
*argv[]) {
printf
(
"Argument count: %d\n"
, argc);
printf
(
"Args:\n"
);
for
(
int
i = 0; i < argc; i++) {
printf
(
" -%s\n"
, argv[i]);
}
size_t
iters = 0;
while
(1) {
printf
(
"Test alive!\n"
);
sleep(1);
iters++;
if
(iters > 5) {
return
0; }
}
}
#include <stdio.h>
#include <unistd.h>
int
main(
int
argc,
char
*argv[]) {
printf
(
"Argument count: %d\n"
, argc);
printf
(
"Args:\n"
);
for
(
int
i = 0; i < argc; i++) {
printf
(
" -%s\n"
, argv[i]);
}
size_t
iters = 0;
while
(1) {
printf
(
"Test alive!\n"
);
sleep(1);
iters++;
if
(iters > 5) {
return
0; }
}
}
dude@lol:~
/lucid
$ gcc
test
.c -o
test
-static-pie
dude@lol:~
/lucid
$
file
test
test
: ELF 64-bit LSB shared object, x86-64, version 1 (GNU
/Linux
), dynamically linked, BuildID[sha1]=6fca6026edb756fa32c966844b29529d579e83b9,
for
GNU
/Linux
3.2.0, not stripped
dude@lol:~
/lucid
$ readelf -l
test
Elf
file
type
is DYN (Shared object
file
)
Entry point 0x9f50
There are 12 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000008158 0x0000000000008158 R 0x1000
LOAD 0x0000000000009000 0x0000000000009000 0x0000000000009000
0x0000000000094d01 0x0000000000094d01 R E 0x1000
LOAD 0x000000000009e000 0x000000000009e000 0x000000000009e000
0x00000000000285e0 0x00000000000285e0 R 0x1000
LOAD 0x00000000000c6de0 0x00000000000c7de0 0x00000000000c7de0
0x0000000000005350 0x0000000000006a80 RW 0x1000
DYNAMIC 0x00000000000c9c18 0x00000000000cac18 0x00000000000cac18
0x00000000000001b0 0x00000000000001b0 RW 0x8
NOTE 0x00000000000002e0 0x00000000000002e0 0x00000000000002e0
0x0000000000000020 0x0000000000000020 R 0x8
NOTE 0x0000000000000300 0x0000000000000300 0x0000000000000300
0x0000000000000044 0x0000000000000044 R 0x4
TLS 0x00000000000c6de0 0x00000000000c7de0 0x00000000000c7de0
0x0000000000000020 0x0000000000000060 R 0x8
GNU_PROPERTY 0x00000000000002e0 0x00000000000002e0 0x00000000000002e0
0x0000000000000020 0x0000000000000020 R 0x8
GNU_EH_FRAME 0x00000000000ba110 0x00000000000ba110 0x00000000000ba110
0x0000000000001cbc 0x0000000000001cbc R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x00000000000c6de0 0x00000000000c7de0 0x00000000000c7de0
0x0000000000003220 0x0000000000003220 R 0x1
Section to Segment mapping:
Segment Sections...
00 .note.gnu.property .note.gnu.build-
id
.note.ABI-tag .gnu.
hash
.dynsym .dynstr .rela.dyn .rela.plt
01 .init .plt .plt.got .plt.sec .text __libc_freeres_fn .fini
02 .rodata .stapsdt.base .eh_frame_hdr .eh_frame .gcc_except_table
03 .tdata .init_array .fini_array .data.rel.ro .dynamic .got .data __libc_subfreeres __libc_IO_vtables __libc_atexit .bss __libc_freeres_ptrs
04 .dynamic
05 .note.gnu.property
06 .note.gnu.build-
id
.note.ABI-tag
07 .tdata .tbss
08 .note.gnu.property
09 .eh_frame_hdr
10
11 .tdata .init_array .fini_array .data.rel.ro .dynamic .got
dude@lol:~
/lucid
$ gcc
test
.c -o
test
-static-pie
dude@lol:~
/lucid
$
file
test
test
: ELF 64-bit LSB shared object, x86-64, version 1 (GNU
/Linux
), dynamically linked, BuildID[sha1]=6fca6026edb756fa32c966844b29529d579e83b9,
for
GNU
/Linux
3.2.0, not stripped
dude@lol:~
/lucid
$ readelf -l
test
Elf
file
type
is DYN (Shared object
file
)
Entry point 0x9f50
There are 12 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000008158 0x0000000000008158 R 0x1000
LOAD 0x0000000000009000 0x0000000000009000 0x0000000000009000
0x0000000000094d01 0x0000000000094d01 R E 0x1000
LOAD 0x000000000009e000 0x000000000009e000 0x000000000009e000
0x00000000000285e0 0x00000000000285e0 R 0x1000
LOAD 0x00000000000c6de0 0x00000000000c7de0 0x00000000000c7de0
0x0000000000005350 0x0000000000006a80 RW 0x1000
DYNAMIC 0x00000000000c9c18 0x00000000000cac18 0x00000000000cac18
0x00000000000001b0 0x00000000000001b0 RW 0x8
NOTE 0x00000000000002e0 0x00000000000002e0 0x00000000000002e0
0x0000000000000020 0x0000000000000020 R 0x8
NOTE 0x0000000000000300 0x0000000000000300 0x0000000000000300
0x0000000000000044 0x0000000000000044 R 0x4
TLS 0x00000000000c6de0 0x00000000000c7de0 0x00000000000c7de0
0x0000000000000020 0x0000000000000060 R 0x8
GNU_PROPERTY 0x00000000000002e0 0x00000000000002e0 0x00000000000002e0
0x0000000000000020 0x0000000000000020 R 0x8
GNU_EH_FRAME 0x00000000000ba110 0x00000000000ba110 0x00000000000ba110
0x0000000000001cbc 0x0000000000001cbc R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x00000000000c6de0 0x00000000000c7de0 0x00000000000c7de0
0x0000000000003220 0x0000000000003220 R 0x1
Section to Segment mapping:
Segment Sections...
00 .note.gnu.property .note.gnu.build-
id
.note.ABI-tag .gnu.
hash
.dynsym .dynstr .rela.dyn .rela.plt
01 .init .plt .plt.got .plt.sec .text __libc_freeres_fn .fini
02 .rodata .stapsdt.base .eh_frame_hdr .eh_frame .gcc_except_table
03 .tdata .init_array .fini_array .data.rel.ro .dynamic .got .data __libc_subfreeres __libc_IO_vtables __libc_atexit .bss __libc_freeres_ptrs
04 .dynamic
05 .note.gnu.property
06 .note.gnu.build-
id
.note.ABI-tag
07 .tdata .tbss
08 .note.gnu.property
09 .eh_frame_hdr
10
11 .tdata .init_array .fini_array .data.rel.ro .dynamic .got
// Constituent parts of the Elf
#[derive(Debug)]
pub
struct
ElfHeader {
pub entry: u64,
pub phoff: u64,
pub shoff: u64,
pub phentsize: u16,
pub phnum: u16,
pub shentsize: u16,
pub shnum: u16,
pub shrstrndx: u16,
}
// Constituent parts of the Elf
#[derive(Debug)]
pub
struct
ElfHeader {
pub entry: u64,
pub phoff: u64,
pub shoff: u64,
pub phentsize: u16,
pub phnum: u16,
pub shentsize: u16,
pub shnum: u16,
pub shrstrndx: u16,
}
#[derive(Debug)]
pub
struct
ProgramHeader {
pub typ: u32,
pub flags: u32,
pub offset: u64,
pub vaddr: u64,
pub paddr: u64,
pub filesz: u64,
pub memsz: u64,
pub align: u64,
}
#[derive(Debug)]
pub
struct
ProgramHeader {
pub typ: u32,
pub flags: u32,
pub offset: u64,
pub vaddr: u64,
pub paddr: u64,
pub filesz: u64,
pub memsz: u64,
pub align: u64,
}
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000008158 0x0000000000008158 R 0x1000
LOAD 0x0000000000009000 0x0000000000009000 0x0000000000009000
0x0000000000094d01 0x0000000000094d01 R E 0x1000
LOAD 0x000000000009e000 0x000000000009e000 0x000000000009e000
0x00000000000285e0 0x00000000000285e0 R 0x1000
LOAD 0x00000000000c6de0 0x00000000000c7de0 0x00000000000c7de0
0x0000000000005350 0x0000000000006a80 RW 0x1000
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000008158 0x0000000000008158 R 0x1000
LOAD 0x0000000000009000 0x0000000000009000 0x0000000000009000
0x0000000000094d01 0x0000000000094d01 R E 0x1000
LOAD 0x000000000009e000 0x000000000009e000 0x000000000009e000
0x00000000000285e0 0x00000000000285e0 R 0x1000
LOAD 0x00000000000c6de0 0x00000000000c7de0 0x00000000000c7de0
0x0000000000005350 0x0000000000006a80 RW 0x1000
// Read the executable file into memory
let data = read(BOCHS_IMAGE).map_err(|_| LucidErr::from(
"Unable to read binary data from Bochs binary"
))?;
// Parse ELF
let elf = parse_elf(&data)?;
// We need to iterate through all of the loadable program headers and
// determine the size of the address range we need
let mut mapping_size: usize = 0;
for
ph in elf.program_headers.iter() {
if
ph.is_load() {
let end_addr = (ph.vaddr + ph.memsz) as usize;
if
mapping_size < end_addr { mapping_size = end_addr; }
}
}
// Round the mapping up to a page
if
mapping_size % PAGE_SIZE > 0 {
mapping_size += PAGE_SIZE - (mapping_size % PAGE_SIZE);
}
// Read the executable file into memory
let data = read(BOCHS_IMAGE).map_err(|_| LucidErr::from(
"Unable to read binary data from Bochs binary"
))?;
// Parse ELF
let elf = parse_elf(&data)?;
// We need to iterate through all of the loadable program headers and
// determine the size of the address range we need
let mut mapping_size: usize = 0;
for
ph in elf.program_headers.iter() {
if
ph.is_load() {
let end_addr = (ph.vaddr + ph.memsz) as usize;
if
mapping_size < end_addr { mapping_size = end_addr; }
}
}
// Round the mapping up to a page
if
mapping_size % PAGE_SIZE > 0 {
mapping_size += PAGE_SIZE - (mapping_size % PAGE_SIZE);
}
// Call `mmap` to map memory into our process to hold all of the loadable
// program header contents in a contiguous range. Right now the perms will be
// generic across the entire range as PROT_WRITE,
// later we'll go back and `mprotect` them appropriately
fn initial_mmap(size: usize) -> Result<usize, LucidErr> {
// We don't want to specify a fixed address
let addr = LOAD_TARGET as *mut libc::c_void;
// Length is straight forward
let length = size as libc::
size_t
;
// Set the protections for now to writable
let prot = libc::PROT_WRITE;
// Set the flags, this is anonymous memory
let flags = libc::MAP_ANONYMOUS | libc::MAP_PRIVATE;
// We don't have a file to map, so this is -1
let fd = -1 as libc::c_int;
// We don't specify an offset
let offset = 0 as libc::off_t;
// Call `mmap` and make sure it succeeds
let result = unsafe {
libc::mmap(
addr,
length,
prot,
flags,
fd,
offset
)
};
if
result == libc::MAP_FAILED {
return
Err(LucidErr::from(
"Failed to `mmap` memory for Bochs"
));
}
Ok(result as usize)
}
// Call `mmap` to map memory into our process to hold all of the loadable
// program header contents in a contiguous range. Right now the perms will be
// generic across the entire range as PROT_WRITE,
// later we'll go back and `mprotect` them appropriately
fn initial_mmap(size: usize) -> Result<usize, LucidErr> {
// We don't want to specify a fixed address
let addr = LOAD_TARGET as *mut libc::c_void;
// Length is straight forward
let length = size as libc::
size_t
;
// Set the protections for now to writable
let prot = libc::PROT_WRITE;
// Set the flags, this is anonymous memory
let flags = libc::MAP_ANONYMOUS | libc::MAP_PRIVATE;
// We don't have a file to map, so this is -1
let fd = -1 as libc::c_int;
// We don't specify an offset
let offset = 0 as libc::off_t;
// Call `mmap` and make sure it succeeds
let result = unsafe {
libc::mmap(
addr,
length,
prot,
flags,
fd,
offset
)
};
if
result == libc::MAP_FAILED {
return
Err(LucidErr::from(
"Failed to `mmap` memory for Bochs"
));
}
Ok(result as usize)
}
let mut load_segments = Vec::
new
();
for
ph in elf.program_headers.iter() {
if
ph.is_load() {
load_segments.push((
ph.flags,
// segment.0
ph.vaddr as usize,
// segment.1
ph.memsz as usize,
// segment.2
ph.offset as usize,
// segment.3
ph.filesz as usize,
// segment.4
));
}
}
let mut load_segments = Vec::
new
();
for
ph in elf.program_headers.iter() {
if
ph.is_load() {
load_segments.push((
ph.flags,
// segment.0
ph.vaddr as usize,
// segment.1
ph.memsz as usize,
// segment.2
ph.offset as usize,
// segment.3
ph.filesz as usize,
// segment.4
));
}
}
// Iterate through the loadable segments and change their perms and then
// copy the data over
for
segment in load_segments.iter() {
// Copy the binary data over, the destination is where in our process
// memory we're copying the binary data to. The source is where we copy
// from, this is going to be an offset into the binary data in the file,
// len is going to be how much binary data is in the file, that's filesz
// This is going to be unsafe no matter what
let len = segment.4;
let dst = (addr + segment.1) as *mut u8;
let src = (elf.data[segment.3..segment.3 + len]).as_ptr();
unsafe {
std::ptr::copy_nonoverlapping(src, dst, len);
}
// Calculate the `mprotect` address by adding the mmap address plus the
// virtual address offset, we also mask off the last 0x1000 bytes so
// that we are always page-aligned as required by `mprotect`
let mprotect_addr = ((addr + segment.1) & !(PAGE_SIZE - 1))
as *mut libc::c_void;
// Get the length
let mprotect_len = segment.2 as libc::
size_t
;
// Get the protection
let mut mprotect_prot = 0 as libc::c_int;
if
segment.0 & 0x1 == 0x1 { mprotect_prot |= libc::PROT_EXEC; }
if
segment.0 & 0x2 == 0x2 { mprotect_prot |= libc::PROT_WRITE; }
if
segment.0 & 0x4 == 0x4 { mprotect_prot |= libc::PROT_READ; }
// Call `mprotect` to change the mapping perms
let result = unsafe {
libc::mprotect(
mprotect_addr,
mprotect_len,
mprotect_prot
)
};
if
result < 0 {
return
Err(LucidErr::from(
"Failed to `mprotect` memory for Bochs"
));
}
}
// Iterate through the loadable segments and change their perms and then
// copy the data over
for
segment in load_segments.iter() {
// Copy the binary data over, the destination is where in our process
// memory we're copying the binary data to. The source is where we copy
// from, this is going to be an offset into the binary data in the file,
// len is going to be how much binary data is in the file, that's filesz
// This is going to be unsafe no matter what
let len = segment.4;
let dst = (addr + segment.1) as *mut u8;
let src = (elf.data[segment.3..segment.3 + len]).as_ptr();
unsafe {
std::ptr::copy_nonoverlapping(src, dst, len);
}
// Calculate the `mprotect` address by adding the mmap address plus the
// virtual address offset, we also mask off the last 0x1000 bytes so
// that we are always page-aligned as required by `mprotect`
let mprotect_addr = ((addr + segment.1) & !(PAGE_SIZE - 1))
as *mut libc::c_void;
// Get the length
let mprotect_len = segment.2 as libc::
size_t
;
// Get the protection
let mut mprotect_prot = 0 as libc::c_int;
if
segment.0 & 0x1 == 0x1 { mprotect_prot |= libc::PROT_EXEC; }
if
segment.0 & 0x2 == 0x2 { mprotect_prot |= libc::PROT_WRITE; }
if
segment.0 & 0x4 == 0x4 { mprotect_prot |= libc::PROT_READ; }
// Call `mprotect` to change the mapping perms
let result = unsafe {
libc::mprotect(
mprotect_addr,
mprotect_len,
mprotect_prot
)
};
if
result < 0 {
return
Err(LucidErr::from(
"Failed to `mprotect` memory for Bochs"
));
}
}
position content size (bytes) + comment
------------------------------------------------------------------------
stack pointer -> [ argc = number of args ] 4
[ argv[0] (pointer) ] 4 (program name)
[ argv[1] (pointer) ] 4
[ argv[..] (pointer) ] 4 * x
[ argv[n - 1] (pointer) ] 4
[ argv[n] (pointer) ] 4 (= NULL)
[ envp[0] (pointer) ] 4
[ envp[1] (pointer) ] 4
[ envp[..] (pointer) ] 4
[ envp[term] (pointer) ] 4 (= NULL)
[ auxv[0] (Elf32_auxv_t) ] 8
[ auxv[1] (Elf32_auxv_t) ] 8
[ auxv[..] (Elf32_auxv_t) ] 8
[ auxv[term] (Elf32_auxv_t) ] 8 (= AT_NULL vector)
[ padding ] 0 - 16
[ argument ASCIIZ strings ] >= 0
[ environment ASCIIZ str. ] >= 0
(0xbffffffc) [ end marker ] 4 (= NULL)
(0xc0000000) < bottom of stack > 0 (virtual)
------------------------------------------------------------------------
position content size (bytes) + comment
------------------------------------------------------------------------
stack pointer -> [ argc = number of args ] 4
[ argv[0] (pointer) ] 4 (program name)
[ argv[1] (pointer) ] 4
[ argv[..] (pointer) ] 4 * x
[ argv[n - 1] (pointer) ] 4
[ argv[n] (pointer) ] 4 (= NULL)
[ envp[0] (pointer) ] 4
[ envp[1] (pointer) ] 4
[ envp[..] (pointer) ] 4
[ envp[term] (pointer) ] 4 (= NULL)