/* * ProGuard -- shrinking, optimization, obfuscation, and preverification * of Java bytecode. * * Copyright (c) 2002-2013 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 = "(?:.*?\\bat\\s+%c.%m\\s*\\(.*?(?::%l)?\\)\\s*)|(?:(?:.*?[:\"]\\s+)?%c(?::.*)?)"; 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 = "?"; 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] []"); 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 ] [-verbose] []"); 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); } }