首页
社区
课程
招聘
[原创] DCO:一种针对小对象 [kmalloc-8~kmalloc-256] double free 的通用利用方式
2024-3-1 13:53 2116

[原创] DCO:一种针对小对象 [kmalloc-8~kmalloc-256] double free 的通用利用方式

2024-3-1 13:53
2116

附件地址:https://github.com/XiaozaYa/DCO

kheap

难度:中等

内核版本:linux-6.2.16smap/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_KERNELGFP_KERNEL_ACCOUNT 分配的对象存在隔离,而在漏洞利用中,对于 0x40 大小并且由 GFP_KERNEL 分配的对象并不多,对于常用的可适配的对象 msg_msgpipe_buffer 都是通过 GFP_KERNEL_ACCOUNT 分配的。所以这里我们得先进行 cross cache attack 去突破 GFP_KERNELGFP_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-256pagesperslab 都为 1,所以这里是有很大的机会使得 empty slab 被伙伴系统回收的。

其实上述的 cross cache attack 并不稳定,因为对于 empty slab 的处理并不是简单粗暴的被伙伴系统回收。当然比较稳定的方式请参考 CVE-2022-29582,但对于 CTF 题目环境而言,简单粗暴的方式就够了。

在完成 corss cache attack 后就比较简单了。

overlap object 劫持 pipe_bufferdirty pipe

在上面我们说了 kmalloc-8~kmalloc-256pagesperslab 都为 1[在我本机是这样的,但是在 qemu 虚拟机中, kmalloc-256pagesperslab = 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 利用,所有的 expLKM 可在我的 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-512pagesperslab = 4,而 kmalloc-192pagesperslab = 1 ,然后我尝试利用页级堆风水,但是发现效果还是不好。

再思考:

其实这里我们非得找一个比 vuln obj 更大的对象吗?其实未必,我们需要的其实是一个对象被另一个对象包含,所以考虑如下布局:

是不是很熟悉,之前的 kmalloc-128 double free 就是这个布局,这次漏洞对象转换到了 kmalloc-192,但是我们可以发现这里是有概率发生 overlap object 的,只不过概率变成了 1/3,但是很可惜的是 pipe_buffer 并不适配 kmalloc-128

我们知道 pipe_buffer 只能适配以下大小:

1
64 96 192 512 1024 4096


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-192kmalloc-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 attackkfree 的地址对齐未检查缺陷。

当然 DCO 其实也是可以用于 kmalloc-512 ~ kmalloc-2k 对象 double free 的利用的,但之所以没有将其归纳进 DCO,是因为在没有 cg 隔离时,大对象的 double free 利用 msg_msg + sk_buff 就可以了。当然主要的原因其实是因为大对象 pagesperslab 不同,所以这里得利用页级堆风水去进行页面布局,当然如果你对页级堆风水比较熟悉也是可以的。

比如在 d3ctf d3kheap 这个题目中,作者给了一个 kmalloc-1024double free,且不存在 GFP_KERNELGFP_KERNEL_ACCOUT 之间的隔离,所以可以很方便的利用 msg_msg + sk_buff 进行利用。如果这里想利用 DCO,理论上也是可行的,通过堆风水,可以形成如下布局:

但是 kmalloc-1kpagesperslab = 4,而 kmalloc-4kpagesperslab = 8,所以如果想让最后堆喷 kmalloc-4kpipe_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_KERNELGFP_KERNEL_ACCOUNT 的隔离,可以突破漏洞堆块大小的限制,可以突破特定缓存对象的限制。当然了,据说 ATUOSLAB 可以防止 cross cache attack 的利用,但攻防本是一体,也希望未来能够看到更多的利用方式。

总的来说,目前针对 kmallc-512 ~ kmalloc-2kdouble free 可以利用 msg_msg + sk_buff 完成利用,而针对 kmalloc-8 ~ kmalloc-256double free 可以利用 DCO 完成利用。而针对 kmalloc-192double 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 的堆喷。

出题总结

这是笔者第一次出题,题目都不算难,由于笔者水平原因,感觉这题有点不太好,所以还是决定废弃该题目。

接触 CTF 快一年了,没啥成绩,大三了,准备退坑了,希望师傅们能够学有所成。

最后,我是小周(XiaozaYa),江湖路远,有缘再见......


[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

最后于 2024-3-1 16:56 被XiaozaYa编辑 ,原因:
收藏
点赞0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回