Files
reverse-skills-max/vendor-new/xbsReverseSkill/web-js-env-patcher/scripts/generate_fingerprint_hook.js

489 lines
22 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
'use strict';
const fs = require('fs');
const path = require('path');
function parseArgs(argv) {
const args = {
types: 'canvas,webgl,dom-geometry',
out: '',
apiPattern: '',
chunkSize: 3000,
deprecatedMaxDataUrlLength: null,
};
for (let i = 2; i < argv.length; i++) {
const a = argv[i];
if (a === '--types') args.types = argv[++i] || args.types;
else if (a === '--out') args.out = argv[++i] || '';
else if (a === '--api-pattern') args.apiPattern = argv[++i] || '';
else if (a === '--chunk-size') args.chunkSize = Number(argv[++i] || args.chunkSize);
else if (a === '--max-data-url-length') {
// 兼容旧参数名:历史版本用它裁剪 dataURL;当前版本严禁裁剪,只把它当作“分片大小”。
args.deprecatedMaxDataUrlLength = Number(argv[++i] || args.chunkSize);
args.chunkSize = args.deprecatedMaxDataUrlLength;
}
else if (a === '--help' || a === '-h') args.help = true;
else throw new Error(`未知参数:${a}`);
}
if (!Number.isFinite(args.chunkSize) || args.chunkSize < 512) args.chunkSize = 3000;
return args;
}
function usage() {
return `用法:
node scripts/generate_fingerprint_hook.js --types canvas,webgl,dom-geometry --out case/hooks/fingerprint-hook.js
node scripts/generate_fingerprint_hook.js --types canvas,webgl,webgpu,audio,dom-geometry --chunk-size 3000
说明:
生成浏览器侧指纹终端 API 采样 Hook。该脚本只用于前置取证,不得进入最终 result/ 交付目录。
长指纹值不会被裁剪为 4000 / 4096 字符;Canvas / WebGL / WebGPU / Audio 等大结果会完整保存或按 chunks 分片保存。
旧参数 --max-data-url-length 已废弃裁剪语义,仅兼容为分片大小,不能用于截断最终值。`;
}
function q(v) { return JSON.stringify(String(v)); }
function header(args) {
return `// 指纹终端 API 采样 Hook,仅用于授权前置取证。
// 使用方式:在用户已确认的取证工具中注入,触发最少量业务动作后执行:
// copy(JSON.stringify(await window.__WEB_JS_ENV_PATCHER_FINALIZE_FINGERPRINT__(), null, 2))
// 如果控制台不支持 await,可先执行:
// window.__WEB_JS_ENV_PATCHER_FINALIZE_FINGERPRINT__().then(v => copy(JSON.stringify(v, null, 2)))
// 注意:长指纹值必须完整保存或分片保存,不得裁剪为 4000 / 4096 字符,也不得把 Trace 可见片段当最终值。
// 不要把本 Hook 放入最终 result/ 交付目录。
(function installFingerprintSampler() {
if (window.__WEB_JS_ENV_PATCHER_FINGERPRINT_HOOKED__) return;
Object.defineProperty(window, "__WEB_JS_ENV_PATCHER_FINGERPRINT_HOOKED__", { value: true, configurable: true });
const chunkSize = ${Number(args.chunkSize) || 3000};
const apiPattern = ${q(args.apiPattern)};
const store = window.__WEB_JS_ENV_PATCHER_FINGERPRINT__ = window.__WEB_JS_ENV_PATCHER_FINGERPRINT__ || {
version: 2,
source: {
pageUrl: location.href,
userAgent: navigator.userAgent,
language: navigator.language,
languages: Array.from(navigator.languages || []),
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "",
capturedAt: new Date().toISOString(),
capturedBy: "真实浏览器指纹采样 Hook",
traceStatus: "browser-complete-sampled",
apiPattern
},
canvas: { toDataURL: [], toBlob: [], measureText: [], getImageData: [] },
webgl: { getParameter: [], getSupportedExtensions: null, getExtension: [], getShaderPrecisionFormat: [], readPixels: [] },
webgpu: { requestAdapter: [] },
audio: { startRendering: [], getChannelData: [] },
domGeometry: { getBoundingClientRect: [], getClientRects: [], offset: [] }
};
function stack() { try { return new Error().stack || ""; } catch(e) { return ""; } }
function chunkString(value, encoding) {
const text = String(value == null ? "" : value);
const chunks = [];
for (let i = 0; i < text.length; i += chunkSize) chunks.push(text.slice(i, i + chunkSize));
return {
encoding,
valueLength: text.length,
chunkSize,
chunks,
truncated: false,
sha256: "",
sha256Of: encoding === "base64-chunks" ? "base64-string" : "string",
hashStatus: "pending-finalize"
};
}
function b64FromArrayLike(arr) {
try {
const bytes = new Uint8Array(arr.buffer || arr, arr.byteOffset || 0, arr.byteLength || arr.length || 0);
let s = "";
const step = 0x8000;
for (let i = 0; i < bytes.length; i += step) {
const sub = bytes.subarray(i, Math.min(i + step, bytes.length));
s += String.fromCharCode.apply(null, sub);
}
return btoa(s);
} catch(e) { return ""; }
}
function chunkArrayLikeAsBase64(arr) {
const encoded = b64FromArrayLike(arr);
const out = chunkString(encoded, "base64-chunks");
try {
const bytes = new Uint8Array(arr.buffer || arr, arr.byteOffset || 0, arr.byteLength || arr.length || 0);
out.rawByteLength = bytes.byteLength;
} catch(e) {}
return out;
}
function clonePlain(value, depth) {
if (depth <= 0) return String(value);
if (value == null) return value;
if (typeof value === "string") return value.length >= 3900 ? chunkString(value, "string-chunks") : value;
if (typeof value === "number" || typeof value === "boolean") return value;
if (Array.isArray(value)) return value.map(v => clonePlain(v, depth - 1));
if (value instanceof Set) return Array.from(value).map(v => clonePlain(v, depth - 1));
if (value instanceof Map) return Array.from(value.entries()).map(([k, v]) => [clonePlain(k, depth - 1), clonePlain(v, depth - 1)]);
if (typeof value === "object") {
const out = {};
for (const key of Object.keys(value)) {
try { out[key] = clonePlain(value[key], depth - 1); } catch(e) { out[key] = "<read-error>"; }
}
return out;
}
return String(value);
}
function selectorOf(el) {
try {
if (!el) return "";
if (el.id) return "#" + el.id;
if (el.className && typeof el.className === "string") return "." + el.className.trim().split(/\\s+/)[0];
return String(el.tagName || "").toLowerCase();
} catch(e) { return ""; }
}
async function sha256Text(text) {
const input = new TextEncoder().encode(String(text));
const digest = await crypto.subtle.digest("SHA-256", input);
return Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2, "0")).join("");
}
async function fillChunkHashes(node) {
if (!node || typeof node !== "object") return;
if ((node.encoding === "string-chunks" || node.encoding === "base64-chunks") && Array.isArray(node.chunks)) {
const text = node.chunks.join("");
node.valueLength = text.length;
node.truncated = false;
try {
node.sha256 = await sha256Text(text);
node.hashStatus = "ok";
} catch(e) {
node.hashStatus = "failed:" + (e && e.message || String(e));
}
}
if (Array.isArray(node)) {
for (const item of node) await fillChunkHashes(item);
} else {
for (const key of Object.keys(node)) await fillChunkHashes(node[key]);
}
}
window.__WEB_JS_ENV_PATCHER_FINALIZE_FINGERPRINT__ = async function finalizeFingerprint() {
await fillChunkHashes(store);
store.source.finalizedAt = new Date().toISOString();
store.source.finalizeNote = "长指纹值已完整保存或分片保存,sha256 针对完整字符串或完整 base64 字符串计算,truncated 必须为 false。";
return store;
};
`;
}
function footer() {
return ` console.info("[web-js-env-patcher] 指纹采样 Hook 已安装。触发目标动作后执行:copy(JSON.stringify(await window.__WEB_JS_ENV_PATCHER_FINALIZE_FINGERPRINT__(), null, 2))");
})();`;
}
function canvasSnippet() {
return `
// ===== Canvas 终端 API 采样 =====
if (window.HTMLCanvasElement) {
const rawToDataURL = HTMLCanvasElement.prototype.toDataURL;
if (rawToDataURL && !rawToDataURL.__wjep_hooked__) {
HTMLCanvasElement.prototype.toDataURL = function patchedToDataURL(type, quality) {
const result = rawToDataURL.apply(this, arguments);
store.canvas.toDataURL.push({
match: { width: this.width, height: this.height, type: type || "image/png" },
result: chunkString(result, "string-chunks"),
source: { capturedBy: "真实浏览器 Hook", tool: "用户选择的取证浏览器", traceStatus: "browser-complete-sampled" },
stack: stack()
});
return result;
};
HTMLCanvasElement.prototype.toDataURL.__wjep_hooked__ = true;
}
const rawToBlob = HTMLCanvasElement.prototype.toBlob;
if (rawToBlob && !rawToBlob.__wjep_hooked__) {
HTMLCanvasElement.prototype.toBlob = function patchedToBlob(callback, type, quality) {
const width = this.width, height = this.height, callStack = stack();
return rawToBlob.call(this, function(blob) {
store.canvas.toBlob.push({
match: { width, height, type: type || (blob && blob.type) || "image/png" },
result: { type: blob && blob.type || type || "", size: blob && blob.size || 0 },
source: { capturedBy: "真实浏览器 Hook", tool: "用户选择的取证浏览器", traceStatus: "browser-complete-sampled" },
stack: callStack
});
return callback && callback.apply(this, arguments);
}, type, quality);
};
HTMLCanvasElement.prototype.toBlob.__wjep_hooked__ = true;
}
}
if (window.CanvasRenderingContext2D) {
const p = CanvasRenderingContext2D.prototype;
const rawMeasureText = p.measureText;
if (rawMeasureText && !rawMeasureText.__wjep_hooked__) {
p.measureText = function patchedMeasureText(text) {
const ret = rawMeasureText.apply(this, arguments);
store.canvas.measureText.push({
match: { text: String(text), font: this.font },
result: {
width: ret.width,
actualBoundingBoxLeft: ret.actualBoundingBoxLeft,
actualBoundingBoxRight: ret.actualBoundingBoxRight,
actualBoundingBoxAscent: ret.actualBoundingBoxAscent,
actualBoundingBoxDescent: ret.actualBoundingBoxDescent,
fontBoundingBoxAscent: ret.fontBoundingBoxAscent,
fontBoundingBoxDescent: ret.fontBoundingBoxDescent
},
source: { capturedBy: "真实浏览器 Hook", tool: "用户选择的取证浏览器", traceStatus: "browser-complete-sampled" },
stack: stack()
});
return ret;
};
p.measureText.__wjep_hooked__ = true;
}
const rawGetImageData = p.getImageData;
if (rawGetImageData && !rawGetImageData.__wjep_hooked__) {
p.getImageData = function patchedGetImageData(sx, sy, sw, sh) {
const ret = rawGetImageData.apply(this, arguments);
store.canvas.getImageData.push({
match: { sx, sy, sw, sh },
result: { width: ret.width, height: ret.height, dataBase64: chunkArrayLikeAsBase64(ret.data) },
source: { capturedBy: "真实浏览器 Hook", tool: "用户选择的取证浏览器", traceStatus: "browser-complete-sampled" },
stack: stack()
});
return ret;
};
p.getImageData.__wjep_hooked__ = true;
}
}
`;
}
function webglSnippet() {
return `
// ===== WebGL 终端 API 采样 =====
for (const Ctor of [window.WebGLRenderingContext, window.WebGL2RenderingContext].filter(Boolean)) {
const p = Ctor.prototype;
if (p.getParameter && !p.getParameter.__wjep_hooked__) {
const raw = p.getParameter;
p.getParameter = function patchedGetParameter(pname) {
const ret = raw.apply(this, arguments);
store.webgl.getParameter.push({
match: { pname },
result: clonePlain(ret, 3),
source: { capturedBy: "真实浏览器 Hook", tool: "用户选择的取证浏览器", traceStatus: "browser-complete-sampled" },
stack: stack()
});
return ret;
};
p.getParameter.__wjep_hooked__ = true;
}
if (p.getSupportedExtensions && !p.getSupportedExtensions.__wjep_hooked__) {
const raw = p.getSupportedExtensions;
p.getSupportedExtensions = function patchedGetSupportedExtensions() {
const ret = raw.apply(this, arguments);
store.webgl.getSupportedExtensions = {
result: Array.from(ret || []),
source: { capturedBy: "真实浏览器 Hook", tool: "用户选择的取证浏览器", traceStatus: "browser-complete-sampled" },
stack: stack()
};
return ret;
};
p.getSupportedExtensions.__wjep_hooked__ = true;
}
if (p.getExtension && !p.getExtension.__wjep_hooked__) {
const raw = p.getExtension;
p.getExtension = function patchedGetExtension(name) {
const ret = raw.apply(this, arguments);
store.webgl.getExtension.push({
match: { name: String(name) },
result: ret ? { exists: true, keys: Object.keys(ret).slice(0, 50) } : null,
source: { capturedBy: "真实浏览器 Hook", tool: "用户选择的取证浏览器", traceStatus: "browser-complete-sampled" },
stack: stack()
});
return ret;
};
p.getExtension.__wjep_hooked__ = true;
}
if (p.getShaderPrecisionFormat && !p.getShaderPrecisionFormat.__wjep_hooked__) {
const raw = p.getShaderPrecisionFormat;
p.getShaderPrecisionFormat = function patchedGetShaderPrecisionFormat(shaderType, precisionType) {
const ret = raw.apply(this, arguments);
store.webgl.getShaderPrecisionFormat.push({
match: { shaderType, precisionType },
result: ret ? { rangeMin: ret.rangeMin, rangeMax: ret.rangeMax, precision: ret.precision } : null,
source: { capturedBy: "真实浏览器 Hook", tool: "用户选择的取证浏览器", traceStatus: "browser-complete-sampled" },
stack: stack()
});
return ret;
};
p.getShaderPrecisionFormat.__wjep_hooked__ = true;
}
if (p.readPixels && !p.readPixels.__wjep_hooked__) {
const raw = p.readPixels;
p.readPixels = function patchedReadPixels(x, y, width, height, format, type, pixels) {
const ret = raw.apply(this, arguments);
store.webgl.readPixels.push({
match: { x, y, width, height, format, type },
result: { dataBase64: pixels ? chunkArrayLikeAsBase64(pixels) : chunkString("", "base64-chunks") },
source: { capturedBy: "真实浏览器 Hook", tool: "用户选择的取证浏览器", traceStatus: "browser-complete-sampled" },
stack: stack()
});
return ret;
};
p.readPixels.__wjep_hooked__ = true;
}
}
`;
}
function webgpuSnippet() {
return `
// ===== WebGPU 终端 API 采样 =====
if (navigator.gpu && navigator.gpu.requestAdapter && !navigator.gpu.requestAdapter.__wjep_hooked__) {
const rawRequestAdapter = navigator.gpu.requestAdapter.bind(navigator.gpu);
navigator.gpu.requestAdapter = async function patchedRequestAdapter(options) {
const adapter = await rawRequestAdapter(options);
const info = adapter && adapter.info ? clonePlain(adapter.info, 4) : null;
store.webgpu.requestAdapter.push({
match: { options: clonePlain(options || {}, 4) },
result: {
info,
features: adapter && adapter.features ? Array.from(adapter.features) : [],
limits: adapter && adapter.limits ? clonePlain(adapter.limits, 3) : {}
},
source: { capturedBy: "真实浏览器 Hook", tool: "用户选择的取证浏览器", traceStatus: "browser-complete-sampled" },
stack: stack()
});
return adapter;
};
navigator.gpu.requestAdapter.__wjep_hooked__ = true;
}
`;
}
function audioSnippet() {
return `
// ===== Audio 指纹终端 API 采样 =====
if (window.AudioBuffer && AudioBuffer.prototype.getChannelData && !AudioBuffer.prototype.getChannelData.__wjep_hooked__) {
const rawGetChannelData = AudioBuffer.prototype.getChannelData;
AudioBuffer.prototype.getChannelData = function patchedGetChannelData(channel) {
const ret = rawGetChannelData.apply(this, arguments);
try {
store.audio.getChannelData.push({
match: { channel, length: this.length, sampleRate: this.sampleRate, numberOfChannels: this.numberOfChannels },
result: { channelBase64: chunkArrayLikeAsBase64(ret) },
source: { capturedBy: "真实浏览器 Hook", tool: "用户选择的取证浏览器", traceStatus: "browser-complete-sampled" },
stack: stack()
});
} catch(e) {}
return ret;
};
AudioBuffer.prototype.getChannelData.__wjep_hooked__ = true;
}
if (window.OfflineAudioContext && OfflineAudioContext.prototype.startRendering && !OfflineAudioContext.prototype.startRendering.__wjep_hooked__) {
const rawStartRendering = OfflineAudioContext.prototype.startRendering;
OfflineAudioContext.prototype.startRendering = function patchedStartRendering() {
const callStack = stack();
const ret = rawStartRendering.apply(this, arguments);
if (ret && typeof ret.then === "function") {
return ret.then(buffer => {
try {
const data = buffer.getChannelData(0);
store.audio.startRendering.push({
match: { length: buffer.length, sampleRate: buffer.sampleRate, numberOfChannels: buffer.numberOfChannels },
result: { channel0Base64: chunkArrayLikeAsBase64(data) },
source: { capturedBy: "真实浏览器 Hook", tool: "用户选择的取证浏览器", traceStatus: "browser-complete-sampled" },
stack: callStack
});
} catch(e) {}
return buffer;
});
}
return ret;
};
OfflineAudioContext.prototype.startRendering.__wjep_hooked__ = true;
}
`;
}
function domSnippet() {
return `
// ===== DOM 几何 / 字体探测终端 API 采样 =====
if (window.Element) {
const p = Element.prototype;
const rawRect = p.getBoundingClientRect;
if (rawRect && !rawRect.__wjep_hooked__) {
p.getBoundingClientRect = function patchedGetBoundingClientRect() {
const ret = rawRect.apply(this, arguments);
store.domGeometry.getBoundingClientRect.push({
match: { selector: selectorOf(this) },
result: { x: ret.x, y: ret.y, width: ret.width, height: ret.height, top: ret.top, left: ret.left, right: ret.right, bottom: ret.bottom },
source: { capturedBy: "真实浏览器 Hook", tool: "用户选择的取证浏览器", traceStatus: "browser-complete-sampled" },
stack: stack()
});
return ret;
};
p.getBoundingClientRect.__wjep_hooked__ = true;
}
const rawRects = p.getClientRects;
if (rawRects && !rawRects.__wjep_hooked__) {
p.getClientRects = function patchedGetClientRects() {
const ret = rawRects.apply(this, arguments);
store.domGeometry.getClientRects.push({
match: { selector: selectorOf(this) },
result: Array.from(ret || []).map(r => ({ x: r.x, y: r.y, width: r.width, height: r.height, top: r.top, left: r.left, right: r.right, bottom: r.bottom })),
source: { capturedBy: "真实浏览器 Hook", tool: "用户选择的取证浏览器", traceStatus: "browser-complete-sampled" },
stack: stack()
});
return ret;
};
p.getClientRects.__wjep_hooked__ = true;
}
for (const key of ["offsetWidth", "offsetHeight", "scrollWidth", "scrollHeight", "clientWidth", "clientHeight"]) {
const desc = Object.getOwnPropertyDescriptor(HTMLElement.prototype, key) || Object.getOwnPropertyDescriptor(Element.prototype, key);
if (!desc || !desc.get || desc.get.__wjep_hooked__) continue;
Object.defineProperty(HTMLElement.prototype, key, {
get: function patchedGeometryGetter() {
const value = desc.get.call(this);
store.domGeometry.offset.push({
match: { selector: selectorOf(this) },
result: { [key]: value },
source: { capturedBy: "真实浏览器 Hook", tool: "用户选择的取证浏览器", traceStatus: "browser-complete-sampled" },
stack: stack()
});
return value;
},
enumerable: desc.enumerable,
configurable: true
});
try { Object.getOwnPropertyDescriptor(HTMLElement.prototype, key).get.__wjep_hooked__ = true; } catch(e) {}
}
}
`;
}
function build(args) {
const types = new Set(String(args.types || '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean));
const parts = [header(args)];
if (types.has('canvas')) parts.push(canvasSnippet());
if (types.has('webgl')) parts.push(webglSnippet());
if (types.has('webgpu')) parts.push(webgpuSnippet());
if (types.has('audio')) parts.push(audioSnippet());
if (types.has('dom') || types.has('dom-geometry') || types.has('font') || types.has('fonts')) parts.push(domSnippet());
parts.push(footer());
return parts.join('\n');
}
try {
const args = parseArgs(process.argv);
if (args.help) { console.log(usage()); process.exit(0); }
const code = build(args);
if (args.out) {
const out = path.resolve(args.out);
fs.mkdirSync(path.dirname(out), { recursive: true });
fs.writeFileSync(out, code, 'utf8');
const compatNote = args.deprecatedMaxDataUrlLength ? '\n- 兼容提醒:--max-data-url-length 已按分片大小处理,没有裁剪任何值。' : '';
console.log(`# 指纹采样 Hook 已生成\n- 输出文件:${out}\n- 类型:${args.types}\n- 分片大小:${args.chunkSize}\n- 提醒:该 Hook 只用于前置取证,不得放入最终 result/ 目录;长指纹值必须 finalize 后完整分片保存。${compatNote}`);
} else {
process.stdout.write(code + '\n');
}
} catch (err) {
console.error(err.message || String(err));
console.error(usage());
process.exit(1);
}