mirror of
https://github.com/tp7309/TTDeDroid.git
synced 2026-10-16 21:01:21 +08:00
437 lines
13 KiB
Python
437 lines
13 KiB
Python
#!/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()
|