Home TFC CTF 2022 writeup
Post
Cancel

TFC CTF 2022 writeup

息抜きに… cryptoctfが重すぎた…

てか、そろそろfirst bloodをとりたいすね…

EXCLUSIVE_ORACLE [75 solve]

繋いで適当な値を入れまくってみると、返り値が80文字以上増えなくなる。 よって、flagは半分の40文字と推測でき、入力値が40文字で、flagが40文字で共通鍵暗号的に何ができるかというとxorかなぁと… keyを復元し、flagも復元するとフラグが取れる

いやこの問題辛くない????え、そうでもない、、そうですか。。。

solve

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
from Crypto.Util.number import *



io = remote( "01.linux.challenges.ctf.thefewchosen.com" ,54784)
io.recvuntil(b"> ")
io.sendline(b"1"*40)
ret = eval(io.recvline(None).decode())

key = bytes_to_long(ret[:40])^bytes_to_long(b"1"*40)
print(long_to_bytes(bytes_to_long(ret[40:])^key))
#TFCCTF{wh4t's_th3_w0rld_w1th0u7_3n1gm4?}

TRAIN_TO_PADDINGTON [132 solves]

chall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import os

BLOCK_SIZE = 16
FLAG = b'|||REDACTED|||'


def pad_pt(pt):
    amount_padding = 16 if (16 - len(pt) % 16) == 0 else 16 - len(pt) % 16
    return pt + (b'\x3f' * amount_padding)


pt = pad_pt(FLAG)
key = os.urandom(BLOCK_SIZE)

ct = b''

j = 0
for i in range(len(pt)):
    ct += (key[j] ^ pt[i]).to_bytes(1, 'big')
    j += 1
    j %= 16

with open('output.txt', 'w') as f:
    f.write(ct.hex())

solve

まあ普通のxorの問題で、先頭と後ろでそれぞれ TFCCTF{ と\x3f*16 でxor取って出てきた値が被っていれば簡単に復元できるから被りがいいなぁとか思っていたら普通に被ってたので簡単にkeyが手に入った。このkeyを使って復元しておしまい!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from Crypto.Util.number import *
from Crypto.Util.strxor import strxor

ct = bytes.fromhex("b4b55c3ee34fac488ebeda573ab1f974bf9b2b0ee865e45a92d2f14b7bdabb6ed4872e4dd974e803d9b2ba1c77baf725")

BLOCK_SIZE = 16

cts = []
for i in range(len(ct)//16):
    cts.append(ct[i*16:(i+1)*16])

flag = b"TFCCTF{"
len = len(flag)
key = strxor(cts[0][:len], flag)
key += strxor(cts[2], b"\x3f"*16)[7:]

flag = b""
for i in range(3):
    flag += strxor(cts[i], key)
print(flag)

# TFCCTF{th3_tr41n_h4s_l3ft_th3_st4t10n}

ADMIN PANEL

chall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import os
import random

from Crypto.Cipher import AES

KEY = os.urandom(16)
PASSWORD = os.urandom(16)
FLAG = os.getenv('FLAG')

menu = """========================
1. Access Flag
2. Change Password
========================"""


def xor(byte, bytes_second):
    d = b''
    for i in range(len(bytes_second)):
        d += bytes([byte ^ bytes_second[i]])
    return d


def decrypt(ciphertext):
    iv = ciphertext[:16]
    ct = ciphertext[16:]
    cipher = AES.new(KEY, AES.MODE_ECB)
    pt = b''
    state = iv
    for i in range(len(ct)):
        b = cipher.encrypt(state)[0]
        c = b ^ ct[i]
        pt += bytes([c])
        print(b,c,pt)
        state = state[1:] + bytes([ct[i]])
    return pt


if __name__ == "__main__":
    while True:
        print(menu)
        option = int(input("> "))
        if option == 1:
            password = bytes.fromhex(input("Password > "))
            if password == PASSWORD:
                print(FLAG)
                exit(0)
            else:
                print("Wrong password!")
                continue
        elif option == 2:
            token = input("Token > ")
            if len(token) != 64:
                print("Wrong length!")
                continue
            hex_token = bytes.fromhex(token)
            # r_byte = random.randbytes(1)
            r_byte = os.urandom(1)
            print(f"XORing with: {r_byte.hex()}")
            xorred = xor(r_byte[0], hex_token)
            PASSWORD = decrypt(xorred)

solve

初手適当に手元で2*64代入したら初めのバイト以外全てのバイト列が同じものが出てきた… 何も考えずにAccess Flagで初めの値のブルートフォースで終わり。

はい、ちゃんとやります。 decrypt関数において1回目のAESでの平文にあたるものはciphertext[:16]で、2回目のAESでの平文にあたるものはciphertext[1:17]とi回目の平文はciphertext[i-1:15+i]となる。今回はこれの値を一定にすると\(c\)の値も一定になりすごーくうれしい(PASSWORDの文字列が一定の文字種が制限できる)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
from Crypto.Util.number import *
from tqdm import tqdm

# io = process("./main.py")
io = remote("01.linux.challenges.ctf.thefewchosen.com", 54928)

io.recvuntil(b"> ")
io.sendline(b"2")
io.recvuntil(b"> ")
io.sendline(b"2"*64)


for i in tqdm(range(256)):
    io.recvuntil(b"> ")
    io.sendline(b"1")
    # print(io.recvuntil(b"> "))
    # print((long_to_bytes(1)*16).hex())
    io.sendline((long_to_bytes(i)*16).hex().encode())
    tmp = io.recvline()
    if b"{" in tmp:
        print(tmp)
        exit()
# TFCCTF{l0g0n_z3r0_w1th_3xtr4_st3ps!}

ADMIN PANEL BUT HARDER [78 solves]

chall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import os
import random

from Crypto.Cipher import AES

KEY = os.urandom(16)
PASSWORD = os.urandom(16)
FLAG = os.getenv('FLAG')

menu = """========================
1. Access Flag
2. Change Password
========================"""


def xor(bytes_first, bytes_second):
    d = b''
    for i in range(len(bytes_second)):
        d += bytes([bytes_first[i] ^ bytes_second[i]])
    return d


def decrypt(ciphertext):
    iv = ciphertext[:16]
    ct = ciphertext[16:]
    cipher = AES.new(KEY, AES.MODE_ECB)
    pt = b''
    state = iv
    for i in range(len(ct)):
        b = cipher.encrypt(state)[0]
        c = b ^ ct[i]
        pt += bytes([c])
        state = state[1:] + bytes([ct[i]])
    return pt


if __name__ == "__main__":
    while True:
        print(menu)
        option = int(input("> "))
        if option == 1:
            password = bytes.fromhex(input("Password > "))
            if password == PASSWORD:
                print(FLAG)
                exit(0)
            else:
                print("Wrong password!")
                continue
        elif option == 2:
            token = input("Token > ")
            if len(token) != 64:
                print("Wrong length!")
                continue
            hex_token = bytes.fromhex(token)
            r_bytes = random.randbytes(32)
            print(f"XORing with: {r_bytes.hex()}")
            xorred = xor(r_bytes, hex_token)
            PASSWORD = decrypt(xorred)

solve

ADMIN PANELから変化した部分はChange Passwordの乱数の部分が増加した。これでは一定値に定めるのが極めてしんどい。。 なので、Change Passwordで乱数の値が返ってくることを利用して乱数予測を行う! 帰ってくる乱数は32bytesより約80回程度集めれば乱数予測が可能となる。あとはADMIN PANELと同じ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import random
from mt19937predictor import MT19937Predictor
from pwn import *
from Crypto.Util.number import *
from tqdm import tqdm

# io = process("./main.py")
io = remote("01.linux.challenges.ctf.thefewchosen.com", 55172)

def xor(bytes_first, bytes_second):
    d = b''
    for i in range(len(bytes_second)):
        d += bytes([bytes_first[i] ^ bytes_second[i]])
    return d


predictor = MT19937Predictor()
for _ in tqdm(range(120)):
    io.recvuntil(b"> ")
    io.sendline(b"2")
    io.recvuntil(b"> ")
    io.sendline(b"2"*64)
    x = bytes.fromhex(io.recvline(None).decode().split(": ")[1])
    # print(x)
    predictor.setrandbits(int.from_bytes(x, byteorder='little'), 8*32)

next_rnd = predictor.getrandbits(8*32).to_bytes(32, 'little')
io.recvuntil(b"> ")
io.sendline(b"2")
io.recvuntil(b"> ")
msg = xor(b"2"*64 ,next_rnd)
io.sendline(msg.hex().encode())
ret = bytes.fromhex(io.recvline(None).decode().split(": ")[1])
# print(ret,next_rnd, long_to_bytes(tst))
assert ret== next_rnd
print("[+] YES!!!!! find seed ")


# part2 
for i in tqdm(range(256)):
    io.recvuntil(b"> ")
    io.sendline(b"1")
    # print(io.recvuntil(b"> "))
    # print((long_to_bytes(1)*16).hex())
    io.sendline((long_to_bytes(i)*16).hex().encode())
    tmp = io.recvline()
    if b"{" in tmp:
        print(tmp)
        exit()
# TFCCTF{n0_th3_fl4g_1s_n0t_th3_0ld_0n3_plus-Th3-w0rd_h4rd3r!}

ADMIN PANEL BUT HARDER FIXED [50 solves ・56 solves]

chall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import os
import random

from Crypto.Cipher import AES

KEY = os.urandom(16)
PASSWORD = os.urandom(16)
FLAG = os.getenv('FLAG')

menu = """========================
1. Access Flag
2. Change Password
========================"""


def xor(bytes_first, bytes_second):
    d = b''
    for i in range(len(bytes_second)):
        d += bytes([bytes_first[i] ^ bytes_second[i]])
    return d


def decrypt(ciphertext):
    iv = ciphertext[:16]
    ct = ciphertext[16:]
    cipher = AES.new(KEY, AES.MODE_ECB)
    pt = b''
    state = iv
    for i in range(len(ct)):
        b = cipher.encrypt(state)[0]
        c = b ^ ct[i]
        pt += bytes([c])
        state = state[1:] + bytes([ct[i]])
    return pt


if __name__ == "__main__":
    while True:
        print(menu)
        option = int(input("> "))
        if option == 1:
            password = bytes.fromhex(input("Password > "))
            if password == PASSWORD:
                print(FLAG)
                exit(0)
            else:
                print("Wrong password!")
                continue
        elif option == 2:
            token = input("Token > ").strip()
            if len(token) != 64:
                print("Wrong length!")
                continue
            hex_token = bytes.fromhex(token)
            r_bytes = random.randbytes(32)
            print(f"XORing with: {r_bytes.hex()}")
            xorred = xor(r_bytes, hex_token)
            PASSWORD = decrypt(xorred)

solve

非想定解があったみたいだけどADMIN PANEL BUT HARDERで作った解法には影響なかったので、そのままlet’s go!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import random
from mt19937predictor import MT19937Predictor
from pwn import *
from Crypto.Util.number import *
from tqdm import tqdm

# io = process("./main.py")
io = remote("01.linux.challenges.ctf.thefewchosen.com", 55225)

def xor(bytes_first, bytes_second):
    d = b''
    for i in range(len(bytes_second)):
        d += bytes([bytes_first[i] ^ bytes_second[i]])
    return d


predictor = MT19937Predictor()
for _ in tqdm(range(120)):
    io.recvuntil(b"> ")
    io.sendline(b"2")
    io.recvuntil(b"> ")
    io.sendline(b"2"*64)
    x = bytes.fromhex(io.recvline(None).decode().split(": ")[1])
    # print(x)
    predictor.setrandbits(int.from_bytes(x, byteorder='little'), 8*32)

next_rnd = predictor.getrandbits(8*32).to_bytes(32, 'little')
io.recvuntil(b"> ")
io.sendline(b"2")
io.recvuntil(b"> ")
msg = xor(b"2"*64 ,next_rnd)
io.sendline(msg.hex().encode())
ret = bytes.fromhex(io.recvline(None).decode().split(": ")[1])
# print(ret,next_rnd, long_to_bytes(tst))
assert ret== next_rnd
print("[+] YES!!!!! find seed ")

# part2 
for i in tqdm(range(256)):
    io.recvuntil(b"> ")
    io.sendline(b"1")
    # print(io.recvuntil(b"> "))
    # print((long_to_bytes(1)*16).hex())
    io.sendline((long_to_bytes(i)*16).hex().encode())
    tmp = io.recvline()
    if b"{" in tmp:
        print(tmp)
        exit()
# TFCCTF{4pp4r3ntly_sp4ces_br34ks_th3_0ld_0ne}
This post is licensed under CC BY 4.0 by the author.