尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

ZIP CRC32校验原理与碰撞修补:Python修改压缩包后同步CRC

ZIP CRC32校验原理与碰撞修补:Python修改压缩包后同步CRC 简介CRC32碰撞密码与ZIP压缩包解密工具包面向对压缩包加密机制、校验算法与数据恢复感兴趣的Python开发者及安全学习者。资源围绕crc32.py脚本演示如何利用CRC32的碰撞特性构造指定校验值的数据块并尝试在ZIP文件中触发完整性校验绕过帮助理解循环冗余校验的局限与其实际利用场景。压缩包共6个文件以3个py脚本为主辅以测试数据、说明文档和配置文件整体仅24KB便于快速下载与阅读。目前已有873人学习适合作为密码学与安全工具链的入门案例。通过脚本与测试样例读者能掌握CRC32碰撞的生成思路、ZIP头中CRC32字段的作用以及如何用Python自动化调节数据内容并验证校验值从而复现碰撞过程为后续分析更复杂的校验与加密机制打下基础。1. 一个“改了配置却没法通过校验”的问题把 zip 的 CRC32 推到了台前运维把配置模板打进 zip 资源包分发出去业务侧解压时对每个条目做 CRC32 校验同事嫌麻烦直接改了压缩包里的config.json结果所有目标机器都报“文件损坏”。更麻烦的是从报错信息里只能看到“bad CRC-32”根本看不出数据哪里错了。这时你多半会去搜“zip crc32”“zip crc 碰撞”这类关键词然后遇到crc32-master.zip里那种命名为crc32.py的小脚本。这类小工具解决的并不是“找到原始密码”而是“让修改后的 zip 包能够通过 CRC32 校验”。zip 的 CRC32 存储在文件头的固定偏移处只要在改写条目的同时把 CRC 字段同步更新解压器就会认为数据完整。标题里那串crc32碰撞密码_zip crc32之所以常被合并搜索是因为很多网文把“绕过校验”和“破解密码”混在一起来写。本文会从 zip 的字节布局出发把 CRC32 计算、头字段定位、原地修补和常见的“could not find EOCD”排错一次讲清楚全程给出可直接复制改用的 Python 代码适合做资源包校验、固件包工具链和磁盘镜像处理的一线工程师阅读。2. zip 文件里 CRC32 的存放位置、校验时机与碰撞可行性2.1 ZIP 本地文件头中 CRC32 的固定偏移zip 不是一个把所有文件数据简单堆叠的格式它由本地文件头、压缩数据、中央目录和尾部 EOCD 四段构成。每个条目在文件靠前位置有一个本地文件头固定 30 字节不含文件名和扩展字段。CRC32 字段位于本地文件头偏移 14 处长度 4 字节小端存储。下表是本地文件头关键字段的偏移和含义偏移长度字段说明04签名固定0x04034b50即 ASCII 的PK\x03\x0442版本压缩所需的 zlib 版本号一般可忽略62标志第 3 位为 1 时表示使用 data descriptor82压缩方式0 为 STORED8 为 DEFLATED102修改时间DOS 时间格式122修改日期DOS 日期格式144CRC32未压缩数据的循环冗余校验值184压缩后大小压缩字节数224未压缩大小原始字节数262文件名长度紧跟在本地文件头后面的名字字节数282额外字段长度名字后再跟的扩展字段字节数注意如果文件头标志位第 3 位是 1本地文件头中的 CRC32 通常填 0真正的值在条目数据结束后的 data descriptor 里。用 Python 的zipfile读出.header_offset后直接去偏移 14 处改写 CRC对这类带描述符的包不一定有效更可靠的做法是同时修补中央目录里的 CRC 字段。中央目录记录PK\x01\x02的标志从签名开始数偏移 16 的位置也是 CRC32而且这个值是被绝大多数严格实现优先读取的。2.2 CRC32 的线性运算与 32 位碰撞空间CRC32 本质上是把数据当作一个很大的二进制多项式模上生成多项式后取余数。zip 使用的标准 CRC-32 多项式是0xEDB88320初值0xFFFFFFFF结果再与0xFFFFFFFF异或。这意味着它不是一个加密哈希没有抗碰撞性设计相同长度的不同数据完全可能得到相同 CRC只是概率约为 2 的负 32 次方在实际的“碰巧”中几乎遇不到但可以被刻意构造。工程上我们不会真的去遍历 2 的 32 次方来撞一个值而是直接重算。Python 的标准库里已经封装好了实现zlib.crc32和binascii.crc32等价只是返回值在不同平台上可能是负数所以需要与0xFFFFFFFF做掩码import zlib raw b{mode:test} crc_a zlib.crc32(raw) 0xffffffff print(f原始 CRC: {crc_a:08X}) patched b{mode:prod} crc_b zlib.crc32(patched) 0xffffffff print(f修改后 CRC: {crc_b:08X})这段代码先把字符串按 UTF-8 编码成字节流然后计算 CRC32。这里关键的细节有两个一是必须在字节层面做运算不能把str直接传进去二是做掩码否则结果可能以有符号数打印出来看起来像一个负的十六进制值干扰后续写入 zip 头的逻辑。2.3 为什么下载站把“CRC 碰撞”和“密码”混为一谈回到标题里的crc32碰撞密码。真实情况是CRC32 与 zip 密码没有直接关系。传统 ZipCrypto 加密时CRC32 会作为密钥派生的一部分参与异或计算所以已知一段明文可以反推密钥流某个片段但这不等于“用 CRC 碰撞能得出密码”。网上很多仓储包把crc32.py描述成“zip 压缩包密码破解工具”其实它的作用是当你知道 zip 内部某个明文条目的原始内容并把内容替换成自己的数据后让修改后的压缩包通过 CRC 校验。在实际做文件分析和数据恢复时不要把 CRC32 当作完整性认证。它能发现随机损坏挡不住人为构造。理解了这一点再去看那些在 zip 头里改 4 个字节让两个不同文件共用同一个 CRC 的脚本就不会被“碰撞”两个字带偏碰撞是数学性质重算才是日常手段。3. 最小可用实现修改 zip 条目后把 CRC 值同步写回头部3.1 用 Python 定位本地文件头与中央目录的 CRC 字段直接读写 zip 二进制的场景一般出现在“不能重新打包”的约束下。比如某个资源包启动器依赖包内条目偏移量和压缩方式重新打包后偏移全部变化导致程序崩在偏移断言上。这时需要在原文件上打补丁保持压缩流不动只替换局部数据并把 CRC 写到本地文件头里。拿zipfile获取某个条目的header_offset是第一步但它只给了本地文件头位置没给中央目录位置。中央目录记录格式固定遍历起来也很简单。下面的函数用来在整段字节流中定位某条目对应中央目录记录中的 CRC 字段import struct CD_SIG bPK\x01\x02 EOCD_SIG bPK\x05\x06 def find_cd_crc_offset(buf: bytes, entry_name: str) - int | None: eocd_pos buf.rfind(EOCD_SIG) if eocd_pos 0: raise ValueError(找不到 EOCD文件可能被截断) cd_offset struct.unpack_from(I, buf, eocd_pos 16)[0] p cd_offset while p eocd_pos: if buf[p:p 4] ! CD_SIG: break # 中央目录记录固定部分 46 字节 nlen, elen, clen struct.unpack_from(HHH, buf, p 28) name_bytes buf[p 46:p 46 nlen] if name_bytes.decode(utf-8, errorsreplace) entry_name: return p 16 # CRC 字段在签名之后的第 16 字节 p 46 nlen elen clen return None逻辑拆开看先从文件尾部反向找PK\x05\x06这是 EOCD 的固定签名EOCD 里偏移 16 处存着中央目录在文件中的绝对偏移。拿到中央目录的起点后逐条解析每条记录从签名开始偏移 28 处是文件名长度偏移 30 是扩展字段长度偏移 32 是注释长度。记录总长就是46 nlen elen clen。文件名加了errorsreplace防止某些工具写入非 UTF-8 名字时直接抛异常。3.2 原地修补 STORED 条目并重写 CRC 的函数STORED 方式表示数据没有压缩文件里存的就是原始字节。这种条目最适合原地修补因为压缩后的文件大小不变头部各长度字段都不用动只需要把 CRC 字段替换成新值。注意还要检查条目是否启用了 data descriptor如果启用了理论上一并更新文件尾部描述符里的 CRC 更严谨但从主流解压器行为看更新本地头和中央目录已经足够。import zipfile import struct import zlib import shutil def patch_stored_entry(zip_path: str, entry_name: str, offset: int, new_bytes: bytes) - None: backup zip_path .bak shutil.copy2(zip_path, backup) with zipfile.ZipFile(backup) as zf: info zf.getinfo(entry_name) if info.compress_type ! zipfile.ZIP_STORED: raise RuntimeError(这个条目不是 STORED 方式请改用重新打包方案) raw bytearray(zf.read(entry_name)) end offset len(new_bytes) if end len(raw): raise ValueError(修改范围超出条目长度) raw[offset:end] new_bytes new_crc zlib.crc32(raw) 0xffffffff local_crc_off info.header_offset 14 with open(backup, rb) as f: buf bytearray(f.read()) cd_crc_off find_cd_crc_offset(bytes(buf), entry_name) if cd_crc_off is None: raise RuntimeError(中央目录里找不到条目) struct.pack_into(I, buf, local_crc_off, new_crc) struct.pack_into(I, buf, cd_crc_off, new_crc) f.seek(0) f.write(buf) shutil.move(backup, zip_path)函数入口参数分别是 zip 文件路径、目标条目名、修改起始字节偏移和要替换的字节串。代码先做备份再从备份里读取原始条目内容patch 完成之后计算新 CRC然后把值写回本地文件头和中央目录。之所以从备份而不是原文件读取是因为后面的二进制写入会破坏zipfile的内存视图。有一点在这里特别值得说明如果你把数据改短后面的字段会错位。所以这个函数要求offset len(new_bytes)不超过原始长度等于说只允许“覆盖内容”不允许改变条目总长。很多场景下的配置修改满足这个条件比如把false改成true把测试环境域名改成长度相同的生产域名。若必须改变长度只能用下一节的重新打包。3.3 不适合原地修补时用 ZIP_STORED 重新打包资源包里的配置一般体积不大重新生成一个 zip 完全可行。用zipfile重打包的收益是省心writestr会自动计算 CRC、压缩体积和头部偏移几乎不可能写出解压器不认的包。以修改一个 JSON 配置为例def repack_with_patch(src: str, dst: str, entry: str, offset: int, new_bytes: bytes) - None: with zipfile.ZipFile(src, r) as zin: with zipfile.ZipFile(dst, w, zipfile.ZIP_DEFLATED) as zout: for info in zin.infolist(): data bytearray(zin.read(info.filename)) if info.filename entry: end offset len(new_bytes) if end len(data): raise ValueError(修改范围超出条目长度) data[offset:end] new_bytes zout.writestr(info, bytes(data))这里最大的坑是zout.writestr(info, ...)接收的是ZipInfo对象而不是名字这样会把原条目的时间、外部属性、注释全部保留。如果改成zout.writestr(info.filename, ...)新包的文件时间会变成当前时间外部属性也丢失部分严格的导入工具会因此判重并拒绝导入。4. 跑通“zip crc 碰撞”资源包修复与 invalid zip archive 排障4.1 给资源包工具链加一段 CRC 重算逻辑我经手过一种现场设备固件里的assets.zip被烧录器直接按偏移索引读取索引表预先算好zip 内部的条目偏移不能动。厂商只开放了 zip 包里的某个model.json给客户调整机型参数而导入工具在写入前会校验整包 CRC。此时前面的patch_stored_entry就是唯一合适的解法在包构建脚本里按model字段判断机型替换 JSON 内容再调用patch_stored_entry同步 CRC。整个过程不重压缩、不改变偏移导入端看到的 CRC 与内容一致不会再报“bad CRC-32”。这类“线刷 zip 工具”场景里常见做法是先用zipinfo -v看每个条目的压缩方式确认是stored还是deflated。若固件包内的镜像文件本身已经压缩过外层的 zip 再压一次收益很小很多工具链会直接选 STORED这也是为什么本章的办法覆盖了大量真实更新包。4.2 invalid zip archive: could not find EOCD 的修复步骤自己改 zip 二进制时最常遇到的解析报错是could not find EOCD。这个错误的本意是在文件末尾往前找PK\x05\x06时没找到。EOCD 的长度不固定末尾有一个可变长度的注释标准实现会从文件末尾向前最多搜索 65557 字节。如果向 zip 文件尾部追加了额外内容或者写入时截断了一半EOCD 就找不到了。针对这种被破坏的包通用恢复流程推荐这样做zip -FF damaged.zip --out repaired.zip-FF的作用是扫描整个文件里的本地文件头和中央目录签名尝试重建中央目录。运行结束后务必再用unzip -t repaired.zip做一次全量 CRC 校验。另一种情况是“文件没有被截断但解压工具仍然报错”这时多半是中央目录里的 CRC 和你改过的数据不一致可以用 Python 的zipfile打开逐个条目读取看报错停留在哪个文件名上然后用上一章的函数修正对应条目。如果破坏发生在本地文件头比如把偏移 14 处的 CRC 字段覆盖成了错误值而中央目录还是旧的正确值解压器在抽出时按中央目录的 CRC 报错但依然能列出文件清单。此时优先改中央目录而不是只动本地头多数实现读数据前会从本地头拿 CRC因此两处都要一致。4.3 注意 crc 碰撞不等于绕密码标题里出现“密码”两个字这里必须把边界划清楚。利用 CRC32 让内容通过校验不等于破解了 zip 的加密也不等于有合法权限修改他人数据。真实世界中的应用在于测试自己的产品导入接口是否对压缩包做了防篡改校验物料包更新脚本是否只依赖 CRC 判断完整性网关在接收配置包时会不会被 4 字节改写骗过。如果你在做安全测试结论应该是CRC32 只能作为“随机错误检测”不能作为“完整性认证”。要防篡改包内至少要额外放一个 HMAC 签名或者用带签名的.zipx、.apk签名机制。生产系统的导入工具如果只查 CRC那它本身就存在设计缺陷正确做法是把校验逻辑从“CRC32”升级到“数字签名”而不是继续在 zip 头里多写几个字节。5. 把 CRC32 修补做成参数化函数并验证解压结果5.1 函数签名与参数说明把前面的逻辑收拢成一个通用工具建议四个参数预留默认值便于 CI 脚本和正式环境共用参数类型必填说明zip_pathstr是目标 zip 文件的路径entrystr是要修改的条目名offsetint是数据区起始偏移从该条目未压缩内容开头计算new_bytesbytes是替换后的字节串长度必须不超过原数据剩余区间strategystr否auto或repack默认autostrategyauto时会自动判断条目压缩方式STORED 走原地修补DEFLATED 走重新打包。这样脚本在拿到未知 zip 时也能一次跑通不需要先手动检查。5.2 用 unzip -t 与 zipfile.testzip 双重验证改完包不建议直接拿去做导入先验证一轮 CRC。命令行验证是终审标准unzip -t fixed.zip若输出全部是OK再去业务系统里试导入。想在自己的 Python 测试里自动断言可以写一个验证函数import zipfile def verify_zip(path: str): with zipfile.ZipFile(path) as zf: bad zf.testzip() if bad: raise RuntimeError(f条目 CRC 校验失败: {bad}) return True这个函数遍历每个条目把数据完整解压出来与头部 CRC 比对任何一个不一致都会返回文件名。把它放在修补脚本的末尾能保证在 CI 流程里不会放出一个带错误 CRC 的包。最后留一个提示如果 zip 里存在带数字签名的外部文件比如 APK 或带签名的固件分区重算 CRC 不会让签名失效签名的验签步骤才是这类文件真正的安全边界。本文还有配套的精品资源点击获取
返回列表