RISC-V Linux 上的间接控制流转移跟踪¶
本文档简要描述了 Linux 为用户空间提供的接口,以便在 RISC-V 上为用户模式应用程序启用间接分支跟踪。
1. 特性概述¶
内存损坏问题通常会导致崩溃。然而,在富有创造力的攻击者手中,这些问题可能导致各种安全漏洞。
其中一些安全问题可能是代码重用攻击,攻击者可以利用被破坏的函数指针,将它们串联起来执行跳转面向编程(JOP)或调用面向编程(COP),从而破坏程序的控制流完整性(CFI)。
函数指针存放在可读写内存中,因此容易受到损坏。这可能允许攻击者控制程序计数器(PC)的值。在 RISC-V 上,zicfilp 扩展对这类间接控制流转移施加了限制:
间接控制流转移必须落脚于落脚垫(landing pad)指令
lpad。该规则有两个例外:rs1 = x1 或 rs1 = x5,即从函数中返回。函数返回受影子栈(shadow stack)保护(参见 RISC-V Linux 上的影子栈函数返回保护)
rs1 = x7。在 RISC-V 上,为了调用超出可能 J 类型指令偏移量的函数,编译器通常会执行以下操作:
auipc x7, <imm> jalr (x7)
这种形式的间接控制流转移是不可变的,且不依赖于内存。因此,rs1=x7 免于跟踪,这些被认为是软件保护的跳转。
lpad 指令是 auipc rd, <imm_20bit> 且 rd=x0 的伪指令。这是一个 HINT 操作。lpad 指令必须对齐到 4 字节边界。它将 20 位立即数与 x7 进行比较。如果 imm_20bit == 0,CPU 不会与 x7 进行任何比较。如果 imm_20bit != 0,则 imm_20bit 必须与 x7 匹配,否则 CPU 将引发 软件检查异常(cause=18),其中 *tval = 2。
编译器可以在函数签名上生成哈希值,并在调用点将其(截断为 20 位)设置到 x7 中。函数序言可以编码带有相同函数哈希值的 lpad 指令。这进一步减少了调用点可以到达的有效程序计数器地址的数量。
2. ELF 和 psABI¶
工具链在目标文件的 notes 段中为属性 GNU_PROPERTY_RISCV_FEATURE_1_AND 设置了 GNU_PROPERTY_RISCV_FEATURE_1_FCFI。
3. Linux 启用¶
用户空间程序的地址空间中可能加载了多个共享对象。确保所有依赖项都已在编译时启用间接分支支持是一项困难的任务。因此,这交由动态加载器来为程序启用间接分支跟踪。
4. prctl() 启用¶
可以通过 PR_GET_CFI 和 PR_SET_CFI prctl() 参数(分别对应)来监视和控制每个任务的间接分支跟踪状态,只需将 PR_CFI_BRANCH_LANDING_PADS 作为第二个参数提供即可。这些是架构无关的,如果底层功能不受支持,将返回 -EINVAL。
prctl(
PR_SET_CFI,PR_CFI_BRANCH_LANDING_PADS, unsigned long arg)
arg 是一个位掩码。
如果在 arg 中设置了 PR_CFI_ENABLE,并且 CPU 支持 zicfilp,则内核将为该任务启用间接分支跟踪。一旦动态加载器确定地址空间中加载的所有对象都支持间接分支跟踪,就可以发出此 prctl() 调用。
间接分支跟踪状态在启用后也可以被锁定。这可以防止任务随后将其禁用。这是通过在 arg 中设置 PR_CFI_LOCK 位来完成的。要执行此操作,要么必须已经为该任务启用了间接分支跟踪,要么必须同时在 arg 中设置 PR_CFI_ENABLE 位。这适用于希望以严格的安全态势运行、且不希望加载不支持 zicfilp 的对象的环境。
假设该任务以前未被启用并锁定,也可以为该任务禁用间接分支跟踪。如果对一个未用 zicfilp 编译的对象进行了 dlopen(),动态加载器可以发出将 arg 设置为 PR_CFI_DISABLE 的 prctl() 调用。如果先前已启用并锁定,则无法为该任务禁用间接分支跟踪。
prctl(
PR_GET_CFI,PR_CFI_BRANCH_LANDING_PADS, unsigned long * arg)
将间接分支跟踪的当前状态以位掩码形式返回,并存储在由 arg 指向的内存位置中。如果当前为该任务启用了间接分支跟踪,则该位掩码将设置 PR_CFI_ENABLE 位;如果已被锁定,还将额外设置 PR_CFI_LOCK 位。如果当前为该任务禁用了间接分支跟踪,则将设置 PR_CFI_DISABLE 位。