Can-CWIC CTF 2017 Writeup

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

Beautiful Blue (Steg 25)

高さ10pxごとに色が微妙に変わっている。青の色情報をASCIIコードとして文字にする。

from PIL import Image

img = Image.open('beautiful_blue.png').convert('RGB')
w, h = img.size

flag = ''
for y in range(0, h, 10):
    r, g, b = img.getpixel((0, y))
    flag += chr(b)

print flag
FLAG{Such_a_beautiful_color}

sudo (Steg 25)

解凍しようとすると、同じファイル名のファイルが2つ入っていることがわかる。1つを別名で展開し、ファイルサイズの小さい方を見てみると、フラグが書いてある。

FLAG{archives_are_waaaaayyy_easier_than_sudokus}

Coffee Stain (Steg 50)

ピクロスパズルのJPG画像。一部見えにくくなっている。
JPGの終端の後に、以下のデータあり。

h14:6,1,1,4,1,4,1
h15:7,2,2,3,1,5,2
h16:1,3,6,7,2
v29:2,5,2,7,10,2
v30:2,5,2,7,10,2

見えにくい部分はこれでわかる。この情報と合わせて、パズルを解いていくと、QRコードのようになる。
f:id:satou-y:20171015150457p:plain
このままだとうまく読み取れないので、3角の部分を少し調整すると、読み取ることができた。
f:id:satou-y:20171015150543p:plain

FLAG{NQNQR_C0D3}

Always Backup (Forensic 30)

http://159.203.38.169:5676/.git/にアクセスすると、flagファイルがある。
http://159.203.38.169:5676/.git/flagにアクセスしたら、フラグが書いてあった。

FLAG{ALWAYS-BACKUP!!!-and-use-git-to-backup...}

All is lost (Forensic 34)

http://159.203.38.169:5676/.git/objects/9d/dbc445f674f2891526db63b7bc5b5faea1e748をダウンロード

$ python -c 'import zlib; print zlib.decompress(open("dbc445f674f2891526db63b7bc5b5faea1e748").read())'
blob 77F
L
A
G
{
n
o
t
h
i
n
g
-
i
s
-
l
o
s
t
-
f
o
r
e
v
e
r
-
w
i
t
h
-
g
i
t
}
FLAG{nothing-is-lost-forever-with-git}

Tri it 0 (Prog 60)

$ nc 159.203.38.169 5672
Read https://en.wikipedia.org/wiki/Balanced_ternary
TT00T + TT010
a
Wrong answer.

https://en.wikipedia.org/wiki/Balanced_ternaryを参考に数値にして計算して、元の形式に戻すというプログラムを作成する。

import socket

bal3_letter = '01T'

def recvuntil(s, tails):
    data = ""
    while True:
        for tail in tails:
            if tail in data:
                return data
        data += s.recv(1)

def get_elem(s):
    elems = []
    elem = ''
    for i in range(len(s)):
        if s[i] in bal3_letter:
            elem += s[i]
        else:
            if elem != '':
                elems.append(elem)
                elem = ''
        if i == len(s) - 1:
            if elem != '':
                elems.append(elem)

    return elems

def get_formula(s, elems):
    formula = ''
    elem = ''
    index = 0
    for i in range(len(s)):
        if s[i] in bal3_letter:
            elem += s[i]
            if elem == elems[index]:
                formula += str(dec_bal3(elem))
                elem = ''
                index += 1
        else:
            formula += s[i]
    return formula

def dec_bal3(s):
    val = 0
    for i in range(len(s)):
        bit = len(s) - i - 1
        if s[i] == '0':
            bit_val = 0
        elif s[i] == '1':
            bit_val = 3 ** bit
        else:
            bit_val = - 3 ** bit
        val += bit_val
    return val

def enc_bal3(n):
    s = ''
    while n:
        s = bal3_letter[n % 3] + s
        n = -~n/3
    return s or 0

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('159.203.38.169', 5672))

data = recvuntil(s, '\n')
print data

# Try it 0
for i in range(2):
    print 'Round %d' % (i+1)
    data = recvuntil(s, '\n')
    print data
    data = data.replace('\n', '')
    elems = get_elem(data)
    formula = get_formula(data, elems)
    ans = enc_bal3(eval(formula))
    print ans
    s.sendall(ans + '\n')

data = recvuntil(s, '\n')
print data

2回答えると、フラグが表示された。

FLAG{I_CAN_MATH_LIKE_A_10RD_GRADER!}

Tri it 1 (Prog 150)

Tri it 0 の続き。足し算だけでなく、引き算、掛け算、かっこなども式に含まれる。

import socket

bal3_letter = '01T'

def recvuntil(s, tails):
    data = ""
    while True:
        for tail in tails:
            if tail in data:
                return data
        data += s.recv(1)

def get_elem(s):
    elems = []
    elem = ''
    for i in range(len(s)):
        if s[i] in bal3_letter:
            elem += s[i]
        else:
            if elem != '':
                elems.append(elem)
                elem = ''
        if i == len(s) - 1:
            if elem != '':
                elems.append(elem)

    return elems

def get_formula(s, elems):
    formula = ''
    elem = ''
    index = 0
    for i in range(len(s)):
        if s[i] in bal3_letter:
            elem += s[i]
            if elem == elems[index]:
                formula += str(dec_bal3(elem))
                elem = ''
                index += 1
        else:
            formula += s[i]
    return formula

def dec_bal3(s):
    val = 0
    for i in range(len(s)):
        bit = len(s) - i - 1
        if s[i] == '0':
            bit_val = 0
        elif s[i] == '1':
            bit_val = 3 ** bit
        else:
            bit_val = - 3 ** bit
        val += bit_val
    return val

def enc_bal3(n):
    s = ''
    while n:
        s = bal3_letter[n % 3] + s
        n = -~n/3
    return s or 0

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('159.203.38.169', 5672))

data = recvuntil(s, '\n')
print data

# Try it 0
for i in range(2):
    print 'Round %d' % (i+1)
    data = recvuntil(s, '\n')
    print data
    data = data.replace('\n', '')
    elems = get_elem(data)
    formula = get_formula(data, elems)
    ans = enc_bal3(eval(formula))
    print ans
    s.sendall(ans + '\n')

data = recvuntil(s, '\n')
print data

# Try it 1
for i in range(17):
    print 'Round %d' % (i+1)
    data = recvuntil(s, '\n')
    print data
    data = data.replace('\n', '')
    elems = get_elem(data)
    formula = get_formula(data, elems)
    ans = enc_bal3(eval(formula))
    print ans
    s.sendall(ans + '\n')

data = recvuntil(s, '\n')
print data

17回答えると、フラグが表示された。

FLAG{ONE_PLUS_ONE_EQ_ONE_TON}

Rich 2 (Crypto 200)

タイトル的な順番では逆だが、結果的にはRich 1よりこちらの方が先に解けた。

$ nc 159.203.38.169 5682
Please give me your wallet and I'll determine if you are rich enough to access out VIP area.
Here is an example wallet:
56e3fc849a2c06c2f858615a2a17b0525a1a5020874b2ceebaf411f2753f91864dced78d862c46daf45a204e3c18b35e40360675d70c7db1f9a417e5337ec4cf01de87d8d67a5ae0e4471a5c7854d613033d2b6ad5117780eee964a36471f6cd15d8ac9c9e330ebfb3521a5c7846de1f04362b7acd0b66939b9209c43d41d1dd028987ecc97d4df5c66c3e5f5c46ee1511351b19ef1052a4fad30ff5103792d237e1a88eec2254d3f9057c155351a262
56e3fc849a2c06c2f858615a2a17b0525a1a5020874b2ceebaf411f2753f91864dced78d862c46daf45a204e3c18b35e40360675d70c7db1f9a417e5337ec4cf01de87d8d67a5ae0e4471a5c7854d613033d2b6ad5117780eee964a36471f6cd15d8ac9c9e330ebfb3521a5c7846de1f04362b7acd0b66939b9209c43d41d1dd028987ecc97d4df5c66c3e5f5c46ee1511351b19ef1052a4fad30ff5103792d237e1a88eec2254d3f9057c155351a262
Plebs be plebs. No money, no access.
Guessing you cant math... 1137 is way too low.

提示された暗号文字列をそのまま入れると、amountの値が1137になっていることがわかる。
コードを読むと、暗号の概要は以下の通りとわかる。

plaintextは以下の形式
{"CCCCCC": "TTTTTT", "EEEEEE": "FLAG{DDDDDD}", "RRRRRR": "EEEEEE", "DDDDDD": "AAAAAA"}

enc_xor_cbc(plaintext, key, iv):暗号化16進数文字列を表示
plaintextを16の倍数になるようパディング(16の倍数の場合は16バイトパディング)

16バイトごとに以下の処理
1ブロック目暗号:1ブロック目平文 ^ key ^ IV
2ブロック目暗号:2ブロック目平文 ^ key ^ 1ブロック目暗号
3ブロック目暗号:3ブロック目平文 ^ key ^ 2ブロック目暗号
          :

このことから以下のようにして平文がわかるはず。

3ブロック目平文:3ブロック目暗号 ^ key ^ 2ブロック目暗号
2ブロック目平文:2ブロック目暗号 ^ key ^ 1ブロック目暗号
1ブロック目平文:1ブロック目暗号 ^ key ^ IV

11ブロック目暗号 ^ key ^ 10ブロック目暗号 = 11ブロック目平文

パディングの長さを1~16で総当たり、さらに部分的に復号された値と平文の一部を頭に入れながら、復号する。最終的には以下のコードでフラグが得られた。

import socket

def recvuntil(s, tails):
    data = ""
    while True:
        for tail in tails:
            if tail in data:
                return data
        data += s.recv(1)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('159.203.38.169', 5682))

data = recvuntil(s, "\n")
print data
data = recvuntil(s, "\n")
print data
data = recvuntil(s, "\n")
print data

c_hex = data.strip()
c_list = []
for i in range(0, len(c_hex), 32):
    c_list.append(c_hex[i:i+32].decode('hex'))

for pad_length in range(16, 0, -1):
    p10_tail = '"}' + chr(pad_length) * pad_length
    block_chk_size = len(p10_tail)
    if block_chk_size > 16:
        block_chk_size = 16
    c09_tail = c_list[9][-block_chk_size:]
    c10_tail = c_list[10][-block_chk_size:]

    key = ''
    for i in range(block_chk_size):
        c09_char = c09_tail[-block_chk_size+i]
        c10_char = c10_tail[-block_chk_size+i]
        p10_char = p10_tail[-block_chk_size+i]
        code = ord(c09_char) ^ ord(c10_char) ^ ord(p10_char)
        key += chr(code)

    plain_list = []
    error = False
    for i in range(9):
        c_pre_tail = c_list[8-i][-block_chk_size:]
        c_cur_tail = c_list[9-i][-block_chk_size:]
        plain_tail = ''
        for j in range(block_chk_size):
            c_pre_char = c_pre_tail[-block_chk_size+j]
            c_cur_char = c_cur_tail[-block_chk_size+j]
            key_char = key[-block_chk_size+j]
            code = ord(c_pre_char) ^ ord(c_cur_char) ^ ord(key_char)
            if code < 32 or code > 126:
                error = True
                break
            else:
                plain_tail += chr(code)
        if error:
            break
        plain_list.append(plain_tail)

    if error == False:
        print plain_list

# 6th block
c_pre_char = c_list[4][12]
c_cur_char = c_list[5][12]
plain_char = '"'
key1_char = chr(ord(c_pre_char) ^ ord(c_cur_char) ^ ord(plain_char))

# 7th block
c_pre_char = c_list[5][:12]
c_cur_char = c_list[6][:12]
plain_char = 'unt": 1137, '
key2_char = ''
for i in range(len(c_cur_char)):
    key2_char += chr(ord(c_pre_char[i]) ^ ord(c_cur_char[i]) ^ ord(plain_char[i]))

key = key2_char + key1_char + key

# Decrypt the Later Parts
plain_tail = ''
for i in range(10):
    plain_part = ''
    c_pre = c_list[9 - i]
    c_cur = c_list[10 - i]
    for j in range(16):
        code = ord(c_pre[j]) ^ ord(c_cur[j]) ^ ord(key[j])
        plain_part += chr(code)
    plain_tail = plain_part + plain_tail

print plain_tail
FLAG{CBse4_Butch3reD_Cryp7o!WoW_R3cur51vE_@Cr0NymZ!?}

Rich 1 (Crypto 150)

Rich 2の続き。amountを1337にして、そのブロック以降を暗号化して投入する。

import socket

def recvuntil(s, tails):
    data = ""
    while True:
        for tail in tails:
            if tail in data:
                return data
        data += s.recv(1)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('159.203.38.169', 5682))

data = recvuntil(s, "\n")
print data
data = recvuntil(s, "\n")
print data
data = recvuntil(s, "\n")
print data

c_hex = data.strip()
c_list = []
for i in range(0, len(c_hex), 32):
    c_list.append(c_hex[i:i+32].decode('hex'))

for pad_length in range(16, 0, -1):
    p10_tail = '"}' + chr(pad_length) * pad_length
    block_chk_size = len(p10_tail)
    if block_chk_size > 16:
        block_chk_size = 16
    c09_tail = c_list[9][-block_chk_size:]
    c10_tail = c_list[10][-block_chk_size:]

    key = ''
    for i in range(block_chk_size):
        c09_char = c09_tail[-block_chk_size+i]
        c10_char = c10_tail[-block_chk_size+i]
        p10_char = p10_tail[-block_chk_size+i]
        code = ord(c09_char) ^ ord(c10_char) ^ ord(p10_char)
        key += chr(code)

    plain_list = []
    error = False
    for i in range(9):
        c_pre_tail = c_list[8-i][-block_chk_size:]
        c_cur_tail = c_list[9-i][-block_chk_size:]
        plain_tail = ''
        for j in range(block_chk_size):
            c_pre_char = c_pre_tail[-block_chk_size+j]
            c_cur_char = c_cur_tail[-block_chk_size+j]
            key_char = key[-block_chk_size+j]
            code = ord(c_pre_char) ^ ord(c_cur_char) ^ ord(key_char)
            if code < 32 or code > 126:
                error = True
                break
            else:
                plain_tail += chr(code)
        if error:
            break
        plain_list.append(plain_tail)

    if error == False:
        print plain_list

# 6th block
c_pre_char = c_list[4][12]
c_cur_char = c_list[5][12]
plain_char = '"'
key1_char = chr(ord(c_pre_char) ^ ord(c_cur_char) ^ ord(plain_char))

# 7th block
c_pre_char = c_list[5][:12]
c_cur_char = c_list[6][:12]
plain_char = 'unt": 1137, '
key2_char = ''
for i in range(len(c_cur_char)):
    key2_char += chr(ord(c_pre_char[i]) ^ ord(c_cur_char[i]) ^ ord(plain_char[i]))

key = key2_char + key1_char + key

# Decrypt the Later Parts
plain_tail = []
for i in range(10):
    plain_part = ''
    c_pre = c_list[9 - i]
    c_cur = c_list[10 - i]
    for j in range(16):
        code = ord(c_pre[j]) ^ ord(c_cur[j]) ^ ord(key[j])
        plain_part += chr(code)
    plain_tail.append(plain_part)

print plain_tail

# Encrypt "amount": 1337
mod_c_list = []
for i in range(5):
    p_part = plain_tail[4-i]
    if i == 0:
        p_part = plain_tail[4-i][:7] + '3' + plain_tail[4-i][8:]
        c_pre = c_list[5+i]
    else:
        c_pre = mod_c_list[-1]
    c_part = ''
    for j in range(16):
        code = ord(p_part[j]) ^ ord(c_pre[j]) ^ ord(key[j])
        c_part += chr(code)
    mod_c_list.append(c_part)

new_c_hex = ''
for i in range(6):
    new_c_hex += c_list[i].encode('hex')

for i in range(5):
    new_c_hex += mod_c_list[i].encode('hex')

print new_c_hex
s.sendall(new_c_hex + '\n')

data = recvuntil(s, "\n")
print data
data = recvuntil(s, "\n")
print data
FLAG{nVm_74BLeZ_FL1P_B!Ts=O}