Merge branch 'MobSF:master' into master

This commit is contained in:
miaoyc
2024-04-03 16:43:51 +08:00
committed by GitHub
33 changed files with 9308 additions and 1112 deletions
+3
View File
@@ -18,8 +18,11 @@ Please report all security issues [here](https://github.com/MobSF/Mobile-Securit
| Vulnerability | Affected Versions |
| ------- | ------------------ |
| [SSRF in AppLink check via abusing url redirect](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-m435-9v6r-v5f6) | `<=3.9.6` |
| [SSRF in AppLink check via crafted android:host](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-wfgj-wrgh-h3r3) | `<=3.9.5`|
| [Arbitrary Local file read in APK icon resource](https://github.com/MobSF/Mobile-Security-Framework-MobSF/commit/a58f8a8c0aa49e1581d97e19e8e2255ca96cd838) | `>=1.0.4, <=3.9.2` |
| [Remote Code Execution via arbitrary file overwrite vulnerability in apktool <2.9.2](https://github.com/MobSF/Mobile-Security-Framework-MobSF/commit/19c1b55c2c59596f2d43439926c9dc976cbeaec4), [[CVE-2024-21633]](https://github.com/0x33c0unt/CVE-2024-21633) | `<=3.9.1` |
| [Arbitrary Local file read regression](https://github.com/MobSF/Mobile-Security-Framework-MobSF/issues/1197) | `<3.0.0` |
| [Upload a malicious zip file can overwrite arbitary files](https://github.com/MobSF/Mobile-Security-Framework-MobSF/issues/358) | `>=0.9.3.2, <=0.9.4.1` |
| [Arbitrary Local file read](https://github.com/MobSF/Mobile-Security-Framework-MobSF/pull/166) | `<=0.9.2` |
+5 -5
View File
@@ -14,20 +14,20 @@ jobs:
uses: actions/checkout@v3
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
-
name: Login to DockerHub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
-
name: Build and push
name: Build and push AMD64, ARM64 images
id: docker_build
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
push: true
context: .
+5 -5
View File
@@ -13,13 +13,13 @@ jobs:
uses: actions/checkout@v3
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
-
name: Login to DockerHub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
@@ -34,9 +34,9 @@ jobs:
fi
echo "tag=${TAG}" >> $GITHUB_OUTPUT
-
name: Build and push release
name: Build and push AMD64, ARM64 image release
id: docker_build
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
push: true
context: .
+15 -6
View File
@@ -13,16 +13,25 @@ jobs:
uses: actions/checkout@v3
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
-
name: Build and push
id: docker_build
uses: docker/build-push-action@v4
name: Build AMD64 image
id: docker_build_amd
uses: docker/build-push-action@v5
with:
push: false
context: .
platforms: linux/amd64,linux/arm64
platforms: linux/amd64
tags: opensecurity/mobile-security-framework-mobsf:latest
-
name: Build ARM64 image
id: docker_build_arm
uses: docker/build-push-action@v5
with:
push: false
context: .
platforms: linux/arm64
tags: opensecurity/mobile-security-framework-mobsf:latest
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -531,11 +531,10 @@ class CorelliumAgentAPI(CorelliumInit):
def install_ipa(self):
"""Install IPA."""
data = {'path': '/tmp/app.ipa'}
r = requests.post(
f'{self.api}/instances/{self.instance_id}/agent/v1/app/install',
headers=self.headers,
json=data,
json={'path': '/tmp/app.ipa'},
proxies=self.proxies,
verify=self.verify)
if r.status_code in SUCCESS_RESP:
@@ -462,6 +462,34 @@ def run_app(request, api=False):
# AJAX
@require_http_methods(['POST'])
def stop_app(request, api=False):
"""Stop an App."""
data = {
'status': 'failed',
'message': 'Failed to stop the app'}
try:
instance_id = request.POST['instance_id']
bundle_id = request.POST['bundle_id']
failed = common_check(instance_id)
if failed:
return send_response(failed, api)
if not strict_package_check(bundle_id):
data['message'] = 'Invalid iOS Bundle id'
return send_response(data, api)
ca = CorelliumAgentAPI(instance_id)
if (ca.agent_ready()
and ca.unlock_device()
and ca.stop_app(bundle_id) == OK):
data['status'] = OK
data['message'] = 'App Killed'
except Exception as exp:
logger.exception('Failed to stop the app')
data['message'] = str(exp)
return send_response(data, api)
# AJAX
@require_http_methods(['POST'])
def remove_app(request, api=False):
"""Remove an app from the device."""
+1 -1
View File
@@ -10,7 +10,7 @@ from mobsf.install.windows.setup import windows_config_local
logger = logging.getLogger(__name__)
VERSION = '3.9.4'
VERSION = '3.9.7'
BANNER = """
__ __ _ ____ _____ _____ ___
| \/ | ___ | |__/ ___|| ___|_ _|___ // _ \
+9 -2
View File
@@ -137,9 +137,16 @@ def store_exec_hashes_at_first_run():
def subprocess_hook(oldfunc, *args, **kwargs):
global EXECUTABLE_HASH_MAP
exec1 = args[0][0] # executable
if isinstance(args[0], str):
# arg is a string
agmtz = args[0].split()
exec1 = agmtz[0]
else:
# list of args
agmtz = args[0]
exec1 = agmtz[0] # executable
exec2 = None # secondary executable
for arg in args[0]:
for arg in agmtz:
if arg.endswith('.jar'):
exec2 = Path(arg).as_posix()
if '/' in exec1 or '\\' in exec1:
+2
View File
@@ -115,6 +115,8 @@ urlpatterns = [
api_idz.api_ios_dynamic_analyzer),
re_path(r'^api/v1/ios/run_app$',
api_idz.api_run_app),
re_path(r'^api/v1/ios/stop_app$',
api_idz.api_stop_app),
re_path(r'^api/v1/ios/remove_app$',
api_idz.api_remove_app),
re_path(r'^api/v1/ios/take_screenshot$',
+40
View File
@@ -16,9 +16,11 @@ import signal
import string
import subprocess
import stat
import socket
import sqlite3
import unicodedata
import threading
from urllib.parse import urlparse
from pathlib import Path
from distutils.version import StrictVersion
@@ -848,3 +850,41 @@ def settings_enabled(attr):
def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
"""Generate random string."""
return ''.join(random.choice(chars) for _ in range(size))
def valid_host(host):
"""Check if host is valid."""
try:
prefixs = ('http://', 'https://')
if not host.startswith(prefixs):
host = f'http://{host}'
parsed = urlparse(host)
domain = parsed.netloc
path = parsed.path
if len(domain) == 0:
# No valid domain
return False
if len(path) > 0:
# Only host is allowed
return False
if ':' in domain:
# IPv6
return False
# Local network
invalid_prefix = (
'127.',
'192.',
'10.',
'172.',
'169',
'0.',
'localhost')
if domain.startswith(invalid_prefix):
return False
ip = socket.gethostbyname(domain)
if ip.startswith(invalid_prefix):
# Resolve dns to get IP
return False
return True
except Exception:
return False
@@ -224,7 +224,7 @@ def api_frida_logs(request):
{'error': 'Missing Parameters'}, 422)
resp = frida.frida_logs(request, True)
# frida logs can be json or html
if resp.get('data'):
if resp.get('data') or resp.get('message'):
return make_api_response(resp, 200)
return make_api_response(resp, 500)
@@ -192,6 +192,20 @@ def api_run_app(request):
return make_api_response(resp, 200)
@request_method(['POST'])
@csrf_exempt
def api_stop_app(request):
"""POST - Run an App."""
params = {INSTANCE_ID, BUNDLE_ID}
if set(request.POST) < params:
return make_api_response(
{'error': 'Missing Parameters'}, 422)
resp = corellium_instance.stop_app(request, True)
if resp.get('status') == FAILED:
return make_api_response(resp, 500)
return make_api_response(resp, 200)
@request_method(['POST'])
@csrf_exempt
def api_remove_app(request):
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,34 @@
# -*- coding: utf_8 -*-
# flake8: noqa
import os
from xml.dom import minidom
_public_res = None
# copy the newest sdk/platforms/android-?/data/res/values/public.xml here
if _public_res is None:
_public_res = {}
root = os.path.dirname(os.path.realpath(__file__))
xmlfile = os.path.join(root, "public.xml")
if os.path.isfile(xmlfile):
with open(xmlfile, "r") as fp:
_xml = minidom.parseString(fp.read())
for element in _xml.getElementsByTagName("public"):
_type = element.getAttribute('type')
_name = element.getAttribute('name')
_id = int(element.getAttribute('id'), 16)
if _type not in _public_res:
_public_res[_type] = {}
_public_res[_type][_name] = _id
else:
raise Exception("need to copy the sdk/platforms/android-?/data/res/values/public.xml here")
SYSTEM_RESOURCES = {
"attributes": {
"forward": {k: v for k, v in _public_res['attr'].items()},
"inverse": {v: k for k, v in _public_res['attr'].items()}
},
"styles": {
"forward": {k: v for k, v in _public_res['style'].items()},
"inverse": {v: k for k, v in _public_res['style'].items()}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,52 @@
# -*- coding: utf_8 -*-
# flake8: noqa
# Type definiton for (type, data) tuples representing a value
# See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#262
# The 'data' is either 0 or 1, specifying this resource is either
# undefined or empty, respectively.
TYPE_NULL = 0x00
# The 'data' holds a ResTable_ref, a reference to another resource
# table entry.
TYPE_REFERENCE = 0x01
# The 'data' holds an attribute resource identifier.
TYPE_ATTRIBUTE = 0x02
# The 'data' holds an index into the containing resource table's
# global value string pool.
TYPE_STRING = 0x03
# The 'data' holds a single-precision floating point number.
TYPE_FLOAT = 0x04
# The 'data' holds a complex number encoding a dimension value
# such as "100in".
TYPE_DIMENSION = 0x05
# The 'data' holds a complex number encoding a fraction of a
# container.
TYPE_FRACTION = 0x06
# The 'data' holds a dynamic ResTable_ref, which needs to be
# resolved before it can be used like a TYPE_REFERENCE.
TYPE_DYNAMIC_REFERENCE = 0x07
# The 'data' holds an attribute resource identifier, which needs to be resolved
# before it can be used like a TYPE_ATTRIBUTE.
TYPE_DYNAMIC_ATTRIBUTE = 0x08
# Beginning of integer flavors...
TYPE_FIRST_INT = 0x10
# The 'data' is a raw integer value of the form n..n.
TYPE_INT_DEC = 0x10
# The 'data' is a raw integer value of the form 0xn..n.
TYPE_INT_HEX = 0x11
# The 'data' is either 0 or 1, for input "false" or "true" respectively.
TYPE_INT_BOOLEAN = 0x12
# Beginning of color integer flavors...
TYPE_FIRST_COLOR_INT = 0x1c
# The 'data' is a raw integer value of the form #aarrggbb.
TYPE_INT_COLOR_ARGB8 = 0x1c
# The 'data' is a raw integer value of the form #rrggbb.
TYPE_INT_COLOR_RGB8 = 0x1d
# The 'data' is a raw integer value of the form #argb.
TYPE_INT_COLOR_ARGB4 = 0x1e
# The 'data' is a raw integer value of the form #rgb.
TYPE_INT_COLOR_RGB4 = 0x1f
# ...end of integer flavors.
TYPE_LAST_COLOR_INT = 0x1f
# ...end of integer flavors.
TYPE_LAST_INT = 0x1f
@@ -0,0 +1,566 @@
# -*- coding: utf_8 -*-
# flake8: noqa
"""This file is from apkinspector licensed under the Apache License 2.0."""
import io
import zlib
import struct
from typing import Dict
def extract_file_based_on_header_info(apk_file, local_header_info, central_directory_info):
"""
Extracts a single file from the apk_file based on the information provided from the offset and the header_info.
It takes into account that the compression method provided might not be STORED or DEFLATED! The returned
'indicator', shows what compression method was used. Besides the standard STORED/DEFLATE it may return
'DEFLATED_TAMPERED', which means that the compression method found was not DEFLATED(8) but it should have been,
and 'STORED_TAMPERED' which means that the compression method found was not STORED(0) but should have been.
:param apk_file: The APK file e.g. with open('test.apk', 'rb') as apk_file
:type apk_file: bytesIO
:param local_header_info: The local header dictionary info for that specific filename
:type local_header_info: dict
:param central_directory_info: The central directory entry for that specific filename
:type central_directory_info: dict
:return: Returns the actual extracted data for that file along with an indication of whether a static analysis evasion technique was used or not.
:rtype: set(bytes, str)
"""
filename_length = local_header_info["file_name_length"]
if local_header_info["compressed_size"] == 0 or local_header_info["uncompressed_size"] == 0:
compressed_size = central_directory_info["compressed_size"]
uncompressed_size = central_directory_info["uncompressed_size"]
else:
compressed_size = local_header_info["compressed_size"]
uncompressed_size = local_header_info["uncompressed_size"]
extra_field_length = local_header_info["extra_field_length"]
compression_method = local_header_info["compression_method"]
# Skip the offset + local header to reach the compressed data
local_header_size = 30 # Size of the local header in bytes
offset = central_directory_info["relative_offset_of_local_file_header"]
apk_file.seek(offset + local_header_size + filename_length + extra_field_length)
if compression_method == 0: # Stored (no compression)
uncompressed_data = apk_file.read(uncompressed_size)
extracted_data = uncompressed_data
indicator = 'STORED'
elif compression_method == 8:
compressed_data = apk_file.read(compressed_size)
# -15 for windows size due to raw stream with no header or trailer
extracted_data = zlib.decompress(compressed_data, -15)
indicator = 'DEFLATED'
else:
try:
cur_loc = apk_file.tell()
compressed_data = apk_file.read(compressed_size)
extracted_data = zlib.decompress(compressed_data, -15)
indicator = 'DEFLATED_TAMPERED'
except:
apk_file.seek(cur_loc)
compressed_data = apk_file.read(uncompressed_size)
extracted_data = compressed_data
indicator = 'STORED_TAMPERED'
return extracted_data, indicator
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,
offset_of_start_of_central_directory, comment_length, comment):
self.signature = signature
self.number_of_this_disk = number_of_this_disk
self.disk_where_central_directory_starts = disk_where_central_directory_starts
self.number_of_central_directory_records_on_this_disk = number_of_central_directory_records_on_this_disk
self.total_number_of_central_directory_records = total_number_of_central_directory_records
self.size_of_central_directory = size_of_central_directory
self.offset_of_start_of_central_directory = offset_of_start_of_central_directory
self.comment_length = comment_length
self.comment = comment
@classmethod
def parse(cls, apk_file):
"""
Method to locate the "end of central directory record signature" as the first step of the correct process of
reading a ZIP archive. Should be noted that certain APKs do not follow the zip specification and declare multiple
"end of central directory records". For this reason the search for the corresponding signature of the eocd starts
from the end of the apk.
:param apk_file: The already read/loaded data of the APK file e.g. with open('test.apk', 'rb') as apk_file
:type apk_file: bytesIO
:return: Returns the end of central directory record with all the information available if the corresponding signature is found. If not, then it returns None.
:rtype: EndOfCentralDirectoryRecord or None
"""
chunk_size = 1024
offset = 0
signature_offset = -1
file_size = apk_file.seek(0, 2)
while offset < file_size:
position = file_size - offset - chunk_size
if position < 0:
position = 0
apk_file.seek(position)
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
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")
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]
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')
return cls(
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,
offset_of_start_of_central_directory,
comment_length,
comment
)
def to_dict(self):
"""
Represent the class as a dictionary.
:return: returns the dictionary
:rtype: dict
"""
return {
"signature": self.signature,
"number_of_this_disk": self.number_of_this_disk,
"disk_where_central_directory_starts": self.disk_where_central_directory_starts,
"number_of_central_directory_records_on_this_disk": self.number_of_central_directory_records_on_this_disk,
"total_number_of_central_directory_records": self.total_number_of_central_directory_records,
"size_of_central_directory": self.size_of_central_directory,
"offset_of_start_of_central_directory": self.offset_of_start_of_central_directory,
"comment_length": self.comment_length,
"comment": self.comment
}
@classmethod
def from_dict(cls, entry_dict):
"""
Convert a dictionary back to an instance of the class.
:param entry_dict: the dictionary
:type entry_dict: dict
:return: the instance of the class
:rtype: EndOfCentralDirectoryRecord
"""
return cls(**entry_dict)
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,
extra_field_length, file_comment_length, disk_number_where_file_starts,
internal_file_attributes, external_file_attributes, relative_offset_of_local_file_header,
filename, extra_field, file_comment, offset_in_central_directory):
self.version_made_by = version_made_by
self.version_needed_to_extract = version_needed_to_extract
self.general_purpose_bit_flag = general_purpose_bit_flag
self.compression_method = compression_method
self.file_last_modification_time = file_last_modification_time
self.file_last_modification_date = file_last_modification_date
self.crc32_of_uncompressed_data = crc32_of_uncompressed_data
self.compressed_size = compressed_size
self.uncompressed_size = uncompressed_size
self.file_name_length = file_name_length
self.extra_field_length = extra_field_length
self.file_comment_length = file_comment_length
self.disk_number_where_file_starts = disk_number_where_file_starts
self.internal_file_attributes = internal_file_attributes
self.external_file_attributes = external_file_attributes
self.relative_offset_of_local_file_header = relative_offset_of_local_file_header
self.filename = filename
self.extra_field = extra_field
self.file_comment = file_comment
self.offset_in_central_directory = offset_in_central_directory
def to_dict(self):
"""
Represent the class as a dictionary.
:return: returns the dictionary
:rtype: dict
"""
return {
"version_made_by": self.version_made_by,
"version_needed_to_extract": self.version_needed_to_extract,
"general_purpose_bit_flag": self.general_purpose_bit_flag,
"compression_method": self.compression_method,
"file_last_modification_time": self.file_last_modification_time,
"file_last_modification_date": self.file_last_modification_date,
"crc32_of_uncompressed_data": self.crc32_of_uncompressed_data,
"compressed_size": self.compressed_size,
"uncompressed_size": self.uncompressed_size,
"file_name_length": self.file_name_length,
"extra_field_length": self.extra_field_length,
"file_comment_length": self.file_comment_length,
"disk_number_where_file_starts": self.disk_number_where_file_starts,
"internal_file_attributes": self.internal_file_attributes,
"external_file_attributes": self.external_file_attributes,
"relative_offset_of_local_file_header": self.relative_offset_of_local_file_header,
"filename": self.filename,
"extra_field": self.extra_field,
"file_comment": self.file_comment,
"offset_in_central_directory": self.offset_in_central_directory
}
@classmethod
def from_dict(cls, entry_dict):
"""
Convert a dictionary back to an instance of the class.
:param entry_dict: the dictionary
:type entry_dict: dict
:return: the instance of the class
:rtype: CentralDirectoryEntry
"""
return cls(**entry_dict)
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
@classmethod
def parse(cls, apk_file, eocd: EndOfCentralDirectoryRecord = None):
"""
Method that is used to parse the central directory header according to the specification
https://pkware.cachefly.net/webdocs/APPNOTE/APPNOTE-6.3.9.TXT
based on the offset provided by the end of central directory record: eocd.offset_of_start_of_central_directory.
:param apk_file: The already read/loaded data of the APK file e.g. with open('test.apk', 'rb') as apk_file
:type apk_file: bytesIO
:param eocd: End of central directory record
:type eocd: EndOfCentralDirectoryRecord
:return: Returns a dictionary with all the entries discovered. The filename of each entry is used as the key. Besides the fields defined by the specification, each entry has an additional field named 'Offset in the central directory header', which includes the offset of the entry in the central directory itself.
:rtype: CentralDirectory
"""
if not eocd:
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!")
central_directory_entries = {}
while True:
c_offset = apk_file.tell()
signature = apk_file.read(4)
if signature != b'\x50\x4b\x01\x02':
break # Reached the end of the central directory
version_made_by = struct.unpack('<H', apk_file.read(2))[0]
version_needed_to_extract = struct.unpack('<H', apk_file.read(2))[0]
general_purpose_bit_flag = struct.unpack('<H', apk_file.read(2))[0]
compression_method = struct.unpack('<H', apk_file.read(2))[0]
file_last_modification_time = struct.unpack('<H', apk_file.read(2))[0]
file_last_modification_date = struct.unpack('<H', apk_file.read(2))[0]
crc32_of_uncompressed_data = struct.unpack('<I', apk_file.read(4))[0]
compressed_size = struct.unpack('<I', apk_file.read(4))[0]
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]
file_comment_length = struct.unpack('<H', apk_file.read(2))[0]
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')
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(
'utf-8', 'ignore')
offset_in_central_directory = c_offset
central_directory_entry = CentralDirectoryEntry(
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, extra_field_length, file_comment_length,
disk_number_where_file_starts, internal_file_attributes, external_file_attributes,
relative_offset_of_local_file_header, filename, extra_field, file_comment,
offset_in_central_directory
)
central_directory_entries[central_directory_entry.filename] = central_directory_entry
return cls(central_directory_entries)
def to_dict(self):
"""
Represent the class as a dictionary.
:return: returns the dictionary
:rtype: dict
"""
return {filename: entry.to_dict() for filename, entry in self.entries.items()}
@classmethod
def from_dict(cls, entry_dict):
"""
Convert a dictionary back to an instance of the class.
:param entry_dict: the dictionary
:type entry_dict: dict
:return: the instance of the class
:rtype: CentralDirectory
"""
entries = {}
for filename, entry_data in entry_dict.items():
entry_instance = CentralDirectoryEntry.from_dict(entry_data)
entries[filename] = entry_instance
return cls(entries=entries)
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,
extra_field_length, filename, extra_field):
self.version_needed_to_extract = version_needed_to_extract
self.general_purpose_bit_flag = general_purpose_bit_flag
self.compression_method = compression_method
self.file_last_modification_time = file_last_modification_time
self.file_last_modification_date = file_last_modification_date
self.crc32_of_uncompressed_data = crc32_of_uncompressed_data
self.compressed_size = compressed_size
self.uncompressed_size = uncompressed_size
self.file_name_length = file_name_length
self.extra_field_length = extra_field_length
self.filename = filename
self.extra_field = extra_field
@classmethod
def parse(cls, apk_file, entry_of_interest: CentralDirectoryEntry):
"""
Method that attempts to read the local file header according to the specification https://pkware.cachefly.net/webdocs/APPNOTE/APPNOTE-6.3.9.TXT.
:param apk_file: The already read/loaded data of the APK file e.g. with open('test.apk', 'rb') as apk_file
:type apk_file: bytesIO
:param entry_of_interest: The central directory header of the specific entry of interest
:type entry_of_interest: CentralDirectoryEntry
:return: Returns a dictionary with the local header information or None if it failed to find the header.
:rtype: LocalHeaderRecord or None
"""
apk_file.seek(entry_of_interest.relative_offset_of_local_file_header)
header_signature = apk_file.read(4)
if not header_signature == b'\x50\x4b\x03\x04':
print(f"Does not seem to be the start of a local header!")
return None
else:
version_needed_to_extract = struct.unpack('<H', apk_file.read(2))[0]
general_purpose_bit_flag = struct.unpack('<H', apk_file.read(2))[0]
compression_method = struct.unpack('<H', apk_file.read(2))[0]
file_last_modification_time = struct.unpack('<H', apk_file.read(2))[0]
file_last_modification_date = struct.unpack('<H', apk_file.read(2))[0]
crc32_of_uncompressed_data = struct.unpack('<I', apk_file.read(4))[0]
compressed_size = struct.unpack('<I', apk_file.read(4))[0]
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')
extra_field = struct.unpack(f'<{extra_field_length}s', apk_file.read(extra_field_length))[0].decode('utf-8',
'ignore')
return cls(
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, extra_field_length,
filename, extra_field)
def to_dict(self):
"""
Represent the class as a dictionary.
:return: returns the dictionary
:rtype: dict
"""
return {
"version_needed_to_extract": self.version_needed_to_extract,
"general_purpose_bit_flag": self.general_purpose_bit_flag,
"compression_method": self.compression_method,
"file_last_modification_time": self.file_last_modification_time,
"file_last_modification_date": self.file_last_modification_date,
"crc32_of_uncompressed_data": self.crc32_of_uncompressed_data,
"compressed_size": self.compressed_size,
"uncompressed_size": self.uncompressed_size,
"file_name_length": self.file_name_length,
"extra_field_length": self.extra_field_length,
"filename": self.filename,
"extra_field": self.extra_field
}
@classmethod
def from_dict(cls, entry_dict):
"""
Convert a dictionary back to an instance of the class.
:param entry_dict: the dictionary
:type entry_dict: dict
:return: the instance of the class
:rtype: LocalHeaderRecord
"""
return cls(**entry_dict)
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
self.eocd = eocd
self.central_directory = central_directory
self.local_headers = local_headers
@classmethod
def parse(cls, inc_apk, raw: bool = True):
"""
Method to start processing an APK. The raw (bytes) APK may be passed or the path to it.
:param inc_apk: the incoming apk, either path or bytes
:type inc_apk: str or bytesIO
:param raw: boolean flag to specify whether it is the raw apk in bytes or not
:type raw: bool
:return: returns the instance of the class
:rtype: ZipEntry
"""
if raw:
apk_file = inc_apk
else:
with open(inc_apk, 'rb') as apk:
apk_file = io.BytesIO(apk.read())
eocd = EndOfCentralDirectoryRecord.parse(apk_file)
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_headers[local_header_entry.filename] = local_header_entry
return cls(apk_file, eocd, central_directory, local_headers)
@classmethod
def parse_single(cls, apk_file, filename, eocd: EndOfCentralDirectoryRecord = None,
central_directory: CentralDirectory = None):
"""
Similar to parse, but instead of parsing the entire APK, it only targets the specified file.
:param apk_file: The apk file expected raw
:type apk_file: io.TextIOWrapper
:param filename: the filename of the file to be parsed
:type filename: str
:param eocd: Optionally, the instance of the end of central directory from the APK
:type eocd: EndOfCentralDirectoryRecord(, optional)
:param central_directory: Optionally, the instance of the central directory record
:type central_directory: CentralDirectory(, optional)
:return: returns the instance of the class
:rtype: 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])}
return cls(apk_file, eocd, central_directory, local_header)
def to_dict(self):
"""
Represent the class as a dictionary.
:return: returns the dictionary
:rtype: dict
"""
return {
"end_of_central_directory": self.eocd.to_dict(),
"central_directory": self.central_directory.to_dict(),
"local_headers": {filename: entry.to_dict() for filename, entry in self.local_headers.items()}
}
def get_central_directory_entry_dict(self, filename):
"""
Method to retrieve the central directory entry for a specific filename.
:param filename: the filename of the file to search for in the central directory
:type filename: str
:return: returns a dictionary of the central directory entry or None if the filename is not found
:rtype: dict
"""
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!")
def get_local_header_dict(self, filename):
"""
Method to retrieve the local header of a specific filename.
:param filename: the filename of the entry to search for among the local headers
:type filename: str
:return: returns a ditionary of the local header entry or None if the filename is not found
:rtype: dict
"""
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!")
def read(self, name, save: bool = False):
"""
Method to utilize the extract module and extract a single entry from the APK based on the filename.
:param name: the name of the file to be read/extracted
:type name: str
:param save: boolean to define whether the extracted file should be saved as well or not
:type save: bool(, optional)
:return: returns the raw bytes of the filename that was extracted
:rtype: bytes
"""
extracted_file = extract_file_based_on_header_info(self.zip, self.get_local_header_dict(name),
self.get_central_directory_entry_dict(name))[0]
# if save:
# save_data_to_file(f"EXTRACTED_{name}", extracted_file)
return extracted_file
def infolist(self) -> Dict[str, CentralDirectoryEntry]:
"""
List of information about the entries in the central directory.
:return: returns a dictionary where the keys are the filenames and the values are each an instance of the CentralDirectoryEntry
:rtype: dict
"""
return self.central_directory.entries
def namelist(self):
"""
List of the filenames included in the central directory.
:return: returns the list of the filenames
:rtype: list
"""
return [vl for vl in self.central_directory.to_dict()]
+3 -2
View File
@@ -5,10 +5,11 @@ import re
import logging
from pathlib import Path
from androguard.core.bytecodes import apk
from mobsf.StaticAnalyzer.tools.androguard4 import (
apk,
)
logger = logging.getLogger(__name__)
logging.getLogger('androguard').setLevel(logging.ERROR)
def parse_apk(app_path):
@@ -8,9 +8,7 @@ import re
import subprocess
from pathlib import Path
from androguard.util import get_certificate_name_string
from asn1crypto import x509
import asn1crypto
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
@@ -26,6 +24,9 @@ from mobsf.MobSF.utils import (
find_java_binary,
gen_sha256_hash,
)
from mobsf.StaticAnalyzer.tools.androguard4.apk import (
get_certificate_name_string,
)
logger = logging.getLogger(__name__)
ANDROID_8_1_LEVEL = 27
@@ -70,7 +71,7 @@ def get_hardcoded_cert_keystore(files):
def get_cert_details(data):
"""Get certificate details."""
certlist = []
x509_cert = x509.Certificate.load(data)
x509_cert = asn1crypto.x509.Certificate.load(data)
subject = get_certificate_name_string(x509_cert.subject, short=True)
certlist.append(f'X.509 Subject: {subject}')
certlist.append(f'Signature Algorithm: {x509_cert.signature_algo}')
@@ -11,10 +11,6 @@ import subprocess
from lxml import etree
from androguard.core.bytecodes import (
axml,
)
from django.conf import settings
from mobsf.MobSF.utils import (
@@ -22,10 +18,12 @@ from mobsf.MobSF.utils import (
is_file_exists,
is_path_traversal,
)
from mobsf.StaticAnalyzer.tools.androguard4 import (
axml,
)
logger = logging.getLogger(__name__)
logging.getLogger('androguard').setLevel(logging.ERROR)
# relative to res folder
@@ -7,7 +7,9 @@ import requests
from concurrent.futures import ThreadPoolExecutor
from mobsf.MobSF.utils import (
is_number,
upstream_proxy,
valid_host,
)
from mobsf.StaticAnalyzer.views.android import (
android_manifest_desc,
@@ -82,12 +84,15 @@ def _check_url(host, w_url):
status_code = 0
r = requests.get(w_url,
allow_redirects=True,
allow_redirects=False,
proxies=proxies,
verify=verify,
timeout=5)
status_code = r.status_code
if status_code == 302:
logger.warning('302 Redirect detected, skipping check')
status = False
if (str(status_code).startswith('2') and iden in str(r.json())):
status = True
@@ -148,9 +153,11 @@ def get_browsable_activities(node, ns):
and scheme in ('http', 'https')
and host
and host != '*'):
host = host.replace('*.', '')
host = host.replace('*.', '').replace('#', '')
if not valid_host(host):
continue
shost = f'{scheme}://{host}'
if port:
if port and is_number(port):
c_url = f'{shost}:{port}{well_known_path}'
else:
c_url = f'{shost}{well_known_path}'
@@ -310,7 +317,10 @@ def manifest_analysis(mfxml, ns, man_data_dic, src_type, app_dir):
try:
target_sdk = int(man_data_dic['target_sdk'])
except Exception:
target_sdk = ANDROID_8_0_LEVEL
try:
target_sdk = int(man_data_dic['min_sdk'])
except Exception:
target_sdk = ANDROID_8_0_LEVEL
if (target_sdk < ANDROID_9_0_LEVEL
and launchmode == 'singleTask'):
ret_list.append(('task_hijacking', (item,), (target_sdk,)))
@@ -290,7 +290,14 @@ class ELFChecksec:
def fortify(self):
fortified_funcs = []
for function in self.elf.symbols:
if function.name.endswith('_chk'):
if isinstance(function.name, bytes):
try:
function_name = function.name.decode('utf-8')
except UnicodeDecodeError:
function_name = function.name.decode('utf-8', 'replace')
else:
function_name = function.name
if function_name.endswith('_chk'):
fortified_funcs.append(function.name)
return fortified_funcs
@@ -32,6 +32,7 @@ from mobsf.MobSF.utils import (
is_safe_path,
print_n_send_error_response,
upstream_proxy,
valid_host,
)
from mobsf.MobSF.views.scanning import (
add_to_recent_scan,
@@ -254,6 +255,9 @@ def get_avg_cvss(findings):
def open_firebase(url):
# Detect Open Firebase Database
try:
if not valid_host(url):
logger.warning('Invalid Firebase URL')
return url, False
purl = urlparse(url)
base_url = '{}://{}/.json'.format(purl.scheme, purl.netloc)
proxies, verify = upstream_proxy('https')
+9
View File
@@ -0,0 +1,9 @@
<!-- "strings" on big binaries can easily result in millions of hits, avoid rendering them unless needed: -->
{% if list|length != 0 %}
<details {% if list|length < limit %}open{% endif %}>
<summary>{% if list|length < limit %}Showing{% else %}Show{% endif %} all <strong>{{ list | length }}</strong> {{ type }}</summary>
{% for val in list %}
{{ val }}<br/>
{% endfor %}
</details>
{% endif %}
+77
View File
@@ -81,6 +81,7 @@
<li><code>api/v1/ios/setup_environment</code> - <a href="#ios-setup-environment-api">Setup iOS Dynamic Analysis Environment</a></li>
<li><code>api/v1/ios/dynamic_analyzer</code> - <a href="#ios-dynamic-analyzer-api">iOS Dynamic Analyzer</a></li>
<li><code>api/v1/ios/run_app</code> - <a href="#ios-run-app-api">Run App</a></li>
<li><code>api/v1/ios/stop_app</code> - <a href="#ios-stop-app-api">Stop App</a></li>
<li><code>api/v1/ios/remove_app</code> - <a href="#ios-remove-app-api">Remove App</a></li>
<li><code>api/v1/ios/take_screenshot</code> - <a href="#ios-takescreenshot-api">Take Screenshot</a></li>
<li><code>api/v1/ios/get_app_container_path</code> - <a href="#ios-get-app-container-path-api">Get App Container Path</a></li>
@@ -3733,6 +3734,82 @@
</li>
</ul>
<hr />
<h2><a id="ios-stop-app-api"></a><strong>Stop App API</strong></h2>
<p>Kill the app in the Corellium VM.</p>
<ul>
<li>
<p><strong>URL:</strong> <code>/api/v1/ios/stop_app</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>
</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>instance_id</td>
<td>iOS instance id (Available from /api/v1/ios/dynamic_analysis)</td>
<td>Yes</td>
</tr>
<tr>
<td>bundle_id</td>
<td>iOS app bundle id (Available from /api/v1/ios/dynamic_analysis)</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>{"status": "ok", "message": "App Killed"}</code>
</li>
</ul>
</li>
<li>
<p><strong>Error Response:</strong></p>
<ul>
<li>
<strong>Code:</strong> <code>500 Internal Server Error</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;error message&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/ios/stop_app --data "instance_id=bd057756-87a8-45a6-945d-35c7ce48eafe&bundle_id=com.highaltitudehacks.DVIAswiftv2" -H "Authorization:{{ api_key}}"
</code></pre>
</li>
</ul>
</li>
</ul>
<hr />
<h2><a id="ios-remove-app-api"></a><strong>Remove App API</strong></h2>
<p>Remove an app from the Corellium VM.</p>
@@ -2118,11 +2118,7 @@
<strong><i class="fas fa-key"></i> POSSIBLE HARDCODED SECRETS</strong>
</p>
<div class="list-group">
<p>
{% for val in secrets %}
{{ val }}<br/>
{% endfor %}
</p>
{% include 'base/list.html' with list=secrets type="secrets" limit=100 %}
</div>
</div>
</div>
@@ -2145,41 +2141,24 @@
<div class="list-group">
{% if app_type not in 'so' %}
<p><strong>From APK Resource</strong></p>
<p>
{% for key, val in strings.items %}
{% if key == 'strings_apk_res' %}
{% for v in val %}
{{ v }}<br/>
{% endfor %}
{% endif %}
{% endfor %}
</p>
<p><strong>From Code</strong></p>
<p>
{% for key, val in strings.items %}
{% if key == 'strings_code' %}
{% for v in val %}
{{ v }}<br/>
{% endfor %}
{% endif %}
{% endfor %}
</p>
{% include 'base/list.html' with list=strings.strings_apk_res type="strings" limit=100 %}
<p></p><p><strong>From Code</strong></p>
{% include 'base/list.html' with list=strings.strings_code type="strings" limit=100 %}
{% endif %}
<p></p>
<p><strong>From Shared Objects</strong></p>
<p>
{% for key, val in strings.items %}
{% if key == 'strings_so' %}
{% for ls in val %}
{% for k,v in ls.items %}
<p></br><b><i>{{ k }}</i></b></p>
{% for i in v %}
{{ i }}<br/>
{% endfor %}
<p></br><strong><i>{{ k }}</i></strong></p>
<div class="list-group">
{% include 'base/list.html' with list=v type="strings" limit=5 %}
</div>
{% endfor %}
{% endfor %}
{% endif %}
{% endfor %}
</p>
</div>
</div>
</div>
@@ -2201,11 +2180,7 @@
<strong><i class="fa fa-th"></i> SYMBOLS</strong>
</p>
<div class="list-group">
<p>
{% for val in file_analysis %}
{{ val }}<br/>
{% endfor %}
</p>
{% include 'base/list.html' with list=file_analysis type="symbols" limit=50 %}
</div>
</div>
</div>
@@ -2228,10 +2203,8 @@
<strong><i class="fa fa-language"></i> ACTIVITIES</strong>
</p>
<div class="list-group">
<p>
{% for act in activities %}
{{ act}} <br/>
{% endfor %}
<p>
{% include 'base/list.html' with list=activities type="activities" limit=50 %}
</p>
</div>
</div>
@@ -2253,10 +2226,8 @@
<strong><i class="fa fa-cogs"></i> SERVICES</strong>
</p>
<div class="list-group">
<p>
{% for srv in services %}
{{ srv}} <br/>
{% endfor %}
<p>
{% include 'base/list.html' with list=services type="services" limit=50 %}
</p>
</div>
</div>
@@ -2278,10 +2249,8 @@
<strong><i class="fa fa-assistive-listening-systems"></i> RECEIVERS</strong>
</p>
<div class="list-group">
<p>
{% for rcv in receivers %}
{{ rcv}} <br/>
{% endfor %}
<p>
{% include 'base/list.html' with list=receivers type="receivers" limit=50 %}
</p>
</div>
</div>
@@ -2304,10 +2273,8 @@
<strong><i class="fa fa-database"></i> PROVIDERS</strong>
</p>
<div class="list-group">
<p>
{% for prv in providers %}
{{ prv }} <br/>
{% endfor %}
<p>
{% include 'base/list.html' with list=providers type="providers" limit=50 %}
</p>
</div>
</div>
@@ -2329,11 +2296,7 @@
<strong><i class="fab fa-buffer"></i> LIBRARIES</strong>
</p>
<div class="list-group">
<p>
{% for lib in libraries %}
{{ lib }} <br/>
{% endfor %}
</p>
{% include 'base/list.html' with list=libraries type="libraries" limit=100 %}
</div>
</div>
</div>
@@ -2354,11 +2317,7 @@
<strong><i class="far fa-copy"></i> FILES</strong>
</p>
<div class="list-group">
<p>
{% for file in files %}
{{ file}} <br/>
{% endfor %}
</p>
{% include 'base/list.html' with list=files type="files" limit=200 %}
</div>
</div>
</div>
@@ -1587,11 +1587,7 @@
<strong><i class="fas fa-key"></i> POSSIBLE HARDCODED SECRETS</strong>
</p>
<div class="list-group">
<p>
{% for val in secrets %}
{{ val }}<br/>
{% endfor %}
</p>
{% include 'base/list.html' with list=secrets type="secrets" limit=100 %}
</div>
</div>
</div>
@@ -1613,15 +1609,7 @@
</p>
<div class="list-group">
<p><strong>From Code</strong></p>
<p>
{% for key, val in strings.items %}
{% if key == 'strings_code' %}
{% for v in val %}
{{ v }}<br/>
{% endfor %}
{% endif %}
{% endfor %}
</p>
{% include 'base/list.html' with list=strings.strings_code type="strings" limit=100 %}
</div>
</div>
</div>
@@ -1642,11 +1630,7 @@
<strong><i class="fa fa-language"></i> ACTIVITIES</strong>
</p>
<div class="list-group">
<p>
{% for act in activities %}
{{ act}} <br/>
{% endfor %}
</p>
{% include 'base/list.html' with list=activities type="activities" limit=50 %}
</div>
</div>
</div>
@@ -1667,11 +1651,7 @@
<strong><i class="fa fa-cogs"></i> SERVICES</strong>
</p>
<div class="list-group">
<p>
{% for srv in services %}
{{ srv}} <br/>
{% endfor %}
</p>
{% include 'base/list.html' with list=services type="services" limit=50 %}
</div>
</div>
</div>
@@ -1692,11 +1672,7 @@
<strong><i class="fa fa-assistive-listening-systems"></i> RECEIVERS</strong>
</p>
<div class="list-group">
<p>
{% for rcv in receivers %}
{{ rcv}} <br/>
{% endfor %}
</p>
{% include 'base/list.html' with list=receivers type="receivers" limit=50%}
</div>
</div>
</div>
@@ -1718,11 +1694,7 @@
<strong><i class="fa fa-database"></i> PROVIDERS</strong>
</p>
<div class="list-group">
<p>
{% for prv in providers %}
{{ prv }} <br/>
{% endfor %}
</p>
{% include 'base/list.html' with list=providers type="providers" limit=50 %}
</div>
</div>
</div>
@@ -1743,11 +1715,7 @@
<strong><i class="fab fa-buffer"></i> LIBRARIES</strong>
</p>
<div class="list-group">
<p>
{% for lib in libraries %}
{{ lib }} <br/>
{% endfor %}
</p>
{% include 'base/list.html' with list=libraries type="libraries" limit=50 %}
</div>
</div>
</div>
@@ -1768,11 +1736,7 @@
<strong><i class="far fa-copy"></i> FILES</strong>
</p>
<div class="list-group">
<p>
{% for file in files %}
{{ file}} <br/>
{% endfor %}
</p>
{% include 'base/list.html' with list=files type="files" limit=200 %}
</div>
</div>
</div>
@@ -1185,14 +1185,36 @@
{{ item.issue }}
</td>
<td>
{% for file in item.files %}
{% if item.files|length < 4 %}
{% for file in item.files %}
<small>
{% if file.type %}
<a href="{% url 'view_file_ios' %}?file={{file.file_path}}&amp;type={{file.type}}&amp;md5={{file.hash}}">{{ file.file_path }}</a>
{% else %}
{{ file.file_path }}
{% endif %}
<br/>
</small>
</br>
{% endfor %}
{% else %}
<a class="btn btn-primary btn-xs" data-toggle="collapse" href="#collapsefiles{{forloop.counter}}" role="button" aria-expanded="false" aria-controls="collapsefiles{{forloop.counter}}">
Show Files
</a>
<div class="collapse" id="collapsefiles{{forloop.counter}}">
{% for file in item.files %}
<small>
{% if file.type %}
<a href="{% url 'view_file_ios' %}?file={{file.file_path}}&amp;type={{file.type}}&amp;md5={{file.hash}}">{{ file.file_path }}</a>
{% else %}
{{ file.file_path }}
{% endif %}
</small>
</br>
{% endfor %}
</div>
{% endif %}
</td>
</tr>
@@ -1572,11 +1594,7 @@
<strong><i class="fas fa-key"></i> POSSIBLE HARDCODED SECRETS</strong>
</p>
<div class="list-group">
<p>
{% for val in secrets %}
{{ val }}<br/>
{% endfor %}
</p>
{% include 'base/list.html' with list=secrets type="secrets" limit=100 %}
</div>
</div>
</div>
@@ -1597,9 +1615,7 @@
<strong><i class="fas fa-font"></i> STRINGS</strong>
</p>
<div class="list-group">
{% for string in strings %}
{{string}} <br>
{% endfor %}
{% include 'base/list.html' with list=strings type="strings" limit=100 %}
</div>
</div>
</div>
@@ -1621,11 +1637,7 @@
<strong><i class="fa fa-th"></i> SYMBOLS</strong>
</p>
<div class="list-group">
<p>
{% for val in file_analysis %}
{{ val }}<br/>
{% endfor %}
</p>
{% include 'base/list.html' with list=file_analysis type="symbols" limit=100 %}
</div>
</div>
</div>
@@ -1648,12 +1660,7 @@
<strong><i class="fab fa-buffer"></i> LIBRARIES</strong>
</p>
<div class="list-group">
<p>
{% for lib in libraries %}
{{ lib }}
<br/>
{% endfor %}
</p>
{% include 'base/list.html' with list=libraries type="libraries" limit=100 %}
</div>
</div>
</div>
@@ -1674,11 +1681,7 @@
<strong><i class="far fa-copy"></i> FILES</strong>
</p>
<div class="list-group">
<p>
{% for file in files %}
{{ file}} <br/>
{% endfor %}
</p>
{% include 'base/list.html' with list=files type="files" limit=200 %}
</div>
</div>
</div>
@@ -1190,11 +1190,7 @@
<strong><i class="fas fa-font"></i> STRINGS</strong>
</p>
<div class="list-group">
<p>
{% for val in strings %}
{{ val }}<br/>
{% endfor %}
</p>
{% include 'base/list.html' with list=strings type="strings" limit=100 %}
</div>
</div>
</div>
@@ -1215,11 +1211,7 @@
<strong><i class="far fa-copy"></i> FILES</strong>
</p>
<div class="list-group">
<p>
{% for file in files %}
{{ file}} <br/>
{% endfor %}
</p>
{% include 'base/list.html' with list=files type="files" limit=200 %}
</div>
</div>
</div>
Generated
+69 -929
View File
File diff suppressed because it is too large Load Diff
+2 -4
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "mobsf"
version = "3.9.4"
version = "3.9.7"
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]>"]
@@ -44,9 +44,7 @@ lief = ">=0.12.3"
http-tools = ">=4.0.0"
pdfkit = ">=0.6.1"
google-play-scraper = ">=0.1.2"
androguard = "3.4.0a1"
apkid = "2.1.5"
quark-engine = "23.7.1"
frida = ">=16.1.4"
tldextract = "3.4.4"
openstep-parser = "1.5.4"
@@ -56,7 +54,7 @@ apksigtool = "0.1.0"
tzdata = "^2023.3"
libsast = "^2.0.0"
paramiko = "^3.3.1"
six = ">=1.16.0"
[build-system]
requires = ["poetry-core"]