Last active
October 9, 2017 04:41
-
-
Save rcx/f72db6a2e2fd8fbd32215975fc401543 to your computer and use it in GitHub Desktop.
Spigot plugin cracker
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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