Azure Assassin Alliance CTF 2022 Writeup

この大会は2022/6/25 10:00(JST)~2022/6/27 10:00(JST)に開催されました。
今回もチームで参戦。結果は980点で310チーム中53位でした。
自分で解けた問題をWriteupとして書いておきます。

signin (Misc)

$ file flag
flag: bzip2 compressed data, block size = 900k
$ mv flag flag.bz2
$ bzip2 -d flag.bz2
$ file flag
flag: XZ compressed data
$ xz -d flag.xz 
xz: flag: ファイルのパーミッションを設定できません: 定義されたデータ型に対して値が大きすぎます
$ file flag
flag: bzip2 compressed data, block size = 900k
$ mv flag flag.bz2
$ bzip2 -d flag.bz2
$ file flag
flag: LZMA compressed data, streamed
$ mv flag flag.lzma
$ xz --format=lzma --decompress flag.lzma
xz: flag: ファイルのパーミッションを設定できません: 定義されたデータ型に対して値が大きすぎます
$ file flag
flag: gzip compressed data, was "flag", last modified: Thu Jun 23 10:23:44 2022, from Unix
$ mv flag flag.gz
$ gzip -d flag.gz
$ file flag
flag: LZMA compressed data, streamed
$ mv flag flag.lzma
$ xz --format=lzma --decompress flag.lzma
xz: flag: ファイルのパーミッションを設定できません: 定義されたデータ型に対して値が大きすぎます
$ file flag
flag: gzip compressed data, was "flag", last modified: Thu Jun 23 10:23:44 2022, from Unix
        :

さまざまな圧縮方式で何重にも圧縮されているので、fileコマンドで確認しながら、解凍していく。

#!/usr/bin/env python3
import subprocess
import os
import time

cmd_file = 'file %s'
cmd_bz2 = 'bzip2 -d %s'
cmd_xz = 'xz -d %s'
cmd_lzma = 'xz --format=lzma --decompress %s'
cmd_gz = 'gzip -d %s'
cmd_zst = 'zstd -d %s'

filename_flag = 'flag'
filename_bz2 = 'flag.bz2'
filename_xz = 'flag.xz'
filename_lzma = 'flag.lzma'
filename_gz = 'flag.gz'
filename_zst = 'flag.zst'

i = 1
while True:
    print('%d times' % i)
    cmd = cmd_file % filename_flag
    ret = subprocess.check_output(cmd.split(' ')).rstrip().decode()
    print(ret)
    if 'bzip2 compressed' in ret:
        os.rename(filename_flag, filename_bz2)
        os.system(cmd_bz2 % filename_bz2)
    elif 'XZ compressed' in ret:
        os.rename(filename_flag, filename_xz)
        os.system(cmd_xz % filename_xz)
    elif 'LZMA compressed' in ret:
        os.rename(filename_flag, filename_lzma)
        os.system(cmd_lzma % filename_lzma)
    elif 'gzip compressed' in ret:
        os.rename(filename_flag, filename_gz)
        os.system(cmd_gz % filename_gz)
    elif 'Zstandard compressed' in ret:
        os.rename(filename_flag, filename_zst)
        os.system(cmd_zst % filename_zst)
    else:
        break
    time.sleep(2)
    i += 1

実行結果は以下の通り。

        :
1017 times
flag: XZ compressed data
xz: flag: ファイルのパーミッションを設定できません: 定義されたデータ型に対して値が大きすぎます
1018 times
flag: bzip2 compressed data, block size = 900k
1019 times
flag: gzip compressed data, was "flag", last modified: Thu Jun 23 10:23:44 2022, from Unix
1020 times
flag: Zstandard compressed data (v0.8+), Dictionary ID: None
flag.zst            : 259 bytes                                                
1021 times
flag: Zstandard compressed data (v0.8+), Dictionary ID: None
flag.zst            : 246 bytes                                                
1022 times
flag: bzip2 compressed data, block size = 900k
1023 times
flag: bzip2 compressed data, block size = 900k
1024 times
flag: XZ compressed data
xz: flag: ファイルのパーミッションを設定できません: 定義されたデータ型に対して値が大きすぎます
1025 times
flag: ASCII text
$ cat flag
ACTF{r0cK_4Nd_rolL_1n_C0mpr33s1ng_aNd_uNCOmrEs5iNg}
ACTF{r0cK_4Nd_rolL_1n_C0mpr33s1ng_aNd_uNCOmrEs5iNg}

Mahjoong (Misc)

麻雀ゲームができるようなので、普通に勝負してみる。1勝したが、特にフラグは表示されない。継続して、もう一度勝負してみる。フラグ表示の条件はわからないが、ゲーム中にフラグが表示された。

ACTF{y@kumAn_1s_incredl3le}

impossible RSA (Crypto)

コードにあるパラメータの条件から、式を変形する。

e * q = p * A + 1
    ↓
e * p * q = p * p * A + p
    ↓
A*p**2 + p - e*n = 0

Aはそれほど大きい数値でないと推測できるので、Aのブルートフォース2次方程式の解が整数になるものを探し、p, qを求める。あとは通常通り復号しフラグを求める。

#!/usr/bin/env python3
from Crypto.Util.number import *
from Crypto.PublicKey import RSA
from sympy import *

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

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

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

A = 1
found = False
while True:
    p = symbols('p')
    eq = Eq(A * p ** 2 + p - e * n, 0)
    sol = solve(eq, p)
    for p in sol:
        if p.is_Integer:
            found = True
            print('[+] A =', A)
            break
    if found:
        break
    A += 1

p = int(p)
q = n // p
assert p * q == n

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

実行結果は以下の通り。

[+] A = 46280
[+] p = 150465840847587996081934790667651610347742504431401795762471467800785876172317705268993152743689967775266712089661128372295606682852482012493939368044600366794969553828079064622047080051569090177885299781981209120854290564064662058027679075401901717932024549311396484660557278975525859127898004619405319768113
[+] q = 106253858346069738600667441477316882476975191191010804704017265511396163224664897689076447029585908855140507431062102645373463498213419404889139172575859514095414665779078979976323891310048026205540865067215318951327289428947198682355325809994354509756230772573224732747769822710641878029801786071777441733193
[*] flag = ACTF{F1nD1nG_5pEcia1_n_i5_nOt_eA5y}
ACTF{F1nD1nG_5pEcia1_n_i5_nOt_eA5y}

secure connection (Crypto)

master.txtの内容と合わせ、サーバの処理概要を考える。

・handler = connection_handle_socket(s, "master", args.dump)
 送受信のオブジェクト生成(ダンプファイルは"master.txt")
・connection_engine(handler, "master", args.encrypt)
 ・state = connection_state("master", args.encrypt)
  ・state.role = "master"
  ・state.local_counter = 0
  ・state.remote_counter = 0
  ・state.encrypt = args.encrypt
  ・state.initCRC = b""
 ・master_hello_procedure(handler, state)
  ・handler.send(state.prepare_hello_packet())
   ・hello_packet = int(state.encrypt << 7 | 1).to_bytes(1, "little")
   ・hello_packet += "\x03"
   ・state.initCRC = ランダム3バイト
   ・hello_packet += state.initCRC
   ・hello_packet += state.calc_crc(hello_packet)
   →helloパケット送信
  ・state.inc_local_counter()
   ・state.local_counter += 1
  ・hello_pkt = handler.recv(2 + 255 + 3)
   →helloパケット受信
  ・state.inc_remote_counter()
   ・state.remote_counter += 1
  ・encrypt_or_not = (hello_pkt[0] >> 7) & 0b1
   →暗号化しない場合、True返却
  ・handler.send(state.prepare_sc_request_packet())
   ・sc_request_packet += int(128 | 2).to_bytes(1, "little")
   ・sc_request_packet += "\x10"
   ・IV: ランダム8バイト
   ・Secret: ランダム8バイト
   ・state.IVm = IV
   ・state.Secretm = Secret
   ・sc_request_packet += IV
   ・sc_request_packet += Secret
   ・sc_request_packet += state.calc_crc(sc_request_packet)
  ・state.inc_local_counter()
   ・state.local_counter += 1
  ・numeric_key: 入力
  ・numeric_key = numeric_key % 0x1000000
  ・state.numeric_key = numeric_key
  ・state.numeric_key_bytes = (numeric_key).to_bytes(16, "little")
  ・sc_respond_pkt: パケット受信
  ・state.inc_remote_counter()
   ・state.remote_counter += 1
  ・sc_respond_pkt[0] & 0x3fと3が一致していない場合、Falseを返す。
  ・recvIVs = sc_respond_pkt[2:10]
  ・recvSecrets = sc_respond_pkt[10:18]
  ・crc = sc_respond_pkt[18: 18 + 3]
  ・should_crc = state.calc_crc(sc_respond_pkt[:18])
  ・crcとshould_crcが一致していない場合、Falseを返す。
  ・handler.send(state.prepare_master_confirm_packet())
   ・master_confirm_packet += int(128 | 4).to_bytes(1, "little")
   ・master_confirm_packet += "\x10"
   ・master_random: ランダム16バイト
   ・master_confirm = secure_confirm(
        state.numeric_key_bytes, master_random, b"\x00" * 16, b"\xff" * 16)
    ・secure_encrypt(state.numeric_key_bytes, 
         bytes_xor_16(secure_encrypt(state.numeric_key_bytes, master_random), b"\xff" * 16))
   ・state.MRandom = master_random
   ・state.MConfirm = master_confirm
   ・master_confirm_packet += master_confirm
   ・master_confirm_packet += state.calc_crc(master_confirm_packet)
  ・state.inc_local_counter()
   ・state.local_counter += 1
  ・sconfirm_pkt: パケット受信
  ・state.inc_remote_counter()
   ・state.remote_counter += 1
  ・sconfirm_pkt[0] & 0x3fと5が一致していない場合、Falseを返す。
  ・recvSConfirm = sconfirm_pkt[2:18]
  ・crc = sconfirm_pkt[18:18 + 3]
  ・should_crc = state.calc_crc(sconfirm_pkt[:18])
  ・crcとshould_crcが一致していない場合、Falseを返す。
  ・state.SConfirm = recvSConfirm
  ・handler.send(state.prepare_master_random_packet())
   ・master_random_packet += int(128 | 6).to_bytes(1, "little")
   ・master_confirm_packet += "\x10"
   ・master_random_packet += state.MRandom
   ・master_random_packet += state.calc_crc(master_random_packet)
  ・state.inc_local_counter()
   ・state.local_counter += 1
  ・srandom_pkt: パケット受信
  ・state.inc_remote_counter()
   ・state.remote_counter += 1
  ・srandom_pkt[0] & 0x3fと7が一致していない場合、Falseを返す。
  ・recvSRandom = srandom_pkt[2:18]
  ・crc = srandom_pkt[18:18 + 3]
  ・should_crc = state.calc_crc(srandom_pkt[:18])
  ・crcとshould_crcが一致していない場合、Falseを返す。
  ・state.check_slave_confirm(recvSRandom)がFalseの場合、Falseを返す。
   ・should_Sconfirm = secure_confirm(
        state.numeric_key_bytes, recvSRandom, b"\x00" * 16, b"\xff" * 16)
   ・state.SConfirmとshould_Sconfirmが一致していない場合、Falseを返す。
  ・state.setup_session()
   ・state.storekey = secure_encrypt(
        state.numeric_key_bytes, state.MRandom[:8] + state.SRandom[8:])
        self.sessionkey = secure_encrypt(
            state.storekey, state.Secretm + state.Secrets)
 ・以下繰り返し
  ・メニュー選択
  ・0を選択した場合、recieve_message(state, handler)
   ・dataheader: 2バイト受信
   ・more_data = (dataheader[0] >> 6) & 0b1
   ・opcode = dataheader[0] & 0x3f(PktOpcode.DATA.value)
   ・datalength = dataheader[1]
   ・payload = handler.recv(datalength)
   ・crc = handler.recv(3)
   ・should_crc = state.calc_crc(dataheader + payload)
   ・payload = state.decrypt_data_packet(payload)
   ・state.inc_remote_counter()
   ・crcとshould_crcが異なる場合はエラー
   ※more_dataが0になるまで、payloadを結合する。
  ・1を選択した場合、send_message(state, handler)
   ・data: 入力
   ・encoded_data: dataのbase64エンコード
   ・encoded_data_len: encoded_dataの長さ
   ・encoded_dataを255バイトごとに以下を実行
    ・moreData: 次のデータの有無(True / False)
    ・data_segment: encoded_dataの対象の255バイト
    ・data_packet = state.prepare_data_packet(data_segment, moreData)
     ・data_packet = b""
     ・data_packet += int(state.encrypt << 7 | moredata << 6 | 8).to_bytes(1, "little")
     ・data_packet += len(data_segment).to_bytes(1, "little")
     ・state.encryptがTrueの場合、
      data_packet += secure_encrypt_packet(state.sessionkey, data_segment,
                  (state.local_counter).to_bytes(13, "little")
      ・AES-CCM暗号化(nonce=state.local_counter)
     ・state.encryptがFalseの場合、
      data_packet += data_segment
     ・data_packet += state.calc_crc(data_packet)
     →データパケット送信
    ・state.inc_local_counter()
     ・state.local_counter += 1

各種パラメータを割り出していく。その際numeric_keyについては0x1000000未満なので、state.MRandomの暗号とstate.MConfirmが同じになるものを探す。あとはそのパラメータを使って、送信データのみ復号する。

#!/usr/bin/env python3
import base64
from Crypto.Cipher import AES
import libscrc

def bytes_xor_16(bytes1, bytes2):
    v1 = int.from_bytes(bytes1, 'big')
    v2 = int.from_bytes(bytes2, 'big')
    v3 = v1 ^ v2
    return (v3).to_bytes(16, 'big')

def secure_encrypt(key, plain):
    aes = AES.new(key=key, mode=AES.MODE_ECB)
    return aes.encrypt(plain)

def secure_decrypt_packet(key, plain, nonce):
    aes = AES.new(key=key, mode=AES.MODE_CCM, nonce=nonce)
    return aes.decrypt(plain)

def secure_confirm(key, r, p1, p2):
    return secure_encrypt(key, bytes_xor_16(secure_encrypt(key, bytes_xor_16(r, p1)), p2))

def calc_crc(initCRC, pdu):
    initvalue = int.from_bytes(initCRC, "little")
    crc = libscrc.hacker24(data=pdu, poly=0x00065B, init=initvalue,
                           xorout=0x00000000, refin=True, refout=True)
    return crc.to_bytes(3, "little")

with open('master.txt', 'r') as f:
    lines = f.read().splitlines()

######## 1st communication data ########
dirs = []
data = []
for line in lines[:31]:
    dir = line.rstrip().split('\t')[0]
    dat = line.rstrip().split('\t')[1].split(' ')
    d = b''.join([bytes([int(d, 16)]) for d in dat])
    if dir != '':
        dirs.append(dir.encode())
        data.append(d)
    else:
        data[-1] = data[-1] + d

######## 1st hello packet ########
assert dirs[0] == b'>'
assert data[0][0] & 1 == 1
encrypt = (data[0][0] >> 7) & 1
assert encrypt == False
assert data[0][1] == 3
initCRC = data[0][2:5]
assert data[0][5:8] == calc_crc(initCRC, data[0][0:5])
assert dirs[1] == b'<'
assert data[1] == data[0]

######## 1st data packet (not encrypted) ########
for i in range(3):
    assert dirs[i*4+2] == b'>'
    assert data[i*4+2][0] & 8 == 8
    encrypt = (data[i*4+2][0] >> 7) & 1
    assert encrypt == False
    more_data =  (data[i*4+2][0] >> 6) & 1
    assert more_data == False
    length = data[i*4+2][1]
    payload = data[i*4+2][2:2+length]
    assert data[i*4+2][2+length:2+length+3] == calc_crc(initCRC, data[i*4+2][0:2+length])
    inp_data = base64.b64decode(payload).decode()
    print(inp_data)

    if i == 2:
        break

    assert dirs[i*4+3] == b'<'
    more_data = (data[i*4+3][0] >> 6) & 0b1
    assert more_data == False
    opcode = data[i*4+3][0] & 0x3f
    assert opcode == 8
    length = data[i*4+3][1]

    assert dirs[i*4+4] == b'<'
    payload = data[i*4+4]

    assert dirs[i*4+5] == b'<'
    crc = data[i*4+5]

    assert data[i*4+5] == calc_crc(initCRC, data[i*4+3] + data[i*4+4])
    inp_data = base64.b64decode(payload).decode()
    print(inp_data)

######## 2nd hello packet ########
local_counter = 0
remote_counter = 0
assert dirs[11] == b'>'
assert data[11][0] & 1 == 1
encrypt = (data[11][0] >> 7) & 1
assert encrypt == True
assert data[11][1] == 3
initCRC = data[11][2:5]
assert data[11][5:8] == calc_crc(initCRC, data[11][0:5])
assert dirs[12] == b'<'
assert data[12] == data[11]
local_counter += 1
remote_counter += 1

assert dirs[13] == b'>'
assert data[13][0] == 128 | 2
assert data[13][1] == 16
IVm = data[13][2:10]
Secretm = data[13][10:18]
assert data[13][18:21] == calc_crc(initCRC, data[13][0:18])
local_counter += 1
remote_counter += 1

assert dirs[14] == b'<'
assert data[14][0] & 0x3f == 3
IVs = data[14][2:10]
Secrets = data[14][10:18]
assert data[14][18:21] == calc_crc(initCRC, data[14][0:18])

assert lines[31].rstrip().split('\t')[0].encode() == b'>'

data15_1 = lines[31].rstrip().split('\t')[1].split(' ')
data15_2 = lines[32].rstrip().split('\t')[1].split(' ')
d15_1 = b''.join([bytes([int(d, 16)]) for d in data15_1])
d15_2 = b''.join([bytes([int(d, 16)]) for d in data15_2[2:]])

assert d15_1[0] == 128 | 4
assert d15_1[1] == 16

for x in range(65536):
    X = x.to_bytes(2, "little")
    crc = calc_crc(initCRC, d15_1 + X)
    if crc == d15_2:
        break

d15 = d15_1 + X + d15_2
MConfirm = d15[2:18]
local_counter += 1
remote_counter += 1

assert lines[33].rstrip().split('\t')[0].encode() == b'<'

data16_1 = lines[33].rstrip().split('\t')[1].split(' ')
data16_2 = lines[34].rstrip().split('\t')[1].split(' ')

assert int(data16_1[0], 16) & 0x3f == 5

assert lines[35].rstrip().split('\t')[0].encode() == b'>'

data17_1 = lines[35].rstrip().split('\t')[1].split(' ')
data17_2 = lines[36].rstrip().split('\t')[1].split(' ')
d17_1 = b''.join([bytes([int(d, 16)]) for d in data17_1])
d17_2 = b''.join([bytes([int(d, 16)]) for d in data17_2])
d17 = d17_1 + d17_2

assert d17[0] == 128 | 6
assert d17[1] == 16
MRandom = d17[2:18]
assert d17[18:] == calc_crc(initCRC, d17[0:18])
local_counter += 1
remote_counter += 1

#for numeric_key in range(0x1000000):
for numeric_key in range(9190693, 0x1000000):
    numeric_key_bytes = (numeric_key).to_bytes(16, "little")
    master_confirm = secure_confirm(numeric_key_bytes,
        MRandom, b"\x00" * 16, b"\xff" * 16)
    if master_confirm == MConfirm:
        break

print('\n[+] numeric_key:', numeric_key)
print()
numeric_key_bytes = (numeric_key).to_bytes(16, "little")

assert lines[37].rstrip().split('\t')[0].encode() == b'<'

data18_1 = lines[37].rstrip().split('\t')[1].split(' ')
data18_2 = lines[38].rstrip().split('\t')[1].split(' ')
d18_1 = b''.join([bytes([int(d, 16)]) for d in data18_1[:14]])
d18_2 = bytes([int(data18_1[15], 16)])
d18_3 = b''.join([bytes([int(d, 16)]) for d in data18_2])

assert d18_1[0] & 0x3f == 7

for x in range(256):
    X = x.to_bytes(1, "little")
    crc = calc_crc(initCRC, d18_1 + X + d18_2 + d18_3[:2])
    if crc == d18_3[2:]:
        break

d18 = d18_1 + X + d18_2 + d18_3
recvSRandom = d18[2:18]
SConfirm = secure_confirm(numeric_key_bytes,
    recvSRandom, b"\x00" * 16, b"\xff" * 16)

storekey = secure_encrypt(numeric_key_bytes, MRandom[:8] + recvSRandom[8:])
sessionkey = secure_encrypt(storekey, Secretm + Secrets)

######## 2nd communication data (only send message) ########
dirs = []
data = []
for line in lines[39:]:
    dir = line.rstrip().split('\t')[0]
    dat = line.rstrip().split('\t')[1].split(' ')
    d = b''.join([bytes([int(d, 16)]) for d in dat])
    if dir != '':
        dirs.append(dir.encode())
        data.append(d)
    else:
        data[-1] = data[-1] + d

send_data = []
for i in range(len(dirs)):
    if dirs[i] == b'>':
        send_data.append(data[i])

######## 2nd data packet (encrypted) (only send message) ########
i = 0
while i < len(send_data):
    payload = b''
    while True:
        assert send_data[i][0] & 8 == 8
        encrypt = (send_data[i][0] >> 7) & 1
        assert encrypt == True
        more_data = (send_data[i][0] >> 6) & 1
        length = send_data[i][1]
        body = send_data[i][2:2+length]
        body = secure_decrypt_packet(sessionkey, body,
            (local_counter).to_bytes(13, "little"))
        payload += body

        i += 1
        local_counter += 1
        if more_data == False:
            break

    inp_data = base64.b64decode(payload).decode()
    print(inp_data)

実行結果は以下の通り。

Hello there, long time no see, zraxx
yeah, I am quite busy making ACTF crypto challenges
well, I can offer you a not bad signin challenge
show me
let's first dive into secure connection

[+] numeric_key: 9190693

I will tell you my flag after you finish your poem
No I mean this one, I never saw a Moor-I never saw the Sea-Yet know I how the Heather looksAnd what a Billow be.I never spoke with GodNor visited in Heaven-Yet certain am I of the spotAs if the Checks were given-
You got your flag: ACTF{ShORt_NUmeR1c_KEY_1s_Vuln3R4bLe_TO_e@V3sDropPEr}
ACTF{ShORt_NUmeR1c_KEY_1s_Vuln3R4bLe_TO_e@V3sDropPEr}

signoff (Misc)

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

ACTF{YOu_4Re_4WEsOme_3njoy_tHE_v4cAT1on}