reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
213
vendor-new/xbsReverseSkill/web-reverse-env/SKILL.md
Normal file
213
vendor-new/xbsReverseSkill/web-reverse-env/SKILL.md
Normal file
@@ -0,0 +1,213 @@
|
||||
---
|
||||
name: web-reverse-env
|
||||
description: 面向 Web/JS 逆向中的浏览器补环境技能,覆盖 Proxy 吐环境、原型链修复、native toString 保护、描述符保护、navigator/document/storage/canvas/WebGL/crypto/performance/WebRTC/Worker 等模块化补环境分析与构建。用户明确提到“补环境”“浏览器环境模拟”“Proxy吐环境”“document.all”“webdriver”“plugins/mimeTypes”“指纹”“jsdom/vm/node 补环境”“补环境 skill”“验证码/风控环境修补”时使用。
|
||||
---
|
||||
|
||||
# Web逆向补环境
|
||||
|
||||
## 概览
|
||||
|
||||
使用这个技能时,不要把“补环境”理解成给 `window/document/navigator` 随便补几个值。
|
||||
|
||||
先把任务视为四类问题之一:
|
||||
|
||||
1. 缺对象或缺属性,代码直接报 `undefined`
|
||||
2. 对象存在,但原型链、描述符、getter/setter、`toString` 暴露
|
||||
3. 基础对象能跑,但被指纹、栈、异步对象、通信对象继续检测
|
||||
4. 补环境成本过高,应切换到浏览器内执行或 `JsRpc/WebSocket` 替代路线
|
||||
|
||||
默认优先走“诊断驱动”的补环境路线,而不是一次性补全浏览器。
|
||||
|
||||
## 快速分流
|
||||
|
||||
根据任务特点,优先读取对应参考文档:
|
||||
|
||||
- 用户要先理解整体流程或不知道从哪里下手:读 [01-workflow.md](references/01-workflow.md)
|
||||
- 用户要模块化设计、生成补丁或定义模块边界:读 [02-module-contracts.md](references/02-module-contracts.md)
|
||||
- 用户卡在 `document.all`、native `toString`、堆栈、反检测、`WebRTC/AudioContext/Worker`:读 [03-special-cases.md](references/03-special-cases.md)
|
||||
- 用户想知道当前草稿还缺什么:读 [04-missing-points.md](references/04-missing-points.md)
|
||||
- 用户要继续扩展外部阅读池或找更多非大站来源:读 [05-diversified-reading.md](references/05-diversified-reading.md)
|
||||
- 用户要做回归验证、模块验收或判断补丁是否够稳:读 [06-validation.md](references/06-validation.md)
|
||||
- 用户要了解现有 GitHub 框架、来源映射和代码级模式:读 [07-source-map.md](references/07-source-map.md)
|
||||
|
||||
## 标准工作流
|
||||
|
||||
### 1. 先判断目标路线
|
||||
|
||||
优先判断当前任务属于哪一类:
|
||||
|
||||
- 只需要最小补丁让样本跑通
|
||||
- 需要做成可复用模块
|
||||
- 需要沉淀为 skill / 框架
|
||||
- 需要高强度检测点补齐
|
||||
- 需要切换到浏览器内执行或 `JsRpc`
|
||||
|
||||
如果是最后一种,不要硬补到底。
|
||||
|
||||
### 2. 先采集,再诊断
|
||||
|
||||
如果用户还没有真实浏览器样本:
|
||||
|
||||
- 优先使用 [collect-browser-env.js](scripts/collect-browser-env.js)
|
||||
- 采集 `navigator`、`document`、`location`、`history`、`screen`
|
||||
- 采集 `cookie`、`localStorage`、`sessionStorage`
|
||||
- 采集基础 `canvas/webgl` 指纹
|
||||
|
||||
如果用户已经有运行日志或报错:
|
||||
|
||||
- 优先使用 [observe-runtime.js](scripts/observe-runtime.js)
|
||||
- 让缺失路径、描述符探测、原型链探测、调用错误先浮出来
|
||||
|
||||
不要在没有诊断信息的前提下大面积硬补对象。
|
||||
|
||||
### 3. 按模块补,而不是按页面乱补
|
||||
|
||||
补环境时,优先按模块推进:
|
||||
|
||||
1. `prototype-builder`
|
||||
2. `descriptor-guard`
|
||||
3. `native-protector`
|
||||
4. 浏览器对象模块
|
||||
5. `performance-module`
|
||||
6. `crypto-module`
|
||||
7. `fingerprint-module`
|
||||
8. `audio-fingerprint-module`
|
||||
9. `webrtc-module`
|
||||
10. `worker-module`
|
||||
11. `proxy-observer`
|
||||
12. `math-precision-module`
|
||||
13. `stack-clean-module`
|
||||
14. `document-all-module`
|
||||
|
||||
如果站点一开始就死在 `document.all`,把特殊对象优先级前置。
|
||||
|
||||
### 4. 先补“能继续执行”的缺口
|
||||
|
||||
补丁优先级默认如下:
|
||||
|
||||
1. `undefined` 或缺对象导致主流程中断
|
||||
2. getter / setter / 方法的 `Illegal invocation`
|
||||
3. 原型链和描述符不一致
|
||||
4. native `toString` 暴露
|
||||
5. 指纹异常
|
||||
6. 异步对象、通信对象、高强度检测点
|
||||
|
||||
### 5. 把高强度检测点单独看
|
||||
|
||||
遇到下面这些对象时,不要按“普通 BOM/DOM 补值”处理:
|
||||
|
||||
- `document.all`
|
||||
- `window.crypto.getRandomValues`
|
||||
- `performance.timing`
|
||||
- `RTCPeerConnection`
|
||||
- `AudioContext` / `OfflineAudioContext`
|
||||
- `Worker` / `SharedWorker`
|
||||
- `Math` 精度差异
|
||||
|
||||
这类对象优先参考:
|
||||
|
||||
- [03-special-cases.md](references/03-special-cases.md)
|
||||
- [02-module-contracts.md](references/02-module-contracts.md)
|
||||
|
||||
## 资源使用规则
|
||||
|
||||
### scripts
|
||||
|
||||
优先使用现有脚本,而不是每次重写。
|
||||
|
||||
- [collect-browser-env.js](scripts/collect-browser-env.js)
|
||||
- 浏览器侧采集环境种子
|
||||
- [observe-runtime.js](scripts/observe-runtime.js)
|
||||
- Proxy 吐环境与缺口聚合
|
||||
- [protect-native.js](scripts/protect-native.js)
|
||||
- `Function.prototype.toString`、getter、setter 保护
|
||||
- [build-plugin-graph.js](scripts/build-plugin-graph.js)
|
||||
- 构建 `plugins/mimeTypes/enabledPlugin` 图结构
|
||||
- [build-navigator-module.js](scripts/build-navigator-module.js)
|
||||
- 生成 `Navigator/navigator/plugins/mimeTypes/getBattery` 模块补丁草稿
|
||||
- [build-document-module.js](scripts/build-document-module.js)
|
||||
- 生成 `Document/HTMLDocument/document/cookie/query API` 模块补丁草稿
|
||||
- [build-storage-module.js](scripts/build-storage-module.js)
|
||||
- 生成 `Storage/localStorage/sessionStorage` 模块补丁草稿
|
||||
- [build-fingerprint-module.js](scripts/build-fingerprint-module.js)
|
||||
- 生成 `screen/canvas/WebGL/battery/windowMetrics` 模块补丁草稿
|
||||
- [build-crypto-module.js](scripts/build-crypto-module.js)
|
||||
- 生成 `crypto/msCrypto/getRandomValues/subtle` 模块补丁草稿
|
||||
- [build-performance-module.js](scripts/build-performance-module.js)
|
||||
- 生成 `performance.now/timeOrigin/timing` 模块补丁草稿
|
||||
- [build-webrtc-module.js](scripts/build-webrtc-module.js)
|
||||
- 生成 `RTCPeerConnection/RTCDataChannel` 模块补丁草稿
|
||||
- [build-worker-module.js](scripts/build-worker-module.js)
|
||||
- 生成 `Worker/SharedWorker/MessagePort/BroadcastChannel` 模块补丁草稿
|
||||
- [analyze-gap-log.js](scripts/analyze-gap-log.js)
|
||||
- 把 Proxy 吐环境日志归并成模块优先级与下一步动作
|
||||
- [merge-storage-state.js](scripts/merge-storage-state.js)
|
||||
- 合并 cookie / storage 运行态
|
||||
- [scaffold-module.js](scripts/scaffold-module.js)
|
||||
- 生成模块骨架
|
||||
|
||||
### references
|
||||
|
||||
当任务复杂或需要解释时,再读细分文档,不要一次性把全部 references 都装进上下文。
|
||||
|
||||
优先只读与当前阶段直接相关的 1 到 2 份。
|
||||
|
||||
### assets
|
||||
|
||||
优先把运行期数据放进 `assets` 样例,而不是写死到脚本里。
|
||||
|
||||
已准备的样例:
|
||||
|
||||
- [fingerprint-seed.example.json](assets/fingerprint-seed.example.json)
|
||||
- [navigator-sample.example.json](assets/navigator-sample.example.json)
|
||||
- [document-sample.example.json](assets/document-sample.example.json)
|
||||
- [plugin-graph.example.json](assets/plugin-graph.example.json)
|
||||
- [storage-state.example.json](assets/storage-state.example.json)
|
||||
- [crypto-sample.example.json](assets/crypto-sample.example.json)
|
||||
- [performance-sample.example.json](assets/performance-sample.example.json)
|
||||
- [webrtc-sample.example.json](assets/webrtc-sample.example.json)
|
||||
- [worker-sample.example.json](assets/worker-sample.example.json)
|
||||
|
||||
## 产出要求
|
||||
|
||||
当这个技能被用于实际任务时,默认产出应尽量包含:
|
||||
|
||||
- 当前任务属于哪一种补环境问题
|
||||
- 建议优先补哪些模块
|
||||
- 需要读取哪些 references
|
||||
- 需要运行哪些 scripts
|
||||
- 如果用户要代码,给出模块化补丁,而不是一整份混乱脚本
|
||||
- 如果风险较高,明确指出纯 JS 难以完整模拟的部分
|
||||
|
||||
## 特殊规则
|
||||
|
||||
### `document.all`
|
||||
|
||||
明确把 `document.all` 视为特殊对象。
|
||||
|
||||
不要轻易宣称纯 JS 可以完全等价模拟。
|
||||
|
||||
优先说明:
|
||||
|
||||
- 这是 `native-addon / V8 API / fallback` 三选一问题
|
||||
- 如果只有纯 JS,必须明确 residual risk
|
||||
|
||||
### 代理反检测
|
||||
|
||||
不要默认认为“代理越多越好”。
|
||||
|
||||
如果用户目标站点会检测代理:
|
||||
|
||||
- 先用代理诊断
|
||||
- 再决定是否去掉部分代理或换更完整 trap
|
||||
|
||||
### `JsRpc` / 浏览器内执行
|
||||
|
||||
当补环境成本明显过高时,可以建议:
|
||||
|
||||
- 真浏览器内执行
|
||||
- `JsRpc`
|
||||
- `WebSocket` 回传结果
|
||||
|
||||
这属于技能允许的替代路线,不是偏题。
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Web<65><62><EFBFBD><EFBFBD><F2B2B9BB><EFBFBD>"
|
||||
short_description: "<22><><EFBFBD><EFBFBD>JS<4A><53><EFBFBD><EFBFBD><F2B2B9BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD><C4BC>ܣ<EFBFBD><DCA3><EFBFBD><EFBFBD><EFBFBD>Proxy<78>»<EFBFBD><C2BB><EFBFBD><EFBFBD><EFBFBD>ԭ<EFBFBD><D4AD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>native<76><65><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ָ<EFBFBD><D6B8><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǿ<EFBFBD>ȼ<EFBFBD><C8BC><EFBFBD>ģ<EFBFBD>顣"
|
||||
default_prompt: "ʹ<><CAB9><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ܷ<EFBFBD><DCB7><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƚ<EFBFBD><C8BD><EFBFBD>references<65><73>scripts<74><73><EFBFBD><EFBFBD>ģ<EFBFBD>黯<EFBFBD><E9BBAF><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϡ<EFBFBD>"
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"crypto": {
|
||||
"exposeCrypto": true,
|
||||
"installMsCrypto": true,
|
||||
"maxGetRandomValuesLength": 65536,
|
||||
"hasSubtle": true,
|
||||
"subtleMethods": [
|
||||
"digest",
|
||||
"generateKey",
|
||||
"importKey",
|
||||
"exportKey",
|
||||
"sign",
|
||||
"verify",
|
||||
"encrypt",
|
||||
"decrypt"
|
||||
],
|
||||
"randomValues": [
|
||||
17,
|
||||
34,
|
||||
51,
|
||||
68,
|
||||
85,
|
||||
102,
|
||||
119,
|
||||
136
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"document": {
|
||||
"URL": "https://example.invalid/path/index.html?scene=env",
|
||||
"referrer": "https://www.google.com/",
|
||||
"documentURI": "https://example.invalid/path/index.html?scene=env",
|
||||
"compatMode": "CSS1Compat",
|
||||
"dir": "",
|
||||
"title": "env-codex sample",
|
||||
"designMode": "off",
|
||||
"readyState": "complete",
|
||||
"contentType": "text/html",
|
||||
"inputEncoding": "UTF-8",
|
||||
"domain": "example.invalid",
|
||||
"characterSet": "UTF-8",
|
||||
"charset": "UTF-8",
|
||||
"hidden": false,
|
||||
"visibilityState": "visible",
|
||||
"cookie": "sid=example_sid; device_id=example_device"
|
||||
},
|
||||
"queryMap": {
|
||||
"#captcha": "div",
|
||||
".token-input": "input",
|
||||
"meta[name=\"viewport\"]": "meta"
|
||||
},
|
||||
"ids": {
|
||||
"captcha-root": "div",
|
||||
"token-input": "input"
|
||||
},
|
||||
"tags": {
|
||||
"meta": [
|
||||
"meta"
|
||||
],
|
||||
"script": [
|
||||
"script"
|
||||
],
|
||||
"canvas": [
|
||||
"canvas"
|
||||
]
|
||||
},
|
||||
"specialCases": {
|
||||
"documentAllMode": "fallback"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"sessionName": "chrome-win-sample",
|
||||
"navigator": {
|
||||
"platform": "Win32",
|
||||
"language": "zh-CN",
|
||||
"languages": ["zh-CN", "zh", "en-US", "en"],
|
||||
"vendor": "Google Inc.",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||||
"hardwareConcurrency": 8,
|
||||
"deviceMemory": 8,
|
||||
"maxTouchPoints": 0,
|
||||
"webdriver": false
|
||||
},
|
||||
"screen": {
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"availWidth": 1890,
|
||||
"availHeight": 1030,
|
||||
"colorDepth": 24,
|
||||
"pixelDepth": 24
|
||||
},
|
||||
"windowMetrics": {
|
||||
"innerWidth": 1914,
|
||||
"innerHeight": 930,
|
||||
"outerWidth": 1914,
|
||||
"outerHeight": 1050,
|
||||
"devicePixelRatio": 1
|
||||
},
|
||||
"fingerprint": {
|
||||
"canvas2dDataUrl": "data:image/png;base64,PLACEHOLDER_CANVAS_2D",
|
||||
"canvasWebglDataUrl": "data:image/png;base64,PLACEHOLDER_CANVAS_WEBGL",
|
||||
"webglVendor": "Google Inc. (Intel)",
|
||||
"webglRenderer": "ANGLE (Intel, Intel(R) UHD Graphics 630 Direct3D11 vs_5_0 ps_5_0, D3D11)"
|
||||
},
|
||||
"battery": {
|
||||
"charging": true,
|
||||
"chargingTime": 0,
|
||||
"dischargingTime": "Infinity",
|
||||
"level": 0.97
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||||
"platform": "Win32",
|
||||
"vendor": "Google Inc.",
|
||||
"language": "zh-CN",
|
||||
"languages": ["zh-CN", "zh", "en-US", "en"],
|
||||
"webdriver": false,
|
||||
"hardwareConcurrency": 8,
|
||||
"deviceMemory": 8,
|
||||
"maxTouchPoints": 0,
|
||||
"pdfViewerEnabled": true
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"performance": {
|
||||
"timeOrigin": 1711957200000,
|
||||
"navigationStart": 1711957200000,
|
||||
"nowOffset": 12.345,
|
||||
"timing": {
|
||||
"navigationStart": 1711957200000,
|
||||
"fetchStart": 1711957200000,
|
||||
"domainLookupStart": 1711957200001,
|
||||
"domainLookupEnd": 1711957200002,
|
||||
"connectStart": 1711957200002,
|
||||
"connectEnd": 1711957200004,
|
||||
"requestStart": 1711957200005,
|
||||
"responseStart": 1711957200009,
|
||||
"responseEnd": 1711957200013,
|
||||
"domLoading": 1711957200015,
|
||||
"domInteractive": 1711957200020,
|
||||
"domContentLoadedEventStart": 1711957200021,
|
||||
"domContentLoadedEventEnd": 1711957200022,
|
||||
"domComplete": 1711957200027,
|
||||
"loadEventStart": 1711957200028,
|
||||
"loadEventEnd": 1711957200029
|
||||
},
|
||||
"memory": {
|
||||
"totalJSHeapSize": 52428800,
|
||||
"usedJSHeapSize": 24117248,
|
||||
"jsHeapSizeLimit": 4294705152
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"plugins": [
|
||||
{
|
||||
"name": "Chrome PDF Viewer",
|
||||
"filename": "internal-pdf-viewer",
|
||||
"description": "Portable Document Format",
|
||||
"items": [
|
||||
{
|
||||
"type": "application/pdf",
|
||||
"suffixes": "pdf",
|
||||
"description": "Portable Document Format"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"mimeTypes": [
|
||||
{
|
||||
"type": "application/pdf",
|
||||
"suffixes": "pdf",
|
||||
"description": "Portable Document Format",
|
||||
"enabledPluginName": "Chrome PDF Viewer"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"cookie": "sid=example_sid; device_id=example_device",
|
||||
"localStorage": {
|
||||
"device_id": "example_device",
|
||||
"user_prefs": "{\"lang\":\"zh-CN\"}"
|
||||
},
|
||||
"sessionStorage": {
|
||||
"csrf_token": "example_csrf"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"webrtc": {
|
||||
"allowCandidateLeak": false,
|
||||
"iceTransportPolicy": "all",
|
||||
"bundlePolicy": "balanced",
|
||||
"rtcpMuxPolicy": "require",
|
||||
"defaultOfferSdp": "v=0\r\no=- 0 0 IN IP4 127.0.0.1\r\ns=env-codex-offer\r\nt=0 0\r\n",
|
||||
"defaultAnswerSdp": "v=0\r\no=- 0 0 IN IP4 127.0.0.1\r\ns=env-codex-answer\r\nt=0 0\r\n",
|
||||
"iceCandidates": [
|
||||
{
|
||||
"candidate": "candidate:842163049 1 udp 1677729535 0.0.0.0 9 typ host generation 0 ufrag env network-id 1",
|
||||
"sdpMid": "0",
|
||||
"sdpMLineIndex": 0,
|
||||
"usernameFragment": "env"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"worker": {
|
||||
"scriptBaseUrl": "https://example.invalid/assets/",
|
||||
"sharedWorkerName": "env-shared-worker",
|
||||
"broadcastChannels": [
|
||||
"env-sync",
|
||||
"risk-channel"
|
||||
],
|
||||
"inlineScripts": {
|
||||
"/worker.js": "self.onmessage = function (event) { self.postMessage({ echo: event.data }); };",
|
||||
"/shared.js": "onconnect = function (event) { var port = event.ports[0]; port.start(); };"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,799 @@
|
||||
# JS 逆向补环境通用流程总结(中文版增强版)
|
||||
|
||||
这份文档讨论的不是“把 Node 里缺的对象补几个上去”这么简单。
|
||||
|
||||
真正成熟的补环境,目标是:
|
||||
|
||||
- 让目标代码在本地的执行路径尽量接近浏览器内的真实路径。
|
||||
- 让对象的值、原型链、属性描述符、访问器、函数元信息、原生指纹都尽量接近浏览器。
|
||||
- 让异常、堆栈、状态数据、指纹对象也尽量不暴露“这是 Node 假环境”。
|
||||
- 最终恢复真实参数、真实 cookie、真实分支,而不是只做到“不报错”。
|
||||
|
||||
## 1. 先判断:这题到底该不该走补环境
|
||||
|
||||
常见信号:
|
||||
|
||||
- 本地运行直接报 `window is not defined`、`document is not defined`、`navigator is not defined`、`location is not defined`。
|
||||
- 浏览器里结果正常,本地结果为空、报错、签名不一致、cookie 不一致。
|
||||
- 目标代码明显在读浏览器对象、指纹对象、DOM/BOM、存储态。
|
||||
- 目标代码依赖 `document.cookie`、`localStorage`、`sessionStorage`、`canvas`、`WebGL`、`WebSocket`、`Worker`、`meta` 标签等运行时对象。
|
||||
- 代码切不干净,硬扣算法的成本已经高于“保留大部分原始逻辑 + 外围补环境”。
|
||||
|
||||
如果只是纯算法函数,不依赖浏览器环境,那么通常不值得补环境,直接还原算法更高效。
|
||||
|
||||
## 2. 永远先定位目标点,再决定补什么
|
||||
|
||||
不要一上来就补整套浏览器。
|
||||
|
||||
先定位:
|
||||
|
||||
- 目标参数或 cookie 是什么。
|
||||
- 它最终写到了哪里。
|
||||
- 是哪个函数返回的。
|
||||
- 是通过 `document.cookie` 写入,还是通过请求头、请求体、query 参数发送。
|
||||
- 它的上游依赖链是什么。
|
||||
|
||||
常见定位入口:
|
||||
|
||||
- Hook `document.cookie`
|
||||
- 搜参数名、cookie 名
|
||||
- 断在 `XMLHttpRequest`、`fetch`、`WebSocket.send`
|
||||
- 沿请求调用链往上回溯
|
||||
- webpack 站点先找模块入口、runtime、构造器
|
||||
- JSVMP 站点先找对外调用入口,而不是一开始就死抠内部实现
|
||||
|
||||
补环境的本质不是“补对象”,而是“围绕真实执行入口,补它运行时真正依赖的对象和状态”。
|
||||
|
||||
## 3. 决定执行策略:手补、半框架、全浏览器
|
||||
|
||||
### 3.1 手动补环境
|
||||
|
||||
适合:
|
||||
|
||||
- 缺失对象不多
|
||||
- 目标依赖链清晰
|
||||
- 你已经把核心逻辑抽出来了
|
||||
|
||||
优点:
|
||||
|
||||
- 最干净
|
||||
- 最容易理解真实依赖
|
||||
- 最适合沉淀成可复用 skill
|
||||
|
||||
缺点:
|
||||
|
||||
- 遇到复杂 DOM、原型链检测、native 指纹检测时,维护成本高
|
||||
|
||||
### 3.2 模拟浏览器环境
|
||||
|
||||
常见工具:
|
||||
|
||||
- `jsdom`
|
||||
- `vm`
|
||||
- `vm2`
|
||||
- `catvm`
|
||||
- `qxVm`
|
||||
- `sdenv`
|
||||
- `node-sandbox`
|
||||
|
||||
适合:
|
||||
|
||||
- DOM/BOM 依赖较多
|
||||
- 想保留更多原始页面代码
|
||||
- 不想手写太多空对象
|
||||
|
||||
注意:
|
||||
|
||||
- 框架只是脚手架,不是魔法。
|
||||
- 真正难的依然是:原型链、描述符、访问器、原生指纹、动态状态恢复。
|
||||
|
||||
### 3.3 真实浏览器执行
|
||||
|
||||
常见方式:
|
||||
|
||||
- Selenium
|
||||
- Headless Chrome
|
||||
- Playwright
|
||||
|
||||
适合:
|
||||
|
||||
- 本地补环境代价明显高于“直接在浏览器执行”
|
||||
- 目标强依赖真实 DOM、事件流、浏览器内部实现、渲染对象、定时器行为
|
||||
|
||||
这个路线不一定是最终方案,但很适合作为“先跑通,再逆向简化”的过渡方案。
|
||||
|
||||
## 4. 第一层:先补最小可运行环境
|
||||
|
||||
第一批对象通常包括:
|
||||
|
||||
- `window`
|
||||
- `self`
|
||||
- `top`
|
||||
- `parent`
|
||||
- `frames`
|
||||
- `globalThis`
|
||||
- `document`
|
||||
- `navigator`
|
||||
- `location`
|
||||
- `history`
|
||||
- `screen`
|
||||
|
||||
第一层目标不是“完整仿真”,而是:
|
||||
|
||||
- 让代码先跑起来
|
||||
- 让访问路径暴露出来
|
||||
- 让缺失依赖快速浮现
|
||||
|
||||
典型第一步包括:
|
||||
|
||||
- 建立全局对象引用关系
|
||||
- 给 `navigator.userAgent`、`language`、`platform` 等常用字段赋值
|
||||
- 给 `location.href`、`host`、`hostname`、`origin`、`pathname` 等字段赋值
|
||||
- 初始化 `document.cookie`
|
||||
- 初始化 `document.referrer`、`document.head`、`document.documentElement`
|
||||
|
||||
但要明确一点:
|
||||
|
||||
这只是起步,不是终点。很多检测并不只看值,还看值在什么层级、怎么取到、是不是原生访问器。
|
||||
|
||||
## 5. 第二层:尽快让环境“自吐”
|
||||
|
||||
这是补环境里最重要的一步之一。
|
||||
|
||||
典型做法是给这些对象尽早套上 `Proxy`:
|
||||
|
||||
- `window`
|
||||
- `document`
|
||||
- `navigator`
|
||||
- `location`
|
||||
- `history`
|
||||
- `screen`
|
||||
- 高风险函数
|
||||
|
||||
重点监控:
|
||||
|
||||
- `get`
|
||||
- `set`
|
||||
- `has`
|
||||
- `deleteProperty`
|
||||
- 必要时监控函数调用
|
||||
|
||||
这样做的价值是:
|
||||
|
||||
- 把“盲猜缺什么”变成“脚本自己告诉你它读了什么”
|
||||
- 把补环境任务转成一份可迭代的待补清单
|
||||
- 让你知道哪些对象必须精细补,哪些对象只是路过
|
||||
|
||||
## 6. `undefined` 驱动补环境是主路线,但不是唯一标准
|
||||
|
||||
很多时候,确实是通过 Proxy 吐出来的 `undefined` 来判断“这里需要补环境”。
|
||||
|
||||
这是非常高效的主路线。
|
||||
|
||||
但它有边界:
|
||||
|
||||
- 某些值虽然不是 `undefined`,但值不真实,依然会算错。
|
||||
- 某些检测根本不关心值是不是 `undefined`,而是看原型链、描述符、访问器。
|
||||
- 某些逻辑依赖的是 cookie、`localStorage`、`sessionStorage` 里“已经存在的历史状态”。
|
||||
- 某些检测通过异常、堆栈、`toString`、`instanceof` 路径触发,Proxy 不会直接提醒你。
|
||||
|
||||
所以正确理解应该是:
|
||||
|
||||
- Proxy 吐 `undefined` 是补环境的主入口。
|
||||
- 但不是补环境完成度的唯一判断标准。
|
||||
|
||||
## 7. 第三层:补值,不如补结构
|
||||
|
||||
很多初学者会这样补:
|
||||
|
||||
```js
|
||||
navigator = { userAgent: "..." };
|
||||
document = {};
|
||||
location = {};
|
||||
```
|
||||
|
||||
这只能解决最基础的报错。
|
||||
|
||||
真正经常导致失败的是结构问题:
|
||||
|
||||
- 属性本该在实例上,你补到了对象本体上
|
||||
- 属性本该在原型上,你补到了实例上
|
||||
- 方法本来是访问器,你补成了普通值
|
||||
- 属性本来不可枚举,你补成了可枚举
|
||||
- 方法本来不可写或不可配置,你把描述符补错了
|
||||
- 函数本来有固定 `name` / `length`,你写出来的不一致
|
||||
|
||||
所以补环境的关键思路,应该从“补字段”升级为“补对象结构”。
|
||||
|
||||
## 8. 第四层:先补原型链,再补实例值
|
||||
|
||||
这是真正的分水岭。
|
||||
|
||||
真实浏览器对象通常不是:
|
||||
|
||||
```js
|
||||
window.navigator = { userAgent: "..." };
|
||||
```
|
||||
|
||||
更接近的模型通常是:
|
||||
|
||||
- 存在 `Navigator` 构造器
|
||||
- `navigator` 是某个原型链实例
|
||||
- 属性可能在原型上,也可能通过 getter 暴露
|
||||
- 方法通常在原型上
|
||||
- 属性描述符与浏览器一致
|
||||
|
||||
因此更合理的顺序通常是:
|
||||
|
||||
1. 先建构造器
|
||||
2. 再建 prototype
|
||||
3. 再补 prototype 上的方法和属性
|
||||
4. 再创建实例
|
||||
5. 最后把实例挂到全局对象上
|
||||
|
||||
高价值对象包括:
|
||||
|
||||
- `Navigator`
|
||||
- `History`
|
||||
- `Location`
|
||||
- `Screen`
|
||||
- `Document`
|
||||
- `HTMLDocument`
|
||||
- `Storage`
|
||||
- `WebSocket`
|
||||
- `Worker`
|
||||
- `EventTarget`
|
||||
- `Node`
|
||||
- `Element`
|
||||
- `HTMLElement`
|
||||
- `HTMLCanvasElement`
|
||||
|
||||
也就是说,补环境不是“给对象赋值”,而是“重建浏览器对象图”。
|
||||
|
||||
## 9. 第五层:保护原生方法的 `toString`
|
||||
|
||||
很多站点会检测:
|
||||
|
||||
- 方法是不是原生函数
|
||||
- `Function.prototype.toString.call(fn)` 的返回值是否像浏览器原生实现
|
||||
|
||||
如果你手写:
|
||||
|
||||
```js
|
||||
document.createElement = function () {}
|
||||
```
|
||||
|
||||
那么很容易被看出来,因为它的 `toString()` 不会像:
|
||||
|
||||
```js
|
||||
function createElement() { [native code] }
|
||||
```
|
||||
|
||||
常见保护思路包括:
|
||||
|
||||
- 改写目标函数自身的 `toString`
|
||||
- 统一 Hook `Function.prototype.toString`
|
||||
- 给受保护函数打隐藏标记,再在 `Function.prototype.toString` 中按标记返回伪原生源码
|
||||
|
||||
高频需要保护的方法包括:
|
||||
|
||||
- `addEventListener`
|
||||
- `removeEventListener`
|
||||
- `createElement`
|
||||
- `getAttribute`
|
||||
- `setAttribute`
|
||||
- `querySelector`
|
||||
- `getContext`
|
||||
- `Storage.prototype.getItem`
|
||||
- `Storage.prototype.setItem`
|
||||
|
||||
## 10. `toString.toString` 这种深层检测也要考虑
|
||||
|
||||
成熟站点不只会检测:
|
||||
|
||||
- `fn.toString()`
|
||||
- `Function.prototype.toString.call(fn)`
|
||||
|
||||
还会继续检测:
|
||||
|
||||
- `fn.toString.toString()`
|
||||
- `Function.prototype.toString.toString()`
|
||||
|
||||
也就是说,它不只检查“你伪装的函数像不像原生”,还检查“你拿来做伪装的 `toString` 本身像不像原生”。
|
||||
|
||||
因此成熟方案通常要同时保护:
|
||||
|
||||
- 被伪装的目标函数
|
||||
- 目标函数自己的 `.toString`
|
||||
- `Function.prototype.toString`
|
||||
- 与 `toString` 相关的 `name` / `length` / 描述符
|
||||
|
||||
## 11. 第六层:保护 getter / setter 访问器
|
||||
|
||||
真实浏览器里很多属性不是普通数据属性,而是访问器属性。
|
||||
|
||||
例如:
|
||||
|
||||
- `document.cookie`
|
||||
- `location.href`
|
||||
- `navigator.userAgent`
|
||||
- 一些 DOM 节点属性
|
||||
|
||||
如果目标代码用了:
|
||||
|
||||
- `Object.getOwnPropertyDescriptor`
|
||||
- `__lookupGetter__`
|
||||
- 直接检测 getter / setter 的 `toString`
|
||||
|
||||
那么你补成普通值就会露馅。
|
||||
|
||||
更稳妥的补法应当是:
|
||||
|
||||
- 使用 `Object.defineProperty`
|
||||
- 显式提供 `get`
|
||||
- 必要时提供 `set`
|
||||
- 对 getter / setter 本身也做 `toString` 保护
|
||||
- 保持 `enumerable` / `configurable` 等标志尽量接近浏览器
|
||||
|
||||
另外还要注意异常路径:
|
||||
|
||||
- 某些 getter 在非法调用时应该抛异常
|
||||
- 某些 setter 在类型不合法时不能静默通过
|
||||
|
||||
所以访问器不只是“能取到值”,还要尽量对齐:
|
||||
|
||||
- 返回值
|
||||
- 报错时机
|
||||
- 报错类型
|
||||
- 报错信息形态
|
||||
|
||||
## 12. 第七层:保护属性描述符
|
||||
|
||||
很多补环境失败,不是值错了,而是描述符错了。
|
||||
|
||||
重点需要关注:
|
||||
|
||||
- `enumerable`
|
||||
- `configurable`
|
||||
- `writable`
|
||||
- `get`
|
||||
- `set`
|
||||
|
||||
还要注意:
|
||||
|
||||
- 某些属性本应定义在 prototype 上
|
||||
- 某些属性本应定义在实例上
|
||||
- 某些属性本应不出现在 `Object.keys`
|
||||
- 某些属性本应能通过 `in` 访问,但不应是 own property
|
||||
|
||||
所以补环境时,经常要把下面这些检测一起纳入验证:
|
||||
|
||||
- `Object.getOwnPropertyDescriptor`
|
||||
- `Object.getOwnPropertyDescriptors`
|
||||
- `Object.keys`
|
||||
- `Reflect.ownKeys`
|
||||
- `hasOwnProperty`
|
||||
- `propertyIsEnumerable`
|
||||
- `in`
|
||||
- `Object.getPrototypeOf`
|
||||
- `constructor`
|
||||
- `instanceof`
|
||||
- `Symbol.toStringTag`
|
||||
- `Object.prototype.toString.call(obj)`
|
||||
|
||||
## 13. 第八层:保护函数的 `name` 和 `length`
|
||||
|
||||
很多环境检测会直接看:
|
||||
|
||||
- `fn.name`
|
||||
- `fn.length`
|
||||
|
||||
如果浏览器原生方法本来:
|
||||
|
||||
- 名字固定
|
||||
- 参数个数固定
|
||||
|
||||
而你手写的函数不一致,就可能被识别。
|
||||
|
||||
补函数时要注意:
|
||||
|
||||
- 用命名函数而不是匿名函数
|
||||
- 参数个数尽量与真实浏览器一致
|
||||
- 必要时通过 `Object.defineProperty` 保护 `name` / `length`
|
||||
- 如果统一封装安全函数或 native 模板,要确保封装后不会把这些信息破坏
|
||||
|
||||
还要注意:
|
||||
|
||||
- 包装函数之后,外层壳子很容易把原始 `name` / `length` 改掉
|
||||
- getter / setter 函数自身也可能被检测 `name`
|
||||
- 某些站点还会顺带看 `caller`、`arguments` 的访问行为
|
||||
|
||||
## 14. Node 环境检测必须单独考虑
|
||||
|
||||
很多站点真正先做的,不是浏览器对象检测,而是先识别“你是不是在 Node 里跑”。
|
||||
|
||||
常见检测点:
|
||||
|
||||
- `process`
|
||||
- `global`
|
||||
- `Buffer`
|
||||
- `require`
|
||||
- `module`
|
||||
- `exports`
|
||||
- Node 版本差异导致的内置对象行为差异
|
||||
- Node 特有的全局痕迹
|
||||
|
||||
所以成熟流程里要考虑:
|
||||
|
||||
- 是否隐藏或清理 Node 痕迹
|
||||
- 是否让全局对象更接近浏览器
|
||||
- 是否避免把明显的 CommonJS / Node 运行特征暴露给目标代码
|
||||
|
||||
## 15. 错误异常检测也必须纳入流程
|
||||
|
||||
有些站点会主动触发错误,然后检查:
|
||||
|
||||
- 抛出的异常类型是否合理
|
||||
- `message` 是否像浏览器
|
||||
- `name` 是否正确
|
||||
- 报错位置是否合理
|
||||
|
||||
这意味着补环境不能只补“正常路径”,还要补“异常路径”。
|
||||
|
||||
## 16. 堆栈检测也要考虑
|
||||
|
||||
错误一旦抛出,目标代码可能会检查:
|
||||
|
||||
- `error.stack`
|
||||
- 栈中的函数名
|
||||
- 栈中的文件路径格式
|
||||
- 是否出现 `vm`、本地文件路径、Node 模块路径
|
||||
|
||||
因此成熟方案里常常还要考虑:
|
||||
|
||||
- 栈裁剪
|
||||
- 栈伪装
|
||||
- 避免在高风险函数中暴露本地包装层
|
||||
|
||||
## 17. 第九层:按对象类型拆分补环境策略
|
||||
|
||||
这一层很适合后面抽象成 skill 的模块能力。
|
||||
|
||||
### 17.1 BOM 对象
|
||||
|
||||
常见包括:
|
||||
|
||||
- `window`
|
||||
- `self`
|
||||
- `top`
|
||||
- `parent`
|
||||
- `frames`
|
||||
- `navigator`
|
||||
- `history`
|
||||
- `location`
|
||||
- `screen`
|
||||
|
||||
常见补法:
|
||||
|
||||
- 建立对象之间的引用关系
|
||||
- 构造器 + prototype + 实例三层补齐
|
||||
- 补属性描述符
|
||||
- 补常见方法
|
||||
- 补 `toString`
|
||||
- 补 getter / setter
|
||||
|
||||
### 17.2 `navigator`
|
||||
|
||||
高频字段包括:
|
||||
|
||||
- `userAgent`
|
||||
- `platform`
|
||||
- `language`
|
||||
- `languages`
|
||||
- `webdriver`
|
||||
- `plugins`
|
||||
- `mimeTypes`
|
||||
- `hardwareConcurrency`
|
||||
- `deviceMemory`
|
||||
- `cookieEnabled`
|
||||
- `vendor`
|
||||
- `appCodeName`
|
||||
- `appName`
|
||||
- `appVersion`
|
||||
- `maxTouchPoints`
|
||||
- `pdfViewerEnabled`
|
||||
- `getBattery`
|
||||
|
||||
高风险点:
|
||||
|
||||
- `navigator.webdriver === false`
|
||||
- `plugins` 不是简单数组,而是类数组结构
|
||||
- `mimeTypes` 通常也不是简单数组
|
||||
- `plugins` 和 `mimeTypes` 之间常有结构关联
|
||||
- `navigator.getBattery()` 常常需要返回 `Promise`
|
||||
- `userAgent`、`platform`、`language`、`languages` 之间需要自洽
|
||||
- 原型归属、描述符、own property 与 prototype property 的差异
|
||||
|
||||
也就是说,`navigator` 不能只补几个字符串,还要补数据结构、返回类型、原型和描述符。
|
||||
|
||||
### 17.3 `history`
|
||||
|
||||
常见方法:
|
||||
|
||||
- `back`
|
||||
- `forward`
|
||||
- `go`
|
||||
- `pushState`
|
||||
- `replaceState`
|
||||
|
||||
很多时候不一定真需要逻辑,但需要:
|
||||
|
||||
- 原型上存在
|
||||
- `name/length` 正确
|
||||
- `toString` 像原生
|
||||
|
||||
### 17.4 `location`
|
||||
|
||||
重点:
|
||||
|
||||
- 许多字段应通过 getter 暴露
|
||||
- `href` 变化可能联动 `host`、`hostname`、`origin`、`pathname`
|
||||
- 有些站会检查 `location.toString()`
|
||||
|
||||
### 17.5 `screen`
|
||||
|
||||
常见字段:
|
||||
|
||||
- `width`
|
||||
- `height`
|
||||
- `availWidth`
|
||||
- `availHeight`
|
||||
- `colorDepth`
|
||||
- `pixelDepth`
|
||||
|
||||
### 17.6 `document`
|
||||
|
||||
高频依赖包括:
|
||||
|
||||
- `cookie`
|
||||
- `referrer`
|
||||
- `readyState`
|
||||
- `head`
|
||||
- `body`
|
||||
- `documentElement`
|
||||
- `createElement`
|
||||
- `addEventListener`
|
||||
- `querySelector`
|
||||
- `getElementById`
|
||||
- `getElementsByTagName`
|
||||
- `all`
|
||||
|
||||
高风险点:
|
||||
|
||||
- `document.cookie` 通常要做成访问器
|
||||
- DOM 节点返回值不能永远是空对象
|
||||
- `createElement("canvas")` 经常需要特殊返回
|
||||
- `meta`、`script`、隐藏节点都可能参与补环境
|
||||
|
||||
`document.all` 需要单独拿出来说,因为它不是普通字段:
|
||||
|
||||
- 它是历史兼容对象
|
||||
- 很多站会拿它做特殊检测
|
||||
- 它在真假判断、相等判断、类型判断上的行为都比较特殊
|
||||
|
||||
所以在真正做 skill 时,`document.all` 最好做成一个独立能力模块,而不是普通属性赋值。
|
||||
|
||||
### 17.7 `localStorage` / `sessionStorage`
|
||||
|
||||
通常不是简单对象,而应尽量按 `Storage` 结构去补。
|
||||
|
||||
常见方法:
|
||||
|
||||
- `getItem`
|
||||
- `setItem`
|
||||
- `removeItem`
|
||||
- `clear`
|
||||
- `key`
|
||||
|
||||
高风险点:
|
||||
|
||||
- `length`
|
||||
- 存取行为
|
||||
- 原型归属
|
||||
- 描述符
|
||||
|
||||
补这两个对象时还要注意:
|
||||
|
||||
- 业务逻辑可能真的依赖已有值,而不是只检测存在性
|
||||
- 需要提前注入站点依赖的键值
|
||||
- 某些参数生成逻辑会从 `localStorage` 或 `sessionStorage` 里读取种子值
|
||||
|
||||
所以补环境时不仅要补对象结构,还要补运行态数据。
|
||||
|
||||
### 17.8 `WebSocket`
|
||||
|
||||
如果代码只检测存在性,可能只需要:
|
||||
|
||||
- 构造器
|
||||
- `prototype.send`
|
||||
- `prototype.close`
|
||||
- `prototype.addEventListener`
|
||||
|
||||
如果代码真在走链路,就要考虑:
|
||||
|
||||
- 实例属性
|
||||
- 回调事件
|
||||
- `readyState`
|
||||
- 发送参数格式
|
||||
- `binaryType`
|
||||
- `bufferedAmount`
|
||||
- `extensions`
|
||||
- `protocol`
|
||||
|
||||
### 17.9 `Worker`
|
||||
|
||||
有些站并不真的依赖 Worker 干活,而是拿它做检测。
|
||||
|
||||
常见检查:
|
||||
|
||||
- `Worker` 是否存在
|
||||
- 是否能 `new Worker()`
|
||||
- 是否存在 `postMessage`
|
||||
- 是否存在 `terminate`
|
||||
|
||||
复杂情况下还要考虑:
|
||||
|
||||
- `importScripts`
|
||||
- `onmessage`
|
||||
- 消息流
|
||||
- `Worker.toString()`
|
||||
- 原型链和实例行为
|
||||
|
||||
## 18. canvas / WebGL 指纹要作为独立模块考虑
|
||||
|
||||
高频指纹包括:
|
||||
|
||||
- canvas 指纹
|
||||
- WebGL 指纹
|
||||
- 音频指纹
|
||||
- 屏幕指纹
|
||||
- 插件 / MIME 类型指纹
|
||||
|
||||
这里尤其要注意:
|
||||
|
||||
- 不是所有题都要完整补,但流程上必须预留能力
|
||||
- canvas 经常从 `createElement("canvas")`、`getContext()`、`toDataURL()` 进入
|
||||
- WebGL 经常从 context、`getParameter()`、vendor / renderer 信息进入
|
||||
|
||||
也就是说,指纹对象补丁最好作为单独模块,而不是零散塞在 `document` 里。
|
||||
|
||||
## 19. Cookie 与存储态必须一起考虑
|
||||
|
||||
很多站点的参数生成,并不只依赖静态环境对象,还依赖运行态数据:
|
||||
|
||||
- `document.cookie`
|
||||
- `localStorage`
|
||||
- `sessionStorage`
|
||||
- 页面初始化时种下的 token
|
||||
- 某些前置请求返回的数据
|
||||
|
||||
因此补环境判断不能只看 Proxy 吐出来的 `undefined`。
|
||||
|
||||
还要看:
|
||||
|
||||
- 有没有读取已有 cookie
|
||||
- 有没有读取已有 storage 值
|
||||
- 是否依赖“浏览器中本来就存在的历史状态”
|
||||
|
||||
也就是说,补环境不仅是补“对象”,还要补“状态”。
|
||||
|
||||
## 20. 第十层:把“报错修复”升级成“检测对抗”
|
||||
|
||||
补环境不是只让脚本跑通。
|
||||
|
||||
你还要考虑它会不会检测:
|
||||
|
||||
- 原型链
|
||||
- native code 指纹
|
||||
- getter / setter
|
||||
- 描述符
|
||||
- `name`
|
||||
- `length`
|
||||
- 枚举行为
|
||||
- own keys
|
||||
- `instanceof`
|
||||
- `constructor`
|
||||
- `Symbol.toStringTag`
|
||||
- `Object.prototype.toString.call(obj)`
|
||||
- Node 痕迹
|
||||
- 异常与堆栈
|
||||
- 指纹对象
|
||||
|
||||
因此一份成熟的补环境流程,至少应包含三类验证:
|
||||
|
||||
1. 值验证
|
||||
这个字段是不是存在,值对不对。
|
||||
|
||||
2. 结构验证
|
||||
它在实例上还是原型上,描述符是否合理。
|
||||
|
||||
3. 指纹验证
|
||||
`toString`、`toString.toString`、`name`、`length`、枚举性、原型链是否像真的。
|
||||
|
||||
## 21. 实操顺序建议:先粗补,再精补
|
||||
|
||||
推荐顺序如下:
|
||||
|
||||
1. 先定位目标参数、cookie、真实调用入口
|
||||
2. 建最小全局对象
|
||||
3. 给高风险对象套 `Proxy`
|
||||
4. 先让代码跑起来
|
||||
5. 根据 `get/set/has` 访问轨迹补缺失项
|
||||
6. 把“直接赋值”逐步替换成“构造器 + prototype + 实例”
|
||||
7. 把关键方法替换成 native 风格函数
|
||||
8. 修正 getter / setter
|
||||
9. 修正属性描述符
|
||||
10. 修正 `name` / `length`
|
||||
11. 修正 `toString` 及 `toString.toString` 路径
|
||||
12. 修正 `Object.prototype.toString.call`
|
||||
13. 修正 `instanceof` / `constructor`
|
||||
14. 检查 Node 痕迹
|
||||
15. 检查异常与堆栈
|
||||
16. 检查 canvas / WebGL 等指纹对象
|
||||
17. 检查 cookie / storage 是否需要预置真实值
|
||||
18. 最后回到真实调用点校验结果
|
||||
|
||||
## 22. 补环境最终要验证什么
|
||||
|
||||
不是“没报错”就算完成。
|
||||
|
||||
至少要验证:
|
||||
|
||||
- 返回的参数值是否一致
|
||||
- `document.cookie` 写入是否一致
|
||||
- 结果长度、格式、字段名是否一致
|
||||
- 后续请求是否成功
|
||||
- 是否走了和浏览器相同的调用分支
|
||||
- 是否触发了隐藏 fallback 逻辑
|
||||
|
||||
必要时还要验证:
|
||||
|
||||
- 描述符是否一致
|
||||
- `toString` 是否一致
|
||||
- `toString.toString` 是否一致
|
||||
- `name/length` 是否一致
|
||||
- `instanceof` 是否成立
|
||||
- 原型链层级是否一致
|
||||
- 异常类型、错误信息、堆栈是否暴露 Node 痕迹
|
||||
- cookie / storage 是否与真实页面初始化状态一致
|
||||
|
||||
## 23. 对后续 skill 设计最有价值的能力模块
|
||||
|
||||
建议优先沉淀这些模块:
|
||||
|
||||
- 目标定位模块
|
||||
- `document.cookie` Hook 模块
|
||||
- Proxy 吐环境模块
|
||||
- 最小全局环境模板
|
||||
- BOM 对象模板
|
||||
- DOM / `document` 模板
|
||||
- `Storage` 模板
|
||||
- `WebSocket` 模板
|
||||
- `Worker` 模板
|
||||
- 原型链构建模块
|
||||
- native `toString` 保护模块
|
||||
- `toString.toString` 保护模块
|
||||
- getter / setter 保护模块
|
||||
- 属性描述符保护模块
|
||||
- `name` / `length` 对齐模块
|
||||
- Node 痕迹清理模块
|
||||
- 异常与堆栈伪装模块
|
||||
- canvas / WebGL 指纹模块
|
||||
- cookie / storage 预置状态模块
|
||||
- 结果校验模块
|
||||
|
||||
## 24. 一句话总结
|
||||
|
||||
真正成熟的补环境流程,不是“给对象补几个字段”,而是:
|
||||
|
||||
先定位真实执行入口,再让环境自吐,再按访问路径逐步补齐对象图,并同时对原型链、原生指纹、访问器、属性描述符、函数元信息、Node 痕迹、异常堆栈、指纹对象、cookie 与 storage 状态做一致性修复,最后回到真实调用点校验结果。
|
||||
@@ -0,0 +1,443 @@
|
||||
# 模块输入输出契约
|
||||
|
||||
这份文档定义 future skill 每个核心模块的输入 / 输出边界,避免后续脚本和文档互相耦合。
|
||||
|
||||
## 1. 通用约定
|
||||
|
||||
所有模块尽量遵循同一套输入 / 输出字段。
|
||||
|
||||
### 输入
|
||||
|
||||
- `target`
|
||||
- 当前要补的对象名或模块名
|
||||
- `seed`
|
||||
- 真实浏览器采集到的种子数据
|
||||
- `runtime`
|
||||
- 当前 Node / jsdom / vm 里的运行时对象
|
||||
- `diagnostics`
|
||||
- Proxy 吐环境日志、异常日志、堆栈、缺口清单
|
||||
- `options`
|
||||
- 开关项,如 `protectNative`、`strictDescriptor`、`logOpen`
|
||||
|
||||
### 输出
|
||||
|
||||
- `patchPlan`
|
||||
- 本模块准备补哪些点
|
||||
- `patchCode`
|
||||
- 可执行补丁代码或代码片段
|
||||
- `runtimeState`
|
||||
- 补丁后应写入的运行态
|
||||
- `validation`
|
||||
- 本模块对应的检查点
|
||||
- `residualRisk`
|
||||
- 当前模块仍无法完全脚本化处理的风险
|
||||
|
||||
## 2. `prototype-builder`
|
||||
|
||||
### 输入
|
||||
|
||||
- `target`
|
||||
- `prototypeChainSnapshot`
|
||||
- `instanceShapeSnapshot`
|
||||
- `constructorMeta`
|
||||
|
||||
### 输出
|
||||
|
||||
- `patchPlan`
|
||||
- 构造函数
|
||||
- 原型链
|
||||
- 实例创建
|
||||
- 全局挂载
|
||||
- `patchCode`
|
||||
- 构造函数定义代码
|
||||
- `Object.setPrototypeOf` / `defineProperty` 代码
|
||||
- `validation`
|
||||
- `instanceof`
|
||||
- `Object.getPrototypeOf`
|
||||
- `constructor === Xxx`
|
||||
|
||||
## 3. `native-protector`
|
||||
|
||||
### 输入
|
||||
|
||||
- `functions`
|
||||
- `getters`
|
||||
- `setters`
|
||||
- `expectedNativeStrings`
|
||||
- `metaOverrides`
|
||||
|
||||
### 输出
|
||||
|
||||
- `patchPlan`
|
||||
- 哪些函数要保护
|
||||
- 哪些 getter / setter 要保护
|
||||
- 是否启用 `toString.toString` 防护
|
||||
- `patchCode`
|
||||
- `Function.prototype.toString` 接管代码
|
||||
- 每个函数的 native 保护代码
|
||||
- `validation`
|
||||
- `fn.toString()`
|
||||
- `fn.toString.toString()`
|
||||
- `fn.name`
|
||||
- `fn.length`
|
||||
|
||||
## 4. `descriptor-guard`
|
||||
|
||||
### 输入
|
||||
|
||||
- `target`
|
||||
- `propertyDescriptors`
|
||||
- `symbolTags`
|
||||
- `constructorStrategy`
|
||||
|
||||
### 输出
|
||||
|
||||
- `patchPlan`
|
||||
- 哪些属性用数据描述符
|
||||
- 哪些属性用访问器描述符
|
||||
- `patchCode`
|
||||
- `defineProperty` / `defineProperties`
|
||||
- `validation`
|
||||
- `getOwnPropertyDescriptor`
|
||||
- `propertyIsEnumerable`
|
||||
- `ownKeys`
|
||||
|
||||
## 5. `navigator-module`
|
||||
|
||||
### 输入
|
||||
|
||||
- `seed.navigator`
|
||||
- `seed.plugins`
|
||||
- `seed.mimeTypes`
|
||||
- `seed.userAgentData`
|
||||
- `diagnostics.navigator`
|
||||
|
||||
### 输出
|
||||
|
||||
- `patchPlan`
|
||||
- basic
|
||||
- plugin graph
|
||||
- async capability
|
||||
- automation flags
|
||||
- `patchCode`
|
||||
- `Navigator.prototype` getter / method
|
||||
- `plugins/mimeTypes` 图结构构建
|
||||
- `runtimeState`
|
||||
- `navigator` 实例值
|
||||
- `validation`
|
||||
- `webdriver`
|
||||
- `plugins.length`
|
||||
- `mimeTypes.length`
|
||||
- `getBattery`
|
||||
- `userAgentData`
|
||||
|
||||
## 6. `document-module`
|
||||
|
||||
### 输入
|
||||
|
||||
- `seed.document`
|
||||
- `seed.cookie`
|
||||
- `diagnostics.document`
|
||||
- `specialCases.documentAll`
|
||||
|
||||
### 输出
|
||||
|
||||
- `patchPlan`
|
||||
- basic document
|
||||
- cookie accessor
|
||||
- query API
|
||||
- special object
|
||||
- `patchCode`
|
||||
- `Document.prototype` / `HTMLDocument.prototype`
|
||||
- `document.cookie` getter / setter
|
||||
- `validation`
|
||||
- `document.URL`
|
||||
- `document.readyState`
|
||||
- `document.cookie`
|
||||
- `document.all`
|
||||
- `residualRisk`
|
||||
- 若无底层支持,明确标记 `document.all` 为特殊风险
|
||||
|
||||
## 7. `storage-module`
|
||||
|
||||
### 输入
|
||||
|
||||
- `seed.localStorage`
|
||||
- `seed.sessionStorage`
|
||||
- `seed.cookies`
|
||||
- `diagnostics.storage`
|
||||
|
||||
### 输出
|
||||
|
||||
- `patchPlan`
|
||||
- shape
|
||||
- state
|
||||
- cookie linkage
|
||||
- `patchCode`
|
||||
- `Storage.prototype`
|
||||
- state merge logic
|
||||
- `runtimeState`
|
||||
- 预置 key/value
|
||||
- `validation`
|
||||
- `length`
|
||||
- `getItem`
|
||||
- `setItem`
|
||||
- `removeItem`
|
||||
- `clear`
|
||||
|
||||
## 8. `fingerprint-module`
|
||||
|
||||
### 输入
|
||||
|
||||
- `seed.fingerprint`
|
||||
- `seed.screen`
|
||||
- `seed.navigator`
|
||||
- `diagnostics.fingerprint`
|
||||
|
||||
### 输出
|
||||
|
||||
- `patchPlan`
|
||||
- canvas
|
||||
- webgl
|
||||
- battery
|
||||
- screen
|
||||
- `patchCode`
|
||||
- `toDataURL`
|
||||
- `getContext`
|
||||
- `getParameter`
|
||||
- `getBattery`
|
||||
- `runtimeState`
|
||||
- 指纹种子池
|
||||
- `validation`
|
||||
- canvas 输出
|
||||
- WebGL vendor / renderer
|
||||
- battery 值
|
||||
- screen / window 尺寸一致性
|
||||
|
||||
## 9. `performance-module`
|
||||
|
||||
### 输入
|
||||
|
||||
- `seed.performance`
|
||||
- `seed.windowMetrics`
|
||||
- `diagnostics.performance`
|
||||
|
||||
### 输出
|
||||
|
||||
- `patchPlan`
|
||||
- `performance.now`
|
||||
- `performance.timeOrigin`
|
||||
- `performance.timing`
|
||||
- 时间一致性修正
|
||||
- `patchCode`
|
||||
- `performance` 对象与 timing 结构
|
||||
- `validation`
|
||||
- `performance.now()`
|
||||
- `performance.timeOrigin`
|
||||
- `performance.timing.navigationStart`
|
||||
- 各 timing 字段前后顺序关系
|
||||
|
||||
## 10. `crypto-module`
|
||||
|
||||
### 输入
|
||||
|
||||
- `seed.crypto`
|
||||
- `diagnostics.crypto`
|
||||
- `typedArraySupport`
|
||||
|
||||
### 输出
|
||||
|
||||
- `patchPlan`
|
||||
- `window.crypto`
|
||||
- `window.msCrypto`
|
||||
- `getRandomValues`
|
||||
- `subtle`
|
||||
- `patchCode`
|
||||
- `getRandomValues` 实现
|
||||
- typed array 校验与异常逻辑
|
||||
- `validation`
|
||||
- `Uint8Array`
|
||||
- `Uint16Array`
|
||||
- `Uint32Array`
|
||||
- 长度超限异常
|
||||
|
||||
## 11. `webrtc-module`
|
||||
|
||||
### 输入
|
||||
|
||||
- `seed.webrtc`
|
||||
- `diagnostics.webrtc`
|
||||
- `networkPolicy`
|
||||
|
||||
### 输出
|
||||
|
||||
- `patchPlan`
|
||||
- `RTCPeerConnection`
|
||||
- `RTCRtpSender`
|
||||
- `RTCRtpReceiver`
|
||||
- `RTCDataChannel`
|
||||
- ICE candidate 路径
|
||||
- `patchCode`
|
||||
- 构造函数与方法补丁
|
||||
- `createOffer`
|
||||
- `createDataChannel`
|
||||
- `onicecandidate`
|
||||
- `validation`
|
||||
- 构造函数存在性
|
||||
- `createOffer()`
|
||||
- `createDataChannel()`
|
||||
- 是否泄露本地候选信息
|
||||
|
||||
## 12. `audio-fingerprint-module`
|
||||
|
||||
### 输入
|
||||
|
||||
- `seed.audio`
|
||||
- `diagnostics.audio`
|
||||
|
||||
### 输出
|
||||
|
||||
- `patchPlan`
|
||||
- `AudioContext`
|
||||
- `OfflineAudioContext`
|
||||
- `AudioBuffer`
|
||||
- `getChannelData`
|
||||
- `patchCode`
|
||||
- 构造函数
|
||||
- 音频缓冲返回值
|
||||
- 常见指纹链路模拟
|
||||
- `validation`
|
||||
- `new AudioContext()`
|
||||
- `new OfflineAudioContext()`
|
||||
- `getChannelData()`
|
||||
- 是否满足基础音频指纹调用流程
|
||||
|
||||
## 13. `worker-module`
|
||||
|
||||
### 输入
|
||||
|
||||
- `seed.worker`
|
||||
- `diagnostics.worker`
|
||||
- `messagePolicy`
|
||||
|
||||
### 输出
|
||||
|
||||
- `patchPlan`
|
||||
- `Worker`
|
||||
- `SharedWorker`
|
||||
- `postMessage`
|
||||
- `MessagePort`
|
||||
- `BroadcastChannel`
|
||||
- `patchCode`
|
||||
- 构造函数
|
||||
- 端口通信对象
|
||||
- message 事件
|
||||
- `validation`
|
||||
- `new Worker()`
|
||||
- `new SharedWorker()`
|
||||
- `postMessage`
|
||||
- `onmessage`
|
||||
|
||||
## 14. `math-precision-module`
|
||||
|
||||
### 输入
|
||||
|
||||
- `seed.math`
|
||||
- `diagnostics.math`
|
||||
- `baselineResults`
|
||||
|
||||
### 输出
|
||||
|
||||
- `patchPlan`
|
||||
- 高风险数学函数差异校验
|
||||
- 浏览器 / Node 结果对照
|
||||
- `patchCode`
|
||||
- 精度修正策略
|
||||
- 或风险标注代码
|
||||
- `validation`
|
||||
- 与基线结果比对
|
||||
- 是否命中已知高风险函数
|
||||
|
||||
## 15. `proxy-observer`
|
||||
|
||||
### 输入
|
||||
|
||||
- `targets`
|
||||
- `logConfig`
|
||||
- `skipRules`
|
||||
- `maxDepth`
|
||||
|
||||
### 输出
|
||||
|
||||
- `patchCode`
|
||||
- Proxy 包装代码
|
||||
- `diagnostics`
|
||||
- `missingPaths`
|
||||
- `descriptorAccess`
|
||||
- `prototypeAccess`
|
||||
- `invocationErrors`
|
||||
- `validation`
|
||||
- 是否成功捕获 `get/set/has/apply/construct`
|
||||
|
||||
## 16. `stack-clean-module`
|
||||
|
||||
### 输入
|
||||
|
||||
- `runtimeError`
|
||||
- `stackRules`
|
||||
- `scriptName`
|
||||
|
||||
### 输出
|
||||
|
||||
- `patchCode`
|
||||
- 栈清洗函数
|
||||
- `validation`
|
||||
- 清洗前后栈帧差异
|
||||
- `residualRisk`
|
||||
- 说明是否需要底层 runtime 配合
|
||||
|
||||
## 17. `document-all-module`
|
||||
|
||||
### 输入
|
||||
|
||||
- `mode`
|
||||
- `native-addon`
|
||||
- `v8-api`
|
||||
- `fallback`
|
||||
- `diagnostics.documentAll`
|
||||
|
||||
### 输出
|
||||
|
||||
- `patchPlan`
|
||||
- 选择的实现路线
|
||||
- `patchCode`
|
||||
- 若有底层支持,输出接线代码
|
||||
- 若无底层支持,输出降级兼容代码
|
||||
- `validation`
|
||||
- `== undefined`
|
||||
- `=== undefined`
|
||||
- `typeof`
|
||||
- 调用行为
|
||||
- `residualRisk`
|
||||
- 清楚声明纯 JS 限制
|
||||
|
||||
## 18. 模块依赖关系
|
||||
|
||||
执行顺序建议:
|
||||
|
||||
1. `prototype-builder`
|
||||
2. `descriptor-guard`
|
||||
3. `native-protector`
|
||||
4. 浏览器对象模块
|
||||
5. `performance-module`
|
||||
6. `crypto-module`
|
||||
7. `fingerprint-module`
|
||||
8. `audio-fingerprint-module`
|
||||
9. `webrtc-module`
|
||||
10. `worker-module`
|
||||
11. `proxy-observer`
|
||||
12. `math-precision-module`
|
||||
13. `stack-clean-module`
|
||||
14. `document-all-module`
|
||||
|
||||
如果站点在早期就卡死在 `document.all`,可以把最后一步提前。
|
||||
@@ -0,0 +1,842 @@
|
||||
# GitHub 代码级补环境设计笔记
|
||||
|
||||
这份笔记不是“仓库推荐清单”,而是把几套 GitHub 补环境框架拆成更接近未来 skill 设计稿的模块说明。
|
||||
|
||||
目标只有一个:为后续构建“JS 逆向补环境 skill”沉淀可复用的代码结构、流程和取舍标准。
|
||||
|
||||
## 1. 这份笔记解决什么问题
|
||||
|
||||
围绕下面八个模块做代码级拆解:
|
||||
|
||||
- 原型链模块
|
||||
- native `toString` 模块
|
||||
- 描述符模块
|
||||
- `navigator` 模块
|
||||
- `document` 模块
|
||||
- `Storage` 模块
|
||||
- 指纹模块
|
||||
- Proxy 吐环境模块
|
||||
|
||||
同时把这些横切难点一起纳入设计视角:
|
||||
|
||||
- Node 环境检测
|
||||
- `document.all` 不可检测对象
|
||||
- 报错异常与堆栈清洗
|
||||
- `toString.toString` 二次检测
|
||||
- getter / setter / 方法描述符保护
|
||||
- 函数 `name` / `length` 对齐
|
||||
- `webdriver` / `plugins` / `mimeTypes` / `getBattery`
|
||||
- `canvas` / `WebGL` 指纹
|
||||
- `cookie` / `localStorage` / `sessionStorage` 初始状态
|
||||
|
||||
## 2. 仓库价值映射
|
||||
|
||||
这批仓库里,最值得吸收的不是“整仓照抄”,而是分工:
|
||||
|
||||
- `node-sandbox`
|
||||
- 最像“底层修复层”
|
||||
- 强项是 `document.all`、堆栈清洗、不可变原型、复杂 `navigator.plugins/mimeTypes` 图结构、`getBattery`、`canvas/WebGL`
|
||||
- `qxVm`
|
||||
- 最像“工程化框架层”
|
||||
- 强项是模块拆分、`safefunction`、描述符保护、可配置 Proxy 吐环境、对象级文件布局
|
||||
- `catvm`
|
||||
- 最像“教学拆解层”
|
||||
- 强项是把补环境拆成非常清楚的步骤,适合沉淀为 skill 的解释文本
|
||||
- `boda_jsEnv`
|
||||
- 最像“配置采集与批量生成层”
|
||||
- 强项是浏览器侧采集、指纹种子、dispatch 分发、海量对象生成
|
||||
- `sdenv`
|
||||
- 最像“jsdom + 定点增强层”
|
||||
- 强项是 `document.all` 的 native addon 路线、`navigator` getter native 化、定向浏览器对象修补
|
||||
- `v_jstools`
|
||||
- 更像“前置采集工具链”
|
||||
- 更适合用于网页分析、注入、导出环境,而不是直接当补环境框架骨架
|
||||
|
||||
## 3. 原型链模块
|
||||
|
||||
### 3.1 最有参考价值的来源
|
||||
|
||||
- `node-sandbox/util/init_env.js`
|
||||
- `qxVm/qxVm_sanbox/env/*`
|
||||
- `catvm/__bak/step.js`
|
||||
- `boda_jsEnv/bodaEnv/env/*`
|
||||
|
||||
### 3.2 GitHub 里反复出现的核心套路
|
||||
|
||||
第一类是“先构造函数,再原型,再实例”。
|
||||
|
||||
- 先定义构造函数,并让非法 `new` 时抛 `Illegal constructor`
|
||||
- 再把原型链接对
|
||||
- 最后才创建实例对象并挂到 `window`
|
||||
|
||||
第二类是“实例属性与原型属性分层”。
|
||||
|
||||
- 原型上放方法、访问器、只读能力
|
||||
- 实例上放真实环境值和状态
|
||||
- 不要把原本应该在原型上的东西全部平铺到实例上
|
||||
|
||||
第三类是“复杂对象图要同时补链和补引用关系”。
|
||||
|
||||
- `navigator.plugins` 不是普通数组
|
||||
- `PluginArray -> Plugin -> MimeType` 和 `MimeTypeArray -> MimeType -> enabledPlugin` 是互相引用的图
|
||||
- 只补值不补图结构,很容易被检测
|
||||
|
||||
### 3.3 代码级启发
|
||||
|
||||
`node-sandbox` 的路线更偏真实运行态:
|
||||
|
||||
- 在 `util/init_env.js` 里大量使用 `Object.setPrototypeOf`
|
||||
- `document.all` 直接挂到 `HTMLAllCollection.prototype`
|
||||
- `navigator.plugins`、`navigator.mimeTypes`、`location`、`Storage` 都明确接回对应原型
|
||||
|
||||
`qxVm` 的路线更偏“模块化对象工厂”:
|
||||
|
||||
- 每个对象一个文件
|
||||
- 每个文件内都区分构造函数、属性映射、函数映射、保护逻辑
|
||||
- 适合未来 skill 输出“补某个对象模块”
|
||||
|
||||
`catvm` 的教学价值非常高:
|
||||
|
||||
- 明确提出先看真浏览器原型链,再决定属性该放原型还是实例
|
||||
- 这很适合写进 future skill 的流程说明
|
||||
|
||||
### 3.4 应该沉淀到 future skill 的结论
|
||||
|
||||
- skill 必须先做“对象拓扑建模”,再做属性填充值
|
||||
- skill 输出不能只是一段平铺赋值脚本,至少要区分:
|
||||
- 构造函数层
|
||||
- prototype 层
|
||||
- instance 层
|
||||
- `window/globalThis/self/top` 挂载层
|
||||
- skill 需要有“复杂图对象模板”:
|
||||
- `PluginArray`
|
||||
- `MimeTypeArray`
|
||||
- `Storage`
|
||||
- `Location`
|
||||
- `Navigator`
|
||||
- `Document`
|
||||
|
||||
## 4. native `toString` 模块
|
||||
|
||||
### 4.1 最有参考价值的来源
|
||||
|
||||
- `qxVm/qxVm_sanbox/env/TOOLS/vm_protection.js`
|
||||
- `catvm/CatVm2/tools/vm_safefunction.js`
|
||||
- `sdenv/browser/chrome/navigator.js`
|
||||
- `node-sandbox/README.md`
|
||||
|
||||
### 4.2 这一模块为什么是补环境的核心
|
||||
|
||||
绝大多数初级补环境失败,不是因为值补少了,而是因为:
|
||||
|
||||
- 函数 `toString()` 露馅
|
||||
- getter / setter 的 `toString()` 露馅
|
||||
- `toString.toString()` 二次检测露馅
|
||||
- 方法名、`name`、`length`、`prototype` 描述符不对
|
||||
|
||||
### 4.3 GitHub 里最值得吸收的套路
|
||||
|
||||
`qxVm` 和 `catvm` 都采用了同一类思路:
|
||||
|
||||
- 接管 `Function.prototype.toString`
|
||||
- 用私有符号保存伪造的 native 字符串
|
||||
- 调 `func.toString()` 时返回该伪造串
|
||||
- 再对新的 `Function.prototype.toString` 自身做一层保护
|
||||
- 从而尽量过掉 `location.reload.toString.toString()` 这一类检测
|
||||
|
||||
这是 future skill 里必须作为独立模块存在的能力。
|
||||
|
||||
### 4.4 skill 设计上必须拆出的子能力
|
||||
|
||||
- `protectFunctionNative(fn, displayName?)`
|
||||
- 保护普通函数
|
||||
- `protectGetter(target, key)`
|
||||
- 保护 getter 的 `toString`
|
||||
- `protectSetter(target, key)`
|
||||
- 保护 setter 的 `toString`
|
||||
- `protectFunctionToStringSelf()`
|
||||
- 专门处理 `toString.toString`
|
||||
- `repairFunctionMeta(fn, expectedName, expectedLength)`
|
||||
- 修正 `name`、`length`
|
||||
|
||||
### 4.5 这里的关键判断
|
||||
|
||||
如果 future skill 没有统一的 native 保护层,那么后面补:
|
||||
|
||||
- `navigator.getBattery`
|
||||
- `document.cookie` 的 getter/setter
|
||||
- `location.reload`
|
||||
- `Storage.prototype.getItem`
|
||||
- `WebSocket`
|
||||
- `Worker`
|
||||
|
||||
这些对象时,几乎都会重复暴露。
|
||||
|
||||
也就是说,`toString` 保护层必须是底座,不是附加项。
|
||||
|
||||
## 5. 描述符模块
|
||||
|
||||
### 5.1 最有参考价值的来源
|
||||
|
||||
- `qxVm/qxVm_sanbox/env/TOOLS/vm_protection.js`
|
||||
- `node-sandbox/README.md`
|
||||
- `boda_jsEnv/bodaEnv/tools/globalThis.js`
|
||||
- `boda_jsEnv/bodaEnv/env/*`
|
||||
|
||||
### 5.2 GitHub 里出现的三条主线
|
||||
|
||||
第一条是“统一走 `Object.defineProperty` / `defineProperties`”。
|
||||
|
||||
原因很简单:
|
||||
|
||||
- 直接赋值很难精确控制 `enumerable`
|
||||
- 不方便处理只读属性
|
||||
- 不方便保持 getter/setter 形态
|
||||
|
||||
第二条是“给 prototype 和 constructor 单独做保护”。
|
||||
|
||||
`qxVm` 里专门有 `safeDescriptor_addConstructor`:
|
||||
|
||||
- 保护 `Obj.prototype`
|
||||
- 保护 `Obj.prototype.constructor`
|
||||
|
||||
第三条是“getter/setter/function 的描述符保护不能分散写”。
|
||||
|
||||
`qxVm.safe_Objattribute` 的价值就在这里:
|
||||
|
||||
- 输入一组 getter 名
|
||||
- 输入一组 setter 名
|
||||
- 输入一组函数名
|
||||
- 统一做 `safefunction`
|
||||
|
||||
### 5.3 未来 skill 必须具备的能力
|
||||
|
||||
- 读取目标浏览器对象的描述符快照
|
||||
- 输出“实例描述符层”和“原型描述符层”
|
||||
- 区分:
|
||||
- 数据属性
|
||||
- 访问器属性
|
||||
- 方法属性
|
||||
- 自动补:
|
||||
- `configurable`
|
||||
- `enumerable`
|
||||
- `writable`
|
||||
- getter / setter 引用
|
||||
- `constructor`
|
||||
- `Symbol.toStringTag`
|
||||
|
||||
### 5.4 不能忽略的细节
|
||||
|
||||
- `Function.prototype.toString` 本身的 `name`、`length`、`prototype` 描述符要处理
|
||||
- 访问器函数本身也要 native 化
|
||||
- 某些对象在 `window` 上是不可枚举的,不能简单 `window.Foo = Foo`
|
||||
- 仅把值补对而描述符错掉,仍然会被 `getOwnPropertyDescriptor`、`ownKeys`、`propertyIsEnumerable` 打穿
|
||||
|
||||
## 6. `navigator` 模块
|
||||
|
||||
### 6.1 最有参考价值的来源
|
||||
|
||||
- `node-sandbox/util/init_env.js`
|
||||
- `node-sandbox/util/cover_function.js`
|
||||
- `qxVm/qxVm_sanbox/env/BOM/Navigator.js`
|
||||
- `sdenv/browser/chrome/navigator.js`
|
||||
- `boda_jsEnv/bodaEnv/env/Navigator.js`
|
||||
- `boda_jsEnv/bodaEnv/config/changeDom.js`
|
||||
|
||||
### 6.2 这一模块的真实难点
|
||||
|
||||
不是把 `userAgent` 填上就结束,而是要同时处理:
|
||||
|
||||
- `webdriver`
|
||||
- `userAgent`
|
||||
- `platform`
|
||||
- `vendor`
|
||||
- `languages`
|
||||
- `plugins`
|
||||
- `mimeTypes`
|
||||
- `userAgentData`
|
||||
- `connection`
|
||||
- `webkitPersistentStorage`
|
||||
- `webkitTemporaryStorage`
|
||||
- `getBattery`
|
||||
|
||||
### 6.3 哪套代码最值得学什么
|
||||
|
||||
`node-sandbox` 最值得学“真实对象图”:
|
||||
|
||||
- 不是简单数组,而是完整接回 `PluginArray.prototype`
|
||||
- 每个 `Plugin` 下再挂 `MimeType`
|
||||
- 每个 `MimeType` 再反向指回 `enabledPlugin`
|
||||
|
||||
这对过 `plugins/mimeTypes` 检测非常重要。
|
||||
|
||||
`qxVm` 最值得学“配置映射”:
|
||||
|
||||
- 用 `$attribute_map` 收口环境值
|
||||
- getter 内统一做 `Illegal invocation` 检查
|
||||
- 适合 future skill 产出模块化代码
|
||||
|
||||
`sdenv` 最值得学“getter native 化”:
|
||||
|
||||
- 给 `userAgent`、`platform`、`appVersion`、`vendor`、`connection` 等 getter 做 native 保护
|
||||
|
||||
`boda_jsEnv` 最值得学“浏览器侧采集”:
|
||||
|
||||
- `changeDom.js` 直接从真实浏览器导出 `navigator`、`screen`、`cookie`、`storage`
|
||||
- 适合作为 future skill 的采集前置步骤
|
||||
|
||||
### 6.4 未来 skill 的设计建议
|
||||
|
||||
把 `navigator` 拆成五个子模块:
|
||||
|
||||
- `navigator-basic`
|
||||
- `userAgent`、`appVersion`、`platform`、`vendor`、`language`
|
||||
- `navigator-automation`
|
||||
- `webdriver`
|
||||
- `permissions`
|
||||
- 可能的 headless 标志
|
||||
- `navigator-plugin-graph`
|
||||
- `plugins`
|
||||
- `mimeTypes`
|
||||
- 双向引用关系
|
||||
- `navigator-device`
|
||||
- `hardwareConcurrency`
|
||||
- `deviceMemory`
|
||||
- `maxTouchPoints`
|
||||
- `pdfViewerEnabled`
|
||||
- `navigator-async-capability`
|
||||
- `getBattery`
|
||||
- `storage`
|
||||
- `connection`
|
||||
- `userAgentData`
|
||||
|
||||
### 6.5 对你当前目标最重要的结论
|
||||
|
||||
未来 skill 不能把 `navigator` 当成一个单文件补完任务,而要把它当成“最复杂的检测入口”。
|
||||
|
||||
优先级应当是:
|
||||
|
||||
1. `webdriver`
|
||||
2. `plugins/mimeTypes`
|
||||
3. `userAgent/platform/vendor/languages`
|
||||
4. `getBattery`
|
||||
5. `userAgentData`
|
||||
6. `connection`
|
||||
|
||||
## 7. `document` 模块
|
||||
|
||||
### 7.1 最有参考价值的来源
|
||||
|
||||
- `sdenv/browser/chrome/document.js`
|
||||
- `sdenv/test/documentAll.test.js`
|
||||
- `node-sandbox/util/init_env.js`
|
||||
- `qxVm/qxVm_sanbox/env/DOM/Document.js`
|
||||
- `qxVm/qxVm_sanbox/env/DOM/document_.js`
|
||||
- `boda_jsEnv/bodaEnv/env/Document.js`
|
||||
|
||||
### 7.2 这个模块里最关键的是 `document.all`
|
||||
|
||||
这是补环境里必须单独对待的对象。
|
||||
|
||||
从现有仓库看,有两条路线:
|
||||
|
||||
第一条是 `sdenv` 的 native addon 路线:
|
||||
|
||||
- 直接通过 `getDocumentAll()` 生成对象
|
||||
- 测试里明确覆盖:
|
||||
- `da == undefined` 为 `true`
|
||||
- `da === undefined` 为 `false`
|
||||
- `typeof da` 为 `"undefined"`
|
||||
- `da()` 返回 `null`
|
||||
|
||||
第二条是 `node-sandbox` 的底层对象路线:
|
||||
|
||||
- `document.all = new wanfeng.xtd`
|
||||
- 再把其原型接到 `HTMLAllCollection.prototype`
|
||||
- README 里明确说明重新引入底层 `setUndetectable`,就是为了 `document.all`
|
||||
|
||||
### 7.3 这里应当得出的结论
|
||||
|
||||
纯 JS 路线可以模拟很多对象,但 `document.all` 这种“不可检测对象”很难完全靠普通脚本等价复刻。
|
||||
|
||||
因此 future skill 应该把 `document.all` 标为:
|
||||
|
||||
- `Tier-1: 可完全脚本化`
|
||||
- 普通 `document` 属性
|
||||
- `cookie`
|
||||
- 常规 getter/setter
|
||||
- `Tier-2: 需底层支持或降级策略`
|
||||
- `document.all`
|
||||
- undetectable object
|
||||
- 特殊 `typeof`
|
||||
|
||||
### 7.4 `document` 模块本身怎么拆
|
||||
|
||||
建议未来 skill 至少拆成:
|
||||
|
||||
- `document-basic`
|
||||
- `URL`
|
||||
- `referrer`
|
||||
- `documentURI`
|
||||
- `readyState`
|
||||
- `visibilityState`
|
||||
- `document-cookie`
|
||||
- `cookie` getter/setter
|
||||
- 同步 `jsonCookie`
|
||||
- `document-query`
|
||||
- `getElementById`
|
||||
- `querySelector`
|
||||
- `createElement`
|
||||
- `createExpression`
|
||||
- `document-special`
|
||||
- `all`
|
||||
- `scripts`
|
||||
- `plugins`
|
||||
- 其他站点敏感接口
|
||||
|
||||
### 7.5 skill 应该吸收的流程点
|
||||
|
||||
- 先从 Proxy 吐环境里看哪个 `document.xxx` 是 `undefined`
|
||||
- 再确认它是真缺对象、缺值、缺 getter,还是缺原型
|
||||
- 如果是 `document.all`,应立刻标为特殊分支,而不是继续普通补值
|
||||
|
||||
## 8. `Storage` 模块
|
||||
|
||||
### 8.1 最有参考价值的来源
|
||||
|
||||
- `qxVm/qxVm_sanbox/env/BOM/Storage.js`
|
||||
- `catvm/CatVm2/browser/Storage.js`
|
||||
- `node-sandbox/util/init_env.js`
|
||||
- `boda_jsEnv/bodaEnv/env/Storage.js`
|
||||
- `boda_jsEnv/bodaEnv/config/configFormChrome.js`
|
||||
|
||||
### 8.2 这一模块看似简单,实际有三个层面
|
||||
|
||||
第一层是对象链:
|
||||
|
||||
- `localStorage.__proto__ === Storage.prototype`
|
||||
- `sessionStorage.__proto__ === Storage.prototype`
|
||||
|
||||
第二层是行为:
|
||||
|
||||
- `length`
|
||||
- `key`
|
||||
- `getItem`
|
||||
- `setItem`
|
||||
- `removeItem`
|
||||
- `clear`
|
||||
|
||||
第三层是状态:
|
||||
|
||||
- 初始 key/value
|
||||
- 业务 cookie 同步
|
||||
- 站点侧 token / sign / deviceId 缓存
|
||||
|
||||
### 8.3 各仓库的价值
|
||||
|
||||
`qxVm` 很适合做通用模板:
|
||||
|
||||
- `length` 直接按 `Object.keys(this).length`
|
||||
- 方法都集中在 `Storage.prototype`
|
||||
|
||||
`catvm` 的优点是直观:
|
||||
|
||||
- 用一个内存对象做状态存储
|
||||
- 适合做最小可用版本
|
||||
|
||||
`node-sandbox` 负责把实例链接对:
|
||||
|
||||
- `localStorage` / `sessionStorage` 接回 `Storage.prototype`
|
||||
|
||||
`boda_jsEnv` 最适合处理“预置状态”:
|
||||
|
||||
- 浏览器侧读取真实 `localStorage`
|
||||
- 导出到配置
|
||||
- Node 侧初始化时灌回去
|
||||
|
||||
### 8.4 未来 skill 的设计建议
|
||||
|
||||
把 `Storage` 拆成两个层面:
|
||||
|
||||
- `storage-shape`
|
||||
- 原型链
|
||||
- 方法
|
||||
- 描述符
|
||||
- native 保护
|
||||
- `storage-state`
|
||||
- 真实 key/value
|
||||
- cookie 联动
|
||||
- 站点初始化快照
|
||||
|
||||
这样 skill 才能区分:
|
||||
|
||||
- “补接口”
|
||||
- “补业务状态”
|
||||
|
||||
## 9. 指纹模块
|
||||
|
||||
### 9.1 最有参考价值的来源
|
||||
|
||||
- `node-sandbox/util/cover_function.js`
|
||||
- `node-sandbox/util/globalMy.js`
|
||||
- `qxVm/qxVm_sanbox/env/DOM/WebGLRenderingContext.js`
|
||||
- `boda_jsEnv/bodaEnv/tools/toolsFunc.js`
|
||||
- `boda_jsEnv/bodaEnv/config/changeDom.js`
|
||||
|
||||
### 9.2 当前最值得学的对象
|
||||
|
||||
- `canvas`
|
||||
- `WebGL`
|
||||
- `screen`
|
||||
- `Battery`
|
||||
- `navigator.device*`
|
||||
|
||||
### 9.3 两条主流思路
|
||||
|
||||
第一条是“固定指纹返回值”。
|
||||
|
||||
`node-sandbox`:
|
||||
|
||||
- `HTMLCanvasElement.getContext('webgl')` 时返回 `WebGLRenderingContext` 风格对象
|
||||
- 把 `toDataURL` 指向预置的 WebGL 指纹 base64
|
||||
|
||||
第二条是“指纹种子池 + 随机取样”。
|
||||
|
||||
`boda_jsEnv.toolsFunc.initEnvFingerPrint()`:
|
||||
|
||||
- 维护 `WebGLRenderingContext_getParameter_37446` 候选池
|
||||
- 维护 `BatteryManager_level`
|
||||
- 维护 `canvas` / `webgl` base64
|
||||
- 维护一组 `screen/window` 尺寸组合
|
||||
|
||||
这条路线更适合 skill,因为它天然支持配置化。
|
||||
|
||||
### 9.4 `qxVm` 的价值
|
||||
|
||||
`qxVm` 不一定提供最真实的完整指纹,但它提供了“对象模块化承载方式”:
|
||||
|
||||
- `WebGLRenderingContext` 单独成文件
|
||||
- `getParameter(37445/37446)` 这类关键点可以单独覆写
|
||||
|
||||
这很适合 future skill 生成“只修一个检测点”的补丁。
|
||||
|
||||
### 9.5 未来 skill 应该怎么拆
|
||||
|
||||
- `fingerprint-canvas`
|
||||
- `toDataURL`
|
||||
- `getImageData`
|
||||
- 2D 绘图结果
|
||||
- `fingerprint-webgl`
|
||||
- `getParameter`
|
||||
- `getExtension`
|
||||
- `getSupportedExtensions`
|
||||
- `getShaderPrecisionFormat`
|
||||
- `fingerprint-screen`
|
||||
- `screen.width/height`
|
||||
- `availWidth/availHeight`
|
||||
- `window.inner/outer*`
|
||||
- `fingerprint-battery`
|
||||
- `getBattery`
|
||||
- Battery 对象值
|
||||
|
||||
### 9.6 关键原则
|
||||
|
||||
不要把指纹模块做成“纯随机”。
|
||||
|
||||
更合理的是:
|
||||
|
||||
- 同一会话内稳定
|
||||
- 各字段彼此一致
|
||||
- 与 `navigator.platform`、`screen`、`UA` 尽量匹配
|
||||
|
||||
否则反而会制造新的指纹冲突。
|
||||
|
||||
## 10. Proxy 吐环境模块
|
||||
|
||||
### 10.1 最有参考价值的来源
|
||||
|
||||
- `qxVm/qxVm_sanbox/env/TOOLS/vm_proxy.js`
|
||||
- `catvm/CatVm2/tools/vm_proxy.js`
|
||||
- `sdenv/browser/chrome/document.js`
|
||||
- `v_jstools` 的浏览器侧注入思路
|
||||
|
||||
### 10.2 为什么这是补环境流程的入口
|
||||
|
||||
你前面提到的一点非常关键:
|
||||
|
||||
- 一般是根据 Proxy 吐出来的值是否为 `undefined` 来判断是否需要补环境
|
||||
|
||||
这和现有框架经验是完全一致的。
|
||||
|
||||
补环境真正高效的流程,不是先列全对象,而是:
|
||||
|
||||
1. 让页面跑起来
|
||||
2. 用 Proxy / Hook 吐访问路径
|
||||
3. 定位 `undefined`、异常、非法调用、原型不匹配
|
||||
4. 只补最短缺口
|
||||
|
||||
### 10.3 各仓库的区别
|
||||
|
||||
`catvm` 是最小版:
|
||||
|
||||
- 只做 `get` / `set`
|
||||
- 适合教学、理解“吐环境”的本质
|
||||
|
||||
`qxVm` 是工程版:
|
||||
|
||||
- `get`
|
||||
- `set`
|
||||
- `has`
|
||||
- `deleteProperty`
|
||||
- `getOwnPropertyDescriptor`
|
||||
- `defineProperty`
|
||||
- `getPrototypeOf`
|
||||
- `setPrototypeOf`
|
||||
- `preventExtensions`
|
||||
- `isExtensible`
|
||||
- `ownKeys`
|
||||
- 并且会继续代理返回的对象和函数
|
||||
|
||||
这才是真正适合 future skill 沉淀的路线。
|
||||
|
||||
### 10.4 future skill 应该输出的日志类型
|
||||
|
||||
建议统一分成四类:
|
||||
|
||||
- `missing`
|
||||
- 访问得到 `undefined`
|
||||
- `shape`
|
||||
- 原型链、`instanceof`、`ownKeys`、描述符异常
|
||||
- `invoke`
|
||||
- `Illegal invocation`
|
||||
- `TypeError`
|
||||
- 参数签名异常
|
||||
- `fingerprint`
|
||||
- `canvas`
|
||||
- `WebGL`
|
||||
- `battery`
|
||||
- `plugins`
|
||||
|
||||
### 10.5 未来 skill 里最值得加的能力
|
||||
|
||||
- 路径聚合
|
||||
- 把高频访问路径折叠统计
|
||||
- 自动分层建议
|
||||
- 判断这是缺值、缺 getter、缺原型、缺描述符、缺特殊对象
|
||||
- 补环境优先级排序
|
||||
- 先补能让主流程继续执行的缺口
|
||||
- 回归验证
|
||||
- 同一路径第二次访问是否仍为 `undefined`
|
||||
|
||||
## 11. 横切能力:必须专门预留的特殊模块
|
||||
|
||||
这些不属于单一对象模块,但 future skill 必须预留接口。
|
||||
|
||||
### 11.1 Node 环境检测
|
||||
|
||||
至少要考虑:
|
||||
|
||||
- `process`
|
||||
- `global`
|
||||
- `Buffer`
|
||||
- `require`
|
||||
- `module`
|
||||
- Node 特有堆栈痕迹
|
||||
|
||||
从当前仓库经验看,真实做法不是只删几个全局,而是:
|
||||
|
||||
- 调整执行容器
|
||||
- 清洗错误堆栈
|
||||
- 避免把 Node 痕迹透给页面逻辑
|
||||
|
||||
`node-sandbox` 的 `Utils.Error_get_stack` 很值得吸收,它不是只改一行字符串,而是按规则删除运行时栈帧。
|
||||
|
||||
### 11.2 `document.all`
|
||||
|
||||
这是特殊对象,不建议在 future skill 里伪装成“普通 JS 模块可完全搞定”。
|
||||
|
||||
更合理的设计是:
|
||||
|
||||
- 明确标记为特殊能力
|
||||
- 标注需要:
|
||||
- native addon
|
||||
- V8 API
|
||||
- 或降级兼容策略
|
||||
|
||||
### 11.3 异常与堆栈检测
|
||||
|
||||
未来 skill 应至少支持:
|
||||
|
||||
- 栈帧过滤
|
||||
- 文件名伪装
|
||||
- `evalmachine.<anonymous>` 替换
|
||||
- 去掉内部包装函数痕迹
|
||||
|
||||
### 11.4 `cookie` / `localStorage` / `sessionStorage` 业务态
|
||||
|
||||
很多页面不是只检查对象形状,而是直接读业务值。
|
||||
|
||||
因此 future skill 不能只生成:
|
||||
|
||||
- `Storage.prototype.getItem`
|
||||
|
||||
还要支持:
|
||||
|
||||
- 从浏览器导出真实状态
|
||||
- 灌入 Node 环境
|
||||
- 保持 cookie 与 storage 的站点态一致
|
||||
|
||||
## 12. 更接近 skill 的补环境流程
|
||||
|
||||
结合这些仓库,比较合理的流程不是“补全浏览器”,而是“按缺口驱动”:
|
||||
|
||||
### 阶段 1:采集真实环境
|
||||
|
||||
- 从真实浏览器导出:
|
||||
- `navigator`
|
||||
- `screen`
|
||||
- `location`
|
||||
- `document`
|
||||
- `localStorage`
|
||||
- `sessionStorage`
|
||||
- `cookie`
|
||||
- 指纹样本
|
||||
|
||||
推荐吸收:
|
||||
|
||||
- `boda_jsEnv/config/changeDom.js`
|
||||
- `v_jstools` 的页面侧注入思想
|
||||
|
||||
### 阶段 2:建立对象骨架
|
||||
|
||||
- 先建构造函数
|
||||
- 再建 prototype
|
||||
- 再建实例
|
||||
- 再挂全局
|
||||
- 再补 `Symbol.toStringTag`
|
||||
- 再补 constructor / prototype 描述符
|
||||
|
||||
推荐吸收:
|
||||
|
||||
- `qxVm`
|
||||
- `catvm`
|
||||
- `node-sandbox`
|
||||
|
||||
### 阶段 3:统一 native 与描述符保护
|
||||
|
||||
- 统一保护函数
|
||||
- 统一保护 getter/setter
|
||||
- 统一修正 `name` / `length`
|
||||
- 统一处理 `toString.toString`
|
||||
|
||||
推荐吸收:
|
||||
|
||||
- `qxVm/vm_protection.js`
|
||||
- `catvm/vm_safefunction.js`
|
||||
|
||||
### 阶段 4:接入 Proxy 吐环境
|
||||
|
||||
- 从主对象开始代理:
|
||||
- `window`
|
||||
- `document`
|
||||
- `navigator`
|
||||
- `location`
|
||||
- `localStorage`
|
||||
- `sessionStorage`
|
||||
- 记录:
|
||||
- `undefined`
|
||||
- `Illegal invocation`
|
||||
- `descriptor mismatch`
|
||||
- `prototype mismatch`
|
||||
|
||||
推荐吸收:
|
||||
|
||||
- `qxVm/vm_proxy.js`
|
||||
|
||||
### 阶段 5:按缺口补丁
|
||||
|
||||
补丁优先级建议:
|
||||
|
||||
1. 缺对象导致主流程中断
|
||||
2. getter / setter 非法调用
|
||||
3. 描述符异常
|
||||
4. 指纹异常
|
||||
5. 特殊对象,如 `document.all`
|
||||
|
||||
### 阶段 6:回归验证
|
||||
|
||||
- 同一条调用链重跑
|
||||
- 看是否从:
|
||||
- `undefined`
|
||||
- `TypeError`
|
||||
- `Illegal invocation`
|
||||
- 栈暴露
|
||||
|
||||
变成可继续执行
|
||||
|
||||
## 13. 未来 skill 最值得优先实现的代码块
|
||||
|
||||
如果后续正式写 skill,最应该先沉淀的不是“某站专用补环境脚本”,而是下面这些通用块:
|
||||
|
||||
1. `prototype-builder`
|
||||
- 构造函数、原型、实例、全局挂载
|
||||
2. `native-protector`
|
||||
- `Function.prototype.toString`
|
||||
- getter / setter / 方法 native 化
|
||||
- `toString.toString`
|
||||
3. `descriptor-guard`
|
||||
- `constructor`
|
||||
- `prototype`
|
||||
- `Symbol.toStringTag`
|
||||
- 访问器/方法描述符
|
||||
4. `navigator-plugin-graph`
|
||||
- `plugins`
|
||||
- `mimeTypes`
|
||||
- `enabledPlugin`
|
||||
5. `document-special`
|
||||
- `cookie`
|
||||
- `document.all`
|
||||
6. `storage-state-loader`
|
||||
- cookie / localStorage / sessionStorage 导入
|
||||
7. `fingerprint-seed`
|
||||
- `canvas`
|
||||
- `WebGL`
|
||||
- `battery`
|
||||
- `screen`
|
||||
8. `proxy-observer`
|
||||
- 访问路径聚合
|
||||
- 缺口诊断
|
||||
- 回归验证
|
||||
|
||||
## 14. 实现路线建议
|
||||
|
||||
如果只从当前 GitHub 仓库里提炼可落地的 skill 设计路线,那么未来补环境 skill 的最佳组合不是单选,而是拼装:
|
||||
|
||||
- 原型链和底层特殊对象:以 `node-sandbox` 为最高参考
|
||||
- native 保护、描述符保护、Proxy 工程化:以 `qxVm` 为最高参考
|
||||
- 教学化流程表达:以 `catvm` 为最高参考
|
||||
- 指纹配置池与浏览器侧采集:以 `boda_jsEnv` 为最高参考
|
||||
- `document.all` addon / jsdom 定点增强:以 `sdenv` 为最高参考
|
||||
- 浏览器侧注入与自动采集工具链:以 `v_jstools` 为补充参考
|
||||
|
||||
换句话说,这一节存在于 skill 中的目的,是帮助后续实现时快速决定“该参考谁来写哪一层能力”,避免:
|
||||
|
||||
- 把 skill 写成一堆站点专用补丁
|
||||
- 把底层能力和采集能力混在一起
|
||||
- 在 `document.all`、native 保护、Proxy、指纹模块上反复走弯路
|
||||
|
||||
更直接的执行建议是:
|
||||
|
||||
- 用 `catvm` 的流程说明来教怎么做
|
||||
- 用 `qxVm` 的工程结构来组织代码
|
||||
- 用 `node-sandbox` 的底层经验补最难的检测点
|
||||
- 用 `boda_jsEnv` 的采集和指纹配置来喂数据
|
||||
- 用 `sdenv` 处理 `document.all` 这类特殊对象
|
||||
|
||||
它不会直接改变补环境运行结果,但会直接影响:
|
||||
|
||||
- 模块拆分方式
|
||||
- 参考代码的优先级
|
||||
- 后续补环境能力是否能长期复用
|
||||
|
||||
这才更接近“可复用补环境 skill”,而不是“又一份站点脚本”。
|
||||
@@ -0,0 +1,110 @@
|
||||
# 特殊检测点与高强度模块
|
||||
|
||||
优先用于这些场景:
|
||||
|
||||
- `document.all`
|
||||
- native `toString`
|
||||
- `toString.toString`
|
||||
- 栈与异常检测
|
||||
- `window.crypto.getRandomValues`
|
||||
- `performance.timing`
|
||||
- `RTCPeerConnection`
|
||||
- `AudioContext` / `OfflineAudioContext`
|
||||
- `Worker` / `SharedWorker`
|
||||
- `Math` 精度差异
|
||||
|
||||
## 1. `document.all`
|
||||
|
||||
把它视为特殊对象,不要当普通属性补。
|
||||
|
||||
优先分成三条路线:
|
||||
|
||||
- `native-addon`
|
||||
- `v8-api`
|
||||
- `fallback`
|
||||
|
||||
验证至少包括:
|
||||
|
||||
- `== undefined`
|
||||
- `=== undefined`
|
||||
- `typeof`
|
||||
- 调用行为
|
||||
|
||||
## 2. native `toString`
|
||||
|
||||
如果站点在这些点报错或暴露:
|
||||
|
||||
- `fn.toString()`
|
||||
- `fn.toString.toString()`
|
||||
- getter / setter `toString`
|
||||
|
||||
优先使用 [protect-native.js](../scripts/protect-native.js)。
|
||||
|
||||
## 3. 栈与异常检测
|
||||
|
||||
重点关注:
|
||||
|
||||
- `evalmachine.<anonymous>`
|
||||
- Node 内部栈帧
|
||||
- 包装函数名泄露
|
||||
- 文件名不真实
|
||||
|
||||
如果只是“看起来像能跑”,但栈还露 Node 痕迹,不算补完。
|
||||
|
||||
## 4. `crypto-module`
|
||||
|
||||
至少考虑:
|
||||
|
||||
- `window.crypto`
|
||||
- `window.msCrypto`
|
||||
- `getRandomValues`
|
||||
- typed array 类型校验
|
||||
- 长度超限异常
|
||||
|
||||
## 5. `performance-module`
|
||||
|
||||
至少考虑:
|
||||
|
||||
- `performance.now`
|
||||
- `performance.timeOrigin`
|
||||
- `performance.timing`
|
||||
- 字段时间顺序一致性
|
||||
|
||||
## 6. `webrtc-module`
|
||||
|
||||
至少考虑:
|
||||
|
||||
- `RTCPeerConnection`
|
||||
- `RTCRtpSender`
|
||||
- `RTCRtpReceiver`
|
||||
- `createDataChannel`
|
||||
- `createOffer`
|
||||
- `onicecandidate`
|
||||
|
||||
## 7. `audio-fingerprint-module`
|
||||
|
||||
至少考虑:
|
||||
|
||||
- `AudioContext`
|
||||
- `OfflineAudioContext`
|
||||
- `AudioBuffer`
|
||||
- `getChannelData`
|
||||
|
||||
## 8. `worker-module`
|
||||
|
||||
至少考虑:
|
||||
|
||||
- `Worker`
|
||||
- `SharedWorker`
|
||||
- `MessagePort`
|
||||
- `postMessage`
|
||||
- `onmessage`
|
||||
|
||||
## 9. `math-precision-module`
|
||||
|
||||
如果用户遇到 wasm、验证码或国外风控,额外警惕:
|
||||
|
||||
- 精度差异
|
||||
- 浏览器 / Node 数值结果不一致
|
||||
|
||||
这类问题不是简单补对象能解决的。
|
||||
@@ -0,0 +1,308 @@
|
||||
# 当前补环境草稿的遗漏点
|
||||
|
||||
这份文档不是重新讲补环境,而是专门检查当前 `H:\CodeSpace\AiReverse\Web\env\codex` 这套草稿资源还缺什么。
|
||||
|
||||
结论先说:
|
||||
|
||||
- 现在已经有“原型链 / native / 描述符 / navigator / document / storage / 指纹 / Proxy”的主骨架
|
||||
- 但如果目标是未来真的做成可复用 skill,仍然缺一批高频检测点、配套脚本和验证资源
|
||||
- 这些遗漏里,最关键的不是再补几个字段,而是补“跨对象关系、异步对象、网络对象、时序与指纹一致性”
|
||||
|
||||
## 1. 当前草稿已经覆盖到的重点
|
||||
|
||||
现有文档和脚本已经比较明确地覆盖了:
|
||||
|
||||
- 原型链分层
|
||||
- native `toString`
|
||||
- getter / setter / 描述符保护
|
||||
- `navigator`
|
||||
- `document`
|
||||
- `localStorage` / `sessionStorage`
|
||||
- `canvas` / `WebGL`
|
||||
- Proxy 吐环境
|
||||
- `document.all`
|
||||
- 堆栈清洗的设计入口
|
||||
|
||||
这部分作为 future skill 的第一层基础已经够用了。
|
||||
|
||||
## 2. 目前最重要的遗漏点
|
||||
|
||||
### 2.1 `window` 图关系还不完整
|
||||
|
||||
当前草稿里虽然提到了 `window/globalThis/self/top`,但还没有把下面这些关系单独模块化:
|
||||
|
||||
- `window.window === window`
|
||||
- `self === window`
|
||||
- `top / parent / frames`
|
||||
- `frameElement`
|
||||
- `opener`
|
||||
- `document.defaultView`
|
||||
|
||||
这类点一旦补错,经常不是直接报错,而是通过一致性检测暴露。
|
||||
|
||||
### 2.2 事件系统还是空白区
|
||||
|
||||
现在还缺:
|
||||
|
||||
- `EventTarget`
|
||||
- `Event`
|
||||
- `UIEvent`
|
||||
- `MouseEvent`
|
||||
- `KeyboardEvent`
|
||||
- `PointerEvent`
|
||||
- `TouchEvent`
|
||||
- `CustomEvent`
|
||||
- `MessageEvent`
|
||||
|
||||
更重要的是缺这些行为细节:
|
||||
|
||||
- `isTrusted`
|
||||
- `timeStamp`
|
||||
- `target/currentTarget/srcElement`
|
||||
- `addEventListener/removeEventListener/dispatchEvent`
|
||||
|
||||
很多风控和验证码环境,不是直接查 `navigator`,而是顺着事件系统走。
|
||||
|
||||
### 2.3 网络与通信对象几乎还没进草稿
|
||||
|
||||
当前最明显的遗漏之一就是:
|
||||
|
||||
- `XMLHttpRequest`
|
||||
- `fetch`
|
||||
- `Headers`
|
||||
- `Request`
|
||||
- `Response`
|
||||
- `sendBeacon`
|
||||
- `WebSocket`
|
||||
- `EventSource`
|
||||
|
||||
你前面已经明确提过 `WebSocket`,但目前这套草稿里还没有把它真正升格成模块。
|
||||
|
||||
这会影响:
|
||||
|
||||
- 某些 cookie / token 写入链
|
||||
- 某些 socket 协议站点
|
||||
- 某些通过 `fetch/xhr` 挂钩的风控
|
||||
|
||||
### 2.4 Worker 只被提到了,但没形成模块
|
||||
|
||||
现在还缺完整设计:
|
||||
|
||||
- `Worker`
|
||||
- `SharedWorker`
|
||||
- `ServiceWorker`
|
||||
- `postMessage`
|
||||
- `MessageChannel`
|
||||
- `BroadcastChannel`
|
||||
- `structuredClone`
|
||||
|
||||
尤其是国外盾和新型验证码,越来越喜欢把检测逻辑拆进 Worker。
|
||||
|
||||
## 3. 指纹层面的明显遗漏
|
||||
|
||||
### 3.1 音频指纹还没单列
|
||||
|
||||
当前草稿重点放在 `canvas/WebGL`,但缺:
|
||||
|
||||
- `AudioContext`
|
||||
- `OfflineAudioContext`
|
||||
- `AudioBuffer`
|
||||
- 常见音频指纹链路
|
||||
|
||||
这点在新型验证码和国外风控里非常常见。
|
||||
|
||||
### 3.2 字体指纹还没单列
|
||||
|
||||
当前文档里虽然提到“字体”,但还没有形成独立模块。
|
||||
|
||||
应该至少考虑:
|
||||
|
||||
- 浏览器侧字体枚举导出
|
||||
- 文本测量差异
|
||||
- `canvas` 字体渲染一致性
|
||||
|
||||
### 3.3 WebRTC 还没进入模块清单
|
||||
|
||||
目前还缺:
|
||||
|
||||
- `RTCPeerConnection`
|
||||
- `RTCRtpSender`
|
||||
- `RTCRtpReceiver`
|
||||
- 相关候选、能力和属性检测
|
||||
|
||||
这类点不一定每个站都用,但一旦碰到就是高优先级。
|
||||
|
||||
### 3.4 `Math` / 引擎精度差异没有单独建模
|
||||
|
||||
现在草稿里还没有明确:
|
||||
|
||||
- 浏览器与 Node 的 `Math` 精度差异
|
||||
- 不同引擎浮点实现差异
|
||||
|
||||
这点在高级风控和 wasm/验证码里会被拿来做环境分辨。
|
||||
|
||||
## 4. 浏览器能力对象的遗漏
|
||||
|
||||
下面这批对象在当前草稿里还比较弱或者没展开:
|
||||
|
||||
- `permissions.query`
|
||||
- `Notification.permission`
|
||||
- `mediaDevices`
|
||||
- `enumerateDevices`
|
||||
- `getUserMedia`
|
||||
- `clipboard`
|
||||
- `geolocation`
|
||||
- `Bluetooth`
|
||||
- `USB`
|
||||
- `HID`
|
||||
- `serial`
|
||||
- `xr`
|
||||
|
||||
原因不是它们每次都必须补,而是:
|
||||
|
||||
- 很多站点会把它们当“是否为真浏览器”的探针
|
||||
- 很多国外站点喜欢同时读多个能力对象做一致性判断
|
||||
|
||||
## 5. 时间、地区、编码对象的遗漏
|
||||
|
||||
这一块目前也不够:
|
||||
|
||||
- `Intl`
|
||||
- 时区
|
||||
- `Date`
|
||||
- `performance`
|
||||
- `performance.now`
|
||||
- `performance.timeOrigin`
|
||||
- `navigation timing`
|
||||
- `TextEncoder`
|
||||
- `TextDecoder`
|
||||
- `atob`
|
||||
- `btoa`
|
||||
- `URL`
|
||||
- `URLSearchParams`
|
||||
- `crypto.getRandomValues`
|
||||
- `crypto.subtle`
|
||||
|
||||
如果 future skill 以后要更稳,这些不能长期缺位。
|
||||
|
||||
## 6. DOM 结构与样式系统的遗漏
|
||||
|
||||
当前 `document` 方向有骨架,但还缺完整 DOM 系统设计:
|
||||
|
||||
- `Node`
|
||||
- `Element`
|
||||
- `HTMLElement`
|
||||
- `HTMLDivElement`
|
||||
- `HTMLCanvasElement`
|
||||
- `DOMParser`
|
||||
- `XMLSerializer`
|
||||
- `MutationObserver`
|
||||
- `CSSStyleDeclaration`
|
||||
- `classList`
|
||||
- `dataset`
|
||||
- `style`
|
||||
- `getAttribute/setAttribute`
|
||||
|
||||
很多补环境失败并不是 BOM 对象缺,而是 DOM 返回值长得不像真浏览器。
|
||||
|
||||
## 7. 现有脚本本身的遗漏
|
||||
|
||||
### 7.1 `collect-browser-env.js` 采集面还不够
|
||||
|
||||
现在它能采集:
|
||||
|
||||
- `navigator`
|
||||
- `document`
|
||||
- `screen`
|
||||
- `history`
|
||||
- `location`
|
||||
- `storage`
|
||||
- 基础 `canvas/webgl`
|
||||
|
||||
但还没采:
|
||||
|
||||
- `plugins`
|
||||
- `mimeTypes`
|
||||
- `permissions`
|
||||
- `Notification.permission`
|
||||
- `mediaDevices`
|
||||
- `battery`
|
||||
- `AudioContext`
|
||||
- 字体样本
|
||||
- `performance`
|
||||
- `Intl/timezone`
|
||||
- `document.all` 特征
|
||||
|
||||
### 7.2 `observe-runtime.js` 还偏轻量
|
||||
|
||||
现在能做基础 Proxy 观察,但还不够:
|
||||
|
||||
- 缺路径频次统计
|
||||
- 缺按对象分类的聚合结果
|
||||
- 缺“读取返回 undefined”与“调用抛错”联动分析
|
||||
- 缺输出到 JSON 的稳定格式
|
||||
- 缺针对 `apply/construct` 的细化报告
|
||||
|
||||
### 7.3 `protect-native.js` 还只是底座雏形
|
||||
|
||||
现在能做:
|
||||
|
||||
- `Function.prototype.toString`
|
||||
- getter / setter 基础保护
|
||||
- `name` / `length` 修正
|
||||
|
||||
但还没做全:
|
||||
|
||||
- `prototype` 描述符修正
|
||||
- `Symbol.toStringTag` 联动
|
||||
- 针对 Proxy 包裹后函数的处理
|
||||
- 多层包装后的 native 串一致性
|
||||
|
||||
## 8. blueprint 里提到但尚未补齐的资源
|
||||
|
||||
当前 blueprint 里已经提到,但实际文件还没有落地的有:
|
||||
|
||||
### 8.1 `references`
|
||||
|
||||
- `03-special-cases.md`
|
||||
- `07-source-map.md`
|
||||
- `06-validation.md`
|
||||
|
||||
### 8.2 `scripts`
|
||||
|
||||
- `build-plugin-graph.js`
|
||||
- `merge-storage-state.js`
|
||||
- `scaffold-module.js`
|
||||
|
||||
### 8.3 `assets`
|
||||
|
||||
- `navigator-sample.example.json`
|
||||
- `plugin-graph.example.json`
|
||||
- `storage-state.example.json`
|
||||
|
||||
这说明现在的草稿已经到了“该从设计走向半成品”的阶段。
|
||||
|
||||
## 9. 最该优先补的遗漏顺序
|
||||
|
||||
如果按未来 skill 的实用性排序,我建议优先级如下:
|
||||
|
||||
1. `window/self/top/parent/defaultView/opener` 一致性
|
||||
2. `WebSocket` / `fetch` / `XMLHttpRequest`
|
||||
3. `Worker` / `SharedWorker` / `postMessage`
|
||||
4. `permissions` / `mediaDevices` / `Notification`
|
||||
5. `AudioContext` / 字体指纹 / `WebRTC`
|
||||
6. `Intl` / `performance` / `crypto`
|
||||
7. 完整 DOM / Event 系统
|
||||
|
||||
## 10. 当前最稳的判断
|
||||
|
||||
现在这套 `codex` 目录已经不再是“零散笔记”,而是一套成型中的 skill 设计草稿。
|
||||
|
||||
但如果要迈过“能跑 Demo”到“能抗检测”的门槛,后续最应该补的不是更多 `navigator.xxx = ...`,而是:
|
||||
|
||||
- 跨对象关系
|
||||
- 异步与通信对象
|
||||
- 网络对象
|
||||
- 时间与地区一致性
|
||||
- 音频 / 字体 / WebRTC 等更深层指纹
|
||||
- 验证与回归资源
|
||||
@@ -0,0 +1,174 @@
|
||||
# 去中心化补环境阅读池
|
||||
|
||||
这份清单专门用来纠正一个问题:
|
||||
|
||||
当前补环境资料很容易被 CSDN / 51CTO / 博客园 / 镜像站的重复转载“制造出很多文章”的错觉,但其实很多内容是同一套思路在多平台反复出现。
|
||||
|
||||
所以这份清单的目标不是“再堆大站镜像”,而是尽量扩展到:
|
||||
|
||||
- 个人博客
|
||||
- 小站
|
||||
- 案例站
|
||||
- 安全社区转载但内容更偏实战的文章
|
||||
|
||||
## 1. 当前阅读池的结构性问题
|
||||
|
||||
现有资料里,确实有一个偏差:
|
||||
|
||||
- 大平台多
|
||||
- 镜像多
|
||||
- Proxy 入门多
|
||||
- 真实复杂检测点少
|
||||
|
||||
最常见的重复内容是:
|
||||
|
||||
- `window/document/navigator` 代理
|
||||
- 简单 `Proxy get/set`
|
||||
- `document.cookie` hook
|
||||
- `window = global`
|
||||
|
||||
这些当然有用,但如果只读这一层,会误以为补环境主要就是“补对象 + 打印日志”。
|
||||
|
||||
实际上更难的部分是:
|
||||
|
||||
- 原型链与描述符
|
||||
- native `toString`
|
||||
- `document.all`
|
||||
- 栈清洗
|
||||
- `plugins/mimeTypes`
|
||||
- `Worker`
|
||||
- `WebSocket`
|
||||
- `AudioContext`
|
||||
- `WebRTC`
|
||||
- 字体 / 精度 / 多模块一致性
|
||||
|
||||
## 2. 更值得扩展阅读的来源
|
||||
|
||||
下面这些来源不一定都是“大站爆文”,但对 future skill 的参考价值更高。
|
||||
|
||||
### 2.1 个人博客 / 小博客
|
||||
|
||||
1. Chen's Blog: web逆向-补环境笔记
|
||||
链接: [chenxs.site](https://chenxs.site/2024/12/13/web%E9%80%86%E5%90%91-%E8%A1%A5%E7%8E%AF%E5%A2%83%E7%AC%94%E8%AE%B0/)
|
||||
价值: 把 `Proxy`、`toString`、`jsdom`、history 补环境和 v_jstools 串成了一条个人实践链,比较接近“自己做笔记并反复试验”的材料。
|
||||
|
||||
2. 博客园: JavaScript逆向之代码补环境(以 iwencai 为例)
|
||||
链接: [cnblogs.com/sbhglqy](https://www.cnblogs.com/sbhglqy/p/18085001)
|
||||
价值: 对原型链、`Object.setPrototypeOf`、history 补环境、`global = window` 这类实战细节写得比较直。
|
||||
|
||||
3. 博客园: js逆向之补环境常用代码
|
||||
链接: [cnblogs.com/caomengqi](https://www.cnblogs.com/caomengqi/p/15370101.html)
|
||||
价值: 虽然偏入门,但把“先吐环境,再按层补对象”的朴素思路讲得很清楚,还点到了 `XMLHttpRequest` 和音频指纹。
|
||||
|
||||
4. 博客园: js逆向7-常见的混淆手段和补环境
|
||||
链接: [cnblogs.com/andy0816](https://www.cnblogs.com/andy0816/p/15087573.html)
|
||||
价值: 虽然不是纯补环境教程,但强调了“hook 原生方法后还要过 `toString` 检测”。
|
||||
|
||||
5. Geek Blog: JS逆向完整技能图谱
|
||||
链接: [51rexue.cn](https://51rexue.cn/blog/50)
|
||||
价值: 更像个人整理版知识图谱,能帮助 future skill 做导航,而不是只做片段代码。
|
||||
|
||||
### 2.2 案例型文章
|
||||
|
||||
6. 博客园: 多个开源的 js 补环境框架测试
|
||||
链接: [cnblogs.com/kanadeblisst](https://www.cnblogs.com/kanadeblisst/p/18177803)
|
||||
价值: 不是讲单个技巧,而是把多个框架放到同一个案例里比较。
|
||||
|
||||
7. 博客园: 某音 `__ac_signature` 补环境案例
|
||||
链接: [cnblogs.com/kanadeblisst](https://www.cnblogs.com/kanadeblisst/p/18183750)
|
||||
价值: 明确展示了 cookie 生成链如何依赖补环境,适合作为“站点驱动补环境”的参考。
|
||||
|
||||
8. EW帮帮网: ali231 补环境案例
|
||||
链接: [ewbang.com](https://www.ewbang.com/community/article/details/1000130780.html)
|
||||
价值: 更偏真实站点流程,适合看“怎么从参数入口一路走到补环境”。
|
||||
|
||||
9. CSDN: 今日头条 `_signature` 补环境
|
||||
链接: [blog.csdn.net/m0_52336378/article/details/133042514](https://blog.csdn.net/m0_52336378/article/details/133042514)
|
||||
价值: 案例导向,强调补环境只是三条路之一,和插桩、JS-RPC 并列。
|
||||
|
||||
10. CSDN: XHS 私信协议 / X-s 补环境
|
||||
链接: [blog.csdn.net/2401_85458050/article/details/139437038](https://blog.csdn.net/2401_85458050/article/details/139437038)
|
||||
价值: 直接碰到了 `AudioContext`、`CanvasRenderingContext2D`、socket 协议和更复杂的运行环境。
|
||||
|
||||
### 2.3 高强度检测点文章
|
||||
|
||||
11. CN-SEC: hCaptcha 无感逆向分析-补环境
|
||||
链接: [cn-sec.com](https://cn-sec.com/archives/4909845.html)
|
||||
价值: 这是当前最值得继续深读的来源之一,因为它已经不满足于基础 BOM/DOM,而是点到了 `RTCPeerConnection`、`OfflineAudioContext`、`WebGL2RenderingContext`、字体指纹、`Worker/SharedWorker`、`Math` 精度和大量描述符检测。
|
||||
|
||||
12. 信息安全知识库: hCaptcha 无感逆向分析-补环境
|
||||
链接: [gm7.org](https://www.gm7.org/archives/27931)
|
||||
价值: 内容与上文接近,但整理摘要里把 `Worker`、`Math` 精度、WebRTC、WASM 交互明确提出来了,适合作为快速索引。
|
||||
|
||||
## 3. 这些来源给 future skill 带来的新提醒
|
||||
|
||||
如果把上面这些小博客 / 小站 / 案例文一起看,能很明显看到几个变化:
|
||||
|
||||
- 基础 BOM/DOM 只是第一层
|
||||
- 真正难的是异步对象、通信对象和更深层指纹
|
||||
- `Proxy` 只是入口,不是结尾
|
||||
- “值对了”远远不够,还要“形状对、关系对、时序对”
|
||||
|
||||
## 4. 后续阅读策略应该怎么改
|
||||
|
||||
后面继续扩资料时,不应再用“收集 30 个链接”这种粗放方式来判断进展,而应该改成:
|
||||
|
||||
### 4.1 按“新信息量”筛文章
|
||||
|
||||
优先保留能新增这些维度的文章:
|
||||
|
||||
- 新对象
|
||||
- 新检测链
|
||||
- 新指纹
|
||||
- 新补环境流程
|
||||
- 新脚本模板
|
||||
|
||||
### 4.2 把镜像站当备份,不当新增样本
|
||||
|
||||
例如同一篇 Proxy 入门文,如果在:
|
||||
|
||||
- CSDN
|
||||
- 博客园
|
||||
- 51CTO
|
||||
- 优快云
|
||||
|
||||
都出现了,那应该视为 1 份思路,不是 4 份新增知识。
|
||||
|
||||
### 4.3 优先阅读“案例里补出了什么”
|
||||
|
||||
比起“补环境是什么”的泛讲,更应该优先看:
|
||||
|
||||
- 补了哪些对象
|
||||
- 为什么补
|
||||
- 哪一步报错
|
||||
- 最后如何验证
|
||||
|
||||
这类文章对 future skill 的模块设计更有价值。
|
||||
|
||||
## 5. 现阶段最值得重点深读的非大站来源
|
||||
|
||||
如果只选 4 个最值得现在继续深读的来源,我建议是:
|
||||
|
||||
1. [Chen's Blog - web逆向-补环境笔记](https://chenxs.site/2024/12/13/web%E9%80%86%E5%90%91-%E8%A1%A5%E7%8E%AF%E5%A2%83%E7%AC%94%E8%AE%B0/)
|
||||
2. [博客园 - JavaScript逆向之代码补环境(以 iwencai 为例)](https://www.cnblogs.com/sbhglqy/p/18085001)
|
||||
3. [EW帮帮网 - ali231 补环境案例](https://www.ewbang.com/community/article/details/1000130780.html)
|
||||
4. [CN-SEC - hCaptcha无感逆向分析-补环境](https://cn-sec.com/archives/4909845.html)
|
||||
|
||||
原因分别是:
|
||||
|
||||
- Chen's Blog: 像个人实战笔记,适合吸收流程和日常补法
|
||||
- iwencai: 适合吸收原型链和 `global/window` 关系
|
||||
- ali231: 适合吸收真实站点导向流程
|
||||
- hCaptcha: 适合吸收更深层检测点
|
||||
|
||||
## 6. 当前最稳的调整方向
|
||||
|
||||
后续继续收资料时,建议把重心从“大站高热度文章”切到:
|
||||
|
||||
- 个人博客
|
||||
- 博客园实战贴
|
||||
- 小型安全社区案例
|
||||
- 含代码片段的镜像站
|
||||
- 国外风控 / 验证码案例分析
|
||||
|
||||
换句话说,后面的重点不再是“找更多同类补环境文章”,而是“找更多不同层级、不同复杂度、不同检测面的补环境文章”。
|
||||
@@ -0,0 +1,52 @@
|
||||
# 验证与回归
|
||||
|
||||
补环境完成后,不要只看“脚本不报错”,至少做下面几类验证。
|
||||
|
||||
## 1. 结构验证
|
||||
|
||||
- `instanceof`
|
||||
- `Object.getPrototypeOf`
|
||||
- `constructor`
|
||||
- `Symbol.toStringTag`
|
||||
- `getOwnPropertyDescriptor`
|
||||
- `ownKeys`
|
||||
|
||||
## 2. native 验证
|
||||
|
||||
- `fn.toString()`
|
||||
- `fn.toString.toString()`
|
||||
- getter / setter `toString`
|
||||
- `name`
|
||||
- `length`
|
||||
|
||||
## 3. 运行验证
|
||||
|
||||
- 同一路径第二次访问是否仍为 `undefined`
|
||||
- 是否还抛 `Illegal invocation`
|
||||
- 是否还抛 `TypeError`
|
||||
- 是否还暴露 Node 栈
|
||||
|
||||
## 4. 指纹验证
|
||||
|
||||
- `canvas`
|
||||
- `WebGL`
|
||||
- `battery`
|
||||
- `screen/window` 一致性
|
||||
- `AudioContext`
|
||||
- `WebRTC`
|
||||
|
||||
## 5. 替代路线判断
|
||||
|
||||
如果补到以下阶段仍然成本过高,考虑切换路线:
|
||||
|
||||
- `document.all`
|
||||
- `Worker`
|
||||
- `WebRTC`
|
||||
- `AudioContext`
|
||||
- `Math` 精度
|
||||
|
||||
可切换为:
|
||||
|
||||
- 真浏览器内执行
|
||||
- `JsRpc`
|
||||
- `WebSocket` 回传
|
||||
@@ -0,0 +1,34 @@
|
||||
# 来源映射
|
||||
|
||||
这份文档说明当前 skill 主要吸收了哪些公开来源。
|
||||
|
||||
## 1. GitHub 框架来源
|
||||
|
||||
- `node-sandbox`
|
||||
- 特殊对象、栈、复杂 `plugins/mimeTypes`、`getBattery`、`canvas/WebGL`
|
||||
- `qxVm`
|
||||
- 工程结构、描述符、native 保护、Proxy 诊断
|
||||
- `catvm`
|
||||
- 教学式拆解、原型链步骤
|
||||
- `boda_jsEnv`
|
||||
- 浏览器采集、指纹种子、配置化环境
|
||||
- `sdenv`
|
||||
- `document.all` addon 路线、jsdom 定点增强
|
||||
|
||||
## 2. 阅读池策略
|
||||
|
||||
为了继续扩充补环境知识库,优先吸收:
|
||||
|
||||
- 个人博客 / 小站
|
||||
- 博客园实战贴
|
||||
- 小型安全社区案例
|
||||
- 验证码与风控分析文
|
||||
|
||||
使用这些来源时,重点提取:
|
||||
|
||||
- 补环境流程
|
||||
- Proxy 吐环境思路
|
||||
- 原型链 / 描述符 / native 保护写法
|
||||
- `navigator` / `document` / `Storage` / 指纹对象代码
|
||||
|
||||
不要把镜像转载重复算作新增知识,也不要把“只讲概念、不落代码”的文章当成高优先级来源。
|
||||
@@ -0,0 +1,271 @@
|
||||
# 第二轮补环境阅读笔记
|
||||
|
||||
这份文档记录本轮继续扩读后,新补进来的信息点。
|
||||
|
||||
这一轮的目标不是重复收集 Proxy 入门文,而是专门补当前草稿缺的方向:
|
||||
|
||||
- `window` / 原型链关系
|
||||
- `window.crypto` / `performance`
|
||||
- `WebRTC`
|
||||
- `AudioContext` / `OfflineAudioContext`
|
||||
- `Worker`
|
||||
- 更复杂的国外风控 / 验证码环境检测
|
||||
|
||||
## 1. 本轮阅读后最明显的变化
|
||||
|
||||
如果把这批文章和前面的 GitHub 框架一起看,会发现一个很重要的分层:
|
||||
|
||||
### 第一层:基础补环境
|
||||
|
||||
- `window = globalThis`
|
||||
- `document = {}`
|
||||
- `navigator = {}`
|
||||
- `location = {}`
|
||||
- `history = {}`
|
||||
- `screen = {}`
|
||||
- `Proxy` 吐环境
|
||||
|
||||
这类内容很多,足够入门,但对 future skill 的增量已经有限。
|
||||
|
||||
### 第二层:工程化补环境
|
||||
|
||||
- 原型链
|
||||
- `Symbol.toStringTag`
|
||||
- native `toString`
|
||||
- getter / setter / 描述符
|
||||
- `plugins/mimeTypes`
|
||||
- `document.all`
|
||||
|
||||
这类内容是现有 `codex` 草稿的核心骨架。
|
||||
|
||||
### 第三层:高强度检测
|
||||
|
||||
- `window.crypto.getRandomValues`
|
||||
- `performance.timing`
|
||||
- `RTCPeerConnection`
|
||||
- `RTCRtpSender` / `RTCRtpReceiver`
|
||||
- `OfflineAudioContext`
|
||||
- `Worker` / `SharedWorker`
|
||||
- 字体指纹
|
||||
- `Math` 精度差异
|
||||
- wasm + Promise 异步链
|
||||
|
||||
这一层才是后续最值得重点扩读和沉淀的方向。
|
||||
|
||||
## 2. 本轮最有价值的文章与新增信息
|
||||
|
||||
### 2.1 博客园:JS逆向之浏览器补环境详解
|
||||
|
||||
链接:
|
||||
[https://www.cnblogs.com/spiderman6/p/16969391.html](https://www.cnblogs.com/spiderman6/p/16969391.html)
|
||||
|
||||
这篇文章的最大价值不是某个单独代码片段,而是把补环境明确拆成两条路线:
|
||||
|
||||
- 递归 Proxy 监控路线
|
||||
- 纯 JS 原型链框架路线
|
||||
|
||||
对 future skill 最有价值的新增点:
|
||||
|
||||
- 明确提出“工程化项目结构”比单脚本更重要
|
||||
- 明确提到事件处理是框架必须考虑的点
|
||||
- 强调每个 BOM / DOM 对象应单文件组织
|
||||
- 强调补环境框架要按原型链顺序拼接
|
||||
|
||||
这对我们前面已经写出的 `future-skill-blueprint` 是很强的佐证。
|
||||
|
||||
### 2.2 博客园:js补环境代码
|
||||
|
||||
链接:
|
||||
[https://www.cnblogs.com/c-x-a/p/18702240](https://www.cnblogs.com/c-x-a/p/18702240)
|
||||
|
||||
这篇文章虽然代码不复杂,但有一个很实用的点:
|
||||
|
||||
- 它把 `performance`、`top`、`self`、`document` 一起纳入了 Proxy 监控清单
|
||||
|
||||
它提醒我们:
|
||||
|
||||
- `performance` 不应该只放在指纹模块里,也应该纳入 Proxy 观察默认目标
|
||||
- `top/self` 这类 `window` 关系也应该是默认监控项
|
||||
|
||||
### 2.3 博客园:window.crypto.getRandomValues 模拟
|
||||
|
||||
链接:
|
||||
[https://www.cnblogs.com/hotdog418/articles/14005518.html](https://www.cnblogs.com/hotdog418/articles/14005518.html)
|
||||
|
||||
这篇文章的价值在于给了一个很具体的 `getRandomValues` 模拟思路,并且点到了真实行为细节:
|
||||
|
||||
- 需要处理 `Uint8Array`
|
||||
- 需要处理 `Uint16Array`
|
||||
- 需要处理 `Uint32Array`
|
||||
- 需要处理长度超过阈值时的异常
|
||||
|
||||
这对 future skill 的意义是:
|
||||
|
||||
- `crypto.getRandomValues` 不能只返回随机数
|
||||
- 还要尽量模拟参数类型和错误行为
|
||||
|
||||
也就是说,`crypto-module` 已经值得单独立项。
|
||||
|
||||
### 2.4 验证码细节总结:补环境中可能用到的方法
|
||||
|
||||
链接:
|
||||
[https://www.cnblogs.com/ocr12/p/18664716](https://www.cnblogs.com/ocr12/p/18664716)
|
||||
|
||||
这篇文章虽然主题更偏验证码,但里面有两个对补环境很重要的点:
|
||||
|
||||
- `window.crypto.getRandomValues`
|
||||
- `window.performance.timing`
|
||||
|
||||
尤其是 `performance.timing`,说明我们前面的遗漏判断是对的:
|
||||
|
||||
- performance 不能只补 `performance.now`
|
||||
- 很多场景会直接读 timing 结构
|
||||
|
||||
所以 future skill 至少应单独补:
|
||||
|
||||
- `performance.now`
|
||||
- `performance.timeOrigin`
|
||||
- `performance.timing`
|
||||
- 时间序列一致性
|
||||
|
||||
### 2.5 博客园:修改浏览器指纹之 webrtc 指纹修改
|
||||
|
||||
链接:
|
||||
[https://www.cnblogs.com/68xi/p/13344520.html](https://www.cnblogs.com/68xi/p/13344520.html)
|
||||
|
||||
这篇文章不是逆向框架文,但对 `WebRTC` 的价值很高:
|
||||
|
||||
- 明确给出了 `RTCPeerConnection` 探测本机 IP 的典型流程
|
||||
- 涉及 `createDataChannel`
|
||||
- 涉及 `createOffer`
|
||||
- 涉及 `onicecandidate`
|
||||
|
||||
这说明 future skill 中的 `webrtc-module` 至少要考虑:
|
||||
|
||||
- 构造函数存在性
|
||||
- `createDataChannel`
|
||||
- `createOffer`
|
||||
- ICE candidate 行为
|
||||
- IP 泄漏与候选收集路径
|
||||
|
||||
### 2.6 CN-SEC:hCaptcha 无感逆向分析-补环境
|
||||
|
||||
链接:
|
||||
[https://cn-sec.com/archives/4909845.html](https://cn-sec.com/archives/4909845.html)
|
||||
|
||||
这是这一轮最关键的文章。
|
||||
|
||||
它几乎直接把我们草稿里“高强度检测层”的缺口点出来了:
|
||||
|
||||
- `RTCPeerConnection`
|
||||
- `RTCRtpSender`
|
||||
- `RTCRtpReceiver`
|
||||
- `OfflineAudioContext`
|
||||
- `WebGL2RenderingContext`
|
||||
- 描述符检测
|
||||
- `AudioBuffer.prototype.getChannelData`
|
||||
- `Math` 精度差异
|
||||
- 字体指纹
|
||||
- canvas 指纹
|
||||
- `Worker`
|
||||
- `SharedWorker`
|
||||
- Promise / wasm 异步调用链
|
||||
|
||||
这篇文章对 future skill 的最大推动有三点:
|
||||
|
||||
1. 说明补环境已经不能只看 BOM / DOM
|
||||
2. 说明“异步链 + wasm 交互 + 指纹一致性”是高阶核心
|
||||
3. 说明验证模块必须能和浏览器日志对照
|
||||
|
||||
## 3. 本轮阅读后,future skill 应新增的模块
|
||||
|
||||
结合这一轮资料,最应该补进 blueprint 的新增模块有:
|
||||
|
||||
### 3.1 `crypto-module`
|
||||
|
||||
至少包含:
|
||||
|
||||
- `window.crypto`
|
||||
- `window.msCrypto`
|
||||
- `getRandomValues`
|
||||
- `subtle`
|
||||
|
||||
### 3.2 `performance-module`
|
||||
|
||||
至少包含:
|
||||
|
||||
- `performance.now`
|
||||
- `performance.timeOrigin`
|
||||
- `performance.timing`
|
||||
- 时间一致性
|
||||
|
||||
### 3.3 `webrtc-module`
|
||||
|
||||
至少包含:
|
||||
|
||||
- `RTCPeerConnection`
|
||||
- `RTCRtpSender`
|
||||
- `RTCRtpReceiver`
|
||||
- ICE candidate 行为
|
||||
|
||||
### 3.4 `audio-fingerprint-module`
|
||||
|
||||
至少包含:
|
||||
|
||||
- `AudioContext`
|
||||
- `OfflineAudioContext`
|
||||
- `AudioBuffer`
|
||||
- `getChannelData`
|
||||
|
||||
### 3.5 `worker-module`
|
||||
|
||||
至少包含:
|
||||
|
||||
- `Worker`
|
||||
- `SharedWorker`
|
||||
- `postMessage`
|
||||
- `MessageEvent`
|
||||
|
||||
### 3.6 `math-precision-module`
|
||||
|
||||
至少包含:
|
||||
|
||||
- 精度差异检查
|
||||
- 高风险数学函数差异记录
|
||||
- 浏览器 / Node 结果对照能力
|
||||
|
||||
## 4. 对当前 codex 草稿最直接的修正建议
|
||||
|
||||
这一轮阅读后,我认为前面的遗漏优先级需要进一步收敛成:
|
||||
|
||||
1. `crypto-module`
|
||||
2. `performance-module`
|
||||
3. `webrtc-module`
|
||||
4. `audio-fingerprint-module`
|
||||
5. `worker-module`
|
||||
6. `window` 关系一致性
|
||||
7. 完整 Event / DOM 系统
|
||||
|
||||
原因很简单:
|
||||
|
||||
- 这些模块在高强度风控里更容易真正卡死流程
|
||||
- 而且它们比基础 BOM/DOM 更难靠“临时补值”解决
|
||||
|
||||
## 5. 当前最稳的结论
|
||||
|
||||
这第二轮阅读已经说明:
|
||||
|
||||
- 我们现在不是资料不够多
|
||||
- 而是资料分布不均
|
||||
|
||||
真正该继续扩读的,不是更多重复的 Proxy 入门文,而是:
|
||||
|
||||
- `crypto`
|
||||
- `performance`
|
||||
- `WebRTC`
|
||||
- `AudioContext`
|
||||
- `Worker`
|
||||
- `Math` 精度
|
||||
- wasm 交互与异步调用链
|
||||
|
||||
这会直接决定 future skill 后续有没有能力从“常规补环境”走向“国外高强度盾补环境”。
|
||||
@@ -0,0 +1,156 @@
|
||||
# 第三轮补环境阅读笔记
|
||||
|
||||
这轮继续扩读后,新增信息已经开始从“模块提醒”进入“具体实现与策略分流”。
|
||||
|
||||
本轮重点补到的不是基础 Proxy,而是:
|
||||
|
||||
- `crypto.getRandomValues`
|
||||
- `performance`
|
||||
- `WebRTC`
|
||||
- 代理反检测
|
||||
- `JsRpc/WebSocket` 作为补环境替代路线
|
||||
|
||||
## 1. 本轮最重要的新增结论
|
||||
|
||||
### 1.1 `crypto-module` 已经可以正式立项
|
||||
|
||||
这一轮多篇文章都重复出现了同一个点:
|
||||
|
||||
- `window.crypto.getRandomValues`
|
||||
|
||||
而且不是只说“需要补”,而是给到了很具体的行为细节:
|
||||
|
||||
- 支持 `Uint8Array`
|
||||
- 支持 `Uint16Array`
|
||||
- 支持 `Uint32Array`
|
||||
- 长度超过 `65536` 时要抛 `QuotaExceededError`
|
||||
|
||||
这说明它已经不是“边角料补丁”,而是可以成为 future skill 的独立模块。
|
||||
|
||||
参考来源:
|
||||
|
||||
- [JS 安全随机数 window.crypto 如何逆向实现](https://www.cnblogs.com/hotdog418/articles/14005518.html)
|
||||
- [某验全家桶细节避坑总结](https://www.cnblogs.com/ikdl/p/17382407.html)
|
||||
- [验证码识别与处理细节总结](https://www.cnblogs.com/ocr12/p/18664716)
|
||||
|
||||
## 2. `performance` 的重要性被再次确认
|
||||
|
||||
这一轮阅读后,`performance` 至少有两层价值:
|
||||
|
||||
- 作为默认 Proxy 监控对象
|
||||
- 作为独立补环境对象
|
||||
|
||||
现在可以明确:
|
||||
|
||||
- `performance.now` 不够
|
||||
- `performance.timing` 也经常被直接读
|
||||
- 时间字段之间的一致性关系也需要考虑
|
||||
|
||||
其中一个很直接的提醒来自:
|
||||
|
||||
- [js补环境代码](https://www.cnblogs.com/c-x-a/p/18702240)
|
||||
|
||||
这篇文章的监控列表示例里已经把 `performance` 和 `top/self/document` 一起当成默认观察对象。
|
||||
|
||||
## 3. `WebRTC` 已经不只是“指纹提醒”,而是行为链
|
||||
|
||||
这一轮最大的变化,是 `WebRTC` 不再只是被当成一个抽象指纹点,而是能看到实际调用链:
|
||||
|
||||
- `RTCPeerConnection`
|
||||
- `createDataChannel`
|
||||
- `createOffer`
|
||||
- `onicecandidate`
|
||||
|
||||
这意味着 future skill 的 `webrtc-module` 不能只补“对象存在”,还得至少考虑:
|
||||
|
||||
- 构造函数存在性
|
||||
- 方法存在性
|
||||
- Promise 或事件链行为
|
||||
- 本地 candidate / IP 暴露路径
|
||||
|
||||
参考来源:
|
||||
|
||||
- [修改浏览器指纹之 webrtc 指纹修改](https://www.cnblogs.com/68xi/p/13344520.html)
|
||||
- [hCaptcha无感逆向分析-补环境](https://cn-sec.com/archives/4909845.html)
|
||||
|
||||
## 4. 代理反检测值得单独做策略说明
|
||||
|
||||
本轮一个很值得注意的新点,是有文章明确讲了“代理本身会被检测”。
|
||||
|
||||
这会带来两个分支:
|
||||
|
||||
1. 把代理去掉,靠更完整的环境直跑
|
||||
2. 使用更完善的代理,不暴露明显破绽
|
||||
|
||||
这和我们之前从 `qxVm` 学到的结论是完全一致的:
|
||||
|
||||
- 入门代理很适合吐环境
|
||||
- 但生产代理必须考虑 `getPrototypeOf`、`setPrototypeOf`、`ownKeys`、`isExtensible` 等更细 trap
|
||||
|
||||
参考来源:
|
||||
|
||||
- [携某 testab 参数补环境详解](https://www.cnblogs.com/ikdl/p/18380671)
|
||||
|
||||
## 5. `JsRpc/WebSocket` 应该被标记为“替代路线”
|
||||
|
||||
这一轮虽然不是直接补环境文章,但有两篇非常值得保留,因为它们点出了另一条现实路径:
|
||||
|
||||
- 不一定非要在 Node 里补到极致
|
||||
- 也可以让浏览器真实执行,再通过 `WebSocket` / RPC 回传结果
|
||||
|
||||
这条路线对 future skill 的意义不是替代补环境,而是:
|
||||
|
||||
- 当补环境成本过高时,skill 应知道还有 `JsRpc` 这种 fallback
|
||||
- 对某些站点,`WebSocket + 浏览器内执行` 可能比硬补 `Worker/WebRTC/AudioContext` 更划算
|
||||
|
||||
参考来源:
|
||||
|
||||
- [Python网络爬虫之js逆向之远程调用(rpc)免去抠代码补环境简介](https://www.cnblogs.com/dcpeng/p/16003260.html)
|
||||
- [js逆向之jsRpc](https://www.cnblogs.com/xingxia/p/18388396/js_rpc)
|
||||
|
||||
## 6. `Worker` 方向的新增认识
|
||||
|
||||
这一轮虽然没找到太多“直接讲补环境 Worker”的逆向文,但通过通用 Worker / SharedWorker 文章,已经足够补出一层接口认识:
|
||||
|
||||
- `Worker` 和 `SharedWorker` 的构造与通信方式不同
|
||||
- `SharedWorker` 通过 `port` 通信
|
||||
- `onconnect`、`port.start()`、`postMessage()` 这些行为都可能被检测
|
||||
|
||||
这意味着 future skill 的 `worker-module` 最起码要先区分:
|
||||
|
||||
- `Worker`
|
||||
- `SharedWorker`
|
||||
- `MessagePort`
|
||||
- `postMessage/onmessage`
|
||||
|
||||
参考来源:
|
||||
|
||||
- [WebWorker SharedWorker ServiceWorker](https://www.cnblogs.com/cart55free99/p/4627094.html)
|
||||
- [sharedWorker 实现多页面通信](https://www.cnblogs.com/imgss/p/14634577.html)
|
||||
- [SharedWorker使用总结](https://www.cnblogs.com/zoexu/p/16790215.html)
|
||||
|
||||
## 7. 这轮阅读对 future skill 最直接的推进
|
||||
|
||||
现在可以把新增模块的重要性排序进一步收紧为:
|
||||
|
||||
1. `crypto-module`
|
||||
2. `performance-module`
|
||||
3. `webrtc-module`
|
||||
4. `worker-module`
|
||||
5. `audio-fingerprint-module`
|
||||
6. 代理反检测策略
|
||||
7. `JsRpc/WebSocket` 替代路线
|
||||
|
||||
## 8. 当前最稳的判断
|
||||
|
||||
这第三轮阅读说明,future skill 后续已经不该只是“补对象清单”了,而应该同时具备三种视角:
|
||||
|
||||
- 正面补环境
|
||||
- 反检测补环境
|
||||
- 无法高性价比补时的替代路线
|
||||
|
||||
也就是说,补环境 skill 最后不应只是一个对象模板库,而应更像:
|
||||
|
||||
- 模块库
|
||||
- 检测诊断器
|
||||
- 路线选择器
|
||||
@@ -0,0 +1,138 @@
|
||||
const MODULE_PRIORITY = [
|
||||
"prototype-builder",
|
||||
"descriptor-guard",
|
||||
"native-protector",
|
||||
"navigator-module",
|
||||
"document-module",
|
||||
"document-all-module",
|
||||
"storage-module",
|
||||
"fingerprint-module",
|
||||
"performance-module",
|
||||
"crypto-module",
|
||||
"audio-fingerprint-module",
|
||||
"webrtc-module",
|
||||
"worker-module",
|
||||
"stack-clean-module",
|
||||
];
|
||||
|
||||
function toArray(value) {
|
||||
if (Array.isArray(value)) return value;
|
||||
if (value && Array.isArray(value.logs)) return value.logs;
|
||||
return [];
|
||||
}
|
||||
|
||||
function unique(values) {
|
||||
return Array.from(new Set(values));
|
||||
}
|
||||
|
||||
function extractPathList(input, key, type) {
|
||||
if (Array.isArray(input[key])) return unique(input[key]);
|
||||
return unique(
|
||||
toArray(input)
|
||||
.filter((item) => item && item.type === type && typeof item.path === "string")
|
||||
.map((item) => item.path)
|
||||
);
|
||||
}
|
||||
|
||||
function inferModulesFromPath(path) {
|
||||
const modules = [];
|
||||
const value = String(path || "");
|
||||
|
||||
if (/__proto__|prototype|constructor/.test(value)) modules.push("prototype-builder");
|
||||
if (/getOwnPropertyDescriptor|defineProperty|descriptor|ownKeys/.test(value)) modules.push("descriptor-guard");
|
||||
if (/toString|name|length|caller|arguments/.test(value)) modules.push("native-protector");
|
||||
if (/navigator|plugins|mimeTypes|webdriver|getBattery|userAgentData/.test(value)) {
|
||||
modules.push("navigator-module");
|
||||
if (/getBattery/.test(value)) modules.push("fingerprint-module");
|
||||
}
|
||||
if (/document\.all/.test(value)) {
|
||||
modules.push("document-all-module");
|
||||
modules.push("document-module");
|
||||
} else if (/document|createElement|querySelector|cookie/.test(value)) {
|
||||
modules.push("document-module");
|
||||
}
|
||||
if (/localStorage|sessionStorage|indexedDB|cookieStore/.test(value)) modules.push("storage-module");
|
||||
if (/canvas|webgl|screen|devicePixelRatio|getContext|toDataURL/.test(value)) modules.push("fingerprint-module");
|
||||
if (/performance|Performance/.test(value)) modules.push("performance-module");
|
||||
if (/crypto|msCrypto|getRandomValues|subtle/.test(value)) modules.push("crypto-module");
|
||||
if (/AudioContext|OfflineAudioContext|Oscillator|Analyser/.test(value)) modules.push("audio-fingerprint-module");
|
||||
if (/RTCPeerConnection|RTCDataChannel|RTCIceCandidate|RTCSessionDescription/.test(value)) modules.push("webrtc-module");
|
||||
if (/Worker|SharedWorker|MessagePort|BroadcastChannel/.test(value)) modules.push("worker-module");
|
||||
if (/Error|stack|prepareStackTrace/.test(value)) modules.push("stack-clean-module");
|
||||
|
||||
return unique(modules);
|
||||
}
|
||||
|
||||
function scoreEvidence(entry) {
|
||||
if (!entry || !entry.type) return 0;
|
||||
if (entry.type === "invoke-error") return 5;
|
||||
if (entry.type === "missing") return 4;
|
||||
if (entry.type === "descriptor" || entry.type === "prototype") return 3;
|
||||
return 1;
|
||||
}
|
||||
|
||||
function analyzeGapLog(input = {}) {
|
||||
const logs = toArray(input);
|
||||
const missingPaths = extractPathList(input, "missingPaths", "missing");
|
||||
const descriptorAccess = extractPathList(input, "descriptorAccess", "descriptor");
|
||||
const prototypeAccess = extractPathList(input, "prototypeAccess", "prototype");
|
||||
const invocationErrors = extractPathList(input, "invocationErrors", "invoke-error");
|
||||
const groupedByModule = Object.create(null);
|
||||
|
||||
for (const entry of logs) {
|
||||
const modules = inferModulesFromPath(entry.path);
|
||||
if (!modules.length) continue;
|
||||
for (const moduleName of modules) {
|
||||
if (!groupedByModule[moduleName]) {
|
||||
groupedByModule[moduleName] = { score: 0, evidence: [] };
|
||||
}
|
||||
groupedByModule[moduleName].score += scoreEvidence(entry);
|
||||
groupedByModule[moduleName].evidence.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
const recommendedModules = Object.keys(groupedByModule).sort((left, right) => {
|
||||
const leftPriority = MODULE_PRIORITY.indexOf(left);
|
||||
const rightPriority = MODULE_PRIORITY.indexOf(right);
|
||||
const leftScore = groupedByModule[left].score;
|
||||
const rightScore = groupedByModule[right].score;
|
||||
if (rightScore !== leftScore) return rightScore - leftScore;
|
||||
return leftPriority - rightPriority;
|
||||
});
|
||||
|
||||
const nextActions = [];
|
||||
if (recommendedModules.includes("prototype-builder")) {
|
||||
nextActions.push("先修复构造器、原型链和 instanceof 关系,再处理对象值。");
|
||||
}
|
||||
if (recommendedModules.includes("descriptor-guard")) {
|
||||
nextActions.push("把 getOwnPropertyDescriptor/defineProperty/propertyIsEnumerable 的结果对齐。");
|
||||
}
|
||||
if (recommendedModules.includes("native-protector")) {
|
||||
nextActions.push("统一接管 Function.prototype.toString,并补齐 getter/setter/name/length 保护。");
|
||||
}
|
||||
if (recommendedModules.includes("document-all-module")) {
|
||||
nextActions.push("单独评估 document.all,明确是 native-addon、V8 API 还是 fallback 路线。");
|
||||
}
|
||||
if (recommendedModules.includes("storage-module")) {
|
||||
nextActions.push("把 cookie/localStorage/sessionStorage 的真实状态同步进运行时。");
|
||||
}
|
||||
if (recommendedModules.includes("fingerprint-module")) {
|
||||
nextActions.push("把 canvas/webgl/screen/battery 等指纹点单独建模,不要和普通 navigator 属性混补。");
|
||||
}
|
||||
|
||||
return {
|
||||
totalLogs: logs.length,
|
||||
missingPaths,
|
||||
descriptorAccess,
|
||||
prototypeAccess,
|
||||
invocationErrors,
|
||||
recommendedModules,
|
||||
groupedByModule,
|
||||
nextActions: unique(nextActions),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
analyzeGapLog,
|
||||
inferModulesFromPath,
|
||||
};
|
||||
@@ -0,0 +1,180 @@
|
||||
const { scaffoldModule } = require("./scaffold-module");
|
||||
|
||||
const DEFAULT_SUBTLE_METHODS = [
|
||||
"digest",
|
||||
"generateKey",
|
||||
"importKey",
|
||||
"exportKey",
|
||||
"sign",
|
||||
"verify",
|
||||
"encrypt",
|
||||
"decrypt",
|
||||
];
|
||||
|
||||
function toFiniteNumber(value, fallback) {
|
||||
return Number.isFinite(value) ? value : fallback;
|
||||
}
|
||||
|
||||
function normalizeCryptoSeed(seed = {}) {
|
||||
const cryptoSeed = seed.crypto && typeof seed.crypto === "object" ? seed.crypto : seed;
|
||||
return {
|
||||
exposeCrypto: cryptoSeed.exposeCrypto !== false,
|
||||
installMsCrypto: cryptoSeed.installMsCrypto !== false,
|
||||
maxGetRandomValuesLength: toFiniteNumber(cryptoSeed.maxGetRandomValuesLength, 65536),
|
||||
hasSubtle: cryptoSeed.hasSubtle !== false,
|
||||
subtleMethods: Array.isArray(cryptoSeed.subtleMethods) && cryptoSeed.subtleMethods.length
|
||||
? cryptoSeed.subtleMethods.slice()
|
||||
: DEFAULT_SUBTLE_METHODS.slice(),
|
||||
randomValues: Array.isArray(cryptoSeed.randomValues)
|
||||
? cryptoSeed.randomValues.map((item) => Number(item) & 255)
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
function renderCryptoPatch(seed) {
|
||||
const serializedSeed = JSON.stringify(seed, null, 2);
|
||||
return `(
|
||||
function installCryptoModule(globalObject) {
|
||||
const seed = ${serializedSeed};
|
||||
const protector = globalObject.__envNativeProtector || null;
|
||||
const integerArrayTags = new Set([
|
||||
"[object Int8Array]",
|
||||
"[object Uint8Array]",
|
||||
"[object Uint8ClampedArray]",
|
||||
"[object Int16Array]",
|
||||
"[object Uint16Array]",
|
||||
"[object Int32Array]",
|
||||
"[object Uint32Array]"
|
||||
]);
|
||||
const randomPool = Array.isArray(seed.randomValues) ? seed.randomValues.slice() : [];
|
||||
|
||||
function defineValue(target, key, value) {
|
||||
Object.defineProperty(target, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: value
|
||||
});
|
||||
}
|
||||
|
||||
function createQuotaError(message) {
|
||||
if (typeof DOMException === "function") {
|
||||
return new DOMException(message, "QuotaExceededError");
|
||||
}
|
||||
const error = new Error(message);
|
||||
error.name = "QuotaExceededError";
|
||||
return error;
|
||||
}
|
||||
|
||||
function assertIntegerTypedArray(view) {
|
||||
const tag = Object.prototype.toString.call(view);
|
||||
if (!integerArrayTags.has(tag)) {
|
||||
throw new TypeError("Failed to execute 'getRandomValues' on 'Crypto': parameter 1 is not an integer array view.");
|
||||
}
|
||||
if (view.byteLength > seed.maxGetRandomValuesLength) {
|
||||
throw createQuotaError("Failed to execute 'getRandomValues' on 'Crypto': The ArrayBufferView's byte length exceeds the number of bytes of entropy available via this API.");
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
function nextByte(index) {
|
||||
if (randomPool.length) {
|
||||
return randomPool[index % randomPool.length] & 255;
|
||||
}
|
||||
return (index * 73 + 19) & 255;
|
||||
}
|
||||
|
||||
function getRandomValues(view) {
|
||||
assertIntegerTypedArray(view);
|
||||
for (let index = 0; index < view.length; index += 1) {
|
||||
view[index] = nextByte(index);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
const subtle = {};
|
||||
for (const methodName of seed.subtleMethods) {
|
||||
subtle[methodName] = function subtlePlaceholder() {
|
||||
return Promise.reject(new Error("SubtleCrypto method not implemented: " + methodName));
|
||||
};
|
||||
if (protector && protector.repairFunctionMeta) {
|
||||
protector.repairFunctionMeta(subtle[methodName], { name: methodName, length: 0 });
|
||||
}
|
||||
if (protector && protector.protectFunction) {
|
||||
protector.protectFunction(subtle[methodName], methodName);
|
||||
}
|
||||
}
|
||||
|
||||
const cryptoObject = {};
|
||||
defineValue(cryptoObject, "getRandomValues", getRandomValues);
|
||||
if (seed.hasSubtle) {
|
||||
defineValue(cryptoObject, "subtle", subtle);
|
||||
}
|
||||
|
||||
if (protector && protector.repairFunctionMeta) {
|
||||
protector.repairFunctionMeta(getRandomValues, { name: "getRandomValues", length: 1 });
|
||||
}
|
||||
if (protector && protector.protectFunction) {
|
||||
protector.protectFunction(getRandomValues, "getRandomValues");
|
||||
}
|
||||
|
||||
if (seed.exposeCrypto) {
|
||||
Object.defineProperty(globalObject, "crypto", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: cryptoObject
|
||||
});
|
||||
}
|
||||
|
||||
if (seed.installMsCrypto) {
|
||||
Object.defineProperty(globalObject, "msCrypto", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: cryptoObject
|
||||
});
|
||||
}
|
||||
|
||||
return cryptoObject;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : window);
|
||||
`;
|
||||
}
|
||||
|
||||
function buildCryptoModule(seed = {}, options = {}) {
|
||||
const module = scaffoldModule("crypto-module");
|
||||
const normalizedSeed = normalizeCryptoSeed(seed);
|
||||
|
||||
module.patchPlan = [
|
||||
"挂载 window.crypto 与可选的 window.msCrypto。",
|
||||
"按种子构建 getRandomValues,并复刻整数 TypedArray 校验和长度上限。",
|
||||
"按模块契约补出 subtle 对象的基础方法占位,并接入 native 保护接口。",
|
||||
];
|
||||
|
||||
module.patchCode = renderCryptoPatch(normalizedSeed, options);
|
||||
module.runtimeState = {
|
||||
crypto: normalizedSeed,
|
||||
};
|
||||
module.validation = [
|
||||
"调用 crypto.getRandomValues(new Uint8Array(16)),确认返回原数组实例。",
|
||||
"调用 crypto.getRandomValues(new Uint16Array(8)) 与 new Uint32Array(4)。",
|
||||
"传入 Float32Array 时应抛出 TypeError。",
|
||||
"传入超出 maxGetRandomValuesLength 的视图时应抛出 QuotaExceededError。",
|
||||
"检查 crypto.getRandomValues.toString() 与 crypto.getRandomValues.name/length。",
|
||||
];
|
||||
module.residualRisk = [
|
||||
"subtle 仍为占位实现,只适合绕过存在性检测,不适合真实密码流程。",
|
||||
"如果目标站点会比对 DOMException message/code,仍需依据真实浏览器日志继续收敛。",
|
||||
];
|
||||
|
||||
if (options.includeLegacyAlias === false) {
|
||||
module.runtimeState.crypto.installMsCrypto = false;
|
||||
}
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildCryptoModule,
|
||||
normalizeCryptoSeed,
|
||||
};
|
||||
@@ -0,0 +1,332 @@
|
||||
const { scaffoldModule } = require("./scaffold-module");
|
||||
|
||||
function clone(value) {
|
||||
return JSON.parse(JSON.stringify(value || {}));
|
||||
}
|
||||
|
||||
function normalizeDocumentSeed(seed = {}) {
|
||||
const documentSeed = seed.document && typeof seed.document === "object" ? seed.document : seed;
|
||||
return {
|
||||
document: {
|
||||
URL: documentSeed.URL || "https://example.invalid/",
|
||||
referrer: documentSeed.referrer || "",
|
||||
documentURI: documentSeed.documentURI || documentSeed.URL || "https://example.invalid/",
|
||||
compatMode: documentSeed.compatMode || "CSS1Compat",
|
||||
dir: documentSeed.dir || "",
|
||||
title: documentSeed.title || "",
|
||||
designMode: documentSeed.designMode || "off",
|
||||
readyState: documentSeed.readyState || "complete",
|
||||
contentType: documentSeed.contentType || "text/html",
|
||||
inputEncoding: documentSeed.inputEncoding || "UTF-8",
|
||||
domain: documentSeed.domain || "example.invalid",
|
||||
characterSet: documentSeed.characterSet || "UTF-8",
|
||||
charset: documentSeed.charset || "UTF-8",
|
||||
hidden: documentSeed.hidden === true,
|
||||
visibilityState: documentSeed.visibilityState || "visible",
|
||||
cookie: typeof documentSeed.cookie === "string" ? documentSeed.cookie : (typeof seed.cookie === "string" ? seed.cookie : ""),
|
||||
},
|
||||
queryMap: clone(seed.queryMap || {}),
|
||||
ids: clone(seed.ids || {}),
|
||||
tags: clone(seed.tags || {}),
|
||||
specialCases: clone(seed.specialCases || {}),
|
||||
};
|
||||
}
|
||||
|
||||
function renderDocumentPatch(seed) {
|
||||
const serializedSeed = JSON.stringify(seed, null, 2);
|
||||
return `(
|
||||
function installDocumentModule(globalObject) {
|
||||
const seed = ${serializedSeed};
|
||||
const protector = globalObject.__envNativeProtector || null;
|
||||
|
||||
function defineValue(target, key, value) {
|
||||
Object.defineProperty(target, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: value
|
||||
});
|
||||
}
|
||||
|
||||
function defineGetter(target, key, getter, setter) {
|
||||
const descriptor = {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
get: getter
|
||||
};
|
||||
if (typeof setter === "function") {
|
||||
descriptor.set = setter;
|
||||
}
|
||||
Object.defineProperty(target, key, descriptor);
|
||||
if (protector && protector.protectDescriptor) {
|
||||
protector.protectDescriptor(target, key);
|
||||
}
|
||||
}
|
||||
|
||||
function applyEventTarget(target) {
|
||||
defineValue(target, "__listeners__", Object.create(null));
|
||||
defineValue(target, "addEventListener", function addEventListener(type, listener) {
|
||||
if (!this.__listeners__[type]) {
|
||||
this.__listeners__[type] = [];
|
||||
}
|
||||
this.__listeners__[type].push(listener);
|
||||
});
|
||||
defineValue(target, "removeEventListener", function removeEventListener(type, listener) {
|
||||
const list = this.__listeners__[type] || [];
|
||||
this.__listeners__[type] = list.filter(function filterItem(item) {
|
||||
return item !== listener;
|
||||
});
|
||||
});
|
||||
defineValue(target, "dispatchEvent", function dispatchEvent(event) {
|
||||
const list = (this.__listeners__[event.type] || []).slice();
|
||||
for (const listener of list) {
|
||||
listener.call(this, event);
|
||||
}
|
||||
const handler = this["on" + event.type];
|
||||
if (typeof handler === "function") {
|
||||
handler.call(this, event);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function createElementNode(tagName) {
|
||||
const normalized = String(tagName || "div").toUpperCase();
|
||||
if (normalized === "CANVAS" && typeof globalObject.__envCreateCanvasElement === "function") {
|
||||
return globalObject.__envCreateCanvasElement();
|
||||
}
|
||||
const element = {
|
||||
nodeType: 1,
|
||||
nodeName: normalized,
|
||||
tagName: normalized,
|
||||
ownerDocument: null,
|
||||
style: {},
|
||||
dataset: {},
|
||||
children: [],
|
||||
childNodes: []
|
||||
};
|
||||
applyEventTarget(element);
|
||||
defineValue(element, "appendChild", function appendChild(child) {
|
||||
this.children.push(child);
|
||||
this.childNodes.push(child);
|
||||
child.parentNode = this;
|
||||
return child;
|
||||
});
|
||||
defineValue(element, "setAttribute", function setAttribute(name, value) {
|
||||
this[name] = String(value);
|
||||
});
|
||||
defineValue(element, "getAttribute", function getAttribute(name) {
|
||||
return Object.prototype.hasOwnProperty.call(this, name) ? this[name] : null;
|
||||
});
|
||||
return element;
|
||||
}
|
||||
|
||||
function buildNodeMap(source, kind) {
|
||||
const out = Object.create(null);
|
||||
Object.keys(source || {}).forEach(function eachKey(key) {
|
||||
const value = source[key];
|
||||
if (Array.isArray(value)) {
|
||||
out[key] = value.map(function mapTag(tagName) {
|
||||
const node = createElementNode(tagName);
|
||||
node.ownerDocument = documentInstance;
|
||||
return node;
|
||||
});
|
||||
} else {
|
||||
const node = createElementNode(value || kind || "div");
|
||||
node.ownerDocument = documentInstance;
|
||||
out[key] = node;
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function Document() {}
|
||||
function HTMLDocument() {}
|
||||
HTMLDocument.prototype = Object.create(Document.prototype);
|
||||
defineValue(HTMLDocument.prototype, "constructor", HTMLDocument);
|
||||
|
||||
const state = seed.document || {};
|
||||
const documentInstance = Object.create(HTMLDocument.prototype);
|
||||
let cookieValue = state.cookie || "";
|
||||
applyEventTarget(documentInstance);
|
||||
|
||||
const queryNodes = buildNodeMap(seed.queryMap || {}, "div");
|
||||
const idNodes = buildNodeMap(seed.ids || {}, "div");
|
||||
const tagNodes = buildNodeMap(seed.tags || {}, "div");
|
||||
const htmlNode = createElementNode("html");
|
||||
const headNode = createElementNode("head");
|
||||
const bodyNode = createElementNode("body");
|
||||
htmlNode.ownerDocument = documentInstance;
|
||||
headNode.ownerDocument = documentInstance;
|
||||
bodyNode.ownerDocument = documentInstance;
|
||||
htmlNode.appendChild(headNode);
|
||||
htmlNode.appendChild(bodyNode);
|
||||
|
||||
defineGetter(Document.prototype, "URL", function getURL() {
|
||||
return state.URL;
|
||||
});
|
||||
defineGetter(Document.prototype, "referrer", function getReferrer() {
|
||||
return state.referrer;
|
||||
});
|
||||
defineGetter(Document.prototype, "documentURI", function getDocumentURI() {
|
||||
return state.documentURI;
|
||||
});
|
||||
defineGetter(Document.prototype, "compatMode", function getCompatMode() {
|
||||
return state.compatMode;
|
||||
});
|
||||
defineGetter(Document.prototype, "dir", function getDir() {
|
||||
return state.dir;
|
||||
});
|
||||
defineGetter(Document.prototype, "title", function getTitle() {
|
||||
return state.title;
|
||||
}, function setTitle(value) {
|
||||
state.title = String(value);
|
||||
});
|
||||
defineGetter(Document.prototype, "designMode", function getDesignMode() {
|
||||
return state.designMode;
|
||||
}, function setDesignMode(value) {
|
||||
state.designMode = String(value);
|
||||
});
|
||||
defineGetter(Document.prototype, "readyState", function getReadyState() {
|
||||
return state.readyState;
|
||||
});
|
||||
defineGetter(Document.prototype, "contentType", function getContentType() {
|
||||
return state.contentType;
|
||||
});
|
||||
defineGetter(Document.prototype, "inputEncoding", function getInputEncoding() {
|
||||
return state.inputEncoding;
|
||||
});
|
||||
defineGetter(Document.prototype, "domain", function getDomain() {
|
||||
return state.domain;
|
||||
});
|
||||
defineGetter(Document.prototype, "characterSet", function getCharacterSet() {
|
||||
return state.characterSet;
|
||||
});
|
||||
defineGetter(Document.prototype, "charset", function getCharset() {
|
||||
return state.charset;
|
||||
});
|
||||
defineGetter(Document.prototype, "hidden", function getHidden() {
|
||||
return !!state.hidden;
|
||||
});
|
||||
defineGetter(Document.prototype, "visibilityState", function getVisibilityState() {
|
||||
return state.visibilityState;
|
||||
});
|
||||
defineGetter(Document.prototype, "cookie", function getCookie() {
|
||||
return cookieValue;
|
||||
}, function setCookie(value) {
|
||||
cookieValue = cookieValue ? cookieValue + "; " + String(value) : String(value);
|
||||
});
|
||||
defineGetter(Document.prototype, "head", function getHead() {
|
||||
return headNode;
|
||||
});
|
||||
defineGetter(Document.prototype, "body", function getBody() {
|
||||
return bodyNode;
|
||||
});
|
||||
defineGetter(Document.prototype, "documentElement", function getDocumentElement() {
|
||||
return htmlNode;
|
||||
});
|
||||
defineGetter(Document.prototype, "defaultView", function getDefaultView() {
|
||||
return globalObject;
|
||||
});
|
||||
|
||||
defineValue(Document.prototype, "createElement", function createElement(tagName) {
|
||||
const element = createElementNode(tagName);
|
||||
element.ownerDocument = documentInstance;
|
||||
return element;
|
||||
});
|
||||
defineValue(Document.prototype, "querySelector", function querySelector(selector) {
|
||||
return queryNodes[selector] || null;
|
||||
});
|
||||
defineValue(Document.prototype, "getElementById", function getElementById(id) {
|
||||
return idNodes[id] || null;
|
||||
});
|
||||
defineValue(Document.prototype, "getElementsByTagName", function getElementsByTagName(tagName) {
|
||||
const key = String(tagName || "").toLowerCase();
|
||||
const list = tagNodes[key] || [];
|
||||
return list.slice();
|
||||
});
|
||||
|
||||
const documentAllFallback = {
|
||||
length: 0,
|
||||
item: function item() {
|
||||
return null;
|
||||
},
|
||||
namedItem: function namedItem() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
defineGetter(Document.prototype, "all", function getAll() {
|
||||
return documentAllFallback;
|
||||
});
|
||||
|
||||
if (protector && protector.repairFunctionMeta) {
|
||||
protector.repairFunctionMeta(Document, { name: "Document", length: 0 });
|
||||
protector.repairFunctionMeta(HTMLDocument, { name: "HTMLDocument", length: 0 });
|
||||
protector.repairFunctionMeta(Document.prototype.createElement, { name: "createElement", length: 1 });
|
||||
protector.repairFunctionMeta(Document.prototype.querySelector, { name: "querySelector", length: 1 });
|
||||
protector.repairFunctionMeta(Document.prototype.getElementById, { name: "getElementById", length: 1 });
|
||||
protector.repairFunctionMeta(Document.prototype.getElementsByTagName, { name: "getElementsByTagName", length: 1 });
|
||||
}
|
||||
if (protector && protector.protectFunction) {
|
||||
protector.protectFunction(Document, "Document");
|
||||
protector.protectFunction(HTMLDocument, "HTMLDocument");
|
||||
protector.protectFunction(Document.prototype.createElement, "createElement");
|
||||
protector.protectFunction(Document.prototype.querySelector, "querySelector");
|
||||
protector.protectFunction(Document.prototype.getElementById, "getElementById");
|
||||
protector.protectFunction(Document.prototype.getElementsByTagName, "getElementsByTagName");
|
||||
}
|
||||
|
||||
Object.defineProperty(globalObject, "Document", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: Document
|
||||
});
|
||||
Object.defineProperty(globalObject, "HTMLDocument", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: HTMLDocument
|
||||
});
|
||||
Object.defineProperty(globalObject, "document", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: documentInstance
|
||||
});
|
||||
|
||||
return documentInstance;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : window);
|
||||
`;
|
||||
}
|
||||
|
||||
function buildDocumentModule(seed = {}) {
|
||||
const module = scaffoldModule("document-module");
|
||||
const normalizedSeed = normalizeDocumentSeed(seed);
|
||||
|
||||
module.patchPlan = [
|
||||
"构建 Document / HTMLDocument 构造器、prototype 和 document 实例。",
|
||||
"把 document.cookie、URL、readyState 等高频字段补成访问器描述符。",
|
||||
"补出 createElement/querySelector/getElementById/getElementsByTagName,并为 canvas 入口预留到 fingerprint 模块的接线点。",
|
||||
];
|
||||
module.patchCode = renderDocumentPatch(normalizedSeed);
|
||||
module.runtimeState = normalizedSeed;
|
||||
module.validation = [
|
||||
"检查 document.URL / document.documentURI / document.readyState。",
|
||||
"检查 document.cookie getter/setter 是否正常工作。",
|
||||
"检查 document.createElement('canvas') 是否能接到 fingerprint 模块。",
|
||||
"检查 querySelector / getElementById / getElementsByTagName 的基本返回。",
|
||||
"检查 document.all 仍被标注为特殊兼容风险,而不是宣称完全等价。",
|
||||
];
|
||||
module.residualRisk = [
|
||||
"这里只覆盖静态 DOM 壳和常见查询入口,未实现完整 Node/Element/MutationObserver/CSSStyleDeclaration 系统。",
|
||||
"document.all 仅提供降级 fallback,无法在纯 JS 中完整等价原生特殊对象语义。",
|
||||
];
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildDocumentModule,
|
||||
normalizeDocumentSeed,
|
||||
};
|
||||
@@ -0,0 +1,228 @@
|
||||
const { scaffoldModule } = require("./scaffold-module");
|
||||
|
||||
function clone(value) {
|
||||
return JSON.parse(JSON.stringify(value || {}));
|
||||
}
|
||||
|
||||
function normalizeFingerprintSeed(seed = {}) {
|
||||
return {
|
||||
fingerprint: clone(seed.fingerprint || {}),
|
||||
screen: clone(seed.screen || {}),
|
||||
windowMetrics: clone(seed.windowMetrics || {}),
|
||||
battery: clone(seed.battery || {}),
|
||||
navigator: clone(seed.navigator || {}),
|
||||
};
|
||||
}
|
||||
|
||||
function renderFingerprintPatch(seed) {
|
||||
const serializedSeed = JSON.stringify(seed, null, 2);
|
||||
return `(
|
||||
function installFingerprintModule(globalObject) {
|
||||
const seed = ${serializedSeed};
|
||||
const protector = globalObject.__envNativeProtector || null;
|
||||
|
||||
function defineValue(target, key, value) {
|
||||
Object.defineProperty(target, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: value
|
||||
});
|
||||
}
|
||||
|
||||
function defineGetter(target, key, getter) {
|
||||
Object.defineProperty(target, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
get: getter
|
||||
});
|
||||
if (protector && protector.protectDescriptor) {
|
||||
protector.protectDescriptor(target, key);
|
||||
}
|
||||
}
|
||||
|
||||
function Screen() {}
|
||||
const screenState = Object.assign({
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
availWidth: 1920,
|
||||
availHeight: 1040,
|
||||
colorDepth: 24,
|
||||
pixelDepth: 24
|
||||
}, seed.screen || {});
|
||||
const screenInstance = Object.create(Screen.prototype);
|
||||
|
||||
["width", "height", "availWidth", "availHeight", "colorDepth", "pixelDepth", "availLeft", "availTop"].forEach(function eachKey(key) {
|
||||
defineGetter(Screen.prototype, key, function getScreenValue() {
|
||||
return screenState[key];
|
||||
});
|
||||
});
|
||||
|
||||
function createCanvas2DContext() {
|
||||
return {
|
||||
fillRect: function fillRect() {},
|
||||
clearRect: function clearRect() {},
|
||||
fillText: function fillText() {},
|
||||
measureText: function measureText(text) {
|
||||
return { width: String(text || "").length * 7.25 };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function WebGLRenderingContext() {}
|
||||
defineValue(WebGLRenderingContext.prototype, "getParameter", function getParameter(param) {
|
||||
if (param === 37445) {
|
||||
return seed.fingerprint.webglVendor || "Google Inc.";
|
||||
}
|
||||
if (param === 37446) {
|
||||
return seed.fingerprint.webglRenderer || "ANGLE";
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
function createWebGLContext() {
|
||||
return Object.create(WebGLRenderingContext.prototype);
|
||||
}
|
||||
|
||||
function HTMLCanvasElement() {
|
||||
this.width = 300;
|
||||
this.height = 150;
|
||||
this.__lastContextType__ = "";
|
||||
}
|
||||
|
||||
defineValue(HTMLCanvasElement.prototype, "getContext", function getContext(type) {
|
||||
this.__lastContextType__ = String(type || "");
|
||||
if (type === "2d") {
|
||||
return createCanvas2DContext();
|
||||
}
|
||||
if (type === "webgl" || type === "experimental-webgl") {
|
||||
return createWebGLContext();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
defineValue(HTMLCanvasElement.prototype, "toDataURL", function toDataURL() {
|
||||
if (this.__lastContextType__ === "webgl" || this.__lastContextType__ === "experimental-webgl") {
|
||||
return seed.fingerprint.canvasWebglDataUrl || seed.fingerprint.canvas2dDataUrl || "data:image/png;base64,";
|
||||
}
|
||||
return seed.fingerprint.canvas2dDataUrl || "data:image/png;base64,";
|
||||
});
|
||||
defineValue(HTMLCanvasElement.prototype, "toBlob", function toBlob(callback) {
|
||||
if (typeof callback === "function") {
|
||||
callback({
|
||||
type: "image/png",
|
||||
size: (seed.fingerprint.canvas2dDataUrl || "").length
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
defineValue(globalObject, "__envCreateCanvasElement", function __envCreateCanvasElement() {
|
||||
return new HTMLCanvasElement();
|
||||
});
|
||||
|
||||
if (globalObject.navigator) {
|
||||
const navigatorProto = Object.getPrototypeOf(globalObject.navigator);
|
||||
if (navigatorProto && !Object.getOwnPropertyDescriptor(navigatorProto, "getBattery")) {
|
||||
defineValue(navigatorProto, "getBattery", function getBattery() {
|
||||
return Promise.resolve(Object.assign({
|
||||
charging: true,
|
||||
chargingTime: 0,
|
||||
dischargingTime: Infinity,
|
||||
level: 1
|
||||
}, seed.battery || {}));
|
||||
});
|
||||
if (protector && protector.repairFunctionMeta) {
|
||||
protector.repairFunctionMeta(navigatorProto.getBattery, { name: "getBattery", length: 0 });
|
||||
}
|
||||
if (protector && protector.protectFunction) {
|
||||
protector.protectFunction(navigatorProto.getBattery, "getBattery");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(seed.windowMetrics || {}).forEach(function eachMetric(key) {
|
||||
Object.defineProperty(globalObject, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: seed.windowMetrics[key]
|
||||
});
|
||||
});
|
||||
|
||||
if (protector && protector.repairFunctionMeta) {
|
||||
protector.repairFunctionMeta(Screen, { name: "Screen", length: 0 });
|
||||
protector.repairFunctionMeta(WebGLRenderingContext, { name: "WebGLRenderingContext", length: 0 });
|
||||
protector.repairFunctionMeta(HTMLCanvasElement, { name: "HTMLCanvasElement", length: 0 });
|
||||
protector.repairFunctionMeta(HTMLCanvasElement.prototype.getContext, { name: "getContext", length: 1 });
|
||||
protector.repairFunctionMeta(HTMLCanvasElement.prototype.toDataURL, { name: "toDataURL", length: 0 });
|
||||
}
|
||||
if (protector && protector.protectFunction) {
|
||||
protector.protectFunction(Screen, "Screen");
|
||||
protector.protectFunction(WebGLRenderingContext, "WebGLRenderingContext");
|
||||
protector.protectFunction(HTMLCanvasElement, "HTMLCanvasElement");
|
||||
protector.protectFunction(HTMLCanvasElement.prototype.getContext, "getContext");
|
||||
protector.protectFunction(HTMLCanvasElement.prototype.toDataURL, "toDataURL");
|
||||
}
|
||||
|
||||
Object.defineProperty(globalObject, "Screen", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: Screen
|
||||
});
|
||||
Object.defineProperty(globalObject, "screen", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: screenInstance
|
||||
});
|
||||
Object.defineProperty(globalObject, "WebGLRenderingContext", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: WebGLRenderingContext
|
||||
});
|
||||
Object.defineProperty(globalObject, "HTMLCanvasElement", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: HTMLCanvasElement
|
||||
});
|
||||
|
||||
return {
|
||||
screen: screenInstance,
|
||||
HTMLCanvasElement: HTMLCanvasElement
|
||||
};
|
||||
})(typeof globalThis !== "undefined" ? globalThis : window);
|
||||
`;
|
||||
}
|
||||
|
||||
function buildFingerprintModule(seed = {}) {
|
||||
const module = scaffoldModule("fingerprint-module");
|
||||
const normalizedSeed = normalizeFingerprintSeed(seed);
|
||||
|
||||
module.patchPlan = [
|
||||
"构建 Screen、HTMLCanvasElement、WebGLRenderingContext 等高频指纹对象壳。",
|
||||
"把 canvas 2d / webgl 的入口统一到 getContext/toDataURL/getParameter。",
|
||||
"把 screen 尺寸、windowMetrics 和 battery Promise 一起收敛到统一指纹种子。",
|
||||
];
|
||||
module.patchCode = renderFingerprintPatch(normalizedSeed);
|
||||
module.runtimeState = normalizedSeed;
|
||||
module.validation = [
|
||||
"检查 screen.width / height / availWidth / availHeight。",
|
||||
"检查 document.createElement('canvas').getContext('2d') / getContext('webgl')。",
|
||||
"检查 canvas.toDataURL() 与 webgl getParameter(37445/37446)。",
|
||||
"检查 navigator.getBattery() 返回 Promise 且值来自种子。",
|
||||
"检查 devicePixelRatio / innerWidth / outerWidth 等窗口尺寸指标。",
|
||||
];
|
||||
module.residualRisk = [
|
||||
"这里只覆盖常见 canvas/WebGL/battery/screen 检测点,未实现音频指纹、字体指纹和 OffscreenCanvas。",
|
||||
"如果目标站点会校验扩展列表、着色器精度或更复杂的渲染链,仍需继续收集真实浏览器结果。",
|
||||
];
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildFingerprintModule,
|
||||
normalizeFingerprintSeed,
|
||||
};
|
||||
@@ -0,0 +1,261 @@
|
||||
const { scaffoldModule } = require("./scaffold-module");
|
||||
const { buildPluginGraph } = require("./build-plugin-graph");
|
||||
|
||||
function clone(value) {
|
||||
return JSON.parse(JSON.stringify(value || {}));
|
||||
}
|
||||
|
||||
function normalizeNavigatorSeed(seed = {}) {
|
||||
const navigatorSeed = seed.navigator && typeof seed.navigator === "object" ? seed.navigator : seed;
|
||||
const plugins = Array.isArray(seed.plugins) ? clone(seed.plugins) : [];
|
||||
const mimeTypes = Array.isArray(seed.mimeTypes) ? clone(seed.mimeTypes) : [];
|
||||
|
||||
return {
|
||||
navigator: {
|
||||
userAgent: navigatorSeed.userAgent || "",
|
||||
platform: navigatorSeed.platform || "",
|
||||
vendor: navigatorSeed.vendor || "",
|
||||
language: navigatorSeed.language || "",
|
||||
languages: Array.isArray(navigatorSeed.languages) ? navigatorSeed.languages.slice() : [],
|
||||
webdriver: typeof navigatorSeed.webdriver === "boolean" ? navigatorSeed.webdriver : false,
|
||||
hardwareConcurrency: Number.isFinite(navigatorSeed.hardwareConcurrency)
|
||||
? navigatorSeed.hardwareConcurrency
|
||||
: 8,
|
||||
deviceMemory: Number.isFinite(navigatorSeed.deviceMemory) ? navigatorSeed.deviceMemory : 8,
|
||||
maxTouchPoints: Number.isFinite(navigatorSeed.maxTouchPoints) ? navigatorSeed.maxTouchPoints : 0,
|
||||
cookieEnabled: navigatorSeed.cookieEnabled !== false,
|
||||
pdfViewerEnabled: navigatorSeed.pdfViewerEnabled !== false,
|
||||
appCodeName: navigatorSeed.appCodeName || "Mozilla",
|
||||
appName: navigatorSeed.appName || "Netscape",
|
||||
appVersion: navigatorSeed.appVersion || navigatorSeed.userAgent || "",
|
||||
userAgentData: clone(seed.userAgentData || navigatorSeed.userAgentData || null),
|
||||
connection: clone(seed.connection || navigatorSeed.connection || null),
|
||||
},
|
||||
plugins,
|
||||
mimeTypes,
|
||||
battery: clone(seed.battery || null),
|
||||
};
|
||||
}
|
||||
|
||||
function renderNavigatorPatch(seed) {
|
||||
const serializedSeed = JSON.stringify(seed, null, 2);
|
||||
return `(
|
||||
function installNavigatorModule(globalObject) {
|
||||
const seed = ${serializedSeed};
|
||||
const protector = globalObject.__envNativeProtector || null;
|
||||
const pluginSeed = Array.isArray(seed.plugins) ? seed.plugins.slice() : [];
|
||||
const mimeSeed = Array.isArray(seed.mimeTypes) ? seed.mimeTypes.slice() : [];
|
||||
|
||||
function defineValue(target, key, value) {
|
||||
Object.defineProperty(target, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: value
|
||||
});
|
||||
}
|
||||
|
||||
function defineGetter(target, key, getter) {
|
||||
Object.defineProperty(target, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
get: getter
|
||||
});
|
||||
if (protector && protector.protectDescriptor) {
|
||||
protector.protectDescriptor(target, key);
|
||||
}
|
||||
}
|
||||
|
||||
function createIndexedCollection(items, namedKey) {
|
||||
const collection = {};
|
||||
for (let index = 0; index < items.length; index += 1) {
|
||||
Object.defineProperty(collection, index, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: items[index]
|
||||
});
|
||||
if (items[index] && items[index][namedKey]) {
|
||||
Object.defineProperty(collection, items[index][namedKey], {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: items[index]
|
||||
});
|
||||
}
|
||||
}
|
||||
defineValue(collection, "length", items.length);
|
||||
defineValue(collection, "item", function item(index) {
|
||||
return items[index] || null;
|
||||
});
|
||||
defineValue(collection, "namedItem", function namedItem(name) {
|
||||
return items.find(function findItem(entry) {
|
||||
return entry && entry[namedKey] === name;
|
||||
}) || null;
|
||||
});
|
||||
defineValue(collection, "refresh", function refresh() {});
|
||||
return collection;
|
||||
}
|
||||
|
||||
const pluginArray = pluginSeed.map(function mapPlugin(plugin) {
|
||||
return {
|
||||
name: plugin.name || "",
|
||||
filename: plugin.filename || "",
|
||||
description: plugin.description || "",
|
||||
length: 0
|
||||
};
|
||||
});
|
||||
|
||||
const mimeTypeArray = mimeSeed.map(function mapMime(mime) {
|
||||
return {
|
||||
type: mime.type || "",
|
||||
suffixes: mime.suffixes || "",
|
||||
description: mime.description || "",
|
||||
enabledPlugin: null
|
||||
};
|
||||
});
|
||||
|
||||
for (let index = 0; index < mimeSeed.length; index += 1) {
|
||||
const pluginName = mimeSeed[index].enabledPluginName || "";
|
||||
const plugin = pluginArray.find(function findPlugin(entry) {
|
||||
return entry.name === pluginName;
|
||||
}) || null;
|
||||
mimeTypeArray[index].enabledPlugin = plugin;
|
||||
if (plugin) {
|
||||
const itemIndex = plugin.length;
|
||||
Object.defineProperty(plugin, itemIndex, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: mimeTypeArray[index]
|
||||
});
|
||||
plugin.length += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const plugins = createIndexedCollection(pluginArray, "name");
|
||||
const mimeTypes = createIndexedCollection(mimeTypeArray, "type");
|
||||
|
||||
function Navigator() {}
|
||||
const navigatorState = seed.navigator || {};
|
||||
const navigatorInstance = Object.create(Navigator.prototype);
|
||||
|
||||
const scalarKeys = [
|
||||
"userAgent",
|
||||
"platform",
|
||||
"vendor",
|
||||
"language",
|
||||
"webdriver",
|
||||
"hardwareConcurrency",
|
||||
"deviceMemory",
|
||||
"maxTouchPoints",
|
||||
"cookieEnabled",
|
||||
"pdfViewerEnabled",
|
||||
"appCodeName",
|
||||
"appName",
|
||||
"appVersion"
|
||||
];
|
||||
|
||||
scalarKeys.forEach(function defineScalar(key) {
|
||||
defineGetter(Navigator.prototype, key, function getScalar() {
|
||||
return navigatorState[key];
|
||||
});
|
||||
});
|
||||
|
||||
defineGetter(Navigator.prototype, "languages", function getLanguages() {
|
||||
return Array.isArray(navigatorState.languages) ? navigatorState.languages.slice() : [];
|
||||
});
|
||||
defineGetter(Navigator.prototype, "plugins", function getPlugins() {
|
||||
return plugins;
|
||||
});
|
||||
defineGetter(Navigator.prototype, "mimeTypes", function getMimeTypes() {
|
||||
return mimeTypes;
|
||||
});
|
||||
defineGetter(Navigator.prototype, "userAgentData", function getUserAgentData() {
|
||||
return navigatorState.userAgentData || null;
|
||||
});
|
||||
defineGetter(Navigator.prototype, "connection", function getConnection() {
|
||||
return navigatorState.connection || null;
|
||||
});
|
||||
|
||||
defineValue(Navigator.prototype, "javaEnabled", function javaEnabled() {
|
||||
return false;
|
||||
});
|
||||
defineValue(Navigator.prototype, "getBattery", function getBattery() {
|
||||
return Promise.resolve(seed.battery || {
|
||||
charging: true,
|
||||
chargingTime: 0,
|
||||
dischargingTime: Infinity,
|
||||
level: 1
|
||||
});
|
||||
});
|
||||
|
||||
if (protector && protector.repairFunctionMeta) {
|
||||
protector.repairFunctionMeta(Navigator, { name: "Navigator", length: 0 });
|
||||
protector.repairFunctionMeta(Navigator.prototype.javaEnabled, { name: "javaEnabled", length: 0 });
|
||||
protector.repairFunctionMeta(Navigator.prototype.getBattery, { name: "getBattery", length: 0 });
|
||||
}
|
||||
if (protector && protector.protectFunction) {
|
||||
protector.protectFunction(Navigator, "Navigator");
|
||||
protector.protectFunction(Navigator.prototype.javaEnabled, "javaEnabled");
|
||||
protector.protectFunction(Navigator.prototype.getBattery, "getBattery");
|
||||
}
|
||||
|
||||
defineValue(navigatorInstance, Symbol.toStringTag, "Navigator");
|
||||
Object.defineProperty(globalObject, "Navigator", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: Navigator
|
||||
});
|
||||
Object.defineProperty(globalObject, "navigator", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: navigatorInstance
|
||||
});
|
||||
|
||||
return navigatorInstance;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : window);
|
||||
`;
|
||||
}
|
||||
|
||||
function buildNavigatorModule(seed = {}) {
|
||||
const module = scaffoldModule("navigator-module");
|
||||
const normalizedSeed = normalizeNavigatorSeed(seed);
|
||||
const pluginGraph = buildPluginGraph({
|
||||
plugins: normalizedSeed.plugins,
|
||||
mimeTypes: normalizedSeed.mimeTypes,
|
||||
});
|
||||
|
||||
module.patchPlan = [
|
||||
"构建 Navigator 构造器、prototype 和 navigator 实例。",
|
||||
"把 userAgent/language/webdriver 等高频字段补成 prototype getter,而不是直接灌到实例上。",
|
||||
"把 plugins/mimeTypes 构造成类数组图结构,并补 getBattery/userAgentData 等高频检测点。",
|
||||
];
|
||||
module.patchCode = renderNavigatorPatch(normalizedSeed);
|
||||
module.runtimeState = {
|
||||
navigator: normalizedSeed.navigator,
|
||||
plugins: pluginGraph.pluginArray,
|
||||
mimeTypes: pluginGraph.mimeTypeArray,
|
||||
battery: normalizedSeed.battery,
|
||||
};
|
||||
module.validation = [
|
||||
"检查 navigator.webdriver === false。",
|
||||
"检查 navigator.plugins.length 与 navigator.mimeTypes.length。",
|
||||
"检查 navigator.plugins.item(0) / namedItem(name) 与 mimeTypes.enabledPlugin 关系。",
|
||||
"检查 navigator.getBattery() 返回 Promise。",
|
||||
"检查 Navigator.prototype 上 getter 的描述符与 toString 保护是否生效。",
|
||||
];
|
||||
module.residualRisk = [
|
||||
"这里只覆盖常见字段与图结构,未实现 permissions、mediaDevices、bluetooth 等更深层能力对象。",
|
||||
"如果目标脚本会校验 brands/mobile/platformVersion 等 userAgentData 细节,仍需真实浏览器种子。",
|
||||
];
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildNavigatorModule,
|
||||
normalizeNavigatorSeed,
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
const { scaffoldModule } = require("./scaffold-module");
|
||||
|
||||
function toFiniteNumber(value, fallback) {
|
||||
return Number.isFinite(value) ? value : fallback;
|
||||
}
|
||||
|
||||
function cloneRecord(value) {
|
||||
return Object.assign({}, value || {});
|
||||
}
|
||||
|
||||
function normalizePerformanceSeed(seed = {}) {
|
||||
const performanceSeed = seed.performance && typeof seed.performance === "object" ? seed.performance : seed;
|
||||
const navigationStart = toFiniteNumber(performanceSeed.navigationStart, Date.now());
|
||||
const timeOrigin = toFiniteNumber(performanceSeed.timeOrigin, navigationStart);
|
||||
const nowOffset = toFiniteNumber(performanceSeed.nowOffset, 12.345);
|
||||
const timing = cloneRecord(performanceSeed.timing);
|
||||
|
||||
if (!Number.isFinite(timing.navigationStart)) timing.navigationStart = navigationStart;
|
||||
if (!Number.isFinite(timing.fetchStart)) timing.fetchStart = timing.navigationStart;
|
||||
if (!Number.isFinite(timing.domainLookupStart)) timing.domainLookupStart = timing.fetchStart + 1;
|
||||
if (!Number.isFinite(timing.domainLookupEnd)) timing.domainLookupEnd = timing.domainLookupStart + 1;
|
||||
if (!Number.isFinite(timing.connectStart)) timing.connectStart = timing.domainLookupEnd;
|
||||
if (!Number.isFinite(timing.connectEnd)) timing.connectEnd = timing.connectStart + 2;
|
||||
if (!Number.isFinite(timing.requestStart)) timing.requestStart = timing.connectEnd + 1;
|
||||
if (!Number.isFinite(timing.responseStart)) timing.responseStart = timing.requestStart + 4;
|
||||
if (!Number.isFinite(timing.responseEnd)) timing.responseEnd = timing.responseStart + 4;
|
||||
if (!Number.isFinite(timing.domLoading)) timing.domLoading = timing.responseEnd + 2;
|
||||
if (!Number.isFinite(timing.domInteractive)) timing.domInteractive = timing.domLoading + 5;
|
||||
if (!Number.isFinite(timing.domContentLoadedEventStart)) timing.domContentLoadedEventStart = timing.domInteractive + 1;
|
||||
if (!Number.isFinite(timing.domContentLoadedEventEnd)) timing.domContentLoadedEventEnd = timing.domContentLoadedEventStart + 1;
|
||||
if (!Number.isFinite(timing.domComplete)) timing.domComplete = timing.domContentLoadedEventEnd + 5;
|
||||
if (!Number.isFinite(timing.loadEventStart)) timing.loadEventStart = timing.domComplete + 1;
|
||||
if (!Number.isFinite(timing.loadEventEnd)) timing.loadEventEnd = timing.loadEventStart + 1;
|
||||
|
||||
return {
|
||||
timeOrigin,
|
||||
navigationStart,
|
||||
nowOffset,
|
||||
timing,
|
||||
memory: cloneRecord(performanceSeed.memory),
|
||||
};
|
||||
}
|
||||
|
||||
function renderPerformancePatch(seed) {
|
||||
const serializedSeed = JSON.stringify(seed, null, 2);
|
||||
return `(
|
||||
function installPerformanceModule(globalObject) {
|
||||
const seed = ${serializedSeed};
|
||||
const protector = globalObject.__envNativeProtector || null;
|
||||
const baseEpoch = Date.now();
|
||||
const monotonicStart = seed.nowOffset;
|
||||
let tick = 0;
|
||||
|
||||
function nextNow() {
|
||||
tick += 1;
|
||||
return monotonicStart + (Date.now() - baseEpoch) + tick * 0.01;
|
||||
}
|
||||
|
||||
function now() {
|
||||
return Number(nextNow().toFixed(3));
|
||||
}
|
||||
|
||||
function clearMarks() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function clearMeasures() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getEntries() {
|
||||
return [];
|
||||
}
|
||||
|
||||
function getEntriesByType() {
|
||||
return [];
|
||||
}
|
||||
|
||||
function getEntriesByName() {
|
||||
return [];
|
||||
}
|
||||
|
||||
const performanceObject = {
|
||||
now: now,
|
||||
timeOrigin: seed.timeOrigin,
|
||||
timing: seed.timing,
|
||||
clearMarks: clearMarks,
|
||||
clearMeasures: clearMeasures,
|
||||
getEntries: getEntries,
|
||||
getEntriesByType: getEntriesByType,
|
||||
getEntriesByName: getEntriesByName,
|
||||
mark: function mark() {},
|
||||
measure: function measure() {}
|
||||
};
|
||||
|
||||
if (seed.memory && Object.keys(seed.memory).length) {
|
||||
performanceObject.memory = seed.memory;
|
||||
}
|
||||
|
||||
if (protector && protector.repairFunctionMeta) {
|
||||
protector.repairFunctionMeta(now, { name: "now", length: 0 });
|
||||
protector.repairFunctionMeta(clearMarks, { name: "clearMarks", length: 0 });
|
||||
protector.repairFunctionMeta(clearMeasures, { name: "clearMeasures", length: 0 });
|
||||
protector.repairFunctionMeta(getEntries, { name: "getEntries", length: 0 });
|
||||
protector.repairFunctionMeta(getEntriesByType, { name: "getEntriesByType", length: 1 });
|
||||
protector.repairFunctionMeta(getEntriesByName, { name: "getEntriesByName", length: 1 });
|
||||
}
|
||||
if (protector && protector.protectFunction) {
|
||||
protector.protectFunction(now, "now");
|
||||
protector.protectFunction(clearMarks, "clearMarks");
|
||||
protector.protectFunction(clearMeasures, "clearMeasures");
|
||||
protector.protectFunction(getEntries, "getEntries");
|
||||
protector.protectFunction(getEntriesByType, "getEntriesByType");
|
||||
protector.protectFunction(getEntriesByName, "getEntriesByName");
|
||||
}
|
||||
|
||||
Object.defineProperty(globalObject, "performance", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: performanceObject
|
||||
});
|
||||
|
||||
return performanceObject;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : window);
|
||||
`;
|
||||
}
|
||||
|
||||
function buildPerformanceModule(seed = {}) {
|
||||
const module = scaffoldModule("performance-module");
|
||||
const normalizedSeed = normalizePerformanceSeed(seed);
|
||||
|
||||
module.patchPlan = [
|
||||
"构建 timeOrigin/navigationStart/timing 基础树。",
|
||||
"补出单调递增的 performance.now()。",
|
||||
"为 getEntries/getEntriesByType/getEntriesByName 等常见检测点提供稳定返回。",
|
||||
];
|
||||
module.patchCode = renderPerformancePatch(normalizedSeed);
|
||||
module.runtimeState = {
|
||||
performance: normalizedSeed,
|
||||
};
|
||||
module.validation = [
|
||||
"连续两次调用 performance.now(),确认返回值递增。",
|
||||
"检查 performance.timeOrigin 与 performance.timing.navigationStart 的相对关系。",
|
||||
"检查 performance.getEntries()/getEntriesByType()/getEntriesByName() 返回数组。",
|
||||
"检查 performance.now.toString() 以及 name/length 元信息。",
|
||||
];
|
||||
module.residualRisk = [
|
||||
"未实现 PerformanceObserver、resource timing、navigation entries 等复杂链路。",
|
||||
"如果目标脚本会校验 performance.memory 或多阶段时间戳分布,仍需采集真实浏览器种子。",
|
||||
];
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildPerformanceModule,
|
||||
normalizePerformanceSeed,
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
function buildPluginGraph(seed = {}) {
|
||||
const plugins = Array.isArray(seed.plugins) ? seed.plugins : [];
|
||||
const mimeTypes = Array.isArray(seed.mimeTypes) ? seed.mimeTypes : [];
|
||||
|
||||
const pluginArray = [];
|
||||
const mimeTypeArray = [];
|
||||
|
||||
for (const pluginSeed of plugins) {
|
||||
const plugin = {
|
||||
name: pluginSeed.name || "",
|
||||
filename: pluginSeed.filename || "",
|
||||
description: pluginSeed.description || "",
|
||||
length: 0,
|
||||
items: [],
|
||||
};
|
||||
pluginArray.push(plugin);
|
||||
}
|
||||
|
||||
for (const mimeSeed of mimeTypes) {
|
||||
const mime = {
|
||||
type: mimeSeed.type || "",
|
||||
suffixes: mimeSeed.suffixes || "",
|
||||
description: mimeSeed.description || "",
|
||||
enabledPluginName: mimeSeed.enabledPluginName || "",
|
||||
enabledPlugin: null,
|
||||
};
|
||||
mimeTypeArray.push(mime);
|
||||
}
|
||||
|
||||
for (const mime of mimeTypeArray) {
|
||||
const plugin = pluginArray.find((item) => item.name === mime.enabledPluginName);
|
||||
if (plugin) {
|
||||
mime.enabledPlugin = plugin;
|
||||
plugin.items.push(mime);
|
||||
plugin.length = plugin.items.length;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pluginArray,
|
||||
mimeTypeArray,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildPluginGraph,
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
const { scaffoldModule } = require("./scaffold-module");
|
||||
const { mergeStorageState } = require("./merge-storage-state");
|
||||
|
||||
function normalizeStorageSeed(seed = {}) {
|
||||
const merged = mergeStorageState(seed);
|
||||
return {
|
||||
cookie: seed.cookie || "",
|
||||
cookies: merged.cookies,
|
||||
localStorage: merged.localStorage,
|
||||
sessionStorage: merged.sessionStorage,
|
||||
};
|
||||
}
|
||||
|
||||
function renderStoragePatch(seed) {
|
||||
const serializedSeed = JSON.stringify(seed, null, 2);
|
||||
return `(
|
||||
function installStorageModule(globalObject) {
|
||||
const seed = ${serializedSeed};
|
||||
const protector = globalObject.__envNativeProtector || null;
|
||||
|
||||
function defineValue(target, key, value) {
|
||||
Object.defineProperty(target, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: value
|
||||
});
|
||||
}
|
||||
|
||||
function Storage() {}
|
||||
|
||||
defineValue(Storage.prototype, "key", function key(index) {
|
||||
const keys = Object.keys(this.__store__);
|
||||
return typeof keys[index] === "string" ? keys[index] : null;
|
||||
});
|
||||
defineValue(Storage.prototype, "getItem", function getItem(name) {
|
||||
const key = String(name);
|
||||
return Object.prototype.hasOwnProperty.call(this.__store__, key) ? this.__store__[key] : null;
|
||||
});
|
||||
defineValue(Storage.prototype, "setItem", function setItem(name, value) {
|
||||
this.__store__[String(name)] = String(value);
|
||||
this.length = Object.keys(this.__store__).length;
|
||||
});
|
||||
defineValue(Storage.prototype, "removeItem", function removeItem(name) {
|
||||
delete this.__store__[String(name)];
|
||||
this.length = Object.keys(this.__store__).length;
|
||||
});
|
||||
defineValue(Storage.prototype, "clear", function clear() {
|
||||
this.__store__ = Object.create(null);
|
||||
this.length = 0;
|
||||
});
|
||||
|
||||
function createStorage(initialState) {
|
||||
const storage = Object.create(Storage.prototype);
|
||||
defineValue(storage, "__store__", Object.assign(Object.create(null), initialState || {}));
|
||||
defineValue(storage, "length", Object.keys(storage.__store__).length);
|
||||
Object.keys(storage.__store__).forEach(function defineEntry(key) {
|
||||
Object.defineProperty(storage, key, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: function getEntry() {
|
||||
return storage.getItem(key);
|
||||
},
|
||||
set: function setEntry(value) {
|
||||
storage.setItem(key, value);
|
||||
}
|
||||
});
|
||||
});
|
||||
return storage;
|
||||
}
|
||||
|
||||
if (protector && protector.repairFunctionMeta) {
|
||||
protector.repairFunctionMeta(Storage, { name: "Storage", length: 0 });
|
||||
protector.repairFunctionMeta(Storage.prototype.key, { name: "key", length: 1 });
|
||||
protector.repairFunctionMeta(Storage.prototype.getItem, { name: "getItem", length: 1 });
|
||||
protector.repairFunctionMeta(Storage.prototype.setItem, { name: "setItem", length: 2 });
|
||||
protector.repairFunctionMeta(Storage.prototype.removeItem, { name: "removeItem", length: 1 });
|
||||
protector.repairFunctionMeta(Storage.prototype.clear, { name: "clear", length: 0 });
|
||||
}
|
||||
if (protector && protector.protectFunction) {
|
||||
protector.protectFunction(Storage, "Storage");
|
||||
protector.protectFunction(Storage.prototype.key, "key");
|
||||
protector.protectFunction(Storage.prototype.getItem, "getItem");
|
||||
protector.protectFunction(Storage.prototype.setItem, "setItem");
|
||||
protector.protectFunction(Storage.prototype.removeItem, "removeItem");
|
||||
protector.protectFunction(Storage.prototype.clear, "clear");
|
||||
}
|
||||
|
||||
const localStorage = createStorage(seed.localStorage || {});
|
||||
const sessionStorage = createStorage(seed.sessionStorage || {});
|
||||
|
||||
Object.defineProperty(globalObject, "Storage", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: Storage
|
||||
});
|
||||
Object.defineProperty(globalObject, "localStorage", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: localStorage
|
||||
});
|
||||
Object.defineProperty(globalObject, "sessionStorage", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: sessionStorage
|
||||
});
|
||||
|
||||
if (globalObject.document && typeof globalObject.document.cookie === "string" && seed.cookie) {
|
||||
globalObject.document.cookie = seed.cookie;
|
||||
}
|
||||
|
||||
return {
|
||||
localStorage: localStorage,
|
||||
sessionStorage: sessionStorage
|
||||
};
|
||||
})(typeof globalThis !== "undefined" ? globalThis : window);
|
||||
`;
|
||||
}
|
||||
|
||||
function buildStorageModule(seed = {}) {
|
||||
const module = scaffoldModule("storage-module");
|
||||
const normalizedSeed = normalizeStorageSeed(seed);
|
||||
|
||||
module.patchPlan = [
|
||||
"构建 Storage 构造器与原型方法。",
|
||||
"按真实种子预置 localStorage / sessionStorage 的 key/value 运行态。",
|
||||
"把 cookie 字符串与 document.cookie 入口联动,而不是把 storage 状态和 cookie 割裂开。",
|
||||
];
|
||||
module.patchCode = renderStoragePatch(normalizedSeed);
|
||||
module.runtimeState = normalizedSeed;
|
||||
module.validation = [
|
||||
"检查 localStorage.length / sessionStorage.length。",
|
||||
"检查 getItem/setItem/removeItem/clear/key 的返回值。",
|
||||
"检查 localStorage.device_id 这类同名属性访问是否与 getItem 一致。",
|
||||
"检查 document 已存在时 cookie 字符串能否同步注入。",
|
||||
];
|
||||
module.residualRisk = [
|
||||
"未实现配额异常、跨 tab 事件同步与 storage event。",
|
||||
"如果站点依赖历史态的更新时间、写入顺序或事件广播,仍需继续扩展。",
|
||||
];
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildStorageModule,
|
||||
normalizeStorageSeed,
|
||||
};
|
||||
@@ -0,0 +1,191 @@
|
||||
const { scaffoldModule } = require("./scaffold-module");
|
||||
|
||||
function normalizeRtcCandidate(candidate = {}) {
|
||||
return {
|
||||
candidate: candidate.candidate || "candidate:842163049 1 udp 1677729535 0.0.0.0 9 typ host generation 0 ufrag env network-id 1",
|
||||
sdpMid: candidate.sdpMid || "0",
|
||||
sdpMLineIndex: Number.isFinite(candidate.sdpMLineIndex) ? candidate.sdpMLineIndex : 0,
|
||||
usernameFragment: candidate.usernameFragment || "env",
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeWebrtcSeed(seed = {}) {
|
||||
const webrtcSeed = seed.webrtc && typeof seed.webrtc === "object" ? seed.webrtc : seed;
|
||||
return {
|
||||
allowCandidateLeak: webrtcSeed.allowCandidateLeak === true,
|
||||
iceTransportPolicy: webrtcSeed.iceTransportPolicy || "all",
|
||||
bundlePolicy: webrtcSeed.bundlePolicy || "balanced",
|
||||
rtcpMuxPolicy: webrtcSeed.rtcpMuxPolicy || "require",
|
||||
defaultOfferSdp: webrtcSeed.defaultOfferSdp || "v=0\\r\\no=- 0 0 IN IP4 127.0.0.1\\r\\ns=env-codex-offer\\r\\nt=0 0\\r\\n",
|
||||
defaultAnswerSdp: webrtcSeed.defaultAnswerSdp || "v=0\\r\\no=- 0 0 IN IP4 127.0.0.1\\r\\ns=env-codex-answer\\r\\nt=0 0\\r\\n",
|
||||
iceCandidates: Array.isArray(webrtcSeed.iceCandidates) && webrtcSeed.iceCandidates.length
|
||||
? webrtcSeed.iceCandidates.map(normalizeRtcCandidate)
|
||||
: [normalizeRtcCandidate()],
|
||||
};
|
||||
}
|
||||
|
||||
function renderWebrtcPatch(seed) {
|
||||
const serializedSeed = JSON.stringify(seed, null, 2);
|
||||
return `(
|
||||
function installWebrtcModule(globalObject) {
|
||||
const seed = ${serializedSeed};
|
||||
const protector = globalObject.__envNativeProtector || null;
|
||||
|
||||
function defineValue(target, key, value) {
|
||||
Object.defineProperty(target, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: value
|
||||
});
|
||||
}
|
||||
|
||||
function applyEventTarget(target) {
|
||||
target._listeners = Object.create(null);
|
||||
target.addEventListener = function addEventListener(type, listener) {
|
||||
if (!this._listeners[type]) this._listeners[type] = [];
|
||||
this._listeners[type].push(listener);
|
||||
};
|
||||
target.removeEventListener = function removeEventListener(type, listener) {
|
||||
const list = this._listeners[type] || [];
|
||||
this._listeners[type] = list.filter(function filterItem(item) {
|
||||
return item !== listener;
|
||||
});
|
||||
};
|
||||
target.dispatchEvent = function dispatchEvent(event) {
|
||||
const list = (this._listeners[event.type] || []).slice();
|
||||
for (const listener of list) listener.call(this, event);
|
||||
const handler = this["on" + event.type];
|
||||
if (typeof handler === "function") handler.call(this, event);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
function RTCSessionDescription(init) {
|
||||
init = init || {};
|
||||
this.type = init.type || "offer";
|
||||
this.sdp = init.sdp || "";
|
||||
}
|
||||
|
||||
function RTCIceCandidate(init) {
|
||||
init = init || {};
|
||||
this.candidate = init.candidate || "";
|
||||
this.sdpMid = init.sdpMid || null;
|
||||
this.sdpMLineIndex = typeof init.sdpMLineIndex === "number" ? init.sdpMLineIndex : null;
|
||||
this.usernameFragment = init.usernameFragment || null;
|
||||
}
|
||||
|
||||
function RTCDataChannel(label, dataChannelDict) {
|
||||
applyEventTarget(this);
|
||||
this.label = label || "";
|
||||
this.ordered = !dataChannelDict || dataChannelDict.ordered !== false;
|
||||
this.readyState = "open";
|
||||
}
|
||||
|
||||
RTCDataChannel.prototype.send = function send(data) {
|
||||
this.lastPayload = data;
|
||||
};
|
||||
RTCDataChannel.prototype.close = function close() {
|
||||
this.readyState = "closed";
|
||||
};
|
||||
|
||||
function RTCPeerConnection(configuration) {
|
||||
applyEventTarget(this);
|
||||
this.configuration = Object.assign({
|
||||
iceTransportPolicy: seed.iceTransportPolicy,
|
||||
bundlePolicy: seed.bundlePolicy,
|
||||
rtcpMuxPolicy: seed.rtcpMuxPolicy
|
||||
}, configuration || {});
|
||||
this.localDescription = null;
|
||||
this.remoteDescription = null;
|
||||
this.connectionState = "new";
|
||||
this.iceConnectionState = "new";
|
||||
this.signalingState = "stable";
|
||||
}
|
||||
|
||||
RTCPeerConnection.prototype.createOffer = function createOffer() {
|
||||
return Promise.resolve(new RTCSessionDescription({ type: "offer", sdp: seed.defaultOfferSdp }));
|
||||
};
|
||||
RTCPeerConnection.prototype.createAnswer = function createAnswer() {
|
||||
return Promise.resolve(new RTCSessionDescription({ type: "answer", sdp: seed.defaultAnswerSdp }));
|
||||
};
|
||||
RTCPeerConnection.prototype.setLocalDescription = function setLocalDescription(description) {
|
||||
this.localDescription = description || null;
|
||||
this.signalingState = "have-local-offer";
|
||||
if (seed.allowCandidateLeak) {
|
||||
for (const candidateSeed of seed.iceCandidates) {
|
||||
this.dispatchEvent({ type: "icecandidate", candidate: new RTCIceCandidate(candidateSeed) });
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
RTCPeerConnection.prototype.setRemoteDescription = function setRemoteDescription(description) {
|
||||
this.remoteDescription = description || null;
|
||||
this.signalingState = "stable";
|
||||
return Promise.resolve();
|
||||
};
|
||||
RTCPeerConnection.prototype.addIceCandidate = function addIceCandidate(candidate) {
|
||||
this.lastIceCandidate = candidate || null;
|
||||
return Promise.resolve();
|
||||
};
|
||||
RTCPeerConnection.prototype.createDataChannel = function createDataChannel(label, dataChannelDict) {
|
||||
return new RTCDataChannel(label, dataChannelDict);
|
||||
};
|
||||
RTCPeerConnection.prototype.close = function close() {
|
||||
this.connectionState = "closed";
|
||||
this.iceConnectionState = "closed";
|
||||
};
|
||||
|
||||
if (protector && protector.repairFunctionMeta) {
|
||||
protector.repairFunctionMeta(RTCSessionDescription, { name: "RTCSessionDescription", length: 1 });
|
||||
protector.repairFunctionMeta(RTCIceCandidate, { name: "RTCIceCandidate", length: 1 });
|
||||
protector.repairFunctionMeta(RTCPeerConnection, { name: "RTCPeerConnection", length: 1 });
|
||||
protector.repairFunctionMeta(RTCDataChannel, { name: "RTCDataChannel", length: 2 });
|
||||
}
|
||||
if (protector && protector.protectFunction) {
|
||||
protector.protectFunction(RTCSessionDescription, "RTCSessionDescription");
|
||||
protector.protectFunction(RTCIceCandidate, "RTCIceCandidate");
|
||||
protector.protectFunction(RTCPeerConnection, "RTCPeerConnection");
|
||||
protector.protectFunction(RTCDataChannel, "RTCDataChannel");
|
||||
}
|
||||
|
||||
defineValue(globalObject, "RTCSessionDescription", RTCSessionDescription);
|
||||
defineValue(globalObject, "RTCIceCandidate", RTCIceCandidate);
|
||||
defineValue(globalObject, "RTCPeerConnection", RTCPeerConnection);
|
||||
defineValue(globalObject, "webkitRTCPeerConnection", RTCPeerConnection);
|
||||
defineValue(globalObject, "RTCDataChannel", RTCDataChannel);
|
||||
|
||||
return { RTCPeerConnection: RTCPeerConnection, RTCDataChannel: RTCDataChannel };
|
||||
})(typeof globalThis !== "undefined" ? globalThis : window);
|
||||
`;
|
||||
}
|
||||
|
||||
function buildWebrtcModule(seed = {}) {
|
||||
const module = scaffoldModule("webrtc-module");
|
||||
const normalizedSeed = normalizeWebrtcSeed(seed);
|
||||
|
||||
module.patchPlan = [
|
||||
"补出 RTCPeerConnection / RTCSessionDescription / RTCIceCandidate / RTCDataChannel 构造器。",
|
||||
"为 createOffer/createAnswer/setLocalDescription/addIceCandidate/createDataChannel 提供 Promise 型基础实现。",
|
||||
"将 ICE candidate 是否泄露做成显式策略,而不是默认暴露本地网络信息。",
|
||||
];
|
||||
module.patchCode = renderWebrtcPatch(normalizedSeed);
|
||||
module.runtimeState = { webrtc: normalizedSeed };
|
||||
module.validation = [
|
||||
"检查 typeof RTCPeerConnection === 'function'。",
|
||||
"执行 new RTCPeerConnection().createOffer(),确认返回 Promise。",
|
||||
"执行 createDataChannel('env'),确认返回对象且包含 send/close。",
|
||||
"在 allowCandidateLeak=false 时确认不会主动抛出真实候选信息。",
|
||||
];
|
||||
module.residualRisk = [
|
||||
"这里只覆盖存在性检测和轻量行为检测,不提供真实音视频协商能力。",
|
||||
"若目标站点会校验 stats/transceiver/sender/receiver/track,仍需按真实浏览器继续扩展。",
|
||||
];
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildWebrtcModule,
|
||||
normalizeWebrtcSeed,
|
||||
};
|
||||
@@ -0,0 +1,195 @@
|
||||
const { scaffoldModule } = require("./scaffold-module");
|
||||
|
||||
function normalizeWorkerSeed(seed = {}) {
|
||||
const workerSeed = seed.worker && typeof seed.worker === "object" ? seed.worker : seed;
|
||||
return {
|
||||
scriptBaseUrl: workerSeed.scriptBaseUrl || "https://example.invalid/assets/",
|
||||
inlineScripts: Object.assign({}, workerSeed.inlineScripts || {}),
|
||||
sharedWorkerName: workerSeed.sharedWorkerName || "env-shared-worker",
|
||||
broadcastChannels: Array.isArray(workerSeed.broadcastChannels)
|
||||
? workerSeed.broadcastChannels.slice()
|
||||
: ["env-sync"],
|
||||
};
|
||||
}
|
||||
|
||||
function renderWorkerPatch(seed) {
|
||||
const serializedSeed = JSON.stringify(seed, null, 2);
|
||||
return `(
|
||||
function installWorkerModule(globalObject) {
|
||||
const seed = ${serializedSeed};
|
||||
const protector = globalObject.__envNativeProtector || null;
|
||||
const sharedChannels = Object.create(null);
|
||||
|
||||
function applyEventTarget(target) {
|
||||
target._listeners = Object.create(null);
|
||||
target.addEventListener = function addEventListener(type, listener) {
|
||||
if (!this._listeners[type]) this._listeners[type] = [];
|
||||
this._listeners[type].push(listener);
|
||||
};
|
||||
target.removeEventListener = function removeEventListener(type, listener) {
|
||||
const list = this._listeners[type] || [];
|
||||
this._listeners[type] = list.filter(function filterItem(item) {
|
||||
return item !== listener;
|
||||
});
|
||||
};
|
||||
target.dispatchEvent = function dispatchEvent(event) {
|
||||
const list = (this._listeners[event.type] || []).slice();
|
||||
for (const listener of list) listener.call(this, event);
|
||||
const handler = this["on" + event.type];
|
||||
if (typeof handler === "function") handler.call(this, event);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
function createMessageEvent(data) {
|
||||
return { type: "message", data: data };
|
||||
}
|
||||
|
||||
function MessagePort() {
|
||||
applyEventTarget(this);
|
||||
this._entangledPort = null;
|
||||
this._started = false;
|
||||
}
|
||||
|
||||
MessagePort.prototype.postMessage = function postMessage(data) {
|
||||
if (this._entangledPort) this._entangledPort.dispatchEvent(createMessageEvent(data));
|
||||
};
|
||||
MessagePort.prototype.start = function start() {
|
||||
this._started = true;
|
||||
};
|
||||
MessagePort.prototype.close = function close() {
|
||||
this._entangledPort = null;
|
||||
this._started = false;
|
||||
};
|
||||
|
||||
function createMessageChannel() {
|
||||
const port1 = new MessagePort();
|
||||
const port2 = new MessagePort();
|
||||
port1._entangledPort = port2;
|
||||
port2._entangledPort = port1;
|
||||
return { port1: port1, port2: port2 };
|
||||
}
|
||||
|
||||
function Worker(scriptUrl) {
|
||||
applyEventTarget(this);
|
||||
this.scriptURL = scriptUrl || "";
|
||||
this.baseUrl = seed.scriptBaseUrl;
|
||||
this.inlineSource = seed.inlineScripts[scriptUrl] || "";
|
||||
}
|
||||
|
||||
Worker.prototype.postMessage = function postMessage(data) {
|
||||
this.dispatchEvent(createMessageEvent(data));
|
||||
};
|
||||
Worker.prototype.terminate = function terminate() {
|
||||
this._terminated = true;
|
||||
};
|
||||
|
||||
function SharedWorker(scriptUrl, name) {
|
||||
applyEventTarget(this);
|
||||
this.scriptURL = scriptUrl || "";
|
||||
this.name = name || seed.sharedWorkerName;
|
||||
const channel = createMessageChannel();
|
||||
this.port = channel.port1;
|
||||
this._workerPort = channel.port2;
|
||||
}
|
||||
|
||||
function BroadcastChannel(name) {
|
||||
applyEventTarget(this);
|
||||
this.name = name || "";
|
||||
if (!sharedChannels[this.name]) sharedChannels[this.name] = [];
|
||||
sharedChannels[this.name].push(this);
|
||||
}
|
||||
|
||||
BroadcastChannel.prototype.postMessage = function postMessage(data) {
|
||||
const peers = sharedChannels[this.name] || [];
|
||||
for (const peer of peers) {
|
||||
if (peer !== this) peer.dispatchEvent(createMessageEvent(data));
|
||||
}
|
||||
};
|
||||
BroadcastChannel.prototype.close = function close() {
|
||||
const peers = sharedChannels[this.name] || [];
|
||||
sharedChannels[this.name] = peers.filter(function filterPeer(item) {
|
||||
return item !== this;
|
||||
}, this);
|
||||
};
|
||||
|
||||
if (protector && protector.repairFunctionMeta) {
|
||||
protector.repairFunctionMeta(MessagePort, { name: "MessagePort", length: 0 });
|
||||
protector.repairFunctionMeta(Worker, { name: "Worker", length: 1 });
|
||||
protector.repairFunctionMeta(SharedWorker, { name: "SharedWorker", length: 2 });
|
||||
protector.repairFunctionMeta(BroadcastChannel, { name: "BroadcastChannel", length: 1 });
|
||||
}
|
||||
if (protector && protector.protectFunction) {
|
||||
protector.protectFunction(MessagePort, "MessagePort");
|
||||
protector.protectFunction(Worker, "Worker");
|
||||
protector.protectFunction(SharedWorker, "SharedWorker");
|
||||
protector.protectFunction(BroadcastChannel, "BroadcastChannel");
|
||||
}
|
||||
|
||||
Object.defineProperty(globalObject, "MessagePort", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: MessagePort
|
||||
});
|
||||
Object.defineProperty(globalObject, "MessageChannel", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: function MessageChannel() {
|
||||
return createMessageChannel();
|
||||
}
|
||||
});
|
||||
Object.defineProperty(globalObject, "Worker", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: Worker
|
||||
});
|
||||
Object.defineProperty(globalObject, "SharedWorker", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: SharedWorker
|
||||
});
|
||||
Object.defineProperty(globalObject, "BroadcastChannel", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: BroadcastChannel
|
||||
});
|
||||
|
||||
return { Worker: Worker, SharedWorker: SharedWorker, BroadcastChannel: BroadcastChannel };
|
||||
})(typeof globalThis !== "undefined" ? globalThis : window);
|
||||
`;
|
||||
}
|
||||
|
||||
function buildWorkerModule(seed = {}) {
|
||||
const module = scaffoldModule("worker-module");
|
||||
const normalizedSeed = normalizeWorkerSeed(seed);
|
||||
|
||||
module.patchPlan = [
|
||||
"补出 Worker/SharedWorker/MessagePort/MessageChannel/BroadcastChannel 构造器。",
|
||||
"用最小事件系统模拟 postMessage/onmessage 通路。",
|
||||
"把 worker 脚本路径、inline source、广播频道名称外置为种子,而不是写死在补丁里。",
|
||||
];
|
||||
module.patchCode = renderWorkerPatch(normalizedSeed);
|
||||
module.runtimeState = { worker: normalizedSeed };
|
||||
module.validation = [
|
||||
"检查 typeof Worker/SharedWorker/BroadcastChannel === 'function'。",
|
||||
"执行 new Worker('/worker.js').postMessage({ping:1}),确认 message 事件能被触发。",
|
||||
"执行 new SharedWorker('/shared.js').port.postMessage('x'),确认端口存在。",
|
||||
"执行两个同名 BroadcastChannel 互发消息,确认消息广播通路正常。",
|
||||
];
|
||||
module.residualRisk = [
|
||||
"这里只模拟消息通路,不提供真实线程、importScripts、URL.createObjectURL 等完整能力。",
|
||||
"若目标站点依赖 Worker 全局作用域、自定义事件循环或二进制消息通路,仍需继续扩展。",
|
||||
];
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildWorkerModule,
|
||||
normalizeWorkerSeed,
|
||||
};
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
Browser-side environment collector template.
|
||||
Reconstructed from public env-harvesting patterns, especially boda_jsEnv's
|
||||
"changeDom.js", but normalized into a structured JSON exporter.
|
||||
*/
|
||||
|
||||
(function collectBrowserEnv() {
|
||||
function safeCall(fn, fallback) {
|
||||
try {
|
||||
return fn();
|
||||
} catch (err) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function dumpStorage(storage) {
|
||||
const out = {};
|
||||
if (!storage) {
|
||||
return out;
|
||||
}
|
||||
for (let i = 0; i < storage.length; i += 1) {
|
||||
const key = storage.key(i);
|
||||
out[key] = storage.getItem(key);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function dumpNavigator() {
|
||||
return {
|
||||
userAgent: safeCall(() => navigator.userAgent, ""),
|
||||
platform: safeCall(() => navigator.platform, ""),
|
||||
vendor: safeCall(() => navigator.vendor, ""),
|
||||
language: safeCall(() => navigator.language, ""),
|
||||
languages: safeCall(() => Array.from(navigator.languages || []), []),
|
||||
webdriver: safeCall(() => navigator.webdriver, undefined),
|
||||
hardwareConcurrency: safeCall(() => navigator.hardwareConcurrency, undefined),
|
||||
deviceMemory: safeCall(() => navigator.deviceMemory, undefined),
|
||||
maxTouchPoints: safeCall(() => navigator.maxTouchPoints, undefined),
|
||||
cookieEnabled: safeCall(() => navigator.cookieEnabled, undefined),
|
||||
pdfViewerEnabled: safeCall(() => navigator.pdfViewerEnabled, undefined),
|
||||
userAgentData: safeCall(() => navigator.userAgentData, null),
|
||||
connection: safeCall(() => navigator.connection, null),
|
||||
webkitPersistentStorage: safeCall(() => navigator.webkitPersistentStorage, null),
|
||||
webkitTemporaryStorage: safeCall(() => navigator.webkitTemporaryStorage, null),
|
||||
};
|
||||
}
|
||||
|
||||
function dumpDocument() {
|
||||
return {
|
||||
URL: safeCall(() => document.URL, ""),
|
||||
referrer: safeCall(() => document.referrer, ""),
|
||||
documentURI: safeCall(() => document.documentURI, ""),
|
||||
compatMode: safeCall(() => document.compatMode, ""),
|
||||
dir: safeCall(() => document.dir, ""),
|
||||
title: safeCall(() => document.title, ""),
|
||||
designMode: safeCall(() => document.designMode, ""),
|
||||
readyState: safeCall(() => document.readyState, ""),
|
||||
contentType: safeCall(() => document.contentType, ""),
|
||||
inputEncoding: safeCall(() => document.inputEncoding, ""),
|
||||
domain: safeCall(() => document.domain, ""),
|
||||
characterSet: safeCall(() => document.characterSet, ""),
|
||||
charset: safeCall(() => document.charset, ""),
|
||||
hidden: safeCall(() => document.hidden, undefined),
|
||||
cookie: safeCall(() => document.cookie, ""),
|
||||
};
|
||||
}
|
||||
|
||||
function dumpScreen() {
|
||||
return {
|
||||
width: safeCall(() => screen.width, undefined),
|
||||
height: safeCall(() => screen.height, undefined),
|
||||
availWidth: safeCall(() => screen.availWidth, undefined),
|
||||
availHeight: safeCall(() => screen.availHeight, undefined),
|
||||
availLeft: safeCall(() => screen.availLeft, undefined),
|
||||
availTop: safeCall(() => screen.availTop, undefined),
|
||||
colorDepth: safeCall(() => screen.colorDepth, undefined),
|
||||
pixelDepth: safeCall(() => screen.pixelDepth, undefined),
|
||||
isExtended: safeCall(() => screen.isExtended, undefined),
|
||||
orientation: safeCall(() => screen.orientation, null),
|
||||
};
|
||||
}
|
||||
|
||||
function dumpWindowMetrics() {
|
||||
return {
|
||||
devicePixelRatio: safeCall(() => window.devicePixelRatio, undefined),
|
||||
innerWidth: safeCall(() => window.innerWidth, undefined),
|
||||
innerHeight: safeCall(() => window.innerHeight, undefined),
|
||||
outerWidth: safeCall(() => window.outerWidth, undefined),
|
||||
outerHeight: safeCall(() => window.outerHeight, undefined),
|
||||
isSecureContext: safeCall(() => window.isSecureContext, undefined),
|
||||
};
|
||||
}
|
||||
|
||||
function collectCanvasFingerprint() {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) {
|
||||
return { canvas2d: null, webgl: null };
|
||||
}
|
||||
ctx.textBaseline = "top";
|
||||
ctx.font = "14px Arial";
|
||||
ctx.fillStyle = "#f60";
|
||||
ctx.fillRect(10, 10, 120, 40);
|
||||
ctx.fillStyle = "#069";
|
||||
ctx.fillText("env-codex", 14, 16);
|
||||
const canvas2d = safeCall(() => canvas.toDataURL(), null);
|
||||
|
||||
let webgl = null;
|
||||
const gl = safeCall(() => canvas.getContext("webgl") || canvas.getContext("experimental-webgl"), null);
|
||||
if (gl) {
|
||||
webgl = {
|
||||
vendor: safeCall(() => gl.getParameter(37445), null),
|
||||
renderer: safeCall(() => gl.getParameter(37446), null),
|
||||
};
|
||||
}
|
||||
return { canvas2d, webgl };
|
||||
}
|
||||
|
||||
const payload = {
|
||||
timestamp: new Date().toISOString(),
|
||||
location: {
|
||||
href: safeCall(() => location.href, ""),
|
||||
origin: safeCall(() => location.origin, ""),
|
||||
protocol: safeCall(() => location.protocol, ""),
|
||||
host: safeCall(() => location.host, ""),
|
||||
hostname: safeCall(() => location.hostname, ""),
|
||||
pathname: safeCall(() => location.pathname, ""),
|
||||
search: safeCall(() => location.search, ""),
|
||||
hash: safeCall(() => location.hash, ""),
|
||||
},
|
||||
history: {
|
||||
length: safeCall(() => history.length, undefined),
|
||||
scrollRestoration: safeCall(() => history.scrollRestoration, undefined),
|
||||
},
|
||||
navigator: dumpNavigator(),
|
||||
document: dumpDocument(),
|
||||
screen: dumpScreen(),
|
||||
windowMetrics: dumpWindowMetrics(),
|
||||
localStorage: dumpStorage(window.localStorage),
|
||||
sessionStorage: dumpStorage(window.sessionStorage),
|
||||
fingerprint: collectCanvasFingerprint(),
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(payload, null, 2));
|
||||
return payload;
|
||||
})();
|
||||
@@ -0,0 +1,32 @@
|
||||
function parseCookieString(cookieString) {
|
||||
const out = {};
|
||||
if (!cookieString) {
|
||||
return out;
|
||||
}
|
||||
for (const item of String(cookieString).split(";")) {
|
||||
const trimmed = item.trim();
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
const idx = trimmed.indexOf("=");
|
||||
if (idx === -1) {
|
||||
out[trimmed] = "";
|
||||
continue;
|
||||
}
|
||||
out[trimmed.slice(0, idx)] = trimmed.slice(idx + 1);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function mergeStorageState(input = {}) {
|
||||
return {
|
||||
cookies: parseCookieString(input.cookie || ""),
|
||||
localStorage: Object.assign({}, input.localStorage || {}),
|
||||
sessionStorage: Object.assign({}, input.sessionStorage || {}),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mergeStorageState,
|
||||
parseCookieString,
|
||||
};
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
Runtime observer template.
|
||||
Derived from qxVm/catvm proxy ideas, but rewritten into a reusable logger that
|
||||
aggregates missing paths, descriptor probes, and invocation traces.
|
||||
*/
|
||||
|
||||
function createRuntimeObserver(config = {}) {
|
||||
const logs = [];
|
||||
const seen = new WeakMap();
|
||||
const maxDepth = config.maxDepth || 4;
|
||||
const logOpen = config.logOpen !== false;
|
||||
const skip = new Set(config.skip || ["toString", "__proto__"]);
|
||||
|
||||
function record(type, path, detail) {
|
||||
const entry = { type, path, detail };
|
||||
logs.push(entry);
|
||||
if (logOpen) {
|
||||
console.log("[observer]", type, path, detail);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
function summarize() {
|
||||
const missingPaths = [];
|
||||
const descriptorAccess = [];
|
||||
const invocationErrors = [];
|
||||
const prototypeAccess = [];
|
||||
|
||||
for (const log of logs) {
|
||||
if (log.type === "missing") {
|
||||
missingPaths.push(log.path);
|
||||
} else if (log.type === "descriptor") {
|
||||
descriptorAccess.push(log.path);
|
||||
} else if (log.type === "invoke-error") {
|
||||
invocationErrors.push(log.path);
|
||||
} else if (log.type === "prototype") {
|
||||
prototypeAccess.push(log.path);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
total: logs.length,
|
||||
missingPaths,
|
||||
descriptorAccess,
|
||||
invocationErrors,
|
||||
prototypeAccess,
|
||||
logs,
|
||||
};
|
||||
}
|
||||
|
||||
function wrap(target, name = "root", depth = 0) {
|
||||
if (!target || (typeof target !== "object" && typeof target !== "function")) {
|
||||
return target;
|
||||
}
|
||||
if (seen.has(target)) {
|
||||
return seen.get(target);
|
||||
}
|
||||
if (depth > maxDepth) {
|
||||
return target;
|
||||
}
|
||||
|
||||
const proxy = new Proxy(target, {
|
||||
get(t, key, receiver) {
|
||||
const path = name + "." + String(key);
|
||||
let value;
|
||||
try {
|
||||
value = Reflect.get(t, key, receiver);
|
||||
} catch (err) {
|
||||
record("invoke-error", path, err && err.message ? err.message : String(err));
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!skip.has(String(key))) {
|
||||
if (typeof value === "undefined") {
|
||||
record("missing", path, "undefined");
|
||||
} else {
|
||||
record("get", path, typeof value);
|
||||
}
|
||||
}
|
||||
|
||||
if (value && (typeof value === "object" || typeof value === "function")) {
|
||||
return wrap(value, path, depth + 1);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
set(t, key, value, receiver) {
|
||||
const path = name + "." + String(key);
|
||||
record("set", path, typeof value);
|
||||
return Reflect.set(t, key, value, receiver);
|
||||
},
|
||||
has(t, key) {
|
||||
const path = name + "." + String(key);
|
||||
record("has", path, Reflect.has(t, key));
|
||||
return Reflect.has(t, key);
|
||||
},
|
||||
deleteProperty(t, key) {
|
||||
const path = name + "." + String(key);
|
||||
record("delete", path, true);
|
||||
return Reflect.deleteProperty(t, key);
|
||||
},
|
||||
getOwnPropertyDescriptor(t, key) {
|
||||
const path = name + "." + String(key);
|
||||
const result = Reflect.getOwnPropertyDescriptor(t, key);
|
||||
record("descriptor", path, result ? Object.keys(result) : null);
|
||||
return result;
|
||||
},
|
||||
defineProperty(t, key, attrs) {
|
||||
const path = name + "." + String(key);
|
||||
record("define", path, Object.keys(attrs || {}));
|
||||
return Reflect.defineProperty(t, key, attrs);
|
||||
},
|
||||
getPrototypeOf(t) {
|
||||
record("prototype", name, "getPrototypeOf");
|
||||
return Reflect.getPrototypeOf(t);
|
||||
},
|
||||
setPrototypeOf(t, proto) {
|
||||
record("prototype", name, "setPrototypeOf");
|
||||
return Reflect.setPrototypeOf(t, proto);
|
||||
},
|
||||
ownKeys(t) {
|
||||
record("ownKeys", name, Reflect.ownKeys(t));
|
||||
return Reflect.ownKeys(t);
|
||||
},
|
||||
isExtensible(t) {
|
||||
record("isExtensible", name, Reflect.isExtensible(t));
|
||||
return Reflect.isExtensible(t);
|
||||
},
|
||||
preventExtensions(t) {
|
||||
record("preventExtensions", name, true);
|
||||
return Reflect.preventExtensions(t);
|
||||
},
|
||||
apply(t, thisArg, args) {
|
||||
record("apply", name, args);
|
||||
return Reflect.apply(t, thisArg, args);
|
||||
},
|
||||
construct(t, args, newTarget) {
|
||||
record("construct", name, args);
|
||||
return Reflect.construct(t, args, newTarget);
|
||||
},
|
||||
});
|
||||
|
||||
seen.set(target, proxy);
|
||||
return proxy;
|
||||
}
|
||||
|
||||
return {
|
||||
wrap,
|
||||
summarize,
|
||||
logs,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createRuntimeObserver,
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
Native protection template.
|
||||
Derived from qxVm/catvm safe-function patterns and normalized into a reusable
|
||||
module for protecting functions, getters, and setters.
|
||||
*/
|
||||
|
||||
function installNativeProtector() {
|
||||
const rawToString = Function.toString;
|
||||
const nativeSymbol = Symbol("native_toString_" + Math.random().toString(36).slice(2));
|
||||
|
||||
function defineHidden(target, key, value) {
|
||||
Object.defineProperty(target, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
function markNative(fn, displayName) {
|
||||
if (typeof fn !== "function") {
|
||||
return fn;
|
||||
}
|
||||
const name = displayName || fn.name || "";
|
||||
defineHidden(fn, nativeSymbol, "function " + name + "() { [native code] }");
|
||||
return fn;
|
||||
}
|
||||
|
||||
function protectedToString() {
|
||||
if (typeof this === "function" && this[nativeSymbol]) {
|
||||
return this[nativeSymbol];
|
||||
}
|
||||
return rawToString.call(this);
|
||||
}
|
||||
|
||||
defineHidden(Function.prototype, "toString", protectedToString);
|
||||
defineHidden(Function, "toString", protectedToString);
|
||||
markNative(Function.prototype.toString, "toString");
|
||||
|
||||
function protectFunction(fn, displayName) {
|
||||
return markNative(fn, displayName);
|
||||
}
|
||||
|
||||
function protectDescriptor(target, key) {
|
||||
const desc = Object.getOwnPropertyDescriptor(target, key);
|
||||
if (!desc) {
|
||||
return false;
|
||||
}
|
||||
if (typeof desc.get === "function") {
|
||||
markNative(desc.get, "get " + key);
|
||||
}
|
||||
if (typeof desc.set === "function") {
|
||||
markNative(desc.set, "set " + key);
|
||||
}
|
||||
if (typeof desc.value === "function") {
|
||||
markNative(desc.value, desc.value.name || key);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function repairFunctionMeta(fn, meta = {}) {
|
||||
if (typeof fn !== "function") {
|
||||
return fn;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(meta, "name")) {
|
||||
Object.defineProperty(fn, "name", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: meta.name,
|
||||
});
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(meta, "length")) {
|
||||
Object.defineProperty(fn, "length", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: meta.length,
|
||||
});
|
||||
}
|
||||
return fn;
|
||||
}
|
||||
|
||||
return {
|
||||
nativeSymbol,
|
||||
protectFunction,
|
||||
protectDescriptor,
|
||||
repairFunctionMeta,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
installNativeProtector,
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
function scaffoldModule(name) {
|
||||
return {
|
||||
name,
|
||||
patchPlan: [],
|
||||
patchCode: "",
|
||||
runtimeState: {},
|
||||
validation: [],
|
||||
residualRisk: [],
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
scaffoldModule,
|
||||
};
|
||||
Reference in New Issue
Block a user