UTCTF 2020 Writeup

この大会は2020/3/7 9:00(JST)~2020/3/9 9:00(JST)に開催されました。
今回もチームで参戦。結果は1713点で1001チーム中122位でした。
自分で解けた問題をWriteupとして書いておきます。

Sanity Check (Misc)

問題文にフラグが書いてあった。

utflag{this_is_the_flag}

[basics] reverse engineering (Reverse Engineering)

$ strings calc | grep utflag
utflag{str1ngs_1s_y0ur_fr13nd}
utflag{str1ngs_1s_y0ur_fr13nd}

.PNG2 (Reverse Engineering)

バイナリの構成は以下のようになっていると推測できる。

0x0000-0x0003 "PNG2"固定
0x0004-0x0009 "width="固定
0x000a-0x000b 幅の値
0x000c-0x0012 "height="固定
0x0013-0x0014 高さの値
0x0015以降    3バイトごとに各ピクセルのRGBの値

このことを元に画像を生成してみる。

from PIL import Image
from struct import *

with open('pic.png2', 'rb') as f:
    data = f.read()

width = unpack('>H', data[10:12])[0]
height = unpack('>H', data[19:21])[0]
colors = data[21:]

img = Image.new('RGB', (width, height), (255, 255, 255))

i = 0
for y in range(height):
    for x in range(width):
        r = ord(colors[i])
        g = ord(colors[i+1])
        b = ord(colors[i+2])
        i += 3
        img.putpixel((x, y), (r, g, b))

img.save('flag.png')

画像を復元すると、フラグが表記されていた。
f:id:satou-y:20200313062547p:plain

utflag{j139adfo_93u12hfaj}

[basics] forensics (Forensics)

$ file secret.jpeg 
secret.jpeg: UTF-8 Unicode text, with CRLF line terminators
$ cat secret.jpeg | grep utflag
utflag{fil3_ext3nsi0ns_4r3nt_r34l}
utflag{fil3_ext3nsi0ns_4r3nt_r34l}

Observe Closely (Forensics)

pngの末尾にzipが付いているので、抽出してextract.zipとして保存する。

$ unzip extract.zip 
Archive:  extract.zip
  inflating: hidden_binary
$ file hidden_binary 
hidden_binary: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, BuildID[sha1]=101705e6f60a300ab34f377a87fdb67f92996732, for GNU/Linux 3.2.0, not stripped
$ ./hidden_binary 
Ah, you found me!
utflag{2fbe9adc2ad89c71da48cabe90a121c0}
utflag{2fbe9adc2ad89c71da48cabe90a121c0}

Spectre (Forensics)

Sonic Visualiserで開き、スペクトログラムを見ると、フラグが現れた。
f:id:satou-y:20200313230010p:plain

utflag{sp3tr0gr4m0ph0n3}

1 Frame per Minute (Forensics)

wavの画像受信データをデコードする必要がある。https://github.com/colaclanth/sstvのツールを使う。

$ sstv -d signals.wav -o flag.png
[sstv] Searching for calibration header... Found!    
[sstv] Detected SSTV mode Martin 1
[sstv] Decoding image...   [#############################################] 100%
[sstv] Drawing image data...
[sstv] ...Done!

f:id:satou-y:20200313230142p:plain
出力された画像にフラグが書かれていた。

utflag{6bdfeac1e2baa12d6ac5384cdfd166b0}

The Legend of Hackerman, Pt. 1 (Forensics)

pngヘッダ4バイトが壊れているので修正すると、画像にフラグが書かれていた。
f:id:satou-y:20200313230236p:plain

utflag{3lit3_h4ck3r}

The Legend of Hackerman, Pt. 2 (Forensics)

docxをzip解凍する。Hacker\word\media\image23.pngにフラグが書かれている。
f:id:satou-y:20200313230351p:plain

utflag{unz1p_3v3ryth1ng}

[basics] crypto (Cryptography)

2進数のコードが並んでいるので、デコードする。すると途中からbase64文字列になるので、さらにデコードする。今度は途中から今度はシーザー暗号になっているので復号する。

import string

def caesar(s, key):
    d = ''
    for c in s:
        code = ord(c)
        if c in string.uppercase:
            code = code - key
            if code < ord('A'):
                code += 26
        elif c in string.lowercase:
            code = code - key
            if code < ord('a'):
                code += 26
        d += chr(code)
    return d

with open('binary.txt', 'r') as f:
    codes = f.read().rstrip()

codes = codes.split(' ')

dec = ''
for code in codes:
    dec += chr(int(code, 2))

pt0 = dec.split('\n')[0]
print pt0
ct0 = dec.split('\n')[1]

dec = ct0.decode('base64')

pt1 = dec.split('\n')[0]
print pt1
ct1 = dec.split('\n')[1]
ct2 = dec.split('\n')[2]

dec = caesar(ct1, 10)
print dec
dec = caesar(ct2, 10)
print dec

実行結果は以下の通り。

Uh-oh, looks like we have another block of text, with some sort of special encoding. Can you figure out what this encoding is? (hint: if you look carefully, you'll notice that there only characters present are A-Z, a-z, 0-9, and sometimes / and +. See if you can find an encoding that looks like this one.)
New challenge! Can you figure out what's going on here? It looks like the letters are shifted by some constant. (hint: you might want to start looking up Roman people).
alright, you're almost there! Now for the final (and maybe the hardest...) part: a substitution cipher. In the following text, I've taken my message and replaced every alphabetic character with a correspondence to a different character - known as a substitution cipher. Can you find the final flag? hint: We know that the flag is going to be of the format utflag{...} - which means that if you see that pattern, you know what the correspondences for u, t, f, l a, and g are. You can probably work out the remaining characters by replacing them and inferring common words in the English language. Another great method is to use frequency analysis: we know that 'e' shows up most often in the alphabet, so that's probably the most common character in the text, followed by 't', and so on. Once you know a few characters, you can infer the rest of the words based on common words that show up in the English language.
hwxdnitvoitjwxk! gwv yiqa sjxjkyau tya padjxxan hngbtwdnibyg hyiooaxda. yana jk i soid swn ioo gwvn yinu asswntk: vtsoid{x0l_ty4tk_ly4t_j_h4oo_hngbt0}. gwv ljoo sjxu tyit i owt ws hngbtwdnibyg jk fvkt pvjoujxd wss tyjk kwnt ws pikjh rxwloauda, ixu jt naioog jk xwt kw piu istan ioo. ywba gwv axfwgau tya hyiooaxda!

最後の一行は換字式暗号になっているので、quipqiupで復号する。

congratulations! you have finished the beginner cryptography challenge. here is a flag for all your hard efforts: utflag{n0w_th4ts_wh4t_i_c4ll_crypt0}. you will find that a lot of cryptography is just building off this sort of basic knowledge, and it really is not so bad after all. hope you enjoyed the challenge!

復号した文章の中にフラグが含まれていた。

utflag{n0w_th4ts_wh4t_i_c4ll_crypt0}

One True Problem (Cryptography)

https://github.com/SpiderLabs/cribdragを使って推測していく。

$ python xorstrings.py 213c234c2322282057730b32492e720b35732b2124553d354c22352224237f1826283d7b0651 3b3b463829225b3632630b542623767f39674431343b353435412223243b7f162028397a103e
1a0765740a007316651000666f0d04740c146f10106e0801796317010018000e06000401166f
$ python cribdrag.py 1a0765740a007316651000666f0d04740c146f10106e0801796317010018000e06000401166f
                                :
Your message is currently:
0	THE BEST CTF CATEGORY IS CRYPTOGRAPHY!
Your key is currently:
0	NO THE BEST ONE IS BINARY EXPLOITATION

xor keyがフラグになる。

from Crypto.Util.strxor import strxor

ct1 = '213c234c2322282057730b32492e720b35732b2124553d354c22352224237f1826283d7b0651'.decode('hex')
ct2 = '3b3b463829225b3632630b542623767f39674431343b353435412223243b7f162028397a103e'.decode('hex')

pt1 = 'THE BEST CTF CATEGORY IS CRYPTOGRAPHY!'
pt2 = 'NO THE BEST ONE IS BINARY EXPLOITATION'


key = strxor(pt1, ct1)
print key
key = strxor(pt2, ct2)
print key

index = key.index('}')
flag = key[:index + 1]
print flag

実行結果は以下の通り。

utflag{tw0_tim3_p4ds}utflag{tw0_tim3_p
utflag{tw0_tim3_p4ds}utflag{tw0_tim3_p
utflag{tw0_tim3_p4ds}
utflag{tw0_tim3_p4ds}

Random ECB (Cryptography)

$ nc crypto.utctf.live 9003
Input a string to encrypt (input 'q' to quit):
1
Here is your encrypted string, have a nice day :)
d825b841547c7d5fddd67841db22b6bf6af5818287b92aa3b4b9043f4f615c5f
Input a string to encrypt (input 'q' to quit):
1
Here is your encrypted string, have a nice day :)
a9d279584431ef8fc22bf882737e87131ae342b5af9b6f75fd1aea064aaee9eb64686f2fb06d61510e745ca67cee4fc9
Input a string to encrypt (input 'q' to quit):
1
Here is your encrypted string, have a nice day :)
a9d279584431ef8fc22bf882737e87131ae342b5af9b6f75fd1aea064aaee9eb64686f2fb06d61510e745ca67cee4fc9
Input a string to encrypt (input 'q' to quit):
1
Here is your encrypted string, have a nice day :)
d825b841547c7d5fddd67841db22b6bf6af5818287b92aa3b4b9043f4f615c5f
Input a string to encrypt (input 'q' to quit):

暗号化処理は以下のようになっている。

plaintext = 'A' * (0 or 1) + [入力] + flag + padding
-> AES-ECB

1で何回か試したところ、長さが32バイトのときと48バイトのときがある。このことから以下のような構造になることがわかる。

0123456789abcdef
A1FFFFFFFFFFFFFF
FFFFFFFFFFFFFFFF
PPPPPPPPPPPPPPPP

flagの長さは30バイトであることがわかる。

0123456789abcdef	0
XXXXXXXXXXXXXXX?
XXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXF
FFFFFFFFFFFFFFFF
FFFFFFFFFFFFFPPP

0123456789abcdef	1
XXXXXXXXXXXXXXF?
XXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXFF
FFFFFFFFFFFFFFFF
FFFFFFFFFFFFPPPP

    :

0123456789abcdef	15
FFFFFFFFFFFFFFF?
XXXXXXXXXXXXXXXX
FFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFPP

0123456789abcdef	16
FFFFFFFFFFFFFFF?
XXXXXXXXXXXXXXXF
FFFFFFFFFFFFFFFF
FFFFFFFFFFFFFPPP

先頭から1文字ずつフラグをはみ出させ、0ブロック目と2ブロック目で暗号を比較する。ただし、'A'が付く場合とそうでない場合があるため、2パターン比較する。

import socket

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('crypto.utctf.live', 9003))

flag = ''
for i in range(30):

    found = False
    for code in range(32, 127):
        print '[+] flag =', flag
        if len(flag) < 16:
            pt = 'X' * (15 - i) + flag + chr(code)  + 'X' * (31 - i)
        else:
            pt = flag[-15:] + chr(code)  + 'X' * (31 - i)

        tmp = ''
        while True:
            data = recvuntil(s, '\n').rstrip()
            print data + pt
            s.sendall(pt + '\n')
            data = recvuntil(s, '\n').rstrip()
            print data
            data = recvuntil(s, '\n').rstrip()
            print data

            enc0 = data[32*0:32*1]
            enc2 = data[32*2:32*3]
            if enc0 == enc2:
                found = True
                flag += chr(code)
                break

            if tmp == '':
                tmp = data
            elif tmp != data:
                break

        if found:
            break

print flag

実行結果は以下の通り。

        :
[+] flag = utflag{3cb_w17h_r4nd0m_pr3f1x
Input a string to encrypt (input 'q' to quit):h_r4nd0m_pr3f1x}XX
Here is your encrypted string, have a nice day :)
c4901be47a39685e49077e97b1f99a01ab7aefb49370f50edf206bdeb9ce2452af93d35d1f8262d2f10a71badc3fe3fc7c55bf8b3be29ef6762e488ffe4ad872
Input a string to encrypt (input 'q' to quit):h_r4nd0m_pr3f1x}XX
Here is your encrypted string, have a nice day :)
c4901be47a39685e49077e97b1f99a01ab7aefb49370f50edf206bdeb9ce2452af93d35d1f8262d2f10a71badc3fe3fc7c55bf8b3be29ef6762e488ffe4ad872
Input a string to encrypt (input 'q' to quit):h_r4nd0m_pr3f1x}XX
Here is your encrypted string, have a nice day :)
c4901be47a39685e49077e97b1f99a01ab7aefb49370f50edf206bdeb9ce2452af93d35d1f8262d2f10a71badc3fe3fc7c55bf8b3be29ef6762e488ffe4ad872
Input a string to encrypt (input 'q' to quit):h_r4nd0m_pr3f1x}XX
Here is your encrypted string, have a nice day :)
9ca4b3c0e1e2244ab3fcff7e91884442c05f4ec1bc2ac4e758261ef3109f96bd9ca4b3c0e1e2244ab3fcff7e9188444250549e4f79525df7e4b66f3bb33b88d6
utflag{3cb_w17h_r4nd0m_pr3f1x}
utflag{3cb_w17h_r4nd0m_pr3f1x}

Galois (Cryptography)

AES-GCMの問題。GCMは暗号化モードにCTRモードを使っているので、keyとnonceが変わらなければ、平文と暗号文のXORは常に同じになる。このことからXORでflagを復号する。

import socket

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

def str_xor(s1, s2):
    return ''.join(chr(ord(a) ^ ord(b)) for a, b in zip(s1, s2))

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('crypto.utctf.live', 9004))

data = recvuntil(s, ': \n').rstrip()
print data

enc_flag = data.split('\n')[0].split(' ')[1].decode('hex')

print '1'
s.sendall('1\n')
data = recvuntil(s, ':\n').rstrip()
print data

try_pt = '1' * len(enc_flag)
print try_pt
s.sendall(try_pt + '\n')
data = recvuntil(s, '\n').rstrip()
print data
data = recvuntil(s, '\n').rstrip()
print data
try_ct = eval(data)[0].decode('hex')

flag = str_xor(str_xor(try_pt, try_ct), enc_flag)
print flag

実行結果は以下の通り。

flag fc4bd0f92507ce533500b4b59deecb74d95633f4c9a54d709e9af27756f4584f
Welcome to the AES GCM encryption and decryption tool!
        1. Encrypt message
        2. Decrypt message
        3. Quit

Select option:
1
Input a string to encrypt (must be at least 32 characters):
11111111111111111111111111111111
Here is your encrypted string & tag, have a nice day :)
('b80e87a475518454675cdae29cad98748c0331aba7a04b769bc8a81950f25e03', '78545c2af8bb95d71eada9703bb5ceb5')
utflag{6cm_f0rb1dd3n_4774ck_777}
utflag{6cm_f0rb1dd3n_4774ck_777}

Hill (Cryptography)

アルファベットのみ取り出し、ヒル暗号として復号する。暗号は4文字セットで、平文が"utfl"から始まることを前提にすれば、鍵を求められる。あとは逆行列を使えば、復号できる。

#!/usr/bin/env sage -python
import string

def letters_to_numlist(s):
    s = s.lower()
    ary = []
    for c in s:
        index = string.lowercase.index(c)
        ary.append(index)
    return ary

def numlist_to_letters(lst):
    s = ''
    for idx in lst:
        s += string.lowercase[idx]
    return s

def ary1x4_to_2x2(ary):
    ary2 = []
    for i in range(2):
        row = []
        for j in range(2):
            row.append(ary[i + j * 2])
        ary2.append(row)
    return ary2

def ary2x2_to_1x4(ary2):
    ary = []
    for i in range(2):
        for j in range(2):
            ary.append(ary2[j][i])
    return ary

def decrypt(k_mat, c_mat):
    p_mat = k_mat.inverse() * c_mat
    return p_mat

enc = 'wznqca{d4uqop0fk_q1nwofDbzg_eu}'
pre_flag = 'utfl'

enc = list(enc)

ct = ''
for c in enc:
    if c in string.letters:
        ct += c

ct = letters_to_numlist(ct)
pt = letters_to_numlist(pre_flag)

#### calculate key ####
p_ary = ary1x4_to_2x2(pt)
c_ary = ary1x4_to_2x2(ct[:4])

p_mat = matrix(Zmod(26), p_ary)
c_mat = matrix(Zmod(26), c_ary)
k_mat = c_mat * p_mat.inverse()

#### decrypt ####
pt = []
for i in range(0, len(ct), 4):
    c_ary = ary1x4_to_2x2(ct[i:i+4])
    c_mat = matrix(Zmod(26), c_ary)
    p_mat = decrypt(k_mat, c_mat)
    p_ary = ary2x2_to_1x4(p_mat)
    pt += p_ary

pt = numlist_to_letters(pt)

index = 0
flag = ''
for c in enc:
    if c in string.lowercase:
        flag += pt[index]
        index += 1
    elif c in string.uppercase:
        flag += pt[index].upper()
        index += 1
    else:
        flag += c

print flag
utflag{d4nger0us_c1pherText_qq}

Survey (Misc)

アンケートに答えたら、フラグが表示された。

utflag{thank_you_RyehGswqqC}