ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

CTF隐写题入门:Base64隐写、LSB图片隐写与文件分离实战

CTF隐写题入门:Base64隐写、LSB图片隐写与文件分离实战

1. 隐写题的共性套路:拿到文件先别急着“看”

CTF里的Misc(杂项)题目,说白了就是考“找藏起来的东西”。这个方向不像PWN和逆向那样门槛极高,但特别考验信息收集的完整度和对各种编码格式的敏感度。很多时候题目本身不难,难的是你以为自己已经找遍了所有线索,其实漏了最关键的一步。拿最近在某公开靶场刷到的三道隐写题来说——base64隐写、一张马里奥主题图片、一个黑客帝国风格的字符雨文件——它们放在一起做非常有意思,几乎把Misc入门阶段最核心的几类考点串了一遍。

我先说一个个人经验:拿到任何题目附件,第一反应不要是“用某个神器跑一下”,而是先把文件当成一个“未知物体”做基础信息收集。这个过程叫“体检”。file查看文件类型,strings查看明文串,hexdump或者010 Editor查看文件头,binwalk扫描有没有内嵌文件,这四步走完,再决定下一步用什么工具。

很多新手拿到一张图片就开始上StegSolve,结果眼都快看瞎了也没发现异常,就是因为漏了strings这一步。如果图片后面直接接了一个zip文件头,binwalk一扫描就出来了,根本不需要去分析像素。再比如某些题目故意把flag写在图片的EXIF信息或者PNG的tEXt块里,strings直接就能打印出来。所以“体检”这个环节不是浪费时间,它决定了你后续用什么思路解题,是整套做题流程的地基。

我在这次的三道题里也踩了这个过程的坑,尤其是第二道马里奥图片,前面体检步骤做全了,后面才顺利很多。下面我把这三道题拆开细讲,每个考点都附上可以直接复用的脚本和命令,最后再整理一份常见坑位速查表,希望能帮你省下一些走弯路的时间。

2. Base64隐写:藏在填充位里的秘密

第一道题给了一个文本文件,打开一看,里面全是标准的base64字符串,大概几十行,每行末尾都带着一个或两个=。看到这种格式,大多数人第一反应是解码,于是一顿操作:base64 -d,结果解出来一堆乱码,或者中文字符,怎么看都不像flag。到这里就卡住了。

此时需要冷静下来想一个问题:如果题目只是想让你解码,为什么每个串的末尾=数量不一致?有些一行一个=,有些一行两个=,有些干脆没有。这在正常的base64输出里是存在的,但几十行规律分布就显得刻意了。这就是base64隐写最核心的信号——填充位被人为塞了数据。

2.1 base64编码的填充机制,到底哪里能藏

base64编码的原理并不复杂,它把每3个字节(24bit)拆成4组6bit数据,每组6bit对应一个字符,字符表就是A-Za-z0-9+/这64个。如果原始数据长度不是3的倍数,编码时会在末尾补零,并用=记录补充的字节数:缺一个字节补一个=,缺两个字节补两个=。

问题就出在这个“补零”上。正常解码器在读取填充位时,并不会去校验那些补的0是不是真的0,它直接忽略掉就用。这就意味着,在凑不满6bit的最后一个有效字符里,有一部分二进制位是完全自由的。具体来说:

  • 剩余2个字节(16bit)时,编码成4个字符,最后一个字符实际只用了4bit,另外2bit可以随便塞数据。
  • 剩余1个字节(8bit)时,最后一个字符实际只用了2bit,另外4bit可以随便塞数据。

所以,一个含一个=的base64串能藏4bit,含两个=的能藏2bit。单独一个串藏的数据太少,但几十个串拼在一起,这几十个bit拼起来重新按8bit切分,就能还原出完整的flag。这就是隐写题里特别常见的“每个载体都贡献一点点,最后拼出完整消息”的套路。

2.2 提取脚本:逐行扫描,把隐藏bit抠出来

思路清楚了,脚本就很好写。核心逻辑是:遍历文件中每一段base64串,检查末尾=数量,取出最后一个有效字符在base64字符表中的索引值,按规则截取它的低位bit,累积成完整的二进制流,最后每8bit转成一个字符。

参考脚本如下:

import re b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' def extract_hidden_bits(b64_str): pad = b64_str.count('=') if pad == 0: return '' # 去掉末尾“=”后取最后一个有效字符 last_char = b64_str.rstrip('=')[-1] idx = b64chars.index(last_char) # 一个“=”时最后一个字符只有2bit有效,可抠出低4位 # 两个“=”时最后一个字符只有4bit有效,可抠出低2位 if pad == 1: return bin(idx)[2:].zfill(6)[-4:] elif pad == 2: return bin(idx)[2:].zfill(6)[-2:] return '' bits = '' with open('base64_hidden.txt', 'r') as f: for line in f: line = line.strip() for chunk in re.findall(r'[A-Za-z0-9+/]+={0,2}', line): bits += extract_hidden_bits(chunk) flag = '' for i in range(0, len(bits), 8): byte = bits[i:i+8] if len(byte) == 8: char = chr(int(byte, 2)) if 32 <= ord(char) <= 126: # 只保留可打印字符,过滤干扰 flag += char print(flag)

跑完这段脚本,flag就会以可打印字符串的形式被拼出来。如果flag前面有一堆乱码,也不用慌,那是隐藏的起始bit没有对齐,可以用bits = bits[1:]或者bits = bits[:-1]微调一下,直到输出中出现flag{为止。这个“对齐”是个常见小坑,因为隐藏位可能有前置的空白padding,并不一定从第一个bit就是有意义数据。

2.3 边界情况的处理心得

这种题有两个容易翻车的地方。一个是base64串里如果出现了标准编码没有的字符,比如换行的处理、带\r\n的Windows换行,读取时要先strip(),否则会把空格也算进字符表检索里导致报错。另一个是有些题目会把base64串放在一行里连续拼接,line.strip()以后可能是好几串连在一起,所以脚本里用re.findall去匹配所有合规的base64格式片段,而不是简单把整行当一个串处理。

我做这道题的时候还犯过一个低级错误:直接用base64.b64decode去解每一行的内容,发现全解出乱码后怀疑是编码表被改过,绕了好大一圈。实际上真正需要做的只是统计末尾=和提取低位bit,根本不关心每行能不能正常解出明文。这也是隐写题和编码题的思维差异——编码题指望你能解码,隐写题则暗示信息根本不在“正常的读取路径”上。

3. 马里奥主题图片:LSB隐写与文件分离实战

第二题的附件是一张PNG图片,内容是一个经典的马里奥角色像素图,红帽子、蓝背带裤那种,看起来人畜无害。这种“知名游戏角色图片”在Misc里出现频率极高,原因很简单:制作题目的师傅们喜欢用承载度高、不引起怀疑的载体,像素风格的游戏角色图天然适合做LSB隐写,因为像素变化不仔细看根本感知不到。

3.1 先体检:binwalk、strings、宽高校验都不能少

处理这类图片,我推荐的顺序是:

  1. file xxx.png确认文件类型,顺便看颜色类型、位深这些参数。
  2. binwalk xxx.png扫描里面是否附着其他文件。
  3. strings xxx.png | head -50快速浏览明文串,有些flag直接裸奔在EXIF信息里。
  4. python3用PIL打开图片,检查宽高是否正常,有没有被改过宽高导致内容被裁掉。

这张马里奥图在binwalk阶段就发现了异常:图片末尾紧跟着一个ZIP压缩包的魔数PK(504B0304)。这里有个细节,binwalk的默认扫描有时候会因为文件系统的“几何位置”判断不准确,漏报某些后附文件,所以我习惯再用010 Editor直接跳转到图片末尾看一眼。更严谨的办法是直接看十六进制,搜索50 4B 03 04这个zip头特征,找到后手动把从该位置到文件末尾的内容dump出来。

手动分离的姿势也不难,Linux下可以用dd加skip参数配合if和of,比如已知zip数据从偏移量0x1234开始,就执行:

dd if=xxx.png of=hidden.zip bs=1 skip=$((0x1234))

dd的bs=1在文件较小的时候没问题,如果图片很大,建议把bs设成大块再配合skip的块数计算,否则速度会慢到你怀疑人生。

解压出来是一个加密的zip,需要密码。围绕密码的线索通常在图片本身:可以继续用zsteg扫描LSB层,用StegSolve逐通道看低位平面,或者检查PNG的文件头信息里有没有自定义的注释字段。这时候我立刻想到刚才strings扫出来的一串疑似密码的字符串。拿过来一试,zip解压成功,里面是一个文本文件,flag就在其中。

3.2 LSB隐写检查的完整操作路径

如果binwalk没查出问题,第二步就要怀疑LSB隐写了。所谓LSB,全称是Least Significant Bit,即每个像素颜色值的最低1bit。图像的高位bit决定了人眼看到的大致颜色,而最低位只有0和1的区别,肉眼根本分辨不出来,所以这里成了最经典的隐藏位置。

检查LSB最直观的工具是StegSolve,打开图片后选择Analyse -> Data Extract,把Red、Green、Blue三个通道的勾选都调整到LSB first,Bit order选LSB,Preview后如果能看到明显的字符串或图形轮廓,基本就实锤了。

命令行选手则强烈推荐zsteg,一条命令能扫描大部分常见组合:

zsteg xxx.png

zsteg内置了PNG和BMP的多种通道、多种位级别的隐写检测算法,跑完一遍如果没有任何输出,那说明大概率不是LSB套路。我之前遇到过一张图,zsteg什么都没扫出来,但用StegSolve手动查看Blue通道0号平面时,隐约看到了一个二维码轮廓。所以工具的结果只能当参考,不能当结论,尤其是暗通道的隐写,自动扫描未必能识别,人眼+经验反而更可靠。

3.3 关于改宽高这个经典陷阱

还有一种非常常见的图片隐写方式不是藏数据,而是“切画面”——把PNG的IHDR块里的宽或高改小,导致图片只显示一部分,而flag就写在被裁掉的那部分区域。判断方法很简单:用Python读取IHDR的宽高,然后用zlib去解图像数据流,如果解出来的原始数据长度和当前宽高计算出的理论值对不上,就说明宽高被改过。

其中有一种只改高度的做法,原始数据长度不变,但CRC32校验会报错。遇到这种情况,直接用pngcheck跑一下:

pngcheck -v xxx.png

它会告诉你CRC校验错误发生在哪个chunk,以及理论上的IHDR校验值。修正宽高的方式是把高度逐字节枚举,直到zlib流能正常解压且CRC匹配。网上有很多现成的脚本,自己写也不复杂,核心就是用PIL逐高度尝试读取,看能否成功load。这个知识点属于Misc图片题必考范围,虽然这道马里奥图没用到,但同场题目里经常和LSB混合出题,我习惯写在这里做个备忘。

4. 黑客帝国里的字符雨:文本载体隐写的变体思路

第三题的附件是一个纯文本文件,打开以后屏幕上全是“黑客帝国”风格的绿色字符雨——满屏随机的小写字母、数字和符号,一行一行往下滚,猛一看毫无规律。很多选手看到这种文件会先入为主地认为“这就是个障眼法,信息得靠图片分析”,于是拿着它当图片工具折腾半天,其实方向就错了。

字符雨文本这类题目,说白了还是在考“从噪声里提取信息”,但载体从图片变成了纯文本,那么手段也从像素分析变成了文本模式分析。

4.1 先看格式:是否有规律性的列结构

我拿到这个文本文件后,第一步是用编辑器查看,发现每一行的长度基本都是一样的,大约是80字符左右,这个长度恰好是传统终端的一行宽度。既然列数固定,那么第一反应就是“按列去读”。

具体操作是:把文件读进Python,按行拆分,然后按列索引依次取字符,看看每一列能不能拼出有意义的字符串。如果某一列拼出来是一串base64字符,那就是突破口;如果每一列都没有意义,那再考虑行变换、列置换等隐藏规则。

这道题的实际情况是:每行看起来都是随机字符,但把每一行的最后一个字符连起来读,得到了一串base64编码。这其实不罕见,在字符雨这种场景里,干扰信息越多,真正有用的信息就越容易被藏在相对固定的位置上。定位方法也很简单,可以用Python统计每行最后一个字符,打印出来看看是不是有规律的ASCII范围,或者直接awk '{print substr($0,length($0),1)}'提取末列。

4.2 绿色字符雨图片版:帧对比和颜色通道

有些“黑客帝国”题目给的不是文本文件,而是一张GIF动画或一段视频,画面就是电影里那种不断下落的绿色字符雨。动态载体相比静态图片,多了一个“帧”维度,最常见的隐写方式是:绝大多数帧都是一样的乱码雨,只有某一帧或某几帧藏着关键信息,比如一个二维码,一串坐标,或者某几个字符明显和其他帧不同。

处理手段是把所有帧拆出来逐张对比。GIF用convert(ImageMagick)可以拆帧:

convert matrix.gif -coalesce frame_%03d.png

拆完以后,可以用Python的PIL库把所有帧叠在一起,逐像素对比帧间的差异,差异最小的位置往往就是藏信息的帧。更简单的方法是肉眼快速浏览一遍缩略图,如果图片有几十帧,一眼能扫完,那个“长得不一样”的孤帧很容易被揪出来。

另外,绿色字符雨本身还有一个经典陷阱:字符的颜色值并不全是同一款绿色。有些字符的RGB是(0, 255, 0),有些是(0, 254, 0),这种微小的色差也是数据载体。把颜色值按低bit提取,和LSB隐写的思路完全一致,只是载体从静态图变成了动图。所以遇到绿色主题的题,记得顺手做一次色值分析,把出现次数最多的绿色值列出来,看看有没有不止一种“绿”。

4.3 文本噪声题的正确解题心态

这类题真正的难点不在技术,而在心态。字符雨文本动辄几百行,每行七八十个字符,肉眼盯久了容易眼花,这时候最容易犯的错误是:用“感觉”去找线索,而不是用“统计”。我在做这道题时,一开始也是盯着屏幕看了半天,试图找出某一行里隐藏的单词,结果什么也没看出来。后来调整思路,把所有行长度、字符频率、每行末列都导出来统计了一遍,才锁定了末列这个位置。

另外一个经验是,如果途中发现某一部分拼出来的字符串疑似base64,但解码后是乱码,不要立刻否定,可以先尝试URL解码、十六进制转ASCII、反转字符串等二次编解码。隐写题经常会出现“base64编码里再套一层十六进制”的情况,留意那些解码结果中出现的0x、%、\x前缀,都是多层编码的信号。

5. 工具选型与快速排查手册

做Misc隐写题,工具不在多,核心那几样必须练熟。我整理了一份自己日常做题时固定使用的工具清单和适用场景,你可以直接抄作业。

工具/命令适用场景常用姿势
file判定文件真实类型,防止扩展名骗人file flag.png,看输出是PNG还是JPEG还是其他
strings提取文件中的可打印字符串strings -n 8 flag.png,过滤太短的噪声串
binwalk检测文件内嵌的其他文件binwalk flag.png,关注是否出现ZIP、RAR、7z等签名
zstegPNG/BMP的LSB隐写检测一行命令,多通道扫描,速度快
StegSolve手动逐通道查看像素平面Java图形工具,适合肉眼判断LSB是否有内容
pngcheck校验PNG结构,检查CRC错误pngcheck -v flag.png,排查宽高是否被改
dd手动截取文件中某段数据dd if=flag.png of=out.zip bs=1 skip=偏移
010 Editor十六进制查看与编辑,搜寻文件签名搜504B0304定位zip头
convertImageMagick,GIF拆帧、格式转换convert a.gif -coalesce frame_%03d.png
Python + PIL /zlib自定义分析像素、文件结构适合写脚本做批量扫描和格式修正

工具的使用有一个原则:先用自动化的广谱扫描,再用人工分析做定点确认。比如zsteg扫完没有输出,再用StegSolve手动看几个关键通道;binwalk扫完没有发现,再用010 Editor人工找签名。反过来先上手手动工具,容易在噪声里迷失方向,效率反而低。

关于binwalk,还有一个小提醒:有些情况下它会报出一堆无关的“压缩包”或“固件”签名,尤其是文件中间某些随机数据恰好命中了常见魔数。这时候不要盲目分离,要用十六进制确认一下签名后的数据是否真的构成了一个完整有效文件——最简单的办法是把分离出来的文件用file命令再看一次,确认是否是Zip archive data而非data。

6. 三道题连做的踩坑心得

最后这条,算是我在做这三道题时反复踩过、又最终趟平的一些经验总结,对入门Misc的选手应该有参考价值。

第一,别把简单问题复杂化。base64隐写那道题,我一开始的时间几乎都花在“分析解码后的乱码”上,总觉得是不是用了自定义字母表,后来才意识到真正的信息根本不在解码结果里,而在编码的“废弃物”填充位。Misc题的答案通常藏得很浅,只是藏的路径刁钻。如果你在一个方向上折腾了半小时没有任何进展,大概率是思路错了,而不是工具不行。

第二,体检步骤永远别省。马里奥图片如果当时直接跳过strings和binwalk,直接上StegSolve,那个zip可能耗掉大量时间才能被偶然翻出来。相反,按顺序做完体检,几分钟就能锁定后续方向。我后来养成一个习惯:拿到附件,先花5分钟做完file、strings、binwalk、hexdump这一套,再决定用什么高级工具,这个小习惯帮我避开了无数次无效分析。

第三,脚本能力是真·核心。base64隐写题没有现成工具,必须自己写Python提取bit,哪怕代码再丑,能跑就行。Misc方向越往后做越能体会:很多题不是“会不会工具”,而是“会不会写脚本把数据批量处理成有意义的信息”。建议初学者至少掌握Python的文件读写、字符串处理、编码转换和PIL图像读取,这四样能覆盖80%的隐写题场景。

第四,注意信息的多层编码。黑客帝国那道题里,从末列提取出来的base64解码后是一段十六进制字符串,那段十六进制再转ASCII才得到真正的flag。很多选手栽在最后一步,解出来一眼看到666C6167这种十六进制文本,就以为是自己提错了数据,其实那正是flag的十六进制表示。“解码结果可能还是一层壳”,这句话在Misc做题时建议贴在脑门上。

对我来说,这三道题的价值不在于难度,而在于完整覆盖了“编码隐写—文件分离—文本噪声”三种载体形态。把它们的解题路径各自走通以后,再遇到类似的题目,基本上看一眼附件特征就能锁定排查方向。后面你如果也碰上了类似风格的题,不妨先按这套流程跑一遍,大概率能少走一些弯路。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进