CTF图片隐写实战:从风景照到flag的完整取证链

发布时间:2026/10/11 13:03:23
CTF图片隐写实战:从风景照到flag的完整取证链
ISCTF2025 的 Misc 题里有一道《美丽的风景照》附件是一张海岸线日落的风景图1920x1080颜色挺好看看着人畜无害。但干 CTF 这行的人都懂一个不成文的规矩题目里给一张图十有八九不是让你看风景而是让你翻字节、数像素、找隐藏数据。这道题就是典型的图片隐写题目考的是文件结构分析、数据分离、编码解码和像素级信息提取这一整套基本功。如果你是刚入坑 CTF 的新人或者想系统补一下图片隐写的解题思路这道题值得从头到尾走一遍踩坑点和经验都很齐全。我拿到附件的第一反应不是双击打开看风景而是先把它丢进终端用file、binwalk、strings这一套标准流程探一遍底。事实证明风景照只是伪装真正的信息藏在图片尾部拼接的数据和一个不起眼的小图里。下面我把完整思路、工具选型和实操过程全部拆开讲后面还会附上这类题常见的变体和排查技巧帮助你下次遇到同类题能直接照方抓药。1. 拿到题目的第一波操作先把“风景照”的真面目摸清楚1.1 题名和分类已经在提示考点了“美丽的风景照”这个标题放在 Misc 分类下基本等同于出题人在说“我往图片里塞了东西你来找”。Misc 的图片题花样很多但万变不离其宗无非下面这几类文件尾部拼接恶意或隐藏数据、修改图片宽高隐藏区域、LSB 或 MSB 隐写、通道分离隐写、文件伪装比如把 zip 改成 jpg、双图盲水印对比、EXIF 元数据藏东西。拿到题先别慌着破解先把可能的考点在脑子里过一遍再决定每一步做什么。这道题的难点不在于某个环节特别深而在于它把好几步串在了一起——压缩包分离、密码获取、二次隐写提取。你只盯着图片本身看可能看半天没结果得顺着“文件里还有文件”的线索一路挖下去。这正是这类题目最真实的解锁方式出题人不会把 flag 直接贴你脸上他会给你设计一条需要层层递进的取证链。1.2 第一步信息收集四个命令打底我习惯拿到附件后先做一轮“全身体检”命令不长但能把图片的基本情况彻底摸透file landscape.jpg ls -la landscape.jpg strings -n 8 landscape.jpg exiftool landscape.jpg当时file输出的结果是 Jinpg 标准格式JPEG image data, JFIF standard 1.01, resolution (DPI), density 96x96, segment length 16, baseline, precision 8, 1920x1080, components 3文件大小是 2.3MB就一张普通照片来说不算异常。strings过滤可打印字符串在图片里搜了flag、ISCTF、CTF这些关键字一无所获。exiftool输出的也是一堆常规的拍摄参数没有明显的经纬度、注释或作者字段。这一步很容易让人陷入“是不是题目没毛病”的错觉但恰恰是这个阶段最容易忽略一个关键指标——文件大小。一张 1920x1080 的 JPEG正常体积应该在 1MB 到 3MB 之间2.3MB 并不夸张可如果这张图是纯自然风景、压缩率高一点体积应该更小一些。我心里打了个问号但没有立刻下结论因为 JPEG 的压缩率本身就飘忽不定单靠体积推断不算扎实。真正让我确定有问题的是接下来这一步结构扫描。2. 工具链拆解常见隐写题需要哪些武器2.1 基础命令行工具先扫再拆处理图片隐写题有一套常规起手工具组合我把用途和常用姿势统一列出来方便对照:工具主要用途常用命令备注file判断文件真实类型file xxx.jpg防止扩展名伪装strings提取文件中可打印字符串strings -n 8 xxx.jpg加 -n 控制最短长度避免噪音binwalk扫描文件中的嵌入式数据binwalk -e xxx.jpg能识别常见文件头特征foremost按文件签名批量分离foremost -i xxx.jpg -o output适合自动提取隐藏文件exiftool查看和修改图片元数据exiftool xxx.jpg检查 EXIF、GPS、备注hexdump按十六进制查看文件细节hexdump -C xxx.jpg | tail -20检查文件尾部附加值pngcheck校验 PNG 结构和 CRCpngcheck -v xxx.png排查宽高被修改的情况这套东西虽然看起来不高端但胜在稳。CTF 题目绝大多数隐藏数据都逃不过这几位的一轮扫描。你不需要把它们的每个参数都背下来只要牢记几条核心用法就够了。2.2 图像专项分析工具从像素层面找破绽当命令行工具扫不出明显问题时就要转到像素级别的分析上了。我包里常备三个Stegsolve图形界面工具可以逐通道、逐位平面查看图片的 R、G、B 各 bit 信息特别适合发现肉眼看不到的隐写。看到某个颜色平面出现有规律的棋盘格或文字区域时基本上就是有隐写的铁证。zsteg这是一个专门检测 PNG 和 BMP 隐写的命令行工具能自动尝试常见的 LSB 隐写组合比如 RGB 三通道的 LSB、提取位顺序差、BGR 顺序转换等等。一条zsteg -a下去能省掉很多手工试错的时间。Steghide它有自己的一套隐写算法常用于 JPEG、BMP 文件。如果你的 binwalk 扫描一无所获但直觉告诉你图片“不对劲”可以试一下steghide extract -sf image.jpg密码可能藏在文件名、提示文本里。2.3 Python 兜底脚本工具失灵时的最后防线依赖 GUI 工具的问题是无回显不知道它到底扫描了哪些组合。真正需要细粒度控制的时候我习惯直接写几十行 Python 脚本用PIL和numpy自由控制像素。比如一个最基础的 LSB 提取脚本长这样from PIL import Image img Image.open(target.png) px img.load() width, height img.size binary for y in range(height): for x in range(width): r, g, b px[x, y][:3] binary str(r 1) binary str(g 1) binary str(b 1) message for i in range(0, len(binary), 8): byte binary[i:i8] if byte 00000000: break message chr(int(byte, 2)) print(message)有些朋友会觉得直接抄现成工具脚本更方便但自己写几张纸代码的收益在于你能精准理解每个参数的含义——读取顺序、位偏移、通道顺序这些细节在题目稍有变形时就是区分你能不能做出来的胜负手。3. 实操全流程还原一道典型风景照题的完整取证链3.1 第二步binwalk 扫描直接破局初步检查没发现异常后我把重点放到了文件结构上直接命令binwalk landscape.jpg输出里除了 JPEG 本体之外尾段出现了一行扎眼的记录DECIMAL HEXADECIMAL DESCRIPTION 0 0x0 JPEG image data, JFIF standard ... 1910770 0x1E7FB2 Zip archive data, at least v2.0 to extract尾部有 zip 数据。这说明出题人把压缩包硬拼接到了图片末尾。JPEG 的解析器通常只会读取到文件结束标记FFD9就会停止渲染所以后面追加多少别的数据都不影响图片正常显示。这是图片隐写里最常见的藏法之一难点不在找而在于把后面的数据干净地分离出来。binwalk 自带的-e参数可以直接自动提取但有些版本会抽风所以我更习惯用 foremost 再跑一次做交叉验证foremost -i landscape.jpg -o extracted分离结果目录下出现了两个文件flag_hint.txt和一个secret.zip。看到 txt 我心里就有数了这种给 hint 的文件十有八九是解开下一步的关键。打开看一眼内容是一串 base64 编码bGFuZHNjYXBlXzIwMjU用命令一键解码echo bGFuZHNjYXBlXzIwMjU | base64 -d输出landscape_2025。这就是密码提示明摆着告诉我们下一步解压 secret.zip 要用这个口令。3.2 第三步压缩包不是真加密而是伪加密开始我直接尝试unzip secret.zip结果提示需要密码。输入landscape_2025之后顺利解压但从做题的角度我必须提一下另一种很常见的坑——伪加密。伪加密这个技术很有意思zip 文件的目录区里有一个 general purpose bit flag如果第 0 位被置 1很多解压工具就认为文件有密码。有些题目并没有真正给文件内容做 AES 加密只是改了标志位让它“看起来像加密了”。判断方法也很简单zipinfo -v secret.zip查看输出中的 “special purpose fields” 或类似字段如果能看到加密标志但怎么输入密码都不对这就可能是伪加密。处理方式是用十六进制编辑器打开 zip找到 local file header 对应的通用位标志把0x0001加密位改成0x0000保存后再解压。有些压缩包是“伪加密真加密”混合的修改后只能读取未真正加密的文件部分。这道题我试密码一次就过了所以没有走到改 zip 标志位这一步但遇到类似情况时这条技巧是必考的建议顺手记住。伪加密的判断本质是“文件内容是否真的被加密”而不是单纯看标志位。3.3 第四步解出来的小图才是真正的藏宝图secret.zip解压后得到一张 PNG文件名叫tiny_landscape.png尺寸只有 200x100看起来像原图的缩小版。按理说一张 200x100 的小风景图应该很干净但我总觉得颜色过渡有点不自然像是夹杂了颗粒噪声。这种“视觉上略微发脏”是 LSB 隐写最常见的特征。LSB 隐写的原理说起来很简单把要隐藏的二进制数据逐位替换成图片每个像素 RGB 分量最低的那一个比特位。由于最低位对颜色影响极小人眼几乎分辨不出来但如果你把一个像素的三个通道的最低位都提取出来按顺序拼接就能还原出一段二进制数据再转成可见字符就是隐藏信息。我直接用小图跑了一轮 zstegzsteg -a tiny_landscape.png输出结果里排在前面的几条全是噪音但中间有一条非常规整的字符串结尾是ISCTF{...}的格式。这两个特征基本意味着这就是我们要找的 flag。当时输出截取大概是b1,r,lsb,xy - 全尺寸图像 001010010... b1,rgb,lsb,xy - 这段有文本数据 ISCTF{xxx_xxx_xxx}如果你的工具没有一体化输出也可以手工写 Python 提取一遍再 binascii 转字符串效果完全一样。需要注意的细节是LSB 提取的二进制串到底是 8 位一组还是 7 位一组读取顺序是从低位到高位还是高位到低位通道顺序是 RGB 还是 BGR这些都可能因为出题脚本的写法不同而变化。zsteg -a能自动尝试大部分组合但如果遇到自动检测失败还是要手动组合参数多试试。3.4 从 binwalk 到 flag 的完整复盘拿到 flag 再回头看整道题其实链路并不长关键是你不能在第一站的常规检查上卡太久。完整的流程是这样的拿到landscape.jpg基础命令检查没有直接发现东西。binwalk扫描出尾部有 zip用foremost分离出 hint 文件和加密压缩包。hint 文件内容 base64 解码后得到压缩包密码。解压出小图tiny_landscape.png。对tiny_landscape.png做 LSB 隐写提取得到 flag。每一步都是独立的点但串起来就是一个标准取证流程。如果中途哪一步没发现后续线索就得回头想想是不是有哪些信息被遗漏了——比如 hint 文件里有没有空格、换行导致解码不干净压缩包里有没有隐藏的注释字段小图有没有 EXIF 信息。这些都是细节坑。4. 风景照藏考点的经典变体遇到这些套路要会变通单一题目的解法不具备普适性真正值钱的是从里面提炼出的通用方法论。这里我整理了另外几种在图片隐写题里出现频率极高的变体顺手给出对应思路。4.1 变体一宽高被改动隐藏区被硬生生裁掉了这类题通常给一张看不出毛病的 PNG但用pngcheck校验时 CRC 报错或者用图片查看器打开后内容明显只有原图的一部分。这是因为出题人在图片头部 IHDR 信息块里把高度或宽度改小了导致图片按错误尺寸渲染下半部分隐藏的二维码、flag 文字被藏在“可视区域之外”。修复思路就是按真实尺寸把宽高改回去。用十六进制编辑器直接定位 PNG 头部的 IHDR 数据块对应位置的宽高字段是明文十六进制数字。如果拿不准真实值可以观察原图宽高比或者根据图像内容里被截断的线条推断。改完记得重新校验 CRC否则一部分查看器会拒绝打开。命令行里可以用pngcheck -v做验证图形界面也可以用 010 Editor 这类工具直接修改。4.2 变体二尾部直拼字符串和 EXIF 藏宝有些题目出得比较直白根本不搞二次分离直接把 base64 文本或者flag{xxx}明文拼在图片尾部。你只需要用hexdump -C查看文件末尾或者权限用strings加上-n 6就能看到。这个变体的防御价值不大但实战中很多新手会忽略对文件尾部的检查看到文件是 jpg 就默认“正常”。EXIF 藏宝则更刁钻一点它的信息隐藏在图片的元数据里不会影响像素。用exiftool -a全量查看所有标签重点关注Artist、Comment、ImageDescription、UserComment这类可写文本字段。user comment 字段里经常出现 base64 或密码也见过直接把 flag 分成两半藏在两个字段里的操作。这种题的难点不是提取而是你能不能想到去翻元数据。4.3 变体三双图对比和盲水印如果题目同时给了你两张看起来几乎一模一样的风景照那大概率不是让你用肉眼找不同而是要用像素差异分析。你可以直接用 ImageMagick 的compare命令生成差异图也可以自己写脚本去逐像素对比import cv2 import numpy as np img1 cv2.imread(scene1.png) img2 cv2.imread(scene2.png) diff np.abs(img1.astype(np.int16) - img2.astype(np.int16)) cv2.imwrite(diff.png, (diff * 10).astype(np.uint8))把差异值乘以一个放大系数再保存原本肉眼不可见的差异区域就会变成清晰的图案可能是二维码、文字或者一串字符。需要注意两张图如果大小不同要先做 resize 对齐否则差值毫无意义。还有一种盲水印玩法是把水印信息隐藏在图片的频域里比如 DCT 系数或者小波变换系数中。这种题目用常规 LSB 检测是扫不出来的需要做 DCT 变换后提取高频分量比对。属于进阶玩法初学阶段先把前面几种吃透就够了。4.4 变体四二重、三重套娃“美丽的风景照”这道题本身就是套娃结构——图片拼 zipzip 里有小图小图里藏 LSB。现实中更变态的出题人会在压缩包里再放一张图片那张图片又是另一个 zip。遇到这种情况我给的建议是重复执行“扫描-分离-检查”这个循环直到碾到底。循环什么时候停看到输出里有 flag 格式的字符串或者有一份明文提示指向最终解答的时候就停。做题最忌讳的是挖到某一步发现没有 flag 就慌了其实你只是在路线上走到了中间点远不到放弃的时候。5. 方法论沉淀做图片隐写题的通用流程和避坑清单5.1 五个固定的检查台阶在实战中我把图片隐写题的处理压缩成五个台阶每道题从上往下走一遍通常在第两三阶就能看到明确线索第一步看类型和大小。file判断是否伪造扩展名对比文件大小与正常范围的偏差。第二步看元数据和字符串。exiftoolstrings重点找明文 flag、base64、关键词提示。第三步看文件结构。binwalk扫描嵌入式数据foremost批量分离要关注尾部附加数据和“文件中的文件”。第四步处理中间产物。遇到压缩包先看是否伪加密遇到编码串先解码遇到密码提示先记录flag 可能就藏在中间环节。第五步像素级隐写检测。LSB 优先用 zsteg 和 Stegsolve 交叉验证最后用 Python 脚本做定制提取。这五个台阶不是死板的顺序而是要形成条件反射。比如文件大小明显异常你甚至可以跳过第一步直接跑 binwalk比如图片是一张纯色背景的 BMP基本可以断定是 LSB直接上 zsteg。5.2 个人经验踩过的坑和操作禁忌做这种题我踩过不少坑挑几个印象深的提醒一下。不要只依赖一个分离工具。binwalk 的自动提取偶尔会把压缩包内容提取不完整foremost 的默认输出也可能漏掉某些无文件头的裸数据。两个工具交叉验证必要时再手动查看十六进制做补充才是最稳妥的。base64 解码前先 clean。hint 文件里可能带着换行符、多余空格直接base64 -d会报错。建议先把整段文本复制到变量里用tr -d \n清理后再解码。伪加密的坑不要急。遇到 zip 解压密码不对先检查通用位标志确认是伪加密再改字节不要一上来就上字典爆破浪费几小时。字典爆破是最后手段不是常规手段。jpg 和 png 的隐写思路不一样。JPEG 更常见尾部拼接和 Steghide 隐写PNG 更常见宽高修改和 LSB/zsteg 隐写。看到文件类型之后隐式调整后续工具选型能显著降低无效操作的时间。最后再分享一个小技巧做题时别急着只盯 flag把每个阶段发现的异常数字、奇怪字符串、可能的密码都记录在草稿里。很多题目的线索是跨文件传递的比如这里的 base64 密码是从 hint 文件里找到的而 hint 文件就在图片尾部。这种“先记录后串联”的习惯能让你在遇到复杂套娃题时保持清晰的逻辑线不至于做到一半就乱了对不上号。这套思路不仅仅适用于 CTF类似技巧用在真实世界的文件取证、恶意文件分析里也完全说得通算是练一种“看到数据不轻易放过”的本能吧。