..

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,在指定的链表头上,先取 cache kmalloc-cg-32 上取一个管理节点,就叫 node 节点好了,结构体如下。
struct {
    struct list_head l;
    void *data;
}
    • 然后 data 指针指向另外从 cache kmalloc-cg-1k 创建的数据节点,称其为 data 节点,结构体如下:
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,读数据节点。根据给定的 indexkey 去找到数据节点,找到后通过 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 的位置,引用其原话

“应该一下子就可以做出来了”

可惜走错路 + 熟练度不足,这里分享一下预期解法

  1. 通过 0x133c 指令实现对于一个 kmalloc-cg-1k 对象,记为 o1 的恶意释放
  2. 通过 pipe 创建 pipe_buffer 占位该对象,记为 p1,注意 p1 == o1
  3. 再通过 0x133c 指令,释放 p1
  4. 操作该 pipe,如通过 dirty pipe 的 splice 在已经释放的 p1 上写上内核数据
  5. 通过读写能力的对象,去泄露 + 修改 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