Nullcon Berlin HackIM 2023 CTF Writeup

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

Read the Rules (misc)

ルールのページを見たら、フラグの例が書かれていた。

ENO{th1s_is_4n_eXample}

babyrand (misc)

100回以下で、9個の32ビット乱数を見て、次の32ビット乱数を当てることができれば、フラグが表示される。Mersenne Twisterの問題と思われる。ただし、毎回10個目の乱数がわからない。Mersenne Twisterは、インデックス0、1、397の値がわかると、インデックス624の値を算出できる。つまりインデックス5、6、402の値がわかると、インデックス629の値を算出できる。このことを使って、63回目の推測時に正解を出すことができ、フラグが得られる。

#!/usr/bin/env python3
import socket
import random

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

def untemper(rand):
    rand ^= rand >> 18;
    rand ^= (rand << 15) & 0xefc60000;
 
    a = rand ^ ((rand << 7) & 0x9d2c5680);
    b = rand ^ ((a << 7) & 0x9d2c5680);
    c = rand ^ ((b << 7) & 0x9d2c5680);
    d = rand ^ ((c << 7) & 0x9d2c5680);
    rand = rand ^ ((d << 7) & 0x9d2c5680);
 
    rand ^= ((rand ^ (rand >> 11)) >> 11);
    return rand

def temper(st):
    y = st
    y ^= y >> 11
    y ^= (y << 7) & 0x9d2c5680
    y ^= (y << 15) & 0xefc60000
    y ^= y >> 18
    return y

def get_st624(st0, st1, st397):
    n = st0 & 0x80000000
    n += st1 & 0x7fffffff
    st624 = st397 ^ (n >> 1)
    if n % 2 != 0:
        st624 ^= 0x9908b0df
    return st624

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('52.59.124.14', 10011))

stats = []
for _ in range(62):
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    for i in range(9):
        data = recvuntil(s, b'\n').rstrip()
        print(data)
        stats.append(untemper(int(data)))
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    print('1')
    s.sendall(b'1\n')
    stats.append(1)

st629 = get_st624(stats[5], stats[6], stats[402])
resp = temper(st629)

data = recvuntil(s, b'\n').rstrip()
print(data)
for i in range(9):
    data = recvuntil(s, b'\n').rstrip()
    print(data)
data = recvuntil(s, b'\n').rstrip()
print(data)
print(str(resp))
s.sendall(str(resp).encode() + b'\n')
data = recvuntil(s, b'\n').rstrip()
print(data)

実行結果は以下の通り。

    :
Hints:
752697820
1260326397
2518899433
24091564
2878452702
3656174986
713195470
1648754691
4100648055
Guess:
1
Hints:
3648537072
3497416159
3633087672
2894461108
746236807
3004193502
2623530859
1416149530
1267855297
Guess:
4138154364
FLAG ENO{U_Gr4du4t3d_R4nd_4c4d3mY!}
ENO{U_Gr4du4t3d_R4nd_4c4d3mY!}

pythopia (rev)

PythonのASTで書かれているので、Pythonコードに起こしてみる。

import os

if not os.path.exists('license.txt'):
    exit('File does not exist.')

with open('license.txt') as f:
    key = f.read()

if not len(key) == 64:
    raise Exception('Wrong key')

key1 = key[:16]
key2 = key[16:32]
key3 = key[32:48]
key4 = key[48:64]

class KeyChkr:
    def __init__(self, k):
        self.k = k

    def check(self):
        k = self.k.upper().lower()
        if k == 'you_solved_it!!}':
            return True
        else:
            return False

ok = False

if key[0] == 'E':
    if key[1] == 'N':
        if key[2] == 'O':
            if key[3] == '{':
                if key[4] == 'L':
                    if key[5] == '1':
                        if key[6] == '3':
                            if key[7] == '3':
                                if key[8] == '3':
                                    if key[9] == '3':
                                        if key[10] == '3':
                                            if key[11] == '3':
                                                if key[12] == '3':
                                                    if key[13] == '3':
                                                        if key[14] == '3':
                                                            if key[15] == '3':
                                                                ok = True
                                                            else:
                                                                ok = False
                                                        else:
                                                            ok = False
                                                    else:
                                                        ok = False
                                                else:
                                                    ok = False
                                            else:
                                                ok = False
                                        else:
                                            ok = False
                                    else:
                                        ok = False
                                else:
                                    ok = False
                            else:
                                ok = False
                        else:
                            ok = False
                    else:
                        ok = False
                else:
                    ok = False
            else:
                ok = False
        else:
            ok = False
    else:
        ok = False
else:
    ok = False

vals = [36, 76, 96, 102, 99, 118, 97, 76, 119, 102, 99, 118, 97, 76, 124, 120]

for i, k in enumerate(key2):
    v = ord(key2[i]) ^ 19
    if v != vals[i]:
         ok = False

def check_key(k):
    if k[::-1] != '_!ftcnocllunlol_':
        return False
    return True

if not check_key(key3):
    ok = False

chk = KeyChkr(key4)
ok = chk.check()
if ok:
    print('Correct license!')
else:
    print('Nope')

このコードからkey1~key4を求め、結合してフラグとなる。

#!/usr/bin/env python3
key1 = 'ENO{L13333333333'

vals = [36, 76, 96, 102, 99, 118, 97, 76, 119, 102, 99, 118, 97, 76, 124, 120]
key2 = ''.join([chr(v ^ 19) for v in vals])
key3 = '_!ftcnocllunlol_'[::-1]
key4 = 'you_solved_it!!}'
key = key1 + key2 + key3 + key4
print(key)
ENO{L133333333337_super_duper_ok_lolnullconctf!_you_solved_it!!}

reguest (web)

クッキーの"role"が"admin"で、"really"が"yes"の場合にフラグが表示される。

$ curl http://52.59.124.14:10014/ -b "role=admin" -b "really=yes"
Usage: Look at the code ;-)

Overwriting cookies with default value! This must be secure!
Prepared request cookies are: [('role', 'guest'), ('really', 'yes')]
Sending request...
Request cookies are: [('role', 'guest'), ('really', 'yes')]

Someone's drunk oO

Response is: Admin: ENO{R3Qu3sts_4r3_s0m3T1m3s_we1rd_dont_get_confused}
ENO{R3Qu3sts_4r3_s0m3T1m3s_we1rd_dont_get_confused}

bmpass (crypto)

AES-ECB暗号化なので、16バイトごとに同じデータは同じ暗号になる。BMP前提で、BMPヘッダのみ付け画像にする。

0000-0001 "BM"
0002-0005 サイズ(= 90 a8 1f 00)
0006-0009 00 00 00 00
000a-000d オフセット(適当に36 00 00 00で設定)
000e-0011 28 00 00 00
0012-0015 画像の幅
0016-0019 画像の高さ
001a-001b 01 00
001c-001d 1画素当たりのデータサイズ(= RGBA = 20 00)
001e-0035 仮で00で埋める。

幅がわからないので、ブルートフォースで読めそうな画像を探す。

#!/usr/bin/env python3
with open('flag.bmp.enc', 'rb') as f:
    data = f.read()

head = b'BM'
head += b'\x90\xa8\x1f\x00'
head += b'\x00\x00\x00\x00'
head += b'\x36\x00\x00\x00'
head += b'\x28\x00\x00\x00'
tail = (512).to_bytes(4, 'little')
tail += b'\x01\x00\x18\x00'
tail += b'\x00' * 24
tail += data[0x36:0x40]
for i in range(0x40, len(data), 16):
    d = data[i:i+16]
    if d == b'\x6b\xe1\x48\x5d\xc7\xd9\x3a\x14\xc7\x18\x85\x5a\x80\xa1\x9a\x87':
        tail += b'\x00' * 16
    else:
        tail += b'\xff' * 16

for i in range(1, 2049):
    fname = 'img/flag_%04d.bmp' % i
    w = (i).to_bytes(4, 'little')
    d = head + w + tail

    with open(fname, 'wb') as f:
        f.write(d)

推測しつつ、幅1280の画像からフラグが読めそう。

ENO{I_c4N_s33_tHr0ugH_3ncrYpt10n}

twin (crypto)

nが同じで、異なるeでフラグを暗号化している。Common Modulus Attackで復号する。ただし、e1とe2のGCDは17であるため、e1, e2を17で割った値で復号し、Low Public-Exponent Attackで復号する。

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

def commom_modulus_attack(c1, c2, e1, e2, n):
    gcd, s1, s2 = gmpy2.gcdext(e1, e2)
    if s1 < 0:
        s1 = -s1
        c1 = gmpy2.invert(c1, n)
    elif s2 < 0:
        s2 = -s2
        c2 = gmpy2.invert(c2, n)

    v = pow(c1, s1, n)
    w = pow(c2, s2, n)
    x = (v*w) % n
    return x

key1 = RSA.import_key(open('key1.pem','rb').read())
key2 = RSA.import_key(open('key2.pem','rb').read())

assert key1.n == key2.n
n = key1.n
e1 = key1.e
e2 = key2.e
e = GCD(e1, e2)
e1 = e1 // e
e2 = e2 // e

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

c1 = int(params[0])
c2 = int(params[1])

m = commom_modulus_attack(c1, c2, e1, e2, n)
m, success = gmpy2.iroot(m, e)
assert success == True
flag = long_to_bytes(m).decode()
print(flag)
ENO{5har1ng_is_n0t_c4r1ng}