附件地址:1f9K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6j5K9h3q4G2P5X3q4k6j5g2)9J5c8V1c8o6e0H3`.`.
kheap
难度:中等
内核版本:linux-6.2.16,smap/smep/pti/kaslr 全开
并且设置了如下编译选项:
1 2 3 4 5 6 | CONFIG_SLUB=y
CONFIG_SLAB_FREELIST_RANDOM=y
CONFIG_SLAB_FREELIST_HARDENED=y
CONFIG_HARDENED_USERCOPY=y
CONFIG_STATIC_USERMODEHELPER=y
CONFIG_STATIC_USERMODEHELPER_PATH=""
|
出题思路:在 cve-2021-22555 中,给出了一种针对大对象 [>= kmalloc-512] double free 的利用方式,即 msg_msg + sk_buff 劫持 pipe_buffer,但是由于 sk_buff 对象最小为 512 字节,所以该方法对于小于 512 字节的对象 double free 无法适用。当然这里也许可以用 setxattr 去替换 sk_buff,但这是不便的,因为其无法控制堆块释放的时间,当然你可以选择利用 fuse 去进行稳定控制。但是 setxattr 申请对象使用的是 GFP_KERNEL,所以如果这里存在 cg 隔离,则 setxattr 也将变得束手无策。于是笔者对小于 512 字节的对象的 double free 漏洞进行利用思考,总结出了 DCO 利用方式,于是出现了此次题目。
题目给了一次 0x40 [GFP_KERNEL] 大小对象 double free 的机会,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | static int ref_count = 1;
static void* kptr = NULL;
static long kheap_ioctl(struct file *filp, unsigned int cmd, unsigned long size) {
long retval = -1;
spin_lock(&sp_lock);
switch (cmd) {
case 0xdead:
if (kptr) {
printk(KERN_INFO "Bad Guy!\n");
break;
}
kptr = kmalloc(64, GFP_KERNEL);
ref_count++;
retval = 0;
break;
case 0xbeef:
if (!ref_count || !kptr) {
printk(KERN_INFO "Bad Guy!\n");
break;
}
ref_count--;
kfree(kptr);
retval = 0;
break;
default:
printk(KERN_INFO "Bad Guy!\n");
break;
}
spin_unlock(&sp_lock);
return retval;
}
|
本题 GFP_KERNEL 与 GFP_KERNEL_ACCOUNT 分配的对象存在隔离,而在漏洞利用中,对于 0x40 大小并且由 GFP_KERNEL 分配的对象并不多,对于常用的可适配的对象 msg_msg、pipe_buffer 都是通过 GFP_KERNEL_ACCOUNT 分配的。所以这里我们得先进行 cross cache attack 去突破 GFP_KERNEL 和 GFP_KERNEL_ACCOUNT 之间的隔离。并且 0x40 大小的由 GFP_KERNEL_ACCOUNT 分配的对象对于 double free 漏洞也不好进行利用。所以经过探索,笔者总结了一种针对 kmalloc-8~kmalloc-256 小堆块 double free 漏洞利用方式。这里命名为 DCO,即 Double free + Cross cache attack + Overlap object。
DCO 能够成功利用的原因:
cross cache attack
kfree 未对释放地址进行地址对齐检查
cross cache attack 突破 GFP_KERNEL/GFP_KERNEL_ACCOUT 和堆块大小的限制
由于是 CTF 环境,系统噪声影响不大,所以这里的 cross cache attack 利用方式比较简单粗暴,即:
- 先堆喷大量的
0x40 [GFP_KERNEL] 大小的对象,并分配 vuln obj [就是 double free 的对象],然后在将其全部释放
这里笔者选择堆喷的对象是 poll_list,最开始本来想用 user_key_payload 的,但是发现其释放回收不太稳定

kmalloc-8~kmalloc-256 的 pagesperslab 都为 1,所以这里是有很大的机会使得 empty slab 被伙伴系统回收的。
其实上述的 cross cache attack 并不稳定,因为对于 empty slab 的处理并不是简单粗暴的被伙伴系统回收。当然比较稳定的方式请参考 CVE-2022-29582,但对于 CTF 题目环境而言,简单粗暴的方式就够了。
在完成 corss cache attack 后就比较简单了。
overlap object 劫持 pipe_buffer 打 dirty pipe
在上面我们说了 kmalloc-8~kmalloc-256 的 pagesperslab 都为 1[在我本机是这样的,但是在 qemu 虚拟机中, kmalloc-256 的 pagesperslab = 2],所以此时我们在堆喷大量的 192 [GFP_KERNEL_ACCOUNT] 大小的 msg_msg,此时很大概率形成如下布局:

然后这里利用 kfree 的一个缺陷:
- 通过查看
kfree 源码可以发现:kfree 在释放对象时,并不会对传入的地址进行对齐检查
所以此时我们可以利用 double free,在此释放 vuln ptr 指向的地址,然后形成如下布局:

可以看到此时我们已经完成了 overlap object,此时我们在堆喷大量的 192 [GFP_KERNEL_ACCOUNT] 大小的pipe_buffer,此时形成如下布局:

此时我们利用 msg_msg 的带 MSG_COPY 标志读即可读取 pipe_buffer 的内容从而泄漏 kernel base [当然这里其实根本不用泄漏任意地址,因为后面我们直接打的 dirty pipe,这里读取的原因主要是为了确认是否命中,因为这里会将下一个 msg_msg2 破坏,直接释放所有的 msg_msg 会导致 kernel panic]。
然后我们在利用 msg_msg 的不带 MSG_COPY 标志读释放掉该 msg_msg,即可形成如下局部:

此时我们再堆喷 192 [GFP_KERNEL_ACCOUNT] 大小的 msg_msg,此时我们就可以篡改 pipe_buffer 的内容了,既然都可以篡改 pipe_buffer,那么自然就是打 dirty pipe 了。
dirty pipe 出来后,很多利用都变得比较简单了,在之前劫持 pipe_buffer 后,一般就是伪造 ops 进行 rop 布置提权。
注:以上方案,在 cross cache attack 稳定的情况下,成功率是 2/3,为啥呢?很简单,我们知道 192 = 0x40 * 3,如果 vuln ptr 指向的是 192 堆块的头部呢?即如下布局:

这时,当我们第二次释放 vuln ptr 时,就不会导致 overlap object
最后效果如下:

DCO kmalloc-32/96/128/192/256 double free 测试
笔者针对 kmalloc-32/96/128/192/256 [GFP_KERNEL] double free 进行了 DCO 利用,所有的 exp 和 LKM 可在我的 github 上获取。对于 kmalloc-8/16 需要 fuse + setxattr 去配合堆喷,所以这里就不再演示。
注:如果读者测试发现成功率比较低,则是堆喷策略导致的,读者可以自行更换堆喷策略
kmalloc-32 [GFP_KERNEL]:
1 2 3 | ......
kptr = kmalloc(32, GFP_KERNEL);
......
|

理论成功率 2/3:由于笔者堆喷策略比较垃圾,实际成功率比较低

kmalloc-96 [GFP_KERNEL]:
1 2 3 | ......
kptr = kmalloc(96, GFP_KERNEL);
......
|

理论成功率 1/2:

kmalloc-128 [GFP_KERNEL]:
1 2 3 | ......
kptr = kmalloc(128, GFP_KERNEL);
......
|

理论成功率 2/3:

kmalloc-192 [GFP_KERNEL]:
1 2 3 | ......
kptr = kmalloc(128, GFP_KERNEL);
......
|
这里由于页级堆风水不稳定导致 cross cache attack 几乎无法成功,所以最后漏洞利用失败,但是理论上是可行的。
最初想法,利用 kmalloc-512 进行利用,理论成功率 7/8:

这里之所以是理论成功率是因为,kmalloc-cg-512 的 pagesperslab = 4,而 kmalloc-192 的 pagesperslab = 1 ,然后我尝试利用页级堆风水,但是发现效果还是不好。
再思考:
其实这里我们非得找一个比 vuln obj 更大的对象吗?其实未必,我们需要的其实是一个对象被另一个对象包含,所以考虑如下布局:

是不是很熟悉,之前的 kmalloc-128 double free 就是这个布局,这次漏洞对象转换到了 kmalloc-192,但是我们可以发现这里是有概率发生 overlap object 的,只不过概率变成了 1/3,但是很可惜的是 pipe_buffer 并不适配 kmalloc-128。
我们知道 pipe_buffer 只能适配以下大小:
kmalloc-256 [GFP_KERNEL]:
1 2 3 | ......
kptr = kmalloc(256, GFP_KERNEL);
......
|
经过上述再思考,对于 kmalloc-256 double free 的利用应该就很清晰了,这里我们为了避免页级堆布局,所以还是在 kmalloc-64/96/192 中适配 pipe_buffer,其中 kmalloc-64 是不可行的,因为 256 / 64 = 4,而在 kmalloc-96/192 中,笔者选择的是 kmalloc-192。
理论成功率 2/3:

当然在我本机是 kmalloc-256's pagesperslab = 1,但是在 CTF 环境中 kmalloc-256's pagesperslab = 2。但是这里比较好的是适配 kmalloc-cg-192 pipe_buffer's pagesperslab = 1,所以这里的页级堆布局比较容易。我们只需要在堆喷 kmalloc-cg-192 pipe_buffer 时,提前消耗掉 buddy system order-0 page 即可,最后效果如下:

总结
这里笔者成功完成了 kmalloc-32/96/128/256 double free 的利用,对于 kmalloc-192 理论上是可行的,但是笔者的页级堆布局策略很烂,所以 corss cache attack 失败,然后笔者由于有其它事情,所以就不再深入下去。
这里得简单说下 kmalloc-192 与 kmalloc-256 double free 利用的区别,以此来说下为啥 kmalloc-256 的页级堆布局比较简单:
kmalloc-192:适配 pipe_buffer 大小为 kmalloc-cg-512
- 其中
kmalloc-192's pagesperslab = 1,而 kmalloc-cg-512's pagesperslab = 4。所以如果想让最后适配的 pipe_buffer 拿到 empty slab page,就得提前消耗掉 buddy system order-0/1 的页面,并且最后得申请 4 张连续的 page,然后得让这 4 张连续的 page 全部被 buddy system 回收,这样最好堆喷适配的 pipe_buffer 时,才有可能从 order-2 中拿到目标 empty slab page。当然页不一定申请 4 张连续的 page,只有最后能够让目标 empty slab page 回到 order->=2 即可。
- 所以经过上述分析,你会发现这种方式理论上可行,但是成功率极低【主要是自己的堆喷策略不行,还是太菜了】
kmalloc-256:适配 pipe_buffer 大小为 kmalloc-cg-192
- 其中
kmalloc-256's pagesperslab = 2,而 kmalloc-cg-192's pagesperslab = 1。所以如果想让最后适配的 pipe_buffer 拿到 empty slab page 就很简单:只需要在堆喷前消耗掉一定的 order-0 页面即可。因为当 order-0 被消耗完时,自然会拿到 order-1 的页面
当然你可能会问为什么 kmalloc-192 不使用 kmallc-64/96 去适配 pipe_buffer 呢?很简单,192 整除 64/96,所以不存在 overlap object
总结
这是得说明一下:笔者接触内核利用不久,不知道以上方式是否已经被命名,这里命名为 DCO 仅仅只是为了方便讨论,如果该方式已经被命名希望读者可以告知笔者,笔者将对上述 DCO 名称作相应的修改。
由于笔者的页级布局策略比较烂,所以DCO 目前无法完美解决 kmalloc-192 double free。而 DCO 之所以能够完成利用的主要原因在于 cross cache attack 和 kfree 的地址对齐未检查缺陷。
当然 DCO 其实也是可以用于 kmalloc-512 ~ kmalloc-2k 对象 double free 的利用的,但之所以没有将其归纳进 DCO,是因为在没有 cg 隔离时,大对象的 double free 利用 msg_msg + sk_buff 就可以了。当然主要的原因其实是因为大对象 pagesperslab 不同,所以这里得利用页级堆风水去进行页面布局,当然如果你对页级堆风水比较熟悉也是可以的。
比如在 d3ctf d3kheap 这个题目中,作者给了一个 kmalloc-1024 的 double free,且不存在 GFP_KERNEL 和 GFP_KERNEL_ACCOUT 之间的隔离,所以可以很方便的利用 msg_msg + sk_buff 进行利用。如果这里想利用 DCO,理论上也是可行的,通过堆风水,可以形成如下布局:

但是 kmalloc-1k 的 pagesperslab = 4,而 kmalloc-4k 的 pagesperslab = 8,所以如果想让最后堆喷 kmalloc-4k 的 pipe_buffer 占据该页面,就得让上述页面来自 order-3,所以就得提前消耗掉 order-2 的页面。当然这里 msgrcv 时还涉及到跨页,所以在开启 CONFIG_HARDENED_USERCOPY 时,又会存在一定的问题。所以综上所述,利用 msg_msg + sk_buff 更优。
当然 DCO 并不局限于 kmalloc-xx 通用缓存池小对象 double free 的利用,针对特定的缓存池小对象 double free 也可以完成利用,因为在完成 cross cache attack 后其实就跟漏洞对象在通用缓存池还是特定缓存池没啥关系了,因为此时漏洞对象所在的 slab page 已经被伙伴系统回收了。
corss cache attack 是一种非常有用且重要的利用技巧,通过 corss cache attack,我们可以突破 GFP_KERNEL 和 GFP_KERNEL_ACCOUNT 的隔离,可以突破漏洞堆块大小的限制,可以突破特定缓存对象的限制。当然了,据说 ATUOSLAB 可以防止 cross cache attack 的利用,但攻防本是一体,也希望未来能够看到更多的利用方式。
总的来说,目前针对 kmallc-512 ~ kmalloc-2k 的 double free 可以利用 msg_msg + sk_buff 完成利用,而针对 kmalloc-8 ~ kmalloc-256 的 double free 可以利用 DCO 完成利用。而针对 kmalloc-192 的 double free,笔者目前还未发现稳定的利用方式。对于 kmalloc-4k 大小的 double free,笔者认为可以利用 Dirty PageTable 完成利用,但是笔者没有进行验证。
当然这里的 kmalloc-192 double free 其实也因为 kmalloc-192's pagesperslab 与 kmalloc-512's pagesperslab 不同而导致没有完全解决。
最后如果有读者针对以上 kmalloc-192/4k 大小对象的 double free 利用或对 kmalloc-192 double free 稳定 corss cache attack 存在一定思路,欢迎联系讨论,企鹅号:MjM1MzU2NzA3MQ==。
后续
在之前说过对于 kmalloc-8/16 double free DCO 利用技术不适用是因为没有合适的堆喷 kmalloc-8/16 的策略,因为 poll_list 能够控制的堆块最小为 32。
但是其实这里还是有堆喷 kmalloc-8/16 的策略的,那就是 setxattr + userfaultfd/fuse,当然感兴趣的可以自行尝试,对于该题目而言 userfaultfd 普通用户是不可用的。但是在真实环境中我们可以利用 fuse 去配合 setxattr 进行 kmalloc-8/16 的堆喷。
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2024-11-24 14:38
被XiaozaYa编辑
,原因: