CTF MISC隐写实战:从low题破解LSB与低频隐写套路
发布时间:2026/9/15 21:01:34 作者:尧图编辑部 阅读量:1,286

攻防世界MISC高手进阶区里有一道叫“low”的题我第一次打开附件的时候文件名就一个low没有任何描述。这种干净得让人发慌的题目其实非常考验你平时积累的做题节奏。这篇文章我就拿这道题当引子把MISC里和“low”相关的隐写思路、工具用法、脚本写法全部拉通讲一遍。无论你最后下载到的附件是图片还是音频这套分析方法都能直接用上。1. 拿到“low”这道题的前十分钟我在想什么1.1 文件名low留给我们的三条线索CTF的出题人很少会起一个毫无意义的名字尤其是MISC题文件名本身往往就是最大的提示。看到low这个词我脑子里第一反应不是“简单”而是“低”。它至少能对应三条技术线索第一是low bit也就是最低有效位隐写通常说的LSB第二是low frequency低频段隐写常见于音频文件第三是low contrast低对比度信息隐藏比如把文字藏在纯色图片的极微小色差里。这三条线索指向的文件类型和解法完全不同。如果一开始就默认它是图片LSB结果附件是个WAV音频那前面所有操作都白费。我的习惯是先不急着开工具而是在心里列一个分支清单等做完基础排查后再收缩范围。很多新人卡题不是因为不会用工具而是没有做这一步“线索管理”看到题目就直接埋头跑命令跑不出结果就慌。1.2 先看文件属性大小、扩展名、哈希值拿到附件的第一步永远是确认文件真实身份而不是看扩展名。我用的是Linux环境一条命令完成file low.png这里有个很常见的坑Windows资源管理器显示的文件扩展名不一定真实很多MISC题会把zip改成png甚至把ELF可执行文件改成jpg。file命令能直接识别文件的真实格式这是所有后续分析的基础。接下来要看文件大小。一张正常的风景PNG在1024x768分辨率下大小通常在1MB到2MB之间如果是纯色或简单渐变可能只有几十KB。如果一张看似简单的图体积异常大或者看起来应该很大的图却只有几KB那就要引起警觉了。体积异常大说明里面可能塞了额外数据体积异常小说明图片本身可能被人为压缩过隐藏信息的方式也会受限。最后顺手算一下哈希值md5sum low.png sha1sum low.png记录下来不是为了炫技而是为了后面方便在搜索引擎里检索原始附件。有些题目可能来自公开的隐写分析样本哈希一查就能找到线索。另外一个实际好处是如果你后面修改了文件比如修复PNG宽度有个原始哈希能随时对比避免弄混版本。1.3 不要跳过strings和binwalk很多老手容易犯一个毛病觉得strings和binwalk太基础直接跳过结果在隐写上折腾半小时最后发现flag就在文件尾部的字符串里。这种题在MISC里并不少见尤其是一些“高手进阶区”的题反而会利用选手的轻视心理。strings low.png | head -50这个命令会把文件里所有长度超过一定阈值的可打印字符串都拉出来。PNG文件的文本块、注释、EXIF信息、藏在文件尾部的数据都会在这里暴露。如果直接看到flag{...}那这道题就结束了。即使没看到flag看到一串可疑的base64或者URL也值得记录下来。接着跑binwalkbinwalk low.pngbinwalk的作用是扫描文件内是否嵌入了其他文件。它会识别出PNG里的IDAT块、zlib压缩数据以及可能存在的ZIP、RAR、其他图片等。看到“Zlib compressed data”不要激动PNG本身的图像数据就是zlib压缩的这属于正常结构。真正的重点是看有没有文件头特征出现在正常结构之外比如在PNG的IEND结束标记之后又跟了一个PK头那说明后面藏了一个压缩包。这两个命令加起来不到一分钟却能过滤掉相当一部分“送分题”。在我做完这轮基础检查之后low这道题并没有直接暴露flag但是文件类型是PNG大小中等偏小没有明显的异常字符串。那接下来重点就要放到图像隐写上了。2. LSB隐写名为low的题十有八九藏在这里2.1 LSB隐写的原理用人话讲清楚LSB全称Least Significant Bit最低有效位。这个技术原理不难但很多人理解得不够透彻。一张24位真彩色PNG图片每个像素由红、绿、蓝三个颜色通道组成每个通道用一个字节表示取值范围从0到255。如果把一个字节写成二进制比如十进制的200就是11001000其中最右边那一位就是最低有效位它只影响数值的奇偶性对颜色的影响微乎其微。把某个像素的红色分量从200改成201肉眼完全看不出区别。隐写就是利用这个“看不出来”的空间把要隐藏的数据逐位拆开写进每个像素某个通道的最低位。举个例子我想嵌入字母A它的ASCII码是65二进制是01000001。那我就需要8个像素依次把它们的某个通道最低位改成0、1、0、0、0、0、0、1这样就完成了1个字节的隐藏。因为每个像素能提供3个bitRGB各1位一张1024x768的图片理论上可以存下约294KB数据足够藏一个文本flag、一个二维码图片甚至一个小压缩包。理解这一点很重要如果你提取出来的数据非常大明显超过了理论容量那说明你的提取方向可能不对。2.2 用Stegsolve逐通道翻看正确姿势是什么Stegsolve是MISC选手的老朋友一个Java小工具专门用来逐通道逐位平面查看图片。用Stegsolve打开low.png后底部有一排按钮通过左右箭头可以切换不同的颜色通道和位平面组合。不要漫无目的地点击要有顺序地看。先从Red plane 0、Green plane 0、Blue plane 0看起这三个分别是RGB通道的最低位平面。如果某个位平面上出现了明显有规律的明暗图案比如一行行条纹、方块状的二维码轮廓或者隐约可辨的文字那基本可以确定存在LSB隐写。还有一种更常见的情况单独看某个通道并不明显但把三个通道的最低位合并成一张灰度图时隐藏信息就会现出原形。Stegsolve的Gray Bits模式就是干这个的。它的计算方式是将三通道的同一个位平面叠加成8位灰度值因为隐写数据可能分布在多个通道里单通道看是破碎的合并后才能拼出完整内容。一个小提醒Stegsolve打开超大尺寸图片会非常卡有些还会直接卡死。如果你遇到的是几千像素宽的大图可以先用Python的PIL把图片长宽缩减到500像素以内保存后再丢进Stegsolve做快速筛查。注意这只是筛查缩小过程会破坏像素最低位所以筛查到疑似区域后必须用原图做最终提取。2.3 zsteg一键扫描比肉眼快得多手动翻位平面比较费时所以我通常先跑一遍zsteg。zsteg是Ruby写的一个隐写检测工具对PNG和BMP的LSB隐写检测非常出色。安装方式很简单gem install zsteg用法zsteg -a low.png-a表示尝试所有已知的通道组合、位序和扫描方向。它会自动输出检测到的隐藏信息并且会注明使用的是哪套参数。比如输出里出现b1,bgr,lsb,xy意思就是三个通道的最低位按BGR顺序按行扫描。这个输出对你后续写脚本非常有价值。它相当于出题人的“说明书”告诉你了数据的嵌入方式。zsteg扫不出来不代表没有LSB只能说明常见组合都不对。有些题目会故意用变种比如只把数据写进绿色通道的最低位或者字节内部位序相反这些zsteg不一定能覆盖。我的经验是zsteg出结果直接收没出结果就手动上Stegsolve再不行就写脚本枚举组合。3. 如果图片不是LSB那“low”还能指向什么3.1 音频文件的低频隐写如果附件不是图片而是一个WAV或MP3那么“low”极有可能指低频段。这个方向我在做low题之前就专门练过因为MISC里音频隐写也是一个高频考点。用Audacity打开音频文件把视图从波形切换到频谱图。默认情况下频谱图显示的频率范围会比较大你需要手动把纵轴范围缩放到0到500Hz甚至更低。低频区域如果出现了一段波形图里看不到的图案、字符或者明显比周围更亮的谱线那就说明有东西藏在低频段。常见的低频隐写形式有几种一是SSTV慢扫描电视能把一张图片调制到音频里需要用RX-SSTV或MMSSTV解码二是DTMF双音多频每个数字对应两个频率的组合录下来再识别电话号码或坐标三是摩斯电码通过不同时长的音调来编码字母数字。判断方法很简单听到音频内容后如果感觉像是“滴答”声或者拨号音就往这三种方向考虑。关于文件大小音频比图片更容易判断异常。一个44.1kHz采样率、16bit量化、双声道的WAV文件每秒大小约176KB。一段30秒的音频应该在5MB左右。如果文件大小明显超出那么多出来的部分很可能就是嵌入数据。3.2 GIF或PNG的调色板隐写还有一种“low”容易被忽略就是颜色索引的最低位。索引色图片比如GIF、PNG-8存储的每个像素不是RGB值而是一个调色板索引号。每个索引号对应一个颜色颜色存放在文件的调色板区域。这种图片的LSB隐写和24位真彩图完全不同。你不能直接看RGB位平面因为像素值本身只是索引。比如两个像素的索引分别是5和6它们对应的实际颜色可能差别很大但最低位分别是1和0。如果隐藏数据时逐个修改索引值的最低位视觉上可能会出现颜色的轻微跳变但用Stegsolve的RGB位平面看是看不出规律的。正确的分析方法是把索引矩阵提取出来对每个索引值取最低位重新排列成黑白图。Python的Pillow库可以直接拿到调色板和像素索引from PIL import Image img Image.open(low.gif) palette img.getpalette() data img.tobytes()拿到data后对每个字节取b 1按图片的宽度重新排列成像素矩阵生成新的黑白图像。如果看到二维码就扫码如果是二进制数据就继续往下处理。这种题在MISC里属于偏冷门但偶尔会考的类型一旦遇到很多人会卡在为什么Stegsolve里什么都看不见。3.3 窄幅图片与像素排列陷阱“low”还有一种非常有趣的解释图片的高度被改低了。很多MISC题会把一张包含二维码或文字的图片的PNG高度改小比如原本是300x300被改成300x3。图片查看器打开后只能看到一条几像素高的色带肉眼完全看不出内容。判断方法很简单先用file看图片尺寸如果宽度和高度比例悬殊马上怀疑宽度或高度被人为改动。然后用十六进制编辑器打开PNG找到IHDR块。IHDR是PNG文件的第一个数据块里面第0到3字节是宽度第4到7字节是高度都是4字节大端整数。把高度改回合理值图片就能恢复。这里有个很有用的技巧PNG的IHDR块后面跟了4字节的CRC32校验值。如果你改了高度但CRC对不上很多图片查看器会提示文件损坏。此时不要盲目猜高度而是爆破。写一个循环从1到5000逐个尝试高度值每试一个就算一次CRC32和文件里的CRC比对一旦相等就是原始高度。import struct import zlib def fix_png_height(filepath): with open(filepath, rb) as f: data f.read() ihdr_start data.find(bIHDR) width, height struct.unpack(II, data[ihdr_start4:ihdr_start12]) old_crc struct.unpack(I, data[ihdr_start17:ihdr_start21])[0] for new_height in range(1, 5000): new_data data[:ihdr_start8] struct.pack(I, new_height) data[ihdr_start12:ihdr_start17] crc zlib.crc32(new_data[4:ihdr_start17]) 0xffffffff if crc old_crc: print(ffind height: {new_height}) break所以看见low不要只往LSB一个方向想。音频低频、调色板低位、被改低的宽高都是常见考点。前面这几节是排查思路接下来我写一下真正动手提取数据时脚本怎么写最顺手。4. 手写Python提取低位数据的完整过程4.1 一个能跑通的LSB提取脚本工具能解决大部分常规情况但遇到变种题还是得自己写脚本。我把我常用的提取脚本简化一下放在这里你们可以在此基础上改参数。from PIL import Image def extract_lsb(image_path, channelsrgb, reverse_bitsFalse, bit0): img Image.open(image_path).convert(RGB) pixels list(img.getdata()) bits [] channel_map {r: 0, g: 1, b: 2} for px in pixels: for ch in channels: bits.append((px[channel_map[ch]] bit) 1) if reverse_bits: for i in range(0, len(bits), 8): bits[i:i8] reversed(bits[i:i8]) data bytearray() for i in range(0, len(bits) - 7, 8): byte 0 for b in bits[i:i8]: byte (byte 1) | b data.append(byte) return bytes(data) if __name__ __main__: result extract_lsb(low.png, channelsbgr, reverse_bitsFalse) with open(extracted.bin, wb) as f: f.write(result)代码核心很简单遍历每个像素的指定通道取出最低位拼接成字节数组写到文件。参数里我特意留了channels和bit因为实际题目里通道顺序和位平面不一定都是常规的。很多朋友问为什么提取出来的数据是乱码绝大多数情况下不是脚本错了而是参数不对。常见方向有四个通道顺序、位序、扫描方向、取第几位。把这几个参数都做成可调一个个试过去效率比每次改脚本高得多。4.2 针对“low”题的特殊处理LSB not MSB、RGB顺序、位序首先说位序。一个字节由8个位组成最高位是bit7最低位是bit0。隐写时有两种写入方式一种是从bit7开始把第一个bit写到第一个像素的最低位第二个bit写到第二个像素的最低位这样提取时正序读取8个bit得到一个字节另一种是从bit0开始先读bit0再读bit1本质上是把8个bit整体反转。很多变种题会把位序倒过来提取结果就会是乱码。判断位序对不对最直接的方法是看文件头。比如提取出来应该是一个PNG图片如果结果以89 50 4E 47开头说明方向正确如果开头是E2 0A 38 81这种乱七八糟的值大部分情况就是位序反了。其次是通道顺序。常见的是RGB顺序即每个像素先取R通道最低位再G再B。有些题为了加大难度会改成BGR或者GRB。zsteg输出里会明确写出它用到的通道组合照着填进脚本的channels参数即可。第三是扫描方向。默认从左到右、从上到下逐行扫描但有些题会按列扫描也就是第一列从上到下读完再第二列。另一种是蛇形扫描奇数行从左到右偶数行从右到左。这些在脚本里都需要把像素遍历顺序改掉。遇到提取出来数据毫无规律且不是文件头时优先试列扫描和蛇形扫描。4.3 从提取结果到flag常见编码和文件头判断提取出的数据不要急着用文本编辑器打开先用命令行判断类型xxd extracted.bin | head看第一行字节。不同文件格式都有固定的魔数特征ZIP是50 4B 03 04PNG是89 50 4E 47JPEG是FF D8 FFGIF是47 49 46 38。如果看到这些直接把文件扩展名改对然后解压或打开。如果提取出来的是BASE64字符串通常以ZmxhZflag的base64开头或者一堆大小写字母加数字加结尾。这种情况需要解码base64 -d extracted.txt flag.txt如果提取出来是一串看起来像是被反转的字符串可以用rev命令倒序如果看起来像是十六进制可以用xxd -r -p转成二进制。这些技巧在low这类题里都很常见。还有一点要注意图片像素数量不一定是8的倍数所以提取出的数据末尾会有几个填充bit一般是0。如果文件头前面有多余的padding需要手动找到文件头的位置从这个位置开始截断。比如从第3个字节开始才是50 4B那就把前两个字节删掉再解压。这个过程用脚本搜索魔数很容易完成。我实际做low这道题的时候一开始用默认的RGB顺序提取出来的完全是乱码后来看了zsteg的输出发现它扫描出的组合是BGR立刻把脚本里的channels改成bgr重新提取就得到了一个PNG文件打开是一张二维码扫码后才拿到最终flag。这个过程的每一步都不复杂但顺序错一个就全错。5. 复盘与通解MISC高手的做题节奏5.1 从附件到flag的决策树经过low这道题我总结了一套多步骤做题流程现在几乎可以无脑复用。第一步永远是file、strings、binwalk三连检查文件类型、可见字符串和隐藏文件。第二步根据文件类型选择主攻方向图片就开Stegsolve翻位平面、跑zsteg音频就切频谱图看低频段压缩包就检查加密类型、伪加密和压缩包内文件列表。第三步如果常规工具没有结果就开始枚举变种通道顺序、位序、扫描方向、位平面选择每一项都写成脚本参数。第四步提取出数据后用xxd和file判断文件类型搜索魔数定位真实数据起点然后交给对应的解码工具。这套流程看起来很简单但真正执行到位需要大量练习。因为每一步之间是有跳转的比如strings看到一串密码接下来可能就要用这个密码去解压binwalk分离出来的zip而不是继续纠结图片隐写。做题节奏的本质是信息流管理你要根据当前信息不断调整下一步动作。5.2 我在这种题上踩过的坑踩过的坑太多了挑几个最典型的说说。第一个坑是Stegsolve翻位平面不够细致。最低位平面的隐写图案往往不是一眼就能看出来的它可能隐藏在看起来像噪点的细碎明暗中。如果没有把图片放大到100%或200%很容易看漏。正确做法是把最低位平面和其他位平面对比如果其它位平面是纯粹雪花噪点而最低位平面有块状结构才能判断有隐写。第二个坑是zsteg没跑出结果就放弃。zsteg默认不会枚举所有组合要加-a参数。而且zsteg对某些特定变种支持不好比如数据不是连续存储而是隔N个像素取一位。这种情况下zsteg扫不出来但手动写脚本反而能解开。第三个坑是二维码扫描不出来。提取出来的图片如果是二维码但用手机或微信扫不出来第一反应不要怀疑图片坏了。大概率是二维码被旋转了、翻转了或者背景反色了。把图片旋转180度、水平翻转、垂直翻转都试一遍或者用Photoshop反色再扫一次。low这道题里提取到的二维码非常浅我当时把对比度拉高之后才扫出来。第四个坑是PNG宽高爆破时忘了CRC。修改高度后如果图片打不开千万不要继续盲试。正确做法是用CRC校验来锁定正确高度。我前面给的脚本就是干这个的你们可以直接拿去用输入文件路径就能跑出正确高度。5.3 高手进阶区“low”到底考什么做完了low这道题我才琢磨出它为什么能进高手进阶区。它没有考九九乘法表式的复杂算法也没有考冷门工具而是把MISC最基本的几个点全部串起来了文件名提示线索、基础命令排查、位平面观察、脚本提取、文件头识别、二维码处理。每一环单拎出来都能写一篇教程但组合在一道题里对完整性和耐心度的要求就上来了。我觉得这道题的核心考点是“对隐写原理的理解程度”。如果你只背过“LSB就是把数据写在最低位”但不知道最低位是奇偶性、不知道通道顺序会影响提取结果、不知道提取后还有文件头判断这一步那遇到这道题还是会卡住。反过来如果你能把这套原理讲给一个完全没接触过的人听那做这道题就只是时间问题。最后分享一个我坚持了很久的习惯每次做MISC题都把命令、输出、尝试过的参数组合、卡住的环节记在Markdown笔记里。后来再遇到类似的题翻笔记比重新找题解快得多。low这道题让我把LSB隐写的所有变种都过了一遍笔记里就多了一张完整的“LSB参数枚举表”。下次你做到类似题目建议也这样做。现在的攻防世界题库里low这道题可能已经改版但底层思路不会变照着上面的流程走一遍你大概率能顺利拿到flag。