被裁剪的手机截图,残留内容以幽灵形态挂载在文件尾部

CVE-2023-21036 · aCropalypse

你裁掉的东西
还在文件里

2023 年 3 月被公开的一个 bug:Pixel 自带的截图编辑工具写文件时,
少了一个字母 t。五年内被你「裁掉」的半张图,可能连同银行卡号、
身份证、聊天记录,原样留在文件尾。

造一张会漏的截图 →

所有解析都在你的浏览器里跑,文件不上传、不联网。

零门槛 · 选一个场景看现场

模块一 · 犯罪现场

三张伪造的截图。点哪个就裁哪个,看「裁掉一半画面,文件一个字节都没变小」是怎么发生的。

原图 · 完整截图
裁剪后 · 你以为文件长这样
—
被裁掉的画面比例
—
文件体积变化(buggy 文件 vs 原图)
—
残留字节数

本该只有 — B,实际 — B

裁掉 55% 的画面,文件一个字节都没变小。

本页样本使用未压缩 IDAT 存储块,体积与像素数成正比;真实 PNG 的压缩比会不同。

一个字母 t 的差别

模块二 · 一个字符的距离

Markup 工具写裁剪结果时,调用了 Android 的 ParcelFileDescriptor.parseMode(...)。传 "w" 和传 "wt",差一个字母,行为却完全相反。

Bug 写法:parseMode("w")
final ParcelFileDescriptor pfd =
  ParcelFileDescriptor.parseMode("w");
Android 10 起,"w" 不再隐含截断。调用方代码没改,行为却悄悄变了。
正确写法:parseMode("wt")
final ParcelFileDescriptor pfd =
  ParcelFileDescriptor.parseMode("wt");
"wt" 显式声明「先截断再写」,文件被清空,新数据只占自己的长度。
点下面的「写入」按钮,把裁剪结果以 bug 模式写回原文件,看字节带的变化。
原文件
新数据
buggy 文件
写入模式 vs 截断模式对比

分步解析 · 四个按钮依次点亮

模块三 · 找到 IEND

每个 PNG 都是「8 字节签名 + 一串 chunk」。解码器读到 IEND 这个 chunk 就停下,IEND 之后挂多少字节都不影响显示 —— 这就是残留看不见的原因。

步骤 3.1 · 校验 8 字节签名

合法的 PNG 一定以这 8 个字节开头:

89 50 4E 47 0D 0A 1A 0A
—

分步 · 五步对应算法核心

模块四 · 把残留拼回图像

PNG 编码器按扫描线写入,每行 1 字节 filter + 宽 × 3 字节 RGB。只要猜对原图宽度,残留字节立刻对齐成一幅画面。

1 找 IEND 边界
点下面的按钮,扫描到 IEND。
2 找 IDAT 起点
顺序扫描 ASCII "IDAT",按长度取数据并校验 CRC32。
3 收集 IDAT 载荷
从 IDAT 起点正常遍历 chunk,把所有 IDAT 拼起来直到 IEND。
4 zlib 存储块重同步
载荷是被切断的 zlib 流。逐字节找合法存储块头,第一个就停下。
5 猜宽度,对齐扫描线
选一个宽度,看对齐后能不能组成画面。
原图
裁剪后
恢复结果
—

简化说明:本演示的 IDAT 用 zlib「未压缩存储块」写,让「从任意字节处接着解」变得确定可算。 真实漏洞的恢复还要额外做 DEFLATE 比特级重同步(Buchanan 的文章讲的就是这一步)。 其余流程与真实漏洞完全一致。

眼见不为实

模块五 · 为什么所有看图软件都看不出来

把带残留的 buggy 文件塞进 <img> 看一眼 —— 显示完全正常。再看字节总数,残留和 IEND 之后的内容都算在文件大小里,但只有你和我知道它们在那儿。

带残留的 buggy PNG,浏览器显示完全正常
—

这张图是用我们自己的编码器造的 buggy 文件,残留字节数 = —,等于把 IEND 之后的内容原样保留在磁盘上。

任何 <img>、任何看图软件、任何 CDN 都会把它当成合法 PNG 来显示,没有任何警告。

聊天截图发到 Discord 后,残留被还原出完整银行卡号

文件不上传,全部在浏览器里解析

模块六 · 测你自己的截图

拖一张本地 PNG 进来,纯前端解析。看到残留字节数;能恢复就试一下。绝大多数现代设备已经打补丁,0 字节是常态 —— 那也值得你点一下确认。

拖拽 PNG 到这里,或

不联网 · 不上传 · 仅在你的浏览器内解析

只列已证实条目

模块七 · 时间线与补丁表

Google · Pixel CVE-2023-21036
  • 触发组件:Pixel 自带截图工具 Markup(parseMode("w"))
  • 根因:Android 10 起 parseMode 不再隐含截断,调用方代码没改
  • 影响时间:约 5 年,Markup 自 Android 9 / Pixel 3 起随机附带
  • 公开者:Simon Aarons 与 David Buchanan,2023 年 3 月
  • 补丁:2023 年 3 月安全更新(Android 13 QPR2 / Pixel 补丁 2023-03)
Wikipedia: aCropalypse →
Microsoft · Windows CVE-2023-28303
  • 触发组件:Windows 11 截图工具、Windows 10 Snip & Sketch
  • 评级:MSRC Low(CVSS 3.3)
  • 修复版本:11.2302.20.0 / 10.2008.3001.0
  • 公开者:David Buchanan
MSRC 官方公告 →
传播放大器 行为差异
  • 保留文件:Discord 等平台原样保留上传文件,残留会一起被带走
  • 重新编码:会重新压缩的 CDN / App 顺手把残留抹掉,文件反而变干净
  • 银行卡案例:Aarons 在 Discord 上公开还原的演示就是这种情况

把自己能做的列清楚

模块八 · 防御清单

另存为新文件

永远不要在原文件上覆盖。哪怕是「裁剪」也要「另存为」到新路径,原文件保持不动。

分享前重新编码

把要发的图先转存成 JPEG 一遍,让任何残留都被压缩抹平。JPEG 本身就是重新编码。

打码靠像素化 + 重编码

不要靠「盖图层」打码。盖的图层在原图文件里还在。必须把敏感区域像素化后再重新保存为新文件。

留意体积异常

裁掉了 90% 的画面,文件却只小了几个百分点,这本身就是个危险信号。怀疑了就别发。

把这一页的要点发到群里,让身边的人也少踩一次坑。