简介:2021年中山市香山杯CTF竞赛完整赛题资源包,面向CTF参赛者、网络安全学习者与战队复盘训练,整理自正式赛题,可用于题目复现、思路拆解与技能提升。整个压缩包共41个文件,约17.3MB,涵盖MISC、PWN、Crypto、Reverse四大方向;题目图片以png/jpg呈现,Python脚本提供解题或加密逻辑,wav音频用于隐写分析,zip为打包附件或容器,txt与rtf包含提示、说明或Flag,目录结构清晰便于按模块检索。已有495人学习下载。包内包含MISC方向的脚本分析、音频隐写、二维码识别、密码爆破等题型,PWN方向的堆/栈利用题,Crypto方向的RSA与简单密码题,以及Reverse方向的Python逆向题,覆盖多种常见考点。读者可对照题目图片与附件自行尝试,借助Python脚本还原加密与解题过程,有助于赛事复盘、新手入门和赛前冲刺训练。
1. 香山杯 2021 CTF 真题包:一份能把 Misc、Crypto、Reverse 全流程跑通的训练靶场
做 CTF 最怕的不是题难,是找不到「有答案、有附件、能反复练」的成套题目。中山市香山杯 2021 的这份题目 zip 恰好补齐了这个缺口——它把 Misc、Crypto、Reverse、PWN 四个方向按真实比赛目录打包,从签到级的图片隐写、二维码修复,到需要写脚本攻击的 ezrsa、Python 逆向 ez_py,再到堆题 build_your_house,难度是梯度上升的。对准备入门或者想系统性刷题的人来说,这份资源的价值在于:你不用再去各个平台零散找题,解压后按目录就能模拟一次完整的参赛过程。正文里涉及的文件名、分类结构都来自题包本身,下文讲的解法也是这个题包场景下最主流的思路。
先说清楚它的题包结构:压缩根目录下按赛题分类命名,MISC-开头的是杂项,crypto-和Crypto-是密码学,reverse-是逆向,PWN-是二进制漏洞利用,每个子目录里至少带一个timu.jpg或题目.jpg作为题干附图。这种组织方式对训练特别友好——每一类题都是一个自包含的最小复现单元,解完一道就是一次完整的方法论演练。下面按方向逐个拆。
2. Misc 题组实战:从图片像素到音频频谱的隐写排查链路
Misc 是 CTF 里最讲「套路」的方向,香山杯这份题包的 Misc 题目覆盖了隐写的几条主干:图片 LSB、文件拼接、音频频谱、二维码修复、压缩包伪加密。逐个过一遍,基本就是一次完整的信息隐藏排查流程。
2.1 i_am_scriptkids:先从 base.txt 找线索再去看图
这个题目录下就两个文件:base.txt和timu.jpg。常规做法是先读base.txt,里面大概率是一段 Base64 编码的字符串,解码后可能是另一段编码、一段提示语,或者直接指向图片里的隐写位置。CTF 里拿文本文件当入口,常见思路是「编码层层剥」,Base64 之后可能跟 Base32、十六进制,甚至栅栏密码。
# 先用命令行原地解码,不要急着开脚本 cat base.txt | base64 -d > decoded.txt file decoded.txt如果file命令显示decoded.txt是 ASCII text,就继续读内容;如果显示是图片或压缩包,说明解码结果本身就是个文件,需要改后缀再处理。这里的关键点是:解码后先file一下,别直接cat,二进制文件直接打印到终端会把终端搞乱,这是新手最容易踩的第一步。
处理完文本再看timu.jpg,用binwalk扫一遍,然后看一眼文件尾部有没有追加数据:
binwalk timu.jpg # 常见输出:Zlib 压缩数据、RAR 压缩包、PNG 图片 # 如果发现偏移量,用 dd 切割 dd if=timu.jpg of=hidden.zip bs=1 skip=偏移量binwalk扫的是文件签名,JPEG 末尾追加 ZIP 或另一个图片是 Misc 题最经典的藏法,没有之一。切出来的文件是压缩包就继续解,是图片就用 010 Editor 之类的工具对比两张图的十六进制差异。
2.2 miao 与「你悟了吗」:音频频谱图和 RTF 文档里的文字隐藏
misc-miao目录里有miao.wav和题目图。音频隐写优先查频谱:用 Audacity 打开 wav,切到频谱图视图(View → Spectrogram),把视图拉宽,看看有没有一段频率上排列规律的点阵或文字。这种题在香山杯这类比赛里出现频率很高,因为出题成本低——录一段猫叫,然后把 flag 文字以特定频率混进去就行。
操作路径:Audacity 打开 miao.wav → 点击轨道左侧的下拉箭头 → 选择 Spectrogram → 缩放比例调大,横向拉伸到能看到完整频谱 → 观察 2kHz-8kHz 区间是否有规律纹理参数上注意,频谱图默认的窗口大小是 1024,分辨率不够时高频细节会糊,设置里把 Window size 调到 2048 或 4096,能有效提升文字可读性。如果频谱里看到的文字是反的,用 Audacity 的「反转」效果处理后再看,这是老选手都知道的细节。
MISC-你悟了吗目录里是你悟了吗.jpg和八卦.rtf。RTF 文件本质是纯文本格式,直接用文本编辑器打开就能看到控制字和内容文本。但 Misc 题里 RTF 的常见套路是:把真正内容隐藏在十六进制层面,或者文档里嵌入了对象。用strings直接扫:
strings -e l 八卦.rtf | head -50 # -e l 表示扫描 UTF-16LE 编码的字符串,RTF 嵌入对象常用这种编码strings默认只处理 ASCII,遇到 UTF-16 的中文内容会漏掉。加了-e l之后,嵌入的 Unicode 文本会原形毕露。如果strings扫出来的是大段不可读控制字,就把 RTF 拖进 010 Editor,搜object关键词定位嵌入对象。
2.3 MISC-qrcode:二维码残缺时的两种修复手法
这个目录是qrcode.jpg加题目.jpg。二维码题的主流考法有三个:定位角被遮挡、二维码只有局部、二维码被 PS 过颜色。先打开图片看定位角(三个角落的方块)是否完整,不完整就补角;完整但扫不出来,就对比题目.jpg找线索。
# 安装 zbar 后直接命令行识别 zbarimg qrcode.jpg # 如果报错 NULL,说明二维码有损坏,需要图像修复遇到扫不出来的情况,我一般是先把图片丢进 Photoshop,用「阈值」调整把灰度图转纯黑白,再手动补全三个定位角。二维码容错率分 L/M/Q/H 四级,L 级只能容忍约 7% 的破损,Q 级以上能到 30%。如果题图破损集中在边缘,可以用 Python 的pyzbar配合 OpenCV 做形态学处理试试:
import cv2 from pyzbar import pyzbar img = cv2.imread("qrcode.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 膨胀 + 腐蚀,把断裂的模块连起来 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) dilated = cv2.dilate(gray, kernel, iterations=2) _, thresh = cv2.threshold(dilated, 127, 255, cv2.THRESH_BINARY) decoded = pyzbar.decode(thresh) print(decoded)cv2.dilate的 iterations 参数控制膨胀强度,设 2 是经验值,太大容易把相邻模块粘在一起,二维码反而更读不出来。pyzbar.decode返回的对象里,.data就是二维码承载的原始内容,可能是字符串也可能是二进制文件头。
2.4 MISC-BrokenPassword:伪装加密的 zip 与 babyrgb 的像素级操作
MISC-BrokenPassword目录里是flag.zip、1.jpg和babyrgb目录。先看 zip:用压缩包工具打开,如果能预览到内部文件名但解压要密码,多半是伪加密——zip 的加密标志位被改了,不是真加密。伪加密的特征是文件头第 6、7 字节为奇数。处理方式两种:用 ZipCenOp 之类的工具修复标志位,或者直接改十六进制。
# 十六进制方式处理伪加密:定位 50 4B 01 02(中央目录文件头) # 找到加密标志位(通用位标志的 bit 0),把 09 00 改成 08 00 # 保存后重新解压,通常密码校验会直接跳过真加密的 zip 也别急着上爆破工具,先看同目录下的1.jpg,文件名带1往往暗示有多张图或者其他提示。babyrgb看名字就是 RGB 通道分离题——一张图把 R、G、B 三个通道分别存成文件,或者一个像素值里藏着另一张图的编码。用 Python 拆通道:
from PIL import Image import numpy as np img = np.array(Image.open("题目.jpg")) r, g, b = img[:, :, 0], img[:, :, 1], img[:, :, 2] # 常见套路:某个通道的低位全是隐写数据 # 提取低位并还原成 0-255 灰度 secret = (r & 0x0F) << 4 Image.fromarray(secret).save("extracted.png")r & 0x0F取的是红色通道的低 4 位,<< 4把低 4 位抬到高 4 位,这样灰度图才有对比度,直接存低 4 位会得到一张几乎全黑的图。如果提取出来还是看不出内容,就换 g、b 通道,或者改成提取最低 1 位再乘 255。
3. Crypto 题组拆解:ezrsa 的数学攻击面与 simpleCrypto 的代码审计
Crypto 方向这份题包给我最深的印象是「题目不长,但每个都卡在一个具体的攻击模型上」。crypto-ezrsa和Crypto-MyBot、Crypto-simpleCrypto分别对应 RSA 攻击脚本、Python 源码审计、自定义加密算法逆向。做这类题的节奏是:先看timu.jpg或task.py里给了什么,再决定用什么攻击模型。
3.1 ezrsa:拿到加密脚本先判断是低加密指数还是共模攻击
ezrsa目录下是加密脚本和timu.jpg。RSA 题的核心永远是「从已知量推未知量」。拿到脚本先看三件事:e多大、有没有给出两组相同的n、p和q是否直接泄露。如果e=3且消息很短,低加密指数攻击直接开立方根;如果同一个n配了两个不同的e,那就是共模攻击;如果n是 512 位以内,直接上 YAFU 分解。
# 低加密指数攻击:m^e = c,当 m^e < n 时直接开方 from gmpy2 import iroot c = 0x # 题目给的密文 e = 3 n = 0x # 模数 m, exact = iroot(c, e) if exact: # 转换成字节 flag = int(m).to_bytes((int(m).bit_length() + 7) // 8, "big") print(flag)iroot返回两个值,第一个是整数根,第二个是布尔值,表示是否开方精确。注意判断条件必须是exact为 True 才说明m^e没超过n,否则结果是错的。如果开立方不精确,就说明明文被 padding 过了,要走 Coppersmith 或爆破填充位。
共模攻击的脚本更固定——两组(e1, c1)和(e2, c2)共享同一个n,用扩展欧几里得算出s1和s2,满足e1*s1 + e2*s2 = 1,然后m = (c1^s1 * c2^s2) mod n。
# 共模攻击核心 def egcd(a, b): if b == 0: return a, 1, 0 g, x, y = egcd(b, a % b) return g, y, x - (a // b) * y _, s1, s2 = egcd(e1, e2) if s1 < 0: s1 = -s1 c1 = pow(c1, -1, n) # s2 同理取反 m = (pow(c1, s1, n) * pow(c2, s2, n)) % n这里最容易翻车的是s1或s2为负数的情况——Python 的pow不支持负指数,必须先把对应的密文取模逆元,再把指数转正。pow(c1, -1, n)是 Python 3.8+ 的内置模逆运算,老版本得用gmpy2.invert(c1, n)。血泪经验:gmpy2 没装,一到算逆元就抛异常。
3.2 Crypto-MyBot:从 task.py 里读出加密的「黑匣子」
这个题给的是task.py和timu.jpg。MyBot 这类题目名往往是个幌子,真正要分析的是 Python 源码里的加密逻辑。打开task.py先别逐行读,先搜关键词:def、encrypt、flag、xor、ord。很多题目写的所谓「加密」,本质就是把 flag 和某个 key 做异或,key 可能藏在图片里或者由图片文件名暗示。
# 异或加密的通用还原模板 cipher = [0x1F, 0x2A, 0x33] # 密文字节 key = b"the_key" # 从题目其他文件推断或爆破 plain = bytes([c ^ key[i % len(key)] for i, c in enumerate(cipher)]) print(plain)异或还原的关键是拿到 key。如果 key 长度未知,先观察密文长度和可能的明文格式——flag 一般以flag{开头,用cipher[0] ^ ord('f')就能推出 key 的第一个字节,以此类推可以还原完整 key。如果 task.py 里用了random.seed(),那就更简单——种子固定,随机序列可复现。
3.3 Crypto-simpleCrypto:自定义加密算法的逆向思路
simpleCrypto这名字暗示加密逻辑「简单但不标准」。处理自定义加密算法,我的习惯是先把加密流程拆成三步:密钥怎么生成、明文怎么变换、密文怎么输出。常见的简单实现是「移位 + 异或 + 置换」的组合,每一步单独拿到都可以逆。
# 假设加密为:c = ((m + shift) ^ key) & 0xFF # 还原:m = ((c ^ key) - shift) & 0xFF def decrypt(cipher, key, shift): m = (cipher ^ key) - shift return m & 0xFFsolve_crypto.py这类脚本写到这个程度就够用。如果题目里出现了bytes到int的转换、long_to_bytes、bytes_to_long,说明加密单位是大整数而不是字节,那就要切成大数运算的思路来还原。不管哪种,先确认题图timu.jpg里有没有写偏移量或密钥——CTF 出题人喜欢把 hint 放在图里,用strings扫图是 30 秒的事,不要一开始就死磕算法。
4. Reverse 与 PWN 题组:ez_py 的 Python 字节码还原与 build_your_house 的堆结构脉络
Reverse 和 PWN 是这份题包里对工具链要求最高的两块。reverse-ez_py名字已经暴露了——Python 逆向,大概率是 pyc 文件或打包后的 exe。PWN-build_your_house则是一道堆题,需要 pwntools 和 glibc 调试环境。这两个方向入门门槛高,但题包里的题选得比较克制,适合做第一个实战样本。
4.1 reverse-ez_py:反编译 pyc 还是直接审计打包 exe
reverse-ez_py目录下是压缩包。解压后如果看到.pyc文件,直接用uncompyle6或pycdc反编译。 uncompyle6 对 Python 3.8 以下支持良好,遇到高版本就换decompyle3。如果拿到的是打包好的 exe,先跑pyinstxtractor.py解包,再从得到的pyc反编译。
# pyc 反编译 uncompyle6 ez_py.pyc > ez_py_source.py # exe 解包 python pyinstxtractor.py ez_py.exe # 输出目录里找 entry.pyc 或 main.pyc,再反编译反编译出来的源码如果直接能看到 flag 计算逻辑,那是最理想的情况。但很多题会做一层混淆——变量名改成无意义字符、控制流平坦化、字符串加密。碰上这种,先别急着人工分析,把反编译代码里的字符串提取出来:
strings ez_py_source.py | grep -i flag如果 flag 是分段拼的,grep flag能直接定位到拼接位置。如果字符串被加密了,就在反编译源码里搜decode或base64,通常能找到解密函数。
4.2 PWN-build_your_house:了解堆结构后再决定打哪个点
build_your_house这类堆题,名字隐喻「自己构建堆布局」,常见漏洞点是 UAF(悬空指针)、double free、堆溢出。做堆题的第一步永远是确定 glibc 版本——不同版本的 tcache、fastbin 机制差异巨大,打错版本等于白做。
# 查看题目给的 libc file libc.so.6 # 或通过 checksec 查看保护 checksec --file=build_your_housechecksec输出里的Partial RELRO、No PIE、NX enabled直接决定了攻击路径。No PIE 意味着可以打固定地址,Partial RELRO 意味着 GOT 表可写,这是两个最常用的切入点。堆题脚本一般用 pwntools 写,交互、发送 payload、接收输出都靠它:
from pwn import * context.arch = "amd64" # 本地调试 p = process("./build_your_house") # 远程连接时改成 remote("ip", port) p.sendlineafter(b"> ", b"1") # 选择菜单功能 p.sendline(p64(0x404500)) # 传入目标地址 p.interactive()sendlineafter的b"> "参数要跟程序实际输出的提示符一致,不一致会导致发送阻塞。p64是打包 64 位地址的标准函数,字节序是小端——地址填错或者字节序不对是堆题脚本里最常见的两个报错点。做题顺序上,「先本地打通,再连远程」,本地调试可以用 gdb 插件 gef/pwndbg 下断点看堆结构,比纯黑盒测试高效得多。
5. CTF 题包实战的五个典型翻车现场:从解压到提权一条龙排查
这一章写我在跑这套题包时遇到的和见过别人遇到的坑,每条都是现象、原因、解决三步给全,按操作先后排序。
5.1 解压后出现 __MACOSX 和垃圾文件
现象:压缩包解压后多出__MACOSX目录,里面是._开头的隐藏文件,污染了题包目录,找不到原题文件。
原因:题包在 macOS 系统上打包,Finder 自动生成了资源派生文件,zip命令默认把这些也压缩进去了。
解决:解压时直接用-x排除,或者解压后统一删除。Linux 下用一条命令搞定:
unzip 中山市香山杯.zip -x "__MACOSX/*" # 已解压的情况 find . -name "._*" -delete rm -rf __MACOSX5.2 base.txt 解码后直接 cat,终端输出乱码
现象:cat decoded.txt后终端出现大量不可见字符,滚动条刷屏,甚至把终端配色搞乱。
原因:解码后的内容不是文本,可能是图片或二进制文件,直接当文本来读。
解决:解码后先file确认类型,不要急着cat。已经乱码了就用reset命令恢复终端,不影响后续操作。
5.3 binwalk 扫不出图片尾部附加数据
现象:binwalk timu.jpg输出为空,但图片打开后右下角有明显异常色块,或者文件大小异常。
原因:出题人把附加数据偏移做了处理,或者 binwalk 特征库没覆盖对应签名。
解决:手动查文件尾:xxd timu.jpg | tail -20,JPEG 的文件尾一般是FF D9,后面如果还有非零字节,就是藏了东西。另一种方式是看文件大小:两张内容相似的图,大小差几 KB,基本可以断定有追加数据。
5.4 zip 爆破工具跑了几小时没结果
现象:fcrackzip或john跑flag.zip,字典加载完就一直在跑,几小时不出结果。
原因:可能不是密码攻击问题——先确认是否为伪加密,或者密码本身就是弱密码,只是字典里没有,也可能是「密码就是文件名或题干里的某个词」。
解决:先把同目录下的1.jpg、题目.jpg用strings扫一遍,找找有没有类似password:、key:的提示。另外试试把目录名、文件名、比赛名当密码(香山杯这类比赛经常用xiangshan2021或类似格式)。爆破是最后手段,不是第一手段。
5.5 pyc 反编译失败,uncompyle6 直接报错
现象:uncompyle6 ez_py.pyc抛出ValueError: bad marshal data或版本不支持的报错。
原因:pyc 的 Python 版本高于 uncompyle6 支持范围,常见于 Python 3.9+ 编译产物,或者 pyc 被处理过(比如去掉头部)。
解决:先file ez_py.pyc看 Python 版本。3.9+ 用pycdc(Decompyle++)试试,报错就用decompyle3。如果反编译不完整,至少用marshal模块把代码对象加载出来:
import marshal, dis, importlib.util, sys with open("ez_py.pyc", "rb") as f: f.read(16) # 跳过 pyc 头部 code = marshal.load(f) dis.dis(code) # 直接看字节码dis.dis输出的字节码虽然可读性差,但能看到全局变量名、函数名和常量表——flag 经常就躺在code.co_consts里。
6. 把所有 Misc 题串成一条自动化检查流水线:从单题手动到一键扫描
Misc 题单题做多了,你会发现流程高度重复:文本解码、文件签名扫描、图片通道提取、频谱查看。把这些步骤串成脚本,能节省大量重复劳动。我整理了一个最小可用的检查流程,覆盖了这个题包里 80% 的 Misc 题入口。
import os import subprocess from PIL import Image import numpy as np def scan_file(path): # 1. 文件类型识别 result = subprocess.run(["file", path], capture_output=True, text=True) print(result.stdout.strip()) # 2. 尾部附加数据检查 with open(path, "rb") as f: data = f.read() tail = data[-4:] print("tail bytes:", tail.hex()) # 3. 图片通道低位提取(全自动跑一遍) if path.endswith((".jpg", ".png")): img = np.array(Image.open(path)) for i, name in enumerate(["R", "G", "B"]): secret = (img[:, :, i] & 0x01) * 255 Image.fromarray(secret.astype("uint8")).save(f"{name}_lsb.png")这段脚本的逻辑是按顺序跑三个检查:file识别真实类型、查看文件尾部字节、把图片每个通道的最低位提取成独立灰度图。执行后如果R_lsb.png或G_lsb.png里有肉眼可辨的轮廓,就说明低位隐写了内容。& 0x01是最低 1 位,如果你怀疑出题人用了 4 位隐写,改成& 0x0F再乘 17 就能把高四位还原。
这个流程在香山杯这套题上跑了一遍之后,我的习惯变了:以前拿到 Misc 题是先双击图片看,现在强制先走脚本再人工判断。这个习惯帮助我避免了好几次「肉眼看了半天,结果答案在 binwalk 里一键就出来」的尴尬。
音频题也按类似逻辑处理——先看频谱再听内容,先看波形再拉长,高频区域永远是检查重点。遇到 wav 就先用 Audacity 的频谱图扫一遍,配合strings扫文件里的文本标记,基本能把 miao 这种题控制在五分钟内解完。
做 CTF 题包复盘,核心不是把某一道题解出来,而是把一类题的检查顺序固化成肌肉记忆。这套流程你跑完一遍,再回头看自己的做题路径,会发现大多数题不是难在知识点,而是难在「没想到去查那个位置」。希望这份题包的拆解和上面的检查顺序能帮到你,遇到类似题目时少走几步弯路。
本文还有配套的精品资源,点击获取