mirror of
https://github.com/MobSF/Mobile-Security-Framework-MobSF.git
synced 2026-10-12 02:06:59 +00:00
Merge branch 'MobSF:master' into master
This commit is contained in:
@@ -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` |
|
||||
|
||||
|
||||
@@ -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: .
|
||||
|
||||
@@ -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: .
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 = """
|
||||
__ __ _ ____ _____ _____ ___
|
||||
| \/ | ___ | |__/ ___|| ___|_ _|___ // _ \
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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$',
|
||||
|
||||
@@ -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()]
|
||||
@@ -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')
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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:<api_key></code> <strong>Or</strong> <code>X-Mobsf-Api-Key:<api_key></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>{"error": <error message> }</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>{"error": "You are unauthorized to make this request." }</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}}&type={{file.type}}&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}}&type={{file.type}}&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
File diff suppressed because it is too large
Load Diff
+2
-4
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user