对于内核中堆溢出的利用手段还是比较多的,在以前常规的堆类漏洞利用方法中,通常是想办法将堆溢出转换为越界读取泄露地址,然后重新溢出转换为任意地址写或劫持RIP 然后ROP。其中用到的技术即复杂限制又大,比如:
不过在前些日子CVE-2022-0847(Dirty Pipe) 漏洞公布之后,经由veritas501的改良,对于绝大部分linux 堆类漏洞,都可以通过篡改pipe_buffer 中的flag成员 (版本大于5.8) 或pipe_buffer 中的ops成员 (版本小于5.8)。来实现将任意堆漏洞转化为Dirty Pipe 的无地址依赖类型漏洞。该漏洞利用方法的优点在于:
本文只介绍一种比较通用的堆漏洞利用手法,根据CVE-2021-22555 变种而来。当然了,如果将"篡改pipe_buffer 的flag 或ops" 视为当前状态下最合适的攻击最终目标的话,攻击路径绝是有非常多的,这里只介绍对大部分堆溢出类漏洞都适用的比较无脑的方式。
前置条件:
使用的技术:
以下将堆溢出漏洞发生的堆块称为vuln obj 简称VO。
首先创建若干msg 队列,然后每个队列中布置两个消息,每个消息内容中都要附带该消息所属消息队列的编号,做到能根据接收的消息内容区分来自哪个消息队列,其他部分消息内容最好用0填充,方便后续利用。分别为PrimaryMsg 和SecondaryMsg,以下简称MSG1和MSG2。MSG1大小为VO大小,MSG2大小为0x400(pipe_buffer x 16 大小)。组成如下图所示:
实际喷射会比上图数量多得多,并且Msg 们在内存中也并不是这么整齐对应的。然后可以释放一些MSG1,如图中的idx 04,构造一些空洞,然后申请VO,这时如果VO 能申请到刚释放出的空洞上,就算堆布置成功,唯一考验成功率的操作就是这一步,接下来的操作成功率都非常高:
将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:
虽然现在有两个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 崩溃,并且也不会破坏链表结构。那么接下来就是通过两次泄露找到该堆的自己的地址。
使用real idx 队列释放target MSG2,然后立刻使用0x400 的sk_buff 占位住,占位sk_buff 的内容构造一个假的msg 头部,其中:
由于target MSG2 的size 被改大,使用corrupt idx 可以越界读取到与target MSG2 内存相连的下一个MSG2 也就是idx 02 MSG2的头部,读取它的prev 指针为leak1。
释放刚刚的sk_buff 并马上重新喷射sk_buff,新sk_buff也是构造一个假的msg 头部,和上面有区别:
如下图所示:
由于target MSG2 大小被修改大于0x1000,在msg 中属于分段存储的msg,并且将msg_msgseg *next 指向刚刚泄露的idx 02 MSG1 - 8,那么直接可以读到idx 02 MSG1->m_list_next 也就是idx 02 MSG2 的地址,然后只需要减掉0x400 就是target MSG2 的地址了。
这时我们已经获得了target MSG2 的地址,释放sk_buff 在马上重新喷射sk_buff,这次伪造的堆头部如下:
其他部分不变,大小还是0x400,喷射之后,就可以使用corrupt idx 来再次释放target MSG2了:
这一段的原理与Dirty Pipe 漏洞的原理相同,不对原理进行阐述,原理可以移步[漏洞分析] CVE-2022-0847 Dirty Pipe linux内核提权分析。
这里需要选择一个Dirty Pipe 需要篡改的文件,一般选择带suid 的文件将其改为恶意shell 文件。通常选择mount。
使用corrupt idx 来再次释放target MSG2,然后创建若干pipe 队列,就会让某一个队列的pipe_buffer 也占用到该刚释放的target MSG2,然后对所有消息队列中的16个页进行填充和释放,来保证pipe_buffer 之中内容被初始化,然后对每个队列都试用splice,将目标文件的缓存页拼接上(这一步同Dirty Pipe),不过这里为了方便到时候判断哪个pipe 占住了target MSG2,我们需要在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结构体:
然后重新喷射sk_buff即可完成dirty pipe 的转化,然后直向该pipe 队列发送消息就可以完成对目标文件的临时篡改。将suid 文件改为恶意shell 执行即可。
下面案例仅代表可以用本文介绍的"胜利方程式"完成利用的CVE漏洞。
CVE-2022-0995
CVE-2021-22555
CVE-2021-42008
CVE-2022-0185分析及利用 与 pipe新原语思考与实践
CVE-2021-22555: Turning \x00\x00 into 10000$
[漏洞分析] CVE-2022-0847 Dirty Pipe linux内核提权分析