你每天扫的二维码,可能早就被人掉了包

你每天扫的二维码,可能早就被人掉了包

安全二维码生活

数据源:Lobsters + web research · HN

你每天扫的二维码,可能早就被人掉了包

各地警方通报过不止一起这样的案子:小摊贩一觉醒来,发现顾客扫了一整天的收款码,钱一笔都没进自己账户。贴在那儿的还是那张码,只是里面的「收款人」,早换成了别人。偷换一张收款码有多简单?打印一张纸,趁没人注意撕下旧的、贴上新的,全程几十秒,成本几分钱。你每天扫码付款、扫码加好友、扫码连 WiFi、扫码点餐——但你大概率从没想过一个问题:这个码,是谁贴的?

这个问题的答案,比大部分人以为的严重。这几天国外技术社区 Lobsters 上,一篇教人「别把二维码搞砸」的设计指南拿到了 104 分的高热度,评论区里最扎眼的却是几条安全警告。设计者关心的是码好不好看、扫不扫得出来;安全研究者关心的是另一件事:扫出来之后,会发生什么。笔者想把这层窗户纸捅破。

二维码是什么:一张印出来的网址

先说清楚二维码的本质。它看起来是密密麻麻的黑白方块,实际内容是一段文字——绝大多数时候是一个网址——被编码成了图案。你的手机扫它,做的只是把图案翻译回那段文字,然后打开。换句话说:二维码就是一张印在纸上的网址。

因为它是「印出来的网址」,所以谁都能造。网上免费的二维码生成器一抓一大把,输入任何文字,一秒出图。打印一张纸的成本,几分钱。它没有任何类似钞票水印、防伪线的东西——所有二维码长得都「差不多」,人眼根本分不出哪张是正品。

这就要说到那篇设计指南了。作者 Rebane 花了不少心思讲规范:码周围要留白边,不然有些扫描器读不出来;可以用深浅两色重新上色,但别把黑白反过来;纠错级别调高以后,最多能容忍约 30% 的面积被盖住或损坏;他拿一根只有 1 像素粗的「小虫子」画上去,码当场报废,而一条粗壮的大蛇盘在上面,依然扫得出来——因为纠错是按整块数据计算的,碰坏一块的一丁点和碰坏一整块,后果一样。作者还用 GrapheneOS、三星、iOS、Binary Eye 四类设备逐一实测。

这些规范有价值,但请记住一句工程判断:它们保证的是「能扫出来」,保证不了「扫出来的是什么」。 设计指南解决的是可读性,和可信性没有半点关系。

一张普通收款码的示意图 图:一张普通收款码的样子。来源:本站生成示意图

为什么二维码没有防伪能力

网络安全里有个概念叫「认证」——怎么证明一个东西确实是它声称的那个东西。钞票靠印刷工艺和油墨,网站靠证书和加密。二维码呢?什么都没有。它只是一串原始数据,谁都能原样复制,谁都能重新生成,谁都能打印出来贴在任何地方。

Lobsters 评论区里,用户 BenjaminRi 把这件事说得最透:只要二维码还只是一串没有任何认证机制的原始数据,它就永远不可能安全——安全性完全取决于有没有人物理上替换或修改它。另一位用户 freddyb 补了一刀:这和把网址印在纸上(甚至是短链接)没有本质区别。

想想看:你在浏览器里打开一个网址,会看地址栏、会认域名、会警惕弹窗。但你扫码的时候,什么都没看。你把自己的信任,交给了一张谁都能打印的纸。

被掉包后的收款码示意图 图:同一位置被换上的假码——肉眼几乎看不出和上一张的区别。来源:本站生成示意图

偷换攻击是怎么发生的

「偷梁换柱」早就是反复上演的事实。几种常见剧本:

收款码被掉包。 小摊、菜市场、小卖部最常中招。商家把收款码贴在摊位上,人来人往,小偷趁店主忙乱时贴上新码。顾客扫码、输密码、付款,钱进了小偷的账户。商家往往要等到晚上对账才发现,那时候小偷早跑了。

共享单车和停车场的码被覆盖。 车身上的骑行码、停车场的缴费码,都是现成的「广告位」。贴一张自己的码盖上去,扫的人还以为是官方的。

钓鱼码。 伪装成「扫码领红包」「扫码连免费 WiFi」「扫码看菜单」的贴纸和海报,扫出来是钓鱼网站、恶意下载链接。这类码甚至不用替换任何东西,往人多的地方一贴就行。

高纠错级别是帮凶。 还记得那个「能容忍 30% 损坏」吗?小偷贴歪了、盖不严实,码照样能扫。设计者为了让码更耐脏、更好看而调高的纠错级别,在攻击者手里成了容错空间。

把这几条摆在一起,账就很清楚了:攻击成本几分钱、几十秒,受害面却是全天所有扫码的人;而防守方靠的,往往只是「顾客多看一眼」。

被贴纸盖住一角的二维码 图:纠错级别高的码,被贴纸盖住一角照样能扫——但扫出什么,码本身不负责。来源:本站生成示意图

为什么设计规范救不了安全

那篇指南的评论区里,ptman 的警告,笔者想单独拎出来说说:把静态二维码用在安全相关场景,是愚蠢的。码应该是限时的、会过期的。他的理由朴素到残忍——攻击者只需要把码换成另一个码,物理世界里的替换,没有任何技术门槛。

这就是为什么现在很多正规商家改用「动态码」:收款码不再打印出来贴在墙上,改成在屏幕或收银设备上展示,隔一会儿就刷新一次。刷新的码没法提前打印、没法偷换——小偷贴一张静态纸码上去,几分钟后它自己就失效了。动态码对抗偷换的武器,就是「过期」。

顺带一提,安全研究者还在担心更复杂的事:比如「中间人攻击」——扫码的那个设备本身是不是可信的。但对我们普通人来说,先记住最简单的几条就够了。

扫码之前,先看一眼这几件事

  1. 付款前核对收款方。 扫码后弹出的页面里,收款方名字是不是商家本名?金额对不对?这一眼比研究码本身重要得多。
  2. 用 App 自带的扫码功能。 微信、支付宝内置的扫一扫会做安全检测,跳转可疑链接前会有提醒。别用手机相机的扫码功能去扫来路不明的码,更别装「扫码才能用的第三方 App」。
  3. 警惕「贴纸叠贴纸」。 码的边缘翘起、摸上去明显有两层、颜色和周围不协调、像是刚贴上去的——都别扫。真商家不会三天两头换码。
  4. 扫码后要看你被带去了哪里。 让输密码、输验证码、让你下载 App 的页面,先停手,退出重来。
  5. 商家朋友们: 收款码别贴在顾客伸手就能够到的地方,用屏幕展示动态码,或者把码放在柜台内侧。每天收摊前对一次账。

写在最后

二维码是便利的胜利。它把「输入一长串网址」压缩成了「扫一下」,让支付、点餐、连网都快得不像话。但便利和安全这笔账,一直有人在算:设计者在算怎么让码更好看,攻击者在算怎么让码指向自己,安全研究者在算怎么让码「会过期、能认证」。夹在中间的,是每天掏出手机扫一下的普通人。

笔者写这篇,无意劝谁放弃扫码——二维码的便利是实打实的。只想把那张纸的底细讲清楚。下次扫码付款前,花一秒钟问自己一个问题:这个码,是谁贴的?

参考链接:

  • 原文: Guide to (not) fucking up QR codes(Mastodon, @rebane2001)
  • Lobsters 讨论 (s/omcyaz):别把二维码搞砸指南,104 分,11 条评论