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 */ 21package proguard.retrace; 22 23import proguard.classfile.util.ClassUtil; 24import proguard.obfuscate.*; 25 26import java.io.*; 27import java.util.*; 28import 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 */ 37public class ReTrace 38implements 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 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 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 639 private FieldInfo(String type, String originalName) 640 { 641 this.type = type; 642 this.originalName = originalName; 643 } 644 645 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 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 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 */ 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