这道题我在不同平台见过好多次几乎每一个入门 Misc 的人都会被它“坑”一次。[BJDCTF2020]认真你就输了 这个名字本身自带劝退效果越是想认真分析越容易被表面信息带走。整道题涉及的其实都是基础功文件类型识别、隐藏文件分离、PNG 结构检查、CRC 校验还有一点 Python 脚本能力。如果你刚接触 CTF或者已经在 Misc 里踩过不少坑这道题都很适合拿来当一次完整的“标准流程”演练。先说结论这道题的 flag 不是放在一个正常文件里的它藏在一个被拼接在文件尾部、解压后无法直接看到的 PNG 中。换句话说你在表面文件上越认真越发现不了真正的问题。下面我把完整过程拆开讲尽量让每个步骤背后的原理也说明白而不只是丢一串命令给你。1. 拿到文件之后先别急着解压1.1 第一步永远是 file而不是双击打开很多人拿到 CTF 压缩包后的反应是先解压然后看到里面有一个没有后缀名的文件于是下意识用记事本打开看到一堆乱码就开始懵。我在第一次做这类题时也犯过同样的错误结果浪费了很长时间在“这到底是个什么文件”这个问题上。正确做法是先用file命令让系统帮你猜类型或者说得更准确一点让系统根据文件内容特征来判断它到底是什么。二进制文件的扩展名完全不可信题目作者完全可以给一个 ELF 可执行文件改成.txt后缀也可以把一个 PNG 藏在一个无后缀文件里靠file才能看到本质。$ file challenge.zip challenge.zip: Zip archive data, at least v2.0 to extract $ unzip challenge.zip Archive: challenge.zip extracting: flag $ file flag flag: data注意这里file flag输出的结果是data意思是它不是一个常规的文本文件、图片或可执行文件。看到data不要慌这恰恰说明里面可能藏着东西。接着用xxd或hexdump看文件头$ xxd flag | head -5如果看到89 50 4E 47 0D 0A 1A 0A那说明文件头是一个 PNG但在这道题里文件头可能是随机填充的数据签名出现的位置往往偏移到文件后半段了。所以不要只盯着前几个字节要继续往后扫。file命令判断的是文件开头一小段特征对于“正常文件 尾部附加数据”这种结构它只能识别出最外层类型。真正要扫尾部隐藏文件需要binwalk或者手动搜签名。1.2 strings 里的那句提示是在给你挖坑解压之后如果顺手用strings扫一遍文本你会看到一些像提示一样的话甚至可能看到一串很像 flag 格式的字符串。第一次做这题的时候我差点就把那串字符串交上去了结果自然是不对的。strings这个工具的作用是从二进制文件里提取可打印字符串很多新手把它当成万能搜索工具。但题目作者很清楚这一点所以经常会把干扰字符串塞在文件某些角落要么是 padding 区要么是附加数据里让你误以为已经拿到答案。“认真你就输了”这句提示本身就是出题人的恶趣味它让你认真分析文件内容但真正的答案根本不在你能直接看到或搜索到的地方。很多 Misc 题都是这个套路表面越明显越说明问题出在看不见的结构层面。所以我的习惯是字符串搜索永远只作为辅助手段不能作为结论依据。真要找隐藏数据还得靠二进制层面的分析。2. 隐藏文件往往就藏在“看起来正常”的文件后面2.1 为什么优先想到 binwalk当我用strings没找到可用信息之后第二步就是用binwalk扫描整个文件。因为我对这道题的出题套路已经有预期它很可能是把一张 PNG 直接拼接在一个普通文件后面。这种“文件拼接”或者说“文件附加”的手法在 CTF 的 Misc 题里出现频率非常高。binwalk的原理就是遍历整个文件用常见文件格式的魔数magic number去匹配内容特征。比如 PNG 的魔数是89 50 4E 47ZIP 的魔数是50 4B 03 04只要出现对应签名它就会在对应偏移位置报告发现了什么。因此它非常适合用来找出藏在文件任意位置的已知格式。$ binwalk flag DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 ... 123456 0x1E240 PNG image, 780 x 412, 8-bit/color RGB, non-interlaced 123488 0x1E2E0 Zlib compressed data, ...上面这些偏移量和图片尺寸不是定值不同版本的题目文件会有区别。关键思路是当你看到binwalk在某个偏移处报出 PNG image基本就能确定这个文件里藏着一张 PNG 图片。它可能是一个完整的图片文件被拼接在尾部也可能是从某个位置开始的图片数据。这里有个细节要注意binwalk报出的尺寸不一定完全准确因为它是通过解析部分结构来判断的。如果图片的宽高被修改过它报出来的尺寸也可能跟着变化。所以binwalk只能帮我们定位“哪里藏着文件”不能完全代替后续的结构校验。2.2 用 foremost 自动分离还是手动截取确定有隐藏文件后下一步就是把它分离出来。最常见的是用foremost它比binwalk -e更简单粗暴直接按文件类型把内容分离到目录里。$ foremost flag -o extracted $ ls extracted/ $ file extracted/png/*foremost会生成一个output目录里面按类型分文件夹存放提取结果。PNG 文件一般会出现在png子目录里。分离完后不要看文件名因为foremost通常会重新命名输出名和原始内容不一定对应一定要用file再确认一次类型。如果foremost分离失败或者你只想要从指定偏移开始的数据可以用dd手动截取$ dd ifflag ofout.png bs1 skip123456skip参数就是binwalk报出来的偏移量。bs1表示一个字节一个字节跳过这样做虽然简单但文件很大时会很慢。如果偏移量很大可以把bs设成一个固定的块大小比如bs4 skip30864只要确保bs * skip等于目标偏移。我自己一般会先用foremost一把梭它不行了再上binwalk -e还不行就dd手动切。三种方式不是互斥的可以互相验证。这道题的隐藏 PNG 结构比较完整foremost基本可以直接分离出来。3. 分离出来的 PNG 到底哪里不对劲3.1 一个标准 PNG 长什么样分离出 PNG 后双击打开可能没什么异常也可能显示成一张半截图片这就要看题目改了什么。为了理解后面的修复过程先简单说清楚 PNG 的文件结构。一个标准 PNG 以 8 字节固定签名开头89 50 4E 47 0D 0A 1A 0A后面跟着一系列 chunk数据块每个 chunk 由四部分组成4 字节长度Length、4 字节类型Type、数据Data、4 字节 CRC 校验。其中第一个 chunk 必须是 IHDR它记录了图片的基本信息包括宽度、高度、位深、颜色类型、压缩方式、滤波方式和隔行扫描方式。IHDR 的数据部分是 13 个字节00 00 00 0D IHDR 数据长度 13 49 48 44 52 IHDR 类型 00 00 03 0C 宽度例如 780 00 00 01 9C 高度例如 412 08 位深 8 02 颜色类型 真彩色 RGB 00 压缩方式 00 滤波方式 00 隔行扫描方式 xx xx xx xx CRC 校验值这里的宽度和高度都是 32 位大端整数也就是 4 字节。如果你在十六进制编辑器里定位到49 48 44 52这 4 个字符那么紧接着的 4 字节就是宽度再往后 4 字节是高度。改宽高时一般就是把这两个值改掉。3.2 用 pngcheck 一眼看出版本问题拿到分离出来的out.png不要急着用图片查看器打开先用pngcheck校验一下结构是否正常$ pngcheck -v out.png File: out.png (780 x 412) ... CRC error in chunk IHDR (computed 3f..., expected 12...)只要出现CRC error就说明这个 PNG 的某个 chunk 被改过而且修改的人没有重新计算 CRC。最常见的就是改 IHDR 里的宽度或高度。为什么改宽高会让人发现问题因为 IHDR 里存的宽高值决定了解码器如何解析后面的像素数据。如果高度被写小了图片只会显示一小部分如果宽度被写大了图片可能会花屏或直接打不开。但真正的图像数据 IDAT 仍然在文件里只是解码器不知道应该按多大的画布去解释它。所以修复的关键就是找到正确的宽高。这也是为什么我在前面反复强调“不要只靠图片查看器”一张看起来正常或半截的图片很可能只是解码器使用了错误的宽高参数。题目作者就是看准了大多数新手不会去校验 PNG 结构这一点。4. 用 CRC 反推被改掉的宽高4.1 IHDR 的校验和到底覆盖哪些字节先明确一点PNG 里每个 chunk 的 CRC 是对该 chunk 的“类型 数据”计算得到的不包含前面的长度字段。对于 IHDR 来说就是49 48 44 52这四个字节加上 13 字节的 IHDR 数据一共 17 个字节。当你修改了 IHDR 里的宽度或高度原本的 CRC 值就不再匹配。反过来想如果作者没重新计算 CRC那文件里保存的 CRC 一定是基于原始宽高计算的。我们只要不断尝试各种宽高组合计算每一组数据的 CRC32看哪一组能和文件里保存的 CRC 对上就能反推出原始宽高。这个过程的计算量并不大。在 CTF 题目里图片尺寸通常不会超过几千像素用脚本遍历完全可行。手动拿十六进制编辑器一个个试没有意义效率太低而且容易出错。4.2 爆破脚本与参数选择下面这个脚本是我常用的修复模板可以直接保存成fix_png.py运行import zlib import struct with open(out.png, rb) as f: data f.read() pos data.find(bIHDR) if pos -1: raise SystemExit(not a PNG) # IHDR 数据是类型字段之后的 13 个字节 ihdr bytearray(data[pos4:pos17]) expected struct.unpack(I, data[pos17:pos21])[0] print(expected crc:, hex(expected)) print(current width:, struct.unpack(I, ihdr[0:4])[0]) print(current height:, struct.unpack(I, ihdr[4:8])[0]) # 常见套路只改高度宽度保持不变 for h in range(1, 5000): trial bytearray(ihdr) trial[4:8] struct.pack(I, h) if zlib.crc32(bytes(trial)) expected: print(matched height:, h) break else: print(height-only no match, try both width and height)解释一下几个关键点。data.find(bIHDR)是定位 IHDR 类型字段的位置找到之后pos4就是数据开始位置pos17是数据结束位置再往后 4 字节就是 CRC。脚本里先读出当前宽高并打印出来方便你确认文件是不是真的被改过。爆破时我选择的循环范围是 1 到 4999这个范围对绝大多数 CTF 图片够用了。如果没跑出来可能是宽度也被改了。这时候可以固定高度爆破宽度或者干脆双层循环宽度和高度都从 1 试到一定范围def check(w, h): trial bytearray(ihdr) trial[0:4] struct.pack(I, w) trial[4:8] struct.pack(I, h) return zlib.crc32(bytes(trial)) expected for w in range(1, 4001): for h in range(1, 4001): if check(w, h): print(w, h) raise SystemExit双层循环计算量会大一些最坏情况要跑 1600 万次。不过 CRC32 的计算速度很快Python 跑下来也就十几秒。如果题目里宽高改动的范围比较大这种兜底方案是有效的。跑出正确的宽高后用十六进制编辑器找到 IHDR 里对应的 4 字节位置把当前值改成爆破出来的值保存即可。比如爆破出高度是 900可以用这行命令快速转成十六进制python -c print(hex(900))输出0x384所以在十六进制编辑器里把高度那 4 个字节改成00 00 03 84就行。5. 完整复现流程命令与步骤5.1 从压缩包到隐藏 PNG 的一站式操作前面讲了很多原理和单点操作这里拉通一遍完整的流程方便你照着做。第一步解压压缩包用file确认解压出来的文件类型$ file challenge.zip $ unzip challenge.zip $ file flag第二步用binwalk扫描是否有隐藏文件$ binwalk flag第三步用foremost自动分离$ foremost flag -o extracted $ file extracted/png/*分离完成后会得到一个或多个 PNG先把它们都复制出来$ cp extracted/png/*.png .第四步用pngcheck检查 PNG 是否符合标准$ pngcheck -v *.png如果显示CRC error进入第五步用前面给的 Python 脚本爆破宽高$ python3 fix_png.py第六步用十六进制编辑器修改 IHDR 中的宽度或高度保存后重新打开图片得到最终 flag。整个流程非常简单但每一步都有它存在的意义。file帮你确定当前文件类型binwalk帮你发现隐藏数据foremost帮你分离pngcheck帮你判断是否被改过脚本帮你找到正确的修复合参数。少做任何一步都可能导致后面走偏。5.2 如果脚本跑不出来怎么办爆破宽高的脚本并不是万能的有时候你按上面的流程走完却发现没有匹配结果。这时候先别怀疑脚本写错按下面几个方向排查。第一个方向是确认 CRC 有没有被重新计算过。有少数题目根本不走“改宽高不更新 CRC”的标准套路作者会直接重新计算 CRC那这个修复方法就完全失效。你可以用十六进制编辑器把当前宽高代入脚本算一次看算出来的 CRC 和文件里保存的是否一致。如果一致说明结构本身没有 CRC 错误问题可能出在别处比如 IDAT 数据被截断或加密过。第二个方向是看看 PNG 里面是不是还有其他隐藏的 chunk。比如tEXt、zTXt、iTXt甚至自定义 chunk都可能在存放附加信息。用pngcheck -7可以看到所有 chunk 列表逐个检查一下内容。第三个方向是重新回到文件分离这一步。有些题目会在隐藏 PNG 后面再藏一层数据或者把多个文件拼接在一起。分离出一个out.png后可以再对它跑一次binwalk确认没有二次隐藏。如果还有数据重复分离过程。第四个方向是打开图片后不要只看表面。如果图片能正常打开但内容看起来是纯色或乱码可以考虑用 stegsolve 查看 RGB 通道或者用其他隐写工具扫一遍。题目叫“认真你就输了”但它没有规定只能有一种解法不同的题目版本可能存在细微差别。6. Misc 题避坑清单经验向6.1 常见误判与排查思路做 Misc 题多了你会发现很多坑是普遍存在的。我把遇到频率比较高的几类整理成了一张速查表方便你卡住时快速对照。误判情况实际原因排查方向图片能打开但内容不明显IHDR 宽高被改显示区域不正确pngcheck 看 CRC爆破宽高strings 扫到疑似 flag 但提交失败干扰字符串或加密后的伪 flag继续做二进制结构分析binwalk 扫到多个文件但分离失败拼接数据缺失或文件头被破坏手动 dd 截取修复文件头file 输出 data无任何格式特征文件被混淆或整体加密先搜索已知魔数再考虑其他隐写PNG 打开提示文件损坏chunk 长度或 CRC 异常用 pngcheck 定位到具体 chunk这类错误很多时候不是因为你不会用工具而是因为每一步之间的因果联系没有建立起来。比如看到 pngcheck 报 CRC error就要立刻联想到“这里很可能被改过宽高”而不是觉得“PNG 坏了跳过这个文件”。6.2 常备工具与个人偏好最后分享一下我在 Misc 题里常用的工具组合不算全面但覆盖了大多数情况。file和xxd是起步阶段必用的一个识别类型一个看原始字节。binwalk和foremost负责文件分离一个有扫描报告一个能直接提取。pngcheck专门用来检查 PNG 结构遇到图片题基本都会用到。修改文件字节时我习惯用十六进制编辑器图形化界面可以直接定位到偏移处修改后保存。至于 Python主要就是 zlib 库算 CRC还有 struct 处理大端整数这两个模块配合起来可以解决绝大多数宽高爆破问题。我个人还有一个习惯每次拿到题先复制一份原始文件到工作目录所有分析都在副本上进行。这样做听起来很基础但实际很有用。你在十六进制编辑器里改错一个字节还能拿原始文件重新来如果你直接改原文件改错了想恢复就得重新下载。解压、分离、修复这些步骤每一步都保留中间产物等回头看某一步想不通时中间文件能帮你快速定位到底是哪一步出了问题。CTF 做题快固然爽但稳才是真正的快。