Fix syntax + dynamic part

This commit is contained in:
Ping2A 2024-10-31 15:50:52 +01:00
parent 3931a45ca4
commit e73ee65fc9
64 changed files with 9586 additions and 4836 deletions

View File

@ -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 "<default>", entry))
print(
"{} = '{}'".format(
(
config.get_qualifier()
if not config.is_default()
else "<default>"
),
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)

View File

@ -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,15 +54,22 @@ 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"
@ -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:
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
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

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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 {}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -35,17 +35,16 @@ TYPE_INT_HEX = 0x11
# The 'data' is either 0 or 1, for input "false" or "true" respectively.
TYPE_INT_BOOLEAN = 0x12
# Beginning of color integer flavors...
TYPE_FIRST_COLOR_INT = 0x1c
TYPE_FIRST_COLOR_INT = 0x1C
# The 'data' is a raw integer value of the form #aarrggbb.
TYPE_INT_COLOR_ARGB8 = 0x1c
TYPE_INT_COLOR_ARGB8 = 0x1C
# The 'data' is a raw integer value of the form #rrggbb.
TYPE_INT_COLOR_RGB8 = 0x1d
TYPE_INT_COLOR_RGB8 = 0x1D
# The 'data' is a raw integer value of the form #argb.
TYPE_INT_COLOR_ARGB4 = 0x1e
TYPE_INT_COLOR_ARGB4 = 0x1E
# The 'data' is a raw integer value of the form #rgb.
TYPE_INT_COLOR_RGB4 = 0x1f
TYPE_INT_COLOR_RGB4 = 0x1F
# ...end of integer flavors.
TYPE_LAST_COLOR_INT = 0x1f
TYPE_LAST_COLOR_INT = 0x1F
# ...end of integer flavors.
TYPE_LAST_INT = 0x1f
TYPE_LAST_INT = 0x1F

View File

@ -5,20 +5,22 @@ from __future__ import annotations
import hashlib
import json
from struct import pack
import textwrap
from struct import pack
from typing import TYPE_CHECKING, Union
from androguard.core.androconf import CONF, color_range
from androguard.core.dex.dex_types import Kind, Operand
if TYPE_CHECKING:
from androguard.core.analysis import DEXBasicBlock, MethodAnalysis
from androguard.core.dex import DEX
from loguru import logger
from xml.sax.saxutils import escape
from loguru import logger
def _PrintBanner():
print_fct = CONF["PRINT_FCT"]
print_fct("*" * 75 + "\n")
@ -36,7 +38,9 @@ def _PrintNote(note, tab=0):
print_fct = CONF["PRINT_FCT"]
note_color = CONF["COLORS"]["NOTE"]
normal_color = CONF["COLORS"]["NORMAL"]
print_fct("\t" * tab + "{}# {}{}".format(note_color, note, normal_color) + "\n")
print_fct(
"\t" * tab + "{}# {}{}".format(note_color, note, normal_color) + "\n"
)
def _Print(name, arg):
@ -57,26 +61,44 @@ def PrettyShowEx(exceptions):
if len(exceptions) > 0:
CONF["PRINT_FCT"]("Exceptions:\n")
for i in exceptions:
CONF["PRINT_FCT"]("\t%s%s%s\n" %
(CONF["COLORS"]["EXCEPTION"], i.show_buff(),
CONF["COLORS"]["NORMAL"]))
CONF["PRINT_FCT"](
"\t%s%s%s\n"
% (
CONF["COLORS"]["EXCEPTION"],
i.show_buff(),
CONF["COLORS"]["NORMAL"],
)
)
def _PrintXRef(tag, items):
print_fct = CONF["PRINT_FCT"]
for i in items:
print_fct("%s: %s %s %s %s\n" %
(tag, i[0].get_class_name(), i[0].get_name(),
i[0].get_descriptor(), ' '.join("%x" % j.get_idx()
for j in i[1])))
print_fct(
"%s: %s %s %s %s\n"
% (
tag,
i[0].get_class_name(),
i[0].get_name(),
i[0].get_descriptor(),
' '.join("%x" % j.get_idx() for j in i[1]),
)
)
def _PrintDRef(tag, items):
print_fct = CONF["PRINT_FCT"]
for i in items:
print_fct("%s: %s %s %s %s\n" %
(tag, i[0].get_class_name(), i[0].get_name(),
i[0].get_descriptor(), ' '.join("%x" % j for j in i[1])))
print_fct(
"%s: %s %s %s %s\n"
% (
tag,
i[0].get_class_name(),
i[0].get_name(),
i[0].get_descriptor(),
' '.join("%x" % j for j in i[1]),
)
)
def _PrintDefault(msg):
@ -90,10 +112,14 @@ def _colorize_operands(operands, colors):
"""
for operand in operands:
if operand[0] == Operand.REGISTER:
yield "{}v{}{}".format(colors["registers"], operand[1], colors["normal"])
yield "{}v{}{}".format(
colors["registers"], operand[1], colors["normal"]
)
elif operand[0] == Operand.LITERAL:
yield "{}{}{}".format(colors["literal"], operand[1], colors["normal"])
yield "{}{}{}".format(
colors["literal"], operand[1], colors["normal"]
)
elif operand[0] == Operand.RAW:
yield "{}{}{}".format(colors["raw"], operand[1], colors["normal"])
@ -103,20 +129,28 @@ def _colorize_operands(operands, colors):
elif operand[0] & Operand.KIND:
if operand[0] == (Operand.KIND + Kind.STRING):
yield "{}{}{}".format(colors["string"], operand[2], colors["normal"])
yield "{}{}{}".format(
colors["string"], operand[2], colors["normal"]
)
elif operand[0] == (Operand.KIND + Kind.METH):
yield "{}{}{}".format(colors["meth"], operand[2], colors["normal"])
yield "{}{}{}".format(
colors["meth"], operand[2], colors["normal"]
)
elif operand[0] == (Operand.KIND + Kind.FIELD):
yield "{}{}{}".format(colors["field"], operand[2], colors["normal"])
yield "{}{}{}".format(
colors["field"], operand[2], colors["normal"]
)
elif operand[0] == (Operand.KIND + Kind.TYPE):
yield "{}{}{}".format(colors["type"], operand[2], colors["normal"])
yield "{}{}{}".format(
colors["type"], operand[2], colors["normal"]
)
else:
yield "{}".format(repr(operands[2]))
else:
yield "{}".format(repr(operands[1]))
def PrettyShow(basic_blocks: list[DEXBasicBlock], notes:list=[]) -> None:
def PrettyShow(basic_blocks: list[DEXBasicBlock], notes: list = []) -> None:
idx = 0
offset_color = CONF["COLORS"]["OFFSET"]
@ -142,51 +176,87 @@ def PrettyShow(basic_blocks: list[DEXBasicBlock], notes:list=[]) -> None:
for note in notes[nb]:
_PrintNote(note, 1)
print_fct("\t%s%-3d%s(%s%08x%s) " %
(offset_color, nb, normal_color, offset_addr_color, idx,
normal_color))
print_fct("%s%-20s%s" %
(instruction_name_color, ins.get_name(), normal_color))
print_fct(
"\t%s%-3d%s(%s%08x%s) "
% (
offset_color,
nb,
normal_color,
offset_addr_color,
idx,
normal_color,
)
)
print_fct(
"%s%-20s%s"
% (instruction_name_color, ins.get_name(), normal_color)
)
operands = ins.get_operands()
print_fct(
"%s" %
", ".join(_colorize_operands(operands, colors)))
print_fct("%s" % ", ".join(_colorize_operands(operands, colors)))
op_value = ins.get_op_value()
if ins == instructions[-1] and i.childs:
print_fct(" ")
# packed/sparse-switch
if (op_value == 0x2b or op_value == 0x2c) and len(i.childs) > 1:
if (op_value == 0x2B or op_value == 0x2C) and len(
i.childs
) > 1:
values = i.get_special_ins(idx).get_values()
print_fct("%s[ D:%s%s " %
(branch_false_color, i.childs[0][2].get_name(),
branch_color))
print_fct(' '.join("%d:%s" % (
values[j], i.childs[j + 1][2].get_name()) for j in
range(0, len(i.childs) - 1)) + " ]%s" %
normal_color)
print_fct(
"%s[ D:%s%s "
% (
branch_false_color,
i.childs[0][2].get_name(),
branch_color,
)
)
print_fct(
' '.join(
"%d:%s"
% (values[j], i.childs[j + 1][2].get_name())
for j in range(0, len(i.childs) - 1)
)
+ " ]%s" % normal_color
)
else:
if len(i.childs) == 2:
print_fct("{}[ {}{} ".format(branch_false_color,
i.childs[0][2].get_name(),
branch_true_color))
print_fct(' '.join("%s" % c[2].get_name(
) for c in i.childs[1:]) + " ]%s" % normal_color)
print_fct(
"{}[ {}{} ".format(
branch_false_color,
i.childs[0][2].get_name(),
branch_true_color,
)
)
print_fct(
' '.join(
"%s" % c[2].get_name() for c in i.childs[1:]
)
+ " ]%s" % normal_color
)
else:
print_fct("%s[ " % branch_color + ' '.join(
"%s" % c[2].get_name() for c in i.childs) + " ]%s" %
normal_color)
print_fct(
"%s[ " % branch_color
+ ' '.join(
"%s" % c[2].get_name() for c in i.childs
)
+ " ]%s" % normal_color
)
idx += ins.get_length()
print_fct("\n")
if i.get_exception_analysis():
print_fct("\t%s%s%s\n" %
(exception_color, i.exception_analysis.show_buff(),
normal_color))
print_fct(
"\t%s%s%s\n"
% (
exception_color,
i.exception_analysis.show_buff(),
normal_color,
)
)
print_fct("\n")
@ -205,36 +275,58 @@ def _get_operand_html(operand, registers_colors, colors):
:returns: HTML code of the operands
"""
if operand[0] == Operand.REGISTER:
return '<FONT color="{}">v{}</FONT>'.format(registers_colors[operand[1]], operand[1])
return '<FONT color="{}">v{}</FONT>'.format(
registers_colors[operand[1]], operand[1]
)
if operand[0] == Operand.LITERAL:
return '<FONT color="{}">0x{:x}</FONT>'.format(colors["literal"], operand[1])
return '<FONT color="{}">0x{:x}</FONT>'.format(
colors["literal"], operand[1]
)
if operand[0] == Operand.RAW:
wrapped_adjust = '<br />'.join(escape(repr(i)[1:-1]) for i in textwrap.wrap(operand[1], 64))
return '<FONT color="{}">{}</FONT>'.format(colors["raw"], wrapped_adjust)
wrapped_adjust = '<br />'.join(
escape(repr(i)[1:-1]) for i in textwrap.wrap(operand[1], 64)
)
return '<FONT color="{}">{}</FONT>'.format(
colors["raw"], wrapped_adjust
)
if operand[0] == Operand.OFFSET:
return '<FONT FACE="Times-Italic" color="{}">@0x{:x}</FONT>'.format(colors["offset"], operand[1])
return '<FONT FACE="Times-Italic" color="{}">@0x{:x}</FONT>'.format(
colors["offset"], operand[1]
)
if operand[0] & Operand.KIND:
if operand[0] == (Operand.KIND + Kind.STRING):
wrapped_adjust = "&quot; &#92;<br />&quot;".join(map(escape, textwrap.wrap(operand[2], 64)))
return '<FONT color="{}">&quot;{}&quot;</FONT>'.format(colors["string"], wrapped_adjust)
wrapped_adjust = "&quot; &#92;<br />&quot;".join(
map(escape, textwrap.wrap(operand[2], 64))
)
return '<FONT color="{}">&quot;{}&quot;</FONT>'.format(
colors["string"], wrapped_adjust
)
if operand[0] == (Operand.KIND + Kind.METH):
return '<FONT color="{}">{}</FONT>'.format(colors["method"], escape(operand[2]))
return '<FONT color="{}">{}</FONT>'.format(
colors["method"], escape(operand[2])
)
if operand[0] == (Operand.KIND + Kind.FIELD):
return '<FONT color="{}">{}</FONT>'.format(colors["field"], escape(operand[2]))
return '<FONT color="{}">{}</FONT>'.format(
colors["field"], escape(operand[2])
)
if operand[0] == (Operand.KIND + Kind.TYPE):
return '<FONT color="{}">{}</FONT>'.format(colors["type"], escape(operand[2]))
return '<FONT color="{}">{}</FONT>'.format(
colors["type"], escape(operand[2])
)
return escape(str(operand[2]))
return escape(str(operand[1]))
def method2dot(mx: MethodAnalysis, colors:Union[dict[str,str],None]=None) -> str:
def method2dot(
mx: MethodAnalysis, colors: Union[dict[str, str], None] = None
) -> str:
"""
Export analysis method to dot format.
@ -268,7 +360,7 @@ def method2dot(mx: MethodAnalysis, colors:Union[dict[str,str],None]=None) -> str
"method": "#DF3A01",
"field": "#088A08",
"type": "#0000FF",
"registers_range": ("#999933", "#6666FF")
"registers_range": ("#999933", "#6666FF"),
}
node_tpl = """
@ -287,7 +379,9 @@ def method2dot(mx: MethodAnalysis, colors:Union[dict[str,str],None]=None) -> str
<FONT FACE="{font_face}" color="%s">%s</FONT> %s
</TD>
</TR>
""".format(font_face=font_face)
""".format(
font_face=font_face
)
link_tpl = '<TR><TD PORT="{}"></TD></TR>\n'
@ -297,12 +391,26 @@ def method2dot(mx: MethodAnalysis, colors:Union[dict[str,str],None]=None) -> str
method = mx.get_method()
# This is used as a seed to create unique hashes for the nodes
sha256 = hashlib.sha256((mx.get_method().get_class_name() + mx.get_method().get_name() + mx.get_method().get_descriptor()).encode("utf-8")).hexdigest()
sha256 = hashlib.sha256(
(
mx.get_method().get_class_name()
+ mx.get_method().get_name()
+ mx.get_method().get_descriptor()
).encode("utf-8")
).hexdigest()
# Collect all used Registers and create colors
if method.get_code() and method.get_code().get_registers_size() != 0:
registers = {i: c for i, c in enumerate(color_range(colors["registers_range"][0], colors["registers_range"][1],
method.get_code().get_registers_size()))}
registers = {
i: c
for i, c in enumerate(
color_range(
colors["registers_range"][0],
colors["registers_range"][1],
method.get_code().get_registers_size(),
)
)
}
else:
registers = dict()
@ -311,27 +419,48 @@ def method2dot(mx: MethodAnalysis, colors:Union[dict[str,str],None]=None) -> str
# Go through all basic blocks and create the CFG
for basic_block in mx.basic_blocks:
ins_idx = basic_block.start
block_id = hashlib.md5((sha256 + basic_block.get_name()).encode("utf-8")).hexdigest()
block_id = hashlib.md5(
(sha256 + basic_block.get_name()).encode("utf-8")
).hexdigest()
content = link_tpl.format('header')
for instruction in basic_block.get_instructions():
if instruction.get_op_value() in (0x2b, 0x2c):
new_links.append((basic_block, ins_idx, instruction.get_ref_off() * 2 + ins_idx))
if instruction.get_op_value() in (0x2B, 0x2C):
new_links.append(
(
basic_block,
ins_idx,
instruction.get_ref_off() * 2 + ins_idx,
)
)
elif instruction.get_op_value() == 0x26:
new_links.append((basic_block, ins_idx, instruction.get_ref_off() * 2 + ins_idx))
new_links.append(
(
basic_block,
ins_idx,
instruction.get_ref_off() * 2 + ins_idx,
)
)
operands = instruction.get_operands(ins_idx)
output = ", ".join(_get_operand_html(i, registers, colors) for i in operands)
output = ", ".join(
_get_operand_html(i, registers, colors) for i in operands
)
bg_idx = colors["bg_idx"]
if ins_idx == 0 and "bg_start_idx" in colors:
bg_idx = colors["bg_start_idx"]
content += label_tpl % (
bg_idx, colors["idx"], ins_idx, colors["bg_instruction"],
bg_idx,
colors["idx"],
ins_idx,
colors["bg_instruction"],
colors["instruction_name"],
instruction.get_name(), output)
instruction.get_name(),
output,
)
ins_idx += instruction.get_length()
@ -350,10 +479,17 @@ def method2dot(mx: MethodAnalysis, colors:Union[dict[str,str],None]=None) -> str
values = None
# The last instruction is important and still set from the loop
# FIXME: what if there is no instruction in the basic block?
if instruction.get_op_value() in (0x2b, 0x2c) and len(basic_block.childs) > 1:
if (
instruction.get_op_value() in (0x2B, 0x2C)
and len(basic_block.childs) > 1
):
val = colors["default_branch"]
values = ["default"]
values.extend(basic_block.get_special_ins(ins_idx - instruction.get_length()).get_values())
values.extend(
basic_block.get_special_ins(
ins_idx - instruction.get_length()
).get_values()
)
# updating dot edges
for DVMBasicMethodBlockChild in basic_block.childs:
@ -362,10 +498,14 @@ def method2dot(mx: MethodAnalysis, colors:Union[dict[str,str],None]=None) -> str
if values:
label_edge = values.pop(0)
child_id = hashlib.md5((sha256 + DVMBasicMethodBlockChild[-1].get_name()).encode("utf-8")).hexdigest()
edges_html += "struct_{}:tail -> struct_{}:header [color=\"{}\", label=\"{}\"];\n".format(block_id,
child_id, val,
label_edge)
child_id = hashlib.md5(
(sha256 + DVMBasicMethodBlockChild[-1].get_name()).encode(
"utf-8"
)
).hexdigest()
edges_html += "struct_{}:tail -> struct_{}:header [color=\"{}\", label=\"{}\"];\n".format(
block_id, child_id, val, label_edge
)
# color switch
if val == colors["false_branch"]:
@ -378,26 +518,42 @@ def method2dot(mx: MethodAnalysis, colors:Union[dict[str,str],None]=None) -> str
for exception_elem in exception_analysis.exceptions:
exception_block = exception_elem[-1]
if exception_block:
exception_id = hashlib.md5((sha256 + exception_block.get_name()).encode("utf-8")).hexdigest()
exception_id = hashlib.md5(
(sha256 + exception_block.get_name()).encode("utf-8")
).hexdigest()
edges_html += "struct_{}:tail -> struct_{}:header [color=\"{}\", label=\"{}\"];\n".format(
block_id, exception_id, "black", exception_elem[0])
block_id, exception_id, "black", exception_elem[0]
)
for link in new_links:
basic_block = link[0]
DVMBasicMethodBlockChild = mx.basic_blocks.get_basic_block(link[2])
if DVMBasicMethodBlockChild:
block_id = hashlib.md5((sha256 + basic_block.get_name()).encode("utf-8")).hexdigest()
child_id = hashlib.md5((sha256 + DVMBasicMethodBlockChild.get_name()).encode("utf-8")).hexdigest()
block_id = hashlib.md5(
(sha256 + basic_block.get_name()).encode("utf-8")
).hexdigest()
child_id = hashlib.md5(
(sha256 + DVMBasicMethodBlockChild.get_name()).encode("utf-8")
).hexdigest()
edges_html += "struct_{}:tail -> struct_{}:header [color=\"{}\", label=\"data(0x{:x}) to @0x{:x}\", style=\"dashed\"];\n".format(
block_id, child_id, "yellow", link[1], link[2])
block_id, child_id, "yellow", link[1], link[2]
)
method_label = method.get_class_name() + "." + method.get_name() + "->" + method.get_descriptor()
method_label = (
method.get_class_name()
+ "."
+ method.get_name()
+ "->"
+ method.get_descriptor()
)
method_information = method.get_information()
if method_information:
method_label += "\\nLocal registers v{} ... v{}".format(*method_information["registers"])
method_label += "\\nLocal registers v{} ... v{}".format(
*method_information["registers"]
)
if "params" in method_information:
for register, rtype in method_information["params"]:
method_label += "\\nparam v%d = %s" % (register, rtype)
@ -407,10 +563,11 @@ def method2dot(mx: MethodAnalysis, colors:Union[dict[str,str],None]=None) -> str
def method2format(
output:str,
_format:str="png",
mx:Union[MethodAnalysis,None]=None,
raw:Union[str,None]=None):
output: str,
_format: str = "png",
mx: Union[MethodAnalysis, None] = None,
raw: Union[str, None] = None,
):
"""
Export method structure as a graph to a specific file format using dot from the graphviz package.
The result is written to the file specified via :code:`output`.
@ -453,11 +610,12 @@ def method2format(
{edges}
}}
""".format(clustername=hashlib.md5(output.encode("UTF-8")).hexdigest(),
classname=data['name'],
nodes=data['nodes'],
edges=data['edges'],
)
""".format(
clustername=hashlib.md5(output.encode("UTF-8")).hexdigest(),
classname=data['name'],
nodes=data['nodes'],
edges=data['edges'],
)
# NOTE: In certain cases the graph_from_dot_data function might fail.
# There is a bug in the code that certain html strings are interpreted as comment
@ -478,17 +636,24 @@ def method2format(
d = pydot.graph_from_dot_data(buff)
if len(d) > 1:
# Not sure what to do in this case?!
logger.warning("The graph generated for '{}' has too many subgraphs! "
"Only plotting the first one.".format(output))
logger.warning(
"The graph generated for '{}' has too many subgraphs! "
"Only plotting the first one.".format(output)
)
for g in d:
try:
getattr(g, "write_" + _format.lower())(output)
break
except FileNotFoundError:
logger.error("Could not write graph image, ensure graphviz is installed!")
logger.error(
"Could not write graph image, ensure graphviz is installed!"
)
raise
def method2png(output:str, mx:MethodAnalysis, raw:Union[str,None]=None) -> None:
def method2png(
output: str, mx: MethodAnalysis, raw: Union[str, None] = None
) -> None:
"""
Export method to a png file format
@ -506,7 +671,9 @@ def method2png(output:str, mx:MethodAnalysis, raw:Union[str,None]=None) -> None:
method2format(output, "png", mx, buff)
def method2jpg(output:str, mx:MethodAnalysis, raw:Union[str,None]=None) -> None:
def method2jpg(
output: str, mx: MethodAnalysis, raw: Union[str, None] = None
) -> None:
"""
Export method to a jpg file format
@ -524,7 +691,7 @@ def method2jpg(output:str, mx:MethodAnalysis, raw:Union[str,None]=None) -> None:
method2format(output, "jpg", mx, buff)
def vm2json(vm:DEX) -> str:
def vm2json(vm: DEX) -> str:
"""
Get a JSON representation of a DEX file
@ -547,14 +714,14 @@ def vm2json(vm:DEX) -> str:
class TmpBlock:
def __init__(self, name:str) -> None:
def __init__(self, name: str) -> None:
self.name = name
def get_name(self) -> str:
return self.name
def method2json(mx: MethodAnalysis, directed_graph:bool=False) -> str:
def method2json(mx: MethodAnalysis, directed_graph: bool = False) -> str:
"""
Create directed or undirected graph in the json format.
@ -578,14 +745,23 @@ def method2json_undirect(mx: MethodAnalysis) -> str:
d["reports"] = reports
for DVMBasicMethodBlock in mx.basic_blocks.gets():
cblock = {"BasicBlockId": DVMBasicMethodBlock.get_name(),
"registers": mx.get_method().get_code().get_registers_size(), "instructions": []}
cblock = {
"BasicBlockId": DVMBasicMethodBlock.get_name(),
"registers": mx.get_method().get_code().get_registers_size(),
"instructions": [],
}
ins_idx = DVMBasicMethodBlock.start
for DVMBasicMethodBlockInstruction in DVMBasicMethodBlock.get_instructions():
c_ins = {"idx": ins_idx, "name": DVMBasicMethodBlockInstruction.get_name(),
"operands": DVMBasicMethodBlockInstruction.get_operands(
ins_idx)}
for (
DVMBasicMethodBlockInstruction
) in DVMBasicMethodBlock.get_instructions():
c_ins = {
"idx": ins_idx,
"name": DVMBasicMethodBlockInstruction.get_name(),
"operands": DVMBasicMethodBlockInstruction.get_operands(
ins_idx
),
}
cblock["instructions"].append(c_ins)
ins_idx += DVMBasicMethodBlockInstruction.get_length()
@ -613,18 +789,25 @@ def method2json_direct(mx: MethodAnalysis) -> str:
l = []
for DVMBasicMethodBlock in mx.basic_blocks.gets():
for index, DVMBasicMethodBlockChild in enumerate(DVMBasicMethodBlock.childs):
if DVMBasicMethodBlock.get_name() == DVMBasicMethodBlockChild[-1].get_name():
for index, DVMBasicMethodBlockChild in enumerate(
DVMBasicMethodBlock.childs
):
if (
DVMBasicMethodBlock.get_name()
== DVMBasicMethodBlockChild[-1].get_name()
):
preblock = TmpBlock(DVMBasicMethodBlock.get_name() + "-pre")
cnblock = {"BasicBlockId": DVMBasicMethodBlock.get_name() + "-pre",
"start": DVMBasicMethodBlock.start,
"notes": [],
"Edge": [DVMBasicMethodBlock.get_name()],
"registers": 0,
"instructions": [],
"info_bb": 0}
cnblock = {
"BasicBlockId": DVMBasicMethodBlock.get_name() + "-pre",
"start": DVMBasicMethodBlock.start,
"notes": [],
"Edge": [DVMBasicMethodBlock.get_name()],
"registers": 0,
"instructions": [],
"info_bb": 0,
}
l.append(cnblock)
@ -633,29 +816,40 @@ def method2json_direct(mx: MethodAnalysis) -> str:
hooks[parent[-1].get_name()].append(preblock)
for idx, child in enumerate(parent[-1].childs):
if child[-1].get_name() == DVMBasicMethodBlock.get_name(
if (
child[-1].get_name()
== DVMBasicMethodBlock.get_name()
):
hooks[parent[-1].get_name()].append(child[-1])
for DVMBasicMethodBlock in mx.basic_blocks.gets():
cblock = {"BasicBlockId": DVMBasicMethodBlock.get_name(),
"start": DVMBasicMethodBlock.start,
"notes": DVMBasicMethodBlock.get_notes(),
"registers": mx.get_method().get_code().get_registers_size(),
"instructions": []}
cblock = {
"BasicBlockId": DVMBasicMethodBlock.get_name(),
"start": DVMBasicMethodBlock.start,
"notes": DVMBasicMethodBlock.get_notes(),
"registers": mx.get_method().get_code().get_registers_size(),
"instructions": [],
}
ins_idx = DVMBasicMethodBlock.start
last_instru = None
for DVMBasicMethodBlockInstruction in DVMBasicMethodBlock.get_instructions():
c_ins = {"idx": ins_idx,
"name": DVMBasicMethodBlockInstruction.get_name(),
"operands": DVMBasicMethodBlockInstruction.get_operands(ins_idx),
}
for (
DVMBasicMethodBlockInstruction
) in DVMBasicMethodBlock.get_instructions():
c_ins = {
"idx": ins_idx,
"name": DVMBasicMethodBlockInstruction.get_name(),
"operands": DVMBasicMethodBlockInstruction.get_operands(
ins_idx
),
}
cblock["instructions"].append(c_ins)
if (DVMBasicMethodBlockInstruction.get_op_value() == 0x2b or
DVMBasicMethodBlockInstruction.get_op_value() == 0x2c):
if (
DVMBasicMethodBlockInstruction.get_op_value() == 0x2B
or DVMBasicMethodBlockInstruction.get_op_value() == 0x2C
):
values = DVMBasicMethodBlock.get_special_ins(ins_idx)
cblock["info_next"] = values.get_values()
@ -667,17 +861,24 @@ def method2json_direct(mx: MethodAnalysis) -> str:
if len(DVMBasicMethodBlock.childs) > 1:
cblock["info_bb"] = 1
if (last_instru.get_op_value() == 0x2b or
last_instru.get_op_value() == 0x2c):
if (
last_instru.get_op_value() == 0x2B
or last_instru.get_op_value() == 0x2C
):
cblock["info_bb"] = 2
cblock["Edge"] = []
for DVMBasicMethodBlockChild in DVMBasicMethodBlock.childs:
ok = False
if DVMBasicMethodBlock.get_name() in hooks:
if DVMBasicMethodBlockChild[-1] in hooks[DVMBasicMethodBlock.get_name()]:
if (
DVMBasicMethodBlockChild[-1]
in hooks[DVMBasicMethodBlock.get_name()]
):
ok = True
cblock["Edge"].append(hooks[DVMBasicMethodBlock.get_name()][0].get_name())
cblock["Edge"].append(
hooks[DVMBasicMethodBlock.get_name()][0].get_name()
)
if not ok:
cblock["Edge"].append(DVMBasicMethodBlockChild[-1].get_name())
@ -693,7 +894,7 @@ def method2json_direct(mx: MethodAnalysis) -> str:
return json.dumps(d)
def object_to_bytes(obj:Union[str,bool,int,bytearray]) -> bytearray:
def object_to_bytes(obj: Union[str, bool, int, bytearray]) -> bytearray:
"""
Convert a object to a bytearray or call get_raw() of the object
if no useful type was found.
@ -712,7 +913,7 @@ def object_to_bytes(obj:Union[str,bool,int,bytearray]) -> bytearray:
return obj.get_raw()
def FormatClassToJava(i:str) -> str:
def FormatClassToJava(i: str) -> str:
"""
Transform a java class name into the typed variant found in DEX files.
@ -727,7 +928,7 @@ def FormatClassToJava(i:str) -> str:
return "L" + i.replace(".", "/") + ";"
def FormatClassToPython(i:str) -> str:
def FormatClassToPython(i: str) -> str:
"""
Transform a typed class name into a form which can be used as a python
attribute
@ -747,7 +948,7 @@ def FormatClassToPython(i:str) -> str:
return i
def get_package_class_name(name:str)->tuple[str, str]:
def get_package_class_name(name: str) -> tuple[str, str]:
"""
Return package and class name in a java variant from a typed variant name.
@ -771,13 +972,17 @@ def get_package_class_name(name:str)->tuple[str, str]:
# name is MUTF8, so make sure we get the string variant
name = str(name)
if name[-1] != ';':
raise ValueError("The name '{}' does not look like a typed name!".format(name))
raise ValueError(
"The name '{}' does not look like a typed name!".format(name)
)
# discard array types, there might be many...
name = name.lstrip('[')
if name[0] != 'L':
raise ValueError("The name '{}' does not look like a typed name!".format(name))
raise ValueError(
"The name '{}' does not look like a typed name!".format(name)
)
name = name[1:-1]
if '/' not in name:
@ -789,7 +994,7 @@ def get_package_class_name(name:str)->tuple[str, str]:
return package, clsname
def FormatNameToPython(i:str) -> str:
def FormatNameToPython(i: str) -> str:
"""
Transform a (method) name into a form which can be used as a python
attribute
@ -810,7 +1015,7 @@ def FormatNameToPython(i:str) -> str:
return i
def FormatDescriptorToPython(i:str) -> str:
def FormatDescriptorToPython(i: str) -> str:
"""
Format a descriptor into a form which can be used as a python attribute

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,9 @@
from enum import IntEnum
from collections import OrderedDict
from enum import IntEnum
# This file contains dictionaries used in the Dalvik Format.
# Used to identify different types of operands
class Kind(IntEnum):
"""
@ -12,6 +13,7 @@ class Kind(IntEnum):
It is used to reference the actual item instead of the refernece index
from the :class:`ClassManager` when disassembling the bytecode.
"""
# Indicates a method reference
METH = 0
# Indicates that opcode argument is a string index
@ -41,6 +43,7 @@ class Operand(IntEnum):
"""
Enumeration used for the operand type of opcodes
"""
REGISTER = 0
LITERAL = 1
RAW = 2
@ -72,44 +75,115 @@ class TypeMapItem(IntEnum):
ANNOTATION_ITEM = 0x2004
ENCODED_ARRAY_ITEM = 0x2005
ANNOTATIONS_DIRECTORY_ITEM = 0x2006
HIDDENAPI_CLASS_DATA_ITEM = 0xf000
HIDDENAPI_CLASS_DATA_ITEM = 0xF000
@staticmethod
def _get_dependencies():
return OrderedDict([
(TypeMapItem.HEADER_ITEM, set()),
(TypeMapItem.STRING_ID_ITEM, {TypeMapItem.STRING_DATA_ITEM}),
(TypeMapItem.TYPE_ID_ITEM, {TypeMapItem.STRING_ID_ITEM}),
(TypeMapItem.PROTO_ID_ITEM, {TypeMapItem.STRING_ID_ITEM, TypeMapItem.TYPE_ID_ITEM, TypeMapItem.TYPE_LIST}),
(TypeMapItem.FIELD_ID_ITEM, {TypeMapItem.STRING_ID_ITEM, TypeMapItem.TYPE_ID_ITEM}),
(TypeMapItem.METHOD_ID_ITEM,
{TypeMapItem.STRING_ID_ITEM, TypeMapItem.TYPE_ID_ITEM, TypeMapItem.PROTO_ID_ITEM}),
(TypeMapItem.CLASS_DEF_ITEM,
{TypeMapItem.TYPE_ID_ITEM, TypeMapItem.TYPE_LIST, TypeMapItem.STRING_ID_ITEM, TypeMapItem.DEBUG_INFO_ITEM,
TypeMapItem.ANNOTATIONS_DIRECTORY_ITEM, TypeMapItem.CLASS_DATA_ITEM, TypeMapItem.ENCODED_ARRAY_ITEM}),
(TypeMapItem.CALL_SITE_ITEM,
{TypeMapItem.METHOD_HANDLE_ITEM, TypeMapItem.STRING_ID_ITEM, TypeMapItem.METHOD_ID_ITEM}),
# TODO: check if this is correct
(TypeMapItem.METHOD_HANDLE_ITEM, {TypeMapItem.FIELD_ID_ITEM, TypeMapItem.METHOD_ID_ITEM}),
# TODO: check if this is correct
(TypeMapItem.MAP_LIST, set()),
(TypeMapItem.TYPE_LIST, {TypeMapItem.TYPE_ID_ITEM}),
(TypeMapItem.ANNOTATION_SET_REF_LIST, {TypeMapItem.ANNOTATION_SET_ITEM}),
(TypeMapItem.ANNOTATION_SET_ITEM, {TypeMapItem.ANNOTATION_ITEM}),
(TypeMapItem.CLASS_DATA_ITEM, {TypeMapItem.FIELD_ID_ITEM, TypeMapItem.METHOD_ID_ITEM}),
(TypeMapItem.CODE_ITEM, {TypeMapItem.DEBUG_INFO_ITEM, TypeMapItem.TYPE_ID_ITEM}),
(TypeMapItem.STRING_DATA_ITEM, set()),
(TypeMapItem.DEBUG_INFO_ITEM, {TypeMapItem.STRING_ID_ITEM, TypeMapItem.TYPE_ID_ITEM}),
(TypeMapItem.ANNOTATION_ITEM,
{TypeMapItem.PROTO_ID_ITEM, TypeMapItem.STRING_ID_ITEM, TypeMapItem.TYPE_ID_ITEM,
TypeMapItem.FIELD_ID_ITEM, TypeMapItem.METHOD_ID_ITEM}),
(TypeMapItem.ENCODED_ARRAY_ITEM,
{TypeMapItem.PROTO_ID_ITEM, TypeMapItem.STRING_ID_ITEM, TypeMapItem.TYPE_ID_ITEM,
TypeMapItem.FIELD_ID_ITEM, TypeMapItem.METHOD_ID_ITEM}),
(TypeMapItem.ANNOTATIONS_DIRECTORY_ITEM,
{TypeMapItem.FIELD_ID_ITEM, TypeMapItem.METHOD_ID_ITEM, TypeMapItem.ANNOTATION_SET_ITEM}),
(TypeMapItem.HIDDENAPI_CLASS_DATA_ITEM, set()),
])
return OrderedDict(
[
(TypeMapItem.HEADER_ITEM, set()),
(TypeMapItem.STRING_ID_ITEM, {TypeMapItem.STRING_DATA_ITEM}),
(TypeMapItem.TYPE_ID_ITEM, {TypeMapItem.STRING_ID_ITEM}),
(
TypeMapItem.PROTO_ID_ITEM,
{
TypeMapItem.STRING_ID_ITEM,
TypeMapItem.TYPE_ID_ITEM,
TypeMapItem.TYPE_LIST,
},
),
(
TypeMapItem.FIELD_ID_ITEM,
{TypeMapItem.STRING_ID_ITEM, TypeMapItem.TYPE_ID_ITEM},
),
(
TypeMapItem.METHOD_ID_ITEM,
{
TypeMapItem.STRING_ID_ITEM,
TypeMapItem.TYPE_ID_ITEM,
TypeMapItem.PROTO_ID_ITEM,
},
),
(
TypeMapItem.CLASS_DEF_ITEM,
{
TypeMapItem.TYPE_ID_ITEM,
TypeMapItem.TYPE_LIST,
TypeMapItem.STRING_ID_ITEM,
TypeMapItem.DEBUG_INFO_ITEM,
TypeMapItem.ANNOTATIONS_DIRECTORY_ITEM,
TypeMapItem.CLASS_DATA_ITEM,
TypeMapItem.ENCODED_ARRAY_ITEM,
},
),
(
TypeMapItem.CALL_SITE_ITEM,
{
TypeMapItem.METHOD_HANDLE_ITEM,
TypeMapItem.STRING_ID_ITEM,
TypeMapItem.METHOD_ID_ITEM,
},
),
# TODO: check if this is correct
(
TypeMapItem.METHOD_HANDLE_ITEM,
{TypeMapItem.FIELD_ID_ITEM, TypeMapItem.METHOD_ID_ITEM},
),
# TODO: check if this is correct
(TypeMapItem.MAP_LIST, set()),
(TypeMapItem.TYPE_LIST, {TypeMapItem.TYPE_ID_ITEM}),
(
TypeMapItem.ANNOTATION_SET_REF_LIST,
{TypeMapItem.ANNOTATION_SET_ITEM},
),
(
TypeMapItem.ANNOTATION_SET_ITEM,
{TypeMapItem.ANNOTATION_ITEM},
),
(
TypeMapItem.CLASS_DATA_ITEM,
{TypeMapItem.FIELD_ID_ITEM, TypeMapItem.METHOD_ID_ITEM},
),
(
TypeMapItem.CODE_ITEM,
{TypeMapItem.DEBUG_INFO_ITEM, TypeMapItem.TYPE_ID_ITEM},
),
(TypeMapItem.STRING_DATA_ITEM, set()),
(
TypeMapItem.DEBUG_INFO_ITEM,
{TypeMapItem.STRING_ID_ITEM, TypeMapItem.TYPE_ID_ITEM},
),
(
TypeMapItem.ANNOTATION_ITEM,
{
TypeMapItem.PROTO_ID_ITEM,
TypeMapItem.STRING_ID_ITEM,
TypeMapItem.TYPE_ID_ITEM,
TypeMapItem.FIELD_ID_ITEM,
TypeMapItem.METHOD_ID_ITEM,
},
),
(
TypeMapItem.ENCODED_ARRAY_ITEM,
{
TypeMapItem.PROTO_ID_ITEM,
TypeMapItem.STRING_ID_ITEM,
TypeMapItem.TYPE_ID_ITEM,
TypeMapItem.FIELD_ID_ITEM,
TypeMapItem.METHOD_ID_ITEM,
},
),
(
TypeMapItem.ANNOTATIONS_DIRECTORY_ITEM,
{
TypeMapItem.FIELD_ID_ITEM,
TypeMapItem.METHOD_ID_ITEM,
TypeMapItem.ANNOTATION_SET_ITEM,
},
),
(TypeMapItem.HIDDENAPI_CLASS_DATA_ITEM, set()),
]
)
@staticmethod
def determine_load_order():
@ -129,6 +203,7 @@ class TypeMapItem(IntEnum):
unloaded.discard(type_name)
return ordered
# https://source.android.com/devices/tech/dalvik/dex-format#access-flags
ACCESS_FLAGS = {
0x1: 'public',
@ -162,4 +237,3 @@ TYPE_DESCRIPTOR = {
'F': 'float',
'D': 'double',
}

View File

@ -18,23 +18,25 @@ if _public_res is None:
_public_res[_type] = {}
_public_res[_type][_name] = _id
else:
raise Exception("need to copy the sdk/platforms/android-?/data/res/values/public.xml here")
raise Exception(
"need to copy the sdk/platforms/android-?/data/res/values/public.xml here"
)
SYSTEM_RESOURCES = {
"attributes": {
"forward": {k: v for k, v in _public_res['attr'].items()},
"inverse": {v: k for k, v in _public_res['attr'].items()}
"inverse": {v: k for k, v in _public_res['attr'].items()},
},
"styles": {
"forward": {k: v for k, v in _public_res['style'].items()},
"inverse": {v: k for k, v in _public_res['style'].items()}
}
"inverse": {v: k for k, v in _public_res['style'].items()},
},
}
if __name__ == '__main__':
import json
_resources = None
if _resources is None:
root = os.path.dirname(os.path.realpath(__file__))
@ -47,10 +49,15 @@ if __name__ == '__main__':
# TODO raise error instead?
_resources = {}
for _type in set([] + list(_public_res.keys()) + list(_resources.keys())):
for k in set([] + list(_public_res.get(_type, {}).keys())
+ list(_resources.get(_type, {}).keys())):
a,b = _public_res.get(_type, {}).get(k), \
_resources.get(_type, {}).get(k),
for k in set(
[]
+ list(_public_res.get(_type, {}).keys())
+ list(_resources.get(_type, {}).keys())
):
a, b = (
_public_res.get(_type, {}).get(k),
_resources.get(_type, {}).get(k),
)
if a != b:
print(k, a,b)
print(k, a, b)
print(None)

View File

@ -17,13 +17,13 @@
from collections import defaultdict
from androguard.decompiler.opcode_ins import INSTRUCTION_SET
from loguru import logger
from androguard.decompiler.instruction import MoveExceptionExpression
from androguard.decompiler.node import Node
from androguard.decompiler.opcode_ins import INSTRUCTION_SET
from androguard.decompiler.util import get_type
from loguru import logger
class BasicBlock(Node):
def __init__(self, name: str, block_ins: list) -> None:
@ -191,8 +191,9 @@ class Condition:
return loc_ins
def visit(self, visitor):
return visitor.visit_short_circuit_condition(self.isnot, self.isand,
self.cond1, self.cond2)
return visitor.visit_short_circuit_condition(
self.isnot, self.isand, self.cond1, self.cond2
)
def __str__(self):
if self.isnot:
@ -326,19 +327,19 @@ def build_node_from_block(block, vmap, gen_ret, exception_type=None):
fillarray = block.get_special_ins(idx)
lins.append(_ins(ins, vmap, fillarray))
# invoke-kind[/range]
elif 0x6e <= opcode <= 0x72 or 0x74 <= opcode <= 0x78:
elif 0x6E <= opcode <= 0x72 or 0x74 <= opcode <= 0x78:
lins.append(_ins(ins, vmap, gen_ret))
# filled-new-array[/range]
elif 0x24 <= opcode <= 0x25:
lins.append(_ins(ins, vmap, gen_ret.new()))
# move-result*
elif 0xa <= opcode <= 0xc:
elif 0xA <= opcode <= 0xC:
lins.append(_ins(ins, vmap, gen_ret.last()))
# move-exception
elif opcode == 0xd:
elif opcode == 0xD:
lins.append(_ins(ins, vmap, exception_type))
# monitor-{enter,exit}
elif 0x1d <= opcode <= 0x1e:
elif 0x1D <= opcode <= 0x1E:
idx += ins.get_length()
continue
else:
@ -346,15 +347,15 @@ def build_node_from_block(block, vmap, gen_ret, exception_type=None):
idx += ins.get_length()
name = block.get_name()
# return*
if 0xe <= opcode <= 0x11:
if 0xE <= opcode <= 0x11:
node = ReturnBlock(name, lins)
# {packed,sparse}-switch
elif 0x2b <= opcode <= 0x2c:
elif 0x2B <= opcode <= 0x2C:
idx -= ins.get_length()
values = block.get_special_ins(idx)
node = SwitchBlock(name, values, lins)
# if-test[z]
elif 0x32 <= opcode <= 0x3d:
elif 0x32 <= opcode <= 0x3D:
node = CondBlock(name, lins)
node.off_last_ins = ins.get_ref_off()
# throw
@ -362,7 +363,7 @@ def build_node_from_block(block, vmap, gen_ret, exception_type=None):
node = ThrowBlock(name, lins)
else:
# goto*
if 0x28 <= opcode <= 0x2a:
if 0x28 <= opcode <= 0x2A:
lins.pop()
node = StatementBlock(name, lins)
return node

View File

@ -16,14 +16,20 @@
# limitations under the License.
from collections import defaultdict
from loguru import logger
from androguard.decompiler.basic_blocks import (
CatchBlock, Condition, LoopBlock, ShortCircuitBlock, TryBlock)
CatchBlock,
Condition,
LoopBlock,
ShortCircuitBlock,
TryBlock,
)
from androguard.decompiler.graph import Graph
from androguard.decompiler.node import Interval
from androguard.decompiler.util import common_dom
from loguru import logger
def intervals(graph):
"""
@ -53,7 +59,8 @@ def intervals(graph):
change = False
for node in graph.rpo[1:]:
if all(
p in interv_heads[head] for p in graph.all_preds(node)):
p in interv_heads[head] for p in graph.all_preds(node)
):
change |= interv_heads[head].add_node(node)
# At this stage, a node which is not in the interval, but has one
@ -62,9 +69,10 @@ def intervals(graph):
for node in graph:
if node not in interv_heads[head] and node not in heads:
if any(
p in interv_heads[head] for p in graph.all_preds(node)):
p in interv_heads[head] for p in graph.all_preds(node)
):
edges[interv_heads[head]].append(node)
assert (node not in heads)
assert node not in heads
heads.append(node)
interval_graph.add_node(interv_heads[head])
@ -163,12 +171,13 @@ def loop_follow(start, end, nodes_in_loop):
num_next = float('inf')
for node in nodes_in_loop:
if node.type.is_cond:
if (node.true.num < num_next and
node.true not in nodes_in_loop):
if node.true.num < num_next and node.true not in nodes_in_loop:
follow = node.true
num_next = follow.num
elif (node.false.num < num_next and
node.false not in nodes_in_loop):
elif (
node.false.num < num_next
and node.false not in nodes_in_loop
):
follow = node.false
num_next = follow.num
start.follow['loop'] = follow
@ -423,7 +432,9 @@ def identify_structures(graph, idoms):
loop_follow(node, node.latch, node.loop_nodes)
for node in if_unresolved:
follows = [n for n in (node.follow['loop'], node.follow['switch']) if n]
follows = [
n for n in (node.follow['loop'], node.follow['switch']) if n
]
if len(follows) >= 1:
follow = min(follows, key=lambda x: x.num)
node.follow['if'] = follow

View File

@ -16,11 +16,11 @@
"""This file is a simplified version of writer.py that outputs an AST instead of source code."""
import struct
from androguard.decompiler import basic_blocks, instruction, opcode_ins
from androguard.core.dex.dex_types import TYPE_DESCRIPTOR
from loguru import logger
from androguard.core.dex.dex_types import TYPE_DESCRIPTOR
from androguard.decompiler import basic_blocks, instruction, opcode_ins
class JSONWriter:
def __init__(self, graph, method):
@ -73,8 +73,8 @@ class JSONWriter:
# DAD doesn't create any params for abstract methods
if len(params) != len(m.params_type):
assert ('abstract' in flags or 'native' in flags)
assert (not params)
assert 'abstract' in flags or 'native' in flags
assert not params
params = list(range(len(m.params_type)))
paramdecls = []
@ -113,14 +113,18 @@ class JSONWriter:
elif isinstance(node, basic_blocks.LoopBlock):
return self.get_cond(node.cond)
else:
assert (type(node) == basic_blocks.CondBlock)
assert (len(node.ins) == 1)
assert type(node) == basic_blocks.CondBlock
assert len(node.ins) == 1
return self.visit_expr(node.ins[-1])
def visit_node(self, node):
if node in (self.if_follow[-1], self.switch_follow[-1],
self.loop_follow[-1], self.latch_node[-1],
self.try_follow[-1]):
if node in (
self.if_follow[-1],
self.switch_follow[-1],
self.loop_follow[-1],
self.latch_node[-1],
self.try_follow[-1],
):
return
if not node.type.is_return and node in self.visited_nodes:
return
@ -166,7 +170,7 @@ class JSONWriter:
else:
self.visit_node(loop.latch)
assert (cond_expr is not None and isDo is not None)
assert cond_expr is not None and isDo is not None
self.add(self.loop_stmt(isDo, cond_expr, body))
if follow is not None:
self.visit_node(follow)
@ -197,8 +201,10 @@ class JSONWriter:
self.add(self.if_stmt(cond_expr, scopes))
elif follow is not None:
if cond.true in (follow, self.next_case) or \
cond.num > cond.true.num:
if (
cond.true in (follow, self.next_case)
or cond.num > cond.true.num
):
# or cond.true.num > cond.false.num:
cond.neg()
cond.true, cond.false = cond.false, cond.true
@ -294,7 +300,7 @@ class JSONWriter:
for catch_node in try_node.catch:
if catch_node.exception_ins:
ins = catch_node.exception_ins
assert (isinstance(ins, instruction.MoveExceptionExpression))
assert isinstance(ins, instruction.MoveExceptionExpression)
var = ins.var_map[ins.ref]
var.declared = True
@ -303,7 +309,9 @@ class JSONWriter:
else:
ctype = catch_node.catch_type
name = '_'
catch_decl = self.var_decl(self.parse_descriptor(ctype), self.local(name))
catch_decl = self.var_decl(
self.parse_descriptor(ctype), self.local(name)
)
with self as body:
self.visit_node(catch_node.catch_start)
@ -323,7 +331,9 @@ class JSONWriter:
def _visit_ins(self, op, isCtor=False):
if isinstance(op, instruction.ReturnInstruction):
expr = None if op.arg is None else self.visit_expr(op.var_map[op.arg])
expr = (
None if op.arg is None else self.visit_expr(op.var_map[op.arg])
)
return self.return_stmt(expr)
elif isinstance(op, instruction.ThrowExpression):
return self.throw_stmt(self.visit_expr(op.var_map[op.ref]))
@ -331,11 +341,20 @@ class JSONWriter:
return None
# Local var decl statements
if isinstance(op, (instruction.AssignExpression, instruction.MoveExpression,
instruction.MoveResultExpression)):
if isinstance(
op,
(
instruction.AssignExpression,
instruction.MoveExpression,
instruction.MoveResultExpression,
),
):
lhs = op.var_map.get(op.lhs)
rhs = op.rhs if isinstance(
op, instruction.AssignExpression) else op.var_map.get(op.rhs)
rhs = (
op.rhs
if isinstance(op, instruction.AssignExpression)
else op.var_map.get(op.rhs)
)
if isinstance(lhs, instruction.Variable) and not lhs.declared:
lhs.declared = True
expr = self.visit_expr(rhs)
@ -344,9 +363,13 @@ class JSONWriter:
# skip this() at top of constructors
if isCtor and isinstance(op, instruction.AssignExpression):
op2 = op.rhs
if op.lhs is None and isinstance(op2, instruction.InvokeInstruction):
if op.lhs is None and isinstance(
op2, instruction.InvokeInstruction
):
if op2.name == '<init>' and len(op2.args) == 0:
if isinstance(op2.var_map[op2.base], instruction.ThisParam):
if isinstance(
op2.var_map[op2.base], instruction.ThisParam
):
return None
# MoveExpression is skipped when lhs = rhs
@ -357,14 +380,22 @@ class JSONWriter:
return self.expression_stmt(self.visit_expr(op))
def write_inplace_if_possible(self, lhs, rhs):
if isinstance(rhs, instruction.BinaryExpression) and lhs == rhs.var_map[rhs.arg1]:
if (
isinstance(rhs, instruction.BinaryExpression)
and lhs == rhs.var_map[rhs.arg1]
):
exp_rhs = rhs.var_map[rhs.arg2]
# post increment/decrement
if rhs.op in '+-' and isinstance(exp_rhs,
instruction.Constant) and exp_rhs.get_int_value() == 1:
if (
rhs.op in '+-'
and isinstance(exp_rhs, instruction.Constant)
and exp_rhs.get_int_value() == 1
):
return self.unary_postfix(self.visit_expr(lhs), rhs.op * 2)
# compound assignment
return self.assignment(self.visit_expr(lhs), self.visit_expr(exp_rhs), op=rhs.op)
return self.assignment(
self.visit_expr(lhs), self.visit_expr(exp_rhs), op=rhs.op
)
return self.assignment(self.visit_expr(lhs), self.visit_expr(rhs))
def visit_expr(self, op):
@ -379,7 +410,9 @@ class JSONWriter:
array_expr = self.visit_expr(op.var_map[op.array])
index_expr = self.visit_expr(op.var_map[op.index])
rhs = self.visit_expr(op.var_map[op.rhs])
return self.assignment(self.array_access(array_expr, index_expr), rhs)
return self.assignment(
self.array_access(array_expr, index_expr), rhs
)
if isinstance(op, instruction.AssignExpression):
lhs = op.var_map.get(op.lhs)
@ -390,25 +423,32 @@ class JSONWriter:
if isinstance(op, instruction.BaseClass):
if op.clsdesc is None:
assert (op.cls == "super")
assert op.cls == "super"
return self.local(op.cls)
return self.parse_descriptor(op.clsdesc)
if isinstance(op, instruction.BinaryExpression):
lhs = op.var_map.get(op.arg1)
rhs = op.var_map.get(op.arg2)
expr = self.binary_infix(op.op, self.visit_expr(lhs), self.visit_expr(rhs))
expr = self.binary_infix(
op.op, self.visit_expr(lhs), self.visit_expr(rhs)
)
if not isinstance(op, instruction.BinaryCompExpression):
expr = self.parenthesis(expr)
return expr
if isinstance(op, instruction.CheckCastExpression):
lhs = op.var_map.get(op.arg)
return self.parenthesis(self.cast(self.parse_descriptor(op.clsdesc),
self.visit_expr(lhs)))
return self.parenthesis(
self.cast(
self.parse_descriptor(op.clsdesc), self.visit_expr(lhs)
)
)
if isinstance(op, instruction.ConditionalExpression):
lhs = op.var_map.get(op.arg1)
rhs = op.var_map.get(op.arg2)
return self.binary_infix(op.op, self.visit_expr(lhs), self.visit_expr(rhs))
return self.binary_infix(
op.op, self.visit_expr(lhs), self.visit_expr(rhs)
)
if isinstance(op, instruction.ConditionalZExpression):
arg = op.var_map[op.arg]
if isinstance(arg, instruction.BinaryCompExpression):
@ -457,7 +497,9 @@ class JSONWriter:
return self.field_access(triple, expr)
if isinstance(op, instruction.InstanceInstruction):
triple = op.clsdesc[1:-1], op.name, op.atype
lhs = self.field_access(triple, self.visit_expr(op.var_map[op.lhs]))
lhs = self.field_access(
triple, self.visit_expr(op.var_map[op.lhs])
)
rhs = self.visit_expr(op.var_map[op.rhs])
return self.assignment(lhs, rhs)
@ -467,20 +509,34 @@ class JSONWriter:
params = list(map(self.visit_expr, params))
if op.name == '<init>':
if isinstance(base, instruction.ThisParam):
keyword = 'this' if base.type[1:-1] == op.triple[0] else 'super'
return self.method_invocation(op.triple, keyword, None, params)
keyword = (
'this' if base.type[1:-1] == op.triple[0] else 'super'
)
return self.method_invocation(
op.triple, keyword, None, params
)
elif isinstance(base, instruction.NewInstance):
return ['ClassInstanceCreation', op.triple, params,
self.parse_descriptor(base.type)]
return [
'ClassInstanceCreation',
op.triple,
params,
self.parse_descriptor(base.type),
]
else:
assert (isinstance(base, instruction.Variable))
assert isinstance(base, instruction.Variable)
# fallthrough to create dummy <init> call
return self.method_invocation(op.triple, op.name, self.visit_expr(base), params)
return self.method_invocation(
op.triple, op.name, self.visit_expr(base), params
)
# for unmatched monitor instructions, just create dummy expressions
if isinstance(op, instruction.MonitorEnterExpression):
return self.dummy("monitor enter(", self.visit_expr(op.var_map[op.ref]), ")")
return self.dummy(
"monitor enter(", self.visit_expr(op.var_map[op.ref]), ")"
)
if isinstance(op, instruction.MonitorExitExpression):
return self.dummy("monitor exit(", self.visit_expr(op.var_map[op.ref]), ")")
return self.dummy(
"monitor exit(", self.visit_expr(op.var_map[op.ref]), ")"
)
if isinstance(op, instruction.MoveExpression):
lhs = op.var_map.get(op.lhs)
rhs = op.var_map.get(op.rhs)
@ -513,7 +569,9 @@ class JSONWriter:
if isinstance(op, instruction.UnaryExpression):
lhs = op.var_map.get(op.arg)
if isinstance(op, instruction.CastExpression):
expr = self.cast(self.parse_descriptor(op.clsdesc), self.visit_expr(lhs))
expr = self.cast(
self.parse_descriptor(op.clsdesc), self.visit_expr(lhs)
)
else:
expr = self.unary_prefix(op.op, self.visit_expr(lhs))
return self.parenthesis(expr)
@ -528,7 +586,7 @@ class JSONWriter:
elem_size = value.element_width
if elem_size == 4:
for i in range(0, value.size * 4, 4):
tab.append(struct.unpack('<i', data[i:i + 4])[0])
tab.append(struct.unpack('<i', data[i : i + 4])[0])
else: # FIXME: other cases
for i in range(value.size):
tab.append(data[i])
@ -569,7 +627,9 @@ class JSONWriter:
@staticmethod
def literal_class(desc):
return JSONWriter.literal(JSONWriter.parse_descriptor(desc), ('java/lang/Class', 0))
return JSONWriter.literal(
JSONWriter.parse_descriptor(desc), ('java/lang/Class', 0)
)
@staticmethod
def literal_string(s):
@ -592,7 +652,7 @@ class JSONWriter:
@staticmethod
def _append(sb, stmt):
# Add a statement to the end of a statement block
assert (sb[0] == 'BlockStatement')
assert sb[0] == 'BlockStatement'
if stmt is not None:
sb[2].append(stmt)

View File

@ -16,12 +16,13 @@
# limitations under the License.
from collections import defaultdict
from androguard.decompiler.instruction import (Variable, ThisParam, Param)
from androguard.decompiler.util import build_path, common_dom
from androguard.decompiler.node import Node
from loguru import logger
from androguard.decompiler.instruction import Param, ThisParam, Variable
from androguard.decompiler.node import Node
from androguard.decompiler.util import build_path, common_dom
class BasicReachDef:
def __init__(self, graph, params):
@ -150,8 +151,9 @@ def clear_path_node(graph, reg, loc1, loc2):
logger.debug(' treat loc: %d, ins: %s', loc, ins)
if ins is None:
continue
logger.debug(' LHS: %s, side_effect: %s', ins.get_lhs(),
ins.has_side_effect())
logger.debug(
' LHS: %s, side_effect: %s', ins.get_lhs(), ins.has_side_effect()
)
if ins.get_lhs() == reg or ins.has_side_effect():
return False
return True
@ -218,8 +220,9 @@ def register_propagation(graph, du, ud):
continue
orig_ins = graph.get_ins_from_loc(loc)
logger.debug(' -> %s', orig_ins)
logger.debug(' -> DU(%s, %s) = %s', var, loc,
du[var, loc])
logger.debug(
' -> DU(%s, %s) = %s', var, loc, du[var, loc]
)
# We defined some instructions as not propagable.
# Actually this is the case only for array creation
@ -232,8 +235,10 @@ def register_propagation(graph, du, ud):
# We only try to propagate constants and definition
# points which are used at only one location.
if len(du[var, loc]) > 1:
logger.debug(' => variable has multiple uses'
' and is not const => skip')
logger.debug(
' => variable has multiple uses'
' and is not const => skip'
)
continue
# We check that the propagation is safe for all the
@ -244,8 +249,11 @@ def register_propagation(graph, du, ud):
# be redifined along this path.
safe = True
orig_ins_used_vars = orig_ins.get_used_vars()
logger.debug(' variables used by the original '
'instruction: %s', orig_ins_used_vars)
logger.debug(
' variables used by the original '
'instruction: %s',
orig_ins_used_vars,
)
for var2 in orig_ins_used_vars:
# loc is the location of the defined variable
# i is the location of the current instruction
@ -262,8 +270,11 @@ def register_propagation(graph, du, ud):
# along the path
if orig_ins.has_side_effect():
if not clear_path(graph, None, loc, i):
logger.debug(' %s has side effect and the '
'path is not clear !', orig_ins)
logger.debug(
' %s has side effect and the '
'path is not clear !',
orig_ins,
)
continue
logger.debug(' => Modification of the instruction!')
@ -289,8 +300,9 @@ def register_propagation(graph, du, ud):
if old_ud is None:
continue
ud[var2, i].extend(old_ud)
logger.debug('\t - ud(%s, %s) = %s', var2, i,
ud[var2, i])
logger.debug(
'\t - ud(%s, %s) = %s', var2, i, ud[var2, i]
)
ud.pop((var2, loc))
for def_loc in old_ud:
@ -444,7 +456,8 @@ def build_def_use(graph, lparams):
UD[var, i].append(prior_def)
else:
intersect = analysis.def_to_loc[var].intersection(
analysis.R[node])
analysis.R[node]
)
UD[var, i].extend(intersect)
DU = defaultdict(list)
for var_loc, defs_loc in UD.items():
@ -460,8 +473,9 @@ def place_declarations(graph, dvars, du, ud):
for node in graph.post_order():
for loc, ins in node.get_loc_with_ins():
for var in ins.get_used_vars():
if (not isinstance(dvars[var], Variable) or
isinstance(dvars[var], Param)):
if not isinstance(dvars[var], Variable) or isinstance(
dvars[var], Param
):
continue
var_defs_locs = ud[var, loc]
def_nodes = set()
@ -476,8 +490,11 @@ def place_declarations(graph, dvars, du, ud):
common_dominator = def_nodes.pop()
for def_node in def_nodes:
common_dominator = common_dom(
idom, common_dominator, def_node)
if any(var in range(*common_dominator.ins_range)
for var in ud[var, loc]):
idom, common_dominator, def_node
)
if any(
var in range(*common_dominator.ins_range)
for var in ud[var, loc]
):
continue
common_dominator.add_variable_declaration(dvars[var])

View File

@ -19,7 +19,9 @@
# in Python >= 3.7
# see https://peps.python.org/pep-0563/
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from androguard.core.analysis.analysis import Analysis, MethodAnalysis
from androguard.core.dex import EncodedField
@ -32,27 +34,34 @@ from loguru import logger
import androguard.core.androconf as androconf
import androguard.decompiler.util as util
from androguard.core.analysis import analysis
from androguard.core import apk, dex
from androguard.core.analysis import analysis
from androguard.decompiler.control_flow import identify_structures
from androguard.decompiler.dast import JSONWriter
from androguard.decompiler.dataflow import (
build_def_use,
place_declarations,
dead_code_elimination,
place_declarations,
register_propagation,
split_variables
split_variables,
)
from androguard.decompiler.graph import construct, simplify, split_if_nodes
from androguard.decompiler.instruction import Param, ThisParam
from androguard.decompiler.writer import Writer
from androguard.util import readFile
logger.add(sys.stderr, format="{time} {level} {message}", filter="dad", level="INFO")
logger.add(
sys.stderr, format="{time} {level} {message}", filter="dad", level="INFO"
)
# No seperate DvField class currently
def get_field_ast(field: EncodedField) -> dict:
triple = field.get_class_name()[1:-1], field.get_name(), field.get_descriptor()
triple = (
field.get_class_name()[1:-1],
field.get_name(),
field.get_descriptor(),
)
expr = None
if field.init_value:
@ -63,7 +72,9 @@ def get_field_ast(field: EncodedField) -> dict:
if field.get_descriptor() == 'Ljava/lang/String;':
expr = JSONWriter.literal_string(val)
elif field.proto == 'B':
expr = JSONWriter.literal_hex_int(struct.unpack('<b', struct.pack("B", val))[0])
expr = JSONWriter.literal_hex_int(
struct.unpack('<b', struct.pack("B", val))[0]
)
return {
'triple': triple,
@ -80,6 +91,7 @@ class DvMethod:
:param androguard.core.analysis.analysis.MethodAnalysis methanalysis:
"""
def __init__(self, methanalysis: MethodAnalysis) -> None:
method = methanalysis.get_method()
self.method = method
@ -119,10 +131,16 @@ class DvMethod:
if not __debug__:
from androguard.core import bytecode
# TODO: use tempfile to create a correct tempfile (cross platform compatible)
bytecode.method2png('/tmp/dad/graphs/{}#{}.png'.format(self.cls_name.split('/')[-1][:-1], self.name), methanalysis)
def process(self, doAST:bool=False) -> None:
# TODO: use tempfile to create a correct tempfile (cross platform compatible)
bytecode.method2png(
'/tmp/dad/graphs/{}#{}.png'.format(
self.cls_name.split('/')[-1][:-1], self.name
),
methanalysis,
)
def process(self, doAST: bool = False) -> None:
"""
Processes the method and decompile the code.
@ -182,13 +200,17 @@ class DvMethod:
if not __debug__:
# TODO: use tempfile to create a correct tempfile (cross platform compatible)
util.create_png(self.cls_name, self.name, graph, '/tmp/dad/pre-structured')
util.create_png(
self.cls_name, self.name, graph, '/tmp/dad/pre-structured'
)
identify_structures(graph, graph.immediate_dominators())
if not __debug__:
# TODO: use tempfile to create a correct tempfile (cross platform compatible)
util.create_png(self.cls_name, self.name, graph, '/tmp/dad/structured')
util.create_png(
self.cls_name, self.name, graph, '/tmp/dad/structured'
)
if doAST:
self.ast = JSONWriter(graph, self).get_ast()
@ -243,7 +265,10 @@ class DvClass:
:param androguard.core.dex.ClassDefItem dvclass: the class item
:param androguard.core.analysis.analysis.Analysis vma: an Analysis object
"""
def __init__(self, dvclass: dex.ClassDefItem, vma: analysis.Analysis) -> None:
def __init__(
self, dvclass: dex.ClassDefItem, vma: analysis.Analysis
) -> None:
name = dvclass.get_name()
if name.find('/') > 0:
pckg, name = name.rsplit('/', 1)
@ -277,13 +302,15 @@ class DvClass:
logger.debug('Class : %s', self.name)
logger.debug('Methods added :')
for meth in self.methods:
logger.debug('%s (%s, %s)', meth.get_method_idx(), self.name, meth.name)
logger.debug(
'%s (%s, %s)', meth.get_method_idx(), self.name, meth.name
)
logger.debug('')
def get_methods(self) -> list[dex.EncodedMethod]:
return self.methods
def process_method(self, num: int, doAST:bool=False) -> None:
def process_method(self, num: int, doAST: bool = False) -> None:
method = self.methods[num]
if not isinstance(method, DvMethod):
self.methods[num] = DvMethod(self.vma.get_method(method))
@ -291,13 +318,15 @@ class DvClass:
else:
method.process(doAST=doAST)
def process(self, doAST:bool=False) -> None:
def process(self, doAST: bool = False) -> None:
for i in range(len(self.methods)):
try:
self.process_method(i, doAST=doAST)
except Exception as e:
# FIXME: too broad exception?
logger.warning('Error decompiling method %s: %s', self.methods[i], e)
logger.warning(
'Error decompiling method %s: %s', self.methods[i], e
)
def get_ast(self) -> dict:
fields = [get_field_ast(f) for f in self.fields]
@ -312,7 +341,9 @@ class DvClass:
'super': JSONWriter.parse_descriptor(self.superclass),
'flags': self.access,
'isInterface': isInterface,
'interfaces': list(map(JSONWriter.parse_descriptor, self.interfaces)),
'interfaces': list(
map(JSONWriter.parse_descriptor, self.interfaces)
),
'fields': fields,
'methods': methods,
}
@ -329,7 +360,8 @@ class DvClass:
if len(self.interfaces) > 0:
prototype += ' implements %s' % ', '.join(
[str(n[1:-1].replace('/', '.')) for n in self.interfaces])
[str(n[1:-1].replace('/', '.')) for n in self.interfaces]
)
source.append('%s {\n' % prototype)
for field in self.fields:
@ -345,7 +377,9 @@ class DvClass:
value = init_value.value
if f_type == 'String':
if value:
value = '"%s"' % str(value).encode("unicode-escape").decode("ascii")
value = '"%s"' % str(value).encode(
"unicode-escape"
).decode("ascii")
else:
# FIXME we can not check if this value here is null or ""
# In both cases we end up here...
@ -369,11 +403,19 @@ class DvClass:
source = []
if not self.inner and self.package:
source.append(
('PACKAGE', [('PACKAGE_START', 'package '), (
'NAME_PACKAGE', '%s' % self.package), ('PACKAGE_END', ';\n')
]))
list_proto = [('PROTOTYPE_ACCESS', '%s class ' % ' '.join(self.access)),
('NAME_PROTOTYPE', '%s' % self.name, self.package)]
(
'PACKAGE',
[
('PACKAGE_START', 'package '),
('NAME_PACKAGE', '%s' % self.package),
('PACKAGE_END', ';\n'),
],
)
)
list_proto = [
('PROTOTYPE_ACCESS', '%s class ' % ' '.join(self.access)),
('NAME_PROTOTYPE', '%s' % self.name, self.package),
]
superclass = self.superclass
if superclass is not None and superclass != 'Ljava/lang/Object;':
superclass = superclass[1:-1].replace('/', '.')
@ -386,15 +428,18 @@ class DvClass:
if i != 0:
list_proto.append(('COMMA', ', '))
list_proto.append(
('NAME_INTERFACE', interface[1:-1].replace('/', '.')))
('NAME_INTERFACE', interface[1:-1].replace('/', '.'))
)
list_proto.append(('PROTOTYPE_END', ' {\n'))
source.append(("PROTOTYPE", list_proto))
for field in self.fields:
field_access_flags = field.get_access_flags()
access = [util.ACCESS_FLAGS_FIELDS[flag]
for flag in util.ACCESS_FLAGS_FIELDS
if flag & field_access_flags]
access = [
util.ACCESS_FLAGS_FIELDS[flag]
for flag in util.ACCESS_FLAGS_FIELDS
if flag & field_access_flags
]
f_type = util.get_type(field.get_descriptor())
name = field.get_name()
if access:
@ -408,28 +453,47 @@ class DvClass:
value = init_value.value
if f_type == 'String':
if value:
value = ' = "%s"' % value.encode("unicode-escape").decode("ascii")
value = ' = "%s"' % value.encode(
"unicode-escape"
).decode("ascii")
else:
# FIXME we can not check if this value here is null or ""
# In both cases we end up here...
value = ' = ""'
elif field.proto == 'B':
# a byte
value = ' = %s' % hex(struct.unpack("b", struct.pack("B", value))[0])
value = ' = %s' % hex(
struct.unpack("b", struct.pack("B", value))[0]
)
else:
value = ' = %s' % str(value)
if value:
source.append(
('FIELD', [('FIELD_ACCESS', access_str), (
'FIELD_TYPE', '%s' % f_type), ('SPACE', ' '), (
'NAME_FIELD', '%s' % name, f_type, field), ('FIELD_VALUE', value), ('FIELD_END',
';\n')]))
(
'FIELD',
[
('FIELD_ACCESS', access_str),
('FIELD_TYPE', '%s' % f_type),
('SPACE', ' '),
('NAME_FIELD', '%s' % name, f_type, field),
('FIELD_VALUE', value),
('FIELD_END', ';\n'),
],
)
)
else:
source.append(
('FIELD', [('FIELD_ACCESS', access_str), (
'FIELD_TYPE', '%s' % f_type), ('SPACE', ' '), (
'NAME_FIELD', '%s' % name, f_type, field), ('FIELD_END',
';\n')]))
(
'FIELD',
[
('FIELD_ACCESS', access_str),
('FIELD_TYPE', '%s' % f_type),
('SPACE', ' '),
('NAME_FIELD', '%s' % name, f_type, field),
('FIELD_END', ';\n'),
],
)
)
for method in self.methods:
if isinstance(method, DvMethod):
@ -455,6 +519,7 @@ class DvMachine:
At first, :py:attr:`classes` contains only :class:`~androguard.core.dex.ClassDefItem` as values.
Then these objects are replaced by :class:`DvClass` items successively.
"""
def __init__(self, name: str) -> None:
"""
@ -474,7 +539,10 @@ class DvMachine:
else:
raise ValueError("Format not recognised for filename '%s'" % name)
self.classes = {dvclass.orig_class.get_name(): dvclass.orig_class for dvclass in self.vma.get_classes()}
self.classes = {
dvclass.orig_class.get_name(): dvclass.orig_class
for dvclass in self.vma.get_classes()
}
# TODO why not?
# util.merge_inner(self.classes)

View File

@ -21,18 +21,18 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from androguard.core.analysis.analysis import Analysis, MethodAnalysis
from androguard.core.dex import DEX, ClassDefItem
from androguard.decompiler import decompile
from loguru import logger
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import TerminalFormatter
from pygments.lexers import get_lexer_by_name
from pygments.token import Token
from loguru import logger
from androguard.decompiler import decompile
class DecompilerDAD:
@ -81,7 +81,9 @@ class DecompilerDAD:
c.process(doAST=True)
return c.get_ast()
def get_source_class_ext(self, _class: ClassDefItem) -> list[tuple[str, list]]:
def get_source_class_ext(
self, _class: ClassDefItem
) -> list[tuple[str, list]]:
c = decompile.DvClass(_class, self.vmx)
c.process()

View File

@ -16,12 +16,15 @@
# limitations under the License.
from collections import defaultdict
from androguard.decompiler.basic_blocks import (build_node_from_block,
StatementBlock, CondBlock)
from androguard.decompiler.instruction import Variable
from loguru import logger
from androguard.decompiler.basic_blocks import (
CondBlock,
StatementBlock,
build_node_from_block,
)
from androguard.decompiler.instruction import Variable
# TODO Could use networkx here, as it has plenty of tools already, no need to reengineer the wheel
class Graph:
@ -31,6 +34,7 @@ class Graph:
The CFG defines an entry node :py:attr:`entry`, a single exit node :py:attr:`exit`, a list of nodes
:py:attr:`nodes` and a list of edges :py:attr:`edges`.
"""
def __init__(self):
self.entry = None
self.exit = None
@ -54,8 +58,9 @@ class Graph:
return [n for n in self.reverse_edges.get(node, []) if not n.in_catch]
def all_preds(self, node):
return (self.reverse_edges.get(node, []) + self.reverse_catch_edges.get(
node, []))
return self.reverse_edges.get(node, []) + self.reverse_catch_edges.get(
node, []
)
def add_node(self, node):
"""
@ -152,6 +157,7 @@ class Graph:
Yields the :class`~androguard.decompiler.node.Node`s of the graph in post-order i.e we visit all the
children of a node before visiting the node itself.
"""
def _visit(n, cnt):
visited.add(n)
for suc in self.all_sucs(n):
@ -174,15 +180,18 @@ class Graph:
:param draw_branches:
:return:
"""
from pydot import Dot, Edge
import os
from pydot import Dot, Edge
g = Dot()
g.set_node_defaults(color='lightgray',
style='filled',
shape='box',
fontname='Courier',
fontsize='10')
g.set_node_defaults(
color='lightgray',
style='filled',
shape='box',
fontname='Courier',
fontsize='10',
)
for node in sorted(self.nodes, key=lambda x: x.num):
if draw_branches and node.type.is_cond:
g.add_edge(Edge(str(node), str(node.true), color='green'))
@ -191,10 +200,14 @@ class Graph:
for suc in self.sucs(node):
g.add_edge(Edge(str(node), str(suc), color='blue'))
for except_node in self.catch_edges.get(node, []):
g.add_edge(Edge(str(node),
str(except_node),
color='black',
style='dashed'))
g.add_edge(
Edge(
str(node),
str(except_node),
color='black',
style='dashed',
)
)
g.write(os.path.join(dname, '%s.png' % name), format='png')
@ -295,8 +308,9 @@ def simplify(graph):
continue
suc = sucs[0]
if len(node.get_ins()) == 0:
if any(pred.type.is_switch
for pred in graph.all_preds(node)):
if any(
pred.type.is_switch for pred in graph.all_preds(node)
):
continue
if node is suc:
continue
@ -312,9 +326,12 @@ def simplify(graph):
if node is graph.entry:
graph.entry = suc
graph.remove_node(node)
elif (suc.type.is_stmt and len(graph.all_preds(suc)) == 1 and
not (suc in graph.catch_edges) and not (
(node is suc) or (suc is graph.entry))):
elif (
suc.type.is_stmt
and len(graph.all_preds(suc)) == 1
and not (suc in graph.catch_edges)
and not ((node is suc) or (suc is graph.entry))
):
ins_to_merge = suc.get_ins()
node.add_ins(ins_to_merge)
for var in suc.var_to_declare:
@ -445,8 +462,9 @@ def make_node(graph, block, block_to_node, vmap, gen_ret):
for _type, _, exception_target in block.exception_analysis.exceptions:
exception_node = block_to_node.get(exception_target)
if exception_node is None:
exception_node = build_node_from_block(exception_target, vmap,
gen_ret, _type)
exception_node = build_node_from_block(
exception_target, vmap, gen_ret, _type
)
exception_node.in_catch = True
block_to_node[exception_target] = exception_node
node.set_catch_type(_type)
@ -461,8 +479,9 @@ def make_node(graph, block, block_to_node, vmap, gen_ret):
if node.type.is_switch:
node.add_case(child_node)
if node.type.is_cond:
if_target = ((block.end // 2) -
(block.last_length // 2) + node.off_last_ins)
if_target = (
(block.end // 2) - (block.last_length // 2) + node.off_last_ins
)
child_addr = child_block.start // 2
if if_target == child_addr:
node.true = child_node

View File

@ -340,10 +340,9 @@ class ArrayStoreInstruction(IRForm):
def visit(self, visitor):
v_m = self.var_map
return visitor.visit_astore(v_m[self.array],
v_m[self.index],
v_m[self.rhs],
data=self)
return visitor.visit_astore(
v_m[self.array], v_m[self.index], v_m[self.rhs], data=self
)
def replace_var(self, old, new):
if self.rhs == old:
@ -379,7 +378,9 @@ class ArrayStoreInstruction(IRForm):
def __str__(self):
v_m = self.var_map
return '{}[{}] = {}'.format(v_m[self.array], v_m[self.index], v_m[self.rhs])
return '{}[{}] = {}'.format(
v_m[self.array], v_m[self.index], v_m[self.rhs]
)
class StaticInstruction(IRForm):
@ -404,7 +405,8 @@ class StaticInstruction(IRForm):
def visit(self, visitor):
return visitor.visit_put_static(
self.cls, self.name, self.var_map[self.rhs])
self.cls, self.name, self.var_map[self.rhs]
)
def replace_var(self, old, new):
self.rhs = new.v
@ -454,10 +456,8 @@ class InstanceInstruction(IRForm):
def visit(self, visitor):
v_m = self.var_map
return visitor.visit_put_instance(
v_m[self.lhs],
self.name,
v_m[self.rhs],
data=self.atype)
v_m[self.lhs], self.name, v_m[self.rhs], data=self.atype
)
def replace_var(self, old, new):
if self.lhs == old:
@ -527,7 +527,7 @@ class InvokeInstruction(IRForm):
self.var_map[arg.v] = arg
self.triple = triple
assert (triple[1] == name)
assert triple[1] == name
def get_type(self):
if self.name == '<init>':
@ -593,32 +593,33 @@ class InvokeInstruction(IRForm):
def visit(self, visitor):
v_m = self.var_map
largs = [v_m[arg] for arg in self.args]
return visitor.visit_invoke(self.name, v_m[self.base], self.ptype,
self.rtype, largs, self)
return visitor.visit_invoke(
self.name, v_m[self.base], self.ptype, self.rtype, largs, self
)
def __str__(self):
v_m = self.var_map
return '{}.{}({})'.format(v_m[self.base], self.name,
', '.join('%s' % v_m[i] for i in self.args))
return '{}.{}({})'.format(
v_m[self.base],
self.name,
', '.join('%s' % v_m[i] for i in self.args),
)
class InvokeRangeInstruction(InvokeInstruction):
def __init__(self, clsname, name, rtype, ptype, args, triple):
base = args.pop(0)
super().__init__(clsname, name, base, rtype,
ptype, args, triple)
super().__init__(clsname, name, base, rtype, ptype, args, triple)
class InvokeDirectInstruction(InvokeInstruction):
def __init__(self, clsname, name, base, rtype, ptype, args, triple):
super().__init__(
clsname, name, base, rtype, ptype, args, triple)
super().__init__(clsname, name, base, rtype, ptype, args, triple)
class InvokeStaticInstruction(InvokeInstruction):
def __init__(self, clsname, name, base, rtype, ptype, args, triple):
super().__init__(
clsname, name, base, rtype, ptype, args, triple)
super().__init__(clsname, name, base, rtype, ptype, args, triple)
def get_used_vars(self):
v_m = self.var_map
@ -737,8 +738,9 @@ class CheckCastExpression(IRForm):
return self.var_map[self.arg].get_used_vars()
def visit(self, visitor):
return visitor.visit_check_cast(self.var_map[self.arg],
util.get_type(self.type))
return visitor.visit_check_cast(
self.var_map[self.arg], util.get_type(self.type)
)
def replace_var(self, old, new):
self.arg = new.v
@ -1083,8 +1085,10 @@ class BinaryExpression(IRForm):
def has_side_effect(self):
v_m = self.var_map
return (v_m[self.arg1].has_side_effect() or
v_m[self.arg2].has_side_effect())
return (
v_m[self.arg1].has_side_effect()
or v_m[self.arg2].has_side_effect()
)
def get_used_vars(self):
v_m = self.var_map
@ -1094,8 +1098,9 @@ class BinaryExpression(IRForm):
def visit(self, visitor):
v_m = self.var_map
return visitor.visit_binary_expression(self.op, v_m[self.arg1],
v_m[self.arg2])
return visitor.visit_binary_expression(
self.op, v_m[self.arg1], v_m[self.arg2]
)
def replace_var(self, old, new):
if self.arg1 == old:
@ -1136,8 +1141,9 @@ class BinaryCompExpression(BinaryExpression):
def visit(self, visitor):
v_m = self.var_map
return visitor.visit_cond_expression(self.op, v_m[self.arg1],
v_m[self.arg2])
return visitor.visit_cond_expression(
self.op, v_m[self.arg1], v_m[self.arg2]
)
class BinaryExpression2Addr(BinaryExpression):
@ -1209,7 +1215,14 @@ class CastExpression(UnaryExpression):
return 'CAST_{}({})'.format(self.op, self.var_map[self.arg])
CONDS = {'==': '!=', '!=': '==', '<': '>=', '<=': '>', '>=': '<', '>': '<=', }
CONDS = {
'==': '!=',
'!=': '==',
'<': '>=',
'<=': '>',
'>=': '<',
'>': '<=',
}
class ConditionalExpression(IRForm):
@ -1237,8 +1250,9 @@ class ConditionalExpression(IRForm):
def visit(self, visitor):
v_m = self.var_map
return visitor.visit_cond_expression(self.op, v_m[self.arg1],
v_m[self.arg2])
return visitor.visit_cond_expression(
self.op, v_m[self.arg1], v_m[self.arg2]
)
def replace_var(self, old, new):
if self.arg1 == old:
@ -1270,7 +1284,9 @@ class ConditionalExpression(IRForm):
def __str__(self):
v_m = self.var_map
return 'COND({}, {}, {})'.format(self.op, v_m[self.arg1], v_m[self.arg2])
return 'COND({}, {}, {})'.format(
self.op, v_m[self.arg1], v_m[self.arg2]
)
class ConditionalZExpression(IRForm):
@ -1335,9 +1351,8 @@ class InstanceExpression(IRForm):
def visit(self, visitor):
return visitor.visit_get_instance(
self.var_map[self.arg],
self.name,
data=self.ftype)
self.var_map[self.arg], self.name, data=self.ftype
)
def replace_var(self, old, new):
self.arg = new.v

View File

@ -43,8 +43,11 @@ class MakeProperties(type):
attrs.append(key[4:])
delattr(cls, key)
for attr in attrs:
setattr(cls, attr[1:],
property(_wrap_get(attr), _wrap_set(attrs, attr)))
setattr(
cls,
attr[1:],
property(_wrap_get(attr), _wrap_set(attrs, attr)),
)
cls._attrs = attrs
def __call__(cls, *args, **kwds):
@ -132,8 +135,9 @@ class Interval:
if item in self.content:
return True
# If the interval contains intervals, we need to check them
return any(item in node for node in self.content
if isinstance(node, Interval))
return any(
item in node for node in self.content if isinstance(node, Interval)
)
def add_node(self, node):
if node in self.content:

View File

@ -16,22 +16,51 @@
# limitations under the License.
from struct import pack, unpack
import androguard.decompiler.util as util
from androguard.decompiler.instruction import (
ArrayLengthExpression, ArrayLoadExpression, ArrayStoreInstruction,
AssignExpression, BaseClass, BinaryCompExpression, BinaryExpression,
BinaryExpression2Addr, BinaryExpressionLit, CastExpression,
CheckCastExpression, ConditionalExpression, ConditionalZExpression,
Constant, FillArrayExpression, FilledArrayExpression, InstanceExpression,
InstanceInstruction, InvokeInstruction, InvokeDirectInstruction,
InvokeRangeInstruction, InvokeStaticInstruction, MonitorEnterExpression,
MonitorExitExpression, MoveExceptionExpression, MoveExpression,
MoveResultExpression, NewArrayExpression, NewInstance, NopExpression,
ThrowExpression, Variable, ReturnInstruction, StaticExpression,
StaticInstruction, SwitchExpression, ThisParam, UnaryExpression)
from loguru import logger
import androguard.decompiler.util as util
from androguard.decompiler.instruction import (
ArrayLengthExpression,
ArrayLoadExpression,
ArrayStoreInstruction,
AssignExpression,
BaseClass,
BinaryCompExpression,
BinaryExpression,
BinaryExpression2Addr,
BinaryExpressionLit,
CastExpression,
CheckCastExpression,
ConditionalExpression,
ConditionalZExpression,
Constant,
FillArrayExpression,
FilledArrayExpression,
InstanceExpression,
InstanceInstruction,
InvokeDirectInstruction,
InvokeInstruction,
InvokeRangeInstruction,
InvokeStaticInstruction,
MonitorEnterExpression,
MonitorExitExpression,
MoveExceptionExpression,
MoveExpression,
MoveResultExpression,
NewArrayExpression,
NewInstance,
NopExpression,
ReturnInstruction,
StaticExpression,
StaticInstruction,
SwitchExpression,
ThisParam,
ThrowExpression,
UnaryExpression,
Variable,
)
class Op:
CMP = 'cmp'
@ -93,14 +122,16 @@ def assign_cast_exp(val_a, val_b, val_op, op_type, vmap):
def assign_binary_exp(ins, val_op, op_type, vmap):
reg_a, reg_b, reg_c = get_variables(vmap, ins.AA, ins.BB, ins.CC)
return AssignExpression(reg_a, BinaryExpression(val_op, reg_b, reg_c,
op_type))
return AssignExpression(
reg_a, BinaryExpression(val_op, reg_b, reg_c, op_type)
)
def assign_binary_2addr_exp(ins, val_op, op_type, vmap):
reg_a, reg_b = get_variables(vmap, ins.A, ins.B)
return AssignExpression(reg_a, BinaryExpression2Addr(val_op, reg_a, reg_b,
op_type))
return AssignExpression(
reg_a, BinaryExpression2Addr(val_op, reg_a, reg_b, op_type)
)
def assign_lit(op_type, val_cst, val_a, val_b, vmap):
@ -111,6 +142,7 @@ def assign_lit(op_type, val_cst, val_a, val_b, vmap):
## From here on, there are all defined instructions
# nop
def nop(ins, vmap):
return NopExpression()
@ -300,9 +332,11 @@ def conststringjumbo(ins, vmap):
# const-class vAA, type@BBBB ( 8b )
def constclass(ins, vmap):
logger.debug('ConstClass : %s', ins.get_output())
cst = Constant(util.get_type(ins.get_string()),
'Ljava/lang/Class;',
descriptor=ins.get_string())
cst = Constant(
util.get_type(ins.get_string()),
'Ljava/lang/Class;',
descriptor=ins.get_string(),
)
return assign_const(ins.AA, cst, vmap)
@ -324,9 +358,9 @@ def checkcast(ins, vmap):
logger.debug('CheckCast: %s', ins.get_output())
cast_type = util.get_type(ins.get_translated_kind())
cast_var = get_variables(vmap, ins.AA)
cast_expr = CheckCastExpression(cast_var,
cast_type,
descriptor=ins.get_translated_kind())
cast_expr = CheckCastExpression(
cast_var, cast_type, descriptor=ins.get_translated_kind()
)
return AssignExpression(cast_var, cast_expr)
@ -334,8 +368,10 @@ def checkcast(ins, vmap):
def instanceof(ins, vmap):
logger.debug('InstanceOf : %s', ins.get_output())
reg_a, reg_b = get_variables(vmap, ins.A, ins.B)
reg_c = BaseClass(util.get_type(ins.get_translated_kind()),
descriptor=ins.get_translated_kind())
reg_c = BaseClass(
util.get_type(ins.get_translated_kind()),
descriptor=ins.get_translated_kind(),
)
exp = BinaryExpression('instanceof', reg_b, reg_c, 'Z')
return AssignExpression(reg_a, exp)
@ -368,7 +404,7 @@ def fillednewarray(ins, vmap, ret):
logger.debug('FilledNewArray : %s', ins.get_output())
c, d, e, f, g = get_variables(vmap, ins.C, ins.D, ins.E, ins.F, ins.G)
array_type = ins.cm.get_type(ins.BBBB)
exp = FilledArrayExpression(ins.A, array_type, [c, d, e, f, g][:ins.A])
exp = FilledArrayExpression(ins.A, array_type, [c, d, e, f, g][: ins.A])
return AssignExpression(ret, exp)
@ -887,8 +923,9 @@ def invokevirtual(ins, vmap, ret):
args = get_args(vmap, param_type, largs)
c = get_variables(vmap, ins.C)
returned = None if ret_type == 'V' else ret.new()
exp = InvokeInstruction(cls_name, name, c, ret_type, param_type, args,
method.get_triple())
exp = InvokeInstruction(
cls_name, name, c, ret_type, param_type, args, method.get_triple()
)
return AssignExpression(returned, exp)
@ -904,8 +941,15 @@ def invokesuper(ins, vmap, ret):
args = get_args(vmap, param_type, largs)
superclass = BaseClass('super')
returned = None if ret_type == 'V' else ret.new()
exp = InvokeInstruction(cls_name, name, superclass, ret_type, param_type,
args, method.get_triple())
exp = InvokeInstruction(
cls_name,
name,
superclass,
ret_type,
param_type,
args,
method.get_triple(),
)
return AssignExpression(returned, exp)
@ -928,8 +972,9 @@ def invokedirect(ins, vmap, ret):
ret.set_to(base)
else:
returned = ret.new()
exp = InvokeDirectInstruction(cls_name, name, base, ret_type, param_type,
args, method.get_triple())
exp = InvokeDirectInstruction(
cls_name, name, base, ret_type, param_type, args, method.get_triple()
)
return AssignExpression(returned, exp)
@ -945,8 +990,9 @@ def invokestatic(ins, vmap, ret):
args = get_args(vmap, param_type, largs)
base = BaseClass(cls_name, descriptor=method.get_class_name())
returned = None if ret_type == 'V' else ret.new()
exp = InvokeStaticInstruction(cls_name, name, base, ret_type, param_type,
args, method.get_triple())
exp = InvokeStaticInstruction(
cls_name, name, base, ret_type, param_type, args, method.get_triple()
)
return AssignExpression(returned, exp)
@ -962,8 +1008,9 @@ def invokeinterface(ins, vmap, ret):
args = get_args(vmap, param_type, largs)
c = get_variables(vmap, ins.C)
returned = None if ret_type == 'V' else ret.new()
exp = InvokeInstruction(cls_name, name, c, ret_type, param_type, args,
method.get_triple())
exp = InvokeInstruction(
cls_name, name, c, ret_type, param_type, args, method.get_triple()
)
return AssignExpression(returned, exp)
@ -979,8 +1026,14 @@ def invokevirtualrange(ins, vmap, ret):
this_arg = get_variables(vmap, largs[0])
args = get_args(vmap, param_type, largs[1:])
returned = None if ret_type == 'V' else ret.new()
exp = InvokeRangeInstruction(cls_name, name, ret_type, param_type,
[this_arg] + args, method.get_triple())
exp = InvokeRangeInstruction(
cls_name,
name,
ret_type,
param_type,
[this_arg] + args,
method.get_triple(),
)
return AssignExpression(returned, exp)
@ -1001,8 +1054,14 @@ def invokesuperrange(ins, vmap, ret):
returned = base
ret.set_to(base)
superclass = BaseClass('super')
exp = InvokeRangeInstruction(cls_name, name, ret_type, param_type,
[superclass] + args, method.get_triple())
exp = InvokeRangeInstruction(
cls_name,
name,
ret_type,
param_type,
[superclass] + args,
method.get_triple(),
)
return AssignExpression(returned, exp)
@ -1023,8 +1082,14 @@ def invokedirectrange(ins, vmap, ret):
else:
returned = base
ret.set_to(base)
exp = InvokeRangeInstruction(cls_name, name, ret_type, param_type,
[this_arg] + args, method.get_triple())
exp = InvokeRangeInstruction(
cls_name,
name,
ret_type,
param_type,
[this_arg] + args,
method.get_triple(),
)
return AssignExpression(returned, exp)
@ -1040,8 +1105,9 @@ def invokestaticrange(ins, vmap, ret):
args = get_args(vmap, param_type, largs)
base = BaseClass(cls_name, descriptor=method.get_class_name())
returned = None if ret_type == 'V' else ret.new()
exp = InvokeStaticInstruction(cls_name, name, base, ret_type, param_type,
args, method.get_triple())
exp = InvokeStaticInstruction(
cls_name, name, base, ret_type, param_type, args, method.get_triple()
)
return AssignExpression(returned, exp)
@ -1057,8 +1123,14 @@ def invokeinterfacerange(ins, vmap, ret):
base_arg = get_variables(vmap, largs[0])
args = get_args(vmap, param_type, largs[1:])
returned = None if ret_type == 'V' else ret.new()
exp = InvokeRangeInstruction(cls_name, name, ret_type, param_type,
[base_arg] + args, method.get_triple())
exp = InvokeRangeInstruction(
cls_name,
name,
ret_type,
param_type,
[base_arg] + args,
method.get_triple(),
)
return AssignExpression(returned, exp)

View File

@ -19,7 +19,9 @@
# in Python >= 3.7
# see https://peps.python.org/pep-0563/
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from androguard.decompiler.graph import Graph
@ -79,28 +81,54 @@ ACCESS_FLAGS_METHODS = {
0x20000: 'declared_synchronized',
}
ACCESS_ORDER = [0x1, 0x4, 0x2, 0x400, 0x8, 0x10, 0x80, 0x40, 0x20, 0x100, 0x800,
0x200, 0x1000, 0x2000, 0x4000, 0x10000, 0x20000]
ACCESS_ORDER = [
0x1,
0x4,
0x2,
0x400,
0x8,
0x10,
0x80,
0x40,
0x20,
0x100,
0x800,
0x200,
0x1000,
0x2000,
0x4000,
0x10000,
0x20000,
]
TYPE_LEN = {'J': 2, 'D': 2, }
TYPE_LEN = {
'J': 2,
'D': 2,
}
def get_access_class(access: int) -> list[str]:
sorted_access = [i for i in ACCESS_ORDER if i & access]
return [ACCESS_FLAGS_CLASSES.get(flag, 'unkn_%d' % flag)
for flag in sorted_access]
return [
ACCESS_FLAGS_CLASSES.get(flag, 'unkn_%d' % flag)
for flag in sorted_access
]
def get_access_method(access: int) -> list[str]:
sorted_access = [i for i in ACCESS_ORDER if i & access]
return [ACCESS_FLAGS_METHODS.get(flag, 'unkn_%d' % flag)
for flag in sorted_access]
return [
ACCESS_FLAGS_METHODS.get(flag, 'unkn_%d' % flag)
for flag in sorted_access
]
def get_access_field(access: int) -> list[str]:
sorted_access = [i for i in ACCESS_ORDER if i & access]
return [ACCESS_FLAGS_FIELDS.get(flag, 'unkn_%d' % flag)
for flag in sorted_access]
return [
ACCESS_FLAGS_FIELDS.get(flag, 'unkn_%d' % flag)
for flag in sorted_access
]
def build_path(graph, node1, node2, path=None):
@ -151,8 +179,9 @@ def merge_inner(clsdict):
innerclass = innerclass[:-1] # remove ';' of the name
mainclass += ';'
if mainclass in clsdict:
clsdict[mainclass].add_subclass(innerclass,
clsdict[classname])
clsdict[mainclass].add_subclass(
innerclass, clsdict[classname]
)
clsdict[classname].name = innerclass
done[classname] = clsdict[classname]
del clsdict[classname]
@ -173,7 +202,7 @@ def get_type_size(param):
return TYPE_LEN.get(param, 1)
def get_type(atype:str, size:int=None) -> str:
def get_type(atype: str, size: int = None) -> str:
"""
Retrieve the java type of a descriptor (e.g : I)
"""
@ -205,7 +234,9 @@ def get_params_type(descriptor: str) -> list[str]:
return []
def create_png(cls_name: str, meth_name: str, graph: Graph, dir_name:str='graphs2') -> None:
def create_png(
cls_name: str, meth_name: str, graph: Graph, dir_name: str = 'graphs2'
) -> None:
"""
Creates a PNG from a given :class:`~androguard.decompiler.graph.Graph`.

View File

@ -16,21 +16,30 @@
# limitations under the License.
from struct import unpack
from androguard.core import mutf8
from androguard.decompiler.util import get_type
from androguard.decompiler.opcode_ins import Op
from androguard.decompiler.instruction import (
Constant, ThisParam, BinaryExpression, BaseClass, InstanceExpression,
NewInstance, Variable, BinaryCompExpression)
from loguru import logger
from androguard.core import mutf8
from androguard.decompiler.instruction import (
BaseClass,
BinaryCompExpression,
BinaryExpression,
Constant,
InstanceExpression,
NewInstance,
ThisParam,
Variable,
)
from androguard.decompiler.opcode_ins import Op
from androguard.decompiler.util import get_type
class Writer:
"""
Transforms a method into Java code.
"""
def __init__(self, graph, method):
self.graph = graph
self.method = method
@ -54,10 +63,10 @@ class Writer:
return self.buffer2
def inc_ind(self, i=1):
self.ind += (4 * i)
self.ind += 4 * i
def dec_ind(self, i=1):
self.ind -= (4 * i)
self.ind -= 4 * i
def space(self):
if self.skip:
@ -104,14 +113,16 @@ class Writer:
# TODO: prefer this class as write_ind_visit_end that should be deprecated
# at the end
def write_ind_visit_end_ext(self,
lhs,
before,
s,
after,
rhs=None,
data=None,
subsection='UNKNOWN_SUBSECTION'):
def write_ind_visit_end_ext(
self,
lhs,
before,
s,
after,
rhs=None,
data=None,
subsection='UNKNOWN_SUBSECTION',
):
self.write_ind()
lhs.visit(self)
self.write(before + s + after)
@ -125,14 +136,15 @@ class Writer:
def write_inplace_if_possible(self, lhs, rhs):
if isinstance(rhs, BinaryExpression) and lhs == rhs.var_map[rhs.arg1]:
exp_rhs = rhs.var_map[rhs.arg2]
if rhs.op in '+-' and isinstance(exp_rhs, Constant) and \
exp_rhs.get_int_value() == 1:
if (
rhs.op in '+-'
and isinstance(exp_rhs, Constant)
and exp_rhs.get_int_value() == 1
):
return self.write_ind_visit_end(lhs, rhs.op * 2, data=rhs)
return self.write_ind_visit_end(
lhs,
' %s= ' % rhs.op,
exp_rhs,
data=rhs)
lhs, ' %s= ' % rhs.op, exp_rhs, data=rhs
)
return self.write_ind_visit_end(lhs, ' = ', rhs, data=rhs)
def visit_ins(self, ins):
@ -157,20 +169,28 @@ class Writer:
self.write(name)
self.write_ext(('NAME_METHOD_PROTOTYPE', '%s' % name, self.method))
else:
self.write('{} {}'.format(get_type(self.method.type), self.method.name))
self.write(
'{} {}'.format(get_type(self.method.type), self.method.name)
)
self.write_ext(
('PROTOTYPE_TYPE', '%s' % get_type(self.method.type)))
('PROTOTYPE_TYPE', '%s' % get_type(self.method.type))
)
self.write_ext(('SPACE', ' '))
self.write_ext(
('NAME_METHOD_PROTOTYPE', '%s' % self.method.name, self.method))
('NAME_METHOD_PROTOTYPE', '%s' % self.method.name, self.method)
)
params = self.method.lparams
if 'static' not in access:
params = params[1:]
proto = ''
self.write_ext(('PARENTHESIS_START', '('))
if self.method.params_type:
proto = ', '.join(['{} p{}'.format(get_type(p_type), param) for p_type,
param in zip(self.method.params_type, params)])
proto = ', '.join(
[
'{} p{}'.format(get_type(p_type), param)
for p_type, param in zip(self.method.params_type, params)
]
)
first = True
for p_type, param in zip(self.method.params_type, params):
if not first:
@ -179,7 +199,9 @@ class Writer:
first = False
self.write_ext(('ARG_TYPE', '%s' % get_type(p_type)))
self.write_ext(('SPACE', ' '))
self.write_ext(('NAME_ARG', 'p%s' % param, p_type, self.method))
self.write_ext(
('NAME_ARG', 'p%s' % param, p_type, self.method)
)
self.write_ext(('PARENTHESIS_END', ')'))
self.write('(%s)' % proto)
if self.graph is None:
@ -195,9 +217,13 @@ class Writer:
self.write_ext(('METHOD_END', '%s}\n' % self.space()))
def visit_node(self, node):
if node in (self.if_follow[-1], self.switch_follow[-1],
self.loop_follow[-1], self.latch_node[-1],
self.try_follow[-1]):
if node in (
self.if_follow[-1],
self.switch_follow[-1],
self.loop_follow[-1],
self.latch_node[-1],
self.try_follow[-1],
):
return
if not node.type.is_return and node in self.visited_nodes:
return
@ -257,12 +283,17 @@ class Writer:
def visit_cond_node(self, cond):
follow = cond.follow['if']
if cond.false is cond.true:
self.write('%s// Both branches of the condition point to the same'
' code.\n' % self.space())
self.write(
'%s// Both branches of the condition point to the same'
' code.\n' % self.space()
)
self.write_ext(
('COMMENT_ERROR_MSG',
'%s// Both branches of the condition point to the same'
' code.\n' % self.space()))
(
'COMMENT_ERROR_MSG',
'%s// Both branches of the condition point to the same'
' code.\n' % self.space(),
)
)
self.write('%s// if (' % self.space())
self.write_ext(('COMMENT_IF', '%s// if (' % self.space()))
cond.visit_cond(self)
@ -286,8 +317,10 @@ class Writer:
self.write('%s}\n' % self.space(), data="IF_END_2")
self.visit_node(cond.false)
elif follow is not None:
if cond.true in (follow, self.next_case) or \
cond.num > cond.true.num:
if (
cond.true in (follow, self.next_case)
or cond.num > cond.true.num
):
# or cond.true.num > cond.false.num:
cond.neg()
cond.true, cond.false = cond.false, cond.true
@ -348,8 +381,8 @@ class Writer:
self.inc_ind()
for case in switch.node_to_case[node]:
self.write(
'%scase %d:\n' % (self.space(), case),
data="CASE_XX")
'%scase %d:\n' % (self.space(), case), data="CASE_XX"
)
if i + 1 < len(cases):
self.next_case = cases[i + 1]
else:
@ -419,16 +452,18 @@ class Writer:
def visit_decl(self, var):
if not var.declared:
var_type = var.get_type() or 'unknownType'
self.write('{}{} v{}'.format(
self.space(), get_type(var_type), var.name),
data="DECLARATION")
self.write(
'{}{} v{}'.format(self.space(), get_type(var_type), var.name),
data="DECLARATION",
)
self.end_ins()
def visit_constant(self, cst):
if isinstance(cst, str):
return self.write(string(cst), data="CONSTANT_STRING")
self.write('%r' % cst,
data="CONSTANT_INTEGER") # INTEGER or also others?
self.write(
'%r' % cst, data="CONSTANT_INTEGER"
) # INTEGER or also others?
def visit_base_class(self, cls, data=None):
self.write(cls)
@ -439,7 +474,8 @@ class Writer:
if not var.declared:
self.write('%s ' % get_type(var_type))
self.write_ext(
('VARIABLE_TYPE', '%s' % get_type(var_type), var_type))
('VARIABLE_TYPE', '%s' % get_type(var_type), var_type)
)
self.write_ext(('SPACE', ' '))
var.declared = True
self.write('v%s' % var.name)
@ -493,13 +529,15 @@ class Writer:
' = ',
rhs,
data=data,
subsection='NAME_CLASS_ASSIGNMENT')
subsection='NAME_CLASS_ASSIGNMENT',
)
def visit_new(self, atype, data=None):
self.write('new %s' % get_type(atype))
self.write_ext(('NEW', 'new '))
self.write_ext(
('NAME_CLASS_NEW', '%s' % get_type(atype), data.type, data))
('NAME_CLASS_NEW', '%s' % get_type(atype), data.type, data)
)
def visit_invoke(self, name, base, ptype, rtype, args, invokeInstr):
if isinstance(base, ThisParam):
@ -507,7 +545,10 @@ class Writer:
if self.constructor and len(args) == 0:
self.skip = True
return
if invokeInstr and base.type[1:-1].replace('/', '.') != invokeInstr.cls:
if (
invokeInstr
and base.type[1:-1].replace('/', '.') != invokeInstr.cls
):
base.super = True
base.visit(self)
if name != '<init>':
@ -522,8 +563,9 @@ class Writer:
if isinstance(base2base, NewInstance):
call_name = "{} -> {}".format(base2base.type, name)
break
elif (hasattr(base2base, "base") and
hasattr(base2base, "var_map")):
elif hasattr(base2base, "base") and hasattr(
base2base, "var_map"
):
continue
else:
call_name = "UNKNOWN_TODO"
@ -537,8 +579,16 @@ class Writer:
self.write('.%s' % name)
self.write_ext(('INVOKE', '.'))
self.write_ext(
('NAME_METHOD_INVOKE', '%s' % name, call_name, ptype, rtype,
base, invokeInstr))
(
'NAME_METHOD_INVOKE',
'%s' % name,
call_name,
ptype,
rtype,
base,
invokeInstr,
)
)
self.write('(', data="PARAM_START")
comma = False
for arg in args:
@ -608,12 +658,16 @@ class Writer:
elem_id = data_types[elem_size]
else:
# FIXME for other types we just assume bytes...
logger.warning("Unknown element size {} for array. Assume bytes.".format(elem_size))
logger.warning(
"Unknown element size {} for array. Assume bytes.".format(
elem_size
)
)
elem_id = 'b'
elem_size = 1
for i in range(0, value.size*elem_size, elem_size):
tab.append('%s' % unpack(elem_id, data[i:i+elem_size])[0])
for i in range(0, value.size * elem_size, elem_size):
tab.append('%s' % unpack(elem_id, data[i : i + elem_size])[0])
self.write(', '.join(tab), data="COMMA")
self.write('}', data="ARRAY_FILLED_END")
self.end_ins()
@ -622,10 +676,13 @@ class Writer:
var.declared = True
var_type = var.get_type() or 'unknownType'
self.write('{} v{}'.format(get_type(var_type), var.name))
self.write_ext(('EXCEPTION_TYPE', '%s' % get_type(var_type), data.type))
self.write_ext(
('EXCEPTION_TYPE', '%s' % get_type(var_type), data.type)
)
self.write_ext(('SPACE', ' '))
self.write_ext(
('NAME_CLASS_EXCEPTION', 'v%s' % var.value(), data.type, data))
('NAME_CLASS_EXCEPTION', 'v%s' % var.value(), data.type, data)
)
def visit_monitor_enter(self, ref):
self.write_ind()
@ -718,8 +775,8 @@ def string(s):
i = ord(c)
ret.append('\\u')
ret.append('%x' % (i >> 12))
ret.append('%x' % ((i >> 8) & 0x0f))
ret.append('%x' % ((i >> 4) & 0x0f))
ret.append('%x' % (i & 0x0f))
ret.append('%x' % ((i >> 8) & 0x0F))
ret.append('%x' % ((i >> 4) & 0x0F))
ret.append('%x' % (i & 0x0F))
ret.append('"')
return ''.join(ret)

View File

@ -2,6 +2,7 @@
# in Python >= 3.7
# see https://peps.python.org/pep-0563/
from __future__ import annotations
import hashlib
import os
import re
@ -9,11 +10,11 @@ from typing import Union
from loguru import logger
from androguard.session import Session
from androguard.decompiler import decompiler
from androguard.core import androconf
from androguard.core import apk, dex
from androguard.core import androconf, apk, dex
from androguard.core.analysis.analysis import Analysis
from androguard.decompiler import decompiler
from androguard.session import Session
def get_default_session() -> Session:
"""
@ -27,7 +28,11 @@ def get_default_session() -> Session:
return androconf.CONF["SESSION"]
def AnalyzeAPK(_file: Union[str,bytes], session:Union[Session,None]=None, raw:bool=False) -> tuple[apk.APK, list[dex.DEX], Analysis]:
def AnalyzeAPK(
_file: Union[str, bytes],
session: Union[Session, None] = None,
raw: bool = False,
) -> tuple[apk.APK, list[dex.DEX], Analysis]:
"""
Analyze an android application and setup all stuff for a more quickly
analysis!
@ -76,7 +81,9 @@ def AnalyzeAPK(_file: Union[str,bytes], session:Union[Session,None]=None, raw:bo
return a, d, dx
def AnalyzeDex(filename: str, session:Session=None, raw:bool=False) -> tuple[str, dex.DEX, Analysis]:
def AnalyzeDex(
filename: str, session: Session = None, raw: bool = False
) -> tuple[str, dex.DEX, Analysis]:
"""
Analyze an android dex file and setup all stuff for a more quickly analysis !
@ -126,7 +133,12 @@ def AnalyzeDex(filename: str, session:Session=None, raw:bool=False) -> tuple[str
# return session.addDEY(filename, data) # <- this function is missing
def clean_file_name(filename: str, unique:bool=True, replace:str="_", force_nt:bool=False) -> str:
def clean_file_name(
filename: str,
unique: bool = True,
replace: str = "_",
force_nt: bool = False,
) -> str:
"""
Return a filename version, which has no characters in it which are forbidden.
On Windows these are for example <, /, ?, ...
@ -167,7 +179,7 @@ def clean_file_name(filename: str, unique:bool=True, replace:str="_", force_nt:b
if len(fname) > PATH_MAX_LENGTH:
if "." in fname:
f, ext = fname.rsplit(".", 1)
fname = "{}.{}".format(f[:PATH_MAX_LENGTH-(len(ext)+1)], ext)
fname = "{}.{}".format(f[: PATH_MAX_LENGTH - (len(ext) + 1)], ext)
else:
fname = fname[:PATH_MAX_LENGTH]
@ -175,7 +187,7 @@ def clean_file_name(filename: str, unique:bool=True, replace:str="_", force_nt:b
# Special behaviour... On Windows, there is also a problem with the maximum path length in explorer.exe
# maximum length is limited to 260 chars, so use 250 to have room for other stuff
if len(os.path.abspath(os.path.join(path, fname))) > 250:
fname = fname[:250 - (len(os.path.abspath(path)) + 1)]
fname = fname[: 250 - (len(os.path.abspath(path)) + 1)]
if unique:
counter = 0
@ -190,5 +202,3 @@ def clean_file_name(filename: str, unique:bool=True, replace:str="_", force_nt:b
counter += 1
return os.path.join(path, fname)

View File

@ -1,43 +1,53 @@
from . import adb
import frida
import glob
import hashlib
import json
import threading
import glob
import os
import queue
import threading
import frida
from . import adb
md5 = lambda bs: hashlib.md5(bs).hexdigest()
from loguru import logger
class Message:
pass
class MessageEvent(Message):
def __init__(self, index, function_callee, function_call, params, ret_value):
def __init__(
self, index, function_callee, function_call, params, ret_value
):
self.index = index
self.from_method = function_call
self.to_method = function_callee
self.params = params
self.ret_value = ret_value
class MessageSystem(Message):
def __init__(self, index, function_callee, function_call, params, information):
def __init__(
self, index, function_callee, function_call, params, information
):
self.index = index
self.from_method = function_call
self.to_method = function_callee
self.params = params
self.ret_value = information
class Pentest:
def __init__(self):
self.device: frida.core.Device
self.frida_session: frida.core.Session | None
self.ag_session = None
self.package_name = None
self.device = None
self.frida_session = None
self.pid = -1
self.pid: int = -1
self.detached = False
self.scripts = []
self.pending = []
@ -62,14 +72,12 @@ class Pentest:
self.frida_session.detach()
self.package_name = None
self.device = None
self.frida_session = None
self.pid = -1
self.scripts = []
self.pending = []
self.list_file_scripts = []
def print_devices(self):
logger.info("List of devices")
devices = frida.enumerate_devices()
@ -91,15 +99,22 @@ class Pentest:
return data_scripts
def read_scripts(self, scripts):
return "Java.perform(function () {\n" + self._read_scripts(self.ag_scripts + scripts) + "\n" + "});"
return (
"Java.perform(function () {\n"
+ self._read_scripts(self.ag_scripts + scripts)
+ "\n"
+ "});"
)
def install_apk(self, filename):
adb.adb(self.device.id, "install {}".format(filename))
def attach_package(self, package_name, list_file_scripts, pid=None):
def attach_package(self, package_name, list_file_scripts):
self.package_name = package_name
logger.info("Starting package {} {} {}".format(package_name, list_file_scripts, pid))
logger.info(
"Starting package {} {}".format(package_name, list_file_scripts)
)
self.list_file_scripts = list_file_scripts
@ -116,11 +131,9 @@ class Pentest:
logger.info('Enabled spawn gating')
try:
# It is not an existing process, spawn a new one
if not pid:
pid = self.device.spawn([package_name])
logger.info('Trying to spawn {}'.format(package_name))
self.pid = pid
self.pid = self.device.spawn([package_name])
self.frida_session = self.device.attach(self.pid)
self.load_scripts(self.frida_session, list_file_scripts)
@ -128,8 +141,6 @@ class Pentest:
except frida.NotSupportedError as e:
logger.error(e)
def load_scripts(self, current_session, scripts):
try:
logger.info('Loading scripts {}'.format(scripts))
@ -142,11 +153,11 @@ class Pentest:
logger.error(e)
def run_frida(self):
logger.info("Running frida ! Resuming the PID {}".format(self.pid))
logger.info("Running Frida ! Resuming the PID {}".format(self.pid))
self.device.resume(self.pid)
def androguard_message_handler(self, message, payload):
# use for system event
# use for system event
previous_stacktrace = None
logger.debug("MESSAGE {} {}".format(message, payload))
@ -164,15 +175,41 @@ class Pentest:
function_callee = msg_payload["stacktrace"][1]
ret_value = json.dumps(msg_payload.get("ret"))
logger.info("{} - [{}:{}] [{}] -> [{}]".format(msg_payload["timestamp"], function_call, function_callee, params, ret_value))
self.message_queue.put(MessageEvent(self.idx, function_call, function_callee, params, ret_value))
self.ag_session.insert_event(call=function_call, callee=function_callee, params=params, ret=ret_value)
logger.info(
"{} - [{}:{}] [{}] -> [{}]".format(
msg_payload["timestamp"],
function_call,
function_callee,
params,
ret_value,
)
)
self.message_queue.put(
MessageEvent(
self.idx,
function_call,
function_callee,
params,
ret_value,
)
)
self.ag_session.insert_event(
call=function_call,
callee=function_callee,
params=params,
ret=ret_value,
)
previous_stacktrace = msg_payload["stacktrace"]
self.idx += 1
elif msg_payload["id"] == "AG-SYSTEM":
for i in msg_payload:
if i not in ["id", "information", "timestamp", "stacktrace"]:
if i not in [
"id",
"information",
"timestamp",
"stacktrace",
]:
params[i] = msg_payload[i]
function_call = None
@ -185,9 +222,30 @@ class Pentest:
else:
function_callee = information
logger.warning("{} - [{}:{}] [{}] -> [{}]".format(msg_payload["timestamp"], function_call, function_callee, information, params))
self.message_queue.put(MessageSystem(self.idx, function_call, function_callee, params, information))
self.ag_session.insert_system_event(call=function_call, callee=function_callee, params=params, information=information)
logger.warning(
"{} - [{}:{}] [{}] -> [{}]".format(
msg_payload["timestamp"],
function_call,
function_callee,
information,
params,
)
)
self.message_queue.put(
MessageSystem(
self.idx,
function_call,
function_callee,
params,
information,
)
)
self.ag_session.insert_system_event(
call=function_call,
callee=function_callee,
params=params,
information=information,
)
if not previous_stacktrace:
self.idx += 1
@ -207,7 +265,12 @@ class Pentest:
bs = api.memorydump(info['addr'], info['size'])
md = md5(bs)
if md in mds:
logger.warning("[DEXDump]: Skip duplicate dex {}<{}>".format(info['addr'], md), fg="blue")
logger.warning(
"[DEXDump]: Skip duplicate dex {}<{}>".format(
info['addr'], md
),
fg="blue",
)
continue
mds.append(md)
@ -215,11 +278,20 @@ class Pentest:
os.mkdir("./" + package_name + "/")
if bs[:4] != "dex\n":
bs = b"dex\n035\x00" + bs[8:]
readable_hash = hashlib.sha256(bs).hexdigest();
with open(package_name + "/" + readable_hash + ".dex", 'wb') as out:
readable_hash = hashlib.sha256(bs).hexdigest()
with open(
package_name + "/" + readable_hash + ".dex", 'wb'
) as out:
out.write(bs)
logger.info("[DEXDump]: DexSize={}, SavePath={}/{}/{}.dex"
.format(hex(info['size']), os.getcwd(), package_name, readable_hash), fg='green')
logger.info(
"[DEXDump]: DexSize={}, SavePath={}/{}/{}.dex".format(
hex(info['size']),
os.getcwd(),
package_name,
readable_hash,
),
fg='green',
)
except Exception as e:
logger.error("[Except] - {}: {}".format(e, info), bg='yellow')
@ -228,22 +300,22 @@ class Pentest:
self.detached = True
def on_spawned(self, spawn):
#logger.info('on_spawned: {}'.format(spawn))
# logger.info('on_spawned: {}'.format(spawn))
self.pending.append(spawn)
self.event.set()
def spawn_added(self, spawn):
#logger.info('spawn_added: {}'.format(spawn))
# logger.info('spawn_added: {}'.format(spawn))
self.event.set()
if(spawn.identifier.startswith(self.package_name)):
#logger.info('added tace: {}'.format(spawn))
if spawn.identifier.startswith(self.package_name):
# logger.info('added tace: {}'.format(spawn))
session = self.device.attach(spawn.pid)
self.load_scripts(session, self.list_file_scripts)
self.device.resume(spawn.pid)
#logger.info('Resumed')
# logger.info('Resumed')
def spawn_removed(self, spawn):
logger.info('spawn_removed: {}'.format(spawn))
@ -252,7 +324,15 @@ class Pentest:
def child_added(self, spawn):
logger.info('child_added: {}'.format(spawn))
def start_trace(self, filename, session, list_modules, live=False, noapk=False, dump=False):
def start_trace(
self,
filename,
session,
list_modules,
live=False,
noapk=False,
dump=False,
):
self.ag_session = session
logger.info("Start to trace {} {}".format(filename, list_modules))
@ -267,7 +347,7 @@ class Pentest:
logger.error("Not connected to any device yet")
return
list_scripts_to_load = []
list_scripts_to_load = []
for new_module in list_modules:
if '*' in new_module:
for i_module in glob.iglob(new_module, recursive=True):
@ -284,11 +364,8 @@ class Pentest:
package_name = apk_obj.get_package()
else:
package_name = filename
pid_value = os.popen("adb -s {} shell pidof {}".format(self.device.id, package_name)).read().strip()
if pid_value:
pid = int(pid_value)
self.attach_package(package_name, list_scripts_to_load, pid)
self.attach_package(package_name, list_scripts_to_load)
if not dump:
self.run_frida()

View File

@ -2,6 +2,7 @@ import subprocess
from loguru import logger
def adb(device_id, cmd=None):
logger.info("ADB: Running on {}:{}".format(device_id, cmd))
subprocess.run('adb -s {} {}'.format(device_id, cmd), shell=True)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,14 @@
import collections
import hashlib
from typing import Union, Iterator
from androguard.core.analysis.analysis import Analysis, StringAnalysis
from androguard.core import dex, apk
from androguard.decompiler.decompiler import DecompilerDAD
from androguard.core import androconf
from typing import Iterator, Union
import dataset
from loguru import logger
from androguard.core import androconf, apk, dex
from androguard.core.analysis.analysis import Analysis, StringAnalysis
from androguard.decompiler.decompiler import DecompilerDAD
class Session:
"""
@ -23,7 +22,11 @@ class Session:
>>> 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.

View File

@ -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):
"""
@ -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] = []

View File

@ -1,13 +1,13 @@
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:
@ -15,7 +15,7 @@ class DisplayTransaction:
@property
def unsupported_call(self) -> bool:
return '' #self.block.unsupported_call
return '' # self.block.unsupported_call
@property
def to_method(self) -> str:
@ -34,8 +34,8 @@ 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:
@ -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:
@ -89,4 +92,3 @@ class DisplayTransaction:
type_name = "unknown" # Should be impossible
return type_name

View File

@ -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)

View File

@ -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()

View File

@ -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,
),
)

View File

@ -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,14 +55,18 @@ 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:

View File

@ -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

View File

@ -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),

View File

@ -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(

View File

@ -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

View File

@ -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,14 +147,13 @@ 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:

View File

@ -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':

View File

@ -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',
],
)

View File

@ -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:
# <class name> <method name> <bytecode as hex string>
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("<I", f.read(4))
(string_data_off,) = unpack("<I", f.read(4))
f.seek(string_data_off)
utf16_size = readuleb128(cm, f)
@ -105,7 +132,7 @@ class read_dex:
self.types = dict()
for i in range(type_ids_size):
f.seek(type_ids_off + i * 4)
descriptor_idx, = unpack("<I", f.read(4))
(descriptor_idx,) = unpack("<I", f.read(4))
self.types[i] = descriptor_idx
method_ids = {}
@ -113,7 +140,10 @@ class read_dex:
for i in range(method_ids_size):
f.seek(method_ids_off + i * 8)
class_idx, proto_idx, name_idx = unpack("<HHI", f.read(8))
method_ids[i] = [strings[self.types[class_idx]], strings[name_idx]]
method_ids[i] = [
strings[self.types[class_idx]],
strings[name_idx],
]
# Now parse the found methods and print to stdout
mres = dict()
@ -123,10 +153,18 @@ class read_dex:
# We just parse everything manually to get the length, then we save the
# complete code block
f.seek(code_off)
registers_size, ins_size, outs_size, tries_size, debug_info_off, insns_size \
= unpack("<4HII", f.read(4 * 2 + 2 * 4))
(
registers_size,
ins_size,
outs_size,
tries_size,
debug_info_off,
insns_size,
) = unpack("<4HII", f.read(4 * 2 + 2 * 4))
insns = unpack("<{}H".format(insns_size), f.read(2 * insns_size))
insns = unpack(
"<{}H".format(insns_size), f.read(2 * insns_size)
)
if tries_size > 0 and insns_size % 2 == 1:
padding = unpack("<H", f.read(2))
@ -134,7 +172,10 @@ class read_dex:
if tries_size > 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)

View File

@ -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="<init>", descriptor=r"^\(.+\).*$"))),
138)
self.assertEqual(len(list(
dx.find_methods(classname="^(?!Landroid).*;$", methodname="<init>", descriptor=r"^\(.+\).*$",
no_external=True))), 94)
len(
list(
dx.find_methods(
classname="^(?!Landroid).*;$",
methodname="<init>",
descriptor=r"^\(.+\).*$",
)
)
),
138,
)
self.assertEqual(
len(
list(
dx.find_methods(
classname="^(?!Landroid).*;$",
methodname="<init>",
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(["<init>", "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(["<init>", "foonbar"]),
)
cfield = next(dx.find_fields(fieldname='cfield'))
# this one is static, hence it must have a write in <clinit>
self.assertListEqual(list(sorted(map(lambda x: x.name, map(itemgetter(1),
cfield.get_xref_write(with_offset=True))))),
sorted(["<clinit>"]))
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(["<clinit>"]),
)
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()

View File

@ -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()
)

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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), "<ARSCHeader idx='0x00000000' type='65226' header_size='8' size='16'>")
self.assertEqual(
repr(a),
"<ARSCHeader idx='0x00000000' type='65226' header_size='8' size='16'>",
)
def testAndroidManifest(self):
filenames = [

View File

@ -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,12 +50,13 @@ 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)
@ -62,7 +68,9 @@ 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)
@ -71,9 +79,13 @@ 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)
@ -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(), '<init>')
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(), '<init>')
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()

View File

@ -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,11 +23,12 @@ 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):

View File

@ -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

View File

@ -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)

View File

@ -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__':

View File

@ -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)

View File

@ -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,6 +17,7 @@ class MockClassManager():
def get_odex_format(self):
return False
class VMClassTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
@ -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': {
'<init>': 0x1 | 0x10000, # public | constructor
'testBreak': 0x1, # public
'testBreak2': 0x1,
'testBreak3': 0x1,
'testBreak4': 0x1,
'testBreakDoWhile': 0x1,
'testBreakMid': 0x1,
'testBreakbis': 0x1,
'<init>': 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': {
'<init>': 0x4 | 0x10000 # protected | constructor
'<init>': 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': {
'<init>': 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
'<init>': 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))

View File

@ -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__':

View File

@ -1,5 +1,3 @@
import unittest
from androguard.core.dex import TypeMapItem

View File

@ -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("<init>", 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):

View File

@ -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;")

View File

@ -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"))

View File

@ -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 = {
('<d', 3),
('<d', 4),
('<d', 5),
('<l', -5),
('<l', -4),
('<l', -3),
@ -35,7 +33,6 @@ VALUES = {
('<l', 3),
('<l', 4),
('<l', 5),
('<f', -5),
('<f', -4),
('<f', -3),
@ -47,61 +44,49 @@ VALUES = {
('<f', 3),
('<f', 4),
('<f', 5),
('<d', 65534),
('<d', 65535),
('<d', 65536),
('<d', 65537),
('<d', 32769),
('<d', 32768),
('<d', 32767),
('<d', 32766),
('<l', 65534),
('<l', 65535),
('<l', 65536),
('<l', 65537),
('<l', 32769),
('<l', 32768),
('<l', 32767),
('<l', 32766),
('<f', 65534),
('<f', 65535),
('<f', 65536),
('<f', 65537),
('<f', 32769),
('<f', 32768),
('<f', 32767),
('<f', 32766),
('<d', 5346952),
('<l', 5346952),
('<f', 5346952),
('<d', 65534.50),
('<d', 65535.50),
('<d', 65536.50),
('<d', 65537.50),
('<d', 32769.50),
('<d', 32768.50),
('<d', 32767.50),
('<d', 32766.50),
('<f', 65534.50),
('<f', 65535.50),
('<f', 65536.50),
('<f', 65537.50),
('<f', 32769.50),
('<f', 32768.50),
('<f', 32767.50),
('<f', 32766.50),
('<f', -5),
('<f', -65535),
('<f', -65536),
@ -140,7 +125,7 @@ def format_value(literal, ins, to):
trailing = bytearray()
if to == '<l' and packed[-1] & 0x80 == 0x80:
# Sign extension
trailing = bytearray([0xff] * (calcsize(to) - calcsize(formats[char])))
trailing = bytearray([0xFF] * (calcsize(to) - calcsize(formats[char])))
elif to == '<l' and packed[-1] & 0x80 == 0x00:
# no sign extension, but requires zero trailing
trailing = bytearray([0] * (calcsize(to) - calcsize(formats[char])))
@ -158,10 +143,14 @@ class TypesTest(unittest.TestCase):
with open(TEST_CASE, "rb") as fd:
digest, d, dx = s.addDEX(TEST_CASE, fd.read())
for method in filter(lambda x: x.full_name in VALUES, d.get_encoded_methods()):
for method in filter(
lambda x: x.full_name in VALUES, d.get_encoded_methods()
):
# print("METHOD", method.full_name)
for i in filter(lambda x: 'const' in x.get_name(), method.get_instructions()):
for i in filter(
lambda x: 'const' in x.get_name(), method.get_instructions()
):
i.show(0)
# ins should only have one literal
self.assertEqual(len(i.get_literals()), 1)