mirror of
https://github.com/ReaJason/MemShellParty.git
synced 2026-09-21 22:50:42 +08:00
feat: support auto decompiling with cfr
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import "./prism-worker-guard";
|
||||
import { decompile } from "@run-slicer/cfr";
|
||||
import Prism from "prismjs";
|
||||
import "prismjs/components/prism-clike";
|
||||
import "prismjs/components/prism-java";
|
||||
|
||||
/** [tokenType, text] segments of one rendered line */
|
||||
export type TokenLine = Array<[string, string]>;
|
||||
|
||||
export interface DecompiledClass {
|
||||
source: string;
|
||||
lines: TokenLine[];
|
||||
}
|
||||
|
||||
export interface DecompileRequest {
|
||||
id: number;
|
||||
shellClassName: string;
|
||||
shellBytesBase64: string;
|
||||
injectorClassName: string;
|
||||
injectorBytesBase64: string;
|
||||
}
|
||||
|
||||
export interface DecompileResponse {
|
||||
id: number;
|
||||
shell?: DecompiledClass;
|
||||
injector?: DecompiledClass;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
function base64ToBytes(base64String: string) {
|
||||
const byteCharacters = atob(base64String);
|
||||
const bytes = new Uint8Array(byteCharacters.length);
|
||||
for (let i = 0; i < byteCharacters.length; i++) {
|
||||
bytes[i] = byteCharacters.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function toJvmClassName(className: string) {
|
||||
return className.replaceAll(".", "/");
|
||||
}
|
||||
|
||||
function tokenizeToLines(code: string): TokenLine[] {
|
||||
const lines: TokenLine[] = [[]];
|
||||
const push = (type: string, text: string) => {
|
||||
const parts = text.split("\n");
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (i > 0) lines.push([]);
|
||||
if (parts[i]) lines[lines.length - 1].push([type, parts[i]]);
|
||||
}
|
||||
};
|
||||
const walk = (token: string | Prism.Token, inheritedType: string) => {
|
||||
if (typeof token === "string") {
|
||||
push(inheritedType, token);
|
||||
return;
|
||||
}
|
||||
const { type, content } = token;
|
||||
if (typeof content === "string") {
|
||||
push(type, content);
|
||||
return;
|
||||
}
|
||||
for (const child of Array.isArray(content) ? content : [content]) {
|
||||
walk(child, type);
|
||||
}
|
||||
};
|
||||
for (const token of Prism.tokenize(code, Prism.languages.java)) {
|
||||
walk(token, "plain");
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
async function decompileClass(
|
||||
jvmClassName: string,
|
||||
classes: Map<string, Uint8Array>,
|
||||
): Promise<DecompiledClass> {
|
||||
const source = await decompile(jvmClassName, { source: (name) => classes.get(name) ?? null });
|
||||
return { source, lines: tokenizeToLines(source) };
|
||||
}
|
||||
|
||||
self.onmessage = async (event: MessageEvent<DecompileRequest>) => {
|
||||
const { id, shellClassName, shellBytesBase64, injectorClassName, injectorBytesBase64 } =
|
||||
event.data;
|
||||
try {
|
||||
const classes = new Map<string, Uint8Array>([
|
||||
[toJvmClassName(shellClassName), base64ToBytes(shellBytesBase64)],
|
||||
[toJvmClassName(injectorClassName), base64ToBytes(injectorBytesBase64)],
|
||||
]);
|
||||
const shell = await decompileClass(toJvmClassName(shellClassName), classes);
|
||||
const injector = await decompileClass(toJvmClassName(injectorClassName), classes);
|
||||
self.postMessage({ id, shell, injector } satisfies DecompileResponse);
|
||||
} catch (error) {
|
||||
self.postMessage({
|
||||
id,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
} satisfies DecompileResponse);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* prismjs auto-registers a `message` listener when loaded inside a Web Worker
|
||||
* (it expects JSON strings and calls JSON.parse on evt.data). Our worker
|
||||
* exchanges structured-clone objects, which crashes that handler with an
|
||||
* uncaught SyntaxError. Setting `self.Prism.disableWorkerMessageHandler`
|
||||
* before prism-core evaluates suppresses the registration.
|
||||
*
|
||||
* Imported for side effects before any prismjs import — ES module evaluation
|
||||
* order guarantees this runs first.
|
||||
*/
|
||||
(globalThis as unknown as { Prism: { disableWorkerMessageHandler: boolean } }).Prism = {
|
||||
disableWorkerMessageHandler: true,
|
||||
};
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,143 @@
|
||||
import type { DecompiledClass, DecompileRequest, DecompileResponse } from "./decompile.worker";
|
||||
import type { MemShellResult } from "@/types/memshell";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export interface DecompiledSources {
|
||||
shell: DecompiledClass | null;
|
||||
injector: DecompiledClass | null;
|
||||
}
|
||||
|
||||
interface DecompileState {
|
||||
sources: DecompiledSources;
|
||||
isDecompiling: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const IDLE: DecompileState = {
|
||||
sources: { shell: null, injector: null },
|
||||
isDecompiling: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
const CACHE_LIMIT = 8;
|
||||
const cache = new Map<string, DecompiledSources>();
|
||||
|
||||
function cacheGet(key: string) {
|
||||
const value = cache.get(key);
|
||||
if (value) {
|
||||
cache.delete(key);
|
||||
cache.set(key, value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function cacheSet(key: string, value: DecompiledSources) {
|
||||
cache.set(key, value);
|
||||
if (cache.size > CACHE_LIMIT) {
|
||||
const oldest = cache.keys().next().value;
|
||||
if (oldest !== undefined) cache.delete(oldest);
|
||||
}
|
||||
}
|
||||
|
||||
/** FNV-1a over both base64 payloads; cheap identity for one generation. */
|
||||
function cacheKey(shellBytes: string, injectorBytes: string) {
|
||||
let hash = 0x811c9dc5;
|
||||
for (const str of [shellBytes, injectorBytes]) {
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash ^= str.charCodeAt(i);
|
||||
hash = Math.imul(hash, 0x01000193);
|
||||
}
|
||||
}
|
||||
return `${(hash >>> 0).toString(36)}:${shellBytes.length}:${injectorBytes.length}`;
|
||||
}
|
||||
|
||||
let worker: Worker | null = null;
|
||||
let nextId = 0;
|
||||
const pending = new Map<
|
||||
number,
|
||||
{ resolve: (value: DecompiledSources) => void; reject: (reason: Error) => void }
|
||||
>();
|
||||
|
||||
function getWorker() {
|
||||
if (!worker) {
|
||||
worker = new Worker(new URL("./decompile.worker.ts", import.meta.url), { type: "module" });
|
||||
worker.onmessage = (event: MessageEvent<DecompileResponse>) => {
|
||||
const { id, shell, injector, error } = event.data;
|
||||
const entry = pending.get(id);
|
||||
if (!entry) return;
|
||||
pending.delete(id);
|
||||
if (error || !shell || !injector) {
|
||||
entry.reject(new Error(error ?? "decompile failed"));
|
||||
} else {
|
||||
entry.resolve({ shell, injector });
|
||||
}
|
||||
};
|
||||
worker.onerror = (event) => {
|
||||
const reason = new Error(event.message || "decompile worker failed");
|
||||
for (const entry of pending.values()) entry.reject(reason);
|
||||
pending.clear();
|
||||
};
|
||||
}
|
||||
return worker;
|
||||
}
|
||||
|
||||
function decompileInWorker(request: Omit<DecompileRequest, "id">) {
|
||||
return new Promise<DecompiledSources>((resolve, reject) => {
|
||||
const id = nextId++;
|
||||
pending.set(id, { resolve, reject });
|
||||
getWorker().postMessage({ ...request, id } satisfies DecompileRequest);
|
||||
});
|
||||
}
|
||||
|
||||
export function useDecompiledSources(generateResult: MemShellResult | undefined) {
|
||||
const shellClassName = generateResult?.shellClassName;
|
||||
const shellBytesBase64 = generateResult?.shellBytesBase64Str;
|
||||
const injectorClassName = generateResult?.injectorClassName;
|
||||
const injectorBytesBase64 = generateResult?.injectorBytesBase64Str;
|
||||
|
||||
const [state, setState] = useState<DecompileState>(IDLE);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shellClassName || !shellBytesBase64 || !injectorClassName || !injectorBytesBase64) {
|
||||
setState(IDLE);
|
||||
return;
|
||||
}
|
||||
|
||||
const key = cacheKey(shellBytesBase64, injectorBytesBase64);
|
||||
const cached = cacheGet(key);
|
||||
if (cached) {
|
||||
setState({ sources: cached, isDecompiling: false, error: null });
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setState({ sources: { shell: null, injector: null }, isDecompiling: true, error: null });
|
||||
|
||||
decompileInWorker({
|
||||
shellClassName,
|
||||
shellBytesBase64,
|
||||
injectorClassName,
|
||||
injectorBytesBase64,
|
||||
})
|
||||
.then((sources) => {
|
||||
cacheSet(key, sources);
|
||||
if (!cancelled) setState({ sources, isDecompiling: false, error: null });
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (!cancelled) {
|
||||
setState({
|
||||
sources: { shell: null, injector: null },
|
||||
isDecompiling: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [injectorBytesBase64, injectorClassName, shellBytesBase64, shellClassName]);
|
||||
|
||||
return state;
|
||||
}
|
||||
Reference in New Issue
Block a user