Multiple Features (Scan timeout, Firebase Remote Config, Search Scans) (#2441)

Support time out for SAST and Binary scans
Search by MD5, package name, file name and app name.
Search REST API + docs + tests
Firebase remote config check [FEATURE] Add support for Firebase Remote Config information  #2429
autopep8
This commit is contained in:
Ajin Abraham
2024-10-29 00:51:13 -07:00
committed by GitHub
parent d817c0bf18
commit 6947649b18
45 changed files with 443529 additions and 343460 deletions
+2 -1
View File
@@ -24,4 +24,5 @@ mobsf/downloads
mobsf/uploads
mobsf/debug.log
mobsf/secret
mobsf/StaticAnalyzer/test_files/
mobsf/StaticAnalyzer/test_files/
TODO.md
+1
View File
@@ -78,3 +78,4 @@ mobsf/secret
mobsf/StaticAnalyzer/migrations
mobsf/MobSF/windows_vm_priv_key.asc
mobsf/setup_done.txt
TODO.md
+19 -4
View File
@@ -6,6 +6,7 @@ from django.conf import settings
from mobsf.MobSF.utils import (
append_scan_status,
run_with_timeout,
settings_enabled,
)
@@ -49,13 +50,28 @@ def apkid_analysis(checksum, apk_file):
)
rules = options.rules_manager.load()
scanner = Scanner(rules, options)
res = scanner.scan_file(apk_file)
findings = {}
res = None
try:
findings = output._build_json_output(res)['files']
res = run_with_timeout(
scanner.scan_file,
settings.BINARY_ANALYSIS_TIMEOUT,
apk_file)
except Exception as e:
msg = 'APKID scan timed out'
logger.error(msg)
append_scan_status(
checksum,
msg,
str(e))
try:
if res:
findings = output._build_json_output(res)['files']
except AttributeError:
# apkid >= 2.0.3
try:
findings = output.build_json_output(res)['files']
if res:
findings = output.build_json_output(res)['files']
except AttributeError:
msg = (
'yara-python dependency required by '
@@ -66,7 +82,6 @@ def apkid_analysis(checksum, apk_file):
checksum,
msg,
'Missing dependency')
findings = {}
sanitized = {}
for item in findings:
filename = item['filename']
+1 -1
View File
@@ -16,7 +16,7 @@ from mobsf.install.windows.setup import windows_config_local
logger = logging.getLogger(__name__)
VERSION = '4.1.0'
VERSION = '4.1.1'
BANNER = r"""
__ __ _ ____ _____ _ _ _
| \/ | ___ | |__/ ___|| ___|_ _| || | / |
+3 -1
View File
@@ -330,7 +330,9 @@ LOGGING = {
},
},
}
JADX_TIMEOUT = int(os.getenv('MOBSF_JADX_TIMEOUT', 1800))
JADX_TIMEOUT = int(os.getenv('MOBSF_JADX_TIMEOUT', 1000))
SAST_TIMEOUT = int(os.getenv('MOBSF_SAST_TIMEOUT', 1000))
BINARY_ANALYSIS_TIMEOUT = int(os.getenv('MOBSF_BINARY_ANALYSIS_TIMEOUT', 600))
DISABLE_AUTHENTICATION = os.getenv('MOBSF_DISABLE_AUTHENTICATION')
RATELIMIT = os.getenv('MOBSF_RATELIMIT', '7/m')
USE_X_FORWARDED_HOST = bool(
+1 -1
View File
@@ -85,6 +85,7 @@ urlpatterns = [
# Static Analysis
re_path(r'^api/v1/upload$', api_sz.api_upload),
re_path(r'^api/v1/scan$', api_sz.api_scan),
re_path(r'^api/v1/search$', api_sz.api_search),
re_path(r'^api/v1/scan_logs$', api_sz.api_scan_logs),
re_path(r'^api/v1/delete_scan$', api_sz.api_delete_scan),
re_path(r'^api/v1/download_pdf$', api_sz.api_pdf_report),
@@ -198,7 +199,6 @@ if settings.API_ONLY == '0':
re_path(r'^search$', home.search),
re_path(r'^status/$', home.scan_status, name='status'),
re_path(r'^error/$', home.error, name='error'),
re_path(r'^not_found/$', home.not_found),
re_path(r'^zip_format/$', home.zip_format),
re_path(r'^dynamic_analysis/$', home.dynamic_analysis, name='dynamic'),
+27
View File
@@ -54,6 +54,8 @@ URL_REGEX = re.compile(
re.UNICODE)
EMAIL_REGEX = re.compile(r'[\w+.-]{1,20}@[\w-]{1,20}\.[\w]{2,10}')
USERNAME_REGEX = re.compile(r'^\w[\w\-\@\.]{1,35}$')
GOOGLE_API_KEY_REGEX = re.compile(r'AIza[0-9A-Za-z-_]{35}$')
GOOGLE_APP_ID_REGEX = re.compile(r'\d{1,2}:\d{1,50}:android:[a-f0-9]{1,50}')
class Color(object):
@@ -947,3 +949,28 @@ def get_scan_logs(checksum):
msg = 'Fetching scan logs from the DB failed.'
logger.exception(msg)
return []
class TaskTimeoutError(Exception):
pass
def run_with_timeout(func, limit, *args, **kwargs):
def run_func(result, *args, **kwargs):
result.append(func(*args, **kwargs))
result = []
thread = threading.Thread(
target=run_func,
args=(result, *args),
kwargs=kwargs)
thread.start()
thread.join(limit)
if thread.is_alive():
msg = (f'function <{func.__name__}> '
f'timed out after {limit} seconds')
raise TaskTimeoutError(msg)
if result:
return result[0]
return None
+21 -1
View File
@@ -12,7 +12,12 @@ from mobsf.MobSF.utils import (
is_md5,
)
from mobsf.MobSF.views.helpers import request_method
from mobsf.MobSF.views.home import RecentScans, Upload, delete_scan
from mobsf.MobSF.views.home import (
RecentScans,
Upload,
delete_scan,
search,
)
from mobsf.MobSF.views.api.api_middleware import make_api_response
from mobsf.StaticAnalyzer.views.android.views import view_source
from mobsf.StaticAnalyzer.views.android.static_analyzer import static_analyzer
@@ -180,6 +185,21 @@ def api_json_report(request):
return response
@request_method(['POST'])
@csrf_exempt
def api_search(request):
"""Search by checksum or text."""
if 'query' not in request.POST:
return make_api_response(
{'error': 'Missing Parameters'}, 422)
resp = search(request, api=True)
if 'checksum' in resp:
request.POST = {'hash': resp['checksum']}
return api_json_report(request)
elif 'error' in resp:
return make_api_response(resp, 404)
@request_method(['POST'])
@csrf_exempt
def api_view_source(request):
+40 -27
View File
@@ -22,6 +22,7 @@ from django.template.defaulttags import register
from mobsf.MobSF.forms import FormUtil, UploadFileForm
from mobsf.MobSF.utils import (
MD5_REGEX,
api_key,
get_md5,
is_dir_exists,
@@ -244,16 +245,6 @@ def zip_format(request):
return render(request, template, context)
def not_found(request, *args):
"""Not Found Route."""
context = {
'title': 'Not Found',
'version': settings.MOBSF_VER,
}
template = 'general/not_found.html'
return render(request, template, context)
@login_required
def dynamic_analysis(request):
"""Dynamic Analysis Landing."""
@@ -337,18 +328,43 @@ def download_apk(request):
@login_required
def search(request):
"""Search Scan by MD5 Route."""
md5 = request.GET['md5']
if re.match('[0-9a-f]{32}', md5):
db_obj = RecentScansDB.objects.filter(MD5=md5)
if db_obj.exists():
e = db_obj[0]
url = f'/{e.ANALYZER}/{e.MD5}/'
return HttpResponseRedirect(url)
else:
return HttpResponseRedirect('/not_found/')
return print_n_send_error_response(request, 'Invalid Scan Hash')
def search(request, api=False):
"""Search scan by checksum or text."""
if request.method == 'POST':
query = request.POST['query']
else:
query = request.GET['query']
if not query:
msg = 'No search query provided.'
return print_n_send_error_response(request, msg, api)
checksum = query if re.match(MD5_REGEX, query) else find_checksum(query)
if checksum and re.match(MD5_REGEX, checksum):
db_obj = RecentScansDB.objects.filter(MD5=checksum).first()
if db_obj:
url = f'/{db_obj.ANALYZER}/{db_obj.MD5}/'
if api:
return {'checksum': db_obj.MD5}
else:
return HttpResponseRedirect(url)
msg = 'You can search by MD5, app name, package name, or file name.'
return print_n_send_error_response(request, msg, api, 'Scan not found')
def find_checksum(query):
"""Get the first matching checksum from the database."""
search_fields = ['FILE_NAME', 'PACKAGE_NAME', 'APP_NAME']
for field in search_fields:
result = RecentScansDB.objects.filter(
**{f'{field}__icontains': query}).first()
if result:
return result.MD5
return None
# AJAX
@@ -453,7 +469,7 @@ def delete_scan(request, api=False):
else:
md5_hash = request.POST['md5']
data = {'deleted': 'scan hash not found'}
if re.match('[0-9a-f]{32}', md5_hash):
if re.match(MD5_REGEX, md5_hash):
# Delete DB Entries
scan = RecentScansDB.objects.filter(MD5=md5_hash)
if scan.exists():
@@ -485,10 +501,7 @@ def delete_scan(request, api=False):
except Exception as exp:
msg = str(exp)
exp_doc = exp.__doc__
if api:
return print_n_send_error_response(request, msg, True, exp_doc)
else:
return print_n_send_error_response(request, msg, False, exp_doc)
return print_n_send_error_response(request, msg, api, exp_doc)
class RecentScans(object):
+23 -6
View File
@@ -117,7 +117,7 @@ def static_analysis_test():
logger.info(resp.content)
return True
# Search by MD5
# Search by MD5 and text
if platform.system() in ['Darwin', 'Linux']:
scan_md5s = ['02e7989c457ab67eb514a8328779f256',
'82ab8b2193b3cfb1c737e3a786be363a',
@@ -133,18 +133,23 @@ def static_analysis_test():
'57bb5be0ea44a755ada4a93885c3825e',
'8179b557433835827a70510584f3143e',
'7b0a23bffc80bac05739ea1af898daad']
# Search by text
queries = [
'diva',
'webview',
]
logger.info('Running Search test')
for scan_md5 in scan_md5s:
url = '/search?md5={}'.format(scan_md5)
for q in scan_md5s + queries:
url = f'/search?query={q}'
resp = http_client.get(url, follow=True)
assert (resp.status_code == 200)
if resp.status_code == 200:
logger.info('[OK] Search by MD5 test passed for %s', scan_md5)
logger.info('[OK] Search by query test passed for %s', q)
else:
logger.error('Search by MD5 test failed for %s', scan_md5)
logger.error('Search by query test failed for %s', q)
logger.info(resp.content)
return True
logger.info('[OK] Search by MD5 tests completed')
logger.info('[OK] Search by MD5 and text tests completed')
# Deleting Scan Results
logger.info('Running Delete Scan Results test')
@@ -256,6 +261,18 @@ def api_test():
logger.error('Scan Logs API test: %s', upl['hash'])
return True
logger.info('[OK] Static Analysis API test completed')
# Search API Tests
logger.info('Running Search API tests')
for term in ['diva', 'webview', '52c50ae824e329ba8b5b7a0f523efffe']:
resp = http_client.post(
'/api/v1/search',
{'query': term},
HTTP_AUTHORIZATION=auth)
if resp.status_code == 200:
logger.info('[OK] Search API test: %s', term)
else:
logger.error('Search API test: %s', term)
return True
# PDF Tests
logger.info('Running PDF Generation API Test')
if platform.system() in ['Darwin', 'Linux']:
+78 -56
View File
@@ -130,11 +130,12 @@ def _dump_additional_attributes(additional_attributes):
def _dump_digests_or_signatures(digests_or_sigs):
infos = ""
for i,dos in enumerate(digests_or_sigs):
for i, dos in enumerate(digests_or_sigs):
infos += "\n"
infos += " [%d]\n" % i
infos += " - Signature Id : %s\n" % APK._APK_SIG_ALGO_IDS.get(dos[0], hex(dos[0]))
infos += " - Signature Id : %s\n" % APK._APK_SIG_ALGO_IDS.get(
dos[0], hex(dos[0]))
infos += " - Digest: %s" % binascii.hexlify(dos[1])
return infos
@@ -149,20 +150,22 @@ class APKV2SignedData:
def __init__(self):
self._bytes = None
self.digests = None
self.certificates = None
self.certificates = None
self.additional_attributes = None
def __str__(self):
certs_infos = ""
for i,cert in enumerate(self.certificates):
for i, cert in enumerate(self.certificates):
x509_cert = x509.Certificate.load(cert)
certs_infos += "\n"
certs_infos += " [%d]\n" % i
certs_infos += " - Issuer: %s\n" % get_certificate_name_string(x509_cert.issuer, short=True)
certs_infos += " - Subject: %s\n" % get_certificate_name_string(x509_cert.subject, short=True)
certs_infos += " - Issuer: %s\n" % get_certificate_name_string(
x509_cert.issuer, short=True)
certs_infos += " - Subject: %s\n" % get_certificate_name_string(
x509_cert.subject, short=True)
certs_infos += " - Serial Number: %s\n" % hex(x509_cert.serial_number)
certs_infos += " - Hash Algorithm: %s\n" % x509_cert.hash_algo
certs_infos += " - Signature Algorithm: %s\n" % x509_cert.signature_algo
@@ -170,7 +173,8 @@ class APKV2SignedData:
certs_infos += " - Valid not after: %s" % x509_cert['tbs_certificate']['validity']['not_after'].native
return "\n".join([
'additional_attributes : {}'.format(_dump_additional_attributes(self.additional_attributes)),
'additional_attributes : {}'.format(
_dump_additional_attributes(self.additional_attributes)),
'digests : {}'.format(_dump_digests_or_signatures(self.digests)),
'certificates : {}'.format(certs_infos),
])
@@ -262,13 +266,15 @@ class APK:
_APK_SIG_ATTR_V2_STRIPPING_PROTECTION = 0xbeeff00d
_APK_SIG_ALGO_IDS = {
0x0101 : "RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc",
0x0102 : "RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc",
0x0103 : "RSASSA-PKCS1-v1_5 with SHA2-256 digest.", # This is for build systems which require deterministic signatures.
0x0104 : "RSASSA-PKCS1-v1_5 with SHA2-512 digest.", # This is for build systems which require deterministic signatures.
0x0201 : "ECDSA with SHA2-256 digest",
0x0202 : "ECDSA with SHA2-512 digest",
0x0301 : "DSA with SHA2-256 digest",
0x0101: "RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc",
0x0102: "RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc",
# This is for build systems which require deterministic signatures.
0x0103: "RSASSA-PKCS1-v1_5 with SHA2-256 digest.",
# This is for build systems which require deterministic signatures.
0x0104: "RSASSA-PKCS1-v1_5 with SHA2-512 digest.",
0x0201: "ECDSA with SHA2-256 digest",
0x0202: "ECDSA with SHA2-512 digest",
0x0301: "DSA with SHA2-256 digest",
}
__no_magic = False
@@ -296,7 +302,8 @@ class APK:
"""
if magic_file:
logger.warning("You set magic_file but this parameter is actually unused. You should remove it.")
logger.warning(
"You set magic_file but this parameter is actually unused. You should remove it.")
self.filename = filename
@@ -345,7 +352,8 @@ class APK:
# we could print the filename here, but there are zip which are so broken
# That the filename is either very very long or does not make any sense.
# Thus we do not do it, the user might find out by using other tools.
raise BrokenAPKError("The APK is probably broken: testzip returned an error.")
raise BrokenAPKError(
"The APK is probably broken: testzip returned an error.")
if not skip_analysis:
self._apk_analysis()
@@ -375,24 +383,30 @@ class APK:
ap = AXMLPrinter(manifest_data)
if not ap.is_valid():
logger.error("Error while parsing AndroidManifest.xml - is the file valid?")
logger.error(
"Error while parsing AndroidManifest.xml - is the file valid?")
return
self.axml[i] = ap
self.xml[i] = self.axml[i].get_xml_obj()
if self.axml[i].is_packed():
logger.warning("XML Seems to be packed, operations on the AndroidManifest.xml might fail.")
logger.warning(
"XML Seems to be packed, operations on the AndroidManifest.xml might fail.")
if self.xml[i] is not None:
if self.xml[i].tag != "manifest":
logger.error("AndroidManifest.xml does not start with a <manifest> tag! Is this a valid APK?")
logger.error(
"AndroidManifest.xml does not start with a <manifest> tag! Is this a valid APK?")
return
self.package = self.get_attribute_value("manifest", "package")
self.androidversion["Code"] = self.get_attribute_value("manifest", "versionCode")
self.androidversion["Name"] = self.get_attribute_value("manifest", "versionName")
permission = list(self.get_all_attribute_value("uses-permission", "name"))
self.androidversion["Code"] = self.get_attribute_value(
"manifest", "versionCode")
self.androidversion["Name"] = self.get_attribute_value(
"manifest", "versionName")
permission = list(self.get_all_attribute_value(
"uses-permission", "name"))
self.permissions = list(set(self.permissions + permission))
for uses_permission in self.find_tags("uses-permission"):
@@ -480,7 +494,8 @@ class APK:
try:
maxSdkVersion = int(self.get_value_from_tag(item, "maxSdkVersion"))
except ValueError:
logger.warning(str(maxSdkVersion) + ' is not a valid value for <uses-permission> maxSdkVersion')
logger.warning(str(maxSdkVersion)
+ ' is not a valid value for <uses-permission> maxSdkVersion')
except TypeError:
pass
return maxSdkVersion
@@ -526,12 +541,14 @@ class APK:
# FIXME: would need to use _format_value inside get_attribute_value for each returned name!
# For example, as the activity name might be foobar.foo.bar but inside the activity it is only .bar
app_name = self.get_attribute_value('activity', 'label', name=main_activity_name)
app_name = self.get_attribute_value(
'activity', 'label', name=main_activity_name)
if app_name is None:
# No App name set
# TODO return packagename instead?
logger.warning("It looks like that no app name is set for the main activity!")
logger.warning(
"It looks like that no app name is set for the main activity!")
return ""
if app_name.startswith("@"):
@@ -549,11 +566,12 @@ class APK:
# TODO: we can not resolve this, as we lack framework-res.apk
# one exception would be when parsing framework-res.apk directly.
logger.warning("Resource ID with android package name encountered! "
"Will not resolve, framework-res.apk would be required.")
"Will not resolve, framework-res.apk would be required.")
return app_name
else:
# TODO should look this up, might be in the resources
logger.warning("Resource ID with Package name '{}' encountered! Will not resolve".format(package))
logger.warning(
"Resource ID with Package name '{}' encountered! Will not resolve".format(package))
return app_name
try:
@@ -624,7 +642,8 @@ class APK:
app_icon = "@%x" % res_id
if not app_icon:
res_id = res_parser.get_res_id_by_key(self.package, 'drawable', 'ic_launcher')
res_id = res_parser.get_res_id_by_key(
self.package, 'drawable', 'ic_launcher')
if res_id:
app_icon = "@%x" % res_id
@@ -726,7 +745,7 @@ class APK:
except AttributeError:
self.__no_magic = True
logger.warning("Not the correct Magic library was found on your "
"system. Please install python-magic or python-magic-bin!")
"system. Please install python-magic or python-magic-bin!")
return default
try:
@@ -793,9 +812,10 @@ class APK:
self.files_crc32[filename] = crc32(buffer)
if self.files_crc32[filename] != self.zip.infolist()[filename].crc32_of_uncompressed_data:
logger.error("File '{}' has different CRC32 after unpacking! "
"Declared: {:08x}, Calculated: {:08x}".format(filename,
self.zip.infolist()[filename].crc32_of_uncompressed_data,
self.files_crc32[filename]))
"Declared: {:08x}, Calculated: {:08x}".format(filename,
self.zip.infolist()[
filename].crc32_of_uncompressed_data,
self.files_crc32[filename]))
return buffer
def get_files_crc32(self):
@@ -986,7 +1006,7 @@ class APK:
if value:
# If value is still None, the attribute could not be found, thus is not present
logger.warning("Failed to get the attribute '{}' on tag '{}' with namespace. "
"But found the same attribute without namespace!".format(attribute, tag.tag))
"But found the same attribute without namespace!".format(attribute, tag.tag))
return value
def find_tags(self, tag_name, **attribute_filter):
@@ -1070,7 +1090,7 @@ class APK:
if self.xml[i] is None:
continue
activities_and_aliases = self.xml[i].findall(".//activity") + \
self.xml[i].findall(".//activity-alias")
self.xml[i].findall(".//activity-alias")
for item in activities_and_aliases:
# Some applications have more than one MAIN activity.
@@ -1139,8 +1159,8 @@ class APK:
for alias in self.find_tags('activity-alias'):
activity_alias = {}
for attribute in ['name', 'targetActivity']:
value = (alias.get(attribute) or
alias.get(self._ns(attribute)))
value = (alias.get(attribute)
or alias.get(self._ns(attribute)))
if not value:
continue
activity_alias[attribute] = self._format_value(value)
@@ -1205,7 +1225,8 @@ class APK:
:param name: the `android:name` of the parent item, e.g. activity name
:returns: a dictionary with the keys `action` and `category` containing the `android:name` of those items
"""
attributes = {"action": ["name"], "category": ["name"], "data": ['scheme', 'host', 'port', 'path', 'pathPattern', 'pathPrefix', 'mimeType']}
attributes = {"action": ["name"], "category": ["name"], "data": [
'scheme', 'host', 'port', 'path', 'pathPattern', 'pathPrefix', 'mimeType']}
d = {}
for element in attributes.keys():
@@ -1218,7 +1239,7 @@ class APK:
for sitem in item.findall(".//intent-filter"):
for element in d.keys():
for ssitem in sitem.findall(element):
if element == 'data': # multiple attributes
if element == 'data': # multiple attributes
values = {}
for attribute in attributes[element]:
value = ssitem.get(self._ns(attribute))
@@ -1288,7 +1309,7 @@ class APK:
implied.append([READ_PHONE_STATE, None])
if (WRITE_EXTERNAL_STORAGE in self.permissions or implied_WRITE_EXTERNAL_STORAGE) \
and READ_EXTERNAL_STORAGE not in self.permissions:
and READ_EXTERNAL_STORAGE not in self.permissions:
maxSdkVersion = None
for name, version in self.uses_permissions:
if name == WRITE_EXTERNAL_STORAGE:
@@ -1298,10 +1319,10 @@ class APK:
if target_sdk_version < 16:
if READ_CONTACTS in self.permissions \
and READ_CALL_LOG not in self.permissions:
and READ_CALL_LOG not in self.permissions:
implied.append([READ_CALL_LOG, None])
if WRITE_CONTACTS in self.permissions \
and WRITE_CALL_LOG not in self.permissions:
and WRITE_CALL_LOG not in self.permissions:
implied.append([WRITE_CALL_LOG, None])
return implied
@@ -1317,8 +1338,8 @@ class APK:
filled_permissions = permissions.copy()
for permission in filled_permissions:
protection_level, label, description = filled_permissions[permission]
if ((not label or not description)
and permission in self.permission_module_min_sdk):
if ((not label or not description)
and permission in self.permission_module_min_sdk):
x = self.permission_module_min_sdk[permission]
protection_level = self._update_permission_protection_level(
x['protectionLevel'], min_sdk)
@@ -1327,7 +1348,7 @@ class APK:
else:
filled_permissions[permission] = [
self._update_permission_protection_level(
protection_level, target_sdk),
protection_level, target_sdk),
label, description]
return filled_permissions
@@ -1697,7 +1718,7 @@ class APK:
if r == self._PK_END_OF_CENTRAL_DIR:
# Read central dir
this_disk, disk_central, this_entries, total_entries, \
size_central, offset_central = unpack('<HHHHII', f.read(16))
size_central, offset_central = unpack('<HHHHII', f.read(16))
# TODO according to the standard we need to check if the
# end of central directory is the last item in the zip file
# TODO We also need to check if the central dir is exactly
@@ -1705,9 +1726,11 @@ class APK:
# These things should not happen for APKs
if this_disk != 0:
logger.warning("This is a multi disk ZIP! Attempting to process its signature anyway!")
logger.warning(
"This is a multi disk ZIP! Attempting to process its signature anyway!")
if disk_central != 0:
logger.warning("This is a multi disk ZIP! Attempting to process its signature anyway!")
logger.warning(
"This is a multi disk ZIP! Attempting to process its signature anyway!")
break
f.seek(-4, io.SEEK_CUR)
@@ -1750,7 +1773,6 @@ class APK:
if self._APK_SIG_KEY_V3_SIGNATURE in self._v2_blocks:
self._is_signed_v3 = True
def parse_v3_signing_block(self):
"""
Parse the V2 signing block and extract all features
@@ -1800,7 +1822,6 @@ class APK:
raw_digests = signed_data.read(len_digests)
digests = self.parse_signatures_or_digests(raw_digests)
# Certs
certs = []
len_certs = self.read_uint32_le(signed_data)
@@ -1841,7 +1862,7 @@ class APK:
publickey = block.read(len_publickey)
signer = APKV3Signer()
signer._bytes = view[off_signer:off_signer+size_signer]
signer._bytes = view[off_signer:off_signer + size_signer]
signer.signed_data = signed_data_object
signer.signatures = sigs
signer.public_key = publickey
@@ -1925,7 +1946,7 @@ class APK:
publickey = block.read(len_publickey)
signer = APKV2Signer()
signer._bytes = view[off_signer:off_signer+size_signer]
signer._bytes = view[off_signer:off_signer + size_signer]
signer.signed_data = signed_data_object
signer.signatures = sigs
signer.public_key = publickey
@@ -1997,14 +2018,14 @@ class APK:
Return a list of :class:`asn1crypto.keys.PublicKeyInfo` which are found
in the v3 signing block.
"""
return [ keys.PublicKeyInfo.load(pkey) for pkey in self.get_public_keys_der_v3()]
return [keys.PublicKeyInfo.load(pkey) for pkey in self.get_public_keys_der_v3()]
def get_public_keys_v2(self):
"""
Return a list of :class:`asn1crypto.keys.PublicKeyInfo` which are found
in the v2 signing block.
"""
return [ keys.PublicKeyInfo.load(pkey) for pkey in self.get_public_keys_der_v2()]
return [keys.PublicKeyInfo.load(pkey) for pkey in self.get_public_keys_der_v2()]
def get_certificates_v3(self):
"""
@@ -2013,7 +2034,7 @@ class APK:
Note that we simply extract all certificates regardless of the signer.
Therefore this is just a list of all certificates found in all signers.
"""
return [ x509.Certificate.load(cert) for cert in self.get_certificates_der_v3()]
return [x509.Certificate.load(cert) for cert in self.get_certificates_der_v3()]
def get_certificates_v2(self):
"""
@@ -2022,7 +2043,7 @@ class APK:
Note that we simply extract all certificates regardless of the signer.
Therefore this is just a list of all certificates found in all signers.
"""
return [ x509.Certificate.load(cert) for cert in self.get_certificates_der_v2()]
return [x509.Certificate.load(cert) for cert in self.get_certificates_der_v2()]
def get_certificates_v1(self):
"""
@@ -2077,7 +2098,8 @@ class APK:
if "{}.SF".format(i.rsplit(".", 1)[0]) in self.get_files():
signatures.append(i)
else:
logger.warning("v1 signature file {} missing .SF file - Partial signature!".format(i))
logger.warning(
"v1 signature file {} missing .SF file - Partial signature!".format(i))
return signatures
+188 -108
View File
@@ -24,23 +24,23 @@ RES_STRING_POOL_TYPE = 0x0001
RES_TABLE_TYPE = 0x0002
RES_XML_TYPE = 0x0003
RES_XML_FIRST_CHUNK_TYPE = 0x0100
RES_XML_START_NAMESPACE_TYPE = 0x0100
RES_XML_END_NAMESPACE_TYPE = 0x0101
RES_XML_START_ELEMENT_TYPE = 0x0102
RES_XML_END_ELEMENT_TYPE = 0x0103
RES_XML_CDATA_TYPE = 0x0104
RES_XML_LAST_CHUNK_TYPE = 0x017f
RES_XML_FIRST_CHUNK_TYPE = 0x0100
RES_XML_START_NAMESPACE_TYPE = 0x0100
RES_XML_END_NAMESPACE_TYPE = 0x0101
RES_XML_START_ELEMENT_TYPE = 0x0102
RES_XML_END_ELEMENT_TYPE = 0x0103
RES_XML_CDATA_TYPE = 0x0104
RES_XML_LAST_CHUNK_TYPE = 0x017f
RES_XML_RESOURCE_MAP_TYPE = 0x0180
RES_XML_RESOURCE_MAP_TYPE = 0x0180
RES_TABLE_PACKAGE_TYPE = 0x0200
RES_TABLE_TYPE_TYPE = 0x0201
RES_TABLE_TYPE_SPEC_TYPE = 0x0202
RES_TABLE_LIBRARY_TYPE = 0x0203
RES_TABLE_OVERLAYABLE_TYPE = 0x0204
RES_TABLE_PACKAGE_TYPE = 0x0200
RES_TABLE_TYPE_TYPE = 0x0201
RES_TABLE_TYPE_SPEC_TYPE = 0x0202
RES_TABLE_LIBRARY_TYPE = 0x0203
RES_TABLE_OVERLAYABLE_TYPE = 0x0204
RES_TABLE_OVERLAYABLE_POLICY_TYPE = 0x0205
RES_TABLE_STAGED_ALIAS_TYPE = 0x0206
RES_TABLE_STAGED_ALIAS_TYPE = 0x0206
# Flags in the STRING Section
SORTED_FLAG = 1 << 0
UTF8_FLAG = 1 << 8
@@ -104,6 +104,7 @@ class StringBlock:
See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#436
"""
def __init__(self, buff, header):
"""
:param buff: buffer which holds the string block
@@ -126,7 +127,8 @@ class StringBlock:
self.stringsOffset = unpack('<I', buff.read(4))[0]
# check if the stringCount is correct
if (self.stringsOffset - (self.styleCount * 4 + 28)) / 4 != self.stringCount:
self.stringCount = int((self.stringsOffset - (self.styleCount * 4 + 28)) / 4)
self.stringCount = int(
(self.stringsOffset - (self.styleCount * 4 + 28)) / 4)
# style_pool_offset
# The styles offset is counted as well from the beginning of the string section
@@ -135,7 +137,7 @@ class StringBlock:
# Check if they supplied a stylesOffset even if the count is 0:
if self.styleCount == 0 and self.stylesOffset > 0:
logger.info("Styles Offset given, but styleCount is zero. "
"This is not a problem but could indicate packers.")
"This is not a problem but could indicate packers.")
self.m_stringOffsets = []
self.m_styleOffsets = []
@@ -246,7 +248,8 @@ class StringBlock:
data = self.m_charbuff[offset: offset + encoded_bytes]
if self.m_charbuff[offset + encoded_bytes] != 0:
raise ResParserError("UTF-8 String is not null terminated! At offset={}".format(offset))
raise ResParserError(
"UTF-8 String is not null terminated! At offset={}".format(offset))
return self._decode_bytes(data, 'utf-8', str_len)
@@ -266,7 +269,8 @@ class StringBlock:
data = self.m_charbuff[offset: offset + encoded_bytes]
if self.m_charbuff[offset + encoded_bytes:offset + encoded_bytes + 2] != b"\x00\x00":
raise ResParserError("UTF-16 String is not null terminated! At offset={}".format(offset))
raise ResParserError(
"UTF-16 String is not null terminated! At offset={}".format(offset))
return self._decode_bytes(data, 'utf-16', str_len)
@@ -319,9 +323,11 @@ class StringBlock:
# These are true asserts, as the size should never be less than the values
if sizeof_char == 1:
assert length <= 0x7FFF, "length of UTF-8 string is too large! At offset={}".format(offset)
assert length <= 0x7FFF, "length of UTF-8 string is too large! At offset={}".format(
offset)
else:
assert length <= 0x7FFFFFFF, "length of UTF-16 string is too large! At offset={}".format(offset)
assert length <= 0x7FFFFFFF, "length of UTF-16 string is too large! At offset={}".format(
offset)
return length, size
@@ -376,6 +382,7 @@ class AXMLParser:
See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#563
"""
def __init__(self, raw_buff):
logger.debug("AXMLParser")
@@ -388,14 +395,16 @@ class AXMLParser:
# Minimum is a single ARSCHeader, which would be a strange edge case...
if self.buff_size < 8:
logger.error("Filesize is too small to be a valid AXML file! Filesize: {}".format(self.buff_size))
logger.error(
"Filesize is too small to be a valid AXML file! Filesize: {}".format(self.buff_size))
self._valid = False
return
# This would be even stranger, if an AXML file is larger than 4GB...
# But this is not possible as the maximum chunk size is a unsigned 4 byte int.
if self.buff_size > 0xFFFFFFFF:
logger.error("Filesize is too large to be a valid AXML file! Filesize: {}".format(self.buff_size))
logger.error(
"Filesize is too large to be a valid AXML file! Filesize: {}".format(self.buff_size))
self._valid = False
return
@@ -412,15 +421,18 @@ class AXMLParser:
if axml_header.header_size == 28024:
# Can be a common error: the file is not an AXML but a plain XML
# The file will then usually start with '<?xm' / '3C 3F 78 6D'
logger.warning("Header size is 28024! Are you trying to parse a plain XML file?")
logger.warning(
"Header size is 28024! Are you trying to parse a plain XML file?")
if axml_header.header_size != 8:
logger.error("This does not look like an AXML file. header size does not equal 8! header size = {}".format(axml_header.header_size))
logger.error("This does not look like an AXML file. header size does not equal 8! header size = {}".format(
axml_header.header_size))
self._valid = False
return
if self.filesize > self.buff_size:
logger.error("This does not look like an AXML file. Declared filesize does not match real size: {} vs {}".format(self.filesize, self.buff_size))
logger.error("This does not look like an AXML file. Declared filesize does not match real size: {} vs {}".format(
self.filesize, self.buff_size))
self._valid = False
return
@@ -428,7 +440,7 @@ class AXMLParser:
# The file can still be parsed up to the point where the chunk should end.
self.axml_tampered = True
logger.warning("Declared filesize ({}) is smaller than total file size ({}). "
"Was something appended to the file? Trying to parse it anyways.".format(self.filesize, self.buff.size()))
"Was something appended to the file? Trying to parse it anyways.".format(self.filesize, self.buff.size()))
# Not that severe of an error, we have plenty files where this is not
# set correctly
@@ -448,7 +460,8 @@ class AXMLParser:
return
if header.header_size != 0x1C:
logger.error("This does not look like an AXML file. String chunk header size does not equal 28! header size = {}".format(header.header_size))
logger.error("This does not look like an AXML file. String chunk header size does not equal 28! header size = {}".format(
header.header_size))
self._valid = False
return
@@ -530,14 +543,15 @@ class AXMLParser:
# h.size is the size of the whole chunk including the header.
# We read already 8 bytes of the header, thus we need to
# subtract them.
logger.error("Not a XML resource chunk type: 0x{:04x}. Skipping {} bytes".format(h.type, h.size))
logger.error(
"Not a XML resource chunk type: 0x{:04x}. Skipping {} bytes".format(h.type, h.size))
self.buff.seek(h.end)
continue
# Check that we read a correct header
if h.header_size != 0x10:
logger.error("XML Resource Type Chunk header size does not match 16! " \
"At chunk type 0x{:04x}, declared header size=0x{:04x}, chunk size=0x{:04x}".format(h.type, h.header_size, h.size))
logger.error("XML Resource Type Chunk header size does not match 16! "
"At chunk type 0x{:04x}, declared header size=0x{:04x}, chunk size=0x{:04x}".format(h.type, h.header_size, h.size))
self.buff.seek(h.end)
continue
@@ -548,7 +562,8 @@ class AXMLParser:
self.m_comment_index, = unpack('<L', self.buff.read(4))
if self.m_comment_index != 0xFFFFFFFF and h.type in [RES_XML_START_NAMESPACE_TYPE, RES_XML_END_NAMESPACE_TYPE]:
logger.warning("Unhandled Comment at namespace chunk: '{}'".format(self.sb[self.m_comment_index]))
logger.warning("Unhandled Comment at namespace chunk: '{}'".format(
self.sb[self.m_comment_index]))
if h.type == RES_XML_START_NAMESPACE_TYPE:
prefix, = unpack('<L', self.buff.read(4))
@@ -557,15 +572,16 @@ class AXMLParser:
s_prefix = self.sb[prefix]
s_uri = self.sb[uri]
logger.debug("Start of Namespace mapping: prefix {}: '{}' --> uri {}: '{}'".format(prefix, s_prefix, uri, s_uri))
logger.debug(
"Start of Namespace mapping: prefix {}: '{}' --> uri {}: '{}'".format(prefix, s_prefix, uri, s_uri))
if s_uri == '':
logger.warning("Namespace prefix '{}' resolves to empty URI. "
"This might be a packer.".format(s_prefix))
"This might be a packer.".format(s_prefix))
if (prefix, uri) in self.namespaces:
logger.debug("Namespace mapping ({}, {}) already seen! "
"This is usually not a problem but could indicate packers or broken AXML compilers.".format(prefix, uri))
"This is usually not a problem but could indicate packers or broken AXML compilers.".format(prefix, uri))
self.namespaces.append((prefix, uri))
# We can continue with the next chunk, as we store the namespace
@@ -582,7 +598,7 @@ class AXMLParser:
self.namespaces.remove((prefix, uri))
else:
logger.warning("Reached a NAMESPACE_END without having the namespace stored before? "
"Prefix ID: {}, URI ID: {}".format(prefix, uri))
"Prefix ID: {}, URI ID: {}".format(prefix, uri))
# We can continue with the next chunk, as we store the namespace
# mappings for each tag
@@ -661,17 +677,18 @@ class AXMLParser:
size, res0, dataType, data = unpack("<HBBL", self.buff.read(8))
logger.debug("found a CDATA Chunk: "
"index={: 6d}, size={: 4d}, res0={: 4d}, dataType={: 4d}, data={: 4d}".format(self.m_name,
size,
res0,
dataType,
data))
"index={: 6d}, size={: 4d}, res0={: 4d}, dataType={: 4d}, data={: 4d}".format(self.m_name,
size,
res0,
dataType,
data))
self.m_event = TEXT
break
# Still here? Looks like we read an unknown XML header, try to skip it...
logger.warning("Unknown XML Chunk: 0x{:04x}, skipping {} bytes.".format(h.type, h.size))
logger.warning(
"Unknown XML Chunk: 0x{:04x}, skipping {} bytes.".format(h.type, h.size))
self.buff.seek(h.end)
@property
@@ -829,7 +846,8 @@ class AXMLParser:
if not res or res == ":":
attr = self.m_resourceIDs[name]
if attr in public.SYSTEM_RESOURCES['attributes']['inverse']:
res = 'android:' + public.SYSTEM_RESOURCES['attributes']['inverse'][attr]
res = 'android:' + \
public.SYSTEM_RESOURCES['attributes']['inverse'][attr]
else:
# Attach the HEX Number, so for multiple missing attributes we do not run
# into problems.
@@ -891,10 +909,10 @@ def format_value(_type, _data, lookup_string=lambda ix: "<string>"):
# Function to prepend android prefix for attributes/references from the
# android library
fmt_package = lambda x: "android:" if x >> 24 == 1 else ""
def fmt_package(x): return "android:" if x >> 24 == 1 else ""
# Function to represent integers
fmt_int = lambda x: (0x7FFFFFFF & x) - 0x80000000 if x > 0x7FFFFFFF else x
def fmt_int(x): return (0x7FFFFFFF & x) - 0x80000000 if x > 0x7FFFFFFF else x
if _type == TYPE_STRING:
return lookup_string(_data)
@@ -962,11 +980,13 @@ class AXMLPrinter:
comment = self.axml.comment
if comment:
if self.root is None:
logger.warning("Can not attach comment with content '{}' without root!".format(comment))
logger.warning(
"Can not attach comment with content '{}' without root!".format(comment))
else:
cur[-1].append(etree.Comment(comment))
logger.debug("START_TAG: {} (line={})".format(tag, self.axml.m_lineNumber))
logger.debug("START_TAG: {} (line={})".format(
tag, self.axml.m_lineNumber))
elem = etree.Element(tag, nsmap=self.axml.nsmap)
for i in range(self.axml.getAttributeCount()):
@@ -974,9 +994,11 @@ class AXMLPrinter:
uri, name = self._fix_name(uri, self.axml.getAttributeName(i))
value = self._fix_value(self._get_attribute_value(i))
logger.debug("found an attribute: {}{}='{}'".format(uri, name, value.encode("utf-8")))
logger.debug("found an attribute: {}{}='{}'".format(
uri, name, value.encode("utf-8")))
if "{}{}".format(uri, name) in elem.attrib:
logger.warning("Duplicate attribute '{}{}'! Will overwrite!".format(uri, name))
logger.warning(
"Duplicate attribute '{}{}'! Will overwrite!".format(uri, name))
elem.set("{}{}".format(uri, name), value)
if self.root is None:
@@ -984,20 +1006,23 @@ class AXMLPrinter:
else:
if not cur:
# looks like we lost the root?
logger.error("No more elements available to attach to! Is the XML malformed?")
logger.error(
"No more elements available to attach to! Is the XML malformed?")
break
cur[-1].append(elem)
cur.append(elem)
if _type == END_TAG:
if not cur:
logger.warning("Too many END_TAG! No more elements available to attach to!")
logger.warning(
"Too many END_TAG! No more elements available to attach to!")
name = self.axml.name
uri = self._print_namespace(self.axml.namespace)
tag = "{}{}".format(uri, name)
if cur[-1].tag != tag:
logger.warning("Closing tag '{}' does not match current stack! At line number: {}. Is the XML malformed?".format(self.axml.name, self.axml.m_lineNumber))
logger.warning("Closing tag '{}' does not match current stack! At line number: {}. Is the XML malformed?".format(
self.axml.name, self.axml.m_lineNumber))
cur.pop()
if _type == TEXT:
logger.debug("TEXT for {}".format(cur[-1]))
@@ -1005,7 +1030,8 @@ class AXMLPrinter:
if _type == END_DOCUMENT:
# Check if all namespace mappings are closed
if len(self.axml.namespaces) > 0:
logger.warning("Not all namespace mappings were closed! Malformed AXML?")
logger.warning(
"Not all namespace mappings were closed! Malformed AXML?")
break
def get_buff(self):
@@ -1089,12 +1115,13 @@ class AXMLPrinter:
"""
if not name[0].isalpha() and name[0] != "_":
logger.warning("Invalid start for name '{}'. "
"XML name must start with a letter.".format(name))
"XML name must start with a letter.".format(name))
self.packerwarning = True
name = "_{}".format(name)
if name.startswith("android:") and prefix == '' and 'android' in self.axml.nsmap:
# Seems be a common thing...
logger.info("Name '{}' starts with 'android:' prefix but 'android' is a known prefix. Replacing prefix.".format(name))
logger.info(
"Name '{}' starts with 'android:' prefix but 'android' is a known prefix. Replacing prefix.".format(name))
prefix = self._print_namespace(self.axml.nsmap['android'])
name = name[len("android:"):]
# It looks like this is some kind of packer... Not sure though.
@@ -1103,13 +1130,14 @@ class AXMLPrinter:
self.packerwarning = True
embedded_prefix, new_name = name.split(":", 1)
if embedded_prefix in self.axml.nsmap:
logger.info("Prefix '{}' is in namespace mapping, assume that it is a prefix.")
logger.info(
"Prefix '{}' is in namespace mapping, assume that it is a prefix.")
prefix = self._print_namespace(self.axml.nsmap[embedded_prefix])
name = new_name
else:
# Print out an extra warning
logger.warning("Confused: name contains a unknown namespace prefix: '{}'. "
"This is either a broken AXML file or some attempt to break stuff.".format(name))
"This is either a broken AXML file or some attempt to break stuff.".format(name))
if not re.match(r"^[a-zA-Z0-9._-]*$", name):
logger.warning("Name '{}' contains invalid characters!".format(name))
self.packerwarning = True
@@ -1129,16 +1157,18 @@ class AXMLPrinter:
:return: the cleaned value
"""
if not self.__charrange or not self.__replacement:
self.__charrange = re.compile('^[\u0020-\uD7FF\u0009\u000A\u000D\uE000-\uFFFD\U00010000-\U0010FFFF]*$')
self.__replacement = re.compile('[^\u0020-\uD7FF\u0009\u000A\u000D\uE000-\uFFFD\U00010000-\U0010FFFF]')
self.__charrange = re.compile(
'^[\u0020-\uD7FF\u0009\u000A\u000D\uE000-\uFFFD\U00010000-\U0010FFFF]*$')
self.__replacement = re.compile(
'[^\u0020-\uD7FF\u0009\u000A\u000D\uE000-\uFFFD\U00010000-\U0010FFFF]')
# Reading string until \x00. This is the same as aapt does.
if "\x00" in value:
self.packerwarning = True
logger.warning("Null byte found in attribute value at position {}: "
"Value(hex): '{}'".format(
value.find("\x00"),
binascii.hexlify(value.encode("utf-8"))))
"Value(hex): '{}'".format(
value.find("\x00"),
binascii.hexlify(value.encode("utf-8"))))
value = value[:value.find("\x00")]
if not self.__charrange.match(value):
@@ -1154,7 +1184,8 @@ class AXMLPrinter:
# See http://aospxref.com/android-13.0.0_r3/xref/frameworks/native/include/android/configuration.h#56
ACONFIGURATION_ORIENTATION_ANY = 0x0000
ACONFIGURATION_ORIENTATION_ANY = 0x0000
ACONFIGURATION_ORIENTATION_PORT = 0x0001
ACONFIGURATION_ORIENTATION_LAND = 0x0002
ACONFIGURATION_ORIENTATION_SQUARE = 0x0003
@@ -1172,11 +1203,11 @@ ACONFIGURATION_DENSITY_XXHIGH = 480
ACONFIGURATION_DENSITY_XXXHIGH = 640
ACONFIGURATION_DENSITY_ANY = 0xfffe
ACONFIGURATION_DENSITY_NONE = 0xffff
ACONFIGURATION_KEYBOARD_ANY = 0x0000
ACONFIGURATION_KEYBOARD_ANY = 0x0000
ACONFIGURATION_KEYBOARD_NOKEYS = 0x0001
ACONFIGURATION_KEYBOARD_QWERTY = 0x0002
ACONFIGURATION_KEYBOARD_12KEY = 0x0003
ACONFIGURATION_NAVIGATION_ANY = 0x0000
ACONFIGURATION_NAVIGATION_ANY = 0x0000
ACONFIGURATION_NAVIGATION_NONAV = 0x0001
ACONFIGURATION_NAVIGATION_DPAD = 0x0002
ACONFIGURATION_NAVIGATION_TRACKBALL = 0x0003
@@ -1357,6 +1388,7 @@ class ARSCParser:
Each package is a chunk of type RES_TABLE_PACKAGE_TYPE.
It contains again many more chunks.
"""
def __init__(self, raw_buff):
"""
:param bytes raw_buff: the raw bytes of the file
@@ -1365,7 +1397,8 @@ class ARSCParser:
self.buff_size = self.buff.raw.getbuffer().nbytes
if self.buff_size < 8 or self.buff_size > 0xFFFFFFFF:
raise ResParserError("Invalid file size {} for a resources.arsc file!".format(self.buff.size()))
raise ResParserError(
"Invalid file size {} for a resources.arsc file!".format(self.buff.size()))
self.analyzed = False
self._resolved_strings = None
@@ -1381,22 +1414,27 @@ class ARSCParser:
# More sanity checks...
if self.header.header_size != 12:
logger.warning("The ResTable_header has an unexpected header size! Expected 12 bytes, got {}.".format(self.header.header_size))
logger.warning("The ResTable_header has an unexpected header size! Expected 12 bytes, got {}.".format(
self.header.header_size))
if self.header.size > self.buff_size:
raise ResParserError("The file seems to be truncated. Refuse to parse the file! Filesize: {}, declared size: {}".format(self.buff_size, self.header.size))
raise ResParserError("The file seems to be truncated. Refuse to parse the file! Filesize: {}, declared size: {}".format(
self.buff_size, self.header.size))
if self.header.size < self.buff_size:
logger.warning("The Resource file seems to have data appended to it. Filesize: {}, declared size: {}".format(self.buff_size, self.header.size))
logger.warning("The Resource file seems to have data appended to it. Filesize: {}, declared size: {}".format(
self.buff_size, self.header.size))
# The ResTable_header contains the packageCount, i.e. the number of ResTable_package
self.packageCount = unpack('<I', self.buff.read(4))[0]
# Even more sanity checks...
if self.packageCount < 1:
logger.warning("The number of packages is smaller than one. There should be at least one package!")
logger.warning(
"The number of packages is smaller than one. There should be at least one package!")
logger.debug("Parsed ResTable_header with {} package(s) inside.".format(self.packageCount))
logger.debug(
"Parsed ResTable_header with {} package(s) inside.".format(self.packageCount))
# skip to the start of the first chunk's data, skipping trailing header bytes (there should be none)
self.buff.seek(self.header.start + self.header.header_size)
@@ -1408,32 +1446,39 @@ class ARSCParser:
if res_header.end > self.header.end:
# this inner chunk crosses the boundary of the table chunk
logger.warning("Invalid chunk found! It is larger than the outer chunk: %s", res_header)
logger.warning(
"Invalid chunk found! It is larger than the outer chunk: %s", res_header)
break
if res_header.type == RES_STRING_POOL_TYPE:
# There should be only one StringPool per resource table.
if self.stringpool_main:
logger.warning("Already found a ResStringPool_header, but there should be only one! Will not parse the Pool again.")
logger.warning(
"Already found a ResStringPool_header, but there should be only one! Will not parse the Pool again.")
else:
self.stringpool_main = StringBlock(self.buff, res_header)
logger.debug("Found the main string pool: %s", self.stringpool_main)
elif res_header.type == RES_TABLE_PACKAGE_TYPE:
if len(self.packages) > self.packageCount:
raise ResParserError("Got more packages ({}) than expected ({})".format(len(self.packages), self.packageCount))
raise ResParserError("Got more packages ({}) than expected ({})".format(
len(self.packages), self.packageCount))
current_package = ARSCResTablePackage(self.buff, res_header)
package_name = current_package.get_name()
# After the Header, we have the resource type symbol table
self.buff.seek(current_package.header.start + current_package.typeStrings)
type_sp_header = ARSCHeader(self.buff, expected_type=RES_STRING_POOL_TYPE)
self.buff.seek(current_package.header.start
+ current_package.typeStrings)
type_sp_header = ARSCHeader(
self.buff, expected_type=RES_STRING_POOL_TYPE)
mTableStrings = StringBlock(self.buff, type_sp_header)
# Next, we should have the resource key symbol table
self.buff.seek(current_package.header.start + current_package.keyStrings)
key_sp_header = ARSCHeader(self.buff, expected_type=RES_STRING_POOL_TYPE)
self.buff.seek(current_package.header.start
+ current_package.keyStrings)
key_sp_header = ARSCHeader(
self.buff, expected_type=RES_STRING_POOL_TYPE)
mKeyStrings = StringBlock(self.buff, key_sp_header)
# Add them to the dict of read packages
@@ -1441,7 +1486,8 @@ class ARSCParser:
self.packages[package_name].append(mTableStrings)
self.packages[package_name].append(mKeyStrings)
pc = PackageContext(current_package, self.stringpool_main, mTableStrings, mKeyStrings)
pc = PackageContext(
current_package, self.stringpool_main, mTableStrings, mKeyStrings)
logger.debug("Constructed a PackageContext: %s", pc)
# skip to the first header in this table package chunk
@@ -1450,14 +1496,18 @@ class ARSCParser:
# this looks more like we want: (???)
# FIXME it looks like that the two string pools we have read might not be concatenated to each other,
# thus jumping to the sum of the sizes might not be correct...
next_idx = res_header.start + res_header.header_size + type_sp_header.size + key_sp_header.size
next_idx = res_header.start + res_header.header_size + \
type_sp_header.size + key_sp_header.size
if next_idx != self.buff.tell():
# If this happens, we have a testfile ;)
logger.error("This looks like an odd resources.arsc file!")
logger.error("Please report this error including the file you have parsed!")
logger.error("next_idx = {}, current buffer position = {}".format(next_idx, self.buff.tell()))
logger.error("Please open a issue at https://github.com/androguard/androguard/issues")
logger.error(
"Please report this error including the file you have parsed!")
logger.error("next_idx = {}, current buffer position = {}".format(
next_idx, self.buff.tell()))
logger.error(
"Please open a issue at https://github.com/androguard/androguard/issues")
logger.error("Thank you!")
self.buff.seek(next_idx)
@@ -1473,21 +1523,24 @@ class ARSCParser:
self.packages[package_name].append(pkg_chunk_header)
if pkg_chunk_header.type == RES_TABLE_TYPE_SPEC_TYPE:
self.packages[package_name].append(ARSCResTypeSpec(self.buff, pc))
self.packages[package_name].append(
ARSCResTypeSpec(self.buff, pc))
elif pkg_chunk_header.type == RES_TABLE_TYPE_TYPE:
# Parse a RES_TABLE_TYPE
# http://androidxref.com/9.0.0_r3/xref/frameworks/base/tools/aapt2/format/binary/BinaryResourceParser.cpp#311
a_res_type = ARSCResType(self.buff, pc)
self.packages[package_name].append(a_res_type)
self.resource_configs[package_name][a_res_type].add(a_res_type.config)
self.resource_configs[package_name][a_res_type].add(
a_res_type.config)
logger.debug("Config: {}".format(a_res_type.config))
entries = []
for i in range(0, a_res_type.entryCount):
current_package.mResId = current_package.mResId & 0xffff0000 | i
entries.append((unpack('<i', self.buff.read(4))[0], current_package.mResId))
entries.append((unpack('<i', self.buff.read(4))[
0], current_package.mResId))
self.packages[package_name].append(entries)
@@ -1508,7 +1561,8 @@ class ARSCParser:
logger.warning("RES_TABLE_LIBRARY_TYPE chunk is not supported")
else:
# Unknown / not-handled chunk type
logger.warning("Unknown chunk type encountered inside RES_TABLE_PACKAGE: %s", pkg_chunk_header)
logger.warning(
"Unknown chunk type encountered inside RES_TABLE_PACKAGE: %s", pkg_chunk_header)
# skip to the next chunk
self.buff.seek(pkg_chunk_header.end)
@@ -1537,7 +1591,8 @@ class ARSCParser:
locale = a_res_type.config.get_language_and_region()
c_value = self.values[package_name].setdefault(locale, {"public": []})
c_value = self.values[package_name].setdefault(
locale, {"public": []})
entries = self.packages[package_name][nb + 2]
nb_i = 0
@@ -1546,7 +1601,8 @@ class ARSCParser:
ate = self.packages[package_name][nb + 3 + nb_i]
self.resource_values[ate.mResId][a_res_type.config] = ate
self.resource_keys[package_name][a_res_type.get_type()][ate.get_value()] = ate.mResId
self.resource_keys[package_name][a_res_type.get_type(
)][ate.get_value()] = ate.mResId
if ate.get_index() != -1:
c_value["public"].append(
@@ -1738,7 +1794,8 @@ class ARSCParser:
buff += '<resources>\n'
try:
for i in self.values[package_name][locale]["string"]:
buff += '<string name="{}">{}</string>\n'.format(i[0], escape(i[1]))
buff += '<string name="{}">{}</string>\n'.format(
i[0], escape(i[1]))
except KeyError:
pass
@@ -1772,7 +1829,7 @@ class ARSCParser:
buff += '<item type="id" name="%s"/>\n' % (i[0])
else:
buff += '<item type="id" name="{}">{}</item>\n'.format(i[0],
escape(i[1]))
escape(i[1]))
except KeyError:
pass
@@ -1905,6 +1962,7 @@ class ARSCParser:
Resolves resources by ID and configuration.
This resolver deals with complex resources as well as with references.
"""
def __init__(self, android_resources, config=None):
"""
:param ARSCParser android_resources: A resource parser
@@ -1965,10 +2023,12 @@ class ARSCParser:
# Infinite loop detection:
# TODO should this stay here or should be detect the loop much earlier?
if res_id == parent.mResId:
logger.warning("Infinite loop detected at resource item {}. It references itself!".format(parent))
logger.warning(
"Infinite loop detected at resource item {}. It references itself!".format(parent))
return
self._resolve_into_result(result, item.get_data(), self.wanted_config)
self._resolve_into_result(
result, item.get_data(), self.wanted_config)
else:
if complex_:
result.append(item.format_value())
@@ -2054,7 +2114,8 @@ class ARSCParser:
raise ValueError("'rid' must be an int")
if rid not in self.resource_values:
logger.warning("The requested rid '0x{:08x}' could not be found in the list of resources.".format(rid))
logger.warning(
"The requested rid '0x{:08x}' could not be found in the list of resources.".format(rid))
return []
res_options = self.resource_values[rid]
@@ -2062,7 +2123,8 @@ class ARSCParser:
if config in res_options:
return [(config, res_options[config])]
elif fallback and config == ARSCResTableConfig.default_config():
logger.warning("No default resource config could be found for the given rid '0x{:08x}', using fallback!".format(rid))
logger.warning(
"No default resource config could be found for the given rid '0x{:08x}', using fallback!".format(rid))
return [list(self.resource_values[rid].items())[0]]
else:
return []
@@ -2096,7 +2158,7 @@ class ARSCParser:
for res_type, configs in list(self.resource_configs[package_name].items()):
if res_type.get_package_name() == package_name and (
type_name is None or res_type.get_type() == type_name):
type_name is None or res_type.get_type() == type_name):
result[res_type.get_type()].extend(configs)
return result
@@ -2116,7 +2178,8 @@ class ARSCParser:
"""
if not name.startswith('@'):
raise ValueError("Not a valid resource ID, must start with @: '{}'".format(name))
raise ValueError(
"Not a valid resource ID, must start with @: '{}'".format(name))
# remove @
name = name[1:]
@@ -2128,7 +2191,8 @@ class ARSCParser:
res_id = name
if len(res_id) != 8:
raise ValueError("Numerical ID is not 8 characters long: '{}'".format(res_id))
raise ValueError(
"Numerical ID is not 8 characters long: '{}'".format(res_id))
try:
return int(res_id, 16), package
@@ -2229,13 +2293,15 @@ class ARSCHeader:
self.start = buff.tell()
# Make sure we do not read over the buffer:
if buff.raw.getbuffer().nbytes < self.start + self.SIZE:
raise ResParserError("Can not read over the buffer size! Offset={}".format(self.start))
raise ResParserError(
"Can not read over the buffer size! Offset={}".format(self.start))
# Checking for dummy data between elements
if possible_types:
while True:
cur_pos = buff.tell()
self._type, self._header_size, self._size = unpack('<HHL', buff.read(self.SIZE))
self._type, self._header_size, self._size = unpack(
'<HHL', buff.read(self.SIZE))
# cases where packers set the EndNamespace with zero size: check we are the end and add the prefix + uri
if self._size < self.SIZE and (buff.raw.getbuffer().nbytes == cur_pos + self._header_size + 4 + 4):
@@ -2248,10 +2314,12 @@ class ARSCHeader:
buff.read(1)
logger.warning("Appears that dummy data are found between elements!")
else:
self._type, self._header_size, self._size = unpack('<HHL', buff.read(self.SIZE))
self._type, self._header_size, self._size = unpack(
'<HHL', buff.read(self.SIZE))
if expected_type and self._type != expected_type:
raise ResParserError("Header type is not equal the expected type: Got 0x{:04x}, wanted 0x{:04x}".format(self._type, expected_type))
raise ResParserError("Header type is not equal the expected type: Got 0x{:04x}, wanted 0x{:04x}".format(
self._type, expected_type))
# Assert that the read data will fit into the chunk.
# The total size must be equal or larger than the header size
@@ -2315,6 +2383,7 @@ class ARSCResTablePackage:
See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#861
"""
def __init__(self, buff, header):
self.header = header
self.start = buff.tell()
@@ -2336,6 +2405,7 @@ class ARSCResTypeSpec:
"""
See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#1327
"""
def __init__(self, buff, parent=None):
self.start = buff.tell()
self.parent = parent
@@ -2356,6 +2426,7 @@ class ARSCResTypeSpec:
except Exception as e:
logger.error(e)
class ARSCResType:
"""
This is a `ResTable_type` without it's `ResChunk_header`.
@@ -2363,6 +2434,7 @@ class ARSCResType:
See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#1364
"""
def __init__(self, buff, parent=None):
self.start = buff.tell()
self.parent = parent
@@ -2463,7 +2535,8 @@ class ARSCResTableConfig:
# uint16_t smallestScreenWidthDp
self.screenConfig, = unpack('<I', buff.read(4))
else:
logger.debug("This file does not have a screenConfig! size={}".format(self.size))
logger.debug(
"This file does not have a screenConfig! size={}".format(self.size))
self.screenConfig = 0
if self.size >= 36:
@@ -2472,7 +2545,8 @@ class ARSCResTableConfig:
# uint16_t screenHeightDp
self.screenSizeDp, = unpack('<I', buff.read(4))
else:
logger.debug("This file does not have a screenSizeDp! size={}".format(self.size))
logger.debug(
"This file does not have a screenSizeDp! size={}".format(self.size))
self.screenSizeDp = 0
if self.size >= 40:
@@ -2488,7 +2562,8 @@ class ARSCResTableConfig:
# uint16_t screenConfigPad2
self.screenConfig2, = unpack("<I", buff.read(4))
else:
logger.debug("This file does not have a screenConfig2! size={}".format(self.size))
logger.debug(
"This file does not have a screenConfig2! size={}".format(self.size))
self.screenConfig2 = 0
self.exceedingSize = self.size - (buff.tell() - self.start)
@@ -2562,8 +2637,10 @@ class ARSCResTableConfig:
:return:
"""
if self.locale != 0:
_language = self._unpack_language_or_region([self.locale & 0xff, (self.locale & 0xff00) >> 8, ], ord('a'))
_region = self._unpack_language_or_region([(self.locale & 0xff0000) >> 16, (self.locale & 0xff000000) >> 24, ], ord('0'))
_language = self._unpack_language_or_region(
[self.locale & 0xff, (self.locale & 0xff00) >> 8, ], ord('a'))
_region = self._unpack_language_or_region(
[(self.locale & 0xff0000) >> 16, (self.locale & 0xff000000) >> 24, ], ord('0'))
return (_language + "-r" + _region) if _region else _language
return "\x00\x00"
@@ -2916,6 +2993,7 @@ class ARSCComplex:
See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#1485 for `ResTable_map_entry`
and http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#1498 for `ResTable_map`
"""
def __init__(self, buff, parent=None):
self.start = buff.tell()
self.parent = parent
@@ -2928,7 +3006,8 @@ class ARSCComplex:
# these are structs of ResTable_ref and Res_value
# ResTable_ref is a uint32_t.
for i in range(0, self.count):
self.items.append((unpack('<I', buff.read(4))[0], ARSCResStringPoolRef(buff, self.parent)))
self.items.append((unpack('<I', buff.read(4))[
0], ARSCResStringPoolRef(buff, self.parent)))
def __repr__(self):
return "<ARSCComplex idx='0x{:08x}' parent='{}' count='{}'>".format(self.start, self.id_parent, self.count)
@@ -2941,6 +3020,7 @@ class ARSCResStringPoolRef:
See: http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#262
"""
def __init__(self, buff, parent=None):
self.start = buff.tell()
self.parent = parent
@@ -3013,4 +3093,4 @@ def get_arsc_info(arscobj):
buff += "\t\t\t" + i + "\n"
except AttributeError:
pass
return buff
return buff
@@ -20,7 +20,8 @@ if _public_res is None:
_public_res[_type] = {}
_public_res[_type][_name] = _id
else:
raise Exception("need to copy the sdk/platforms/android-?/data/res/values/public.xml here")
raise Exception(
"need to copy the sdk/platforms/android-?/data/res/values/public.xml here")
SYSTEM_RESOURCES = {
"attributes": {
@@ -65,6 +65,7 @@ class EndOfCentralDirectoryRecord:
"""
A class to provide details about the end of central directory record.
"""
def __init__(self, signature, number_of_this_disk, disk_where_central_directory_starts,
number_of_central_directory_records_on_this_disk,
total_number_of_central_directory_records, size_of_central_directory,
@@ -104,24 +105,29 @@ class EndOfCentralDirectoryRecord:
chunk = apk_file.read(chunk_size)
if not chunk:
break
signature_offset = chunk.rfind(b'\x50\x4b\x05\x06') # end of Central Directory File Header signature
# end of Central Directory File Header signature
signature_offset = chunk.rfind(b'\x50\x4b\x05\x06')
if signature_offset != -1:
eo_central_directory_offset = position + signature_offset
break # Found End of central directory record (EOCD) signature
offset += chunk_size
if signature_offset == -1:
raise ValueError("End of central directory record (EOCD) signature not found")
raise ValueError(
"End of central directory record (EOCD) signature not found")
apk_file.seek(eo_central_directory_offset)
signature = apk_file.read(4)
number_of_this_disk = struct.unpack('<H', apk_file.read(2))[0]
disk_where_central_directory_starts = struct.unpack('<H', apk_file.read(2))[0]
number_of_central_directory_records_on_this_disk = struct.unpack('<H', apk_file.read(2))[0]
total_number_of_central_directory_records = struct.unpack('<H', apk_file.read(2))[0]
number_of_central_directory_records_on_this_disk = struct.unpack('<H', apk_file.read(2))[
0]
total_number_of_central_directory_records = struct.unpack('<H', apk_file.read(2))[
0]
size_of_central_directory = struct.unpack('<I', apk_file.read(4))[0]
offset_of_start_of_central_directory = struct.unpack('<I', apk_file.read(4))[0]
comment_length = struct.unpack('<H', apk_file.read(2))[0]
comment = struct.unpack(f'<{comment_length}s', apk_file.read(comment_length))[0].decode('utf-8', 'ignore')
comment = struct.unpack(f'<{comment_length}s', apk_file.read(comment_length))[
0].decode('utf-8', 'ignore')
return cls(
signature,
number_of_this_disk,
@@ -170,6 +176,7 @@ class CentralDirectoryEntry:
"""
A class representing each entry in the central directory.
"""
def __init__(self, version_made_by, version_needed_to_extract, general_purpose_bit_flag,
compression_method, file_last_modification_time, file_last_modification_date,
crc32_of_uncompressed_data, compressed_size, uncompressed_size, file_name_length,
@@ -245,6 +252,7 @@ class CentralDirectory:
The CentralDirectory containing all the CentralDirectoryEntry entries discovered.
The entries are listed as a dictionary where the filename is the key.
"""
def __init__(self, entries):
self.entries = entries
@@ -266,7 +274,8 @@ class CentralDirectory:
eocd = EndOfCentralDirectoryRecord.parse(apk_file)
apk_file.seek(eocd.offset_of_start_of_central_directory)
if apk_file.tell() != eocd.offset_of_start_of_central_directory:
raise ValueError(f"Failed to find the offset for the central directory within the file!")
raise ValueError(
f"Failed to find the offset for the central directory within the file!")
central_directory_entries = {}
while True:
@@ -289,8 +298,10 @@ class CentralDirectory:
disk_number_where_file_starts = struct.unpack('<H', apk_file.read(2))[0]
internal_file_attributes = struct.unpack('<H', apk_file.read(2))[0]
external_file_attributes = struct.unpack('<I', apk_file.read(4))[0]
relative_offset_of_local_file_header = struct.unpack('<I', apk_file.read(4))[0]
filename = struct.unpack(f'<{file_name_length}s', apk_file.read(file_name_length))[0].decode('utf-8')
relative_offset_of_local_file_header = struct.unpack('<I', apk_file.read(4))[
0]
filename = struct.unpack(f'<{file_name_length}s', apk_file.read(file_name_length))[
0].decode('utf-8')
extra_field = struct.unpack(f'<{extra_field_length}s', apk_file.read(extra_field_length))[0].decode('utf-8',
'ignore')
file_comment = struct.unpack(f'<{file_comment_length}s', apk_file.read(file_comment_length))[0].decode(
@@ -339,6 +350,7 @@ class LocalHeaderRecord:
"""
The local header for each entry discovered.
"""
def __init__(self, version_needed_to_extract, general_purpose_bit_flag,
compression_method, file_last_modification_time, file_last_modification_date,
crc32_of_uncompressed_data, compressed_size, uncompressed_size, file_name_length,
@@ -386,7 +398,8 @@ class LocalHeaderRecord:
uncompressed_size = struct.unpack('<I', apk_file.read(4))[0]
file_name_length = struct.unpack('<H', apk_file.read(2))[0]
extra_field_length = struct.unpack('<H', apk_file.read(2))[0]
filename = struct.unpack(f'<{file_name_length}s', apk_file.read(file_name_length))[0].decode('utf-8')
filename = struct.unpack(f'<{file_name_length}s', apk_file.read(file_name_length))[
0].decode('utf-8')
extra_field = struct.unpack(f'<{extra_field_length}s', apk_file.read(extra_field_length))[0].decode('utf-8',
'ignore')
return cls(
@@ -434,6 +447,7 @@ class ZipEntry:
"""
Is the actual APK represented as a composition of the previous classes, which are: the EndOfCentralDirectoryRecord, the CentralDirectory and a dictionary of values of LocalHeaderRecord.
"""
def __init__(self, zip_bytes, eocd: EndOfCentralDirectoryRecord, central_directory: CentralDirectory,
local_headers: Dict[str, LocalHeaderRecord]):
self.zip = zip_bytes
@@ -462,7 +476,8 @@ class ZipEntry:
central_directory = CentralDirectory.parse(apk_file, eocd)
local_headers = {}
for entry in central_directory.entries:
local_header_entry = LocalHeaderRecord.parse(apk_file, central_directory.entries[entry])
local_header_entry = LocalHeaderRecord.parse(
apk_file, central_directory.entries[entry])
local_headers[local_header_entry.filename] = local_header_entry
return cls(apk_file, eocd, central_directory, local_headers)
@@ -486,7 +501,8 @@ class ZipEntry:
if not eocd or not central_directory:
eocd = EndOfCentralDirectoryRecord.parse(apk_file)
central_directory = CentralDirectory.parse(apk_file, eocd)
local_header = {filename: LocalHeaderRecord.parse(apk_file, central_directory.entries[filename])}
local_header = {filename: LocalHeaderRecord.parse(
apk_file, central_directory.entries[filename])}
return cls(apk_file, eocd, central_directory, local_header)
def to_dict(self):
@@ -514,7 +530,8 @@ class ZipEntry:
if filename in self.central_directory.entries:
return self.central_directory.entries[filename].to_dict()
else:
raise KeyError(f"Key: {filename} was not found within the central directory entries!")
raise KeyError(
f"Key: {filename} was not found within the central directory entries!")
def get_local_header_dict(self, filename):
"""
@@ -528,7 +545,8 @@ class ZipEntry:
if filename in self.local_headers:
return self.local_headers[filename].to_dict()
else:
raise KeyError(f"Key: {filename} was not found within the local headers list!")
raise KeyError(
f"Key: {filename} was not found within the local headers list!")
def read(self, name, save: bool = False):
"""
@@ -14,11 +14,13 @@ from mobsf.MobSF.utils import (
print_n_send_error_response,
)
from mobsf.StaticAnalyzer.views.common.shared_func import (
firebase_analysis,
get_avg_cvss,
hash_gen,
unzip,
)
from mobsf.StaticAnalyzer.views.common.firebase import (
firebase_analysis,
)
from mobsf.StaticAnalyzer.views.common.appsec import (
get_android_dashboard,
)
@@ -215,7 +217,7 @@ def common_analysis(request, app_dic, rescan, api, analysis_type):
# Firebase DB Check
code_an_dic['firebase'] = firebase_analysis(
checksum,
code_an_dic['urls_list'])
code_an_dic)
# Domain Extraction and Malware Check
code_an_dic['domains'] = MalwareDomainCheck().scan(
checksum,
@@ -79,6 +79,7 @@ def assetlinks_check(act_name, well_knowns):
return findings
def _check_url(host, w_url):
try:
iden = 'sha256_cert_fingerprints'
@@ -87,10 +88,10 @@ def _check_url(host, w_url):
status_code = 0
r = requests.get(w_url,
allow_redirects=False,
proxies=proxies,
verify=verify,
timeout=5)
allow_redirects=False,
proxies=proxies,
verify=verify,
timeout=5)
status_code = r.status_code
if status_code == 302:
@@ -106,7 +107,7 @@ def _check_url(host, w_url):
except Exception:
logger.error(f'Well Known Assetlinks Check for URL: {w_url}')
return {'url': w_url,
return {'url': w_url,
'host': host,
'status_code': None,
'status': False}
@@ -153,9 +154,9 @@ def get_browsable_activities(node, ns):
path_patterns.append(path_pattern)
# Collect possible well-known paths
if (scheme
and scheme in ('http', 'https')
and host
and host != '*'):
and scheme in ('http', 'https')
and host
and host != '*'):
host = host.replace('*.', '').replace('#', '')
if not valid_host(host):
continue
+4 -2
View File
@@ -12,10 +12,12 @@ from mobsf.MobSF.utils import (
print_n_send_error_response,
)
from mobsf.StaticAnalyzer.views.common.shared_func import (
firebase_analysis,
get_symbols,
hash_gen,
)
from mobsf.StaticAnalyzer.views.common.firebase import (
firebase_analysis,
)
from mobsf.StaticAnalyzer.views.common.binary.lib_analysis import (
library_analysis,
)
@@ -134,7 +136,7 @@ def so_analysis(request, app_dic, rescan, api):
# Firebase DB Check
code_an_dic['firebase'] = firebase_analysis(
checksum,
code_an_dic['urls_list'])
code_an_dic)
# Domain Extraction and Malware Check
code_an_dic['domains'] = MalwareDomainCheck().scan(
checksum,
@@ -84,11 +84,13 @@ from mobsf.StaticAnalyzer.views.android.so import (
so_analysis,
)
from mobsf.StaticAnalyzer.views.common.shared_func import (
firebase_analysis,
get_avg_cvss,
hash_gen,
unzip,
)
from mobsf.StaticAnalyzer.views.common.firebase import (
firebase_analysis,
)
from mobsf.StaticAnalyzer.views.common.appsec import (
get_android_dashboard,
)
@@ -313,7 +315,7 @@ def static_analyzer(request, checksum, api=False):
# Firebase DB Check
code_an_dic['firebase'] = firebase_analysis(
checksum,
code_an_dic['urls_list'])
code_an_dic)
# Domain Extraction and Malware Check
code_an_dic['domains'] = MalwareDomainCheck().scan(
checksum,
@@ -492,7 +494,7 @@ def static_analyzer(request, checksum, api=False):
# Firebase DB Check
code_an_dic['firebase'] = firebase_analysis(
checksum,
code_an_dic['urls_list'])
code_an_dic)
# Domain Extraction and Malware Check
code_an_dic['domains'] = MalwareDomainCheck().scan(
checksum,
@@ -12,6 +12,8 @@ from mobsf.StaticAnalyzer.views.common.entropy import (
get_entropies,
)
from mobsf.MobSF.utils import (
GOOGLE_API_KEY_REGEX,
GOOGLE_APP_ID_REGEX,
append_scan_status,
get_android_src_dir,
)
@@ -55,6 +57,7 @@ def strings_from_apk(checksum, apk):
urls = []
urls_nf = []
emails_nf = []
firebase_creds = {}
try:
msg = 'Extracting String data from APK'
logger.info(msg)
@@ -68,6 +71,12 @@ def strings_from_apk(checksum, apk):
if not res_string:
continue
for duo in res_string:
if (duo[0] == 'google_api_key'
and GOOGLE_API_KEY_REGEX.match(duo[1])):
firebase_creds['google_api_key'] = duo[1]
if (duo[0] == 'google_app_id'
and GOOGLE_APP_ID_REGEX.match(duo[1])):
firebase_creds['google_app_id'] = duo[1]
cap_str = '"' + duo[0] + '" : "' + duo[1] + '"'
# Extract possible secret holding keys
if is_secret_key(duo[0] + '"') and ' ' not in duo[1]:
@@ -86,6 +95,7 @@ def strings_from_apk(checksum, apk):
'urls_nf': urls_nf,
'emails_nf': emails_nf,
'secrets': secrets,
'firebase_creds': firebase_creds,
}
@@ -123,6 +133,7 @@ def get_strings_metadata(
if apk:
# APK
apk_res = strings_from_apk(checksum, apk)
code_dic['firebase_creds'] = apk_res['firebase_creds']
strings['strings_apk_res'] = apk_res['strings']
urls_list.extend(apk_res['urls_list'])
urls_n_files.extend(apk_res['urls_nf'])
+4 -2
View File
@@ -31,10 +31,12 @@ from mobsf.StaticAnalyzer.views.ios.strings import (
)
from mobsf.StaticAnalyzer.views.common.shared_func import (
ar_extract,
firebase_analysis,
get_symbols,
hash_gen,
)
from mobsf.StaticAnalyzer.views.common.firebase import (
firebase_analysis,
)
from mobsf.MalwareAnalyzer.views.MalwareDomainCheck import (
MalwareDomainCheck,
)
@@ -166,7 +168,7 @@ def a_analysis(request, app_dict, rescan, api):
code_dict['code_anal'] = {}
code_dict['firebase'] = firebase_analysis(
checksum,
code_dict['urls_list'])
code_dict)
code_dict['trackers'] = trackers
context = save_get_ctx(
app_dict,
+5 -9
View File
@@ -123,15 +123,11 @@ def common_fields(findings, data):
})
# Firebase
for fb in data['firebase_urls']:
if fb['open']:
fdb = fb['url']
findings['high'].append({
'title': 'Firebase DB is exposed publicly.',
'description': (
f'The Firebase database at {fdb} is exposed'
' to internet without any authentication'),
'section': 'firebase',
})
findings[fb['severity']].append({
'title': fb['title'],
'description': fb['description'],
'section': 'firebase',
})
# Trackers
if 'trackers' in data['trackers']:
findings['total_trackers'] = data['trackers']['total_trackers']
@@ -8,6 +8,11 @@ import lief
from mobsf.StaticAnalyzer.views.common.binary.strings import (
strings_on_binary,
)
from mobsf.MobSF.utils import (
run_with_timeout,
)
from django.conf import settings
NA = 'Not Applicable'
@@ -32,7 +37,10 @@ class ELFChecksec:
def __init__(self, elf_file, so_rel):
self.elf_path = elf_file.as_posix()
self.elf_rel = so_rel
self.elf = lief.parse(self.elf_path)
self.elf = run_with_timeout(
lief.parse,
settings.BINARY_ANALYSIS_TIMEOUT,
self.elf_path)
def checksec(self):
elf_dict = {}
@@ -27,6 +27,9 @@ def library_analysis(checksum, src, arch):
f'{arch}_analysis': [],
f'{arch}_strings': [],
f'{arch}_symbols': [],
'framework_analysis': [],
'framework_strings': [],
'framework_symbols': [],
}
try:
if arch == 'macho':
@@ -79,9 +82,6 @@ def library_analysis(checksum, src, arch):
rel_path: symbols})
if ext == '*.dylib':
# Do Framework Analysis for iOS
res['framework_analysis'] = []
res['framework_strings'] = []
res['framework_symbols'] = []
frameworks_analysis(checksum, src, base_dir, res)
if res['framework_strings']:
res[f'{arch}_strings'].extend(
@@ -9,6 +9,11 @@ import lief
from mobsf.StaticAnalyzer.views.common.binary.strings import (
strings_on_binary,
)
from mobsf.MobSF.utils import (
run_with_timeout,
)
from django.conf import settings
def objdump_is_debug_symbol_stripped(macho_file):
@@ -28,7 +33,10 @@ class MachOChecksec:
self.macho_name = rel_path
else:
self.macho_name = macho.name
self.macho = lief.parse(self.macho_path)
self.macho = run_with_timeout(
lief.parse,
settings.BINARY_ANALYSIS_TIMEOUT,
self.macho_path)
def checksec(self):
macho_dict = {}
@@ -0,0 +1,174 @@
"""Firebase Analysis."""
import logging
from urllib.parse import urlparse
from mobsf.MobSF.utils import (
append_scan_status,
upstream_proxy,
valid_host,
)
import requests
logger = logging.getLogger(__name__)
HIGH = 'high'
WARNING = 'warning'
INFO = 'info'
SECURE = 'secure'
FIREBASE_FINDINGS = {
'firebase_db_open': {
'title': 'Open Firebase database',
'severity': HIGH,
'description': (
'The Firebase database at %s is exposed'
' to internet without any authentication'),
},
'firebase_db_exists': {
'title': 'App talks to a Firebase database',
'severity': INFO,
'description': 'The app talks to Firebase database at %s',
},
'firebase_remote_config_enabled': {
'title': 'Firebase Remote Config enabled',
'severity': WARNING,
'description': (
'The Firebase Remote Config at %s is enabled.'
' Ensure that the configurations are not sensitive.'
' This is indicated by the response:\n\n%s'),
},
'firebase_remote_config_disabled': {
'title': 'Firebase Remote Config disabled',
'severity': SECURE,
'description': (
'Firebase Remote Config is disabled for %s.',
' This is indicated by the response:\n\n%s'),
},
'firebase_remote_config_failed': {
'title': 'Firebase Remote Config check failed',
'description': (
'Failed to check for Firebase Remote Config.'
' Please verify this manually. Error:\n\n%s'),
'severity': INFO,
},
}
def firebase_analysis(checksum, code_an_dic):
"""Firebase Analysis."""
urls = list(set(code_an_dic['urls_list']))
findings = []
finds = None
logger.info('Starting Firebase Analysis')
# Check for Firebase Database
logger.info('Looking for Firebase URL(s)')
for url in urls:
if 'firebaseio.com' not in url:
continue
returl, is_open = open_firebase(checksum, url)
if is_open:
item = FIREBASE_FINDINGS['firebase_db_open']
item['description'] = item['description'] % returl
findings.append(item)
else:
item = FIREBASE_FINDINGS['firebase_db_exists']
item['description'] = item['description'] % returl
findings.append(item)
# Check for Firebase Remote Config
firebase_creds = code_an_dic.get('firebase_creds')
finds = firebase_remote_config(checksum, firebase_creds)
if finds:
findings.extend(finds)
return findings
def open_firebase(checksum, url):
# Detect Open Firebase Database
try:
invalid = 'Invalid Firebase URL'
if not valid_host(url):
logger.warning(invalid)
return url, False
purl = urlparse(url)
if not purl.netloc.endswith('firebaseio.com'):
logger.warning(invalid)
return url, False
base_url = '{}://{}/.json'.format(purl.scheme, purl.netloc)
proxies, verify = upstream_proxy('https')
headers = {
'User-Agent': ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1)'
' AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/39.0.2171.95 Safari/537.36')}
resp = requests.get(base_url, headers=headers,
proxies=proxies, verify=verify,
allow_redirects=False)
if resp.status_code == 200:
return base_url, True
except Exception as exp:
msg = 'Open Firebase DB detection failed'
logger.warning(msg)
append_scan_status(checksum, msg, repr(exp))
return url, False
def firebase_remote_config(checksum, creds):
"""Check for Firebase Remote Config."""
url = None
findings = []
try:
if not creds:
return None
google_api_key = creds.get('google_api_key')
google_app_id = creds.get('google_app_id')
if not (google_app_id and google_api_key):
return None
project_id = google_app_id.split(':')[1]
if not project_id.isnumeric():
return None
# https://docs.emergetools.com/docs/firebase-remote-config-api-exposed
# https://blog.deesee.xyz/android/automation/2019/08/03/
# firebase-remote-config-dump.html
logger.info('Checking for Firebase Remote Config')
url = (f'https://firebaseremoteconfig.googleapis.com/v1/projects/'
f'{project_id}/namespaces/firebase:fetch?key={google_api_key}')
body = {
'appId': google_app_id,
'appInstanceId': 'required_but_unused_value',
}
proxies, verify = upstream_proxy('https')
response = requests.post(
url,
timeout=4,
json=body,
proxies=proxies,
verify=verify,
allow_redirects=False)
if response.status_code == 200:
resp = response.json()
if resp.get('state') == 'NO_TEMPLATE':
item = FIREBASE_FINDINGS['firebase_remote_config_disabled']
item['description'] = item['description'] % (url, resp)
findings.append(item)
else:
item = FIREBASE_FINDINGS['firebase_remote_config_enabled']
item['description'] = item['description'] % (url, resp)
findings.append(item)
else:
item = FIREBASE_FINDINGS['firebase_remote_config_disabled']
response_msg = f'The response code is {response.status_code}'
item['description'] = item['description'] % (url, response_msg)
findings.append(item)
except Exception as exp:
msg = 'Failed to check for Firebase Remote Config'
logger.warning(msg)
append_scan_status(checksum, msg, repr(exp))
item = FIREBASE_FINDINGS['firebase_remote_config_failed']
item['description'] = item['description'] % repr(exp)
findings.append(item)
return findings
@@ -13,11 +13,8 @@ import re
import shutil
import subprocess
import zipfile
from urllib.parse import urlparse
from pathlib import Path
import requests
import arpy
from django.utils.html import escape
@@ -33,8 +30,6 @@ from mobsf.MobSF.utils import (
is_path_traversal,
is_safe_path,
print_n_send_error_response,
upstream_proxy,
valid_host,
)
from mobsf.MobSF.views.scanning import (
add_to_recent_scan,
@@ -291,49 +286,6 @@ def get_avg_cvss(findings):
return avg_cvss
def open_firebase(checksum, url):
# Detect Open Firebase Database
try:
invalid = 'Invalid Firebase URL'
if not valid_host(url):
logger.warning(invalid)
return url, False
purl = urlparse(url)
if not purl.netloc.endswith('firebaseio.com'):
logger.warning(invalid)
return url, False
base_url = '{}://{}/.json'.format(purl.scheme, purl.netloc)
proxies, verify = upstream_proxy('https')
headers = {
'User-Agent': ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1)'
' AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/39.0.2171.95 Safari/537.36')}
resp = requests.get(base_url, headers=headers,
proxies=proxies, verify=verify,
allow_redirects=False)
if resp.status_code == 200:
return base_url, True
except Exception as exp:
msg = 'Open Firebase DB detection failed'
logger.warning(msg)
append_scan_status(checksum, msg, repr(exp))
return url, False
def firebase_analysis(checksum, urls):
# Detect Firebase URL
firebase_db = []
logger.info('Detecting Firebase URL(s)')
for url in urls:
if 'firebaseio.com' not in url:
continue
returl, is_open = open_firebase(checksum, url)
fbdic = {'url': returl, 'open': is_open}
if fbdic not in firebase_db:
firebase_db.append(fbdic)
return firebase_db
def find_java_source_folder(base_folder: Path):
# Find the correct java/kotlin source folder for APK/source zip
# Returns a Tuple of - (SRC_PATH, SRC_TYPE, SRC_SYNTAX)
@@ -56,20 +56,23 @@ def get_bin_info(bin_file):
def ipa_macho_analysis(binary):
data = {
'checksec': {},
'symbols': [],
'libraries': [],
}
try:
logger.info('Running MachO Analysis on: %s', binary.name)
cs = MachOChecksec(binary)
chksec = cs.checksec()
symbols = cs.get_symbols()
libs = cs.get_libraries()
return {
'checksec': chksec,
'symbols': symbols,
'libraries': libs,
}
data['checksec'] = chksec
data['symbols'] = symbols
data['libraries'] = libs
except Exception:
logger.exception('Running MachO Analysis')
return {}
return data
def binary_analysis(checksum, src, tools_dir, app_dir, executable_name):
+4 -2
View File
@@ -32,10 +32,12 @@ from mobsf.StaticAnalyzer.views.ios.strings import (
get_strings_metadata,
)
from mobsf.StaticAnalyzer.views.common.shared_func import (
firebase_analysis,
get_symbols,
hash_gen,
)
from mobsf.StaticAnalyzer.views.common.firebase import (
firebase_analysis,
)
from mobsf.MalwareAnalyzer.views.MalwareDomainCheck import (
MalwareDomainCheck,
)
@@ -148,7 +150,7 @@ def dylib_analysis(request, app_dict, rescan, api):
code_dict['code_anal'] = {}
code_dict['firebase'] = firebase_analysis(
checksum,
code_dict['urls_list'])
code_dict)
code_dict['trackers'] = trackers
context = save_get_ctx(
app_dict,
@@ -50,12 +50,14 @@ from mobsf.StaticAnalyzer.views.common.a import (
a_analysis,
)
from mobsf.StaticAnalyzer.views.common.shared_func import (
firebase_analysis,
get_avg_cvss,
hash_gen,
strings_and_entropies,
unzip,
)
from mobsf.StaticAnalyzer.views.common.firebase import (
firebase_analysis,
)
from mobsf.StaticAnalyzer.views.common.appsec import (
get_ios_dashboard,
)
@@ -225,7 +227,7 @@ def static_analyzer_ios(request, checksum, api=False):
code_dict['code_anal'] = {}
code_dict['firebase'] = firebase_analysis(
checksum,
code_dict['urls_list'])
code_dict)
code_dict['trackers'] = trackers
context = save_get_ctx(
app_dict,
@@ -303,7 +305,7 @@ def static_analyzer_ios(request, checksum, api=False):
# Firebase DB Check
code_analysis_dic['firebase'] = firebase_analysis(
checksum,
list(set(code_analysis_dic['urls_list'])))
code_analysis_dic)
# Extract Trackers from Domains
trk = Trackers.Trackers(
checksum,
+7 -4
View File
@@ -4,8 +4,11 @@ import logging
from libsast import Scanner
from django.conf import settings
from mobsf.MobSF.utils import (
append_scan_status,
run_with_timeout,
settings_enabled,
)
@@ -21,8 +24,8 @@ def scan(checksum, rule, extensions, paths, ignore_paths=None):
'ignore_paths': ignore_paths,
'show_progress': False}
scanner = Scanner(options, paths)
res = scanner.scan()
if res:
res = run_with_timeout(scanner.scan, settings.SAST_TIMEOUT)
if res and res.get('pattern_matcher'):
return format_findings(res['pattern_matcher'], paths[0])
except Exception as exp:
msg = 'libsast scan failed'
@@ -48,8 +51,8 @@ def niap_scan(checksum, rule, extensions, paths, apath, ignore_paths=None):
'ignore_paths': ignore_paths,
'show_progress': False}
scanner = Scanner(options, paths)
res = scanner.scan()
if res:
res = run_with_timeout(scanner.scan, settings.SAST_TIMEOUT)
if res and res.get('choice_matcher'):
return res['choice_matcher']
except Exception as exp:
msg = 'NIAP Analyzer Failed'
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -40,7 +40,7 @@
<!-- SEARCH FORM -->
<form class="form-inline ml-3" action="/search" method="GET">
<div class="input-group input-group-sm">
<input class="form-control form-control-navbar" id="navbar-search-input" type="search" placeholder="Search MD5" name="md5" aria-label="Search">
<input class="form-control form-control-navbar" id="navbar-search-input" type="search" placeholder="Search" name="query" aria-label="Search">
<div class="input-group-append">
<button class="btn btn-navbar" type="submit">
<i class="fas fa-search"></i>
+1 -1
View File
@@ -31,7 +31,7 @@
<!-- SEARCH FORM -->
<form class="form-inline ml-3" action="/search" method="GET">
<div class="input-group input-group-sm">
<input class="form-control form-control-navbar" id="navbar-search-input" type="search" placeholder="Search MD5" name="md5" aria-label="Search">
<input class="form-control form-control-navbar" id="navbar-search-input" type="search" placeholder="Search" name="query" aria-label="Search">
<div class="input-group-append">
<button class="btn btn-navbar" type="submit">
<i class="fas fa-search"></i>
+82
View File
@@ -30,6 +30,7 @@
<li><code>api/v1/upload</code> - <a href="#upload-file-api">Upload a File</a></li>
<li><code>api/v1/scan</code> - <a href="#scan-file-api">Scan a File</a></li>
<li><code>api/v1/scan_logs</code> - <a href="#scan-logs-api">Display Live Scan Logs</a></li>
<li><code>api/v1/search</code> - <a href="#search-api">Search a Scan</a></li>
<li><code>api/v1/scans</code> - <a href="#recent-scans-api">Display Recent Scans</a></li>
<li><code>api/v1/delete_scan</code> - <a href="#delete-scan-api">Delete a Scan</a></li>
<li><code>api/v1/scorecard</code> - <a href="#scorecard-api">App Scorecard</a></li>
@@ -358,6 +359,87 @@
</li>
</ul>
<hr />
<h2><a id="search-api"></a><strong>Search API</strong></h2>
<p>API for querying scan results. You can search using an MD5 checksum, app name, package name, or file name. The API returns the closest match based on your search term.</p>
<ul>
<li>
<p><strong>URL:</strong> <code>/api/v1/search</code></p>
</li>
<li>
<p><strong>Method:</strong> <code>POST</code></p>
</li>
<li>
<p><strong>Header:</strong> <code>Authorization: &lt;api_key&gt;</code> <strong>Or</strong> <code>X-Mobsf-Api-Key: &lt;api_key&gt;</code></p>
</li>
<li>
<p><strong>Data Params</strong></p>
</li>
</ul>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Param Name</th>
<th>Param Value</th>
<th>Required</th>
</tr>
</thead>
<tbody>
<tr>
<td>query</td>
<td>hash of the scan or text</td>
<td>Yes</td>
</tr>
</tbody>
</table>
<br />
<ul>
<li>
<p><strong>Success Response:</strong></p>
<ul>
<li>
<strong>Code:</strong> <code>200</code><br />
<strong>Content-Type:</strong> <code>application/json; charset=utf-8</code> <br />
<strong>Content:</strong> <code>JSON Contents</code>
</li>
</ul>
</li>
<li>
<p><strong>Error Response:</strong></p>
<ul>
<li>
<strong>Code:</strong> <code>404 Not Found</code> or <code>405 Method Not Allowed</code> or <code>422 Unprocessable Entity</code><br />
<strong>Content-Type:</strong> <code>application/json; charset=utf-8</code><br />
<strong>Content:</strong> <code>{&quot;error&quot;: &lt;You can search by MD5, app name, package name, or file name.&gt; }</code>
</li>
</ul>
<p>OR</p>
<ul>
<li>
<strong>Code:</strong> <code>401 Unauthorized</code><br />
<strong>Content-Type:</strong> <code>application/json; charset=utf-8</code><br />
<strong>Content:</strong> <code>{&quot;error&quot;: &quot;You are unauthorized to make this request.&quot; }</code>
</li>
</ul>
</li>
<li>
<p><strong>Sample Call:</strong></p>
<ul>
<li>
<pre><code>curl -X POST --url http://localhost:8000/api/v1/search --data &quot;query=com.foo.bar&quot; -H &quot;Authorization: {{ api_key}}&quot;
</code></pre>
</li>
</ul>
<p>OR</p>
<ul>
<li>
<pre><code>curl -X POST --url http://localhost:8000/api/v1/search --data &quot;query=82ab8b2193b3cfb1c737e3a786be363a&quot; -H &quot;X-Mobsf-Api-Key: {{ api_key}}&quot;
</code></pre>
</li>
</ul>
</li>
</ul>
<hr />
<h2><a id="delete-scan-api"></a><strong>Delete Scan API</strong></h2>
<p>API to delete scan results.</p>
<ul>
+1 -1
View File
@@ -40,7 +40,7 @@
{% endif %}
<a class="nav-link">
<form action="/search" method="GET">
<input name="md5" type="text" class="form-control form-control-sm" placeholder="Search MD5">
<input name="query" type="text" class="form-control form-control-sm" placeholder="Search">
</form>
</a>
</nav>
-22
View File
@@ -1,22 +0,0 @@
{% extends "base/base_layout.html" %}
{% block sidebar_option %}
sidebar-collapse
{% endblock %}
{% block content %}
<div class="content-wrapper">
<div class="content-header">
</div>
<div class="container-fluid">
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="card-body">
<h3>Scan hash not found!</h3>
<p class="lead">We couldn't find a scan matching that MD5 hash.</p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
+32 -31
View File
@@ -749,6 +749,38 @@
{% endif %}
-->
{% if firebase_urls %}
<h2><i class="fas fa-database"></i> FIREBASE DATABASES ANALYSIS</h2>
<table class="basic">
<thead>
<tr>
<th>TITLE</th>
<th>SEVERITY</th>
<th>DESCRIPTION</th>
</tr>
</thead>
<tbody>
{% for find in firebase_urls %}
<tr>
<td>{{ find.title }}</td>
<td>
{% if find.severity == 'high' %}
<span class="danger">high</span>
{% elif find.severity == 'secure' %}
<span class="success">secure</span>
{% elif find.severity == 'warning' %}
<span class="warning">warning</span>
{% elif find.severity == 'info' %}
<span class="info">info</span>
{% endif %}
</td>
<td>{{ find.description }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if malware_permissions %}
<h2><i class="fa fa-braille"></i> ABUSED PERMISSIONS</h2>
@@ -884,37 +916,6 @@
</table>
{% endif %}
{% endif %}
{% if firebase_urls %}
<h2><i class="fas fa-database"></i> FIREBASE DATABASES</h2>
<table class="basic">
<thead>
<tr>
<th>FIREBASE URL</th>
<th>DETAILS</th>
</tr>
</thead>
<tbody>
{% for item in firebase_urls %}
<tr>
<td>
{{ item.url }}
</td>
<td>
{% if item.open %}
<span class="danger">high</span></br>Firebase DB is exposed publicly.
{% else %}
<span class="info">info</span></br>App talks to a Firebase Database.
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if emails %}
<h2><i class="fas fa-envelope"></i> EMAILS</h2>
<table class="basic">
+31 -31
View File
@@ -793,6 +793,37 @@
{% endif %}
{% endif %}
{% if firebase_urls %}
<h2><i class="fas fa-database"></i> FIREBASE DATABASES ANALYSIS</h2>
<table class="basic">
<thead>
<tr>
<th>TITLE</th>
<th>SEVERITY</th>
<th>DESCRIPTION</th>
</tr>
</thead>
<tbody>
{% for find in firebase_urls %}
<tr>
<td>{{ find.title }}</td>
<td>
{% if find.severity == 'high' %}
<span class="danger">high</span>
{% elif find.severity == 'secure' %}
<span class="success">secure</span>
{% elif find.severity == 'warning' %}
<span class="warning">warning</span>
{% elif find.severity == 'info' %}
<span class="info">info</span>
{% endif %}
</td>
<td>{{ find.description }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if domains %}
<h2><i class="fa fa-exclamation"></i> OFAC SANCTIONED COUNTRIES</h2>
@@ -892,37 +923,6 @@
</table>
{% endif %}
{% endif %}
{% if firebase_urls %}
<h2><i class="fas fa-database"></i> FIREBASE DATABASES</h2>
<table class="basic">
<thead>
<tr>
<th>FIREBASE URL</th>
<th>DETAILS</th>
</tr>
</thead>
<tbody>
{% for item in firebase_urls %}
<tr>
<td>
{{ item.url }}
</td>
<td>
{% if item.open %}
<span class="danger">high</span></br>Firebase DB is exposed publicly.
{% else %}
<span class="info">info</span></br>App talks to a Firebase Database.
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if emails %}
<h2><i class="fas fa-envelope"></i> EMAILS</h2>
<table class="basic">
@@ -167,6 +167,12 @@
</a>
</li>
{% endif %}
<li class="nav-item">
<a href="#firebase" class="nav-link">
<i class="fas fa-database nav-icon"></i>
<p>Firebase Analysis</p>
</a>
</li>
</ul>
</li>
<li class="nav-item has-treeview">
@@ -235,12 +241,6 @@
<p>URLs</p>
</a>
</li>
<li class="nav-item">
<a href="#firebase" class="nav-link">
<i class="fas fa-database nav-icon"></i>
<p>Firebase DB</p>
</a>
</li>
<li class="nav-item">
<a href="#emails" class="nav-link">
<i class="fas fa-envelope nav-icon"></i>
@@ -1568,6 +1568,54 @@
</div>
</section>
<!-- ===========================end file analysis ================================== -->
<a id="firebase" class="anchor"></a>
<section class="content">
<div class="container-fluid">
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="card-body">
<p>
<strong><i class="fas fa-database"></i> FIREBASE DATABASE ANALYSIS</strong>
</p>
<div class="table-responsive">
<table id="table_firebase" class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th>TITLE</th>
<th>SEVERITY</th>
<th>DESCRIPTION</th>
</tr>
</thead>
<tbody>
{% for find in firebase_urls %}
<tr>
<td>{{ find.title }}</td>
<td>
{% if find.severity == 'high' %}
<span class="badge bg-danger">high</span>
{% elif find.severity == 'secure' %}
<span class="badge bg-success">secure</span>
{% elif find.severity == 'warning' %}
<span class="badge bg-warning">warning</span>
{% elif find.severity == 'info' %}
<span class="badge bg-info">info</span>
{% endif %}
</td>
<td>{{ find.description }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div><!-- /.card -->
</div>
<!-- end row -->
</div>
</div>
</section>
<!-- ===========================end firebase db analysis ================================== -->
<a id="apkid" class="anchor"></a>
<section class="content">
@@ -1975,53 +2023,6 @@
</div>
</section>
<!-- ===========================end urls ================================== -->
<a id="firebase" class="anchor"></a>
<section class="content">
<div class="container-fluid">
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="card-body">
<p>
<strong><i class="fas fa-database"></i> FIREBASE DATABASE</strong>
</p>
<div class="table-responsive">
{% if firebase_urls %}
<table id="table_firebase" class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th>FIREBASE URL</th>
<th>DETAILS</th>
</tr>
</thead>
<tbody>
{% for item in firebase_urls %}
<tr>
<td>
{{ item.url }}
</td>
<td>
{% if item.open %}
<span class="badge bg-danger">high</span><br/>Firebase Database is exposed publicly.
{% else %}
<span class="badge bg-primary">info</span><br/>App talks to a Firebase database.
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
</div><!-- /.card -->
</div>
<!-- end row -->
</div>
</div>
</section>
<!-- ===========================end firebase db ================================== -->
<a id="emails" class="anchor"></a>
<section class="content">
<div class="container-fluid">
@@ -119,6 +119,12 @@
<p>File Analysis</p>
</a>
</li>
<li class="nav-item">
<a href="#firebase" class="nav-link">
<i class="fas fa-database nav-icon"></i>
<p>Firebase Analysis</p>
</a>
</li>
</ul>
</li>
<li class="nav-item has-treeview">
@@ -165,12 +171,6 @@
<p>URLs</p>
</a>
</li>
<li class="nav-item">
<a href="#firebase" class="nav-link">
<i class="fas fa-database nav-icon"></i>
<p>Firebase DB</p>
</a>
</li>
<li class="nav-item">
<a href="#emails" class="nav-link">
<i class="fas fa-envelope nav-icon"></i>
@@ -1218,6 +1218,55 @@
</div>
</section>
<!-- ===========================end file analysis ================================== -->
<a id="firebase" class="anchor"></a>
<section class="content">
<div class="container-fluid">
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="card-body">
<p>
<strong><i class="fas fa-database"></i> FIREBASE DATABASE ANALYSIS</strong>
</p>
<div class="table-responsive">
<table id="table_firebase" class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th>TITLE</th>
<th>SEVERITY</th>
<th>DESCRIPTION</th>
</tr>
</thead>
<tbody>
{% for find in firebase_urls %}
<tr>
<td>{{ find.title }}</td>
<td>
{% if find.severity == 'high' %}
<span class="badge bg-danger">high</span>
{% elif find.severity == 'secure' %}
<span class="badge bg-success">secure</span>
{% elif find.severity == 'warning' %}
<span class="badge bg-warning">warning</span>
{% elif find.severity == 'info' %}
<span class="badge bg-info">info</span>
{% endif %}
</td>
<td>{{ find.description }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div><!-- /.card -->
</div>
<!-- end row -->
</div>
</div>
</section>
<!-- ===========================end firebase db analysis ================================== -->
<a id="malware_permissions" class="anchor"></a>
<section class="content">
<div class="container-fluid">
@@ -1440,53 +1489,6 @@
</div>
</section>
<!-- ===========================end urls ================================== -->
<a id="firebase" class="anchor"></a>
<section class="content">
<div class="container-fluid">
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="card-body">
<p>
<strong><i class="fas fa-database"></i> FIREBASE DATABASE</strong>
</p>
<div class="table-responsive">
{% if firebase_urls %}
<table id="table_firebase" class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th>FIREBASE URL</th>
<th>DETAILS</th>
</tr>
</thead>
<tbody>
{% for item in firebase_urls %}
<tr>
<td>
{{ item.url }}
</td>
<td>
{% if item.open %}
<span class="badge bg-danger">high</span><br/>Firebase Database is exposed publicly.
{% else %}
<span class="badge bg-primary">info</span><br/>App talks to a Firebase database.
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
</div><!-- /.card -->
</div>
<!-- end row -->
</div>
</div>
</section>
<!-- ===========================end firebase db ================================== -->
<a id="emails" class="anchor"></a>
<section class="content">
<div class="container-fluid">
@@ -141,6 +141,12 @@
</a>
</li>
{% endif %}
<li class="nav-item">
<a href="#firebase" class="nav-link">
<i class="fas fa-database nav-icon"></i>
<p>Firebase Analysis</p>
</a>
</li>
</ul>
</li>
<li class="nav-item has-treeview">
@@ -191,12 +197,6 @@
<p>URLs</p>
</a>
</li>
<li class="nav-item">
<a href="#firebase" class="nav-link">
<i class="fas fa-database nav-icon"></i>
<p>Firebase DB</p>
</a>
</li>
<li class="nav-item">
<a href="#emails" class="nav-link">
<i class="fas fa-envelope nav-icon"></i>
@@ -1232,6 +1232,54 @@
</section>
<!-- ===========================end file analysis ================================== -->
{% endif %}
<a id="firebase" class="anchor"></a>
<section class="content">
<div class="container-fluid">
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="card-body">
<p>
<strong><i class="fas fa-database"></i> FIREBASE DATABASE ANALYSIS</strong>
</p>
<div class="table-responsive">
<table id="table_firebase" class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th>TITLE</th>
<th>SEVERITY</th>
<th>DESCRIPTION</th>
</tr>
</thead>
<tbody>
{% for find in firebase_urls %}
<tr>
<td>{{ find.title }}</td>
<td>
{% if find.severity == 'high' %}
<span class="badge bg-danger">high</span>
{% elif find.severity == 'secure' %}
<span class="badge bg-success">secure</span>
{% elif find.severity == 'warning' %}
<span class="badge bg-warning">warning</span>
{% elif find.severity == 'info' %}
<span class="badge bg-info">info</span>
{% endif %}
</td>
<td>{{ find.description }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div><!-- /.card -->
</div>
<!-- end row -->
</div>
</div>
</section>
<!-- ===========================end firebase db analysis ================================== -->
{% if virus_total %}
<a id="virus_total" class="anchor"></a>
<section class="content">
@@ -1447,53 +1495,6 @@
</div>
</section>
<!-- ===========================end urls ================================== -->
<a id="firebase" class="anchor"></a>
<section class="content">
<div class="container-fluid">
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="card-body">
<p>
<strong><i class="fas fa-database"></i> FIREBASE DATABASE</strong>
</p>
<div class="table-responsive">
{% if firebase_urls %}
<table id="table_firebase" class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th>FIREBASE URL</th>
<th>DETAILS</th>
</tr>
</thead>
<tbody>
{% for item in firebase_urls %}
<tr>
<td>
{{ item.url }}
</td>
<td>
{% if item.open %}
<span class="badge bg-danger">high</span><br/>Firebase Database is exposed publicly.
{% else %}
<span class="badge bg-primary">info</span><br/>App talks to a Firebase database.
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
</div><!-- /.card -->
</div>
<!-- end row -->
</div>
</div>
</section>
<!-- ===========================end firebase db ================================== -->
<a id="emails" class="anchor"></a>
<section class="content">
<div class="container-fluid">
@@ -114,6 +114,12 @@
<p>File Analysis</p>
</a>
</li>
<li class="nav-item">
<a href="#firebase" class="nav-link">
<i class="fas fa-database nav-icon"></i>
<p>Firebase Analysis</p>
</a>
</li>
</ul>
</li>
<li class="nav-item has-treeview">
@@ -154,12 +160,6 @@
<p>URLs</p>
</a>
</li>
<li class="nav-item">
<a href="#firebase" class="nav-link">
<i class="fas fa-database nav-icon"></i>
<p>Firebase DB</p>
</a>
</li>
<li class="nav-item">
<a href="#emails" class="nav-link">
<i class="fas fa-envelope nav-icon"></i>
@@ -854,6 +854,54 @@
</div>
</section>
<!-- ===========================end file analysis ================================== -->
<a id="firebase" class="anchor"></a>
<section class="content">
<div class="container-fluid">
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="card-body">
<p>
<strong><i class="fas fa-database"></i> FIREBASE DATABASE ANALYSIS</strong>
</p>
<div class="table-responsive">
<table id="table_firebase" class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th>TITLE</th>
<th>SEVERITY</th>
<th>DESCRIPTION</th>
</tr>
</thead>
<tbody>
{% for find in firebase_urls %}
<tr>
<td>{{ find.title }}</td>
<td>
{% if find.severity == 'high' %}
<span class="badge bg-danger">high</span>
{% elif find.severity == 'secure' %}
<span class="badge bg-success">secure</span>
{% elif find.severity == 'warning' %}
<span class="badge bg-warning">warning</span>
{% elif find.severity == 'info' %}
<span class="badge bg-info">info</span>
{% endif %}
</td>
<td>{{ find.description }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div><!-- /.card -->
</div>
<!-- end row -->
</div>
</div>
</section>
<!-- ===========================end firebase db analysis ================================== -->
<style>
#chartdiv {
width: 100%;
@@ -1017,55 +1065,8 @@
</div>
</div>
</section>
<!-- ===========================end urls ================================== -->
<a id="firebase" class="anchor"></a>
<section class="content">
<div class="container-fluid">
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="card-body">
<p>
<strong><i class="fas fa-database"></i> FIREBASE DATABASE</strong>
</p>
<div class="table-responsive">
{% if firebase_urls %}
<table id="table_firebase" class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th>FIREBASE URL</th>
<th>DETAILS</th>
</tr>
</thead>
<tbody>
{% for item in firebase_urls %}
<tr>
<td>
{{ item.url }}
</td>
<td>
{% if item.open %}
<span class="badge bg-danger">high</span><br/>Firebase Database is exposed publicly.
{% else %}
<span class="badge bg-primary">info</span><br/>App talks to a Firebase database.
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
</div><!-- /.card -->
</div>
<!-- end row -->
</div>
</div>
</section>
<!-- ===========================end firebase db ================================== -->
<a id="emails" class="anchor"></a>
<!-- ===========================end urls ================================== -->
<a id="emails" class="anchor"></a>
<section class="content">
<div class="container-fluid">
<div class="row">
Generated
+7 -7
View File
@@ -2420,13 +2420,13 @@ files = [
[[package]]
name = "tqdm"
version = "4.66.5"
version = "4.66.6"
description = "Fast, Extensible Progress Meter"
optional = false
python-versions = ">=3.7"
files = [
{file = "tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd"},
{file = "tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad"},
{file = "tqdm-4.66.6-py3-none-any.whl", hash = "sha256:223e8b5359c2efc4b30555531f09e9f2f3589bcd7fdd389271191031b49b7a63"},
{file = "tqdm-4.66.6.tar.gz", hash = "sha256:4bdd694238bef1485ce839d67967ab50af8f9272aab687c0d7702a01da0be090"},
]
[package.dependencies]
@@ -2647,13 +2647,13 @@ watchdog = ["watchdog (>=2.3)"]
[[package]]
name = "whitenoise"
version = "6.7.0"
version = "6.8.1"
description = "Radically simplified static file serving for WSGI applications"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
files = [
{file = "whitenoise-6.7.0-py3-none-any.whl", hash = "sha256:a1ae85e01fdc9815d12fa33f17765bc132ed2c54fa76daf9e39e879dd93566f6"},
{file = "whitenoise-6.7.0.tar.gz", hash = "sha256:58c7a6cd811e275a6c91af22e96e87da0b1109e9a53bb7464116ef4c963bf636"},
{file = "whitenoise-6.8.1-py3-none-any.whl", hash = "sha256:196ba04ca0a80f4a3f99f88381864f218a28b5fb5b44d29feea484d501fa0ba3"},
{file = "whitenoise-6.8.1.tar.gz", hash = "sha256:11042f39f1dcfbb3814726b9364703af6901706582d988e96494cfefdc3a89e2"},
]
[package.extras]
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "mobsf"
version = "4.1.0"
version = "4.1.1"
description = "Mobile Security Framework (MobSF) is an automated, all-in-one mobile application (Android/iOS/Windows) pen-testing, malware analysis and security assessment framework capable of performing static and dynamic analysis."
keywords = ["mobsf", "mobile security framework", "mobile security", "security tool", "static analysis", "dynamic analysis", "malware analysis"]
authors = ["Ajin Abraham <[email protected]>"]
+4 -1
View File
@@ -1,5 +1,5 @@
[tox]
envlist = py310, py311
envlist = py310, py311, p312
skipsdist = True
isolated_build = true
@@ -17,6 +17,7 @@ commands =
[testenv:lint]
skip_install = true
deps =
autopep8
pydocstyle
flake8
flake8-broken-line
@@ -35,6 +36,7 @@ deps =
radon
codespell
commands =
autopep8 . --recursive --in-place
flake8 {posargs}
codespell --ignore-words-list="doubleclick,dout,ne,upto,lief,afile" \
--skip="./mobsf/StaticAnalyzer/tools/*,./mobsf/signatures/*,./mobsf/StaticAnalyzer/views/android/rules/*,.*.map,*.js,*.svg,./build/*,./.tox/*,./venv/*,./.git/*,./poetry.lock"
@@ -98,3 +100,4 @@ ignore =
# remove # flake8: noqa from manifest_analysis.py, should be fixed with R701
max-complexity = 42
radon_max_cc = 10
max-line-length = 88