学习 Page-Level 堆风水,学习a3大佬的文章 : -)

保护机制

查看 run.sh 脚本内容,发现开启 smep, smap, kpti, kaslr 。

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
qemu-system-x86_64 \
-m 256M \
-cpu kvm64,+smep,+smap \
-smp cores=2,threads=2 \
-kernel bzImage \
-hda ./rootfs.img \
-nographic \
-monitor /dev/null \
-snapshot \
-append "console=ttyS0 root=/dev/sda rw rdinit=/sbin/init kaslr pti=on quiet oops=panic panic=1" \
-no-reboot \
-s

查看 config 文件,常见的保护机制如下,基本上能开的都开了QAQ:

1
2
3
4
5
6
CONFIG_MEMCG_KMEM=y
CONFIG_STATIC_USERMODEHELPER=y
CONFIG_STATIC_USERMODEHELPER_PATH=""
CONFIG_CFI_CLANG=y # clang实现的控制流执行保护,应该是对指针跳转做了加强
CONFIG_SLAB_FREELIST_RANDOM=y
CONFIG_SLAB_FREELIST_HARDENED=y

题目分析

/root/d3kcache.ko定义了字符设备 d3kcache,并注册了 ioctl, read, write, open, release操作。

在 init_module 函数使用 kmem_cache_create_usercopy (kmem_cache_create的内部实现)创建了一个 kmem_cache ,传入的flag参数为:SLAB_HWCACHE_ALIGN | SLAB_PANIC | SLAB_ACCOUNT

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
__int64 init_module()
{
unsigned int v0; // ebx

printk(&unk_96B);
major_num = _register_chrdev(0, 0, 256, "d3kcache", &d3kcache_fo);
if ( major_num >= 0 )
{
module_class = _class_create(&_this_module, "d3kcache", &d3kcache_module_init___key);
if ( (unsigned __int64)module_class < 0xFFFFFFFFFFFFF001LL )
{
printk(&unk_A0D);
v0 = 0;
module_device = device_create(module_class, 0, (unsigned int)(major_num << 20), 0, "d3kcache");
if ( (unsigned __int64)module_device < 0xFFFFFFFFFFFFF001LL )
{
printk(&unk_A66);
spin = 0;
kcache_jar = kmem_cache_create_usercopy("kcache_jar", 2048, 0, 0x4042000, 0, 2048, 0);
memset(&kcache_list, 0, 0x100u);
}
else
{
class_destroy(module_class);
_unregister_chrdev((unsigned int)major_num, 0, 256, "d3kcache");
printk(&unk_A3B);
return (unsigned int)module_device;
}
}
else
{
_unregister_chrdev((unsigned int)major_num, 0, 256, "d3kcache");
printk(&unk_9DE);
return (unsigned int)module_class;
}
}
else
{
printk(&unk_9AD);
return (unsigned int)major_num;
}
return v0;
}

d3kcache_readd3kcache_write函数没有实现功能。

d3kcache_ioctl功能为堆菜单,包括增删改查功能,其中在”改”功能中存在 off-by-null 漏洞。

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
157
158
159
160
161
162
163
164
165
166
167
168
169
__int64 __fastcall d3kcache_ioctl(__int64 a1, int cmd, __int64 arg)
{
__int64 v4; // rax
__int64 v5; // rbx
unsigned int size2; // ecx
__int64 object3; // r14
__int64 size3; // r15
char *data_1; // r12
unsigned int size; // ecx
__int64 size_1; // rbx
__int64 object2; // r14
char *data; // r15
__int64 object; // rax
__int64 object_1; // r15
unsigned int size4; // r13d
__int64 max_size; // r14
char *data_2; // r12
__int64 object_2; // rsi
__int64 idx1; // r14
unsigned __int64 idx2; // rbx
__int64 n2047; // rax
__int64 idx3; // r12
unsigned __int64 idx4; // rbx
void *err_msg; // rdi
request request_data; // [rsp-48h] [rbp-48h] BYREF
unsigned __int64 v28; // [rsp-38h] [rbp-38h]

v28 = __readgsqword(0x28u);
raw_spin_lock(&spin);
v4 = copy_from_user(&request_data, arg, 0x10);
v5 = -1;
if ( v4 )
goto LABEL_2;
if ( cmd > 0x80F )
{
if ( cmd == 0x810 ) // free
{
if ( request_data.index > 0xFuLL || (object_2 = object_list[2 * request_data.index]) == 0 )
{
err_msg = &unk_882;
goto LABEL_46;
}
kmem_cache_free(kcache_jar, object_2);
idx1 = (int)request_data.index;
if ( (unsigned __int64)(int)request_data.index > 0xF )
{
_ubsan_handle_out_of_bounds(&off_12A0, request_data.index);// "/home/arttnba3/Desktop/self_questioning/D3CTF2023/d3kcache/code/d3kcache.c"
idx2 = (int)request_data.index;
object_list[2 * idx1] = 0;
if ( idx2 >= 0x10 )
_ubsan_handle_out_of_bounds(&off_12C0, (unsigned int)idx2);// "/home/arttnba3/Desktop/self_questioning/D3CTF2023/d3kcache/code/d3kcache.c"
}
else
{
object_list[2 * (int)request_data.index] = 0;
idx2 = (unsigned int)idx1;
}
object_size_list[4 * idx2] = 0;
v5 = 0;
}
else
{
if ( cmd != 0x1919 ) // read
goto LABEL_42;
if ( request_data.index > 0xFuLL || !object_list[2 * request_data.index] )
{
err_msg = &unk_85D;
goto LABEL_46;
}
size = request_data.size;
if ( request_data.size > object_size_list[4 * request_data.index] )
size = object_size_list[4 * request_data.index];
if ( (size & 0x80000000) != 0 )
BUG();
size_1 = size;
object2 = object_list[2 * request_data.index];
data = request_data.data;
_check_object_size(object2, size, 1);
v5 = -(__int64)(copy_to_user(data, object2, size_1) != 0);
}
}
else
{
if ( cmd != 0x114 )
{
if ( cmd == 0x514 ) // append
{
if ( request_data.index <= 0xFuLL && object_list[2 * request_data.index] )
{
size2 = request_data.size;
if ( request_data.size > 0x800 || request_data.size + object_size_list[4 * request_data.index] >= 0x800 )
size2 = 0x800 - object_size_list[4 * request_data.index];
if ( (size2 & 0x80000000) != 0 )
BUG();
object3 = object_list[2 * request_data.index] + (unsigned int)object_size_list[4 * request_data.index];
size3 = size2;
data_1 = request_data.data;
_check_object_size(object3, size2, 0);
if ( !copy_from_user(object3, data_1, size3) )
{
*(_BYTE *)(object3 + size3) = 0;
v5 = 0;
}
goto LABEL_2;
}
err_msg = &unk_837;
LABEL_46:
printk(err_msg);
goto LABEL_2;
}
LABEL_42:
err_msg = &unk_8AA;
goto LABEL_46;
}
if ( request_data.index >= 0x10uLL ) // alloc
{
err_msg = &unk_782;
goto LABEL_46;
}
if ( object_list[2 * request_data.index] )
{
err_msg = &unk_7F6;
goto LABEL_46;
}
object = kmem_cache_alloc(kcache_jar, 0xDC0);
if ( !object )
{
err_msg = &unk_81A;
goto LABEL_46;
}
object_1 = object;
size4 = request_data.size;
max_size = 0x800;
if ( request_data.size < 0x800 )
max_size = request_data.size;
data_2 = request_data.data;
_check_object_size(object, max_size, 0);
if ( copy_from_user(object_1, data_2, max_size) )
{
kmem_cache_free(kcache_jar, object_1);
}
else
{
n2047 = 2047;
if ( size4 < 0x7FF )
n2047 = size4;
*(_BYTE *)(object_1 + n2047) = 0;
idx3 = (int)request_data.index;
if ( (unsigned __int64)(int)request_data.index > 0xF )
{
_ubsan_handle_out_of_bounds(&off_1260, request_data.index);// "/home/arttnba3/Desktop/self_questioning/D3CTF2023/d3kcache/code/d3kcache.c"
idx4 = (int)request_data.index;
object_list[2 * idx3] = object_1;
if ( idx4 >= 0x10 )
_ubsan_handle_out_of_bounds(&off_1280, (unsigned int)idx4);// "/home/arttnba3/Desktop/self_questioning/D3CTF2023/d3kcache/code/d3kcache.c"
}
else
{
object_list[2 * (int)request_data.index] = object_1;
idx4 = (unsigned int)idx3;
}
object_size_list[4 * idx4] = max_size;
v5 = 0;
}
}
LABEL_2:
raw_spin_unlock(&spin);
return v5;
}

该菜单维护了一个堆列表,每个object大小不能超过0x800,在 cmd = 0x514时,填满object内容会将末尾设置为0。因此存在漏洞。

漏洞利用

由于 kmem_cache 相对于其他的结构体独立分配,只能使用cross_cache overflow的方式来进行利用。

Step1. 使用 Page-Level 堆风水来实现稳定的 cross_cache overflow

Page-Level Fengshui,在页级构建可控的内存布局。
由 buddy system 分配页面的机制引入。buddy system 分配页面时,其会维护 2 ^ order 大小的内存页,并且同一 order 由链表连接。当对应 order 无法提供页内存时,就会向上级更大的页面请求,拆分为两个对应大小的内存页,一份提供给用户请求,一份提供给其链表。

page.gif

注意到两个页面是物理连续的。因此,我们可以进行如下操作:

  • 请求到两个连续的内存页
  • 释放其中一个,将其堆喷为存在漏洞的kmem_cache,方便后续利用
  • 释放另一个,将其堆喷为要篡改的目标结构体。

现在这样我们就可以进行cross-cache overflow了。
可以通过setsockopt系统调用来申请大量的内存页面。
利用手法对应的内存布局如下:

Step2. Use fcntl(F_SETPIPE_SZ) to extend pipe_buffer, construct page-level UAF

pipe_buffer的第一个对象为page指针,而且page指针大小只有0x40,因此通过 off-by-null 可以将其修改为其他的page指针(75% chance),从而实现两个pipe_buffer指向同一个page,然后就可以释放其中一个,实现Page-Level Use-after-free。
以下面几张图为说明:

然后pipe的函数是支持我们对page进行写入和读取的。不过有一个问题是,pipe_buffer是从kmalloc-cg-1k中分配的,其需要从order-2page中进行申请。而漏洞模块申请的是从order-3page中进行申请,如果在不同order之间进行堆风水,那么成功率会大幅降低。
幸运的是pipe_buffer实际上是一个pipe_buffer数组,其数组元素数目为pipe_bufs

1
2
3
4
5
6
struct pipe_inode_info *alloc_pipe_info(void)
{
//...

pipe->bufs = kcalloc(pipe_bufs, sizeof(struct pipe_buffer),
GFP_KERNEL_ACCOUNT);

我们可以通过fcntl(F_SETPIPE_SZ)来调整pipe_buffer的大小,相当于realloc。

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
long pipe_fcntl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct pipe_inode_info *pipe;
long ret;

pipe = get_pipe_info(file, false);
if (!pipe)
return -EBADF;

__pipe_lock(pipe);

switch (cmd) {
case F_SETPIPE_SZ:
ret = pipe_set_size(pipe, arg);
//...

static long pipe_set_size(struct pipe_inode_info *pipe, unsigned long arg)
{
//...

ret = pipe_resize_ring(pipe, nr_slots);

//...

int pipe_resize_ring(struct pipe_inode_info *pipe, unsigned int nr_slots)
{
struct pipe_buffer *bufs;
unsigned int head, tail, mask, n;

bufs = kcalloc(nr_slots, sizeof(*bufs),
GFP_KERNEL_ACCOUNT | __GFP_NOWARN);

这里,我们可以调整其为64个,从而令其从order-3 page(kmalloc-cg-2k)中申请内存页。

构建 self-writing pipe 来实现任意读写

dirty pipe的pipe primitive参考,修改其splice也中的flag |= PIPE_BUF_FLAG_CAN_MERGE即可。
将UAF的page再次申请到pipe_buffer,然后通过UAF读取page指针的地址,那么我们就可以再通过写来修改二级的page的指针,从而又实现一个uaf:

在二级UAF中,我们释放pipe,触发UAF,然后再让其申请到pipe_buffer,然后我们通过一级UAF,可以读取到二级UAF的page指针,因此我们可以直接修改其再次申请的pipe_buffer内的page指针为二级UAF的page对象,如下:

我们可以通过修改 pipe_buffer.offset 和 pipe_buffer.len 来重定位要读写的位置,但是在读写后这些值会被重新设置。因此我们可以通过上面这样一个结构来重新修改这些变量,这里我们在三级UAFpage中可以有三个pipe指向了UAF的page:

  • 第一个pipe用于进行任意地址读写。
  • 第二个pipe用于修改第三个pipe的start point,从而第三个pipe可以用来修改第一个和第二个pipe变量。
  • 第三个pipe用于修改第一/二个pipe,从而能够循环起来。
    可以实现不限次数的任意地址读写操作。

Step4. 提权

  1. 修改cred结构体
  2. 读取页表解析内核栈物理地址,然后直接写入栈来执行ROP
  3. 读取页表解析内核代码物理地址,然后将其映射到用户空间来覆写内核代码(USMA)

参考链接