Unikernels安全探索:从内存保护到漏洞利用

本文深入探讨Unikernels的安全特性,包括内存保护机制、漏洞利用技术以及PHP和nginx在Unikernel环境下的行为分析。文章还讨论了ASLR的缺失、堆分配器的细节,并提出了针对开发者和攻击者的实用建议。

happy unikernels

日记:逆向工程档案

调试

漏洞利用

杂项

混淆

逆向工程

关于

演示文稿


日期:2016年12月21日 星期三
作者:yrp
分类:漏洞利用
标签:unikernel, rumpkernel, exploitation

简介

以下是一系列关于Unikernels的笔记。我最初准备将这些内容提交给EkoParty的CFP,但最终不想花时间稳定PHP7的堆结构,并且在项目完成前对其余部分失去了兴趣。不过,仍然有一些很酷的收获,我觉得可以写下来。也许它们会有用?如果有用,请告诉我。

Unikernels是将一切转化为容器或虚拟机的延续。基本上,由于许多虚拟机目前只运行一个用户态应用程序,其想法是通过移除用户态/内核态屏障,并将我们的用户模式进程编译到内核中,从而简化整个软件栈。在我查看的实现中,这是通过NetBSD内核和各种原生或轻度修补的POSIX应用程序完成的(额外好处:上游修复和rump包修复之间存在显著的滞后时间,就像所有其他容器化解决方案一样)。

虽然我不一定认为概念上Unikernels是个好主意(攻击面减少 vs 缓解措施移除),但我确实认为人们很快就会开始更广泛地部署它们,并且我好奇内存损坏漏洞利用在它们内部会是什么样子,更一般地说,你的payload选项会是什么样子。

以下所有内容基于两个Unikernel程序,nginx和php5,并且只使用公开漏洞。我很乐意应要求提供所有引用的代码(处于不同的不完整状态)。

目录

  • 简介
  • 基本‘Hello World’示例
  • 编译和‘Baking’
  • 启动和调试
  • Peek/Poke工具
  • 内存保护
  • nginx
    • 为什么这不起作用的原因
    • 一些有趣的事情
  • ASLR
  • PHP
  • 持久性
  • 堆笔记
  • 符号解析
  • 虚拟机管理程序模糊测试
  • 最终建议
  • 致谢

基本‘Hello World’示例

为了基本理解Unikernel,我们将通过一个简单的‘Hello World’示例。首先,你需要克隆并构建(./build-rr.sh)rumprun工具链。这将为你设置所需的各种实用程序。

编译和‘Baking’

在rumpkernel应用程序中,我们有一个标准的POSIX环境,减去任何涉及多个进程的内容。标准的内存、文件系统和网络调用都按预期工作。唯一的区别在于多进程相关的调用,如fork()、signal()、pthread_create()等。这些差异的范围可以在《The Design and Implementation of the Anykernel and Rump Kernels》[pdf]中找到。

从一个超级基本、标准的‘hello world’程序开始:

1
2
3
4
5
#include <stdio.h>
void main(void)
{
    printf("Hello\n");
}

构建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。

启动和调试

此时,假设你已安装qemu,启动新映像应该像rumprun qemu -g “-curses” -i hello.bin一样简单。如果一切按计划进行,你应该看到类似的内容:

由于此时这只是qemu,如果你需要调试,可以通过qemu的系统调试器轻松附加。此外,这个工具链的一个很好的副作用是非常容易调试——你基本上可以在本机架构上调试大部分问题,然后只需切换编译器来构建可启动映像。而且,由于启动时间快得多,调试和修复问题的速度大大加快。

如果你有进一步的问题,或想要更多细节,Rumpkernel Wiki有一些非常好的文档解释各种组件和选项。

Peek/Poke工具

最初为了熟悉代码,我写了一个简单的peek/poke原语进程。虚拟机会启动并暴露一个tcp套接字,允许客户端读取或写入任意内存,以及围绕malloc()和free()的包装器来玩堆状态。这里的大部分知识来自这个测试代码,用调试器戳它,以及阅读rump kernel源代码。

内存保护

Unikernels的好处之一是可以修剪你可能不需要的组件。例如,如果你的Unikernel应用程序不接触文件系统,那么该代码可以从结果VM中移除。一个有趣的后果是只运行一个进程——由于VM上只运行一个进程,不需要虚拟内存系统来按进程分离地址空间。

目前,这意味着所有内存都是读-写-执行。我不确定是否可以在虚拟机管理程序中配置MMU以强制执行内存保护而不启用虚拟内存,因为我查看的大部分虚拟内存代码都与使用页表等进行进程分离有关。无论如何,目前将新代码引入系统相当简单,并且不需要太多依赖ROP。

nginx

Nginx是我查看的第一个目标;我以为我可以挖掘2013年的栈溢出(CVE-2013-2028),并将其用作基线漏洞利用来看看可能的情况。这最终失败了,但在此过程中暴露了一些有趣的事情。

为什么这不起作用的原因

CVE-2013-2028是nginx处理分块请求时的栈缓冲区溢出。我认为这将是一个很好的测试,因为用户控制栈上的大部分数据,然而,各种触发溢出的尝试都失败了。在调试器中运行VM,你可以看到尽管大小值足够大,但漏洞并未触发。实际上,系统调用返回了一个错误。

然而,事实证明NetBSD在内核中有代码来防止这种情况:

1
2
3
4
5
6
7
8
do_sys_recvmsg_so(struct lwp *l, int s, struct socket *so, struct msghdr *mp,
        struct mbuf **from, struct mbuf **control, register_t *retsize) {
// …
        if (tiov->iov_len > SSIZE_MAX || auio.uio_resid > SSIZE_MAX) {
            error = EINVAL;
            goto out;
        }
// …

iov_len是我们的recv()大小参数,所以这个漏洞已经死了。顺便说一句,这也让我想知道如果你传递一个大于LONG_MAX的大小到recv()并且它成功了,Linux应用程序会如何响应……

一些有趣的事情

传统上,在利用这个漏洞时,必须担心栈cookie。Nginx有一个从主进程fork的工作进程池。在崩溃事件中,一个新进程将从父进程fork,这意味着栈cookie在后续连接中保持不变。这允许你将其分解为四个1字节的暴力破解,而不是一个4字节,意味着最多可以在1024个连接内完成。然而,在Unikernel内部,只有一个进程——如果一个进程崩溃,整个VM必须重启,并且由于唯一的进程是内核,栈cookie应该(理论上)重新生成。查看反汇编的nginx代码,你可以在所有相关函数中看到栈cookie检查。

实际上,这一点没有实际意义,因为栈cookie总是零。编译器创建并检查cookie,它只是从不填充fs:0x28(cookie值的位置),所以它总是一个常数值,假设你可以写入空字节,这应该没有问题。

ASLR

我好奇Unikernels是否会实现某种形式的ASLR,因为在构建过程中它们被编译为ELF(这对分析来说非常好!),这可能使位置无关代码更容易处理。它们没有:所有映像都加载在0x100000。然而,存在“自然的ASLR”,因为这些映像不以二进制形式分发。因此,由于每个人都必须编译自己的映像,这些映像会根据编译器版本、软件版本等略有不同。然而,即使这个约束也变得更容易。如果你查看加载映像的格式,它们看起来像这样:

1
2
3
0x100000: <unikernel init code>
0x110410: <application code starts>

这意味着在任何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堆时感到无聊。

持久性

这是我想要为EkoParty完成的部分,但它没有完成。理论上,由于所有内存都是读-写-执行,应该相当简单,只需修补recv()或类似的东西来检查接收到的数据,如果匹配某个常量,则执行数据包的其余部分。这严格在内存中,任何涉及磁盘的内容都将是应用程序特定的。

假设你的payload稳定,你应该能够安装一个内存中的后门,该后门将在该会话的运行时持续(并在关机时删除)。虽然在许多配置中没有可写的持久存储会在重启后存活,但这并非所有Unikernels都如此(例如mysql)。在这些情况下,可能可以跨电源周期持久化,但这将是应用程序特定的。

最后一个,希望是明显的注意事项:Unikernels漏洞利用的最大差异之一是缺乏多个进程。漏洞利用经常使用多个进程的存在来避免在payload运行后清理应用程序状态。在Unikernel中,你的payload必须修复应用程序状态或使VM崩溃。在这方面,它更类似于内核漏洞利用。

堆笔记

从漏洞利用的角度来看,Unikernel堆非常不错。它是一个slab风格的分配器,每个块上都有内联元数据。具体来说,元数据包含分配所属的‘桶’(以及因此块应释放到的空闲列表)。这意味着相对覆盖加上free()到更小的桶应该允许相当精细的内容控制。此外,堆是LIFO,允许标准的堆按摩。

另外,虽然有点未经测试,我相信rumpkernel应用程序编译时没有定义QUEUEDEBUG。这是相关的,因为取消链接操作(“安全取消链接”)的健全性检查需要定义此选项。这意味着在某些情况下,如果空闲列表本身可以被溢出然后移除,你可以获得写-什么-在哪里。然而,我认为这在实践中相当不可能,并且由于其他地方缺乏内存保护,如果它目前有用,我会感到惊讶。

你可以在这里找到大部分相关的堆源代码。

符号解析

Rumpkernels有帮助地在mysys符号下包含整个系统调用表。当rumpkernel映像加载时,ELF头被剥离,但其余内存被连续加载:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
gef➤  info file
Symbols from "/home/x/rumprun-packages/php5/bin/php.bin".
Remote serial target in gdb-specific protocol:
Debugging a target over a serial line.
        While running this, GDB does not access memory from...
Local exec file:
        `/home/x/rumprun-packages/php5/bin/php.bin', file type elf64-x86-64.
        Entry point: 0x104000
        0x0000000000100000 - 0x0000000000101020 is .bootstrap
        0x0000000000102000 - 0x00000000008df31c is .text
        0x00000000008df31c - 0x00000000008df321 is .init
        0x00000000008df340 - 0x0000000000bba9f0 is .rodata
        0x0000000000bba9f0 - 0x0000000000cfbcd0 is .eh_frame
        0x0000000000cfbcd0 - 0x0000000000cfbd28 is link_set_sysctl_funcs
        0x0000000000cfbd28 - 0x0000000000cfbd50 is link_set_bufq_strats
        0x0000000000cfbd50 - 0x0000000000cfbde0 is link_set_modules
        0x0000000000cfbde0 - 0x0000000000cfbf18 is link_set_rump_components
        0x0000000000cfbf18 - 0x0000000000cfbf60 is link_set_domains
        0x0000000000cfbf60 - 0x0000000000cfbf88 is link_set_evcnts
        0x0000000000cfbf88 - 0x0000000000cfbf90 is link_set_dkwedge_methods
        0x0000000000cfbf90 - 0x0000000000cfbfd0 is link_set_prop_linkpools
        0x0000000000cfbfd0 - 0x0000000000cfbfe0 is .initfini
        0x0000000000cfc000 - 0x0000000000d426cc is .data
        0x0000000000d426d0 - 0x0000000000d426d8 is .got
        0x0000000000d426d8 - 0x0000000000d426f0 is .got.plt
        0x0000000000d426f0 - 0x0000000000d42710 is .tbss
        0x0000000000d42700 - 0x0000000000e57320 is .bss

这意味着你应该能够运行简单的线性扫描,寻找mysys表。一个基本的启发式方法应该没问题,8字节系统调用号,8字节地址。在PHP5解释器中,这个表有67个条目,给它一个巨大、肥大的足迹:

1
2
3
4
5
gef➤  x/6g mysys
0xaeea60 <mysys>:       0x0000000000000003      0x000000000080b790 -- <sys_read>
0xaeea70 <mysys+16>:    0x0000000000000004      0x000000000080b9d0 -- <sys_write>
0xaeea80 <mysys+32>:    0x0000000000000006      0x000000000080c8e0 -- <sys_close>
...

在初始常量0x10410字节中可能有一个指针链你也可以跟随,但这种方法应该工作良好。

虚拟机管理程序模糊测试

玩了一会儿这些之后,我有了另一个想法:与其使用Unikernels来托管用户态服务,我认为有一个非常酷的机会在Unikernel中编写一个虚拟机管理程序模糊测试器。考虑:

  • 你拥有POSIX用户态的所有好处,只是你在ring0。你不需要将数据导出到用户态以获得简单和熟悉的IO函数。
  • Unikernels启动非常、非常快。如在一秒内。这应该允许相当快的状态清除。

这绝对是一个有趣的未来工作领域,我想回头再来。

最终建议

如果你开发Unikernels:

  • 填充栈cookie的随机性。
  • 加载到随机位置以获得某种ASLR的 semblance。
  • 有没有办法强制执行内存权限?某种形式的NX会大有帮助。
  • 如果不能,一些控制流完整性东西可能是个好主意?还没有真正思考或尝试过。
  • 尽可能从grsec中吸取教训。

如果你在利用Unikernels:

  • 玩得开心。

如果你在利用虚拟机管理程序:

  • Unikernels可能提供一个酷的平台来轻松地在ring0中玩。

致谢

对于反馈、使用的错误或编辑:@seanhn, @hugospns, @0vercl0k, @darkarnium, 其他非常有帮助的匿名类型。


自豪地由Pelican提供动力,它充分利用了Python的优势。

主题来自Bootstrap from Twitter和Font-Awesome,谢谢!

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计