├── Class Decompile.py ├── README.md └── Screenshots ├── decompile_choose_type.png ├── decompile_input_class_name.png └── decompile_menu.png /Class Decompile.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | import gc 5 | import os 6 | 7 | 8 | IGNORED_CLASS_PREFIXES = [ 9 | 'AFNetwork', 'AFHTTP', 'AFURL', 'AFSecurity', 10 | 'Flurry', 'FMDatabase', 11 | 'MBProgressHUD', 'MJ', 12 | 'SDWebImage', 13 | ] 14 | 15 | 16 | IGNORED_CLASS_LABEL_NAMES = [ 17 | '-[ClassName methodName:]', 18 | ] 19 | 20 | 21 | def is_ignored_class(class_name): 22 | for prefix in IGNORED_CLASS_PREFIXES: 23 | if class_name.startswith(prefix): 24 | return True 25 | return False 26 | 27 | 28 | def is_ignored_method(label_name): 29 | for name in IGNORED_CLASS_LABEL_NAMES: 30 | if name == label_name: 31 | return True 32 | return False 33 | 34 | 35 | #pragma mark - Save File 36 | 37 | def get_file_path(class_name): 38 | return '%s/%s.m'%(path, class_name) 39 | 40 | 41 | def get_file_header(class_name): 42 | return '''\ 43 | // 44 | // %s.m 45 | // 46 | // Generated by Class Decompile. 47 | // Repository is https://github.com/poboke/Class-Decompile 48 | // Copyright © 2016 www.poboke.com. All rights reserved. 49 | // 50 | 51 | @implementation %s 52 | 53 | '''%(class_name, class_name) 54 | 55 | 56 | def get_file_footer(): 57 | return '''\ 58 | @end 59 | ''' 60 | 61 | 62 | #pragma mark - Decompile 63 | 64 | def parse_label_name(label_name): 65 | result = re.search(r'^([+-])\[(.+)\s(.+)\]', label_name) 66 | if result: 67 | symbol, class_name, method_name = result.groups() 68 | params_count = method_name.count(':') 69 | params = tuple(['arg%d'%(i+2) for i in range(params_count)]) 70 | method_name = method_name.replace(':', ':(id)%s ')%(params) 71 | method_name = '%s (%%s)%s'%(symbol, method_name) 72 | return (class_name, method_name) 73 | else: 74 | return (None, None) 75 | 76 | 77 | def start_decompile(input_class_name=None): 78 | classes = {} 79 | total_count = 0 80 | for i in range(segment.getProcedureCount()): 81 | procedure = segment.getProcedureAtIndex(i) 82 | address = procedure.getEntryPoint() 83 | 84 | label_name = segment.getNameAtAddress(address) 85 | if not label_name: 86 | continue 87 | if is_ignored_method(label_name): 88 | continue 89 | 90 | class_name, method_name = parse_label_name(label_name) 91 | if not class_name: 92 | continue 93 | if input_class_name and class_name != input_class_name: 94 | continue 95 | if is_ignored_class(class_name): 96 | continue 97 | if os.path.exists(get_file_path(class_name)): 98 | continue 99 | 100 | procedure.label_name = label_name 101 | procedure.method_name = method_name 102 | classes.setdefault(class_name, []).append(procedure) 103 | total_count += 1 104 | 105 | print 'total count :', total_count 106 | 107 | current_count = 0 108 | for class_name in classes: 109 | print '\n***** %s *****'%(class_name) 110 | codes = get_file_header(class_name) 111 | procedures = classes[class_name] 112 | for procedure in procedures: 113 | current_count += 1 114 | percent = (float(current_count) / total_count) * 100 115 | print '%05.2f%% | %s'%(percent, procedure.label_name) 116 | pseudo_code = procedure.decompile() 117 | if not pseudo_code: 118 | continue 119 | match = re.match(r'.+return\s.+;', pseudo_code, re.DOTALL) 120 | method_type = 'id' if match else 'void' 121 | method_name = procedure.method_name%method_type 122 | codes += '%s\n{\n%s}\n\n'%(method_name, pseudo_code) 123 | codes += get_file_footer() 124 | 125 | file_path = get_file_path(class_name) 126 | with open(file_path, 'w') as file: 127 | file.write(codes) 128 | 129 | del codes 130 | gc.collect() 131 | 132 | os.system('open "%s"'%(path)) 133 | print 'Done!' 134 | 135 | 136 | document = Document.getCurrentDocument() 137 | segment = document.getSegmentByName('__TEXT') 138 | if not segment: 139 | segment = document.getSegmentsList()[0] 140 | 141 | app_name = document.getExecutableFilePath().split('/')[-1] 142 | path = os.path.expanduser('~/ClassDecompiles/' + app_name) 143 | if not os.path.exists(path): 144 | os.makedirs(path) 145 | 146 | message = 'Please choose the decompile type:' 147 | buttons = ['Decompile All Classes', 'Decompile One Class', 'Cancel'] 148 | button_index = document.message(message, buttons) 149 | if button_index == 0: 150 | start_decompile() 151 | elif button_index == 1: 152 | message = 'Please input the class name:' 153 | input_class_name = document.ask(message) 154 | if input_class_name is None: 155 | print 'Cancel decompile!' 156 | elif input_class_name == '': 157 | print 'Class name can not be empty!' 158 | else: 159 | start_decompile(input_class_name) 160 | elif button_index == 2: 161 | print 'Cancel decompile!' 162 | 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Class Decompile 4 | 5 | Class Decompile is a python script for Hopper Disassembler. This script can export pseudo code of the classes. 6 | 7 | 8 | ## How to use 9 | 10 | 1. Move the `Class Decompile.py` file into the `~/Library/Application Support/Hopper/Scripts` directory. 11 | 12 | 2. Drag the executable file to Hopper and wait for the analysis to be completed. 13 | 14 | 3. Click the menu button Scripts -> Class Decompile: 15 | ![image](https://github.com/poboke/Class-Decompile/raw/master/Screenshots/decompile_menu.png) 16 | 17 | 4. Hopper will pop up a message box, you can choose the decompile type: 18 | ![image](https://github.com/poboke/Class-Decompile/raw/master/Screenshots/decompile_choose_type.png) 19 | 20 | 5. If you choose Decompile One Class, the following box will appear: 21 | ![image](https://github.com/poboke/Class-Decompile/raw/master/Screenshots/decompile_input_class_name.png) 22 | Enter a class name, click the OK button to decompile that class. 23 | 24 | 6. The decompiled pseudo-code stored in the `~/ClassDecompiles` directory. 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Screenshots/decompile_choose_type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poboke/Class-Decompile/ae27d52f9ec56d4f6e81d6775e4bac1fcad47dc6/Screenshots/decompile_choose_type.png -------------------------------------------------------------------------------- /Screenshots/decompile_input_class_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poboke/Class-Decompile/ae27d52f9ec56d4f6e81d6775e4bac1fcad47dc6/Screenshots/decompile_input_class_name.png -------------------------------------------------------------------------------- /Screenshots/decompile_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poboke/Class-Decompile/ae27d52f9ec56d4f6e81d6775e4bac1fcad47dc6/Screenshots/decompile_menu.png --------------------------------------------------------------------------------