Qualys 团队在 Glibc 库的 __vsyslog_internal 函数中发现了一个堆溢出,这个问题允许攻击者仅通过更改程序名称来提升权限。本文介绍堆溢出是什么,它们是如何工作的,以及开发 CVE-2023-6246 利用程序来提升权限 [1] 。
一、基本概念
什么是堆?堆是计算机内存中用于动态内存分配的区域。它是全局可访问的,这意味着在堆上分配的内存不与特定函数的作用域绑定,可以从程序的任何部分访问。与栈不同,栈通常具有固定大小,并遵循后进先出(LIFO)模型用于函数调用和局部变量,堆更加灵活,可以在程序执行期间动态分配和释放内存。
栈是按照特定的顺序进行分配和回收的,而堆则不同,堆的分配和回收没有特定的顺序或偏好。图二展示了堆是如何工作的:
-
使用像malloc这样的函数来分配特定大小的内存空间A。 -
同样的过程也适用于另一块大小不同的数据B,它被存储在堆中。 -
A中的数据被释放,因此,它所使用的空间现在可用。 -
另一块数据C要存储在堆中,但A留下的空间不够,因此,它将被分配在B之后的内存空间。 -
最后,当分配另一组较小的数据D,B和C之前的空间满足要求,所以它可以分配在B和C之前。
什么是堆溢出
堆溢出是一种内存损坏漏洞,当程序在堆中写入长度超出分配的内存块的大小时,就会产生这种问题。
在上图中,D 是动态分配的内存,但由于堆溢出,可以写入超出 D 边界的数据,这意味着它将覆盖 B 和 C 并破坏堆结构。这种情况将会导致各种崩溃:
-
malloc(): invalid size
:这表示在调用malloc
函数时传递了一个无效的大小参数。可能是负数、零或其他不合法的值; -
malloc(): corrupted top size
:这通常是由于堆溢出或其他内存错误导致的。堆被破坏,导致malloc
函数无法正确管理内存; -
double free or corruption (out)
:这表示尝试释放已经被释放的内存块,或者堆被破坏; -
free(): corrupted unsorted chunks
:这也是堆破坏的迹象。堆的数据结构被破坏,导致free
函数无法正确释放内存; -
free(): invalid size
:类似于malloc
中的错误,这表示在调用free
函数时传递了无效的大小参数。
以攻击者视角来看,可以通过两种不同的方式利用这种情况:
-
攻击堆元数据:堆是一种带有元数据的数据结构。这些元数据包括当前堆块的大小、前一个块、下一个块、是否正在使用等信息。基于这些知识,可以尝试利用堆的功能算法; -
攻击堆数据:另一方面,根据之前显示的堆溢出,可以考虑覆盖其他内存块。如果在当前内存之后的堆块中存储了有用的数据,此时可以覆盖并利用它。在本文中就是采用的这种利用方式。
在上面提到过,堆分配时难以预测。在不同执行过程中,缓冲区位置可能会因大小、环境变量和其他因素不同,导致缓冲区分配在堆的不同位置。
那么,问题来了:应该如何布局,将缓冲区定位在特定“有用”数据之前呢?这就需要用到堆风水。
堆风水/堆喷
堆风水是一种复杂的内存利用技术,涉及操纵堆内存的布局,以实现有利于利用的特定内存条件。堆风水的主要目标是操纵堆内存的布局,通过策略性地分配和释放内存块,影响内存块分配到堆中特定位置的过程。这可能涉及反复分配和释放块,以控制敏感数据或函数指针的放置位置。
二、GLIBC 堆溢出漏洞(CVE-2023-6246)
Qualys 安全团队发现了 GLIBC 堆溢出漏洞。在他们的研究中,发现某些版本 GLIBC 的 __vsyslog_internal 函数在处理动态内存时方式不当,攻击者可以更改程序名称(argv[0]),以操纵缓冲区大小并将超长的数据写入堆中导致堆溢出。
以下命令可以测试是否容易受到此堆溢出的影响:
$ (exec -a "`printf '%0128000x' 1`" /usr/bin/su < /dev/null)
Password: Segmentation fault (core dumped)
该漏洞影响Debian、Ubuntu、Fedora等多个操作系统,但具体利用方法可能有所不同。
利用分析
根据 Qualys 的披露信息,攻击者可以根据不同大小的程序名称来触发漏洞,并通过 ‘-w’ 白名单选项来构造堆风水。通过堆布局,可以覆盖模块的字段名称,并注入自己的共享库以提升权限。
首先,必须确保在堆溢出发生后,能够访问操纵模块结构的程序函数。这一点至关重要,因为如果配置或操作系统在程序的执行流程中进行了更改,并且没有调用这些函数,那么利用就不会成功。
好消息是,我们知道我们需要什么。因此,使用了gdb-peda来调试“su”二进制文件。强烈建议使用调试符号来与源代码一起进行调试。为了方便设置程序名称,定义了一个类似下面的 wrapper.sh 文件:
p=""
for i in {1..buff_length}; do
p+="A"
done
p=$(echo -e "$p")
exec -a "$p" "$@"
然后,在 gdb 中,设置了exec-wrapper作为命令将程序名称更改为大于 1024 的值。并在一些有趣的函数中设置了断点,以了解它们的上下文、工作原理、调用方式和时间。
set exec-wrapper ./wrapper.sh
set env A=a
b __vsyslog_internal
b __nss_lookup_function
b __nss_module_get_function
b module_load
b env_whitelist_from_string
print(‘A’)”` r -w`python -c “
经过调试分析后得出了一些结论:
-
堆溢出发生在 vsyslog_internal
中,在用户输入密码后; -
在程序执行期间, __nss_lookup_function
和__nss_module_get_function
都被多次调用,即使在vsyslog_internal
之后也是如此; -
它们只在模块尚未加载的情况下会调用 module_load
。 -
白名单选项(-w)允许根据用户输入和环境变量的存在来分配和释放一些数据,稍后会进一步探讨这个问题。
接下来进行模糊测试。
Fuzzing
以下 Fuzzer 侧重于 fuzz 程序名称大小、白名单选项及其关联的环境变量。
这个 fuzzer 非常基础,因为它只迭代测试了不同长度的程序名称和环境变量。fuzzer 监视每次执行产生的核心转储文件, 如果之前没有记录过核心转储,fuzzer 就会存储它,这使得能够跟踪具有不同错误的核心转储文件,及其相应的参数组合。
然而,这个 fuzzer 产生了大量的案例。因为对于每组环境变量和白名单选项,fuzzer 每次都将程序名称长度从 1000 增长到 120000。而且它并没有涵盖所有可能的场景,如果想在白名单选项中使用重复值,例如“-w A,A,A,B,B,C,C”,该怎么办?
在手动测试期间观察到,如果用户手动输入密码,而不是从 /dev/null 重定向,堆分配会发生变化。因此创建了另一个版本的 fuzzer 来测试,就是使用 forkpty 使用文件描述符向密码发送换行符,这次发现了一些有意思的输出,例如可以找到 Qualys 在其披露中提到的一些案例,比如对“RAX”的调用:
这种情况可能很有意思,但如果使用这种方式进行利用,那么需要在我们的输入中加入内存地址,这会使得程序名称必须包含至少一个 x00 字节(空字节),但是我们不能在程序名称中插入空字节。而且由于ASLR 的存在,没有内存泄漏使得利用变得非常困难,便放弃了这种利用方式,尽管也许可以利用。
除此之外,fuzzer 还生成了几个值得注意的输出:
这些结果非常有意思,因为这些情况与 sudo baron SameEdit (CVE-2021-3156,sudo堆溢出漏洞)堆溢出利用非常相似,可以通过使用包含斜杠的库名称来注入共享库。例如,如果在名称字段中写入“X/X”,则会加载 libnss_X/X.so.2库文件。
想要在执行时覆盖 ni 和 module 结构体指针,二者实际上会使用相同的环境变量和白名单选项,只是程序名称的长度不同:
program_name_length = 9000 # Case overwrite module
program_name_length = 12000 # Case overwrite Ni
gdb-peda$ env A=a
gdb-peda$ run -w`python -c "print('A,'*2500)"`
经过调试和比较后发现,module.name 字段非常靠近输入的缓冲区,只要输入更长的程序名称即可覆盖它:
由于我们的缓冲区放置在 module 内容之前,因此可以很容易进行堆块布局,但现在还存在两个问题:
-
我们没办法在程序名称中包含斜杠。即使我们能够覆盖名称字段,它也不会被加载,因为我们的堆溢出(程序名称)不能包含斜杠。为了解决这个问题,观察了堆溢出的缓冲区, 我们的堆附加了身份验证失败的错误消息,其中在 tty=/dev/pts/1 中会包含斜杠(参见图十)。因此,现在我们知道,如果我们创建一个与错误消息结束的缓冲区同名的共享库,这种利用很可能成功。(这里用户名是kali,但操作系统是fedora,kali只是使用的用户名) -
第二个问题是程序在执行“__nss_module_get_function”之前,程序需要使用 module 指针和 ni 结构,由于它们被我们的缓冲区覆盖,因此程序发生了崩溃。
这时候就需要使用堆风水对堆块进行布局了。
三、Fengshui
在图六中,可以观察到我们实际上已经覆盖了module指针。但现在要搞清楚堆布局,弄清楚在这儿发生了什么,就不能使其发生溢出。现在设置一些断点,并使用相同的白名单选项和环境变量重新运行:
set env A=a
print(‘A,’*2500)”` r -w`python -c “
现在可以看到环境变量被放置在堆中。经过仔细观察,可以注意到的是这些间隙的尺寸正在增大,形成了一种规律。这种现象是有原因的,因为我们重复执行了大量的 malloc 和 free 操作,这些间隙很可能是按这种分配和释放模式产生的。
在堆溢出时,其中一些间隙可能被程序用于其他目的,当缓冲区覆盖这些间隙时,就会出现问题,导致有用数据被破坏。这就是我们覆盖 module 指针和 ni 结构的两种情况所发生的情况,导致了程序的崩溃。那应该如何处理呢?
试想一下, 如果我们根据破坏程序的块的大小创建更多环境变量会怎样?如下图:
在上图的左侧部分,描述了当前的堆布局。module 指针和 ni 结构体在这些缓冲区中间,随意的溢出将会破坏这些缓冲区的数据。但是,如果我们控制一些其他环境变量来占用它们的位置呢?在这种情况下,它们将无法再占用这些位置,因此将会在不同内存位置进行分配。这正是在上图右边堆图中实现的,我们又定义了两个环境变量B和C,其内容与 module 指针块和 ni 结构堆块的大小相同,新的环境变量占用了先前执行中 module 指针和 ni 结构间隙所占用的空间,从而迫使它们重新定位到不同的位置。
首先尝试使用 module 指针将猜想付诸实践,为了避免覆盖到它,声明了一个具有适当大小的环境变量并分配了 5 次:
在上图中,我们可以看到左侧的 RAX 是模块的指针 0x55555557c6c0,底部有一个 0x60 大小的块,与存储该指针的位置相匹配。在图右侧部分,我们将环境变量“F”设置为值 f*0x55,因为我们想要占用块大小为 0x60 的空间。正如所看到的,模块指针现在是 0x5555555bc2e0,因为环境变量中的“f”占用了 0x60 块。
接下来对 ni 结构重复相同的过程,设置与 ni 结构大小相同的堆块。此时可能会注意到一些事情:更改环境变量中的大小,甚至使用更多环境变量,可能会改变堆的整个布局,这可能导致我们的缓冲区被放置在不同的位置。为了防止缓冲区被分配到意想不到的位置,调整环境变量的数量和白名单选项非常重要。
经过一番尝试,最终实现了所需的布局,程序不再崩溃,并且成功覆盖了 module 数据:
接下来就可以尝试权限提升了。
四、权限提升
如果想加载自己的共享库,则需要稍微调整一下缓冲区消息:
此时程序正在尝试加载名为“libnss_AAAAAAAAAA: pam_unix(su:auth): authentication failure; logname= uid=$uid euid=0 tty=/dev/pts/2 ruser=$user rhost= user=roo.so.2”的共享库。
此处“uid”、“ruser”和“tty”中的值可能会有所不同,具体取决于 tty 和用户名,这不难获取。此时可以创建一个与程序正在查找的文件名相匹配的文件夹结构,并编译具有预期名称的提权代码:
最终效果如下:
利用成功,获得root权限 。 可以在这里 [2] 找到整个脚本。
注意:运行 ./run.sh 时,会提示输入密码,此提示来自请求密码的 su 二进制文件。在此过程中必须手动输入密码,但不必是正确的密码。
参考
[1] https://medium.com/@elpepinillo/heap-heap-hooray-unveiling-glibc-heap-overflow-vulnerability-cve-2023-6246-0c6412423269
[2] https://github.com/elpe-pinillo/CVE-2023-6246
原文始发于微信公众号(山石网科安全技术研究院):HEAP HEAP HOORAY - 揭示 GLIBC 堆溢出漏洞 (CVE-2023–6246)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论