Merge branch 'master' into master

This commit is contained in:
erev0s 2024-05-07 18:05:35 +03:00 committed by GitHub
commit c72e8f8ddc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1684 additions and 1537 deletions

View File

@ -3,6 +3,7 @@ import os
import re
import shutil
import sys
from typing import Union
# 3rd party modules
from lxml import etree
@ -10,8 +11,11 @@ from loguru import logger
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters.terminal import TerminalFormatter
from oscrypto import asymmetric
# 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
@ -19,8 +23,11 @@ from androguard.core.dex import get_bytecodes_method
from androguard.util import readFile
from androguard.ui import DynamicUI
def androaxml_main(inp, outp=None, resource=None):
def androaxml_main(
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)
@ -46,7 +53,13 @@ def androaxml_main(inp, outp=None, resource=None):
sys.stdout.write(highlight(buff.decode("UTF-8"), get_lexer_by_name("xml"), TerminalFormatter()))
def androarsc_main(arscobj, outp=None, package=None, typ=None, locale=None):
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:
package = package or arscobj.get_packages_names()[0]
ttype = typ or "public"
locale = locale or '\x00\x00'
@ -74,13 +87,15 @@ def androarsc_main(arscobj, outp=None, package=None, typ=None, locale=None):
sys.stdout.write(highlight(buff.decode("UTF-8"), get_lexer_by_name("xml"), TerminalFormatter()))
def export_apps_to_format(filename,
s,
output,
methods_filter=None,
jar=None,
decompiler_type=None,
form=None):
def export_apps_to_format(
filename:str,
s: Session,
output: str,
methods_filter:Union[str,None]=None,
jar:bool=False,
decompiler_type:Union[str,None]=None,
form:Union[str,None]=None) -> None:
from androguard.misc import clean_file_name
from androguard.core.bytecode import method2dot, method2format
from androguard.decompiler import decompiler
@ -187,18 +202,18 @@ def export_apps_to_format(filename,
print()
def valid_class_name(class_name):
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):
def create_directory(pathdir:str) -> None:
if not os.path.exists(pathdir):
os.makedirs(pathdir)
def androlyze_main(session, filename):
def androlyze_main(session:Session, filename:str) -> None:
"""
Start an interactive shell
@ -274,7 +289,7 @@ def androlyze_main(session, filename):
print(dx)
print()
def shutdown_hook():
def shutdown_hook() -> None:
"""Save the session on exit, if wanted"""
if not s.isOpen():
return
@ -297,7 +312,7 @@ def androlyze_main(session, filename):
ipshell()
def androsign_main(args_apk, args_hash, args_all, show):
def androsign_main(args_apk:list[str], args_hash:str, args_all:bool, show:bool) -> None:
from androguard.core.apk import APK
from androguard.util import get_certificate_name_string
@ -360,14 +375,14 @@ def androsign_main(args_apk, args_hash, args_all, show):
for public_key in pkeys:
if show:
x509_public_key = keys.PublicKeyInfo.load(public_key)
x509_public_key = asymmetric.load_public_key(public_key)
print("PublicKey Algorithm:", x509_public_key.algorithm)
print("Bit Size:", x509_public_key.bit_size)
print("Fingerprint:", binascii.hexlify(x509_public_key.fingerprint))
try:
print("Hash Algorithm:", x509_public_key.hash_algo)
print("Hash Algorithm:", x509_public_key.asn1.hash_algo)
except ValueError as ve:
# RSA pkey does not have an hash algorithm
# RSA pkey does not have a hash algorithm
pass
print()
@ -380,7 +395,7 @@ def androsign_main(args_apk, args_hash, args_all, show):
print()
def androdis_main(offset, size, dex_file):
def androdis_main(offset:int, size:int, dex_file:str) -> None:
from androguard.core.dex import DEX
with open(dex_file, "rb") as fp:
@ -413,7 +428,7 @@ def androdis_main(offset, size, dex_file):
idx += i.get_length()
def androtrace_main(apk_file, list_modules, live=False, enable_ui=False):
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
@ -458,7 +473,7 @@ def androtrace_main(apk_file, list_modules, live=False, enable_ui=False):
s = input("Type 'e' to exit:")
def androdump_main(package_name, list_modules):
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

@ -1,14 +1,20 @@
import sys
# Allows type hinting of types not-yet-declared
# in Python >= 3.7
# see https://peps.python.org/pep-0563/
from __future__ import annotations
import os
import logging
import sys
import tempfile
from colorama import init, Fore
from loguru import logger
from typing import Union
from androguard import __version__
from androguard.core.api_specific_resources import load_permission_mappings, load_permissions
ANDROGUARD_VERSION = __version__
from colorama import init, Fore
from loguru import logger
# initialize colorama, only has an effect on windows
init()
@ -21,7 +27,7 @@ class InvalidResourceError(Exception):
pass
def is_ascii_problem(s):
def is_ascii_problem(s: str) -> bool:
"""
Test if a string contains other chars than ASCII
@ -85,7 +91,7 @@ default_conf = {
class Configuration:
instance = None
def __init__(self):
def __init__(self) -> None:
"""
A Wrapper for the CONF object
This creates a singleton, which has the same attributes everywhere.
@ -112,7 +118,7 @@ class Configuration:
CONF = Configuration()
def is_android(filename):
def is_android(filename: str) -> str:
"""
Return the type of the file
@ -127,7 +133,7 @@ def is_android(filename):
return is_android_raw(f_bytes)
def is_android_raw(raw):
def is_android_raw(raw: bytes) -> str:
"""
Returns a string that describes the type of file, for common Android
specific formats
@ -155,7 +161,7 @@ def is_android_raw(raw):
return val
def rrmdir(directory):
def rrmdir(directory: str) -> None:
"""
Recursivly delete a directory
@ -169,7 +175,7 @@ def rrmdir(directory):
os.rmdir(directory)
def make_color_tuple(color):
def make_color_tuple(color: str) -> tuple[int,int,int]:
"""
turn something like "#000000" into 0,0,0
or "#FFFFFF into "255,255,255"
@ -185,7 +191,7 @@ def make_color_tuple(color):
return R, G, B
def interpolate_tuple(startcolor, goalcolor, steps):
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
"""
@ -228,7 +234,7 @@ def interpolate_tuple(startcolor, goalcolor, steps):
return buffer
def color_range(startcolor, goalcolor, steps):
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)
"""
@ -238,7 +244,7 @@ def color_range(startcolor, goalcolor, steps):
return interpolate_tuple(start_tuple, goal_tuple, steps)
def load_api_specific_resource_module(resource_name, api=None):
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.

View File

@ -1,6 +1,8 @@
import json
import os
import re
from typing import Union
from loguru import logger
@ -8,8 +10,7 @@ from loguru import logger
class APILevelNotFoundError(Exception):
pass
def load_permissions(apilevel, permtype='permissions'):
def load_permissions(apilevel:Union[str,int], permtype:str='permissions') -> dict[str, dict[str,str]]:
"""
Load the Permissions for the given apilevel.
@ -64,7 +65,7 @@ def load_permissions(apilevel, permtype='permissions'):
return json.load(fp)[permtype]
def load_permission_mappings(apilevel):
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.

View File

@ -1,34 +1,46 @@
# Allows type hinting of types not-yet-declared
# in Python >= 3.7
# see https://peps.python.org/pep-0563/
from __future__ import annotations
# Python core
import binascii
import hashlib
import io
import os
import re
from struct import unpack
from typing import Iterator, Union
import zipfile
from zlib import crc32
# Androguard
from androguard.core import androconf
from androguard.util import get_certificate_name_string
from apkInspector.headers import ZipEntry
from androguard.core.axml import ARSCParser, AXMLPrinter, ARSCResTableConfig, AXMLParser, format_value, START_TAG, END_TAG, TEXT, END_DOCUMENT
# Python core
import io
from zlib import crc32
import os
import re
import binascii
import zipfile
import logging
from struct import unpack
import hashlib
import warnings
from androguard.core.axml import (ARSCParser,
AXMLPrinter,
ARSCResTableConfig,
AXMLParser,
format_value,
START_TAG,
END_TAG,
TEXT,
END_DOCUMENT)
# External dependecies
from lxml.etree import Element
import lxml.sax
from xml.dom.pulldom import SAX2DOM
# Used for reading Certificates
from asn1crypto import cms, x509, keys
from loguru import logger
NS_ANDROID_URI = 'http://schemas.android.com/apk/res/android'
NS_ANDROID = '{{{}}}'.format(NS_ANDROID_URI) # Namespace as used by etree
def parse_lxml_dom(tree):
handler = SAX2DOM()
lxml.sax.saxify(tree, handler)
@ -89,7 +101,7 @@ class APKV2SignedData:
source : https://source.android.com/security/apksigning/v2.html
"""
def __init__(self):
def __init__(self) -> None:
self._bytes = None
self.digests = None
self.certificates = None
@ -125,7 +137,7 @@ class APKV3SignedData(APKV2SignedData):
source : https://source.android.com/security/apksigning/v3.html
"""
def __init__(self):
def __init__(self) -> None:
super().__init__()
self.minSDK = None
self.maxSDK = None
@ -152,7 +164,7 @@ class APKV2Signer:
source : https://source.android.com/security/apksigning/v2.html
"""
def __init__(self):
def __init__(self) -> None:
self._bytes = None
self.signed_data = None
self.signatures = None
@ -172,7 +184,7 @@ class APKV3Signer(APKV2Signer):
source : https://source.android.com/security/apksigning/v3.html
"""
def __init__(self):
def __init__(self) -> None:
super().__init__()
self.minSDK = None
self.maxSDK = None
@ -216,7 +228,7 @@ class APK:
__no_magic = False
def __init__(self, filename, raw=False, magic_file=None, skip_analysis=False, testzip=False):
def __init__(self, filename:str, raw:bool=False, magic_file:Union[str,None]=None, skip_analysis:bool=False, testzip:bool=False) -> None:
"""
This class can access to all elements in an APK file
@ -428,7 +440,7 @@ class APK:
pass
return maxSdkVersion
def is_valid_APK(self):
def is_valid_APK(self) -> bool:
"""
Return true if the APK is valid, false otherwise.
An APK is seen as valid, if the AndroidManifest.xml could be successful parsed.
@ -439,7 +451,7 @@ class APK:
"""
return self.valid_apk
def get_filename(self):
def get_filename(self) -> str:
"""
Return the filename of the APK
@ -508,7 +520,7 @@ class APK:
logger.warning("Exception selecting app name: %s" % e)
return app_name
def get_app_icon(self, max_dpi=65536):
def get_app_icon(self, max_dpi:int=65536) -> Union[str,None]:
"""
Return the first icon file name, which density is not greater than max_dpi,
unless exact icon resolution is set in the manifest, in which case
@ -596,7 +608,7 @@ class APK:
return app_icon
def get_package(self):
def get_package(self) -> str:
"""
Return the name of the package
@ -606,7 +618,7 @@ class APK:
"""
return self.package
def get_androidversion_code(self):
def get_androidversion_code(self) -> str:
"""
Return the android version code
@ -616,7 +628,7 @@ class APK:
"""
return self.androidversion["Code"]
def get_androidversion_name(self):
def get_androidversion_name(self) -> str:
"""
Return the android version name
@ -626,7 +638,7 @@ class APK:
"""
return self.androidversion["Name"]
def get_files(self):
def get_files(self) -> list[str]:
"""
Return the file names inside the APK.
@ -634,7 +646,7 @@ class APK:
"""
return self.zip.namelist()
def _get_file_magic_name(self, buffer):
def _get_file_magic_name(self, buffer: bytes) -> str:
"""
Return the filetype guessed for a buffer
:param buffer: bytes
@ -686,7 +698,7 @@ class APK:
return self._patch_magic(buffer, ftype)
@property
def files(self):
def files(self) -> dict[str, str]:
"""
Returns a dictionary of filenames and detected magic type
@ -694,7 +706,7 @@ class APK:
"""
return self.get_files_types()
def get_files_types(self):
def get_files_types(self) -> dict[str,str]:
"""
Return the files inside the APK with their associated types (by using python-magic)
@ -742,7 +754,7 @@ class APK:
self.files_crc32[filename]))
return buffer
def get_files_crc32(self):
def get_files_crc32(self) -> dict[str, int]:
"""
Calculates and returns a dictionary of filenames and CRC32
@ -754,7 +766,7 @@ class APK:
return self.files_crc32
def get_files_information(self):
def get_files_information(self) -> Iterator[tuple[str, str, int]]:
"""
Return the files inside the APK with their associated types and crc32
@ -763,7 +775,7 @@ class APK:
for k in self.get_files():
yield k, self.get_files_types()[k], self.get_files_crc32()[k]
def get_raw(self):
def get_raw(self) -> bytes:
"""
Return raw bytes of the APK
@ -777,7 +789,7 @@ class APK:
self.__raw = bytearray(f.read())
return self.__raw
def get_file(self, filename):
def get_file(self, filename:str) -> bytes:
"""
Return the raw data of the specified filename
inside the APK
@ -789,7 +801,7 @@ class APK:
except KeyError:
raise FileNotPresent(filename)
def get_dex(self):
def get_dex(self) -> bytes:
"""
Return the raw data of the classes dex file
@ -804,7 +816,7 @@ class APK:
# TODO is this a good idea to return an empty string?
return b""
def get_dex_names(self):
def get_dex_names(self) -> list[str]:
"""
Return the names of all DEX files found in the APK.
This method only accounts for "offical" dex files, i.e. all files
@ -815,7 +827,7 @@ class APK:
dexre = re.compile(r"^classes(\d*).dex$")
return filter(lambda x: dexre.match(x), self.get_files())
def get_all_dex(self):
def get_all_dex(self) -> Iterator[bytes]:
"""
Return the raw data of all classes dex files
@ -824,7 +836,7 @@ class APK:
for dex_name in self.get_dex_names():
yield self.get_file(dex_name)
def is_multidex(self):
def is_multidex(self) -> bool:
"""
Test if the APK has multiple DEX files
@ -855,8 +867,8 @@ class APK:
return value
def get_all_attribute_value(
self, tag_name, attribute, format_value=True, **attribute_filter
):
self, tag_name: str, attribute: str, format_value:bool=True, **attribute_filter
) -> Iterator[str]:
"""
Yields all the attribute values in xml files which match with the tag name and the specific attribute
@ -874,8 +886,8 @@ class APK:
yield value
def get_attribute_value(
self, tag_name, attribute, format_value=False, **attribute_filter
):
self, tag_name:str, attribute:str, format_value:bool=False, **attribute_filter
) -> str:
"""
Return the attribute value in xml files which matches the tag name and the specific attribute
@ -889,7 +901,7 @@ class APK:
if value is not None:
return value
def get_value_from_tag(self, tag, attribute):
def get_value_from_tag(self, tag: Element, attribute: str) -> Union[str,None]:
"""
Return the value of the android prefixed attribute in a specific tag.
@ -933,7 +945,7 @@ class APK:
"But found the same attribute without namespace!".format(attribute, tag.tag))
return value
def find_tags(self, tag_name, **attribute_filter):
def find_tags(self, tag_name: str, **attribute_filter) -> list[str]:
"""
Return a list of all the matched tags in all available xml
@ -948,8 +960,8 @@ class APK:
return [tag for tag_list in all_tags for tag in tag_list]
def find_tags_from_xml(
self, xml_name, tag_name, **attribute_filter
):
self, xml_name: str, tag_name: str, **attribute_filter
) -> list[str]:
"""
Return a list of all the matched tags in a specific xml
w
@ -972,7 +984,7 @@ class APK:
)
]
def is_tag_matched(self, tag, **attribute_filter):
def is_tag_matched(self, tag: str, **attribute_filter) -> bool:
r"""
Return true if the attributes matches in attribute filter.
@ -999,7 +1011,7 @@ class APK:
return False
return True
def get_main_activities(self):
def get_main_activities(self) -> set[str]:
"""
Return names of the main activities
@ -1043,7 +1055,7 @@ class APK:
return x.intersection(y)
def get_main_activity(self):
def get_main_activity(self) -> Union[str,None]:
"""
Return the name of the main activity
@ -1065,7 +1077,7 @@ class APK:
return sorted(main_activities)[0]
return None
def get_activities(self):
def get_activities(self) -> list[str]:
"""
Return the android:name attribute of all activities
@ -1073,7 +1085,7 @@ class APK:
"""
return list(self.get_all_attribute_value("activity", "name"))
def get_activity_aliases(self):
def get_activity_aliases(self) -> list[dict[str,str]]:
"""
Return the android:name and android:targetActivity attribute of all activity aliases.
@ -1092,7 +1104,7 @@ class APK:
ali.append(activity_alias)
return ali
def get_services(self):
def get_services(self) -> list[str]:
"""
Return the android:name attribute of all services
@ -1100,7 +1112,7 @@ class APK:
"""
return list(self.get_all_attribute_value("service", "name"))
def get_receivers(self):
def get_receivers(self) -> list[str]:
"""
Return the android:name attribute of all receivers
@ -1108,7 +1120,7 @@ class APK:
"""
return list(self.get_all_attribute_value("receiver", "name"))
def get_providers(self):
def get_providers(self) -> list[str]:
"""
Return the android:name attribute of all providers
@ -1116,7 +1128,7 @@ class APK:
"""
return list(self.get_all_attribute_value("provider", "name"))
def get_res_value(self, name):
def get_res_value(self, name:str) -> str:
"""
Return the literal value with a resource id
:rtype: str
@ -1137,7 +1149,7 @@ class APK:
return value
def get_intent_filters(self, itemtype, name):
def get_intent_filters(self, itemtype:str, name:str) -> dict[str,list[str]]:
"""
Find intent filters for a given item and name.
@ -1188,7 +1200,7 @@ class APK:
return d
def get_permissions(self):
def get_permissions(self) -> list[str]:
"""
Return permissions names declared in the AndroidManifest.xml.
@ -1205,7 +1217,7 @@ class APK:
"""
return self.permissions
def get_uses_implied_permission_list(self):
def get_uses_implied_permission_list(self) -> list[str]:
"""
Return all permissions implied by the target SDK or other permissions.
@ -1275,7 +1287,7 @@ class APK:
label, description]
return filled_permissions
def get_details_permissions(self):
def get_details_permissions(self) -> dict[str, list[str]]:
"""
Return permissions with details.
@ -1296,7 +1308,7 @@ class APK:
"Unknown permission from android reference"]
return self._fill_deprecated_permissions(l)
def get_requested_aosp_permissions(self):
def get_requested_aosp_permissions(self) -> list[str]:
"""
Returns requested permissions declared within AOSP project.
@ -1311,7 +1323,7 @@ class APK:
aosp_permissions.append(perm)
return aosp_permissions
def get_requested_aosp_permissions_details(self):
def get_requested_aosp_permissions_details(self) -> dict[str, list[str]]:
"""
Returns requested aosp permissions with details.
@ -1326,7 +1338,7 @@ class APK:
continue
return l
def get_requested_third_party_permissions(self):
def get_requested_third_party_permissions(self) -> list[str]:
"""
Returns list of requested permissions not declared within AOSP project.
@ -1339,7 +1351,7 @@ class APK:
third_party_permissions.append(perm)
return third_party_permissions
def get_declared_permissions(self):
def get_declared_permissions(self) -> list[str]:
"""
Returns list of the declared permissions.
@ -1347,7 +1359,7 @@ class APK:
"""
return list(self.declared_permissions.keys())
def get_declared_permissions_details(self):
def get_declared_permissions_details(self) -> dict[str, list[str]]:
"""
Returns declared permissions with the details.
@ -1355,7 +1367,7 @@ class APK:
"""
return self.declared_permissions
def get_max_sdk_version(self):
def get_max_sdk_version(self) -> str:
"""
Return the android:maxSdkVersion attribute
@ -1363,7 +1375,7 @@ class APK:
"""
return self.get_attribute_value("uses-sdk", "maxSdkVersion")
def get_min_sdk_version(self):
def get_min_sdk_version(self) -> str:
"""
Return the android:minSdkVersion attribute
@ -1371,7 +1383,7 @@ class APK:
"""
return self.get_attribute_value("uses-sdk", "minSdkVersion")
def get_target_sdk_version(self):
def get_target_sdk_version(self) -> str:
"""
Return the android:targetSdkVersion attribute
@ -1379,7 +1391,7 @@ class APK:
"""
return self.get_attribute_value("uses-sdk", "targetSdkVersion")
def get_effective_target_sdk_version(self):
def get_effective_target_sdk_version(self) -> int:
"""
Return the effective targetSdkVersion, always returns int > 0.
@ -1397,7 +1409,7 @@ class APK:
except (ValueError, TypeError):
return 1
def get_libraries(self):
def get_libraries(self) -> list[str]:
"""
Return the android:name attributes for libraries
@ -1405,7 +1417,7 @@ class APK:
"""
return list(self.get_all_attribute_value("uses-library", "name"))
def get_features(self):
def get_features(self) -> list[str]:
"""
Return a list of all android:names found for the tag uses-feature
in the AndroidManifest.xml
@ -1414,7 +1426,7 @@ class APK:
"""
return list(self.get_all_attribute_value("uses-feature", "name"))
def is_wearable(self):
def is_wearable(self) -> bool:
"""
Checks if this application is build for wearables by
checking if it uses the feature 'android.hardware.type.watch'
@ -1427,7 +1439,7 @@ class APK:
"""
return 'android.hardware.type.watch' in self.get_features()
def is_leanback(self):
def is_leanback(self) -> bool:
"""
Checks if this application is build for TV (Leanback support)
by checkin if it uses the feature 'android.software.leanback'
@ -1436,7 +1448,7 @@ class APK:
"""
return 'android.software.leanback' in self.get_features()
def is_androidtv(self):
def is_androidtv(self) -> bool:
"""
Checks if this application does not require a touchscreen,
as this is the rule to get into the TV section of the Play Store
@ -1446,7 +1458,7 @@ class APK:
"""
return self.get_attribute_value('uses-feature', 'name', required="false", name="android.hardware.touchscreen") == "android.hardware.touchscreen"
def get_certificate_der(self, filename):
def get_certificate_der(self, filename:str) -> bytes:
"""
Return the DER coded X.509 certificate from the signature file.
@ -1459,7 +1471,7 @@ class APK:
cert = pkcs7obj['content']['certificates'][0].chosen.dump()
return cert
def get_certificate(self, filename):
def get_certificate(self, filename:str) -> x509.Certificate:
"""
Return a X.509 certificate object by giving the name in the apk file
@ -1471,7 +1483,7 @@ class APK:
return certificate
def new_zip(self, filename, deleted_files=None, new_files={}):
def new_zip(self, filename:str, deleted_files:Union[str,None]=None, new_files:dict={}) -> None:
"""
Create a new zip file
@ -1512,18 +1524,18 @@ class APK:
zout.writestr(item, buffer)
zout.close()
def get_android_manifest_axml(self):
def get_android_manifest_axml(self) -> Union[AXMLPrinter,None]:
"""
Return the :class:`AXMLPrinter` object which corresponds to the AndroidManifest.xml file
:rtype: :class:`~androguard.core.bytecodes.axml.AXMLPrinter`
:rtype: :class:`~androguard.core.axml.AXMLPrinter`
"""
try:
return self.axml["AndroidManifest.xml"]
except KeyError:
return None
def get_android_manifest_xml(self):
def get_android_manifest_xml(self) -> Union[lxml.etree.Element, None]:
"""
Return the parsed xml object which corresponds to the AndroidManifest.xml file
@ -1534,11 +1546,11 @@ class APK:
except KeyError:
return None
def get_android_resources(self):
def get_android_resources(self) -> Union[ARSCParser,None]:
"""
Return the :class:`~androguard.core.bytecodes.axml.ARSCParser` object which corresponds to the resources.arsc file
Return the :class:`~androguard.core.axml.ARSCParser` object which corresponds to the resources.arsc file
:rtype: :class:`~androguard.core.bytecodes.axml.ARSCParser`
:rtype: :class:`~androguard.core.axml.ARSCParser`
"""
try:
return self.arsc["resources.arsc"]
@ -1550,13 +1562,13 @@ class APK:
self.arsc["resources.arsc"] = ARSCParser(self.zip.read("resources.arsc"))
return self.arsc["resources.arsc"]
def is_signed(self):
def is_signed(self) -> bool:
"""
Returns true if any of v1, v2, or v3 signatures were found.
"""
return self.is_signed_v1() or self.is_signed_v2() or self.is_signed_v3()
def is_signed_v1(self):
def is_signed_v1(self) -> bool:
"""
Returns true if a v1 / JAR signature was found.
@ -1565,7 +1577,7 @@ class APK:
"""
return self.get_signature_name() is not None
def is_signed_v2(self):
def is_signed_v2(self) -> bool:
"""
Returns true of a v2 / APK signature was found.
@ -1577,7 +1589,7 @@ class APK:
return self._is_signed_v2
def is_signed_v3(self):
def is_signed_v3(self) -> bool:
"""
Returns true of a v3 / APK signature was found.
@ -1589,11 +1601,11 @@ class APK:
return self._is_signed_v3
def read_uint32_le(self, io_stream):
def read_uint32_le(self, io_stream) -> int:
value, = unpack('<I', io_stream.read(4))
return value
def parse_signatures_or_digests(self, digest_bytes):
def parse_signatures_or_digests(self, digest_bytes) -> list[tuple[int, bytes]]:
""" Parse digests """
if not len(digest_bytes):
@ -1613,7 +1625,7 @@ class APK:
return digests
def parse_v2_v3_signature(self):
def parse_v2_v3_signature(self) -> None:
# Need to find an v2 Block in the APK.
# The Google Docs gives you the following rule:
# * go to the end of the ZIP File
@ -1695,7 +1707,7 @@ class APK:
self._is_signed_v3 = True
def parse_v3_signing_block(self):
def parse_v3_signing_block(self) -> None:
"""
Parse the V2 signing block and extract all features
"""
@ -1794,7 +1806,7 @@ class APK:
self._v3_signing_data.append(signer)
def parse_v2_signing_block(self):
def parse_v2_signing_block(self) -> None:
"""
Parse the V2 signing block and extract all features
"""
@ -1876,7 +1888,7 @@ class APK:
self._v2_signing_data.append(signer)
def get_public_keys_der_v3(self):
def get_public_keys_der_v3(self) -> list[bytes]:
"""
Return a list of DER coded X.509 public keys from the v3 signature block
"""
@ -1891,7 +1903,7 @@ class APK:
return public_keys
def get_public_keys_der_v2(self):
def get_public_keys_der_v2(self) -> list[bytes]:
"""
Return a list of DER coded X.509 public keys from the v3 signature block
"""
@ -1906,7 +1918,7 @@ class APK:
return public_keys
def get_certificates_der_v3(self):
def get_certificates_der_v3(self) -> list[bytes]:
"""
Return a list of DER coded X.509 certificates from the v3 signature block
"""
@ -1921,7 +1933,7 @@ class APK:
return certs
def get_certificates_der_v2(self):
def get_certificates_der_v2(self) -> list[bytes]:
"""
Return a list of DER coded X.509 certificates from the v3 signature block
"""
@ -1936,21 +1948,21 @@ class APK:
return certs
def get_public_keys_v3(self):
def get_public_keys_v3(self) -> list[keys.PublicKeyInfo]:
"""
Return a list of :class:`asn1crypto.keys.PublicKeyInfo` which are found
in the v3 signing block.
"""
return [ keys.PublicKeyInfo.load(pkey) for pkey in self.get_public_keys_der_v3()]
def get_public_keys_v2(self):
def get_public_keys_v2(self) -> list[keys.PublicKeyInfo]:
"""
Return a list of :class:`asn1crypto.keys.PublicKeyInfo` which are found
in the v2 signing block.
"""
return [ keys.PublicKeyInfo.load(pkey) for pkey in self.get_public_keys_der_v2()]
def get_certificates_v3(self):
def get_certificates_v3(self) -> list[x509.Certificate]:
"""
Return a list of :class:`asn1crypto.x509.Certificate` which are found
in the v3 signing block.
@ -1959,7 +1971,7 @@ class APK:
"""
return [ x509.Certificate.load(cert) for cert in self.get_certificates_der_v3()]
def get_certificates_v2(self):
def get_certificates_v2(self) -> list[x509.Certificate]:
"""
Return a list of :class:`asn1crypto.x509.Certificate` which are found
in the v2 signing block.
@ -1968,7 +1980,7 @@ class APK:
"""
return [ x509.Certificate.load(cert) for cert in self.get_certificates_der_v2()]
def get_certificates_v1(self):
def get_certificates_v1(self) -> list[x509.Certificate]:
"""
Return a list of :class:`asn1crypto.x509.Certificate` which are found
in the META-INF folder (v1 signing).
@ -1981,7 +1993,7 @@ class APK:
return certs
def get_certificates(self):
def get_certificates(self) -> list[x509.Certificate]:
"""
Return a list of unique :class:`asn1crypto.x509.Certificate` which are found
in v1, v2 and v3 signing
@ -1996,7 +2008,7 @@ class APK:
certs.append(x)
return certs
def get_signature_name(self):
def get_signature_name(self) -> Union[str,None]:
"""
Return the name of the first signature file found.
"""
@ -2006,7 +2018,7 @@ class APK:
# Unsigned APK
return None
def get_signature_names(self):
def get_signature_names(self) -> list[str]:
"""
Return a list of the signature file names (v1 Signature / JAR
Signature)
@ -2025,7 +2037,7 @@ class APK:
return signatures
def get_signature(self):
def get_signature(self) -> Union[str,None]:
"""
Return the data of the first signature file found (v1 Signature / JAR
Signature)
@ -2037,7 +2049,7 @@ class APK:
else:
return None
def get_signatures(self):
def get_signatures(self) -> list[bytes]:
"""
Return a list of the data of the signature files.
Only v1 / JAR Signing.
@ -2053,7 +2065,7 @@ class APK:
return signature_datas
def show(self):
def show(self) -> None:
self.get_files_types()
print("FILES: ")
@ -2106,7 +2118,7 @@ class APK:
show_Certificate(c)
def show_Certificate(cert, short=False):
def show_Certificate(cert, short:bool=False) -> None:
"""
Print Fingerprints, Issuer and Subject of an X509 Certificate.
@ -2122,7 +2134,7 @@ def show_Certificate(cert, short=False):
print("Subject: {}".format(get_certificate_name_string(cert.subject.native, short=short)))
def ensure_final_value(packageName, arsc, value):
def ensure_final_value(packageName:str, arsc:ARSCParser, value:str) -> str:
"""Ensure incoming value is always the value, not the resid
androguard will sometimes return the Android "resId" aka
@ -2145,7 +2157,7 @@ def ensure_final_value(packageName, arsc, value):
return ''
def get_apkid(apkfile):
def get_apkid(apkfile: str) -> tuple[str,str,str]:
"""Read (appid, versionCode, versionName) from an APK
This first tries to do quick binary XML parsing to just get the

View File

@ -1,16 +1,22 @@
# Allows type hinting of types not-yet-declared
# in Python >= 3.7
# see https://peps.python.org/pep-0563/
from __future__ import annotations
import binascii
from collections import defaultdict
import collections
import io
import re
from struct import pack, unpack
from typing import BinaryIO, Union
from androguard.core.resources import public
from .types import *
from struct import pack, unpack
from xml.sax.saxutils import escape
import collections
from collections import defaultdict
from lxml import etree
from loguru import logger
import re
import binascii
import io
from lxml import etree
from xml.sax.saxutils import escape
# Constants for ARSC Files
# see http://aospxref.com/android-13.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#233
@ -85,7 +91,7 @@ class ResParserError(Exception):
pass
def complexToFloat(xcomplex):
def complexToFloat(xcomplex) -> float:
"""
Convert a complex unit into float
"""
@ -99,7 +105,7 @@ class StringBlock:
See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#436
"""
def __init__(self, buff, header):
def __init__(self, buff:BinaryIO, header:ARSCHeader) -> None:
"""
:param buff: buffer which holds the string block
:param header: a instance of :class:`~ARSCHeader`
@ -191,7 +197,7 @@ class StringBlock:
for i in range(self.stringCount):
yield self.getString(i)
def getString(self, idx):
def getString(self, idx: int) -> str:
"""
Return the string at the index in the string table
@ -213,7 +219,7 @@ class StringBlock:
return self._cache[idx]
def getStyle(self, idx):
def getStyle(self, idx:int) -> int:
"""
Return the style associated with the index
@ -222,7 +228,7 @@ class StringBlock:
"""
return self.m_styles[idx]
def _decode8(self, offset):
def _decode8(self, offset:int) -> str:
"""
Decode an UTF-8 String at the given offset
@ -238,6 +244,13 @@ class StringBlock:
encoded_bytes, skip = self._decode_length(offset, 1)
offset += skip
# Two checks should happen here:
# a) offset + encoded_bytes surpassing the string_pool length and
# b) non-null terminated strings which should be rejected
# platform/frameworks/base/libs/androidfw/ResourceTypes.cpp#789
if len(self.m_charbuff) < (offset + encoded_bytes):
logger.warning(f"String size: {offset + encoded_bytes} is exceeding string pool size. Returning empty string.")
return ""
data = self.m_charbuff[offset: offset + encoded_bytes]
if self.m_charbuff[offset + encoded_bytes] != 0:
@ -245,7 +258,7 @@ class StringBlock:
return self._decode_bytes(data, 'utf-8', str_len)
def _decode16(self, offset):
def _decode16(self, offset:int) -> str:
"""
Decode an UTF-16 String at the given offset
@ -258,6 +271,14 @@ class StringBlock:
# The len is the string len in utf-16 units
encoded_bytes = str_len * 2
# Two checks should happen here:
# a) offset + encoded_bytes surpassing the string_pool length and
# b) non-null terminated strings which should be rejected
# platform/frameworks/base/libs/androidfw/ResourceTypes.cpp#789
if len(self.m_charbuff) < (offset + encoded_bytes):
logger.warning(f"String size: {offset + encoded_bytes} is exceeding string pool size. Returning empty string.")
return ""
data = self.m_charbuff[offset: offset + encoded_bytes]
if self.m_charbuff[offset + encoded_bytes:offset + encoded_bytes + 2] != b"\x00\x00":
@ -266,7 +287,7 @@ class StringBlock:
return self._decode_bytes(data, 'utf-16', str_len)
@staticmethod
def _decode_bytes(data, encoding, str_len):
def _decode_bytes(data:bytes, encoding:str, str_len:int) -> str:
"""
Generic decoding with length check.
The string is decoded from bytes with the given encoding, then the length
@ -283,7 +304,7 @@ class StringBlock:
logger.warning("invalid decoded string length")
return string
def _decode_length(self, offset, sizeof_char):
def _decode_length(self, offset:int, sizeof_char:int) -> tuple[int,int]:
"""
Generic Length Decoding at offset of string
@ -320,7 +341,7 @@ class StringBlock:
return length, size
def show(self):
def show(self) -> None:
"""
Print some information on stdout about the string table
"""
@ -371,7 +392,7 @@ class AXMLParser:
See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#563
"""
def __init__(self, raw_buff):
def __init__(self, raw_buff:bytes) -> None:
logger.debug("AXMLParser")
self._reset()
@ -380,6 +401,7 @@ class AXMLParser:
self.axml_tampered = False
self.buff = io.BufferedReader(io.BytesIO(raw_buff))
self.buff_size = self.buff.raw.getbuffer().nbytes
self.packerwarning = False
# Minimum is a single ARSCHeader, which would be a strange edge case...
if self.buff_size < 8:
@ -457,7 +479,7 @@ class AXMLParser:
# Store a list of prefix/uri mappings encountered
self.namespaces = []
def is_valid(self):
def is_valid(self) -> bool:
"""
Get the state of the AXMLPrinter.
if an error happend somewhere in the process of parsing the file,
@ -670,7 +692,7 @@ class AXMLParser:
self.buff.seek(h.end)
@property
def name(self):
def name(self) -> str:
"""
Return the String assosciated with the tag name
"""
@ -680,7 +702,7 @@ class AXMLParser:
return self.sb[self.m_name]
@property
def comment(self):
def comment(self) -> Union[str,None]:
"""
Return the comment at the current position or None if no comment is given
@ -693,7 +715,7 @@ class AXMLParser:
return self.sb[self.m_comment_index]
@property
def namespace(self):
def namespace(self) -> str:
"""
Return the Namespace URI (if any) as a String for the current tag
"""
@ -707,7 +729,7 @@ class AXMLParser:
return self.sb[self.m_namespaceUri]
@property
def nsmap(self):
def nsmap(self) -> dict[str,str]:
"""
Returns the current namespace mapping as a dictionary
@ -733,7 +755,7 @@ class AXMLParser:
return NSMAP
@property
def text(self):
def text(self) -> str:
"""
Return the String assosicated with the current text
"""
@ -742,21 +764,21 @@ class AXMLParser:
return self.sb[self.m_name]
def getName(self):
def getName(self) -> str:
"""
Legacy only!
use :py:attr:`~androguard.core.bytecodes.AXMLParser.name` instead
"""
return self.name
def getText(self):
def getText(self) -> str:
"""
Legacy only!
use :py:attr:`~androguard.core.bytecodes.AXMLParser.text` instead
"""
return self.text
def getPrefix(self):
def getPrefix(self) -> str:
"""
Legacy only!
use :py:attr:`~androguard.core.bytecodes.AXMLParser.namespace` instead
@ -776,7 +798,7 @@ class AXMLParser:
return offset
def getAttributeCount(self):
def getAttributeCount(self) -> int:
"""
Return the number of Attributes for a Tag
or -1 if not in a tag
@ -786,7 +808,7 @@ class AXMLParser:
return self.m_attribute_count
def getAttributeUri(self, index):
def getAttributeUri(self, index:int):
"""
Returns the numeric ID for the namespace URI of an attribute
"""
@ -797,7 +819,7 @@ class AXMLParser:
return uri
def getAttributeNamespace(self, index):
def getAttributeNamespace(self, index:int):
"""
Return the Namespace URI (if any) for the attribute
"""
@ -811,7 +833,7 @@ class AXMLParser:
return self.sb[uri]
def getAttributeName(self, index):
def getAttributeName(self, index:int):
"""
Returns the String which represents the attribute name
"""
@ -821,17 +843,21 @@ class AXMLParser:
res = self.sb[name]
# If the result is a (null) string, we need to look it up.
if not res or res == ":":
if name <= len(self.m_resourceIDs):
attr = self.m_resourceIDs[name]
if attr in public.SYSTEM_RESOURCES['attributes']['inverse']:
res = 'android:' + public.SYSTEM_RESOURCES['attributes']['inverse'][attr]
else:
# Attach the HEX Number, so for multiple missing attributes we do not run
# into problems.
res = 'android:UNKNOWN_SYSTEM_ATTRIBUTE_{:08x}'.format(attr)
res = public.SYSTEM_RESOURCES['attributes']['inverse'][attr].replace("_",
":")
if res != self.sb[name]:
self.packerwarning = True
if not res or res == ":":
# Attach the HEX Number, so for multiple missing attributes we do not run
# into problems.
res = 'android:UNKNOWN_SYSTEM_ATTRIBUTE_{:08x}'.format(attr)
return res
def getAttributeValueType(self, index):
def getAttributeValueType(self, index:int):
"""
Return the type of the attribute at the given index
@ -842,7 +868,7 @@ class AXMLParser:
offset = self._get_attribute_offset(index)
return self.m_attributes[offset + ATTRIBUTE_IX_VALUE_TYPE]
def getAttributeValueData(self, index):
def getAttributeValueData(self, index:int):
"""
Return the data of the attribute at the given index
@ -872,7 +898,7 @@ class AXMLParser:
return ''
def format_value(_type, _data, lookup_string=lambda ix: "<string>"):
def format_value(_type: int, _data: int, lookup_string=lambda ix: "<string>") -> str:
"""
Format a value based on type and data.
By default, no strings are looked up and "<string>" is returned.
@ -936,7 +962,7 @@ class AXMLPrinter:
__charrange = None
__replacement = None
def __init__(self, raw_buff):
def __init__(self, raw_buff: bytes)->bytes:
logger.debug("AXMLPrinter")
self.axml = AXMLParser(raw_buff)
@ -1003,7 +1029,7 @@ class AXMLPrinter:
logger.warning("Not all namespace mappings were closed! Malformed AXML?")
break
def get_buff(self):
def get_buff(self) -> bytes:
"""
Returns the raw XML file without prettification applied.
@ -1011,7 +1037,7 @@ class AXMLPrinter:
"""
return self.get_xml(pretty=False)
def get_xml(self, pretty=True):
def get_xml(self, pretty:bool=True) -> bytes:
"""
Get the XML as an UTF-8 string
@ -1019,7 +1045,7 @@ class AXMLPrinter:
"""
return etree.tostring(self.root, encoding="utf-8", pretty_print=pretty)
def get_xml_obj(self):
def get_xml_obj(self) -> etree.Element:
"""
Get the XML as an ElementTree object
@ -1027,7 +1053,7 @@ class AXMLPrinter:
"""
return self.root
def is_valid(self):
def is_valid(self) -> bool:
"""
Return the state of the AXMLParser.
If this flag is set to False, the parsing has failed, thus
@ -1035,7 +1061,7 @@ class AXMLPrinter:
"""
return self.axml.is_valid()
def is_packed(self):
def is_packed(self) -> bool:
"""
Returns True if the AXML is likely to be packed
@ -1046,9 +1072,9 @@ class AXMLPrinter:
:returns: True if packer detected, False otherwise
"""
return self.packerwarning
return self.packerwarning or self.axml.packerwarning
def _get_attribute_value(self, index):
def _get_attribute_value(self, index: int):
"""
Wrapper function for format_value to resolve the actual value of an attribute in a tag
:param index: index of the current attribute
@ -1352,7 +1378,7 @@ class ARSCParser:
Each package is a chunk of type RES_TABLE_PACKAGE_TYPE.
It contains again many more chunks.
"""
def __init__(self, raw_buff):
def __init__(self, raw_buff:bytes) -> None:
"""
:param bytes raw_buff: the raw bytes of the file
"""
@ -1581,10 +1607,10 @@ class ARSCParser:
nb += 3 + nb_i - 1 # -1 to account for the nb+=1 on the next line
nb += 1
def get_resource_string(self, ate):
def get_resource_string(self, ate:ARSCResTableEntry) -> list:
return [ate.get_value(), ate.get_key_data()]
def get_resource_id(self, ate):
def get_resource_id(self, ate:ARSCResTableEntry) -> list[str]:
x = [ate.get_value()]
if ate.key.get_data() == 0:
x.append("false")
@ -1592,7 +1618,7 @@ class ARSCParser:
x.append("true")
return x
def get_resource_bool(self, ate):
def get_resource_bool(self, ate:ARSCResTableEntry) -> list[str]:
x = [ate.get_value()]
if ate.key.get_data() == 0:
x.append("false")
@ -1600,10 +1626,10 @@ class ARSCParser:
x.append("true")
return x
def get_resource_integer(self, ate):
def get_resource_integer(self, ate:ARSCResTableEntry) -> list:
return [ate.get_value(), ate.key.get_data()]
def get_resource_color(self, ate):
def get_resource_color(self, ate:ARSCResTableEntry) -> list:
entry_data = ate.key.get_data()
return [
ate.get_value(),
@ -1614,7 +1640,7 @@ class ARSCParser:
(entry_data & 0xFF))
]
def get_resource_dimen(self, ate):
def get_resource_dimen(self, ate:ARSCResTableEntry) -> list:
try:
return [
ate.get_value(), "{}{}".format(
@ -1628,17 +1654,17 @@ class ARSCParser:
return [ate.get_value(), ate.key.get_data()]
# FIXME
def get_resource_style(self, ate):
def get_resource_style(self, ate:ARSCResTableEntry) -> list:
return ["", ""]
def get_packages_names(self):
def get_packages_names(self) -> list[str]:
"""
Retrieve a list of all package names, which are available
in the given resources.arsc.
"""
return list(self.packages.keys())
def get_locales(self, package_name):
def get_locales(self, package_name:str) -> list[str]:
"""
Retrieve a list of all available locales in a given packagename.
@ -1647,7 +1673,7 @@ class ARSCParser:
self._analyse()
return list(self.values[package_name].keys())
def get_types(self, package_name, locale='\x00\x00'):
def get_types(self, package_name:str, locale:str='\x00\x00') -> list[str]:
"""
Retrieve a list of all types which are available in the given
package and locale.
@ -1658,7 +1684,7 @@ class ARSCParser:
self._analyse()
return list(self.values[package_name][locale].keys())
def get_public_resources(self, package_name, locale='\x00\x00'):
def get_public_resources(self, package_name:str, locale:str='\x00\x00') -> bytes:
"""
Get the XML (as string) of all resources of type 'public'.
@ -1684,7 +1710,7 @@ class ARSCParser:
return buff.encode('utf-8')
def get_string_resources(self, package_name, locale='\x00\x00'):
def get_string_resources(self, package_name:str, locale:str='\x00\x00') -> bytes:
"""
Get the XML (as string) of all resources of type 'string'.
@ -1713,7 +1739,7 @@ class ARSCParser:
return buff.encode('utf-8')
def get_strings_resources(self):
def get_strings_resources(self) -> bytes:
"""
Get the XML (as string) of all resources of type 'string'.
This is a combined variant, which has all locales and all package names
@ -1746,7 +1772,7 @@ class ARSCParser:
return buff.encode('utf-8')
def get_id_resources(self, package_name, locale='\x00\x00'):
def get_id_resources(self, package_name:str, locale:str='\x00\x00') -> bytes:
"""
Get the XML (as string) of all resources of type 'id'.
@ -1775,7 +1801,7 @@ class ARSCParser:
return buff.encode('utf-8')
def get_bool_resources(self, package_name, locale='\x00\x00'):
def get_bool_resources(self, package_name:str, locale:str='\x00\x00') -> bytes:
"""
Get the XML (as string) of all resources of type 'bool'.
@ -1800,7 +1826,7 @@ class ARSCParser:
return buff.encode('utf-8')
def get_integer_resources(self, package_name, locale='\x00\x00'):
def get_integer_resources(self, package_name:str, locale:str='\x00\x00') -> bytes:
"""
Get the XML (as string) of all resources of type 'integer'.
@ -1825,7 +1851,7 @@ class ARSCParser:
return buff.encode('utf-8')
def get_color_resources(self, package_name, locale='\x00\x00'):
def get_color_resources(self, package_name:str, locale:str='\x00\x00') -> bytes:
"""
Get the XML (as string) of all resources of type 'color'.
@ -1850,7 +1876,7 @@ class ARSCParser:
return buff.encode('utf-8')
def get_dimen_resources(self, package_name, locale='\x00\x00'):
def get_dimen_resources(self, package_name:str, locale:str='\x00\x00') -> bytes:
"""
Get the XML (as string) of all resources of type 'dimen'.
@ -1875,7 +1901,7 @@ class ARSCParser:
return buff.encode('utf-8')
def get_id(self, package_name, rid, locale='\x00\x00'):
def get_id(self, package_name:str, rid:int, locale:str='\x00\x00') -> tuple:
"""
Returns the tuple (resource_type, resource_name, resource_id)
for the given resource_id.
@ -1900,7 +1926,7 @@ class ARSCParser:
Resolves resources by ID and configuration.
This resolver deals with complex resources as well as with references.
"""
def __init__(self, android_resources, config=None):
def __init__(self, android_resources:ARSCParser, config:Union[ARSCResTableConfig,None]=None) -> None:
"""
:param ARSCParser android_resources: A resource parser
:param ARSCResTableConfig config: The desired configuration or None to resolve all.
@ -1908,7 +1934,7 @@ class ARSCParser:
self.resources = android_resources
self.wanted_config = config
def resolve(self, res_id):
def resolve(self, res_id: int) -> list[tuple[ARSCResTableConfig, str]]:
"""
the given ID into the Resource and returns a list of matching resources.
@ -1927,7 +1953,7 @@ class ARSCParser:
# deconstruct them and check if more candidates are generated
self.put_ate_value(result, ate, config)
def put_ate_value(self, result, ate, config):
def put_ate_value(self, result: list, ate:ARSCResTableEntry, config:ARSCResTableConfig) -> None:
"""
Put a ResTableEntry into the list of results
:param list result: results array
@ -1943,7 +1969,7 @@ class ARSCParser:
else:
self.put_item_value(result, ate.key, config, ate, complex_=False)
def put_item_value(self, result, item, config, parent, complex_):
def put_item_value(self, result:list, item:ARSCResStringPoolRef, config:ARSCResTableConfig, parent:ARSCResTableEntry, complex_:bool)->None:
"""
Put the tuple (ARSCResTableConfig, resolved string) into the result set
@ -1970,7 +1996,7 @@ class ARSCParser:
else:
result.append((config, item.format_value()))
def get_resolved_res_configs(self, rid, config=None):
def get_resolved_res_configs(self, rid:int, config:Union[ARSCTableResConfig, None]=None) -> list[tuple[ARSCResTableConfig, str]]:
"""
Return a list of resolved resource IDs with their corresponding configuration.
It has a similar return type as :meth:`get_res_configs` but also handles complex entries
@ -1986,7 +2012,7 @@ class ARSCParser:
resolver = ARSCParser.ResourceResolver(self, config)
return resolver.resolve(rid)
def get_resolved_strings(self):
def get_resolved_strings(self) -> list[str]:
self._analyse()
if self._resolved_strings:
return self._resolved_strings
@ -2021,7 +2047,7 @@ class ARSCParser:
self._resolved_strings = r
return r
def get_res_configs(self, rid, config=None, fallback=True):
def get_res_configs(self, rid:int, config:Union[ARSCResTableConfig,None]=None, fallback:bool=True) -> list[ARSCResTableConfig]:
"""
Return the resources found with the ID `rid` and select
the right one based on the configuration, or return all if no configuration was set.
@ -2039,7 +2065,7 @@ class ARSCParser:
:param rid: resource id as int
:param config: a config to resolve from, or None to get all results
:param fallback: Enable the fallback for resolving default configuration (default: True)
:return: a list of ARSCResTableConfig: ARSCResTableEntry
:return: a list of ARSCResTableConfig:
"""
self._analyse()
@ -2064,7 +2090,7 @@ class ARSCParser:
else:
return list(res_options.items())
def get_string(self, package_name, name, locale='\x00\x00'):
def get_string(self, package_name:str, name:str, locale:str='\x00\x00') -> Union[str,None]:
self._analyse()
try:
@ -2097,7 +2123,7 @@ class ARSCParser:
return result
@staticmethod
def parse_id(name):
def parse_id(name:str) -> tuple[str,str]:
"""
Resolves an id from a binary XML file in the form "@[package:]DEADBEEF"
and returns a tuple of package name and resource id.
@ -2130,7 +2156,7 @@ class ARSCParser:
except ValueError:
raise ValueError("ID is not a hex ID: '{}'".format(res_id))
def get_resource_xml_name(self, r_id, package=None):
def get_resource_xml_name(self, r_id:int, package:Union[str,None]=None) -> str:
"""
Returns the XML name for a resource, including the package name if package is None.
A full name might look like `@com.example:string/foobar`
@ -2168,7 +2194,7 @@ class ARSCParser:
class PackageContext:
def __init__(self, current_package, stringpool_main, mTableStrings, mKeyStrings):
def __init__(self, current_package: ARSCResTablePackage, stringpool_main: StringBlock, mTableStrings: StringBlock, mKeyStrings: StringBlock) -> None:
"""
:param ARSCResTablePackage current_package:
:param StringBlock stringpool_main:
@ -2180,13 +2206,13 @@ class PackageContext:
self.mKeyStrings = mKeyStrings
self.current_package = current_package
def get_mResId(self):
def get_mResId(self) -> int:
return self.current_package.mResId
def set_mResId(self, mResId):
def set_mResId(self, mResId: int) -> None:
self.current_package.mResId = mResId
def get_package_name(self):
def get_package_name(self) -> str:
return self.current_package.get_name()
def __repr__(self):
@ -2216,9 +2242,9 @@ class ARSCHeader:
# This is the minimal size such a header must have. There might be other header data too!
SIZE = 2 + 2 + 4
def __init__(self, buff, expected_type=None, possible_types=None):
def __init__(self, buff: BinaryIO, expected_type:Union[int,None]=None, possible_types:Union[set[int], None]=None) -> None:
"""
:param androguard.core.bytecode.BuffHandle buff: the buffer set to the position where the header starts.
:param buff: the buffer set to the position where the header starts.
:param int expected_type: the type of the header which is expected.
"""
self.start = buff.tell()
@ -2263,14 +2289,14 @@ class ARSCHeader:
self.start))
@property
def type(self):
def type(self) -> int:
"""
Type identifier for this chunk
"""
return self._type
@property
def header_size(self):
def header_size(self) -> int:
"""
Size of the chunk header (in bytes). Adding this value to
the address of the chunk allows you to find its associated data
@ -2279,7 +2305,7 @@ class ARSCHeader:
return self._header_size
@property
def size(self):
def size(self) -> int:
"""
Total size of this chunk (in bytes). This is the chunkSize plus
the size of any data associated with the chunk. Adding this value
@ -2290,7 +2316,7 @@ class ARSCHeader:
return self._size
@property
def end(self):
def end(self) -> int:
"""
Get the absolute offset inside the file, where the chunk ends.
This is equal to `ARSCHeader.start + ARSCHeader.size`.
@ -2310,7 +2336,7 @@ class ARSCResTablePackage:
See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#861
"""
def __init__(self, buff, header):
def __init__(self, buff: BinaryIO, header:ARSCHeader) -> None:
self.header = header
self.start = buff.tell()
self.id = unpack('<I', buff.read(4))[0]
@ -2321,7 +2347,7 @@ class ARSCResTablePackage:
self.lastPublicKey = unpack('<I', buff.read(4))[0]
self.mResId = self.id << 24
def get_name(self):
def get_name(self) -> None:
name = self.name.decode("utf-16", 'replace')
name = name[:name.find("\x00")]
return name
@ -2331,7 +2357,7 @@ class ARSCResTypeSpec:
"""
See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#1327
"""
def __init__(self, buff, parent=None):
def __init__(self, buff: BinaryIO, parent:Union[PackageContext, None]=None) -> None:
self.start = buff.tell()
self.parent = parent
self.id = unpack('<B', buff.read(1))[0]
@ -2358,7 +2384,7 @@ class ARSCResType:
See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#1364
"""
def __init__(self, buff, parent=None):
def __init__(self, buff: BinaryIO, parent:Union[PackageContext,None]=None) -> None:
self.start = buff.tell()
self.parent = parent
@ -2378,10 +2404,10 @@ class ARSCResType:
logger.debug("Parsed {}".format(self))
def get_type(self):
def get_type(self) -> str:
return self.parent.mTableStrings.getString(self.id - 1)
def get_package_name(self):
def get_package_name(self) -> str:
return self.parent.get_package_name()
def __repr__(self):
@ -2411,7 +2437,7 @@ class ARSCResTableConfig:
cls.DEFAULT = ARSCResTableConfig(None)
return cls.DEFAULT
def __init__(self, buff=None, **kwargs):
def __init__(self, buff:Union[BinaryIO, None]=None, **kwargs) -> None:
if buff is not None:
self.start = buff.tell()
@ -2599,7 +2625,7 @@ class ARSCResTableConfig:
return (_language + "-r" + _region) if _region else _language
return "\x00\x00"
def get_config_name_friendly(self):
def get_config_name_friendly(self) -> str:
"""
Here for legacy reasons.
@ -2607,7 +2633,7 @@ class ARSCResTableConfig:
"""
return self.get_qualifier()
def get_qualifier(self):
def get_qualifier(self) -> str:
"""
Return resource name qualifier for the current configuration.
for example
@ -2828,19 +2854,19 @@ class ARSCResTableConfig:
return "-".join(res)
def get_language(self):
def get_language(self) -> str:
x = self.locale & 0x0000ffff
return chr(x & 0x00ff) + chr((x & 0xff00) >> 8)
def get_country(self):
def get_country(self) -> str:
x = (self.locale & 0xffff0000) >> 16
return chr(x & 0x00ff) + chr((x & 0xff00) >> 8)
def get_density(self):
def get_density(self) -> str:
x = ((self.screenType >> 16) & 0xffff)
return x
def is_default(self):
def is_default(self) -> bool:
"""
Test if this is a default resource, which matches all
@ -2891,7 +2917,7 @@ class ARSCResTableEntry:
# linking with other resource tables.
FLAG_WEAK = 4
def __init__(self, buff, mResId, parent=None):
def __init__(self, buff:BinaryIO, mResId:int, parent:Union[PackageContext, None]=None) -> None:
self.start = buff.tell()
self.mResId = mResId
self.parent = parent
@ -2910,22 +2936,22 @@ class ARSCResTableEntry:
if self.is_weak():
logger.debug("Parsed {}".format(self))
def get_index(self):
def get_index(self) -> int:
return self.index
def get_value(self):
def get_value(self) -> str:
return self.parent.mKeyStrings.getString(self.index)
def get_key_data(self):
def get_key_data(self) -> str:
return self.key.get_data_value()
def is_public(self):
def is_public(self) -> bool:
return (self.flags & self.FLAG_PUBLIC) != 0
def is_complex(self):
def is_complex(self) -> bool:
return (self.flags & self.FLAG_COMPLEX) != 0
def is_weak(self):
def is_weak(self) -> bool:
return (self.flags & self.FLAG_WEAK) != 0
def __repr__(self):
@ -2948,7 +2974,7 @@ class ARSCComplex:
See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#1485 for `ResTable_map_entry`
and http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#1498 for `ResTable_map`
"""
def __init__(self, buff, parent=None):
def __init__(self, buff:BinaryIO, parent:Union[PackageContext,None]=None) -> None:
self.start = buff.tell()
self.parent = parent
@ -2973,7 +2999,7 @@ class ARSCResStringPoolRef:
See: http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#262
"""
def __init__(self, buff, parent=None):
def __init__(self, buff:BinaryIO, parent:Union[PackageContext, None]=None) -> None:
self.start = buff.tell()
self.parent = parent
@ -2988,19 +3014,19 @@ class ARSCResStringPoolRef:
except Exception as e:
logger.error(e)
def get_data_value(self):
def get_data_value(self) -> str:
return self.parent.stringpool_main.getString(self.data)
def get_data(self):
def get_data(self) -> int:
return self.data
def get_data_type(self):
def get_data_type(self) -> bytes:
return self.data_type
def get_data_type_string(self):
def get_data_type_string(self) -> str:
return TYPE_TABLE[self.data_type]
def format_value(self):
def format_value(self) -> str:
"""
Return the formatted (interpreted) data according to `data_type`.
"""
@ -3010,7 +3036,7 @@ class ARSCResStringPoolRef:
self.parent.stringpool_main.getString
)
def is_reference(self):
def is_reference(self) -> bool:
"""
Returns True if the Res_value is actually a reference to another resource
"""
@ -3024,7 +3050,7 @@ class ARSCResStringPoolRef:
self.data)
def get_arsc_info(arscobj):
def get_arsc_info(arscobj:ARSCParser) -> str:
"""
Return a string containing all resources packages ordered by packagename, locale and type.

View File

@ -1,13 +1,23 @@
# Allows type hinting of types not-yet-declared
# in Python >= 3.7
# see https://peps.python.org/pep-0563/
from __future__ import annotations
import hashlib
from xml.sax.saxutils import escape
import json
from struct import pack
import textwrap
import json
from loguru import logger
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
def _PrintBanner():
print_fct = CONF["PRINT_FCT"]
@ -106,7 +116,7 @@ def _colorize_operands(operands, colors):
yield "{}".format(repr(operands[1]))
def PrettyShow(basic_blocks, notes={}):
def PrettyShow(basic_blocks: list[DEXBasicBlock], notes:list=[]) -> None:
idx = 0
offset_color = CONF["COLORS"]["OFFSET"]
@ -126,6 +136,8 @@ def PrettyShow(basic_blocks, notes={}):
print_fct("{}{}{} : \n".format(bb_color, i.get_name(), normal_color))
instructions = list(i.get_instructions())
for ins in instructions:
# TODO: this seems wrong, notes is a list[str], but maybe it used to be a dict?
if nb in notes:
for note in notes[nb]:
_PrintNote(note, 1)
@ -185,7 +197,7 @@ def _get_operand_html(operand, registers_colors, colors):
The HTML should be compatible with pydot/graphviz to be used
inside a node label.
This is solely used in :func:`~androguard.core.bytecodes.method2dot`
This is solely used in :func:`~androguard.core.bytecode.method2dot`
:param operand: tuple containing the operand type and operands
:param dict register_colors: key: register number, value: register color
@ -222,7 +234,7 @@ def _get_operand_html(operand, registers_colors, colors):
return escape(str(operand[1]))
def method2dot(mx, colors=None):
def method2dot(mx: MethodAnalysis, colors:Union[dict[str,str],None]=None) -> str:
"""
Export analysis method to dot format.
@ -394,7 +406,11 @@ def method2dot(mx, colors=None):
return {'name': method_label, 'nodes': blocks_html, 'edges': edges_html}
def method2format(output, _format="png", mx=None, raw=None):
def method2format(
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`.
@ -472,7 +488,7 @@ def method2format(output, _format="png", mx=None, raw=None):
logger.error("Could not write graph image, ensure graphviz is installed!")
raise
def method2png(output, mx, raw=False):
def method2png(output:str, mx:MethodAnalysis, raw:Union[str,None]=None) -> None:
"""
Export method to a png file format
@ -490,7 +506,7 @@ def method2png(output, mx, raw=False):
method2format(output, "png", mx, buff)
def method2jpg(output, mx, raw=False):
def method2jpg(output:str, mx:MethodAnalysis, raw:Union[str,None]=None) -> None:
"""
Export method to a jpg file format
@ -508,12 +524,12 @@ def method2jpg(output, mx, raw=False):
method2format(output, "jpg", mx, buff)
def vm2json(vm):
def vm2json(vm:DEX) -> str:
"""
Get a JSON representation of a DEX file
:param vm: :class:`~androguard.core.bytecodes.dvm.DEX`
:return:
:param vm: :class:`~androguard.core.dex.DEX`
:return: str
"""
d = {"name": "root", "children": []}
@ -531,31 +547,31 @@ def vm2json(vm):
class TmpBlock:
def __init__(self, name):
def __init__(self, name:str) -> None:
self.name = name
def get_name(self):
def get_name(self) -> str:
return self.name
def method2json(mx, directed_graph=False):
def method2json(mx: MethodAnalysis, directed_graph:bool=False) -> str:
"""
Create directed or undirected graph in the json format.
:param mx: :class:`~androguard.core.analysis.analysis.MethodAnalysis`
:param directed_graph: True if a directed graph should be created (default: False)
:return:
:return: str
"""
if directed_graph:
return method2json_direct(mx)
return method2json_undirect(mx)
def method2json_undirect(mx):
def method2json_undirect(mx: MethodAnalysis) -> str:
"""
:param mx: :class:`~androguard.core.analysis.analysis.MethodAnalysis`
:return:
:return: str
"""
d = {}
reports = []
@ -583,7 +599,7 @@ def method2json_undirect(mx):
return json.dumps(d)
def method2json_direct(mx):
def method2json_direct(mx: MethodAnalysis) -> str:
"""
:param mx: :class:`~androguard.core.analysis.analysis.MethodAnalysis`
@ -677,7 +693,7 @@ def method2json_direct(mx):
return json.dumps(d)
def object_to_bytes(obj):
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.
@ -696,7 +712,7 @@ def object_to_bytes(obj):
return obj.get_raw()
def FormatClassToJava(i):
def FormatClassToJava(i:str) -> str:
"""
Transform a java class name into the typed variant found in DEX files.
@ -711,7 +727,7 @@ def FormatClassToJava(i):
return "L" + i.replace(".", "/") + ";"
def FormatClassToPython(i):
def FormatClassToPython(i:str) -> str:
"""
Transform a typed class name into a form which can be used as a python
attribute
@ -731,7 +747,7 @@ def FormatClassToPython(i):
return i
def get_package_class_name(name):
def get_package_class_name(name:str)->tuple[str, str]:
"""
Return package and class name in a java variant from a typed variant name.
@ -773,7 +789,7 @@ def get_package_class_name(name):
return package, clsname
def FormatNameToPython(i):
def FormatNameToPython(i:str) -> str:
"""
Transform a (method) name into a form which can be used as a python
attribute
@ -794,7 +810,7 @@ def FormatNameToPython(i):
return i
def FormatDescriptorToPython(i):
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

@ -16,6 +16,7 @@
# limitations under the License.
from collections import defaultdict
from androguard.decompiler.opcode_ins import INSTRUCTION_SET
from androguard.decompiler.instruction import MoveExceptionExpression
from androguard.decompiler.node import Node
@ -25,7 +26,7 @@ from loguru import logger
class BasicBlock(Node):
def __init__(self, name, block_ins):
def __init__(self, name: str, block_ins: list) -> None:
super().__init__(name)
self.ins = block_ins
self.ins_range = None
@ -33,26 +34,26 @@ class BasicBlock(Node):
self.var_to_declare = set()
self.catch_type = None
def get_ins(self):
def get_ins(self) -> list:
return self.ins
def get_loc_with_ins(self):
def get_loc_with_ins(self) -> list:
if self.loc_ins is None:
self.loc_ins = list(zip(range(*self.ins_range), self.ins))
return self.loc_ins
def remove_ins(self, loc, ins):
def remove_ins(self, loc, ins) -> None:
self.ins.remove(ins)
self.loc_ins.remove((loc, ins))
def add_ins(self, new_ins_list):
def add_ins(self, new_ins_list: list) -> None:
for new_ins in new_ins_list:
self.ins.append(new_ins)
def add_variable_declaration(self, variable):
self.var_to_declare.add(variable)
def number_ins(self, num):
def number_ins(self, num: int) -> int:
last_ins_num = num + len(self.ins)
self.ins_range = [num, last_ins_num]
self.loc_ins = None

View File

@ -22,7 +22,7 @@ from androguard.core.dex.dex_types import TYPE_DESCRIPTOR
from loguru import logger
def array_access(arr, ind):
def array_access(arr, ind) -> list:
return ['ArrayAccess', [arr, ind]]
@ -68,7 +68,7 @@ def parenthesis(expr):
return ['Parenthesis', [expr]]
def typen(baset, dim):
def typen(baset: str, dim: int) -> list:
return ['TypeName', (baset, dim)]
@ -141,7 +141,7 @@ def _append(sb, stmt):
sb[2].append(stmt)
def parse_descriptor(desc):
def parse_descriptor(desc: str) -> list:
dim = 0
while desc and desc[0] == '[':
desc = desc[1:]

View File

@ -15,6 +15,15 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# Allows type hinting of types not-yet-declared
# 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
import struct
import sys
from collections import defaultdict
@ -48,7 +57,7 @@ from androguard.util import readFile
logger.add(sys.stderr, format="{time} {level} {message}", filter="dad", level="INFO")
# No seperate DvField class currently
def get_field_ast(field):
def get_field_ast(field: EncodedField) -> dict:
triple = field.get_class_name()[1:-1], field.get_name(), field.get_descriptor()
expr = None
@ -73,11 +82,11 @@ def get_field_ast(field):
class DvMethod:
"""
This is a wrapper around :class:`~androguard.core.analysis.analysis.MethodAnalysis` and
:class:`~androguard.core.bytecodes.dvm.EncodedMethod` inside the decompiler.
:class:`~androguard.core.dex.EncodedMethod` inside the decompiler.
:param androguard.core.analysis.analysis.MethodAnalysis methanalysis:
"""
def __init__(self, methanalysis):
def __init__(self, methanalysis: MethodAnalysis) -> None:
method = methanalysis.get_method()
self.method = method
self.start_block = next(methanalysis.get_basic_blocks().get(), None)
@ -119,7 +128,7 @@ class DvMethod:
# 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=False):
def process(self, doAST:bool=False) -> None:
"""
Processes the method and decompile the code.
@ -193,7 +202,7 @@ class DvMethod:
self.writer = Writer(graph, self)
self.writer.write_method()
def get_ast(self):
def get_ast(self) -> dict:
"""
Returns the AST, if previously was generated by calling :meth:`process` with argument :code:`doAST=True`.
@ -212,15 +221,15 @@ class DvMethod:
"""
return self.ast
def show_source(self):
def show_source(self) -> None:
print(self.get_source())
def get_source(self):
def get_source(self) -> str:
if self.writer:
return str(self.writer)
return ''
def get_source_ext(self):
def get_source_ext(self) -> list[tuple]:
if self.writer:
return self.writer.str_ext()
return []
@ -234,13 +243,13 @@ class DvClass:
"""
This is a wrapper for :class:`~androguard.core.bytecodes.dvm.ClassDefItem` inside the decompiler.
At first, :py:attr:`methods` contains a list of :class:`~androguard.core.bytecodes.dvm.EncodedMethods`,
At first, :py:attr:`methods` contains a list of :class:`~androguard.core.dex.EncodedMethod`,
which are successively replaced by :class:`DvMethod` in the process of decompilation.
:param androguard.core.bytecodes.dvm.ClassDefItem dvclass: the class item
:param androguard.core.dex.ClassDefItem dvclass: the class item
:param androguard.core.analysis.analysis.Analysis vma: an Analysis object
"""
def __init__(self, dvclass, vma):
def __init__(self, dvclass: dex.ClassDefItem, vma: analysis.Analysis) -> None:
name = dvclass.get_name()
if name.find('/') > 0:
pckg, name = name.rsplit('/', 1)
@ -277,10 +286,10 @@ class DvClass:
logger.debug('%s (%s, %s)', meth.get_method_idx(), self.name, meth.name)
logger.debug('')
def get_methods(self):
def get_methods(self) -> list[dex.EncodedMethod]:
return self.methods
def process_method(self, num, doAST=False):
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))
@ -288,7 +297,7 @@ class DvClass:
else:
method.process(doAST=doAST)
def process(self, doAST=False):
def process(self, doAST:bool=False) -> None:
for i in range(len(self.methods)):
try:
self.process_method(i, doAST=doAST)
@ -296,7 +305,7 @@ class DvClass:
# FIXME: too broad exception?
logger.warning('Error decompiling method %s: %s', self.methods[i], e)
def get_ast(self):
def get_ast(self) -> dict:
fields = [get_field_ast(f) for f in self.fields]
methods = []
for m in self.methods:
@ -314,7 +323,7 @@ class DvClass:
'methods': methods,
}
def get_source(self):
def get_source(self) -> str:
source = []
if not self.inner and self.package:
source.append('package %s;\n' % self.package)
@ -362,7 +371,7 @@ class DvClass:
source.append('}\n')
return ''.join(source)
def get_source_ext(self):
def get_source_ext(self) -> list[tuple[str, list]]:
source = []
if not self.inner and self.package:
source.append(
@ -434,7 +443,7 @@ class DvClass:
source.append(("CLASS_END", [('CLASS_END', '}\n')]))
return source
def show_source(self):
def show_source(self) -> None:
print(self.get_source())
def __repr__(self):
@ -449,10 +458,10 @@ class DvMachine:
The :class:`~androguard.decompiler.decompile.DvMachine` can take either an APK file directly,
where all DEX files from the multidex are used, or a single DEX or ODEX file as an argument.
At first, :py:attr:`classes` contains only :class:`~androguard.core.bytecodes.dvm.ClassDefItem` as values.
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):
def __init__(self, name: str) -> None:
"""
:param name: filename to load
@ -475,7 +484,7 @@ class DvMachine:
# TODO why not?
# util.merge_inner(self.classes)
def get_classes(self):
def get_classes(self) -> list[str]:
"""
Return a list of classnames contained in this machine.
The format of each name is Lxxx;
@ -484,7 +493,7 @@ class DvMachine:
"""
return list(self.classes.keys())
def get_class(self, class_name):
def get_class(self, class_name: str) -> DvClass:
"""
Return the :class:`DvClass` with the given name
@ -493,7 +502,7 @@ class DvMachine:
:param str class_name:
:return: the class matching on the name
:rtype: DvClass
:rtype: :class:`DvClass`
"""
for name, klass in self.classes.items():
# TODO why use the name partially?
@ -503,7 +512,7 @@ class DvMachine:
dvclass = self.classes[name] = DvClass(klass, self.vma)
return dvclass
def process(self):
def process(self) -> None:
"""
Process all classes inside the machine.
@ -517,7 +526,7 @@ class DvMachine:
dvclass = self.classes[name] = DvClass(klass, self.vma)
dvclass.process()
def show_source(self):
def show_source(self) -> None:
"""
Calls `show_source` on all classes inside the machine.
This prints the source to stdout.
@ -527,7 +536,7 @@ class DvMachine:
for klass in self.classes.values():
klass.show_source()
def process_and_show(self):
def process_and_show(self) -> None:
"""
Run :meth:`process` and :meth:`show_source` after each other.
"""
@ -538,7 +547,7 @@ class DvMachine:
klass.process()
klass.show_source()
def get_ast(self):
def get_ast(self) -> dict:
"""
Processes each class with AST enabled and returns a dictionary with all single ASTs
Classnames as keys.

View File

@ -15,6 +15,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# Allows type hinting of types not-yet-declared
# 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 DEX, ClassDefItem
from androguard.decompiler import decompile
from pygments import highlight
@ -26,7 +36,7 @@ from loguru import logger
class DecompilerDAD:
def __init__(self, vm, vmx):
def __init__(self, vm: DEX, vmx: Analysis) -> None:
"""
Decompiler wrapper for DAD: **D**AD is **A** **D**ecompiler
DAD is the androguard internal decompiler.
@ -35,25 +45,25 @@ class DecompilerDAD:
creates :class:`~androguard.decompiler.decompile.DvClass` and
:class:`~androguard.decompiler.decompile.DvMethod` on demand.
:param androguard.core.bytecodes.dvm.DEX vm: `DEX` object
:param androguard.core.bytecodes.DEX vm: `DEX` object
:param androguard.core.analysis.analysis.Analysis vmx: `Analysis` object
"""
self.vm = vm
self.vmx = vmx
def get_source_method(self, m):
def get_source_method(self, m: MethodAnalysis) -> str:
mx = self.vmx.get_method(m)
z = decompile.DvMethod(mx)
z.process()
return z.get_source()
def get_ast_method(self, m):
def get_ast_method(self, m: MethodAnalysis) -> dict:
mx = self.vmx.get_method(m)
z = decompile.DvMethod(mx)
z.process(doAST=True)
return z.get_ast()
def display_source(self, m):
def display_source(self, m: MethodAnalysis) -> None:
result = self.get_source_method(m)
lexer = get_lexer_by_name("java", stripall=True)
@ -61,17 +71,17 @@ class DecompilerDAD:
result = highlight(result, lexer, formatter)
print(result)
def get_source_class(self, _class):
def get_source_class(self, _class: ClassDefItem) -> str:
c = decompile.DvClass(_class, self.vmx)
c.process()
return c.get_source()
def get_ast_class(self, _class):
def get_ast_class(self, _class: ClassDefItem) -> dict:
c = decompile.DvClass(_class, self.vmx)
c.process(doAST=True)
return c.get_ast()
def get_source_class_ext(self, _class):
def get_source_class_ext(self, _class: ClassDefItem) -> list[tuple[str, list]]:
c = decompile.DvClass(_class, self.vmx)
c.process()
@ -79,7 +89,7 @@ class DecompilerDAD:
return result
def display_all(self, _class):
def display_all(self, _class: ClassDefItem) -> None:
result = self.get_source_class(_class)
lexer = get_lexer_by_name("java", stripall=True)

View File

@ -15,6 +15,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# Allows type hinting of types not-yet-declared
# 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
from loguru import logger
TYPE_DESCRIPTOR = {
@ -77,19 +85,19 @@ ACCESS_ORDER = [0x1, 0x4, 0x2, 0x400, 0x8, 0x10, 0x80, 0x40, 0x20, 0x100, 0x800,
TYPE_LEN = {'J': 2, 'D': 2, }
def get_access_class(access):
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]
def get_access_method(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]
def get_access_field(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]
@ -165,7 +173,7 @@ def get_type_size(param):
return TYPE_LEN.get(param, 1)
def get_type(atype, size=None):
def get_type(atype:str, size:int=None) -> str:
"""
Retrieve the java type of a descriptor (e.g : I)
"""
@ -187,7 +195,7 @@ def get_type(atype, size=None):
return res
def get_params_type(descriptor):
def get_params_type(descriptor: str) -> list[str]:
"""
Return the parameters type of a descriptor (e.g (IC)V)
"""
@ -197,7 +205,7 @@ def get_params_type(descriptor):
return []
def create_png(cls_name, meth_name, graph, dir_name='graphs2'):
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

@ -50,7 +50,7 @@ class Writer:
def __str__(self):
return ''.join([str(i) for i in self.buffer])
def str_ext(self):
def str_ext(self) -> list[tuple]:
return self.buffer2
def inc_ind(self, i=1):

View File

@ -1,16 +1,21 @@
# Allows type hinting of types not-yet-declared
# in Python >= 3.7
# see https://peps.python.org/pep-0563/
from __future__ import annotations
import hashlib
import os
import re
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.analysis.analysis import Analysis
import hashlib
import re
import os
from loguru import logger
def get_default_session():
def get_default_session() -> Session:
"""
Return the default Session from the configuration
or create a new one, if the session in the configuration is None.
@ -22,7 +27,7 @@ def get_default_session():
return androconf.CONF["SESSION"]
def AnalyzeAPK(_file, session=None, raw=False):
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!
@ -36,7 +41,7 @@ def AnalyzeAPK(_file, session=None, raw=False):
:type _file: string (for filename) or bytes (for raw)
:param session: A session (default: None)
:param raw: boolean if raw bytes are supplied instead of a filename
:rtype: return the :class:`~androguard.core.apk.APK`, list of :class:`~androguard.core.dvm.DEX`, and :class:`~androguard.core.analysis.analysis.Analysis` objects
:rtype: return the :class:`~androguard.core.apk.APK`, list of :class:`~androguard.core.dex.DEX`, and :class:`~androguard.core.analysis.analysis.Analysis` objects
"""
logger.debug("AnalyzeAPK")
@ -71,7 +76,7 @@ def AnalyzeAPK(_file, session=None, raw=False):
return a, d, dx
def AnalyzeDex(filename, session=None, raw=False):
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 !
@ -96,32 +101,32 @@ def AnalyzeDex(filename, session=None, raw=False):
return session.addDEX(filename, data)
def AnalyzeODex(filename, session=None, raw=False):
"""
Analyze an android odex file and setup all stuff for a more quickly analysis !
# def AnalyzeODex(filename: str, session:Session=None, raw:bool=False):
# """
# Analyze an android odex file and setup all stuff for a more quickly analysis !
:param filename: the filename of the android dex file or a buffer which represents the dex file
:type filename: string
:param session: The Androguard Session to add the ODex to (default: None)
:param raw: If set, ``filename`` will be used as the odex's data (bytes). Defaults to ``False``
# :param filename: the filename of the android dex file or a buffer which represents the dex file
# :type filename: string
# :param session: The Androguard Session to add the ODex to (default: None)
# :param raw: If set, ``filename`` will be used as the odex's data (bytes). Defaults to ``False``
:rtype: return a tuple of (sha256hash, :class:`DalvikOdexVMFormat`, :class:`Analysis`)
"""
logger.debug("AnalyzeODex")
# :rtype: return a tuple of (sha256hash, :class:`DalvikOdexVMFormat`, :class:`Analysis`)
# """
# logger.debug("AnalyzeODex")
if not session:
session = get_default_session()
# if not session:
# session = get_default_session()
if raw:
data = filename
else:
with open(filename, "rb") as fd:
data = fd.read()
# if raw:
# data = filename
# else:
# with open(filename, "rb") as fd:
# data = fd.read()
return session.addDEY(filename, data)
# return session.addDEY(filename, data) # <- this function is missing
def clean_file_name(filename, unique=True, replace="_", force_nt=False):
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 <, /, ?, ...

View File

@ -1,12 +1,13 @@
from androguard.core.analysis.analysis import Analysis
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
import hashlib
import collections
import dataset
from loguru import logger
@ -22,7 +23,7 @@ class Session:
>>> Should we go back to pickling or proceed further with the dataset ?<<<
"""
def __init__(self, export_ipython=False):
def __init__(self, export_ipython:bool=False) -> None:
"""
Create a new Session object
@ -44,7 +45,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=None):
def save(self, filename:Union[str,None]=None) -> None:
"""
Save the current session, see also :func:`~androguard.session.Save`.
"""
@ -67,13 +68,13 @@ class Session:
# files as well, but we do not remove it here for legacy reasons
self.analyzed_dex = dict()
def reset(self):
def reset(self) -> None:
"""
Reset the current session, delete all added files.
"""
self._setup_objects()
def isOpen(self):
def isOpen(self) -> bool:
"""
Test if any file was analyzed in this session
@ -81,7 +82,7 @@ class Session:
"""
return len(self.analyzed_digest) > 0
def show(self):
def show(self) -> None:
"""
Print information to stdout about the current session.
Gets all APKs, all DEX files and all Analysis objects.
@ -106,7 +107,7 @@ class Session:
self.table_system.insert(
dict(session_id=str(self.session_id), call=call, callee=callee, information=information, params=params))
def addAPK(self, filename, data):
def addAPK(self, filename: str, data:bytes) -> tuple[str, apk.APK]:
"""
Add an APK file to the Session and run analysis on it.
@ -139,7 +140,7 @@ class Session:
logger.info("added APK {}:{}".format(filename, digest))
return digest, newapk
def addDEX(self, filename, data, dx=None, postpone_xref=False):
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.
@ -184,7 +185,7 @@ class Session:
return digest, d, dx
def addODEX(self, filename, data, dx=None):
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
"""
@ -220,7 +221,7 @@ class Session:
return digest, d, dx
def add(self, filename, raw_data=None, dx=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.
@ -258,7 +259,7 @@ class Session:
return digest
def get_classes(self):
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.
"""
@ -268,7 +269,7 @@ class Session:
filename = self.analyzed_digest[digest]
yield idx, filename, digest, vm.get_classes()
def get_analysis(self, current_class):
def get_analysis(self, current_class: dex.ClassDefItem) -> Analysis:
"""
Returns the :class:`~androguard.core.analysis.analysis.Analysis` object
which contains the `current_class`.
@ -283,7 +284,7 @@ class Session:
return dx
return None
def get_format(self, current_class):
def get_format(self, current_class: dex.ClassDefItem) -> dex.DEX:
"""
Returns the :class:`~androguard.core.bytecodes.dvm.DEX` of a
given :class:`~androguard.core.bytecodes.dvm.ClassDefItem`.
@ -292,7 +293,7 @@ class Session:
"""
return current_class.CM.vm
def get_filename_by_class(self, current_class):
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.
@ -308,7 +309,7 @@ class Session:
return self.analyzed_digest[digest]
return None
def get_digest_by_class(self, current_class):
def get_digest_by_class(self, current_class: dex.ClassDefItem) -> Union[str,None]:
"""
Return the SHA256 hash of the object containing the ClassDefItem
@ -321,7 +322,7 @@ class Session:
return digest
return None
def get_strings(self):
def get_strings(self) -> Iterator[tuple[str, str, dict[str,StringAnalysis]]]:
"""
Yields all StringAnalysis for all unique Analysis objects
"""
@ -332,7 +333,7 @@ class Session:
seen.append(dx)
yield digest, self.analyzed_digest[digest], dx.get_strings_analysis()
def get_nb_strings(self):
def get_nb_strings(self) -> int:
"""
Return the total number of strings in all Analysis objects
"""
@ -345,7 +346,7 @@ class Session:
nb += len(dx.get_strings_analysis())
return nb
def get_all_apks(self):
def get_all_apks(self) -> Iterator[tuple[str, apk.APK]]:
"""
Yields a list of tuples of SHA256 hash of the APK and APK objects
of all analyzed APKs in the Session.
@ -353,7 +354,7 @@ class Session:
for digest, a in self.analyzed_apk.items():
yield digest, a
def get_objects_apk(self, filename=None, digest=None):
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.
@ -391,7 +392,7 @@ class Session:
dx = self.analyzed_vms[digest]
return a, dx.vms, dx
def get_objects_dex(self):
def get_objects_dex(self) -> Iterator[tuple[str, dex.DEX, Analysis]]:
"""
Yields all dex objects inclduing their Analysis objects

View File

@ -1,12 +1,15 @@
#  External dependecies
import asn1crypto
import sys
from typing import Union, BinaryIO
#  External dependecies
# import asn1crypto
from asn1crypto.x509 import Name
from loguru import logger
class MyFilter:
def __init__(self, level):
def __init__(self, level:int) -> None:
self.level = level
def __call__(self, record):
@ -14,7 +17,7 @@ class MyFilter:
return record["level"].no >= levelno
def set_log(level):
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
@ -26,7 +29,7 @@ def set_log(level):
# Stuff that might be useful
def read_at(buff, offset, size=-1):
def read_at(buff: BinaryIO, offset:int, size:int=-1) -> bytes:
idx = buff.tell()
buff.seek(offset)
d = buff.read(size)
@ -34,7 +37,7 @@ def read_at(buff, offset, size=-1):
return d
def readFile(filename, binary=True):
def readFile(filename: str, binary:bool=True) -> bytes:
"""
Open and read a file
:param filename: filename to open and read
@ -45,7 +48,7 @@ def readFile(filename, binary=True):
return f.read()
def get_certificate_name_string(name, short=False, delimiter=', '):
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.
@ -59,7 +62,7 @@ def get_certificate_name_string(name, short=False, delimiter=', '):
:rtype: str
"""
if isinstance(name, asn1crypto.x509.Name):
if isinstance(name, Name):#asn1crypto.x509.Name):
name = name.native
# For the shortform, we have a lookup table

View File

@ -36,6 +36,7 @@ PyQt5-Qt5 = [
{version = "5.15.2", markers = "sys_platform != 'darwin'"}
]
pyyaml = "*"
oscrypto = ">=1.3.0"
[tool.setuptools.package_data]
"androguard.core.api_specific_resources" = ["aosp_permissions/*.json", "api_permission_mappings/*.json"]

View File

@ -14,3 +14,4 @@ matplotlib
networkx
PyQt5
pyyaml
oscrypto>=1.3.0

View File

@ -299,8 +299,8 @@ class AnalysisTest(unittest.TestCase):
sa = dx.strings['Hello world']
self.assertEqual(len(sa.get_xref_from()), 1)
self.assertEqual(len(sa.get_xref_from(withoffset=True)), 1)
self.assertEqual(next(iter(sa.get_xref_from(withoffset=True)))[2], 4) # offset is 4
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
def testXrefOffsetsFields(self):
"""Tests if Field offsets in bytecode are correctly stored"""
@ -315,26 +315,26 @@ class AnalysisTest(unittest.TestCase):
afield = next(dx.find_fields(fieldname='afield'))
self.assertEqual(len(afield.get_xref_read()), 1) # always same method
self.assertEqual(len(afield.get_xref_read(withoffset=True)), 2)
self.assertListEqual(list(sorted(map(itemgetter(2), afield.get_xref_read(withoffset=True)))), [4, 40])
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(withoffset=True)))),
afield.get_xref_read(with_offset=True)))),
["foonbar", "foonbar"])
self.assertEqual(len(afield.get_xref_write()), 2)
self.assertEqual(len(afield.get_xref_write(withoffset=True)), 2)
self.assertListEqual(list(sorted(map(itemgetter(2), afield.get_xref_write(withoffset=True)))), [10, 32])
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(withoffset=True))))),
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(withoffset=True))))),
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(withoffset=True))))),
cfield.get_xref_read(with_offset=True))))),
sorted(["foonbar"]))
def testPermissions(self):