CTF 的 Misc 方向里压缩包类题目几乎是绕不过去的一关而 BUUCTF 上那道名叫zip的题我带过的几届新人里十有八九都是拿它当 Misc 入门第一课的。原因不复杂它不难但它把 zip 压缩包能被做手脚的地方几乎塞了个遍——套娃式层层嵌套、伪加密、弱口令、有时还得上已知明文攻击。很多人第一次做这题习惯性地双击、右键、输入密码、再双击解到第六七层就崩溃了。正确的姿势是先把它当成一个需要逆向的数据结构来看先用file判真实类型再用十六进制工具看内部字段最后才决定拿哪把刀去切。这篇东西写给两类人一类是刚接触 CTF、看到压缩包就只会右键解压的新手另一类是工作里偶尔要处理一堆来路不明的 zip、epub、各类封装包想搞清楚为什么解压会报错为什么提示要密码但文件又很小的人。核心关键词就俩BUUCTF、zip。我会从 zip 的字节结构讲起把套娃、伪加密、密码恢复、CRC32 碰撞这几条主线拆开揉碎再给一份可以直接抄的实操脚本和一套速查表。看完你至少能做到三件事知道一个压缩包到底被动了什么手脚、知道每种手脚用什么工具最快、知道哪些坑是新手最容易掉进去的。1. 拿到压缩包先别急着解压看懂 zip 的骨架再动手1.1 zip 的三层结构决定了出题人能玩的全部花样zip 这个格式看着简单其实内部是三件套结构每一个被打包的文件叫一个条目entry每个条目都有自己的**本地文件头Local File HeaderLFH加文件数据所有条目汇总在文件尾部的中央目录Central Directory HeaderCDH里最后用一个中央目录结束记录End of Central DirectoryEOCD**收尾。这三个部分的签名是固定的认准这三个就能在二进制里定位一切结构签名hex看得见的字符关键偏移本地文件头 LFH50 4B 03 04PK..flag 在 6中央目录 CDH50 4B 01 02PK..flag 在 8结束记录 EOCD50 4B 05 06PK..记录中央目录大小和偏移真正要盯死的是那 2 字节的通用位标记General Purpose Bit Flag。它的最低位bit 0表示这个条目是否加密置 0 就是明文存储置 1 就是加密存储常见值就是00 00、01 00、09 00这几种。除了标志位本地文件头里还有压缩方法00表示 store 不压缩08表示 deflate 压缩、CRC-32 校验值、压缩前后大小这几个字段。为什么要背这些偏移因为出题人所有的手脚都改在这几个字节上改标志位就是伪加密改压缩方法能让普通解压工具直接报不支持的压缩方式改大小字段能让工具以为数据被截断。你要是只会右键解压这些改动你一个都看不出来只能靠感觉和反复试错。而一旦你打开了十六进制编辑器对着这几个偏移一看基本就是一目了然的事。提示不要一上来就用解压软件去猜密码。伪加密的包你猜一晚上也猜不出来因为它根本没有密码。先 dump 结构再决定策略能省掉大量无用功。1.2 这道题的整体设计思路为什么是套娃 伪加密 爆破的组合拳把这道zip题的套路抽象出来它其实是一条非常典型的递进式考察链。第一层考识别能力解压出来的文件可能没有后缀名甚至不是一个正经的 zip你得用file去问系统你到底是谁。第二层考自动化能力一层一层套下去手点十几次谁都烦但你要是会写个循环脚本十层二十层都是秒过。第三层考格式理解某一层突然提示要密码这时候你得判断它是真加密还是伪加密——判断错了后面的时间全白费。第四层考工具链和密码学常识真加密的情况下是查弱口令字典、还是做已知明文攻击、还是上 CRC32 碰撞选错工具同样寸步难行。出题人为什么偏爱这种设计因为 Misc 题的核心从来不是难而是信息收集 格式理解 工具选择这三件事的综合运用。用一个压缩包把这三件事串起来题面成本极低就一个文件但覆盖面极广。而且它有个天然优势每个考点之间是解耦的你卡在哪一层就说明你哪一块知识有缺口反馈非常明确。这也是我推荐新手从这类题入手的原因——它不会让你陷入某个深不见底的算法里但会强迫你把基础工具摸熟。需要说明的是不同版本、不同出题人的 zip 题在这几个考点上各有侧重有的就一层伪加密有的套娃套到十几层有的最后一步是明文攻击。下面我按最全的情况来写你实际遇到时按需取用就行。2. 逐层拆解套娃解压、伪加密修复、密码恢复三大考点2.1 套娃解压与其手点十次不如写个循环脚本套娃题的第一个坎不是解压本身而是识别。解出来的文件经常没有扩展名或者扩展名根本是骗人的——叫1.png的可能是 zip叫flag.txt的可能是 gzip。这时候file命令就是你的眼睛file -b zip.zip # 输出类似Zip archive data, at least v2.0 to extract-b是只看类型不看文件名输出干净好解析。如果文件内部还嵌了别的文件可以再用binwalk扫一遍它会把扫描到的每一段结构的偏移和类型都列出来。不过对于纯套娃题file基本够用了。更稳的做法是直接读魔数magic bytes不看后缀、不看file的输出格式因为不同系统file的措辞会有差异。常见的魔数就那么几个格式魔数hexASCII 表现zip50 4B 03 04PK..gzip1F 8B不可见bzip242 5A 68BZh7z37 7A BC AF 27 1C7z..rar52 61 72 21Rar!xzFD 37 7A 58 5A 00不可见有了这套判断标准写循环就非常顺了。下面这个 Python 脚本是我自己常用的版本逻辑是识别当前文件类型 → 是压缩包就解到一个新目录 → 在解出的文件里找下一个候选 → 重复直到遇到不认识的类型或者达到层数上限import os, zipfile, tarfile, gzip, shutil SIGNS [ (b\x50\x4b\x03\x04, zip), (b\x1f\x8b, gz), (b\x42\x5a\x68, bz2), (b\x37\x7a\xbc\xaf\x27\x1c, 7z), (b\x52\x61\x72\x21, rar), ] def sniff(path): with open(path, rb) as f: head f.read(8) for sig, name in SIGNS: if head.startswith(sig): return name return unknown def extract(path, kind, outdir): os.makedirs(outdir, exist_okTrue) if kind zip: with zipfile.ZipFile(path) as z: z.extractall(outdir) elif kind gz: with gzip.open(path, rb) as fin, \ open(os.path.join(outdir, out), wb) as fout: shutil.copyfileobj(fin, fout) elif kind bz2: import bz2 with bz2.open(path, rb) as fin, \ open(os.path.join(outdir, out), wb) as fout: shutil.copyfileobj(fin, fout) else: raise ValueError(暂不支持的类型 kind) cur zip.zip for level in range(1, 31): # 最多套 30 层防死循环 kind sniff(cur) print(f[{level}] {cur} - {kind}) if kind unknown: print(停在第 %d 层剩余文件 % level, cur) break out flayer_{level} extract(cur, kind, out) files [os.path.join(out, f) for f in os.listdir(out)] files [f for f in files if os.path.isfile(f)] if not files: print(解压后没有文件检查是否被加密) break cur files[0]这个脚本里有两个设计细节值得说一下。一是层数上限我设了 30 层这是防解压炸弹的最后一道保险——有些恶意的压缩包会无限嵌套或者解压后体积暴涨经典的 42.zip 能解出 PB 级别的数据脚本一旦跑飞会把磁盘写满。二是优先取第一个文件因为套娃题的每层通常只有一个文件如果你遇到一层里解出好几个文件就得改成人工判断哪个是下一层别盲目取第一个很可能取到的是干扰项。注意递归解压时一定要指定独立的输出目录。如果所有层都解到同一个目录里同名文件会互相覆盖你可能在半路就丢失了真正的下一层。2.2 zip 伪加密本质上只是被人改了一个标志位伪加密是这类题里最坑人的设计因为它的表象和真加密一模一样——你双击、解压软件弹窗问你要密码。但实际上包里的数据从头到尾都是明文出题人只是改了通用位标记里的加密位骗过了大多数解压软件的判断逻辑。原理说透就一句话zip 判断一个条目是否加密看的是通用位标记的 bit 0。真加密的包本地文件头和中央目录里两处的标志位都是 1写成01 00或09 00而伪加密出题人往往只改其中一处通常是中央目录那处因为很多工具在解压前会先读中央目录于是软件认为要密码可实际数据又没加密你就卡在这个矛盾里了。修复思路也很直接把两处的标志位都统一改成00 00。具体操作分两条路。第一条路是十六进制工具手工改。用 010 Editor、HxD、WinHex 之类的工具打开压缩包搜索50 4B 03 04定位本地文件头跳到偏移 6 看那 2 个字节再搜50 4B 01 02定位中央目录跳到偏移 8 看那 2 个字节。凡是01 00、09 00这种带加密标志的值全部改成00 00保存即可。用 010 Editor 的话它自带 zip 结构模板加载模板后字段名直接标出来点着改比数偏移舒服得多。第二条路是写脚本批量清标志位一层里有很多条目的时候特别省事import sys def fix_pseudo_zip(path, outNone): data bytearray(open(path, rb).read()) i, n 0, len(data) while i n - 4: sig bytes(data[i:i 4]) if sig bPK\x03\x04: # 本地文件头flag 在 6 data[i 6] 0 data[i 7] 0 i 4 continue if sig bPK\x01\x02: # 中央目录flag 在 8 data[i 8] 0 data[i 9] 0 i 4 continue i 1 out out or path.rsplit(., 1)[0] _fixed.zip open(out, wb).write(bytes(data)) print(已修复并保存, out) if __name__ __main__: fix_pseudo_zip(sys.argv[1])注意这个脚本是无脑清零它会把真加密包里的标志也一起抹掉。抹掉之后真加密的数据是解不出来的因为加密数据没法当明文用。所以用它之前你最好先确认这个包大概率是伪加密——判断方法很简单一个提示要密码的包如果里面的文件都非常小几字节到几十字节那基本就是伪加密或者 CRC 碰撞题真加密的包很少做得这么小。顺便提一个新手常见的误区伪加密的包用不同的解压工具表现会不一样。有的工具尤其是某些命令行工具根本不管你标志位怎么设照着数据往下解反而自动修复了有的工具又特别严格一点不对就罢工。所以遇到解压报错时换一个工具试试也是排查手段之一但别把换个工具能解出来当成常态该修的标志位还是要修。2.3 密码恢复从弱口令字典到已知明文攻击如果确认是真加密那就进入密码恢复环节。这里的关键是分场景选工具千万别一上来就打开某个 GUI 破解软件开始暴力跑。场景一怀疑是弱口令或纯数字。题目描述里但凡出现密码是几位数字和名字有关这类暗示优先走字典或规则爆破。命令行下fcrackzip最方便# 字典攻击-u 表示用 unzip 实际验证准确但要慢一点 fcrackzip -D -p rockyou.txt -u locked.zip # 纯数字暴力1 位到 6 位 fcrackzip -b -c 1 -l 1-6 -u locked.zip这里-c后面跟的是字符集1表示纯数字a表示小写字母A表示大写!表示特殊符号可以组合。需要提醒的是fcrackzip只支持传统的 ZipCrypto 加密如果你遇到的包是用 WinZip AES-256 或者 7z 新格式加密的它一点办法都没有会直接告诉你识别不了。场景二字典够大想榨干性能。这时候走john或hashcat的路子先提取哈希再离线跑zip2john locked.zip zip.hash john zip.hash --wordlistrockyou.txt john --show zip.hashzip2john会把压缩包里每个条目的校验信息抽成一段 John 能识别的哈希行。转到 hashcat 的话ZipCrypto 对应的模式是17200系列WinZip AES 是13600系列具体编号建议用hashcat --example-hashes | grep -i zip现查一次因为模式号会随版本调整。GPU 跑哈希比 CPU 快几个数量级字典又比较大的话这一步的差距非常明显。场景三有已知明文。这是 zip 传统加密最致命的一个弱点。ZipCrypto 用的是基于 CRC32 的流加密只要你能拿到压缩包里某个文件至少 12 个连续字节的明文就能反推出内部密钥进而解密整个包——不管密码多复杂。这在 CTF 里太常见了题目给了一张原始图片、一份公开文档压缩包里恰好也有同名文件这就是明摆着让你做已知明文攻击。工具首选bkcrack比老的pkcrack兼容性更好# 先看加密包里有哪些条目 bkcrack -L locked.zip # 假设包里有 encrypted.png你手上有它的明文 plain.png bkcrack -C locked.zip -c encrypted.png -p plain.png # 恢复出三把内部密钥后生成一个无密码的新包 bkcrack -C locked.zip -k key1 key2 key3 -U unlocked.zip 新密码 # 也可以直接把内容解到目录里 bkcrack -C locked.zip -k key1 key2 key3 -D out_dir如果明文是放在另一个压缩包里给你的可以用-P plain.zip -p plainfile指定让 bkcrack 自己去找条目。注意这是这条链路里最容易翻车的一步。已知明文攻击对明文字节的要求很严格——如果目标条目是 store不压缩存储的你手上的明文文件直接就能用成功率极高如果目标条目是 deflate 压缩存储的那么参与运算的其实是压缩后的数据你手上的明文文件必须先保证在相同工具、相同压缩级别下打包字节才能对得上。很多人卡在这里明明明文是对的就是跑不出来问题十有八九出在压缩级别不一致。稳妥的做法是拿已知文件按默认参数自己打一个包把里面该条目的压缩数据抠出来当明文用。2.4 极端情况文件太小干脆用 CRC32 碰撞还原有时候你会遇到更刁钻的一层压缩包里就一个内容极短的加密文件比如 1 到 4 个字节而且没有任何密码线索。这时候爆破密码是舍近求远正确的思路是攻击 CRC-32。原理不复杂zip 的每个条目都会存一个 CRC-32 校验值这个值不看密码、从中央目录里直接就能读到。而 CRC-32 只有 32 位如果明文只有 1 个字节那可能的内容也就 256 种2 个字节是 65536 种3 个字节也就一千六百多万种一台普通电脑几秒钟就能枚举完。枚举出内容之后你再回头看它对应的 CRC 值是不是和中央目录里存的一样一样就说明你还原对了。import binascii def crack_crc(target_crc, length): 已知 CRC32 和明文长度暴力还原 1~3 字节的内容 import itertools for combo in itertools.product(range(256), repeatlength): data bytes(combo) if binascii.crc32(data) 0xFFFFFFFF target_crc: return data return None # 比如从中央目录读到 CRC 是 0x352441c2明文长度 3 print(crack_crc(0x352441C2, 3))4 字节的话单纯用 Python 的itertools会慢得离谱得换更聪明的做法——比如用查表法预先构建 CRC 状态转移或者直接上那些在线 CRC32 反查站点它们后台基本都预计算了常见长度的所有组合。这类题的核心考点其实是你知不知道 CRC-32 可以被反推知道之后工具反而是次要的。3. 完整实操复现从 zip.zip 到 flag3.1 环境与工具清单先把家伙什备齐。我不建议一开始就装一堆 GUI命令行工具先把链路跑通遇到需要手工改字节的时候再上十六进制编辑器。工具用途平台file读文件真实类型不看后缀Linux / macOS / WSLbinwalk扫描并分离内嵌文件Linuxunzip/7z命令行解压7z对异常包更宽容全平台zipinfo详细列出条目信息Linux010 Editor十六进制 结构模板看 zip 结构神器全平台HxD轻量十六进制编辑器WindowsfcrackzipZipCrypto 字典 / 暴力Linux / macOSzip2johnjohn提取哈希后离线破解全平台bkcrack已知明文攻击全平台Pythonzipfile脚本化处理、批量修复全平台这里单独说下7z和unzip的取舍。unzip是标准实现报错信息清晰适合排查7z对格式异常、编码混乱的包容忍度更高经常在unzip罢工的时候它还能解出个七七八八。两个都装上遇到问题交叉验证比死磕一个工具强。3.2 分步实操识别、套娃、修复、解密第一步识别真实类型。题目给的可能是zip.zip也别信后缀先问一遍file zip.zip # zip.zip: Zip archive data, at least v2.0 to extract binwalk zip.zip如果是第一次做这类题解出来的第一个文件先别管它叫什么直接file一下大概率又是一个压缩包。第二步跑套娃脚本。用 2.1 里那个 Python 脚本把入口文件名改成实际的然后观察输出。正常的输出长这样[1] zip.zip - zip [2] layer_1/flag - zip [3] layer_2/data - zip [4] layer_3/secret - unknown 停在第 4 层剩余文件layer_3/secret停下来的那个文件要么是明文答案要么是需要进一步处理的目标。如果是unknown先file一下确认它是文本还是别的什么东西如果脚本在某一层报解压失败或者需要密码那就进入下一步。第三步判断加密真伪。用unzip -v或者7z l -slt看条目信息同时用十六进制工具打开包对着 2.2 里的偏移表看标志位。有个很实用的经验判断**如果一个加密包里的所有文件都小得可怜几十字节以内那它十有八九是伪加密或者 CRC 碰撞不要浪费时间去爆破密码。**反过来如果包里有一个几 MB 的图片或者文档又提示要密码那才是真加密走密码恢复。第四步修复伪加密。确认是伪加密后用脚本一键清标志位或者手工在两处把01 00/09 00改成00 00。改完保存成新文件再解一次。这一步做完通常就能拿到下一层的内容了。第五步处理真加密。先看题目描述有没有给密码线索没有就按弱口令字典 → 纯数字暴力 → 已知明文攻击的顺序试。我一般先用rockyou这类通用字典快速过一遍几十秒没结果就换思路因为出题人给弱口令的话通常是最常见的那几个字典一小会儿就跑完了。跑不出结果就找明文走bkcrack。第六步拿到最终内容。最后一层一般是个文本文件直接cat出来就是 flag格式通常是flag{...}。有时候 flag 还藏在 zip 的注释里别忘了用zipinfo -z或者十六进制工具看一眼 EOCD 后面那段注释内容——这个位置非常容易被忽略但出题人特别喜欢往那儿塞东西。3.3 结果校验与提交拿到内容之后核对两件事一是格式对不对flag 的固定前缀是不是题目要求的那一套二是内容有没有被截断或者乱码尤其是套娃很多层、又经过编码转换的情况下末尾的}经常在复制的时候被漏掉。确认无误后提交到平台即可。如果是自己工作里遇到的、想恢复密码的压缩包同样的流程也适用先判断是不是伪加密或者 CRC 小文件再决定要不要跑字典能用已知明文就别用暴力——暴力破解对复杂密码基本是无解的别在一棵树上吊死。4. 踩坑实录与常见问题速查4.1 我在这类题上栽过的几个跟头第一个坑把伪加密当成真加密去爆破。我最早做这题的时候一看到输入密码的弹窗就下意识去找破解工具结果跑了半天毫无进度。后来才反应过来包里那几个文件小得离谱明显是伪加密。现在的习惯是任何提示要密码的包先看文件大小再决定策略小文件优先怀疑伪加密或 CRC 碰撞大文件才考虑真加密。这一个判断能省掉半条命。第二个坑套娃脚本按文件名判断类型。我写过一版脚本逻辑是后缀是 .zip 就解压结果遇到一个没后缀的文件直接卡死。后来改成读魔数看文件头的50 4B 03 04再也没出过问题。这个教训在 CTF 里是通用的——永远不要相信文件名包括后缀名。第三个坑明文攻击压缩级别不一致。有一次题目给了一张原始 png压缩包里也有一张同名 png我信心满满地拿原始图去跑bkcrack怎么都出不来。折腾半天才发现压缩包里的那张图是用更高的压缩级别打的包字节和原始文件对不上。后来我把原始图用默认参数重新打了一次包把对应条目抠出来当明文一次就过了。这个细节几乎所有教程都不会讲但它是明文攻击失败率最高的原因。第四个坑解压炸弹写满磁盘。有次遇到一个嵌套特别深的包脚本没设层数上限跑到一半磁盘告警。从那以后我所有递归解压的脚本都加了上限并且会顺手检查解压出的总体积。第五个坑中文文件名乱码。zip 对文件名的编码一直没有强统一Windows 下打的包经常用 GBKLinux 下用 UTF-8跨平台解压就容易乱码。unzip可以用-O gbk指定编码7z一般会自动处理。文件名乱码本身不影响内容但如果你要按文件名去找下一层乱码就会很麻烦。心得把每次遇到的异常都记下来慢慢你会形成一套自己的排查顺序。这套顺序比任何一个具体工具都值钱因为它让你在面对全新题型时也不会慌。4.2 常见问题速查表现象可能原因处理方式提示要密码但文件都很小伪加密 / CRC 碰撞看标志位改00 00或反推 CRC一层层套娃解不完嵌套压缩循环脚本加层数上限报不支持的压缩方式压缩方法字段被改 / 伪加密dump 结构看压缩方法字段报END header not found文件被截断或中央目录被删试zip -FF修复或手工重建 EOCD加密包里有可获取明文的文件ZipCrypto 已知明文攻击bkcrack注意压缩级别一致密码是复杂随机串暴力无解放弃爆破找明文或换思路解压出来文件名乱码编码不统一GBK / UTF-8unzip -O gbk或换7zflag 在文件里找不到藏在 zip 注释里zipinfo -z或看 EOCD 之后的注释段5. 延伸日常工作中 zip 还会在哪里绊你一脚5.1 epub、nsz 这类披着别的皮的压缩包搞清楚 zip 的内部结构之后你会发现很多看起来跟压缩无关的格式其实底层就是 zip。最典型的是epub它本质上就是一个 zip 包里面装着mimetype、META-INF/container.xml、以及一堆 XHTML 和 CSS。你把一个 epub 文件的后缀直接改成.zip用解压工具就能打开看到里面的目录结构。所以epub 转 txt解析电子书内容这类需求操作路径其实是解 zip → 找到正文所在的 XHTML → 解析正文而不是去找什么格式转换神器。这就顺带解释了一个常见误解很多人以为rar 转 zipzip 转 epub是改个后缀的事。不是的改后缀只改了个名字文件内部结构一点没变。真正的转换是先解开再按目标格式重新打包——rar 转 zip 是解开 rar 再用 zip 压一遍其他格式互转同理。至于主机平台上那些nsz之类的封装格式它们和 zip 一样都是容器 压缩的思路但容器结构是为特定场景定制的互转必须用对应的专用工具改后缀是没用的。这个道理和上面说的一样判断一个文件是什么看的是它的内部结构不是它的名字。5.2 构建与运维里那些 zip 相关报错怎么定位日常开发里也经常被 zip 绊住。比如用构建工具拉依赖时偶尔会蹦出跟 zip 有关的错误十有八九是某个依赖包下载不完整或者本地缓存坏了——因为 zip 是流式结构文件的任何一段没下全整个包就校验不过。处理办法也很直接清掉依赖缓存目录重新拉或者强制刷新依赖。碰到读取 zip 归档失败这类提示先怀疑下载中断而不是代码写错了。再比如便携版软件很多小工具都以 zip 形式分发正确用法是先解压到本地目录再运行别直接在压缩包里双击可执行文件。因为压缩包在预览时是解到临时目录运行的有些程序需要读写同目录下的配置文件临时目录一关就出问题表现就是第一次能开第二次打不开。还有一类是路径相关的报错比如复制某个 zip 时失败多半是路径太长、文件被杀软或者其他进程占用了或者目标磁盘空间不够。这时候换成短路径、关掉占用程序、确认空间基本都能解决。你会发现这些排查思路背后是同一个逻辑zip 对完整性极其敏感所以遇到跟它相关的报错第一个要问的问题永远是这个文件是完整的吗、有没有被别的进程动过。我个人在做完这道zip题之后最大的收获其实不是记住了几个工具命令而是养成了一个反射般的习惯**任何压缩包到手先file判类型再打开十六进制看结构最后才决定用哪种方式解。**这个顺序看起来慢实际上最快因为它把猜变成了看。工具会过时偏移量会忘但这套先看清结构再动手的思路套到任何封装格式、任何陌生文件上都成立。最后再分享一个小技巧把 2.1 和 2.2 那两个脚本存成自己的常用工具遇到新题直接改个入口文件名就能跑比每次现写省事得多。
阅读完成 · 觉得有帮助?