1 /* 2 * ProGuard -- shrinking, optimization, obfuscation, and preverification 3 * of Java bytecode. 4 * 5 * Copyright (c) 2002-2014 Eric Lafortune (eric@graphics.cornell.edu) 6 * 7 * This program is free software; you can redistribute it and/or modify it 8 * under the terms of the GNU General Public License as published by the Free 9 * Software Foundation; either version 2 of the License, or (at your option) 10 * any later version. 11 * 12 * This program is distributed in the hope that it will be useful, but WITHOUT 13 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 14 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 15 * more details. 16 * 17 * You should have received a copy of the GNU General Public License along 18 * with this program; if not, write to the Free Software Foundation, Inc., 19 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 20 */ 21 package proguard.retrace; 22 23 import proguard.classfile.util.ClassUtil; 24 import proguard.obfuscate.*; 25 26 import java.io.*; 27 import java.util.*; 28 import java.util.regex.*; 29 30 31 /** 32 * Tool for de-obfuscating stack traces of applications that were obfuscated 33 * with ProGuard. 34 * 35 * @author Eric Lafortune 36 */ 37 public class ReTrace 38 implements MappingProcessor 39 { 40 private static final String REGEX_OPTION = "-regex"; 41 private static final String VERBOSE_OPTION = "-verbose"; 42 43 44 public static final String STACK_TRACE_EXPRESSION = "(?:.*?\\bat\\s+%c\\.%m\\s*\\(.*?(?::%l)?\\)\\s*)|(?:(?:.*?[:\"]\\s+)?%c(?::.*)?)"; 45 46 private static final String REGEX_CLASS = "\\b(?:[A-Za-z0-9_$]+\\.)*[A-Za-z0-9_$]+\\b"; 47 private static final String REGEX_CLASS_SLASH = "\\b(?:[A-Za-z0-9_$]+/)*[A-Za-z0-9_$]+\\b"; 48 private static final String REGEX_LINE_NUMBER = "\\b[0-9]+\\b"; 49 private static final String REGEX_TYPE = REGEX_CLASS + "(?:\\[\\])*"; 50 private static final String REGEX_MEMBER = "<?\\b[A-Za-z0-9_$]+\\b>?"; 51 private static final String REGEX_ARGUMENTS = "(?:" + REGEX_TYPE + "(?:\\s*,\\s*" + REGEX_TYPE + ")*)?"; 52 53 // The class settings. 54 private final String regularExpression; 55 private final boolean verbose; 56 private final File mappingFile; 57 private final File stackTraceFile; 58 59 private Map classMap = new HashMap(); 60 private Map classFieldMap = new HashMap(); 61 private Map classMethodMap = new HashMap(); 62 63 64 /** 65 * Creates a new ReTrace object to process stack traces on the standard 66 * input, based on the given mapping file name. 67 * @param regularExpression the regular expression for parsing the lines in 68 * the stack trace. 69 * @param verbose specifies whether the de-obfuscated stack trace 70 * should be verbose. 71 * @param mappingFile the mapping file that was written out by 72 * ProGuard. 73 */ ReTrace(String regularExpression, boolean verbose, File mappingFile)74 public ReTrace(String regularExpression, 75 boolean verbose, 76 File mappingFile) 77 { 78 this(regularExpression, verbose, mappingFile, null); 79 } 80 81 82 /** 83 * Creates a new ReTrace object to process a stack trace from the given file, 84 * based on the given mapping file name. 85 * @param regularExpression the regular expression for parsing the lines in 86 * the stack trace. 87 * @param verbose specifies whether the de-obfuscated stack trace 88 * should be verbose. 89 * @param mappingFile the mapping file that was written out by 90 * ProGuard. 91 * @param stackTraceFile the optional name of the file that contains the 92 * stack trace. 93 */ ReTrace(String regularExpression, boolean verbose, File mappingFile, File stackTraceFile)94 public ReTrace(String regularExpression, 95 boolean verbose, 96 File mappingFile, 97 File stackTraceFile) 98 { 99 this.regularExpression = regularExpression; 100 this.verbose = verbose; 101 this.mappingFile = mappingFile; 102 this.stackTraceFile = stackTraceFile; 103 } 104 105 106 /** 107 * Performs the subsequent ReTrace operations. 108 */ execute()109 public void execute() throws IOException 110 { 111 // Read the mapping file. 112 MappingReader mappingReader = new MappingReader(mappingFile); 113 mappingReader.pump(this); 114 115 // Construct the regular expression. 116 StringBuffer expressionBuffer = new StringBuffer(regularExpression.length() + 32); 117 char[] expressionTypes = new char[32]; 118 int expressionTypeCount = 0; 119 int index = 0; 120 while (true) 121 { 122 int nextIndex = regularExpression.indexOf('%', index); 123 if (nextIndex < 0 || 124 nextIndex == regularExpression.length()-1 || 125 expressionTypeCount == expressionTypes.length) 126 { 127 break; 128 } 129 130 expressionBuffer.append(regularExpression.substring(index, nextIndex)); 131 expressionBuffer.append('('); 132 133 char expressionType = regularExpression.charAt(nextIndex + 1); 134 switch(expressionType) 135 { 136 case 'c': 137 expressionBuffer.append(REGEX_CLASS); 138 break; 139 140 case 'C': 141 expressionBuffer.append(REGEX_CLASS_SLASH); 142 break; 143 144 case 'l': 145 expressionBuffer.append(REGEX_LINE_NUMBER); 146 break; 147 148 case 't': 149 expressionBuffer.append(REGEX_TYPE); 150 break; 151 152 case 'f': 153 expressionBuffer.append(REGEX_MEMBER); 154 break; 155 156 case 'm': 157 expressionBuffer.append(REGEX_MEMBER); 158 break; 159 160 case 'a': 161 expressionBuffer.append(REGEX_ARGUMENTS); 162 break; 163 } 164 165 expressionBuffer.append(')'); 166 167 expressionTypes[expressionTypeCount++] = expressionType; 168 169 index = nextIndex + 2; 170 } 171 172 expressionBuffer.append(regularExpression.substring(index)); 173 174 Pattern pattern = Pattern.compile(expressionBuffer.toString()); 175 176 // Open the stack trace file. 177 LineNumberReader reader = 178 new LineNumberReader(stackTraceFile == null ? 179 (Reader)new InputStreamReader(System.in) : 180 (Reader)new BufferedReader(new FileReader(stackTraceFile))); 181 182 // Read and process the lines of the stack trace. 183 try 184 { 185 StringBuffer outLine = new StringBuffer(256); 186 List extraOutLines = new ArrayList(); 187 188 String className = null; 189 190 // Read all lines from the stack trace. 191 while (true) 192 { 193 // Read a line. 194 String line = reader.readLine(); 195 if (line == null) 196 { 197 break; 198 } 199 200 // Try to match it against the regular expression. 201 Matcher matcher = pattern.matcher(line); 202 203 if (matcher.matches()) 204 { 205 // The line matched the regular expression. 206 int lineNumber = 0; 207 String type = null; 208 String arguments = null; 209 210 // Extract a class name, a line number, a type, and 211 // arguments. 212 for (int expressionTypeIndex = 0; expressionTypeIndex < expressionTypeCount; expressionTypeIndex++) 213 { 214 int startIndex = matcher.start(expressionTypeIndex + 1); 215 if (startIndex >= 0) 216 { 217 String match = matcher.group(expressionTypeIndex + 1); 218 219 char expressionType = expressionTypes[expressionTypeIndex]; 220 switch (expressionType) 221 { 222 case 'c': 223 className = originalClassName(match); 224 break; 225 226 case 'C': 227 className = originalClassName(ClassUtil.externalClassName(match)); 228 break; 229 230 case 'l': 231 lineNumber = Integer.parseInt(match); 232 break; 233 234 case 't': 235 type = originalType(match); 236 break; 237 238 case 'a': 239 arguments = originalArguments(match); 240 break; 241 } 242 } 243 } 244 245 // Deconstruct the input line and reconstruct the output 246 // line. Also collect any additional output lines for this 247 // line. 248 int lineIndex = 0; 249 250 outLine.setLength(0); 251 extraOutLines.clear(); 252 253 for (int expressionTypeIndex = 0; expressionTypeIndex < expressionTypeCount; expressionTypeIndex++) 254 { 255 int startIndex = matcher.start(expressionTypeIndex + 1); 256 if (startIndex >= 0) 257 { 258 int endIndex = matcher.end(expressionTypeIndex + 1); 259 String match = matcher.group(expressionTypeIndex + 1); 260 261 // Copy a literal piece of the input line. 262 outLine.append(line.substring(lineIndex, startIndex)); 263 264 // Copy a matched and translated piece of the input line. 265 char expressionType = expressionTypes[expressionTypeIndex]; 266 switch (expressionType) 267 { 268 case 'c': 269 className = originalClassName(match); 270 outLine.append(className); 271 break; 272 273 case 'C': 274 className = originalClassName(ClassUtil.externalClassName(match)); 275 outLine.append(ClassUtil.internalClassName(className)); 276 break; 277 278 case 'l': 279 lineNumber = Integer.parseInt(match); 280 outLine.append(match); 281 break; 282 283 case 't': 284 type = originalType(match); 285 outLine.append(type); 286 break; 287 288 case 'f': 289 originalFieldName(className, 290 match, 291 type, 292 outLine, 293 extraOutLines); 294 break; 295 296 case 'm': 297 originalMethodName(className, 298 match, 299 lineNumber, 300 type, 301 arguments, 302 outLine, 303 extraOutLines); 304 break; 305 306 case 'a': 307 arguments = originalArguments(match); 308 outLine.append(arguments); 309 break; 310 } 311 312 // Skip the original element whose processed version 313 // has just been appended. 314 lineIndex = endIndex; 315 } 316 } 317 318 // Copy the last literal piece of the input line. 319 outLine.append(line.substring(lineIndex)); 320 321 // Print out the processed line. 322 System.out.println(outLine); 323 324 // Print out any additional lines. 325 for (int extraLineIndex = 0; extraLineIndex < extraOutLines.size(); extraLineIndex++) 326 { 327 System.out.println(extraOutLines.get(extraLineIndex)); 328 } 329 } 330 else 331 { 332 // The line didn't match the regular expression. 333 // Print out the original line. 334 System.out.println(line); 335 } 336 } 337 } 338 catch (IOException ex) 339 { 340 throw new IOException("Can't read stack trace (" + ex.getMessage() + ")"); 341 } 342 finally 343 { 344 if (stackTraceFile != null) 345 { 346 try 347 { 348 reader.close(); 349 } 350 catch (IOException ex) 351 { 352 // This shouldn't happen. 353 } 354 } 355 } 356 } 357 358 359 /** 360 * Finds the original field name(s), appending the first one to the out 361 * line, and any additional alternatives to the extra lines. 362 */ originalFieldName(String className, String obfuscatedFieldName, String type, StringBuffer outLine, List extraOutLines)363 private void originalFieldName(String className, 364 String obfuscatedFieldName, 365 String type, 366 StringBuffer outLine, 367 List extraOutLines) 368 { 369 int extraIndent = -1; 370 371 // Class name -> obfuscated field names. 372 Map fieldMap = (Map)classFieldMap.get(className); 373 if (fieldMap != null) 374 { 375 // Obfuscated field names -> fields. 376 Set fieldSet = (Set)fieldMap.get(obfuscatedFieldName); 377 if (fieldSet != null) 378 { 379 // Find all matching fields. 380 Iterator fieldInfoIterator = fieldSet.iterator(); 381 while (fieldInfoIterator.hasNext()) 382 { 383 FieldInfo fieldInfo = (FieldInfo)fieldInfoIterator.next(); 384 if (fieldInfo.matches(type)) 385 { 386 // Is this the first matching field? 387 if (extraIndent < 0) 388 { 389 extraIndent = outLine.length(); 390 391 // Append the first original name. 392 if (verbose) 393 { 394 outLine.append(fieldInfo.type).append(' '); 395 } 396 outLine.append(fieldInfo.originalName); 397 } 398 else 399 { 400 // Create an additional line with the proper 401 // indentation. 402 StringBuffer extraBuffer = new StringBuffer(); 403 for (int counter = 0; counter < extraIndent; counter++) 404 { 405 extraBuffer.append(' '); 406 } 407 408 // Append the alternative name. 409 if (verbose) 410 { 411 extraBuffer.append(fieldInfo.type).append(' '); 412 } 413 extraBuffer.append(fieldInfo.originalName); 414 415 // Store the additional line. 416 extraOutLines.add(extraBuffer); 417 } 418 } 419 } 420 } 421 } 422 423 // Just append the obfuscated name if we haven't found any matching 424 // fields. 425 if (extraIndent < 0) 426 { 427 outLine.append(obfuscatedFieldName); 428 } 429 } 430 431 432 /** 433 * Finds the original method name(s), appending the first one to the out 434 * line, and any additional alternatives to the extra lines. 435 */ originalMethodName(String className, String obfuscatedMethodName, int lineNumber, String type, String arguments, StringBuffer outLine, List extraOutLines)436 private void originalMethodName(String className, 437 String obfuscatedMethodName, 438 int lineNumber, 439 String type, 440 String arguments, 441 StringBuffer outLine, 442 List extraOutLines) 443 { 444 int extraIndent = -1; 445 446 // Class name -> obfuscated method names. 447 Map methodMap = (Map)classMethodMap.get(className); 448 if (methodMap != null) 449 { 450 // Obfuscated method names -> methods. 451 Set methodSet = (Set)methodMap.get(obfuscatedMethodName); 452 if (methodSet != null) 453 { 454 // Find all matching methods. 455 Iterator methodInfoIterator = methodSet.iterator(); 456 while (methodInfoIterator.hasNext()) 457 { 458 MethodInfo methodInfo = (MethodInfo)methodInfoIterator.next(); 459 if (methodInfo.matches(lineNumber, type, arguments)) 460 { 461 // Is this the first matching method? 462 if (extraIndent < 0) 463 { 464 extraIndent = outLine.length(); 465 466 // Append the first original name. 467 if (verbose) 468 { 469 outLine.append(methodInfo.type).append(' '); 470 } 471 outLine.append(methodInfo.originalName); 472 if (verbose) 473 { 474 outLine.append('(').append(methodInfo.arguments).append(')'); 475 } 476 } 477 else 478 { 479 // Create an additional line with the proper 480 // indentation. 481 StringBuffer extraBuffer = new StringBuffer(); 482 for (int counter = 0; counter < extraIndent; counter++) 483 { 484 extraBuffer.append(' '); 485 } 486 487 // Append the alternative name. 488 if (verbose) 489 { 490 extraBuffer.append(methodInfo.type).append(' '); 491 } 492 extraBuffer.append(methodInfo.originalName); 493 if (verbose) 494 { 495 extraBuffer.append('(').append(methodInfo.arguments).append(')'); 496 } 497 498 // Store the additional line. 499 extraOutLines.add(extraBuffer); 500 } 501 } 502 } 503 } 504 } 505 506 // Just append the obfuscated name if we haven't found any matching 507 // methods. 508 if (extraIndent < 0) 509 { 510 outLine.append(obfuscatedMethodName); 511 } 512 } 513 514 515 /** 516 * Returns the original argument types. 517 */ originalArguments(String obfuscatedArguments)518 private String originalArguments(String obfuscatedArguments) 519 { 520 StringBuffer originalArguments = new StringBuffer(); 521 522 int startIndex = 0; 523 while (true) 524 { 525 int endIndex = obfuscatedArguments.indexOf(',', startIndex); 526 if (endIndex < 0) 527 { 528 break; 529 } 530 531 originalArguments.append(originalType(obfuscatedArguments.substring(startIndex, endIndex).trim())).append(','); 532 533 startIndex = endIndex + 1; 534 } 535 536 originalArguments.append(originalType(obfuscatedArguments.substring(startIndex).trim())); 537 538 return originalArguments.toString(); 539 } 540 541 542 /** 543 * Returns the original type. 544 */ originalType(String obfuscatedType)545 private String originalType(String obfuscatedType) 546 { 547 int index = obfuscatedType.indexOf('['); 548 549 return index >= 0 ? 550 originalClassName(obfuscatedType.substring(0, index)) + obfuscatedType.substring(index) : 551 originalClassName(obfuscatedType); 552 } 553 554 555 /** 556 * Returns the original class name. 557 */ originalClassName(String obfuscatedClassName)558 private String originalClassName(String obfuscatedClassName) 559 { 560 String originalClassName = (String)classMap.get(obfuscatedClassName); 561 562 return originalClassName != null ? 563 originalClassName : 564 obfuscatedClassName; 565 } 566 567 568 // Implementations for MappingProcessor. 569 processClassMapping(String className, String newClassName)570 public boolean processClassMapping(String className, String newClassName) 571 { 572 // Obfuscated class name -> original class name. 573 classMap.put(newClassName, className); 574 575 return true; 576 } 577 578 processFieldMapping(String className, String fieldType, String fieldName, String newFieldName)579 public void processFieldMapping(String className, String fieldType, String fieldName, String newFieldName) 580 { 581 // Original class name -> obfuscated field names. 582 Map fieldMap = (Map)classFieldMap.get(className); 583 if (fieldMap == null) 584 { 585 fieldMap = new HashMap(); 586 classFieldMap.put(className, fieldMap); 587 } 588 589 // Obfuscated field name -> fields. 590 Set fieldSet = (Set)fieldMap.get(newFieldName); 591 if (fieldSet == null) 592 { 593 fieldSet = new LinkedHashSet(); 594 fieldMap.put(newFieldName, fieldSet); 595 } 596 597 // Add the field information. 598 fieldSet.add(new FieldInfo(fieldType, 599 fieldName)); 600 } 601 602 processMethodMapping(String className, int firstLineNumber, int lastLineNumber, String methodReturnType, String methodName, String methodArguments, String newMethodName)603 public void processMethodMapping(String className, int firstLineNumber, int lastLineNumber, String methodReturnType, String methodName, String methodArguments, String newMethodName) 604 { 605 // Original class name -> obfuscated method names. 606 Map methodMap = (Map)classMethodMap.get(className); 607 if (methodMap == null) 608 { 609 methodMap = new HashMap(); 610 classMethodMap.put(className, methodMap); 611 } 612 613 // Obfuscated method name -> methods. 614 Set methodSet = (Set)methodMap.get(newMethodName); 615 if (methodSet == null) 616 { 617 methodSet = new LinkedHashSet(); 618 methodMap.put(newMethodName, methodSet); 619 } 620 621 // Add the method information. 622 methodSet.add(new MethodInfo(firstLineNumber, 623 lastLineNumber, 624 methodReturnType, 625 methodArguments, 626 methodName)); 627 } 628 629 630 /** 631 * A field record. 632 */ 633 private static class FieldInfo 634 { 635 private String type; 636 private String originalName; 637 638 FieldInfo(String type, String originalName)639 private FieldInfo(String type, String originalName) 640 { 641 this.type = type; 642 this.originalName = originalName; 643 } 644 645 matches(String type)646 private boolean matches(String type) 647 { 648 return 649 type == null || type.equals(this.type); 650 } 651 } 652 653 654 /** 655 * A method record. 656 */ 657 private static class MethodInfo 658 { 659 private int firstLineNumber; 660 private int lastLineNumber; 661 private String type; 662 private String arguments; 663 private String originalName; 664 665 MethodInfo(int firstLineNumber, int lastLineNumber, String type, String arguments, String originalName)666 private MethodInfo(int firstLineNumber, int lastLineNumber, String type, String arguments, String originalName) 667 { 668 this.firstLineNumber = firstLineNumber; 669 this.lastLineNumber = lastLineNumber; 670 this.type = type; 671 this.arguments = arguments; 672 this.originalName = originalName; 673 } 674 675 matches(int lineNumber, String type, String arguments)676 private boolean matches(int lineNumber, String type, String arguments) 677 { 678 return 679 (lineNumber == 0 || (firstLineNumber <= lineNumber && lineNumber <= lastLineNumber) || lastLineNumber == 0) && 680 (type == null || type.equals(this.type)) && 681 (arguments == null || arguments.equals(this.arguments)); 682 } 683 } 684 685 686 /** 687 * The main program for ReTrace. 688 */ main(String[] args)689 public static void main(String[] args) 690 { 691 if (args.length < 1) 692 { 693 System.err.println("Usage: java proguard.ReTrace [-verbose] <mapping_file> [<stacktrace_file>]"); 694 System.exit(-1); 695 } 696 697 String regularExpresssion = STACK_TRACE_EXPRESSION; 698 boolean verbose = false; 699 700 int argumentIndex = 0; 701 while (argumentIndex < args.length) 702 { 703 String arg = args[argumentIndex]; 704 if (arg.equals(REGEX_OPTION)) 705 { 706 regularExpresssion = args[++argumentIndex]; 707 } 708 else if (arg.equals(VERBOSE_OPTION)) 709 { 710 verbose = true; 711 } 712 else 713 { 714 break; 715 } 716 717 argumentIndex++; 718 } 719 720 if (argumentIndex >= args.length) 721 { 722 System.err.println("Usage: java proguard.ReTrace [-regex <regex>] [-verbose] <mapping_file> [<stacktrace_file>]"); 723 System.exit(-1); 724 } 725 726 File mappingFile = new File(args[argumentIndex++]); 727 File stackTraceFile = argumentIndex < args.length ? 728 new File(args[argumentIndex]) : 729 null; 730 731 ReTrace reTrace = new ReTrace(regularExpresssion, verbose, mappingFile, stackTraceFile); 732 733 try 734 { 735 // Execute ReTrace with its given settings. 736 reTrace.execute(); 737 } 738 catch (IOException ex) 739 { 740 if (verbose) 741 { 742 // Print a verbose stack trace. 743 ex.printStackTrace(); 744 } 745 else 746 { 747 // Print just the stack trace message. 748 System.err.println("Error: "+ex.getMessage()); 749 } 750 751 System.exit(1); 752 } 753 754 System.exit(0); 755 } 756 } 757