Misc
哈基米得了mvp
附件是一个加密压缩包 encrypted.zip 和明文 plaintext.txt,立马想到已知明文攻击
- 使用 bkcrack 简单检查一下,加密算法是 ZipCrypto,直接确定
- 使用 7zip 制作明文压缩包,注意使用“仅存储”
- 使用下面的命令破解密钥
1
| bkcrack -C hakimi_mvp_challenge.zip -c plaintext.txt -P plain.zip -p plaintext.txt
|
- 得到密钥后直接解密压缩包拿到 flag
1
| bkcrack -C hakimi_mvp_challenge.zip -k a0b1c2d3 e4f5g6h7 i8j9k0l1 -d cracked.zip
|
嗷呜
今年的 misc 白给题,有经验 (找过资源) 的人应该能直接认出来,直接放到兽音解码工具中解码即可
我们的游戏確有問題
主办方在赛程中间突然放出的 misc 游戏题(今年貌似因为时间有限完成度不高),首杀还有特殊奖励
这题发布的时候我还在三食堂啃鸡腿呢,看大伙在群里面哀嚎,我只能用手机面对着 exe 发呆,想着首杀估计是没戏了
回寝室打开电脑一看,游戏图标怎么是个 python?顿时“恶相胆边生😈”:你说,出题人出题的时候会不会忘记做加密/混淆呢?
然后这题就从 misc 题变成了 reverse 题(
- 首先从图标几乎可以肯定用的是 PyInstaller 打包的,但是以防出题人有诈(x),我们用
strings 命令再确认一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| ❯ strings 橘雪莉的奇幻冒险.exe | grep "pyi"
_pyi_main_co
pyi-python-flag
pyi-runtime-tmpdir
pyi-contents-directory
pyi-hide-console
pyi-
_pyinstaller_pyz
mpyimod01_archive
mpyimod02_importers
mpyimod03_ctypes
mpyimod04_pywin32
spyiboot01_bootstrap
spyi_rth_inspect
opyi-contents-directory _internal
|
这下确定是 PyInstaller 打包了
直接使用 pyinstxtractor.py 进行解包,获取文件夹 橘雪莉的奇幻冒险.exe_extracted
找到里面的最大的 pyc 文件 game.pyc,(这一步本来应该还需要使用 struct.pyc 进行补头,但是新版本的 pyinstxtractor 已经帮我们把这一步做完了)。直接使用 pycdc 或是在线工具进行反编译
游戏逻辑
从反编译结果可以看到,正常通关方法是搜集字母拼出单词 kindred,flag 就会出现
但是这个游戏还隐藏了几个后门:
- 用户名使用 kindred 时,直接显示 flag 前半部分,并且解锁传送和全图查看功能,还可以穿墙和加速
- 用户名使用 admin/debug 时,可以查看坐标/内存变量状态
- 在游戏中使用
上上下下左右左右BABA 这个魂斗罗经典秘籍的时候,直接获得 flag 后半部分
- 最后拼出完整答案
flag{kindred_thank_you_for_playing_this_game}

最后拿到了奖品魔审,好耶🥰
星环日志:第7号残片
pdf 题,依旧文件隐写,题目提示是 XOR+?+?
- 首先用纯文本格式打开查看,发现 Object 9 是一个嵌入式文件流(Stream),长度仅 60 字节,且没有标注 Filter(过滤器),这意味着它是裸字节(Raw Bytes)
- 提取出字节流内容:
/:*q6 .*!s{&1 ;# $(r s{&4!$s.!/&. / w$ 确实有很多不可见字符,根据密文的前几个字节以及固定格式 “flag{…}",这里可以猜出密钥应该是 42(宇宙的终极答案) - 接下来使用 42 进行异或,得 base64 结果
ZmxhZ3tFbGlhc19TdGVsbGFyaXNfMjE0N19FdXJvcGFfRW1lcmdlbmN5fQ==,解码得flag{Elias_Stellaris_2147_Europa_Emergency}
月半猫和奶龙真是一对苦命鸳鸯
一开始用 stegsolve 叠了半天,看胖猫奶龙的缝合怪把我 san 值都要干没了,最后发现放进随波逐流一把梭了(,两个图片RG0和00B拼起来就是 flag

阿萨拉电台
音频隐写题,由题目的提示 “you could see the audio”,首先使用频谱分析,但是分析无果。后面怀疑使用 LSB 在每个采样点的最低位进行隐写,经测试正好发现了 PNG 文件头89 50 4E 47 0D 0A 1A 0A。编写下方脚本可以提取出一个二维码,扫描即可得出 flag
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| import numpy as np
from scipy.io import wavfile
def solve():
fs, data = wavfile.read('asara_radio_station.wav')
lsb = data & 1
lsb_bytes = np.packbits(lsb)
png_signature = b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'
lsb_data = lsb_bytes.tobytes()
start_offset = lsb_data.find(png_signature)
if start_offset != -1:
print(f"在偏移量 {start_offset} 处发现 PNG ⽂件头!")
png_data = lsb_data[start_offset:]
with open('flag.png', 'wb') as f:
f.write(png_data)
print("成功提取⽂件: flag.png")
print("请打开 flag.png 查看最终 Flag。")
else:
print("未在 LSB 数据中发现 PNG ⽂件头。")
if __name__ == '__main__':
solve()
|
怨念:每次有二维码的题都要卡半天,去年曼彻斯特的也是(吐血
Crypto
Broadcast Mayday!
当同一个明文消息 $m$ 使用相同的小公钥指数 $e$(这里 $e=3$)加密发送给 $k$ 个不同的接收者(模数为 $n_1, n_2, \dots, n_k$),且 $k \ge e$ 时,我们可以利用中国剩余定理 (CRT) 恢复出明文。
$$
\begin{cases}
c_1 \equiv m^e \pmod{n_1} \\
c_2 \equiv m^e \pmod{n_2} \\
c_3 \equiv m^e \pmod{n_3}
\end{cases}
$$$$ C \equiv m^e \pmod{n_1 \cdot n_2 \cdot n_3} $$解题脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
| import sympy
import gmpy2
# --- 题目给出的数据 ---
e = 3
n1 = 92115348414647145744942290482438108731351762209411954562581348940707868731036245627500433519371993679505350448706770557161219940579492266005829570662132118431769877704072034110899075752607538375198156669112004335086224921217459978021792019801412747035487994163136912896890937398834748390623372881042019936713
c1 = 37200871830656989509585109324571909904756015127793123197130883254106018377171885657835514800162401320146006985022958231927796179533942338054373296193632356913787597295032396539550096310631833715431578658537578254861531650216064367780472605097680343437199708398358791853856381695077
n2 = 107724705233055883081751721820024537967383348784274707683197256750962086833992211892130729806540411724416536258297016669042940442848435141693540234286837159113373010687953933673937825312480194072635181507552396292463829107673816320217815715976290351449432206814810274039955958399317467366451913130432389085003
c2 = 37200871830656989509585109324571909904756015127793123197130883254106018377171885657835514800162401320146006985022958231927796179533942338054373296193632356913787597295032396539550096310631833715431578658537578254861531650216064367780472605097680343437199708398358791853856381695077
n3 = 94872673633406396995297289835600763806262337074512934673113566808569442630985008762887860592268702511296878448137078036296381031226430144507819817141157483185804334623630822323025118948888645875600836830885573655432196307004742051095606839449546166172293018214114182272767946968456915879022656708269505066749
c3 = 37200871830656989509585109324571909904756015127793123197130883254106018377171885657835514800162401320146006985022958231927796179533942338054373296193632356913787597295032396539550096310631833715431578658537578254861531650216064367780472605097680343437199708398358791853856381695077
# --- 攻击过程 ---
print("开始执行哈斯塔德广播攻击...")
# 1. 使用中国剩余定理 (CRT) 求解 M = m^e
# sympy.ntheory.modular.crt 的参数是 (模数列表, 余数列表)
moduli = [n1, n2, n3]
remainders = [c1, c2, c3]
# crt 返回一个元组 (解, 模数的乘积)
M, N = sympy.ntheory.modular.crt(moduli, remainders)
print(f"1. 使用 CRT 求得 M = m^{e} = {M}")
print(f" 模数的乘积 N = n1*n2*n3 = {N}")
print("-" * 20)
# 2. 对 M 开 e 次方根,得到 m
# 使用 gmpy2.iroot 进行精确的整数开方,它返回 (根, 是否为完美幂)
m, is_perfect_root = gmpy2.iroot(M, e)
if is_perfect_root:
print(f"2. 成功对 M 开 {e} 次方根,得到 m = {m}")
print("-" * 20)
# 3. 将整数 m 转换为字符串 (flag)
# 计算将整数 m 转换为字节所需的长度
byte_length = (m.bit_length() + 7) // 8
flag_bytes = m.to_bytes(byte_length, "big")
try:
flag = flag_bytes.decode("utf-8")
print(f"3. 将 m 转换为字符串,得到 Flag:")
print(flag)
except UnicodeDecodeError:
print("3. 无法解码为 UTF-8,原始字节为:")
print(flag_bytes)
else:
print(f"2. M 不是一个完美的 {e} 次方,攻击失败。")
print(" 这可能意味着模数不互质,或者这不是一个标准的广播攻击。")
|
Secure Token?
这题的目标已经说明:伪造一个具有管理员权限的 token,从而访问 /admin 页面
那么我们来复习一下 MD5、SHA1、SHA256 这些算法的核心逻辑:
初始化: 算法有一个固定的初始状态(IV)。
第一棒(处理 Block 1): 拿着 IV 和第一块数据进行复杂的运算,得出一个中间状态(State)。
第二棒(处理 Block 2): 拿着“第一棒的中间状态”作为起点,和第二块数据运算,得出新的状态。
…以此类推…
终点: 处理完所有数据后,最后的中间状态就是我们看到的哈希值(Signature)。
这里的漏洞就在于:如果我们知道了中间状态,即使不知道前面的数据是什么,依然能接力继续计算出新的哈希值
本题使用了这个验证方式 $Token=SHA1(SECRET_KEY+username)$,即使不知道前面的数据(SECRET_KEY)是什么,也可以接过这一棒,继续往后添加数据,算出新的哈希值
哈希算法在处理数据前,一定会对数据进行填充,使其长度满足整块的要求(64字节倍数)。 原本服务器做的事情是: SHA1( [Key] [guest] [Padding] ) –> 得到 Token(旧)
这里的 [Padding] 是根据 len(Key) + len(“guest”) 自动生成的,通常包含 \x80、一堆 \x00 和 数据总长度。
我们要在这个基础上“续写”数据。既然我们要利用旧的 Token 继续算,那么新的数据结构必须严格遵守之前的物理顺序:伪造的数据流 = [Key] [guest] [Padding] [admin]
然后使用使用 hashpump,继续处理字符串 “admin”
以下是爆破 key 长度加伪造数据的脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
| from pwn import *
import hashpumpy
import re
# 配置连接信息
HOST = '47.108.129.134'
PORT = 33769 # 请确保端口与你当前的题目实例一致
# 开启 pwntools 的日志,方便看过程(如果觉得太吵可以改成 'error')
context.log_level = 'info'
def attack():
# 原始数据和要附加的数据
original_data = 'guest'
append_data = 'admin'
# 我们不知道密钥长度,需要爆破 (假设长度在 1-64 之间)
# 虽然 Token 变了,但通常密钥的长度是固定的配置
for key_length in range(1, 64):
try:
p = remote(HOST, PORT)
# 1. 读取服务器的欢迎信息
# 读取直到提示输入的地方,这样我们就拿到了上面所有的文本
welcome_msg = p.recvuntil(b"Enter 'username:token' to login (raw bytes allowed): ")
# 2. 使用正则提取当前的 Guest token
# 寻找 "Guest token: " 后面的 40 位 hex 字符
# output decode 为 string 方便正则匹配
match = re.search(r'Guest token: ([a-f0-9]{40})', welcome_msg.decode(errors='ignore'))
if not match:
log.failure("Could not extract token from server response.")
p.close()
continue
current_token = match.group(1)
log.info(f"[*] Key Len Guess: {key_length} | Captured Token: {current_token}")
# 3. 使用 HashPump 计算新的签名和 payload
# 参数: (current_hash, original_data, data_to_add, key_length)
new_hash, new_message = hashpumpy.hashpump(current_token, original_data, append_data, key_length)
# 4. 发送 Payload
# 格式: username:token
# new_message 已经是包含 padding 的 bytes 了,无需 encode
payload = new_message + b':' + new_hash.encode()
p.sendline(payload)
# 5. 获取结果
response = p.recvall(timeout=2).decode(errors='ignore')
p.close()
# 6. 检查 Flag
if 'flag{' in response or 'success' in response.lower():
print("\n" + "="*50)
print(f"[+] SUCCESS! Found Flag with key length: {key_length}")
print("Response content:")
print(response)
print("="*50 + "\n")
return # 拿到 flag 直接退出函数
# 如果没有 flag,说明这个 key_length 不对(或者服务器真的在校验 key 内容是否正确)
# 继续下一次循环
except Exception as e:
log.warning(f"Connection error or exception: {e}")
try:
p.close()
except:
pass
if __name__ == "__main__":
attack()
|