reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
const MODULE_PRIORITY = [
|
||||
"prototype-builder",
|
||||
"descriptor-guard",
|
||||
"native-protector",
|
||||
"navigator-module",
|
||||
"document-module",
|
||||
"document-all-module",
|
||||
"storage-module",
|
||||
"fingerprint-module",
|
||||
"performance-module",
|
||||
"crypto-module",
|
||||
"audio-fingerprint-module",
|
||||
"webrtc-module",
|
||||
"worker-module",
|
||||
"stack-clean-module",
|
||||
];
|
||||
|
||||
function toArray(value) {
|
||||
if (Array.isArray(value)) return value;
|
||||
if (value && Array.isArray(value.logs)) return value.logs;
|
||||
return [];
|
||||
}
|
||||
|
||||
function unique(values) {
|
||||
return Array.from(new Set(values));
|
||||
}
|
||||
|
||||
function extractPathList(input, key, type) {
|
||||
if (Array.isArray(input[key])) return unique(input[key]);
|
||||
return unique(
|
||||
toArray(input)
|
||||
.filter((item) => item && item.type === type && typeof item.path === "string")
|
||||
.map((item) => item.path)
|
||||
);
|
||||
}
|
||||
|
||||
function inferModulesFromPath(path) {
|
||||
const modules = [];
|
||||
const value = String(path || "");
|
||||
|
||||
if (/__proto__|prototype|constructor/.test(value)) modules.push("prototype-builder");
|
||||
if (/getOwnPropertyDescriptor|defineProperty|descriptor|ownKeys/.test(value)) modules.push("descriptor-guard");
|
||||
if (/toString|name|length|caller|arguments/.test(value)) modules.push("native-protector");
|
||||
if (/navigator|plugins|mimeTypes|webdriver|getBattery|userAgentData/.test(value)) {
|
||||
modules.push("navigator-module");
|
||||
if (/getBattery/.test(value)) modules.push("fingerprint-module");
|
||||
}
|
||||
if (/document\.all/.test(value)) {
|
||||
modules.push("document-all-module");
|
||||
modules.push("document-module");
|
||||
} else if (/document|createElement|querySelector|cookie/.test(value)) {
|
||||
modules.push("document-module");
|
||||
}
|
||||
if (/localStorage|sessionStorage|indexedDB|cookieStore/.test(value)) modules.push("storage-module");
|
||||
if (/canvas|webgl|screen|devicePixelRatio|getContext|toDataURL/.test(value)) modules.push("fingerprint-module");
|
||||
if (/performance|Performance/.test(value)) modules.push("performance-module");
|
||||
if (/crypto|msCrypto|getRandomValues|subtle/.test(value)) modules.push("crypto-module");
|
||||
if (/AudioContext|OfflineAudioContext|Oscillator|Analyser/.test(value)) modules.push("audio-fingerprint-module");
|
||||
if (/RTCPeerConnection|RTCDataChannel|RTCIceCandidate|RTCSessionDescription/.test(value)) modules.push("webrtc-module");
|
||||
if (/Worker|SharedWorker|MessagePort|BroadcastChannel/.test(value)) modules.push("worker-module");
|
||||
if (/Error|stack|prepareStackTrace/.test(value)) modules.push("stack-clean-module");
|
||||
|
||||
return unique(modules);
|
||||
}
|
||||
|
||||
function scoreEvidence(entry) {
|
||||
if (!entry || !entry.type) return 0;
|
||||
if (entry.type === "invoke-error") return 5;
|
||||
if (entry.type === "missing") return 4;
|
||||
if (entry.type === "descriptor" || entry.type === "prototype") return 3;
|
||||
return 1;
|
||||
}
|
||||
|
||||
function analyzeGapLog(input = {}) {
|
||||
const logs = toArray(input);
|
||||
const missingPaths = extractPathList(input, "missingPaths", "missing");
|
||||
const descriptorAccess = extractPathList(input, "descriptorAccess", "descriptor");
|
||||
const prototypeAccess = extractPathList(input, "prototypeAccess", "prototype");
|
||||
const invocationErrors = extractPathList(input, "invocationErrors", "invoke-error");
|
||||
const groupedByModule = Object.create(null);
|
||||
|
||||
for (const entry of logs) {
|
||||
const modules = inferModulesFromPath(entry.path);
|
||||
if (!modules.length) continue;
|
||||
for (const moduleName of modules) {
|
||||
if (!groupedByModule[moduleName]) {
|
||||
groupedByModule[moduleName] = { score: 0, evidence: [] };
|
||||
}
|
||||
groupedByModule[moduleName].score += scoreEvidence(entry);
|
||||
groupedByModule[moduleName].evidence.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
const recommendedModules = Object.keys(groupedByModule).sort((left, right) => {
|
||||
const leftPriority = MODULE_PRIORITY.indexOf(left);
|
||||
const rightPriority = MODULE_PRIORITY.indexOf(right);
|
||||
const leftScore = groupedByModule[left].score;
|
||||
const rightScore = groupedByModule[right].score;
|
||||
if (rightScore !== leftScore) return rightScore - leftScore;
|
||||
return leftPriority - rightPriority;
|
||||
});
|
||||
|
||||
const nextActions = [];
|
||||
if (recommendedModules.includes("prototype-builder")) {
|
||||
nextActions.push("先修复构造器、原型链和 instanceof 关系,再处理对象值。");
|
||||
}
|
||||
if (recommendedModules.includes("descriptor-guard")) {
|
||||
nextActions.push("把 getOwnPropertyDescriptor/defineProperty/propertyIsEnumerable 的结果对齐。");
|
||||
}
|
||||
if (recommendedModules.includes("native-protector")) {
|
||||
nextActions.push("统一接管 Function.prototype.toString,并补齐 getter/setter/name/length 保护。");
|
||||
}
|
||||
if (recommendedModules.includes("document-all-module")) {
|
||||
nextActions.push("单独评估 document.all,明确是 native-addon、V8 API 还是 fallback 路线。");
|
||||
}
|
||||
if (recommendedModules.includes("storage-module")) {
|
||||
nextActions.push("把 cookie/localStorage/sessionStorage 的真实状态同步进运行时。");
|
||||
}
|
||||
if (recommendedModules.includes("fingerprint-module")) {
|
||||
nextActions.push("把 canvas/webgl/screen/battery 等指纹点单独建模,不要和普通 navigator 属性混补。");
|
||||
}
|
||||
|
||||
return {
|
||||
totalLogs: logs.length,
|
||||
missingPaths,
|
||||
descriptorAccess,
|
||||
prototypeAccess,
|
||||
invocationErrors,
|
||||
recommendedModules,
|
||||
groupedByModule,
|
||||
nextActions: unique(nextActions),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
analyzeGapLog,
|
||||
inferModulesFromPath,
|
||||
};
|
||||
@@ -0,0 +1,180 @@
|
||||
const { scaffoldModule } = require("./scaffold-module");
|
||||
|
||||
const DEFAULT_SUBTLE_METHODS = [
|
||||
"digest",
|
||||
"generateKey",
|
||||
"importKey",
|
||||
"exportKey",
|
||||
"sign",
|
||||
"verify",
|
||||
"encrypt",
|
||||
"decrypt",
|
||||
];
|
||||
|
||||
function toFiniteNumber(value, fallback) {
|
||||
return Number.isFinite(value) ? value : fallback;
|
||||
}
|
||||
|
||||
function normalizeCryptoSeed(seed = {}) {
|
||||
const cryptoSeed = seed.crypto && typeof seed.crypto === "object" ? seed.crypto : seed;
|
||||
return {
|
||||
exposeCrypto: cryptoSeed.exposeCrypto !== false,
|
||||
installMsCrypto: cryptoSeed.installMsCrypto !== false,
|
||||
maxGetRandomValuesLength: toFiniteNumber(cryptoSeed.maxGetRandomValuesLength, 65536),
|
||||
hasSubtle: cryptoSeed.hasSubtle !== false,
|
||||
subtleMethods: Array.isArray(cryptoSeed.subtleMethods) && cryptoSeed.subtleMethods.length
|
||||
? cryptoSeed.subtleMethods.slice()
|
||||
: DEFAULT_SUBTLE_METHODS.slice(),
|
||||
randomValues: Array.isArray(cryptoSeed.randomValues)
|
||||
? cryptoSeed.randomValues.map((item) => Number(item) & 255)
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
function renderCryptoPatch(seed) {
|
||||
const serializedSeed = JSON.stringify(seed, null, 2);
|
||||
return `(
|
||||
function installCryptoModule(globalObject) {
|
||||
const seed = ${serializedSeed};
|
||||
const protector = globalObject.__envNativeProtector || null;
|
||||
const integerArrayTags = new Set([
|
||||
"[object Int8Array]",
|
||||
"[object Uint8Array]",
|
||||
"[object Uint8ClampedArray]",
|
||||
"[object Int16Array]",
|
||||
"[object Uint16Array]",
|
||||
"[object Int32Array]",
|
||||
"[object Uint32Array]"
|
||||
]);
|
||||
const randomPool = Array.isArray(seed.randomValues) ? seed.randomValues.slice() : [];
|
||||
|
||||
function defineValue(target, key, value) {
|
||||
Object.defineProperty(target, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: value
|
||||
});
|
||||
}
|
||||
|
||||
function createQuotaError(message) {
|
||||
if (typeof DOMException === "function") {
|
||||
return new DOMException(message, "QuotaExceededError");
|
||||
}
|
||||
const error = new Error(message);
|
||||
error.name = "QuotaExceededError";
|
||||
return error;
|
||||
}
|
||||
|
||||
function assertIntegerTypedArray(view) {
|
||||
const tag = Object.prototype.toString.call(view);
|
||||
if (!integerArrayTags.has(tag)) {
|
||||
throw new TypeError("Failed to execute 'getRandomValues' on 'Crypto': parameter 1 is not an integer array view.");
|
||||
}
|
||||
if (view.byteLength > seed.maxGetRandomValuesLength) {
|
||||
throw createQuotaError("Failed to execute 'getRandomValues' on 'Crypto': The ArrayBufferView's byte length exceeds the number of bytes of entropy available via this API.");
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
function nextByte(index) {
|
||||
if (randomPool.length) {
|
||||
return randomPool[index % randomPool.length] & 255;
|
||||
}
|
||||
return (index * 73 + 19) & 255;
|
||||
}
|
||||
|
||||
function getRandomValues(view) {
|
||||
assertIntegerTypedArray(view);
|
||||
for (let index = 0; index < view.length; index += 1) {
|
||||
view[index] = nextByte(index);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
const subtle = {};
|
||||
for (const methodName of seed.subtleMethods) {
|
||||
subtle[methodName] = function subtlePlaceholder() {
|
||||
return Promise.reject(new Error("SubtleCrypto method not implemented: " + methodName));
|
||||
};
|
||||
if (protector && protector.repairFunctionMeta) {
|
||||
protector.repairFunctionMeta(subtle[methodName], { name: methodName, length: 0 });
|
||||
}
|
||||
if (protector && protector.protectFunction) {
|
||||
protector.protectFunction(subtle[methodName], methodName);
|
||||
}
|
||||
}
|
||||
|
||||
const cryptoObject = {};
|
||||
defineValue(cryptoObject, "getRandomValues", getRandomValues);
|
||||
if (seed.hasSubtle) {
|
||||
defineValue(cryptoObject, "subtle", subtle);
|
||||
}
|
||||
|
||||
if (protector && protector.repairFunctionMeta) {
|
||||
protector.repairFunctionMeta(getRandomValues, { name: "getRandomValues", length: 1 });
|
||||
}
|
||||
if (protector && protector.protectFunction) {
|
||||
protector.protectFunction(getRandomValues, "getRandomValues");
|
||||
}
|
||||
|
||||
if (seed.exposeCrypto) {
|
||||
Object.defineProperty(globalObject, "crypto", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: cryptoObject
|
||||
});
|
||||
}
|
||||
|
||||
if (seed.installMsCrypto) {
|
||||
Object.defineProperty(globalObject, "msCrypto", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: cryptoObject
|
||||
});
|
||||
}
|
||||
|
||||
return cryptoObject;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : window);
|
||||
`;
|
||||
}
|
||||
|
||||
function buildCryptoModule(seed = {}, options = {}) {
|
||||
const module = scaffoldModule("crypto-module");
|
||||
const normalizedSeed = normalizeCryptoSeed(seed);
|
||||
|
||||
module.patchPlan = [
|
||||
"挂载 window.crypto 与可选的 window.msCrypto。",
|
||||
"按种子构建 getRandomValues,并复刻整数 TypedArray 校验和长度上限。",
|
||||
"按模块契约补出 subtle 对象的基础方法占位,并接入 native 保护接口。",
|
||||
];
|
||||
|
||||
module.patchCode = renderCryptoPatch(normalizedSeed, options);
|
||||
module.runtimeState = {
|
||||
crypto: normalizedSeed,
|
||||
};
|
||||
module.validation = [
|
||||
"调用 crypto.getRandomValues(new Uint8Array(16)),确认返回原数组实例。",
|
||||
"调用 crypto.getRandomValues(new Uint16Array(8)) 与 new Uint32Array(4)。",
|
||||
"传入 Float32Array 时应抛出 TypeError。",
|
||||
"传入超出 maxGetRandomValuesLength 的视图时应抛出 QuotaExceededError。",
|
||||
"检查 crypto.getRandomValues.toString() 与 crypto.getRandomValues.name/length。",
|
||||
];
|
||||
module.residualRisk = [
|
||||
"subtle 仍为占位实现,只适合绕过存在性检测,不适合真实密码流程。",
|
||||
"如果目标站点会比对 DOMException message/code,仍需依据真实浏览器日志继续收敛。",
|
||||
];
|
||||
|
||||
if (options.includeLegacyAlias === false) {
|
||||
module.runtimeState.crypto.installMsCrypto = false;
|
||||
}
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildCryptoModule,
|
||||
normalizeCryptoSeed,
|
||||
};
|
||||
@@ -0,0 +1,332 @@
|
||||
const { scaffoldModule } = require("./scaffold-module");
|
||||
|
||||
function clone(value) {
|
||||
return JSON.parse(JSON.stringify(value || {}));
|
||||
}
|
||||
|
||||
function normalizeDocumentSeed(seed = {}) {
|
||||
const documentSeed = seed.document && typeof seed.document === "object" ? seed.document : seed;
|
||||
return {
|
||||
document: {
|
||||
URL: documentSeed.URL || "https://example.invalid/",
|
||||
referrer: documentSeed.referrer || "",
|
||||
documentURI: documentSeed.documentURI || documentSeed.URL || "https://example.invalid/",
|
||||
compatMode: documentSeed.compatMode || "CSS1Compat",
|
||||
dir: documentSeed.dir || "",
|
||||
title: documentSeed.title || "",
|
||||
designMode: documentSeed.designMode || "off",
|
||||
readyState: documentSeed.readyState || "complete",
|
||||
contentType: documentSeed.contentType || "text/html",
|
||||
inputEncoding: documentSeed.inputEncoding || "UTF-8",
|
||||
domain: documentSeed.domain || "example.invalid",
|
||||
characterSet: documentSeed.characterSet || "UTF-8",
|
||||
charset: documentSeed.charset || "UTF-8",
|
||||
hidden: documentSeed.hidden === true,
|
||||
visibilityState: documentSeed.visibilityState || "visible",
|
||||
cookie: typeof documentSeed.cookie === "string" ? documentSeed.cookie : (typeof seed.cookie === "string" ? seed.cookie : ""),
|
||||
},
|
||||
queryMap: clone(seed.queryMap || {}),
|
||||
ids: clone(seed.ids || {}),
|
||||
tags: clone(seed.tags || {}),
|
||||
specialCases: clone(seed.specialCases || {}),
|
||||
};
|
||||
}
|
||||
|
||||
function renderDocumentPatch(seed) {
|
||||
const serializedSeed = JSON.stringify(seed, null, 2);
|
||||
return `(
|
||||
function installDocumentModule(globalObject) {
|
||||
const seed = ${serializedSeed};
|
||||
const protector = globalObject.__envNativeProtector || null;
|
||||
|
||||
function defineValue(target, key, value) {
|
||||
Object.defineProperty(target, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: value
|
||||
});
|
||||
}
|
||||
|
||||
function defineGetter(target, key, getter, setter) {
|
||||
const descriptor = {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
get: getter
|
||||
};
|
||||
if (typeof setter === "function") {
|
||||
descriptor.set = setter;
|
||||
}
|
||||
Object.defineProperty(target, key, descriptor);
|
||||
if (protector && protector.protectDescriptor) {
|
||||
protector.protectDescriptor(target, key);
|
||||
}
|
||||
}
|
||||
|
||||
function applyEventTarget(target) {
|
||||
defineValue(target, "__listeners__", Object.create(null));
|
||||
defineValue(target, "addEventListener", function addEventListener(type, listener) {
|
||||
if (!this.__listeners__[type]) {
|
||||
this.__listeners__[type] = [];
|
||||
}
|
||||
this.__listeners__[type].push(listener);
|
||||
});
|
||||
defineValue(target, "removeEventListener", function removeEventListener(type, listener) {
|
||||
const list = this.__listeners__[type] || [];
|
||||
this.__listeners__[type] = list.filter(function filterItem(item) {
|
||||
return item !== listener;
|
||||
});
|
||||
});
|
||||
defineValue(target, "dispatchEvent", function dispatchEvent(event) {
|
||||
const list = (this.__listeners__[event.type] || []).slice();
|
||||
for (const listener of list) {
|
||||
listener.call(this, event);
|
||||
}
|
||||
const handler = this["on" + event.type];
|
||||
if (typeof handler === "function") {
|
||||
handler.call(this, event);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function createElementNode(tagName) {
|
||||
const normalized = String(tagName || "div").toUpperCase();
|
||||
if (normalized === "CANVAS" && typeof globalObject.__envCreateCanvasElement === "function") {
|
||||
return globalObject.__envCreateCanvasElement();
|
||||
}
|
||||
const element = {
|
||||
nodeType: 1,
|
||||
nodeName: normalized,
|
||||
tagName: normalized,
|
||||
ownerDocument: null,
|
||||
style: {},
|
||||
dataset: {},
|
||||
children: [],
|
||||
childNodes: []
|
||||
};
|
||||
applyEventTarget(element);
|
||||
defineValue(element, "appendChild", function appendChild(child) {
|
||||
this.children.push(child);
|
||||
this.childNodes.push(child);
|
||||
child.parentNode = this;
|
||||
return child;
|
||||
});
|
||||
defineValue(element, "setAttribute", function setAttribute(name, value) {
|
||||
this[name] = String(value);
|
||||
});
|
||||
defineValue(element, "getAttribute", function getAttribute(name) {
|
||||
return Object.prototype.hasOwnProperty.call(this, name) ? this[name] : null;
|
||||
});
|
||||
return element;
|
||||
}
|
||||
|
||||
function buildNodeMap(source, kind) {
|
||||
const out = Object.create(null);
|
||||
Object.keys(source || {}).forEach(function eachKey(key) {
|
||||
const value = source[key];
|
||||
if (Array.isArray(value)) {
|
||||
out[key] = value.map(function mapTag(tagName) {
|
||||
const node = createElementNode(tagName);
|
||||
node.ownerDocument = documentInstance;
|
||||
return node;
|
||||
});
|
||||
} else {
|
||||
const node = createElementNode(value || kind || "div");
|
||||
node.ownerDocument = documentInstance;
|
||||
out[key] = node;
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function Document() {}
|
||||
function HTMLDocument() {}
|
||||
HTMLDocument.prototype = Object.create(Document.prototype);
|
||||
defineValue(HTMLDocument.prototype, "constructor", HTMLDocument);
|
||||
|
||||
const state = seed.document || {};
|
||||
const documentInstance = Object.create(HTMLDocument.prototype);
|
||||
let cookieValue = state.cookie || "";
|
||||
applyEventTarget(documentInstance);
|
||||
|
||||
const queryNodes = buildNodeMap(seed.queryMap || {}, "div");
|
||||
const idNodes = buildNodeMap(seed.ids || {}, "div");
|
||||
const tagNodes = buildNodeMap(seed.tags || {}, "div");
|
||||
const htmlNode = createElementNode("html");
|
||||
const headNode = createElementNode("head");
|
||||
const bodyNode = createElementNode("body");
|
||||
htmlNode.ownerDocument = documentInstance;
|
||||
headNode.ownerDocument = documentInstance;
|
||||
bodyNode.ownerDocument = documentInstance;
|
||||
htmlNode.appendChild(headNode);
|
||||
htmlNode.appendChild(bodyNode);
|
||||
|
||||
defineGetter(Document.prototype, "URL", function getURL() {
|
||||
return state.URL;
|
||||
});
|
||||
defineGetter(Document.prototype, "referrer", function getReferrer() {
|
||||
return state.referrer;
|
||||
});
|
||||
defineGetter(Document.prototype, "documentURI", function getDocumentURI() {
|
||||
return state.documentURI;
|
||||
});
|
||||
defineGetter(Document.prototype, "compatMode", function getCompatMode() {
|
||||
return state.compatMode;
|
||||
});
|
||||
defineGetter(Document.prototype, "dir", function getDir() {
|
||||
return state.dir;
|
||||
});
|
||||
defineGetter(Document.prototype, "title", function getTitle() {
|
||||
return state.title;
|
||||
}, function setTitle(value) {
|
||||
state.title = String(value);
|
||||
});
|
||||
defineGetter(Document.prototype, "designMode", function getDesignMode() {
|
||||
return state.designMode;
|
||||
}, function setDesignMode(value) {
|
||||
state.designMode = String(value);
|
||||
});
|
||||
defineGetter(Document.prototype, "readyState", function getReadyState() {
|
||||
return state.readyState;
|
||||
});
|
||||
defineGetter(Document.prototype, "contentType", function getContentType() {
|
||||
return state.contentType;
|
||||
});
|
||||
defineGetter(Document.prototype, "inputEncoding", function getInputEncoding() {
|
||||
return state.inputEncoding;
|
||||
});
|
||||
defineGetter(Document.prototype, "domain", function getDomain() {
|
||||
return state.domain;
|
||||
});
|
||||
defineGetter(Document.prototype, "characterSet", function getCharacterSet() {
|
||||
return state.characterSet;
|
||||
});
|
||||
defineGetter(Document.prototype, "charset", function getCharset() {
|
||||
return state.charset;
|
||||
});
|
||||
defineGetter(Document.prototype, "hidden", function getHidden() {
|
||||
return !!state.hidden;
|
||||
});
|
||||
defineGetter(Document.prototype, "visibilityState", function getVisibilityState() {
|
||||
return state.visibilityState;
|
||||
});
|
||||
defineGetter(Document.prototype, "cookie", function getCookie() {
|
||||
return cookieValue;
|
||||
}, function setCookie(value) {
|
||||
cookieValue = cookieValue ? cookieValue + "; " + String(value) : String(value);
|
||||
});
|
||||
defineGetter(Document.prototype, "head", function getHead() {
|
||||
return headNode;
|
||||
});
|
||||
defineGetter(Document.prototype, "body", function getBody() {
|
||||
return bodyNode;
|
||||
});
|
||||
defineGetter(Document.prototype, "documentElement", function getDocumentElement() {
|
||||
return htmlNode;
|
||||
});
|
||||
defineGetter(Document.prototype, "defaultView", function getDefaultView() {
|
||||
return globalObject;
|
||||
});
|
||||
|
||||
defineValue(Document.prototype, "createElement", function createElement(tagName) {
|
||||
const element = createElementNode(tagName);
|
||||
element.ownerDocument = documentInstance;
|
||||
return element;
|
||||
});
|
||||
defineValue(Document.prototype, "querySelector", function querySelector(selector) {
|
||||
return queryNodes[selector] || null;
|
||||
});
|
||||
defineValue(Document.prototype, "getElementById", function getElementById(id) {
|
||||
return idNodes[id] || null;
|
||||
});
|
||||
defineValue(Document.prototype, "getElementsByTagName", function getElementsByTagName(tagName) {
|
||||
const key = String(tagName || "").toLowerCase();
|
||||
const list = tagNodes[key] || [];
|
||||
return list.slice();
|
||||
});
|
||||
|
||||
const documentAllFallback = {
|
||||
length: 0,
|
||||
item: function item() {
|
||||
return null;
|
||||
},
|
||||
namedItem: function namedItem() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
defineGetter(Document.prototype, "all", function getAll() {
|
||||
return documentAllFallback;
|
||||
});
|
||||
|
||||
if (protector && protector.repairFunctionMeta) {
|
||||
protector.repairFunctionMeta(Document, { name: "Document", length: 0 });
|
||||
protector.repairFunctionMeta(HTMLDocument, { name: "HTMLDocument", length: 0 });
|
||||
protector.repairFunctionMeta(Document.prototype.createElement, { name: "createElement", length: 1 });
|
||||
protector.repairFunctionMeta(Document.prototype.querySelector, { name: "querySelector", length: 1 });
|
||||
protector.repairFunctionMeta(Document.prototype.getElementById, { name: "getElementById", length: 1 });
|
||||
protector.repairFunctionMeta(Document.prototype.getElementsByTagName, { name: "getElementsByTagName", length: 1 });
|
||||
}
|
||||
if (protector && protector.protectFunction) {
|
||||
protector.protectFunction(Document, "Document");
|
||||
protector.protectFunction(HTMLDocument, "HTMLDocument");
|
||||
protector.protectFunction(Document.prototype.createElement, "createElement");
|
||||
protector.protectFunction(Document.prototype.querySelector, "querySelector");
|
||||
protector.protectFunction(Document.prototype.getElementById, "getElementById");
|
||||
protector.protectFunction(Document.prototype.getElementsByTagName, "getElementsByTagName");
|
||||
}
|
||||
|
||||
Object.defineProperty(globalObject, "Document", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: Document
|
||||
});
|
||||
Object.defineProperty(globalObject, "HTMLDocument", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: HTMLDocument
|
||||
});
|
||||
Object.defineProperty(globalObject, "document", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: documentInstance
|
||||
});
|
||||
|
||||
return documentInstance;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : window);
|
||||
`;
|
||||
}
|
||||
|
||||
function buildDocumentModule(seed = {}) {
|
||||
const module = scaffoldModule("document-module");
|
||||
const normalizedSeed = normalizeDocumentSeed(seed);
|
||||
|
||||
module.patchPlan = [
|
||||
"构建 Document / HTMLDocument 构造器、prototype 和 document 实例。",
|
||||
"把 document.cookie、URL、readyState 等高频字段补成访问器描述符。",
|
||||
"补出 createElement/querySelector/getElementById/getElementsByTagName,并为 canvas 入口预留到 fingerprint 模块的接线点。",
|
||||
];
|
||||
module.patchCode = renderDocumentPatch(normalizedSeed);
|
||||
module.runtimeState = normalizedSeed;
|
||||
module.validation = [
|
||||
"检查 document.URL / document.documentURI / document.readyState。",
|
||||
"检查 document.cookie getter/setter 是否正常工作。",
|
||||
"检查 document.createElement('canvas') 是否能接到 fingerprint 模块。",
|
||||
"检查 querySelector / getElementById / getElementsByTagName 的基本返回。",
|
||||
"检查 document.all 仍被标注为特殊兼容风险,而不是宣称完全等价。",
|
||||
];
|
||||
module.residualRisk = [
|
||||
"这里只覆盖静态 DOM 壳和常见查询入口,未实现完整 Node/Element/MutationObserver/CSSStyleDeclaration 系统。",
|
||||
"document.all 仅提供降级 fallback,无法在纯 JS 中完整等价原生特殊对象语义。",
|
||||
];
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildDocumentModule,
|
||||
normalizeDocumentSeed,
|
||||
};
|
||||
@@ -0,0 +1,228 @@
|
||||
const { scaffoldModule } = require("./scaffold-module");
|
||||
|
||||
function clone(value) {
|
||||
return JSON.parse(JSON.stringify(value || {}));
|
||||
}
|
||||
|
||||
function normalizeFingerprintSeed(seed = {}) {
|
||||
return {
|
||||
fingerprint: clone(seed.fingerprint || {}),
|
||||
screen: clone(seed.screen || {}),
|
||||
windowMetrics: clone(seed.windowMetrics || {}),
|
||||
battery: clone(seed.battery || {}),
|
||||
navigator: clone(seed.navigator || {}),
|
||||
};
|
||||
}
|
||||
|
||||
function renderFingerprintPatch(seed) {
|
||||
const serializedSeed = JSON.stringify(seed, null, 2);
|
||||
return `(
|
||||
function installFingerprintModule(globalObject) {
|
||||
const seed = ${serializedSeed};
|
||||
const protector = globalObject.__envNativeProtector || null;
|
||||
|
||||
function defineValue(target, key, value) {
|
||||
Object.defineProperty(target, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: value
|
||||
});
|
||||
}
|
||||
|
||||
function defineGetter(target, key, getter) {
|
||||
Object.defineProperty(target, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
get: getter
|
||||
});
|
||||
if (protector && protector.protectDescriptor) {
|
||||
protector.protectDescriptor(target, key);
|
||||
}
|
||||
}
|
||||
|
||||
function Screen() {}
|
||||
const screenState = Object.assign({
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
availWidth: 1920,
|
||||
availHeight: 1040,
|
||||
colorDepth: 24,
|
||||
pixelDepth: 24
|
||||
}, seed.screen || {});
|
||||
const screenInstance = Object.create(Screen.prototype);
|
||||
|
||||
["width", "height", "availWidth", "availHeight", "colorDepth", "pixelDepth", "availLeft", "availTop"].forEach(function eachKey(key) {
|
||||
defineGetter(Screen.prototype, key, function getScreenValue() {
|
||||
return screenState[key];
|
||||
});
|
||||
});
|
||||
|
||||
function createCanvas2DContext() {
|
||||
return {
|
||||
fillRect: function fillRect() {},
|
||||
clearRect: function clearRect() {},
|
||||
fillText: function fillText() {},
|
||||
measureText: function measureText(text) {
|
||||
return { width: String(text || "").length * 7.25 };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function WebGLRenderingContext() {}
|
||||
defineValue(WebGLRenderingContext.prototype, "getParameter", function getParameter(param) {
|
||||
if (param === 37445) {
|
||||
return seed.fingerprint.webglVendor || "Google Inc.";
|
||||
}
|
||||
if (param === 37446) {
|
||||
return seed.fingerprint.webglRenderer || "ANGLE";
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
function createWebGLContext() {
|
||||
return Object.create(WebGLRenderingContext.prototype);
|
||||
}
|
||||
|
||||
function HTMLCanvasElement() {
|
||||
this.width = 300;
|
||||
this.height = 150;
|
||||
this.__lastContextType__ = "";
|
||||
}
|
||||
|
||||
defineValue(HTMLCanvasElement.prototype, "getContext", function getContext(type) {
|
||||
this.__lastContextType__ = String(type || "");
|
||||
if (type === "2d") {
|
||||
return createCanvas2DContext();
|
||||
}
|
||||
if (type === "webgl" || type === "experimental-webgl") {
|
||||
return createWebGLContext();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
defineValue(HTMLCanvasElement.prototype, "toDataURL", function toDataURL() {
|
||||
if (this.__lastContextType__ === "webgl" || this.__lastContextType__ === "experimental-webgl") {
|
||||
return seed.fingerprint.canvasWebglDataUrl || seed.fingerprint.canvas2dDataUrl || "data:image/png;base64,";
|
||||
}
|
||||
return seed.fingerprint.canvas2dDataUrl || "data:image/png;base64,";
|
||||
});
|
||||
defineValue(HTMLCanvasElement.prototype, "toBlob", function toBlob(callback) {
|
||||
if (typeof callback === "function") {
|
||||
callback({
|
||||
type: "image/png",
|
||||
size: (seed.fingerprint.canvas2dDataUrl || "").length
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
defineValue(globalObject, "__envCreateCanvasElement", function __envCreateCanvasElement() {
|
||||
return new HTMLCanvasElement();
|
||||
});
|
||||
|
||||
if (globalObject.navigator) {
|
||||
const navigatorProto = Object.getPrototypeOf(globalObject.navigator);
|
||||
if (navigatorProto && !Object.getOwnPropertyDescriptor(navigatorProto, "getBattery")) {
|
||||
defineValue(navigatorProto, "getBattery", function getBattery() {
|
||||
return Promise.resolve(Object.assign({
|
||||
charging: true,
|
||||
chargingTime: 0,
|
||||
dischargingTime: Infinity,
|
||||
level: 1
|
||||
}, seed.battery || {}));
|
||||
});
|
||||
if (protector && protector.repairFunctionMeta) {
|
||||
protector.repairFunctionMeta(navigatorProto.getBattery, { name: "getBattery", length: 0 });
|
||||
}
|
||||
if (protector && protector.protectFunction) {
|
||||
protector.protectFunction(navigatorProto.getBattery, "getBattery");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(seed.windowMetrics || {}).forEach(function eachMetric(key) {
|
||||
Object.defineProperty(globalObject, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: seed.windowMetrics[key]
|
||||
});
|
||||
});
|
||||
|
||||
if (protector && protector.repairFunctionMeta) {
|
||||
protector.repairFunctionMeta(Screen, { name: "Screen", length: 0 });
|
||||
protector.repairFunctionMeta(WebGLRenderingContext, { name: "WebGLRenderingContext", length: 0 });
|
||||
protector.repairFunctionMeta(HTMLCanvasElement, { name: "HTMLCanvasElement", length: 0 });
|
||||
protector.repairFunctionMeta(HTMLCanvasElement.prototype.getContext, { name: "getContext", length: 1 });
|
||||
protector.repairFunctionMeta(HTMLCanvasElement.prototype.toDataURL, { name: "toDataURL", length: 0 });
|
||||
}
|
||||
if (protector && protector.protectFunction) {
|
||||
protector.protectFunction(Screen, "Screen");
|
||||
protector.protectFunction(WebGLRenderingContext, "WebGLRenderingContext");
|
||||
protector.protectFunction(HTMLCanvasElement, "HTMLCanvasElement");
|
||||
protector.protectFunction(HTMLCanvasElement.prototype.getContext, "getContext");
|
||||
protector.protectFunction(HTMLCanvasElement.prototype.toDataURL, "toDataURL");
|
||||
}
|
||||
|
||||
Object.defineProperty(globalObject, "Screen", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: Screen
|
||||
});
|
||||
Object.defineProperty(globalObject, "screen", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: screenInstance
|
||||
});
|
||||
Object.defineProperty(globalObject, "WebGLRenderingContext", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: WebGLRenderingContext
|
||||
});
|
||||
Object.defineProperty(globalObject, "HTMLCanvasElement", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: HTMLCanvasElement
|
||||
});
|
||||
|
||||
return {
|
||||
screen: screenInstance,
|
||||
HTMLCanvasElement: HTMLCanvasElement
|
||||
};
|
||||
})(typeof globalThis !== "undefined" ? globalThis : window);
|
||||
`;
|
||||
}
|
||||
|
||||
function buildFingerprintModule(seed = {}) {
|
||||
const module = scaffoldModule("fingerprint-module");
|
||||
const normalizedSeed = normalizeFingerprintSeed(seed);
|
||||
|
||||
module.patchPlan = [
|
||||
"构建 Screen、HTMLCanvasElement、WebGLRenderingContext 等高频指纹对象壳。",
|
||||
"把 canvas 2d / webgl 的入口统一到 getContext/toDataURL/getParameter。",
|
||||
"把 screen 尺寸、windowMetrics 和 battery Promise 一起收敛到统一指纹种子。",
|
||||
];
|
||||
module.patchCode = renderFingerprintPatch(normalizedSeed);
|
||||
module.runtimeState = normalizedSeed;
|
||||
module.validation = [
|
||||
"检查 screen.width / height / availWidth / availHeight。",
|
||||
"检查 document.createElement('canvas').getContext('2d') / getContext('webgl')。",
|
||||
"检查 canvas.toDataURL() 与 webgl getParameter(37445/37446)。",
|
||||
"检查 navigator.getBattery() 返回 Promise 且值来自种子。",
|
||||
"检查 devicePixelRatio / innerWidth / outerWidth 等窗口尺寸指标。",
|
||||
];
|
||||
module.residualRisk = [
|
||||
"这里只覆盖常见 canvas/WebGL/battery/screen 检测点,未实现音频指纹、字体指纹和 OffscreenCanvas。",
|
||||
"如果目标站点会校验扩展列表、着色器精度或更复杂的渲染链,仍需继续收集真实浏览器结果。",
|
||||
];
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildFingerprintModule,
|
||||
normalizeFingerprintSeed,
|
||||
};
|
||||
@@ -0,0 +1,261 @@
|
||||
const { scaffoldModule } = require("./scaffold-module");
|
||||
const { buildPluginGraph } = require("./build-plugin-graph");
|
||||
|
||||
function clone(value) {
|
||||
return JSON.parse(JSON.stringify(value || {}));
|
||||
}
|
||||
|
||||
function normalizeNavigatorSeed(seed = {}) {
|
||||
const navigatorSeed = seed.navigator && typeof seed.navigator === "object" ? seed.navigator : seed;
|
||||
const plugins = Array.isArray(seed.plugins) ? clone(seed.plugins) : [];
|
||||
const mimeTypes = Array.isArray(seed.mimeTypes) ? clone(seed.mimeTypes) : [];
|
||||
|
||||
return {
|
||||
navigator: {
|
||||
userAgent: navigatorSeed.userAgent || "",
|
||||
platform: navigatorSeed.platform || "",
|
||||
vendor: navigatorSeed.vendor || "",
|
||||
language: navigatorSeed.language || "",
|
||||
languages: Array.isArray(navigatorSeed.languages) ? navigatorSeed.languages.slice() : [],
|
||||
webdriver: typeof navigatorSeed.webdriver === "boolean" ? navigatorSeed.webdriver : false,
|
||||
hardwareConcurrency: Number.isFinite(navigatorSeed.hardwareConcurrency)
|
||||
? navigatorSeed.hardwareConcurrency
|
||||
: 8,
|
||||
deviceMemory: Number.isFinite(navigatorSeed.deviceMemory) ? navigatorSeed.deviceMemory : 8,
|
||||
maxTouchPoints: Number.isFinite(navigatorSeed.maxTouchPoints) ? navigatorSeed.maxTouchPoints : 0,
|
||||
cookieEnabled: navigatorSeed.cookieEnabled !== false,
|
||||
pdfViewerEnabled: navigatorSeed.pdfViewerEnabled !== false,
|
||||
appCodeName: navigatorSeed.appCodeName || "Mozilla",
|
||||
appName: navigatorSeed.appName || "Netscape",
|
||||
appVersion: navigatorSeed.appVersion || navigatorSeed.userAgent || "",
|
||||
userAgentData: clone(seed.userAgentData || navigatorSeed.userAgentData || null),
|
||||
connection: clone(seed.connection || navigatorSeed.connection || null),
|
||||
},
|
||||
plugins,
|
||||
mimeTypes,
|
||||
battery: clone(seed.battery || null),
|
||||
};
|
||||
}
|
||||
|
||||
function renderNavigatorPatch(seed) {
|
||||
const serializedSeed = JSON.stringify(seed, null, 2);
|
||||
return `(
|
||||
function installNavigatorModule(globalObject) {
|
||||
const seed = ${serializedSeed};
|
||||
const protector = globalObject.__envNativeProtector || null;
|
||||
const pluginSeed = Array.isArray(seed.plugins) ? seed.plugins.slice() : [];
|
||||
const mimeSeed = Array.isArray(seed.mimeTypes) ? seed.mimeTypes.slice() : [];
|
||||
|
||||
function defineValue(target, key, value) {
|
||||
Object.defineProperty(target, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: value
|
||||
});
|
||||
}
|
||||
|
||||
function defineGetter(target, key, getter) {
|
||||
Object.defineProperty(target, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
get: getter
|
||||
});
|
||||
if (protector && protector.protectDescriptor) {
|
||||
protector.protectDescriptor(target, key);
|
||||
}
|
||||
}
|
||||
|
||||
function createIndexedCollection(items, namedKey) {
|
||||
const collection = {};
|
||||
for (let index = 0; index < items.length; index += 1) {
|
||||
Object.defineProperty(collection, index, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: items[index]
|
||||
});
|
||||
if (items[index] && items[index][namedKey]) {
|
||||
Object.defineProperty(collection, items[index][namedKey], {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: items[index]
|
||||
});
|
||||
}
|
||||
}
|
||||
defineValue(collection, "length", items.length);
|
||||
defineValue(collection, "item", function item(index) {
|
||||
return items[index] || null;
|
||||
});
|
||||
defineValue(collection, "namedItem", function namedItem(name) {
|
||||
return items.find(function findItem(entry) {
|
||||
return entry && entry[namedKey] === name;
|
||||
}) || null;
|
||||
});
|
||||
defineValue(collection, "refresh", function refresh() {});
|
||||
return collection;
|
||||
}
|
||||
|
||||
const pluginArray = pluginSeed.map(function mapPlugin(plugin) {
|
||||
return {
|
||||
name: plugin.name || "",
|
||||
filename: plugin.filename || "",
|
||||
description: plugin.description || "",
|
||||
length: 0
|
||||
};
|
||||
});
|
||||
|
||||
const mimeTypeArray = mimeSeed.map(function mapMime(mime) {
|
||||
return {
|
||||
type: mime.type || "",
|
||||
suffixes: mime.suffixes || "",
|
||||
description: mime.description || "",
|
||||
enabledPlugin: null
|
||||
};
|
||||
});
|
||||
|
||||
for (let index = 0; index < mimeSeed.length; index += 1) {
|
||||
const pluginName = mimeSeed[index].enabledPluginName || "";
|
||||
const plugin = pluginArray.find(function findPlugin(entry) {
|
||||
return entry.name === pluginName;
|
||||
}) || null;
|
||||
mimeTypeArray[index].enabledPlugin = plugin;
|
||||
if (plugin) {
|
||||
const itemIndex = plugin.length;
|
||||
Object.defineProperty(plugin, itemIndex, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: mimeTypeArray[index]
|
||||
});
|
||||
plugin.length += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const plugins = createIndexedCollection(pluginArray, "name");
|
||||
const mimeTypes = createIndexedCollection(mimeTypeArray, "type");
|
||||
|
||||
function Navigator() {}
|
||||
const navigatorState = seed.navigator || {};
|
||||
const navigatorInstance = Object.create(Navigator.prototype);
|
||||
|
||||
const scalarKeys = [
|
||||
"userAgent",
|
||||
"platform",
|
||||
"vendor",
|
||||
"language",
|
||||
"webdriver",
|
||||
"hardwareConcurrency",
|
||||
"deviceMemory",
|
||||
"maxTouchPoints",
|
||||
"cookieEnabled",
|
||||
"pdfViewerEnabled",
|
||||
"appCodeName",
|
||||
"appName",
|
||||
"appVersion"
|
||||
];
|
||||
|
||||
scalarKeys.forEach(function defineScalar(key) {
|
||||
defineGetter(Navigator.prototype, key, function getScalar() {
|
||||
return navigatorState[key];
|
||||
});
|
||||
});
|
||||
|
||||
defineGetter(Navigator.prototype, "languages", function getLanguages() {
|
||||
return Array.isArray(navigatorState.languages) ? navigatorState.languages.slice() : [];
|
||||
});
|
||||
defineGetter(Navigator.prototype, "plugins", function getPlugins() {
|
||||
return plugins;
|
||||
});
|
||||
defineGetter(Navigator.prototype, "mimeTypes", function getMimeTypes() {
|
||||
return mimeTypes;
|
||||
});
|
||||
defineGetter(Navigator.prototype, "userAgentData", function getUserAgentData() {
|
||||
return navigatorState.userAgentData || null;
|
||||
});
|
||||
defineGetter(Navigator.prototype, "connection", function getConnection() {
|
||||
return navigatorState.connection || null;
|
||||
});
|
||||
|
||||
defineValue(Navigator.prototype, "javaEnabled", function javaEnabled() {
|
||||
return false;
|
||||
});
|
||||
defineValue(Navigator.prototype, "getBattery", function getBattery() {
|
||||
return Promise.resolve(seed.battery || {
|
||||
charging: true,
|
||||
chargingTime: 0,
|
||||
dischargingTime: Infinity,
|
||||
level: 1
|
||||
});
|
||||
});
|
||||
|
||||
if (protector && protector.repairFunctionMeta) {
|
||||
protector.repairFunctionMeta(Navigator, { name: "Navigator", length: 0 });
|
||||
protector.repairFunctionMeta(Navigator.prototype.javaEnabled, { name: "javaEnabled", length: 0 });
|
||||
protector.repairFunctionMeta(Navigator.prototype.getBattery, { name: "getBattery", length: 0 });
|
||||
}
|
||||
if (protector && protector.protectFunction) {
|
||||
protector.protectFunction(Navigator, "Navigator");
|
||||
protector.protectFunction(Navigator.prototype.javaEnabled, "javaEnabled");
|
||||
protector.protectFunction(Navigator.prototype.getBattery, "getBattery");
|
||||
}
|
||||
|
||||
defineValue(navigatorInstance, Symbol.toStringTag, "Navigator");
|
||||
Object.defineProperty(globalObject, "Navigator", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: Navigator
|
||||
});
|
||||
Object.defineProperty(globalObject, "navigator", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: navigatorInstance
|
||||
});
|
||||
|
||||
return navigatorInstance;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : window);
|
||||
`;
|
||||
}
|
||||
|
||||
function buildNavigatorModule(seed = {}) {
|
||||
const module = scaffoldModule("navigator-module");
|
||||
const normalizedSeed = normalizeNavigatorSeed(seed);
|
||||
const pluginGraph = buildPluginGraph({
|
||||
plugins: normalizedSeed.plugins,
|
||||
mimeTypes: normalizedSeed.mimeTypes,
|
||||
});
|
||||
|
||||
module.patchPlan = [
|
||||
"构建 Navigator 构造器、prototype 和 navigator 实例。",
|
||||
"把 userAgent/language/webdriver 等高频字段补成 prototype getter,而不是直接灌到实例上。",
|
||||
"把 plugins/mimeTypes 构造成类数组图结构,并补 getBattery/userAgentData 等高频检测点。",
|
||||
];
|
||||
module.patchCode = renderNavigatorPatch(normalizedSeed);
|
||||
module.runtimeState = {
|
||||
navigator: normalizedSeed.navigator,
|
||||
plugins: pluginGraph.pluginArray,
|
||||
mimeTypes: pluginGraph.mimeTypeArray,
|
||||
battery: normalizedSeed.battery,
|
||||
};
|
||||
module.validation = [
|
||||
"检查 navigator.webdriver === false。",
|
||||
"检查 navigator.plugins.length 与 navigator.mimeTypes.length。",
|
||||
"检查 navigator.plugins.item(0) / namedItem(name) 与 mimeTypes.enabledPlugin 关系。",
|
||||
"检查 navigator.getBattery() 返回 Promise。",
|
||||
"检查 Navigator.prototype 上 getter 的描述符与 toString 保护是否生效。",
|
||||
];
|
||||
module.residualRisk = [
|
||||
"这里只覆盖常见字段与图结构,未实现 permissions、mediaDevices、bluetooth 等更深层能力对象。",
|
||||
"如果目标脚本会校验 brands/mobile/platformVersion 等 userAgentData 细节,仍需真实浏览器种子。",
|
||||
];
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildNavigatorModule,
|
||||
normalizeNavigatorSeed,
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
const { scaffoldModule } = require("./scaffold-module");
|
||||
|
||||
function toFiniteNumber(value, fallback) {
|
||||
return Number.isFinite(value) ? value : fallback;
|
||||
}
|
||||
|
||||
function cloneRecord(value) {
|
||||
return Object.assign({}, value || {});
|
||||
}
|
||||
|
||||
function normalizePerformanceSeed(seed = {}) {
|
||||
const performanceSeed = seed.performance && typeof seed.performance === "object" ? seed.performance : seed;
|
||||
const navigationStart = toFiniteNumber(performanceSeed.navigationStart, Date.now());
|
||||
const timeOrigin = toFiniteNumber(performanceSeed.timeOrigin, navigationStart);
|
||||
const nowOffset = toFiniteNumber(performanceSeed.nowOffset, 12.345);
|
||||
const timing = cloneRecord(performanceSeed.timing);
|
||||
|
||||
if (!Number.isFinite(timing.navigationStart)) timing.navigationStart = navigationStart;
|
||||
if (!Number.isFinite(timing.fetchStart)) timing.fetchStart = timing.navigationStart;
|
||||
if (!Number.isFinite(timing.domainLookupStart)) timing.domainLookupStart = timing.fetchStart + 1;
|
||||
if (!Number.isFinite(timing.domainLookupEnd)) timing.domainLookupEnd = timing.domainLookupStart + 1;
|
||||
if (!Number.isFinite(timing.connectStart)) timing.connectStart = timing.domainLookupEnd;
|
||||
if (!Number.isFinite(timing.connectEnd)) timing.connectEnd = timing.connectStart + 2;
|
||||
if (!Number.isFinite(timing.requestStart)) timing.requestStart = timing.connectEnd + 1;
|
||||
if (!Number.isFinite(timing.responseStart)) timing.responseStart = timing.requestStart + 4;
|
||||
if (!Number.isFinite(timing.responseEnd)) timing.responseEnd = timing.responseStart + 4;
|
||||
if (!Number.isFinite(timing.domLoading)) timing.domLoading = timing.responseEnd + 2;
|
||||
if (!Number.isFinite(timing.domInteractive)) timing.domInteractive = timing.domLoading + 5;
|
||||
if (!Number.isFinite(timing.domContentLoadedEventStart)) timing.domContentLoadedEventStart = timing.domInteractive + 1;
|
||||
if (!Number.isFinite(timing.domContentLoadedEventEnd)) timing.domContentLoadedEventEnd = timing.domContentLoadedEventStart + 1;
|
||||
if (!Number.isFinite(timing.domComplete)) timing.domComplete = timing.domContentLoadedEventEnd + 5;
|
||||
if (!Number.isFinite(timing.loadEventStart)) timing.loadEventStart = timing.domComplete + 1;
|
||||
if (!Number.isFinite(timing.loadEventEnd)) timing.loadEventEnd = timing.loadEventStart + 1;
|
||||
|
||||
return {
|
||||
timeOrigin,
|
||||
navigationStart,
|
||||
nowOffset,
|
||||
timing,
|
||||
memory: cloneRecord(performanceSeed.memory),
|
||||
};
|
||||
}
|
||||
|
||||
function renderPerformancePatch(seed) {
|
||||
const serializedSeed = JSON.stringify(seed, null, 2);
|
||||
return `(
|
||||
function installPerformanceModule(globalObject) {
|
||||
const seed = ${serializedSeed};
|
||||
const protector = globalObject.__envNativeProtector || null;
|
||||
const baseEpoch = Date.now();
|
||||
const monotonicStart = seed.nowOffset;
|
||||
let tick = 0;
|
||||
|
||||
function nextNow() {
|
||||
tick += 1;
|
||||
return monotonicStart + (Date.now() - baseEpoch) + tick * 0.01;
|
||||
}
|
||||
|
||||
function now() {
|
||||
return Number(nextNow().toFixed(3));
|
||||
}
|
||||
|
||||
function clearMarks() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function clearMeasures() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getEntries() {
|
||||
return [];
|
||||
}
|
||||
|
||||
function getEntriesByType() {
|
||||
return [];
|
||||
}
|
||||
|
||||
function getEntriesByName() {
|
||||
return [];
|
||||
}
|
||||
|
||||
const performanceObject = {
|
||||
now: now,
|
||||
timeOrigin: seed.timeOrigin,
|
||||
timing: seed.timing,
|
||||
clearMarks: clearMarks,
|
||||
clearMeasures: clearMeasures,
|
||||
getEntries: getEntries,
|
||||
getEntriesByType: getEntriesByType,
|
||||
getEntriesByName: getEntriesByName,
|
||||
mark: function mark() {},
|
||||
measure: function measure() {}
|
||||
};
|
||||
|
||||
if (seed.memory && Object.keys(seed.memory).length) {
|
||||
performanceObject.memory = seed.memory;
|
||||
}
|
||||
|
||||
if (protector && protector.repairFunctionMeta) {
|
||||
protector.repairFunctionMeta(now, { name: "now", length: 0 });
|
||||
protector.repairFunctionMeta(clearMarks, { name: "clearMarks", length: 0 });
|
||||
protector.repairFunctionMeta(clearMeasures, { name: "clearMeasures", length: 0 });
|
||||
protector.repairFunctionMeta(getEntries, { name: "getEntries", length: 0 });
|
||||
protector.repairFunctionMeta(getEntriesByType, { name: "getEntriesByType", length: 1 });
|
||||
protector.repairFunctionMeta(getEntriesByName, { name: "getEntriesByName", length: 1 });
|
||||
}
|
||||
if (protector && protector.protectFunction) {
|
||||
protector.protectFunction(now, "now");
|
||||
protector.protectFunction(clearMarks, "clearMarks");
|
||||
protector.protectFunction(clearMeasures, "clearMeasures");
|
||||
protector.protectFunction(getEntries, "getEntries");
|
||||
protector.protectFunction(getEntriesByType, "getEntriesByType");
|
||||
protector.protectFunction(getEntriesByName, "getEntriesByName");
|
||||
}
|
||||
|
||||
Object.defineProperty(globalObject, "performance", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: performanceObject
|
||||
});
|
||||
|
||||
return performanceObject;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : window);
|
||||
`;
|
||||
}
|
||||
|
||||
function buildPerformanceModule(seed = {}) {
|
||||
const module = scaffoldModule("performance-module");
|
||||
const normalizedSeed = normalizePerformanceSeed(seed);
|
||||
|
||||
module.patchPlan = [
|
||||
"构建 timeOrigin/navigationStart/timing 基础树。",
|
||||
"补出单调递增的 performance.now()。",
|
||||
"为 getEntries/getEntriesByType/getEntriesByName 等常见检测点提供稳定返回。",
|
||||
];
|
||||
module.patchCode = renderPerformancePatch(normalizedSeed);
|
||||
module.runtimeState = {
|
||||
performance: normalizedSeed,
|
||||
};
|
||||
module.validation = [
|
||||
"连续两次调用 performance.now(),确认返回值递增。",
|
||||
"检查 performance.timeOrigin 与 performance.timing.navigationStart 的相对关系。",
|
||||
"检查 performance.getEntries()/getEntriesByType()/getEntriesByName() 返回数组。",
|
||||
"检查 performance.now.toString() 以及 name/length 元信息。",
|
||||
];
|
||||
module.residualRisk = [
|
||||
"未实现 PerformanceObserver、resource timing、navigation entries 等复杂链路。",
|
||||
"如果目标脚本会校验 performance.memory 或多阶段时间戳分布,仍需采集真实浏览器种子。",
|
||||
];
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildPerformanceModule,
|
||||
normalizePerformanceSeed,
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
function buildPluginGraph(seed = {}) {
|
||||
const plugins = Array.isArray(seed.plugins) ? seed.plugins : [];
|
||||
const mimeTypes = Array.isArray(seed.mimeTypes) ? seed.mimeTypes : [];
|
||||
|
||||
const pluginArray = [];
|
||||
const mimeTypeArray = [];
|
||||
|
||||
for (const pluginSeed of plugins) {
|
||||
const plugin = {
|
||||
name: pluginSeed.name || "",
|
||||
filename: pluginSeed.filename || "",
|
||||
description: pluginSeed.description || "",
|
||||
length: 0,
|
||||
items: [],
|
||||
};
|
||||
pluginArray.push(plugin);
|
||||
}
|
||||
|
||||
for (const mimeSeed of mimeTypes) {
|
||||
const mime = {
|
||||
type: mimeSeed.type || "",
|
||||
suffixes: mimeSeed.suffixes || "",
|
||||
description: mimeSeed.description || "",
|
||||
enabledPluginName: mimeSeed.enabledPluginName || "",
|
||||
enabledPlugin: null,
|
||||
};
|
||||
mimeTypeArray.push(mime);
|
||||
}
|
||||
|
||||
for (const mime of mimeTypeArray) {
|
||||
const plugin = pluginArray.find((item) => item.name === mime.enabledPluginName);
|
||||
if (plugin) {
|
||||
mime.enabledPlugin = plugin;
|
||||
plugin.items.push(mime);
|
||||
plugin.length = plugin.items.length;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pluginArray,
|
||||
mimeTypeArray,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildPluginGraph,
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
const { scaffoldModule } = require("./scaffold-module");
|
||||
const { mergeStorageState } = require("./merge-storage-state");
|
||||
|
||||
function normalizeStorageSeed(seed = {}) {
|
||||
const merged = mergeStorageState(seed);
|
||||
return {
|
||||
cookie: seed.cookie || "",
|
||||
cookies: merged.cookies,
|
||||
localStorage: merged.localStorage,
|
||||
sessionStorage: merged.sessionStorage,
|
||||
};
|
||||
}
|
||||
|
||||
function renderStoragePatch(seed) {
|
||||
const serializedSeed = JSON.stringify(seed, null, 2);
|
||||
return `(
|
||||
function installStorageModule(globalObject) {
|
||||
const seed = ${serializedSeed};
|
||||
const protector = globalObject.__envNativeProtector || null;
|
||||
|
||||
function defineValue(target, key, value) {
|
||||
Object.defineProperty(target, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: value
|
||||
});
|
||||
}
|
||||
|
||||
function Storage() {}
|
||||
|
||||
defineValue(Storage.prototype, "key", function key(index) {
|
||||
const keys = Object.keys(this.__store__);
|
||||
return typeof keys[index] === "string" ? keys[index] : null;
|
||||
});
|
||||
defineValue(Storage.prototype, "getItem", function getItem(name) {
|
||||
const key = String(name);
|
||||
return Object.prototype.hasOwnProperty.call(this.__store__, key) ? this.__store__[key] : null;
|
||||
});
|
||||
defineValue(Storage.prototype, "setItem", function setItem(name, value) {
|
||||
this.__store__[String(name)] = String(value);
|
||||
this.length = Object.keys(this.__store__).length;
|
||||
});
|
||||
defineValue(Storage.prototype, "removeItem", function removeItem(name) {
|
||||
delete this.__store__[String(name)];
|
||||
this.length = Object.keys(this.__store__).length;
|
||||
});
|
||||
defineValue(Storage.prototype, "clear", function clear() {
|
||||
this.__store__ = Object.create(null);
|
||||
this.length = 0;
|
||||
});
|
||||
|
||||
function createStorage(initialState) {
|
||||
const storage = Object.create(Storage.prototype);
|
||||
defineValue(storage, "__store__", Object.assign(Object.create(null), initialState || {}));
|
||||
defineValue(storage, "length", Object.keys(storage.__store__).length);
|
||||
Object.keys(storage.__store__).forEach(function defineEntry(key) {
|
||||
Object.defineProperty(storage, key, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: function getEntry() {
|
||||
return storage.getItem(key);
|
||||
},
|
||||
set: function setEntry(value) {
|
||||
storage.setItem(key, value);
|
||||
}
|
||||
});
|
||||
});
|
||||
return storage;
|
||||
}
|
||||
|
||||
if (protector && protector.repairFunctionMeta) {
|
||||
protector.repairFunctionMeta(Storage, { name: "Storage", length: 0 });
|
||||
protector.repairFunctionMeta(Storage.prototype.key, { name: "key", length: 1 });
|
||||
protector.repairFunctionMeta(Storage.prototype.getItem, { name: "getItem", length: 1 });
|
||||
protector.repairFunctionMeta(Storage.prototype.setItem, { name: "setItem", length: 2 });
|
||||
protector.repairFunctionMeta(Storage.prototype.removeItem, { name: "removeItem", length: 1 });
|
||||
protector.repairFunctionMeta(Storage.prototype.clear, { name: "clear", length: 0 });
|
||||
}
|
||||
if (protector && protector.protectFunction) {
|
||||
protector.protectFunction(Storage, "Storage");
|
||||
protector.protectFunction(Storage.prototype.key, "key");
|
||||
protector.protectFunction(Storage.prototype.getItem, "getItem");
|
||||
protector.protectFunction(Storage.prototype.setItem, "setItem");
|
||||
protector.protectFunction(Storage.prototype.removeItem, "removeItem");
|
||||
protector.protectFunction(Storage.prototype.clear, "clear");
|
||||
}
|
||||
|
||||
const localStorage = createStorage(seed.localStorage || {});
|
||||
const sessionStorage = createStorage(seed.sessionStorage || {});
|
||||
|
||||
Object.defineProperty(globalObject, "Storage", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: Storage
|
||||
});
|
||||
Object.defineProperty(globalObject, "localStorage", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: localStorage
|
||||
});
|
||||
Object.defineProperty(globalObject, "sessionStorage", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: sessionStorage
|
||||
});
|
||||
|
||||
if (globalObject.document && typeof globalObject.document.cookie === "string" && seed.cookie) {
|
||||
globalObject.document.cookie = seed.cookie;
|
||||
}
|
||||
|
||||
return {
|
||||
localStorage: localStorage,
|
||||
sessionStorage: sessionStorage
|
||||
};
|
||||
})(typeof globalThis !== "undefined" ? globalThis : window);
|
||||
`;
|
||||
}
|
||||
|
||||
function buildStorageModule(seed = {}) {
|
||||
const module = scaffoldModule("storage-module");
|
||||
const normalizedSeed = normalizeStorageSeed(seed);
|
||||
|
||||
module.patchPlan = [
|
||||
"构建 Storage 构造器与原型方法。",
|
||||
"按真实种子预置 localStorage / sessionStorage 的 key/value 运行态。",
|
||||
"把 cookie 字符串与 document.cookie 入口联动,而不是把 storage 状态和 cookie 割裂开。",
|
||||
];
|
||||
module.patchCode = renderStoragePatch(normalizedSeed);
|
||||
module.runtimeState = normalizedSeed;
|
||||
module.validation = [
|
||||
"检查 localStorage.length / sessionStorage.length。",
|
||||
"检查 getItem/setItem/removeItem/clear/key 的返回值。",
|
||||
"检查 localStorage.device_id 这类同名属性访问是否与 getItem 一致。",
|
||||
"检查 document 已存在时 cookie 字符串能否同步注入。",
|
||||
];
|
||||
module.residualRisk = [
|
||||
"未实现配额异常、跨 tab 事件同步与 storage event。",
|
||||
"如果站点依赖历史态的更新时间、写入顺序或事件广播,仍需继续扩展。",
|
||||
];
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildStorageModule,
|
||||
normalizeStorageSeed,
|
||||
};
|
||||
@@ -0,0 +1,191 @@
|
||||
const { scaffoldModule } = require("./scaffold-module");
|
||||
|
||||
function normalizeRtcCandidate(candidate = {}) {
|
||||
return {
|
||||
candidate: candidate.candidate || "candidate:842163049 1 udp 1677729535 0.0.0.0 9 typ host generation 0 ufrag env network-id 1",
|
||||
sdpMid: candidate.sdpMid || "0",
|
||||
sdpMLineIndex: Number.isFinite(candidate.sdpMLineIndex) ? candidate.sdpMLineIndex : 0,
|
||||
usernameFragment: candidate.usernameFragment || "env",
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeWebrtcSeed(seed = {}) {
|
||||
const webrtcSeed = seed.webrtc && typeof seed.webrtc === "object" ? seed.webrtc : seed;
|
||||
return {
|
||||
allowCandidateLeak: webrtcSeed.allowCandidateLeak === true,
|
||||
iceTransportPolicy: webrtcSeed.iceTransportPolicy || "all",
|
||||
bundlePolicy: webrtcSeed.bundlePolicy || "balanced",
|
||||
rtcpMuxPolicy: webrtcSeed.rtcpMuxPolicy || "require",
|
||||
defaultOfferSdp: webrtcSeed.defaultOfferSdp || "v=0\\r\\no=- 0 0 IN IP4 127.0.0.1\\r\\ns=env-codex-offer\\r\\nt=0 0\\r\\n",
|
||||
defaultAnswerSdp: webrtcSeed.defaultAnswerSdp || "v=0\\r\\no=- 0 0 IN IP4 127.0.0.1\\r\\ns=env-codex-answer\\r\\nt=0 0\\r\\n",
|
||||
iceCandidates: Array.isArray(webrtcSeed.iceCandidates) && webrtcSeed.iceCandidates.length
|
||||
? webrtcSeed.iceCandidates.map(normalizeRtcCandidate)
|
||||
: [normalizeRtcCandidate()],
|
||||
};
|
||||
}
|
||||
|
||||
function renderWebrtcPatch(seed) {
|
||||
const serializedSeed = JSON.stringify(seed, null, 2);
|
||||
return `(
|
||||
function installWebrtcModule(globalObject) {
|
||||
const seed = ${serializedSeed};
|
||||
const protector = globalObject.__envNativeProtector || null;
|
||||
|
||||
function defineValue(target, key, value) {
|
||||
Object.defineProperty(target, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: value
|
||||
});
|
||||
}
|
||||
|
||||
function applyEventTarget(target) {
|
||||
target._listeners = Object.create(null);
|
||||
target.addEventListener = function addEventListener(type, listener) {
|
||||
if (!this._listeners[type]) this._listeners[type] = [];
|
||||
this._listeners[type].push(listener);
|
||||
};
|
||||
target.removeEventListener = function removeEventListener(type, listener) {
|
||||
const list = this._listeners[type] || [];
|
||||
this._listeners[type] = list.filter(function filterItem(item) {
|
||||
return item !== listener;
|
||||
});
|
||||
};
|
||||
target.dispatchEvent = function dispatchEvent(event) {
|
||||
const list = (this._listeners[event.type] || []).slice();
|
||||
for (const listener of list) listener.call(this, event);
|
||||
const handler = this["on" + event.type];
|
||||
if (typeof handler === "function") handler.call(this, event);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
function RTCSessionDescription(init) {
|
||||
init = init || {};
|
||||
this.type = init.type || "offer";
|
||||
this.sdp = init.sdp || "";
|
||||
}
|
||||
|
||||
function RTCIceCandidate(init) {
|
||||
init = init || {};
|
||||
this.candidate = init.candidate || "";
|
||||
this.sdpMid = init.sdpMid || null;
|
||||
this.sdpMLineIndex = typeof init.sdpMLineIndex === "number" ? init.sdpMLineIndex : null;
|
||||
this.usernameFragment = init.usernameFragment || null;
|
||||
}
|
||||
|
||||
function RTCDataChannel(label, dataChannelDict) {
|
||||
applyEventTarget(this);
|
||||
this.label = label || "";
|
||||
this.ordered = !dataChannelDict || dataChannelDict.ordered !== false;
|
||||
this.readyState = "open";
|
||||
}
|
||||
|
||||
RTCDataChannel.prototype.send = function send(data) {
|
||||
this.lastPayload = data;
|
||||
};
|
||||
RTCDataChannel.prototype.close = function close() {
|
||||
this.readyState = "closed";
|
||||
};
|
||||
|
||||
function RTCPeerConnection(configuration) {
|
||||
applyEventTarget(this);
|
||||
this.configuration = Object.assign({
|
||||
iceTransportPolicy: seed.iceTransportPolicy,
|
||||
bundlePolicy: seed.bundlePolicy,
|
||||
rtcpMuxPolicy: seed.rtcpMuxPolicy
|
||||
}, configuration || {});
|
||||
this.localDescription = null;
|
||||
this.remoteDescription = null;
|
||||
this.connectionState = "new";
|
||||
this.iceConnectionState = "new";
|
||||
this.signalingState = "stable";
|
||||
}
|
||||
|
||||
RTCPeerConnection.prototype.createOffer = function createOffer() {
|
||||
return Promise.resolve(new RTCSessionDescription({ type: "offer", sdp: seed.defaultOfferSdp }));
|
||||
};
|
||||
RTCPeerConnection.prototype.createAnswer = function createAnswer() {
|
||||
return Promise.resolve(new RTCSessionDescription({ type: "answer", sdp: seed.defaultAnswerSdp }));
|
||||
};
|
||||
RTCPeerConnection.prototype.setLocalDescription = function setLocalDescription(description) {
|
||||
this.localDescription = description || null;
|
||||
this.signalingState = "have-local-offer";
|
||||
if (seed.allowCandidateLeak) {
|
||||
for (const candidateSeed of seed.iceCandidates) {
|
||||
this.dispatchEvent({ type: "icecandidate", candidate: new RTCIceCandidate(candidateSeed) });
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
RTCPeerConnection.prototype.setRemoteDescription = function setRemoteDescription(description) {
|
||||
this.remoteDescription = description || null;
|
||||
this.signalingState = "stable";
|
||||
return Promise.resolve();
|
||||
};
|
||||
RTCPeerConnection.prototype.addIceCandidate = function addIceCandidate(candidate) {
|
||||
this.lastIceCandidate = candidate || null;
|
||||
return Promise.resolve();
|
||||
};
|
||||
RTCPeerConnection.prototype.createDataChannel = function createDataChannel(label, dataChannelDict) {
|
||||
return new RTCDataChannel(label, dataChannelDict);
|
||||
};
|
||||
RTCPeerConnection.prototype.close = function close() {
|
||||
this.connectionState = "closed";
|
||||
this.iceConnectionState = "closed";
|
||||
};
|
||||
|
||||
if (protector && protector.repairFunctionMeta) {
|
||||
protector.repairFunctionMeta(RTCSessionDescription, { name: "RTCSessionDescription", length: 1 });
|
||||
protector.repairFunctionMeta(RTCIceCandidate, { name: "RTCIceCandidate", length: 1 });
|
||||
protector.repairFunctionMeta(RTCPeerConnection, { name: "RTCPeerConnection", length: 1 });
|
||||
protector.repairFunctionMeta(RTCDataChannel, { name: "RTCDataChannel", length: 2 });
|
||||
}
|
||||
if (protector && protector.protectFunction) {
|
||||
protector.protectFunction(RTCSessionDescription, "RTCSessionDescription");
|
||||
protector.protectFunction(RTCIceCandidate, "RTCIceCandidate");
|
||||
protector.protectFunction(RTCPeerConnection, "RTCPeerConnection");
|
||||
protector.protectFunction(RTCDataChannel, "RTCDataChannel");
|
||||
}
|
||||
|
||||
defineValue(globalObject, "RTCSessionDescription", RTCSessionDescription);
|
||||
defineValue(globalObject, "RTCIceCandidate", RTCIceCandidate);
|
||||
defineValue(globalObject, "RTCPeerConnection", RTCPeerConnection);
|
||||
defineValue(globalObject, "webkitRTCPeerConnection", RTCPeerConnection);
|
||||
defineValue(globalObject, "RTCDataChannel", RTCDataChannel);
|
||||
|
||||
return { RTCPeerConnection: RTCPeerConnection, RTCDataChannel: RTCDataChannel };
|
||||
})(typeof globalThis !== "undefined" ? globalThis : window);
|
||||
`;
|
||||
}
|
||||
|
||||
function buildWebrtcModule(seed = {}) {
|
||||
const module = scaffoldModule("webrtc-module");
|
||||
const normalizedSeed = normalizeWebrtcSeed(seed);
|
||||
|
||||
module.patchPlan = [
|
||||
"补出 RTCPeerConnection / RTCSessionDescription / RTCIceCandidate / RTCDataChannel 构造器。",
|
||||
"为 createOffer/createAnswer/setLocalDescription/addIceCandidate/createDataChannel 提供 Promise 型基础实现。",
|
||||
"将 ICE candidate 是否泄露做成显式策略,而不是默认暴露本地网络信息。",
|
||||
];
|
||||
module.patchCode = renderWebrtcPatch(normalizedSeed);
|
||||
module.runtimeState = { webrtc: normalizedSeed };
|
||||
module.validation = [
|
||||
"检查 typeof RTCPeerConnection === 'function'。",
|
||||
"执行 new RTCPeerConnection().createOffer(),确认返回 Promise。",
|
||||
"执行 createDataChannel('env'),确认返回对象且包含 send/close。",
|
||||
"在 allowCandidateLeak=false 时确认不会主动抛出真实候选信息。",
|
||||
];
|
||||
module.residualRisk = [
|
||||
"这里只覆盖存在性检测和轻量行为检测,不提供真实音视频协商能力。",
|
||||
"若目标站点会校验 stats/transceiver/sender/receiver/track,仍需按真实浏览器继续扩展。",
|
||||
];
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildWebrtcModule,
|
||||
normalizeWebrtcSeed,
|
||||
};
|
||||
@@ -0,0 +1,195 @@
|
||||
const { scaffoldModule } = require("./scaffold-module");
|
||||
|
||||
function normalizeWorkerSeed(seed = {}) {
|
||||
const workerSeed = seed.worker && typeof seed.worker === "object" ? seed.worker : seed;
|
||||
return {
|
||||
scriptBaseUrl: workerSeed.scriptBaseUrl || "https://example.invalid/assets/",
|
||||
inlineScripts: Object.assign({}, workerSeed.inlineScripts || {}),
|
||||
sharedWorkerName: workerSeed.sharedWorkerName || "env-shared-worker",
|
||||
broadcastChannels: Array.isArray(workerSeed.broadcastChannels)
|
||||
? workerSeed.broadcastChannels.slice()
|
||||
: ["env-sync"],
|
||||
};
|
||||
}
|
||||
|
||||
function renderWorkerPatch(seed) {
|
||||
const serializedSeed = JSON.stringify(seed, null, 2);
|
||||
return `(
|
||||
function installWorkerModule(globalObject) {
|
||||
const seed = ${serializedSeed};
|
||||
const protector = globalObject.__envNativeProtector || null;
|
||||
const sharedChannels = Object.create(null);
|
||||
|
||||
function applyEventTarget(target) {
|
||||
target._listeners = Object.create(null);
|
||||
target.addEventListener = function addEventListener(type, listener) {
|
||||
if (!this._listeners[type]) this._listeners[type] = [];
|
||||
this._listeners[type].push(listener);
|
||||
};
|
||||
target.removeEventListener = function removeEventListener(type, listener) {
|
||||
const list = this._listeners[type] || [];
|
||||
this._listeners[type] = list.filter(function filterItem(item) {
|
||||
return item !== listener;
|
||||
});
|
||||
};
|
||||
target.dispatchEvent = function dispatchEvent(event) {
|
||||
const list = (this._listeners[event.type] || []).slice();
|
||||
for (const listener of list) listener.call(this, event);
|
||||
const handler = this["on" + event.type];
|
||||
if (typeof handler === "function") handler.call(this, event);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
function createMessageEvent(data) {
|
||||
return { type: "message", data: data };
|
||||
}
|
||||
|
||||
function MessagePort() {
|
||||
applyEventTarget(this);
|
||||
this._entangledPort = null;
|
||||
this._started = false;
|
||||
}
|
||||
|
||||
MessagePort.prototype.postMessage = function postMessage(data) {
|
||||
if (this._entangledPort) this._entangledPort.dispatchEvent(createMessageEvent(data));
|
||||
};
|
||||
MessagePort.prototype.start = function start() {
|
||||
this._started = true;
|
||||
};
|
||||
MessagePort.prototype.close = function close() {
|
||||
this._entangledPort = null;
|
||||
this._started = false;
|
||||
};
|
||||
|
||||
function createMessageChannel() {
|
||||
const port1 = new MessagePort();
|
||||
const port2 = new MessagePort();
|
||||
port1._entangledPort = port2;
|
||||
port2._entangledPort = port1;
|
||||
return { port1: port1, port2: port2 };
|
||||
}
|
||||
|
||||
function Worker(scriptUrl) {
|
||||
applyEventTarget(this);
|
||||
this.scriptURL = scriptUrl || "";
|
||||
this.baseUrl = seed.scriptBaseUrl;
|
||||
this.inlineSource = seed.inlineScripts[scriptUrl] || "";
|
||||
}
|
||||
|
||||
Worker.prototype.postMessage = function postMessage(data) {
|
||||
this.dispatchEvent(createMessageEvent(data));
|
||||
};
|
||||
Worker.prototype.terminate = function terminate() {
|
||||
this._terminated = true;
|
||||
};
|
||||
|
||||
function SharedWorker(scriptUrl, name) {
|
||||
applyEventTarget(this);
|
||||
this.scriptURL = scriptUrl || "";
|
||||
this.name = name || seed.sharedWorkerName;
|
||||
const channel = createMessageChannel();
|
||||
this.port = channel.port1;
|
||||
this._workerPort = channel.port2;
|
||||
}
|
||||
|
||||
function BroadcastChannel(name) {
|
||||
applyEventTarget(this);
|
||||
this.name = name || "";
|
||||
if (!sharedChannels[this.name]) sharedChannels[this.name] = [];
|
||||
sharedChannels[this.name].push(this);
|
||||
}
|
||||
|
||||
BroadcastChannel.prototype.postMessage = function postMessage(data) {
|
||||
const peers = sharedChannels[this.name] || [];
|
||||
for (const peer of peers) {
|
||||
if (peer !== this) peer.dispatchEvent(createMessageEvent(data));
|
||||
}
|
||||
};
|
||||
BroadcastChannel.prototype.close = function close() {
|
||||
const peers = sharedChannels[this.name] || [];
|
||||
sharedChannels[this.name] = peers.filter(function filterPeer(item) {
|
||||
return item !== this;
|
||||
}, this);
|
||||
};
|
||||
|
||||
if (protector && protector.repairFunctionMeta) {
|
||||
protector.repairFunctionMeta(MessagePort, { name: "MessagePort", length: 0 });
|
||||
protector.repairFunctionMeta(Worker, { name: "Worker", length: 1 });
|
||||
protector.repairFunctionMeta(SharedWorker, { name: "SharedWorker", length: 2 });
|
||||
protector.repairFunctionMeta(BroadcastChannel, { name: "BroadcastChannel", length: 1 });
|
||||
}
|
||||
if (protector && protector.protectFunction) {
|
||||
protector.protectFunction(MessagePort, "MessagePort");
|
||||
protector.protectFunction(Worker, "Worker");
|
||||
protector.protectFunction(SharedWorker, "SharedWorker");
|
||||
protector.protectFunction(BroadcastChannel, "BroadcastChannel");
|
||||
}
|
||||
|
||||
Object.defineProperty(globalObject, "MessagePort", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: MessagePort
|
||||
});
|
||||
Object.defineProperty(globalObject, "MessageChannel", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: function MessageChannel() {
|
||||
return createMessageChannel();
|
||||
}
|
||||
});
|
||||
Object.defineProperty(globalObject, "Worker", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: Worker
|
||||
});
|
||||
Object.defineProperty(globalObject, "SharedWorker", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: SharedWorker
|
||||
});
|
||||
Object.defineProperty(globalObject, "BroadcastChannel", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: BroadcastChannel
|
||||
});
|
||||
|
||||
return { Worker: Worker, SharedWorker: SharedWorker, BroadcastChannel: BroadcastChannel };
|
||||
})(typeof globalThis !== "undefined" ? globalThis : window);
|
||||
`;
|
||||
}
|
||||
|
||||
function buildWorkerModule(seed = {}) {
|
||||
const module = scaffoldModule("worker-module");
|
||||
const normalizedSeed = normalizeWorkerSeed(seed);
|
||||
|
||||
module.patchPlan = [
|
||||
"补出 Worker/SharedWorker/MessagePort/MessageChannel/BroadcastChannel 构造器。",
|
||||
"用最小事件系统模拟 postMessage/onmessage 通路。",
|
||||
"把 worker 脚本路径、inline source、广播频道名称外置为种子,而不是写死在补丁里。",
|
||||
];
|
||||
module.patchCode = renderWorkerPatch(normalizedSeed);
|
||||
module.runtimeState = { worker: normalizedSeed };
|
||||
module.validation = [
|
||||
"检查 typeof Worker/SharedWorker/BroadcastChannel === 'function'。",
|
||||
"执行 new Worker('/worker.js').postMessage({ping:1}),确认 message 事件能被触发。",
|
||||
"执行 new SharedWorker('/shared.js').port.postMessage('x'),确认端口存在。",
|
||||
"执行两个同名 BroadcastChannel 互发消息,确认消息广播通路正常。",
|
||||
];
|
||||
module.residualRisk = [
|
||||
"这里只模拟消息通路,不提供真实线程、importScripts、URL.createObjectURL 等完整能力。",
|
||||
"若目标站点依赖 Worker 全局作用域、自定义事件循环或二进制消息通路,仍需继续扩展。",
|
||||
];
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildWorkerModule,
|
||||
normalizeWorkerSeed,
|
||||
};
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
Browser-side environment collector template.
|
||||
Reconstructed from public env-harvesting patterns, especially boda_jsEnv's
|
||||
"changeDom.js", but normalized into a structured JSON exporter.
|
||||
*/
|
||||
|
||||
(function collectBrowserEnv() {
|
||||
function safeCall(fn, fallback) {
|
||||
try {
|
||||
return fn();
|
||||
} catch (err) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function dumpStorage(storage) {
|
||||
const out = {};
|
||||
if (!storage) {
|
||||
return out;
|
||||
}
|
||||
for (let i = 0; i < storage.length; i += 1) {
|
||||
const key = storage.key(i);
|
||||
out[key] = storage.getItem(key);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function dumpNavigator() {
|
||||
return {
|
||||
userAgent: safeCall(() => navigator.userAgent, ""),
|
||||
platform: safeCall(() => navigator.platform, ""),
|
||||
vendor: safeCall(() => navigator.vendor, ""),
|
||||
language: safeCall(() => navigator.language, ""),
|
||||
languages: safeCall(() => Array.from(navigator.languages || []), []),
|
||||
webdriver: safeCall(() => navigator.webdriver, undefined),
|
||||
hardwareConcurrency: safeCall(() => navigator.hardwareConcurrency, undefined),
|
||||
deviceMemory: safeCall(() => navigator.deviceMemory, undefined),
|
||||
maxTouchPoints: safeCall(() => navigator.maxTouchPoints, undefined),
|
||||
cookieEnabled: safeCall(() => navigator.cookieEnabled, undefined),
|
||||
pdfViewerEnabled: safeCall(() => navigator.pdfViewerEnabled, undefined),
|
||||
userAgentData: safeCall(() => navigator.userAgentData, null),
|
||||
connection: safeCall(() => navigator.connection, null),
|
||||
webkitPersistentStorage: safeCall(() => navigator.webkitPersistentStorage, null),
|
||||
webkitTemporaryStorage: safeCall(() => navigator.webkitTemporaryStorage, null),
|
||||
};
|
||||
}
|
||||
|
||||
function dumpDocument() {
|
||||
return {
|
||||
URL: safeCall(() => document.URL, ""),
|
||||
referrer: safeCall(() => document.referrer, ""),
|
||||
documentURI: safeCall(() => document.documentURI, ""),
|
||||
compatMode: safeCall(() => document.compatMode, ""),
|
||||
dir: safeCall(() => document.dir, ""),
|
||||
title: safeCall(() => document.title, ""),
|
||||
designMode: safeCall(() => document.designMode, ""),
|
||||
readyState: safeCall(() => document.readyState, ""),
|
||||
contentType: safeCall(() => document.contentType, ""),
|
||||
inputEncoding: safeCall(() => document.inputEncoding, ""),
|
||||
domain: safeCall(() => document.domain, ""),
|
||||
characterSet: safeCall(() => document.characterSet, ""),
|
||||
charset: safeCall(() => document.charset, ""),
|
||||
hidden: safeCall(() => document.hidden, undefined),
|
||||
cookie: safeCall(() => document.cookie, ""),
|
||||
};
|
||||
}
|
||||
|
||||
function dumpScreen() {
|
||||
return {
|
||||
width: safeCall(() => screen.width, undefined),
|
||||
height: safeCall(() => screen.height, undefined),
|
||||
availWidth: safeCall(() => screen.availWidth, undefined),
|
||||
availHeight: safeCall(() => screen.availHeight, undefined),
|
||||
availLeft: safeCall(() => screen.availLeft, undefined),
|
||||
availTop: safeCall(() => screen.availTop, undefined),
|
||||
colorDepth: safeCall(() => screen.colorDepth, undefined),
|
||||
pixelDepth: safeCall(() => screen.pixelDepth, undefined),
|
||||
isExtended: safeCall(() => screen.isExtended, undefined),
|
||||
orientation: safeCall(() => screen.orientation, null),
|
||||
};
|
||||
}
|
||||
|
||||
function dumpWindowMetrics() {
|
||||
return {
|
||||
devicePixelRatio: safeCall(() => window.devicePixelRatio, undefined),
|
||||
innerWidth: safeCall(() => window.innerWidth, undefined),
|
||||
innerHeight: safeCall(() => window.innerHeight, undefined),
|
||||
outerWidth: safeCall(() => window.outerWidth, undefined),
|
||||
outerHeight: safeCall(() => window.outerHeight, undefined),
|
||||
isSecureContext: safeCall(() => window.isSecureContext, undefined),
|
||||
};
|
||||
}
|
||||
|
||||
function collectCanvasFingerprint() {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) {
|
||||
return { canvas2d: null, webgl: null };
|
||||
}
|
||||
ctx.textBaseline = "top";
|
||||
ctx.font = "14px Arial";
|
||||
ctx.fillStyle = "#f60";
|
||||
ctx.fillRect(10, 10, 120, 40);
|
||||
ctx.fillStyle = "#069";
|
||||
ctx.fillText("env-codex", 14, 16);
|
||||
const canvas2d = safeCall(() => canvas.toDataURL(), null);
|
||||
|
||||
let webgl = null;
|
||||
const gl = safeCall(() => canvas.getContext("webgl") || canvas.getContext("experimental-webgl"), null);
|
||||
if (gl) {
|
||||
webgl = {
|
||||
vendor: safeCall(() => gl.getParameter(37445), null),
|
||||
renderer: safeCall(() => gl.getParameter(37446), null),
|
||||
};
|
||||
}
|
||||
return { canvas2d, webgl };
|
||||
}
|
||||
|
||||
const payload = {
|
||||
timestamp: new Date().toISOString(),
|
||||
location: {
|
||||
href: safeCall(() => location.href, ""),
|
||||
origin: safeCall(() => location.origin, ""),
|
||||
protocol: safeCall(() => location.protocol, ""),
|
||||
host: safeCall(() => location.host, ""),
|
||||
hostname: safeCall(() => location.hostname, ""),
|
||||
pathname: safeCall(() => location.pathname, ""),
|
||||
search: safeCall(() => location.search, ""),
|
||||
hash: safeCall(() => location.hash, ""),
|
||||
},
|
||||
history: {
|
||||
length: safeCall(() => history.length, undefined),
|
||||
scrollRestoration: safeCall(() => history.scrollRestoration, undefined),
|
||||
},
|
||||
navigator: dumpNavigator(),
|
||||
document: dumpDocument(),
|
||||
screen: dumpScreen(),
|
||||
windowMetrics: dumpWindowMetrics(),
|
||||
localStorage: dumpStorage(window.localStorage),
|
||||
sessionStorage: dumpStorage(window.sessionStorage),
|
||||
fingerprint: collectCanvasFingerprint(),
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(payload, null, 2));
|
||||
return payload;
|
||||
})();
|
||||
@@ -0,0 +1,32 @@
|
||||
function parseCookieString(cookieString) {
|
||||
const out = {};
|
||||
if (!cookieString) {
|
||||
return out;
|
||||
}
|
||||
for (const item of String(cookieString).split(";")) {
|
||||
const trimmed = item.trim();
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
const idx = trimmed.indexOf("=");
|
||||
if (idx === -1) {
|
||||
out[trimmed] = "";
|
||||
continue;
|
||||
}
|
||||
out[trimmed.slice(0, idx)] = trimmed.slice(idx + 1);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function mergeStorageState(input = {}) {
|
||||
return {
|
||||
cookies: parseCookieString(input.cookie || ""),
|
||||
localStorage: Object.assign({}, input.localStorage || {}),
|
||||
sessionStorage: Object.assign({}, input.sessionStorage || {}),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mergeStorageState,
|
||||
parseCookieString,
|
||||
};
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
Runtime observer template.
|
||||
Derived from qxVm/catvm proxy ideas, but rewritten into a reusable logger that
|
||||
aggregates missing paths, descriptor probes, and invocation traces.
|
||||
*/
|
||||
|
||||
function createRuntimeObserver(config = {}) {
|
||||
const logs = [];
|
||||
const seen = new WeakMap();
|
||||
const maxDepth = config.maxDepth || 4;
|
||||
const logOpen = config.logOpen !== false;
|
||||
const skip = new Set(config.skip || ["toString", "__proto__"]);
|
||||
|
||||
function record(type, path, detail) {
|
||||
const entry = { type, path, detail };
|
||||
logs.push(entry);
|
||||
if (logOpen) {
|
||||
console.log("[observer]", type, path, detail);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
function summarize() {
|
||||
const missingPaths = [];
|
||||
const descriptorAccess = [];
|
||||
const invocationErrors = [];
|
||||
const prototypeAccess = [];
|
||||
|
||||
for (const log of logs) {
|
||||
if (log.type === "missing") {
|
||||
missingPaths.push(log.path);
|
||||
} else if (log.type === "descriptor") {
|
||||
descriptorAccess.push(log.path);
|
||||
} else if (log.type === "invoke-error") {
|
||||
invocationErrors.push(log.path);
|
||||
} else if (log.type === "prototype") {
|
||||
prototypeAccess.push(log.path);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
total: logs.length,
|
||||
missingPaths,
|
||||
descriptorAccess,
|
||||
invocationErrors,
|
||||
prototypeAccess,
|
||||
logs,
|
||||
};
|
||||
}
|
||||
|
||||
function wrap(target, name = "root", depth = 0) {
|
||||
if (!target || (typeof target !== "object" && typeof target !== "function")) {
|
||||
return target;
|
||||
}
|
||||
if (seen.has(target)) {
|
||||
return seen.get(target);
|
||||
}
|
||||
if (depth > maxDepth) {
|
||||
return target;
|
||||
}
|
||||
|
||||
const proxy = new Proxy(target, {
|
||||
get(t, key, receiver) {
|
||||
const path = name + "." + String(key);
|
||||
let value;
|
||||
try {
|
||||
value = Reflect.get(t, key, receiver);
|
||||
} catch (err) {
|
||||
record("invoke-error", path, err && err.message ? err.message : String(err));
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!skip.has(String(key))) {
|
||||
if (typeof value === "undefined") {
|
||||
record("missing", path, "undefined");
|
||||
} else {
|
||||
record("get", path, typeof value);
|
||||
}
|
||||
}
|
||||
|
||||
if (value && (typeof value === "object" || typeof value === "function")) {
|
||||
return wrap(value, path, depth + 1);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
set(t, key, value, receiver) {
|
||||
const path = name + "." + String(key);
|
||||
record("set", path, typeof value);
|
||||
return Reflect.set(t, key, value, receiver);
|
||||
},
|
||||
has(t, key) {
|
||||
const path = name + "." + String(key);
|
||||
record("has", path, Reflect.has(t, key));
|
||||
return Reflect.has(t, key);
|
||||
},
|
||||
deleteProperty(t, key) {
|
||||
const path = name + "." + String(key);
|
||||
record("delete", path, true);
|
||||
return Reflect.deleteProperty(t, key);
|
||||
},
|
||||
getOwnPropertyDescriptor(t, key) {
|
||||
const path = name + "." + String(key);
|
||||
const result = Reflect.getOwnPropertyDescriptor(t, key);
|
||||
record("descriptor", path, result ? Object.keys(result) : null);
|
||||
return result;
|
||||
},
|
||||
defineProperty(t, key, attrs) {
|
||||
const path = name + "." + String(key);
|
||||
record("define", path, Object.keys(attrs || {}));
|
||||
return Reflect.defineProperty(t, key, attrs);
|
||||
},
|
||||
getPrototypeOf(t) {
|
||||
record("prototype", name, "getPrototypeOf");
|
||||
return Reflect.getPrototypeOf(t);
|
||||
},
|
||||
setPrototypeOf(t, proto) {
|
||||
record("prototype", name, "setPrototypeOf");
|
||||
return Reflect.setPrototypeOf(t, proto);
|
||||
},
|
||||
ownKeys(t) {
|
||||
record("ownKeys", name, Reflect.ownKeys(t));
|
||||
return Reflect.ownKeys(t);
|
||||
},
|
||||
isExtensible(t) {
|
||||
record("isExtensible", name, Reflect.isExtensible(t));
|
||||
return Reflect.isExtensible(t);
|
||||
},
|
||||
preventExtensions(t) {
|
||||
record("preventExtensions", name, true);
|
||||
return Reflect.preventExtensions(t);
|
||||
},
|
||||
apply(t, thisArg, args) {
|
||||
record("apply", name, args);
|
||||
return Reflect.apply(t, thisArg, args);
|
||||
},
|
||||
construct(t, args, newTarget) {
|
||||
record("construct", name, args);
|
||||
return Reflect.construct(t, args, newTarget);
|
||||
},
|
||||
});
|
||||
|
||||
seen.set(target, proxy);
|
||||
return proxy;
|
||||
}
|
||||
|
||||
return {
|
||||
wrap,
|
||||
summarize,
|
||||
logs,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createRuntimeObserver,
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
Native protection template.
|
||||
Derived from qxVm/catvm safe-function patterns and normalized into a reusable
|
||||
module for protecting functions, getters, and setters.
|
||||
*/
|
||||
|
||||
function installNativeProtector() {
|
||||
const rawToString = Function.toString;
|
||||
const nativeSymbol = Symbol("native_toString_" + Math.random().toString(36).slice(2));
|
||||
|
||||
function defineHidden(target, key, value) {
|
||||
Object.defineProperty(target, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
function markNative(fn, displayName) {
|
||||
if (typeof fn !== "function") {
|
||||
return fn;
|
||||
}
|
||||
const name = displayName || fn.name || "";
|
||||
defineHidden(fn, nativeSymbol, "function " + name + "() { [native code] }");
|
||||
return fn;
|
||||
}
|
||||
|
||||
function protectedToString() {
|
||||
if (typeof this === "function" && this[nativeSymbol]) {
|
||||
return this[nativeSymbol];
|
||||
}
|
||||
return rawToString.call(this);
|
||||
}
|
||||
|
||||
defineHidden(Function.prototype, "toString", protectedToString);
|
||||
defineHidden(Function, "toString", protectedToString);
|
||||
markNative(Function.prototype.toString, "toString");
|
||||
|
||||
function protectFunction(fn, displayName) {
|
||||
return markNative(fn, displayName);
|
||||
}
|
||||
|
||||
function protectDescriptor(target, key) {
|
||||
const desc = Object.getOwnPropertyDescriptor(target, key);
|
||||
if (!desc) {
|
||||
return false;
|
||||
}
|
||||
if (typeof desc.get === "function") {
|
||||
markNative(desc.get, "get " + key);
|
||||
}
|
||||
if (typeof desc.set === "function") {
|
||||
markNative(desc.set, "set " + key);
|
||||
}
|
||||
if (typeof desc.value === "function") {
|
||||
markNative(desc.value, desc.value.name || key);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function repairFunctionMeta(fn, meta = {}) {
|
||||
if (typeof fn !== "function") {
|
||||
return fn;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(meta, "name")) {
|
||||
Object.defineProperty(fn, "name", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: meta.name,
|
||||
});
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(meta, "length")) {
|
||||
Object.defineProperty(fn, "length", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: meta.length,
|
||||
});
|
||||
}
|
||||
return fn;
|
||||
}
|
||||
|
||||
return {
|
||||
nativeSymbol,
|
||||
protectFunction,
|
||||
protectDescriptor,
|
||||
repairFunctionMeta,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
installNativeProtector,
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
function scaffoldModule(name) {
|
||||
return {
|
||||
name,
|
||||
patchPlan: [],
|
||||
patchCode: "",
|
||||
runtimeState: {},
|
||||
validation: [],
|
||||
residualRisk: [],
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
scaffoldModule,
|
||||
};
|
||||
Reference in New Issue
Block a user