1 /* 2 * ProGuard -- shrinking, optimization, obfuscation, and preverification 3 * of Java bytecode. 4 * 5 * Copyright (c) 2002-2009 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 = "(?:\\s*%c:.*)|(?:\\s*at\\s+%c.%m\\s*\\(.*?(?::%l)?\\)\\s*)"; 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 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 // Read 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 183 try 184 { 185 StringBuffer outLine = new StringBuffer(256); 186 List extraOutLines = new ArrayList(); 187 188 String className = null; 189 190 // Read the line in the stack trace. 191 while (true) 192 { 193 String line = reader.readLine(); 194 if (line == null) 195 { 196 break; 197 } 198 199 Matcher matcher = pattern.matcher(line); 200 201 if (matcher.matches()) 202 { 203 int lineNumber = 0; 204 String type = null; 205 String arguments = null; 206 207 // Figure out a class name, line number, type, and 208 // arguments beforehand. 209 for (int expressionTypeIndex = 0; expressionTypeIndex < expressionTypeCount; expressionTypeIndex++) 210 { 211 int startIndex = matcher.start(expressionTypeIndex + 1); 212 if (startIndex >= 0) 213 { 214 String match = matcher.group(expressionTypeIndex + 1); 215 216 char expressionType = expressionTypes[expressionTypeIndex]; 217 switch (expressionType) 218 { 219 case 'c': 220 className = originalClassName(match); 221 break; 222 223 case 'C': 224 className = originalClassName(ClassUtil.externalClassName(match)); 225 break; 226 227 case 'l': 228 lineNumber = Integer.parseInt(match); 229 break; 230 231 case 't': 232 type = originalType(match); 233 break; 234 235 case 'a': 236 arguments = originalArguments(match); 237 break; 238 } 239 } 240 } 241 242 // Actually construct the output line. 243 int lineIndex = 0; 244 245 outLine.setLength(0); 246 extraOutLines.clear(); 247 248 for (int expressionTypeIndex = 0; expressionTypeIndex < expressionTypeCount; expressionTypeIndex++) 249 { 250 int startIndex = matcher.start(expressionTypeIndex + 1); 251 if (startIndex >= 0) 252 { 253 int endIndex = matcher.end(expressionTypeIndex + 1); 254 String match = matcher.group(expressionTypeIndex + 1); 255 256 // Copy a literal piece of input line. 257 outLine.append(line.substring(lineIndex, startIndex)); 258 259 char expressionType = expressionTypes[expressionTypeIndex]; 260 switch (expressionType) 261 { 262 case 'c': 263 className = originalClassName(match); 264 outLine.append(className); 265 break; 266 267 case 'C': 268 className = originalClassName(ClassUtil.externalClassName(match)); 269 outLine.append(ClassUtil.internalClassName(className)); 270 break; 271 272 case 'l': 273 lineNumber = Integer.parseInt(match); 274 outLine.append(match); 275 break; 276 277 case 't': 278 type = originalType(match); 279 outLine.append(type); 280 break; 281 282 case 'f': 283 originalFieldName(className, 284 match, 285 type, 286 outLine, 287 extraOutLines); 288 break; 289 290 case 'm': 291 originalMethodName(className, 292 match, 293 lineNumber, 294 type, 295 arguments, 296 outLine, 297 extraOutLines); 298 break; 299 300 case 'a': 301 arguments = originalArguments(match); 302 outLine.append(arguments); 303 break; 304 } 305 306 // Skip the original element whose processed version 307 // has just been appended. 308 lineIndex = endIndex; 309 } 310 } 311 312 // Copy the last literal piece of input line. 313 outLine.append(line.substring(lineIndex)); 314 315 // Print out the main line. 316 System.out.println(outLine); 317 318 // Print out any additional lines. 319 for (int extraLineIndex = 0; extraLineIndex < extraOutLines.size(); extraLineIndex++) 320 { 321 System.out.println(extraOutLines.get(extraLineIndex)); 322 } 323 } 324 else 325 { 326 // Print out the original line. 327 System.out.println(line); 328 } 329 } 330 } 331 catch (IOException ex) 332 { 333 throw new IOException("Can't read stack trace (" + ex.getMessage() + ")"); 334 } 335 finally 336 { 337 if (stackTraceFile != null) 338 { 339 try 340 { 341 reader.close(); 342 } 343 catch (IOException ex) 344 { 345 // This shouldn't happen. 346 } 347 } 348 } 349 } 350 351 352 /** 353 * Finds the original field name(s), appending the first one to the out 354 * line, and any additional alternatives to the extra lines. 355 */ originalFieldName(String className, String obfuscatedFieldName, String type, StringBuffer outLine, List extraOutLines)356 private void originalFieldName(String className, 357 String obfuscatedFieldName, 358 String type, 359 StringBuffer outLine, 360 List extraOutLines) 361 { 362 int extraIndent = -1; 363 364 // Class name -> obfuscated field names. 365 Map fieldMap = (Map)classFieldMap.get(className); 366 if (fieldMap != null) 367 { 368 // Obfuscated field names -> fields. 369 Set fieldSet = (Set)fieldMap.get(obfuscatedFieldName); 370 if (fieldSet != null) 371 { 372 // Find all matching fields. 373 Iterator fieldInfoIterator = fieldSet.iterator(); 374 while (fieldInfoIterator.hasNext()) 375 { 376 FieldInfo fieldInfo = (FieldInfo)fieldInfoIterator.next(); 377 if (fieldInfo.matches(type)) 378 { 379 // Is this the first matching field? 380 if (extraIndent < 0) 381 { 382 extraIndent = outLine.length(); 383 384 // Append the first original name. 385 if (verbose) 386 { 387 outLine.append(fieldInfo.type).append(' '); 388 } 389 outLine.append(fieldInfo.originalName); 390 } 391 else 392 { 393 // Create an additional line with the proper 394 // indentation. 395 StringBuffer extraBuffer = new StringBuffer(); 396 for (int counter = 0; counter < extraIndent; counter++) 397 { 398 extraBuffer.append(' '); 399 } 400 401 // Append the alternative name. 402 if (verbose) 403 { 404 extraBuffer.append(fieldInfo.type).append(' '); 405 } 406 extraBuffer.append(fieldInfo.originalName); 407 408 // Store the additional line. 409 extraOutLines.add(extraBuffer); 410 } 411 } 412 } 413 } 414 } 415 416 // Just append the obfuscated name if we haven't found any matching 417 // fields. 418 if (extraIndent < 0) 419 { 420 outLine.append(obfuscatedFieldName); 421 } 422 } 423 424 425 /** 426 * Finds the original method name(s), appending the first one to the out 427 * line, and any additional alternatives to the extra lines. 428 */ originalMethodName(String className, String obfuscatedMethodName, int lineNumber, String type, String arguments, StringBuffer outLine, List extraOutLines)429 private void originalMethodName(String className, 430 String obfuscatedMethodName, 431 int lineNumber, 432 String type, 433 String arguments, 434 StringBuffer outLine, 435 List extraOutLines) 436 { 437 int extraIndent = -1; 438 439 // Class name -> obfuscated method names. 440 Map methodMap = (Map)classMethodMap.get(className); 441 if (methodMap != null) 442 { 443 // Obfuscated method names -> methods. 444 Set methodSet = (Set)methodMap.get(obfuscatedMethodName); 445 if (methodSet != null) 446 { 447 // Find all matching methods. 448 Iterator methodInfoIterator = methodSet.iterator(); 449 while (methodInfoIterator.hasNext()) 450 { 451 MethodInfo methodInfo = (MethodInfo)methodInfoIterator.next(); 452 if (methodInfo.matches(lineNumber, type, arguments)) 453 { 454 // Is this the first matching method? 455 if (extraIndent < 0) 456 { 457 extraIndent = outLine.length(); 458 459 // Append the first original name. 460 if (verbose) 461 { 462 outLine.append(methodInfo.type).append(' '); 463 } 464 outLine.append(methodInfo.originalName); 465 if (verbose) 466 { 467 outLine.append('(').append(methodInfo.arguments).append(')'); 468 } 469 } 470 else 471 { 472 // Create an additional line with the proper 473 // indentation. 474 StringBuffer extraBuffer = new StringBuffer(); 475 for (int counter = 0; counter < extraIndent; counter++) 476 { 477 extraBuffer.append(' '); 478 } 479 480 // Append the alternative name. 481 if (verbose) 482 { 483 extraBuffer.append(methodInfo.type).append(' '); 484 } 485 extraBuffer.append(methodInfo.originalName); 486 if (verbose) 487 { 488 extraBuffer.append('(').append(methodInfo.arguments).append(')'); 489 } 490 491 // Store the additional line. 492 extraOutLines.add(extraBuffer); 493 } 494 } 495 } 496 } 497 } 498 499 // Just append the obfuscated name if we haven't found any matching 500 // methods. 501 if (extraIndent < 0) 502 { 503 outLine.append(obfuscatedMethodName); 504 } 505 } 506 507 508 /** 509 * Returns the original argument types. 510 */ originalArguments(String obfuscatedArguments)511 private String originalArguments(String obfuscatedArguments) 512 { 513 StringBuffer originalArguments = new StringBuffer(); 514 515 int startIndex = 0; 516 while (true) 517 { 518 int endIndex = obfuscatedArguments.indexOf(',', startIndex); 519 if (endIndex < 0) 520 { 521 break; 522 } 523 524 originalArguments.append(originalType(obfuscatedArguments.substring(startIndex, endIndex).trim())).append(','); 525 526 startIndex = endIndex + 1; 527 } 528 529 originalArguments.append(originalType(obfuscatedArguments.substring(startIndex).trim())); 530 531 return originalArguments.toString(); 532 } 533 534 535 /** 536 * Returns the original type. 537 */ originalType(String obfuscatedType)538 private String originalType(String obfuscatedType) 539 { 540 int index = obfuscatedType.indexOf('['); 541 542 return index >= 0 ? 543 originalClassName(obfuscatedType.substring(0, index)) + obfuscatedType.substring(index) : 544 originalClassName(obfuscatedType); 545 } 546 547 548 /** 549 * Returns the original class name. 550 */ originalClassName(String obfuscatedClassName)551 private String originalClassName(String obfuscatedClassName) 552 { 553 String originalClassName = (String)classMap.get(obfuscatedClassName); 554 555 return originalClassName != null ? 556 originalClassName : 557 obfuscatedClassName; 558 } 559 560 561 // Implementations for MappingProcessor. 562 processClassMapping(String className, String newClassName)563 public boolean processClassMapping(String className, String newClassName) 564 { 565 // Obfuscated class name -> original class name. 566 classMap.put(newClassName, className); 567 568 return true; 569 } 570 571 processFieldMapping(String className, String fieldType, String fieldName, String newFieldName)572 public void processFieldMapping(String className, String fieldType, String fieldName, String newFieldName) 573 { 574 // Original class name -> obfuscated field names. 575 Map fieldMap = (Map)classFieldMap.get(className); 576 if (fieldMap == null) 577 { 578 fieldMap = new HashMap(); 579 classFieldMap.put(className, fieldMap); 580 } 581 582 // Obfuscated field name -> fields. 583 Set fieldSet = (Set)fieldMap.get(newFieldName); 584 if (fieldSet == null) 585 { 586 fieldSet = new LinkedHashSet(); 587 fieldMap.put(newFieldName, fieldSet); 588 } 589 590 // Add the field information. 591 fieldSet.add(new FieldInfo(fieldType, 592 fieldName)); 593 } 594 595 processMethodMapping(String className, int firstLineNumber, int lastLineNumber, String methodReturnType, String methodName, String methodArguments, String newMethodName)596 public void processMethodMapping(String className, int firstLineNumber, int lastLineNumber, String methodReturnType, String methodName, String methodArguments, String newMethodName) 597 { 598 // Original class name -> obfuscated method names. 599 Map methodMap = (Map)classMethodMap.get(className); 600 if (methodMap == null) 601 { 602 methodMap = new HashMap(); 603 classMethodMap.put(className, methodMap); 604 } 605 606 // Obfuscated method name -> methods. 607 Set methodSet = (Set)methodMap.get(newMethodName); 608 if (methodSet == null) 609 { 610 methodSet = new LinkedHashSet(); 611 methodMap.put(newMethodName, methodSet); 612 } 613 614 // Add the method information. 615 methodSet.add(new MethodInfo(firstLineNumber, 616 lastLineNumber, 617 methodReturnType, 618 methodArguments, 619 methodName)); 620 } 621 622 623 /** 624 * A field record. 625 */ 626 private static class FieldInfo 627 { 628 private String type; 629 private String originalName; 630 631 FieldInfo(String type, String originalName)632 private FieldInfo(String type, String originalName) 633 { 634 this.type = type; 635 this.originalName = originalName; 636 } 637 638 matches(String type)639 private boolean matches(String type) 640 { 641 return 642 type == null || type.equals(this.type); 643 } 644 } 645 646 647 /** 648 * A method record. 649 */ 650 private static class MethodInfo 651 { 652 private int firstLineNumber; 653 private int lastLineNumber; 654 private String type; 655 private String arguments; 656 private String originalName; 657 658 MethodInfo(int firstLineNumber, int lastLineNumber, String type, String arguments, String originalName)659 private MethodInfo(int firstLineNumber, int lastLineNumber, String type, String arguments, String originalName) 660 { 661 this.firstLineNumber = firstLineNumber; 662 this.lastLineNumber = lastLineNumber; 663 this.type = type; 664 this.arguments = arguments; 665 this.originalName = originalName; 666 } 667 668 matches(int lineNumber, String type, String arguments)669 private boolean matches(int lineNumber, String type, String arguments) 670 { 671 return 672 (lineNumber == 0 || (firstLineNumber <= lineNumber && lineNumber <= lastLineNumber) || lastLineNumber == 0) && 673 (type == null || type.equals(this.type)) && 674 (arguments == null || arguments.equals(this.arguments)); 675 } 676 } 677 678 679 /** 680 * The main program for ReTrace. 681 */ main(String[] args)682 public static void main(String[] args) 683 { 684 if (args.length < 1) 685 { 686 System.err.println("Usage: java proguard.ReTrace [-verbose] <mapping_file> [<stacktrace_file>]"); 687 System.exit(-1); 688 } 689 690 String regularExpresssion = STACK_TRACE_EXPRESSION; 691 boolean verbose = false; 692 693 int argumentIndex = 0; 694 while (argumentIndex < args.length) 695 { 696 String arg = args[argumentIndex]; 697 if (arg.equals(REGEX_OPTION)) 698 { 699 regularExpresssion = args[++argumentIndex]; 700 } 701 else if (arg.equals(VERBOSE_OPTION)) 702 { 703 verbose = true; 704 } 705 else 706 { 707 break; 708 } 709 710 argumentIndex++; 711 } 712 713 if (argumentIndex >= args.length) 714 { 715 System.err.println("Usage: java proguard.ReTrace [-regex <regex>] [-verbose] <mapping_file> [<stacktrace_file>]"); 716 System.exit(-1); 717 } 718 719 File mappingFile = new File(args[argumentIndex++]); 720 File stackTraceFile = argumentIndex < args.length ? 721 new File(args[argumentIndex]) : 722 null; 723 724 ReTrace reTrace = new ReTrace(regularExpresssion, verbose, mappingFile, stackTraceFile); 725 726 try 727 { 728 // Execute ReTrace with its given settings. 729 reTrace.execute(); 730 } 731 catch (IOException ex) 732 { 733 if (verbose) 734 { 735 // Print a verbose stack trace. 736 ex.printStackTrace(); 737 } 738 else 739 { 740 // Print just the stack trace message. 741 System.err.println("Error: "+ex.getMessage()); 742 } 743 744 System.exit(1); 745 } 746 747 System.exit(0); 748 } 749 } 750