# KERNEL\_PWN状态切换原理及KPTI绕过

## 0x0 System call and return method

> 本文不关注系统调用函数的参数，以及返回值，只关注系统调用指令本身。
>
> 这里就拿经典的 int 0x80 与 syscall 来说

### int 0x80

int 0x80是传统的系统调用，它过中断/异常实现，在执行 int 指令时，发生 trap。硬件根据向量号0x80找到在中断描述符表中的表项，在自动切换到内核栈 `(tss.ss0 : tss.esp0)` 后根据中断描述符的 segment selector 在 GDT / LDT 中找到对应的段描述符，从段描述符拿到段的基址，加载到 cs ，将 offset 加载到 eip。最后硬件将用户态`ss / sp / eflags / cs / ip / error code` 依次压到内核栈。然后会执行eip的entry函数，通常在保存一系列寄存器后会`SET_KERNEL_GS`设置内核GS。

返回时，最后会执行SWAPGS交换内核和用户GS寄存器，然后执行iret指令将先前压栈的 `ss / sp / eflags / cs / ip` 弹出，恢复用户态调用时的寄存器上下文。

**总结一下**：在提权时，如要使用64 位的iretq指令 从内核态返回到用户态，我们首先要执行SWAPGS切换GS，然后执行iretq指令时的栈布局应该如下：

```python
rsp ---> rip 
         cs
         rflags
         rsp
         ss
```

### syscall

根据 Intel SDM，[syscall](https://www.felixcloutier.com/x86/syscall.html) 指令执行时会将当前 rip（syscall的下一条指令地址） 存到 rcx ，将 rflags 保存到 r11 中。 然后使用 MSR寄存器中的 `IA32_FMASK`屏蔽 rflags，将 `IA32_LSTAR` 加载到 rip (entry\_SYSCALL\_64)，同时将 `IA32_STAR[47:32]` 加载到 cs，`IA32_STAR[47:32] + 8` 加载到 ss (在 GDT 中，ss 就跟在 cs 后面)。

其中的 MSR `IA32_LSTAR` (MSR\_LSTAR) 和 `IA32_STAR` (MSR\_STAR) 在 `arch/x86/kernel/cpu/common.c` 的 `syscall_init` 中初始化：

```c
void syscall_init(void)
{
    wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS);
    wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);

#ifdef CONFIG_IA32_EMULATION
    wrmsrl(MSR_CSTAR, (unsigned long)entry_SYSCALL_compat);
    /*
     * This only works on Intel CPUs.
     * On AMD CPUs these MSRs are 32-bit, CPU truncates MSR_IA32_SYSENTER_EIP.
     * This does not cause SYSENTER to jump to the wrong location, because
     * AMD doesn't allow SYSENTER in long mode (either 32- or 64-bit).
     */
    wrmsrl_safe(MSR_IA32_SYSENTER_CS, (u64)__KERNEL_CS);
    wrmsrl_safe(MSR_IA32_SYSENTER_ESP, 0ULL);
    wrmsrl_safe(MSR_IA32_SYSENTER_EIP, (u64)entry_SYSENTER_compat);
#else
    wrmsrl(MSR_CSTAR, (unsigned long)ignore_sysret);
    wrmsrl_safe(MSR_IA32_SYSENTER_CS, (u64)GDT_ENTRY_INVALID_SEG);
    wrmsrl_safe(MSR_IA32_SYSENTER_ESP, 0ULL);
    wrmsrl_safe(MSR_IA32_SYSENTER_EIP, 0ULL);
#endif

    /* Flags to clear on syscall */
    wrmsrl(MSR_SYSCALL_MASK,
           X86_EFLAGS_TF|X86_EFLAGS_DF|X86_EFLAGS_IF|
           X86_EFLAGS_IOPL|X86_EFLAGS_AC|X86_EFLAGS_NT);
}
```

可以看到 MSR\_STAR 的第 32-47 位设置为 kernel mode 的 cs，48-63位设置为 user mode 的 cs。而 `IA32_LSTAR` 被设置为函数 `entry_SYSCALL_64` 的起始地址。

于是 syscall 时，跳转到 `entry_SYSCALL_64` 开始执行。

```c
ENTRY(entry_SYSCALL_64)
 /* SWAPGS_UNSAFE_STACK是一个宏，x86直接定义为swapgs指令 */
 SWAPGS_UNSAFE_STACK

 /* 保存栈值，并设置内核栈 */
 movq %rsp, PER_CPU_VAR(rsp_scratch)
 movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp


/* 通过push保存寄存器值，形成一个pt_regs结构 */
/* Construct struct pt_regs on stack */
pushq  $__USER_DS      /* pt_regs->ss */
pushq  PER_CPU_VAR(rsp_scratch)  /* pt_regs->sp */
pushq  %r11             /* pt_regs->flags */
pushq  $__USER_CS      /* pt_regs->cs */
pushq  %rcx             /* pt_regs->ip */
pushq  %rax             /* pt_regs->orig_ax */
pushq  %rdi             /* pt_regs->di */
pushq  %rsi             /* pt_regs->si */
pushq  %rdx             /* pt_regs->dx */
pushq  %rcx tuichu    /* pt_regs->cx */
pushq  $-ENOSYS        /* pt_regs->ax */
pushq  %r8              /* pt_regs->r8 */
pushq  %r9              /* pt_regs->r9 */
pushq  %r10             /* pt_regs->r10 */
pushq  %r11             /* pt_regs->r11 */
sub $(6*8), %rsp      /* pt_regs->bp, bx, r12-15 not saved */

.......
.......
```

首先通过 swapgs 切换 GS 段寄存器，将 GS 寄存器值和一个特定位置的值进行交换，目的是保存 GS 值，同时将该位置的值作为内核执行时的 GS 值使用。

然后将当前栈顶（用户空间栈顶）记录在 CPU 独占变量区域里，将 CPU 独占区域里记录的内核栈顶放入 rsp/esp。

最后通过 push 保存各寄存器值......

syscall里面的细节我们不探究，直接看从syscall返回那部分

```c
    LOCKDEP_SYS_EXIT   // 宏的实现与 CONFIG_DEBUG_LOCK_ALLOC 内核配置选项相关，该配置允许在退出系统调用时调试锁。
    TRACE_IRQS_ON       /* user mode is traced as IRQs on */
    movq    RIP(%rsp), %rcx
    movq    EFLAGS(%rsp), %r11

    RESTORE_C_REGS_EXCEPT_RCX_R11
    // 恢复除 rxc 和 r11 外所有通用寄存器, 因为 rcx 寄存器为调用系统调用的应用程序的返回地址， r11 寄存器为老的 flags register

    /* 根据压栈的内容，恢复 rsp 为用户态的栈顶 */
    movq    RSP(%rsp), %rsp

    USERGS_SYSRET64
   /* 调用宏 USERGS_SYSRET64 ，其扩展调用 swapgs 指令交换用户 GS 和内核GS， sysret 指令执行从系统调用处理退出 */
```

关注一下[sysret](https://www.felixcloutier.com/x86/sysret)指令，它是syscall从内核态返回用户态的伴随指令。执行sysret时，它从rcx加载rip，并从r11加载rflags，从 MSR的 `IA32_STAR[63:48]` 加载CS ，从 `IA32_STAR[63:48] + 8` 加载SS。SYSRET指令不会修改堆栈指针（ESP或RSP），因此在执行SYSRET之前rsp必须切换到用户堆栈，当然还要切换GS寄存器。

**总结一下:** 在提权时，当我们使用sysret指令从内核态中返回前，我们需要先设置rcx为用户态rip，设置r11为用户态rflags，设置rsp为一个用户态堆栈，并执行swapgs交换GS寄存器。

## 0x1 About KPTI

在这之前你需要了解内存分页机制。

### before

每个进程都有一套指向进程自身的页表，由CR3寄存器指向。

早期的Linux内核，每当执行用户空间代码（应用程序）时，Linux会在其分页表中保留整个内核内存的映射(内核地址空间和用户地址空间共用一个页全局目录表PGD)，并保护其访问。这样做的优点是当应用程序向内核发送系统调用或收到中断时，内核页表始终存在，可以避免绝大多数上下文交换相关的开销（TLB刷新、页表交换等）。

尽管阻止了对这些内核映射的访问，但在之后的一段时间，英特尔x86处理器还是被爆出了可用于页表泄露的旁路攻击，可能绕过KASLR.

### kpti

KPTI(Kernel PageTable Isolation)全称内核页表隔离,它通过完全分离用户空间与内核空间页表来解决页表泄露。

KPTI中每个进程有两套页表——内核态页表与用户态页表(两个地址空间)。内核态页表只能在内核态下访问，可以创建到内核和用户的映射（不过用户空间受SMAP和SMEP保护）。用户态页表只包含用户空间。不过由于涉及到上下文切换，所以在用户态页表中必须包含部分内核地址，用来建立到中断入口和出口的映射。

当中断在用户态发生时，就涉及到切换CR3寄存器，从用户态地址空间切换到内核态的地址空间。中断上半部的要求是尽可能的快，从而切换CR3这个操作也要求尽可能的快。为了达到这个目的，KPTI中将内核空间的PGD和用户空间的PGD连续的放置在一个8KB的内存空间中（内核态在低位，用户态在高位）。这段空间必须是8K对齐的，这样将CR3的切换操作转换为将CR3值的第13位(由低到高)的置位或清零操作，提高了CR3切换的速度。

![](/files/-M5ByTbDZIMXqq20ElN1)

开启KPTI后，再想提权就比较有局限性，比如我们常用的ret2usr方式在KPTI下将成为过去时。

### swap CR3

下面我们来看一个开启KPTI内核的`entry_SYSCALL_64`函数

```c
ENTRY(entry_SYSCALL_64)
    /*
     * Interrupts are off on entry.
     * We do not frame this tiny irq-off block with TRACE_IRQS_OFF/ON,
     * it is too small to ever cause noticeable irq latency.
     */
    SWAPGS_UNSAFE_STACK
    // KPTI 进内核态需要切到内核页表
    SWITCH_KERNEL_CR3_NO_STACK
    /*
     * A hypervisor implementation might want to use a label
     * after the swapgs, so that it can do the swapgs
     * for the guest and jump here on syscall.
     */
GLOBAL(entry_SYSCALL_64_after_swapgs)
    // 将用户栈偏移保存到 per-cpu 变量 rsp_scratch 中
    movq    %rsp, PER_CPU_VAR(rsp_scratch)
    // 加载内核栈偏移
    movq    PER_CPU_VAR(cpu_current_top_of_stack), %rsp

    TRACE_IRQS_OFF

    /* Construct struct pt_regs on stack */
    pushq   $__USER_DS          /* pt_regs->ss */
    pushq   PER_CPU_VAR(rsp_scratch)    /* pt_regs->sp */
    pushq   %r11                /* pt_regs->flags */
    pushq   $__USER_CS          /* pt_regs->cs */
    pushq   %rcx                /* pt_regs->ip */
    pushq   %rax                /* pt_regs->orig_ax */
    pushq   %rdi                /* pt_regs->di */
    pushq   %rsi                /* pt_regs->si */
    pushq   %rdx                /* pt_regs->dx */
    pushq   %rcx                /* pt_regs->cx */
    pushq   $-ENOSYS            /* pt_regs->ax */
    pushq   %r8             /* pt_regs->r8 */
    pushq   %r9             /* pt_regs->r9 */
    pushq   %r10                /* pt_regs->r10 */
    pushq   %r11                /* pt_regs->r11 */
    // 为r12-r15, rbp, rbx保留位置
    sub $(6*8), %rsp            /* pt_regs->bp, bx, r12-15 not saved */

    /*
     * If we need to do entry work or if we guess we'll need to do
     * exit work, go straight to the slow path.
     */
    movq    PER_CPU_VAR(current_task), %r11
    testl   $_TIF_WORK_SYSCALL_ENTRY|_TIF_ALLWORK_MASK, TASK_TI_flags(%r11)
    jnz entry_SYSCALL64_slow_path

entry_SYSCALL_64_fastpath:
    /*
     * Easy case: enable interrupts and issue the syscall.  If the syscall
     * needs pt_regs, we'll call a stub that disables interrupts again
     * and jumps to the slow path.
     */
    TRACE_IRQS_ON
    ENABLE_INTERRUPTS(CLBR_NONE)
#if __SYSCALL_MASK == ~0
    // 确保系统调用号没超过最大值，超过了则跳转到后面的符号 1 处进行返回
    cmpq    $__NR_syscall_max, %rax
#else
    andl    $__SYSCALL_MASK, %eax
    cmpl    $__NR_syscall_max, %eax
#endif
    ja  1f              /* return -ENOSYS (already in pt_regs->ax) */
    // 除系统调用外的其他调用都通过 rcx 来传第四个参数，因此将 r10 的内容设置到 rcx
    movq    %r10, %rcx

    /*
     * This call instruction is handled specially in stub_ptregs_64.
     * It might end up jumping to the slow path.  If it jumps, RAX
     * and all argument registers are clobbered.
     */
    // 调用系统调用表中对应的函数
    call    *sys_call_table(, %rax, 8)
.Lentry_SYSCALL_64_after_fastpath_call:
    // 将函数返回值压到栈中，返回时弹出
    movq    %rax, RAX(%rsp)
1:

    /*
     * If we get here, then we know that pt_regs is clean for SYSRET64.
     * If we see that no exit work is required (which we are required
     * to check with IRQs off), then we can go straight to SYSRET64.
     */
    DISABLE_INTERRUPTS(CLBR_NONE)
    TRACE_IRQS_OFF
    movq    PER_CPU_VAR(current_task), %r11
    testl   $_TIF_ALLWORK_MASK, TASK_TI_flags(%r11)
    jnz 1f

    LOCKDEP_SYS_EXIT   // 宏的实现与 CONFIG_DEBUG_LOCK_ALLOC 内核配置选项相关，该配置允许在退出系统调用时调试锁。
    TRACE_IRQS_ON       /* user mode is traced as IRQs on */
    movq    RIP(%rsp), %rcx
    movq    EFLAGS(%rsp), %r11
    RESTORE_C_REGS_EXCEPT_RCX_R11
    // 恢复除 rxc 和 r11 外所有通用寄存器, 因为 rcx 寄存器为调用系统调用的应用程序的返回地址， r11 寄存器为老的 flags register
    /*
     * This opens a window where we have a user CR3, but are
     * running in the kernel.  This makes using the CS
     * register useless for telling whether or not we need to
     * switch CR3 in NMIs.  Normal interrupts are OK because
     * they are off here.
     */
    SWITCH_USER_CR3    // KPTI 返回用户态需要切回用户页表
    /* 根据压栈的内容，恢复 rsp 为用户态的栈顶 */
    movq    RSP(%rsp), %rsp
    USERGS_SYSRET64
   /* 调用宏 USERGS_SYSRET64 ，其扩展调用 swapgs 指令交换用户 GS 和内核GS， sysret 指令执行从系统调用处理退出 */

........
........
```

可以看出，在入口和结束的地方都加了`SWITCH_CR3`相关的宏定义，尝试着分析`SWITCH_KERNEL_CR3_NO_STACK`，里面汇编实现如下：

```python
mov     rdi, cr3
nop
nop
nop
nop
nop
and     rdi, 0xFFFFFFFFFFFFE7FF
mov     cr3, rdi
```

拆分FFFFFFFFFFFFE7FF，它的第12和13位是零，这段代码目的就是将CR3的第12位与第13位置零（页表的第12位在CR4寄存器的PCIDE位未开启的情况下，都是保留给OS留做他用），我们只关心13位置零，就相当于CR3-0x1000，从用户态PGD转换成内核态PGD。

再看`SWITCH_USER_CR3`宏定义的汇编：

```python
mov     rdi, cr3
or      rdi, 1000h
mov     cr3, rdi
```

同理，将CR3第13位置1，相当于CR3+0x1000，从内核态PGD切换成用户态PGD。

## 0x2 Bypass KPTI

在开启KPTI内核，提权返回到用户态（iretq/sysret）之前如果不设置CR3寄存器的值，就会导致进程找不到当前程序的正确页表，引发段错误，程序退出。

知道KPTI原理，在kernel提权返回用户态的时候绕过kpti的话就很简单了，利用内核映像中现有的gadget

```python
mov     rdi, cr3
or      rdi, 1000h
mov     cr3, rdi
```

来设置CR3寄存器，并按照iretq/sysret 的需求构造内容，再返回就OK了。

有一种比较懒惰的方法就是利用`swapgs_restore_regs_and_return_to_usermode`这个函数返回：

可`cat /proc/kallsyms| grep swapgs_restore_regs_and_return_to_usermode`找到它在内核地址

```c
arch/x86/entry/entry_64.S

SYM_INNER_LABEL(swapgs_restore_regs_and_return_to_usermode, SYM_L_GLOBAL)

    POP_REGS pop_rdi=0

    /*
     * The stack is now user RDI, orig_ax, RIP, CS, EFLAGS, RSP, SS.
     * Save old stack pointer and switch to trampoline stack.
     */
    movq    %rsp, %rdi
    movq    PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp

    /* Copy the IRET frame to the trampoline stack. */
    pushq    6*8(%rdi)    /* SS */
    pushq    5*8(%rdi)    /* RSP */
    pushq    4*8(%rdi)    /* EFLAGS */
    pushq    3*8(%rdi)    /* CS */
    pushq    2*8(%rdi)    /* RIP */

    /* Push user RDI on the trampoline stack. */
    pushq    (%rdi)

    /*
     * We are on the trampoline stack.  All regs except RDI are live.
     * We can do future final exit work right here.
     */
    STACKLEAK_ERASE_NOCLOBBER

    SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi

    /* Restore RDI. */
    popq    %rdi
    SWAPGS
    INTERRUPT_RETURN
```

纯汇编代码如下：

```python
swapgs_restore_regs_and_return_to_usermode

.text:FFFFFFFF81600A34 41 5F                          pop     r15
.text:FFFFFFFF81600A36 41 5E                          pop     r14
.text:FFFFFFFF81600A38 41 5D                          pop     r13
.text:FFFFFFFF81600A3A 41 5C                          pop     r12
.text:FFFFFFFF81600A3C 5D                             pop     rbp
.text:FFFFFFFF81600A3D 5B                             pop     rbx
.text:FFFFFFFF81600A3E 41 5B                          pop     r11
.text:FFFFFFFF81600A40 41 5A                          pop     r10
.text:FFFFFFFF81600A42 41 59                          pop     r9
.text:FFFFFFFF81600A44 41 58                          pop     r8
.text:FFFFFFFF81600A46 58                             pop     rax
.text:FFFFFFFF81600A47 59                             pop     rcx
.text:FFFFFFFF81600A48 5A                             pop     rdx
.text:FFFFFFFF81600A49 5E                             pop     rsi
.text:FFFFFFFF81600A4A 48 89 E7                       mov     rdi, rsp    <<<<<<<<<<<<<<<<<<
.text:FFFFFFFF81600A4D 65 48 8B 24 25+                mov     rsp, gs: 0x5004
.text:FFFFFFFF81600A56 FF 77 30                       push    qword ptr [rdi+30h]
.text:FFFFFFFF81600A59 FF 77 28                       push    qword ptr [rdi+28h]
.text:FFFFFFFF81600A5C FF 77 20                       push    qword ptr [rdi+20h]
.text:FFFFFFFF81600A5F FF 77 18                       push    qword ptr [rdi+18h]
.text:FFFFFFFF81600A62 FF 77 10                       push    qword ptr [rdi+10h]
.text:FFFFFFFF81600A65 FF 37                          push    qword ptr [rdi]
.text:FFFFFFFF81600A67 50                             push    rax
.text:FFFFFFFF81600A68 EB 43                          nop
.text:FFFFFFFF81600A6A 0F 20 DF                       mov     rdi, cr3
.text:FFFFFFFF81600A6D EB 34                          jmp     0xFFFFFFFF81600AA3

.text:FFFFFFFF81600AA3 48 81 CF 00 10+                or      rdi, 1000h
.text:FFFFFFFF81600AAA 0F 22 DF                       mov     cr3, rdi
.text:FFFFFFFF81600AAD 58                             pop     rax
.text:FFFFFFFF81600AAE 5F                             pop     rdi
.text:FFFFFFFF81600AAF FF 15 23 65 62+                call    cs: SWAPGS
.text:FFFFFFFF81600AB5 FF 25 15 65 62+                jmp     cs: INTERRUPT_RETURN

_SWAPGS
.text:FFFFFFFF8103EFC0 55                             push    rbp
.text:FFFFFFFF8103EFC1 48 89 E5                       mov     rbp, rsp
.text:FFFFFFFF8103EFC4 0F 01 F8                       swapgs
.text:FFFFFFFF8103EFC7 5D                             pop     rbp
.text:FFFFFFFF8103EFC8 C3                             retn


_INTERRUPT_RETURN
.text:FFFFFFFF81600AE0 F6 44 24 20 04                 test    byte ptr [rsp+0x20], 4
.text:FFFFFFFF81600AE5 75 02                          jnz     native_irq_return_ldt
.text:FFFFFFFF81600AE7 48 CF                          iretq
```

在ROP时，将程序流程控制到 mov rdi, rsp 指令，栈布局如下就行：

```python
rsp  ---->  mov_rdi_rsp
            0
            0
            rip
            cs
            rflags
            rsp
            ss
```

## 0x3 trick

不一定要按照常规方法返回，可以使用`signal(SIGSEGV,shell)`捕获常规方式返回导致的SIGSEGV来get root shell

## 0x3 Reference

[如何检查我的Ubuntu上是否启用了KPTI？ ](https://ubuntuqa.com/article/1242.html)

[【内核漏洞利用】TokyoWesternsCTF-2019-gnote Double-Fetch](https://mp.weixin.qq.com/s?__biz=MzI2ODM4NzUyNQ==\&mid=2247484029\&idx=1\&sn=f6bfec08b43c75ca7af6bd12f4999113\&chksm=eaf117d7dd869ec1eee66d75abac5d38112b7a63c3bf28edc32960c60d74541a75bb2e4dfe9d\&mpshare=1\&scene=23\&srcid=\&sharer_sharetime=1585655592580\&sharer_shareid=85875ea7087d5e89ec671378652f4aac#rd)

[Linux系统调用过程分析](https://zhuanlan.zhihu.com/p/79236207?from_voters_page=true)

[KPTI补丁分析\_运维\_Linux阅码场](https://blog.csdn.net/juS3Ve/article/details/79544927)

[内核页表隔离](https://baike.baidu.com/item/%E5%86%85%E6%A0%B8%E9%A1%B5%E8%A1%A8%E9%9A%94%E7%A6%BB/22785908?fr=aladdin)

<https://www.felixcloutier.com/x86>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://b0ldfrev.gitbook.io/note/linux_kernel/kernelpwn-zhuang-tai-qie-huan-yuan-li-ji-kpti-rao-guo.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
