IrisCTF 2023 Writeup

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

Sanity Check (Welcome)

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

irisctf{w31c0m3_t0_1r15ctf_2023}

Discord (Welcome)

Discordに入り、#miscチャネルのトピックを見ると、フラグが書いてあった。

irisctf{d15c0rd_c0nn3cts_y0u_t0_0ur_0rg4n1z3rs}

Meaning of Python 1 (Reverse Engineering)

スクリプトの処理概要は以下の通り。

・flag_to_check: 第一引数
・flag_length: flag_to_checkの長さ
・flag_lengthが44より小さい場合、NGで終了
・scramble1(flag_to_check)
 ※内部の処理結果を使用していないため、何もしていないのと同じ。
・flag_compressed = zlib.compress(flag_to_check.encode("utf-8"))
・flag_compressed_length: flag_compressedの長さ
・flag_compressed_lengthが52より小さい場合、NGで終了
・scramble2(flag_compressed)
 ※内部の処理結果を使用していないため、何もしていないのと同じ。
・flag_compressedがb'x\x9c\xcb,\xca,N.I\xab.\xc9\xc8,\x8e7,\x8eOIM3\xcc3,1\xce\xa9\x8c7\x89/\xa8,\xc90\xc8\x8bO\xcc)2L\xcf(\xa9\x05\x00\x83\x0c\x10\xf9'
 と一致していたら正しい。

最後の結果をdecompressするだけでよい。

#!/usr/bin/env python3
import zlib

flag_compressed = b'x\x9c\xcb,\xca,N.I\xab.\xc9\xc8,\x8e7,\x8eOIM3\xcc3,1\xce\xa9\x8c7\x89/\xa8,\xc90\xc8\x8bO\xcc)2L\xcf(\xa9\x05\x00\x83\x0c\x10\xf9'

flag = zlib.decompress(flag_compressed).decode()
print(flag)
irisctf{this_1s_def1n1t3ly_4_pyth0n_alr1ght}

Meaning of Python 2 (Reverse Engineering)

途中まで、難読化を解除すると、以下のようになる。

from builtins import *;
WWXWXWXXXXXWWWWXXWWXXWX,lIIIlIIIllIllIIllIII,LILJIJJIJJILLIIJJLL,S2S2SS2S222S2S2SS2222,wxwwxxwwwxxxxwxxxwxx=(lambda wxxwwxwxwxxxwwxwwwwxw:wxxwwxwxwxxxwwxwwwwxw(__import__('zlib'))),(lambda wxxwwxwxwxxxwwxwwwwxw:wxxwwxwxwxxxwwxwwwwxw['decompress']),(lambda wxxwwxwxwxxxwwxwwwwxw:globals()['eval'](globals()['compile'](globals()['str']("globals()['eval'](wxxwwxwxwxxxwwxwwwwxw)"),filename=nmnnnmnnnnmnnnnnmnnmnnmnn',mode='eval'))),(lambda DDDDDDOOooOoDoDooD,wxxwwxwxwxxxwwxwwwwxw:DDDDDDOOooOoDoDooD(wxxwwxwxwxxxwwxwwwwxw)),(lambda:(lambda wxxwwxwxwxxxwwxwwwwxw:globals()['eval'](globals()['compile'](globals()['str']("globals()['eval'](wxxwwxwxwxxxwwxwwwwxw)"),filename='nmnnnmnnnnmnnnnnmnnmnnmnn',mode='eval')))('__import__('builtins').exec'));

その後、b'x\x9c\xe4'から始まるデータ部をzlib.decompressすれば良さそうという推測ができる。

#!/usr/bin/env python3
import zlib

enc = b'x\x9c\xe4\xbd\xdbn\xe3\xc8\x92.|\xbf\x9eb\xdd\xa9\nc\x0f(\x9e\xb9\x80\xba3~@・・・(省略)・・・=\xfe\xe8\xd5s\xc2@zzx\xc6"\xff\xc7\xff\x01\t\xaa\x1b!'

dec = zlib.decompress(enc).decode()
print(dec)

結果以下のようになり、再び難読化コードになる。

        :
       (省略)
        :
        elif locals()[MNMNMMMNNNNNNNNNMNMNNM]==LJJJLLLIJLLIJJLJIJJIILJ:
            locals()[XWXXXWWXXWWWWWXWX]=nmnmnnnmmmnmnnnnmmmmmnnnn(locals()[IIllIllIllIlIlIlIlIIlllll],lIlIIlIlIlIlIIllIIIlIl,nmnnmmmmnmmmmnmmmnmnm)
            locals()[XXXXWXWXWXXXXXWWWXX]=NMMNMNMNNMMMMNMMMMMMNMNNN
        elif locals()[IIllIllIllIlIIlllI]==NMMNNMMMNNMNMNMNNMNMNNN:
            locals()[IlIIlllIIIlIIIIIIllII]=nmnmnnnmmmnmnnnnmmnmmnnnn(locals()[oODDooDoOOOoDODoDoODooD],IJILIIJJJJLLILIILJLI,JIIILLLJJJJJJJILJJJJLLII)
            locals()[MNMMNNMMNMMMNMMNMNN]=S2S2SS222S2SS222SS2
        elif locals()[IIllIllIIIIlIIIIIllI]==mmmnnmmmmmnmmnnnmmmnmnm:
            return
def main():
    if len(sys.argv)<=LIJIIILJLJJILIILLILJL:
        print(S22222S22S2SSS2S22SS2S22S2)
        exit(Oo00ooO0OOO00Oo0O0)
    locals()[SSS22SS222S222SSS2S]=sys.argv[DoOoODOoooDOoODoD]
    locals()[oDDDoOOoooOOoooOoODDO]=len(locals()[wxxwxwxxxxxwwxxxwxwxwxww])
    if locals()[S2222SSS2SSS22222S]<IIlIllIIlIlIIIlIIl:
        print(ODOOOoDDDDOOoooDo)
        exit(IIILLJIIIJIIJJLIJIJLJLILI)
    SS2SS2S22S22SSSSSS2SS22S2S(locals()[lljlililiiljliiij])
    locals()[S2S2SS222S22S222S22SSSS]=zlib.compress(locals()[mnmmmnnmnmnnmnnmmmmmmm].encode(O00Oo0OO00OOOOoo00OooOo0O))
    locals()[LJLJJLJIJIIJILLJJ]=len(locals()[OoO0O000000ooo0oOo0ooOO0O0])
    if locals()[xwwxxxwxxxxwwxwwxww]<wwxwxwxxwxwxxwxxwwwwx:
        print(WWWXXWXWXWWXWXWWXWXXWX)
        exit(ljijlljjiiiljjljjjiji)
    SS2SS2S22S22SSSSSS22S22S2S(locals()[SS2SS2SS22S222SSSS22SSS2])
    if locals()[SSSS222S22SS2222SS2S2S]==OO0oOO00oOo0O000oo0OOO0:
        print(XXWWWWXXWWWWWXWXW)
    else:
        print(nmnmnmnnnnmnnmnmnmmnnm)
main()

main関数の中のパラメータを標準出力しながら、コードを書き換える。

def main():
    if len(sys.argv)<=1:
        print("Missing argument")
        exit(1)
    locals()["nmnmnmnnmmnmmnmmmm"]=sys.argv[1]
    locals()["MNNMMNMNMNNMMNNMNMN"]=len(locals()["nmnmnmnnmmnmmnmmmm"])
    if locals()["MNNMMNMNMNNMMNNMNMN"]<44:
        print("Incorrect!")
        exit(1)
    SS2SS2S22S22SSSSSS2SS22S2S(locals()["nmnmnmnnmmnmmnmmmm"])
    locals()["NMMNMNMNMNNNNNNNNNNM"]=zlib.compress(locals()["nmnmnmnnmmnmmnmmmm"].encode("utf-8"))
    locals()["NMMNMNMNMMNNNNNNNNNM"]=len(locals()["NMMNMNMNMNNNNNNNNNNM"])
    if locals()["NMMNMNMNMMNNNNNNNNNM"]<56:
        print("Incorrect!")
        exit(1)
    SS2SS2S22S22SSSSSS22S22S2S(locals()["NMMNMNMNMNNNNNNNNNNM"])
    if locals()["NMMNMNMNMNNNNNNNNNNM"]==b'x\x9c\xcb,\xca,N.I\xab\xce\xa8,H-\xca\xcc\xcf\x8b7,\x8e\xcf3(\x89O\x8c/3.\xaa\x8cO70H\x897HJ+5M6)1(R\xac\x05\x00\xce\xff\x11\xdb':
        print("Correct!")
    else:
        print("Incorrect!")

最後の比較バイト文字列をzlib.decompressする。

#!/usr/bin/env python3
import zlib

flag_compressed = b'x\x9c\xcb,\xca,N.I\xab\xce\xa8,H-\xca\xcc\xcf\x8b7,\x8e\xcf3(\x89O\x8c/3.\xaa\x8cO70H\x897HJ+5M6)1(R\xac\x05\x00\xce\xff\x11\xdb'

flag = zlib.decompress(flag_compressed).decode()
print(flag)
irisctf{hyperion_1s_n0t_a_v3ry_g00d_0bfu5c4t0r!}

babystrechy (Web Exploitation)

$ nc stretchy.chal.irisc.tf 10704
== proof-of-work: enabled ==
please solve a pow first
You can run the solver with:
    python3 <(curl -sSL https://goo.gle/kctf-pow) solve s.ADQ6.AAAusC+HkKwoFsogo/cf6NgW
===================

Solution? s.AABcs2780SHoJWB+tOoePCMgzK96TY0BKE8b7pzQ+ANGYQQ0K0eh0u+naVKDcEta+gzt8zcH81PtBMLMG+xvZFYYpDxyNLXbzhY0XWjLIB5h46/uQncyKET0SXtevB0EHf7VXBNoyMxptfXndzQyvZoMiWAdzXPuup/0cfET50vFem4r4a8Agv7fAer3jrCVlVA3LoGOfQo/2b2MzRVOKTa9
Correct
Fear my 4096 byte password!
> 

phpスクリプトの処理概要は以下の通り。

・$password: ランダム64バイト文字列の16進数表記
・$stretched_password: $passwordの各文字を64個ずつ連結した文字列
・$h = password_hash($stretched_password, PASSWORD_DEFAULT);
・$line: 入力
・password_verify(trim($line), $h)がtrueの場合にフラグを表示

password_hash関数は最大72バイトで文字列を切り、ハッシュ値を取る。このため、以下の場合のブルートフォースで条件を満たすものを探す。

xxx.....xxxyyyyyyyy(16進文字 x を64バイト+16進文字 y を8バイト)
#!/usr/bin/env python3
import socket
import subprocess
import string

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(('stretchy.chal.irisc.tf', 10704))

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

cmdline = data.splitlines()[3].lstrip()
cmd = ['python3', 'pow.py']
cmd += cmdline.split(' ')[-2:]
sol = subprocess.check_output(cmd).decode()
print(sol)

data = recvuntil(s, b'? ')
print(data + sol)
s.sendall(sol.encode() + b'\n')
data = recvuntil(s, b'!\n').rstrip()
print(data)

found = False
for c1 in string.hexdigits[:16]:
    for c2 in string.hexdigits[:16]:
        password = c1 * 64 + c2 * 8
        data = s.recv(2).decode()
        if data == '> ':
            print(data + password)
            s.sendall(password.encode() + b'\n')
        else:
            found = True
            data += recvuntil(s, b'}')
            print(data)
            break
    if found:
        break

実行結果は以下の通り。

== proof-of-work: enabled ==
please solve a pow first
You can run the solver with:
    python3 <(curl -sSL https://goo.gle/kctf-pow) solve s.ADQ6.AAAR/vy+Kp2TLgkDdHqzdh8s
===================
Solution: 

s.AAAjxFawR0RZldfC+wpq19M5XFWgOlSTEWYOMIV7gTvDGI3ObRCq/JD5vEGrSRi2TVutbh4qmfilYSp0E7eixFBH99qDXF2kj+Re1QdCVSF5UUl/uOp3W8QIdTudUkzmTXKAa/slj++ORh9Y88Q3rqO0OH+tWI7XwBpnbUvritDTG08ES83LHF9S1pJMOhgXOM0+YU0BsJOmKKgEX169aBmX

Solution? s.AAAjxFawR0RZldfC+wpq19M5XFWgOlSTEWYOMIV7gTvDGI3ObRCq/JD5vEGrSRi2TVutbh4qmfilYSp0E7eixFBH99qDXF2kj+Re1QdCVSF5UUl/uOp3W8QIdTudUkzmTXKAa/slj++ORh9Y88Q3rqO0OH+tWI7XwBpnbUvritDTG08ES83LHF9S1pJMOhgXOM0+YU0BsJOmKKgEX169aBmX
Correct
Fear my 4096 byte password!
> 000000000000000000000000000000000000000000000000000000000000000000000000
> 000000000000000000000000000000000000000000000000000000000000000011111111
> 000000000000000000000000000000000000000000000000000000000000000022222222
> 000000000000000000000000000000000000000000000000000000000000000033333333
> 000000000000000000000000000000000000000000000000000000000000000044444444
> 000000000000000000000000000000000000000000000000000000000000000055555555
> 000000000000000000000000000000000000000000000000000000000000000066666666
> 000000000000000000000000000000000000000000000000000000000000000077777777
> 000000000000000000000000000000000000000000000000000000000000000088888888
> 000000000000000000000000000000000000000000000000000000000000000099999999
> 0000000000000000000000000000000000000000000000000000000000000000aaaaaaaa
> 0000000000000000000000000000000000000000000000000000000000000000bbbbbbbb
> 0000000000000000000000000000000000000000000000000000000000000000cccccccc
> 0000000000000000000000000000000000000000000000000000000000000000dddddddd
> 0000000000000000000000000000000000000000000000000000000000000000eeeeeeee
> 0000000000000000000000000000000000000000000000000000000000000000ffffffff
> 111111111111111111111111111111111111111111111111111111111111111100000000
> 111111111111111111111111111111111111111111111111111111111111111111111111
> 111111111111111111111111111111111111111111111111111111111111111122222222
> 111111111111111111111111111111111111111111111111111111111111111133333333
> 111111111111111111111111111111111111111111111111111111111111111144444444
> 111111111111111111111111111111111111111111111111111111111111111155555555
> 111111111111111111111111111111111111111111111111111111111111111166666666
> 111111111111111111111111111111111111111111111111111111111111111177777777
> 111111111111111111111111111111111111111111111111111111111111111188888888
> 111111111111111111111111111111111111111111111111111111111111111199999999
> 1111111111111111111111111111111111111111111111111111111111111111aaaaaaaa
> 1111111111111111111111111111111111111111111111111111111111111111bbbbbbbb
> 1111111111111111111111111111111111111111111111111111111111111111cccccccc
> 1111111111111111111111111111111111111111111111111111111111111111dddddddd
> 1111111111111111111111111111111111111111111111111111111111111111eeeeeeee
> 1111111111111111111111111111111111111111111111111111111111111111ffffffff
> 222222222222222222222222222222222222222222222222222222222222222200000000
> 222222222222222222222222222222222222222222222222222222222222222211111111
> 222222222222222222222222222222222222222222222222222222222222222222222222
> 222222222222222222222222222222222222222222222222222222222222222233333333
> 222222222222222222222222222222222222222222222222222222222222222244444444
> 222222222222222222222222222222222222222222222222222222222222222255555555
> 222222222222222222222222222222222222222222222222222222222222222266666666
> 222222222222222222222222222222222222222222222222222222222222222277777777
> 222222222222222222222222222222222222222222222222222222222222222288888888
> 222222222222222222222222222222222222222222222222222222222222222299999999
> 2222222222222222222222222222222222222222222222222222222222222222aaaaaaaa
> 2222222222222222222222222222222222222222222222222222222222222222bbbbbbbb
> 2222222222222222222222222222222222222222222222222222222222222222cccccccc
> 2222222222222222222222222222222222222222222222222222222222222222dddddddd
> 2222222222222222222222222222222222222222222222222222222222222222eeeeeeee
> 2222222222222222222222222222222222222222222222222222222222222222ffffffff
> 333333333333333333333333333333333333333333333333333333333333333300000000
        :
> 9999999999999999999999999999999999999999999999999999999999999999ffffffff
> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa00000000
> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa11111111
> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa22222222
> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa33333333
> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa44444444
> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa55555555
> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa66666666
> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa77777777
> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa88888888
> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa99999999
> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbb
> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacccccccc
> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaadddddddd
> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaeeeeeeee
irisctf{truncation_silent_and_deadly}
irisctf{truncation_silent_and_deadly}

babyshark (Networks)

httpでフィルタリングする。No.307のパケットで GET /babyshark.gifのレスポンスが返ってきているので、エクスポートする。エクスポートした画像にフラグが書いてあった。

irisctf{welc0m3_t0_n3tw0rks}

wi-the-fi (Networks)

WPAのキーをクラックする。

$ aircrack-ng -w dict/rockyou.txt BobertsonNet.cap 
Reading packets, please wait...
Opening BobertsonNet.cap
Read 20980 packets.

   #  BSSID              ESSID                     Encryption

   1  A0:28:ED:C3:CC:C1  BobertsonNet              WPA (1 handshake)

Choosing first network as target.

Reading packets, please wait...
Opening BobertsonNet.cap
Read 20980 packets.

1 potential targets



                               Aircrack-ng 1.6 

      [00:00:01] 13774/10303727 keys tested (23513.52 k/s) 

      Time left: 7 minutes, 17 seconds                           0.13%

                           KEY FOUND! [ billybob1 ]


      Master Key     : 84 F1 82 B2 91 7C D3 DD 26 B2 19 34 C0 7B 27 2F 
                       AC CF 32 B2 DF A3 56 01 9E FE 8C 7A 23 38 15 F8 

      Transient Key  : BA 3F 2A 6D D0 DB 35 F8 D4 60 FF BB EF CB B5 1A 
                       CE 60 FD 5F 58 D0 1F C4 3A EA 03 EA D4 DE 41 C9 
                       4B 0B 91 19 1F 47 2D E3 8A 26 19 85 69 89 37 F3 
                       2D 24 11 A4 C7 31 F9 D6 8F 6D 87 4F 17 87 01 9D 

      EAPOL HMAC     : 80 8E 84 12 3F EA 18 4E 2D 25 9A 19 10 54 E2 51 

Wiresharkの[編集]>[設定]から[Protocols]>[IEEE 802.11]の画面を表示後、Decryption keysの編集から以下を設定し、通信パケットを復号する。

・Key type: wpa-pwd
・Key     : billybob1

tcpでフィルタリングし、No.20422のパケットを見ると、TCPペイロードにフラグが入っていた。

irisctf{4ircr4ck_g0_brrrrrrrrrrrrrrr}

babyforens (Forensics)

JPEGの先頭10バイトが壊れているので、以下に書き換える。

FF D8 FF E0 00 10 4A 46 49 46


書き換えたJPG画像には以下のsecretの情報が書いてある。

exif_data_can_leak_a_lot_of_info

次にJPGのEXIFを見る。

$ exiftool IMG_0917_fix.jpg
    :
Date/Time Original              : 2022:08:27 10:04:56
Create Date                     : 2022:08:27 10:04:56
    :
Serial Number                   : 392075057288
    :
GPS Latitude                    : 37 deg 44' 49.46" N
GPS Longitude                   : 119 deg 35' 46.77" W
    :

緯度、経度は小数で小数点以下2桁に切り捨てた値が必要なので、Googleマップで以下を検索する。

37°44' 49.46" N, 119°35' 46.77" W
→37.747072, -119.596325

場所はUSのカリフォルニア州なので、GMT-8。このタイムゾーンで、2022-08-27 10:04:56 の Unixtime は 1661619896。

irisctf{37.74_-119.59_1661619896_392075057288_exif_data_can_leak_a_lot_of_info}

babynotrsa (Cryptography)

暗号は以下の計算で行っている。

encrypted = (flag * e) % n

flagは以下の計算で求めることができる。

flag = (encrypted * inverse(e, n)) % n
#!/usr/bin/env python3
from Crypto.Util.number import *

with open('output.txt', 'r') as f:
    params = f.read().splitlines()

n = int(params[0].split(' ')[-1])
e = int(params[1].split(' ')[-1])
encrypted = int(params[2].split(' ')[-1])

flag = (encrypted * inverse(e, n)) % n
flag = long_to_bytes(flag).decode()
print(flag)
irisctf{discrete_divide_isn't_hard}

babymixup (Cryptography)

暗号処理の概要は以下の通り。

・key: ランダム16バイト文字列
・flagの長さは16の倍数
・iv: ランダム16バイト文字列
・ivを表示
・以下の文字列をkey=iv, iv=keyでAES-CBC暗号化
 b"Hello, this is a public message. This message contains no flags."
・iv: ランダム16バイト文字列
・ivを表示
・flagをkey=key, iv=ivでAES-CBC暗号化

最初の暗号化で鍵がわかっているので、それからXORでIVを求める。あとはその情報を使ってフラグを復号する。

#!/usr/bin/env python3
from Crypto.Cipher import AES
from Crypto.Util.strxor import strxor

with open('output.txt', 'r') as f:
    params = f.read().splitlines()

IV1 = bytes.fromhex(params[0].split(' ')[-1])
CT1 = bytes.fromhex(params[1].split(' ')[-1])
IV2 = bytes.fromhex(params[2].split(' ')[-1])
CT2 = bytes.fromhex(params[3].split(' ')[-1])

PT1 = b"Hello, this is a public message. This message contains no flags."

tmp_key = b'\x00' * 16
cipher = AES.new(IV1, AES.MODE_CBC, tmp_key)
tmp_pt = cipher.decrypt(CT1)
assert tmp_pt[16:] == PT1[16:]

key = strxor(tmp_pt[:16], PT1[:16])

cipher = AES.new(key, AES.MODE_CBC, IV2)
flag = cipher.decrypt(CT2).decode()
print(flag)
irisctf{the_iv_aint_secret_either_way_using_cbc}

Nonces and Keys (Cryptography)

AES暗号OFBモードで暗号化されている。

OFBモードの場合以下のように暗号化される。
IV      --(AES暗号化)--> TMP_CT0、TMPCT0 ^ PT0 = CT0
TMP_CT0 --(AES暗号化)--> TMP_CT1、TMPCT1 ^ PT1 = CT1
TMP_CT1 --(AES暗号化)--> TMP_CT2、TMPCT2 ^ PT2 = CT2
        :

鍵が提示されているので、平文の先頭16バイト(PT1)と、暗号文(CT1, CT2, ...)がわかれば、TMP_CT1, TMP_CT2, ...も算出でき、復号できる。

#!/usr/bin/env python3
from Crypto.Cipher import AES
from Crypto.Util.strxor import strxor

key = (0x13371337133713371337133713371337).to_bytes(16, 'big')

with open('challenge_enc.sqlite3', 'rb') as f:
    enc = f.read()

pt0 = b'SQLite format 3\x00'

tmp_ct = strxor(pt0, enc[:16])

dec = pt0
for i in range(16, len(enc), 16):
    cipher = AES.new(key, mode=AES.MODE_ECB)
    tmp_ct = cipher.encrypt(tmp_ct)
    c = strxor(tmp_ct, enc[i:i+16])
    dec += c

with open('password.sqlite3', 'wb') as f:
    f.write(dec)

復号したsqlite3ファイルをDB Browser for SQLiteで見てみる。usersテーブルのnameがMichaelのレコードのpasswordにフラグが設定されていた。

irisctf{g0tt4_l0v3_s7re4mciph3rs}

SMarT 1 (Cryptography)

暗号の処理概要は以下の通り。

・SBOX: 256個の既知固定数値配列
・TRANSPOSE: 8個の既知固定数値配列の8個の配列
・RR: 8個の既知固定数値配列
・MASK: ランダム8バイト文字列
・KEYLEN = 12
・key: ランダム12バイト文字列
・MASK, keyを出力
・pairs = []
・以下8回繰り返し
 ・pt: ランダム8バイト文字列
 ・ptとencrypt(pt, key)の16進数表記の配列をpairsに追加
・pairsを出力
・ecb(FLAG, key).hex()を出力

鍵がわかっているので、逆算していけば、フラグを割り出せる。

#!/usr/bin/env python3
from Crypto.Util.strxor import strxor

SBOX = [99, 124, 119, 123, 242, 107, 111, 197, 48, 1, 103, 43, 254, 215, 171,
    118, 202, 130, 201, 125, 250, 89, 71, 240, 173, 212, 162, 175, 156, 164,
    114, 192, 183, 253, 147, 38, 54, 63, 247, 204, 52, 165, 229, 241, 113, 216,
    49, 21, 4, 199, 35, 195, 24, 150, 5, 154, 7, 18, 128, 226, 235, 39, 178,
    117, 9, 131, 44, 26, 27, 110, 90, 160, 82, 59, 214, 179, 41, 227, 47, 132,
    83, 209, 0, 237, 32, 252, 177, 91, 106, 203, 190, 57, 74, 76, 88, 207, 208,
    239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168, 81, 163,
    64, 143, 146, 157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210, 205, 12,
    19, 236, 95, 151, 68, 23, 196, 167, 126, 61, 100, 93, 25, 115, 96, 129, 79,
    220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94, 11, 219, 224, 50, 58, 10,
    73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121, 231, 200, 55, 109,
    141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174, 8, 186, 120, 37, 46,
    28, 166, 180, 198, 232, 221, 116, 31, 75, 189, 139, 138, 112, 62, 181, 102,
    72, 3, 246, 14, 97, 53, 87, 185, 134, 193, 29, 158, 225, 248, 152, 17, 105,
    217, 142, 148,155, 30, 135, 233, 206, 85, 40, 223, 140, 161, 137, 13, 191,
    230, 66, 104, 65, 153, 45, 15, 176, 84, 187, 22]

TRANSPOSE = [
    [3, 1, 4, 5, 6, 7, 0, 2],
    [1, 5, 7, 3, 0, 6, 2, 4],
    [2, 7, 5, 4, 0, 6, 1, 3],
    [2, 0, 1, 6, 4, 3, 5, 7],
    [6, 5, 0, 3, 2, 4, 1, 7],
    [2, 0, 6, 1, 5, 7, 4, 3],
    [1, 6, 2, 5, 0, 7, 4, 3],
    [4, 5, 6, 1, 2, 3, 7, 0]
]

RR = [4, 2, 0, 6, 9, 3, 5, 7]

ROUNDS = 2

def rev_rr(c, n):
    n = n % 8
    return ((c >> (8 - n)) | (c << n)) & 0xff

def decrypt(block, key):
    block = bytearray(block)

    for r in range(ROUNDS-1, -1, -1):
        block = strxor(block, MASK)

        temp = bytearray(8)
        for i in range(8):
            for j in range(8):
                temp[i] |= ((block[j] >> i) & 1) << TRANSPOSE[i][j]
        block = temp

        for i in range(8):
            block[i] = rev_rr(block[i], RR[i])
            block[i] = SBOX.index(block[i])

        block = strxor(block, key[r*4:(r+2)*4])

    return block

def rev_ecb(ct, key):
    out = b''
    for i in range(0, len(ct), 8):
        out += decrypt(ct[i:i+8], key)
    return out.rstrip(b'\x00')

with open('output1.txt', 'r') as f:
    params = f.read().splitlines()

MASK = bytes.fromhex(params[0].split(': ')[1])
key = bytes.fromhex(params[1].split(': ')[1])
ct = bytes.fromhex(params[3].split(': ')[1])

flag = rev_ecb(ct, key).decode()
print(flag)
irisctf{ok_at_least_it_works}