IERAE CTF 2024 writeup

Writer:b1uef0x / Webページ建造途中

概要

warmup以外では2問回答、1問を途中まで。

目次

derangement (crypto warmup)

I've made a secret magic string, perfectly encrypted!

サーバー上で次のプログラムが動作している。

#!/usr/bin/env python

from os import getenv
import random
import string
import sys

FLAG = getenv("FLAG", "TEST{TEST_FLAG}")

LENGTH = 15
CHAR_SET = string.ascii_letters + string.digits + string.punctuation

def generate_magic_word(length=LENGTH, char_set=CHAR_SET):
    return ''.join(random.sample(char_set, length))

def is_derangement(perm, original):
    return all(p != o for p, o in zip(perm, original))

def output_derangement(magic_word):
    while True:
        deranged = ''.join(random.sample(magic_word, len(magic_word)))
        if is_derangement(deranged, magic_word):
            print('hint:', deranged)
            break

def guess_random(magic_word, flag):
    print('Oops, I spilled the beans! What is the magic word?')
    if input('> ') == magic_word:
        print('Congrats!\n', flag)
        return True
    print('Nope')
    return False

def main():
    magic_word = generate_magic_word()
    banner = """
/********************************************************\\
|                                                        |
|   Abracadabra, let's perfectly rearrange everything!   |
|                                                        |
\\********************************************************/
"""
    print(banner)
    connection_count = 0

    while connection_count < 300:
        print('type 1 to show hint')
        print('type 2 to submit the magic word')
        try:
            connection_count += 1
            user_input = int(input('> '))

            if user_input == 1:
                output_derangement(magic_word)
            elif user_input == 2:
                if guess_random(magic_word, FLAG):
                    break
                sys.exit()
            else:
                print('bye!')
                sys.exit()
        except:
            sys.exit(-1)
    
    print('Connection limit reached. Exiting...')

if __name__ == "__main__":
    main()

文字列をランダムに並び替えるところを読むと、各文字が元の位置以外に並び替えるようにできているので、繰り返し取得して各文字の元の位置を特定できる。

次のソルバーを作成。

from pwn import *

derangement = ["" for j in range(15)]
magicsample = ""

io = remote('104.199.135.28',55555)
for i in range(300):
        io.recvuntil(b"> ")
        io.sendline(b"1")
        line = io.recvline().decode()
        if magicsample == "":
                magicsample = line[6:6+15]
        for j in range(15):
                charj = line[6+j]
                if charj not in derangement[j]:
                        derangement[j] += charj
        f = 0
        for k in derangement:
                if len(k)==14:
                        f += 1
        if f==15:
                break
        else:
                debug = ""
                for j in derangement:
                        debug += str(len(j)) + " "
                print(debug)

magicword = ""
for i in range(15):
        for j in range(15):
                if magicsample[j] not in derangement[i]:
                        magicword += magicsample[j]
                        break
print("magicword:",magicword)


io.recvuntil(b"> ")
io.sendline(b"2")
io.sendline(magicword.encode())
io.interactive()
$ python3 solve.py 
[+] Opening connection to 104.199.135.28 on port 55555: Done
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 
2 1 2 2 2 2 2 2 1 2 2 1 2 1 2 
3 2 3 3 3 3 3 2 2 3 3 2 2 2 3 
3 3 4 4 4 4 4 3 3 4 4 3 3 3 4 
4 4 4 5 4 5 4 4 4 5 4 4 3 3 5 
5 5 5 5 4 6 5 4 5 5 5 5 4 4 6 
6 6 5 5 5 7 6 5 5 6 6 5 5 5 6 
7 7 5 6 5 8 6 6 6 6 7 6 5 6 7 
8 8 5 7 6 8 6 7 7 6 7 7 6 6 7 
8 8 6 8 6 8 7 7 7 7 7 7 6 6 8 
9 9 6 8 6 8 8 7 7 7 8 7 6 7 9 
9 9 7 9 6 8 8 8 8 7 8 7 7 7 10 
9 9 8 9 7 8 9 8 9 7 9 8 8 8 10 
10 9 9 9 8 8 9 8 9 8 9 8 8 8 10 
10 9 9 9 8 8 9 9 9 8 9 9 8 9 10 
11 10 9 9 9 9 9 10 10 8 10 9 9 10 11 
11 10 9 9 10 9 10 10 10 8 11 9 10 10 11 
12 11 9 9 11 9 10 10 10 8 11 9 10 10 11 
13 11 10 10 11 10 10 10 11 8 11 9 10 10 12 
14 12 10 10 11 10 10 10 11 8 11 9 10 10 12 
14 12 10 11 11 10 10 11 11 9 11 9 10 10 12 
14 13 10 11 11 11 11 11 11 9 11 10 10 10 12 
14 13 11 11 11 11 11 12 11 10 11 10 10 10 12 
14 13 11 11 11 11 11 12 12 11 12 10 10 10 12 
14 13 11 11 11 12 11 12 12 12 12 11 11 10 12 
14 13 11 12 11 12 11 12 12 12 12 11 11 10 12 
14 13 11 12 11 12 11 12 12 12 12 11 11 10 13 
14 13 11 12 11 12 11 13 12 12 12 11 11 10 13 
14 14 11 13 12 12 11 13 12 12 12 11 11 10 13 
14 14 11 13 12 12 11 14 12 12 12 12 11 10 13 
14 14 12 13 12 12 11 14 12 12 13 12 11 10 13 
14 14 12 13 12 12 12 14 12 13 14 13 11 10 13 
14 14 12 13 13 12 12 14 12 13 14 13 11 11 13 
14 14 12 13 13 12 12 14 13 13 14 13 11 11 13 
14 14 12 13 13 12 12 14 13 13 14 13 11 11 13 
14 14 12 13 13 12 12 14 13 13 14 13 11 11 13 
14 14 12 13 13 12 12 14 13 13 14 13 11 11 13 
14 14 12 14 13 12 12 14 13 13 14 14 11 12 13 
14 14 12 14 13 12 12 14 13 13 14 14 11 12 13 
14 14 12 14 13 12 12 14 13 13 14 14 11 12 13 
14 14 12 14 13 12 12 14 13 13 14 14 11 12 13 
14 14 12 14 13 12 12 14 13 13 14 14 11 12 13 
14 14 12 14 13 12 12 14 13 13 14 14 12 12 14 
14 14 13 14 13 13 12 14 13 13 14 14 12 12 14 
14 14 13 14 13 13 13 14 13 13 14 14 12 12 14 
14 14 13 14 13 13 13 14 13 13 14 14 12 12 14 
14 14 13 14 13 13 13 14 14 13 14 14 12 12 14 
14 14 13 14 13 13 13 14 14 14 14 14 12 13 14 
14 14 14 14 13 13 14 14 14 14 14 14 12 13 14 
14 14 14 14 13 13 14 14 14 14 14 14 12 13 14 
14 14 14 14 13 13 14 14 14 14 14 14 12 14 14 
14 14 14 14 13 13 14 14 14 14 14 14 12 14 14 
14 14 14 14 13 13 14 14 14 14 14 14 12 14 14 
14 14 14 14 13 13 14 14 14 14 14 14 12 14 14 
14 14 14 14 13 13 14 14 14 14 14 14 12 14 14 
14 14 14 14 13 13 14 14 14 14 14 14 13 14 14 
14 14 14 14 13 13 14 14 14 14 14 14 13 14 14 
14 14 14 14 13 13 14 14 14 14 14 14 13 14 14 
14 14 14 14 13 13 14 14 14 14 14 14 13 14 14 
14 14 14 14 13 13 14 14 14 14 14 14 13 14 14 
14 14 14 14 13 13 14 14 14 14 14 14 13 14 14 
14 14 14 14 13 13 14 14 14 14 14 14 13 14 14 
14 14 14 14 13 13 14 14 14 14 14 14 13 14 14 
14 14 14 14 13 13 14 14 14 14 14 14 13 14 14 
14 14 14 14 13 13 14 14 14 14 14 14 13 14 14 
14 14 14 14 13 13 14 14 14 14 14 14 13 14 14 
14 14 14 14 13 13 14 14 14 14 14 14 13 14 14 
14 14 14 14 13 13 14 14 14 14 14 14 13 14 14 
14 14 14 14 13 14 14 14 14 14 14 14 13 14 14 
14 14 14 14 13 14 14 14 14 14 14 14 13 14 14 
14 14 14 14 13 14 14 14 14 14 14 14 13 14 14 
14 14 14 14 13 14 14 14 14 14 14 14 13 14 14 
14 14 14 14 14 14 14 14 14 14 14 14 13 14 14 
14 14 14 14 14 14 14 14 14 14 14 14 13 14 14 
14 14 14 14 14 14 14 14 14 14 14 14 13 14 14 
magicword: ,<afv8zQU(>)|x%
[*] Switching to interactive mode
Oops, I spilled the beans! What is the magic word?
> Congrats!
 IERAE{th3r35_n0_5uch_th!ng_45_p3rf3ct_3ncrypt!0n}

IERAE{th3r35_n0_5uch_th!ng_45_p3rf3ct_3ncrypt!0n}

Weak PRNG (crypto easy)

Do you understand the traits of that famous PRNG?

恒例のメルセンヌ・ツイスタ問題。

#!/usr/bin/env python

from os import getenv
import random
import secrets

FLAG = getenv("FLAG", "TEST{TEST_FLAG}")


def main():
    # Python uses the Mersenne Twister (MT19937) as the core generator.
    # Setup Random Number Generator
    rng = random.Random()
    rng.seed(secrets.randbits(32))

    secret = rng.getrandbits(32)

    print("Welcome!")
    print("Recover the initial output and input them to get the flag.")

    while True:
        print("--------------------")
        print("Menu")
        print("1. Get next 16 random data")
        print("2. Submit your answer")
        print("3. Quit")
        print("Enter your choice (1-3)")
        choice = input("> ").strip()

        if choice == "1":
            print("Here are your random data:")
            for _ in range(16):
                print(rng.getrandbits(32))
        elif choice == "2":
            print("Enter the secret decimal number")
            try:
                num = int(input("> ").strip())

                if num == secret:
                    print("Correct! Here is your flag:")
                    print(FLAG)
                else:
                    print("Incorrect number. Bye!")
                break
            except (ValueError, EOFError):
                print("Invalid input. Exiting.")
                break
        elif choice == "3":
            print("Bye!")
            break
        else:
            print("Invalid choice. Please enter 1, 2 or 3.")
            continue


if __name__ == "__main__":
    main()

ランダムなシード値が与えられたPythonのRandom関数が用意され、1個目の乱数を当てることができればflagが表示される。2個目以降は好きなだけ表示させることができる。 pythonのgetrandbits(32)はメルセンヌ・ツイスタの内部状態624個の配列から1個分の乱数を順番に出力する。よって2番目以降の乱数を取得してsecretに入っている1番目の内部状態を復元する。

手持ちのメルセンヌ・ツイスタ逆算コードが上手に動かなかったので次のWebページから、次の内部状態から前の内部状態を復元するコードをもらった。

最初の624個の内部状態の配列のうち、1個目はsecretに入っている。続く623個を破棄して、内部状態が更新された次の624個を取得して前の状態を逆算する。

import random
from pwn import *

def untemper(x):
    x = unBitshiftRightXor(x, 18)
    x = unBitshiftLeftXor(x, 15, 0xefc60000)
    x = unBitshiftLeftXor(x, 7, 0x9d2c5680)
    x = unBitshiftRightXor(x, 11)
    return x

def unBitshiftRightXor(x, shift):
    i = 1
    y = x
    while i * shift < 32:
        z = y >> shift
        y = x ^ z
        i += 1
    return y

def unBitshiftLeftXor(x, shift, mask):
    i = 1
    y = x
    while i * shift < 32:
        z = y << shift
        y = x ^ (z & mask)
        i += 1
    return y

def get_prev_state(state):
    for i in range(623, -1, -1):
        result = 0
        tmp = state[i]
        tmp ^= state[(i + 397) % 624]
        if ((tmp & 0x80000000) == 0x80000000):
            tmp ^= 0x9908b0df
        result = (tmp << 1) & 0x80000000
        tmp = state[(i - 1 + 624) % 624]
        tmp ^= state[(i + 396) % 624]
        if ((tmp & 0x80000000) == 0x80000000):
            tmp ^= 0x9908b0df
            result |= 1
        result |= (tmp << 1) & 0x7fffffff
        state[i] = result
    return state

N = 624

output = []
output.append(0)
io = remote('35.201.137.32',19937)
for i in range(78):
        io.recvuntil(b"> ")
        io.sendline(b"1")
        io.recvline()
        for j in range(16):
                line = io.recvline().decode()
                output.append(int(line))


xs0 = [0]
for i in range(1,N,1):
        xs0 += [output[i]]

xs1 = []
for i in range(0,N,1):
        xs1 += [output[i+N]]

mt_state = [untemper(x) for x in xs1]
prev_mt_state = get_prev_state(mt_state)
random.setstate((3, tuple(prev_mt_state + [0]), None))

predicted = [random.getrandbits(32) for _ in range(N)]

io.recvuntil(b"> ")
io.sendline(b"2")
io.recvuntil(b"> ")
io.sendline(str(predicted[0]).encode())

io.interactive()
$ python3 solve2.py
[+] Opening connection to 35.201.137.32 on port 19937: Done
[*] Switching to interactive mode
Correct! Here is your flag:
IERAE{WhY_4r3_n'7_Y0u_u51n6_4_CSPRNG_3v3n_1n_2024}
IERAE{WhY_4r3_n'7_Y0u_u51n6_4_CSPRNG_3v3n_1n_2024}

splitting (crypto easy)

Do you need to solve many ECDLPs?

解けなかった問題。進んだところまで。

楕円曲線の離散対数問題で、サーバーで次のプログラムが走っている。

#!/usr/bin/env sage

from Crypto.Util.number import *
from os import getenv

FLAG = getenv("FLAG", "TEST{TEST_FLAG}").encode()
f = bytes_to_long(FLAG)

p = random_prime(2^128)
Fp = GF(p)
a, b = Fp.random_element(), Fp.random_element()
E = EllipticCurve(Fp, [a, b])

print(a)
print(b)
print(p)

gens = list(E.gens())
if len(gens) < 2:
    gens.append(ZZ(Fp.random_element()) * E.gens()[0])

res = []
while f > 0:
    r = Fp.random_element()
    res.append(ZZ(r) * gens[f & 1])
    f >>= 1

for R in res:
    print(R.xy())

このプログラムの内容は以下の通り。

  1. 素数pを作成、ランダムなa,bを作成
  2. pを法とする有限体上にaとbを係数とする楕円曲線Eを作成
  3. Eの生成元(gens)を作る。生成元が1つしかなければスカラー倍したものを追加して2つにする
  4. flagのデータ1ビットごとにランダムなrを選び、flagのビットが0か1かによってgens[0]とgens[1]を切り替えて、R=r*gens[0 or 1]を計算
  5. a, b, p、各Rの座標(xy)を出力

各Rの座標からflagのビットを復元したい。ChatGPT-o1-previewを使って解き方を検討したところ、次の手順を考えた。

  1. 2つの生成元gensの位数orderを計算
  2. 各orderと座標Rを計算した結果が単位元であれば、そのRはその生成元部分群に属するため、使用された生成元を特定できる

これに基づいて次のソルバーを作成。

from sage.all import *
from pwn import *

ff = False

for i in range(100):
        io = remote('35.236.178.27',11119)
        a = int(io.recvline().decode())
        b = int(io.recvline().decode())
        p = int(io.recvline().decode())
        points = []
        for j in range(567):
                line = io.recvline().decode()[1:-3].split(",")
                points.append([int(line[0]),int(line[1])])
        io.close()
        E = EllipticCurve(GF(p), [a, b])
        gens = list(E.gens())
        print(gens)
        if len(gens)>1 :
                print("a",a)
                print("b",b)
                print("p",p)
                
                order0 = E.gens()[0].order()
                order1 = E.gens()[1].order()
                if order0 == order1:
                        print("order match",order0)
                else:
                        ff = True
                        break

if not ff:
        exit()

print(points)

# Reconstruct the elliptic curve
#E = EllipticCurve(GF(p), [a, b])

# Get the generators and their orders
gens = E.gens()
order0 = gens[0].order()
order1 = gens[1].order()
print("order0",order0)
print("order1",order1)
print("gcd",gcd(order0, order1))

# Initialize the list to hold bits
bits = []

# Process each point
for xy in points:
    R_i = E.point([xy[0],xy[1]])
    #print(R_i,order0 * R_i,order1 * R_i)
    if order1 * R_i == E(0):
        bits.append('1')
    elif order0 * R_i == E(0):
        bits.append('0')
    else:
        raise ValueError("Point does not belong to known subgroups.")

# Reconstruct 'f' from bits
f = int(''.join(bits[::-1]), 2)

# Convert 'f' back to the FLAG
from Crypto.Util.number import long_to_bytes
FLAG = long_to_bytes(f)
print("Recovered FLAG:", FLAG)

ソルバーでは繰り返し問題を取得しているが、これは多くの場合で生成元gensが1個しか存在せず、1個の場合はスカラー倍で2個になるため2つの生成元がお互いに独立ではなくなってしまうためである。

しかし運良く生成元が2個のパターンを引いても、gens2個のGCDを取ると片方がもう片方の倍数になったものしか入手できず、flagの完全な復元には失敗した。

flagの復元がある程度成功することもあり、以下の不完全なflagを得た。

Recovered FLAG: b'IERAE{7de6b745404269a0f7b40955047921c6860e4438c73eb095090e75c8fb00cb51}'
Recovered FLAG: b'IERAE{7de6f7454\xb04269a0d7b40955047969c6860e4438c73\xe5b095090e75c\xbcfb04wb51}'
Recovered FLAG: b']eRQE{\xb7dg?r?4540\xbc\xb2v9k0g7b4p955147\xb961c7\xba6\xf1m4<38c73ej\xb09w\xb292e?5s:fjq0cb\xb51}'
Recovered FLAG: b'IErAE{7devf\xb75u60v2v9gpd?k\xb4\xb0\xb97\xb5\xb0\xb47925w686pe|\xb43<s7\xb3eb\xb1{5x92\xed7\xb7c:gg10\xe3b59}'
Recovered FLAG: b'YMRAg{?dgvf7\xbd54v<:69e\xb8d7b429\xb552\xb4?9\xb21k6:7\xb0\xe54438s73er0=509pe7ug;fb00\xe3b=\xf9}'
Recovered FLAG: b'IERQE{7d\xef6b74=6\xb0tr\xf6y\xe30fw\xe2t1955047\xb921c68>0m44{8c73eb0{5096e7}g8ff0pcru3}'
Recovered FLAG: b'IERAE\x7f7tu6c745\xb464269q0d7r64955047921k6860\xe54<38c73er09549\xb0e77\xe38fb00c\xe251}'
Recovered FLAG: b'IERCG{7fe7r75u\xb48<:69\xe10d7f40\xbd5u\xb14\x7f=\xb21c\xbe\xbc60e\xb5>3|c73mb095:}8e7=c8fb\xb04kj5q}'
Recovered FLAG: b'IE\xd2EE{7dg\xb6b74=|7>3\xb6=\xe14\xf47b649550t7;23c686re64s8c\xb7web0958\xb90e77\xe3xfb13sb51}'
Recovered FLAG: b'IGRaE{7\xecevg7t5614269q\xb0d7z40y550~\xb7=21g6:v1e45;x\xe3w3\xef\xf20;5290\xe575c8\xe6r01ccu1\xfd'

さらにここから最頻出ビットを計算して、次の2択に絞り込んだが、正しいflagではなかった。

IERAE{de6f745404269a0d7b40955047921c6860e4438c73eb095090e75c8fb00cb51}
IERAE{de6b745404269a0d7b40955047921c6860e4438c73eb095090e75c8fb00cb51}

追記:凡ミス

絞り込んだflagの出力に欠損があり、IERAE{7de...がIERAE[de...となっていることに気づいた。修正した統合プログラムは以下の通り。10サンプル中7サンプル以上が共通するビットを採用する方法。

from Crypto.Util.number import long_to_bytes,bytes_to_long
import re

def is_hexadecimal(s):
        try:
                hex_pattern = re.fullmatch(r'[0-9A-Fa-f]+', s.decode())
                return hex_pattern is not None
        except:
                return False

flags = []
flags.append(b'IERAE{7de6b745404269a0f7b40955047921c6860e4438c73eb095090e75c8fb00cb51}')
flags.append(b'IERAE{7de6f7454\xb04269a0d7b40955047969c6860e4438c73\xe5b095090e75c\xbcfb04wb51}')
flags.append(b']eRQE{\xb7dg?r?4540\xbc\xb2v9k0g7b4p955147\xb961c7\xba6\xf1m4<38c73ej\xb09w\xb292e?5s:fjq0cb\xb51}')
flags.append(b'IErAE{7devf\xb75u60v2v9gpd?k\xb4\xb0\xb97\xb5\xb0\xb47925w686pe|\xb43<s7\xb3eb\xb1{5x92\xed7\xb7c:gg10\xe3b59}')
flags.append(b'YMRAg{?dgvf7\xbd54v<:69e\xb8d7b429\xb552\xb4?9\xb21k6:7\xb0\xe54438s73er0=509pe7ug;fb00\xe3b=\xf9}')
flags.append(b'IERQE{7d\xef6b74=6\xb0tr\xf6y\xe30fw\xe2t1955047\xb921c68>0m44{8c73eb0{5096e7}g8ff0pcru3}')
flags.append(b'IERAE\x7f7tu6c745\xb464269q0d7r64955047921k6860\xe54<38c73er09549\xb0e77\xe38fb00c\xe251}')
flags.append(b'IERCG{7fe7r75u\xb48<:69\xe10d7f40\xbd5u\xb14\x7f=\xb21c\xbe\xbc60e\xb5>3|c73mb095:}8e7=c8fb\xb04kj5q}')
flags.append(b'IE\xd2EE{7dg\xb6b74=|7>3\xb6=\xe14\xf47b649550t7;23c686re64s8c\xb7web0958\xb90e77\xe3xfb13sb51}')
flags.append(b'IGRaE{7\xecevg7t5614269q\xb0d7z40y550~\xb7=21g6:v1e45;x\xe3w3\xef\xf20;5290\xe575c8\xe6r01ccu1\xfd')

bitss = []
for i in flags:
        bitss.append("0"+bin(bytes_to_long(i))[2:])

ans = ""
for i in range(568):
        c0 = 0
        c1 = 0
        for j in bitss:
                if j[i]=="1":
                        c1 += 1
                else:
                        c0 += 1
        if c1>6 :
                ans += "1"
        elif c0>6:
                ans += "0"
        else:
                ans += "*"


def createbits(bits,x):
        if x>=len(ans) :
                s = long_to_bytes(int(bits, 2))
                if is_hexadecimal(s[6:-1]):
                        print(s)
        elif ans[x] == "*":
                createbits(bits+"1",x+1)
                createbits(bits+"0",x+1)
        else:
                createbits(bits+ans[x],x+1)

createbits("",0)

出力は次の2つで、2番目がflagだった。

IERAE{7de6f745404269a0d7b40955047921c6860e4438c73eb095090e75c8fb00cb51}
IERAE{7de6b745404269a0d7b40955047921c6860e4438c73eb095090e75c8fb00cb51}

Assignment (rev warmup)

Assignment is the root of everything in procedural programs.

Linuxの実行バイナリが配布される。Ghidraで逆コンパイルすると次のmain関数が見つかる。

undefined8 main(int param_1,long param_2)

{
  int iVar1;
  
  flag[28] = 0x33;
  flag[1] = 0x45;
  flag[2] = 0x52;
  flag[20] = 0x72;
  flag[26] = 0x61;
  flag[10] = 0x5f;
  flag[32] = 0x7d;
  flag[9] = 0x65;
  flag[22] = 0x6e;
  flag[17] = 0x5f;
  flag[6] = 0x73;
  flag[7] = 0x30;
  flag[15] = 0x30;
  flag[16] = 0x6d;
  flag[21] = 0x31;
  flag[24] = 0x5f;
  flag[12] = 0x34;
  flag[25] = 0x35;
  flag[31] = 99;
  flag[3] = 0x41;
  flag[0] = 0x49;
  flag[29] = 0x35;
  flag[18] = 0x73;
  flag[19] = 0x74;
  flag[11] = 0x72;
  flag[8] = 0x6d;
  flag[5] = 0x7b;
  flag[4] = 0x45;
  flag[27] = 0x39;
  flag[30] = 0x34;
  flag[23] = 0x67;
  flag[13] = 0x6e;
  flag[14] = 100;
  if (1 < param_1) {
    iVar1 = strcmp(flag,*(char **)(param_2 + 8));
    if (iVar1 == 0) {
      puts(flag);
    }
  }
  return 0;
}

flagが直接ハードコーディングされており、ここから復元。

IERAE{s0me_r4nd0m_str1ng_5a9354c}

Luz Da Lua (rev was_warmup easy)

The luac file is compiled and tested on Lua 5.4.7

luacファイルが与えられるので、Webデコンパイラを使って逆コンパイルした。

-- filename: @/mnt/LuzDaLua.lua
-- version: lua54
-- line: [0, 0] id: 0
io.write("Input > ")
input = io.read("*l")
if string.len(input) ~= 28 then
  goto label_301
elseif string.byte(input, 1) ~ 232 ~= 161 then
  goto label_301
elseif string.byte(input, 2) ~ 110 ~= 43 then
  goto label_301
elseif string.byte(input, 3) ~ 178 ~= 224 then
  goto label_301
elseif string.byte(input, 4) ~ 172 ~= 237 then
  goto label_301
elseif string.byte(input, 5) ~ 212 ~= 145 then
  goto label_301
elseif string.byte(input, 6) ~ 25 ~= 98 then
  goto label_301
elseif string.byte(input, 7) ~ 53 ~= 121 then
  goto label_301
elseif string.byte(input, 8) ~ 63 ~= 74 then
  goto label_301
elseif string.byte(input, 9) ~ 135 ~= 230 then
  goto label_301
elseif string.byte(input, 10) ~ 92 ~= 3 then
  goto label_301
elseif string.byte(input, 11) ~ 38 ~= 23 then
  goto label_301
elseif string.byte(input, 12) ~ 250 ~= 137 then
  goto label_301
elseif string.byte(input, 13) ~ 216 ~= 135 then
  goto label_301
elseif string.byte(input, 14) ~ 5 ~= 86 then
  goto label_301
elseif string.byte(input, 15) ~ 69 ~= 117 then
  goto label_301
elseif string.byte(input, 16) ~ 226 ~= 189 then
  goto label_301
elseif string.byte(input, 17) ~ 137 ~= 186 then
  goto label_301
elseif string.byte(input, 18) ~ 148 ~= 240 then
  goto label_301
elseif string.byte(input, 19) ~ 64 ~= 53 then
  goto label_301
elseif string.byte(input, 20) ~ 130 ~= 225 then
  goto label_301
elseif string.byte(input, 21) ~ 241 ~= 197 then
  goto label_301
elseif string.byte(input, 22) ~ 151 ~= 227 then
  goto label_301
elseif string.byte(input, 23) ~ 203 ~= 250 then
  goto label_301
elseif string.byte(input, 24) ~ 179 ~= 220 then
  goto label_301
elseif string.byte(input, 25) ~ 216 ~= 182 then
  goto label_301
elseif string.byte(input, 26) ~ 101 ~= 4 then
  goto label_301
elseif string.byte(input, 27) ~ 238 ~= 130 then
  goto label_301
elseif string.byte(input, 28) ~ 61 ~= 64 then
  goto label_301
else
  print("Correct")
end
-- warn: not visited block [59]
-- block#59:
-- _ENV.print("Wrong")

入力文字を1文字ずつXOR演算して不一致を見ているので、2つの数字をXORすればflagの文字列を復元できる。

IERAE{Lua_1s_S0_3duc4t1onal}

The Kudryavka Sequence (rev easy)

The flag has been lost.

laika.exeによって暗号化されたflag.pngを復元する問題。laika.exeをGhidraで逆コンパイルすると次の関数が見つかる。

void FUN_140001aa0(void)

{
  int iVar1;
  BOOL BVar2;
  HANDLE pvVar3;
  FILE *pFVar4;
  longlong lVar5;
  UCHAR *pUVar6;
  undefined8 uVar7;
  ulonglong uVar8;
  undefined8 uVar9;
  LPDWORD lpNumberOfBytesWritten;
  undefined auStackY_c8 [32];
  uint local_84;
  uint local_80;
  PUCHAR local_70;
  PUCHAR local_68;
  uint local_60;
  uint local_5c;
  _SYSTEMTIME local_58;
  DWORD local_48 [2];
  UCHAR local_40 [16];
  UCHAR local_30 [32];
  ulonglong local_10;
  
  local_10 = DAT_140005000 ^ (ulonglong)auStackY_c8;
  pUVar6 = local_30;
  for (lVar5 = 0x20; lVar5 != 0; lVar5 = lVar5 + -1) {
    *pUVar6 = '\0';
    pUVar6 = pUVar6 + 1;
  }
  pUVar6 = local_40;
  for (lVar5 = 0x10; lVar5 != 0; lVar5 = lVar5 + -1) {
    *pUVar6 = '\0';
    pUVar6 = pUVar6 + 1;
  }
  local_68 = (PUCHAR)0x0;
  local_5c = 0;
  FUN_140001410(&local_68,&local_5c);
  FUN_1400016e0();
  FUN_1400018d0();
  GetLocalTime(&local_58);
  srand((((((((local_58.wYear + 0x200ab) * 0x200ab + (uint)local_58.wMonth) * 0x200ab +
            (uint)local_58.wDayOfWeek) * 0x200ab + (uint)local_58.wDay) * 0x200ab +
          (uint)local_58.wHour) * 0x200ab + (uint)local_58.wMinute) * 0x200ab +
        (uint)local_58.wSecond) * 0x200ab + (uint)local_58.wMilliseconds);
  for (local_84 = 0; local_84 < 0x20; local_84 = local_84 + 1) {
    iVar1 = rand();
    local_30[local_84] = (UCHAR)iVar1;
  }
  for (local_80 = 0; local_80 < 0x10; local_80 = local_80 + 1) {
    iVar1 = rand();
    local_40[local_80] = (UCHAR)iVar1;
  }
  local_70 = (PUCHAR)0x0;
  local_60 = 0;
  FUN_1400010d0(local_30,0x20,local_40,0x10,local_68,local_5c,&local_70,&local_60);
  FUN_140001650((longlong)local_70,local_60);
  uVar9 = 0;
  uVar7 = 0;
  pvVar3 = CreateFileW(L"flag.png.laika",0x40000000,0,(LPSECURITY_ATTRIBUTES)0x0,2,0x80,(HANDLE)0x0)
  ;
  if (pvVar3 == (HANDLE)0xffffffffffffffff) {
    pFVar4 = (FILE *)__acrt_iob_func(2);
    FUN_140001060(pFVar4,"Error: CreateFile\n",uVar7,uVar9);
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  lpNumberOfBytesWritten = local_48;
  uVar8 = (ulonglong)local_60;
  BVar2 = WriteFile(pvVar3,local_70,local_60,lpNumberOfBytesWritten,(LPOVERLAPPED)0x0);
  if (BVar2 == 0) {
    pFVar4 = (FILE *)__acrt_iob_func(2);
    FUN_140001060(pFVar4,"Error: WriteFile\n",uVar8,lpNumberOfBytesWritten);
    CloseHandle(pvVar3);
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  CloseHandle(pvVar3);
  if (local_70 != (PUCHAR)0x0) {
    pvVar3 = GetProcessHeap();
    HeapFree(pvVar3,0,local_70);
  }
  if (local_68 != (PUCHAR)0x0) {
    pvVar3 = GetProcessHeap();
    HeapFree(pvVar3,0,local_68);
  }
  FUN_140001de0(local_10 ^ (ulonglong)auStackY_c8);
  return;
}

中身は割愛するが、FUN_140001410関数でflag.pngを読み込み、FUN_1400016e0でflag.pngを削除し、FUN_1400018d0でstatement.pngを出力する。暗号化後のflag.pngはflag.png.laikaとして出力される。

暗号化を実行する箇所はFUN_1400010d0とFUN_140001650の2つである。

void FUN_1400010d0(PUCHAR param_1,ULONG param_2,PUCHAR param_3,ULONG param_4,PUCHAR param_5,
                  uint param_6,PUCHAR *param_7,ULONG *param_8)

{
  NTSTATUS NVar1;
  FILE *pFVar2;
  ulonglong uVar3;
  HANDLE hHeap;
  PUCHAR pbOutput;
  wchar_t *pwVar4;
  undefined8 uVar5;
  undefined8 uVar6;
  undefined auStackY_98 [32];
  uint local_30 [2];
  BCRYPT_KEY_HANDLE local_28;
  BCRYPT_HANDLE local_20;
  ULONG local_18 [2];
  ulonglong local_10;
  
  local_10 = DAT_140005000 ^ (ulonglong)auStackY_98;
  local_20 = (BCRYPT_HANDLE)0x0;
  local_28 = (BCRYPT_KEY_HANDLE)0x0;
  local_30[0] = 0;
  local_18[0] = 0;
  uVar5 = 0;
  uVar6 = 0;
  NVar1 = BCryptOpenAlgorithmProvider(&local_20,L"AES",(LPCWSTR)0x0,0);
  if (NVar1 < 0) {
    pFVar2 = (FILE *)__acrt_iob_func(2);
    FUN_140001060(pFVar2,"Error: BCryptOpenAlgorithmProvider\n",uVar6,uVar5);
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  uVar6 = 0x20;
  pwVar4 = L"ChainingModeCBC";
  NVar1 = BCryptSetProperty(local_20,L"ChainingMode",(PUCHAR)L"ChainingModeCBC",0x20,0);
  if (NVar1 < 0) {
    pFVar2 = (FILE *)__acrt_iob_func(2);
    FUN_140001060(pFVar2,"Error: BCryptSetProperty\n",pwVar4,uVar6);
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  uVar5 = 0;
  uVar6 = 0;
  NVar1 = BCryptGenerateSymmetricKey(local_20,&local_28,(PUCHAR)0x0,0,param_1,param_2,0);
  if (NVar1 < 0) {
    pFVar2 = (FILE *)__acrt_iob_func(2);
    FUN_140001060(pFVar2,"Error: BCryptGenerateSymmetricKey\n",uVar6,uVar5);
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  uVar6 = 0;
  uVar3 = (ulonglong)param_6;
  NVar1 = BCryptEncrypt(local_28,param_5,param_6,(void *)0x0,param_3,param_4,(PUCHAR)0x0,0,local_30,
                        1);
  if (NVar1 < 0) {
    pFVar2 = (FILE *)__acrt_iob_func(2);
    FUN_140001060(pFVar2,"Error: BCryptEncrypt (get size)\n",uVar3,uVar6);
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  uVar3 = (ulonglong)local_30[0];
  hHeap = GetProcessHeap();
  pbOutput = (PUCHAR)HeapAlloc(hHeap,0,uVar3);
  if (pbOutput == (PUCHAR)0x0) {
    pFVar2 = (FILE *)__acrt_iob_func(2);
    FUN_140001060(pFVar2,"Error: HeapAlloc\n",uVar3,uVar6);
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  uVar6 = 0;
  uVar3 = (ulonglong)param_6;
  NVar1 = BCryptEncrypt(local_28,param_5,param_6,(void *)0x0,param_3,param_4,pbOutput,local_30[0],
                        local_18,1);
  if (NVar1 < 0) {
    pFVar2 = (FILE *)__acrt_iob_func(2);
    FUN_140001060(pFVar2,"Error: BCryptEncrypt\n",uVar3,uVar6);
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  *param_7 = pbOutput;
  *param_8 = local_18[0];
  if (local_28 != (BCRYPT_KEY_HANDLE)0x0) {
    BCryptDestroyKey(local_28);
  }
  if (local_20 != (BCRYPT_ALG_HANDLE)0x0) {
    BCryptCloseAlgorithmProvider(local_20,0);
  }
  FUN_140001de0(local_10 ^ (ulonglong)auStackY_98);
  return;
}

まずFUN_1400010d0ではAES暗号をCBCモードで実行している。

void FUN_140001650(longlong param_1,uint param_2)

{
  undefined uVar1;
  uint uVar2;
  uint local_14;
  
  for (local_14 = 0; local_14 < param_2; local_14 = local_14 + 1) {
    uVar2 = FUN_1400015d0(0,param_2 - 1);
    uVar1 = *(undefined *)(param_1 + (ulonglong)local_14);
    *(undefined *)(param_1 + (ulonglong)local_14) = *(undefined *)(param_1 + (ulonglong)uVar2);
    *(undefined *)(param_1 + (ulonglong)uVar2) = uVar1;
  }
  return;
}



int FUN_1400015d0(int param_1,int param_2)

{
  int iVar1;
  int iVar2;
  int iVar3;
  
  iVar1 = rand();
  iVar2 = rand();
  iVar3 = rand();
  return param_1 + (uint)(iVar1 * iVar2 * iVar3) /
                   ((int)(0xffffffff / (ulonglong)((param_2 - param_1) + 1)) + 1U);
}

続くFUN_140001650では、AESで暗号化されたバイト列をランダムに並び替えている。

これらの暗号化(AESのkeyとiv、およびランダムな並び替え)に使用する乱数はsrandで初期化されていて、FUN_140001aa0中の以下のコードになる。


  GetLocalTime(&local_58);
  srand((((((((local_58.wYear + 0x200ab) * 0x200ab + (uint)local_58.wMonth) * 0x200ab +
            (uint)local_58.wDayOfWeek) * 0x200ab + (uint)local_58.wDay) * 0x200ab +
          (uint)local_58.wHour) * 0x200ab + (uint)local_58.wMinute) * 0x200ab +
        (uint)local_58.wSecond) * 0x200ab + (uint)local_58.wMilliseconds);

シード値はGetLocalTimeで取得した現在の日付時刻から作られており、暗号化されたflag.png.laikaのタイムスタンプから2024/9/17 12:49:20(+9:00)まで判明している。

したがって解読方針は、rand関数のシード値をミリ秒以下で総当りして乱数を再現し、ランダム並び替え→AES復号の順で元に戻して、PNGファイルのシグネチャを探せばいい。

ChatGPT-o1-previewにPython上でCの乱数を再現するコードを書いてもらったところ、乱数を出力するexeファイルとPythonファイルの2つが得られた。

rand_generator.c#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "使用法: %s シード値 生成数\n", argv[0]);
        return 1;
    }
    int seed = atoi(argv[1]);
    int num_values = atoi(argv[2]);

    srand(seed);

    for (int i = 0; i < num_values; i++) {
        printf("%d\n", rand());
    }

    return 0;
}
pythonimport subprocess

def get_random_numbers(seed, num_values):
    # 'rand_generator.exe'が同じディレクトリにあると仮定
    cmd = ['rand_generator.exe', str(seed), str(num_values)]
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, check=True)
        output = result.stdout.strip().split('\n')
        random_numbers = [int(num) for num in output]
        return random_numbers
    except subprocess.CalledProcessError as e:
        print("rand_generator.exeの実行中にエラーが発生しました:")
        print(e.stderr)
        return None

# 使用例
seed = 12345
num_values = 5
random_numbers = get_random_numbers(seed, num_values)
print(random_numbers)

しかしながらこれは高速ではなかったため、チームメイトが教えてくれたctypeを使う方法を最終的に採用した。

import ctypes

seed = 1234

msvcrt = ctypes.windll.msvcrt
msvcrt.srand(seed)
msvcrt.rand()

再現した乱数を用いて、次の総当たりソルバーを作成。

from Crypto.Cipher import AES
import ctypes

msvcrt = ctypes.windll.msvcrt

def uint(s):
        return s%4294967296

def createkeys(wSecond,wMilliseconds):
        wYear = 2024
        wMonth = 9
        wDayOfWeek = 2
        wDay = 17
        wHour = 12
        wMinute = 49
        #wSecond = 20
        #wMilliseconds = 0
        
        seed = uint(uint(wYear + 0x200ab)* 0x200ab)
        seed = uint(uint(wMonth + seed)* 0x200ab)
        seed = uint(uint(wDayOfWeek + seed)* 0x200ab)
        seed = uint(uint(wDay + seed)* 0x200ab)
        seed = uint(uint(wHour + seed)* 0x200ab)
        seed = uint(uint(wMinute + seed)* 0x200ab)
        seed = uint(uint(wSecond + seed)* 0x200ab)
        seed = uint(wMilliseconds + seed)
        
        msvcrt.srand(seed)
        
        key = b""
        for i in range(0x20):
                key += int(msvcrt.rand()%256).to_bytes(1, byteorder='big')
        iv = b""
        for i in range(0x10):
                iv += int(msvcrt.rand()%256).to_bytes(1, byteorder='big')
        
        return key,iv


def getrands(s):
        result = []
        for i in range(s):
                result += [msvcrt.rand()]
        return result


def shuffle_pos(min,max,r1,r2,r3):
        s_product = uint(r1 * uint(r2 * r3))
        s_range = uint(max - min + 1)
        s_denominator = uint(0xffffffff // s_range) + 1
        return min + s_product // s_denominator


def shuffle_rev(arr,rands):
        rc = len(rands)-1
        for i in range(0,len(arr),1):
                j = len(arr)-1-i
                uVar2 = shuffle_pos(0,len(arr)-1,rands[rc-2],rands[rc-1],rands[rc])
                uVar1 = arr[j];
                arr[j] = arr[uVar2]
                arr[uVar2] = uVar1
                rc -= 3


cipher_text_o = open('flag.png.laika', 'rb').read()

for tSecond in range(20,21,1):
        for tMilliseconds in range(0,1000,1):
                cipher_text = bytearray(cipher_text_o)
                key,iv = createkeys(tSecond,tMilliseconds)
                cipher = AES.new(key, AES.MODE_CBC, iv)
                rands = getrands(len(cipher_text)*3)
                shuffle_rev(cipher_text,rands)
                decoded = cipher.decrypt(cipher_text)
                
                if b"\x89PNG" in decoded[0:16]:
                        print("hit!!",decoded[0:16]);
                        print("tSecond",tSecond);
                        print("tMilliseconds",tMilliseconds);
                        f = open('flag.png', 'wb')
                        f.write(decoded)
                        f.close()
                        exit()
                else:
                        print("not hit",tSecond,tMilliseconds,decoded[0:16]);

実行すると2024年9月17日12時49分20秒307ミリ秒でヒット。flagが復元された。なお、statement.pngの中身が日本語であったため、GetLocalTimeは日本時間と仮定したが、その通りであった。

The_Kudryavka_Sequence flag

IERAE{3xpec7at1on_1s_a_w0rd_r00ted_1n_g1ving_up.}

OMG (misc warmup)

Oh my God!!!My browser history has been hijacked!

オーマイガー!!!ブラウザの履歴が乗っ取られてしまった!

ブラウザの履歴を書き換えて戻るを潰しに来る広告ウザいよね。

OMG flag

IERAE{Tr3ndy_4ds.LOL}

Futari APIs (web warmup)

curl 'http://34.81.219.110:3000/search?user=peroro'

search?user=でユーザーがヒットしたかどうかを表示するWebページが与えられる。サーバーサイドの処理は二段構え。

frontend.tsconst FLAG: string = Deno.env.get("FLAG") || "IERAE{dummy}";
const USER_SEARCH_API: string = Deno.env.get("USER_SEARCH_API") ||
  "http://user-search:3000";
const PORT: number = parseInt(Deno.env.get("PORT") || "3000");

async function searchUser(user: string, userSearchAPI: string) {
  const uri = new URL(`${user}?apiKey=${FLAG}`, userSearchAPI);
  return await fetch(uri);
}

async function handler(req: Request): Promise<Response> {
  const url = new URL(req.url);
  switch (url.pathname) {
    case "/search": {
      const user = url.searchParams.get("user") || "";
      return await searchUser(user, USER_SEARCH_API);
    }
    default:
      return new Response("Not found.");
  }
}

Deno.serve({ port: PORT, handler });
user-search.tstype User = {
  name: string;
};

const FLAG: string = Deno.env.get("FLAG") || "IERAE{dummy}";
const PORT: number = parseInt(Deno.env.get("PORT") || "3000");

const users = new Map<string, User>();
users.set("peroro", { name: "Peroro sama" });
users.set("wavecat", { name: "Wave Cat" });
users.set("nicholai", { name: "Mr.Nicholai" });
users.set("bigbrother", { name: "Big Brother" });
users.set("pinkypaca", { name: "Pinky Paca" });
users.set("adelie", { name: "Angry Adelie" });
users.set("skullman", { name: "Skullman" });

function search(id: string) {
  const user = users.get(id);
  return user;
}

function handler(req: Request): Response {
  // API format is /:id
  const url = new URL(req.url);
  const id = url.pathname.slice(1);
  const apiKey = url.searchParams.get("apiKey") || "";

  if (apiKey !== FLAG) {
    return new Response("Invalid API Key.");
  }

  const user = search(id);
  if (!user) {
    return new Response("User not found.");
  }

  return new Response(`User ${user.name} found.`);
}

Deno.serve({ port: PORT, handler });

HTTPリクエストをfrontendで受けて、ここからuser-search.tsにHTTPリクエストを投げている。apiKeyにflagが入っているので、これを流出させればいい。

frontendのsearchUserに問題がある。

async function searchUser(user: string, userSearchAPI: string) {
  const uri = new URL(`${user}?apiKey=${FLAG}`, userSearchAPI);
  return await fetch(uri);
}

ここでは検索対象のuser名をURLの一部としてnew URLで結合してfetchでリクエストする先のURLを作成しているが、new URLは第一引数にURLの絶対パスが入っていれば第二引数を無視することが知られている。

通常ではnew URL("Alice?apiKey=FLAG","http://user-search:3000")となるので、作られるURLオブジェクトはhttp://user-search:3000/Alice?apiKey=FLAGとなる。

しかし、user名が絶対パスのとき、new URL("https://hoge.hoge.hoge/?apiKey=FLAG","http://user-search:3000")の場合、後半は無視されて作られるURLオブジェクトはhttps://hoge.hoge.hoge/?apiKey=FLAGになる。

よってuser名にアクセスさせたいURLを指定すればFLAGをつけてfetchでアクセスしてくれる。

IERAE{yey!you_got_a_web_warmup_flag!}