From 37467e4a7b5e05e4da413a1e06e597fa806b72e4 Mon Sep 17 00:00:00 2001 From: jeff Date: Fri, 15 Aug 2014 01:43:48 -0400 Subject: trying to get inner/anonymous classes working... I have a working heuristic in place to detect anonymous classes, but I can't seem to get Procyon to decompile them correctly. I'm writing the InnerClasses attribute and translating all the inner class names, but there must be something else I'm missing... --- .hgignore | 4 +- src/cuchaz/enigma/Deobfuscator.java | 18 +-- src/cuchaz/enigma/TranslatingTypeLoader.java | 86 ++++++++--- src/cuchaz/enigma/analysis/JarClassIterator.java | 134 ++++++++++++++++ src/cuchaz/enigma/analysis/JarIndex.java | 187 ++++++++++++++--------- src/cuchaz/enigma/bytecode/InnerClassWriter.java | 79 ++++++++++ 6 files changed, 399 insertions(+), 109 deletions(-) create mode 100644 src/cuchaz/enigma/analysis/JarClassIterator.java create mode 100644 src/cuchaz/enigma/bytecode/InnerClassWriter.java diff --git a/.hgignore b/.hgignore index 9581253..ddf86cc 100644 --- a/.hgignore +++ b/.hgignore @@ -10,4 +10,6 @@ syntax: regexp syntax: regexp ^libs$ syntax: regexp -^build$ \ No newline at end of file +^build$ +syntax: regexp +^data$ \ No newline at end of file diff --git a/src/cuchaz/enigma/Deobfuscator.java b/src/cuchaz/enigma/Deobfuscator.java index 770172e..127a0d9 100644 --- a/src/cuchaz/enigma/Deobfuscator.java +++ b/src/cuchaz/enigma/Deobfuscator.java @@ -11,9 +11,7 @@ package cuchaz.enigma; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; import java.io.StringWriter; import java.util.List; import java.util.jar.JarFile; @@ -58,18 +56,9 @@ public class Deobfuscator m_file = file; m_jar = new JarFile( m_file ); - // build the ancestries - InputStream jarIn = null; - try - { - m_jarIndex = new JarIndex( m_jar ); - jarIn = new FileInputStream( m_file ); - m_jarIndex.indexJar( jarIn ); - } - finally - { - Util.closeQuietly( jarIn ); - } + // build the jar index + m_jarIndex = new JarIndex(); + m_jarIndex.indexJar( m_jar ); // config the decompiler m_settings = DecompilerSettings.javaDefaults(); @@ -105,6 +94,7 @@ public class Deobfuscator // update decompiler options m_settings.setTypeLoader( new TranslatingTypeLoader( m_jar, + m_jarIndex, getTranslator( TranslationDirection.Obfuscating ), getTranslator( TranslationDirection.Deobfuscating ) ) ); diff --git a/src/cuchaz/enigma/TranslatingTypeLoader.java b/src/cuchaz/enigma/TranslatingTypeLoader.java index f5112e0..fdfcea0 100644 --- a/src/cuchaz/enigma/TranslatingTypeLoader.java +++ b/src/cuchaz/enigma/TranslatingTypeLoader.java @@ -17,42 +17,77 @@ import java.util.jar.JarEntry; import java.util.jar.JarFile; import javassist.ByteArrayClassPath; +import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; +import javassist.NotFoundException; import javassist.bytecode.Descriptor; import com.strobel.assembler.metadata.Buffer; import com.strobel.assembler.metadata.ITypeLoader; +import cuchaz.enigma.analysis.JarIndex; import cuchaz.enigma.bytecode.ClassTranslator; +import cuchaz.enigma.bytecode.InnerClassWriter; import cuchaz.enigma.bytecode.MethodParameterWriter; import cuchaz.enigma.mapping.Translator; public class TranslatingTypeLoader implements ITypeLoader { private JarFile m_jar; + private JarIndex m_jarIndex; private Translator m_obfuscatingTranslator; private Translator m_deobfuscatingTranslator; - public TranslatingTypeLoader( JarFile jar, Translator obfuscatingTranslator, Translator deobfuscatingTranslator ) + public TranslatingTypeLoader( JarFile jar, JarIndex jarIndex, Translator obfuscatingTranslator, Translator deobfuscatingTranslator ) { m_jar = jar; + m_jarIndex = jarIndex; m_obfuscatingTranslator = obfuscatingTranslator; m_deobfuscatingTranslator = deobfuscatingTranslator; } @Override - public boolean tryLoadType( String name, Buffer out ) + public boolean tryLoadType( String deobfClassName, Buffer out ) { - // is this a deobufscated class name? - String obfName = m_obfuscatingTranslator.translateClass( name ); - if( obfName != null ) + // TEMP + if( !deobfClassName.startsWith( "java" ) && !deobfClassName.startsWith( "org" ) ) { - // point to the obfuscated class - name = obfName; + System.out.println( "Looking for: " + deobfClassName ); } - JarEntry entry = m_jar.getJarEntry( name + ".class" ); + // what class file should we actually load? + String obfClassName = m_obfuscatingTranslator.translateClass( deobfClassName ); + if( obfClassName == null ) + { + obfClassName = deobfClassName; + } + String classFileName = obfClassName; + + // is this a properly-referenced inner class? + boolean isInnerClass = deobfClassName.indexOf( '$' ) >= 0; + if( isInnerClass ) + { + // get just the bare inner class name + String[] parts = deobfClassName.split( "\\$" ); + String deobfClassFileName = parts[parts.length - 1]; + + // make sure the bare inner class name is obfuscated + classFileName = m_obfuscatingTranslator.translateClass( deobfClassFileName ); + if( classFileName == null ) + { + classFileName = deobfClassFileName; + } + } + + // TEMP + if( !deobfClassName.startsWith( "java" ) && !deobfClassName.startsWith( "org" ) ) + { + System.out.println( "\tLooking at class file: " + classFileName ); + } + + // get the jar entry + JarEntry entry = m_jar.getJarEntry( classFileName + ".class" ); if( entry == null ) { return false; @@ -73,32 +108,43 @@ public class TranslatingTypeLoader implements ITypeLoader } data.write( buf, 0, bytesRead ); } + data.close(); + in.close(); buf = data.toByteArray(); - // translate the class - String javaName = Descriptor.toJavaName( name ); + // load the javassist handle to the class + String javaClassFileName = Descriptor.toJavaName( classFileName ); ClassPool classPool = new ClassPool(); - classPool.insertClassPath( new ByteArrayClassPath( javaName, buf ) ); - try + classPool.insertClassPath( new ByteArrayClassPath( javaClassFileName, buf ) ); + CtClass c = classPool.get( javaClassFileName ); + + if( isInnerClass ) { - CtClass c = classPool.get( javaName ); - new MethodParameterWriter( m_deobfuscatingTranslator ).writeMethodArguments( c ); - new ClassTranslator( m_deobfuscatingTranslator ).translate( c ); - buf = c.toBytecode(); + // rename the class to what procyon expects + c.setName( deobfClassName ); } - catch( Exception ex ) + else { - throw new Error( ex ); + // maybe it's an outer class + new InnerClassWriter( m_deobfuscatingTranslator, m_jarIndex ).writeInnerClasses( c ); } - // pass it along to the decompiler + new MethodParameterWriter( m_deobfuscatingTranslator ).writeMethodArguments( c ); + new ClassTranslator( m_deobfuscatingTranslator ).translate( c ); + + assert( Descriptor.toJvmName( c.getName() ).equals( deobfClassName ) ); + assert( c.getClassFile().getName().equals( deobfClassName ) ); + + buf = c.toBytecode(); + + // pass the transformed class along to the decompiler out.reset( buf.length ); System.arraycopy( buf, 0, out.array(), out.position(), buf.length ); out.position( 0 ); return true; } - catch( IOException ex ) + catch( IOException | NotFoundException | CannotCompileException ex ) { throw new Error( ex ); } diff --git a/src/cuchaz/enigma/analysis/JarClassIterator.java b/src/cuchaz/enigma/analysis/JarClassIterator.java new file mode 100644 index 0000000..cf6df80 --- /dev/null +++ b/src/cuchaz/enigma/analysis/JarClassIterator.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * Copyright (c) 2014 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ +package cuchaz.enigma.analysis; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import javassist.ByteArrayClassPath; +import javassist.ClassPool; +import javassist.CtClass; +import javassist.NotFoundException; +import javassist.bytecode.Descriptor; + +import com.beust.jcommander.internal.Lists; + +import cuchaz.enigma.Constants; + +public class JarClassIterator implements Iterator +{ + private JarFile m_jar; + private Iterator m_iter; + + public JarClassIterator( JarFile jar ) + { + this( jar, getClassEntries( jar ) ); + } + + public JarClassIterator( JarFile jar, List entries ) + { + m_jar = jar; + m_iter = entries.iterator(); + } + + @Override + public boolean hasNext( ) + { + return m_iter.hasNext(); + } + + @Override + public CtClass next( ) + { + JarEntry entry = m_iter.next(); + try + { + // read the class into a buffer + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] buf = new byte[Constants.KiB]; + int totalNumBytesRead = 0; + InputStream in = m_jar.getInputStream( entry ); + while( in.available() > 0 ) + { + int numBytesRead = in.read( buf ); + if( numBytesRead < 0 ) + { + break; + } + bos.write( buf, 0, numBytesRead ); + + // sanity checking + totalNumBytesRead += numBytesRead; + if( totalNumBytesRead > Constants.MiB ) + { + throw new Error( "Class file " + entry.getName() + " larger than 1 MiB! Something is wrong!" ); + } + } + + // determine the class name (ie chop off the ".class") + String className = Descriptor.toJavaName( entry.getName().substring( 0, entry.getName().length() - ".class".length() ) ); + + // get a javassist handle for the class + ClassPool classPool = new ClassPool(); + classPool.insertClassPath( new ByteArrayClassPath( className, bos.toByteArray() ) ); + return classPool.get( className ); + } + catch( IOException ex ) + { + throw new Error( "Unable to read class: " + entry.getName() ); + } + catch( NotFoundException ex ) + { + throw new Error( "Unable to load class: " + entry.getName() ); + } + } + + @Override + public void remove( ) + { + throw new UnsupportedOperationException(); + } + + public static List getClassEntries( JarFile jar ) + { + List classes = Lists.newArrayList(); + Enumeration entries = jar.entries(); + while( entries.hasMoreElements() ) + { + JarEntry entry = entries.nextElement(); + + // is this a class file? + if( !entry.isDirectory() && entry.getName().endsWith( ".class" ) ) + { + classes.add( entry ); + } + } + return classes; + } + + public static Iterable classes( final JarFile jar ) + { + return new Iterable( ) + { + @Override + public Iterator iterator( ) + { + return new JarClassIterator( jar ); + } + }; + } +} diff --git a/src/cuchaz/enigma/analysis/JarIndex.java b/src/cuchaz/enigma/analysis/JarIndex.java index 845be60..9962bfa 100644 --- a/src/cuchaz/enigma/analysis/JarIndex.java +++ b/src/cuchaz/enigma/analysis/JarIndex.java @@ -10,27 +10,21 @@ ******************************************************************************/ package cuchaz.enigma.analysis; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; import java.util.Collection; -import java.util.Enumeration; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; -import javassist.ByteArrayClassPath; import javassist.CannotCompileException; -import javassist.ClassPool; import javassist.CtBehavior; import javassist.CtClass; import javassist.CtConstructor; import javassist.CtMethod; -import javassist.NotFoundException; +import javassist.bytecode.AccessFlag; import javassist.bytecode.Descriptor; +import javassist.bytecode.FieldInfo; import javassist.expr.ConstructorCall; import javassist.expr.ExprEditor; import javassist.expr.FieldAccess; @@ -39,10 +33,10 @@ import javassist.expr.NewExpr; import com.google.common.collect.HashMultimap; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; -import cuchaz.enigma.Constants; import cuchaz.enigma.mapping.ClassEntry; import cuchaz.enigma.mapping.ConstructorEntry; import cuchaz.enigma.mapping.Entry; @@ -57,89 +51,52 @@ public class JarIndex private Multimap m_methodImplementations; private Multimap m_methodCalls; private Multimap m_fieldCalls; + private Multimap m_innerClasses; + private Map m_outerClasses; - public JarIndex( JarFile jar ) + public JarIndex( ) { m_obfClassNames = Sets.newHashSet(); m_ancestries = new Ancestries(); m_methodImplementations = HashMultimap.create(); m_methodCalls = HashMultimap.create(); m_fieldCalls = HashMultimap.create(); - - // read the class names - Enumeration enumeration = jar.entries(); - while( enumeration.hasMoreElements() ) + m_innerClasses = HashMultimap.create(); + m_outerClasses = Maps.newHashMap(); + } + + public void indexJar( JarFile jar ) + { + // pass 1: read the class names + for( JarEntry entry : JarClassIterator.getClassEntries( jar ) ) { - JarEntry entry = enumeration.nextElement(); - - // filter out non-classes - if( entry.isDirectory() || !entry.getName().endsWith( ".class" ) ) - { - continue; - } - String className = entry.getName().substring( 0, entry.getName().length() - 6 ); m_obfClassNames.add( Descriptor.toJvmName( className ) ); } - } - - public void indexJar( InputStream in ) - throws IOException - { - ClassPool classPool = new ClassPool(); - ZipInputStream zin = new ZipInputStream( in ); - ZipEntry entry; - while( ( entry = zin.getNextEntry() ) != null ) + // pass 2: index the types, methods + for( CtClass c : JarClassIterator.classes( jar ) ) { - // filter out non-classes - if( entry.isDirectory() || !entry.getName().endsWith( ".class" ) ) + m_ancestries.addSuperclass( c.getName(), c.getClassFile().getSuperclass() ); + for( CtBehavior behavior : c.getDeclaredBehaviors() ) { - continue; + indexBehavior( behavior ); } - - // read the class into a buffer - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - byte[] buf = new byte[Constants.KiB]; - int totalNumBytesRead = 0; - while( zin.available() > 0 ) - { - int numBytesRead = zin.read( buf ); - if( numBytesRead < 0 ) - { - break; - } - bos.write( buf, 0, numBytesRead ); - - // sanity checking - totalNumBytesRead += numBytesRead; - if( totalNumBytesRead > Constants.MiB ) - { - throw new Error( "Class file " + entry.getName() + " larger than 1 MiB! Something is wrong!" ); - } - } - - // determine the class name (ie chop off the ".class") - String className = Descriptor.toJavaName( entry.getName().substring( 0, entry.getName().length() - ".class".length() ) ); - - // get a javassist handle for the class - classPool.insertClassPath( new ByteArrayClassPath( className, bos.toByteArray() ) ); - try - { - CtClass c = classPool.get( className ); - m_ancestries.addSuperclass( c.getName(), c.getClassFile().getSuperclass() ); - for( CtBehavior behavior : c.getDeclaredBehaviors() ) - { - indexBehavior( behavior ); - } - } - catch( NotFoundException ex ) + } + + // pass 2: index inner classes + for( CtClass c : JarClassIterator.classes( jar ) ) + { + String outerClassName = isInnerClass( c ); + if( outerClassName != null ) { - throw new Error( "Unable to load class: " + className ); + String innerClassName = Descriptor.toJvmName( c.getName() ); + m_innerClasses.put( outerClassName, innerClassName ); + m_outerClasses.put( innerClassName, outerClassName ); } } } - + private void indexBehavior( CtBehavior behavior ) { // get the method entry @@ -226,6 +183,78 @@ public class JarIndex } } + @SuppressWarnings( "unchecked" ) + private String isInnerClass( CtClass c ) + { + String innerClassName = Descriptor.toJvmName( c.getName() ); + + // first, is this an anonymous class? + // for anonymous classes: + // the outer class is always a synthetic field + // there's at least one constructor with the type of the synthetic field as an argument + // this constructor is called exactly once by the class of the synthetic field + + for( FieldInfo field : (List)c.getClassFile().getFields() ) + { + boolean isSynthetic = (field.getAccessFlags() & AccessFlag.SYNTHETIC) != 0; + if( !isSynthetic ) + { + continue; + } + + // skip non-class types + if( !field.getDescriptor().startsWith( "L" ) ) + { + continue; + } + + // get the outer class from the field type + String outerClassName = Descriptor.toJvmName( Descriptor.toClassName( field.getDescriptor() ) ); + + // look for a constructor where this type is the first parameter + CtConstructor targetConstructor = null; + for( CtConstructor constructor : c.getDeclaredConstructors() ) + { + String signature = Descriptor.getParamDescriptor( constructor.getMethodInfo().getDescriptor() ); + if( Descriptor.numOfParameters( signature ) < 1 ) + { + continue; + } + + // match the first parameter to the outer class + Descriptor.Iterator iter = new Descriptor.Iterator( signature ); + int pos = iter.next(); + if( iter.isParameter() && signature.charAt( pos ) == 'L' ) + { + String argumentDesc = signature.substring( pos, signature.indexOf(';', pos) + 1 ); + String argumentClassName = Descriptor.toJvmName( Descriptor.toClassName( argumentDesc ) ); + if( argumentClassName.equals( outerClassName ) ) + { + // is this constructor called exactly once? + ConstructorEntry constructorEntry = new ConstructorEntry( + new ClassEntry( innerClassName ), + constructor.getMethodInfo().getDescriptor() + ); + if( this.getMethodCallers( constructorEntry ).size() == 1 ) + { + targetConstructor = constructor; + break; + } + } + } + } + if( targetConstructor == null ) + { + continue; + } + + // yeah, this is an inner class + return outerClassName; + } + + return null; + } + public Set getObfClassNames( ) { return m_obfClassNames; @@ -304,4 +333,14 @@ public class JarIndex { return m_methodCalls.get( entry ); } + + public Collection getInnerClasses( String obfOuterClassName ) + { + return m_innerClasses.get( obfOuterClassName ); + } + + public String getOuterClass( String obfInnerClassName ) + { + return m_outerClasses.get( obfInnerClassName ); + } } diff --git a/src/cuchaz/enigma/bytecode/InnerClassWriter.java b/src/cuchaz/enigma/bytecode/InnerClassWriter.java new file mode 100644 index 0000000..d4abe4e --- /dev/null +++ b/src/cuchaz/enigma/bytecode/InnerClassWriter.java @@ -0,0 +1,79 @@ +/******************************************************************************* + * Copyright (c) 2014 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ +package cuchaz.enigma.bytecode; + +import java.util.Collection; + +import javassist.CtClass; +import javassist.bytecode.AccessFlag; +import javassist.bytecode.Descriptor; +import javassist.bytecode.InnerClassesAttribute; +import cuchaz.enigma.analysis.JarIndex; +import cuchaz.enigma.mapping.Translator; + +public class InnerClassWriter +{ + private Translator m_deobfuscatingTranslator; + private JarIndex m_jarIndex; + + public InnerClassWriter( Translator deobfuscatingTranslator, JarIndex jarIndex ) + { + m_deobfuscatingTranslator = deobfuscatingTranslator; + m_jarIndex = jarIndex; + } + + public void writeInnerClasses( CtClass c ) + { + // is this an outer class with inner classes? + String obfOuterClassName = Descriptor.toJvmName( c.getName() ); + Collection obfInnerClassNames = m_jarIndex.getInnerClasses( obfOuterClassName ); + if( obfInnerClassNames != null && !obfInnerClassNames.isEmpty() ) + { + writeInnerClasses( c, obfInnerClassNames ); + } + } + + private void writeInnerClasses( CtClass c, Collection obfInnerClassNames ) + { + String obfOuterClassName = Descriptor.toJvmName( c.getName() ); + InnerClassesAttribute attr = new InnerClassesAttribute( c.getClassFile().getConstPool() ); + c.getClassFile().addAttribute( attr ); + for( String obfInnerClassName : obfInnerClassNames ) + { + // deobfuscate the class names + String deobfOuterClassName = m_deobfuscatingTranslator.translateClass( obfOuterClassName ); + if( deobfOuterClassName == null ) + { + deobfOuterClassName = obfOuterClassName; + } + String deobfInnerClassName = m_deobfuscatingTranslator.translateClass( obfInnerClassName ); + if( deobfInnerClassName == null ) + { + deobfInnerClassName = obfInnerClassName; + } + + // update the attribute + String deobfOuterInnerClassName = deobfOuterClassName + "$" + deobfInnerClassName; + attr.append( + deobfOuterInnerClassName, + deobfOuterClassName, + deobfInnerClassName, + c.getClassFile().getAccessFlags() & ~AccessFlag.SUPER + ); + + // make sure the outer class references only the new inner class names + c.replaceClassName( obfInnerClassName, deobfOuterInnerClassName ); + + // TEMP + System.out.println( "\tInner " + obfInnerClassName + " -> " + deobfOuterInnerClassName ); + } + } +} -- cgit v1.2.3