reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
@@ -0,0 +1,437 @@
|
||||
# 复杂入口分析规则
|
||||
|
||||
本文用于处理复杂的加密入口场景,包括异步函数、Promise、Web Crypto API、WebAssembly、Webpack 模块、Vite/Rollup/ESM 以及闭包作用域限制。
|
||||
|
||||
---
|
||||
|
||||
## 1. 异步与 Promise 入口
|
||||
|
||||
### 适用条件
|
||||
|
||||
- 目标入口返回 Promise,例如 crypto.subtle.digest(...)、Webpack 动态 import() 或 async function。
|
||||
- JSRPC 动作处理器必须等待 Promise 完成后再返回结果。
|
||||
|
||||
### 识别方式
|
||||
|
||||
- 函数返回带有 .then() 方法的对象。
|
||||
- analysis_result.json 中的 call_signature.async 为 true。
|
||||
- 源码使用 async function 或返回 new Promise()。
|
||||
- 运行时 Hook 会在 crypto[] 事件中记录返回 Promise 的函数。
|
||||
- 在依赖提取阶段调用入口,并检查 typeof result.then === 'function'。
|
||||
|
||||
### JSRPC 处理要求
|
||||
|
||||
动作处理器必须识别 Promise 并等待结果。这适用于所有调用,不仅限于源码显式声明为异步的函数。
|
||||
|
||||
~~~js
|
||||
var result = fn.apply(ctx, args);
|
||||
if (result && typeof result.then === 'function') {
|
||||
result.then(function(asyncResult) {
|
||||
// 处理 crypto.subtle 返回的 ArrayBuffer/Uint8Array
|
||||
if (asyncResult instanceof ArrayBuffer) {
|
||||
resolve(Array.from(new Uint8Array(asyncResult)));
|
||||
} else {
|
||||
resolve(asyncResult);
|
||||
}
|
||||
}).catch(function(error) {
|
||||
resolve(normalizeError(parameter, error));
|
||||
});
|
||||
return;
|
||||
}
|
||||
resolve(result);
|
||||
~~~
|
||||
|
||||
当 invocation.mode 为 "async" 或 "promise" 时,生成器必须包含 Promise 处理逻辑,否则 JSRPC /go 可能返回 undefined。
|
||||
|
||||
### 异步入口前置条件
|
||||
|
||||
- 调用 crypto.subtle.encrypt/decrypt/sign/verify 前,可能必须先导入或生成 CryptoKey。
|
||||
- Webpack 动态导入可能要求目标分块先完成加载。
|
||||
- 将这些条件记录到 invocation.preconditions 和 parameters[].runtime.bootstrap。
|
||||
|
||||
### 异步入口证据
|
||||
|
||||
- 运行时 Hook 从 crypto[] 中记录带有业务调用栈的事件。
|
||||
- Promise 完成后填写的 outputLen 可以证明算法产生了输出。
|
||||
- 与 requests[] 交叉比对:如果加密事件后很快出现 fetch/XHR 请求,说明业务函数可能把加密结果写入了请求体。
|
||||
|
||||
---
|
||||
|
||||
## 2. Web Crypto API(crypto.subtle)
|
||||
|
||||
### 特征
|
||||
|
||||
- 所有方法返回 Promise<ArrayBuffer>。
|
||||
- digest(algorithm, data):摘要。
|
||||
- sign(algorithm, key, data):数字签名(HMAC/RSASSA/ECDSA)。
|
||||
- encrypt(algorithm, key, data):加密(AES/RSA)。
|
||||
- decrypt(algorithm, key, data):解密。
|
||||
- importKey(...):导入密钥。
|
||||
- exportKey(...):导出密钥。
|
||||
- 输入通常是 ArrayBuffer/TypedArray,输出是 ArrayBuffer。
|
||||
|
||||
### 识别
|
||||
|
||||
运行时 Hook 会自动包装核心方法,证据记录在 window.__JSRA_TRACE__.crypto[]。
|
||||
|
||||
### 算法识别
|
||||
|
||||
- crypto[] 事件的 algorithm 字段:对象取 algorithm.name,字符串直接记录原值。
|
||||
- 常见算法名包括:SHA-1、SHA-256、SHA-384、SHA-512、AES-CBC、AES-GCM、AES-CTR、RSA-OAEP、RSASSA-PKCS1-v1_5、ECDSA、HMAC。
|
||||
|
||||
### Hook 示例(摘要)
|
||||
|
||||
~~~js
|
||||
var origDigest = crypto.subtle.digest.bind(crypto.subtle);
|
||||
crypto.subtle.digest = function(algorithm, data) {
|
||||
var entry = { type: 'digest', algorithm: algorithm.name || algorithm, inputLen: data.byteLength };
|
||||
trace.crypto.push(entry);
|
||||
return origDigest(algorithm, data).then(function(hash) {
|
||||
entry.outputLen = hash.byteLength;
|
||||
entry.outputHex = Array.from(new Uint8Array(hash)).map(function(b) {
|
||||
return b.toString(16).padStart(2, '0');
|
||||
}).join('').substring(0, 32) + '...';
|
||||
return hash;
|
||||
});
|
||||
};
|
||||
~~~
|
||||
|
||||
### 密钥材料
|
||||
|
||||
- crypto.subtle.sign/encrypt/decrypt 的第二个参数必须是 CryptoKey。
|
||||
- 如果密钥通过 crypto.subtle.importKey 在运行时导入,导入事件也会出现在 crypto[] 中。
|
||||
- CryptoKey 对页面脚本通常不可直接读取;但通过 importKey Hook 可以观察 keyData 原始参数。
|
||||
|
||||
### 补充 importKey Hook
|
||||
|
||||
如果运行时探针尚未覆盖 importKey,可以临时补充:
|
||||
|
||||
~~~js
|
||||
(function() {
|
||||
'use strict';
|
||||
if (!window.crypto || !window.crypto.subtle) return;
|
||||
var origImportKey = crypto.subtle.importKey.bind(crypto.subtle);
|
||||
crypto.subtle.importKey = function(format, keyData, algorithm, extractable, usages) {
|
||||
console.log('[JSRA] crypto.subtle.importKey', {
|
||||
format: format,
|
||||
algorithm: typeof algorithm === 'string' ? algorithm : algorithm.name,
|
||||
usages: usages,
|
||||
keyDataSnippet: keyData instanceof ArrayBuffer
|
||||
? '[ArrayBuffer:' + keyData.byteLength + ']'
|
||||
: (keyData instanceof Uint8Array ? '[Uint8Array:' + keyData.length + ']' : String(keyData))
|
||||
});
|
||||
return origImportKey(format, keyData, algorithm, extractable, usages);
|
||||
};
|
||||
})();
|
||||
~~~
|
||||
|
||||
### ArrayBuffer 转十六进制
|
||||
|
||||
~~~js
|
||||
function arrayBufferToHex(buffer) {
|
||||
return Array.from(new Uint8Array(buffer))
|
||||
.map(function(b) { return b.toString(16).padStart(2, '0'); })
|
||||
.join('');
|
||||
}
|
||||
~~~
|
||||
|
||||
### crypto.subtle 的 JSRPC 生成要求
|
||||
|
||||
- 入口策略使用 async_crypto。
|
||||
- JSRPC 必须使用正确的算法、密钥和数据调用函数,等待 Promise 完成,并按目标协议将 ArrayBuffer 转为十六进制或 Base64。
|
||||
- 入口为 crypto.subtle.* 时,处理器必须使用 ArrayBuffer 参数、识别返回的 Promise,并在完成后转换输出。
|
||||
- 如果必须先导入密钥,应在处理器中完成导入,或将其记录为启动依赖。
|
||||
|
||||
### 限制
|
||||
|
||||
- CryptoKey 不能传出浏览器上下文;JSRPC 必须在密钥被导入的同一页面上下文中执行。
|
||||
- 使用 extractable: false 生成的密钥不能导出,必须直接使用页面内的密钥引用执行签名或加密。
|
||||
- chrome-devtools-mcp 无法直接拦截浏览器内部密码学运算,只能通过软 Hook 观察输入和输出。
|
||||
|
||||
---
|
||||
|
||||
## 3. WebAssembly(WASM)
|
||||
|
||||
### 识别信号
|
||||
|
||||
- 网络请求加载 .wasm 文件。
|
||||
- window 上存在 WebAssembly 对象。
|
||||
- 页面调用 WebAssembly.instantiate 或 WebAssembly.instantiateStreaming。
|
||||
- 存在仅含数字的函数名,或参数/返回值为 Number、BigInt 的不透明函数。
|
||||
|
||||
### 可以执行的操作
|
||||
|
||||
- Hook WebAssembly.instantiate / WebAssembly.instantiateStreaming。
|
||||
- 使用 WebAssembly.Module.exports(module) 查看导出函数。
|
||||
- 从 JavaScript 调用已导出的 WASM 函数。
|
||||
|
||||
### 不可以执行的操作
|
||||
|
||||
- 调用 WASM 模块未导出的函数。
|
||||
- 查看 WASM 内部内存或调用栈。
|
||||
- 读取 WASM 源码。
|
||||
- 绕过 WASM 完整性校验。
|
||||
|
||||
### WASM 实例化 Hook
|
||||
|
||||
用于观察 WASM 模块导出的函数:
|
||||
|
||||
~~~js
|
||||
var origInstantiate = WebAssembly.instantiate;
|
||||
WebAssembly.instantiate = function(bytes, imports) {
|
||||
console.log('[JSRA] WebAssembly.instantiate called, bytes length:', bytes ? bytes.byteLength || bytes.length : 'unknown');
|
||||
return origInstantiate(bytes, imports).then(function(result) {
|
||||
var module = result.module || result;
|
||||
if (module instanceof WebAssembly.Module) {
|
||||
window.__JSRA_WASM_EXPORTS = WebAssembly.Module.exports(module);
|
||||
console.log('[JSRA] WASM exports:', window.__JSRA_WASM_EXPORTS);
|
||||
}
|
||||
if (result.instance) {
|
||||
window.__JSRA_WASM_INSTANCE = result.instance;
|
||||
var exportNames = Object.keys(result.instance.exports);
|
||||
console.log('[JSRA] WASM instance exports:', exportNames);
|
||||
for (var i = 0; i < exportNames.length; i++) {
|
||||
var name = exportNames[i];
|
||||
if (typeof result.instance.exports[name] === 'function') {
|
||||
console.log('[JSRA] WASM export function:', name, 'arity:', result.instance.exports[name].length);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
};
|
||||
~~~
|
||||
|
||||
### 调用 WASM 导出函数
|
||||
|
||||
- 如果模块导出函数且页面通过 JavaScript 包装器暴露,例如 window.wasmEncrypt(data),可以将其作为 JSRPC 入口。
|
||||
- 入口策略使用 wasm_export。
|
||||
- WASM 通常使用 Number、BigInt 或内存指针;JavaScript 包装器负责字符串/数组与内存之间的转换。
|
||||
|
||||
### 证据要求
|
||||
|
||||
确认 WASM 函数是入口至少需要:
|
||||
|
||||
1. 网络证据:请求体包含 WASM 函数产生的结果。
|
||||
2. 运行时证据:观察到 JavaScript 包装器以目标参数调用,并且返回值与请求字段一致。
|
||||
|
||||
仅观察到 WASM 加载、没有确认导出函数参与目标请求时,标记为 manual_observed_only。
|
||||
|
||||
### 限制
|
||||
|
||||
- WASM 内部逻辑未导出时,不能从 JavaScript 上下文调用,标记 strategy=unsupported,并填写 unsupported_reason。
|
||||
- chrome-devtools-mcp 不能直接读取 WASM 内存。
|
||||
- 不在范围内反编译 WASM 字节码。
|
||||
- WASM 混淆时仍可使用导出函数接口,但不能仅凭接口理解内部逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 4. Webpack 模块发现
|
||||
|
||||
### 概述
|
||||
|
||||
现代 Web 应用通常使用 Webpack 打包。目标入口可能位于 Webpack 模块闭包中,无法通过 window.* 直接访问。模块探针会发现模块系统,并尝试捕获 __webpack_require__,从而访问模块导出。
|
||||
|
||||
### 探针安装
|
||||
|
||||
生成:
|
||||
|
||||
~~~bash
|
||||
python3 scripts/emit_module_probe.py --output generated/module_probe.js
|
||||
~~~
|
||||
|
||||
注入:
|
||||
|
||||
- 通过 evaluate_script 注入。
|
||||
- 安装前检查 window.__JSRA_MODULES__,保证重复注入不会重复安装。
|
||||
|
||||
### 探针检测内容
|
||||
|
||||
#### 分块数组
|
||||
|
||||
探针会在 window 上检测 Webpack 分块数组:
|
||||
|
||||
- Webpack 3/4:window.webpackJsonp,格式为 [chunkIds, modules, executeModules]。
|
||||
- Webpack 5:符合 window.webpackChunk* 的数组,格式为 [[chunkIds], modules, runtime]。
|
||||
- Loadable 组件:window.__LOADABLE_LOADED_CHUNKS__。
|
||||
|
||||
发现分块数组后,设置 module_runtime.detected = true,并将类型记录为 webpack4、webpack5 或 unknown。
|
||||
|
||||
#### 捕获 __webpack_require__
|
||||
|
||||
按以下顺序尝试三种策略:
|
||||
|
||||
**策略一:Hook webpackJsonp.push**
|
||||
|
||||
- 覆盖分块数组上的 Array.prototype.push。
|
||||
- 新分块进入时,从模块工厂创建可加载模块的伪 require 函数。
|
||||
- 将捕获到的函数保存为 window.__JSRA_require。
|
||||
|
||||
**策略二:检查已有暴露对象**
|
||||
|
||||
- 检查 window.__webpack_require__、window.__webpack_modules__ 和 window.webpackJsonp 中是否已有可用函数。
|
||||
|
||||
**策略三:扫描 window 中的 require 类函数**
|
||||
|
||||
- 遍历 window 属性,寻找带有 .c 模块缓存属性的函数。
|
||||
|
||||
#### 模块缓存枚举
|
||||
|
||||
捕获 __webpack_require__ 后,枚举 require.c 或 require.cache:
|
||||
|
||||
~~~js
|
||||
var cache = require.c || require.cache || {};
|
||||
var keys = Object.keys(cache);
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var mod = cache[keys[i]];
|
||||
if (mod && mod.exports) {
|
||||
// 检查 mod.exports 中的函数
|
||||
}
|
||||
}
|
||||
~~~
|
||||
|
||||
- 最多列出 500 个模块 ID。
|
||||
- 导出值是函数时,记录 moduleId、exportName、type、前 200 个字符的 srcSnippet 和参数个数 arity。
|
||||
- 导出值是带函数属性的对象时,记录形如 exportName.subKey 的子导出。
|
||||
|
||||
结果保存到 window.__JSRA_MODULES__.candidateExports。
|
||||
|
||||
### 证据读取
|
||||
|
||||
读取模块发现结果:
|
||||
|
||||
~~~js
|
||||
evaluate_script("window.__JSRA_MODULES__.dump()")
|
||||
~~~
|
||||
|
||||
结果包含:detected、type、requireAvailable、requirePath、moduleCacheKeys(最多 200 个)、candidateExports、chunkArrays、globalExports(最多 100 个)和 errors。
|
||||
|
||||
### 与运行时 Hook 证据交叉验证
|
||||
|
||||
必须把模块探针结果与运行时 Hook 证据交叉比对:
|
||||
|
||||
1. 从运行时追踪的 requests[].stack 中提取函数名或源码文件引用。
|
||||
2. 在 candidateExports 中查找匹配的 exportName 或 srcSnippet 特征。
|
||||
3. 只有同时出现在 Hook 调用栈和模块导出中的候选,才能标记为 confidence=high。
|
||||
4. 仅出现在模块导出中、没有运行时 Hook 证据的候选,标记为 confidence=low。
|
||||
|
||||
交叉验证规则:候选导出必须与运行时 Hook 调用栈中观察到的函数匹配;不匹配时标记为 manual_observed_only。
|
||||
|
||||
### Webpack 导出的 JSRPC 生成
|
||||
|
||||
- 入口类型:webpack_export。
|
||||
- entrypoint 必须包含 module_id 和 export_path,分别表示模块 ID 和导出名称路径,例如 encrypt 或 default.encrypt。
|
||||
- JSRPC 使用 window.__JSRA_require(module_id) 获取模块,再通过 getByPath(module, export_path) 调用导出。
|
||||
|
||||
### 限制
|
||||
|
||||
- 如果应用不使用 webpackJsonp 或分块数组不可访问,捕获可能失败,标记 require_available: false。
|
||||
- Webpack 动态 import() 产生的分块可能不在已捕获缓存中;探针可以观察请求,但不能稳定获取导出。
|
||||
- 模块 ID 可能随构建变化;JSRPC 应使用运行时捕获的 require,不要硬编码模块 ID。
|
||||
- 启用热更新时模块缓存可能失效,应关闭热更新,或在热更新运行前捕获 require。
|
||||
|
||||
---
|
||||
|
||||
## 5. Vite、Rollup 与 ESM
|
||||
|
||||
### 识别
|
||||
|
||||
- 文档中出现 <script type="module">。
|
||||
- Vite 特征:import.meta.hot、window.__vite_plugin_meta__。
|
||||
- Rollup 特征:window.__rollup_plugin__。
|
||||
|
||||
识别后,将 module_runtime.type 设置为 vite 或 rollup。
|
||||
|
||||
### 全局导出发现
|
||||
|
||||
即使使用 Vite/Rollup,部分库仍会暴露全局对象。模块探针的 detectGlobalExports() 会扫描 window:
|
||||
|
||||
- 带有密码学相关名称的函数:CryptoJS、JSEncrypt、md5、sha256、sha1、aes、Base64、encrypt、decrypt、sign、verify、hash、hmac。
|
||||
- 函数属性名称符合密码学特征的对象。
|
||||
|
||||
结果保存在 window.__JSRA_MODULES__.globalExports。
|
||||
|
||||
### 限制
|
||||
|
||||
- ES 模块使用原生 import/export,不存在全局 require。
|
||||
- 浏览器模块加载器属于内部实现,没有可直接 Hook 的导入解析入口,chrome-devtools-mcp 不能观察或修改原生模块解析。
|
||||
- 动态 import() 返回 Promise,探针可以观察模块网络请求,但只有模块同时暴露到全局时才能捕获导出。
|
||||
- Vite 的导入映射只提供 DOM 层面的映射信息,不能访问模块内部运行时对象。
|
||||
- 如果加密逻辑运行在服务端,例如 Next.js SSR,则无法从浏览器上下文获取。
|
||||
- 只能观察全局对象、模块脚本请求和 DOM 元素;不能可靠地动态调用 import() 并捕获导出。
|
||||
|
||||
### Vite/Rollup 页面处理策略
|
||||
|
||||
1. 在 globalExports 中查找暴露的密码学库。
|
||||
2. 找到后使用 global_path 策略。
|
||||
3. 未找到时,检查目标参数是否流经运行时 Hook 捕获的 fetch/XHR,再根据源码位置从请求体回溯。
|
||||
4. 入口位于没有全局导出的 ES 模块闭包时,标记 manual_observed_only,记录观察结果供人工复现。
|
||||
|
||||
---
|
||||
|
||||
## 6. 闭包作用域函数
|
||||
|
||||
### 可以执行的操作
|
||||
|
||||
- 函数通过模块系统导出:使用 Webpack 捕获。
|
||||
- 函数流经 fetch/XHR/crypto Hook:通过调用栈回溯。
|
||||
- 函数位于 window:直接查找路径。
|
||||
|
||||
### 不可以执行的操作
|
||||
|
||||
- 调用未导出的闭包函数。
|
||||
- 从外部重建词法作用域。
|
||||
- 访问闭包捕获的变量。
|
||||
|
||||
### 可接受的替代方案
|
||||
|
||||
1. **Hook 边界**:闭包调用 fetch、XMLHttpRequest.send 或 JSON.stringify 时,Hook 外层边界截获数据。
|
||||
2. **Hook 密码学 API**:闭包使用 crypto.subtle、CryptoJS 或 JSEncrypt 时,Hook 库获取输入和输出。
|
||||
3. **解析器策略**:闭包通过动态解析器暴露目标函数时,捕获解析器。
|
||||
4. **Webpack 模块访问**:闭包属于 Webpack 模块时,使用模块探针捕获 __webpack_require__ 并访问导出。
|
||||
|
||||
### 何时标记为不支持
|
||||
|
||||
以上方案都无法访问函数时,标记为:
|
||||
|
||||
~~~json
|
||||
{
|
||||
"entrypoint_discovery": {
|
||||
"strategy": "unsupported",
|
||||
"confidence": "high",
|
||||
"unsupported_reason": "入口位于没有导出引用、没有可 Hook 边界且无法访问模块系统的闭包中。"
|
||||
}
|
||||
}
|
||||
~~~
|
||||
|
||||
### 禁止事项
|
||||
|
||||
- 不得声称未导出的闭包函数可调用。
|
||||
- 不得为运行时无法解析的函数生成 JSRPC 动作。
|
||||
- 不能仅凭静态源码分析猜测闭包内部结构。
|
||||
|
||||
---
|
||||
|
||||
## 7. 策略选择参考
|
||||
|
||||
| 场景 | 策略 | 是否生成 JSRPC 动作 |
|
||||
|---|---|---|
|
||||
| 函数位于 window.xxx.yyy | global_path | 是 |
|
||||
| 通过软 Hook 调用栈发现函数 | runtime_hook | 是 |
|
||||
| 函数位于 Webpack 模块缓存 | webpack_export | 是(需要已捕获的 require) |
|
||||
| 异步函数(crypto.subtle、Promise) | async_crypto | 是(必须处理 Promise) |
|
||||
| 函数由 WASM 模块导出 | wasm_export | 是 |
|
||||
| 有证据但尚未确认入口 | manual_observed_only | 否(输出证据供人工使用) |
|
||||
| 函数不可访问(未导出闭包、WASM 内部函数、Service Worker 内部闭包) | unsupported | 否(输出 unsupported_reason) |
|
||||
|
||||
---
|
||||
|
||||
## 8. 复杂场景的置信度要求
|
||||
|
||||
| 场景 | high 置信度的最低要求 |
|
||||
|---|---|
|
||||
| async_crypto | 网络证据(请求体)+ 匹配算法和调用栈的 crypto[] 事件 |
|
||||
| webpack_export | 网络证据 + candidateExports 匹配 + 运行时 Hook 调用栈交叉验证 |
|
||||
| wasm_export | 网络证据 + 观察到 JavaScript 包装器调用且输入输出匹配 |
|
||||
| global_path | 网络证据 + 通过 evaluate_script 观察到函数调用 |
|
||||
| runtime_hook | 网络证据 + Hook 捕获的、经过业务函数的调用栈 |
|
||||
@@ -0,0 +1,73 @@
|
||||
# 对抗运行时能力
|
||||
|
||||
本模块服务于已授权目标的动态分析,目标是增加可观察证据和入口存活能力,不承诺绕过验证码、WebAuthn、设备证明或跨域隔离。
|
||||
|
||||
## 能力顺序
|
||||
|
||||
1. `observe`:记录反调试、反 Hook、环境属性、动态代码、加载器和 Realm 信号。
|
||||
2. `intervene`:只针对已观察到的动态 `debugger` 构造做最小干预。
|
||||
3. 源码 tap:对明确选择的简单属性读取做保守插桩,先验证行为再扩大范围。
|
||||
4. 差分:比较原始与干预结果;事件增加不是业务成功,必须继续核对目标请求和业务状态。
|
||||
|
||||
## 多探针组合
|
||||
|
||||
对抗探针和运行时加密探针可以同时注入。生成脚本会共享
|
||||
`window.__JSRA_HOOK_REGISTRY__`,对同一 `fetch`、XHR 或 WebCrypto 方法按层组合,避免
|
||||
后注入的探针覆盖前一个探针。外部代码重新赋值时,持久探针会记录
|
||||
`patch.reconciled` 并重建组合链;`patch.reconcile_error` 才表示真正的重建失败。
|
||||
|
||||
运行时探针的 `captureRaw` 使用有界、循环引用安全的快照。需要稳定落盘时优先使用默认
|
||||
预览模式;开启原始捕获也必须通过 `dump()` 或 JSON Schema 验证后再进入差分流程。
|
||||
|
||||
## 生成与读取
|
||||
|
||||
```bash
|
||||
python3 scripts/emit_adversarial_runtime_probe.py \
|
||||
--output generated/adversarial_runtime_probe.js \
|
||||
--mode observe --properties "navigator.webdriver,navigator.userAgent,document.cookie"
|
||||
```
|
||||
|
||||
完整性方法默认采用首段全量、后续采样和每方法预算,避免 `Object.getPrototypeOf`
|
||||
等高频 API 挤掉真正的加密/网络证据。可按目标特征调整:
|
||||
|
||||
```bash
|
||||
python3 scripts/emit_adversarial_runtime_probe.py \
|
||||
--output generated/adversarial_runtime_probe.js \
|
||||
--integrity-event-budget 256 \
|
||||
--integrity-sample-every 64 \
|
||||
--integrity-burst 16
|
||||
```
|
||||
|
||||
被采样抑制的调用会记录在 `state.suppressed` 和
|
||||
`state.suppressedByPath`,不计入 `dropped`;`dropped > 0` 仍表示事件环发生了真正丢弃。
|
||||
|
||||
页面中:
|
||||
|
||||
```js
|
||||
window.__JSRA_ADVERSARIAL__.export()
|
||||
window.__JSRA_ADVERSARIAL__.clear()
|
||||
window.__JSRA_ADVERSARIAL__.uninstall()
|
||||
```
|
||||
|
||||
## 证据类型
|
||||
|
||||
| 类型 | 用途 |
|
||||
|---|---|
|
||||
| `integrity.*` | 函数、原型或属性描述符完整性检查 |
|
||||
| `dynamic.*` | eval/Function 动态代码 |
|
||||
| `environment.*` | navigator、screen、document 等属性访问 |
|
||||
| `realm.*` | iframe、Worker、SharedWorker 创建 |
|
||||
| `loader.*` | script、WASM 等动态加载 |
|
||||
| `patch.*` | 补丁安装、丢失、重挂载和恢复 |
|
||||
| `intervention.*` | 发生过的明确干预 |
|
||||
|
||||
## 源码级保守插桩
|
||||
|
||||
```bash
|
||||
node scripts/source_instrumentor.js \
|
||||
--input bundle.js --output generated/bundle.tap.js \
|
||||
--objects navigator,document,screen \
|
||||
--properties webdriver,userAgent,cookie
|
||||
```
|
||||
|
||||
默认只改写简单的 member read,并跳过字符串、注释、赋值和方法调用。它不是通用 AST 等价变换;插桩后必须与原始脚本做异常、请求和关键输出对照。
|
||||
@@ -0,0 +1,687 @@
|
||||
# 反调试与环境对抗模式
|
||||
|
||||
本文汇总六类反调试规则中的反调试、反检测和环境校验模式。每个章节保留检测特征、补丁代码、证据要求和风险提示。
|
||||
|
||||
**基本原则:**先验证,再修改;使用最小补丁;记录影响范围。关于 chrome-devtools-mcp 的能力边界,参阅 `capability-boundaries.md`。
|
||||
|
||||
**验证顺序:**
|
||||
1. 不加补丁,先记录原始现象。
|
||||
2. 判断问题类型:`debugger`/动态代码构造、控制台清理或日志抑制、计时或 Promise 计时、视口/ webdriver / UA / DevTools 检测、导航/关闭/历史记录干扰。
|
||||
3. 每次只选择一条最小规则进行验证。
|
||||
4. 比较前后差异:请求是否恢复、Hook 是否开始产生日志、调用栈是否可见、页面是否产生新的异常。
|
||||
|
||||
**验收标准:**
|
||||
- 补丁后产生可用于后续分析的新证据。
|
||||
- 能解释补丁的影响范围和残余风险。
|
||||
- 补丁不改变最终生成的 JSRPC / Flask / Burp 代码结构。
|
||||
|
||||
**禁止行为:**
|
||||
- 未验证就同时启用多条反调试规则。
|
||||
- 大范围伪造环境,只为了提高“看起来能运行”的概率。
|
||||
- 将仅用于调试的补丁写入最终生成产物的依赖。
|
||||
|
||||
---
|
||||
|
||||
## 1. Debugger 循环
|
||||
|
||||
**来源:** `debugger-loop.md`
|
||||
|
||||
### 检测特征
|
||||
- 目标请求发送前 DevTools 持续暂停。
|
||||
- `eval`、`Function` 或 `constructor` 收到包含 `debugger` 的源码字符串。
|
||||
- 同一调用栈反复出现,始终到不了参数变化位置。
|
||||
|
||||
### 需要记录
|
||||
- 触发问题的 API(`eval`、`Function` 或 `constructor`)。
|
||||
- 源码提示或调用栈帧。
|
||||
- 是否需要伪装 `toString`。
|
||||
|
||||
### 补丁:绕过动态 `debugger`
|
||||
|
||||
适用于 `eval`、`new Function`、`Function.prototype.constructor` 以及持续触发 `debugger` 的动态组装代码。
|
||||
|
||||
```js
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
const tempEval = eval;
|
||||
const tempToString = Function.prototype.toString;
|
||||
|
||||
Function.prototype.toString = function () {
|
||||
if (this === eval) {
|
||||
return 'function eval() { [native code] }';
|
||||
} else if (this === Function) {
|
||||
return 'function Function() { [native code] }';
|
||||
} else if (this === Function.prototype.toString) {
|
||||
return 'function toString() { [native code] }';
|
||||
} else if (this === Function.prototype.constructor) {
|
||||
return 'function Function() { [native code] }';
|
||||
}
|
||||
return tempToString.apply(this, arguments);
|
||||
};
|
||||
|
||||
window.eval = function () {
|
||||
if (typeof arguments[0] === 'string') {
|
||||
arguments[0] = arguments[0].replaceAll(/debugger/g, '');
|
||||
}
|
||||
return tempEval(...arguments);
|
||||
};
|
||||
|
||||
const OriginalFunction = Function;
|
||||
Function = function () {
|
||||
for (let i = 0; i < arguments.length; i++) {
|
||||
if (typeof arguments[i] === 'string') {
|
||||
arguments[i] = arguments[i].replaceAll(/debugger/g, '');
|
||||
}
|
||||
}
|
||||
return OriginalFunction(...arguments);
|
||||
};
|
||||
|
||||
Function.prototype = OriginalFunction.prototype;
|
||||
|
||||
Function.prototype.constructor = function () {
|
||||
for (let i = 0; i < arguments.length; i++) {
|
||||
if (typeof arguments[i] === 'string') {
|
||||
arguments[i] = arguments[i].replaceAll(/debugger/g, '');
|
||||
}
|
||||
}
|
||||
return OriginalFunction(...arguments);
|
||||
};
|
||||
|
||||
Function.prototype.constructor.prototype = Function.prototype;
|
||||
})();
|
||||
```
|
||||
|
||||
### 补丁后验证
|
||||
- 页面不再因 `debugger` 反复中断。
|
||||
- `Function.prototype.toString` 相关检查不产生新的异常。
|
||||
- 主要业务流程仍可正常执行。
|
||||
|
||||
### 风险
|
||||
- 该规则会改变全局执行行为,可能触发完整性检查。应尽量缩小影响范围并记录影响面。
|
||||
|
||||
---
|
||||
|
||||
## 2. 控制台检测
|
||||
|
||||
**来源:** `console-detect.md`
|
||||
|
||||
### 检测特征
|
||||
- `console.log`、`console.table` 或 `console.clear` 被覆盖。
|
||||
- 触发请求流程后控制台输出消失。
|
||||
- 计时检测依赖控制台渲染副作用。
|
||||
|
||||
### 需要记录
|
||||
- 受影响的控制台方法。
|
||||
- 覆盖操作的源码提示。
|
||||
- 恢复控制台后页面行为是否改变。
|
||||
|
||||
### 补丁一:保护 `console.log` / `trace` / `groupCollapsed` / `groupEnd`(Proxy 方法)
|
||||
|
||||
```js
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
const readonlyProps = ['log', 'trace', 'groupCollapsed', 'groupEnd'];
|
||||
const readonlyConsole = new Proxy(console, {
|
||||
set(t, k, v, r) {
|
||||
if (readonlyProps.includes(k)) {
|
||||
console.groupCollapsed(`%cBlocked overwrite: console.${String(k)}`, 'color: #ff6348;', v);
|
||||
console.trace();
|
||||
console.groupEnd();
|
||||
return true;
|
||||
}
|
||||
return Reflect.set(t, k, v, r);
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(window, 'console', {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
get() {
|
||||
return readonlyConsole;
|
||||
},
|
||||
set(v) {
|
||||
console.groupCollapsed('%cBlocked overwrite: window.console', 'color: #ff6348;', v);
|
||||
console.trace();
|
||||
console.groupEnd();
|
||||
}
|
||||
});
|
||||
})();
|
||||
```
|
||||
|
||||
### 补丁二:阻止 `console.clear()`
|
||||
|
||||
```js
|
||||
(() => {
|
||||
'use strict';
|
||||
console.clear = function () {};
|
||||
})();
|
||||
```
|
||||
|
||||
### 补丁三:阻止 `console.table()`(用于计时或 Getter 诱导检测)
|
||||
|
||||
```js
|
||||
(() => {
|
||||
'use strict';
|
||||
console.table = function () {};
|
||||
})();
|
||||
```
|
||||
|
||||
### 补丁后验证
|
||||
- 仍可使用 `console.log` 等方法观察运行时数据。
|
||||
- 控制台输出不再被清空或干扰。
|
||||
- 页面不会因控制台完整性检查产生新的异常。
|
||||
|
||||
### 风险
|
||||
- 与执行流 Hook 相比,控制台补丁风险较低,但在加固构建中仍可能触发完整性检查。
|
||||
|
||||
---
|
||||
|
||||
## 3. 计时检测
|
||||
|
||||
**来源:** `timer-check.md`
|
||||
|
||||
### 检测特征
|
||||
- 只有单步调试时流程才中断。
|
||||
- 调试状态下 Promise 回调或计时器处理器进入不同分支。
|
||||
- `performance.now`、`Date.now` 或时间间隔差值决定是否发送请求。
|
||||
|
||||
### 需要记录
|
||||
- 使用的计时原语。
|
||||
- 观测到的阈值。
|
||||
- 绕过后解锁的分支或回调。
|
||||
|
||||
### 补丁:Hook Promise Resolve
|
||||
|
||||
适用于快速定位异步回调入口,或确认哪个 Promise resolve 产生了关键参数。
|
||||
|
||||
```js
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
const OriginalPromise = Promise;
|
||||
|
||||
Promise = function (callback) {
|
||||
if (!callback) {
|
||||
return new OriginalPromise(callback);
|
||||
}
|
||||
const originCallback = callback;
|
||||
callback = function (resolve, reject) {
|
||||
const originResolve = resolve;
|
||||
resolve = function (result) {
|
||||
if (result && !(result instanceof Promise)) {
|
||||
try {
|
||||
console.groupCollapsed('[Promise resolve]');
|
||||
console.log(result);
|
||||
console.trace();
|
||||
console.groupEnd();
|
||||
} catch (e) {}
|
||||
}
|
||||
return originResolve.apply(this, arguments);
|
||||
};
|
||||
return originCallback(resolve, reject);
|
||||
};
|
||||
return new OriginalPromise(callback);
|
||||
};
|
||||
|
||||
Promise.prototype = OriginalPromise.prototype;
|
||||
Object.defineProperties(Promise, Object.getOwnPropertyDescriptors(OriginalPromise));
|
||||
})();
|
||||
```
|
||||
|
||||
### 补丁后验证
|
||||
- Promise 链仍可正常执行。
|
||||
- 可以看到 resolve 参数和调用栈。
|
||||
- 页面没有明显性能下降。
|
||||
|
||||
### 风险
|
||||
- 时间归一化可能掩盖真实竞态,只应在跟踪期间使用,不得默认静默启用。
|
||||
|
||||
---
|
||||
|
||||
## 4. 环境检测
|
||||
|
||||
**来源:** `env-detect.md`
|
||||
|
||||
### 检测特征
|
||||
- 分支逻辑依赖视口尺寸、DevTools 打开状态、webdriver 标志、UA 或扩展状态。
|
||||
- 同一页面在不同浏览器配置下表现不一致。
|
||||
- 打开 DevTools 后请求链消失。
|
||||
|
||||
### 需要记录
|
||||
- 被检查的属性。
|
||||
- 原始值。
|
||||
- 替换值。
|
||||
- 受影响的源码提示。
|
||||
|
||||
### 补丁:修正窗口尺寸
|
||||
|
||||
适用于网站使用 `innerHeight` / `innerWidth` 或 `outerHeight` / `outerWidth` 判断 DevTools 是否打开的场景。
|
||||
|
||||
```js
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
const innerHeightValue = 660;
|
||||
const innerWidthValue = 1366;
|
||||
const outerHeightValue = 760;
|
||||
const outerWidthValue = 1400;
|
||||
|
||||
const innerHeightDesc = Object.getOwnPropertyDescriptor(window, 'innerHeight');
|
||||
const innerWidthDesc = Object.getOwnPropertyDescriptor(window, 'innerWidth');
|
||||
const outerHeightDesc = Object.getOwnPropertyDescriptor(window, 'outerHeight');
|
||||
const outerWidthDesc = Object.getOwnPropertyDescriptor(window, 'outerWidth');
|
||||
|
||||
Object.defineProperty(window, 'innerHeight', {
|
||||
get() { return innerHeightValue; },
|
||||
set() { return innerHeightDesc.set.call(window, innerHeightValue); }
|
||||
});
|
||||
|
||||
Object.defineProperty(window, 'innerWidth', {
|
||||
get() { return innerWidthValue; },
|
||||
set() { return innerWidthDesc.set.call(window, innerWidthValue); }
|
||||
});
|
||||
|
||||
Object.defineProperty(window, 'outerHeight', {
|
||||
get() { return outerHeightValue; },
|
||||
set() { return outerHeightDesc.set.call(window, outerHeightValue); }
|
||||
});
|
||||
|
||||
Object.defineProperty(window, 'outerWidth', {
|
||||
get() { return outerWidthValue; },
|
||||
set() { return outerWidthDesc.set.call(window, outerWidthValue); }
|
||||
});
|
||||
})();
|
||||
```
|
||||
|
||||
### 补丁后验证
|
||||
- 打开 DevTools 后不再触发尺寸检测。
|
||||
- 页面布局没有不可接受的变形。
|
||||
|
||||
### 风险
|
||||
- 环境伪造可能影响对正式环境行为的判断,只能用于调查,并且必须明确记录。
|
||||
|
||||
---
|
||||
|
||||
## 5. 代理与导航保护
|
||||
|
||||
**来源:** `proxy-guard.md`
|
||||
|
||||
### 检测特征
|
||||
- `window.close`、`history.back`、重定向 Hook 或卸载处理器中断跟踪流程。
|
||||
- 只有配置代理后请求才失败。
|
||||
- 页面导航后扩展或注入脚本被阻止。
|
||||
|
||||
### 需要记录
|
||||
- 保护类型(`close`、`history`、`redirect`、`proxy`、`extension`)。
|
||||
- 源码提示。
|
||||
- 处理后请求重放是否成功。
|
||||
|
||||
### 补丁一:阻止 `window.close`
|
||||
|
||||
```js
|
||||
(() => {
|
||||
'use strict';
|
||||
window.close = function () {};
|
||||
})();
|
||||
```
|
||||
|
||||
### 补丁二:阻止 `history.go` / `history.back`
|
||||
|
||||
```js
|
||||
(() => {
|
||||
'use strict';
|
||||
window.history.go = function () {};
|
||||
window.history.back = function () {};
|
||||
})();
|
||||
```
|
||||
|
||||
### 补丁三:导航前断点(用于定位源码)
|
||||
|
||||
适用于页面即将导航,需要定位导航发生瞬间源码的场景。
|
||||
|
||||
```js
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
window.onbeforeunload = () => {
|
||||
debugger;
|
||||
return false;
|
||||
};
|
||||
})();
|
||||
```
|
||||
|
||||
### 补丁后验证
|
||||
- 页面不再被强制关闭或返回上一页。
|
||||
- 导航前可以稳定命中断点。
|
||||
- 调试结束后及时移除这些补丁。
|
||||
|
||||
### 风险
|
||||
- 导航保护补丁可能改变页面状态。条件允许时,应移除补丁后重新验证请求链。
|
||||
|
||||
---
|
||||
|
||||
## 6. 动态别名(密码库 Hook)
|
||||
|
||||
**来源:** `dynamic-alias.md`
|
||||
|
||||
### 检测特征
|
||||
- 请求参数发生变化,但看不到稳定的全局路径。
|
||||
- 多层包装函数逐层转发到真正的加密或签名函数。
|
||||
- 缺少 source map,且对象路径每次刷新都会变化。
|
||||
|
||||
### 策略
|
||||
- 稳定性较低时,优先使用解析器策略,不要硬编码对象路径。
|
||||
- 记录包装链,以及解析到可调用函数所需的最小运行时前置条件。
|
||||
- 记录生成的 JSRPC 使用的是解析器还是静态路径。
|
||||
|
||||
### 需要记录
|
||||
- 包装链。
|
||||
- 解析器触发条件。
|
||||
- 解析前需要满足的运行时依赖。
|
||||
|
||||
### 补丁一:Hook CryptoJS
|
||||
|
||||
适用于目标网站使用 CryptoJS,需要快速定位 AES / DES / MD5 / SHA / HMAC 参数来源的场景。
|
||||
|
||||
```js
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
let time = 0;
|
||||
|
||||
function hasEncryptProp(obj) {
|
||||
const requiredProps = [
|
||||
'ciphertext',
|
||||
'key',
|
||||
'iv',
|
||||
'algorithm',
|
||||
'mode',
|
||||
'padding',
|
||||
'blockSize',
|
||||
'formatter'
|
||||
];
|
||||
if (!obj || typeof obj !== 'object') return false;
|
||||
for (const prop of requiredProps) {
|
||||
if (!(prop in obj)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function hasDecryptProp(obj) {
|
||||
const requiredProps = ['sigBytes', 'words'];
|
||||
if (!obj || typeof obj !== 'object') return false;
|
||||
for (const prop of requiredProps) {
|
||||
if (!(prop in obj)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function getSigBytes(size) {
|
||||
switch (size) {
|
||||
case 8: return '64bits';
|
||||
case 16: return '128bits';
|
||||
case 24: return '192bits';
|
||||
case 32: return '256bits';
|
||||
default: return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
const tempApply = Function.prototype.apply;
|
||||
Function.prototype.apply = function () {
|
||||
// === 对称加密检测 ===
|
||||
if (
|
||||
arguments.length === 2 &&
|
||||
arguments[0] &&
|
||||
arguments[1] &&
|
||||
typeof arguments[1] === 'object' &&
|
||||
arguments[1].length === 1 &&
|
||||
hasEncryptProp(arguments[1][0])
|
||||
) {
|
||||
if (Object.hasOwn(arguments[0], '$super') && Object.hasOwn(arguments[1], 'callee')) {
|
||||
if (
|
||||
this.toString().indexOf('function()') !== -1 ||
|
||||
/^\s*function(?:\s*\*)?\s+[A-Za-z_$][\w$]*\s*\([^)]*\)\s*\{/.test(this.toString()) ||
|
||||
/^\s*function\s*\(\s*\)\s*\{/.test(this.toString())
|
||||
) {
|
||||
console.log(...arguments);
|
||||
|
||||
const encryptText = arguments[0].$super.toString.call(arguments[1][0]);
|
||||
if (encryptText !== '[object Object]') {
|
||||
console.log('Symmetric ciphertext:', encryptText);
|
||||
} else {
|
||||
console.log('Symmetric ciphertext: toString unavailable, use the printed object above to call toString manually.');
|
||||
}
|
||||
|
||||
const key = arguments[1][0].key.toString();
|
||||
if (key !== '[object Object]') {
|
||||
console.log('Symmetric Hex key:', key);
|
||||
} else {
|
||||
console.log('Symmetric Hex key: toString unavailable, use the printed object above to call toString manually.');
|
||||
}
|
||||
|
||||
const iv = arguments[1][0].iv;
|
||||
if (iv) {
|
||||
if (iv.toString() !== '[object Object]') {
|
||||
console.log('Symmetric Hex iv:', iv.toString());
|
||||
} else {
|
||||
console.log('Symmetric Hex iv: toString unavailable, use the printed object above to call toString manually.');
|
||||
}
|
||||
} else {
|
||||
console.log('Symmetric encryption: no iv used');
|
||||
}
|
||||
|
||||
if (arguments[1][0].padding) {
|
||||
console.log('Padding mode:', arguments[1][0].padding);
|
||||
}
|
||||
if (arguments[1][0].mode && Object.hasOwn(arguments[1][0].mode, 'Encryptor')) {
|
||||
console.log('Block mode:', arguments[1][0].mode.Encryptor.processBlock);
|
||||
}
|
||||
if (arguments[1][0].key && Object.hasOwn(arguments[1][0].key, 'sigBytes')) {
|
||||
console.log('Key length:', getSigBytes(arguments[1][0].key.sigBytes));
|
||||
}
|
||||
console.log('%c---------------------------------------------------------------------', 'color: green;');
|
||||
} else {
|
||||
console.groupCollapsed('If the above correctly output the key/iv etc., ignore this message.');
|
||||
console.log(...arguments);
|
||||
console.log('Symmetric encryption: due to some necessary factors, key/iv etc. were not output. Use the printed object above to call toString manually.');
|
||||
console.log('%c---------------------------------------------------------------------', 'color: green;');
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
// === 对称解密检测 ===
|
||||
else if (
|
||||
arguments.length === 2 &&
|
||||
arguments[0] &&
|
||||
arguments[1] &&
|
||||
typeof arguments[1] === 'object' &&
|
||||
arguments[1].length === 3 &&
|
||||
hasDecryptProp(arguments[1][1])
|
||||
) {
|
||||
if (Object.hasOwn(arguments[0], '$super') && Object.hasOwn(arguments[1], 'callee')) {
|
||||
if (this.toString().indexOf('function()') === -1 && arguments[1][0] === 2) {
|
||||
console.log(...arguments);
|
||||
|
||||
const key = arguments[1][1].toString();
|
||||
if (key !== '[object Object]') {
|
||||
console.log('Symmetric decrypt Hex key:', key);
|
||||
} else {
|
||||
console.log('Symmetric decrypt Hex key: toString unavailable, use the printed object above to call toString manually.');
|
||||
}
|
||||
|
||||
if (Object.hasOwn(arguments[1][2], 'iv') && arguments[1][2].iv) {
|
||||
const iv2 = arguments[1][2].iv.toString();
|
||||
if (iv2 !== '[object Object]') {
|
||||
console.log('Symmetric decrypt Hex iv:', iv2);
|
||||
} else {
|
||||
console.log('Symmetric decrypt Hex iv: toString unavailable, use the printed object above to call toString manually.');
|
||||
}
|
||||
} else {
|
||||
console.log('Symmetric decryption: no iv used');
|
||||
}
|
||||
|
||||
if (Object.hasOwn(arguments[1][2], 'padding') && arguments[1][2].padding) {
|
||||
console.log('Decrypt padding mode:', arguments[1][2].padding);
|
||||
}
|
||||
if (Object.hasOwn(arguments[1][2], 'mode') && arguments[1][2].mode) {
|
||||
console.log('Decrypt block mode:', arguments[1][2].mode.Encryptor.processBlock);
|
||||
}
|
||||
if (time === 0) {
|
||||
console.log('如需继续确认算法类型,请结合当前页面的实际调用参数和输出长度进行定向验证。');
|
||||
time += 1;
|
||||
}
|
||||
console.log('%c---------------------------------------------------------------------', 'color: green;');
|
||||
}
|
||||
}
|
||||
}
|
||||
// === 哈希/HMAC 检测 ===
|
||||
else if (
|
||||
arguments.length === 2 &&
|
||||
arguments[0] &&
|
||||
arguments[1] &&
|
||||
typeof arguments[0] === 'object' &&
|
||||
typeof arguments[1] === 'object'
|
||||
) {
|
||||
if (
|
||||
arguments[0].__proto__ &&
|
||||
Object.hasOwn(arguments[0].__proto__, '$super') &&
|
||||
Object.hasOwn(arguments[0].__proto__, '_doFinalize') &&
|
||||
arguments[0].__proto__.__proto__ &&
|
||||
Object.hasOwn(arguments[0].__proto__.__proto__, 'finalize')
|
||||
) {
|
||||
if (arguments[0].__proto__.__proto__.finalize.toString().indexOf('Hash/HMAC') === -1) {
|
||||
const tempFinalize = arguments[0].__proto__.__proto__.finalize;
|
||||
arguments[0].__proto__.__proto__.finalize = function () {
|
||||
if (!Object.hasOwn(this, 'init')) {
|
||||
const hash = tempFinalize.call(this, ...arguments);
|
||||
console.log('Hash/HMAC raw data:', ...arguments);
|
||||
console.log('Hash/HMAC ciphertext:', hash.toString());
|
||||
console.log('Hash/HMAC ciphertext length:', hash.toString().length);
|
||||
console.log('Note: If HMAC, this script cannot hook the key. Search for it manually.');
|
||||
console.log('%c---------------------------------------------------------------------', 'color: green;');
|
||||
return hash;
|
||||
}
|
||||
return tempFinalize.call(this, ...arguments);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return tempApply.call(this, ...arguments);
|
||||
};
|
||||
})();
|
||||
```
|
||||
|
||||
### 补丁二:Hook JSEncrypt RSA
|
||||
|
||||
适用于目标网站使用 JSEncrypt,需要直接获取 RSA 公钥、私钥、明文和密文的场景。
|
||||
|
||||
```js
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
let u, c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
function f(t) {
|
||||
let e, i, r = '';
|
||||
for (e = 0; e + 3 <= t.length; e += 3) {
|
||||
i = parseInt(t.substring(e, e + 3), 16);
|
||||
r += c.charAt(i >> 6) + c.charAt(63 & i);
|
||||
}
|
||||
if (e + 1 == t.length) {
|
||||
i = parseInt(t.substring(e, e + 1), 16);
|
||||
r += c.charAt(i << 2);
|
||||
} else if (e + 2 == t.length) {
|
||||
i = parseInt(t.substring(e, e + 2), 16);
|
||||
r += c.charAt(i >> 2) + c.charAt((3 & i) << 4);
|
||||
}
|
||||
while ((3 & r.length) > 0) r += '=';
|
||||
return r;
|
||||
}
|
||||
|
||||
function hasRSAProp(obj) {
|
||||
const requiredProps = [
|
||||
'constructor',
|
||||
'getPrivateBaseKey',
|
||||
'getPrivateBaseKeyB64',
|
||||
'getPrivateKey',
|
||||
'getPublicBaseKey',
|
||||
'getPublicBaseKeyB64',
|
||||
'getPublicKey',
|
||||
'parseKey',
|
||||
'parsePropertiesFrom'
|
||||
];
|
||||
if (!obj || typeof obj !== 'object') return false;
|
||||
for (const prop of requiredProps) {
|
||||
if (!(prop in obj)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const tempCall = Function.prototype.call;
|
||||
Function.prototype.call = function () {
|
||||
if (
|
||||
arguments.length === 1 &&
|
||||
arguments[0] &&
|
||||
arguments[0].__proto__ &&
|
||||
typeof arguments[0].__proto__ === 'object' &&
|
||||
hasRSAProp(arguments[0].__proto__)
|
||||
) {
|
||||
if (
|
||||
'__proto__' in arguments[0].__proto__ &&
|
||||
arguments[0].__proto__.__proto__ &&
|
||||
Object.hasOwn(arguments[0].__proto__.__proto__, 'encrypt') &&
|
||||
Object.hasOwn(arguments[0].__proto__.__proto__, 'decrypt')
|
||||
) {
|
||||
if (arguments[0].__proto__.__proto__.encrypt.toString().indexOf('RSA encrypt') === -1) {
|
||||
const tempEncrypt = arguments[0].__proto__.__proto__.encrypt;
|
||||
arguments[0].__proto__.__proto__.encrypt = function () {
|
||||
const encryptText = tempEncrypt.bind(this, ...arguments)();
|
||||
console.log('RSA public key:\n', this.getPublicKey());
|
||||
console.log('RSA encrypt plaintext:', ...arguments);
|
||||
console.log('RSA encrypt Base64 ciphertext:', f(encryptText));
|
||||
console.log('%c---------------------------------------------------------------------', 'color: green;');
|
||||
return encryptText;
|
||||
};
|
||||
}
|
||||
|
||||
if (arguments[0].__proto__.__proto__.decrypt.toString().indexOf('RSA decrypt') === -1) {
|
||||
const tempDecrypt = arguments[0].__proto__.__proto__.decrypt;
|
||||
arguments[0].__proto__.__proto__.decrypt = function () {
|
||||
const decryptText = tempDecrypt.bind(this, ...arguments)();
|
||||
console.log('RSA private key:\n', this.getPrivateKey());
|
||||
console.log('RSA decrypt Base64 input:', f(...arguments));
|
||||
console.log('RSA decrypt plaintext:', decryptText);
|
||||
console.log('%c---------------------------------------------------------------------', 'color: green;');
|
||||
return decryptText;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return tempCall.bind(this, ...arguments)();
|
||||
};
|
||||
})();
|
||||
```
|
||||
|
||||
### 补丁后验证
|
||||
- 成功从密码学包装链输出关键参数。
|
||||
- 可以定位真实的 CryptoJS 或 JSEncrypt 调用位置。
|
||||
- 重写底层 `call`/`apply` 后页面功能没有损坏。
|
||||
|
||||
### 风险
|
||||
- 重写 `call`/`apply` 会影响全局作用域,可能触发加固构建的完整性检查。
|
||||
|
||||
---
|
||||
|
||||
## 7. 模式快速选择
|
||||
|
||||
| 模式 | 现象 | 文件 |
|
||||
|---|---|---|
|
||||
| Debugger 循环 | 反复出现 `debugger`,或 `eval`/`Function`/`constructor` 被篡改 | `debugger-loop.md` |
|
||||
| 控制台检测 | 控制台方法被覆盖、调用 `console.clear`/`console.table`、日志被抑制 | `console-detect.md` |
|
||||
| 计时检测 | 计时差值检查、Promise 计时、性能探针 | `timer-check.md` |
|
||||
| 环境检测 | 视口尺寸、DevTools、webdriver、UA 检查 | `env-detect.md` |
|
||||
| 代理与导航保护 | 导航、关闭、历史记录、重定向 Hook 阻止请求重放 | `proxy-guard.md` |
|
||||
| 动态别名 | 混淆别名、动态解析器、密码学包装、异步间接调用 | `dynamic-alias.md` |
|
||||
|
||||
只有在某条规则确实改变调查路径或风险面时,才引用并启用它。
|
||||
@@ -0,0 +1,26 @@
|
||||
# 架构
|
||||
|
||||
## 设计
|
||||
- chrome-devtools-mcp:页面交互、网络、Console
|
||||
- 本地 JSRA Engine:逆向分析
|
||||
- 可选 Burp MCP:流量重放
|
||||
|
||||
## 证据模型
|
||||
```json
|
||||
{
|
||||
"event_id": "evt-1",
|
||||
"trace_id": "trace-1",
|
||||
"parent_event_id": null,
|
||||
"type": "crypto.encrypt",
|
||||
"function": "JSEncrypt.encrypt",
|
||||
"input_fingerprint": "sha256:...",
|
||||
"output_fingerprint": "sha256:...",
|
||||
"stack": [],
|
||||
"timestamp": 0
|
||||
}
|
||||
```
|
||||
|
||||
## 置信度
|
||||
- `high`:真实运行时 + 差分验证
|
||||
- `medium`:多源证据一致
|
||||
- `low`:启发式匹配
|
||||
@@ -0,0 +1,55 @@
|
||||
# 浏览器会话与证据契约
|
||||
|
||||
真实浏览器分析不能用“端口已监听”“动作已注册”或“页面出现日志”代替成功。每次导航、注入和调用都要绑定到当前浏览器会话与当前文档,并在状态变化后重新确认。
|
||||
|
||||
## 操作前
|
||||
|
||||
1. 读取浏览器连接状态,确认连接的是用户指定的浏览器,而不是新建的临时页面。
|
||||
2. 列出页面并锁定 `tabId`、当前 URL 和当前文档身份;没有稳定文档身份时,至少记录页面 URL 与导航时间。
|
||||
3. 注入或修改前记录操作 ID、目标页面、脚本版本指纹和预期作用范围。
|
||||
4. 页面发生导航、刷新、跨文档 iframe 切换或调试器重新连接后,旧的元素引用、调用栈、断点和注入确认全部视为失效,必须重新快照或重新注入。
|
||||
|
||||
## 操作中
|
||||
|
||||
- 超时只表示“结果未知”,不表示操作没有执行。不要盲目重放可能有副作用的登录、签名或状态修改动作;先读取状态、事件和当前文档。
|
||||
- 调试器暂停时,调用、导航和注入的返回值可能只是“已排队”或“等待恢复”。必须恢复后再次读取状态,不能把旧的暂停信息当成新结果。
|
||||
- 桥接断开时,清理未完成请求;重新连接后重新列页面并重新绑定文档。只有幂等的只读操作可以自动重试,登录和加密动作默认不自动重放。
|
||||
- 事件、源代码和请求体采用有界读取;敏感字段默认只保留长度、哈希或脱敏预览,不能因为导出失败而放宽范围。
|
||||
|
||||
## 操作后
|
||||
|
||||
JSRPC action 级结果至少要同时出现:
|
||||
|
||||
- 输入明文或其明确的参数标识;
|
||||
- 非空的最终请求体;
|
||||
- 最终请求 route/URL;
|
||||
- HTTP 状态;
|
||||
- 最终业务响应。
|
||||
|
||||
如果浏览器控制面能提供会话信息,额外保存如下绑定:
|
||||
|
||||
```json
|
||||
{
|
||||
"session": {
|
||||
"connected": true,
|
||||
"tabId": 1,
|
||||
"documentId": "document-generation"
|
||||
},
|
||||
"operation": {
|
||||
"id": "operation-id",
|
||||
"status": "confirmed",
|
||||
"applied": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`status=timeout`、`status=pending`、`applied=false`、文档身份缺失或返回中包含错误标记时,只能报告为未确认。使用 `scripts/validate_browser_evidence.py` 生成 `artifacts/browser_evidence.json`,再把它作为最终报告的验证依据。
|
||||
|
||||
## 最小恢复顺序
|
||||
|
||||
```text
|
||||
读取桥接状态 → 列出页面 → 重新绑定 tab/document → 重新快照或注入
|
||||
→ 触发一次动作 → 等待最终网络请求 → 导出有界证据 → 校验交付契约
|
||||
```
|
||||
|
||||
不要用旧页面的元素引用、旧 WebSocket 请求 ID 或旧文档上的 Hook 结果替代这条恢复链。
|
||||
@@ -0,0 +1,44 @@
|
||||
# 能力边界
|
||||
|
||||
## 能做
|
||||
- 页面导航、evaluate_script、initScript 预注入
|
||||
- Hook fetch/XHR/crypto/WebSocket/库
|
||||
- Webpack 模块捕获
|
||||
- 证据驱动候选发现
|
||||
- 差分验证
|
||||
- JSRPC/Flask/Burp 产物生成
|
||||
|
||||
## 不能做
|
||||
- 闭包未导出函数
|
||||
- WASM 未导出内部函数
|
||||
- Service Worker 内部闭包
|
||||
- ES module import 拦截
|
||||
- VMP 静态还原
|
||||
- CSP WebSocket 绕过
|
||||
- 跨域 iframe
|
||||
- VM 保护 JS
|
||||
- multipart/form-data 的边界级字段改写;此类请求必须人工处理或生成专用适配器
|
||||
|
||||
## 模块内部加密
|
||||
当加密函数在模块内部时,按顺序尝试:
|
||||
1. initScript Hook `Object.defineProperty`
|
||||
2. initScript Hook `Function.prototype`
|
||||
3. 搜索 RSA 实例
|
||||
4. 静态 AST 分析
|
||||
5. 源码分析 + Python 实现
|
||||
|
||||
## 置信度
|
||||
- `high`:真实运行时调用 + 差分验证
|
||||
- `medium`:单一证据源
|
||||
- `low`:仅启发式匹配
|
||||
|
||||
## 策略值
|
||||
- `global_path`:函数在 window 上
|
||||
- `runtime_hook`:Hook 调用栈发现
|
||||
- `webpack_export`:Webpack 模块导出
|
||||
- `async_crypto`:异步加密
|
||||
- `initscript_hook`:initScript Hook
|
||||
- `static_ast`:静态 AST 分析
|
||||
- `source_analysis`:源码分析
|
||||
- `manual_observed_only`:有证据未确认
|
||||
- `unsupported`:所有方法失败
|
||||
@@ -0,0 +1,337 @@
|
||||
# 证据收集规则
|
||||
|
||||
本文说明 JS 逆向自动化中的证据收集方法:网络捕获、软 Hook、源码定位和运行时 Hook 跟踪,并规定证据强度、记录方式与风险控制。
|
||||
|
||||
---
|
||||
|
||||
## 1. 网络捕获
|
||||
|
||||
### 适用场景
|
||||
|
||||
- 确认目标参数位于 URL、请求头、Cookie、JSON 请求体还是表单请求体。
|
||||
- 页面动作触发多个相似请求,需要缩小到真正参与签名或加密的请求。
|
||||
- 用户提供了 Optional Fetch Example,需要映射到真实浏览器请求。
|
||||
|
||||
### 证据优先级
|
||||
|
||||
1. 真实页面动作触发的网络记录。
|
||||
2. 请求详情:方法、URL、请求头、请求体和响应。
|
||||
3. 页面内观察代码输出:调用栈、序列化对象和函数参数。
|
||||
4. 控制台日志与页面状态快照。
|
||||
|
||||
### 推荐流程
|
||||
|
||||
1. 打开目标页面并完成必要交互,确认请求确实发出。
|
||||
2. 使用 chrome-devtools-mcp 的网络请求列表筛选候选请求。
|
||||
3. 读取每个候选请求的详情,确认目标参数位置和请求体结构。
|
||||
4. 请求较多时,按以下条件缩小范围:
|
||||
- 是否出现目标参数。
|
||||
- 请求触发时间是否接近用户动作。
|
||||
- 响应状态是否符合业务预期。
|
||||
- 请求体是否同时包含未加密明文和加密结果等相邻字段。
|
||||
5. 锁定请求后立即记录:
|
||||
- 请求 URL。
|
||||
- HTTP 方法。
|
||||
- Content-Type。
|
||||
- 参数位置。
|
||||
- 关键请求头。
|
||||
- 触发动作。
|
||||
6. 请求详情不足以定位源码时,再增加最小观察代码或最小 Hook。
|
||||
|
||||
### 成功标准
|
||||
|
||||
- 至少一个请求被明确标记为目标请求。
|
||||
- 每个待分析参数的位置已知。
|
||||
- 已记录足以支持后续源码定位的请求上下文。
|
||||
|
||||
### 失败信号
|
||||
|
||||
- 只能看到静态资源请求,看不到业务 API。
|
||||
- 目标动作触发大量相似请求,无法区分签名参与者。
|
||||
- 能看到请求,但无法确认参数经过哪一层序列化后出现。
|
||||
|
||||
### 处理策略
|
||||
|
||||
- 多请求竞争时只改变一个输入值,观察哪个请求字段同步变化。
|
||||
- 序列化不透明时,对以下边界增加最小 Hook:
|
||||
- window.fetch
|
||||
- XMLHttpRequest.prototype.open
|
||||
- XMLHttpRequest.prototype.send
|
||||
- JSON.stringify
|
||||
- 页面会刷新时,通过 evaluate_script 注入;需要在刷新前生效时,使用 navigate_page(initScript=...) 预注入。
|
||||
- Hook 的目的只是记录“发送前最后一个可观察形态”,不能替代网络证据。
|
||||
|
||||
---
|
||||
|
||||
## 2. 软 Hook 规则
|
||||
|
||||
### 适用范围
|
||||
|
||||
- 网络证据已锁定目标请求,但仍缺少入口函数证据。
|
||||
- 需要确认参数在某个函数调用前后是否发生变化。
|
||||
- 需要确认返回值、this 绑定、异步行为或全局依赖。
|
||||
|
||||
### 最小 Hook 原则
|
||||
|
||||
- 先 Hook 通用边界,再 Hook 业务函数。
|
||||
- 先记录后改写,默认不替换原函数。
|
||||
- 每个 Hook 只增加一个观察点,避免多个 Hook 同时污染证据。
|
||||
- 页面已加载时,优先使用 evaluate_script 注入。
|
||||
- 页面将刷新或导航时,使用 navigate_page(initScript=...) 预注入。
|
||||
|
||||
### 推荐 Hook 顺序
|
||||
|
||||
1. window.fetch
|
||||
2. XMLHttpRequest.prototype.open
|
||||
3. XMLHttpRequest.prototype.send
|
||||
4. JSON.stringify
|
||||
5. 已明确匹配的业务函数
|
||||
6. 必要时再补充 eval、Function 或 Promise 相关节点
|
||||
|
||||
### 每个 Hook 至少记录
|
||||
|
||||
- Hook 点路径。
|
||||
- 匹配条件。
|
||||
- 输入参数摘要。
|
||||
- 返回值摘要。
|
||||
- 调用栈摘要。
|
||||
- 是否改变页面行为。
|
||||
- 注入方式:evaluate_script 或 navigate_page(initScript=...)。
|
||||
|
||||
### 入口确认规则
|
||||
|
||||
业务函数 Hook 至少要证明以下一项:
|
||||
|
||||
- 明文输入与请求中的目标参数存在可验证映射。
|
||||
- 函数返回值直接进入请求体、请求头或 Cookie。
|
||||
|
||||
只看到密码库内部调用、没有业务调用者上下文,不能认定为最终入口。
|
||||
|
||||
### 依赖提取规则
|
||||
|
||||
明确记录:
|
||||
|
||||
- this 绑定来源。
|
||||
- 依赖的全局对象或闭包对象。
|
||||
- 是否需要等待异步结果。
|
||||
- 页面是否必须先完成启动流程。
|
||||
|
||||
这些信息最终写入 artifacts/phase3_dependencies.json 和 analysis_result.json。
|
||||
|
||||
### 风险控制
|
||||
|
||||
- 默认不要冻结大量原型。
|
||||
- 不要为了方便观察而全局替换所有密码学 API。
|
||||
- Hook 导致业务分支改变时,先回退到更小范围,再继续分析。
|
||||
|
||||
---
|
||||
|
||||
## 3. 源码定位
|
||||
|
||||
### 核心原则
|
||||
|
||||
- 先从请求详情、页面内调用栈和序列化节点回溯,再进行关键词搜索。
|
||||
- 先找“参数最后一次发生变化的位置”,再找“最深层算法位置”。
|
||||
- 只接受有证据支持的源码定位结果,不接受纯文本匹配结论。
|
||||
|
||||
### 定位优先级
|
||||
|
||||
1. 请求详情、页面内 Error().stack 或观察代码捕获的调用栈。
|
||||
2. 发送前对象构造或序列化位置。
|
||||
3. 与参数同名或相邻的源码片段。
|
||||
4. 密码库调用位置。
|
||||
|
||||
### 推荐流程
|
||||
|
||||
1. 从目标请求开始,确认请求方法、请求体和相关脚本线索。
|
||||
2. 在请求构造处加入最小观察代码:
|
||||
- 观察 fetch / XHR send。
|
||||
- 观察 JSON.stringify。
|
||||
- 在页面内输出 Error().stack。
|
||||
3. 页面刷新或导航时:
|
||||
- 使用 navigate_page(initScript=...) 预注入。
|
||||
- 重新触发目标动作并记录序列化前对象。
|
||||
4. 标记参数的三个关键位置:
|
||||
- 明文入口。
|
||||
- 处理或加密位置。
|
||||
- 发送前最终写入位置。
|
||||
5. 只有其中至少两个位置被证据串联后,才能输出首选入口。
|
||||
|
||||
### 候选入口接受标准
|
||||
|
||||
- 能解释参数从明文到密文的变化。
|
||||
- 能定位可调用函数或稳定解析器。
|
||||
- 能解释 this 绑定、参数签名和必要依赖。
|
||||
|
||||
### 常见误判
|
||||
|
||||
- 只匹配到参数名,但变量只是中间副本。
|
||||
- 只匹配到 md5、aes 或 sha,但它并不服务于目标请求。
|
||||
- 只看到包装函数,没有继续确认真实执行点。
|
||||
|
||||
### 输出要求
|
||||
|
||||
- source_hint 尽量落到具体 bundle 位置或对象路径。
|
||||
- evidence 至少包含以下一项:
|
||||
- 页面内调用栈。
|
||||
- Hook 捕获的输入参数和返回值。
|
||||
- 序列化前对象快照。
|
||||
- 参数值对照实验结果。
|
||||
|
||||
---
|
||||
|
||||
## 4. 运行时 Hook 跟踪
|
||||
|
||||
### 概述
|
||||
|
||||
运行时 Hook 使用预注入探针,在网络、密码学和序列化层捕获证据,是入口发现的重要证据源。
|
||||
|
||||
### 探针安装
|
||||
|
||||
生成:
|
||||
|
||||
~~~bash
|
||||
python3 scripts/emit_runtime_hook_probe.py --output generated/runtime_hook_probe.js --params "target_param1,target_param2"
|
||||
~~~
|
||||
|
||||
注入方式:
|
||||
|
||||
- 页面已加载时使用 evaluate_script。
|
||||
- 页面会刷新或导航时使用 navigate_page(initScript=...)。
|
||||
|
||||
幂等性:
|
||||
|
||||
- 探针安装前检查 window.__JSRA_TRACE__,重复注入应安全地跳过。
|
||||
|
||||
### 探针捕获内容
|
||||
|
||||
探针在以下边界安装软 Hook,并将证据写入 window.__JSRA_TRACE__。
|
||||
|
||||
#### 4.1 网络请求(requests[])
|
||||
|
||||
- Hook 点:window.fetch、XMLHttpRequest.prototype.open、XMLHttpRequest.prototype.send。
|
||||
- 每条记录包括:
|
||||
- type:fetch 或 xhr。
|
||||
- url:请求 URL。
|
||||
- method:HTTP 方法。
|
||||
- headers:摘要后的请求头。
|
||||
- bodySnippet:截断后的请求体。
|
||||
- timestamp:Date.now()。
|
||||
- stack:通过 Error().stack 捕获的调用栈,最多 10 帧。
|
||||
|
||||
#### 4.2 密码学事件(crypto[])
|
||||
|
||||
- Hook 点:crypto.subtle.digest、sign、encrypt、decrypt。
|
||||
- 每条记录包括:
|
||||
- type:digest、sign、encrypt 或 decrypt。
|
||||
- algorithm:算法名称。
|
||||
- inputLen:输入字节数。
|
||||
- outputLen:Promise 完成后的输出字节数。
|
||||
- outputHex:摘要结果前 32 个十六进制字符,仅用于 digest。
|
||||
- timestamp:Date.now()。
|
||||
- stack:调用栈,最多 10 帧。
|
||||
|
||||
#### 4.3 序列化事件(serializers[])
|
||||
|
||||
- Hook 点:FormData.append、FormData.set、URLSearchParams.append、URLSearchParams.set、URLSearchParams.toString、JSON.stringify。
|
||||
- 每条记录包括:
|
||||
- type:序列化方法名。
|
||||
- name:字段名。
|
||||
- valueSnippet 或 resultSnippet:截断后的值。
|
||||
- keys:JSON.stringify 对象的键,最多 20 个。
|
||||
- timestamp:Date.now()。
|
||||
- stack:JSON.stringify 和 URLSearchParams.toString 的调用栈。
|
||||
|
||||
#### 4.4 选择性 JSON.stringify Hook
|
||||
|
||||
- 只有待序列化对象包含目标参数或敏感字段时才记录。
|
||||
- 敏感字段包括 sign、token、enc、password、signature、hash、key、nonce、timestamp、ts、data、encrypt、decrypt。
|
||||
- 这样可以避免无关序列化调用造成日志洪泛。
|
||||
|
||||
### 读取证据
|
||||
|
||||
导出全部证据:
|
||||
|
||||
~~~js
|
||||
evaluate_script("window.__JSRA_TRACE__.dump()")
|
||||
~~~
|
||||
|
||||
返回包含 requests、crypto、serializers、errors 的 JSON 字符串。
|
||||
|
||||
清空证据缓存:
|
||||
|
||||
~~~js
|
||||
evaluate_script("window.__JSRA_TRACE__.clear()")
|
||||
~~~
|
||||
|
||||
### 使用证据发现入口
|
||||
|
||||
1. 从 requests[].stack 中寻找加密或签名函数调用帧,这些帧指向业务调用者,而不仅是密码库。
|
||||
2. 与序列化事件交叉比对:如果 serializers[] 显示 JSON.stringify 或 URLSearchParams.toString 处理了关注字段,调用栈可以定位请求体组装位置。
|
||||
3. 将密码学算法和请求体对应:crypto[] 中的 digest 或 encrypt 事件,应与请求体中的密文长度和算法相匹配。
|
||||
4. 从调用栈提取最外层业务函数,排除 fetch、XMLHttpRequest 和密码库内部函数,作为候选入口。
|
||||
|
||||
### 运行时跟踪的证据标准
|
||||
|
||||
- 高置信度:至少一条经过业务函数的 requests[] 调用栈,以及一条与目标参数相关的 crypto[] 或 serializers[] 事件。
|
||||
- 中置信度:至少一条 bodySnippet 中出现目标参数的 requests[] 记录,即使调用栈未明确指向业务函数。
|
||||
- 仅有密码库内部调用、没有业务调用者上下文,不能达到高置信度。
|
||||
|
||||
### 失败处理
|
||||
|
||||
| 现象 | 处理方式 |
|
||||
|---|---|
|
||||
| 探针注入失败(被反调试阻断) | 记录反调试现象,参阅反调试规则,降级到静态分析 |
|
||||
| 触发目标动作后没有 requests[] | 确认动作确实执行,并检查探针是否注入到正确执行上下文 |
|
||||
| crypto[] 为空但请求体包含密文 | 可能使用非 Subtle 密码库,使用 CryptoJS/JSEncrypt Hook 规则 |
|
||||
| stack 全部匿名或已混淆 | 使用源码定位规则,将调用栈与 bundle 行号关联 |
|
||||
|
||||
### 风险提示
|
||||
|
||||
- 探针不能替代网络证据,只能补充网络证据。
|
||||
- 探针使用软包装,不应替换原函数或破坏页面功能。
|
||||
- Hook 造成计时变化时,先记录未启用 Hook 的基线,再记录启用后的观察结果。
|
||||
- 探针不会自动跨页面导航持久化,页面刷新后需要通过 initScript 重新注入。
|
||||
|
||||
---
|
||||
|
||||
## 5. 跨场景证据标准
|
||||
|
||||
### 证据层级
|
||||
|
||||
1. 网络证据:请求 URL、方法、请求体、请求头和响应,强度最高。
|
||||
2. 运行时 Hook 证据:调用栈、密码学事件和序列化事件,强度较高。
|
||||
3. 源码证据:bundle 位置、对象路径和关键词匹配,仅作为辅助。
|
||||
|
||||
### 置信度要求
|
||||
|
||||
| 置信度 | 最低证据 |
|
||||
|---|---|
|
||||
| high | 网络证据 + 运行时/调用栈/模块证据,至少两类 |
|
||||
| medium | 单一证据源(网络或运行时) |
|
||||
| low | 仅有关键词搜索或源码模式 |
|
||||
|
||||
### 可计入证据的内容
|
||||
|
||||
- 运行时探针产生的 requests[],且调用栈经过业务函数。
|
||||
- 能将算法和输出长度与实际密文对应的 crypto[] 事件。
|
||||
- 显示参数在请求发送前被序列化的 serializers[] 事件。
|
||||
- evaluate_script 捕获并指向业务调用者的 Error().stack。
|
||||
- 能将明文映射到密文的 Hook 输入/输出对。
|
||||
- 只改变一个输入值后,能证明哪个请求字段随之变化的实验结果。
|
||||
|
||||
### 不可单独计入证据的内容
|
||||
|
||||
- 没有运行时关联的源码关键词匹配。
|
||||
- 没有业务调用者上下文的密码库函数匹配。
|
||||
- 存在于全局对象上、但目标动作期间从未被观察到调用的路径。
|
||||
- 没有调用栈、Hook 或网络确认的疑似入口。
|
||||
|
||||
### 记录要求
|
||||
|
||||
每条证据必须包含:
|
||||
|
||||
- 来源类型:网络、Hook、调用栈、序列化、密码学或实验。
|
||||
- 时间戳或序列号。
|
||||
- 足以复现观察的上下文:URL、方法、Hook 点路径和调用栈。
|
||||
- 观察发生时是否启用了补丁或 Hook。
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"iterations": [
|
||||
{"round":1,"hardening":"运行时路由间接调用与入口包装","capability":"全局入口与网络证据关联","result":"8/8"},
|
||||
{"round":2,"hardening":"字符编码路由重建与不可枚举解析器","capability":"动态路由解析","result":"8/8"},
|
||||
{"round":3,"hardening":"函数/API 完整性快照与反调试标记","capability":"完整性与外部变化观测","result":"8/8"},
|
||||
{"round":4,"hardening":"不可写、不可配置入口","capability":"入口补丁受阻时优先使用网络证据降级","result":"8/8"},
|
||||
{"round":5,"hardening":"CryptoJS AES 与 JSEncrypt 代理层","capability":"组合密码学原语跟踪","result":"8/8"},
|
||||
{"round":6,"hardening":"HMAC-SHA256 与 DES 代理层","capability":"签名与传统密码算法跟踪","result":"8/8"},
|
||||
{"round":7,"hardening":"fetch 代理层","capability":"网络 Hook 组合","result":"8/8"},
|
||||
{"round":8,"hardening":"webdriver、插件、语言和可见性信号","capability":"观测环境信号且避免误阻断","result":"8/8"},
|
||||
{"round":9,"hardening":"事件循环计时异常检测","capability":"反单步与延迟信号收集","result":"8/8"},
|
||||
{"round":10,"hardening":"源码/API 指纹与动态干扰项","capability":"保留运行时证据并隔离静态噪声","result":"8/8"}
|
||||
],
|
||||
"delivery_regressions": [
|
||||
{"issue":"fetch 完成后页面函数返回空值","fix":"生成的 JSRPC 在调用期间捕获 fetch 请求和响应"},
|
||||
{"issue":"最终业务请求前存在多个 fetch","fix":"按规范化后的精确路径匹配目标路由"},
|
||||
{"issue":"成功导航中断重复测试","fix":"提供可选的 capture.suppress_page_success 测试模式"},
|
||||
{"issue":"仅使用 XHR 的页面在 JSRPC 输出中缺少最终请求体","fix":"统一 fetch/XHR 捕获、路由匹配、响应解析和请求头记录"},
|
||||
{"issue":"成功调用后超时计时器仍存活","fix":"Promise.race 结束后清理结果和网络超时计时器"}
|
||||
],
|
||||
"limits": [
|
||||
"suppress_page_success 仅用于测试,不改变捕获到的服务端响应",
|
||||
"密码学协议强度不变;加固提升的是逆向分析成本",
|
||||
"验证码、WebAuthn、设备证明和未导出的 WASM 内部函数仍需人工处理或标记为不支持"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
# 最终报告模板
|
||||
|
||||
验证通过!现在输出最终结果。
|
||||
|
||||
---
|
||||
|
||||
## JS 逆向分析完成 <任务名称>
|
||||
|
||||
### 分析结果
|
||||
|
||||
- 加密函数:`<函数路径>`
|
||||
- 加密方式:`<算法/编码/组合>`
|
||||
- 入口路径:`<页面入口或运行时路径>`
|
||||
- 置信度:`高`(网络请求 + 运行时 Hook 双重验证)
|
||||
|
||||
### 验证结果
|
||||
|
||||
| 测试 | 输入 | 输出 |
|
||||
|---|---|---|
|
||||
| JSRPC | `<plaintext>` | `<ciphertext/signature>` |
|
||||
| Flask 代理 | `<original body>` | `<transformed body>` |
|
||||
|
||||
### 验证命令
|
||||
|
||||
```bash
|
||||
curl -sS --get \
|
||||
--data-urlencode group=<group> \
|
||||
--data-urlencode action=<action> \
|
||||
--data-urlencode 'param={"parameter":"<parameter>","value":"<plaintext>"}' \
|
||||
http://127.0.0.1:12080/go
|
||||
```
|
||||
|
||||
预期结果必须包含明文、最终请求体、密文/签名和最终业务响应。
|
||||
|
||||
将上述真实返回保存为 `artifacts/jsrpc_smoke.json` 后,必须执行:
|
||||
|
||||
```bash
|
||||
python3 scripts/validate_browser_evidence.py \
|
||||
--input artifacts/jsrpc_smoke.json \
|
||||
--output artifacts/browser_evidence.json
|
||||
```
|
||||
|
||||
只有 `browser_evidence.json` 中 `passed=true` 时,才能在报告中写“验证通过”。如果保存了桥接连接、`tabId` 和 `documentId`,追加 `--require-session`。
|
||||
|
||||
### Flask 代理验证
|
||||
|
||||
```bash
|
||||
curl --noproxy '*' -sS -X POST http://127.0.0.1:<flask_port>/<route> \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'dataBody=<完整的原始 URL-encoded 请求体>' \
|
||||
--data-urlencode 'dataHeaders=<可选的请求头 JSON>'
|
||||
```
|
||||
|
||||
预期结果必须返回代理改写后的请求体,其中目标字段已完成加密或签名;如果配置了响应透传,还应包含最终业务响应。
|
||||
|
||||
Burp“请求数据包/响应数据包”整包模式还必须单独验证:
|
||||
|
||||
```bash
|
||||
curl --noproxy '*' -sS -X POST http://127.0.0.1:<flask_port>/encode \
|
||||
-H 'Content-Type: application/octet-stream' \
|
||||
--data-binary $'POST /path HTTP/1.1\\r\\nHost: target\\r\\nContent-Type: application/x-www-form-urlencoded\\r\\n\\r\\npassword=plaintext'
|
||||
```
|
||||
|
||||
预期返回完整 HTTP 报文,目标字段已替换且 `Content-Length` 已同步。外层 `application/octet-stream` 不代表目标 body 是二进制;代理必须识别内层 HTTP 报文的 Content-Type。
|
||||
|
||||
### Burp autoDecoder 页面配置
|
||||
|
||||
以下 `<flask_port>` 必须替换为 `artifacts/flask_status.json` 中服务实际返回的 `port`。如果默认端口被占用,服务管理器会自动选择备用端口。
|
||||
|
||||
#### 请求体加密
|
||||
|
||||
- 加密接口:`http://127.0.0.1:<flask_port>/encode`。
|
||||
- 可以同时配置加密接口和解密接口;当前测试时选择“请求数据包”,不要同时选择“响应数据包”。两者是互斥方向。
|
||||
- 原始数据包区域粘贴完整 HTTP 请求,不要只粘贴 body。
|
||||
- “处理请求头”:仅当签名或加密依赖请求头时勾选,否则不勾选。
|
||||
- “请求 base64 编码”:只有请求体进入接口前需要 Base64 编码时勾选。
|
||||
- “请求自动 base64 解码”:只有接口需要先自动还原 Base64 请求体时勾选。
|
||||
- “Proxy、Repeater 等模块真实调试”:需要在 Burp Proxy/Repeater 联调时勾选,否则不勾选。
|
||||
|
||||
#### 响应体解密
|
||||
|
||||
- 解密接口:`http://127.0.0.1:<flask_port>/decode`(可与加密接口同时配置)。
|
||||
- 取消选择“请求数据包”,改为选择“响应数据包”,不能同时选择两个方向。
|
||||
- 原始数据包区域粘贴完整 HTTP 响应,不要只粘贴 body。
|
||||
- “处理响应头”:仅当响应解密依赖响应头时勾选,否则不勾选。
|
||||
- “响应 base64 编码”:只有响应体进入接口前需要 Base64 编码时勾选。
|
||||
- “响应自动 base64 解码”:只有接口返回 Base64、需要还原二进制响应时勾选。
|
||||
- 响应方向传递 `requestorresponse=response`;请求方向传递 `requestorresponse=request`。
|
||||
|
||||
启用请求头或响应头处理时,接口必须返回 `请求头 + "\\r\\n\\r\\n\\r\\n\\r\\n" + 改写后的请求/响应体`。
|
||||
|
||||
### 关闭服务命令
|
||||
|
||||
```bash
|
||||
kill $(lsof -t -i:12080)
|
||||
kill $(lsof -t -i:<flask_port>)
|
||||
```
|
||||
|
||||
### Burp autoDecoder 配置
|
||||
|
||||
- 加密接口:`http://127.0.0.1:<flask_port>/encode`
|
||||
- 解密接口:`http://127.0.0.1:<flask_port>/decode`(可与加密接口同时配置)
|
||||
- HTTP 方法:`POST`
|
||||
- 表单字段:`dataBody`、`dataHeaders`
|
||||
- 生成后必须通过 `python3 scripts/check_delivery_contract.py --proxy generated/flask_proxy.py --output artifacts/delivery_contract.json`。
|
||||
- `jsrpc.base_url` 无论配置为 `http://127.0.0.1:12080` 还是带 `/go` 的完整地址,最终请求只能包含一次 `/go`。
|
||||
|
||||
### 生成的产物
|
||||
|
||||
- `analysis_result.json`
|
||||
- `generated/jsrpc_inject.js`
|
||||
- `generated/flask_proxy.py`
|
||||
- `generated/burp-autodecoder.md`
|
||||
- `artifacts/validation_report.json`
|
||||
- `artifacts/browser_evidence.json`
|
||||
|
||||
### 对抗分析追加
|
||||
|
||||
- 对抗类型:`<反调试/反 Hook/环境检测/动态加载>`
|
||||
- 探针健康:`<丢失补丁/重建错误/丢弃事件>`
|
||||
- 差分结论:`<观察结果/干预结果>`
|
||||
@@ -0,0 +1,71 @@
|
||||
# 输出契约
|
||||
|
||||
## 最终交付格式
|
||||
|
||||
最终回复不得退化为“只返回候选 JSON”或“只报告入口已识别”。必须保留以下顺序:
|
||||
|
||||
1. `验证通过!现在输出最终结果。`
|
||||
2. `JS 逆向分析完成` 与任务名称
|
||||
3. `分析结果`:加密函数、加密方式、入口路径、置信度
|
||||
4. `验证结果` 表:至少包含 `JSRPC`、`Flask 代理`、输入和输出
|
||||
5. `验证命令`:可复制的 JSRPC curl、Flask curl,以及预期返回
|
||||
6. `关闭服务命令`
|
||||
7. `Burp autoDecoder 配置`
|
||||
8. `生成的产物`
|
||||
|
||||
`对抗分析`、`runtime_health`、`adversarial_diff` 和能力边界只能作为追加章节,不能删除或改写上述交付项。
|
||||
|
||||
JSRPC 验证输出必须能证明完整链路:`plaintext`、最终目标 `route`、非空 `requestBody`(密文/签名)、HTTP 状态和最终业务 `response`。捕获层同时覆盖 `fetch` 与 `XMLHttpRequest`;对存在前置取钥匙/取签名请求的函数,必须按规范化 URL 路径选择最终请求。
|
||||
|
||||
## 运行时产物
|
||||
| 产物 | 生成脚本 | 说明 |
|
||||
|---|---|---|
|
||||
| `artifacts/probe_dump.json` | `emit_runtime_hook_probe.py` | 运行时事件 |
|
||||
| `artifacts/module_dump.json` | `emit_module_probe.py` | Webpack 模块 |
|
||||
| `artifacts/evidence_graph.json` | `build_evidence_graph.py` | 事件关联图 |
|
||||
| `artifacts/encryption_candidates.json` | `detect_encryption.py` | 候选发现 |
|
||||
| `artifacts/encryption_candidates.verified.json` | `differential_verifier.py` | 差分验证 |
|
||||
| `artifacts/static_candidates.json` | `ast_candidate_analyzer.js` | AST 分析 |
|
||||
| `artifacts/source_analysis.json` | 人工分析 | 源码分析 |
|
||||
| `artifacts/quarantine.json` | `quarantine.py` | 隔离报告 |
|
||||
| `artifacts/validation_report.json` | `validate_artifacts.py` | 验证报告 |
|
||||
| `artifacts/delivery_contract.json` | `check_delivery_contract.py` | JSRPC 地址、Burp 整包和 wrapper 回归报告 |
|
||||
| `artifacts/browser_evidence.json` | `validate_browser_evidence.py` | action 级真实浏览器/JSRPC 结果验收 |
|
||||
| `artifacts/adversarial_trace.json` | `adversarial_runtime_probe.js` | 对抗运行时事件、Patch、Realm 和健康状态 |
|
||||
| `artifacts/adversarial_diff.json` | `adversarial_diff.py` | 基线与干预差分 |
|
||||
| `artifacts/source_instrumentation_report.json` | `source_instrumentor.js` | 源码级保守插桩报告 |
|
||||
|
||||
## 生成产物
|
||||
| 产物 | 生成脚本 | 说明 |
|
||||
|---|---|---|
|
||||
| `generated/jsrpc_inject.js` | `emit_jsrpc_stub.py` | JSRPC 注入 |
|
||||
| `generated/flask_proxy.py` | `emit_flask_proxy.py` | Flask 代理 |
|
||||
| `generated/burp-autodecoder.md` | `emit_burp_doc.py` | Burp 文档 |
|
||||
| `generated/adversarial_runtime_probe.js` | `emit_adversarial_runtime_probe.py` | 对抗运行时探针 |
|
||||
|
||||
## analysis_result.json 契约
|
||||
|
||||
Schema 层只强制授权和目标信息:
|
||||
|
||||
- `authorization.confirmed=true`
|
||||
- `target.url_pattern`
|
||||
- `transforms` 或 `parameters` 至少存在一个
|
||||
|
||||
最终交付结果还必须尽可能提供以下运行信息;缺失时必须明确标记为
|
||||
`unsupported`、`manual_observed_only` 或未完成,不能伪造成功:
|
||||
|
||||
- `skill`、`input`、`trace`、`parameters`
|
||||
- `jsrpc`、`flask`、`burp`、`diagnostics`
|
||||
- `entrypoint_discovery`、`module_runtime`、`invocation`
|
||||
- `capability_boundary`、`runtime_trace`、`runtime_health`
|
||||
|
||||
分析结果不再携带交付版本号;协议兼容性由当前 Schema 和生成器共同定义。
|
||||
|
||||
## 候选不变量
|
||||
- `verified=true` 必须有 verification 证据
|
||||
- `confidence=high` 必须 `verified=true`
|
||||
- 所有候选必须标记来源
|
||||
- 失败验证保留为负证据
|
||||
|
||||
## strategy 值
|
||||
`global_path`、`runtime_hook`、`webpack_export`、`async_crypto`、`initscript_hook`、`static_ast`、`source_analysis`、`adversarial_runtime`、`source_tap`、`manual_observed_only`、`unsupported`
|
||||
@@ -0,0 +1,9 @@
|
||||
# 安全模型
|
||||
|
||||
- 仅分析授权目标
|
||||
- 探针默认脱敏(SHA-256 指纹)
|
||||
- `--capture-raw` 仅在隔离环境使用
|
||||
- 主动调用需 `safe_to_invoke=true`
|
||||
- 服务绑定 `127.0.0.1`
|
||||
- 不自动启动任意可执行文件
|
||||
- 不终止身份未确认的进程
|
||||
Reference in New Issue
Block a user