Skip to content

Instantly share code, notes, and snippets.

@rcx
Last active October 9, 2017 04:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rcx/f72db6a2e2fd8fbd32215975fc401543 to your computer and use it in GitHub Desktop.
Save rcx/f72db6a2e2fd8fbd32215975fc401543 to your computer and use it in GitHub Desktop.
Spigot plugin cracker
package org.spigotmc.plugincracker;
import org.objectweb.asm.*;
import org.objectweb.asm.commons.JSRInlinerAdapter;
import org.objectweb.asm.tree.*;
import org.objectweb.asm.util.CheckClassAdapter;
import java.io.*;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Random;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
@SuppressWarnings("Duplicates")
public class PluginCracker
{
private static File input, output;
private static JarFile jar;
private static HashMap<String, ClassNode> classes;
private static HashMap<String, JarEntry> resources;
private static void addToClassPath(File file) throws MalformedURLException
{
Method method;
try
{
method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
method.setAccessible(true);
method.invoke(ClassLoader.getSystemClassLoader(), file.toURI().toURL());
}
catch (ReflectiveOperationException e) // this will never happen
{
e.printStackTrace();
}
}
private static void setupClassPath(String[] args)
{
try
{
input = new File(args[0]);
addToClassPath(input);
}
catch (IOException e)
{
throw new RuntimeException("Couldn't load input jar " + args[0], e);
}
if (args[1].equals("-cp"))
{
for (int i = 2; i < args.length - 1; i++)
{
String path = args[i];
try
{
addToClassPath(new File(path));
}
catch (IOException e)
{
throw new RuntimeException("Couldn't load dependency " + path, e);
}
}
}
output = new File(args[args.length - 1]);
}
private static void loadInput()
{
classes = new HashMap<>();
resources = new HashMap<>();
try
{
jar = new JarFile(input);
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements())
{
JarEntry entry = entries.nextElement();
if (entry.isDirectory() || !entry.getName().endsWith(".class"))
{
resources.put(entry.getName(), entry);
}
else
{
ClassNode clazz = new ClassNode();
new ClassReader(jar.getInputStream(entry)).accept(clazz, 0);
classes.put(clazz.name, clazz);
}
}
}
catch(IOException e)
{
throw new RuntimeException("Couldn't load input jar " + input.getPath(), e);
}
}
private static void crack()
{
// Parse plugin.yml to locate main class
if (!resources.containsKey("plugin.yml"))
throw new IllegalArgumentException("plugin.yml not found");
String mainClass = null;
try
{
BufferedReader reader = new BufferedReader(new InputStreamReader(jar.getInputStream(resources.get("plugin.yml"))));
String line;
while ((line = reader.readLine()) != null)
{
line = line.replaceAll("\\s+","");
if (line.startsWith("main:"))
{
mainClass = line.split(":")[1].replace('.', '/');
break;
}
}
}
catch (IOException e)
{
e.printStackTrace();
}
if (mainClass == null)
throw new IllegalArgumentException("Failed to parse plugin.yml; main class property not found");
if (!classes.containsKey(mainClass))
throw new IllegalArgumentException("Main class " + mainClass + " not found in jar");
ClassNode main = classes.get(mainClass);
System.out.println("Main class: " + mainClass);
// Locate onEnable and drm stub methods
MethodNode onEnable = null, drmStub = null;
for (MethodNode mn : main.methods)
{
if (!mn.desc.equals("()V"))
continue;
if (mn.name.equals("onEnable"))
onEnable = mn;
else if (mn.name.equals("loadConfig0") && (mn.access & Opcodes.ACC_BRIDGE) == Opcodes.ACC_BRIDGE)
drmStub = mn;
}
if (onEnable == null)
throw new IllegalArgumentException("onEnable method not found");
if (drmStub == null)
throw new IllegalArgumentException("loadConfig0 (DRM stub) method not found");
// Clear userid and nonce from request URL
// Not neccessary; ASM removes the string from the constant pool
/*
for (AbstractInsnNode insn : drmStub.instructions.toArray())
{
if (insn.getOpcode() == Opcodes.LDC)
{
LdcInsnNode ldc = (LdcInsnNode) insn;
if (ldc.cst instanceof String)
{
String str = (String) ldc.cst;
if (str.startsWith("http://www.spigotmc.org/api/resource.php?"))
{
System.out.println("Request URL: " + str);
Random rng = new Random((System.currentTimeMillis() ^ resources.get("plugin.yml").getCrc()) * str.length());
str = str.replaceFirst("user_id=\\d+", "user_id=" + rng.nextInt(50000));
ldc.cst = (str = str.replaceFirst("nonce=\\d+", "nonce=" + Math.abs(rng.nextInt())));
System.out.println("Patched URL: " + str);
}
}
}
}
*/
// Neuter DRM stub
drmStub.instructions.clear();
drmStub.instructions.add(new InsnNode(Opcodes.RETURN));
drmStub.localVariables.clear();
drmStub.instructions.resetLabels();
drmStub.tryCatchBlocks.clear();
// Remove call to drm stub
MethodInsnNode call = null;
for (AbstractInsnNode insn : onEnable.instructions.toArray())
{
if (insn instanceof MethodInsnNode)
{
MethodInsnNode min = (MethodInsnNode) insn;
if (min.name.equals("loadConfig0") && min.desc.equals("()V"))
{
call = min;
break;
}
}
}
if (call == null)
throw new IllegalArgumentException("Call to loadConfig0 not found");
onEnable.instructions.remove(call);
System.out.println("Call to loadConfig0 (DRM stub) removed");
}
private static void export()
{
try
{
JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(output));
for (ClassNode clazz : classes.values())
{
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
try
{
clazz.accept(new ClassVisitor(Opcodes.ASM5, new CheckClassAdapter(writer))
{
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces)
{
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
mv = new JSRInlinerAdapter(mv, access, name, desc, signature, exceptions);
return mv;
}
});
}
catch (Exception e)
{
System.out.println("ASM threw exception while writing " + clazz.name);
throw e;
}
JarEntry entry = new JarEntry(clazz.name + ".class");
outputStream.putNextEntry(entry);
outputStream.write(writer.toByteArray());
outputStream.closeEntry();
}
for (JarEntry entry : resources.values())
{
outputStream.putNextEntry(entry);
byte[] buf = new byte[1024];
InputStream inputStream = jar.getInputStream(entry);
int read;
while ((read = inputStream.read(buf)) > -1)
outputStream.write(buf, 0, read);
outputStream.closeEntry();
}
outputStream.close();
jar.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
public static void main(String[] args)
{
if (args.length < 2)
{
System.out.println("Usage: PluginCracker <input.jar> [-cp classpath] <output.jar>");
System.out.println("Classpath is semicolon-delimited. Use *.jar wildcard for directories.");
return;
}
setupClassPath(args);
loadInput();
crack();
export();
System.out.println("Saved cracked plugin to " + output);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment