Linux与XNU的KPTI实现解读
2021-11-29 14:8:41 Author: mp.weixin.qq.com(查看原文) 阅读量:3 收藏

由于cpu的指令预取存在漏洞,可以从el0直接访问el1内容,主流os对其缓解措施就是限制el0能读取的el1代码范围, 注意程序运行在el0时,并不能直接关闭el1的所有代码访问路径,一个原因是程序运行过程会产生错误,需要el1的异常处理逻辑来接管,另一个原因是程序需要主动使用svc请求el1的系统服务,同样还是会被异常处理逻辑来接管。为此linuxxnu使用了两种不同的方案来实现el1的页表隔离。

linux的做法是在el0 level上提供一个简单的异常处理程序trampline以及新的ttbr1_el1页表,每次从el0进入el1时,切换vbar_el1的地址以及ttbr1_el1的地址为内核使用的异常处理地址和页表地址,每次从el1退回el0时,再次切换vbar_el1trampline的地址以及切换新的ttbr1_el1地址。trampline的代码段很小,只会映射el1异常处理代码的范围,这样新的ttbr_el1的页表占用空间也会非常小。

./arch/arm64/kernel/vmlinux.lds.S

#ifdef CONFIG_UNMAP_KERNEL_AT_EL0

#define TRAMP_TEXT                                      \

        . = ALIGN(PAGE_SIZE);                           \

        __entry_tramp_text_start = .;                   \

        *(.entry.tramp.text)                            \

        . = ALIGN(PAGE_SIZE);                           \

        __entry_tramp_text_end = .;

内核的链接脚本里预先划分出trampline的代码空间。

        idmap_pg_dir = .;

        . += IDMAP_DIR_SIZE;

#ifdef CONFIG_UNMAP_KERNEL_AT_EL0

        tramp_pg_dir = .;

        . += PAGE_SIZE;

#endif

trampline的页表地址紧挨在idmap_pg_dir后面。

./arch/arm64/kernel/entry.S

.pushsection ".entry.tramp.text", "ax"

.macro tramp_map_kernel, tmp

mrs     \tmp, ttbr1_el1

    add     \tmp, \tmp, #(PAGE_SIZE + RESERVED_TTBR0_SIZE)

    bic     \tmp, \tmp, #USER_ASID_FLAG

    msr     ttbr1_el1, \tmp

.endm

.macro tramp_unmap_kernel, tmp

    mrs     \tmp, ttbr1_el1

    sub     \tmp, \tmp, #(PAGE_SIZE + RESERVED_TTBR0_SIZE)

    orr     \tmp, \tmp, #USER_ASID_FLAG

    msr     ttbr1_el1, \tmp

.endm

tramp_map_kernel用来每次进入内核时,更新内核的ttbr1_el1地址。宏tramp_unmap_kernel用来每次退出内核时,切换tramplinettbr_el1地址。

        .macro tramp_ventry, regsize = 64

        .align  7

1:

        .if     \regsize == 64

        msr     tpidrro_el0, x30        // Restored in kernel_ventry

        .endif

        bl      2f

        b       .

2:

tramp_map_kernel        x30

#ifdef CONFIG_RANDOMIZE_BASE

        adr     x30, tramp_vectors + PAGE_SIZE

alternative_insn isb, nop, ARM64_WORKAROUND_QCOM_FALKOR_E1003

        ldr     x30, [x30]

#else

        ldr     x30, =vectors

#endif

        msr     vbar_el1, x30

        add     x30, x30, #(1b - tramp_vectors)

        isb

        ret

.endm

tramp_ventryel1异常处理的入口地址,首先调用了tramp_map_kernel切换内核的ttbr1_el1,紧接着切换vbar_el1为内核的异常处理地址=vectors,所以trampline的作用顾名思义只起到跳转的作用,真正对异常处理的流程还是要通过内核来接管。

        .macro tramp_exit, regsize = 64

        adr     x30, tramp_vectors

        msr     vbar_el1, x30

        tramp_unmap_kernel      x30

        .if     \regsize == 64

        mrs     x30, far_el1

        .endif

        eret

        Sb

        .endm

每次退出异常处理时,首先调用tramp_unmap_kernel更新tramplinettbr1_el1页表地址,然后更新vbar_el1trampline使用的tramp_vectors异常处理入口。

ENTRY(tramp_vectors)

        .space  0x400

        tramp_ventry

        tramp_ventry

        tramp_ventry

        tramp_ventry

        tramp_ventry    32

        tramp_ventry    32

        tramp_ventry    32

        tramp_ventry    32

END(tramp_vectors)

可以看到trampline的异常处理地址,只需填充了0x400偏移,也就是来自el0的同步类型的异常处理,因为trampline的存在仅是为了服务来自el0的异常处理请求。

我们看到linux为了在el0隔离el1的代码,使用了一个新的代码段叫trampline,一个新的页表tramp_pg_dir,每次进入内核与退出内核时都要切换vbar_el1以及ttbr1_el1。这样频繁的切换这两个寄存器,性能肯定会受到影响。而XNU利用了一个更聪明更巧妙的做法来使性能损失达到最小。

XNU通过使tcr.T1SZ字段减去1,使从内核返回用户空间时,内核的虚拟地址空间减半,也就是缩减了内核空间的大小,这样即使el0程序利用cpu漏洞,也只能读取较低的内核地址空间,没有关键的内核数据。每次从用户空间进入内核时,在将tcr.T1SZ字段加1,恢复整个的内核地址空间。

./osfmk/arm64/locore.s

.macro MAP_KERNEL

        mrs             x18, TTBR0_EL1

        orr             x18, x18, #(1 << TTBR_ASID_SHIFT)

        msr             TTBR0_EL1, x18

        MOV64           x18, TCR_EL1_BOOT

        msr             TCR_EL1, x18

        isb             sy

.endmacro

每次进入内核时跟新TCR_EL1

        /* Update TCR to unmap the kernel. */

        MOV64           x18, TCR_EL1_USER

        msr             TCR_EL1, x18

每次从异常处理退出内核时,修改TCR_EL1缩减内核地址空间。

osfmk/arm64/proc_reg.h

#define TCR_EL1_BOOT (TCR_EL1_BASE | (T1SZ_BOOT << TCR_T1SZ_SHIFT) | (TCR_TG0_GRANULE_SIZE))

#define T1SZ_USER (T1SZ_BOOT + 1)

#define TCR_EL1_USER (TCR_EL1_BASE | (T1SZ_USER << TCR_T1SZ_SHIFT) | (TCR_TG0_GRANULE_SIZE))

同样对于在el0时触发的异常处理地址,XNU利用了一个tricky技巧,将内核态的异常处理地址的虚拟地址和在el0时触发的异常处理的虚拟地址,都映射到了内核原有的异常处理地址的物理地址并且el0时触发的异常处理的虚拟地址正好选在了内核地址空间的一半位置处。

./osfmk/arm64/arm_vm_init.c

static void

arm_vm_prepare_kernel_el0_mappings(bool alloc_only)

{

        pt_entry_t pte = 0;

        vm_offset_t start = ((vm_offset_t)&ExceptionVectorsBase) & ~PAGE_MASK;

        vm_offset_t end = (((vm_offset_t)&ExceptionVectorsEnd) + PAGE_MASK) & ~PAGE_MASK;

        vm_offset_t cur = 0;

        vm_offset_t cur_fixed = 0;

        for (cur = start, cur_fixed = ARM_KERNEL_PROTECT_EXCEPTION_START; cur < end; cur += ARM_PGBYTES, cur_fixed += ARM_PGBYTES) {

                if (!alloc_only) {

                        pte = arm_vm_kernel_pte(cur);

                }

                arm_vm_kernel_el1_map(cur_fixed, pte);

                arm_vm_kernel_el0_map(cur_fixed, pte);

        }

        __builtin_arm_dmb(DMB_ISH);

        __builtin_arm_isb(ISB_SY);

        if (!alloc_only) {

                set_vbar_el1(ARM_KERNEL_PROTECT_EXCEPTION_START);

                __builtin_arm_isb(ISB_SY);

        }

}

osfmk/arm/pmap.h

#define ARM_KERNEL_PROTECT_EXCEPTION_START ((~((ARM_TT_ROOT_SIZE + ARM_TT_ROOT_INDEX_MASK) / 2ULL)) + 1ULL)

static void

arm_vm_kernel_el0_map(vm_offset_t vaddr, pt_entry_t pte)

{

        /* Calculate where vaddr will be in the EL1 kernel page tables. */

        vm_offset_t kernel_pmap_vaddr = vaddr - ((ARM_TT_ROOT_INDEX_MASK + ARM_TT_ROOT_SIZE) / 2ULL);

        arm_vm_map(cpu_tte, kernel_pmap_vaddr, pte);

}

注意看arm_vm_map的第一个参数为cpu_tte,也就是内核的页表地址,XNUKPTI使用了内核页表中的一个表项,没有像linux定义了一个全新的页表。所以不在需要切换vbar_el1ttbr1_el1寄存器,性能相比linux会提升很多。


文章来源: https://mp.weixin.qq.com/s?__biz=Mzg4NjU1NDU4MA==&mid=2247483769&idx=1&sn=3f6fae0b609addcd9b0aa5c736eef0e0&chksm=cf96abc2f8e122d481f435c3b814bcb4e48605af62369cd7848702446f8bd02e56f6d06ae6de&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh