Files
Mobile-Security-Framework-M…/mobsf/StaticAnalyzer/views/windows/windows.py
T
Ajin Abraham 680ca5d302 [HOTFIX] Support AAB with MobSF, Convert AAB to APK, Fixes #2387 (#2391)
* AAB to APK conversion
* relative urls fix for recent scan
2024-05-24 20:46:23 -07:00

602 lines
21 KiB
Python
Executable File

# -*- coding: utf_8 -*-
"""Windows Analysis Module."""
import base64
# Local analysis
import configparser
import json
import logging
import os
import platform
import re
import subprocess
# Binskim/Binscope analysis
import xmlrpc.client
from os.path import expanduser
import rsa
from lxml import etree
from django.conf import settings
from django.shortcuts import render
from django.utils.html import escape
from mobsf.MobSF.utils import (
file_size,
get_config_loc,
is_md5,
print_n_send_error_response,
)
from mobsf.MobSF.views.home import update_scan_timestamp
import mobsf.MalwareAnalyzer.views.VirusTotal as VirusTotal
from mobsf.StaticAnalyzer.models import (
RecentScansDB,
StaticAnalyzerWindows,
)
from mobsf.StaticAnalyzer.tools.strings import strings_util
from mobsf.StaticAnalyzer.views.common.shared_func import (
hash_gen,
unzip,
)
from mobsf.StaticAnalyzer.views.windows.db_interaction import (
get_context_from_analysis,
get_context_from_db_entry,
save_or_update,
)
from mobsf.MobSF.views.authentication import (
login_required,
)
from mobsf.MobSF.views.authorization import (
Permissions,
has_permission,
)
logger = logging.getLogger(__name__)
# Only used when xmlrpc is used
proxy = None
# Used to store the local config if windows analysis happens local
config = None
##############################################################
# Code to support Windows Static Code Analysis
##############################################################
# Windows Support Functions
@login_required
def staticanalyzer_windows(request, checksum, api=False):
"""Analyse a windows app."""
try:
# Input validation
logger.info('Windows Static Analysis Started')
rescan = False
app_dic = {} # Dict to store the binary attributes
if api:
re_scan = request.POST.get('re_scan', 0)
else:
re_scan = request.GET.get('rescan', 0)
if re_scan == '1':
rescan = True
if not is_md5(checksum):
return print_n_send_error_response(
request,
'Invalid Hash',
api)
robj = RecentScansDB.objects.filter(MD5=checksum)
if not robj.exists():
return print_n_send_error_response(
request,
'The file is not uploaded/available',
api)
typ = robj[0].SCAN_TYPE
filename = robj[0].FILE_NAME
if typ not in settings.WINDOWS_EXTS:
return print_n_send_error_response(
request,
'File type not supported',
api)
app_dic['app_name'] = filename # APP ORIGINAL NAME
app_dic['md5'] = checksum
app_dic['app_dir'] = os.path.join(
settings.UPLD_DIR, app_dic['md5'] + '/')
app_dic['tools_dir'] = os.path.join(
settings.BASE_DIR, 'StaticAnalyzer/tools/windows/')
# DB
db_entry = StaticAnalyzerWindows.objects.filter(
MD5=app_dic['md5'],
)
if db_entry.exists() and not rescan:
logger.info(
'Analysis is already Done.'
' Fetching data from the DB...')
context = get_context_from_db_entry(db_entry)
else:
if not has_permission(request, Permissions.SCAN, api):
return print_n_send_error_response(
request,
'Permission Denied',
False)
logger.info('Windows Binary Analysis Started')
app_dic['app_path'] = os.path.join(
app_dic['app_dir'], app_dic['md5'] + '.appx')
# ANALYSIS BEGINS
app_dic['size'] = str(
file_size(app_dic['app_path'])) + 'MB'
# Generate hashes
app_dic['sha1'], app_dic[
'sha256'] = hash_gen(app_dic['app_path'])
# EXTRACT APPX
logger.info('Extracting APPX')
app_dic['files'] = unzip(
app_dic['app_path'], app_dic['app_dir'])
xml_dic = _parse_xml(app_dic['app_dir'])
bin_an_dic = _binary_analysis(app_dic)
# Saving to db
logger.info('Connecting to DB')
if rescan:
logger.info('Updating Database...')
save_or_update('update',
app_dic,
xml_dic,
bin_an_dic)
update_scan_timestamp(app_dic['md5'])
else:
logger.info('Saving to Database')
save_or_update('save',
app_dic,
xml_dic,
bin_an_dic)
context = get_context_from_analysis(app_dic,
xml_dic,
bin_an_dic)
context['virus_total'] = None
template = 'static_analysis/windows_binary_analysis.html'
context['virus_total'] = None
if settings.VT_ENABLED:
vt = VirusTotal.VirusTotal()
context['virus_total'] = vt.get_result(
os.path.join(app_dic['app_dir'], app_dic['md5']) + '.appx',
app_dic['md5'])
if api:
return context
else:
return render(request, template, context)
except Exception as exception:
logger.exception('Error Performing Static Analysis')
msg = str(exception)
exp_doc = exception.__doc__
return print_n_send_error_response(request, msg, api, exp_doc)
def _get_token():
"""Get the authentication token for windows vm xmlrpc client."""
challenge = proxy.get_challenge()
priv_key = rsa.PrivateKey.load_pkcs1(
open(settings.WINDOWS_VM_SECRET).read())
signature = rsa.sign(challenge.encode('ascii'), priv_key, 'SHA-512')
sig_b64 = base64.b64encode(signature)
return sig_b64
def _binary_analysis(app_dic):
"""Start binary analsis."""
logger.info('Starting Binary Analysis')
bin_an_dic = {}
# Init optional sections to prevent None-Pointer-Errors
bin_an_dic['results'] = []
bin_an_dic['warnings'] = []
# Search for exe
for file_name in app_dic['files']:
if file_name.endswith('.exe'):
bin_an_dic['bin'] = file_name
bin_an_dic['bin_name'] = file_name.replace('.exe', '')
break
if not bin_an_dic.get('bin_name'):
logger.exception('No executable in appx.')
bin_path = os.path.join(app_dic['app_dir'], bin_an_dic['bin'])
# Execute strings command
bin_an_dic['strings'] = ''
# Make unique # pylint: disable-msg=R0204
str_list = list(set(strings_util(bin_path)))
str_list = [escape(s) for s in str_list]
bin_an_dic['strings'] = str_list
# Search for unsave function
pattern = re.compile('(alloca|gets|memcpy|printf|scanf|sprintf|sscanf|'
'strcat|StrCat|strcpy|StrCpy|strlen|StrLen|strncat|'
'StrNCat|strncpy|StrNCpy|strtok|swprintf|vsnprintf|'
'vsprintf|vswprintf|wcscat|wcscpy|wcslen|wcsncat|'
'wcsncpy|wcstok|wmemcpy)')
for elem in str_list:
if pattern.match(elem[5:-5]):
result = {
'rule_id': 'Possible Insecure Function',
'status': 'Insecure',
'desc': ('Possible Insecure '
'Function detected: {}').format(elem[5:-5])}
bin_an_dic['results'].append(result)
# Execute binskim analysis if vm is available
if platform.system() != 'Windows' or 'CI' in os.environ:
if settings.WINDOWS_VM_IP:
logger.info('Windows VM configured.')
global proxy
proxy = xmlrpc.client.ServerProxy( # pylint: disable-msg=C0103
'http://{}:{}'.format(
settings.WINDOWS_VM_IP,
settings.WINDOWS_VM_PORT))
name = _upload_sample(bin_path)
bin_an_dic = binskim(name, bin_an_dic)
bin_an_dic = binscope(name, bin_an_dic)
else:
logger.warning(
'Windows VM not configured in %s.'
' Skipping Binskim and Binscope.', get_config_loc())
warning = {
'rule_id': 'VM',
'status': 'Info',
'info': '',
'desc': 'VM is not configured. Please read the readme.md'
' in MobSF/install/windows.',
}
bin_an_dic['results'].append(warning)
else:
logger.info('Running local analysis.')
global config
config = configparser.ConfigParser()
# Switch to settings defined path if available
config.read(expanduser('~') + '\\MobSF\\Config\\config.txt')
# Run analysis functions
bin_an_dic = binskim(bin_path,
bin_an_dic,
run_local=True,
app_dir=app_dic['app_dir'])
bin_an_dic = binscope(bin_path,
bin_an_dic,
run_local=True,
app_dir=app_dic['app_dir'])
return bin_an_dic
def _upload_sample(bin_path):
"""Upload sample to windows vm."""
logger.info('Uploading sample.')
# Upload test
with open(bin_path, 'rb') as handle:
binary_data = xmlrpc.client.Binary(handle.read())
# Name of the sample is return by the remote machine
name = proxy.upload_file(binary_data, _get_token())
return name
def binskim(name, bin_an_dic, run_local=False, app_dir=None):
"""Run the binskim analysis."""
logger.info('Running binskim.')
if run_local:
bin_path = os.path.join(app_dir, bin_an_dic['bin'])
# Set params for execution of binskim
if platform.machine().endswith('64'):
binskim_path = config['binskim']['file_x64']
else:
binskim_path = config['binskim']['file_x86']
command = 'analyze'
path = bin_path
output_p = '-o'
output_d = bin_path + '_binskim'
verbose = '--verbose'
policy_p = '--config'
force = '--force'
policy_d = 'default' # TODO(Other policies?)
# Assemble
params = [
binskim_path,
command,
path,
verbose,
output_p, output_d,
policy_p, policy_d, force,
]
# Execute process
pipe = subprocess.Popen(subprocess.list2cmdline(params))
pipe.wait() # Wait for the process to finish..
# Open the file and return the json
out_file = open(output_d)
output = json.loads(out_file.read())
else:
# Analyse the sample
response = proxy.binskim(name, _get_token())
# Load output as json
output = json.loads(response)
bin_an_dic = parse_binskim(bin_an_dic, output)
return bin_an_dic
def parse_binskim(bin_an_dic, output):
"""Parse output to results and warnings."""
try:
output['runs'][0]['rules']
return parse_binskim_old(bin_an_dic, output)
except Exception:
return parse_binskim_sarif(bin_an_dic, output)
def get_short_desc(rules, rule_id):
"""Get short description from sarif."""
for i in rules:
if i['id'] == rule_id:
return i['shortDescription']['text']
def parse_binskim_sarif(bin_an_dic, output):
"""Parse new version of binskim sarif output."""
current_run = output['runs'][0]
rules = current_run['tool']['driver']['rules']
if 'results' in current_run:
for res in current_run['results']:
if res['level'] != 'pass':
if len(res['message']['arguments']) > 2:
info = ('{}, {}').format(
res['message']['arguments'][1],
res['message']['arguments'][2])
else:
info = ''
result = {
'rule_id': res['ruleId'],
'status': 'Insecure',
'info': info,
'desc': get_short_desc(rules, res['ruleId']),
}
else:
result = {
'rule_id': res['ruleId'],
'status': 'Secure',
'info': '',
'desc': get_short_desc(rules, res['ruleId']),
}
bin_an_dic['results'].append(result)
else:
logger.warning('binskim has no results.')
# Create an warning for the gui
warning = {
'rule_id': 'No Binskim-Results',
'status': 'Info',
'info': '',
'desc': 'No results from Binskim.',
}
bin_an_dic['warnings'].append(warning)
return bin_an_dic
def parse_binskim_old(bin_an_dic, output):
"""Parse old version of binskim."""
current_run = output['runs'][0]
if 'results' in current_run:
rules = output['runs'][0]['rules']
for res in current_run['results']:
if res['level'] != 'pass':
if len(res['formattedRuleMessage']['arguments']) > 2:
info = ('{}, {}').format(
res['formattedRuleMessage']['arguments'][1],
res['formattedRuleMessage']['arguments'][2])
else:
info = ''
result = {
'rule_id': res['ruleId'],
'status': 'Insecure',
'info': info,
'desc': rules[res['ruleId']]['shortDescription'],
}
else:
result = {
'rule_id': res['ruleId'],
'status': 'Secure',
'info': '',
'desc': rules[res['ruleId']]['shortDescription'],
}
bin_an_dic['results'].append(result)
else:
logger.warning('binskim has no results.')
# Create an warning for the gui
warning = {
'rule_id': 'No Binskim-Results',
'status': 'Info',
'info': '',
'desc': 'No results from Binskim.',
}
bin_an_dic['warnings'].append(warning)
if 'configurationNotifications' in current_run:
for warn in current_run['configurationNotifications']:
warning = {
'rule_id': warn['ruleId'],
'status': 'Info',
'info': '',
'desc': warn['message'],
}
bin_an_dic['warnings'].append(warning)
# Return updated dict
return bin_an_dic
def binscope(name, bin_an_dic, run_local=False, app_dir=None):
"""Run the binskim analysis."""
logger.info(
'Running binscope. This might take a'
' while, depending on the binary size.')
if run_local:
global config
bin_path = os.path.join(app_dir, bin_an_dic['bin'])
# Set params for execution of binskim
binscope_path = [config['binscope']['file']]
target = [bin_path]
out_type = ['/Red', '/v']
output = ['/l', target[0] + '_binscope']
checks = [
'/Checks', 'ATLVersionCheck',
'/Checks', 'ATLVulnCheck',
'/Checks', 'AppContainerCheck',
'/Checks', 'CompilerVersionCheck',
'/Checks', 'DBCheck',
'/Checks', 'DefaultGSCookieCheck',
'/Checks', 'ExecutableImportsCheck',
'/Checks', 'FunctionPointersCheck',
'/Checks', 'GSCheck',
'/Checks', 'GSFriendlyInitCheck',
'/Checks', 'GSFunctionSafeBuffersCheck',
'/Checks', 'HighEntropyVACheck',
'/Checks', 'NXCheck',
'/Checks', 'RSA32Check',
'/Checks', 'SafeSEHCheck',
'/Checks', 'SharedSectionCheck',
'/Checks', 'VB6Check',
'/Checks', 'WXCheck',
]
# Assemble
params = (
binscope_path
+ target
+ out_type
+ output
+ checks
)
# Execute process
p = subprocess.Popen(subprocess.list2cmdline(params))
p.wait() # Wait for the process to finish..
# Open the file and return the json
f = open(output[1])
response = f.read()
else:
# Analyse the sample via rpc
response = proxy.binscope(name, _get_token())
res = response[response.find('<'):]
config = etree.XMLParser( # pylint: disable-msg=E1101
remove_blank_text=True,
resolve_entities=False,
)
xml_file = etree.XML(bytes(res, 'utf-8', 'ignore'),
config) # pylint: disable-msg=E1101
for item in xml_file.find('items').getchildren():
if item.find('issueType') is not None:
res = item.find('result').text
if res == 'PASS':
status = 'Secure'
try:
desc = item.find('Information').text
except AttributeError:
desc = 'No description provided by analysing tool.'
elif res == 'FAIL':
status = 'Insecure'
if item.find('Failure1') is not None:
desc = item.find('Failure1').text
elif item.find('Information') is not None:
desc = item.find('Information').text
elif item.find('diagnostic') is not None:
status = 'Info'
desc = item.find('diagnostic').text
else:
desc = 'No description provided by analysing tool.'
result = {
'rule_id': item.find('issueType').text,
'status': status,
'info': '',
'desc': desc,
}
bin_an_dic['results'].append(result)
return bin_an_dic
def _parse_xml(app_dir):
"""Parse the AppxManifest file to get basic information."""
logger.info('Starting Binary Analysis - XML')
xml_file = os.path.join(app_dir, 'AppxManifest.xml')
xml_dic = {
'version': '',
'arch': '',
'app_name': '',
'pub_name': '',
'compiler_version': '',
'visual_studio_version': '',
'visual_studio_edition': '',
'target_os': '',
'appx_dll_version': '',
'proj_guid': '',
'opti_tool': '',
'target_run': '',
}
try:
logger.info('Reading AppxManifest')
config = etree.XMLParser( # pylint: disable-msg=E1101
remove_blank_text=True,
resolve_entities=False,
)
xml = etree.XML(open(xml_file, 'rb').read(),
config) # pylint: disable-msg=E1101
for child in xml.getchildren():
# } to prevent conflict with PhoneIdentity..
if isinstance(child.tag, str) and child.tag.endswith('}Identity'):
xml_dic['version'] = child.get('Version')
xml_dic['arch'] = child.get('ProcessorArchitecture')
elif (isinstance(child.tag, str)
and child.tag.endswith('Properties')):
for sub_child in child.getchildren():
if sub_child.tag.endswith('}DisplayName'):
# TODO(Needed? Compare to existing app_name)
xml_dic['app_name'] = sub_child.text
elif sub_child.tag.endswith('}PublisherDisplayName'):
xml_dic['pub_name'] = sub_child.text
elif (isinstance(child.tag, str)
and child.tag.endswith('}Metadata')):
xml_dic = parse_xml_metadata(xml_dic, child)
except Exception:
logger.exception('Reading from AppxManifest.xml')
return xml_dic
def parse_xml_metadata(xml_dic, xml_node):
"""Return the XML Metadata."""
for child in xml_node.getchildren():
if child.get('Name') == 'cl.exe':
xml_dic['compiler_version'] = child.get('Version')
elif child.get('Name') == 'VisualStudio':
xml_dic['visual_studio_version'] = child.get('Version')
elif child.get('Name') == 'VisualStudioEdition':
xml_dic['visual_studio_edition'] = child.get('Value')
elif child.get('Name') == 'OperatingSystem':
xml_dic['target_os'] = child.get('Version')
elif child.get('Name') == 'Microsoft.Build.AppxPackage.dll':
xml_dic['appx_dll_version'] = child.get('Version')
elif child.get('Name') == 'ProjectGUID':
xml_dic['proj_guid'] = child.get('Value')
elif child.get('Name') == 'OptimizingToolset':
xml_dic['opti_tool'] = child.get('Value')
elif child.get('Name') == 'TargetRuntime':
xml_dic['target_run'] = child.get('Value')
return xml_dic