前几天在某个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字符.PNG); - JPEG图片头部通常是
FF D8 FF E0; - ZIP压缩包头部通常是
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 04 | PK\x03\x04 | 每个压缩条目开始处,记录文件名、压缩方式、CRC32等信息 |
| 中央目录 | 50 4B 01 02 | PK\x01\x02 | 压缩包的文件索引,记录所有条目的偏移和属性 |
| EOCD(中央目录结尾记录) | 50 4B 05 06 | PK\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(f"extracted {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 |
| 解压 | 修复后的ZIP | unzip | readme.txt、cyberpunk.png |
| LSB提取 | PNG图片 | StegSolve/自定义脚本按BGR提取 | extracted.bin |
| 编码解码 | extracted.bin | Base64解码 | 最终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 -d | Base64解码 | 处理提取出的编码数据 |
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里反复出现。希望这篇记录能帮你少走几步弯路,下次刷到类似题目时,能直接想起“先看头、再看尾、最后按提示调通道顺序”这条路径。