-
Notifications
You must be signed in to change notification settings - Fork 176
/
showjar.py
436 lines (358 loc) · 12.9 KB
/
showjar.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author: tp7309
# usage:
# python showjar.py *.apk/*.aar/*.dex/*.jar
from __future__ import print_function
import os
import shutil
import subprocess
import zipfile
import argparse
import glob
import sys
import stat
import fnmatch
import re
# determine if application is a script file or frozen exe
if getattr(sys, 'frozen', False):
bundle_dir = os.path.abspath(os.path.join(os.path.dirname(sys.executable), os.path.pardir))
else:
# we are running in a normal Python environment
bundle_dir = os.path.dirname(os.path.abspath(__file__))
_ROOT_PATH = bundle_dir
print("root path: " + _ROOT_PATH)
_LIBS = os.path.join(_ROOT_PATH, 'libs')
DEX2JAR = os.path.join(_LIBS, 'dex2jar')
JDGUI = os.path.join(_LIBS, 'jd-gui')
APKTOOL = os.path.join(_LIBS, 'apktool')
JADX = os.path.join(_LIBS, 'jadx')
ENJARIFY = os.path.join(_LIBS, 'enjarify')
FERNFLOWER = os.path.join(_LIBS, 'fernflower')
CACHE_DIR = os.path.join(_ROOT_PATH, 'cache')
# may be robust patch file, match full path.
_NEED_UNZIP_FILES = ['patch.jar']
def newshell(command):
if os.name == 'nt':
return "start cmd /c %s" % (command)
else:
return "sh -c %s" % (command)
def sh(command, print_msg=True):
p = subprocess.Popen(
command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
bytes_result = p.stdout.read()
try:
result = bytes_result.decode('utf-8')
except Exception:
# fix some window system error
result = bytes_result.decode('gbk')
if print_msg:
print(result)
return result
def run(command, print_msg=True):
if print_msg:
print(command)
subprocess.call(command, shell=True)
def make_executable(file):
if not os.name == 'nt':
run("chmod a+x %s" % (file), print_msg=False)
def clean_temp_error_files():
# when dex2jar decompile error, some error file generated.
files = glob.glob("%s/classes?-error.zip" % (_ROOT_PATH))
for file in files:
os.remove(file)
# clean strange file name
def clean_filename(cache, path):
name = os.path.basename(path)
pattern = re.compile(u"[-\\.\\w]+")
name2 = '_'.join(re.findall(pattern, name))
if name != name2:
newname = "ttdedroid_%s"%(name2)
newpath = os.path.join(cache, newname)
shutil.copyfile(path, newpath)
return newpath
return path
def readonly_handler(func, path, execinfo):
# or os.chmod(path, stat.S_IWRITE) from "stat" module
os.chmod(path, stat.S_IWRITE)
func(path)
def rmtree(path):
if os.path.exists(path):
shutil.rmtree(path, onerror=readonly_handler)
def hasexec(cmd):
if ' ' in cmd:
cmd = "\"%s\""%(cmd)
result = sh("%s --version" % (cmd), print_msg=False)
return not ('not found' in result or u'不是' in result)
def find_jdkpaths():
jdks = []
# find 'java' in "JAVA_HOME" directory
java_home = os.environ.get('JAVA_HOME', '')
if java_home:
# java_home_parent_dir = os.path.abspath(os.path.join(java_home, os.path.pardir))
jdks.append(java_home)
# IDEA jdk path
jdks.append(os.path.join(os.path.join(os.path.expanduser('~'), '.jdks')))
if os.name == 'nt':
jdks.append("C:\\Program Files\\Android\\Android Studio\\jre")
jdks.append("D:\\Program Files\\Android\\Android Studio\\jre")
pass
elif sys.platform == 'darwin':
jdks.append("/Applications/Android Studio.app")
jdks.append("/Applications/IntelliJ IDEA CE.app")
jdks.append("/Applications/IntelliJ IDEA.app")
jdkpaths = []
java_paths = []
for path in jdks:
java_paths = java_paths + glob.glob("%s/**/java"%(path), recursive=True)
java_paths = java_paths + glob.glob("%s/**/java.exe"%(path), recursive=True)
# print(jdks)
for path in java_paths:
if hasexec(path):
jdkpaths.append(os.path.abspath(os.path.join(path, os.path.pardir, os.path.pardir)))
print("founded jdk locations:")
print(jdkpaths)
return jdkpaths
def find_jdk11_or_greater_version():
def isValidJdk(jdkpath):
re_jdk_version = r'\"(\d+)\.(\d+)\.(\d+).*\"'
try:
# print(jdkpath)
result = re.findall(re_jdk_version, sh("\"%s\" -version"%(jdkpath), print_msg=False))
print("find java version: %s"%(result))
if not result:
return False
result = result[0]
major_version = int(result[0])
second_version = int(result[1])
# print("%i.%i"%(major_version, second_version))
if major_version > 1:
return True
else:
return major_version >= 1 and second_version >= 11
except Exception as e:
print(e)
return False
# check current java version
if (isValidJdk('java')):
return 'java'
files = []
jdks = find_jdkpaths()
if not jdks:
return None
for jdkpath in jdks:
file = ''
if os.name == 'nt':
file = os.path.join("%s/bin/java.exe"%(jdkpath))
else:
file = os.path.join("%s/bin/java"%(jdkpath))
if isValidJdk(file):
print("found jdk location(jdk>=11): %s"%(file))
return file
return None
def ensure_java_home():
"""if no default 'JAVA_HOME', try find a available java home path"""
# export java_home
java_home = os.environ.get('JAVA_HOME', '')
if not java_home:
jdkpaths = find_jdkpaths()
if not jdkpaths:
return False
temp_java_home = jdkpaths[0]
os.environ['JAVA_HOME'] = temp_java_home
print("no default 'JAVA_HOME' system variable, set JAVA_HOME=%s"%(temp_java_home))
else:
print("find JAVA_HOME=%s"%(java_home))
return True
def openfile(path):
# open with vscode by default
if hasexec('code'):
run("code \"%s\""%(path))
return
if os.name == 'nt':
run("start %s" % (path))
else:
run("open %s" % (path))
def find_files(directory, pattern):
filenames = []
for root, dirs, files in os.walk(directory):
for basename in files:
if fnmatch.fnmatch(basename, pattern):
filename = os.path.join(root, basename)
filenames.append(filename)
return filenames
def findjar(dirpath, rejar):
jars = glob.glob("%s/%s" % (dirpath, rejar))
if jars:
return jars[0]
return ''
def jdguipath():
return findjar(JDGUI, 'jd-gui*.jar')
def apktoolpath():
return findjar(APKTOOL, 'apktool*.jar')
def jadxpath():
if os.name == 'nt':
return os.path.join(JADX, 'bin', 'jadx-gui.bat')
else:
return os.path.join(JADX, 'bin', 'jadx-gui')
def fernflowerpath():
return os.path.join(FERNFLOWER, 'fernflower.jar')
# -----------------------------------tool end-----------------------------------#
def show_jar_gui(files):
if not files:
print("not jar input")
make_executable(jadxpath())
for jar in files:
run("java -jar %s %s"%(jdguipath(), jar))
# filepath = ' '.join(files)
# print("use jadx to view class reference...")
# run("%s -j 8 %s" % (jadxpath(), filepath))
def decompile_by_enjarify(cache, args):
if not args.file.endswith('.apk') and not args.file.endswith('.dex'):
print("enjarify only support apk/dex file!")
return
if sys.version_info < (3, 5):
print("enjarify only support python3")
return
deres(cache, args)
os.chdir(ENJARIFY)
enjarify = 'enjarify.bat' if os.name == 'nt' else './enjarify.sh'
make_executable(enjarify)
output_file = os.path.join(cache, "%s-enjarify.jar" %
(os.path.splitext(os.path.basename(args.file))[0]))
run("%s -o %s %s" % (enjarify, output_file, args.file))
jars = glob.glob("%s/*.jar" % (cache))
if jars and args.t == 0:
show_jar_gui(jars)
def decompile_by_jadx(cache, args):
# support *.apk/*.aar/*.dex/*.jar
make_executable(jadxpath())
if args.res == 1:
run("%s -r -j 8 %s" % (jadxpath(), args.file))
else:
run("%s -j 8 %s" % (jadxpath(), args.file))
def dex2jar(cache, args):
cmd = os.path.join(DEX2JAR, 'd2j-dex2jar.bat')
if not os.name == 'nt':
cmd = os.path.join(DEX2JAR, 'd2j-dex2jar.sh')
run("chmod a+x %s" % (cmd))
dexes = []
jars = []
deres(cache, args)
temp_dir = os.path.abspath(
os.path.join(cache,
os.path.splitext(os.path.basename(args.file))[0]))
print(args.file)
if args.file.endswith(".apk") or args.file in _NEED_UNZIP_FILES:
print("unzip %s..." % (args.file))
with zipfile.ZipFile(args.file, 'r') as z:
z.extractall(temp_dir)
dexes = [
os.path.join(temp_dir, dex) for dex in os.listdir(temp_dir)
if dex.endswith('.dex')
]
print("founded dexes: " + ', '.join(dexes))
for dex in dexes:
dest = os.path.splitext(dex)[0] + "-dex2jar.jar"
run("%s -f %s -o %s" % (cmd, dex, dest))
if not os.path.exists(dest):
print("\n%s decompile failed!\n" % (dest))
else:
jars.append(dest)
elif args.file.endswith(".aar"):
print("unzip %s..." % (args.file))
with zipfile.ZipFile(args.file, 'r') as z:
z.extractall(temp_dir)
# maybe has ''libs' directory
jars.extend(find_files(temp_dir, "*.jar"))
elif args.file.endswith(".dex"):
dest = os.path.join(temp_dir, "classes-dex2jar.jar")
print(dest)
run("%s -f %s -o %s" % (cmd, args.file, dest))
if not os.path.exists(dest):
print("%s decompile failed!" % (dest))
else:
jars.append(dest)
elif args.file.endswith(".jar"):
jars.append(args.file)
else:
print("invalid file extension!")
resdest = os.path.join(cache, 'res')
print("when decompile resources done, resources stored in: %s" % (resdest))
return jars
def decompile_by_dex2jar(cache, args):
jars = dex2jar(cache, args)
if jars and args.t == 0:
show_jar_gui(jars)
def decompile_by_fernflower(cache, args):
# support *.apk/*.aar/*.dex/*.jar
make_executable(fernflowerpath())
jars = dex2jar(cache, args)
if not jars:
return
print('fernflower need jdk>=11, ensure jdk11 or above version is installed...')
jdkpath = find_jdk11_or_greater_version()
if not jdkpath:
print("fernflower need jdk>=11, but not found!")
return
for jar in jars:
run("%s -jar %s %s %s"%(jdkpath, fernflowerpath(), jar, cache))
jars = glob.glob("%s/%s"%(cache, "*.jar"))
print(jars)
if jars and args.t == 0:
show_jar_gui(jars)
def deres(cache, args):
if args.res == 1 and args.file.endswith(".apk"):
print("decompile resources...")
resdest = os.path.join(cache, 'res')
run(newshell("java -jar %s d %s -f -o %s" %
(apktoolpath(), args.file, resdest)))
print("decompile resources done, path: %s" % (resdest))
def main():
parser = argparse.ArgumentParser(description='android decompile tool',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('-o', '--output', nargs='?',
help='output directory, optional')
parser.add_argument('-r', '--res', nargs='?', type=int, default=0,
help='decode resources, 0:disable, 1:enable')
parser.add_argument('-e', '--engine', nargs='?', default='jadx',
help='decompiler engine, [jadx, dex2jar, enjarify,fernflower]')
parser.add_argument('-t', nargs='?', type=int,
default=0, help=argparse.SUPPRESS)
parser.add_argument(
'file', help='input file path, *.apk/*.aar/*.dex/*.jar')
args = parser.parse_args()
if args.output:
args.output = os.path.join(os.getcwd(), args.output)
cache = args.output if args.output else CACHE_DIR
# recreate cache dir
if os.path.abspath(cache) == os.path.abspath(CACHE_DIR):
print("clearing cache...")
rmtree(CACHE_DIR)
print("clear cache done")
if not os.path.exists(cache):
os.mkdir(cache)
f = args.file
if not os.path.isabs(args.file):
f = os.path.join(os.getcwd(), args.file)
f = clean_filename(cache, f)
# fix windows path
if ":\\" in f and ":\\\\" not in f:
f = f.replace("\\", "\\\\")
args.file = f
print("output dir: %s" % (cache))
if not ensure_java_home():
print("can not found jdk, ensure jdk is installed!")
return
if args.engine == 'jadx':
decompile_by_jadx(cache, args)
elif args.engine == 'enjarify':
decompile_by_enjarify(cache, args)
elif args.engine == 'fernflower':
decompile_by_fernflower(cache, args)
else:
decompile_by_dex2jar(cache, args)
clean_temp_error_files()
print("\nDone")
if __name__ == "__main__":
main()