LINE CTF 2021 Writeup

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

Welcome (WEB)

問題に記載のURLにアクセスしたら、フラグが表示された。

LINECTF{welcome_to_linectf}

babycrypto1 (CRY)

$ nc 35.200.115.41 16001
test Command: bkmYVFb7Ab2XprBQmVT7s4Abm5jHL6dwii7HmWzSUur7LOaa0ZWWhf0kfFXLbuExQNQW+Kq7AnFY614rnMlxRfT4+e7fKB4xQJ1ITbojA14AcY+VhlNZ/ymJ/ElOdyQSzHc8ncGi5XPrMPIN123O/t1hFrQ0jGNJtaSL4r2T024LAIlX414BNSBxWfukXPnNiTYrQ27qMJNJjwEDMoa7rn2ClOlu1cgkoWIyRq1xbESgl/zia/xB7ezP/6ygGWK8
**Cipher oracle**
IV...: YWJjZGVmMDEyMzQ1Njc4OQ==
Message...: YQ==
Ciphertext:YWJjZGVmMDEyMzQ1Njc4Ob9uqp+fqBo8SBWm00miLVs=

Enter your command: bkmYVFb7Ab2XprBQmVT7s4Abm5jHL6dwii7HmWzSUur7LOaa0ZWWhf0kfFXLbuExQNQW+Kq7AnFY614rnMlxRfT4+e7fKB4xQJ1ITbojA14AcY+VhlNZ/ymJ/ElOdyQSzHc8ncGi5XPrMPIN123O/t1hFrQ0jGNJtaSL4r2T024LAIlX414BNSBxWfukXPnNiTYrQ27qMJNJjwEDMoa7rn2ClOlu1cgkoWIyRq1xbESgl/zia/xB7ezP/6ygGWK8
T1k+JaWJbIdLK14ECGNtt9+Jw9UjcdzirjBpEZbBzMRIrQdEjVzrKjPsD7rMu6mxmNrmxZ4qp27XL1Br7Qk1qnMrmOJV9caqxhe940pYzbSHZCMeeXuT07eWnY8/mx/V3bMHFDO4Ku5gSLrOQ9VbVuEmzpFfVP3rtest

サーバの処理概要は以下の通り。

・aes_key: ランダム16バイト文字列
・token: 160バイトランダム文字列をbase64にしたものの先頭160バイト
・token + 'test' のAES-CBC暗号化(iv(ランダム) + enc)を表示(base64)
・iv: base64で入力
・msg: base64で入力
・入力したivでmsgをAES-CBC暗号化して表示(base64)
・以下、繰り返し
 ・tt: コマンド入力(先頭16バイトはiv)
 ・tt2: ttを復号 → 表示
 ・tt2とtoken + 'show'が一致したら、フラグを表示

T: token

0123456789abcdef
TTTTTTTTTTTTTTTT
TTTTTTTTTTTTTTTT
TTTTTTTTTTTTTTTT
TTTTTTTTTTTTTTTT
TTTTTTTTTTTTTTTT
TTTTTTTTTTTTTTTT
TTTTTTTTTTTTTTTT
TTTTTTTTTTTTTTTT
TTTTTTTTTTTTTTTT
TTTTTTTTTTTTTTTT ^ C 9 --AES--> C10
showPPPPPPPPPPPP ^ C10 --AES--> C11

test commandの表示は192バイトのbase64。16バイトがiv、次の160バイトがtoken、残り16バイトが"test"の暗号。ivでC10, msgで"show"を指定すれば、11ブロック目の暗号を入手できる。
これを元にスクリプトにして実行する。

from base64 import b64decode
from base64 import b64encode
import socket

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

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('35.200.115.41', 16001))

data = recvuntil(s, '\n').rstrip()
print data
test_ct = b64decode(data.split(': ')[1])

iv = b64encode(test_ct[16*10:16*11])
msg = b64encode('show')

data = recvuntil(s, ': ')
print data + iv
s.sendall(iv + '\n')

data = recvuntil(s, ': ')
print data + msg
s.sendall(msg + '\n')

data = recvuntil(s, '\n').rstrip()
print data
ct11 = b64decode(data.split(':')[1])[16:]

ct = b64encode(test_ct[:16*11] + ct11)
test_ct = b64encode(test_ct)

data = recvuntil(s, ': ')
print data + ct
s.sendall(ct + '\n')
data = recvuntil(s, '\n').rstrip()
print data
data = recvuntil(s, '}')
print data

実行結果は以下の通り。

test Command: wxugw/hyLsxPjOenrAL9vQ70GvyXPKOUVb3ByqUscFyCPLdDamKLP8hH1cHKlJF6mdrRibn5gjcM+3l/d28AH6x9c68jhN20MQ1pgnWSMdZjujt7Y54gona4R4E4ObON9+QYWmplrZZFuSzPcDrJBNVdQeiHmqw9SvrJUOUVSKKnCEJQqsD8Luynu5fKB7dHvl/7lK3awmbNhC/uo//ZlU/q0xMF/fLnPPbzG1+jWZhax2Jb5mzKXHjckCSZhSWr
**Cipher oracle**
IV...: T+rTEwX98uc89vMbX6NZmA==
Message...: c2hvdw==
Ciphertext:T+rTEwX98uc89vMbX6NZmH/Y3mVSU+npGRYyjfNkxmU=

Enter your command: wxugw/hyLsxPjOenrAL9vQ70GvyXPKOUVb3ByqUscFyCPLdDamKLP8hH1cHKlJF6mdrRibn5gjcM+3l/d28AH6x9c68jhN20MQ1pgnWSMdZjujt7Y54gona4R4E4ObON9+QYWmplrZZFuSzPcDrJBNVdQeiHmqw9SvrJUOUVSKKnCEJQqsD8Luynu5fKB7dHvl/7lK3awmbNhC/uo//ZlU/q0xMF/fLnPPbzG1+jWZh/2N5lUlPp6RkWMo3zZMZl
0VOipsv3sjGUZ1HyvFhe9FEKJe5YkRlYkiCpyjAPb38EvmMX2E+zqm6RcghrRBkejy2K46l3095tQvjJ/klmDumWps4WJkoSmnMtrOnfdmri1+8IjfLP7WKlqH4r9gHEqdPJRr0JyRtwFdMc9Qgk1xneuic9Mj7kshow
The flag is: LINECTF{warming_up_crypto_YEAH}
LINECTF{warming_up_crypto_YEAH}

babycrypto2 (CRY)

$ nc 35.200.39.68 16002
test Command: q7iDALThVWeTOdhhltJrf4qOsRdvEARRMKo2CtzgMD5HpVZf9Xcm4f9HQgq4rCVM9L2kmX2tQ1JSwhaynRHHQYlfMmK1ALK1OgxBoeSUoJxVPUIWVna9dhDU9+eXALKKWrVBz+q7h4P1etX5aAfspMx6B4kv8LdmOSm++gAqTvdgBNYZfOrP/iQn+4OSDcRgVny1CRVqW2HbqTo709e2WPeP7/n0LGHM9orabta/+cS+L4pBm/bDyiy6uO1ZzbdxrdjypjA9sQYZL2NwrMcNBkeit72kEOBiSBJQFJpN8RImcN0m6vzlXdeejkbCR+ocB5AXIT3tPYdEacV79gdlFQ==
Enter your command: q7iDALThVWeTOdhhltJrf4qOsRdvEARRMKo2CtzgMD5HpVZf9Xcm4f9HQgq4rCVM9L2kmX2tQ1JSwhaynRHHQYlfMmK1ALK1OgxBoeSUoJxVPUIWVna9dhDU9+eXALKKWrVBz+q7h4P1etX5aAfspMx6B4kv8LdmOSm++gAqTvdgBNYZfOrP/iQn+4OSDcRgVny1CRVqW2HbqTo709e2WPeP7/n0LGHM9orabta/+cS+L4pBm/bDyiy6uO1ZzbdxrdjypjA9sQYZL2NwrMcNBkeit72kEOBiSBJQFJpN8RImcN0m6vzlXdeejkbCR+ocB5AXIT3tPYdEacV79gdlFQ==
Command: testZzTosTPcUnBu44roqYGeyIjyJ22OoYp66c67bitZZthoRjCwTC9sW5jOFkVqW7WlMwJqI5Y+eJtxCkY4lkVkX3vzdDSkPtwB1FOrfHWPeGq4jOeprbrqhtcQzRdUsukyF1YdRzzZ8ezm2g82ydjFYcDrkfzk3ZQOzx0CpVulL80HGbwsB6amUMsmmVsC4jULWpg66++vj30B6p/aKGbh
Enter your command:

サーバの処理概要は以下の通り。

・AES_KEY: ランダム16バイト文字列
・TOKEN: 159バイトランダム文字列をbase64にしたもの→212バイト
・PREFIX = 'Command: '
・PREFIX + 'test' + TOKEN のAES-CBC暗号化(iv(ランダム) + enc)を表示(base64)
・以下、繰り返し
 ・tt: コマンド入力(先頭16バイトはiv)
 ・tt2: ttを復号 → 表示
 ・tt2とPREFIX + 'show' + TOKEN が一致したら、フラグを表示

T: token

0123456789abcdef
Command: testTTT
TTTTTTTTTTTTTTTT
TTTTTTTTTTTTTTTT
TTTTTTTTTTTTTTTT
TTTTTTTTTTTTTTTT
TTTTTTTTTTTTTTTT
TTTTTTTTTTTTTTTT
TTTTTTTTTTTTTTTT
TTTTTTTTTTTTTTTT
TTTTTTTTTTTTTTTT
TTTTTTTTTTTTTTTT
TTTTTTTTTTTTTTTT
TTTTTTTTTTTTTTTT
TTTTTTTTTTTTTTTT
TPPPPPPPPPPPPPPP

ivを調整するだけで1ブロック目の平文は変更できる。

iv1 ^ pt1 = iv2 ^ pt2

上のようになるようpt1で"test"の部分をpt2で"show"になるようiv2を算出する。
これを元にスクリプトにして実行する。

from base64 import b64decode
from base64 import b64encode
import socket
from Crypto.Util.strxor import strxor

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

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('35.200.39.68', 16002))

data = recvuntil(s, '\n').rstrip()
print data
test_ct = b64decode(data.split(': ')[1])

ct_body = test_ct[16:]
iv1 = test_ct[:16]
iv2 = iv1[:9] + strxor(strxor('test', 'show'), iv1[9:13]) + iv1[13:]

ct = b64encode(iv2 + ct_body)

data = recvuntil(s, ': ')
print data + ct
s.sendall(ct + '\n')
data = recvuntil(s, '\n').rstrip()
print data
data = recvuntil(s, '}')
print data

実行結果は以下の通り。

test Command: K0FUm+tgUeQz1SLEnBfhzrQ3nwbyDB8HmmUP7pUaVNS/u9pTLhQEu1M6E3Ag/V4dS2Eu/DXOWvjNCz1jL+UXICCY//ZxzPzPrOU7Esoxu+wd3KJYCpe8Jms58aGELPHcjjPM5DX7m7LpsoJ3O80yBqACoP/CHvalPd9hJFh8pCf6PKuamG/Kii97Vgtjp63qthSUeQE0IyKqLGOnimqd9fEsCKFCuuvH55xPxM0apu80PRmP3I0w3+uHj4t5Uld66IZi4w5HF5d1dCcfEWQXV6OOFacOZRV4bmcrH+teGEi1lEi4iW2tA2QWmr0GnvBxpl0+NIPJ+CCvMdn2hyEdPA==
Enter your command: K0FUm+tgUeQz0i/YnxfhzrQ3nwbyDB8HmmUP7pUaVNS/u9pTLhQEu1M6E3Ag/V4dS2Eu/DXOWvjNCz1jL+UXICCY//ZxzPzPrOU7Esoxu+wd3KJYCpe8Jms58aGELPHcjjPM5DX7m7LpsoJ3O80yBqACoP/CHvalPd9hJFh8pCf6PKuamG/Kii97Vgtjp63qthSUeQE0IyKqLGOnimqd9fEsCKFCuuvH55xPxM0apu80PRmP3I0w3+uHj4t5Uld66IZi4w5HF5d1dCcfEWQXV6OOFacOZRV4bmcrH+teGEi1lEi4iW2tA2QWmr0GnvBxpl0+NIPJ+CCvMdn2hyEdPA==
Command: showZzTosTPcUnBu44roqYGeyIjyJ22OoYp66c67bitZZthoRjCwTC9sW5jOFkVqW7WlMwJqI5Y+eJtxCkY4lkVkX3vzdDSkPtwB1FOrfHWPeGq4jOeprbrqhtcQzRdUsukyF1YdRzzZ8ezm2g82ydjFYcDrkfzk3ZQOzx0CpVulL80HGbwsB6amUMsmmVsC4jULWpg66++vj30B6p/aKGbh
The flag is: LINECTF{echidna_kawaii_and_crypto_is_difficult}
LINECTF{echidna_kawaii_and_crypto_is_difficult}

babycrypto3 (CRY)

公開鍵を見ると、以下のパラメータになっていることがわかる。

n = 31864103015143373750025799158312253992115354944560440908105912458749205531455987590931871433911971516176954193675507337
e = 65537

nをyafuで素因数分解する。

>yafu-x64.exe "factor(31864103015143373750025799158312253992115354944560440908105912458749205531455987590931871433911971516176954193675507337)" -v -threads 4


03/20/21 14:35:11 v1.34.5 @ SOPHIA, System/Build Info:
Using GMP-ECM 6.3, Powered by GMP 5.1.1
detected       Intel(R) Core(TM) i7-2670QM CPU @ 2.20GHz
detected L1 = 32768 bytes, L2 = 6291456 bytes, CL = 64 bytes
measured cpu frequency ~= 2210.531110
using 20 random witnesses for Rabin-Miller PRP checks

===============================================================
======= Welcome to YAFU (Yet Another Factoring Utility) =======
=======             bbuhrow@gmail.com                   =======
=======     Type help at any time, or quit to quit      =======
===============================================================
cached 78498 primes. pmax = 999983


>> fac: factoring 31864103015143373750025799158312253992115354944560440908105912458749205531455987590931871433911971516176954193675507337
fac: using pretesting plan: normal
fac: no tune info: using qs/gnfs crossover of 95 digits
div: primes less than 10000
fmt: 1000000 iterations
rho: x^2 + 3, starting 1000 iterations on C119
rho: x^2 + 2, starting 1000 iterations on C119
rho: x^2 + 1, starting 1000 iterations on C119
pm1: starting B1 = 150K, B2 = gmp-ecm default on C119
fac: setting target pretesting digits to 36.62
fac: sum of completed work is t0.00
fac: work done at B1=2000: 0 curves, max work = 30 curves
fac: 30 more curves at B1=2000 needed to get to t36.62
ecm: 30/30 curves on C119, B1=2K, B2=gmp-ecm default
fac: setting target pretesting digits to 36.62
fac: t15: 1.00
fac: t20: 0.04
fac: sum of completed work is t15.18
fac: work done at B1=11000: 0 curves, max work = 74 curves
fac: 74 more curves at B1=11000 needed to get to t36.62
ecm: 74/74 curves on C119, B1=11K, B2=gmp-ecm default
fac: setting target pretesting digits to 36.62
fac: t15: 7.17
fac: t20: 1.04
fac: t25: 0.05
fac: sum of completed work is t20.24
fac: work done at B1=50000: 0 curves, max work = 214 curves
fac: 214 more curves at B1=50000 needed to get to t36.62
ecm: 214/214 curves on C119, B1=50K, B2=gmp-ecm default, ETA: 0 sec
pm1: starting B1 = 3750K, B2 = gmp-ecm default on C119
fac: setting target pretesting digits to 36.62
fac: t15: 37.74
fac: t20: 11.23
fac: t25: 1.05
fac: t30: 0.07
fac: sum of completed work is t25.33
fac: work done at B1=250000: 0 curves, max work = 430 curves
fac: 430 more curves at B1=250000 needed to get to t36.62
ecm: 430/430 curves on C119, B1=250K, B2=gmp-ecm default, ETA: 2 sec
pm1: starting B1 = 15M, B2 = gmp-ecm default on C119
fac: setting target pretesting digits to 36.62
fac: t15: 123.74
fac: t20: 64.98
fac: t25: 9.65
fac: t30: 1.07
fac: t35: 0.09
fac: sum of completed work is t30.45
fac: work done at B1=1000000: 0 curves, max work = 904 curves
fac: 904 more curves at B1=1000000 needed to get to t36.62
ecm: 904/904 curves on C119, B1=1M, B2=gmp-ecm default, ETA: 5 sec
fac: setting target pretesting digits to 36.62
fac: t15: 425.07
fac: t20: 245.78
fac: t25: 54.85
fac: t30: 8.73
fac: t35: 1.09
fac: t40: 0.11
fac: sum of completed work is t35.56
fac: work done at B1=3000000: 0 curves, max work = 2350 curves
fac: 499 more curves at B1=3000000 needed to get to t36.62
ecm: 461/499 curves on C119, B1=3M, B2=gmp-ecm default, ETA: 9.4 min
ecm: found prp42 factor = 291664785919250248097148750343149685985101

fac: setting target pretesting digits to 24.00
fac: t15: 656.07
fac: t20: 399.78
fac: t25: 96.85
fac: t30: 17.28
fac: t35: 2.53
fac: t40: 0.31
fac: t45: 0.03
fac: sum of completed work is t36.54
pretesting / qs ratio was 136097.68
Total factoring time = 11836.5628 seconds


***factors found***

P42 = 291664785919250248097148750343149685985101
P78 = 109249057662947381148470526527596255527988598887891132224092529799478353198637

ans = 1

素因数分解できたので、あとはそのまま復号する。途中からbase64文字列になっているので、その部分をデコードする。

from Crypto.PublicKey import RSA
from Crypto.Util.number import *

with open('ciphertext.txt', 'rb') as f:
    c = bytes_to_long(f.read())

with open('pub.pem', 'r') as f:
    pub_data = f.read()

pubkey = RSA.importKey(pub_data)
n = pubkey.n
e = pubkey.e

p = 291664785919250248097148750343149685985101
q = 109249057662947381148470526527596255527988598887891132224092529799478353198637
assert p * q == n

phi = (p - 1) * (q - 1)
d = inverse(e, phi)
m = pow(c, d, n)
msg = long_to_bytes(m)
print '[+] msg =', msg

flag = msg.rstrip()[-32:].decode('base64').rstrip()
flag = 'LINECTF{%s}' % flag
print '[*] flag =', flag

実行結果は以下の通り。

[+] msg = `g・ヘヒa・Η・ Q0xPU0lORyBUSEUgRElTVEFOQ0UuCg==

[*] flag = LINECTF{CLOSING THE DISTANCE.}
LINECTF{CLOSING THE DISTANCE.}