存档:TS 重构前基线(lx 聚合源 = isolated-vm 加载器,签名方案已移除)

This commit is contained in:
archive committed 2026-08-28 23:24:49 +08:00
commit f289df4cd7
124 files changed
+14110

No files matched your search

+139
View File
@@ -0,0 +1,139 @@
// lx-music-source-v5 复现/追踪脚本(与生产加载器 src/apis/lx-source-ivm.js 相互独立)
// 1) 将去混淆脚本末尾的字符串缓存 [] 替换为 LOGCACHE(Proxy),按序记录 JSVMP 解码的全部字符串
// 2) 模拟 lx-music-desktop preload.js 的 globalThis.lx 环境,观察脚本的注册与取流行为
// 运行:node tmp/reproduce-lx-source-v5.mjs
import crypto from 'node:crypto'
import fs from 'node:fs'
import vm from 'node:vm'
import zlib from 'node:zlib'
import { promisify } from 'node:util'
import { fileURLToPath } from 'node:url'
const inflate = promisify(zlib.inflate)
const deflate = promisify(zlib.deflate)
const md5 = (s) => crypto.createHash('md5').update(s).digest('hex')
const deobPath = fileURLToPath(new URL('../lx-music-source-v5-deobfuscated.js', import.meta.url))
const obfPath = fileURLToPath(new URL('../lx-music-source-v5.js', import.meta.url))
let code = fs.readFileSync(deobPath, 'utf8')
// 字符串缓存:文件末尾 IIFE 最后一个参数 []
const cacheLog = []
const LOGCACHE = new Proxy([], {
set(t, prop, val) {
if (typeof val === 'string') cacheLog.push(val)
t[prop] = val
return true
},
})
const idx = code.lastIndexOf(', []);')
code = code.slice(0, idx) + ', LOGCACHE);'
// ---- lx 环境(复刻 preload.js) ----
const handlers = {}
const requestCalls = []
let rconfigBody = process.env.RCONFIG_BODY ?? JSON.stringify({ code: 0, msg: 'success', data: 'https://example.test/fake.mp3' })
const lx = {
EVENT_NAMES: { request: 'request', inited: 'inited', updateAlert: 'updateAlert' },
request(url, options = {}, callback) {
const rec = { url, options: { method: options.method ?? 'get', timeout: options.timeout, headers: options.headers, body: options.body, form: options.form, formData: options.formData } }
requestCalls.push(rec)
console.log('[lx.request]', url)
console.log(' options:', JSON.stringify(rec.options))
const isRconfig = url.includes('/rconfig')
const raw = isRconfig ? rconfigBody : JSON.stringify({ code: 0, msg: 'success', data: 'https://example.test/fake.mp3' })
const parsed = (() => { try { return JSON.parse(raw) } catch { return raw } })()
setTimeout(() => {
callback(null, { statusCode: 200, statusMessage: 'OK', headers: { 'content-type': 'application/json' }, raw: Buffer.from(raw), body: parsed }, parsed)
}, 5)
return () => {}
},
send(eventName, data) {
console.log('[lx.send]', eventName, JSON.stringify(data))
return Promise.resolve()
},
on(eventName, handler) {
console.log('[lx.on]', eventName, typeof handler, handler && handler.length)
handlers[eventName] = handler
return Promise.resolve()
},
utils: {
crypto: {
aesEncrypt(b, mode, key, iv) { const c = crypto.createCipheriv(mode, key, iv); return Buffer.concat([c.update(b), c.final()]) },
rsaEncrypt(b, key) { b = Buffer.concat([Buffer.alloc(128 - b.length), b]); return crypto.publicEncrypt({ key, padding: crypto.constants.RSA_NO_PADDING }, b) },
randomBytes: (n) => crypto.randomBytes(n),
md5,
},
buffer: { from: (...a) => Buffer.from(...a), bufToString: (b, f) => Buffer.from(b, 'binary').toString(f) },
zlib: { inflate: (b) => inflate(b), deflate: (d) => deflate(d) },
},
currentScriptInfo: { name: '独家音源', description: '', version: '5', author: 'w', homepage: '', rawScript: code },
version: '2.0.0',
env: 'desktop',
}
const ctx = {
console, setTimeout, clearTimeout, setInterval, clearInterval,
Promise, Error, TypeError, RangeError, SyntaxError, EvalError, URIError,
Array, Object, String, Number, Boolean, Date, Math, JSON, RegExp, BigInt, Symbol,
Function, parseInt, parseFloat, isNaN, isFinite,
encodeURIComponent, decodeURIComponent, escape, unescape,
Uint8Array, Uint16Array, Uint32Array, Int8Array, Int16Array, Int32Array, Float32Array, Float64Array, ArrayBuffer, DataView, Map, Set, WeakMap, WeakSet,
TextEncoder, TextDecoder, URL, URLSearchParams,
Buffer,
atob: (s) => Buffer.from(s, 'base64').toString('binary'),
btoa: (s) => Buffer.from(s, 'binary').toString('base64'),
SCRIPT_MD5: md5(fs.readFileSync(obfPath)),
LOGCACHE,
lx,
}
ctx.globalThis = ctx
ctx.window = ctx
ctx.self = ctx
ctx.global = ctx
process.on('uncaughtException', (e) => console.error('[uncaught]', e.stack?.split('\n').slice(0, 6).join('\n')))
process.on('unhandledRejection', (e) => console.error('[unhandledRejection]', e?.stack?.split('\n').slice(0, 6).join('\n') || e))
const context = vm.createContext(ctx)
console.error('[run]')
try {
vm.runInContext(code, context, { filename: 'lx-source-v5.js', timeout: 15000 })
console.error('[run ok]')
} catch (e) {
console.error('[vm error]', e.stack?.split('\n').slice(0, 8).join('\n'))
}
await new Promise((r) => setTimeout(r, 1500))
console.error('\n===== decoded strings (%d) =====', cacheLog.length)
console.log(cacheLog.map((s, i) => `${i}\t${JSON.stringify(s)}`).join('\n'))
console.error('\n===== summary =====')
console.error('events registered:', Object.keys(handlers))
console.error('requests made:', requestCalls.length)
// 触发 musicUrl 请求
const handler = handlers.request
if (handler) {
console.error('\n[trigger] musicUrl kw/128k')
const markBefore = cacheLog.length
try {
const p = handler.call(lx, { source: 'kw', action: 'musicUrl', info: { type: '128k', musicInfo: { songmid: '28423286', hash: 'fakehash123', id: 'kw_28423286' } } })
console.error('[trigger] handler returned:', p && p.constructor?.name)
if (p && typeof p.then === 'function') {
const res = await Promise.race([p.then((v) => ({ ok: true, v }), (e) => ({ ok: false, msg: e?.message, name: e?.name })), new Promise((r) => setTimeout(() => r({ timeout: true }), 8000))])
console.error('[trigger result]', JSON.stringify(res, null, 1))
}
} catch (e) {
console.error('[trigger sync error]', e?.message)
}
console.error('\n[strings used during trigger]')
console.error(cacheLog.slice(markBefore).map((s) => JSON.stringify(s)).join('\n'))
await new Promise((r) => setTimeout(r, 500))
console.error('\n[requests during trigger]')
for (const rc of requestCalls) console.log(rc.url, JSON.stringify(rc.options))
}
fs.writeFileSync(fileURLToPath(new URL('./trace-strings.json', import.meta.url)), JSON.stringify(cacheLog, null, 1))
process.exit(0)