先讲一件我亲身经历的事。凌晨两点多,值班手机震个不停,告警只有一句话:Web服务平均响应时间从80毫秒直接飙到15秒以上。我爬起来打开监控大屏,第一眼去看带宽曲线——平坦得像一条直线,流量型DDoS的特征完全没有。再切到CPU和数据库监控,CPU已经烧到95%,数据库连接数打满,应用日志里全是超时。那一刻我脑子里蹦出来的词就是:CC攻击。
这不是我第一次被CC攻击搞得焦头烂额,但每次复盘都会发现,这类攻击最让人头疼的就在于"看不见"。它不像SYN Flood那样有汹涌的流量可供感知,也不像暴力破解那样有清晰的失败日志可供追踪,它就是让你服务变慢、变卡、最终不可用,而你打开监控面板时甚至找不到明显的异常流量。这篇文章我想把这几年和CC攻击对抗的完整思路整理出来——从它到底"看不见"在哪,到怎么把伪装撕开,再到防御体系怎么从浅到深一层层搭起来。
1. 表面看起来一切正常,服务器却在垂死挣扎:CC攻击到底"看不见"在哪
1.1 一次告警背后的真实场景
那天晚上我最终确认问题花了将近40分钟,不是因为工具不够,而是因为一开始的排查方向完全错了。带宽正常、入向流量平稳、机房层面没有丢包,按照传统DDoS的排查经验,我一度怀疑是代码BUG或者数据库慢查询。直到我把Nginx访问日志按秒拉出来,才看到触目惊心的数字:某一个商品查询接口,每秒请求量从平时的20左右暴增到3000多。
日志里的请求长得非常"正常"——User-Agent是常见的浏览器标识,Referer五花八门,请求参数也都是合法结构,甚至每隔几秒还会换一个IP。这就是CC攻击最经典的伪装:从单条请求看,每一笔都是合法用户行为;从整体看,量级已经完全压垮了应用层计算资源。
1.2 CC攻击与流量型DDoS的本质差异
很多人把CC攻击和DDoS混为一谈,实际上两者打的层次完全不同。
流量型DDoS(比如UDP Flood、ICMP Flood)攻击的是网络层和传输层,核心手段是灌满带宽或者耗尽防火墙连接表。这类攻击的特点是"猛",监控上很容易看到入向流量从几百兆跳到几十G,防御思路也相对清晰——上高防IP、做流量清洗。
CC攻击(Challenge Collapsar的缩写,历史原因沿用了这个名字)打的是应用层。攻击者模拟真实用户的浏览行为,不断向Web应用发起看起来完全合法的HTTP请求。它不追求大流量,追求的是并发数和请求频率。单个请求可能只有几百字节,但架不住量太大、太密集,Web服务器光是接收请求、解析报文、调度进程、执行逻辑、读写数据库就已经被拖垮了。
我用一张表来对比这两者:
| 对比维度 | 流量型DDoS | CC攻击 |
|---|---|---|
| 攻击层次 | 网络层/传输层 | 应用层 |
| 核心资源消耗 | 带宽、防火墙会话 | CPU、内存、数据库连接、应用线程 |
| 流量特征 | 入向流量暴涨、包数量巨大 | 总流量不大、QPS奇高、请求合法 |
| 监控可见性 | 带宽曲线一眼可见 | 带宽正常、应用响应变慢 |
| 主要危害 | 链路拥塞、无法访问 | 应用假死、业务不可用 |
| 防御侧重点 | 流量清洗、高防IP | 限流、验证码、WAF、缓存降级 |
这个区别直接决定了为什么CC攻击"看不见"——我们习惯用带宽和连接数来判断是否被攻击,但CC攻击恰恰绕开了这两个指标。如果监控体系只盯着网络层,应用层被打死的时候你很可能还在排查数据库慢查询。
1.3 为什么"每个请求都是合法的"才是最棘手的
流量型DDoS的报文一眼就能识别出异常,比如源IP伪造、协议栈不完整、包大小异常。而CC攻击的请求是从真实浏览器环境中模拟出来的:完整的HTTP头、合理的Accept-Language、正常的Cookie行为、符合业务逻辑的请求参数。
这就带来了一个巨大的难题:你没法通过"请求是否合法"来决定是否拦截,因为每一笔都合法。传统防火墙在这里几乎失效,IPS的签名特征也很难覆盖——因为攻击脚本可以无限变化UA、Referer、URL参数,你今天封掉的组合,明天就换了新的。
我后来在内部复盘时打过一个比方:流量型DDoS像有人抡着大锤砸你家的门,你一眼就能看到;CC攻击像一群穿着和你家客人一模一样的衣服、拿着一样的请柬的人,假扮成客人不停地进进出出,把你家的沙发坐满、把茶水喝完、把通道堵死。你真要拦,还得分清楚谁是客人谁是坏人。
2. 攻击方的成本账:用三分力气,让你付出三十分代价
2.1 资源不对称原理:请求很廉价,处理很昂贵
CC攻击能够持续这么长时间、这么难防,根本原因在于成本极不对称。
攻击方发起一个HTTP请求的成本几乎可以忽略不计——一台普通的攻击机器单线程每秒能构造并发出几十个请求,如果用自动化脚本配合多线程、协程,单机每秒几百上千个请求毫无压力。而服务器处理一个请求的成本则高得多:接收连接、TCP握手、TLS协商、解析HTTP头、路由匹配、鉴权、参数校验、查询数据库、渲染模板、返回响应,这一整条链路的开销可能是攻击方的几十甚至上百倍。
这就形成了典型的资源消耗战:攻击方用很低的成本持续施压,你的每台服务器却要用昂贵得多资源去应对。我见过一个真实案例,攻击方用了不到两千台肉鸡组成的网络,每台每秒只发5个请求,加起来QPS才一万左右,就把一套8核16G的应用集群打到CPU满载、接口大面积超时。单看每台的请求量,甚至不够触发共用的限流阈值——这是分布式CC最狡猾的地方,把总量打散到大量IP上,每个IP都"不够可疑"。
2.2 高频刷新、慢速连接、随机伪装:三种常见形态
从攻击行为特征上看,CC攻击大致有三种形态,防御时需要分别对待。
第一种是高频请求型,也是最常见的。攻击脚本盯着一个或几个消耗资源大的接口(比如搜索、查询、报表导出),以极高的频率反复请求。这类攻击的特征非常明显:单一URL的请求量占比异常高,QPS曲线呈尖峰状,来源IP遍布各地。
第二种是慢速连接型,学术名叫Slowloris类攻击。攻击方建立HTTP连接后,不发送完整请求,而是每隔几十秒才发送一个字节的头部数据,把服务器的连接池占满。服务器为了等待完整请求,不得不一直保持这些连接,最终导致正常的用户连接无法建立。这类攻击不需要高并发量,几百个并发连接就能拖垮一台配置一般的服务器,监控上表现为并发连接数奇高、但带宽和CPU却相对平稳。
第三种是随机伪装型,也是最难防御的。攻击脚本每一次请求都会随机生成UA、Referer、Cookie,甚至随机切换URL参数,让请求看起来分散在各种业务路径上。这类攻击的特征是"整体QPS升高、但没有一个单一维度特别突出",给基于频率统计的告警和拦截带来了很大麻烦。
2.3 被误判为"正常波动"的典型特征
我接触过不少被CC攻击打了几天才发现的案例,复盘时发现共同点:监控告警阈值设置得太粗。
很多团队的告警看的是"5分钟平均QPS",这个粒度对CC攻击来说太钝了。攻击方只要把请求控制在每秒正常峰值的2-3倍以内,5分钟平均下来就可能被平滑到阈值以下。真正有效的监控维度应该是秒级或者百毫秒级的P99响应时间和错误率,这两个指标对资源争抢非常敏感。CPU从30%跳到90%可能要几分钟,但P99响应时间从80ms跳到2秒,只需要攻击方把并发提上来的一瞬间。
还有一个典型的误判场景:CC攻击的请求如果集中在某个动态接口,数据库的慢查询日志会大量出现之前从未见过的慢SQL。这时候非常容易把方向带偏,让人以为是代码上线引入了性能问题。我见过有团队花了半天去优化SQL、加索引,结果该被攻击的接口还是被打死——因为问题根因根本不是SQL效率,而是请求量超出了一个数量级。
3. 接到告警后的完整排查链路:从Nginx日志到数据库连接
3.1 第一步:不依赖监控面板,先看access.log按分钟聚合
监控面板在高负载下可能延迟甚至失准,最可靠的排查起点是Web服务器访问日志本身。我习惯使用Nginx,下面的思路也以Nginx为主,但换成Apache访问日志也一样,关键是命令思路。
# 统计最近1分钟里,每个URL的请求次数,按降序排列 awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20 # 按分钟切片统计整体请求总量,看增长率 tail -50000 /var/log/nginx/access.log | awk '{print $4}' | cut -c1-16 | sort | uniq -c第一条命令的作用是立刻确认"请求是否集中在某一个URL"。如果某一个接口的请求量占到了总数的80%以上,基本可以断定攻击目标就是它。第二条命令则用来确认增长的曲线形态——CC攻击的请求量增长通常是断崖式的,一瞬间从正常水位跳到高位,这和业务突发(比如限时抢购)的渐进式增长有明显区别。
我还会顺手看一眼状态码的分布:
# 查看最近请求的状态码分布 tail -50000 /var/log/nginx/access.log | awk '{print $9}' | sort | uniq -c | sort -rn正常的业务流量状态码应该以200为主,其次是304、404。如果出现大量499(客户端主动断开,通常是攻击方请求排队超时后主动放弃)、502或者504,说明后端应用已经过载,请求处理不过来。这时候脑子里的第一个判断应该是:应用层已经被拖垮了。
3.2 第二步:看来源IP和连接状态,识别分布式攻击
确认URL集中之后,下一步是判断攻击是不是分布式的。这一步决定了很多防御手段怎么选。
# 统计请求量最高的前20个来源IP awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20如果Top IP的请求量就占了总请求量的绝大部分,说明攻击源比较集中,可以用IP维度的限流快速止血。但更多时候你会发现Top IP的请求量很平均——每个IP每秒可能只有十几二十个请求,没有哪个IP特别突出。这就是我说的"分布式CC",IP维度限流基本失效,必须靠行为维度的策略。
紧接着要确认系统的连接状态:
# 查看服务器的TCP连接状态分布 ss -ant | awk '{print $1}' | sort | uniq -c重点关注两个状态:TIME_WAIT和ESTABLISHED。如果TIME_WAIT数量暴增,说明有大量短连接在快速建立和断开——这是高频请求的典型特征;如果ESTABLISHED大量积压且持续不降,则要怀疑是慢速连接型攻击,连接建立后一直不发完整请求,把并发连接池占满了。
3.3 第三步:顺着链路往应用和数据库走
网络层和Web层看完之后,如果还没法完全定位,就需要往应用层深处走。这一阶段我通常按"应用慢还是数据库慢"来分叉。
先看应用服务器指标:
# 监控CPU和负载 top -n 1 # 查看Java应用线程状态(以Java为例,其他语言看对应工具) jstack <pid> | grep -A 5 "java.lang.Thread.State" | grep "RUNNABLE" | wc -l # 查看数据库当前连接数(以MySQL为例) mysql -e "SHOW STATUS LIKE 'Threads_connected';"排查逻辑是这样的:如果CPU烧满、数据库连接数打满,说明入口层的请求量已经把应用进程打成了"只接收不处理"的状态,问题在流量端;如果CPU不高、但某个接口的时延明显上升,那要重点看该接口内部是否有锁竞争、慢查询或者外部依赖阻塞,这时候再用慢查询日志确认。
我踩过的一个坑是:在CPU已经飙到95%的时候去开top看哪个进程消耗CPU,结果发现CPU全都消耗在操作系统层面的网络中断处理和进程调度上,而不是应用代码本身。后来才明白,高并发短连接场景下,CPU有很大一部分被内核协议栈和线程上下文切换吃掉了。这也是CC攻击区别于普通性能问题的特征之一。
3.4 排查过程中常见的错误方向:不要只盯流量
这里必须单独提醒一下:CC攻击的排查一定要从应用层向上看,而不是从网络层向下看。我见过太多人接到告警后第一反应是打开流量分析系统找大流量,发现没流量之后就开始怀疑硬件故障、运营商链路问题,白白浪费一两个小时。
正确顺序应该是:应用响应变慢 → 查访问日志请求量 → 确认URL和IP分布 → 查系统连接状态 → 查应用线程和数据库连接。整个链路走下来,正常情况下10分钟之内就能确认是否CC攻击、攻击目标和大概的攻击规模。我也建议每支运维团队把这套排查命令整理成文档放在手边,真到告警响起来的时候,人都容易慌,有一份checklist照着做会从容很多。
4. 防御从浅到深:频率限制、验证码、WAF规则与缓存降级
第3节讲的是"被打时怎么快速确认",这一节说怎么防。我的观点是:没有一套配置能永远生效,防御必须做分层。每一层拦掉一部分攻击流量,让真正到达应用层的恶意请求降到应用能承受的量级以下。
4.1 第一层:Nginx 频率限制和验证码
对于普通规模的CC攻击,Nginx自带的limit_req模块就能解决大部分问题。它的原理是令牌桶算法,控制每个IP在单位时间内能通过的请求数。我常用的配置如下:
# 定义限流区域:每个IP每秒平均放行5个请求,允许瞬时突发10个 limit_req_zone $binary_remote_addr zone=perip:10m rate=5r/s; server { location /api/ { limit_req zone=perip burst=10 nodelay; proxy_pass http://backend; } }注意几个容易出错的地方。第一,$binary_remote_addr比$remote_addr更省内存,10M共享内存大约能存16万个IP状态,业务量不大的场景足够用。第二,burst要结合实际业务压测来定,太小会把正常用户也拦了,太大又起不到限流效果。第三,限流返回给用户的是503,最好在配置里加上自定义的错误页面逻辑,否则用户看到白屏反而会加剧恐慌。
不过频率限制有个天然软肋:分布式CC攻击时,每个IP的请求量可能根本不到阈值。所以频率限制只能作为第一层。
真正的强验证要看验证码和JS挑战。当检测到可疑的高风险行为时(比如某个IP在短时间内访问了超过正常用户次数的页面、或者触发了Web应用防火墙的高风险规则),强制要求完成一次滑块验证或点击验证,能非常有效地把自动化脚本挡在外面。自动化的成本被拉高了,攻击方要么放弃,要么得花钱接入打码平台,这会让攻击的ROI变得很低。
需要注意的是,验证码策略要尽量放在最前置的网关层,不要打进业务代码里。让业务代码去判断验证码会引入耦合,而且如果攻击打的就是业务服务器本身,验证码判断逻辑也跟着一起被打挂了。整理一下当前常见的验证方式选择:
| 验证方式 | 防自动化效果 | 用户体验影响 | 适用场景 |
|---|---|---|---|
| 频率限制 | 中 | 极低 | 通用第一层防线 |
| 滑块/点击验证 | 高 | 中 | 风险触发时使用 |
| JS挑战/空闲检测 | 中高 | 低 | 反脚本、反非浏览器环境 |
| 设备指纹+行为分析 | 高 | 低 | 对特定接口做精准风控 |
4.2 第二层:缓存优先、动态接口降级
很多被CC攻击打得特别惨的团队,都有一个共同问题:所有请求都穿透到动态层。哪怕是商品详情这种几乎不变化的页面,每次请求都要走一遍数据库查询、模板渲染,成本和风险都被放大了。
正确的思路是"能缓存的全缓存"。静态资源(图片、CSS、JS)交给CDN承担,这不用说;动态接口也要尽可能地做结果缓存。比如商品信息、公告配置、分类列表这类"读多写少"的接口,完全可以在Redis里缓存结果,设置短TTL(比如30秒到60秒),数据库压力瞬间就能降下来。
# 以Python Flask + Redis为例,伪代码示意 cache_key = "product:detail:{}".format(product_id) data = redis.get(cache_key) if data is None: data = query_mysql(product_id) redis.setex(cache_key, 30, data) # 30秒缓存 return data缓存之外,还要做服务降级预案。我参与过的一个项目,核心交易链路里混着一个报表统计接口,平时没人在意,CC攻击一来,攻击方死死盯住这个接口打,结果整个应用的所有接口都跟着超时——因为报表查询占用了大量数据库连接和CPU时间片。后来我们把报表接口做了异步化改造和独立资源池,攻击打它的时候,其他核心接口基本不受影响。这个经验后来形成了一条铁律:明确哪些是核心链路、哪些是边缘能力,边缘能力在高负载下必须能快速熔断降级。
4.3 慢速连接型攻击的专项防护
慢速连接型CC攻击用前面的频率限制很难防住,因为攻击方每秒建立的连接数可能并不多,但它把每个连接都"吊"在服务器上不释放。针对这类攻击,互联网服务器里有专门的参数可以调:
# 调整请求头和请求体的超时时间,防止慢速连接长期占用 client_header_timeout 10s; client_body_timeout 10s; # 限制单个连接传输速率,防止慢速持续占用 limit_rate 512k; # 同时限制连接数 limit_conn_zone $binary_remote_addr zone=peripconn:10m; server { limit_conn peripconn 20; }client_header_timeout设成10秒的意思是:如果客户端10秒内没有发送完整的请求头,直接断开连接。这基本能废掉Slowloris类攻击,因为攻击方专门就靠"慢"来拖垮服务器,你不给它慢的权力,它的攻击就失去了根基。
这里有一个容易被忽视的问题:超时参数并不是越小越好。太小会有误伤,移动网络下用户网络抖动可能导致正常的请求头还没传完就被断开。建议结合业务实际情况压测,10秒是一个比较保守但稳妥的起点。
4.4 在线WAF规则:特征识别与自动更新
频率限制、验证码、缓存属于"半静态"的防御手段,真正要做到动态对抗,还得靠Web应用防火墙(WAF)的规则引擎。这部分我重点讲一下规则设计的思路,而不是具体产品的配置。
一个标准的CC防护规则应该从四个维度提取特征:IP维度、会话维度、行为维度、资源维度。IP维度包括IP的信誉分、历史请求频率、是否来自数据中心IP段;会话维度包括Cookie是否存在、访问间隔是否符合人类行为(比如间隔太规律就是脚本特征)、是否频繁更换UA;行为维度包括访问路径是否跳跃性太大、是否总是访问耗费资源的接口;资源维度包括单个会话的服务端执行时间、数据库查询次数等。
把这些维度组合起来,就能形成高可信的拦截决策。举个例子:某个IP在5分钟内访问了200次,其中全部请求都集中在搜索接口,UA还是同一个老旧的浏览器标识,Cookie每次都是新建的——这四个特征叠加起来,是正常用户的概率微乎其微,可以直接拉黑。
关于WAF我还要强调一点:规则必须能够动态更新。CC攻击的自动化脚本迭代很快,你今天封掉的特征组合,明天攻击方可能就换了。如果规则完全靠人工维护,根本跟不上。现在主流的做法是引入半自动化的异常流量分析模块,定期从访问日志中学习正常用户的访问画像,然后以此作为基线去比对实时流量,偏差超过阈值就触发拦截或验证码。这也是CC防御从"规则对抗"走向"行为对抗"的分水岭。
5. 我把系统调好了,为什么又一次被打崩——防御实践中容易忽略的坑
5.1 误杀正常用户:限流阈值太高拦不住、太低一场事故
我把最常见的一个坑放在最前面讲:限流参数设置不当,会直接制造一次业务事故。有一次我们在配置Nginx限流时,把rate设成了2r/s,测试环境完全正常,但上线后白天高峰期出现大量用户访问报503。排查才发现,同一栋办公楼出口的NAT IP只有一个,整层的同事都走同一个公网IP访问,限流一开,整个办公室的人都在同一秒被限住了。
从那以后我规定:限流阈值设置前必须做业务压测和流量分析,了解NAT出口IP的聚合情况。同时配置里要给搜索引擎爬虫、内部监控探针、合作伙伴回调等合法的高频来源加上白名单,否则内置系统的健康检查也会被当成攻击拦掉,引发误告警甚至自动故障转移。
5.2 攻击者绕过CDN直打源站IP
第二个坑更隐蔽:很多团队给Web服务套了CDN和WAF,以为防护已经到位了,结果攻击一打就崩。后来查登录日志发现,攻击方压根没有通过CDN域名进来,而是直接用扫描工具找到了源站的真实IP,绕过CDN把请求直接打到了源站服务器上。
要堵住这个口子需要做几件事:源站IP不要暴露在域名解析记录里;源站服务器只允许CDN回源IP访问(在安全组或防火墙层面对来源IP做白名单),其他来源一律拒绝;如果有必须对外开放的端口,一定要在这些端口前面也部署一道轻量级的应用层防护。
5.3 防御规则的滞后性:特征变化比规则更新更快
第三个坑我在前面提到过,但值得单独展开:静态规则注定跟不上一线攻击的变化。攻击脚本的作者也很勤奋,他们会分析WAF特征、观察哪些请求被拦截,然后动态调整UA、调整请求间隔、随机化参数。如果防御方只是坐等人写规则然后发版,整个流程走完,攻击特征早就换了三遍。
我的应对经验是两条腿走路:一是把日志分析做成持续运行的模块,异常检测不能靠人肉看,要让系统基于统计模型自动发现偏离基线的请求模式;二是保持和同行的信息互通,定期关注样本分析、特征库更新的公开信息,主动把最新的攻击特征同步到自己的规则引擎里。
5.4 给团队留下一份经过验证的防御预案
最后一条经验来自那一次凌晨的告警之后。我们在复盘时发现,虽然各个防御组件都有了,但整套体系从没做过一次完整的攻击模拟演练。真到攻击发生时,连告警应该通知谁、由谁来决策开启JS挑战、哪些接口可以先降级、谁负责跟云服务商沟通应急防护,这些问题都没有答案,现场一片混乱。
于是我们后来按季度组织一次CC攻击模拟演练:运维负责执行排查链路,研发负责确认降级接口的切换流程,值班组负责验证告警有效性和通知时效。经过两轮演练,再遇到类似攻击,整个团队的反应速度和规范性已经完全不同。防御预案这个东西,平时看着是文档,真出事了就是保命的工具。
在我个人的防守经验里,对待CC攻击最关键的心态是接受一个现实:你不可能做到100%的物理拦截,但是你可以做到把攻击的影响控制在可承受范围内。频率限制、验证码、缓存降级、慢速防护、动态规则,每一层多挡住一点,应用层扛住的压力就小一些。系统的设计目标不应该是一台服务器硬抗全世界,而是让攻击方在试图打垮你的时候,付出的成本远远高于你承受的成本——这场账算清楚了,CC攻击就真的没有那么可怕。