-
-
[原创] DCO:一种针对小对象 [kmalloc-8~kmalloc-256] double free 的通用利用方式
-
发表于: 2024-3-1 13:53 3551
-
附件地址:https://github.com/XiaozaYa/DCO
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
只能适配以下大小:
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-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
的堆喷。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!