手机验证码登录是一种常见的应用登录方式,简单方便,不用记忆密码,市面上能见到的App基本都支持这种登录方式,很多应用还把登录和注册集成到了一起,注册+登录一气呵成,给用户省去了很多麻烦,颇有一机在手、天下我有的感觉。
登录原理
手机验证码登录的原理很简单,对于一个正常的登录流程,看下边这张图就够了:
实际应用中会存在一些收不到验证码的情况,可能的原因如下:
- 在手机端,短信被某些软件认为是垃圾信息而被拦截或者删除,或者因为手机卡欠费导致收不到短信。
- 在应用服务端,因为程序错误,或者安全控制策略导致部分短信发送失败。
- 在短信平台或者电信运营商系统,因为黑名单、关键字、流量控制,或者其它某些技术原因导致发送失败。
针对收不到短信的问题,系统中会增加重发验证码功能,如果多次重发还收不到,系统可以支持上行短信或者语音验证码的方式,这两种方式都是短信验证码的变种。
- 上行短信是让用户将系统提前生成好的若干字符发送到系统指定的短信号码,据此可以验证用户拥有指定手机的控制权,从而也就认证了用户的身份。
- 语音验证码可以让用户发起,也可以在系统收到短信发送未成功的回执时主动推送,用户手机会收到一个自动语音通话,其中包含登录所需的验证码。
安全风险和应对策略
手机验证码的安全风险主要是被恶意利用和窃取。
因为手机验证码的应用十分广泛,为了有一个更全面的认识,这里说的安全风险没有局限在登录这一点上,所有使用手机验证码的场景都可能存在。这里的应对策略主要是站在系统开发者的角度,通过各种技术方案来解决或者降低手机验证码的安全风险。
短信诈骗
诈骗者先获取到用户手机号,然后冒充金融机构、公权力部门、亲朋好友,在应用中输入用户手机号请求验证码后,向用户索要对应的手机验证码,用户稍不注意可能就会造成金钱损失。
针对此类问题,系统开发者可以考虑如下一些方案:
- 在验证码中声明:工作人员不会索取,打死也不要泄露给别人。不过人在一些特殊情况下是不会理会这些警告的。
- 跟踪用户的常用登录特征,比如获取验证码时的设备、IP、wifi、地域不是常用的,系统就可以马上短信或者语音通知用户可能存在安全风险,请谨慎操作;系统还可以直接升级安全级别要求更多的验证方式,比如需要再次获取验证码、输入安全码、刷取指纹、识别人脸、插入U盾等等验证方式。
还有一种相对隐蔽的诈骗方式,诈骗者直接向用户发送仿冒钓鱼网站的地址,用户在钓鱼网站获取验证码时,诈骗者拿着用户手机号去真实网站请求验证码,此时用户会收到一个真实的验证码,用户在钓鱼网站输入验证码后,诈骗者就可以拿着这个验证码去真实网站使用。
针对这种情况,前边的识别用户常用登录特征的方式仍然有效。此外短信平台和电信运营商也有责任对短信内容进行把关,短信平台需要验证发送者的真实身份、审核短信内容,并提供动态的流量控制机制,这样可以过滤掉绝大部分诈骗短信。
其实电信运营商是能够识别手机位置的,如果电信运营商能够提供一种安全的位置认证服务,也可以解决大部分验证码诈骗问题,比如前端提交验证码认证时携带电信运营商提供的位置标识,应用服务商可以拿着这个位置标识去找电信运营商验证位置,当然这只是一个设想,现实中还没有这种方法。
短信攻击
可能有两种场景下的短信攻击:
- 用户在前端不停地点击获取验证码,可能是担心收不到验证码,也可能是失去了等待的耐心,也可能是恶意向别的手机号发送。
- 攻击者直接调用发送验证码的接口,在极端的时间发送大量验证码请求,可能是发给某个用户也可能是一批用户。
此类操作首先会浪费短信资源,给应用服务商造成损失;恶意攻击还会向无辜的用户发送大量短信,造成骚扰攻击。
应对这种问题,可以考虑如下一些方案:
- 增加其它验证。 获取短信验证码之前必须先通过这些验证,比如图形验证码、滑动验证码、数学公式验证码等等。这些方式可以增加发送短信验证码的难度,降低人工的发送速度,尽量避免机器人自动操作。
- 对操作进行限流。 比如现在前端常见的发送短信验证码倒计时,一般每次请求验证码后经过若干秒才能再次发送。因为如果攻击者获取到了发送验证码的服务接口,就可以摆脱前端逻辑的限制,所以后端也可以采用同样的策略,对设备Id、手机号、IP、用户、业务类型等等,以及它们的各种组合,进行频率控制。应用开发者还可以根据发送结果特征来进行控制,比如空号率,如果空号太多则说明可能是机器人随机生成的手机号。在单一频率的限制基础之上,还可以增加更多的时间控制,在分钟、小时、天等时间维度上做不同的阈值限制。
- 给用户提供一个短信退订入口。 用户频繁收到非自己主动发起的验证码短信时,可以提供一个退订入口,让用户在短时间内关闭短信验证码,应用服务此时可以忽略给用户发送验证码的请求,或者直接去掉发送验证码的功能入口。
但是这种控制要尽量以不影响用户的正常业务操作为前提,否则就得不偿失了。
- 比如图形验证码的难度不要太高,毕竟大部分业务不是12306,你照搬过来可能就会弄巧成拙。
- 再比如对于限流控制,假设正常用户一般只在一天的某些时候进行操作,不会一天24小时都在做某一件事,则可以这样做:每个手机号每小时只能发送X次,每天只能发送Y次,这两个数值要符合 X>Y。
- 对于严重的攻击,应该设置熔断机制,此时不得不牺牲可用性。比如短时间涌入了大量针对不同手机号的验证码需求,很可能是受到了DDoS攻击,因为资源有限,此时正常用户的操作也会受到影响,可以依托全局限流,触发限流时直接关闭验证码服务一段时间。
网络窃听
假设用户收到了登录验证码,输入正确后提交服务端验证。在从手机端到服务端的传输过程中,会经过很多的网络设备和服务器系统,登录提交的内容有被拦截获取的可能,此时攻击者就可以阻断请求,自己拿着用户的手机号和验证码去登录。
应对这种问题,一般需要对网络传输内容进行加密,比如现在常用的https通信,可以保证两端之间的传输内容安全,不被窃听。对于传输安全,一般这样处理也就够了。
不过https也不是银弹,如果有攻击者在客户端偷偷导入了自己的证书,然后让网络请求都先通过自己进行代理,再发送到目标地址,则攻击者还是能够获取到请求内容,想体验这种方式的可以使用fiddler试试。还有https证书存在错发的可能,如果给攻击者发放了别人的证书,此时安全传输也就没什么意义了。
为了更高的安全性,传输内容可以在应用中加解密,客户端对要传输的数据按照与服务端的约定进行加密,然后再发送到网络,攻击者截获后,如果没有有效的解密手段,则可以保证数据不被窃听。加密的重点是保证密钥安全,不被窃取和替换,可以采用其它安全信道传输,甚至线下传递的方式。对于验证码这种仅做验证的数据,还可以通过加盐后进行慢Hash运算,攻击者即使拿到了传输内容,要进行破解的难度也相当巨大。
本地窃听
如果系统上安装了恶意软件或者非官方版本的软件,特别是在盗版系统、被Root或者越狱的手机系统中,攻击者也能比较容易的拦截并窃取短信验证码;同时网络窃听中的加解密也可能失去作用,因为软件已经不可信,在不同的操作之间有没有发生什么猫腻,很难确定。
最近几年在移动设备上引入了一个称为可信执行环境(简称TEE)的概念,独立于操作系统,单独的应用,单独运行,有的甚至有单独的处理器和存储,外部很难进入和破解。一些关键的操作都封装在这里边,比如指纹的采集、注册和认证,密钥的生成和使用,版权视频的解码和显示,等等。如果把短信验证码的处理也放在这里边,无疑会安全很多,不过这要解决很多通信方面的问题,收益与成本可能不成正比。在台式机中这一技术还所见不多,可能台式机的环境已经有了比较成熟的安全体系,不过从移动端迁移过来的难度应该也不大。
短信嗅探
短信嗅探也是一种窃听技术,不过是通过攻击电信网络通信的方式。
现在手机一般都使用4G、5G网络了,但是“短信嗅探”技术只针对2G网络,不法分子通过特殊设备压制基站信号,或者选择网络质量不佳的地方,或者使用4G伪基站欺骗手机,这会导致网络降频,使手机的3G、4G通信降低到2G。
2G网络下,只有基站验证手机,手机不能验证基站,攻击者通过架设伪基站,让目标手机连接上来,然后就能获取一些连接鉴权信息,再冒充目标手机去连接真基站,连上以后拨打攻击者的另一个手机,通过来电显示得到目标手机号码。
基站本身并不会用特定方向的信号与每部手机通信,而是向四周以广播的形式发送信号。所以每部手机实际上也是可以接收到其他手机的信号,2G网络传输数据时没有加密,短信内容是明文传输的,就可以嗅探到目标手机的短信。加之2G通讯协议是开源的,所以这件事的技术门槛并不高。
因为这种攻击要求手机不能移动,如果基站切换就没用了,所以攻击一般选择夜深人静的时候。对于普通用户来说,睡觉的时候可以选择关机或者开启飞行模式;另外开通 VoLTE ,可以让电话和短信都是走 4G 通道,不过网络降级很难防范;或者买个能识别伪基站的手机,不过没办法保证百分百能够识别;或者就只能等着移动运营商关闭2G网络了。
对于应用系统开发者,应该认识到通信通道的不安全性。必要的时候开启双因子验证,除短信验证码外还可以使用短信上行验证、语音通话传输、专用密码验证、常用设备绑定、生物特征识别、动态选择身份验证方式等等多种二次验证方法。
重放攻击
假设某些交易服务需要通过短信验证码来验证用户的身份。如果有攻击者截获了交易请求报文,然后多次发送到服务端,服务端仅检查了验证码是否正确,则可能实际发生多次交易。此时攻击者都不需要解密传输内容。
此时应该限制验证码只能够使用一次,服务端收到交易请求时首先检查验证码,检查通过后将验证码置位或删除,然后再处理交易,不管交易是否成功,验证码都不能再次使用。另外还应该在生成验证码时设置一个较短的有效期,如果用户没有实际提交,攻击者也必须在有效期内才能使用,增加攻击难度。
当然你也可以使用更通用的防重放手段,比如每次请求验证码都先从后端获取一个随机数,随机数如果已经使用过则不能再次使用,随机数如果不存在也不能处理请求。当然随机数也可以在前端生成,服务端如果收到了重复的随机数则拒绝请求,但是需要防止传输过程中随机数不被篡改,可以通过密钥签名的方式。