1. 动手之前,先把F5 Shape这玩意儿看透
做逆向分析最忌讳的就是拿到样本就开干,连人家是个什么东西都没搞清楚。F5 Shape(现在官方叫F5 Shape Security,老玩家还是习惯叫shape)是业界用得比较多的反爬虫、反自动化攻击防护体系,尤其是航空、银行、零售这些对风控要求极高的行业,几乎成了标配。美西南、xbk这类业务场景里,它承担的责任就是在你还没碰到业务接口之前,先把一大批自动化流量挡在门外。
Shape这套东西的核心逻辑不是简单的验证码,也不是IP黑白名单,而是一整套客户端环境信任评估体系。它的核心组件包括:
- Telemetry SDK(JS采集端):嵌入页面,采集浏览器指纹、环境参数、用户行为轨迹。
- Shape Collector(流量采集端):在服务网关处采集请求特征,包括TLS指纹、HTTP头顺序、请求频率等。
- Shape AI/决策引擎:把前端采集的数据和后端流量特征合并,算出一个风险分数,返回给业务方。
说白了,Shape更像一个“环境裁判”,它判断的不是“你是谁”,而是“你运行在什么环境里、你的行为像不像真人”。理解了这一点,你才能明白为什么单纯改UA、挂代理根本绕不过它——因为它看的维度太多了,任何单一维度的伪装在它眼里都破绽百出。
我见过很多新手一上来就盯着JS逆向,想着把那个几千行的challenge脚本逆向完就能通关。这个思路没有错,但只对了一半。Shape的防护强度是分级的,有些场景下它只做轻量采集,有些场景下它会下发强校验challenge,而美西南、xbk这类高价值场景,通常是多层防护叠加。你要是只盯着前端,忽略了TLS指纹和请求行为一致性,照样会翻车。
这篇文章我打算沿着我自己的实操路径讲:先看采集逻辑,再做环境模拟,最后处理动态challenge,再聊聊那些网上基本搜不到、需要自己踩坑踩出来的细节。整个过程以F5 Shape最新版为主,美西南和xbk场景为辅,尽量把思路讲透,让你换一个目标也能举一反三。
2. 逆向分析的前置准备:工具、环境与“为什么是这些方案”
2.1 工具链选型,不踩坑的基本盘
既然要动Shape这种级别的目标,工具链不能将就。我在实际测试中用的组合是:
- 抓包层:mitmproxy(配合自定义脚本)+ Wireshark(看TLS握手细节)。Charles和Fiddler不是不行,但在处理TLS指纹模拟和HTTP2复用时,没有mitmproxy灵活。
- JS分析层:Chrome DevTools(断点调试)+ 本地Node环境(快速验证JS逻辑)+ 必要的Browser环境(有些JS代码段只能在Browser环境下运行,Node里会缺API)。
- 指纹模拟层:Node.js为主,配合Alembic(一个开源工具)或者自己写TLS指纹模拟逻辑。这里重点不是工具本身,而是你得清楚Node的TLS指纹跟浏览器有本质区别,需要针对性调整。
- 流程编排:Python脚本做整体调度,Node只负责执行Shape的challenge计算。两者通过HTTP服务互通。
这套组合的核心思路是“解耦”:抓包、分析、执行、调度各自独立,避免在一个环境里做所有事情导致排查问题时分不清是哪个环节出的错。
2.2 环境准备里的隐性门槛
Shape对运行环境的检测非常敏感,环境准备不到位,后面的分析全是白费功夫。
第一,你必须要有一个完整的浏览器环境来做JS动态调试,或者说至少得能模拟出完整的浏览器环境。Shape的JS里检测项包括但不限于:window对象的关键属性是否存在且可枚举、Canvas指纹、WebGL渲染器信息、Navigator对象的属性顺序、iframe的contentWindow行为、Storage类型的可用性和行为。这些检测单个看都不难,难的是“全都要对”,缺一个就可能触发风险标记。
第二,网络环境要干净。这里的“干净”不是指IP干净这么简单,而是DNS解析路径、TLS握手特征、HTTP头顺序都要接近真实浏览器的行为。Shape的后端有专门的流量分析模块,它不看你IP是不是机房IP(当然这也是一个维度),更关键的是看你整个请求链路的“形状”是否符合真实用户的统计规律。这一点我会在后面的实操章节展开讲。
第三,时间同步很重要。Shape的challenge里经常带时间戳校验,你的环境时间如果和真实时间偏差过大,JS计算出的签名值在服务端校验时直接失败。我踩过这个坑,本地虚拟机时间慢了3分钟,排查了半天才发现是时间同步的问题。建议在做这类分析时,先给环境配置好NTP自动同步。
注意:如果你准备用自己的主力机做分析,建议开一个独立的虚拟机或者容器环境。Shape的JS会做环境持久化标记(比如通过localStorage、IndexedDB写入特征),一旦被标记,后续所有请求都会被重点观察。用隔离环境,随时可以重置。
3. 核心细节解析:Shape的采集逻辑、检测维度与动态challenge机制
3.1 Telemetry SDK的采集维度拆解
Shape的采集端JS在最新版里做了大量混淆和动态加载,但核心采集维度万变不离其宗,大致可以分为四层:
第一层是基础环境层。包括UA、平台、语言、时区、屏幕分辨率、色深、插件列表、字体列表。这些属于“明牌”,正常浏览器和自动化工具的差异最大,也最容易暴露。
第二层是图形渲染层。通过Canvas 2D和WebGL绘制特定图形,然后取渲染结果的摘要值。这里的坑在于,不同GPU驱动、不同操作系统渲染同一段图形,结果都会有差异。Shape不仅记录最终的摘要值,还记录渲染过程中调用了哪些API、是否使用硬件加速等细节。自动化环境如果用headless浏览器,WebGL的软件渲染特征和真实浏览器差异非常明显。
第三层是浏览器行为层。比如鼠标轨迹、键盘事件间隔、滚动行为、焦点切换频率、页面可见性变化(visibilitychange事件)等。最新版Shape把行为采集细分到了极其变态的程度——它不光记录鼠标的移动轨迹,还计算轨迹的曲率、加速度变化、停顿模式。真人操作和脚本模拟的轨迹在统计学上差异巨大。
第四层是存储与持久化层。Shape会在localStorage、IndexedDB、WebSQL里写入自己的标记数据,下次访问时读取校验。同时还会用Cookie做会话关联。如果你清理了这些存储,反而会触发风险标记——因为真实用户很少每次都清空浏览器存储。
3.2 动态challenge的生成与校验流程
当Shape判定当前请求的风险分达到阈值时,会返回一个challenge页面或一个JSON格式的challenge指令。整个校验流程大致如下:
第一步,服务端下发challenge配置。这段配置里包含时间戳、随机数、加密算法类型(最新版已经不局限单算法了)、需要客户端计算的证明量(PoW,工作量证明)。
第二步,前端JS加载挑战模块。这个模块是动态加载的,而且每次的代码都有可能不同——这就是“动态”的核心含义。同一份challenge逻辑,会做多种变换:变量名混淆、控制流平坦化、字符串编码、死代码注入等,每次返回的代码都长得不一样。
第三步,客户端执行计算并回传结果。计算结果通常包括:challenge的响应值、执行过程中的性能埋点(用了多少时间算完)、以及一段加密的证明数据。
第四步,服务端校验。服务端会验证结果是否正确、计算时间是否在合理范围、客户端上报的证明数据是否有效。任何一项不通过,都会触发二次challenge或者直接拒绝请求。
最新的F5 Shape版本在challenge计算里加入了“执行环境感知”逻辑,也就是说,它在计算过程中会实时检测当前运行环境的特征,把环境特征和计算结果绑定在一起。这意味着你就算把JS代码逆向得明明白白,拿Node直接执行计算出的结果,服务端也能识别出环境不匹配。这就是很多逆向者卡在最后一步的根本原因。
3.3 为什么纯JS逆向不是万能的,还差哪一步
顺着上面的逻辑,你应该已经看到了:即使你把challenge的JS逻辑完全逆向清楚,也还差两层需要打通。
第一层是浏览器环境模拟层。你需要构造一个完整的浏览器环境,让Shape的JS在运行时所有环境检测点都通过。包括:补全Window/Document/Navigator属性、模拟Canvas和WebGL渲染、伪造行为轨迹数据、构造合理的存储状态。这属于浏览器环境仿真技术,本质上是在Node或者Python里“凭空捏造”一个浏览器。
第二层是TLS指纹模拟层。这是最容易被忽略的一层,也是最重要的。你的请求从代码发出,经过TLS握手到达服务端,在这个过程中,你的客户端TLS指纹(JA3/JA4指纹)会暴露你的真实身份。Node的TLS指纹和Chrome的TLS指纹截然不同。Shape的网关会记录每个请求的TLS指纹,如果前端的JS显示你在Chrome环境里运行,但请求的TLS指纹是Node的,不匹配,直接判定为自动化。
所以,完整的解决方案是:用Node执行Shape的challenge计算(走JS逆向路线),但发请求时必须用能模拟Chrome TLS指纹的HTTP客户端。这个方案我在实际测试中是验证过的,可行、稳定,但需要踩的坑不少。
4. 实操过程:从抓包分析到完整模拟的完整闭环
4.1 抓包定位采集与challenge入口
第一步不是去看JS代码,而是先把请求链路画清楚。对美西南场景,我在本地启动mitmproxy,把浏览器的代理指过去,然后正常访问目标页面。重点关注以下几类请求:
- 页面首次加载时请求的Telemetry JS脚本,记录它的URL和参数。
- 埋点上报的接口(通常是形如
/collect、/t之类的POST请求),看它带了哪些参数,参数是否加密。 - 主动触发的challenge流程,比如快速刷新页面或者用脚本模拟点击时,观察返回的challenge结构。
抓包的同时,我会同步打开DevTools的Sources面板,在关键JS文件上下断点,观察执行流程。mitmproxy的好处是可以写Python脚本对流量做实时处理,比如自动解包请求、提取关键参数,甚至在返回内容里做替换,便于调试。
有一个实用技巧:在mitmproxy的脚本里对challenge响应的JSON做格式化输出,把动态加载的JS代码保存到本地,然后用prettier格式化后做静态分析。这样比在Chrome里直接看混淆代码要高效很多。
4.2 断点调试与JS逻辑还原
拿到challenge模块的JS代码后,我用Chrome DevTools的Formatter功能先做初步格式化,然后再用本地Node环境配合jsdom或者同类库做动态补充验证。
具体做法是:
- 用正则提取JS里的字符串数组,先还原最简单的字符串解密逻辑。
- 找到challenge的计算入口函数,这个函数通常有明确的特征:接收一个配置对象,返回一个计算结果对象。
- 在计算入口函数处打断点,逐步执行,记录每一步的输入输出,把算法流程摸清楚。
这里有一个典型的坑:最新版Shape的JS代码里故意设置了反调试逻辑。最常见的是检测DevTools是否打开(通过Debugger对象的差异检测、console.log调用栈检测、Debugger statement死循环等)。用本地Node环境执行可以绕开一大部分反调试逻辑,但有些代码片段依赖浏览器API,直接跑会报错。我的做法是:把这些依赖浏览器API的地方用jsdom补全或自定义桩函数替换,保证代码整体能跑通。
实际还原过程中,你可能会发现challenge算法并不复杂,复杂的是它把环境和结果绑定在一起。比如它会在计算过程里读取window.screen.width、navigator.hardwareConcurrency、Canvas指纹等值,然后把它们参与哈希计算或加密运算。服务端拿到结果后反推:如果你的环境参数和TLS指纹对应的环境不一致,马上判废。
4.3 构造完整浏览器环境的关键点
在Node里构造浏览器环境,我自己用下来比较靠谱的方案是:Puppeteer + stealth插件,或者纯Node环境配合自定义的浏览器环境补全库。两种方案各有优劣:
- 方案A:Puppeteer + stealth插件。优点是环境几乎是真实浏览器,环境检测点基本全过;缺点是性能和资源占用高,并发能力差。适合量小的场景。
- 方案B:纯Node环境 + 自定义环境补全。优点是并发能力强、速度快、资源占用少;缺点是需要自己维护一套环境补全逻辑,对Shape的每一次升级都要排查新增检测点。适合量大、需要稳定并发的场景。
我个人的建议是:如果只是做技术验证,方案A就够了;如果要做长期运行的服务,方案B是更优解。
方案B里最关键的是这几点:
- window、document、navigator对象的属性要补全到“枚举顺序”都和真实浏览器一致。很多检测不看值,只看属性是否存在以及顺序是否正确。
- Canvas和WebGL的渲染结果需要预生成。我的做法是:在真实浏览器里跑一次渲染脚本,把渲染结果的数据URI保存下来,然后在Node环境里直接返回这些预生成值。
- 事件行为数据(鼠标轨迹、键盘事件)需要在请求之前生成好,存到对应的采集接口里。
- localStorage和IndexedDB要有合理的初始状态,因此你需要事先模拟正常用户访问过几次,让存储里积累一些“历史数据”。
4.4 TLS指纹的模拟与验证
TLS指纹这块,我用的方案是自己写TLS ClientHello参数配置。具体来说,需要调整这些参数让Node的TLS握手接近Chrome:
- TLS版本:主要是TLS 1.2和TLS 1.3,Chrome对两者的支持策略和密码套件顺序是固定的。
- Cipher Suites顺序:要严格对齐Chrome的列表。
- extensions顺序和内容:Chrome会带特定顺序和数量的扩展,包括server_name、supported_groups、ec_point_formats、application_layer_protocol_negotiation、supported_versions、signature_algorithms、psk_key_exchange_modes、record_size_limit等。
- 椭圆曲线参数:Chrome偏好x25519,这个需要和实际浏览器保持一致。
这些参数凑在一起,产生的JA3/JA4指纹才会和Chrome一致。在实际操作里,我会用Wireshark抓真实浏览器的ClientHello包,提取参数后照抄进自己的HTTP客户端配置里。
另外,HTTP/2的设置也需要注意。Chrome在使用HTTP/2时有一些特有的帧发送顺序和头部压缩策略,Node的原生HTTP/2实现和Chrome有差异。如果服务端开启了HTTP/2指纹检测(Shape的网关确实支持),那还需要调整HTTP/2层面的行为。
4.5 从单请求打通到流程编排
当所有单点都打通之后,剩下的工作是流程编排。我在美西南、xbk这类场景里实际跑的流程是:
- 获取页面基础数据并解析出初始cookie。
- 加载Telemetry JS,用自定义环境执行,生成一份“环境指纹上报数据”。
- 发送埋点上报请求,拿到服务端对应的会话标记。
- 发起业务请求,根据响应判断是否触发challenge。
- 如果触发,则加载challenge JS,在自定义环境里完成计算,回传结果。
- 拿到有效会话cookie后,继续正常业务请求。
整个流程用Python做调度,Node服务处理JS执行,请求库处理TLS指纹。Python这边每次发起请求前,先向Node服务请求一份计算好的challenge结果和对应的时间戳,然后在HTTP请求里带上。这样可以保证每次请求的“环境数据”和“执行结果”都是配套的。
5. 常见问题与排查技巧实录
5.1 指纹上报成功但业务请求仍被拦截
这是我被问得最多的问题。现象是:埋点上报请求200,cookie也成功设置了,但一发业务请求就被challenge或直接拦截。
排查思路是:先对比成功和失败请求之间的差异。用Wireshark或mitmproxy同时抓两个请求的包,逐项对比请求头顺序、cookie值、TLS指纹、HTTP头是否存在多余或缺失字段。大部分情况下你会发现,业务请求漏带了一些cookie字段,或者TLS指纹和前端JS上报的环境不一致。
另外,Shape的决策不是单次请求决定的,它会综合最近几次请求的行为做概率评估。如果你前面的“热身请求”表现得太顺滑(没有真实用户常见的犹豫、回退、点击偏移),也可能触发后端风控。解决方法是适当引入一些行为噪音,比如随机的鼠标轨迹、随机的阅读时长、偶尔的页面滚动。
5.2 本地执行正常,部署到服务器后被识别
这个问题非常经典。原因有两点:一是本机浏览器环境上下文比较完整,换了干净的服务器环境就缺了关键属性;二是服务器的出口IP被标记过,属于Shape黑名单区域。
第一点好解决,把部署环境搭成和本地一致的仿真环境即可。第二点比较麻烦,需要更换IP、清理关联数据,并且在部署前先做“热身”——用正常的浏览器行为去访问几次目标站点,让服务端记录一些信任数据。如果出口IP质量太差,建议考虑换一个更干净的IP段。
5.3 Challenge明明算对了,却显示校验失败
核心原因通常是时间戳或随机数传递错误。Shape的challenge配置里带有服务端生成的时间戳和nonce,客户端计算时要把它们参与运算。有些实现里还会把前一次请求的cookie值一并参与计算。如果你的流程里cookie或时间戳传递链路出了问题,算出的结果就是错的。
另一个容易被忽略的点是编码问题。challenge计算结果通常要求用特定的编码格式(如base64、hex、或自定义的字符集)回传。你把结果算对了,编码格式搞错了,服务端也校验不过。建议在本地把各种编码方式都试一遍,确认目标站点用的是哪一种。
5.4 快速排查对照表
| 异常现象 | 最常见原因 | 排查方向 |
|---|---|---|
| 埋点上报被拒 | JS环境不完整,某环境检测项不匹配 | 检查window/navigator属性枚举顺序、Canvas/WebGL预生成值 |
| 拿到cookie但业务接口403 | TLS指纹或请求头顺序与上报环境不一致 | 抓包对比真实浏览器与自动化请求的TLS ClientHello差异 |
| Challenge计算超时 | Node事件循环中同步阻塞或异步任务处理慢 | 检查算法里是否包含循环等待、休眠等逻辑,优化计算效率 |
| 部署环境不稳定 | 服务器环境缺少浏览器特征或IP被标记 | 使用独立IP、重置存储、预跑正常访问流程建立信任 |
| 高频请求触发风控 | 并发模型太机械,缺少行为随机性 | 加入随机延迟、随机行为时长、适当控制并发峰值 |
5.5 几个值得养成的好习惯
第一,所有关键节点都要有日志。每一步请求的可能的返回体、执行时间、关键参数,都要记录清楚。Shape这类系统是动态的,昨天还能过的请求今天可能就不过了,有日志才能快速定位变化点。
第二,定期重复测试。Shape会不定期更新JS逻辑和防护策略,建议每周做一次完整回归,确认自己的解决方案在目标场景下依然有效。
第三,给Node服务和Python调度之间留出接口扩展的空间。Shape的challenge类型可能会增加,算法可能会调整,你的执行服务最好做成插件化结构,新算法来了能快速接入。
第四,不要在一台机器上同时跑多个业务场景的账号/环境数据。隔离是保护你整个链路稳定性的基础。
6. 对F5 Shape技术演进方向的一点观察
做过几轮Shape逆向之后,我越来越觉得这个系统的核心能力不在单一的某个技术点,而在于它把多个检测维度做成了“组合拳”。前端JS采集、后端流量分析、TLS指纹识别、行为模型评估、动态challenge,这些技术单独拿出来都有对应的绕过方案,但当它们组合在一起,并且随时动态调整权重时,整体防护强度就上了一个大台阶。
最新版F5 Shape给我的感受是,它正在从“规则对抗”转向“模型对抗”和“行为对抗”。具体体现在:规则是固定判断条件,可以通过逆向直接找出并绕过;模型则是对大量样本训练出的统计规律,没有明确的规则可寻,只能靠模拟出统计学上“正常”的行为分布来通过检测。这对逆向者提出了更高要求——你不仅要懂代码,还要对浏览器底层的各种行为模型有深入理解。
另外,Shape对基础设施的依赖也在增强。它对请求的TLS指纹、HTTP/2行为、DNS解析路径、IP信誉等多维信息做关联分析,单纯模拟浏览器JS执行已经远远不够。这种趋势是行业性的,不只是Shape一家,主流的反爬产品都在往这个方向走。
所以我建议对这块感兴趣的朋友,不要只盯着JS逆向这一个点。多花时间研究浏览器底层原理、TLS协议细节、HTTP/2行为特征,这些基础能力才是长期对抗中的真正护城河。工具和技术会过时,对底层原理的认知不会。
从实操角度讲,我不推荐你去逆向最新版在售所有JS,尤其是一上来就挑战高强度混淆。建议先从老版本或者demo站点开始练手,跑通了再升级。这跟打游戏练装备是一样的道理,装备等级不够直接去刷高级副本,挫败感会很强,也不利于建立起系统的分析框架。
我的个人体会是:F5 Shape的逆向分析,最大的价值不在于“攻破”了它,而在于整个分析过程中,你会被迫把一个现代反爬系统背后的技术栈全部翻一遍——从浏览器API到TLS协议,从行为分析到统计学模型。这套知识体系打下来,未来遇到任何类似的反爬系统,你都不会发怵。