☰
BJDCTF藏藏藏:从图片尾部ZIP到LSB隐写的完整拆解
2026/10/16 7:13:41 网站建设 项目流程

BJDCTF2020里有一道Misc杂项题叫“藏藏藏”,光看名字就知道出题人不想让你轻松拿到flag。三个藏字,几乎等于明说:至少三层隐藏。很多人第一次碰这题会卡很久,不是说工具不会用,而是没想明白到底该往哪里挖。这篇就顺着我当时的解题路线,从拿到图片到最终提交flag,把完整过程拆开讲一遍,包括每一步为什么这么做、用哪个工具、有哪些坑。如果你刚开始玩CTF,或者做Misc老在图片题上卡住,这篇应该能帮你省不少时间。这题的思路也不只对一道题有效,图片隐写题的解法基本都是这个套路。

1. 题目分析与整体思路拆解

1.1 从“藏藏藏”三个字反推题目结构

CTF里有个不成文的惯例:题目名字往往就是出题人送给选手的剧透。“藏藏藏”这种三连,最合理的翻译就是“三层隐藏”。CTF的Misc类图片题里,三层嵌套是一个经典模板:最外面是一张看起来完全正常的图片,用看图软件打开只看到风景或人物,没有任何异常;但用文件分析工具一扫,图片尾部可能挂着一个ZIP压缩包。把压缩包分离出来解压,发现里面不是flag,而是另一张图片;对这张新图片做像素级分析,最后在最低有效位里翻出flag。这样一个文件套一个文件,一层藏一层,正好对应题目名里的三个“藏”。

这样的结构不是只在一道题里出现,大量练习题的出题思路都一样。如果你已经在题目名字里看到“藏”“hidden”“secret”这类词,第一反应就应该是“先分离,再深挖”。反过来,如果你做完分离,发现只拿出了一个普通图片,不要觉得完事了——名字说了三个藏,那说明后面至少还有两层等着你。

当然也有变形。有的题三层都藏在同一张图里:尾部挂一个压缩包,EXIF里塞一段提示,像素低位再藏一段数据。有的题是压缩包里套压缩包,解了一层还有一层。不管是哪种,解题节奏都是一样的:收集信息、定位异常、提取还原、循环往复。掌握了这套节奏,“藏藏藏”和它的兄弟们就都不会难倒你。

1.2 隐写术的核心原理:加密是防读,隐写是防看

这里先补一个基础知识。信息安全里有两类保护手段:加密和隐写。加密是把内容变成一团乱码,别人拿到也知道这是密文,只是解不开,类似把文件锁进保险箱;隐写是让你根本看不出有内容,类似把字写在墙缝里、写在一幅画的颜料夹层里,外人看到的依然是一幅正常的画。CTF的隐写题,本质上就是“找出那面墙上被藏起来的字”。

在图片载体里,常见隐藏方式大概分三类。第一类是文件附加,也就是在正常图片文件的末尾再塞一段其它文件的数据,图片查看器会忽略尾部数据,照常显示图片。第二类是数据块隐藏,利用EXIF、PNG的辅助数据块之类的字段存放信息。第三类是修改图像本身的数值,比如改像素的RGB最低位,也就是LSB隐写,还有改频域系数的JPEG隐写、加盲水印等等。这道“藏藏藏”基本上把第一类和第三类各用了一遍,用来入门或者练手都很合适。

理解这个分类很重要,因为不同隐藏方式对应不同排查工具。文件附加用binwalk扫,数据块隐藏用strings和十六进制编辑器找,像素级隐写用Stegsolve和zsteg。你只有先判断出是哪一类,才知道该上哪个工具,而不是拿着一个工具从头试到尾。

1.3 这类题到底在考什么

很多人觉得隐写题是“背工具题”,我倒是觉得它更像是“信息收集题”。题目不会告诉你数据藏在哪一层、以什么形式存在,你能做的就是一步一步收集信息,从文件名、文件类型、可打印字符串、文件尾部结构、像素分布里判断下一步。这个过程里考察的不是某一门高深算法,而是你对文件格式、文件签名、压缩包原理这些基础知识的熟悉程度。

所以解这类题的正确心态是:把每个工具的输出都当成一条“线索”,而不是“答案”。比如strings里出现“flag.zip”不一定代表zip里就是flag,可能只是出题人故意留的引导;binwalk报出有ZIP偏移,也不代表这个ZIP一定能直接解压,后面还有伪加密在等你。别急着高兴,把每一步的基础信息收齐,再决定挖哪里,才是效率最高的做法。我在带新人时最喜欢说的一句话是:隐写题不是比谁运算快,而是比谁更细心。

2. 工具选型与准备:CTF隐写题的弹药库

2.1 文件识别与信息收集:file、strings、binwalk

开始之前先把弹药备齐。我用的是Linux环境,Kali或者任意发行版都行,Windows下用WSL也可以。我的主力工具清单其实不长:file、strings、binwalk、foremost、dd、010 Editor、Stegsolve、zsteg,再加一个处理压缩包的unzip和ARCHPR。工具不在多,够用且会用才是关键。

第一个工具是file,用来判断文件真实类型。很多隐写题会故意把文件后缀改掉,比如把zip改名成jpg,或者把png后缀改成gif,file能根据文件头识别出真实格式,一下就拆穿伪装。

file 藏藏藏.jpg

第二个是strings,提取文件里的可打印字符串。图片里如果藏了文件名提示、密码、注释、压缩包名,用strings往往能直接看到线索。它的原理就是把文件中连续一段可打印字符抓出来,是一个纯“找明文”的思路,简单但极其有效:

strings 藏藏藏.jpg | head -50

第三个是binwalk,它扫描文件里的文件签名。原理很简单:每种文件格式都有固定的magic number,比如PNG开头是89 50 4E 47,ZIP开头是50 4B 03 04,binwalk把整个文件从头到尾扫一遍,发现某个偏移位置出现了另一个文件的签名,就会报出来。这一步能把“图片尾部挂着ZIP”这类隐藏直接暴露:

binwalk 藏藏藏.jpg

输出会告诉你扫描到的各种文件以及它们的起始偏移量,这个偏移量是后面手动分离的关键数据,务必记下来。binwalk虽然好用,但它不是万能的,遇到加密、压缩、小文件碎片可能会漏报,所以它更适合作为“第一轮扫描”而不是唯一依据。

2.2 数据分割与还原:binwalk -e、foremost、dd

扫描出来只是第一步,接下来是把藏在图片里的数据抽出来。最简单的方式是让binwalk自己提取:

binwalk -e 藏藏藏.jpg

它会把识别出来的文件自动解压到以“_藏藏藏.jpg.extracted”命名的目录里。但是binwalk的自动提取偶尔会出问题,特别是嵌套压缩包或者文件头被破坏的情况。提取出来的文件如果是坏的,或者binwalk干脆没拆干净,就用foremost:

foremost -i 藏藏藏.jpg -o output

foremost也是基于文件签名做数据恢复,它在处理碎片和异常偏移时更稳,因为它会按文件类型去扫描整个输入文件,并且允许你指定只恢复某一种类型,比如只抠zip。还有更精细控制的做法是用dd手动分离。假如binwalk告诉你ZIP起始偏移是123456,就可以这样扣出完整的压缩包:

dd if=藏藏藏.jpg of=hidden.zip bs=1 skip=123456

这里有个细节,binwalk给出的偏移如果是十进制,dd的skip直接用十进制就行;如果偏移带0x前缀,记得先把十六进制转成十进制或者直接让bs支持十六进制。手动分离的好处是你能完整拿到一个干净的zip文件,不会因为binwalk的额外处理造成文件头损坏,后续分析也更好定位。

2.3 像素级取证:Stegsolve与zsteg

等文件被还原成完全的图片形态后,就要进入像素级的分析阶段。这时候常用的是两个工具:Stegsolve和zsteg。

Stegsolve是Java写的图像分析小工具,可以逐层查看图片的RGB分量、各bit平面,还能做XOR、反色之类的像素运算。它最适合用来“肉眼找线索”——比如LSB隐写出来的二维码,在某个bit平面上能直接看出来。打开图片后,挨个翻到Red plane 0、Green plane 1这样的视图,如果看到有明显的规律图案,基本就是藏了东西。它还能把当前视图导出成图片,方便后续扫码或再分析。

zsteg则适合快速扫描PNG和BMP的LSB隐写,它会自动检测嵌入顺序、bit位数。命令非常简单:

zsteg -a flag.png

如果图片里藏了一段字符串,zsteg基本能直接打出来。这两个工具互相补充:zsteg负责自动检测,Stegsolve负责人工确认和视觉化提取。我一般先跑zsteg,如果结果太碎或者不明确,再开Stegsolve人工翻位平面。

3. 实操过程:一步一步拆开这个“藏藏藏”

3.1 第一轮信息收集:先看表面,再挖底层

拿到题面给的文件,我先做的是最基本的file,确认它是一张JPEG图片。然后跑strings,输出里能看到类似“flag.zip”或者“key.txt”这样的字符串。这里要提个醒:strings看到字符串不代表文件就在里面,但可以作为线索记录。我当时还把strings的完整输出保存了一份,后面推理时经常回头翻。

接下来上binwalk。扫描结果里,除了JPEG自身的图像数据,在文件末尾某偏移处,扫描到了一个ZIP压缩包的签名。很多图片查看器不会管尾部的数据,所以普通人看这张图完全正常,而这正是出题人想要的效果。到这一步我已经确认了第一层隐藏:图片文件尾部挂着一个ZIP压缩包。如果你在做题时也走到这一步,恭喜你,第一藏已经现形了。

3.2 把隐藏的ZIP抠出来,并且处理伪加密

我用binwalk -e让工具自动拆,但解压时压缩包要密码。这就尴尬了,题目没给密码,那就先判断是真密码还是伪加密。

伪加密是CTF里特别常见的坑:压缩包里其实没有对数据做加密,目录结构里的加密标志位却是1,解压软件看到这个标志就要求输密码。判断方法很简单,用010 Editor打开zip,定位到本地文件头或者中央目录文件头,找到偏移第10-11字节的“通用标志位”,也就是General Purpose Bit Flag。如果它的最低位是1,说明文件被声明为加密;如果里面根本不存在真正的加密头,那这就是伪加密。把这个标志位改成0,再保存,就能直接解压。

以ZIP本地文件头为例,它的结构是固定的:前4字节是签名50 4B 03 04,接着2字节是版本,紧接着2字节就是通用标志位。在010 Editor里你甚至能在模板解析面板直接看到“Encryption: Yes/No”的字段,比手数十六进制更直观。把标志位改成0后,保存文件重新用unzip解压,就能看到压缩包里的真实内容。

在“藏藏藏”这道题里,ZIP包里的文件名已经指明了下一步方向,比如是一个叫secret.png或key.png的文件。伪加密处理掉之后,解压得到一个PNG图片。如果你在实战中遇到真正加密的压缩包,那就得走另一条路:先看文件名、注释、strings提示里有没有密码线索,没有的话再考虑字典爆破,这个后面第4部分专门说。

3.3 解压之后的二次分析:第二个藏身处

得到第二张图之后,我还是按老规矩来一遍:file确认PNG,strings扫一遍,binwalk再扫一遍。这三步如果干净,说明这张图本身没有附加文件,接下来就考虑像素级隐藏。很多新手到这里容易直接放弃,觉得“没线索了”,其实恰恰相反:文件和字符串都干净,意味着数据极有可能藏在图像数值本身里,该换一套思路了。

到这一步,很多人这才进入这道题的真正难点。如果直接拿Stegsolve打开,一张看似正常的图片,在RGB界面里翻到几位平面,你可能会发现Bit Planes的0号低位平面出现了明显的非随机图案;用zsteg一跑,也能检测出嵌入的数据。这就是第二层隐藏的藏身处:LSB隐写。同时你也可以看一下图片属性里的宽高、位深,如果图片尺寸异常偏大或者色彩模式很奇怪,也可能是线索。

当然,也有些题目会在第二张图片里放一个二维码,必须用Stegsolve切到某个bit plane才看得到,再用手机或工具扫码。这属于一种很常见的变体,所以看到bit plane里有块状图案时,别急着当噪声忽略,先导出成图片看一眼。

3.4 最后一层:把隐藏的数据从LSB里捞出来

先花点时间讲明白LSB到底是怎么回事。图片里的每种颜色都由RGB三个通道组成,每个通道用一个字节表示,取值范围是0到255。把一个像素的红色分量从255改成254,人的眼睛基本感觉不到差异,但二进制里最低的那一位变成了0。LSB隐写就是把信息按位拆开,每一位替掉像素RGB的最低有效位。藏一整条消息,通常需要几千个像素,这就是为什么图片看起来一切正常。

比较省力的提取方法是直接用zsteg:

zsteg -a key.png

zsteg会把检测到的数据直接打印出来。实际操作中,你可能会看到一串Base64字符,复制出来解码就是flag。Base64解码可以用Linux自带的base64命令,也可以扔进任意在线解码工具。遇到别的题目,也可能直接是一串二进制或者十六进制,需要再手工转一层。如果是二维码形态的数据,zsteg的文本输出不一定好看,这时就回到Stegsolve,打开Bit Plane视图,手动选择出现图案的通道,截图出来识别。

到这里,第三层也被拆掉了,把最终得到的flag字符串提交到平台,本题就算完成。完整复盘一下:第一藏是图片尾部挂ZIP,第二藏是ZIP里套PNG,第三藏是PNG的LSB位藏着数据——是不是正好对上题目名里的三个“藏”?

3.5 提交前再确认一遍flag格式

有件事值得单独提醒:CTF平台对flag的大小写、格式非常敏感,这类比赛的flag通常有固定前缀和花括号,形如 flag{...}。提交前先确认是不是完整结构,不要多复制空格或换行。如果平台提示错误,仔细看看从解码结果里提取时是不是混入了多余字符,以及大小写是否一致。这是一个很蠢但很常见的丢分点,我见过不止一个人解出了flag却因为多复制了一个换行符导致提交失败,心态直接爆炸。稳妥的做法是把输出复制到编辑器里,肉眼检查一遍首尾,再把不可见字符显示出来。

4. 常见问题与排查技巧实录

4.1 binwalk提取出来的文件打不开、CRC报错

这个问题出现频率最高。binwalk -e在自动提取多个内嵌文件时,偶尔会把文件头切多或者切少,导致解压时报CRC错误。头几回遇到这事容易慌,其实解法很朴实:第一,用foremost重新提取,它对文件边界的判断更稳;第二,参照binwalk给出的偏移量,用dd手动抠出从ZIP签名到文件尾部的完整数据,再解压。手动抠出来的文件通常是最干净的。

还有一种CRC报错是因为你用了Windows的右键“全部解压”,它对伪加密和部分损坏的zip更敏感,换Linux的unzip或者7z命令行工具往往就能过。如果还是报错,可以用zip的修复工具试一下,但大多数情况下,重新分离一次比修复更靠谱。我自己的习惯是,分离后第一件事就是用file再确认一次文件类型,避免花半天时间分析一个根本没抠对的东西。

4.2 怎么区分伪加密和真密码爆破

判断伪加密的方法我在3.2已经写了,这里再总结成好记的要点:用010 Editor看General Purpose Bit Flag,如果最低位是1,但文件数据部分并没有一个真正的加密头部,就是伪加密,改位即可解压。如果改成0之后解压依然提示密码错误,那基本就是真加密。

判断真加密时,优先尝试strings和文件注释里出现的字符串作为密码,因为出题人不会无缘无故留提示。很多时候密码就藏在前面图片的EXIF里,或者藏在某个txt文件的文件名里。没有提示再上字典爆破,常见的字典比如rockyou。老实说,爆破是最后手段,大多数CTF题目设计时都会留一条线索链,不会要求你硬跑几百万条密码。如果你发现自己已经准备上大字典了,不妨先回头看看是不是漏了什么字符串没注意到。

4.3 LSB提取出来是乱码,问题出在哪

用zsteg跑出来一大段乱码是常有的事。这不是题目坏了,而是你没看对方向。常见原因有四个:

  • 嵌入顺序不同:有的题按RGB逐像素取最低位,有的按BGR,有的只取单通道。zsteg的 -a 参数就是用来遍历这些顺序的,如果第一遍是乱码,多看几个输出,往往会有一个可读。
  • 数据本身不是明文:中间可能套了Base64、十六进制,甚至是异或加密。先试试Base64解码,很多“乱码”其实是Base64字符串。
  • 藏的位不是最低位,而是次低位甚至更高位。Stegsolve的Bit Plane界面能帮你人工检查多个位平面。
  • 藏的是图片而不是字符串:这种情况用字符串视角看当然是乱码,切到对应bit plane,你会发现其实是二维码或者另一张缩略图。

遇到乱码不要立刻改方向,先把这四种可能挨个排除一遍。尤其是“藏的是图片”这一点,很多新手会忽略,导致明明已经找到了数据,却因为用错工具而错过。

4.4 工具都不好使?手写一个Python脚本兜底

万一机器上装不上工具,或者题目出的比较怪,写个Python脚本也能兜底。下面是一个最朴素的LSB提取示例,把图片每个像素的RGB最低位按顺序拼出来,再每8位转成一个ASCII字符:

from PIL import Image img = Image.open('key.png') bits = [] for pixel in img.getdata(): for channel in pixel[:3]: bits.append(str(channel & 1)) data = ''.join(bits) out = [] for i in range(0, len(data) - 7, 8): out.append(chr(int(data[i:i+8], 2))) print(''.join(out))

注意,这只是最基础的版本,没有考虑bit序、通道顺序、数据截断。真实题目里,你大概率要在通道顺序上做调整,比如把三通道的顺序从RGB换成BGR,或者只取某一个通道。这个小脚本的作用是让你理解LSB提取的本质,理解之后再用现成工具,就能读懂工具输出,不会一头雾水。

把这个脚本稍微改一改,还能实现“只提取红色通道最低位”“从图片某个区域开始提取”“把提取结果按行列输出成黑白图”等功能。有一次我就是靠改脚本,提取出了一个二维码,而现成工具因为自动选择顺序不合适,输出一直是乱码。所以关键时候,自己写几行代码比翻工具文档快得多。


我个人在做这类隐写题时,最深的体会是:不要一上来就开Stegsolve这些花哨工具,先把file、strings、binwalk这三板斧走完,至少能解决一半的图片题。还有一个我到现在都在用的习惯:每分离出一个新文件,立刻对新文件重新跑一遍file加binwalk,因为出题人特别喜欢把隐藏做成嵌套,你只要少跑一次,后面的路就很容易走偏。最后再分享一个小技巧,解题过程中随时把每个阶段得到的关键偏移量、文件名、可疑字符串记录下来,看着像草稿纸,其实能帮你把整条线索链串起来。隐写这门技术也不只在CTF里出现,数字水印、版权溯源、隐蔽通信都会用到同一套原理,学会了不亏。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询