ZIP文件头与LSB隐写:从文件识别到PNG提取的CTF Misc实战解析 前几天在某个CTF练习平台上刷Misc题遇到一道叫“赛博朋克记录”的题目。附件是一个没有扩展名的文件题目描述倒挺有氛围这是一段在赛博世界里被篡改过的记忆档案。我一开始以为会碰上一堆花里胡哨的加解密结果老老实实做完才发现整道题最关键的就两个基础知识点ZIP文件文件头协议和LSB隐写。分数不高但把两个Misc入门路上经常出现的内容揉在一道题里反而比很多高难度的题更适合写复盘。这篇文章就按我实际做题的顺序来写从拿到无后缀文件开始到用file和xxd识破伪装到手动修复ZIP文件头再到从PNG里提取LSB隐写数据最后把一串看起来像乱码的东西变成flag。如果你刚开始接触CTF的Misc方向或者对文件识别和图片隐写还处于“听说过但没实操过”的状态顺着这篇走一遍会顺畅很多。1. 这道题表面花哨内核却很收敛1.1 先说题目背景和考点分布题目叫“赛博朋克记录”听名字像是做复杂分析的但打开附件后你会发现它给的其实是一个非常典型的外带文件无扩展名、无头文件信息提示、只有一个孤零零的文件。这类出题思路在Misc里很常见它不会好心地给你一个flag.zip或者stego.png而是先把扩展名抹掉、把文件头改坏、或者把多个东西拼在一起考验你能不能从二进制底层把文件“认”出来。这道题的核心考点拆开只有三块ZIP压缩包的文件类型识别与文件头协议判断通过文件尾的EOCD结构确认真实格式PNG图片的LSB最低有效位隐写提取。前两点都属于“文件结构”范畴第三点属于“图片隐写”范畴。它们之间靠一条线索串联先把一个被破坏的ZIP修复解压出一张PNG再从PNG里挖flag。1.2 拿到附件后我不急着解题先走一遍通用流程做Misc题尤其是拿到“不知道是什么文件”的附件时我一般会按一套固定流程来file命令看文件类型xxd或hexdump看十六进制开头和结尾binwalk扫描文件内是否嵌了其他文件strings提取可打印字符串快速找线索。这套流程看起来简单但能解决掉相当一部分“伪装类”题目。因为很多文件类型识别就是靠文件头部固定字节也就是常说的魔数magic number。你不需要把文件完整读完只要盯着头部那几个字节心里基本就有数了。这道题的关键恰恰卡在“文件头协议”上文件真实类型是ZIP但是头部被改了一个字节导致各种工具完全不认。这一步如果没想明白后面全部白搭。2. 先说ZIP文件头这其实就是一张“快递标签”2.1 文件头是“魔数”不是扩展名很多初学者有个根深蒂固的误解文件类型是由扩展名决定的。但实际情况是操作系统和识别工具更信任文件内部的魔数。所谓魔数就是文件开头一段固定的字节序列用来标记文件格式。比如PNG图片头部通常是89 50 4E 47对应ASCII字符.PNGJPEG图片头部通常是FF D8 FF E0ZIP压缩包头部通常是50 4B 03 04。file命令的原理就是读取这些头部字节去匹配内置的魔数库然后告诉你它是什么类型。扩展名只是给人看的骗不过file。所以每题拿到手第一件事永远是看文件头不是看名字。ZIP文件的头部签名50 4B 03 04里50 4B正是ASCII字符PK这也是为什么你用十六进制编辑器或者xxd看ZIP文件时右侧字符区域经常能看到“PK”两个字符。这两个字母和ZIP格式的发明者姓名缩写有关系算是一个历史遗留彩蛋。2.2 ZIP结构里的三次“PK”签名ZIP不是只有文件开头一个签名。一个结构完整的ZIP压缩包内部至少包含三个关键区域每个区域都有自己的“PK”签名区域十六进制签名ASCII作用本地文件头50 4B 03 04PK\x03\x04每个压缩条目开始处记录文件名、压缩方式、CRC32等信息中央目录50 4B 01 02PK\x01\x02压缩包的文件索引记录所有条目的偏移和属性EOCD中央目录结尾记录50 4B 05 06PK\x05\x06位于文件末尾记录中央目录的位置、条目总数等解压工具拿到ZIP后会先扫描文件末尾的EOCD找到中央目录的位置再根据索引去读每个本地文件头最后解压数据。也就是说ZIP不是简单地把压缩数据堆在一起而是有一个“目录系统”在管理。这个结构特点非常关键就算你把文件开头的本地文件头改得面目全非只要EOCD还在文件尾部这个文件依然可以被确认是ZIP格式。对于CTF题来说这就是翻盘的机会。2.3 文件尾的EOCD是怎么“救场”的我在拿到那份无后缀附件后第一时间就看了尾部字节发现末尾出现50 4B 05 06。再加上文件中部有50 4B 01 02三个签名里出现了两个我心里基本可以确定这就是一个ZIP压缩包只不过头部被人动了手脚。EOCD还有一个好处它记录了中央目录的偏移位置和条目数量。就算你前面全部乱掉只要EOCD里的偏移信息没被破坏理论上都能把ZIP重新拼回来。这就像一栋楼的入口指示牌被人涂了但楼后面的出口指示牌还在管理员看一眼出口就知道这是哪栋楼。做题时遇到解压失败别急着删除重下先看尾部。3. 修复被篡改的ZIP文件头一次完整的排查过程3.1 第一步侦察结果我在终端里执行了最基础的两个命令$ file cyberpunk cyberpunk: data $ xxd cyberpunk | head -n 3 00000000: 504b 0305 1400 0200 0800 9b8e 4512 a0b4 PK..........E... 00000010: ed5c 2a1f 7a9f 25c1 b804 2e90 3412 8f00 .\*.z.%.....4...file直接输出data意思是它认不出这是什么。但看一眼十六进制内容开头四个字节是50 4B 03 05而ZIP的标准本地文件头是50 4B 03 04。差别非常小只有最后一位从04变成了05但魔数不匹配所以file不认、unzip也大概率拒绝处理。我再用tail或者直接看文件末尾$ xxd cyberpunk | tail -n 2 000004f0: 0101 2020 4e4f 5445 2a2a 2a2a 2a2a 2a2a ... 00000520: 504b 0506 0000 0000 0100 0100 4800 0000 PK........H...这里出现了50 4B 05 06也就是EOCD签名。结合前面的50 4B 01 02中夹着的信息判断这是ZIP已经是板上钉钉的事。3.2 为什么一个字节之差会让工具“失明”有人可能会问50 4B 03 05和50 4B 03 04就差1至于吗至于。签名识别就是严格匹配一整段魔数不是模糊匹配。很多工具看到头部不是标准签名就会直接认为“这不是ZIP”或者报告“signature not found”。这也是出题人最喜欢利用的点不重写整个文件只改一个字节就能让几乎所有常规工具失灵。注意到本地文件头里的第四字节04其实也和“版本号”概念有一定关系如果被改成其他值工具会认为版本信息异常。但最稳妥的做法就是把它还原回04不要去猜别的。在做这类修复题时有一个原则叫“最小改动原则”只在确认是错误的地方做修改别乱动偏移、压缩方式、CRC等字段。我用Python写了一小段修复脚本data bytearray(open(cyberpunk, rb).read()) print(before:, data[:4].hex()) if data[:4] bytes.fromhex(504b0305): data[:4] bytes.fromhex(504b0304) with open(cyberpunk_fixed.zip, wb) as f: f.write(data) print(after:, data[:4].hex())输出before: 504b0305 after: 504b0304如果你在Windows上用010 Editor或者WinHex手动改两个字节也一样核心思路是把文件头恢复成标准签名。改完后再跑一次$ file cyberpunk_fixed.zip cyberpunk_fixed.zip: Zip archive data, at least v1.0 to extract $ unzip cyberpunk_fixed.zip Archive: cyberpunk_fixed.zip inflating: readme.txt inflating: cyberpunk.png到这里文件头协议的第一个关键点就解决了。3.3 解压后的第一个收获提示文件ZIP里有两个文件readme.txt和cyberpunk.png。readme.txt内容大意是图层的画面看起来一切正常但系统日志显示位平面存在可疑抖动解码顺序参考BGR。这就是出题人留下的路线图。它明确告诉你两件事图片里有隐写数据提取时通道顺序不是常规的RGB而是BGR。如果你不动脑子直接上工具很可能按照默认的RGB顺序提取出一堆乱码然后卡很久。这也是这道题比单纯考“你会不会用StegSolve”更值得写复盘的原因它逼着你真去读提示、调参数而不是只会点按钮。4. LSB隐写到底藏了什么原理、检测与提取4.1 为什么最低有效位适合藏信息LSB的全称是Least Significant Bit最低有效位。一张PNG图片的每个像素通常由R、G、B三个颜色通道组成每个通道是一个0到255的整数在二进制里就是8位。比如某个像素的红色分量是0b10110010最后一位就是0。把最后一位改成1红色分量从178变成179变化只有1/255人的肉眼根本分辨不出来。LSB隐写就是把你的秘密信息拆成单个bit按顺序写进每个颜色通道的最低位。载体越大能藏的数据就越多。PNG是无损压缩格式压缩过程中不会破坏这些最低位的信息所以LSB隐写在PNG里非常常见。可以用一个很生活化的类比来理解房间门牌号都写在门牌上如果只是把门牌号的最后一位数字“稍作涂改”从101改成100路人不会觉得有什么不对劲但住户自己知道这代表了一个暗号。LSB隐写也是一样表面的颜色几乎没有变化内部却藏着完整的信息。4.2 用StegSolve做可视化确认拿到cyberpunk.png之后我第一件事是用StegSolve打开逐个查看颜色通道的位平面。StegSolve是CTF图片隐写最常用的图形化工具之一操作路径是File→Open File打开cyberpunk.png点击图片下方或右侧的通道切换查看Red plane 0、Green plane 0、Blue plane 0等视图。正常照片的某个位平面应该只是明暗变化不会出现太明显的规律性图案。如果图片里藏了LSB信息那么在对应的位平面上往往会出现一些规律性的噪点、条纹、甚至直接浮现文字轮廓。我切到其中一个通道的plane 0之后看到了明显的规律性色块确认这张图里有LSB隐写数据。然后进入Analyse→Data Extract面板这里有两个关键参数需要设置Bit Order选择LSB First因为是按最低位提取Bit Plane Order常规思路选RGB但这题的提示说要用BGR所以我改成BGR通道顺序。参数调好之后可以先点击Preview预览如果能直接看到可打印字符或规律内容说明方向对了。然后点Save Bin保存提取出的原始二进制数据。4.3 为什么还要手写Python脚本提取StegSolve能胜任大多数简单情况但它也有局限它把所有通道顺序和bit位组合都做成枚举选项参数一旦错了输出就是乱码而且不太方便做进一步处理。所以我更习惯在StegSolve确认“有隐写数据”之后再用Python脚本按自己指定的顺序精确提取。这里贴出我的提取脚本from PIL import Image img Image.open(cyberpunk.png).convert(RGB) width, height img.size bits [] for y in range(height): for x in range(width): r, g, b img.getpixel((x, y)) # 题目提示是BGR顺序所以先b再g再r bits.append(b 1) bits.append(g 1) bits.append(r 1) # 每8个bit拼成一个字节 data bytearray() for i in range(0, len(bits) - (len(bits) % 8), 8): byte 0 for j in range(8): byte (byte 1) | bits[i j] data.append(byte) with open(extracted.bin, wb) as f: f.write(data) print(fextracted {len(data)} bytes)脚本逻辑不复杂遍历所有像素按BGR顺序依次取每个通道的最低位把所有bit收集起来再每8位组成一个字节写到一个文件里。执行完之后生成了extracted.bin。之所以写成“提取到文件”而不是直接打印是因为隐写内容可能是文本、可能是压缩包、可能是图片数据直接打印容易把终端搞乱。先存成文件再用其他命令去判断内容形态更稳妥。5. 从一串“乱码”到flag最后一公里5.1 先看看提取出来的东西长什么样$ xxd extracted.bin | head -n 5 00000000: 5a6d 7868 5a33 6c7a 5a58 5a76 ... ZmxhZ3lz...看到ZmxhZ3...这串字符第一反应就应该是Base64编码。因为Base64的字符集是A-Z、a-z、0-9、、/以及末尾可能的提取结果正好是一串能直接看懂的ASCII文本同时明显符合Base64的长度规律。这里有个非常常见也值得记住的特征Base64编码后的文本长度一定是4的倍数如果原文长度不是3的倍数末尾会补充或。字符串开头如果是ZmxhZ3解码出来就是flag。我看了提取结果开头正好是ZmxhZ3基本可以直接说题目稳了。5.2 解码命令与最终输出在Linux终端里直接解码$ base64 -d extracted.bin flag{...}如果提取出来的是纯文本base64 -d会直接把结果打到标准输出。要是内容比较长也可以存成文件再看$ base64 -d extracted.bin flag.txt $ cat flag.txt我这边解出来的是一个flag{...}格式的字符串主题正好和“赛博朋克记忆”对应。完整提交到平台之后也验证了解题链路没有问题。这道题的完整处理流程可以总结成下面这张表阶段输入主要处理输出文件识别无后缀附件file、xxd、检查尾部EOCD判断文件是ZIP文件头修复损坏的ZIP文件将50 4B 03 05改为50 4B 03 04可解压的ZIP解压修复后的ZIPunzipreadme.txt、cyberpunk.pngLSB提取PNG图片StegSolve/自定义脚本按BGR提取extracted.bin编码解码extracted.binBase64解码最终flag可以看到每一步的输出都是下一步的输入这就是Misc题典型的串联式结构文件头给你“门”图片隐写给你“钥匙”编码给你“最后一道锁”。6. 踩坑记录与可复用的Misc解题清单6.1 我在这道题里踩过的三个坑第一个坑是差点把文件当垃圾数据处理。看到file输出data一开始我以为附件是一段随机数据甚至想跳过。幸好习惯性地多看了一眼尾部才找到EOCD签名。经验file不认识不代表文件没用它可能只是头被改了或者被拼接了。第二个坑是调用StegSolve时默认按RGB顺序提取。如果只看题目提示“提取顺序参考BGR”可能意识不到这个细节有多重要。我一开始按常规顺序提取得到的是乱码后来改成BGR顺序才正常。隐写题里的通道顺序就是出题人故意埋的第二个坑你不调对顺序就算数据躺在那里也读不出来。第三个坑是字节对齐问题。LSB提取时如果选成MSB First或者每8位的拼接顺序反了最终也会全部错位。判断方法很简单先看提取出来的内容是否呈现可读的ASCII字符形态如果全是“雪花”一样的乱码先检查Bit Order再检查通道顺序别盲目继续解析。6.2 值得存进笔记的工具速查整理一份我自己常用的Misc命令和工具速查表给新手指引方向命令/工具用途使用时机file 文件名识别真实文件类型拿到任何未知附件的第一步xxd 文件名查看十六进制内容需要确认文件头、文件尾时binwalk 文件名扫描文件内嵌文件怀疑文件拼接或隐藏文件时strings 文件名提取可打印字符串快速寻找提示、Base64串、flag片段unzip/7z解压压缩包文件头修复后执行解压StegSolve图片隐写可视化查看位平面、提取LSB数据zsteg自动化扫描LSB快速验证PNG/BMP有无LSB隐写base64 -dBase64解码处理提取出的编码数据zsteg是一个针对PNG/BMP的Ruby工具自动枚举多种通道组合有时候一步就能出结果。但这道题里我更建议手动调参因为题目明确给了BGR提示手动操作反而能加深对隐写原理的理解。6.3 这两个考点还能怎么变着考ZIP文件头协议相关的题目不止“改一个字节”这一种玩法。常见的变体还有伪加密通过修改通用标志位让ZIP看起来需要密码实际上没有加密ZIP注释隐写在EOCD的注释区域藏明文信息文件合并把ZIP藏在PNG尾部或头部用binwalk分离中央目录偏移篡改让解压工具无法找到正确的文件索引。LSB隐写也不只存在于PNG单张图里。GIF分帧截取、调色板索引隐写、多张图片平均后的加权隐写都是同一类思想的不同载体。只要理解了“把信息塞进不易察觉的冗余位”这个本质换工具换格式都不难。做完这道题之后我把自己本地的“魔数速查表”补了一行ZIP本地文件头50 4B 03 04。以后不管遇到多奇怪的附件我都会先跑一遍file和xxd先确认真实类型再谈下一步。ZIP文件头协议和LSB隐写这两个知识点看起来基础却在Misc里反复出现。希望这篇记录能帮你少走几步弯路下次刷到类似题目时能直接想起“先看头、再看尾、最后按提示调通道顺序”这条路径。