浏览器指纹大版本修改检测分析

欢迎学员来参加小肩膀广东逆向安全百人聚会:

广东安全逆向圈子 | 十年逆向教育二次聚会

本目录分析 BrowserScan 首页 https://www.browserscan.net/zh 的浏览器版本检测,使用当前定制 Firefox 完成 baseline 与 hooked 两轮采集。

目标类型:浏览器指纹/版本一致性检测分析。没有验证码、签名 token 或业务登录链。

成功口径:

  • 页面显示 Browser=Firefox、Browser Version=151.0
  • Header UA 与 JavaScript UA 一致。
  • /api/8e3f1bbb 的 isBrowserVersionCorrect 返回通过。
  • baseline 与 hooked 结果一致。

采集结果

项目
Baseline
Hooked
页面
https://www.browserscan.net/zh
相同
Browser
Firefox
Firefox
Browser Version
151.0
151.0
JavaScript UA
Firefox/151.0
Firefox/151.0
Header UA
Firefox/151.0
Firefox/151.0
/api/8e3f1bbb[1,1,1,1,null,"",null]
相同
指纹真实度
80%
80%

页面存在时区与 IP 位置减分,但浏览器名称、版本和 UA 没有减分。这些网络位置问题与本次版本检测结论无关。

3. 当前版本身份面

navigator.userAgent
= Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:151.0)
  Gecko/20100101 Firefox/151.0

navigator.appVersion = 5.0 (Windows)
navigator.platform = Win32
navigator.oscpu = Windows NT 10.0; Win64; x64
navigator.vendor = ""
navigator.product = Gecko
navigator.productSub = 20100101
navigator.buildID = 20181001000000
navigator.userAgentData = null

UA 中存在两个应同步的 Firefox 版本点:

rv:151.0
Firefox/151.0

只修改其中一个会形成直接矛盾。

4. 检测点一:UAParser

BrowserScan 当前共享 bundle 使用 UAParser。Browser Checker 在 CxFbXgIB.js 中执行:

new UAParser().getResult()

随后读取:

result.browser.name
result.browser.version
result.browser.major
result.engine.name
result.os.name
result.ua

本轮解析结果:

browser.name = Firefox
browser.version = 151.0
browser.major = 151
engine.name = Gecko

页面卡片直接展示 Firefox 和 151.0。

UAParser 的浏览器匹配存在优先级,Edge/Opera 必须先于 Chrome,Safari 使用 Version/x 而不是 Safari/605... 作为浏览器版本。code.js 已按这一原则复现常见浏览器解析。

5. 检测点二:Header UA 与 JavaScript UA

BrowserScan 请求:

GET https://tls.browserscan.net/api/tls

返回的 user_agent 是服务端看到的 HTTP Header UA。本轮为 Firefox 151.0,与 navigator.userAgent 完全一致。

首页 Cozm_HMb.js 将以下材料传给从 ClTriCii.js 导入的 q0

q0({
  browser: jsUA.ua,
  header: headerUA,
  clientHints,
  experimental,
  speechVoice,
})

这说明版本判断不是单纯读取 DOM:服务端同时拿到 JavaScript UA 和网络 Header UA。

6. 检测点三:后端一致性接口

真实请求:

POST https://www.browserscan.net/api/8e3f1bbb

网络 body 为:

{"_k":"...","_p":"..."}

_k/_p 每轮变化,是加密封装。不能把历史值写死,也不能仅凭抓包密文推断字段;明文参数结构以主 bundle 的 q0({...}) 调用点为准。

当前响应:

[1,1,1,1,null,"",null]

主 bundle 的数组映射为:

下标
字段
当前结果
0
isUACorrect
true
1
isOSCorrect
true
2
isBrowserVersionCorrect
true
3
isBrowserCorrect
true
4
uaFullVersion
null
5
platformVersion
空字符串
6
isMobile
null/false

Firefox 没有 navigator.userAgentData,因此 uaFullVersion=null 在当前环境是合理结果,不代表版本检测失败。

7. 检测点四:UA Client Hints

当前 Firefox:

navigator.userAgentData = null
Sec-CH-UA* = 不发送

BrowserScan 会异步尝试获取 Client Hints,并将结果纳入一致性接口。Firefox 强行增加 Chromium 风格的 brands、fullVersionList 或 Sec-CH-UA-Full-Version-List 会与 Gecko 外观冲突。

Chromium 版本修改则必须同时维护:

brands
fullVersionList
uaFullVersion
platformVersion
Sec-CH-UA
Sec-CH-UA-Full-Version-List
Sec-CH-UA-Platform
Sec-CH-UA-Mobile

8. 检测点五:CSS/Navigator/Window 特征版本

CxFbXgIB.js 的 Browser Checker 不止运行 UAParser。它并行采集:

CSS computed 特征
Navigator computed 特征
Window feature computed 特征

然后调用特征推算函数,获得:

engine
version
liedCSS
liedJS
liedWindow

页面将推算版本与 UAParser 的 browser.major 比较。比较规则:

没有特征版本 -> 暂不判错
完全相等 -> 通过
特征版本为 A-B 且 UA major 位于区间 -> 通过
其它情况 -> 版本不一致

因此将 Firefox 151 UA 改成明显不同的大版本,但保留当前 CSS/API/Window feature,可能被判定为 Different browser version。

9. 检测点六:引擎与跨 realm 外观

Browser Checker 还观察:

  • Gecko/Blink/WebKit 的异常栈长度特征。
  • DocumentFragment、隐藏元素与 iframe contentWindow
  • 主窗口与 iframe/跨 realm 的属性和方法行为。
  • descriptor、prototype、constructor、异常和 native toString 外观。

这意味着仅在主窗口覆盖 navigator.userAgent 不够。about:blank iframe、worker 或新 realm 暴露原始版本时,版本身份会分裂。

10. Hooked 调用证据

hooked/jscall/browser_version_calls.jsonl 中,BrowserScan 自身脚本读取:

字段
BrowserScan 调用数
代表调用点
Navigator.userAgent
22
CCZS-DkR.js:11:2421
Navigator.appVersion
1
k4WjIakY.js
 -> Cozm_HMb.js
Navigator.vendor
8
Cozm_HMb.js:18
Navigator.product
1
CCZS-DkR.js:9
Navigator.platform
2
CCZS-DkR.js:25
Navigator.oscpu
1
Cozm_HMb.js
 / CCZS-DkR.js

buildIDproductSub 和 userAgentData 在 probe 中被读取,但本轮 Hook 日志没有记录到 BrowserScan 自身直接调用栈。因此它们属于环境一致性面;不能把它们描述成已确认的首页主判定输入。

11. JavaScript 复现

文件:code.js

入口:

BrowserScanBrowserVersionRepro.collect({
  headerUA"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:151.0) Gecko/20100101 Firefox/151.0",
  featureVersion"150-152",
}).then(console.log);

复现内容:

  • Navigator 版本字段采集。
  • Firefox、Chrome、Chromium、Edge、Opera、Safari UA 版本解析。
  • UA Client Hints brands/fullVersionList 解析。
  • Header UA 与 JavaScript UA 对比。
  • Header 与 JS 的浏览器名称/版本对比。
  • Browser Checker 的精确版本/版本区间比较。

12. 修改建议

12.1 配置模型

不要增加互不关联的散点版本开关。建议在当前 fpfile/配置体系中建立一个 browser profile:

browserProfile
├─ name
├─ version / major
├─ engine / engineVersion
├─ userAgent / appVersion
├─ vendor / product / productSub
├─ platform / oscpu
├─ userAgentData / UA-CH
├─ HTTP User-Agent
└─ TLS/HTTP2 profile

所有版本相关返回值和 Header 从同一 profile 派生。

12.2 Firefox 同版本修改

若只在 Firefox 内切换版本,应同步:

rv:x.y
Firefox/x.y
浏览器主版本
引擎 feature version
版本相关 CSS.supports/WebAPI 行为
插件/PDF Viewer/字体等随版本变化的组合
升级通道、buildID 和资源版本的合理关系

保持:

vendor = ""
product = Gecko
productSub = 20100101
userAgentData = null
不发送 Sec-CH-UA*

12.3 跨引擎伪装

Firefox 直接伪装 Chromium 风险很高,因为还要同步:

UA Client Hints
window.chrome / Blink API
CSS 与 Window feature
异常栈/descriptor/prototype
iframe/worker realm
TLS/HTTP2
PDF Viewer/plugins
字体、Canvas、WebGL、Audio

建议默认仅支持同引擎、同 OS profile 内的版本控制。

12.4 Firefox/domtrace 实现位置建议

配置解析层:
  读取 browserProfile,默认关闭,不新增散点环境变量。

网络层:
  HTTP User-Agent 与 UA-CH 统一从 profile 派生。

Navigator 层:
  userAgent/appVersion/vendor/product/productSub/platform/oscpu/userAgentData。

Realm 层:
  主窗口、iframe、about:blank、worker 使用同一 profile。

一致性层:
  descriptor、prototype、native toString 与真实 Firefox WebIDL 外观一致。

验证层:
  BrowserScan Header/JavaScript UA 相同;
  Browser Version 正确;
  /api/8e3f1bbb 前四位为 1;
  Browser Checker feature version 与 UA major 匹配。

13. 最终结论

BrowserScan 的浏览器版本检测是多层组合:

UAParser 版本
+ HTTP Header UA
+ JavaScript UA
+ UA Client Hints
+ CSS/Navigator/Window feature version
+ 引擎与跨 realm 外观
+ 服务端一致性接口

当前 Firefox 151.0 在 baseline 和 hooked 两轮均通过浏览器版本、浏览器名称、UA 和 OS 一致性检查。

修改版本时,最关键的不是替换一个字符串,而是让网络层、Navigator、UA-CH、引擎特征、跨 realm 和 TLS/HTTP2 从同一 browser profile 派生并保持一致。

如意本人联系方式