在不同设备上显示不一样内容的神奇 PNG 图片

在不同设备上显示不一样内容的神奇 PNG 图片

最近在 PNG Parser Differential 看到一个神奇的 PNG 图片,在不同的设备/环境下能显示不同的内容,具体来说在 Apple 设备上用系统的图片解码器能显示的内容(Safari 浏览器或者在访达预览)与其他地方(谷歌浏览器、Android 或 Windows)显示的不同

在 Safari 或 访达中你会看到 “Hello Apple”,在 Chrome 中会显示 “HELLO WORLD”
在 Safari 或 访达中你会看到 “Hello Apple”,在 Chrome 中会显示 “HELLO WORLD”

这就非常神奇,因为 PNG 图片格式并不存在这样的能动态显示内容的特性,如果能复用这个特性就可以实现一些有趣的事情,比如一个下载包的说明图片能给 MacOS 用户显示专有的的内容、在不支持脚本的的网页中给不同系统显示不同内容、在微信群里筛选 Apple 用户、当然还有创造更难以被发现的图片隐写术......

Apple 中可以看到一只黄色鸭嘴兽
Apple 中可以看到一只黄色鸭嘴兽
这张图可以检查你是否是 Apple 用户
这张图可以检查你是否是 Apple 用户
iOS 微信中
iOS 微信中

原理

在了解一番后,弄明白了其中的原理,简单来说是 Apple 的 PNG 解码器有一个私有的功能:通过 iDOT 进行并行解码,而其存在一个 BUG 或者说设计不严谨的地方

这原本是 Apple 为了 PNG 解码速度而做的一个优化 。按照 PNG 规范 ,PNG 图片分为多个数据块,图片内容的像素流会使用 zlib.deflate 算法压缩后存储在 IDAT 块中。按照标准可以把所有数据存储在一个大的 IDAT 块中,也可以分割存储在多个 IDAT 块中,这原本是为了编码解码时可控内存占用量而设计的,而没有考虑到并行解码,也就是说在对每一个 IDAT 块解压前并不会知道到其中包含了多少像素 。

苹果为了让 PNG 格式的图片可以并行解码并且兼容原本的 PNG 标准,把图片内容分为「前半」与「后半」2 部分,并把像素流分为多个 IDAT 块,然后在 PNG 文件里添加了一个 iDOT 辅助块,其中记录了「前半」「后半」的分隔位置(「后半」数据相对 iDOT 块的偏移值)和解压后的像素高度,这样用 Apple 的 PNG 编码器编码的 PNG 图片用 Apple 自家的 PNG 解码器解码就可以分为 2 个并行的过程,以此充分利用多核性能,同时这样的 PNG 图片也能被普通解码器解码

Apple 的 PNG 图片中的 iDOT 块
Apple 的 PNG 图片中的 iDOT 块

那要如何利用这点让 Apple PNG 解码器解码出不一样的内容呢? 这个问题可以转化为让顺序解压的数据与并行解压后把数据拼一起的内容不一样:

解压(上半 + 下半) != 解压(上半) + 解压(下半)

实现的原理非常的巧妙,就是通过利用 zlib 的非压缩块特性:非压缩块标识可以在压缩数据中指定一块区域在解压时忽略掉它。如果把非压缩块标识放到「上半」的末尾,而把被标记的数据放到「下半」的起始,当单独解压「下半」时数据不会被忽略,而「上半」「下半」连在一起解压时,这些数据就会被忽略,这样就可以让数据在并行压缩被用到而顺序压缩时被忽略

上半 + 下半 =  [数据, 非压缩标识, 秘密数据,数据] // 非压缩标识起作用把秘密数据忽略了
上半 = [数据, 非压缩标识] // 非压缩标识在这没用
下半 = [秘密数据,数据] // 秘密数据被解压了

这个技巧相当的巧妙以至于 Apple 也没有想到,在 Apple 系统(MacOS、iOS)只要利用了系统的 PNG 解码器的软件都会受到影响(MacOS 上 Chrome 浏览器自带 PNG 解码器所以不受影响)

生成工具

这个问题的发现者提供了一个工具用来生成这样的图片,不过要使用它比较麻烦,要安装 Python 和通过命令行,我把它搬到了网页上并且让它更容易使用

使用起来非常简单,选择 2 张图片后点击生成即可

Apple 设备显示的图片 一般设备显示的图片
iOS 相册中大图 iOS 相册中缩略图
iOS 微信中大图 iOS 微信中缩略图
安卓不显示 安卓显示
Windows 中不显示 安卓中显示

小技巧

如何更清晰

生成后图片难免有一些混杂,可以选择图片内容简单的、色彩数少的原图

如何用微信发给朋友

在微信中需要用发送原图、对方点击「查看原图」才能让对方看到效果。另外要注意如果微信发送的图片尺寸太小它也会强制使用缩略图,所以生成的图要大一点才行(我试了试大概大于 1000x1000 没问题)

去推特留言