正规短信也能成为攻击载体:验证码洪泛的终端侧检测与保护
字数 3826
更新时间 2026-10-25 15:02:50

正规短信也能成为攻击载体:验证码洪泛的终端侧检测与保护

一句话结论:当攻击者借大量正规业务平台对某个手机号做分布式验证码轰炸时,每一条短信都是合法的,传统「单条是不是垃圾」的过滤彻底失效。本文的思路是把判定对象从「短信」提升到「号码上的事件流」,用时间衰减密度 + 分布式协同证据 + 攻击状态机,在终端可见的有限数据里识别出「这个号码正被分布式洪泛」。

排版说明:为规避排版转义问题,文中的指数、求和等公式用等价的纯文本与常见符号书写,正文引用一律使用「」。

一、从一次真实经历说起

某天,作者手机号在十分钟左右收到一大串验证码短信。在意的不是「收到很多短信」,而是这些短信几乎全都没有问题:分别来自不同平台、不同发送主体,各自看都是完全正常、合规的业务验证码。单独分析每一条,都找不到任何违规点——它确实来自大厂官方的 OTP 通道。

真正奇怪的是:为什么十几个互不相关的服务,会在极短时间内、同时向同一个手机号发送验证码?

于是问题变成:如果攻击者借助大量正规业务平台完成轰炸,那么作为终端用户,在拿不到任何服务端日志的情况下,还能做些什么?

二、与已有工作的区别

短信轰炸(SMS Bomb)、OTP Abuse、SMS Pumping / AIT(人为夸大流量)等问题已有较多研究,已有方案大多集中在短信服务商、业务平台或运营商侧,利用请求频率、设备指纹、账号关联、IP、短信流量等维度做风控;Android 生态也有针对 SMS Bomb 的终端侧防护实现。

本文不主张首创、也不提出一种通用短信过滤算法,而是从一个非常具体的切入点出发:当攻击者使用多个合法平台、多个发送主体,而终端用户又无法获得服务端日志时,能否仅根据终端可见的短信事件序列,判断出「这个号码正在遭受一次分布式攻击」。

一句话:本文不关心「某条短信是不是垃圾」,只关心「这一批短信作为一个整体,是不是一次攻击」。

三、检测对象不是短信,而是「攻击事件」

不要把它当成又一种 Spam Filter。传统过滤视角与本文视角的对比:

传统过滤视角:
短信1 → 垃圾 / 正常
短信2 → 垃圾 / 正常
短信3 → 垃圾 / 正常

本文的视角:
短信1 ─┐
短信2 ─┤
短信3 ─┼→ 这些短信是否共同构成同一个攻击事件?
短信4 ─┤
短信5 ─┘

核心观察可以概括为一句话:每一条短信都可以是合法的,但多条短信的联合行为可能是恶意的。单一短信合法性判断永远无法识别「用正规平台轰炸」,因为每条都合法。只有把判定对象从「短信」提升到「号码上的事件流」,才有可能在早期识别这种分布式行为。

四、从现象到设计:为什么是这些模块

以下设计不是罗列 DecayingMeter、Entropy、Novelty、State Machine,而是说明:每个模块都是从对现象的观察里推理出来的。

现象 1:攻击不是持续高频,而是「突然爆发」

轰炸的典型形态是短时间内集中涌入,之后可能停顿、再涌。持续性的高频率(比如一整天都在收消息)更像正常使用。

→ 所以需要时间衰减计数:给「刚发生」的消息更高权重,让突发被放大,让很久以前的消息自然衰减,不至于累积成永恒的高危。稳态下用封闭式给出物理可解释的标定:

v_eq = strength / (1 - e^(-Δ/τ))   (Δ = 平均到达间隔,τ = 时间常数)

把阈值映射到现实速率:约 0.5 条/秒 对应 6.5,1 条/秒 对应 12.5,2 条/秒 对应 24.5。

现象 2:攻击者会切换不同的发送主体

如果所有验证码都来自同一个发送方,那更像单个渠道的问题;但分布式轰炸恰恰相反:攻击者借大量不同商家/平台的 OTP 通道,发送主体极其分散。

→ 所以需要发送方多样性:对最近一个窗口的发送方求归一化香农熵,多样性越高,越不像单点故障。

现象 3:业务来源还会跨通道扩散

轰炸不只是短信,还可能混用语音呼叫、邮件、App 推送,进一步扩散业务面。

→ 所以需要通道扩散(Channel Diffusion):把「同时出现的不同通道数量」作为一个信号。

现象 4:正常用户一天也会收到很多验证码

靠绝对速率阈值会误伤高活跃用户(开发者、商务、常登录的人),因为他们可能长期处于较高流量。

→ 所以需要自适应基线:只有当「持续显著高于该号码自己的基线」达到足够长的时间,才把阈值抬上去;短时突发则保持低阈值、快速检测。这样能区分「长期高活跃号码」和「短时攻击」。

现象 5:攻击有明显阶段性

一次轰炸通常会经历:正常 → 初现异常 → 全面爆发 →(攻击结束)→ 恢复。不存在「一条短信就立刻开始全面防御」,也不应该「攻击一停就立刻解除」。

→ 所以需要攻击状态机:Normal → Suspicious → Under Attack → Protected。升级需要连续确认(避免误抖),降级需要持续平静周期(避免反锁)。

五、集成模型与 iOS 侧的数据可见性取舍

上面的信号被组织成一个以受害手机号为中心的攻击状态模型。综合判定可写成:

威胁 = 密度达标 ∧ 分布式达标
分布式协同证据 = w1·发送方熵 + w2·通道扩散 + w3·新发送方来袭率

判定的核心是:密度单高(单个熟人狂发)最多到 Suspicious;分布式强但稀疏也最多到 Suspicious;只有两者同时强,才判为分布式洪泛。

需要说明:这套检测只是把多个常见的异常检测特征组合到一个以受害手机号为中心的攻击状态模型里,基础技术和数据维度都不是新概念。真正要解决的是:在给定平台上这些数据到底可见到什么程度,又能据此做到哪一步。

以 iOS 为例有一个硬约束:iOS 的短信通道走的是 Message Filter Extension(IdentityLookup),而它在沙盒中无状态、逐条被系统调用——你拿不到「前一条短信」、也拿不到「最近几分钟到达了几条」,更不能在短信到达的瞬间跑时间序列和状态机。这是平台硬限制。

因此 iOS 侧被拆成两层:

  • 扩展层(无状态):逐条折叠疑似轰炸的短信,抑制预览和响铃;
  • App 聚合层:把历史短信事件归一化成(时间, 发送方, 通道),喂给上面的状态机,输出「保护期报告」。

这不是绕开平台,而是在平台给的数据可见性约束下做的取舍。

六、项目代码怎么看

实现仓库为 github.com/ZSH402/Malicious-bombing-without-parents,零第三方依赖,Node ≥ 14 可直接跑。下面摘几段项目里的实际代码。

6.1 时间衰减计数(DecayingMeter)

这就是「现象 1 → 时间衰减」的最小落地:每条消息给能量,两条之间按 τ 指数衰减。

class DecayingMeter {
  constructor(tauMs) { this.tau = tauMs; this.value = 0; }
  _decay(now) {
    if (!this._lastAt) { this._lastAt = now; this.value = 0; return; }
    const dt = Math.max(0, now - this._lastAt);
    if (dt > 0) this.value *= Math.exp(-dt / this.tau);
    this._lastAt = now;
  }
  add(strength, now) { this._decay(now); this.value += strength; return this.value; }
  peek(now) { this._decay(now); return this.value; }
}

6.2 发送方多样性(归一化香农熵)

对最近 N 个发送方求归一化香农熵——对应「现象 2 → 发送方多样性」。

normalizedEntropy() {
  if (this.recent.length < 2) return 0;
  const counts = new Map();
  for (const id of this.recent) counts.set(id, (counts.get(id) || 0) + 1);
  const n = this.recent.length;
  let H = 0;
  for (const c of counts.values()) { const p = c / n; H -= p * Math.log(p); }
  const Hmax = Math.log(counts.size);
  return Hmax === 0 ? 0 : H / Hmax;
}

6.3 状态机与判定

判定代码把上面几路信号合起来,交给 Normal → Suspicious → Under Attack → Protected 状态机消费;升级需连续确认、降级需持续平静,避免在噪声边缘来回抖。

七、局限性(主动承认做不到的部分)

诚实地说,这套方案能做的和不能做的边界很清晰:

  1. 无法阻止短信在运营商网络侧产生。终端 App 控制不了短信的发送链路,只能在「已经到达终端的时候」做检测与通知收敛。
  2. iOS 的 Message Filter 机制限制了终端能获取/关联的历史短信数据,因此当前更适合作为攻击检测与通知保护 PoC,而不是完整的网络侧阻断系统。
  3. 要真正阻断短信到达,仍然需要运营商或短信服务商参与。终端侧方案再完善,也无法替代网络侧。

八、一个脱敏后的真实样本

时间窗口:21:42 到 21:52(约 10 分钟)。合成量(转为仿真对标口径):

接收到的消息   约数百条
不同业务主体   约几十个
不同发送主体   约十多个
验证码类占比   高度集中
通道           短信 / 语音 / 推送 均有出现

单条观察:某金融 App、某手机厂商、某快递、某在线问诊——每一个单条判定都是「合法」。

联合观察:

同一手机号 + 极短时间 + 多业务 + 多发送主体 + 验证码高度集中
→ 疑似分布式 OTP Flood

把脱敏样本喂给上面的状态模型,在 8 类事件流上的仿真结果(node sim/run.js 可复现):

场景 固定阈值 FP 自适应基线 FP 轰炸命中 恢复时延
纯正常(昼夜) 0 0 否 约 0min
正常 + 营销 0 0 否 约 0min
合法登录风暴 32 32 否 约 1007min
分布式轰炸 0 0 是 约 718min
长期高活跃用户 0 0 否 约 0min

几点如实说明:

  • 分布式轰炸在两种模式下都正确命中(TP)。早期一版自适应基线会把密度阈值抬到约 4 倍、从而压制真实轰炸(TP 变否),已修复为只温和上浮、不破坏检测。
  • 同一昼夜尺度上,固定阈值与自适应基线在误报上差别不大(都是 0),因为时间衰减计数本身就对「持续中低速率」有天然容忍——自适应基线真正的价值在边界场景(长期高活跃、风格波动),更适合作为保险而非主力。
  • 必须强调:以上全部是合成流量,没有真实生产数据,只是把作者自己遇到的样本形态复现出来做验证。

九、复现

零第三方依赖,Node ≥ 14:

node sim/run.js          # 复现第五节的对比表
node test/engine.test.js # 引擎单元测试
node test/sim.test.js    # 仿真回归
node cli.js              # 命令行演示

十、结语

回到最初的问题:当攻击者借助大量正规业务平台完成轰炸时,终端用户到底还能做什么?

答案是:虽然无法在终端阻止短信产生,但可以把判定对象从「单条短信」提升到「号码上的攻击事件」,从而在终端能看到的有限数据里,识别出「这个号码正在被分布式轰炸」,进而进保护、收敛骚扰、并如实告诉用户发生了什么。这未必能拦住攻击,但至少让用户知道自己正在被针对,而不是被一条条看似无害的验证码淹没。

免责声明:本文仅用于技术学习、实践与交流,相关项目针对的是已获得授权的终端侧防护研究。

相似文章
相似文章
小程序二维码
 全屏