mirror of
https://github.com/MobSF/Mobile-Security-Framework-MobSF.git
synced 2026-10-11 22:50:09 +00:00
614 lines
23 KiB
Python
614 lines
23 KiB
Python
# -*- coding: utf_8 -*-
|
|
"""Dynamic Analyzer Helpers."""
|
|
import logging
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
|
|
from django.conf import settings
|
|
|
|
from OpenSSL import crypto
|
|
|
|
from mobsf.DynamicAnalyzer.tools.webproxy import (
|
|
get_ca_file,
|
|
start_proxy,
|
|
stop_httptools,
|
|
)
|
|
from mobsf.MobSF.utils import (
|
|
get_adb,
|
|
get_device,
|
|
get_proxy_ip,
|
|
is_file_exists,
|
|
python_list,
|
|
)
|
|
from mobsf.StaticAnalyzer.models import StaticAnalyzerAndroid
|
|
|
|
logger = logging.getLogger(__name__)
|
|
ANDROID_API_SUPPORTED = 29
|
|
FRIDA_VERSION = '14.2.6'
|
|
|
|
|
|
class Environment:
|
|
|
|
def __init__(self, identifier=None):
|
|
if identifier:
|
|
self.identifier = identifier
|
|
else:
|
|
self.identifier = get_device()
|
|
self.tools_dir = settings.TOOLS_DIR
|
|
self.frida_str = f'MobSF-Frida-{FRIDA_VERSION}'.encode('utf-8')
|
|
self.xposed_str = b'MobSF-Xposed'
|
|
|
|
def wait(self, sec):
|
|
"""Wait in Seconds."""
|
|
logger.info('Waiting for %s seconds...', str(sec))
|
|
time.sleep(sec)
|
|
|
|
def check_connect_error(self, output):
|
|
"""Check if connect failed."""
|
|
if b'unable to connect' in output or b'failed to connect' in output:
|
|
logger.error('%s', output.decode('utf-8').replace('\n', ''))
|
|
return False
|
|
return True
|
|
|
|
def run_subprocess_verify_output(self, command):
|
|
"""Run subprocess and verify execution."""
|
|
out = subprocess.check_output(command)
|
|
self.wait(2)
|
|
return self.check_connect_error(out)
|
|
|
|
def connect_n_mount(self):
|
|
"""Test ADB Connection."""
|
|
self.adb_command(['kill-server'])
|
|
self.adb_command(['start-server'])
|
|
logger.info('ADB Restarted')
|
|
self.wait(2)
|
|
logger.info('Connecting to Android %s', self.identifier)
|
|
if not self.run_subprocess_verify_output([get_adb(),
|
|
'connect',
|
|
self.identifier]):
|
|
return False
|
|
logger.info('Restarting ADB Daemon as root')
|
|
if not self.run_subprocess_verify_output([get_adb(),
|
|
'-s',
|
|
self.identifier,
|
|
'root']):
|
|
return False
|
|
logger.info('Reconnecting to Android Device')
|
|
# connect again with root adb
|
|
if not self.run_subprocess_verify_output([get_adb(),
|
|
'connect',
|
|
self.identifier]):
|
|
return False
|
|
# identify environment
|
|
runtime = self.get_environment()
|
|
logger.info('Remounting')
|
|
# Allow non supported environments also
|
|
self.adb_command(['remount'])
|
|
logger.info('Performing System check')
|
|
if not self.system_check(runtime):
|
|
return False
|
|
return True
|
|
|
|
def is_package_installed(self, package, extra):
|
|
"""Check if package is installed."""
|
|
success = '\nSuccess' in extra
|
|
out = self.adb_command(['pm', 'list', 'packages'], True)
|
|
pkg = f'{package}'.encode('utf-8')
|
|
pkg_fmts = [pkg + b'\n', pkg + b'\r\n', pkg + b'\r\r\n']
|
|
if any(pkg in out for pkg in pkg_fmts):
|
|
# Windows uses \r\n and \r\r\n
|
|
return True
|
|
if success:
|
|
# Fallback check
|
|
return True
|
|
return False
|
|
|
|
def install_apk(self, apk_path, package):
|
|
"""Install APK and Verify Installation."""
|
|
if self.is_package_installed(package, ''):
|
|
logger.info('Removing existing installation')
|
|
# Remove existing installation'
|
|
self.adb_command(['uninstall', package], False, True)
|
|
# Disable install verification
|
|
self.adb_command([
|
|
'settings',
|
|
'put',
|
|
'global',
|
|
'verifier_verify_adb_installs',
|
|
'0',
|
|
], True)
|
|
logger.info('Installing APK')
|
|
# Install APK
|
|
out = self.adb_command([
|
|
'install',
|
|
'-r',
|
|
'-t',
|
|
'-d',
|
|
apk_path], False, True)
|
|
if not out:
|
|
return False, 'adb install failed'
|
|
out = out.decode('utf-8', 'ignore')
|
|
# Verify Installation
|
|
return self.is_package_installed(package, out), out
|
|
|
|
def adb_command(self, cmd_list, shell=False, silent=False):
|
|
"""ADB Command wrapper."""
|
|
args = [get_adb(),
|
|
'-s',
|
|
self.identifier]
|
|
if shell:
|
|
args += ['shell']
|
|
args += cmd_list
|
|
try:
|
|
result = subprocess.check_output(
|
|
args,
|
|
stderr=subprocess.STDOUT)
|
|
return result
|
|
except Exception:
|
|
if not silent:
|
|
logger.exception('Error Running ADB Command')
|
|
return None
|
|
|
|
def dz_cleanup(self, bin_hash):
|
|
"""Clean up before Dynamic Analysis."""
|
|
# Delete ScreenStream 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, bin_hash + '/screenshots-apk/')
|
|
if os.path.isdir(screen_dir):
|
|
shutil.rmtree(screen_dir)
|
|
else:
|
|
os.makedirs(screen_dir)
|
|
|
|
def configure_proxy(self, project):
|
|
"""HTTPS Proxy."""
|
|
self.install_mobsf_ca('install')
|
|
proxy_port = settings.PROXY_PORT
|
|
logger.info('Starting HTTPs Proxy on %s', proxy_port)
|
|
stop_httptools(proxy_port)
|
|
start_proxy(proxy_port, project)
|
|
|
|
def install_mobsf_ca(self, action):
|
|
"""Install or Remove MobSF Root CA."""
|
|
mobsf_ca = get_ca_file()
|
|
ca_file = None
|
|
if is_file_exists(mobsf_ca):
|
|
ca_construct = '{}.0'
|
|
pem = open(mobsf_ca, 'rb').read()
|
|
ca_obj = crypto.load_certificate(crypto.FILETYPE_PEM, pem)
|
|
ca_file_hash = hex(ca_obj.subject_name_hash()).lstrip('0x')
|
|
ca_file = os.path.join('/system/etc/security/cacerts/',
|
|
ca_construct.format(ca_file_hash))
|
|
else:
|
|
logger.warning('mitmproxy root CA is not generated yet.')
|
|
return
|
|
if action == 'install':
|
|
logger.info('Installing MobSF RootCA')
|
|
self.adb_command(['push',
|
|
mobsf_ca,
|
|
ca_file])
|
|
self.adb_command(['chmod',
|
|
'644',
|
|
ca_file], True)
|
|
elif action == 'remove':
|
|
logger.info('Removing MobSF RootCA')
|
|
self.adb_command(['rm',
|
|
ca_file], True)
|
|
# with a high timeout afterwards
|
|
|
|
def set_global_proxy(self, version):
|
|
"""Set Global Proxy on device."""
|
|
# Android 4.4+ supported
|
|
proxy_ip = None
|
|
proxy_port = settings.PROXY_PORT
|
|
if version < 5:
|
|
proxy_ip = get_proxy_ip(self.identifier)
|
|
else:
|
|
proxy_ip = settings.PROXY_IP
|
|
if proxy_ip:
|
|
if version < 4.4:
|
|
logger.warning('Please set Android VM proxy as %s:%s',
|
|
proxy_ip, proxy_port)
|
|
return
|
|
logger.info('Setting Global Proxy for Android VM')
|
|
self.adb_command(
|
|
['settings',
|
|
'put',
|
|
'global',
|
|
'http_proxy',
|
|
'{}:{}'.format(proxy_ip, proxy_port)], True)
|
|
|
|
def unset_global_proxy(self):
|
|
"""Unset Global Proxy on device."""
|
|
logger.info('Removing Global Proxy for Android VM')
|
|
self.adb_command(
|
|
['settings',
|
|
'delete',
|
|
'global',
|
|
'http_proxy'], True)
|
|
self.adb_command(
|
|
['settings',
|
|
'delete',
|
|
'global',
|
|
'global_http_proxy_host'], True)
|
|
self.adb_command(
|
|
['settings',
|
|
'delete',
|
|
'global',
|
|
'global_http_proxy_port'], True)
|
|
self.adb_command(
|
|
['settings',
|
|
'put',
|
|
'global',
|
|
'http_proxy',
|
|
':0'], True)
|
|
|
|
def enable_adb_reverse_tcp(self, version):
|
|
"""Enable ADB Reverse TCP for Proxy."""
|
|
# Androd 5+ supported
|
|
if not version >= 5:
|
|
return
|
|
proxy_port = settings.PROXY_PORT
|
|
logger.info('Enabling ADB Reverse TCP on %s', proxy_port)
|
|
tcp = 'tcp:{}'.format(proxy_port)
|
|
try:
|
|
proc = subprocess.Popen([get_adb(),
|
|
'-s', self.identifier,
|
|
'reverse', tcp, tcp],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
_, stderr = proc.communicate()
|
|
if b'error: closed' in stderr:
|
|
logger.warning('ADB Reverse TCP works only on'
|
|
' Android 5.0 and above. Please '
|
|
'configure a reachable IP Address'
|
|
' in Android proxy settings.')
|
|
elif stderr:
|
|
logger.error(stderr.decode('utf-8').replace('\n', ''))
|
|
except Exception:
|
|
logger.exception('Enabling ADB Reverse TCP')
|
|
|
|
def start_clipmon(self):
|
|
"""Start Clipboard Monitoring."""
|
|
logger.info('Starting Clipboard Monitor')
|
|
args = ['am', 'startservice',
|
|
'opensecurity.clipdump/.ClipDumper']
|
|
self.adb_command(args, True)
|
|
|
|
def get_screen_res(self):
|
|
"""Get Screen Resolution of Android Instance."""
|
|
logger.info('Getting screen resolution')
|
|
try:
|
|
resp = self.adb_command(['dumpsys', 'window'], True)
|
|
scn_rgx = re.compile(r'mUnrestrictedScreen=\(0,0\) .*')
|
|
scn_rgx2 = re.compile(r'mUnrestricted=\[0,0\]\[.*\]')
|
|
match = scn_rgx.search(resp.decode('utf-8'))
|
|
if match:
|
|
screen_res = match.group().split(' ')[1]
|
|
width, height = screen_res.split('x', 1)
|
|
return width, height
|
|
match = scn_rgx2.search(resp.decode('utf-8'))
|
|
if match:
|
|
res = match.group().split('][')[1].replace(']', '')
|
|
width, height = res.split(',', 1)
|
|
return width, height
|
|
else:
|
|
logger.error('Error getting screen resolution')
|
|
except Exception:
|
|
logger.exception('Getting screen resolution')
|
|
return '1440', '2560'
|
|
|
|
def screen_shot(self, outfile):
|
|
"""Take Screenshot."""
|
|
self.adb_command(['screencap',
|
|
'-p',
|
|
'/data/local/screen.png'], True)
|
|
self.adb_command(['pull',
|
|
'/data/local/screen.png',
|
|
outfile])
|
|
|
|
def screen_stream(self):
|
|
"""Screen Stream."""
|
|
self.adb_command(['screencap',
|
|
'-p',
|
|
'/data/local/stream.png'],
|
|
True)
|
|
self.adb_command(['pull',
|
|
'/data/local/stream.png',
|
|
'{}screen.png'.format(settings.SCREEN_DIR)])
|
|
|
|
def android_component(self, bin_hash, comp):
|
|
"""Get APK Components."""
|
|
anddb = StaticAnalyzerAndroid.objects.get(MD5=bin_hash)
|
|
resp = []
|
|
if comp == 'activities':
|
|
resp = python_list(anddb.ACTIVITIES)
|
|
elif comp == 'receivers':
|
|
resp = python_list(anddb.RECEIVERS)
|
|
elif comp == 'providers':
|
|
resp = python_list(anddb.PROVIDERS)
|
|
elif comp == 'services':
|
|
resp = python_list(anddb.SERVICES)
|
|
elif comp == 'libraries':
|
|
resp = python_list(anddb.LIBRARIES)
|
|
elif comp == 'exported_activities':
|
|
resp = python_list(anddb.EXPORTED_ACTIVITIES)
|
|
return '\n'.join(resp)
|
|
|
|
def get_environment(self):
|
|
"""Identify the environment."""
|
|
out = self.adb_command(['getprop',
|
|
'ro.boot.serialno'], True)
|
|
out += self.adb_command(['getprop',
|
|
'ro.serialno'], True)
|
|
out += self.adb_command(['getprop',
|
|
'ro.build.user'], True)
|
|
out += self.adb_command(['getprop',
|
|
'ro.manufacturer.geny-def'], True)
|
|
out += self.adb_command(['getprop',
|
|
'ro.product.manufacturer.geny-def'], True)
|
|
ver = self.adb_command(['getprop',
|
|
'ro.genymotion.version'],
|
|
True).decode('utf-8', 'ignore')
|
|
if b'EMULATOR' in out:
|
|
logger.info('Found Android Studio Emulator')
|
|
return 'emulator'
|
|
elif (b'genymotion' in out.lower()
|
|
or any(char.isdigit() for char in ver)):
|
|
logger.info('Found Genymotion x86 Android VM')
|
|
return 'genymotion'
|
|
else:
|
|
logger.warning(
|
|
'Unable to identify Dynamic Analysis environment. '
|
|
'Official support is available only for Android '
|
|
'Emulator and Genymotion VM')
|
|
return ''
|
|
|
|
def get_android_version(self):
|
|
"""Get Android version."""
|
|
out = self.adb_command(['getprop',
|
|
'ro.build.version.release'], True)
|
|
and_version = out.decode('utf-8').rstrip()
|
|
if and_version.count('.') > 1:
|
|
and_version = and_version.rsplit('.', 1)[0]
|
|
if and_version.count('.') > 1:
|
|
and_version = and_version.split('.', 1)[0]
|
|
return float(and_version)
|
|
|
|
def get_android_arch(self):
|
|
"""Get Android Architecture."""
|
|
out = self.adb_command([
|
|
'getprop',
|
|
'ro.product.cpu.abi'], True)
|
|
return out.decode('utf-8').rstrip()
|
|
|
|
def system_check(self, runtime):
|
|
"""Check if /system is writable."""
|
|
try:
|
|
try:
|
|
out = self.adb_command([
|
|
'getprop',
|
|
'ro.build.version.sdk'], True)
|
|
if out:
|
|
api = int(out.decode('utf-8').strip())
|
|
logger.info('Android API Level '
|
|
'identified as %s', api)
|
|
if api > ANDROID_API_SUPPORTED:
|
|
logger.error('This API Level is not supported'
|
|
' for Dynamic Analysis.')
|
|
return False
|
|
except Exception:
|
|
pass
|
|
err_msg = ('VM\'s /system is not writable. '
|
|
'This VM cannot be used for '
|
|
'Dynamic Analysis.')
|
|
proc = subprocess.Popen([get_adb(),
|
|
'-s', self.identifier,
|
|
'shell',
|
|
'touch',
|
|
'/system/test'],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
_, stderr = proc.communicate()
|
|
if b'Read-only' in stderr:
|
|
logger.error(err_msg)
|
|
if runtime == 'emulator':
|
|
logger.error('Please start the AVD as per '
|
|
'MobSF documentation!')
|
|
return False
|
|
except Exception:
|
|
logger.error(err_msg)
|
|
return False
|
|
return True
|
|
|
|
def launch_n_capture(self, package, activity, outfile):
|
|
"""Launch and Capture Activity."""
|
|
self.adb_command(['am',
|
|
'start',
|
|
'-n',
|
|
package + '/' + activity], True)
|
|
self.wait(3)
|
|
self.screen_shot(outfile)
|
|
logger.info('Activity screenshot captured')
|
|
logger.info('Stopping app')
|
|
self.adb_command(['am', 'force-stop', package], True)
|
|
|
|
def is_mobsfyied(self, android_version):
|
|
"""Check is Device is MobSFyed."""
|
|
logger.info('Environment MobSFyed Check')
|
|
if android_version < 5:
|
|
agent_file = '.mobsf-x'
|
|
agent_str = self.xposed_str
|
|
else:
|
|
agent_file = '.mobsf-f'
|
|
agent_str = self.frida_str
|
|
try:
|
|
out = subprocess.check_output(
|
|
[get_adb(),
|
|
'-s', self.identifier,
|
|
'shell',
|
|
'cat',
|
|
'/system/' + agent_file])
|
|
if agent_str not in out:
|
|
return False
|
|
except Exception:
|
|
return False
|
|
return True
|
|
|
|
def mobsfy_init(self):
|
|
"""Init MobSFy."""
|
|
version = self.get_android_version()
|
|
logger.info('Android Version identified as %s', version)
|
|
try:
|
|
if version < 5:
|
|
self.xposed_setup(version)
|
|
self.mobsf_agents_setup('xposed')
|
|
else:
|
|
self.frida_setup()
|
|
self.mobsf_agents_setup('frida')
|
|
logger.info('MobSFying Completed!')
|
|
return version
|
|
except Exception:
|
|
logger.exception('Failed to MobSFy Android Instance')
|
|
return False
|
|
|
|
def mobsf_agents_setup(self, agent):
|
|
"""Setup MobSF agents."""
|
|
# Install MITM RootCA
|
|
self.install_mobsf_ca('install')
|
|
# Install MobSF Agents
|
|
mobsf_agents = 'onDevice/mobsf_agents/'
|
|
clip_dump = os.path.join(self.tools_dir,
|
|
mobsf_agents,
|
|
'ClipDump.apk')
|
|
logger.info('Installing MobSF Clipboard Dumper')
|
|
self.adb_command(['install', '-r', clip_dump])
|
|
if agent == 'frida':
|
|
agent_file = '.mobsf-f'
|
|
agent_str = self.frida_str
|
|
else:
|
|
agent_file = '.mobsf-x'
|
|
agent_str = self.xposed_str
|
|
f = tempfile.NamedTemporaryFile(delete=False)
|
|
f.write(agent_str)
|
|
f.close()
|
|
self.adb_command(['push', f.name, '/system/' + agent_file])
|
|
os.unlink(f.name)
|
|
|
|
def xposed_setup(self, android_version):
|
|
"""Setup Xposed."""
|
|
xposed_dir = 'onDevice/xposed/'
|
|
xposed_modules = xposed_dir + 'modules/'
|
|
if android_version < 5:
|
|
logger.info('Installing Xposed for Kitkat and below')
|
|
xposed_apk = os.path.join(self.tools_dir,
|
|
xposed_dir,
|
|
'Xposed.apk')
|
|
hooks = os.path.join(self.tools_dir,
|
|
xposed_modules,
|
|
'hooks.json')
|
|
droidmon = os.path.join(self.tools_dir,
|
|
xposed_modules,
|
|
'Droidmon.apk')
|
|
logger.info('Installing Droidmon API Analyzer')
|
|
self.adb_command(['install', '-r', droidmon])
|
|
logger.info('Copying Droidmon hooks config')
|
|
self.adb_command(['push', hooks, '/data/local/tmp/'])
|
|
else:
|
|
logger.info('Installing Xposed for Lollipop and above')
|
|
xposed_apk = os.path.join(self.tools_dir,
|
|
xposed_dir,
|
|
'XposedInstaller_3.1.5.apk')
|
|
self.adb_command(['install', '-r', xposed_apk])
|
|
# Xposed Modules and Support Files
|
|
justrustme = os.path.join(self.tools_dir,
|
|
xposed_modules,
|
|
'JustTrustMe.apk')
|
|
rootcloak = os.path.join(self.tools_dir,
|
|
xposed_modules,
|
|
'RootCloak.apk')
|
|
proxyon = os.path.join(self.tools_dir,
|
|
xposed_modules,
|
|
'mobi.acpm.proxyon_v1_419b04.apk')
|
|
sslunpin = os.path.join(self.tools_dir,
|
|
xposed_modules,
|
|
'mobi.acpm.sslunpinning_v2_37f44f.apk')
|
|
bluepill = os.path.join(self.tools_dir,
|
|
xposed_modules,
|
|
'AndroidBluePill.apk')
|
|
logger.info('Installing JustTrustMe')
|
|
self.adb_command(['install', '-r', justrustme])
|
|
logger.info('Installing SSLUnpinning')
|
|
self.adb_command(['install', '-r', sslunpin])
|
|
logger.info('Installing ProxyOn')
|
|
self.adb_command(['install', '-r', proxyon])
|
|
logger.info('Installing RootCloak')
|
|
self.adb_command(['install', '-r', rootcloak])
|
|
logger.info('Installing Android BluePill')
|
|
self.adb_command(['install', '-r', bluepill])
|
|
logger.info('Launching Xposed Framework.')
|
|
xposed_installer = ('de.robv.android.xposed.installer/'
|
|
'de.robv.android.xposed.installer.'
|
|
'WelcomeActivity')
|
|
self.adb_command(['am', 'start', '-n',
|
|
xposed_installer], True)
|
|
|
|
def frida_setup(self):
|
|
"""Setup Frida."""
|
|
frida_arch = None
|
|
frida_dir = 'onDevice/frida/'
|
|
arch = self.get_android_arch()
|
|
logger.info('Android OS architecture identified as %s', arch)
|
|
if arch in ['armeabi-v7a', 'armeabi']:
|
|
frida_arch = 'arm'
|
|
elif arch == 'arm64-v8a':
|
|
frida_arch = 'arm64'
|
|
elif arch == 'x86':
|
|
frida_arch = 'x86'
|
|
elif arch == 'x86_64':
|
|
frida_arch = 'x86_64'
|
|
else:
|
|
logger.error('Make sure a Genymotion Android x86 VM'
|
|
' or Android Studio Emulator'
|
|
' instance is running')
|
|
return
|
|
frida_bin = 'frida-server-{}-android-{}'.format(
|
|
FRIDA_VERSION,
|
|
frida_arch)
|
|
frida_path = os.path.join(self.tools_dir,
|
|
frida_dir,
|
|
frida_bin)
|
|
logger.info('Copying frida server for %s', frida_arch)
|
|
self.adb_command(['push', frida_path, '/system/fd_server'])
|
|
self.adb_command(['chmod', '755', '/system/fd_server'], True)
|
|
|
|
def run_frida_server(self):
|
|
"""Start Frida Server."""
|
|
check = self.adb_command(['ps'], True)
|
|
if b'fd_server' in check:
|
|
logger.info('Frida Server is already running')
|
|
return
|
|
|
|
def start_frida():
|
|
fnull = open(os.devnull, 'w')
|
|
argz = [get_adb(),
|
|
'-s',
|
|
self.identifier,
|
|
'shell',
|
|
'/system/fd_server']
|
|
subprocess.call(argz, stdout=fnull, stderr=subprocess.STDOUT)
|
|
trd = threading.Thread(target=start_frida)
|
|
trd.daemon = True
|
|
trd.start()
|
|
logger.info('Starting Frida Server')
|
|
logger.info('Waiting for 2 seconds...')
|
|
time.sleep(2)
|