短信验证码接口的CC攻击,本质上不是拼带宽,而是拼谁能把对方的短信余额更快耗尽。一条国内验证码短信的成本通常在0.03元到0.06元之间,看似不起眼,但攻击者用并发请求打进去,一分钟烧掉几千条,一小时就是上万元的真金白银。攻击者要的不是把服务器打趴下,而是让短信通道欠费停服,或者让运营商把号码标记成垃圾通道直接拦截。所以做成本控制,核心逻辑是:在攻击流量到达短信下发这一环之前,用最低的计算资源把恶意请求过滤掉。
真正有效的防御,不是堆机器硬抗,而是把决策点前置。前置的意思就是,在请求进入业务逻辑之前,甚至在TCP连接建立之后、HTTP请求体还没读完的时候,就判断这个请求值不值得继续处理。这个思路下,成本控制的抓手有三个:客户端行为校验、协议层指纹、以及业务层风控模型。三者配合起来,才能把恶意请求的通过率压到千分之一以下,同时把正常用户的误伤率控制在可接受范围。
客户端行为校验:用JS Challenge把攻击成本翻倍CC攻击工具大多不是完整浏览器,而是基于Python requests、Go http库或者Node.js的脚本。这些脚本不会执行JavaScript,也不维护DOM环境。利用这一点,可以在用户点击“获取验证码”按钮时,由前端注入一个动态计算任务。具体做法是:服务端下发一个随机数和一段混淆过的JS算法,要求客户端在200毫秒内返回正确的计算结果。这个算法可以简单到把随机数做几次位移和异或,也可以复杂到模拟一个小型哈希函数。关键点是,这个JS片段每次请求都不一样,攻击者想硬编码绕过,就得把V8引擎嵌进脚本里,运行成本直接拉高几十倍。
实现上,前端可以用Web Worker在后台执行计算,不阻塞UI。服务端把Challenge和验证码下发接口绑定,同一个Session下必须先通过Challenge校验,才允许调用短信发送接口。对于没有Session的首次请求,可以用Token绑定:服务端生成一次性Token,前端拿到Token后完成计算,再用Token换取短信下发权限。攻击脚本如果直接请求短信接口,没有有效Token,请求在网关层就被丢弃,连业务逻辑都进不去。
协议层指纹:从TLS握手就开始识别大部分CC工具为了追求发包速度,会复用TLS会话或者使用特定TLS指纹。JA3和JA4指纹是目前识别客户端TLS特征的成熟方案。通过Nginx的nginx-module-vts模块或者OpenResty的lua-resty-waf,可以在SSL握手阶段提取Client Hello包中的密码套件列表、椭圆曲线参数、压缩方式等字段,计算出JA3指纹。已知的Python requests库、Go net/http库都有固定的JA3指纹,把这些指纹加入黑名单,可以直接在四层拒绝连接,连七层资源都不消耗。
更进一步,可以在TLS层面做主动探测。比如服务端故意在Server Hello中返回一个非标准的椭圆曲线,正常浏览器会忽略并继续握手,但很多脚本库会直接断开连接。这种协议层面的不一致性,是区分真浏览器和脚本的极好信号。部署时,可以在Nginx前面挂一层HAProxy或者直接使用支持TLS指纹检测的四层网关,比如Caddy的Layer4插件或者自研的eBPF程序,在TCP握手完成后立刻判断,恶意连接直接RST,不进入后续的HTTP解析流程。
频率控制:用滑动窗口加令牌桶做精准限流单纯的“同一IP一分钟内最多发5条”已经不够用了,攻击者手里有上百万的代理IP池。有效的频率控制必须基于多维特征:设备指纹、手机号段、IP段、甚至请求时间分布。设备指纹可以用canvas指纹、WebGL指纹、字体列表等前端信息生成,存储在客户端LocalStorage中。服务端用Redis维护一个滑动窗口计数器,Key是设备指纹加手机号哈希,窗口大小设为60秒,用ZSET实现,每次请求时先清理过期记录,再判断当前窗口内请求数是否超过阈值。
这里有一个容易被忽视的成本点:Redis的ZSET操作本身也有开销。如果攻击者用大量不重复的设备指纹打过来,Redis内存会迅速膨胀。解决办法是对设备指纹做布隆过滤器预判,只有布隆过滤器中已存在的指纹才进入滑动窗口逻辑,全新的指纹先进入一个观察窗口,观察期内请求频率超过阈值的直接拉黑,没超过的才加入布隆过滤器。这样可以把Redis的内存增长控制在线性范围内,同时避免恶意指纹污染缓存。
手机号风控:空号检测和号段画像攻击者往往使用接码平台或者虚拟号段的号码来触发短信,这些号码的特点是:号段集中、在线时间短、历史上被大量不同IP请求过。接入空号检测API是基础操作,但空号检测本身有成本,每次检测大概0.01元到0.02元。如果对每个请求都做空号检测,等于又开了一个花钱的口子。正确的做法是先用本地缓存和规则过滤:把已知的虚拟号段、物联网卡号段做成前缀树存储在内存中,请求进来先匹配号段,命中虚拟号段直接拒绝,不触发空号检测。只有号段正常、但行为可疑的号码才走空号检测流程。
更进一步,可以自建号码行为画像库。每次短信下发成功或失败,记录号码、IP、设备指纹、时间戳。积累一定数据量后,用简单的规则引擎就能识别出规律:同一个号码在5分钟内被3个以上不同IP请求过,大概率是接码平台;同一个IP在10分钟内请求了20个以上不同号码,且号码号段分散,大概率是攻击脚本。这些规则可以用Flink或者自研的流处理程序实时计算,结果写入Redis供短信接口实时查询。
验证码形态升级:从短信到无感验证短信验证码最大的问题在于,它是一条必须花钱才能送达的信息。如果能把验证方式从“下发短信”变成“客户端自证”,成本直接降为零。无感验证的核心是:让客户端证明自己是一个真实设备,而不是模拟器或脚本。具体技术路径有两条:一是调用系统级安全API,比如Android的SafetyNet Attestation或iOS的DeviceCheck,由操作系统层面证明设备完整性;二是用行为生物特征,比如用户在页面上滑动、点击的轨迹特征,判断是否是人类操作。
行为生物特征的做法是:在发送验证码按钮上绑定touchstart、touchmove、touchend事件,采集手指按下的压力值、滑动轨迹的加速度、角度变化等数据。把这些数据序列化后发给服务端,服务端用一个轻量级的随机森林模型判断操作是否来自人类。这个模型的推理速度很快,单次判断在1毫秒以内,可以部署在Nginx的Lua插件中,不增加额外延迟。攻击脚本模拟这些行为特征的成本极高,因为需要理解前端采集逻辑并生成符合人类行为分布的数据,这比单纯发HTTP请求难得多。
架构层面的成本隔离:短信接口独立部署和资源限制很多中小公司把短信接口和用户中心、订单系统放在同一个服务集群里,一旦短信接口被打,整个集群的数据库连接池、线程池都被占满,其他业务跟着瘫痪。正确的做法是把短信接口拆成独立微服务,使用独立的数据库连接、独立的Redis实例、甚至独立的出口IP。这样即使短信服务被CC攻击打爆,也不会影响核心业务流程。独立部署后,还可以对这个微服务设置严格的资源上限:最大QPS、最大并发连接数、最大内存使用量,超过上限直接熔断,返回友好提示而不是继续消耗资源。
熔断策略上,建议用Netflix Hystrix或者Sentinel的线程池隔离模式,给短信下发单独分配一个固定大小的线程池,比如20个线程。当线程池满时,新的请求直接走降级逻辑,返回“系统繁忙请稍后再试”。这个线程池大小根据短信通道的QPS上限反推,假设通道每秒最多下发100条,每条下发耗时200毫秒,那么20个线程刚好能打满通道能力。攻击者再多的请求,也只能在这20个线程外面排队,不会无限制地穿透到短信通道侧。
日志和监控:用最小的存储成本实现攻击溯源CC攻击期间,日志量会暴增,如果所有请求都打全量日志,ELK集群的存储成本可能比短信消耗还高。需要做采样日志:正常请求只记录聚合指标,可疑请求才记录详细日志。具体做法是在网关层用Lua脚本判断,如果请求通过了所有风控检查并成功下发短信,只增加一个Prometheus计数器;如果请求被风控拦截,记录拦截原因、请求IP、手机号哈希、设备指纹、时间戳,写入本地文件,再用Filebeat异步采集到日志中心。这样日志量可以压缩90%以上,同时保留了攻击样本用于后续分析。
监控指标上,重点盯三个数据:短信下发成功率、风控拦截率、以及短信余额消耗速率。当风控拦截率突然升高时,不一定是攻击,也可能是风控规则误伤,需要人工介入查看拦截日志样本。当短信余额消耗速率超过日常峰值的3倍时,触发告警,自动开启更严格的限流策略,比如把滑动窗口从60秒缩小到30秒,或者临时启用图形验证码。这些自动化策略可以用Prometheus Alertmanager配合Webhook触发运维脚本实现,不需要人工24小时盯着。
攻击者的成本模型与防御者的博弈理解攻击者的成本结构,才能做出最优的防御决策。攻击者的成本主要包括:代理IP池的租赁费用、接码平台的号码费用、以及脚本开发和维护的时间成本。一个质量尚可的国内代理IP池,日租价格在200元到500元之间,包含几万个IP。接码平台的号码每条大概0.1元到0.2元,比正常短信成本还高。所以攻击者要打掉你1000元的短信余额,自己可能要先花掉500元到800元。防御的目标就是把这个比例拉到1:1甚至更高,让攻击变得不划算。
当你在前端加了JS Challenge,攻击者就得用无头浏览器或者V8引擎来执行JS,单台服务器的并发能力从几万QPS骤降到几百QPS,要达到同样的攻击效果,需要更多的机器和IP,成本翻倍。当你在TLS层做了指纹识别,攻击者就得定制TLS库来伪装指纹,开发成本从几小时变成几天。当你在业务层做了设备指纹和行为检测,攻击者就得逆向你的前端代码,分析采集逻辑,这个时间成本可能高达几周。每一层防御都在增加攻击者的边际成本,最终让攻击者觉得不值得继续。
防御策略的选择本身也要考虑成本收益比。一个小型SaaS服务,月短信费用可能只有几千元,花几十万去买高防IP或者上WAF集群显然不划算。这种情况下,最经济的方案是:前端加一个轻量级JS Challenge,后端用Redis做简单的滑动窗口限流,再加上号段黑名单过滤,总开发成本不超过几个工作日,但能挡住90%以上的自动化攻击。剩下的漏网之鱼,靠短信余额告警和人工介入处理,综合成本最低。
最后一点,短信通道的选择本身也是成本控制的一环。主流的短信服务商都提供防轰炸的阈值设置,比如阿里云短信可以设置单号码每日下发上限、单IP每日下发上限。但这些阈值往往比较粗糙,误伤率高。更优的做法是接入多家短信通道,根据号码的运营商和号段做智能路由,把高风险号码分配到成本更低的通道上,比如营销类通道单价只有验证码通道的一半,即使被消耗,损失也更小。同时,多家通道互为备份,一家被攻击欠费,自动切换到另一家,避免单点故障导致的业务中断。
