From 6cd9c2eadf54faffe42d0eccd9683c1f866c6e67 Mon Sep 17 00:00:00 2001 From: Ajin Abraham Date: Mon, 24 Apr 2017 01:34:34 +0530 Subject: [PATCH] AVD test locally (#389) This PR add support for using AVD with MobSF by @matandobr Code QA and Refactoring of Android Dynamic Analysis Added functionality to remove scan results --- DynamicAnalyzer/views/android.py | 1141 --------------- DynamicAnalyzer/views/android/__init__.py | 0 DynamicAnalyzer/views/android/android_avd.py | 192 +++ .../views/android/android_dyn_shared.py | 110 ++ .../views/android/android_dynamic.py | 1297 +++++++++++++++++ .../views/android/android_virtualbox_vm.py | 22 + DynamicAnalyzer/views/android_standalone.py | 37 +- MobSF/settings.py | 255 ++-- MobSF/urls.py | 47 +- MobSF/utils.py | 2 +- MobSF/views.py | 58 +- README.md | 22 +- StaticAnalyzer/tests.py | 33 +- mobsfy_AVD.py | 317 ++++ static/dash/css/sweetalert2.min.css | 1 + static/dash/js/sweetalert2.min.js | 1 + templates/dynamic_analysis/start_test.html | 7 + templates/general/recent.html | 61 +- 18 files changed, 2296 insertions(+), 1307 deletions(-) delete mode 100644 DynamicAnalyzer/views/android.py create mode 100644 DynamicAnalyzer/views/android/__init__.py create mode 100644 DynamicAnalyzer/views/android/android_avd.py create mode 100644 DynamicAnalyzer/views/android/android_dyn_shared.py create mode 100644 DynamicAnalyzer/views/android/android_dynamic.py create mode 100644 DynamicAnalyzer/views/android/android_virtualbox_vm.py create mode 100755 mobsfy_AVD.py create mode 100644 static/dash/css/sweetalert2.min.css create mode 100644 static/dash/js/sweetalert2.min.js diff --git a/DynamicAnalyzer/views/android.py b/DynamicAnalyzer/views/android.py deleted file mode 100644 index 1d7c0fcc..00000000 --- a/DynamicAnalyzer/views/android.py +++ /dev/null @@ -1,1141 +0,0 @@ -# -*- coding: utf_8 -*- -import subprocess -import os -import re -import shutil -import tarfile -import ntpath -import io -import json -import random -import time -import unicodedata -import socket -import threading -import base64 -import sqlite3 as sq - -from django.shortcuts import render -from django.conf import settings -from django.template.defaulttags import register -from django.http import HttpResponseRedirect, HttpResponse -from django.utils.html import escape - -from StaticAnalyzer.models import StaticAnalyzerAndroid -from DynamicAnalyzer.pyWebProxy.pywebproxy import * -from MobSF.utils import PrintException, is_number, python_list, isBase64, isFileExists, getADB -from MalwareAnalyzer.views import MalwareCheck - -#=================================== -# Dynamic Analyzer Calls begins here! -#=================================== -''' -Need to improve RCE Detection on Framework, audit all subprocess calls -TCP Connnection to screenshot service needs to be secured. -Globals! -''' -tcp_server_mode = "off" # ScreenCast TCP Service Status - - -@register.filter -def key(d, key_name): - return d.get(key_name) - - -def DynamicAnalyzer(request): - - print "\n[INFO] Dynamic Analysis Started" - try: - if request.method == 'POST': - MD5 = request.POST['md5'] - PKG = request.POST['pkg'] - LNCH = request.POST['lng'] - if re.findall(";|\$\(|\|\||&&", PKG) or re.findall(";|\$\(|\|\||&&", LNCH): - print "[ATTACK] Possible RCE" - return HttpResponseRedirect('/error/') - m = re.match('^[0-9a-f]{32}$', MD5) - if m: - # Delete ScreenCast Cache - SCREEN_FILE = os.path.join(settings.SCREEN_DIR, 'screen.png') - if os.path.exists(SCREEN_FILE): - os.remove(SCREEN_FILE) - # Delete Contents of Screenshot Dir - SCRDIR = os.path.join( - settings.UPLD_DIR, MD5 + '/screenshots-apk/') - if os.path.isdir(SCRDIR): - shutil.rmtree(SCRDIR) - # Start DM - Proxy("", "", "", "") - if settings.REAL_DEVICE: - print "\n[INFO] MobSF will perform Dynamic Analysis on real Android Device" - else: - # Refersh VM - RefreshVM(settings.UUID, settings.SUUID, settings.VBOX) - context = {'md5': MD5, - 'pkg': PKG, - 'lng': LNCH, - 'title': 'Start Testing', } - template = "dynamic_analysis/start_test.html" - return render(request, template, context) - else: - return HttpResponseRedirect('/error/') - else: - return HttpResponseRedirect('/error/') - except: - PrintException("[ERROR] DynamicAnalyzer") - return HttpResponseRedirect('/error/') - -# AJAX - - -def GetEnv(request): - - print "\n[INFO] Setting up Dynamic Analysis Environment" - try: - if request.method == 'POST': - data = {} - MD5 = request.POST['md5'] - PKG = request.POST['pkg'] - LNCH = request.POST['lng'] - if re.findall(";|\$\(|\|\||&&", PKG) or re.findall(";|\$\(|\|\||&&", LNCH): - print "[ATTACK] Possible RCE" - return HttpResponseRedirect('/error/') - m = re.match('^[0-9a-f]{32}$', MD5) - if m: - DIR = settings.BASE_DIR - APP_DIR = os.path.join( - settings.UPLD_DIR, MD5 + '/') # APP DIRECTORY - APP_FILE = MD5 + '.apk' # NEW FILENAME - APP_PATH = APP_DIR + APP_FILE # APP PATH - TOOLS_DIR = os.path.join( - DIR, 'DynamicAnalyzer/tools/') # TOOLS DIR - DWD_DIR = settings.DWD_DIR - PROXY_IP = settings.PROXY_IP # Proxy IP - PORT = str(settings.PORT) # Proxy Port - WebProxy(APP_DIR, PROXY_IP, PORT) - Connect(TOOLS_DIR) - # Change True to support non-activity components - InstallRun(TOOLS_DIR, APP_PATH, PKG, LNCH, True) - SCREEN_WIDTH, SCREEN_HEIGHT = GetRes() - data = {'ready': 'yes', - 'screen_witdth': SCREEN_WIDTH, - 'screen_height': SCREEN_HEIGHT, } - return HttpResponse(json.dumps(data), content_type='application/json') - else: - return HttpResponseRedirect('/error/') - else: - return HttpResponseRedirect('/error/') - except: - PrintException("[ERROR] Setting up Dynamic Analysis Environment") - return HttpResponseRedirect('/error/') -# AJAX - - -def TakeScreenShot(request): - print "\n[INFO] Taking Screenshot" - try: - if request.method == 'POST': - MD5 = request.POST['md5'] - m = re.match('^[0-9a-f]{32}$', MD5) - if m: - data = {} - r = random.randint(1, 1000000) - DIR = settings.BASE_DIR - # make sure that list only png from this directory - SCRDIR = os.path.join( - settings.UPLD_DIR, MD5 + '/screenshots-apk/') - TOOLSDIR = os.path.join( - DIR, 'DynamicAnalyzer/tools/') # TOOLS DIR - adb = getADB(TOOLSDIR) - subprocess.call([adb, "-s", getIdentifier(), "shell", - "screencap", "-p", "/data/local/screen.png"]) - subprocess.call([adb, "-s", getIdentifier(), "pull", - "/data/local/screen.png", SCRDIR + "screenshot-" + str(r) + ".png"]) - print "\n[INFO] Screenshot Taken" - data = {'screenshot': 'yes'} - return HttpResponse(json.dumps(data), content_type='application/json') - else: - return HttpResponseRedirect('/error/') - else: - return HttpResponseRedirect('/error/') - except: - PrintException("[ERROR] Taking Screenshot") - return HttpResponseRedirect('/error/') -# AJAX - - -def ScreenCast(request): - print "\n[INFO] Invoking ScreenCast Service in VM/Device" - try: - global tcp_server_mode - data = {} - if (request.method == 'POST'): - mode = request.POST['mode'] - TOOLSDIR = os.path.join( - settings.BASE_DIR, 'DynamicAnalyzer/tools/') # TOOLS DIR - adb = getADB(TOOLSDIR) - IP = settings.SCREEN_IP - PORT = str(settings.SCREEN_PORT) - if mode == "on": - args = [adb, "-s", getIdentifier(), "shell", "am", "startservice", - "-a", IP + ":" + PORT, "opensecurity.screencast/.StartScreenCast"] - data = {'status': 'on'} - tcp_server_mode = "on" - elif mode == "off": - args = [adb, "-s", getIdentifier(), "shell", "am", - "force-stop", "opensecurity.screencast"] - data = {'status': 'off'} - tcp_server_mode = "off" - if (mode in ["on", "off"]): - try: - subprocess.call(args) - t = threading.Thread(target=ScreenCastService) - t.setDaemon(True) - t.start() - except: - PrintException("[ERROR] Casting Screen") - data = {'status': 'error'} - return HttpResponse(json.dumps(data), content_type='application/json') - else: - data = {'status': 'failed'} - else: - data = {'status': 'failed'} - return HttpResponse(json.dumps(data), content_type='application/json') - except: - PrintException("[ERROR] Casting Screen") - return HttpResponseRedirect('/error/') - -# AJAX - - -def clip_dump(request): - """ - Dump Android ClipBoard - """ - print "\n[INFO] Starting Clipboard Dump Service in VM/Device" - try: - data = {} - if request.method == 'POST': - tools_dir = os.path.join( - settings.BASE_DIR, 'DynamicAnalyzer/tools/') # TOOLS DIR - adb = getADB(tools_dir) - args = [adb, "-s", getIdentifier(), "shell", "am", "startservice", - "opensecurity.clipdump/.ClipDumper"] - try: - subprocess.call(args) - data = {'status': 'success'} - except: - PrintException("[ERROR] Dumping Clipboard") - data = {'status': 'error'} - else: - data = {'status': 'failed'} - return HttpResponse(json.dumps(data), content_type='application/json') - except: - PrintException("[ERROR] Dumping Clipboard") - return HttpResponseRedirect('/error/') - -# AJAX - - -def Touch(request): - print "\n[INFO] Sending Touch Events" - try: - data = {} - if (request.method == 'POST') and (is_number(request.POST['x'])) and (is_number(request.POST['y'])): - x_axis = request.POST['x'] - y_axis = request.POST['y'] - TOOLSDIR = os.path.join( - settings.BASE_DIR, 'DynamicAnalyzer/tools/') # TOOLS DIR - adb = getADB(TOOLSDIR) - args = [adb, "-s", getIdentifier(), "shell", "input", - "tap", x_axis, y_axis] - data = {'status': 'success'} - try: - subprocess.call(args) - except: - data = {'status': 'error'} - PrintException("[ERROR] Performing Touch Action") - else: - data = {'status': 'failed'} - return HttpResponse(json.dumps(data), content_type='application/json') - except: - PrintException("[ERROR] Sending Touch Events") - return HttpResponseRedirect('/error/') -# AJAX - - -def ExecuteADB(request): - print "\n[INFO] Executing ADB Commands" - try: - if request.method == 'POST': - data = {} - CMD = request.POST['cmd'] - ''' - #Allow it Since it's functional - if re.findall(";|\$\(|\|\||&&",CMD): - print "[ATTACK] Possible RCE" - return HttpResponseRedirect('/error/') - ''' - TOOLSDIR = os.path.join( - settings.BASE_DIR, 'DynamicAnalyzer/tools/') # TOOLS DIR - adb = getADB(TOOLSDIR) - args = [adb, "-s", getIdentifier()] + CMD.split(' ') - resp = "error" - try: - resp = subprocess.check_output(args) - except: - PrintException("[ERROR] Executing ADB Commands") - data = {'cmd': 'yes', 'resp': resp} - return HttpResponse(json.dumps(data), content_type='application/json') - else: - return HttpResponseRedirect('/error/') - except: - PrintException("[ERROR] Executing ADB Commands") - return HttpResponseRedirect('/error/') - -# AJAX - - -def MobSFCA(request): - try: - if request.method == 'POST': - data = {} - act = request.POST['action'] - TOOLSDIR = os.path.join( - settings.BASE_DIR, 'DynamicAnalyzer/tools/') # TOOLS DIR - ROOTCA = os.path.join( - settings.BASE_DIR, 'DynamicAnalyzer/pyWebProxy/ca.crt') - adb = getADB(TOOLSDIR) - if act == "install": - print "\n[INFO] Installing MobSF RootCA" - subprocess.call([adb, "-s", getIdentifier(), "push", - ROOTCA, "/data/local/tmp/" + settings.ROOT_CA]) - subprocess.call([adb, "-s", getIdentifier(), "shell", "su", "-c", "cp", "/data/local/tmp/" + - settings.ROOT_CA, "/system/etc/security/cacerts/" + settings.ROOT_CA]) - subprocess.call([adb, "-s", getIdentifier(), "shell", "su", "-c", - "chmod", "644", "/system/etc/security/cacerts/" + settings.ROOT_CA]) - subprocess.call([adb, "-s", getIdentifier(), "shell", - "rm", "/data/local/tmp/" + settings.ROOT_CA]) - data = {'ca': 'installed'} - elif act == "remove": - print "\n[INFO] Removing MobSF RootCA" - subprocess.call([adb, "-s", getIdentifier(), "shell", "su", "-c", - "rm", "/system/etc/security/cacerts/" + settings.ROOT_CA]) - data = {'ca': 'removed'} - return HttpResponse(json.dumps(data), content_type='application/json') - else: - return HttpResponseRedirect('/error/') - except: - PrintException("[ERROR] MobSF RootCA Handler") - return HttpResponseRedirect('/error/') - -# AJAX - - -def FinalTest(request): - # Closing Services in VM/Device - global tcp_server_mode - print "\n[INFO] Collecting Data and Cleaning Up" - try: - if request.method == 'POST': - data = {} - MD5 = request.POST['md5'] - PACKAGE = request.POST['pkg'] - if re.findall(";|\$\(|\|\||&&", PACKAGE): - print "[ATTACK] Possible RCE" - return HttpResponseRedirect('/error/') - m = re.match('^[0-9a-f]{32}$', MD5) - if m: - # Stop ScreenCast Client if it is running - tcp_server_mode = "off" - DIR = settings.BASE_DIR - APKDIR = os.path.join(settings.UPLD_DIR, MD5 + '/') - TOOLSDIR = os.path.join( - DIR, 'DynamicAnalyzer/tools/') # TOOLS DIR - adb = getADB(TOOLSDIR) - # Change to check output of subprocess when analysis is done - # Can't RCE - os.system(adb + ' -s ' + getIdentifier() + - ' logcat -d dalvikvm:W ActivityManager:I > "' + APKDIR + 'logcat.txt"') - print "\n[INFO] Downloading Logcat logs" - #os.system(adb+' -s '+getIdentifier()+' logcat -d Xposed:I *:S > "'+APKDIR + 'x_logcat.txt"') - subprocess.call([adb, "-s", getIdentifier(), "pull", - "/data/data/de.robv.android.xposed.installer/log/error.log", APKDIR + "x_logcat.txt"]) - - print "\n[INFO] Downloading Droidmon API Monitor Logcat logs" - # Can't RCE - os.system(adb + ' -s ' + getIdentifier() + - ' shell dumpsys > "' + APKDIR + 'dump.txt"') - print "\n[INFO] Downloading Dumpsys logs" - subprocess.call([adb, "-s", getIdentifier(), - "shell", "am", "force-stop", PACKAGE]) - print "\n[INFO] Stopping Application" - - subprocess.call([adb, "-s", getIdentifier(), "shell", - "am", "force-stop", "opensecurity.screencast"]) - print "\n[INFO] Stopping ScreenCast Service" - - data = {'final': 'yes'} - return HttpResponse(json.dumps(data), content_type='application/json') - else: - return HttpResponseRedirect('/error/') - else: - return HttpResponseRedirect('/error/') - except: - PrintException("[ERROR] Clean Up") - return HttpResponseRedirect('/error/') -# AJAX - - -def DumpData(request): - # Closing External Services and Dumping data - print "\n[INFO] Device Data Dump" - try: - if request.method == 'POST': - data = {} - PACKAGE = request.POST['pkg'] - MD5 = request.POST['md5'] - m = re.match('^[0-9a-f]{32}$', MD5) - if m: - if re.findall(";|\$\(|\|\||&&", PACKAGE): - print "[ATTACK] Possible RCE" - return HttpResponseRedirect('/error/') - DIR = settings.BASE_DIR - APKDIR = os.path.join(settings.UPLD_DIR, MD5 + '/') - TOOLSDIR = os.path.join( - DIR, 'DynamicAnalyzer/tools/') # TOOLS DIR - adb = getADB(TOOLSDIR) - # Let's try to close Proxy a bit early as we don't have much - # control on the order of thread execution - Proxy("", "", "", "") - print "\n[INFO] Deleting Dump Status File" - subprocess.call([adb, "-s", getIdentifier(), - "shell", "rm", "/sdcard/mobsec_status"]) - print "\n[INFO] Creating TAR of Application Files." - subprocess.call([adb, "-s", getIdentifier(), "shell", "am", "startservice", - "-a", PACKAGE, "opensecurity.ajin.datapusher/.GetPackageLocation"]) - print "\n[INFO] Waiting for TAR dump to complete..." - if settings.REAL_DEVICE: - timeout = settings.DEVICE_TIMEOUT - else: - timeout = settings.VM_TIMEOUT - start_time = time.time() - while True: - current_time = time.time() - if "MOBSEC-TAR-CREATED" in subprocess.check_output([adb, "-s", getIdentifier(), "shell", "cat", "/sdcard/mobsec_status"]): - break - if (current_time - start_time) > timeout: - print "\n[ERROR] TAR Generation Failed. Process timed out." - break - print "\n[INFO] Dumping Application Files from Device/VM" - subprocess.call([adb, "-s", getIdentifier(), "pull", - "/data/local/" + PACKAGE + ".tar", APKDIR + PACKAGE + ".tar"]) - print "\n[INFO] Stopping ADB" - subprocess.call([adb, "-s", getIdentifier(), "kill-server"]) - data = {'dump': 'yes'} - return HttpResponse(json.dumps(data), content_type='application/json') - else: - return HttpResponseRedirect('/error/') - else: - return HttpResponseRedirect('/error/') - except: - PrintException("[ERROR] Device Data Dump") - return HttpResponseRedirect('/error/') -# AJAX - - -def ExportedActivityTester(request): - print "\n[INFO] Exported Activity Tester" - try: - MD5 = request.POST['md5'] - PKG = request.POST['pkg'] - m = re.match('^[0-9a-f]{32}$', MD5) - if m: - if re.findall(";|\$\(|\|\||&&", PKG): - print "[ATTACK] Possible RCE" - return HttpResponseRedirect('/error/') - if request.method == 'POST': - DIR = settings.BASE_DIR - APP_DIR = os.path.join(settings.UPLD_DIR, MD5 + '/') - TOOLS_DIR = os.path.join( - DIR, 'DynamicAnalyzer/tools/') # TOOLS DIR - SCRDIR = os.path.join(APP_DIR, 'screenshots-apk/') - data = {} - adb = getADB(TOOLS_DIR) - - DB = StaticAnalyzerAndroid.objects.filter(MD5=MD5) - if DB.exists(): - print "\n[INFO] Fetching Exported Activity List from DB" - EXPORTED_ACT = python_list(DB[0].EXPORTED_ACT) - if EXPORTED_ACT: - n = 0 - print "\n[INFO] Starting Exported Activity Tester..." - print "\n[INFO] " + str(len(EXPORTED_ACT)) + " Exported Activities Identified" - for line in EXPORTED_ACT: - try: - n += 1 - print "\n[INFO] Launching Exported Activity - " + str(n) + ". " + line - subprocess.call( - [adb, "-s", getIdentifier(), "shell", "am", "start", "-n", PKG + "/" + line]) - Wait(4) - subprocess.call( - [adb, "-s", getIdentifier(), "shell", "screencap", "-p", "/data/local/screen.png"]) - #? get appended from Air :-() if activity names are used - subprocess.call( - [adb, "-s", getIdentifier(), "pull", "/data/local/screen.png", SCRDIR + "expact-" + str(n) + ".png"]) - print "\n[INFO] Activity Screenshot Taken" - subprocess.call( - [adb, "-s", getIdentifier(), "shell", "am", "force-stop", PKG]) - print "\n[INFO] Stopping App" - except: - PrintException( - "[ERROR] Exported Activity Tester") - data = {'expacttest': 'done'} - else: - print "\n[INFO] Exported Activity Tester - No Activity Found!" - data = {'expacttest': 'noact'} - return HttpResponse(json.dumps(data), content_type='application/json') - else: - print "\n[ERROR] Entry does not exist in DB." - return HttpResponseRedirect('/error/') - else: - return HttpResponseRedirect('/error/') - else: - return HttpResponseRedirect('/error/') - except: - PrintException("ERROR] Exported Activity Tester") - return HttpResponseRedirect('/error/') - -# AJAX - - -def ActivityTester(request): - print "\n[INFO] Activity Tester" - try: - MD5 = request.POST['md5'] - PKG = request.POST['pkg'] - m = re.match('^[0-9a-f]{32}$', MD5) - if m: - if re.findall(";|\$\(|\|\||&&", PKG): - print "[ATTACK] Possible RCE" - return HttpResponseRedirect('/error/') - if request.method == 'POST': - DIR = settings.BASE_DIR - APP_DIR = os.path.join(settings.UPLD_DIR, MD5 + '/') - TOOLS_DIR = os.path.join( - DIR, 'DynamicAnalyzer/tools/') # TOOLS DIR - SCRDIR = os.path.join(APP_DIR, 'screenshots-apk/') - data = {} - adb = getADB(TOOLS_DIR) - DB = StaticAnalyzerAndroid.objects.filter(MD5=MD5) - if DB.exists(): - print "\n[INFO] Fetching Activity List from DB" - ACTIVITIES = python_list(DB[0].ACTIVITIES) - if ACTIVITIES: - n = 0 - print "\n[INFO] Starting Activity Tester..." - print "\n[INFO] " + str(len(ACTIVITIES)) + " Activities Identified" - for line in ACTIVITIES: - try: - n += 1 - print "\n[INFO] Launching Activity - " + str(n) + ". " + line - subprocess.call( - [adb, "-s", getIdentifier(), "shell", "am", "start", "-n", PKG + "/" + line]) - Wait(4) - subprocess.call( - [adb, "-s", getIdentifier(), "shell", "screencap", "-p", "/data/local/screen.png"]) - #? get appended from Air :-() if activity names are used - subprocess.call( - [adb, "-s", getIdentifier(), "pull", "/data/local/screen.png", SCRDIR + "act-" + str(n) + ".png"]) - print "\n[INFO] Activity Screenshot Taken" - subprocess.call( - [adb, "-s", getIdentifier(), "shell", "am", "force-stop", PKG]) - print "\n[INFO] Stopping App" - except: - PrintException("Activity Tester") - data = {'acttest': 'done'} - else: - print "\n[INFO] Activity Tester - No Activity Found!" - data = {'acttest': 'noact'} - return HttpResponse(json.dumps(data), content_type='application/json') - else: - print "\n[ERROR] Entry does not exist in DB." - return HttpResponseRedirect('/error/') - else: - return HttpResponseRedirect('/error/') - else: - return HttpResponseRedirect('/error/') - except: - PrintException("[ERROR] Activity Tester") - return HttpResponseRedirect('/error/') - - -def Wait(sec): - print "\n[INFO] Waiting for " + str(sec) + " seconds..." - time.sleep(sec) - - -def Report(request): - print "\n[INFO] Dynamic Analysis Report Generation" - try: - if request.method == 'GET': - MD5 = request.GET['md5'] - PKG = request.GET['pkg'] - if re.findall(";|\$\(|\|\||&&", PKG): - print "[ATTACK] Possible RCE" - return HttpResponseRedirect('/error/') - m = re.match('^[0-9a-f]{32}$', MD5) - if m: - DIR = settings.BASE_DIR - APP_DIR = os.path.join( - settings.UPLD_DIR, MD5 + '/') # APP DIRECTORY - DWD_DIR = settings.DWD_DIR - DRDMONAPISLOC = os.path.join(APP_DIR, 'x_logcat.txt') - API_NET, API_BASE64, API_FILEIO, API_BINDER, API_CRYPTO, API_DEVICEINFO, API_CNTVL, API_SMS, API_SYSPROP, API_DEXLOADER, API_RELECT, API_ACNTMNGER, API_CMD = APIAnalysis( - PKG, DRDMONAPISLOC) - URL, DOMAINS, EMAIL, CLIPBOARD, HTTP, XML, SQLiteDB, OtherFiles = RunAnalysis( - APP_DIR, MD5, PKG) - Download(MD5, DWD_DIR, APP_DIR, PKG) - # Only After Download Process is Done - IMGS = [] - ACTIMGS = [] - ACT = {} - EXPACTIMGS = [] - EXPACT = {} - if os.path.exists(os.path.join(DWD_DIR, MD5 + "-screenshots-apk/")): - try: - for img in os.listdir(os.path.join(DWD_DIR, MD5 + "-screenshots-apk/")): - if img.endswith(".png"): - if img.startswith("act"): - ACTIMGS.append(img) - elif img.startswith("expact"): - EXPACTIMGS.append(img) - else: - IMGS.append(img) - DB = StaticAnalyzerAndroid.objects.filter(MD5=MD5) - if DB.exists(): - print "\n[INFO] Fetching Exported Activity & Activity List from DB" - EXPORTED_ACT = python_list(DB[0].EXPORTED_ACT) - ACTDESC = python_list(DB[0].ACTIVITIES) - if ACTIMGS: - if len(ACTIMGS) == len(ACTDESC): - ACT = dict(zip(ACTIMGS, ACTDESC)) - if EXPACTIMGS: - if len(EXPACTIMGS) == len(EXPORTED_ACT): - EXPACT = dict( - zip(EXPACTIMGS, EXPORTED_ACT)) - else: - print "\n[WARNING] Entry does not exists in the DB." - except: - PrintException("[ERROR] Screenshot Sorting") - - context = {'emails': EMAIL, - 'urls': URL, - 'domains': DOMAINS, - 'clipboard': CLIPBOARD, - 'md5': MD5, - 'http': HTTP, - 'xml': XML, - 'sqlite': SQLiteDB, - 'others': OtherFiles, - 'imgs': IMGS, - 'acttest': ACT, - 'expacttest': EXPACT, - 'net': API_NET, - 'base64': API_BASE64, - 'crypto': API_CRYPTO, - 'fileio': API_FILEIO, - 'binder': API_BINDER, - 'divinfo': API_DEVICEINFO, - 'cntval': API_CNTVL, - 'sms': API_SMS, - 'sysprop': API_SYSPROP, - 'dexload': API_DEXLOADER, - 'reflect': API_RELECT, - 'sysman': API_ACNTMNGER, - 'process': API_CMD, - 'pkg': PKG, - 'title': 'Dynamic Analysis'} - template = "dynamic_analysis/dynamic_analysis.html" - return render(request, template, context) - else: - return HttpResponseRedirect('/error/') - else: - return HttpResponseRedirect('/error/') - except: - PrintException("[ERROR] Dynamic Analysis Report Generation") - return HttpResponseRedirect('/error/') - - -def RefreshVM(uuid, snapshot_uuid, vbox_exe): - print "\n[INFO] Refreshing MobSF VM" - try: - # Close VM - args = [vbox_exe, 'controlvm', uuid, 'poweroff'] - subprocess.call(args) - print "\n[INFO] VM Closed" - # Restore Snapshot - args = [vbox_exe, 'snapshot', uuid, 'restore', snapshot_uuid] - subprocess.call(args) - print "\n[INFO] VM Restore Snapshot" - # Start Fresh VM - args = [vbox_exe, 'startvm', uuid] - subprocess.call(args) - print "\n[INFO] VM Starting" - except: - PrintException("[ERROR] Refreshing MobSF VM") - - -def WebProxy(APKDIR, ip, port): - print "\n[INFO] Starting Web Proxy" - try: - Proxy(ip, port, APKDIR, "on") - except: - PrintException("[ERROR] Starting Web Proxy") - - -def Connect(TOOLSDIR): - print "\n[INFO] Connecting to VM/Device" - try: - adb = getADB(TOOLSDIR) - subprocess.call([adb, "kill-server"]) - subprocess.call([adb, "start-server"]) - print "\n[INFO] ADB Started" - Wait(5) - print "\n[INFO] Connecting to VM/Device" - subprocess.call([adb, "connect", getIdentifier()]) - subprocess.call([adb, "-s", getIdentifier(), "wait-for-device"]) - print "\n[INFO] Mounting" - if settings.REAL_DEVICE: - subprocess.call([adb, "-s", getIdentifier(), "shell", - "su", "-c", "mount", "-o", "rw,remount,rw", "/system"]) - else: - subprocess.call([adb, "-s", getIdentifier(), "shell", - "su", "-c", "mount", "-o", "rw,remount,rw", "/system"]) - # This may not work for VMs other than the default MobSF VM - subprocess.call([adb, "-s", getIdentifier(), "shell", "mount", - "-o", "rw,remount", "-t", "rfs", "/dev/block/sda6", "/system"]) - except: - PrintException("[ERROR] Connecting to VM/Device") - - -def InstallRun(TOOLSDIR, APKPATH, PACKAGE, LAUNCH, isACT): - print "\n[INFO] Starting App for Dynamic Analysis" - try: - adb = getADB(TOOLSDIR) - print "\n[INFO] Installing APK" - subprocess.call([adb, "-s", getIdentifier(), "install", "-r", APKPATH]) - if isACT: - runApp = PACKAGE + "/" + LAUNCH - print "\n[INFO] Launching APK Main Activity" - subprocess.call([adb, "-s", getIdentifier(), - "shell", "am", "start", "-n", runApp]) - else: - print "\n[INFO] App Doesn't have a Main Activity" - # Handle Service or Give Choice to Select in Future. - pass - print "[INFO] Testing Environment is Ready!" - except: - PrintException("[ERROR] Starting App for Dynamic Analysis") - - -def HandleSqlite(SFile): - print "\n[INFO] SQLite DB Extraction" - try: - data = '' - con = sq.connect(SFile) - cur = con.cursor() - cur.execute("SELECT name FROM sqlite_master WHERE type='table';") - tables = cur.fetchall() - for table in tables: - data += "\nTABLE: " + str(table[0]).decode('utf8', 'ignore') + \ - " \n=====================================================\n" - cur.execute("PRAGMA table_info('%s')" % table) - rows = cur.fetchall() - head = '' - for r in rows: - z = r[1] - if type(z) is unicode: - z = unicodedata.normalize( - 'NFKD', z).encode('ascii', 'ignore') - head += str(z).decode('utf8', 'ignore') + " | " - data += head + " \n=====================================================================\n" - cur.execute("SELECT * FROM '%s'" % table) - rows = cur.fetchall() - for r in rows: - dat = '' - for x in r: - if type(x) is unicode: - x = unicodedata.normalize( - 'NFKD', x).encode('ascii', 'ignore') - dat += str(x).decode('utf8', 'ignore') + " | " - data += dat + "\n" - return data - except: - PrintException("[ERROR] SQLite DB Extraction") - pass - - -def APIAnalysis(PKG, LOCATION): - print "\n[INFO] Dynamic API Analysis" - dat = "" - API_BASE64 = [] - API_FILEIO = [] - API_RELECT = [] - API_SYSPROP = [] - API_CNTRSLVR = [] - API_CNTVAL = [] - API_BINDER = [] - API_CRYPTO = [] - API_ACNTMNGER = [] - API_DEVICEINFO = [] - API_NET = [] - API_DEXLOADER = [] - API_CMD = [] - API_SMS = [] - try: - with open(LOCATION, "r") as f: - dat = f.readlines() - ID = "Droidmon-apimonitor-" + PKG + ":" - for line in dat: - line = line.decode('utf8', 'ignore') - if (ID) in line: - # print "LINE: " + line - param, value = line.split(ID, 1) - # print "PARAM is :" + param - # print "Value is :"+ value - try: - APIs = json.loads(value, strict=False) - RET = '' - CLS = '' - MTD = '' - ARGS = '' - MTD = str(APIs["method"]) - CLS = str(APIs["class"]) - # print "Called Class: " + CLS - # print "Called Method: " + MTD - if APIs.get('return'): - RET = str(APIs["return"]) - # print "Return Data: " + RET - else: - # print "No Return Data" - RET = "No Return Data" - if APIs.get('args'): - ARGS = str(APIs["args"]) - # print "Passed Arguments" + ARGS - else: - # print "No Arguments Passed" - ARGS = "No Arguments Passed" - # XSS Safe - D = "
METHOD: " + \ - escape(MTD) + "
ARGUMENTS: " + escape(ARGS) + \ - "
RETURN DATA: " + escape(RET) - - if re.findall("android.util.Base64", CLS): - # Base64 Decode - if ("decode" in MTD): - args_list = python_list(ARGS) - if isBase64(args_list[0]): - D += '
Decoded String: ' + \ - escape(base64.b64decode(args_list[0])) - API_BASE64.append(D) - if re.findall('libcore.io|android.app.SharedPreferencesImpl$EditorImpl', CLS): - API_FILEIO.append(D) - if re.findall('java.lang.reflect', CLS): - API_RELECT.append(D) - if re.findall('android.content.ContentResolver|android.location.Location|android.media.AudioRecord|android.media.MediaRecorder|android.os.SystemProperties', CLS): - API_SYSPROP.append(D) - if re.findall('android.app.Activity|android.app.ContextImpl|android.app.ActivityThread', CLS): - API_BINDER.append(D) - if re.findall('javax.crypto.spec.SecretKeySpec|javax.crypto.Cipher|javax.crypto.Mac', CLS): - API_CRYPTO.append(D) - if re.findall('android.accounts.AccountManager|android.app.ApplicationPackageManager|android.app.NotificationManager|android.net.ConnectivityManager|android.content.BroadcastReceiver', CLS): - API_ACNTMNGER.append(D) - if re.findall('android.telephony.TelephonyManager|android.net.wifi.WifiInfo|android.os.Debug', CLS): - API_DEVICEINFO.append(D) - if re.findall('dalvik.system.BaseDexClassLoader|dalvik.system.DexFile|dalvik.system.DexClassLoader|dalvik.system.PathClassLoader', CLS): - API_DEXLOADER.append(D) - if re.findall('java.lang.Runtime|java.lang.ProcessBuilder|java.io.FileOutputStream|java.io.FileInputStream|android.os.Process', CLS): - API_CMD.append(D) - if re.findall('android.content.ContentValues', CLS): - API_CNTVAL.append(D) - if re.findall('android.telephony.SmsManager', CLS): - API_SMS.append(D) - if re.findall('java.net.URL|org.apache.http.impl.client.AbstractHttpClient', CLS): - API_NET.append(D) - except: - PrintException("[ERROR] Parsing JSON Failed for: " + value) - except: - PrintException("[ERROR] Dynamic API Analysis") - pass - return list(set(API_NET)), list(set(API_BASE64)), list(set(API_FILEIO)), list(set(API_BINDER)), list(set(API_CRYPTO)), list(set(API_DEVICEINFO)), list(set(API_CNTVAL)), list(set(API_SMS)), list(set(API_SYSPROP)), list(set(API_DEXLOADER)), list(set(API_RELECT)), list(set(API_ACNTMNGER)), list(set(API_CMD)) - - -def Download(MD5, DWDDIR, APKDIR, PKG): - print "\n[INFO] Generating Downloads" - try: - Logcat = os.path.join(APKDIR, 'logcat.txt') - xLogcat = os.path.join(APKDIR, 'x_logcat.txt') - Dumpsys = os.path.join(APKDIR, 'dump.txt') - Sshot = os.path.join(APKDIR, 'screenshots-apk/') - Web = os.path.join(APKDIR, 'WebTraffic.txt') - Star = os.path.join(APKDIR, PKG + '.tar') - - DLogcat = os.path.join(DWDDIR, MD5 + '-logcat.txt') - DxLogcat = os.path.join(DWDDIR, MD5 + '-x_logcat.txt') - DDumpsys = os.path.join(DWDDIR, MD5 + '-dump.txt') - DSshot = os.path.join(DWDDIR, MD5 + '-screenshots-apk/') - DWeb = os.path.join(DWDDIR, MD5 + '-WebTraffic.txt') - DStar = os.path.join(DWDDIR, MD5 + '-AppData.tar') - - # Delete existing data - dellist = [DLogcat, DxLogcat, DDumpsys, DSshot, DWeb, DStar] - for item in dellist: - if os.path.isdir(item): - shutil.rmtree(item) - elif os.path.isfile(item): - os.remove(item) - # Copy new data - shutil.copyfile(Logcat, DLogcat) - shutil.copyfile(xLogcat, DxLogcat) - shutil.copyfile(Dumpsys, DDumpsys) - try: - shutil.copytree(Sshot, DSshot) - except: - pass - try: - shutil.copyfile(Web, DWeb) - except: - pass - try: - shutil.copyfile(Star, DStar) - except: - pass - except: - PrintException("[ERROR] Generating Downloads") - - -def RunAnalysis(APKDIR, MD5, PACKAGE): - print "\n[INFO] Dynamic File Analysis" - Web = os.path.join(APKDIR, 'WebTraffic.txt') - Logcat = os.path.join(APKDIR, 'logcat.txt') - xLogcat = os.path.join(APKDIR, 'x_logcat.txt') - traffic = '' - wb = '' - xlg = '' - DOMAINS = {} - logcat_data = [] - CLIPBOARD = [] - CLIP_TAG = "I/CLIPDUMP-INFO-LOG" - try: - with io.open(Web, mode='r', encoding="utf8", errors="ignore") as f: - wb = f.read() - except: - pass - - with io.open(Logcat, mode='r', encoding="utf8", errors="ignore") as f: - logcat_data = f.readlines() - traffic = ''.join(logcat_data) - with io.open(xLogcat, mode='r', encoding="utf8", errors="ignore") as f: - xlg = f.read() - traffic = wb + traffic + xlg - for log_line in logcat_data: - if log_line.startswith(CLIP_TAG): - CLIPBOARD.append(log_line.replace(CLIP_TAG, "Process ID ")) - URLS = [] - # URLs My Custom regex - p = re.compile(ur'((?:https?://|s?ftps?://|file://|javascript:|data:|www\d{0,3}[.])[\w().=/;,#:@?&~*+!$%\'{}-]+)', re.UNICODE) - urllist = re.findall(p, traffic.lower()) - # Domain Extraction and Malware Check - print "[INFO] Performing Malware Check on extracted Domains" - DOMAINS = MalwareCheck(urllist) - for url in urllist: - if url not in URLS: - URLS.append(url) - - # Email Etraction Regex - EMAILS = [] - regex = re.compile(("[\w.-]+@[\w-]+\.[\w.]+")) - for email in regex.findall(traffic.lower()): - if ((email not in EMAILS) and (not email.startswith('//'))): - if email == "yodleebanglore@gmail.com": - pass - else: - EMAILS.append(email) - # Extract Device Data - try: - TARLOC = os.path.join(APKDIR, PACKAGE + '.tar') - UNTAR_DIR = os.path.join(APKDIR, 'DYNAMIC_DeviceData/') - if not os.path.exists(UNTAR_DIR): - os.makedirs(UNTAR_DIR) - with tarfile.open(TARLOC) as tar: - try: - tar.extractall(UNTAR_DIR) - except: - pass - except: - PrintException("[ERROR] TAR EXTRACTION FAILED") - # Do Static Analysis on Data from Device - xmlfiles = '' - SQLiteDB = '' - OtherFiles = '' - typ = '' - UNTAR_DIR = os.path.join(APKDIR, 'DYNAMIC_DeviceData/') - if not os.path.exists(UNTAR_DIR): - os.makedirs(UNTAR_DIR) - try: - for dirName, subDir, files in os.walk(UNTAR_DIR): - for jfile in files: - file_path = os.path.join(UNTAR_DIR, dirName, jfile) - if "+" in file_path: - shutil.move(file_path, file_path.replace("+", "x")) - file_path = file_path.replace("+", "x") - fileparam = file_path.replace(UNTAR_DIR, '') - if jfile == 'lib': - pass - else: - if jfile.endswith('.xml'): - typ = 'xml' - xmlfiles += "" + escape(fileparam) + "" - else: - with io.open(file_path, mode='r', encoding="utf8", errors="ignore") as f: - b = f.read(6) - if b == "SQLite": - typ = 'db' - SQLiteDB += "" + \ - escape(fileparam) + "" - elif not jfile.endswith('.DS_Store'): - typ = 'others' - OtherFiles += "" + \ - escape(fileparam) + "" - except: - PrintException("[ERROR] Dynamic File Analysis") - pass - return URLS, DOMAINS, EMAILS, CLIPBOARD, wb, xmlfiles, SQLiteDB, OtherFiles - - -def View(request): - print "\n[INFO] Viewing File" - try: - typ = '' - fil = '' - rtyp = '' - dat = '' - m = re.match('^[0-9a-f]{32}$', request.GET['md5']) - if m: - fil = request.GET['file'] - MD5 = request.GET['md5'] - typ = request.GET['type'] - SRC = os.path.join(settings.UPLD_DIR, MD5 + '/DYNAMIC_DeviceData/') - sfile = os.path.join(SRC, fil) - # Prevent Directory Traversal Attacks - if (("../" in fil) or ("%2e%2e" in fil) or (".." in fil) or ("%252e" in fil)): - return HttpResponseRedirect('/error/') - else: - with io.open(sfile, mode='r', encoding="utf8", errors="ignore") as f: - dat = f.read() - if ((fil.endswith('.xml')) and (typ == 'xml')): - rtyp = 'xml' - elif typ == 'db': - dat = HandleSqlite(sfile) - rtyp = 'asciidoc' - elif typ == 'others': - rtyp = 'asciidoc' - else: - return HttpResponseRedirect('/error/') - context = {'title': escape(ntpath.basename(fil)), 'file': escape( - ntpath.basename(fil)), 'dat': dat, 'type': rtyp, } - template = "general/view.html" - return render(request, template, context) - - else: - return HttpResponseRedirect('/error/') - except: - PrintException("[ERROR] Viewing File") - return HttpResponseRedirect('/error/') - - -def ScreenCastService(): - global tcp_server_mode - print "\n[INFO] ScreenCast Service Status: " + tcp_server_mode - try: - SCREEN_DIR = settings.SCREEN_DIR - if not os.path.exists(SCREEN_DIR): - os.makedirs(SCREEN_DIR) - - s = socket.socket() - if tcp_server_mode == "on": - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - ADDR = (settings.SCREEN_IP, settings.SCREEN_PORT) - s.bind(ADDR) - s.listen(10) - while (tcp_server_mode == "on"): - ss, address = s.accept() - print "Got Connection from: ", address[0] - if settings.REAL_DEVICE: - IP = settings.DEVICE_IP - else: - IP = settings.VM_IP - if address[0] == IP: - ''' - Very Basic Check to ensure that only MobSF VM/Device is allowed to connect - to MobSF ScreenCast Service. - ''' - with open(SCREEN_DIR + 'screen.png', 'wb') as f: - while True: - data = ss.recv(1024) - if not data: - break - f.write(data) - else: - print "\n[ATTACK] An unknown client :" + address[0] + " is trying to make a connection with MobSF ScreenCast Service!" - elif tcp_server_mode == "off": - s.close() - except: - s.close() - PrintException("[ERROR] ScreenCast Server") - pass - - -def GetRes(): - print "\n[INFO] Getting Screen Resolution" - try: - TOOLSDIR = os.path.join( - settings.BASE_DIR, 'DynamicAnalyzer/tools/') # TOOLS DIR - adb = getADB(TOOLSDIR) - resp = subprocess.check_output( - [adb, "-s", getIdentifier(), "shell", "dumpsys", "window"]) - resp = resp.split("\n") - res = "" - for line in resp: - if "mUnrestrictedScreen" in line: - res = line - break - res = res.split("(0,0)")[1] - res = res.strip() - res = res.split("x") - if len(res) == 2: - return res[0], res[1] - #width, height - return "", "" - except: - PrintException("[ERROR] Getting Screen Resolution") - return "", "" - - -# Helper Functions -def getIdentifier(): - try: - if settings.REAL_DEVICE: - return settings.DEVICE_IP + ":" + str(settings.DEVICE_ADB_PORT) - else: - return settings.VM_IP + ":" + str(settings.VM_ADB_PORT) - except: - PrintException( - "[ERROR] Getting ADB Connection Identifier for Device/VM") diff --git a/DynamicAnalyzer/views/android/__init__.py b/DynamicAnalyzer/views/android/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/DynamicAnalyzer/views/android/android_avd.py b/DynamicAnalyzer/views/android/android_avd.py new file mode 100644 index 00000000..4430b280 --- /dev/null +++ b/DynamicAnalyzer/views/android/android_avd.py @@ -0,0 +1,192 @@ +""" +Android Dynamic Analyzer for Android AVD (ARM) VM +""" +import os +import io +import time +import platform +import shutil +import subprocess + +from DynamicAnalyzer.views.android.android_dyn_shared import get_identifier +from MobSF.utils import PrintException +from django.conf import settings + + +def stop_avd(adb): + """Stop AVD""" + print "\n[INFO] Stopping MobSF Emulator" + try: + # adb -s emulator-xxxx emu kill + FNULL = open(os.devnull, 'w') + args = [adb, '-s', get_identifier(), 'emu', 'kill'] + subprocess.call(args, stderr=FNULL) + except: + PrintException("[ERROR] Stopping MobSF Emulator") + + +def delete_avd(avd_path, avd_name): + """Delete AVD""" + print "\n[INFO] Deleting emulator files" + try: + config_file = os.path.join(avd_path, avd_name + '.ini') + if os.path.exists(config_file): + os.remove(config_file) + ''' + # todo: Sometimes there is an error here because of the locks that avd + # does - check this out + ''' + avd_folder = os.path.join(avd_path, avd_name + '.avd') + if os.path.isdir(avd_folder): + shutil.rmtree(avd_folder) + except: + PrintException("[ERROR] Deleting emulator files") + + +def duplicate_avd(avd_path, reference_name, dup_name): + """Duplicate AVD""" + print "\n[INFO] Duplicating MobSF Emulator" + try: + reference_ini = os.path.join(avd_path, reference_name + '.ini') + dup_ini = os.path.join(avd_path, dup_name + '.ini') + reference_avd = os.path.join(avd_path, reference_name + '.avd') + dup_avd = os.path.join(avd_path, dup_name + '.avd') + + # Copy the files from the referenve avd to the one-time analysis avd + shutil.copyfile(reference_ini, dup_ini) + shutil.copytree(reference_avd, dup_avd) + + # Replacing every occuration of the reference avd name to the dup one + for path_to_update in [dup_ini, + os.path.join(dup_avd, 'hardware-qemu.ini'), + os.path.join(dup_avd, 'config.ini') + ]: + with io.open(path_to_update, mode='r', encoding="utf8", errors="ignore") as fled: + replaced_file = fled.read() + replaced_file = replaced_file.replace(reference_name, dup_name) + with io.open(path_to_update, 'w') as fled: + fled.write(replaced_file) + except: + PrintException("[ERROR] Duplicating MobSF Emulator") + + +def start_avd(emulator, avd_name, emulator_port): + """Start AVD""" + print "\n[INFO] Starting MobSF Emulator" + try: + args = [ + emulator, + '-avd', + avd_name, + "-no-snapshot-save", + "-netspeed", + "full", + "-netdelay", + "none", + "-port", + str(emulator_port), + ] + + if platform.system() == 'Darwin': + # There is a strage error in mac with the dyld one in a while.. + # this should fix it.. + if 'DYLD_FALLBACK_LIBRARY_PATH' in os.environ.keys(): + del os.environ['DYLD_FALLBACK_LIBRARY_PATH'] + + subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except: + PrintException("[ERROR] Starting MobSF Emulator") + + +def refresh_avd(adb, avd_path, reference_name, dup_name, emulator): + """Refresh AVD""" + print "\n[INFO] Refreshing MobSF Emulator" + try: + # Stop existing emulator on the spesified port + stop_avd(adb) + + # Windows has annoying lock system, it takes time for it to remove the locks after we stopped the emulator + if platform.system() == 'Windows': + time.sleep(3) + + # Delete old emulator + delete_avd(avd_path, dup_name) + + # Copy and replace the contents of the reference machine + duplicate_avd(avd_path, reference_name, dup_name) + + # Start emulator + start_avd(emulator, dup_name, settings.AVD_ADB_PORT) + except: + PrintException("[ERROR] Refreshing MobSF VM") + + +def avd_load_wait(adb): + """Wait for AVD Load""" + try: + emulator = get_identifier() + + print "[INFO] Wait for emulator to load" + args = [adb, + "-s", + emulator, + "wait-for-device"] + subprocess.call(args) + + print "[INFO] Wait for dev.boot_complete loop" + while True: + args = [adb, + "-s", + emulator, + "shell", + "getprop", + "dev.bootcomplete"] + try: + result = subprocess.check_output(args) + except: + result = None + if result is not None and result.strip() == "1": + break + else: + time.sleep(1) + + print "[INFO] Wait for sys.boot_complete loop" + while True: + args = [adb, + "-s", + emulator, + "shell", + "getprop", + "sys.boot_completed"] + try: + result = subprocess.check_output(args) + except: + result = None + if result is not None and result.strip() == "1": + break + else: + time.sleep(1) + + print "[INFO] Wait for svc.boot_complete loop" + while True: + args = [adb, + "-s", + emulator, + "shell", + "getprop", + "init.svc.bootanim"] + try: + result = subprocess.check_output(args) + except: + result = None + if result is not None and result.strip() == "stopped": + break + else: + time.sleep(1) + time.sleep(5) + # Remount the partitions for RW + subprocess.call([adb, "-s", emulator, "remount"]) + return True + except: + PrintException("[ERROR] emulator did not boot properly") + return False diff --git a/DynamicAnalyzer/views/android/android_dyn_shared.py b/DynamicAnalyzer/views/android/android_dyn_shared.py new file mode 100644 index 00000000..8310f745 --- /dev/null +++ b/DynamicAnalyzer/views/android/android_dyn_shared.py @@ -0,0 +1,110 @@ +"""Shared Functions for Android Dynamic Analyzer""" +# -*- coding: utf_8 -*- +import subprocess +import os +import time +from django.conf import settings +from DynamicAnalyzer.pyWebProxy.pywebproxy import Proxy +from MobSF.utils import PrintException, getADB + +def connect(toolsdir): + """Connect to VM/Device""" + print "\n[INFO] Connecting to VM/Device" + try: + adb = getADB(toolsdir) + subprocess.call([adb, "kill-server"]) + subprocess.call([adb, "start-server"]) + print "\n[INFO] ADB Started" + wait(5) + print "\n[INFO] Connecting to VM/Device" + subprocess.call([adb, "connect", get_identifier()]) + subprocess.call([adb, "-s", get_identifier(), "wait-for-device"]) + print "\n[INFO] Mounting" + if settings.ANDROID_DYNAMIC_ANALYZER == "MobSF_REAL_DEVICE": + subprocess.call([adb, "-s", get_identifier(), "shell", + "su", "-c", "mount", "-o", "rw,remount,rw", "/system"]) + else: + subprocess.call([adb, "-s", get_identifier(), "shell", + "su", "-c", "mount", "-o", "rw,remount,rw", "/system"]) + # This may not work for VMs other than the default MobSF VM + subprocess.call([adb, "-s", get_identifier(), "shell", "mount", + "-o", "rw,remount", "-t", "rfs", "/dev/block/sda6", "/system"]) + except: + PrintException("[ERROR] Connecting to VM/Device") + + +def install_and_run(toolsdir, apk_path, package, launcher, is_activity): + """Install APK and Run it""" + print "\n[INFO] Starting App for Dynamic Analysis" + try: + adb = getADB(toolsdir) + print "\n[INFO] Installing APK" + subprocess.call([adb, "-s", get_identifier(), + "install", "-r", apk_path]) + if is_activity: + run_app = package + "/" + launcher + print "\n[INFO] Launching APK Main Activity" + subprocess.call([adb, "-s", get_identifier(), + "shell", "am", "start", "-n", run_app]) + else: + print "\n[INFO] App Doesn't have a Main Activity" + # Handle Service or Give Choice to Select in Future. + print "[INFO] Testing Environment is Ready!" + except: + PrintException("[ERROR] Starting App for Dynamic Analysis") + + +def wait(sec): + """Wait in Seconds""" + print "\n[INFO] Waiting for " + str(sec) + " seconds..." + time.sleep(sec) + + +def web_proxy(apk_dir, ip_address, port): + """Run MITM Proxy""" + print "\n[INFO] Starting Web Proxy" + try: + Proxy(ip_address, port, apk_dir, "on") + except: + PrintException("[ERROR] Starting Web Proxy") + + +def get_res(): + """Get Screen Resolution or Device or VM""" + print "\n[INFO] Getting Screen Resolution" + try: + toolsdir = os.path.join( + settings.BASE_DIR, 'DynamicAnalyzer/tools/') # TOOLS DIR + adb = getADB(toolsdir) + resp = subprocess.check_output( + [adb, "-s", get_identifier(), "shell", "dumpsys", "window"]) + resp = resp.split("\n") + res = "" + for line in resp: + if "mUnrestrictedScreen" in line: + res = line + break + res = res.split("(0,0)")[1] + res = res.strip() + res = res.split("x") + if len(res) == 2: + return res[0], res[1] + # width, height + return "", "" + except: + PrintException("[ERROR] Getting Screen Resolution") + return "", "" + + +def get_identifier(): + """Get Device Type""" + try: + if settings.ANDROID_DYNAMIC_ANALYZER == "MobSF_REAL_DEVICE": + return settings.DEVICE_IP + ":" + str(settings.DEVICE_ADB_PORT) + elif settings.ANDROID_DYNAMIC_ANALYZER == "MobSF_AVD": + return 'emulator-' + str(settings.AVD_ADB_PORT) + else: + return settings.VM_IP + ":" + str(settings.VM_ADB_PORT) + except: + PrintException( + "[ERROR] Getting ADB Connection Identifier for Device/VM") diff --git a/DynamicAnalyzer/views/android/android_dynamic.py b/DynamicAnalyzer/views/android/android_dynamic.py new file mode 100644 index 00000000..2ee839f9 --- /dev/null +++ b/DynamicAnalyzer/views/android/android_dynamic.py @@ -0,0 +1,1297 @@ +"""Core Functions of Android Dynamic Analysis""" +# -*- coding: utf_8 -*- +import subprocess +import os +import re +import shutil +import tarfile +import ntpath +import io +import json +import random +import time +import socket +import unicodedata +import threading +import base64 +import sqlite3 as sq + +from django.shortcuts import render +from django.conf import settings +from django.template.defaulttags import register +from django.http import HttpResponseRedirect, HttpResponse +from django.utils.html import escape + +from StaticAnalyzer.models import StaticAnalyzerAndroid +from DynamicAnalyzer.pyWebProxy.pywebproxy import Proxy +from DynamicAnalyzer.views.android.android_avd import ( + avd_load_wait, + refresh_avd, + stop_avd +) +from DynamicAnalyzer.views.android.android_virtualbox_vm import ( + refresh_vm +) +from DynamicAnalyzer.views.android.android_dyn_shared import ( + connect, + install_and_run, + web_proxy, + get_res, + get_identifier, + wait, +) +from MobSF.utils import PrintException, is_number, python_list, isBase64, getADB +from MalwareAnalyzer.views import MalwareCheck + +#=========================================== +# Dynamic Analyzer Related Views for Android +#=========================================== + + +''' +Need to improve RCE Detection on Framework, audit all subprocess calls +TCP Connnection to screenshot service needs to be secured. +Globals! +''' + +TCP_SERVER_MODE = "off" # ScreenCast TCP Service Status + + +@register.filter +def key(d, key_name): + """To get dict element by key name in template""" + return d.get(key_name) + + +def android_dynamic_analyzer(request): + """Android Dynamic Analyzer View""" + print "\n[INFO] Dynamic Analysis Started" + try: + if request.method == 'POST': + md5_hash = request.POST['md5'] + package = request.POST['pkg'] + launcher = request.POST['lng'] + if re.findall(r';|\$\(|\|\||&&', package) or re.findall(r';|\$\(|\|\||&&', launcher): + print "[ATTACK] Possible RCE" + return HttpResponseRedirect('/error/') + if re.match('^[0-9a-f]{32}$', md5_hash): + # Delete ScreenCast Cache + screen_file = os.path.join(settings.SCREEN_DIR, 'screen.png') + if os.path.exists(screen_file): + os.remove(screen_file) + # Delete Contents of Screenshot Dir + screen_dir = os.path.join( + settings.UPLD_DIR, md5_hash + '/screenshots-apk/') + if os.path.isdir(screen_dir): + shutil.rmtree(screen_dir) + else: + os.makedirs(screen_dir) + # Start DM + Proxy("", "", "", "") + toolsdir = os.path.join( + settings.BASE_DIR, 'DynamicAnalyzer/tools/') # TOOLS DIR + adb = getADB(toolsdir) + if settings.ANDROID_DYNAMIC_ANALYZER == "MobSF_REAL_DEVICE": + print "\n[INFO] MobSF will perform Dynamic Analysis on real Android Device" + is_avd = False + elif settings.ANDROID_DYNAMIC_ANALYZER == "MobSF_AVD": + # adb, avd_path, reference_name, dup_name, emulator + is_avd = True + refresh_avd(adb, settings.AVD_PATH, settings.AVD_REFERENCE_NAME, + settings.AVD_DUP_NAME, settings.AVD_EMULATOR) + else: + # Refersh VM + is_avd = False + refresh_vm(settings.UUID, settings.SUUID, settings.VBOX) + context = {'md5': md5_hash, + 'pkg': package, + 'lng': launcher, + 'title': 'Start Testing', + 'AVD': is_avd, } + template = "dynamic_analysis/start_test.html" + return render(request, template, context) + else: + return HttpResponseRedirect('/error/') + else: + return HttpResponseRedirect('/error/') + except: + PrintException("[ERROR] DynamicAnalyzer") + return HttpResponseRedirect('/error/') + +# AJAX + + +def get_env(request): + """Get Dynamic Analysis Environment for Android""" + print "\n[INFO] Setting up Dynamic Analysis Environment" + try: + if request.method == 'POST': + data = {} + md5_hash = request.POST['md5'] + package = request.POST['pkg'] + launcher = request.POST['lng'] + if re.findall(r";|\$\(|\|\||&&", package) or re.findall(r";|\$\(|\|\||&&", launcher): + print "[ATTACK] Possible RCE" + return HttpResponseRedirect('/error/') + if re.match('^[0-9a-f]{32}$', md5_hash): + base_dir = settings.BASE_DIR + app_dir = os.path.join( + settings.UPLD_DIR, md5_hash + '/') # APP DIRECTORY + app_file = md5_hash + '.apk' # NEW FILENAME + app_path = app_dir + app_file # APP PATH + toolsdir = os.path.join( + base_dir, 'DynamicAnalyzer/tools/') # TOOLS DIR + adb = getADB(toolsdir) + if settings.ANDROID_DYNAMIC_ANALYZER == "MobSF_AVD": + proxy_ip = '127.0.0.1' + else: + proxy_ip = settings.PROXY_IP # Proxy IP + port = str(settings.PORT) # Proxy Port + web_proxy(app_dir, proxy_ip, port) + # AVD only needs to wait, vm needs the connect function + if settings.ANDROID_DYNAMIC_ANALYZER == "MobSF_AVD": + if not avd_load_wait(adb): + print "\n[WARNING] ADB Load Wait Failed" + return HttpResponseRedirect('/error/') + else: + connect(toolsdir) + # Change True to support non-activity components + install_and_run(toolsdir, app_path, package, launcher, True) + screen_width, screen_width = get_res() + data = {'ready': 'yes', + 'screen_witdth': screen_width, + 'screen_height': screen_width, } + return HttpResponse(json.dumps(data), content_type='application/json') + else: + return HttpResponseRedirect('/error/') + else: + return HttpResponseRedirect('/error/') + except: + PrintException("[ERROR] Setting up Dynamic Analysis Environment") + return HttpResponseRedirect('/error/') +# AJAX + + +def take_screenshot(request): + """Take Screenshot""" + print "\n[INFO] Taking Screenshot" + try: + if request.method == 'POST': + md5_hash = request.POST['md5'] + if re.match('^[0-9a-f]{32}$', md5_hash): + data = {} + rand_int = random.randint(1, 1000000) + base_dir = settings.BASE_DIR + # make sure that list only png from this directory + screen_dir = os.path.join( + settings.UPLD_DIR, md5_hash + '/screenshots-apk/') + if not os.path.exists(screen_dir): + os.makedirs(screen_dir) + toolsdir = os.path.join( + base_dir, 'DynamicAnalyzer/tools/') # TOOLS DIR + adb = getADB(toolsdir) + subprocess.call([adb, + "-s", + get_identifier(), + "shell", + "screencap", + "-p", + "/data/local/screen.png"]) + subprocess.call([adb, + "-s", + get_identifier(), + "pull", + "/data/local/screen.png", + screen_dir + "screenshot-" + str(rand_int) + ".png"]) + print "\n[INFO] Screenshot Taken" + data = {'screenshot': 'yes'} + return HttpResponse(json.dumps(data), content_type='application/json') + else: + return HttpResponseRedirect('/error/') + else: + return HttpResponseRedirect('/error/') + except: + PrintException("[ERROR] Taking Screenshot") + return HttpResponseRedirect('/error/') +# AJAX + + +def screen_cast(request): + """Start or Stop ScreenCast Feature""" + print "\n[INFO] Invoking ScreenCast Service in VM/Device" + try: + global TCP_SERVER_MODE + data = {} + if request.method == 'POST': + mode = request.POST['mode'] + toolsdir = os.path.join( + settings.BASE_DIR, 'DynamicAnalyzer/tools/') # TOOLS DIR + adb = getADB(toolsdir) + if settings.ANDROID_DYNAMIC_ANALYZER == "MobSF_AVD": + ip_address = '10.0.2.2' + else: + ip_address = settings.SCREEN_IP + port = str(settings.SCREEN_PORT) + if mode == "on": + args = [adb, + "-s", + get_identifier(), + "shell", + "am", + "startservice", + "-a", + ip_address + ":" + port, + "opensecurity.screencast/.StartScreenCast"] + data = {'status': 'on'} + TCP_SERVER_MODE = "on" + elif mode == "off": + args = [adb, + "-s", + get_identifier(), + "shell", + "am", + "force-stop", + "opensecurity.screencast"] + data = {'status': 'off'} + TCP_SERVER_MODE = "off" + if (mode in ["on", "off"]): + try: + subprocess.call(args) + screen_trd = threading.Thread(target=screencast_service) + screen_trd.setDaemon(True) + screen_trd.start() + except: + PrintException("[ERROR] Casting Screen") + data = {'status': 'error'} + return HttpResponse(json.dumps(data), content_type='application/json') + else: + data = {'status': 'failed'} + else: + data = {'status': 'failed'} + return HttpResponse(json.dumps(data), content_type='application/json') + except: + PrintException("[ERROR] Casting Screen") + return HttpResponseRedirect('/error/') + +# AJAX + + +def clip_dump(request): + """Dump Android ClipBoard""" + print "\n[INFO] Starting Clipboard Dump Service in VM/Device" + try: + data = {} + if request.method == 'POST': + toolsdir = os.path.join( + settings.BASE_DIR, 'DynamicAnalyzer/tools/') # TOOLS DIR + adb = getADB(toolsdir) + args = [adb, + "-s", + get_identifier(), + "shell", + "am", + "startservice", + "opensecurity.clipdump/.ClipDumper"] + try: + subprocess.call(args) + data = {'status': 'success'} + except: + PrintException("[ERROR] Dumping Clipboard") + data = {'status': 'error'} + else: + data = {'status': 'failed'} + return HttpResponse(json.dumps(data), content_type='application/json') + except: + PrintException("[ERROR] Dumping Clipboard") + return HttpResponseRedirect('/error/') + +# AJAX + + +def touch(request): + """Sending Touch Events""" + print "\n[INFO] Sending Touch Events" + try: + data = {} + if (request.method == 'POST') and (is_number(request.POST['x'])) and (is_number(request.POST['y'])): + x_axis = request.POST['x'] + y_axis = request.POST['y'] + toolsdir = os.path.join( + settings.BASE_DIR, 'DynamicAnalyzer/tools/') # TOOLS DIR + adb = getADB(toolsdir) + args = [adb, + "-s", + get_identifier(), + "shell", + "input", + "tap", + x_axis, + y_axis] + data = {'status': 'success'} + try: + subprocess.call(args) + except: + data = {'status': 'error'} + PrintException("[ERROR] Performing Touch Action") + else: + data = {'status': 'failed'} + return HttpResponse(json.dumps(data), content_type='application/json') + except: + PrintException("[ERROR] Sending Touch Events") + return HttpResponseRedirect('/error/') +# AJAX + + +def execute_adb(request): + """Execute ADB Commands""" + print "\n[INFO] Executing ADB Commands" + try: + if request.method == 'POST': + data = {} + cmd = request.POST['cmd'] + ''' + Allow dangerous chars as it's functional + TODO: Deal with it. + ''' + toolsdir = os.path.join( + settings.BASE_DIR, 'DynamicAnalyzer/tools/') # TOOLS DIR + adb = getADB(toolsdir) + args = [adb, + "-s", + get_identifier()] + cmd.split(' ') + resp = "error" + try: + resp = subprocess.check_output(args) + except: + PrintException("[ERROR] Executing ADB Commands") + data = {'cmd': 'yes', 'resp': resp} + return HttpResponse(json.dumps(data), content_type='application/json') + else: + return HttpResponseRedirect('/error/') + except: + PrintException("[ERROR] Executing ADB Commands") + return HttpResponseRedirect('/error/') + +# AJAX + + +def mobsf_ca(request): + """Install and Remove MobSF Proxy RootCA""" + try: + if request.method == 'POST': + data = {} + act = request.POST['action'] + toolsdir = os.path.join( + settings.BASE_DIR, 'DynamicAnalyzer/tools/') # TOOLS DIR + rootca = os.path.join( + settings.BASE_DIR, 'DynamicAnalyzer/pyWebProxy/ca.crt') + adb = getADB(toolsdir) + if act == "install": + print "\n[INFO] Installing MobSF RootCA" + subprocess.call([adb, + "-s", + get_identifier(), + "push", + rootca, + "/data/local/tmp/" + settings.ROOT_CA]) + if settings.ANDROID_DYNAMIC_ANALYZER == "MobSF_AVD": + # For some reason, avd emulator does not have cp binary + subprocess.call([adb, + "-s", + get_identifier(), + "shell", + "/data/local/tmp/busybox", + "cp", + "/data/local/tmp/" + settings.ROOT_CA, + "/system/etc/security/cacerts/" + settings.ROOT_CA]) + subprocess.call([adb, + "-s", + get_identifier(), + "shell", + "chmod", + "644", + "/system/etc/security/cacerts/" + settings.ROOT_CA]) + else: + subprocess.call([adb, + "-s", + get_identifier(), + "shell", + "su", + "-c", + "cp", + "/data/local/tmp/" + settings.ROOT_CA, + "/system/etc/security/cacerts/" + settings.ROOT_CA]) + subprocess.call([adb, + "-s", + get_identifier(), + "shell", + "su", + "-c", + "chmod", + "644", + "/system/etc/security/cacerts/" + settings.ROOT_CA]) + subprocess.call([adb, + "-s", + get_identifier(), + "shell", + "rm", + "/data/local/tmp/" + settings.ROOT_CA]) + data = {'ca': 'installed'} + elif act == "remove": + print "\n[INFO] Removing MobSF RootCA" + if settings.ANDROID_DYNAMIC_ANALYZER == "MobSF_AVD": + subprocess.call([adb, + "-s", + get_identifier(), + "shell", + "rm", + "/system/etc/security/cacerts/" + settings.ROOT_CA]) + else: + subprocess.call([adb, + "-s", + get_identifier(), + "shell", + "su", + "-c", + "rm", + "/system/etc/security/cacerts/" + settings.ROOT_CA]) + data = {'ca': 'removed'} + return HttpResponse(json.dumps(data), content_type='application/json') + else: + return HttpResponseRedirect('/error/') + except: + PrintException("[ERROR] MobSF RootCA Handler") + return HttpResponseRedirect('/error/') + +# AJAX + + +def final_test(request): + """Collecting Data and Cleanup""" + global TCP_SERVER_MODE + print "\n[INFO] Collecting Data and Cleaning Up" + try: + if request.method == 'POST': + data = {} + md5_hash = request.POST['md5'] + package = request.POST['pkg'] + if re.findall(r";|\$\(|\|\||&&", package): + print "[ATTACK] Possible RCE" + return HttpResponseRedirect('/error/') + if re.match('^[0-9a-f]{32}$', md5_hash): + # Stop ScreenCast Client if it is running + TCP_SERVER_MODE = "off" + base_dir = settings.BASE_DIR + apk_dir = os.path.join(settings.UPLD_DIR, md5_hash + '/') + toolsdir = os.path.join( + base_dir, 'DynamicAnalyzer/tools/') # TOOLS DIR + adb = getADB(toolsdir) + # Change to check output of subprocess when analysis is done + # Can't RCE + os.system(adb + ' -s ' + get_identifier() + + ' logcat -d dalvikvm:W ActivityManager:I > "' + apk_dir + 'logcat.txt"') + print "\n[INFO] Downloading Logcat logs" + subprocess.call([adb, + "-s", + get_identifier(), + "pull", + "/data/data/de.robv.android.xposed.installer/log/error.log", + apk_dir + "x_logcat.txt"]) + + print "\n[INFO] Downloading Droidmon API Monitor Logcat logs" + # Can't RCE + os.system(adb + ' -s ' + get_identifier() + + ' shell dumpsys > "' + apk_dir + 'dump.txt"') + print "\n[INFO] Downloading Dumpsys logs" + subprocess.call([adb, + "-s", + get_identifier(), + "shell", + "am", + "force-stop", + package]) + print "\n[INFO] Stopping Application" + + subprocess.call([adb, + "-s", + get_identifier(), + "shell", + "am", + "force-stop", + "opensecurity.screencast"]) + print "\n[INFO] Stopping ScreenCast Service" + + if settings.ANDROID_DYNAMIC_ANALYZER == "MobSF_AVD": + stop_avd(adb) + + data = {'final': 'yes'} + return HttpResponse(json.dumps(data), content_type='application/json') + else: + return HttpResponseRedirect('/error/') + else: + return HttpResponseRedirect('/error/') + except: + PrintException("[ERROR] Clean Up") + return HttpResponseRedirect('/error/') +# AJAX + + +def dump_data(request): + """Downloading Application Data from Device""" + print "\n[INFO] Downloading Application Data from Device" + try: + if request.method == 'POST': + data = {} + package = request.POST['pkg'] + md5_hash = request.POST['md5'] + if re.match('^[0-9a-f]{32}$', md5_hash): + if re.findall(r";|\$\(|\|\||&&", package): + print "[ATTACK] Possible RCE" + return HttpResponseRedirect('/error/') + base_dir = settings.BASE_DIR + apk_dir = os.path.join(settings.UPLD_DIR, md5_hash + '/') + toolsdir = os.path.join( + base_dir, 'DynamicAnalyzer/tools/') # TOOLS DIR + adb = getADB(toolsdir) + # Let's try to close Proxy a bit early as we don't have much + # control on the order of thread execution + Proxy("", "", "", "") + print "\n[INFO] Deleting Dump Status File" + subprocess.call([adb, + "-s", + get_identifier(), + "shell", + "rm", + "/sdcard/mobsec_status"]) + print "\n[INFO] Creating TAR of Application Files." + if settings.ANDROID_DYNAMIC_ANALYZER == "MobSF_AVD": + # tar -cvf /data/local/"+pkg+".tar /data/data/"+pkg+"/", + subprocess.call([adb, + "-s", + get_identifier(), + "shell", + "/data/local/tmp/tar.sh", + package]) + else: + subprocess.call([adb, + "-s", + get_identifier(), + "shell", + "am", + "startservice", + "-a", + package, + "opensecurity.ajin.datapusher/.GetPackageLocation"]) + print "\n[INFO] Waiting for TAR dump to complete..." + if settings.ANDROID_DYNAMIC_ANALYZER == "MobSF_REAL_DEVICE": + timeout = settings.DEVICE_TIMEOUT + else: + timeout = settings.VM_TIMEOUT + start_time = time.time() + while True: + current_time = time.time() + if "MOBSEC-TAR-CREATED" in subprocess.check_output([adb, + "-s", + get_identifier(), + "shell", + "cat", + "/sdcard/mobsec_status"]): + break + if (current_time - start_time) > timeout: + print "\n[ERROR] TAR Generation Failed. Process timed out." + break + print "\n[INFO] Dumping Application Files from Device/VM" + subprocess.call([adb, + "-s", + get_identifier(), + "pull", + "/data/local/" + package + ".tar", + apk_dir + package + ".tar"]) + print "\n[INFO] Stopping ADB" + subprocess.call([adb, + "-s", + get_identifier(), + "kill-server"]) + data = {'dump': 'yes'} + return HttpResponse(json.dumps(data), content_type='application/json') + else: + return HttpResponseRedirect('/error/') + else: + return HttpResponseRedirect('/error/') + except: + PrintException("[ERROR] Downloading Application Data from Device") + return HttpResponseRedirect('/error/') +# AJAX + + +def exported_activity_tester(request): + """Exported Activity Tester""" + print "\n[INFO] Exported Activity Tester" + try: + md5_hash = request.POST['md5'] + package = request.POST['pkg'] + if re.match('^[0-9a-f]{32}$', md5_hash): + if re.findall(r";|\$\(|\|\||&&", package): + print "[ATTACK] Possible RCE" + return HttpResponseRedirect('/error/') + if request.method == 'POST': + base_dir = settings.BASE_DIR + app_dir = os.path.join(settings.UPLD_DIR, md5_hash + '/') + toolsdir = os.path.join( + base_dir, 'DynamicAnalyzer/tools/') # TOOLS DIR + screen_dir = os.path.join(app_dir, 'screenshots-apk/') + if not os.path.exists(screen_dir): + os.makedirs(screen_dir) + data = {} + adb = getADB(toolsdir) + + static_android_db = StaticAnalyzerAndroid.objects.filter( + MD5=md5_hash) + if static_android_db.exists(): + print "\n[INFO] Fetching Exported Activity List from DB" + exported_act = python_list( + static_android_db[0].EXPORTED_ACT) + if exported_act: + exp_act_no = 0 + print "\n[INFO] Starting Exported Activity Tester..." + print "\n[INFO] " + str(len(exported_act)) + \ + " Exported Activities Identified" + for line in exported_act: + try: + exp_act_no += 1 + print "\n[INFO] Launching Exported Activity - " + \ + str(exp_act_no) + ". " + line + subprocess.call( + [adb, + "-s", + get_identifier(), + "shell", + "am", + "start", + "-n", + package + "/" + line]) + # AVD is much slower, it should get extra time + if settings.ANDROID_DYNAMIC_ANALYZER == "MobSF_AVD": + wait(8) + else: + wait(4) + subprocess.call( + [adb, + "-s", + get_identifier(), + "shell", "screencap", + "-p", + "/data/local/screen.png"]) + #? get appended from Air :-() if activity names are used + subprocess.call( + [adb, + "-s", + get_identifier(), + "pull", + "/data/local/screen.png", + screen_dir + "expact-" + str(exp_act_no) + ".png"]) + print "\n[INFO] Activity Screenshot Taken" + subprocess.call( + [adb, + "-s", + get_identifier(), + "shell", + "am", + "force-stop", + package]) + print "\n[INFO] Stopping App" + except: + PrintException( + "[ERROR] Exported Activity Tester") + data = {'expacttest': 'done'} + else: + print "\n[INFO] Exported Activity Tester - No Activity Found!" + data = {'expacttest': 'noact'} + return HttpResponse(json.dumps(data), content_type='application/json') + else: + print "\n[ERROR] Entry does not exist in DB." + return HttpResponseRedirect('/error/') + else: + return HttpResponseRedirect('/error/') + else: + return HttpResponseRedirect('/error/') + except: + PrintException("ERROR] Exported Activity Tester") + return HttpResponseRedirect('/error/') + +# AJAX + + +def activity_tester(request): + """Activity Tester""" + print "\n[INFO] Activity Tester" + try: + md5_hash = request.POST['md5'] + package = request.POST['pkg'] + if re.match('^[0-9a-f]{32}$', md5_hash): + if re.findall(r";|\$\(|\|\||&&", package): + print "[ATTACK] Possible RCE" + return HttpResponseRedirect('/error/') + if request.method == 'POST': + base_dir = settings.BASE_DIR + app_dir = os.path.join(settings.UPLD_DIR, md5_hash + '/') + toolsdir = os.path.join( + base_dir, 'DynamicAnalyzer/tools/') # TOOLS DIR + screen_dir = os.path.join(app_dir, 'screenshots-apk/') + if not os.path.exists(screen_dir): + os.makedirs(screen_dir) + data = {} + adb = getADB(toolsdir) + static_android_db = StaticAnalyzerAndroid.objects.filter( + MD5=md5_hash) + if static_android_db.exists(): + print "\n[INFO] Fetching Activity List from DB" + activities = python_list(static_android_db[0].ACTIVITIES) + if activities: + act_no = 0 + print "\n[INFO] Starting Activity Tester..." + print "\n[INFO] " + str(len(activities)) + " Activities Identified" + for line in activities: + try: + act_no += 1 + print "\n[INFO] Launching Activity - " + str(act_no) + ". " + line + subprocess.call( + [adb, + "-s", + get_identifier(), + "shell", + "am", + "start", + "-n", + package + "/" + line]) + # AVD is much slower, it should get extra time + if settings.ANDROID_DYNAMIC_ANALYZER == "MobSF_AVD": + wait(8) + else: + wait(4) + subprocess.call( + [adb, + "-s", + get_identifier(), + "shell", + "screencap", + "-p", + "/data/local/screen.png"]) + #? get appended from Air :-() if activity names are used + subprocess.call( + [adb, + "-s", + get_identifier(), + "pull", + "/data/local/screen.png", + screen_dir + "act-" + str(act_no) + ".png"]) + print "\n[INFO] Activity Screenshot Taken" + subprocess.call([adb, + "-s", + get_identifier(), + "shell", + "am", + "force-stop", + package]) + print "\n[INFO] Stopping App" + except: + PrintException("Activity Tester") + data = {'acttest': 'done'} + else: + print "\n[INFO] Activity Tester - No Activity Found!" + data = {'acttest': 'noact'} + return HttpResponse(json.dumps(data), content_type='application/json') + else: + print "\n[ERROR] Entry does not exist in DB." + return HttpResponseRedirect('/error/') + else: + return HttpResponseRedirect('/error/') + else: + return HttpResponseRedirect('/error/') + except: + PrintException("[ERROR] Activity Tester") + return HttpResponseRedirect('/error/') + + +def report(request): + """Dynamic Analysis Report Generation""" + print "\n[INFO] Dynamic Analysis Report Generation" + try: + if request.method == 'GET': + md5_hash = request.GET['md5'] + package = request.GET['pkg'] + if re.findall(r";|\$\(|\|\||&&", package): + print "[ATTACK] Possible RCE" + return HttpResponseRedirect('/error/') + if re.match('^[0-9a-f]{32}$', md5_hash): + app_dir = os.path.join( + settings.UPLD_DIR, md5_hash + '/') # APP DIRECTORY + download_dir = settings.DWD_DIR + droidmon_api_loc = os.path.join(app_dir, 'x_logcat.txt') + api_analysis_result = api_analysis(package, droidmon_api_loc) + analysis_result = run_analysis(app_dir, md5_hash, package) + download(md5_hash, download_dir, app_dir, package) + # Only After Download Process is Done + imgs = [] + act_imgs = [] + act = {} + expact_imgs = [] + exp_act = {} + if os.path.exists(os.path.join(download_dir, md5_hash + "-screenshots-apk/")): + try: + imp_path = os.path.join( + download_dir, md5_hash + "-screenshots-apk/") + for img in os.listdir(imp_path): + if img.endswith(".png"): + if img.startswith("act"): + act_imgs.append(img) + elif img.startswith("expact"): + expact_imgs.append(img) + else: + imgs.append(img) + static_android_db = StaticAnalyzerAndroid.objects.filter( + MD5=md5_hash) + if static_android_db.exists(): + print "\n[INFO] Fetching Exported Activity & Activity List from DB" + exported_act = python_list( + static_android_db[0].EXPORTED_ACT) + act_desc = python_list( + static_android_db[0].ACTIVITIES) + if act_imgs: + if len(act_imgs) == len(act_desc): + act = dict(zip(act_imgs, act_desc)) + if expact_imgs: + if len(expact_imgs) == len(exported_act): + exp_act = dict( + zip(expact_imgs, exported_act)) + else: + print "\n[WARNING] Entry does not exists in the DB." + except: + PrintException("[ERROR] Screenshot Sorting") + context = {'md5': md5_hash, + 'emails': analysis_result["emails"], + 'urls': analysis_result["urls"], + 'domains': analysis_result["domains"], + 'clipboard': analysis_result["clipboard"], + 'http': analysis_result["web_data"], + 'xml': analysis_result["xmlfiles"], + 'sqlite': analysis_result["sqlite_db"], + 'others': analysis_result["other_files"], + 'imgs': imgs, + 'acttest': act, + 'expacttest': exp_act, + 'net': api_analysis_result["api_net"], + 'base64': api_analysis_result["api_base64"], + 'crypto': api_analysis_result["api_crypto"], + 'fileio': api_analysis_result["api_fileio"], + 'binder': api_analysis_result["api_binder"], + 'divinfo': api_analysis_result["api_deviceinfo"], + 'cntval': api_analysis_result["api_cntvl"], + 'sms': api_analysis_result["api_sms"], + 'sysprop': api_analysis_result["api_sysprop"], + 'dexload': api_analysis_result["api_dexloader"], + 'reflect': api_analysis_result["api_reflect"], + 'sysman': api_analysis_result["api_acntmnger"], + 'process': api_analysis_result["api_cmd"], + 'pkg': package, + 'title': 'Dynamic Analysis'} + template = "dynamic_analysis/dynamic_analysis.html" + return render(request, template, context) + else: + return HttpResponseRedirect('/error/') + else: + return HttpResponseRedirect('/error/') + except: + PrintException("[ERROR] Dynamic Analysis Report Generation") + return HttpResponseRedirect('/error/') + + +def handle_sqlite(sfile): + """SQLite Dump - Readable Text""" + print "\n[INFO] SQLite DB Extraction" + try: + data = '' + con = sq.connect(sfile) + cur = con.cursor() + cur.execute("SELECT name FROM sqlite_master WHERE type='table';") + tables = cur.fetchall() + for table in tables: + data += "\nTABLE: " + str(table[0]).decode('utf8', 'ignore') + \ + " \n=====================================================\n" + cur.execute("PRAGMA table_info('%s')" % table) + rows = cur.fetchall() + head = '' + for sq_row in rows: + elm_data = sq_row[1] + if isinstance(elm_data) is unicode: + elm_data = unicodedata.normalize( + 'NFKD', elm_data).encode('ascii', 'ignore') + head += str(elm_data).decode('utf8', 'ignore') + " | " + data += head + " \n============================================" + \ + "=========================\n" + cur.execute("SELECT * FROM '%s'" % table) + rows = cur.fetchall() + for sq_row in rows: + dat = '' + for each_row in sq_row: + if isinstance(each_row) is unicode: + each_row = unicodedata.normalize( + 'NFKD', each_row).encode('ascii', 'ignore') + dat += str(each_row).decode('utf8', 'ignore') + " | " + data += dat + "\n" + return data + except: + PrintException("[ERROR] SQLite DB Extraction") + + +def api_analysis(package, location): + """API Analysis""" + api_analysis_result = {} + print "\n[INFO] Dynamic API Analysis" + dat = "" + api_base64 = [] + api_fileio = [] + api_reflect = [] + api_sysprop = [] + api_cntvl = [] + api_binder = [] + api_crypto = [] + api_acntmnger = [] + api_deviceinfo = [] + api_net = [] + api_dexloader = [] + api_cmd = [] + api_sms = [] + try: + with open(location, "r") as flip: + dat = flip.readlines() + res_id = "Droidmon-apimonitor-" + package + ":" + for line in dat: + line = line.decode('utf8', 'ignore') + if res_id in line: + # print "LINE: " + line + _, value = line.split(res_id, 1) + # print "PARAM is :" + param + # print "Value is :"+ value + try: + apis = json.loads(value, strict=False) + ret = '' + args = '' + mtd = str(apis["method"]) + clss = str(apis["class"]) + # print "Called Class: " + CLS + # print "Called Method: " + MTD + if apis.get('return'): + ret = str(apis["return"]) + # print "Return Data: " + RET + else: + # print "No Return Data" + ret = "No Return Data" + if apis.get('args'): + args = str(apis["args"]) + # print "Passed Arguments" + ARGS + else: + # print "No Arguments Passed" + args = "No Arguments Passed" + # XSS Safe + call_data = "
METHOD: " + \ + escape(mtd) + "
ARGUMENTS: " + escape(args) + \ + "
RETURN DATA: " + escape(ret) + + if re.findall("android.util.Base64", clss): + # Base64 Decode + if "decode" in mtd: + args_list = python_list(args) + if isBase64(args_list[0]): + call_data += '
' +\ + 'Decoded String: ' + \ + escape(base64.b64decode(args_list[0])) + api_base64.append(call_data) + if re.findall('libcore.io|android.app.SharedPreferencesImpl$EditorImpl', clss): + api_fileio.append(call_data) + if re.findall('java.lang.reflect', clss): + api_reflect.append(call_data) + if re.findall('android.content.ContentResolver|android.location.Location|android.media.AudioRecord|android.media.MediaRecorder|android.os.SystemProperties', clss): + api_sysprop.append(call_data) + if re.findall('android.app.Activity|android.app.ContextImpl|android.app.ActivityThread', clss): + api_binder.append(call_data) + if re.findall('javax.crypto.spec.SecretKeySpec|javax.crypto.Cipher|javax.crypto.Mac', clss): + api_crypto.append(call_data) + if re.findall('android.accounts.AccountManager|android.app.ApplicationPackageManager|android.app.NotificationManager|android.net.ConnectivityManager|android.content.BroadcastReceiver', clss): + api_acntmnger.append(call_data) + if re.findall('android.telephony.TelephonyManager|android.net.wifi.WifiInfo|android.os.Debug', clss): + api_deviceinfo.append(call_data) + if re.findall('dalvik.system.BaseDexClassLoader|dalvik.system.DexFile|dalvik.system.DexClassLoader|dalvik.system.PathClassLoader', clss): + api_dexloader.append(call_data) + if re.findall('java.lang.Runtime|java.lang.ProcessBuilder|java.io.FileOutputStream|java.io.FileInputStream|android.os.Process', clss): + api_cmd.append(call_data) + if re.findall('android.content.ContentValues', clss): + api_cntvl.append(call_data) + if re.findall('android.telephony.SmsManager', clss): + api_sms.append(call_data) + if re.findall('java.net.URL|org.apache.http.impl.client.AbstractHttpClient', clss): + api_net.append(call_data) + except: + PrintException("[ERROR] Parsing JSON Failed for: " + value) + except: + PrintException("[ERROR] Dynamic API Analysis") + api_analysis_result["api_net"] = list(set(api_net)) + api_analysis_result["api_base64"] = list(set(api_base64)) + api_analysis_result["api_fileio"] = list(set(api_fileio)) + api_analysis_result["api_binder"] = list(set(api_binder)) + api_analysis_result["api_crypto"] = list(set(api_crypto)) + api_analysis_result["api_deviceinfo"] = list(set(api_deviceinfo)) + api_analysis_result["api_cntvl"] = list(set(api_cntvl)) + api_analysis_result["api_sms"] = list(set(api_sms)) + api_analysis_result["api_sysprop"] = list(set(api_sysprop)) + api_analysis_result["api_dexloader"] = list(set(api_dexloader)) + api_analysis_result["api_reflect"] = list(set(api_reflect)) + api_analysis_result["api_acntmnger"] = list(set(api_acntmnger)) + api_analysis_result["api_cmd"] = list(set(api_cmd)) + return api_analysis_result + + +def download(md5_hash, download_dir, apk_dir, package): + """Generating Downloads""" + print "\n[INFO] Generating Downloads" + try: + logcat = os.path.join(apk_dir, 'logcat.txt') + xlogcat = os.path.join(apk_dir, 'x_logcat.txt') + dumpsys = os.path.join(apk_dir, 'dump.txt') + sshot = os.path.join(apk_dir, 'screenshots-apk/') + web = os.path.join(apk_dir, 'WebTraffic.txt') + star = os.path.join(apk_dir, package + '.tar') + + dlogcat = os.path.join(download_dir, md5_hash + '-logcat.txt') + dxlogcat = os.path.join(download_dir, md5_hash + '-x_logcat.txt') + ddumpsys = os.path.join(download_dir, md5_hash + '-dump.txt') + dsshot = os.path.join(download_dir, md5_hash + '-screenshots-apk/') + dweb = os.path.join(download_dir, md5_hash + '-WebTraffic.txt') + dstar = os.path.join(download_dir, md5_hash + '-AppData.tar') + + # Delete existing data + dellist = [dlogcat, dxlogcat, ddumpsys, dsshot, dweb, dstar] + for item in dellist: + if os.path.isdir(item): + shutil.rmtree(item) + elif os.path.isfile(item): + os.remove(item) + # Copy new data + shutil.copyfile(logcat, dlogcat) + shutil.copyfile(xlogcat, dxlogcat) + shutil.copyfile(dumpsys, ddumpsys) + try: + shutil.copytree(sshot, dsshot) + except: + pass + try: + shutil.copyfile(web, dweb) + except: + pass + try: + shutil.copyfile(star, dstar) + except: + pass + except: + PrintException("[ERROR] Generating Downloads") + + +def run_analysis(apk_dir, md5_hash, package): + """Run Dynamic File Analysis""" + analysis_result = {} + print "\n[INFO] Dynamic File Analysis" + web = os.path.join(apk_dir, 'WebTraffic.txt') + logcat = os.path.join(apk_dir, 'logcat.txt') + xlogcat = os.path.join(apk_dir, 'x_logcat.txt') + traffic = '' + web_data = '' + xlg = '' + domains = {} + logcat_data = [] + clipboard = [] + clip_tag = "I/CLIPDUMP-INFO-LOG" + try: + with io.open(web, mode='r', encoding="utf8", errors="ignore") as flip: + web_data = flip.read() + except: + pass + with io.open(logcat, mode='r', encoding="utf8", errors="ignore") as flip: + logcat_data = flip.readlines() + traffic = ''.join(logcat_data) + with io.open(xlogcat, mode='r', encoding="utf8", errors="ignore") as flip: + xlg = flip.read() + traffic = web_data + traffic + xlg + for log_line in logcat_data: + if log_line.startswith(clip_tag): + clipboard.append(log_line.replace(clip_tag, "Process ID ")) + urls = [] + # URLs My Custom regex + url_pattern = re.compile(ur'((?:https?://|s?ftps?://|file://|javascript:|data:|www\d{0,3}[.])[\w().=/;,#:@?&~*+!$%\'{}-]+)', re.UNICODE) + urllist = re.findall(url_pattern, traffic.lower()) + # Domain Extraction and Malware Check + print "[INFO] Performing Malware Check on extracted Domains" + domains = MalwareCheck(urllist) + for url in urllist: + if url not in urls: + urls.append(url) + + # Email Etraction Regex + emails = [] + regex = re.compile(r"[\w.-]+@[\w-]+\.[\w.]+") + for email in regex.findall(traffic.lower()): + if (email not in emails) and (not email.startswith('//')): + if email == "yodleebanglore@gmail.com": + pass + else: + emails.append(email) + # Extract Device Data + try: + tar_loc = os.path.join(apk_dir, package + '.tar') + untar_dir = os.path.join(apk_dir, 'DYNAMIC_DeviceData/') + if not os.path.exists(untar_dir): + os.makedirs(untar_dir) + with tarfile.open(tar_loc) as tar: + try: + tar.extractall(untar_dir) + except: + pass + except: + PrintException("[ERROR] TAR EXTRACTION FAILED") + # Do Static Analysis on Data from Device + xmlfiles = '' + sqlite_db = '' + other_files = '' + typ = '' + untar_dir = os.path.join(apk_dir, 'DYNAMIC_DeviceData/') + if not os.path.exists(untar_dir): + os.makedirs(untar_dir) + try: + for dir_name, _, files in os.walk(untar_dir): + for jfile in files: + file_path = os.path.join(untar_dir, dir_name, jfile) + if "+" in file_path: + shutil.move(file_path, file_path.replace("+", "x")) + file_path = file_path.replace("+", "x") + fileparam = file_path.replace(untar_dir, '') + if jfile == 'lib': + pass + else: + if jfile.endswith('.xml'): + typ = 'xml' + xmlfiles += "" + escape(fileparam) + "" + else: + with io.open(file_path, mode='r', encoding="utf8", errors="ignore") as flip: + file_cnt_sig = flip.read(6) + if file_cnt_sig == "SQLite": + typ = 'db' + sqlite_db += "" + \ + escape(fileparam) + "" + elif not jfile.endswith('.DS_Store'): + typ = 'others' + other_files += "" + \ + escape(fileparam) + "" + except: + PrintException("[ERROR] Dynamic File Analysis") + analysis_result["urls"] = urls + analysis_result["domains"] = domains + analysis_result["emails"] = emails + analysis_result["clipboard"] = clipboard + analysis_result["web_data"] = web_data + analysis_result["xmlfiles"] = xmlfiles + analysis_result["sqlite_db"] = sqlite_db + analysis_result["other_files"] = other_files + return analysis_result + + +def view(request): + """View File""" + print "\n[INFO] Viewing File" + try: + typ = '' + fil = '' + rtyp = '' + dat = '' + if re.match('^[0-9a-f]{32}$', request.GET['md5']): + fil = request.GET['file'] + md5_hash = request.GET['md5'] + typ = request.GET['type'] + src = os.path.join(settings.UPLD_DIR, + md5_hash + '/DYNAMIC_DeviceData/') + sfile = os.path.join(src, fil) + # Prevent Directory Traversal Attacks + if ("../" in fil) or ("%2e%2e" in fil) or (".." in fil) or ("%252e" in fil): + return HttpResponseRedirect('/error/') + else: + with io.open(sfile, mode='r', encoding="utf8", errors="ignore") as flip: + dat = flip.read() + if (fil.endswith('.xml')) and (typ == 'xml'): + rtyp = 'xml' + elif typ == 'db': + dat = handle_sqlite(sfile) + rtyp = 'asciidoc' + elif typ == 'others': + rtyp = 'asciidoc' + else: + return HttpResponseRedirect('/error/') + context = {'title': escape(ntpath.basename(fil)), 'file': escape( + ntpath.basename(fil)), 'dat': dat, 'type': rtyp, } + template = "general/view.html" + return render(request, template, context) + + else: + return HttpResponseRedirect('/error/') + except: + PrintException("[ERROR] Viewing File") + return HttpResponseRedirect('/error/') + + +def screencast_service(): + """Start or Stop ScreenCast Services""" + global TCP_SERVER_MODE + print "\n[INFO] ScreenCast Service Status: " + TCP_SERVER_MODE + try: + screen_dir = settings.SCREEN_DIR + if not os.path.exists(screen_dir): + os.makedirs(screen_dir) + + screen_socket = socket.socket() + if TCP_SERVER_MODE == "on": + screen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if settings.ANDROID_DYNAMIC_ANALYZER == "MobSF_AVD": + addr = ('127.0.0.1', settings.SCREEN_PORT) + else: + addr = (settings.SCREEN_IP, settings.SCREEN_PORT) + screen_socket.bind(addr) + screen_socket.listen(10) + while TCP_SERVER_MODE == "on": + screens, address = screen_socket.accept() + print "Got Connection from: ", address[0] + if settings.ANDROID_DYNAMIC_ANALYZER == "MobSF_REAL_DEVICE": + ip_address = settings.DEVICE_IP + else: + ip_address = settings.VM_IP + if address[0] in [ip_address, '127.0.0.1']: + ''' + Very Basic Check to ensure that only MobSF VM/Device/Emulator + is allowed to connect to MobSF ScreenCast Service. + ''' + with open(screen_dir + 'screen.png', 'wb') as flip: + while True: + data = screens.recv(1024) + if not data: + break + flip.write(data) + else: + print "\n[ATTACK] An unknown client :" + address[0] + " is trying " + \ + "to make a connection with MobSF ScreenCast Service!" + elif TCP_SERVER_MODE == "off": + screen_socket.close() + except: + screen_socket.close() + PrintException("[ERROR] ScreenCast Server") diff --git a/DynamicAnalyzer/views/android/android_virtualbox_vm.py b/DynamicAnalyzer/views/android/android_virtualbox_vm.py new file mode 100644 index 00000000..d0b0c5f2 --- /dev/null +++ b/DynamicAnalyzer/views/android/android_virtualbox_vm.py @@ -0,0 +1,22 @@ +import subprocess +from MobSF.utils import PrintException + + +def refresh_vm(uuid, snapshot_uuid, vbox_exe): + """Refresh VirtualBox based VMs""" + print "\n[INFO] Refreshing MobSF VM" + try: + # Close VM + args = [vbox_exe, 'controlvm', uuid, 'poweroff'] + subprocess.call(args) + print "\n[INFO] VM Closed" + # Restore Snapshot + args = [vbox_exe, 'snapshot', uuid, 'restore', snapshot_uuid] + subprocess.call(args) + print "\n[INFO] VM Restore Snapshot" + # Start Fresh VM + args = [vbox_exe, 'startvm', uuid] + subprocess.call(args) + print "\n[INFO] VM Starting" + except: + PrintException("[ERROR] Refreshing MobSF VM") diff --git a/DynamicAnalyzer/views/android_standalone.py b/DynamicAnalyzer/views/android_standalone.py index b8656ebd..8c2d74f1 100644 --- a/DynamicAnalyzer/views/android_standalone.py +++ b/DynamicAnalyzer/views/android_standalone.py @@ -2,28 +2,34 @@ Standalone Android Dynamic Analysis """ # -*- coding: utf_8 -*- +import os +import subprocess from django.shortcuts import render from django.conf import settings -from django.http import HttpResponseRedirect, HttpResponse -from django.utils.html import escape -from DynamicAnalyzer.views.android import RefreshVM, getADB, getIdentifier, Connect -from DynamicAnalyzer.pyWebProxy.pywebproxy import * - -import subprocess +from DynamicAnalyzer.views.android.android_virtualbox_vm import ( + refresh_vm +) +from DynamicAnalyzer.views.android.android_dyn_shared import ( + connect, + get_identifier, +) +from DynamicAnalyzer.pyWebProxy.pywebproxy import Proxy +from MobSF.utils import getADB -def DynamicAnalyzer(request): +def dynamic_analyzer_standalone(request): + """Standalone Dynamic Analysis""" print "\n[INFO] Starting Standalone Android Dynamic Analyzer" - TOOLS_DIR = os.path.join( + toolsdir = os.path.join( settings.BASE_DIR, 'DynamicAnalyzer/tools/') # TOOLS DIR Proxy("", "", "", "") - if settings.REAL_DEVICE: + if settings.ANDROID_DYNAMIC_ANALYZER == "MobSF_REAL_DEVICE": print "\n[INFO] MobSF will perform Dynamic Analysis on real Android Device" else: # Refersh VM - RefreshVM(settings.UUID, settings.SUUID, settings.VBOX) - Connect(TOOLS_DIR) - print GetPackages(TOOLS_DIR) + refresh_vm(settings.UUID, settings.SUUID, settings.VBOX) + connect(toolsdir) + print get_packages(toolsdir) context = {'md5': '', 'pkg': '', 'lng': '', @@ -32,9 +38,10 @@ def DynamicAnalyzer(request): return render(request, template, context) -def GetPackages(TOOLSDIR): - adb = getADB(TOOLSDIR) - args = [adb, "-s", getIdentifier(), "shell", "ls", "/data/data"] +def get_packages(toolsdir): + """Get List of Pacakges""" + adb = getADB(toolsdir) + args = [adb, "-s", get_identifier(), "shell", "ls", "/data/data"] # prolly a better way to get packages is needed try: return subprocess.check_output(args) diff --git a/MobSF/settings.py b/MobSF/settings.py index 7318e85a..b19f2f2c 100644 --- a/MobSF/settings.py +++ b/MobSF/settings.py @@ -21,14 +21,14 @@ from install.windows.setup import windows_config_local BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) #============================================== -MOBSF_VER = "v0.9.4.3 Beta" +MOBSF_VER = "v0.9.5.1 Beta" BANNER = """ - __ __ _ ____ _____ ___ ___ _ _ - | \/ | ___ | |__/ ___|| ___|_ __/ _ \ / _ \| || | - | |\/| |/ _ \| '_ \___ \| |_ \ \ / / | | | (_) | || |_ - | | | | (_) | |_) |__) | _| \ V /| |_| |\__, |__ _| - |_| |_|\___/|_.__/____/|_| \_/ \___(_) /_(_) |_| - + __ __ _ ____ _____ ___ ___ ____ +| \/ | ___ | |__/ ___|| ___|_ __/ _ \ / _ \ | ___| +| |\/| |/ _ \| '_ \___ \| |_ \ \ / / | | | (_) ||___ \ +| | | | (_) | |_) |__) | _| \ V /| |_| |\__, | ___) | +|_| |_|\___/|_.__/____/|_| \_/ \___(_) /_(_)____/ + """ #============================================== @@ -148,7 +148,7 @@ except NameError: Exception('Please create a %s file with random characters \ to generate your secret key!' % SECRET_FILE) # Run Once - #Windows Setup + # Windows Setup windows_config_local(MobSF_HOME) utils.make_migrations(BASE_DIR) utils.migrate(BASE_DIR) @@ -219,7 +219,10 @@ STATICFILES_DIRS = ( # https://docs.djangoproject.com/en/dev/howto/static-files/ STATIC_URL = '/static/' -#=============================================== +#=================== +# USER CONFIGURATION +#=================== + if CONFIG_HOME: print "[INFO] Loading User config from: " + USER_CONFIG else: @@ -233,7 +236,11 @@ else: # MOBSF USER CONFIGURATIONS #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - #==========SKIP CLASSES========================== + #------------------------- + # STATIC ANALYZER SETTINGS + #------------------------- + + #==========ANDROID SKIP CLASSES========================== SKIP_CLASSES = [ 'android/support/', 'com/google/', 'android/content/', @@ -242,99 +249,6 @@ else: 'oauth/signpost/', 'org/chromium/' ] - #==============3rd Party Tools================= - ''' - If you want to use a different version of 3rd party tools used by MobSF. - You can do that by specifying the path here. If specified, MobSF will run - the tool from this location. - ''' - - # Android 3P Tools - DEX2JAR_BINARY = "" - BACKSMALI_BINARY = "" - AXMLPRINTER_BINARY = "" - CFR_DECOMPILER_BINARY = "" - JD_CORE_DECOMPILER_BINARY = "" - PROCYON_DECOMPILER_BINARY = "" - ADB_BINARY = "" - ENJARIFY_DIRECTORY = "" - - # iOS 3P Tools - OTOOL_BINARY = "" - CLASSDUMPZ_BINARY = "" - - # COMMON - JAVA_DIRECTORY = "" - VBOXMANAGE_BINARY = "" - - ''' - Examples: - JAVA_DIRECTORY = "C:/Program Files/Java/jdk1.7.0_17/bin/" - JAVA_DIRECTORY = "/usr/bin/" - DEX2JAR_BINARY = "/Users/ajin/dex2jar/d2j-dex2jar.sh" - ENJARIFY_DIRECTORY = "D:/enjarify/" - VBOXMANAGE_BINARY = "/usr/bin/VBoxManage" - CFR_DECOMPILER_BINARY = "/home/ajin/tools/cfr.jar" - ''' - #=============================================== - - #==============RESPONSE VALIDATION============== - XXE_VALIDATE_STRING = "m0bsfxx3" - #=============================================== - - #=========Path Traversal - API Testing========== - CHECK_FILE = "/etc/passwd" - RESPONSE_REGEX = "root:|nobody:" - #=============================================== - - #=========Rate Limit Check - API Testing======== - RATE_REGISTER = 20 - RATE_LOGIN = 20 - #=============================================== - - #===============MobSF Cloud Settings============ - CLOUD_SERVER = 'http://opensecurity.in:8080' - ''' - This server validates SSRF and XXE during Web API Testing - See the source code of the cloud server from APITester/cloud/cloud_server.py - You can also host the cloud server. Host it on a public IP and point CLOUD_SERVER to that IP. - ''' - - #===============DEVICE SETTINGS================= - REAL_DEVICE = False - DEVICE_IP = '192.168.1.18' - DEVICE_ADB_PORT = 5555 - DEVICE_TIMEOUT = 300 - #=============================================== - #================VM SETTINGS =================== - - # VM UUID - UUID = '408e1874-759f-4417-9453-53ef21dc2ade' - # Snapshot UUID - SUUID = '5c9deb28-def6-49c0-9233-b5e03edd85c6' - # IP of the MobSF VM - VM_IP = '192.168.56.101' - VM_ADB_PORT = 5555 - VM_TIMEOUT = 100 - #============================================== - #================HOST/PROXY SETTINGS ========== - - PROXY_IP = '192.168.56.1' # Host/Server/Proxy IP - PORT = 1337 # Proxy Port - ROOT_CA = '0025aabb.0' - SCREEN_IP = PROXY_IP # ScreenCast IP - SCREEN_PORT = 9339 # ScreenCast Port - - #============================================== - - #========UPSTREAM PROXY SETTINGS ============== - # If you are behind a Proxy - UPSTREAM_PROXY_IP = None - UPSTREAM_PROXY_PORT = None - UPSTREAM_PROXY_USERNAME = None - UPSTREAM_PROXY_PASSWORD = None - #============================================== - #==========DECOMPILER SETTINGS================= DECOMPILER = "cfr" @@ -371,15 +285,146 @@ else: # python setup.py install #============================================== - #================WINDOWS-Analysis-Settings ==== + #======WINDOWS STATIC ANALYSIS SETTINGS =========== - # Private key if rpc server is needed + # Private key WINDOWS_VM_SECRET = 'MobSF/windows_vm_priv_key.asc' # IP and Port of the MobSF Windows VM # eg: WINDOWS_VM_IP = '127.0.0.1' WINDOWS_VM_IP = None WINDOWS_VM_PORT = '8000' + #================================================== + + #==============3rd Party Tools================= + ''' + If you want to use a different version of 3rd party tools used by MobSF. + You can do that by specifying the path here. If specified, MobSF will run + the tool from this location. + ''' + + # Android 3P Tools + DEX2JAR_BINARY = "" + BACKSMALI_BINARY = "" + AXMLPRINTER_BINARY = "" + CFR_DECOMPILER_BINARY = "" + JD_CORE_DECOMPILER_BINARY = "" + PROCYON_DECOMPILER_BINARY = "" + ADB_BINARY = "" + ENJARIFY_DIRECTORY = "" + + # iOS 3P Tools + OTOOL_BINARY = "" + CLASSDUMPZ_BINARY = "" + + # COMMON + JAVA_DIRECTORY = "" + VBOXMANAGE_BINARY = "" + + ''' + Examples: + JAVA_DIRECTORY = "C:/Program Files/Java/jdk1.7.0_17/bin/" + JAVA_DIRECTORY = "/usr/bin/" + DEX2JAR_BINARY = "/Users/ajin/dex2jar/d2j-dex2jar.sh" + ENJARIFY_DIRECTORY = "D:/enjarify/" + VBOXMANAGE_BINARY = "/usr/bin/VBoxManage" + CFR_DECOMPILER_BINARY = "/home/ajin/tools/cfr.jar" + ''' + #=============================================== + + #------------------------- + # DYNAMIC ANALYZER SETTINGS + #------------------------- + + #========ANDROID DYNAMIC ANALYSIS SETTINGS================================ + + ANDROID_DYNAMIC_ANALYZER = "MobSF_VM" + + # You can choose any of the below + # 1. MobSF_VM + # 2. MobSF_AVD + # 3. MobSF_REAL_DEVICE + + ''' + MobSF_VM - x86 Android 4.4.2 running on VirtualBox (Fast, not all Apps work) + MobSF_AVD - ARM Android 4.1.2 running on Android Emulator (Slow, Most Apps work) + MobSF_REAL_DEVICE - Rooted Android 4.03 - 4.4 Device (Very Fast, All Apps work) + ''' + + #========================================================================= + + #=======ANDROID REAL DEVICE SETTINGS=========== + DEVICE_IP = '192.168.1.18' + DEVICE_ADB_PORT = 5555 + DEVICE_TIMEOUT = 300 #============================================== + + #===========ANDROID EMULATOR SETTINGS =========== + # generated by mobsfy_AVD.py, do not edit the + # below AVD settings yourself. + AVD_EMULATOR = "avd_emulator" + AVD_PATH = "avd_path" + AVD_REFERENCE_NAME = r'Nexus5API16' + AVD_DUP_NAME = r'Nexus5API16_1' + AVD_ADB_PORT = 5554 + #================================================ + + #====ANDROID MOBSF VIRTUALBOX VM SETTINGS ===== + # VM UUID + UUID = '408e1874-759f-4417-9453-53ef21dc2ade' + # Snapshot UUID + SUUID = '5c9deb28-def6-49c0-9233-b5e03edd85c6' + # IP of the MobSF VM + VM_IP = '192.168.56.101' + VM_ADB_PORT = 5555 + VM_TIMEOUT = 100 + #============================================== + + #-------------------------- + # MobSF MITM PROXY SETTINGS + #-------------------------- + + #================HOST/PROXY SETTINGS =============== + PROXY_IP = '192.168.56.1' # Host/Server/Proxy IP + PORT = 1337 # Proxy Port + ROOT_CA = '0025aabb.0' + SCREEN_IP = PROXY_IP # ScreenCast IP + SCREEN_PORT = 9339 # ScreenCast Port(Do not Change) + #=================================================== + + #========UPSTREAM PROXY SETTINGS ============== + # If you are behind a Proxy + UPSTREAM_PROXY_IP = None + UPSTREAM_PROXY_PORT = None + UPSTREAM_PROXY_USERNAME = None + UPSTREAM_PROXY_PASSWORD = None + #============================================== + + #------------------------ + # WEB API FUZZER SETTINGS + #------------------------ + + #==============RESPONSE VALIDATION============== + XXE_VALIDATE_STRING = "m0bsfxx3" + #=============================================== + + #=========Path Traversal - API Testing========== + CHECK_FILE = "/etc/passwd" + RESPONSE_REGEX = "root:|nobody:" + #=============================================== + + #=========Rate Limit Check - API Testing======== + RATE_REGISTER = 20 + RATE_LOGIN = 20 + #=============================================== + + #===============MobSF Cloud Settings============ + CLOUD_SERVER = 'http://opensecurity.in:8080' + ''' + This server validates SSRF and XXE during Web API Testing + See the source code of the cloud server from APITester/cloud/cloud_server.py + You can also host the cloud server. Host it on a public IP and point CLOUD_SERVER to that IP. + ''' + #^CONFIG-END^: Do not edit this line # The below code should be loaded last. diff --git a/MobSF/urls.py b/MobSF/urls.py index 5e064fe6..4324f030 100644 --- a/MobSF/urls.py +++ b/MobSF/urls.py @@ -1,7 +1,7 @@ from django.conf.urls import url import MobSF.views import APITester.views -import DynamicAnalyzer.views.android +import DynamicAnalyzer.views.android.android_dynamic import DynamicAnalyzer.views.android_standalone import StaticAnalyzer.views.android.static_analyzer import StaticAnalyzer.views.android.java @@ -23,15 +23,19 @@ urlpatterns = [ url(r'^download/', MobSF.views.download), url(r'^about/$', MobSF.views.about), url(r'^recent_scans/$', MobSF.views.recent_scans), + url(r'^delete_scan/$', MobSF.views.delete_scan), url(r'^search/$', MobSF.views.search), url(r'^error/$', MobSF.views.error), url(r'^not_found/$', MobSF.views.not_found), url(r'^zip_format/$', MobSF.views.zip_format), url(r'^mac_only/$', MobSF.views.mac_only), - url(r'^StaticAnalyzer/$', StaticAnalyzer.views.android.static_analyzer.static_analyzer), - url(r'^StaticAnalyzer_iOS/$', StaticAnalyzer.views.ios.static_analyzer.static_analyzer_ios), - url(r'^StaticAnalyzer_Windows/$', StaticAnalyzer.views.windows.staticanalyzer_windows), + url(r'^StaticAnalyzer/$', + StaticAnalyzer.views.android.static_analyzer.static_analyzer), + url(r'^StaticAnalyzer_iOS/$', + StaticAnalyzer.views.ios.static_analyzer.static_analyzer_ios), + url(r'^StaticAnalyzer_Windows/$', + StaticAnalyzer.views.windows.staticanalyzer_windows), url(r'^ViewFile/$', StaticAnalyzer.views.ios.static_analyzer.view_file), url(r'^ViewSource/$', StaticAnalyzer.views.android.view_source.run), url(r'^PDF/$', StaticAnalyzer.views.shared_func.PDF), @@ -40,27 +44,30 @@ urlpatterns = [ url(r'^Find/$', StaticAnalyzer.views.android.find.run), url(r'^ManifestView/$', StaticAnalyzer.views.android.manifest_view.run), - url(r'^DynamicAnalyzer/$', DynamicAnalyzer.views.android.DynamicAnalyzer), - url(r'^GetEnv/$', DynamicAnalyzer.views.android.GetEnv), - url(r'^GetRes/$', DynamicAnalyzer.views.android.GetRes), - url(r'^MobSFCA/$', DynamicAnalyzer.views.android.MobSFCA), - url(r'^TakeScreenShot/$', DynamicAnalyzer.views.android.TakeScreenShot), - url(r'^ClipDump/$', DynamicAnalyzer.views.android.clip_dump), + url(r'^DynamicAnalyzer/$', + DynamicAnalyzer.views.android.android_dynamic.android_dynamic_analyzer), + url(r'^GetEnv/$', DynamicAnalyzer.views.android.android_dynamic.get_env), + url(r'^GetRes/$', DynamicAnalyzer.views.android.android_dynamic.get_res), + url(r'^MobSFCA/$', DynamicAnalyzer.views.android.android_dynamic.mobsf_ca), + url(r'^TakeScreenShot/$', + DynamicAnalyzer.views.android.android_dynamic.take_screenshot), + url(r'^ClipDump/$', DynamicAnalyzer.views.android.android_dynamic.clip_dump), url(r'^ExportedActivityTester/$', - DynamicAnalyzer.views.android.ExportedActivityTester), - url(r'^ActivityTester/$', DynamicAnalyzer.views.android.ActivityTester), - url(r'^FinalTest/$', DynamicAnalyzer.views.android.FinalTest), - url(r'^DumpData/$', DynamicAnalyzer.views.android.DumpData), - url(r'^ExecuteADB/$', DynamicAnalyzer.views.android.ExecuteADB), - url(r'^Report/$', DynamicAnalyzer.views.android.Report), - url(r'^View/$', DynamicAnalyzer.views.android.View), - url(r'^ScreenCast/$', DynamicAnalyzer.views.android.ScreenCast), - url(r'^Touch/$', DynamicAnalyzer.views.android.Touch), + DynamicAnalyzer.views.android.android_dynamic.exported_activity_tester), + url(r'^ActivityTester/$', + DynamicAnalyzer.views.android.android_dynamic.activity_tester), + url(r'^FinalTest/$', DynamicAnalyzer.views.android.android_dynamic.final_test), + url(r'^DumpData/$', DynamicAnalyzer.views.android.android_dynamic.dump_data), + url(r'^ExecuteADB/$', DynamicAnalyzer.views.android.android_dynamic.execute_adb), + url(r'^Report/$', DynamicAnalyzer.views.android.android_dynamic.report), + url(r'^View/$', DynamicAnalyzer.views.android.android_dynamic.view), + url(r'^ScreenCast/$', DynamicAnalyzer.views.android.android_dynamic.screen_cast), + url(r'^Touch/$', DynamicAnalyzer.views.android.android_dynamic.touch), url(r'^APIFuzzer/$', APITester.views.APIFuzzer), url(r'^StartScan/$', APITester.views.StartScan), url(r'^NoAPI/$', APITester.views.NoAPI), - #Test + # Test url(r'^runtest/$', StaticAnalyzer.tests.start_test), ] diff --git a/MobSF/utils.py b/MobSF/utils.py index 01e4be9b..0f85273c 100644 --- a/MobSF/utils.py +++ b/MobSF/utils.py @@ -157,7 +157,7 @@ def kali_fix(BASE_DIR): def FindVbox(debug=False): try: - if settings.REAL_DEVICE == False: + if settings.ANDROID_DYNAMIC_ANALYZER == "MobSF_VM": if len(settings.VBOXMANAGE_BINARY) > 0 and isFileExists(settings.VBOXMANAGE_BINARY): return settings.VBOXMANAGE_BINARY if platform.system() == "Windows": diff --git a/MobSF/views.py b/MobSF/views.py index e74e6fd0..217d7b8b 100644 --- a/MobSF/views.py +++ b/MobSF/views.py @@ -4,6 +4,7 @@ MobSF File Upload and Home Routes """ import os import hashlib +import shutil import platform import json import re @@ -14,8 +15,15 @@ from django.http import HttpResponse from django.http import HttpResponseRedirect from django.conf import settings from django.utils import timezone -from MobSF.utils import PrintException +from MobSF.utils import PrintException, isDirExists, isFileExists from MobSF.models import RecentScansDB +from APITester.models import ScopeURLSandTests +from StaticAnalyzer.models import ( + StaticAnalyzerAndroid, + StaticAnalyzerIPA, + StaticAnalyzerIOSZIP, + StaticAnalyzerWindows, +) from .forms import UploadFileForm @@ -110,12 +118,15 @@ def upload(request): response_data['url'] = 'mac_only/' response_data['status'] = 'success' print "\n[ERROR] Static Analysis of iOS IPA requires OSX" - elif (file_type in settings.APPX_MIME) and request.FILES['file'].name.lower().endswith('.appx'): #Windows APPX - md5=handle_uploaded_file(request.FILES['file'],'.appx') - response_data['url'] = 'StaticAnalyzer_Windows/?name='+request.FILES['file'].name+'&type=appx&checksum='+md5 + # Windows APPX + elif (file_type in settings.APPX_MIME) and request.FILES['file'].name.lower().endswith('.appx'): + md5 = handle_uploaded_file(request.FILES['file'], '.appx') + response_data['url'] = 'StaticAnalyzer_Windows/?name=' + \ + request.FILES['file'].name + \ + '&type=appx&checksum=' + md5 response_data['status'] = 'success' add_to_recent_scan( - request.FILES['file'].name,md5,response_data['url']) + request.FILES['file'].name, md5, response_data['url']) print "\n[INFO] Performing Static Analysis of Windows APP" else: response_data['url'] = '' @@ -235,3 +246,40 @@ def download(request): except: PrintException("Error Downloading File") return HttpResponseRedirect('/error/') + + +def delete_scan(request): + """ + Delete Scan from DB and remove the scan related files + """ + try: + if request.method == 'POST': + md5_hash = request.POST['md5'] + data = {'deleted': 'no'} + if re.match('[0-9a-f]{32}', md5_hash): + # Delete DB Entries + RecentScansDB.objects.filter(MD5=md5_hash).delete() + ScopeURLSandTests.objects.filter(MD5=md5_hash).delete() + StaticAnalyzerAndroid.objects.filter(MD5=md5_hash).delete() + StaticAnalyzerIPA.objects.filter(MD5=md5_hash).delete() + StaticAnalyzerIOSZIP.objects.filter(MD5=md5_hash).delete() + StaticAnalyzerWindows.objects.filter(MD5=md5_hash).delete() + # Delete Upload Dir Contents + app_upload_dir = os.path.join(settings.UPLD_DIR, md5_hash) + if isDirExists(app_upload_dir): + shutil.rmtree(app_upload_dir) + # Delete Download Dir Contents + dw_dir = settings.DWD_DIR + for item in os.listdir(dw_dir): + item_path = os.path.join(dw_dir, item) + # Delete all related files + if isFileExists(item_path) and item.startswith(md5_hash + "-"): + os.remove(item_path) + # Delete related directories + if isDirExists(item_path) and item.startswith(md5_hash + "-"): + shutil.rmtree(item_path) + data = {'deleted': 'yes'} + return HttpResponse(json.dumps(data), content_type='application/json') + except: + PrintException("Error Deleting Scan") + return HttpResponseRedirect('/error/') diff --git a/README.md b/README.md index e895aadf..2b2aea69 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ MobSF is also bundled with [Android Tamer](https://androidtamer.com/tamer4-relea * [Ajin Abraham](https://in.linkedin.com/in/ajinabraham) * [Dominik Schlecht](https://github.com/DominikSchlecht) +* [Matan Dobrushin](https://github.com/matandobr) ## Presentations * OWASP APPSEC EU 2016 - [Slides](http://www.slideshare.net/ajin25/automated-mobile-application-security-assessment-with-mobsf) | [Video](https://www.youtube.com/watch?v=h00v1euuFXg) @@ -73,15 +74,16 @@ MobSF is also bundled with [Android Tamer](https://androidtamer.com/tamer4-relea ![api-fuzzer-start-report](https://cloud.githubusercontent.com/assets/4301109/13615236/5d8df210-e59a-11e5-827a-ccf642e96609.png) ## Credits -* Dominik Schlecht - For the awesome work on adding Windows Phone App Static Analysis to MobSF -* Bharadwaj Machiraju (@tunnelshade_) - For writing pyWebProxy from scratch -* MindMac - For writing Android Blue Pill -* Thomas Abraham - For JS Hacks on UI. -* Anto Joseph (@antojosep007) - For the help with SuperSU. -* Tim Brown (@timb_machine) - For the iOS Binary Analysis Ruleset. -* Abhinav Sejpal (@Abhinav_Sejpal) - For poking me with bugs and feature requests. -* Anant Srivastava (@anantshri) - For Activity Tester Idea +* Abhinav Sejpal (@Abhinav_Sejpal) - For poking me with bugs, feature requests, and UI & UX suggestions. * Amrutha VC (@amruthavc) - For the new MobSF logo -* Rahul (@c0dist) - Kali Support -* shuxin - Android Binary Analysis +* Anant Srivastava (@anantshri) - For Activity Tester Idea +* Anto Joseph (@antojosep007) - For the help with SuperSU. +* Bharadwaj Machiraju (@tunnelshade_) - For writing pyWebProxy from scratch +* Dominik Schlecht - For the awesome work on adding Windows Phone App Static Analysis to MobSF * Esteban - Better Android Manifest Analysis and Static Analysis Improvement. +* Matan Dobrushin - For adding Android ARM Emulator support to MobSF - Special thanks goes for cuckoo-droid, I got inspierd by their code and idea for this implementation. +* MindMac - For writing Android Blue Pill +* Rahul (@c0dist) - Kali Support +* Shuxin - Android Binary Analysis +* Thomas Abraham - For JS Hacks on UI. +* Tim Brown (@timb_machine) - For the iOS Binary Analysis Ruleset. diff --git a/StaticAnalyzer/tests.py b/StaticAnalyzer/tests.py index 18bb7d12..fae50f37 100644 --- a/StaticAnalyzer/tests.py +++ b/StaticAnalyzer/tests.py @@ -10,14 +10,15 @@ from django.http import HttpResponse from MobSF.utils import PrintException RESCAN = False -#Set RESCAN to True if Static Analyzer Code is modified +# Set RESCAN to True if Static Analyzer Code is modified + def static_analysis(): """Test Static Analyzer""" failed = False - ERR_MSG = '%s' + err_msg = '%s' if platform.system() != "Windows": - ERR_MSG = '\033[91m \033[1m %s \033[0m' + err_msg = '\033[91m \033[1m %s \033[0m' try: uploaded = [] print "[INFO] Running Upload Test" @@ -32,7 +33,7 @@ def static_analysis(): print "[OK] Upload OK: " + filename uploaded.append(obj["url"]) else: - print ERR_MSG % "[ERROR] Performing Upload" + filename + print err_msg % "[ERROR] Performing Upload" + filename failed = True print "[OK] Completed Upload test" print "[INFO] Running Static Analysis Test" @@ -45,7 +46,7 @@ def static_analysis(): if resp.status_code == 200: print "[OK] Static Analysis Complete: " + upl else: - print ERR_MSG % "[ERROR] Performing Static Analysis: " + upl + print err_msg % "[ERROR] Performing Static Analysis: " + upl failed = True print "[OK] Static Analysis test completed" print "[INFO] Running PDF Generation Test" @@ -60,14 +61,31 @@ def static_analysis(): for pdf in pdfs: resp = http_client.get(pdf) if (resp.status_code == 200 and - resp._headers['content-type'][1] == "application/pdf" + resp._headers['content-type'][1] == "application/pdf" ): print "[OK] PDF Report Generated: " + pdf else: - print ERR_MSG % "[ERROR] Generating PDF: " + pdf + print err_msg % "[ERROR] Generating PDF: " + pdf print resp.content failed = True print "[OK] PDF Generation test completed" + print "[INFO] Running Delete Scan Results test" + # Deleting Scan Results + scan_md5s = ["3a552566097a8de588b8184b059b0158", "6c23c2970551be15f32bbab0b5db0c71", + "52c50ae824e329ba8b5b7a0f523efffe", "57bb5be0ea44a755ada4a93885c3825e", "8179b557433835827a70510584f3143e"] + for md5 in scan_md5s: + resp = http_client.post('/delete_scan/', {'md5': md5}) + if resp.status_code == 200: + dat = json.loads(resp.content) + if dat["deleted"] == "yes": + print "[OK] Deleted Scan: " + md5 + else: + print err_msg % "[ERROR] Deleting Scan: " + md5 + failed = True + else: + print err_msg % "[ERROR] Deleting Scan: " + md5 + failed = True + print "[INFO] Delete Scan Results test completed" except: PrintException("[ERROR] Completing Static Analyzer Test") return failed @@ -85,5 +103,6 @@ def start_test(request): except: message = "error" print "\n\n[INFO] ALL TESTS COMPLETED!" + print "[INFO] Test Status: " + message return HttpResponse(json.dumps({"static_analyzer_test": message}), content_type="application/json") diff --git a/mobsfy_AVD.py b/mobsfy_AVD.py new file mode 100755 index 00000000..f300d9e9 --- /dev/null +++ b/mobsfy_AVD.py @@ -0,0 +1,317 @@ +#!/usr/bin/env python +# -*- coding: utf_8 -*- +import os +import platform +import subprocess +import sys +import getpass +import shutil +import io +import string +import re + +BASE_DIR = os.path.dirname(os.path.realpath(__file__)) + + +def get_windows_drive(): + drive = os.getenv('WINDIR')[:1] + ':' + if not drive: + return 'C:' + return drive + + +def print_log(msg, log_type='INFO'): + print '\n[' + log_type + '] ' + msg + '\n' + + +def execute_cmd(args, ret=False): + try: + print "\n[INFO] Executing Command - " + ' '.join(args) + if ret: + return subprocess.check_output(args) + else: + subprocess.call(args) + except Exception as e: + print "\n[ERROR] Executing Command - " + str(e) + + +def verify_path(help_msg): + path = raw_input(help_msg + ': ') + path = path.strip() + while not os.path.exists(path): + print_log('Path specified does not exists \ no access', 'ERROR') + path = raw_input(help_msg) + return path + + +def guess_android_avd_folder(): + system = platform.system() + + if system == 'Darwin': + username = getpass.getuser() + first_guess = os.path.join('/Users', username, '.android/avd') + if os.path.exists(first_guess): + return first_guess + + elif system == "Linux": + for path in [os.path.expanduser('~/.android/avd/')]: + if os.path.exists(path): + return path + + elif system == 'Windows': + username = getpass.getuser() + drive = get_windows_drive() + for path in [os.path.join(drive + '\\Documents and Settings', username, '.android\\avd'), + os.path.join(drive + '\\Users', username, '.android\\avd')]: + if os.path.exists(path): + return path + + return False + + +def guess_android_sdk_folder(): + system = platform.system() + username = getpass.getuser() + + if system == 'Darwin': + for path in [os.path.join('/Users', username, 'Library/Android/Sdk/'), + os.path.join('/Users', username, 'Library/Android/sdk/')]: + if os.path.exists(path): + return path + + elif system == "Linux": + for path in ['/usr/local/android-sdk', + '/usr/local/android', + '/usr/local/Android', + os.path.expanduser('~/Android/Sdk'), + os.path.expanduser('~/Android/sdk'), + os.path.expanduser('~/android/Sdk'), + os.path.expanduser('~/android/sdk')]: + if os.path.exists(path): + return path + + elif system == 'Windows': + drive = get_windows_drive() + for path in [os.path.join(drive + '\\Users', username, 'AppData\\Local\\Android\\sdk'), + os.path.join(drive + '\\Users', username, + 'AppData\\Local\\Android\\Sdk'), + os.path.join(drive + '\\Documents and Settings', + username, 'AppData\\Local\\Android\\sdk'), + os.path.join(drive + '\\Documents and Settings', username, 'AppData\\Local\\Android\\Sdk')]: + if os.path.exists(path): + return path + + return False + + +def find_emulator_binary(sdk): + system = platform.system() + + if system in ['Darwin', 'Linux']: + # Prefer emulator folder on tools folder + for path in [os.path.join(sdk, 'emulator', 'emulator'), + os.path.join(sdk, 'tools', 'emulator')]: + if os.path.exists(path): + return path + + elif system == 'Windows': + for path in [os.path.join(sdk, 'emulator', 'emulator.exe'), + os.path.join(sdk, 'tools', 'emulator.exe')]: + if os.path.exists(path): + return path + + return False + + +def find_adb_binary(sdk): + system = platform.system() + + if system in ['Darwin', 'Linux']: + guess = os.path.join(sdk, 'platform-tools', 'adb') + if os.path.exists(guess): + return guess + + elif system == 'Windows': + guess = os.path.join(sdk, 'platform-tools', 'adb.exe') + if os.path.exists(guess): + return guess + + return False + + +def find_skin(sdk): + # Just a basic check + system = platform.system() + + if system == 'Darwin': + guess = r'/Applications/Android Studio.app/Contents/plugins/android/lib/device-art-resources/nexus_5' + if os.path.exists(guess): + return guess + + elif system in ['Windows', 'Linux']: + guess = os.path.join(sdk, 'skins', 'nexus_5') + if os.path.exists(guess): + return guess + + return False + + +def is_file_exists(file_path): + """Check if File Exists""" + return bool(os.path.isfile(file_path)) + + +# returns an array of [str(tabs_string), str(rest_of_the_string)] +def split_tabs(inp_string): + rgx = re.compile(r"([\s]+)(.*)") + match = rgx.match(inp_string) + if match: + return [match.group(1), match.group(2)] + else: + return ['', inp_string] + + +# path to modify, replace dict = {'field_to_replace1':'value1', +# 'field_to_replace2': 'value2'} +def replace_values_by_fieldnames(path, replace_dict): + replaced_lines = [] + with io.open(path, mode='r', encoding="utf8", errors="ignore") as fild: + for line in fild.readlines(): + tmp_line = line + if path.endswith('.py'): + tabs_and_str = split_tabs(line) + for field_to_replace in replace_dict.keys(): + # Python files has annoying tabs that we should consider + if path.endswith('.py'): + if tabs_and_str[1].lower().startswith(field_to_replace.lower()): + tmp_line = tabs_and_str[0] + field_to_replace + " = r\"" + replace_dict[ + field_to_replace].strip(" \"'").lstrip("r\"") + "\"\n" + else: + if line.startswith(field_to_replace + '='): + tmp_line = field_to_replace + '=' + \ + replace_dict[field_to_replace].strip() + '\n' + replaced_lines.append(tmp_line) + with io.open(path, 'w') as fild: + # newlines are validated before + fild.write(string.join(replaced_lines, '')) + + +def main(): + sdk_path = '' + avd_path = '' + adb_path = '' + emulator_binary = '' + mobsf_arm_folder = '' + settings_py = '' + + print "\nMobSFy_AVD Script\n\n" + print_log('Starting MobSF - AVD interactive configuration script') + print_log('Make sure to run this script ONLY after you successfuly installed leatest AndroidStudio & downloaded MobSF_ARM_Emulator.zip') + + # First gather all the paths needed to make to copy opera + + print_log('Please specify the path to MobSF_ARM_Emulator extracted folder') + mobsf_arm_folder = verify_path('MobSF_ARM_Emulator folder') + + # Give the user the ability to change the sdk and avd folder, let me guess + # the other tools + print_log('This script will overwrite any previously generated files.') + guessd_sdk_path = guess_android_sdk_folder() + if guessd_sdk_path: + user_approve = raw_input( + "Guessing Android sdk path: " + guessd_sdk_path + '\n Press Enter/alternative path') + if user_approve.strip() == '': + sdk_path = guessd_sdk_path + elif os.path.exists(user_approve): + sdk_path = user_approve + if not sdk_path: + sdk_path = verify_path('Android SDK path') + + guessd_avd_path = guess_android_avd_folder() + if guessd_avd_path: + user_approve = raw_input( + "Guessing Android AVD folder: " + guessd_avd_path + '\n Press Enter/alternative path') + if user_approve.strip() == '': + avd_path = guessd_avd_path + elif os.path.exists(user_approve): + avd_path = user_approve + if not avd_path: + avd_path = verify_path('Android AVD path') + + emulator_binary = find_emulator_binary(sdk_path) + if not emulator_binary: + emulator_binary = verify_path('emulator binary') + + adb_path = find_adb_binary(sdk_path) + if not adb_path: + adb_path = verify_path('adb binary') + + settings_py = os.path.join(BASE_DIR, 'MobSF', 'settings.py') + if not os.path.exists(settings_py): + settings_py = verify_path('MobSF/settings.py file') + + skin_path = find_skin(sdk_path) + if not skin_path: + skin_path = verify_path('nexus 5 skin path') + + print_log('Finished finding all the paths needed') + + ################## Copy the downloaded emulator and system image ######### + + emulator_avd = os.path.join(mobsf_arm_folder, 'Nexus5API16.avd') + emulator_ini = os.path.join(mobsf_arm_folder, 'Nexus5API16.ini') + new_emulator_avd = os.path.join(avd_path, 'Nexus5API16.avd') + new_emulator_ini = os.path.join(avd_path, 'Nexus5API16.ini') + print_log('Copying emulator files to avd folder: ' + avd_path) + if is_file_exists(new_emulator_ini): + print_log("Replacing old Emulator INI") + os.remove(new_emulator_ini) + shutil.copyfile(emulator_ini, new_emulator_ini) + if os.path.isdir(new_emulator_avd): + print_log("Replacing old Emulator AVD") + shutil.rmtree(new_emulator_avd) + shutil.copytree(emulator_avd, new_emulator_avd) + system_images = os.path.join(sdk_path, 'system-images') + xposed_image_path = os.path.join(system_images, 'Xposed-android-16') + downloaded_xposed_image = os.path.join( + mobsf_arm_folder, 'Xposed-android-16') + if os.path.isdir(xposed_image_path): + print_log("Replacing old Xposed image") + shutil.rmtree(xposed_image_path) + shutil.copytree(downloaded_xposed_image, xposed_image_path) + + ################## Modify all the config files ########################### + + print_log('Modifying config files') + + # Nexus5API16.ini + replace_values_by_fieldnames(new_emulator_ini, { + 'path': new_emulator_avd, + 'skin.path': skin_path + }) + + # Nexus5API16.avd/config.ini + replace_values_by_fieldnames(os.path.join(new_emulator_avd, 'config.ini'), { + 'skin.path': skin_path + }) + + # Nexus5API16.avd/hardware-qemu.ini + replace_values_by_fieldnames(os.path.join(new_emulator_avd, 'hardware-qemu.ini'), { + 'hw.sdCard.path': os.path.join(new_emulator_avd, 'sdcard.img'), + 'disk.cachePartition.path': os.path.join(new_emulator_avd, 'cache.img'), + 'kernel.path': os.path.join(xposed_image_path, 'kernel-qemu'), + 'disk.ramdisk.path': os.path.join(xposed_image_path, 'ramdisk.img'), + 'disk.systemPartition.initPath': os.path.join(xposed_image_path, 'system.img'), + 'disk.dataPartition.path': os.path.join(new_emulator_avd, 'userdata.img'), + }) + + replace_values_by_fieldnames(settings_py, { + 'AVD_EMULATOR': emulator_binary, + 'AVD_PATH': avd_path, + 'ADB_BINARY': 'r"' + adb_path + '"' + }) + + print "\n\nAll Done! you can now use MobSF AVD Emulator :)\n\n" + +if __name__ == '__main__': + sys.exit(main()) diff --git a/static/dash/css/sweetalert2.min.css b/static/dash/css/sweetalert2.min.css new file mode 100644 index 00000000..0c9d0815 --- /dev/null +++ b/static/dash/css/sweetalert2.min.css @@ -0,0 +1 @@ +body.swal2-shown{overflow-y:hidden}body.swal2-iosfix{position:fixed;left:0;right:0}.swal2-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;position:fixed;top:0;left:0;bottom:0;right:0;padding:10px;background-color:transparent;z-index:1060}.swal2-container.swal2-fade{-webkit-transition:background-color .1s;transition:background-color .1s}.swal2-container.swal2-shown{background-color:rgba(0,0,0,.4)}.swal2-modal{background-color:#fff;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border-radius:5px;box-sizing:border-box;text-align:center;margin:auto;overflow-x:hidden;overflow-y:auto;display:none;position:relative}.swal2-modal:focus{outline:0}.swal2-modal.swal2-loading{overflow-y:hidden}.swal2-modal .swal2-title{color:#595959;font-size:30px;text-align:center;font-weight:600;text-transform:none;position:relative;margin:0 0 .4em;padding:0;display:block;word-wrap:break-word}.swal2-modal .swal2-buttonswrapper{margin-top:15px}.swal2-modal .swal2-buttonswrapper:not(.swal2-loading) .swal2-styled[disabled]{opacity:.4;cursor:no-drop}.swal2-modal .swal2-buttonswrapper.swal2-loading .swal2-styled.swal2-confirm{box-sizing:border-box;border:4px solid transparent;border-color:transparent;width:40px;height:40px;padding:0;margin:7.5px;vertical-align:top;background-color:transparent!important;color:transparent;cursor:default;border-radius:100%;-webkit-animation:rotate-loading 1.5s linear 0s infinite normal;animation:rotate-loading 1.5s linear 0s infinite normal;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.swal2-modal .swal2-buttonswrapper.swal2-loading .swal2-styled.swal2-cancel{margin-left:30px;margin-right:30px}.swal2-modal .swal2-buttonswrapper.swal2-loading :not(.swal2-styled).swal2-confirm::after{display:inline-block;content:'';margin-left:5px 0 15px;vertical-align:-1px;height:15px;width:15px;border:3px solid #999;box-shadow:1px 1px 1px #fff;border-right-color:transparent;border-radius:50%;-webkit-animation:rotate-loading 1.5s linear 0s infinite normal;animation:rotate-loading 1.5s linear 0s infinite normal}.swal2-modal .swal2-styled{border:0;border-radius:3px;box-shadow:none;color:#fff;cursor:pointer;font-size:17px;font-weight:500;margin:15px 5px 0;padding:10px 32px}.swal2-modal .swal2-image{margin:20px auto;max-width:100%}.swal2-modal .swal2-close{background:0 0;border:0;margin:0;padding:0;width:38px;height:40px;font-size:36px;line-height:40px;font-family:serif;position:absolute;top:5px;right:8px;cursor:pointer;color:#ccc;-webkit-transition:color .1s ease;transition:color .1s ease}.swal2-modal .swal2-close:hover{color:#d55}.swal2-modal>.swal2-checkbox,.swal2-modal>.swal2-file,.swal2-modal>.swal2-input,.swal2-modal>.swal2-radio,.swal2-modal>.swal2-select,.swal2-modal>.swal2-textarea{display:none}.swal2-modal .swal2-content{font-size:18px;text-align:center;font-weight:300;position:relative;float:none;margin:0;padding:0;line-height:normal;color:#545454;word-wrap:break-word}.swal2-modal .swal2-checkbox,.swal2-modal .swal2-file,.swal2-modal .swal2-input,.swal2-modal .swal2-radio,.swal2-modal .swal2-select,.swal2-modal .swal2-textarea{margin:20px auto}.swal2-modal .swal2-file,.swal2-modal .swal2-input,.swal2-modal .swal2-textarea{width:100%;box-sizing:border-box;font-size:18px;border-radius:3px;border:1px solid #d9d9d9;box-shadow:inset 0 1px 1px rgba(0,0,0,.06);-webkit-transition:border-color box-shadow .3s;transition:border-color box-shadow .3s}.swal2-modal .swal2-file.swal2-inputerror,.swal2-modal .swal2-input.swal2-inputerror,.swal2-modal .swal2-textarea.swal2-inputerror{border-color:#f27474!important;box-shadow:0 0 2px #f27474!important}.swal2-modal .swal2-file:focus,.swal2-modal .swal2-input:focus,.swal2-modal .swal2-textarea:focus{outline:0;border:1px solid #b4dbed;box-shadow:0 0 3px #c4e6f5}.swal2-modal .swal2-file:focus::-webkit-input-placeholder,.swal2-modal .swal2-input:focus::-webkit-input-placeholder,.swal2-modal .swal2-textarea:focus::-webkit-input-placeholder{-webkit-transition:opacity .3s .03s ease;transition:opacity .3s .03s ease;opacity:.8}.swal2-modal .swal2-file:focus:-ms-input-placeholder,.swal2-modal .swal2-input:focus:-ms-input-placeholder,.swal2-modal .swal2-textarea:focus:-ms-input-placeholder{-webkit-transition:opacity .3s .03s ease;transition:opacity .3s .03s ease;opacity:.8}.swal2-modal .swal2-file:focus::placeholder,.swal2-modal .swal2-input:focus::placeholder,.swal2-modal .swal2-textarea:focus::placeholder{-webkit-transition:opacity .3s .03s ease;transition:opacity .3s .03s ease;opacity:.8}.swal2-modal .swal2-file::-webkit-input-placeholder,.swal2-modal .swal2-input::-webkit-input-placeholder,.swal2-modal .swal2-textarea::-webkit-input-placeholder{color:#e6e6e6}.swal2-modal .swal2-file:-ms-input-placeholder,.swal2-modal .swal2-input:-ms-input-placeholder,.swal2-modal .swal2-textarea:-ms-input-placeholder{color:#e6e6e6}.swal2-modal .swal2-file::placeholder,.swal2-modal .swal2-input::placeholder,.swal2-modal .swal2-textarea::placeholder{color:#e6e6e6}.swal2-modal .swal2-range input{float:left;width:80%}.swal2-modal .swal2-range output{float:right;width:20%;font-size:20px;font-weight:600;text-align:center}.swal2-modal .swal2-range input,.swal2-modal .swal2-range output{height:43px;line-height:43px;vertical-align:middle;margin:20px auto;padding:0}.swal2-modal .swal2-input{height:43px;padding:0 12px}.swal2-modal .swal2-input[type=number]{max-width:150px}.swal2-modal .swal2-file{font-size:20px}.swal2-modal .swal2-textarea{height:108px;padding:12px}.swal2-modal .swal2-select{color:#545454;font-size:inherit;padding:5px 10px;min-width:40%;max-width:100%}.swal2-modal .swal2-radio{border:0}.swal2-modal .swal2-radio label:not(:first-child){margin-left:20px}.swal2-modal .swal2-radio input,.swal2-modal .swal2-radio span{vertical-align:middle}.swal2-modal .swal2-radio input{margin:0 3px 0 0}.swal2-modal .swal2-checkbox{color:#545454}.swal2-modal .swal2-checkbox input,.swal2-modal .swal2-checkbox span{vertical-align:middle}.swal2-modal .swal2-validationerror{background-color:#f0f0f0;margin:0 -20px;overflow:hidden;padding:10px;color:gray;font-size:16px;font-weight:300;display:none}.swal2-modal .swal2-validationerror::before{content:'!';display:inline-block;width:24px;height:24px;border-radius:50%;background-color:#ea7d7d;color:#fff;line-height:24px;text-align:center;margin-right:10px}@supports (-ms-accelerator:true){.swal2-range input{width:100%!important}.swal2-range output{display:none}}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){.swal2-range input{width:100%!important}.swal2-range output{display:none}}.swal2-icon{width:80px;height:80px;border:4px solid transparent;border-radius:50%;margin:20px auto 30px;padding:0;position:relative;box-sizing:content-box;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.swal2-icon.swal2-error{border-color:#f27474}.swal2-icon.swal2-error .swal2-x-mark{position:relative;display:block}.swal2-icon.swal2-error [class^=swal2-x-mark-line]{position:absolute;height:5px;width:47px;background-color:#f27474;display:block;top:37px;border-radius:2px}.swal2-icon.swal2-error [class^=swal2-x-mark-line][class$=left]{-webkit-transform:rotate(45deg);transform:rotate(45deg);left:17px}.swal2-icon.swal2-error [class^=swal2-x-mark-line][class$=right]{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);right:16px}.swal2-icon.swal2-warning{font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#f8bb86;border-color:#facea8;font-size:60px;line-height:80px;text-align:center}.swal2-icon.swal2-info{font-family:'Open Sans',sans-serif;color:#3fc3ee;border-color:#9de0f6;font-size:60px;line-height:80px;text-align:center}.swal2-icon.swal2-question{font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#87adbd;border-color:#c9dae1;font-size:60px;line-height:80px;text-align:center}.swal2-icon.swal2-success{border-color:#a5dc86}.swal2-icon.swal2-success [class^=swal2-success-circular-line]{border-radius:50%;position:absolute;width:60px;height:120px;-webkit-transform:rotate(45deg);transform:rotate(45deg)}.swal2-icon.swal2-success [class^=swal2-success-circular-line][class$=left]{border-radius:120px 0 0 120px;top:-7px;left:-33px;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);-webkit-transform-origin:60px 60px;transform-origin:60px 60px}.swal2-icon.swal2-success [class^=swal2-success-circular-line][class$=right]{border-radius:0 120px 120px 0;top:-11px;left:30px;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);-webkit-transform-origin:0 60px;transform-origin:0 60px}.swal2-icon.swal2-success .swal2-success-ring{width:80px;height:80px;border:4px solid rgba(165,220,134,.2);border-radius:50%;box-sizing:content-box;position:absolute;left:-4px;top:-4px;z-index:2}.swal2-icon.swal2-success .swal2-success-fix{width:7px;height:90px;position:absolute;left:28px;top:8px;z-index:1;-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}.swal2-icon.swal2-success [class^=swal2-success-line]{height:5px;background-color:#a5dc86;display:block;border-radius:2px;position:absolute;z-index:2}.swal2-icon.swal2-success [class^=swal2-success-line][class$=tip]{width:25px;left:14px;top:46px;-webkit-transform:rotate(45deg);transform:rotate(45deg)}.swal2-icon.swal2-success [class^=swal2-success-line][class$=long]{width:47px;right:8px;top:38px;-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}.swal2-progresssteps{font-weight:600;margin:0 0 20px;padding:0}.swal2-progresssteps li{display:inline-block;position:relative}.swal2-progresssteps .swal2-progresscircle{background:#3085d6;border-radius:2em;color:#fff;height:2em;line-height:2em;text-align:center;width:2em;z-index:20}.swal2-progresssteps .swal2-progresscircle:first-child{margin-left:0}.swal2-progresssteps .swal2-progresscircle:last-child{margin-right:0}.swal2-progresssteps .swal2-progresscircle.swal2-activeprogressstep{background:#3085d6}.swal2-progresssteps .swal2-progresscircle.swal2-activeprogressstep~.swal2-progresscircle{background:#add8e6}.swal2-progresssteps .swal2-progresscircle.swal2-activeprogressstep~.swal2-progressline{background:#add8e6}.swal2-progresssteps .swal2-progressline{background:#3085d6;height:.4em;margin:0 -1px;z-index:10}[class^=swal2]{-webkit-tap-highlight-color:transparent}@-webkit-keyframes showSweetAlert{0%{-webkit-transform:scale(.7);transform:scale(.7)}45%{-webkit-transform:scale(1.05);transform:scale(1.05)}80%{-webkit-transform:scale(.95);transform:scale(.95)}100%{-webkit-transform:scale(1);transform:scale(1)}}@keyframes showSweetAlert{0%{-webkit-transform:scale(.7);transform:scale(.7)}45%{-webkit-transform:scale(1.05);transform:scale(1.05)}80%{-webkit-transform:scale(.95);transform:scale(.95)}100%{-webkit-transform:scale(1);transform:scale(1)}}@-webkit-keyframes hideSweetAlert{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}100%{-webkit-transform:scale(.5);transform:scale(.5);opacity:0}}@keyframes hideSweetAlert{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}100%{-webkit-transform:scale(.5);transform:scale(.5);opacity:0}}.swal2-show{-webkit-animation:showSweetAlert .3s;animation:showSweetAlert .3s}.swal2-show.swal2-noanimation{-webkit-animation:none;animation:none}.swal2-hide{-webkit-animation:hideSweetAlert .15s forwards;animation:hideSweetAlert .15s forwards}.swal2-hide.swal2-noanimation{-webkit-animation:none;animation:none}@-webkit-keyframes animate-success-tip{0%{width:0;left:1px;top:19px}54%{width:0;left:1px;top:19px}70%{width:50px;left:-8px;top:37px}84%{width:17px;left:21px;top:48px}100%{width:25px;left:14px;top:45px}}@keyframes animate-success-tip{0%{width:0;left:1px;top:19px}54%{width:0;left:1px;top:19px}70%{width:50px;left:-8px;top:37px}84%{width:17px;left:21px;top:48px}100%{width:25px;left:14px;top:45px}}@-webkit-keyframes animate-success-long{0%{width:0;right:46px;top:54px}65%{width:0;right:46px;top:54px}84%{width:55px;right:0;top:35px}100%{width:47px;right:8px;top:38px}}@keyframes animate-success-long{0%{width:0;right:46px;top:54px}65%{width:0;right:46px;top:54px}84%{width:55px;right:0;top:35px}100%{width:47px;right:8px;top:38px}}@-webkit-keyframes rotatePlaceholder{0%{-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}5%{-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}12%{-webkit-transform:rotate(-405deg);transform:rotate(-405deg)}100%{-webkit-transform:rotate(-405deg);transform:rotate(-405deg)}}@keyframes rotatePlaceholder{0%{-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}5%{-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}12%{-webkit-transform:rotate(-405deg);transform:rotate(-405deg)}100%{-webkit-transform:rotate(-405deg);transform:rotate(-405deg)}}.swal2-animate-success-line-tip{-webkit-animation:animate-success-tip .75s;animation:animate-success-tip .75s}.swal2-animate-success-line-long{-webkit-animation:animate-success-long .75s;animation:animate-success-long .75s}.swal2-success.swal2-animate-success-icon .swal2-success-circular-line-right{-webkit-animation:rotatePlaceholder 4.25s ease-in;animation:rotatePlaceholder 4.25s ease-in}@-webkit-keyframes animate-error-icon{0%{-webkit-transform:rotateX(100deg);transform:rotateX(100deg);opacity:0}100%{-webkit-transform:rotateX(0);transform:rotateX(0);opacity:1}}@keyframes animate-error-icon{0%{-webkit-transform:rotateX(100deg);transform:rotateX(100deg);opacity:0}100%{-webkit-transform:rotateX(0);transform:rotateX(0);opacity:1}}.swal2-animate-error-icon{-webkit-animation:animate-error-icon .5s;animation:animate-error-icon .5s}@-webkit-keyframes animate-x-mark{0%{-webkit-transform:scale(.4);transform:scale(.4);margin-top:26px;opacity:0}50%{-webkit-transform:scale(.4);transform:scale(.4);margin-top:26px;opacity:0}80%{-webkit-transform:scale(1.15);transform:scale(1.15);margin-top:-6px}100%{-webkit-transform:scale(1);transform:scale(1);margin-top:0;opacity:1}}@keyframes animate-x-mark{0%{-webkit-transform:scale(.4);transform:scale(.4);margin-top:26px;opacity:0}50%{-webkit-transform:scale(.4);transform:scale(.4);margin-top:26px;opacity:0}80%{-webkit-transform:scale(1.15);transform:scale(1.15);margin-top:-6px}100%{-webkit-transform:scale(1);transform:scale(1);margin-top:0;opacity:1}}.swal2-animate-x-mark{-webkit-animation:animate-x-mark .5s;animation:animate-x-mark .5s}@-webkit-keyframes rotate-loading{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes rotate-loading{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}} \ No newline at end of file diff --git a/static/dash/js/sweetalert2.min.js b/static/dash/js/sweetalert2.min.js new file mode 100644 index 00000000..e1297ab9 --- /dev/null +++ b/static/dash/js/sweetalert2.min.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Sweetalert2=t()}(this,function(){"use strict";var e={title:"",titleText:"",text:"",html:"",type:null,customClass:"",target:"body",animation:!0,allowOutsideClick:!0,allowEscapeKey:!0,allowEnterKey:!0,showConfirmButton:!0,showCancelButton:!1,preConfirm:null,confirmButtonText:"OK",confirmButtonColor:"#3085d6",confirmButtonClass:null,cancelButtonText:"Cancel",cancelButtonColor:"#aaa",cancelButtonClass:null,buttonsStyling:!0,reverseButtons:!1,focusCancel:!1,showCloseButton:!1,showLoaderOnConfirm:!1,imageUrl:null,imageWidth:null,imageHeight:null,imageClass:null,timer:null,width:500,padding:20,background:"#fff",input:null,inputPlaceholder:"",inputValue:"",inputOptions:{},inputAutoTrim:!0,inputClass:null,inputAttributes:{},inputValidator:null,progressSteps:[],currentProgressStep:null,progressStepsDistance:"40px",onOpen:null,onClose:null},t=function(e){var t={};for(var n in e)t[e[n]]="swal2-"+e[n];return t},n=t(["container","shown","iosfix","modal","overlay","fade","show","hide","noanimation","close","title","content","buttonswrapper","confirm","cancel","icon","image","input","file","range","select","radio","checkbox","textarea","inputerror","validationerror","progresssteps","activeprogressstep","progresscircle","progressline","loading","styled"]),o=t(["success","warning","info","question","error"]),r=function(e,t){e=String(e).replace(/[^0-9a-f]/gi,""),e.length<6&&(e=e[0]+e[0]+e[1]+e[1]+e[2]+e[2]),t=t||0;for(var n="#",o=0;o<3;o++){var r=parseInt(e.substr(2*o,2),16);r=Math.round(Math.min(Math.max(0,r+r*t),255)).toString(16),n+=("00"+r).substr(r.length)}return n},i=function(e){var t=[];for(var n in e)-1===t.indexOf(e[n])&&t.push(e[n]);return t},a={previousWindowKeyDown:null,previousActiveElement:null,previousBodyPadding:null},l=function(e){if("undefined"==typeof document)return void console.error("SweetAlert2 requires document to initialize");var t=document.createElement("div");t.className=n.container,t.innerHTML=s;var o=document.querySelector(e.target);o||(console.warn("SweetAlert2: Can't find the target \""+e.target+'"'),o=document.body),o.appendChild(t);var r=c(),i=A(r,n.input),a=A(r,n.file),l=r.querySelector("."+n.range+" input"),u=r.querySelector("."+n.range+" output"),d=A(r,n.select),p=r.querySelector("."+n.checkbox+" input"),f=A(r,n.textarea);return i.oninput=function(){J.resetValidationError()},i.onkeydown=function(t){setTimeout(function(){13===t.keyCode&&e.allowEnterKey&&(t.stopPropagation(),J.clickConfirm())},0)},a.onchange=function(){J.resetValidationError()},l.oninput=function(){J.resetValidationError(),u.value=l.value},l.onchange=function(){J.resetValidationError(),l.previousSibling.value=l.value},d.onchange=function(){J.resetValidationError()},p.onchange=function(){J.resetValidationError()},f.oninput=function(){J.resetValidationError()},r},s=('\n \n').replace(/(^|\n)\s*/g,""),u=function(){return document.body.querySelector("."+n.container)},c=function(){return u()?u().querySelector("."+n.modal):null},d=function(){return c().querySelectorAll("."+n.icon)},p=function(e){return u()?u().querySelector("."+e):null},f=function(){return p(n.title)},m=function(){return p(n.content)},v=function(){return p(n.image)},h=function(){return p(n.buttonswrapper)},g=function(){return p(n.progresssteps)},y=function(){return p(n.validationerror)},b=function(){return p(n.confirm)},w=function(){return p(n.cancel)},C=function(){return p(n.close)},k=function(e){var t=[b(),w()];e&&t.reverse();var n=t.concat(Array.prototype.slice.call(c().querySelectorAll('button, input:not([type=hidden]), textarea, select, a, *[tabindex]:not([tabindex="-1"])')));return i(n)},x=function(e,t){return!!e.classList&&e.classList.contains(t)},S=function(e){if(e.focus(),"file"!==e.type){var t=e.value;e.value="",e.value=t}},E=function(e,t){if(e&&t){t.split(/\s+/).filter(Boolean).forEach(function(t){e.classList.add(t)})}},B=function(e,t){if(e&&t){t.split(/\s+/).filter(Boolean).forEach(function(t){e.classList.remove(t)})}},A=function(e,t){for(var n=0;n"),t.text||t.html){if("object"===j(t.html))if(p.innerHTML="",0 in t.html)for(var A=0;A in t.html;A++)p.appendChild(t.html[A].cloneNode(!0));else p.appendChild(t.html.cloneNode(!0));else t.html?p.innerHTML=t.html:t.text&&(p.textContent=t.text);P(p)}else T(p);t.showCloseButton?P(S):T(S),r.className=n.modal,t.customClass&&E(r,t.customClass);var M=g(),V=parseInt(null===t.currentProgressStep?J.getQueueStep():t.currentProgressStep,10);t.progressSteps.length?(P(M),L(M),V>=t.progressSteps.length&&console.warn("SweetAlert2: Invalid currentProgressStep parameter, it should be less than progressSteps.length (currentProgressStep like JS arrays starts from 0)"),t.progressSteps.forEach(function(e,o){var r=document.createElement("li");if(E(r,n.progresscircle),r.innerHTML=e,o===V&&E(r,n.activeprogressstep),M.appendChild(r),o!==t.progressSteps.length-1){var i=document.createElement("li");E(i,n.progressline),i.style.width=t.progressStepsDistance,M.appendChild(i)}})):T(M);for(var O=d(),H=0;Hwindow.innerHeight&&(a.previousBodyPadding=document.body.style.paddingRight,document.body.style.paddingRight=N()+"px")},Y=function(){null!==a.previousBodyPadding&&(document.body.style.paddingRight=a.previousBodyPadding,a.previousBodyPadding=null)},Z=function(){if(/iPad|iPhone|iPod/.test(navigator.userAgent)&&!window.MSStream&&!x(document.body,n.iosfix)){var e=document.body.scrollTop;document.body.style.top=-1*e+"px",E(document.body,n.iosfix)}},$=function(){if(x(document.body,n.iosfix)){var e=parseInt(document.body.style.top,10);B(document.body,n.iosfix),document.body.style.top="",document.body.scrollTop=-1*e}},J=function e(){for(var t=arguments.length,o=Array(t),i=0;i + +
@@ -16,7 +70,7 @@ Name MD5 - View Report + Report @@ -24,7 +78,8 @@ {{ e.NAME }} {{ e.MD5 }} - Open Report + View | + Delete {% endfor %}