[kernel exploit] linux 内核常规堆溢出漏洞的"胜利方程式"
文章目录
简介
背景
对于内核中堆溢出的利用手段还是比较多的,在以前常规的堆类漏洞利用方法中,通常是想办法将堆溢出转换为越界读取泄露地址,然后重新溢出转换为任意地址写或劫持RIP 然后ROP。其中用到的技术即复杂限制又大,比如:
- ROP中还要绕过很多内核的防御机制;
- 通常要使用两次左右的溢出操作,对堆布局稳定性要求高;
- 使用原语种类繁多,技术复杂难度高;
- 由于不同内核版本和编译批次,内核的地址是不同的,exp 对于目标的普遍适用性很低,更多的是体现在研究层面,实战可用性不大。
不过在前些日子CVE-2022-0847(Dirty Pipe) 漏洞公布之后,经由veritas501的改良,对于绝大部分linux 堆类漏洞,都可以通过篡改pipe_buffer 中的flag成员 (版本大于5.8) 或pipe_buffer 中的ops成员 (版本小于5.8)。来实现将任意堆漏洞转化为Dirty Pipe 的无地址依赖类型漏洞。该漏洞利用方法的优点在于:
- 首先就是不依赖任何地址,只要满足利用条件,无视内核版本进行利用,无需修改exp
- 大部分情况只需要布置一次堆(溢出一次),成功率也有提升。
- 原语简单,容易理解
前置条件与技术
本文只介绍一种比较通用的堆漏洞利用手法,根据CVE-2021-22555 变种而来。当然了,如果将"篡改pipe_buffer 的flag 或ops" 视为当前状态下最合适的攻击最终目标的话,攻击路径绝是有非常多的,这里只介绍对大部分堆溢出类漏洞都适用的比较无脑的方式。
前置条件:
- 能确保溢出到下一个堆块的连续溢出任意内容;
- 溢出写0;
- 跳跃溢出但可以经过计算后溢出位置一定程度可控,溢出的堆大小属于64-4k。
- 下面两条满足一条即可
- 内核版本 ∈[5.9~5.14] (在该版本区间内kmalloc 分配flag GFP_KERNEL_ACCOUNT 和GFP_KERNEL 分配的slab不会隔离)
- 漏洞堆申请试用GFP_KERNEL_ACCOUNT flag,当然如果不是的话也可以利用,就是概率很低并且可能需要在堆布局上改良。(如果不满足这一条,其他利用方法也是一样头疼)
使用的技术:
- msg_msg 原语,用于堆喷射和堆布局,作为溢出目标堆
- sk_buff 原语,用于堆喷射占位
- pipe_buffer原语,最终漏洞利用,转换为dirty pipe
漏洞利用
准备
以下将堆溢出漏洞发生的堆块称为vuln obj 简称VO。
msg队列布置
首先创建若干msg 队列,然后每个队列中布置两个消息,每个消息内容中都要附带该消息所属消息队列的编号,做到能根据接收的消息内容区分来自哪个消息队列,其他部分消息内容最好用0填充,方便后续利用。分别为PrimaryMsg 和SecondaryMsg,以下简称MSG1和MSG2。MSG1大小为VO大小,MSG2大小为0x400(pipe_buffer x 16 大小)。组成如下图所示:
实际喷射会比上图数量多得多,并且Msg 们在内存中也并不是这么整齐对应的。然后可以释放一些MSG1,如图中的idx 04,构造一些空洞,然后申请VO,这时如果VO 能申请到刚释放出的空洞上,就算堆布置成功,唯一考验成功率的操作就是这一步,接下来的操作成功率都非常高:
溢出构造msg corrupt
将VO部分放大,堆布局如下,数字为地址低二字节:
通过堆溢出,越界写下面的目标MSG1的msg_msg->m_list_next 成员通常需要覆盖两个字节为0 或至少覆盖第二个字节为0(第一个字节本来就是0),就会形成下图所示布局:
这时我们遍历所有消息队列使用MSG_COPY的方法接收每个队列中的消息二 MSG2(MSG_COPY 不会释放消息),并且判断接收到的消息编号,这时会发现有一个消息队列接收到了别的队列的MSG2,记录这两个队列编号,被接受到的队列编号为real idx ,接收到real idx 的MSG2 的队列编号为corrupt idx ,将这个被两个MSG1 同时指向的MSG2 称为target MSG2:
构造 MSG UAF
虽然现在有两个MSG1 都指向了同一个MSG2 ,我们的目的是让target MSG2这个堆空间free两次,这样就能让sk_buff 和pipe_buffer 同时占用target MSG2 这个堆区域。但如果直接将MSG2 释放两次的话,会过不了内核中双向链表的unlink,所以我们还得帮target 构造两个可以通过unlink 的next 和prev 指针。
unlink 操作:
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
prev->next = next;
}
根据unlink 的代码,我们可以看出,只需要让它的next 指针和prev 指针都指向自己,就不会让unlink 崩溃,并且也不会破坏链表结构。那么接下来就是通过两次泄露找到该堆的自己的地址。
泄露msg 地址 1
使用real idx 队列释放target MSG2,然后立刻使用0x400 的sk_buff 占位住,占位sk_buff 的内容构造一个假的msg 头部,其中:
- m_ts 成员(代表msg 大小)要写的大一些,一会会越界读泄露下面msg 的指针
- msg 类型为一个自定义的类型,方便一会接收。形成如下结构:
- 其他部分不至于覆盖成0即可
由于target MSG2 的size 被改大,使用corrupt idx 可以越界读取到与target MSG2 内存相连的下一个MSG2 也就是idx 02 MSG2的头部,读取它的prev 指针为leak1。
泄露msg 地址 2
释放刚刚的sk_buff 并马上重新喷射sk_buff,新sk_buff也是构造一个假的msg 头部,和上面有区别:
- m_ts 成员大于4k ,如0x1000+0x100
- msg 类型为一个自定义的类型,方便一会接收
- msg_msg * next指针为leak1 - 8,即刚泄露的msg 地址- 8
- 其他位置不重要,0即可
如下图所示:
由于target MSG2 大小被修改大于0x1000,在msg 中属于分段存储的msg,并且将msg_msgseg *next 指向刚刚泄露的idx 02 MSG1 - 8,那么直接可以读到idx 02 MSG1->m_list_next 也就是idx 02 MSG2 的地址,然后只需要减掉0x400 就是target MSG2 的地址了。
msg UAF
这时我们已经获得了target MSG2 的地址,释放sk_buff 在马上重新喷射sk_buff,这次伪造的堆头部如下:
- m_list_next 和m_list_prev 都是自己的地址
- msg 类型为一个自定义的类型,方便一会接收
- 大小、msg_msgseg *next都改回来
- 其他不重要
其他部分不变,大小还是0x400,喷射之后,就可以使用corrupt idx 来再次释放target MSG2了:
Dirty Pipe
这一段的原理与Dirty Pipe 漏洞的原理相同,不对原理进行阐述,原理可以移步[漏洞分析] CVE-2022-0847 Dirty Pipe linux内核提权分析。
这里需要选择一个Dirty Pipe 需要篡改的文件,一般选择带suid 的文件将其改为恶意shell 文件。通常选择mount。
初始化pipe
使用corrupt idx 来再次释放target MSG2,然后创建若干pipe 队列,就会让某一个队列的pipe_buffer 也占用到该刚释放的target MSG2,然后对所有消息队列中的16个页进行填充和释放,来保证pipe_buffer 之中内容被初始化,然后对每个队列都试用splice,将目标文件的缓存页拼接上(这一步同Dirty Pipe),不过这里为了方便到时候判断哪个pipe 占住了target MSG2,我们需要在pipe 的第一页写上长度不同的内容:
篡改flag/ops 进行Dirty Pipe
不要忘记我们还有一个sk_buff 占用这这个target MSG2呢,遍历读取所有sk_buff,寻找pipe_buffer ,根据pipe 结构的内容,判断对应位置的内容是否是,这里判断len 字段就可以,由于我们每一个pipe 队列第一页的len 不同,如果遇到了满足区间的大小,那么久确定是目标target pipe_buffer。根据读取的len 可以确定pipe 队列编号。
读取到该pipe_buffer 的内容,由于第二页是我们splice 的文件缓存页,直接修改刚读取的pipe_buffer内容的第二个pipe_buffer结构体:
- flag为PIPE_BUF_FLAG_CAN_MERGE,
- 如果内核版本小于5.8 还没有PIPE_BUF_FLAG_CAN_MERGE 的话,就修改的第二个pipe_buffer 的ops 结构体为第一个pipe_buffer 的ops 就可以。
- offset 和len 修改为0
然后重新喷射sk_buff即可完成dirty pipe 的转化,然后直向该pipe 队列发送消息就可以完成对目标文件的临时篡改。将suid 文件改为恶意shell 执行即可。
成功案例
下面案例仅代表可以用本文介绍的"胜利方程式"完成利用的CVE漏洞。
CVE-2022-0995
CVE-2021-22555
CVE-2021-42008
参考
CVE-2022-0185分析及利用 与 pipe新原语思考与实践