Badlist
错题记忆
北京线下,京麒CTF 2024决赛(10小时)赛制下的内核题,还是很懊悔的,如果做出来就可以上台领奖了。其实吃午饭前就已经找好洞并且 double free 了。但是后续全部时间都在脑浆想实现任意写去写 modprobe_path
,现在回忆起来,脑海里当时就固执的认为这是预期解,明明善良的出题人都过来提示走错路了,仍然是不悔改 QwQ。最终结果是能在本地不开 KASLR 的情况下搜出对应的页框号,但题目环境里就一直搜不到而且读原语也卡住。
赛后复现了预期解法,可以通过控 kmalloc-cg-1k
去实现 dirty pipe 的漏洞利用,从而覆盖 /etc/passwd
进而提权。在此特别感谢出题人 Tplus 师傅(不嫌弃)的指导与提示。
赛题漏洞
题目附件以及代码可以到此处下载:
逆向还是蛮痛苦的,少见的有 6 个指令的漏洞模块,且有几个堆对象。这里简单阐述一下指令逻辑
0x133a
,创建 head 节点,允许存在 16 个链表头节点,节点来自kmalloc-32
的 cache。不同 head 通过 1 字节index
作为区分。0x1337
,在指定的链表头上,先取 cachekmalloc-cg-32
上取一个管理节点,就叫 node 节点好了,结构体如下。
struct {
struct list_head l;
void *data;
}
-
- 然后 data 指针指向另外从 cache
kmalloc-cg-1k
创建的数据节点,称其为 data 节点,结构体如下:
- 然后 data 指针指向另外从 cache
struct {
u16 key;
u16 refcnt;
u8 data[508];
}
-
- 通过
copy_from_user
读取用户数据存放 508 字节。
- 通过
-
- 注意这里的 key 是用户给定的,相当于后续用来索引的值,而 refcnt 在这里初始为 1。
0x1338
,在给定的链表头上删去(第一个找到的)node 以及 data 节点。有趣的是,创建时候使用的 2 字节key
,在这里删除搜索时候,只匹配 1 字节。留意,如果此时相关的 data 节点引用减少为 0,则释放该节点。0x1339
,数据克隆,给定两个链表头,在检查目标头不存在key
标识的数据且源头存在相关数据节点时,会在目标链表插入一个新的 node 节点,指向源的 data 节点,此时增加该 data 节点的引用。0x133b
,摧毁给定链表。非常暴力,直接通过给定index
把整个链表删了。0x133c
,读数据节点。根据给定的index
和key
去找到数据节点,找到后通过copy_to_user
将数据交给用户态。
Task is cheap,封装的交互代码可以由此查看
漏洞方面,最开始是非常怀疑 0x1339
的实现的,毕竟 data 节点实现一个引用计数应该就是为了这种共享数据的情况。从计数的角度来看,如果能一直 clone 一个节点,那么它的引用可以一直向上增加,直到上溢为 0。但具体写交互时候发现,只允许创建 16 个链表头,那么是不具备这么大的上溢条件的。
不过有了这个可溢出的思路后,查看 0x133c
的代码,就能窥见端倪
if ( cmd != 0x133C ) // 指令 0x133c 确认
return -22LL;
if ( ioc_arg.b0 > 0x10u ) // 检查最多 16 个链表
return -22LL;
v14 = head_list[(unsigned __int8)ioc_arg.b0];
if ( !v14 )
return -22LL;
v15 = (struct_list_node *)v14->next;
b3 = ioc_arg.b3;
if ( v14 == v14->next ) // 链表不为空
return -22LL;
while ( 1 ) // 遍历链表
{
++v15->data->ref; // 搜索过程中先加引用
v17 = v15->data;
if ( b3 == HIBYTE(v17->field_0) ) // key 匹配确认
break;
refcnt = v17->ref-- == 1; // 再减去引用
if ( refcnt )
kfree(v17);
v15 = (struct_list_node *)v15->next;
if ( v14 == v15 ) // 是否来到链表尾
return -22LL;
}
if ( !v17 )
return -22LL;
// 成功搜索到目标数据
copy_to_user(ioc_arg.addr, v17->field_4, 508LL);
return 0LL;
没错,代码漏洞在于当给定的 key 匹配目标数据,离开循环调用 copy_to_user
的路径上,没有将错误抬高的引用计数减去进行还原。(要较真的话这里加引用不加锁本身也很奇怪)。
换言之,如果我们持续去使用 133c
指令,就可以不断抬升目标节点的 refcnt
。当抬到 0 时,我们再通过 133c
去搜这个节点后面跟着的数据节点,就可以实现将其异常释放的目的。多跑几次就能实现 double free
预期解法(简)
这里 data 节点出题人特别安排在了 kmalloc-cg-1k
的位置,引用其原话
“应该一下子就可以做出来了”
可惜走错路 + 熟练度不足,这里分享一下预期解法
- 通过
0x133c
指令实现对于一个kmalloc-cg-1k
对象,记为 o1 的恶意释放 - 通过
pipe
创建pipe_buffer
占位该对象,记为 p1,注意 p1 == o1 - 再通过
0x133c
指令,释放 p1 - 操作该 pipe,如通过 dirty pipe 的
splice
在已经释放的 p1 上写上内核数据 - 通过读写能力的对象,去泄露 + 修改 p1 上的数据
- 可以仍然借助
0x133c
指令去读数据,读pipe_buf_operations
完成 KASLR 绕过 - 可以通过
msg_msg
去写数据,写上PIPE_BUF_FLAG_CAN_MERGE
即可实现对只读文件的写操作[^1] [^2]
- 可以仍然借助
其实挺直观的,这里不打算放自己写的丑陋代码。需要稍微留意一点的是:
- 5 中读操作时候,这时候数据节点的
key
已经被page
指针覆盖了,所以这个时候用户是不知道要用什么作为索引才能去读数据的。可行的解决方案(之一)是爆破,可能这也是为啥出题人用 2 字节创建索引但是只用 1 字节去比较,反正 256 种可能都读出来。
其他部分的话就是不断的调试了~Happy Debugging
BONUS
唯一解出来的 Loτυs 师傅分享说除去写 /etc/passwd
外,还可以考虑写 busybox 为一个 ORW flag 文件的程序,由于这个题的 init 是带 poweroff -f
的。所以覆盖完 busybox 退出后,具有 root 权限的 init 进程便会把 flag 打出来,确实涨知识。
Anyhow,通过这个题,以后可以留意一下是否提供了 su
能力,以及 init
后有没有可以可以骚操作的余地了。
[^1] Dirty Pipe, LINK
[^2] Dirty Pipe 的利用拓展,LINK