308 lines
9.1 KiB
JavaScript
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();
|
|
}
|