前言:安全是操作系统的底线
前62章我们构建了功能完备的操作系统,但功能完备不等于安全。一个生产级内核必须抵御:
- 提权攻击:用户进程获取root权限
- 内存破坏:缓冲区溢出、Use-After-Free
- 侧信道攻击:Spectre、Meltdown
- 恶意代码注入:Rootkit、内核模块攻击
本章从防御纵深角度,逐层构建安全体系。
安全威胁模型
攻击面全景
┌─────────────────────────────────────────────────┐
│ 用户空间 │
│ ├── 缓冲区溢出(栈/堆) │
│ ├── 格式化字符串漏洞 │
│ ├── 整数溢出 │
│ └── 竞争条件(TOCTOU) │
├─────────────────────────────────────────────────┤
│ 系统调用边界 │
│ ├── 参数验证缺失 │
│ ├── 权限检查绕过 │
│ └── 信息泄露 │
├─────────────────────────────────────────────────┤
│ 内核空间 │
│ ├── 内核模块攻击 │
│ ├── 内核ROP/JOP │
│ ├── Use-After-Free(kUAF) │
│ └── 堆溢出(kmalloc/slab) │
├─────────────────────────────────────────────────┤
│ 硬件层 │
│ ├── Spectre(分支预测) │
│ ├── Meltdown(乱序执行) │
│ └── Rowhammer(内存位翻转) │
└─────────────────────────────────────────────────┘
实战代码
内核地址空间布局随机化(KASLR)
KASLR在每次启动时随机化内核基地址,增加攻击者定位内核符号的难度。
// ★ 启动早期:获取随机基地址
// arch/x86/boot/compressed/kaslr.c
#include <linux/efi.h>
#include <asm/boot.h>
// ★ 从硬件RNG或EFI获取熵
static unsigned long get_random_boot_addr(void) {
unsigned long random;
// 尝试从EFI获取随机数
if (efi_get_random_bytes(&random, sizeof(random)) == 0)
return random;
// 回退:使用RDTSC + 内存时序
random = rdtsc();
random ^= *(unsigned long *)0x00000400; // 内存时序熵
random ^= *(unsigned long *)0x00000408;
return random;
}
// ★ 计算随机偏移
unsigned long kaslr_offset(void) {
static unsigned long offset = -1;
if (offset == -1) {
unsigned long random = get_random_boot_addr();
// 对齐到2MB,限制在1GB范围内
offset = (random % (1UL << 30)) & ~((1UL << 21) - 1);
}
return offset;
}
// ★ 重定位内核
void *relocate_kernel(void *kernel_base) {
unsigned long offset = kaslr_offset();
void *new_base = kernel_base + offset;
// 复制内核到新位置
memcpy(new_base, kernel_base, kernel_size);
// 修复重定位表
apply_relocations(new_base, offset);
return new_base;
}
# ★ 验证KASLR:检查内核地址是否随机
import subprocess
def check_kaslr():
"""检查KASLR是否启用"""
with open('/proc/cmdline') as f:
cmdline = f.read()
if 'nokaslr' in cmdline:
print("[!] KASLR disabled")
return False
# 读取内核基地址(需要root)
with open('/proc/kallsyms') as f:
for line in f:
if 'T _stext' in line:
addr = int(line.split()[0], 16)
print(f"[*] Kernel base: 0x{addr:x}")
# 多次重启后地址应不同
return True
return False
SMEP/SMAP:用户-内核隔离
SMEP(Supervisor Mode Execution Prevention)和SMAP(Supervisor Mode Access Prevention)阻止内核执行/访问用户空间内存。
// ★ 启用SMEP/SMAP
// arch/x86/include/asm/special_insns.h
static inline void enable_smep(void) {
unsigned long cr4 = read_cr4();
cr4 |= X86_CR4_SMEP; // Bit 20
write_cr4(cr4);
}
static inline void enable_smap(void) {
unsigned long cr4 = read_cr4();
cr4 |= X86_CR4_SMAP; // Bit 21
write_cr4(cr4);
}
// ★ 内核访问用户空间时必须使用安全接口
// include/linux/uaccess.h
// 从用户空间复制数据(带权限检查)
static inline long copy_from_user(void *to, const void __user *from,
unsigned long n) {
// ★ SMAP会自动阻止直接访问,必须用此接口
if (!access_ok(from, n))
return n; // 地址无效
return __copy_from_user(to, from, n);
}
// 向用户空间复制数据
static inline long copy_to_user(void __user *to, const void *from,
unsigned long n) {
if (!access_ok(to, n))
return n;
return __copy_to_user(to, from, n);
}
// ★ 验证用户指针
static inline bool access_ok(const void __user *addr, unsigned long size) {
// 检查地址是否在用户空间范围内
unsigned long start = (unsigned long)addr;
unsigned long end = start + size;
if (start >= TASK_SIZE_MAX || end > TASK_SIZE_MAX)
return false;
// 检查是否溢出
if (end < start)
return false;
return true;
}
内核栈保护(Stack Canary)
// ★ 编译时插入canary检查
// gcc -fstack-protector-strong
// 函数入口:保存canary
void protected_function(void) {
unsigned long canary = __stack_chk_guard; // 随机值
// ... 函数体 ...
// 函数出口:检查canary
if (__stack_chk_guard != canary)
__stack_chk_fail(); // 检测到溢出,终止
}
// ★ 在内核中实现canary
// arch/x86/kernel/stackprotector.c
unsigned long __stack_chk_guard __read_mostly;
EXPORT_SYMBOL(__stack_chk_guard);
void __stack_chk_fail(void) {
panic("Kernel stack-overflow detected!");
}
EXPORT_SYMBOL(__stack_chk_fail);
// 启动时初始化随机canary
void __init stackprotector_init(void) {
get_random_bytes(&__stack_chk_guard, sizeof(__stack_chk_guard));
// 确保canary不包含0x00(阻止strcpy绕过)
__stack_chk_guard |= 0x0101010101010101UL;
}
KPTI:隔离页表
KPTI(Kernel Page Table Isolation)为内核和用户空间使用不同的页表,彻底阻断Meltdown攻击。
// ★ 每进程维护两套页表
struct mm_struct {
pgd_t *pgd; // 用户页表(不含内核映射)
pgd_t *kernel_pgd; // 内核页表(含完整内核映射)
// ...
};
// ★ 切换页表(用户→内核)
// arch/x86/entry/entry_64.S
.macro SWITCH_TO_KERNEL_CR3 scratch_reg:req
mov %cr3, \scratch_reg
test $PTI_MASK, \scratch_reg // 检查是否已在内核页表
jz .Ldone_\@
// 切换到内核页表
and $~PTI_MASK, \scratch_reg
mov \scratch_reg, %cr3
.Ldone_\@:
.endm
// ★ 切换页表(内核→用户)
.macro SWITCH_TO_USER_CR3 scratch_reg:req
mov %cr3, \scratch_reg
test $PTI_MASK, \scratch_reg // 检查是否已在用户页表
jnz .Ldone_\@
// 切换到用户页表
or $PTI_MASK, \scratch_reg
mov \scratch_reg, %cr3
.Ldone_\@:
.endm
// ★ 系统调用入口
ENTRY(entry_SYSCALL_64)
SWAPGS
SWITCH_TO_KERNEL_CR3 scratch_reg=%rax
// ... 处理系统调用 ...
SWITCH_TO_USER_CR3 scratch_reg=%rax
SWAPGS
SYSRET
END(entry_SYSCALL_64)
控制流完整性(CFI)
CFI防止ROP/JOP攻击,确保间接调用跳转到合法目标。
// ★ 编译时启用CFI:gcc -fsanitize=cfi
// 函数指针类型定义
typedef int (*syscall_fn_t)(unsigned long, unsigned long, unsigned long);
// ★ CFI检查:确保函数指针类型匹配
static syscall_fn_t syscall_table[__NR_syscall_max] = {
[__NR_read] = (syscall_fn_t)sys_read,
[__NR_write] = (syscall_fn_t)sys_write,
// ...
};
// 系统调用分发
long do_syscall(unsigned long nr, unsigned long arg1,
unsigned long arg2, unsigned long arg3) {
if (nr >= __NR_syscall_max)
return -EINVAL;
syscall_fn_t fn = syscall_table[nr];
if (!fn)
return -ENOSYS;
// ★ CFI:编译器自动插入检查
// 确保fn的类型与syscall_fn_t匹配
return fn(arg1, arg2, arg3);
}
// ★ 内核模块CFI
// 模块加载时验证所有导出符号的类型
int module_init_cfi(struct module *mod) {
struct kernel_symbol *sym;
for_each_symbol(sym, mod) {
if (!cfi_check_symbol(sym)) {
pr_err("Module %s: CFI violation on symbol %s\n",
mod->name, sym->name);
return -EINVAL;
}
}
return 0;
}
seccomp:系统调用过滤
seccomp限制进程可使用的系统调用集合,最小化攻击面。
// ★ 用户空间:设置seccomp过滤器
// tools/testing/selftests/seccomp/seccomp_bpf.c
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <linux/audit.h>
// 允许read/write/exit,其他系统调用返回EPERM
int install_seccomp_filter(void) {
struct sock_filter filter[] = {
// 加载系统调用号
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
offsetof(struct seccomp_data, nr)),
// 允许 read
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_read, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
// 允许 write
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_write, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
// 允许 exit/exit_group
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_exit, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_exit_group, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
// 其他:终止进程
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
};
struct sock_fprog prog = {
.len = ARRAY_SIZE(filter),
.filter = filter,
};
// 应用过滤器
return prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);
}
// ★ 内核空间:seccomp检查
// kernel/seccomp.c
long __seccomp_filter(int this_syscall, const struct seccomp_data *sd,
bool recheck_after_trace) {
const struct seccomp_filter *f;
// 遍历过滤器链
for (f = current->seccomp.filter; f; f = f->prev) {
u32 ret = seccomp_run_filters(sd, f);
switch (ret) {
case SECCOMP_RET_ALLOW:
return 0; // 允许
case SECCOMP_RET_KILL:
do_exit(SIGSYS); // 终止
case SECCOMP_RET_ERRNO:
return -SECCOMP_RET_DATA(ret); // 返回错误
case SECCOMP_RET_TRAP:
send_sig(SIGSYS, current, 0); // 发送信号
return -ENOSYS;
case SECCOMP_RET_LOG:
audit_seccomp(this_syscall); // 记录日志
break;
}
}
return -ENOSYS;
}
内核模块签名验证
// ★ 模块加载时验证签名
// kernel/module/signing.c
#include <linux/crypto.h>
#include <keys/system_keyring.h>
// 模块签名结构
struct module_signature {
u8 algo; // 签名算法
u8 hash; // 哈希算法
u8 id_type; // 密钥ID类型
u8 signer_len;// 签名者名称长度
u8 key_id_len;// 密钥ID长度
u8 __pad[3];
__be32 sig_len;// 签名长度(网络字节序)
};
// ★ 验证模块签名
int verify_module_signature(const void *mod, unsigned long modlen) {
const struct module_signature *sig;
const void *sig_data;
unsigned long sig_len;
int ret;
// 定位签名(位于模块末尾)
sig = mod + modlen - sizeof(*sig);
sig_len = be32_to_cpu(sig->sig_len);
sig_data = sig - sig_len;
// ★ 验证签名
ret = verify_pkcs7_signature(sig_data, sig_len,
mod, modlen - sig_len - sizeof(*sig),
VERIFY_USE_SECONDARY_KEYRING,
VERIFYING_MODULE_SIGNATURE,
NULL, NULL);
if (ret) {
pr_err("Module signature verification failed: %d\n", ret);
return -ENOKEY;
}
return 0;
}
// ★ 模块加载入口
static int load_module(struct load_info *info) {
// 1. 验证签名
if (IS_ENABLED(CONFIG_MODULE_SIG)) {
int ret = verify_module_signature(info->hdr, info->len);
if (ret && !module_sig_enforce) {
pr_warn("Loading unsigned module\n");
} else if (ret) {
return ret;
}
}
// 2. 加载模块
// ...
return 0;
}
内存安全:KASAN
KASAN(Kernel Address Sanitizer)检测内核内存错误。
// ★ 编译时启用KASAN:CONFIG_KASAN=y
// 每字节内存对应1字节shadow memory
// shadow值含义:
// 0x00: 完全可访问
// 0x01~0x07: 部分可访问(低N位)
// 0xFE: 栈溢出保护(redzone)
// 0xFD: 释放后内存(kfree后的堆)
// 0xFA: 全局变量溢出保护
// ★ 插桩:内存访问前检查
static inline bool kasan_check_range(unsigned long addr,
unsigned long size, bool is_write) {
const u8 *shadow = (const u8 *)kasan_mem_to_shadow(addr);
u8 shadow_val = *shadow;
if (shadow_val == 0)
return true; // 完全可访问
// 检查部分访问
if (shadow_val >= size)
return false; // 越界
return true;
}
// ★ 检测Use-After-Free
void *kasan_kfree(void *ptr) {
if (!ptr)
return NULL;
// 标记shadow为"已释放"
void *shadow = kasan_mem_to_shadow(ptr);
memset(shadow, KASAN_KFREE, PAGE_SIZE);
// 实际释放
__kfree(ptr);
return ptr;
}
// ★ 检测缓冲区溢出
void kasan_report(unsigned long addr, unsigned long size,
bool is_write, unsigned long ip) {
pr_err("BUG: KASAN: %s in %pS\n",
is_write ? "write" : "read", (void *)ip);
pr_err("Address: %px, size: %lu\n", (void *)addr, size);
dump_stack();
}
沙箱:用户命名空间与cgroups
// ★ 创建沙箱进程
int create_sandbox(pid_t *pid) {
// 使用clone创建隔离的命名空间
*pid = clone(sandbox_main, stack + STACK_SIZE,
CLONE_NEWUSER | // 用户命名空间
CLONE_NEWPID | // PID命名空间
CLONE_NEWNS | // 挂载命名空间
CLONE_NEWNET | // 网络命名空间
CLONE_NEWIPC | // IPC命名空间
CLONE_NEWUTS | // UTS命名空间
SIGCHLD, NULL);
if (*pid < 0) {
perror("clone");
return -1;
}
return 0;
}
int sandbox_main(void *arg) {
// ★ 在沙箱内:root映射为普通用户
// 设置cgroups限制资源
setup_cgroups(getpid(),
.cpu_quota = 50000, // 50% CPU
.memory_limit = 256MB, // 256MB内存
.io_weight = 100); // IO权重
// 挂载受限文件系统
mount("proc", "/proc", "proc", 0, NULL);
mount("tmpfs", "/tmp", "tmpfs", 0, "size=64m");
// 执行沙箱内程序
execl("/bin/sh", "sh", NULL);
return 0;
}
安全加固检查清单
| 层级 | 机制 | 防御目标 |
|---|---|---|
| 启动 | KASLR | 地址随机化 |
| 硬件 | SMEP/SMAP | 用户-内核隔离 |
| 页表 | KPTI | 防Meltdown |
| 编译 | Stack Canary | 栈溢出检测 |
| 编译 | CFI | 防ROP/JOP |
| 运行时 | seccomp | 系统调用最小化 |
| 运行时 | KASAN | 内存错误检测 |
| 模块 | 签名验证 | 防恶意模块 |
| 隔离 | 命名空间+cgroups | 沙箱 |
总结
操作系统安全是纵深防御:
- 硬件层:SMEP/SMAP/KPTI阻断侧信道
- 编译层:Canary/CFI/KASAN在编译/运行时检测漏洞
- 内核层:KASLR/签名验证增加攻击难度
- 用户层:seccomp/沙箱最小化攻击面
没有绝对安全,只有不断提高攻击成本。下一章我们将探讨操作系统形式化验证:用数学方法证明内核正确性。