UNPKG

wedecode

Version:

微信小程序源代码还原工具, 线上代码安全审计

1,674 lines (1,671 loc) 101 kB
#!/usr/bin/env node var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; import { Command } from "commander"; import path from "node:path"; import process$1 from "node:process"; import "single-line-log"; import JS from "js-beautify"; import os from "node:os"; import fs from "node:fs"; import { glob } from "glob"; import colors from "picocolors"; import checkForUpdate from "update-check"; import figlet from "figlet"; import inquirer from "inquirer"; import { SelectTableTablePrompt } from "@biggerstar/inquirer-selectable-table"; import axios from "axios"; import openFileExplorer from "open-file-explorer"; import { deepmerge } from "@biggerstar/deepmerge"; import { VM } from "vm2"; import { JSDOM } from "jsdom"; import * as cheerio from "cheerio"; import cssbeautify from "cssbeautify"; import esprima from "esprima"; import escodegen from "escodegen"; const name = "wedecode"; const version = "0.8.1"; const type = "module"; const description = "微信小程序源代码还原工具, 线上代码安全审计"; const bin = { wedecode: "./dist/wedecode.js" }; const scripts = { bootstrap: "pnpm install", start: "vite build && node dist/wedecode.js", dev: "vite build --watch", build: "vite build", "test:cmd": "wedecode", "test:cmd:dev": "DEV=true wedecode", "test:cmd:args": "DEV=true wedecode -o OUTPUT", link: "vite build && pnpm link --dir= ./", unlink: "pnpm unlink", "release:npm": "vite build && npm publish", "release:git": "vite build && git commit -am v$npm_package_version && git tag $npm_package_version && git push --tags ", "dev:unpack:dir": "DEV=true wedecode --clear -o OUTPUT pkg/fen-cao", "dev:unpack:subPack": "DEV=true wedecode --clear -o OUTPUT pkg/mt/_mobike_.wxapkg", "dev:unpack:game-dir": "DEV=true wedecode --clear -o OUTPUT-GAME pkg/weixin-dushu", "preview:unpack": "wedecode -ow true -o OUTPUT pkg/issues8-sxd" }; const repository = { type: "git", url: "git+https://github.com/biggerstar/wedecode.git" }; const license = "GPL-3.0-or-later"; const bugs = { url: "https://github.com/biggerstar/wedecode/issues" }; const files = [ "dist", "decryption-tool" ]; const homepage = "https://github.com/biggerstar/wedecode#readme"; const dependencies = { "@biggerstar/deepmerge": "^1.0.3", "@biggerstar/inquirer-selectable-table": "^1.0.12", axios: "^1.7.4", cheerio: "1.0.0-rc.12", commander: "^12.1.0", cssbeautify: "^0.3.1", escodegen: "^1.14.3", esprima: "^4.0.1", figlet: "^1.7.0", glob: "^10.4.1", inquirer: "^9.2.23", "js-beautify": "^1.15.1", jsdom: "^24.1.0", "open-file-explorer": "^1.0.2", picocolors: "^1.0.1", "single-line-log": "^1.1.2", "update-check": "^1.5.4", vm2: "^3.9.19" }; const devDependencies = { "@types/cssbeautify": "^0.3.5", "@types/escodegen": "^0.0.10", "@types/esprima": "^4.0.6", "@types/figlet": "^1.5.8", "@types/inquirer": "^9.0.7", "@types/js-beautify": "^1.14.3", "@types/jsdom": "^21.1.7", "@types/node": "^20.14.2", "@types/open-file-explorer": "^1.0.2", "@types/single-line-log": "^1.1.2", lerna: "^8.1.7", "rollup-plugin-copy": "^3.5.0", vite: "^5.2.13" }; const keywords = [ "wxapkg", "Decompilation", "小程序", "反编译", "审计", "安全" ]; const pkg = { name, version, type, description, bin, scripts, repository, license, bugs, files, homepage, dependencies, devDependencies, keywords }; /^win/.test(process.platform); /^darwin/.test(process.platform); const cssBodyToPageReg = /body\s*\{/g; const PUBLIC_OUTPUT_PATH = "OUTPUT"; const pluginDirRename = ["__plugin__", "plugin_"]; const removeAppFileList = [ // 'app-config.json', "page-frame.html", "app-wxss.js", "app-service.js", "index.appservice.js", "index.webview.js", "appservice.app.js", "page-frame.js", "webview.app.js", "common.app.js" // 'plugin.json', ]; const removeGameFileList = [ // 'app-config.json', // 'game.js', "subContext.js", "worker.js" ]; const appJsonExcludeKeys = [ "navigateToMiniProgramAppIdList" ]; const GameJsonExcludeKeys = [ "openDataContext" ]; const macGlob = [ // 版本3+ "/Users/*/Library/Containers/*/Data/.wxapplet/packages", // 版本4.0+ "/Users/*/Library/Containers/*/Data/Documents/app_data/radium/Applet/packages" ]; const winGlob = [ // 版本3+ "C:/Users/*/weixin/WeChat Files", "C:/Users/*/Documents/WeChat Files/Applet", // 版本4.0+ "C:/Users/*/Documents/xwechat_files", "C:/Users/*/AppData/Roaming/*/xwechat/radium/Applet/packages", // 安装到其他盘 "D:/WeChat Files/Applet", "E:/WeChat Files/Applet", "F:/WeChat Files/Applet" ]; const linuxGlob = [ "/home/*/.config/WeChat/Applet" ]; function getPlatformGlob() { const platform = os.platform(); switch (platform) { case "win32": return winGlob; case "darwin": return macGlob; case "linux": return linuxGlob; default: return []; } } const globPathList = getPlatformGlob(); const AppMainPackageNames = ["__APP__.wxapkg", "app.wxapkg"]; var CacheClearEnum = /* @__PURE__ */ ((CacheClearEnum2) => { CacheClearEnum2["clear"] = "清空"; CacheClearEnum2["notClear"] = "不清空"; return CacheClearEnum2; })(CacheClearEnum || {}); var OperationModeEnum = /* @__PURE__ */ ((OperationModeEnum2) => { OperationModeEnum2["autoScan"] = "▶ 自动扫描小程序包"; OperationModeEnum2["manualScan"] = "▶ 手动设定扫描目录"; OperationModeEnum2["manualDir"] = "▶ 直接指定包路径( 非扫描 )"; return OperationModeEnum2; })(OperationModeEnum || {}); var StreamPathDefaultEnum = /* @__PURE__ */ ((StreamPathDefaultEnum2) => { StreamPathDefaultEnum2["inputPath"] = "./"; StreamPathDefaultEnum2[StreamPathDefaultEnum2["publicOutputPath"] = PUBLIC_OUTPUT_PATH] = "publicOutputPath"; StreamPathDefaultEnum2["defaultOutputPath"] = "default"; return StreamPathDefaultEnum2; })(StreamPathDefaultEnum || {}); var YesOrNoEnum = /* @__PURE__ */ ((YesOrNoEnum2) => { YesOrNoEnum2["yes"] = "是"; YesOrNoEnum2["no"] = "否"; return YesOrNoEnum2; })(YesOrNoEnum || {}); const isDev = process$1.env.DEV === "true"; function getPathResolveInfo(outputDir) { let _packRootPath = outputDir; const resolve = (_new_resolve_path = "./", ...args) => { return path.resolve(outputDir, _packRootPath, _new_resolve_path, ...args); }; const outputResolve = (_new_resolve_path = "./", ...args) => { return path.resolve(outputDir, _new_resolve_path, ...args); }; return { /** 相对当前包( 子包, 主包, 插件包都算当前路径 )作为根路径路径进行解析 */ resolve, /** 相对当前主包路径进行解析 */ outputResolve, outputPath: outputDir, join(_path) { return path.join(_packRootPath, _path); }, setPackRootPath(rootPath) { _packRootPath = rootPath; }, /** * 当前的包根路径, 主包为 ./ , 分包为相对主包根的相对路径 * */ get packRootPath() { return _packRootPath === outputDir ? "./" : _packRootPath; }, get appJsonPath() { return resolve("app.json"); }, get appConfigJsonPath() { return resolve("app-config.json"); }, get projectPrivateConfigJsonPath() { return resolve("project.private.config.json"); }, get appWxssPath() { return resolve("app-wxss.js"); }, get workersPath() { return resolve("workers.js"); }, get pageFramePath() { return resolve("page-frame.js"); }, get pageFrameHtmlPath() { return resolve("page-frame.html"); }, get appJsPath() { return resolve("app.js"); }, get appServicePath() { return resolve("app-service.js"); }, get appServiceAppPath() { return resolve("appservice.app.js"); }, get gameJsonPath() { return resolve("game.json"); }, get gameJsPath() { return resolve("game.js"); }, get subContextJsPath() { return resolve("subContext.js"); } }; } function jsBeautify(code) { return JS.js_beautify(code, { indent_size: 2 }); } function clearScreen() { process$1.stdout.write(process$1.platform === "win32" ? "\x1Bc" : "\x1B[2J\x1B[3J\x1B[H"); } const excludesLogMatch = isDev ? [ "Completed" ] : []; function printLog(log, opt = {}) { if (excludesLogMatch.some((item) => log.includes(item))) return; { console.log(log); return; } } function removeElement(arr, elementToRemove) { const index = arr.indexOf(elementToRemove); if (index > -1) { arr.splice(index, 1); } } function commonDir(pathA, pathB) { if (pathA[0] === ".") pathA = pathA.slice(1); if (pathB[0] === ".") pathB = pathB.slice(1); pathA = pathA.replace(/\\/g, "/"); pathB = pathB.replace(/\\/g, "/"); let a = Math.min(pathA.length, pathB.length); for (let i = 1, m = Math.min(pathA.length, pathB.length); i <= m; i++) if (!pathA.startsWith(pathB.slice(0, i))) { a = i - 1; break; } let pub = pathB.slice(0, a); let len = pub.lastIndexOf("/") + 1; return pathA.slice(0, len); } function findCommonRoot(paths) { const splitPaths = paths.map((path2) => path2.split("/").filter(Boolean)); const commonRoot = []; for (let i = 0; i < splitPaths[0].length; i++) { const partsMatch = splitPaths.every((path2) => path2[i] === splitPaths[0][i]); if (partsMatch) { commonRoot.push(splitPaths[0][i]); } else { break; } } return commonRoot.join("/"); } function replaceExt(name2, ext = "") { const hasSuffix = name2.lastIndexOf(".") > 2; return hasSuffix ? name2.slice(0, name2.lastIndexOf(".")) + ext : `${name2}${ext}`; } function sleep(time) { return new Promise((resolve1) => setTimeout(resolve1, time)); } function arrayDeduplication(arr, cb) { return arr.reduce((pre, cur) => { !pre.includes(cur) && pre.push(cur); return pre; }, []); } function removeVM2ExceptionLine(code) { const reg = /\s*[a-z]\x20?=\x20?VM2_INTERNAL_STATE_DO_NOT_USE_OR_PROGRAM_WILL_FAIL\.handleException\([a-z]\);?/g; return code.replace(reg, ""); } function resetWxsRequirePath(p, resetString = "") { return p.replaceAll("p_./", resetString).replaceAll("m_./", resetString); } function isPluginPath(path2) { return path2.startsWith("plugin-private://") || path2.startsWith("plugin://"); } function resetPluginPath(_path, prefixDir) { return path.join( prefixDir || "./", _path.replaceAll("plugin-private://", "").replaceAll("plugin://", "") ); } function getParameterNames(fn) { if (typeof fn !== "function") return []; const COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; const code = fn.toString().replace(COMMENTS, ""); const result = code.slice(code.indexOf("(") + 1, code.indexOf(")")).match(/([^\s,]+)/g); return result === null ? [] : result; } function isWxAppid(str) { const reg = /^wx[0-9a-f]{16}$/i; str = str.trim(); return str.length === 18 && reg.test(str); } inquirer.registerPrompt("table", SelectTableTablePrompt); process$1.stdout.setMaxListeners(200); async function onResize() { if (lastTableOptions) { await prompts.showScanPackTable(lastTableOptions); clearScreen(); } } let lastTableOptions = null; let online = false; async function checkOnline() { online = await internetAvailable(); } setTimeout(checkOnline, 0); const prompts = { async selectMode() { const offlineTip = `( ${colors.yellow("联网可显示小程序信息")} )`; const onlineTip = `( ${colors.green("网络正常")} )`; await sleep(1e3); return inquirer["prompt"]( [ { type: "list", message: `请选择操作模式 ? ${!online ? offlineTip : onlineTip}`, name: "selectMode", choices: [ OperationModeEnum.autoScan, OperationModeEnum.manualScan, OperationModeEnum.manualDir ] } ] ); }, async inputManualScanPath() { return inquirer["prompt"]( [ { type: "input", message: `输入您要扫描的小程序包路径 ( ${colors.yellow(".")} 表示使用当前路径 ): `, name: "manualScanPath", validate(input) { if (!input) return false; return checkExistsWithFilePath(input, { throw: true, checkWxapkg: false, showInputPathLog: false }); } } ] ); }, async showDangerScanPrompt(_path) { return inquirer["prompt"]( [ { type: "list", message: `您指定的路径可能会花大量时间扫描文件系统, 确定继续 ? ${colors.yellow(_path)}`, name: "dangerScan", choices: [ YesOrNoEnum.no, YesOrNoEnum.yes ], default: YesOrNoEnum.no } ] ); }, async showScanPackTable(opt) { lastTableOptions = opt; if (!onResize["onResize"]) { process$1.stdout.on("resize", onResize); onResize["onResize"] = true; } await sleep(50); clearScreen(); const part = process$1.stdout.columns / 10; const result = await inquirer["prompt"]( [ { type: "table", name: "packInfo", message: "", pageSize: 6, showIndex: true, tableOptions: { // wordWrap: true, wrapOnWordBoundary: true, colWidths: [part / 2, part * 2, part * 2, part * 5.3].map((n) => Math.floor(n)) }, columns: opt.columns || [], rows: opt.rows || [] } ] ); onResize["onResize"] = false; process$1.stdout.off("resize", onResize); return result; }, async questionInputPath() { return inquirer["prompt"]( [ { type: "input", message: `输入 ${colors.blue("wxapkg文件")} 或 ${colors.blue("目录")} 默认为( ${colors.yellow("./")} ): `, name: "inputPath", validate(input, _) { return checkExistsWithFilePath(path.resolve(input), { throw: true }); } } ] ); }, async questionOutputPath() { return inquirer["prompt"]( [ { type: "input", message: `输出目录, 默认为当前所在目录的( ${colors.yellow(PUBLIC_OUTPUT_PATH)} ): `, name: "outputPath" } ] ); }, async isClearOldCache(cachePath = "") { return inquirer["prompt"]( [ { type: "list", message: `输出目录中存在上次旧的编译产物,是否清空 ? ${colors.blue(`当前缓存路径( ${colors.yellow(cachePath)} )`)}`, name: "isClearCache", choices: [ CacheClearEnum.clear, CacheClearEnum.notClear ] } ] ); }, async showFileExplorer() { return inquirer["prompt"]( [ { type: "list", message: ` 将打开文件管理器, 确定继续 ?`, name: "showFileExplorer", choices: [ YesOrNoEnum.no, YesOrNoEnum.yes ], default: YesOrNoEnum.no } ] ); } }; function createNewVersionUpdateNotice() { let updateInfo; return { /** 进行查询 */ query() { checkForUpdate(pkg).then((res) => updateInfo = res).catch(() => void 0); }, /** * 异步使用, 时间错开,因为查询需要时间, 如果查询到新版本, 则进行通知 * 基于 update-check 如果本次查到更新但是没通知, 下次启动将会从缓存中获取版本信息并通知 * */ async notice() { await sleep(200); if (updateInfo && updateInfo.latest) { printLog(` 🎉 wedecode 有新版本: v${pkg.version} --> v${updateInfo.latest} 🎄 您可以直接使用 ${colors.blue(`npm i -g wedecode@${updateInfo.latest}`)} 进行更新 💬 npm地址: https://www.npmjs.com/package/wedecode `, { isStart: true }); } else { printLog(` 🎄 当前使用版本: v${pkg.version} `, { isStart: true }); } } }; } function createSlogan(str = " wedecode") { const slogan = figlet.textSync(str, { horizontalLayout: "default", verticalLayout: "default", whitespaceBreak: true }); return colors.bold(colors.yellow(slogan)); } async function startCacheQuestionProcess(isClear, inputPath, outputPath) { const OUTPUT_PATH = path.resolve(outputPath); if (fs.existsSync(OUTPUT_PATH)) { const isClearCache = isClear ? CacheClearEnum.clear : (await prompts.isClearOldCache(OUTPUT_PATH))["isClearCache"]; if (isClearCache === CacheClearEnum.clear || isClear) { fs.rmSync(OUTPUT_PATH, { recursive: true }); printLog(` ▶ 移除旧产物成功 `); } } } function checkExistsWithFilePath(targetPath, opt) { const { throw: isThrow = true, checkWxapkg = true, showInputPathLog = true } = opt || {}; const printErr = (log) => { if (showInputPathLog) { console.log("\n输入路径: ", colors.yellow(path.resolve(targetPath))); } isThrow && console.log(`${colors.red(`❌ ${log}`)} `); }; if (!fs.existsSync(targetPath)) { printErr("文件 或者 目录不存在, 请检查!"); return false; } if (checkWxapkg) { const isDirectory = fs.statSync(targetPath).isDirectory(); if (isDirectory) { const wxapkgPathList = glob.globSync(`${targetPath}/*.wxapkg`); if (!wxapkgPathList.length) { console.log( "\n", colors.red("❌ 文件夹下不存在 .wxapkg 包"), colors.yellow(path.resolve(targetPath)), "\n" ); return false; } } } return true; } function stopCommander() { console.log(colors.red("❌ 操作已主动终止!")); process$1.exit(0); } function getPathSplitList(_path) { let delimiter = "\\"; let partList; partList = _path.split("\\"); if (partList.length <= 1) { delimiter = "/"; partList = _path.split("/"); } return { partList, delimiter }; } function findWxAppIdPath(_path) { const { partList, delimiter } = getPathSplitList(_path); let newPathList = [...partList]; for (const index in partList.reverse()) { const dirName = partList[index]; if (isWxAppid(dirName)) { break; } newPathList.pop(); } return newPathList.join(delimiter).trim(); } function findWxAppIdForPath(_path) { return path.parse(findWxAppIdPath(_path)).name; } async function internetAvailable() { return axios.request({ url: "https://bing.com", maxRedirects: 0, timeout: 2e3, validateStatus: () => true }).then(() => true).catch(() => Promise.resolve(false)); } function inDangerScanPathList(_path) { _path = path.resolve(_path); let partList; if (_path.includes(":")) _path = _path.split(":")[1]; partList = getPathSplitList(_path).partList; return partList.map((s) => s.trim()).filter(Boolean).length <= 1; } function findWxMiniProgramPackDir(manualScanPath) { const foundPackageList = []; glob.globSync(path.resolve(manualScanPath, "**/*.wxapkg"), { dot: true, windowsPathsNoEscape: true, nocase: true }).map((_path) => { const foundMainPackage = AppMainPackageNames.find((fileName) => _path.endsWith(fileName)); if (foundMainPackage) return _path; return false; }).filter(Boolean).reduce((pre, cur) => { if (pre.includes(cur)) return pre; pre.push(cur); return pre; }, []).forEach((_path) => { const foundPath = findWxAppIdPath(_path); const isFoundWxId = !!foundPath; let appIdPath = path.dirname(_path); const { partList } = getPathSplitList(appIdPath); let appId = partList.filter(Boolean).pop(); if (isFoundWxId) { appIdPath = foundPath; appId = findWxAppIdForPath(_path); } foundPackageList.push({ isAppId: isFoundWxId, appId, path: isFoundWxId ? foundPath : appIdPath, storagePath: path.dirname(_path) }); }); return foundPackageList; } async function sacnPackages(manualScanPath = "") { const foundPackageList = []; let scanPathList = globPathList; if (Boolean(manualScanPath.trim())) { const absolutePath = path.resolve(manualScanPath); if (inDangerScanPathList(absolutePath)) { const { dangerScan } = await prompts.showDangerScanPrompt(absolutePath); if (dangerScan === YesOrNoEnum.no) { stopCommander(); } } scanPathList = [absolutePath]; } if (scanPathList.length) { console.log(" 扫描中..."); } scanPathList.forEach((matchPath) => { const foundPList = findWxMiniProgramPackDir(matchPath); foundPList.forEach((item) => foundPackageList.push(item)); }); if (foundPackageList.length === 0) { console.log(` ${colors.red("未找到小程序包,您需要电脑先访问某个小程序后产生缓存再扫描, 如果还扫描不到请反馈 ")} 当前所处目录: $ ${colors.yellow(path.resolve(manualScanPath || "./"))} ▶ 随着微信版本更新, 新版本小程序路径可能和已知位置不一样, 如果出现问题请到 github 反馈 ▶ 提交时请带上您电脑中小程序的 '${colors.bold("微信官方的 wxapkg 包在硬盘中的存放路径")}' 和 '${colors.bold("微信版本号")}' ▶ https://github.com/biggerstar/wedecode/issues `); stopCommander(); } return foundPackageList; } async function startSacnPackagesProcess(manualScanPath) { const foundPackageList = await sacnPackages(manualScanPath); const columns = [ { name: "名字", value: "appName" }, { name: "修改时间", value: "updateDate" }, { name: "描述", value: "description" } ]; const rowsPromiseList = foundPackageList.map(async (item) => { const statInfo = fs.statSync(item.storagePath); const date = new Date(statInfo.mtime); const dateString = `${date.getMonth() + 1}/${date.getDate()} ${date.toLocaleTimeString()}`; if (!item.isAppId) return { appName: item.appId, updateDate: dateString, description: item.storagePath }; const appId = item.appId; const { nickname, description: description2 } = await getWxAppInfo(appId); return { appName: nickname || appId, updateDate: dateString, description: description2 || "" }; }); if (rowsPromiseList.length) { console.log(" 获取小程序信息中..."); } const rows = await Promise.all(rowsPromiseList); if (rowsPromiseList.length) { clearScreen(); console.log("$ 选择一个包进行编译: "); } const result = await prompts.showScanPackTable({ columns, rows }); const foundIndex = rows.findIndex((item) => { var _a; return item.appName === ((_a = result.packInfo) == null ? void 0 : _a.appName); }); const packInfo = { ...rows[foundIndex], ...foundPackageList[foundIndex] }; console.log(`$ 选择了 ${packInfo.appName}( ${packInfo.appId} )`); return packInfo; } async function getWxAppInfo(appid) { const options = { method: "POST", timeout: 6e3, url: "https://kainy.cn/api/weapp/info/", headers: { "content-type": "application/json" }, data: { appid } }; return axios.request(options).then((res) => { var _a; return Promise.resolve(((_a = res.data) == null ? void 0 : _a.data) || {}); }).catch(() => { return Promise.resolve({}); }); } function readLocalFile(path2, encoding = "utf-8") { return fs.existsSync(path2) ? fs.readFileSync(path2, encoding) : ""; } function readLocalJsonFile(path2, encoding = "utf-8") { try { return JSON.parse(readLocalFile(path2, encoding)); } catch (e) { return null; } } function saveLocalFile(filepath, data, opt = {}) { filepath = filepath.replace(pluginDirRename[0], pluginDirRename[1]); const targetData = fs.existsSync(filepath) ? fs.readFileSync(filepath, { encoding: "utf-8" }).trim() : ""; let force = typeof opt.force === "boolean" ? opt.force : opt.emptyInstead || !targetData.length; const outputDirPath = path.dirname(filepath); const isExistsFile = fs.existsSync(filepath); const isExistsPath = fs.existsSync(outputDirPath); if (isExistsFile && !force) return false; if (!isExistsPath) { fs.mkdirSync(outputDirPath, { recursive: true }); } fs.writeFileSync(filepath, data); return true; } function deleteLocalFile(path2, opt = {}) { try { fs.rmSync(path2, opt); } catch (e) { if (!opt.catch) throw e; } } const systemInfo = { windowWidth: 375, windowHeight: 600, pixelRatio: 2, language: "en", version: "1.9.90", platform: "ios" }; function createWxFakeDom() { return { console, setTimeout, setInterval, clearTimeout, clearInterval, __wxConfig: {}, App() { }, Component() { }, Page() { }, getApp: () => ({}), require: () => void 0, module: {}, exports: {}, global: {}, Behavior: function() { }, getCurrentPages: () => [], requireMiniProgram: function() { }, $gwx: () => void 0, WXWebAssembly: {}, __wxCodeSpace__: {}, wx: { request() { }, getExtConfig() { }, getExtConfigSync() { }, postMessageToReferrerPage: function() { }, postMessageToReferrerMiniProgram: function() { }, onUnhandledRejection: function() { }, onThemeChange: function() { }, onPageNotFound: function() { }, onLazyLoadError: function() { }, onError: function() { }, onAudioInterruptionEnd: function() { }, onAudioInterruptionBegin: function() { }, onAppShow: function() { }, onAppHide: function() { }, offUnhandledRejection: function() { }, offThemeChange: function() { }, offPageNotFound: function() { }, offLazyLoadError: function() { }, offError: function() { }, offAudioInterruptionEnd: function() { }, offAudioInterruptionBegin: function() { }, offAppShow: function() { }, offAppHide: function() { }, getStorageSync: function() { }, setStorageSync: function() { }, getStorage: function() { }, setStorage: function() { }, getSystemInfo() { return systemInfo; }, getSystemInfoSync() { return systemInfo; }, getRealtimeLogManager() { return { log: (msg) => console.log(msg), err: (msg) => console.error(msg) }; }, getMenuButtonBoundingClientRect() { return { top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0 }; } } }; } function createVM(vmOptions = {}) { const domBaseHtml = `<!DOCTYPE html><html lang="en"><head><title>''</title></head><body></body></html>`; const dom = new JSDOM(domBaseHtml); const vm_window = dom.window; const vm_navigator = dom.window.navigator; const vm_document = dom.window.document; const __wxAppCode__ = {}; const fakeGlobal = { __wxAppCode__, publishDomainComponents: () => void 0 }; Object.assign(vm_window, fakeGlobal); return new VM(deepmerge({ sandbox: { ...createWxFakeDom(), setInterval: () => null, setTimeout: () => null, console: { ...console, // 在 vm 执行的时候,对于小程序源码中的 info, log, warn 打印直接忽略 log: () => void 0, warn: () => void 0, info: () => void 0 }, window: vm_window, location: dom.window.location, navigator: vm_navigator, document: vm_document, define: () => void 0, require: () => void 0, requirePlugin: () => void 0, global: { __wcc_version__: "v0.5vv_20211229_syb_scopedata" }, System: { register: () => void 0 }, __vd_version_info__: {}, __wxAppCode__, __wxCodeSpace__: { setRuntimeGlobals: () => void 0, addComponentStaticConfig: () => void 0, setStyleScope: () => void 0, enableCodeChunk: () => void 0, initializeCodeChunk: () => void 0, addTemplateDependencies: () => void 0, batchAddCompiledScripts: () => void 0, batchAddCompiledTemplate: () => void 0 } } }, vmOptions)); } function runVmCode(vm, code) { try { vm.run(code); } catch (e) { console.log(e.message); } } class PloyFillCover { constructor(packPath) { __publicField(this, "allPloyFills", []); const customHeaderPathPart = path.resolve(path.dirname(packPath), "polyfill"); const customPloyfillGlobMatch = path.resolve(customHeaderPathPart, "./**/*.js"); const customPloyfill = glob.globSync(customPloyfillGlobMatch); const customPloyfillInfo = customPloyfill.map((str) => { return { fullPath: str, ployfillPath: path.relative(customHeaderPathPart, str) }; }); const urls = new URL(import.meta.url); const headerPathPart = path.resolve(path.dirname(urls.pathname), "polyfill"); const ployfillGlobMatch = path.resolve(headerPathPart, "./**/*.js"); let builtinPloyfill = glob.globSync(ployfillGlobMatch); const builtinPloyfillInfo = builtinPloyfill.map((str) => { return { fullPath: str, ployfillPath: path.relative(headerPathPart, str) }; }); this.allPloyFills = [...customPloyfillInfo, ...builtinPloyfillInfo]; } findPloyfill(targetPath) { return this.allPloyFills.find((item) => { return targetPath.endsWith(item.ployfillPath); }); } } var PackTypeMapping = /* @__PURE__ */ ((PackTypeMapping2) => { PackTypeMapping2["main"] = "主包"; PackTypeMapping2["sub"] = "分包"; PackTypeMapping2["independent"] = "独立分包"; return PackTypeMapping2; })(PackTypeMapping || {}); var AppTypeMapping = /* @__PURE__ */ ((AppTypeMapping2) => { AppTypeMapping2["app"] = "小程序"; AppTypeMapping2["game"] = "小游戏"; return AppTypeMapping2; })(AppTypeMapping || {}); class BaseDecompilation { constructor(packInfo) { __publicField(this, "pathInfo"); __publicField(this, "outputPathInfo"); __publicField(this, "packPath"); __publicField(this, "packType"); __publicField(this, "appType"); __publicField(this, "ployFill"); this.pathInfo = packInfo.pathInfo; this.outputPathInfo = packInfo.outputPathInfo; this.packPath = packInfo.inputPath; this.packType = packInfo.packType; this.appType = packInfo.appType; this.ployFill = new PloyFillCover(this.packPath); } async decompileAppWorker() { await sleep(200); if (!fs.existsSync(this.pathInfo.workersPath)) { return; } const appConfigString = readLocalFile(this.pathInfo.appJsonPath); if (!appConfigString) return; const appConfig = JSON.parse(appConfigString); let code = readLocalFile(this.pathInfo.workersPath); let commPath = ""; let vm = createVM({ sandbox: { define(name2) { name2 = path.dirname(name2) + "/"; if (!commPath) commPath = name2; commPath = commonDir(commPath, name2); } } }); runVmCode(vm, code.slice(code.indexOf("define("))); if (commPath.length > 0) commPath = commPath.slice(0, -1); printLog(`Worker path: ${commPath}`); appConfig.workers = commPath; saveLocalFile(this.pathInfo.appJsonPath, JSON.stringify(appConfig, null, 2)); printLog(` ▶ 反编译 Worker 文件成功. `, { isStart: true }); } /** * 反编译 Worker 文件 * */ async decompileAppWorkers() { await sleep(200); if (!fs.existsSync(this.pathInfo.workersPath)) { return; } const _this = this; let commPath = ""; let code = readLocalFile(this.pathInfo.workersPath); let vm = createVM({ sandbox: { define(name2, func) { _this._parseJsDefine(name2, func); const workerPath = path.dirname(name2) + "/"; if (!commPath) commPath = workerPath; commPath = commonDir(commPath, workerPath); } } }); runVmCode(vm, code); printLog(`Worker path: ${commPath}`); if (commPath) { const configFileName = this.appType === "game" ? this.pathInfo.gameJsonPath : this.pathInfo.appJsonPath; const appConfig = JSON.parse(readLocalFile(configFileName)); appConfig.workers = commPath; saveLocalFile(configFileName, JSON.stringify(appConfig, null, 2), { force: true }); } printLog(` ▶ 反编译 Worker 文件成功. `, { isStart: true }); } decompileAll() { printLog(` ▶ 当前反编译目标[ ${AppTypeMapping[this.appType]} ] (${colors.yellow(PackTypeMapping[this.packType])}) : ` + colors.blue(this.packPath)); printLog(` ▶ 当前输出目录: ${colors.blue(this.pathInfo.outputPath)} `, { isEnd: true }); } _parseJsDefine(name2, func) { if (path.extname(name2) !== ".js") return; const foundPloyfill = this.ployFill.findPloyfill(name2); let resultCode = ""; if (foundPloyfill) { resultCode = readLocalFile(foundPloyfill.fullPath); } else { let code = func.toString(); code = code.slice(code.indexOf("{") + 1, code.lastIndexOf("}") - 1).trim(); if (code.startsWith('"use strict";')) { code = code.replaceAll('"use strict";', ""); } else if (code.startsWith("'use strict';")) { code = code.replaceAll(`'use strict';`, ""); } else if ((code.startsWith('(function(){"use strict";') || code.startsWith("(function(){'use strict';")) && code.endsWith("})();")) { code = code.slice(25, -5); } code = code.replaceAll('require("@babel', 'require("./@babel'); resultCode = jsBeautify(code.trim()); } saveLocalFile( this.pathInfo.outputResolve(name2), removeVM2ExceptionLine(resultCode.trim()), { force: true } ); printLog(` Completed (${resultCode.length}) ` + colors.bold(colors.gray(name2))); } } function getAppPackCodeInfo(pathInfo, opt = {}) { const { adaptLen = 100 } = opt || {}; function __readFile(path2) { if (!path2) return ""; const content = readLocalFile(path2); return content.length > adaptLen ? content : ""; } let pageFrameHtmlCode = __readFile(pathInfo.pageFrameHtmlPath); if (pageFrameHtmlCode) { const $ = cheerio.load(pageFrameHtmlCode); pageFrameHtmlCode = $("script").text(); } const appServiceCode = __readFile(pathInfo.appServicePath); const appServiceAppCode = __readFile(pathInfo.appServiceAppPath); return { appConfigJson: __readFile(pathInfo.appConfigJsonPath), appWxss: __readFile(pathInfo.appWxssPath), appService: appServiceCode, appServiceApp: appServiceAppCode, pageFrame: __readFile(pathInfo.pageFramePath), workers: __readFile(pathInfo.workersPath), pageFrameHtml: pageFrameHtmlCode }; } function getGamePackCodeInfo(pathInfo, opt = {}) { const { adaptLen = 100 } = opt || {}; function __readFile(path2) { if (!path2) return ""; const content = readLocalFile(path2); return content.length > adaptLen ? content : ""; } return { workers: __readFile(pathInfo.workersPath), gameJs: __readFile(pathInfo.gameJsPath), appConfigJson: __readFile(pathInfo.appConfigJsonPath), subContextJs: __readFile(pathInfo.subContextJsPath) }; } class GameDecompilation extends BaseDecompilation { constructor(packInfo) { super(packInfo); __publicField(this, "codeInfo"); __publicField(this, "wxsList"); __publicField(this, "allRefComponentList", []); __publicField(this, "allSubPackagePages", []); __publicField(this, "allPloyFill", []); } /** * 初始化小游戏所需环境和变量 * */ async initGame() { this.codeInfo = getGamePackCodeInfo(this.pathInfo); const loadInfo = {}; for (const name2 in this.codeInfo) { loadInfo[name2] = this.codeInfo[name2].length; } console.log(loadInfo); } /** * 反编译 game.json 文件, 只有主包需要处理 * */ async decompileGameJSON() { if (this.packType !== "main") return; await sleep(200); const gameConfigString = this.codeInfo.appConfigJson; const gameConfig = JSON.parse(gameConfigString); Object.assign(gameConfig, gameConfig.global); GameJsonExcludeKeys.forEach((key) => delete gameConfig[key]); const outputFileName = "game.json"; const gameConfigSaveString = JSON.stringify(gameConfig, null, 2); saveLocalFile(this.pathInfo.outputResolve(outputFileName), gameConfigSaveString, { force: true }); printLog(` Completed (${gameConfigSaveString.length}) ` + colors.bold(colors.gray(this.pathInfo.outputResolve(outputFileName)))); printLog(` ▶ 反编译 ${outputFileName} 文件成功. `, { isStart: true }); } /** * 反编译小游戏的js文件 * */ async decompileGameJS() { const _this = this; let cont = 0; const evalCodeList = [ this.codeInfo.subContextJs, this.codeInfo.gameJs ].filter(Boolean); const sandbox = { define(name2, func) { _this._parseJsDefine(name2, func); cont++; }, require() { } }; evalCodeList.forEach((code) => { const vm = createVM({ sandbox }); if (!code.includes("define(") || !code.includes("function(require, module, exports)")) return; try { runVmCode(vm, code); } catch (e) { console.log(e.message); } }); if (cont) { printLog(` ▶ 反编译所有 js 文件成功. `); } } async decompileAll() { super.decompileAll(); await this.initGame(); await this.decompileGameJSON(); await this.decompileGameJS(); await this.decompileAppWorkers(); } } function restoreSingle(ops, withScope = false) { if (typeof ops == "undefined") return ""; function scope(value) { if (withScope) return value; if (value.startsWith("{") && value.endsWith("}")) return "{{(" + value + ")}}"; if (value.startsWith("...")) return "{" + value.substring(3) + "}"; return "{{" + value + "}}"; } function enBrace(value, type2 = "{") { if (value.startsWith("{") || value.startsWith("[") || value.startsWith("(") || value.endsWith("}") || value.endsWith("]") || value.endsWith(")")) { value = " " + value + " "; } switch (type2) { case "{": return "{" + value + "}"; case "[": return "[" + value + "]"; case "(": return "(" + value + ")"; default: throw Error("Unknown brace type " + type2); } } function restoreNext(ops2, w = withScope) { return restoreSingle(ops2, w); } function jsoToWxOn(obj) { let ans = ""; if (typeof obj === "undefined") { return "undefined"; } else if (obj === null) { return "null"; } else if (obj instanceof RegExp) { return obj.toString(); } else if (obj instanceof Array) { for (let i = 0; i < obj.length; i++) ans += "," + jsoToWxOn(obj[i]); return enBrace(ans.slice(1), "["); } else if (typeof obj == "object") { for (let k in obj) ans += "," + k + ":" + jsoToWxOn(obj[k]); return enBrace(ans.slice(1), "{"); } else if (typeof obj == "string") { let parts = obj.split('"'), ret = []; for (let part of parts) { let atoms = part.split("'"), ans2 = []; for (let atom of atoms) ans2.push(JSON.stringify(atom).slice(1, -1)); ret.push(ans2.join("\\'")); } return "'" + ret.join('"') + "'"; } else return JSON.stringify(obj); } let op = ops[0]; if (!Array.isArray(op)) { switch (op) { case 3: return ops[1]; case 1: const val = jsoToWxOn(ops[1]); return scope(val); case 11: let ans = ""; ops.shift(); for (let perOp of ops) ans += restoreNext(perOp); return ans; } } else { let ans = ""; switch (op[0]) { case 2: { let getPrior = function(op2, len) { const priorList = { "?:": 4, "&&": 6, "||": 5, "+": 13, "*": 14, "/": 14, "%": 14, "|": 7, "^": 8, "&": 9, "!": 16, "~": 16, "===": 10, "==": 10, "!=": 10, "!==": 10, ">=": 11, "<=": 11, ">": 11, "<": 11, "<<": 12, ">>": 12, "-": len === 3 ? 13 : 16 }; return priorList[op2] ? priorList[op2] : 0; }, getOp = function(i) { let ret = restoreNext(ops[i], true); if (ops[i] instanceof Object && typeof ops[i][0] == "object" && ops[i][0][0] === 2) { if (getPrior(op[1], ops.length) > getPrior(ops[i][0][1], ops[i].length)) ret = enBrace(ret, "("); } return ret; }; switch (op[1]) { case "?:": ans = getOp(1) + "?" + getOp(2) + ":" + getOp(3); break; case "!": case "~": ans = op[1] + getOp(1); break; case "-": if (ops.length !== 3) { ans = op[1] + getOp(1); break; } default: ans = getOp(1) + op[1] + getOp(2); } break; } case 4: ans = restoreNext(ops[1], true); break; case 5: { switch (ops.length) { case 2: ans = enBrace(restoreNext(ops[1], true), "["); break; case 1: ans = "[]"; break; default: { let a = restoreNext(ops[1], true); if (a.startsWith("[") && a.endsWith("]")) { if (a !== "[]") { ans = enBrace(a.slice(1, -1).trim() + "," + restoreNext(ops[2], true), "["); } else { ans = enBrace(restoreNext(ops[2], true), "["); } } else { ans = enBrace("..." + a + "," + restoreNext(ops[2], true), "["); } } } break; } case 6: { let sonName = restoreNext(ops[2], true); if (sonName._type === "var") { ans = restoreNext(ops[1], true) + enBrace(sonName, "["); } else { let attach = ""; if (/^[A-Za-z\_][A-Za-z\d\_]*$/.test(sonName)) attach = "." + sonName; else attach = enBrace(sonName, "["); ans = restoreNext(ops[1], true) + attach; } break; } case 7: { switch (ops[1][0]) { case 11: ans = enBrace("__unTestedGetValue:" + enBrace(jsoToWxOn(ops), "["), "{"); break; case 3: ans = new String(ops[1][1]); ans["_type"] = "var"; break; default: throw Error("Unknown type to get value"); } break; } case 8: ans = enBrace(ops[1] + ":" + restoreNext(ops[2], true), "{"); break; case 9: { let type2 = function(x) { if (x.startsWith("...")) return 1; if (x.startsWith("{") && x.endsWith("}")) return 0; return 2; }; let a = restoreNext(ops[1], true); let b = restoreNext(ops[2], true); let xa = type2(a), xb = type2(b); if (xa == 2 || xb == 2) ans = enBrace("__unkownMerge:" + enBrace(a + "," + b, "["), "{"); else { if (!xa) a = a.slice(1, -1).trim(); if (!xb) b = b.slice(1, -1).trim(); ans = enBrace(a + "," + b, "{"); } break; } case 10: ans = "..." + restoreNext(ops[1], true); break; case 12: { let arr = restoreNext(ops[2], true); if (arr.startsWith("[") && arr.endsWith("]")) ans = restoreNext(ops[1], true) + enBrace(arr.slice(1, -1).trim(), "("); else ans = restoreNext(ops[1], true) + ".apply" + enBrace("null," + arr, "("); break; } default: ans = enBrace("__unkownSpecific:" + jsoToWxOn(ops), "{"); } return scope(ans); } } function catchZ(code, cb) { const reg = /function\s+gz\$gwx(\w+)\(\)\{(?:.|\n)*?;return\s+__WXML_GLOBAL__\.ops_cached\.\$gwx[\w\n]+}/g; const allGwxFunctionMatch = code.match(reg); if (!allGwxFunctionMatch) return; const zObject = {}; const vm = createVM({ sandbox: { __WXML_GLOBAL__: { ops_cached: {} } } }); allGwxFunctionMatch.forEach((funcString) => { var _a; const funcReg = /function\s+gz\$gwx(\w*)\(\)/g; const funcName = (_a = funcReg.exec(funcString)) == null ? void 0 : _a[1]; if (!funcName) return; vm.run(funcString); const hookZFunc = vm.sandbox[`gz$gwx${funcName}`]; if (hookZFunc) { zObject[funcName] = hookZFunc(); zObject[funcName] = zObject[funcName].map((data) => { if (Array.isArray(data) && data[0] === "11182016" && Array.isArray(data[1])) return data[1]; return data; }); } }); cb(zObject); } function getZ(code, cb) { catchZ(code, (z) => { let ans = {}; for (let gwxFuncName in z) { ans[gwxFuncName] = z[gwxFuncName].map((gwxData) => restoreSingle(gwxData, false)); } cb(ans); }); } function analyze(core, z, namePool, xPool, fakePool = {}, zMulName = "0") { function anaRecursion(core2, fakePool2 = {}) { return analyze(core2, z, namePool, xPool, fakePool2, zMulName); } function push(name2, elem) { namePool[name2] = elem; } function pushSon(pname, son) { if (fakePool[pname]) fakePool[pname].son.push(son); else namePool[pname].son.push(son); } for (let ei = 0; ei < core.length; ei++) { let e = core[ei]; switch (e.type) { case "ExpressionStatement": { let f = e.expression; if (f.callee) { if (f.callee.type == "Identifier") { switch (f.callee.name) { case "_r": namePool[f.arguments[0].name].v[f.arguments[1].value] = z[f.arguments[2].value]; break; case "_rz": namePool[f.arguments[1].name].v[f.arguments[2].value] = z[zMulName][f.arguments[3].value]; break; case "_": pushSon(f.arguments[0].name, namePool[f.arguments[1].name]); break; case "_2": { let item = f.arguments[6].value; let index = f.arguments[7].value; let data = z[f.arguments[0].value]; let key = escodegen.generate(f.arguments[8]).slice(1, -1); let obj = namePool[f.arguments[5].name]; let gen = namePool[f.arguments[1].name]; if (gen.tag == "gen") { let ret = gen.func.body.body.pop().argument.name; anaRecursion(gen.func.body.body, { [ret]: obj }); } obj.v["wx:for"] = data; if (index != "index") obj.v["wx:for-index"] = index; if (item != "item") obj.v["wx:for-item"] = item; if (key != "") obj.v["wx:key"] = key; } break; case "_2z": { let item = f.arguments[7].value; let index = f.arguments[8].value; let data = z[zMulName][f.arguments[1].value]; let key = escodegen.generate(f.arguments[9]).slice(1, -1); let obj = namePool[f.arguments[6].name]; let gen = namePool[f.arguments[2].name]; if (gen.tag == "gen") { let ret = gen.func.body.body.pop().argument.name; anaRecursion(gen.func.body.body, { [ret]: obj }); } obj.v["wx:for"] = data; if (index != "index") obj.v["wx:for-index"] = index; if (item != "item") obj.v["wx:for-item"] = item; if (key != "") obj.v["wx:key"] = key; } break; case "_ic": pushSon(f.arguments[5].name, { tag: "include", son: [], v: { src: xPool[f.arguments[0].property.value] } }); break; case "_ai": { let to = Object.keys(fakePool)[0]; if (to) pushSon(to, { tag: "import", son: [], v: { src: xPool[f.arguments[1].property.value] } }); else throw Error("Unexpected fake pool"); } break; case "_af": break; default: throw Error("Unknown expression callee name " + f.callee.name); } } else if (f.callee.type == "MemberExpression") { if (f.callee.object.name == "cs" || f.callee.property.name == "pop") break; throw Error("Unknown member expression"); } else throw Error("Unknown callee type " + f.callee.type); } else if (f.type == "AssignmentExpression" && f.operator == "=") ; else throw Error("Unknown expression statement."); break; } case "VariableDeclaration": for (let dec of e.declarations) { if (dec.init.type == "CallExpression") { switch (dec.init.callee.name) { case "_n": let tagName = dec.init.arguments[0].value; if (["wx-scope"].includes(tagName)) { tagName = "view"; } push(dec.id.name, { tag: tagName, son: [], v: {} }); break; case "_v": push(dec.id.name, { tag: "block", son: [], v: {} }); break; case "_o": push(dec.id.name, { tag: "__textNode__", textNode: true, content: z[dec.init.arguments[0].value] }); break; case "_oz": push(dec.id.name, { tag: "__textNode__", textNode: true, content: z[zMulName][dec.init.arguments[1].value] }); break; case "_