├── 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 | 
16 |
17 | 4. Hopper will pop up a message box, you can choose the decompile type:
18 | 
19 |
20 | 5. If you choose Decompile One Class, the following box will appear:
21 | 
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
--------------------------------------------------------------------------------