ksjsb/core/h5_kws_sign.mjs
2026-07-30 20:25:56 +08:00

308 lines
9.1 KiB
JavaScript

import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import vm from "node:vm";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const DEFAULT_KWS_FILE = path.join(
__dirname,
"kws-11-0.0.1-obfuscated.5e0a90af726d8a7e.js",
);
const MAX_EVENTS = Number(process.env.KS_KWS_MAX_EVENTS || 240);
function sha16(value) {
return crypto.createHash("sha256").update(String(value)).digest("hex").slice(0, 16);
}
function preview(value) {
try {
if (value === null) return "null";
if (value === undefined) return "undefined";
if (typeof value === "string") {
return value.length > 120 ? `${value.slice(0, 120)}...(len=${value.length})` : value;
}
if (typeof value === "number" || typeof value === "boolean") return value;
if (typeof value === "function") return `[function ${value.name || "anonymous"}]`;
if (Array.isArray(value)) return `[array len=${value.length}]`;
return `[object ${Object.keys(value).slice(0, 8).join(",")}]`;
} catch {
return "[unpreviewable]";
}
}
function buildContext() {
const events = [];
const record = (type, detail = {}) => {
if (events.length >= MAX_EVENTS) return;
events.push({ type, ...detail });
};
const mockCache = new Map();
function mock(name = "mock") {
if (mockCache.has(name)) return mockCache.get(name);
const fn = function (...args) {
record("call", { path: name, args: args.map(preview) });
return mock(`${name}()`);
};
const proxy = new Proxy(fn, {
get(_target, prop) {
if (prop === Symbol.toPrimitive) return () => "";
if (prop === "toString") return () => `[mock ${name}]`;
if (prop === "valueOf") return () => 0;
if (prop === "then") return undefined;
return mock(`${name}.${String(prop)}`);
},
set(_target, prop, value) {
record("mock-set", { path: `${name}.${String(prop)}`, value: preview(value) });
return true;
},
apply(_target, _thisArg, args) {
record("apply", { path: name, args: args.map(preview) });
return mock(`${name}()`);
},
construct(_target, args) {
record("new", { path: name, args: args.map(preview) });
return mock(`new ${name}`);
},
has() {
return true;
},
});
mockCache.set(name, proxy);
return proxy;
}
const storage = new Map();
const localStorage = {
getItem(key) {
const text = String(key);
const value = storage.get(text) || null;
record("localStorage.getItem", { key: text, value: preview(value) });
return value;
},
setItem(key, value) {
const text = String(key);
storage.set(text, String(value));
record("localStorage.setItem", { key: text, value: preview(String(value)) });
},
removeItem(key) {
storage.delete(String(key));
record("localStorage.removeItem", { key: String(key) });
},
clear() {
storage.clear();
record("localStorage.clear");
},
};
class FakeXMLHttpRequest {
open(method, url) {
this.method = method;
this.url = url;
record("xhr.open", { method: preview(method), url: preview(url) });
}
setRequestHeader(key, value) {
record("xhr.setRequestHeader", { key: preview(key), value: preview(value) });
}
send(body) {
record("xhr.send", { url: preview(this.url), body: preview(body) });
this.readyState = 4;
this.status = 200;
this.responseText = "{}";
if (typeof this.onreadystatechange === "function") this.onreadystatechange();
if (typeof this.onload === "function") this.onload();
}
addEventListener(name, cb) {
record("xhr.addEventListener", { name: preview(name), cb: preview(cb) });
}
}
let kwscode = "";
const rawWindow = {};
const windowProxy = new Proxy(rawWindow, {
get(target, prop) {
if (prop in target) return target[prop];
const key = String(prop);
const value = mock(`window.${key}`);
target[prop] = value;
record("window.get-unknown", { key });
return value;
},
set(target, prop, value) {
target[prop] = value;
record("window.set", { key: String(prop), value: preview(value) });
return true;
},
has() {
return true;
},
});
Object.assign(rawWindow, {
window: windowProxy,
self: windowProxy,
top: windowProxy,
parent: windowProxy,
globalThis: windowProxy,
Object,
Array,
String,
Function,
Number,
Boolean,
Date,
Math,
RegExp,
Error,
TypeError,
Promise,
JSON,
Infinity,
undefined,
Uint8Array,
parseInt,
parseFloat,
isNaN,
escape,
encodeURI,
encodeURIComponent,
decodeURI,
decodeURIComponent,
Window: function Window() {},
Navigator: function Navigator() {},
Location: function Location() {},
kwscb(value) {
kwscode = String(value || "");
record("kwscb", { length: kwscode.length, sha16: sha16(kwscode) });
},
location: {
href: "https://nebula.kuaishou.com/nebula/task/earning?layoutType=4&source=bottom_guide_first",
origin: "https://nebula.kuaishou.com",
protocol: "https:",
host: "nebula.kuaishou.com",
hostname: "nebula.kuaishou.com",
pathname: "/nebula/task/earning",
search: "?layoutType=4&source=bottom_guide_first",
},
document: {
cookie: "kpn=NEBULA; kpf=ANDROID_PHONE; userId=0; did=ANDROID_FAKE; kuaishou.h5_st=FAKE",
referrer: "",
createElement(tag) {
record("document.createElement", { tag: preview(tag) });
return mock(`element.${tag}`);
},
getElementsByTagName(tag) {
record("document.getElementsByTagName", { tag: preview(tag) });
return [mock(`tag.${tag}`)];
},
addEventListener(name, cb) {
record("document.addEventListener", { name: preview(name), cb: preview(cb) });
},
body: mock("document.body"),
head: mock("document.head"),
documentElement: mock("document.documentElement"),
},
navigator: {
userAgent:
"Mozilla/5.0 (Linux; Android 16; PJZ110 Build/BP2A.250605.015; wv) AppleWebKit/537.36",
language: "zh-CN",
languages: ["zh-CN", "zh"],
platform: "Linux armv8l",
webdriver: false,
},
screen: { width: 1080, height: 2376, colorDepth: 24, pixelDepth: 24 },
localStorage,
sessionStorage: localStorage,
performance: { now: () => 1234.5, timing: {}, getEntriesByType: () => [] },
crypto: {
getRandomValues(arr) {
for (let i = 0; i < arr.length; i += 1) arr[i] = (i * 17 + 3) & 0xff;
return arr;
},
},
Image: function Image() {
return mock("new Image");
},
XMLHttpRequest: FakeXMLHttpRequest,
fetch(url, init) {
record("fetch", { url: preview(url), init: preview(init) });
return Promise.resolve({
ok: true,
status: 200,
text: () => Promise.resolve("{}"),
json: () => Promise.resolve({}),
});
},
setTimeout(cb) {
record("setTimeout", { cb: preview(cb) });
if (typeof cb === "function") cb();
return 1;
},
clearTimeout() {},
setInterval(cb) {
record("setInterval", { cb: preview(cb) });
return 1;
},
clearInterval() {},
console: {
log: (...args) => record("console.log", { args: args.map(preview) }),
warn: (...args) => record("console.warn", { args: args.map(preview) }),
error: (...args) => record("console.error", { args: args.map(preview) }),
},
});
const context = vm.createContext(windowProxy, {
name: "h5-kws-sign",
codeGeneration: { strings: true, wasm: false },
});
return {
context,
events,
rawWindow,
getKwscode: () => kwscode,
};
}
export function runKwsSignScript(scriptPath = DEFAULT_KWS_FILE, timeoutMs = 5000) {
const { context, events, rawWindow, getKwscode } = buildContext();
const code = fs.readFileSync(scriptPath, "utf8");
const beforeKeys = new Set(Object.keys(rawWindow));
vm.runInContext(code, context, { filename: scriptPath, timeout: timeoutMs });
const kwscode = getKwscode();
if (!kwscode) {
throw new Error("KWS script did not call kwscb()");
}
const eventTypeCounts = {};
for (const event of events) eventTypeCounts[event.type] = (eventTypeCounts[event.type] || 0) + 1;
return {
ok: true,
kwscode,
length: kwscode.length,
sha16: sha16(kwscode),
newWindowKeys: Object.keys(rawWindow).filter((key) => !beforeKeys.has(key)),
eventTypeCounts,
};
}
async function main() {
const scriptPath = process.argv[2] || DEFAULT_KWS_FILE;
const timeoutMs = Number(process.env.KS_KWS_VM_TIMEOUT_MS || 5000);
try {
const result = runKwsSignScript(scriptPath, timeoutMs);
console.log(JSON.stringify(result));
} catch (err) {
const message = String(err && err.stack ? err.stack : err);
console.log(JSON.stringify({ ok: false, error: message.split(/\r?\n/).slice(0, 12).join("\n") }));
process.exitCode = 1;
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
await main();
}