作者: wzt
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送! 投稿郵箱:paper@seebug.org

概述

由于cpu的指令預取存在漏洞,可以從el0直接訪問el1內容,主流os對其緩解措施就是限制el0能讀取的el1代碼范圍, 注意程序運行在el0時,并不能直接關閉el1的所有代碼訪問路徑,一個原因是程序運行過程會產生錯誤,需要el1的異常處理邏輯來接管,另一個原因是程序需要主動使用svc請求el1的系統服務,同樣還是會被異常處理邏輯來接管。為此linux和xnu使用了兩種不同的方案來實現el1的頁表隔離。

Linux kpti實現

linux的做法是在el0 level上提供一個簡單的異常處理程序trampline以及新的ttbr1_el1頁表,每次從el0進入el1時,切換vbar_el1的地址以及ttbr1_el1的地址為內核使用的異常處理地址和頁表地址,每次從el1退回el0時,再次切換vbar_el1為trampline的地址以及切換新的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用來每次退出內核時,切換trampline的ttbr_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_ventry是el1異常處理的入口地址,首先調用了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更新trampline的ttbr1_el1頁表地址,然后更新vbar_el1為trampline使用的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的異常處理請求。

XNU kpti實現

我們看到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,也就是內核的頁表地址,XNU的KPTI使用了內核頁表中的一個表項,沒有像linux定義了一個全新的頁表。所以不在需要切換vbar_el1ttbr1_el1寄存器,性能相比linux會提升很多。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1770/