首页 / 资讯中心 / 文章详情

把压缩文件藏进PNG图片:一个纯本地小工具的原理与实现

把压缩文件藏进PNG图片:一个纯本地小工具的原理与实现 ★ FEATURED ARTICLE
简介一款将ZIP压缩包伪装成JPG图片的小工具面向有文件保密传输、隐私隐藏需求的个人用户与办公场景。工具能把压缩文件转换为JPEG图片同时保留解压还原能力——将文件后缀改回zip即可恢复内容部分转换过程还结合了加密逻辑可防止未授权访问适合在商业敏感数据传递或本地隐私保护等场景中使用。包体内容相当精简共3个文件压缩包仅43KB一张示例JPG图片、一个readme.html说明文档和一个bat批处理脚本结构清晰下载后按说明操作即可上手目前已有120人学习下载。对想快速体验“隐写式文件保护”的读者来说这套工具提供了轻量的实操方案省去复杂配置示例图片和说明脚本有助于理解转换原理灵活性与可玩性都不错。需要提醒的是这类隐藏方式并非绝对安全建议配合其他加密手段共同使用。1. 把压缩文件藏进图片一个 600 行不到的本地小工具做美术外包那阵子经常要跨网传素材包、模板包和参考图集。压缩包倒是不大但网盘链接动不动就失效邮件附件对 zip 又卡得严微信传压缩包还会被安全策略拦一下来回折腾半天对方一个字没收到。后来我发现一个土办法——把压缩文件直接塞进图片文件里图片能正常打开压缩包也完整带过去了。这个思路不新鲜做隐写的人早就玩腻了但全网找个专门干这活、还能把还原步骤一起打包进文件里的小软件并不多。这个工具就是这样一个小软件纯本地运行不需要安装运行库把 ZIP 或 RAR 绑定到一张 PNG 尾部图片照样预览压缩包照样解压接收方拖进工具点一下就能把原文件还原出来。适合发素材包被拦截的设计师、被网盘折磨的编辑以及懒得学命令行只想双击完事的人。2. 绑定原理与真实边界不是加密而是寄生在 PNG 的尾部空位上先把这个工具的原理说透否则你一旦自定义改参数就会翻车。它做的不是把文件“压进图片”而是利用 PNG 文件格式自身的一个结构空位做拼接。2.1 PNG 文件读取为什么会在 IEND 停住PNG 文件由多个数据块组成关键节点是文件头、IHDR图像头、IDAT图像像素数据、IEND图像结束标记。看图软件读取 PNG 时会从头开始解析数据块一路读到 IEND 就认为文件结束、停止解析。IEND 位于文件尾部但有意思的是IEND 之后如果还有字节看图软件一般不会报错也不会去解析它们——这些多出来的字节对图片显示毫无影响却被文件系统完整保留。这就留出了一个可以动手脚的空位把任意压缩包、文本、PDF 的二进制数据顺着 IEND 追加到 PNG 末尾图片在查看器里没有任何变化但文件体积会变成原图加附加文件的总和。处理这类任务时读取图片用的标准库会停留在 IEND 之前而文件拷贝操作则可以访问全部字节包括你追加在后面的那一大段。这也是很多图片压缩工具在导出 PNG 时文件莫名变大的隐藏原因——某些编辑器会把自己的元数据追加在 IEND 之后。理解到这个层面你就知道这个工具的三个边界了第一它只做拼接不做加密真敏感的数据不能这么传第二接收方必须用支持完整复制字节的方式处理图片如果经过重新编码或压缩追加的数据会被丢弃第三它利用的是文件结构特性不是漏洞所以不会触发杀毒软件对“捆绑”的告警但你要是往里面塞跟图片毫无关系的东西容易被平台的内容检测盯上。这些坑在第 3 章会展开讲。2.2 我的实现Python 读二进制、追加字节、自动附加还原说明常规做法用 Python 的二进制读写就能完成绑定不需要第三方库。我这里把每一步的关键代码拆出来你照着就能自己写一个等价的小工具。# bind.py import sys import os import datetime def bind_png(png_path, payload_path, output_path): # 读取 PNG 图片数据 with open(png_path, rb) as f_png: png_data f_png.read() # 读取要绑定的压缩包/任意文件数据 with open(payload_path, rb) as f_payload: payload_data f_payload.read() # 生成一段可读的说明信息追加在压缩包之后 note build_note(payload_path) note_data note.encode(utf-8) # 拼接PNG 原数据 压缩包数据 说明文本 with open(output_path, wb) as f_out: f_out.write(png_data) f_out.write(payload_data) f_out.write(note_data) print(f绑定完成: {output_path}) print(f输出大小: {os.path.getsize(output_path)} 字节) def build_note(payload_path): # 注意这里用固定分隔符还原脚本按这个标记切数据 fname os.path.basename(payload_path) fsize os.path.getsize(payload_path) ts datetime.datetime.now().strftime(%Y-%m-%d %H:%M:%S) return f\n[PAYLOAD_END]|{fname}|{fsize}|{ts}\n if __name__ __main__: bind_png(sys.argv[1], sys.argv[2], sys.argv[3])逻辑说明程序分三步走——把 PNG 读成字节流把压缩包读成字节流两者加上末尾的说明文本直接写进新文件。中间的 build_note 函数做的事情是制造一个还原锚点尾部那段文本不仅告诉你这个图片里藏了哪个文件还给出原文件名和大小提取脚本拿到这个标记就可以精确切出压缩包。参数说明sys.argv[1] 是宿主图片路径sys.argv[2] 是要藏进去的文件路径sys.argv[3] 是输出图片路径。宿主图片建议用 PNG 格式宽度高度不限但注意后续会用二进制方式读取所以任何压缩算法处理的图片都不合适必须保证文件系统里存的是原始字节。输出路径尽量不要和输入路径放在同一个目录避免误覆盖。2.3 为什么选 PNG 而不选 JPG查看器容错差异很多人第一次做这种绑定下意识会拿手边的 JPG 来试结果十有八九翻车。JPG 文件在 EOS结束标记之后追加数据理论上主流看图软件会忽略但实际行为因软件而异有些桌面看图器会弹出“文件已损坏”的警告某些移动端图片库甚至直接拒绝缩略图浏览器端行为更乱。PNG 在 IEND 之后追加数据则相对宽松主流浏览器和操作系统自带的看图工具都能正常渲染兼容性明显好一截。另外一个原因是压缩行为差异JPG 是破坏性压缩原始数据一旦经过任何重编码工具处理就会变化你追加在 EOS 后的数据可能被清理掉PNG 用的是无损压缩只要你不主动做像素重采样文件系统层面的字节不会被编辑。这个工具在实现时也是只认 PNG 宿主图如果你拿 JPG 硬套程序不会报错但还原时大概率拿到残缺数据。3. 使用命令行与包装脚本把工具落地成能双击运行的软件原理清楚了接下来是实际操作。这个工具的使用分两个入口一个是直接跑 Python 脚本适合你自己批量处理另一个是打包成 exe 小软件适合扔给不熟悉命令行的同事和对接方。3.1 命令行三种用法与输出位置说明# 最基本的绑定把 design.zip 绑定到 cover.png输出 out.png python bind.py cover.png design.zip out.png # 绑定时附加说明信息 python bind.py source.png template_pack.rar output_pic.png # 不当心把输入输出放到同目录程序会照常运行但要注意别覆盖宿主图 python bind.py img.png img.png img.png命令行的参数顺序是宿主图片、载荷文件、输出文件。第三种用法是典型误操作把同一个文件既当宿主又当载荷再写回原路径程序不会拦你但输出会变成“图片图片”的叠加体图片打不开载荷也坏了。我一般会在脚本里加一个判断当输出路径和输入路径相同时直接退出并提示。输出位置建议单独建一个目录比如 output/绑定好的图片和原始图片分开存放方便后续验证。这一步没有技术难度但非常影响你的排查效率——很多人在一堆文件里找不到那张“藏了东西的图”最后又重新绑了一遍。3.2 把脚本包装成双击即用的小软件光有 Python 脚本还不够接收方不会装 Python。常规做法是用打包工具把 bind.py 和 extract.py第 4 章的还原脚本分别封装成两个独立的小程序封装时不带控制台窗口绑定和还原都通过文件拖拽触发。# 安装打包器只需要一次 pip install pyinstaller # 打包绑定程序--onefile 生成单文件--noconsole 隐藏黑窗口 pyinstaller --onefile --noconsole bind.py # 打包还原程序 pyinstaller --onefile --noconsole extract.py打包完成后dist 目录下会生成两个可执行文件。把 bind.exe 和 extract.exe 放到同一个文件夹给接收方发一个压缩包里面是这两个程序加上一张示例图片。对方想要还原时把图片拖到 extract.exe 上松开鼠标就能在当前目录看到还原出来的压缩包。参数说明--onefile 表示所有依赖都打成一个文件方便分发--noconsole 会隐藏掉命令行窗口图形界面程序拖拽文件时不会有黑框闪出来。如果打包后 exe 被杀毒软件误报常见做法是加一个 --uac-admin 或调整打包器的版本但更稳妥的方式是直接换用 Nuitka 编译。3.3 使用中的常见问题与排查这一节专门收集我实际分发这个工具时遇到的典型问题全是踩过的坑每一条都按照现象、原因、解决的顺序写。现象 1绑定后的图片在部分手机上打开正常但传到微信再点开还原出来的压缩包损坏。原因微信对图片做了二次压缩和转码发送原图也没用——它在服务端会把图片重新编码你追加在 IEND 后的数据被清洗掉了。解决不要通过聊天软件传绑定图用网盘或文件传输工具直接传文件保证字节不被改动。现象 2用 extract 还原出来的压缩包大小正确但解压提示“文件损坏”。原因宿主图片其实是 JPG 转存为 PNG 的原文件头部是 JPG 结构IEND 标记位置和你预期的不一样。解决用真正的 PNG 作为宿主或者先检查宿主图的文件头确认它是 PNG十六进制开头应显示 89 50 4E 47。现象 3把图片拖到 extract.exe 上程序闪退没有生成任何文件。原因图片不是标准 PNG或者文件在传输过程中已经被改动。解决先看文件大小和宿主图原文件是否一致再用十六进制编辑器查看文件尾部是否包含第 2 章里定义的分隔符。现象 4exe 打包后体积高达 20MB发给别人对方不想下载。原因Python 解释器被完整打包进去了这是规避不了的成本。解决可以用 UPX 压缩壳把体积压到 10MB 以内或者改用 .NET/C 重写一个轻量的命令行版本。现象 5绑定时不想把说明文本追加在尾部但还原脚本按分隔符切数据找不到标记就报错。原因说明文本既是给接收方看的信息也是还原时的切分标记。解决不要删掉 build_note 生成的内容如果你实在不想暴露文件名可以把分隔符改成自定义口令代码里对应改 extract 的匹配规则即可。4. 把图片还原成压缩包提取脚本与边界校验绑定只是半截工作真正让接收方能落地的是还原环节。这个工具把还原设计得足够简单——拿到图片拖进还原程序原压缩包自动出现在当前目录。4.1 提取脚本找到尾部标记切出原始压缩包还原的核心思路是反向拼接读图片的全部字节从尾部往前找分隔符找到后把分隔符之前的部分切出来去掉前面的 PNG 头字节剩下的就是压缩包。实现方式如下。# extract.py import sys import os SEPARATOR b[PAYLOAD_END] def extract_payload(image_path): with open(image_path, rb) as f: data f.read() # 从尾部搜索分隔符 idx data.rfind(SEPARATOR) if idx -1: print(未找到负载标记当前图片可能没有绑定文件) return # 分隔符之前的部分包含 PNG 和压缩包但要找到 PNG 的 IEND png_end data.rfind(bIEND) if png_end -1: png_end 0 # IEND 之后的数据就是压缩包 payload_start png_end 8 # IEND 标记4字节 CRC4字节 payload_data data[payload_start:idx] # 从说明文本里解析原始文件名 note data[idxlen(SEPARATOR):].decode(utf-8, errorsignore) meta_part note.split(|) if len(meta_part) 2: original_name meta_part[0].strip() else: original_name restored_payload with open(original_name, wb) as f_out: f_out.write(payload_data) print(f已还原: {original_name}) print(f大小: {len(payload_data)} 字节) if __name__ __main__: extract_payload(sys.argv[1])逻辑说明程序先读入图片的全部字节从尾部搜索分隔符确定附加数据的存在然后从数据流中间定位 PNG 的 IEND 标记IEND 占 4 个字节其后还有 4 字节 CRC跳过这 8 字节开始提取载荷。这个设计比直接“从尾部刨掉说明文本”更可靠因为它不依赖对附加数据长度的猜测只依赖标准库的标记查找。参数说明SEPARATOR 必须和 bind 阶段保持一致如果你自定义了口令这里也要同步改。extract 脚本对宿主图的要求是文件完整且未被转码如果图片经过任何二次编码rfind 可能找不到 IEND 或者找到了错误位置。还有一种情况——图片里绑定的是多个文件第 5 章会讲这段脚本只能还原最后一个附加段多文件场景需要扩展循环。4.2 ZIP 与 RAR 魔数与恢复校验还原之后的压缩包能不能被解压工具识别取决于文件头魔数是否正确。ZIP 文件以 50 4B 03 04 开头RAR 文件以 52 61 72 21 开头这两个字节串是解压软件的判断依据。# 用十六进制查看还原文件的文件头 xxd restored.zip | head -1 # 应该看到 504b0304如果看到的是 89504e47说明切错了多切了 PNG 的尾部 xxd restored.rar | head -1 # RAR 开头应该是 52617221对应 ASCII 是 Rar!一个常见的翻车点是还原出来的“压缩包”头部带着 PNG 的 IEND 残留解压工具直接拒绝打开。这时候打开十六进制看头部如果是 89 50 4E 47 开头说明提取时没正确越过 IEND 的 CRC 字节。我的代码里用 png_end 8 来跳过这个 8 是 IEND 块标记4 字节加 CRC 校验值4 字节的固定长度不能改。4.3 提取验证干净还原才可用工具里内置的验证机制做得比较简单只对比还原后的大小和绑定前载荷的大小。绑定时会在说明文本里写入原始文件大小提取时做一个等于判断不一致就直接报错。逻辑如下。original_size int(meta_part[1]) restored_size len(payload_data) if original_size ! restored_size: print(警告: 还原大小不一致文件可能已损坏) else: print(大小校验通过)这段代码的价值在于能立刻发现传输链路里有没有“中间商”动过手脚。我之前遇到过一种情况——接收方把绑定图上传到某个图床再下载回来大小没变但中间被加了一段水印 Meta 数据还原出来的压缩包虽然大小吻合解压后却有文件缺失。所以最终验证还是得靠解压工具实测大小校验只能作为第一道防线。5. 给接收方写清楚说明压缩包内容的可读化约定工具做出来不是给你自己用的是要给不懂技术的人用的。接收方拿到一张图片怎么知道它里面藏了东西这是个体验问题同时也是技术问题。5.1 尾部附件的三种形式bind 程序默认会在尾部追加一段明文字符串接收方用文本编辑器打开图片在最后几行就能看到类似这样的内容[PAYLOAD_END]|design_source.zip|31457280|2025-11-09 14:32:00这段文字的作用有两个一是告诉接收方这个图片里确实绑定了文件二是给还原程序提供切分标记。不想暴露文件名的话可以把文件名位置改成“attached_file”但代价是接收方不知道还原出来该叫什么名字。我一般建议保留文件名因为对接方看到文件名就知道是什么素材信任感会强很多。还有一种形式是绑定多个文件时在尾部附加一段清单类似[PAYLOAD_END]|files_list.txt|1024|2025-11-09 14:32:00files_list.txt 里写清楚每个文件的名称和起始偏移量还原程序按偏移量逐个切出。这种方式适合在模板包、素材合集分发时用一个图片承载多份源文件。5.2 压缩率与图片体积的取舍建议绑定后的图片体积等于宿主图加上压缩包体积没有任何压缩收益。所以对打包手法有个建议先把源数据用最高压缩等级压缩成 ZIP再绑定到图片上这样传输体积最小。ZIP 的压缩等级设置在压缩软件里选“最大压缩”RAR 用“压缩最好”模式效果差异根据内容类型浮动文档类能压掉一大半图片类基本压不动。# 推荐做法先压缩后绑定 zip -9 -r source_pack.zip source_folder/ python bind.py cover.png source_pack.zip output.png这里有个小技巧宿主图尽量选有大量渐变和噪点的照片而不是纯色扁平图因为纯色图被社交平台压缩算法盯上的概率更高图片一旦被服务端优化绑定数据就没了。5.3 已验证的边界我实际测试过几类使用场景边界很清楚。本地文件传输U盘、移动硬盘、局域网共享完全没问题图片格式始终是原始字节还原成功率百分百。网盘上传下载基本没问题只要不做在线预览就行——部分网盘会转码图片预览预览一次就有风险。最不可靠的是即时通讯软件和社交平台几乎都会动图片。所以我在这类工具文档里通常会写一句经聊天软件传输后请主动校验还原大小失败就换网盘重传。6. 最后一道保险用十个字节验证还原结果别让坏文件流出去工具做完之后我一直在想怎么让还原过程更不容易出错。后来给 extract 程序加了一个自动校验解压前先检查还原文件的魔数魔数不对直接报错不再生成文件。这个小改动避免了半个小时的排查过程。def check_magic(file_path): with open(file_path, rb) as f: head f.read(4) if head bPK\x03\x04: return ZIP elif head bRar!: return RAR elif head.startswith(b\x7fELF): return 可执行文件 else: return 未知类型魔数校验实际上是在还原脚本退出前做的最后一次防护它能把“切错位置”这类低级错误直接挡在前面。以前有一次给某公司交付素材包我用的是绑定测试图还原出来头两个字节是 89 50明显是 PNG 的残留头但我没查就直接发给了对接方对方解压报错来回解释半天才弄清楚是切分没到位。从那以后我每次跑完还原脚本都会强制走一遍魔数校验就十行代码的事却能拦下最容易忽略的坑。另外一个习惯是把校验通过的文件再解压一次看是否报“文件损坏”。两步验证做完再外发基本没有返工的情况。这个小工具的价值不在于技术多高深而在于它把常见的资源传输痛点压缩成了一个双击动作。如果你也经常被素材包、模板包、参考文件的分发搞得头大这个思路值得一试——先在一张普通 PNG 上测试一遍绑定和还原流程再拿去应对真实的工作场景希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站