# 复杂入口分析规则 本文用于处理复杂的加密入口场景,包括异步函数、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。 - 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 ### 识别 - 文档中出现