From 6c68150676a1dd2dd6e3817acfcaba6fd197e589 Mon Sep 17 00:00:00 2001 From: Ajin Abraham Date: Thu, 6 May 2021 20:20:04 -0700 Subject: [PATCH] Multiple Features Static Analyzer & Dynamic Analyzer (#1730) * Improved Split APK support now supports .apks file. * Ability to look up and download APK from apktada/apkpure/apkplz. * Dynamic Analyzer: Get Runtime Application Third party dependencies. * Static Analysis Rule QA and Typo Fix. * Persist Frida Code change in session storage. * Show Base64 strings decoded at runtime and the called class. * Detect Trackers from Runtime Dependencies and Network Traffic * Windows Binskim version pinning --- .../auxiliary/get_loaded_classes.js | 1 - .../views/android/dynamic_analyzer.py | 2 +- .../views/android/environment.py | 5 +- .../views/android/frida_core.py | 9 + .../views/android/frida_scripts.py | 2 +- .../views/android/operations.py | 20 --- mobsf/DynamicAnalyzer/views/android/report.py | 17 +- .../views/android/tests_frida.py | 69 +++++++- mobsf/MalwareAnalyzer/views/Trackers.py | 67 ++++++- mobsf/MobSF/settings.py | 3 + mobsf/MobSF/urls.py | 3 + mobsf/MobSF/utils.py | 27 +++ mobsf/MobSF/views/api/api_dynamic_analysis.py | 13 ++ mobsf/MobSF/views/apk_downloader.py | 163 ++++++++++++++++++ mobsf/MobSF/views/helpers.py | 15 +- mobsf/MobSF/views/home.py | 22 +++ mobsf/MobSF/views/scanning.py | 39 ++++- .../views/android/rules/android_rules.yaml | 8 +- .../views/android/static_analyzer.py | 20 ++- mobsf/StaticAnalyzer/views/android/strings.py | 43 +++-- mobsf/StaticAnalyzer/views/android/xapk.py | 29 +++- .../views/ios/rules/objective_c_rules.yaml | 4 +- .../views/ios/rules/swift_rules.yaml | 10 +- mobsf/install/windows/setup.py | 1 + .../android/dynamic_analyzer.html | 33 +++- .../android/dynamic_report.html | 127 ++++++++++++++ .../dynamic_analysis/dynamic_analysis.html | 14 +- mobsf/templates/general/apidocs.html | 75 ++++++++ mobsf/templates/general/home.html | 106 +++++++++--- mobsf/templates/general/recent.html | 3 +- mobsf/templates/pdf/android_report.html | 27 +-- .../android_binary_analysis.html | 10 +- 32 files changed, 851 insertions(+), 136 deletions(-) create mode 100644 mobsf/MobSF/views/apk_downloader.py diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/auxiliary/get_loaded_classes.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/auxiliary/get_loaded_classes.js index 7a61fe05..3b1baecc 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/auxiliary/get_loaded_classes.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/auxiliary/get_loaded_classes.js @@ -5,7 +5,6 @@ */ Java.perform(function() { var classes = Java.enumerateLoadedClassesSync(); - send('[AUXILIARY] Loaded Classes'); classes.forEach(function(aClass) { try{ var className = aClass.match(/[L](.*);/)[1].replace(/\//g, "."); diff --git a/mobsf/DynamicAnalyzer/views/android/dynamic_analyzer.py b/mobsf/DynamicAnalyzer/views/android/dynamic_analyzer.py index ad75980d..2d2dd222 100755 --- a/mobsf/DynamicAnalyzer/views/android/dynamic_analyzer.py +++ b/mobsf/DynamicAnalyzer/views/android/dynamic_analyzer.py @@ -17,7 +17,6 @@ from django.shortcuts import render from mobsf.DynamicAnalyzer.views.android.environment import Environment from mobsf.DynamicAnalyzer.views.android.operations import ( get_package_name, - strict_package_check, ) from mobsf.DynamicAnalyzer.tools.webproxy import ( get_http_tools_url, @@ -30,6 +29,7 @@ from mobsf.MobSF.utils import ( get_proxy_ip, is_md5, print_n_send_error_response, + strict_package_check, ) from mobsf.MobSF.views.scanning import add_to_recent_scan from mobsf.StaticAnalyzer.models import StaticAnalyzerAndroid diff --git a/mobsf/DynamicAnalyzer/views/android/environment.py b/mobsf/DynamicAnalyzer/views/android/environment.py index e7d7eee3..9178c03b 100644 --- a/mobsf/DynamicAnalyzer/views/android/environment.py +++ b/mobsf/DynamicAnalyzer/views/android/environment.py @@ -378,11 +378,14 @@ class Environment: or any(char.isdigit() for char in ver)): logger.info('Found Genymotion x86 Android VM') return 'genymotion' + elif b'corellium' in out: + logger.info('Found Corellium ARM Android VM') + return 'corellium' else: logger.warning( 'Unable to identify Dynamic Analysis environment. ' 'Official support is available only for Android ' - 'Emulator and Genymotion VM') + 'Emulator, Corellium, and Genymotion') return '' def get_android_version(self): diff --git a/mobsf/DynamicAnalyzer/views/android/frida_core.py b/mobsf/DynamicAnalyzer/views/android/frida_core.py index db2e81b2..8808216b 100644 --- a/mobsf/DynamicAnalyzer/views/android/frida_core.py +++ b/mobsf/DynamicAnalyzer/views/android/frida_core.py @@ -40,6 +40,7 @@ class Frida: self.apk_dir = os.path.join(settings.UPLD_DIR, self.hash + '/') self.api_mon = os.path.join(self.apk_dir, 'mobsf_api_monitor.txt') self.frida_log = os.path.join(self.apk_dir, 'mobsf_frida_out.txt') + self.deps = os.path.join(self.apk_dir, 'mobsf_app_deps.txt') def get_default_scripts(self): """Get default Frida Scripts.""" @@ -65,6 +66,9 @@ class Frida: for itm in self.auxiliary: if itm == 'enum_class': scripts.append(get_loaded_classes()) + elif itm == 'get_dependencies': + scripts.append(get_loaded_classes().replace( + '[AUXILIARY] ', '[RUNTIME-DEPS] ')) elif itm == 'string_catch': scripts.append(string_catch()) elif itm == 'string_compare': @@ -95,10 +99,15 @@ class Frida: msg = message['payload'] api_mon = 'MobSF-API-Monitor: ' aux = '[AUXILIARY] ' + deps = '[RUNTIME-DEPS] ' if not isinstance(msg, str): msg = str(msg) if msg.startswith(api_mon): self.write_log(self.api_mon, msg.replace(api_mon, '')) + elif msg.startswith(deps): + info = msg.replace(deps, '') + '\n' + self.write_log(self.deps, info) + self.write_log(self.frida_log, info) elif msg.startswith(aux): self.write_log(self.frida_log, msg.replace(aux, '[*] ') + '\n') diff --git a/mobsf/DynamicAnalyzer/views/android/frida_scripts.py b/mobsf/DynamicAnalyzer/views/android/frida_scripts.py index c633f145..59e369ac 100644 --- a/mobsf/DynamicAnalyzer/views/android/frida_scripts.py +++ b/mobsf/DynamicAnalyzer/views/android/frida_scripts.py @@ -2,7 +2,7 @@ import os from django.conf import settings -from mobsf.DynamicAnalyzer.views.android.operations import strict_package_check +from mobsf.MobSF.utils import strict_package_check def get_content(file_name): diff --git a/mobsf/DynamicAnalyzer/views/android/operations.py b/mobsf/DynamicAnalyzer/views/android/operations.py index e2b2491b..98b6facc 100644 --- a/mobsf/DynamicAnalyzer/views/android/operations.py +++ b/mobsf/DynamicAnalyzer/views/android/operations.py @@ -65,26 +65,6 @@ def is_attack_pattern(user_input): return stat -def strict_package_check(user_input): - """Strict package name check.""" - pat = re.compile(r'^\w+\.*[\w\.\$]+$') - resp = re.match(pat, user_input) - if not resp: - logger.error('Invalid package/class name') - return resp - - -def is_path_traversal(user_input): - """Check for path traversal.""" - if (('../' in user_input) - or ('%2e%2e' in user_input) - or ('..' in user_input) - or ('%252e' in user_input)): - logger.error('Path traversal attack detected') - return True - return False - - def invalid_params(api=False): """Standard response for invalid params.""" msg = 'Invalid Parameters' diff --git a/mobsf/DynamicAnalyzer/views/android/report.py b/mobsf/DynamicAnalyzer/views/android/report.py index 87f00382..1d785dd6 100644 --- a/mobsf/DynamicAnalyzer/views/android/report.py +++ b/mobsf/DynamicAnalyzer/views/android/report.py @@ -10,6 +10,7 @@ from django.shortcuts import render from django.template.defaulttags import register from django.utils.html import escape +import mobsf.MalwareAnalyzer.views.Trackers as Trackers from mobsf.DynamicAnalyzer.views.android.analysis import ( generate_download, get_screenshots, @@ -17,17 +18,18 @@ from mobsf.DynamicAnalyzer.views.android.analysis import ( ) from mobsf.DynamicAnalyzer.views.android.operations import ( get_package_name, - is_path_traversal, ) from mobsf.DynamicAnalyzer.views.android.tests_xposed import ( droidmon_api_analysis, ) from mobsf.DynamicAnalyzer.views.android.tests_frida import ( apimon_analysis, + dependency_analysis, ) from mobsf.MobSF.utils import ( is_file_exists, is_md5, + is_path_traversal, is_safe_path, print_n_send_error_response, read_sqlite, @@ -49,6 +51,7 @@ def view_report(request, checksum, api=False): try: droidmon = {} apimon = {} + b64_strings = [] if not is_md5(checksum): # We need this check since checksum is not validated # in REST API @@ -64,6 +67,7 @@ def view_report(request, checksum, api=False): api) app_dir = os.path.join(settings.UPLD_DIR, checksum + '/') download_dir = settings.DWD_DIR + tools_dir = settings.TOOLS_DIR if not is_file_exists(os.path.join(app_dir, 'logcat.txt')): msg = ('Dynamic Analysis report is not available ' 'for this app. Perform Dynamic Analysis ' @@ -71,14 +75,18 @@ def view_report(request, checksum, api=False): return print_n_send_error_response(request, msg, api) fd_log = os.path.join(app_dir, 'mobsf_frida_out.txt') droidmon = droidmon_api_analysis(app_dir, package) - apimon = apimon_analysis(app_dir) + apimon, b64_strings = apimon_analysis(app_dir) + deps = dependency_analysis(package, app_dir) analysis_result = run_analysis(app_dir, checksum, package) + domains = analysis_result['domains'] + trk = Trackers.Trackers(app_dir, tools_dir) + trackers = trk.get_runtime_trackers(domains, deps) generate_download(app_dir, checksum, download_dir, package) images = get_screenshots(checksum, download_dir) context = {'hash': checksum, 'emails': analysis_result['emails'], 'urls': analysis_result['urls'], - 'domains': analysis_result['domains'], + 'domains': domains, 'clipboard': analysis_result['clipboard'], 'xml': analysis_result['xml'], 'sqlite': analysis_result['sqlite'], @@ -89,7 +97,10 @@ def view_report(request, checksum, api=False): 'exported_activity_tester': images['exported_activities'], 'droidmon': droidmon, 'apimon': apimon, + 'base64_strings': b64_strings, + 'trackers': trackers, 'frida_logs': is_file_exists(fd_log), + 'runtime_dependencies': deps, 'package': package, 'version': settings.MOBSF_VER, 'title': 'Dynamic Analysis'} diff --git a/mobsf/DynamicAnalyzer/views/android/tests_frida.py b/mobsf/DynamicAnalyzer/views/android/tests_frida.py index 67f28588..5762f77f 100644 --- a/mobsf/DynamicAnalyzer/views/android/tests_frida.py +++ b/mobsf/DynamicAnalyzer/views/android/tests_frida.py @@ -48,6 +48,29 @@ def list_frida_scripts(request, api=False): # AJAX +@require_http_methods(['POST']) +def get_runtime_dependencies(request, api=False): + """Get App runtime dependencies.""" + data = { + 'status': 'failed', + 'message': 'Failed to get runtime dependencies'} + try: + checksum = request.POST['hash'] + if not is_md5(checksum): + return invalid_params(api) + package = get_package_name(checksum) + if not package: + return invalid_params(api) + get_dependencies(package, checksum) + return send_response( + {'status': 'ok'}, + api) + except Exception: + pass + return send_response(data, api) +# AJAX + + @require_http_methods(['POST']) def get_script(request, api=False): """Get frida scripts from others.""" @@ -240,9 +263,10 @@ def apimon_analysis(app_dir): """API Analysis.""" api_details = {} try: + strings = [] location = os.path.join(app_dir, 'mobsf_api_monitor.txt') if not is_file_exists(location): - return {} + return api_details, strings logger.info('Frida API Monitor Analysis') with open(location, 'r', encoding='utf8', @@ -262,6 +286,7 @@ def apimon_analysis(app_dir): if to_decode: api['decoded'] = decode_base64( to_decode).decode('utf-8', 'ignore') + strings.append((api['calledFrom'], api['decoded'])) except Exception: pass api['icon'] = get_icon_map(api['name']) @@ -271,4 +296,44 @@ def apimon_analysis(app_dir): api_details[api['name']] = [api] except Exception: logger.exception('API Monitor Analysis') - return api_details + return api_details, strings + + +def get_dependencies(package, checksum): + """Get 3rd party dependencies at runtime.""" + frd = Frida( + checksum, + package, + ['ssl_pinning_bypass', 'debugger_check_bypass', 'root_bypass'], + ['get_dependencies'], + None, + None, + ) + location = Path(frd.deps) + if location.exists(): + location.write_text('') + trd = threading.Thread(target=frd.connect) + trd.daemon = True + trd.start() + + +def dependency_analysis(package, app_dir): + deps = set() + msg = 'Collecting Runtime Dependency Analysis data' + try: + ignore = ( + package, + 'android.', 'androidx.', 'kotlin.', 'kotlinx.', 'java.', 'javax.', + 'sun.', 'com.android.', 'j$', 'dalvik.system.', 'libcore.', + 'com.google.', 'org.kxml2.', 'org.apache.', 'org.json.') + location = Path(app_dir) / 'mobsf_app_deps.txt' + if not location.exists(): + return deps + logger.info(msg) + data = location.read_text('utf-8', 'ignore').splitlines() + for dep in data: + if not dep.startswith(ignore): + deps.add(dep.rsplit('.', 1)[0]) + except Exception: + logger.exception(msg) + return deps diff --git a/mobsf/MalwareAnalyzer/views/Trackers.py b/mobsf/MalwareAnalyzer/views/Trackers.py index 6739b9c4..29dad6ad 100644 --- a/mobsf/MalwareAnalyzer/views/Trackers.py +++ b/mobsf/MalwareAnalyzer/views/Trackers.py @@ -30,6 +30,7 @@ class Trackers: self.signatures = None self.nb_trackers_signature = 0 self.compiled_tracker_signature = None + self.compiled_network_tracker_sig = None self.classes = None self.tools_dir = tools_dir self._update_tracker_db() @@ -78,9 +79,14 @@ class Trackers: :return: A compiled list of signatures. """ self.compiled_tracker_signature = [] + self.compiled_network_tracker_sig = [] try: - self.compiled_tracker_signature = [re.compile(track.code_signature) - for track in self.signatures] + self.compiled_tracker_signature = [ + re.compile(track.code_signature) + for track in self.signatures] + self.compiled_network_tracker_sig = [ + re.compile(track.network_signature) + for track in self.signatures] except TypeError: logger.exception('compiling tracker signature failed') @@ -128,9 +134,9 @@ class Trackers: self.classes = classes return self.classes - def detect_trackers_in_list(self, class_list): + def detect_trackers_in_list(self, class_list, network=False): """ - Detect embedded trackers in the provided classes list. + Detect embedded trackers in the provided classes list/urls. :return: list of embedded trackers """ @@ -144,7 +150,17 @@ class Trackers: return None results = [] - args = [(self.compiled_tracker_signature[index], tracker, class_list) + if network: + compiled = self.compiled_network_tracker_sig + args = [ + (compiled[index], tracker, class_list) + for (index, tracker) in enumerate(self.signatures) if + len(tracker.network_signature) > 3] + + else: + compiled = self.compiled_tracker_signature + args = [ + (compiled[index], tracker, class_list) for (index, tracker) in enumerate(self.signatures) if len(tracker.code_signature) > 3] @@ -169,6 +185,22 @@ class Trackers: return self.detect_trackers_in_list(eclasses) return [] + def detect_runtime_trackers(self, items, deps=False): + """ + Detect runtime trackers. + + :return: list of embedded trackers + """ + if self.signatures is None: + self.load_trackers_signatures() + if items and not deps: + # Domains + return self.detect_trackers_in_list(items.keys(), True) + elif items and deps: + # Runtime Dependencies + return self.detect_trackers_in_list(items) + return [] + def get_trackers(self): """Get Trackers.""" logger.info('Detecting Trackers') @@ -178,5 +210,28 @@ class Trackers: 'trackers': []} for trk in trackers: trk_url = '{}/trackers/{}'.format(settings.EXODUS_URL, trk.id) - tracker_dict['trackers'].append({trk.name: trk_url}) + tracker_dict['trackers'].append({ + 'name': trk.name, + 'categories': ', '.join(trk.categories), + 'url': trk_url, + }) return tracker_dict + + def get_runtime_trackers(self, domains, deps): + """Get Trackers from Runtime.""" + trackers = [] + logger.info('Detecting Trackers from HTTP(s) traffic') + network = self.detect_runtime_trackers(domains) + logger.info('Detecting Trackers from Runtime dependencies') + runtime = self.detect_runtime_trackers(deps, True) + for i in runtime: + if i not in network: + network.append(i) + for trk in network: + trk_url = '{}/trackers/{}'.format(settings.EXODUS_URL, trk.id) + trackers.append({ + 'name': trk.name, + 'categories': ', '.join(trk.categories), + 'url': trk_url, + }) + return trackers diff --git a/mobsf/MobSF/settings.py b/mobsf/MobSF/settings.py index b2223a9a..129351ac 100644 --- a/mobsf/MobSF/settings.py +++ b/mobsf/MobSF/settings.py @@ -114,6 +114,9 @@ GITHUB_URL = ('https://raw.githubusercontent.com/' FRIDA_SERVER = 'https://api.github.com/repos/frida/frida/releases/tags/' GOOGLE = 'https://www.google.com' BAIDU = 'https://www.baidu.com/' +APKPURE = 'https://m.apkpure.com/android/{}/download?from=details' +APKTADA = 'https://apktada.com/download-apk/' +APKPLZ = 'https://apkplz.net/download-app/' # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # ============DJANGO SETTINGS ================= diff --git a/mobsf/MobSF/urls.py b/mobsf/MobSF/urls.py index ecaed312..04b9b1f9 100755 --- a/mobsf/MobSF/urls.py +++ b/mobsf/MobSF/urls.py @@ -60,6 +60,7 @@ urlpatterns = [ url(r'^api/v1/frida/logs$', api_dz.api_frida_logs), url(r'^api/v1/frida/list_scripts$', api_dz.api_list_frida_scripts), url(r'^api/v1/frida/get_script$', api_dz.api_get_script), + url(r'^api/v1/frida/get_dependencies$', api_dz.api_get_dependencies), ] if settings.API_ONLY == '0': urlpatterns.extend([ @@ -67,6 +68,7 @@ if settings.API_ONLY == '0': url(r'^$', home.index, name='home'), url(r'^upload/$', home.Upload.as_view), url(r'^download/', home.download), + url(r'^download_scan/', home.download_apk), url(r'^about$', home.about, name='about'), url(r'^api_docs$', home.api_docs, name='api_docs'), url(r'^recent_scans/$', home.recent_scans, name='recent'), @@ -131,6 +133,7 @@ if settings.API_ONLY == '0': url(r'^frida_logs/$', tests_frida.frida_logs), url(r'^list_frida_scripts/$', tests_frida.list_frida_scripts), url(r'^get_script/$', tests_frida.get_script), + url(r'^get_dependencies/$', tests_frida.get_runtime_dependencies), # Report url(r'^dynamic_report/(?P[0-9a-f]{32})$', report.view_report), diff --git a/mobsf/MobSF/utils.py b/mobsf/MobSF/utils.py index 4dbf86d1..761a0f4f 100755 --- a/mobsf/MobSF/utils.py +++ b/mobsf/MobSF/utils.py @@ -569,3 +569,30 @@ def cmd_injection_check(data): '%7C%7C', '%0a', '%0d%0a', ] return any(i in data for i in breakers) + + +def strict_package_check(user_input): + """Strict package name check.""" + pat = re.compile(r'^\w+\.*[\w\.\$]+$') + resp = re.match(pat, user_input) + if not resp: + logger.error('Invalid package/class name') + return resp + + +def is_path_traversal(user_input): + """Check for path traversal.""" + if (('../' in user_input) + or ('%2e%2e' in user_input) + or ('..' in user_input) + or ('%252e' in user_input)): + logger.error('Path traversal attack detected') + return True + return False + + +def is_zip_magic(file_obj): + magic = file_obj.read(4) + file_obj.seek(0, 0) + # ZIP magic PK.. no support for spanned and empty arch + return bool(magic == b'\x50\x4B\x03\x04') diff --git a/mobsf/MobSF/views/api/api_dynamic_analysis.py b/mobsf/MobSF/views/api/api_dynamic_analysis.py index 5e890e77..9b4083ba 100644 --- a/mobsf/MobSF/views/api/api_dynamic_analysis.py +++ b/mobsf/MobSF/views/api/api_dynamic_analysis.py @@ -234,6 +234,19 @@ def api_get_script(request): return make_api_response(resp, 500) +@request_method(['POST']) +@csrf_exempt +def api_get_dependencies(request): + """POST - Frida Get Runtime Dependencies.""" + if 'hash' not in request.POST: + return make_api_response( + {'error': 'Missing Parameters'}, 422) + resp = tests_frida.get_runtime_dependencies(request, True) + if resp['status'] == 'ok': + return make_api_response(resp, 200) + return make_api_response(resp, 500) + + # Report APIs @request_method(['POST']) @csrf_exempt diff --git a/mobsf/MobSF/views/apk_downloader.py b/mobsf/MobSF/views/apk_downloader.py new file mode 100644 index 00000000..0dcc7160 --- /dev/null +++ b/mobsf/MobSF/views/apk_downloader.py @@ -0,0 +1,163 @@ +# -*- coding: utf_8 -*- +"""Android APK Downloader.""" +import logging +from tempfile import gettempdir +from pathlib import Path +from zipfile import ZipFile + +import requests + +from bs4 import BeautifulSoup + +from django.conf import settings + +from mobsf.MobSF.views.scanning import ( + add_to_recent_scan, + handle_uploaded_file, +) +from mobsf.MobSF.utils import ( + is_path_traversal, + is_zip_magic, + strict_package_check, + upstream_proxy, +) + + +logger = logging.getLogger(__name__) + + +def fetch_html(url): + """Get Result HTML.""" + headers = { + 'User-Agent': ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) ' + 'AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/39.0.2171.95 Safari/537.36'), + 'Accept-Encoding': 'deflate, gzip'} + try: + proxies, verify = upstream_proxy('https') + res = requests.get(url, + headers=headers, + proxies=proxies, + verify=verify, + stream=True) + if res.status_code == 200: + return BeautifulSoup(res.text, features='lxml') + except Exception: + pass + return None + + +def download_file(url, outfile): + try: + logger.info('Downloading APK...') + with requests.get(url, stream=True) as r: + r.raise_for_status() + with open(outfile, 'wb') as f: + for chunk in r.iter_content(chunk_size=8192): + f.write(chunk) + return outfile + except Exception: + pass + return None + + +def get_scan_type(location): + """Get APK type.""" + with ZipFile(location, 'r') as zf: + for fil in zf.namelist(): + if fil.endswith('.apk'): + return 'apks' + return 'apk' + + +def add_apk(dwd_file, filename): + """Add APK to MobSF.""" + with dwd_file.open('rb') as flip: + if not is_zip_magic(flip): + logger.warning('Downloaded file is not an APK/Split APK') + return None + md5 = handle_uploaded_file(flip, '.apk') + apk = Path(settings.UPLD_DIR) / md5 / f'{md5}.apk' + scan_type = get_scan_type(apk) + data = { + 'analyzer': 'static_analyzer', + 'status': 'success', + 'hash': md5, + 'scan_type': scan_type, + 'file_name': filename, + } + add_to_recent_scan(data) + return data + return None + + +def find_apk_link(url, domain): + """Find APK download link.""" + try: + logger.info('Looking for download link form %s', domain) + bsp = fetch_html(url) + if not bsp: + return None + link = bsp.find('a', href=True, string='click here') + if link: + logger.info('Download link found from %s', domain) + return link['href'] + logger.warning('Download link not found in %s', domain) + except Exception: + logger.warning('Failed to obtain download link from %s', domain) + return None + + +def try_provider(package, provider, domain): + """Try using a provider.""" + downloaded_file = None + data = None + apk_name = f'{package}.apk' + temp_file = Path(gettempdir()) / apk_name + link = find_apk_link(provider, domain) + if link: + downloaded_file = download_file(link, temp_file) + if downloaded_file: + data = add_apk(downloaded_file, apk_name) + if data: + return data + return None + + +def apk_download(package): + """Download APK.""" + downloaded_file = None + data = None + try: + if not strict_package_check(package) or is_path_traversal(package): + return None + logger.info('Attempting to download: %s', package) + # APKTADA + data = try_provider( + package, + f'{settings.APKTADA}{package}', + 'apktada.com') + if data: + return data + # APKPURE + data = try_provider( + package, + settings.APKPURE.format(package), + 'apkpure.com') + if data: + return data + # APKPLZ + data = try_provider( + package, + f'{settings.APKPLZ}{package}', + 'apkplz.net') + if data: + return data + logger.warning('Unable to find download link for %s', package) + return None + except Exception: + logger.exception('Failed to download the apk') + return None + finally: + if downloaded_file: + downloaded_file.unlink() diff --git a/mobsf/MobSF/views/helpers.py b/mobsf/MobSF/views/helpers.py index 4135c56d..29688641 100644 --- a/mobsf/MobSF/views/helpers.py +++ b/mobsf/MobSF/views/helpers.py @@ -1,6 +1,8 @@ """Helpers.""" import functools +from mobsf.MobSF.utils import is_zip_magic + from django.conf import settings from django.http import HttpRequest, HttpResponseNotAllowed @@ -13,7 +15,7 @@ class FileType(object): def __init__(self, file_obj): self.file_type = file_obj.content_type self.file_name_lower = file_obj.name.lower() - self.zip = self.is_zip_magic(file_obj) + self.zip = is_zip_magic(file_obj) def is_allow_file(self): """ @@ -26,15 +28,14 @@ class FileType(object): or self.is_xapk() or self.is_zip() or self.is_ipa() - or self.is_appx()): + or self.is_appx() + or self.is_apks()): return True return False - def is_zip_magic(self, file_obj): - magic = file_obj.read(4) - file_obj.seek(0, 0) - # ZIP magic PK.. no support for spanned and empty arch - return bool(magic == b'\x50\x4B\x03\x04') + def is_apks(self): + return (self.file_type in settings.APK_MIME + and self.file_name_lower.endswith('.apks')) def is_xapk(self): return (self.file_type in settings.APK_MIME diff --git a/mobsf/MobSF/views/home.py b/mobsf/MobSF/views/home.py index f03eb42f..877a824f 100755 --- a/mobsf/MobSF/views/home.py +++ b/mobsf/MobSF/views/home.py @@ -24,6 +24,7 @@ from mobsf.MobSF.utils import ( ) from mobsf.MobSF.views.helpers import FileType from mobsf.MobSF.views.scanning import Scanning +from mobsf.MobSF.views.apk_downloader import apk_download from mobsf.StaticAnalyzer.models import ( RecentScansDB, StaticAnalyzerAndroid, @@ -137,6 +138,8 @@ class Upload(object): return scanning.scan_apk() elif self.file_type.is_xapk(): return scanning.scan_xapk() + elif self.file_type.is_apks(): + return scanning.scan_apks() elif self.file_type.is_zip(): return scanning.scan_zip() elif self.file_type.is_ipa(): @@ -219,6 +222,25 @@ def recent_scans(request): return render(request, template, context) +def download_apk(request): + """Download and APK by package name.""" + package = request.POST['package'] + # Package validated in apk_download() + context = { + 'status': 'failed', + 'description': 'Unable to download APK', + } + res = apk_download(package) + if res: + context = res + context['status'] = 'ok' + context['package'] = package + resp = HttpResponse( + json.dumps(context), + content_type='application/json; charset=utf-8') + return resp + + def search(request): """Search Scan by MD5 Route.""" md5 = request.GET['md5'] diff --git a/mobsf/MobSF/views/scanning.py b/mobsf/MobSF/views/scanning.py index bef8b100..2e0d36f7 100644 --- a/mobsf/MobSF/views/scanning.py +++ b/mobsf/MobSF/views/scanning.py @@ -1,6 +1,7 @@ # -*- coding: utf_8 -*- import hashlib import logging +import io import os from django.conf import settings @@ -30,18 +31,30 @@ def add_to_recent_scan(data): logger.exception('Adding Scan URL to Database') -def handle_uploaded_file(filecnt, typ): +def handle_uploaded_file(content, typ): """Write Uploaded File.""" - md5 = hashlib.md5() # modify if crash for large - for chunk in filecnt.chunks(): - md5.update(chunk) + md5 = hashlib.md5() + bfr = isinstance(content, io.BufferedReader) + if bfr: + # Not File upload + while chunk := content.read(8192): + md5.update(chunk) + else: + # File upload + for chunk in content.chunks(): + md5.update(chunk) md5sum = md5.hexdigest() anal_dir = os.path.join(settings.UPLD_DIR, md5sum + '/') if not os.path.exists(anal_dir): os.makedirs(anal_dir) with open(anal_dir + md5sum + typ, 'wb+') as destination: - for chunk in filecnt.chunks(): - destination.write(chunk) + if bfr: + content.seek(0, 0) + while chunk := content.read(8192): + destination.write(chunk) + else: + for chunk in content.chunks(): + destination.write(chunk) return md5sum @@ -79,6 +92,20 @@ class Scanning(object): logger.info('Performing Static Analysis of Android XAPK base APK') return data + def scan_apks(self): + """Android Split APK.""" + md5 = handle_uploaded_file(self.file, '.apk') + data = { + 'analyzer': 'static_analyzer', + 'status': 'success', + 'hash': md5, + 'scan_type': 'apks', + 'file_name': self.file_name, + } + add_to_recent_scan(data) + logger.info('Performing Static Analysis of Android Split APK') + return data + def scan_zip(self): """Android /iOS Zipped Source.""" md5 = handle_uploaded_file(self.file, '.zip') diff --git a/mobsf/StaticAnalyzer/views/android/rules/android_rules.yaml b/mobsf/StaticAnalyzer/views/android/rules/android_rules.yaml index 8b0e6657..f66080bd 100644 --- a/mobsf/StaticAnalyzer/views/android/rules/android_rules.yaml +++ b/mobsf/StaticAnalyzer/views/android/rules/android_rules.yaml @@ -202,7 +202,7 @@ ref: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04c-Tampering-and-Reverse-Engineering.md#debugging-and-tracing - id: android_clipboard_listen description: >- - This app listens to Clipboard changes. Some malwares also listen to + This app listens to Clipboard changes. Some malware also listen to Clipboard changes. type: RegexAnd pattern: @@ -449,8 +449,8 @@ ref: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05j-Testing-Resiliency-Against-Reverse-Engineering.md#testing-root-detection-mstg-resilience-1 - id: android_jackson_deserialize description: >- - The app uses jackson deserialization libraryDeserialization of untrusted - input can result inarbitary code execution + The app uses jackson deserialization library. Deserialization of untrusted + input can result in arbitrary code execution. type: RegexAnd pattern: - com\.fasterxml\.jackson\.databind\.ObjectMapper @@ -626,7 +626,7 @@ cvss: 7.4 cwe: cwe-312 description: >- - Files may contain hardcoded sensitive informations like usernames, + Files may contain hardcoded sensitive information like usernames, passwords, keys etc. input_case: lower masvs: storage-14 diff --git a/mobsf/StaticAnalyzer/views/android/static_analyzer.py b/mobsf/StaticAnalyzer/views/android/static_analyzer.py index ebc0bcb8..2328ecd4 100755 --- a/mobsf/StaticAnalyzer/views/android/static_analyzer.py +++ b/mobsf/StaticAnalyzer/views/android/static_analyzer.py @@ -53,7 +53,10 @@ from mobsf.StaticAnalyzer.views.android.manifest_analysis import ( ) from mobsf.StaticAnalyzer.views.android.playstore import get_app_details from mobsf.StaticAnalyzer.views.android.strings import strings_from_apk -from mobsf.StaticAnalyzer.views.android.xapk import handle_xapk +from mobsf.StaticAnalyzer.views.android.xapk import ( + handle_split_apk, + handle_xapk, +) from mobsf.StaticAnalyzer.views.shared_func import ( firebase_analysis, hash_gen, @@ -94,8 +97,9 @@ def static_analyzer(request, api=False): app_dic = {} match = re.match('^[0-9a-f]{32}$', checksum) if (match - and filename.lower().endswith(('.apk', '.xapk', '.zip')) - and typ in ['zip', 'apk', 'xapk']): + and filename.lower().endswith( + ('.apk', '.xapk', '.zip', '.apks')) + and typ in ['zip', 'apk', 'xapk', 'apks']): app_dic['dir'] = Path(settings.BASE_DIR) # BASE DIR app_dic['app_name'] = filename # APP ORGINAL NAME app_dic['md5'] = checksum # MD5 @@ -103,14 +107,18 @@ def static_analyzer(request, api=False): app_dic['app_dir'] = Path(settings.UPLD_DIR) / checksum app_dic['tools_dir'] = app_dic['dir'] / 'StaticAnalyzer' / 'tools' app_dic['tools_dir'] = app_dic['tools_dir'].as_posix() - logger.info('Starting Analysis on : %s', app_dic['app_name']) + logger.info('Starting Analysis on: %s', app_dic['app_name']) if typ == 'xapk': # Handle XAPK # Base APK will have the MD5 of XAPK - res = handle_xapk(app_dic) - if not res: + if not handle_xapk(app_dic): raise Exception('Invalid XAPK File') typ = 'apk' + elif typ == 'apks': + # Handle Split APK + if not handle_split_apk(app_dic): + raise Exception('Invalid Split APK File') + typ = 'apk' if typ == 'apk': app_dic['app_file'] = app_dic['md5'] + '.apk' # NEW FILENAME app_dic['app_path'] = ( diff --git a/mobsf/StaticAnalyzer/views/android/strings.py b/mobsf/StaticAnalyzer/views/android/strings.py index a587b6a2..ec112c04 100755 --- a/mobsf/StaticAnalyzer/views/android/strings.py +++ b/mobsf/StaticAnalyzer/views/android/strings.py @@ -42,22 +42,26 @@ def strings_from_apk(app_file, app_dir, elf_strings): logger.info('Extracting Strings from APK') dat = [] secrets = [] + urls = [] + urls_nf = [] + emails_nf = [] apk_file = os.path.join(app_dir, app_file) and_a = apk.APK(apk_file) rsrc = and_a.get_android_resources() - pkg = rsrc.get_packages_names()[0] - rsrc.get_strings_resources() - for i in rsrc.values[pkg].keys(): - res_string = rsrc.values[pkg][i].get('string') - if res_string: - for duo in res_string: - cap_str = '"' + duo[0] + '" : "' + duo[1] + '"' - if is_secret(duo[0] + '"'): - secrets.append(cap_str) - dat.append(cap_str) - data_string = ''.join(dat) - urls, urls_nf, emails_nf = url_n_email_extract( - data_string, 'Android String Resource') + if rsrc: + pkg = rsrc.get_packages_names()[0] + rsrc.get_strings_resources() + for i in rsrc.values[pkg].keys(): + res_string = rsrc.values[pkg][i].get('string') + if res_string: + for duo in res_string: + cap_str = '"' + duo[0] + '" : "' + duo[1] + '"' + if is_secret(duo[0] + '"'): + secrets.append(cap_str) + dat.append(cap_str) + data_string = ''.join(dat) + urls, urls_nf, emails_nf = url_n_email_extract( + data_string, 'Android String Resource') if elf_strings: for solib in elf_strings: for so, str_list in solib.items(): @@ -71,12 +75,13 @@ def strings_from_apk(app_file, app_dir, elf_strings): urls_nf.extend(suf) emails_nf.extend(sem) strings_dat = list(set(dat)) - return {'strings': strings_dat, - 'urls_list': urls, - 'url_nf': urls_nf, - 'emails_nf': emails_nf, - 'secrets': secrets, - } + return { + 'strings': strings_dat, + 'urls_list': urls, + 'url_nf': urls_nf, + 'emails_nf': emails_nf, + 'secrets': secrets, + } except Exception: logger.exception('Extracting Strings from APK') return {} diff --git a/mobsf/StaticAnalyzer/views/android/xapk.py b/mobsf/StaticAnalyzer/views/android/xapk.py index ce6d8f99..6f17e35c 100644 --- a/mobsf/StaticAnalyzer/views/android/xapk.py +++ b/mobsf/StaticAnalyzer/views/android/xapk.py @@ -5,6 +5,7 @@ from json import load from shutil import move from mobsf.StaticAnalyzer.views.shared_func import unzip +from mobsf.MobSF.utils import is_safe_path logger = logging.getLogger(__name__) @@ -12,8 +13,9 @@ logger = logging.getLogger(__name__) def handle_xapk(app_dic): """Unzip and Extract APK.""" data = None - xapk = app_dic['app_dir'] / (app_dic['md5'] + '.xapk') - apk = app_dic['app_dir'] / (app_dic['md5'] + '.apk') + checksum = app_dic['md5'] + xapk = app_dic['app_dir'] / f'{checksum}.xapk' + apk = app_dic['app_dir'] / f'{checksum}.apk' files = unzip(xapk.as_posix(), app_dic['app_dir']) if 'manifest.json' not in files: logger.error('Manifest file not found in XAPK') @@ -31,6 +33,25 @@ def handle_xapk(app_dic): for a in apks: if a['id'] == 'base': base_apk = app_dic['app_dir'] / a['file'] - move(base_apk, apk) - return True + if is_safe_path(app_dic['app_dir'], base_apk): + move(base_apk, apk) + return True + return None + + +def handle_split_apk(app_dic): + """Unzip and Extract Split APKs.""" + checksum = app_dic['md5'] + apks = app_dic['app_dir'] / f'{checksum}.apk' + # Check if previously extracted + manifest = app_dic['app_dir'] / 'AndroidManifest.xml' + if manifest.exists(): + return True + files = unzip(apks.as_posix(), app_dic['app_dir']) + for apk in files: + if not apk.startswith('config.') and apk.endswith('.apk'): + full_path = app_dic['app_dir'] / apk + if is_safe_path(app_dic['app_dir'], full_path): + move(full_path, apks) + return True return None diff --git a/mobsf/StaticAnalyzer/views/ios/rules/objective_c_rules.yaml b/mobsf/StaticAnalyzer/views/ios/rules/objective_c_rules.yaml index 7f150624..5018788e 100644 --- a/mobsf/StaticAnalyzer/views/ios/rules/objective_c_rules.yaml +++ b/mobsf/StaticAnalyzer/views/ios/rules/objective_c_rules.yaml @@ -11,7 +11,7 @@ - id: ios_hardcoded_info cvss: 7.4 cwe: cwe-312 - description: Files may contain hardcoded sensitive informations like usernames, + description: Files may contain hardcoded sensitive information like usernames, passwords, keys etc. input_case: lower masvs: storage-14 @@ -160,7 +160,7 @@ cvss: 0 cwe: '' description: >- - This app listens to Clipboard changes. Some malwares also listen to + This app listens to Clipboard changes. Some malware also listen to Clipboard changes. input_case: exact masvs: platform-4 diff --git a/mobsf/StaticAnalyzer/views/ios/rules/swift_rules.yaml b/mobsf/StaticAnalyzer/views/ios/rules/swift_rules.yaml index c0aee7b0..787206f4 100644 --- a/mobsf/StaticAnalyzer/views/ios/rules/swift_rules.yaml +++ b/mobsf/StaticAnalyzer/views/ios/rules/swift_rules.yaml @@ -11,7 +11,7 @@ - id: ios_hardcoded_info cvss: 7.4 cwe: cwe-312 - description: Files may contain hardcoded sensitive informations like usernames, + description: Files may contain hardcoded sensitive information like usernames, passwords, keys etc. input_case: lower masvs: storage-14 @@ -136,7 +136,6 @@ masvs: crypto-4 owasp-mobile: m5 pattern: - - (?i)MD2\( - CC_MD2\( severity: high type: RegexOr @@ -149,7 +148,6 @@ masvs: crypto-4 owasp-mobile: m5 pattern: - - (?i)MD4\( - CC_MD4\( severity: high type: RegexOr @@ -194,7 +192,7 @@ - id: ios_keyboard_cache cvss: 0 cwe: '' - description: Keyboard cache should be disabled for all sensitve data inputs. + description: Keyboard cache should be disabled for all sensitive data inputs. input_case: exact masvs: storage-5 owasp-mobile: '' @@ -218,7 +216,7 @@ - id: ios_detect_reversing cvss: 0 cwe: '' - description: This App may have Reverse enginering detection capabilities. + description: This App may have Reverse engineering detection capabilities. input_case: exact masvs: resilience-4 owasp-mobile: '' @@ -243,7 +241,7 @@ - id: ios_biometric_bool cvss: 0 cwe: '' - description: 'Biometric authentication should be based on Keychain, not based on bool.' + description: 'Biometric authentication should be based on iOS Keychain, not based on bool.' input_case: exact masvs: auth-8 owasp-mobile: m1 diff --git a/mobsf/install/windows/setup.py b/mobsf/install/windows/setup.py index c1f3f921..3a8f707f 100755 --- a/mobsf/install/windows/setup.py +++ b/mobsf/install/windows/setup.py @@ -171,6 +171,7 @@ def tools_binskim(): [ nuget, 'install', binskim_nuget, '-Pre', + '-Version', '1.7.2', '-o', mobsf_subdir_tools]) # Some code to determine the version on the fly so we don't have to fix the diff --git a/mobsf/templates/dynamic_analysis/android/dynamic_analyzer.html b/mobsf/templates/dynamic_analysis/android/dynamic_analyzer.html index c084b084..445cc6ba 100644 --- a/mobsf/templates/dynamic_analysis/android/dynamic_analyzer.html +++ b/mobsf/templates/dynamic_analysis/android/dynamic_analyzer.html @@ -84,6 +84,9 @@ {% endif %} Exported Activity Tester Activity Tester + {% if android_version >= 5 %} + Get Dependencies + {% endif %} Take a Screenshot Logcat Stream @@ -469,8 +472,17 @@ var editor = CodeMirror.fromTextArea(document.getElementById("code-js"), { gutters: ["CodeMirror-lint-markers"], lint: true, lineWrapping: true, + onLoad: restore(), }); +//Restore Code +function restore(){ + var custom_code = window.sessionStorage.getItem("code"); + if (custom_code !== null) { + setTimeout(function(){ editor.getDoc().setValue(custom_code); }, 2000); + } +} + // Terminal var util = util || {}; util.toArray = function (list) { @@ -955,9 +967,13 @@ $("#loadscript").click(function() { return false; }); + // Frida Spawn $("#frida_spawn").click(function() { print_status("Instrumenting the application with Frida."); + // Save Frida Code to Session Storage + var code = editor.getDoc().getValue(); + window.sessionStorage.setItem('code', code); $('#fd_log').removeAttr('disabled'); $('#frida-tab').removeClass('disabled'); var default_hooks = []; @@ -991,6 +1007,20 @@ $("#frida_spawn").click(function() { return false; }); +//Frida Get Runtime Dependencies +$("#deps").click(function() { + print_status("Getting application runtime dependencies."); + action(document.location.origin + '/get_dependencies/', {hash: '{{ hash }}'}, function(json) { + if (json.status==="ok"){ + $('#fd_log').removeAttr('disabled'); + $('#frida-tab').removeClass('disabled'); + print_status("Dependencies collected!"); + } else { + print_status(json.message); + } + }); + return false; +}); //Dump App data function dump(){ @@ -1026,8 +1056,7 @@ $("#stop").one( "click", function() { dump(); }); return false; -}); - +}); //end document.ready }); diff --git a/mobsf/templates/dynamic_analysis/android/dynamic_report.html b/mobsf/templates/dynamic_analysis/android/dynamic_report.html index 52e4d642..da2d7211 100644 --- a/mobsf/templates/dynamic_analysis/android/dynamic_report.html +++ b/mobsf/templates/dynamic_analysis/android/dynamic_report.html @@ -114,6 +114,14 @@

+ + +