1st TQLCTF Review
Misc
wordle
附件中给出了如下源代码以及需要用到的单词。
import os
import random
from flag import award
random.seed(os.urandom(64))
with open('allowed_guesses.txt', 'r') as f:
allowed_guesses = set([x.strip() for x in f.readlines()])
with open('valid_words.txt', 'r') as f:
valid_words = [x.strip() for x in f.readlines()]
MAX_LEVEL = 512
GREEN = '\033[42m \033[0m'
YELLOW = '\033[43m \033[0m'
WHITE = '\033[47m \033[0m'
def get_challenge():
# id = random.getrandbits(32)
# answer = valid_words[id % len(valid_words)]
# return hex(id)[2:].zfill(8), answer
# To prevent the disclosure of answer
id = random.randrange(len(valid_words) * (2 ** 20))
answer = valid_words[id % len(valid_words)]
id = (id // len(valid_words)) ^ (id % len(valid_words))
return hex(id)[2:].zfill(5), answer
def check(answer, guess):
answer_chars = []
for i in range(5):
if guess[i] != answer[i]:
answer_chars.append(answer[i])
result = []
for i in range(5):
if guess[i] == answer[i]:
result.append(GREEN)
elif guess[i] in answer_chars:
result.append(YELLOW)
answer_chars.remove(guess[i])
else:
result.append(WHITE)
return ' '.join(result)
def game(limit):
round = 0
while round < MAX_LEVEL:
round += 1
id, answer = get_challenge()
print(f'Round {round}: #{id}')
correct = False
for _ in range(limit):
while True:
guess = input('> ')
if len(guess) == 5 and guess in allowed_guesses:
break
print('Invalid guess')
result = check(answer, guess)
if result == ' '.join([GREEN] * 5):
print(f'Correct! {result}')
correct = True
break
else:
print(f'Wrong! {result}')
if not correct:
print('You failed...')
return round - 1
return MAX_LEVEL
def choose_mode():
print('Choose gamemode:')
print('0: Easy mode')
print('1: Normal mode')
print('2: Hard mode')
print('3: Insane mode')
# print('4: Expert mode')
# print('-1: Osu! mode')
mode = int(input('> '))
assert 0 <= mode <= 3
return mode
if __name__ == '__main__':
print('Guess the WORDLE in a few tries.')
print('Each guess must be a valid 5 letter word.')
print('After each guess, the color of the tiles will change to show how close your guess was to the word.')
while True:
mode = choose_mode()
if mode == 0:
limit = 999999999
else:
limit = 7 - mode
final_level = game(limit)
if final_level < MAX_LEVEL:
pass
else:
print('You are the Master of WORDLE!')
flag = award(mode, final_level)
print(f'Here is you award: {flag}')
其中关键部分是 while True
和 random.seed(os.urandom(64))
。在一轮游戏结束之后由于循环使得随机数种子不会变换。而使用模式 0 可以爆破猜解单词从而算出生成的随机数,因此可以尝试随机数预测。
环境搭建
附件中的源码将获取 flag 的逻辑去掉之后可以成功运行。因此用 ncat 监听一个端口即可模拟远程。在 Linux 下使用如下指令即可。
ncat -lvp 1234 -e "/usr/bin/python3 main.py"
Windows 下的 Netcat 由于换行的差异会导致脚本与实际远程不一致。
随机数预测
模式 0 下的游戏拥有很多次猜解机会,因此可以尝试使用正则的办法逐步排除,从而得出正确的单词。进一步可以使用如下代码计算出生成的随机数 id。
hexId = int(hexId, 16)
moded = WORDS.index(word)
id = (hexId ^ moded) * len(WORDS) + moded
其中的 hexId 是题目给出的 round 的 id。
使用 randcrack 库可以完成随机数预测的工作,只需要收集 624 个正确的结果即可预测后面 624 个随机数。因此写出如下代码来梭这道题。
# wordleLib
import pwn
import re
import string
FILE = open("G:\\valid_words.txt", "rb").read().decode()
WORDS = FILE.split("\n")
GREEN = '\033[42m \033[0m'
YELLOW = '\033[43m \033[0m'
WHITE = '\033[47m \033[0m'
def guess(proc: pwn.remote):
print("=== Guess Start ===")
# Receive the hexId
# print(f"Receive generated id {proc.recvline().decode().strip()}")
line = proc.recvline_regex(r"Round (\d{1,3}): #([\w\d]+)\n")
round, hexId = re.findall(r"Round (\d{1,3}): #([\w\d]+)", line.decode())[0]
round = int(round)
print(f"Round {round}, hexId {hexId}")
wrongs = []
alphabet = string.ascii_lowercase
MATCH = [f"[{alphabet}]", f"[{alphabet}]", f"[{alphabet}]", f"[{alphabet}]", f"[{alphabet}]"]
while True:
word = ""
matches = re.findall(r"".join(MATCH), FILE)
for match in matches:
if match not in wrongs:
word = match
break
# Send Guess
proc.sendline(word)
review = proc.recvline().decode().replace(GREEN, "G").replace(YELLOW, "Y").replace(WHITE, "W")[-10:-1].split(
" ")
for i, char in enumerate(review, 0):
if char == 'W':
alphabet = alphabet.replace(word[i], "")
elif char == 'Y':
MATCH[i] = MATCH[i].replace(word[i], "")
else:
MATCH[i] = f"[{word[i]}]"
if "W" in review or "Y" in review:
wrongs.append(word)
else:
print(f"Correct with {word} in {WORDS.index(word)}")
# Calculate id
hexId = int(hexId, 16)
moded = WORDS.index(word)
id = (hexId ^ moded) * len(WORDS) + moded
print(f"Calculated Id {id}")
return id
def bingo(proc: pwn.remote, crack: int):
print("=== Bingo Start ===")
# print(f"Receive generated id {proc.recvline().decode().strip()}")
line = proc.recvline_regex(r"Round (\d{1,3}): #([\w\d]+)\n")
round, hexId = re.findall(r"Round (\d{1,3}): #([\w\d]+)", line.decode())[0]
round = int(round)
answer = WORDS[crack % len(WORDS)]
id = (crack // len(WORDS)) ^ (crack % len(WORDS))
print(f"round {round}, hexId is {hex(id)[2:].zfill(5)}, answer is {answer}, crack Id is {crack}")
assert hex(id)[2:].zfill(5) == hexId
proc.sendline(answer)
print(proc.recvline().decode())
# wordle
from randcrack import RandCrack
from pwn import *
from wordleLib import guess, bingo
MODE = 0
RC = RandCrack()
proc = remote("192.168.5.129", 1234)
proc.recvuntil(b'> ')
proc.sendline(str(MODE).encode())
for x in range(512):
if x > 350:
print(f"Predict next number as {RC.predict_randrange(4090 * (2 ** 20))}")
RC.submit(guess(proc))
else:
guess(proc)
proc.recvuntil(b'> ')
proc.sendline(str(MODE).encode())
for x in range(512):
print(f"Predict next number as {RC.predict_randrange(4090 * (2 ** 20))}")
RC.submit(guess(proc))
MODE = 3
proc.recvuntil(b'> ')
proc.sendline(str(MODE).encode())
for x in range(512):
bingo(proc, RC.predict_randrange(4090 * (2 ** 20)))
proc.interactive()
完成两轮模式 0 的游戏之后即可将模式改为 3 并预测游戏结果。进而在游戏限制下通过模式 3。
Ranma½
将附件使用如下 CyberChef Receipt 处理后可得相对可读的内容。
Decode_text('UTF-8 (65001)')
也可以使用 vim 的 set fileencoding 将编码转为 ANSI。
KGR/QRI 10646-1 zswtqgg d tnxcs tsdtofbrx osk ndnzhl gna Ietygfviy Idoilfvsu Arz (QQJ) hkkqk maikaglvusv ubyp cw ekg krzyj'o kitwkbj alypsdd. Wjs rzvmebrwoa duwcuosu pqecgqamo cw ekg IFA, uussmpu, ysum aup qfxschljyk swks pcbb khxnsee drdoqpgpwfyv cbg xeupctzou, oql gneg ylv nsg bb zds upygzrxzkjh fq XVT-8, wpr uxxvnw qt wpvy isdz. XVT-8 kif zds tsdtofbrxegktf qt szryafmtqi hkm sahz LD-DUQLQ egjuv, auqjllvtc qfxschljvrehp hlvv iqyk omjehog, sieyafj lqf cwprx ocwezcfh bugp fvwb qb XA-NYYWZ gdniha oap oip wtoqacgnsee wq cwprx rocfhu. HTTPZB{QFOLP6_KRZ1Q}
猜测是 Vigenère 加密,因此直接爆破密钥可得 codingworld
。解密内容后可得如下信息,flag 位于其末尾。
ISO/IEC 10646-1 defines a large character set called the Universal Character Set (UCS) which encompasses most of the world's writing systems. The originally proposed encodings of the UCS, however, were not compatible with many current applications and protocols, and this has led to the development of UTF-8, the object of this memo. UTF-8 has the characteristic of preserving the full US-ASCII range, providing compatibility with file systems, parsers and other software that rely on US-ASCII values but are transparent to other values. TQLCTF{CODIN6_WOR1D}
TQLCTF{CODIN6_WOR1D}
UTF-8 可变长编码
附件采用的编码其实是 UTF-8,但是经过了特殊的变长编码。
因此可以使用脚本来一把梭解码并完成这道题。
from libcodebusters import Decrypt
from morse3 import Morse
data = open("flag_4c7b25b7ade73ac3a6b3081c81633fe6", "rb").read()
text = morse = key = ""
for x in range(len(data)):
if data[x] >= 0xE0:
text += chr(((data[x] ^ 0xE0) << 12) + ((data[x + 1] ^ 0x80) << 6) + (data[x + 2] ^ 0x80))
morse += " "
elif data[x] >= 0xC0:
text += chr(((data[x] ^ 0xC0) << 6) + (data[x + 1] ^ 0x80))
morse += "-"
elif data[x] >= 0x80:
continue
else:
text += chr(data[x])
morse += "."
morse = morse[:43]
key = Morse(morse).morseToString()
print(Decrypt.vigenere(text, key))
the Ohio State University
将谱面文件解压后筛选出修改时间更新的文件,发现有两个难度的谱面被修改了。在官网下载原版的谱面可以进行比较。
谱面二进制
VIVID 难度的谱面的末尾被改成了绝赞纵连,而且重复度很高。
考虑是两个拍子合起来作为一个八位二进制存储信息。因此写出下列脚本将内容提取出来。
import re
data = open("./Modified/MisoilePunch - VVelcome!! (Fresh Chicken) [VIVID].osu", "rb").read().decode()
data = re.findall(r"(\d{2,3}),192,(\d{6}),1,0,0:0:0:0:", data)
now = 0
byte = []
chars = 0
for position, time in data:
if int(time) > now:
if now != 0:
byte.append(f"{chars:04d}")
chars = 0
now = int(time)
if position == "64":
chars += 1000
elif position == "192":
chars += 100
elif position == "320":
chars += 10
elif position == "448":
chars += 1
else:
pass
byte = "".join(byte)
byte = [chr(int(byte[x : x + 8], 2)) for x in range(0, len(byte), 8)]
byte = "".join(byte)
print(byte[66 : -5 : 4])
得到的内容如下。
5HoWtIme}
SilentEye wave 隐写
在 BASIC 的谱面中可以发现如下信息。
WAVPassword: MisoilePunch
结合文件中被修改的音频,使用 SilentEye 尝试进行解码。
可以得到如下信息。
_TO_O$u_i7s_
Steghide 隐写
封面图的属性中可以发现一段信息。
pwd: VVelcome!!
使用上述的密码对图片施加 steghide 可得如下内容。
TQLCTF{VVElcOM3
将上述得到的信息拼接即可得到 flag。
TQLCTF{VVElcOM3_TO_O$u_i7s_5HoWtIme}
osu! 好难,我还在打三星的谱子,不会打 Mania。