从刷题人到出题人:一道Misc题如何融合隐写、流量与WebShell检测
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 的老牌隐写库。最终选定的工具组合是zstegRuby 写的 LSB 检测工具跨平台容易装binwalk文件分离GitHub 上直接有 releaseWireshark流量分析最通用的没有之一stringsgrep文本检索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-databody 里携带一个 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 通信并分析 Flag5.1 为什么我不想只做一个“导出文件就结束”的题说实话市面上很多 Misc 流量题到“导出文件并解压”就结束了。但我这次故意往前多走了一步把WebShell 后门通信的识别设计成第三关的核心。WebShell 后门这个概念在 CTF 的 Web 方向很常见但在 Misc 的流量分析里反而不常作为主要考点。实际上真实的安全运营场景中防守方最头疼的问题之一就是从海量流量里发现 WebShell 的可疑通信——攻击者拿到一个后门后十有八九会通过 HTTP 请求执行命令、回传数据这些流量混在正常的业务请求里极难一眼看出来。我这个题的第三关就是在第二关导出的压缩包里放了一段访问 WebShell 后的 HTTP 请求日志文本文件。选手需要从日志中识别出哪个 URL 是 WebShell 的入口攻击者通过 WebShell 执行了哪些命令其中哪个命令响应内容里藏着 Flag5.2 三道检测思路这是我出题时的核心参考我在设计这块内容时参考了日常做 WebShell 后门检测的三种主流思路。顺便说一句这个思路不仅对做题有用对实际做流量监控的同行来说也值得收藏检测思路具体特征本题中的应用静态特征匹配URL 参数里出现eval、base64_decode、assert等危险函数名攻击者第一次访问后门时URL 中带有一个cmdbase64_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 的“杂”恰恰是它最迷人的地方——它逼着你去了解文件格式的底层细节、网络协议的交互过程、甚至攻击者的思维方式。如果你在刷题之余也有了一点“手痒”的感觉真的建议找个小题目试一试哪怕只是改一道现成题的参数你也会收获完全不同的体验。