|
package amped; |
|
|
|
import backstage.runtime.ApplicationClassLoader; |
|
|
|
import java.io.BufferedOutputStream; |
|
import java.io.File; |
|
import java.io.FileOutputStream; |
|
import java.io.IOException; |
|
import java.lang.instrument.ClassFileTransformer; |
|
import java.lang.instrument.Instrumentation; |
|
import java.security.ProtectionDomain; |
|
import java.util.ArrayList; |
|
import java.util.HashSet; |
|
import java.util.List; |
|
import java.util.Map; |
|
import java.util.Set; |
|
import java.util.concurrent.ConcurrentHashMap; |
|
import java.util.Enumeration; |
|
import java.util.jar.JarEntry; |
|
import java.util.jar.JarFile; |
|
import java.util.jar.JarOutputStream; |
|
|
|
public final class Decrypter { |
|
private static volatile Instrumentation instrumentation; |
|
|
|
public static void premain(String agentArgs, Instrumentation inst) { |
|
instrumentation = inst; |
|
} |
|
|
|
public static void main(String[] args) throws Exception { |
|
|
|
// parse args |
|
if (instrumentation == null) { |
|
System.err.println("Missing -javaagent. Run with: -javaagent:<path-to-built-jar>"); |
|
System.exit(2); |
|
} |
|
|
|
if (args.length < 2) { |
|
System.err.println("Usage: Decrypter <input-amped-jar> <output-jar>"); |
|
System.exit(1); |
|
} |
|
|
|
File inputJar = new File(args[0]); |
|
File outputJar = new File(args[1]); |
|
|
|
if (!inputJar.isFile()) { |
|
System.err.println("Input jar not found: " + inputJar.getAbsolutePath()); |
|
System.exit(1); |
|
} |
|
|
|
final Set<String> targetClassNamesSlash = new HashSet<String>(); |
|
List<String> classNamesDot = new ArrayList<String>(); |
|
|
|
// Enumerate .clazz entries in the amped.jar so we know which classes to force-load. |
|
JarFile jar = null; |
|
try { |
|
jar = new JarFile(inputJar); |
|
Enumeration<JarEntry> entries = jar.entries(); |
|
|
|
while (entries.hasMoreElements()) { |
|
JarEntry entry = entries.nextElement(); |
|
if (entry.isDirectory()) { |
|
continue; |
|
} |
|
String name = entry.getName(); |
|
if (!name.endsWith(".clazz")) { |
|
continue; |
|
} |
|
|
|
// name ends with '.clazz' |
|
String slash = name.substring(0, name.length() - ".clazz".length()); |
|
targetClassNamesSlash.add(slash); |
|
classNamesDot.add(slash.replace('/', '.')); |
|
} |
|
} finally { |
|
if (jar != null) { |
|
jar.close(); |
|
} |
|
} |
|
|
|
// Capture decoded bytecode as the native loader defines classes. |
|
// class name -> decoded bytecode |
|
final Map<String, byte[]> decodedBytesBySlashName = new ConcurrentHashMap<String, byte[]>(); |
|
|
|
ClassFileTransformer transformer = new ClassFileTransformer() { |
|
public byte[] transform(ClassLoader loader, |
|
String className, |
|
Class<?> classBeingRedefined, |
|
ProtectionDomain protectionDomain, |
|
byte[] classfileBuffer) { |
|
if (className == null || classfileBuffer == null) { |
|
return null; |
|
} |
|
if (targetClassNamesSlash.contains(className)) { |
|
if (!decodedBytesBySlashName.containsKey(className)) { |
|
decodedBytesBySlashName.put(className, classfileBuffer.clone()); |
|
} |
|
} |
|
return null; |
|
} |
|
}; |
|
|
|
instrumentation.addTransformer(transformer, true); |
|
|
|
// Preparation done. now the decryption part. |
|
// Use the nkeel ApplicationClassLoader so the native defineClass path is exercised. |
|
ApplicationClassLoader loader = new ApplicationClassLoader("com.cyberstep.ring.client.Game"); |
|
loader.openArchive(inputJar.getAbsolutePath()); |
|
|
|
List<String> failures = new ArrayList<String>(); |
|
for (String className : classNamesDot) { |
|
try { |
|
// Force-load each class to trigger decoding and transformer capture. |
|
Class.forName(className, false, loader); |
|
} catch (Throwable t) { |
|
failures.add(className + " -> " + t.getClass().getName() + ": " + t.getMessage()); |
|
} |
|
} |
|
|
|
// Class load done. |
|
instrumentation.removeTransformer(transformer); |
|
|
|
// Write captured decoded classes to a standard .class jar. |
|
writeJar(outputJar, decodedBytesBySlashName); |
|
|
|
int total = classNamesDot.size(); |
|
int decoded = decodedBytesBySlashName.size(); |
|
System.out.println("Decoded classes: " + decoded + " / " + total); |
|
if (!failures.isEmpty()) { |
|
System.err.println("Load failures: " + failures.size()); |
|
for (String f : failures) { |
|
System.err.println(f); |
|
} |
|
} |
|
|
|
if (decoded < total) { |
|
System.err.println("Some classes were not captured. If needed, retry with a more complete classpath."); |
|
} |
|
} |
|
|
|
private static void writeJar(File outputJar, Map<String, byte[]> classMap) throws IOException { |
|
if (outputJar.getParentFile() != null) { |
|
outputJar.getParentFile().mkdirs(); |
|
} |
|
|
|
JarOutputStream jos = null; |
|
try { |
|
jos = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(outputJar))); |
|
for (Map.Entry<String, byte[]> entry : classMap.entrySet()) { |
|
String name = entry.getKey() + ".class"; |
|
JarEntry jarEntry = new JarEntry(name); |
|
jos.putNextEntry(jarEntry); |
|
jos.write(entry.getValue()); |
|
jos.closeEntry(); |
|
} |
|
} finally { |
|
if (jos != null) { |
|
jos.close(); |
|
} |
|
} |
|
} |
|
} |