暴力破解攻击原理与检测思路

暴力破解,说白了就是攻击者把所有可能的密码组合挨个试一遍。你想想看,如果你的密码是“123456”,那别人用脚本从“000000”试到“999999”,十有八九就撞上了。我刚开始做安全运维时,总觉得这种事离自己很远,直到有一次接到报警,一台测试服务器的SSH端口在五分钟内被尝试了八千多次……嗯,从那以后我再也不敢轻视暴力破解了。

字典攻击——最省力的暴力破解

真正聪明的攻击者不会从“aaaaaa”开始傻试。他们会先收集常见密码、泄露库中高频口令、个人信息组合,做成一个**字典文件**。字典攻击就是用这个字典里的词条逐一尝试登录。

关键特征: 攻击流量集中在同一账号或少数几个账号上,登录失败的 IP 稳定,请求间隔相对均匀(通常在 100~500 毫秒)。

字典攻击的检测思路

我建议从三个维度入手:

  • 单账号失败次数: 比如 5 分钟内同一账号登录失败超过 10 次,就很可疑。
  • 单 IP 失败次数: 同一个 IP 对任意账号尝试失败超过 30 次,基本可以确定是字典跑法。
  • 失败率+时间分布: 连续失败间隔稳定在 50~200 毫秒,明显不是人手动输入。

我在项目中遇到过一种情况:攻击者把字典拆分成 10 个小文件,用 10 个 IP 同时去试。单个 IP 看每个才失败了 3 次,完全触发不了阈值。这时候单纯的“单 IP 计数”就失效了——所以必须引入聚合计数。

避坑指南: 我曾经把单 IP 阈值设成 10 次/分钟,结果凌晨三点被误报警吵醒——原来是监控自己跑脚本在查端口。后来我把白名单机制加上,总算消停了。

分布式破解——用数量碾压阈值

分布式破解是字典攻击的“升级版”。攻击者控制几百甚至上千个僵尸主机(肉鸡),每个 IP 只试几个密码,避免触发单 IP 的封禁。你想想看,1000 个 IP 每个只试 3 个密码,总共就能试 3000 次,而你的告警规则可能只盯着单个 IP。这就是所谓的“低慢攻击”。

如何识别分布式破解?

单纯看单点失败了,得换思路。我个人习惯做以下几种聚合分析:

  1. 源 IP 聚合: 统计同一个目标账号在短时间内出现的不同源 IP 数量。如果 5 分钟内同一个账号被超过 20 个不同 IP 尝试过,大概率是分布式。
  2. 目标账号聚合: 统计同一个 IP 在短时间内尝试过的不同用户名数量。如果超过 50 个,说明是在扫号。
  3. 失败模式匹配: 看所有失败请求中的用户名列表是否和已知泄露字典重合。我曾见过攻击者直接用 github 上公开的“top1000 密码”文件,连排序都没改。
攻击类型 典型特征 检测重点
传统字典攻击 单 IP、固定频率、慢速 单点失败计数 + 时间窗口
分布式破解 多 IP、低频率、统一目标账号 源 IP 聚合 + 账号聚合
混合型 多 IP + 字典变体 + 随机延迟 模式识别 + 异常比例分析

注意: 分布式破解的流量来源可能遍布全球,地域分布极广。不要指望简单 IP 段封禁就能解决——我曾经在一个金融客户那里看到,攻击 IP 覆盖了 30 多个国家,手动封禁根本不现实。

失败计数策略——把逻辑讲清楚

失败计数不是“失败了就+1”这么简单。在实际系统中,我们需要设计一套**滑动窗口 + 分级阈值**的计数机制。

核心设计要素

  • 时间窗口: 常用 1 分钟、5 分钟、15 分钟。窗口太短容易漏报,太长延迟高。我建议业务系统用 5 分钟窗口做主检测,1 分钟窗口做快速熔断。
  • 阈值等级: 设置警告级(触发通知)和阻断级(直接封禁或人机验证)。例如:5 分钟内失败 10 次告警,20 次阻断。
  • 计数器粒度: 按 (IP, 账号) 组合计数最精确,但存储量大。如果资源有限,至少按 IP 和按账号分别计数。

实战经验: 我做过一个电商登录防护,最初只用了 Redis 的 INCR 命令,每失败一次就 +1,60 秒后过期。结果攻击者发现规律,每 55 秒换一个 IP,恰好躲过窗口。后来我改用 滑动时间窗口(比如 sorted set + ZREMRANGEBYSCORE),才彻底堵住这个漏洞。

代码示例——滑动窗口计数(Redis + Python)

import redis
import time

r = redis.StrictRedis(decode_responses=True)

def record_failed_login(ip, account, window_sec=300):
    key = f"failed:{ip}:{account}"
    now = time.time()
    # 移除窗口外的记录
    r.zremrangebyscore(key, 0, now - window_sec)
    # 添加当前失败时间戳
    r.zadd(key, {str(now): now})
    # 设置 key 自动过期(窗口 + 30 秒缓冲)
    r.expire(key, window_sec + 30)
    return r.zcard(key)

def is_blocked(ip, account, threshold=20):
    count = r.zcard(f"failed:{ip}:{account}")
    return count >= threshold

整体检测思路框架

说了这么多,我画一个结构图,把三种攻击和对应的检测策略串起来。

暴力破解检测知识体系 暴力破解攻击 字典攻击 分布式破解 混合型攻击 单 IP 失败计数 滑动时间窗口 IP 聚合 / 账号聚合 分布式特征识别 模式识别 + 异常比例 机器学习辅助 告警 + 运维通知 日志存储 / 分析 阻断 + 验证码 / 封禁 自动拉黑 / 临时冻结 核心原则:先告警、后阻断,避免误伤正常用户

总结——别只盯着一个指标

暴力破解的检测不是单一规则就能搞定的。我在几个大客户的线上环境中见过太多次“规则形同虚设”的情况——要么阈值设太高,攻击跑了半小时才告警;要么阈值太低,搞得运营同事一到晚上就收几百条垃圾告警。

给你一个实在的建议:

  • 数据层面: 记录每次登录请求的 IP、用户名、时间戳、结果(成功/失败)。越细越好,后期聚合分析全靠它。
  • 检测层面: 同时跑多套规则——单 IP 计数、单账号计数、IP 聚合计数、失败模式匹配。不同规则用不同权重打分。
  • 响应层面: 低分告警给人工,高分直接上验证码或临时冻结。冻结时间可以指数增长:第一次 5 分钟,第二次 30 分钟,第三次 2 小时……攻击者耗不下去的。

要记住,攻击者也在不断进化。你今天写的规则,明天可能就被绕过了。保持日志看得见、规则可调、响应可扩展,这才是长期有效的防御姿态。