8月份,FreeBSD发布了更新,以解决检查时间到使用时间(TOCTOU)漏洞,该漏洞可能会被非特权恶意用户空间程序利用来进行权限提升。该漏洞是由一位名为m00nbsd的研究人员报告给ZDI程序的。他很客气地提供了详细说明ZDI-20-949/CVE-2020-7460的编写和概念验证代码。
目标是利用32位sendmsg()系统调用中存在的TOCTOU漏洞,从非特权用户开始在FreeBSD上执行内核代码。此漏洞已被指定为CVE-2020-7460,并自2014年起影响所有FreeBSD内核。在我们进入细节之前,这里有一段简短的视频,展示了如何利用漏洞进行攻击。
让我们直接跳到漏洞所在的位置:frebsd32_copy in_control()函数。此函数由两个循环组成,注释如下:
让我们看看这是怎么回事。第一个循环从userland获取数据。此数据是一组连续的cmsghdr结构:
第一个循环执行长度检查,并确保缓冲区的总长度(len字节)可以容纳随后分配的内核缓冲区(大小为MLEN字节)。
一旦长度检查通过,则分配所述内核缓冲区,并且第二循环将用户区域数据复制到该缓冲区中。内核执行一些处理将结构从32位转换为64位,但这在这里并不相关,因此我们将省略这一点。
但是,这里有一个TOCTOU漏洞:在第一个循环和第二个循环之间,可能是userland修改了其cmsghdr结构的cmsg_len字段,所产生的总长度现在超过了MLEN。
假设就在第一次循环之后,userland增加了最后一个cmsg_len字段的值:
我们可以通过32位sendmsg()syscall触发此堆溢出,其格式如下:
MSG_CONTROL是我们的用户内存缓冲区开始的地方。这就是相邻的cmsghdr结构所在的位置。
--创建cmsghdr结构的连续块,这些结构最初是有效的。--在我们的块循环中产生一个调用sendmsg()的线程。--生成另一个线程,该线程增加最后一个cmsg_len字段的值,然后在循环中恢复为正确的值。
一旦完成,我们只需等待几秒钟,让两个线程竞争。很快,内核就出现了恐慌。我们有一个良好的开端。
我们感兴趣的是何时触发堆溢出,这样我们就可以知道何时停止两个竞争线程。也就是说,我们不想让线程运行太长时间,从而以可能导致死机的过于极端的方式使内核内存溢出。相反,我们更愿意在第一次成功溢出后立即停止,以提高利用漏洞的可靠性并限制对内核的损害。
这个想法包括利用copy in()的行为,该函数用于将用户数据复制到内核内存中。为了防止用户给出未映射的页面(或者只是一般意义上的垃圾),copy in()优雅地处理内核中的页面错误,并在复制过程中遇到未映射的页面时安全地返回EFAULT。系统调用依次返回EFAULT。
在堆溢出的情况下,我们只需将未映射的页面放在我们希望复制结束的位置即可。然后,CopyIn()将复制所有内容,直到这个未映射的页面,并在该页面上出错,然后返回EFAULT,这会导致sendmsg()也返回EFAULT。
因此,如果sendmsg()返回EFAULT,则命中未映射的页面,我们知道已触发溢出。
因此,我们可以选择写什么、写多少,并确切知道何时触发溢出。更多的进步。
我们溢出的内核缓冲区是一个nmbuf。Mbuf是一种特殊的结构,使用FreeBSD的常规区域分配器进行分配。在不涉及不必要的细节的情况下,mbuf在大页面上一个接一个地放置。这意味着,在堆溢出的情况下,很有可能会覆盖内存中的下一个mbuf。
从开发的角度来看,mbuf结构具有有趣的领域。这里,我们将使用m->;m_ext.ext_free字段,它是指向函数的指针,具有以下原型:
当内核希望释放mbuf时,它会检查mbuf中的某些标志。如果检查成功,内核将调用mbuf的ext_free函数,并期望该函数释放mbuf。
目前的情况是,我们有一个堆溢出,它允许我们覆盖内存中的下一个mbuf,因此,我们可以修补该下一个mbuf的EXT_FREE字段。
究竟如何才能调用EXT_FREE呢?一旦我们检测到堆溢出成功,我们必须快速触发释放内存中的下一个mbuf。
--客户端使用常规的sendto()向服务器发送数据包。这会导致在内核中分配mbufs。可以将其视为基本上分配mbuf的PushMbuf()原语。--服务器使用常规的recvfrom()接收这些数据包。这会释放分配的mbuf,并调用它们的ext_free。可以将其视为释放mbuf的PopMbuf()原语。
我们使用PopMbuf()弹出我们刚刚推送的50%的mbuf(通过实验选择)。这会在分配图中创建洞。
我们触发堆溢出并覆盖紧跟在我们之后的某个mbuf的ext_free。
一旦我们知道堆溢出被触发,我们就使用PopMbuf()弹出我们推送的剩余50%的mbuf。
如果幸运的话,这将释放我们刚刚覆盖了其ext_free的mbuf。因此,EXT_FREE被调用并跳转到我们控制的地址。如果我们运气不佳,我们将返回步骤1并再次尝试,直到成功为止。
该过程通常在不到1秒内成功,并且内核跳转到我们完全控制的EXT_FREE地址。在这里,一条COP/JOP/ROP链开始了。
我们在哪里?我们刚刚设法让内核跳转到我们控制的地址。寄存器的状态相当简单:
这里,%rdi指向我们覆盖的mbuf。如果我们将我们的COP/JOP链建立在某个偏移量(%RDI)上,那么我们实际上是在一个缓冲区上,因为我们之前设法将其内容溢出到该缓冲区中,所以我们可以控制该缓冲区的内容。
不幸的是,很少有有用的小工具可以链接在一起,而且我们的空间也很有限,因为mbuf内容的一部分已经被需要保持有效的字段使用了。
考虑到显然缺乏好的小工具,我们不得不求助于一些扭曲的COP/JOP小工具。
我们让两个线程相互竞争以触发sendmsg()中的堆溢出,这允许我们覆盖mbuf。在我们溢出的mbuf中,我们编写了一个新的EXT_FREE指针和COP/JOP/ROP链的数据。
我们检测到成功的溢出,并使用UDP服务器释放我们分配的mbuf,这会导致调用ext_free并启动链。
此处提供的概念验证代码将您从非特权用户带到根shell。它的观测可靠性为90%。
根据FreeBSD的说法,i386和其他32位平台不容易受到攻击。对于那些易受攻击的系统,您需要升级到受支持的FreeBSD稳定版或版本/安全分支(重新生成),日期在更正日期之后,然后重新启动。那应该可以解决虫子了。FreeBSD的团队仅用了两周时间就构建并发布了补丁。确实干得不错。
再次感谢m00nbsd提供了这篇很棒的文章和PoC。这是他第一次向ZDI计划提交FreeBSD,我们当然希望在未来能看到他提交更多。在此之前,请关注团队以获取最新的利用漏洞技术和安全补丁。