def testAPKManifest(self): from androguard.core.bytecodes.apk import APK a = APK("examples/android/TestsAndroguard/bin/TestActivity.apk", testzip=True) self.assertEqual(a.get_app_name(), "TestsAndroguardApplication") self.assertEqual(a.get_app_icon(), "res/drawable-hdpi/icon.png") self.assertEqual(a.get_app_icon(max_dpi=120), "res/drawable-ldpi/icon.png") self.assertEqual(a.get_app_icon(max_dpi=160), "res/drawable-mdpi/icon.png") self.assertEqual(a.get_app_icon(max_dpi=240), "res/drawable-hdpi/icon.png") self.assertIsNone(a.get_app_icon(max_dpi=1)) self.assertEqual(a.get_main_activity(), "tests.androguard.TestActivity") self.assertEqual(a.get_package(), "tests.androguard") self.assertEqual(a.get_androidversion_code(), '1') self.assertEqual(a.get_androidversion_name(), "1.0") self.assertEqual(a.get_min_sdk_version(), "9") self.assertEqual(a.get_target_sdk_version(), "16") self.assertIsNone(a.get_max_sdk_version()) self.assertEqual(a.get_permissions(), []) self.assertEqual(a.get_declared_permissions(), []) self.assertTrue(a.is_valid_APK())
def testAdaptiveIcon(self): # See https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive.html from androguard.core.bytecodes.apk import APK from androguard.core.bytecodes.axml import AXMLPrinter a = APK("examples/tests/com.android.example.text.styling.apk") self.assertEqual(a.get_app_icon(), "res/mipmap-anydpi-v26/ic_launcher.xml") x = AXMLPrinter(a.get_file(a.get_app_icon())).get_xml().decode("UTF-8") self.assertIn("adaptive-icon", x) # * ldpi (low) ~120dpi # * mdpi (medium) ~160dpi # * hdpi (high) ~240dpi # * xhdpi (extra-high) ~320dpi # * xxhdpi (extra-extra-high) ~480dpi # * xxxhdpi (extra-extra-extra-high) ~640dpi self.assertIsNone(a.get_app_icon(max_dpi=120)) # No LDPI icon self.assertIn("mdpi", a.get_app_icon(max_dpi=160)) self.assertIn("hdpi", a.get_app_icon(max_dpi=240)) self.assertIn("xhdpi", a.get_app_icon(max_dpi=320)) self.assertIn("xxhdpi", a.get_app_icon(max_dpi=480)) self.assertIn("xxxhdpi", a.get_app_icon(max_dpi=640)) self.assertIn(".png", a.get_app_icon(max_dpi=65533)) self.assertIn(".xml", a.get_app_icon(max_dpi=65534))
def testAdaptiveIcon(self): # See https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive.html from androguard.core.bytecodes.apk import APK from androguard.core.bytecodes.axml import AXMLPrinter a = APK("examples/tests/com.android.example.text.styling.apk") self.assertEqual(a.get_app_icon(), "res/mipmap-anydpi-v26/ic_launcher.xml") x = AXMLPrinter(a.get_file(a.get_app_icon())).get_xml().decode("UTF-8") self.assertIn("adaptive-icon", x) # * ldpi (low) ~120dpi # * mdpi (medium) ~160dpi # * hdpi (high) ~240dpi # * xhdpi (extra-high) ~320dpi # * xxhdpi (extra-extra-high) ~480dpi # * xxxhdpi (extra-extra-extra-high) ~640dpi self.assertIsNone(a.get_app_icon(max_dpi=120)) # No LDPI icon self.assertIn("mdpi", a.get_app_icon(max_dpi=160)) self.assertIn("hdpi", a.get_app_icon(max_dpi=240)) self.assertIn("xhdpi", a.get_app_icon(max_dpi=320)) self.assertIn("xxhdpi", a.get_app_icon(max_dpi=480)) self.assertIn("xxxhdpi", a.get_app_icon(max_dpi=640)) self.assertIn(".png", a.get_app_icon(max_dpi=65533)) self.assertIn(".xml", a.get_app_icon(max_dpi=65534))
def get_apk_info(self): apk = APK(self.apk_file) app_icon_file = apk.get_app_icon() app_icon_data = apk.get_file(app_icon_file) size = (256, 256) buffered = BytesIO() im = Image.open(BytesIO(app_icon_data)) im = im.resize(size, Image.ANTIALIAS) im.save(buffered, "PNG") app_icon_b64 = "data:image/png;base64," + base64.b64encode( buffered.getvalue()).decode('utf-8') self.package_name = apk.get_package() self.app_name = apk.get_app_name() self.report_saver.package_name = self.package_name self.report_saver.app_name = self.app_name self.report_saver.version = apk.get_androidversion_code() self.report_saver.app_icon = app_icon_b64 permission_parser = PermissionParser(mode='groups') permission_values = permission_parser.transform( apk.get_permissions()).flatten().tolist() permission_labels = permission_parser.labels() self.report_saver.permissions_actual = { permission_labels[i]: bool(v) for i, v in enumerate(permission_values) }
def testAPKManifest(self): from androguard.core.bytecodes.apk import APK a = APK("examples/android/TestsAndroguard/bin/TestActivity.apk", testzip=True) self.assertEqual(a.get_app_name(), "TestsAndroguardApplication") self.assertEqual(a.get_app_icon(), "res/drawable-hdpi/icon.png") self.assertEqual(a.get_app_icon(max_dpi=120), "res/drawable-ldpi/icon.png") self.assertEqual(a.get_app_icon(max_dpi=160), "res/drawable-mdpi/icon.png") self.assertEqual(a.get_app_icon(max_dpi=240), "res/drawable-hdpi/icon.png") self.assertIsNone(a.get_app_icon(max_dpi=1)) self.assertEqual(a.get_main_activity(), "tests.androguard.TestActivity") self.assertEqual(a.get_package(), "tests.androguard") self.assertEqual(a.get_androidversion_code(), '1') self.assertEqual(a.get_androidversion_name(), "1.0") self.assertEqual(a.get_min_sdk_version(), "9") self.assertEqual(a.get_target_sdk_version(), "16") self.assertIsNone(a.get_max_sdk_version()) self.assertEqual(a.get_permissions(), []) self.assertEqual(a.get_declared_permissions(), []) self.assertTrue(a.is_valid_APK())
def get_apk_information(_file): ''' get apk information by androguard params: _file: apk file path return: 1. apkname: apk name 2. apkcode: apk version code, eg: 222, 333 3. apkiconpath: get icon path ''' apkinformation = APK(_file) apkname = apkinformation.get_app_name() apkcode = apkinformation.get_androidversion_code() apkiconpath = apkinformation.get_app_icon() # packagename = apkinformation.get_package() return apkname, apkcode, apkiconpath
def get_apk_info(self): apk = APK(self.apk_file) app_icon_file = apk.get_app_icon() app_icon_data = apk.get_file(app_icon_file) size = (256, 256) buffered = BytesIO() im = Image.open(BytesIO(app_icon_data)) im = im.resize(size, Image.ANTIALIAS) im.save(buffered, "PNG") app_icon_b64 = "data:image/png;base64," + base64.b64encode( buffered.getvalue()).decode('utf-8') self.package_name = apk.get_package() self.app_name = apk.get_app_name() self.report_saver.package_name = self.package_name self.report_saver.app_name = self.app_name self.report_saver.version = apk.get_androidversion_code() self.report_saver.app_icon = app_icon_b64
class XAPK: def __init__(self, folder): self.folder = Path(folder) for x in self.folder.glob('*.apk'): self.apk_src = Path(x) break for x in self.folder.glob('*.obb'): self.obb_src = Path(x) break self.apk = APK(self.apk_src) self.manifest = self.make_manifest() self.icon = self.apk.get_file(self.apk.get_app_icon()) def make_manifest(self): apk_size = self.apk_src.stat().st_size if self.obb_src: obb_size = self.obb_src.stat().st_size else: obb_size = 0 total_size = apk_size + obb_size filename = self.apk.get_filename() manifest = {} manifest['xapk_version'] = 1 manifest['package_name'] = self.apk.get_package() manifest['name'] = self.apk.get_app_name() # manifest['locales_name'] = {} # TODO manifest['version_code'] = self.apk.get_androidversion_code() manifest['version_name'] = self.apk.get_androidversion_name() manifest['min_sdk_version'] = self.apk.get_min_sdk_version() manifest['target_sdk_version'] = self.apk.get_target_sdk_version() manifest['permissions'] = self.apk.get_declared_permissions() manifest['total_size'] = total_size manifest['expansions'] = [] if obb_size: main_obb = {} main_obb[ 'file'] = 'Android/obb/{package_name}/main.{version_code}.{package_name}.obb'.format( **manifest) main_obb['install_location'] = 'EXTERNAL_STORAGE' main_obb[ 'install_path'] = 'Android/obb/{package_name}/main.{version_code}.{package_name}.obb'.format( **manifest) manifest['expansions'].push(main_obb) return manifest def save(self): self.name = '{package_name}_v{version_name}.xapk'.format( **self.manifest) zip_path = self.folder.joinpath(self.name) zip_dir = tempfile.mkdtemp() try: print('copying apk to temp directory...') apk_name = '{package_name}.apk'.format(**self.manifest) apk_src = self.apk_src.resolve() apk_dest = PurePath(zip_dir).joinpath(apk_name) shutil.copy2(apk_src, apk_dest) print('apk: OK') if self.manifest.get('expansions'): print('copying obb to temp directory...') obb_name = self.manifest['expansions'][0]['install_path'] obb_src = self.obb_src.resolve() obb_dest = PurePath(zip_dir).joinpath(obb_name) os.makedirs(Path(obb_dest).parent, exist_ok=True) shutil.copy2(obb_src, obb_dest) print('obb: OK') else: print('no obb found') print('creating icon in temp directory...') icon = self.icon icon_dest = PurePath(zip_dir).joinpath('icon.png') with open(icon_dest, 'wb') as iconfile: iconfile.write(icon) print('icon: OK') print('creating manifest in temp directory...') manifest_dest = PurePath(zip_dir).joinpath('manifest.json') with open(manifest_dest, 'w') as manifestfile: s = json.dumps(self.manifest, separators=(':', ',')) manifestfile.write(s) print('manifest: OK') print('creating xapk archive...') with zipfile.ZipFile(zip_path, 'w', compression=zipfile.ZIP_DEFLATED) as zfd: for root, dirs, files in os.walk(zip_dir): for f in files: filename = os.path.join(root, f) zfd.write(filename, os.path.relpath(filename, zip_dir)) print('xapk: OK') finally: print('cleaning up temp directory...') shutil.rmtree(zip_dir) print('cleanup: OK')
def analyze_apk(task, scan_id): # Start the APK analysis global APK_PATH global DECOMPILE_PATH try: scan = Scan.objects.get(pk=scan_id) APK_PATH = settings.BASE_DIR + scan.apk.url DECOMPILE_PATH = os.path.splitext(APK_PATH)[0] scan.status = 'In Progress' scan.progress = 3 scan.save() task.update_state(state = 'STARTED', meta = {'current': scan.progress, 'total': 100, 'status': scan.status}) logger.debug(scan.status) a = APK(APK_PATH) scan = set_hash_app(scan) scan.status = 'Getting info of apk' scan.progress = 5 scan.save() task.update_state(state = 'STARTED', meta = {'current': scan.progress, 'total': 100, 'status': scan.status}) logger.debug(scan.status) scan = get_info_apk(a, scan) scan.status = 'Getting info of certificates' scan.progress = 10 scan.save() task.update_state(state = 'STARTED', meta = {'current': scan.progress, 'total': 100, 'status': scan.status}) logger.debug(scan.status) certificates = get_info_certificate(a, scan) if (settings.VIRUSTOTAL_ENABLED): scan.status = 'Getting info of VT' scan.progress = 15 scan.save() task.update_state(state = 'STARTED', meta = {'current': scan.progress, 'total': 100, 'status': scan.status}) logger.debug(scan.status) report = get_report_virus_total(scan, scan.sha256) if (not report and settings.VIRUSTOTAL_UPLOAD): scan.status = 'Upload to VT' scan.save() upload_virus_total(scan, APK_PATH, scan.sha256) scan.status = 'Decompiling' scan.progress = 20 scan.save() task.update_state(state = 'STARTED', meta = {'current': scan.progress, 'total': 100, 'status': scan.status}) logger.debug(scan.status) decompile_jadx() if (a.get_app_icon()): update_icon(scan, DECOMPILE_PATH + '/resources/' + a.get_app_icon()) scan.status = 'Finding vulnerabilities' scan.progress = 40 task.update_state(state = 'STARTED', meta = {'current': scan.progress, 'total': 100, 'status': scan.status}) scan.save() logger.debug(scan.status) findings = get_tree_dir(scan) scan.status = 'Finished' scan.progress = 100 scan.finished_on = datetime.now() scan.save() task.update_state(state = 'STARTED', meta = {'current': scan.progress, 'total': 100, 'status': scan.status}) logger.debug(scan.status) except Exception as e: scan.progress = 100 scan.status = "Error" scan.finished_on = datetime.now() scan.save() task.update_state(state = 'STARTED', meta = {'current': scan.progress, 'total': 100, 'status': scan.status}) logger.error(e)
class StaticAnalysis: def __init__(self, apk_path=None): self.apk = None self.apk_path = apk_path self.signatures = None self.compiled_tracker_signature = None self.classes = None self.app_details = None if apk_path is not None: self.load_apk() def _compile_signatures(self): """ Compiles the regex associated to each signature, in order to speed up the trackers detection. :return: A compiled list of signatures. """ self.compiled_tracker_signature = [] try: self.compiled_tracker_signature = [ re.compile(track.code_signature) for track in self.signatures ] except TypeError: print("self.signatures is not iterable") def load_trackers_signatures(self): """ Load trackers signatures from the official Exodus database. :return: a dictionary containing signatures. """ self.signatures = [] exodus_url = "https://reports.exodus-privacy.eu.org/api/trackers" r = requests.get(exodus_url) data = r.json() for e in data['trackers']: self.signatures.append( namedtuple( 'tracker', data['trackers'][e].keys())(*data['trackers'][e].values())) self._compile_signatures() logging.debug('{} trackers signatures loaded'.format( len(self.signatures))) def load_apk(self): """ Load the APK file. """ if self.apk is None: self.apk = APK(self.apk_path) def get_embedded_classes(self): """ Get the list of Java classes embedded into all DEX files. :return: array of Java classes names as string """ if self.classes is not None: return self.classes class_regex = re.compile(r'classes.*\.dex') with TemporaryDirectory() as tmp_dir: with zipfile.ZipFile(self.apk_path, "r") as apk_zip: class_infos = (info for info in apk_zip.infolist() if class_regex.search(info.filename)) for info in class_infos: apk_zip.extract(info, tmp_dir) dexdump = which('dexdump') cmd = '{} {}/classes*.dex | perl -n -e\'/[A-Z]+((?:\w+\/)+\w+)/ && print "$1\n"\'|sort|uniq'.format( dexdump, tmp_dir) try: self.classes = subprocess.check_output( cmd, stderr=subprocess.STDOUT, shell=True, universal_newlines=True).splitlines() logging.debug('{} classes found in {}'.format( len(self.classes), self.apk_path)) return self.classes except subprocess.CalledProcessError: logging.error('Unable to decode {}'.format(self.apk_path)) raise Exception('Unable to decode the APK') def detect_trackers_in_list(self, class_list): """ Detect embedded trackers in the provided classes list. :return: list of embedded trackers """ if self.signatures is None: self.load_trackers_signatures() def _detect_tracker(sig, tracker, class_list): for clazz in class_list: if sig.search(clazz): return tracker return None results = [] args = [(self.compiled_tracker_signature[index], tracker, class_list) for (index, tracker) in enumerate(self.signatures) if len(tracker.code_signature) > 3] for res in itertools.starmap(_detect_tracker, args): if res: results.append(res) trackers = [t for t in results if t is not None] logging.debug('{} trackers detected in {}'.format( len(trackers), self.apk_path)) return trackers def detect_trackers(self, class_list_file=None): """ Detect embedded trackers. :return: list of embedded trackers """ if self.signatures is None: self.load_trackers_signatures() if class_list_file is None: return self.detect_trackers_in_list(self.get_embedded_classes()) else: with open(class_list_file, 'r') as classes_file: classes = classes_file.readlines() return self.detect_trackers_in_list(classes) def get_version(self): """ Get the application version name :return: version name """ self.load_apk() return self.apk.get_androidversion_name() def get_version_code(self): """ Get the application version code :return: version code """ self.load_apk() return self.apk.get_androidversion_code() def get_permissions(self): """ Get application permissions :return: application permissions list """ self.load_apk() return self.apk.get_permissions() def get_app_name(self): """ Get application name :return: application name """ self.load_apk() return self.apk.get_app_name() def get_package(self): """ Get application package :return: application package """ self.load_apk() return self.apk.get_package() def get_libraries(self): """ Get application libraries :return: application libraries list """ self.load_apk() return self.apk.get_libraries() def get_icon_path(self): """ Get the icon path in the ZIP archive :return: icon path in the ZIP archive """ self.load_apk() return self.apk.get_app_icon() def get_application_details(self): """ Get the application details like creator, number of downloads, etc. :param handle: application handle :return: application details dictionary """ self.load_apk() if self.app_details is not None: return self.app_details details = get_details_from_gplaycli(self.get_package()) if details is not None: self.app_details = details return details def _get_icon_from_details(self, path): """ Get icon from applications details dictionary :param path: path where to write the icon file :return: icon path :raises Exception: if unable to find icon """ details = self.get_application_details() if details is not None: for i in details.get('images'): if i.get('imageType') == 4: f = requests.get(i.get('url')) with open(path, mode='wb') as fp: fp.write(f.content) if os.path.isfile(path) and os.path.getsize(path) > 0: return path raise Exception('Unable to download the icon from details') @staticmethod def _render_drawable_to_png(self, bxml, path): ap = axml.AXMLPrinter(bxml) print(ap.get_buff()) def save_icon(self, path): """ Extract the icon from the ZIP archive and save it at the given path :param path: destination path of the icon :return: destination path of the icon, None in case of error """ try: icon = self.get_icon_path() if icon is None: raise Exception('Unable to get icon path') with zipfile.ZipFile(self.apk_path) as z: with open(path, 'wb') as f: f.write(z.read(icon)) with Image.open(path) as _: logging.info('Get icon from APK: success') return path except Exception: logging.warning('Unable to get the icon from the APK') return None # TODO: Set this back once details download is working again # logging.warning('Downloading icon from details') # try: # saved_path = self._get_icon_from_details(path) # logging.debug('Icon downloaded from application details') # return saved_path # except Exception as e: # logging.warning(e) def get_icon_phash(self): """ Get the perceptual hash of the application icon :return: the perceptual hash, empty string in case of error """ with NamedTemporaryFile() as ic: path = self.save_icon(ic.name) if path is None: logging.error('Unable to save the icon') return '' return self.get_phash(ic.name) @staticmethod def get_phash(image_name): """ Get the perceptual hash of the given image :param image_name: name of the image file :return: the perceptual hash, empty string in case of error """ dhash.force_pil() # Force PIL try: image = Image.open(image_name).convert("RGBA") row, col = dhash.dhash_row_col(image, size=PHASH_SIZE) return row << (PHASH_SIZE * PHASH_SIZE) | col except IOError as e: logging.error(e) return '' @staticmethod def get_icon_similarity(phash_origin, phash_candidate): """ Get icons similarity score [0,1.0] :param phash_origin: original icon :param phash_candidate: icon to be compared :return: similarity score [0,1.0] """ diff = dhash.get_num_bits_different(phash_origin, phash_candidate) return 1 - 1. * diff / (PHASH_SIZE * PHASH_SIZE * 2) def get_application_universal_id(self): parts = [self.get_package()] for c in self.get_certificates(): parts.append(c.fingerprint.upper()) return sha1(' '.join(parts).encode('utf-8')).hexdigest().upper() def get_certificates(self): certificates = [] def _my_name_init(self, oid, value, _type=_SENTINEL): if not isinstance(oid, ObjectIdentifier): raise TypeError( "oid argument must be an ObjectIdentifier instance.") if not isinstance(value, six.text_type): raise TypeError("value argument must be a text type.") if len(value) == 0: raise ValueError("Value cannot be an empty string") if _type == _SENTINEL: _type = _NAMEOID_DEFAULT_TYPE.get(oid, _ASN1Type.UTF8String) if not isinstance(_type, _ASN1Type): raise TypeError("_type must be from the _ASN1Type enum") self._oid = oid self._value = value self._type = _type NameAttribute.__init__ = _my_name_init signs = self.apk.get_signature_names() for s in signs: c = self.apk.get_certificate(s) cert = Certificate(c) certificates.append(cert) return certificates def get_apk_size(self): """ Get the APK file size in bytes :return: APK file size """ return os.path.getsize(self.apk_path) def get_sha256(self): """ Get the sha256sum of the APK file :return: hex sha256sum """ BLOCKSIZE = 65536 hasher = sha256() with open(self.apk_path, 'rb') as apk: buf = apk.read(BLOCKSIZE) while len(buf) > 0: hasher.update(buf) buf = apk.read(BLOCKSIZE) return hasher.hexdigest() def save_embedded_classes_in_file(self, file_path): """ Save list of embedded classes in file. :param file_path: file to write """ with open(file_path, 'w+') as f: f.write('\n'.join(self.get_embedded_classes())) def print_apk_infos(self): """ Print APK information """ permissions = self.get_permissions() libraries = self.get_libraries() certificates = self.get_certificates() print("=== Information") print('- APK path: {}'.format(self.apk_path)) print('- APK sum: {}'.format(self.get_sha256())) print('- App version: {}'.format(self.get_version())) print('- App version code: {}'.format(self.get_version_code())) print('- App UID: {}'.format(self.get_application_universal_id())) print('- App name: {}'.format(self.get_app_name())) print('- App package: {}'.format(self.get_package())) print('- App permissions: {}'.format(len(permissions))) for perm in permissions: print(' - {}'.format(perm)) print('- App libraries:') for lib in libraries: print(' - {}'.format(lib)) print('- Certificates: {}'.format(len(certificates))) for cert in certificates: print(' - {}'.format(cert)) def print_embedded_trackers(self): """ Print detected trackers """ trackers = self.detect_trackers() print('=== Found trackers: {}'.format(len(trackers))) for t in trackers: print(' - {}'.format(t.name))