happy unikernels
Date: 2016年12月21日
By: yrp
Category: exploitation
Tags: unikernel, rumpkernel, exploitation
Intro
以下是一组关于Unikernels的笔记。我最初准备将这些内容提交给EkoParty的CFP,但最终不想花时间稳定PHP7的堆结构,并在项目完成前失去了兴趣。不过,还有一些很酷的收获,我觉得可以写下来。也许它们会派上用场?如果是的话,请告诉我。
Unikernels是将所有东西容器化或虚拟机化的延续。基本上,由于许多虚拟机目前只运行一个用户态应用程序,想法是通过移除用户态/内核态屏障,将我们的用户态进程编译到内核中,从而简化整个软件栈。在我查看的实现中,这是通过NetBSD内核和各种原生或轻量修补的POSIX应用程序完成的(额外好处:上游修复和rump包修复之间存在显著的延迟时间,就像其他所有容器化解决方案一样)。
虽然我不一定认为概念上Unikernels是个好主意(攻击面减少 vs 缓解措施移除),但我认为人们很快就会开始更广泛地部署它们,我很好奇内存损坏漏洞利用在它们内部会是什么样子,更一般地说,你的payload选项是什么。
以下所有内容基于两个Unikernel程序,nginx和php5,并且只使用公开漏洞。我很乐意应要求提供所有引用的代码(处于不同不完整状态)。
目录
- Intro
- Basic ‘Hello World’ Example
- Compiling and ‘Baking’
- Booting and Debugging
- Peek/Poke Tool
- Memory Protections
- nginx
- ASLR
- PHP
- Persistence
- Heap Notes
- Symbol Resolution
- Hypervisor fuzzing
- Final Suggestions
- Thanks
Basic ‘Hello World’ Example
为了基本理解Unikernel,我们将通过一个简单的“Hello World”示例。首先,你需要克隆并构建(./build-rr.sh)rumprun工具链。这将为你设置所需的各种实用程序。
Compiling and ‘Baking’
在rumpkernel应用程序中,我们有一个标准的POSIX环境,减去任何涉及多进程的东西。标准内存、文件系统和网络调用都按预期工作。唯一的区别在于多进程相关调用,如fork()、signal()、pthread_create()等。这些差异的范围可以在The Design and Implementation of the Anykernel and Rump Kernels [pdf]中找到。
从一个超级基础、标准的“hello world”程序开始:
|
|
构建rumprun后,我们应该有一个新的编译器x86_64-rumprun-netbsd-gcc。这是一个针对rumpkernel平台的交叉编译器。我们可以正常编译:x86_64-rumprun-netbsd-gcc hello.c -o hello-rump
,实际上输出是一个ELF:hello-rump: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
。然而,由于我们显然不能直接启动ELF,我们必须操作可执行文件(在rumpkernel术语中称为“baking”)。
Rump kernels提供了一个rumprun-bake shell脚本。该脚本接受用rumprun工具链编译的ELF,并将其转换为可启动映像,然后我们可以将其提供给qemu或xen。继续我们的示例:rumprun-bake hw_generic hello.bin hello-rump
,其中hw_generic仅表示我们针对qemu。
Booting and Debugging
此时,假设你安装了qemu,启动新映像应该像rumprun qemu -g "-curses" -i hello.bin
一样简单。如果一切按计划进行,你应该看到类似的内容:
因为此时这只是qemu,如果你需要调试,你可以通过qemu的系统调试器轻松附加。此外,这个工具链的一个很好的副作用是非常容易调试——你基本上可以在本地架构上调试大部分问题,然后只需切换编译器来构建可启动映像。而且,由于启动时间快得多,调试和修复问题的速度大大加快。
如果你有进一步的问题或想要更多细节,Rumpkernel Wiki有一些非常好的文档解释各种组件和选项。
Peek/Poke Tool
最初为了熟悉代码,我写了一个简单的peek/poke原语进程。VM将启动并暴露一个tcp套接字,允许客户端读取或写入任意内存,以及围绕malloc()和free()的包装器来玩堆状态。这里的大部分知识来自这个测试代码、用调试器戳它以及阅读rump kernel源代码。
Memory Protections
Unikernels的好处之一是可以修剪你可能不需要的组件。例如,如果你的Unikernel应用程序不接触文件系统,那么该代码可以从结果VM中移除。一个有趣的后果是只运行一个进程——因为VM上只运行一个进程,不需要虚拟内存系统来按进程分离地址空间。
现在这意味着所有内存都是读-写-执行。我不确定是否可以在hypervisor中配置MMU来强制执行内存保护而不启用虚拟内存,因为我查看的大部分虚拟内存代码都与使用页表等进行进程分离有关。无论如何,目前将新代码引入系统相当简单,不应该需要求助于ROP。
nginx
Nginx是我查看的第一个目标;我想我可以挖掘2013年的栈溢出(CVE-2013-2028)并将其用作基线漏洞利用来看看可能是什么。这最终失败了,但沿途暴露了一些有趣的事情。
Reason Why This Doesn’t Work
CVE-2013-2028是nginx处理分块请求时的栈缓冲区溢出。我认为这将是一个很好的测试,因为用户控制栈上的大部分数据,然而,各种触发溢出的尝试都失败了。在调试器中运行VM,你可以看到尽管大小值足够大,但漏洞并未触发。实际上,系统调用返回错误。
然而,事实证明NetBSD在内核中有代码来防止这种情况:
|
|
iov_len是我们的recv()大小参数,所以这个漏洞没戏了。顺便说一句,这也让我想知道如果你传递一个大于LONG_MAX的大小到recv()并且它成功了,Linux应用程序会如何响应……
Something Interesting
传统上,利用此漏洞时必须担心栈cookie。Nginx有一个从主进程fork的工作进程池。在崩溃事件中,将从父进程fork一个新进程,这意味着栈cookie在后续连接中将保持不变。这允许你将其分解为四个1字节暴力破解,而不是一个4字节,意味着最多可以在1024次连接内完成。然而,在Unikernel内部,只有一个进程——如果一个进程崩溃,整个VM必须重启,并且因为唯一的进程是内核,栈cookie应该(理论上)重新生成。查看反汇编的nginx代码,你可以在所有相关函数中看到栈cookie检查。
实际上,这一点没有实际意义,因为栈cookie总是零。编译器创建并检查cookie,它只是从不填充fs:0x28(cookie值的位置),所以它总是一个常数值,假设你可以写入空字节,这应该不成问题。
ASLR
我很好奇Unikernels是否会实现某种形式的ASLR,因为在构建过程中它们被编译为ELF(这对分析非常友好!),这可能使位置无关代码更容易处理。它们没有:所有映像都加载在0x100000。然而,有“自然的ASLR”,因为这些映像不是以二进制形式分发。因此,由于每个人都必须编译自己的映像,这些映像将根据编译器版本、软件版本等略有不同。然而,即使这个约束也变得更容易。如果你查看加载映像的格式,它们看起来像这样:
|
|
这意味着在任何Unikernel应用程序中,你将有大约0x10000字节的固定值、固定位置的可执行内存。如果你找到一个可利用的漏洞,应该可以完全使用此部分中的代码构建payload。这个payload可用于泄漏应用程序代码、安装持久性等。
PHP
一旦nginx出局,我需要另一个有rumpkernel包和可利用漏洞历史的应用程序。PHP解释器符合要求。我最终使用了Sean Heelan的PHP bug #70068,因为错误描述中提供了触发器和详细解释错误的描述。与其尝试拙劣地重述Sean的工作,我鼓励你如果你对漏洞感兴趣,只需阅读初始报告。
回想起来,我为这个漏洞选择了一个糟糕的利用路径。因为堆slab没有ASLR,你可以相当自信地预测PHP解释器内的映射地址。此外,通过控制payload的大小,你可以确定它将落入哪个桶,并选择一个较少使用的桶以获得更稳定性。这允许你懒惰,并硬编码payload地址,导致容易利用。这非常有效——我基本上能够拿Sean的触发器,塞入一些地址和payload,并从中获得代码执行。然而,这种方法的缺点很快变得明显。当试图从我的payload返回并让解释器处于正常状态(即运行)时,我意识到我实际上需要理解PHP堆来修复它。我通过检查rump堆(见下文)开始了这个过程,但当最终进入PHP堆时感到无聊。
Persistence
这是我为EkoParty想要完成的部分,但没有完成。理论上,由于所有内存都是读-写-执行,应该相当简单,只需修补recv()或类似的东西来检查接收到的数据,如果匹配某个常量,则执行数据包的其余部分。这严格在内存中,任何接触磁盘的东西都将特定于应用程序。
假设你的payload稳定,你应该能够安装一个内存中的后门,该后门将在该会话的运行时持续(并在关机时删除)。虽然在许多配置中没有可写的持久存储会在重启后存活,但这并非对所有Unikernels都成立(例如mysql)。在这些情况下,可能可以跨电源周期持久化,但这将特定于应用程序。
最后一个,希望明显的注意:Unikernels漏洞利用的最大差异之一是缺乏多进程。漏洞利用经常使用多进程的存在来避免在payload运行后清理应用程序状态。在Unikernel中,你的payload必须修复应用程序状态或使VM崩溃。在这方面,它更类似于内核漏洞利用。
Heap Notes
从漏洞利用的角度来看,Unikernel堆非常好。它是一个slab风格的分配器,每个块上有内联元数据。具体来说,元数据包含分配所属的“桶”(以及因此块应释放到的空闲列表)。这意味着相对覆盖加上free()到较小的桶应该允许相当精细的内容控制。此外,堆是LIFO,允许标准堆按摩。
另外,虽然有点未经测试,我相信rumpkernel应用程序编译时没有定义QUEUEDEBUG。这是相关的,因为取消链接操作(“安全取消链接”)的健全性检查需要定义此。这意味着在某些情况下,如果空闲列表本身可以被溢出然后移除,你可以获得写-什么-哪里。然而,我认为这在实践中相当不可能,并且由于其他地方缺乏内存保护,如果它目前有用,我会感到惊讶。
你可以在这里找到大部分相关的堆源代码。
Symbol Resolution
Rumpkernels有帮助地在mysys符号下包含整个系统调用表。当rumpkernel映像加载时,ELF头被剥离,但其余内存连续加载:
|
|
这意味着你应该能够运行简单的线性扫描,查找mysys表。一个基本的启发式方法应该没问题,8字节系统调用号,8字节地址。在PHP5解释器中,此表有67个条目,给它一个巨大、肥大的足迹:
|
|
在初始常量0x10410字节中可能有一个指针链你也可以跟随,但这种方法应该工作良好。
Hypervisor fuzzing
玩了一会儿这些之后,我有了另一个想法:与其使用Unikernels托管用户态服务,我认为有一个非常酷的机会在Unikernel中编写一个hypervisor fuzzer。考虑:
- 你拥有POSIX用户态的所有好处,只是你在ring0。你不需要将数据导出到用户态来获得简单和熟悉的IO函数。
- Unikernels启动非常、非常快。如 under 1 second。这应该允许相当快的状态清除。
这绝对是一个有趣的未来工作领域,我想回头看看。
Final Suggestions
如果你开发Unikernels:
- 填充栈cookie的随机性。
- 加载到随机位置以获得某种ASLR。
- 有没有办法强制执行内存权限?某种形式的NX会大有帮助。
- 如果不能,一些控制流完整性东西可能是个好主意?还没有真正想过或尝试过。
- 尽可能从grsec吸取教训。
如果你在利用Unikernels:
- 玩得开心。
如果你在利用hypervisors:
- Unikernels可能提供一个很酷的平台来轻松在ring0中玩。
Thanks
对于反馈、使用的漏洞或编辑:@seanhn, @hugospns, @0vercl0k, @darkarnium, 其他非常有帮助的匿名类型。
Proudly powered by Pelican, which takes great advantage of Python.
The theme is from Bootstrap from Twitter, and Font-Awesome, thanks!