Featured image of post SWJTU-CTF-25 新秀杯 WP

SWJTU-CTF-25 新秀杯 WP

Misc

哈基米得了mvp

附件是一个加密压缩包 encrypted.zip 和明文 plaintext.txt,立马想到已知明文攻击

  1. 使用 bkcrack 简单检查一下,加密算法是 ZipCrypto,直接确定
  2. 使用 7zip 制作明文压缩包,注意使用“仅存储”
  3. 使用下面的命令破解密钥
1
bkcrack -C hakimi_mvp_challenge.zip -c plaintext.txt -P plain.zip -p plaintext.txt
  1. 得到密钥后直接解密压缩包拿到 flag
1
bkcrack -C hakimi_mvp_challenge.zip -k a0b1c2d3 e4f5g6h7 i8j9k0l1 -d cracked.zip

嗷呜

今年的 misc 白给题,有经验 (找过资源) 的人应该能直接认出来,直接放到兽音解码工具中解码即可

我们的游戏確有問題

主办方在赛程中间突然放出的 misc 游戏题(今年貌似因为时间有限完成度不高),首杀还有特殊奖励

这题发布的时候我还在三食堂啃鸡腿呢,看大伙在群里面哀嚎,我只能用手机面对着 exe 发呆,想着首杀估计是没戏了

回寝室打开电脑一看,游戏图标怎么是个 python?顿时“恶相胆边生😈”:你说,出题人出题的时候会不会忘记做加密/混淆呢? 然后这题就从 misc 题变成了 reverse 题(

  1. 首先从图标几乎可以肯定用的是 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 打包了

  1. 直接使用 pyinstxtractor.py 进行解包,获取文件夹 橘雪莉的奇幻冒险.exe_extracted

  2. 找到里面的最大的 pyc 文件 game.pyc,(这一步本来应该还需要使用 struct.pyc 进行补头,但是新版本的 pyinstxtractor 已经帮我们把这一步做完了)。直接使用 pycdc 或是在线工具进行反编译

  3. 游戏逻辑 从反编译结果可以看到,正常通关方法是搜集字母拼出单词 kindred,flag 就会出现 但是这个游戏还隐藏了几个后门:

  1. 用户名使用 kindred 时,直接显示 flag 前半部分,并且解锁传送和全图查看功能,还可以穿墙和加速
  2. 用户名使用 admin/debug 时,可以查看坐标/内存变量状态
  3. 在游戏中使用 上上下下左右左右BABA 这个魂斗罗经典秘籍的时候,直接获得 flag 后半部分
  1. 最后拼出完整答案 flag{kindred_thank_you_for_playing_this_game}

手刹已拿 和出题人的对话

最后拿到了奖品魔审,好耶🥰

星环日志:第7号残片

pdf 题,依旧文件隐写,题目提示是 XOR+?+?

  1. 首先用纯文本格式打开查看,发现 Object 9 是一个嵌入式文件流(Stream),长度仅 60 字节,且没有标注 Filter(过滤器),这意味着它是裸字节(Raw Bytes)
  2. 提取出字节流内容:/:*q6 .*!s{&1 ;# $(r s{&4!$s.!/&. / w$ 确实有很多不可见字符,根据密文的前几个字节以及固定格式 “flag{…}",这里可以猜出密钥应该是 42(宇宙的终极答案)
  3. 接下来使用 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 这些算法的核心逻辑:

  1. 初始化: 算法有一个固定的初始状态(IV)。

  2. 第一棒(处理 Block 1): 拿着 IV 和第一块数据进行复杂的运算,得出一个中间状态(State)。

  3. 第二棒(处理 Block 2): 拿着“第一棒的中间状态”作为起点,和第二块数据运算,得出新的状态。

  4. …以此类推…

  5. 终点: 处理完所有数据后,最后的中间状态就是我们看到的哈希值(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()
Licensed under CC BY-NC-SA 4.0