const fs = require("fs"); const https = require("https"); const { parse: parseHtml } = require('node-html-parser'); const { parseScript } = require('esprima'); const { exec, execSync } = require("child_process"); const FLYFF_URL = "https://universe.flyff.com/play"; const OUTPUT_DIR = "./out/"; const { edit } = require("@webassemblyjs/wasm-edit"); const IMPORT_NAMESPACE = "flyff"; if (!fs.existsSync(OUTPUT_DIR)) fs.mkdirSync(OUTPUT_DIR); download(FLYFF_URL, main); function main(html) { const root = parseHtml(html); const launcherScript = (() => { for(const script of root.querySelectorAll("script[type='text/javascript']")) { if(script.innerText.indexOf("FilemapVersion") > 0) { return script; } } })(); const launcherJs = launcherScript.innerText; if(launcherJs.length == 0) { console.error("Unable to get launcher javascript."); } const version = (() => { // Figure out which FilemapVersion the client currently uses. // HACKY: // - The JS that's downloaded is written for use in HTML. // - The fact that it crashes very fast is abused. try { eval(launcherJs); } catch (e) { } return FilemapVersion; })(); if (!fs.existsSync(OUTPUT_DIR + version)) fs.mkdirSync(OUTPUT_DIR + version); // Write the html out for debugging, if something has changed.try { fs.writeFileSync(OUTPUT_DIR + version + "/index.html", html); console.log("Detected version: " + version); const binaryUrl = "https://gcpcdn-universe.flyff.com/client/program/web/main-wasm32.wasm?" + version; const emscriptenUrl = "https://gcpcdn-universe.flyff.com/client/program/web/main-wasm32.js?" + version; const binaryPath = OUTPUT_DIR + version + "/client-original.wasm"; const file = fs.createWriteStream(binaryPath); https.get(binaryUrl, function (response) { response.pipe(file); // after download completed close filestream file.on("finish", () => { file.close(); console.log("Binary: downloaded."); download(emscriptenUrl, (js) => { const path = OUTPUT_DIR + version + "/client-original.js"; fs.writeFileSync(path, js); console.log("Javascript Environment: downloaded."); const importFnMap = new Map(); const exportFnMap = new Map(); const handleVariableDeclarator = (node, meta) => { if(node.id.name === "wasmImports") { // Find minified import names. for(const property of node.init.properties) { const obfuscated = property.key.value; const nice = property.value.name; importFnMap.set(obfuscated, nice); } } else { // Find minified export names. if(node.init && node.init.type === "AssignmentExpression" && node.init.right && node.init.right.type === "FunctionExpression") { const nice = node.id.name; // What a mess... const initExpression = node.init.right; const returnStatement = initExpression.body.body[0]; const callee = returnStatement.argument.callee; const computedMemberExpression = callee.object.right.right; const obfuscated = computedMemberExpression.property.value; exportFnMap.set(obfuscated, nice); } } } console.log("Javascript Environment: Finding minified function map..."); parseScript(js, null, (node, meta) => { if(node.type === "VariableDeclarator") return handleVariableDeclarator(node, meta); if(node.type === "ExpressionStatement") { // Find minified memory and table names. if((node.expression.left?.name === "wasmTable" || node.expression.left?.name === "wasmMemory") && node.expression.right?.object?.object?.name === "Module") { const obfuscated = node.expression.right.property.value; const jsName = node.expression.left.name; const nice = jsName === "wasmMemory" ? "memory" : jsName === "wasmTable" ? "table" : ""; exportFnMap.set(obfuscated, nice); } } }); // Use this to identify what you are looking for. // parseScript("var wasmTable; wasmTable = other['kc'];", null, (node, meta) => { // console.log(node); // }); const jsonObj = { // Sort 'm imports: Object.fromEntries(new Map([...importFnMap.entries()].sort())), exports: Object.fromEntries(new Map([...exportFnMap.entries()].sort())), }; fs.writeFileSync(OUTPUT_DIR + version + "/functions.json", JSON.stringify(jsonObj, null, 4)); // console.log(fnMap); console.log("Identified " + importFnMap.size + " imported, and " + exportFnMap.size + " exported symbols!"); // console.log("Loading binary from " + binaryPath); // const binary = fs.readFileSync(binaryPath); // console.log("Loaded binary."); // const visitors = { // ModuleImport({ node }) { // console.log("Renaming " + node.module + "::" + node.name + " to " + IMPORT_NAMESPACE + "::" + fnMap[node.name]); // // console.log("MODULE IMPORT: ", node); // node.module = "flyff"; // node.name = fnMap[node.name]; // } // }; // console.log("Fixing imports (this may take a while...)"); // const newBinary = edit(binary, visitors); // console.log("Fixed imports, writing new binary..."); // console.log(typeof(newBinary), newBinary); // fs.writeFileSync(binaryPath, Buffer.from(newBinary)); // console.log("Done writing new binary."); // console.log("Binary: Starting decompilation..."); // const outputPath = OUTPUT_DIR + version + "/client.c"; // execSync("wasm2c " + binaryPath + " -o " + outputPath); // console.log("Binary: Decompilation finished."); // Tag our header file so we know what version of the filemap was when decompiled. // let data = fs.readFileSync(OUTPUT_DIR + version + "/client.h", 'utf8'); // data = "/* Automagically edited by fugg-fetch for FilemapVersion " + version + " */\n\n" + data; // fs.writeFileSync(OUTPUT_DIR + version + "/client.h", data); // console.log("Binary: wrote FilemapVersion to file."); process.exit(); }); }); }); } function download(url, cb) { https.get(url, (res) => { console.log("Got response: " + res.statusCode); let body = ""; res.on("data", function (chunk) { body += chunk; }); res.on('end', function () { cb(body); }); }).on('error', function (e) { console.log("Unable to retrieve webpage at '" + FLYFF_URL + "': " + e.message); }); }