Skip to content

Instantly share code, notes, and snippets.

@SamuelScheit
Created January 29, 2023 13:10
Show Gist options
  • Save SamuelScheit/3a44237539b44f1a5e207847fc0b0d8e to your computer and use it in GitHub Desktop.
Save SamuelScheit/3a44237539b44f1a5e207847fc0b0d8e to your computer and use it in GitHub Desktop.
Java Class Name Deobfuscater based on Logs/Strings
const { copyFile, mkdir, readdir, readFile, rm, unlink, writeFile } = require("fs/promises");
const { dirname, resolve } = require("path");
async function* getFiles(dir) {
const dirents = await readdir(dir, { withFileTypes: true });
for (const dirent of dirents) {
const res = resolve(dir, dirent.name);
if (dirent.isDirectory()) {
yield* getFiles(res);
} else {
yield res;
}
}
}
var files = [];
var i = 0;
console.error = () => {};
const FileCache = new Map();
const ClassNames = new Set();
const replacements = [];
const CWD = process.cwd() + "/deobfuscated/";
const PropertyRegex = /public( *static *)?( *final *)?( *volatile *)? ?([\w.]+) (A0[\w])(;| = )/g;
const LogRegex = /Log\.\w\("([^,\n]+)"/g;
const StringRegex = /"([^"]*)"/g;
const ClassRegex = /public *( *abstract *)?( *final *)?( *\/\* synthetic \*\/ *)?(class|interface) ([^\s]+)/;
const getNameRegex = /public String getName\(\) {\n.+return "(.+)";/;
const threads = 100;
function logSameLine(text) {
process.stdout.write("\033[H\033[2J" + text);
}
function threaded(threads, iterator, callback) {
if (Array.isArray(iterator)) iterator = iterator[Symbol.iterator]();
let promises = [];
for (let i = 0; i < threads; i++) {
promises.push(
new Promise(async (resolve, reject) => {
while (true) {
let next = await iterator.next();
if (next.done) {
resolve();
break;
}
try {
await callback(await next.value);
} catch (error) {
console.log(error);
}
}
})
);
}
return Promise.all(promises);
}
async function main() {
const copyFiles = true;
if (copyFiles) {
await rm(CWD, { recursive: true, force: true });
await threaded(threads, getFiles(__dirname + "/sources/"), async (path) => {
if (!path.endsWith(".java")) return;
// if (!path.includes("/X/")) return;
const dest = CWD + path.replace(__dirname + "/sources/", "");
await mkdir(dirname(dest), { recursive: true });
await copyFile(path, dest);
logSameLine(`[File] ${i++} copied to ${dest}`);
files.push(dest);
});
await mkdir(CWD + "/api/", { recursive: true });
await mkdir(CWD + "/interface/", { recursive: true });
await mkdir(CWD + "/layout/", { recursive: true });
await mkdir(CWD + "/other/", { recursive: true });
} else {
for await (const path of getFiles(CWD)) {
files.push(path);
if (!path.endsWith(".java")) continue;
// if (!path.includes("/X/")) continue;
}
}
await threaded(threads, files, async (path) => {
await processFile(path).catch((e) => {
console.log(e);
});
});
console.log("\n[File] done");
console.log(replacements);
console.log("[Replacements] processing " + replacements.length + " entries");
await replaceInAllFiles();
console.log("\n[Replacements] done");
}
async function cacheReadFile(path, opts) {
if (FileCache.has(path)) return FileCache.get(path);
return FileCache.set(path, await readFile(path, opts || { encoding: "utf-8" })).get(path);
}
async function replaceInAllFiles() {
await threaded(threads, files, async (path) => {
logSameLine("[Replacements] processing " + path);
var source = FileCache.get(path);
const oldPath = path;
for (const [from, to] of replacements) {
source = source.replaceAll(from, to);
// also transform variable names, which have the same name, but start with a lowercase character
source = source.replaceAll(from.slice(0, 1).toLowerCase() + from.slice(1), to.slice(0, 1).toLowerCase() + to.slice(1));
if (path.includes(from)) {
await unlink(path);
path = path.replace(from, to);
}
}
const properties = new Set();
for (const x of source.matchAll(PropertyRegex)) {
const type = x[4];
const property = x[5];
if (properties.has(property)) {
var i = 0;
while (properties.has(property + i)) i++;
property += i;
}
}
if (source.includes("ProtocolTreeNode") || source.includes("xmpp")) {
path = path.replace("/X/", "/api/");
} else if (source.includes("public interface")) {
path = path.replace("/X/", "/interface/");
} else if (source.includes("android.graphics") || source.includes("android.view")) {
path = path.replace("/X/", "/layout/");
} else if (oldPath != path) {
path = path.replace("/X/", "/other/");
}
await writeFile(path, source, { encoding: "utf8" });
});
}
async function processFile(path) {
process.stdout.write("\r[File] processing " + path.replace(CWD, ""));
const source = await cacheReadFile(path, { encoding: "utf8" });
if (!path.includes("/X/")) return;
const isInterface = source.match(/public interface/);
const className = source.match(ClassRegex)?.[5];
if (!className && !isInterface) return;
const logEntries = source.matchAll(LogRegex);
const logs = [];
for (const log of logEntries) {
logs.push(log[1]);
}
var candidate = logs[0];
const getName = source.match(getNameRegex);
if (getName) candidate = getName[1];
if (!candidate) {
const literals = Array.from(source.matchAll(StringRegex))
.map((x) => x[1])
.sort((a, b) => {
if (b.includes("/")) return 1;
if (a.includes("/")) return -1;
if (b.includes(" ")) return -1;
if (a.includes(" ")) return 1;
if (b.includes("urn") || b.includes("xmpp")) return 1;
if (a.includes("urn") || a.includes("xmpp")) return -1;
return b.length - a.length;
});
candidate = literals[0];
}
if (!candidate && isInterface) {
candidate = "Interface";
}
if (!candidate) return;
candidate = candidate
.trim()
.split(".")
.map((x) => x.slice(0, 1).toUpperCase() + x.slice(1)) // make first letter capital
.join("")
.replace(/[:\s-]/g, "_")
.split("/")[0]
.replace(/[^\w\d]/g, "")
.slice(0, 100);
if (!candidate) return;
var newClassName = candidate;
if (ClassNames.has(newClassName)) {
var i = 0;
while (ClassNames.has(newClassName + i)) i++;
newClassName += i;
}
ClassNames.add(newClassName);
replacements.push([className, newClassName]);
logSameLine("[Replacements] adding " + className + " -> " + newClassName);
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment