Walkthrough Solution of kernel-rop from hxpctf 2020
Environment Setup
OS: Ubuntu 20.04 VM
Tools: ROPgadget
You can download the files given for this challenge here.
1 | osboxes@osboxes:~/Desktop/kernel$ ls |
We’re given three files.run.sh
to run the kernel in qemu
vmlinuz
as the kernel imageinitramfs.cpio.gz
as the file system
Looking at run.sh
tells us that we are against a kernel with all protections enabled.
1 |
|
The original code does not have -s
, but I enabled it so it’s easier to debug. -s
opens up port 1234, which we can connect with gdb remote:1234
to debug the kernel.
To summarise:
- KASLR is similar to ASLR in userland, a pain to deal with.
- SMEP does not allow userland pages to be executed while in kernel mode.
- SMAP does not allow userland pages to be accessed(basically blocks all RWX) while in kernel mode.
run.sh
also requires a dummy file.txt to be in place, so we need to create one.
Next let’s extract the filesystem to get the vulnerable kernel module.
1 | osboxes@osboxes:~/Desktop/kernel$ vim run.sh |
hackme.ko
is the kernel module that we need to somehow break, so we should copy it out to analyse.
1 | osboxes@osboxes:~/Desktop/kernel/initramfs/etc/init.d$ cat rcS |
The kernel module will be loaded at /dev/hackme
at runtime.
While we’re at it, let’s also manually give ourselves root privileges to make debugging easy.
1 | osboxes@osboxes:~/Desktop/kernel/initramfs/etc$ cat inittab |
Change the 1000
in inittab
to 0, so we spawn with a root shell.
To commit the changes we have to compress the filesystem back.
I wrote a shell script to do so.
1 |
|
Now if we launch ./run.sh
, we will get a root shell.
Finally, let’s extract the vm image and run ROPgadget
on it to obviously find rop gadgets.
The process will take around 15 minutes because the image is huge.
extract-vmlinux.sh
1 | $ ./extract-image.sh ./vmlinuz > vmlinux |
Reversing
There are 2 main functions, hackme_read
and hackme_write
.
hackme_read
1 | ssize_t __fastcall hackme_read(file *f, char *data, size_t size, loff_t *off) |
hackme_write
1 | ssize_t __fastcall hackme_write(file *f, const char *data, size_t size, loff_t *off) |
The bug occurs as we can read and write up to 4096(0x1000) bytes of data onto a stack buffer tmp
, which has a fixed size of 32*4 which is 128 bytes.
This means that we can easily leak a stack cookie to defeat canary, and also possibly leak other values to defeat KASLR.
Exploitation
First we leak the stack to defeat canary and KASLR.
1 |
|
This code will leak 50 values from the stack. We used an array of unsigned long
because the target system is 64bit, and each value on the stack is 8 bytes.
Compile the code with
1 | gcc -o initramfs/exploit exploit.c -static |
because the target system does not have libc.
Running it should produce an output similar to this.
1 | / $ ./exploit |
Just by looking its obvious that the canary is at index 2 and 16, but you can run it through a debugger to make sure.
We also realise that the target is running FGKASLR
instead of the normal KASLR
, which means that individual functions have their addresses randomized and we cannot defeat FGKASLR
by just leaking one address and finding the offset.
boot 1:
1 | / # cat /proc/kallsyms | grep "T startup_64" |
boot2:
1 | / # cat /proc/kallsyms | grep "T startup_64" |
no fixed offset from base, hence FGKASLR
is on
Defeating FGKASLR
However, there are parts of the image that cannot be randomized by FGKASLR
, so if we find those we can still use a fixed offset to get their addresses.
I wrote a python script to compare the output of /proc/kallsyms
to find the regions that have a fixed offset from the base value.
If we examine the stack leak, we also realise that at index 38 there’s an address that’s not randomized by FGKASLR
, so we can use that to defeat regular KASLR
I also wrote a script to pick out the rop gadgets not affected by FGKASLR
.
find_gadget.py
1 | #!/usr/bin/python3 |
0xffffffff81400dc6
is because the base address shown by ROPgadget
is 0xffffffff81000000
, and the previous script tells us that the text segment with offsets lower than 0x400dc6 is not affected by FGKASLR
Additionally, ksymtab
of important functions like commit_creds
and prepare_kernel_cred
are also not affected by FGKASLR
, so we can use them to locate the addresses of these functions at runtime.
ksymtab
1 | struct kernel_symbol { |
We just have to add the 4 byte value stored at the address of the function’s ksymtab
to the address of that function’s ksymtab
to get the actual address of that function.
We can use these gadgets to achieve an arbitrary read, thus leaking the ksymtab
offset.
1 | //arbitrary read gadgets |
This will allow us to set rax
to an arbitrary address, then move a 4 byte value from that address into eax
.
Finally we can return back to userland via the KPTI trampoline, which is luckily also not affected by FGKASLR
, and read the value from eax
.
This is an example of a leak:
1 | void leak_prep(void) |
The values user_cs
, user_ss
etc are saved values for the iretq
instruction.
We can save these values at the start of the exploit with this function:
1 | unsigned long user_cs, user_ss, user_sp, user_rflags; |
From here onwards it’s just leaking until you get both commit_creds
and prepare_kernel_cred
addresses, then execute prepare_kernel_cred(0)
, bring it back to userland and save in a temporary variable, then feed that variable into commit_creds
.
End
important pointer(no pun intended):
you must immediately call another function upon returning to userland with KPTI trampoline because one of the arbitrary read gadgets messes up the base pointer.
exploit.c
1 |
|
BYEBYE :)