《从零手写操作系统 (63):安全与加固——从内核保护到沙箱隔离》

前言:安全是操作系统的底线

前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 沙箱

总结

操作系统安全是纵深防御:

  1. 硬件层:SMEP/SMAP/KPTI阻断侧信道
  2. 编译层:Canary/CFI/KASAN在编译/运行时检测漏洞
  3. 内核层:KASLR/签名验证增加攻击难度
  4. 用户层:seccomp/沙箱最小化攻击面

没有绝对安全,只有不断提高攻击成本。下一章我们将探讨操作系统形式化验证:用数学方法证明内核正确性。

相关推荐
Lzg_na12 分钟前
VXLAN:背景、机制、报文字段与主机转发流程
网络
其实防守也摸鱼18 分钟前
个人学习 Pwn 思路:从基础到漏洞利用的进阶路径
大数据·前端·人工智能·chrome·算法·安全·网络安全
乐维_lwops2 小时前
案例解读:大型综合性科创产业园运维监控落地实践
运维·网络·人工智能·安全·it监控·国产运维智能体
m0_715674434 小时前
政务泛监测体系能力排行:AI 重塑监测边界,治理的颗粒度革命
大数据·网络·人工智能·安全·网络安全·政务
山东科恩光电4 小时前
安全触边在智能制造产线自动化设备防夹伤防护中的选型与应用实践
安全
AI 编程助手GPT4 小时前
curl 返回成功就能收文件?用本地 HTTP 验证错误页与超时
网络·网络协议·http
躺柒5 小时前
读数据安全架构设计与实践20安全政策文件体系(上)
大数据·人工智能·安全·数据安全·安全架构·安全体系
十碗饭吃不饱5 小时前
后端运维:网络联通、进程查询、Java服务启动脚本
java·运维·网络
文谦南宁AI普工6 小时前
CVSS 10.0 沙箱逃逸 + 假扩展后门:给 Agent 开发者的供应链与沙箱安全复盘(附可跑的配置审计器)
安全·agent·ai编程