一叶梦花
2025 usenix System Register Hijacking

2025 usenix System Register Hijacking

咕咕咕

1.Instruction

Linux kernel提出了如smep smap kaslr CR-pining 和 NX-Physmap等缓解控制流劫持的保护措施,仍有各种攻击技术(ret2usr ret2dir retspill)被不断发现

几年前,google团队发布了一个覆盖cr4寄存器来禁用smap和smep的漏洞,换种角度来看,内核中的大多数漏洞利用在通过通用寄存器来索引内存或者参数传递与用户空间漏洞没有区别,但是内核有使用特别的东西:系统寄存器

Such as cr寄存器以及EFLAGS(某些值可以关闭smap)这种在内核模式中有特殊含义的寄存器

总之,作者分析了x86_64和aarch64下系统寄存器在控制流劫持下能起到的作用以及展示了7种技术,最后新提出的技术依赖swapgs指令并劫持KERNEL_GSBASE_MSR系统寄存器,可以绕过FinelBT缓解措施(类似CFI,防御ROP和JOP)

本文贡献:

  • 提出System register hijacking
  • 通过poc和cve验证了可行性
  • 提出防护措施和缓解措施

2.Background

2.1 Control Flow Hijacking

一般使用类似pop rsp,val or mov rsp 等gadget将控制流劫持到kernel 堆或者栈的可控区域,然后提权正常返回用户态

2.2 Control Flow Integrity

Linux 内核支持一种基于软件的控制流完整性(CFI)解决方案,称为 kCFI,旨在防止前向控制流劫持。

kCFI 在编译时为函数和间接控制流发生的位置分配标签,然后在调用时比较调用点的标签与目标的标签,以确保目标合法。已有研究发现,一些 kCFI 下的间接控制流并没有验证目标,使得 kCFI 在这方面并不完全有效。目前,kCFI 并未在主流 Linux 桌面和服务器发行版中启用,但在 Android 上是启用的。

现代 Intel x86-64 处理器支持硬件级的 CFI 保护:前向边保护通过 IBT,后向边保护通过 Shadow Stack

  • IBT:endbr64 指令,现代提供更精细的 FineIBT,提供software checks
  • Shadow Stack:一个内存区域去存储原本的栈帧,如果比对失败则报错

2.3 kaslr bypass

由于各种架构的测信道存在,开启与否并无影响,认为与本地攻击场景无关,可以不开启

2.4 SMAP and PAN Bypasses

用户数据不可访问

2.5 Kernel Page Table Isolation

KPTI,简单来说如图所示,内核用户页表分隔

img

2.6 x86-64 FSGSBASE Extension

这个扩展支持了用户态对gsbase和fsbase直接读取和修改的指令

文章引用cve-2019-1125,我印象中这个指令在CTF中也被使用过corCTF 2023: sysruption writeup,而这篇blog作者似乎与论文作者都是同一个学校(笑,orz)

2.7 per-cpu

GSBase → 指向当前 CPU 的 per-CPU 数据块的基地址

per-cpu保存了例如线程栈,task_struct,stack canary

gsbase+offset来访问各个成员

And more,GSBase 寄存器也可用于用户态程序,进入和退出内核时,swapgs 指令用于在用户 gsbase 和内核 gsbase 之间切换

2.8 Privileged Instructions for ROP

内核rop利用,先前通过native_write_cr4的gaget进行关闭smap smep保护,后来CR-Pinning下面添加了检测(如下代码)

但是整个内核中还有额外的没有保护的cr4 gadget可以被拿来利用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void __no_profile native_write_cr4(unsigned long val)
{
unsigned long bits_changed = 0;

set_register:
asm volatile("mov %0,%%cr4" : "+r" (val) : : "memory");

if (static_branch_likely(&cr_pinning)) {
if (unlikely((val & cr4_pinned_mask) != cr4_pinned_bits)) {
bits_changed = (val & cr4_pinned_mask) ^ cr4_pinned_bits;
val = (val & ~cr4_pinned_mask) | cr4_pinned_bits;
goto set_register;
}
/* Warn after we've corrected the changed bits. */
WARN_ONCE(bits_changed, "pinned CR4 bits changed: 0x%lx!?\n",
bits_changed);
}
}

#if IS_MODULE(CONFIG_LKDTM)
EXPORT_SYMBOL_GPL(native_write_cr4);
#endif

3.System register search

在开启了常见保护(SMEP、SMAP、KPTI、NX-physmap、CR Pinning、STATIC_USERMODE_HELPER、RANDKSTACK 和 STACK CANARY)以及CFI的内核中,作者进行了对于system register的gadget查找并对angr做了一定的修改进行了验证

iret和calc对cr4的操作都是无效的,并不能完成关闭smap等行为

img

4.System Register Hijacking Techniques

这里主要讲述了提出的利用手法,仍需结合exp去理解

4.1 x86-64: swapgs Stack Pivoting

swapgs 指令通常出现在内核的入口、退出以及其他中断处理代码中。它会交换两个系统寄存器的值:GSBase 寄存器和 KernelGSBase 寄存器。

  • 在用户态,通过 wrgsbase 指令(用户态可以访问)设置 GSBase,那么当内核入口执行 swapgs 时,KernelGSBase 就会被设置为用户态的那个值(也就是之前wrgsbase写入的值)。

执行 swapgs gadget 的结果是,内核中用于存放 per-cpu 变量的内存指针(即 GSBase 指向的内存)可以被攻击者重定向到自己控制的地址。

有些 swapgs gadget(例如 swapgs; ret)不太实用,因为内核栈保护的 Canary 值存储在 per-cpu 变量中,如果调用者返回时 Canary 校验失败,内核可能会崩溃。

作者发现许多 swapgs gadget 执行后,会把从 per-cpu 变量读取的值写入rsp。通常,内核从用户态进入做这一步骤,以切换到内核栈。

如果劫持执行流程跳转到这些 gadget,就会因为栈指针被设置为攻击者控制的 per-cpu 变量里的值,从而实现栈切换(stack pivot)

要使这种技术生效,攻击者必须能够在内核地址空间中伪造一个 per-CPU 结构,以便 GSBase 可以指向它。 可以通过以下两种方式满足这一条件:

  • 泄露一个可控页面的地址
  • 通过 透明大页(Transparent Huge Page,THP) 喷射来实现(madvise会使内核分配物理内存区域给它,做到了用户页面拥有内核区域映射)

主要问题就在这里,物理直接映射地址不确定,这个地址只能根据大致范围来guess一个

THP 在物理内存中是 2MB 对齐 的,这使用户能够在内核物理内存映射的大范围区域中填充可控页面。 而要在物理内存映射中稳定地猜测可控页面的地址,还依赖于已知内核物理内存映射的基地址,这个基地址可以通过泄露信息或侧信道攻击获得。

为了让该技术成功,伪造的 GSBase 结构必须至少包含:

  • 内核栈指针 pop rsp value
  • task_struct 相关偏移位置上的一个指针

这样才能避免在页错误处理程序(page fault handler)中可能出现的 双重错误(double fault)

作者发现一种稳定使该劫持成功的方法是:

故意触发 代码访问未映射内存,造成页错误;

在页错误处理程序接近中断返回(interrupt return)时覆盖栈(将page_exec_fault指针返回的位置,该位置由per-cpu指引,覆盖成pop_rsp ropchain)ropchain

当页错误处理程序执行时,栈会被劫持,从而导致返回处理程序时被劫持,执行攻击者指定的 ROP 链

该 ROP 链需要:gsbase=我们控制的地址

  • 使用 swapgs; ret gadget 切换回原来的 GSBase 值;
  • 执行权限提升所需的步骤,例如调用 commit_creds(init_cred)
  • 最后使用 iretsysret 将控制流返回到用户态

最后实现的效果是: 劫持rsp,绕过保护,劫持控制流,然后返回到用户空间

Eval-module 具体exp分析

首先spray THP

然后guess一个 Direct Mapping Area地址,wrgsbase赋值给gsbase

此时往每个被喷射的THP page对应per-cpu 偏移处写入值

多线程spawn_orw_thread 根据特征值找到page 写入pop rsp ;ropchain

img

entry_SYSCALL_compat内核入口swapgs后切换为guess地址触发劫持(试了几个entry_SYSCALL,似乎只有这个能满足条件,但论文中也似乎没有说原因)

最后调试时候发现kvm有些情况下不需要hb(疑惑.jpg),然后是最后打断点cli前下断点会调试失败,不理解.jpg

CVE-2023-6111 具体exp分析

Linux 内核的 netfilter: nf_tables 有uaf漏洞

漏洞原因分析

在函数 nft_trans_gc_catchall 中,开发者忘记在参数 sync 为 true 时,从 catchall_list 中移除 catchall set element。

因此,如果你创建一个带有 NFT_SET_EXT_EXPIRATION 的 catchall element在 pipapo set 中,就有可能多次释放同一个 catchall set element(double free)。

相关代码片段:

1
2
3
4
5
6
7
8
void nft_trans_gc_queue_sync_done(struct nft_trans_gc *trans)
{
WARN_ON_ONCE(!lockdep_commit_lock_is_held(trans->net));
if (trans->count == 0) {
nft_trans_gc_destroy(trans);return;
}
call_rcu(&trans->rcu, nft_trans_gc_trans_free);
}
触发漏洞
  1. 创建一个带有 NFT_SET_TIMEOUT 标记的 pipapo set
  2. 插入element A,带有 NFT_SET_ELEM_CATCHALLNFTA_SET_ELEM_TIMEOUTNFTA_SET_ELEM_EXPIRATION
  3. 等待几秒(让element A 超时)
  4. 插入element B,触发调用链: nft_set_commit_update -> nft_pipapo_commit -> pipapo_gc -> nft_trans_gc_queue_sync_done
泄露信息
  1. 创建 pipapo set A,带有 NFT_SET_TIMEOUT 标记
  2. 插入element B(带 NFT_SET_ELEM_CATCHALLNFTA_SET_ELEM_TIMEOUTNFTA_SET_ELEM_EXPIRATION
  3. 插入另一个element,触发漏洞,element B 被释放
  4. 创建许多带 NFTA_TABLE_USERDATA 的表,以重新占用element B 的堆空间(长度应与element B 相同)
  5. 再次插入element,触发漏洞,element B 再次被释放
  6. 创建许多object(大小与element B 相同),其中一个会占用element B 的堆空间
  7. Dump 所有喷射的表,找到占用element B 堆的表,其 NFTA_TABLE_USERDATA 会成为object结构

经典的堆占位

object头部有一个双向链表,指向前后object,可以得到下一个object的名称和指针,可用作 ROP gadget。

步骤:

  • 删除下一个object
  • 再次创建许多带 NFTA_TABLE_USERDATA 的表,占用下一个object的堆空间
RIP

控制 RIP 步骤与泄露信息类似:

  1. 创建 pipapo set A,with flag NFT_SET_TIMEOUT
  2. 插入element B(带 catchall、timeout、expiration)
  3. 插入另一个element,触发漏洞,B 被释放
  4. 创建表,占用 B 的堆空间
  5. 再次触发漏洞,B 被释放
  6. 创建与 B 大小相同的object,其中一个占用 B 的堆空间
  7. Dump 并找到目标表,其 NFTA_TABLE_USERDATA 成为object结构
  8. 删除目标表,释放object堆空间
  9. 喷射许多表,占用object堆空间,填充伪造object数据,覆盖 object->ops 控制 RIP

关键调用点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int nft_object_dump(struct sk_buff *skb, unsigned int attr,
struct nft_object *obj, bool reset)
{
struct nlattr *nest;

nest = nla_nest_start_noflag(skb, attr);
if (!nest)
goto nla_put_failure;

/* 覆盖 ops 后,这里会控制 RIP */
if (obj->ops->dump(skb, obj, reset) < 0)
goto nla_put_failure;

nla_nest_end(skb, nest);
return 0;

nla_put_failure:
return -1;
}

伪造object数据:

1
2
3
4
5
6
7
8
9
// ops 是我们填入 NFTA_OBJ_USERDATA 的指针
// ops+0x20 是 ops->dump
*(uint64_t *)&ops[0x20] = kernel_off + 0xffffffff8198954b; // push rsi ; jmp qword ptr [rsi + 0x39]

// 第一次栈迁移
*(uint64_t *)(&leak_obj[0x39]) = kernel_off + 0xffffffff811b365b; // pop rsp ; ret
*(uint64_t *)(&leak_obj[0]) = kernel_off + 0xffffffff811b365b; // pop rsp ; ret
*(uint64_t *)(&leak_obj[8]) = target_rop + 0x60; // 最终跳转到目标 ROP
*(uint64_t *)(&leak_obj[0x80]) = target_rop; // obj->ops = 我们的目标 ROP

ROP 执行步骤:

  1. obj->ops->dump(skb, obj, reset)
  2. 执行 push rsi ; jmp qword ptr [rsi + 0x39](RSI 是object指针)
  3. 执行 pop rsp ; ret,栈迁移到object指针
  4. 再次 pop rsp ; ret,栈迁移到 target_rop + 0x60(即 NFTA_OBJ_USERDATA + 0x60)
  5. 现在可以执行正常的 ROP 链
How to change to swapgs

其实也没什么好分析的,可以只要覆盖ops函数表的open指针修改为eval_module exp的entry_SYSCALL_compat函数即可,原先是执行rop迁移栈完成控制流劫持

其他spray THP等步骤并无不同

4.x CR0/CR4 popf+retspill IDT

后面这些是一些在aarch or x86_64 gadget应用,其中一些需要stack的控制之类的,算是rop的扩展,感觉并没有产生全新的劫持方法

5.验证poc以及realworld案例

通过人为编写模块来验证,以及对多个cve改写exp来验证是否可行

主要讲述了CVE-2024-26925,CVE-2024-1085

以及对fineIBT的研究,qemu中没法模拟cet,这里也懒得深入探究防护机制了

6.Mitigations

6.1 Mitigating swapgs

建议对来自用户空间的GSbase进行检查,例如执行rdmsr指令来进行

6.2 Mitigating cr0 cr4

给那些没有check的gadget加上check

6.3 Mitigating lidt

《请输入文本》

7.discussion

ropgadget等gadget工具和angrrop各有优劣,一些无法利用的系统寄存器

swapgs办法是通用的,目前手动的工作包括识别per-cpu变量偏移量、要覆盖的伪造stack的偏移量、构建用于权限提升的 ROP 链,以及将控制流目标为其中一个系统调用入口点。

8.Conclusion

完结撒花,非常cool的应用

img

9.open science

“Our artifacts have been made available at https://doi.org/10.5281/zenodo.14728440.”

《感谢x圣开源》orz

10.个人总结

总之,这种利用手法需要劫持控制流,一般是通过劫持返回地址或者劫持某些结构体的ops来实现的调用entry

算是对stack privot这种攻击的补强(很好的补强,让我的漏洞旋转)

先前对于劫持控制流,似乎多是寻找gadget关闭smap并劫持rsp,或者是5.12对pt_reg攻击(后面有随机偏移)

相比data-only攻击还是弱半档的存在

本文作者:一叶梦花
本文链接:http://example.com/2025/08/26/System_registers/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可