|
原文链接:【漏洞分析】sudo堆溢出漏洞分析(CVE-2021-3156)
介绍
2021年1月26日,sudo被曝存在一个“新”漏洞,但是实际上这个漏洞可能已经存在10年了。该漏洞可以使攻击者通过堆缓冲区溢出来进行权限提升。但是漏洞发布并没有exploit/POC,笔者所以决定自己构建一个。
脆弱性
简要介绍该漏洞,本质上攻击者可以通过在给sudo的任何argv或env参数的末尾插入一个反斜杠来让堆溢出,从而导致参数超出范围,来看一下简化版本的代码片段。
第一个for循环中,正在遍历每个参数,并使用strlen判断其大小(加上空终止符),现在我们假设有空字符串“AAAA\\”(\\是一个字符),大小为5,并且加上这是唯一的参数,仅分配5个字节。
在下一部分中,我们将有一个用于参数的外部for循环和一个将所有参数的内容复制到单个缓冲区user_args中的内部循环,本质上是将所有参数串联在一起。
考虑与前面相同的字符串“AAAA\\”,当[0]=='\\'成立时,进入if并从from++递增,因为它指向空终止符,所以导致递增。之后,我们使用下一条语句*to++=*from++;继续循环。复制空终止符,复制字节,最终产出界限。
之所以会发生这种情况,是因为它希望每个\\后面都有一个元字符,作者提出了一种巧妙的绕过方法,使它容易溢出。如果您想知道为什么以及进入该块时我们最终在参数中只有\\的原因,请阅读本文。
通过使用符号链接sudoedit到sudo,我们可以做到这一点:
溢出的属性
作者陈述了有关此溢出的3个重要属性,这些属性使其非常强大。
首先,最简单的就是控制user_args的分配大小,因此选择sudo参数的数量和时间属性。
其次,控制溢出区域的内容。可以通过使用提供的环境变量来实现。环境变量实际上存储在最后一个参数传递给sudoedit之后,这意味着如果我们执行env -i'A =BBBB'sudoedit -s'CCCCCCCCCCCCCCCC',我们会将C插入到user_args缓冲区中,而A = BBBB将紧随其后插入到边界区域。请注意,块大小应与0x10大小保持一致,例如env -i'A = BB'sudoedit-s'CCCCBBBBBBBB'仅填充缓冲区。
如果密切关注串联块中的内部循环,您可能会注意到可以多次利用它。通过用\\结束环境变量,可以再次跳到下一个环境变量。那为什么要那样?因为随着from++的增加,在以下to++=*from++上指向空终止符的指针,它将插入该空终止符。这能够在不结束溢出的情况下插入0x0,从而使该溢出功能极为强大。
作者的例子:
env -i'AA = a \''B = b \''C = c \''D = d \''E = e \''F = f'sudoedit-s'1234567890123456789012 \'
这样最终会在缓冲区中结束:
因此,我们将不讨论堆块的正向(fd)或反向(bk)指针的含义,因为我们仅在使用内存中进行利用。
- 第一个大小是后续块的大小。它等于给malloc的0x10+参数,因为还需要本身和对齐/以前大小的空间。
- 下一个大小是连续的块大小。
- fd和bk分别指向此释放块链接列表中的下一个和上一个块的指针。这仅适用于释放的块。否则,malloc的调用者可以使用这个空间。
关于空终止符插入,这里需要注意的一点是,我认为原稿中缺少的是,我们也可以插入多个连续的空字节。
首先要了解的是,环境变量不必以SOMETHING=SOMETHING_more的形式出现。与其他所有内容一样,这些只是字符数组,我们可以在C语言中使用它们。例如:
在这里,我们使用execve在完全控制环境变量的情况下执行过程。在内部for循环中,我们在””\\””处插入if语句,并通过from++反斜杠跳过一个字符,然后仅将null插入到下一个“\\”,然后在a中插入两个null字节。
开发
尽管作者在本文中提到了3个可能的目标,但我们仅涵盖第二个目标。
原因:
- 与第一种选择相反,它们没有暴力破解,在第一种选择中,它们部分溢出了以暴力破解击败ASLR的函数指针。
- 他们说,他们成功地在3个操作系统上成功做到了这一点,而其他两个操作系统都只有一个。
在第二个选项中,我们尝试溢出到存储在堆中的service_user结构中。
nss_load_library在溢出加载新的动态链接库后,libc中经常使用此结构,我们可以使文件名溢出,然后控制要加载的库。然后,我们可以针对我们可以制作的将以root特权运行的非特权库为目标。
该函数如下所示:
此功能的目标是点击ni->library->lib_handle =__libc_dlopen(shlib_name)加载一个我们控制的新库。
这里有两件事要注意,第一件事是本文提到的。如果ni->library不为NULL,我们将在ni->library->lib_handle中使用该指针,并且由于ASLR是一个X子,因此我们无法预测没有泄漏的有效指针。幸运的是,此结构存在一个初始情况,如果该结构为null,则可以通过ni->library=nss_new_service来设置它,现在,多个空字节写入就派上用场了!
然后,我们只需要将此结构完全溢出到其名称字段,即可将其更改为我们控制的非特权库。
第二个挑战是我们拥有下一个指针struct service_user * next;。在结构内部形成一个链表,当加载发生时将遍历该链表。因此,如果我们在过程中意外溢出另一个service_user结构,则当我们因fx A溢出而导致错误时,就将编写垃圾指针。可以通过在该位置插入空字节来避免这种情况,但这会带来另一个问题,现在断开链表,并且可以从列表中完全删除目标结构,而在整个内存空间中都没有指向它的指针。
这意味着我们必须定位到分配区域之后的链表中的第一个结构。事实证明,这是要克服的最大挑战,因为您可以想象这需要对堆分配进行很好的控制。
在本文中,它们service_user以systemd我们绝对无法定位的名称为目标。因此,我们在分配之前设置了一个断点来检查链表。然后,我们搜索systemd并向后遍历该列表,直到找到靠近我们的分配的第一个service_user为止。(结合A的一些反复试验,以了解其崩溃的结构))
在这里,我service_user在内存中显示了不同的名称,并在vmmaps下面显示了相同的名称。如图所示,第二个vmmap对应于systemd,其偏移量为距堆基0x47e0。这显然是一个问题,因为我们service_user在列表0x4790之前的列表中看到了另一个,而两个结构之间只有0x50的空间,所以两个结构之间只有0的空间。这使得不可能针对这一目标,但是我们可以只选择之前的目标。但是,为什么不针对其他一些fx某些0x2000结构呢?好吧……您根本无法进行那么早的分配。
堆修饰
那么,如何在接近目标的内存区域分配内存呢?因此,这篇文章中的任务似乎并不明确,听起来好像他们“蛮力”尝试了很多,直到解决方案崩溃。如果不正确,请随时与我们的团队联系。
无论如何,我们都不希望尝试各种不同的分配方式。但是,他们确实提到了在我们控制大小的sudo进程中尽早进行分配的巧妙方法。
此技巧利用了以下事实:setlocale被称为第一件事,并且它们声明:在154行的setlocale()中,我们malloc()ate和free()几个LC环境变量(LC_CTYPE,LC_MESSAGES,LC_TIME等),从而在sudo堆的开始处创建了小洞(空闲的fast或tcache块)
这是一个巧妙的技巧,我们通过打破setlocale和以下所有免费代码来研究此大小,以检查将要释放的大小块。
第一个有趣的测试:
第二个有趣的测试:
现在,实际上第二个被分配并在setlocale内不久之后再次释放。这使我相信它比第一个更加不可靠(可能是因为它同样稳定)。
但是有趣的是,我们没有发现本文所期望的其他LC变量中的其他自由变量,这很可能是libc依赖的,或者我完全不在本地使用。
这意味着我们只有这个分配可以使用。令人遗憾的是,现在可以使用的malloc更少了,但同时也限制了搜索空间。
现在,您还记得我说过的关于前向和后向指针的事情,我们应该不在乎。是的..现在我们需要这些知识。因此,这里介绍了世界上最快的垃圾箱介绍。
实际上,释放的块并不存储在单个链表中,而是分成多个链表,这些链表按边上的块大小排序。更糟糕的是,我们提供了5种不同的列表:
- tcache适用于大小从0x20到0x408的超快速分配
- fast bins也是从0x20到0x80的超快速分配
- small bins比tcache和fastbins大的小分配
- large bins大的可变大小的块
- unsorted bin一个包含尚未分类到其他箱中的块的箱
我们将仅关注tcache和fast bins,因为其他箱中的数据块可能会合并,这意味着连续的数据块可以合并为一个更大的数据块,这使得以后很难预测垃圾箱的状态。在这两类箱中,块大小每0x10增量存在一个箱。(bin==链表)
现在,我们要使用LC_MESSAGE分配一个块,并在setlocale中再次释放它,以使该块可在以后执行溢出时使用。这样,我们就可以在堆上获得更大的块。
我不希望在我的分配之前释放的容器中的块,而当我们进行最终的溢出分配时,容器中的块仍然存在,因为它们可能来自sudo中的其他位置。
因此,在setlocale的末尾和最后的分配之前中断,让我对sudo期间使用了哪些bin有了一个了解。请注意,这并不能说明所有分配情况,实际上与分配情况相去甚远,因此仍会涉及一些反复试验。
我试图说明搜索空间,我们将首先尝试:
现在,这是一个粗略的计划,我没有完全坚持下去。
经过令人惊讶的几次尝试之后,我们就在溢出分配之前将这一块可用了:
上面的是我们的分配,下面是目标字符串mymachine。仅相隔0x4790-0x4370 == 0x420字节。不错,这似乎可行。
现在,我们只需要使用null进行溢出,直到命中该结构并重新组合具有ni-library null和另一个名称的相同结构即可。
我们首先通过如下设置args来进行分配,以匹配之前找到的大小。以前面所述的分配取决于提供给sudoedit的第一个参数的长度。我们将尝试将此大小与LC_MESSAGE释放的块进行匹配。[核对原件]
- char *args[] = {
- "/usr/bin/sudoedit",
- "-s",
- "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBAAAAAAAAAAAAAAAA\",
- NULL
- }; //B and A's to match the chunk size we want freed in thebeginning
复制代码 然后,我们创建一长串环境变量以放入null并以伪造的service_user结构结尾:
- char *extra_args[] = {
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\x01\",
- "\",
- "\",
- "\x01\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "\",
- "X/X\",
- "a",
- "LC_MESSAGES=C.UTF-8@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
- NULL,
- };
复制代码
然后,nss_load_library中的_stpcpy序列将基于上面的X/X\\arg创建路径libnss_X/X.so.2:
现在我们只需要创建一个简单的库即可加载。我们只是创建一个带有初始化函数设置ID(不确定是否需要)的小型库,并执行/bin/sh并在nss_load_library中的ld_open时生成根shell。使用gcc-Os-Wall-Wextra-fPIC-sharednss.c-oX.so.2进行编译。
看到这个我真高兴!!
shell终于弹出了
结论
最终的利用是100%可靠的,并且可以在我的环境中使用libc2.32启用ASLR,这在ubuntu 20.10中也可以找到,并且可能很容易在许多发行版中进行了重新设计。由于许多系统仍然容易受到攻击,我们目前尚未发布最终的利用代码。感谢所有参与发现此漏洞并加以利用的研究人员。
参考链接
- https://www.sudo.ws/download.html
- https://blog.qualys.com/vulnerabilities-research/2021/01/26/cve-2021-3156-heap-based-buffer-overflow-in-sudo-baron-samedit
- https://www.kalmarunionen.dk/writeups/sudo/
|
|