CTF Misc隐写三连:base64隐写、音频频谱与图片LSB实操
BUUCTF上这套Misc三连——base64隐写、我有一只马里奥、黑客帝国我做完之后最大的感受是三道题没有一道是靠“硬猜”的每一步都能从文件本身、文件名或者格式特性上找到线索。base64隐写考的是编码原理里最容易被忽略的“丢弃位”马里奥考的是频谱图里画音符这种音频隐写经典玩法黑客帝国则是图片隐写里分离、位平面、通道翻转三板斧的综合运用。这篇就把三道题的完整解题过程、用到的脚本和参数、以及我踩过的坑全部写出来适合正在刷Misc入门题、想系统过一遍隐写分析基础操作的人参考。1. 开搞之前Misc隐写分析必备的环境与工具清单1.1 为什么这三道题放一起说很多人在入门Misc的时候第一反应就是“隐写嘛就是拿工具扫一遍”。但实际刷题下来你会发现真正的分水岭不在于会不会点工具按钮而在于能不能根据题目特征判断“该上什么工具”。base64隐写靠的是脚本和编码原理马里奥靠的是音频频谱分析黑客帝国靠的是图片通道提取三道题恰好覆盖了CTF Misc里最主流的三个方向文本编码隐写、音频隐写、图像隐写。把这三道题放在一起做一遍相当于把隐写分析的底层框架过了一遍。以后再遇到类似题目先看文件类型再看文件名暗示然后按“文本-音频-图像”的路径去排查基本不会乱。1.2 工具清单与各自定位先说结论这套题不需要重型商业工具全是开源或者免费的东西。我实际用的工具如下Python 3.x必装主要用base64标准库和PILPillow处理图片像素。Audacity免费音频编辑器用于查看频谱图。一个隐藏的知识点是Audacity的“频谱图”视图本身就是隐写分析的标配。StegsolveJava写的图片分析小工具用于逐通道、逐位平面查看图片。老牌但实用很多LSB隐写题靠它一眼就能看出来。binwalk固件和文件分离工具用于检测图片里是否嵌入了其他文件。010 Editor / HxD十六进制查看器用于检查文件头、尾部数据、隐藏文本。注意Stegsolve需要Java环境遇到双击打不开的情况先查Java版本实测JDK 8和JDK 11都能正常运行JDK 17在某些系统上反而会报错。环境准备这块我踩过一个坑一开始我以为binwalk在Windows下用起来麻烦就直接在Linux虚拟机里做结果来回传文件浪费时间。后来发现Windows下装一个WSL或者直接用Git Bash里跑binwalk的Python版本一样能出结果。工具链越简单越好别把时间浪费在配环境上。2. base64隐写考的不只是解码而是编码原理本身2.1 base64编码原理中那个“被丢弃”的角落base64隐写题光会解码是不够的你得理解base64编码的过程。base64的规则是将每3个字节24 bit拆成4组每组6 bit再按照一张由64个可打印字符组成的索引表进行映射。如果待编码数据的字节数不是3的倍数编码结果末尾会出现“”作为填充符。这里的关键点在于当最后一组不足3个字节时实际参与编码的有效bit可能不足24位。举个例子如果原文只有1个字节8 bitbase64编码时会把8 bit凑成12 bit分成两组6 bit然后末尾补两个“”。在这种场景下最后一个“”之前的那一组6 bit里只有前2 bit是有效数据后4 bit是填充的0解码端在还原时会直接丢弃它们。这个“会被丢弃的bit”就是隐写空间。攻击者可以在这些位里写入任意值不影响解码出来的原始数据。每出现一个“”就多出2 bit的隐写空间出现两个“”就有4 bit。把这些bit按顺序提取出来再拼接成字节就能还原隐藏信息。这个原理很多文章提过但没有人说清楚“为什么解码不受影响”。我用自己的话解释一下编码/解码的对应关系解码时程序会先把有效字符转成6 bit索引值去除填充的“”再用“”的数量乘以2把最后一组的尾部bit直接截断。也就是说只要被截断的bit位里写什么都不会影响前面有效bit还原出的字节。隐写者正是利用了这个“不影响结果”的尾部空间。2.2 识别与提取flag是怎么藏在bit流里的拿到一个base64隐写题第一件事不是写脚本而是观察。题目给出的文件通常是一堆base64字符串比如每行一个。先把所有行单独解码一遍看看能不能得到完整可读的内容。如果解码结果看起来是正常的文本、图片或者压缩包但末尾又有大量“”这时候就要怀疑采用了base64隐写。更直接的特征是某些行的“”数量不一致例如有的行一个“”有的行两个“”有的行根本没有——这种差异本身就是一种信号。提取思路是把每行解码后末尾被丢弃的bit全部收集起来。每行能提取的bit数由“”的个数决定一个“”提取2 bit两个“”提取4 bit没有“”则提取0 bit。按行顺序把这些bit拼接成完整的二进制串每8 bit组成一个ASCII字符最后得到的就是隐藏的flag。下面的脚本是实际可用的提取逻辑逐行读取base64字符串计算填充符产生的隐写位再拼接还原import base64 def extract_hidden_bits(b64_str): padding b64_str.count() if padding 0: return # 解码得到原始字节 decoded base64.b64decode(b64_str) # 将解码结果转成二进制字符串 bin_str .join(format(b, 08b) for b in decoded) # 有效bit数 原始比特数 - padding * 2 valid_bits len(decoded) * 8 - padding * 2 # 隐写空间就是有效bit之后的那些bit hidden_bits bin_str[valid_bits:] return hidden_bits lines open(base64.txt, r).read().strip().splitlines() bit_stream for line in lines: bit_stream extract_hidden_bits(line.strip()) # 按8bit一组还原字符 flag_chars [] for i in range(0, len(bit_stream) - 7, 8): byte bit_stream[i:i8] flag_chars.append(chr(int(byte, 2))) print(.join(flag_chars))注意脚本不能直接对所有行强行提取bit就完事一定要先确认原始数据本身是正常的文件内容。有些题目为了隐藏信息会故意把原来的数据弄成乱码这时候提取出来的隐藏信息可能不再按行对应需要把所有bit流重新按8 bit切割。我试过的另一个做法是先用常规手段把所有行解码拼成一个文件看看文件的类型。如果拼出来是一个PNG或者压缩包那么隐藏的bit流往往就在文件末尾。还有更隐蔽的玩法隐写bit不是放在每行末尾而是放在某些特定行的尾部这种情况就需要特征匹配了。2.3 变种表与干扰项一道题的多种变形上面说的是经典base64隐写。但当题目加了“变异”之后情况会复杂一些。实际遇到过的变体有这几种换表把标准索引表替换成自定义字符表比如一些CTF平台会调整大小写顺序。判断方法是标准base64解码出来是乱码但你用肉眼比对发现字符集仍然接近base64的64个字符只是顺序不同。多重base64先base64编码再编码每一层的输出长度和““数量都会变化。遇到这种情况时就去掉一层解码重新观察。不规则的换行题目文件的行长度不一定一致有些行故意插入干扰信息。处理方法是按固定长度截取或者用正则匹配base64字符串。这类变体没有万能脚本重点在于先理解编码原理中的“丢弃位”在哪再根据实际变化调整提取逻辑。这套题里的base64隐写恰好是标准玩法所以脚本一次就过但我还是建议把变体情况都掌握因为同一考点在不同比赛中换个包装是常事。3. 我有一只马里奥音频隐写的经典玩法——频谱图里画音符3.1 为什么是音频从题干名称想到频谱分析“我有一只马里奥”这个题名第一眼看过去有点像童谣《我有一只小羊羔》但结合Misc的语境马里奥指的就是游戏《超级马里奥》的主题曲。这道题给的是一个音频文件刚打开时长很短正常播放只能听到一段类似哼唱的旋律但信息密度明显不够藏一个完整flag。问题的突破口在“马里奥”三个字上。马里奥主题曲的旋律是固定的一连串音符如果把这些音符画出来就是既有规律又有辨识度的频谱图案。CTF音频隐写里有一类非常经典的题目用频谱图把一段旋律或一幅图藏进音频里。解题方式也固定打开音频的频谱视图看有没有非自然的图形轮廓。实际操作中用Audacity打开音频点击视图切换为频谱图或者直接按快捷键CtrlM切换。如果频谱图上出现了一排排整齐的横条纹那就是音符的痕迹。每个横条纹的高度对应一个音高频率从左到右就是旋律的走向。3.2 读谱与转换从频谱图到可播放的MIDI把频谱图上的音符转成可读的旋律这是马里奥题的核心步骤。具体操作如下第一步在Audacity的频谱视图下把显示窗口拉大确保每个音符横条都能看清。很多音频隐写的音符横条之间距离很近不放大容易看错。第二步记录每个横条出现的时间和对应的频率。Audacity的频谱图底部有频率刻度鼠标移到横条中间就能读出中心频率。把这些频率按时间顺序记下来。第三步将频率转换成音乐音高。以A4440Hz为标准通过公式 n 69 12 * log2(f/440) 就能算出对应的MIDI音高序号。但实际操作中没人会自己算直接对照音名频率表更快C4、D4、E4这些音名对应的频率都是公开数据。第四步把读出的音高序列转成简谱或者MIDI文件。这里有个小技巧直接用手写MIDI文件不稳定我习惯把音符序列转成简谱数字再用在线的MIDI转换工具生成可播放的音频。转换完播放一遍如果能听出马里奥主题曲的调子说明之前读谱基本读对了。这套流程听着繁琐实际熟练之后十分钟内能走完。关键是抓准中心频率不要把相邻半音搞混。马里奥主题曲的音程跳跃比较明显C到G这种大跳很多一旦读错一个音后面的旋律走向很容易错位。3.3 实操心得与坑几个容易被忽视的细节这道题实际做的时候有几个坑特别容易踩第一音频的采样率会影响频谱图的频率轴范围。如果文件采样率是8000 Hz最高频率只有4000 Hz马里奥主题曲的高频部分可能显示不全。遇到这种情况先看音频属性确定采样率再判断频谱图显示范围是否完整。第二有些版本的马里奥音频里不止一段旋律会把flag信息藏在第二层频谱或者倒放的音频里。建议先看整段音频的波形如果有明显的空白段或者“咝咝”声就要检查是不是有隐蔽的第二轨。我用Audacity导出了分轨数据发现有些题的flag藏在最后几秒的噪声里单听根本听不出来。第三转MIDI的时候容易忽略时值。频谱图只显示音符的高度不直接显示时值长短而时值恰恰决定了旋律的节奏。马里奥主题曲的节奏是固定节拍建议先把整段音频的节拍标出来再对照每个横条的长度判断它属于八分音符、四分音符还是附点音符。注意读频谱图时最好把视图色彩方案调成灰度。Audacity默认的彩色频谱在某些显示器上对比度不高灰阶模式更容易看清音符边界。我实际做这道题的时候第一次读谱把第四和第五个音的顺序搞反了生成的MIDI听起来完全不是马里奥。后来靠对比原曲旋律才纠正过来。经验就是读谱之后别急着提交先自己听一遍生成结果确认没问题再往下走。4. 黑客帝国图片隐写三板斧——分离、位平面、通道翻转4.1 binwalk先扫一遍有没有藏文件黑客帝国这道题给了一张PNG图片画面风格是绿黑色的矩阵字符雨信息密度很高第一眼根本看不出哪里有异常。图片隐写的第一步永远是文件分离和文件头检查。先用file命令确认文件类型然后用binwalk扫描一遍看图片尾部有没有额外嵌入的内容binwalk -e matrix.png如果binwalk分离出一个压缩包或者文本文件那就直接解压看内容。实测很多“黑客帝国”风格的图片题都采用了这种套路PNG外壳只是个容器真正的flag被藏在尾部的zip文件里。但如果分离结果为空或者分离出来的文件是无效的就要进入第二步检查图片本身的像素信息。4.2 LSB提取用Stegsolve查看位平面PNG图片里最常见的隐写方式就是LSB最低有效位隐写。每个像素由RGB三个通道组成每个通道值是一个0到255的整数转成二进制后最低位的改变对视觉效果影响极小。隐写者可以把flag编码进所有像素的最低位人眼几乎无法察觉。破解LSB的标准工具是Stegsolve。打开图片后通过“Bit Planes”功能逐通道、逐位平面查看图像。实际操作里优先看RGB三个通道的0位平面最低位如果发现原本看似噪点的像素点呈现出规律性排列——比如形成文字或者二维码轮廓——那就是LSB隐写的痕迹。当然如果位平面上没有明显图形也可能采用了其他通道组合。我一般会把8个位平面都过一遍顺便看一下Alpha通道。PNG的Alpha通道偶尔也会藏东西新手很容易忽略。对于这道“黑客帝国”的题打开LSB平面后通常能看到模糊的二维码图案。但要注意这时候的图案对比度往往很低黑是灰黑、白是灰白直接拿扫码工具识别大概率失败。需要先做一步图像增强。4.3 二维码修复的关键操作补全、二值化、定位角从LSB位平面提取出来的二维码通常存在三个问题对比度不足、图案翻转、定位角缺失。逐个说解法。对比度不足的处理方法是二值化。可以用PIL直接把灰度图按阈值分成纯黑纯白from PIL import Image img Image.open(qr_raw.png).convert(L) threshold 128 bin_img img.point(lambda p: 255 if p threshold else 0, 1) bin_img.save(qr_bin.png)阈值不是固定的建议先用Stegsolve看直方图确定黑白两个峰的中间值再设阈值。有几次我直接用默认阈值处理结果二维码边框糊成一团导致扫码失败。图案翻转的问题也常见。有的LSB提取是黑底白图有的相反。二维码的定位角特征是左上、右上、左下三个角的“回”字形方框如果这三个角的边框颜色和周围相反那就是翻转了用PIL的ImageOps.invert翻回来就行。定位角缺失的修复稍微麻烦一点。二维码能不能被识别很大程度上依赖三个定位角。如果提取出的图案定位角破损根本扫不出来。修复思路是手动补画按照QR码的规范定位角是一个7x7的黑色方块外框、内部是3x3的黑色实心块四周还要有一圈浅色边界。用PIL在已有图像上绘制补齐from PIL import Image, ImageDraw img Image.open(qr_broken.png) draw ImageDraw.Draw(img) # 手动填充左上角定位角坐标按实际情况调整 draw.rectangle([0, 0, 7, 7], fill0) draw.rectangle([2, 2, 4, 4], fill0) # 补齐另外两个角 img.save(qr_fixed.png)关于二维码容错率还有一个隐藏知识点QR码有L、M、Q、H四级纠错级别越高允许缺失/污损的面积就越大。如果提取出来的二维码只缺一个小角且扫描失败可能是容错级别太低比如L级这时候可以尝试手动补全更多区域或者换用识别更强的库比如pyzbar。注意二维码的方向也很关键。如果提取出来后扫码一直失败尝试旋转90度、180度、270度以及水平/垂直翻转。很多参与者在这里卡了很久结果只是方向不对。“黑客帝国”这道题最终的flag往往藏在修复后的二维码里。整体思路就是分离无果→查位平面→提取图案→修复扫码。这个流程对绝大多数PNG型的LSB题都适用。5. 通用排查思路把三道题串成一条方法论这三次做题的体验让我整理出了一套通用排查顺序现在遇到任何Misc题都按这个顺序来基本不会漏掉关键信息排查层级操作适用场景文件识别file查看类型hexdump检查文件头确认容器是否真实有效字符串扫描strings提取可打印字符快速定位台面上的文字信息文件分离binwalk -e或者手动按文件头切割检查是否有附加文件元数据查看Exif、注释字段、文件描述有些flag直接写在备注里基础编码base64、hex、ROT13依次尝试文本类隐写的常见套路图片通道Stegsolve逐位平面查看LSB、通道隐藏图音频频谱Audacity频谱视图频谱图、倒放、批量符号深度分析结合题目名称和提示词推理马里奥这种谐音梗提示这套顺序最核心的原则是“由浅入深、由通用到特殊”。字符串扫描看一遍只要几秒钟值得做直接上Stegsolve慢慢翻位平面反而效率低。先排除简单解法再上重型手段。个人建议做题的时候把每一步的结果都截图保存。不是形式主义而是因为很多图片题在多次操作之后原图已经被修改过。如果中间某一步做错了至少还能回到上一步的状态重新来。我见过不少人在binwalk -e之后直接把原图覆盖了后面想重新提取位平面就只能重新下载题目浪费时间。关于base64隐写这道题多说一句常规解码结果如果是一张图片别忘了把图片保存下来看看因为图片本身可能还是一层隐写容器。这套题的base64隐写是直接藏在bit流里的所以没有这层嵌套但其他平台上的类似题目经常把base64解码后的内容再做一次图片隐写。多层嵌套是Misc的常态。6. 踩过的坑和一点个人心得三个题目做下来真正卡住我的不是技术难点而是几个很“低级”的细节问题写出来给后来的人避避坑。第一个坑是base64隐写脚本里没有处理非ASCII字符。提取出来的bit流如果包含了除flag{}之外的可见字符直接chr()转出来的结果可能包含不可见控制字符。后来我在脚本里加了过滤逻辑只保留可打印字符才看清楚真正的flag。第二个坑是马里奥音频的频谱图在Audacity默认窗口下只显示前几分钟而题目的音频文件虽然短但音符密集默认视图下很多横条叠在一起。放大波形宽度之后才分辨出每个音符。第三个坑是黑客帝国的二维码用手机扫码软件扫不出来一度以为修复方向错了。后来发现手机扫码依赖摄像头对准时间对高密度二维码的容错能力远不如专用解码库。换上电脑端的pyzbar识别之后就顺利通过。工具选择本身也影响结果很多时候不是你的解法错了而是工具不给力。最后说点个人体会Misc的门槛不在于会不会用某个工具而在于能不能建立“看到现象联想到原理”的反射。看到大量““想到base64填充位看到音频文件想到频谱图看到高信息密度图片想到位平面。这三道题恰好把这三个反应练了一遍做完之后最大的收获不是三个flag而是这套一眼定位考点的能力。这套思路放到其他Misc题上一样通用。