这篇文章将重点介绍apport CVE-2019-15790,这个漏洞允许本地攻击者可以获取他启动(或重启)的进程的ASLR偏移量。
简介
这个漏洞不容易在源代码中找到。相反,它与使用PIDs作为授权令牌有关,内核调用Apport来为崩溃的进程生成错误报告,apport的命令行参数之一是进程的PID。虽然进程崩溃了,但它并没有从PID表中删除,因为这要等到apport报告生成完成之后才会发生。这让apport能够在/proc/[pid]
中读取有关崩溃进程的更多信息。并将其包含在错误报告中。那如果我通过发送SIGKILL
信号更快地结束进程,会发生什么呢?
- 1.由于PID被回收,是否有可能为新进程分配相同的PID?
- 2.如果为一个新进程分配了相同的PID,是否可以检测到已发生这种情况?
第一个问题的答案是肯定的,第二个问题的答案是否定的。原因是可用的pid池非常小。在一个默认安装的Ubuntu中只有32768个,因此PID作为一个令牌标记是非常不可靠的。这让我能够欺骗Apport泄露不属于我的进程的信息。
Exploit方案
对于如何构造这个漏洞的Exploit,我有两个想法。
方案A(失败的)
我的第一个想法是让这个漏洞来规避我在CVE-2019-7307 exploit中遇到的问题,在本系列的第二篇文章中讨论了这个问题。这里的问题是apport查看/proc/[pid]/stat
的所有权,来确定错误报告属于谁。但是/proc/[pid]/stat
属于root所有,所以我无法查看错误报告。如果我使用PID回收机制将/proc/[PID]/stat
替换为我拥有的文件,会发生什么?能让我查看错误报告吗?
我写了一个exploit来做到这一点,并且几乎有用。但问题是apport很早就检查了 /proc/[pid]/stat
所有权。也就表示该exploit需要在apport检查所有者之前向崩溃的进程发送一个SIGKILL
。内核使用管道将核心转储文件发送到apport的stdin
。因为从进程表中删除了进程,内核将中止发送剩余的核心转储文件。不过,我发现第一个64KB的核心转储文件已成功写入错误报告,管道确实有缓冲区。不幸的是,这还不足以包含我想要窃取的那部分信息。如果管道缓冲区更大,这将起作用。我差点就要完成exploit了。
方案B
方案A无效,但我意识到可以用一种相反的方式来利用这个漏洞,方案A的主要思路是在特权进程崩溃后取得其PID的所有权。相反,方案B是在特权进程为其分配回收之前的PID,故意让属于自己的进程崩溃。这不允许我访问特权进程的核心转储文件,因为核心转储文件将来自我故意崩溃的进程。但它能够让我访问apport从/proc/[pid]
中读取的额外信息,这些信息包含在错误报告中。其中最有价值的是/proc/[pid]/maps
,它包含进程的ASLR偏移量。
这是方案B的简图:
该图显示的时间向下走动。从最上面开始,以下是发生的事件:
- 1.启动一个
/bin/sleep
进程,该进程将获得分配的PID(例如1234). - 2.发送一个
SIGSEGV
到PID(1234),这会触发内核启动apport并为/bin/sleep
发送一个内核转储文件. - 3.暂停apport.
- 4.等待特权进程启动(并分配PID 1234).
- 5.恢复apport.
- 6.Apport为
/bin/sleep
创建一个错误报告,其中包含特权进程的ASLR偏移量。
当然,还有一些细节问题需要解决。这个exploit取决于特权进程被分配与/bin/sleep
相同的PID。确保这一点的最简单方法是反复启动和停止进程,直到它获取正确的PID,但这可能需要一段时间。所以我需要暂停apport。否则,apport将很快完成,并读取/bin/sleep
的ASLR偏移量,我并不关心这些。
实现exploit
我的POC源代码已经公布在GitHub上。这个漏洞可以用于获取任何进程的ASLR偏移量,前提是可以控制进程何时创建。但是,我将目标对准了whoopsie,因为它可以帮我完成在第二篇文章中的漏洞利用链,需要知道我可以根据需求随时重启whoopsie守护进程,因为它有一个堆溢出漏洞,我可以使用它来触发SIGSEGV
。下一篇文章将介绍如何利用堆溢出来获取whoopsie用户的代码执行,但是首先我需要知道ASLR的偏移量。
exploit按照我描述的方案执行,并采用了我在前一篇文章中描述的许多相同的技术。例如,它使用inotify监控/var/crash/.lock
来检测apport何时启动。但我需要使用一种新技术来暂停apport和重启whoopsie,注意,大约需要15秒才能使whoopsie崩溃并重新启动,这使暂停变得至关重要。这个exploit暂停apport需要调用第452行的get_pid_info和第500行的add_proc_info:
get_pid_info(pid)
# Partially drop privs to gain proper os.access() checks
drop_privileges(True)
# *** Lines 454-499 omitted ***
info.add_proc_info(pid)
这是因为get_pid_info
读取/proc/[pid]
中的文件来确定谁拥有这个进程,然后 add_proc_info
读取/proc/[pid]/maps
。这表示,在get_pid_info
被调用时PID需要属于我所拥有的进程,但是到add_proc_info
调用时,PID需要被回收并重新分配给whoopsie守护程序。
我之所以能够暂停apport是因为在第455行调用了drop_privileges
,它将apport改变为”partially drop privs”(取消部分特权)状态。这个状态允许我向apport发送信号,因为它仍然具有root EUID,从而让它有读取系统上任何文件的特权!因此,我可以向apport发送SIGSTOP
,这会在whoopsie崩溃并重新启动时暂停该进程。然后,我向apport发送一个SIGCON
T,使它恢复。
遗憾的是,由于inotify对/proc
中的文件不起作用,我无法在精确的时间发送SIGCONT
。相反,我必须循环发送,直到信号被接受。
PID Feng Shui
我如何确保whoopsie获得的PID与我故意崩溃的/bin/sleep
相同?如上所述,在默认安装的Ubuntu中,pid是从一个32768的池中依次分配的。当这个数字达到32768时,它将变为0。假如我想要whoopsie从PID 10000开始。基本思路是不断地启动和停止无关的进程,直到我得到一个带有PID 9999的进程。下一个被分配的PID将会是10000,所以这就是whoopsie会获得的PID,前提是在whoopsie启动之前,不会启动另一个随机进程。我编写了一个名为fork_child_with_pid
的程序,它可以启动和停止一些无关的进程。我遇到的问题是,大约15秒在新的Whoopsie进程启始的同时总是启动/lib/systemd/systemd-udevd
进程。这意味着在whoopsie和udev进程之间存在争夺PID的竞争,这增加了一些不稳定性,并降低了whoopsie获得我想要的PID的机会。但这并不是一个大问题,因为whoopsie大约有33%的时间被分配到了正确的PID。
在这篇文章中描述的exploit使我能够获取whoopsie守护程序的ASLR偏移量。在本系列的最后一篇文章中,我将使用这些偏移量来成功利用whoopsie中的堆溢出漏洞,并以whoopsie用户获得代码执行权限。
本文翻译自GitHub Security Lab
发表评论
您还未登录,请先登录。
登录