WaniCTF 2024 脆弱Writeup

2024/06/24に公開

結果

Wani CTF 2024をチームで参加してきました。
36位/1475チームでした!

解けなかった問題も含めて振り返ります。(追記予定)

Crypto

beginners_rsa (121pt 530 solves Beginner)✅

問題

RSAの素数が5つあるという問題。

p = getPrime(64)
q = getPrime(64)
r = getPrime(64)
s = getPrime(64)
a = getPrime(64)
n = p*q*r*s*a
e = 0x10001

FLAG = b'FLAG{This_is_a_fake_flag}'
m = bytes_to_long(FLAG)
enc = pow(m, e, n)
n = 317903423385943473062528814030345176720578295695512495346444822768171649361480819163749494400347
e = 65537
enc = 127075137729897107295787718796341877071536678034322988535029776806418266591167534816788125330265

解法

factordbでnを素因数分解できた。

p = 9953162929836910171
q = 11771834931016130837
r = 12109985960354612149
s = 13079524394617385153
a = 17129880600534041513

素数5つのバージョンなど知らなかったが、勘でRSAの逆算をさせてみたらできてしまった。

phi = (p - 1) * (q - 1) * (r - 1) * (s - 1) * (a - 1)
d = pow(e, -1, phi)
m = pow(enc, d, n)
flag = long_to_bytes(m)

FLAG{S0_3a5y_1254!!}

beginners_aes (125pt 453 solves Beginner)✅

問題

AESのkeyとivの最後の一文字がランダムでわからないという問題。

key = b'the_enc_key_is_'
iv = b'my_great_iv_is_'
key += urandom(1)
iv += urandom(1)

cipher = AES.new(key, AES.MODE_CBC, iv)
FLAG = b'FLAG{This_is_a_dummy_flag}'
flag_hash = hashlib.sha256(FLAG).hexdigest()

msg = pad(FLAG, 16)
enc = cipher.encrypt(msg)
enc = b'\x16\x97,\xa7\xfb_\xf3\x15.\x87jKRaF&"\xb6\xc4x\xf4.K\xd77j\xe5MLI_y\xd96\xf1$\xc5\xa3\x03\x990Q^\xc0\x17M2\x18'
flag_hash = 6a96111d69e015a07e96dcd141d31e7fc81c4420dbbef75aef5201809093210e

解法

ランダムでわからない部分を総当たりでdecryptする。
flag_hashのハッシュが与えられているということは総当たりしろってことですね。

for i in range(256):
    for j in range(256):
        key = key_prefix + bytes([i])
        iv = iv_prefix + bytes([j])
        try:
            cipher = AES.new(key, AES.MODE_CBC, iv)
            dec = unpad(cipher.decrypt(enc), 16)
            if hashlib.sha256(dec).hexdigest() == flag_hash:
                print(f'Decrypted FLAG: {dec}')
                break
        except ValueError:
            continue

FLAG{7h3_f1r57_5t3p_t0_Crypt0!!}

replacement (126pt 431 solves Easy)✅

問題

秘密の文章がmd5のハッシュで暗号化されている。

enc = []
for char in cal:
    x = ord(char)
    x = hashlib.md5(str(x).encode()).hexdigest()
    enc.append(int(x, 16))
[265685380796387128074260337556987156845, 75371056103973480373443517203033791314, 330443362254714811278522520670919771869, 127044987962124214100696270195559210814, 75371056103973480373443517203033791314, 57512852240092789512489991536185408584, 330443362254714811278522520670919771869, 301648155472379285594517050531127483548,
# 以下略

解法

文字列を全てmd5にして、テーブルで置換するという作戦。
まず、コードをそのまま流用してテーブルを書き出す。

cal = string.printable
enc = []
for char in cal:
    x = ord(char)
    x = hashlib.md5(str(x).encode()).hexdigest()
    enc.append(int(x, 16))
        
for e, c in zip(enc, cal):
    print(c, e)
0 133164625458131999197654962205376583862
1 324787361952219506718126426467652498112
2 256249087116017178292043637459846421514
3 53459933652527578064242465506376923016
4 204791166937441563272975036703176244680
5 287344569722037931951990749151720527344
6 221342036062217931314813830374780339254
...

このテーブルに対して、ヒットしたものを置換する
(シェル芸ごめん)

$ cat my_diary_11_8_Wednesday.txt | tr "," "\n" | xargs -I{} grep {} rep.csv | cut -f 1 -d " " | tr -d "\n"

Wednesday,11/8,clearskies.Thismorning,Ihadbreakfastatmyfavoritecafe.Drinkingthefreshlybrewedcoffeeandsavoringthewarmbutterytoastisthebest.Changingthesubject,Ireceivedanemailtodaywithsomethingratherpeculiarinit.Itcontainedamysteriousmessagethatsaid"Thisisasecretcode,sopleasedon'ttellanyone.FLAG{13epl4cem3nt}".Howstrange!Gureisya

Easy calc (197pt 95 solves Easy)

問題

FLAG = os.getenvb(b"FLAG", b"FAKE{THIS_IS_NOT_THE_FLAG!!!!!!}")

def encrypt(m: bytes, key: int) -> bytes:
    iv = os.urandom(16)
    key = long_to_bytes(key)
    key = md5(key).digest()
    cipher = AES.new(key, AES.MODE_CBC, iv=iv)
    return iv + cipher.encrypt(m)

def f(s, p):
    u = 0
    for i in range(p):
        u += p - i
        u *= s
        u %= p

    return u

p = getPrime(1024)
s = random.randint(1, p - 1)

A = f(s, p)
ciphertext = encrypt(FLAG, s).hex()
p = 108159532265181242371960862176089900437183046655107822712736597793129430067645352619047923366465213553080964155205008757015024406041606723580700542617009651237415277095236385696694741342539811786180063943404300498027896890240121098409649537982185247548732754713793214557909539077228488668731016501718242238229
A = 60804426023059829529243916100868813693528686280274100232668009387292986893221484159514697867975996653561494260686110180269479231384753818873838897508257692444056934156009244570713404772622837916262561177765724587140931364577707149626116683828625211736898598854127868638686640564102372517526588283709560663960
ciphertext = '9fb749ef7467a5aff04ec5c751e7dceca4f3386987f252a2fc14a8970ff097a81fcb1a8fbe173465eecb74fb1a843383'

解法

解けなかったので、復習しました。

数式を展開する。s^pがあるからsについて解けないじゃんって諦めていた。フェルマーの小定理を使っていいんだ。

fについては以下のように書ける。

def f(s, p):
    return s * pow(1 - s, -1, p) % p

sについて解くと s = A / (1 + A)

s = A * pow(1 + A, -1 , p) % p

iv = bytes.fromhex(ciphertext[:32])
ciphertext = bytes.fromhex(ciphertext[32:])
key = long_to_bytes(s)
key = md5(key).digest()
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
print(cipher.decrypt(ciphertext).decode())

dance (205pt 85 solves Normal)✅

問題

ログインして暗号化できるシステムのPythonコードが与えられる(一部抜粋)

def Register():
    global d
    username = input('Enter username: ')
    if username in d:
        print('Username already exists')
        return
    dt_now = datetime.datetime.now()
    minutes = dt_now.minute
    sec = dt_now.second
    data1 = f'user: {username}, {minutes}:{sec}'
    data2 = f'{username}'+str(random.randint(0, 10))
    d[username] = make_token(data1, data2)
    print('Registered successfully!')
    print('Your token is:', d[username])
    return
class MyCipher:
    def __init__(self, key: bytes, nonce: bytes):
        self.key = key
        self.nonce = nonce
        self.counter = 1
        self.state = List[F2_32]

    def __quarter_round(self, a: F2_32, b: F2_32, c: F2_32, d: F2_32):
        a += b; d ^= a; d <<= 16
        c += d; b ^= c; b <<= 12
        a += b; d ^= a; d <<= 8
        c += d; b ^= c; b <<= 7
        return a, b, c, d
    
    def __Qround(self, idx1, idx2, idx3, idx4):
        self.state[idx1], self.state[idx2], self.state[idx3], self.state[idx4] = \
            self.__quarter_round(self.state[idx1], self.state[idx2], self.state[idx3], self.state[idx4])

    def __update_state(self):
        for _ in range(10):
            self.__Qround(0, 4, 8, 12)
            self.__Qround(1, 5, 9, 13)
            self.__Qround(2, 6, 10, 14)
            self.__Qround(3, 7, 11, 15)
            self.__Qround(0, 5, 10, 15)
            self.__Qround(1, 6, 11, 12)
            self.__Qround(2, 7, 8, 13)
            self.__Qround(3, 4, 9, 14)
username = 'gureisya'
ciphertext = '061ff06da6fbf8efcd2ca0c1d3b236aede3f5d4b6e8ea24179'

解法

使われている暗号について調べてみると、ChaCha20という暗号そのものであった。
参考:https://tex2e.github.io/blog/crypto/chacha20poly1305

tokenを作るところをよく見ると、60x60x11通りしか存在しないことがわかる。

data1 = f'user: {username}, {minutes}:{sec}'
data2 = f'{username}'+str(random.randint(0, 10))
d[username] = make_token(data1, data2)

ということで、tokenを全て求めて、復号する。
ちなみに、ChaCha20のdecryptが記述されていないが、encryptと同じ処理をすることで復号できる。

tokens = []
for minutes in range(60):
    for sec in range(60):
        for randnum in range(11):
            data1 = f'user: {username}, {minutes}:{sec}'
            data2 = f'{username}'+str(randnum)
            tokens.append(make_token(data1, data2))

for token in tokens:
    cipherbytes = bytes.fromhex(ciphertext)

    sha256 = hashlib.sha256()
    sha256.update(token.encode())
    key = sha256.hexdigest()[:32]
    nonce = token[:12]
    cipher = MyCipher(key.encode(), nonce.encode())
    decrypted = cipher.encrypt(cipherbytes)

    if b'FLAG' in decrypted:
        print(decrypted)

FLAG{d4nc3_l0b0t_d4nc3!!}

speedy (235pt 60 solves Hard) ✅

問題

オリジナルであろうブロック暗号について、解読する問題。

def rotl(x, y):
    x &= 0xFFFFFFFFFFFFFFFF
    return ((x << y) | (x >> (64 - y))) & 0xFFFFFFFFFFFFFFFF

class MyCipher:
    def __init__(self, s0, s1):
        self.X = s0
        self.Y = s1
        self.mod = 0xFFFFFFFFFFFFFFFF
        self.BLOCK_SIZE = 8
    
    def get_key_stream(self):
        s0 = self.X
        s1 = self.Y
        sum = (s0 + s1) & self.mod
        s1 ^= s0
        key = []
        for _ in range(8):
            key.append(sum & 0xFF)
            sum >>= 8
        
        self.X = (rotl(s0, 24) ^ s1 ^ (s1 << 16)) & self.mod
        self.Y = rotl(s1, 37) & self.mod
        return key
    
    def encrypt(self, pt: bytes):
        ct = b''
        for i in range(0, len(pt), self.BLOCK_SIZE):
            ct += long_to_bytes(self.X)
            key = self.get_key_stream()
            block = pt[i:i+self.BLOCK_SIZE]
            ct += bytes([block[j] ^ key[j] for j in range(len(block))])
        return ct

暗号化処理

s0 = bytes_to_long(os.urandom(8))
s1 = bytes_to_long(os.urandom(8))

cipher = MyCipher(s0, s1)
secret = b'FLAG{'+b'*'*19+b'}'
pt = pad(secret, 8)
ct = cipher.encrypt(pt)

暗号化結果

ct = b'"G:F\xfe\x8f\xb0<O\xc0\x91\xc8\xa6\x96\xc5\xf7N\xc7n\xaf8\x1c,\xcb\xebY<z\xd7\xd8\xc0-\x08\x8d\xe9\x9e\xd8\xa51\xa8\xfbp\x8f\xd4\x13\xf5m\x8f\x02\xa3\xa9\x9e\xb7\xbb\xaf\xbd\xb9\xdf&Y3\xf3\x80\xb8'

解法

8文字を1ブロックとして、4つのブロックで暗号化されている。
それぞれのブロックにおいて使われる変数をx_i, y_i, key_i、暗号化文をc_i、平文をm_iと留守。

コードをよく読むと、以下の手がかりが得られた。

  1. c1~c4は出力結果ctに含まれている
  2. x1~x4も出力結果ctに含まれている
  3. y_i→yi-1が求められる(xi-1は判明しているため)
  4. 平文m4はb'}\x07\x07\x07\x07\x07\x07\x07'である
  5. key_i→y_iが求められる(xiは判明しているため)

つまり、地道に計算していけば、y_iが求まり、keyも平文も求めることができる。

key4を求める

c4 = b'\xb9\xdf&Y3\xf3\x80\xb8'
m4 = b'}\x07\x07\x07\x07\x07\x07\x07'
key4 = bytes([c4[j] ^ m4[j] for j in range(8)])

key4->y4

x4 = 190182108432150461
sum = key4[::-1] # 逆順
y4 = (bytes_to_long(sum) - x4) & 0xFFFFFFFFFFFFFFFF
# y4 = 13611085980422777095

yiを求める

# y4 -> y3
y3 = cp.rotl(y4, 27)
y3 ^= x3

# y3 -> y2
y2 = cp.rotl(y3, 27)
y2 ^= x2

# y2 -> y1
y1 = cp.rotl(y2, 27)
y1 ^= x1

decrypt

これまで求めた値に対して、decrypt(x1, y1, c1)みたいに計算すれば、各ブロックの平文がもとまる。

def decrypt(x, y, c):
    s0 = x
    s1 = y
    sum = (s0 + s1) & 0xFFFFFFFFFFFFFFFF
    s1 ^= s0
    key = []
    for _ in range(8):
        key.append(sum & 0xFF)
        sum >>= 8
    
    m = bytes([c[j] ^ key[j] for j in range(len(c))])
    print(m)

FLAG{x013_ro74te_5hif7!!}

Many Xor Shift (307pt 29 solves Normal)

問題

FLAG = b'FAKE{XXXXXXXXXXXXXXXXXXXXXX}'

N = 7
M = 17005450388330379
WORD_SIZE = 32
WORD_MASK = (1 << WORD_SIZE) - 1

def encrypt(m):
    state = [int.from_bytes(m[i:i+4]) for i in range(0, len(m), 4)]
    assert len(state) == N

    def xor_shift():
        nonlocal state
        t = state[0] ^ ((state[0] << 11) & WORD_MASK)
        for i in range(N-1):
            state[i] = state[i+1]
        state[-1] = (state[-1] ^ (state[-1] >> 19)) ^ (t ^ (t >> 8))

    for _ in range(M):
        xor_shift()

    return state
N = 7
M = 17005450388330379
WORD_SIZE = 32
state = [1927245640, 871031439, 789877080, 4042398809, 3950816575, 2366948739, 935819524]

解法

解けなかったので、後日復習しました。

Xorshiftという擬似乱数生成らしい。
(編集中)

uf

問題

解けなかったので、後日復習

解法

解けなかったので、後日復習

Forensics

I_wanna_be_a_streamer (169pt 144 solves Easy)✅

問題

ストリーミングした通信のpcapファイルが与えられる

解法

RCPのパケットから、映像の取得を試みた。

チームメンバーが以下のような参考を教えてくれた。

記事を参考に、GStreamerを使って以下のコマンドを実行すると動画が生成できた。

C:\gstreamer\1.0\msvc_x86_64\bin\gst-launch-1.0.exe -m -v filesrc location=input.pcap ! pcapparse src-port=22000 dst-port=59974 ! application/x-rtp,media=video,clock-rate=90000 ! rtpjitterbuffer ! rtph264depay ! avdec_h264 ! videoscale ! video/x-raw, width=1280, height=720 ! videoconvert ! x264enc ! mp4mux ! filesink location=output.mp4

src-port=22000 dst-port=59974 は、WiresharkからTelephony>RTP>RTP Streamsから確認。

おわりに

チーム「脆弱エンジニア(full_weak_engineer)」では、SECCON CTF決勝進出を目指してCTFに参加しています!
チームでCTFをやりたいメンバーを常に募集しています。(Pwn, Rev担当大歓迎)

YouTubeの概要欄にあるDiscordリンクから、気軽にご参加ください!
https://www.youtube.com/@full-weak-engineer

Discussion