> For the complete documentation index, see [llms.txt](https://lightc.gitbook.io/pwn-gitbook/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lightc.gitbook.io/pwn-gitbook/kpwn/kpwn-internals/bao-hu-ji-zhi.md).

# 保护机制

## 机制

### KASLR

和普通用户态的ASLR差不多，都是基地址+偏移

在未开启 KASLR 保护机制时

* 内核代码段的基址为 `0xffffffff81000000`&#x20;
* 直接映射区域的基址为 `0xffff888000000000`

### FGKASLR

KASLR的plus版本，**以函数粒度重新排布内核代码** 原来不同的函数会在`.text`一个节上，现在不同的函数在不同的节上

#### ksymtab

`kernel_symbol`结构体其记录了函数的偏移、函数名的偏移以及命名空间的偏移 在使用`fgkalsr`编译后函数重定向通过此结构体

```c
struct kernel_symbol {
    int value_offset;      // 函数的偏移量
    int name_offset;       // 符号名称的偏移量
    int namespace_offset;  // 符号命名空间的偏移量
};
```

利用`kernel_symbol`结构体存储的偏移就能找到具体函数的内存地址 比如

```bash
cat /proc/kallsyms | grep commit_creds
```

有时候内核符号表不会记录`ksymtab`的偏移 `__start___ksymtab`和`__stop___ksymtab` 被记录在`each_symbol_section`函数中 只需要

```bash
cat /proc/kallsyms | grep each_symbols_section
> addr_A

x/10i arrd_A
> ...
> mov rbx,addr_B
> ...

x/10gx addr_B
> addr_C

x/10wx addr_C
> neg_offset

x/10i addr_B + neg_offset - 0x100000000
> addr_offset_function
```

### STACK PROTECTOR

类似于canary，用以检测**是否发生内核堆栈溢出**，通常取自 gs 段寄存器某个固定偏移处的值

### SMAP/SMEP

指管理模式访问保护和管理模式执行保护 **用来防止内核态访问/执行用户态数据**，完全将内核空间与用户空间隔离 **绕过的两种方式:** **篡改CR4寄存器->ret2usr**：CR4寄存器的第20位标识SMEP开关（0关，1开），利用`kernel ROP`篡改`CR4`，然后完成`ret2usr`。 不过现在都是`KPTI`的内核，**内核页面的用户地址没有执行权限**，**ret2usr已经过时**

**ret2dir**：简单说，**把用户地址的数据映射到内核地址空间上**。利用内核线性映射区对物理空间地址的完整映射，可以找到用户空间的数据，但是地址在内核空间上，利用内核地址访问用户的数据

### KPTI

指**内核页表隔离**，内核空间与用户空间使用两组不同的页表集

### 系统调用初始化

[Linux Kernel源码阅读： x86-64 系统调用实现细节（超详细） - 知乎](https://zhuanlan.zhihu.com/p/609245050)

[Linux Kernel 源码学习：PER\_CPU 变量、swapgs及栈切换（一） - 知乎](https://zhuanlan.zhihu.com/p/635938327)

先看比较老的代码

```c
// file: arch/x86/kernel/cpu/common.c
void syscall_init(void)
{
    /*
     * LSTAR and STAR live in a bit strange symbiosis.
     * They both write to the same internal register. STAR allows to
     * set CS/DS but only a 32bit target. LSTAR sets the 64bit rip.
     */
    wrmsrl(MSR_STAR,  ((u64)__USER32_CS)<<48  | ((u64)__KERNEL_CS)<<32);
    wrmsrl(MSR_LSTAR, system_call);
    wrmsrl(MSR_CSTAR, ignore_sysret);

    ......

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

系统启动时会初始化以下型号特定寄存器

* `IA32_STAR`：CS/SS段选择符，决定内核CS/SS
* `IA32_LSTAR`：RIP内核入口，syscall后跳转地址
* `IA32_CSTAR`：32位兼容入口
* `IA32_SYSCALL_MASK`：RFLAGS掩码，内核RFLAGS设置
* `IA32_KERNEL_GS_BASE`：GS基址，内核局部数据访问

#### 段选择子（CS）

[图解CPU的实模式与保护模式 - 小牛呼噜噜 - 博客园](https://www.cnblogs.com/xiaoniuhululu/p/18282033)

最开始是CPU和内存的地址总线宽度为16位，而8086型CPU的地址线有20条，为了增强CPU和内存的寻址能力，发明了段

使用段基址左移加上段内偏移地址得到实际的物理地址，在8086中

```c
addr = segment_base << 4 + offset
```

但是安全问题也很突出，”实模式“（今天的叫法）哪怕引入段后，还是直接操作系统的实际内存，程序之间的地址没有隔离，程序可以访问另一个程序地址，甚至是操作系统的程序地址，所以一不小心就可能直接把操作系统给干废了

**保护模式**概念首次出现于80286，并将以前"老办法"称为**实模式**

> 很快推出的**80386DX**，CPU、寄存器、地址总线都是32位的，寻址空间直接达4GB
>
> 此时无需段的帮助，只需使用偏移地址就可以访问内存中的任一个字节，此时段的机制就多余了，但是为了保持兼容，保留了段的机制，只是将段全部设置为0
>
> 此时在操作系统不在分段（也叫平坦模式）

为了保证程序访问安全的内存，在访问内存时会进行检查，检查段的访问权限、段的长度、段的线性基址、段的特权级等等

于是设置了一个结构体保存这些信息，这个结构体就叫做**段描述符**

结构体如下

```c
struct desc_struct {
	u16                        limit0;               /*     0     2 */
	u16                        base0;                /*     2     2 */
	u16                        base1:8;              /*     4: 0  2 */
	u16                        type:4;               /*     4: 8  2 */
	u16                        s:1;                  /*     4:12  2 */
	u16                        dpl:2;                /*     4:13  2 */
	u16                        p:1;                  /*     4:15  2 */
	u16                        limit1:4;             /*     6: 0  2 */
	u16                        avl:1;                /*     6: 4  2 */
	u16                        l:1;                  /*     6: 5  2 */
	u16                        d:1;                  /*     6: 6  2 */
	u16                        g:1;                  /*     6: 7  2 */
	u16                        base2:8;              /*     6: 8  2 */

	/* size: 8, cachelines: 1, members: 13 */
	/* last cacheline: 8 bytes */
};
```

这样每个段都有自己的段描述符，信息非常庞大，不是寄存器可以保存的，于是操作系统在启动时在内存中开辟了一块空间用于保存段描述符

这就是全局描述表**GDT**，也引入了一个寄存器**GDTR**用于保存**GDT**的地址

有了**GDT**后就可以查表，而关于索引则参考了实模式的设计，使用段寄存器存索引，称为**段选择符/段选择子**

而段寄存器的段选择符是16位的，所传入的段选择符的结构体

```c
u16		RPL:2
u16		T1:1	/* T1 = 0 :GDL ; T1 = 1 :LDT */
u16		index:13
```

使用请求的RPL和段描述符的RPL比较（ring 0 \~ 3），如果**段选择符**的**请求特权级别RPL** 的权限低于**段描述符**的**特权级DPL**时，就会拒绝访问

还有LDT和LDTR，IDT和IDTR，不过多介绍

为了解决内存碎片的问题，引入了页机制，同时兼容段机制形成了独特的段页机制

**段机制实现虚拟地址到线性地址的转换，分页机制实现线性地址到物理地址的转换**

CPU内部有一个控制寄存器**CR3**，存放着当前进程的**页目录表**的物理内存基地址，页目录表存放的是**页表**的物理内存基地址，页表存放的是**页**的物理内存基地址

通过拆分线性地址，查询页表对应的项目得到其真实地址

通过线性地址的**4个9位索引**逐级查找**PML4 -> PDPT -> PD -> PT**，最后加上**12位偏移量**得到物理地址

```c
// file: arch/x86/include/asm/segment.h
#define GDT_ENTRY_KERNEL_CS 2
#define GDT_ENTRY_DEFAULT_USER32_CS 4
#define __USER32_CS   (GDT_ENTRY_DEFAULT_USER32_CS*8+3)
#define __KERNEL_CS (GDT_ENTRY_KERNEL_CS*8)
```

由此可见`kernel cs`为`0x10`，`user cs`为`0x23`

回到关于系统调用初始化的过程

```c
wrmsrl(MSR_STAR,  ((u64)__USER32_CS)<<48  | ((u64)__KERNEL_CS)<<32);
```

将用户态和内核态的段选择子存入`MSR_STAR`寄存器中

```c
wrmsrl(MSR_LSTAR, system_call);
```

将syscall的进入点存入`MSR_LSTAR`

还有

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

`syscall`指令执行时，凡是`MSR_SYSCALL_MASK`中置位的标志位，都会从`EFALGS`中清除

接下来是比较新的内核代码

```c
/* May not be marked __init: used by software suspend */
void syscall_init(void)
{
	/* The default user and kernel segments */
	wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS);

	/*
	 * Except the IA32_STAR MSR, there is NO need to setup SYSCALL and
	 * SYSENTER MSRs for FRED, because FRED uses the ring 3 FRED
	 * entrypoint for SYSCALL and SYSENTER, and ERETU is the only legit
	 * instruction to return to ring 3 (both sysexit and sysret cause
	 * #UD when FRED is enabled).
	 */
	if (!cpu_feature_enabled(X86_FEATURE_FRED))
		idt_syscall_init();
}
#endif /* CONFIG_X86_64 */
```

通过对比新老代码，发现一个`FRED`代替了原来的代码，也就是

```c
static inline void idt_syscall_init(void)
{
	wrmsrq(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);

	if (ia32_enabled()) {
		wrmsrq_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).
		 */
		wrmsrq_safe(MSR_IA32_SYSENTER_CS, (u64)__KERNEL_CS);
		wrmsrq_safe(MSR_IA32_SYSENTER_ESP,
			    (unsigned long)(cpu_entry_stack(smp_processor_id()) + 1));
		wrmsrq_safe(MSR_IA32_SYSENTER_EIP, (u64)entry_SYSENTER_compat);
	} else {
		wrmsrq_cstar((unsigned long)entry_SYSCALL32_ignore);
		wrmsrq_safe(MSR_IA32_SYSENTER_CS, (u64)GDT_ENTRY_INVALID_SEG);
		wrmsrq_safe(MSR_IA32_SYSENTER_ESP, 0ULL);
		wrmsrq_safe(MSR_IA32_SYSENTER_EIP, 0ULL);
	}

	/*
	 * Flags to clear on syscall; clear as much as possible
	 * to minimize user space-kernel interference.
	 */
	wrmsrq(MSR_SYSCALL_MASK,
	       X86_EFLAGS_CF|X86_EFLAGS_PF|X86_EFLAGS_AF|
	       X86_EFLAGS_ZF|X86_EFLAGS_SF|X86_EFLAGS_TF|
	       X86_EFLAGS_IF|X86_EFLAGS_DF|X86_EFLAGS_OF|
	       X86_EFLAGS_IOPL|X86_EFLAGS_NT|X86_EFLAGS_RF|
	       X86_EFLAGS_AC|X86_EFLAGS_ID);
}
```

那么什么是`FRED`呢？AI回答说

> **FRED** 全称是 **Flexible Return and Event Delivery**（灵活返回与事件交付）。
>
> 它是 Intel 提出的一种新的硬件机制，旨在**取代**传统的 x86 中断和系统调用处理流程（如 `INT/IRET`、`SYSCALL/SYSRET`、`SYSENTER/SYSEXIT`），提供更统一、更高效的事件（中断、异常、系统调用）处理流程
>
> 在传统模式下
>
> 从用户态进入内核态（或处理异常/中断）有几种不同的指令（`INT n` / `IRET`：非常慢，因为涉及大量的状态保存和权限检查；`SYSCALL` / `SYSRET`：虽然快，但功能有限），内核必须为不同的入口方式编写不同的汇编代码路径（例如 Linux 中有 `entry_SYSCALL_64`、`entry_INT80_compat`、`entry_SYSENTER_compat` 等），中断、异常、系统调用的进入和返回机制各不相同，导致硬件和软件的处理逻辑碎片化
>
> 在 FRED 模式下：
>
> 引入一套**统一的、基于堆栈的**事件处理机制
>
> 无论是中断、异常还是系统调用（`SYSCALL`、`SYSENTER`、`INT`），CPU 都会跳转到**同一个预定义的内核入口点**（由 FRED 配置寄存器指定）
>
> 内核不再需要为每种事件类型维护独立的汇编入口桩（Stub）与手动保存上下文，FRED 硬件会**自动**将关键的寄存器状态压入内核堆栈
>
> 返回时通过专用的`ERETU/ERETS`自动恢复之前硬件保存的状态并**原子性地**完成返回过程，消除了竞态条件
>
> 此外，SYSRET 和 SYSEXIT 在 FRED 启用时会抛出 #UD（无效指令异常），必须使用 ERETU (Event Return to User) 指令返回用户态

### 系统调用
