diff options
Diffstat (limited to 'src/proguard/retrace/ReTrace.java')
-rw-r--r-- | src/proguard/retrace/ReTrace.java | 749 |
1 files changed, 749 insertions, 0 deletions
diff --git a/src/proguard/retrace/ReTrace.java b/src/proguard/retrace/ReTrace.java new file mode 100644 index 0000000..97ab27b --- /dev/null +++ b/src/proguard/retrace/ReTrace.java @@ -0,0 +1,749 @@ +/* + * ProGuard -- shrinking, optimization, obfuscation, and preverification + * of Java bytecode. + * + * Copyright (c) 2002-2009 Eric Lafortune (eric@graphics.cornell.edu) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package proguard.retrace; + +import proguard.classfile.util.ClassUtil; +import proguard.obfuscate.*; + +import java.io.*; +import java.util.*; +import java.util.regex.*; + + +/** + * Tool for de-obfuscating stack traces of applications that were obfuscated + * with ProGuard. + * + * @author Eric Lafortune + */ +public class ReTrace +implements MappingProcessor +{ + private static final String REGEX_OPTION = "-regex"; + private static final String VERBOSE_OPTION = "-verbose"; + + + public static final String STACK_TRACE_EXPRESSION = "(?:\\s*%c:.*)|(?:\\s*at\\s+%c.%m\\s*\\(.*?(?::%l)?\\)\\s*)"; + + private static final String REGEX_CLASS = "\\b(?:[A-Za-z0-9_$]+\\.)*[A-Za-z0-9_$]+\\b"; + private static final String REGEX_CLASS_SLASH = "\\b(?:[A-Za-z0-9_$]+/)*[A-Za-z0-9_$]+\\b"; + private static final String REGEX_LINE_NUMBER = "\\b[0-9]+\\b"; + private static final String REGEX_TYPE = REGEX_CLASS + "(?:\\[\\])*"; + private static final String REGEX_MEMBER = "\\b[A-Za-z0-9_$]+\\b"; + private static final String REGEX_ARGUMENTS = "(?:" + REGEX_TYPE + "(?:\\s*,\\s*" + REGEX_TYPE + ")*)?"; + + // The class settings. + private final String regularExpression; + private final boolean verbose; + private final File mappingFile; + private final File stackTraceFile; + + private Map classMap = new HashMap(); + private Map classFieldMap = new HashMap(); + private Map classMethodMap = new HashMap(); + + + /** + * Creates a new ReTrace object to process stack traces on the standard + * input, based on the given mapping file name. + * @param regularExpression the regular expression for parsing the lines in + * the stack trace. + * @param verbose specifies whether the de-obfuscated stack trace + * should be verbose. + * @param mappingFile the mapping file that was written out by + * ProGuard. + */ + public ReTrace(String regularExpression, + boolean verbose, + File mappingFile) + { + this(regularExpression, verbose, mappingFile, null); + } + + + /** + * Creates a new ReTrace object to process a stack trace from the given file, + * based on the given mapping file name. + * @param regularExpression the regular expression for parsing the lines in + * the stack trace. + * @param verbose specifies whether the de-obfuscated stack trace + * should be verbose. + * @param mappingFile the mapping file that was written out by + * ProGuard. + * @param stackTraceFile the optional name of the file that contains the + * stack trace. + */ + public ReTrace(String regularExpression, + boolean verbose, + File mappingFile, + File stackTraceFile) + { + this.regularExpression = regularExpression; + this.verbose = verbose; + this.mappingFile = mappingFile; + this.stackTraceFile = stackTraceFile; + } + + + /** + * Performs the subsequent ReTrace operations. + */ + public void execute() throws IOException + { + // Read the mapping file. + MappingReader mappingReader = new MappingReader(mappingFile); + mappingReader.pump(this); + + + StringBuffer expressionBuffer = new StringBuffer(regularExpression.length() + 32); + char[] expressionTypes = new char[32]; + int expressionTypeCount = 0; + int index = 0; + while (true) + { + int nextIndex = regularExpression.indexOf('%', index); + if (nextIndex < 0 || + nextIndex == regularExpression.length()-1 || + expressionTypeCount == expressionTypes.length) + { + break; + } + + expressionBuffer.append(regularExpression.substring(index, nextIndex)); + expressionBuffer.append('('); + + char expressionType = regularExpression.charAt(nextIndex + 1); + switch(expressionType) + { + case 'c': + expressionBuffer.append(REGEX_CLASS); + break; + + case 'C': + expressionBuffer.append(REGEX_CLASS_SLASH); + break; + + case 'l': + expressionBuffer.append(REGEX_LINE_NUMBER); + break; + + case 't': + expressionBuffer.append(REGEX_TYPE); + break; + + case 'f': + expressionBuffer.append(REGEX_MEMBER); + break; + + case 'm': + expressionBuffer.append(REGEX_MEMBER); + break; + + case 'a': + expressionBuffer.append(REGEX_ARGUMENTS); + break; + } + + expressionBuffer.append(')'); + + expressionTypes[expressionTypeCount++] = expressionType; + + index = nextIndex + 2; + } + + expressionBuffer.append(regularExpression.substring(index)); + + Pattern pattern = Pattern.compile(expressionBuffer.toString()); + + // Read the stack trace file. + LineNumberReader reader = + new LineNumberReader(stackTraceFile == null ? + (Reader)new InputStreamReader(System.in) : + (Reader)new BufferedReader(new FileReader(stackTraceFile))); + + + try + { + StringBuffer outLine = new StringBuffer(256); + List extraOutLines = new ArrayList(); + + String className = null; + + // Read the line in the stack trace. + while (true) + { + String line = reader.readLine(); + if (line == null) + { + break; + } + + Matcher matcher = pattern.matcher(line); + + if (matcher.matches()) + { + int lineNumber = 0; + String type = null; + String arguments = null; + + // Figure out a class name, line number, type, and + // arguments beforehand. + for (int expressionTypeIndex = 0; expressionTypeIndex < expressionTypeCount; expressionTypeIndex++) + { + int startIndex = matcher.start(expressionTypeIndex + 1); + if (startIndex >= 0) + { + String match = matcher.group(expressionTypeIndex + 1); + + char expressionType = expressionTypes[expressionTypeIndex]; + switch (expressionType) + { + case 'c': + className = originalClassName(match); + break; + + case 'C': + className = originalClassName(ClassUtil.externalClassName(match)); + break; + + case 'l': + lineNumber = Integer.parseInt(match); + break; + + case 't': + type = originalType(match); + break; + + case 'a': + arguments = originalArguments(match); + break; + } + } + } + + // Actually construct the output line. + int lineIndex = 0; + + outLine.setLength(0); + extraOutLines.clear(); + + for (int expressionTypeIndex = 0; expressionTypeIndex < expressionTypeCount; expressionTypeIndex++) + { + int startIndex = matcher.start(expressionTypeIndex + 1); + if (startIndex >= 0) + { + int endIndex = matcher.end(expressionTypeIndex + 1); + String match = matcher.group(expressionTypeIndex + 1); + + // Copy a literal piece of input line. + outLine.append(line.substring(lineIndex, startIndex)); + + char expressionType = expressionTypes[expressionTypeIndex]; + switch (expressionType) + { + case 'c': + className = originalClassName(match); + outLine.append(className); + break; + + case 'C': + className = originalClassName(ClassUtil.externalClassName(match)); + outLine.append(ClassUtil.internalClassName(className)); + break; + + case 'l': + lineNumber = Integer.parseInt(match); + outLine.append(match); + break; + + case 't': + type = originalType(match); + outLine.append(type); + break; + + case 'f': + originalFieldName(className, + match, + type, + outLine, + extraOutLines); + break; + + case 'm': + originalMethodName(className, + match, + lineNumber, + type, + arguments, + outLine, + extraOutLines); + break; + + case 'a': + arguments = originalArguments(match); + outLine.append(arguments); + break; + } + + // Skip the original element whose processed version + // has just been appended. + lineIndex = endIndex; + } + } + + // Copy the last literal piece of input line. + outLine.append(line.substring(lineIndex)); + + // Print out the main line. + System.out.println(outLine); + + // Print out any additional lines. + for (int extraLineIndex = 0; extraLineIndex < extraOutLines.size(); extraLineIndex++) + { + System.out.println(extraOutLines.get(extraLineIndex)); + } + } + else + { + // Print out the original line. + System.out.println(line); + } + } + } + catch (IOException ex) + { + throw new IOException("Can't read stack trace (" + ex.getMessage() + ")"); + } + finally + { + if (stackTraceFile != null) + { + try + { + reader.close(); + } + catch (IOException ex) + { + // This shouldn't happen. + } + } + } + } + + + /** + * Finds the original field name(s), appending the first one to the out + * line, and any additional alternatives to the extra lines. + */ + private void originalFieldName(String className, + String obfuscatedFieldName, + String type, + StringBuffer outLine, + List extraOutLines) + { + int extraIndent = -1; + + // Class name -> obfuscated field names. + Map fieldMap = (Map)classFieldMap.get(className); + if (fieldMap != null) + { + // Obfuscated field names -> fields. + Set fieldSet = (Set)fieldMap.get(obfuscatedFieldName); + if (fieldSet != null) + { + // Find all matching fields. + Iterator fieldInfoIterator = fieldSet.iterator(); + while (fieldInfoIterator.hasNext()) + { + FieldInfo fieldInfo = (FieldInfo)fieldInfoIterator.next(); + if (fieldInfo.matches(type)) + { + // Is this the first matching field? + if (extraIndent < 0) + { + extraIndent = outLine.length(); + + // Append the first original name. + if (verbose) + { + outLine.append(fieldInfo.type).append(' '); + } + outLine.append(fieldInfo.originalName); + } + else + { + // Create an additional line with the proper + // indentation. + StringBuffer extraBuffer = new StringBuffer(); + for (int counter = 0; counter < extraIndent; counter++) + { + extraBuffer.append(' '); + } + + // Append the alternative name. + if (verbose) + { + extraBuffer.append(fieldInfo.type).append(' '); + } + extraBuffer.append(fieldInfo.originalName); + + // Store the additional line. + extraOutLines.add(extraBuffer); + } + } + } + } + } + + // Just append the obfuscated name if we haven't found any matching + // fields. + if (extraIndent < 0) + { + outLine.append(obfuscatedFieldName); + } + } + + + /** + * Finds the original method name(s), appending the first one to the out + * line, and any additional alternatives to the extra lines. + */ + private void originalMethodName(String className, + String obfuscatedMethodName, + int lineNumber, + String type, + String arguments, + StringBuffer outLine, + List extraOutLines) + { + int extraIndent = -1; + + // Class name -> obfuscated method names. + Map methodMap = (Map)classMethodMap.get(className); + if (methodMap != null) + { + // Obfuscated method names -> methods. + Set methodSet = (Set)methodMap.get(obfuscatedMethodName); + if (methodSet != null) + { + // Find all matching methods. + Iterator methodInfoIterator = methodSet.iterator(); + while (methodInfoIterator.hasNext()) + { + MethodInfo methodInfo = (MethodInfo)methodInfoIterator.next(); + if (methodInfo.matches(lineNumber, type, arguments)) + { + // Is this the first matching method? + if (extraIndent < 0) + { + extraIndent = outLine.length(); + + // Append the first original name. + if (verbose) + { + outLine.append(methodInfo.type).append(' '); + } + outLine.append(methodInfo.originalName); + if (verbose) + { + outLine.append('(').append(methodInfo.arguments).append(')'); + } + } + else + { + // Create an additional line with the proper + // indentation. + StringBuffer extraBuffer = new StringBuffer(); + for (int counter = 0; counter < extraIndent; counter++) + { + extraBuffer.append(' '); + } + + // Append the alternative name. + if (verbose) + { + extraBuffer.append(methodInfo.type).append(' '); + } + extraBuffer.append(methodInfo.originalName); + if (verbose) + { + extraBuffer.append('(').append(methodInfo.arguments).append(')'); + } + + // Store the additional line. + extraOutLines.add(extraBuffer); + } + } + } + } + } + + // Just append the obfuscated name if we haven't found any matching + // methods. + if (extraIndent < 0) + { + outLine.append(obfuscatedMethodName); + } + } + + + /** + * Returns the original argument types. + */ + private String originalArguments(String obfuscatedArguments) + { + StringBuffer originalArguments = new StringBuffer(); + + int startIndex = 0; + while (true) + { + int endIndex = obfuscatedArguments.indexOf(',', startIndex); + if (endIndex < 0) + { + break; + } + + originalArguments.append(originalType(obfuscatedArguments.substring(startIndex, endIndex).trim())).append(','); + + startIndex = endIndex + 1; + } + + originalArguments.append(originalType(obfuscatedArguments.substring(startIndex).trim())); + + return originalArguments.toString(); + } + + + /** + * Returns the original type. + */ + private String originalType(String obfuscatedType) + { + int index = obfuscatedType.indexOf('['); + + return index >= 0 ? + originalClassName(obfuscatedType.substring(0, index)) + obfuscatedType.substring(index) : + originalClassName(obfuscatedType); + } + + + /** + * Returns the original class name. + */ + private String originalClassName(String obfuscatedClassName) + { + String originalClassName = (String)classMap.get(obfuscatedClassName); + + return originalClassName != null ? + originalClassName : + obfuscatedClassName; + } + + + // Implementations for MappingProcessor. + + public boolean processClassMapping(String className, String newClassName) + { + // Obfuscated class name -> original class name. + classMap.put(newClassName, className); + + return true; + } + + + public void processFieldMapping(String className, String fieldType, String fieldName, String newFieldName) + { + // Original class name -> obfuscated field names. + Map fieldMap = (Map)classFieldMap.get(className); + if (fieldMap == null) + { + fieldMap = new HashMap(); + classFieldMap.put(className, fieldMap); + } + + // Obfuscated field name -> fields. + Set fieldSet = (Set)fieldMap.get(newFieldName); + if (fieldSet == null) + { + fieldSet = new LinkedHashSet(); + fieldMap.put(newFieldName, fieldSet); + } + + // Add the field information. + fieldSet.add(new FieldInfo(fieldType, + fieldName)); + } + + + public void processMethodMapping(String className, int firstLineNumber, int lastLineNumber, String methodReturnType, String methodName, String methodArguments, String newMethodName) + { + // Original class name -> obfuscated method names. + Map methodMap = (Map)classMethodMap.get(className); + if (methodMap == null) + { + methodMap = new HashMap(); + classMethodMap.put(className, methodMap); + } + + // Obfuscated method name -> methods. + Set methodSet = (Set)methodMap.get(newMethodName); + if (methodSet == null) + { + methodSet = new LinkedHashSet(); + methodMap.put(newMethodName, methodSet); + } + + // Add the method information. + methodSet.add(new MethodInfo(firstLineNumber, + lastLineNumber, + methodReturnType, + methodArguments, + methodName)); + } + + + /** + * A field record. + */ + private static class FieldInfo + { + private String type; + private String originalName; + + + private FieldInfo(String type, String originalName) + { + this.type = type; + this.originalName = originalName; + } + + + private boolean matches(String type) + { + return + type == null || type.equals(this.type); + } + } + + + /** + * A method record. + */ + private static class MethodInfo + { + private int firstLineNumber; + private int lastLineNumber; + private String type; + private String arguments; + private String originalName; + + + private MethodInfo(int firstLineNumber, int lastLineNumber, String type, String arguments, String originalName) + { + this.firstLineNumber = firstLineNumber; + this.lastLineNumber = lastLineNumber; + this.type = type; + this.arguments = arguments; + this.originalName = originalName; + } + + + private boolean matches(int lineNumber, String type, String arguments) + { + return + (lineNumber == 0 || (firstLineNumber <= lineNumber && lineNumber <= lastLineNumber) || lastLineNumber == 0) && + (type == null || type.equals(this.type)) && + (arguments == null || arguments.equals(this.arguments)); + } + } + + + /** + * The main program for ReTrace. + */ + public static void main(String[] args) + { + if (args.length < 1) + { + System.err.println("Usage: java proguard.ReTrace [-verbose] <mapping_file> [<stacktrace_file>]"); + System.exit(-1); + } + + String regularExpresssion = STACK_TRACE_EXPRESSION; + boolean verbose = false; + + int argumentIndex = 0; + while (argumentIndex < args.length) + { + String arg = args[argumentIndex]; + if (arg.equals(REGEX_OPTION)) + { + regularExpresssion = args[++argumentIndex]; + } + else if (arg.equals(VERBOSE_OPTION)) + { + verbose = true; + } + else + { + break; + } + + argumentIndex++; + } + + if (argumentIndex >= args.length) + { + System.err.println("Usage: java proguard.ReTrace [-regex <regex>] [-verbose] <mapping_file> [<stacktrace_file>]"); + System.exit(-1); + } + + File mappingFile = new File(args[argumentIndex++]); + File stackTraceFile = argumentIndex < args.length ? + new File(args[argumentIndex]) : + null; + + ReTrace reTrace = new ReTrace(regularExpresssion, verbose, mappingFile, stackTraceFile); + + try + { + // Execute ReTrace with its given settings. + reTrace.execute(); + } + catch (IOException ex) + { + if (verbose) + { + // Print a verbose stack trace. + ex.printStackTrace(); + } + else + { + // Print just the stack trace message. + System.err.println("Error: "+ex.getMessage()); + } + + System.exit(1); + } + + System.exit(0); + } +} |