1. 为什么我决定从“刷题人”转身变成“出题人”
先交代下背景。我在 BUUCTF 上刷 Misc 也刷了一阵子,从最开始连 PNG 文件头都不认识,到后来看到图片先看宽高、看到流量包先过滤 http,算是把常规套路都摸了个遍。但刷到某一个阶段之后,我发现一个很尴尬的问题:做出来的题越来越多,但真正学到的东西越来越少。
原因不复杂。Misc 这个方向本身就有点“套路守恒”的意思——文件分离、LSB 隐写、压缩包伪加密、流量追踪、NTFS 流、二维码修复,翻来覆去就这些考点。做题做多了以后,我甚至能猜到出题人想让我干什么:看到一张图先扔进 binwalk,看到 zip 先试伪加密,看到 pcap 先查 HTTP 对象。这种“肌肉记忆式解题”确实能拿分,但它绕过了真正有价值的思考:为什么这个题要这么出?这个隐写手法在实际场景里对应什么问题?
所以我决定换个角度,自己动手出一道原创 Misc 题。一方面是想验证一下自己对考点的理解是不是真的透彻,另一方面也是想体验一下“出题人视角”——当你需要藏东西、埋线索、诱导选手走某条路径的时候,你才会发现以前做题时忽略的大量细节。
这篇文章就完整复盘一下我出这道原创题的全过程,包括选题思路、三关卡的考点设计、踩过的坑、以及从 BUUCTF 通关经验里反向提炼出题灵感的几点体会。如果你也想从“刷题人”变成“出题人”,或者单纯想看看一道 Misc 题是怎么从零到一被搭出来的,这篇应该能给你不少参考。
2. 出题前的考点地图:隐写、流量与后门检测怎么串起来
2.1 考点选择:为什么是“图片隐写 + 流量分析 + WebShell 通信识别”这个组合
Misc 的考点很多,但有一个方向我一直觉得潜力很大,就是将静态隐写和网络流量结合起来。因为大多数 Misc 新手对隐写的理解停留在“文件里藏东西”这个层面,对“数据藏好之后怎么传出去”这件事缺乏概念。而实际上,真正有现实意义的安全问题,往往是藏和传连在一起的。
这道原创题我设计了三个递进关卡:
- 第一关:图片文件中的隐写信息提取(经典 Misc)
- 第二关:从流量包中还原被传输的压缩包/脚本文件
- 第三关:识别流量中的 WebShell 通信行为并分析出 Flag
选择“图片隐写 + 流量 + WebShell 后门”这个组合,还有一个个人原因——BUUCTF 上 Misc 方向的题目大多是单点考点,一道题只考一种隐写或一种文件格式,很少有多关卡串联的。但实际场景里,攻击者拿到一个 webshell 后,一定会通过 HTTP 请求把数据传输出去,流量里就会留下蛛丝马迹。把这三个点串成一道题,本质上就是在模拟一条完整的攻击链。
2.2 难度定位:单人 20 分钟,卡新人不卡老手
出题之前我给自己定了一个原则:可解性第一,趣味性第二。一道 Misc 题如果思路不清晰,选手卡了半天发现只是某个工具版本不对,那是纯恶心人。
理想的时间分配是这样的:
| 关卡 | 预期耗时 | 核心操作 |
|---|---|---|
| 第一关:图片隐写 | 5-8 分钟 | 文件识别、LSB 提取、隐写信息解读 |
| 第二关:流量还原 | 8-10 分钟 | pcap 过滤、文件导出、压缩包还原 |
| 第三关:WebShell 通信分析 | 5-8 分钟 | 请求特征识别、命令还原、Flag 拼接 |
整体控制在 20 分钟左右,对新手来说有点挑战但不会绝望,对老手来说也能感受到一道题里三个考点之间的逻辑连贯性。
2.3 工具可用性:不能要求选手装冷门环境
这是一个经常被忽视的点。出题人自己用的工具链和选手的环境未必一致,尤其是 Misc 方向,有些隐写工具在 Python 3.10+ 下跑不动,有些 Wireshark 插件需要特定版本。
所以我在选考点时特意避开了两类工具:需要手动编译的、以及只支持 Python 2 的老牌隐写库。最终选定的工具组合是:
zsteg(Ruby 写的 LSB 检测工具,跨平台,容易装)binwalk(文件分离,GitHub 上直接有 release)- Wireshark(流量分析,最通用的没有之一)
strings+grep(文本检索,Linux/macOS 自带)
这一套工具链在绝大多数 CTF 环境里都能直接用,不需要额外折腾。
3. 第一道关卡:图片隐写里的“藏”与“隐”
3.1 容器选择:我为什么最终选了一张 PNG 而不是 JPG
这个决定花了我不少时间。最开始我打算用 JPG,原因是 JPG 可以玩 DCT 域隐写(比如 outguess、steghide 这类工具),听起来比 PNG 的 LSB 高级不少。但我后来还是改成了 PNG,原因很实际:
PNG 的 LSB 隐写给选手的反馈路径更清晰。
选手拿到一张 PNG,用zsteg扫一遍,如果有 LSB 隐写,结果几乎是秒出的。而 JPG 的隐写工具依赖参数较多,不同工具扫描出来的结果可能不一样,容易让选手在“我到底有没有找对方向”这个问题上浪费时间。
我的设计是:在一张正常风景图里,把一段文本通过 LSB 算法藏在 RGB 每个通道的最低位。选 PNG 还有一个好处——PNG 是无损压缩,隐写进去的数据不会因为压缩而失真。如果用 JPG 本身有损压缩的特性,隐写的稳定性就会差很多,尤其当你想藏的数据量比较大的时候,JPG 的 DCT 系数修改很容易引入可见噪声。
3.2 伪 Flag:给选手一点“干扰”,但不恶意
这里必须说明,Misc 题里的“伪 Flag”是一个很有争议的设计。有些选手很反感这道题里面放一个假的 flag 来浪费自己的时间,但我觉得,如果处理得当,伪 flag 反而是很好的教学点。
我在这个题里放的“伪 flag”其实不是一串乱码,而是一段看起来很像 Base64 的字符串。选手如果直接解码,会得到一句话:“你确定这是你要找的东西吗?”——这是在暗示选手:你可能已经拿到了隐写数据,但这并不是最终答案。
我当时在这张图的元数据里也加了一条带误导性的字符串,内容是flag{not_this_one}。这算是一个小小的心理战:选手如果只执行strings就以为自己通关了,后面真正的流量分析环节就直接错过了。
3.3 实际制作过程:LSB 隐写的代码逻辑
LSB 隐写本身不复杂,核心思路就是把要隐藏的数据转成二进制,然后逐位替换图片像素的 RGB 最低位。这里要注意一个细节:替换的最低位数建议只改 1 位,不要为了提升容量改到 2 位甚至更多,否则图片会出现肉眼可见的噪点。
我用 Python 加 PIL 库做了这样一个处理:
- 读取一张 1920x1080 的 PNG 风景图
- 把隐藏文本转成二进制,头部加上固定的起始/结束标记,比如
[START]和[END] - 按行扫描像素,把二进制数据塞进 R、G、B 三个通道的最低位
- 保存为新 PNG
关键点在于起始标记。没有标记的话,选手即使提取出 LSB 数据,也很难判断数据从哪里开始,到哪里结束。加了这个标记之后,选手用zsteg扫出来能直接看到可读文本的结构,解题体验会更顺畅。
这一步实操时有一个容易踩的坑:PNG 的像素尺寸如果不够大,而你要藏的数据又比较长,数据装不下就会被截断。我一开始用了一张 800x600 的图,藏一小段文本没问题,但后来我想在隐写数据里放一段 Base64 编码的提示文本,结果数据量超过了图片容量,程序报错。后来换成了 1920x1080 的高清图,这个问题才解决。
4. 第二道关卡:流量包里的“传”与“析”
4.1 流量生成思路:模拟一次文件传输,而不是刻意“留后门”
第二关的设计思路是这样的:选手在图片隐写里拿到了一串提示,告诉他在一个.pcap文件里存在进一步线索。这个 pcap 文件被塞在第一关图片的文件尾,用binwalk可以分离出来。这也算是常见套路——把 pcap 藏在图片的文件尾,让选手通过binwalk从图片里分离出附加文件。
关键问题来了:这个 pcap 里应该是什么流量?
我最初的计划是生成一个包含 TCP 文件传输的 pcap,让选手用 Wireshark 的“导出 HTTP 对象”功能直接提取文件。但后来我改了主意,决定模拟一次更贴近真实场景的 HTTP POST 上传流量:客户端通过一个上传接口,把服务器上的一个压缩包传到了另一个位置。
这样设计的原因在于,“导出 HTTP 对象”这个操作对所有玩过 CTF 的人来说都太熟了,缺乏新鲜感。而如果选手需要在 POST 请求的 body 里用 Wireshark 的“追踪 TCP 流”功能手动拼接二进制数据,就更能理解“数据在真实网络里是怎么被分块传输的”。
4.2 生成流量的具体做法
我没有用真实的 Web 服务器来生成流量,而是用 Scapy 构造了 HTTP 数据包。构造的流量特征包含:
- 客户端先发一个 GET 请求,获取一个临时上传凭证(这一段是干扰项)
- 客户端再发一个 POST 请求,
Content-Type为multipart/form-data,body 里携带一个 gzip 压缩的文本文件 - 服务端返回 200 OK,附带一句返回文本
upload complete
流量包构造的过程中,Scapy 会自动把 HTTP body 拆分成多个 TCP segment,这就生成了正常的 TCP 分片效果。选手在 Wireshark 里打开后,如果直接看包列表,会看到一堆 TCP 重传和 ACK,容易眼花——但是只要选中 POST 请求,右键选择“追踪 TCP 流”,就能看到完整的 HTTP 内容。
4.3 流量包里的“陷阱”:命名要讲究,不能白给
说到这个地方,我必须强调一个出题心得:流量包里的文件名和接口名本身就是线索,取名的“可信度”直接影响题目难度。
如果我把接口名直接命名为/upload_flag,那选手压根不用分析,看到这个 URL 就直接提取数据了,整道题变成了体力活。但如果接口名过于泛化,比如/upload.php,选手又可能不知道哪个请求才是关键。
我最终的取舍是:接口名是/api/v1/upload/report,文件名是report_2024_11_05.gz。配合请求头里的 User-Agent 设置为正常的浏览器 UA,整个流量看起来就像是一次内部系统上报月度报告的日常行为。选手需要结合第一关拿到的提示,才能判断哪一段流量才是真正要关注的。
pcap 里我还有一个加密压缩包,设置了密码。密码不是凭空来的,而是从第一关 LSB 隐写的那段文本中提取出来的关键词。这样一来,三个关卡之间就形成了一条完整的线索链:图片隐写出提示 → 提示指向流量 → 流量导出加密包 → 密码回到第一关的隐写数据里找。没有一环是孤立的。
5. 第三道关卡:从流量中识别 WebShell 通信并分析 Flag
5.1 为什么我不想只做一个“导出文件就结束”的题
说实话,市面上很多 Misc 流量题到“导出文件并解压”就结束了。但我这次故意往前多走了一步,把WebShell 后门通信的识别设计成第三关的核心。
WebShell 后门这个概念,在 CTF 的 Web 方向很常见,但在 Misc 的流量分析里反而不常作为主要考点。实际上,真实的安全运营场景中,防守方最头疼的问题之一,就是从海量流量里发现 WebShell 的可疑通信——攻击者拿到一个后门后,十有八九会通过 HTTP 请求执行命令、回传数据,这些流量混在正常的业务请求里,极难一眼看出来。
我这个题的第三关,就是在第二关导出的压缩包里放了一段访问 WebShell 后的 HTTP 请求日志(文本文件)。选手需要从日志中识别出:
- 哪个 URL 是 WebShell 的入口
- 攻击者通过 WebShell 执行了哪些命令
- 其中哪个命令响应内容里藏着 Flag
5.2 三道检测思路:这是我出题时的核心参考
我在设计这块内容时,参考了日常做 WebShell 后门检测的三种主流思路。顺便说一句,这个思路不仅对做题有用,对实际做流量监控的同行来说也值得收藏:
| 检测思路 | 具体特征 | 本题中的应用 |
|---|---|---|
| 静态特征匹配 | URL 参数里出现eval、base64_decode、assert等危险函数名 | 攻击者第一次访问后门时,URL 中带有一个cmd=base64_decode(...)的参数 |
| 请求行为分析 | 单个来源 IP 在短时间内对同一 URL 发起大量请求,且参数内容不断变化 | 日志中同一个 IP 在 2 分钟内访问同一个脚本超过 30 次,参数各不相同 |
| 响应内容分析 | 服务器响应中出现非业务类输出,比如uid=,whoami, 目录列表 | 攻击者执行ls -la后,返回内容里包含服务器文件目录结构 |
第三关的 Flag 藏在最后一条命令的回显内容里,选手需要把前面几条命令的响应内容串联起来,发现攻击者最终读取了一个配置文件,配置文件的末尾就是 Flag。
5.3 防“跳关”的细节处理
这里有一个容易被忽略的问题:如果选手在第二关压缩包里直接搜“flag”关键词,是不是就能跳过第三关直接拿答案?
对,这就是很多 Misc 题设计上的一个致命伤。如果 Flag 以明文形式躺在某个文件里,那所有前置关卡都失去意义。
我的处理方式是:Flag 不是整串存在的。压缩包里那段日志里的配置文件内容是分段加密的,选手需要把前面几步的命令执行结果作为一个 key,对配置文件里的密文做一次简单的按位异或,才能得到最终 Flag。这样即使选手直接打开配置文件,看到的也只是一串看不出含义的十六进制数据,不会被瞬间秒杀。
这种方式其实就是经典的“逐层解密”设计,既保证了线索的连贯性,又避免了“一把梭”式的跳关解法。
6. 出题人的自我验证:一道题打磨三次的完整经历
6.1 第一版:LSB 隐写强度太高,工具直接解不出来
第一次完整搭完题之后,我自己先试做了一遍,结果第一关就翻车了。
原因是这样的:我用 Python 脚本写入 LSB 隐写时,为了“稳妥”,把隐藏文本做了十六进制编码再写入,而且文本前面加了一段随机字节作为填充。这导致zsteg默认扫描结果里全是乱码,没有任何可识别的字符串。我自己提取的时候都要靠专门脚本去定位起始标记,才能把数据还原出来。
这个结果完全违背了我“可解性第一”的原则。后来我把填充字节删掉,隐藏文本也改成了 Base64 编码,zsteg一扫就能在输出里看到连续的 Base64 字符串,问题才解决。
这个坑值得说出来:出题人一定要站在选手的工具链角度去验证题目,不能只用自己的脚本测。测题时至少用三种不同的工具各跑一遍,尤其是常见的foremost、binwalk、zsteg、strings,确保它们在默认参数下能给出有效的输出。
6.2 第二版:流量包时间戳误导了我
第二版的时候,我在流量包里加入了一个看似“智能”的设计——把 POST 请求的时间分散在几个不同时间段,模拟真实用户不会集中在一个时间点上传文件。
听起来很合理对吧?但实际测题时,我自己被这个设计坑了:因为 pcap 里第一个 GET 请求和最后一个 POST 请求之间的时间跨度太大,Wireshark 的“导出 HTTP 对象”列表里出现了大量无关请求,我自己的排查效率反而变低了,花了十几分钟才找到那个关键 POST 请求。
后来我把时间戳统一到了一个紧凑的时间窗口内,整个流量包的大小也从 15 分钟压缩到了 3 分钟。测试下来,选手定位关键请求的时间大幅缩短,题目整体节奏顺畅了很多。
这个经历也让我意识到一个道理:出题时不要为了“真实感”牺牲“可解性”。CTF 题目本质上是出题人和选手之间的一场对话,对话应该简洁高效,而不是像现实世界一样充满噪声。
6.3 第三版:伪 Flag 的“反转点”需要恰到好处
最终版本里,我保留了伪 Flag,但调整了它的位置和表述方式。第一版伪 Flag 藏得太深,很多选手根本没发现它,所以它的存在感几乎为零;第三版我把伪 Flag 放在了一个更显眼但需要稍加思考才能识别的位置——图片的 EXIF 注释字段里,内容是一段看起来很像 Base64 的字符串,解码后是一句半开玩笑的话:“Good job!但你漏掉了真正重要的东西。”
这句话的作用是提醒选手:你已经发现了隐写信息,但这不是全部,继续往下挖。测试下来,大约 60% 的选手看到这句话之后会自然地意识到还有后续内容,不会再在伪 Flag 上纠结太久。
6.4 出题检查清单(实测有用)
经过三轮打磨,我把自己的测题流程总结成一个清单,每次出完题都会按这个顺序过一遍:
- 第一遍:按正常解题思路走,用全部常规工具默认参数跑,记录整体耗时
- 第二遍:故意不按思路走,尝试暴力搜文件、搜关键词、翻 strings,看看能不能跳关
- 第三遍:换一台干净环境(没有安装任何额外工具的系统)测试,确保选手只用题目说明里提到的工具也能解题
- 第四遍:反向检查每一条线索的可读性,确保提示语句没有歧义,不会误导选手走向死胡同
7. 从 BUUCTF 做题中积累的 Misc 出题灵感库
7.1 做题与出题的正反馈:刷题量不是白刷的
说回我在 BUUCTF 上的做题经验。很多人觉得刷题就是刷题,出题就是出题,两者关系不大,但我的体会是:做题时的每一次“卡住”,都在为以后出题积累素材。
举几个例子。我以前做流量题最怕的就是 HTTP 请求对象太多,找不到目标文件,后来我在自己出题的时候就有意识地控制流量包大小,尽量让关键请求在可视范围内不超过 3 到 5 个对象;我以前做隐写题最烦的就是图片处理脚本得自己从零写,后来我就把自己常用的 LSB 提取脚本、PNG 宽高修改脚本、zip 伪加密检测脚本统统整理到了本地,出题时直接复用。
BUUCTF 的通关之路里,Misc 部分的题目风格差异挺大的,有的偏向脑洞,有的偏工具流,有的则完全是信息收集。这些不同风格在我出题时都成了“反向参考”——我会想:如果这道题的出题人换一种思路出,会不会更好?如果我改编这个题,保留它的内核但换一个载体,难度会不会更平衡?
7.2 可复用的“梗”:一些适合做成 Misc 设计的好素材
根据我在 BUUCTF 和各种比赛里的做题经验,以下几个“梗”很适合用来设计原创 Misc 题,后续你也可以拿去做变体:
- 文件签名伪装:把 PDF 的文件头改成 PNG 的文件头,考验选手对文件真实类型的判断能力
- 多文件拼接:一张图片可能同时包含 zip、docx 和 pcap,但真正的线索在第三个文件里,前两个是干扰
- 宽高修改:PNG 的宽度字段被改小,导致图片显示不全,需要手动修复宽高才能看到完整图像里的信息
- 流量中的 DNS 查询:把数据拆分成多个 DNS 请求的域名部分,通过解析域名记录还原隐藏信息,这是经典但依然有效的隐写手段
- 压缩包伪加密:用十六进制编辑器查看 zip 的加密标志位,这是 Misc 入门最常考的知识点之一,但和流量题结合反而能有新意
7.3 给想出题的朋友几句心里话
最后说几句比较主观的话。想出题的朋友,无论你是想给社团出新人赛的题目,还是想给平台投稿原创题,都建议你先从“给自己出题”开始:假设你是一个对某个考点完全不熟的选手,按最低工具配置推演一遍解题路径,你会发现很多你以为是“常识”的东西,对别人来说都是需要提示才能跨过的坎。
我个人在实际操作中的体会是:出题比做题更能暴露自己知识的盲区。一道原题里需要我用 Python 写 LSB 提取脚本,我才会发现自己对 PIL 的像素读写机制理解得不够深;需要我构造 pcap,我才会发现自己对 TCP 分片和 HTTP 层的交互过程模糊不清。这些盲区,靠刷题是很难被触发的。
8. 一点扩展:这套设计还可以怎么变
我的这道题做成的是一个“图片 → 流量 → 后门识别”的三层结构。如果你觉得还不够过瘾,还可以这样扩展:
- 把 HTTP 改成 DNS 隧道:让选手通过 DNS 请求的域名记录来还原加密包,难度会明显上升,但需要选手了解 DNS 协议的基础字段。
- 加入压缩包伪加密:在第二关的压缩包上加上伪加密标志,选手需要先修复压缩包才能解压,多一个考点。
- 把 WebShell 日志换成内存镜像:让选手从内存取证文件里提取 WebShell 进程的命令行参数,这其实就是 Memory Forensics 方向的内容了,Misc 和取证之间本来就没有清晰的边界。
这些变体每个都可以单独成题,核心思想一致:用一条隐式的线索链把多个孤立考点串联起来,让选手每解开一层,都能对下一步的方向有更清晰的预期,而不是靠瞎猜。
做这一道原创题的过程,让我对 Misc 这个方向的看法有了很大变化。以前我总觉得 Misc 是 CTF 里最“杂”的方向,好像什么都能往里塞,没什么体系。但当我真正站在出题人的角度去规划考点、控制节奏、设计干扰项的时候,我发现 Misc 的“杂”恰恰是它最迷人的地方——它逼着你去了解文件格式的底层细节、网络协议的交互过程、甚至攻击者的思维方式。如果你在刷题之余也有了一点“手痒”的感觉,真的建议找个小题目试一试,哪怕只是改一道现成题的参数,你也会收获完全不同的体验。