CTF竞赛里遇到一个ZIP文件双击压缩包弹窗要密码但题目描述里又没给任何提示这种情况十个选手有八个都碰到过。很多人第一反应是上字典爆破结果跑了半天一无所获最后才发现文件根本没加密只是被人把“加密标志位”改了一下。这种把戏就是ZIP伪加密在CTF入门题里出现频率极高说穿了就是一个字节的事。这篇文章我会从ZIP文件本身的二进制结构讲起把伪加密的原理拆开揉碎然后带你用手工十六进制编辑、Python脚本、以及常见的命令行工具完整走一遍解题流程保证你以后再看到这类题不用再猜密码。先说清楚这篇文章适合谁。如果你刚接触CTF对ZIP格式还不熟悉可以先看前两章如果你已经会解普通ZIP加密但对伪加密只停留在“用某个工具自动修”的层面那你可以直接跳到第三章我给的脚本和实战案例能帮你彻底吃透原理。整个内容不依赖任何图形化收费软件只要一台装了Python的电脑再加一个十六进制查看器就能实操。1. ZIP伪加密的本质一个标志位引发的“假密码”1.1 先看懂ZIP文件的三段式结构要理解伪加密不能只把ZIP当成一个“压缩包”。ZIP本质上是一个带索引的文件容器它的结构由三大部分组成本地文件头、压缩数据区、中央目录最后还有一个中央目录结尾标记。每个被压缩的文件在ZIP里都对应一个“本地文件头”这个头里面记录了文件名、压缩方式、时间戳、CRC校验值、大小等关键信息然后紧跟着就是压缩后的数据。而所有文件的“索引”则统一放在文件末尾的中央目录里中央目录里面每个文件也有一份文件头记录用来告诉解压程序“这个压缩包有哪些文件各自在什么位置”。我们一般用十六进制编辑器打开ZIP看到的开头通常是50 4B 03 04也就是ASCII字符的“PK”这是ZIP本地文件头的固定签名。继续往后看在偏移4到5是版本号偏移6到底7是两个字节的“通用位标志”General Purpose Bit Flag。这个词听起来很学术但它就是整个伪加密的命门。通用位标志一共有16个bit每一位代表一种特性比如bit 0表示文件是否加密bit 1表示是否使用了流式压缩bit 2表示是否使用了更高级的压缩选项bit 3表示数据描述符是否存在等等。对CTF选手来说bit 0是这个标志位里最重要的一个。你可能会问为什么这些标志位不设计成一个单独字段非要挤在两个字节里这其实是为了兼容早期的DOS/PKZIP版本设计者需要在一个固定大小的字段里塞进尽可能多的功能开关于是就有了这种“位图式”的设计。理解这一点对于后面手工修改标志位非常有帮助你不需要关心整个值是多少只需要关心第0位是不是1。1.2 加密标志位在哪本地头与中央目录的关系现在我们把目光集中到偏移6处的那个通用位标志。假如它的十六进制值是00 01低字节为1高字节为0那么bit 0就是1解压程序就会认为这个文件采用了ZIP传统加密于是弹出密码输入框。问题来了如果这个文件实际上是用普通方式压缩的压缩数据本身根本没有经过任何密码算法处理那么它就是一个“伪加密”文件纸面上写着加密实际数据却是明文的。更坑的一点是ZIP里同一个文件在本地文件头和中央目录里各有一份“通用位标志”。本地文件头位于压缩包前部中央目录里的那一份位于压缩包末尾。大部分解压软件在打开压缩包时会先读取中央目录来构建文件列表然后再根据本地文件头的标志位来判断是否加密。因此只要你把其中任意一处的bit 0改成1某些软件就会把它当加密文件处理。CTF的出题人往往会把两个标志位都改成1这样无论你用Windows自带解压、WinRAR还是7-Zip打开都会要求输入密码。这里有个很重要的细节标志位是分文件记录的。如果一个ZIP包里有多个文件出题人可能只把其中一个文件的标志位改了其他文件保持正常。你打开压缩包时虽然能看到整个文件列表但只有那个被标记的文件无法正常解出其他文件直接拖出来就能用。遇到这种情况不要急着爆破先逐个文件检查标志位很可能目标文件就是伪加密。1.3 伪加密与真加密的判定逻辑知道了标志位的位置你就可以从二进制层面区分伪加密和真加密。判断的关键是标志位为1时压缩数据的实际内容是否真的经过加密算法处理。ZIP传统加密是在压缩完成后对压缩数据流进行异或加密同时还会用一个基于密码生成的密钥来初始化。如果数据是伪加密的你在十六进制编辑器里用压缩工具查看会发现压缩数据流里的字节分布很正常而且用任何密码都无法解压——因为它压根没走加密流程自然也不存在“密码正确”这回事。从操作上看最直接的判定方法是用十六进制编辑器打开ZIP手动把标志位的bit 0改成0保存后尝试解压。如果能直接解出文件、且CRC校验通过那就基本石锤是伪加密。另一种判断方法是用无密码模式尝试某些命令行解压工具比如7z t会显示“Encrypted -”或“Encrypted ”配合数据流分析就能识别。实操中我一般会先用Python快速读一下标志位值再用解压结果反推两步走基本不会误判。2. 手工识别与修复从十六进制字节看穿伪加密2.1 使用十六进制编辑器定位关键字节手工方式虽然不如脚本快但能让你对ZIP结构建立直观感受。我建议每个CTF新手都至少手工修一次伪加密。这里我们用WinHex或010 Editor打开一个伪加密ZIP文件第一步是找到第一个本地文件头签名。搜索十六进制50 4B 03 04定位到后从签名开始数6个字节即偏移6处是一个两字节的通用位标志。比如原始字节是01 00高字节在前实际上ZIP是小端序数值0x0001在文件中表现为01 00也就是说第一个字节是0x01第二个字节是0x00。这个0x01就代表bit 0是1文件被标记为加密。更常见的场景是出题人会把通用位标志写成00 01这里容易迷糊我按标准格式解释清楚。ZIP标准里通用位标志是16位字段小端序存储所以bit 0在最低位。数值0x0001存储为01 00数值0x0002存储为02 00数值0x0008存储为08 00。虽然十六进制编辑器显示的是两个字节但逻辑上它是一个小端数值。因此当你看到01 00时第0位确实是1。如果改成00 00加密标志就清掉了。有些文章会说“把偏移6的字节改成00”说得不够准确因为你可能是要在01 00中间改第一个字节为00但也要注意不要误改第二个字节否则会改变其他标志位。这里我的建议是只针对bit 0操作而不是粗暴地把整个字段清零。找到本地文件头之后还要去包末尾找中央目录头。搜索签名50 4B 01 02定位到中央目录文件头之后从签名开始数8个字节也就是偏移8处同样是两字节通用位标志。到这里就能看到同一文件在索引区的加密标记。手工修复时本地文件头和中央目录头都要把bit 0清掉。2.2 手工修改标志位最小干预恢复明文ZIP手工修改的思路是把bit 0置0其余位保持不变。具体操作时如果看到01 00就把它改为00 00如果看到09 00bit 3也为1应该改成08 00而不是改成00 00因为bit 3可能表示使用了数据描述符如果贸然清零解压时读取结构就可能错位。但CTF里绝大多数情况是01 00所以直接改两个字节为00 00通常没问题。修改前最好备份原始文件因为一旦改坏了还能恢复。修改后用普通的解压软件打开如果不再要求密码说明伪加密修复成功。我见过有的选手只改了本地文件头Windows解压已经不弹窗了但用某些在线解压网站仍然提示加密就是因为中央目录没改。所以我的习惯是两个位置同步修保证工具链的兼容性。这里还要注意如果一个ZIP包里有多个文件就要逐一搜索本地文件头和中央目录头逐个清除加密标志。手工操作时你可以用010 Editor的模板功能把ZIP头部字段解析出来直接修改模板中的flag字段这样比裸改十六进制更方便。没有模板就用搜索功能一个个来数量多的话容易遗漏我建议进入下一章节的脚本环节。2.3 为什么两个标志位都要改踩坑记录我最初学这道题时也是只改本地文件头的标志位结果用Python的zipfile模块读取时虽然已经不报加密了但用7-Zip打开还是能看到文件后带着一个星号代表加密。查了很久才意识到中央目录里的标志位同样需要修改。原因很简单Windows资源管理器、7-Zip之类的软件在加载压缩包时会优先读取中央目录来显示文件列表和属性而zipfile模块在读取每个文件时会以本地文件头为准。两边不一致就会出现不同工具行为不一致的诡异现象。只改一个标志位有时候也能解压但会造成一种“薛定谔的加密”状态这个工具认为没加密那个工具认为加密。所以稳妥起见凡是遇到伪加密题目我都会写一个通用脚本把ZIP内所有文件的本地头和中央目录头都扫描一遍把所有bit 0都置0。这既符合标准也避免后续验证环节踩坑。如果你在解题时发现“改了还是弹密码”第一件事不是怀疑脚本而是检查有没有遗漏另一个位置。3. 自动化脚本与工具链实战3.1 用Python写一个批量修复脚本手工修复学一次就够了真正常用的是自动化脚本。Python的zipfile模块不能直接改标志位所以我们要自己操作二进制数据。核心思路是读入ZIP全部字节遍历搜索本地文件头签名和中央目录头签名对每个签名后的标志位字段做“清零bit 0”操作。注意不能影响其他字段所以用位运算把标志位数值与0xFFFE做与运算。我常用的脚本如下直接保存为fix_zip_flag.py运行即可import struct import sys def clear_encryption_flag(data: bytes) - bytes: data bytearray(data) # 本地文件头签名 local_sig bPK\x03\x04 # 中央目录文件头签名 central_sig bPK\x01\x02 # 处理本地文件头 offset 0 while True: idx data.find(local_sig, offset) if idx -1: break # 本地文件头签名偏移6处是通用位标志2字节小端 flag_off idx 6 flag struct.unpack_from(H, data, flag_off)[0] # 清零bit 0保留其余bit new_flag flag 0xFFFE struct.pack_into(H, data, flag_off, new_flag) # 跳到下一个要找的位置这里不能直接跳过整个文件结构 # 因为我们需要遍历所有可能的本地文件头。 offset idx 4 # 处理中央目录文件头 offset 0 while True: idx data.find(central_sig, offset) if idx -1: break # 中央目录文件头签名偏移8处是通用位标志2字节小端 flag_off idx 8 flag struct.unpack_from(H, data, flag_off)[0] new_flag flag 0xFFFE struct.pack_into(H, data, flag_off, new_flag) offset idx 4 return bytes(data) if __name__ __main__: if len(sys.argv) 2: print(usage: python fix_zip_flag.py file.zip) sys.exit(1) path sys.argv[1] with open(path, rb) as f: raw f.read() fixed clear_encryption_flag(raw) out_path path .fixed.zip with open(out_path, wb) as f: f.write(fixed) print(f[] done: {out_path})这段代码有一个需要注意的地方搜索签名时我用的是find并且每次只把偏移4这是为了安全防止签名本身跨越边界。不过如果压缩数据里恰好出现了PK\x03\x04或PK\x01\x02这几个字节可能会误改数据区里的内容。实操中概率很低但如果你发现解压后文件内容有异常就要改用更精确的方式从ZIP结构上解析通过文件名长度和压缩数据长度来跳过数据区。CTF题一般没有这种复杂情况简单脚本完全够用。更严谨的做法是直接解析中央目录在文件末尾搜索50 4B 05 06中央目录结尾签名从那里得到中央目录偏移和条目数量然后逐个解析每个中央目录文件头再根据文件名、扩展字段长度和注释长度跳到下一个条目。同时还可以通过中央目录中的本地头偏移去修正对应本地文件头的标志位。这种解析方式字段多但不会误伤数据区。我在下面给出一个简化但更稳的版本适合处理常规ZIPimport struct import sys def fix_zip(path): with open(path, rb) as f: data bytearray(f.read()) # 从末尾找中央目录结尾记录 eocd eocd_sig bPK\x05\x06 eocd_idx data.rfind(eocd_sig) if eocd_idx -1: print([-] not a zip file?) return None # 中央目录偏移从eocd中读取eocd结构偏移16处4字节为中央目录偏移 cd_offset struct.unpack_from(I, data, eocd_idx 16)[0] total_entries struct.unpack_from(H, data, eocd_idx 10)[0] # 遍历中央目录条目 for i in range(total_entries): # 中央目录文件头签名 PK\x01\x02 if data[cd_offset:cd_offset4] ! bPK\x01\x02: print([-] bad central directory) break central_flag_off cd_offset 8 flag struct.unpack_from(H, data, central_flag_off)[0] struct.pack_into(H, data, central_flag_off, flag 0xFFFE) # 读取文件名长度、扩展字段长度、注释长度 name_len struct.unpack_from(H, data, cd_offset 28)[0] extra_len struct.unpack_from(H, data, cd_offset 30)[0] comment_len struct.unpack_from(H, data, cd_offset 32)[0] # 本地文件头偏移在中央目录条目偏移42处4字节 local_offset struct.unpack_from(I, data, cd_offset 42)[0] if local_offset 4 len(data) and data[local_offset:local_offset4] bPK\x03\x04: local_flag_off local_offset 6 lflag struct.unpack_from(H, data, local_flag_off)[0] struct.pack_into(H, data, local_flag_off, lflag 0xFFFE) # 跳到下一个中央目录条目 cd_offset 46 name_len extra_len comment_len out_path path.replace(.zip, .fixed.zip) with open(out_path, wb) as f: f.write(data) return out_path if __name__ __main__: if len(sys.argv) 2: print(usage: python fix_zip.py file.zip) sys.exit(1) result fix_zip(sys.argv[1]) if result: print([] fixed:, result)这个版本能按中央目录结构精确遍历并且顺带修正对应本地文件头遇到真实ZIP结构基本不会出错。如果你的电脑里装了pefile不需要就用标准库struct就够了。3.2 搭配binwalk与7z快速验证脚本写完后如何验证是否修好我一般先在命令行下看压缩包信息。在Kali Linux里先执行binwalk file.zipbinwalk会识别ZIP文件并列出结构还能显示压缩包内文件列表。虽然binwalk在伪加密识别上不直接给结论但它能帮我们定位文件偏移配合十六进制查看很方便。然后执行7z l file.zip7-Zip会在“属性”列显示每个文件是否加密。如果一个文件显示带或者提示“Encrypted”就说明被标记为加密修复后再次运行7z l file.fixed.zip属性列的加密标记应该消失。这一步比用图形界面更直观也方便写进解题笔记里。解压一个修复后的ZIP我推荐用7-Zip的命令行7z x file.fixed.zip -ooutput如果你的环境是Windows也可以直接右键解压。但命令行好处是可以看到解压过程中的CRC校验结果如果出现“CRC Failed”说明修复改错了地方需要检查脚本是否误改了数据区。除了7z还可以用Python的zipfile模块验证比如import zipfile with zipfile.ZipFile(file.fixed.zip, r) as z: for info in z.infolist(): print(info.filename, info.flag_bits)info.flag_bits是zipfile.ZipInfo里保存的标志位如果第0位为0flag_bits 1 0就说明脚本生效了。3.3 实战案例一道CTF题的完整解题流程为了让你有更具体的体感我虚构一道入门题。题目附件是一个叫secret.zip的压缩包双击后提示要密码。你按下面顺序排查。第一步先看文件类型。终端里执行file secret.zip确认是ZIP压缩包。接着用binwalk secret.zip可以看到文件结构和内部文件名比如flag.txt。第二步查看加密状态。执行7z l secret.zip输出里会显示类似Encrypted 的一行说明文件被标记为加密。但题目没有给任何密码这种“无来源密码”是伪加密的强信号。第三步用十六进制编辑器打开或者直接用上一节的脚本扫描标志位。如果看到本地文件头偏移6处是01 00中央目录偏移8处也是01 00就能确认是伪加密。第四步运行修复脚本python3 fix_zip.py secret.zip得到secret.fixed.zip再执行7z x secret.fixed.zip -oflag解压出flag.txt读取flag。整个流程不超过两分钟。如果解压出来发现文件损坏八成是因为ZIP内部有多个文件或者标志位不只一处需要检查脚本是否完整处理了所有条目。这个流程里我没有用任何密码爆破工具也没有去猜密码因为伪加密的解药不是密码而是格式理解。4. 常见问题与进阶排查4.1 误判真加密还是伪加密很多人一看到ZIP要密码就直接上字典爆破浪费时间不说还可能把本机CPU跑满。判断真伪加密除了看标志位还可以看压缩包的大小和文件内容。真加密的压缩数据字节分布会显得比较“乱”而且没有标准压缩头特征伪加密则保持了普通压缩数据的特征用7-Zip打开时会发现文件虽然标记加密但压缩率显得不太正常甚至能看到部分明文特征。不过最可靠的还是手工查标志位。还有一个判断技巧如果压缩包是用ZIP传统加密不是AES加密它会在压缩数据前附带一个12字节的加密头。伪加密不会有这个头。你可以用十六进制编辑器在本地文件头后面的压缩数据区检查如果看到有规律的可打印文本比如压缩数据开头是常见的x\x9czlib压缩头那说明数据没有加密只是标志位被改了。x\x9c这个魔数在CTF里出现频率极高看到它基本就放心了。4.2 修复后解压失败怎么办修复脚本跑完解压却报CRC错误这种事我也遇到过。排查方向有三条。第一检查是否误改了数据区中恰好出现的PK签名。如果是解压后数据会被改动CRC自然失败。解决办法是改用解析中央目录的精确脚本而不是盲搜签名。第二检查是否只改了本地文件头而忘了中央目录导致部分工具还是按加密流程处理。第三检查压缩包内是否还有其他文件没有处理比如有的ZIP包含目录条目目录条目也有标志位但目录本身不需要加密标志如果误清了其他必要标志位也可能导致结构读取失败。如果解压后文件大小正常但内容不对可以用zipinfo -v查看每个条目的完整字段确认压缩方法、CRC、大小等是否与中央目录记录一致。大多数伪加密题目改坏的情况都是脚本太粗暴把整个通用位标志清零而不是只清bit 0。我自己的脚本用了 0xFFFE就是专门保留其他bit避免把数据描述符等关键标志改掉。4.3 CTF中的变体标志位翻转、多重嵌套、ZIP注释伪加密这个套路还有不少变体。有的题目只改一个标志位考验你能否发现不一致有的把标志位改成00 01这类“反向”值真实数据结构结合其他bit比如0x0101代表加密的同时还有其他特性这时候你只看到01 01需要判断bit 0和bit 8同时为1bit 8其实是Unicode文件名标志不能乱清。还有的题目把ZIP包装在另一个压缩包里外层的文件正常内层是伪加密你得先解开外层再对内层做一次修复。另一个常见陷阱是ZIP注释。出题人喜欢把密码提示藏在ZIP文件的注释字段里而这个字段在普通文件管理器里看不到。用zipdetails这类工具能看到中央目录结尾的注释内容有时候伪加密的密码就写在注释里比如“密码是1234”这样的提示。不要一味只知道修标志位也要养成看注释的习惯。说到工具链除了Pythonzipdetails是Perl写的小工具能逐字节解析ZIP结构展示每个字段的值和含义非常适合用来理解ZIP内部结构。Kali自带这个工具没有的话用cpan装一下也不难。还有一个思路是直接修改ZIP本地文件头的通用位标志后再用zip -FF修复损坏的压缩包不过这个命令主要用来修复“中央目录损坏”的ZIP在伪加密修复里不太常用我就不展开了。4.4 几个提升效率的小建议最后补充一些我在实际做题中积累的经验。第一不管用哪种脚本修完一定要跑一遍7z t或python3 -m zipfile -t来测试完整性。这一条能帮你拦住80%的脚本误伤问题。第二在做题环境里建议准备两个脚本一个是快速盲搜型一个是精确解析型遇到复杂ZIP用后者遇到简单题用前者节约时间。第三如果发现ZIP里只有一个文件且是伪加密你甚至可以不用修复直接找一个能忽略加密标志的解压工具比如某些老版本FTP工具或定制脚本直接按未加密数据流解压但在CTF里我建议还是按标准流程修复因为后续可能还有其他格式的题要用到类似技巧。还有不要忘了ZIP压缩支持存储模式和压缩模式两种。如果伪加密文件的压缩方法是“存储”即没有压缩那你甚至可以直接在十六进制编辑器里搜索明文关键词比如flag{因为数据根本没有被压缩也没有被加密。这种情况在一些脑洞题里出现过。反过来如果文件真的被压缩了你修复后解压出来才能看到明文。所以解题时先看压缩方法如果压缩方法为0stored直接在二进制里搜字符串往往比修复更快。我个人在实际操作中最深的体会是所有玄乎的伪加密本质上都只是ZIP结构里一个位(bit)的问题。只要熟悉二进制结构会写几行脚本这类题就可以稳定秒杀。CTF考的从来不是谁字典更大而是谁对文件格式的理解更深。希望这篇内容能帮你把ZIP伪加密彻底变成送分题。
阅读完成 · 觉得有帮助?