/* * ProGuard -- shrinking, optimization, obfuscation, and preverification * of Java bytecode. * * Copyright (c) 2002-2017 Eric Lafortune @ GuardSquare * * 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.obfuscate.MappingReader; import java.io.*; import java.util.*; /** * Tool for de-obfuscating stack traces of applications that were obfuscated * with ProGuard. * * @author Eric Lafortune */ public class ReTrace { private static final String USAGE = "Usage: java proguard.retrace.ReTrace [-regex <regex>] [-verbose] <mapping_file> [<stacktrace_file>]"; 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*\\(%s(?::%l)?\\)\\s*(?:~\\[.*\\])?)|(?:(?:.*?[:\"]\\s+)?%c(?::.*)?)"; // The settings. private final String regularExpression; private final boolean verbose; private final File mappingFile; /** * Creates a new ReTrace instance. * @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 = regularExpression; this.verbose = verbose; this.mappingFile = mappingFile; } /** * De-obfuscates a given stack trace. * @param stackTraceReader a reader for the obfuscated stack trace. * @param stackTraceWriter a writer for the de-obfuscated stack trace. */ public void retrace(LineNumberReader stackTraceReader, PrintWriter stackTraceWriter) throws IOException { // Create a pattern for stack frames. FramePattern pattern = new FramePattern(regularExpression, verbose); // Create a remapper. FrameRemapper mapper = new FrameRemapper(); // Read the mapping file. MappingReader mappingReader = new MappingReader(mappingFile); mappingReader.pump(mapper); // Read and process the lines of the stack trace. while (true) { // Read a line. String obfuscatedLine = stackTraceReader.readLine(); if (obfuscatedLine == null) { break; } // Try to match it against the regular expression. FrameInfo obfuscatedFrame = pattern.parse(obfuscatedLine); if (obfuscatedFrame != null) { // Transform the obfuscated frame back to one or more // original frames. Iterator<FrameInfo> retracedFrames = mapper.transform(obfuscatedFrame).iterator(); String previousLine = null; while (retracedFrames.hasNext()) { // Retrieve the next retraced frame. FrameInfo retracedFrame = retracedFrames.next(); // Format the retraced line. String retracedLine = pattern.format(obfuscatedLine, retracedFrame); // Clear the common first part of ambiguous alternative // retraced lines, to present a cleaner list of // alternatives. String trimmedLine = previousLine != null && obfuscatedFrame.getLineNumber() == 0 ? trim(retracedLine, previousLine) : retracedLine; // Print out the retraced line. if (trimmedLine != null) { stackTraceWriter.println(trimmedLine); } previousLine = retracedLine; } } else { // Print out the original line. stackTraceWriter.println(obfuscatedLine); } } stackTraceWriter.flush(); } /** * Returns the first given string, with any leading characters that it has * in common with the second string replaced by spaces. */ private String trim(String string1, String string2) { StringBuilder line = new StringBuilder(string1); // Find the common part. int trimEnd = firstNonCommonIndex(string1, string2); if (trimEnd == string1.length()) { return null; } // Don't clear the last identifier characters. trimEnd = lastNonIdentifierIndex(string1, trimEnd) + 1; // Clear the common characters. for (int index = 0; index < trimEnd; index++) { if (!Character.isWhitespace(string1.charAt(index))) { line.setCharAt(index, ' '); } } return line.toString(); } /** * Returns the index of the first character that is not the same in both * given strings. */ private int firstNonCommonIndex(String string1, String string2) { int index = 0; while (index < string1.length() && index < string2.length() && string1.charAt(index) == string2.charAt(index)) { index++; } return index; } /** * Returns the index of the last character that is not an identifier * character in the given string, at or before the given index. */ private int lastNonIdentifierIndex(String line, int index) { while (index >= 0 && Character.isJavaIdentifierPart(line.charAt(index))) { index--; } return index; } /** * The main program for ReTrace. */ public static void main(String[] args) { // Parse the arguments. if (args.length < 1) { System.err.println(USAGE); 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); System.exit(-1); } // Convert the arguments into File instances. File mappingFile = new File(args[argumentIndex++]); File stackTraceFile = argumentIndex < args.length ? new File(args[argumentIndex]) : null; try { // Open the input stack trace. We're always using the UTF-8 // character encoding, even for reading from the standard // input. LineNumberReader reader = new LineNumberReader( new BufferedReader( new InputStreamReader(stackTraceFile == null ? System.in : new FileInputStream(stackTraceFile), "UTF-8"))); // Open the output stack trace, again using UTF-8 encoding. PrintWriter writer = new PrintWriter(new OutputStreamWriter(System.out, "UTF-8")); try { // Execute ReTrace with the collected settings. new ReTrace(regularExpresssion, verbose, mappingFile) .retrace(reader, writer); } finally { // Close the input stack trace if it was a file. if (stackTraceFile != null) { reader.close(); } } } 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); } }