diff --git a/androguard/cli/cli.py b/androguard/cli/cli.py index c01b9012..9e7cad11 100755 --- a/androguard/cli/cli.py +++ b/androguard/cli/cli.py @@ -6,25 +6,32 @@ import json import sys import click +import networkx as nx from loguru import logger import androguard.core.apk from androguard import util -from androguard.cli.main import (androarsc_main, - androaxml_main, - export_apps_to_format, - androsign_main, - androlyze_main, - androdis_main, - androtrace_main, - androdump_main, - ) +from androguard.cli.main import ( + androarsc_main, + androaxml_main, + androdis_main, + androdump_main, + androlyze_main, + androsign_main, + androtrace_main, + export_apps_to_format, +) -import networkx as nx @click.group(help=__doc__) @click.version_option(version=androguard.__version__) -@click.option("--verbose", "--debug", 'verbosity', flag_value='verbose', help="Print more") +@click.option( + "--verbose", + "--debug", + 'verbosity', + flag_value='verbose', + help="Print more", +) def entry_point(verbosity): if verbosity is None: util.set_log("ERROR") @@ -35,17 +42,22 @@ def entry_point(verbosity): @entry_point.command() @click.option( - '--input', '-i', 'input_', + '--input', + '-i', + 'input_', type=click.Path(exists=True, file_okay=True, dir_okay=False), help='AndroidManifest.xml or APK to parse (legacy option)', ) @click.option( - '--output', '-o', + '--output', + '-o', help='filename to save the decoded AndroidManifest.xml to, default stdout', ) -@click.option("--resource", "-r", - help="Resource (any binary XML file) inside the APK to parse instead of AndroidManifest.xml" - ) +@click.option( + "--resource", + "-r", + help="Resource (any binary XML file) inside the APK to parse instead of AndroidManifest.xml", +) @click.argument( 'file_', required=False, @@ -67,8 +79,10 @@ def axml(input_, output, file_, resource): $ androguard axml AndroidManifest.xml """ if file_ is not None and input_ is not None: - print("Can not give --input and positional argument! " - "Please use only one of them!") + print( + "Can not give --input and positional argument! " + "Please use only one of them!" + ) sys.exit(1) if file_ is None and input_ is None: @@ -83,7 +97,9 @@ def axml(input_, output, file_, resource): @entry_point.command() @click.option( - '--input', '-i', 'input_', + '--input', + '-i', + 'input_', type=click.Path(exists=True), help='resources.arsc or APK to parse (legacy option)', ) @@ -92,52 +108,63 @@ def axml(input_, output, file_, resource): required=False, ) @click.option( - '--output', '-o', + '--output', + '-o', # required=True, # not required due to --list-types help='filename to save the decoded resources to', ) @click.option( - '--package', '-p', + '--package', + '-p', help='Show only resources for the given package name ' - '(default: the first package name found)', + '(default: the first package name found)', ) @click.option( - '--locale', '-l', + '--locale', + '-l', help='Show only resources for the given locale (default: \'\\x00\\x00\')', ) @click.option( - '--type', '-t', 'type_', + '--type', + '-t', + 'type_', help='Show only resources of the given type (default: public)', ) @click.option( - '--id', 'id_', - help="Resolve the given ID for the given locale and package. Provide the hex ID!" + '--id', + 'id_', + help="Resolve the given ID for the given locale and package. Provide the hex ID!", ) @click.option( - '--list-packages', is_flag=True, + '--list-packages', + is_flag=True, default=False, help='List all package names and exit', ) @click.option( - '--list-locales', is_flag=True, + '--list-locales', + is_flag=True, default=False, help='List all package names and exit', ) @click.option( - '--list-types', is_flag=True, + '--list-types', + is_flag=True, default=False, help='List all types and exit', ) -def arsc(input_, - file_, - output, - package, - locale, - type_, - id_, - list_packages, - list_locales, - list_types): +def arsc( + input_, + file_, + output, + package, + locale, + type_, + id_, + list_packages, + list_locales, + list_types, +): """ Decode resources.arsc either directly from a given file or from an APK. @@ -147,11 +174,12 @@ def arsc(input_, $ androguard arsc app.apk """ - from androguard.core import androconf - from androguard.core import axml, apk + from androguard.core import androconf, apk, axml if file_ and input_: - logger.info("Can not give --input and positional argument! Please use only one of them!") + logger.info( + "Can not give --input and positional argument! Please use only one of them!" + ) sys.exit(1) if not input_ and not file_: @@ -187,7 +215,11 @@ def arsc(input_, try: i_id = int(id_, 16) except ValueError: - print("ID '{}' could not be parsed! have you supplied the correct hex ID?".format(id_)) + print( + "ID '{}' could not be parsed! have you supplied the correct hex ID?".format( + id_ + ) + ) sys.exit(1) name = arscobj.get_resource_xml_name(i_id) @@ -201,7 +233,16 @@ def arsc(input_, # All the information is in the config. # we simply need to get the actual value of the entry for config, entry in arscobj.get_resolved_res_configs(i_id): - print("{} = '{}'".format(config.get_qualifier() if not config.is_default() else "", entry)) + print( + "{} = '{}'".format( + ( + config.get_qualifier() + if not config.is_default() + else "" + ), + entry, + ) + ) sys.exit(0) @@ -212,32 +253,49 @@ def arsc(input_, if list_locales: for p in arscobj.get_packages_names(): print("In Package:", p) - print("\n".join(map(lambda x: " \\x00\\x00" - if x == "\x00\x00" - else " {}".format(x), - sorted(arscobj.get_locales(p))))) + print( + "\n".join( + map( + lambda x: ( + " \\x00\\x00" + if x == "\x00\x00" + else " {}".format(x) + ), + sorted(arscobj.get_locales(p)), + ) + ) + ) sys.exit(0) if list_types: for p in arscobj.get_packages_names(): print("In Package:", p) for locale in sorted(arscobj.get_locales(p)): - print(" In Locale: {}".format("\\x00\\x00" - if locale == "\x00\x00" else locale)) - print("\n".join(map(" {}".format, - sorted(arscobj.get_types(p, locale))))) + print( + " In Locale: {}".format( + "\\x00\\x00" if locale == "\x00\x00" else locale + ) + ) + print( + "\n".join( + map( + " {}".format, + sorted(arscobj.get_types(p, locale)), + ) + ) + ) sys.exit(0) - androarsc_main(arscobj, - outp=output, - package=package, - typ=type_, - locale=locale) + androarsc_main( + arscobj, outp=output, package=package, typ=type_, locale=locale + ) @entry_point.command() @click.option( - '--input', '-i', 'input_', + '--input', + '-i', + 'input_', type=click.Path(exists=True, dir_okay=False, file_okay=True), help='APK to parse (legacy option)', ) @@ -247,29 +305,35 @@ def arsc(input_, required=False, ) @click.option( - '--output', '-o', + '--output', + '-o', required=True, help='output directory. If the output folder already exsist, ' - 'it will be overwritten!', + 'it will be overwritten!', ) @click.option( - '--format', '-f', 'format_', + '--format', + '-f', + 'format_', help='Additionally write control flow graphs for each method, specify ' - 'the format for example png, jpg, raw (write dot file), ...', - type=click.Choice(['png', 'jpg', 'raw']) + 'the format for example png, jpg, raw (write dot file), ...', + type=click.Choice(['png', 'jpg', 'raw']), ) @click.option( - '--jar', '-j', + '--jar', + '-j', is_flag=True, default=False, help='Use DEX2JAR to create a JAR file', ) @click.option( - '--limit', '-l', + '--limit', + '-l', help='Limit to certain methods only by regex (default: \'.*\')', ) @click.option( - '--decompiler', '-d', + '--decompiler', + '-d', help='Use a different decompiler (default: DAD)', ) def decompile(input_, file_, output, format_, jar, limit, decompiler): @@ -282,9 +346,13 @@ def decompile(input_, file_, output, format_, jar, limit, decompiler): $ androguard resources.arsc """ from androguard import session + if file_ and input_: - print("Can not give --input and positional argument! " - "Please use only one of them!", file=sys.stderr) + print( + "Can not give --input and positional argument! " + "Please use only one of them!", + file=sys.stderr, + ) sys.exit(1) if not input_ and not file_: @@ -299,29 +367,35 @@ def decompile(input_, file_, output, format_, jar, limit, decompiler): s = session.Session() with open(fname, "rb") as fd: s.add(fname, fd.read()) - export_apps_to_format(fname, s, output, limit, - jar, decompiler, format_) + export_apps_to_format(fname, s, output, limit, jar, decompiler, format_) @entry_point.command() @click.option( - '--hash', 'hash_', + '--hash', + 'hash_', type=click.Choice(['md5', 'sha1', 'sha256', 'sha512']), - default='sha1', show_default=True, + default='sha1', + show_default=True, help='Fingerprint Hash algorithm', ) @click.option( - '--all', '-a', 'print_all_hashes', + '--all', + '-a', + 'print_all_hashes', is_flag=True, - default=False, show_default=True, + default=False, + show_default=True, help='Print all supported hashes', ) @click.option( - '--show', '-s', + '--show', + '-s', is_flag=True, - default=False, show_default=True, + default=False, + show_default=True, help='Additionally of printing the fingerprints, show more ' - 'certificate information', + 'certificate information', ) @click.argument( 'apk', @@ -369,14 +443,20 @@ def analyze(session, apk): @entry_point.command() -@click.option("-o", "--offset", - default=0, - type=int, - help="Offset to start dissassembly inside the file") -@click.option("-s", "--size", - default=0, - type=int, - help="Number of bytes from offset to disassemble, 0 for whole file") +@click.option( + "-o", + "--offset", + default=0, + type=int, + help="Offset to start dissassembly inside the file", +) +@click.option( + "-s", + "--size", + default=0, + type=int, + help="Number of bytes from offset to disassemble, 0 for whole file", +) @click.argument( "DEX", type=click.Path(exists=True, dir_okay=False, file_okay=True), @@ -395,11 +475,16 @@ def disassemble(offset, size, dex): required=False, type=click.Path(exists=True, dir_okay=False, file_okay=True), ) -@click.option("-m", "--modules", - multiple=True, default=[], - help="A list of modules to load in frida") @click.option( - '--enable-ui', is_flag=True, + "-m", + "--modules", + multiple=True, + default=[], + help="A list of modules to load in frida", +) +@click.option( + '--enable-ui', + is_flag=True, default=False, help='Enable UI', ) @@ -422,9 +507,13 @@ def trace(apk, modules, enable_ui): default=None, required=False, ) -@click.option("-m", "--modules", - multiple=True, default=[], - help="A list of modules to load in frida") +@click.option( + "-m", + "--modules", + multiple=True, + default=[], + help="A list of modules to load in frida", +) def dtrace(package_name, modules): """ Start dynamically an installed APK on the phone and start to trace all interesting methods from the modules list @@ -443,9 +532,13 @@ def dtrace(package_name, modules): default=None, required=False, ) -@click.option("-m", "--modules", - multiple=True, default=["androguard/pentest/modules/helpers/dump/dexdump.js"], - help="A list of modules to load in frida") +@click.option( + "-m", + "--modules", + multiple=True, + default=["androguard/pentest/modules/helpers/dump/dexdump.js"], + help="A list of modules to load in frida", +) def dump(package_name, modules): """ Start and dump dynamically an installed APK on the phone @@ -457,23 +550,29 @@ def dump(package_name, modules): """ androdump_main(package_name, modules) + # callgraph exporting utility functions def _write_gml(G, path): """Wrapper around nx.write_gml""" return nx.write_gml(G, path, stringizer=str) + def _write_gpickle(G, path): """Wrapper around pickle dump""" import pickle + with open(path, 'wb') as f: pickle.dump(G, f, pickle.HIGHEST_PROTOCOL) + def _write_yaml(G, path): """Wrapper around yaml dump""" import yaml + with open(path, 'w') as f: yaml.dump(G, f) + # mapping of types to their respective exporting functions write_methods = dict( gml=_write_gml, @@ -481,7 +580,9 @@ write_methods = dict( # gpickle=_write_gpickle, # Pickling can't be done due to BufferedReader attributes (e.g. EncodedMethod.buff) not being serializable graphml=nx.write_graphml, # yaml=_write_yaml, # Same limitation as gpickle - net=nx.write_pajek) + net=nx.write_pajek, +) + @entry_point.command() @click.argument( @@ -490,20 +591,20 @@ write_methods = dict( required=True, ) @click.option( - '--output', '-o', + '--output', + '-o', default='callgraph.gml', help='Filename of the output graph file', ) @click.option( '--output-type', - type=click.Choice( - list(write_methods.keys()), - case_sensitive=False), + type=click.Choice(list(write_methods.keys()), case_sensitive=False), default='gml', - help='Type of the graph to output ' + help='Type of the graph to output ', ) @click.option( - '--show', '-s', + '--show', + '-s', default=False, is_flag=True, help='instead of saving the graph file, render it with matplotlib', @@ -543,21 +644,26 @@ def cg( methodname, descriptor, accessflag, - no_isolated): + no_isolated, +): """ Create a call graph based on the data of Analysis and export it into a graph format. """ + import matplotlib.pyplot as plt + + from androguard.core.analysis.analysis import ExternalMethod from androguard.core.bytecode import FormatClassToJava from androguard.misc import AnalyzeAPK - from androguard.core.analysis.analysis import ExternalMethod - - import matplotlib.pyplot as plt a, d, dx = AnalyzeAPK(file_) - entry_points = map(FormatClassToJava, - a.get_activities() + a.get_providers() + - a.get_services() + a.get_receivers()) + entry_points = map( + FormatClassToJava, + a.get_activities() + + a.get_providers() + + a.get_services() + + a.get_receivers(), + ) entry_points = list(entry_points) callgraph = dx.get_call_graph( @@ -566,14 +672,16 @@ def cg( descriptor, accessflag, no_isolated, - entry_points + entry_points, ) if show: try: import PyQt5 except ImportError: - print("PyQt5 is not installed. In most OS you can install it by running 'pip install PyQt5'.\n") + print( + "PyQt5 is not installed. In most OS you can install it by running 'pip install PyQt5'.\n" + ) exit() pos = nx.spring_layout(callgraph) internal = [] @@ -586,27 +694,24 @@ def cg( internal.append(n) nx.draw_networkx_nodes( - callgraph, - pos=pos, node_color='r', - nodelist=internal) + callgraph, pos=pos, node_color='r', nodelist=internal + ) nx.draw_networkx_nodes( + callgraph, pos=pos, node_color='b', nodelist=external + ) + + nx.draw_networkx_edges(callgraph, pos, width=0.5, arrows=True) + + nx.draw_networkx_labels( callgraph, pos=pos, - node_color='b', - nodelist=external) - - nx.draw_networkx_edges( - callgraph, - pos, - width=0.5, - arrows=True) - - nx.draw_networkx_labels(callgraph, - pos=pos, - font_size=6, - labels={n: f"{n.get_class_name()} {n.name} {n.descriptor}" - for n in callgraph.nodes}) + font_size=6, + labels={ + n: f"{n.get_class_name()} {n.name} {n.descriptor}" + for n in callgraph.nodes + }, + ) plt.draw() plt.show() @@ -614,7 +719,9 @@ def cg( else: output_type_lower = output_type.lower() if output_type_lower not in write_methods: - print(f"Could not find a method to export files to {output_type_lower}!") + print( + f"Could not find a method to export files to {output_type_lower}!" + ) sys.exit(1) write_methods[output_type_lower](callgraph, output) diff --git a/androguard/cli/main.py b/androguard/cli/main.py index 178d6420..0444cad7 100644 --- a/androguard/cli/main.py +++ b/androguard/cli/main.py @@ -5,35 +5,39 @@ import shutil import sys from typing import Union +from loguru import logger + # 3rd party modules from lxml import etree -from loguru import logger from pygments import highlight -from pygments.lexers import get_lexer_by_name from pygments.formatters.terminal import TerminalFormatter +from pygments.lexers import get_lexer_by_name + +from androguard.core import androconf, apk # internal modules -from androguard.core.axml import ARSCParser -from androguard.session import Session -from androguard.core import androconf -from androguard.core import apk -from androguard.core.axml import AXMLPrinter +from androguard.core.axml import ARSCParser, AXMLPrinter from androguard.core.dex import get_bytecodes_method -from androguard.util import readFile +from androguard.session import Session from androguard.ui import DynamicUI -from androguard.util import parse_public, calculate_fingerprint +from androguard.util import calculate_fingerprint, parse_public, readFile + def androaxml_main( - inp:str, - outp:Union[str,None]=None, - resource:Union[str,None]=None) -> None: - + inp: str, outp: Union[str, None] = None, resource: Union[str, None] = None +) -> None: + ret_type = androconf.is_android(inp) if ret_type == "APK": a = apk.APK(inp) if resource: if resource not in a.files: - logger.error("The APK does not contain a file called '{}'".format(resource), file=sys.stderr) + logger.error( + "The APK does not contain a file called '{}'".format( + resource + ), + file=sys.stderr, + ) sys.exit(1) axml = AXMLPrinter(a.get_file(resource)).get_xml_obj() @@ -50,16 +54,23 @@ def androaxml_main( with open(outp, "wb") as fd: fd.write(buff) else: - sys.stdout.write(highlight(buff.decode("UTF-8"), get_lexer_by_name("xml"), TerminalFormatter())) + sys.stdout.write( + highlight( + buff.decode("UTF-8"), + get_lexer_by_name("xml"), + TerminalFormatter(), + ) + ) def androarsc_main( - arscobj: ARSCParser, - outp:Union[str,None]=None, - package:Union[str,None]=None, - typ:Union[str,None]=None, - locale:Union[str,None]=None) -> None: - + arscobj: ARSCParser, + outp: Union[str, None] = None, + package: Union[str, None] = None, + typ: Union[str, None] = None, + locale: Union[str, None] = None, +) -> None: + package = package or arscobj.get_packages_names()[0] ttype = typ or "public" locale = locale or '\x00\x00' @@ -69,36 +80,47 @@ def androarsc_main( # res folder with all the XML files if not hasattr(arscobj, "get_{}_resources".format(ttype)): - print("No decoder found for type: '{}'! Please open a bug report." - .format(ttype), - file=sys.stderr) + print( + "No decoder found for type: '{}'! Please open a bug report.".format( + ttype + ), + file=sys.stderr, + ) sys.exit(1) x = getattr(arscobj, "get_" + ttype + "_resources")(package, locale) - buff = etree.tostring(etree.fromstring(x), - pretty_print=True, - encoding="UTF-8") + buff = etree.tostring( + etree.fromstring(x), pretty_print=True, encoding="UTF-8" + ) if outp: with open(outp, "wb") as fd: fd.write(buff) else: - sys.stdout.write(highlight(buff.decode("UTF-8"), get_lexer_by_name("xml"), TerminalFormatter())) + sys.stdout.write( + highlight( + buff.decode("UTF-8"), + get_lexer_by_name("xml"), + TerminalFormatter(), + ) + ) def export_apps_to_format( - filename:str, - s: Session, - output: str, - methods_filter:Union[str,None]=None, - jar:bool=False, - decompiler_type:Union[str,None]=None, - form:Union[str,None]=None) -> None: - - from androguard.misc import clean_file_name + filename: str, + s: Session, + output: str, + methods_filter: Union[str, None] = None, + jar: bool = False, + decompiler_type: Union[str, None] = None, + form: Union[str, None] = None, +) -> None: + from androguard.core.bytecode import method2dot, method2format from androguard.decompiler import decompiler + from androguard.misc import clean_file_name + print("Dump information {} in {}".format(filename, output)) if not os.path.exists(output): @@ -106,7 +128,11 @@ def export_apps_to_format( os.makedirs(output) else: while True: - user_input = input(f"Do you want to clean the directory {output}? (Y/N): ").strip().lower() + user_input = ( + input(f"Do you want to clean the directory {output}? (Y/N): ") + .strip() + .lower() + ) if user_input == 'y': print("Deleting...") @@ -129,40 +155,61 @@ def export_apps_to_format( sys.stdout.flush() if decompiler_type == "dex2jad": - vm.set_decompiler(decompiler.DecompilerDex2Jad(vm, - androconf.CONF["BIN_DEX2JAR"], - androconf.CONF["BIN_JAD"], - androconf.CONF["TMP_DIRECTORY"])) + vm.set_decompiler( + decompiler.DecompilerDex2Jad( + vm, + androconf.CONF["BIN_DEX2JAR"], + androconf.CONF["BIN_JAD"], + androconf.CONF["TMP_DIRECTORY"], + ) + ) elif decompiler_type == "dex2winejad": - vm.set_decompiler(decompiler.DecompilerDex2WineJad(vm, - androconf.CONF["BIN_DEX2JAR"], - androconf.CONF["BIN_WINEJAD"], - androconf.CONF["TMP_DIRECTORY"])) + vm.set_decompiler( + decompiler.DecompilerDex2WineJad( + vm, + androconf.CONF["BIN_DEX2JAR"], + androconf.CONF["BIN_WINEJAD"], + androconf.CONF["TMP_DIRECTORY"], + ) + ) elif decompiler_type == "ded": - vm.set_decompiler(decompiler.DecompilerDed(vm, - androconf.CONF["BIN_DED"], - androconf.CONF["TMP_DIRECTORY"])) + vm.set_decompiler( + decompiler.DecompilerDed( + vm, + androconf.CONF["BIN_DED"], + androconf.CONF["TMP_DIRECTORY"], + ) + ) elif decompiler_type == "dex2fernflower": - vm.set_decompiler(decompiler.DecompilerDex2Fernflower(vm, - androconf.CONF["BIN_DEX2JAR"], - androconf.CONF["BIN_FERNFLOWER"], - androconf.CONF["OPTIONS_FERNFLOWER"], - androconf.CONF["TMP_DIRECTORY"])) + vm.set_decompiler( + decompiler.DecompilerDex2Fernflower( + vm, + androconf.CONF["BIN_DEX2JAR"], + androconf.CONF["BIN_FERNFLOWER"], + androconf.CONF["OPTIONS_FERNFLOWER"], + androconf.CONF["TMP_DIRECTORY"], + ) + ) print("End") if jar: print("jar ...", end=' ') - filenamejar = decompiler.Dex2Jar(vm, - androconf.CONF["BIN_DEX2JAR"], - androconf.CONF["TMP_DIRECTORY"]).get_jar() + filenamejar = decompiler.Dex2Jar( + vm, + androconf.CONF["BIN_DEX2JAR"], + androconf.CONF["TMP_DIRECTORY"], + ).get_jar() shutil.move(filenamejar, os.path.join(output, "classes.jar")) print("End") for method in vm.get_encoded_methods(): if methods_filter_expr: - msig = "{}{}{}".format(method.get_class_name(), method.get_name(), - method.get_descriptor()) + msig = "{}{}{}".format( + method.get_class_name(), + method.get_name(), + method.get_descriptor(), + ) if not methods_filter_expr.search(msig): continue @@ -171,11 +218,18 @@ def export_apps_to_format( filename_class = os.path.join(output, filename_class) create_directory(filename_class) - print("Dump {} {} {} ...".format(method.get_class_name(), - method.get_name(), - method.get_descriptor()), end=' ') + print( + "Dump {} {} {} ...".format( + method.get_class_name(), + method.get_name(), + method.get_descriptor(), + ), + end=' ', + ) - filename = clean_file_name(os.path.join(filename_class, method.get_short_string())) + filename = clean_file_name( + os.path.join(filename_class, method.get_short_string()) + ) buff = method2dot(vmx.get_method(method)) # Write Graph of method @@ -187,9 +241,13 @@ def export_apps_to_format( if str(method.get_class_name()) not in dump_classes: print("source codes ...", end=' ') current_class = vm.get_class(method.get_class_name()) - current_filename_class = valid_class_name(str(current_class.get_name())) + current_filename_class = valid_class_name( + str(current_class.get_name()) + ) - current_filename_class = os.path.join(output, current_filename_class + ".java") + current_filename_class = os.path.join( + output, current_filename_class + ".java" + ) with open(current_filename_class, "w") as fd: fd.write(current_class.get_source()) dump_classes.append(method.get_class_name()) @@ -202,30 +260,29 @@ def export_apps_to_format( print() -def valid_class_name(class_name:str) -> str: +def valid_class_name(class_name: str) -> str: if class_name[-1] == ";": class_name = class_name[1:-1] return os.path.join(*class_name.split("/")) -def create_directory(pathdir:str) -> None: +def create_directory(pathdir: str) -> None: if not os.path.exists(pathdir): os.makedirs(pathdir) -def androlyze_main(session:Session, filename:str) -> None: +def androlyze_main(session: Session, filename: str) -> None: """ Start an interactive shell :param session: Session file to load :param filename: File to analyze, can be APK or DEX (or ODEX) """ - from colorama import Fore - import colorama import atexit + import colorama + from colorama import Fore from IPython.terminal.embed import embed - from traitlets.config import Config from androguard.core.androconf import ANDROGUARD_VERSION, CONF @@ -251,8 +308,11 @@ def androlyze_main(session:Session, filename:str) -> None: if filetype not in ['DEX', 'DEY', 'APK']: logger.error( - Fore.RED + "This file type is not supported by androlyze for auto loading right now!" + Fore.RESET, - file=sys.stderr) + Fore.RED + + "This file type is not supported by androlyze for auto loading right now!" + + Fore.RESET, + file=sys.stderr, + ) logger.error("But your file is still available:") logger.error(">>> filename") logger.error(repr(filename)) @@ -312,42 +372,59 @@ def androlyze_main(session:Session, filename:str) -> None: ipshell() -def androsign_main(args_apk:list[str], args_hash:str, args_all:bool, show:bool) -> None: +def androsign_main( + args_apk: list[str], args_hash: str, args_all: bool, show: bool +) -> None: + import binascii + import hashlib + import traceback + + from asn1crypto import keys, x509 + from colorama import Fore, Style + from androguard.core.apk import APK from androguard.util import get_certificate_name_string - import hashlib - import binascii - import traceback - from colorama import Fore, Style - from asn1crypto import x509, keys - # Keep the list of hash functions in sync with cli/entry_points.py:sign - hashfunctions = dict(md5=hashlib.md5, - sha1=hashlib.sha1, - sha256=hashlib.sha256, - sha512=hashlib.sha512, - ) + hashfunctions = dict( + md5=hashlib.md5, + sha1=hashlib.sha1, + sha256=hashlib.sha256, + sha512=hashlib.sha512, + ) if args_hash.lower() not in hashfunctions: - print("Hash function {} not supported!" - .format(args_hash.lower()), file=sys.stderr) - print("Use one of {}" - .format(", ".join(hashfunctions.keys())), file=sys.stderr) + print( + "Hash function {} not supported!".format(args_hash.lower()), + file=sys.stderr, + ) + print( + "Use one of {}".format(", ".join(hashfunctions.keys())), + file=sys.stderr, + ) sys.exit(1) for path in args_apk: try: a = APK(path) - print("{}, package: '{}'".format(os.path.basename(path), a.get_package())) + print( + "{}, package: '{}'".format( + os.path.basename(path), a.get_package() + ) + ) print("Is signed v1: {}".format(a.is_signed_v1())) print("Is signed v2: {}".format(a.is_signed_v2())) print("Is signed v3: {}".format(a.is_signed_v3())) - certs = set(a.get_certificates_der_v3() + a.get_certificates_der_v2() + [a.get_certificate_der(x) for x in - a.get_signature_names()]) - pkeys = set(a.get_public_keys_der_v3() + a.get_public_keys_der_v2()) + certs = set( + a.get_certificates_der_v3() + + a.get_certificates_der_v2() + + [a.get_certificate_der(x) for x in a.get_signature_names()] + ) + pkeys = set( + a.get_public_keys_der_v3() + a.get_public_keys_der_v2() + ) if len(certs) > 0: print("Found {} unique certificates".format(len(certs))) @@ -355,30 +432,61 @@ def androsign_main(args_apk:list[str], args_hash:str, args_all:bool, show:bool) for cert in certs: if show: x509_cert = x509.Certificate.load(cert) - print("Issuer:", get_certificate_name_string(x509_cert.issuer, short=True)) - print("Subject:", get_certificate_name_string(x509_cert.subject, short=True)) + print( + "Issuer:", + get_certificate_name_string( + x509_cert.issuer, short=True + ), + ) + print( + "Subject:", + get_certificate_name_string( + x509_cert.subject, short=True + ), + ) print("Serial Number:", hex(x509_cert.serial_number)) print("Hash Algorithm:", x509_cert.hash_algo) print("Signature Algorithm:", x509_cert.signature_algo) - print("Valid not before:", x509_cert['tbs_certificate']['validity']['not_before'].native) - print("Valid not after:", x509_cert['tbs_certificate']['validity']['not_after'].native) + print( + "Valid not before:", + x509_cert['tbs_certificate']['validity'][ + 'not_before' + ].native, + ) + print( + "Valid not after:", + x509_cert['tbs_certificate']['validity'][ + 'not_after' + ].native, + ) if not args_all: - print("{} {}".format(args_hash.lower(), hashfunctions[args_hash.lower()](cert).hexdigest())) + print( + "{} {}".format( + args_hash.lower(), + hashfunctions[args_hash.lower()](cert).hexdigest(), + ) + ) else: for k, v in hashfunctions.items(): print("{} {}".format(k, v(cert).hexdigest())) print() if len(certs) > 0: - print("Found {} unique public keys associated with the certs".format(len(pkeys))) + print( + "Found {} unique public keys associated with the certs".format( + len(pkeys) + ) + ) for public_key in pkeys: if show: parsed_key = parse_public(public_key) print(f"Algorithm: {parsed_key.algorithm}") print(f"Bit size: {parsed_key.bit_size}") - print(f"Fingerprint: {calculate_fingerprint(parsed_key).hex()}") + print( + f"Fingerprint: {calculate_fingerprint(parsed_key).hex()}" + ) try: print(f"Hash Algorithm: {parsed_key.hash_algo}") except ValueError as ve: @@ -386,16 +494,20 @@ def androsign_main(args_apk:list[str], args_hash:str, args_all:bool, show:bool) pass print() - except: - print(Fore.RED + "Error in {}".format(os.path.basename(path)) + Style.RESET_ALL, file=sys.stderr) + print( + Fore.RED + + "Error in {}".format(os.path.basename(path)) + + Style.RESET_ALL, + file=sys.stderr, + ) traceback.print_exc(file=sys.stderr) if len(args_apk) > 1: print() -def androdis_main(offset:int, size:int, dex_file:str) -> None: +def androdis_main(offset: int, size: int, dex_file: str) -> None: from androguard.core.dex import DEX with open(dex_file, "rb") as fp: @@ -408,7 +520,13 @@ def androdis_main(offset:int, size:int, dex_file:str) -> None: for cls in d.get_classes(): print("# CLASS: {}".format(cls.get_name())) for m in cls.get_methods(): - print("## METHOD: {} {} {}".format(m.get_access_flags_string(), m.get_name(), m.get_descriptor())) + print( + "## METHOD: {} {} {}".format( + m.get_access_flags_string(), + m.get_name(), + m.get_descriptor(), + ) + ) for idx, ins in m.get_instructions_idx(): print('{:08x} {}'.format(idx, ins.disasm())) @@ -428,7 +546,12 @@ def androdis_main(offset:int, size:int, dex_file:str) -> None: idx += i.get_length() -def androtrace_main(apk_file:str, list_modules:list[str], live:bool=False, enable_ui:bool=False) -> None: +def androtrace_main( + apk_file: str, + list_modules: list[str], + live: bool = False, + enable_ui: bool = False, +) -> None: from androguard.pentest import Pentest from androguard.session import Session @@ -448,10 +571,14 @@ def androtrace_main(apk_file:str, list_modules:list[str], live:bool=False, enabl if enable_ui: logger.remove(1) - from prompt_toolkit.eventloop.inputhook import InputHookContext, set_eventloop_with_inputhook - from prompt_toolkit.application import get_app import time + from prompt_toolkit.application import get_app + from prompt_toolkit.eventloop.inputhook import ( + InputHookContext, + set_eventloop_with_inputhook, + ) + time.sleep(1) ui = DynamicUI(p.message_queue) @@ -473,7 +600,7 @@ def androtrace_main(apk_file:str, list_modules:list[str], live:bool=False, enabl s = input("Type 'e' to exit:") -def androdump_main(package_name:str, list_modules:list[str]) -> None: +def androdump_main(package_name: str, list_modules: list[str]) -> None: from androguard.pentest import Pentest from androguard.session import Session diff --git a/androguard/core/analysis/analysis.py b/androguard/core/analysis/analysis.py index 32de22b9..4165f00d 100644 --- a/androguard/core/analysis/analysis.py +++ b/androguard/core/analysis/analysis.py @@ -4,17 +4,20 @@ from __future__ import annotations import collections -from enum import IntEnum -from operator import itemgetter import re import time -from typing import Union, Iterator +from enum import IntEnum +from operator import itemgetter +from typing import Iterator, Union -from androguard.core.androconf import is_ascii_problem, load_api_specific_resource_module -from androguard.core import bytecode, dex - -from loguru import logger import networkx as nx +from loguru import logger + +from androguard.core import bytecode, dex +from androguard.core.androconf import ( + is_ascii_problem, + load_api_specific_resource_module, +) BasicOPCODES = set() for i in dex.BRANCH_DEX_OPCODES: @@ -23,16 +26,18 @@ for i in dex.BRANCH_DEX_OPCODES: if p.match(items[1][0]): BasicOPCODES.add(op) + class REF_TYPE(IntEnum): """ Stores the opcodes for the type of usage in an XREF. Used in :class:`ClassAnalysis` to store the type of reference to the class. """ + REF_NEW_INSTANCE = 0x22 - REF_CLASS_USAGE = 0x1c - INVOKE_VIRTUAL = 0x6e - INVOKE_SUPER = 0x6f + REF_CLASS_USAGE = 0x1C + INVOKE_VIRTUAL = 0x6E + INVOKE_SUPER = 0x6F INVOKE_DIRECT = 0x70 INVOKE_STATIC = 0x71 INVOKE_INTERFACE = 0x72 @@ -42,11 +47,12 @@ class REF_TYPE(IntEnum): INVOKE_STATIC_RANGE = 0x77 INVOKE_INTERFACE_RANGE = 0x78 + class ExceptionAnalysis: def __init__(self, exception: list, basic_blocks: BasicBlocks): self.start = exception[0] self.end = exception[1] - + self.exceptions = exception[2:] for i in self.exceptions: @@ -59,7 +65,9 @@ class ExceptionAnalysis: if i[2] is None: buff += "\t({} -> {:x} {})\n".format(i[0], i[1], i[2]) else: - buff += "\t({} -> {:x} {})\n".format(i[0], i[1], i[2].get_name()) + buff += "\t({} -> {:x} {})\n".format( + i[0], i[1], i[2].get_name() + ) return buff[:-1] @@ -67,7 +75,9 @@ class ExceptionAnalysis: d = {"start": self.start, "end": self.end, "list": []} for i in self.exceptions: - d["list"].append({"name": i[0], "idx": i[1], "basic_block": i[2].get_name()}) + d["list"].append( + {"name": i[0], "idx": i[1], "basic_block": i[2].get_name()} + ) return d @@ -80,7 +90,9 @@ class Exceptions: for i in exceptions: self.exceptions.append(ExceptionAnalysis(i, basic_blocks)) - def get_exception(self, addr_start: int, addr_end: int) -> Union[ExceptionAnalysis,None]: + def get_exception( + self, addr_start: int, addr_end: int + ) -> Union[ExceptionAnalysis, None]: for i in self.exceptions: if i.start >= addr_start and i.end <= addr_end: return i @@ -97,12 +109,14 @@ class Exceptions: for i in self.exceptions: yield i + class BasicBlocks: """ This class represents all basic blocks of a method. It is a collection of many :class:`DEXBasicBlock`. """ + def __init__(self) -> None: self.bb = [] @@ -117,7 +131,7 @@ class BasicBlocks: def pop(self, idx: int) -> DEXBasicBlock: return self.bb.pop(idx) - def get_basic_block(self, idx: int) -> Union[DEXBasicBlock,None]: + def get_basic_block(self, idx: int) -> Union[DEXBasicBlock, None]: for i in self.bb: if i.get_start() <= idx < i.get_end(): return i @@ -153,6 +167,7 @@ class BasicBlocks: get = __iter__ get_basic_block_pos = __getitem__ + class DEXBasicBlock: """ A simple basic block of a DEX method. @@ -160,7 +175,14 @@ class DEXBasicBlock: A basic block consists of a series of :class:`~androguard.core.dex.Instruction` which are not interrupted by branch or jump instructions such as `goto`, `if`, `throw`, `return`, `switch` etc. """ - def __init__(self, start: int, vm: dex.DEX, method: dex.EncodedMethod, context: BasicBlocks): + + def __init__( + self, + start: int, + vm: dex.DEX, + method: dex.EncodedMethod, + context: BasicBlocks, + ): """_summary_ :param start: _description_ @@ -187,11 +209,7 @@ class DEXBasicBlock: self.special_ins = {} - self.name = ''.join([ - self.method.get_name(), - '-BB@', - hex(self.start) - ]) + self.name = ''.join([self.method.get_name(), '-BB@', hex(self.start)]) self.exception_analysis = None self.notes = [] @@ -262,7 +280,7 @@ class DEXBasicBlock: :return: androguard.core.dex.Instruction """ return list(self.get_instructions())[-1] - + def get_next(self) -> DEXBasicBlock: """ Get next basic blocks @@ -280,7 +298,7 @@ class DEXBasicBlock: :rtype: DEXBasicBlock """ return self.fathers - + def set_fathers(self, f: DEXBasicBlock) -> None: self.fathers.append(f) @@ -292,15 +310,17 @@ class DEXBasicBlock: if not values: next_block = self.context.get_basic_block(self.end + 1) if next_block is not None: - self.childs.append((self.end - self.get_last_length(), self.end, - next_block)) + self.childs.append( + (self.end - self.get_last_length(), self.end, next_block) + ) else: for i in values: if i != -1: next_block = self.context.get_basic_block(i) if next_block is not None: - self.childs.append((self.end - self.get_last_length(), - i, next_block)) + self.childs.append( + (self.end - self.get_last_length(), i, next_block) + ) for c in self.childs: if c[2] is not None: @@ -314,11 +334,11 @@ class DEXBasicBlock: op_value = i.get_op_value() - if op_value == 0x26 or (0x2b <= op_value <= 0x2c): + if op_value == 0x26 or (0x2B <= op_value <= 0x2C): code = self.method.get_code().get_bc() self.special_ins[idx] = code.get_ins_off(idx + i.get_ref_off() * 2) - - def get_special_ins(self, idx: int) -> Union[dex.Instruction,None]: + + def get_special_ins(self, idx: int) -> Union[dex.Instruction, None]: """ Return the associated instruction to a specific instruction (for example a packed/sparse switch) @@ -338,7 +358,11 @@ class DEXBasicBlock: self.exception_analysis = exception_analysis def show(self) -> None: - print("{}: {:04x} - {:04x}".format(self.get_name(), self.get_start(), self.get_end())) + print( + "{}: {:04x} - {:04x}".format( + self.get_name(), self.get_start(), self.get_end() + ) + ) for note in self.get_notes(): print(note) print('=' * 20) @@ -353,8 +377,13 @@ class MethodAnalysis: :type vm: a :class:`DEX` object :type method: a :class:`EncodedMethod` object """ + def __init__(self, vm: dex.DEX, method: dex.EncodedMethod): - logger.debug("Adding new method {} {}".format(method.get_class_name(), method.get_name())) + logger.debug( + "Adding new method {} {}".format( + method.get_class_name(), method.get_name() + ) + ) self.__vm = vm self.method = method @@ -429,13 +458,19 @@ class MethodAnalysis: Internal Method to create the basic block structure Parses all instructions and exceptions. """ - current_basic = DEXBasicBlock(0, self.__vm, self.method, self.basic_blocks) + current_basic = DEXBasicBlock( + 0, self.__vm, self.method, self.basic_blocks + ) self.basic_blocks.push(current_basic) l = [] h = dict() - logger.debug("Parsing instructions for method at @0x{:08x}".format(self.method.get_code_off())) + logger.debug( + "Parsing instructions for method at @0x{:08x}".format( + self.method.get_code_off() + ) + ) for idx, ins in self.method.get_instructions_idx(): if ins.get_op_value() in BasicOPCODES: v = dex.determineNext(ins, idx, self.method) @@ -454,14 +489,24 @@ class MethodAnalysis: # index is a destination if idx in l: if current_basic.get_nb_instructions() != 0: - current_basic = DEXBasicBlock(current_basic.get_end(), self.__vm, self.method, self.basic_blocks) + current_basic = DEXBasicBlock( + current_basic.get_end(), + self.__vm, + self.method, + self.basic_blocks, + ) self.basic_blocks.push(current_basic) current_basic.push(ins) # index is a branch instruction if idx in h: - current_basic = DEXBasicBlock(current_basic.get_end(), self.__vm, self.method, self.basic_blocks) + current_basic = DEXBasicBlock( + current_basic.get_end(), + self.__vm, + self.method, + self.basic_blocks, + ) self.basic_blocks.push(current_basic) if current_basic.get_nb_instructions() == 0: @@ -479,9 +524,13 @@ class MethodAnalysis: for i in self.basic_blocks.get(): # setup exception by basic block - i.set_exception_analysis(self.exceptions.get_exception(i.start, i.end - 1)) + i.set_exception_analysis( + self.exceptions.get_exception(i.start, i.end - 1) + ) - def add_xref_read(self, classobj:ClassAnalysis, fieldobj: FieldAnalysis, offset: int) -> None: + def add_xref_read( + self, classobj: ClassAnalysis, fieldobj: FieldAnalysis, offset: int + ) -> None: """ :param ClassAnalysis classobj: :param FieldAnalysis fieldobj: @@ -489,7 +538,9 @@ class MethodAnalysis: """ self.xrefread.add((classobj, fieldobj, offset)) - def add_xref_write(self, classobj: ClassAnalysis, fieldobj: FieldAnalysis, offset: int) -> None: + def add_xref_write( + self, classobj: ClassAnalysis, fieldobj: FieldAnalysis, offset: int + ) -> None: """ :param ClassAnalysis classobj: :param FieldAnalysis fieldobj: @@ -517,7 +568,9 @@ class MethodAnalysis: """ return self.xrefwrite - def add_xref_to(self, classobj: ClassAnalysis, methodobj: MethodAnalysis, offset: int) -> None: + def add_xref_to( + self, classobj: ClassAnalysis, methodobj: MethodAnalysis, offset: int + ) -> None: """ Add a crossreference to another method (this method calls another method) @@ -528,7 +581,9 @@ class MethodAnalysis: """ self.xrefto.add((classobj, methodobj, offset)) - def add_xref_from(self, classobj: ClassAnalysis, methodobj: MethodAnalysis, offset: int) -> None: + def add_xref_from( + self, classobj: ClassAnalysis, methodobj: MethodAnalysis, offset: int + ) -> None: """ Add a crossrefernece from another method (this method is called by another method) @@ -563,7 +618,9 @@ class MethodAnalysis: """ return self.xrefto - def add_xref_new_instance(self, classobj: ClassAnalysis, offset: int) -> None: + def add_xref_new_instance( + self, classobj: ClassAnalysis, offset: int + ) -> None: """ Add a crossreference to another class that is instanced within this method. @@ -584,7 +641,9 @@ class MethodAnalysis: """ return self.xrefnewinstance - def add_xref_const_class(self, classobj: ClassAnalysis, offset: int) -> None: + def add_xref_const_class( + self, classobj: ClassAnalysis, offset: int + ) -> None: """ Add a crossreference to another classtype. @@ -626,8 +685,19 @@ class MethodAnalysis: return False # Packages found at https://developer.android.com/reference/packages.html - api_candidates = ["Landroid/", "Lcom/android/internal/util", "Ldalvik/", "Ljava/", "Ljavax/", "Lorg/apache/", - "Lorg/json/", "Lorg/w3c/dom/", "Lorg/xml/sax", "Lorg/xmlpull/v1/", "Ljunit/"] + api_candidates = [ + "Landroid/", + "Lcom/android/internal/util", + "Ldalvik/", + "Ljava/", + "Ljavax/", + "Lorg/apache/", + "Lorg/json/", + "Lorg/w3c/dom/", + "Lorg/xml/sax", + "Lorg/xmlpull/v1/", + "Ljunit/", + ] if self.apilist: # FIXME: This will not work... need to introduce a name for lookup (like EncodedMethod.__str__ but without @@ -686,14 +756,20 @@ class MethodAnalysis: nb_args = len(args) start_reg = reg_len - nb_args - args = ["{} v{}".format(a, start_reg + i) for i, a in enumerate(args)] + args = [ + "{} v{}".format(a, start_reg + i) for i, a in enumerate(args) + ] + + print( + "METHOD {} {} {} ({}){}".format( + self.method.get_class_name(), + self.method.get_access_flags_string(), + self.method.get_name(), + ", ".join(args), + ret, + ) + ) - print("METHOD {} {} {} ({}){}".format( - self.method.get_class_name(), - self.method.get_access_flags_string(), - self.method.get_name(), - ", ".join(args), ret)) - if not self.is_external(): bytecode.PrettyShow(self.basic_blocks.gets(), self.method.notes) @@ -701,12 +777,16 @@ class MethodAnalysis: data = "XREFto for %s\n" % self.method for ref_class, ref_method, offset in self.xrefto: data += "in\n" - data += "{}:{} @0x{:x}\n".format(ref_class.get_vm_class().get_name(), ref_method, offset) + data += "{}:{} @0x{:x}\n".format( + ref_class.get_vm_class().get_name(), ref_method, offset + ) data += "XREFFrom for %s\n" % self.method for ref_class, ref_method, offset in self.xreffrom: data += "in\n" - data += "{}:{} @0x{:x}\n".format(ref_class.get_vm_class().get_name(), ref_method, offset) + data += "{}:{} @0x{:x}\n".format( + ref_class.get_vm_class().get_name(), ref_method, offset + ) return data @@ -723,6 +803,7 @@ class StringAnalysis: This Array stores the information in which method the String is used. """ + def __init__(self, value: str) -> None: """ @@ -732,7 +813,9 @@ class StringAnalysis: self.orig_value = value self.xreffrom = set() - def add_xref_from(self, classobj: ClassAnalysis, methodobj: MethodAnalysis, off: int) -> None: + def add_xref_from( + self, classobj: ClassAnalysis, methodobj: MethodAnalysis, off: int + ) -> None: """ Adds a xref from the given method to this string @@ -742,7 +825,9 @@ class StringAnalysis: """ self.xreffrom.add((classobj, methodobj, off)) - def get_xref_from(self, with_offset:bool = False) -> list[tuple[ClassAnalysis, MethodAnalysis]]: + def get_xref_from( + self, with_offset: bool = False + ) -> list[tuple[ClassAnalysis, MethodAnalysis]]: """ Returns a list of xrefs accessing the String. @@ -789,7 +874,9 @@ class StringAnalysis: def __str__(self): data = "XREFto for string %s in\n" % repr(self.get_value()) for ref_class, ref_method, _ in self.xreffrom: - data += "{}:{}\n".format(ref_class.get_vm_class().get_name(), ref_method) + data += "{}:{}\n".format( + ref_class.get_vm_class().get_name(), ref_method + ) return data def __repr__(self): @@ -812,6 +899,7 @@ class FieldAnalysis: :param androguard.core.dex.EncodedField field: `dvm.EncodedField` """ + def __init__(self, field: dex.EncodedField) -> None: self.field = field self.xrefread = set() @@ -821,7 +909,9 @@ class FieldAnalysis: def name(self) -> str: return self.field.get_name() - def add_xref_read(self, classobj: ClassAnalysis, methodobj: MethodAnalysis, offset: int) -> None: + def add_xref_read( + self, classobj: ClassAnalysis, methodobj: MethodAnalysis, offset: int + ) -> None: """ :param ClassAnalysis classobj: :param MethodAnalysis methodobj: @@ -829,7 +919,9 @@ class FieldAnalysis: """ self.xrefread.add((classobj, methodobj, offset)) - def add_xref_write(self, classobj: ClassAnalysis, methodobj: MethodAnalysis, offset: int) -> None: + def add_xref_write( + self, classobj: ClassAnalysis, methodobj: MethodAnalysis, offset: int + ) -> None: """ :param ClassAnalysis classobj: :param MethodAnalysis methodobj: @@ -837,7 +929,9 @@ class FieldAnalysis: """ self.xrefwrite.add((classobj, methodobj, offset)) - def get_xref_read(self, with_offset:bool = False) -> list[tuple[ClassAnalysis, MethodAnalysis]]: + def get_xref_read( + self, with_offset: bool = False + ) -> list[tuple[ClassAnalysis, MethodAnalysis]]: """ Returns a list of xrefs where the field is read. @@ -852,7 +946,9 @@ class FieldAnalysis: # Legacy option, might be removed in the future return set(map(itemgetter(slice(0, 2)), self.xrefread)) - def get_xref_write(self, with_offset:bool = False) -> list[tuple[ClassAnalysis, MethodAnalysis]]: + def get_xref_write( + self, with_offset: bool = False + ) -> list[tuple[ClassAnalysis, MethodAnalysis]]: """ Returns a list of xrefs where the field is written to. @@ -879,17 +975,23 @@ class FieldAnalysis: data = "XREFRead for %s\n" % self.field for ref_class, ref_method, off in self.xrefread: data += "in\n" - data += "{}:{} @{}\n".format(ref_class.get_vm_class().get_name(), ref_method, off) + data += "{}:{} @{}\n".format( + ref_class.get_vm_class().get_name(), ref_method, off + ) data += "XREFWrite for %s\n" % self.field for ref_class, ref_method, off in self.xrefwrite: data += "in\n" - data += "{}:{} @{}\n".format(ref_class.get_vm_class().get_name(), ref_method, off) + data += "{}:{} @{}\n".format( + ref_class.get_vm_class().get_name(), ref_method, off + ) return data def __repr__(self): - return "{}>".format(self.field.class_name, self.field.name) + return "{}>".format( + self.field.class_name, self.field.name + ) class ExternalClass: @@ -899,6 +1001,7 @@ class ExternalClass: :param name: Name of the external class """ + def __init__(self, name: str) -> None: self.name = name self.methods = [] @@ -939,6 +1042,7 @@ class ExternalMethod: :param str name: name of the method :param str descriptor: descriptor string """ + def __init__(self, class_name: str, name: str, descriptor: str) -> None: self.class_name = class_name self.name = name @@ -956,12 +1060,24 @@ class ExternalMethod: @property def full_name(self) -> str: """Returns classname + name + descriptor, separated by spaces (no access flags)""" - return self.class_name + " " + self.name + " " + str(self.get_descriptor()) + return ( + self.class_name + + " " + + self.name + + " " + + str(self.get_descriptor()) + ) @property def permission_api_name(self) -> str: """Returns a name which can be used to look up in the permission maps""" - return self.class_name + "-" + self.name + "-" + str(self.get_descriptor()) + return ( + self.class_name + + "-" + + self.name + + "-" + + str(self.get_descriptor()) + ) def get_access_flags_string(self) -> str: """ @@ -977,7 +1093,11 @@ class ExternalMethod: return "" def __str__(self): - return "{}->{}{}".format(self.class_name.__str__(), self.name.__str__(), str(self.get_descriptor())) + return "{}->{}{}".format( + self.class_name.__str__(), + self.name.__str__(), + str(self.get_descriptor()), + ) def __repr__(self): return "".format(self.__str__()) @@ -995,7 +1115,9 @@ class ClassAnalysis: :param classobj: class:`~androguard.core.dex.ClassDefItem` or :class:`ExternalClass` """ - def __init__(self, classobj: Union[dex.ClassDefItem,ExternalClass]) -> None: + def __init__( + self, classobj: Union[dex.ClassDefItem, ExternalClass] + ) -> None: logger.info(f"Adding new ClassAnalysis: {classobj}") # Automatically decide if the class is external or not self.external = isinstance(classobj, ExternalClass) @@ -1084,8 +1206,19 @@ class ClassAnalysis: """ # Packages found at https://developer.android.com/reference/packages.html - api_candidates = ["Landroid/", "Lcom/android/internal/util", "Ldalvik/", "Ljava/", "Ljavax/", "Lorg/apache/", - "Lorg/json/", "Lorg/w3c/dom/", "Lorg/xml/sax", "Lorg/xmlpull/v1/", "Ljunit/"] + api_candidates = [ + "Landroid/", + "Lcom/android/internal/util", + "Ldalvik/", + "Ljava/", + "Ljavax/", + "Lorg/apache/", + "Lorg/json/", + "Lorg/w3c/dom/", + "Lorg/xml/sax", + "Lorg/xmlpull/v1/", + "Ljunit/", + ] if not self.is_external(): # API must be external @@ -1153,7 +1286,13 @@ class ClassAnalysis: # # Propagate ExternalField to ExternalClass # self.orig_class.add_method(field_analysis.get_field()) - def add_field_xref_read(self, method: MethodAnalysis, classobj: ClassAnalysis, field: dex.EncodedField, off: int) -> None: + def add_field_xref_read( + self, + method: MethodAnalysis, + classobj: ClassAnalysis, + field: dex.EncodedField, + off: int, + ) -> None: """ Add a Field Read to this class @@ -1167,7 +1306,13 @@ class ClassAnalysis: self._fields[field] = FieldAnalysis(field) self._fields[field].add_xref_read(classobj, method, off) - def add_field_xref_write(self, method: MethodAnalysis, classobj: ClassAnalysis, field: dex.EncodedField, off: int) -> None: + def add_field_xref_write( + self, + method: MethodAnalysis, + classobj: ClassAnalysis, + field: dex.EncodedField, + off: int, + ) -> None: """ Add a Field Write to this class in a given method @@ -1181,7 +1326,13 @@ class ClassAnalysis: self._fields[field] = FieldAnalysis(field) self._fields[field].add_xref_write(classobj, method, off) - def add_method_xref_to(self, method1: MethodAnalysis, classobj: ClassAnalysis, method2: MethodAnalysis, offset: int) -> None: + def add_method_xref_to( + self, + method1: MethodAnalysis, + classobj: ClassAnalysis, + method2: MethodAnalysis, + offset: int, + ) -> None: """ :param MethodAnalysis method1: the calling method @@ -1195,9 +1346,17 @@ class ClassAnalysis: if method1.get_method() not in self._methods: self.add_method(method1) - self._methods[method1.get_method()].add_xref_to(classobj, method2, offset) + self._methods[method1.get_method()].add_xref_to( + classobj, method2, offset + ) - def add_method_xref_from(self, method1: MethodAnalysis, classobj: ClassAnalysis, method2: MethodAnalysis, offset: int) -> None: + def add_method_xref_from( + self, + method1: MethodAnalysis, + classobj: ClassAnalysis, + method2: MethodAnalysis, + offset: int, + ) -> None: """ :param MethodAnalysis method1: @@ -1210,9 +1369,17 @@ class ClassAnalysis: if method1.get_method() not in self._methods: self.add_method(method1) - self._methods[method1.get_method()].add_xref_from(classobj, method2, offset) + self._methods[method1.get_method()].add_xref_from( + classobj, method2, offset + ) - def add_xref_to(self, ref_kind: REF_TYPE, classobj: ClassAnalysis, methodobj: MethodAnalysis, offset: int) -> None: + def add_xref_to( + self, + ref_kind: REF_TYPE, + classobj: ClassAnalysis, + methodobj: MethodAnalysis, + offset: int, + ) -> None: """ Creates a crossreference to another class. XrefTo means, that the current class calls another class. @@ -1231,7 +1398,13 @@ class ClassAnalysis: """ self.xrefto[classobj].add((ref_kind, methodobj, offset)) - def add_xref_from(self, ref_kind: REF_TYPE, classobj: ClassAnalysis, methodobj: MethodAnalysis, offset: int) -> None: + def add_xref_from( + self, + ref_kind: REF_TYPE, + classobj: ClassAnalysis, + methodobj: MethodAnalysis, + offset: int, + ) -> None: """ Creates a crossreference from this class. XrefFrom means, that the current class is called by another class. @@ -1244,7 +1417,9 @@ class ClassAnalysis: """ self.xreffrom[classobj].add((ref_kind, methodobj, offset)) - def get_xref_from(self) -> dict[ClassAnalysis, tuple[REF_TYPE, MethodAnalysis, int]]: + def get_xref_from( + self, + ) -> dict[ClassAnalysis, tuple[REF_TYPE, MethodAnalysis, int]]: """ Returns a dictionary of all classes calling the current class. This dictionary contains also information from which method the class is accessed. @@ -1270,7 +1445,9 @@ class ClassAnalysis: """ return self.xreffrom - def get_xref_to(self) -> dict[ClassAnalysis, tuple[REF_TYPE, MethodAnalysis, int]]: + def get_xref_to( + self, + ) -> dict[ClassAnalysis, tuple[REF_TYPE, MethodAnalysis, int]]: """ Returns a dictionary of all classes which are called by the current class. This dictionary contains also information about the method which is called. @@ -1295,7 +1472,9 @@ class ClassAnalysis: """ return self.xrefto - def add_xref_new_instance(self, methobj: MethodAnalysis, offset: int) -> None: + def add_xref_new_instance( + self, methobj: MethodAnalysis, offset: int + ) -> None: """ Add a crossreference to another method that is instancing this class. @@ -1317,7 +1496,9 @@ class ClassAnalysis: """ return self.xrefnewinstance - def add_xref_const_class(self, methobj: MethodAnalysis, offset: int) -> None: + def add_xref_const_class( + self, methobj: MethodAnalysis, offset: int + ) -> None: """ Add a crossreference to a method referencing this classtype. @@ -1337,7 +1518,7 @@ class ClassAnalysis: """ return self.xrefconstclass - def get_vm_class(self) -> Union[dex.ClassDefItem,ExternalClass]: + def get_vm_class(self) -> Union[dex.ClassDefItem, ExternalClass]: """ Returns the original Dalvik VM class or the external class object. @@ -1346,7 +1527,9 @@ class ClassAnalysis: """ return self.orig_class - def set_restriction_flag(self, flag: dex.HiddenApiClassDataItem.RestrictionApiFlag) -> None: + def set_restriction_flag( + self, flag: dex.HiddenApiClassDataItem.RestrictionApiFlag + ) -> None: """ Set the level of restriction for this class (hidden level, from Android 10) (only applicable to internal classes) @@ -1357,10 +1540,13 @@ class ClassAnalysis: if self.is_external(): raise RuntimeError( "Can\'t set restriction flag for external class: %s" - % (self.orig_class.name,)) + % (self.orig_class.name,) + ) self.restriction_flag = flag - def set_domain_flag(self, flag: dex.HiddenApiClassDataItem.DomapiApiFlag) -> None: + def set_domain_flag( + self, flag: dex.HiddenApiClassDataItem.DomapiApiFlag + ) -> None: """ Set the api domain for this class (hidden level, from Android 10) (only applicable to internal classes) @@ -1371,15 +1557,18 @@ class ClassAnalysis: if self.is_external(): raise RuntimeError( "Can\'t set domain flag for external class: %s" - % (self.orig_class.name,)) + % (self.orig_class.name,) + ) self.domain_flag = flag # Alias get_class = get_vm_class def __repr__(self): - return "".format(self.orig_class.get_name(), - " EXTERNAL" if isinstance(self.orig_class, ExternalClass) else "") + return "".format( + self.orig_class.get_name(), + " EXTERNAL" if isinstance(self.orig_class, ExternalClass) else "", + ) def __str__(self): # Print only instantiation from other classes here @@ -1428,7 +1617,8 @@ class Analysis: :param Union[androguard.core.dex.DEX, None] vm: inital DEX object (default None) """ - def __init__(self, vm: Union[dex.DEX, None]=None) -> None: + + def __init__(self, vm: Union[dex.DEX, None] = None) -> None: # Contains DEX objects self.vms = [] # A dict of {classname: ClassAnalysis}, populated on add(vm) @@ -1457,7 +1647,7 @@ class Analysis: :param androguard.core.dex.DEX vm: :class:`androguard.core.dex.DEX` to add to this Analysis """ - + self.vms.append(vm) logger.info("Adding DEX file version {}".format(vm.version)) @@ -1466,7 +1656,9 @@ class Analysis: tic = time.time() for i, current_class in enumerate(vm.get_classes()): # seed ClassAnalysis objects into classes attribute and add as new class - self.classes[current_class.get_name()] = ClassAnalysis(current_class) + self.classes[current_class.get_name()] = ClassAnalysis( + current_class + ) new_class = self.classes[current_class.get_name()] # Fix up the hidden api annotations (Android 10) @@ -1482,7 +1674,11 @@ class Analysis: new_class.add_method(self.methods[method]) # Store for faster lookup during create_xrefs - m_hash = (current_class.get_name(), method.get_name(), str(method.get_descriptor())) + m_hash = ( + current_class.get_name(), + method.get_name(), + str(method.get_descriptor()), + ) self.__method_hashes[m_hash] = self.methods[method] # seed FieldAnalysis objects into to new class analysis @@ -1496,7 +1692,11 @@ class Analysis: for string_value in vm.get_strings(): self.strings[string_value] = StringAnalysis(string_value) - logger.info("Added DEX in the analysis took : {:0d}min {:02d}s".format(*divmod(int(time.time() - tic), 60))) + logger.info( + "Added DEX in the analysis took : {:0d}min {:02d}s".format( + *divmod(int(time.time() - tic), 60) + ) + ) def create_xref(self) -> None: """ @@ -1511,9 +1711,11 @@ class Analysis: if self.__created_xrefs: # TODO on concurrent runs, we probably need to clean up first, # or check that we do not write garbage. - logger.error("You have requested to run create_xref() twice! " - "This will not work and cause problems! This function will exit right now. " - "If you want to add multiple DEX files, use add() several times and then run create_xref() once.") + logger.error( + "You have requested to run create_xref() twice! " + "This will not work and cause problems! This function will exit right now. " + "If you want to add multiple DEX files, use add() several times and then run create_xref() once." + ) return self.__created_xrefs = True @@ -1530,8 +1732,12 @@ class Analysis: # TODO: After we collected all the information, we should add field and # string xrefs to each MethodAnalysis - logger.info("End of creating cross references (XREF) " - "run time: {:0d}min {:02d}s".format(*divmod(int(time.time() - tic), 60))) + logger.info( + "End of creating cross references (XREF) " + "run time: {:0d}min {:02d}s".format( + *divmod(int(time.time() - tic), 60) + ) + ) def _create_xref(self, current_class: dex.ClassDefItem) -> None: """ @@ -1551,9 +1757,17 @@ class Analysis: """ cur_cls_name = current_class.get_name() - logger.debug("Creating XREF/DREF for class at @0x{:08x}".format(current_class.get_class_data_off())) + logger.debug( + "Creating XREF/DREF for class at @0x{:08x}".format( + current_class.get_class_data_off() + ) + ) for current_method in current_class.get_methods(): - logger.debug("Creating XREF for method at @0x{:08x}".format(current_method.get_code_off())) + logger.debug( + "Creating XREF for method at @0x{:08x}".format( + current_method.get_code_off() + ) + ) cur_meth = self.get_method(current_method) cur_cls = self.classes[cur_cls_name] @@ -1562,10 +1776,12 @@ class Analysis: op_value = instruction.get_op_value() # 1) check for class calls: const-class (0x1c), new-instance (0x22) - if op_value in [0x1c, 0x22]: + if op_value in [0x1C, 0x22]: idx_type = instruction.get_ref_kind() # type_info is the string like 'Ljava/lang/Object;' - type_info = instruction.cm.vm.get_cm_type(idx_type).lstrip('[') + type_info = instruction.cm.vm.get_cm_type(idx_type).lstrip( + '[' + ) if type_info[0] != 'L': # Need to make sure, that we get class types and not other types continue @@ -1576,7 +1792,9 @@ class Analysis: if type_info not in self.classes: # Create new external class - self.classes[type_info] = ClassAnalysis(ExternalClass(type_info)) + self.classes[type_info] = ClassAnalysis( + ExternalClass(type_info) + ) oth_cls = self.classes[type_info] @@ -1586,10 +1804,14 @@ class Analysis: # as there is no called method! # With the _new_instance and _const_class can this be deprecated? # Removing these does not impact tests - cur_cls.add_xref_to(REF_TYPE(op_value), oth_cls, cur_meth, off) - oth_cls.add_xref_from(REF_TYPE(op_value), cur_cls, cur_meth, off) + cur_cls.add_xref_to( + REF_TYPE(op_value), oth_cls, cur_meth, off + ) + oth_cls.add_xref_from( + REF_TYPE(op_value), cur_cls, cur_meth, off + ) - if op_value == 0x1c: + if op_value == 0x1C: cur_meth.add_xref_const_class(oth_cls, off) oth_cls.add_xref_const_class(cur_meth, off) if op_value == 0x22: @@ -1597,13 +1819,17 @@ class Analysis: oth_cls.add_xref_new_instance(cur_meth, off) # 2) check for method calls: invoke-* (0x6e ... 0x72), invoke-xxx/range (0x74 ... 0x78) - elif (0x6e <= op_value <= 0x72) or (0x74 <= op_value <= 0x78): + elif (0x6E <= op_value <= 0x72) or (0x74 <= op_value <= 0x78): idx_meth = instruction.get_ref_kind() method_info = instruction.cm.vm.get_cm_method(idx_meth) if not method_info: - logger.warning("Could not get method_info " - "for instruction at {} in method at @{}. " - "Requested IDX {}".format(off, current_method.get_code_off(), idx_meth)) + logger.warning( + "Could not get method_info " + "for instruction at {} in method at @{}. " + "Requested IDX {}".format( + off, current_method.get_code_off(), idx_meth + ) + ) continue class_info = method_info[0].lstrip('[') @@ -1613,46 +1839,74 @@ class Analysis: continue # Resolve the second MethodAnalysis - oth_meth = self._resolve_method(class_info, method_info[1], method_info[2]) + oth_meth = self._resolve_method( + class_info, method_info[1], method_info[2] + ) oth_cls = self.classes[class_info] # FIXME: we could merge add_method_xref_* and add_xref_* - cur_cls.add_method_xref_to(cur_meth, oth_cls, oth_meth, off) - oth_cls.add_method_xref_from(oth_meth, cur_cls, cur_meth, off) + cur_cls.add_method_xref_to( + cur_meth, oth_cls, oth_meth, off + ) + oth_cls.add_method_xref_from( + oth_meth, cur_cls, cur_meth, off + ) # Internal xref related to class manipulation - cur_cls.add_xref_to(REF_TYPE(op_value), oth_cls, oth_meth, off) - oth_cls.add_xref_from(REF_TYPE(op_value), cur_cls, cur_meth, off) + cur_cls.add_xref_to( + REF_TYPE(op_value), oth_cls, oth_meth, off + ) + oth_cls.add_xref_from( + REF_TYPE(op_value), cur_cls, cur_meth, off + ) # 3) check for string usage: const-string (0x1a), const-string/jumbo (0x1b) - elif 0x1a <= op_value <= 0x1b: - string_value = instruction.cm.vm.get_cm_string(instruction.get_ref_kind()) + elif 0x1A <= op_value <= 0x1B: + string_value = instruction.cm.vm.get_cm_string( + instruction.get_ref_kind() + ) if string_value not in self.strings: - self.strings[string_value] = StringAnalysis(string_value) + self.strings[string_value] = StringAnalysis( + string_value + ) - self.strings[string_value].add_xref_from(cur_cls, cur_meth, off) + self.strings[string_value].add_xref_from( + cur_cls, cur_meth, off + ) # TODO maybe we should add a step 3a) here and check for all const fields. You can then xref for integers etc! # But: This does not work, as const fields are usually optimized internally to const calls... # 4) check for field usage: i*op (0x52 ... 0x5f), s*op (0x60 ... 0x6d) - elif 0x52 <= op_value <= 0x6d: + elif 0x52 <= op_value <= 0x6D: idx_field = instruction.get_ref_kind() field_info = instruction.cm.vm.get_cm_field(idx_field) - field_item = instruction.cm.vm.get_encoded_field_descriptor(field_info[0], field_info[2], field_info[1]) + field_item = ( + instruction.cm.vm.get_encoded_field_descriptor( + field_info[0], field_info[2], field_info[1] + ) + ) if not field_item: continue - if (0x52 <= op_value <= 0x58) or (0x60 <= op_value <= 0x66): + if (0x52 <= op_value <= 0x58) or ( + 0x60 <= op_value <= 0x66 + ): # read access to a field - self.classes[cur_cls_name].add_field_xref_read(cur_meth, cur_cls, field_item, off) + self.classes[cur_cls_name].add_field_xref_read( + cur_meth, cur_cls, field_item, off + ) cur_meth.add_xref_read(cur_cls, field_item, off) else: # write access to a field - self.classes[cur_cls_name].add_field_xref_write(cur_meth, cur_cls, field_item, off) + self.classes[cur_cls_name].add_field_xref_write( + cur_meth, cur_cls, field_item, off + ) cur_meth.add_xref_write(cur_cls, field_item, off) - def get_method(self, method: dex.EncodedMethod) -> Union[MethodAnalysis,None]: + def get_method( + self, method: dex.EncodedMethod + ) -> Union[MethodAnalysis, None]: """ Get the :class:`MethodAnalysis` object for a given :class:`EncodedMethod`. This Analysis object is used to enhance EncodedMethods. @@ -1668,7 +1922,9 @@ class Analysis: # Alias get_method_analysis = get_method - def _resolve_method(self, class_name: str, method_name: str, method_descriptor: list[str]) -> MethodAnalysis: + def _resolve_method( + self, class_name: str, method_name: str, method_descriptor: list[str] + ) -> MethodAnalysis: """ Resolves the Method and returns MethodAnalysis. Will automatically create ExternalMethods if can not resolve and add to the ClassAnalysis etc @@ -1684,10 +1940,14 @@ class Analysis: # Need to create a new method if class_name not in self.classes: # External class? no problem! - self.classes[class_name] = ClassAnalysis(ExternalClass(class_name)) + self.classes[class_name] = ClassAnalysis( + ExternalClass(class_name) + ) # Create external method - meth = ExternalMethod(class_name, method_name, ''.join(method_descriptor)) + meth = ExternalMethod( + class_name, method_name, ''.join(method_descriptor) + ) meth_analysis = MethodAnalysis(None, meth) # add to all the collections we have @@ -1697,7 +1957,9 @@ class Analysis: return self.__method_hashes[m_hash] - def get_method_by_name(self, class_name: str, method_name: str, method_descriptor: str) -> Union[dex.EncodedMethod,None]: + def get_method_by_name( + self, class_name: str, method_name: str, method_descriptor: str + ) -> Union[dex.EncodedMethod, None]: """ Search for a :class:`EncodedMethod` in all classes in this analysis @@ -1707,12 +1969,16 @@ class Analysis: :return: :class:`EncodedMethod` or None if method was not found :rtype: androguard.core.dex.EncodedMethod """ - m_a = self.get_method_analysis_by_name(class_name, method_name, method_descriptor) + m_a = self.get_method_analysis_by_name( + class_name, method_name, method_descriptor + ) if m_a and not m_a.is_external(): return m_a.get_method() return None - def get_method_analysis_by_name(self, class_name: str, method_name: str, method_descriptor: str) -> Union[MethodAnalysis,None]: + def get_method_analysis_by_name( + self, class_name: str, method_name: str, method_descriptor: str + ) -> Union[MethodAnalysis, None]: """ Returns the crossreferencing object for a given method. @@ -1730,7 +1996,9 @@ class Analysis: return None return self.__method_hashes[m_hash] - def get_field_analysis(self, field: dex.EncodedField) -> Union[FieldAnalysis,None]: + def get_field_analysis( + self, field: dex.EncodedField + ) -> Union[FieldAnalysis, None]: """ Get the FieldAnalysis for a given fieldname @@ -1807,7 +2075,7 @@ class Analysis: if m.is_external(): yield m - def get_strings_analysis(self) -> dict[str,StringAnalysis]: + def get_strings_analysis(self) -> dict[str, StringAnalysis]: """ Returns a dictionary of strings and their corresponding :class:`StringAnalysis` @@ -1852,7 +2120,9 @@ class Analysis: for f in c.get_fields(): yield f - def find_classes(self, name:str = ".*", no_external:bool = False) -> Iterator[ClassAnalysis]: + def find_classes( + self, name: str = ".*", no_external: bool = False + ) -> Iterator[ClassAnalysis]: """ Find classes by name, using regular expression This method will return all ClassAnalysis Object that match the name of @@ -1870,11 +2140,12 @@ class Analysis: def find_methods( self, - classname:str=".*", - methodname:str=".*", - descriptor:str=".*", - accessflags:str=".*", - no_external:bool=False) -> Iterator[MethodAnalysis]: + classname: str = ".*", + methodname: str = ".*", + descriptor: str = ".*", + accessflags: str = ".*", + no_external: bool = False, + ) -> Iterator[MethodAnalysis]: """ Find a method by name using regular expression. This method will return all MethodAnalysis objects, which match the @@ -1900,12 +2171,14 @@ class Analysis: # external calls if no_external and isinstance(z, ExternalMethod): continue - if re.match(methodname, z.get_name()) and \ - re.match(descriptor, z.get_descriptor()) and \ - re.match(accessflags, z.get_access_flags_string()): + if ( + re.match(methodname, z.get_name()) + and re.match(descriptor, z.get_descriptor()) + and re.match(accessflags, z.get_access_flags_string()) + ): yield m - def find_strings(self, string:str=".*") -> Iterator[StringAnalysis]: + def find_strings(self, string: str = ".*") -> Iterator[StringAnalysis]: """ Find strings by regex @@ -1918,10 +2191,11 @@ class Analysis: def find_fields( self, - classname:str=".*", - fieldname:str=".*", - fieldtype:str=".*", - accessflags:str=".*") -> Iterator[FieldAnalysis]: + classname: str = ".*", + fieldname: str = ".*", + fieldtype: str = ".*", + accessflags: str = ".*", + ) -> Iterator[FieldAnalysis]: """ find fields by regex @@ -1935,22 +2209,30 @@ class Analysis: if re.match(classname, cname): for f in c.get_fields(): z = f.get_field() - if re.match(fieldname, z.get_name()) and \ - re.match(fieldtype, z.get_descriptor()) and \ - re.match(accessflags, z.get_access_flags_string()): + if ( + re.match(fieldname, z.get_name()) + and re.match(fieldtype, z.get_descriptor()) + and re.match(accessflags, z.get_access_flags_string()) + ): yield f def __repr__(self): - return "".format(len(self.vms), len(self.classes), len(self.methods), len(self.strings)) + return "".format( + len(self.vms), + len(self.classes), + len(self.methods), + len(self.strings), + ) def get_call_graph( self, - classname:str=".*", - methodname:str=".*", - descriptor:str=".*", - accessflags:str=".*", - no_isolated:bool=False, - entry_points:list=[]) -> nx.DiGraph: + classname: str = ".*", + methodname: str = ".*", + descriptor: str = ".*", + accessflags: str = ".*", + no_isolated: bool = False, + entry_points: list = [], + ) -> nx.DiGraph: """ Generate a directed graph based on the methods found by the filters applied. The filters are the same as in @@ -1992,8 +2274,8 @@ class Analysis: methodname=method.get_name(), descriptor=method.get_descriptor(), accessflags=method.get_access_flags_string(), - classname=method.get_class_name() - ) + classname=method.get_class_name(), + ) CG = nx.DiGraph() @@ -2003,13 +2285,16 @@ class Analysis: classname=classname, methodname=methodname, descriptor=descriptor, - accessflags=accessflags): - + accessflags=accessflags, + ): + orig_method = m.get_method() logger.info("Found Method --> {}".format(orig_method)) if no_isolated and len(m.get_xref_to()) == 0: - logger.info("Skipped {}, because if has no xrefs".format(orig_method)) + logger.info( + "Skipped {}, because if has no xrefs".format(orig_method) + ) continue _add_node(CG, orig_method, entry_points) @@ -2055,9 +2340,20 @@ class Analysis: _, method_name = bytecode.get_package_class_name(cls.name) # FIXME this naming schema is not very good... but to describe a method uniquely, we need all of it - mname = "METH_" + method_name + "_" + bytecode.FormatDescriptorToPython(meth.access) + "_" + bytecode.FormatDescriptorToPython(meth.descriptor) + mname = ( + "METH_" + + method_name + + "_" + + bytecode.FormatDescriptorToPython(meth.access) + + "_" + + bytecode.FormatDescriptorToPython(meth.descriptor) + ) if hasattr(cls, mname): - logger.warning("already existing method: {} at class {}".format(mname, name)) + logger.warning( + "already existing method: {} at class {}".format( + mname, name + ) + ) setattr(cls, mname, meth) # FIXME: syntetic classes produce problems here. @@ -2065,10 +2361,16 @@ class Analysis: for field in cls.get_fields(): mname = "FIELD_" + bytecode.FormatNameToPython(field.name) if hasattr(cls, mname): - logger.warning("already existing field: {} at class {}".format(mname, name)) + logger.warning( + "already existing field: {} at class {}".format( + mname, name + ) + ) setattr(cls, mname, field) - def get_permissions(self, apilevel:Union[str,int,None]=None) -> Iterator[MethodAnalysis, list[str]]: + def get_permissions( + self, apilevel: Union[str, int, None] = None + ) -> Iterator[MethodAnalysis, list[str]]: """ Returns the permissions and the API method based on the API level specified. This can be used to find usage of API methods which require a permission. @@ -2102,10 +2404,14 @@ class Analysis: """ # TODO maybe have the API level loading in the __init__ method and pass the APK as well? - permmap = load_api_specific_resource_module('api_permission_mappings', apilevel) + permmap = load_api_specific_resource_module( + 'api_permission_mappings', apilevel + ) if not permmap: - raise ValueError("No permission mapping found! Is one available? " - "The requested API level was '{}'".format(apilevel)) + raise ValueError( + "No permission mapping found! Is one available? " + "The requested API level was '{}'".format(apilevel) + ) for cls in self.get_external_classes(): for meth_analysis in cls.get_methods(): @@ -2113,7 +2419,9 @@ class Analysis: if meth.permission_api_name in permmap: yield meth_analysis, permmap[meth.permission_api_name] - def get_permission_usage(self, permission:str, apilevel:Union[str,int,None]=None) -> Iterator[MethodAnalysis]: + def get_permission_usage( + self, permission: str, apilevel: Union[str, int, None] = None + ) -> Iterator[MethodAnalysis]: """ Find the usage of a permission inside the Analysis. @@ -2136,15 +2444,23 @@ class Analysis: """ # TODO maybe have the API level loading in the __init__ method and pass the APK as well? - permmap = load_api_specific_resource_module('api_permission_mappings', apilevel) + permmap = load_api_specific_resource_module( + 'api_permission_mappings', apilevel + ) if not permmap: - raise ValueError("No permission mapping found! Is one available? " - "The requested API level was '{}'".format(apilevel)) + raise ValueError( + "No permission mapping found! Is one available? " + "The requested API level was '{}'".format(apilevel) + ) apis = {k for k, v in permmap.items() if permission in v} if not apis: - raise ValueError("No API methods could be found which use the permission. " - "Does the permission exists? You requested: '{}'".format(permission)) + raise ValueError( + "No API methods could be found which use the permission. " + "Does the permission exists? You requested: '{}'".format( + permission + ) + ) for cls in self.get_external_classes(): for meth_analysis in cls.get_methods(): diff --git a/androguard/core/androconf.py b/androguard/core/androconf.py index 9c8f2eed..dbb4e9d3 100644 --- a/androguard/core/androconf.py +++ b/androguard/core/androconf.py @@ -3,17 +3,20 @@ # see https://peps.python.org/pep-0563/ from __future__ import annotations - import os import sys import tempfile from typing import Union from androguard import __version__ -from androguard.core.api_specific_resources import load_permission_mappings, load_permissions +from androguard.core.api_specific_resources import ( + load_permission_mappings, + load_permissions, +) + ANDROGUARD_VERSION = __version__ -from colorama import init, Fore +from colorama import Fore, init from loguru import logger # initialize colorama, only has an effect on windows @@ -24,6 +27,7 @@ class InvalidResourceError(Exception): """ Invalid Resource Erorr is thrown by load_api_specific_resource_module """ + pass @@ -46,21 +50,16 @@ def is_ascii_problem(s: str) -> bool: default_conf = { ## Configuration for executables used by androguard # Assume the binary is in $PATH, otherwise give full path - # Runtime variables # # A path to the temporary directory "TMP_DIRECTORY": tempfile.gettempdir(), - # Function to print stuff "PRINT_FCT": sys.stdout.write, - # Default API level, if requested API is not available "DEFAULT_API": 16, # this is the minimal API version we have - # Session, for persistence "SESSION": None, - # Color output configuration "COLORS": { "OFFSET": Fore.YELLOW, @@ -149,7 +148,7 @@ def is_android_raw(raw: bytes) -> str: # probably it would be better to rewrite this and add more sanity checks. if raw[0:2] == b"PK" and b'AndroidManifest.xml' in raw: val = "APK" - # check out + # check out elif raw[0:3] == b"dex": val = "DEX" elif raw[0:3] == b"dey": @@ -161,6 +160,7 @@ def is_android_raw(raw: bytes) -> str: return val + def rrmdir(directory: str) -> None: """ Recursivly delete a directory @@ -175,7 +175,7 @@ def rrmdir(directory: str) -> None: os.rmdir(directory) -def make_color_tuple(color: str) -> tuple[int,int,int]: +def make_color_tuple(color: str) -> tuple[int, int, int]: """ turn something like "#000000" into 0,0,0 or "#FFFFFF into "255,255,255" @@ -191,7 +191,11 @@ def make_color_tuple(color: str) -> tuple[int,int,int]: return R, G, B -def interpolate_tuple(startcolor: tuple[int,int,int], goalcolor: tuple[int,int,int], steps: int) -> list[str]: +def interpolate_tuple( + startcolor: tuple[int, int, int], + goalcolor: tuple[int, int, int], + steps: int, +) -> list[str]: """ Take two RGB color sets and mix them over a specified number of steps. Return the list """ @@ -234,7 +238,11 @@ def interpolate_tuple(startcolor: tuple[int,int,int], goalcolor: tuple[int,int,i return buffer -def color_range(startcolor: tuple[int,int,int], goalcolor: tuple[int,int,int], steps: int) -> list[str]: +def color_range( + startcolor: tuple[int, int, int], + goalcolor: tuple[int, int, int], + steps: int, +) -> list[str]: """ wrapper for interpolate_tuple that accepts colors as html ("#CCCCC" and such) """ @@ -244,7 +252,9 @@ def color_range(startcolor: tuple[int,int,int], goalcolor: tuple[int,int,int], s return interpolate_tuple(start_tuple, goal_tuple, steps) -def load_api_specific_resource_module(resource_name: str, api:Union[str,int,None]=None) -> dict: +def load_api_specific_resource_module( + resource_name: str, api: Union[str, int, None] = None +) -> dict: """ Load the module from the JSON files and return a dict, which might be empty if the resource could not be loaded. @@ -255,11 +265,17 @@ def load_api_specific_resource_module(resource_name: str, api:Union[str,int,None :param api: API version :return: dict """ - loader = dict(aosp_permissions=load_permissions, - api_permission_mappings=load_permission_mappings) + loader = dict( + aosp_permissions=load_permissions, + api_permission_mappings=load_permission_mappings, + ) if resource_name not in loader: - raise InvalidResourceError("Invalid Resource '{}', not in [{}]".format(resource_name, ", ".join(loader.keys()))) + raise InvalidResourceError( + "Invalid Resource '{}', not in [{}]".format( + resource_name, ", ".join(loader.keys()) + ) + ) if not api: api = CONF["DEFAULT_API"] @@ -268,9 +284,12 @@ def load_api_specific_resource_module(resource_name: str, api:Union[str,int,None if ret == {}: # No API mapping found, return default - logger.warning("API mapping for API level {} was not found! " - "Returning default, which is API level {}".format(api, CONF['DEFAULT_API'])) + logger.warning( + "API mapping for API level {} was not found! " + "Returning default, which is API level {}".format( + api, CONF['DEFAULT_API'] + ) + ) ret = loader[resource_name](CONF['DEFAULT_API']) return ret - diff --git a/androguard/core/api_specific_resources/__init__.py b/androguard/core/api_specific_resources/__init__.py index 8daa76cb..c74a617c 100644 --- a/androguard/core/api_specific_resources/__init__.py +++ b/androguard/core/api_specific_resources/__init__.py @@ -1,4 +1,3 @@ - import json import os import re @@ -10,7 +9,10 @@ from loguru import logger class APILevelNotFoundError(Exception): pass -def load_permissions(apilevel:Union[str,int], permtype:str='permissions') -> dict[str, dict[str,str]]: + +def load_permissions( + apilevel: Union[str, int], permtype: str = 'permissions' +) -> dict[str, dict[str, str]]: """ Load the Permissions for the given apilevel. @@ -37,35 +39,56 @@ def load_permissions(apilevel:Union[str,int], permtype:str='permissions') -> dic apilevel = int(apilevel) root = os.path.dirname(os.path.realpath(__file__)) - permissions_file = os.path.join(root, "aosp_permissions", "permissions_{}.json".format(apilevel)) + permissions_file = os.path.join( + root, "aosp_permissions", "permissions_{}.json".format(apilevel) + ) - levels = filter(lambda x: re.match(r'^permissions_\d+\.json$', x), os.listdir(os.path.join(root, "aosp_permissions"))) + levels = filter( + lambda x: re.match(r'^permissions_\d+\.json$', x), + os.listdir(os.path.join(root, "aosp_permissions")), + ) levels = list(map(lambda x: int(x[:-5].split('_')[1]), levels)) if not levels: logger.error("No Permissions available, can not load!") return {} - logger.debug("Available API levels: {}".format(", ".join(map(str, sorted(levels))))) + logger.debug( + "Available API levels: {}".format(", ".join(map(str, sorted(levels)))) + ) if not os.path.isfile(permissions_file): if apilevel > max(levels): - logger.warning("Requested API level {} is larger than maximum we have, returning API level {} instead.".format(apilevel, max(levels))) + logger.warning( + "Requested API level {} is larger than maximum we have, returning API level {} instead.".format( + apilevel, max(levels) + ) + ) return load_permissions(max(levels), permtype) if apilevel < min(levels): - logger.warning("Requested API level {} is smaller than minimal we have, returning API level {} instead.".format(apilevel, max(levels))) + logger.warning( + "Requested API level {} is smaller than minimal we have, returning API level {} instead.".format( + apilevel, max(levels) + ) + ) return load_permissions(min(levels), permtype) # Missing level between existing ones, return the lower level lower_level = max(filter(lambda x: x < apilevel, levels)) - logger.warning("Requested API Level could not be found, using {} instead".format(lower_level)) + logger.warning( + "Requested API Level could not be found, using {} instead".format( + lower_level + ) + ) return load_permissions(lower_level, permtype) with open(permissions_file, "r") as fp: return json.load(fp)[permtype] -def load_permission_mappings(apilevel:Union[str,int]) -> dict[str, list[str]]: +def load_permission_mappings( + apilevel: Union[str, int] +) -> dict[str, list[str]]: """ Load the API/Permission mapping for the requested API level. If the requetsed level was not found, None is returned. @@ -74,7 +97,9 @@ def load_permission_mappings(apilevel:Union[str,int]) -> dict[str, list[str]]: :return: a dictionary of {MethodSignature: [List of Permissions]} """ root = os.path.dirname(os.path.realpath(__file__)) - permissions_file = os.path.join(root, "api_permission_mappings", "permissions_{}.json".format(apilevel)) + permissions_file = os.path.join( + root, "api_permission_mappings", "permissions_{}.json".format(apilevel) + ) if not os.path.isfile(permissions_file): return {} diff --git a/androguard/core/apk/__init__.py b/androguard/core/apk/__init__.py index 2c5f30b2..a5f4cb67 100644 --- a/androguard/core/apk/__init__.py +++ b/androguard/core/apk/__init__.py @@ -3,40 +3,42 @@ # see https://peps.python.org/pep-0563/ from __future__ import annotations -# Python core +# Python core import binascii import hashlib import io import os import re +import zipfile from struct import unpack from typing import Iterator, Union -import zipfile +from xml.dom.pulldom import SAX2DOM from zlib import crc32 -# Androguard -from androguard.core import androconf -from androguard.util import get_certificate_name_string +import lxml.sax from apkInspector.headers import ZipEntry -from androguard.core.axml import (ARSCParser, - AXMLPrinter, +# Used for reading Certificates +from asn1crypto import cms, keys, x509 +from loguru import logger + +# External dependencies +from lxml.etree import Element + +# Androguard +from androguard.core import androconf +from androguard.core.axml import ( + END_DOCUMENT, + END_TAG, + START_TAG, + TEXT, + ARSCParser, ARSCResTableConfig, AXMLParser, + AXMLPrinter, format_value, - START_TAG, - END_TAG, - TEXT, - END_DOCUMENT) - -# External dependencies -from lxml.etree import Element -import lxml.sax -from xml.dom.pulldom import SAX2DOM -# Used for reading Certificates -from asn1crypto import cms, x509, keys - -from loguru import logger +) +from androguard.util import get_certificate_name_string NS_ANDROID_URI = 'http://schemas.android.com/apk/res/android' NS_ANDROID = '{{{}}}'.format(NS_ANDROID_URI) # Namespace as used by etree @@ -72,9 +74,10 @@ protection_flags_to_attributes = { "0x01000000": "retail demo", "0x02000000": "recents", "0x04000000": "role", - "0x08000000": "known signer" + "0x08000000": "known signer", } + def parse_lxml_dom(tree): handler = SAX2DOM() lxml.sax.saxify(tree, handler) @@ -83,6 +86,7 @@ def parse_lxml_dom(tree): class Error(Exception): """Base class for exceptions in this module.""" + pass @@ -95,7 +99,7 @@ class BrokenAPKError(Error): def _dump_additional_attributes(additional_attributes): - """ try to parse additional attributes, but ends up to hexdump if the scheme is unknown """ + """try to parse additional attributes, but ends up to hexdump if the scheme is unknown""" attributes_raw = io.BytesIO(additional_attributes) attributes_hex = binascii.hexlify(additional_attributes) @@ -103,15 +107,15 @@ def _dump_additional_attributes(additional_attributes): if not len(additional_attributes): return attributes_hex - len_attribute, = unpack(' None: self._bytes = None self.digests = None - self.certificates = None + self.certificates = None self.additional_attributes = None def __str__(self): certs_infos = "" - for i,cert in enumerate(self.certificates): + for i, cert in enumerate(self.certificates): x509_cert = x509.Certificate.load(cert) certs_infos += "\n" certs_infos += " [%d]\n" % i - certs_infos += " - Issuer: %s\n" % get_certificate_name_string(x509_cert.issuer, short=True) - certs_infos += " - Subject: %s\n" % get_certificate_name_string(x509_cert.subject, short=True) - certs_infos += " - Serial Number: %s\n" % hex(x509_cert.serial_number) + certs_infos += " - Issuer: %s\n" % get_certificate_name_string( + x509_cert.issuer, short=True + ) + certs_infos += " - Subject: %s\n" % get_certificate_name_string( + x509_cert.subject, short=True + ) + certs_infos += " - Serial Number: %s\n" % hex( + x509_cert.serial_number + ) certs_infos += " - Hash Algorithm: %s\n" % x509_cert.hash_algo - certs_infos += " - Signature Algorithm: %s\n" % x509_cert.signature_algo - certs_infos += " - Valid not before: %s\n" % x509_cert['tbs_certificate']['validity']['not_before'].native - certs_infos += " - Valid not after: %s" % x509_cert['tbs_certificate']['validity']['not_after'].native + certs_infos += ( + " - Signature Algorithm: %s\n" % x509_cert.signature_algo + ) + certs_infos += ( + " - Valid not before: %s\n" + % x509_cert['tbs_certificate']['validity']['not_before'].native + ) + certs_infos += ( + " - Valid not after: %s" + % x509_cert['tbs_certificate']['validity']['not_after'].native + ) - return "\n".join([ - 'additional_attributes : {}'.format(_dump_additional_attributes(self.additional_attributes)), - 'digests : {}'.format(_dump_digests_or_signatures(self.digests)), - 'certificates : {}'.format(certs_infos), - ]) + return "\n".join( + [ + 'additional_attributes : {}'.format( + _dump_additional_attributes(self.additional_attributes) + ), + 'digests : {}'.format( + _dump_digests_or_signatures(self.digests) + ), + 'certificates : {}'.format(certs_infos), + ] + ) class APKV3SignedData(APKV2SignedData): @@ -182,14 +208,16 @@ class APKV3SignedData(APKV2SignedData): # maxSDK is set to a negative value if there is no upper bound on the sdk targeted max_sdk_str = "%d" % self.maxSDK - if self.maxSDK >= 0x7fffffff: + if self.maxSDK >= 0x7FFFFFFF: max_sdk_str = "0x%x" % self.maxSDK - return "\n".join([ - 'signer minSDK : {:d}'.format(self.minSDK), - 'signer maxSDK : {:s}'.format(max_sdk_str), - base_str - ]) + return "\n".join( + [ + 'signer minSDK : {:d}'.format(self.minSDK), + 'signer maxSDK : {:s}'.format(max_sdk_str), + base_str, + ] + ) class APKV2Signer: @@ -205,11 +233,15 @@ class APKV2Signer: self.public_key = None def __str__(self): - return "\n".join([ - '{:s}'.format(str(self.signed_data)), - 'signatures : {}'.format(_dump_digests_or_signatures(self.signatures)), - 'public key : {}'.format(binascii.hexlify(self.public_key)), - ]) + return "\n".join( + [ + '{:s}'.format(str(self.signed_data)), + 'signatures : {}'.format( + _dump_digests_or_signatures(self.signatures) + ), + 'public key : {}'.format(binascii.hexlify(self.public_key)), + ] + ) class APKV3Signer(APKV2Signer): @@ -229,14 +261,16 @@ class APKV3Signer(APKV2Signer): # maxSDK is set to a negative value if there is no upper bound on the sdk targeted max_sdk_str = "%d" % self.maxSDK - if self.maxSDK >= 0x7fffffff: + if self.maxSDK >= 0x7FFFFFFF: max_sdk_str = "0x%x" % self.maxSDK - return "\n".join([ - 'signer minSDK : {:d}'.format(self.minSDK), - 'signer maxSDK : {:s}'.format(max_sdk_str), - base_str - ]) + return "\n".join( + [ + 'signer minSDK : {:d}'.format(self.minSDK), + 'signer maxSDK : {:s}'.format(max_sdk_str), + base_str, + ] + ) class APK: @@ -246,23 +280,30 @@ class APK: # Constants in the APK Signature Block _APK_SIG_MAGIC = b"APK Sig Block 42" - _APK_SIG_KEY_V2_SIGNATURE = 0x7109871a - _APK_SIG_KEY_V3_SIGNATURE = 0xf05368c0 - _APK_SIG_ATTR_V2_STRIPPING_PROTECTION = 0xbeeff00d + _APK_SIG_KEY_V2_SIGNATURE = 0x7109871A + _APK_SIG_KEY_V3_SIGNATURE = 0xF05368C0 + _APK_SIG_ATTR_V2_STRIPPING_PROTECTION = 0xBEEFF00D _APK_SIG_ALGO_IDS = { - 0x0101 : "RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc", - 0x0102 : "RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc", - 0x0103 : "RSASSA-PKCS1-v1_5 with SHA2-256 digest.", # This is for build systems which require deterministic signatures. - 0x0104 : "RSASSA-PKCS1-v1_5 with SHA2-512 digest.", # This is for build systems which require deterministic signatures. - 0x0201 : "ECDSA with SHA2-256 digest", - 0x0202 : "ECDSA with SHA2-512 digest", - 0x0301 : "DSA with SHA2-256 digest", + 0x0101: "RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc", + 0x0102: "RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc", + 0x0103: "RSASSA-PKCS1-v1_5 with SHA2-256 digest.", # This is for build systems which require deterministic signatures. + 0x0104: "RSASSA-PKCS1-v1_5 with SHA2-512 digest.", # This is for build systems which require deterministic signatures. + 0x0201: "ECDSA with SHA2-256 digest", + 0x0202: "ECDSA with SHA2-512 digest", + 0x0301: "DSA with SHA2-256 digest", } __no_magic = False - def __init__(self, filename:str, raw:bool=False, magic_file:Union[str,None]=None, skip_analysis:bool=False, testzip:bool=False) -> None: + def __init__( + self, + filename: str, + raw: bool = False, + magic_file: Union[str, None] = None, + skip_analysis: bool = False, + testzip: bool = False, + ) -> None: """ This class can access to all elements in an APK file @@ -285,7 +326,9 @@ class APK: """ if magic_file: - logger.warning("You set magic_file but this parameter is actually unused. You should remove it.") + logger.warning( + "You set magic_file but this parameter is actually unused. You should remove it." + ) self.filename = filename @@ -320,7 +363,9 @@ class APK: self.__raw = self.zip.zip.getvalue() if testzip: - logger.info("Testing zip file integrity, this might take a while...") + logger.info( + "Testing zip file integrity, this might take a while..." + ) # Test the zipfile for integrity before continuing. # This process might be slow, as the whole file is read. # Therefore it is possible to enable it as a separate feature. @@ -334,7 +379,9 @@ class APK: # we could print the filename here, but there are zip which are so broken # That the filename is either very very long or does not make any sense. # Thus we do not do it, the user might find out by using other tools. - raise BrokenAPKError("The APK is probably broken: testzip returned an error.") + raise BrokenAPKError( + "The APK is probably broken: testzip returned an error." + ) if not skip_analysis: self._apk_analysis() @@ -364,44 +411,73 @@ class APK: ap = AXMLPrinter(manifest_data) if not ap.is_valid(): - logger.error("Error while parsing AndroidManifest.xml - is the file valid?") + logger.error( + "Error while parsing AndroidManifest.xml - is the file valid?" + ) return self.axml[i] = ap self.xml[i] = self.axml[i].get_xml_obj() if self.axml[i].is_packed(): - logger.warning("XML Seems to be packed, operations on the AndroidManifest.xml might fail.") + logger.warning( + "XML Seems to be packed, operations on the AndroidManifest.xml might fail." + ) if self.xml[i] is not None: if self.xml[i].tag != "manifest": - logger.error("AndroidManifest.xml does not start with a tag! Is this a valid APK?") + logger.error( + "AndroidManifest.xml does not start with a tag! Is this a valid APK?" + ) return self.package = self.get_attribute_value("manifest", "package") - self.androidversion["Code"] = self.get_attribute_value("manifest", "versionCode") - self.androidversion["Name"] = self.get_attribute_value("manifest", "versionName") - permission = list(self.get_all_attribute_value("uses-permission", "name")) + self.androidversion["Code"] = self.get_attribute_value( + "manifest", "versionCode" + ) + self.androidversion["Name"] = self.get_attribute_value( + "manifest", "versionName" + ) + permission = list( + self.get_all_attribute_value("uses-permission", "name") + ) self.permissions = list(set(self.permissions + permission)) for uses_permission in self.find_tags("uses-permission"): - self.uses_permissions.append([ - self.get_value_from_tag(uses_permission, "name"), - self._get_permission_maxsdk(uses_permission) - ]) + self.uses_permissions.append( + [ + self.get_value_from_tag(uses_permission, "name"), + self._get_permission_maxsdk(uses_permission), + ] + ) # getting details of the declared permissions for d_perm_item in self.find_tags('permission'): d_perm_name = self._get_res_string_value( - str(self.get_value_from_tag(d_perm_item, "name"))) + str(self.get_value_from_tag(d_perm_item, "name")) + ) d_perm_label = self._get_res_string_value( - str(self.get_value_from_tag(d_perm_item, "label"))) + str(self.get_value_from_tag(d_perm_item, "label")) + ) d_perm_description = self._get_res_string_value( - str(self.get_value_from_tag(d_perm_item, "description"))) + str( + self.get_value_from_tag(d_perm_item, "description") + ) + ) d_perm_permissionGroup = self._get_res_string_value( - str(self.get_value_from_tag(d_perm_item, "permissionGroup"))) + str( + self.get_value_from_tag( + d_perm_item, "permissionGroup" + ) + ) + ) d_perm_protectionLevel = self._get_res_string_value( - str(self.get_value_from_tag(d_perm_item, "protectionLevel"))) + str( + self.get_value_from_tag( + d_perm_item, "protectionLevel" + ) + ) + ) d_perm_details = { "label": d_perm_label, @@ -415,9 +491,13 @@ class APK: logger.info("APK file was successfully validated!") self.permission_module = androconf.load_api_specific_resource_module( - "aosp_permissions", self.get_target_sdk_version()) - self.permission_module_min_sdk = androconf.load_api_specific_resource_module( - "aosp_permissions", self.get_min_sdk_version()) + "aosp_permissions", self.get_target_sdk_version() + ) + self.permission_module_min_sdk = ( + androconf.load_api_specific_resource_module( + "aosp_permissions", self.get_min_sdk_version() + ) + ) def __getstate__(self): """ @@ -469,7 +549,10 @@ class APK: try: maxSdkVersion = int(self.get_value_from_tag(item, "maxSdkVersion")) except ValueError: - logger.warning(str(maxSdkVersion) + ' is not a valid value for maxSdkVersion') + logger.warning( + str(maxSdkVersion) + + ' is not a valid value for maxSdkVersion' + ) except TypeError: pass return maxSdkVersion @@ -515,12 +598,16 @@ class APK: # FIXME: would need to use _format_value inside get_attribute_value for each returned name! # For example, as the activity name might be foobar.foo.bar but inside the activity it is only .bar - app_name = self.get_attribute_value('activity', 'label', name=main_activity_name) + app_name = self.get_attribute_value( + 'activity', 'label', name=main_activity_name + ) if app_name is None: # No App name set # TODO return packagename instead? - logger.warning("It looks like that no app name is set for the main activity!") + logger.warning( + "It looks like that no app name is set for the main activity!" + ) return "" if app_name.startswith("@"): @@ -537,24 +624,34 @@ class APK: if package == 'android': # TODO: we can not resolve this, as we lack framework-res.apk # one exception would be when parsing framework-res.apk directly. - logger.warning("Resource ID with android package name encountered! " - "Will not resolve, framework-res.apk would be required.") + logger.warning( + "Resource ID with android package name encountered! " + "Will not resolve, framework-res.apk would be required." + ) return app_name else: # TODO should look this up, might be in the resources - logger.warning("Resource ID with Package name '{}' encountered! Will not resolve".format(package)) + logger.warning( + "Resource ID with Package name '{}' encountered! Will not resolve".format( + package + ) + ) return app_name try: - config = ARSCResTableConfig(None, locale=locale) if locale else ARSCResTableConfig.default_config() - app_name = res_parser.get_resolved_res_configs( - res_id, - config)[0][1] + config = ( + ARSCResTableConfig(None, locale=locale) + if locale + else ARSCResTableConfig.default_config() + ) + app_name = res_parser.get_resolved_res_configs(res_id, config)[ + 0 + ][1] except Exception as e: logger.warning("Exception selecting app name: %s" % e) return app_name - def get_app_icon(self, max_dpi:int=65536) -> Union[str,None]: + def get_app_icon(self, max_dpi: int = 65536) -> Union[str, None]: """ Return the first icon file name, which density is not greater than max_dpi, unless exact icon resolution is set in the manifest, in which case @@ -598,7 +695,8 @@ class APK: main_activity_name = self.get_main_activity() app_icon = self.get_attribute_value( - 'activity', 'icon', name=main_activity_name) + 'activity', 'icon', name=main_activity_name + ) if not app_icon: app_icon = self.get_attribute_value('application', 'icon') @@ -609,12 +707,16 @@ class APK: return None if not app_icon: - res_id = res_parser.get_res_id_by_key(self.package, 'mipmap', 'ic_launcher') + res_id = res_parser.get_res_id_by_key( + self.package, 'mipmap', 'ic_launcher' + ) if res_id: app_icon = "@%x" % res_id if not app_icon: - res_id = res_parser.get_res_id_by_key(self.package, 'drawable', 'ic_launcher') + res_id = res_parser.get_res_id_by_key( + self.package, 'drawable', 'ic_launcher' + ) if res_id: app_icon = "@%x" % res_id @@ -701,10 +803,16 @@ class APK: return default except TypeError as e: self.__no_magic = True - logger.warning("It looks like you have the magic python package installed but not the magic library itself!") + logger.warning( + "It looks like you have the magic python package installed but not the magic library itself!" + ) logger.warning("Error from magic library: %s", e) - logger.warning("Please follow the installation instructions at https://github.com/ahupp/python-magic/#installation") - logger.warning("You can also install the 'python-magic-bin' package on Windows and MacOS") + logger.warning( + "Please follow the installation instructions at https://github.com/ahupp/python-magic/#installation" + ) + logger.warning( + "You can also install the 'python-magic-bin' package on Windows and MacOS" + ) return default try: @@ -715,8 +823,10 @@ class APK: getattr(magic, "MagicException") except AttributeError: self.__no_magic = True - logger.warning("Not the correct Magic library was found on your " - "system. Please install python-magic or python-magic-bin!") + logger.warning( + "Not the correct Magic library was found on your " + "system. Please install python-magic or python-magic-bin!" + ) return default try: @@ -740,7 +850,7 @@ class APK: """ return self.get_files_types() - def get_files_types(self) -> dict[str,str]: + def get_files_types(self) -> dict[str, str]: """ Return the files inside the APK with their associated types (by using python-magic) @@ -764,7 +874,11 @@ class APK: :param orig: guess by mime libary :returns: corrected guess """ - if ("Zip" in orig) or ('(JAR)' in orig) and androconf.is_android_raw(buffer) == 'APK': + if ( + ("Zip" in orig) + or ('(JAR)' in orig) + and androconf.is_android_raw(buffer) == 'APK' + ): return "Android application package file" return orig @@ -781,11 +895,20 @@ class APK: buffer = self.zip.read(filename) if filename not in self.files_crc32: self.files_crc32[filename] = crc32(buffer) - if self.files_crc32[filename] != self.zip.infolist()[filename].crc32_of_uncompressed_data: - logger.error("File '{}' has different CRC32 after unpacking! " - "Declared: {:08x}, Calculated: {:08x}".format(filename, - self.zip.infolist()[filename].crc32_of_uncompressed_data, - self.files_crc32[filename])) + if ( + self.files_crc32[filename] + != self.zip.infolist()[filename].crc32_of_uncompressed_data + ): + logger.error( + "File '{}' has different CRC32 after unpacking! " + "Declared: {:08x}, Calculated: {:08x}".format( + filename, + self.zip.infolist()[ + filename + ].crc32_of_uncompressed_data, + self.files_crc32[filename], + ) + ) return buffer def get_files_crc32(self) -> dict[str, int]: @@ -823,7 +946,7 @@ class APK: self.__raw = bytearray(f.read()) return self.__raw - def get_file(self, filename:str) -> bytes: + def get_file(self, filename: str) -> bytes: """ Return the raw data of the specified filename inside the APK @@ -877,7 +1000,16 @@ class APK: :returns: True if multiple dex found, otherwise False """ dexre = re.compile(r"^classes(\d+)?.dex$") - return len([instance for instance in self.get_files() if dexre.search(instance)]) > 1 + return ( + len( + [ + instance + for instance in self.get_files() + if dexre.search(instance) + ] + ) + > 1 + ) def _format_value(self, value): """ @@ -901,7 +1033,11 @@ class APK: return value def get_all_attribute_value( - self, tag_name: str, attribute: str, format_value:bool=True, **attribute_filter + self, + tag_name: str, + attribute: str, + format_value: bool = True, + **attribute_filter, ) -> Iterator[str]: """ Yields all the attribute values in xml files which match with the tag name and the specific attribute @@ -920,7 +1056,11 @@ class APK: yield value def get_attribute_value( - self, tag_name:str, attribute:str, format_value:bool=False, **attribute_filter + self, + tag_name: str, + attribute: str, + format_value: bool = False, + **attribute_filter, ) -> str: """ Return the attribute value in xml files which matches the tag name and the specific attribute @@ -931,11 +1071,14 @@ class APK: """ for value in self.get_all_attribute_value( - tag_name, attribute, format_value, **attribute_filter): + tag_name, attribute, format_value, **attribute_filter + ): if value is not None: return value - def get_value_from_tag(self, tag: Element, attribute: str) -> Union[str,None]: + def get_value_from_tag( + self, tag: Element, attribute: str + ) -> Union[str, None]: """ Return the value of the android prefixed attribute in a specific tag. @@ -975,8 +1118,12 @@ class APK: if value: # If value is still None, the attribute could not be found, thus is not present - logger.warning("Failed to get the attribute '{}' on tag '{}' with namespace. " - "But found the same attribute without namespace!".format(attribute, tag.tag)) + logger.warning( + "Failed to get the attribute '{}' on tag '{}' with namespace. " + "But found the same attribute without namespace!".format( + attribute, tag.tag + ) + ) return value def find_tags(self, tag_name: str, **attribute_filter) -> list[str]: @@ -986,9 +1133,7 @@ class APK: :param str tag: specify the tag name """ all_tags = [ - self.find_tags_from_xml( - i, tag_name, **attribute_filter - ) + self.find_tags_from_xml(i, tag_name, **attribute_filter) for i in self.xml ] return [tag for tag_list in all_tags for tag in tag_list] @@ -1006,9 +1151,7 @@ class APK: if xml is None: return [] if xml.tag == tag_name: - if self.is_tag_matched( - xml.tag, **attribute_filter - ): + if self.is_tag_matched(xml.tag, **attribute_filter): return [xml] return [] tags = set() @@ -1018,9 +1161,7 @@ class APK: # permission declared using tag bool: @@ -1064,8 +1205,9 @@ class APK: for i in self.xml: if self.xml[i] is None: continue - activities_and_aliases = self.xml[i].findall(".//activity") + \ - self.xml[i].findall(".//activity-alias") + activities_and_aliases = self.xml[i].findall( + ".//activity" + ) + self.xml[i].findall(".//activity-alias") for item in activities_and_aliases: # Some applications have more than one MAIN activity. @@ -1094,7 +1236,7 @@ class APK: return x.intersection(y) - def get_main_activity(self) -> Union[str,None]: + def get_main_activity(self) -> Union[str, None]: """ Return the name of the main activity @@ -1107,10 +1249,11 @@ class APK: return self._format_value(activities.pop()) elif len(activities) > 1: main_activities = {self._format_value(ma) for ma in activities} - # sorted is necessary + # sorted is necessary # 9fc7d3e8225f6b377f9181a92c551814317b77e1aa0df4c6d508d24b18f0f633 good_main_activities = sorted( - main_activities.intersection(self.get_activities())) + main_activities.intersection(self.get_activities()) + ) if good_main_activities: return good_main_activities[0] return sorted(main_activities)[0] @@ -1124,7 +1267,7 @@ class APK: """ return list(self.get_all_attribute_value("activity", "name")) - def get_activity_aliases(self) -> list[dict[str,str]]: + def get_activity_aliases(self) -> list[dict[str, str]]: """ Return the android:name and android:targetActivity attribute of all activity aliases. @@ -1134,8 +1277,7 @@ class APK: for alias in self.find_tags('activity-alias'): activity_alias = {} for attribute in ['name', 'targetActivity']: - value = (alias.get(attribute) or - alias.get(self._ns(attribute))) + value = alias.get(attribute) or alias.get(self._ns(attribute)) if not value: continue activity_alias[attribute] = self._format_value(value) @@ -1167,7 +1309,7 @@ class APK: """ return list(self.get_all_attribute_value("provider", "name")) - def get_res_value(self, name:str) -> str: + def get_res_value(self, name: str) -> str: """ Return the literal value with a resource id :rtype: str @@ -1180,15 +1322,17 @@ class APK: res_id = res_parser.parse_id(name)[0] try: value = res_parser.get_resolved_res_configs( - res_id, - ARSCResTableConfig.default_config())[0][1] + res_id, ARSCResTableConfig.default_config() + )[0][1] except Exception as e: logger.warning("Exception get resolved resource id: %s" % e) return name return value - def get_intent_filters(self, itemtype:str, name:str) -> dict[str,list[str]]: + def get_intent_filters( + self, itemtype: str, name: str + ) -> dict[str, list[str]]: """ Find intent filters for a given item and name. @@ -1200,7 +1344,19 @@ class APK: :param name: the `android:name` of the parent item, e.g. activity name :returns: a dictionary with the keys `action` and `category` containing the `android:name` of those items """ - attributes = {"action": ["name"], "category": ["name"], "data": ['scheme', 'host', 'port', 'path', 'pathPattern', 'pathPrefix', 'mimeType']} + attributes = { + "action": ["name"], + "category": ["name"], + "data": [ + 'scheme', + 'host', + 'port', + 'path', + 'pathPattern', + 'pathPrefix', + 'mimeType', + ], + } d = {} for element in attributes.keys(): @@ -1213,13 +1369,15 @@ class APK: for sitem in item.findall(".//intent-filter"): for element in d.keys(): for ssitem in sitem.findall(element): - if element == 'data': # multiple attributes + if element == 'data': # multiple attributes values = {} for attribute in attributes[element]: value = ssitem.get(self._ns(attribute)) if value: if value.startswith('@'): - value = self.get_res_value(value) + value = self.get_res_value( + value + ) values[attribute] = value if values: @@ -1258,9 +1416,9 @@ class APK: def get_uses_implied_permission_list(self) -> list[str]: """ - Return all permissions implied by the target SDK or other permissions. + Return all permissions implied by the target SDK or other permissions. - :rtype: list of string + :rtype: list of string """ target_sdk_version = self.get_effective_target_sdk_version() @@ -1282,8 +1440,10 @@ class APK: if READ_PHONE_STATE not in self.permissions: implied.append([READ_PHONE_STATE, None]) - if (WRITE_EXTERNAL_STORAGE in self.permissions or implied_WRITE_EXTERNAL_STORAGE) \ - and READ_EXTERNAL_STORAGE not in self.permissions: + if ( + WRITE_EXTERNAL_STORAGE in self.permissions + or implied_WRITE_EXTERNAL_STORAGE + ) and READ_EXTERNAL_STORAGE not in self.permissions: maxSdkVersion = None for name, version in self.uses_permissions: if name == WRITE_EXTERNAL_STORAGE: @@ -1292,16 +1452,22 @@ class APK: implied.append([READ_EXTERNAL_STORAGE, maxSdkVersion]) if target_sdk_version < 16: - if READ_CONTACTS in self.permissions \ - and READ_CALL_LOG not in self.permissions: + if ( + READ_CONTACTS in self.permissions + and READ_CALL_LOG not in self.permissions + ): implied.append([READ_CALL_LOG, None]) - if WRITE_CONTACTS in self.permissions \ - and WRITE_CALL_LOG not in self.permissions: + if ( + WRITE_CONTACTS in self.permissions + and WRITE_CALL_LOG not in self.permissions + ): implied.append([WRITE_CALL_LOG, None]) return implied - def _update_permission_protection_level(self, protection_level, sdk_version): + def _update_permission_protection_level( + self, protection_level, sdk_version + ): if not sdk_version or int(sdk_version) <= 15: return protection_level.replace('Or', '|').lower() return protection_level @@ -1311,19 +1477,29 @@ class APK: target_sdk = self.get_target_sdk_version() filled_permissions = permissions.copy() for permission in filled_permissions: - protection_level, label, description = filled_permissions[permission] - if ((not label or not description) - and permission in self.permission_module_min_sdk): + protection_level, label, description = filled_permissions[ + permission + ] + if ( + not label or not description + ) and permission in self.permission_module_min_sdk: x = self.permission_module_min_sdk[permission] protection_level = self._update_permission_protection_level( - x['protectionLevel'], min_sdk) + x['protectionLevel'], min_sdk + ) filled_permissions[permission] = [ - protection_level, x['label'], x['description']] + protection_level, + x['label'], + x['description'], + ] else: filled_permissions[permission] = [ self._update_permission_protection_level( - protection_level, target_sdk), - label, description] + protection_level, target_sdk + ), + label, + description, + ] return filled_permissions def get_details_permissions(self) -> dict[str, list[str]]: @@ -1342,10 +1518,17 @@ class APK: x = self.permission_module[i] l[i] = [x["protectionLevel"], x["label"], x["description"]] elif i in self.declared_permissions: - protectionLevel_hex = self.declared_permissions[i]["protectionLevel"] - protectionLevel = protection_flags_to_attributes[protectionLevel_hex] - l[i] = [protectionLevel, "Unknown permission from android reference", - "Unknown permission from android reference"] + protectionLevel_hex = self.declared_permissions[i][ + "protectionLevel" + ] + protectionLevel = protection_flags_to_attributes[ + protectionLevel_hex + ] + l[i] = [ + protectionLevel, + "Unknown permission from android reference", + "Unknown permission from android reference", + ] else: # Is there a valid case not belonging to the above two? logger.info(f"Unknown permission {i}") @@ -1412,37 +1595,37 @@ class APK: def get_max_sdk_version(self) -> str: """ - Return the android:maxSdkVersion attribute + Return the android:maxSdkVersion attribute - :rtype: string + :rtype: string """ return self.get_attribute_value("uses-sdk", "maxSdkVersion") def get_min_sdk_version(self) -> str: """ - Return the android:minSdkVersion attribute + Return the android:minSdkVersion attribute - :rtype: string + :rtype: string """ return self.get_attribute_value("uses-sdk", "minSdkVersion") def get_target_sdk_version(self) -> str: """ - Return the android:targetSdkVersion attribute + Return the android:targetSdkVersion attribute - :rtype: string + :rtype: string """ return self.get_attribute_value("uses-sdk", "targetSdkVersion") def get_effective_target_sdk_version(self) -> int: """ - Return the effective targetSdkVersion, always returns int > 0. + Return the effective targetSdkVersion, always returns int > 0. - If the targetSdkVersion is not set, it defaults to 1. This is - set based on defaults as defined in: - https://developer.android.com/guide/topics/manifest/uses-sdk-element.html + If the targetSdkVersion is not set, it defaults to 1. This is + set based on defaults as defined in: + https://developer.android.com/guide/topics/manifest/uses-sdk-element.html - :rtype: int + :rtype: int """ target_sdk_version = self.get_target_sdk_version() if not target_sdk_version: @@ -1454,9 +1637,9 @@ class APK: def get_libraries(self) -> list[str]: """ - Return the android:name attributes for libraries + Return the android:name attributes for libraries - :rtype: list + :rtype: list """ return list(self.get_all_attribute_value("uses-library", "name")) @@ -1499,9 +1682,17 @@ class APK: :returns: True if 'android.hardware.touchscreen' is not required, False otherwise """ - return self.get_attribute_value('uses-feature', 'name', required="false", name="android.hardware.touchscreen") == "android.hardware.touchscreen" + return ( + self.get_attribute_value( + 'uses-feature', + 'name', + required="false", + name="android.hardware.touchscreen", + ) + == "android.hardware.touchscreen" + ) - def get_certificate_der(self, filename:str) -> bytes: + def get_certificate_der(self, filename: str) -> bytes: """ Return the DER coded X.509 certificate from the signature file. @@ -1514,11 +1705,13 @@ class APK: # TODO: should be returning the matching cert here! # https://github.com/androguard/androguard/pull/1038 if len(pkcs7obj['content']['certificates']) > 1: - logger.warning(f"Multiple certificates found. Returning the first one!") + logger.warning( + f"Multiple certificates found. Returning the first one!" + ) cert = pkcs7obj['content']['certificates'][0].chosen.dump() return cert - def get_certificate(self, filename:str) -> x509.Certificate: + def get_certificate(self, filename: str) -> x509.Certificate: """ Return a X.509 certificate object by giving the name in the apk file @@ -1530,17 +1723,22 @@ class APK: return certificate - def new_zip(self, filename:str, deleted_files:Union[str,None]=None, new_files:dict={}) -> None: + def new_zip( + self, + filename: str, + deleted_files: Union[str, None] = None, + new_files: dict = {}, + ) -> None: """ - Create a new zip file + Create a new zip file - :param filename: the output filename of the zip - :param deleted_files: a regex pattern to remove specific file - :param new_files: a dictionnary of new files + :param filename: the output filename of the zip + :param deleted_files: a regex pattern to remove specific file + :param new_files: a dictionnary of new files - :type filename: string - :type deleted_files: None or a string - :type new_files: a dictionnary (key:filename, value:content of the file) + :type filename: string + :type deleted_files: None or a string + :type new_files: a dictionnary (key:filename, value:content of the file) """ zout = zipfile.ZipFile(filename, 'w') @@ -1571,11 +1769,11 @@ class APK: zout.writestr(item, buffer) zout.close() - def get_android_manifest_axml(self) -> Union[AXMLPrinter,None]: + def get_android_manifest_axml(self) -> Union[AXMLPrinter, None]: """ - Return the :class:`AXMLPrinter` object which corresponds to the AndroidManifest.xml file + Return the :class:`AXMLPrinter` object which corresponds to the AndroidManifest.xml file - :rtype: :class:`~androguard.core.axml.AXMLPrinter` + :rtype: :class:`~androguard.core.axml.AXMLPrinter` """ try: return self.axml["AndroidManifest.xml"] @@ -1593,7 +1791,7 @@ class APK: except KeyError: return None - def get_android_resources(self) -> Union[ARSCParser,None]: + def get_android_resources(self) -> Union[ARSCParser, None]: """ Return the :class:`~androguard.core.axml.ARSCParser` object which corresponds to the resources.arsc file @@ -1606,14 +1804,18 @@ class APK: # There is a rare case, that no resource file is supplied. # Maybe it was added manually, thus we check here return None - self.arsc["resources.arsc"] = ARSCParser(self.zip.read("resources.arsc")) + self.arsc["resources.arsc"] = ARSCParser( + self.zip.read("resources.arsc") + ) return self.arsc["resources.arsc"] def is_signed(self) -> bool: """ Returns true if any of v1, v2, or v3 signatures were found. """ - return self.is_signed_v1() or self.is_signed_v2() or self.is_signed_v3() + return ( + self.is_signed_v1() or self.is_signed_v2() or self.is_signed_v3() + ) def is_signed_v1(self) -> bool: """ @@ -1649,11 +1851,13 @@ class APK: return self._is_signed_v3 def read_uint32_le(self, io_stream) -> int: - value, = unpack(' list[tuple[int, bytes]]: - """ Parse digests """ + def parse_signatures_or_digests( + self, digest_bytes + ) -> list[tuple[int, bytes]]: + """Parse digests""" if not len(digest_bytes): return [] @@ -1696,11 +1900,17 @@ class APK: while f.tell() > 0: f.seek(-1, io.SEEK_CUR) - r, = unpack('<4s', f.read(4)) + (r,) = unpack('<4s', f.read(4)) if r == self._PK_END_OF_CENTRAL_DIR: # Read central dir - this_disk, disk_central, this_entries, total_entries, \ - size_central, offset_central = unpack(' None: """ Parse the V2 signing block and extract all features @@ -1791,7 +2006,9 @@ class APK: # * publickey size_sequence = self.read_uint32_le(block) if size_sequence + 4 != len(block_bytes): - raise BrokenAPKError("size of sequence and blocksize does not match") + raise BrokenAPKError( + "size of sequence and blocksize does not match" + ) while block.tell() < len(block_bytes): off_signer = block.tell() @@ -1808,7 +2025,6 @@ class APK: raw_digests = signed_data.read(len_digests) digests = self.parse_signatures_or_digests(raw_digests) - # Certs certs = [] len_certs = self.read_uint32_le(signed_data) @@ -1849,7 +2065,7 @@ class APK: publickey = block.read(len_publickey) signer = APKV3Signer() - signer._bytes = view[off_signer:off_signer+size_signer] + signer._bytes = view[off_signer : off_signer + size_signer] signer.signed_data = signed_data_object signer.signatures = sigs signer.public_key = publickey @@ -1887,7 +2103,9 @@ class APK: size_sequence = self.read_uint32_le(block) if size_sequence + 4 != len(block_bytes): - raise BrokenAPKError("size of sequence and blocksize does not match") + raise BrokenAPKError( + "size of sequence and blocksize does not match" + ) while block.tell() < len(block_bytes): off_signer = block.tell() @@ -1933,7 +2151,7 @@ class APK: publickey = block.read(len_publickey) signer = APKV2Signer() - signer._bytes = view[off_signer:off_signer+size_signer] + signer._bytes = view[off_signer : off_signer + size_signer] signer.signed_data = signed_data_object signer.signatures = sigs signer.public_key = publickey @@ -1979,7 +2197,9 @@ class APK: self.parse_v3_signing_block() certs = [] - for signed_data in [signer.signed_data for signer in self._v3_signing_data]: + for signed_data in [ + signer.signed_data for signer in self._v3_signing_data + ]: for cert in signed_data.certificates: certs.append(cert) @@ -1994,7 +2214,9 @@ class APK: self.parse_v2_signing_block() certs = [] - for signed_data in [signer.signed_data for signer in self._v2_signing_data]: + for signed_data in [ + signer.signed_data for signer in self._v2_signing_data + ]: for cert in signed_data.certificates: certs.append(cert) @@ -2005,14 +2227,20 @@ class APK: Return a list of :class:`asn1crypto.keys.PublicKeyInfo` which are found in the v3 signing block. """ - return [ keys.PublicKeyInfo.load(pkey) for pkey in self.get_public_keys_der_v3()] + return [ + keys.PublicKeyInfo.load(pkey) + for pkey in self.get_public_keys_der_v3() + ] def get_public_keys_v2(self) -> list[keys.PublicKeyInfo]: """ Return a list of :class:`asn1crypto.keys.PublicKeyInfo` which are found in the v2 signing block. """ - return [ keys.PublicKeyInfo.load(pkey) for pkey in self.get_public_keys_der_v2()] + return [ + keys.PublicKeyInfo.load(pkey) + for pkey in self.get_public_keys_der_v2() + ] def get_certificates_v3(self) -> list[x509.Certificate]: """ @@ -2021,7 +2249,10 @@ class APK: Note that we simply extract all certificates regardless of the signer. Therefore this is just a list of all certificates found in all signers. """ - return [ x509.Certificate.load(cert) for cert in self.get_certificates_der_v3()] + return [ + x509.Certificate.load(cert) + for cert in self.get_certificates_der_v3() + ] def get_certificates_v2(self) -> list[x509.Certificate]: """ @@ -2030,7 +2261,10 @@ class APK: Note that we simply extract all certificates regardless of the signer. Therefore this is just a list of all certificates found in all signers. """ - return [ x509.Certificate.load(cert) for cert in self.get_certificates_der_v2()] + return [ + x509.Certificate.load(cert) + for cert in self.get_certificates_der_v2() + ] def get_certificates_v1(self) -> list[x509.Certificate]: """ @@ -2054,15 +2288,19 @@ class APK: """ fps = [] certs = [] - for x in self.get_certificates_v1() + self.get_certificates_v2() + self.get_certificates_v3(): + for x in ( + self.get_certificates_v1() + + self.get_certificates_v2() + + self.get_certificates_v3() + ): if x.sha256 not in fps: fps.append(x.sha256) certs.append(x) return certs - def get_signature_name(self) -> Union[str,None]: + def get_signature_name(self) -> Union[str, None]: """ - Return the name of the first signature file found. + Return the name of the first signature file found. """ if self.get_signature_names(): return self.get_signature_names()[0] @@ -2085,11 +2323,15 @@ class APK: if "{}.SF".format(i.rsplit(".", 1)[0]) in self.get_files(): signatures.append(i) else: - logger.warning("v1 signature file {} missing .SF file - Partial signature!".format(i)) + logger.warning( + "v1 signature file {} missing .SF file - Partial signature!".format( + i + ) + ) return signatures - def get_signature(self) -> Union[str,None]: + def get_signature(self) -> Union[str, None]: """ Return the data of the first signature file found (v1 Signature / JAR Signature) @@ -2170,23 +2412,31 @@ class APK: show_Certificate(c) -def show_Certificate(cert, short:bool=False) -> None: +def show_Certificate(cert, short: bool = False) -> None: """ - Print Fingerprints, Issuer and Subject of an X509 Certificate. + Print Fingerprints, Issuer and Subject of an X509 Certificate. - :param cert: X509 Certificate to print - :param short: Print in shortform for DN (Default: False) + :param cert: X509 Certificate to print + :param short: Print in shortform for DN (Default: False) - :type cert: :class:`asn1crypto.x509.Certificate` - :type short: Boolean + :type cert: :class:`asn1crypto.x509.Certificate` + :type short: Boolean """ print("SHA1 Fingerprint: {}".format(cert.sha1_fingerprint)) print("SHA256 Fingerprint: {}".format(cert.sha256_fingerprint)) - print("Issuer: {}".format(get_certificate_name_string(cert.issuer.native, short=short))) - print("Subject: {}".format(get_certificate_name_string(cert.subject.native, short=short))) + print( + "Issuer: {}".format( + get_certificate_name_string(cert.issuer.native, short=short) + ) + ) + print( + "Subject: {}".format( + get_certificate_name_string(cert.subject.native, short=short) + ) + ) -def ensure_final_value(packageName:str, arsc:ARSCParser, value:str) -> str: +def ensure_final_value(packageName: str, arsc: ARSCParser, value: str) -> str: """Ensure incoming value is always the value, not the resid androguard will sometimes return the Android "resId" aka @@ -2209,7 +2459,7 @@ def ensure_final_value(packageName:str, arsc:ARSCParser, value:str) -> str: return '' -def get_apkid(apkfile: str) -> tuple[str,str,str]: +def get_apkid(apkfile: str) -> tuple[str, str, str]: """Read (appid, versionCode, versionName) from an APK This first tries to do quick binary XML parsing to just get the @@ -2239,7 +2489,9 @@ def get_apkid(apkfile: str) -> tuple[str,str,str]: name = axml.getAttributeName(i) _type = axml.getAttributeValueType(i) _data = axml.getAttributeValueData(i) - value = format_value(_type, _data, lambda _: axml.getAttributeValue(i)) + value = format_value( + _type, _data, lambda _: axml.getAttributeValue(i) + ) if appid is None and name == 'package': appid = value elif versionCode is None and name == 'versionCode': @@ -2253,12 +2505,17 @@ def get_apkid(apkfile: str) -> tuple[str,str,str]: if axml.name == 'manifest': break elif _type == END_TAG or _type == TEXT or _type == END_DOCUMENT: - raise RuntimeError('{path}: must be the first element in AndroidManifest.xml' - .format(path=apkfile)) + raise RuntimeError( + '{path}: must be the first element in AndroidManifest.xml'.format( + path=apkfile + ) + ) if not versionName or versionName[0] == '@': a = APK(apkfile) - versionName = ensure_final_value(a.package, a.get_android_resources(), a.get_androidversion_name()) + versionName = ensure_final_value( + a.package, a.get_android_resources(), a.get_androidversion_name() + ) if not versionName: versionName = '' # versionName is expected to always be a str diff --git a/androguard/core/axml/__init__.py b/androguard/core/axml/__init__.py index 4333688e..258e10e1 100644 --- a/androguard/core/axml/__init__.py +++ b/androguard/core/axml/__init__.py @@ -4,19 +4,20 @@ from __future__ import annotations import binascii -from collections import defaultdict import collections import io import re +from collections import defaultdict from struct import pack, unpack from typing import BinaryIO, Union - -from androguard.core.resources import public -from .types import * +from xml.sax.saxutils import escape from loguru import logger from lxml import etree -from xml.sax.saxutils import escape + +from androguard.core.resources import public + +from .types import * # Constants for ARSC Files # see http://aospxref.com/android-13.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#233 @@ -25,23 +26,23 @@ RES_STRING_POOL_TYPE = 0x0001 RES_TABLE_TYPE = 0x0002 RES_XML_TYPE = 0x0003 -RES_XML_FIRST_CHUNK_TYPE = 0x0100 -RES_XML_START_NAMESPACE_TYPE = 0x0100 -RES_XML_END_NAMESPACE_TYPE = 0x0101 -RES_XML_START_ELEMENT_TYPE = 0x0102 -RES_XML_END_ELEMENT_TYPE = 0x0103 -RES_XML_CDATA_TYPE = 0x0104 -RES_XML_LAST_CHUNK_TYPE = 0x017f +RES_XML_FIRST_CHUNK_TYPE = 0x0100 +RES_XML_START_NAMESPACE_TYPE = 0x0100 +RES_XML_END_NAMESPACE_TYPE = 0x0101 +RES_XML_START_ELEMENT_TYPE = 0x0102 +RES_XML_END_ELEMENT_TYPE = 0x0103 +RES_XML_CDATA_TYPE = 0x0104 +RES_XML_LAST_CHUNK_TYPE = 0x017F -RES_XML_RESOURCE_MAP_TYPE = 0x0180 +RES_XML_RESOURCE_MAP_TYPE = 0x0180 -RES_TABLE_PACKAGE_TYPE = 0x0200 -RES_TABLE_TYPE_TYPE = 0x0201 -RES_TABLE_TYPE_SPEC_TYPE = 0x0202 -RES_TABLE_LIBRARY_TYPE = 0x0203 -RES_TABLE_OVERLAYABLE_TYPE = 0x0204 +RES_TABLE_PACKAGE_TYPE = 0x0200 +RES_TABLE_TYPE_TYPE = 0x0201 +RES_TABLE_TYPE_SPEC_TYPE = 0x0202 +RES_TABLE_LIBRARY_TYPE = 0x0203 +RES_TABLE_OVERLAYABLE_TYPE = 0x0204 RES_TABLE_OVERLAYABLE_POLICY_TYPE = 0x0205 -RES_TABLE_STAGED_ALIAS_TYPE = 0x0206 +RES_TABLE_STAGED_ALIAS_TYPE = 0x0206 # Flags in the STRING Section SORTED_FLAG = 1 << 0 UTF8_FLAG = 1 << 8 @@ -79,7 +80,7 @@ TYPE_TABLE = { TYPE_STRING: "string", } -RADIX_MULTS = [0.00390625, 3.051758E-005, 1.192093E-007, 4.656613E-010] +RADIX_MULTS = [0.00390625, 3.051758e-005, 1.192093e-007, 4.656613e-010] DIMENSION_UNITS = ["px", "dip", "sp", "pt", "in", "mm"] FRACTION_UNITS = ["%", "%p"] @@ -88,6 +89,7 @@ COMPLEX_UNIT_MASK = 0x0F class ResParserError(Exception): """Exception for the parsers""" + pass @@ -105,7 +107,8 @@ class StringBlock: See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#436 """ - def __init__(self, buff:BinaryIO, header:ARSCHeader) -> None: + + def __init__(self, buff: BinaryIO, header: ARSCHeader) -> None: """ :param buff: buffer which holds the string block :param header: a instance of :class:`~ARSCHeader` @@ -120,14 +123,18 @@ class StringBlock: # flags self.flags = unpack(' 0: - logger.info("Styles Offset given, but styleCount is zero. " - "This is not a problem but could indicate packers.") + logger.info( + "Styles Offset given, but styleCount is zero. " + "This is not a problem but could indicate packers." + ) self.m_stringOffsets = [] self.m_styleOffsets = [] @@ -176,7 +185,9 @@ class StringBlock: self.m_styles.append(unpack('".format(self.stringCount, self.styleCount, self.m_isUTF8) + return "".format( + self.stringCount, self.styleCount, self.m_isUTF8 + ) def __getitem__(self, idx): """ @@ -219,7 +230,7 @@ class StringBlock: return self._cache[idx] - def getStyle(self, idx:int) -> int: + def getStyle(self, idx: int) -> int: """ Return the style associated with the index @@ -228,7 +239,7 @@ class StringBlock: """ return self.m_styles[idx] - def _decode8(self, offset:int) -> str: + def _decode8(self, offset: int) -> str: """ Decode an UTF-8 String at the given offset @@ -249,16 +260,22 @@ class StringBlock: # b) non-null terminated strings which should be rejected # platform/frameworks/base/libs/androidfw/ResourceTypes.cpp#789 if len(self.m_charbuff) < (offset + encoded_bytes): - logger.warning(f"String size: {offset + encoded_bytes} is exceeding string pool size. Returning empty string.") + logger.warning( + f"String size: {offset + encoded_bytes} is exceeding string pool size. Returning empty string." + ) return "" - data = self.m_charbuff[offset: offset + encoded_bytes] + data = self.m_charbuff[offset : offset + encoded_bytes] if self.m_charbuff[offset + encoded_bytes] != 0: - raise ResParserError("UTF-8 String is not null terminated! At offset={}".format(offset)) + raise ResParserError( + "UTF-8 String is not null terminated! At offset={}".format( + offset + ) + ) return self._decode_bytes(data, 'utf-8', str_len) - def _decode16(self, offset:int) -> str: + def _decode16(self, offset: int) -> str: """ Decode an UTF-16 String at the given offset @@ -276,18 +293,29 @@ class StringBlock: # b) non-null terminated strings which should be rejected # platform/frameworks/base/libs/androidfw/ResourceTypes.cpp#789 if len(self.m_charbuff) < (offset + encoded_bytes): - logger.warning(f"String size: {offset + encoded_bytes} is exceeding string pool size. Returning empty string.") + logger.warning( + f"String size: {offset + encoded_bytes} is exceeding string pool size. Returning empty string." + ) return "" - data = self.m_charbuff[offset: offset + encoded_bytes] + data = self.m_charbuff[offset : offset + encoded_bytes] - if self.m_charbuff[offset + encoded_bytes:offset + encoded_bytes + 2] != b"\x00\x00": - raise ResParserError("UTF-16 String is not null terminated! At offset={}".format(offset)) + if ( + self.m_charbuff[ + offset + encoded_bytes : offset + encoded_bytes + 2 + ] + != b"\x00\x00" + ): + raise ResParserError( + "UTF-16 String is not null terminated! At offset={}".format( + offset + ) + ) return self._decode_bytes(data, 'utf-16', str_len) @staticmethod - def _decode_bytes(data:bytes, encoding:str, str_len:int) -> str: + def _decode_bytes(data: bytes, encoding: str, str_len: int) -> str: """ Generic decoding with length check. The string is decoded from bytes with the given encoding, then the length @@ -304,7 +332,7 @@ class StringBlock: logger.warning("invalid decoded string length") return string - def _decode_length(self, offset:int, sizeof_char:int) -> tuple[int,int]: + def _decode_length(self, offset: int, sizeof_char: int) -> tuple[int, int]: """ Generic Length Decoding at offset of string @@ -324,7 +352,9 @@ class StringBlock: fmt = "<2{}".format('B' if sizeof_char == 1 else 'H') highbit = 0x80 << (8 * (sizeof_char - 1)) - length1, length2 = unpack(fmt, self.m_charbuff[offset:(offset + sizeof_2chars)]) + length1, length2 = unpack( + fmt, self.m_charbuff[offset : (offset + sizeof_2chars)] + ) if (length1 & highbit) != 0: length = ((length1 & ~highbit) << (8 * sizeof_char)) | length2 @@ -335,9 +365,17 @@ class StringBlock: # These are true asserts, as the size should never be less than the values if sizeof_char == 1: - assert length <= 0x7FFF, "length of UTF-8 string is too large! At offset={}".format(offset) + assert ( + length <= 0x7FFF + ), "length of UTF-8 string is too large! At offset={}".format( + offset + ) else: - assert length <= 0x7FFFFFFF, "length of UTF-16 string is too large! At offset={}".format(offset) + assert ( + length <= 0x7FFFFFFF + ), "length of UTF-16 string is too large! At offset={}".format( + offset + ) return length, size @@ -345,16 +383,21 @@ class StringBlock: """ Print some information on stdout about the string table """ - print("StringBlock(stringsCount=0x%x, " - "stringsOffset=0x%x, " - "stylesCount=0x%x, " - "stylesOffset=0x%x, " - "flags=0x%x" - ")" % (self.stringCount, - self.stringsOffset, - self.styleCount, - self.stylesOffset, - self.flags)) + print( + "StringBlock(stringsCount=0x%x, " + "stringsOffset=0x%x, " + "stylesCount=0x%x, " + "stylesOffset=0x%x, " + "flags=0x%x" + ")" + % ( + self.stringCount, + self.stringsOffset, + self.styleCount, + self.stylesOffset, + self.flags, + ) + ) if self.stringCount > 0: print() @@ -392,7 +435,8 @@ class AXMLParser: See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#563 """ - def __init__(self, raw_buff:bytes) -> None: + + def __init__(self, raw_buff: bytes) -> None: logger.debug("AXMLParser") self._reset() @@ -405,14 +449,22 @@ class AXMLParser: # Minimum is a single ARSCHeader, which would be a strange edge case... if self.buff_size < 8: - logger.error("Filesize is too small to be a valid AXML file! Filesize: {}".format(self.buff_size)) + logger.error( + "Filesize is too small to be a valid AXML file! Filesize: {}".format( + self.buff_size + ) + ) self._valid = False return # This would be even stranger, if an AXML file is larger than 4GB... # But this is not possible as the maximum chunk size is a unsigned 4 byte int. if self.buff_size > 0xFFFFFFFF: - logger.error("Filesize is too large to be a valid AXML file! Filesize: {}".format(self.buff_size)) + logger.error( + "Filesize is too large to be a valid AXML file! Filesize: {}".format( + self.buff_size + ) + ) self._valid = False return @@ -429,43 +481,67 @@ class AXMLParser: if axml_header.header_size == 28024: # Can be a common error: the file is not an AXML but a plain XML # The file will then usually start with ' self.buff_size: - logger.error("This does not look like an AXML file. Declared filesize does not match real size: {} vs {}".format(self.filesize, self.buff_size)) + logger.error( + "This does not look like an AXML file. Declared filesize does not match real size: {} vs {}".format( + self.filesize, self.buff_size + ) + ) self._valid = False return if self.filesize < self.buff_size: # The file can still be parsed up to the point where the chunk should end. self.axml_tampered = True - logger.warning("Declared filesize ({}) is smaller than total file size ({}). " - "Was something appended to the file? Trying to parse it anyways.".format(self.filesize, self.buff_size)) + logger.warning( + "Declared filesize ({}) is smaller than total file size ({}). " + "Was something appended to the file? Trying to parse it anyways.".format( + self.filesize, self.buff_size + ) + ) # Not that severe of an error, we have plenty files where this is not # set correctly if axml_header.type != RES_XML_TYPE: self.axml_tampered = True - logger.warning("AXML file has an unusual resource type! " - "Malware likes to to such stuff to anti androguard! " - "But we try to parse it anyways. Resource Type: 0x{:04x}".format(axml_header.type)) + logger.warning( + "AXML file has an unusual resource type! " + "Malware likes to to such stuff to anti androguard! " + "But we try to parse it anyways. Resource Type: 0x{:04x}".format( + axml_header.type + ) + ) # Now we parse the STRING POOL try: header = ARSCHeader(self.buff, expected_type=RES_STRING_POOL_TYPE) logger.debug("STRING_POOL {}".format(header)) except ResParserError as e: - logger.error("Error parsing resource header of string pool: {}".format(e)) + logger.error( + "Error parsing resource header of string pool: {}".format(e) + ) self._valid = False return if header.header_size != 0x1C: - logger.error("This does not look like an AXML file. String chunk header size does not equal 28! header size = {}".format(header.header_size)) + logger.error( + "This does not look like an AXML file. String chunk header size does not equal 28! header size = {}".format( + header.header_size + ) + ) self._valid = False return @@ -532,57 +608,89 @@ class AXMLParser: # Check size: < 8 bytes mean that the chunk is not complete # Should be aligned to 4 bytes. if h.size < 8 or (h.size % 4) != 0: - logger.error("Invalid chunk size in chunk XML_RESOURCE_MAP") + logger.error( + "Invalid chunk size in chunk XML_RESOURCE_MAP" + ) self._valid = False return for i in range((h.size - h.header_size) // 4): - self.m_resourceIDs.append(unpack(' RES_XML_LAST_CHUNK_TYPE: + if ( + h.type < RES_XML_FIRST_CHUNK_TYPE + or h.type > RES_XML_LAST_CHUNK_TYPE + ): # h.size is the size of the whole chunk including the header. # We read already 8 bytes of the header, thus we need to # subtract them. - logger.error("Not a XML resource chunk type: 0x{:04x}. Skipping {} bytes".format(h.type, h.size)) + logger.error( + "Not a XML resource chunk type: 0x{:04x}. Skipping {} bytes".format( + h.type, h.size + ) + ) self.buff.seek(h.end) continue # Check that we read a correct header if h.header_size != 0x10: - logger.error("XML Resource Type Chunk header size does not match 16! " \ - "At chunk type 0x{:04x}, declared header size=0x{:04x}, chunk size=0x{:04x}".format(h.type, h.header_size, h.size)) + logger.error( + "XML Resource Type Chunk header size does not match 16! " + "At chunk type 0x{:04x}, declared header size=0x{:04x}, chunk size=0x{:04x}".format( + h.type, h.header_size, h.size + ) + ) self.buff.seek(h.end) continue # Line Number of the source file, only used as meta information - self.m_lineNumber, = unpack(' uri {}: '{}'".format(prefix, s_prefix, uri, s_uri)) + logger.debug( + "Start of Namespace mapping: prefix {}: '{}' --> uri {}: '{}'".format( + prefix, s_prefix, uri, s_uri + ) + ) if s_uri == '': - logger.warning("Namespace prefix '{}' resolves to empty URI. " - "This might be a packer.".format(s_prefix)) + logger.warning( + "Namespace prefix '{}' resolves to empty URI. " + "This might be a packer.".format(s_prefix) + ) if (prefix, uri) in self.namespaces: - logger.debug("Namespace mapping ({}, {}) already seen! " - "This is usually not a problem but could indicate packers or broken AXML compilers.".format(prefix, uri)) + logger.debug( + "Namespace mapping ({}, {}) already seen! " + "This is usually not a problem but could indicate packers or broken AXML compilers.".format( + prefix, uri + ) + ) self.namespaces.append((prefix, uri)) # We can continue with the next chunk, as we store the namespace @@ -591,15 +699,17 @@ class AXMLParser: if h.type == RES_XML_END_NAMESPACE_TYPE: # END_PREFIX contains again prefix and uri field - prefix, = unpack('> 16) - 1 self.m_attribute_count = attributeCount & 0xFFFF @@ -642,20 +752,26 @@ class AXMLParser: # * Type # * Data for j in range(0, ATTRIBUTE_LENGTH): - self.m_attributes.append(unpack('> 24 self.m_event = START_TAG break if h.type == RES_XML_END_ELEMENT_TYPE: - self.m_namespaceUri, = unpack(' uint32_t index - self.m_name, = unpack(' Union[str,None]: + def comment(self) -> Union[str, None]: """ Return the comment at the current position or None if no comment is given @@ -719,7 +841,9 @@ class AXMLParser: """ Return the Namespace URI (if any) as a String for the current tag """ - if self.m_name == -1 or (self.m_event != START_TAG and self.m_event != END_TAG): + if self.m_name == -1 or ( + self.m_event != START_TAG and self.m_event != END_TAG + ): return '' # No Namespace @@ -729,7 +853,7 @@ class AXMLParser: return self.sb[self.m_namespaceUri] @property - def nsmap(self) -> dict[str,str]: + def nsmap(self) -> dict[str, str]: """ Returns the current namespace mapping as a dictionary @@ -808,7 +932,7 @@ class AXMLParser: return self.m_attribute_count - def getAttributeUri(self, index:int): + def getAttributeUri(self, index: int): """ Returns the numeric ID for the namespace URI of an attribute """ @@ -819,7 +943,7 @@ class AXMLParser: return uri - def getAttributeNamespace(self, index:int): + def getAttributeNamespace(self, index: int): """ Return the Namespace URI (if any) for the attribute """ @@ -833,7 +957,7 @@ class AXMLParser: return self.sb[uri] - def getAttributeName(self, index:int): + def getAttributeName(self, index: int): """ Returns the String which represents the attribute name """ @@ -846,8 +970,9 @@ class AXMLParser: if name < len(self.m_resourceIDs): attr = self.m_resourceIDs[name] if attr in public.SYSTEM_RESOURCES['attributes']['inverse']: - res = public.SYSTEM_RESOURCES['attributes']['inverse'][attr].replace("_", - ":") + res = public.SYSTEM_RESOURCES['attributes']['inverse'][ + attr + ].replace("_", ":") if res != self.sb[name]: self.packerwarning = True @@ -857,7 +982,7 @@ class AXMLParser: res = 'android:UNKNOWN_SYSTEM_ATTRIBUTE_{:08x}'.format(attr) return res - def getAttributeValueType(self, index:int): + def getAttributeValueType(self, index: int): """ Return the type of the attribute at the given index @@ -868,7 +993,7 @@ class AXMLParser: offset = self._get_attribute_offset(index) return self.m_attributes[offset + ATTRIBUTE_IX_VALUE_TYPE] - def getAttributeValueData(self, index:int): + def getAttributeValueData(self, index: int): """ Return the data of the attribute at the given index @@ -898,7 +1023,9 @@ class AXMLParser: return '' -def format_value(_type: int, _data: int, lookup_string=lambda ix: "") -> str: +def format_value( + _type: int, _data: int, lookup_string=lambda ix: "" +) -> str: """ Format a value based on type and data. By default, no strings are looked up and "" is returned. @@ -938,10 +1065,15 @@ def format_value(_type: int, _data: int, lookup_string=lambda ix: "") -> return "true" elif _type == TYPE_DIMENSION: - return "{:f}{}".format(complexToFloat(_data), DIMENSION_UNITS[_data & COMPLEX_UNIT_MASK]) + return "{:f}{}".format( + complexToFloat(_data), DIMENSION_UNITS[_data & COMPLEX_UNIT_MASK] + ) elif _type == TYPE_FRACTION: - return "{:f}{}".format(complexToFloat(_data) * 100, FRACTION_UNITS[_data & COMPLEX_UNIT_MASK]) + return "{:f}{}".format( + complexToFloat(_data) * 100, + FRACTION_UNITS[_data & COMPLEX_UNIT_MASK], + ) elif TYPE_FIRST_COLOR_INT <= _type <= TYPE_LAST_COLOR_INT: return "#%08X" % _data @@ -959,10 +1091,11 @@ class AXMLPrinter: A Reference Implementation can be found at http://androidxref.com/9.0.0_r3/xref/frameworks/base/tools/aapt/XMLNode.cpp """ + __charrange = None __replacement = None - def __init__(self, raw_buff: bytes)->bytes: + def __init__(self, raw_buff: bytes) -> bytes: logger.debug("AXMLPrinter") self.axml = AXMLParser(raw_buff) @@ -986,11 +1119,19 @@ class AXMLPrinter: comment = self.axml.comment if comment: if self.root is None: - logger.warning("Can not attach comment with content '{}' without root!".format(comment)) + logger.warning( + "Can not attach comment with content '{}' without root!".format( + comment + ) + ) else: cur[-1].append(etree.Comment(comment)) - logger.debug("START_TAG: {} (line={})".format(tag, self.axml.m_lineNumber)) + logger.debug( + "START_TAG: {} (line={})".format( + tag, self.axml.m_lineNumber + ) + ) try: elem = etree.Element(tag, nsmap=self.axml.nsmap) @@ -998,20 +1139,33 @@ class AXMLPrinter: logger.error(e) # nsmap= {' */ + while (byteRead != -1) { + data.push(("0" + (byteRead & 0xff).toString(16)).slice(-2)); + /* <---------------- binary to hex ---------------> */ byteRead = stream.read(); } stream.close(); - return data.join(''); + return data.join(""); } - - const jni_struct_array = [ - "reserved0", - "reserved1", - "reserved2", - "reserved3", - "GetVersion", - "DefineClass", - "FindClass", - "FromReflectedMethod", - "FromReflectedField", - "ToReflectedMethod", - "GetSuperclass", - "IsAssignableFrom", - "ToReflectedField", - "Throw", - "ThrowNew", - "ExceptionOccurred", - "ExceptionDescribe", - "ExceptionClear", - "FatalError", - "PushLocalFrame", - "PopLocalFrame", - "NewGlobalRef", - "DeleteGlobalRef", - "DeleteLocalRef", - "IsSameObject", - "NewLocalRef", - "EnsureLocalCapacity", - "AllocObject", - "NewObject", - "NewObjectV", - "NewObjectA", - "GetObjectClass", - "IsInstanceOf", - "GetMethodID", - "CallObjectMethod", - "CallObjectMethodV", - "CallObjectMethodA", - "CallBooleanMethod", - "CallBooleanMethodV", - "CallBooleanMethodA", - "CallByteMethod", - "CallByteMethodV", - "CallByteMethodA", - "CallCharMethod", - "CallCharMethodV", - "CallCharMethodA", - "CallShortMethod", - "CallShortMethodV", - "CallShortMethodA", - "CallIntMethod", - "CallIntMethodV", - "CallIntMethodA", - "CallLongMethod", - "CallLongMethodV", - "CallLongMethodA", - "CallFloatMethod", - "CallFloatMethodV", - "CallFloatMethodA", - "CallDoubleMethod", - "CallDoubleMethodV", - "CallDoubleMethodA", - "CallVoidMethod", - "CallVoidMethodV", - "CallVoidMethodA", - "CallNonvirtualObjectMethod", - "CallNonvirtualObjectMethodV", - "CallNonvirtualObjectMethodA", - "CallNonvirtualBooleanMethod", - "CallNonvirtualBooleanMethodV", - "CallNonvirtualBooleanMethodA", - "CallNonvirtualByteMethod", - "CallNonvirtualByteMethodV", - "CallNonvirtualByteMethodA", - "CallNonvirtualCharMethod", - "CallNonvirtualCharMethodV", - "CallNonvirtualCharMethodA", - "CallNonvirtualShortMethod", - "CallNonvirtualShortMethodV", - "CallNonvirtualShortMethodA", - "CallNonvirtualIntMethod", - "CallNonvirtualIntMethodV", - "CallNonvirtualIntMethodA", - "CallNonvirtualLongMethod", - "CallNonvirtualLongMethodV", - "CallNonvirtualLongMethodA", - "CallNonvirtualFloatMethod", - "CallNonvirtualFloatMethodV", - "CallNonvirtualFloatMethodA", - "CallNonvirtualDoubleMethod", - "CallNonvirtualDoubleMethodV", - "CallNonvirtualDoubleMethodA", - "CallNonvirtualVoidMethod", - "CallNonvirtualVoidMethodV", - "CallNonvirtualVoidMethodA", - "GetFieldID", - "GetObjectField", - "GetBooleanField", - "GetByteField", - "GetCharField", - "GetShortField", - "GetIntField", - "GetLongField", - "GetFloatField", - "GetDoubleField", - "SetObjectField", - "SetBooleanField", - "SetByteField", - "SetCharField", - "SetShortField", - "SetIntField", - "SetLongField", - "SetFloatField", - "SetDoubleField", - "GetStaticMethodID", - "CallStaticObjectMethod", - "CallStaticObjectMethodV", - "CallStaticObjectMethodA", - "CallStaticBooleanMethod", - "CallStaticBooleanMethodV", - "CallStaticBooleanMethodA", - "CallStaticByteMethod", - "CallStaticByteMethodV", - "CallStaticByteMethodA", - "CallStaticCharMethod", - "CallStaticCharMethodV", - "CallStaticCharMethodA", - "CallStaticShortMethod", - "CallStaticShortMethodV", - "CallStaticShortMethodA", - "CallStaticIntMethod", - "CallStaticIntMethodV", - "CallStaticIntMethodA", - "CallStaticLongMethod", - "CallStaticLongMethodV", - "CallStaticLongMethodA", - "CallStaticFloatMethod", - "CallStaticFloatMethodV", - "CallStaticFloatMethodA", - "CallStaticDoubleMethod", - "CallStaticDoubleMethodV", - "CallStaticDoubleMethodA", - "CallStaticVoidMethod", - "CallStaticVoidMethodV", - "CallStaticVoidMethodA", - "GetStaticFieldID", - "GetStaticObjectField", - "GetStaticBooleanField", - "GetStaticByteField", - "GetStaticCharField", - "GetStaticShortField", - "GetStaticIntField", - "GetStaticLongField", - "GetStaticFloatField", - "GetStaticDoubleField", - "SetStaticObjectField", - "SetStaticBooleanField", - "SetStaticByteField", - "SetStaticCharField", - "SetStaticShortField", - "SetStaticIntField", - "SetStaticLongField", - "SetStaticFloatField", - "SetStaticDoubleField", - "NewString", - "GetStringLength", - "GetStringChars", - "ReleaseStringChars", - "NewStringUTF", - "GetStringUTFLength", - "GetStringUTFChars", - "ReleaseStringUTFChars", - "GetArrayLength", - "NewObjectArray", - "GetObjectArrayElement", - "SetObjectArrayElement", - "NewBooleanArray", - "NewByteArray", - "NewCharArray", - "NewShortArray", - "NewIntArray", - "NewLongArray", - "NewFloatArray", - "NewDoubleArray", - "GetBooleanArrayElements", - "GetByteArrayElements", - "GetCharArrayElements", - "GetShortArrayElements", - "GetIntArrayElements", - "GetLongArrayElements", - "GetFloatArrayElements", - "GetDoubleArrayElements", - "ReleaseBooleanArrayElements", - "ReleaseByteArrayElements", - "ReleaseCharArrayElements", - "ReleaseShortArrayElements", - "ReleaseIntArrayElements", - "ReleaseLongArrayElements", - "ReleaseFloatArrayElements", - "ReleaseDoubleArrayElements", - "GetBooleanArrayRegion", - "GetByteArrayRegion", - "GetCharArrayRegion", - "GetShortArrayRegion", - "GetIntArrayRegion", - "GetLongArrayRegion", - "GetFloatArrayRegion", - "GetDoubleArrayRegion", - "SetBooleanArrayRegion", - "SetByteArrayRegion", - "SetCharArrayRegion", - "SetShortArrayRegion", - "SetIntArrayRegion", - "SetLongArrayRegion", - "SetFloatArrayRegion", - "SetDoubleArrayRegion", - "RegisterNatives", - "UnregisterNatives", - "MonitorEnter", - "MonitorExit", - "GetJavaVM", - "GetStringRegion", - "GetStringUTFRegion", - "GetPrimitiveArrayCritical", - "ReleasePrimitiveArrayCritical", - "GetStringCritical", - "ReleaseStringCritical", - "NewWeakGlobalRef", - "DeleteWeakGlobalRef", - "ExceptionCheck", - "NewDirectByteBuffer", - "GetDirectBufferAddress", - "GetDirectBufferCapacity", - "GetObjectRefType" -] + "reserved0", + "reserved1", + "reserved2", + "reserved3", + "GetVersion", + "DefineClass", + "FindClass", + "FromReflectedMethod", + "FromReflectedField", + "ToReflectedMethod", + "GetSuperclass", + "IsAssignableFrom", + "ToReflectedField", + "Throw", + "ThrowNew", + "ExceptionOccurred", + "ExceptionDescribe", + "ExceptionClear", + "FatalError", + "PushLocalFrame", + "PopLocalFrame", + "NewGlobalRef", + "DeleteGlobalRef", + "DeleteLocalRef", + "IsSameObject", + "NewLocalRef", + "EnsureLocalCapacity", + "AllocObject", + "NewObject", + "NewObjectV", + "NewObjectA", + "GetObjectClass", + "IsInstanceOf", + "GetMethodID", + "CallObjectMethod", + "CallObjectMethodV", + "CallObjectMethodA", + "CallBooleanMethod", + "CallBooleanMethodV", + "CallBooleanMethodA", + "CallByteMethod", + "CallByteMethodV", + "CallByteMethodA", + "CallCharMethod", + "CallCharMethodV", + "CallCharMethodA", + "CallShortMethod", + "CallShortMethodV", + "CallShortMethodA", + "CallIntMethod", + "CallIntMethodV", + "CallIntMethodA", + "CallLongMethod", + "CallLongMethodV", + "CallLongMethodA", + "CallFloatMethod", + "CallFloatMethodV", + "CallFloatMethodA", + "CallDoubleMethod", + "CallDoubleMethodV", + "CallDoubleMethodA", + "CallVoidMethod", + "CallVoidMethodV", + "CallVoidMethodA", + "CallNonvirtualObjectMethod", + "CallNonvirtualObjectMethodV", + "CallNonvirtualObjectMethodA", + "CallNonvirtualBooleanMethod", + "CallNonvirtualBooleanMethodV", + "CallNonvirtualBooleanMethodA", + "CallNonvirtualByteMethod", + "CallNonvirtualByteMethodV", + "CallNonvirtualByteMethodA", + "CallNonvirtualCharMethod", + "CallNonvirtualCharMethodV", + "CallNonvirtualCharMethodA", + "CallNonvirtualShortMethod", + "CallNonvirtualShortMethodV", + "CallNonvirtualShortMethodA", + "CallNonvirtualIntMethod", + "CallNonvirtualIntMethodV", + "CallNonvirtualIntMethodA", + "CallNonvirtualLongMethod", + "CallNonvirtualLongMethodV", + "CallNonvirtualLongMethodA", + "CallNonvirtualFloatMethod", + "CallNonvirtualFloatMethodV", + "CallNonvirtualFloatMethodA", + "CallNonvirtualDoubleMethod", + "CallNonvirtualDoubleMethodV", + "CallNonvirtualDoubleMethodA", + "CallNonvirtualVoidMethod", + "CallNonvirtualVoidMethodV", + "CallNonvirtualVoidMethodA", + "GetFieldID", + "GetObjectField", + "GetBooleanField", + "GetByteField", + "GetCharField", + "GetShortField", + "GetIntField", + "GetLongField", + "GetFloatField", + "GetDoubleField", + "SetObjectField", + "SetBooleanField", + "SetByteField", + "SetCharField", + "SetShortField", + "SetIntField", + "SetLongField", + "SetFloatField", + "SetDoubleField", + "GetStaticMethodID", + "CallStaticObjectMethod", + "CallStaticObjectMethodV", + "CallStaticObjectMethodA", + "CallStaticBooleanMethod", + "CallStaticBooleanMethodV", + "CallStaticBooleanMethodA", + "CallStaticByteMethod", + "CallStaticByteMethodV", + "CallStaticByteMethodA", + "CallStaticCharMethod", + "CallStaticCharMethodV", + "CallStaticCharMethodA", + "CallStaticShortMethod", + "CallStaticShortMethodV", + "CallStaticShortMethodA", + "CallStaticIntMethod", + "CallStaticIntMethodV", + "CallStaticIntMethodA", + "CallStaticLongMethod", + "CallStaticLongMethodV", + "CallStaticLongMethodA", + "CallStaticFloatMethod", + "CallStaticFloatMethodV", + "CallStaticFloatMethodA", + "CallStaticDoubleMethod", + "CallStaticDoubleMethodV", + "CallStaticDoubleMethodA", + "CallStaticVoidMethod", + "CallStaticVoidMethodV", + "CallStaticVoidMethodA", + "GetStaticFieldID", + "GetStaticObjectField", + "GetStaticBooleanField", + "GetStaticByteField", + "GetStaticCharField", + "GetStaticShortField", + "GetStaticIntField", + "GetStaticLongField", + "GetStaticFloatField", + "GetStaticDoubleField", + "SetStaticObjectField", + "SetStaticBooleanField", + "SetStaticByteField", + "SetStaticCharField", + "SetStaticShortField", + "SetStaticIntField", + "SetStaticLongField", + "SetStaticFloatField", + "SetStaticDoubleField", + "NewString", + "GetStringLength", + "GetStringChars", + "ReleaseStringChars", + "NewStringUTF", + "GetStringUTFLength", + "GetStringUTFChars", + "ReleaseStringUTFChars", + "GetArrayLength", + "NewObjectArray", + "GetObjectArrayElement", + "SetObjectArrayElement", + "NewBooleanArray", + "NewByteArray", + "NewCharArray", + "NewShortArray", + "NewIntArray", + "NewLongArray", + "NewFloatArray", + "NewDoubleArray", + "GetBooleanArrayElements", + "GetByteArrayElements", + "GetCharArrayElements", + "GetShortArrayElements", + "GetIntArrayElements", + "GetLongArrayElements", + "GetFloatArrayElements", + "GetDoubleArrayElements", + "ReleaseBooleanArrayElements", + "ReleaseByteArrayElements", + "ReleaseCharArrayElements", + "ReleaseShortArrayElements", + "ReleaseIntArrayElements", + "ReleaseLongArrayElements", + "ReleaseFloatArrayElements", + "ReleaseDoubleArrayElements", + "GetBooleanArrayRegion", + "GetByteArrayRegion", + "GetCharArrayRegion", + "GetShortArrayRegion", + "GetIntArrayRegion", + "GetLongArrayRegion", + "GetFloatArrayRegion", + "GetDoubleArrayRegion", + "SetBooleanArrayRegion", + "SetByteArrayRegion", + "SetCharArrayRegion", + "SetShortArrayRegion", + "SetIntArrayRegion", + "SetLongArrayRegion", + "SetFloatArrayRegion", + "SetDoubleArrayRegion", + "RegisterNatives", + "UnregisterNatives", + "MonitorEnter", + "MonitorExit", + "GetJavaVM", + "GetStringRegion", + "GetStringUTFRegion", + "GetPrimitiveArrayCritical", + "ReleasePrimitiveArrayCritical", + "GetStringCritical", + "ReleaseStringCritical", + "NewWeakGlobalRef", + "DeleteWeakGlobalRef", + "ExceptionCheck", + "NewDirectByteBuffer", + "GetDirectBufferAddress", + "GetDirectBufferCapacity", + "GetObjectRefType", +]; /* Calculate the given funcName address from the JNIEnv pointer */ -function getJNIFunctionAdress(jnienv_addr,func_name){ - var offset = jni_struct_array.indexOf(func_name) * Process.pointerSize - - // console.log("offset : 0x" + offset.toString(16)) - - return Memory.readPointer(jnienv_addr.add(offset)) +function getJNIFunctionAdress(jnienv_addr, func_name) { + var offset = jni_struct_array.indexOf(func_name) * Process.pointerSize; + + // console.log("offset : 0x" + offset.toString(16)) + + return Memory.readPointer(jnienv_addr.add(offset)); } - // Hook all function to have an overview of the function called -function hook_all(jnienv_addr){ - jni_struct_array.forEach(function(func_name){ - // Calculating the address of the function - if(!func_name.includes("reserved")) - { - var func_addr = getJNIFunctionAdress(jnienv_addr,func_name) - Interceptor.attach(func_addr,{ - onEnter: function(args){ - console.log("[+] Entered : " + func_name) - } - }) - } - }) +function hook_all(jnienv_addr) { + jni_struct_array.forEach(function (func_name) { + // Calculating the address of the function + if (!func_name.includes("reserved")) { + var func_addr = getJNIFunctionAdress(jnienv_addr, func_name); + Interceptor.attach(func_addr, { + onEnter: function (args) { + console.log("[+] Entered : " + func_name); + }, + }); + } + }); } function inspectObject(obj) { @@ -742,123 +779,147 @@ function inspectObject(obj) { const fields = obj_class.getDeclaredFields(); const methods = obj_class.getMethods(); console.log("Inspecting " + obj.getClass().toString()); - console.log("[+]------------------------------Fields------------------------------:"); - for (var i in fields) - console.log("\t\t" + fields[i].toString()); - console.log("[+]------------------------------Methods-----------------------------:"); - for (var i in methods) - console.log("\t\t" + methods[i].toString()); + console.log( + "[+]------------------------------Fields------------------------------:", + ); + for (var i in fields) console.log("\t\t" + fields[i].toString()); + console.log( + "[+]------------------------------Methods-----------------------------:", + ); + for (var i in methods) console.log("\t\t" + methods[i].toString()); } - //------------------------https://github.com/CreditTone/hooker---------------------------- function classExists(className) { - var exists = false; - try { - var clz = Java.use(className); - exists = true; - } catch(err) { - //console.log(err); - } - return exists; -}; - -function methodInBeat(invokeId, timestamp, methodName, executor) { -var startTime = timestamp; - var androidLogClz = Java.use("android.util.Log"); - var exceptionClz = Java.use("java.lang.Exception"); - var threadClz = Java.use("java.lang.Thread"); - var currentThread = threadClz.currentThread(); - var stackInfo = androidLogClz.getStackTraceString(exceptionClz.$new()); - var str = ("------------startFlag:" + invokeId + ",objectHash:"+executor+",thread(id:" + currentThread.getId() +",name:" + currentThread.getName() + "),timestamp:" + startTime+"---------------\n"); - str += methodName + "\n"; - str += stackInfo.substring(20); - str += ("------------endFlag:" + invokeId + ",usedtime:" + (new Date().getTime() - startTime) +"---------------\n"); -console.log(str); -}; - -function log(str) { - console.log(str); -}; - -function tryGetClass(className) { - var clz = undefined; - try { - clz = Java.use(className); - } catch(e) {} - return clz; + var exists = false; + try { + var clz = Java.use(className); + exists = true; + } catch (err) { + //console.log(err); + } + return exists; } -var containRegExps = new Array() +function methodInBeat(invokeId, timestamp, methodName, executor) { + var startTime = timestamp; + var androidLogClz = Java.use("android.util.Log"); + var exceptionClz = Java.use("java.lang.Exception"); + var threadClz = Java.use("java.lang.Thread"); + var currentThread = threadClz.currentThread(); + var stackInfo = androidLogClz.getStackTraceString(exceptionClz.$new()); + var str = + "------------startFlag:" + + invokeId + + ",objectHash:" + + executor + + ",thread(id:" + + currentThread.getId() + + ",name:" + + currentThread.getName() + + "),timestamp:" + + startTime + + "---------------\n"; + str += methodName + "\n"; + str += stackInfo.substring(20); + str += + "------------endFlag:" + + invokeId + + ",usedtime:" + + (new Date().getTime() - startTime) + + "---------------\n"; + console.log(str); +} -var notContainRegExps = new Array(RegExp(/\.jpg/), RegExp(/\.png/)) +function log(str) { + console.log(str); +} + +function tryGetClass(className) { + var clz = undefined; + try { + clz = Java.use(className); + } catch (e) {} + return clz; +} + +var containRegExps = new Array(); + +var notContainRegExps = new Array(RegExp(/\.jpg/), RegExp(/\.png/)); function check(str) { - str = str.toString(); - if (! (str && str.match)) { - return false; - } - for (var i = 0; i < containRegExps.length; i++) { - if (!str.match(containRegExps[i])) { - return false; - } - } - for (var i = 0; i < notContainRegExps.length; i++) { - if (str.match(notContainRegExps[i])) { - return false; - } - } - return true; + str = str.toString(); + if (!(str && str.match)) { + return false; + } + for (var i = 0; i < containRegExps.length; i++) { + if (!str.match(containRegExps[i])) { + return false; + } + } + for (var i = 0; i < notContainRegExps.length; i++) { + if (str.match(notContainRegExps[i])) { + return false; + } + } + return true; } //------------------------https://github.com/CreditTone/hooker EOF---------------------------- -function sendAppInfo(){ - var context = null - var ActivityThread = Java.use('android.app.ActivityThread'); - var app = ActivityThread.currentApplication(); +function sendAppInfo() { + var context = null; + var ActivityThread = Java.use("android.app.ActivityThread"); + var app = ActivityThread.currentApplication(); - if (app != null) { - context = app.getApplicationContext(); - var app_classname = app.getClass().toString().split(' ')[1]; - - var filesDirectory= context.getFilesDir().getAbsolutePath().toString(); - var cacheDirectory= context.getCacheDir().getAbsolutePath().toString(); - var externalCacheDirectory= context.getExternalCacheDir().getAbsolutePath().toString(); - var codeCacheDirectory= 'getCodeCacheDir' in context ? context.getCodeCacheDir().getAbsolutePath().toString() : 'N/A'; - var obbDir= context.getObbDir().getAbsolutePath().toString(); - var packageCodePath= context.getPackageCodePath().toString(); - var applicationName= app_classname; + if (app != null) { + context = app.getApplicationContext(); + var app_classname = app.getClass().toString().split(" ")[1]; - var info = {}; - info.applicationName = applicationName; - info.filesDirectory = filesDirectory; - info.cacheDirectory = cacheDirectory; - info.externalCacheDirectory = externalCacheDirectory; - info.codeCacheDirectory = codeCacheDirectory; - info.obbDir = obbDir; - info.packageCodePath = packageCodePath; + var filesDirectory = context.getFilesDir().getAbsolutePath().toString(); + var cacheDirectory = context.getCacheDir().getAbsolutePath().toString(); + var externalCacheDirectory = context + .getExternalCacheDir() + .getAbsolutePath() + .toString(); + var codeCacheDirectory = + "getCodeCacheDir" in context + ? context.getCodeCacheDir().getAbsolutePath().toString() + : "N/A"; + var obbDir = context.getObbDir().getAbsolutePath().toString(); + var packageCodePath = context.getPackageCodePath().toString(); + var applicationName = app_classname; - agSysPacket({information: "app", info: info}).send(); + var info = {}; + info.applicationName = applicationName; + info.filesDirectory = filesDirectory; + info.cacheDirectory = cacheDirectory; + info.externalCacheDirectory = externalCacheDirectory; + info.codeCacheDirectory = codeCacheDirectory; + info.obbDir = obbDir; + info.packageCodePath = packageCodePath; - } else { - console.log("No context yet!") - } + agSysPacket({ information: "app", info: info }).send(); + } else { + console.log("No context yet!"); + } } - //------------------------https://github.com/CreditTone/hooker EOF---------------------------- function notifyNewSharedPreference(key, value) { - var k = key; - var v = value; - Java.use('android.app.SharedPreferencesImpl$EditorImpl').putString.overload('java.lang.String', 'java.lang.String').implementation = function(k, v) { - console.log('[SharedPreferencesImpl]', k, '=', v); - return this.putString(k, v); - } + var k = key; + var v = value; + Java.use("android.app.SharedPreferencesImpl$EditorImpl").putString.overload( + "java.lang.String", + "java.lang.String", + ).implementation = function (k, v) { + console.log("[SharedPreferencesImpl]", k, "=", v); + return this.putString(k, v); + }; } // Send some stuff about the app -setTimeout(function() { - sendAppInfo(); +setTimeout(function () { + sendAppInfo(); }, 1000); diff --git a/androguard/pentest/modules/helpers/pinning/ssl.js b/androguard/pentest/modules/helpers/pinning/ssl.js index 57928be4..930f3950 100644 --- a/androguard/pentest/modules/helpers/pinning/ssl.js +++ b/androguard/pentest/modules/helpers/pinning/ssl.js @@ -1,474 +1,854 @@ // Ripped from https://github.com/Ch0pin/medusa/ and modified to fit Androguard packets -colorLog('[+] LOADING HELPERS/PINNING/SSL.JS',{c: Color.Red}); +colorLog("[+] LOADING HELPERS/PINNING/SSL.JS", { c: Color.Red }); // Bypass TrustManager pinning function bypass_trustmanager_pinning() { - var X509TrustManager = Java.use('javax.net.ssl.X509TrustManager'); - var SSLContext = Java.use('javax.net.ssl.SSLContext'); - + var X509TrustManager = Java.use("javax.net.ssl.X509TrustManager"); + var SSLContext = Java.use("javax.net.ssl.SSLContext"); + // TrustManager (Android < 7) var TrustManager = Java.registerClass({ // Implement a custom TrustManager - name: 'dev.asd.test.TrustManager', + name: "dev.asd.test.TrustManager", implements: [X509TrustManager], methods: { checkClientTrusted: function (chain, authType) {}, checkServerTrusted: function (chain, authType) {}, - getAcceptedIssuers: function () {return []; } - } + getAcceptedIssuers: function () { + return []; + }, + }, }); // Prepare the TrustManager array to pass to SSLContext.init() var TrustManagers = [TrustManager.$new()]; // Get a handle on the init() on the SSLContext class var SSLContext_init = SSLContext.init.overload( - '[Ljavax.net.ssl.KeyManager;', '[Ljavax.net.ssl.TrustManager;', 'java.security.SecureRandom'); + "[Ljavax.net.ssl.KeyManager;", + "[Ljavax.net.ssl.TrustManager;", + "java.security.SecureRandom", + ); try { // Override the init method, specifying the custom TrustManager - SSLContext_init.implementation = function(keyManager, trustManager, secureRandom) { - agSysPacket({information: "trustmanager pinning", keyManager: keyManager, trustManager: trustManager, secureRandom: secureRandom}).send(); + SSLContext_init.implementation = function ( + keyManager, + trustManager, + secureRandom, + ) { + agSysPacket({ + information: "trustmanager pinning", + keyManager: keyManager, + trustManager: trustManager, + secureRandom: secureRandom, + }).send(); SSLContext_init.call(this, keyManager, TrustManagers, secureRandom); }; - agSysPacket({information: "trustmanager hooked installed"}).send(); + agSysPacket({ information: "trustmanager hooked installed" }).send(); } catch (err) { - agSysPacket({information: "error: TrustManager (Android < 7) pinner not found"}).send(); + agSysPacket({ + information: "error: TrustManager (Android < 7) pinner not found", + }).send(); } } // OkHTTPv3 (4 bypass) function bypass_okhttp3_pinning() { try { - var okhttp3_Activity = Java.use('okhttp3.CertificatePinner'); + var okhttp3_Activity = Java.use("okhttp3.CertificatePinner"); } catch (err) { - agSysPacket({information: "error: OkHTTPv3 pinner not found"}).send(); + agSysPacket({ information: "error: OkHTTPv3 pinner not found" }).send(); } try { okhttp3_Activity["check$okhttp"].implementation = function (str) { - agSysPacket({information: "Bypassing OkHTTPv3: {1} " + str}).send(); + agSysPacket({ + information: "Bypassing OkHTTPv3: {1} " + str, + }).send(); return; }; - agSysPacket({information: "OkHTTPv3: {1} hooked installed"}).send(); + agSysPacket({ information: "OkHTTPv3: {1} hooked installed" }).send(); } catch (err) { - agSysPacket({information: "error: OkHTTPv3: {1} not found"}).send(); + agSysPacket({ information: "error: OkHTTPv3: {1} not found" }).send(); } try { - okhttp3_Activity.check.overload('java.lang.String', 'java.util.List').implementation = function (str) { - agSysPacket({information: "Bypassing OkHTTPv3 {2}: " + str}).send(); + okhttp3_Activity.check.overload( + "java.lang.String", + "java.util.List", + ).implementation = function (str) { + agSysPacket({ + information: "Bypassing OkHTTPv3 {2}: " + str, + }).send(); return; }; - agSysPacket({information: "OkHTTPv3: {2} hooked installed"}).send(); + agSysPacket({ information: "OkHTTPv3: {2} hooked installed" }).send(); } catch (err) { - agSysPacket({information: "error: OkHTTPv3: {2} not found"}).send(); + agSysPacket({ information: "error: OkHTTPv3: {2} not found" }).send(); } try { // This method of CertificatePinner.check could be found in some old Android app - okhttp3_Activity.check.overload('java.lang.String', 'java.security.cert.Certificate').implementation = function (str) { - agSysPacket({information: "Bypassing OkHTTPv3 {3}: " + str}).send(); + okhttp3_Activity.check.overload( + "java.lang.String", + "java.security.cert.Certificate", + ).implementation = function (str) { + agSysPacket({ + information: "Bypassing OkHTTPv3 {3}: " + str, + }).send(); return; }; - agSysPacket({information: "OkHTTPv3: {3} hooked installed"}).send(); + agSysPacket({ information: "OkHTTPv3: {3} hooked installed" }).send(); } catch (err) { - agSysPacket({information: "error: OkHTTPv3: {3} not found"}).send(); + agSysPacket({ information: "error: OkHTTPv3: {3} not found" }).send(); } try { - okhttp3_Activity.check$okhttp.overload('java.lang.String', 'kotlin.jvm.functions.Function0').implementation = function(a, b) { - agSysPacket({information: "Bypassing OkHTTPv3 {4}: " + a}).send(); - return; + okhttp3_Activity.check$okhttp.overload( + "java.lang.String", + "kotlin.jvm.functions.Function0", + ).implementation = function (a, b) { + agSysPacket({ information: "Bypassing OkHTTPv3 {4}: " + a }).send(); + return; }; - agSysPacket({information: "OkHTTPv3: {4} hooked installed"}).send(); - } catch(err) { - agSysPacket({information: "error: OkHTTPv3: {4} not found"}).send(); + agSysPacket({ information: "OkHTTPv3: {4} hooked installed" }).send(); + } catch (err) { + agSysPacket({ information: "error: OkHTTPv3: {4} not found" }).send(); } } function bypass_trustkit_pinning() { try { - var trustkit_Activity = Java.use('com.datatheorem.android.trustkit.pinning.OkHostnameVerifier'); - trustkit_Activity.verify.overload('java.lang.String', 'javax.net.ssl.SSLSession').implementation = function (str) { - agSysPacket({information: "Bypassing Trustkit {1}: " + str}).send(); + var trustkit_Activity = Java.use( + "com.datatheorem.android.trustkit.pinning.OkHostnameVerifier", + ); + trustkit_Activity.verify.overload( + "java.lang.String", + "javax.net.ssl.SSLSession", + ).implementation = function (str) { + agSysPacket({ + information: "Bypassing Trustkit {1}: " + str, + }).send(); return true; }; - agSysPacket({information: "Trustkit {1} hooked installed"}).send(); + agSysPacket({ information: "Trustkit {1} hooked installed" }).send(); - trustkit_Activity.verify.overload('java.lang.String', 'java.security.cert.X509Certificate').implementation = function (str) { - agSysPacket({information: "Bypassing Trustkit {2}: " + str}).send(); + trustkit_Activity.verify.overload( + "java.lang.String", + "java.security.cert.X509Certificate", + ).implementation = function (str) { + agSysPacket({ + information: "Bypassing Trustkit {2}: " + str, + }).send(); return true; }; - agSysPacket({information: "Trustkit {2} hooked installed"}).send(); + agSysPacket({ information: "Trustkit {2} hooked installed" }).send(); - var trustkit_PinningTrustManager = Java.use('com.datatheorem.android.trustkit.pinning.PinningTrustManager'); - trustkit_PinningTrustManager.checkServerTrusted.overload('[Ljava.security.cert.X509Certificate;', 'java.lang.String').implementation = function(chain, authType) { - agSysPacket({information: "Bypassing Trustkit {3}: " + chain}).send(); + var trustkit_PinningTrustManager = Java.use( + "com.datatheorem.android.trustkit.pinning.PinningTrustManager", + ); + trustkit_PinningTrustManager.checkServerTrusted.overload( + "[Ljava.security.cert.X509Certificate;", + "java.lang.String", + ).implementation = function (chain, authType) { + agSysPacket({ + information: "Bypassing Trustkit {3}: " + chain, + }).send(); }; - agSysPacket({information: "Trustkit {3} hooked installed"}).send(); - + agSysPacket({ information: "Trustkit {3} hooked installed" }).send(); } catch (err) { - agSysPacket({information: "error: Trustkit pinner not found"}).send(); + agSysPacket({ information: "error: Trustkit pinner not found" }).send(); } } -function bypass_trustmanagerimpl_pinning(){ +function bypass_trustmanagerimpl_pinning() { try { // Bypass TrustManagerImpl (Android > 7) {1} var array_list = Java.use("java.util.ArrayList"); - var TrustManagerImpl_Activity_1 = Java.use('com.android.org.conscrypt.TrustManagerImpl'); - TrustManagerImpl_Activity_1.checkTrustedRecursive.implementation = function(certs, ocspData, tlsSctData, host, clientAuth, untrustedChain, trustAnchorChain, used) { - agSysPacket({information: "Bypassing TrustManagerImpl (Android > 7) checkTrustedRecursive check: "+ host}).send(); - return array_list.$new(); - }; - agSysPacket({information: "TrustManagerImpl checkTrustedRecursive hooked installed"}).send(); + var TrustManagerImpl_Activity_1 = Java.use( + "com.android.org.conscrypt.TrustManagerImpl", + ); + TrustManagerImpl_Activity_1.checkTrustedRecursive.implementation = + function ( + certs, + ocspData, + tlsSctData, + host, + clientAuth, + untrustedChain, + trustAnchorChain, + used, + ) { + agSysPacket({ + information: + "Bypassing TrustManagerImpl (Android > 7) checkTrustedRecursive check: " + + host, + }).send(); + return array_list.$new(); + }; + agSysPacket({ + information: + "TrustManagerImpl checkTrustedRecursive hooked installed", + }).send(); } catch (err) { - agSysPacket({information: "error: TrustManagerImpl (Android > 7) checkTrustedRecursive check not found"}).send(); - } + agSysPacket({ + information: + "error: TrustManagerImpl (Android > 7) checkTrustedRecursive check not found", + }).send(); + } // TrustManagerImpl (Android > 7) try { - var TrustManagerImpl = Java.use('com.android.org.conscrypt.TrustManagerImpl'); - TrustManagerImpl.verifyChain.implementation = function (untrustedChain, trustAnchorChain, host, clientAuth, ocspData, tlsSctData) { - agSysPacket({information: "Bypassing TrustManagerImpl verifyChain (Android > 7): ' + host): "+ host}).send(); + var TrustManagerImpl = Java.use( + "com.android.org.conscrypt.TrustManagerImpl", + ); + TrustManagerImpl.verifyChain.implementation = function ( + untrustedChain, + trustAnchorChain, + host, + clientAuth, + ocspData, + tlsSctData, + ) { + agSysPacket({ + information: + "Bypassing TrustManagerImpl verifyChain (Android > 7): ' + host): " + + host, + }).send(); return untrustedChain; }; - agSysPacket({information: "TrustManagerImpl verifyChain hooked installed"}).send(); + agSysPacket({ + information: "TrustManagerImpl verifyChain hooked installed", + }).send(); } catch (err) { - agSysPacket({information: "error: TrustManagerImpl (Android > 7) verifyChain pinner not found"}).send(); + agSysPacket({ + information: + "error: TrustManagerImpl (Android > 7) verifyChain pinner not found", + }).send(); } } function bypass_appcelerator_pinning() { try { - var appcelerator_PinningTrustManager = Java.use('appcelerator.https.PinningTrustManager'); - appcelerator_PinningTrustManager.checkServerTrusted.implementation = function () { - agSysPacket({information: "Bypassing Appcelerator PinningTrustManager"}).send(); - return; - }; - agSysPacket({information: "Appcelerator hooked installed"}).send(); + var appcelerator_PinningTrustManager = Java.use( + "appcelerator.https.PinningTrustManager", + ); + appcelerator_PinningTrustManager.checkServerTrusted.implementation = + function () { + agSysPacket({ + information: "Bypassing Appcelerator PinningTrustManager", + }).send(); + return; + }; + agSysPacket({ information: "Appcelerator hooked installed" }).send(); } catch (err) { - agSysPacket({information: "error: Appcelerator PinningTrustManager pinner not found"}).send(); + agSysPacket({ + information: + "error: Appcelerator PinningTrustManager pinner not found", + }).send(); } } function bypass_conscript_pinning() { try { - var OpenSSLSocketImpl = Java.use('com.android.org.conscrypt.OpenSSLSocketImpl'); - OpenSSLSocketImpl.verifyCertificateChain.implementation = function (certRefs, JavaObject, authMethod) { - agSysPacket({information: "Bypassing OpenSSLSocketImpl Conscrypt"}).send(); + var OpenSSLSocketImpl = Java.use( + "com.android.org.conscrypt.OpenSSLSocketImpl", + ); + OpenSSLSocketImpl.verifyCertificateChain.implementation = function ( + certRefs, + JavaObject, + authMethod, + ) { + agSysPacket({ + information: "Bypassing OpenSSLSocketImpl Conscrypt", + }).send(); }; - agSysPacket({information: "OpenSSLSocketImpl hooked installed"}).send(); + agSysPacket({ + information: "OpenSSLSocketImpl hooked installed", + }).send(); } catch (err) { - agSysPacket({information: "error: OpenSSLSocketImpl Conscrypt pinner not found"}).send(); + agSysPacket({ + information: "error: OpenSSLSocketImpl Conscrypt pinner not found", + }).send(); } - // OpenSSLEngineSocketImpl Conscrypt try { - var OpenSSLEngineSocketImpl_Activity = Java.use('com.android.org.conscrypt.OpenSSLEngineSocketImpl'); - OpenSSLSocketImpl_Activity.verifyCertificateChain.overload('[Ljava.lang.Long;', 'java.lang.String').implementation = function (str1, str2) { - agSysPacket({information: "Bypassing OpenSSLEngineSocketImpl Conscrypt: " + str2}).send(); + var OpenSSLEngineSocketImpl_Activity = Java.use( + "com.android.org.conscrypt.OpenSSLEngineSocketImpl", + ); + OpenSSLSocketImpl_Activity.verifyCertificateChain.overload( + "[Ljava.lang.Long;", + "java.lang.String", + ).implementation = function (str1, str2) { + agSysPacket({ + information: + "Bypassing OpenSSLEngineSocketImpl Conscrypt: " + str2, + }).send(); }; - agSysPacket({information: "OpenSSLEngineSocketImpl hooked installed"}).send(); + agSysPacket({ + information: "OpenSSLEngineSocketImpl hooked installed", + }).send(); } catch (err) { - agSysPacket({information: "error: OpenSSLEngineSocketImpl Conscrypt pinner not found"}).send(); + agSysPacket({ + information: + "error: OpenSSLEngineSocketImpl Conscrypt pinner not found", + }).send(); } try { - var conscrypt_CertPinManager_Activity = Java.use('com.android.org.conscrypt.CertPinManager'); - conscrypt_CertPinManager_Activity.isChainValid.overload('java.lang.String', 'java.util.List').implementation = function (str) { - agSysPacket({information: "Bypassing Conscrypt CertPinManager: " + str}).send(); + var conscrypt_CertPinManager_Activity = Java.use( + "com.android.org.conscrypt.CertPinManager", + ); + conscrypt_CertPinManager_Activity.isChainValid.overload( + "java.lang.String", + "java.util.List", + ).implementation = function (str) { + agSysPacket({ + information: "Bypassing Conscrypt CertPinManager: " + str, + }).send(); return true; }; - agSysPacket({information: "Conscrypt hooked installed"}).send(); + agSysPacket({ information: "Conscrypt hooked installed" }).send(); } catch (err) { - agSysPacket({information: "error: Conscrypt CertPinManager pinner not found"}).send(); + agSysPacket({ + information: "error: Conscrypt CertPinManager pinner not found", + }).send(); } - } function bypass_apacheharmony_pinning() { try { - var OpenSSLSocketImpl_Harmony = Java.use('org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl'); - OpenSSLSocketImpl_Harmony.verifyCertificateChain.implementation = function (asn1DerEncodedCertificateChain, authMethod) { - agSysPacket({information: "Bypassing OpenSSLSocketImpl Apache Harmony"}).send(); - }; - agSysPacket({information: "OpenSSLSocketImpl Apache Harmony hooked installed"}).send(); + var OpenSSLSocketImpl_Harmony = Java.use( + "org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl", + ); + OpenSSLSocketImpl_Harmony.verifyCertificateChain.implementation = + function (asn1DerEncodedCertificateChain, authMethod) { + agSysPacket({ + information: "Bypassing OpenSSLSocketImpl Apache Harmony", + }).send(); + }; + agSysPacket({ + information: "OpenSSLSocketImpl Apache Harmony hooked installed", + }).send(); } catch (err) { - agSysPacket({information: "error: OpenSSLSocketImpl Apache Harmony pinner not found"}).send(); + agSysPacket({ + information: + "error: OpenSSLSocketImpl Apache Harmony pinner not found", + }).send(); } } function bypass_phonegapp_pinning() { // PhoneGap sslCertificateChecker (https://github.com/EddyVerbruggen/SSLCertificateChecker-PhoneGap-Plugin) try { - var phonegap_Activity = Java.use('nl.xservices.plugins.sslCertificateChecker'); - phonegap_Activity.execute.overload('java.lang.String', 'org.json.JSONArray', 'org.apache.cordova.CallbackContext').implementation = function (str) { - agSysPacket({information: "Bypassing PhoneGap sslCertificateChecker: " + str}).send(); + var phonegap_Activity = Java.use( + "nl.xservices.plugins.sslCertificateChecker", + ); + phonegap_Activity.execute.overload( + "java.lang.String", + "org.json.JSONArray", + "org.apache.cordova.CallbackContext", + ).implementation = function (str) { + agSysPacket({ + information: "Bypassing PhoneGap sslCertificateChecker: " + str, + }).send(); return true; }; - agSysPacket({information: "PhoneGap sslCertificateChecker hooked installed"}).send(); + agSysPacket({ + information: "PhoneGap sslCertificateChecker hooked installed", + }).send(); } catch (err) { - agSysPacket({information: "error: PhoneGap sslCertificateChecker pinner not found"}).send(); + agSysPacket({ + information: + "error: PhoneGap sslCertificateChecker pinner not found", + }).send(); } } function bypass_ibm_pinning() { // IBM MobileFirst pinTrustedCertificatePublicKey (double bypass) try { - var WLClient_Activity = Java.use('com.worklight.wlclient.api.WLClient'); - WLClient_Activity.getInstance().pinTrustedCertificatePublicKey.overload('java.lang.String').implementation = function (cert) { - agSysPacket({information: "Bypassing IBM MobileFirst pinTrustedCertificatePublicKey {1}: " + cert}).send(); + var WLClient_Activity = Java.use("com.worklight.wlclient.api.WLClient"); + WLClient_Activity.getInstance().pinTrustedCertificatePublicKey.overload( + "java.lang.String", + ).implementation = function (cert) { + agSysPacket({ + information: + "Bypassing IBM MobileFirst pinTrustedCertificatePublicKey {1}: " + + cert, + }).send(); return; }; - agSysPacket({information: "IBM MobileFirst pinTrustedCertificatePublicKey hooked installed"}).send(); + agSysPacket({ + information: + "IBM MobileFirst pinTrustedCertificatePublicKey hooked installed", + }).send(); - WLClient_Activity.getInstance().pinTrustedCertificatePublicKey.overload('[Ljava.lang.String;').implementation = function (cert) { - agSysPacket({information: "Bypassing IBM MobileFirst pinTrustedCertificatePublicKey {2}: " + cert}).send(); + WLClient_Activity.getInstance().pinTrustedCertificatePublicKey.overload( + "[Ljava.lang.String;", + ).implementation = function (cert) { + agSysPacket({ + information: + "Bypassing IBM MobileFirst pinTrustedCertificatePublicKey {2}: " + + cert, + }).send(); return; }; - agSysPacket({information: "IBM MobileFirst pinTrustedCertificatePublicKey hooked installed"}).send(); - + agSysPacket({ + information: + "IBM MobileFirst pinTrustedCertificatePublicKey hooked installed", + }).send(); } catch (err) { - agSysPacket({information: "error: IBM MobileFirst pinTrustedCertificatePublicKey pinner not found"}).send(); + agSysPacket({ + information: + "error: IBM MobileFirst pinTrustedCertificatePublicKey pinner not found", + }).send(); } } function bypass_ibmworklight_pinning() { // IBM WorkLight (ancestor of MobileFirst) HostNameVerifierWithCertificatePinning (quadruple bypass) try { - var worklight_Activity = Java.use('com.worklight.wlclient.certificatepinning.HostNameVerifierWithCertificatePinning'); - worklight_Activity.verify.overload('java.lang.String', 'javax.net.ssl.SSLSocket').implementation = function (str) { - agSysPacket({information: "Bypassing IBM WorkLight HostNameVerifierWithCertificatePinning {1}: " + str}).send(); + var worklight_Activity = Java.use( + "com.worklight.wlclient.certificatepinning.HostNameVerifierWithCertificatePinning", + ); + worklight_Activity.verify.overload( + "java.lang.String", + "javax.net.ssl.SSLSocket", + ).implementation = function (str) { + agSysPacket({ + information: + "Bypassing IBM WorkLight HostNameVerifierWithCertificatePinning {1}: " + + str, + }).send(); return; }; - worklight_Activity.verify.overload('java.lang.String', 'java.security.cert.X509Certificate').implementation = function (str) { - agSysPacket({information: "Bypassing IBM WorkLight HostNameVerifierWithCertificatePinning {2}: " + str}).send(); + worklight_Activity.verify.overload( + "java.lang.String", + "java.security.cert.X509Certificate", + ).implementation = function (str) { + agSysPacket({ + information: + "Bypassing IBM WorkLight HostNameVerifierWithCertificatePinning {2}: " + + str, + }).send(); return; }; - - worklight_Activity.verify.overload('java.lang.String', '[Ljava.lang.String;', '[Ljava.lang.String;').implementation = function (str) { - agSysPacket({information: "Bypassing IBM WorkLight HostNameVerifierWithCertificatePinning {3}: " + str}).send(); - return; - }; - worklight_Activity.verify.overload('java.lang.String', 'javax.net.ssl.SSLSession').implementation = function (str) { - agSysPacket({information: "Bypassing IBM WorkLight HostNameVerifierWithCertificatePinning {4}: " + str}).send(); - return true; - }; + worklight_Activity.verify.overload( + "java.lang.String", + "[Ljava.lang.String;", + "[Ljava.lang.String;", + ).implementation = function (str) { + agSysPacket({ + information: + "Bypassing IBM WorkLight HostNameVerifierWithCertificatePinning {3}: " + + str, + }).send(); + return; + }; + worklight_Activity.verify.overload( + "java.lang.String", + "javax.net.ssl.SSLSession", + ).implementation = function (str) { + agSysPacket({ + information: + "Bypassing IBM WorkLight HostNameVerifierWithCertificatePinning {4}: " + + str, + }).send(); + return true; + }; } catch (err) { - agSysPacket({information: "error: IBM WorkLight HostNameVerifierWithCertificatePinning pinner not found"}).send(); + agSysPacket({ + information: + "error: IBM WorkLight HostNameVerifierWithCertificatePinning pinner not found", + }).send(); } } function bypass_cwac_pinning() { // CWAC-Netsecurity (unofficial back-port pinner for Android < 4.2) CertPinManager try { - var cwac_CertPinManager_Activity = Java.use('com.commonsware.cwac.netsecurity.conscrypt.CertPinManager'); - cwac_CertPinManager_Activity.isChainValid.overload('java.lang.String', 'java.util.List').implementation = function (str) { - agSysPacket({information: "Bypassing CWAC-Netsecurity CertPinManager: " + str}).send(); + var cwac_CertPinManager_Activity = Java.use( + "com.commonsware.cwac.netsecurity.conscrypt.CertPinManager", + ); + cwac_CertPinManager_Activity.isChainValid.overload( + "java.lang.String", + "java.util.List", + ).implementation = function (str) { + agSysPacket({ + information: + "Bypassing CWAC-Netsecurity CertPinManager: " + str, + }).send(); return true; }; - } catch (err) { - agSysPacket({information: "error: CWAC-Netsecurity CertPinManager pinner not found"}).send(); + agSysPacket({ + information: + "error: CWAC-Netsecurity CertPinManager pinner not found", + }).send(); } } -function bypass_netty_pinning() { +function bypass_netty_pinning() { // Netty FingerprintTrustManagerFactory try { - var netty_FingerprintTrustManagerFactory = Java.use('io.netty.handler.ssl.util.FingerprintTrustManagerFactory'); - //NOTE: sometimes this below implementation could be useful + var netty_FingerprintTrustManagerFactory = Java.use( + "io.netty.handler.ssl.util.FingerprintTrustManagerFactory", + ); + //NOTE: sometimes this below implementation could be useful //var netty_FingerprintTrustManagerFactory = Java.use('org.jboss.netty.handler.ssl.util.FingerprintTrustManagerFactory'); - netty_FingerprintTrustManagerFactory.checkTrusted.implementation = function (type, chain) { - agSysPacket({information: "Bypassing Netty FingerprintTrustManagerFactory"}).send(); - }; - + netty_FingerprintTrustManagerFactory.checkTrusted.implementation = + function (type, chain) { + agSysPacket({ + information: + "Bypassing Netty FingerprintTrustManagerFactory", + }).send(); + }; } catch (err) { - agSysPacket({information: "error: Netty FingerprintTrustManagerFactory pinner not found"}).send(); + agSysPacket({ + information: + "error: Netty FingerprintTrustManagerFactory pinner not found", + }).send(); } } function bypass_worklight_pinning() { // Worklight Androidgap WLCertificatePinningPlugin try { - var androidgap_WLCertificatePinningPlugin_Activity = Java.use('com.worklight.androidgap.plugin.WLCertificatePinningPlugin'); - androidgap_WLCertificatePinningPlugin_Activity.execute.overload('java.lang.String', 'org.json.JSONArray', 'org.apache.cordova.CallbackContext').implementation = function (str) { - agSysPacket({information: "Bypassing Worklight Androidgap WLCertificatePinningPlugin: " + str}).send(); + var androidgap_WLCertificatePinningPlugin_Activity = Java.use( + "com.worklight.androidgap.plugin.WLCertificatePinningPlugin", + ); + androidgap_WLCertificatePinningPlugin_Activity.execute.overload( + "java.lang.String", + "org.json.JSONArray", + "org.apache.cordova.CallbackContext", + ).implementation = function (str) { + agSysPacket({ + information: + "Bypassing Worklight Androidgap WLCertificatePinningPlugin: " + + str, + }).send(); return true; }; - } catch (err) { - agSysPacket({information: "error: Worklight Androidgap WLCertificatePinningPlugin pinner not found"}).send(); + agSysPacket({ + information: + "error: Worklight Androidgap WLCertificatePinningPlugin pinner not found", + }).send(); } } function bypass_squareup_pinning() { try { - var Squareup_CertificatePinner_Activity = Java.use('com.squareup.okhttp.CertificatePinner'); - Squareup_CertificatePinner_Activity.check.overload('java.lang.String', 'java.security.cert.Certificate').implementation = function (str1, str2) { - agSysPacket({information: "Bypassing Squareup CertificatePinner {1}: " + str1}).send(); + var Squareup_CertificatePinner_Activity = Java.use( + "com.squareup.okhttp.CertificatePinner", + ); + Squareup_CertificatePinner_Activity.check.overload( + "java.lang.String", + "java.security.cert.Certificate", + ).implementation = function (str1, str2) { + agSysPacket({ + information: + "Bypassing Squareup CertificatePinner {1}: " + str1, + }).send(); return; }; - Squareup_CertificatePinner_Activity.check.overload('java.lang.String', 'java.util.List').implementation = function (str1, str2) { - agSysPacket({information: "Bypassing Squareup CertificatePinner {2}: " + str1}).send(); + Squareup_CertificatePinner_Activity.check.overload( + "java.lang.String", + "java.util.List", + ).implementation = function (str1, str2) { + agSysPacket({ + information: + "Bypassing Squareup CertificatePinner {2}: " + str1, + }).send(); return; }; - } catch (err) { - agSysPacket({information: "error: Squareup CertificatePinner pinner not found"}).send(); + agSysPacket({ + information: "error: Squareup CertificatePinner pinner not found", + }).send(); } try { - var Squareup_OkHostnameVerifier_Activity = Java.use('com.squareup.okhttp.internal.tls.OkHostnameVerifier'); - Squareup_OkHostnameVerifier_Activity.verify.overload('java.lang.String', 'java.security.cert.X509Certificate').implementation = function (str1, str2) { - agSysPacket({information: "Bypassing Squareup OkHostnameVerifier {1}: " + str1}).send(); + var Squareup_OkHostnameVerifier_Activity = Java.use( + "com.squareup.okhttp.internal.tls.OkHostnameVerifier", + ); + Squareup_OkHostnameVerifier_Activity.verify.overload( + "java.lang.String", + "java.security.cert.X509Certificate", + ).implementation = function (str1, str2) { + agSysPacket({ + information: + "Bypassing Squareup OkHostnameVerifier {1}: " + str1, + }).send(); return true; }; - - Squareup_OkHostnameVerifier_Activity.verify.overload('java.lang.String', 'javax.net.ssl.SSLSession').implementation = function (str1, str2) { - agSysPacket({information: "Bypassing Squareup OkHostnameVerifier {2}: " + str1}).send(); + + Squareup_OkHostnameVerifier_Activity.verify.overload( + "java.lang.String", + "javax.net.ssl.SSLSession", + ).implementation = function (str1, str2) { + agSysPacket({ + information: + "Bypassing Squareup OkHostnameVerifier {2}: " + str1, + }).send(); return true; }; - } catch (err) { - agSysPacket({information: "error: Squareup OkHostnameVerifier pinner not found"}).send(); + agSysPacket({ + information: "error: Squareup OkHostnameVerifier pinner not found", + }).send(); } } function bypass_webview_pinning() { try { // Bypass WebViewClient {1} (deprecated from Android 6) - var AndroidWebViewClient_Activity_1 = Java.use('android.webkit.WebViewClient'); - AndroidWebViewClient_Activity_1.onReceivedSslError.overload('android.webkit.WebView', 'android.webkit.SslErrorHandler', 'android.net.http.SslError').implementation = function(obj1, obj2, obj3) { - agSysPacket({information: "Bypassing Android WebViewClient check {1}"}).send(); + var AndroidWebViewClient_Activity_1 = Java.use( + "android.webkit.WebViewClient", + ); + AndroidWebViewClient_Activity_1.onReceivedSslError.overload( + "android.webkit.WebView", + "android.webkit.SslErrorHandler", + "android.net.http.SslError", + ).implementation = function (obj1, obj2, obj3) { + agSysPacket({ + information: "Bypassing Android WebViewClient check {1}", + }).send(); }; } catch (err) { - agSysPacket({information: "error: Android WebViewClient {1} check not found"}).send(); + agSysPacket({ + information: "error: Android WebViewClient {1} check not found", + }).send(); } try { // Bypass WebViewClient {2} - var AndroidWebViewClient_Activity_2 = Java.use('android.webkit.WebViewClient'); - AndroidWebViewClient_Activity_2.onReceivedSslError.overload('android.webkit.WebView', 'android.webkit.WebResourceRequest', 'android.webkit.WebResourceError').implementation = function(obj1, obj2, obj3) { - agSysPacket({information: "Bypassing Android WebViewClient check {2}"}).send(); + var AndroidWebViewClient_Activity_2 = Java.use( + "android.webkit.WebViewClient", + ); + AndroidWebViewClient_Activity_2.onReceivedSslError.overload( + "android.webkit.WebView", + "android.webkit.WebResourceRequest", + "android.webkit.WebResourceError", + ).implementation = function (obj1, obj2, obj3) { + agSysPacket({ + information: "Bypassing Android WebViewClient check {2}", + }).send(); }; } catch (err) { - agSysPacket({information: "error: Android WebViewClient {2} check not found"}).send(); + agSysPacket({ + information: "error: Android WebViewClient {2} check not found", + }).send(); } try { // Bypass WebViewClient {3} - var AndroidWebViewClient_Activity_3 = Java.use('android.webkit.WebViewClient'); - AndroidWebViewClient_Activity_3.onReceivedError.overload('android.webkit.WebView', 'int', 'java.lang.String', 'java.lang.String').implementation = function(obj1, obj2, obj3, obj4) { - agSysPacket({information: "Bypassing Android WebViewClient check {3}"}).send(); + var AndroidWebViewClient_Activity_3 = Java.use( + "android.webkit.WebViewClient", + ); + AndroidWebViewClient_Activity_3.onReceivedError.overload( + "android.webkit.WebView", + "int", + "java.lang.String", + "java.lang.String", + ).implementation = function (obj1, obj2, obj3, obj4) { + agSysPacket({ + information: "Bypassing Android WebViewClient check {3}", + }).send(); }; } catch (err) { - agSysPacket({information: "error: Android WebViewClient {3} check not found"}).send(); + agSysPacket({ + information: "error: Android WebViewClient {3} check not found", + }).send(); } try { // Bypass WebViewClient {4} - var AndroidWebViewClient_Activity_4 = Java.use('android.webkit.WebViewClient'); - AndroidWebViewClient_Activity_4.onReceivedError.overload('android.webkit.WebView', 'android.webkit.WebResourceRequest', 'android.webkit.WebResourceError').implementation = function(obj1, obj2, obj3) { - agSysPacket({information: "Bypassing Android WebViewClient check {4}"}).send(); + var AndroidWebViewClient_Activity_4 = Java.use( + "android.webkit.WebViewClient", + ); + AndroidWebViewClient_Activity_4.onReceivedError.overload( + "android.webkit.WebView", + "android.webkit.WebResourceRequest", + "android.webkit.WebResourceError", + ).implementation = function (obj1, obj2, obj3) { + agSysPacket({ + information: "Bypassing Android WebViewClient check {4}", + }).send(); }; } catch (err) { - agSysPacket({information: "error: Android WebViewClient {4} check not found"}).send(); + agSysPacket({ + information: "error: Android WebViewClient {4} check not found", + }).send(); } } function bypass_cordova_pinning() { try { - var CordovaWebViewClient_Activity = Java.use('org.apache.cordova.CordovaWebViewClient'); - CordovaWebViewClient_Activity.onReceivedSslError.overload('android.webkit.WebView', 'android.webkit.SslErrorHandler', 'android.net.http.SslError').implementation = function (obj1, obj2, obj3) { - agSysPacket({information: "Bypassing Apache Cordova WebViewClient"}).send(); + var CordovaWebViewClient_Activity = Java.use( + "org.apache.cordova.CordovaWebViewClient", + ); + CordovaWebViewClient_Activity.onReceivedSslError.overload( + "android.webkit.WebView", + "android.webkit.SslErrorHandler", + "android.net.http.SslError", + ).implementation = function (obj1, obj2, obj3) { + agSysPacket({ + information: "Bypassing Apache Cordova WebViewClient", + }).send(); obj3.proceed(); }; - } catch (err) { - agSysPacket({information: "error: Apache Cordova WebViewClient pinner not found"}).send(); + agSysPacket({ + information: "error: Apache Cordova WebViewClient pinner not found", + }).send(); } } function bypass_boye_pinning() { try { - var boye_AbstractVerifier = Java.use('ch.boye.httpclientandroidlib.conn.ssl.AbstractVerifier'); + var boye_AbstractVerifier = Java.use( + "ch.boye.httpclientandroidlib.conn.ssl.AbstractVerifier", + ); boye_AbstractVerifier.verify.implementation = function (host, ssl) { - agSysPacket({information: "Bypassing Boye AbstractVerifier: " + host}).send(); + agSysPacket({ + information: "Bypassing Boye AbstractVerifier: " + host, + }).send(); }; - } catch (err) { - agSysPacket({information: "error: Boye AbstractVerifier pinner not found"}).send(); + agSysPacket({ + information: "error: Boye AbstractVerifier pinner not found", + }).send(); } } function bypass_apache_pinning() { try { - var apache_AbstractVerifier = Java.use('org.apache.http.conn.ssl.AbstractVerifier'); - apache_AbstractVerifier.verify.implementation = function(a, b, c, d) { - agSysPacket({information: "Bypassing Apache AbstractVerifier check: " + a}).send(); + var apache_AbstractVerifier = Java.use( + "org.apache.http.conn.ssl.AbstractVerifier", + ); + apache_AbstractVerifier.verify.implementation = function (a, b, c, d) { + agSysPacket({ + information: "Bypassing Apache AbstractVerifier check: " + a, + }).send(); return; }; } catch (err) { - agSysPacket({information: "error: Apache AbstractVerifier check not found"}).send(); + agSysPacket({ + information: "error: Apache AbstractVerifier check not found", + }).send(); } } function bypass_chromecronet_pinning() { - try { - var CronetEngineBuilderImpl_Activity = Java.use("org.chromium.net.impl.CronetEngineBuilderImpl"); + try { + var CronetEngineBuilderImpl_Activity = Java.use( + "org.chromium.net.impl.CronetEngineBuilderImpl", + ); // Setting argument to TRUE (default is TRUE) to disable Public Key pinning for local trust anchors - CronetEngine_Activity.enablePublicKeyPinningBypassForLocalTrustAnchors.overload('boolean').implementation = function(a) { - agSysPacket({information: "Bypassing Chromium Cronet {1}"}).send(); + CronetEngine_Activity.enablePublicKeyPinningBypassForLocalTrustAnchors.overload( + "boolean", + ).implementation = function (a) { + agSysPacket({ + information: "Bypassing Chromium Cronet {1}", + }).send(); - var cronet_obj_1 = CronetEngine_Activity.enablePublicKeyPinningBypassForLocalTrustAnchors.call(this, true); + var cronet_obj_1 = + CronetEngine_Activity.enablePublicKeyPinningBypassForLocalTrustAnchors.call( + this, + true, + ); return cronet_obj_1; }; // Bypassing Chromium Cronet pinner - CronetEngine_Activity.addPublicKeyPins.overload('java.lang.String', 'java.util.Set', 'boolean', 'java.util.Date').implementation = function(hostName, pinsSha256, includeSubdomains, expirationDate) { - agSysPacket({information: "Bypassing Chromium Cronet {2}"}).send(); - var cronet_obj_2 = CronetEngine_Activity.addPublicKeyPins.call(this, hostName, pinsSha256, includeSubdomains, expirationDate); + CronetEngine_Activity.addPublicKeyPins.overload( + "java.lang.String", + "java.util.Set", + "boolean", + "java.util.Date", + ).implementation = function ( + hostName, + pinsSha256, + includeSubdomains, + expirationDate, + ) { + agSysPacket({ + information: "Bypassing Chromium Cronet {2}", + }).send(); + var cronet_obj_2 = CronetEngine_Activity.addPublicKeyPins.call( + this, + hostName, + pinsSha256, + includeSubdomains, + expirationDate, + ); return cronet_obj_2; }; } catch (err) { - agSysPacket({information: "error: Chromium Cronet pinner not found"}).send(); + agSysPacket({ + information: "error: Chromium Cronet pinner not found", + }).send(); } } function bypass_flutter_pinning() { try { // Bypass HttpCertificatePinning.check {1} - var HttpCertificatePinning_Activity = Java.use('diefferson.http_certificate_pinning.HttpCertificatePinning'); - HttpCertificatePinning_Activity.checkConnexion.overload("java.lang.String", "java.util.List", "java.util.Map", "int", "java.lang.String").implementation = function (a, b, c ,d, e) { - agSysPacket({information: "Bypassing Flutter HttpCertificatePinning :" + a}).send(); + var HttpCertificatePinning_Activity = Java.use( + "diefferson.http_certificate_pinning.HttpCertificatePinning", + ); + HttpCertificatePinning_Activity.checkConnexion.overload( + "java.lang.String", + "java.util.List", + "java.util.Map", + "int", + "java.lang.String", + ).implementation = function (a, b, c, d, e) { + agSysPacket({ + information: "Bypassing Flutter HttpCertificatePinning :" + a, + }).send(); return true; }; } catch (err) { - agSysPacket({information: "error: Flutter HttpCertificatePinning pinner not found"}).send(); + agSysPacket({ + information: + "error: Flutter HttpCertificatePinning pinner not found", + }).send(); } try { // Bypass SslPinningPlugin.check {2} - var SslPinningPlugin_Activity = Java.use('com.macif.plugin.sslpinningplugin.SslPinningPlugin'); - SslPinningPlugin_Activity.checkConnexion.overload("java.lang.String", "java.util.List", "java.util.Map", "int", "java.lang.String").implementation = function (a, b, c ,d, e) { - agSysPacket({information: "Bypassing Flutter SslPinningPlugin :" + a}).send(); + var SslPinningPlugin_Activity = Java.use( + "com.macif.plugin.sslpinningplugin.SslPinningPlugin", + ); + SslPinningPlugin_Activity.checkConnexion.overload( + "java.lang.String", + "java.util.List", + "java.util.Map", + "int", + "java.lang.String", + ).implementation = function (a, b, c, d, e) { + agSysPacket({ + information: "Bypassing Flutter SslPinningPlugin :" + a, + }).send(); return true; }; } catch (err) { - agSysPacket({information: "error: Flutter SslPinningPlugin pinner not found"}).send(); + agSysPacket({ + information: "error: Flutter SslPinningPlugin pinner not found", + }).send(); } } - function rudimentaryFix(typeName) { // This is a improvable rudimentary fix, if not works you can patch it manually - if (typeName === undefined){ + if (typeName === undefined) { return; - } else if (typeName === 'boolean') { + } else if (typeName === "boolean") { return true; } else { return null; @@ -482,13 +862,21 @@ function rudimentaryFix(typeName) { /////////////////////////////////////////////////////////////////////////////// function bypass_sslpeerunverifiedexception_pinning() { try { - var UnverifiedCertError = Java.use('javax.net.ssl.SSLPeerUnverifiedException'); + var UnverifiedCertError = Java.use( + "javax.net.ssl.SSLPeerUnverifiedException", + ); UnverifiedCertError.$init.implementation = function (str) { - console.log('Unexpected SSLPeerUnverifiedException occurred, trying to patch it dynamically..'); + console.log( + "Unexpected SSLPeerUnverifiedException occurred, trying to patch it dynamically..", + ); try { - var stackTrace = Java.use('java.lang.Thread').currentThread().getStackTrace(); - var exceptionStackIndex = stackTrace.findIndex(stack => - stack.getClassName() === "javax.net.ssl.SSLPeerUnverifiedException" + var stackTrace = Java.use("java.lang.Thread") + .currentThread() + .getStackTrace(); + var exceptionStackIndex = stackTrace.findIndex( + (stack) => + stack.getClassName() === + "javax.net.ssl.SSLPeerUnverifiedException", ); // Retrieve the method raising the SSLPeerUnverifiedException var callingFunctionStack = stackTrace[exceptionStackIndex + 1]; @@ -496,68 +884,120 @@ function bypass_sslpeerunverifiedexception_pinning() { var methodName = callingFunctionStack.getMethodName(); var callingClass = Java.use(className); var callingMethod = callingClass[methodName]; - console.log('Attempting to bypass uncommon SSL Pinning method on: '+className+'.'+methodName + '->' + callingClass + ' ' + callingMethod); + console.log( + "Attempting to bypass uncommon SSL Pinning method on: " + + className + + "." + + methodName + + "->" + + callingClass + + " " + + callingMethod, + ); // Skip it when already patched by Frida if (callingMethod.implementation) { console.log("Already patched"); - return; + return; } // Trying to patch the uncommon SSL Pinning method via implementation var returnTypeName = callingMethod.returnType.type; - callingMethod.implementation = function() { + callingMethod.implementation = function () { console.log("Automatic patch"); rudimentaryFix(returnTypeName); }; } catch (e) { // Dynamic patching via implementation does not works, then trying via function overloading - //console.log('[!] The uncommon SSL Pinning method has more than one overload); + //console.log('[!] The uncommon SSL Pinning method has more than one overload); if (String(e).includes(".overload")) { var splittedList = String(e).split(".overload"); - for (let i=2; i>> Should we go back to pickling or proceed further with the dataset ?<<< """ - def __init__(self, export_ipython:bool=False, db_url:str='sqlite:///androguard.db') -> None: + def __init__( + self, + export_ipython: bool = False, + db_url: str = 'sqlite:///androguard.db', + ) -> None: """ Create a new Session object @@ -45,7 +48,7 @@ class Session: self.table_session.insert(dict(id=self.session_id)) logger.info("Creating new session [{}]".format(self.session_id)) - def save(self, filename:Union[str,None]=None) -> None: + def save(self, filename: Union[str, None] = None) -> None: """ Save the current session, see also :func:`~androguard.session.Save`. """ @@ -101,13 +104,27 @@ class Session: def insert_event(self, call, callee, params, ret): self.table_pentest.insert( - dict(session_id=str(self.session_id), call=call, callee=callee, params=params, ret=ret)) + dict( + session_id=str(self.session_id), + call=call, + callee=callee, + params=params, + ret=ret, + ) + ) def insert_system_event(self, call, callee, information, params): self.table_system.insert( - dict(session_id=str(self.session_id), call=call, callee=callee, information=information, params=params)) + dict( + session_id=str(self.session_id), + call=call, + callee=callee, + information=information, + params=params, + ) + ) - def addAPK(self, filename: str, data:bytes) -> tuple[str, apk.APK]: + def addAPK(self, filename: str, data: bytes) -> tuple[str, apk.APK]: """ Add an APK file to the Session and run analysis on it. @@ -119,7 +136,13 @@ class Session: logger.info("add APK {}:{}".format(filename, digest)) self.table_information.insert( - dict(session_id=str(self.session_id), filename=filename, digest=digest, type="APK")) + dict( + session_id=str(self.session_id), + filename=filename, + digest=digest, + type="APK", + ) + ) newapk = apk.APK(data, True) self.analyzed_apk[digest] = [newapk] @@ -140,7 +163,13 @@ class Session: logger.info("added APK {}:{}".format(filename, digest)) return digest, newapk - def addDEX(self, filename: str, data: bytes, dx:Union[Analysis,None]=None, postpone_xref:bool=False) -> tuple[str, dex.DEX, Analysis]: + def addDEX( + self, + filename: str, + data: bytes, + dx: Union[Analysis, None] = None, + postpone_xref: bool = False, + ) -> tuple[str, dex.DEX, Analysis]: """ Add a DEX file to the Session and run analysis. @@ -154,7 +183,13 @@ class Session: logger.info("add DEX:{}".format(digest)) self.table_information.insert( - dict(session_id=str(self.session_id), filename=filename, digest=digest, type="DEX")) + dict( + session_id=str(self.session_id), + filename=filename, + digest=digest, + type="DEX", + ) + ) logger.debug("Parsing format ...") d = dex.DEX(data) @@ -185,7 +220,9 @@ class Session: return digest, d, dx - def addODEX(self, filename:str, data:bytes, dx:Union[Analysis,None]=None) -> tuple[str, dex.ODEX, Analysis]: + def addODEX( + self, filename: str, data: bytes, dx: Union[Analysis, None] = None + ) -> tuple[str, dex.ODEX, Analysis]: """ Add an ODEX file to the session and run the analysis """ @@ -193,7 +230,13 @@ class Session: logger.info("add ODEX:%s" % digest) self.table_information.insert( - dict(session_id=str(self.session_id), filename=filename, digest=digest, type="ODEX")) + dict( + session_id=str(self.session_id), + filename=filename, + digest=digest, + type="ODEX", + ) + ) d = dex.ODEX(data) logger.debug("added ODEX:%s" % digest) @@ -221,7 +264,12 @@ class Session: return digest, d, dx - def add(self, filename: str, raw_data:Union[bytes,None]=None, dx:Union[Analysis,None]=None) -> Union[str,None]: + def add( + self, + filename: str, + raw_data: Union[bytes, None] = None, + dx: Union[Analysis, None] = None, + ) -> Union[str, None]: """ Generic method to add a file to the session. @@ -259,7 +307,9 @@ class Session: return digest - def get_classes(self) -> Iterator[tuple[int, str, str, list[dex.ClassDefItem]]]: + def get_classes( + self, + ) -> Iterator[tuple[int, str, str, list[dex.ClassDefItem]]]: """ Returns all Java Classes from the DEX objects as an array of DEX files. """ @@ -293,7 +343,9 @@ class Session: """ return current_class.CM.vm - def get_filename_by_class(self, current_class: dex.ClassDefItem) -> Union[str,None]: + def get_filename_by_class( + self, current_class: dex.ClassDefItem + ) -> Union[str, None]: """ Returns the filename of the DEX file where the class is in. @@ -309,7 +361,9 @@ class Session: return self.analyzed_digest[digest] return None - def get_digest_by_class(self, current_class: dex.ClassDefItem) -> Union[str,None]: + def get_digest_by_class( + self, current_class: dex.ClassDefItem + ) -> Union[str, None]: """ Return the SHA256 hash of the object containing the ClassDefItem @@ -322,7 +376,9 @@ class Session: return digest return None - def get_strings(self) -> Iterator[tuple[str, str, dict[str,StringAnalysis]]]: + def get_strings( + self, + ) -> Iterator[tuple[str, str, dict[str, StringAnalysis]]]: """ Yields all StringAnalysis for all unique Analysis objects """ @@ -331,7 +387,9 @@ class Session: if dx in seen: continue seen.append(dx) - yield digest, self.analyzed_digest[digest], dx.get_strings_analysis() + yield digest, self.analyzed_digest[ + digest + ], dx.get_strings_analysis() def get_nb_strings(self) -> int: """ @@ -354,7 +412,11 @@ class Session: for digest, a in self.analyzed_apk.items(): yield digest, a - def get_objects_apk(self, filename:Union[str,None]=None, digest:Union[str,None]=None) -> Iterator[tuple[apk.APK, list[dex.DEX], Analysis]]: + def get_objects_apk( + self, + filename: Union[str, None] = None, + digest: Union[str, None] = None, + ) -> Iterator[tuple[apk.APK, list[dex.DEX], Analysis]]: """ Returns APK, DEX and Analysis of a specified APK. diff --git a/androguard/ui/__init__.py b/androguard/ui/__init__.py index d19c425d..447c3007 100644 --- a/androguard/ui/__init__.py +++ b/androguard/ui/__init__.py @@ -1,26 +1,35 @@ import os import queue +from loguru import logger from prompt_toolkit import Application from prompt_toolkit.application import get_app from prompt_toolkit.filters import Condition from prompt_toolkit.formatted_text import StyleAndTextTuples -from prompt_toolkit.layout import HSplit, Layout, VSplit, Window, FloatContainer, Float, ConditionalContainer, UIControl, UIContent from prompt_toolkit.key_binding import KeyBindings, merge_key_bindings +from prompt_toolkit.layout import ( + ConditionalContainer, + Float, + FloatContainer, + HSplit, + Layout, + UIContent, + UIControl, + VSplit, + Window, +) from prompt_toolkit.styles import Style -from loguru import logger - -from androguard.ui.selection import SelectionViewList -from androguard.ui.widget.transactions import TransactionFrame -from androguard.ui.widget.toolbar import StatusToolbar -from androguard.ui.widget.filters import FiltersPanel -from androguard.ui.widget.help import HelpPanel -from androguard.ui.widget.details import DetailsFrame +from androguard.pentest import Message from androguard.ui.data_types import DisplayTransaction from androguard.ui.filter import Filter +from androguard.ui.selection import SelectionViewList +from androguard.ui.widget.details import DetailsFrame +from androguard.ui.widget.filters import FiltersPanel +from androguard.ui.widget.help import HelpPanel +from androguard.ui.widget.toolbar import StatusToolbar +from androguard.ui.widget.transactions import TransactionFrame -from androguard.pentest import Message class DummyControl(UIControl): """ @@ -44,7 +53,7 @@ class DummyControl(UIControl): class DynamicUI: - def __init__(self, input_queue): + def __init__(self, input_queue): logger.info("Starting the Terminal UI") self.filter: Filter | None = None @@ -61,7 +70,6 @@ class DynamicUI: self.resize_components(os.get_terminal_size()) - def run(self): self.focusable = [self.transaction_table, self.details_pane] self.focus_index = 0 @@ -77,7 +85,11 @@ class DynamicUI: @kb1.add('s-tab') def _(event): - self.focus_index = len(self.focusable) - 1 if self.focus_index == 0 else self.focus_index -1 + self.focus_index = ( + len(self.focusable) - 1 + if self.focus_index == 0 + else self.focus_index - 1 + ) for i, f in enumerate(self.focusable): f.activated = i == self.focus_index @@ -86,12 +98,14 @@ class DynamicUI: key_bindings=kb1, children=[ self.transaction_table, - VSplit([ - self.details_pane, - # self.structure_pane, - ]), + VSplit( + [ + self.details_pane, + # self.structure_pane, + ] + ), StatusToolbar(self.transactions, self.filter_panel), - Window(content=dummy_control) + Window(content=dummy_control), ], ) @@ -107,41 +121,52 @@ class DynamicUI: def show_help(): return self.help_panel.visible - layout = Layout( container=FloatContainer( content=main_layout, floats=[ - Float(top=10, content=ConditionalContainer(content=self.filter_panel, filter=show_filters)), - Float(top=10, content=ConditionalContainer(content=self.help_panel, filter=show_help)), - ] + Float( + top=10, + content=ConditionalContainer( + content=self.filter_panel, filter=show_filters + ), + ), + Float( + top=10, + content=ConditionalContainer( + content=self.help_panel, filter=show_help + ), + ), + ], ) ) - style = Style([ - ('field.selected', 'ansiblack bg:ansiwhite'), - ('field.default', 'fg:ansiwhite'), - ('frame.label', 'fg:ansiwhite'), - ('frame.border', 'fg:ansiwhite'), - ('frame.border.selected', 'fg:ansibrightgreen'), - ('transaction.heading', 'ansiblack bg:ansigray'), - ('transaction.selected', 'ansiblack bg:ansiwhite'), - ('transaction.default', 'fg:ansiwhite'), - ('transaction.unsupported', 'fg:ansibrightblack'), - ('transaction.error', 'fg:ansired'), - ('transaction.no_aidl', 'fg:ansiwhite'), - ('transaction.oneway', 'fg:ansimagenta'), - ('transaction.request', 'fg:ansicyan'), - ('transaction.response', 'fg:ansiyellow'), - ('hexdump.default', 'fg:ansiwhite'), - ('hexdump.selected', 'fg:ansiblack bg:ansiwhite'), - ('toolbar', 'bg:ansigreen'), - ('toolbar.text', 'fg:ansiblack'), - ('dialog', 'fg:ansiblack bg:ansiwhite'), - ('dialog frame.border', 'fg:ansiblack bg:ansiwhite'), - ('dialog frame.label', 'fg:ansiblack bg:ansiwhite'), - ('dialogger.textarea', 'fg:ansiwhite bg:ansiblack'), - ]) + style = Style( + [ + ('field.selected', 'ansiblack bg:ansiwhite'), + ('field.default', 'fg:ansiwhite'), + ('frame.label', 'fg:ansiwhite'), + ('frame.border', 'fg:ansiwhite'), + ('frame.border.selected', 'fg:ansibrightgreen'), + ('transaction.heading', 'ansiblack bg:ansigray'), + ('transaction.selected', 'ansiblack bg:ansiwhite'), + ('transaction.default', 'fg:ansiwhite'), + ('transaction.unsupported', 'fg:ansibrightblack'), + ('transaction.error', 'fg:ansired'), + ('transaction.no_aidl', 'fg:ansiwhite'), + ('transaction.oneway', 'fg:ansimagenta'), + ('transaction.request', 'fg:ansicyan'), + ('transaction.response', 'fg:ansiyellow'), + ('hexdump.default', 'fg:ansiwhite'), + ('hexdump.selected', 'fg:ansiblack bg:ansiwhite'), + ('toolbar', 'bg:ansigreen'), + ('toolbar.text', 'fg:ansiblack'), + ('dialog', 'fg:ansiblack bg:ansiwhite'), + ('dialog frame.border', 'fg:ansiblack bg:ansiwhite'), + ('dialog frame.label', 'fg:ansiblack bg:ansiwhite'), + ('dialogger.textarea', 'fg:ansiwhite bg:ansiblack'), + ] + ) kb = KeyBindings() @@ -150,13 +175,11 @@ class DynamicUI: logger.info("Q pressed. App exiting.") event.app.exit(exception=KeyboardInterrupt, style='class:aborting') - @kb.add('h', filter=~modal_panel_visible | show_help) @kb.add("enter", filter=show_help) def _(event): self.help_panel.visible = not self.help_panel.visible - @kb.add('f', filter=~modal_panel_visible) @kb.add("enter", filter=show_filters) def _(event): @@ -165,7 +188,9 @@ class DynamicUI: get_app().layout.focus(self.filter_panel.interface_textarea) else: self.filter = self.filter_panel.filter() - self.transactions.assign([t for t in self.all_transactions if self.filter.passes(t)]) + self.transactions.assign( + [t for t in self.all_transactions if self.filter.passes(t)] + ) get_app().layout.focus(dummy_control) @kb.add("c-c") @@ -173,17 +198,18 @@ class DynamicUI: active_frame = self.focusable[self.focus_index] active_frame.copy_to_clipboard() - app = Application( layout, - key_bindings=merge_key_bindings([ - kb, - self.transaction_table.key_bindings(), - #self.structure_pane.key_bindings(), - #self.hexdump_pane.key_bindings() - ]), + key_bindings=merge_key_bindings( + [ + kb, + self.transaction_table.key_bindings(), + # self.structure_pane.key_bindings(), + # self.hexdump_pane.key_bindings() + ] + ), full_screen=True, - style=style + style=style, ) app.before_render += self.check_resize @@ -210,11 +236,13 @@ class DynamicUI: lower_panels_height = available_height // 2 logger.debug(f"New terminal dimension: {dimensions}") - logger.debug(f"{border_allowance=}, {transactions_height=}, {lower_panels_height=}, total={border_allowance+transactions_height+lower_panels_height}") + logger.debug( + f"{border_allowance=}, {transactions_height=}, {lower_panels_height=}, total={border_allowance+transactions_height+lower_panels_height}" + ) self.transaction_table.resize(transactions_height) - #self.structure_pane.max_height = lower_panels_height - #self.hexdump_pane.max_lines = lower_panels_height + # self.structure_pane.max_height = lower_panels_height + # self.hexdump_pane.max_lines = lower_panels_height def get_available_blocks(self): blocks: list[Message] = [] diff --git a/androguard/ui/data_types.py b/androguard/ui/data_types.py index 6a80770b..4980dfd7 100644 --- a/androguard/ui/data_types.py +++ b/androguard/ui/data_types.py @@ -1,26 +1,26 @@ - import datetime from androguard.pentest import Message, MessageEvent, MessageSystem + class DisplayTransaction: def __init__(self, block: Message) -> None: self.block: Message = block - self.timestamp = datetime.datetime.now().strftime('%H:%M:%S'), + self.timestamp = (datetime.datetime.now().strftime('%H:%M:%S'),) @property def index(self) -> int: return self.block.index - + @property def unsupported_call(self) -> bool: - return '' #self.block.unsupported_call + return '' # self.block.unsupported_call @property def to_method(self) -> str: return self.block.to_method - + @property def from_method(self) -> str: return self.block.from_method @@ -34,13 +34,13 @@ class DisplayTransaction: return self.block.ret_value @property - def fields(self): #-> Field | None: - return None #self.block.root_field + def fields(self): # -> Field | None: + return None # self.block.root_field @property def direction_indicator(self) -> str: return '\u21D0' - + if self.block.direction == Direction.IN: return '\u21D0' if self.block.oneway else '\u21D2' elif self.block.direction == Direction.OUT: @@ -48,7 +48,6 @@ class DisplayTransaction: else: return '' - def style(self) -> str: if type(self.block) is MessageEvent: style = "class:transaction.oneway" @@ -65,7 +64,11 @@ class DisplayTransaction: elif self.block.unsupported_call: style = "class:transaction.no_aidl" elif self.block.direction == Direction.IN: - style = "class:transaction.oneway" if self.block.oneway else "class:transaction.request" + style = ( + "class:transaction.oneway" + if self.block.oneway + else "class:transaction.request" + ) elif self.block.direction == Direction.OUT: style = "class:transaction.response" else: @@ -75,7 +78,7 @@ class DisplayTransaction: def type(self) -> str: """Gets the type of the Block as a simple short string for use in pattern matching""" return "oneway" - + if self.unsupported_call: type_name = "unsupported type" elif self.block.errors: @@ -89,4 +92,3 @@ class DisplayTransaction: type_name = "unknown" # Should be impossible return type_name - diff --git a/androguard/ui/filter.py b/androguard/ui/filter.py index 869d589b..afe543e0 100644 --- a/androguard/ui/filter.py +++ b/androguard/ui/filter.py @@ -16,11 +16,13 @@ class Filter: interface: Optional[str] = None, method: Optional[str] = None, types: Optional[list[str]] = None, - include: bool = True + include: bool = True, ): self.interface = interface self.method = method - self.types = types or [] # List of associated types of the filter (call, return, etc) + self.types = ( + types or [] + ) # List of associated types of the filter (call, return, etc) self.inclusive = include def passes(self): @@ -34,12 +36,12 @@ class Filter: The code checks if the filter passes the checks, and then tailors the output to the filter_mode The type is either Inclusive ("Incl") or Exclusive ("Excl") """ - #matches = ( + # matches = ( # (not self.types or block.type() in self.types) and # (not self.interface or self.interface in block.interface) and # (not self.method or self.method in block.method) - #) - #return not matches ^ self.inclusive + # ) + # return not matches ^ self.inclusive return False def toggle_inclusivity(self): @@ -52,6 +54,7 @@ class Filter: return f"interface={interface}, method={method}, types={types}" + _T = TypeVar('_T', bound=Filter) diff --git a/androguard/ui/selection.py b/androguard/ui/selection.py index a3aaad6d..cb39a2fb 100644 --- a/androguard/ui/selection.py +++ b/androguard/ui/selection.py @@ -15,8 +15,10 @@ class View: def size(self): return self.end - self.start + _T = TypeVar('_T') + class SelectionViewList(UserList[_T]): def __init__(self, iterable=None, max_view_size=80, view_padding=5): @@ -53,7 +55,7 @@ class SelectionViewList(UserList[_T]): return self.data[self.selection] def view_slice(self): - return self.data[self.view.start:self.view.end] + return self.data[self.view.start : self.view.end] def resize_view(self, view_size): if self.selection_valid(): @@ -81,7 +83,9 @@ class SelectionViewList(UserList[_T]): # We're adding a negative here so although it looks a bit odd we are moving the view backwards self.view.start = max(self.view.start + step, 0) - self.view.end = min(len(self), self.view.start + self.max_view_size) + self.view.end = min( + len(self), self.view.start + self.max_view_size + ) self.on_update_event() def __delitem__(self, i: int): @@ -89,7 +93,9 @@ class SelectionViewList(UserList[_T]): self._delete_from_view(i) def _expand_view(self): - self.view.end = self.view.start + min(self.max_view_size, len(self) - self.view.start) + self.view.end = self.view.start + min( + self.max_view_size, len(self) - self.view.start + ) if not self.selection_valid(): self.selection = 0 self.on_selection_change() @@ -104,16 +110,16 @@ class SelectionViewList(UserList[_T]): self.move_selection(-1) self.on_update_event() - # TODO: once the selection is within padding of the start of the window it shoud move up - # if i <= self.view.end: - # self.view.end = min(self.view.end, len(self)) - # if i < self.selection: - # self.selection -= 1 - # elif i == self.selection: - # self.selection = min(self.selection, len(self)) - # if i <= self.view.start: - # self.view.start = max(0, self.view.start - 1) - # self.view.end -= 1 + # TODO: once the selection is within padding of the start of the window it shoud move up + # if i <= self.view.end: + # self.view.end = min(self.view.end, len(self)) + # if i < self.selection: + # self.selection -= 1 + # elif i == self.selection: + # self.selection = min(self.selection, len(self)) + # if i <= self.view.start: + # self.view.start = max(0, self.view.start - 1) + # self.view.end -= 1 def append(self, item: _T): super().append(item) @@ -156,4 +162,3 @@ class SelectionViewList(UserList[_T]): self.clear() self.data += items self._reset_view() - diff --git a/androguard/ui/table.py b/androguard/ui/table.py index b819694e..a96fb181 100644 --- a/androguard/ui/table.py +++ b/androguard/ui/table.py @@ -1,18 +1,32 @@ #!/usr/bin/env python3 from typing import Type, Union + from prompt_toolkit import Application -from prompt_toolkit.layout.layout import Layout -from prompt_toolkit.layout.dimension import Dimension as D, sum_layout_dimensions, max_layout_dimensions, to_dimension -from prompt_toolkit.widgets import Box, TextArea, Label, Button from prompt_toolkit.cache import SimpleCache -# from prompt_toolkit.widgets.base import Border -from prompt_toolkit.layout.containers import Window, VSplit, HSplit, HorizontalAlign, VerticalAlign from prompt_toolkit.key_binding.key_bindings import KeyBindings + +# from prompt_toolkit.widgets.base import Border +from prompt_toolkit.layout.containers import ( + HorizontalAlign, + HSplit, + VerticalAlign, + VSplit, + Window, +) +from prompt_toolkit.layout.dimension import Dimension as D +from prompt_toolkit.layout.dimension import ( + max_layout_dimensions, + sum_layout_dimensions, + to_dimension, +) +from prompt_toolkit.layout.layout import Layout from prompt_toolkit.utils import take_using_weights +from prompt_toolkit.widgets import Box, Button, Label, TextArea from androguard.ui.selection import SelectionViewList + class EmptyBorder: HORIZONTAL = '' VERTICAL = '' @@ -31,7 +45,7 @@ class EmptyBorder: class SpaceBorder: - " Box drawing characters. (Spaces) " + "Box drawing characters. (Spaces)" HORIZONTAL = ' ' VERTICAL = ' ' @@ -49,7 +63,7 @@ class SpaceBorder: class AsciiBorder: - " Box drawing characters. (ASCII) " + "Box drawing characters. (ASCII)" HORIZONTAL = '-' VERTICAL = '|' @@ -67,7 +81,7 @@ class AsciiBorder: class ThinBorder: - " Box drawing characters. (Thin) " + "Box drawing characters. (Thin)" HORIZONTAL = '\u2500' VERTICAL = '\u2502' @@ -85,7 +99,7 @@ class ThinBorder: class RoundedBorder(ThinBorder): - " Box drawing characters. (Rounded) " + "Box drawing characters. (Rounded)" TOP_LEFT = '\u256d' TOP_RIGHT = '\u256e' BOTTOM_LEFT = '\u2570' @@ -93,7 +107,7 @@ class RoundedBorder(ThinBorder): class ThickBorder: - " Box drawing characters. (Thick) " + "Box drawing characters. (Thick)" HORIZONTAL = '\u2501' VERTICAL = '\u2503' @@ -111,7 +125,7 @@ class ThickBorder: class DoubleBorder: - " Box drawing characters. (Thin) " + "Box drawing characters. (Thin)" HORIZONTAL = '\u2550' VERTICAL = '\u2551' @@ -127,6 +141,7 @@ class DoubleBorder: INTERSECT = '\u256c' + AnyBorderStyle = Union[ Type[EmptyBorder], Type[SpaceBorder], @@ -150,12 +165,25 @@ class Merge: class Table(HSplit): - def __init__(self, table, - borders: AnyBorderStyle=ThinBorder, column_width=None, column_widths=[], - window_too_small=None, align=VerticalAlign.JUSTIFY, - padding=0, padding_char=None, padding_style='', - width=None, height=None, z_index=None, - modal=False, key_bindings=None, style='', selected_style=''): + def __init__( + self, + table, + borders: AnyBorderStyle = ThinBorder, + column_width=None, + column_widths=[], + window_too_small=None, + align=VerticalAlign.JUSTIFY, + padding=0, + padding_char=None, + padding_style='', + width=None, + height=None, + z_index=None, + modal=False, + key_bindings=None, + style='', + selected_style='', + ): self.borders = borders self.column_width = column_width self.column_widths = column_widths @@ -165,7 +193,10 @@ class Table(HSplit): if not isinstance(table, list): table = [table] - children = [_Row(row=row, table=self, borders=borders, height=1) for row in table] + children = [ + _Row(row=row, table=self, borders=borders, height=1) + for row in table + ] super().__init__( children=children, @@ -179,7 +210,7 @@ class Table(HSplit): z_index=z_index, modal=modal, key_bindings=key_bindings, - style=style + style=style, ) self.row_cache = SimpleCache(maxsize=30) @@ -188,7 +219,16 @@ class Table(HSplit): # self.children.extend(_Row(row=row, table=self, borders=self.borders, height=1, style="#000000 bg:#ffffff") for row in rows) def add_row(self, row, style, cache_id): - r = self.row_cache.get(cache_id, lambda: _Row(row=row, table=self, borders=self.borders, height=1, style=style)) + r = self.row_cache.get( + cache_id, + lambda: _Row( + row=row, + table=self, + borders=self.borders, + height=1, + style=style, + ), + ) self.children.append(r) @property @@ -200,6 +240,7 @@ class Table(HSplit): """ List of child objects, including padding & borders. """ + def get(): result = [] @@ -306,7 +347,8 @@ class _BaseRow(VSplit): child_generator = take_using_weights( items=list(range(len(dimensions))), - weights=[d.weight for d in dimensions]) + weights=[d.weight for d in dimensions], + ) i = next(child_generator) @@ -334,7 +376,7 @@ class _BaseRow(VSplit): for c in children: if isinstance(c, _Cell): inc = (c.merge * 2) - 1 - tmp.append(sum(sizes[i:i + inc])) + tmp.append(sum(sizes[i : i + inc])) else: inc = 1 tmp.append(sizes[i]) @@ -345,11 +387,23 @@ class _BaseRow(VSplit): class _Row(_BaseRow): - def __init__(self, row, table, borders, - window_too_small=None, align=HorizontalAlign.JUSTIFY, - padding=D.exact(0), padding_char=None, padding_style='', - width=None, height=None, z_index=None, - modal=False, key_bindings=None, style=''): + def __init__( + self, + row, + table, + borders, + window_too_small=None, + align=HorizontalAlign.JUSTIFY, + padding=D.exact(0), + padding_char=None, + padding_style='', + width=None, + height=None, + z_index=None, + modal=False, + key_bindings=None, + style='', + ): self.table = table self.borders = borders @@ -377,7 +431,8 @@ class _Row(_BaseRow): z_index=z_index, modal=modal, key_bindings=key_bindings, - style=style) + style=style, + ) @property def raw_columns(self): @@ -388,6 +443,7 @@ class _Row(_BaseRow): """ List of child objects, including padding & borders. """ + def get(): result = [] @@ -421,11 +477,24 @@ class _Row(_BaseRow): class _Border(_BaseRow): - def __init__(self, prev, next, table, borders, - window_too_small=None, align=HorizontalAlign.JUSTIFY, - padding=D.exact(0), padding_char=None, padding_style='', - width=None, height=None, z_index=None, - modal=False, key_bindings=None, style=''): + def __init__( + self, + prev, + next, + table, + borders, + window_too_small=None, + align=HorizontalAlign.JUSTIFY, + padding=D.exact(0), + padding_char=None, + padding_style='', + width=None, + height=None, + z_index=None, + modal=False, + key_bindings=None, + style='', + ): assert prev or next self.prev = prev self.next = next @@ -446,7 +515,8 @@ class _Border(_BaseRow): z_index=z_index, modal=modal, key_bindings=key_bindings, - style=style) + style=style, + ) def has_borders(self, row): yield None # first (outer) border @@ -470,6 +540,7 @@ class _Border(_BaseRow): """ List of child objects, including padding & borders. """ + def get(): result = [] @@ -525,13 +596,26 @@ class _Border(_BaseRow): class _Cell(HSplit): - def __init__(self, cell, table, row, merge=1, - padding=0, char=None, - padding_left=None, padding_right=None, - padding_top=None, padding_bottom=None, - window_too_small=None, - width=None, height=None, z_index=None, - modal=False, key_bindings=None, style=''): + def __init__( + self, + cell, + table, + row, + merge=1, + padding=0, + char=None, + padding_left=None, + padding_right=None, + padding_top=None, + padding_bottom=None, + window_too_small=None, + width=None, + height=None, + z_index=None, + modal=False, + key_bindings=None, + style='', + ): self.table = table self.row = row self.merge = merge @@ -569,7 +653,8 @@ class _Cell(HSplit): z_index=z_index, modal=modal, key_bindings=key_bindings, - style=style) + style=style, + ) def demo(): @@ -589,9 +674,10 @@ def demo(): sht3 = "The quick brown fox jumps over the lazy dog." kb = KeyBindings() + @kb.add('c-c') def _(event): - " Abort when Control-C has been pressed. " + "Abort when Control-C has been pressed." event.app.exit(exception=KeyboardInterrupt, style='class:aborting') table = [ @@ -610,7 +696,8 @@ def demo(): table=table, column_width=D.exact(15), column_widths=[None], - borders=DoubleBorder), + borders=DoubleBorder, + ), padding=1, ), ) @@ -618,4 +705,4 @@ def demo(): if __name__ == '__main__': - demo().run() \ No newline at end of file + demo().run() diff --git a/androguard/ui/util.py b/androguard/ui/util.py index 61a58519..218177df 100644 --- a/androguard/ui/util.py +++ b/androguard/ui/util.py @@ -1,3 +1,3 @@ def clamp(range_min: int, range_max: int, value: int) -> int: """Return value if its is within range_min and range_max else return the nearest bound""" - return max(min(range_max, value), range_min) \ No newline at end of file + return max(min(range_max, value), range_min) diff --git a/androguard/ui/widget/details.py b/androguard/ui/widget/details.py index b56b5e6e..f587481c 100644 --- a/androguard/ui/widget/details.py +++ b/androguard/ui/widget/details.py @@ -1,19 +1,27 @@ import json - from typing import Optional from prompt_toolkit.filters import Condition from prompt_toolkit.formatted_text import FormattedText from prompt_toolkit.key_binding import KeyBindings -from prompt_toolkit.layout import AnyContainer, Dimension, HSplit, FormattedTextControl, Window +from prompt_toolkit.layout import ( + AnyContainer, + Dimension, + FormattedTextControl, + HSplit, + Window, +) from prompt_toolkit.layout.dimension import AnyDimension -from androguard.ui.selection import SelectionViewList from androguard.ui.data_types import DisplayTransaction +from androguard.ui.selection import SelectionViewList from androguard.ui.widget.frame import SelectableFrame + class DetailsFrame: - def __init__(self, transactions: SelectionViewList, max_lines: int) -> None: + def __init__( + self, transactions: SelectionViewList, max_lines: int + ) -> None: self.transactions = transactions self.max_lines = max_lines @@ -25,7 +33,7 @@ class DetailsFrame: title="Details", body=self.get_content, width=Dimension(min=56, preferred=100, max=100), - height=Dimension(preferred=max_lines) + height=Dimension(preferred=max_lines), ) @property @@ -47,15 +55,19 @@ class DetailsFrame: ignore_content_height=True, content=FormattedTextControl( text=self.get_current_details() - ) + ), ), ] ) - + def get_current_details(self): if self.transactions.selection_valid(): - return json.dumps(self.transactions.selected().params, indent=2) + '\n' + json.dumps(self.transactions.selected().ret_value, indent=2) + return ( + json.dumps(self.transactions.selected().params, indent=2) + + '\n' + + json.dumps(self.transactions.selected().ret_value, indent=2) + ) return '' def __pt_container__(self) -> AnyContainer: - return self.container \ No newline at end of file + return self.container diff --git a/androguard/ui/widget/filters.py b/androguard/ui/widget/filters.py index 53e79a8a..e9e2bde8 100644 --- a/androguard/ui/widget/filters.py +++ b/androguard/ui/widget/filters.py @@ -1,7 +1,7 @@ from prompt_toolkit.key_binding import KeyBindings -from prompt_toolkit.layout import AnyContainer, HSplit, VerticalAlign, VSplit from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous -from prompt_toolkit.widgets import Frame, CheckboxList, Box, TextArea, Label +from prompt_toolkit.layout import AnyContainer, HSplit, VerticalAlign, VSplit +from prompt_toolkit.widgets import Box, CheckboxList, Frame, Label, TextArea from androguard.ui.filter import Filter @@ -9,23 +9,28 @@ from androguard.ui.filter import Filter class TypeCheckboxlist(CheckboxList): def __init__(self) -> None: - values=[ - ("call", "call"), - ("return", "return"), - ("oneway", "oneway"), - ("error", "error"), - ("unknown", "unknown"), + values = [ + ("call", "call"), + ("return", "return"), + ("oneway", "oneway"), + ("error", "error"), + ("unknown", "unknown"), ] super().__init__(values) self.show_scrollbar = False + class FiltersPanel: def __init__(self) -> None: self.visible = False - self.interface_textarea = TextArea(multiline=False, style="class:dialogger.textarea") - self.method_textarea = TextArea(multiline=False, style="class:dialogger.textarea") + self.interface_textarea = TextArea( + multiline=False, style="class:dialogger.textarea" + ) + self.method_textarea = TextArea( + multiline=False, style="class:dialogger.textarea" + ) self.type_filter_checkboxes = TypeCheckboxlist() @@ -38,20 +43,26 @@ class FiltersPanel: width=50, align=VerticalAlign.TOP, children=[ - VSplit(children=[ - Label("Interface", width=10), - self.interface_textarea - ]), - VSplit(children=[ - Label("Method", width=10), - self.method_textarea - ]), - VSplit(children=[ - Label("Type", width=10, dont_extend_height=False), - self.type_filter_checkboxes, - ]), - ] - ) + VSplit( + children=[ + Label("Interface", width=10), + self.interface_textarea, + ] + ), + VSplit( + children=[ + Label("Method", width=10), + self.method_textarea, + ] + ), + VSplit( + children=[ + Label("Type", width=10, dont_extend_height=False), + self.type_filter_checkboxes, + ] + ), + ], + ), ) kb = KeyBindings() @@ -64,7 +75,7 @@ class FiltersPanel: body=float_frame, style="class:dialogger.background", modal=True, - key_bindings=kb + key_bindings=kb, ) def filter(self) -> Filter: @@ -72,7 +83,8 @@ class FiltersPanel: return Filter( self.interface_textarea.text, self.method_textarea.text, - self.type_filter_checkboxes.current_values) + self.type_filter_checkboxes.current_values, + ) def __pt_container__(self) -> AnyContainer: - return self.container \ No newline at end of file + return self.container diff --git a/androguard/ui/widget/frame.py b/androguard/ui/widget/frame.py index 9ee1e217..ccfb9593 100644 --- a/androguard/ui/widget/frame.py +++ b/androguard/ui/widget/frame.py @@ -1,5 +1,3 @@ - - from functools import partial from typing import Optional @@ -14,10 +12,11 @@ from prompt_toolkit.layout import ( DynamicContainer, HSplit, VSplit, - Window + Window, ) from prompt_toolkit.widgets.base import Border, Label + class SelectableFrame: """ Draw a border around any container, optionally with a title text. @@ -88,8 +87,12 @@ class SelectableFrame: self.container = HSplit( [ - ConditionalContainer(content=top_row_with_title, filter=has_title), - ConditionalContainer(content=top_row_without_title, filter=~has_title), + ConditionalContainer( + content=top_row_with_title, filter=has_title + ), + ConditionalContainer( + content=top_row_without_title, filter=~has_title + ), VSplit( [ fill(width=1, char=Border.VERTICAL), diff --git a/androguard/ui/widget/help.py b/androguard/ui/widget/help.py index fbad7c1e..1387e25a 100644 --- a/androguard/ui/widget/help.py +++ b/androguard/ui/widget/help.py @@ -1,17 +1,18 @@ from prompt_toolkit.layout import AnyContainer, HSplit -from prompt_toolkit.widgets import Frame, Box, Label +from prompt_toolkit.widgets import Box, Frame, Label + class HelpPanel: def __init__(self) -> None: self.visible = False - float_frame = Box( padding_top=1, padding_left=2, padding_right=2, - body=HSplit(children=[ + body=HSplit( + children=[ Label("up Move up"), Label("down Move down"), Label("shift + up Page up"), @@ -24,7 +25,8 @@ class HelpPanel: Label("f Open filter options"), Label("h Help"), Label("q Quit"), - ]), + ] + ), ) self.container = Frame( @@ -35,4 +37,4 @@ class HelpPanel: ) def __pt_container__(self) -> AnyContainer: - return self.container \ No newline at end of file + return self.container diff --git a/androguard/ui/widget/toolbar.py b/androguard/ui/widget/toolbar.py index 3abe8506..7e7907f1 100644 --- a/androguard/ui/widget/toolbar.py +++ b/androguard/ui/widget/toolbar.py @@ -1,11 +1,12 @@ - from typing import Sequence + from prompt_toolkit.formatted_text import AnyFormattedText, FormattedText from prompt_toolkit.layout.containers import AnyContainer, DynamicContainer from prompt_toolkit.widgets import FormattedTextToolbar from androguard.ui.widget.filters import FiltersPanel + class StatusToolbar: def __init__(self, transactions: Sequence, filters: FiltersPanel) -> None: @@ -13,7 +14,6 @@ class StatusToolbar: self.filters = filters self.container = DynamicContainer(self.toolbar_container) - def toolbar_container(self) -> AnyContainer: return FormattedTextToolbar( text=self.toolbar_text(), @@ -21,9 +21,14 @@ class StatusToolbar: ) def toolbar_text(self) -> AnyFormattedText: - return FormattedText([ - ("class:toolbar.text", f"Transactions: {len(self.transactions)}, Filter: {self.filters.filter()}") - ]) + return FormattedText( + [ + ( + "class:toolbar.text", + f"Transactions: {len(self.transactions)}, Filter: {self.filters.filter()}", + ) + ] + ) def __pt_container__(self) -> AnyContainer: - return self.container \ No newline at end of file + return self.container diff --git a/androguard/ui/widget/transactions.py b/androguard/ui/widget/transactions.py index 5ece7b47..f00a0de8 100644 --- a/androguard/ui/widget/transactions.py +++ b/androguard/ui/widget/transactions.py @@ -1,21 +1,25 @@ import csv import io -#import pyperclip - from prompt_toolkit.filters import Condition from prompt_toolkit.key_binding import KeyBindings -from prompt_toolkit.layout.dimension import AnyDimension, Dimension from prompt_toolkit.layout import AnyContainer +from prompt_toolkit.layout.dimension import AnyDimension, Dimension from androguard.ui import table from androguard.ui.selection import SelectionViewList from androguard.ui.widget.frame import SelectableFrame +# import pyperclip + + + class TransactionFrame: - def __init__(self, transactions: SelectionViewList, height: AnyDimension = None) -> None: + def __init__( + self, transactions: SelectionViewList, height: AnyDimension = None + ) -> None: self.transactions = transactions self.transactions.on_update_event += self.update_table @@ -56,18 +60,28 @@ class TransactionFrame: def update_table(self, _): self.table.children.clear() - self.table.add_row(self.headings, "class:transactions.heading", id(self.headings)) - for i in range(self.transactions.view.start, self.transactions.view.end): + self.table.add_row( + self.headings, "class:transactions.heading", id(self.headings) + ) + for i in range( + self.transactions.view.start, self.transactions.view.end + ): row, style = self._to_row(self.transactions[i]) self.table.add_row( row, - f"{style} reverse" if i == self.transactions.selection else style, - (id(self.transactions[i]), i == self.transactions.selection) + ( + f"{style} reverse" + if i == self.transactions.selection + else style + ), + (id(self.transactions[i]), i == self.transactions.selection), ) self.pad_table() def pad_table(self): - padding = self.transactions.max_view_size - self.transactions.view.size() + padding = ( + self.transactions.max_view_size - self.transactions.view.size() + ) empty_row = [ table.Label(""), table.Label(""), @@ -76,8 +90,9 @@ class TransactionFrame: table.Label(""), ] for _ in range(padding): - self.table.add_row(empty_row, "class:transaction.default", id(empty_row)) - + self.table.add_row( + empty_row, "class:transaction.default", id(empty_row) + ) def key_bindings(self) -> KeyBindings: kb = KeyBindings() @@ -104,7 +119,9 @@ class TransactionFrame: @kb.add('end', filter=Condition(lambda: self.activated)) def _(event): - self.transactions.move_selection(len(self.transactions) - self.transactions.selection) + self.transactions.move_selection( + len(self.transactions) - self.transactions.selection + ) return kb @@ -117,8 +134,7 @@ class TransactionFrame: def activated(self, value): self.container.activated = value - - #def copy_to_clipboard(self): + # def copy_to_clipboard(self): # if self.transactions.selection_valid(): # output = io.StringIO() # writer = csv.writer(output, quoting=csv.QUOTE_NONE) @@ -131,15 +147,14 @@ class TransactionFrame: # ]) # pyperclip.copy(output.getvalue()) - def _to_row(self, transaction): # TODO: Cache the rows so we don't need to recreate them. return [ table.Label(transaction.direction_indicator), table.Label(str(transaction.index)), table.Label(transaction.from_method), - table.Label(transaction.to_method) + table.Label(transaction.to_method), ], transaction.style() def __pt_container__(self) -> AnyContainer: - return self.container \ No newline at end of file + return self.container diff --git a/androguard/util.py b/androguard/util.py index 804b8866..a40efdec 100644 --- a/androguard/util.py +++ b/androguard/util.py @@ -1,17 +1,18 @@ - -import sys -from typing import Union, BinaryIO -from asn1crypto import keys, x509 -import hashlib import binascii +import hashlib +import sys +from typing import BinaryIO, Union + +from asn1crypto import keys, x509 #  External dependencies # import asn1crypto from asn1crypto.x509 import Name from loguru import logger + class MyFilter: - def __init__(self, level:int) -> None: + def __init__(self, level: int) -> None: self.level = level def __call__(self, record): @@ -19,7 +20,7 @@ class MyFilter: return record["level"].no >= levelno -def set_log(level:int) -> None: +def set_log(level: int) -> None: """ Sets the log for loguru based on the level being passed. The possible values are TRACE, DEBUG, INFO, SUCCESS, WARNING, ERROR, CRITICAL @@ -31,7 +32,8 @@ def set_log(level:int) -> None: # Stuff that might be useful -def read_at(buff: BinaryIO, offset:int, size:int=-1) -> bytes: + +def read_at(buff: BinaryIO, offset: int, size: int = -1) -> bytes: idx = buff.tell() buff.seek(offset) d = buff.read(size) @@ -39,7 +41,7 @@ def read_at(buff: BinaryIO, offset:int, size:int=-1) -> bytes: return d -def readFile(filename: str, binary:bool=True) -> bytes: +def readFile(filename: str, binary: bool = True) -> bytes: """ Open and read a file :param filename: filename to open and read @@ -50,7 +52,9 @@ def readFile(filename: str, binary:bool=True) -> bytes: return f.read() -def get_certificate_name_string(name:Union[dict,Name], short:bool=False, delimiter:str=', ') -> str: +def get_certificate_name_string( + name: Union[dict, Name], short: bool = False, delimiter: str = ', ' +) -> str: """ Format the Name type of a X509 Certificate in a human readable form. @@ -64,7 +68,7 @@ def get_certificate_name_string(name:Union[dict,Name], short:bool=False, delimit :rtype: str """ - if isinstance(name, Name):#asn1crypto.x509.Name): + if isinstance(name, Name): # asn1crypto.x509.Name): name = name.native # For the shortform, we have a lookup table @@ -92,13 +96,24 @@ def get_certificate_name_string(name:Union[dict,Name], short:bool=False, delimit 'email_address': ("E", "emailAddress"), 'domain_component': ("DC", "domainComponent"), 'name_distinguisher': ("nameDistinguisher", "nameDistinguisher"), - 'organization_identifier': ("organizationIdentifier", "organizationIdentifier"), + 'organization_identifier': ( + "organizationIdentifier", + "organizationIdentifier", + ), } - return delimiter.join(["{}={}".format(_.get(attr, (attr, attr))[0 if short else 1], name[attr]) for attr in name]) + return delimiter.join( + [ + "{}={}".format( + _.get(attr, (attr, attr))[0 if short else 1], name[attr] + ) + for attr in name + ] + ) def parse_public(data): - from asn1crypto import pem, keys, x509 + from asn1crypto import keys, pem, x509 + """ Loads a public key from a DER or PEM-formatted input. Supports RSA, DSA, EC public keys, and X.509 certificates. @@ -112,7 +127,9 @@ def parse_public(data): if pem.detect(data): type_name, _, der_bytes = pem.unarmor(data) if type_name in ['PRIVATE KEY', 'RSA PRIVATE KEY']: - raise ValueError("The data specified appears to be a private key, not a public key.") + raise ValueError( + "The data specified appears to be a private key, not a public key." + ) else: # If not PEM, assume it's DER-encoded der_bytes = data @@ -128,7 +145,9 @@ def parse_public(data): # Try to parse the data as an X.509 certificate try: certificate = x509.Certificate.load(der_bytes) - public_key_info = certificate['tbs_certificate']['subject_public_key_info'] + public_key_info = certificate['tbs_certificate'][ + 'subject_public_key_info' + ] public_key_info.native # Fully parse the object return public_key_info except ValueError: @@ -143,7 +162,9 @@ def parse_public(data): except ValueError: pass # Not an RSAPublicKey structure - raise ValueError("The data specified does not appear to be a known public key or certificate format.") + raise ValueError( + "The data specified does not appear to be a known public key or certificate format." + ) def calculate_fingerprint(key_object): @@ -160,7 +181,10 @@ def calculate_fingerprint(key_object): if key_object.algorithm == 'rsa': key = key_object['public_key'].parsed # Prepare string with modulus and public exponent - to_hash = '%d:%d' % (key['modulus'].native, key['public_exponent'].native) + to_hash = '%d:%d' % ( + key['modulus'].native, + key['public_exponent'].native, + ) # DSA Public Key elif key_object.algorithm == 'dsa': @@ -186,4 +210,4 @@ def calculate_fingerprint(key_object): to_hash = to_hash.encode('utf-8') # Return the SHA-256 hash of the formatted key data - return hashlib.sha256(to_hash).digest() \ No newline at end of file + return hashlib.sha256(to_hash).digest() diff --git a/setup.py b/setup.py index 4aba401d..5cd57143 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ -from androguard import __version__ -from setuptools import setup, find_packages +from setuptools import find_packages, setup +from androguard import __version__ with open('requirements.txt', 'r') as fp: install_requires = fp.read().splitlines() @@ -14,24 +14,26 @@ setup( url="https://github.com/androguard/androguard", install_requires=install_requires, package_data={ - "androguard.core.api_specific_resources": ["aosp_permissions/*.json", - "api_permission_mappings/*.json"], + "androguard.core.api_specific_resources": [ + "aosp_permissions/*.json", + "api_permission_mappings/*.json", + ], "androguard.core.resources": ["public.xml"], }, entry_points={ - 'console_scripts': [ - 'androguard = androguard.cli.cli:entry_point'] }, + 'console_scripts': ['androguard = androguard.cli.cli:entry_point'] + }, setup_requires=['setuptools'], python_requires='>=3.9', classifiers=[ - 'License :: OSI Approved :: Apache Software License', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3 :: Only', - 'Topic :: Security', - 'Topic :: Software Development', - 'Topic :: Utilities', - ], + 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3 :: Only', + 'Topic :: Security', + 'Topic :: Software Development', + 'Topic :: Utilities', + ], ) diff --git a/tests/parse_dex.py b/tests/parse_dex.py index c72f248c..6dd5007f 100644 --- a/tests/parse_dex.py +++ b/tests/parse_dex.py @@ -1,14 +1,13 @@ -from struct import pack, unpack -from binascii import hexlify - # This is a very simple DEX parser, to get the bytecodes for each method # Output format will be: # import sys +from binascii import hexlify +from struct import pack, unpack sys.path.append('.') -from androguard.core.dex import readuleb128, readsleb128, DalvikPacker +from androguard.core.dex import DalvikPacker, readsleb128, readuleb128 def read_null_terminated(f): @@ -21,7 +20,7 @@ def read_null_terminated(f): x.append(ord(z)) -class MockClassManager(): +class MockClassManager: @property def packer(self): return DalvikPacker(0x12345678) @@ -36,18 +35,46 @@ class read_dex: methods = [] # Stores method_idx, code_off with open(fname, "rb") as f: - magic, checksum, signature, file_size, header_size, endian_tag, link_size, \ - link_off, map_off, self.string_ids_size, string_ids_off, type_ids_size, \ - type_ids_off, proto_ids_size, proto_ids_off, field_ids_size, field_ids_off, \ - method_ids_size, method_ids_off, class_defs_size, class_defs_off, data_size, \ - data_off = unpack("<8sI20s20I", f.read(112)) + ( + magic, + checksum, + signature, + file_size, + header_size, + endian_tag, + link_size, + link_off, + map_off, + self.string_ids_size, + string_ids_off, + type_ids_size, + type_ids_off, + proto_ids_size, + proto_ids_off, + field_ids_size, + field_ids_off, + method_ids_size, + method_ids_off, + class_defs_size, + class_defs_off, + data_size, + data_off, + ) = unpack("<8sI20s20I", f.read(112)) # print("class_defs_size", class_defs_size, "class_defs_off", class_defs_off) for i in range(class_defs_size): # class_def_item f.seek(class_defs_off + i * 8 * 4) - class_idx, access_flags, superclass_idx, interfaces_off, source_file_idx, \ - annotations_off, class_data_off, static_values_off = unpack("<8I", f.read(8 * 4)) + ( + class_idx, + access_flags, + superclass_idx, + interfaces_off, + source_file_idx, + annotations_off, + class_data_off, + static_values_off, + ) = unpack("<8I", f.read(8 * 4)) # Now parse the class_data_item if class_data_off == 0: @@ -92,7 +119,7 @@ class read_dex: self.str_raw = dict() for i in range(self.string_ids_size): f.seek(string_ids_off + i * 4) - string_data_off, = unpack(" 0 and insns_size % 2 == 1: padding = unpack(" 0: # try_item[tries_size] - tries = unpack("<{}".format("".join(["IHH"] * tries_size)), f.read(8 * tries_size)) + tries = unpack( + "<{}".format("".join(["IHH"] * tries_size)), + f.read(8 * tries_size), + ) # encoded_catch_handler_list size = readuleb128(cm, f) diff --git a/tests/test_analysis.py b/tests/test_analysis.py index e0a9d944..1f267523 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -1,6 +1,5 @@ import os import unittest - from operator import itemgetter from androguard.core.analysis import analysis @@ -19,13 +18,23 @@ class AnalysisTest(unittest.TestCase): self.assertIsInstance(dx, analysis.Analysis) def testAPK(self): - a, d, dx = AnalyzeAPK(os.path.join(test_dir, "data/APK/a2dp.Vol_137.apk")) + a, d, dx = AnalyzeAPK( + os.path.join(test_dir, "data/APK/a2dp.Vol_137.apk") + ) - self.assertEqual(len(list(dx.get_internal_classes())), 1353) # dex header header->headerItem->classDefsSize - self.assertEqual(len(list(dx.get_external_classes())), 388) # difficult to check, cannot find using JADX - self.assertEqual(len(list(dx.get_classes())), 1741) # sum of internal and external classes + self.assertEqual( + len(list(dx.get_internal_classes())), 1353 + ) # dex header header->headerItem->classDefsSize + self.assertEqual( + len(list(dx.get_external_classes())), 388 + ) # difficult to check, cannot find using JADX + self.assertEqual( + len(list(dx.get_classes())), 1741 + ) # sum of internal and external classes - self.assertEqual(len(dx.get_strings()), 13523) # dex header header->headerItem->stringsIdsSize + self.assertEqual( + len(dx.get_strings()), 13523 + ) # dex header header->headerItem->stringsIdsSize # don't have a way to discern external vs internal fields currently, # header->headerItemFieldIdsSize is 4005, but there must be 573 more external added @@ -33,42 +42,88 @@ class AnalysisTest(unittest.TestCase): self.assertEqual(len(list(dx.get_fields())), 4005 + 573) # internal+external methods should sum up to header->headerItem->methodIdsSize - self.assertEqual(len(list(dx.get_internal_methods())), 9676) # difficult to check, can use jadx-gui and see summary - self.assertEqual(len(list(dx.get_external_methods())), 3116) # difficult to check + self.assertEqual( + len(list(dx.get_internal_methods())), 9676 + ) # difficult to check, can use jadx-gui and see summary + self.assertEqual( + len(list(dx.get_external_methods())), 3116 + ) # difficult to check # TODO: the DEX header says 12795 here, but 9676 + 3116 adds up to 12792 # JADX corroborates 9676, so I think 3116 is off, and a few unnecessary # ExternalMethods are added somewhere - self.assertEqual(len(list(dx.get_methods())), 12792) # dex header header->headerItem->methodIdsSize - - + self.assertEqual( + len(list(dx.get_methods())), 12792 + ) # dex header header->headerItem->methodIdsSize for cls in dx.get_external_classes(): self.assertEqual(cls.name[0], 'L') self.assertEqual(cls.name[-1], ';') # Filter all support libraries - self.assertEqual(len(list(dx.find_classes("^(?!Landroid/support).*;$"))), 512) - self.assertEqual(len(list(dx.find_classes("^(?!Landroid/support).*;$", no_external=True))), 124) + self.assertEqual( + len(list(dx.find_classes("^(?!Landroid/support).*;$"))), 512 + ) + self.assertEqual( + len( + list( + dx.find_classes( + "^(?!Landroid/support).*;$", no_external=True + ) + ) + ), + 124, + ) # Find all constructors by method name self.assertEqual( - len(list(dx.find_methods(classname="^(?!Landroid).*;$", methodname="", descriptor=r"^\(.+\).*$"))), - 138) - self.assertEqual(len(list( - dx.find_methods(classname="^(?!Landroid).*;$", methodname="", descriptor=r"^\(.+\).*$", - no_external=True))), 94) + len( + list( + dx.find_methods( + classname="^(?!Landroid).*;$", + methodname="", + descriptor=r"^\(.+\).*$", + ) + ) + ), + 138, + ) + self.assertEqual( + len( + list( + dx.find_methods( + classname="^(?!Landroid).*;$", + methodname="", + descriptor=r"^\(.+\).*$", + no_external=True, + ) + ) + ), + 94, + ) # Find url like strings self.assertEqual(len(list(dx.find_strings(r".*:\/\/.*"))), 16) # find String fields - self.assertEqual(len(list(dx.find_fields(classname="^(?!Landroid).*;$", fieldtype=r"Ljava\/lang\/String;"))), - 95)#63) + self.assertEqual( + len( + list( + dx.find_fields( + classname="^(?!Landroid).*;$", + fieldtype=r"Ljava\/lang\/String;", + ) + ) + ), + 95, + ) # 63) def testAnalysis(self): import sys - h, d, dx = AnalyzeDex(os.path.join(test_dir, "data/APK/AnalysisTest.dex")) + + h, d, dx = AnalyzeDex( + os.path.join(test_dir, "data/APK/AnalysisTest.dex") + ) self.assertEqual(len(list(dx.get_internal_classes())), 1) self.assertEqual(len(list(dx.get_internal_methods())), 4) @@ -77,21 +132,35 @@ class AnalysisTest(unittest.TestCase): self.assertEqual(len(dx.get_strings()), 21) self.assertEqual(len(list(dx.get_fields())), 0) - self.assertEqual(h, "4595fc25104f3fcd709163eb70ca476edf116753607ec18f09548968c71910dc") + self.assertEqual( + h, + "4595fc25104f3fcd709163eb70ca476edf116753607ec18f09548968c71910dc", + ) self.assertIsInstance(d, DEX) self.assertIsInstance(dx, analysis.Analysis) - cls = ["Ljava/io/PrintStream;", "Ljava/lang/Object;", - "Ljava/math/BigDecimal;", "Ljava/math/BigInteger;"] + cls = [ + "Ljava/io/PrintStream;", + "Ljava/lang/Object;", + "Ljava/math/BigDecimal;", + "Ljava/math/BigInteger;", + ] for c in cls: - self.assertIn(c, map(lambda x: x.orig_class.get_name(), - dx.get_external_classes())) + self.assertIn( + c, + map( + lambda x: x.orig_class.get_name(), + dx.get_external_classes(), + ), + ) def testMultidex(self): a, d, dx = AnalyzeAPK(os.path.join(test_dir, "data/APK/multidex.apk")) - cls = list(map(lambda x: x.get_vm_class().get_name(), dx.get_classes())) + cls = list( + map(lambda x: x.get_vm_class().get_name(), dx.get_classes()) + ) self.assertIn('Lcom/foobar/foo/Foobar;', cls) self.assertIn('Lcom/blafoo/bar/Blafoo;', cls) @@ -138,14 +207,18 @@ class AnalysisTest(unittest.TestCase): self.assertFalse(dx.classes["Lcom/foobar/foo/Foobar;"].is_external()) def testInterfaces(self): - h, d, dx = AnalyzeDex(os.path.join(test_dir, "data/APK/InterfaceCls.dex")) + h, d, dx = AnalyzeDex( + os.path.join(test_dir, "data/APK/InterfaceCls.dex") + ) cls = dx.classes['LInterfaceCls;'] self.assertIn('Ljavax/net/ssl/X509TrustManager;', cls.implements) self.assertEqual(cls.name, 'LInterfaceCls;') def testExtends(self): - h, d, dx = AnalyzeDex(os.path.join(test_dir, "data/APK/ExceptionHandling.dex")) + h, d, dx = AnalyzeDex( + os.path.join(test_dir, "data/APK/ExceptionHandling.dex") + ) cls = dx.classes['LSomeException;'] self.assertEqual(cls.extends, 'Ljava/lang/Exception;') @@ -169,29 +242,52 @@ class AnalysisTest(unittest.TestCase): testcls = dx.classes['Ltests/androguard/TestActivity;'] self.assertIsInstance(testcls, analysis.ClassAnalysis) - testmeth = list(filter(lambda x: x.name == 'onCreate', testcls.get_methods()))[0] + testmeth = list( + filter(lambda x: x.name == 'onCreate', testcls.get_methods()) + )[0] - self.assertEqual(len(list(dx.find_methods(testcls.name, '^onCreate$'))), 1) - self.assertEqual(list(dx.find_methods(testcls.name, '^onCreate$'))[0], testmeth) + self.assertEqual( + len(list(dx.find_methods(testcls.name, '^onCreate$'))), 1 + ) + self.assertEqual( + list(dx.find_methods(testcls.name, '^onCreate$'))[0], testmeth + ) self.assertIsInstance(testmeth, analysis.MethodAnalysis) self.assertFalse(testmeth.is_external()) self.assertIsInstance(testmeth.method, EncodedMethod) self.assertEqual(testmeth.name, 'onCreate') - xrefs = list(map(lambda x: x.full_name, map(itemgetter(1), sorted(testmeth.get_xref_to(), key=itemgetter(2))))) + xrefs = list( + map( + lambda x: x.full_name, + map( + itemgetter(1), + sorted(testmeth.get_xref_to(), key=itemgetter(2)), + ), + ) + ) self.assertEqual(len(xrefs), 5) # First, super is called: - self.assertEqual(xrefs.pop(0), 'Landroid/app/Activity; onCreate (Landroid/os/Bundle;)V') + self.assertEqual( + xrefs.pop(0), + 'Landroid/app/Activity; onCreate (Landroid/os/Bundle;)V', + ) # then setContentView (which is in the current class but the method is external) - self.assertEqual(xrefs.pop(0), 'Ltests/androguard/TestActivity; setContentView (I)V') + self.assertEqual( + xrefs.pop(0), 'Ltests/androguard/TestActivity; setContentView (I)V' + ) # then getApplicationContext (inside the Toast) - self.assertEqual(xrefs.pop(0), - 'Ltests/androguard/TestActivity; getApplicationContext ()Landroid/content/Context;') + self.assertEqual( + xrefs.pop(0), + 'Ltests/androguard/TestActivity; getApplicationContext ()Landroid/content/Context;', + ) # then Toast.makeText - self.assertEqual(xrefs.pop(0), - 'Landroid/widget/Toast; makeText (Landroid/content/Context; Ljava/lang/CharSequence; I)Landroid/widget/Toast;') + self.assertEqual( + xrefs.pop(0), + 'Landroid/widget/Toast; makeText (Landroid/content/Context; Ljava/lang/CharSequence; I)Landroid/widget/Toast;', + ) # then show() self.assertEqual(xrefs.pop(0), 'Landroid/widget/Toast; show ()V') @@ -204,7 +300,11 @@ class AnalysisTest(unittest.TestCase): # We have MethodAnalysis now stored in the xref! self.assertIn(testmeth, map(itemgetter(1), other[0].get_xref_from())) - other = list(dx.find_methods('^Ltests/androguard/TestActivity;$', '^setContentView$')) + other = list( + dx.find_methods( + '^Ltests/androguard/TestActivity;$', '^setContentView$' + ) + ) # External because not overwritten in class: self.assertEqual(len(other), 1) self.assertIsInstance(other[0], analysis.MethodAnalysis) @@ -212,7 +312,11 @@ class AnalysisTest(unittest.TestCase): self.assertFalse(other[0].is_android_api()) self.assertIn(testmeth, map(itemgetter(1), other[0].get_xref_from())) - other = list(dx.find_methods('^Ltests/androguard/TestActivity;$', '^getApplicationContext$')) + other = list( + dx.find_methods( + '^Ltests/androguard/TestActivity;$', '^getApplicationContext$' + ) + ) # External because not overwritten in class: self.assertEqual(len(other), 1) self.assertIsInstance(other[0], analysis.MethodAnalysis) @@ -235,32 +339,58 @@ class AnalysisTest(unittest.TestCase): self.assertIn(testmeth, map(itemgetter(1), other[0].get_xref_from())) # Next test internal calls - testmeth = list(filter(lambda x: x.name == 'testCalls', testcls.get_methods()))[0] + testmeth = list( + filter(lambda x: x.name == 'testCalls', testcls.get_methods()) + )[0] - self.assertEqual(len(list(dx.find_methods(testcls.name, '^testCalls$'))), 1) - self.assertEqual(list(dx.find_methods(testcls.name, '^testCalls$'))[0], testmeth) + self.assertEqual( + len(list(dx.find_methods(testcls.name, '^testCalls$'))), 1 + ) + self.assertEqual( + list(dx.find_methods(testcls.name, '^testCalls$'))[0], testmeth + ) self.assertIsInstance(testmeth, analysis.MethodAnalysis) self.assertFalse(testmeth.is_external()) self.assertIsInstance(testmeth.method, EncodedMethod) self.assertEqual(testmeth.name, 'testCalls') - xrefs = list(map(lambda x: x.full_name, map(itemgetter(1), sorted(testmeth.get_xref_to(), key=itemgetter(2))))) + xrefs = list( + map( + lambda x: x.full_name, + map( + itemgetter(1), + sorted(testmeth.get_xref_to(), key=itemgetter(2)), + ), + ) + ) self.assertEqual(len(xrefs), 4) - self.assertEqual(xrefs.pop(0), 'Ltests/androguard/TestActivity; testCall2 (J)V') - self.assertEqual(xrefs.pop(0), 'Ltests/androguard/TestIfs; testIF (I)I') - self.assertEqual(xrefs.pop(0), 'Ljava/lang/Object; getClass ()Ljava/lang/Class;') - self.assertEqual(xrefs.pop(0), 'Ljava/io/PrintStream; println (Ljava/lang/Object;)V') + self.assertEqual( + xrefs.pop(0), 'Ltests/androguard/TestActivity; testCall2 (J)V' + ) + self.assertEqual( + xrefs.pop(0), 'Ltests/androguard/TestIfs; testIF (I)I' + ) + self.assertEqual( + xrefs.pop(0), 'Ljava/lang/Object; getClass ()Ljava/lang/Class;' + ) + self.assertEqual( + xrefs.pop(0), 'Ljava/io/PrintStream; println (Ljava/lang/Object;)V' + ) - other = list(dx.find_methods('^Ltests/androguard/TestActivity;$', '^testCall2$')) + other = list( + dx.find_methods('^Ltests/androguard/TestActivity;$', '^testCall2$') + ) self.assertEqual(len(other), 1) self.assertIsInstance(other[0], analysis.MethodAnalysis) self.assertFalse(other[0].is_external()) self.assertFalse(other[0].is_android_api()) self.assertIn(testmeth, map(itemgetter(1), other[0].get_xref_from())) - other = list(dx.find_methods('^Ltests/androguard/TestIfs;$', '^testIF$')) + other = list( + dx.find_methods('^Ltests/androguard/TestIfs;$', '^testIF$') + ) self.assertEqual(len(other), 1) self.assertIsInstance(other[0], analysis.MethodAnalysis) self.assertFalse(other[0].is_external()) @@ -275,7 +405,9 @@ class AnalysisTest(unittest.TestCase): self.assertIn(testmeth, map(itemgetter(1), other[0].get_xref_from())) # Testing new_instance - testmeth = list(filter(lambda x: x.name == 'testString', testcls.get_methods()))[0] + testmeth = list( + filter(lambda x: x.name == 'testString', testcls.get_methods()) + )[0] self.assertIsInstance(testmeth, analysis.MethodAnalysis) self.assertFalse(testmeth.is_external()) self.assertIsInstance(testmeth.method, EncodedMethod) @@ -284,27 +416,39 @@ class AnalysisTest(unittest.TestCase): stringcls = dx.classes['Ljava/lang/String;'] self.assertIsInstance(stringcls, analysis.ClassAnalysis) - self.assertIn(stringcls, map(itemgetter(0), testmeth.get_xref_new_instance())) - self.assertIn(testmeth, map(itemgetter(0), stringcls.get_xref_new_instance())) + self.assertIn( + stringcls, map(itemgetter(0), testmeth.get_xref_new_instance()) + ) + self.assertIn( + testmeth, map(itemgetter(0), stringcls.get_xref_new_instance()) + ) # Not testing println, as it has too many variants... def testXrefOffsets(self): """Tests if String offsets in bytecode are correctly stored""" - _, _, dx = AnalyzeDex(os.path.join(test_dir, "data/APK/AnalysisTest.dex")) + _, _, dx = AnalyzeDex( + os.path.join(test_dir, "data/APK/AnalysisTest.dex") + ) self.assertEqual(len(dx.get_strings()), 21) - self.assertIsInstance(dx.strings['Hello world'], analysis.StringAnalysis) + self.assertIsInstance( + dx.strings['Hello world'], analysis.StringAnalysis + ) sa = dx.strings['Hello world'] self.assertEqual(len(sa.get_xref_from()), 1) self.assertEqual(len(sa.get_xref_from(with_offset=True)), 1) - self.assertEqual(next(iter(sa.get_xref_from(with_offset=True)))[2], 4) # offset is 4 + self.assertEqual( + next(iter(sa.get_xref_from(with_offset=True)))[2], 4 + ) # offset is 4 def testXrefOffsetsFields(self): """Tests if Field offsets in bytecode are correctly stored""" - _, _, dx = AnalyzeDex(os.path.join(test_dir, "data/APK/FieldsTest.dex")) + _, _, dx = AnalyzeDex( + os.path.join(test_dir, "data/APK/FieldsTest.dex") + ) self.assertEqual(len(dx.get_strings()), 20) self.assertIn('hello world', dx.strings.keys()) @@ -316,63 +460,163 @@ class AnalysisTest(unittest.TestCase): self.assertEqual(len(afield.get_xref_read()), 1) # always same method self.assertEqual(len(afield.get_xref_read(with_offset=True)), 2) - self.assertListEqual(list(sorted(map(itemgetter(2), afield.get_xref_read(with_offset=True)))), [4, 40]) - self.assertListEqual(list(map(lambda x: x.name, map(itemgetter(1), - afield.get_xref_read(with_offset=True)))), - ["foonbar", "foonbar"]) + self.assertListEqual( + list( + sorted( + map(itemgetter(2), afield.get_xref_read(with_offset=True)) + ) + ), + [4, 40], + ) + self.assertListEqual( + list( + map( + lambda x: x.name, + map(itemgetter(1), afield.get_xref_read(with_offset=True)), + ) + ), + ["foonbar", "foonbar"], + ) self.assertEqual(len(afield.get_xref_write()), 2) self.assertEqual(len(afield.get_xref_write(with_offset=True)), 2) - self.assertListEqual(list(sorted(map(itemgetter(2), afield.get_xref_write(with_offset=True)))), [10, 32]) - self.assertListEqual(list(sorted(map(lambda x: x.name, map(itemgetter(1), - afield.get_xref_write(with_offset=True))))), - sorted(["", "foonbar"])) + self.assertListEqual( + list( + sorted( + map(itemgetter(2), afield.get_xref_write(with_offset=True)) + ) + ), + [10, 32], + ) + self.assertListEqual( + list( + sorted( + map( + lambda x: x.name, + map( + itemgetter(1), + afield.get_xref_write(with_offset=True), + ), + ) + ) + ), + sorted(["", "foonbar"]), + ) cfield = next(dx.find_fields(fieldname='cfield')) # this one is static, hence it must have a write in - self.assertListEqual(list(sorted(map(lambda x: x.name, map(itemgetter(1), - cfield.get_xref_write(with_offset=True))))), - sorted([""])) - self.assertListEqual(list(sorted(map(lambda x: x.name, map(itemgetter(1), - cfield.get_xref_read(with_offset=True))))), - sorted(["foonbar"])) + self.assertListEqual( + list( + sorted( + map( + lambda x: x.name, + map( + itemgetter(1), + cfield.get_xref_write(with_offset=True), + ), + ) + ) + ), + sorted([""]), + ) + self.assertListEqual( + list( + sorted( + map( + lambda x: x.name, + map( + itemgetter(1), + cfield.get_xref_read(with_offset=True), + ), + ) + ) + ), + sorted(["foonbar"]), + ) def testPermissions(self): """Test the get_permissions and get_permission_usage methods""" - a, _, dx = AnalyzeAPK(os.path.join(test_dir, "data/APK/TestActivity.apk")) + a, _, dx = AnalyzeAPK( + os.path.join(test_dir, "data/APK/TestActivity.apk") + ) api_level = a.get_effective_target_sdk_version() - used_permissions = ['android.permission.BROADCAST_STICKY', 'android.permission.ACCESS_NETWORK_STATE'] + used_permissions = [ + 'android.permission.BROADCAST_STICKY', + 'android.permission.ACCESS_NETWORK_STATE', + ] sticky_meths = ['onMenuItemSelected', 'navigateUpTo'] - network_meths = ['getNetworkInfo', 'getActiveNetworkInfo', 'isActiveNetworkMetered'] + network_meths = [ + 'getNetworkInfo', + 'getActiveNetworkInfo', + 'isActiveNetworkMetered', + ] for _, perm in dx.get_permissions(api_level): for p in perm: self.assertIn(p, used_permissions) - meths = [x.name for x in dx.get_permission_usage('android.permission.BROADCAST_STICKY', api_level)] + meths = [ + x.name + for x in dx.get_permission_usage( + 'android.permission.BROADCAST_STICKY', api_level + ) + ] self.assertListEqual(sorted(meths), sorted(sticky_meths)) - meths = [x.name for x in dx.get_permission_usage('android.permission.ACCESS_NETWORK_STATE', api_level)] + meths = [ + x.name + for x in dx.get_permission_usage( + 'android.permission.ACCESS_NETWORK_STATE', api_level + ) + ] self.assertListEqual(sorted(meths), sorted(network_meths)) # Should give same result if no API level is given for _, perm in dx.get_permissions(): for p in perm: self.assertIn(p, used_permissions) - meths = [x.name for x in dx.get_permission_usage('android.permission.BROADCAST_STICKY')] + meths = [ + x.name + for x in dx.get_permission_usage( + 'android.permission.BROADCAST_STICKY' + ) + ] self.assertListEqual(sorted(meths), sorted(sticky_meths)) - meths = [x.name for x in dx.get_permission_usage('android.permission.ACCESS_NETWORK_STATE')] + meths = [ + x.name + for x in dx.get_permission_usage( + 'android.permission.ACCESS_NETWORK_STATE' + ) + ] self.assertListEqual(sorted(meths), sorted(network_meths)) def testHiddenAnnotation(self): - a, d, dx = AnalyzeAPK(os.path.join(test_dir, "data/APK/OPCommonTelephony.jar")) + a, d, dx = AnalyzeAPK( + os.path.join(test_dir, "data/APK/OPCommonTelephony.jar") + ) - class1 = dx.classes["Lvendor/mediatek/hardware/radio_op/V1_2/IRadioIndicationOp$Stub;"] - self.assertEqual(class1.restriction_flag, HiddenApiClassDataItem.RestrictionApiFlag.WHITELIST) - self.assertEqual(class1.domain_flag, HiddenApiClassDataItem.DomapiApiFlag.CORE_PLATFORM_API) + class1 = dx.classes[ + "Lvendor/mediatek/hardware/radio_op/V1_2/IRadioIndicationOp$Stub;" + ] + self.assertEqual( + class1.restriction_flag, + HiddenApiClassDataItem.RestrictionApiFlag.WHITELIST, + ) + self.assertEqual( + class1.domain_flag, + HiddenApiClassDataItem.DomapiApiFlag.CORE_PLATFORM_API, + ) + + class1 = dx.classes[ + "Lcom/mediatek/opcommon/telephony/MtkRILConstantsOp;" + ] + self.assertEqual( + class1.restriction_flag, + HiddenApiClassDataItem.RestrictionApiFlag.BLACKLIST, + ) + self.assertEqual( + class1.domain_flag, HiddenApiClassDataItem.DomapiApiFlag.NONE + ) - class1 = dx.classes["Lcom/mediatek/opcommon/telephony/MtkRILConstantsOp;"] - self.assertEqual(class1.restriction_flag, HiddenApiClassDataItem.RestrictionApiFlag.BLACKLIST) - self.assertEqual(class1.domain_flag, HiddenApiClassDataItem.DomapiApiFlag.NONE) if __name__ == '__main__': unittest.main() diff --git a/tests/test_annotations.py b/tests/test_annotations.py index 61ab1a61..3ee71f97 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -8,12 +8,23 @@ test_dir = os.path.dirname(os.path.abspath(__file__)) class AnnotationTest(unittest.TestCase): def testAnnotation(self): - with open(os.path.join(test_dir, 'data/APK/Annotation_classes.dex'), "rb") as f: + with open( + os.path.join(test_dir, 'data/APK/Annotation_classes.dex'), "rb" + ) as f: d = DEX(f.read()) - clazz = d.get_class('Landroid/support/v4/widget/SlidingPaneLayout$SlidingPanelLayoutImplJB;') + clazz = d.get_class( + 'Landroid/support/v4/widget/SlidingPaneLayout$SlidingPanelLayoutImplJB;' + ) annotations = clazz._get_annotation_type_ids() - self.assertIn('Landroid/support/annotation/RequiresApi;', - [clazz.CM.get_type(annotation.type_idx) for annotation in annotations]) + self.assertIn( + 'Landroid/support/annotation/RequiresApi;', + [ + clazz.CM.get_type(annotation.type_idx) + for annotation in annotations + ], + ) - self.assertIn('Landroid/support/annotation/RequiresApi;', clazz.get_annotations()) + self.assertIn( + 'Landroid/support/annotation/RequiresApi;', clazz.get_annotations() + ) diff --git a/tests/test_apk.py b/tests/test_apk.py index aa9fc53b..f1ec5a02 100644 --- a/tests/test_apk.py +++ b/tests/test_apk.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- -import unittest -import os +import binascii import glob import hashlib -import binascii -from unittest.mock import patch, MagicMock +import os +import unittest +from unittest.mock import MagicMock, patch from androguard.core import apk, axml from androguard.core.analysis.analysis import Analysis @@ -16,6 +16,7 @@ from androguard.misc import AnalyzeAPK test_dir = os.path.dirname(os.path.abspath(__file__)) + class APKTest(unittest.TestCase): def testAPK(self): for f in glob.glob(os.path.join(test_dir, 'data/APK/*.apk')): @@ -24,7 +25,9 @@ class APKTest(unittest.TestCase): self.assertTrue(a) def testAPKWrapper(self): - a, d, dx = AnalyzeAPK(os.path.join(test_dir, 'data/APK/TestActivity.apk')) + a, d, dx = AnalyzeAPK( + os.path.join(test_dir, 'data/APK/TestActivity.apk') + ) self.assertIsInstance(a, APK) self.assertIsInstance(d[0], DEX) @@ -37,8 +40,8 @@ class APKTest(unittest.TestCase): def testAPKWrapperRaw(self): with open( - os.path.join(test_dir, 'data/APK/TestActivity.apk'), 'rb') \ - as file_obj: + os.path.join(test_dir, 'data/APK/TestActivity.apk'), 'rb' + ) as file_obj: file_contents = file_obj.read() a, d, dx = AnalyzeAPK(file_contents, raw=True) self.assertIsInstance(a, APK) @@ -51,7 +54,9 @@ class APKTest(unittest.TestCase): self.assertIsNotNone(a.get_certificate(a.get_signature_name())) def testMultiDexAPK(self): - a, d, dx = AnalyzeAPK(os.path.join(test_dir, 'data/APK/app-prod-debug.apk')) + a, d, dx = AnalyzeAPK( + os.path.join(test_dir, 'data/APK/app-prod-debug.apk') + ) self.assertIsInstance(a, APK) self.assertIsInstance(d[0], DEX) @@ -63,26 +68,33 @@ class APKTest(unittest.TestCase): Test if certificates are correctly unpacked from the SignatureBlock files :return: """ - a = APK(os.path.join(test_dir, 'data/APK/TestActivity.apk'), skip_analysis=True) + a = APK( + os.path.join(test_dir, 'data/APK/TestActivity.apk'), + skip_analysis=True, + ) cert = a.get_certificate_der(a.get_signature_name()) - expected = "308201E53082014EA00302010202045114FECF300D06092A864886F70D010105" \ - "05003037310B30090603550406130255533110300E060355040A1307416E6472" \ - "6F6964311630140603550403130D416E64726F6964204465627567301E170D31" \ - "33303230383133333430375A170D3433303230313133333430375A3037310B30" \ - "090603550406130255533110300E060355040A1307416E64726F696431163014" \ - "0603550403130D416E64726F696420446562756730819F300D06092A864886F7" \ - "0D010101050003818D00308189028181009903975EC93F0F3CCB54BD1A415ECF" \ - "3505993715B8B9787F321104ACC7397D186F01201341BCC5771BB28695318E00" \ - "6E47C888D3C7EE9D952FF04DF06EDAB1B511F51AACDCD02E0ECF5AA7EC6B51BA" \ - "08C601074CF2DA579BD35054E4F77BAAAAF0AA67C33C1F1C3EEE05B5862952C0" \ - "888D39179C0EDD785BA4F47FB7DF5D5F030203010001300D06092A864886F70D" \ - "0101050500038181006B571D685D41E77744F5ED20822AE1A14199811CE649BB" \ - "B29248EB2F3CC7FB70F184C2A3D17C4F86B884FCA57EEB289ECB5964A1DDBCBD" \ - "FCFC60C6B7A33D189927845067C76ED29B42D7F2C7F6E2389A4BC009C01041A3" \ - "6E666D76D1D66467416E68659D731DC7328CB4C2E989CF59BB6D2D2756FDE7F2" \ - "B3FB733EBB4C00FD3B" + expected = ( + "308201E53082014EA00302010202045114FECF300D06092A864886F70D010105" + "05003037310B30090603550406130255533110300E060355040A1307416E6472" + "6F6964311630140603550403130D416E64726F6964204465627567301E170D31" + "33303230383133333430375A170D3433303230313133333430375A3037310B30" + "090603550406130255533110300E060355040A1307416E64726F696431163014" + "0603550403130D416E64726F696420446562756730819F300D06092A864886F7" + "0D010101050003818D00308189028181009903975EC93F0F3CCB54BD1A415ECF" + "3505993715B8B9787F321104ACC7397D186F01201341BCC5771BB28695318E00" + "6E47C888D3C7EE9D952FF04DF06EDAB1B511F51AACDCD02E0ECF5AA7EC6B51BA" + "08C601074CF2DA579BD35054E4F77BAAAAF0AA67C33C1F1C3EEE05B5862952C0" + "888D39179C0EDD785BA4F47FB7DF5D5F030203010001300D06092A864886F70D" + "0101050500038181006B571D685D41E77744F5ED20822AE1A14199811CE649BB" + "B29248EB2F3CC7FB70F184C2A3D17C4F86B884FCA57EEB289ECB5964A1DDBCBD" + "FCFC60C6B7A33D189927845067C76ED29B42D7F2C7F6E2389A4BC009C01041A3" + "6E666D76D1D66467416E68659D731DC7328CB4C2E989CF59BB6D2D2756FDE7F2" + "B3FB733EBB4C00FD3B" + ) - self.assertEqual(binascii.hexlify(cert).decode("ascii").upper(), expected) + self.assertEqual( + binascii.hexlify(cert).decode("ascii").upper(), expected + ) def testAPKCertFingerprint(self): """ @@ -92,7 +104,11 @@ class APKTest(unittest.TestCase): """ from hashlib import md5, sha1, sha256 - a = APK(os.path.join(test_dir, 'data/APK/TestActivity.apk'), skip_analysis=True) + + a = APK( + os.path.join(test_dir, 'data/APK/TestActivity.apk'), + skip_analysis=True, + ) # this one is not signed v2, it is v1 only self.assertTrue(a.is_signed_v1()) self.assertFalse(a.is_signed_v2()) @@ -107,18 +123,29 @@ class APKTest(unittest.TestCase): cert_der = a.get_certificate_der(a.get_signature_name()) # Keytool are the hashes collected by keytool -printcert -file CERT.RSA - for h2, keytool in [(md5, "99:FF:FC:37:D3:64:87:DD:BA:AB:F1:7F:94:59:89:B5"), - (sha1, "1E:0B:E4:01:F9:34:60:E0:8D:89:A3:EF:6E:27:25:55:6B:E1:D1:6B"), - (sha256, - "6F:5C:31:60:8F:1F:9E:28:5E:B6:34:3C:7C:8A:F0:7D:E8:1C:1F:B2:14:8B:53:49:BE:C9:06:44:41:44:57:6D")]: + for h2, keytool in [ + (md5, "99:FF:FC:37:D3:64:87:DD:BA:AB:F1:7F:94:59:89:B5"), + ( + sha1, + "1E:0B:E4:01:F9:34:60:E0:8D:89:A3:EF:6E:27:25:55:6B:E1:D1:6B", + ), + ( + sha256, + "6F:5C:31:60:8F:1F:9E:28:5E:B6:34:3C:7C:8A:F0:7D:E8:1C:1F:B2:14:8B:53:49:BE:C9:06:44:41:44:57:6D", + ), + ]: x = h2() x.update(cert_der) hash_hashlib = x.hexdigest() - self.assertEqual(hash_hashlib.lower(), keytool.replace(":", "").lower()) + self.assertEqual( + hash_hashlib.lower(), keytool.replace(":", "").lower() + ) def testAPKv2Signature(self): - a = APK(os.path.join(test_dir, 'data/APK/TestActivity_signed_both.apk')) + a = APK( + os.path.join(test_dir, 'data/APK/TestActivity_signed_both.apk') + ) self.assertTrue(a.is_signed_v1()) self.assertTrue(a.is_signed_v2()) @@ -129,14 +156,19 @@ class APKTest(unittest.TestCase): self.assertEqual(len(a.get_certificates_der_v2()), 1) # As we signed with the same certificate, both methods should return the # same content - self.assertEqual(a.get_certificate_der(a.get_signature_name()), - a.get_certificates_der_v2()[0]) + self.assertEqual( + a.get_certificate_der(a.get_signature_name()), + a.get_certificates_der_v2()[0], + ) from asn1crypto import x509 + self.assertIsInstance(a.get_certificates_v2()[0], x509.Certificate) # Test if the certificate is also the same as on disk - with open(os.path.join(test_dir, 'data/APK/certificate.der'), "rb") as f: + with open( + os.path.join(test_dir, 'data/APK/certificate.der'), "rb" + ) as f: cert = f.read() cert_der_v1 = a.get_certificate_der(a.get_signature_name()) cert_der_v2 = a.get_certificates_der_v2()[0] @@ -153,7 +185,8 @@ class APKTest(unittest.TestCase): def testApksignAPKs(self): # These APKs are from the apksign testcases and cover # all different signature algorithms as well as some error cases - from asn1crypto import x509, pem + from asn1crypto import pem, x509 + root = os.path.join(test_dir, 'data/APK/apksig') # Correct values generated with openssl: @@ -214,9 +247,11 @@ class APKTest(unittest.TestCase): # Test if the correct method returns True, while others return # False - m_tests = {'1': a.is_signed_v1, - '2': a.is_signed_v2, - '3': a.is_signed_v3} + m_tests = { + '1': a.is_signed_v1, + '2': a.is_signed_v2, + '3': a.is_signed_v3, + } # These APKs will raise an error excluded = [ @@ -249,7 +284,10 @@ class APKTest(unittest.TestCase): with self.assertRaises(apk.BrokenAPKError): a.is_signed_v2() continue - elif apath == "v3-only-with-rsa-pkcs1-sha512-4096-apk-sig-block-size-mismatch.apk": + elif ( + apath + == "v3-only-with-rsa-pkcs1-sha512-4096-apk-sig-block-size-mismatch.apk" + ): with self.assertRaises(apk.BrokenAPKError): a.is_signed_v3() continue @@ -264,7 +302,9 @@ class APKTest(unittest.TestCase): der = a.get_certificate_der(sig) apk.show_Certificate(c, True) apk.show_Certificate(c, False) - self.assertEqual(hashlib.sha256(der).hexdigest(), h) + self.assertEqual( + hashlib.sha256(der).hexdigest(), h + ) pass else: for sig in a.get_signature_names(): @@ -274,13 +314,18 @@ class APKTest(unittest.TestCase): # Check that we get the same signature if we take the DER der = a.get_certificate_der(sig) - self.assertEqual(hashlib.sha256(der).hexdigest(), h) + self.assertEqual( + hashlib.sha256(der).hexdigest(), h + ) if a.is_signed_v2(): if apath == "weird-compression-method.apk": with self.assertRaises(NotImplementedError): a.get_certificates_der_v2() - elif apath == "v2-only-with-rsa-pkcs1-sha256-1024-cert-not-der.apk": + elif ( + apath + == "v2-only-with-rsa-pkcs1-sha256-1024-cert-not-der.apk" + ): # FIXME # Not sure what this one should do... but the certificate fingerprint is weird # as the hash over the DER is not the same when using the certificate @@ -288,7 +333,9 @@ class APKTest(unittest.TestCase): else: for c in a.get_certificates_der_v2(): cert = x509.Certificate.load(c) - h = cert.sha256_fingerprint.replace(" ", "").lower() + h = cert.sha256_fingerprint.replace( + " ", "" + ).lower() self.assertIn(h, certfp.values()) # Check that we get the same signature if we take the DER self.assertEqual(hashlib.sha256(c).hexdigest(), h) @@ -297,21 +344,30 @@ class APKTest(unittest.TestCase): if apath == "weird-compression-method.apk": with self.assertRaises(NotImplementedError): a.get_certificates_der_v3() - elif apath == "v3-only-with-rsa-pkcs1-sha256-3072-sig-does-not-verify.apk" or \ - apath == "v3-only-cert-and-public-key-mismatch.apk": - cert = x509.Certificate.load(a.get_certificates_der_v3()[0]) + elif ( + apath + == "v3-only-with-rsa-pkcs1-sha256-3072-sig-does-not-verify.apk" + or apath == "v3-only-cert-and-public-key-mismatch.apk" + ): + cert = x509.Certificate.load( + a.get_certificates_der_v3()[0] + ) h = cert.sha256_fingerprint.replace(" ", "").lower() self.assertNotIn(h, certfp.values()) else: for c in a.get_certificates_der_v3(): cert = x509.Certificate.load(c) - h = cert.sha256_fingerprint.replace(" ", "").lower() + h = cert.sha256_fingerprint.replace( + " ", "" + ).lower() self.assertIn(h, certfp.values()) # Check that we get the same signature if we take the DER self.assertEqual(hashlib.sha256(c).hexdigest(), h) def testAPKWrapperUnsigned(self): - a, d, dx = AnalyzeAPK(os.path.join(test_dir, 'data/APK/TestActivity_unsigned.apk')) + a, d, dx = AnalyzeAPK( + os.path.join(test_dir, 'data/APK/TestActivity_unsigned.apk') + ) self.assertIsInstance(a, APK) self.assertIsInstance(d[0], DEX) self.assertIsInstance(dx, Analysis) @@ -320,14 +376,24 @@ class APKTest(unittest.TestCase): self.assertEqual(a.get_signature_names(), []) def testAPKManifest(self): - a = APK(os.path.join(test_dir, 'data/APK/TestActivity.apk'), testzip=True) + a = APK( + os.path.join(test_dir, 'data/APK/TestActivity.apk'), testzip=True + ) self.assertEqual(a.get_app_name(), "TestsAndroguardApplication") self.assertEqual(a.get_app_icon(), "res/drawable-hdpi/icon.png") - self.assertEqual(a.get_app_icon(max_dpi=120), "res/drawable-ldpi/icon.png") - self.assertEqual(a.get_app_icon(max_dpi=160), "res/drawable-mdpi/icon.png") - self.assertEqual(a.get_app_icon(max_dpi=240), "res/drawable-hdpi/icon.png") + self.assertEqual( + a.get_app_icon(max_dpi=120), "res/drawable-ldpi/icon.png" + ) + self.assertEqual( + a.get_app_icon(max_dpi=160), "res/drawable-mdpi/icon.png" + ) + self.assertEqual( + a.get_app_icon(max_dpi=240), "res/drawable-hdpi/icon.png" + ) self.assertIsNone(a.get_app_icon(max_dpi=1)) - self.assertEqual(a.get_main_activity(), "tests.androguard.TestActivity") + self.assertEqual( + a.get_main_activity(), "tests.androguard.TestActivity" + ) self.assertEqual(a.get_package(), "tests.androguard") self.assertEqual(a.get_androidversion_code(), '1') self.assertEqual(a.get_androidversion_name(), "1.0") @@ -339,36 +405,52 @@ class APKTest(unittest.TestCase): self.assertTrue(a.is_valid_APK()) def testAPKPermissions(self): - a = APK(os.path.join(test_dir, 'data/APK/a2dp.Vol_137.apk'), testzip=True) + a = APK( + os.path.join(test_dir, 'data/APK/a2dp.Vol_137.apk'), testzip=True + ) self.assertEqual(a.get_package(), "a2dp.Vol") - self.assertListEqual(sorted(a.get_permissions()), sorted(["android.permission.RECEIVE_BOOT_COMPLETED", - "android.permission.CHANGE_WIFI_STATE", - "android.permission.ACCESS_WIFI_STATE", - "android.permission.KILL_BACKGROUND_PROCESSES", - "android.permission.BLUETOOTH", - "android.permission.BLUETOOTH_ADMIN", - "com.android.launcher.permission.READ_SETTINGS", - "android.permission.RECEIVE_SMS", - "android.permission.MODIFY_AUDIO_SETTINGS", - "android.permission.READ_CONTACTS", - "android.permission.ACCESS_COARSE_LOCATION", - "android.permission.ACCESS_FINE_LOCATION", - "android.permission.ACCESS_LOCATION_EXTRA_COMMANDS", - "android.permission.WRITE_EXTERNAL_STORAGE", - "android.permission.READ_PHONE_STATE", - "android.permission.BROADCAST_STICKY", - "android.permission.GET_ACCOUNTS"])) + self.assertListEqual( + sorted(a.get_permissions()), + sorted( + [ + "android.permission.RECEIVE_BOOT_COMPLETED", + "android.permission.CHANGE_WIFI_STATE", + "android.permission.ACCESS_WIFI_STATE", + "android.permission.KILL_BACKGROUND_PROCESSES", + "android.permission.BLUETOOTH", + "android.permission.BLUETOOTH_ADMIN", + "com.android.launcher.permission.READ_SETTINGS", + "android.permission.RECEIVE_SMS", + "android.permission.MODIFY_AUDIO_SETTINGS", + "android.permission.READ_CONTACTS", + "android.permission.ACCESS_COARSE_LOCATION", + "android.permission.ACCESS_FINE_LOCATION", + "android.permission.ACCESS_LOCATION_EXTRA_COMMANDS", + "android.permission.WRITE_EXTERNAL_STORAGE", + "android.permission.READ_PHONE_STATE", + "android.permission.BROADCAST_STICKY", + "android.permission.GET_ACCOUNTS", + ] + ), + ) def testAPKActivitiesAreString(self): - a = APK(os.path.join(test_dir, 'data/APK/a2dp.Vol_137.apk'), testzip=True) + a = APK( + os.path.join(test_dir, 'data/APK/a2dp.Vol_137.apk'), testzip=True + ) activities = a.get_activities() - self.assertTrue(isinstance(activities[0], str), 'activities[0] is not of type str') + self.assertTrue( + isinstance(activities[0], str), 'activities[0] is not of type str' + ) def testAPKIntentFilters(self): from androguard.util import set_log + set_log("ERROR") - a = APK(os.path.join(test_dir, 'data/APK/a2dp.Vol_137.apk'), testzip=True) + a = APK( + os.path.join(test_dir, 'data/APK/a2dp.Vol_137.apk'), testzip=True + ) activities = a.get_activities() receivers = a.get_receivers() services = a.get_services() @@ -377,73 +459,143 @@ class APKTest(unittest.TestCase): filters = a.get_intent_filters("activity", i) if len(filters) > 0: filter_list.append(filters) - self.assertEqual([{'action': ['android.intent.action.MAIN'], 'category': ['android.intent.category.LAUNCHER']}], - filter_list) - filter_list = [] - for i in receivers: - filters = a.get_intent_filters("receiver", i) - if len(filters) > 0: - filter_list.append(filters) - for expected in [{ - 'action': ['android.intent.action.BOOT_COMPLETED', 'android.intent.action.MY_PACKAGE_REPLACED'], - 'category': ['android.intent.category.HOME']}, {'action': ['android.appwidget.action.APPWIDGET_UPDATE']}]: - assert expected in filter_list - filter_list = [] - for i in services: - filters = a.get_intent_filters("service", i) - if len(filters) > 0: - filter_list.append(filters) - self.assertEqual(filter_list, [{'action': ['android.service.notification.NotificationListenerService']}]) - - a = APK(os.path.join(test_dir, 'data/APK/com.test.intent_filter.apk'), testzip=True) - - activities = a.get_activities() - receivers = a.get_receivers() - services = a.get_services() - filter_list = [] - for i in activities: - filters = a.get_intent_filters("activity", i) - if len(filters) > 0: - filter_list.append(filters) - for expected in [{ - 'action': ['android.intent.action.VIEW'], - 'category': [ - 'android.intent.category.APP_BROWSER', - 'android.intent.category.DEFAULT', 'android.intent.category.BROWSABLE' + self.assertEqual( + [ + { + 'action': ['android.intent.action.MAIN'], + 'category': ['android.intent.category.LAUNCHER'], + } ], - 'data': [{ - 'scheme': 'testscheme', - 'host': 'testhost', - 'port': '0301', - 'path': '/testpath', - 'pathPattern': 'testpattern', - 'mimeType': 'text/html' - }] - }, { - 'action': ['android.intent.action.MAIN'], - 'category': ['android.intent.category.LAUNCHER'] - }]: + filter_list, + ) + filter_list = [] + for i in receivers: + filters = a.get_intent_filters("receiver", i) + if len(filters) > 0: + filter_list.append(filters) + for expected in [ + { + 'action': [ + 'android.intent.action.BOOT_COMPLETED', + 'android.intent.action.MY_PACKAGE_REPLACED', + ], + 'category': ['android.intent.category.HOME'], + }, + {'action': ['android.appwidget.action.APPWIDGET_UPDATE']}, + ]: + assert expected in filter_list + filter_list = [] + for i in services: + filters = a.get_intent_filters("service", i) + if len(filters) > 0: + filter_list.append(filters) + self.assertEqual( + filter_list, + [ + { + 'action': [ + 'android.service.notification.NotificationListenerService' + ] + } + ], + ) + + a = APK( + os.path.join(test_dir, 'data/APK/com.test.intent_filter.apk'), + testzip=True, + ) + + activities = a.get_activities() + receivers = a.get_receivers() + services = a.get_services() + filter_list = [] + for i in activities: + filters = a.get_intent_filters("activity", i) + if len(filters) > 0: + filter_list.append(filters) + for expected in [ + { + 'action': ['android.intent.action.VIEW'], + 'category': [ + 'android.intent.category.APP_BROWSER', + 'android.intent.category.DEFAULT', + 'android.intent.category.BROWSABLE', + ], + 'data': [ + { + 'scheme': 'testscheme', + 'host': 'testhost', + 'port': '0301', + 'path': '/testpath', + 'pathPattern': 'testpattern', + 'mimeType': 'text/html', + } + ], + }, + { + 'action': ['android.intent.action.MAIN'], + 'category': ['android.intent.category.LAUNCHER'], + }, + ]: assert expected in filter_list filter_list = [] for i in receivers: filters = a.get_intent_filters("receiver", i) if len(filters) > 0: filter_list.append(filters) - self.assertEqual(filter_list, [{'action': ['android.intent.action.VIEW'], - 'category': ['android.intent.category.DEFAULT', - 'android.intent.category.BROWSABLE'], 'data': [ - {'scheme': 'testhost', 'host': 'testscheme', 'port': '0301', 'path': '/testpath', - 'pathPattern': 'testpattern', 'mimeType': 'text/html'}]}]) + self.assertEqual( + filter_list, + [ + { + 'action': ['android.intent.action.VIEW'], + 'category': [ + 'android.intent.category.DEFAULT', + 'android.intent.category.BROWSABLE', + ], + 'data': [ + { + 'scheme': 'testhost', + 'host': 'testscheme', + 'port': '0301', + 'path': '/testpath', + 'pathPattern': 'testpattern', + 'mimeType': 'text/html', + } + ], + } + ], + ) filter_list = [] for i in services: filters = a.get_intent_filters("service", i) if len(filters) > 0: filter_list.append(filters) - self.assertEqual(filter_list, [{'action': ['android.intent.action.RESPOND_VIA_MESSAGE'], 'data': [ - {'scheme': 'testhost', 'host': 'testscheme', 'port': '0301', 'path': '/testpath', - 'pathPattern': 'testpattern', 'mimeType': 'text/html'}, - {'scheme': 'testscheme2', 'host': 'testhost2', 'port': '0301', 'path': '/testpath2', - 'pathPattern': 'testpattern2', 'mimeType': 'image/png'}]}]) + self.assertEqual( + filter_list, + [ + { + 'action': ['android.intent.action.RESPOND_VIA_MESSAGE'], + 'data': [ + { + 'scheme': 'testhost', + 'host': 'testscheme', + 'port': '0301', + 'path': '/testpath', + 'pathPattern': 'testpattern', + 'mimeType': 'text/html', + }, + { + 'scheme': 'testscheme2', + 'host': 'testhost2', + 'port': '0301', + 'path': '/testpath2', + 'pathPattern': 'testpattern2', + 'mimeType': 'image/png', + }, + ], + } + ], + ) def testEffectiveTargetSdkVersion(self): @@ -477,7 +629,11 @@ class APKTest(unittest.TestCase): a = APK(os.path.join(test_dir, 'data/APK/hello-world.apk')) self.assertEqual(25, a.get_effective_target_sdk_version()) - a = APK(os.path.join(test_dir, 'data/APK/duplicate.permisssions_9999999.apk')) + a = APK( + os.path.join( + test_dir, 'data/APK/duplicate.permisssions_9999999.apk' + ) + ) self.assertEqual(27, a.get_effective_target_sdk_version()) a = APK(os.path.join(test_dir, 'data/APK/com.politedroid_4.apk')) @@ -486,58 +642,96 @@ class APKTest(unittest.TestCase): def testUsesImpliedPermissions(self): a = APK(os.path.join(test_dir, 'data/APK/app-prod-debug.apk')) - self.assertEqual([['android.permission.READ_EXTERNAL_STORAGE', None], ], - a.get_uses_implied_permission_list()) + self.assertEqual( + [ + ['android.permission.READ_EXTERNAL_STORAGE', None], + ], + a.get_uses_implied_permission_list(), + ) a = APK(os.path.join(test_dir, 'data/APK/Invalid.apk')) - self.assertEqual([], - a.get_uses_implied_permission_list()) + self.assertEqual([], a.get_uses_implied_permission_list()) a = APK(os.path.join(test_dir, 'data/APK/TC-debug.apk')) - self.assertEqual([['android.permission.WRITE_EXTERNAL_STORAGE', None], - ['android.permission.READ_PHONE_STATE', None], - ['android.permission.READ_EXTERNAL_STORAGE', None], ], - a.get_uses_implied_permission_list()) + self.assertEqual( + [ + ['android.permission.WRITE_EXTERNAL_STORAGE', None], + ['android.permission.READ_PHONE_STATE', None], + ['android.permission.READ_EXTERNAL_STORAGE', None], + ], + a.get_uses_implied_permission_list(), + ) a = APK(os.path.join(test_dir, 'data/APK/TCDiff-debug.apk')) - self.assertEqual([['android.permission.WRITE_EXTERNAL_STORAGE', None], - ['android.permission.READ_PHONE_STATE', None], - ['android.permission.READ_EXTERNAL_STORAGE', None], ], - a.get_uses_implied_permission_list()) + self.assertEqual( + [ + ['android.permission.WRITE_EXTERNAL_STORAGE', None], + ['android.permission.READ_PHONE_STATE', None], + ['android.permission.READ_EXTERNAL_STORAGE', None], + ], + a.get_uses_implied_permission_list(), + ) a = APK(os.path.join(test_dir, 'data/APK/TestActivity.apk')) - self.assertEqual([], - a.get_uses_implied_permission_list()) + self.assertEqual([], a.get_uses_implied_permission_list()) a = APK(os.path.join(test_dir, 'data/APK/TestActivity_unsigned.apk')) - self.assertEqual([], - a.get_uses_implied_permission_list()) + self.assertEqual([], a.get_uses_implied_permission_list()) a = APK(os.path.join(test_dir, 'data/APK/Test-debug.apk')) - self.assertEqual([['android.permission.WRITE_EXTERNAL_STORAGE', None], - ['android.permission.READ_PHONE_STATE', None], - ['android.permission.READ_EXTERNAL_STORAGE', None], ], - a.get_uses_implied_permission_list()) + self.assertEqual( + [ + ['android.permission.WRITE_EXTERNAL_STORAGE', None], + ['android.permission.READ_PHONE_STATE', None], + ['android.permission.READ_EXTERNAL_STORAGE', None], + ], + a.get_uses_implied_permission_list(), + ) a = APK(os.path.join(test_dir, 'data/APK/Test-debug-unaligned.apk')) - self.assertEqual([['android.permission.WRITE_EXTERNAL_STORAGE', None], - ['android.permission.READ_PHONE_STATE', None], - ['android.permission.READ_EXTERNAL_STORAGE', None], ], - a.get_uses_implied_permission_list()) + self.assertEqual( + [ + ['android.permission.WRITE_EXTERNAL_STORAGE', None], + ['android.permission.READ_PHONE_STATE', None], + ['android.permission.READ_EXTERNAL_STORAGE', None], + ], + a.get_uses_implied_permission_list(), + ) a = APK(os.path.join(test_dir, 'data/APK/a2dp.Vol_137.apk')) - self.assertEqual([['android.permission.READ_EXTERNAL_STORAGE', None], ], - a.get_uses_implied_permission_list()) + self.assertEqual( + [ + ['android.permission.READ_EXTERNAL_STORAGE', None], + ], + a.get_uses_implied_permission_list(), + ) a = APK(os.path.join(test_dir, 'data/APK/com.politedroid_4.apk')) - self.assertEqual([['android.permission.WRITE_EXTERNAL_STORAGE', None], - ['android.permission.READ_PHONE_STATE', None], - ['android.permission.READ_EXTERNAL_STORAGE', None], ], - a.get_uses_implied_permission_list()) - a = APK(os.path.join(test_dir, 'data/APK/duplicate.permisssions_9999999.apk')) - self.assertEqual([['android.permission.READ_EXTERNAL_STORAGE', 18], ], - a.get_uses_implied_permission_list()) + self.assertEqual( + [ + ['android.permission.WRITE_EXTERNAL_STORAGE', None], + ['android.permission.READ_PHONE_STATE', None], + ['android.permission.READ_EXTERNAL_STORAGE', None], + ], + a.get_uses_implied_permission_list(), + ) + a = APK( + os.path.join( + test_dir, 'data/APK/duplicate.permisssions_9999999.apk' + ) + ) + self.assertEqual( + [ + ['android.permission.READ_EXTERNAL_STORAGE', 18], + ], + a.get_uses_implied_permission_list(), + ) a = APK(os.path.join(test_dir, 'data/APK/hello-world.apk')) - self.assertEqual([], - a.get_uses_implied_permission_list()) + self.assertEqual([], a.get_uses_implied_permission_list()) - a = APK(os.path.join(test_dir, 'data/APK/urzip-πÇÇπÇÇ现代汉语通用字-български-عربي1234.apk')) - self.assertEqual([], - a.get_uses_implied_permission_list()) + a = APK( + os.path.join( + test_dir, + 'data/APK/urzip-πÇÇπÇÇ现代汉语通用字-български-عربي1234.apk', + ) + ) + self.assertEqual([], a.get_uses_implied_permission_list()) def testNewZipWithoutModification(self): - a = APK(os.path.join(test_dir, 'data/APK/a2dp.Vol_137.apk'), testzip=True) + a = APK( + os.path.join(test_dir, 'data/APK/a2dp.Vol_137.apk'), testzip=True + ) with patch('zipfile.ZipFile') as zipFile: mockZip = MagicMock() zipFile.return_value = mockZip @@ -546,7 +740,9 @@ class APKTest(unittest.TestCase): self.assertTrue(mockZip.close.called) def testNewZipWithDeletedFile(self): - a = APK(os.path.join(test_dir, 'data/APK/a2dp.Vol_137.apk'), testzip=True) + a = APK( + os.path.join(test_dir, 'data/APK/a2dp.Vol_137.apk'), testzip=True + ) with patch('zipfile.ZipFile') as zipFile: mockZip = MagicMock() zipFile.return_value = mockZip @@ -555,36 +751,64 @@ class APKTest(unittest.TestCase): self.assertTrue(mockZip.close.called) def testNewZipWithNewFile(self): - a = APK(os.path.join(test_dir, 'data/APK/a2dp.Vol_137.apk'), testzip=True) + a = APK( + os.path.join(test_dir, 'data/APK/a2dp.Vol_137.apk'), testzip=True + ) with patch('zipfile.ZipFile') as zipFile: mockZip = MagicMock() zipFile.return_value = mockZip - a.new_zip("testout.apk", new_files={'res/menu/menu.xml': 'content'}) + a.new_zip( + "testout.apk", new_files={'res/menu/menu.xml': 'content'} + ) self.assertEqual(mockZip.writestr.call_count, 48) self.assertTrue(mockZip.close.called) def testFeatures(self): - a = APK(os.path.join(test_dir, 'data/APK/com.example.android.tvleanback.apk')) - self.assertListEqual(sorted(list(a.get_features())), ["android.hardware.microphone", - "android.hardware.touchscreen", - "android.software.leanback"]) + a = APK( + os.path.join( + test_dir, 'data/APK/com.example.android.tvleanback.apk' + ) + ) + self.assertListEqual( + sorted(list(a.get_features())), + [ + "android.hardware.microphone", + "android.hardware.touchscreen", + "android.software.leanback", + ], + ) self.assertTrue(a.is_androidtv()) self.assertFalse(a.is_wearable()) self.assertTrue(a.is_leanback()) # Second Demo App - a = APK(os.path.join(test_dir, 'data/APK/com.example.android.wearable.wear.weardrawers.apk')) - self.assertListEqual(list(a.get_features()), ["android.hardware.type.watch"]) + a = APK( + os.path.join( + test_dir, + 'data/APK/com.example.android.wearable.wear.weardrawers.apk', + ) + ) + self.assertListEqual( + list(a.get_features()), ["android.hardware.type.watch"] + ) self.assertTrue(a.is_wearable()) self.assertFalse(a.is_leanback()) self.assertFalse(a.is_androidtv()) - self.assertListEqual(list(a.get_libraries()), ["com.google.android.wearable"]) + self.assertListEqual( + list(a.get_libraries()), ["com.google.android.wearable"] + ) def testAdaptiveIcon(self): # See https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive.html - a = APK(os.path.join(test_dir, 'data/APK/com.android.example.text.styling.apk')) + a = APK( + os.path.join( + test_dir, 'data/APK/com.android.example.text.styling.apk' + ) + ) - self.assertEqual(a.get_app_icon(), "res/mipmap-anydpi-v26/ic_launcher.xml") + self.assertEqual( + a.get_app_icon(), "res/mipmap-anydpi-v26/ic_launcher.xml" + ) x = AXMLPrinter(a.get_file(a.get_app_icon())).get_xml().decode("UTF-8") self.assertIn("adaptive-icon", x) @@ -605,7 +829,10 @@ class APKTest(unittest.TestCase): self.assertIn(".xml", a.get_app_icon(max_dpi=65534)) def testPartialSignature(self): - a = APK(os.path.join(test_dir, 'data/APK/partialsignature.apk'), skip_analysis=True) + a = APK( + os.path.join(test_dir, 'data/APK/partialsignature.apk'), + skip_analysis=True, + ) self.assertIn("META-INF/CERT.RSA", a.get_files()) self.assertIn("META-INF/6AD89F48.RSA", a.get_files()) @@ -614,19 +841,34 @@ class APKTest(unittest.TestCase): self.assertIn("META-INF/6AD89F48.RSA", a.get_signature_names()) def testFrameworkResAPK(self): - a = APK(os.path.join(test_dir, 'data/APK/lineageos_nexus5_framework-res.apk')) + a = APK( + os.path.join( + test_dir, 'data/APK/lineageos_nexus5_framework-res.apk' + ) + ) self.assertEqual(a.get_app_name(), 'Android System') self.assertEqual(a.get_package(), 'android') def testPermissionLoading(self): """Test if fallbacks for permission lists are working""" - from androguard.core.api_specific_resources import load_permissions - from androguard.core.androconf import load_api_specific_resource_module, InvalidResourceError, CONF import re - aosp_permissions = os.path.join(test_dir, '../androguard/core/api_specific_resources/aosp_permissions') - levels = filter(lambda x: re.match(r'^permissions_\d+\.json$', x), - os.listdir(aosp_permissions)) + + from androguard.core.androconf import ( + CONF, + InvalidResourceError, + load_api_specific_resource_module, + ) + from androguard.core.api_specific_resources import load_permissions + + aosp_permissions = os.path.join( + test_dir, + '../androguard/core/api_specific_resources/aosp_permissions', + ) + levels = filter( + lambda x: re.match(r'^permissions_\d+\.json$', x), + os.listdir(aosp_permissions), + ) levels = list(map(lambda x: int(x[:-5].split('_')[1]), levels)) min_level = min(levels) @@ -646,8 +888,12 @@ class APKTest(unittest.TestCase): self.assertNotEqual(load_permissions(min_level + 1), {}) self.assertNotEqual(load_permissions(min_level + 1, 'groups'), {}) - self.assertEqual(load_permissions(min_level - 1), load_permissions(min_level)) - self.assertEqual(load_permissions(max_level + 1), load_permissions(max_level)) + self.assertEqual( + load_permissions(min_level - 1), load_permissions(min_level) + ) + self.assertEqual( + load_permissions(max_level + 1), load_permissions(max_level) + ) self.assertEqual(load_permissions(0), load_permissions(min_level)) self.assertEqual(load_permissions(1337), load_permissions(max_level)) @@ -658,8 +904,14 @@ class APKTest(unittest.TestCase): with self.assertRaises(InvalidResourceError): load_api_specific_resource_module('blablabla') - self.assertEqual(load_permissions(16), load_api_specific_resource_module('aosp_permissions', 16)) - self.assertEqual(load_permissions(CONF['DEFAULT_API']), load_api_specific_resource_module('aosp_permissions')) + self.assertEqual( + load_permissions(16), + load_api_specific_resource_module('aosp_permissions', 16), + ) + self.assertEqual( + load_permissions(CONF['DEFAULT_API']), + load_api_specific_resource_module('aosp_permissions'), + ) for level in levels: perm = load_permissions(level) @@ -668,16 +920,31 @@ class APKTest(unittest.TestCase): self.assertIsInstance(perm['android.permission.INTERNET'], dict) self.assertIn('description', perm['android.permission.INTERNET']) self.assertIn('label', perm['android.permission.INTERNET']) - self.assertIn('protectionLevel', perm['android.permission.INTERNET']) - self.assertIn('permissionGroup', perm['android.permission.INTERNET']) + self.assertIn( + 'protectionLevel', perm['android.permission.INTERNET'] + ) + self.assertIn( + 'permissionGroup', perm['android.permission.INTERNET'] + ) def testCustomPermissionProtectionLevel(self): - a = APK(os.path.join(test_dir, 'data/APK/com.example.android.tvleanback.apk')) - self.assertEqual(a.get_details_permissions()["com.example.android.tvleanback.ACCESS_VIDEO_DATA"][0], 'signature') + a = APK( + os.path.join( + test_dir, 'data/APK/com.example.android.tvleanback.apk' + ) + ) + self.assertEqual( + a.get_details_permissions()[ + "com.example.android.tvleanback.ACCESS_VIDEO_DATA" + ][0], + 'signature', + ) def testShortNamesInManifest(self): """Test if shortnames are correctly handled""" - a = apk.APK(os.path.join(test_dir, 'data/APK/AndroidManifest_ShortName.apk')) + a = apk.APK( + os.path.join(test_dir, 'data/APK/AndroidManifest_ShortName.apk') + ) self.assertEqual(a.get_package(), 'com.android.galaxy4') @@ -685,45 +952,73 @@ class APKTest(unittest.TestCase): self.assertEqual(len(a.get_services()), 1) self.assertEqual(a.get_activities()[0], 'com.android.galaxy4.Galaxy4') - self.assertEqual(a.get_services()[0], 'com.android.galaxy4.Galaxy4Wallpaper') + self.assertEqual( + a.get_services()[0], 'com.android.galaxy4.Galaxy4Wallpaper' + ) - self.assertEqual(list(a.get_all_attribute_value("activity", "name"))[0], 'com.android.galaxy4.Galaxy4') - self.assertEqual(list(a.get_all_attribute_value("activity", "name", format_value=False))[0], '.Galaxy4') + self.assertEqual( + list(a.get_all_attribute_value("activity", "name"))[0], + 'com.android.galaxy4.Galaxy4', + ) + self.assertEqual( + list( + a.get_all_attribute_value( + "activity", "name", format_value=False + ) + )[0], + '.Galaxy4', + ) # Test some formatting self.assertEqual(a._format_value('foo'), 'com.android.galaxy4.foo') self.assertEqual(a._format_value('.foo'), 'com.android.galaxy4.foo') - self.assertEqual(a._format_value('com.android.galaxy4.foo'), 'com.android.galaxy4.foo') + self.assertEqual( + a._format_value('com.android.galaxy4.foo'), + 'com.android.galaxy4.foo', + ) self.assertEqual(a._format_value('bla.bar.foo'), 'bla.bar.foo') self.assertEqual(a._format_value(None), None) a.package = None self.assertEqual(a._format_value('foo'), 'foo') self.assertEqual(a._format_value('.foo'), '.foo') - self.assertEqual(a._format_value('com.android.galaxy4.foo'), 'com.android.galaxy4.foo') + self.assertEqual( + a._format_value('com.android.galaxy4.foo'), + 'com.android.galaxy4.foo', + ) self.assertEqual(a._format_value('bla.bar.foo'), 'bla.bar.foo') self.assertEqual(a._format_value(None), None) def testMultipleLocaleAppName(self): """Test multiple locale appname""" - a = apk.APK(os.path.join(test_dir, 'data/APK/multiple_locale_appname_test.apk')) + a = apk.APK( + os.path.join(test_dir, 'data/APK/multiple_locale_appname_test.apk') + ) self.assertEqual(a.get_app_name(), "values") self.assertEqual(a.get_app_name(locale='en'), "values-en") self.assertEqual(a.get_app_name(locale='zh-rCN'), "values-zh-rCN") self.assertEqual(a.get_app_name(locale='zh-rTW'), "values-zh-rTW") self.assertEqual(a.get_app_name(locale='ru-rRU'), "values-ru-rRU") - def testPublicKeysofApk(self): - a = APK(os.path.join(test_dir, 'data/APK/com.example.android.wearable.wear.weardrawers.apk')) + a = APK( + os.path.join( + test_dir, + 'data/APK/com.example.android.wearable.wear.weardrawers.apk', + ) + ) pkeys = set(a.get_public_keys_der_v3() + a.get_public_keys_der_v2()) for public_key in pkeys: - from androguard.util import parse_public - from androguard.util import calculate_fingerprint + from androguard.util import calculate_fingerprint, parse_public + parsed_key = parse_public(public_key) self.assertEqual(parsed_key.algorithm, 'rsa') self.assertEqual(parsed_key.bit_size, 2048) - self.assertEqual(calculate_fingerprint(parsed_key).hex(), '98917cd03c6277d73d58b661d614c442f2981a35a5aa122a61049215ba85c1d4') + self.assertEqual( + calculate_fingerprint(parsed_key).hex(), + '98917cd03c6277d73d58b661d614c442f2981a35a5aa122a61049215ba85c1d4', + ) + if __name__ == '__main__': unittest.main(failfast=True) diff --git a/tests/test_arsc.py b/tests/test_arsc.py index 5e3ff9d9..a200494d 100644 --- a/tests/test_arsc.py +++ b/tests/test_arsc.py @@ -1,19 +1,21 @@ # -*- coding: utf-8 -*- import os import unittest + from lxml import etree test_dir = os.path.dirname(os.path.abspath(__file__)) -from androguard.core import apk, axml from operator import itemgetter +from androguard.core import apk, axml + TEST_APP_NAME = "TestsAndroguardApplication" TEST_ICONS = { 120: "res/drawable-ldpi/icon.png", 160: "res/drawable-mdpi/icon.png", 240: "res/drawable-hdpi/icon.png", - 65536: "res/drawable-hdpi/icon.png" + 65536: "res/drawable-hdpi/icon.png", } TEST_CONFIGS = { "layout": [axml.ARSCResTableConfig.default_config()], @@ -21,15 +23,17 @@ TEST_CONFIGS = { "drawable": [ axml.ARSCResTableConfig(sdkVersion=4, density=120), axml.ARSCResTableConfig(sdkVersion=4, density=160), - axml.ARSCResTableConfig(sdkVersion=4, density=240) - ] + axml.ARSCResTableConfig(sdkVersion=4, density=240), + ], } class ARSCTest(unittest.TestCase): @classmethod def setUpClass(cls): - with open(os.path.join(test_dir, 'data/APK/TestActivity.apk'), "rb") as fd: + with open( + os.path.join(test_dir, 'data/APK/TestActivity.apk'), "rb" + ) as fd: cls.apk = apk.APK(fd.read(), True) def testARSC(self): @@ -38,13 +42,20 @@ class ARSCTest(unittest.TestCase): def testAppName(self): app_name = self.apk.get_app_name() - self.assertEqual(app_name, TEST_APP_NAME, "Couldn't deduce application/activity label") + self.assertEqual( + app_name, + TEST_APP_NAME, + "Couldn't deduce application/activity label", + ) def testAppIcon(self): for wanted_density, correct_path in TEST_ICONS.items(): app_icon_path = self.apk.get_app_icon(wanted_density) - self.assertEqual(app_icon_path, correct_path, - "Incorrect icon path for requested density") + self.assertEqual( + app_icon_path, + correct_path, + "Incorrect icon path for requested density", + ) def testStrings(self): arsc = self.apk.get_android_resources() @@ -54,8 +65,14 @@ class ARSCTest(unittest.TestCase): e = etree.fromstring(arsc.get_string_resources(p, l)) - self.assertEqual(e.find("string[@name='hello']").text, 'Hello World, TestActivity! kikoololmodif') - self.assertEqual(e.find("string[@name='app_name']").text, 'TestsAndroguardApplication') + self.assertEqual( + e.find("string[@name='hello']").text, + 'Hello World, TestActivity! kikoololmodif', + ) + self.assertEqual( + e.find("string[@name='app_name']").text, + 'TestsAndroguardApplication', + ) def testResourceNames(self): """ @@ -63,16 +80,30 @@ class ARSCTest(unittest.TestCase): """ arsc = self.apk.get_android_resources() - self.assertEqual(arsc.get_resource_xml_name(0x7F040001), "@tests.androguard:string/app_name") - self.assertEqual(arsc.get_resource_xml_name(0x7F020000), "@tests.androguard:drawable/icon") + self.assertEqual( + arsc.get_resource_xml_name(0x7F040001), + "@tests.androguard:string/app_name", + ) + self.assertEqual( + arsc.get_resource_xml_name(0x7F020000), + "@tests.androguard:drawable/icon", + ) - self.assertEqual(arsc.get_resource_xml_name(0x7F040001, 'tests.androguard'), "@string/app_name") - self.assertEqual(arsc.get_resource_xml_name(0x7F020000, 'tests.androguard'), "@drawable/icon") + self.assertEqual( + arsc.get_resource_xml_name(0x7F040001, 'tests.androguard'), + "@string/app_name", + ) + self.assertEqual( + arsc.get_resource_xml_name(0x7F020000, 'tests.androguard'), + "@drawable/icon", + ) # Also test non existing resources self.assertIsNone(arsc.get_resource_xml_name(0xFFFFFFFF)) self.assertEqual(arsc.get_id('sdf', 0x7F040001), (None, None, None)) - self.assertEqual(arsc.get_id('tests.androguard', 0xFFFFFFFF), (None, None, None)) + self.assertEqual( + arsc.get_id('tests.androguard', 0xFFFFFFFF), (None, None, None) + ) def testDifferentStringLocales(self): """ @@ -84,18 +115,21 @@ class ARSCTest(unittest.TestCase): p = arsc.get_packages_names()[0] - self.assertEqual(sorted(["\x00\x00", "da", "de", "el", "fr", "ja", "ru"]), - sorted(arsc.get_locales(p))) + self.assertEqual( + sorted(["\x00\x00", "da", "de", "el", "fr", "ja", "ru"]), + sorted(arsc.get_locales(p)), + ) item = "SMSDelayText" - strings = {"\x00\x00": "Delay for reading text message", - "da": "Forsinkelse for læsning af tekst besked", - "de": "Verzögerung vor dem Lesen einer SMS", - "el": "Χρονοκαθυστέρηση ανάγνωσης μηνυμάτων SMS", - "fr": "Délai pour lire un SMS", - "ja": "テキストメッセージ読み上げの遅延", - "ru": "Задержка зачитывания SMS", - } + strings = { + "\x00\x00": "Delay for reading text message", + "da": "Forsinkelse for læsning af tekst besked", + "de": "Verzögerung vor dem Lesen einer SMS", + "el": "Χρονοκαθυστέρηση ανάγνωσης μηνυμάτων SMS", + "fr": "Délai pour lire un SMS", + "ja": "テキストメッセージ読み上げの遅延", + "ru": "Задержка зачитывания SMS", + } for k, v in strings.items(): e = etree.fromstring(arsc.get_string_resources(p, k)) self.assertEqual(e.find("string[@name='{}']".format(item)).text, v) @@ -106,23 +140,31 @@ class ARSCTest(unittest.TestCase): for res_type, test_configs in list(TEST_CONFIGS.items()): config_set = set(test_configs) - self.assertIn(res_type, configs, - "resource type %s was not found" % res_type) + self.assertIn( + res_type, configs, "resource type %s was not found" % res_type + ) for config in configs[res_type]: print(config.get_config_name_friendly()) - self.assertIn(config, config_set, - "config %r was not expected" % config) + self.assertIn( + config, config_set, "config %r was not expected" % config + ) config_set.remove(config) - self.assertEqual(len(config_set), 0, - "configs were not found: %s" % config_set) + self.assertEqual( + len(config_set), 0, "configs were not found: %s" % config_set + ) unexpected_types = set(TEST_CONFIGS.keys()) - set(configs.keys()) - self.assertEqual(len(unexpected_types), 0, - "received unexpected resource types: %s" % unexpected_types) + self.assertEqual( + len(unexpected_types), + 0, + "received unexpected resource types: %s" % unexpected_types, + ) def testFallback(self): - a = apk.APK(os.path.join(test_dir, 'data/APK/com.teleca.jamendo_35.apk')) + a = apk.APK( + os.path.join(test_dir, 'data/APK/com.teleca.jamendo_35.apk') + ) # Should use the fallback self.assertEqual(a.get_app_name(), "Jamendo") @@ -133,17 +175,44 @@ class ARSCTest(unittest.TestCase): # Default Mode, no config self.assertEqual(len(res_parser.get_res_configs(res_id)), 2) # With default config, but fallback - self.assertEqual(len(res_parser.get_res_configs(res_id, axml.ARSCResTableConfig.default_config())), 1) + self.assertEqual( + len( + res_parser.get_res_configs( + res_id, axml.ARSCResTableConfig.default_config() + ) + ), + 1, + ) # With default config but no fallback self.assertEqual( - len(res_parser.get_res_configs(res_id, axml.ARSCResTableConfig.default_config(), fallback=False)), 0) + len( + res_parser.get_res_configs( + res_id, + axml.ARSCResTableConfig.default_config(), + fallback=False, + ) + ), + 0, + ) # Also test on resolver: - self.assertListEqual(list(map(itemgetter(1), res_parser.get_resolved_res_configs(res_id))), - ["Jamendo", "Jamendo"]) - self.assertListEqual(list( - map(itemgetter(1), res_parser.get_resolved_res_configs(res_id, axml.ARSCResTableConfig.default_config()))), - ["Jamendo"]) + self.assertListEqual( + list( + map(itemgetter(1), res_parser.get_resolved_res_configs(res_id)) + ), + ["Jamendo", "Jamendo"], + ) + self.assertListEqual( + list( + map( + itemgetter(1), + res_parser.get_resolved_res_configs( + res_id, axml.ARSCResTableConfig.default_config() + ), + ) + ), + ["Jamendo"], + ) def testIDParsing(self): parser = axml.ARSCParser.parse_id diff --git a/tests/test_axml.py b/tests/test_axml.py index af80b26f..31f9dd90 100644 --- a/tests/test_axml.py +++ b/tests/test_axml.py @@ -1,14 +1,14 @@ +import io import os import unittest - from xml.dom import minidom -import io from androguard.core import axml from androguard.util import set_log test_dir = os.path.dirname(os.path.abspath(__file__)) + def is_valid_manifest(tree): # We can not really check much more... print(tree.tag, tree.attrib) @@ -43,18 +43,23 @@ def xml_compare(x1, x2, reporter=None): pass elif x2.attrib.get(name) != value: if reporter: - reporter('Attributes do not match: %s=%r, %s=%r' - % (name, value, name, x2.attrib.get(name))) + reporter( + 'Attributes do not match: %s=%r, %s=%r' + % (name, value, name, x2.attrib.get(name)) + ) return False for name in x2.attrib.keys(): if name not in x1.attrib: - if x2.tag == "application" and name == "{http://schemas.android.com/apk/res/android}debuggable": + if ( + x2.tag == "application" + and name + == "{http://schemas.android.com/apk/res/android}debuggable" + ): # Debug attribute might be added by aapt pass else: if reporter: - reporter('x2 has an attribute x1 is missing: %s' - % name) + reporter('x2 has an attribute x1 is missing: %s' % name) return False if not text_compare(x1.text, x2.text): if reporter: @@ -68,16 +73,16 @@ def xml_compare(x1, x2, reporter=None): cl2 = x2.getchildren() if len(cl1) != len(cl2): if reporter: - reporter('children length differs, %i != %i' - % (len(cl1), len(cl2))) + reporter( + 'children length differs, %i != %i' % (len(cl1), len(cl2)) + ) return False i = 0 for c1, c2 in zip(cl1, cl2): i += 1 if not xml_compare(c1, c2, reporter=reporter): if reporter: - reporter('children %i do not match: %s' - % (i, c1.tag)) + reporter('children %i do not match: %s' % (i, c1.tag)) return False return True @@ -89,73 +94,170 @@ class AXMLTest(unittest.TestCase): :return: """ # Fake, Empty AXML file - a = axml.AXMLPrinter(b"\x03\x00\x08\x00\x24\x00\x00\x00" - b"\x01\x00\x1c\x00\x1c\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00") + a = axml.AXMLPrinter( + b"\x03\x00\x08\x00\x24\x00\x00\x00" + b"\x01\x00\x1c\x00\x1c\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00" + ) self.assertIsNotNone(a) self.assertEqual(a._fix_value("hello world"), "hello world") - self.assertEqual(a._fix_value("Foobar \u000a\u000d\u0b12"), "Foobar \u000a\u000d\u0b12") + self.assertEqual( + a._fix_value("Foobar \u000a\u000d\u0b12"), + "Foobar \u000a\u000d\u0b12", + ) self.assertEqual(a._fix_value("hello \U00011234"), "hello \U00011234") self.assertEqual(a._fix_value("\uFFFF"), "_") self.assertEqual(a._fix_value("hello\x00world"), "hello") self.assertEqual(a._fix_name('', 'foobar'), ('', 'foobar')) self.assertEqual(a._fix_name('', '5foobar'), ('', '_5foobar')) - self.assertEqual(a._fix_name('', 'android:foobar'), ('', 'android_foobar')) - self.assertEqual(a._fix_name('', 'androiddd:foobar'), ('', 'androiddd_foobar')) + self.assertEqual( + a._fix_name('', 'android:foobar'), ('', 'android_foobar') + ) + self.assertEqual( + a._fix_name('', 'androiddd:foobar'), ('', 'androiddd_foobar') + ) self.assertEqual(a._fix_name('', 'sdf:foobar'), ('', 'sdf_foobar')) - self.assertEqual(a._fix_name('', 'android:sdf:foobar'), ('', 'android_sdf_foobar')) + self.assertEqual( + a._fix_name('', 'android:sdf:foobar'), ('', 'android_sdf_foobar') + ) self.assertEqual(a._fix_name('', '5:foobar'), ('', '_5_foobar')) - self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', 'foobar'), - ('{http://schemas.android.com/apk/res/android}', 'foobar')) - self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', '5foobar'), - ('{http://schemas.android.com/apk/res/android}', '_5foobar')) - self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', 'android:foobar'), - ('{http://schemas.android.com/apk/res/android}', 'android_foobar')) - self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', 'androiddd:foobar'), - ('{http://schemas.android.com/apk/res/android}', 'androiddd_foobar')) - self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', 'sdf:foobar'), - ('{http://schemas.android.com/apk/res/android}', 'sdf_foobar')) - self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', 'android:sdf:foobar'), - ('{http://schemas.android.com/apk/res/android}', 'android_sdf_foobar')) - self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', '5:foobar'), - ('{http://schemas.android.com/apk/res/android}', '_5_foobar')) + self.assertEqual( + a._fix_name( + '{http://schemas.android.com/apk/res/android}', 'foobar' + ), + ('{http://schemas.android.com/apk/res/android}', 'foobar'), + ) + self.assertEqual( + a._fix_name( + '{http://schemas.android.com/apk/res/android}', '5foobar' + ), + ('{http://schemas.android.com/apk/res/android}', '_5foobar'), + ) + self.assertEqual( + a._fix_name( + '{http://schemas.android.com/apk/res/android}', + 'android:foobar', + ), + ('{http://schemas.android.com/apk/res/android}', 'android_foobar'), + ) + self.assertEqual( + a._fix_name( + '{http://schemas.android.com/apk/res/android}', + 'androiddd:foobar', + ), + ( + '{http://schemas.android.com/apk/res/android}', + 'androiddd_foobar', + ), + ) + self.assertEqual( + a._fix_name( + '{http://schemas.android.com/apk/res/android}', 'sdf:foobar' + ), + ('{http://schemas.android.com/apk/res/android}', 'sdf_foobar'), + ) + self.assertEqual( + a._fix_name( + '{http://schemas.android.com/apk/res/android}', + 'android:sdf:foobar', + ), + ( + '{http://schemas.android.com/apk/res/android}', + 'android_sdf_foobar', + ), + ) + self.assertEqual( + a._fix_name( + '{http://schemas.android.com/apk/res/android}', '5:foobar' + ), + ('{http://schemas.android.com/apk/res/android}', '_5_foobar'), + ) # Add a namespace mapping and try again def new_nsmap(self): - return {"android": "http://schemas.android.com/apk/res/android", - "something": "http://example/url"} + return { + "android": "http://schemas.android.com/apk/res/android", + "something": "http://example/url", + } setattr(axml.AXMLParser, 'nsmap', property(new_nsmap)) self.assertEqual(a._fix_name('', 'foobar'), ('', 'foobar')) self.assertEqual(a._fix_name('', '5foobar'), ('', '_5foobar')) - self.assertEqual(a._fix_name('', 'android:foobar'), ('{http://schemas.android.com/apk/res/android}', 'foobar')) - self.assertEqual(a._fix_name('', 'something:foobar'), ('{http://example/url}', 'foobar')) - self.assertEqual(a._fix_name('', 'androiddd:foobar'), ('', 'androiddd_foobar')) + self.assertEqual( + a._fix_name('', 'android:foobar'), + ('{http://schemas.android.com/apk/res/android}', 'foobar'), + ) + self.assertEqual( + a._fix_name('', 'something:foobar'), + ('{http://example/url}', 'foobar'), + ) + self.assertEqual( + a._fix_name('', 'androiddd:foobar'), ('', 'androiddd_foobar') + ) self.assertEqual(a._fix_name('', 'sdf:foobar'), ('', 'sdf_foobar')) - self.assertEqual(a._fix_name('', 'android:sdf:foobar'), - ('{http://schemas.android.com/apk/res/android}', 'sdf_foobar')) + self.assertEqual( + a._fix_name('', 'android:sdf:foobar'), + ('{http://schemas.android.com/apk/res/android}', 'sdf_foobar'), + ) self.assertEqual(a._fix_name('', '5:foobar'), ('', '_5_foobar')) - self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', 'foobar'), - ('{http://schemas.android.com/apk/res/android}', 'foobar')) - self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', '5foobar'), - ('{http://schemas.android.com/apk/res/android}', '_5foobar')) - self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', 'android:foobar'), - ('{http://schemas.android.com/apk/res/android}', 'android_foobar')) - self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', 'androiddd:foobar'), - ('{http://schemas.android.com/apk/res/android}', 'androiddd_foobar')) - self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', 'sdf:foobar'), - ('{http://schemas.android.com/apk/res/android}', 'sdf_foobar')) - self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', 'android:sdf:foobar'), - ('{http://schemas.android.com/apk/res/android}', 'android_sdf_foobar')) - self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', '5:foobar'), - ('{http://schemas.android.com/apk/res/android}', '_5_foobar')) + self.assertEqual( + a._fix_name( + '{http://schemas.android.com/apk/res/android}', 'foobar' + ), + ('{http://schemas.android.com/apk/res/android}', 'foobar'), + ) + self.assertEqual( + a._fix_name( + '{http://schemas.android.com/apk/res/android}', '5foobar' + ), + ('{http://schemas.android.com/apk/res/android}', '_5foobar'), + ) + self.assertEqual( + a._fix_name( + '{http://schemas.android.com/apk/res/android}', + 'android:foobar', + ), + ('{http://schemas.android.com/apk/res/android}', 'android_foobar'), + ) + self.assertEqual( + a._fix_name( + '{http://schemas.android.com/apk/res/android}', + 'androiddd:foobar', + ), + ( + '{http://schemas.android.com/apk/res/android}', + 'androiddd_foobar', + ), + ) + self.assertEqual( + a._fix_name( + '{http://schemas.android.com/apk/res/android}', 'sdf:foobar' + ), + ('{http://schemas.android.com/apk/res/android}', 'sdf_foobar'), + ) + self.assertEqual( + a._fix_name( + '{http://schemas.android.com/apk/res/android}', + 'android:sdf:foobar', + ), + ( + '{http://schemas.android.com/apk/res/android}', + 'android_sdf_foobar', + ), + ) + self.assertEqual( + a._fix_name( + '{http://schemas.android.com/apk/res/android}', '5:foobar' + ), + ('{http://schemas.android.com/apk/res/android}', '_5_foobar'), + ) def testNoStringPool(self): """Test if a single header without string pool is rejected""" @@ -171,30 +273,37 @@ class AXMLTest(unittest.TestCase): def testWrongHeaderSize(self): """Test if a wrong header size is rejected""" - a = axml.AXMLPrinter(b"\x03\x00\x10\x00\x2c\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x01\x00\x1c\x00\x1c\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00") + a = axml.AXMLPrinter( + b"\x03\x00\x10\x00\x2c\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x01\x00\x1c\x00\x1c\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00" + ) self.assertFalse(a.is_valid()) def testWrongStringPoolHeader(self): """Test if a wrong header type is rejected""" - a = axml.AXMLPrinter(b"\x03\x00\x08\x00\x24\x00\x00\x00" b"\xDE\xAD\x1c\x00\x1c\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00") + a = axml.AXMLPrinter( + b"\x03\x00\x08\x00\x24\x00\x00\x00" + b"\xDE\xAD\x1c\x00\x1c\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00" + ) self.assertFalse(a.is_valid()) def testWrongStringPoolSize(self): """Test if a wrong string pool header size is rejected""" - a = axml.AXMLPrinter(b"\x03\x00\x08\x00\x2c\x00\x00\x00" - b"\x01\x00\x24\x00\x24\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00") + a = axml.AXMLPrinter( + b"\x03\x00\x08\x00\x2c\x00\x00\x00" + b"\x01\x00\x24\x00\x24\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00" + ) self.assertFalse(a.is_valid()) def testArscHeader(self): @@ -204,26 +313,53 @@ class AXMLTest(unittest.TestCase): self.assertIn("Can not read over the buffer size", str(cnx.exception)) with self.assertRaises(axml.ResParserError) as cnx: - axml.ARSCHeader(io.BufferedReader(io.BytesIO(b"\x02\x01\xFF\xFF\x08\x00\x00\x00"))) + axml.ARSCHeader( + io.BufferedReader( + io.BytesIO(b"\x02\x01\xFF\xFF\x08\x00\x00\x00") + ) + ) self.assertIn("smaller than header size", str(cnx.exception)) with self.assertRaises(axml.ResParserError) as cnx: - axml.ARSCHeader(io.BufferedReader(io.BytesIO(b"\x02\x01\x01\x00\x08\x00\x00\x00"))) - self.assertIn("declared header size is smaller than required size", str(cnx.exception)) + axml.ARSCHeader( + io.BufferedReader( + io.BytesIO(b"\x02\x01\x01\x00\x08\x00\x00\x00") + ) + ) + self.assertIn( + "declared header size is smaller than required size", + str(cnx.exception), + ) with self.assertRaises(axml.ResParserError) as cnx: - axml.ARSCHeader(io.BufferedReader(io.BytesIO(b"\x02\x01\x08\x00\x04\x00\x00\x00"))) - self.assertIn("declared chunk size is smaller than required size", str(cnx.exception)) + axml.ARSCHeader( + io.BufferedReader( + io.BytesIO(b"\x02\x01\x08\x00\x04\x00\x00\x00") + ) + ) + self.assertIn( + "declared chunk size is smaller than required size", + str(cnx.exception), + ) - a = axml.ARSCHeader(io.BufferedReader(io.BytesIO(b"\xCA\xFE\x08\x00\x10\x00\x00\x00" - b"\xDE\xEA\xBE\xEF\x42\x42\x42\x42"))) + a = axml.ARSCHeader( + io.BufferedReader( + io.BytesIO( + b"\xCA\xFE\x08\x00\x10\x00\x00\x00" + b"\xDE\xEA\xBE\xEF\x42\x42\x42\x42" + ) + ) + ) self.assertEqual(a.type, 0xFECA) self.assertEqual(a.header_size, 8) self.assertEqual(a.size, 16) self.assertEqual(a.start, 0) self.assertEqual(a.end, 16) - self.assertEqual(repr(a), "") + self.assertEqual( + repr(a), + "", + ) def testAndroidManifest(self): filenames = [ diff --git a/tests/test_callgraph.py b/tests/test_callgraph.py index 5076278c..49df0878 100644 --- a/tests/test_callgraph.py +++ b/tests/test_callgraph.py @@ -1,11 +1,12 @@ -from androguard.misc import AnalyzeAPK -from androguard.core.analysis.analysis import ExternalMethod - import os import unittest +from androguard.core.analysis.analysis import ExternalMethod +from androguard.misc import AnalyzeAPK + test_dir = os.path.dirname(os.path.abspath(__file__)) + class TestCallgraph(unittest.TestCase): @classmethod def setUpClass(cls): @@ -18,9 +19,9 @@ class TestCallgraph(unittest.TestCase): for n in callgraph: if isinstance(n, ExternalMethod): - num_external+=1 + num_external += 1 else: - num_internal+=1 + num_internal += 1 return callgraph.number_of_nodes(), num_external, num_internal @@ -28,10 +29,14 @@ class TestCallgraph(unittest.TestCase): """test callgraph generated with default parameter values""" callgraph = self.dx.get_call_graph() - total_nodes, total_external_nodes, total_internal_nodes = self._get_num_nodes(callgraph) + total_nodes, total_external_nodes, total_internal_nodes = ( + self._get_num_nodes(callgraph) + ) # ensure total of internal and external nodes equals the total nodes - self.assertEqual(total_nodes, total_external_nodes + total_internal_nodes) + self.assertEqual( + total_nodes, total_external_nodes + total_internal_nodes + ) # total num nodes self.assertEqual(total_nodes, 3600) @@ -45,13 +50,14 @@ class TestCallgraph(unittest.TestCase): # total num edges self.assertEqual(callgraph.number_of_edges(), 4490) - def testCallgraphFilterClassname(self): """test callgraph with classname filter parameter""" callgraph = self.dx.get_call_graph(classname='Ltests/androguard/*') - total_nodes, total_external_nodes, total_internal_nodes = self._get_num_nodes(callgraph) - + total_nodes, total_external_nodes, total_internal_nodes = ( + self._get_num_nodes(callgraph) + ) + self.assertEqual(total_nodes, 165) self.assertEqual(total_external_nodes, 41) self.assertEqual(total_internal_nodes, 124) @@ -62,8 +68,10 @@ class TestCallgraph(unittest.TestCase): """test callgraph with methodname filter parameter""" callgraph = self.dx.get_call_graph(methodname='Test*') - total_nodes, total_external_nodes, total_internal_nodes = self._get_num_nodes(callgraph) - + total_nodes, total_external_nodes, total_internal_nodes = ( + self._get_num_nodes(callgraph) + ) + self.assertEqual(total_nodes, 36) self.assertEqual(total_external_nodes, 12) self.assertEqual(total_internal_nodes, 24) @@ -71,10 +79,14 @@ class TestCallgraph(unittest.TestCase): def testCallgraphFilterDescriptor(self): """test callgraph with descriptor filter parameter""" - callgraph = self.dx.get_call_graph(descriptor='\(LTestDefaultPackage;\sI\sI\sLTestDefaultPackage\$TestInnerClass;\)V') + callgraph = self.dx.get_call_graph( + descriptor='\(LTestDefaultPackage;\sI\sI\sLTestDefaultPackage\$TestInnerClass;\)V' + ) + + total_nodes, total_external_nodes, total_internal_nodes = ( + self._get_num_nodes(callgraph) + ) - total_nodes, total_external_nodes, total_internal_nodes = self._get_num_nodes(callgraph) - self.assertEqual(total_nodes, 2) self.assertEqual(total_external_nodes, 0) self.assertEqual(total_internal_nodes, 2) @@ -86,16 +98,30 @@ class TestCallgraph(unittest.TestCase): src_node, dst_node = list(callgraph.edges)[0] # check source node - self.assertEqual(src_node.get_class_name(), 'LTestDefaultPackage$TestInnerClass;') + self.assertEqual( + src_node.get_class_name(), 'LTestDefaultPackage$TestInnerClass;' + ) self.assertEqual(src_node.get_name(), '') - self.assertEqual(src_node.get_access_flags_string(), 'synthetic constructor') - self.assertEqual(src_node.get_descriptor(), '(LTestDefaultPackage; I I LTestDefaultPackage$TestInnerClass;)V') + self.assertEqual( + src_node.get_access_flags_string(), 'synthetic constructor' + ) + self.assertEqual( + src_node.get_descriptor(), + '(LTestDefaultPackage; I I LTestDefaultPackage$TestInnerClass;)V', + ) # check dest node - self.assertEqual(dst_node.get_class_name(), 'LTestDefaultPackage$TestInnerClass;') + self.assertEqual( + dst_node.get_class_name(), 'LTestDefaultPackage$TestInnerClass;' + ) self.assertEqual(dst_node.get_name(), '') - self.assertEqual(dst_node.get_access_flags_string(), 'private constructor') - self.assertEqual(dst_node.get_descriptor(), '(LTestDefaultPackage; I I)V') + self.assertEqual( + dst_node.get_access_flags_string(), 'private constructor' + ) + self.assertEqual( + dst_node.get_descriptor(), '(LTestDefaultPackage; I I)V' + ) + if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/test_cli_decompile.py b/tests/test_cli_decompile.py index 35805e39..ad53424f 100644 --- a/tests/test_cli_decompile.py +++ b/tests/test_cli_decompile.py @@ -1,16 +1,19 @@ -from androguard.cli.main import export_apps_to_format -from androguard import session - import os import shutil import unittest +from androguard import session +from androguard.cli.main import export_apps_to_format + test_dir = os.path.dirname(os.path.abspath(__file__)) + class TestCLIDecompile(unittest.TestCase): @classmethod def setUpClass(cls): - test_testactivity_apk_path = os.path.join(test_dir, 'data/APK/TestActivity.apk') + test_testactivity_apk_path = os.path.join( + test_dir, 'data/APK/TestActivity.apk' + ) cls.s = session.Session() with open(test_testactivity_apk_path, "rb") as fd: cls.s.add(test_testactivity_apk_path, fd.read()) @@ -20,18 +23,19 @@ class TestCLIDecompile(unittest.TestCase): export_apps_to_format( None, self.s, - os.path.join(test_dir,'tmp_TestActivity_decompilation'), + os.path.join(test_dir, 'tmp_TestActivity_decompilation'), None, False, None, - None) - + None, + ) + @classmethod def tearDownClass(cls): decomp_dir = os.path.join(test_dir, 'tmp_TestActivity_decompilation') if os.path.exists(decomp_dir): shutil.rmtree(decomp_dir) - + if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/test_decompiler.py b/tests/test_decompiler.py index c6805b79..394e2183 100644 --- a/tests/test_decompiler.py +++ b/tests/test_decompiler.py @@ -1,10 +1,9 @@ -import unittest - -from androguard.misc import AnalyzeDex import os import re -from androguard.misc import AnalyzeAPK -from androguard.decompiler.decompile import DvMethod, DvClass +import unittest + +from androguard.decompiler.decompile import DvClass, DvMethod +from androguard.misc import AnalyzeAPK, AnalyzeDex test_dir = os.path.dirname(os.path.abspath(__file__)) @@ -13,14 +12,16 @@ class DecompilerTest(unittest.TestCase): def testSimplification(self): h, d, dx = AnalyzeDex(os.path.join(test_dir, 'data/APK/Test.dex')) - z, = d.get_classes() + (z,) = d.get_classes() self.assertIn("return ((23 - p3) | ((p3 + 66) & 26));", z.get_source()) def testArrays(self): - h, d, dx = AnalyzeDex(os.path.join(test_dir, 'data/APK/FillArrays.dex')) + h, d, dx = AnalyzeDex( + os.path.join(test_dir, 'data/APK/FillArrays.dex') + ) - z, = d.get_classes() + (z,) = d.get_classes() self.assertIn("{20, 30, 40, 50};", z.get_source()) self.assertIn("{1, 2, 3, 4, 5, 999, 10324234};", z.get_source()) @@ -33,7 +34,9 @@ class DecompilerTest(unittest.TestCase): def test_all_decompiler(self): # Generate test cases for this APK: - a, d, dx = AnalyzeAPK(os.path.join(test_dir, 'data/APK/hello-world.apk')) + a, d, dx = AnalyzeAPK( + os.path.join(test_dir, 'data/APK/hello-world.apk') + ) for c in d[0].get_classes(): test_name = re.sub("[^a-zA-Z0-9_]", "_", str(c.get_name())[1:-1]) # Test the decompilation of a single class diff --git a/tests/test_decompiler_dataflow.py b/tests/test_decompiler_dataflow.py index 7f22819b..045f545c 100644 --- a/tests/test_decompiler_dataflow.py +++ b/tests/test_decompiler_dataflow.py @@ -1,15 +1,11 @@ """Tests for def_use.""" +import collections import os +import unittest from unittest import mock -import collections -import unittest - -from androguard.decompiler import dataflow -from androguard.decompiler import graph -from androguard.decompiler import instruction -from androguard.decompiler import basic_blocks +from androguard.decompiler import basic_blocks, dataflow, graph, instruction test_dir = os.path.dirname(os.path.abspath(__file__)) @@ -23,8 +19,8 @@ class DataflowTest(unittest.TestCase): def _CreateMockNode(self, node_name, start_ins_idx, lins): mock_node = mock.create_autospec( - basic_blocks.BasicBlock, - _name=node_name) + basic_blocks.BasicBlock, _name=node_name + ) mock_node.__repr__ = mock.Mock(return_value=node_name) loc_ins = [] ins_idx = start_ins_idx @@ -92,7 +88,7 @@ class DataflowTest(unittest.TestCase): n5: [n6, n7], n6: [n8], n7: [n8], - n8: [n9] + n8: [n9], } preds = collections.defaultdict(list) for pred, lsucs in sucs.items(): @@ -129,7 +125,7 @@ class DataflowTest(unittest.TestCase): n7: {-2, -1, 0, 7}, n8: {-2, -1, 0, 1, 6, 7, 8}, n9: {-2, -1, 0, 1, 3, 6, 7, 8}, - dummy_exit_mock: {-2, -1, 0, 1, 3, 6, 7, 8} + dummy_exit_mock: {-2, -1, 0, 1, 3, 6, 7, 8}, } expected_R = { n1: {-2, -1}, @@ -141,14 +137,14 @@ class DataflowTest(unittest.TestCase): n7: {-2, -1, 0, 1}, n8: {-2, -1, 0, 1, 6, 7}, n9: {-2, -1, 0, 1, 3, 6, 7, 8}, - dummy_exit_mock: {-2, -1, 0, 1, 3, 6, 7, 8} + dummy_exit_mock: {-2, -1, 0, 1, 3, 6, 7, 8}, } expected_def_to_loc = { 'a': {-1}, 'b': {-2}, 'c': {0, 6}, 'd': {1, 7}, - 'ret': {3, 8} + 'ret': {3, 8}, } self.assertDictEqual(analysis.A, expected_A) self.assertDictEqual(analysis.R, expected_R) @@ -176,11 +172,10 @@ class DataflowTest(unittest.TestCase): 'b': {-2}, 'c': {0, 6}, 'd': {1, 7}, - 'ret': {3, 8} + 'ret': {3, 8}, } mock_analysis.defs = { - n1: {'c': {0}, - 'd': {1}}, + n1: {'c': {0}, 'd': {1}}, n2: {}, n3: {'ret': {3}}, n4: {}, @@ -188,7 +183,7 @@ class DataflowTest(unittest.TestCase): n6: {'c': {6}}, n7: {'d': {7}}, n8: {'ret': {8}}, - n9: {} + n9: {}, } mock_analysis.R = { n1: {-2, -1}, @@ -199,7 +194,7 @@ class DataflowTest(unittest.TestCase): n6: {-2, -1, 0, 1}, n7: {-2, -1, 0, 1}, n8: {-2, -1, 0, 1, 6, 7}, - n9: {-2, -1, 0, 1, 3, 6, 7, 8} + n9: {-2, -1, 0, 1, 3, 6, 7, 8}, } expected_du = { ('a', -1): [0], @@ -208,7 +203,7 @@ class DataflowTest(unittest.TestCase): ('c', 6): [8], ('d', 1): [3, 4, 5, 6, 7], ('ret', 3): [9], - ('ret', 8): [9] + ('ret', 8): [9], } expected_ud = { ('a', 0): [-1], @@ -223,7 +218,7 @@ class DataflowTest(unittest.TestCase): ('d', 5): [1], ('d', 6): [1], ('d', 7): [1], - ('ret', 9): [3, 8] + ('ret', 9): [3, 8], } ud, du = dataflow.build_def_use(graph_mock, mock.sentinel) @@ -248,21 +243,15 @@ class DataflowTest(unittest.TestCase): graph_mock.rpo = [n1, n2, n3, n4, n5, n6, n7] mock_analysis = mock_reach_def.return_value - mock_analysis.def_to_loc = { - 0: {0, 4, 5, 7}, - 1: {6}, - 2: {-1}, - 3: {-2} - } + mock_analysis.def_to_loc = {0: {0, 4, 5, 7}, 1: {6}, 2: {-1}, 3: {-2}} mock_analysis.defs = { n1: {0: {0}}, n2: {}, n3: {}, n4: {0: {4}}, n5: {0: {5}}, - n6: {0: {7}, - 1: {6}}, - n7: {} + n6: {0: {7}, 1: {6}}, + n7: {}, } mock_analysis.R = { @@ -272,7 +261,7 @@ class DataflowTest(unittest.TestCase): n4: {0, -2, -1}, n5: {0, -2, -1}, n6: {0, -1, -2}, - n7: {4, -1, 6, 7, -2, 5} + n7: {4, -1, 6, 7, -2, 5}, } expected_du = { @@ -282,7 +271,7 @@ class DataflowTest(unittest.TestCase): (0, 7): [8], (1, 6): [7], (2, -1): [6, 1], - (3, -2): [2, 3] + (3, -2): [2, 3], } expected_ud = { (0, 5): [0], @@ -292,7 +281,7 @@ class DataflowTest(unittest.TestCase): (2, 1): [-1], (2, 6): [-1], (3, 2): [-2], - (3, 3): [-2] + (3, 3): [-2], } ud, du = dataflow.build_def_use(graph_mock, mock.sentinel) @@ -307,7 +296,7 @@ class DataflowTest(unittest.TestCase): ('c', 6): [8], ('d', 1): [3, 4, 5, 6, 7], ('ret', 3): [9], - ('ret', 8): [9] + ('ret', 8): [9], } ud = { ('a', 0): [-1], @@ -322,14 +311,14 @@ class DataflowTest(unittest.TestCase): ('d', 5): [1], ('d', 6): [1], ('d', 7): [1], - ('ret', 9): [3, 8] + ('ret', 9): [3, 8], } expected_groups = { 'a': [([-1], [0])], 'b': [([-2], [1])], 'c': [([0, 6], [8, 2, 5, 6, 7])], 'd': [([1], [3, 4, 5, 6, 7])], - 'ret': [([3, 8], [9])] + 'ret': [([3, 8], [9])], } groups = dataflow.group_variables(['a', 'b', 'c', 'd', 'ret'], du, ud) self.assertCountEqual(groups, expected_groups) @@ -342,7 +331,7 @@ class DataflowTest(unittest.TestCase): (0, 7): [8], (1, 6): [7], (2, -1): [6, 1], - (3, -2): [2, 3] + (3, -2): [2, 3], } ud = { (0, 5): [0], @@ -352,14 +341,14 @@ class DataflowTest(unittest.TestCase): (2, 1): [-1], (2, 6): [-1], (3, 2): [-2], - (3, 3): [-2] + (3, 3): [-2], } groups = dataflow.group_variables([0, 1, 2, 3], du, ud) expected_groups = { 0: [([0], [5, 7]), ([4, 5, 7], [8])], 1: [([6], [7])], 2: [([-1], [1, 6])], - 3: [([-2], [2, 3])] + 3: [([-2], [2, 3])], } self.assertCountEqual(groups, expected_groups) for entry in groups: @@ -372,11 +361,12 @@ class DataflowTest(unittest.TestCase): 'b': [([-2], [1])], 'c': [([0, 6], [2, 5, 6, 7, 8])], 'd': [([1], [3, 4, 5, 6, 7])], - 'ret': [([3, 8], [9])] + 'ret': [([3, 8], [9])], } group_variables_mock.return_value = group dataflow.split_variables( - mock.sentinel, [0, 1, 2, 3, 4], mock.sentinel, mock.sentinel) + mock.sentinel, [0, 1, 2, 3, 4], mock.sentinel, mock.sentinel + ) @mock.patch.object(dataflow, 'group_variables') def testSplitVariablesIfBool(self, group_variables_mock): @@ -384,7 +374,7 @@ class DataflowTest(unittest.TestCase): 0: [([0], [5, 7]), ([4, 5, 7], [8])], 1: [([6], [7])], 2: [([-1], [1, 6])], - 3: [([-2], [2, 3])] + 3: [([-2], [2, 3])], } group_variables_mock.return_value = group param1_mock = mock.Mock() @@ -399,7 +389,7 @@ class DataflowTest(unittest.TestCase): (0, 7): [8], (1, 6): [7], (2, -1): [6, 1], - (3, -2): [2, 3] + (3, -2): [2, 3], } ud = { (0, 5): [0], @@ -409,7 +399,7 @@ class DataflowTest(unittest.TestCase): (2, 1): [-1], (2, 6): [-1], (3, 2): [-2], - (3, 3): [-2] + (3, 3): [-2], } graph_mock = mock.Mock() dataflow.split_variables(graph_mock, lvars, du, ud) @@ -421,7 +411,7 @@ class DataflowTest(unittest.TestCase): (4, 0): [7, 5], (5, 4): [8], (5, 5): [8], - (5, 7): [8] + (5, 7): [8], } expected_ud = { (1, 7): [6], @@ -431,7 +421,7 @@ class DataflowTest(unittest.TestCase): (3, 3): [-2], (4, 5): [0], (4, 7): [0], - (5, 8): [4, 5, 7] + (5, 8): [4, 5, 7], } self.assertEqual(du, expected_du) self.assertEqual(ud, expected_ud) diff --git a/tests/test_decompiler_dominator.py b/tests/test_decompiler_dominator.py index 9e648c17..b6853484 100644 --- a/tests/test_decompiler_dominator.py +++ b/tests/test_decompiler_dominator.py @@ -1,6 +1,7 @@ """Tests for graph.""" import unittest + from androguard.decompiler import graph @@ -25,7 +26,7 @@ class DominatorTest(unittest.TestCase): 'i': ['k'], 'j': ['i'], 'k': ['i', 'r'], - 'l': ['h'] + 'l': ['h'], } expected_dominators = { 'r': None, @@ -40,12 +41,14 @@ class DominatorTest(unittest.TestCase): 'i': 'r', 'j': 'g', 'k': 'r', - 'l': 'd' + 'l': 'd', } self.graph.entry = 'r' self.graph.edges = edges self.graph.nodes = list(expected_dominators.keys()) - self.assertEqual(expected_dominators, self.graph.immediate_dominators()) + self.assertEqual( + expected_dominators, self.graph.immediate_dominators() + ) def testFirstGraph(self): edges = { @@ -68,7 +71,7 @@ class DominatorTest(unittest.TestCase): 'z2': ['z1', 'z3'], 'z3': ['z2', 'z4'], 'z4': ['z3', 'z5'], - 'z5': ['z4'] + 'z5': ['z4'], } expected_dominators = { 'r': None, @@ -91,12 +94,14 @@ class DominatorTest(unittest.TestCase): 'z2': 'r', 'z3': 'r', 'z4': 'r', - 'z5': 'r' + 'z5': 'r', } self.graph.entry = 'r' self.graph.edges = edges self.graph.nodes = list(expected_dominators.keys()) - self.assertEqual(expected_dominators, self.graph.immediate_dominators()) + self.assertEqual( + expected_dominators, self.graph.immediate_dominators() + ) def testSecondGraph(self): edges = { @@ -106,7 +111,7 @@ class DominatorTest(unittest.TestCase): 'x11': ['x12', 'x22'], 'x12': ['x11'], 'x21': ['x22'], - 'x22': ['x21'] + 'x22': ['x21'], } expected_dominators = { 'r': None, @@ -115,12 +120,14 @@ class DominatorTest(unittest.TestCase): 'x11': 'r', 'x12': 'r', 'x21': 'r', - 'x22': 'r' + 'x22': 'r', } self.graph.entry = 'r' self.graph.edges = edges self.graph.nodes = list(expected_dominators.keys()) - self.assertEqual(expected_dominators, self.graph.immediate_dominators()) + self.assertEqual( + expected_dominators, self.graph.immediate_dominators() + ) def testThirdGraph(self): edges = { @@ -128,7 +135,7 @@ class DominatorTest(unittest.TestCase): 'w': ['x1', 'x2'], 'y1': ['y2'], 'y2': ['x2'], - 'x2': ['x1'] + 'x2': ['x1'], } expected_dominators = { 'r': None, @@ -137,12 +144,14 @@ class DominatorTest(unittest.TestCase): 'y1': 'r', 'y2': 'y1', 'x1': 'r', - 'x2': 'r' + 'x2': 'r', } self.graph.entry = 'r' self.graph.edges = edges self.graph.nodes = list(expected_dominators.keys()) - self.assertEqual(expected_dominators, self.graph.immediate_dominators()) + self.assertEqual( + expected_dominators, self.graph.immediate_dominators() + ) def testFourthGraph(self): edges = {'r': ['x1', 'y1', 'y2'], 'x1': ['x2'], 'x2': ['y1', 'y2']} @@ -151,12 +160,14 @@ class DominatorTest(unittest.TestCase): 'x1': 'r', 'x2': 'x1', 'y1': 'r', - 'y2': 'r' + 'y2': 'r', } self.graph.entry = 'r' self.graph.edges = edges self.graph.nodes = list(expected_dominators.keys()) - self.assertEqual(expected_dominators, self.graph.immediate_dominators()) + self.assertEqual( + expected_dominators, self.graph.immediate_dominators() + ) def testFifthGraph(self): edges = { @@ -168,7 +179,7 @@ class DominatorTest(unittest.TestCase): 'e': ['c', 'f'], 'f': ['i'], 'g': ['h'], - 'h': ['d', 'f', 'i'] + 'h': ['d', 'f', 'i'], } expected_dominators = { 'r': None, @@ -180,12 +191,14 @@ class DominatorTest(unittest.TestCase): 'f': 'b', 'g': 'b', 'h': 'g', - 'i': 'r' + 'i': 'r', } self.graph.entry = 'r' self.graph.edges = edges self.graph.nodes = list(expected_dominators.keys()) - self.assertEqual(expected_dominators, self.graph.immediate_dominators()) + self.assertEqual( + expected_dominators, self.graph.immediate_dominators() + ) def testLinearVitGraph(self): edges = { @@ -198,7 +211,7 @@ class DominatorTest(unittest.TestCase): 'x4': ['x3', 'x5'], 'x5': ['x4', 'x6'], 'x6': ['x5', 'x7'], - 'x7': ['x6'] + 'x7': ['x6'], } expected_dominators = { 'r': None, @@ -210,12 +223,14 @@ class DominatorTest(unittest.TestCase): 'x4': 'r', 'x5': 'r', 'x6': 'r', - 'x7': 'r' + 'x7': 'r', } self.graph.entry = 'r' self.graph.edges = edges self.graph.nodes = list(expected_dominators.keys()) - self.assertEqual(expected_dominators, self.graph.immediate_dominators()) + self.assertEqual( + expected_dominators, self.graph.immediate_dominators() + ) def testCrossGraph(self): edges = { @@ -225,7 +240,7 @@ class DominatorTest(unittest.TestCase): 'c': ['a', 'd', 'g'], 'd': ['e'], 'e': ['f'], - 'f': ['a', 'd', 'g'] + 'f': ['a', 'd', 'g'], } expected_dominators = { 'r': None, @@ -235,12 +250,14 @@ class DominatorTest(unittest.TestCase): 'd': 'r', 'e': 'd', 'f': 'e', - 'g': 'r' + 'g': 'r', } self.graph.entry = 'r' self.graph.edges = edges self.graph.nodes = list(expected_dominators.keys()) - self.assertEqual(expected_dominators, self.graph.immediate_dominators()) + self.assertEqual( + expected_dominators, self.graph.immediate_dominators() + ) def testTVerifyGraph(self): edges = { @@ -255,7 +272,7 @@ class DominatorTest(unittest.TestCase): 'n9': ['n10', 'n11', 'n12'], 'n10': ['n11'], 'n11': ['n7'], - 'n12': ['n10'] + 'n12': ['n10'], } expected_dominators = { 'n1': None, @@ -269,12 +286,14 @@ class DominatorTest(unittest.TestCase): 'n9': 'n1', 'n10': 'n1', 'n11': 'n1', - 'n12': 'n1' + 'n12': 'n1', } self.graph.entry = 'n1' self.graph.edges = edges self.graph.nodes = list(expected_dominators.keys()) - self.assertEqual(expected_dominators, self.graph.immediate_dominators()) + self.assertEqual( + expected_dominators, self.graph.immediate_dominators() + ) if __name__ == '__main__': diff --git a/tests/test_decompiler_rpo.py b/tests/test_decompiler_rpo.py index c0338599..0f7db736 100644 --- a/tests/test_decompiler_rpo.py +++ b/tests/test_decompiler_rpo.py @@ -1,8 +1,8 @@ """Tests for rpo.""" import unittest -from androguard.decompiler import graph -from androguard.decompiler import node + +from androguard.decompiler import graph, node class NodeTest(node.Node): @@ -58,7 +58,7 @@ class RpoTest(unittest.TestCase): 'i': ['k'], 'j': ['i'], 'k': ['i', 'r'], - 'l': ['h'] + 'l': ['h'], } n_map = self._createGraphFrom(edges) self.graph.compute_rpo() @@ -76,7 +76,7 @@ class RpoTest(unittest.TestCase): 'i': 12, 'j': 4, 'k': 11, - 'l': 9 + 'l': 9, } self._verifyRpo(n_map, expected_rpo) @@ -102,7 +102,7 @@ class RpoTest(unittest.TestCase): 'z2': ['z1', 'z3'], 'z3': ['z2', 'z4'], 'z4': ['z3', 'z5'], - 'z5': ['z4'] + 'z5': ['z4'], } n_map = self._createGraphFrom(edges) self.graph.compute_rpo() @@ -128,7 +128,7 @@ class RpoTest(unittest.TestCase): 'z2': 13, 'z3': 14, 'z4': 15, - 'z5': 16 + 'z5': 16, } self._verifyRpo(n_map, expected_rpo) @@ -141,7 +141,7 @@ class RpoTest(unittest.TestCase): 'x21': ['x22'], 'x22': ['x21'], 'y1': ['y2', 'x11'], - 'y2': ['x21'] + 'y2': ['x21'], } n_map = self._createGraphFrom(edges) self.graph.compute_rpo() @@ -153,7 +153,7 @@ class RpoTest(unittest.TestCase): 'x21': 6, 'x22': 7, 'y1': 2, - 'y2': 5 + 'y2': 5, } self._verifyRpo(n_map, expected_rpo) @@ -164,7 +164,7 @@ class RpoTest(unittest.TestCase): 'w': ['x1', 'x2'], 'x2': ['x1'], 'y1': ['y2'], - 'y2': ['x2'] + 'y2': ['x2'], } n_map = self._createGraphFrom(edges) self.graph.compute_rpo() @@ -177,7 +177,7 @@ class RpoTest(unittest.TestCase): None: 'r', 'r': ['x1', 'y1', 'y2'], 'x1': ['x2'], - 'x2': ['y1', 'y2'] + 'x2': ['y1', 'y2'], } n_map = self._createGraphFrom(edges) self.graph.compute_rpo() @@ -196,7 +196,7 @@ class RpoTest(unittest.TestCase): 'e': ['c', 'f'], 'f': ['i'], 'g': ['h'], - 'h': ['d', 'f', 'i'] + 'h': ['d', 'f', 'i'], } n_map = self._createGraphFrom(edges) self.graph.compute_rpo() @@ -211,7 +211,7 @@ class RpoTest(unittest.TestCase): 'f': 7, 'g': 4, 'h': 5, - 'i': 10 + 'i': 10, } self._verifyRpo(n_map, expected_rpo) @@ -227,7 +227,7 @@ class RpoTest(unittest.TestCase): 'x4': ['x3', 'x5'], 'x5': ['x4', 'x6'], 'x6': ['x5', 'x7'], - 'x7': ['x6'] + 'x7': ['x6'], } n_map = self._createGraphFrom(edges) self.graph.compute_rpo() @@ -242,7 +242,7 @@ class RpoTest(unittest.TestCase): 'x5': 8, 'x6': 9, 'x7': 10, - 'y': 2 + 'y': 2, } self._verifyRpo(n_map, expected_rpo) @@ -255,7 +255,7 @@ class RpoTest(unittest.TestCase): 'c': ['a', 'd', 'g'], 'd': ['e'], 'e': ['f'], - 'f': ['a', 'd', 'g'] + 'f': ['a', 'd', 'g'], } n_map = self._createGraphFrom(edges) self.graph.compute_rpo() @@ -268,7 +268,7 @@ class RpoTest(unittest.TestCase): 'd': 5, 'e': 6, 'f': 7, - 'g': 8 + 'g': 8, } self._verifyRpo(n_map, expected_rpo) @@ -286,7 +286,7 @@ class RpoTest(unittest.TestCase): 'n9': ['n10', 'n11', 'n12'], 'n10': ['n11'], 'n11': ['n7'], - 'n12': ['n10'] + 'n12': ['n10'], } n_map = self._createGraphFrom(edges) self.graph.compute_rpo() @@ -303,7 +303,7 @@ class RpoTest(unittest.TestCase): 'n9': 5, 'n10': 7, 'n11': 8, - 'n12': 6 + 'n12': 6, } self._verifyRpo(n_map, expected_rpo) diff --git a/tests/test_dex.py b/tests/test_dex.py index 55587af3..82aa2625 100644 --- a/tests/test_dex.py +++ b/tests/test_dex.py @@ -1,15 +1,15 @@ -import unittest - +import binascii import os import random -import binascii +import unittest from androguard.core import dex from androguard.misc import AnalyzeAPK test_dir = os.path.dirname(os.path.abspath(__file__)) -class MockClassManager(): + +class MockClassManager: @property def packer(self): return dex.DalvikPacker(0x12345678) @@ -17,12 +17,13 @@ class MockClassManager(): def get_odex_format(self): return False + class VMClassTest(unittest.TestCase): @classmethod def setUpClass(cls): test_apk_path = os.path.join(test_dir, 'data/APK/TestActivity.apk') cls.a, cls.d, cls.dx = AnalyzeAPK(test_apk_path) - + def testVMClass(self): """test number of ClassDefItems, StringDataItems, FieldIdItems, and MethodIdItems""" @@ -46,10 +47,10 @@ class VMClassTest(unittest.TestCase): # field ids, type ids, and method ids references # are not required to be defined in the dex since they can be resolved at runtime via shared library for vm in self.dx.vms: - num_class_def_items += vm.get_len_classes() # ClassDefItems - num_strings_data_items += vm.get_len_strings() # StringDataItems - num_field_id_items += vm.get_len_fields() # FieldIdItems - num_method_id_items += vm.get_len_methods() # MethodIdItems + num_class_def_items += vm.get_len_classes() # ClassDefItems + num_strings_data_items += vm.get_len_strings() # StringDataItems + num_field_id_items += vm.get_len_fields() # FieldIdItems + num_method_id_items += vm.get_len_methods() # MethodIdItems self.assertEqual(len(self.dx.vms), 1) self.assertEqual(num_class_def_items, 340) @@ -60,68 +61,62 @@ class VMClassTest(unittest.TestCase): def testAccessflags(self): class_name_accessflag_map = { 'Ltests/androguard/TestLoops;': { - 'access_flag': 0x1, # public + 'access_flag': 0x1, # public 'methods': { - '': 0x1 | 0x10000, # public | constructor - 'testBreak': 0x1, # public - 'testBreak2': 0x1, - 'testBreak3': 0x1, - 'testBreak4': 0x1, - 'testBreakDoWhile': 0x1, - 'testBreakMid': 0x1, - 'testBreakbis': 0x1, + '': 0x1 | 0x10000, # public | constructor + 'testBreak': 0x1, # public + 'testBreak2': 0x1, + 'testBreak3': 0x1, + 'testBreak4': 0x1, + 'testBreakDoWhile': 0x1, + 'testBreakMid': 0x1, + 'testBreakbis': 0x1, 'testDiffWhileDoWhile': 0x1, - 'testDoWhile': 0x1, - 'testDoWhileTrue': 0x1, - 'testFor': 0x1, - 'testIrreducible': 0x1, - 'testMultipleLoops': 0x1, - 'testNestedLoops': 0x1, - 'testReducible': 0x1, - 'testWhile': 0x1, - 'testWhile2': 0x1, - 'testWhile3': 0x1, - 'testWhile4': 0x1, - 'testWhile5': 0x1, - 'testWhileTrue': 0x1, + 'testDoWhile': 0x1, + 'testDoWhileTrue': 0x1, + 'testFor': 0x1, + 'testIrreducible': 0x1, + 'testMultipleLoops': 0x1, + 'testNestedLoops': 0x1, + 'testReducible': 0x1, + 'testWhile': 0x1, + 'testWhile2': 0x1, + 'testWhile3': 0x1, + 'testWhile4': 0x1, + 'testWhile5': 0x1, + 'testWhileTrue': 0x1, }, - 'fields': { - } + 'fields': {}, }, 'Ltests/androguard/TestLoops$Loop;': { - 'access_flag': 0x1, # public + 'access_flag': 0x1, # public 'methods': { - '': 0x4 | 0x10000 # protected | constructor + '': 0x4 | 0x10000 # protected | constructor }, 'fields': { - 'i': 0x1 | 0x8, # public | static - 'j': 0x1 | 0x8 # public | static - } + 'i': 0x1 | 0x8, # public | static + 'j': 0x1 | 0x8, # public | static + }, }, - 'Ltests/androguard/TestIfs;': { - 'access_flag': 0x1, # public + 'Ltests/androguard/TestIfs;': { + 'access_flag': 0x1, # public 'methods': { - '': 0x1 | 0x10000, # public | constructor - 'testIF': 0x1 | 0x8, # public | static - 'testIF2': 0x1 | 0x8, - 'testIF3': 0x1 | 0x8, - 'testIF4': 0x1 | 0x8, - 'testIF5': 0x1 | 0x8, - 'testIfBool': 0x1 | 0x8, - 'testShortCircuit': 0x1 | 0x8, - 'testShortCircuit2': 0x1 | 0x8, - 'testShortCircuit3': 0x1 | 0x8, - 'testShortCircuit4': 0x1 | 0x8, - 'testCFG': 0x1, # public - 'testCFG2': 0x1 + '': 0x1 | 0x10000, # public | constructor + 'testIF': 0x1 | 0x8, # public | static + 'testIF2': 0x1 | 0x8, + 'testIF3': 0x1 | 0x8, + 'testIF4': 0x1 | 0x8, + 'testIF5': 0x1 | 0x8, + 'testIfBool': 0x1 | 0x8, + 'testShortCircuit': 0x1 | 0x8, + 'testShortCircuit2': 0x1 | 0x8, + 'testShortCircuit3': 0x1 | 0x8, + 'testShortCircuit4': 0x1 | 0x8, + 'testCFG': 0x1, # public + 'testCFG2': 0x1, }, - 'fields': { - 'P': 0x2, # private - 'Q': 0x2, - 'R': 0x2, - 'S': 0x2 - } - } + 'fields': {'P': 0x2, 'Q': 0x2, 'R': 0x2, 'S': 0x2}, # private + }, } # ensure these classes exist in the Analysis @@ -129,32 +124,52 @@ class VMClassTest(unittest.TestCase): self.assertTrue(self.dx.is_class_present(expected_class_name)) # test access flags for classes - for expected_class_name, class_data in class_name_accessflag_map.items(): + for ( + expected_class_name, + class_data, + ) in class_name_accessflag_map.items(): class_analysis = self.dx.get_class_analysis(expected_class_name) - class_access_flags = class_analysis.get_vm_class().get_access_flags() + class_access_flags = ( + class_analysis.get_vm_class().get_access_flags() + ) self.assertEqual(class_access_flags, class_data['access_flag']) # test access flags for methods encoded_methods = class_analysis.get_vm_class().get_methods() - for expected_method_name, expected_access_flags in class_data['methods'].items(): + for expected_method_name, expected_access_flags in class_data[ + 'methods' + ].items(): # ensure this method is in the class - self.assertIn(expected_method_name, [method.name for method in encoded_methods]) - + self.assertIn( + expected_method_name, + [method.name for method in encoded_methods], + ) + # ensure access flags match for method in encoded_methods: if method.name == expected_method_name: - self.assertEqual(method.get_access_flags(), expected_access_flags) + self.assertEqual( + method.get_access_flags(), expected_access_flags + ) # test access flags for fields encoded_fields = class_analysis.get_vm_class().get_fields() - for expected_field_name, expected_access_flags in class_data['fields'].items(): + for expected_field_name, expected_access_flags in class_data[ + 'fields' + ].items(): # ensure this field is in the class - self.assertIn(expected_field_name, [field.name for field in encoded_fields]) + self.assertIn( + expected_field_name, + [field.name for field in encoded_fields], + ) # ensure access flags match for field in encoded_fields: if field.name == expected_field_name: - self.assertEqual(field.get_access_flags(), expected_access_flags) + self.assertEqual( + field.get_access_flags(), expected_access_flags + ) + class InstructionTest(unittest.TestCase): def testInstructions(self): @@ -188,14 +203,25 @@ class InstructionTest(unittest.TestCase): self.assertEqual(instruction.get_raw(), bytecode) # Test with some pseudorandom stuff - if ins.__name__ in ['Instruction10x', 'Instruction20t', 'Instruction30t', 'Instruction32x', - 'Instruction45cc']: + if ins.__name__ in [ + 'Instruction10x', + 'Instruction20t', + 'Instruction30t', + 'Instruction32x', + 'Instruction45cc', + ]: # note this only works for certain opcode (which are not forced to 0 in certain places) # Thus we need to make sure these places are zero. # Instruction45cc: Has constrained regarding the parameter AA - bytecode = bytearray([op_value, 0] + [random.randint(0x00, 0xff) for _ in range(length - 2)]) + bytecode = bytearray( + [op_value, 0] + + [random.randint(0x00, 0xFF) for _ in range(length - 2)] + ) else: - bytecode = bytearray([op_value] + [random.randint(0x00, 0xff) for _ in range(length - 1)]) + bytecode = bytearray( + [op_value] + + [random.randint(0x00, 0xFF) for _ in range(length - 1)] + ) instruction = ins(MockClassManager(), bytecode) self.assertIsInstance(instruction, dex.Instruction) self.assertEqual(instruction.get_op_value(), op_value) @@ -203,7 +229,9 @@ class InstructionTest(unittest.TestCase): def testNOP(self): """test if NOP instructions are parsed""" - instruction = dex.Instruction10x(MockClassManager(), bytearray(b"\x00\x00")) + instruction = dex.Instruction10x( + MockClassManager(), bytearray(b"\x00\x00") + ) self.assertEqual(instruction.get_name(), "nop") def testLinearSweep(self): @@ -212,7 +240,9 @@ class InstructionTest(unittest.TestCase): instructions = ['nop', 'nop', 'nop', 'return-void'] l = 0 - for ins in dex.LinearSweepAlgorithm.get_instructions(MockClassManager(), 4, bytecode, 0): + for ins in dex.LinearSweepAlgorithm.get_instructions( + MockClassManager(), 4, bytecode, 0 + ): self.assertIsInstance(ins, dex.Instruction10x) self.assertEqual(ins.get_length(), 2) self.assertEqual(ins.get_name(), instructions.pop(0)) @@ -223,32 +253,53 @@ class InstructionTest(unittest.TestCase): def testLinearSweepStrings(self): # very basic function, strings and invokes - bytecode = bytearray(binascii.unhexlify('1A000F001A0100001A0214001A0311001A0415001A0413001A0508001A061200' - '1A0716001A081000620900006E2002000900620000006E200200100062000000' - '6E2002002000620000006E2002003000620000006E2002003000620000006E20' - '02004000620000006E2002005000620000006E2002006000620000006E200200' - '7000620000006E20020080000E00')) + bytecode = bytearray( + binascii.unhexlify( + '1A000F001A0100001A0214001A0311001A0415001A0413001A0508001A061200' + '1A0716001A081000620900006E2002000900620000006E200200100062000000' + '6E2002002000620000006E2002003000620000006E2002003000620000006E20' + '02004000620000006E2002005000620000006E2002006000620000006E200200' + '7000620000006E20020080000E00' + ) + ) instructions = [ - 'const-string', 'const-string', - 'const-string', 'const-string', - 'const-string', 'const-string', - 'const-string', 'const-string', - 'const-string', 'const-string', - 'sget-object', 'invoke-virtual', - 'sget-object', 'invoke-virtual', - 'sget-object', 'invoke-virtual', - 'sget-object', 'invoke-virtual', - 'sget-object', 'invoke-virtual', - 'sget-object', 'invoke-virtual', - 'sget-object', 'invoke-virtual', - 'sget-object', 'invoke-virtual', - 'sget-object', 'invoke-virtual', - 'sget-object', 'invoke-virtual', + 'const-string', + 'const-string', + 'const-string', + 'const-string', + 'const-string', + 'const-string', + 'const-string', + 'const-string', + 'const-string', + 'const-string', + 'sget-object', + 'invoke-virtual', + 'sget-object', + 'invoke-virtual', + 'sget-object', + 'invoke-virtual', + 'sget-object', + 'invoke-virtual', + 'sget-object', + 'invoke-virtual', + 'sget-object', + 'invoke-virtual', + 'sget-object', + 'invoke-virtual', + 'sget-object', + 'invoke-virtual', + 'sget-object', + 'invoke-virtual', + 'sget-object', + 'invoke-virtual', 'return-void', ] l = 0 - for ins in dex.LinearSweepAlgorithm.get_instructions(MockClassManager(), 71, bytecode, 0): + for ins in dex.LinearSweepAlgorithm.get_instructions( + MockClassManager(), 71, bytecode, 0 + ): self.assertIsInstance(ins, dex.Instruction) self.assertEqual(ins.get_name(), instructions.pop(0)) l += ins.get_length() @@ -259,8 +310,12 @@ class InstructionTest(unittest.TestCase): def testLinearSweepSwitch(self): """test if switch payloads are unpacked correctly""" - bytecode = bytearray(binascii.unhexlify('2B02140000001300110038030400130063000F001300170028F913002A0028F6' - '1300480028F3000000010300010000000A0000000D00000010000000')) + bytecode = bytearray( + binascii.unhexlify( + '2B02140000001300110038030400130063000F001300170028F913002A0028F6' + '1300480028F3000000010300010000000A0000000D00000010000000' + ) + ) instructions = [ 'packed-switch', @@ -279,7 +334,9 @@ class InstructionTest(unittest.TestCase): ] l = 0 - for ins in dex.LinearSweepAlgorithm.get_instructions(MockClassManager(), 30, bytecode, 0): + for ins in dex.LinearSweepAlgorithm.get_instructions( + MockClassManager(), 30, bytecode, 0 + ): if len(instructions) > 1: self.assertIsInstance(ins, dex.Instruction) else: @@ -292,21 +349,41 @@ class InstructionTest(unittest.TestCase): def testLSAArrays(self): """Test if fill-array-data-payload is parsed""" - bytecode = bytearray(binascii.unhexlify('12412310030026002D0000005B30000012702300050026002B0000005B300300' - '1250230004002600350000005B300100231007002600380000005B3002001220' - '2300060012011A020D004D02000112111A0211004D0200015B3004000E000000' - '0003010004000000141E28320003040007000000010000000200000003000000' - '0400000005000000E70300000A899D0000030200050000006100620078007A00' - '63000000000302000400000005000A000F001400')) + bytecode = bytearray( + binascii.unhexlify( + '12412310030026002D0000005B30000012702300050026002B0000005B300300' + '1250230004002600350000005B300100231007002600380000005B3002001220' + '2300060012011A020D004D02000112111A0211004D0200015B3004000E000000' + '0003010004000000141E28320003040007000000010000000200000003000000' + '0400000005000000E70300000A899D0000030200050000006100620078007A00' + '63000000000302000400000005000A000F001400' + ) + ) instructions = [ - 'const/4', 'new-array', 'fill-array-data', 'iput-object', - 'const/4', 'new-array', 'fill-array-data', 'iput-object', - 'const/4', 'new-array', 'fill-array-data', 'iput-object', - 'new-array', 'fill-array-data', 'iput-object', - 'const/4', 'new-array', - 'const/4', 'const-string', 'aput-object', - 'const/4', 'const-string', 'aput-object', + 'const/4', + 'new-array', + 'fill-array-data', + 'iput-object', + 'const/4', + 'new-array', + 'fill-array-data', + 'iput-object', + 'const/4', + 'new-array', + 'fill-array-data', + 'iput-object', + 'new-array', + 'fill-array-data', + 'iput-object', + 'const/4', + 'new-array', + 'const/4', + 'const-string', + 'aput-object', + 'const/4', + 'const-string', + 'aput-object', 'iput-object', 'return-void', 'nop', # alignment @@ -321,7 +398,9 @@ class InstructionTest(unittest.TestCase): # array information: (element_width, size) arrays = [(1, 4), (4, 7), (2, 5), (2, 4)] - for ins in dex.LinearSweepAlgorithm.get_instructions(MockClassManager(), 90, bytecode, 0): + for ins in dex.LinearSweepAlgorithm.get_instructions( + MockClassManager(), 90, bytecode, 0 + ): self.assertEqual(ins.get_name(), instructions.pop(0)) if ins.get_name() != 'fill-array-data-payload': self.assertIsInstance(ins, dex.Instruction) @@ -340,75 +419,140 @@ class InstructionTest(unittest.TestCase): def testWrongInstructions(self): """Test if unknown instructions raise an InvalidInstruction error""" with self.assertRaises(dex.InvalidInstruction): - ins = list(dex.LinearSweepAlgorithm.get_instructions(MockClassManager(), 1, bytearray(b"\xff\xab"), 0)) + ins = list( + dex.LinearSweepAlgorithm.get_instructions( + MockClassManager(), 1, bytearray(b"\xff\xab"), 0 + ) + ) with self.assertRaises(dex.InvalidInstruction): - ins = list(dex.LinearSweepAlgorithm.get_instructions(MockClassManager(), 2, bytearray(b"\x00\x00" - b"\xff\xab"), 0)) + ins = list( + dex.LinearSweepAlgorithm.get_instructions( + MockClassManager(), + 2, + bytearray(b"\x00\x00" b"\xff\xab"), + 0, + ) + ) def testIncompleteInstruction(self): """Test if incomplete bytecode log an error""" # Test if instruction can be parsed - self.assertIsInstance(dex.Instruction51l(MockClassManager(), - bytearray(b'\x18\x01\x23\x23\x00\xff\x99\x11\x22\x22')), - dex.Instruction51l) + self.assertIsInstance( + dex.Instruction51l( + MockClassManager(), + bytearray(b'\x18\x01\x23\x23\x00\xff\x99\x11\x22\x22'), + ), + dex.Instruction51l, + ) with self.assertRaises(dex.InvalidInstruction): ins = list( - dex.LinearSweepAlgorithm.get_instructions(MockClassManager(), 5, bytearray(b"\x18\x01\xff\xff"), 0)) + dex.LinearSweepAlgorithm.get_instructions( + MockClassManager(), 5, bytearray(b"\x18\x01\xff\xff"), 0 + ) + ) def testInstruction21h(self): """Test function of Instruction 21h used for const{,-wide}/high16""" - ins = dex.Instruction21h(MockClassManager(), bytearray([0x15, 0x00, 0x42, 0x11])) + ins = dex.Instruction21h( + MockClassManager(), bytearray([0x15, 0x00, 0x42, 0x11]) + ) self.assertEqual(ins.get_op_value(), 0x15) self.assertEqual(ins.get_literals(), [0x11420000]) - self.assertEqual(ins.get_operands(), [(dex.Operand.REGISTER, 0x00), (dex.Operand.LITERAL, 0x11420000)]) + self.assertEqual( + ins.get_operands(), + [(dex.Operand.REGISTER, 0x00), (dex.Operand.LITERAL, 0x11420000)], + ) self.assertEqual(ins.get_name(), 'const/high16') self.assertEqual(ins.get_output(), 'v0, 289538048') self.assertEqual(ins.get_raw(), bytearray([0x15, 0x00, 0x42, 0x11])) - ins = dex.Instruction21h(MockClassManager(), bytearray([0x19, 0x00, 0x42, 0x11])) + ins = dex.Instruction21h( + MockClassManager(), bytearray([0x19, 0x00, 0x42, 0x11]) + ) self.assertEqual(ins.get_op_value(), 0x19) self.assertEqual(ins.get_literals(), [0x1142000000000000]) - self.assertEqual(ins.get_operands(), [(dex.Operand.REGISTER, 0x00), (dex.Operand.LITERAL, 0x1142000000000000)]) + self.assertEqual( + ins.get_operands(), + [ + (dex.Operand.REGISTER, 0x00), + (dex.Operand.LITERAL, 0x1142000000000000), + ], + ) self.assertEqual(ins.get_name(), 'const-wide/high16') self.assertEqual(ins.get_output(), 'v0, 1243556447107678208') self.assertEqual(ins.get_raw(), bytearray([0x19, 0x00, 0x42, 0x11])) - ins = dex.Instruction21h(MockClassManager(), bytearray([0x19, 0x00, 0xbe, 0xff])) + ins = dex.Instruction21h( + MockClassManager(), bytearray([0x19, 0x00, 0xBE, 0xFF]) + ) self.assertEqual(ins.get_op_value(), 0x19) self.assertEqual(ins.get_literals(), [-18577348462903296]) - self.assertEqual(ins.get_operands(), [(dex.Operand.REGISTER, 0x00), (dex.Operand.LITERAL, -18577348462903296)]) + self.assertEqual( + ins.get_operands(), + [ + (dex.Operand.REGISTER, 0x00), + (dex.Operand.LITERAL, -18577348462903296), + ], + ) self.assertEqual(ins.get_name(), 'const-wide/high16') self.assertEqual(ins.get_output(), 'v0, -18577348462903296') - self.assertEqual(ins.get_raw(), bytearray([0x19, 0x00, 0xbe, 0xff])) + self.assertEqual(ins.get_raw(), bytearray([0x19, 0x00, 0xBE, 0xFF])) def testInstruction51l(self): """test the functionality of const-wide""" - ins = dex.Instruction51l(MockClassManager(), - bytearray([0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + ins = dex.Instruction51l( + MockClassManager(), + bytearray( + [0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + ), + ) self.assertEqual(ins.get_op_value(), 0x18) self.assertEqual(ins.get_literals(), [0]) - self.assertEqual(ins.get_operands(), [(dex.Operand.REGISTER, 0x00), (dex.Operand.LITERAL, 0)]) + self.assertEqual( + ins.get_operands(), + [(dex.Operand.REGISTER, 0x00), (dex.Operand.LITERAL, 0)], + ) self.assertEqual(ins.get_name(), 'const-wide') self.assertEqual(ins.get_output(), 'v0, 0') - self.assertEqual(ins.get_raw(), bytearray([0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + self.assertEqual( + ins.get_raw(), + bytearray( + [0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + ), + ) - bytecode = bytearray([0x18, 0x00, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x70]) + bytecode = bytearray( + [0x18, 0x00, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x70] + ) ins = dex.Instruction51l(MockClassManager(), bytecode) self.assertEqual(ins.get_op_value(), 0x18) self.assertEqual(ins.get_literals(), [0x7034129078563412]) - self.assertEqual(ins.get_operands(), [(dex.Operand.REGISTER, 0x00), (dex.Operand.LITERAL, 0x7034129078563412)]) + self.assertEqual( + ins.get_operands(), + [ + (dex.Operand.REGISTER, 0x00), + (dex.Operand.LITERAL, 0x7034129078563412), + ], + ) self.assertEqual(ins.get_name(), 'const-wide') self.assertEqual(ins.get_output(), 'v0, 8085107642740388882') self.assertEqual(ins.get_raw(), bytecode) - bytecode = bytearray([0x18, 0x00, 0xee, 0xcb, 0xa9, 0x87, 0x6f, 0xed, 0xcb, 0x8f]) + bytecode = bytearray( + [0x18, 0x00, 0xEE, 0xCB, 0xA9, 0x87, 0x6F, 0xED, 0xCB, 0x8F] + ) ins = dex.Instruction51l(MockClassManager(), bytecode) self.assertEqual(ins.get_op_value(), 0x18) self.assertEqual(ins.get_literals(), [-8085107642740388882]) - self.assertEqual(ins.get_operands(), - [(dex.Operand.REGISTER, 0x00), (dex.Operand.LITERAL, -8085107642740388882)]) + self.assertEqual( + ins.get_operands(), + [ + (dex.Operand.REGISTER, 0x00), + (dex.Operand.LITERAL, -8085107642740388882), + ], + ) self.assertEqual(ins.get_name(), 'const-wide') self.assertEqual(ins.get_output(), 'v0, -8085107642740388882') self.assertEqual(ins.get_raw(), bytecode) @@ -427,11 +571,16 @@ class InstructionTest(unittest.TestCase): (0x86, 6, -8), ] for args, reg, lit in tests: - ins = dex.Instruction11n(MockClassManager(), bytearray([0x12, args])) + ins = dex.Instruction11n( + MockClassManager(), bytearray([0x12, args]) + ) self.assertEqual(ins.get_name(), 'const/4') self.assertEqual(ins.get_literals(), [lit]) self.assertEqual(ins.get_raw(), bytearray([0x12, args])) - self.assertEqual(ins.get_operands(), [(dex.Operand.REGISTER, reg), (dex.Operand.LITERAL, lit)]) + self.assertEqual( + ins.get_operands(), + [(dex.Operand.REGISTER, reg), (dex.Operand.LITERAL, lit)], + ) self.assertEqual(ins.get_output(), 'v{}, {}'.format(reg, lit)) def testInstruction21s(self): @@ -457,7 +606,10 @@ class InstructionTest(unittest.TestCase): self.assertEqual(ins.get_name(), 'const/16') self.assertEqual(ins.get_literals(), [lit]) self.assertEqual(ins.get_raw(), bytecode) - self.assertEqual(ins.get_operands(), [(dex.Operand.REGISTER, reg), (dex.Operand.LITERAL, lit)]) + self.assertEqual( + ins.get_operands(), + [(dex.Operand.REGISTER, reg), (dex.Operand.LITERAL, lit)], + ) self.assertEqual(ins.get_output(), 'v{}, {}'.format(reg, lit)) # const-wide/16 @@ -466,7 +618,10 @@ class InstructionTest(unittest.TestCase): self.assertEqual(ins.get_name(), 'const-wide/16') self.assertEqual(ins.get_literals(), [lit]) self.assertEqual(ins.get_raw(), bytecode) - self.assertEqual(ins.get_operands(), [(dex.Operand.REGISTER, reg), (dex.Operand.LITERAL, lit)]) + self.assertEqual( + ins.get_operands(), + [(dex.Operand.REGISTER, reg), (dex.Operand.LITERAL, lit)], + ) self.assertEqual(ins.get_output(), 'v{}, {}'.format(reg, lit)) def testInstruction31i(self): @@ -486,7 +641,10 @@ class InstructionTest(unittest.TestCase): self.assertEqual(ins.get_name(), 'const') self.assertEqual(ins.get_literals(), [lit]) self.assertEqual(ins.get_raw(), bytecode) - self.assertEqual(ins.get_operands(), [(dex.Operand.REGISTER, reg), (dex.Operand.LITERAL, lit)]) + self.assertEqual( + ins.get_operands(), + [(dex.Operand.REGISTER, reg), (dex.Operand.LITERAL, lit)], + ) self.assertEqual(ins.get_output(), 'v{}, {}'.format(reg, lit)) # const-wide/32 @@ -495,7 +653,10 @@ class InstructionTest(unittest.TestCase): self.assertEqual(ins.get_name(), 'const-wide/32') self.assertEqual(ins.get_literals(), [lit]) self.assertEqual(ins.get_raw(), bytecode) - self.assertEqual(ins.get_operands(), [(dex.Operand.REGISTER, reg), (dex.Operand.LITERAL, lit)]) + self.assertEqual( + ins.get_operands(), + [(dex.Operand.REGISTER, reg), (dex.Operand.LITERAL, lit)], + ) self.assertEqual(ins.get_output(), 'v{}, {}'.format(reg, lit)) def testBrokenDex(self): @@ -506,10 +667,12 @@ class InstructionTest(unittest.TestCase): self.assertIn('Header too small', str(cnx.exception)) # Adler32 will not match, zeroed out file - data_dex = binascii.unhexlify('6465780A303335001F6C4D5A6ACF889AF588F3237FC9F20B41F56A2408749D1B' - 'C81E000070000000785634120000000000000000341E00009400000070000000' - '2E000000C0020000310000007803000011000000C4050000590000004C060000' - '090000001409000094140000340A0000' + ('00' * (7880 - 0x70))) + data_dex = binascii.unhexlify( + '6465780A303335001F6C4D5A6ACF889AF588F3237FC9F20B41F56A2408749D1B' + 'C81E000070000000785634120000000000000000341E00009400000070000000' + '2E000000C0020000310000007803000011000000C4050000590000004C060000' + '090000001409000094140000340A0000' + ('00' * (7880 - 0x70)) + ) with self.assertRaises(ValueError) as cnx: dex.DEX(data_dex) @@ -517,42 +680,52 @@ class InstructionTest(unittest.TestCase): # A very very basic dex file (without a map) # But should parse... - data_dex = binascii.unhexlify('6465780A30333500460A4882696E76616C6964696E76616C6964696E76616C69' - '7000000070000000785634120000000000000000000000000000000000000000' - '0000000000000000000000000000000000000000000000000000000000000000' - '00000000000000000000000000000000') + data_dex = binascii.unhexlify( + '6465780A30333500460A4882696E76616C6964696E76616C6964696E76616C69' + '7000000070000000785634120000000000000000000000000000000000000000' + '0000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000' + ) self.assertIsNotNone(dex.DEX(data_dex)) # Wrong header size - data_dex = binascii.unhexlify('6465780A30333500480A2C8D696E76616C6964696E76616C6964696E76616C69' - '7100000071000000785634120000000000000000000000000000000000000000' - '0000000000000000000000000000000000000000000000000000000000000000' - '0000000000000000000000000000000000') + data_dex = binascii.unhexlify( + '6465780A30333500480A2C8D696E76616C6964696E76616C6964696E76616C69' + '7100000071000000785634120000000000000000000000000000000000000000' + '0000000000000000000000000000000000000000000000000000000000000000' + '0000000000000000000000000000000000' + ) with self.assertRaises(ValueError) as cnx: dex.DEX(data_dex) self.assertIn("Wrong header size", str(cnx.exception)) # Non integer version, but parse it - data_dex = binascii.unhexlify('6465780AFF00AB00460A4882696E76616C6964696E76616C6964696E76616C69' - '7000000070000000785634120000000000000000000000000000000000000000' - '0000000000000000000000000000000000000000000000000000000000000000' - '00000000000000000000000000000000') + data_dex = binascii.unhexlify( + '6465780AFF00AB00460A4882696E76616C6964696E76616C6964696E76616C69' + '7000000070000000785634120000000000000000000000000000000000000000' + '0000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000' + ) self.assertIsNotNone(dex.DEX(data_dex)) # Big Endian file - data_dex = binascii.unhexlify('6465780A30333500460AF480696E76616C6964696E76616C6964696E76616C69' - '7000000070000000123456780000000000000000000000000000000000000000' - '0000000000000000000000000000000000000000000000000000000000000000' - '00000000000000000000000000000000') + data_dex = binascii.unhexlify( + '6465780A30333500460AF480696E76616C6964696E76616C6964696E76616C69' + '7000000070000000123456780000000000000000000000000000000000000000' + '0000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000' + ) with self.assertRaises(NotImplementedError) as cnx: dex.DEX(data_dex) self.assertIn("swapped endian tag", str(cnx.exception)) # Weird endian file - data_dex = binascii.unhexlify('6465780A30333500AB0BC3E4696E76616C6964696E76616C6964696E76616C69' - '7000000070000000ABCDEF120000000000000000000000000000000000000000' - '0000000000000000000000000000000000000000000000000000000000000000' - '00000000000000000000000000000000') + data_dex = binascii.unhexlify( + '6465780A30333500AB0BC3E4696E76616C6964696E76616C6964696E76616C69' + '7000000070000000ABCDEF120000000000000000000000000000000000000000' + '0000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000' + ) with self.assertRaises(ValueError) as cnx: dex.DEX(data_dex) self.assertIn("Wrong endian tag", str(cnx.exception)) diff --git a/tests/test_dexcodeparsing.py b/tests/test_dexcodeparsing.py index d68ec896..04c418a8 100644 --- a/tests/test_dexcodeparsing.py +++ b/tests/test_dexcodeparsing.py @@ -1,11 +1,11 @@ import os +import unittest +from binascii import hexlify +from difflib import Differ -from androguard.core import dex import parse_dex -from binascii import hexlify -import unittest -from difflib import Differ +from androguard.core import dex test_dir = os.path.dirname(os.path.abspath(__file__)) @@ -32,25 +32,25 @@ class TestDexCodeParsing(unittest.TestCase): continue code = hexlify(m.get_code().get_raw()) - self.assertEqual(parsed.methods[m.get_method_idx()], - code, - "incorrect code for " - "[{}]: {} --> {}:\n" - "{}\ntries_size: {}, insns_size: {}\nSHOULD BE {}\n{}\n{}".format(m.get_method_idx(), - m.get_class_name(), - m.get_name(), - "".join(dif.compare( - parsed.methods[ - m.get_method_idx()], - code)), - m.get_code().tries_size, - m.get_code().insns_size, - hexlify( - m.get_code().get_raw()), - parsed.methods[ - m.get_method_idx()], - hexlify( - m.get_code().code.get_raw()))) + self.assertEqual( + parsed.methods[m.get_method_idx()], + code, + "incorrect code for " + "[{}]: {} --> {}:\n" + "{}\ntries_size: {}, insns_size: {}\nSHOULD BE {}\n{}\n{}".format( + m.get_method_idx(), + m.get_class_name(), + m.get_name(), + "".join( + dif.compare(parsed.methods[m.get_method_idx()], code) + ), + m.get_code().tries_size, + m.get_code().insns_size, + hexlify(m.get_code().get_raw()), + parsed.methods[m.get_method_idx()], + hexlify(m.get_code().code.get_raw()), + ), + ) def testClassManager(self): """Test if the classmanager has the same items""" @@ -74,13 +74,17 @@ class TestDexCodeParsing(unittest.TestCase): for idx in range(parsed.string_ids_size): self.assertNotEqual(cm.get_string(idx), ERR_STR) self.assertNotEqual(cm.get_raw_string(idx), ERR_STR) - self.assertEqual(cm.get_raw_string(idx), decode(parsed.str_raw[idx])) + self.assertEqual( + cm.get_raw_string(idx), decode(parsed.str_raw[idx]) + ) self.assertEqual(cm.get_string(parsed.string_ids_size), ERR_STR) self.assertEqual(cm.get_raw_string(parsed.string_ids_size), ERR_STR) self.assertEqual(cm.get_string(parsed.string_ids_size + 100), ERR_STR) - self.assertEqual(cm.get_raw_string(parsed.string_ids_size + 100), ERR_STR) + self.assertEqual( + cm.get_raw_string(parsed.string_ids_size + 100), ERR_STR + ) if __name__ == '__main__': diff --git a/tests/test_loadorder.py b/tests/test_loadorder.py index a7ecdd3d..a4a5b4e0 100644 --- a/tests/test_loadorder.py +++ b/tests/test_loadorder.py @@ -1,5 +1,3 @@ - - import unittest from androguard.core.dex import TypeMapItem diff --git a/tests/test_misc.py b/tests/test_misc.py index 32ed6ee4..ac52eab7 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -1,36 +1,78 @@ # -*- coding: utf8- -*- -import unittest import os +import tempfile +import unittest from androguard.misc import clean_file_name -import tempfile + class MiscTest(unittest.TestCase): def testCleanPath(self): - self.assertEqual("foobarfoo_", clean_file_name("foobarfoo ", unique=False)) - self.assertEqual("foobarsdf_", clean_file_name("foobarsdf.", unique=False)) + self.assertEqual( + "foobarfoo_", clean_file_name("foobarfoo ", unique=False) + ) + self.assertEqual( + "foobarsdf_", clean_file_name("foobarsdf.", unique=False) + ) self.assertEqual("_init_", clean_file_name("", unique=False)) if os.name == "nt": - self.assertEqual("C:\\" + "a" * 230, clean_file_name("C:\\" + "a" * 999, unique=False)) - self.assertEqual("C:\\" + "a" * 226 + ".foo", clean_file_name("C:\\" + "a" * 999 + ".foo", unique=False)) + self.assertEqual( + "C:\\" + "a" * 230, + clean_file_name("C:\\" + "a" * 999, unique=False), + ) + self.assertEqual( + "C:\\" + "a" * 226 + ".foo", + clean_file_name("C:\\" + "a" * 999 + ".foo", unique=False), + ) else: - self.assertEqual("/some/path/" + "a" * 230, clean_file_name("/some/path/" + "a" * 999, unique=False)) - self.assertEqual("/some/path/" + "a" * 226 + ".foo", clean_file_name("/some/path/" + "a" * 999 + ".foo", unique=False)) + self.assertEqual( + "/some/path/" + "a" * 230, + clean_file_name("/some/path/" + "a" * 999, unique=False), + ) + self.assertEqual( + "/some/path/" + "a" * 226 + ".foo", + clean_file_name( + "/some/path/" + "a" * 999 + ".foo", unique=False + ), + ) with tempfile.NamedTemporaryFile() as fp: - self.assertEqual(fp.name + "_0", clean_file_name(fp.name, unique=True)) + self.assertEqual( + fp.name + "_0", clean_file_name(fp.name, unique=True) + ) def testClassNameFormatting(self): from androguard.core.bytecode import get_package_class_name - self.assertEqual(get_package_class_name('Ljava/lang/Object;'), ('java.lang', 'Object')) - self.assertEqual(get_package_class_name('[Ljava/lang/Object;'), ('java.lang', 'Object')) - self.assertEqual(get_package_class_name('[[Ljava/lang/Object;'), ('java.lang', 'Object')) - self.assertEqual(get_package_class_name('[[[[[[[[[[[[[[[[[[[[[[[Ljava/lang/Object;'), ('java.lang', 'Object')) - self.assertEqual(get_package_class_name('[[[[[[[[[[[[[[[[[[[[[[[LObject;'), ('', 'Object')) + self.assertEqual( + get_package_class_name('Ljava/lang/Object;'), + ('java.lang', 'Object'), + ) + self.assertEqual( + get_package_class_name('[Ljava/lang/Object;'), + ('java.lang', 'Object'), + ) + self.assertEqual( + get_package_class_name('[[Ljava/lang/Object;'), + ('java.lang', 'Object'), + ) + self.assertEqual( + get_package_class_name( + '[[[[[[[[[[[[[[[[[[[[[[[Ljava/lang/Object;' + ), + ('java.lang', 'Object'), + ) + self.assertEqual( + get_package_class_name('[[[[[[[[[[[[[[[[[[[[[[[LObject;'), + ('', 'Object'), + ) self.assertEqual(get_package_class_name('LFoobar;'), ('', 'Foobar')) - self.assertEqual(get_package_class_name('Lsdflkjdsklfjsdkjfklsdjfkljsdkflsd/shdfjksdhkjfhsdkjfsh;'), - ('sdflkjdsklfjsdkjfklsdjfkljsdkflsd', 'shdfjksdhkjfhsdkjfsh')) + self.assertEqual( + get_package_class_name( + 'Lsdflkjdsklfjsdkjfklsdjfkljsdkflsd/shdfjksdhkjfhsdkjfsh;' + ), + ('sdflkjdsklfjsdkjfklsdjfkljsdkflsd', 'shdfjksdhkjfhsdkjfsh'), + ) self.assertEqual(get_package_class_name('L;'), ('', '')) with self.assertRaises(ValueError): diff --git a/tests/test_rename.py b/tests/test_rename.py index bc97b5cf..989d69d9 100644 --- a/tests/test_rename.py +++ b/tests/test_rename.py @@ -1,6 +1,5 @@ -import unittest - import os +import unittest from androguard.core import dex from androguard.core.analysis import analysis @@ -11,14 +10,13 @@ test_dir = os.path.dirname(os.path.abspath(__file__)) class RenameTest(unittest.TestCase): def __init__(self, *args, **kwargs): super(RenameTest, self).__init__(*args, **kwargs) - with open(os.path.join(test_dir, 'data/APK/classes.dex'), - "rb") as fd: + with open(os.path.join(test_dir, 'data/APK/classes.dex'), "rb") as fd: self.d = dex.DEX(fd.read()) self.dx = analysis.Analysis(self.d) # self.d.set_vmanalysis(self.dx) def testMethodRename(self): - meth, = self.d.get_encoded_method("testDouble") + (meth,) = self.d.get_encoded_method("testDouble") clas = self.d.get_class(meth.get_class_name()) self.assertEqual(meth.get_name(), "testDouble") self.assertIn(meth.get_name(), [i.name for i in clas.get_methods()]) @@ -28,10 +26,12 @@ class RenameTest(unittest.TestCase): self.assertNotIn("testDouble", [i.name for i in clas.get_methods()]) def testFieldRename(self): - field, = self.d.get_encoded_field("FLAG_REGISTER_CONTENT_OBSERVER") + (field,) = self.d.get_encoded_field("FLAG_REGISTER_CONTENT_OBSERVER") self.assertEqual(field.get_name(), "FLAG_REGISTER_CONTENT_OBSERVER") field.set_name("FLAG_REGISTER_CONTENT_OBSERVER_RENAMED") - self.assertEqual(field.get_name(), "FLAG_REGISTER_CONTENT_OBSERVER_RENAMED") + self.assertEqual( + field.get_name(), "FLAG_REGISTER_CONTENT_OBSERVER_RENAMED" + ) def testClassRename(self): clazz = self.d.get_class("LTestDefaultPackage;") diff --git a/tests/test_strings.py b/tests/test_strings.py index f46abe26..095a27d0 100644 --- a/tests/test_strings.py +++ b/tests/test_strings.py @@ -1,29 +1,31 @@ # -*- coding: utf8- -*- +import os import unittest -import os - -from androguard.core import mutf8 -from androguard.core import dex +from androguard.core import dex, mutf8 test_dir = os.path.dirname(os.path.abspath(__file__)) class StringTest(unittest.TestCase): def testDex(self): - with open(os.path.join(test_dir, 'data/APK/StringTests.dex'), "rb") as fd: + with open( + os.path.join(test_dir, 'data/APK/StringTests.dex'), "rb" + ) as fd: d = dex.DEX(fd.read()) - stests = ["this is a quite normal string", - "\u0000 \u0001 \u1234", - "使用在線工具將字符串翻譯為中文", - "перевод строки на русский с помощью онлайн-инструментов", - "온라인 도구를 사용하여 문자열을 한국어로 번역", - "オンラインツールを使用して文字列を日本語に翻訳", - "This is \U0001f64f, an emoji.", # complete surrogate - "\u2713 check this string", - "\uffff \u0000 \uff00", - "\u0420\u043e\u0441\u0441\u0438\u044f"] + stests = [ + "this is a quite normal string", + "\u0000 \u0001 \u1234", + "使用在線工具將字符串翻譯為中文", + "перевод строки на русский с помощью онлайн-инструментов", + "온라인 도구를 사용하여 문자열을 한국어로 번역", + "オンラインツールを使用して文字列を日本語に翻訳", + "This is \U0001f64f, an emoji.", # complete surrogate + "\u2713 check this string", + "\uffff \u0000 \uff00", + "\u0420\u043e\u0441\u0441\u0438\u044f", + ] for s in stests: self.assertIn(s, d.get_strings()) @@ -35,41 +37,64 @@ class StringTest(unittest.TestCase): self.assertEqual("\uacf0", mutf8.decode(b"\xea\xb3\xb0")) # # Surrogates self.assertEqual("🙏", mutf8.decode(b"\xed\xa0\xbd\xed\xb9\x8f")) - self.assertEqual("\U00014f5c", mutf8.decode(b"\xed\xa1\x93\xed\xbd\x9c")) + self.assertEqual( + "\U00014f5c", mutf8.decode(b"\xed\xa1\x93\xed\xbd\x9c") + ) # # Lonely surrogates # self.assertEqual("\ud853", mutf8.decode(b"\xed\xa1\x93")) # self.assertEqual("\udf5c", mutf8.decode(b"\xed\xbd\x9c")) # # Normal ASCII String - self.assertEqual("hello world", mutf8.decode(b"\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64")) + self.assertEqual( + "hello world", + mutf8.decode(b"\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64"), + ) # Testing decode - b = b"\xed\xa1\x93\xed\xbd\x9c" + \ - b"\xed\xa0\xbd\xed\xb9\x8f" + \ - b"\xed\xa0\xbd" + \ - b"\xea\xb3\xb0" + \ - b"\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64" + \ - b"\xc0\x80" + b = ( + b"\xed\xa1\x93\xed\xbd\x9c" + + b"\xed\xa0\xbd\xed\xb9\x8f" + + b"\xed\xa0\xbd" + + b"\xea\xb3\xb0" + + b"\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64" + + b"\xc0\x80" + ) - self.assertEqual("hello world", mutf8.decode(b"\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64").encode('utf8', - errors='backslashreplace').decode( - 'utf8')) - self.assertEqual("\U00014f5c", - mutf8.decode(b"\xed\xa1\x93\xed\xbd\x9c").encode('utf8', errors='backslashreplace').decode( - 'utf8')) - self.assertEqual("\U0001f64f", - mutf8.decode(b"\xed\xa0\xbd\xed\xb9\x8f").encode('utf8', errors='backslashreplace').decode( - 'utf8')) + self.assertEqual( + "hello world", + mutf8.decode(b"\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64") + .encode('utf8', errors='backslashreplace') + .decode('utf8'), + ) + self.assertEqual( + "\U00014f5c", + mutf8.decode(b"\xed\xa1\x93\xed\xbd\x9c") + .encode('utf8', errors='backslashreplace') + .decode('utf8'), + ) + self.assertEqual( + "\U0001f64f", + mutf8.decode(b"\xed\xa0\xbd\xed\xb9\x8f") + .encode('utf8', errors='backslashreplace') + .decode('utf8'), + ) # self.assertEqual("\\ud853", - # mutf8.decode(b"\xed\xa1\x93").encode('utf8', errors='backslashreplace').decode('utf8')) + # mutf8.decode(b"\xed\xa1\x93").encode('utf8', errors='backslashreplace').decode('utf8')) # self.assertEqual("\U00024f5c\U0001f64f\\ud83d\uacf0hello world\x00", # mutf8.decode(b).encode('utf8', errors='backslashreplace').decode('utf8')) # Testing encode - self.assertEqual(b"\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64", mutf8.encode("hello world")) - self.assertEqual(b"\xed\xa2\x93\xed\xbd\x9c", mutf8.encode("\U00024f5c")) - self.assertEqual(b"\xed\xa1\xbd\xed\xb9\x8f", mutf8.encode("\U0001f64f")) + self.assertEqual( + b"\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64", + mutf8.encode("hello world"), + ) + self.assertEqual( + b"\xed\xa2\x93\xed\xbd\x9c", mutf8.encode("\U00024f5c") + ) + self.assertEqual( + b"\xed\xa1\xbd\xed\xb9\x8f", mutf8.encode("\U0001f64f") + ) # self.assertEqual(b"\xed\xa1\x93", mutf8.encode("\ud853")) # self.assertEqual(b, mutf8.encode("\U00024f5c\U0001f64f\ud83d\uacf0hello world\x00")) diff --git a/tests/test_types.py b/tests/test_types.py index 51cd6bc6..42f865af 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -2,11 +2,10 @@ import os import unittest -from struct import pack, unpack, calcsize +from struct import calcsize, pack, unpack from androguard.session import Session - test_dir = os.path.dirname(os.path.abspath(__file__)) TEST_CASE = os.path.join(test_dir, 'data/APK/classes.dex') @@ -23,7 +22,6 @@ VALUES = { ('