reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
@@ -0,0 +1,488 @@
|
||||
#!/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);
|
||||
}
|
||||
Reference in New Issue
Block a user