一个爬虫农场把User-Agent伪装成iPhone,用10个轮换IP在15分钟内拉走375MB图片流量。常规UA黑名单对它无效,因为它的Safari版本号还在不停轮换。本文用一份真实百度云防护WAF日志,拆解「UA版本矛盾识别法」和「JA3指纹聚合统计法」两板斧,从发现、取证、写规则到对抗它的三次马甲进化,附可直接照抄的WAF规则配置和误杀控制方法,站长可以直接上手。
下午分析论坛的WAF日志时发现一件有意思的事:15分钟出口流量3.1GB,其中有一股流量特别扎眼——1,253次请求,274MB全是图片,UA清一色是iPhone,但IP换了10个。
一开始以为是重度图片党,直到我把UA串完整拉出来对了一遍,发现这批「iPhone」的出身有问题。
一、假iPhone是怎么露馅的
抓出来的UA长这样:
懂行的看一眼就笑了:iPhone OS 18.7 的机器,装不上 Safari 27.2。
iPhone的Safari是跟iOS系统捆绑发布的,一台iOS 18.7的设备,UA里的Safari版本永远只可能是18.7.x。我在日志里全站拉了一遍真机对照,规律非常干净:
| 真机UA组合 | 请求量 | 版本关系 |
| iOS 16.3 + Safari 16.2 | 304 | 锁定 |
| iOS 27.0.1 + Safari 27.0 | 55 | 锁定 |
| iOS 18.7 + Safari 18.7.5 | 24 | 锁定 |
| iOS 18.7 + Safari 27.2 | 1,253 | 跨了9个大版本,物理不可能 |
真机的OS版本和Safari版本是锁死的,爬虫 farm 编UA的时候只顾着堆新版本号,忘了这两者要配套。而那台真正的iOS 18.7老设备(24次请求,正常浏览论坛页面),我全程没动它,零误伤。
更实锤的证据在TLS指纹层:这10个IP虽然UA版本号在27.2、26.6.1、27.0.1、26.4之间来回换,但1,253次请求全部共享同一个JA3指纹 ecdf4f49dd59effc439639da29186671。UA随便编,TLS协议栈是程序自带的,编不了。同一套采集程序、IP轮换池,齐活。
二、为什么不能直接拉黑这个JA3
这里有个大坑,我差点踩进去。
查这个指纹的全站分布,发现它命中2,550次请求、62个IP——除了农场的2,043次,还混着500多次真人流量:Mac上的Safari、iPad、正经iOS 27设备。
原因很简单:这个农场不是自己写的TLS栈,而是直接跑在真WebKit内核上(设备农场或者Playwright WebKit一类的东西),所以它的TLS指纹跟真人Safari天然重合。
结论就一句话:
JA3指纹固定,但它姓Apple不姓攻击者。拿来做统计维度完美,拿来做黑名单会误杀。
这也是我要重点说的方法论:JA3在百度云防护里正确的用法,是当计数器用,不是当名单用。
三、两板斧:矛盾UA锁目标 + JA3聚合计数
第一板斧:UA矛盾组合做匹配条件
规则不是拦「某个UA」,而是拦「自相矛盾的UA组合」:
| 规则 | 匹配条件(且) | 动作 |
| 规则1 | User-Agent 包含 iPhone OS 18_7 且 User-Agent 包含 Version/27 | 拦截 3600秒 |
| 规则2 | User-Agent 包含 iPhone OS 18_7 且 User-Agent 包含 Version/26 | 拦截 3600秒 |
为什么这么写:
- 双条件AND:真iOS 27.2用户的UA写的是
iPhone OS 27_2,不含18_7,第二条就过不去 - 版本写宽不写死:农场在26.x和27.x之间轮换,条件只锁大版本
Version/26和Version/27,换小版本没用 - 放过唯一的正常版本:真老设备的
Version/18.7.5两个条件都不含,天然放行
这两条规则上线后的实测:农场228次请求被403,同期的Mac真人2,973次请求只被误拦6次,全站出口流量直接降了26%。
第二板斧:精准自定义CC,统计维度选JA3
百度云防护的「精准自定义CC」支持按JA3统计,这是对付IP轮换的杀手锏:
| 表单字段 | 填法 |
| 防护类型 | 精准自定义CC |
| 匹配条件 | User-Agent 包含 iPhone OS 18_7,且 User-Agent 包含 Version/27 |
| 统计信息 | 选 JA3(不要选IP) |
| 频率 | 10秒 / 2次 |
| 处置动作 | 拦截 3600秒 |
| 生效范围 | 仅作用于当前规则的匹配条件 |
原理:农场10个IP轮着打,按IP统计的话每个IP只打几下,阈值设多低都漏。但它们的JA3是同一个,按JA3统计后所有IP的请求累加进同一个计数器,10秒2次的低阈值就能全覆盖。而匹配条件已经用矛盾UA把目标锁死了,真人Safari用户虽然共享这个JA3,但根本进不了匹配条件,不会被计数。
这个「匹配条件锁特征 + 统计维度聚合」的组合,是百度云防护里对付分布式低频爬虫的标准打法,比单纯堆IP黑名单高效得多。
四、对抗升级:它一小时换了三套马甲
别以为写完规则就完事了。同一天晚些时候我拉了第二份日志,农场已经换了打法:
| 马甲 | 手法 | 穿透量 | 应对 |
iPhone OS 27_2_0 | OS和Safari版本配套了,伪装成完全正常的UA | 单IP 350次/107MB | UA拦不住,直接封IP |
iPhone OS 9_0 | 换个古董系统,但iOS 9不支持TLS 1.3,指纹却是现代WebKit的,照样矛盾 | 74次 | UA含iPhone OS 9_0 + JA3属于ecdf4f49,双条件拦 |
| 砍掉Version字段 | UA以Mobile/15E148结尾,不带Safari版本 | 50次 | UA条件覆盖不到,靠频率规则兜底 |
| 复用真机UA | 直接抄「18_7 + Version/18.7.5」真机格式 | 64次 | UA和指纹都和真人一致了,只能按行为频率拦 |
看明白了吗?这是一个动态对抗过程:UA规则拦一层,它蜕一层皮,最后逼到行为频率层。所以规则要分层布置,UA层、指纹层、频率层各管一段,别指望一条规则一劳永逸。
另外同期还顺手处理了两个小角色,一并给出配置:
| 对象 | 特征 | 规则 |
| ktor-client脚本 | UA直接暴露Kotlin HTTP库,184次盗图 | 自定义规则:UA包含ktor-client → 拒绝(无需频率条件) |
| HeadlessChrome | 无头浏览器,1,152次请求遍历全站 | 自定义规则:UA包含HeadlessChrome → 拒绝 |
这两个是送分题,没有任何正常浏览器会发这种UA,见一个拦一个。
五、误杀控制:三道保险
拦得狠不如拦得准,误杀真人的代价远大于放过几个爬虫。我的做法:
- 写规则前先跑数据。把要拦的UA特征在全站日志里拉一遍,看命中的IP数、JA3种类数、云主机占比。比如那次
Chrome/59老版本特征,全场1种UA、96%云主机IP、100%打根路径,三项全中才敢拦;而Chrome/57这种特征一查全是真人老手机,碰都不能碰 - 处理动作优先按IP/JA3处置,慎用全局拦截。封IP会有连坐——农场用的移动动态IP,真人轮到同一个IP会被牵连。实测连坐量在15分钟20次请求以内,可接受;如果你们的处置时长能选,3600秒以内别上永久
- 上线后拉下一份日志复盘。重点看两个数字:被拦请求里非目标特征的占比(误杀率),和目标特征还剩多少200(穿透率)。我这轮复盘就是靠第二份日志发现了马甲进化

六、写在最后
这轮对抗下来,15分钟出口流量从3.1GB压到2.3GB,农场的主力流量被摁掉七成,真人几乎无感。
总结成三句话:
- UA会骗人,但TLS指纹骗不了——JA3做统计维度,把分散IP聚成一个计数器
- 别拦指纹本身,拦矛盾特征——真WebKit指纹和真人重合,直接拉黑等于自杀
- 规则是活的——爬虫换马甲比你想的快,定期拉日志复盘,穿透率一抬头就升级规则
这些能力百度云防护的自定义规则都原生支持,站长在控制台照着上面的表单填就行。你们站点要是也有图片被盗刷、带宽莫名跑高的情况,先把WAF日志导出来按这个思路过一遍,多半能抓到惊喜(吓)。
