140 lines
6.2 KiB
JavaScript
140 lines
6.2 KiB
JavaScript
// 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)
|