All Articles

SECCON Beginners CTF 2023 Writeup

6/3 から開催されていた SECCON Beginners CTF 2023 に 0nePadding で参加していました。

最終順位は 35 位 / 778 チームでした。

image-20230604164204980

最初の 6 時間くらいは 5 位以内をギリギリキープできていたのですが、途中から解ける問題がなくなり、ずるずると順位を落とした結果 35 位で着地しました。

悔しいところではありますが完全に実力不足ですので精進します。

いつも通り Rev を中心に解きましたが、例年同様 Rev はあっさり全完できたので、今回は Misc や Pwn などにも挑戦してみていました。

今回は Rev と Misc の問題の一部の Writeup を書きます。

Pwn で挑戦していたカーネルエクスプロイトの問題はいい機会でしたので別の記事で詳しく書こうと思っています。

もくじ

Half(Rev)

バイナリファイルってなんのファイルなのか調べてみよう!

あとこのファイルってどうやって中身を見るんだろう…?

ダウンロードしたバイナリを strings で調べると Flag が取得できます。

Three(Rev)

このファイル、中身をちょっと見ただけではフラグは分からないみたい!

バイナリファイルを解析する、専門のツールとか必要かな?

Ghidra でデコンパイルした結果を読むと、3 つのデータ領域内で 4 バイトごとに定義されている値を 1 文字ずつ参照して Flag としていることがわかりました。

そのため、以下の Solver スクリプトで Flag を取得できました。

f1 = [ 0x63, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x75, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x72, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x7d, 0x00, 0x00, 0x00 ]
f2 = [ 0x74, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x79, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x75, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00 ]
f3 = [ 0x66, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, 0x6e, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x6e, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00 ]

flag = ""
for i in range(0x31):
    if i % 3 == 0:
        flag += chr(f1[i//3*4])

    elif i % 3 == 1:
        flag += chr(f2[i//3*4])

    elif i % 3 == 2:
        flag += chr(f3[i//3*4])

print(flag)
# ctf4b{c4n_y0u_ab1e_t0_und0_t4e_t4ree_sp1it_f14g3}

Poker(Rev)

みんなでポーカーで遊ぼう!点数をたくさん獲得するとフラグがもらえるみたい!

でもこのバイナリファイル、動かしてみると…?実行しながら中身が確認できる専門のツールを使ってみよう!

バイナリを実行すると、以下のようにランダムで勝ったり負けたりしてスコアが増減するプログラムであることがわかります。

image-20230604165010482

Ghidra でデコンパイルすると、スコアの合計が現実的に困難なほど大きな値になると Flag を取得する関数が実行されることがわかりました。

gdb などで実行時のメモリを書き換えてもよさそうでしたが、面倒だったので Ghidra で勝利条件のスコアを 0 に書き換えることにしました。

パッチしたバイナリを実行すると、1 勝すると Flag を取得できるようになります。

image-20230604165229548

Leak(Rev)

サーバーから不審な通信を検出しました!

調査したところさらに不審なファイルを発見したので、通信記録と合わせて解析してください。

機密情報が流出してしまったかも…?

与えられた pcap ファイルを参照すると、以下のようなバイト列を C2 に送信していることがわかりました。

これは何かしら暗号化されたバイト列のようなので、Ghidra でプログラムの解析を行います。

0x8e,0x57,0xff,0x59,0x45,0xda,0x90,0x06,0x28,0xb2,0xab,0xfa,0x49,0x73,0x32,0x33,0x4a,0x73,0x29,0x41,0x3c,0x34,0xb7,0xf6,0x62,0x73,0x25,0x0f,0x95,0x40,0x16,0xfa,0x47,0xe9,0x22,0x8d,0xa5,0xcd,0x3d,0x53,0xee,0xb4,0xb3,0x51,0x8e,0xd2,0x89,0x93,0x5b,0xe0,0x59,0xcb,0xfb,0xb1,0x1b

デコンパイルした結果を参照したところ、Flag の暗号化は以下の処理で行っているようでした。

image-20230604165644627

パット見で Flag の文字列とローカル関数内で作成している Key で XOR しているだけであることがわかります。

1 文字ずつ生成されている Key を作成する演算を手動でやるのは少々面倒だったので以下の gdb スクリプトで抽出することにしました。

Key の作成は暗号化対象の文字列には依存しないので、ここで取得した Key はそのまま復号に利用できます。

# gdb -x run.py
import gdb
from pprint import pprint

# pprint(dir(gdb))
BINDIR = "/home/ubuntu/Hacking/CTF/2023/sec4b/Rev/Leak"
BIN = "leak"
INPUT = "./in.txt"
OUT = "./out.txt"
BREAK = "0x555555555518"

gdb.execute('file {}/{}'.format(BINDIR, BIN))
gdb.execute('b *{}'.format(BREAK))
gdb.execute('run < {}'.format(INPUT, OUT))

key = []
while True:
    # register
    reg = int(gdb.parse_and_eval("$rcx"))
    key.append(hex(reg))
    print(key)
    if not reg == "0x29":
        gdb.execute("continue")

# ['0xed', '0x23', '0x99', '0x6d', '0x27', '0xa1', '0xe0', '0x32', '0x51', '0xed', '0xc5', '0xca', '0x16', '0x47', '0x46', '0x47', '0x2f', '0x1d', '0x5d', '0x70', '0xc', '0x5a', '0xe8', '0x82', '0x52', '0x2c', '0x51', '0x3b', '0xf4', '0x34', '0x49', '0x97', '0x73', '0x87', '0x7d', '0xef', '0xc0', '0xa5', '0xc', '0x3d', '0x8a', '0xeb', '0xc7', '0x65', '0xeb', '0x8d', '0xea', '0xe6', '0x29']

これで、特定した Key と pcap から抜き出した暗号化バイト列を XOR することで Flag を取得できました。

key=[0xed, 0x23, 0x99, 0x6d, 0x27, 0xa1, 0xe0, 0x32, 0x51, 0xed, 0xc5, 0xca, 0x16, 0x47, 0x46, 0x47, 0x2f, 0x1d, 0x5d, 0x70, 0xc, 0x5a, 0xe8, 0x82, 0x52, 0x2c, 0x51, 0x3b, 0xf4, 0x34, 0x49, 0x97, 0x73, 0x87, 0x7d, 0xef, 0xc0, 0xa5, 0xc, 0x3d, 0x8a, 0xeb, 0xc7, 0x65, 0xeb, 0x8d, 0xea, 0xe6, 0x29, 0xd4, 0x38, 0xfa, 0x95, 0xcc, 0x11]
enc=[0x8e,0x57,0xff,0x59,0x45,0xda,0x90,0x06,0x28,0xb2,0xab,0xfa,0x49,0x73,0x32,0x33,0x4a,0x73,0x29,0x41,0x3c,0x34,0xb7,0xf6,0x62,0x73,0x25,0x0f,0x95,0x40,0x16,0xfa,0x47,0xe9,0x22,0x8d,0xa5,0xcd,0x3d,0x53,0xee,0xb4,0xb3,0x51,0x8e,0xd2,0x89,0x93,0x5b,0xe0,0x59,0xcb,0xfb,0xb1,0x1b]

for i in range(len(enc)):
    print(chr(key[i]^enc[i]),end="")

# ctf4b{p4y_n0_4ttent10n_t0_t4at_m4n_beh1nd_t4e_cur4a1n}

Heaven(Rev)

メッセージを暗号化するプログラムを作りました。

解読してみてください!

問題バイナリとして、暗号化に使用する ELF ファイルと Flag を暗号化した際の出力結果が与えられます。

$ ./heaven
------ menu ------
0: encrypt message
1: decrypt message
2: exit
> 0
message: ctf4b{---CENSORED---}
encrypted message: ca6ae6e83d63c90bed34a8be8a0bfd3ded34f25034ec508ae8ec0b7f

Ghidra でデコンパイルすると、暗号化を行うための処理は以下のように実装されていることがわかりました。

image-20230604170357860

暗号化を行う対象である message の値が変化していないことから、とりあえず以下の実装以外は無視できることがわかります。

do {
    lVar11 = lVar10 + 1;
    bVar2 = calc_xor(message[lVar10],uVar1);
    message[lVar10] = sbox[bVar2];
    lVar10 = lVar11;
} while (lVar7 != lVar11);

__printf_chk(1,"encrypted message: %02x",local_21);
print_hexdump(message,lVar7);

上記のデコンパイル結果から、以下のような処理で暗号化と出力が行われていることがわかります。

  1. 事前にランダムな 1 バイトの値 uVar1 を生成しておく。
  2. 暗号化対象の文字列 message を先頭から 1 文字ずつ、uVar1 とともに calc_xor に与える。
  3. calc_xor の演算結果を添え字として使用し、sbox にハードコードされている 1 文字を message の文字と置き換える。
  4. 最後に、使用した uVar1 と暗号化した message の Hex 文字列を表示する。

ここで、Flag を暗号化した際に使用された uVar1 は 0xca であることが出力結果から特定できます。

また、sbox のバイト列はハードコードされているので、あとは calc_xor の実装さえ特定すれば Flag を復号できます。

しかし、Ghidra で calc_xor を解析しても、うまく処理を特定することができませんでした。

image-20230604171234531

仕方がないので objdmp でディスアセンブル結果を取得します。

image-20230604171419586

なぜこのようなことをしているのかはよくわからなかったのですが、0x404181 のアドレスから定義されている値をスタックポインタに格納した後、lret で pop させる処理をしているようです。

意図はわかりませんでしたが、gdb で解析すると以下のような処理を行っていることがわかりました。

  1. 受け取った Flag 文字を1 バイト減らす。
  2. [1.] の結果を uVar1 で XOR する。

というわけで、これらをリバースするために以下のスクリプトを作成し、 Flag を取得することができました。

sbox = [ 0xc2, 0x53, 0xbb, 0x80, 0x2e, 0x5f, 0x1e, 0xb5, 0x17, 0x11, 0x00, 0x9e, 0x24, 0xc5, 0xcd, 0xd2, 0x7e, 0x39, 0xc6, 0x1a, 0x41, 0x52, 0xa9, 0x99, 0x03, 0x69, 0x8b, 0x73, 0x6f, 0xa0, 0xf1, 0xd8, 0xf5, 0x43, 0x7d, 0x0e, 0x19, 0x94, 0xb9, 0x36, 0x7b, 0x30, 0x25, 0x18, 0x02, 0xa7, 0xdb, 0xb3, 0x90, 0x98, 0x74, 0xaa, 0xa3, 0x20, 0xea, 0x72, 0xa2, 0x8e, 0x14, 0x5b, 0x23, 0x96, 0x62, 0xa4, 0x46, 0x22, 0x65, 0x7a, 0x08, 0xf6, 0x12, 0xac, 0x44, 0xe9, 0x28, 0x8d, 0xfe, 0x84, 0xc3, 0xe3, 0xfb, 0x15, 0x91, 0x3a, 0x8f, 0x56, 0xeb, 0x33, 0x6d, 0x0a, 0x31, 0x27, 0x54, 0xf9, 0x4a, 0xf3, 0xbf, 0x4b, 0xda, 0x68, 0xa1, 0x3c, 0xff, 0x38, 0xa6, 0x3e, 0xb7, 0xc0, 0x9a, 0x35, 0xca, 0x09, 0xb8, 0x8c, 0xde, 0x1c, 0x0c, 0x32, 0x2a, 0x0f, 0x82, 0xad, 0x64, 0x45, 0x85, 0xd1, 0xaf, 0xd9, 0xfc, 0xb4, 0x29, 0x01, 0x9b, 0x60, 0x75, 0xce, 0x4f, 0xc8, 0xcc, 0xe2, 0xe4, 0xf7, 0xd4, 0x04, 0x67, 0x92, 0xe5, 0xc7, 0x34, 0x0d, 0xf0, 0x93, 0x2c, 0xd5, 0xdd, 0x13, 0x95, 0x81, 0x88, 0x47, 0x9d, 0x0b, 0x1f, 0x5e, 0x5d, 0xa8, 0xe7, 0x05, 0x6a, 0xed, 0x2b, 0x63, 0x2f, 0x4c, 0xcb, 0xe8, 0xc9, 0x5a, 0xdc, 0xc4, 0xb0, 0xe1, 0x7f, 0x9f, 0x06, 0xe6, 0x57, 0xbe, 0xbd, 0xc1, 0xec, 0x59, 0x26, 0xf4, 0xb1, 0x16, 0x86, 0xd7, 0x70, 0x37, 0x4d, 0x71, 0x77, 0xdf, 0xba, 0xf8, 0x3b, 0x55, 0x9c, 0x79, 0x07, 0x83, 0x97, 0xd6, 0x6e, 0x61, 0x1d, 0x1b, 0xa5, 0x40, 0xab, 0xbc, 0x6b, 0x89, 0xae, 0x51, 0x78, 0xb6, 0xb2, 0xfd, 0xfa, 0xd3, 0x87, 0xef, 0xee, 0xe0, 0x2d, 0x4e, 0x3f, 0x6c, 0x66, 0x5c, 0x7c, 0x10, 0xcf, 0x49, 0x48, 0x21, 0x8a, 0x3d, 0xf2, 0x76, 0xd0, 0x42, 0x50, 0x58, 0x00 ]

enc = [0x6a, 0xe6, 0xe8, 0x3d, 0x63, 0xc9, 0x0b, 0xed, 0x34, 0xa8, 0xbe, 0x8a, 0x0b, 0xfd, 0x3d, 0xed, 0x34, 0xf2, 0x50, 0x34, 0xec, 0x50, 0x8a, 0xe8, 0xec, 0x0b, 0x7f]

# unk はランダムな 1 バイト
# bVar1 = calc_xor(message[k],unk);
# message[k] = sbox[bVar1];

# for i in range(256):
flag = ""
for e in enc:
    bVar1 = sbox.index(e)
    flag += chr((bVar1 ^ 0xca) + 1)
print(flag)

YARO(Misc)

サーバーにマルウェアが混入している可能性があるので、あなたの完璧なシグネチャで探してください

比較的珍しい YARA の問題でテンションが上がりました。

サーバには YARA ルールのテキストを送ることができ、root 配下のファイルに対して送信したルールを検証した結果を返してくれます。

このとき、サーバには複数のルールをまとめて送ることができ、マッチしたルールのルール名についても教えてくれます。

シンプルに先頭から 1 文字ずつ特定すればいいことがわかるので、効率的にブルートフォースを行うために Flag の文字種と長さを特定することにしました。

まず、以下のような定義を含むルールを送り付けて Flag の長さを 2 分探索で特定しました。

$ctf4b_string = /.*ctf4b\{.{20,30}\}.*/
$ctf4b_string = /.*ctf4b\{.{25,30}\}.*/
$ctf4b_string = /.*ctf4b\{.{28,30}\}.*/
$ctf4b_string = /.*ctf4b\{.{28}\}.*/

最終的に Flag のかっこ内の長さは 28 文字であることがわかりました。

つづいて、以下のような定義を送り、Flag の文字種を特定しました。

$ctf4b_string = /.*ctf4b\{[0-9|A-Z|a-z]{28}\}.*/
$ctf4b_string = /.*ctf4b\{[0-9|a-z|_]{28}\}.*/
$ctf4b_string = /.*ctf4b\{[A-Z|a-z|_]{28}\}.*/
$ctf4b_string = /.*ctf4b\{[0-9|A-Z|_]{28}\}.*/

大文字、小文字、数字、そしてアンダーバーが使用されていることがわかります。

というわけで、先頭から 1 文字ずつ Flag の文字を特定可能なルールを送信する以下のような Solver スクリプトを作成し、Flag を取得することができました。

from pwn import *

for i in range(28):
    allrule = ""
    p = remote("yaro.beginners.seccon.games", 5003)

    for c in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_":
        c = c
        R = "." * i
        L = 28-1-i
        sig = R + c + "[0-9|A-Z|a-z|_]{" + str(L) +"}"
        if c == "_":
            c = "ubar"
        rule = "rule check_" + c +""" {
    strings:
        $ctf4b_string = /.*ctf4b\{""" + sig + """\}.*/
    condition:
        $ctf4b_string
}
"""
        allrule += rule

    r = p.recvuntil(b"rule:\n")
    p.sendline(allrule.encode())
    r = p.recvuntil(b"Not found: ./server.py\n")
    r = p.recvline()
    print(r)
    p.close()


# b'Found: ./flag.txt, matched: [check_Y]\n'
# b'Found: ./flag.txt, matched: [check_3]\n'
# b'Found: ./flag.txt, matched: [check_t]\n'
# b'Found: ./flag.txt, matched: [check_ubar]\n'
# b'Found: ./flag.txt, matched: [check_A]\n'
# b'Found: ./flag.txt, matched: [check_n]\n'
# b'Found: ./flag.txt, matched: [check_0]\n'
# b'Found: ./flag.txt, matched: [check_t]\n'
# b'Found: ./flag.txt, matched: [check_h]\n'
# b'Found: ./flag.txt, matched: [check_3]\n'
# b'Found: ./flag.txt, matched: [check_r]\n'
# b'Found: ./flag.txt, matched: [check_ubar]\n'
# b'Found: ./flag.txt, matched: [check_R]\n'
# b'Found: ./flag.txt, matched: [check_3]\n'
# b'Found: ./flag.txt, matched: [check_4]\n'
# b'Found: ./flag.txt, matched: [check_d]\n'
# b'Found: ./flag.txt, matched: [check_ubar]\n'
# b'Found: ./flag.txt, matched: [check_O]\n'
# b'Found: ./flag.txt, matched: [check_p]\n'
# b'Found: ./flag.txt, matched: [check_p]\n'
# b'Found: ./flag.txt, matched: [check_0]\n'
# b'Found: ./flag.txt, matched: [check_r]\n'
# b'Found: ./flag.txt, matched: [check_t]\n'
# b'Found: ./flag.txt, matched: [check_u]\n'
# b'Found: ./flag.txt, matched: [check_n]\n'
# b'Found: ./flag.txt, matched: [check_1]\n'
# b'Found: ./flag.txt, matched: [check_t]\n'

# ctf4b{Y3t_An0th3r_R34d_Opp0rtun1ty}

まとめ

上位までの壁の厚さを感じますが、今回も楽しく参加することができました。

0nePadding のチームも少しメンバーが増えて楽しくなってきたところですので、引き続き頑張ります。