2023 西湖论剑 Pwn JIT
赛时没出, 复现.
赛时没出, 复现.
一些废话: 比赛时先去看了 V8, 瞄了眼这题没什么想法后又跑去做 misc 了 (逃). 当时以为是什么打解释器的难题就先放了去做 shellgame, 结果啥都没做出来, 喜提零贡献 (逃).
JS 有这几种基本数据类型: Undefined, Null, Boolean, String, Symbol, Number, Object. 数组和函数实际上是 Object. 显然, 最复杂的一种类型就是 对象. 本文主要介绍 Object 在 V8 中的表示.
Object 的本质是一组有序的 属性, 类似于有序字典, 即键值对有序集合. 键可以是非负整数, 也可以是字符串. 键为数字的属性称为 编号属性, 为字符串的称为 命名属性. 比如一个 object = {'x': 5, 1: 6};
. 引用这个属性可以用 .
或者 []
, 如 object.x
, object[1]
.
每个属性都有一系列 属性特性, 它描述了属性的状态, 比如 object.x
的值, 它是否可写, 可枚举等等.
每当创建一个对象时, V8 会在堆上分配一个 JSObject (C++ class), 来表示这个对象:
其中, Map 是用来确定一个 Object 的形状的, Proerties 和 Elements 都是 Object 中的属性. Properties 和 Elements 独立存储, 为两个 FixedArray (V8 定义的 C++ class), 编号属性一般也叫 元素, 他是可以用整数下标来访问的, 一般也就存储在连续的空间中. 而由于动态的原因, 命名属性难以使用固定的下标进行检索. V8 使用 Map Transition 的机制来动态表示命名属性.
和用户态一样, 控持程序流的一个最有效的方法就是覆盖函数指针. 在用户态, 我们可能会去覆盖一些 hook 函数指针, 或者是 FILE vtable 的指针. 在内核态也是一样的. 下面会结合例题来介绍一些可以利用的结构体中的函数指针.
检查启动脚本和 init, 仅开启 smep, kallsyms 可读, 显示 dmesg. 加载了一个 babydriver.ko 到 /dev/babydev
.
检查 ko 的保护, 只开启了 NX. 设备注册了 open, close, read, write, ioctl, open 的时候会 kmalloc 一个 chunk, 指针在全局变量上. ioctl 里可以重新 kmalloc 一个任意大小的 chunk, 覆盖全局变量上的指针. read 和 write 正常读写. close 的时候 kfree, 但是没有将指针置零. 由于指针是在全局变量上, 所以我们打开两次设备, 申请一块我们可以控制大小的空间, 关闭一次设备, 将其释放, 这样没有关闭的那个还可以进行读写操作, 也就是 UAF.
kernel 4.4.72 时, cred 结构体没有专门的一个 kmem_cache (slub allocator) 来分配. struct cred 大小是 0xa8, 会使用 kmalloc-192 这个 kmem_cache 来分配. 而我们知道, cred 中保存了 uid, gid, 等, 如果可以改变这些, 那么就可以成功提权.
专门用来分配以页为单位的大内存空间, 且大小必须是 2 的整数次幂, $2^{order} \times PGSIZE$. 分配时会找 order 对应的块, 如果没有, 则向上找更大的去分割一半, 直到分割出一块 order 对应大小的 (线段树动态开点!). 释放时, 如果可以, 会将相邻的两个合并成一个大的, 一直往上合并.
TODO, 学到再说, 估计学不到 kernel heap 风水了
Slub allocator 最初是 slab allocator, 但是效率不高, 现在普遍是用的一个优化版本 slub allocator. slub allocator 是面向数据对象 (结构体) 的堆分配器, 某个或某些对象 (结构体) 的分配使用对应的某个 slub allocator. 最初 slub allocator 会向 buddy system 申请一个或多个连续页面, 这一整个空间称为一个 slub. 一个 slub 会被划分成多个大小相等的 object, 分配给特定的对象 (结构体) 使用.
slub allocator 的实现在 kernel 里是 kmem_cache
这个结构体, 可以简单认为他是 kmem_cache_cpu
+ kmem_cache_node
. kmem_cache
管理多个 slub, 这些 slub 的 object 大小都相同. cat /proc/slabinfo
可以查看这些 slub allocator. slulb allocator 可以通过 kmem_cache_create()
来创建
在不开 KPTI 的情况下, 每个进程的页表用时有 kernel space 和 user space 的映射, 但是处于用户态时没有权限访问 kernel space. 陷入内核后不需要切换页表和刷新 TLB, 可以避免很大的开销. 不过在 Intel 的 CPU 上, 存在可以侧信道攻击的硬件漏洞 (在 “用户态” 访问内核态?) 于是产生了这种缓解措施: 内核页表隔离.
KPTI 的想法也很简单, bug 无法避免, 那就让 bug 不能被利用. 每个进程维护两个页表, 一个是用户态使用的, 另一个是内核态使用的. 用户态使用的页表不再包含 kernel space 的所有映射, 仅仅包含如用于处理系统调用等必要的部分. 而内核态使用的页表则包含所有 kernel space 的映射, 以及所有 user space 的映射 (要 copy from, copy to). 既然内核态用了另一张页表, 那么显然给 user space 读写权限就够了, 所以即使是代码段 (对应的页), 在内核态的这张表中, 也没有可执行权限.
某天突然想到开了 SMAP 那怎么进行 copy 呢?
在不开启 SMEP, SMAP, KPTI 的情况下, 内核态是可以执行用户空间的代码的. 于是我们只需要在内核态中控制程序流, 使其跳转到用户空间, 执行我们写好的代码, 如 commit_creds(prepare_kernel_cred(NULL))
便可提取.
不过, 最终的目的是起一个 shell, 但是在内核态无法完成这件事, 所以还需要返回用户态.
iretq
是 x64 的中断返回指令. 当发生中断 (系统调用除外) 陷入内核时, 会发生这些事:
返回用户态时, 发生的事情就反了过来:
swapgs
指令恢复用户态 gs 的值iretq
指令, 内核将栈上用户态 rip, cs, rflags, rsp, ss 恢复所以, 我们只需要 swapgs
和 iretq
, 就能够回到用户态. 寄存器的值是否恢复对我们的利用来说并不重要, 不恢复也是可以的.