diff --git a/.dockerignore b/.dockerignore index fc801a92ab..68d2ed037b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -24,4 +24,5 @@ mobsf/downloads mobsf/uploads mobsf/debug.log mobsf/secret -mobsf/StaticAnalyzer/test_files/ \ No newline at end of file +mobsf/StaticAnalyzer/test_files/ +TODO.md \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 780032eefe..8ab9756897 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -15,7 +15,7 @@ The issue tracker is the preferred channel for [bug reports](#bugs), [features requests](#features) and [submitting pull requests](#pull-requests), but please respect the following restrictions: -* Please **do not** use the issue tracker for personal support requests (use [MobSF Slack channel](https://join.slack.com/t/mobsf/shared_invite/zt-153nfus2r-hMCGrwzm8Lyy3OxsihnolQ) or +* Please **do not** use the issue tracker for personal support requests (use [MobSF Slack channel](https://join.slack.com/t/mobsf/shared_invite/zt-2umjnqlsm-sNSh9g4GFraPUBPqatwTxw) or [Stack Overflow](https://stackoverflow.com/search?q=mobsf)). * Please **do not** derail or troll issues. Keep the discussion on topic and diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index a3d0f15e37..f30cddf29e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -8,9 +8,9 @@ assignees: '' --- - - ## ENVIRONMENT @@ -26,7 +26,7 @@ MobSF Version: ``` What happens, under which versions, under what conditions, when, and what were you expecting instead. ``` - + ## STEPS TO REPRODUCE THE ISSUE diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 0c99bb9fe3..a5c26247ad 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -1,14 +1,6 @@ # Security Policy -## Supported Versions - -| Version | Supported | -| ------- | ------------------ | -| 1.0.x | :x: | -| 2.0.x | :x: | -| 3.0.x | :white_check_mark: | -| 4.0.x | :white_check_mark: | - +Keeping MobSF updated to the latest version is essential for ensuring security and stability. ## Reporting a Vulnerability @@ -18,6 +10,9 @@ Please report all security issues [here](https://github.com/MobSF/Mobile-Securit | Vulnerability | Affected Versions | | ------- | ------------------ | +| [Stored Cross-Site Scripting Vulnerability in Recent Scans "Diff or Compare"](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-5jc6-h9w7-jm3p) | `<=4.2.8` | +| [Zip Slip Vulnerability in .a extraction](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-4hh3-vj32-gr6j) | `<=4.0.6` | +| [Open Redirect in Login redirect](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-8m9j-2f32-2vx4) | `<=4.0.4` | | [SSRF in firebase database check](https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-wpff-wm84-x5cx) | `<=3.9.7` | | [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`| diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md index 1198ddcb60..99edf1f0e5 100644 --- a/.github/SUPPORT.md +++ b/.github/SUPPORT.md @@ -1 +1 @@ -Github Issues are ONLY for reporting bugs and feature requests. For support, questions, queries and discussions use our slack channel. [Join MobSF Slack Channel](https://join.slack.com/t/mobsf/shared_invite/zt-153nfus2r-hMCGrwzm8Lyy3OxsihnolQ) +Github Issues are ONLY for reporting bugs and feature requests. For support, questions, queries and discussions use our slack channel. [Join MobSF Slack Channel](https://join.slack.com/t/mobsf/shared_invite/zt-2umjnqlsm-sNSh9g4GFraPUBPqatwTxw) diff --git a/.github/codeql-config.yml b/.github/codeql-config.yml new file mode 100644 index 0000000000..2372524493 --- /dev/null +++ b/.github/codeql-config.yml @@ -0,0 +1,12 @@ +name: "CodeQL config" + +queries: + - uses: security-extended + +query-filters: + - exclude: + id: py/path-injection # To much false positives + +paths-ignore: + - "**/.git/**" + - "**/.github/**" diff --git a/.github/workflows/auto-comment.yml b/.github/workflows/auto-comment.yml index 9932210c73..d61a35b545 100644 --- a/.github/workflows/auto-comment.yml +++ b/.github/workflows/auto-comment.yml @@ -12,7 +12,7 @@ jobs: issuesOpened: > 👋 @{{ author }} - Issues is only for reporting a bug/feature request. For limited support, questions, and discussions, please join [MobSF Slack channel](https://join.slack.com/t/mobsf/shared_invite/zt-153nfus2r-hMCGrwzm8Lyy3OxsihnolQ) + Issues is only for reporting a bug/feature request. For limited support, questions, and discussions, please join [MobSF Slack channel](https://join.slack.com/t/mobsf/shared_invite/zt-2umjnqlsm-sNSh9g4GFraPUBPqatwTxw) Please include all the requested and relevant information when opening a bug report. Improper reports will be closed without any response. diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ce094bd8d7..6af8532505 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,61 +1,48 @@ -name: "CodeQL" +name: "CodeQL Advanced" on: push: branches: [ "master" ] pull_request: - # The branches below must be a subset of the branches above branches: [ "master" ] schedule: - - cron: '17 16 * * 0' + - cron: '18 14 * * 3' jobs: analyze: - name: Analyze + name: Analyze (${{ matrix.language }}) runs-on: ubuntu-latest permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories actions: read contents: read - security-events: write strategy: fail-fast: false matrix: - language: [ 'python' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - + include: + - language: python + build-mode: none + steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - + build-mode: ${{ matrix.build-mode }} + config-file: .github/codeql-config.yml + - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/mobsf-test.yml b/.github/workflows/mobsf-test.yml index 045fa14134..7677cbcb40 100644 --- a/.github/workflows/mobsf-test.yml +++ b/.github/workflows/mobsf-test.yml @@ -6,13 +6,16 @@ on: pull_request: branches: [ master ] +env: + MOBSF_DISABLE_AUTHENTICATION: "1" + jobs: build: strategy: fail-fast: false matrix: os: [ubuntu-22.04, macos-latest, windows-latest] - python-version: ['3.10', '3.11'] + python-version: ['3.12'] runs-on: ${{ matrix.os }} steps: @@ -22,14 +25,15 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Setup Pip and Poetry + - name: Setup pip, poetry and tox run: | - python -m pip install pip==22.3.1 poetry==1.6.1 + python -m ensurepip --upgrade + python -m pip install pip poetry==1.8.4 + python -m pip install --upgrade setuptools tox - name: Lint on Ubuntu if: startsWith(matrix.os, 'ubuntu') run: | - python -m pip install --upgrade tox tox -e lint - name: Install Ubuntu Dependencies @@ -59,9 +63,16 @@ jobs: poetry run python manage.py makemigrations poetry run python manage.py makemigrations StaticAnalyzer poetry run python manage.py migrate - + poetry run python manage.py create_roles + + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + - name: Unit Tests on Ubuntu, macOS and Windows run: | + java -version git submodule update --init --recursive poetry run python manage.py test mobsf diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 1927a33bb2..4d530fabeb 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -18,7 +18,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install poetry==1.6.1 + pip install poetry==1.8.4 - name: Build and publish env: PYPI_TOKEN: ${{ secrets.PYPI_PASSWORD }} diff --git a/.gitignore b/.gitignore index 90521489c5..e7bfad96ac 100644 --- a/.gitignore +++ b/.gitignore @@ -78,3 +78,4 @@ mobsf/secret mobsf/StaticAnalyzer/migrations mobsf/MobSF/windows_vm_priv_key.asc mobsf/setup_done.txt +TODO.md \ No newline at end of file diff --git a/.sonarcloud.properties b/.sonarcloud.properties index 28eead4702..649b785465 100644 --- a/.sonarcloud.properties +++ b/.sonarcloud.properties @@ -1,4 +1,4 @@ sonar.sources=. sonar.exclusions=mobsf/static/**/*,mobsf/templates/**/* sonar.sourceEncoding=UTF-8 -sonar.python.version=3.10, 3.11 \ No newline at end of file +sonar.python.version=3.10, 3.11, 3.12 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 8e742fcfea..1ae2880009 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,6 @@ # Base image -FROM ubuntu:22.04 +FROM python:3.12-slim-bookworm -# Labels and Credits LABEL \ name="MobSF" \ author="Ajin Abraham " \ @@ -10,71 +9,68 @@ LABEL \ contributor_2="Vincent Nadal " \ 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." -ENV DEBIAN_FRONTEND=noninteractive +ENV DEBIAN_FRONTEND=noninteractive \ + LANG=en_US.UTF-8 \ + LANGUAGE=en_US:en \ + LC_ALL=en_US.UTF-8 \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONFAULTHANDLER=1 \ + MOBSF_USER=mobsf \ + USER_ID=9901 \ + MOBSF_PLATFORM=docker \ + MOBSF_ADB_BINARY=/usr/bin/adb \ + JAVA_HOME=/jdk-22.0.2 \ + PATH=/jdk-22.0.2/bin:/root/.local/bin:$PATH \ + DJANGO_SUPERUSER_USERNAME=mobsf \ + DJANGO_SUPERUSER_PASSWORD=mobsf # See https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run -RUN apt update -y && apt install -y --no-install-recommends \ +RUN apt update -y && \ + apt install -y --no-install-recommends \ + android-sdk-build-tools \ + android-tools-adb \ build-essential \ - locales \ - sqlite3 \ + curl \ + fontconfig \ fontconfig-config \ - libjpeg-turbo8 \ - libxrender1 \ + git \ libfontconfig1 \ + libjpeg62-turbo \ libxext6 \ - fontconfig \ - xfonts-75dpi \ - xfonts-base \ - python3 \ + libxrender1 \ + locales \ python3-dev \ - python3-pip \ - wget \ - curl \ - git \ - jq \ + sqlite3 \ unzip \ - android-tools-adb && \ + wget \ + xfonts-75dpi \ + xfonts-base && \ + echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && \ locale-gen en_US.UTF-8 && \ - apt upgrade -y + update-locale LANG=en_US.UTF-8 && \ + apt upgrade -y && \ + curl -sSL https://install.python-poetry.org | python3 - && \ + apt autoremove -y && apt clean -y && rm -rf /var/lib/apt/lists/* /tmp/* -ENV MOBSF_USER=mobsf \ - MOBSF_PLATFORM=docker \ - MOBSF_ADB_BINARY=/usr/bin/adb \ - JDK_FILE=openjdk-20.0.2_linux-x64_bin.tar.gz \ - JDK_FILE_ARM=openjdk-20.0.2_linux-aarch64_bin.tar.gz \ - WKH_FILE=wkhtmltox_0.12.6.1-2.jammy_amd64.deb \ - WKH_FILE_ARM=wkhtmltox_0.12.6.1-2.jammy_arm64.deb \ - JAVA_HOME=/jdk-20.0.2 \ - PATH=$JAVA_HOME/bin:$PATH \ - LANG=en_US.UTF-8 \ - LANGUAGE=en_US:en \ - LC_ALL=en_US.UTF-8 \ - PYTHONUNBUFFERED=1 \ - PYTHONDONTWRITEBYTECODE=1 \ - PYTHONFAULTHANDLER=1 \ - POETRY_VERSION=1.6.1 - -# Install wkhtmltopdf & OpenJDK ARG TARGETPLATFORM -COPY scripts/install_java_wkhtmltopdf.sh . -RUN ./install_java_wkhtmltopdf.sh +# Install wkhtmltopdf, OpenJDK and jadx +COPY scripts/dependencies.sh mobsf/MobSF/tools_download.py ./ +RUN ./dependencies.sh -RUN groupadd -g 9901 $MOBSF_USER -RUN adduser $MOBSF_USER --shell /bin/false -u 9901 --ingroup $MOBSF_USER --gecos "" --disabled-password - -COPY poetry.lock pyproject.toml ./ -RUN python3 -m pip install --upgrade --no-cache-dir pip poetry==${POETRY_VERSION} && \ - poetry config virtualenvs.create false && \ - poetry install --only main --no-root --no-interaction --no-ansi +# Install Python dependencies +COPY pyproject.toml . +RUN poetry config virtualenvs.create false && \ + poetry lock && \ + poetry install --only main --no-root --no-interaction --no-ansi && \ + poetry cache clear . --all && \ + rm -rf /root/.cache/ # Cleanup RUN \ apt remove -y \ - libssl-dev \ - libffi-dev \ - libxml2-dev \ - libxslt1-dev \ + git \ python3-dev \ wget && \ apt clean && \ @@ -82,27 +78,22 @@ RUN \ apt autoremove -y && \ rm -rf /var/lib/apt/lists/* /tmp/* > /dev/null 2>&1 -WORKDIR /home/mobsf/Mobile-Security-Framework-MobSF # Copy source code +WORKDIR /home/mobsf/Mobile-Security-Framework-MobSF COPY . . -# Check if Postgres support needs to be enabled. -# Disabled by default -ARG POSTGRES=False -ENV POSTGRES_USER=postgres \ - POSTGRES_PASSWORD=password \ - POSTGRES_DB=mobsf \ - POSTGRES_HOST=postgres - -RUN ./scripts/postgres_support.sh $POSTGRES - HEALTHCHECK CMD curl --fail http://host.docker.internal:8000/ || exit 1 # Expose MobSF Port and Proxy Port -EXPOSE 8000 8000 1337 1337 +EXPOSE 8000 1337 + +# Create mobsf user +RUN groupadd --gid $USER_ID $MOBSF_USER && \ + useradd $MOBSF_USER --uid $USER_ID --gid $MOBSF_USER --shell /bin/false && \ + chown -R $MOBSF_USER:$MOBSF_USER /home/mobsf -RUN chown -R $MOBSF_USER:$MOBSF_USER /home/mobsf -USER mobsf +# Switch to mobsf user +USER $MOBSF_USER # Run MobSF CMD ["/home/mobsf/Mobile-Security-Framework-MobSF/scripts/entrypoint.sh"] diff --git a/LICENSES/androguard.txt b/LICENSES/androguard.txt index 0c2f45d526..49b27b8f78 100644 --- a/LICENSES/androguard.txt +++ b/LICENSES/androguard.txt @@ -1,76 +1,178 @@ - Androguard - -Copyright (C) 2012 - 2016, Anthony Desnos (desnos at t0t0.fr) All rights reserved. - -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. - -================================================================================================================================================================================================================================================================================================================== - - DAD - -Copyright (C) 2012 - 2016, Geoffroy Gueguen (geoffroy dot gueguen at gmail dot com) All rights reserved. - -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. - -================================================================================================================================================================================================================================================================================================================== -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: - - You must give any other recipients of the Work or Derivative Works a copy of this License; and - You must cause any modified files to carry prominent notices stating that You changed the files; and - 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 - 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. - +Androguard 4 + + 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 diff --git a/LICENSES/backsmali.txt b/LICENSES/backsmali.txt index 32d91ed370..a4068b9a68 100644 --- a/LICENSES/backsmali.txt +++ b/LICENSES/backsmali.txt @@ -1,29 +1,294 @@ -The BSD 3-Clause License -The following is a BSD 3-Clause ("BSD New" or "BSD Simplified") license template. To generate your own license, change the values of OWNER, ORGANIZATION and YEAR from their original values as given here, and substitute your own. +The majority of smali/baksmali is written and copyrighted by me (Ben Gruver) +and released under the following license: -Note: You may omit clause 3 and still be OSD-conformant. Despite its colloquial name "BSD New", this is not the newest version of the BSD license; it was followed by the even newer BSD-2-Clause version, sometimes known as the "Simplified BSD License". On January 9th, 2008 the OSI Board approved BSD-2-Clause, which is used by FreeBSD and others. It omits the final "no-endorsement" clause and is thus roughly equivalent to the MIT License. +******************************************************************************* +Copyright (c) 2010 Ben Gruver (JesusFreke) +All rights reserved. -Historical Background: The original license used on BSD Unix had four clauses. The advertising clause (the third of four clauses) required you to acknowledge use of U.C. Berkeley code in your advertising of any product using that code. It was officially rescinded by the Director of the Office of Technology Licensing of the University of California on July 22nd, 1999. He states that clause 3 is "hereby deleted in its entirety." The four clause license has not been approved by OSI. The license below does not contain the advertising clause. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. -This prelude is not part of the license. +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +******************************************************************************* - = Regents of the University of California - = University of California, Berkeley - = 1998 -In the original BSD license, the occurrence of "copyright holder" in the 3rd clause read "ORGANIZATION", placeholder for "University of California". In the original BSD license, both occurrences of the phrase "COPYRIGHT HOLDERS AND CONTRIBUTORS" in the disclaimer read "REGENTS AND CONTRIBUTORS". +Unless otherwise stated in the code/commit message, any changes with the +committer of bgruv@google.com or wkal@google.com is copyrighted by +Google LLC and released under the following license: -Here is the license template: +******************************************************************************* +Copyright 2011, Google LLC -Copyright (c) , -All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google LLC nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +******************************************************************************* + + +Various portions of the code are taken from the Android Open Source Project, +and are used in accordance with the following license: + +******************************************************************************* +Copyright (C) 2007 The Android Open Source Project + +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. +******************************************************************************* + +Various portions of the code are taken from https://github.com/google/guava, +and are used in accordance with the following license: + +Content of http://www.apache.org/licenses/LICENSE-2.0: + +******************************************************************************* + + + 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. -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + 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. -1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + Copyright 2007 The Android Open Source Project -2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + 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 -3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + http://www.apache.org/licenses/LICENSE-2.0 -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file + 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. diff --git a/README.md b/README.md index d735ce4b1b..918fb5ed13 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ # Mobile Security Framework (MobSF) -Version: v3.9 beta ![](https://cloud.githubusercontent.com/assets/4301109/20019521/cc61f7fc-a2f2-11e6-95f3-407030d9fdde.png) @@ -7,12 +6,10 @@ Mobile Security Framework (MobSF) is a security research platform for mobile app Made with ![Love](https://cloud.githubusercontent.com/assets/4301109/16754758/82e3a63c-4813-11e6-9430-6015d98aeaab.png) in India -[![python](https://img.shields.io/badge/python-3.10+-blue.svg?logo=python&labelColor=yellow)](https://www.python.org/downloads/) +[![Docker Pulls](https://img.shields.io/docker/pulls/opensecurity/mobile-security-framework-mobsf?style=social)](https://hub.docker.com/r/opensecurity/mobile-security-framework-mobsf/) [![python](https://img.shields.io/badge/python-3.10+-blue.svg?logo=python&labelColor=yellow)](https://www.python.org/downloads/) [![PyPI version](https://badge.fury.io/py/mobsf.svg)](https://badge.fury.io/py/mobsf) [![platform](https://img.shields.io/badge/platform-osx%2Flinux%2Fwindows-green.svg)](https://github.com/MobSF/Mobile-Security-Framework-MobSF/) [![License](https://img.shields.io/:license-GPL--3.0--only-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.html) -[![Docker Pulls](https://img.shields.io/docker/pulls/opensecurity/mobile-security-framework-mobsf?style=social)](https://hub.docker.com/r/opensecurity/mobile-security-framework-mobsf/) - [![MobSF tests](https://github.com/MobSF/Mobile-Security-Framework-MobSF/workflows/MobSF%20tests/badge.svg?branch=master)](https://github.com/MobSF/Mobile-Security-Framework-MobSF/actions) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=MobSF_Mobile-Security-Framework-MobSF&metric=alert_status)](https://sonarcloud.io/dashboard?id=MobSF_Mobile-Security-Framework-MobSF) ![GitHub closed issues](https://img.shields.io/github/issues-closed/MobSF/Mobile-Security-Framework-MobSF) @@ -32,23 +29,23 @@ MobSF is also bundled with [Android Tamer](https://tamerplatform.com), [BlackArc [![Donate to MobSF](https://user-images.githubusercontent.com/4301109/117404264-7aab5480-aebe-11eb-9cbd-da82d7346bb3.png)](https://opensecurity.in/donate) -If you liked MobSF and find it useful, please consider donating. -*It's easy to build open source, maintaining one is a different story. Long live open source!* +> Has MobSF made a difference for you? Show your support and help us innovate with a donation. It's easy to build open source, maintaining one is a different story. + +*Long live open source!* ## Documentation -Quick setup +Quick setup with docker ``` docker pull opensecurity/mobile-security-framework-mobsf:latest docker run -it --rm -p 8000:8000 opensecurity/mobile-security-framework-mobsf:latest + +# Default username and password: mobsf/mobsf ``` [![See MobSF Documentation](https://user-images.githubusercontent.com/4301109/70686099-3855f780-1c79-11ea-8141-899e39459da2.png)](https://mobsf.github.io/docs) -[![See MobSF Documentation in Chinese](https://user-images.githubusercontent.com/4301109/117404947-b09d0880-aebf-11eb-9db8-3d7360f47914.png)](https://mobsf.github.io/docs/#/zh-cn/) -[![See MobSF Documentation in Japanese](https://user-images.githubusercontent.com/4301109/148662149-7ee671b4-66a2-4232-9522-276b8e88d924.png)](https://mobsf.github.io/docs/#/ja-jp/) -[![See MobSF Documentation in Español](https://user-images.githubusercontent.com/4301109/173221657-ac1f7221-6ae9-44d8-bf6b-8732d84bf120.png)](https://mobsf.github.io/docs/#/es/) * Try MobSF Static Analyzer Online: [mobsf.live](https://mobsf.live) * MobSF in CI/CD: [mobsfscan](https://github.com/MobSF/mobsfscan) @@ -67,7 +64,7 @@ docker run -it --rm -p 8000:8000 opensecurity/mobile-security-framework-mobsf:la ## MobSF Support -* **Free Support:** Free limited support, questions, help and discussions, join our Slack channel [![Join_MobSF_Slack](https://img.shields.io/badge/mobsf%20slack-join-green?logo=slack&labelColor=4A154B)](https://join.slack.com/t/mobsf/shared_invite/zt-153nfus2r-hMCGrwzm8Lyy3OxsihnolQ) +* **Free Support:** Free limited support, questions, help and discussions, join our Slack channel [![Join_MobSF_Slack](https://img.shields.io/badge/mobsf%20slack-join-green?logo=slack&labelColor=4A154B)](https://join.slack.com/t/mobsf/shared_invite/zt-2umjnqlsm-sNSh9g4GFraPUBPqatwTxw) * **Enterprise Support:** Priority feature requests, live support & onsite training, see [![MobSF Support Packages](https://img.shields.io/badge/enterprise-support%20package-blue?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGMAAABaCAMAAACbkBjCAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAkNQTFRFAAAA////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////o1yoNQAAAMF0Uk5TAAQ1YH6IXzYFAUqq9P/1rU4CPdHWQ4X+jgOkro+hVWYR6vIajJvz+XN/z9sjLW6lrNPX+PsGHyE5TE9XWV1hW0k6LC7hs7SEhgr2sGRiFBCXJf38sgwOtmpoIiDf68vSGS+Z9yeR70RNvUEVfOa/WgdCR8XuUeeA8Du7Vporrx66SwlcqWV92g2HY+2Ki/GQJjxpM8TVcIGodjJ3ybETJOjUyiop4sxYkkVSGJ/pD6cdwlTOHNnIZ+B13bXkKL7NMOC0/xQAAAPPSURBVHic7dn5PxVRFADwY0sPN0t4Uj0vLQq9lkelRVFRKklZs+RRiiTtSbRoD9GuhdK+Ky3S9qf1Zu68bczcO5rjhz4f5yfnOud9mTszd+Y+gLH4f8PL28fXz893nP/4UQIMAYFBRIoJwSGho0CETSQeER6BLYRGGoksjFGTcIlouSDEZNRpmaJEEDIVkTApE4TEoBHmaWpG7HQsY4YaQchMJMI8S92IQ/pHZqsThMzBMeJZRgIKkcgiCJmLYcSwDQuGMY9tzMcwFrCNhQiEl5VtJCXrNxaxCUIW6zf8ecYS/UYKz1iq31jGM5brN1bwDB/9RirPQFhDuMdqpX5jFc9I0W+k8Yww/UY6z1it3zDzDLN+A9awiTUIBKxlGxkYRibbWIdhLGUbCLcrgPUTWETWegyDPSEbUAjYyDI24RjZWepE7GYcA3LUjSlIBGzJVSO2JmIZsG2UZ1wI1TUEYZ11RJ7KrFvz8Qy1S6QAkYBCZaMI0yhWNrZjGlCiRJSiElCmZJSPGWOGrtiheKFjGhW2yiVVO+XL+i6bz9pqHCB09x77B9YA1O6tcwHGffX7oYGQsnr9+2R5MeHiZx4QkmTTQSocsqUL+WHh5yNHs3UJxVHHpL87RBo5HphkbDwhvS770N9lNZ38Z6Gm2bXGmpyjLa7l1eI8cqdO/8tW7JmNHnutZ6XhtJzA1nOOx7bzbgUXLBdHKLRcKvU8iS4Lo2YL3de4cpXe1Ns8Sto7ro1A6AxMkl8KXQDXbc5tapLbuNsA0CSvumHy0gTkF96Ut9rjFtyWjRwFuDO8ruRuLVfoblLePLwH4bKRmwAFSpXW6E4WYC66rwjYw3fYY8MDgIMqxeWWh8qAobq1Uk0gJBJ6ej0GensAHqmWW1urDQoH6bE6YI82gD53pPeJvecpq+OZt5x4ztkJewEeSG+f0NTObIlL9SReDjtZZWEDd4QSoaqP2jRyX7kTr49wCPJGrKuqE5O34h0R3vGaYqvcjAxeNXlPC+k7biZNPnC7+l1EA7eYBNPKVjG5QxPuLgchH51GP79YetP/JCbxNDnOb5voIE7ya0kjvYboTSCInvsBGvocG5odGmrpkR2QsgF6vmvos0nGZw21X8TKCClrELOvGvq+UYK7SSwEnYIiKesSs0EtjXSl6dZSGi6WOr5ei3I7kTnRrd34LpYOSdmQmL1ANn6IpQlSRr+8mYpsiK9MyY672lbx4acZ2WgXKp84059C+gvZsAqVrt0ycWPsN7JhNHicrIPus6PBMJu0hPAcWOHM/giNKVr6zDAWI4i/nmw15nhs85kAAAAASUVORK5CYII=)](https://opensecurity.in/#support) @@ -101,7 +98,7 @@ docker run -it --rm -p 8000:8000 opensecurity/mobile-security-framework-mobsf:la * [Dominik Schlecht](https://github.com/sn0b4ll) ![germany](https://user-images.githubusercontent.com/4301109/37564176-743238ba-2ab6-11e8-9666-5d98f0a1d127.png) -## Honorable Contributors +## Honorable Contributors & Shoutouts * Amrutha VC - For the new MobSF logo * Dominik Schlecht - For the awesome work on adding Windows Phone App Static Analysis to MobSF @@ -111,9 +108,6 @@ docker run -it --rm -p 8000:8000 opensecurity/mobile-security-framework-mobsf:la * Abhinav Saxena - (@xandfury) - For Travis CI and Logging integration * ![netguru](https://user-images.githubusercontent.com/4301109/76340877-a3dc4f00-62d2-11ea-8631-b4cc8d9e42ed.png) [Netguru](https://www.netguru.com/) (@karolpiateknet, @mtbrzeski) - For iOS Swift support, Rule contributions and SAST refactoring. * Maxime Fawe - (@Arenash13) - For Matching Strategy implementation of SAST pattern matching algorithms. - -## Shoutouts - * Abhinav Sejpal (@Abhinav_Sejpal) - For poking me with bugs, feature requests, and UI & UX suggestions * Anant Srivastava (@anantshri) - For Activity Tester Idea * Anto Joseph (@antojoseph) - For the help with SuperSU diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index ae09bc7b8f..0000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,39 +0,0 @@ -version: '3.8' -services: - postgres: - image: "postgres:latest" - restart: always - volumes: - - $HOME/MobSF/postgresql_data:/var/lib/postgresql - environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=password - - POSTGRES_DB=mobsf - ports: - - "5432:5432" - networks: - - mobsf - - mobsf: - environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=password - - POSTGRES_DB=mobsf - - POSTGRES_HOST=postgres - build: - context: . - dockerfile: Dockerfile - args: - - POSTGRES=True - volumes: - - $HOME/MobSF/mobsf_data:/home/mobsf/.MobSF - ports: - - "8000:8000" - networks: - - mobsf - depends_on: - - postgres - links: - - "postgres" -networks: - mobsf: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000000..e79255df71 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,91 @@ +services: + + postgres: + image: "postgres:13" + restart: always + deploy: + resources: + limits: + memory: 1G + reservations: + memory: 512M + volumes: + - $HOME/MobSF/postgresql_data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + - POSTGRES_DB=mobsf + networks: + - mobsf_network + + nginx: + image: nginx:latest + restart: always + deploy: + resources: + limits: + memory: 1G + reservations: + memory: 256M + ports: + - "80:4000" + - "1337:4001" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + depends_on: + - mobsf + networks: + - mobsf_network + + djangoq: + image: opensecurity/mobile-security-framework-mobsf:latest + build: + context: .. + dockerfile: Dockerfile + restart: unless-stopped + command: /home/mobsf/Mobile-Security-Framework-MobSF/scripts/qcluster.sh + volumes: + - $HOME/MobSF/mobsf_data:/home/mobsf/.MobSF + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + - POSTGRES_DB=mobsf + - POSTGRES_HOST=postgres + - POSTGRES_PORT=5432 + depends_on: + - postgres + networks: + - mobsf_network + + mobsf: + image: opensecurity/mobile-security-framework-mobsf:latest + build: + context: .. + dockerfile: Dockerfile + restart: always + tty: true + volumes: + - $HOME/MobSF/mobsf_data:/home/mobsf/.MobSF + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + - POSTGRES_DB=mobsf + - POSTGRES_HOST=postgres + - POSTGRES_PORT=5432 + - MOBSF_ASYNC_ANALYSIS=1 + healthcheck: + test: curl -f http://localhost:8000/login/ || exit 1 + interval: 30s + timeout: 10s + retries: 5 + depends_on: + - postgres + - djangoq + networks: + - mobsf_network + extra_hosts: + - "host.docker.internal:host-gateway" + +networks: + mobsf_network: + driver: bridge diff --git a/docker/docker-compose_swarm.yml b/docker/docker-compose_swarm.yml new file mode 100644 index 0000000000..0d98aa8a79 --- /dev/null +++ b/docker/docker-compose_swarm.yml @@ -0,0 +1,51 @@ +services: + + postgres: + image: "postgres:${POSTGRES_VERSION:-17.0-bookworm}" + restart: always + volumes: + - $HOME/MobSF/postgresql_data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD_FILE=/run/secrets/mobsfDB_password + - POSTGRES_DB=mobsf + networks: + - mobsf_network + secrets: + - mobsfDB_password + + mobsf: + image: ${MOBSF_IMAGE:-opensecurity/mobile-security-framework-mobsf:latest} + restart: always + ports: + - "8000:8000" + - "1337:1337" + volumes: + - $HOME/MobSF/mobsf_data:/home/mobsf/.MobSF + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD_FILE=/run/secrets/mobsfDB_password + - POSTGRES_DB=mobsf + - POSTGRES_HOST=postgres + - POSTGRES_PORT=5432 + - MOBSF_API_KEY_FILE=/run/secrets/mobsf_api_key + healthcheck: + test: curl -f http://localhost:8000/login/ || exit 1 + depends_on: + - postgres + networks: + - mobsf_network + extra_hosts: + - "host.docker.internal:host-gateway" + secrets: + - mobsfDB_password + - mobsf_api_key + +networks: + mobsf_network: + +secrets: + mobsfDB_password: + external: true + mobsf_api_key: + external: true diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000000..6f3a1713ae --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,42 @@ +user nginx; +events { + worker_connections 1000; +} + +http { + client_max_body_size 256M; + upstream mobsf_upstream { + server mobsf:8000; + server mobsf:1337; + keepalive 16; + } + + map $server_port $forwarded_port { + 4000 443; + 4001 443; + } + + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $forwarded_port; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_redirect off; + proxy_buffering on; + + server { + listen 4000; + location / { + proxy_pass http://mobsf:8000; + proxy_read_timeout 900; + client_max_body_size 256M; + } + } + + server { + listen 4001; + location / { + proxy_pass http://mobsf:1337; + proxy_read_timeout 120; + client_max_body_size 10M; + } + } +} diff --git a/mobsf/DynamicAnalyzer/tools/apk_patcher.py b/mobsf/DynamicAnalyzer/tools/apk_patcher.py index 56ef7ee810..0dd803798b 100644 --- a/mobsf/DynamicAnalyzer/tools/apk_patcher.py +++ b/mobsf/DynamicAnalyzer/tools/apk_patcher.py @@ -79,7 +79,7 @@ def download_frida_gadget(self, frida_arch, aarch, version): return None try: response = requests.get(f'{settings.FRIDA_SERVER}{version}', - timeout=3, + timeout=5, proxies=proxies, verify=verify) for item in response.json()['assets']: @@ -90,6 +90,7 @@ def download_frida_gadget(self, frida_arch, aarch, version): return None logger.info('Downloading frida-gadget %s', fgadget) with requests.get(url, + timeout=5, stream=True, proxies=proxies, verify=verify) as r: diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/default/debugger_check_bypass.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/default/debugger_check_bypass.js index aa50c92d3d..8ae94fa651 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/default/debugger_check_bypass.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/default/debugger_check_bypass.js @@ -196,3 +196,22 @@ Java.perform(function() { } } catch(e){} }) + +/* React Native JailMonkey Detection Bypass */ + +Java.perform(function() { + try{ + let hook = Java.use("com.gantix.JailMonkey.JailMonkeyModule")['isDevelopmentSettingsMode']; + if (hook) { + hook.overload("com.facebook.react.bridge.Promise").implementation = function(p) { + p.resolve(Java.use("java.lang.Boolean").$new(false)); + } + } + let hook2 = Java.use("com.gantix.JailMonkey.JailMonkeyModule")['isDebuggedMode']; + if (hook2) { + hook2.overload("com.facebook.react.bridge.Promise").implementation = function(p) { + p.resolve(Java.use("java.lang.Boolean").$new(false)); + } + } + } catch(e){} +}); \ No newline at end of file diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/default/root_bypass.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/default/root_bypass.js index 3ca264049f..3defe9e745 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/default/root_bypass.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/default/root_bypass.js @@ -163,7 +163,6 @@ Java.performNow(function () { } } catch (err) { send('[RootDetection Bypass] Error ' + className + '.' + classMethod + err); - return } try { @@ -183,7 +182,6 @@ Java.performNow(function () { } } catch (err) { send('[RootDetection Bypass] Error ' + className + '.' + classMethod + err); - return } try { className = 'android.security.keystore.KeyInfo' @@ -203,7 +201,6 @@ Java.performNow(function () { } } catch (err) { send('[RootDetection Bypass] Error ' + className + '.' + classMethod + err); - return } // Native Root Check Bypass @@ -257,4 +254,53 @@ Java.performNow(function () { int execvpe(const char *file, char *const argv[], char *const envp[]); */ -}); \ No newline at end of file +}); +Java.perform(function() { + // Bypassing Root in React Native JailMonkey + // Source: https://codeshare.frida.re/@RohindhR/react-native-jail-monkey-bypass-all-checks/ + try { + let toHook = Java.use('com.gantix.JailMonkey.JailMonkeyModule')['getConstants']; + toHook.implementation = function() { + var hashmap = this.getConstants(); + hashmap.put('isJailBroken', Java.use("java.lang.Boolean").$new(false)); + hashmap.put('hookDetected', Java.use("java.lang.Boolean").$new(false)); + hashmap.put('canMockLocation', Java.use("java.lang.Boolean").$new(false)); + hashmap.put('isOnExternalStorage', Java.use("java.lang.Boolean").$new(false)); + hashmap.put('AdbEnabled', Java.use("java.lang.Boolean").$new(false)); + return hashmap; + } + } catch (err) {} + try{ + // Bypassing Rooted Check + let hook = Java.use('com.gantix.JailMonkey.Rooted.RootedCheck')['getResultByDetectionMethod'] + hook.implementation = function() { + let map = this.getResultByDetectionMethod(); + map.put("jailMonkey", Java.use("java.lang.Boolean").$new(false)); + return map; + } + + } catch (err) {} + try{ + // Bypassing Root detection method's result of RootBeer library + var className = 'com.gantix.JailMonkey.Rooted.RootedCheck$RootBeerResults'; + let toHook = Java.use(className)['isJailBroken']; + toHook.implementation = function() { + return false; + }; + + let toHook2 = Java.use(className)['toNativeMap'] + toHook2.implementation = function() { + var map = this.toNativeMap.call(this); + map.put("detectRootManagementApps", Java.use("java.lang.Boolean").$new(false)); + map.put("detectPotentiallyDangerousApps", Java.use("java.lang.Boolean").$new(false)); + map.put("checkForSuBinary", Java.use("java.lang.Boolean").$new(false)); + map.put("checkForDangerousProps", Java.use("java.lang.Boolean").$new(false)); + map.put("checkForRWPaths", Java.use("java.lang.Boolean").$new(false)); + map.put("detectTestKeys", Java.use("java.lang.Boolean").$new(false)); + map.put("checkSuExists", Java.use("java.lang.Boolean").$new(false)); + map.put("checkForRootNative", Java.use("java.lang.Boolean").$new(false)); + map.put("checkForMagiskBinary", Java.use("java.lang.Boolean").$new(false)); + return map; + }; + } catch (err) {} +}) \ No newline at end of file diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/default/ssl_pinning_bypass.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/default/ssl_pinning_bypass.js index 7793712af1..723381bf99 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/default/ssl_pinning_bypass.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/default/ssl_pinning_bypass.js @@ -241,15 +241,82 @@ Java.perform(function() { } catch (err) { send('[SSL Pinning Bypass] Cronet not found'); } - /* Certificate Transparency Bypass - Ajin Abraham - opensecurity.in */ - try{ + /* Boye AbstractVerifier */ + try { + Java.use("ch.boye.httpclientandroidlib.conn.ssl.AbstractVerifier").verify.implementation = function(host, ssl) { + send("[SSL Pinning Bypass] Bypassing Boye AbstractVerifier" + host); + }; + } catch (err) { + send("[SSL Pinning Bypass] Boye AbstractVerifier not found"); + } + /* Appmattus */ + try { + /* Certificate Transparency Bypass Ajin Abraham - opensecurity.in */ Java.use('com.babylon.certificatetransparency.CTInterceptorBuilder').includeHost.overload('java.lang.String').implementation = function(host) { send('[SSL Pinning Bypass] Bypassing Certificate Transparency check'); return this.includeHost('nonexistent.domain'); }; + } catch (err) { + send('[SSL Pinning Bypass] babylon certificatetransparency.CTInterceptorBuilder not found'); + } + try { + Java.use("com.appmattus.certificatetransparency.internal.verifier.CertificateTransparencyInterceptor")["intercept"].implementation = function(a) { + send("[SSL Pinning Bypass] Appmattus Certificate Transparency"); + return a.proceed(a.request()); + }; + } catch (err) { + send("[SSL Pinning Bypass] Appmattus CertificateTransparencyInterceptor not found"); + } + try{ + bypassOkHttp3CertificateTransparency(); } catch (err) { send('[SSL Pinning Bypass] certificatetransparency.CTInterceptorBuilder not found'); } - }, 0); + + +function bypassOkHttp3CertificateTransparency() { + // https://gist.github.com/m-rey/f2a235123908ca42395b6d3c5fe1128e + var CertificateTransparencyInterceptor = Java.use('com.appmattus.certificatetransparency.internal.verifier.CertificateTransparencyInterceptor'); + var OkHttpClientBuilder = Java.use('okhttp3.OkHttpClient$Builder'); + + CertificateTransparencyInterceptor.intercept.implementation = function (chain) { + var request = chain.request(); + var url = request.url(); + var host = url.host(); + + // Dynamically access the VerificationResult classes + var VerificationResult = Java.use('com.appmattus.certificatetransparency.VerificationResult'); + var VerificationResultSuccessInsecureConnection = Java.use('com.appmattus.certificatetransparency.VerificationResult$Success$InsecureConnection'); + var VerificationResultFailureNoCertificates = Java.use('com.appmattus.certificatetransparency.VerificationResult$Failure$NoCertificates'); + + // Create instances of the desired VerificationResult classes + var success = VerificationResultSuccessInsecureConnection.$new(host); + var failureNoCertificates = VerificationResultFailureNoCertificates.$new(); + + // Bypass certificate transparency verification + var certs = chain.connection().handshake().peerCertificates(); + if (certs.length === 0) { + send('[SSL Pinning Bypass] Certificate transparency bypassed.'); + return failureNoCertificates; + } + + try { + // Proceed with the original request + return chain.proceed(request); + } catch (e) { + // Catch SSLPeerUnverifiedException and return intercepted response + if (e.toString().includes('SSLPeerUnverifiedException')) { + send('[SSL Pinning Bypass] Certificate transparency failed.'); + return failureNoCertificates; + } + throw e; + } + }; + + OkHttpClientBuilder.build.implementation = function () { + // Intercept the OkHttpClient creation + var client = this.build(); + return client; + }; +} \ No newline at end of file diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/audit-webview.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/audit-webview.js new file mode 100644 index 0000000000..c9e0b2810a --- /dev/null +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/audit-webview.js @@ -0,0 +1,53 @@ +Java.perform(function () { + send("Starting WebView configuration dump..."); + + const WebView = Java.use('android.webkit.WebView'); + + // Hook the first overload: loadUrl(String) + WebView.loadUrl.overload('java.lang.String').implementation = function (url) { + send("[+] WebView.loadUrl(String) called: " + url); + + // Dump WebSettings after loading a URL + dumpWebSettingsSafely(this); + + // Call the original method + this.loadUrl(url); + }; + + // Hook the second overload: loadUrl(String, Map) + WebView.loadUrl.overload('java.lang.String', 'java.util.Map').implementation = function (url, additionalHttpHeaders) { + send("[+] WebView.loadUrl(String, Map) called: " + url); + send(" Additional HTTP Headers: " + additionalHttpHeaders); + + // Dump WebSettings after loading a URL + dumpWebSettingsSafely(this); + + // Call the original method + this.loadUrl(url, additionalHttpHeaders); + }; + + function dumpWebSettingsSafely(webView) { + try { + const webSettings = webView.getSettings(); + send("\n[+] Dumping WebSettings:"); + + // Security-sensitive settings + send(" JavaScript Enabled: " + webSettings.getJavaScriptEnabled()); + send(" Allow File Access: " + webSettings.getAllowFileAccess()); + send(" Allow Content Access: " + webSettings.getAllowContentAccess()); + send(" Mixed Content Mode: " + webSettings.getMixedContentMode()); + send(" Safe Browsing Enabled: " + webSettings.getSafeBrowsingEnabled()); + send(" Dom Storage Enabled: " + webSettings.getDomStorageEnabled()); + send(" Allow Universal Access From File URLs: " + webSettings.getAllowUniversalAccessFromFileURLs()); + send(" Allow File Access From File URLs: " + webSettings.getAllowFileAccessFromFileURLs()); + // Caching and storage + send(" Cache Mode: " + webSettings.getCacheMode()); + // User agent and other information + send(" User Agent String: " + webSettings.getUserAgentString()); + } catch (err) { + send("Error while dumping WebView configuration: " + err); + } + } + + send("Hooks installed for WebView."); +}); diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/detect-ssl-pinning.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/detect-ssl-pinning.js new file mode 100644 index 0000000000..d5efa7a525 --- /dev/null +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/detect-ssl-pinning.js @@ -0,0 +1,20 @@ +try { + var UnverifiedCertError = Java.use('javax.net.ssl.SSLPeerUnverifiedException'); + UnverifiedCertError.$init.implementation = function(str) { + send('Unexpected SSLPeerUnverifiedException occurred'); + try { + var stackTrace = Java.use('java.lang.Thread').currentThread().getStackTrace(); + var exceptionStackIndex = stackTrace.findIndex(stack => stack.getClassName() === "javax.net.ssl.SSLPeerUnverifiedException"); + var callingFunctionStack = stackTrace[exceptionStackIndex + 1]; + var className = callingFunctionStack.getClassName(); + var methodName = callingFunctionStack.getMethodName(); + var callingClass = Java.use(className); + var callingMethod = callingClass[methodName]; + send('SSL exception caused: ' + className + '.' + methodName + '. Patch this method to bypass pinning.'); + if (callingMethod.implementation) { + return; + } + } catch (e) {} + return this.$init(str); + }; +} catch (err) {} diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/dump-intent.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/dump-intent.js index 189c6dffbf..2fd465e47f 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/dump-intent.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/dump-intent.js @@ -1,21 +1,93 @@ -// https://gist.github.com/bet4it/b62ac2d5bd45b8cb699905fa498baf5e Java.perform(function () { - var act = Java.use("android.app.Activity"); - act.getIntent.overload().implementation = function () { - var intent = this.getIntent() - var cp = intent.getComponent() - send("[Intent Dumper] Starting " + cp.getPackageName() + "/" + cp.getClassName()) - var ext = intent.getExtras(); - if (ext) { - var keys = ext.keySet() - var iterator = keys.iterator() - while (iterator.hasNext()) { - var k = iterator.next().toString() - var v = ext.get(k) - send("\t" + v.getClass().getName()) - send("\t" + k + ' : ' + v.toString()) - } + var Activity = Java.use("android.app.Activity"); + + Activity.getIntent.overload().implementation = function () { + var intent = this.getIntent(); + var component = intent.getComponent(); + + send("[Intent Dumper] Captured Intent for Activity:"); + + // Component (target package and class) + if (component) { + send(" Component:"); + send(" Package: " + component.getPackageName()); + send(" Class: " + component.getClassName()); + } else { + send(" Component: None"); } - return intent; - }; - }) \ No newline at end of file + + // Action + var action = intent.getAction(); + send(" Action: " + (action ? action : "None")); + + // Data URI + var dataUri = intent.getDataString(); + send(" Data URI: " + (dataUri ? dataUri : "None")); + + // Flags + var flags = intent.getFlags(); + send(" Flags: " + flags); + + // Dumping extras in the Intent + var extras = intent.getExtras(); + if (extras) { + send(" Extras:"); + var iterator = extras.keySet().iterator(); + while (iterator.hasNext()) { + var key = iterator.next(); + var value = extras.get(key); + if (value !== null) { + send(" " + key + " (" + value.getClass().getName() + "): " + valueToString(value)); + } + } + } else { + send(" Extras: None"); + } + + return intent; + }; + + // Helper function to convert intent extras to a readable string + function valueToString(value) { + var valueType = value.getClass().getName(); + + if (valueType === "android.os.Bundle") { + return bundleToString(Java.cast(value, Java.use("android.os.Bundle"))); + } else if (valueType === "java.lang.String") { + return '"' + value + '"'; + } else if (valueType === "java.lang.Integer" || valueType === "java.lang.Float" || valueType === "java.lang.Boolean") { + return value.toString(); + } else if (valueType === "java.util.ArrayList") { + return arrayListToString(Java.cast(value, Java.use("java.util.ArrayList"))); + } else { + send("Unsupported extra type for key. Type: " + valueType); + return value.toString(); + } + } + + // Function to handle nested Bundles + function bundleToString(bundle) { + var result = "{"; + var iterator = bundle.keySet().iterator(); + while (iterator.hasNext()) { + var key = iterator.next(); + var value = bundle.get(key); + result += key + ": " + (value !== null ? valueToString(value) : "null") + ", "; + } + result = result.slice(0, -2); // Remove trailing comma and space + result += "}"; + return result; + } + + // Function to handle ArrayLists (if any) + function arrayListToString(arrayList) { + var result = "["; + for (var i = 0; i < arrayList.size(); i++) { + var item = arrayList.get(i); + result += valueToString(item) + ", "; + } + result = result.slice(0, -2); // Remove trailing comma and space + result += "]"; + return result; + } +}); diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/ssl-pinning-bypass.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/ssl-pinning-bypass.js index 42f380076c..0b97998426 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/ssl-pinning-bypass.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/ssl-pinning-bypass.js @@ -720,69 +720,70 @@ function dynamicPatching() { return null; } } - try { - var UnverifiedCertError = Java.use('javax.net.ssl.SSLPeerUnverifiedException'); - UnverifiedCertError.$init.implementation = function(str) { - console.log('[!] Unexpected SSLPeerUnverifiedException occurred, trying to patch it dynamically...!'); - try { - var stackTrace = Java.use('java.lang.Thread').currentThread().getStackTrace(); - var exceptionStackIndex = stackTrace.findIndex(stack => stack.getClassName() === "javax.net.ssl.SSLPeerUnverifiedException"); - var callingFunctionStack = stackTrace[exceptionStackIndex + 1]; - var className = callingFunctionStack.getClassName(); - var methodName = callingFunctionStack.getMethodName(); - var callingClass = Java.use(className); - var callingMethod = callingClass[methodName]; - console.log('[!] Attempting to bypass uncommon SSL Pinning method on: ' + className + '.' + methodName + '!'); - if (callingMethod.implementation) { - return; - } - var returnTypeName = callingMethod.returnType.type; - callingMethod.implementation = function() { - rudimentaryFix(returnTypeName); - }; - } catch (e) { - if (String(e).includes(".overload")) { - var splittedList = String(e).split(".overload"); - for (let i = 2; i < splittedList.length; i++) { - var extractedOverload = splittedList[i].trim().split("(")[1].slice(0, -1).replaceAll("'", ""); - if (extractedOverload.includes(",")) { - var argList = extractedOverload.split(", "); - console.log('[!] Attempting overload of ' + className + '.' + methodName + ' with arguments: ' + extractedOverload + '!'); - if (argList.length == 2) { - callingMethod.overload(argList[0], argList[1]).implementation = function(a, b) { - rudimentaryFix(returnTypeName); - } - } else if (argNum == 3) { - callingMethod.overload(argList[0], argList[1], argList[2]).implementation = function(a, b, c) { - rudimentaryFix(returnTypeName); - } - } else if (argNum == 4) { - callingMethod.overload(argList[0], argList[1], argList[2], argList[3]).implementation = function(a, b, c, d) { - rudimentaryFix(returnTypeName); - } - } else if (argNum == 5) { - callingMethod.overload(argList[0], argList[1], argList[2], argList[3], argList[4]).implementation = function(a, b, c, d, e) { - rudimentaryFix(returnTypeName); - } - } else if (argNum == 6) { - callingMethod.overload(argList[0], argList[1], argList[2], argList[3], argList[4], argList[5]).implementation = function(a, b, c, d, e, f) { - rudimentaryFix(returnTypeName); - } - } - } else { - callingMethod.overload(extractedOverload).implementation = function(a) { - rudimentaryFix(returnTypeName); - } - } - } - } else { - console.log('[-] Failed to dynamically patch SSLPeerUnverifiedException ' + e + '!'); - } - } - return this.$init(str); - }; - } catch (err) {} + // try { + // var UnverifiedCertError = Java.use('javax.net.ssl.SSLPeerUnverifiedException'); + // UnverifiedCertError.$init.implementation = function(str) { + // console.log('[!] Unexpected SSLPeerUnverifiedException occurred, trying to patch it dynamically...!'); + // try { + // var stackTrace = Java.use('java.lang.Thread').currentThread().getStackTrace(); + // var exceptionStackIndex = stackTrace.findIndex(stack => stack.getClassName() === "javax.net.ssl.SSLPeerUnverifiedException"); + // var callingFunctionStack = stackTrace[exceptionStackIndex + 1]; + // var className = callingFunctionStack.getClassName(); + // var methodName = callingFunctionStack.getMethodName(); + // var callingClass = Java.use(className); + // var callingMethod = callingClass[methodName]; + // console.log('[!] Attempting to bypass uncommon SSL Pinning method on: ' + className + '.' + methodName + '!'); + // if (callingMethod.implementation) { + // return; + // } + // var returnTypeName = callingMethod.returnType.type; + // callingMethod.implementation = function() { + // rudimentaryFix(returnTypeName); + // }; + // } catch (e) { + // if (String(e).includes(".overload")) { + // var splittedList = String(e).split(".overload"); + // for (let i = 2; i < splittedList.length; i++) { + // var extractedOverload = splittedList[i].trim().split("(")[1].slice(0, -1).replaceAll("'", ""); + // if (extractedOverload.includes(",")) { + // var argList = extractedOverload.split(", "); + // console.log('[!] Attempting overload of ' + className + '.' + methodName + ' with arguments: ' + extractedOverload + '!'); + // if (argList.length == 2) { + // callingMethod.overload(argList[0], argList[1]).implementation = function(a, b) { + // rudimentaryFix(returnTypeName); + // } + // } else if (argNum == 3) { + // callingMethod.overload(argList[0], argList[1], argList[2]).implementation = function(a, b, c) { + // rudimentaryFix(returnTypeName); + // } + // } else if (argNum == 4) { + // callingMethod.overload(argList[0], argList[1], argList[2], argList[3]).implementation = function(a, b, c, d) { + // rudimentaryFix(returnTypeName); + // } + // } else if (argNum == 5) { + // callingMethod.overload(argList[0], argList[1], argList[2], argList[3], argList[4]).implementation = function(a, b, c, d, e) { + // rudimentaryFix(returnTypeName); + // } + // } else if (argNum == 6) { + // callingMethod.overload(argList[0], argList[1], argList[2], argList[3], argList[4], argList[5]).implementation = function(a, b, c, d, e, f) { + // rudimentaryFix(returnTypeName); + // } + // } + // } else { + // callingMethod.overload(extractedOverload).implementation = function(a) { + // rudimentaryFix(returnTypeName); + // } + // } + // } + // } else { + // console.log('[-] Failed to dynamically patch SSLPeerUnverifiedException ' + e + '!'); + // } + // } + // return this.$init(str); + // }; + // } catch (err) {} } + setTimeout(function() { Java.perform(function() { var X509TrustManager = Java.use('javax.net.ssl.X509TrustManager'); diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/trace-intent.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/trace-intent.js new file mode 100644 index 0000000000..5495153105 --- /dev/null +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/trace-intent.js @@ -0,0 +1,78 @@ +Java.perform(function () { + // Hook the startActivity method in the Activity class + var Activity = Java.use("android.app.Activity"); + + Activity.startActivity.overload("android.content.Intent").implementation = function (intent) { + send("Intercepted startActivity with Intent:"); + + // Dump the Intent details + dumpIntent(intent); + + // Call the original startActivity method to ensure normal behavior + this.startActivity(intent); + }; + + // Function to dump intent details + function dumpIntent(intent) { + // Action + var action = intent.getAction(); + send(" Action: " + (action ? action : "None")); + + // Data URI + var dataUri = intent.getDataString(); + send(" Data URI: " + (dataUri ? dataUri : "None")); + + // Component (target package and class) + var component = intent.getComponent(); + if (component) { + send(" Component:"); + send(" Package: " + component.getPackageName()); + send(" Class: " + component.getClassName()); + } else { + send(" Component: None"); + } + + // Flags + var flags = intent.getFlags(); + send(" Flags: " + flags); + + // Extras + var extras = intent.getExtras(); + if (extras) { + send(" Extras:"); + var iterator = extras.keySet().iterator(); + while (iterator.hasNext()) { + var key = iterator.next(); + var value = extras.get(key); + if (value !== null) { + send(" " + key + ": " + valueToString(value)); + } + } + } else { + send(" Extras: None"); + } + } + + // Helper function to convert intent extras to string for logging + function valueToString(value) { + // Check if the value is a Bundle and handle it accordingly + if (value.getClass().getName() === "android.os.Bundle") { + return bundleToString(Java.cast(value, Java.use("android.os.Bundle"))); + } + return value.toString(); + } + + // Function to handle nested Bundles (if any) + function bundleToString(bundle) { + var result = "{"; + var iterator = bundle.keySet().iterator(); + while (iterator.hasNext()) { + var key = iterator.next(); + var value = bundle.get(key); + result += key + ": " + (value !== null ? value.toString() : "null") + ", "; + } + result = result.slice(0, -2); // Remove trailing comma and space + result += "}"; + return result; + } +}); diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/trace-javascript-interface.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/trace-javascript-interface.js new file mode 100644 index 0000000000..3905230e1a --- /dev/null +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/trace-javascript-interface.js @@ -0,0 +1,25 @@ +Java.perform(function () { + send("Starting JavaScript bridge enumeration..."); + + // Hook the WebView class + const WebView = Java.use('android.webkit.WebView'); + + // Hook the addJavascriptInterface method + WebView.addJavascriptInterface.overload('java.lang.Object', 'java.lang.String').implementation = function (obj, interfaceName) { + send("[+] addJavascriptInterface called"); + send(" Interface Name: " + interfaceName); + send(" Methods exposed:"); + + // Reflect on the object to enumerate methods + const objectClass = obj.getClass(); + const methods = objectClass.getDeclaredMethods(); + for (let i = 0; i < methods.length; i++) { + send(" - " + methods[i].getName()); + } + + // Call the original method + this.addJavascriptInterface(obj, interfaceName); + }; + + send("Hook installed for WebView.addJavascriptInterface."); +}); diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/string-compare.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/string-compare.js index 6d4997ac55..4869ba7a8c 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/string-compare.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/string-compare.js @@ -2,8 +2,9 @@ function captureStringCompare() { send('Capturing string comparisons') Interceptor.attach(ObjC.classes.__NSCFString['- isEqualToString:'].implementation, { onEnter: function (args) { + var src = new ObjC.Object(ptr(args[0])).toString() var str = new ObjC.Object(ptr(args[2])).toString() - send('[AUXILIARY] [__NSCFString isEqualToString:] -> '+ str); + send('[AUXILIARY] [__NSCFString isEqualToString:] -> \nstring 1: '+ src + '\nstring 2: '+ str); } }); } @@ -11,8 +12,9 @@ function captureStringCompare() { function captureStringCompare2(){ Interceptor.attach(ObjC.classes.NSTaggedPointerString['- isEqualToString:'].implementation, { onEnter: function (args) { + var src = new ObjC.Object(ptr(args[0])).toString() var str = new ObjC.Object(ptr(args[2])).toString() - send('[AUXILIARY] NSTaggedPointerString[- isEqualToString:] -> '+ str); + send('[AUXILIARY] NSTaggedPointerString[- isEqualToString:] -> \nstring 1: '+ src + '\nstring 2: '+ str); } }); } diff --git a/mobsf/DynamicAnalyzer/tools/webproxy.py b/mobsf/DynamicAnalyzer/tools/webproxy.py index 8292b7cf38..390c1f31b5 100644 --- a/mobsf/DynamicAnalyzer/tools/webproxy.py +++ b/mobsf/DynamicAnalyzer/tools/webproxy.py @@ -27,8 +27,11 @@ def stop_httptools(url): http_proxy = url.replace('https://', 'http://') headers = {'httptools': 'kill'} url = 'http://127.0.0.1' - requests.get(url, headers=headers, proxies={ - 'http': http_proxy}) + requests.get( + url, + timeout=5, + headers=headers, + proxies={'http': http_proxy}) logger.info('Killing httptools Proxy') except Exception: pass @@ -66,8 +69,8 @@ def create_ca(): def get_ca_file(): """Get CA Dir.""" - from mitmproxy import ctx - ca_dir = Path(ctx.mitmproxy.options.CONF_DIR).expanduser() + from mitmproxy import options + ca_dir = Path(options.CONF_DIR).expanduser() ca_file = ca_dir / 'mitmproxy-ca-cert.pem' if not ca_file.exists(): create_ca() diff --git a/mobsf/DynamicAnalyzer/views/android/analysis.py b/mobsf/DynamicAnalyzer/views/android/analysis.py index 7b10630034..4ec84bbddb 100644 --- a/mobsf/DynamicAnalyzer/views/android/analysis.py +++ b/mobsf/DynamicAnalyzer/views/android/analysis.py @@ -44,6 +44,7 @@ def run_analysis(apk_dir, md5_hash, package): log_line = log_line.split(clip_tag2)[1] clipboard.append(log_line) urls, domains, emails = extract_urls_domains_emails( + md5_hash, data['traffic'].lower()) # Tar dump and fetch files all_files = get_app_files(apk_dir, package) @@ -202,7 +203,7 @@ def generate_download(apk_dir, md5_hash, download_dir, package): if is_file_exists(fd_logs): shutil.copyfile(fd_logs, dfd_logs) try: - shutil.copytree(sshot, dsshot) + shutil.copytree(sshot, dsshot, dirs_exist_ok=True) except Exception: pass if is_file_exists(web): diff --git a/mobsf/DynamicAnalyzer/views/android/dynamic_analyzer.py b/mobsf/DynamicAnalyzer/views/android/dynamic_analyzer.py index d753cbe05e..ae0f576dea 100755 --- a/mobsf/DynamicAnalyzer/views/android/dynamic_analyzer.py +++ b/mobsf/DynamicAnalyzer/views/android/dynamic_analyzer.py @@ -34,15 +34,25 @@ get_proxy_ip, is_md5, print_n_send_error_response, + python_dict, python_list, strict_package_check, ) from mobsf.MobSF.views.scanning import add_to_recent_scan from mobsf.StaticAnalyzer.models import StaticAnalyzerAndroid +from mobsf.MobSF.views.authentication import ( + login_required, +) +from mobsf.MobSF.views.authorization import ( + Permissions, + permission_required, +) logger = logging.getLogger(__name__) +@login_required +@permission_required(Permissions.SCAN) def android_dynamic_analysis(request, api=False): """Android Dynamic Analysis Entry point.""" try: @@ -104,11 +114,14 @@ def android_dynamic_analysis(request, api=False): return print_n_send_error_response(request, exp, api) +@login_required +@permission_required(Permissions.SCAN) def dynamic_analyzer(request, checksum, api=False): """Android Dynamic Analyzer Environment.""" try: identifier = None activities = None + deeplinks = None exported_activities = None if api: reinstall = request.POST.get('re_install', '1') @@ -144,9 +157,11 @@ def dynamic_analyzer(request, checksum, api=False): static_android_db.EXPORTED_ACTIVITIES) activities = python_list( static_android_db.ACTIVITIES) + deeplinks = python_dict( + static_android_db.BROWSABLE_ACTIVITIES) except ObjectDoesNotExist: logger.warning( - 'Failed to get Activities. ' + 'Failed to get Activities/Deeplinks. ' 'Static Analysis not completed for the app.') env = Environment(identifier) if not env.connect_n_mount(): @@ -207,6 +222,7 @@ def dynamic_analyzer(request, checksum, api=False): 'version': settings.MOBSF_VER, 'activities': activities, 'exported_activities': exported_activities, + 'deeplinks': deeplinks, 'title': 'Dynamic Analyzer'} template = 'dynamic_analysis/android/dynamic_analyzer.html' if api: @@ -220,6 +236,8 @@ def dynamic_analyzer(request, checksum, api=False): api) +@login_required +@permission_required(Permissions.SCAN) def httptools_start(request): """Start httprools UI.""" logger.info('Starting httptools Web UI') @@ -241,6 +259,8 @@ def httptools_start(request): return print_n_send_error_response(request, err) +@login_required +@permission_required(Permissions.SCAN) def logcat(request, api=False): logger.info('Starting Logcat streaming') try: @@ -284,6 +304,8 @@ def read_process(): return print_n_send_error_response(request, err, api) +@login_required +@permission_required(Permissions.SCAN) def trigger_static_analysis(request, checksum): """On device APK Static Analysis.""" try: @@ -303,16 +325,19 @@ def trigger_static_analysis(request, checksum): err = 'Cannot connect to Android Runtime' return print_n_send_error_response(request, err) env = Environment(identifier) - apk_file = env.get_apk(checksum, package) - if not apk_file: + scan_type = env.get_apk(checksum, package) + if not scan_type: err = 'Failed to download APK file' return print_n_send_error_response(request, err) + file_name = f'{package}.apk' + if scan_type == 'apks': + file_name = f'{file_name}s' data = { 'analyzer': 'static_analyzer', 'status': 'success', 'hash': checksum, - 'scan_type': 'apk', - 'file_name': f'{package}.apk', + 'scan_type': scan_type, + 'file_name': file_name, } add_to_recent_scan(data) return HttpResponseRedirect(f'/static_analyzer/{checksum}/') diff --git a/mobsf/DynamicAnalyzer/views/android/environment.py b/mobsf/DynamicAnalyzer/views/android/environment.py index c7fcd3e7bf..340990d4de 100644 --- a/mobsf/DynamicAnalyzer/views/android/environment.py +++ b/mobsf/DynamicAnalyzer/views/android/environment.py @@ -7,6 +7,7 @@ import tempfile import threading import time +from pathlib import Path from base64 import b64encode from hashlib import md5 @@ -368,7 +369,7 @@ def get_environment(self): return 'emulator' elif (b'genymotion' in out.lower() or any(char.isdigit() for char in ver)): - logger.info('Found Genymotion x86 Android VM') + logger.info('Found Genymotion Android VM') return 'genymotion' elif b'corellium' in out: logger.info('Found Corellium ARM Android VM') @@ -436,31 +437,71 @@ def get_device_packages(self): device_packages[md5] = (pkg, apk) return device_packages - def get_apk(self, checksum, package): - """Download APK from device.""" - try: - out_dir = os.path.join(settings.UPLD_DIR, checksum + '/') - if not os.path.exists(out_dir): - os.makedirs(out_dir) - out_file = os.path.join(out_dir, f'{checksum}.apk') - if is_file_exists(out_file): - return out_file + def download_apk_packages(self, pkg_path, out_file): + """Download APK package(s).""" + with tempfile.TemporaryDirectory() as temp_dir: + # Download APK package(s) + # Can be single or multiple packages out = self.adb_command([ - 'pm', - 'path', - package], True) - out = out.decode('utf-8').rstrip() - path = out.split('package:', 1)[1].strip() - logger.info('Downloading APK') - self.adb_command([ 'pull', - path, - out_file, + pkg_path.as_posix(), + temp_dir, ]) - if is_file_exists(out_file): - return out_file + fmt = out.decode('utf-8').strip() + logger.info('ADB Pull Output: %s', fmt) + # Filter for APK files in the directory + apk_files = [] + for f in Path(temp_dir).glob('*.apk'): + if f.is_file(): + apk_files.append(f) + # Check if there is exactly one APK file + if len(apk_files) == 1: + shutil.move(apk_files[0], out_file) + return 'apk' + else: + # If there are multiple APK files, zip them + shutil.make_archive(out_file, 'zip', root_dir=temp_dir, base_dir='.') + # Rename the zip file to APK + apks_file = out_file.with_suffix('.apk') + os.rename(out_file.as_posix() + '.zip', apks_file) + return 'apks' + + def get_apk_packages(self, package): + """Get all APK packages from device.""" + out = self.adb_command([ + 'pm', + 'path', + package], True) + return out.decode('utf-8').strip() + + def get_apk_parent_directory(self, package): + """Get parent directory of APK packages.""" + package_out = self.get_apk_packages(package) + package_out = package_out.split() + if ('package:' in package_out[0] + and package_out[0].endswith('.apk')): + path = package_out[0].split('package:', 1)[1].strip() + return Path(path).parent + return False + + def get_apk(self, checksum, package): + """Download APK from device.""" + try: + # Do not download if already exists + out_dir = Path(settings.UPLD_DIR) / checksum + out_dir.mkdir(parents=True, exist_ok=True) + out_file = out_dir / f'{checksum}.apk' + if out_file.exists(): + return 'apk' + # Get APK package parent directory + pkg_path = self.get_apk_parent_directory(package) + if pkg_path: + # Download APK package(s) + logger.info('Downloading APK') + return self.download_apk_packages(pkg_path, out_file) except Exception: - return False + logger.exception('Failed to download APK') + return False def system_check(self, runtime): """Check if /system is writable.""" @@ -494,6 +535,7 @@ def system_check(self, runtime): 'MobSF documentation!') return False except Exception: + logger.exception('System check failed') logger.error(err_msg) return False return True @@ -652,7 +694,7 @@ def frida_setup(self): elif arch == 'x86_64': frida_arch = 'x86_64' else: - logger.error('Make sure a Genymotion Android x86 VM' + logger.error('Make sure a Genymotion Android VM' ' or Android Studio Emulator' ' instance is running') return diff --git a/mobsf/DynamicAnalyzer/views/android/frida_core.py b/mobsf/DynamicAnalyzer/views/android/frida_core.py index 01d21c77c9..21cc1d76fd 100644 --- a/mobsf/DynamicAnalyzer/views/android/frida_core.py +++ b/mobsf/DynamicAnalyzer/views/android/frida_core.py @@ -228,6 +228,8 @@ def api_handler(self, api): loaded_class_methods = [] implementations = [] try: + if not self.extras: + return raction = self.extras.get('rclass_action') rclass = self.extras.get('rclass_name') rclass_pattern = self.extras.get('rclass_pattern') diff --git a/mobsf/DynamicAnalyzer/views/android/frida_server_download.py b/mobsf/DynamicAnalyzer/views/android/frida_server_download.py index d808c05776..3765572fc1 100644 --- a/mobsf/DynamicAnalyzer/views/android/frida_server_download.py +++ b/mobsf/DynamicAnalyzer/views/android/frida_server_download.py @@ -30,13 +30,17 @@ def clean_up_old_binaries(dirc, version): pass -def download_frida_server(url, version, fname): +def download_frida_server(url, version, fname, proxies): """Download frida-server-binary.""" try: download_dir = Path(settings.DWD_DIR) logger.info('Downloading binary %s', fname) dwd_loc = download_dir / fname - with requests.get(url, stream=True) as r: + with requests.get( + url, + timeout=5, + proxies=proxies, + stream=True) as r: with LZMAFile(r.raw) as f: with open(dwd_loc, 'wb') as flip: copyfileobj(f, flip) @@ -62,13 +66,13 @@ def update_frida_server(arch, version): logger.exception('[ERROR] Setting upstream proxy') try: response = requests.get(f'{settings.FRIDA_SERVER}{version}', - timeout=3, + timeout=5, proxies=proxies, verify=verify) for item in response.json()['assets']: if item['name'] == f'{fserver}.xz': url = item['browser_download_url'] - return download_frida_server(url, version, fserver) + return download_frida_server(url, version, fserver, proxies) return False except Exception: logger.exception('[ERROR] Fetching Frida Server Release') diff --git a/mobsf/DynamicAnalyzer/views/android/operations.py b/mobsf/DynamicAnalyzer/views/android/operations.py index 8451635a12..53911d1b3e 100644 --- a/mobsf/DynamicAnalyzer/views/android/operations.py +++ b/mobsf/DynamicAnalyzer/views/android/operations.py @@ -29,6 +29,13 @@ is_number, ) from mobsf.StaticAnalyzer.models import StaticAnalyzerAndroid +from mobsf.MobSF.views.authentication import ( + login_required, +) +from mobsf.MobSF.views.authorization import ( + Permissions, + permission_required, +) logger = logging.getLogger(__name__) @@ -53,6 +60,8 @@ def get_package_name(checksum): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def mobsfy(request, api=False): """Configure Instance for Dynamic Analysis.""" @@ -87,6 +96,8 @@ def mobsfy(request, api=False): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def execute_adb(request, api=False): """Execute ADB Commands.""" @@ -115,6 +126,8 @@ def execute_adb(request, api=False): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def get_component(request): """Get Android Component.""" @@ -135,6 +148,8 @@ def get_component(request): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def run_apk(request): """Run Android APK.""" @@ -157,6 +172,8 @@ def run_apk(request): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def take_screenshot(request, api=False): """Take Screenshot.""" @@ -186,6 +203,8 @@ def take_screenshot(request, api=False): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def screen_cast(request): """ScreenCast.""" @@ -205,6 +224,8 @@ def screen_cast(request): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def touch(request): """Sending Touch/Swipe/Text Events.""" @@ -261,6 +282,8 @@ def touch(request): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def mobsf_ca(request, api=False): """Install and Remove MobSF Proxy RootCA.""" @@ -284,6 +307,8 @@ def mobsf_ca(request, api=False): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def global_proxy(request, api=False): """Set/unset global proxy.""" diff --git a/mobsf/DynamicAnalyzer/views/android/report.py b/mobsf/DynamicAnalyzer/views/android/report.py index dd841f7b04..6c779b6182 100644 --- a/mobsf/DynamicAnalyzer/views/android/report.py +++ b/mobsf/DynamicAnalyzer/views/android/report.py @@ -29,12 +29,15 @@ key, print_n_send_error_response, ) - +from mobsf.MobSF.views.authentication import ( + login_required, +) logger = logging.getLogger(__name__) register.filter('key', key) +@login_required def view_report(request, checksum, api=False): """Dynamic Analysis Report Generation.""" logger.info('Dynamic Analysis Report Generation') @@ -69,7 +72,7 @@ def view_report(request, checksum, api=False): deps = dependency_analysis(package, app_dir) analysis_result = run_analysis(app_dir, checksum, package) domains = analysis_result['domains'] - trk = Trackers.Trackers(app_dir, tools_dir) + trk = Trackers.Trackers(checksum, app_dir, tools_dir) trackers = trk.get_trackers_domains_or_deps(domains, deps) generate_download(app_dir, checksum, download_dir, package) images = get_screenshots(checksum, download_dir) diff --git a/mobsf/DynamicAnalyzer/views/android/tests_common.py b/mobsf/DynamicAnalyzer/views/android/tests_common.py index 2925de356f..b57581474c 100644 --- a/mobsf/DynamicAnalyzer/views/android/tests_common.py +++ b/mobsf/DynamicAnalyzer/views/android/tests_common.py @@ -28,18 +28,30 @@ stop_httptools, ) from mobsf.MobSF.utils import ( + cmd_injection_check, is_md5, python_list, ) from mobsf.StaticAnalyzer.models import StaticAnalyzerAndroid +from mobsf.MobSF.views.authentication import ( + login_required, +) +from mobsf.MobSF.views.authorization import ( + Permissions, + permission_required, +) logger = logging.getLogger(__name__) # AJAX + + +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def start_activity(request, api=False): - """Lunch a specific activity.""" + """Launch a specific activity.""" try: env = Environment() activity = request.POST['activity'] @@ -66,8 +78,37 @@ def start_activity(request, api=False): data = {'status': 'failed', 'message': str(exp)} return send_response(data, api) +# AJAX + + +@login_required +@permission_required(Permissions.SCAN) +@require_http_methods(['POST']) +def start_deeplink(request, api=False): + """Launch a specific deeplink.""" + try: + env = Environment() + url = request.POST['url'] + md5_hash = request.POST['hash'] + + valid_md5 = is_md5(md5_hash) + if cmd_injection_check(url) or not valid_md5: + return invalid_params(api) + env.adb_command( + ['am', 'start', '-a', + 'android.intent.action.VIEW', + '-d', url], True) + data = {'status': 'ok'} + except Exception as exp: + logger.exception('Start Activity') + data = {'status': 'failed', 'message': str(exp)} + return send_response(data, api) # AJAX + + +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def activity_tester(request, api=False): """Exported & non exported activity Tester.""" @@ -129,6 +170,8 @@ def activity_tester(request, api=False): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def download_data(request, api=False): """Download Application Data from Device.""" @@ -164,6 +207,8 @@ def download_data(request, api=False): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def collect_logs(request, api=False): """Collecting Data and Cleanup.""" @@ -208,6 +253,8 @@ def collect_logs(request, api=False): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def tls_tests(request, api=False): """Perform TLS tests.""" diff --git a/mobsf/DynamicAnalyzer/views/android/tests_frida.py b/mobsf/DynamicAnalyzer/views/android/tests_frida.py index 05f438e132..0fda8e3068 100644 --- a/mobsf/DynamicAnalyzer/views/android/tests_frida.py +++ b/mobsf/DynamicAnalyzer/views/android/tests_frida.py @@ -27,12 +27,21 @@ print_n_send_error_response, strict_package_check, ) +from mobsf.MobSF.views.authentication import ( + login_required, +) +from mobsf.MobSF.views.authorization import ( + Permissions, + permission_required, +) logger = logging.getLogger(__name__) # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def get_runtime_dependencies(request, api=False): """Get App runtime dependencies.""" @@ -56,12 +65,14 @@ def get_runtime_dependencies(request, api=False): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def instrument(request, api=False): """Instrument app with frida.""" data = { 'status': 'failed', - 'message': 'Failed to instrument app'} + 'message': ''} try: action = request.POST.get('frida_action', 'spawn') pid = request.POST.get('pid') @@ -123,6 +134,7 @@ def instrument(request, api=False): return send_response(data, api) +@login_required def live_api(request, api=False): try: if api: diff --git a/mobsf/DynamicAnalyzer/views/common/device.py b/mobsf/DynamicAnalyzer/views/common/device.py index f674b4d281..2340f4cd3a 100644 --- a/mobsf/DynamicAnalyzer/views/common/device.py +++ b/mobsf/DynamicAnalyzer/views/common/device.py @@ -8,6 +8,9 @@ from django.shortcuts import render from django.utils.html import escape +from mobsf.MobSF.views.authentication import ( + login_required, +) from mobsf.MobSF.utils import ( is_md5, is_path_traversal, @@ -16,14 +19,15 @@ read_sqlite, ) -from biplist import ( - writePlistToString, +from plistlib import ( + FMT_XML, + dumps, ) - logger = logging.getLogger(__name__) +@login_required def view_file(request, api=False): """View File in app data directory.""" logger.info('Viewing File') @@ -53,7 +57,7 @@ def view_file(request, api=False): return print_n_send_error_response(request, err, api) dat = sfile.read_text('ISO-8859-1') if fil.endswith('.plist') and dat.startswith('bplist0'): - dat = writePlistToString(dat).decode('utf-8', 'ignore') + dat = dumps(dat, fmt=FMT_XML).decode('utf-8', 'ignore') if fil.endswith(('.xml', '.plist')) and typ in ['xml', 'plist']: rtyp = 'xml' elif typ == 'db': diff --git a/mobsf/DynamicAnalyzer/views/common/frida.py b/mobsf/DynamicAnalyzer/views/common/frida.py index 1f426a24da..d398f705d4 100644 --- a/mobsf/DynamicAnalyzer/views/common/frida.py +++ b/mobsf/DynamicAnalyzer/views/common/frida.py @@ -17,6 +17,9 @@ is_safe_path, print_n_send_error_response, ) +from mobsf.MobSF.views.authentication import ( + login_required, +) logger = logging.getLogger(__name__) @@ -25,6 +28,7 @@ # AJAX +@login_required @require_http_methods(['POST']) def list_frida_scripts(request, api=False): """List frida scripts from others.""" @@ -47,6 +51,7 @@ def list_frida_scripts(request, api=False): # AJAX +@login_required @require_http_methods(['POST']) def get_script(request, api=False): """Get frida scripts from others.""" @@ -77,6 +82,7 @@ def get_script(request, api=False): # AJAX + HTML +@login_required def frida_logs(request, api=False): try: data = { diff --git a/mobsf/DynamicAnalyzer/views/common/shared.py b/mobsf/DynamicAnalyzer/views/common/shared.py index 6bd3f26fdd..a84bc49758 100644 --- a/mobsf/DynamicAnalyzer/views/common/shared.py +++ b/mobsf/DynamicAnalyzer/views/common/shared.py @@ -3,6 +3,7 @@ import logging import os import re +import errno import json import tarfile import shutil @@ -23,7 +24,7 @@ logger = logging.getLogger(__name__) -def extract_urls_domains_emails(data): +def extract_urls_domains_emails(checksum, data): """Extract URLs, Domains and Emails.""" # URL Extraction urls = re.findall(URL_REGEX, data.lower()) @@ -32,8 +33,10 @@ def extract_urls_domains_emails(data): else: urls = [] # Domain Extraction and Malware Check - logger.info('Performing Malware Check on extracted Domains') - domains = MalwareDomainCheck().scan(urls) + logger.info('Performing Malware check on extracted domains') + domains = MalwareDomainCheck().scan( + checksum, + urls) # Email Etraction Regex emails = set() for email in EMAIL_REGEX.findall(data.lower()): @@ -52,18 +55,35 @@ def safe_paths(tar_meta): yield fh +def onerror(func, path, exc_info): + _, exc, _ = exc_info + if exc.errno == errno.EACCES: # Permission error + try: + os.chmod(path, 0o755) + func(path) + except Exception: + pass + elif exc.errno == errno.ENOTEMPTY: # Directory not empty + try: + func(path) + except Exception: + pass + else: + raise + + def untar_files(tar_loc, untar_dir): """Untar files.""" logger.info('Extracting Tar files') - # Extract Device Data - if not tar_loc.exists(): - return False - if untar_dir.exists(): - # fix for permission errors - shutil.rmtree(untar_dir) - else: - os.makedirs(untar_dir) try: + # Extract Device Data + if not tar_loc.exists(): + return False + if untar_dir.exists(): + # fix for permission errors + shutil.rmtree(untar_dir, onerror=onerror) + else: + os.makedirs(untar_dir) with tarfile.open(tar_loc.as_posix(), errorlevel=1) as tar: def is_within_directory(directory, target): @@ -140,7 +160,7 @@ def send_response(data, api=False): return data return HttpResponse( json.dumps(data), - content_type='application/json') + content_type='application/json; charset=utf-8') def invalid_params(api=False): diff --git a/mobsf/DynamicAnalyzer/views/ios/analysis.py b/mobsf/DynamicAnalyzer/views/ios/analysis.py index 550e604fb6..070b848834 100644 --- a/mobsf/DynamicAnalyzer/views/ios/analysis.py +++ b/mobsf/DynamicAnalyzer/views/ios/analysis.py @@ -46,7 +46,9 @@ def run_analysis(app_dir, bundle_id, checksum): domains = {} # Collect Log data data = get_logs_data(app_dir, bundle_id) - urls, domains, emails = extract_urls_domains_emails(data) + urls, domains, emails = extract_urls_domains_emails( + checksum, + data) # App data files analysis pfiles = get_app_files(app_dir, f'{checksum}-app-container') analysis_result['sqlite'] = pfiles['sqlite'] diff --git a/mobsf/DynamicAnalyzer/views/ios/corellium_apis.py b/mobsf/DynamicAnalyzer/views/ios/corellium_apis.py index 70790bfaec..ecfc80cf6d 100644 --- a/mobsf/DynamicAnalyzer/views/ios/corellium_apis.py +++ b/mobsf/DynamicAnalyzer/views/ios/corellium_apis.py @@ -29,6 +29,7 @@ settings, 'CORELLIUM_API_KEY', '') logger = logging.getLogger(__name__) +TIMEOUT = 20 class CorelliumInit: @@ -54,6 +55,7 @@ def api_ready(self): """Check API Availability.""" try: r = requests.get(f'{self.api}/ready', + timeout=TIMEOUT, proxies=self.proxies, verify=self.verify) if r.status_code in SUCCESS_RESP: @@ -73,6 +75,7 @@ def api_auth(self): return False r = requests.get( f'{self.api}/projects', + timeout=TIMEOUT, headers=self.headers, proxies=self.proxies, verify=self.verify) @@ -89,6 +92,7 @@ def get_projects(self): ids = [] r = requests.get( f'{self.api}/projects?ids_only=true', + timeout=TIMEOUT, headers=self.headers, proxies=self.proxies, verify=self.verify) @@ -104,6 +108,7 @@ def get_authorized_keys(self): """Get SSH public keys associated with a project.""" r = requests.get( f'{self.api}/projects/{self.project_id}/keys', + timeout=TIMEOUT, headers=self.headers, proxies=self.proxies, verify=self.verify) @@ -124,6 +129,7 @@ def add_authorized_key(self, key): } r = requests.post( f'{self.api}/projects/{self.project_id}/keys', + timeout=TIMEOUT, headers=self.headers, json=data, proxies=self.proxies, @@ -149,6 +155,7 @@ def get_instances(self): instances = [] r = requests.get( f'{self.api}/instances', + timeout=TIMEOUT, headers=self.headers, proxies=self.proxies, verify=self.verify) @@ -168,6 +175,7 @@ def create_ios_instance(self, name, flavor, version): } r = requests.post( f'{self.api}/instances', + timeout=TIMEOUT, headers=self.headers, json=data, proxies=self.proxies, @@ -182,6 +190,7 @@ class CorelliumModelsAPI(CorelliumInit): def get_models(self): r = requests.get( f'{self.api}/models', + timeout=TIMEOUT, headers=self.headers, proxies=self.proxies, verify=self.verify) @@ -202,6 +211,7 @@ def get_supported_os(self, model): return False r = requests.get( f'{self.api}/models/{model}/software', + timeout=TIMEOUT, headers=self.headers, proxies=self.proxies, verify=self.verify) @@ -223,6 +233,7 @@ def start_instance(self): data = {'paused': False} r = requests.post( f'{self.api}/instances/{self.instance_id}/start', + timeout=TIMEOUT, headers=self.headers, json=data, proxies=self.proxies, @@ -238,6 +249,7 @@ def stop_instance(self): data = {'soft': True} r = requests.post( f'{self.api}/instances/{self.instance_id}/stop', + timeout=TIMEOUT, headers=self.headers, json=data, proxies=self.proxies, @@ -252,6 +264,7 @@ def unpause_instance(self): """Unpause instance.""" r = requests.post( f'{self.api}/instances/{self.instance_id}/unpause', + timeout=TIMEOUT, headers=self.headers, proxies=self.proxies, verify=self.verify) @@ -265,6 +278,7 @@ def reboot_instance(self): """Reboot instance.""" r = requests.post( f'{self.api}/instances/{self.instance_id}/reboot', + timeout=TIMEOUT, headers=self.headers, proxies=self.proxies, verify=self.verify) @@ -291,6 +305,7 @@ def poll_instance(self): """Check instance status.""" r = requests.get( f'{self.api}/instances/{self.instance_id}', + timeout=TIMEOUT, headers=self.headers, proxies=self.proxies, verify=self.verify) @@ -306,6 +321,7 @@ def screenshot(self): r = requests.get( (f'{self.api}/instances/{self.instance_id}' '/screenshot.png?scale=1'), + timeout=TIMEOUT, headers=self.headers, stream=True, proxies=self.proxies, @@ -322,6 +338,7 @@ def start_network_capture(self): """Start network capture.""" r = requests.post( f'{self.api}/instances/{self.instance_id}/sslsplit/enable', + timeout=TIMEOUT, headers=self.headers, proxies=self.proxies, verify=self.verify) @@ -338,6 +355,7 @@ def stop_network_capture(self): """Stop network capture.""" r = requests.post( f'{self.api}/instances/{self.instance_id}/sslsplit/disable', + timeout=TIMEOUT, headers=self.headers, proxies=self.proxies, verify=self.verify) @@ -351,6 +369,7 @@ def download_network_capture(self): """Download network capture.""" r = requests.get( f'{self.api}/instances/{self.instance_id}/networkMonitor.pcap', + timeout=TIMEOUT, headers=self.headers, proxies=self.proxies, verify=self.verify) @@ -364,6 +383,7 @@ def console_log(self): """Get Console Log.""" r = requests.get( f'{self.api}/instances/{self.instance_id}/consoleLog', + timeout=TIMEOUT, headers=self.headers, proxies=self.proxies, verify=self.verify) @@ -377,6 +397,7 @@ def get_ssh_connection_string(self): """Get SSH connection string.""" r = requests.get( f'{self.api}/instances/{self.instance_id}/quickConnectCommand', + timeout=TIMEOUT, headers=self.headers, proxies=self.proxies, verify=self.verify) @@ -464,6 +485,7 @@ def device_input(self, event, x, y, max_x, max_y): {'buttons': [], 'wait': 100}] r = requests.post( f'{self.api}/instances/{self.instance_id}/input', + timeout=TIMEOUT, headers=self.headers, json=data, proxies=self.proxies, @@ -485,6 +507,7 @@ def agent_ready(self): """Agent ready.""" r = requests.get( f'{self.api}/instances/{self.instance_id}/agent/v1/app/ready', + timeout=TIMEOUT, headers=self.headers, proxies=self.proxies, verify=self.verify) @@ -500,6 +523,7 @@ def unlock_device(self): """Unlock iOS device.""" r = requests.post( f'{self.api}/instances/{self.instance_id}/agent/v1/system/unlock', + timeout=TIMEOUT, headers=self.headers, proxies=self.proxies, verify=self.verify) @@ -533,6 +557,7 @@ def install_ipa(self): """Install IPA.""" r = requests.post( f'{self.api}/instances/{self.instance_id}/agent/v1/app/install', + timeout=TIMEOUT, headers=self.headers, json={'path': '/tmp/app.ipa'}, proxies=self.proxies, @@ -548,6 +573,7 @@ def run_app(self, bundle_id): r = requests.post( (f'{self.api}/instances/{self.instance_id}' f'/agent/v1/app/apps/{bundle_id}/run'), + timeout=TIMEOUT, headers=self.headers, proxies=self.proxies, verify=self.verify) @@ -562,6 +588,7 @@ def stop_app(self, bundle_id): r = requests.post( (f'{self.api}/instances/{self.instance_id}' f'/agent/v1/app/apps/{bundle_id}/kill'), + timeout=TIMEOUT, headers=self.headers, proxies=self.proxies, verify=self.verify) @@ -576,6 +603,7 @@ def remove_app(self, bundle_id): r = requests.post( (f'{self.api}/instances/{self.instance_id}' f'/agent/v1/app/apps/{bundle_id}/uninstall'), + timeout=TIMEOUT, headers=self.headers, proxies=self.proxies, verify=self.verify) @@ -589,6 +617,7 @@ def list_apps(self): """List all apps installed.""" r = requests.get( f'{self.api}/instances/{self.instance_id}/agent/v1/app/apps', + timeout=TIMEOUT, headers=self.headers, proxies=self.proxies, verify=self.verify) @@ -603,6 +632,7 @@ def get_icons(self, bundleids): r = requests.get( (f'{self.api}/instances/{self.instance_id}' f'/agent/v1/app/icons?{bundleids}'), + timeout=TIMEOUT, headers=self.headers, proxies=self.proxies, verify=self.verify) diff --git a/mobsf/DynamicAnalyzer/views/ios/corellium_instance.py b/mobsf/DynamicAnalyzer/views/ios/corellium_instance.py index 4dd4df2b5b..77a9c55922 100644 --- a/mobsf/DynamicAnalyzer/views/ios/corellium_instance.py +++ b/mobsf/DynamicAnalyzer/views/ios/corellium_instance.py @@ -47,7 +47,13 @@ CorelliumModelsAPI, OK, ) - +from mobsf.MobSF.views.authentication import ( + login_required, +) +from mobsf.MobSF.views.authorization import ( + Permissions, + permission_required, +) logger = logging.getLogger(__name__) @@ -55,6 +61,8 @@ # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def start_instance(request, api=False): """Start iOS VM instance.""" @@ -82,6 +90,8 @@ def start_instance(request, api=False): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def stop_instance(request, api=False): """Stop iOS VM instance.""" @@ -109,6 +119,8 @@ def stop_instance(request, api=False): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def unpause_instance(request, api=False): """Unpause iOS VM instance.""" @@ -136,6 +148,8 @@ def unpause_instance(request, api=False): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def reboot_instance(request, api=False): """Reboot iOS VM instance.""" @@ -163,6 +177,8 @@ def reboot_instance(request, api=False): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def destroy_instance(request, api=False): """Destroy iOS VM instance.""" @@ -190,6 +206,8 @@ def destroy_instance(request, api=False): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def list_apps(request, api=False): """List installed apps.""" @@ -251,6 +269,8 @@ def list_apps(request, api=False): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def get_supported_models(request, api=False): """Get Supported iOS VM models.""" @@ -269,6 +289,8 @@ def get_supported_models(request, api=False): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def get_supported_os(request, api=False): """Get Supported iOS OS versions.""" @@ -288,6 +310,8 @@ def get_supported_os(request, api=False): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def create_vm_instance(request, api=False): """Create and iOS VM in Corellium.""" @@ -380,6 +404,8 @@ def appsync_ipa_install(ssh_string): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def setup_environment(request, checksum, api=False): """Setup iOS Dynamic Analyzer Environment.""" @@ -434,6 +460,8 @@ def setup_environment(request, checksum, api=False): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def run_app(request, api=False): """Run an App.""" @@ -462,6 +490,8 @@ def run_app(request, api=False): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def stop_app(request, api=False): """Stop an App.""" @@ -490,6 +520,8 @@ def stop_app(request, api=False): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def remove_app(request, api=False): """Remove an app from the device.""" @@ -517,6 +549,8 @@ def remove_app(request, api=False): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def take_screenshot(request, api=False): """Take a Screenshot.""" @@ -553,6 +587,8 @@ def take_screenshot(request, api=False): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def get_container_path(request, api=False): """Get App Container path.""" @@ -579,6 +615,8 @@ def get_container_path(request, api=False): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def network_capture(request, api=False): """Enable/Disable Network Capture.""" @@ -614,6 +652,8 @@ def network_capture(request, api=False): # File Download +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['GET', 'POST']) def live_pcap_download(request, api=False): """Download Network Capture.""" @@ -649,6 +689,8 @@ def live_pcap_download(request, api=False): SSH_TARGET = None +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def ssh_execute(request, api=False): """Execute commands in VM over SSH.""" @@ -706,6 +748,8 @@ def download_app_data(ci, checksum): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def download_data(request, bundle_id, api=False): """Download Application Data from Device.""" @@ -756,6 +800,8 @@ def download_data(request, bundle_id, api=False): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def touch(request, api=False): """Sending Touch/Swipe/Text Events.""" @@ -792,6 +838,8 @@ def touch(request, api=False): # AJAX + HTML +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST', 'GET']) def system_logs(request, api=False): """Show system logs.""" @@ -830,6 +878,8 @@ def system_logs(request, api=False): # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def upload_file(request, api=False): """Upload file to device.""" @@ -859,6 +909,8 @@ def upload_file(request, api=False): # File Download +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def download_file(request, api=False): """Download file from device.""" diff --git a/mobsf/DynamicAnalyzer/views/ios/dynamic_analyzer.py b/mobsf/DynamicAnalyzer/views/ios/dynamic_analyzer.py index d6ba063317..2119ea8c74 100644 --- a/mobsf/DynamicAnalyzer/views/ios/dynamic_analyzer.py +++ b/mobsf/DynamicAnalyzer/views/ios/dynamic_analyzer.py @@ -32,10 +32,19 @@ from mobsf.DynamicAnalyzer.views.ios.corellium_ssh import ( ssh_jumphost_reverse_port_forward, ) +from mobsf.MobSF.views.authentication import ( + login_required, +) +from mobsf.MobSF.views.authorization import ( + Permissions, + permission_required, +) logger = logging.getLogger(__name__) +@login_required +@permission_required(Permissions.SCAN) def dynamic_analysis(request, api=False): """The iOS Dynamic Analysis Entry point.""" try: @@ -87,6 +96,8 @@ def dynamic_analysis(request, api=False): return print_n_send_error_response(request, exp, api) +@login_required +@permission_required(Permissions.SCAN) def dynamic_analyzer(request, api=False): """Dynamic Analyzer for in-device iOS apps.""" try: diff --git a/mobsf/DynamicAnalyzer/views/ios/frida_core.py b/mobsf/DynamicAnalyzer/views/ios/frida_core.py index 6c23aef8f7..01406c17a8 100644 --- a/mobsf/DynamicAnalyzer/views/ios/frida_core.py +++ b/mobsf/DynamicAnalyzer/views/ios/frida_core.py @@ -268,6 +268,8 @@ def api_handler(self, api): self.container_file.write_text(self.app_container) except frida.InvalidOperationError: pass + if not self.extras: + return raction = self.extras.get('rclass_action') rclass = self.extras.get('rclass_name') rclass_pattern = self.extras.get('rclass_pattern') diff --git a/mobsf/DynamicAnalyzer/views/ios/report.py b/mobsf/DynamicAnalyzer/views/ios/report.py index 3de93e52e1..6305db635a 100644 --- a/mobsf/DynamicAnalyzer/views/ios/report.py +++ b/mobsf/DynamicAnalyzer/views/ios/report.py @@ -23,6 +23,9 @@ replace, strict_package_check, ) +from mobsf.MobSF.views.authentication import ( + login_required, +) logger = logging.getLogger(__name__) @@ -32,6 +35,7 @@ register.filter('base64_decode', base64_decode) +@login_required def ios_view_report(request, bundle_id, api=False): """Dynamic Analysis Report Generation.""" logger.info('iOS Dynamic Analysis Report Generation') @@ -63,7 +67,7 @@ def ios_view_report(request, bundle_id, api=False): return print_n_send_error_response(request, msg, api) api_analysis = ios_api_analysis(app_dir) dump_analaysis = run_analysis(app_dir, bundle_id, checksum) - trk = Trackers.Trackers(app_dir, tools_dir) + trk = Trackers.Trackers(checksum, app_dir, tools_dir) trackers = trk.get_trackers_domains_or_deps( dump_analaysis['domains'], None) screenshots = get_screenshots(checksum, download_dir) diff --git a/mobsf/DynamicAnalyzer/views/ios/tests_frida.py b/mobsf/DynamicAnalyzer/views/ios/tests_frida.py index 17faf34aff..4ccf0bbaf1 100644 --- a/mobsf/DynamicAnalyzer/views/ios/tests_frida.py +++ b/mobsf/DynamicAnalyzer/views/ios/tests_frida.py @@ -22,12 +22,21 @@ is_md5, strict_package_check, ) +from mobsf.MobSF.views.authentication import ( + login_required, +) +from mobsf.MobSF.views.authorization import ( + Permissions, + permission_required, +) logger = logging.getLogger(__name__) # AJAX +@login_required +@permission_required(Permissions.SCAN) @require_http_methods(['POST']) def ios_instrument(request, api=False): """Instrument app with frida.""" diff --git a/mobsf/MalwareAnalyzer/views/MalwareDomainCheck.py b/mobsf/MalwareAnalyzer/views/MalwareDomainCheck.py index 212e3e5e2c..63ec4169af 100644 --- a/mobsf/MalwareAnalyzer/views/MalwareDomainCheck.py +++ b/mobsf/MalwareAnalyzer/views/MalwareDomainCheck.py @@ -15,6 +15,7 @@ import IP2Location from mobsf.MobSF.utils import ( + append_scan_status, is_internet_available, settings_enabled, update_local_db, @@ -34,27 +35,6 @@ def __init__(self): self.domainlist = None self.IP2Loc = IP2Location.IP2Location() - def update_malware_db(self): - """Check for update in malware DB.""" - try: - mal_db = self.malwaredomainlist - resp = update_local_db('Malware', settings.MALWARE_DB_URL, mal_db) - if not resp: - return - # DB needs update - # Check2: DB Syntax Changed - line = resp.decode('utf-8', 'ignore').split('\n')[0] - lst = line.split('",') - if len(lst) == 10: - # DB Format is not changed. Let's update DB - logger.info('Updating Malware Database') - with open(mal_db, 'wb') as wfp: - wfp.write(resp) - else: - logger.warning('Unable to Update Malware DB') - except Exception: - logger.exception('[ERROR] Malware DB Update') - def update_maltrail_db(self): """Check for update in maltrail DB.""" try: @@ -150,7 +130,7 @@ def malware_check(self): or details_dict['ip'].startswith(domain)): self.result[domain] = details_dict except Exception: - logger.exception('[ERROR] Performing Malware Check') + logger.exception('[ERROR] Performing Malware check') def maltrail_check(self): try: @@ -178,10 +158,12 @@ def update(self): logger.warning('Internet not available. ' 'Skipping Malware Database Update.') - def scan(self, urls): + def scan(self, checksum, urls): if not settings_enabled('DOMAIN_MALWARE_SCAN'): - logger.info('Domain Malware Check disabled in settings') + logger.info('Domain Malware check disabled in settings') return self.result + msg = 'Performing Malware check on extracted domains' + append_scan_status(checksum, msg) self.domainlist = get_domains(urls) if self.domainlist: self.update() diff --git a/mobsf/MalwareAnalyzer/views/Trackers.py b/mobsf/MalwareAnalyzer/views/Trackers.py index 7827425e9b..e826e9dc28 100644 --- a/mobsf/MalwareAnalyzer/views/Trackers.py +++ b/mobsf/MalwareAnalyzer/views/Trackers.py @@ -14,6 +14,7 @@ from tldextract import extract from mobsf.MobSF.utils import ( + append_scan_status, find_java_binary, is_file_exists, is_internet_available, @@ -24,7 +25,8 @@ class Trackers: - def __init__(self, apk_dir, tools_dir): + def __init__(self, checksum, apk_dir, tools_dir): + self.checksum = checksum self.apk = None self.apk_dir = apk_dir self.tracker_db = os.path.join( @@ -61,17 +63,28 @@ def _update_tracker_db(self): is_db_format_good = True if is_db_format_good: # DB Format is not changed. Let's update DB - logger.info('Updating Trackers Database....') + msg = 'Updating Trackers Database....' + logger.info(msg) + append_scan_status(self.checksum, msg) with open(self.tracker_db, 'wb') as wfp: wfp.write(resp) else: - logger.info('Trackers Database format from ' - 'reports.exodus-privacy.eu.org has changed.' - ' Database is not updated. ' - 'Please report to: https://github.com/MobSF/' - 'Mobile-Security-Framework-MobSF/issues') - except Exception: - logger.exception('[ERROR] Trackers DB Update') + desc = ( + 'Trackers Database format from ' + 'reports.exodus-privacy.eu.org has changed.' + ' Database is not updated. ' + 'Please report to: https://github.com/MobSF/' + 'Mobile-Security-Framework-MobSF/issues' + ) + logger.info(desc) + append_scan_status( + self.checksum, + 'Tracker Database format changed', + desc) + except Exception as exp: + msg = '[ERROR] Trackers DB Update' + logger.exception(msg) + append_scan_status(self.checksum, msg, repr(exp)) def _compile_signatures(self): """ @@ -131,7 +144,7 @@ def get_embedded_classes(self): and is_file_exists(settings.BACKSMALI_BINARY)): bs_path = settings.BACKSMALI_BINARY else: - bs_path = os.path.join(self.tools_dir, 'baksmali-2.5.2.jar') + bs_path = os.path.join(self.tools_dir, 'baksmali-3.0.8-dev-fat.jar') args = [find_java_binary(), '-jar', bs_path, 'list', 'classes', dex_file] try: @@ -214,7 +227,9 @@ def detect_runtime_trackers(self, items, deps=False): def get_trackers(self): """Get Trackers.""" - logger.info('Detecting Trackers') + msg = 'Detecting Trackers' + logger.info(msg) + append_scan_status(self.checksum, msg) trackers = self.detect_trackers() tracker_dict = {'detected_trackers': len(trackers), 'total_trackers': self.nb_trackers_signature, @@ -235,7 +250,9 @@ def get_trackers_domains_or_deps(self, domains, deps): 'detected_trackers': 0, 'total_trackers': 0, 'trackers': []} - logger.info('Detecting Trackers from Domains') + msg = 'Detecting Trackers from Domains' + logger.info(msg) + append_scan_status(self.checksum, msg) # Extract Trackers from Domains x_domains = set() for d in domains: @@ -244,7 +261,9 @@ def get_trackers_domains_or_deps(self, domains, deps): trackers = self.detect_runtime_trackers(x_domains) # Extract Trackers from Runtime dependencies if deps: - logger.info('Detecting Trackers from Runtime dependencies') + msg = 'Detecting Trackers from Runtime dependencies' + logger.info(msg) + append_scan_status(self.checksum, msg) runtime = self.detect_runtime_trackers(deps, True) for i in runtime: if i not in trackers: diff --git a/mobsf/MalwareAnalyzer/views/VirusTotal.py b/mobsf/MalwareAnalyzer/views/VirusTotal.py index 8924b003ee..30d75cc16d 100755 --- a/mobsf/MalwareAnalyzer/views/VirusTotal.py +++ b/mobsf/MalwareAnalyzer/views/VirusTotal.py @@ -5,6 +5,7 @@ from django.conf import settings from mobsf.MobSF.utils import ( + append_scan_status, file_size, get_config_loc, upstream_proxy, @@ -15,9 +16,15 @@ class VirusTotal: - base_url = settings.VIRUS_TOTAL_BASE_URL + API_KEY_ERROR = 'VirusTotal Permission denied, wrong api key?' + API_KEY_ERROR_SHORT = 'VirusTotal API error' + API_CONN_ERROR = 'VirusTotal Connection Error' - def get_report(self, file_hash): + def __init__(self, checksum): + self.base_url = settings.VIRUS_TOTAL_BASE_URL + self.checksum = checksum + + def get_report(self): """ Get Report from VT. @@ -25,6 +32,7 @@ def get_report(self, file_hash): :return: json response / None """ try: + file_hash = self.checksum url = self.base_url + 'report' params = { 'apikey': settings.VT_API_KEY, @@ -32,30 +40,41 @@ def get_report(self, file_hash): headers = {'Accept-Encoding': 'gzip, deflate'} try: proxies, verify = upstream_proxy('https') - except Exception: - logger.exception('Setting upstream proxy') + except Exception as exp: + msg = 'Setting upstream proxy' + logger.exception(msg) + append_scan_status(file_hash, msg, repr(exp)) try: response = requests.get( url, + timeout=5, params=params, headers=headers, proxies=proxies, verify=verify) if response.status_code == 403: - logger.error( - 'VirusTotal Permission denied, wrong api key?') + logger.error(self.API_KEY_ERROR) + append_scan_status( + file_hash, + self.API_KEY_ERROR, + self.API_KEY_ERROR_SHORT) return None - except Exception: - logger.error( - 'VirusTotal ConnectionError, check internet connectivity') + except Exception as exp: + logger.error(self.API_CONN_ERROR) + append_scan_status( + file_hash, + self.API_CONN_ERROR, + repr(exp)) return None try: json_response = response.json() return json_response except ValueError: return None - except Exception: - logger.exception('VirusTotal get_report') + except Exception as exp: + msg = 'Failed to get report from VirusTotal' + logger.exception(msg) + append_scan_status(file_hash, msg, repr(exp)) return None def upload_file(self, file_path): @@ -75,66 +94,87 @@ def upload_file(self, file_path): headers = {'apikey': settings.VT_API_KEY} try: proxies, verify = upstream_proxy('https') - except Exception: - logger.exception('Setting upstream proxy') + except Exception as exp: + msg = 'Setting upstream proxy' + logger.exception(msg) + append_scan_status(self.checksum, msg, repr(exp)) try: response = requests.post( url, + timeout=5, files=files, data=headers, proxies=proxies, verify=verify) if response.status_code == 403: - logger.error( - 'VirusTotal Permission denied, wrong api key?') + logger.error(self.API_KEY_ERROR) + append_scan_status( + self.checksum, + self.API_KEY_ERROR, + self.API_KEY_ERROR_SHORT) return None - except Exception: - logger.error( - 'VirusTotal Connection Error, check internet connectivity') + except Exception as exp: + logger.error(self.API_CONN_ERROR) + append_scan_status( + self.checksum, + self.API_CONN_ERROR, + repr(exp)) return None json_response = response.json() return json_response - except Exception: - logger.exception('VirusTotal upload_file') + except Exception as exp: + msg = 'Failed to upload file to VirusTotal' + logger.exception(msg) + append_scan_status(self.checksum, msg, repr(exp)) return None - def get_result(self, file_path, file_hash): + def get_result(self, file_path): """ Get Results from VT. Uploading a file and getting the approval msg from VT or fetching existing report :param file_path: file's path - :param file_hash: file's hash - md5/sha1/sha256 :return: VirusTotal result json / None upon error """ try: - logger.info('VirusTotal: Check for existing report') - report = self.get_report(file_hash) + file_hash = self.checksum + msg = 'VirusTotal: Check for existing report' + logger.info(msg) + append_scan_status(file_hash, msg) + report = self.get_report() # Check for existing report if report: if report['response_code'] == 1: - logger.info('VirusTotal: %s', report['verbose_msg']) + msg = f'VirusTotal: {report["verbose_msg"]}' + logger.info(msg) + append_scan_status(file_hash, msg) return report if settings.VT_UPLOAD: - logger.info('VirusTotal: file upload') + msg = 'VirusTotal: file upload' + logger.info(msg) + append_scan_status(file_hash, msg) upload_response = self.upload_file(file_path) if upload_response: - logger.info('VirusTotal: %s', - upload_response['verbose_msg']) + msg = f'VirusTotal: {upload_response["verbose_msg"]}' + logger.info(msg) + append_scan_status(file_hash, msg) return upload_response else: - logger.info('VirusTotal Scan not performed as file' - ' upload is disabled in %s. ' - 'To enable file upload, ' - 'set VT_UPLOAD to True.', get_config_loc()) - message = ('Scan not performed, VirusTotal file' - f' upload disabled in {get_config_loc()}') + msg = ('VirusTotal Scan not performed as file ' + f'upload is disabled in {get_config_loc()}. ' + 'To enable file upload, set VT_UPLOAD to True.') + logger.info(msg) + append_scan_status(file_hash, msg) + message = ('Scan not performed, VirusTotal file ' + f'upload disabled in {get_config_loc()}') report = { 'verbose_msg': message, 'positives': 0, 'total': 0} return report - except Exception: - logger.exception('VirusTotal get_result') + except Exception as exp: + msg = 'VirusTotal: Error getting result' + logger.exception(msg) + append_scan_status(file_hash, msg, repr(exp)) diff --git a/mobsf/MalwareAnalyzer/views/android/apkid.py b/mobsf/MalwareAnalyzer/views/android/apkid.py index f1fc324a2f..375e73a14c 100644 --- a/mobsf/MalwareAnalyzer/views/android/apkid.py +++ b/mobsf/MalwareAnalyzer/views/android/apkid.py @@ -1,27 +1,28 @@ # -*- coding: utf_8 -*- import logging -import os +from pathlib import Path from django.conf import settings from mobsf.MobSF.utils import ( + append_scan_status, + run_with_timeout, settings_enabled, ) logger = logging.getLogger(__name__) -def apkid_analysis(app_dir, apk_file, apk_name): +def apkid_analysis(checksum, apk_file): """APKID Analysis of DEX files.""" if not settings_enabled('APKID_ENABLED'): return {} try: import apkid - except ImportError: - logger.error('APKiD - Could not import APKiD') - return {} - if not os.path.exists(apk_file): - logger.error('APKiD - APK not found') + except ImportError as exp: + msg = 'APKiD - Could not import APKiD' + logger.error(msg) + append_scan_status(checksum, msg, repr(exp)) return {} apkid_ver = apkid.__version__ @@ -29,7 +30,9 @@ def apkid_analysis(app_dir, apk_file, apk_name): from apkid.output import OutputFormatter from apkid.rules import RulesManager - logger.info('Running APKiD %s', apkid_ver) + msg = f'Running APKiD {apkid_ver}' + logger.info(msg) + append_scan_status(checksum, msg) options = Options( timeout=30, verbose=False, @@ -44,22 +47,44 @@ def apkid_analysis(app_dir, apk_file, apk_name): ) rules = options.rules_manager.load() scanner = Scanner(rules, options) - res = scanner.scan_file(apk_file) + findings = {} + res = None + try: + res = run_with_timeout( + scanner.scan_file, + settings.BINARY_ANALYSIS_TIMEOUT, + apk_file) + except Exception as e: + msg = 'APKID scan timed out' + logger.error(msg) + append_scan_status( + checksum, + msg, + str(e)) try: - findings = output._build_json_output(res)['files'] + if res: + findings = output._build_json_output(res)['files'] except AttributeError: # apkid >= 2.0.3 try: - findings = output.build_json_output(res)['files'] + if res: + findings = output.build_json_output(res)['files'] except AttributeError: - logger.error('yara-python dependency required by ' - 'APKiD is not installed properly. ' - 'Skipping APKiD analysis!') - findings = {} + msg = ( + 'yara-python dependency required by ' + 'APKiD is not installed properly. ' + 'Skipping APKiD analysis!') + logger.error(msg) + append_scan_status( + checksum, + msg, + 'Missing dependency') sanitized = {} for item in findings: filename = item['filename'] if '!' in filename: filename = filename.split('!', 1)[1] + else: + filename = Path(filename).name sanitized[filename] = item['matches'] return sanitized diff --git a/mobsf/MalwareAnalyzer/views/android/behaviour_analysis.py b/mobsf/MalwareAnalyzer/views/android/behaviour_analysis.py new file mode 100644 index 0000000000..9425bf6bbf --- /dev/null +++ b/mobsf/MalwareAnalyzer/views/android/behaviour_analysis.py @@ -0,0 +1,32 @@ +# -*- coding: utf_8 -*- +"""Perform behaviour analysis.""" +import logging +from pathlib import Path + +from django.conf import settings + +from mobsf.MobSF.utils import ( + append_scan_status, +) + +logger = logging.getLogger(__name__) + + +def analyze(checksum, sast, data): + """Perform behaviour analysis.""" + try: + root = Path(settings.BASE_DIR) / 'MalwareAnalyzer' / 'views' + rules = root / 'android' / 'rules' / 'behaviour_rules.yaml' + msg = 'Android Behaviour Analysis Started' + logger.info(msg) + append_scan_status(checksum, msg) + behaviour_finds = sast.run_rules(data, rules.as_posix()) + msg = 'Android Behaviour Analysis Completed' + logger.info(msg) + append_scan_status(checksum, msg) + return behaviour_finds + except Exception as exp: + msg = 'Failed to perform behaviour analysis' + logger.exception(msg) + append_scan_status(checksum, msg, repr(exp)) + return {} diff --git a/mobsf/MalwareAnalyzer/views/android/permissions.py b/mobsf/MalwareAnalyzer/views/android/permissions.py index 137813e51c..cb4d5467e8 100644 --- a/mobsf/MalwareAnalyzer/views/android/permissions.py +++ b/mobsf/MalwareAnalyzer/views/android/permissions.py @@ -2,6 +2,10 @@ # Check against most common malware permissions. import logging +from mobsf.MobSF.utils import ( + append_scan_status, +) + TOP_MALWARE_PERMISSIONS = [ 'android.permission.ACCEPT_HANDOVER', @@ -28,6 +32,7 @@ 'android.permission.WRITE_EXTERNAL_STORAGE', 'android.permission.READ_EXTERNAL_STORAGE', 'android.permission.VIBRATE', + 'android.permission.REQUEST_INSTALL_PACKAGES', ] OTHER_PERMISSIONS = [ 'android.permission.ACCESS_BACKGROUND_LOCATION', @@ -67,7 +72,6 @@ 'android.permission.READ_CALENDAR', 'android.permission.PACKAGE_USAGE_STATS', 'android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS', - 'android.permission.REQUEST_INSTALL_PACKAGES', 'android.permission.WRITE_CONTACTS', 'android.permission.WRITE_SMS', 'com.android.launcher.permission.INSTALL_SHORTCUT', @@ -81,9 +85,11 @@ logger = logging.getLogger(__name__) -def check_malware_permission(perms): +def check_malware_permission(checksum, perms): """Check against most common malware permissions.""" - logger.info('Checking for Malware Permissions') + msg = 'Checking for Malware Permissions' + logger.info(msg) + append_scan_status(checksum, msg) malware_perms = [] other_perms = [] for permission in perms: diff --git a/mobsf/MalwareAnalyzer/views/android/quark.py b/mobsf/MalwareAnalyzer/views/android/quark.py deleted file mode 100644 index 976625feec..0000000000 --- a/mobsf/MalwareAnalyzer/views/android/quark.py +++ /dev/null @@ -1,140 +0,0 @@ -# -*- coding: utf_8 -*- -import logging -from pathlib import Path - -from mobsf.MobSF.utils import ( - disable_print, - enable_print, - settings_enabled, -) - -logger = logging.getLogger(__name__) - - -def quark_analysis(app_dir, apk_file): - """QUARK Analysis of APK files.""" - if not settings_enabled('QUARK_ENABLED'): - return [] - try: - import quark - except ImportError: - logger.error('Failed to import Quark') - return [] - if not Path(apk_file).exists(): - logger.error('APK not found') - return [] - - quark_ver = quark.__version__ - - from quark import config - from quark.freshquark import download - from quark.report import Report - - logger.info('Running Quark %s', quark_ver) - json_report = {} - try: - # freshquark: update quark rules - disable_print() - download() - enable_print() - - # default rules path - rules_dir = Path(f'{config.HOME_DIR}quark-rules/rules') - report = Report() - - # Analyze apk - report.analysis(apk_file, rules_dir) - - # Generate Report - json_report = report.get_report('json') - # Clear all functools.lru_cache from quark - report.quark.apkinfo.find_method.cache_clear() - report.quark.apkinfo.upperfunc.cache_clear() - report.quark.apkinfo.get_wrapper_smali.cache_clear() - except Exception: - logger.exception('Quark APK Analysis') - return _convert_report(json_report, app_dir) - - -def _convert_report(origin_report, app_dir): - new_report = [] - if not origin_report or not origin_report.get('crimes'): - logger.warning('Skipping Quark Analysis') - return new_report - for crime in origin_report.get('crimes'): - if not crime['confidence'] == '100%': - continue - - new_crime = {} - new_crime['crime'] = crime['crime'] - new_crime['score'] = crime['score'] - new_crime['weight'] = crime['weight'] - new_crime['confidence'] = crime['confidence'] - new_crime['permissions'] = crime['permissions'] - new_crime['register'] = [] - - for item in crime['register']: - source_code = {} - cls_and_md = next(iter(item)) - detail = item[cls_and_md] - - file_path = cls_and_md.split(' ')[0].replace(';', '.smali')[1:] - method = ''.join(cls_and_md.split(' ')[1:]) - source_code['file'] = file_path - source_code['method'] = method - source_code['first_api'] = detail['first'] - source_code['second_api'] = detail['second'] - - source_code_dir = Path(app_dir) / 'smali_source' / file_path - source_code['line_numbers'] = _get_line_numbers( - source_code_dir, - method, - detail['first'], - detail['second'], - ) - - new_crime['register'].append(source_code) - - new_report.append(new_crime) - - return new_report - - -def _get_line_numbers(source_code_path, method, first_api, second_api): - line_numbers = { - 'method_start': -1, - 'method_end': -1, - 'first_api': -1, - 'second_api': -1, - } - method_found = False - - try: - with open(source_code_path, 'r') as file: - for num, line in enumerate(file, 1): - if (not method_found - and line.startswith('.method') - and method in line): - line_numbers['method_start'] = num - method_found = True - elif method_found: - first_api_opcode = first_api[0] - first_api_method = first_api[-1].replace(' ', '') - if first_api_opcode in line and first_api_method in line: - line_numbers['first_api'] = num - - second_api_opcode = second_api[0] - second_api_method = second_api[-1].replace(' ', '') - if second_api_opcode in line and second_api_method in line: - line_numbers['second_api'] = num - - if line.startswith('.end method'): - line_numbers['method_end'] = num - return line_numbers - return line_numbers - except EnvironmentError: - return line_numbers - - except Exception: - logger.exception('Finding line numbers of method and apis') - return line_numbers diff --git a/mobsf/MalwareAnalyzer/views/android/rules/behaviour_rules.yaml b/mobsf/MalwareAnalyzer/views/android/rules/behaviour_rules.yaml new file mode 100644 index 0000000000..b9286337fe --- /dev/null +++ b/mobsf/MalwareAnalyzer/views/android/rules/behaviour_rules.yaml @@ -0,0 +1,2819 @@ +- id: 00019 + message: 'Find a method from given class name, usually for reflection' + metadata: + label: + - reflection + pattern: + - java\.lang\.Object + - java\.lang\.Class + - \.getMethod\( + - \.getClass\( + type: RegexAnd + severity: info + input_case: exact +- id: 00058 + message: Connect to the specific WIFI network + metadata: + label: + - wifi + - control + pattern: + - android\.net\.wifi\.WifiManager + - \.getConfiguredNetworks\( + - \.enableNetwork\( + type: RegexAnd + severity: info + input_case: exact +- id: '00166' + message: Get SMS message body and retrieve a string from it (possibly PIN / mTAN) + metadata: + label: + - sms + - pin + pattern: + - android\.telephony\.SmsMessage + - java\.util\.regex\.Pattern + - \.getMessageBody\( + - \.matcher\( + type: RegexAnd + severity: info + input_case: exact +- id: '00023' + message: Start another application from current application + metadata: + label: + - reflection + - control + pattern: + - android\.content\.pm\.PackageManager + - android\.content\.Context + - \.getLaunchIntentForPackage\( + - \.startActivity\( + type: RegexAnd + severity: info + input_case: exact +- id: 00189 + message: Get the content of a SMS message + metadata: + label: + - sms + pattern: + - android\.content\.ContentResolver + - android\.database\.Cursor + - \.query\( + - \.getColumnIndex\( + type: RegexAnd + severity: info + input_case: exact +- id: '00131' + message: Get location of the current GSM and put it into JSON + metadata: + label: + - collection + - location + pattern: + - android\.telephony\.gsm\.GsmCellLocation + - org\.json\.JSONObject + - \.put\( + - \.getLac\( + type: RegexAnd + severity: info + input_case: exact +- id: '00074' + message: Get IMSI and the ISO country code + metadata: + label: + - collection + - telephony + pattern: + - android\.telephony\.TelephonyManager + - \.getNetworkCountryIso\( + - \.getSubscriberId\( + type: RegexAnd + severity: info + input_case: exact +- id: '00127' + message: 'Monitor the broadcast action events (BOOT_COMPLETED, etc)' + metadata: + label: + - command + pattern: + - android\.content\.Intent + - java\.lang\.String + - \.compareToIgnoreCase\( + - \.getAction\( + type: RegexAnd + severity: info + input_case: exact +- id: '00062' + message: Query WiFi information and WiFi Mac Address + metadata: + label: + - wifi + - collection + pattern: + - android\.net\.wifi\.WifiInfo + - android\.net\.wifi\.WifiManager + - \.getMacAddress\( + - \.getConnectionInfo\( + type: RegexAnd + severity: info + input_case: exact +- id: '00170' + message: Get installed applications and put the list in shared preferences + metadata: + label: + - applications + - privacy + pattern: + - android\.content\.pm\.PackageManager + - android\.content\.SharedPreferences$Editor + - \.getInstalledApplications\( + - \.putString\( + type: RegexAnd + severity: info + input_case: exact +- id: '00035' + message: Query the list of the installed packages + metadata: + label: + - reflection + pattern: + - android\.content\.pm\.PackageManager + - android\.content\.Context + - \.getInstalledPackages\( + - \.getPackageManager\( + type: RegexAnd + severity: info + input_case: exact +- id: '00042' + message: Query WiFi BSSID and scan results + metadata: + label: + - collection + - wifi + pattern: + - android\.net\.wifi\.WifiInfo + - android\.net\.wifi\.WifiManager + - \.getBSSID\( + - \.getScanResults\( + type: RegexAnd + severity: info + input_case: exact +- id: '00107' + message: Write the IMSI number into a file + metadata: + label: + - collection + - telephony + - file + - command + pattern: + - java\.io\.FileOutputStream + - android\.telephony\.TelephonyManager + - \.write\( + - \.getSubscriberId\( + type: RegexAnd + severity: info + input_case: exact +- id: '00015' + message: Put buffer stream (data) to JSON object + metadata: + label: + - file + pattern: + - java\.io\.BufferedInputStream + - org\.json\.JSONObject + - \.put\( + - \.read\( + type: RegexAnd + severity: info + input_case: exact +- id: '00150' + message: Send IMSI over Internet + metadata: + label: + - phone + pattern: + - java\.net\.URL + - android\.telephony\.TelephonyManager + - \.openConnection\( + - \.getSubscriberId\( + type: RegexAnd + severity: info + input_case: exact +- id: '00003' + message: Put the compressed bitmap data into JSON object + metadata: + label: + - camera + pattern: + - android\.graphics\.Bitmap + - org\.json\.JSONObject + - \.put\( + - \.compress\( + type: RegexAnd + severity: info + input_case: exact +- id: '00146' + message: Get the network operator name and IMSI + metadata: + label: + - telephony + - collection + pattern: + - android\.telephony\.TelephonyManager + - \.getNetworkOperatorName\( + - \.getSubscriberId\( + type: RegexAnd + severity: info + input_case: exact +- id: '00054' + message: Install other APKs from file + metadata: + label: + - reflection + pattern: + - android\.content\.Intent + - android\.net\.Uri + - \.fromFile\( + - \.setDataAndType\( + type: RegexAnd + severity: info + input_case: exact +- id: '00111' + message: Get the sender address of the SMS + metadata: + label: + - collection + - sms + pattern: + - android\.telephony\.SmsMessage + - java\.lang\.String + - \.toString\( + - \.getOriginatingAddress\( + type: RegexAnd + severity: info + input_case: exact +- id: 00185 + message: Start capturing camera preview frames to the screen + metadata: + label: + - camera + pattern: + - java\.lang\.Object + - android\.hardware\.Camera + - \.startPreview\( + type: RegexAnd + severity: info + input_case: exact +- id: 00097 + message: Get the sender address of the SMS and put it into JSON + metadata: + label: + - collection + - sms + pattern: + - android\.telephony\.SmsMessage + - org\.json\.JSONObject + - \.put\( + - \.getOriginatingAddress\( + type: RegexAnd + severity: info + input_case: exact +- id: 00078 + message: Get the network operator name + metadata: + label: + - collection + - telephony + pattern: + - android\.content\.Context + - android\.telephony\.TelephonyManager + - \.getNetworkOperatorName\( + - \.getSystemService\( + type: RegexAnd + severity: info + input_case: exact +- id: '00202' + message: Make a phone call + metadata: + label: + - control + pattern: + - android\.content\.Intent + - "tel:" + - \.setData\( + type: RegexAnd + severity: info + input_case: exact +- id: 00081 + message: Get declared method from given method name + metadata: + label: + - reflection + pattern: + - java\.lang\.Class + - java\.lang\.StringBuilder + - \.getDeclaredMethods\( + type: RegexAnd + severity: info + input_case: exact +- id: 00039 + message: Start a web server + metadata: + label: + - control + - network + pattern: + - java\.net\.Socket + - java\.net\.ServerSocket + - \.getInetAddress\( + - \.accept\( + type: RegexAnd + severity: info + input_case: exact +- id: 00193 + message: Send a SMS message + metadata: + label: + - sms + pattern: + - android\.telephony\.SmsManager + - \.sendTextMessage\( + - \.getDefault\( + type: RegexAnd + severity: info + input_case: exact +- id: 00038 + message: Query the phone number + metadata: + label: + - collection + pattern: + - android\.content\.Context + - android\.telephony\.TelephonyManager + - \.getLine1Number\( + - \.getSystemService\( + type: RegexAnd + severity: info + input_case: exact +- id: 00192 + message: Get messages in the SMS inbox + metadata: + label: + - sms + pattern: + - android\.database\.Cursor + - android\.net\.Uri + - \.parse\( + - \.getColumnIndexOrThrow\( + type: RegexAnd + severity: info + input_case: exact +- id: 00080 + message: Save recorded audio/video to a file + metadata: + label: + - record + - file + pattern: + - android\.os\.Bundle + - android\.media\.MediaRecorder + - \.getString\( + - \.setOutputFile\( + type: RegexAnd + severity: info + input_case: exact +- id: '00203' + message: Put a phone number into an intent + metadata: + label: + - control + pattern: + - android\.content\.Intent + - android\.net\.Uri + - \.parse\( + - \.setData\( + - "tel:" + type: RegexAnd + severity: info + input_case: exact +- id: 00096 + message: Connect to a URL and set request method + metadata: + label: + - command + - network + pattern: + - java\.net\.HttpURLConnection + - java\.net\.URL + - \.openConnection\( + - \.setRequestMethod\( + type: RegexAnd + severity: info + input_case: exact +- id: 00079 + message: Hide the current app's icon + metadata: + label: + - evasion + pattern: + - android\.content\.pm\.PackageManager + - android\.content\.Context + - \.setComponentEnabledSetting\( + - \.getPackageManager\( + type: RegexAnd + severity: info + input_case: exact +- id: 00184 + message: Set camera preview texture + metadata: + label: + - camera + pattern: + - java\.lang\.Object + - android\.hardware\.Camera + - \.setPreviewTexture\( + type: RegexAnd + severity: info + input_case: exact +- id: '00055' + message: Query the SMS content and the source of the phone number + metadata: + label: + - sms + - collection + pattern: + - android\.telephony\.SmsMessage + - \.getDisplayOriginatingAddress\( + - \.getDisplayMessageBody\( + type: RegexAnd + severity: info + input_case: exact +- id: '00110' + message: Query the ICCID number + metadata: + label: + - collection + - telephony + pattern: + - android\.telephony\.SubscriptionManager + - android\.telephony\.SubscriptionInfo + - \.getActiveSubscriptionInfoList\( + - \.getIccId\( + type: RegexAnd + severity: info + input_case: exact +- id: '00002' + message: Open the camera and take picture + metadata: + label: + - camera + pattern: + - android\.hardware\.Camera + - \.open\( + - \.takePicture\( + type: RegexAnd + severity: info + input_case: exact +- id: '00147' + message: Get the time of current location + metadata: + label: + - collection + - location + pattern: + - android\.location\.LocationManager + - android\.location\.Location + - \.getTime\( + - \.isProviderEnabled\( + type: RegexAnd + severity: info + input_case: exact +- id: '00014' + message: Read file into a stream and put it into a JSON object + metadata: + label: + - file + pattern: + - java\.io\.FileInputStream + - org\.json\.JSONObject + - \.put\( + type: RegexAnd + severity: info + input_case: exact +- id: '00151' + message: Send phone number over Internet + metadata: + label: + - phone + - privacy + pattern: + - java\.net\.URL + - android\.telephony\.TelephonyManager + - \.openConnection\( + - \.getLine1Number\( + type: RegexAnd + severity: info + input_case: exact +- id: '00043' + message: Calculate WiFi signal strength + metadata: + label: + - collection + - wifi + pattern: + - android\.net\.wifi\.WifiInfo + - android\.net\.wifi\.WifiManager + - \.calculateSignalLevel\( + - \.getRssi\( + type: RegexAnd + severity: info + input_case: exact +- id: '00106' + message: Get the currently formatted WiFi IP address + metadata: + label: + - collection + - wifi + pattern: + - android\.net\.wifi\.WifiInfo + - android\.text\.format\.Formatter + - \.formatIpAddress\( + - \.getIpAddress\( + type: RegexAnd + severity: info + input_case: exact +- id: '00171' + message: Compare network operator with a string + metadata: + label: + - network + pattern: + - java\.lang\.String + - android\.telephony\.TelephonyManager + - \.equals\( + - \.getNetworkOperatorName\( + type: RegexAnd + severity: info + input_case: exact +- id: '00034' + message: Query the current data network type + metadata: + label: + - collection + - network + pattern: + - android\.content\.Context + - android\.telephony\.TelephonyManager + - \.getNetworkType\( + - \.getSystemService\( + type: RegexAnd + severity: info + input_case: exact +- id: '00126' + message: 'Read sensitive data(SMS, CALLLOG, etc)' + metadata: + label: + - collection + - sms + - calllog + - calendar + pattern: + - android\.content\.ContentResolver + - java\.lang\.String + - \.query\( + - \.toString\( + type: RegexAnd + severity: info + input_case: exact +- id: '00063' + message: 'Implicit intent(view a web page, make a phone call, etc.)' + metadata: + label: + - control + pattern: + - android\.content\.Intent + - android\.net\.Uri + - \.parse\( + type: RegexAnd + severity: info + input_case: exact +- id: '00130' + message: Get the current WIFI information + metadata: + label: + - wifi + - collection + pattern: + - android\.net\.wifi\.WifiManager + - android\.content\.Context + - \.getConnectionInfo\( + - \.getSystemService\( + type: RegexAnd + severity: info + input_case: exact +- id: '00075' + message: Get location of the device + metadata: + label: + - collection + - location + pattern: + - android\.location\.LocationManager + - \.getLastKnownLocation\( + - \.isProviderEnabled\( + type: RegexAnd + severity: info + input_case: exact +- id: '00167' + message: Use accessibility service to perform action getting root in active window + metadata: + label: + - accessibility service + pattern: + - android\.view\.accessibility\.AccessibilityNodeInfo + - android\.accessibilityservice\.AccessibilityService + - \.performAction\( + - \.getRootInActiveWindow\( + type: RegexAnd + severity: info + input_case: exact +- id: '00022' + message: Open a file from given absolute path of the file + metadata: + label: + - file + pattern: + - java\.io\.File + - \.getAbsolutePath\( + type: RegexAnd + severity: info + input_case: exact +- id: 00188 + message: Get the address of a SMS message + metadata: + label: + - sms + pattern: + - android\.content\.ContentResolver + - android\.database\.Cursor + - \.query\( + - \.getColumnIndex\( + type: RegexAnd + severity: info + input_case: exact +- id: 00059 + message: Query the SIM card status + metadata: + label: + - collection + pattern: + - java\.lang\.Integer + - android\.telephony\.TelephonyManager + - \.getSimState\( + - \.intValue\( + type: RegexAnd + severity: info + input_case: exact +- id: 00018 + message: Get JSON object prepared and fill in location info + metadata: + label: + - location + - collection + pattern: + - android\.location\.LocationManager + - org\.json\.JSONObject + - \.requestLocationUpdates\( + type: RegexAnd + severity: info + input_case: exact +- id: '00044' + message: Query the last time this package's activity was used + metadata: + label: + - collection + - reflection + pattern: + - android\.app\.usage\.UsageStatsManager + - android\.app\.usage\.UsageStats + - \.getLastTimeUsed\( + - \.queryUsageStats\( + type: RegexAnd + severity: info + input_case: exact +- id: '00101' + message: Initialize recorder + metadata: + label: + - record + pattern: + - android\.os\.Bundle + - android\.media\.MediaRecorder + - \.getString\( + - \.prepare\( + type: RegexAnd + severity: info + input_case: exact +- id: '00013' + message: Read file and put it into a stream + metadata: + label: + - file + pattern: + - java\.io\.FileInputStream + - java\.io\.File + type: RegexAnd + severity: info + input_case: exact +- id: '00156' + message: 'Acquire lock on Power Manager ' + metadata: + label: + - lock + - power manager + pattern: + - android\.os\.PowerManager$WakeLock + - android\.os\.PowerManager + - \.newWakeLock\( + - \.acquire\( + type: RegexAnd + severity: info + input_case: exact +- id: '00005' + message: Get absolute path of file and put it to JSON object + metadata: + label: + - file + pattern: + - java\.io\.File + - org\.json\.JSONObject + - \.getAbsolutePath\( + - \.put\( + type: RegexAnd + severity: info + input_case: exact +- id: '00140' + message: Write the phone number into a file + metadata: + label: + - collection + - telephony + - file + - command + pattern: + - java\.io\.FileOutputStream + - android\.telephony\.TelephonyManager + - \.getLine1Number\( + - \.write\( + type: RegexAnd + severity: info + input_case: exact +- id: '00052' + message: 'Deletes media specified by a content URI(SMS, CALL_LOG, File, etc.)' + metadata: + label: + - sms + pattern: + - android\.content\.ContentResolver + - android\.net\.Uri + - \.parse\( + - \.delete\( + type: RegexAnd + severity: info + input_case: exact +- id: '00117' + message: Get the IMSI and network operator name + metadata: + label: + - telephony + - collection + pattern: + - android\.telephony\.TelephonyManager + - \.getNetworkOperatorName\( + - \.getSubscriberId\( + type: RegexAnd + severity: info + input_case: exact +- id: 00183 + message: Get current camera parameters and change the setting. + metadata: + label: + - camera + pattern: + - android\.hardware\.Camera + - \.setParameters\( + - \.getParameters\( + type: RegexAnd + severity: info + input_case: exact +- id: 00029 + message: Initialize class object dynamically + metadata: + label: + - reflection + pattern: + - java\.lang\.Class + - java\.lang\.reflect\.Constructor + - \.newInstance\( + - \.forName\( + type: RegexAnd + severity: info + input_case: exact +- id: 00091 + message: Retrieve data from broadcast + metadata: + label: + - collection + pattern: + - android\.content\.Intent + - android\.os\.Bundle + - \.getString\( + - \.getExtras\( + type: RegexAnd + severity: info + input_case: exact +- id: '00204' + message: Get the default ringtone + metadata: + label: + - collection + pattern: + - android\.media\.RingtoneManager + - \.getDefaultUri\( + - \.getRingtone\( + type: RegexAnd + severity: info + input_case: exact +- id: 00087 + message: Check the current network type + metadata: + label: + - network + pattern: + - java\.lang\.Object + - android\.net\.NetworkInfo + - \.equals\( + - \.getType\( + type: RegexAnd + severity: info + input_case: exact +- id: 00068 + message: Executes the specified string Linux command + metadata: + label: + - control + pattern: + - java\.lang\.Runtime + - \.getRuntime\( + - \.exec\( + type: RegexAnd + severity: info + input_case: exact +- id: 00195 + message: Set the output path of the recorded file + metadata: + label: + - record + - file + pattern: + - java\.io\.File + - android\.media\.MediaRecorder + - \.setOutputFile\( + - \.getAbsolutePath\( + type: RegexAnd + severity: info + input_case: exact +- id: 00048 + message: Query the SMS contents + metadata: + label: + - sms + - collection + pattern: + - android\.telephony\.SmsMessage + - \.getDisplayMessageBody\( + - \.createFromPdu\( + type: RegexAnd + severity: info + input_case: exact +- id: 00009 + message: Put data in cursor to JSON object + metadata: + label: + - file + pattern: + - android\.database\.Cursor + - org\.json\.JSONObject + - \.getString\( + - \.put\( + type: RegexAnd + severity: info + input_case: exact +- id: '00160' + message: Use accessibility service to perform action getting node info by View Id + metadata: + label: + - accessibility service + pattern: + - android\.view\.accessibility\.AccessibilityNodeInfo + - \.performAction\( + - \.findAccessibilityNodeInfosByViewId\( + type: RegexAnd + severity: info + input_case: exact +- id: '00025' + message: Monitor the general action to be performed + metadata: + label: + - reflection + pattern: + - android\.content\.Intent + - java\.lang\.String + - \.equals\( + - \.getAction\( + type: RegexAnd + severity: info + input_case: exact +- id: '00137' + message: Get last known location of the device + metadata: + label: + - location + - collection + pattern: + - android\.location\.LocationManager + - android\.location\.Location + - \.getLastKnownLocation\( + - \.toString\( + type: RegexAnd + severity: info + input_case: exact +- id: '00072' + message: Write HTTP input stream into a file + metadata: + label: + - command + - network + - file + pattern: + - java\.net\.HttpURLConnection + - java\.io\.FileOutputStream + - \.getInputStream\( + - \.write\( + type: RegexAnd + severity: info + input_case: exact +- id: '00121' + message: Create a directory + metadata: + label: + - file + - command + pattern: + - java\.io\.File + - android\.os\.Bundle + - \.getString\( + - \.mkdirs\( + type: RegexAnd + severity: info + input_case: exact +- id: '00064' + message: Monitor incoming call status + metadata: + label: + - control + pattern: + - android\.content\.Context + - android\.telephony\.TelephonyManager + - \.getCallState\( + - \.getSystemService\( + type: RegexAnd + severity: info + input_case: exact +- id: 00208 + message: Capture the contents of the device screen + metadata: + label: + - collection + - screen + pattern: + - android\.media\.projection\.MediaProjection + - \.createVirtualDisplay\( + - \.registerCallback\( + type: RegexAnd + severity: info + input_case: exact +- id: '00176' + message: Send sms to a contact of contact list + metadata: + label: + - sms + pattern: + - android\.telephony\.SmsManager + - android\.content\.ContentResolver + - \.sendTextMessage\( + - \.query\( + type: RegexAnd + severity: info + input_case: exact +- id: 00199 + message: Stop recording and release recording resources + metadata: + label: + - record + pattern: + - android\.media\.MediaRecorder + - \.stop\( + - \.release\( + type: RegexAnd + severity: info + input_case: exact +- id: '00033' + message: Query the IMEI number + metadata: + label: + - collection + pattern: + - android\.content\.Context + - android\.telephony\.TelephonyManager + - \.getDeviceId\( + - \.getSystemService\( + type: RegexAnd + severity: info + input_case: exact +- id: '00177' + message: Check if permission is granted and request it + metadata: + label: + - permission + pattern: + - android\.app\.Activity + - android\.content\.Context + - \.checkPermission\( + - \.requestPermissions\( + type: RegexAnd + severity: info + input_case: exact +- id: 00198 + message: Initialize the recorder and start recording + metadata: + label: + - record + pattern: + - android\.media\.MediaRecorder + - \.prepare\( + - \.start\( + type: RegexAnd + severity: info + input_case: exact +- id: '00032' + message: Load external class + metadata: + label: + - reflection + pattern: + - java\.lang\.Class + - java\.lang\.ClassLoader + - \.getClassLoader\( + - \.loadClass\( + type: RegexAnd + severity: info + input_case: exact +- id: 00209 + message: Get pixels from the latest rendered image + metadata: + label: + - collection + pattern: + - android\.media\.ImageReader + - android\.media\.Image + - \.getPlanes\( + - \.acquireLatestImage\( + type: RegexAnd + severity: info + input_case: exact +- id: '00120' + message: Append the sender's address to the string + metadata: + label: + - sms + - collection + pattern: + - android\.telephony\.SmsMessage + - java\.lang\.StringBuilder + - \.append\( + - \.getOriginatingAddress\( + type: RegexAnd + severity: info + input_case: exact +- id: '00065' + message: Get the country code of the SIM card provider + metadata: + label: + - collection + pattern: + - android\.content\.Context + - android\.telephony\.TelephonyManager + - \.getSimCountryIso\( + - \.getSystemService\( + type: RegexAnd + severity: info + input_case: exact +- id: '00136' + message: Stop recording + metadata: + label: + - record + - command + pattern: + - android\.os\.Bundle + - android\.media\.MediaRecorder + - \.getString\( + - \.stop\( + type: RegexAnd + severity: info + input_case: exact +- id: '00073' + message: Write the SIM card information into a file + metadata: + label: + - collection + - telephony + - file + pattern: + - android\.telephony\.SubscriptionManager + - java\.io\.FileOutputStream + - \.getActiveSubscriptionInfoList\( + - \.write\( + type: RegexAnd + severity: info + input_case: exact +- id: '00161' + message: Perform accessibility service action on accessibility node info + metadata: + label: + - accessibility service + pattern: + - android\.view\.accessibility\.AccessibilityNodeInfo + - \.performAction\( + - \.getParent\( + type: RegexAnd + severity: info + input_case: exact +- id: '00024' + message: Write file after Base64 decoding + metadata: + label: + - reflection + - file + pattern: + - java\.io\.FileOutputStream + - android\.util\.Base64 + - \.decode\( + - \.write\( + type: RegexAnd + severity: info + input_case: exact +- id: 00008 + message: Check if successfully sending out SMS + metadata: + label: + - sms + pattern: + - android\.telephony\.SmsManager + - java\.lang\.Boolean + - \.sendTextMessage\( + - \.valueOf\( + type: RegexAnd + severity: info + input_case: exact +- id: 00049 + message: Query the phone number from SMS sender + metadata: + label: + - sms + - collection + pattern: + - android\.telephony\.SmsMessage + - \.getDisplayOriginatingAddress\( + - \.createFromPdu\( + type: RegexAnd + severity: info + input_case: exact +- id: 00194 + message: Set the audio source (MIC) and recorded file format + metadata: + label: + - record + pattern: + - android\.media\.MediaRecorder + - \.setAudioSource\( + - \.setOutputFormat\( + type: RegexAnd + severity: info + input_case: exact +- id: 00086 + message: Check if the device is in data roaming mode + metadata: + label: + - telephony + pattern: + - java\.lang\.Object + - android\.net\.NetworkInfo + - \.isRoaming\( + - \.equals\( + type: RegexAnd + severity: info + input_case: exact +- id: 00069 + message: Run shell script programmably + metadata: + label: + - control + pattern: + - java\.lang\.Runtime + - java\.lang\.Process + - \.exec\( + - \.getOutputStream\( + type: RegexAnd + severity: info + input_case: exact +- id: '00205' + message: Simulate a touch gesture on the device screen + metadata: + label: + - accessibility service + - control + pattern: + - android\.accessibilityservice\.GestureDescription$Builder + - \.build\( + - \.addStroke\( + type: RegexAnd + severity: info + input_case: exact +- id: 00090 + message: Set recroded audio/video file format + metadata: + label: + - record + pattern: + - android\.os\.Bundle + - android\.media\.MediaRecorder + - \.getString\( + - \.setOutputFormat\( + type: RegexAnd + severity: info + input_case: exact +- id: 00182 + message: Open camera. + metadata: + label: + - camera + pattern: + - java\.lang\.Object + - android\.hardware\.Camera + - \.open\( + type: RegexAnd + severity: info + input_case: exact +- id: 00028 + message: Read file from assets directory + metadata: + label: + - file + pattern: + - android\.content\.res\.AssetManager + - java\.io\.InputStream + - \.open\( + - \.read\( + type: RegexAnd + severity: info + input_case: exact +- id: '00053' + message: 'Monitor data identified by a given content URI changes(SMS, MMS, etc.)' + metadata: + label: + - sms + pattern: + - android\.content\.ContentResolver + - android\.net\.Uri + - \.parse\( + - \.registerContentObserver\( + type: RegexAnd + severity: info + input_case: exact +- id: '00116' + message: Get the current WiFi MAC address and put it into JSON + metadata: + label: + - wifi + - collection + pattern: + - android\.net\.wifi\.WifiInfo + - org\.json\.JSONObject + - \.getMacAddress\( + - \.put\( + type: RegexAnd + severity: info + input_case: exact +- id: '00004' + message: Get filename and put it to JSON object + metadata: + label: + - file + - collection + pattern: + - java\.io\.File + - org\.json\.JSONObject + - \.put\( + - \.getName\( + type: RegexAnd + severity: info + input_case: exact +- id: '00141' + message: Load class from given class name + metadata: + label: + - reflection + pattern: + - java\.lang\.ClassLoader + - java\.lang\.StringBuilder + - \.toString\( + - \.loadClass\( + type: RegexAnd + severity: info + input_case: exact +- id: '00012' + message: Read data and put it into a buffer stream + metadata: + label: + - file + pattern: + - java\.io\.FileInputStream + - java\.io\.BufferedInputStream + type: RegexAnd + severity: info + input_case: exact +- id: '00157' + message: 'Instantiate new object using reflection, possibly used for dexClassLoader ' + metadata: + label: + - reflection + - dexClassLoader + pattern: + - java\.lang\.Class + - java\.lang\.reflect\.Constructor + - \.newInstance\( + - \.getConstructor\( + type: RegexAnd + severity: info + input_case: exact +- id: '00045' + message: Query the name of currently running application + metadata: + label: + - collection + - reflection + pattern: + - android\.app\.usage\.UsageStatsManager + - android\.app\.usage\.UsageStats + - \.getPackageName\( + - \.queryUsageStats\( + type: RegexAnd + severity: info + input_case: exact +- id: '00100' + message: Check the network capabilities + metadata: + label: + - collection + - network + pattern: + - java\.lang\.Object + - android\.net\.ConnectivityManager + - \.equals\( + - \.getNetworkCapabilities\( + type: RegexAnd + severity: info + input_case: exact +- id: 00197 + message: Set the audio encoder and initialize the recorder + metadata: + label: + - record + pattern: + - android\.media\.MediaRecorder + - \.prepare\( + - \.setAudioEncoder\( + type: RegexAnd + severity: info + input_case: exact +- id: 00178 + message: Execute Linux commands via ProcessBuilder + metadata: + label: + - command + pattern: + - java\.lang\.ProcessBuilder + - \.start\( + type: RegexAnd + severity: info + input_case: exact +- id: 00085 + message: Get the ISO country code and put it into JSON + metadata: + label: + - collection + - telephony + pattern: + - org\.json\.JSONObject + - android\.telephony\.TelephonyManager + - \.put\( + - \.getNetworkCountryIso\( + type: RegexAnd + severity: info + input_case: exact +- id: '00206' + message: Check if the text of the view contains the given string + metadata: + label: + - accessibility service + pattern: + - android\.view\.accessibility\.AccessibilityNodeInfo + - java\.lang\.String + - \.getText\( + - \.contains\( + type: RegexAnd + severity: info + input_case: exact +- id: 00139 + message: Get the current WiFi id + metadata: + label: + - collection + - wifi + pattern: + - android\.net\.wifi\.WifiInfo + - android\.content\.Context + - \.getNetworkId\( + - \.getSystemService\( + type: RegexAnd + severity: info + input_case: exact +- id: 00093 + message: Get the content of SMS and forward it to others via SMS + metadata: + label: + - collection + - sms + - command + pattern: + - android\.telephony\.SmsMessage + - android\.telephony\.SmsManager + - \.sendTextMessage\( + - \.getMessageBody\( + type: RegexAnd + severity: info + input_case: exact +- id: '00210' + message: Copy pixels from the latest rendered image into a Bitmap + metadata: + label: + - collection + pattern: + - android\.media\.ImageReader + - android\.graphics\.Bitmap + - \.copyPixelsFromBuffer\( + - \.acquireLatestImage\( + type: RegexAnd + severity: info + input_case: exact +- id: 00181 + message: Load native libraries(.so) via System.load (60% means caught) + metadata: + label: + - so + pattern: + - java\.lang\.System + - \.load\( + type: RegexAnd + severity: info + input_case: exact +- id: '00115' + message: Get last known location of the device + metadata: + label: + - collection + - location + pattern: + - android\.location\.LocationManager + - android\.location\.Location + - \.getLastKnownLocation\( + - \.getLongitude\( + type: RegexAnd + severity: info + input_case: exact +- id: '00050' + message: Query the SMS service centre timestamp + metadata: + label: + - sms + - collection + pattern: + - android\.telephony\.SmsMessage + - \.getTimestampMillis\( + - \.createFromPdu\( + type: RegexAnd + severity: info + input_case: exact +- id: '00142' + message: Get calendar information + metadata: + label: + - collection + - calendar + pattern: + - java\.lang\.StringBuilder + - java\.util\.Calendar + - \.append\( + - \.get\( + type: RegexAnd + severity: info + input_case: exact +- id: '00007' + message: Use absolute path of directory for the output media file path + metadata: + label: + - file + pattern: + - java\.io\.File + - android\.media\.MediaRecorder + - \.setOutputFile\( + - \.getAbsolutePath\( + type: RegexAnd + severity: info + input_case: exact +- id: '00154' + message: Connect hostname to TCP or UDP socket using KryoNet + metadata: + label: + - socket + pattern: + - com\.esotericsoftware\.kryonet\.Client + - java\.net\.InetAddress + - \.connect\( + - \.getByName\( + type: RegexAnd + severity: info + input_case: exact +- id: '00011' + message: 'Query data from URI (SMS, CALLLOGS)' + metadata: + label: + - sms + - calllog + - collection + pattern: + - android\.content\.ContentResolver + - android\.net\.Uri + - \.parse\( + - \.query\( + type: RegexAnd + severity: info + input_case: exact +- id: '00103' + message: Check the active network type + metadata: + label: + - network + pattern: + - java\.lang\.Object + - android\.net\.ConnectivityManager + - \.equals\( + - \.getActiveNetworkInfo\( + type: RegexAnd + severity: info + input_case: exact +- id: '00046' + message: Method reflection + metadata: + label: + - reflection + pattern: + - java\.lang\.reflect\.Method + - java\.lang\.Class + - \.getDeclaredMethod\( + - \.invoke\( + type: RegexAnd + severity: info + input_case: exact +- id: '00031' + message: Check the list of currently running applications + metadata: + label: + - reflection + - collection + pattern: + - android\.content\.ComponentName + - android\.app\.ActivityManager + - \.getPackageName\( + - \.getRunningTasks\( + type: RegexAnd + severity: info + input_case: exact +- id: '00174' + message: Get all accounts by type and put them in a JSON object + metadata: + label: + - accounts + - collection + pattern: + - org\.json\.JSONObject + - android\.accounts\.AccountManager + - \.put\( + - \.getAccountsByType\( + type: RegexAnd + severity: info + input_case: exact +- id: '00066' + message: Query the ICCID number + metadata: + label: + - collection + pattern: + - android\.content\.Context + - android\.telephony\.TelephonyManager + - \.getSimSerialNumber\( + - \.getSystemService\( + type: RegexAnd + severity: info + input_case: exact +- id: '00123' + message: Save the response to JSON after connecting to the remote server + metadata: + label: + - network + - command + pattern: + - java\.net\.HttpURLConnection + - org\.json\.JSONObject + - \.connect\( + type: RegexAnd + severity: info + input_case: exact +- id: 00089 + message: Connect to a URL and receive input stream from the server + metadata: + label: + - command + - network + pattern: + - java\.net\.HttpURLConnection + - java\.net\.URL + - \.openConnection\( + - \.getInputStream\( + type: RegexAnd + severity: info + input_case: exact +- id: '00070' + message: Get sender's address and send SMS + metadata: + label: + - collection + - command + - sms + pattern: + - android\.telephony\.SmsMessage + - android\.telephony\.SmsManager + - \.sendTextMessage\( + - \.getOriginatingAddress\( + type: RegexAnd + severity: info + input_case: exact +- id: '00135' + message: Get the current WiFi id and put it into JSON. + metadata: + label: + - wifi + - collection + pattern: + - android\.net\.wifi\.WifiInfo + - org\.json\.JSONObject + - \.getNetworkId\( + - \.put\( + type: RegexAnd + severity: info + input_case: exact +- id: '00027' + message: Get specific method from other Dex files + metadata: + label: + - reflection + pattern: + - java\.lang\.Class + - java\.lang\.ClassLoader + - \.getMethod\( + - \.loadClass\( + type: RegexAnd + severity: info + input_case: exact +- id: '00162' + message: Create InetSocketAddress object and connecting to it + metadata: + label: + - socket + pattern: + - java\.net\.Socket + - java\.net\.InetSocketAddress + - \.connect\( + type: RegexAnd + severity: info + input_case: exact +- id: 00119 + message: Write the IMEI number into a file + metadata: + label: + - collection + - file + - telephony + - command + pattern: + - java\.io\.FileOutputStream + - android\.telephony\.TelephonyManager + - \.getDeviceId\( + - \.write\( + type: RegexAnd + severity: info + input_case: exact +- id: 00158 + message: Connect to a URL and send sensitive data got from resolver + metadata: + label: + - privacy + - connection + pattern: + - java\.net\.HttpURLConnection + - android\.content\.ContentResolver + - \.query\( + - \.getOutputStream\( + type: RegexAnd + severity: info + input_case: exact +- id: 00159 + message: Use accessibility service to perform action getting node info by text + metadata: + label: + - accessibility service + pattern: + - android\.view\.accessibility\.AccessibilityNodeInfo + - \.findAccessibilityNodeInfosByText\( + - \.performAction\( + type: RegexAnd + severity: info + input_case: exact +- id: 00118 + message: Check if the content of SMS contains given string + metadata: + label: + - sms + - collection + pattern: + - android\.telephony\.SmsMessage + - java\.lang\.String + - \.getMessageBody\( + - \.contains\( + type: RegexAnd + severity: info + input_case: exact +- id: '00026' + message: Method reflection + metadata: + label: + - reflection + pattern: + - java\.lang\.reflect\.Method + - java\.lang\.Class + - \.getMethod\( + - \.invoke\( + type: RegexAnd + severity: info + input_case: exact +- id: '00163' + message: Create new Socket and connecting to it + metadata: + label: + - socket + pattern: + - java\.net\.Socket + - \.connect\( + type: RegexAnd + severity: info + input_case: exact +- id: '00071' + message: Write the ISO country code of the current network operator into a file + metadata: + label: + - collection + - command + - network + - file + pattern: + - java\.io\.FileOutputStream + - android\.telephony\.TelephonyManager + - \.getNetworkCountryIso\( + - \.write\( + type: RegexAnd + severity: info + input_case: exact +- id: '00134' + message: Get the current WiFi IP address + metadata: + label: + - wifi + - collection + pattern: + - android\.net\.wifi\.WifiInfo + - android\.content\.Context + - \.getIpAddress\( + - \.getSystemService\( + type: RegexAnd + severity: info + input_case: exact +- id: '00067' + message: Query the IMSI number + metadata: + label: + - collection + pattern: + - android\.content\.Context + - android\.telephony\.TelephonyManager + - \.getSubscriberId\( + - \.getSystemService\( + type: RegexAnd + severity: info + input_case: exact +- id: '00122' + message: Check if the sender address of SMS contains the given string + metadata: + label: + - sms + - collection + pattern: + - android\.telephony\.SmsMessage + - java\.lang\.String + - \.getOriginatingAddress\( + - \.contains\( + type: RegexAnd + severity: info + input_case: exact +- id: 00088 + message: Create a secure socket connection to the given host address + metadata: + label: + - command + - network + pattern: + - javax\.net\.ssl\.SSLSocketFactory + - java\.net\.InetAddress + - \.getHostAddress\( + - \.createSocket\( + type: RegexAnd + severity: info + input_case: exact +- id: '00030' + message: Connect to the remote server through the given URL + metadata: + label: + - network + pattern: + - java\.net\.HttpURLConnection + - java\.net\.URL + - \.openConnection\( + - \.connect\( + type: RegexAnd + severity: info + input_case: exact +- id: '00175' + message: 'Get notification manager and cancel notifications ' + metadata: + label: + - notification + pattern: + - android\.app\.NotificationManager + - android\.content\.Context + - \.cancelAll\( + - \.getSystemService\( + type: RegexAnd + severity: info + input_case: exact +- id: '00102' + message: Set the phone speaker on + metadata: + label: + - command + pattern: + - android\.media\.AudioManager + - android\.content\.Context + - \.setSpeakerphoneOn\( + - \.getSystemService\( + type: RegexAnd + severity: info + input_case: exact +- id: '00047' + message: Query the local IP address + metadata: + label: + - network + - collection + pattern: + - java\.net\.Socket + - java\.net\.InetAddress + - \.getHostAddress\( + - \.getLocalAddress\( + type: RegexAnd + severity: info + input_case: exact +- id: '00155' + message: Execute commands on shell using DataOutputStream object + metadata: + label: + - exec + - command + pattern: + - java\.io\.DataOutputStream + - java\.lang\.Runtime + - \.writeBytes\( + - \.exec\( + type: RegexAnd + severity: info + input_case: exact +- id: '00010' + message: 'Read sensitive data(SMS, CALLLOG) and put it into JSON object' + metadata: + label: + - sms + - calllog + - collection + pattern: + - android\.content\.ContentResolver + - org\.json\.JSONObject + - \.put\( + - \.query\( + type: RegexAnd + severity: info + input_case: exact +- id: '00143' + message: Get external class from given path or file name + metadata: + label: + - reflection + pattern: + - android\.app\.Service + - java\.lang\.StringBuilder + - \.getClassLoader\( + - \.toString\( + type: RegexAnd + severity: info + input_case: exact +- id: '00006' + message: Scheduling recording task + metadata: + label: + - record + pattern: + - java\.util\.Timer + - android\.media\.MediaRecorder + type: RegexAnd + severity: info + input_case: exact +- id: '00114' + message: Create a secure socket connection to the proxy address + metadata: + label: + - network + - command + pattern: + - javax\.net\.ssl\.SSLSocketFactory + - java\.net\.Proxy + - \.address\( + - \.createSocket\( + type: RegexAnd + severity: info + input_case: exact +- id: '00051' + message: 'Implicit intent(view a web page, make a phone call, etc.) via setData' + metadata: + label: + - control + pattern: + - android\.content\.Intent + - android\.net\.Uri + - \.parse\( + - \.setData\( + type: RegexAnd + severity: info + input_case: exact +- id: 00180 + message: Load native libraries(.so) via System.loadLibrary (60% means caught) + metadata: + label: + - so + pattern: + - java\.lang\.System + - \.loadLibrary\( + type: RegexAnd + severity: info + input_case: exact +- id: '00211' + message: Open an URL in Wevbiew + metadata: + label: + - http + pattern: + - com\.google\.youngandroid\.runtime + - gnu\.lists\.LList + - \.callComponentMethod\( + - \.list1\( + type: RegexAnd + severity: info + input_case: exact +- id: 00138 + message: Set the audio source (MIC) + metadata: + label: + - record + pattern: + - android\.os\.Bundle + - android\.media\.MediaRecorder + - \.getString\( + - \.setAudioSource\( + type: RegexAnd + severity: info + input_case: exact +- id: 00092 + message: Send broadcast + metadata: + label: + - command + pattern: + - android\.app\.Activity + - android\.content\.Context + - \.sendBroadcast\( + - \.getApplicationContext\( + type: RegexAnd + severity: info + input_case: exact +- id: '00207' + message: Check if the resource name of the view contains the given string + metadata: + label: + - accessibility service + pattern: + - android\.view\.accessibility\.AccessibilityNodeInfo + - java\.lang\.String + - \.contains\( + - \.getViewIdResourceName\( + type: RegexAnd + severity: info + input_case: exact +- id: 00084 + message: Get the ISO country code and IMSI + metadata: + label: + - collection + - telephony + pattern: + - android\.telephony\.TelephonyManager + - \.getNetworkCountryIso\( + - \.getSubscriberId\( + type: RegexAnd + severity: info + input_case: exact +- id: 00196 + message: Set the recorded file format and output path + metadata: + label: + - record + - file + pattern: + - android\.media\.MediaRecorder + - \.setOutputFile\( + - \.setOutputFormat\( + type: RegexAnd + severity: info + input_case: exact +- id: 00179 + message: Send Location via SMS + metadata: + label: + - location + - collection + pattern: + - Landroid/telephony/TelephonyManager + - Landroid/telephony/SmsManager + - \.sendTextMessage\( + - \.getCellLocation\( + type: RegexAnd + severity: info + input_case: exact +- id: '00037' + message: Send notification + metadata: + label: + - control + pattern: + - android\.app\.Notification$Builder + - android\.app\.NotificationManager + - \.notify\( + - \.build\( + type: RegexAnd + severity: info + input_case: exact +- id: '00172' + message: Check Admin permissions to (probably) get them + metadata: + label: + - admin + pattern: + - android\.app\.admin\.DevicePolicyManager + - android\.content\.Context + - \.isAdminActive\( + - \.getSystemService\( + type: RegexAnd + severity: info + input_case: exact +- id: '00060' + message: Query the network operator name + metadata: + label: + - network + - collection + pattern: + - java\.lang\.Integer + - android\.telephony\.TelephonyManager + - \.getNetworkOperatorName\( + - \.valueOf\( + type: RegexAnd + severity: info + input_case: exact +- id: '00125' + message: Check if the given file path exist + metadata: + label: + - file + pattern: + - java\.io\.File + - android\.os\.Bundle + - \.getString\( + - \.exists\( + type: RegexAnd + severity: info + input_case: exact +- id: '00076' + message: Get the current WiFi information and put it into JSON + metadata: + label: + - collection + - wifi + pattern: + - android\.net\.wifi\.WifiManager + - org\.json\.JSONObject + - \.put\( + - \.getConnectionInfo\( + type: RegexAnd + severity: info + input_case: exact +- id: 00099 + message: Get location of the current GSM and put it into JSON + metadata: + label: + - collection + - location + pattern: + - android\.telephony\.gsm\.GsmCellLocation + - org\.json\.JSONObject + - \.getCid\( + - \.put\( + type: RegexAnd + severity: info + input_case: exact +- id: '00133' + message: Start recording + metadata: + label: + - record + - command + pattern: + - android\.os\.Bundle + - android\.media\.MediaRecorder + - \.getString\( + - \.start\( + type: RegexAnd + severity: info + input_case: exact +- id: '00021' + message: Load additional DEX files dynamically + metadata: + label: + - reflection + pattern: + - java\.lang\.ClassLoader + - java\.io\.File + - \.getAbsolutePath\( + - \.loadClass\( + type: RegexAnd + severity: info + input_case: exact +- id: '00164' + message: Get SMS address and send it through http + metadata: + label: + - sms + - http + pattern: + - java\.net\.HttpURLConnection + - android\.telephony\.SmsMessage + - \.getOriginatingAddress\( + - \.getOutputStream\( + type: RegexAnd + severity: info + input_case: exact +- id: 00148 + message: Create a socket connection to the given host address + metadata: + label: + - network + - command + pattern: + - javax\.net\.SocketFactory + - java\.net\.InetAddress + - \.getHostAddress\( + - \.createSocket\( + type: RegexAnd + severity: info + input_case: exact +- id: 00109 + message: Connect to a URL and get the response code + metadata: + label: + - network + - command + pattern: + - java\.net\.HttpURLConnection + - java\.net\.URL + - \.openConnection\( + - \.getResponseCode\( + type: RegexAnd + severity: info + input_case: exact +- id: 00191 + message: Get messages in the SMS inbox + metadata: + label: + - sms + pattern: + - android\.database\.Cursor + - android\.net\.Uri + - \.parse\( + - \.getColumnIndex\( + type: RegexAnd + severity: info + input_case: exact +- id: 00083 + message: Query the IMEI number + metadata: + label: + - collection + - telephony + pattern: + - android\.app\.Activity + - android\.telephony\.TelephonyManager + - \.getDeviceId\( + - \.getSystemService\( + type: RegexAnd + severity: info + input_case: exact +- id: 00129 + message: Get the content of SMS + metadata: + label: + - sms + - collection + pattern: + - android\.telephony\.SmsMessage + - java\.lang\.String + - \.getMessageBody\( + - \.toString\( + type: RegexAnd + severity: info + input_case: exact +- id: '00200' + message: Query data from the contact list + metadata: + label: + - collection + - contact + pattern: + - android\.content\.ContentResolver + - android\.database\.Cursor + - \.query\( + - \.getColumnIndex\( + type: RegexAnd + severity: info + input_case: exact +- id: 00095 + message: Write the ICCID of device into a file + metadata: + label: + - collection + - telephony + pattern: + - java\.io\.FileOutputStream + - android\.telephony\.SubscriptionInfo + - \.getIccId\( + - \.write\( + type: RegexAnd + severity: info + input_case: exact +- id: 00187 + message: Query a URI and check the result + metadata: + label: + - collection + - sms + - calllog + - calendar + pattern: + - android\.content\.ContentResolver + - android\.database\.Cursor + - \.query\( + - \.moveToNext\( + type: RegexAnd + severity: info + input_case: exact +- id: 00168 + message: Use accessibility service to perform global action getting node info by text + metadata: + label: + - accessibility service + pattern: + - android\.view\.accessibility\.AccessibilityNodeInfo + - android\.accessibilityservice\.AccessibilityService + - \.findAccessibilityNodeInfosByText\( + - \.performGlobalAction\( + type: RegexAnd + severity: info + input_case: exact +- id: '00113' + message: Get location and put it into JSON + metadata: + label: + - collection + - location + pattern: + - android\.location\.LocationManager + - org\.json\.JSONObject + - \.getLastKnownLocation\( + - \.put\( + type: RegexAnd + severity: info + input_case: exact +- id: '00056' + message: Modify voice volume + metadata: + label: + - control + pattern: + - android\.media\.AudioManager + - \.getStreamMaxVolume\( + - \.setStreamVolume\( + type: RegexAnd + severity: info + input_case: exact +- id: '00144' + message: Write SIM card serial number into a file + metadata: + label: + - collection + - telephony + - file + - command + pattern: + - java\.io\.FileOutputStream + - android\.telephony\.TelephonyManager + - \.write\( + - \.getSimSerialNumber\( + type: RegexAnd + severity: info + input_case: exact +- id: '00001' + message: Initialize bitmap object and compress data (e.g. JPEG) into bitmap object + metadata: + label: + - camera + pattern: + - android\.graphics\.BitmapFactory + - android\.graphics\.Bitmap + - \.decodeByteArray\( + - \.compress\( + type: RegexAnd + severity: info + input_case: exact +- id: '00152' + message: Get data from HTTP and send SMS + metadata: + label: + - command + - sms + pattern: + - android\.telephony\.SmsManager + - java\.net\.URL + - \.openConnection\( + - \.sendTextMessage\( + type: RegexAnd + severity: info + input_case: exact +- id: '00017' + message: Get Location of the device and append this info to a string + metadata: + label: + - location + - collection + pattern: + - android\.location\.Location + - java\.lang\.StringBuilder + - \.append\( + - \.getLatitude\( + type: RegexAnd + severity: info + input_case: exact +- id: '00105' + message: Append the sender's address to the string + metadata: + label: + - collection + - sms + pattern: + - android\.telephony\.SmsMessage + - java\.lang\.StringBuilder + - \.append\( + - \.getOriginatingAddress\( + type: RegexAnd + severity: info + input_case: exact +- id: '00040' + message: Send SMS + metadata: + label: + - sms + pattern: + - android\.telephony\.SmsManager + - \.divideMessage\( + - \.sendMultipartTextMessage\( + type: RegexAnd + severity: info + input_case: exact +- id: '00104' + message: Check if the given path is directory + metadata: + label: + - file + pattern: + - java\.io\.File + - android\.os\.Bundle + - \.getString\( + - \.isDirectory\( + type: RegexAnd + severity: info + input_case: exact +- id: '00041' + message: Save recorded audio/video to file + metadata: + label: + - record + pattern: + - java\.io\.File + - android\.media\.MediaRecorder + - \.setOutputFile\( + - \.toString\( + type: RegexAnd + severity: info + input_case: exact +- id: '00153' + message: Send binary data over HTTP + metadata: + label: + - http + pattern: + - java\.net\.HttpURLConnection + - java\.io\.DataOutputStream + - \.write\( + - \.getOutputStream\( + type: RegexAnd + severity: info + input_case: exact +- id: '00016' + message: Get location info of the device and put it to JSON object + metadata: + label: + - location + - collection + pattern: + - android\.location\.Location + - org\.json\.JSONObject + - \.getLongitude\( + - \.put\( + type: RegexAnd + severity: info + input_case: exact +- id: '00145' + message: Create a socket connection to the proxy address + metadata: + label: + - network + - command + pattern: + - java\.net\.Proxy + - javax\.net\.SocketFactory + - \.address\( + - \.createSocket\( + type: RegexAnd + severity: info + input_case: exact +- id: '00112' + message: Get the date of the calendar event + metadata: + label: + - collection + - calendar + pattern: + - java\.util\.Date + - java\.util\.Calendar + - \.toString\( + - \.getTimeInMillis\( + type: RegexAnd + severity: info + input_case: exact +- id: '00057' + message: Return the DHCP-assigned addresses from the last successful DHCP request + metadata: + label: + - network + - collection + pattern: + - android\.net\.wifi\.WifiManager + - java\.lang\.StringBuilder + - \.getDhcpInfo\( + - \.toString\( + type: RegexAnd + severity: info + input_case: exact +- id: 00186 + message: Control camera to take picture + metadata: + label: + - camera + pattern: + - java\.lang\.Object + - android\.hardware\.Camera + - \.takePicture\( + type: RegexAnd + severity: info + input_case: exact +- id: 00169 + message: >- + Use accessibility service to perform global action getting node info by View + Id + metadata: + label: + - accessibility service + pattern: + - android\.view\.accessibility\.AccessibilityNodeInfo + - android\.accessibilityservice\.AccessibilityService + - \.findAccessibilityNodeInfosByViewId\( + - \.performGlobalAction\( + type: RegexAnd + severity: info + input_case: exact +- id: 00094 + message: Connect to a URL and read data from it + metadata: + label: + - command + - network + pattern: + - java\.io\.InputStream + - java\.net\.URL + - \.openConnection\( + - \.read\( + type: RegexAnd + severity: info + input_case: exact +- id: '00201' + message: Query data from the call log + metadata: + label: + - collection + - calllog + pattern: + - android\.content\.ContentResolver + - android\.database\.Cursor + - \.query\( + - \.getColumnIndex\( + type: RegexAnd + severity: info + input_case: exact +- id: 00082 + message: Get the current WiFi MAC address + metadata: + label: + - collection + - wifi + pattern: + - android\.net\.wifi\.WifiInfo + - android\.content\.Context + - \.getMacAddress\( + - \.getSystemService\( + type: RegexAnd + severity: info + input_case: exact +- id: 00128 + message: Query user account information + metadata: + label: + - collection + - account + pattern: + - android\.accounts\.AccountManager + - \.getAccounts\( + - \.get\( + type: RegexAnd + severity: info + input_case: exact +- id: 00190 + message: Query a URI and append the result into a string + metadata: + label: + - collection + - sms + - calllog + - calendar + pattern: + - android\.content\.ContentResolver + - java\.lang\.StringBuilder + - \.append\( + - \.query\( + type: RegexAnd + severity: info + input_case: exact +- id: 00108 + message: Read the input stream from given URL + metadata: + label: + - network + - command + pattern: + - java\.net\.HttpURLConnection + - java\.io\.InputStream + - \.getInputStream\( + - \.read\( + type: RegexAnd + severity: info + input_case: exact +- id: 00149 + message: 'Unpack an asset, possibly decrypt it and load it as DEX' + metadata: + label: + - packer + pattern: + - android/content/res/Resources; + - dalvik\.system\.DexClassLoader + - \.getAssets\( + type: RegexAnd + severity: info + input_case: exact +- id: '00020' + message: Get absolute path of the file and store in string + metadata: + label: + - file + pattern: + - java\.io\.File + - java\.lang\.StringBuilder + - \.getAbsolutePath\( + - \.toString\( + type: RegexAnd + severity: info + input_case: exact +- id: '00165' + message: Get SMS message body and send it through http + metadata: + label: + - sms + - http + pattern: + - java\.net\.HttpURLConnection + - android\.telephony\.SmsMessage + - \.getMessageBody\( + - \.getOutputStream\( + type: RegexAnd + severity: info + input_case: exact +- id: '00077' + message: 'Read sensitive data(SMS, CALLLOG, etc)' + metadata: + label: + - collection + - sms + - calllog + - calendar + pattern: + - android\.content\.ContentResolver + - android\.content\.Context + - \.query\( + - \.getContentResolver\( + type: RegexAnd + severity: info + input_case: exact +- id: 00098 + message: Check if the network is connected + metadata: + label: + - network + pattern: + - java\.lang\.Object + - android\.net\.NetworkInfo + - \.isConnected\( + - \.equals\( + type: RegexAnd + severity: info + input_case: exact +- id: '00132' + message: Query The ISO country code + metadata: + label: + - telephony + - collection + pattern: + - android\.content\.Context + - android\.telephony\.TelephonyManager + - \.getNetworkCountryIso\( + - \.getSystemService\( + type: RegexAnd + severity: info + input_case: exact +- id: '00061' + message: Return dynamic information about the current Wi-Fi connection + metadata: + label: + - wifi + - collection + pattern: + - java\.lang\.Integer + - android\.net\.wifi\.WifiManager + - \.valueOf\( + - \.getConnectionInfo\( + type: RegexAnd + severity: info + input_case: exact +- id: '00124' + message: Check the current active network type + metadata: + label: + - network + pattern: + - java\.lang\.Object + - android\.net\.ConnectivityManager + - \.equals\( + - \.getActiveNetwork\( + type: RegexAnd + severity: info + input_case: exact +- id: '00036' + message: Get resource file from res/raw directory + metadata: + label: + - reflection + pattern: + - android\.net\.Uri + - android\.content\.Context + - \.parse\( + - \.getPackageName\( + type: RegexAnd + severity: info + input_case: exact +- id: '00173' + message: Get bounds in screen of an AccessibilityNodeInfo and perform action + metadata: + label: + - accessibility service + pattern: + - android\.view\.accessibility\.AccessibilityNodeInfo + - \.performAction\( + - \.getBoundsInScreen\( + type: RegexAnd + severity: info + input_case: exact diff --git a/mobsf/MobSF/forms.py b/mobsf/MobSF/forms.py index cb57c9894c..604ca0be63 100755 --- a/mobsf/MobSF/forms.py +++ b/mobsf/MobSF/forms.py @@ -1,4 +1,6 @@ from django import forms +from django.contrib.auth.models import User +from django.contrib.auth.forms import UserCreationForm class UploadFileForm(forms.Form): @@ -34,3 +36,23 @@ def errors_message(form): @staticmethod def errors(form): return form.errors.get_json_data() + + +class RegisterForm(UserCreationForm): + + role = forms.ChoiceField( + choices=(('viewer', 'Viewer'), ('maintainer', 'Maintainer')), + required=True, + help_text='User Role') + + def clean_email(self): + email = self.cleaned_data.get('email') + if User.objects.filter(email=email).exists(): + raise forms.ValidationError('Email already exists') + return email + + class Meta: + """Meta Class.""" + + model = User + fields = ['username', 'password1', 'password2', 'email', 'role'] diff --git a/mobsf/MobSF/init.py b/mobsf/MobSF/init.py index e96914b89a..b07aa2b954 100644 --- a/mobsf/MobSF/init.py +++ b/mobsf/MobSF/init.py @@ -5,51 +5,65 @@ import subprocess import sys import shutil - +import threading +from hashlib import sha256 +from pathlib import Path +from importlib import ( + machinery, + util, +) + +from mobsf.MobSF.tools_download import install_jadx from mobsf.install.windows.setup import windows_config_local logger = logging.getLogger(__name__) -VERSION = '3.9.8' -BANNER = """ - __ __ _ ____ _____ _____ ___ - | \/ | ___ | |__/ ___|| ___|_ _|___ // _ \ - | |\/| |/ _ \| '_ \___ \| |_ \ \ / / |_ \ (_) | - | | | | (_) | |_) |__) | _| \ V / ___) \__, | - |_| |_|\___/|_.__/____/|_| \_/ |____(_)/_/ +VERSION = '4.2.9' +BANNER = r""" + __ __ _ ____ _____ _ _ ____ + | \/ | ___ | |__/ ___|| ___|_ _| || | |___ \ + | |\/| |/ _ \| '_ \___ \| |_ \ \ / / || |_ __) | + | | | | (_) | |_) |__) | _| \ V /|__ _| / __/ + |_| |_|\___/|_.__/____/|_| \_/ |_|(_)_____| """ # noqa: W291 # ASCII Font: Standard def first_run(secret_file, base_dir, mobsf_home): # Based on https://gist.github.com/ndarville/3452907#file-secret-key-gen-py - if 'MOBSF_SECRET_KEY' in os.environ: + base_dir = Path(base_dir) + mobsf_home = Path(mobsf_home) + secret_file = Path(secret_file) + if os.getenv('MOBSF_SECRET_KEY'): secret_key = os.environ['MOBSF_SECRET_KEY'] - elif os.path.isfile(secret_file): - secret_key = open(secret_file).read().strip() + elif secret_file.exists() and secret_file.is_file(): + secret_key = secret_file.read_text().strip() else: try: secret_key = get_random() - secret = open(secret_file, 'w') - secret.write(secret_key) - secret.close() + secret_file.write_text(secret_key) except IOError: raise Exception('Secret file generation failed' % secret_file) # Run Once make_migrations(base_dir) migrate(base_dir) + # Install JADX + thread = threading.Thread( + target=install_jadx, + name='install_jadx', + args=(mobsf_home.as_posix(),)) + thread.start() # Windows Setup - windows_config_local(mobsf_home) + windows_config_local(mobsf_home.as_posix()) return secret_key def create_user_conf(mobsf_home, base_dir): try: - config_path = os.path.join(mobsf_home, 'config.py') - if not os.path.isfile(config_path): - sample_conf = os.path.join(base_dir, 'MobSF/settings.py') - with open(sample_conf, 'r') as f: - dat = f.readlines() + config_path = mobsf_home / 'config.py' + if not config_path.exists(): + sample_conf = base_dir / 'MobSF' / 'settings.py' + dat = sample_conf.read_text().splitlines() config = [] add = False for line in dat: @@ -60,20 +74,20 @@ def create_user_conf(mobsf_home, base_dir): if add: config.append(line.lstrip()) config.pop(0) - conf_str = ''.join(config) - with open(config_path, 'w') as f: - f.write(conf_str) + conf_str = '\n'.join(config) + config_path.write_text(conf_str) except Exception: logger.exception('Cannot create config file') def django_operation(cmds, base_dir): """Generic Function for Djano operations.""" - manage = os.path.join(base_dir, '../manage.py') - if not os.path.exists(manage): + manage = base_dir.parent / 'manage.py' + if manage.exists() and manage.is_file(): # Bail out for package return - args = [sys.executable, manage] + print(manage) + args = [sys.executable, manage.as_posix()] args.extend(cmds) subprocess.call(args) @@ -92,6 +106,7 @@ def migrate(base_dir): try: django_operation(['migrate'], base_dir) django_operation(['migrate', '--run-syncdb'], base_dir) + django_operation(['create_roles'], base_dir) except Exception: logger.exception('Cannot Migrate') @@ -103,41 +118,97 @@ def get_random(): def get_mobsf_home(use_home, base_dir): try: + base_dir = Path(base_dir) mobsf_home = '' if use_home: - mobsf_home = os.path.join(os.path.expanduser('~'), '.MobSF') + mobsf_home = Path.home() / '.MobSF' + custom_home = os.getenv('MOBSF_HOME_DIR') + if custom_home: + p = Path(custom_home) + if p.exists() and p.is_absolute() and p.is_dir(): + mobsf_home = p # MobSF Home Directory - if not os.path.exists(mobsf_home): - os.makedirs(mobsf_home) + if not mobsf_home.exists(): + mobsf_home.mkdir(parents=True, exist_ok=True) create_user_conf(mobsf_home, base_dir) else: mobsf_home = base_dir # Download Directory - dwd_dir = os.path.join(mobsf_home, 'downloads/') - if not os.path.exists(dwd_dir): - os.makedirs(dwd_dir) + dwd_dir = mobsf_home / 'downloads' + dwd_dir.mkdir(parents=True, exist_ok=True) # Screenshot Directory - screen_dir = os.path.join(dwd_dir, 'screen/') - if not os.path.exists(screen_dir): - os.makedirs(screen_dir) + screen_dir = mobsf_home / 'screen' + screen_dir.mkdir(parents=True, exist_ok=True) # Upload Directory - upload_dir = os.path.join(mobsf_home, 'uploads/') - if not os.path.exists(upload_dir): - os.makedirs(upload_dir) - # Signature Directory - sig_dir = os.path.join(mobsf_home, 'signatures/') + upload_dir = mobsf_home / 'uploads' + upload_dir.mkdir(parents=True, exist_ok=True) + # Downloaded tools + downloaded_tools_dir = mobsf_home / 'tools' + downloaded_tools_dir.mkdir(parents=True, exist_ok=True) + # Signatures Directory + sig_dir = mobsf_home / 'signatures' + sig_dir.mkdir(parents=True, exist_ok=True) if use_home: - src = os.path.join(base_dir, 'signatures/') + src = Path(base_dir) / 'signatures' try: - shutil.copytree(src, sig_dir) + shutil.copytree(src, sig_dir, dirs_exist_ok=True) except Exception: pass - elif not os.path.exists(sig_dir): - os.makedirs(sig_dir) - return mobsf_home + return mobsf_home.as_posix() except Exception: logger.exception('Creating MobSF Home Directory') def get_mobsf_version(): - return BANNER, VERSION, f'v{VERSION} Beta' + return BANNER, VERSION, f'v{VERSION}' + + +def load_source(modname, filename): + loader = machinery.SourceFileLoader(modname, filename) + spec = util.spec_from_file_location(modname, filename, loader=loader) + module = util.module_from_spec(spec) + loader.exec_module(module) + return module + + +def get_docker_secret_by_file(secret_key): + try: + secret_path = os.environ.get(secret_key) + path = Path(secret_path) + if path.exists() and path.is_file(): + return path.read_text().strip() + except Exception: + logger.exception('Cannot read secret from %s', secret_path) + raise Exception('Cannot read secret from file') + + +def get_secret_from_file_or_env(env_secret_key): + docker_secret_key = f'{env_secret_key}_FILE' + if os.environ.get(docker_secret_key): + return get_docker_secret_by_file(docker_secret_key) + else: + return os.environ[env_secret_key] + + +def api_key(home_dir): + """Print REST API Key.""" + # Form Docker Secrets + if os.environ.get('MOBSF_API_KEY_FILE'): + logger.info('\nAPI Key read from docker secrets') + try: + return get_docker_secret_by_file('MOBSF_API_KEY_FILE') + except Exception: + logger.exception('Cannot read API Key from docker secrets') + # From Environment Variable + if os.environ.get('MOBSF_API_KEY'): + logger.info('\nAPI Key read from environment variable') + return os.environ['MOBSF_API_KEY'] + home_dir = Path(home_dir) + secret_file = home_dir / 'secret' + if secret_file.exists() and secret_file.is_file(): + try: + _api_key = secret_file.read_bytes().strip() + return sha256(_api_key).hexdigest() + except Exception: + logger.exception('Cannot Read API Key') + return None diff --git a/mobsf/MobSF/management/commands/clear_tasks.py b/mobsf/MobSF/management/commands/clear_tasks.py new file mode 100644 index 0000000000..3e436f0384 --- /dev/null +++ b/mobsf/MobSF/management/commands/clear_tasks.py @@ -0,0 +1,18 @@ +from django.core.management.base import BaseCommand + +from django_q.models import ( + OrmQ, + Task, +) + +from mobsf.StaticAnalyzer.models import EnqueuedTask + + +class Command(BaseCommand): + help = 'Deletes all tasks in Django Q' # noqa: A003 + + def handle(self, *args, **kwargs): + Task.objects.all().delete() + OrmQ.objects.all().delete() + EnqueuedTask.objects.all().delete() + self.stdout.write(self.style.SUCCESS('Successfully deleted all Django Q tasks')) diff --git a/mobsf/MobSF/management/commands/create_roles.py b/mobsf/MobSF/management/commands/create_roles.py new file mode 100644 index 0000000000..fc457523ef --- /dev/null +++ b/mobsf/MobSF/management/commands/create_roles.py @@ -0,0 +1,12 @@ +"""Command to create Authorization Roles.""" +from django.core.management.base import BaseCommand + +from mobsf.MobSF.views.authorization import create_authorization_roles + + +class Command(BaseCommand): + help = 'Create Authorization Roles.' # noqa: A003 + + def handle(self, *args, **kwargs): + create_authorization_roles() + self.stdout.write('Roles Created Successfully!') diff --git a/mobsf/MobSF/security.py b/mobsf/MobSF/security.py index 97edc7cf85..3687c4faf4 100644 --- a/mobsf/MobSF/security.py +++ b/mobsf/MobSF/security.py @@ -2,13 +2,15 @@ import subprocess import functools import logging +import re import sys from shutil import which from pathlib import Path +from platform import system from concurrent.futures import ThreadPoolExecutor - from mobsf.MobSF.utils import ( + find_aapt, find_java_binary, gen_sha256_hash, get_adb, @@ -63,15 +65,29 @@ def generate_hashes(dirlocs): def get_executable_hashes(): # Internal Binaries shipped with MobSF base = Path(settings.BASE_DIR) + downloaded_tools = Path(settings.DOWNLOADED_TOOLS_DIR) manage_py = base.parent / 'manage.py' exec_loc = [ base / 'DynamicAnalyzer' / 'tools', base / 'StaticAnalyzer' / 'tools', + downloaded_tools, manage_py, ] + aapt = 'aapt' + aapt2 = 'aapt2' + if system() == 'Windows': + aapt = 'aapt.exe' + aapt2 = 'aapt2.exe' + aapts = [find_aapt(aapt), find_aapt(aapt2)] + exec_loc.extend(Path(a) for a in aapts if a) # External binaries used directly by MobSF system_bins = [ + 'aapt', + 'aapt.exe', + 'aapt2', + 'aapt2.exe', 'adb', + 'adb.exe', 'which', 'wkhtmltopdf', 'httptools', @@ -86,6 +102,8 @@ def get_executable_hashes(): 'BinSkim.exe', 'BinScope.exe', 'nuget.exe', + 'where.exe', + 'wkhtmltopdf.exe', ] for sbin in system_bins: bin_path = which(sbin) @@ -104,6 +122,9 @@ def get_executable_hashes(): settings.JTOOL_BINARY, settings.CLASSDUMP_BINARY, settings.CLASSDUMP_SWIFT_BINARY, + getattr(settings, 'BUNDLE_TOOL', ''), + getattr(settings, 'AAPT2_BINARY', ''), + getattr(settings, 'AAPT_BINARY', ''), ] for ubin in user_defined_bins: if ubin: @@ -130,9 +151,9 @@ def store_exec_hashes_at_first_run(): hashes['signature'] = signature EXECUTABLE_HASH_MAP = hashes except Exception: - logger.warning('Cannot calculate executable hashes, ' - 'disabling runtime executable ' - 'tampering detection') + logger.exception('Cannot calculate executable hashes, ' + 'disabling runtime executable ' + 'tampering detection') def subprocess_hook(oldfunc, *args, **kwargs): @@ -149,6 +170,7 @@ def subprocess_hook(oldfunc, *args, **kwargs): for arg in agmtz: if arg.endswith('.jar'): exec2 = Path(arg).as_posix() + break if '/' in exec1 or '\\' in exec1: exec1 = Path(exec1).as_posix() else: @@ -162,7 +184,7 @@ def subprocess_hook(oldfunc, *args, **kwargs): ' has been modified during runtime') logger.error(msg) raise Exception(msg) - if exec2 and exec1 in EXECUTABLE_HASH_MAP: + if exec2 and exec2 in EXECUTABLE_HASH_MAP: executable_in_hash_map = True if EXECUTABLE_HASH_MAP[exec2] != sha256(exec2): msg = ( @@ -193,3 +215,37 @@ def wrap_function(oldfunction, newfunction): def run(*args, **kwargs): return newfunction(oldfunction, *args, **kwargs) return run + + +def sanitize_redirect(url): + """Sanitize Redirect URL.""" + root = '/' + if url.startswith('//'): + return root + elif url.startswith('/'): + return url + return root + + +def sanitize_filename(filename): + """Sanitize Filename.""" + # Remove any characters + # that are not alphanumeric, hyphens, underscores, or dots + safe_filename = re.sub(r'[^a-zA-Z0-9._-]', '_', filename) + # Merge multiple underscores into one + safe_filename = re.sub(r'__+', '_', safe_filename) + # Remove leading and trailing underscores + safe_filename = safe_filename.strip('_') + return safe_filename + + +def sanitize_for_logging(filename: str, max_length: int = 255) -> str: + """Sanitize a filename to prevent log injection.""" + # Remove newline, carriage return, and other risky characters + filename = filename.replace('\n', '_').replace('\r', '_').replace('\t', '_') + + # Allow only safe characters (alphanumeric, underscore, dash, and period) + filename = re.sub(r'[^a-zA-Z0-9._-]', '_', filename) + + # Truncate filename to the maximum allowed length + return filename[:max_length] diff --git a/mobsf/MobSF/settings.py b/mobsf/MobSF/settings.py index cd52acad7e..b9525bfbe5 100644 --- a/mobsf/MobSF/settings.py +++ b/mobsf/MobSF/settings.py @@ -5,7 +5,6 @@ MobSF and Django settings """ -import imp import logging import os @@ -13,10 +12,11 @@ first_run, get_mobsf_home, get_mobsf_version, + get_secret_from_file_or_env, + load_source, ) logger = logging.getLogger(__name__) - # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # MOBSF CONFIGURATION # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -27,27 +27,29 @@ # MobSF Data Directory BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -MobSF_HOME = get_mobsf_home(USE_HOME, BASE_DIR) +MOBSF_HOME = get_mobsf_home(USE_HOME, BASE_DIR) # Download Directory -DWD_DIR = os.path.join(MobSF_HOME, 'downloads/') +DWD_DIR = os.path.join(MOBSF_HOME, 'downloads/') # Screenshot Directory -SCREEN_DIR = os.path.join(MobSF_HOME, 'downloads/screen/') +SCREEN_DIR = os.path.join(MOBSF_HOME, 'downloads/screen/') # Upload Directory -UPLD_DIR = os.path.join(MobSF_HOME, 'uploads/') +UPLD_DIR = os.path.join(MOBSF_HOME, 'uploads/') # Database Directory -DB_DIR = os.path.join(MobSF_HOME, 'db.sqlite3') +DB_DIR = os.path.join(MOBSF_HOME, 'db.sqlite3') # Signatures used by modules -SIGNATURE_DIR = os.path.join(MobSF_HOME, 'signatures/') +SIGNATURE_DIR = os.path.join(MOBSF_HOME, 'signatures/') # Tools Directory TOOLS_DIR = os.path.join(BASE_DIR, 'DynamicAnalyzer/tools/') +# Downloaded Tools Directory +DOWNLOADED_TOOLS_DIR = os.path.join(MOBSF_HOME, 'tools/') # Secret File -SECRET_FILE = os.path.join(MobSF_HOME, 'secret') +SECRET_FILE = os.path.join(MOBSF_HOME, 'secret') # ==========Load MobSF User Settings========== try: if USE_HOME: - USER_CONFIG = os.path.join(MobSF_HOME, 'config.py') - sett = imp.load_source('user_settings', USER_CONFIG) + USER_CONFIG = os.path.join(MOBSF_HOME, 'config.py') + sett = load_source('user_settings', USER_CONFIG) locals().update( # lgtm [py/modification-of-locals] {k: v for k, v in list(sett.__dict__.items()) if not k.startswith('__')}) @@ -59,7 +61,7 @@ CONFIG_HOME = False # ===MOBSF SECRET GENERATION AND DB MIGRATION==== -SECRET_KEY = first_run(SECRET_FILE, BASE_DIR, MobSF_HOME) +SECRET_KEY = first_run(SECRET_FILE, BASE_DIR, MOBSF_HOME) # =============ALLOWED DOWNLOAD EXTENSIONS===== ALLOWED_EXTENSIONS = { @@ -71,6 +73,9 @@ '.zip': 'application/zip', '.tar': 'application/x-tar', '.apk': 'application/octet-stream', + '.apks': 'application/octet-stream', + '.xapk': 'application/octet-stream', + '.aab': 'application/octet-stream', '.ipa': 'application/octet-stream', '.jar': 'application/java-archive', '.aar': 'application/octet-stream', @@ -78,6 +83,7 @@ '.dylib': 'application/octet-stream', '.a': 'application/octet-stream', '.pcap': 'application/vnd.tcpdump.pcap', + '.appx': 'application/vns.ms-appx', } # =============ALLOWED MIMETYPES================= APK_MIME = [ @@ -86,6 +92,7 @@ 'application/x-zip-compressed', 'binary/octet-stream', 'application/java-archive', + 'application/x-authorware-bin', ] IPA_MIME = [ 'application/iphone', @@ -107,7 +114,13 @@ 'application/vns.ms-appx', 'application/x-zip-compressed', ] - +# Supported File Extensions +ANDROID_EXTS = ( + 'apk', 'xapk', 'apks', 'zip', + 'aab', 'so', 'jar', 'aar', +) +IOS_EXTS = ('ipa', 'dylib', 'a') +WINDOWS_EXTS = ('appx',) # REST API only mode # Set MOBSF_API_ONLY to 1 to enable REST API only mode # In this mode, web UI related urls are disabled. @@ -136,38 +149,38 @@ # Database # https://docs.djangoproject.com/en/dev/ref/settings/#databases -# Sqlite3 support - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': DB_DIR, - }, -} -# End Sqlite3 support - -# Postgres DB - Install psycopg2 -""" -DATABASES = { - 'default': { +if (os.environ.get('POSTGRES_USER') + and (os.environ.get('POSTGRES_PASSWORD') + or os.environ.get('POSTGRES_PASSWORD_FILE')) + and os.environ.get('POSTGRES_HOST')): + # Postgres support + default = { 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'mobsf', + 'NAME': os.getenv('POSTGRES_DB', 'mobsf'), 'USER': os.environ['POSTGRES_USER'], - 'PASSWORD': os.environ['POSTGRES_PASSWORD'], + 'PASSWORD': get_secret_from_file_or_env('POSTGRES_PASSWORD'), 'HOST': os.environ['POSTGRES_HOST'], - 'PORT': 5432, + 'PORT': int(os.getenv('POSTGRES_PORT', 5432)), } +else: + # Sqlite3 support + default = { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': DB_DIR, + } +DATABASES = { + 'default': default, } -# End Postgres support -""" # =============================================== DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' -DEBUG = True +DEBUG = bool(os.getenv('MOBSF_DEBUG', '0') == '1') DJANGO_LOG_LEVEL = DEBUG +TEMPLATE_DEBUG = DEBUG ALLOWED_HOSTS = ['127.0.0.1', 'mobsf', '*'] # Application definition INSTALLED_APPS = ( # 'django.contrib.admin', + 'django_q', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', @@ -187,10 +200,14 @@ 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django_ratelimit.middleware.RatelimitMiddleware', ) MIDDLEWARE = ( 'mobsf.MobSF.views.api.api_middleware.RestApiAuthMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + ) ROOT_URLCONF = 'mobsf.MobSF.urls' WSGI_APPLICATION = 'mobsf.MobSF.wsgi.application' @@ -209,7 +226,13 @@ ], 'OPTIONS': { - 'debug': True, + 'debug': TEMPLATE_DEBUG, + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], }, }, ] @@ -220,6 +243,29 @@ STATICFILES_STORAGE = 'whitenoise.storage.CompressedStaticFilesStorage' # 256MB DATA_UPLOAD_MAX_MEMORY_SIZE = 268435456 +LOGIN_URL = 'login' +LOGOUT_REDIRECT_URL = '/' +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': ('django.contrib.auth.password_validation.' + 'UserAttributeSimilarityValidator'), + }, + { + 'NAME': ('django.contrib.auth.password_validation.' + 'MinimumLengthValidator'), + 'OPTIONS': { + 'min_length': 6, + }, + }, + { + 'NAME': ('django.contrib.auth.password_validation.' + 'CommonPasswordValidator'), + }, + { + 'NAME': ('django.contrib.auth.password_validation.' + 'NumericPasswordValidator'), + }, +] # Better logging LOGGING = { 'version': 1, @@ -247,7 +293,7 @@ 'logfile': { 'level': 'DEBUG', 'class': 'logging.FileHandler', - 'filename': os.path.join(MobSF_HOME, 'debug.log'), + 'filename': os.path.join(MOBSF_HOME, 'debug.log'), 'formatter': 'standard', }, 'console': { @@ -262,6 +308,11 @@ 'level': 'DEBUG', 'propagate': True, }, + 'django_q': { + 'handlers': ['console', 'logfile'], + 'level': 'DEBUG', + 'propagate': True, + }, 'django.db.backends': { 'handlers': ['console', 'logfile'], # DEBUG will log all queries, so change it to WARNING. @@ -290,11 +341,48 @@ }, }, } -JADX_TIMEOUT = int(os.getenv('MOBSF_JADX_TIMEOUT', 1800)) +ASYNC_ANALYSIS = bool(os.getenv('MOBSF_ASYNC_ANALYSIS', '0') == '1') +ASYNC_ANALYSIS_TIMEOUT = int(os.getenv('MOBSF_ASYNC_ANALYSIS_TIMEOUT', '60')) +Q_CLUSTER = { + 'name': 'scan_queue', + 'workers': int(os.getenv('MOBSF_ASYNC_WORKERS', '2')), + 'recycle': 100, + 'timeout': ASYNC_ANALYSIS_TIMEOUT * 60, + 'retry': (ASYNC_ANALYSIS_TIMEOUT * 60) + 100, + 'compress': True, + 'label': 'scan_queue', + 'orm': 'default', + 'max_attempts': 1, + 'save_limit': -1, + 'ack_failures': True, +} +QUEUE_MAX_SIZE = 100 +MULTIPROCESSING = os.getenv('MOBSF_MULTIPROCESSING') +JADX_TIMEOUT = int(os.getenv('MOBSF_JADX_TIMEOUT', 1000)) +SAST_TIMEOUT = int(os.getenv('MOBSF_SAST_TIMEOUT', 1000)) +BINARY_ANALYSIS_TIMEOUT = int(os.getenv('MOBSF_BINARY_ANALYSIS_TIMEOUT', 600)) +DISABLE_AUTHENTICATION = os.getenv('MOBSF_DISABLE_AUTHENTICATION') +RATELIMIT = os.getenv('MOBSF_RATELIMIT', '7/m') +USE_X_FORWARDED_HOST = bool( + os.getenv('MOBSF_USE_X_FORWARDED_HOST', '1') == '1') +USE_X_FORWARDED_PORT = bool( + os.getenv('MOBSF_USE_X_FORWARDED_PORT', '1') == '1') +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') # =========================== # ENTERPRISE FEATURE REQUESTS # =========================== EFR_01 = os.getenv('EFR_01', '0') +# SAML SSO +# IdP Configuration +IDP_METADATA_URL = os.getenv('MOBSF_IDP_METADATA_URL') +IDP_ENTITY_ID = os.getenv('MOBSF_IDP_ENTITY_ID') +IDP_SSO_URL = os.getenv('MOBSF_IDP_SSO_URL') +IDP_X509CERT = os.getenv('MOBSF_IDP_X509CERT') +IDP_IS_ADFS = os.getenv('MOBSF_IDP_IS_ADFS', '0') +# SP Configuration +SP_HOST = os.getenv('MOBSF_SP_HOST') +SP_ALLOW_PASSWORD = os.getenv('MOBSF_SP_ALLOW_PASSWORD', '0') +# =================== # USER CONFIGURATION # =================== if CONFIG_HOME: @@ -349,7 +437,6 @@ DOMAIN_MALWARE_SCAN = os.getenv('MOBSF_DOMAIN_MALWARE_SCAN', '1') APKID_ENABLED = os.getenv('MOBSF_APKID_ENABLED', '1') - QUARK_ENABLED = bool(os.getenv('MOBSF_QUARK_ENABLED', '')) # ================================================== # ======WINDOWS STATIC ANALYSIS SETTINGS =========== # Private key @@ -369,11 +456,14 @@ """ # Android 3P Tools + BUNDLE_TOOL = os.getenv('MOBSF_BUNDLE_TOOL', '') JADX_BINARY = os.getenv('MOBSF_JADX_BINARY', '') BACKSMALI_BINARY = os.getenv('MOBSF_BACKSMALI_BINARY', '') VD2SVG_BINARY = os.getenv('MOBSF_VD2SVG_BINARY', '') APKTOOL_BINARY = os.getenv('MOBSF_APKTOOL_BINARY', '') ADB_BINARY = os.getenv('MOBSF_ADB_BINARY', '') + AAPT2_BINARY = os.getenv('MOBSF_AAPT2_BINARY', '') + AAPT_BINARY = os.getenv('MOBSF_AAPT_BINARY', '') # iOS 3P Tools JTOOL_BINARY = os.getenv('MOBSF_JTOOL_BINARY', '') diff --git a/mobsf/MobSF/tools_download.py b/mobsf/MobSF/tools_download.py new file mode 100644 index 0000000000..2039944678 --- /dev/null +++ b/mobsf/MobSF/tools_download.py @@ -0,0 +1,118 @@ +import logging +import sys +import shutil +import tempfile +import zipfile +import platform +from pathlib import Path +from urllib.request import ( + ProxyHandler, + Request, + build_opener, + getproxies, +) + +logging.basicConfig( + level=logging.INFO, + format='[%(levelname)s] %(asctime)-15s - %(message)s', + datefmt='%d/%b/%Y %H:%M:%S') +logger = logging.getLogger(__name__) + + +def download_file(url, file_path): + req = Request(url) + system_proxies = getproxies() + proxy_handler = ProxyHandler(system_proxies) + opener = build_opener(proxy_handler) + + with opener.open(req) as response: + if response.status == 200: + file_size = int(response.headers.get('Content-Length', 0)) + downloaded = 0 + block_size = 8192 # 8KB + + with open(file_path, 'wb') as f: + while True: + buffer = response.read(block_size) + if not buffer: + break + downloaded += len(buffer) + f.write(buffer) + + # Print progress + if file_size > 0: + done = int(50 * downloaded / file_size) + fmt = (f'\r[{"#" * done}{"-" * (50 - done)}] ' + f'{downloaded * 100 / file_size:.2f}%') + sys.stdout.write(fmt) + sys.stdout.flush() + + if downloaded != file_size: + err = (f'Downloaded file size ({downloaded}) ' + f'does not match expected size ({file_size})') + raise Exception(err) + + return downloaded + else: + raise Exception(f'Failed to download file. Status code: {response.status}') + + +def install_jadx(mobsf_home, version='1.5.0'): + """Install JADX dynamically.""" + try: + url = ('https://github.com/skylot/jadx/releases/download/' + f'v{version}/jadx-{version}.zip') + jadx_dir = Path(mobsf_home) / 'tools' / 'jadx' + extract_dir = jadx_dir / f'jadx-{version}' + + if extract_dir.exists(): + logger.info('JADX is already installed at %s', extract_dir) + return + + logger.info('Downloading JADX from %s', url) + shutil.rmtree(jadx_dir, ignore_errors=True) + + with tempfile.NamedTemporaryFile( + delete=False, + mode='wb', + suffix='.zip') as tmp_zip_file: + + downloaded_size = download_file(url, tmp_zip_file.name) + logger.info('JADX download complete. File size: %d bytes', downloaded_size) + + # Extract the zip file + logger.info('Extracting JADX to %s', extract_dir) + extract_dir.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(tmp_zip_file.name, 'r') as zip_ref: + for member in zip_ref.namelist(): + zip_ref.extract(member, extract_dir) + + # Set execute permission + set_rwxr_xr_x_permission_recursively(extract_dir) + + logger.info('JADX installed successfully') + except Exception: + logger.exception('Error during JADX installation') + finally: + if 'tmp_zip_file' in locals(): + Path(tmp_zip_file.name).unlink() + + +def set_rwxr_xr_x_permission_recursively(directory_path): + """Set execute permissions recursively.""" + if platform.system() == 'Windows': + logger.info('Permission setting is skipped on non-Unix systems.') + return + + logger.info('Setting execute permission for JADX directory') + directory_path.chmod(0o755) + + # Recursively set permissions for all files and + # directories within the root directory + for path in directory_path.rglob('*'): + path.chmod(0o755) + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + install_jadx(sys.argv[1]) diff --git a/mobsf/MobSF/urls.py b/mobsf/MobSF/urls.py index 808f859905..35c1e3e5a5 100755 --- a/mobsf/MobSF/urls.py +++ b/mobsf/MobSF/urls.py @@ -22,18 +22,24 @@ init_exec_hooks, store_exec_hashes_at_first_run, ) -from mobsf.MobSF.views import home +from mobsf.MobSF.views import ( + authentication, + authorization, + home, + saml2, +) from mobsf.MobSF.views.api import api_static_analysis as api_sz from mobsf.MobSF.views.api import api_android_dynamic_analysis as api_dz from mobsf.MobSF.views.api import api_ios_dynamic_analysis as api_idz from mobsf.StaticAnalyzer import tests from mobsf.StaticAnalyzer.views.common import ( appsec, + async_task, pdf, shared_func, suppression, ) -from mobsf.StaticAnalyzer.views.android import ( +from mobsf.StaticAnalyzer.views.android.views import ( find, manifest_view, source_tree, @@ -42,16 +48,47 @@ from mobsf.StaticAnalyzer.views.windows import windows from mobsf.StaticAnalyzer.views.android import static_analyzer as android_sa from mobsf.StaticAnalyzer.views.ios import static_analyzer as ios_sa -from mobsf.StaticAnalyzer.views.ios import view_source as io_view_source +from mobsf.StaticAnalyzer.views.ios.views import view_source as io_view_source from . import settings +bundle_id_regex = r'(?P([a-zA-Z0-9]{1}[\w.-]{1,255}))$' +checksum_regex = r'(?P[0-9a-f]{32})' +paginate = r'(?P[0-9]{1,10})/(?P[0-9]{1,10})' urlpatterns = [ + re_path(r'^login/$', + authentication.login_view, + name='login'), + re_path(r'^logout$', + authentication.logout_view, + name='logout'), + re_path(r'^change_password/$', + authentication.change_password, + name='change_password'), + re_path(r'^users/$', + authorization.users, + name='users'), + re_path(r'^create_user/$', + authorization.create_user, + name='create_user'), + re_path(r'^delete_user/$', + authorization.delete_user, + name='delete_user'), + # SAML2 + re_path(r'^sso/$', + saml2.saml_login, + name='saml_login'), + re_path(r'^sso/acs/$', + saml2.saml_acs, + name='saml_acs'), # REST API # Static Analysis re_path(r'^api/v1/upload$', api_sz.api_upload), re_path(r'^api/v1/scan$', api_sz.api_scan), + re_path(r'^api/v1/search$', api_sz.api_search), + re_path(r'^api/v1/scan_logs$', api_sz.api_scan_logs), + re_path(r'^api/v1/tasks$', api_sz.api_tasks), re_path(r'^api/v1/delete_scan$', api_sz.api_delete_scan), re_path(r'^api/v1/download_pdf$', api_sz.api_pdf_report), re_path(r'^api/v1/report_json$', api_sz.api_json_report), @@ -147,8 +184,11 @@ urlpatterns.extend([ # General re_path(r'^$', home.index, name='home'), - re_path(r'^upload/$', home.Upload.as_view), + re_path(r'^upload/$', home.Upload.as_view, name='upload'), re_path(r'^download/', home.download, name='download'), + re_path(fr'^download_binary/{checksum_regex}/$', + home.download_binary, + name='download_binary'), re_path(r'^download_scan/', home.download_apk, name='download_scan'), re_path(r'^generate_downloads/$', home.generate_download, @@ -157,39 +197,45 @@ re_path(r'^donate$', home.donate, name='donate'), re_path(r'^api_docs$', home.api_docs, name='api_docs'), re_path(r'^recent_scans/$', home.recent_scans, name='recent'), + re_path(fr'^recent_scans/{paginate}/$', + home.recent_scans, + name='scans_paginated'), re_path(r'^delete_scan/$', home.delete_scan, name='delete_scan'), re_path(r'^search$', home.search), + re_path(r'^status/$', home.scan_status, name='status'), re_path(r'^error/$', home.error, name='error'), - re_path(r'^not_found/$', home.not_found), re_path(r'^zip_format/$', home.zip_format), + re_path(r'^robots.txt$', home.robots_txt), re_path(r'^dynamic_analysis/$', home.dynamic_analysis, name='dynamic'), - + re_path(r'^tasks$', + async_task.list_tasks, + name='list_tasks'), # Static Analysis # Android - re_path(r'^static_analyzer/(?P[0-9a-f]{32})/$', + re_path(fr'^static_analyzer/{checksum_regex}/$', android_sa.static_analyzer, name='static_analyzer'), # Remove this is version 4/5 re_path(r'^source_code/$', source_tree.run, name='tree_view'), re_path(r'^view_file/$', view_source.run, name='view_source'), re_path(r'^find/$', find.run, name='find_files'), - re_path(r'^manifest_view/(?P[0-9a-f]{32})/$', + re_path(fr'^manifest_view/{checksum_regex}/$', manifest_view.run, name='manifest_view'), # IOS - re_path(r'^static_analyzer_ios/(?P[0-9a-f]{32})/$', + re_path(fr'^static_analyzer_ios/{checksum_regex}/$', ios_sa.static_analyzer_ios, name='static_analyzer_ios'), re_path(r'^view_file_ios/$', io_view_source.run, name='view_file_ios'), # Windows - re_path(r'^static_analyzer_windows/(?P[0-9a-f]{32})/$', + re_path(fr'^static_analyzer_windows/{checksum_regex}/$', windows.staticanalyzer_windows, name='static_analyzer_windows'), # Shared - re_path(r'^pdf/(?P[0-9a-f]{32})/$', pdf.pdf, name='pdf'), - re_path(r'^appsec_dashboard/(?P[0-9a-f]{32})/$', + re_path(fr'^pdf/{checksum_regex}/$', pdf.pdf, name='pdf'), + re_path(fr'^appsec_dashboard/{checksum_regex}/$', appsec.appsec_dashboard, name='appsec_dashboard'), # Suppression @@ -209,21 +255,21 @@ re_path(r'^compare/(?P[0-9a-f]{32})/(?P[0-9a-f]{32})/$', shared_func.compare_apps), # Relative Shared & Dynamic Library scan - re_path(r'^scan_library/(?P[0-9a-f]{32})$', + re_path(fr'^scan_library/{checksum_regex}$', shared_func.scan_library, name='scan_library'), # Dynamic Analysis re_path(r'^android/dynamic_analysis/$', dz.android_dynamic_analysis, name='dynamic_android'), - re_path(r'^android_dynamic/(?P[0-9a-f]{32})$', + re_path(fr'^android_dynamic/{checksum_regex}$', dz.dynamic_analyzer, name='dynamic_analyzer'), re_path(r'^httptools$', dz.httptools_start, name='httptools'), re_path(r'^logcat/$', dz.logcat), - re_path(r'^static_scan/(?P[0-9a-f]{32})$', + re_path(fr'^static_scan/{checksum_regex}$', dz.trigger_static_analysis, name='static_scan'), # Android Operations @@ -245,6 +291,9 @@ re_path(r'^start_activity/$', tests_common.start_activity, name='start_activity'), + re_path(r'^start_deeplink/$', + tests_common.start_deeplink, + name='start_deeplink'), re_path(r'^download_data/$', tests_common.download_data), re_path(r'^collect_logs/$', tests_common.collect_logs), re_path(r'^tls_tests/$', tests_common.tls_tests), @@ -260,7 +309,7 @@ name='frida_logs'), re_path(r'^get_dependencies/$', tests_frida.get_runtime_dependencies), # Report - re_path(r'^dynamic_report/(?P[0-9a-f]{32})$', + re_path(fr'^dynamic_report/{checksum_regex}$', report.view_report, name='dynamic_report'), # Shared @@ -304,7 +353,7 @@ re_path(r'^ios/list_apps/$', instance.list_apps, name='list_apps'), - re_path(r'^ios/setup_environment/(?P[0-9a-f]{32})$', + re_path(fr'^ios/setup_environment/{checksum_regex}$', instance.setup_environment, name='setup_environment'), re_path(r'^ios/dynamic_analyzer/$', @@ -343,13 +392,13 @@ re_path(r'^ios/system_logs/$', instance.system_logs, name='ios_system_logs'), - re_path(r'^ios/download_data/(?P([\w-]*\.)+[\w-]{2,155})$', + re_path(fr'^ios/download_data/{bundle_id_regex}', instance.download_data, name='ios_download_data'), re_path(r'^ios/instrument/$', ios_tests_frida.ios_instrument, name='ios_instrument'), - re_path(r'^ios/view_report/(?P([\w-]*\.)+[\w-]{2,155})$', + re_path(fr'^ios/view_report/{bundle_id_regex}', ios_view_report.ios_view_report, name='ios_view_report'), diff --git a/mobsf/MobSF/utils.py b/mobsf/MobSF/utils.py index 358fdce40d..d24e4367a4 100755 --- a/mobsf/MobSF/utils.py +++ b/mobsf/MobSF/utils.py @@ -22,7 +22,12 @@ import threading from urllib.parse import urlparse from pathlib import Path -from distutils.version import StrictVersion +from concurrent.futures import ( + ThreadPoolExecutor, + TimeoutError as ThreadPoolTimeoutError, +) + +from packaging.version import Version import distro @@ -31,6 +36,10 @@ import requests from django.shortcuts import render +from django.utils import timezone + +from mobsf.StaticAnalyzer.models import RecentScansDB +from mobsf.MobSF. init import api_key from . import settings @@ -49,11 +58,16 @@ ), re.UNICODE) EMAIL_REGEX = re.compile(r'[\w+.-]{1,20}@[\w-]{1,20}\.[\w]{2,10}') +USERNAME_REGEX = re.compile(r'^\w[\w\-\@\.]{1,35}$') +GOOGLE_API_KEY_REGEX = re.compile(r'AIza[0-9A-Za-z-_]{35}$') +GOOGLE_APP_ID_REGEX = re.compile(r'\d{1,2}:\d{1,50}:android:[a-f0-9]{1,50}') +PKG_REGEX = re.compile( + r'package\s+([a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*);') class Color(object): GREEN = '\033[92m' - ORANGE = '\033[33m' + GREY = '\033[0;37m' RED = '\033[91m' BOLD = '\033[1m' END = '\033[0m' @@ -84,19 +98,15 @@ def upstream_proxy(flaw_type): return proxies, verify -def api_key(): - """Print REST API Key.""" - if os.environ.get('MOBSF_API_KEY'): - logger.info('\nAPI Key read from environment variable') - return os.environ['MOBSF_API_KEY'] - - secret_file = os.path.join(settings.MobSF_HOME, 'secret') - if is_file_exists(secret_file): - try: - _api_key = open(secret_file).read().strip() - return gen_sha256_hash(_api_key) - except Exception: - logger.exception('Cannot Read API Key') +def get_system_resources(): + """Get CPU and Memory Available.""" + # Get number of physical cores + physical_cores = psutil.cpu_count(logical=False) + # Get number of logical processors (threads) + logical_processors = psutil.cpu_count(logical=True) + # Get total RAM + total_ram = psutil.virtual_memory().total / (1024 ** 3) # Convert bytes to GB + return physical_cores, logical_processors, total_ram def print_version(): @@ -104,12 +114,16 @@ def print_version(): logger.info(settings.BANNER) ver = settings.MOBSF_VER logger.info('Author: Ajin Abraham | opensecurity.in') + mobsf_api_key = api_key(settings.MOBSF_HOME) if platform.system() == 'Windows': logger.info('Mobile Security Framework %s', ver) - print('REST API Key: ' + api_key()) + print(f'REST API Key: {mobsf_api_key}') + print('Default Credentials: mobsf/mobsf') else: - logger.info('\033[1m\033[34mMobile Security Framework %s\033[0m', ver) - print('REST API Key: ' + Color.BOLD + api_key() + Color.END) + logger.info( + '%sMobile Security Framework %s%s', Color.GREY, ver, Color.END) + print(f'REST API Key: {Color.BOLD}{mobsf_api_key}{Color.END}') + print(f'Default Credentials: {Color.BOLD}mobsf/mobsf{Color.END}') os = platform.system() pltfm = platform.platform() dist = ' '.join(distro.linux_distribution( @@ -119,6 +133,8 @@ def print_version(): dst_str = f' ({dist}) ' env_str = f'OS Environment: {os}{dst_str}{pltfm}' logger.info(env_str) + cores, threads, ram = get_system_resources() + logger.info('CPU Cores: %s, Threads: %s, RAM: %.2f GB', cores, threads, ram) find_java_binary() check_basic_env() thread = threading.Thread(target=check_update, name='check_update') @@ -141,8 +157,8 @@ def check_update(): proxies=proxies, verify=verify) remote_version = response.next.path_url.split('v')[1] if remote_version: - sem_loc = StrictVersion(local_version) - sem_rem = StrictVersion(remote_version) + sem_loc = Version(local_version) + sem_rem = Version(remote_version) if sem_loc < sem_rem: logger.warning('A new version of MobSF is available, ' 'Please update to %s from master branch.', @@ -181,6 +197,32 @@ def find_java_binary(): return 'java' +def find_aapt(tool_name): + """Find the specified tool (aapt or aapt2).""" + # Check system PATH for the tool + tool_path = shutil.which(tool_name) + if tool_path: + return tool_path + + # Check common Android SDK locations + home_dir = Path.home() # Get the user's home directory + sdk_paths = [ + home_dir / 'Library' / 'Android' / 'sdk', # macOS + home_dir / 'Android' / 'Sdk', # Linux + home_dir / 'AppData' / 'Local' / 'Android' / 'Sdk', # Windows + ] + + for sdk_path in sdk_paths: + build_tools_path = sdk_path / 'build-tools' + if build_tools_path.exists(): + for version in sorted(build_tools_path.iterdir(), reverse=True): + tool_path = version / tool_name + if tool_path.exists(): + return str(tool_path) + + return None + + def print_n_send_error_response(request, msg, api=False, @@ -207,6 +249,8 @@ def filename_from_path(path): def get_md5(data): + if isinstance(data, str): + data = data.encode('utf-8') return hashlib.md5(data).hexdigest() @@ -621,7 +665,7 @@ def strict_package_check(user_input): For android package and ios bundle id """ - pat = re.compile(r'^([\w-]*\.)+[\w-]{2,155}$') + pat = re.compile(r'^([a-zA-Z]{1}[\w.-]{1,255})$') resp = re.match(pat, user_input) if not resp or '..' in user_input: logger.error('Invalid package name/bundle id/class name') @@ -662,6 +706,8 @@ def common_check(instance_id): def is_path_traversal(user_input): """Check for path traversal.""" + if not user_input: + return False if (('../' in user_input) or ('%2e%2e' in user_input) or ('..' in user_input) @@ -753,6 +799,11 @@ def replace(value, arg): return value.replace(what, to) +def pathify(value): + """Convert to path.""" + return value.replace('.', '/') + + def relative_path(value): """Show relative path to two parents.""" sep = None @@ -826,6 +877,7 @@ def get_android_dm_exception_msg(): def get_android_src_dir(app_dir, typ): """Get Android source code location.""" + src = None if typ == 'apk': src = app_dir / 'java_source' elif typ == 'studio': @@ -872,13 +924,27 @@ def valid_host(host): return False # Local network invalid_prefix = ( + '100.64.', '127.', '192.', + '198.', '10.', '172.', - '169', + '169.', '0.', - 'localhost') + '203.0.', + '224.0.', + '240.0', + '255.255.', + 'localhost', + '::1', + '64::ff9b::', + '100::', + '2001::', + '2002::', + 'fc00::', + 'fe80::', + 'ff00::') if domain.startswith(invalid_prefix): return False ip = socket.gethostbyname(domain) @@ -888,3 +954,70 @@ def valid_host(host): return True except Exception: return False + + +def append_scan_status(checksum, status, exception=None): + """Append Scan Status to Database.""" + try: + db_obj = RecentScansDB.objects.get(MD5=checksum) + if status == 'init': + db_obj.SCAN_LOGS = [] + db_obj.save() + return + current_logs = python_dict(db_obj.SCAN_LOGS) + current_logs.append({ + 'timestamp': timezone.now().strftime('%Y-%m-%d %H:%M:%S'), + 'status': status, + 'exception': exception}) + db_obj.SCAN_LOGS = current_logs + db_obj.save() + except RecentScansDB.DoesNotExist: + # Expected to fail for iOS Dynamic Analysis Report Generation + # Calls MalwareScan and TrackerScan with different checksum + pass + except Exception: + logger.exception('Appending Scan Status to Database') + + +def get_scan_logs(checksum): + """Get the scan logs for the given checksum.""" + try: + db_entry = RecentScansDB.objects.filter(MD5=checksum) + if db_entry.exists(): + return python_list(db_entry[0].SCAN_LOGS) + except Exception: + msg = 'Fetching scan logs from the DB failed.' + logger.exception(msg) + return [] + + +class TaskTimeoutError(Exception): + pass + + +def run_with_timeout(func, limit, *args, **kwargs): + with ThreadPoolExecutor(max_workers=1) as executor: + future = executor.submit(func, *args, **kwargs) + try: + return future.result(timeout=limit) + except ThreadPoolTimeoutError: + msg = f'function <{func.__name__}> timed out after {limit} seconds' + raise TaskTimeoutError(msg) + + +def set_permissions(path): + base_path = Path(path) + # Read/Write for directories without execute + perm_dir = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH + # Read/Write for files + perm_file = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH + + # Set permissions for directories and files + for item in base_path.rglob('*'): + try: + if item.is_dir(): + item.chmod(perm_dir) + elif item.is_file(): + item.chmod(perm_file) + except Exception: + pass diff --git a/mobsf/MobSF/views/api/api_middleware.py b/mobsf/MobSF/views/api/api_middleware.py index 48bc992e5f..efa418641c 100644 --- a/mobsf/MobSF/views/api/api_middleware.py +++ b/mobsf/MobSF/views/api/api_middleware.py @@ -1,9 +1,12 @@ # -*- coding: utf_8 -*- """REST API Middleware.""" +from hmac import compare_digest + from django.http import JsonResponse from django.utils.deprecation import MiddlewareMixin +from django.conf import settings -from mobsf.MobSF.utils import api_key +from mobsf.MobSF.init import api_key OK = 200 @@ -11,8 +14,9 @@ def make_api_response(data, status=OK): """Make API Response.""" resp = JsonResponse( - data=data, # lgtm [py/stack-trace-exposure] - status=status) + data=data, + status=status, + safe=False) resp['Access-Control-Allow-Origin'] = '*' resp['Access-Control-Allow-Methods'] = 'POST' resp['Access-Control-Allow-Headers'] = 'Authorization, X-Mobsf-Api-Key' @@ -22,10 +26,11 @@ def make_api_response(data, status=OK): def api_auth(meta): """Check if API Key Matches.""" + mobsf_api_key = api_key(settings.MOBSF_HOME) if 'HTTP_X_MOBSF_API_KEY' in meta: - return bool(api_key() == meta['HTTP_X_MOBSF_API_KEY']) + return compare_digest(mobsf_api_key, meta['HTTP_X_MOBSF_API_KEY']) elif 'HTTP_AUTHORIZATION' in meta: - return bool(api_key() == meta['HTTP_AUTHORIZATION']) + return compare_digest(mobsf_api_key, meta['HTTP_AUTHORIZATION']) return False diff --git a/mobsf/MobSF/views/api/api_static_analysis.py b/mobsf/MobSF/views/api/api_static_analysis.py index bf3bcaaf4a..436ce02971 100755 --- a/mobsf/MobSF/views/api/api_static_analysis.py +++ b/mobsf/MobSF/views/api/api_static_analysis.py @@ -2,20 +2,28 @@ """MobSF REST API V 1.""" from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt +from django.conf import settings from mobsf.StaticAnalyzer.models import ( RecentScansDB, ) from mobsf.MobSF.utils import ( + get_scan_logs, is_md5, ) from mobsf.MobSF.views.helpers import request_method -from mobsf.MobSF.views.home import RecentScans, Upload, delete_scan +from mobsf.MobSF.views.home import ( + RecentScans, + Upload, + delete_scan, + search, +) from mobsf.MobSF.views.api.api_middleware import make_api_response -from mobsf.StaticAnalyzer.views.android import view_source +from mobsf.StaticAnalyzer.views.android.views import view_source from mobsf.StaticAnalyzer.views.android.static_analyzer import static_analyzer -from mobsf.StaticAnalyzer.views.ios import view_source as ios_view_source +from mobsf.StaticAnalyzer.views.ios.views import view_source as ios_view_source from mobsf.StaticAnalyzer.views.ios.static_analyzer import static_analyzer_ios +from mobsf.StaticAnalyzer.views.common.async_task import list_tasks from mobsf.StaticAnalyzer.views.common.shared_func import compare_apps from mobsf.StaticAnalyzer.views.common.suppression import ( delete_suppression, @@ -66,7 +74,7 @@ def api_scan(request): {'error': 'The file is not uploaded/available'}, 500) scan_type = robj[0].SCAN_TYPE # APK, Source Code (Android/iOS) ZIP, SO, JAR, AAR - if scan_type in {'xapk', 'apk', 'apks', 'zip', 'so', 'jar', 'aar'}: + if scan_type in settings.ANDROID_EXTS: resp = static_analyzer(request, checksum, True) if 'type' in resp: resp = static_analyzer_ios(request, checksum, True) @@ -75,14 +83,14 @@ def api_scan(request): else: response = make_api_response(resp, 200) # IPA - elif scan_type in {'ipa', 'dylib', 'a'}: + elif scan_type in settings.IOS_EXTS: resp = static_analyzer_ios(request, checksum, True) if 'error' in resp: response = make_api_response(resp, 500) else: response = make_api_response(resp, 200) # APPX - elif scan_type == 'appx': + elif scan_type in settings.WINDOWS_EXTS: resp = windows.staticanalyzer_windows(request, checksum, True) if 'error' in resp: response = make_api_response(resp, 500) @@ -91,6 +99,31 @@ def api_scan(request): return response +@request_method(['POST']) +@csrf_exempt +def api_scan_logs(request): + """POST - Get Scan logs.""" + if 'hash' not in request.POST: + return make_api_response( + {'error': 'Missing Parameters'}, 422) + resp = get_scan_logs(request.POST['hash']) + if not resp: + return make_api_response( + {'error': 'No scan logs found'}, 400) + return make_api_response({'logs': resp}, 200) + + +@request_method(['POST']) +@csrf_exempt +def api_tasks(request): + """POST - Get Scan Queue.""" + resp = list_tasks(request, True) + if not resp: + return make_api_response( + {'error': 'Scan queue empty'}, 400) + return make_api_response(resp, 200) + + @request_method(['POST']) @csrf_exempt def api_delete_scan(request): @@ -161,6 +194,21 @@ def api_json_report(request): return response +@request_method(['POST']) +@csrf_exempt +def api_search(request): + """Search by checksum or text.""" + if 'query' not in request.POST: + return make_api_response( + {'error': 'Missing Parameters'}, 422) + resp = search(request, api=True) + if 'checksum' in resp: + request.POST = {'hash': resp['checksum']} + return api_json_report(request) + elif 'error' in resp: + return make_api_response(resp, 404) + + @request_method(['POST']) @csrf_exempt def api_view_source(request): diff --git a/mobsf/MobSF/views/apk_downloader.py b/mobsf/MobSF/views/apk_downloader.py index 923fafd14a..ca5a0cd4c9 100644 --- a/mobsf/MobSF/views/apk_downloader.py +++ b/mobsf/MobSF/views/apk_downloader.py @@ -16,6 +16,7 @@ handle_uploaded_file, ) from mobsf.MobSF.utils import ( + is_internet_available, is_path_traversal, is_zip_magic, strict_package_check, @@ -36,6 +37,7 @@ def fetch_html(url): try: proxies, verify = upstream_proxy('https') res = requests.get(url, + timeout=5, headers=headers, proxies=proxies, verify=verify, @@ -52,6 +54,7 @@ def download_file(url, outfile): logger.info('Downloading APK...') proxies, verify = upstream_proxy('https') with requests.get(url, + timeout=5, stream=True, proxies=proxies, verify=verify) as r: @@ -133,6 +136,9 @@ def apk_download(package): downloaded_file = None data = None try: + if not is_internet_available(): + logger.warning('Internet Not Available. Unable to download APK') + return None if not strict_package_check(package) or is_path_traversal(package): return None logger.info('Attempting to download: %s', package) diff --git a/mobsf/MobSF/views/authentication.py b/mobsf/MobSF/views/authentication.py new file mode 100644 index 0000000000..ddd9f07853 --- /dev/null +++ b/mobsf/MobSF/views/authentication.py @@ -0,0 +1,121 @@ +"""User Login and Logout.""" +from inspect import signature + +from django.shortcuts import ( + redirect, + render, +) +from django.contrib.auth import ( + login, + logout, + update_session_auth_hash, +) +from django.contrib.auth.forms import ( + AuthenticationForm, + PasswordChangeForm, +) +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.decorators import login_required as lg + +from mobsf.MobSF.security import ( + sanitize_redirect, +) + +from django_ratelimit.decorators import ratelimit + + +def login_required(func): + """Login required decorator.""" + sig = signature(func) + + def wrapper(request, *args, **kwargs): + arguments = sig.bind(request, *args, **kwargs) + api = arguments.arguments.get('api') + # Handle functions that are used by API and Web + if settings.DISABLE_AUTHENTICATION == '1' or api: + # Disable authentication for all functions + return func(request, *args, **kwargs) + # Force authentication for all + # web function calls + return lg(func)(request, *args, **kwargs) + return wrapper + + +@ratelimit(key='user_or_ip', + rate=settings.RATELIMIT, + method='POST', + block=True) +def login_view(request): + """Login Controller.""" + if settings.DISABLE_AUTHENTICATION == '1': + return redirect('/') + sso = (settings.IDP_METADATA_URL + or (settings.IDP_SSO_URL + and settings.IDP_ENTITY_ID + and settings.IDP_X509CERT)) + if not sso: + allow_pwd = True + elif bool(settings.SP_ALLOW_PASSWORD == '1'): + allow_pwd = True + else: + allow_pwd = False + nextp = request.GET.get('next', '') + redirect_url = sanitize_redirect(nextp) + if request.user.is_authenticated: + return redirect(redirect_url) + if request.method == 'POST': + if sso and not allow_pwd: + return redirect('/') + form = AuthenticationForm(request, request.POST) + if form.is_valid(): + user = form.get_user() + login(request, user) + return redirect(redirect_url) + else: + form = AuthenticationForm() + context = { + 'title': 'Sign In', + 'version': settings.VERSION, + 'next': redirect_url, + 'form': form, + 'sso': sso, + 'allow_pwd': allow_pwd, + } + return render(request, 'auth/login.html', context) + + +def logout_view(request): + """Logout Controller.""" + logout(request) + return redirect(settings.LOGIN_URL) + + +@login_required +def change_password(request): + if settings.DISABLE_AUTHENTICATION == '1': + return redirect('/') + if request.method == 'POST': + form = PasswordChangeForm(request.user, request.POST) + if form.is_valid(): + user = form.save() + update_session_auth_hash(request, user) + messages.success( + request, + 'Your password was successfully updated!') + return redirect('change_password') + else: + messages.error( + request, + 'Please correct the error below.') + else: + form = PasswordChangeForm(request.user) + context = { + 'title': 'Change Password', + 'version': settings.VERSION, + 'form': form, + } + return render( + request, + 'auth/change_password.html', + context) diff --git a/mobsf/MobSF/views/authorization.py b/mobsf/MobSF/views/authorization.py new file mode 100644 index 0000000000..b1d724036e --- /dev/null +++ b/mobsf/MobSF/views/authorization.py @@ -0,0 +1,189 @@ +"""User management and authorization.""" +from itertools import chain +from inspect import signature +from functools import wraps +from enum import Enum +import logging + +from django.contrib.auth.models import ( + Group, + Permission, + User, +) +from django.shortcuts import ( + redirect, + render, +) +from django.contrib import messages +from django.contrib.auth import get_user_model +from django.contrib.admin.views.decorators import staff_member_required +from django.contrib.auth.decorators import ( + login_required, + permission_required as pr, +) +from django.views.decorators.http import require_http_methods +from django.template.defaulttags import register +from django.conf import settings + +from mobsf.MobSF.forms import RegisterForm +from mobsf.MobSF.utils import ( + USERNAME_REGEX, + get_md5, +) +from mobsf.DynamicAnalyzer.views.common.shared import ( + send_response, +) + +logger = logging.getLogger(__name__) +register.filter('md5', get_md5) + + +PERM_CAN_SCAN = 'can_scan' +PERM_CAN_SUPPRESS = 'can_suppress' +PERM_CAN_DELETE = 'can_delete' + + +class Permissions(Enum): + SCAN = f'StaticAnalyzer.{PERM_CAN_SCAN}' + SUPPRESS = f'StaticAnalyzer.{PERM_CAN_SUPPRESS}' + DELETE = f'StaticAnalyzer.{PERM_CAN_DELETE}' + + +MAINTAINER_GROUP = 'Maintainer' +VIEWER_GROUP = 'Viewer' + + +def permission_required(perm): + def decorator(view): + @wraps(view) + def wrapper(request, *args, **kwargs): + sig = signature(view) + arguments = sig.bind(request, *args, **kwargs) + api = arguments.arguments.get('api') + if settings.DISABLE_AUTHENTICATION == '1' or api: + # Disable authorization for all functions + return view(request, *args, **kwargs) + # Enforce authorization for all web function calls + return pr( + perm.value, + raise_exception=True)(view)(request, *args, **kwargs) + return wrapper + return decorator + + +def has_permission(request, permission, api): + """Check if user has permission.""" + try: + if request.user.is_staff: + return True + if settings.DISABLE_AUTHENTICATION == '1' or api: + return True + if request.user.has_perm(permission.value): + return True + except Exception: + logger.exception('[ERROR] Failed to check permissions') + return False + + +def create_authorization_roles(): + """Create Authorization Roles.""" + try: + maintainer, _created = Group.objects.get_or_create( + name=MAINTAINER_GROUP) + Group.objects.get_or_create(name=VIEWER_GROUP) + + scan_permissions = Permission.objects.filter( + codename=PERM_CAN_SCAN) + suppress_permissions = Permission.objects.filter( + codename=PERM_CAN_SUPPRESS) + delete_permissions = Permission.objects.filter( + codename=PERM_CAN_DELETE) + all_perms = list(chain( + scan_permissions, suppress_permissions, delete_permissions)) + maintainer.permissions.set(all_perms) + except Exception: + logger.exception('[ERROR] Failed to create roles and permissions') + + +@login_required +@staff_member_required +def users(request): + """Show all users.""" + if settings.DISABLE_AUTHENTICATION == '1': + return redirect('/') + users = get_user_model().objects.all() + context = { + 'title': 'All Users', + 'users': users, + 'version': settings.MOBSF_VER, + } + return render(request, 'auth/users.html', context) + + +@login_required +@staff_member_required +def create_user(request): + if settings.DISABLE_AUTHENTICATION == '1': + return redirect('/') + if request.method == 'POST': + form = RegisterForm(request.POST) + if form.is_valid(): + role = request.POST.get('role') + username = request.POST.get('username') + if not username: + messages.error(request, 'No Username Provided') + return redirect('create_user') + if not USERNAME_REGEX.match(username): + messages.error(request, 'Invalid Username') + return redirect('create_user') + user = form.save() + user.is_staff = False + if role == 'maintainer': + user.groups.add(Group.objects.get(name=MAINTAINER_GROUP)) + else: + user.groups.add(Group.objects.get(name=VIEWER_GROUP)) + messages.success( + request, + 'User created successfully!') + return redirect('create_user') + else: + messages.error( + request, + 'Please correct the error below.') + else: + form = RegisterForm() + context = { + 'title': 'Create User', + 'version': settings.VERSION, + 'form': form, + } + return render(request, 'auth/register.html', context) + + +@login_required +@staff_member_required +@require_http_methods(['POST']) +def delete_user(request): + data = {'deleted': 'Failed to delete user'} + try: + if settings.DISABLE_AUTHENTICATION == '1': + return redirect('/') + username = request.POST.get('username') + if not username: + data = {'deleted': 'No Username Provided'} + return send_response(data) + if not USERNAME_REGEX.match(username): + data = {'deleted': 'Invalid Username'} + return send_response(data) + u = User.objects.get(username=username) + if u.is_staff: + data = {'deleted': 'Cannot delete staff users'} + return send_response(data) + u.groups.clear() + u.delete() + data = {'deleted': 'yes'} + except User.DoesNotExist: + data = {'deleted': 'User does not exist'} + except Exception as e: + data = {'deleted': e.message} + return send_response(data) diff --git a/mobsf/MobSF/views/helpers.py b/mobsf/MobSF/views/helpers.py index 102c2904eb..0bd0f93f42 100644 --- a/mobsf/MobSF/views/helpers.py +++ b/mobsf/MobSF/views/helpers.py @@ -44,6 +44,7 @@ def is_allow_file(self): or self.is_ipa() or self.is_appx() or self.is_apks() + or self.is_aab() or self.is_jar() or self.is_aar()): return True @@ -57,6 +58,10 @@ def is_xapk(self): return (self.file_type in settings.APK_MIME and self.file_name_lower.endswith('.xapk')) + def is_aab(self): + return (self.file_type in settings.APK_MIME + and self.file_name_lower.endswith('.aab')) + def is_apk(self): return (self.file_type in settings.APK_MIME and self.file_name_lower.endswith('.apk')) diff --git a/mobsf/MobSF/views/home.py b/mobsf/MobSF/views/home.py index b66e7583c9..68614d15fe 100755 --- a/mobsf/MobSF/views/home.py +++ b/mobsf/MobSF/views/home.py @@ -7,11 +7,14 @@ import re import shutil from pathlib import Path +from datetime import timedelta from wsgiref.util import FileWrapper from django.conf import settings +from django.utils.timezone import now from django.core.paginator import Paginator from django.http import HttpResponse, HttpResponseRedirect +from django.views.decorators.http import require_http_methods from django.utils import timezone from django.shortcuts import ( redirect, @@ -21,7 +24,7 @@ from mobsf.MobSF.forms import FormUtil, UploadFileForm from mobsf.MobSF.utils import ( - api_key, + MD5_REGEX, get_md5, is_dir_exists, is_file_exists, @@ -29,32 +32,55 @@ is_safe_path, key, print_n_send_error_response, + python_dict, ) +from mobsf.MobSF.init import api_key +from mobsf.MobSF.security import sanitize_filename from mobsf.MobSF.views.helpers import FileType from mobsf.MobSF.views.scanning import Scanning from mobsf.MobSF.views.apk_downloader import apk_download from mobsf.StaticAnalyzer.models import ( + EnqueuedTask, RecentScansDB, StaticAnalyzerAndroid, StaticAnalyzerIOS, StaticAnalyzerWindows, ) +from mobsf.DynamicAnalyzer.views.common.shared import ( + invalid_params, + send_response, +) +from mobsf.MobSF.views.authentication import ( + login_required, +) +from mobsf.MobSF.views.authorization import ( + MAINTAINER_GROUP, + Permissions, + permission_required, +) LINUX_PLATFORM = ['Darwin', 'Linux'] HTTP_BAD_REQUEST = 400 +HTTP_STATUS_404 = 404 +HTTP_SERVER_ERROR = 500 logger = logging.getLogger(__name__) register.filter('key', key) +@login_required def index(request): """Index Route.""" mimes = (settings.APK_MIME + settings.IPA_MIME + settings.ZIP_MIME + settings.APPX_MIME) + exts = (settings.ANDROID_EXTS + + settings.IOS_EXTS + + settings.WINDOWS_EXTS) context = { 'version': settings.MOBSF_VER, 'mimes': mimes, + 'exts': '|'.join(exts), } template = 'general/home.html' return render(request, template, context) @@ -70,6 +96,8 @@ def __init__(self, request): self.file = None @staticmethod + @login_required + @permission_required(Permissions.SCAN) def as_view(request): upload = Upload(request) return upload.upload_html() @@ -135,7 +163,7 @@ def upload(self): request = self.request scanning = Scanning(request) content_type = self.file.content_type - file_name = self.file.name + file_name = sanitize_filename(self.file.name) logger.info('MIME Type: %s FILE: %s', content_type, file_name) if self.file_type.is_apk(): return scanning.scan_apk() @@ -143,6 +171,8 @@ def upload(self): return scanning.scan_xapk() elif self.file_type.is_apks(): return scanning.scan_apks() + elif self.file_type.is_aab(): + return scanning.scan_aab() elif self.file_type.is_jar(): return scanning.scan_jar() elif self.file_type.is_aar(): @@ -161,11 +191,20 @@ def upload(self): return scanning.scan_appx() +@login_required def api_docs(request): """Api Docs Route.""" + key = '*******' + try: + if (settings.DISABLE_AUTHENTICATION == '1' + or request.user.is_staff + or request.user.groups.filter(name=MAINTAINER_GROUP).exists()): + key = api_key(settings.MOBSF_HOME) + except Exception: + logger.exception('[ERROR] Failed to get API key') context = { 'title': 'API Docs', - 'api_key': api_key(), + 'api_key': key, 'version': settings.MOBSF_VER, } template = 'general/apidocs.html' @@ -212,6 +251,12 @@ def zip_format(request): return render(request, template, context) +def robots_txt(request): + content = 'User-agent: *\nDisallow: /*/\nAllow: /*\n' + return HttpResponse(content, content_type='text/plain') + + +@login_required def dynamic_analysis(request): """Dynamic Analysis Landing.""" context = { @@ -222,22 +267,22 @@ def dynamic_analysis(request): return render(request, template, context) -def not_found(request): - """Not Found Route.""" - context = { - 'title': 'Not Found', - 'version': settings.MOBSF_VER, - } - template = 'general/not_found.html' - return render(request, template, context) - - -def recent_scans(request): +@login_required +def recent_scans(request, page_size=10, page_number=1): """Show Recent Scans Route.""" entries = [] - db_obj = RecentScansDB.objects.all().order_by('-TIMESTAMP').values() - android = StaticAnalyzerAndroid.objects.all() - ios = StaticAnalyzerIOS.objects.all() + paginator = Paginator( + RecentScansDB.objects.all().order_by('-TIMESTAMP').values(), page_size) + page_obj = paginator.get_page(page_number) + page_obj.page_size = page_size + md5_list = [i['MD5'] for i in page_obj] + + android = StaticAnalyzerAndroid.objects.filter( + MD5__in=md5_list).only( + 'PACKAGE_NAME', 'VERSION_NAME', 'FILE_NAME', 'MD5') + ios = StaticAnalyzerIOS.objects.filter( + MD5__in=md5_list).only('FILE_NAME', 'MD5') + updir = Path(settings.UPLD_DIR) icon_mapping = {} package_mapping = {} @@ -246,12 +291,14 @@ def recent_scans(request): icon_mapping[item.MD5] = item.ICON_PATH for item in ios: icon_mapping[item.MD5] = item.ICON_PATH - for entry in db_obj: + + for entry in page_obj: if entry['MD5'] in package_mapping.keys(): entry['PACKAGE'] = package_mapping[entry['MD5']] else: entry['PACKAGE'] = '' entry['ICON_PATH'] = icon_mapping.get(entry['MD5'], '') + if entry['FILE_NAME'].endswith('.ipa'): entry['BUNDLE_HASH'] = get_md5( entry['PACKAGE_NAME'].encode('utf-8')) @@ -264,11 +311,15 @@ def recent_scans(request): 'title': 'Recent Scans', 'entries': entries, 'version': settings.MOBSF_VER, + 'page_obj': page_obj, + 'async_scans': settings.ASYNC_ANALYSIS, } template = 'general/recent.html' return render(request, template, context) +@login_required +@permission_required(Permissions.SCAN) def download_apk(request): """Download and APK by package name.""" package = request.POST['package'] @@ -288,55 +339,154 @@ def download_apk(request): return resp -def search(request): - """Search Scan by MD5 Route.""" - md5 = request.GET['md5'] - if re.match('[0-9a-f]{32}', md5): - db_obj = RecentScansDB.objects.filter(MD5=md5) - if db_obj.exists(): - e = db_obj[0] - url = f'/{e.ANALYZER }/{e.MD5}/' - return HttpResponseRedirect(url) - else: - return HttpResponseRedirect('/not_found/') - return print_n_send_error_response(request, 'Invalid Scan Hash') +@login_required +def search(request, api=False): + """Search scan by checksum or text.""" + if request.method == 'POST': + query = request.POST['query'] + else: + query = request.GET['query'] + if not query: + msg = 'No search query provided.' + return print_n_send_error_response(request, msg, api) -def download(request): - """Download from mobsf.MobSF Route.""" - if request.method == 'GET': - root = settings.DWD_DIR + checksum = query if re.match(MD5_REGEX, query) else find_checksum(query) + + if checksum and re.match(MD5_REGEX, checksum): + db_obj = RecentScansDB.objects.filter(MD5=checksum).first() + if db_obj: + url = f'/{db_obj.ANALYZER}/{db_obj.MD5}/' + if api: + return {'checksum': db_obj.MD5} + else: + return HttpResponseRedirect(url) + + msg = 'You can search by MD5, app name, package name, or file name.' + return print_n_send_error_response(request, msg, api, 'Scan not found') + + +def find_checksum(query): + """Get the first matching checksum from the database.""" + search_fields = ['FILE_NAME', 'PACKAGE_NAME', 'APP_NAME'] + + for field in search_fields: + result = RecentScansDB.objects.filter( + **{f'{field}__icontains': query}).first() + if result: + return result.MD5 + + return None + +# AJAX + + +@login_required +@require_http_methods(['POST']) +def scan_status(request, api=False): + """Get Current Status of a scan in progress.""" + try: + scan_hash = request.POST['hash'] + if not is_md5(scan_hash): + return invalid_params(api) + robj = RecentScansDB.objects.filter(MD5=scan_hash) + if not robj.exists(): + data = {'status': 'failed', 'error': 'scan hash not found'} + return send_response(data, api) + data = {'status': 'ok', 'logs': python_dict(robj[0].SCAN_LOGS)} + except Exception as exp: + logger.exception('Fetching Scan Status') + data = {'status': 'failed', 'message': str(exp)} + return send_response(data, api) + + +def file_download(dwd_file, filename, content_type): + """HTTP file download response.""" + with open(dwd_file, 'rb') as file: + wrapper = FileWrapper(file) + response = HttpResponse(wrapper, content_type=content_type) + response['Content-Length'] = dwd_file.stat().st_size + if filename: + val = f'attachment; filename="{filename}"' + response['Content-Disposition'] = val + return response + + +@login_required +@require_http_methods(['GET']) +def download_binary(request, checksum, api=False): + """Download binary from uploads directory.""" + try: allowed_exts = settings.ALLOWED_EXTENSIONS - filename = request.path.replace('/download/', '', 1) - dwd_file = os.path.join(root, filename) - # Security Checks - if '../' in filename or not is_safe_path(root, dwd_file): - msg = 'Path Traversal Attack Detected' - return print_n_send_error_response(request, msg) - ext = os.path.splitext(filename)[1] - if ext in allowed_exts: - if os.path.isfile(dwd_file): - wrapper = FileWrapper( - open(dwd_file, 'rb')) # lgtm [py/path-injection] - response = HttpResponse( - wrapper, content_type=allowed_exts[ext]) - response['Content-Length'] = os.path.getsize(dwd_file) - return response - if filename.endswith(('screen/screen.png', '-icon.png')): - return HttpResponse('') - return HttpResponse(status=404) + if not is_md5(checksum): + return HttpResponse( + 'Invalid MD5 Hash', + status=HTTP_STATUS_404) + robj = RecentScansDB.objects.filter(MD5=checksum).first() + if not robj: + return HttpResponse( + 'Scan hash not found', + status=HTTP_STATUS_404) + file_ext = f'.{robj.SCAN_TYPE}' + if file_ext not in allowed_exts.keys(): + return HttpResponse( + 'Invalid Scan Type', + status=HTTP_STATUS_404) + filename = f'{checksum}{file_ext}' + dwd_file = Path(settings.UPLD_DIR) / checksum / filename + if not dwd_file.exists(): + return HttpResponse( + 'File not found', + status=HTTP_STATUS_404) + return file_download( + dwd_file, + sanitize_filename(robj.FILE_NAME), + allowed_exts[file_ext]) + except Exception: + logger.exception('Download Binary Failed') + return HttpResponse( + 'Failed to download file due to an error', + status=HTTP_SERVER_ERROR) + + +@login_required +@require_http_methods(['GET']) +def download(request): + """Download from mobsf downloads directory.""" + root = settings.DWD_DIR + filename = request.path.replace('/download/', '', 1) + dwd_file = Path(root) / filename + + # Security Checks + if '../' in filename or not is_safe_path(root, dwd_file): + msg = 'Path Traversal Attack Detected' + return print_n_send_error_response(request, msg) + + # File and Extension Check + ext = dwd_file.suffix + allowed_exts = settings.ALLOWED_EXTENSIONS + if ext in allowed_exts and dwd_file.is_file(): + return file_download( + dwd_file, + None, + allowed_exts[ext]) + + # Special Case for Certain Image Files + if filename.endswith(('screen/screen.png', '-icon.png')): + return HttpResponse('') + return HttpResponse(status=HTTP_STATUS_404) + +@login_required def generate_download(request): - """Generate downloads for uploaded binaries/source.""" + """Generate downloads for smali/java zip.""" try: - binary = ('apk', 'ipa', 'jar', 'aar', 'so', 'dylib', 'a') - source = ('smali', 'java') logger.info('Generating Downloads') md5 = request.GET['hash'] file_type = request.GET['file_type'] if (not is_md5(md5) - or file_type not in binary + source): + or file_type not in ('smali', 'java')): msg = 'Invalid download type or hash' logger.exception(msg) return print_n_send_error_response(request, msg) @@ -357,12 +507,6 @@ def generate_download(request): shutil.make_archive( dwd_file.as_posix(), 'zip', directory.as_posix()) file_name = f'{md5}-smali.zip' - elif file_type in binary: - # Binaries - file_name = f'{md5}.{file_type}' - src = app_dir / file_name - dst = dwd_dir / file_name - shutil.copy2(src.as_posix(), dst.as_posix()) return redirect(f'/download/{file_name}') except Exception: msg = 'Generating Downloads' @@ -370,51 +514,61 @@ def generate_download(request): return print_n_send_error_response(request, msg) +@login_required +@permission_required(Permissions.DELETE) +@require_http_methods(['POST']) def delete_scan(request, api=False): """Delete Scan from DB and remove the scan related files.""" try: - if request.method == 'POST': - if api: - md5_hash = request.POST['hash'] - else: - md5_hash = request.POST['md5'] - data = {'deleted': 'scan hash not found'} - if re.match('[0-9a-f]{32}', md5_hash): - # Delete DB Entries - scan = RecentScansDB.objects.filter(MD5=md5_hash) - if scan.exists(): - RecentScansDB.objects.filter(MD5=md5_hash).delete() - StaticAnalyzerAndroid.objects.filter(MD5=md5_hash).delete() - StaticAnalyzerIOS.objects.filter(MD5=md5_hash).delete() - StaticAnalyzerWindows.objects.filter(MD5=md5_hash).delete() - # Delete Upload Dir Contents - app_upload_dir = os.path.join(settings.UPLD_DIR, md5_hash) - if is_dir_exists(app_upload_dir): - shutil.rmtree(app_upload_dir) - # Delete Download Dir Contents - dw_dir = settings.DWD_DIR - for item in os.listdir(dw_dir): - item_path = os.path.join(dw_dir, item) - valid_item = item.startswith(md5_hash + '-') - # Delete all related files - if is_file_exists(item_path) and valid_item: - os.remove(item_path) - # Delete related directories - if is_dir_exists(item_path) and valid_item: - shutil.rmtree(item_path) - data = {'deleted': 'yes'} - if api: - return data - else: - ctype = 'application/json; charset=utf-8' - return HttpResponse(json.dumps(data), content_type=ctype) + if api: + md5_hash = request.POST['hash'] + else: + md5_hash = request.POST['md5'] + + if not re.match(MD5_REGEX, md5_hash): + return send_response({'deleted': 'Invalid scan hash'}, api) + + # Delete DB Entries + scan = RecentScansDB.objects.filter(MD5=md5_hash) + if not scan.exists(): + return send_response({'deleted': 'Scan not found in Database'}, api) + if settings.ASYNC_ANALYSIS: + # Handle Async Tasks + et = EnqueuedTask.objects.filter(checksum=md5_hash).first() + if et: + max_time_passed = now() - et.created_at > timedelta( + minutes=settings.ASYNC_ANALYSIS_TIMEOUT) + if not (et.completed_at or max_time_passed): + # Queue is in progress, cannot delete the task + return send_response( + {'deleted': 'A scan can only be deleted after it is completed'}, + api) + # Delete all related DB entries + EnqueuedTask.objects.filter(checksum=md5_hash).all().delete() + RecentScansDB.objects.filter(MD5=md5_hash).delete() + StaticAnalyzerAndroid.objects.filter(MD5=md5_hash).delete() + StaticAnalyzerIOS.objects.filter(MD5=md5_hash).delete() + StaticAnalyzerWindows.objects.filter(MD5=md5_hash).delete() + # Delete Upload Dir Contents + app_upload_dir = os.path.join(settings.UPLD_DIR, md5_hash) + if is_dir_exists(app_upload_dir): + shutil.rmtree(app_upload_dir) + # Delete Download Dir Contents + dw_dir = settings.DWD_DIR + for item in os.listdir(dw_dir): + item_path = os.path.join(dw_dir, item) + valid_item = item.startswith(md5_hash + '-') + # Delete all related files + if is_file_exists(item_path) and valid_item: + os.remove(item_path) + # Delete related directories + if is_dir_exists(item_path) and valid_item: + shutil.rmtree(item_path, ignore_errors=True) + return send_response({'deleted': 'yes'}, api) except Exception as exp: msg = str(exp) exp_doc = exp.__doc__ - if api: - return print_n_send_error_response(request, msg, True, exp_doc) - else: - return print_n_send_error_response(request, msg, False, exp_doc) + return print_n_send_error_response(request, msg, api, exp_doc) class RecentScans(object): diff --git a/mobsf/MobSF/views/saml2.py b/mobsf/MobSF/views/saml2.py new file mode 100644 index 0000000000..7ede726de6 --- /dev/null +++ b/mobsf/MobSF/views/saml2.py @@ -0,0 +1,194 @@ +"""SAML2 SSO logic.""" +from urllib.parse import urlparse +import logging + +from onelogin.saml2.auth import ( + OneLogin_Saml2_Auth, +) +from onelogin.saml2.idp_metadata_parser import ( + OneLogin_Saml2_IdPMetadataParser, +) + +from django.conf import settings +from django.contrib.auth.models import ( + Group, + User, +) +from django.contrib.auth import login +from django.urls import reverse +from django.shortcuts import redirect +from django.views.decorators.http import require_http_methods + +from mobsf.MobSF.views.authorization import ( + MAINTAINER_GROUP, + VIEWER_GROUP, +) +from mobsf.MobSF.utils import ( + print_n_send_error_response, +) +from mobsf.MobSF.security import ( + sanitize_redirect, +) + +logger = logging.getLogger(__name__) +ASSERTION_IDS = set() + + +def get_url_components(url): + """Get URL components.""" + purl = urlparse(url) + return purl.scheme, purl.netloc, purl.port + + +def init_saml_auth(req): + """Initialize SAML auth.""" + host = req['sp_url'] + acs_route = reverse('saml_acs') + saml_settings = { + 'strict': True, + 'debug': True, + 'sp': { + 'entityId': f'{host}{acs_route}', + 'assertionConsumerService': { + 'url': f'{host}{acs_route}', + 'binding': ('urn:oasis:names:tc:' + 'SAML:2.0:bindings:HTTP-POST'), + }, + }, + 'idp': { + 'entityId': settings.IDP_ENTITY_ID, + 'singleSignOnService': { + 'url': settings.IDP_SSO_URL, + 'binding': ('urn:oasis:names:tc:' + 'SAML:2.0:bindings:HTTP-Redirect'), + }, + 'x509cert': settings.IDP_X509CERT, + }, + } + try: + idp_data = None + if settings.IDP_METADATA_URL: + idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote( + settings.IDP_METADATA_URL, + timeout=5) + if idp_data: + saml_settings['idp'] = idp_data['idp'] + except Exception: + logger.exception('[ERROR] parsing IdP metadata URL.') + return OneLogin_Saml2_Auth(req, saml_settings) + + +def prepare_django_request(request): + """Prepare Django request for SAML.""" + scheme = 'https' if request.is_secure() else 'http' + netloc = request.get_host() + port = request.get_port() + if settings.SP_HOST: + scheme, netloc, port = get_url_components( + settings.SP_HOST.strip('/')) + if not port: + port = 443 if scheme == 'https' else 80 + https_state = 'on' if scheme == 'https' else 'off' + sp_url = f'{scheme}://{netloc}' + result = { + 'https': https_state, + 'http_host': netloc, + 'server_port': port, + 'script_name': request.get_full_path_info(), + 'get_data': request.GET.copy(), + 'post_data': request.POST.copy(), + 'lowercase_urlencoding': bool(settings.IDP_IS_ADFS == '1'), + 'query_string': request.META['QUERY_STRING'], + 'sp_url': sp_url, + } + return result + + +def check_replay(auth): + """Check for replay attack.""" + request_id = auth.get_last_assertion_id() + if request_id: + if request_id in ASSERTION_IDS: + raise Exception('Replay attack detected.') + ASSERTION_IDS.add(request_id) + + +def get_redirect_url(req): + """Check for open redirect and return redirect url.""" + redirect_url = '/' + if 'RelayState' not in req['post_data']: + return redirect_url + relay_state = req['post_data']['RelayState'] + # Allow only relative URLs + if relay_state: + redirect_url = sanitize_redirect(relay_state) + return redirect_url + + +def get_user_role(roles): + """Get user role.""" + mrole = any(MAINTAINER_GROUP.lower() in gp.lower() for gp in roles) + if mrole: + return MAINTAINER_GROUP + return VIEWER_GROUP + + +@require_http_methods(['GET']) +def saml_login(request): + """Handle SSO Login.""" + try: + if settings.DISABLE_AUTHENTICATION == '1': + return redirect('/') + req = prepare_django_request(request) + auth = init_saml_auth(req) + nextp = request.GET.get('next', '') + redirect_url = sanitize_redirect(nextp) + return redirect(auth.login(return_to=redirect_url)) + except Exception as exp: + return print_n_send_error_response( + request, + exp, + False) + + +@require_http_methods(['POST']) +def saml_acs(request): + """Handle SSO Assertion Consumer Service.""" + try: + if settings.DISABLE_AUTHENTICATION == '1': + return redirect('/') + req = prepare_django_request(request) + auth = init_saml_auth(req) + auth.process_response() + check_replay(auth) + if not auth.is_authenticated(): + raise Exception( + 'SAML authentication failed.') + # Extract user attributes for AuthZ and AuthN + attributes = auth.get_attributes() + if not attributes.get('email'): + raise Exception( + 'email attribute not found in SAML response.') + if not attributes.get('role'): + raise Exception( + 'role attribute not found in SAML response.') + email = attributes['email'][0] + role = get_user_role(attributes['role']) + if User.objects.filter(username=email).exists(): + user = User.objects.get(username=email) + user.groups.clear() + user.groups.add(Group.objects.get(name=role)) + login(request, user) + else: + user = User.objects.create_user( + username=email, + email=email) + user.is_staff = False + user.groups.add(Group.objects.get(name=role)) + login(request, user) + return redirect(get_redirect_url(req)) + except Exception as exp: + return print_n_send_error_response( + request, + exp, + False) diff --git a/mobsf/MobSF/views/scanning.py b/mobsf/MobSF/views/scanning.py index 9ef47ec512..ccf1133525 100644 --- a/mobsf/MobSF/views/scanning.py +++ b/mobsf/MobSF/views/scanning.py @@ -8,6 +8,7 @@ from django.utils import timezone from mobsf.StaticAnalyzer.models import RecentScansDB +from mobsf.MobSF.security import sanitize_filename logger = logging.getLogger(__name__) @@ -62,7 +63,8 @@ class Scanning(object): def __init__(self, request): self.file = request.FILES['file'] - self.file_name = request.FILES['file'].name + self.file_name = sanitize_filename( + request.FILES['file'].name) self.data = { 'analyzer': 'static_analyzer', 'status': 'success', @@ -77,7 +79,7 @@ def scan_apk(self): self.data['hash'] = md5 self.data['scan_type'] = 'apk' add_to_recent_scan(self.data) - logger.info('Performing Static Analysis of Android APK') + logger.info('Android APK uploaded') return self.data def scan_xapk(self): @@ -86,7 +88,7 @@ def scan_xapk(self): self.data['hash'] = md5 self.data['scan_type'] = 'xapk' add_to_recent_scan(self.data) - logger.info('Performing Static Analysis of Android XAPK base APK') + logger.info('Android XAPK uploaded') return self.data def scan_apks(self): @@ -95,7 +97,16 @@ def scan_apks(self): self.data['hash'] = md5 self.data['scan_type'] = 'apks' add_to_recent_scan(self.data) - logger.info('Performing Static Analysis of Android Split APK') + logger.info('Android Split APK uploaded') + return self.data + + def scan_aab(self): + """Android App Bundle.""" + md5 = handle_uploaded_file(self.file, '.aab') + self.data['hash'] = md5 + self.data['scan_type'] = 'aab' + add_to_recent_scan(self.data) + logger.info('Android App Bundle uploaded') return self.data def scan_jar(self): @@ -104,7 +115,7 @@ def scan_jar(self): self.data['hash'] = md5 self.data['scan_type'] = 'jar' add_to_recent_scan(self.data) - logger.info('Performing Static Analysis of Java JAR') + logger.info('Java JAR uploaded') return self.data def scan_aar(self): @@ -113,7 +124,7 @@ def scan_aar(self): self.data['hash'] = md5 self.data['scan_type'] = 'aar' add_to_recent_scan(self.data) - logger.info('Performing Static Analysis of Android AAR') + logger.info('Android AAR uploaded') return self.data def scan_so(self): @@ -122,7 +133,7 @@ def scan_so(self): self.data['hash'] = md5 self.data['scan_type'] = 'so' add_to_recent_scan(self.data) - logger.info('Performing Static Analysis of Shared Object') + logger.info('Shared Object Library uploaded') return self.data def scan_zip(self): @@ -131,7 +142,7 @@ def scan_zip(self): self.data['hash'] = md5 self.data['scan_type'] = 'zip' add_to_recent_scan(self.data) - logger.info('Performing Static Analysis of Android/iOS Source Code') + logger.info('Android/iOS Source code ZIP uploaded') return self.data def scan_ipa(self): @@ -141,7 +152,7 @@ def scan_ipa(self): self.data['scan_type'] = 'ipa' self.data['analyzer'] = 'static_analyzer_ios' add_to_recent_scan(self.data) - logger.info('Performing Static Analysis of iOS IPA') + logger.info('iOS IPA uploaded') return self.data def scan_dylib(self): @@ -151,7 +162,7 @@ def scan_dylib(self): self.data['scan_type'] = 'dylib' self.data['analyzer'] = 'static_analyzer_ios' add_to_recent_scan(self.data) - logger.info('Performing Static Analysis of iOS IPA') + logger.info('iOS dylib uploaded') return self.data def scan_a(self): @@ -161,7 +172,7 @@ def scan_a(self): self.data['scan_type'] = 'a' self.data['analyzer'] = 'static_analyzer_ios' add_to_recent_scan(self.data) - logger.info('Performing Static Analysis of Static Library') + logger.info('Static Library uploaded') return self.data def scan_appx(self): @@ -171,5 +182,5 @@ def scan_appx(self): self.data['scan_type'] = 'appx' self.data['analyzer'] = 'static_analyzer_windows' add_to_recent_scan(self.data) - logger.info('Performing Static Analysis of Windows APP') + logger.info('Windows APPX uploaded') return self.data diff --git a/mobsf/StaticAnalyzer/models.py b/mobsf/StaticAnalyzer/models.py index 76c5677bd5..349653b9dc 100755 --- a/mobsf/StaticAnalyzer/models.py +++ b/mobsf/StaticAnalyzer/models.py @@ -1,10 +1,25 @@ from datetime import datetime +from enum import Enum from django.db import models -# Create your models here. +from django.utils import timezone + + +class DjangoPermissions(Enum): + SCAN = ('can_scan', 'Scan Files') + SUPPRESS = ('can_suppress', 'Suppress Findings') + DELETE = ('can_delete', 'Delete Scans') + + +P = DjangoPermissions class RecentScansDB(models.Model): + class Meta: + """Meta class for RecentScansDB model.""" + + permissions = (P.DELETE.value, P.SCAN.value) + ANALYZER = models.CharField(max_length=50, default='') SCAN_TYPE = models.CharField(max_length=10, default='') FILE_NAME = models.CharField(max_length=260, default='') @@ -13,9 +28,15 @@ class RecentScansDB(models.Model): VERSION_NAME = models.CharField(max_length=50, default='') MD5 = models.CharField(max_length=32, default='', primary_key=True) TIMESTAMP = models.DateTimeField(default=datetime.now) + SCAN_LOGS = models.TextField(default=[]) class StaticAnalyzerAndroid(models.Model): + class Meta: + """Meta class for StaticAnalyzerAndroid model.""" + + permissions = (P.DELETE.value, P.SCAN.value) + FILE_NAME = models.CharField(max_length=260, default='') APP_NAME = models.CharField(max_length=255, default='') APP_TYPE = models.CharField(max_length=20, default='') @@ -61,9 +82,15 @@ class StaticAnalyzerAndroid(models.Model): PLAYSTORE_DETAILS = models.TextField(default={}) NETWORK_SECURITY = models.TextField(default=[]) SECRETS = models.TextField(default=[]) + SBOM = models.TextField(default={}) class StaticAnalyzerIOS(models.Model): + class Meta: + """Meta class for StaticAnalyzerIOS model.""" + + permissions = (P.DELETE.value, P.SCAN.value) + FILE_NAME = models.CharField(max_length=255, default='') APP_NAME = models.CharField(max_length=255, default='') APP_TYPE = models.CharField(max_length=20, default='') @@ -104,6 +131,11 @@ class StaticAnalyzerIOS(models.Model): class StaticAnalyzerWindows(models.Model): + class Meta: + """Meta class for StaticAnalyzerWindows model.""" + + permissions = (P.DELETE.value, P.SCAN.value) + FILE_NAME = models.CharField(max_length=260, default='') APP_NAME = models.CharField(max_length=260, default='') PUBLISHER_NAME = models.TextField(default='') @@ -128,7 +160,26 @@ class StaticAnalyzerWindows(models.Model): class SuppressFindings(models.Model): + class Meta: + """Meta class for SuppressFindings model.""" + + permissions = (P.DELETE.value, P.SCAN.value, P.SUPPRESS.value) + PACKAGE_NAME = models.CharField(max_length=260, default='') SUPPRESS_RULE_ID = models.TextField(default=[]) SUPPRESS_FILES = models.TextField(default={}) SUPPRESS_TYPE = models.TextField(default='') + + +class EnqueuedTask(models.Model): + task_id = models.CharField(max_length=255) + checksum = models.CharField(max_length=255) + file_name = models.CharField(max_length=255) + created_at = models.DateTimeField(default=timezone.now) + status = models.CharField(max_length=255, default='Enqueued') + started_at = models.DateTimeField(null=True) + completed_at = models.DateTimeField(null=True) + app_name = models.CharField(max_length=255, default='') + + def __str__(self): + return f'{self.name} ({self.status})' diff --git a/mobsf/StaticAnalyzer/tests.py b/mobsf/StaticAnalyzer/tests.py index 19e9ab7395..510fce87f9 100755 --- a/mobsf/StaticAnalyzer/tests.py +++ b/mobsf/StaticAnalyzer/tests.py @@ -4,27 +4,17 @@ import os import platform +from mobsf.MobSF.init import api_key + from django.conf import settings from django.http import HttpResponse from django.test import Client, TestCase -from mobsf.MobSF.utils import api_key - logger = logging.getLogger(__name__) RESCAN = False # Set RESCAN to True if Static Analyzer Code is modified -EXTS = ( - '.xapk', - '.apk', - '.ipa', - '.appx', - '.zip', - '.a', - '.so', - '.dylib', - '.aar', - '.jar') +EXTS = settings.ANDROID_EXTS + settings.IOS_EXTS + settings.WINDOWS_EXTS def static_analysis_test(): @@ -127,7 +117,7 @@ def static_analysis_test(): logger.info(resp.content) return True - # Search by MD5 + # Search by MD5 and text if platform.system() in ['Darwin', 'Linux']: scan_md5s = ['02e7989c457ab67eb514a8328779f256', '82ab8b2193b3cfb1c737e3a786be363a', @@ -143,18 +133,23 @@ def static_analysis_test(): '57bb5be0ea44a755ada4a93885c3825e', '8179b557433835827a70510584f3143e', '7b0a23bffc80bac05739ea1af898daad'] + # Search by text + queries = [ + 'diva', + 'webview', + ] logger.info('Running Search test') - for scan_md5 in scan_md5s: - url = '/search?md5={}'.format(scan_md5) + for q in scan_md5s + queries: + url = f'/search?query={q}' resp = http_client.get(url, follow=True) assert (resp.status_code == 200) if resp.status_code == 200: - logger.info('[OK] Search by MD5 test passed for %s', scan_md5) + logger.info('[OK] Search by query test passed for %s', q) else: - logger.error('Search by MD5 test failed for %s', scan_md5) + logger.error('Search by query test failed for %s', q) logger.info(resp.content) return True - logger.info('[OK] Search by MD5 tests completed') + logger.info('[OK] Search by MD5 and text tests completed') # Deleting Scan Results logger.info('Running Delete Scan Results test') @@ -180,7 +175,7 @@ def static_analysis_test(): def api_test(): """View for Handling REST API Test.""" logger.info('\nRunning REST API Unit test') - auth = api_key() + auth = api_key(settings.MOBSF_HOME) try: uploaded = [] logger.info('Running Test on Upload API') @@ -251,6 +246,33 @@ def api_test(): logger.error('Scan List API Test with custom http header 2') return True logger.info('[OK] Scan List API tests completed') + # Scan logs tests + logger.info('Running Scan Logs API tests') + for upl in uploaded: + resp = http_client.post( + '/api/v1/scan_logs', + {'hash': upl['hash']}, + HTTP_AUTHORIZATION=auth) + if resp.status_code == 200: + logs = json.loads(resp.content.decode('utf-8')) + if 'logs' in logs and len(logs['logs']) > 0: + logger.info('[OK] Scan Logs API test: %s', upl['hash']) + else: + logger.error('Scan Logs API test: %s', upl['hash']) + return True + logger.info('[OK] Static Analysis API test completed') + # Search API Tests + logger.info('Running Search API tests') + for term in ['diva', 'webview', '52c50ae824e329ba8b5b7a0f523efffe']: + resp = http_client.post( + '/api/v1/search', + {'query': term}, + HTTP_AUTHORIZATION=auth) + if resp.status_code == 200: + logger.info('[OK] Search API test: %s', term) + else: + logger.error('Search API test: %s', term) + return True # PDF Tests logger.info('Running PDF Generation API Test') if platform.system() in ['Darwin', 'Linux']: diff --git a/mobsf/StaticAnalyzer/tools/androguard4/apk.py b/mobsf/StaticAnalyzer/tools/androguard4/apk.py index dbb7f203d1..6aac025f71 100644 --- a/mobsf/StaticAnalyzer/tools/androguard4/apk.py +++ b/mobsf/StaticAnalyzer/tools/androguard4/apk.py @@ -1,89 +1,95 @@ # -*- coding: utf_8 -*- # flake8: noqa -# Androguard - -from .axml import ARSCParser, AXMLPrinter, ARSCResTableConfig -from .zipfile import ZipEntry +# Androguard4 APK - Nov 24, 2024 - 04a5703b8ba7c181bb9f5f5995a2c16b6f9353cf +# Allows type hinting of types not-yet-declared +# in Python >= 3.7 +# see https://peps.python.org/pep-0563/ +from __future__ import annotations # Python core +import binascii +import hashlib import io -from zlib import crc32 import os import re -import binascii +import unicodedata import zipfile +from hashlib import md5, sha1, sha224, sha256, sha384, sha512 from struct import unpack -import hashlib -import asn1crypto -import logging +from typing import Any, Iterator, List, Tuple, Union +from xml.dom.pulldom import SAX2DOM +from zlib import crc32 -# External dependecies import lxml.sax -from xml.dom.pulldom import SAX2DOM +from .apkinspector.headers import ZipEntry + # Used for reading Certificates -from asn1crypto import cms, x509, keys +from asn1crypto import cms, keys, x509 +from asn1crypto.util import OrderedDict +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import dsa, ec, padding, rsa -logger = logging.getLogger(__name__) -logger.setLevel(level=logging.INFO) - -NS_ANDROID_URI = 'http://schemas.android.com/apk/res/android' -NS_ANDROID = '{{{}}}'.format(NS_ANDROID_URI) # Namespace as used by etree +# External dependencies +from lxml.etree import Element -def get_certificate_name_string(name, short=False, delimiter=', '): - """ - Function from androguard. +# Androguard +from .axml import ( + END_DOCUMENT, + END_TAG, + START_TAG, + TEXT, + ARSCParser, + ARSCResTableConfig, + AXMLParser, + AXMLPrinter, + format_value, +) +from .util import get_certificate_name_string - licensed under the Apache License, Version 2.0. - https://github.com/androguard/androguard/blob/master/androguard/util.py - Format the Name type of a X509 Certificate in a human readable form. +import logging - :param name: Name object to return the DN from - :param short: Use short form (default: False) - :param delimiter: Delimiter string or character between - two parts (default: ', ') - :type name: dict or :class:`asn1crypto.x509.Name` - :type short: boolean - :type delimiter: str +logger = logging.getLogger(__name__) +logger.setLevel(level=logging.CRITICAL) +NS_ANDROID_URI = 'http://schemas.android.com/apk/res/android' +NS_ANDROID = '{{{}}}'.format(NS_ANDROID_URI) # Namespace as used by etree - :rtype: str - """ - if isinstance(name, asn1crypto.x509.Name): - name = name.native - - # For the shortform, we have a lookup table - # See RFC4514 for more details - _ = { - 'business_category': ('businessCategory', 'businessCategory'), - 'serial_number': ('serialNumber', 'serialNumber'), - 'country_name': ('C', 'countryName'), - 'postal_code': ('postalCode', 'postalCode'), - 'state_or_province_name': ('ST', 'stateOrProvinceName'), - 'locality_name': ('L', 'localityName'), - 'street_address': ('street', 'streetAddress'), - 'organization_name': ('O', 'organizationName'), - 'organizational_unit_name': ('OU', 'organizationalUnitName'), - 'title': ('title', 'title'), - 'common_name': ('CN', 'commonName'), - 'initials': ('initials', 'initials'), - 'generation_qualifier': ('generationQualifier', 'generationQualifier'), - 'surname': ('SN', 'surname'), - 'given_name': ('GN', 'givenName'), - 'name': ('name', 'name'), - 'pseudonym': ('pseudonym', 'pseudonym'), - 'dn_qualifier': ('dnQualifier', 'dnQualifier'), - 'telephone_number': ('telephoneNumber', 'telephoneNumber'), - 'email_address': ('E', 'emailAddress'), - 'domain_component': ('DC', 'domainComponent'), - 'name_distinguisher': ('nameDistinguisher', 'nameDistinguisher'), - 'organization_identifier': ( - 'organizationIdentifier', 'organizationIdentifier'), - } - return delimiter.join( - ['{}={}'.format( - _.get(attr, (attr, attr))[0 if short else 1], - name[attr]) for attr in name]) +# Dictionary of the different protection levels mapped to their corresponding attribute names as described in +# https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/content/pm/PermissionInfo.java +protection_flags_to_attributes = { + "0x00000000": "normal", + "0x00000001": "dangerous", + "0x00000002": "signature", + "0x00000003": "signature or system", + "0x00000004": "internal", + "0x00000010": "privileged", + "0x00000020": "development", + "0x00000040": "appop", + "0x00000080": "pre23", + "0x00000100": "installer", + "0x00000200": "verifier", + "0x00000400": "preinstalled", + "0x00000800": "setup", + "0x00001000": "instant", + "0x00002000": "runtime only", + "0x00004000": "oem", + "0x00008000": "vendor privileged", + "0x00010000": "system text classifier", + "0x00020000": "wellbeing", + "0x00040000": "documenter", + "0x00080000": "configurator", + "0x00100000": "incident report approver", + "0x00200000": "app predictor", + "0x00400000": "module", + "0x00800000": "companion", + "0x01000000": "retail demo", + "0x02000000": "recents", + "0x04000000": "role", + "0x08000000": "known signer", +} def parse_lxml_dom(tree): @@ -94,6 +100,7 @@ def parse_lxml_dom(tree): class Error(Exception): """Base class for exceptions in this module.""" + pass @@ -106,7 +113,7 @@ class BrokenAPKError(Error): def _dump_additional_attributes(additional_attributes): - """ try to parse additional attributes, but ends up to hexdump if the scheme is unknown """ + """try to parse additional attributes, but ends up to hexdump if the scheme is unknown""" attributes_raw = io.BytesIO(additional_attributes) attributes_hex = binascii.hexlify(additional_attributes) @@ -114,15 +121,15 @@ def _dump_additional_attributes(additional_attributes): if not len(additional_attributes): return attributes_hex - len_attribute, = unpack(' None: self._bytes = None self.digests = None - self.certificates = None + self.certificates = None self.additional_attributes = None def __str__(self): certs_infos = "" - for i,cert in enumerate(self.certificates): + for i, cert in enumerate(self.certificates): x509_cert = x509.Certificate.load(cert) certs_infos += "\n" certs_infos += " [%d]\n" % i - certs_infos += " - Issuer: %s\n" % get_certificate_name_string(x509_cert.issuer, short=True) - certs_infos += " - Subject: %s\n" % get_certificate_name_string(x509_cert.subject, short=True) - certs_infos += " - Serial Number: %s\n" % hex(x509_cert.serial_number) + certs_infos += " - Issuer: %s\n" % get_certificate_name_string( + x509_cert.issuer, short=True + ) + certs_infos += " - Subject: %s\n" % get_certificate_name_string( + x509_cert.subject, short=True + ) + certs_infos += " - Serial Number: %s\n" % hex( + x509_cert.serial_number + ) certs_infos += " - Hash Algorithm: %s\n" % x509_cert.hash_algo - certs_infos += " - Signature Algorithm: %s\n" % x509_cert.signature_algo - certs_infos += " - Valid not before: %s\n" % x509_cert['tbs_certificate']['validity']['not_before'].native - certs_infos += " - Valid not after: %s" % x509_cert['tbs_certificate']['validity']['not_after'].native + certs_infos += ( + " - Signature Algorithm: %s\n" % x509_cert.signature_algo + ) + certs_infos += ( + " - Valid not before: %s\n" + % x509_cert['tbs_certificate']['validity']['not_before'].native + ) + certs_infos += ( + " - Valid not after: %s" + % x509_cert['tbs_certificate']['validity']['not_after'].native + ) - return "\n".join([ - 'additional_attributes : {}'.format(_dump_additional_attributes(self.additional_attributes)), - 'digests : {}'.format(_dump_digests_or_signatures(self.digests)), - 'certificates : {}'.format(certs_infos), - ]) + return "\n".join( + [ + 'additional_attributes : {}'.format( + _dump_additional_attributes(self.additional_attributes) + ), + 'digests : {}'.format( + _dump_digests_or_signatures(self.digests) + ), + 'certificates : {}'.format(certs_infos), + ] + ) class APKV3SignedData(APKV2SignedData): @@ -182,7 +211,7 @@ class APKV3SignedData(APKV2SignedData): source : https://source.android.com/security/apksigning/v3.html """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.minSDK = None self.maxSDK = None @@ -193,14 +222,16 @@ def __str__(self): # maxSDK is set to a negative value if there is no upper bound on the sdk targeted max_sdk_str = "%d" % self.maxSDK - if self.maxSDK >= 0x7fffffff: + if self.maxSDK >= 0x7FFFFFFF: max_sdk_str = "0x%x" % self.maxSDK - return "\n".join([ - 'signer minSDK : {:d}'.format(self.minSDK), - 'signer maxSDK : {:s}'.format(max_sdk_str), - base_str - ]) + return "\n".join( + [ + 'signer minSDK : {:d}'.format(self.minSDK), + 'signer maxSDK : {:s}'.format(max_sdk_str), + base_str, + ] + ) class APKV2Signer: @@ -209,18 +240,22 @@ class APKV2Signer: source : https://source.android.com/security/apksigning/v2.html """ - def __init__(self): + def __init__(self) -> None: self._bytes = None self.signed_data = None self.signatures = None self.public_key = None def __str__(self): - return "\n".join([ - '{:s}'.format(str(self.signed_data)), - 'signatures : {}'.format(_dump_digests_or_signatures(self.signatures)), - 'public key : {}'.format(binascii.hexlify(self.public_key)), - ]) + return "\n".join( + [ + '{:s}'.format(str(self.signed_data)), + 'signatures : {}'.format( + _dump_digests_or_signatures(self.signatures) + ), + 'public key : {}'.format(binascii.hexlify(self.public_key)), + ] + ) class APKV3Signer(APKV2Signer): @@ -229,7 +264,7 @@ class APKV3Signer(APKV2Signer): source : https://source.android.com/security/apksigning/v3.html """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.minSDK = None self.maxSDK = None @@ -240,14 +275,16 @@ def __str__(self): # maxSDK is set to a negative value if there is no upper bound on the sdk targeted max_sdk_str = "%d" % self.maxSDK - if self.maxSDK >= 0x7fffffff: + if self.maxSDK >= 0x7FFFFFFF: max_sdk_str = "0x%x" % self.maxSDK - return "\n".join([ - 'signer minSDK : {:d}'.format(self.minSDK), - 'signer maxSDK : {:s}'.format(max_sdk_str), - base_str - ]) + return "\n".join( + [ + 'signer minSDK : {:d}'.format(self.minSDK), + 'signer maxSDK : {:s}'.format(max_sdk_str), + base_str, + ] + ) class APK: @@ -257,23 +294,32 @@ class APK: # Constants in the APK Signature Block _APK_SIG_MAGIC = b"APK Sig Block 42" - _APK_SIG_KEY_V2_SIGNATURE = 0x7109871a - _APK_SIG_KEY_V3_SIGNATURE = 0xf05368c0 - _APK_SIG_ATTR_V2_STRIPPING_PROTECTION = 0xbeeff00d + _APK_SIG_KEY_V2_SIGNATURE = 0x7109871A + _APK_SIG_KEY_V3_SIGNATURE = 0xF05368C0 + _APK_SIG_ATTR_V2_STRIPPING_PROTECTION = 0xBEEFF00D _APK_SIG_ALGO_IDS = { - 0x0101 : "RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc", - 0x0102 : "RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc", - 0x0103 : "RSASSA-PKCS1-v1_5 with SHA2-256 digest.", # This is for build systems which require deterministic signatures. - 0x0104 : "RSASSA-PKCS1-v1_5 with SHA2-512 digest.", # This is for build systems which require deterministic signatures. - 0x0201 : "ECDSA with SHA2-256 digest", - 0x0202 : "ECDSA with SHA2-512 digest", - 0x0301 : "DSA with SHA2-256 digest", + 0x0101: "RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc", + 0x0102: "RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc", + # This is for build systems which require deterministic signatures. + 0x0103: "RSASSA-PKCS1-v1_5 with SHA2-256 digest.", + # This is for build systems which require deterministic signatures. + 0x0104: "RSASSA-PKCS1-v1_5 with SHA2-512 digest.", + 0x0201: "ECDSA with SHA2-256 digest", + 0x0202: "ECDSA with SHA2-512 digest", + 0x0301: "DSA with SHA2-256 digest", } __no_magic = False - def __init__(self, filename, raw=False, magic_file=None, skip_analysis=False, testzip=False): + def __init__( + self, + filename: str, + raw: bool = False, + magic_file: Union[str, None] = None, + skip_analysis: bool = False, + testzip: bool = False, + ) -> None: """ This class can access to all elements in an APK file @@ -296,7 +342,9 @@ def __init__(self, filename, raw=False, magic_file=None, skip_analysis=False, te """ if magic_file: - logger.warning("You set magic_file but this parameter is actually unused. You should remove it.") + logger.warning( + "You set magic_file but this parameter is actually unused. You should remove it." + ) self.filename = filename @@ -331,7 +379,9 @@ def __init__(self, filename, raw=False, magic_file=None, skip_analysis=False, te self.__raw = self.zip.zip.getvalue() if testzip: - logger.info("Testing zip file integrity, this might take a while...") + logger.info( + "Testing zip file integrity, this might take a while..." + ) # Test the zipfile for integrity before continuing. # This process might be slow, as the whole file is read. # Therefore it is possible to enable it as a separate feature. @@ -345,7 +395,9 @@ def __init__(self, filename, raw=False, magic_file=None, skip_analysis=False, te # we could print the filename here, but there are zip which are so broken # That the filename is either very very long or does not make any sense. # Thus we do not do it, the user might find out by using other tools. - raise BrokenAPKError("The APK is probably broken: testzip returned an error.") + raise BrokenAPKError( + "The APK is probably broken: testzip returned an error." + ) if not skip_analysis: self._apk_analysis() @@ -375,44 +427,73 @@ def _apk_analysis(self): ap = AXMLPrinter(manifest_data) if not ap.is_valid(): - logger.error("Error while parsing AndroidManifest.xml - is the file valid?") + logger.error( + "Error while parsing AndroidManifest.xml - is the file valid?" + ) return self.axml[i] = ap self.xml[i] = self.axml[i].get_xml_obj() if self.axml[i].is_packed(): - logger.warning("XML Seems to be packed, operations on the AndroidManifest.xml might fail.") + logger.warning( + "XML Seems to be packed, operations on the AndroidManifest.xml might fail." + ) if self.xml[i] is not None: if self.xml[i].tag != "manifest": - logger.error("AndroidManifest.xml does not start with a tag! Is this a valid APK?") + logger.error( + "AndroidManifest.xml does not start with a tag! Is this a valid APK?" + ) return self.package = self.get_attribute_value("manifest", "package") - self.androidversion["Code"] = self.get_attribute_value("manifest", "versionCode") - self.androidversion["Name"] = self.get_attribute_value("manifest", "versionName") - permission = list(self.get_all_attribute_value("uses-permission", "name")) + self.androidversion["Code"] = self.get_attribute_value( + "manifest", "versionCode" + ) + self.androidversion["Name"] = self.get_attribute_value( + "manifest", "versionName" + ) + permission = list( + self.get_all_attribute_value("uses-permission", "name") + ) self.permissions = list(set(self.permissions + permission)) for uses_permission in self.find_tags("uses-permission"): - self.uses_permissions.append([ - self.get_value_from_tag(uses_permission, "name"), - self._get_permission_maxsdk(uses_permission) - ]) + self.uses_permissions.append( + [ + self.get_value_from_tag(uses_permission, "name"), + self._get_permission_maxsdk(uses_permission), + ] + ) # getting details of the declared permissions for d_perm_item in self.find_tags('permission'): d_perm_name = self._get_res_string_value( - str(self.get_value_from_tag(d_perm_item, "name"))) + str(self.get_value_from_tag(d_perm_item, "name")) + ) d_perm_label = self._get_res_string_value( - str(self.get_value_from_tag(d_perm_item, "label"))) + str(self.get_value_from_tag(d_perm_item, "label")) + ) d_perm_description = self._get_res_string_value( - str(self.get_value_from_tag(d_perm_item, "description"))) + str( + self.get_value_from_tag(d_perm_item, "description") + ) + ) d_perm_permissionGroup = self._get_res_string_value( - str(self.get_value_from_tag(d_perm_item, "permissionGroup"))) + str( + self.get_value_from_tag( + d_perm_item, "permissionGroup" + ) + ) + ) d_perm_protectionLevel = self._get_res_string_value( - str(self.get_value_from_tag(d_perm_item, "protectionLevel"))) + str( + self.get_value_from_tag( + d_perm_item, "protectionLevel" + ) + ) + ) d_perm_details = { "label": d_perm_label, @@ -423,12 +504,16 @@ def _apk_analysis(self): self.declared_permissions[d_perm_name] = d_perm_details self.valid_apk = True - logger.debug("APK file was successfully validated!") + logger.info("APK file was successfully validated!") # self.permission_module = androconf.load_api_specific_resource_module( - # "aosp_permissions", self.get_target_sdk_version()) - # self.permission_module_min_sdk = androconf.load_api_specific_resource_module( - # "aosp_permissions", self.get_min_sdk_version()) + # "aosp_permissions", self.get_target_sdk_version() + # ) + # self.permission_module_min_sdk = ( + # androconf.load_api_specific_resource_module( + # "aosp_permissions", self.get_min_sdk_version() + # ) + # ) def __getstate__(self): """ @@ -480,12 +565,15 @@ def _get_permission_maxsdk(self, item): try: maxSdkVersion = int(self.get_value_from_tag(item, "maxSdkVersion")) except ValueError: - logger.warning(str(maxSdkVersion) + ' is not a valid value for maxSdkVersion') + logger.warning( + str(maxSdkVersion) + + ' is not a valid value for maxSdkVersion' + ) except TypeError: pass return maxSdkVersion - def is_valid_APK(self): + def is_valid_APK(self) -> bool: """ Return true if the APK is valid, false otherwise. An APK is seen as valid, if the AndroidManifest.xml could be successful parsed. @@ -496,7 +584,7 @@ def is_valid_APK(self): """ return self.valid_apk - def get_filename(self): + def get_filename(self) -> str: """ Return the filename of the APK @@ -504,7 +592,7 @@ def get_filename(self): """ return self.filename - def get_app_name(self): + def get_app_name(self, locale=None) -> str: """ Return the appname of the APK @@ -526,12 +614,16 @@ def get_app_name(self): # FIXME: would need to use _format_value inside get_attribute_value for each returned name! # For example, as the activity name might be foobar.foo.bar but inside the activity it is only .bar - app_name = self.get_attribute_value('activity', 'label', name=main_activity_name) + app_name = self.get_attribute_value( + 'activity', 'label', name=main_activity_name + ) if app_name is None: # No App name set # TODO return packagename instead? - logger.warning("It looks like that no app name is set for the main activity!") + logger.warning( + "It looks like that no app name is set for the main activity!" + ) return "" if app_name.startswith("@"): @@ -548,23 +640,34 @@ def get_app_name(self): if package == 'android': # TODO: we can not resolve this, as we lack framework-res.apk # one exception would be when parsing framework-res.apk directly. - logger.warning("Resource ID with android package name encountered! " - "Will not resolve, framework-res.apk would be required.") + logger.warning( + "Resource ID with android package name encountered! " + "Will not resolve, framework-res.apk would be required." + ) return app_name else: # TODO should look this up, might be in the resources - logger.warning("Resource ID with Package name '{}' encountered! Will not resolve".format(package)) + logger.warning( + "Resource ID with Package name '{}' encountered! Will not resolve".format( + package + ) + ) return app_name try: - app_name = res_parser.get_resolved_res_configs( - res_id, - ARSCResTableConfig.default_config())[0][1] + config = ( + ARSCResTableConfig(None, locale=locale) + if locale + else ARSCResTableConfig.default_config() + ) + app_name = res_parser.get_resolved_res_configs(res_id, config)[ + 0 + ][1] except Exception as e: logger.warning("Exception selecting app name: %s" % e) return app_name - def get_app_icon(self, max_dpi=65536): + def get_app_icon(self, max_dpi: int = 65536) -> Union[str, None]: """ Return the first icon file name, which density is not greater than max_dpi, unless exact icon resolution is set in the manifest, in which case @@ -608,7 +711,8 @@ def get_app_icon(self, max_dpi=65536): main_activity_name = self.get_main_activity() app_icon = self.get_attribute_value( - 'activity', 'icon', name=main_activity_name) + 'activity', 'icon', name=main_activity_name + ) if not app_icon: app_icon = self.get_attribute_value('application', 'icon') @@ -619,12 +723,16 @@ def get_app_icon(self, max_dpi=65536): return None if not app_icon: - res_id = res_parser.get_res_id_by_key(self.package, 'mipmap', 'ic_launcher') + res_id = res_parser.get_res_id_by_key( + self.package, 'mipmap', 'ic_launcher' + ) if res_id: app_icon = "@%x" % res_id if not app_icon: - res_id = res_parser.get_res_id_by_key(self.package, 'drawable', 'ic_launcher') + res_id = res_parser.get_res_id_by_key( + self.package, 'drawable', 'ic_launcher' + ) if res_id: app_icon = "@%x" % res_id @@ -652,7 +760,7 @@ def get_app_icon(self, max_dpi=65536): return app_icon - def get_package(self): + def get_package(self) -> str: """ Return the name of the package @@ -662,7 +770,7 @@ def get_package(self): """ return self.package - def get_androidversion_code(self): + def get_androidversion_code(self) -> str: """ Return the android version code @@ -672,7 +780,7 @@ def get_androidversion_code(self): """ return self.androidversion["Code"] - def get_androidversion_name(self): + def get_androidversion_name(self) -> str: """ Return the android version name @@ -682,7 +790,7 @@ def get_androidversion_name(self): """ return self.androidversion["Name"] - def get_files(self): + def get_files(self) -> list[str]: """ Return the file names inside the APK. @@ -690,7 +798,7 @@ def get_files(self): """ return self.zip.namelist() - # def _get_file_magic_name(self, buffer): + # def _get_file_magic_name(self, buffer: bytes) -> str: # """ # Return the filetype guessed for a buffer # :param buffer: bytes @@ -711,52 +819,60 @@ def get_files(self): # return default # except TypeError as e: # self.__no_magic = True - # logger.warning("It looks like you have the magic python package installed but not the magic library itself!") + # logger.warning( + # "It looks like you have the magic python package installed but not the magic library itself!" + # ) # logger.warning("Error from magic library: %s", e) - # logger.warning("Please follow the installation instructions at https://github.com/ahupp/python-magic/#installation") - # logger.warning("You can also install the 'python-magic-bin' package on Windows and MacOS") + # logger.warning( + # "Please follow the installation instructions at https://github.com/ahupp/python-magic/#installation" + # ) + # logger.warning( + # "You can also install the 'python-magic-bin' package on Windows and MacOS" + # ) # return default - try: - # There are several implementations of magic, - # unfortunately all called magic - # We use this one: https://github.com/ahupp/python-magic/ - # You can also use python-magic-bin on Windows or MacOS - getattr(magic, "MagicException") - except AttributeError: - self.__no_magic = True - logger.warning("Not the correct Magic library was found on your " - "system. Please install python-magic or python-magic-bin!") - return default + # try: + # # There are several implementations of magic, + # # unfortunately all called magic + # # We use this one: https://github.com/ahupp/python-magic/ + # # You can also use python-magic-bin on Windows or MacOS + # getattr(magic, "MagicException") + # except AttributeError: + # self.__no_magic = True + # logger.warning( + # "Not the correct Magic library was found on your " + # "system. Please install python-magic or python-magic-bin!" + # ) + # return default - try: - # 1024 byte are usually enough to test the magic - ftype = magic.from_buffer(buffer[:1024]) - except magic.MagicException as e: - logger.exception("Error getting the magic type: %s", e) - return default - - if not ftype: - return default - else: - return self._patch_magic(buffer, ftype) + # try: + # # 1024 byte are usually enough to test the magic + # ftype = magic.from_buffer(buffer[:1024]) + # except magic.MagicException as e: + # logger.exception("Error getting the magic type: %s", e) + # return default - @property - def files(self): - """ - Returns a dictionary of filenames and detected magic type + # if not ftype: + # return default + # else: + # return self._patch_magic(buffer, ftype) - :returns: dictionary of files and their mime type - """ - return self.get_files_types() + # @property + # def files(self) -> dict[str, str]: + # """ + # Returns a dictionary of filenames and detected magic type - # def get_files_types(self): + # :returns: dictionary of files and their mime type + # """ + # return self.get_files_types() + + # def get_files_types(self) -> dict[str, str]: # """ # Return the files inside the APK with their associated types (by using python-magic) # At the same time, the CRC32 are calculated for the files. - # :rtype: a dictionnary + # :rtype: a dictionary # """ # if self._files == {}: # # Generate File Types / CRC List @@ -774,7 +890,11 @@ def files(self): # :param orig: guess by mime libary # :returns: corrected guess # """ - # if ("Zip" in orig) or ('(JAR)' in orig) and androconf.is_android_raw(buffer) == 'APK': + # if ( + # ("Zip" in orig) + # or ('(JAR)' in orig) + # and androconf.is_android_raw(buffer) == 'APK' + # ): # return "Android application package file" # return orig @@ -791,14 +911,23 @@ def _get_crc32(self, filename): buffer = self.zip.read(filename) if filename not in self.files_crc32: self.files_crc32[filename] = crc32(buffer) - if self.files_crc32[filename] != self.zip.infolist()[filename].crc32_of_uncompressed_data: - logger.error("File '{}' has different CRC32 after unpacking! " - "Declared: {:08x}, Calculated: {:08x}".format(filename, - self.zip.infolist()[filename].crc32_of_uncompressed_data, - self.files_crc32[filename])) + if ( + self.files_crc32[filename] + != self.zip.infolist()[filename].crc32_of_uncompressed_data + ): + logger.error( + "File '{}' has different CRC32 after unpacking! " + "Declared: {:08x}, Calculated: {:08x}".format( + filename, + self.zip.infolist()[ + filename + ].crc32_of_uncompressed_data, + self.files_crc32[filename], + ) + ) return buffer - def get_files_crc32(self): + def get_files_crc32(self) -> dict[str, int]: """ Calculates and returns a dictionary of filenames and CRC32 @@ -810,16 +939,16 @@ def get_files_crc32(self): return self.files_crc32 - # def get_files_information(self): - # """ - # Return the files inside the APK with their associated types and crc32 + def get_files_information(self) -> Iterator[tuple[str, str, int]]: + """ + Return the files inside the APK with their associated types and crc32 - # :rtype: str, str, int - # """ - # for k in self.get_files(): - # yield k, self.get_files_types()[k], self.get_files_crc32()[k] + :rtype: str, str, int + """ + for k in self.get_files(): + yield k, self.get_files_types()[k], self.get_files_crc32()[k] - def get_raw(self): + def get_raw(self) -> bytes: """ Return raw bytes of the APK @@ -833,7 +962,7 @@ def get_raw(self): self.__raw = bytearray(f.read()) return self.__raw - def get_file(self, filename): + def get_file(self, filename: str) -> bytes: """ Return the raw data of the specified filename inside the APK @@ -845,7 +974,7 @@ def get_file(self, filename): except KeyError: raise FileNotPresent(filename) - def get_dex(self): + def get_dex(self) -> bytes: """ Return the raw data of the classes dex file @@ -860,7 +989,7 @@ def get_dex(self): # TODO is this a good idea to return an empty string? return b"" - def get_dex_names(self): + def get_dex_names(self) -> list[str]: """ Return the names of all DEX files found in the APK. This method only accounts for "offical" dex files, i.e. all files @@ -871,7 +1000,7 @@ def get_dex_names(self): dexre = re.compile(r"^classes(\d*).dex$") return filter(lambda x: dexre.match(x), self.get_files()) - def get_all_dex(self): + def get_all_dex(self) -> Iterator[bytes]: """ Return the raw data of all classes dex files @@ -880,14 +1009,23 @@ def get_all_dex(self): for dex_name in self.get_dex_names(): yield self.get_file(dex_name) - def is_multidex(self): + def is_multidex(self) -> bool: """ Test if the APK has multiple DEX files :returns: True if multiple dex found, otherwise False """ dexre = re.compile(r"^classes(\d+)?.dex$") - return len([instance for instance in self.get_files() if dexre.search(instance)]) > 1 + return ( + len( + [ + instance + for instance in self.get_files() + if dexre.search(instance) + ] + ) + > 1 + ) def _format_value(self, value): """ @@ -911,8 +1049,12 @@ def _format_value(self, value): return value def get_all_attribute_value( - self, tag_name, attribute, format_value=True, **attribute_filter - ): + self, + tag_name: str, + attribute: str, + format_value: bool = True, + **attribute_filter, + ) -> Iterator[str]: """ Yields all the attribute values in xml files which match with the tag name and the specific attribute @@ -930,8 +1072,12 @@ def get_all_attribute_value( yield value def get_attribute_value( - self, tag_name, attribute, format_value=False, **attribute_filter - ): + self, + tag_name: str, + attribute: str, + format_value: bool = False, + **attribute_filter, + ) -> str: """ Return the attribute value in xml files which matches the tag name and the specific attribute @@ -941,11 +1087,14 @@ def get_attribute_value( """ for value in self.get_all_attribute_value( - tag_name, attribute, format_value, **attribute_filter): + tag_name, attribute, format_value, **attribute_filter + ): if value is not None: return value - def get_value_from_tag(self, tag, attribute): + def get_value_from_tag( + self, tag: Element, attribute: str + ) -> Union[str, None]: """ Return the value of the android prefixed attribute in a specific tag. @@ -985,27 +1134,29 @@ def get_value_from_tag(self, tag, attribute): if value: # If value is still None, the attribute could not be found, thus is not present - logger.warning("Failed to get the attribute '{}' on tag '{}' with namespace. " - "But found the same attribute without namespace!".format(attribute, tag.tag)) + logger.warning( + "Failed to get the attribute '{}' on tag '{}' with namespace. " + "But found the same attribute without namespace!".format( + attribute, tag.tag + ) + ) return value - def find_tags(self, tag_name, **attribute_filter): + def find_tags(self, tag_name: str, **attribute_filter) -> list[str]: """ Return a list of all the matched tags in all available xml :param str tag: specify the tag name """ all_tags = [ - self.find_tags_from_xml( - i, tag_name, **attribute_filter - ) + self.find_tags_from_xml(i, tag_name, **attribute_filter) for i in self.xml ] return [tag for tag_list in all_tags for tag in tag_list] def find_tags_from_xml( - self, xml_name, tag_name, **attribute_filter - ): + self, xml_name: str, tag_name: str, **attribute_filter + ) -> list[str]: """ Return a list of all the matched tags in a specific xml w @@ -1016,19 +1167,20 @@ def find_tags_from_xml( if xml is None: return [] if xml.tag == tag_name: - if self.is_tag_matched( - xml.tag, **attribute_filter - ): + if self.is_tag_matched(xml.tag, **attribute_filter): return [xml] return [] - tags = xml.findall(".//" + tag_name) + tags = set() + tags.update(xml.findall(".//" + tag_name)) + + # https://github.com/androguard/androguard/pull/1053 + # permission declared using tag bool: r""" Return true if the attributes matches in attribute filter. @@ -1055,7 +1207,7 @@ def is_tag_matched(self, tag, **attribute_filter): return False return True - def get_main_activities(self): + def get_main_activities(self) -> set[str]: """ Return names of the main activities @@ -1069,8 +1221,9 @@ def get_main_activities(self): for i in self.xml: if self.xml[i] is None: continue - activities_and_aliases = self.xml[i].findall(".//activity") + \ - self.xml[i].findall(".//activity-alias") + activities_and_aliases = self.xml[i].findall( + ".//activity" + ) + self.xml[i].findall(".//activity-alias") for item in activities_and_aliases: # Some applications have more than one MAIN activity. @@ -1099,7 +1252,7 @@ def get_main_activities(self): return x.intersection(y) - def get_main_activity(self): + def get_main_activity(self) -> Union[str, None]: """ Return the name of the main activity @@ -1115,13 +1268,14 @@ def get_main_activity(self): # sorted is necessary # 9fc7d3e8225f6b377f9181a92c551814317b77e1aa0df4c6d508d24b18f0f633 good_main_activities = sorted( - main_activities.intersection(self.get_activities())) + main_activities.intersection(self.get_activities()) + ) if good_main_activities: return good_main_activities[0] return sorted(main_activities)[0] return None - def get_activities(self): + def get_activities(self) -> list[str]: """ Return the android:name attribute of all activities @@ -1129,7 +1283,7 @@ def get_activities(self): """ return list(self.get_all_attribute_value("activity", "name")) - def get_activity_aliases(self): + def get_activity_aliases(self) -> list[dict[str, str]]: """ Return the android:name and android:targetActivity attribute of all activity aliases. @@ -1139,8 +1293,7 @@ def get_activity_aliases(self): for alias in self.find_tags('activity-alias'): activity_alias = {} for attribute in ['name', 'targetActivity']: - value = (alias.get(attribute) or - alias.get(self._ns(attribute))) + value = alias.get(attribute) or alias.get(self._ns(attribute)) if not value: continue activity_alias[attribute] = self._format_value(value) @@ -1148,7 +1301,7 @@ def get_activity_aliases(self): ali.append(activity_alias) return ali - def get_services(self): + def get_services(self) -> list[str]: """ Return the android:name attribute of all services @@ -1156,7 +1309,7 @@ def get_services(self): """ return list(self.get_all_attribute_value("service", "name")) - def get_receivers(self): + def get_receivers(self) -> list[str]: """ Return the android:name attribute of all receivers @@ -1164,7 +1317,7 @@ def get_receivers(self): """ return list(self.get_all_attribute_value("receiver", "name")) - def get_providers(self): + def get_providers(self) -> list[str]: """ Return the android:name attribute of all providers @@ -1172,7 +1325,7 @@ def get_providers(self): """ return list(self.get_all_attribute_value("provider", "name")) - def get_res_value(self, name): + def get_res_value(self, name: str) -> str: """ Return the literal value with a resource id :rtype: str @@ -1185,15 +1338,17 @@ def get_res_value(self, name): res_id = res_parser.parse_id(name)[0] try: value = res_parser.get_resolved_res_configs( - res_id, - ARSCResTableConfig.default_config())[0][1] + res_id, ARSCResTableConfig.default_config() + )[0][1] except Exception as e: logger.warning("Exception get resolved resource id: %s" % e) return name return value - def get_intent_filters(self, itemtype, name): + def get_intent_filters( + self, itemtype: str, name: str + ) -> dict[str, list[str]]: """ Find intent filters for a given item and name. @@ -1205,7 +1360,19 @@ def get_intent_filters(self, itemtype, name): :param name: the `android:name` of the parent item, e.g. activity name :returns: a dictionary with the keys `action` and `category` containing the `android:name` of those items """ - attributes = {"action": ["name"], "category": ["name"], "data": ['scheme', 'host', 'port', 'path', 'pathPattern', 'pathPrefix', 'mimeType']} + attributes = { + "action": ["name"], + "category": ["name"], + "data": [ + 'scheme', + 'host', + 'port', + 'path', + 'pathPattern', + 'pathPrefix', + 'mimeType', + ], + } d = {} for element in attributes.keys(): @@ -1218,13 +1385,15 @@ def get_intent_filters(self, itemtype, name): for sitem in item.findall(".//intent-filter"): for element in d.keys(): for ssitem in sitem.findall(element): - if element == 'data': # multiple attributes + if element == 'data': # multiple attributes values = {} for attribute in attributes[element]: value = ssitem.get(self._ns(attribute)) if value: if value.startswith('@'): - value = self.get_res_value(value) + value = self.get_res_value( + value + ) values[attribute] = value if values: @@ -1244,7 +1413,7 @@ def get_intent_filters(self, itemtype, name): return d - def get_permissions(self): + def get_permissions(self) -> list[str]: """ Return permissions names declared in the AndroidManifest.xml. @@ -1261,11 +1430,11 @@ def get_permissions(self): """ return self.permissions - def get_uses_implied_permission_list(self): + def get_uses_implied_permission_list(self) -> list[str]: """ - Return all permissions implied by the target SDK or other permissions. + Return all permissions implied by the target SDK or other permissions. - :rtype: list of string + :rtype: list of string """ target_sdk_version = self.get_effective_target_sdk_version() @@ -1287,8 +1456,10 @@ def get_uses_implied_permission_list(self): if READ_PHONE_STATE not in self.permissions: implied.append([READ_PHONE_STATE, None]) - if (WRITE_EXTERNAL_STORAGE in self.permissions or implied_WRITE_EXTERNAL_STORAGE) \ - and READ_EXTERNAL_STORAGE not in self.permissions: + if ( + WRITE_EXTERNAL_STORAGE in self.permissions + or implied_WRITE_EXTERNAL_STORAGE + ) and READ_EXTERNAL_STORAGE not in self.permissions: maxSdkVersion = None for name, version in self.uses_permissions: if name == WRITE_EXTERNAL_STORAGE: @@ -1297,16 +1468,22 @@ def get_uses_implied_permission_list(self): implied.append([READ_EXTERNAL_STORAGE, maxSdkVersion]) if target_sdk_version < 16: - if READ_CONTACTS in self.permissions \ - and READ_CALL_LOG not in self.permissions: + if ( + READ_CONTACTS in self.permissions + and READ_CALL_LOG not in self.permissions + ): implied.append([READ_CALL_LOG, None]) - if WRITE_CONTACTS in self.permissions \ - and WRITE_CALL_LOG not in self.permissions: + if ( + WRITE_CONTACTS in self.permissions + and WRITE_CALL_LOG not in self.permissions + ): implied.append([WRITE_CALL_LOG, None]) return implied - def _update_permission_protection_level(self, protection_level, sdk_version): + def _update_permission_protection_level( + self, protection_level, sdk_version + ): if not sdk_version or int(sdk_version) <= 15: return protection_level.replace('Or', '|').lower() return protection_level @@ -1316,22 +1493,32 @@ def _fill_deprecated_permissions(self, permissions): target_sdk = self.get_target_sdk_version() filled_permissions = permissions.copy() for permission in filled_permissions: - protection_level, label, description = filled_permissions[permission] - if ((not label or not description) - and permission in self.permission_module_min_sdk): + protection_level, label, description = filled_permissions[ + permission + ] + if ( + not label or not description + ) and permission in self.permission_module_min_sdk: x = self.permission_module_min_sdk[permission] protection_level = self._update_permission_protection_level( - x['protectionLevel'], min_sdk) + x['protectionLevel'], min_sdk + ) filled_permissions[permission] = [ - protection_level, x['label'], x['description']] + protection_level, + x['label'], + x['description'], + ] else: filled_permissions[permission] = [ self._update_permission_protection_level( - protection_level, target_sdk), - label, description] + protection_level, target_sdk + ), + label, + description, + ] return filled_permissions - def get_details_permissions(self): + def get_details_permissions(self) -> dict[str, list[str]]: """ Return permissions with details. @@ -1346,13 +1533,24 @@ def get_details_permissions(self): if i in self.permission_module: x = self.permission_module[i] l[i] = [x["protectionLevel"], x["label"], x["description"]] + elif i in self.declared_permissions: + protectionLevel_hex = self.declared_permissions[i][ + "protectionLevel" + ] + protectionLevel = protection_flags_to_attributes[ + protectionLevel_hex + ] + l[i] = [ + protectionLevel, + "Unknown permission from android reference", + "Unknown permission from android reference", + ] else: - # FIXME: the permission might be signature, if it is defined by the app itself! - l[i] = ["normal", "Unknown permission from android reference", - "Unknown permission from android reference"] + # Is there a valid case not belonging to the above two? + logger.info(f"Unknown permission {i}") return self._fill_deprecated_permissions(l) - def get_requested_aosp_permissions(self): + def get_requested_aosp_permissions(self) -> list[str]: """ Returns requested permissions declared within AOSP project. @@ -1367,7 +1565,7 @@ def get_requested_aosp_permissions(self): aosp_permissions.append(perm) return aosp_permissions - def get_requested_aosp_permissions_details(self): + def get_requested_aosp_permissions_details(self) -> dict[str, list[str]]: """ Returns requested aosp permissions with details. @@ -1382,7 +1580,7 @@ def get_requested_aosp_permissions_details(self): continue return l - def get_requested_third_party_permissions(self): + def get_requested_third_party_permissions(self) -> list[str]: """ Returns list of requested permissions not declared within AOSP project. @@ -1395,7 +1593,7 @@ def get_requested_third_party_permissions(self): third_party_permissions.append(perm) return third_party_permissions - def get_declared_permissions(self): + def get_declared_permissions(self) -> list[str]: """ Returns list of the declared permissions. @@ -1403,7 +1601,7 @@ def get_declared_permissions(self): """ return list(self.declared_permissions.keys()) - def get_declared_permissions_details(self): + def get_declared_permissions_details(self) -> dict[str, list[str]]: """ Returns declared permissions with the details. @@ -1411,39 +1609,39 @@ def get_declared_permissions_details(self): """ return self.declared_permissions - def get_max_sdk_version(self): + def get_max_sdk_version(self) -> str: """ - Return the android:maxSdkVersion attribute + Return the android:maxSdkVersion attribute - :rtype: string + :rtype: string """ return self.get_attribute_value("uses-sdk", "maxSdkVersion") - def get_min_sdk_version(self): + def get_min_sdk_version(self) -> str: """ - Return the android:minSdkVersion attribute + Return the android:minSdkVersion attribute - :rtype: string + :rtype: string """ return self.get_attribute_value("uses-sdk", "minSdkVersion") - def get_target_sdk_version(self): + def get_target_sdk_version(self) -> str: """ - Return the android:targetSdkVersion attribute + Return the android:targetSdkVersion attribute - :rtype: string + :rtype: string """ return self.get_attribute_value("uses-sdk", "targetSdkVersion") - def get_effective_target_sdk_version(self): + def get_effective_target_sdk_version(self) -> int: """ - Return the effective targetSdkVersion, always returns int > 0. + Return the effective targetSdkVersion, always returns int > 0. - If the targetSdkVersion is not set, it defaults to 1. This is - set based on defaults as defined in: - https://developer.android.com/guide/topics/manifest/uses-sdk-element.html + If the targetSdkVersion is not set, it defaults to 1. This is + set based on defaults as defined in: + https://developer.android.com/guide/topics/manifest/uses-sdk-element.html - :rtype: int + :rtype: int """ target_sdk_version = self.get_target_sdk_version() if not target_sdk_version: @@ -1453,15 +1651,15 @@ def get_effective_target_sdk_version(self): except (ValueError, TypeError): return 1 - def get_libraries(self): + def get_libraries(self) -> list[str]: """ - Return the android:name attributes for libraries + Return the android:name attributes for libraries - :rtype: list + :rtype: list """ return list(self.get_all_attribute_value("uses-library", "name")) - def get_features(self): + def get_features(self) -> list[str]: """ Return a list of all android:names found for the tag uses-feature in the AndroidManifest.xml @@ -1470,7 +1668,7 @@ def get_features(self): """ return list(self.get_all_attribute_value("uses-feature", "name")) - def is_wearable(self): + def is_wearable(self) -> bool: """ Checks if this application is build for wearables by checking if it uses the feature 'android.hardware.type.watch' @@ -1483,7 +1681,7 @@ def is_wearable(self): """ return 'android.hardware.type.watch' in self.get_features() - def is_leanback(self): + def is_leanback(self) -> bool: """ Checks if this application is build for TV (Leanback support) by checkin if it uses the feature 'android.software.leanback' @@ -1492,7 +1690,7 @@ def is_leanback(self): """ return 'android.software.leanback' in self.get_features() - def is_androidtv(self): + def is_androidtv(self) -> bool: """ Checks if this application does not require a touchscreen, as this is the rule to get into the TV section of the Play Store @@ -1500,22 +1698,297 @@ def is_androidtv(self): :returns: True if 'android.hardware.touchscreen' is not required, False otherwise """ - return self.get_attribute_value('uses-feature', 'name', required="false", name="android.hardware.touchscreen") == "android.hardware.touchscreen" + return ( + self.get_attribute_value( + 'uses-feature', + 'name', + required="false", + name="android.hardware.touchscreen", + ) + == "android.hardware.touchscreen" + ) - def get_certificate_der(self, filename): + def get_certificate_der( + self, filename: str, max_sdk_version: int = None + ) -> Union[bytes, None]: """ Return the DER coded X.509 certificate from the signature file. + If minSdkVersion is prior to Android N only the first SignerInfo is used. + If signed attributes are present, they are taken into account + Note that unsupported critical extensions and key usage are not verified! + https://android.googlesource.com/platform/tools/apksig/+/refs/tags/platform-tools-34.0.5/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java#668 :param filename: Signature filename in APK - :returns: DER coded X.509 certificate as binary + :param max_sdk_version: An optional integer parameter for the max sdk version + :returns: DER coded X.509 certificate as binary or None """ + + # Get the signature pkcs7message = self.get_file(filename) + # Get the .SF + sf_filename = os.path.splitext(filename)[0] + '.SF' + sf_object = self.get_file(sf_filename) + # Load the signature + signed_data = cms.ContentInfo.load(pkcs7message) + # Locate the SignerInfo structure + signer_infos = signed_data['content']['signer_infos'] + if not signer_infos: + logger.error( + 'No signer information found in the PKCS7 object. The APK may not be properly signed.' + ) + return None + + # Prior to Android N, Android attempts to verify only the first SignerInfo. From N onwards, Android attempts + # to verify all SignerInfos and then picks the first verified SignerInfo. + min_sdk_version = self.get_min_sdk_version() + if ( + min_sdk_version is None or int(min_sdk_version) < 24 + ): # AndroidSdkVersion.N + logger.info( + f"minSdkVersion: {min_sdk_version} is less than 24. Getting the first signerInfo only!" + ) + unverified_signer_infos_to_try = [signer_infos[0]] + else: + unverified_signer_infos_to_try = signer_infos + + # Extract certificates from the PKCS7 object + certificates = signed_data['content']['certificates'] + return_certificate = None + list_certificates_verified = [] + for signer_info in unverified_signer_infos_to_try: + try: + matching_certificate_verified = ( + self.verify_signer_info_against_sig_file( + signed_data, + certificates, + signer_info, + sf_object, + max_sdk_version, + ) + ) + except (ValueError, TypeError, OSError, InvalidSignature) as e: + logger.error( + f"The following exception was raised while verifying the certificate: {e}" + ) + return ( + None # the validation stops due to the exception raised! + ) + if matching_certificate_verified is not None: + list_certificates_verified.append( + matching_certificate_verified + ) + if not list_certificates_verified: + logger.error( + f"minSdkVersion: {min_sdk_version}, # of SignerInfos: {len(unverified_signer_infos_to_try)}. None Verified!" + ) + else: + return_certificate = list_certificates_verified[0] + return return_certificate + + def verify_signer_info_against_sig_file( + self, + signed_data, + certificates, + signer_info, + sf_object, + max_sdk_version, + ): + matching_certificate = self.find_certificate(certificates, signer_info) + matching_certificate_verified = None + digest_algorithm, crypto_hash_algorithm = self.get_hash_algorithm( + signer_info + ) + if matching_certificate is None: + raise ValueError( + "Signing certificate referenced in SignerInfo not found in SignedData" + ) + else: + if signer_info['signed_attrs'].native: + logger.info("Signed Attributes detected!") + signed_attrs = signer_info['signed_attrs'] + signed_attrs_dict = OrderedDict() + for attr in signed_attrs: + if attr['type'].dotted in signed_attrs_dict: + raise ValueError( + f"Duplicate signed attribute: {attr['type'].dotted}" + ) + signed_attrs_dict[attr['type'].dotted] = attr['values'] + + # Check content type attribute (for Android N and newer) + if max_sdk_version is None or int(max_sdk_version) >= 24: + content_type_oid = ( + '1.2.840.113549.1.9.3' # OID for contentType + ) + if content_type_oid not in signed_attrs_dict: + raise ValueError( + "No Content Type in signed attributes" + ) + content_type = signed_attrs_dict[content_type_oid][ + 0 + ].native + if ( + content_type + != signed_data['content']['encap_content_info'][ + 'content_type' + ].native + ): + logger.error( + "Content Type mismatch. Continuing to next SignerInfo, if any." + ) + return None + + # Check message digest attribute + message_digest_oid = ( + '1.2.840.113549.1.9.4' # OID for messageDigest + ) + if message_digest_oid not in signed_attrs_dict: + raise ValueError("No content digest in signed attributes") + expected_signature_file_digest = signed_attrs_dict[ + message_digest_oid + ][0].native + hash_algo = digest_algorithm() + hash_algo.update(sf_object) + actual_digest = hash_algo.digest() + + # Compare digests + if actual_digest != expected_signature_file_digest: + logger.error( + "Digest mismatch. Continuing to next SignerInfo, if any." + ) + return None + + signed_attrs_dump = signed_attrs.dump() + # Modify the first byte to 0x31 for UNIVERSAL SET + signed_attrs_dump = b'\x31' + signed_attrs_dump[1:] + matching_certificate_verified = self.verify_signature( + signer_info, + matching_certificate, + signed_attrs_dump, + crypto_hash_algorithm, + ) + else: + matching_certificate_verified = self.verify_signature( + signer_info, + matching_certificate, + sf_object, + crypto_hash_algorithm, + ) + return matching_certificate_verified + + @staticmethod + def verify_signature( + signer_info, matching_certificate, signed_data, crypto_hash_algorithm + ): + matching_certificate_verified = None + signature = signer_info['signature'].native + + # Load the certificate using asn1crypto as it can handle more cases (v1-only-with-rsa-1024-cert-not-der.apk) + cert = x509.Certificate.load(matching_certificate.chosen.dump()) + public_key_info = cert.public_key + + # Convert the ASN.1 public key to a cryptography-compatible object + public_key_der = public_key_info.dump() + public_key = serialization.load_der_public_key( + public_key_der, backend=default_backend() + ) - pkcs7obj = cms.ContentInfo.load(pkcs7message) - cert = pkcs7obj['content']['certificates'][0].chosen.dump() - return cert + try: + # RSA Key + if isinstance(public_key, rsa.RSAPublicKey): + public_key.verify( + signature, + signed_data, + padding.PKCS1v15(), + crypto_hash_algorithm(), + ) + + # DSA Key + elif isinstance(public_key, dsa.DSAPublicKey): + public_key.verify( + signature, signed_data, crypto_hash_algorithm() + ) + + # EC Key + elif isinstance(public_key, ec.EllipticCurvePublicKey): + public_key.verify( + signature, signed_data, ec.ECDSA(crypto_hash_algorithm()) + ) + + else: + raise ValueError( + f"Unsupported key algorithm: {public_key.__class__.__name__.lower()}" + ) + + # If verification succeeds, return the certificate + matching_certificate_verified = matching_certificate.chosen.dump() + + except InvalidSignature: + logger.info( + f"The public key of the certificate: {hashlib.sha256(matching_certificate.chosen.dump()).hexdigest()} " + f"is not associated with the signature!" + ) + + return matching_certificate_verified + + @staticmethod + def get_hash_algorithm(signer_info): + # Determine the hash algorithm from the SignerInfo + digest_algorithm = signer_info['digest_algorithm']['algorithm'].native + # Map the digest algorithm to a hash function + hash_algorithms = { + 'md5': (md5, hashes.MD5), + 'sha1': (sha1, hashes.SHA1), + 'sha224': (sha224, hashes.SHA224), + 'sha256': (sha256, hashes.SHA256), + 'sha384': (sha384, hashes.SHA384), + 'sha512': (sha512, hashes.SHA512), + } + if digest_algorithm not in hash_algorithms: + raise ValueError(f"Unsupported hash algorithm: {digest_algorithm}") + return hash_algorithms[digest_algorithm] + + def find_certificate(self, signed_data_certificates, signer_info): + """ + From the bag of certs, obtain the certificate referenced by the SignerInfo. + + Args: + signed_data_certificates: List of certificates in the SignedData. + signer_info: SignerInfo object containing the issuer and serial number reference. + + Returns: + The matching certificate if found, otherwise None. + """ + matching_certificate = None + issuer_and_serial_number = signer_info['sid'] + issuer_str = self.canonical_name( + issuer_and_serial_number.chosen['issuer'] + ) + serial_number = issuer_and_serial_number.native['serial_number'] + + # # Create a x509.Name object for the issuer in the SignerInfo + # issuer_name = x509.Name.build(issuer) + # issuer_str = self.canonical_name(issuer_name) + + for cert in signed_data_certificates: + if cert.name == 'certificate': + cert_issuer = self.canonical_name( + cert.chosen['tbs_certificate']['issuer'] + ) + cert_serial_number = cert.native['tbs_certificate'][ + 'serial_number' + ] + + # Compare the canonical string representations of the issuers and the serial numbers + if ( + cert_issuer == issuer_str + and cert_serial_number == serial_number + ): + matching_certificate = cert + break + + return matching_certificate - def get_certificate(self, filename): + def get_certificate(self, filename: str) -> Union[x509.Certificate, None]: """ Return a X.509 certificate object by giving the name in the apk file @@ -1523,21 +1996,151 @@ def get_certificate(self, filename): :returns: a :class:`Certificate` certificate """ cert = self.get_certificate_der(filename) - certificate = x509.Certificate.load(cert) - + if cert: + certificate = x509.Certificate.load(cert) + else: + certificate = None return certificate - def new_zip(self, filename, deleted_files=None, new_files={}): + def canonical_name(self, name: Any, android: bool = False) -> str: + """ + /* + * Method is dual-licensed under the Apache License 2.0 and GPLv3+. + * The original author has granted permission to use this code snippet under the + * Apache License 2.0 for inclusion in this project. + * https://github.com/obfusk/x509_canonical_name.py/blob/master/x509_canonical_name.py + */ + Canonical representation of x509.Name as str (with raw control characters + in places those are not stripped by normalisation). + """ + # return ",".join("+".join(f"{t}:{v}" for _, t, v in avas) for avas in self.comparison_name(name)) + return ",".join( + "+".join(f"{t}={v}" for t, v in avas) + for avas in self.comparison_name(name, android=android) + ) + + def comparison_name( + self, name: x509.Name, *, android: bool = False + ) -> List[List[Tuple[str, str]]]: + """ + /* + * Method is dual-licensed under the Apache License 2.0 and GPLv3+. + * The original author has granted permission to use this code snippet under the + * Apache License 2.0 for inclusion in this project. + * https://github.com/obfusk/x509_canonical_name.py/blob/master/x509_canonical_name.py + */ + Canonical representation of x509.Name as nested list. + + Returns a list of RDNs which are a list of AVAs which are a (type, value) + tuple, where type is the standard name or dotted OID, and value is the + normalised string representation of the value. """ - Create a new zip file - :param filename: the output filename of the zip - :param deleted_files: a regex pattern to remove specific file - :param new_files: a dictionnary of new files + return [ + [(t, nv) for _, t, nv, _ in avas] + for avas in self.x509_ordered_name(name, android=android) + ] - :type filename: string - :type deleted_files: None or a string - :type new_files: a dictionnary (key:filename, value:content of the file) + @staticmethod + def x509_ordered_name( + name: x509.Name, + *, # type: ignore[no-any-unimported] + android: bool = False, + ) -> List[List[Tuple[int, str, str, str]]]: + """ + /* + * Method is dual-licensed under the Apache License 2.0 and GPLv3+. + * The original author has granted permission to use this code snippet under the + * Apache License 2.0 for inclusion in this project. + * https://github.com/obfusk/x509_canonical_name.py/blob/master/x509_canonical_name.py + */ + Representation of x509.Name as nested list, in canonical ordering (but also + including non-canonical pre-normalised string values). + + Returns a list of RDNs which are a list of AVAs which are a (oid, type, + normalised_value, esc_value) tuple, where oid is 0 for standard names and 1 + for dotted OIDs, type is the standard name or dotted OID, normalised_value + is the normalised string representation of the value, and esc_value is the + string value before normalisation (but after escaping). + + NB: control characters are not escaped, only characters in ",+<>;\"\\" and + "#" at the start (before "whitespace" trimming) are. + + https://docs.oracle.com/en/java/javase/21/docs/api/java.base/javax/security/auth/x500/X500Principal.html#getName(java.lang.String) + https://github.com/openjdk/jdk/blob/jdk-21%2B35/src/java.base/share/classes/sun/security/x509/AVA.java#L805 + https://github.com/openjdk/jdk/blob/jdk-21%2B35/src/java.base/share/classes/sun/security/x509/RDN.java#L472 + https://android.googlesource.com/platform/libcore/+/refs/heads/android14-release/ojluni/src/main/java/sun/security/x509/RDN.java#481 + """ + + def key( + ava: Tuple[int, str, str, str] + ) -> Tuple[int, Union[str, List[int]], str]: + o, t, nv, _ = ava + if android and o: + return o, [int(x) for x in t.split(".")], nv + return o, t, nv + + DS, U8, PS = ( + x509.DirectoryString, + x509.UTF8String, + x509.PrintableString, + ) + oids = { + "2.5.4.3": ("common_name", "cn"), + "2.5.4.6": ("country_name", "c"), + "2.5.4.7": ("locality_name", "l"), + "2.5.4.8": ("state_or_province_name", "st"), + "2.5.4.9": ("street_address", "street"), + "2.5.4.10": ("organization_name", "o"), + "2.5.4.11": ("organizational_unit_name", "ou"), + "0.9.2342.19200300.100.1.1": ("user_id", "uid"), + "0.9.2342.19200300.100.1.25": ("domain_component", "dc"), + } + esc = {ord(c): f"\\{c}" for c in ",+<>;\"\\"} + cws = "".join( + chr(i) for i in range(32 + 1) + ) # control (but not esc) and whitespace + data = [] + for rdn in reversed(name.chosen): + avas = [] + for ava in rdn: + at, av = ava["type"], ava["value"] + if at.dotted in oids: + o, t = 0, oids[at.dotted][1] # order standard before OID + else: + o, t = 1, at.dotted + if o or not ( + isinstance(av, DS) and isinstance(av.chosen, (U8, PS)) + ): + ev = nv = "#" + binascii.hexlify(av.dump()).decode() + else: + ev = (av.native or "").translate(esc) + if ev.startswith("#"): + ev = "\\" + ev + nv = unicodedata.normalize( + "NFKD", + re.sub(r" +", " ", ev).strip(cws).upper().lower(), + ) + avas.append((o, t, nv, ev)) + data.append(sorted(avas, key=key)) + return data + + def new_zip( + self, + filename: str, + deleted_files: Union[str, None] = None, + new_files: dict = {}, + ) -> None: + """ + Create a new zip file + + :param filename: the output filename of the zip + :param deleted_files: a regex pattern to remove specific file + :param new_files: a dictionnary of new files + + :type filename: string + :type deleted_files: None or a string + :type new_files: a dictionnary (key:filename, value:content of the file) """ zout = zipfile.ZipFile(filename, 'w') @@ -1568,18 +2171,18 @@ def new_zip(self, filename, deleted_files=None, new_files={}): zout.writestr(item, buffer) zout.close() - def get_android_manifest_axml(self): + def get_android_manifest_axml(self) -> Union[AXMLPrinter, None]: """ - Return the :class:`AXMLPrinter` object which corresponds to the AndroidManifest.xml file + Return the :class:`AXMLPrinter` object which corresponds to the AndroidManifest.xml file - :rtype: :class:`~androguard.core.bytecodes.axml.AXMLPrinter` + :rtype: :class:`~androguard.core.axml.AXMLPrinter` """ try: return self.axml["AndroidManifest.xml"] except KeyError: return None - def get_android_manifest_xml(self): + def get_android_manifest_xml(self) -> Union[lxml.etree.Element, None]: """ Return the parsed xml object which corresponds to the AndroidManifest.xml file @@ -1590,11 +2193,11 @@ def get_android_manifest_xml(self): except KeyError: return None - def get_android_resources(self): + def get_android_resources(self) -> Union[ARSCParser, None]: """ - Return the :class:`~androguard.core.bytecodes.axml.ARSCParser` object which corresponds to the resources.arsc file + Return the :class:`~androguard.core.axml.ARSCParser` object which corresponds to the resources.arsc file - :rtype: :class:`~androguard.core.bytecodes.axml.ARSCParser` + :rtype: :class:`~androguard.core.axml.ARSCParser` """ try: return self.arsc["resources.arsc"] @@ -1603,16 +2206,20 @@ def get_android_resources(self): # There is a rare case, that no resource file is supplied. # Maybe it was added manually, thus we check here return None - self.arsc["resources.arsc"] = ARSCParser(self.zip.read("resources.arsc")) + self.arsc["resources.arsc"] = ARSCParser( + self.zip.read("resources.arsc") + ) return self.arsc["resources.arsc"] - def is_signed(self): + def is_signed(self) -> bool: """ Returns true if any of v1, v2, or v3 signatures were found. """ - return self.is_signed_v1() or self.is_signed_v2() or self.is_signed_v3() + return ( + self.is_signed_v1() or self.is_signed_v2() or self.is_signed_v3() + ) - def is_signed_v1(self): + def is_signed_v1(self) -> bool: """ Returns true if a v1 / JAR signature was found. @@ -1621,7 +2228,7 @@ def is_signed_v1(self): """ return self.get_signature_name() is not None - def is_signed_v2(self): + def is_signed_v2(self) -> bool: """ Returns true of a v2 / APK signature was found. @@ -1633,7 +2240,7 @@ def is_signed_v2(self): return self._is_signed_v2 - def is_signed_v3(self): + def is_signed_v3(self) -> bool: """ Returns true of a v3 / APK signature was found. @@ -1645,12 +2252,14 @@ def is_signed_v3(self): return self._is_signed_v3 - def read_uint32_le(self, io_stream): - value, = unpack(' int: + (value,) = unpack(' list[tuple[int, bytes]]: + """Parse digests""" if not len(digest_bytes): return [] @@ -1669,7 +2278,7 @@ def parse_signatures_or_digests(self, digest_bytes): return digests - def parse_v2_v3_signature(self): + def parse_v2_v3_signature(self) -> None: # Need to find an v2 Block in the APK. # The Google Docs gives you the following rule: # * go to the end of the ZIP File @@ -1693,11 +2302,17 @@ def parse_v2_v3_signature(self): while f.tell() > 0: f.seek(-1, io.SEEK_CUR) - r, = unpack('<4s', f.read(4)) + (r,) = unpack('<4s', f.read(4)) if r == self._PK_END_OF_CENTRAL_DIR: # Read central dir - this_disk, disk_central, this_entries, total_entries, \ - size_central, offset_central = unpack(' None: """ Parse the V2 signing block and extract all features """ @@ -1783,7 +2408,9 @@ def parse_v3_signing_block(self): # * publickey size_sequence = self.read_uint32_le(block) if size_sequence + 4 != len(block_bytes): - raise BrokenAPKError("size of sequence and blocksize does not match") + raise BrokenAPKError( + "size of sequence and blocksize does not match" + ) while block.tell() < len(block_bytes): off_signer = block.tell() @@ -1800,7 +2427,6 @@ def parse_v3_signing_block(self): raw_digests = signed_data.read(len_digests) digests = self.parse_signatures_or_digests(raw_digests) - # Certs certs = [] len_certs = self.read_uint32_le(signed_data) @@ -1841,7 +2467,7 @@ def parse_v3_signing_block(self): publickey = block.read(len_publickey) signer = APKV3Signer() - signer._bytes = view[off_signer:off_signer+size_signer] + signer._bytes = view[off_signer: off_signer + size_signer] signer.signed_data = signed_data_object signer.signatures = sigs signer.public_key = publickey @@ -1850,7 +2476,7 @@ def parse_v3_signing_block(self): self._v3_signing_data.append(signer) - def parse_v2_signing_block(self): + def parse_v2_signing_block(self) -> None: """ Parse the V2 signing block and extract all features """ @@ -1879,7 +2505,9 @@ def parse_v2_signing_block(self): size_sequence = self.read_uint32_le(block) if size_sequence + 4 != len(block_bytes): - raise BrokenAPKError("size of sequence and blocksize does not match") + raise BrokenAPKError( + "size of sequence and blocksize does not match" + ) while block.tell() < len(block_bytes): off_signer = block.tell() @@ -1925,14 +2553,14 @@ def parse_v2_signing_block(self): publickey = block.read(len_publickey) signer = APKV2Signer() - signer._bytes = view[off_signer:off_signer+size_signer] + signer._bytes = view[off_signer: off_signer + size_signer] signer.signed_data = signed_data_object signer.signatures = sigs signer.public_key = publickey self._v2_signing_data.append(signer) - def get_public_keys_der_v3(self): + def get_public_keys_der_v3(self) -> list[bytes]: """ Return a list of DER coded X.509 public keys from the v3 signature block """ @@ -1947,7 +2575,7 @@ def get_public_keys_der_v3(self): return public_keys - def get_public_keys_der_v2(self): + def get_public_keys_der_v2(self) -> list[bytes]: """ Return a list of DER coded X.509 public keys from the v3 signature block """ @@ -1962,7 +2590,7 @@ def get_public_keys_der_v2(self): return public_keys - def get_certificates_der_v3(self): + def get_certificates_der_v3(self) -> list[bytes]: """ Return a list of DER coded X.509 certificates from the v3 signature block """ @@ -1971,13 +2599,15 @@ def get_certificates_der_v3(self): self.parse_v3_signing_block() certs = [] - for signed_data in [signer.signed_data for signer in self._v3_signing_data]: + for signed_data in [ + signer.signed_data for signer in self._v3_signing_data + ]: for cert in signed_data.certificates: certs.append(cert) return certs - def get_certificates_der_v2(self): + def get_certificates_der_v2(self) -> list[bytes]: """ Return a list of DER coded X.509 certificates from the v3 signature block """ @@ -1986,75 +2616,93 @@ def get_certificates_der_v2(self): self.parse_v2_signing_block() certs = [] - for signed_data in [signer.signed_data for signer in self._v2_signing_data]: + for signed_data in [ + signer.signed_data for signer in self._v2_signing_data + ]: for cert in signed_data.certificates: certs.append(cert) return certs - def get_public_keys_v3(self): + def get_public_keys_v3(self) -> list[keys.PublicKeyInfo]: """ Return a list of :class:`asn1crypto.keys.PublicKeyInfo` which are found in the v3 signing block. """ - return [ keys.PublicKeyInfo.load(pkey) for pkey in self.get_public_keys_der_v3()] + return [ + keys.PublicKeyInfo.load(pkey) + for pkey in self.get_public_keys_der_v3() + ] - def get_public_keys_v2(self): + def get_public_keys_v2(self) -> list[keys.PublicKeyInfo]: """ Return a list of :class:`asn1crypto.keys.PublicKeyInfo` which are found in the v2 signing block. """ - return [ keys.PublicKeyInfo.load(pkey) for pkey in self.get_public_keys_der_v2()] + return [ + keys.PublicKeyInfo.load(pkey) + for pkey in self.get_public_keys_der_v2() + ] - def get_certificates_v3(self): + def get_certificates_v3(self) -> list[x509.Certificate]: """ Return a list of :class:`asn1crypto.x509.Certificate` which are found in the v3 signing block. Note that we simply extract all certificates regardless of the signer. Therefore this is just a list of all certificates found in all signers. """ - return [ x509.Certificate.load(cert) for cert in self.get_certificates_der_v3()] + return [ + x509.Certificate.load(cert) + for cert in self.get_certificates_der_v3() + ] - def get_certificates_v2(self): + def get_certificates_v2(self) -> list[x509.Certificate]: """ Return a list of :class:`asn1crypto.x509.Certificate` which are found in the v2 signing block. Note that we simply extract all certificates regardless of the signer. Therefore this is just a list of all certificates found in all signers. """ - return [ x509.Certificate.load(cert) for cert in self.get_certificates_der_v2()] + return [ + x509.Certificate.load(cert) + for cert in self.get_certificates_der_v2() + ] - def get_certificates_v1(self): + def get_certificates_v1(self) -> list[Union[x509.Certificate, None]]: """ - Return a list of :class:`asn1crypto.x509.Certificate` which are found + Return a list of verified :class:`asn1crypto.x509.Certificate` which are found in the META-INF folder (v1 signing). - Note that we simply extract all certificates regardless of the signer. - Therefore this is just a list of all certificates found in all signers. """ certs = [] for x in self.get_signature_names(): - certs.append(x509.Certificate.load(self.get_certificate_der(x))) - + cc = self.get_certificate_der(x) + if cc is not None: + certs.append(x509.Certificate.load(cc)) return certs - def get_certificates(self): + def get_certificates(self) -> list[x509.Certificate]: """ Return a list of unique :class:`asn1crypto.x509.Certificate` which are found in v1, v2 and v3 signing Note that we simply extract all certificates regardless of the signer. Therefore this is just a list of all certificates found in all signers. + Exception is v1, for which the certificate returned is verified. """ fps = [] certs = [] - for x in self.get_certificates_v1() + self.get_certificates_v2() + self.get_certificates_v3(): + for x in ( + self.get_certificates_v1() + + self.get_certificates_v2() + + self.get_certificates_v3() + ): if x.sha256 not in fps: fps.append(x.sha256) certs.append(x) return certs - def get_signature_name(self): + def get_signature_name(self) -> Union[str, None]: """ - Return the name of the first signature file found. + Return the name of the first signature file found. """ if self.get_signature_names(): return self.get_signature_names()[0] @@ -2062,7 +2710,7 @@ def get_signature_name(self): # Unsigned APK return None - def get_signature_names(self): + def get_signature_names(self) -> list[str]: """ Return a list of the signature file names (v1 Signature / JAR Signature) @@ -2077,11 +2725,15 @@ def get_signature_names(self): if "{}.SF".format(i.rsplit(".", 1)[0]) in self.get_files(): signatures.append(i) else: - logger.warning("v1 signature file {} missing .SF file - Partial signature!".format(i)) + logger.warning( + "v1 signature file {} missing .SF file - Partial signature!".format( + i + ) + ) return signatures - def get_signature(self): + def get_signature(self) -> Union[str, None]: """ Return the data of the first signature file found (v1 Signature / JAR Signature) @@ -2093,7 +2745,7 @@ def get_signature(self): else: return None - def get_signatures(self): + def get_signatures(self) -> list[bytes]: """ Return a list of the data of the signature files. Only v1 / JAR Signing. @@ -2109,7 +2761,7 @@ def get_signatures(self): return signature_datas - def show(self): + def show(self) -> None: self.get_files_types() print("FILES: ") @@ -2162,23 +2814,31 @@ def show(self): show_Certificate(c) -def show_Certificate(cert, short=False): +def show_Certificate(cert, short: bool = False) -> None: """ - Print Fingerprints, Issuer and Subject of an X509 Certificate. + Print Fingerprints, Issuer and Subject of an X509 Certificate. - :param cert: X509 Certificate to print - :param short: Print in shortform for DN (Default: False) + :param cert: X509 Certificate to print + :param short: Print in shortform for DN (Default: False) - :type cert: :class:`asn1crypto.x509.Certificate` - :type short: Boolean + :type cert: :class:`asn1crypto.x509.Certificate` + :type short: Boolean """ print("SHA1 Fingerprint: {}".format(cert.sha1_fingerprint)) print("SHA256 Fingerprint: {}".format(cert.sha256_fingerprint)) - print("Issuer: {}".format(get_certificate_name_string(cert.issuer.native, short=short))) - print("Subject: {}".format(get_certificate_name_string(cert.subject.native, short=short))) - - -def ensure_final_value(packageName, arsc, value): + print( + "Issuer: {}".format( + get_certificate_name_string(cert.issuer.native, short=short) + ) + ) + print( + "Subject: {}".format( + get_certificate_name_string(cert.subject.native, short=short) + ) + ) + + +def ensure_final_value(packageName: str, arsc: ARSCParser, value: str) -> str: """Ensure incoming value is always the value, not the resid androguard will sometimes return the Android "resId" aka @@ -2199,3 +2859,66 @@ def ensure_final_value(packageName, arsc, value): pass return returnValue return '' + + +def get_apkid(apkfile: str) -> tuple[str, str, str]: + """Read (appid, versionCode, versionName) from an APK + + This first tries to do quick binary XML parsing to just get the + values that are needed. It will fallback to full androguard + parsing, which is slow, if it can't find the versionName value or + versionName is set to a Android String Resource (e.g. an integer + hex value that starts with @). + + """ + logger.debug("GET_APKID") + + if not os.path.exists(apkfile): + logger.error("'{apkfile}' does not exist!".format(apkfile=apkfile)) + + appid = None + versionCode = None + versionName = None + apk = ZipEntry.parse(apkfile, False) + manifest = apk.read('AndroidManifest.xml') + axml = AXMLParser(manifest) + count = 0 + while axml.is_valid(): + _type = next(axml) + count += 1 + if _type == START_TAG: + for i in range(0, axml.getAttributeCount()): + name = axml.getAttributeName(i) + _type = axml.getAttributeValueType(i) + _data = axml.getAttributeValueData(i) + value = format_value( + _type, _data, lambda _: axml.getAttributeValue(i) + ) + if appid is None and name == 'package': + appid = value + elif versionCode is None and name == 'versionCode': + if value.startswith('0x'): + versionCode = str(int(value, 16)) + else: + versionCode = value + elif versionName is None and name == 'versionName': + versionName = value + + if axml.name == 'manifest': + break + elif _type == END_TAG or _type == TEXT or _type == END_DOCUMENT: + raise RuntimeError( + '{path}: must be the first element in AndroidManifest.xml'.format( + path=apkfile + ) + ) + + if not versionName or versionName[0] == '@': + a = APK(apkfile) + versionName = ensure_final_value( + a.package, a.get_android_resources(), a.get_androidversion_name() + ) + if not versionName: + versionName = '' # versionName is expected to always be a str + + return appid, versionCode, versionName.strip('\0') diff --git a/mobsf/StaticAnalyzer/tools/androguard4/apkinspector/extract.py b/mobsf/StaticAnalyzer/tools/androguard4/apkinspector/extract.py new file mode 100644 index 0000000000..697b09b46b --- /dev/null +++ b/mobsf/StaticAnalyzer/tools/androguard4/apkinspector/extract.py @@ -0,0 +1,106 @@ +# -*- coding: utf_8 -*- +# flake8: noqa +import zlib +import os + + +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' + elif compressed_size == uncompressed_size: + compressed_data = apk_file.read(uncompressed_size) + extracted_data = compressed_data + indicator = 'STORED_TAMPERED' + else: + cur_loc = apk_file.tell() + try: + 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 + + +def extract_all_files_from_central_directory(apk_file, central_directory_entries, local_header_entries, output_dir): + """ + Extracts all files from an APK based on the entries detected in the central_directory_entries. + + :param apk_file: The APK file e.g. with open('test.apk', 'rb') as apk_file + :type apk_file: bytesIO + :param central_directory_entries: The dictionary with all the entries for the central directory + :type central_directory_entries: dict + :param local_header_entries: The dictionary with all the local header entries + :type local_header_entries: dict + :param output_dir: The output directory where to save the files. + :type output_dir: str + :return: Returns 0 if no errors, 1 if an exception and 2 if the output directory already exists + :rtype: int + """ + try: + # Check if the output directory already exists + if os.path.exists(output_dir): + print("Extraction aborted. Output directory already exists.") + return 2 + # Create the output directory or overwrite if it already exists + os.makedirs(output_dir, exist_ok=True) + # Iterate over central directory entries + for filename, cd_header_info in central_directory_entries.items(): + if not filename: + # to account for the cases where an empty filename entry is added + continue + # Extract the file using the local header information + extracted_data = \ + extract_file_based_on_header_info( + apk_file, local_header_entries[filename], cd_header_info)[0] + # Construct the output file path + output_path = os.path.join(output_dir, filename) + # Create directories if necessary + os.makedirs(os.path.dirname(output_path), exist_ok=True) + # Write the extracted data to the output file + with open(output_path, 'wb') as output_file: + output_file.write(extracted_data) + return 0 + except Exception as e: + print(f"Error extracting files: {e}") + return 1 diff --git a/mobsf/StaticAnalyzer/tools/androguard4/zipfile.py b/mobsf/StaticAnalyzer/tools/androguard4/apkinspector/headers.py similarity index 82% rename from mobsf/StaticAnalyzer/tools/androguard4/zipfile.py rename to mobsf/StaticAnalyzer/tools/androguard4/apkinspector/headers.py index 2ce878e687..a9d3096c32 100644 --- a/mobsf/StaticAnalyzer/tools/androguard4/zipfile.py +++ b/mobsf/StaticAnalyzer/tools/androguard4/apkinspector/headers.py @@ -1,70 +1,20 @@ # -*- coding: utf_8 -*- # flake8: noqa -"""This file is from apkinspector licensed under the Apache License 2.0.""" +# ApkInspector - Nov 24, 2024 - 293ab2d89ab9ce011c7dbbc5df3c876172875a1c import io -import zlib +import os 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 +from .extract import extract_file_based_on_header_info, extract_all_files_from_central_directory +from .helpers import pretty_print_header, save_to_json, save_data_to_file 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, @@ -97,31 +47,35 @@ def parse(cls, apk_file): 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 + position = max(0, file_size - offset - chunk_size) 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 + signature_offset = chunk.rfind(b'\x50\x4b\x05\x06') # EOCD signature if signature_offset != -1: eo_central_directory_offset = position + signature_offset - break # Found End of central directory record (EOCD) signature - offset += chunk_size + break # Found EOCD signature + # Adjust offset to overlap by 4 bytes + offset += chunk_size - 4 + if signature_offset == -1: - raise ValueError("End of central directory record (EOCD) signature not found") + raise ValueError( + "End of central directory record (EOCD) signature not found") apk_file.seek(eo_central_directory_offset) signature = apk_file.read(4) number_of_this_disk = struct.unpack(' Dict[str, CentralDirectoryEntry]: @@ -564,3 +534,63 @@ def namelist(self): :rtype: list """ return [vl for vl in self.central_directory.to_dict()] + + def extract_all(self, extract_path, apk_name): + """ + Extracts all the contents of the APK. + + :param extract_path: where to extract it + :type extract_path: str + :param apk_name: the name of the apk + :type apk_name: str + """ + output_path = os.path.join(extract_path, apk_name) + if not extract_all_files_from_central_directory(self.zip, self.to_dict()["central_directory"], + self.to_dict()["local_headers"], output_path): + print(f"Extraction successful for: {apk_name}") + + +def print_headers_of_filename(cd_h_of_file, local_header_of_file): + """ + Prints out the details for both the central directory header and the local file header. Useful for the CLI. + + :param cd_h_of_file: central directory header of a filename as it may be retrieved from headers_of_filename + :type cd_h_of_file: dict + :param local_header_of_file: local header dictionary of a filename as it may be retrieved from headers_of_filename + :type local_header_of_file: dict + """ + if not cd_h_of_file or not local_header_of_file: + print("Are you sure the filename exists?") + return + pretty_print_header("CENTRAL DIRECTORY") + for k in cd_h_of_file: + if k == 'Relative offset of local file header' or k == 'Offset in the central directory header': + print(f"{k:40} : {hex(int(cd_h_of_file[k]))} | {cd_h_of_file[k]}") + else: + print(f"{k:40} : {cd_h_of_file[k]}") + pretty_print_header("LOCAL HEADER") + for k in local_header_of_file: + print(f"{k:40} : {local_header_of_file[k]}") + + +def show_and_save_info_of_headers(entries, apk_name, header_type: str, export: bool, show: bool): + """ + Print information for each entry for the central directory header and allow to possibly export to JSON. + + :param entries: The dictionary with all the entries for the central directory + :type entries: dict + :param apk_name: String with the name of the APK, so it can be used for the export. + :type apk_name: str + :param header_type: What type of header that is, either central_directory or local, to be used for the export + :type header_type: str + :param export: Boolean for exporting or not to JSON + :type export: bool + :param show: Boolean for printing or not the entries + :type show: bool + """ + if show: + for entry in entries: + pretty_print_header(entry) + print(entries[entry]) + if export: + save_to_json(f"{apk_name}_{header_type}_header.json", entries) diff --git a/mobsf/StaticAnalyzer/tools/androguard4/apkinspector/helpers.py b/mobsf/StaticAnalyzer/tools/androguard4/apkinspector/helpers.py new file mode 100644 index 0000000000..44213a0e33 --- /dev/null +++ b/mobsf/StaticAnalyzer/tools/androguard4/apkinspector/helpers.py @@ -0,0 +1,68 @@ +# -*- coding: utf_8 -*- +# flake8: noqa +import json + + +def pretty_print_header(header_text, width=50, char='-'): + """ + Formatting output used for the CLI + + :param header_text: The text to be displayed + :type header_text: str + :param width: total width of the display + :type width: int + :param char: which char to be used as a filler + :type char: str + """ + padding = max(0, width - len(header_text)) // 2 + formatted_header = f"\n{char * padding} {header_text} {char * padding}" + print(formatted_header) + + +def save_data_to_file(filename, data): + """ + Write data to file + + :param data: the actual data + :type data: bytes + :param filename: file to be saved in + :type filename: str + """ + try: + with open(filename, 'wb') as output_file: + output_file.write(data) + print(f"Data saved to {filename}") + except Exception as e: + print(f"Error while saving data to {filename}: {e}") + + +def save_to_json(filename, dictionary): + """ + Simple method to save a dictionary as JSON into the filename. + + :param filename: the name of the file to be saved as + :type filename: str + :param dictionary: the dictionary to be saved as JSON + :type dictionary: dict + """ + with open(filename, "w") as h_file: + json.dump(dictionary, h_file, indent=4) + + +def escape_xml_entities(data): + """ + Escaping characters that cant be included within an XML file. + + :param data: The string to escape + :type data: str + :return: The escaped output + :rtype: str + """ + replacements = { + '<': '<', + '>': '>', + '&': '&', + '"': '"', + "'": ''' + } + return ''.join(replacements.get(c, c) for c in data) diff --git a/mobsf/StaticAnalyzer/tools/androguard4/axml.py b/mobsf/StaticAnalyzer/tools/androguard4/axml.py index 569ae6b934..c85c2de5b9 100644 --- a/mobsf/StaticAnalyzer/tools/androguard4/axml.py +++ b/mobsf/StaticAnalyzer/tools/androguard4/axml.py @@ -1,21 +1,31 @@ # -*- coding: utf_8 -*- # flake8: noqa -from .resources import public -from .types import * +# Androguard4 AXML - Nov 24, 2024 -04a5703b8ba7c181bb9f5f5995a2c16b6f9353cf +# Allows type hinting of types not-yet-declared +# in Python >= 3.7 +# see https://peps.python.org/pep-0563/ +from __future__ import annotations -from struct import pack, unpack -from xml.sax.saxutils import escape +import binascii import collections +import io +import re from collections import defaultdict +from struct import pack, unpack +from typing import BinaryIO, Union +from xml.sax.saxutils import escape from lxml import etree -import re -import binascii -import io + import logging +from .resources import public +from .types import * + + logger = logging.getLogger(__name__) -logger.setLevel(level=logging.INFO) +logger.setLevel(level=logging.CRITICAL) + # Constants for ARSC Files # see http://aospxref.com/android-13.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#233 @@ -24,23 +34,23 @@ RES_TABLE_TYPE = 0x0002 RES_XML_TYPE = 0x0003 -RES_XML_FIRST_CHUNK_TYPE = 0x0100 -RES_XML_START_NAMESPACE_TYPE = 0x0100 -RES_XML_END_NAMESPACE_TYPE = 0x0101 -RES_XML_START_ELEMENT_TYPE = 0x0102 -RES_XML_END_ELEMENT_TYPE = 0x0103 -RES_XML_CDATA_TYPE = 0x0104 -RES_XML_LAST_CHUNK_TYPE = 0x017f - -RES_XML_RESOURCE_MAP_TYPE = 0x0180 - -RES_TABLE_PACKAGE_TYPE = 0x0200 -RES_TABLE_TYPE_TYPE = 0x0201 -RES_TABLE_TYPE_SPEC_TYPE = 0x0202 -RES_TABLE_LIBRARY_TYPE = 0x0203 -RES_TABLE_OVERLAYABLE_TYPE = 0x0204 +RES_XML_FIRST_CHUNK_TYPE = 0x0100 +RES_XML_START_NAMESPACE_TYPE = 0x0100 +RES_XML_END_NAMESPACE_TYPE = 0x0101 +RES_XML_START_ELEMENT_TYPE = 0x0102 +RES_XML_END_ELEMENT_TYPE = 0x0103 +RES_XML_CDATA_TYPE = 0x0104 +RES_XML_LAST_CHUNK_TYPE = 0x017F + +RES_XML_RESOURCE_MAP_TYPE = 0x0180 + +RES_TABLE_PACKAGE_TYPE = 0x0200 +RES_TABLE_TYPE_TYPE = 0x0201 +RES_TABLE_TYPE_SPEC_TYPE = 0x0202 +RES_TABLE_LIBRARY_TYPE = 0x0203 +RES_TABLE_OVERLAYABLE_TYPE = 0x0204 RES_TABLE_OVERLAYABLE_POLICY_TYPE = 0x0205 -RES_TABLE_STAGED_ALIAS_TYPE = 0x0206 +RES_TABLE_STAGED_ALIAS_TYPE = 0x0206 # Flags in the STRING Section SORTED_FLAG = 1 << 0 UTF8_FLAG = 1 << 8 @@ -78,7 +88,7 @@ TYPE_STRING: "string", } -RADIX_MULTS = [0.00390625, 3.051758E-005, 1.192093E-007, 4.656613E-010] +RADIX_MULTS = [0.00390625, 3.051758e-005, 1.192093e-007, 4.656613e-010] DIMENSION_UNITS = ["px", "dip", "sp", "pt", "in", "mm"] FRACTION_UNITS = ["%", "%p"] @@ -87,10 +97,11 @@ class ResParserError(Exception): """Exception for the parsers""" + pass -def complexToFloat(xcomplex): +def complexToFloat(xcomplex) -> float: """ Convert a complex unit into float """ @@ -100,11 +111,12 @@ def complexToFloat(xcomplex): class StringBlock: """ StringBlock is a CHUNK inside an AXML File: `ResStringPool_header` - It contains all strings, which are used by referecing to ID's + It contains all strings, which are used by referencing to ID's See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#436 """ - def __init__(self, buff, header): + + def __init__(self, buff: BinaryIO, header: ARSCHeader) -> None: """ :param buff: buffer which holds the string block :param header: a instance of :class:`~ARSCHeader` @@ -119,14 +131,18 @@ def __init__(self, buff, header): # flags self.flags = unpack(' 0: - logger.info("Styles Offset given, but styleCount is zero. " - "This is not a problem but could indicate packers.") + logger.info( + "Styles Offset given, but styleCount is zero. " + "This is not a problem but could indicate packers." + ) self.m_stringOffsets = [] self.m_styleOffsets = [] @@ -175,7 +193,9 @@ def __init__(self, buff, header): self.m_styles.append(unpack('".format(self.stringCount, self.styleCount, self.m_isUTF8) + return "".format( + self.stringCount, self.styleCount, self.m_isUTF8 + ) def __getitem__(self, idx): """ @@ -196,7 +216,7 @@ def __iter__(self): for i in range(self.stringCount): yield self.getString(i) - def getString(self, idx): + def getString(self, idx: int) -> str: """ Return the string at the index in the string table @@ -206,7 +226,7 @@ def getString(self, idx): if idx in self._cache: return self._cache[idx] - if idx < 0 or not self.m_stringOffsets or idx > self.stringCount: + if idx < 0 or not self.m_stringOffsets or idx >= self.stringCount: return "" offset = self.m_stringOffsets[idx] @@ -218,7 +238,7 @@ def getString(self, idx): return self._cache[idx] - def getStyle(self, idx): + def getStyle(self, idx: int) -> int: """ Return the style associated with the index @@ -227,7 +247,7 @@ def getStyle(self, idx): """ return self.m_styles[idx] - def _decode8(self, offset): + def _decode8(self, offset: int) -> str: """ Decode an UTF-8 String at the given offset @@ -243,14 +263,27 @@ def _decode8(self, offset): encoded_bytes, skip = self._decode_length(offset, 1) offset += skip + # Two checks should happen here: + # a) offset + encoded_bytes surpassing the string_pool length and + # b) non-null terminated strings which should be rejected + # platform/frameworks/base/libs/androidfw/ResourceTypes.cpp#789 + if len(self.m_charbuff) < (offset + encoded_bytes): + logger.warning( + f"String size: {offset + encoded_bytes} is exceeding string pool size. Returning empty string." + ) + return "" data = self.m_charbuff[offset: offset + encoded_bytes] if self.m_charbuff[offset + encoded_bytes] != 0: - raise ResParserError("UTF-8 String is not null terminated! At offset={}".format(offset)) + raise ResParserError( + "UTF-8 String is not null terminated! At offset={}".format( + offset + ) + ) return self._decode_bytes(data, 'utf-8', str_len) - def _decode16(self, offset): + def _decode16(self, offset: int) -> str: """ Decode an UTF-16 String at the given offset @@ -263,15 +296,34 @@ def _decode16(self, offset): # The len is the string len in utf-16 units encoded_bytes = str_len * 2 + # Two checks should happen here: + # a) offset + encoded_bytes surpassing the string_pool length and + # b) non-null terminated strings which should be rejected + # platform/frameworks/base/libs/androidfw/ResourceTypes.cpp#789 + if len(self.m_charbuff) < (offset + encoded_bytes): + logger.warning( + f"String size: {offset + encoded_bytes} is exceeding string pool size. Returning empty string." + ) + return "" + data = self.m_charbuff[offset: offset + encoded_bytes] - if self.m_charbuff[offset + encoded_bytes:offset + encoded_bytes + 2] != b"\x00\x00": - raise ResParserError("UTF-16 String is not null terminated! At offset={}".format(offset)) + if ( + self.m_charbuff[ + offset + encoded_bytes: offset + encoded_bytes + 2 + ] + != b"\x00\x00" + ): + raise ResParserError( + "UTF-16 String is not null terminated! At offset={}".format( + offset + ) + ) return self._decode_bytes(data, 'utf-16', str_len) @staticmethod - def _decode_bytes(data, encoding, str_len): + def _decode_bytes(data: bytes, encoding: str, str_len: int) -> str: """ Generic decoding with length check. The string is decoded from bytes with the given encoding, then the length @@ -288,7 +340,7 @@ def _decode_bytes(data, encoding, str_len): logger.warning("invalid decoded string length") return string - def _decode_length(self, offset, sizeof_char): + def _decode_length(self, offset: int, sizeof_char: int) -> tuple[int, int]: """ Generic Length Decoding at offset of string @@ -308,7 +360,9 @@ def _decode_length(self, offset, sizeof_char): fmt = "<2{}".format('B' if sizeof_char == 1 else 'H') highbit = 0x80 << (8 * (sizeof_char - 1)) - length1, length2 = unpack(fmt, self.m_charbuff[offset:(offset + sizeof_2chars)]) + length1, length2 = unpack( + fmt, self.m_charbuff[offset: (offset + sizeof_2chars)] + ) if (length1 & highbit) != 0: length = ((length1 & ~highbit) << (8 * sizeof_char)) | length2 @@ -319,26 +373,39 @@ def _decode_length(self, offset, sizeof_char): # These are true asserts, as the size should never be less than the values if sizeof_char == 1: - assert length <= 0x7FFF, "length of UTF-8 string is too large! At offset={}".format(offset) + assert ( + length <= 0x7FFF + ), "length of UTF-8 string is too large! At offset={}".format( + offset + ) else: - assert length <= 0x7FFFFFFF, "length of UTF-16 string is too large! At offset={}".format(offset) + assert ( + length <= 0x7FFFFFFF + ), "length of UTF-16 string is too large! At offset={}".format( + offset + ) return length, size - def show(self): + def show(self) -> None: """ Print some information on stdout about the string table """ - print("StringBlock(stringsCount=0x%x, " - "stringsOffset=0x%x, " - "stylesCount=0x%x, " - "stylesOffset=0x%x, " - "flags=0x%x" - ")" % (self.stringCount, - self.stringsOffset, - self.styleCount, - self.stylesOffset, - self.flags)) + print( + "StringBlock(stringsCount=0x%x, " + "stringsOffset=0x%x, " + "stylesCount=0x%x, " + "stylesOffset=0x%x, " + "flags=0x%x" + ")" + % ( + self.stringCount, + self.stringsOffset, + self.styleCount, + self.stylesOffset, + self.flags, + ) + ) if self.stringCount > 0: print() @@ -376,7 +443,8 @@ class AXMLParser: See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#563 """ - def __init__(self, raw_buff): + + def __init__(self, raw_buff: bytes) -> None: logger.debug("AXMLParser") self._reset() @@ -385,17 +453,26 @@ def __init__(self, raw_buff): self.axml_tampered = False self.buff = io.BufferedReader(io.BytesIO(raw_buff)) self.buff_size = self.buff.raw.getbuffer().nbytes + self.packerwarning = False # Minimum is a single ARSCHeader, which would be a strange edge case... if self.buff_size < 8: - logger.error("Filesize is too small to be a valid AXML file! Filesize: {}".format(self.buff_size)) + logger.error( + "Filesize is too small to be a valid AXML file! Filesize: {}".format( + self.buff_size + ) + ) self._valid = False return # This would be even stranger, if an AXML file is larger than 4GB... # But this is not possible as the maximum chunk size is a unsigned 4 byte int. if self.buff_size > 0xFFFFFFFF: - logger.error("Filesize is too large to be a valid AXML file! Filesize: {}".format(self.buff_size)) + logger.error( + "Filesize is too large to be a valid AXML file! Filesize: {}".format( + self.buff_size + ) + ) self._valid = False return @@ -412,43 +489,67 @@ def __init__(self, raw_buff): if axml_header.header_size == 28024: # Can be a common error: the file is not an AXML but a plain XML # The file will then usually start with ' self.buff_size: - logger.error("This does not look like an AXML file. Declared filesize does not match real size: {} vs {}".format(self.filesize, self.buff_size)) + logger.error( + "This does not look like an AXML file. Declared filesize does not match real size: {} vs {}".format( + self.filesize, self.buff_size + ) + ) self._valid = False return if self.filesize < self.buff_size: # The file can still be parsed up to the point where the chunk should end. self.axml_tampered = True - logger.warning("Declared filesize ({}) is smaller than total file size ({}). " - "Was something appended to the file? Trying to parse it anyways.".format(self.filesize, self.buff.size())) + logger.warning( + "Declared filesize ({}) is smaller than total file size ({}). " + "Was something appended to the file? Trying to parse it anyways.".format( + self.filesize, self.buff_size + ) + ) # Not that severe of an error, we have plenty files where this is not # set correctly if axml_header.type != RES_XML_TYPE: self.axml_tampered = True - logger.warning("AXML file has an unusual resource type! " - "Malware likes to to such stuff to anti androguard! " - "But we try to parse it anyways. Resource Type: 0x{:04x}".format(axml_header.type)) + logger.warning( + "AXML file has an unusual resource type! " + "Malware likes to to such stuff to anti androguard! " + "But we try to parse it anyways. Resource Type: 0x{:04x}".format( + axml_header.type + ) + ) # Now we parse the STRING POOL try: header = ARSCHeader(self.buff, expected_type=RES_STRING_POOL_TYPE) logger.debug("STRING_POOL {}".format(header)) except ResParserError as e: - logger.error("Error parsing resource header of string pool: {}".format(e)) + logger.error( + "Error parsing resource header of string pool: {}".format(e) + ) self._valid = False return if header.header_size != 0x1C: - logger.error("This does not look like an AXML file. String chunk header size does not equal 28! header size = {}".format(header.header_size)) + logger.error( + "This does not look like an AXML file. String chunk header size does not equal 28! header size = {}".format( + header.header_size + ) + ) self._valid = False return @@ -462,7 +563,7 @@ def __init__(self, raw_buff): # Store a list of prefix/uri mappings encountered self.namespaces = [] - def is_valid(self): + def is_valid(self) -> bool: """ Get the state of the AXMLPrinter. if an error happend somewhere in the process of parsing the file, @@ -515,57 +616,89 @@ def _do_next(self): # Check size: < 8 bytes mean that the chunk is not complete # Should be aligned to 4 bytes. if h.size < 8 or (h.size % 4) != 0: - logger.error("Invalid chunk size in chunk XML_RESOURCE_MAP") + logger.error( + "Invalid chunk size in chunk XML_RESOURCE_MAP" + ) self._valid = False return for i in range((h.size - h.header_size) // 4): - self.m_resourceIDs.append(unpack(' RES_XML_LAST_CHUNK_TYPE: + if ( + h.type < RES_XML_FIRST_CHUNK_TYPE + or h.type > RES_XML_LAST_CHUNK_TYPE + ): # h.size is the size of the whole chunk including the header. # We read already 8 bytes of the header, thus we need to # subtract them. - logger.error("Not a XML resource chunk type: 0x{:04x}. Skipping {} bytes".format(h.type, h.size)) + logger.error( + "Not a XML resource chunk type: 0x{:04x}. Skipping {} bytes".format( + h.type, h.size + ) + ) self.buff.seek(h.end) continue # Check that we read a correct header if h.header_size != 0x10: - logger.error("XML Resource Type Chunk header size does not match 16! " \ - "At chunk type 0x{:04x}, declared header size=0x{:04x}, chunk size=0x{:04x}".format(h.type, h.header_size, h.size)) + logger.error( + "XML Resource Type Chunk header size does not match 16! " + "At chunk type 0x{:04x}, declared header size=0x{:04x}, chunk size=0x{:04x}".format( + h.type, h.header_size, h.size + ) + ) self.buff.seek(h.end) continue # Line Number of the source file, only used as meta information - self.m_lineNumber, = unpack(' uri {}: '{}'".format(prefix, s_prefix, uri, s_uri)) + logger.debug( + "Start of Namespace mapping: prefix {}: '{}' --> uri {}: '{}'".format( + prefix, s_prefix, uri, s_uri + ) + ) if s_uri == '': - logger.warning("Namespace prefix '{}' resolves to empty URI. " - "This might be a packer.".format(s_prefix)) + logger.warning( + "Namespace prefix '{}' resolves to empty URI. " + "This might be a packer.".format(s_prefix) + ) if (prefix, uri) in self.namespaces: - logger.debug("Namespace mapping ({}, {}) already seen! " - "This is usually not a problem but could indicate packers or broken AXML compilers.".format(prefix, uri)) + logger.debug( + "Namespace mapping ({}, {}) already seen! " + "This is usually not a problem but could indicate packers or broken AXML compilers.".format( + prefix, uri + ) + ) self.namespaces.append((prefix, uri)) # We can continue with the next chunk, as we store the namespace @@ -574,15 +707,17 @@ def _do_next(self): if h.type == RES_XML_END_NAMESPACE_TYPE: # END_PREFIX contains again prefix and uri field - prefix, = unpack('> 16) - 1 self.m_attribute_count = attributeCount & 0xFFFF @@ -625,20 +760,26 @@ def _do_next(self): # * Type # * Data for j in range(0, ATTRIBUTE_LENGTH): - self.m_attributes.append(unpack('> 24 self.m_event = START_TAG break if h.type == RES_XML_END_ELEMENT_TYPE: - self.m_namespaceUri, = unpack(' uint32_t index - self.m_name, = unpack(' str: """ - Return the String assosciated with the tag name + Return the String associated with the tag name """ - if self.m_name == -1 or (self.m_event != START_TAG and self.m_event != END_TAG): + if self.m_name == -1 or ( + self.m_event != START_TAG and self.m_event != END_TAG + ): return '' return self.sb[self.m_name] @property - def comment(self): + def comment(self) -> Union[str, None]: """ Return the comment at the current position or None if no comment is given @@ -698,11 +845,13 @@ def comment(self): return self.sb[self.m_comment_index] @property - def namespace(self): + def namespace(self) -> str: """ Return the Namespace URI (if any) as a String for the current tag """ - if self.m_name == -1 or (self.m_event != START_TAG and self.m_event != END_TAG): + if self.m_name == -1 or ( + self.m_event != START_TAG and self.m_event != END_TAG + ): return '' # No Namespace @@ -712,7 +861,7 @@ def namespace(self): return self.sb[self.m_namespaceUri] @property - def nsmap(self): + def nsmap(self) -> dict[str, str]: """ Returns the current namespace mapping as a dictionary @@ -733,12 +882,12 @@ def nsmap(self): # Solve 2) & 4) by not including if s_uri != "" and s_prefix != "": # solve 1) by using the last one in the list - NSMAP[s_prefix] = s_uri + NSMAP[s_prefix] = s_uri.strip() return NSMAP @property - def text(self): + def text(self) -> str: """ Return the String assosicated with the current text """ @@ -747,21 +896,21 @@ def text(self): return self.sb[self.m_name] - def getName(self): + def getName(self) -> str: """ Legacy only! use :py:attr:`~androguard.core.bytecodes.AXMLParser.name` instead """ return self.name - def getText(self): + def getText(self) -> str: """ Legacy only! use :py:attr:`~androguard.core.bytecodes.AXMLParser.text` instead """ return self.text - def getPrefix(self): + def getPrefix(self) -> str: """ Legacy only! use :py:attr:`~androguard.core.bytecodes.AXMLParser.namespace` instead @@ -781,7 +930,7 @@ def _get_attribute_offset(self, index): return offset - def getAttributeCount(self): + def getAttributeCount(self) -> int: """ Return the number of Attributes for a Tag or -1 if not in a tag @@ -791,7 +940,7 @@ def getAttributeCount(self): return self.m_attribute_count - def getAttributeUri(self, index): + def getAttributeUri(self, index: int): """ Returns the numeric ID for the namespace URI of an attribute """ @@ -802,7 +951,7 @@ def getAttributeUri(self, index): return uri - def getAttributeNamespace(self, index): + def getAttributeNamespace(self, index: int): """ Return the Namespace URI (if any) for the attribute """ @@ -816,7 +965,7 @@ def getAttributeNamespace(self, index): return self.sb[uri] - def getAttributeName(self, index): + def getAttributeName(self, index: int): """ Returns the String which represents the attribute name """ @@ -826,17 +975,22 @@ def getAttributeName(self, index): res = self.sb[name] # If the result is a (null) string, we need to look it up. - if not res or res == ":": + if name < len(self.m_resourceIDs): attr = self.m_resourceIDs[name] if attr in public.SYSTEM_RESOURCES['attributes']['inverse']: - res = 'android:' + public.SYSTEM_RESOURCES['attributes']['inverse'][attr] - else: - # Attach the HEX Number, so for multiple missing attributes we do not run - # into problems. - res = 'android:UNKNOWN_SYSTEM_ATTRIBUTE_{:08x}'.format(attr) + res = public.SYSTEM_RESOURCES['attributes']['inverse'][ + attr + ].replace("_", ":") + if res != self.sb[name]: + self.packerwarning = True + + if not res or res == ":": + # Attach the HEX Number, so for multiple missing attributes we do not run + # into problems. + res = 'android:UNKNOWN_SYSTEM_ATTRIBUTE_{:08x}'.format(attr) return res - def getAttributeValueType(self, index): + def getAttributeValueType(self, index: int): """ Return the type of the attribute at the given index @@ -847,7 +1001,7 @@ def getAttributeValueType(self, index): offset = self._get_attribute_offset(index) return self.m_attributes[offset + ATTRIBUTE_IX_VALUE_TYPE] - def getAttributeValueData(self, index): + def getAttributeValueData(self, index: int): """ Return the data of the attribute at the given index @@ -877,7 +1031,9 @@ def getAttributeValue(self, index): return '' -def format_value(_type, _data, lookup_string=lambda ix: ""): +def format_value( + _type: int, _data: int, lookup_string=lambda ix: "" +) -> str: """ Format a value based on type and data. By default, no strings are looked up and "" is returned. @@ -891,10 +1047,10 @@ def format_value(_type, _data, lookup_string=lambda ix: ""): # Function to prepend android prefix for attributes/references from the # android library - fmt_package = lambda x: "android:" if x >> 24 == 1 else "" + def fmt_package(x): return "android:" if x >> 24 == 1 else "" # Function to represent integers - fmt_int = lambda x: (0x7FFFFFFF & x) - 0x80000000 if x > 0x7FFFFFFF else x + def fmt_int(x): return (0x7FFFFFFF & x) - 0x80000000 if x > 0x7FFFFFFF else x if _type == TYPE_STRING: return lookup_string(_data) @@ -917,10 +1073,15 @@ def format_value(_type, _data, lookup_string=lambda ix: ""): return "true" elif _type == TYPE_DIMENSION: - return "{:f}{}".format(complexToFloat(_data), DIMENSION_UNITS[_data & COMPLEX_UNIT_MASK]) + return "{:f}{}".format( + complexToFloat(_data), DIMENSION_UNITS[_data & COMPLEX_UNIT_MASK] + ) elif _type == TYPE_FRACTION: - return "{:f}{}".format(complexToFloat(_data) * 100, FRACTION_UNITS[_data & COMPLEX_UNIT_MASK]) + return "{:f}{}".format( + complexToFloat(_data) * 100, + FRACTION_UNITS[_data & COMPLEX_UNIT_MASK], + ) elif TYPE_FIRST_COLOR_INT <= _type <= TYPE_LAST_COLOR_INT: return "#%08X" % _data @@ -938,10 +1099,11 @@ class AXMLPrinter: A Reference Implementation can be found at http://androidxref.com/9.0.0_r3/xref/frameworks/base/tools/aapt/XMLNode.cpp """ + __charrange = None __replacement = None - def __init__(self, raw_buff): + def __init__(self, raw_buff: bytes) -> bytes: logger.debug("AXMLPrinter") self.axml = AXMLParser(raw_buff) @@ -955,6 +1117,9 @@ def __init__(self, raw_buff): logger.debug("DEBUG ARSC TYPE {}".format(_type)) if _type == START_TAG: + if not self.axml.name: # Check if the name is empty + logger.debug("Empty tag name, skipping to next element") + continue # Skip this iteration uri = self._print_namespace(self.axml.namespace) uri, name = self._fix_name(uri, self.axml.name) tag = "{}{}".format(uri, name) @@ -962,21 +1127,53 @@ def __init__(self, raw_buff): comment = self.axml.comment if comment: if self.root is None: - logger.warning("Can not attach comment with content '{}' without root!".format(comment)) + logger.warning( + "Can not attach comment with content '{}' without root!".format( + comment + ) + ) else: cur[-1].append(etree.Comment(comment)) - logger.debug("START_TAG: {} (line={})".format(tag, self.axml.m_lineNumber)) - elem = etree.Element(tag, nsmap=self.axml.nsmap) + logger.debug( + "START_TAG: {} (line={})".format( + tag, self.axml.m_lineNumber + ) + ) + + try: + elem = etree.Element(tag, nsmap=self.axml.nsmap) + except ValueError as e: + logger.error(e) + # nsmap= {' + + + + + + + +
+ {% include "base/nav.html" %} +
+
+
+
+
+
+
+
+

Forbidden

+

You do not have required permissions to perform this action.

+
+
+
+
+
+
+ + + + +
+ + + + diff --git a/mobsf/templates/404.html b/mobsf/templates/404.html new file mode 100644 index 0000000000..5ad301dca7 --- /dev/null +++ b/mobsf/templates/404.html @@ -0,0 +1,48 @@ +{% load static %} + + + + + + Not Found + + + + + + + + + + + + + +
+ {% include "base/nav.html" %} +
+
+
+
+
+
+
+
+

Not Found

+

We couldn't find the resource you are looking for.

+
+
+
+
+
+
+ + + +
+ + + + diff --git a/mobsf/templates/500.html b/mobsf/templates/500.html new file mode 100644 index 0000000000..c28aa0bb22 --- /dev/null +++ b/mobsf/templates/500.html @@ -0,0 +1,82 @@ +{% load static %} + + + + + + Internal Server Error + + + + + + + + + + + + + +
+ + + + + + +
+
+
+
+
+
+
+
+

Server Error (500)

+

This request caused an Internal Server error. Check the server logs for more details. For debugging, try setting DEBUG=True in settings.py

+
+
+
+
+
+
+ + + +
+ + + + diff --git a/mobsf/templates/auth/change_password.html b/mobsf/templates/auth/change_password.html new file mode 100644 index 0000000000..5b68940910 --- /dev/null +++ b/mobsf/templates/auth/change_password.html @@ -0,0 +1,81 @@ +{% extends "base/base_layout.html" %} +{% load static %} +{% block sidebar_option %} +sidebar-collapse +{% endblock %} +{% block content %} +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/mobsf/templates/auth/login.html b/mobsf/templates/auth/login.html new file mode 100644 index 0000000000..e4e4b747c7 --- /dev/null +++ b/mobsf/templates/auth/login.html @@ -0,0 +1,84 @@ +{% extends "base/base_layout.html" %} +{% load static %} +{% block sidebar_option %} +sidebar-collapse +{% endblock %} +{% block content %} +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/mobsf/templates/auth/register.html b/mobsf/templates/auth/register.html new file mode 100644 index 0000000000..3719546859 --- /dev/null +++ b/mobsf/templates/auth/register.html @@ -0,0 +1,107 @@ +{% extends "base/base_layout.html" %} +{% load static %} +{% block sidebar_option %} +sidebar-collapse +{% endblock %} +{% block content %} +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/mobsf/templates/auth/users.html b/mobsf/templates/auth/users.html new file mode 100644 index 0000000000..5f1ffa2557 --- /dev/null +++ b/mobsf/templates/auth/users.html @@ -0,0 +1,124 @@ +{% extends "base/base_layout.html" %} +{% load static %} +{% block sidebar_option %} +sidebar-collapse +{% endblock %} +{% block extra_css %} + +{% endblock %} +{% block content %} +
+
+
+
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+{% endblock %} +{% block extra_scripts %} + + +{% endblock %} \ No newline at end of file diff --git a/mobsf/templates/base/base_layout.html b/mobsf/templates/base/base_layout.html index d423d7446c..89ec10082b 100644 --- a/mobsf/templates/base/base_layout.html +++ b/mobsf/templates/base/base_layout.html @@ -22,46 +22,28 @@ - {% block extra_css %} + + {% block extra_css %} + {% endblock %}
- - - - +{% include "base/nav.html" %}
+ + @@ -104,6 +94,16 @@ diff --git a/mobsf/templates/base/list_href.html b/mobsf/templates/base/list_href.html new file mode 100644 index 0000000000..bebee28766 --- /dev/null +++ b/mobsf/templates/base/list_href.html @@ -0,0 +1,8 @@ +{% if list|length != 0 %} +
+ {% if list|length < limit %}Showing{% else %}Show{% endif %} all {{ list | length }} {{ type }} + {% for val in list %} + {{ val }}
+ {% endfor %} +
+{% endif %} \ No newline at end of file diff --git a/mobsf/templates/base/nav.html b/mobsf/templates/base/nav.html new file mode 100644 index 0000000000..166e5da888 --- /dev/null +++ b/mobsf/templates/base/nav.html @@ -0,0 +1,47 @@ + + + \ No newline at end of file diff --git a/mobsf/templates/dynamic_analysis/android/dynamic_analysis.html b/mobsf/templates/dynamic_analysis/android/dynamic_analysis.html index 6c0efcb035..15b3382cac 100644 --- a/mobsf/templates/dynamic_analysis/android/dynamic_analysis.html +++ b/mobsf/templates/dynamic_analysis/android/dynamic_analysis.html @@ -88,14 +88,14 @@

Android Runtime not found!

MobSF Dynamic Analyzer Supports

- • Genymotion Android VM version 4.1 - 11.0 (x86, upto API 30)
- • Android Emulator AVD (non production) version 5.0 - 9.0 (arm, arm64, x86, and x86_64 upto API 28)
+ • Genymotion Android VM version 4.1 - 11.0 (arm64, x86, and x86_64 upto API 30)
+ • Android Emulator AVD (non production) version 5.0 - 11.0 (arm, arm64, x86, and x86_64 upto API 30)
• Corellium Android VM (userdebug builds) version 7.1.2 - 11.0 (arm64 upto API 30)

{% if android_version %} - Recommended Android version is 9.0
- Detected Android Version: {{android_version}}, SDK: {{ android_sdk }}
+ Android version >= 9.0 recommended
+ Detected Android Version: {{android_version}}, SDK: API level {{ android_sdk }}
{% if android_sdk|floatformat > android_supported|floatformat %} - - - + + + +{% endblock %} \ No newline at end of file diff --git a/mobsf/templates/pdf/android_report.html b/mobsf/templates/pdf/android_report.html index a925260886..cfc2b4638e 100755 --- a/mobsf/templates/pdf/android_report.html +++ b/mobsf/templates/pdf/android_report.html @@ -29,7 +29,6 @@ font-display: swap; src: local('Oswald'), local('Oswald'), url('{{base_url}}{% static 'fonts/Oswald/Oswald-Regular.ttf' %}') format('truetype'); } - {% endif %} @@ -634,6 +633,7 @@

SHARED LIBRARY BINARY ANALYSIS

NO SHARED OBJECT NX + PIE STACK CANARY RELRO RPATH @@ -654,7 +654,11 @@

SHARED LIBRARY BINARY ANALYSIS


{{so.nx.severity}}
{{so.nx.description}} - {{so.stack_canary.has_canary}} + {{so.pie.is_pie}} +
+ {{so.pie.severity}} +
{{so.pie.description}} + {{so.stack_canary.has_canary}}
{{so.stack_canary.severity}}
{{so.stack_canary.description}} @@ -714,36 +718,74 @@

NIAP ANALYSIS v1.3

{% endfor %} - + + {% endif %} + + {% if behaviour %} +

BEHAVIOUR ANALYSIS

+ + + + + + + + + + + {% for rule, details in behaviour.items %} + + + + + + + {% endfor %} + +
RULE IDBEHAVIOURLABELFILES
{{ rule }} + {{ details.metadata.description }} + {% for lbl in details.metadata.label %} + {{ lbl }} + {% endfor %} + + {% for file_path in details.files %} + {{ file_path }} +
+ {% endfor %} +
+ {% endif %} + + {% if firebase_urls %} +

FIREBASE DATABASES ANALYSIS

+ + + + + + + + + + {% for find in firebase_urls %} + + + + + + {% endfor %} + +
TITLESEVERITYDESCRIPTION
{{ find.title }} + {% if find.severity == 'high' %} + high + {% elif find.severity == 'secure' %} + secure + {% elif find.severity == 'warning' %} + warning + {% elif find.severity == 'info' %} + info + {% endif %} + {{ find.description }}
{% endif %} - {% if malware_permissions %}

ABUSED PERMISSIONS

@@ -880,37 +922,6 @@

URLS

{% endif %} {% endif %} - {% if firebase_urls %} -

FIREBASE DATABASES

- - - - - - - - - {% for item in firebase_urls %} - - - - - {% endfor %} - -
FIREBASE URLDETAILS
- {{ item.url }} - - {% if item.open %} - high
Firebase DB is exposed publicly. - {% else %} - info
App talks to a Firebase Database. - {% endif %} - -
- {% endif %} - - - {% if emails %}

EMAILS

@@ -1051,7 +1062,38 @@
Description:

{% endif %} - +

SCAN LOGS

+
+ + + + + + + {% for log in logs %} + + + + + + {% endfor %} + +
TimestampEventError
+ {{log.timestamp}} + + {{log.status}} + + {% if not log.exception %} +

+ OK +

+ {% else %} +

+ {{log.exception}} +

+ {% endif %} +
+


diff --git a/mobsf/templates/pdf/ios_report.html b/mobsf/templates/pdf/ios_report.html index ce8f4219da..8d53af8f25 100644 --- a/mobsf/templates/pdf/ios_report.html +++ b/mobsf/templates/pdf/ios_report.html @@ -793,6 +793,37 @@
CVSS V2:
{% endif %} {% endif %} + {% if firebase_urls %} +

FIREBASE DATABASES ANALYSIS

+ + + + + + + + + + {% for find in firebase_urls %} + + + + + + {% endfor %} + +
TITLESEVERITYDESCRIPTION
{{ find.title }} + {% if find.severity == 'high' %} + high + {% elif find.severity == 'secure' %} + secure + {% elif find.severity == 'warning' %} + warning + {% elif find.severity == 'info' %} + info + {% endif %} + {{ find.description }}
+ {% endif %} {% if domains %}

OFAC SANCTIONED COUNTRIES

@@ -892,37 +923,6 @@

URLS

{% endif %} {% endif %} - {% if firebase_urls %} -

FIREBASE DATABASES

- - - - - - - - - {% for item in firebase_urls %} - - - - - {% endfor %} - -
FIREBASE URLDETAILS
- {{ item.url }} - - {% if item.open %} - high
Firebase DB is exposed publicly. - {% else %} - info
App talks to a Firebase Database. - {% endif %} - -
- {% endif %} - - - {% if emails %}

EMAILS

@@ -1065,6 +1065,37 @@
Description:

{% endif %} {% endif %} +

SCAN LOGS

+
+ + + + + + + {% for log in logs %} + + + + + + {% endfor %} + +
TimestampEventError
+ {{log.timestamp}} + + {{log.status}} + + {% if not log.exception %} +

+ OK +

+ {% else %} +

+ {{log.exception}} +

+ {% endif %} +

diff --git a/mobsf/templates/pdf/windows_report.html b/mobsf/templates/pdf/windows_report.html index 49a786eb72..870ee0f450 100644 --- a/mobsf/templates/pdf/windows_report.html +++ b/mobsf/templates/pdf/windows_report.html @@ -190,9 +190,39 @@

APPX BINARY ANALYSIS

{% endfor %} - -
+

SCAN LOGS

+ + + + + + + + {% for log in logs %} + + + + + + {% endfor %} + +
TimestampEventError
+ {{log.timestamp}} + + {{log.status}} + + {% if not log.exception %} +

+ OK +

+ {% else %} +

+ {{log.exception}} +

+ {% endif %} +
+


diff --git a/mobsf/templates/static_analysis/android_binary_analysis.html b/mobsf/templates/static_analysis/android_binary_analysis.html index 3c95452318..cc1aced3df 100755 --- a/mobsf/templates/static_analysis/android_binary_analysis.html +++ b/mobsf/templates/static_analysis/android_binary_analysis.html @@ -167,6 +167,12 @@ {% endif %} +
@@ -544,14 +558,14 @@

{{ activities | length }}

-

{{ services | length }}

+

{{ exported_count.exported_services }} / {{ services | length }}

-

SERVICES

+

EXPORTED SERVICES

- View + View All
@@ -559,14 +573,14 @@

{{ services | length }}

-

{{ receivers | length }}

+

{{ exported_count.exported_receivers }} / {{ receivers | length }}

-

RECEIVERS

+

EXPORTED RECEIVERS

- View + View All
@@ -574,74 +588,17 @@

{{ receivers | length }}

-

{{ providers | length }}

+

{{exported_count.exported_providers}} / {{ providers | length }}

-

PROVIDERS

+

EXPORTED PROVIDERS

- View + View All
-
-
- - -
- Exported
Activities
- - {{ exported_count.exported_activities }} - -
- -
- -
- -
-
- - -
- Exported
Services
- {{ exported_count.exported_services }} -
- -
- -
- - - -
- -
-
- - -
- Exported
Receivers
- {{ exported_count.exported_receivers }} -
- -
- -
- -
-
- - -
- Exported
Providers
- {{exported_count.exported_providers}} -
- -
- -
@@ -665,17 +622,19 @@

{{ providers | length }}

Rescan {% if app_type in 'so' %} - Download {{ app_type | upper}} + Download {{ app_type | upper}} {% endif %} {% if app_type not in 'so' %} - Manage Suppressions + Manage Suppressions {% endif %}

- {% if app_type not in 'jar,aar,so' %}

- Start Dynamic Analysis -

+ {% if app_type not in 'jar,aar,so' %} + Start Dynamic Analysis {% endif %} + +

+ @@ -700,7 +659,7 @@

{{ providers | length }}

{% if app_type not in 'jar,aar' %} Download Smali Code {% endif %} - Download {{ app_type | upper}} + Download {{ app_type | upper}}

@@ -1404,6 +1363,7 @@
{{ code_analysis.summary.suppressed }}
SHARED OBJECT {% endif %} NX + PIE STACK CANARY RELRO RPATH @@ -1428,6 +1388,10 @@
{{ code_analysis.summary.suppressed }}

{{so.nx.severity}}
{{so.nx.description}} + {{so.pie.is_pie}} +
+ {{so.pie.severity}} +
{{so.pie.description}} {{so.stack_canary.has_canary}}
{{so.stack_canary.severity}} @@ -1561,7 +1525,105 @@
{{ code_analysis.summary.suppressed }}
- + {% endif %} + +
+
+
+
+
+
+

+ FIREBASE DATABASE ANALYSIS +

+
+ + + + + + + + + + {% for find in firebase_urls %} + + + + + + {% endfor %} + +
TITLESEVERITYDESCRIPTION
{{ find.title }} + {% if find.severity == 'high' %} + high + {% elif find.severity == 'secure' %} + secure + {% elif find.severity == 'warning' %} + warning + {% elif find.severity == 'info' %} + info + {% endif %} + {{ find.description }}
+
+
+
+
+ +
+
+
+ + + +
+
+
+
+
+
+

+ MALWARE LOOKUP +

+
+
+ + +
+ +
+ + + + +
+
+
+
+ +
+
+
+ + {% if app_type not in 'so' %} +
@@ -1647,7 +1709,7 @@
{{ code_analysis.summary.suppressed }}
- +
@@ -1655,34 +1717,56 @@
{{ code_analysis.summary.suppressed }}

- QUARK ANALYSIS + BEHAVIOUR ANALYSIS

- - - - - - - - - {% if quark %} - {% for item in quark %} - - - - - {% endfor%} - {% endif %} - - -
POTENTIAL MALICIOUS BEHAVIOUREVIDENCE
{{ item.crime }} - {% for api in item.register %} - {{api.file}} -> {{api.method}} -
- {% endfor %} -
+ + + + + + + + + + + {% for rule, details in behaviour.items %} + + + + + + + {% endfor %} + +
RULE IDBEHAVIOURLABELFILES
{{ rule }} + {{ details.metadata.description }} + {% for lbl in details.metadata.label %} + {{ lbl }} + {% endfor %} + + {% if details.files|length < 4 %} + {% for file_path, lines in details.files.items %} + + {{ file_path }} + +
+ {% endfor %} + {% else %} + +
+ {% for file_path, lines in details.files.items %} + + {{ file_path }} + +
+ {% endfor %} +
+ {% endif %} +
@@ -1691,7 +1775,7 @@
{{ code_analysis.summary.suppressed }}
- + {% endif %} {% if virus_total %} @@ -1742,8 +1826,8 @@
{{ code_analysis.summary.suppressed }}
-{% endif %} +{% endif %}
@@ -1968,53 +2052,6 @@
{{ code_analysis.summary.suppressed }}
- -
-
-
-
-
-
-

- FIREBASE DATABASE -

-
- {% if firebase_urls %} - - - - - - - - - - {% for item in firebase_urls %} - - - - - {% endfor %} - -
FIREBASE URLDETAILS
- {{ item.url }} - - {% if item.open %} - high
Firebase Database is exposed publicly. - {% else %} - info
App talks to a Firebase database. - {% endif %} -
- {% endif %} -
-
-
-
- -
-
-
-
@@ -2204,7 +2241,7 @@
{{ code_analysis.summary.suppressed }}

- {% include 'base/list.html' with list=activities type="activities" limit=50 %} + {% include 'base/list_href.html' with list=activities type="activities" limit=50 %}

@@ -2227,7 +2264,7 @@
{{ code_analysis.summary.suppressed }}

- {% include 'base/list.html' with list=services type="services" limit=50 %} + {% include 'base/list_href.html' with list=services type="services" limit=50 %}

@@ -2250,7 +2287,7 @@
{{ code_analysis.summary.suppressed }}

- {% include 'base/list.html' with list=receivers type="receivers" limit=50 %} + {% include 'base/list_href.html' with list=receivers type="receivers" limit=50 %}

@@ -2274,7 +2311,7 @@
{{ code_analysis.summary.suppressed }}

- {% include 'base/list.html' with list=providers type="providers" limit=50 %} + {% include 'base/list_href.html' with list=providers type="providers" limit=50 %}

@@ -2306,6 +2343,30 @@
{{ code_analysis.summary.suppressed }}
+ +
+
+
+
+
+
+

+ SBOM +

+
+ {% if sbom %} + {% include 'base/list.html' with list=sbom.sbom_versioned type="Versioned Packages" limit=100 %} + {% include 'base/list.html' with list=sbom.sbom_packages type="Packages" limit=100 %} + {% endif %} +
+
+
+
+
+ +
+
+
@@ -2494,3 +2555,38 @@ {% endblock %} +{% block scan_logs %} + + + + + + + + + + {% for log in logs %} + + + + + + {% endfor %} + +
TimestampEventError
+ {{log.timestamp}} + + {{log.status}} + + {% if not log.exception %} +

+ OK +

+ {% else %} +

+ {{log.exception}} +

+ {% endif %} +
+ +{% endblock %} \ No newline at end of file diff --git a/mobsf/templates/static_analysis/android_source_analysis.html b/mobsf/templates/static_analysis/android_source_analysis.html index ec093128f5..c9f06cd67e 100755 --- a/mobsf/templates/static_analysis/android_source_analysis.html +++ b/mobsf/templates/static_analysis/android_source_analysis.html @@ -119,6 +119,12 @@

File Analysis

+
-
-
-
- -
-
-
-
- -
- -
-
-

{{ activities | length }}

+
+
+
-

ACTIVITIES

-
-
- -
- View -
-
- -
- -
-
-

{{ services | length }}

+
+
+
+
+ + - -
- - - -
- -
-
-

{{ providers | length }}

+
+ +
+ +
+
+

{{ exported_count.exported_services }} / {{ services | length }}

-

PROVIDERS

-
-
- -
- View +

EXPORTED SERVICES

- -
-
-
- - -
- Exported
Activities
- - {{ exported_count.exported_activities }} - -
- +
+
- + View All
- -
-
- +
+ +
+ +
+
+

{{ exported_count.exported_receivers }} / {{ receivers | length }}

-
- Exported
Services
- {{ exported_count.exported_services }} -
- +

EXPORTED RECEIVERS

- -
- - - -
- -
-
- - -
- Exported
Receivers
- {{ exported_count.exported_receivers}} -
- +
+
- + View All
- -
-
- +
+ +
+ +
+
+

{{exported_count.exported_providers}} / {{ providers | length }}

-
- Exported
Providers
- {{ exported_count.exported_providers }} -
- +

EXPORTED PROVIDERS

- +
+ +
+ View All
-
-
+ +
+
-
+
-
-
+ + + @@ -543,9 +498,11 @@

{{ providers | length }}

Rescan - Manage Suppressions + Manage Suppressions View AndroidManifest.xml View Source + + Download ZIP

@@ -1217,6 +1174,122 @@
{{ code_analysis.summary.suppressed }}
+ +
+
+
+
+
+
+

+ FIREBASE DATABASE ANALYSIS +

+
+ + + + + + + + + + {% for find in firebase_urls %} + + + + + + {% endfor %} + +
TITLESEVERITYDESCRIPTION
{{ find.title }} + {% if find.severity == 'high' %} + high + {% elif find.severity == 'secure' %} + secure + {% elif find.severity == 'warning' %} + warning + {% elif find.severity == 'info' %} + info + {% endif %} + {{ find.description }}
+
+
+
+
+ +
+
+
+ + +
+
+
+
+
+
+

+ BEHAVIOUR ANALYSIS +

+
+ + + + + + + + + + + + {% for rule, details in behaviour.items %} + + + + + + + {% endfor %} + +
RULE IDBEHAVIOURLABELFILES
{{ rule }} + {{ details.metadata.description }} + {% for lbl in details.metadata.label %} + {{ lbl }} + {% endfor %} + + {% if details.files|length < 4 %} + {% for file_path, lines in details.files.items %} + + {{ file_path }} + +
+ {% endfor %} + {% else %} + +
+ {% for file_path, lines in details.files.items %} + + {{ file_path }} + +
+ {% endfor %} +
+ {% endif %} +
+
+
+
+
+ +
+
+
+ +
@@ -1439,53 +1512,6 @@
{{ code_analysis.summary.suppressed }}
- -
-
-
-
-
-
-

- FIREBASE DATABASE -

-
- {% if firebase_urls %} - - - - - - - - - - {% for item in firebase_urls %} - - - - - {% endfor %} - -
FIREBASE URLDETAILS
- {{ item.url }} - - {% if item.open %} - high
Firebase Database is exposed publicly. - {% else %} - info
App talks to a Firebase database. - {% endif %} -
- {% endif %} -
-
-
-
- -
-
-
-
@@ -1725,6 +1751,30 @@
{{ code_analysis.summary.suppressed }}
+ +
+
+
+
+
+
+

+ SBOM +

+
+ {% if sbom %} + {% include 'base/list.html' with list=sbom.sbom_versioned type="Versioned Packages" limit=100 %} + {% include 'base/list.html' with list=sbom.sbom_packages type="Packages" limit=100 %} + {% endif %} +
+
+
+
+
+ +
+
+
@@ -1892,3 +1942,38 @@ {% endblock %} +{% block scan_logs %} + + + + + + + + + + {% for log in logs %} + + + + + + {% endfor %} + +
TimestampEventError
+ {{log.timestamp}} + + {{log.status}} + + {% if not log.exception %} +

+ OK +

+ {% else %} +

+ {{log.exception}} +

+ {% endif %} +
+ +{% endblock %} \ No newline at end of file diff --git a/mobsf/templates/static_analysis/ios_binary_analysis.html b/mobsf/templates/static_analysis/ios_binary_analysis.html index 73d3f7d240..6a3d690fee 100755 --- a/mobsf/templates/static_analysis/ios_binary_analysis.html +++ b/mobsf/templates/static_analysis/ios_binary_analysis.html @@ -141,6 +141,12 @@ {% endif %} +
@@ -463,7 +470,7 @@
{% if app_type not in 'Dylib,A' %} View Class Dump {% endif %} - Download {% if app_type in 'Dylib' %}DYLIB{% elif app_type in 'A' %}A{% else %}IPA{% endif %} + Download {% if app_type in 'Dylib' %}DYLIB{% elif app_type in 'A' %}A{% else %}IPA{% endif %}

@@ -1231,6 +1238,102 @@
{{ binary_analysis.summary.suppressed }}
{% endif %} + +
+
+
+
+
+
+

+ FIREBASE DATABASE ANALYSIS +

+
+ + + + + + + + + + {% for find in firebase_urls %} + + + + + + {% endfor %} + +
TITLESEVERITYDESCRIPTION
{{ find.title }} + {% if find.severity == 'high' %} + high + {% elif find.severity == 'secure' %} + secure + {% elif find.severity == 'warning' %} + warning + {% elif find.severity == 'info' %} + info + {% endif %} + {{ find.description }}
+
+
+
+
+ +
+
+
+ + + +
+
+
+
+
+
+

+ MALWARE LOOKUP +

+
+
+ + +
+ +
+ + + + +
+
+
+
+ +
+
+
+ {% if virus_total %}
@@ -1446,53 +1549,6 @@
{{ binary_analysis.summary.suppressed }}
- -
-
-
-
-
-
-

- FIREBASE DATABASE -

-
- {% if firebase_urls %} - - - - - - - - - - {% for item in firebase_urls %} - - - - - {% endfor %} - -
FIREBASE URLDETAILS
- {{ item.url }} - - {% if item.open %} - high
Firebase Database is exposed publicly. - {% else %} - info
App talks to a Firebase database. - {% endif %} -
- {% endif %} -
-
-
-
- -
-
-
-
@@ -1863,3 +1919,38 @@ ]; {% endblock %} +{% block scan_logs %} + + + + + + + + + + {% for log in logs %} + + + + + + {% endfor %} + +
TimestampEventError
+ {{log.timestamp}} + + {{log.status}} + + {% if not log.exception %} +

+ OK +

+ {% else %} +

+ {{log.exception}} +

+ {% endif %} +
+ +{% endblock %} \ No newline at end of file diff --git a/mobsf/templates/static_analysis/ios_source_analysis.html b/mobsf/templates/static_analysis/ios_source_analysis.html index c831ef83a4..80c6057d89 100755 --- a/mobsf/templates/static_analysis/ios_source_analysis.html +++ b/mobsf/templates/static_analysis/ios_source_analysis.html @@ -7,7 +7,6 @@ -