mirror of
https://github.com/mvt-project/mvt.git
synced 2026-02-15 10:02:43 +00:00
Compare commits
53 Commits
v2.4.3
...
wip/androi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d873f14dd | ||
|
|
524bfcf649 | ||
|
|
b58351bfbd | ||
|
|
efe46d7b49 | ||
|
|
102dd31bd6 | ||
|
|
caeeec2816 | ||
|
|
9e19abb5d3 | ||
|
|
cf5cf3b85d | ||
|
|
f0dbe0bfa6 | ||
|
|
555e49fda7 | ||
|
|
a6d32e1c88 | ||
|
|
f155146f1e | ||
|
|
9d47acc228 | ||
|
|
cbd41b2aff | ||
|
|
0509eaa162 | ||
|
|
59e6dff1e1 | ||
|
|
f1821d1a02 | ||
|
|
6c7ad0ac95 | ||
|
|
3a997d30d2 | ||
|
|
6f56939dd7 | ||
|
|
7a4946e2c6 | ||
|
|
e1c4f4eb7a | ||
|
|
f9d7b550dc | ||
|
|
b738603911 | ||
|
|
5826e6b11c | ||
|
|
54c5d549af | ||
|
|
dded863e58 | ||
|
|
fc7ea5383e | ||
|
|
04b78a4d60 | ||
|
|
4ea53d707b | ||
|
|
da743a2878 | ||
|
|
4681b57adc | ||
|
|
bb7a22ed0b | ||
|
|
b2df17b4a0 | ||
|
|
278611a753 | ||
|
|
cd4d468553 | ||
|
|
1182587094 | ||
|
|
ad3bc3470e | ||
|
|
2c5ae696b1 | ||
|
|
5d2ff32e3a | ||
|
|
2838bac63f | ||
|
|
b7df87a62f | ||
|
|
013282dbba | ||
|
|
ab33789f06 | ||
|
|
a1571c127d | ||
|
|
61f33f7ecb | ||
|
|
4a6b483ce3 | ||
|
|
101098cbb7 | ||
|
|
fd3ef76873 | ||
|
|
fb52f73556 | ||
|
|
acc950377f | ||
|
|
c8a0327768 | ||
|
|
1d075abde9 |
19
.github/workflows/add-issue-to-project.yml
vendored
Normal file
19
.github/workflows/add-issue-to-project.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Add issue to project
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
|
||||
jobs:
|
||||
add-to-project:
|
||||
name: Add issue to project
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/add-to-project@v0.5.0
|
||||
with:
|
||||
# You can target a project in a different organization
|
||||
# to the issue
|
||||
project-url: https://github.com/orgs/mvt-project/projects/1
|
||||
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
|
||||
7
.github/workflows/python-package.yml
vendored
7
.github/workflows/python-package.yml
vendored
@@ -40,10 +40,13 @@ jobs:
|
||||
- name: Safety checks
|
||||
run: safety check
|
||||
- name: Test with pytest and coverage
|
||||
run: pytest --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=mvt tests/ | tee pytest-coverage.txt
|
||||
run: |
|
||||
set -o pipefail
|
||||
pytest --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=mvt tests/ | tee pytest-coverage.txt
|
||||
- name: Pytest coverage comment
|
||||
continue-on-error: true # Workflows running on a fork can't post comments
|
||||
uses: MishaKav/pytest-coverage-comment@main
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
pytest-coverage-path: ./pytest-coverage.txt
|
||||
junitxml-path: ./pytest.xml
|
||||
junitxml-path: ./pytest.xml
|
||||
|
||||
11
.safety-policy.yml
Normal file
11
.safety-policy.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
# Safety Security and License Configuration file
|
||||
# We recommend checking this file into your source control in the root of your Python project
|
||||
# If this file is named .safety-policy.yml and is in the same directory where you run `safety check` it will be used by default.
|
||||
# Otherwise, you can use the flag `safety check --policy-file <path-to-this-file>` to specify a custom location and name for the file.
|
||||
# To validate and review your policy file, run the validate command: `safety validate policy_file --path <path-to-this-file>`
|
||||
security: # configuration for the `safety check` command
|
||||
ignore-vulnerabilities: # Here you can list multiple specific vulnerabilities you want to ignore (optionally for a time period)
|
||||
67599: # Example vulnerability ID
|
||||
reason: disputed, inapplicable
|
||||
70612:
|
||||
reason: disputed, inapplicable
|
||||
@@ -57,12 +57,12 @@ RUN git clone https://github.com/libimobiledevice/libplist \
|
||||
|
||||
# Installing MVT
|
||||
# --------------
|
||||
RUN pip3 install mvt
|
||||
RUN pip3 install git+https://github.com/mvt-project/mvt.git@main
|
||||
|
||||
# Installing ABE
|
||||
# --------------
|
||||
RUN mkdir /opt/abe \
|
||||
&& wget https://github.com/nelenkov/android-backup-extractor/releases/download/20210709062403-4c55371/abe.jar -O /opt/abe/abe.jar \
|
||||
&& wget https://github.com/nelenkov/android-backup-extractor/releases/download/master-20221109063121-8fdfc5e/abe.jar -O /opt/abe/abe.jar \
|
||||
# Create alias for abe
|
||||
&& echo 'alias abe="java -jar /opt/abe/abe.jar"' >> ~/.bashrc
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ MVT supports using public [indicators of compromise (IOCs)](https://github.com/m
|
||||
>
|
||||
> Reliable and comprehensive digital forensic support and triage requires access to non-public indicators, research and threat intelligence.
|
||||
>
|
||||
>Such support is available to civil society through [Amnesty International's Security Lab](https://www.amnesty.org/en/tech/) or through our forensic partnership with [Access Now’s Digital Security Helpline](https://www.accessnow.org/help/).
|
||||
>Such support is available to civil society through [Amnesty International's Security Lab](https://securitylab.amnesty.org/get-help/?c=mvt_docs) or through our forensic partnership with [Access Now’s Digital Security Helpline](https://www.accessnow.org/help/).
|
||||
|
||||
More information about using indicators of compromise with MVT is available in the [documentation](https://docs.mvt.re/en/latest/iocs/).
|
||||
|
||||
|
||||
@@ -7,11 +7,27 @@ Before proceeding, please note that MVT requires Python 3.6+ to run. While it sh
|
||||
First install some basic dependencies that will be necessary to build all required tools:
|
||||
|
||||
```bash
|
||||
sudo apt install python3 python3-pip libusb-1.0-0 sqlite3
|
||||
sudo apt install python3 python3-venv python3-pip sqlite3 libusb-1.0-0
|
||||
```
|
||||
|
||||
*libusb-1.0-0* is not required if you intend to only use `mvt-ios` and not `mvt-android`.
|
||||
|
||||
(Recommended) Set up `pipx`
|
||||
|
||||
For Ubuntu 23.04 or above:
|
||||
```bash
|
||||
sudo apt install pipx
|
||||
pipx ensurepath
|
||||
```
|
||||
|
||||
For Ubuntu 22.04 or below:
|
||||
```
|
||||
python3 -m pip install --user pipx
|
||||
python3 -m pipx ensurepath
|
||||
```
|
||||
|
||||
Other distributions: check for a `pipx` or `python-pipx` via your package manager.
|
||||
|
||||
When working with Android devices you should additionally install [Android SDK Platform Tools](https://developer.android.com/studio/releases/platform-tools). If you prefer to install a package made available by your distribution of choice, please make sure the version is recent to ensure compatibility with modern Android devices.
|
||||
|
||||
## Dependencies on macOS
|
||||
@@ -21,7 +37,7 @@ Running MVT on macOS requires Xcode and [homebrew](https://brew.sh) to be instal
|
||||
In order to install dependencies use:
|
||||
|
||||
```bash
|
||||
brew install python3 libusb sqlite3
|
||||
brew install python3 pipx libusb sqlite3
|
||||
```
|
||||
|
||||
*libusb* is not required if you intend to only use `mvt-ios` and not `mvt-android`.
|
||||
@@ -42,24 +58,43 @@ It is recommended to try installing and running MVT from [Windows Subsystem Linu
|
||||
|
||||
## Installing MVT
|
||||
|
||||
If you haven't done so, you can add this to your `.bashrc` or `.zshrc` file in order to add locally installed PyPI binaries to your `$PATH`:
|
||||
### Installing from PyPI with pipx (recommended)
|
||||
1. Install `pipx` following the instructions above for your OS/distribution. Make sure to run `pipx ensurepath` and open a new terminal window.
|
||||
2. ```bash
|
||||
pipx install mvt
|
||||
```
|
||||
|
||||
You now should have the `mvt-ios` and `mvt-android` utilities installed. If you run into problems with these commands not being found, ensure you have run `pipx ensurepath` and opened a new terminal window.
|
||||
|
||||
### Installing from PyPI directly into a virtual environment
|
||||
You can use `pipenv`, `poetry` etc. for your virtual environment, but the provided example is with the built-in `venv` tool:
|
||||
|
||||
1. Create the virtual environment in a folder in the current directory named `env`:
|
||||
```bash
|
||||
export PATH=$PATH:~/.local/bin
|
||||
python3 -m venv env
|
||||
```
|
||||
|
||||
Then you can install MVT directly from [PyPI](https://pypi.org/project/mvt/)
|
||||
|
||||
2. Activate the virtual environment:
|
||||
```bash
|
||||
pip3 install mvt
|
||||
source env/bin/activate
|
||||
```
|
||||
|
||||
If you want to have the latest features in development, you can install MVT directly from the source code. If you installed MVT previously from pypi, you should first uninstall it using `pip3 uninstall mvt` and then install from the source code:
|
||||
3. Install `mvt` into the virtual environment:
|
||||
```bash
|
||||
pip install mvt
|
||||
```
|
||||
|
||||
The `mvt-ios` and `mvt-android` utilities should now be available as commands whenever the virtual environment is active.
|
||||
|
||||
### Installing from git source with pipx
|
||||
If you want to have the latest features in development, you can install MVT directly from the source code in git.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mvt-project/mvt.git
|
||||
cd mvt
|
||||
pip3 install .
|
||||
pipx install --force git+https://github.com/mvt-project/mvt.git
|
||||
```
|
||||
|
||||
You now should have the `mvt-ios` and `mvt-android` utilities installed.
|
||||
|
||||
**Notes:**
|
||||
1. The `--force` flag is necessary to force the reinstallation of the package.
|
||||
2. To revert to using a PyPI version, it will be necessary to `pipx uninstall mvt` first.
|
||||
|
||||
@@ -21,7 +21,7 @@ MVT supports using [indicators of compromise (IOCs)](https://github.com/mvt-proj
|
||||
|
||||
Reliable and comprehensive digital forensic support and triage requires access to non-public indicators, research and threat intelligence.
|
||||
|
||||
Such support is available to civil society through [Amnesty International's Security Lab](https://securitylab.amnesty.org/contact-us/) or [Access Now’s Digital Security Helpline](https://www.accessnow.org/help/).
|
||||
Such support is available to civil society through [Amnesty International's Security Lab](https://securitylab.amnesty.org/get-help/?c=mvt_docs) or [Access Now’s Digital Security Helpline](https://www.accessnow.org/help/).
|
||||
|
||||
More information about using indicators of compromise with MVT is available in the [documentation](iocs.md).
|
||||
|
||||
|
||||
@@ -45,10 +45,10 @@ Once the idevice tools are available you can check if everything works fine by c
|
||||
ideviceinfo
|
||||
```
|
||||
|
||||
This should some many details on the connected iOS device. If you are connecting the device to your laptop for the first time, it will require to unlock and enter the PIN code on the mobile device. If it complains that no device is connected and the mobile device is indeed plugged in through the USB cable, you might need to do this first, although typically the pairing is automatically done when connecting the device:
|
||||
This should show many details on the connected iOS device. If you are connecting the device to your laptop for the first time, it will require to unlock and enter the PIN code on the mobile device. If it complains that no device is connected and the mobile device is indeed plugged in through the USB cable, you might need to do this first, although typically the pairing is automatically done when connecting the device:
|
||||
|
||||
```bash
|
||||
sudo usbmuxd -f -d
|
||||
sudo usbmuxd -f -v
|
||||
idevicepair pair
|
||||
```
|
||||
|
||||
|
||||
165
mvt/android/artifacts/dumpstate_artifact.py
Normal file
165
mvt/android/artifacts/dumpstate_artifact.py
Normal file
@@ -0,0 +1,165 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2023 The MVT Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
import re
|
||||
|
||||
from .artifact import AndroidArtifact
|
||||
|
||||
|
||||
# The AOSP dumpstate code is available at https://cs.android.com/android/platform/superproject/+/master:frameworks/native/cmds/dumpstate/
|
||||
# The dumpstate code is used to generate bugreports on Android devices. It looks like there are
|
||||
# bugs in the code that leave some sections with out ending lines. We need to handle these cases.
|
||||
#
|
||||
# The approach here is to flag probably broken section, and to search for plausible new section headers
|
||||
# to close the previous section. This is a heuristic approach, and may not work in all cases. We can't do
|
||||
# this for all sections as we will detect subsections as new sections.
|
||||
SECTION_BROKEN_TERMINATORS = [
|
||||
b"VM TRACES AT LAST ANR",
|
||||
b"DIGITAL_HALL",
|
||||
]
|
||||
|
||||
|
||||
class DumpStateArtifact(AndroidArtifact):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.dumpstate_sections = []
|
||||
self.dumpstate_header = {}
|
||||
self.unparsed_lines = []
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _parse_dumpstate_header(self, header_text):
|
||||
"""
|
||||
Parse dumpstate header metadata
|
||||
"""
|
||||
fields = {}
|
||||
for line in header_text.splitlines():
|
||||
if line.startswith(b"="):
|
||||
continue
|
||||
|
||||
if b":" in line:
|
||||
# Save line if it's a key-value pair.
|
||||
key, value = line.split(b":", 1)
|
||||
fields[key] = value[1:]
|
||||
|
||||
if not line and fields:
|
||||
# Finish if we get an empty line and already parsed lines
|
||||
break
|
||||
else:
|
||||
# Skip until we find lines
|
||||
continue
|
||||
|
||||
self.dumpstate_header = fields
|
||||
return fields
|
||||
|
||||
def _get_section_header(self, header_match):
|
||||
"""
|
||||
Create internal dictionary to track dumpsys section.
|
||||
"""
|
||||
section_full = header_match.group(0).strip(b"-").strip()
|
||||
section_name = header_match.group(1).rstrip()
|
||||
|
||||
if header_match.group(2):
|
||||
section_command = header_match.group(2).strip(b"()")
|
||||
else:
|
||||
# Some headers can missing the command
|
||||
section_command = ""
|
||||
|
||||
has_broken_terminator = False
|
||||
for broken_section in SECTION_BROKEN_TERMINATORS:
|
||||
if broken_section in section_name:
|
||||
has_broken_terminator = True
|
||||
break
|
||||
|
||||
section = {
|
||||
"section_name": section_name,
|
||||
"section_command": section_command,
|
||||
"section_full": section_full,
|
||||
"missing_terminator": has_broken_terminator,
|
||||
"lines": [],
|
||||
"error": False,
|
||||
}
|
||||
self.dumpstate_sections.append(section)
|
||||
return section
|
||||
|
||||
def parse_dumpstate(self, text: str) -> list:
|
||||
"""
|
||||
Extract all sections from a full dumpstate file.
|
||||
|
||||
:param text: content of the full dumpstate file (string)
|
||||
"""
|
||||
# Parse the header
|
||||
self._parse_dumpstate_header(text)
|
||||
|
||||
header = b"------ "
|
||||
|
||||
# Regexes to parse headers
|
||||
section_name_re = re.compile(rb"------ ([\w\d\s\-\/\&]+)(\(.*\))? ------")
|
||||
end_of_section_re = re.compile(rb"------ End of .* ------")
|
||||
missing_file_error_re = re.compile(rb"\*\*\* (.*): No such file or directory")
|
||||
generic_error_re = re.compile(rb"\*\*\* (.*) (?<!\*\*\*)$")
|
||||
|
||||
section = None
|
||||
|
||||
# Parse each line in dumpstate and look for headers
|
||||
for line in text.splitlines():
|
||||
if not section:
|
||||
# If we find an end section when not in a section, we can skip
|
||||
# It's probably the trailing line of a section.
|
||||
end_of_section_match = re.match(end_of_section_re, line)
|
||||
if end_of_section_match:
|
||||
self.unparsed_lines.append(line)
|
||||
continue
|
||||
|
||||
possible_section_header = re.match(section_name_re, line)
|
||||
if possible_section_header:
|
||||
section = self._get_section_header(possible_section_header)
|
||||
# print("found section", section)
|
||||
continue
|
||||
else:
|
||||
# We continue to next line as we weren't already in a section
|
||||
self.unparsed_lines.append(line)
|
||||
continue
|
||||
|
||||
if line.lstrip().startswith(header):
|
||||
# This may be an internal section, or the terminator for our current section
|
||||
# Ending looks like: ------ 0.557s was the duration of 'DUMPSYS CRITICAL' ------
|
||||
|
||||
# Check that we have the end for the right command.
|
||||
section_command_in_quotes = b"'" + section["section_name"] + b"'"
|
||||
if (
|
||||
section_command_in_quotes in line
|
||||
or section["section_full"]
|
||||
in line # Needed for 0.070s was the duration of 'KERNEL LOG (dmesg)'
|
||||
):
|
||||
# Add end line and finish up the section
|
||||
section["lines"].append(line)
|
||||
section = None
|
||||
continue
|
||||
|
||||
# If we haven't closed previous, but this matches a section header, we can try close.
|
||||
# Probably a bug where not closed properly. We explicitly flag known broken fields.
|
||||
|
||||
# This fails on these blocks if we dont blacklist. Maybe we need to make a blacklist of badly closed items
|
||||
# ------ DUMP BLOCK STAT ------
|
||||
# ------ BLOCK STAT (/sys/block/dm-20) ------
|
||||
|
||||
possible_section_header = re.match(section_name_re, line)
|
||||
if possible_section_header and section["missing_terminator"]:
|
||||
section = self._get_section_header(possible_section_header)
|
||||
else:
|
||||
# Probably terminator for subsection, ignore and treat as a regular line.
|
||||
pass
|
||||
|
||||
# Handle lines with special meaning
|
||||
# TODO: This is failing as sometime errors are followed by a terminator and sometimes not.
|
||||
if re.match(missing_file_error_re, line) or re.match(
|
||||
generic_error_re, line
|
||||
):
|
||||
# The line in a failed file read which is dumped without an header end section.
|
||||
section["failed"] = True
|
||||
section["lines"].append(line)
|
||||
section = None
|
||||
else:
|
||||
section["lines"].append(line)
|
||||
|
||||
return self.dumpstate_sections
|
||||
@@ -4,6 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
from .artifact import AndroidArtifact
|
||||
import re
|
||||
|
||||
|
||||
class DumpsysAccessibilityArtifact(AndroidArtifact):
|
||||
@@ -25,6 +26,8 @@ class DumpsysAccessibilityArtifact(AndroidArtifact):
|
||||
|
||||
:param content: content of the accessibility section (string)
|
||||
"""
|
||||
|
||||
# "Old" syntax
|
||||
in_services = False
|
||||
for line in content.splitlines():
|
||||
if line.strip().startswith("installed services:"):
|
||||
@@ -35,6 +38,7 @@ class DumpsysAccessibilityArtifact(AndroidArtifact):
|
||||
continue
|
||||
|
||||
if line.strip() == "}":
|
||||
# At end of installed services
|
||||
break
|
||||
|
||||
service = line.split(":")[1].strip()
|
||||
@@ -45,3 +49,19 @@ class DumpsysAccessibilityArtifact(AndroidArtifact):
|
||||
"service": service,
|
||||
}
|
||||
)
|
||||
|
||||
# "New" syntax - AOSP >= 14 (?)
|
||||
# Looks like:
|
||||
# Enabled services:{{com.azure.authenticator/com.microsoft.brooklyn.module.accessibility.BrooklynAccessibilityService}, {com.agilebits.onepassword/com.agilebits.onepassword.filling.accessibility.FillingAccessibilityService}}
|
||||
|
||||
for line in content.splitlines():
|
||||
if line.strip().startswith("Enabled services:"):
|
||||
matches = re.finditer(r"{([^{]+?)}", line)
|
||||
|
||||
for match in matches:
|
||||
# Each match is in format: <package_name>/<service>
|
||||
package_name, _, service = match.group(1).partition("/")
|
||||
|
||||
self.results.append(
|
||||
{"package_name": package_name, "service": service}
|
||||
)
|
||||
|
||||
203
mvt/android/artifacts/dumpsys_packages.py
Normal file
203
mvt/android/artifacts/dumpsys_packages.py
Normal file
@@ -0,0 +1,203 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2023 The MVT Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import re
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
from mvt.android.utils import ROOT_PACKAGES
|
||||
|
||||
from .artifact import AndroidArtifact
|
||||
|
||||
|
||||
class DumpsysPackagesArtifact(AndroidArtifact):
|
||||
def check_indicators(self) -> None:
|
||||
for result in self.results:
|
||||
if result["package_name"] in ROOT_PACKAGES:
|
||||
self.log.warning(
|
||||
"Found an installed package related to "
|
||||
'rooting/jailbreaking: "%s"',
|
||||
result["package_name"],
|
||||
)
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
if not self.indicators:
|
||||
continue
|
||||
|
||||
ioc = self.indicators.check_app_id(result.get("package_name", ""))
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
def serialize(self, record: dict) -> Union[dict, list]:
|
||||
records = []
|
||||
|
||||
timestamps = [
|
||||
{"event": "package_install", "timestamp": record["timestamp"]},
|
||||
{
|
||||
"event": "package_first_install",
|
||||
"timestamp": record["first_install_time"],
|
||||
},
|
||||
{"event": "package_last_update", "timestamp": record["last_update_time"]},
|
||||
]
|
||||
|
||||
for timestamp in timestamps:
|
||||
records.append(
|
||||
{
|
||||
"timestamp": timestamp["timestamp"],
|
||||
"module": self.__class__.__name__,
|
||||
"event": timestamp["event"],
|
||||
"data": f"Install or update of package {record['package_name']}",
|
||||
}
|
||||
)
|
||||
|
||||
return records
|
||||
|
||||
@staticmethod
|
||||
def parse_dumpsys_package_for_details(output: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Parse one entry of a dumpsys package information
|
||||
"""
|
||||
details = {
|
||||
"uid": "",
|
||||
"version_name": "",
|
||||
"version_code": "",
|
||||
"timestamp": "",
|
||||
"first_install_time": "",
|
||||
"last_update_time": "",
|
||||
"permissions": [],
|
||||
"requested_permissions": [],
|
||||
}
|
||||
in_install_permissions = False
|
||||
in_runtime_permissions = False
|
||||
in_declared_permissions = False
|
||||
in_requested_permissions = True
|
||||
for line in output.splitlines():
|
||||
if in_install_permissions:
|
||||
if line.startswith(" " * 4) and not line.startswith(" " * 6):
|
||||
in_install_permissions = False
|
||||
else:
|
||||
lineinfo = line.strip().split(":")
|
||||
permission = lineinfo[0]
|
||||
granted = None
|
||||
if "granted=" in lineinfo[1]:
|
||||
granted = "granted=true" in lineinfo[1]
|
||||
|
||||
details["permissions"].append(
|
||||
{"name": permission, "granted": granted, "type": "install"}
|
||||
)
|
||||
if in_runtime_permissions:
|
||||
if not line.startswith(" " * 8):
|
||||
in_runtime_permissions = False
|
||||
else:
|
||||
lineinfo = line.strip().split(":")
|
||||
permission = lineinfo[0]
|
||||
granted = None
|
||||
if "granted=" in lineinfo[1]:
|
||||
granted = "granted=true" in lineinfo[1]
|
||||
|
||||
details["permissions"].append(
|
||||
{"name": permission, "granted": granted, "type": "runtime"}
|
||||
)
|
||||
if in_declared_permissions:
|
||||
if not line.startswith(" " * 6):
|
||||
in_declared_permissions = False
|
||||
else:
|
||||
permission = line.strip().split(":")[0]
|
||||
details["permissions"].append(
|
||||
{"name": permission, "type": "declared"}
|
||||
)
|
||||
if in_requested_permissions:
|
||||
if not line.startswith(" " * 6):
|
||||
in_requested_permissions = False
|
||||
else:
|
||||
details["requested_permissions"].append(line.strip())
|
||||
if line.strip().startswith("userId="):
|
||||
details["uid"] = line.split("=")[1].strip()
|
||||
elif line.strip().startswith("versionName="):
|
||||
details["version_name"] = line.split("=")[1].strip()
|
||||
elif line.strip().startswith("versionCode="):
|
||||
details["version_code"] = line.split("=", 1)[1].strip()
|
||||
elif line.strip().startswith("timeStamp="):
|
||||
details["timestamp"] = line.split("=")[1].strip()
|
||||
elif line.strip().startswith("firstInstallTime="):
|
||||
details["first_install_time"] = line.split("=")[1].strip()
|
||||
elif line.strip().startswith("lastUpdateTime="):
|
||||
details["last_update_time"] = line.split("=")[1].strip()
|
||||
elif line.strip() == "install permissions:":
|
||||
in_install_permissions = True
|
||||
elif line.strip() == "runtime permissions:":
|
||||
in_runtime_permissions = True
|
||||
elif line.strip() == "declared permissions:":
|
||||
in_declared_permissions = True
|
||||
elif line.strip() == "requested permissions:":
|
||||
in_requested_permissions = True
|
||||
|
||||
return details
|
||||
|
||||
def parse_dumpsys_packages(self, output: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Parse the dumpsys package service data
|
||||
"""
|
||||
pkg_rxp = re.compile(r" Package \[(.+?)\].*")
|
||||
|
||||
results = []
|
||||
package_name = None
|
||||
package = {}
|
||||
lines = []
|
||||
for line in output.splitlines():
|
||||
if line.startswith(" Package ["):
|
||||
if len(lines) > 0:
|
||||
details = self.parse_dumpsys_package_for_details("\n".join(lines))
|
||||
package.update(details)
|
||||
results.append(package)
|
||||
lines = []
|
||||
package = {}
|
||||
|
||||
matches = pkg_rxp.findall(line)
|
||||
if not matches:
|
||||
continue
|
||||
|
||||
package_name = matches[0]
|
||||
package["package_name"] = package_name
|
||||
continue
|
||||
|
||||
if not package_name:
|
||||
continue
|
||||
|
||||
lines.append(line)
|
||||
|
||||
if len(lines) > 0:
|
||||
details = self.parse_dumpsys_package_for_details("\n".join(lines))
|
||||
package.update(details)
|
||||
results.append(package)
|
||||
|
||||
return results
|
||||
|
||||
def parse(self, content: str):
|
||||
"""
|
||||
Parse the Dumpsys Package section for activities
|
||||
Adds results to self.results
|
||||
|
||||
:param content: content of the package section (string)
|
||||
"""
|
||||
self.results = []
|
||||
package = []
|
||||
|
||||
in_package_list = False
|
||||
for line in content.split("\n"):
|
||||
if line.startswith("Packages:"):
|
||||
in_package_list = True
|
||||
continue
|
||||
|
||||
if not in_package_list:
|
||||
continue
|
||||
|
||||
if line.strip() == "":
|
||||
break
|
||||
|
||||
package.append(line)
|
||||
|
||||
self.results = self.parse_dumpsys_packages("\n".join(package))
|
||||
@@ -51,6 +51,11 @@ ANDROID_DANGEROUS_SETTINGS = [
|
||||
"key": "install_non_market_apps",
|
||||
"safe_value": "0",
|
||||
},
|
||||
{
|
||||
"description": "enabled accessibility services",
|
||||
"key": "accessibility_enabled",
|
||||
"safe_value": "0",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ def version():
|
||||
is_flag=True,
|
||||
help="Extract all packages installed on the phone, including system packages",
|
||||
)
|
||||
@click.option("--virustotal", "-v", is_flag=True, help="Check packages on VirusTotal")
|
||||
@click.option("--virustotal", "-V", is_flag=True, help="Check packages on VirusTotal")
|
||||
@click.option(
|
||||
"--output",
|
||||
"-o",
|
||||
|
||||
@@ -4,86 +4,25 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import List, Optional, Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from rich.console import Console
|
||||
from rich.progress import track
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
from mvt.android.parsers.dumpsys import parse_dumpsys_package_for_details
|
||||
from mvt.android.artifacts.dumpsys_packages import DumpsysPackagesArtifact
|
||||
from mvt.android.utils import (
|
||||
DANGEROUS_PERMISSIONS,
|
||||
DANGEROUS_PERMISSIONS_THRESHOLD,
|
||||
ROOT_PACKAGES,
|
||||
SECURITY_PACKAGES,
|
||||
SYSTEM_UPDATE_PACKAGES,
|
||||
)
|
||||
from mvt.common.virustotal import VTNoKey, VTQuotaExceeded, virustotal_lookup
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
DANGEROUS_PERMISSIONS_THRESHOLD = 10
|
||||
DANGEROUS_PERMISSIONS = [
|
||||
"android.permission.ACCESS_COARSE_LOCATION",
|
||||
"android.permission.ACCESS_FINE_LOCATION",
|
||||
"android.permission.AUTHENTICATE_ACCOUNTS",
|
||||
"android.permission.CAMERA",
|
||||
"android.permission.DISABLE_KEYGUARD",
|
||||
"android.permission.PROCESS_OUTGOING_CALLS",
|
||||
"android.permission.READ_CALENDAR",
|
||||
"android.permission.READ_CALL_LOG",
|
||||
"android.permission.READ_CONTACTS",
|
||||
"android.permission.READ_PHONE_STATE",
|
||||
"android.permission.READ_SMS",
|
||||
"android.permission.RECEIVE_MMS",
|
||||
"android.permission.RECEIVE_SMS",
|
||||
"android.permission.RECEIVE_WAP_PUSH",
|
||||
"android.permission.RECORD_AUDIO",
|
||||
"android.permission.SEND_SMS",
|
||||
"android.permission.SYSTEM_ALERT_WINDOW",
|
||||
"android.permission.USE_CREDENTIALS",
|
||||
"android.permission.USE_SIP",
|
||||
"com.android.browser.permission.READ_HISTORY_BOOKMARKS",
|
||||
]
|
||||
ROOT_PACKAGES: List[str] = [
|
||||
"com.noshufou.android.su",
|
||||
"com.noshufou.android.su.elite",
|
||||
"eu.chainfire.supersu",
|
||||
"com.koushikdutta.superuser",
|
||||
"com.thirdparty.superuser",
|
||||
"com.yellowes.su",
|
||||
"com.koushikdutta.rommanager",
|
||||
"com.koushikdutta.rommanager.license",
|
||||
"com.dimonvideo.luckypatcher",
|
||||
"com.chelpus.lackypatch",
|
||||
"com.ramdroid.appquarantine",
|
||||
"com.ramdroid.appquarantinepro",
|
||||
"com.devadvance.rootcloak",
|
||||
"com.devadvance.rootcloakplus",
|
||||
"de.robv.android.xposed.installer",
|
||||
"com.saurik.substrate",
|
||||
"com.zachspong.temprootremovejb",
|
||||
"com.amphoras.hidemyroot",
|
||||
"com.amphoras.hidemyrootadfree",
|
||||
"com.formyhm.hiderootPremium",
|
||||
"com.formyhm.hideroot",
|
||||
"me.phh.superuser",
|
||||
"eu.chainfire.supersu.pro",
|
||||
"com.kingouser.com",
|
||||
"com.topjohnwu.magisk",
|
||||
]
|
||||
SECURITY_PACKAGES = [
|
||||
"com.policydm",
|
||||
"com.samsung.android.app.omcagent",
|
||||
"com.samsung.android.securitylogagent",
|
||||
"com.sec.android.soagent",
|
||||
]
|
||||
SYSTEM_UPDATE_PACKAGES = [
|
||||
"com.android.updater",
|
||||
"com.google.android.gms",
|
||||
"com.huawei.android.hwouc",
|
||||
"com.lge.lgdmsclient",
|
||||
"com.motorola.ccc.ota",
|
||||
"com.oneplus.opbackup",
|
||||
"com.oppo.ota",
|
||||
"com.transsion.systemupdate",
|
||||
"com.wssyncmldm",
|
||||
]
|
||||
|
||||
|
||||
class Packages(AndroidExtraction):
|
||||
"""This module extracts the list of installed packages."""
|
||||
@@ -234,7 +173,9 @@ class Packages(AndroidExtraction):
|
||||
if line.strip() == "Packages:":
|
||||
in_packages = True
|
||||
|
||||
return parse_dumpsys_package_for_details("\n".join(lines))
|
||||
return DumpsysPackagesArtifact.parse_dumpsys_package_for_details(
|
||||
"\n".join(lines)
|
||||
)
|
||||
|
||||
def _get_files_for_package(self, package_name: str) -> list:
|
||||
command = f"pm path {package_name}"
|
||||
|
||||
@@ -12,6 +12,7 @@ from .dumpsys_dbinfo import DumpsysDBInfo
|
||||
from .dumpsys_packages import DumpsysPackages
|
||||
from .dumpsys_receivers import DumpsysReceivers
|
||||
from .getprop import Getprop
|
||||
from .packages import Packages
|
||||
from .processes import Processes
|
||||
from .settings import Settings
|
||||
from .sms import SMS
|
||||
@@ -24,6 +25,7 @@ ANDROIDQF_MODULES = [
|
||||
DumpsysDBInfo,
|
||||
DumpsysBatteryDaily,
|
||||
DumpsysBatteryHistory,
|
||||
Packages,
|
||||
Processes,
|
||||
Getprop,
|
||||
Settings,
|
||||
|
||||
@@ -12,7 +12,7 @@ from .base import AndroidQFModule
|
||||
|
||||
|
||||
class DumpsysAccessibility(DumpsysAccessibilityArtifact, AndroidQFModule):
|
||||
"""This module analyse dumpsys accessbility"""
|
||||
"""This module analyses dumpsys accessibility"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -4,19 +4,18 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from mvt.android.artifacts.dumpsys_packages import DumpsysPackagesArtifact
|
||||
from mvt.android.modules.adb.packages import (
|
||||
DANGEROUS_PERMISSIONS,
|
||||
DANGEROUS_PERMISSIONS_THRESHOLD,
|
||||
ROOT_PACKAGES,
|
||||
)
|
||||
from mvt.android.parsers.dumpsys import parse_dumpsys_packages
|
||||
|
||||
from .base import AndroidQFModule
|
||||
|
||||
|
||||
class DumpsysPackages(AndroidQFModule):
|
||||
class DumpsysPackages(DumpsysPackagesArtifact, AndroidQFModule):
|
||||
"""This module analyse dumpsys packages"""
|
||||
|
||||
def __init__(
|
||||
@@ -37,70 +36,15 @@ class DumpsysPackages(AndroidQFModule):
|
||||
results=results,
|
||||
)
|
||||
|
||||
def serialize(self, record: dict) -> Union[dict, list]:
|
||||
entries = []
|
||||
for entry in ["timestamp", "first_install_time", "last_update_time"]:
|
||||
if entry in record:
|
||||
entries.append(
|
||||
{
|
||||
"timestamp": record[entry],
|
||||
"module": self.__class__.__name__,
|
||||
"event": entry,
|
||||
"data": f"Package {record['package_name']} "
|
||||
f"({record['uid']})",
|
||||
}
|
||||
)
|
||||
|
||||
return entries
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
for result in self.results:
|
||||
if result["package_name"] in ROOT_PACKAGES:
|
||||
self.log.warning(
|
||||
"Found an installed package related to "
|
||||
'rooting/jailbreaking: "%s"',
|
||||
result["package_name"],
|
||||
)
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
if not self.indicators:
|
||||
continue
|
||||
|
||||
ioc = self.indicators.check_app_id(result.get("package_name", ""))
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
def run(self) -> None:
|
||||
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
|
||||
if len(dumpsys_file) != 1:
|
||||
self.log.info("Dumpsys file not found")
|
||||
return
|
||||
|
||||
data = self._get_file_content(dumpsys_file[0])
|
||||
|
||||
package = []
|
||||
in_service = False
|
||||
in_package_list = False
|
||||
for line in data.decode("utf-8").split("\n"):
|
||||
if line.strip().startswith("DUMP OF SERVICE package:"):
|
||||
in_service = True
|
||||
continue
|
||||
|
||||
if in_service and line.startswith("Packages:"):
|
||||
in_package_list = True
|
||||
continue
|
||||
|
||||
if not in_service or not in_package_list:
|
||||
continue
|
||||
|
||||
if line.strip() == "":
|
||||
break
|
||||
|
||||
package.append(line)
|
||||
|
||||
self.results = parse_dumpsys_packages("\n".join(package))
|
||||
data = self._get_file_content(dumpsys_file[0]).decode("utf-8", errors="replace")
|
||||
content = self.extract_dumpsys_section(data, "DUMP OF SERVICE package:")
|
||||
self.parse(content)
|
||||
|
||||
for result in self.results:
|
||||
dangerous_permissions_count = 0
|
||||
|
||||
97
mvt/android/modules/androidqf/packages.py
Normal file
97
mvt/android/modules/androidqf/packages.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2023 The MVT Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
import json
|
||||
|
||||
from mvt.android.utils import (
|
||||
ROOT_PACKAGES,
|
||||
BROWSER_INSTALLERS,
|
||||
PLAY_STORE_INSTALLERS,
|
||||
THIRD_PARTY_STORE_INSTALLERS,
|
||||
)
|
||||
|
||||
from .base import AndroidQFModule
|
||||
|
||||
|
||||
class Packages(AndroidQFModule):
|
||||
"""This module examines the installed packages in packages.json"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
module_options: Optional[dict] = None,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
file_path=file_path,
|
||||
target_path=target_path,
|
||||
results_path=results_path,
|
||||
module_options=module_options,
|
||||
log=log,
|
||||
results=results,
|
||||
)
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
for result in self.results:
|
||||
if result["name"] in ROOT_PACKAGES:
|
||||
self.log.warning(
|
||||
"Found an installed package related to "
|
||||
'rooting/jailbreaking: "%s"',
|
||||
result["name"],
|
||||
)
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
# Detections for apps installed via unusual methods
|
||||
if result["installer"] in THIRD_PARTY_STORE_INSTALLERS:
|
||||
self.log.warning(
|
||||
'Found a package installed via a third party store (installer="%s"): "%s"',
|
||||
result["installer"],
|
||||
result["name"],
|
||||
)
|
||||
elif result["installer"] in BROWSER_INSTALLERS:
|
||||
self.log.warning(
|
||||
'Found a package installed via a browser (installer="%s"): "%s"',
|
||||
result["installer"],
|
||||
result["name"],
|
||||
)
|
||||
elif result["installer"] == "null" and result["system"] is False:
|
||||
self.log.warning(
|
||||
'Found a non-system package installed via adb or another method: "%s"',
|
||||
result["name"],
|
||||
)
|
||||
elif result["installer"] in PLAY_STORE_INSTALLERS:
|
||||
pass
|
||||
|
||||
if not self.indicators:
|
||||
continue
|
||||
|
||||
ioc = self.indicators.check_app_id(result.get("name"))
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
for package_file in result.get("files", []):
|
||||
ioc = self.indicators.check_file_hash(package_file["sha256"])
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
def run(self) -> None:
|
||||
packages = self._get_files_by_pattern("*/packages.json")
|
||||
if not packages:
|
||||
self.log.error(
|
||||
"packages.json file not found in this androidqf bundle. Possibly malformed?"
|
||||
)
|
||||
return
|
||||
|
||||
self.results = json.loads(self._get_file_content(packages[0]))
|
||||
self.log.info("Found %d packages in packages.json", len(self.results))
|
||||
@@ -4,19 +4,15 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional, Union
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.modules.adb.packages import (
|
||||
DANGEROUS_PERMISSIONS,
|
||||
DANGEROUS_PERMISSIONS_THRESHOLD,
|
||||
ROOT_PACKAGES,
|
||||
)
|
||||
from mvt.android.parsers.dumpsys import parse_dumpsys_packages
|
||||
from mvt.android.artifacts.dumpsys_packages import DumpsysPackagesArtifact
|
||||
from mvt.android.utils import DANGEROUS_PERMISSIONS, DANGEROUS_PERMISSIONS_THRESHOLD
|
||||
|
||||
from .base import BugReportModule
|
||||
|
||||
|
||||
class Packages(BugReportModule):
|
||||
class Packages(DumpsysPackagesArtifact, BugReportModule):
|
||||
"""This module extracts details on receivers for risky activities."""
|
||||
|
||||
def __init__(
|
||||
@@ -37,83 +33,18 @@ class Packages(BugReportModule):
|
||||
results=results,
|
||||
)
|
||||
|
||||
def serialize(self, record: dict) -> Union[dict, list]:
|
||||
records = []
|
||||
|
||||
timestamps = [
|
||||
{"event": "package_install", "timestamp": record["timestamp"]},
|
||||
{
|
||||
"event": "package_first_install",
|
||||
"timestamp": record["first_install_time"],
|
||||
},
|
||||
{"event": "package_last_update", "timestamp": record["last_update_time"]},
|
||||
]
|
||||
|
||||
for timestamp in timestamps:
|
||||
records.append(
|
||||
{
|
||||
"timestamp": timestamp["timestamp"],
|
||||
"module": self.__class__.__name__,
|
||||
"event": timestamp["event"],
|
||||
"data": f"Install or update of package {record['package_name']}",
|
||||
}
|
||||
)
|
||||
|
||||
return records
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
for result in self.results:
|
||||
if result["package_name"] in ROOT_PACKAGES:
|
||||
self.log.warning(
|
||||
"Found an installed package related to "
|
||||
'rooting/jailbreaking: "%s"',
|
||||
result["package_name"],
|
||||
)
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
if not self.indicators:
|
||||
continue
|
||||
|
||||
ioc = self.indicators.check_app_id(result.get("package_name"))
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
def run(self) -> None:
|
||||
content = self._get_dumpstate_file()
|
||||
if not content:
|
||||
data = self._get_dumpstate_file()
|
||||
if not data:
|
||||
self.log.error(
|
||||
"Unable to find dumpstate file. "
|
||||
"Did you provide a valid bug report archive?"
|
||||
)
|
||||
return
|
||||
|
||||
in_package = False
|
||||
in_packages_list = False
|
||||
lines = []
|
||||
for line in content.decode(errors="ignore").splitlines():
|
||||
if line.strip() == "DUMP OF SERVICE package:":
|
||||
in_package = True
|
||||
continue
|
||||
|
||||
if not in_package:
|
||||
continue
|
||||
|
||||
if line.strip() == "Packages:":
|
||||
in_packages_list = True
|
||||
continue
|
||||
|
||||
if not in_packages_list:
|
||||
continue
|
||||
|
||||
if line.strip() == "":
|
||||
break
|
||||
|
||||
lines.append(line)
|
||||
|
||||
self.results = parse_dumpsys_packages("\n".join(lines))
|
||||
data = data.decode("utf-8", errors="replace")
|
||||
content = self.extract_dumpsys_section(data, "DUMP OF SERVICE package:")
|
||||
self.parse(content)
|
||||
|
||||
for result in self.results:
|
||||
dangerous_permissions_count = 0
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2023 The MVT Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import re
|
||||
from typing import Any, Dict, List
|
||||
|
||||
|
||||
def parse_dumpsys_package_for_details(output: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Parse one entry of a dumpsys package information
|
||||
"""
|
||||
details = {
|
||||
"uid": "",
|
||||
"version_name": "",
|
||||
"version_code": "",
|
||||
"timestamp": "",
|
||||
"first_install_time": "",
|
||||
"last_update_time": "",
|
||||
"permissions": [],
|
||||
"requested_permissions": [],
|
||||
}
|
||||
|
||||
in_install_permissions = False
|
||||
in_runtime_permissions = False
|
||||
in_declared_permissions = False
|
||||
in_requested_permissions = True
|
||||
for line in output.splitlines():
|
||||
if in_install_permissions:
|
||||
if line.startswith(" " * 4) and not line.startswith(" " * 6):
|
||||
in_install_permissions = False
|
||||
else:
|
||||
lineinfo = line.strip().split(":")
|
||||
permission = lineinfo[0]
|
||||
granted = None
|
||||
if "granted=" in lineinfo[1]:
|
||||
granted = "granted=true" in lineinfo[1]
|
||||
|
||||
details["permissions"].append(
|
||||
{"name": permission, "granted": granted, "type": "install"}
|
||||
)
|
||||
|
||||
if in_runtime_permissions:
|
||||
if not line.startswith(" " * 8):
|
||||
in_runtime_permissions = False
|
||||
else:
|
||||
lineinfo = line.strip().split(":")
|
||||
permission = lineinfo[0]
|
||||
granted = None
|
||||
if "granted=" in lineinfo[1]:
|
||||
granted = "granted=true" in lineinfo[1]
|
||||
|
||||
details["permissions"].append(
|
||||
{"name": permission, "granted": granted, "type": "runtime"}
|
||||
)
|
||||
|
||||
if in_declared_permissions:
|
||||
if not line.startswith(" " * 6):
|
||||
in_declared_permissions = False
|
||||
else:
|
||||
permission = line.strip().split(":")[0]
|
||||
details["permissions"].append({"name": permission, "type": "declared"})
|
||||
if in_requested_permissions:
|
||||
if not line.startswith(" " * 6):
|
||||
in_requested_permissions = False
|
||||
else:
|
||||
details["requested_permissions"].append(line.strip())
|
||||
|
||||
if line.strip().startswith("userId="):
|
||||
details["uid"] = line.split("=")[1].strip()
|
||||
elif line.strip().startswith("versionName="):
|
||||
details["version_name"] = line.split("=")[1].strip()
|
||||
elif line.strip().startswith("versionCode="):
|
||||
details["version_code"] = line.split("=", 1)[1].strip()
|
||||
elif line.strip().startswith("timeStamp="):
|
||||
details["timestamp"] = line.split("=")[1].strip()
|
||||
elif line.strip().startswith("firstInstallTime="):
|
||||
details["first_install_time"] = line.split("=")[1].strip()
|
||||
elif line.strip().startswith("lastUpdateTime="):
|
||||
details["last_update_time"] = line.split("=")[1].strip()
|
||||
elif line.strip() == "install permissions:":
|
||||
in_install_permissions = True
|
||||
elif line.strip() == "runtime permissions:":
|
||||
in_runtime_permissions = True
|
||||
elif line.strip() == "declared permissions:":
|
||||
in_declared_permissions = True
|
||||
elif line.strip() == "requested permissions:":
|
||||
in_requested_permissions = True
|
||||
|
||||
return details
|
||||
|
||||
|
||||
def parse_dumpsys_packages(output: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Parse the dumpsys package service data
|
||||
"""
|
||||
pkg_rxp = re.compile(r" Package \[(.+?)\].*")
|
||||
|
||||
results = []
|
||||
package_name = None
|
||||
package = {}
|
||||
lines = []
|
||||
for line in output.splitlines():
|
||||
if line.startswith(" Package ["):
|
||||
if len(lines) > 0:
|
||||
details = parse_dumpsys_package_for_details("\n".join(lines))
|
||||
package.update(details)
|
||||
results.append(package)
|
||||
lines = []
|
||||
package = {}
|
||||
|
||||
matches = pkg_rxp.findall(line)
|
||||
if not matches:
|
||||
continue
|
||||
|
||||
package_name = matches[0]
|
||||
package["package_name"] = package_name
|
||||
continue
|
||||
|
||||
if not package_name:
|
||||
continue
|
||||
|
||||
lines.append(line)
|
||||
|
||||
if len(lines) > 0:
|
||||
details = parse_dumpsys_package_for_details("\n".join(lines))
|
||||
package.update(details)
|
||||
results.append(package)
|
||||
|
||||
return results
|
||||
@@ -3,6 +3,7 @@
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List
|
||||
|
||||
|
||||
def warn_android_patch_level(patch_level: str, log) -> bool:
|
||||
@@ -17,3 +18,88 @@ def warn_android_patch_level(patch_level: str, log) -> bool:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
ROOT_PACKAGES: List[str] = [
|
||||
"com.noshufou.android.su",
|
||||
"com.noshufou.android.su.elite",
|
||||
"eu.chainfire.supersu",
|
||||
"com.koushikdutta.superuser",
|
||||
"com.thirdparty.superuser",
|
||||
"com.yellowes.su",
|
||||
"com.koushikdutta.rommanager",
|
||||
"com.koushikdutta.rommanager.license",
|
||||
"com.dimonvideo.luckypatcher",
|
||||
"com.chelpus.lackypatch",
|
||||
"com.ramdroid.appquarantine",
|
||||
"com.ramdroid.appquarantinepro",
|
||||
"com.devadvance.rootcloak",
|
||||
"com.devadvance.rootcloakplus",
|
||||
"de.robv.android.xposed.installer",
|
||||
"com.saurik.substrate",
|
||||
"com.zachspong.temprootremovejb",
|
||||
"com.amphoras.hidemyroot",
|
||||
"com.amphoras.hidemyrootadfree",
|
||||
"com.formyhm.hiderootPremium",
|
||||
"com.formyhm.hideroot",
|
||||
"me.phh.superuser",
|
||||
"eu.chainfire.supersu.pro",
|
||||
"com.kingouser.com",
|
||||
"com.topjohnwu.magisk",
|
||||
]
|
||||
|
||||
DANGEROUS_PERMISSIONS_THRESHOLD = 10
|
||||
|
||||
DANGEROUS_PERMISSIONS = [
|
||||
"android.permission.ACCESS_COARSE_LOCATION",
|
||||
"android.permission.ACCESS_FINE_LOCATION",
|
||||
"android.permission.AUTHENTICATE_ACCOUNTS",
|
||||
"android.permission.CAMERA",
|
||||
"android.permission.DISABLE_KEYGUARD",
|
||||
"android.permission.PROCESS_OUTGOING_CALLS",
|
||||
"android.permission.READ_CALENDAR",
|
||||
"android.permission.READ_CALL_LOG",
|
||||
"android.permission.READ_CONTACTS",
|
||||
"android.permission.READ_PHONE_STATE",
|
||||
"android.permission.READ_SMS",
|
||||
"android.permission.RECEIVE_MMS",
|
||||
"android.permission.RECEIVE_SMS",
|
||||
"android.permission.RECEIVE_WAP_PUSH",
|
||||
"android.permission.RECORD_AUDIO",
|
||||
"android.permission.SEND_SMS",
|
||||
"android.permission.SYSTEM_ALERT_WINDOW",
|
||||
"android.permission.USE_CREDENTIALS",
|
||||
"android.permission.USE_SIP",
|
||||
"com.android.browser.permission.READ_HISTORY_BOOKMARKS",
|
||||
]
|
||||
|
||||
SECURITY_PACKAGES = [
|
||||
"com.policydm",
|
||||
"com.samsung.android.app.omcagent",
|
||||
"com.samsung.android.securitylogagent",
|
||||
"com.sec.android.soagent",
|
||||
]
|
||||
|
||||
SYSTEM_UPDATE_PACKAGES = [
|
||||
"com.android.updater",
|
||||
"com.google.android.gms",
|
||||
"com.huawei.android.hwouc",
|
||||
"com.lge.lgdmsclient",
|
||||
"com.motorola.ccc.ota",
|
||||
"com.oneplus.opbackup",
|
||||
"com.oppo.ota",
|
||||
"com.transsion.systemupdate",
|
||||
"com.wssyncmldm",
|
||||
]
|
||||
|
||||
# Apps installed from the Play store have this installer
|
||||
PLAY_STORE_INSTALLERS = ["com.android.vending"]
|
||||
|
||||
# Installer id for apps from common 3rd party stores
|
||||
THIRD_PARTY_STORE_INSTALLERS = ["com.aurora.store", "org.fdroid.fdroid"]
|
||||
|
||||
# Packages installed via a browser have these installers
|
||||
BROWSER_INSTALLERS = [
|
||||
"com.google.android.packageinstaller",
|
||||
"com.android.packageinstaller",
|
||||
]
|
||||
|
||||
@@ -53,7 +53,7 @@ class CmdCheckIOCS(Command):
|
||||
if self.module_name and iocs_module.__name__ != self.module_name:
|
||||
continue
|
||||
|
||||
if iocs_module().get_slug() != name_only:
|
||||
if iocs_module.get_slug() != name_only:
|
||||
continue
|
||||
|
||||
log.info(
|
||||
|
||||
@@ -85,6 +85,15 @@ class Command:
|
||||
)
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_handler.setFormatter(formatter)
|
||||
|
||||
# MVT can be run in a loop
|
||||
# Old file handlers stick around in subsequent loops
|
||||
# Remove any existing logging.FileHandler instances
|
||||
for handler in logger.handlers:
|
||||
if isinstance(handler, logging.FileHandler):
|
||||
logger.removeHandler(handler)
|
||||
|
||||
# And finally add the new one
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
def _store_timeline(self) -> None:
|
||||
@@ -160,6 +169,27 @@ class Command:
|
||||
def finish(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def _show_disable_adb_warning(self) -> None:
|
||||
"""Warn if ADB is enabled"""
|
||||
if type(self).__name__ in ["CmdAndroidCheckADB", "CmdAndroidCheckAndroidQF"]:
|
||||
self.log.info(
|
||||
"Please disable Developer Options and ADB (Android Debug Bridge) on the device once finished with the acquisition. "
|
||||
"ADB is a powerful tool which can allow unauthorized access to the device."
|
||||
)
|
||||
|
||||
def _show_support_message(self) -> None:
|
||||
support_message = "Please seek reputable expert help if you have serious concerns about a possible spyware attack. Such support is available to human rights defenders and civil society through Amnesty International's Security Lab at https://securitylab.amnesty.org/get-help/?c=mvt"
|
||||
if self.detected_count == 0:
|
||||
self.log.info(
|
||||
f"[bold]NOTE:[/bold] Using MVT with public indicators of compromise (IOCs) [bold]WILL NOT[/bold] automatically detect advanced attacks.\n\n{support_message}",
|
||||
extra={"markup": True},
|
||||
)
|
||||
else:
|
||||
self.log.warning(
|
||||
f"[bold]NOTE: Detected indicators of compromise[/bold]. Only expert review can confirm if the detected indicators are signs of an attack.\n\n{support_message}",
|
||||
extra={"markup": True},
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
try:
|
||||
self.init()
|
||||
@@ -208,3 +238,6 @@ class Command:
|
||||
|
||||
self._store_timeline()
|
||||
self._store_info()
|
||||
|
||||
self._show_disable_adb_warning()
|
||||
self._show_support_message()
|
||||
|
||||
@@ -10,7 +10,7 @@ from .version import MVT_VERSION
|
||||
|
||||
|
||||
def check_updates() -> None:
|
||||
# First we check for MVT version udpates.
|
||||
# First we check for MVT version updates.
|
||||
mvt_updates = MVTUpdates()
|
||||
try:
|
||||
latest_version = mvt_updates.check()
|
||||
|
||||
@@ -74,12 +74,13 @@ class MVTModule:
|
||||
log.info('Loaded %d results from "%s"', len(results), json_path)
|
||||
return cls(results=results, log=log)
|
||||
|
||||
def get_slug(self) -> str:
|
||||
@classmethod
|
||||
def get_slug(cls) -> str:
|
||||
"""Use the module's class name to retrieve a slug"""
|
||||
if self.slug:
|
||||
return self.slug
|
||||
if cls.slug:
|
||||
return cls.slug
|
||||
|
||||
sub = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", self.__class__.__name__)
|
||||
sub = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", cls.__name__)
|
||||
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", sub).lower()
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
|
||||
@@ -9,47 +9,65 @@ import requests
|
||||
from tld import get_tld
|
||||
|
||||
SHORTENER_DOMAINS = [
|
||||
"0rz.tw",
|
||||
"1drv.ms",
|
||||
"1link.in",
|
||||
"1url.com",
|
||||
"2big.at",
|
||||
"2.gp",
|
||||
"2pl.us",
|
||||
"2tu.us",
|
||||
"2ya.com",
|
||||
"3.ly",
|
||||
"4sq.com",
|
||||
"4url.cc",
|
||||
"6url.com",
|
||||
"a.gg",
|
||||
"a.nf",
|
||||
"7.ly",
|
||||
"a2a.me",
|
||||
"abbrr.com",
|
||||
"adf.ly",
|
||||
"adjix.com",
|
||||
"a.gg",
|
||||
"alturl.com",
|
||||
"a.nf",
|
||||
"anon.to",
|
||||
"apple.news",
|
||||
"atu.ca",
|
||||
"b23.ru",
|
||||
"bacn.me",
|
||||
"bc.vc",
|
||||
"bfy.tw",
|
||||
"binged.it",
|
||||
"bit.do",
|
||||
"bit.ly",
|
||||
"bizj.us",
|
||||
"bkite.com",
|
||||
"bloat.me",
|
||||
"budurl.com",
|
||||
"buff.ly",
|
||||
"buk.me",
|
||||
"burnurl.com",
|
||||
"c-o.in",
|
||||
"chilp.it",
|
||||
"chn.ge",
|
||||
"clck.ru",
|
||||
"cli.gs",
|
||||
"clickmeter.com",
|
||||
"cli.gs",
|
||||
"c-o.in",
|
||||
"cort.as",
|
||||
"cut.ly",
|
||||
"cutt.ly",
|
||||
"cuturl.com",
|
||||
"decenturl.com",
|
||||
"dai.ly",
|
||||
"dailym.ai",
|
||||
"db.tt",
|
||||
"decenturl.com",
|
||||
"dfl8.me",
|
||||
"digbig.com",
|
||||
"digg.com",
|
||||
"disq.us",
|
||||
"dlvr.it",
|
||||
"doiop.com",
|
||||
"do.my",
|
||||
"dwarfurl.com",
|
||||
"dy.fi",
|
||||
"easyuri.com",
|
||||
@@ -58,27 +76,35 @@ SHORTENER_DOMAINS = [
|
||||
"esyurl.com",
|
||||
"ewerl.com",
|
||||
"fa.b",
|
||||
"ff.im",
|
||||
"fa.by",
|
||||
"fb.me",
|
||||
"fff.to",
|
||||
"ff.im",
|
||||
"fhurl.com",
|
||||
"fire.to",
|
||||
"firsturl.de",
|
||||
"firsturl.net",
|
||||
"flic.kr",
|
||||
"flq.us",
|
||||
"fly2.ws",
|
||||
"fon.gs",
|
||||
"forms.gle",
|
||||
"fwd4.me",
|
||||
"gdurl.com",
|
||||
"gg.gg",
|
||||
"gl.am",
|
||||
"go.9nl.com",
|
||||
"go2.me",
|
||||
"go2cut.com",
|
||||
"go2.me",
|
||||
"go.9nl.com",
|
||||
"goo.gl",
|
||||
"goshrink.com",
|
||||
"got.by",
|
||||
"gowat.ch",
|
||||
"gri.ms",
|
||||
"gurl.es",
|
||||
"hellotxt.com",
|
||||
"hex.io",
|
||||
"hongkiat.shorturl.com",
|
||||
"hover.com",
|
||||
"href.in",
|
||||
"ht.ly",
|
||||
@@ -87,13 +113,15 @@ SHORTENER_DOMAINS = [
|
||||
"hurl.it",
|
||||
"hurl.me",
|
||||
"hurl.ws",
|
||||
"ibb.co",
|
||||
"icanhaz.com",
|
||||
"idek.net",
|
||||
"inreply.to",
|
||||
"is.gd",
|
||||
"iscool.net",
|
||||
"is.gd",
|
||||
"iterasi.net",
|
||||
"jijr.com",
|
||||
"j.mp",
|
||||
"jmp2.net",
|
||||
"just.as",
|
||||
"kissa.be",
|
||||
@@ -101,21 +129,23 @@ SHORTENER_DOMAINS = [
|
||||
"klck.me",
|
||||
"korta.nu",
|
||||
"krunchd.com",
|
||||
"lat.ms",
|
||||
"liip.to",
|
||||
"liltext.com",
|
||||
"lin.cr",
|
||||
"linkbee.com",
|
||||
"linkbun.ch",
|
||||
"liurl.cn",
|
||||
"ln-s.net",
|
||||
"ln-s.ru",
|
||||
"lnkd.in",
|
||||
"lnk.gd",
|
||||
"lnk.in",
|
||||
"lnkd.in",
|
||||
"ln-s.net",
|
||||
"ln-s.ru",
|
||||
"loopt.us",
|
||||
"lru.jp",
|
||||
"lt.tl",
|
||||
"lurl.no",
|
||||
"lyhyt.eu",
|
||||
"metamark.net",
|
||||
"migre.me",
|
||||
"minilien.com",
|
||||
@@ -123,52 +153,71 @@ SHORTENER_DOMAINS = [
|
||||
"minurl.fr",
|
||||
"moourl.com",
|
||||
"myurl.in",
|
||||
"nbcnews.to",
|
||||
"ne1.net",
|
||||
"njx.me",
|
||||
"nn.nf",
|
||||
"notlong.com",
|
||||
"n.pr",
|
||||
"nsfw.in",
|
||||
"o-x.fr",
|
||||
"nyti.ms",
|
||||
"om.ly",
|
||||
"onforb.es",
|
||||
"on.mktw.net",
|
||||
"ow.ly",
|
||||
"o-x.fr",
|
||||
"pca.st",
|
||||
"pd.am",
|
||||
"pic.gd",
|
||||
"ping.fm",
|
||||
"piurl.com",
|
||||
"pnt.me",
|
||||
"politi.co",
|
||||
"poprl.com",
|
||||
"post.ly",
|
||||
"posted.at",
|
||||
"post.ly",
|
||||
"profile.to",
|
||||
"q.gs",
|
||||
"qicute.com",
|
||||
"qlnk.net",
|
||||
"qr.ae",
|
||||
"qte.me",
|
||||
"quip-art.com",
|
||||
"rb6.me",
|
||||
"rb.gy",
|
||||
"read.bi",
|
||||
"redir.ec",
|
||||
"redirx.com",
|
||||
"ri.ms",
|
||||
"redr.me",
|
||||
"reut.rs",
|
||||
"rickroll.it",
|
||||
"r.im",
|
||||
"ri.ms",
|
||||
"riz.gd",
|
||||
"rsmonkey.com",
|
||||
"ru.ly",
|
||||
"rubyurl.com",
|
||||
"ru.ly",
|
||||
"s7y.us",
|
||||
"safe.mn",
|
||||
"sharein.com",
|
||||
"sharetabs.com",
|
||||
"shorl.com",
|
||||
"short.ie",
|
||||
"short.to",
|
||||
"shortlinks.co.uk",
|
||||
"shortna.me",
|
||||
"short.to",
|
||||
"shorturl.at",
|
||||
"shorturl.com",
|
||||
"shoturl.us",
|
||||
"shout.to",
|
||||
"shrinkify.com",
|
||||
"shrinkster.com",
|
||||
"shrt.st",
|
||||
"shrten.com",
|
||||
"shrt.st",
|
||||
"shrunkin.com",
|
||||
"shw.me",
|
||||
"simurl.com",
|
||||
"smsh.me",
|
||||
"sn.im",
|
||||
"snipr.com",
|
||||
"snipurl.com",
|
||||
@@ -179,24 +228,30 @@ SHORTENER_DOMAINS = [
|
||||
"starturl.com",
|
||||
"sturly.com",
|
||||
"su.pr",
|
||||
"t.cn",
|
||||
"t.co",
|
||||
"tcrn.ch",
|
||||
"tgr.ph",
|
||||
"thrdl.es",
|
||||
"tighturl.com",
|
||||
"tiny.cc",
|
||||
"tiny.pl",
|
||||
"tiny123.com",
|
||||
"tinyarro.ws",
|
||||
"tiny.cc",
|
||||
"tinylink.in",
|
||||
"tiny.pl",
|
||||
"tiny.tw",
|
||||
"tinytw.it",
|
||||
"tinyuri.ca",
|
||||
"tinyurl.com",
|
||||
"tinyvid.io",
|
||||
"t.me",
|
||||
"tnij.org",
|
||||
"to.ly",
|
||||
"tnw.to",
|
||||
"togoto.us",
|
||||
"to.ly",
|
||||
"traceurl.com",
|
||||
"tr.im",
|
||||
"tr.my",
|
||||
"traceurl.com",
|
||||
"turo.us",
|
||||
"tweetburner.com",
|
||||
"twirl.at",
|
||||
@@ -206,49 +261,62 @@ SHORTENER_DOMAINS = [
|
||||
"twiturl.de",
|
||||
"twurl.cc",
|
||||
"twurl.nl",
|
||||
"u.mavrev.com",
|
||||
"u.nu",
|
||||
"u6e.de",
|
||||
"ub0.cc",
|
||||
"ukl.me.uk",
|
||||
"u.mavrev.com",
|
||||
"u.nu",
|
||||
"updating.me",
|
||||
"ur1.ca",
|
||||
"url.co.uk",
|
||||
"url.ie",
|
||||
"url4.eu",
|
||||
"urlao.com",
|
||||
"urlbrief.com",
|
||||
"url.co.uk",
|
||||
"urlcover.com",
|
||||
"urlcut.com",
|
||||
"urlenco.de",
|
||||
"urlhawk.com",
|
||||
"url.ie",
|
||||
"urlkiss.com",
|
||||
"urlot.com",
|
||||
"urlpire.com",
|
||||
"urlx.ie",
|
||||
"urlx.org",
|
||||
"urlzen.com",
|
||||
"use.my",
|
||||
"u.to",
|
||||
"v.gd",
|
||||
"virl.com",
|
||||
"vl.am",
|
||||
"vurl.com",
|
||||
"vzturl.com",
|
||||
"w3t.org",
|
||||
"wapo.st",
|
||||
"wapurl.co.uk",
|
||||
"wipi.es",
|
||||
"wp.me",
|
||||
"x.co",
|
||||
"x.se",
|
||||
"xaddr.com",
|
||||
"x.co",
|
||||
"xeeurl.com",
|
||||
"xr.com",
|
||||
"xrl.in",
|
||||
"xrl.us",
|
||||
"x.se",
|
||||
"xurl.es",
|
||||
"xurl.jp",
|
||||
"xzb.cc",
|
||||
"ye.pe",
|
||||
"yep.it",
|
||||
"yfrog.com",
|
||||
"yhoo.it",
|
||||
"ymlp.com",
|
||||
"yuarel.com",
|
||||
"yweb.com",
|
||||
"zi.ma",
|
||||
"zi.pe",
|
||||
"zipmyurl.com",
|
||||
"zurl.to",
|
||||
"zurl.ws",
|
||||
"zz.gd",
|
||||
]
|
||||
|
||||
|
||||
@@ -53,20 +53,23 @@ def convert_chrometime_to_datetime(timestamp: int) -> datetime.datetime:
|
||||
def convert_datetime_to_iso(date_time: datetime.datetime) -> str:
|
||||
"""Converts datetime to ISO string.
|
||||
|
||||
:param datetime: datetime.
|
||||
:param datetime: datetime, naive or timezone aware
|
||||
:type datetime: datetime.datetime
|
||||
:returns: ISO datetime string in YYYY-mm-dd HH:MM:SS.ms format.
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
try:
|
||||
return date_time.strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||
except Exception:
|
||||
if not date_time:
|
||||
return ""
|
||||
|
||||
if date_time.tzinfo:
|
||||
# Timezone aware object - convert to UTC
|
||||
date_time = date_time.astimezone(tz=datetime.timezone.utc)
|
||||
return date_time.strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||
|
||||
|
||||
def convert_unix_to_utc_datetime(
|
||||
timestamp: Union[int, float, str]
|
||||
timestamp: Union[int, float, str],
|
||||
) -> datetime.datetime:
|
||||
"""Converts a unix epoch timestamp to UTC datetime.
|
||||
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
MVT_VERSION = "2.4.3"
|
||||
MVT_VERSION = "2.5.4"
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
},
|
||||
{
|
||||
"identifier": "iPhone14,8",
|
||||
"decription": "iPhone 14 Plus"
|
||||
"description": "iPhone 14 Plus"
|
||||
},
|
||||
{
|
||||
"identifier": "iPhone15,2",
|
||||
|
||||
@@ -883,6 +883,14 @@
|
||||
"version": "15.8",
|
||||
"build": "19H370"
|
||||
},
|
||||
{
|
||||
"version": "15.8.1",
|
||||
"build": "19H380"
|
||||
},
|
||||
{
|
||||
"version": "15.8.2",
|
||||
"build": "19H384"
|
||||
},
|
||||
{
|
||||
"build": "20A362",
|
||||
"version": "16.0"
|
||||
@@ -960,6 +968,30 @@
|
||||
"version": "16.7.2",
|
||||
"build": "20H115"
|
||||
},
|
||||
{
|
||||
"version": "16.7.3",
|
||||
"build": "20H232"
|
||||
},
|
||||
{
|
||||
"version": "16.7.4",
|
||||
"build": "20H240"
|
||||
},
|
||||
{
|
||||
"version": "16.7.5",
|
||||
"build": "20H307"
|
||||
},
|
||||
{
|
||||
"version": "16.7.6",
|
||||
"build": "20H320"
|
||||
},
|
||||
{
|
||||
"version": "16.7.7",
|
||||
"build": "20H330"
|
||||
},
|
||||
{
|
||||
"version": "16.7.8",
|
||||
"build": "20H343"
|
||||
},
|
||||
{
|
||||
"version": "17.0",
|
||||
"build": "21A327"
|
||||
@@ -995,5 +1027,57 @@
|
||||
{
|
||||
"version": "17.1.1",
|
||||
"build": "21B91"
|
||||
},
|
||||
{
|
||||
"version": "17.1.2",
|
||||
"build": "21B101"
|
||||
},
|
||||
{
|
||||
"version": "17.2",
|
||||
"build": "21C62"
|
||||
},
|
||||
{
|
||||
"version": "17.2.1",
|
||||
"build": "21C66"
|
||||
},
|
||||
{
|
||||
"version": "17.3",
|
||||
"build": "21D50"
|
||||
},
|
||||
{
|
||||
"version": "17.3.1",
|
||||
"build": "21D61"
|
||||
},
|
||||
{
|
||||
"version": "17.4",
|
||||
"build": "21E219"
|
||||
},
|
||||
{
|
||||
"version": "17.4.1",
|
||||
"build": "21E236"
|
||||
},
|
||||
{
|
||||
"version": "17.4.1",
|
||||
"build": "21E237"
|
||||
},
|
||||
{
|
||||
"version": "17.5",
|
||||
"build": "21F79"
|
||||
},
|
||||
{
|
||||
"version": "17.5.1",
|
||||
"build": "21F90"
|
||||
},
|
||||
{
|
||||
"version": "17.6.1",
|
||||
"build": "21G93"
|
||||
},
|
||||
{
|
||||
"version": "17.6.1",
|
||||
"build": "21G101"
|
||||
},
|
||||
{
|
||||
"version": "18",
|
||||
"build": "22A3354"
|
||||
}
|
||||
]
|
||||
@@ -8,7 +8,6 @@ import io
|
||||
import logging
|
||||
import os
|
||||
import plistlib
|
||||
import sqlite3
|
||||
from typing import Optional
|
||||
|
||||
from mvt.common.module import DatabaseNotFoundError
|
||||
@@ -124,7 +123,7 @@ class Manifest(IOSExtraction):
|
||||
|
||||
self.log.info("Found Manifest.db database at path: %s", manifest_db_path)
|
||||
|
||||
conn = sqlite3.connect(manifest_db_path)
|
||||
conn = self._open_sqlite_db(manifest_db_path)
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("SELECT * FROM Files;")
|
||||
|
||||
@@ -49,7 +49,7 @@ class IOSExtraction(MVTModule):
|
||||
"""
|
||||
# TODO: Find a better solution.
|
||||
if not forced:
|
||||
conn = sqlite3.connect(file_path)
|
||||
conn = self._open_sqlite_db(file_path)
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
@@ -91,6 +91,9 @@ class IOSExtraction(MVTModule):
|
||||
|
||||
self.log.info("Database at path %s recovered successfully!", file_path)
|
||||
|
||||
def _open_sqlite_db(self, file_path: str) -> sqlite3.Connection:
|
||||
return sqlite3.connect(f"file:{file_path}?immutable=1", uri=True)
|
||||
|
||||
def _get_backup_files_from_manifest(
|
||||
self, relative_path: Optional[str] = None, domain: Optional[str] = None
|
||||
) -> Iterator[dict]:
|
||||
@@ -109,7 +112,7 @@ class IOSExtraction(MVTModule):
|
||||
base_sql = "SELECT fileID, domain, relativePath FROM Files WHERE "
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(manifest_db_path)
|
||||
conn = self._open_sqlite_db(manifest_db_path)
|
||||
cur = conn.cursor()
|
||||
if relative_path and domain:
|
||||
cur.execute(
|
||||
|
||||
@@ -85,7 +85,7 @@ class Analytics(IOSExtraction):
|
||||
def _extract_analytics_data(self):
|
||||
artifact = self.file_path.split("/")[-1]
|
||||
|
||||
conn = sqlite3.connect(self.file_path)
|
||||
conn = self._open_sqlite_db(self.file_path)
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
|
||||
@@ -64,7 +64,7 @@ class CacheFiles(IOSExtraction):
|
||||
def _process_cache_file(self, file_path):
|
||||
self.log.info("Processing cache file at path: %s", file_path)
|
||||
|
||||
conn = sqlite3.connect(file_path)
|
||||
conn = self._open_sqlite_db(file_path)
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
import sqlite3
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import convert_mactime_to_iso
|
||||
@@ -61,7 +60,7 @@ class SafariFavicon(IOSExtraction):
|
||||
self.detected.append(result)
|
||||
|
||||
def _process_favicon_db(self, file_path):
|
||||
conn = sqlite3.connect(file_path)
|
||||
conn = self._open_sqlite_db(file_path)
|
||||
|
||||
# Fetch valid icon cache.
|
||||
cur = conn.cursor()
|
||||
|
||||
@@ -70,6 +70,9 @@ class ShutdownLog(IOSExtraction):
|
||||
|
||||
def process_shutdownlog(self, content):
|
||||
current_processes = []
|
||||
recent_processes = []
|
||||
times_delayed = 0
|
||||
delay = 0.0
|
||||
for line in content.split("\n"):
|
||||
line = line.strip()
|
||||
|
||||
@@ -78,9 +81,22 @@ class ShutdownLog(IOSExtraction):
|
||||
{
|
||||
"pid": line[line.find("pid: ") + 5 : line.find(" (")],
|
||||
"client": line[line.find("(") + 1 : line.find(")")],
|
||||
"delay": delay,
|
||||
"times_delayed": times_delayed,
|
||||
}
|
||||
)
|
||||
elif line.startswith("After "):
|
||||
# Consider the previous processes
|
||||
# End of the current processes
|
||||
for p in current_processes:
|
||||
recent_processes.append(p)
|
||||
delay += float(line.split(" ")[1][:-2])
|
||||
times_delayed += 1
|
||||
current_processes = []
|
||||
elif line.startswith("SIGTERM: "):
|
||||
for p in current_processes:
|
||||
recent_processes.append(p)
|
||||
|
||||
try:
|
||||
mac_timestamp = int(line[line.find("[") + 1 : line.find("]")])
|
||||
except ValueError:
|
||||
@@ -92,16 +108,21 @@ class ShutdownLog(IOSExtraction):
|
||||
|
||||
isodate = convert_mactime_to_iso(mac_timestamp, from_2001=False)
|
||||
|
||||
for current_process in current_processes:
|
||||
for process in recent_processes:
|
||||
self.results.append(
|
||||
{
|
||||
"isodate": isodate,
|
||||
"pid": current_process["pid"],
|
||||
"client": current_process["client"],
|
||||
"pid": process["pid"],
|
||||
"client": process["client"],
|
||||
"delay": process["delay"],
|
||||
"times_delayed": process["times_delayed"],
|
||||
}
|
||||
)
|
||||
|
||||
current_processes = []
|
||||
recent_processes = []
|
||||
times_delayed = 0
|
||||
delay = 0.0
|
||||
|
||||
self.results = sorted(self.results, key=lambda entry: entry["isodate"])
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
import sqlite3
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import convert_mactime_to_iso
|
||||
@@ -82,7 +81,7 @@ class Calendar(IOSExtraction):
|
||||
"""
|
||||
Parse the calendar database
|
||||
"""
|
||||
conn = sqlite3.connect(self.file_path)
|
||||
conn = self._open_sqlite_db(self.file_path)
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute(
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
import sqlite3
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import convert_mactime_to_iso
|
||||
@@ -53,7 +52,7 @@ class Calls(IOSExtraction):
|
||||
)
|
||||
self.log.info("Found Calls database at path: %s", self.file_path)
|
||||
|
||||
conn = sqlite3.connect(self.file_path)
|
||||
conn = self._open_sqlite_db(self.file_path)
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
@@ -70,9 +69,9 @@ class Calls(IOSExtraction):
|
||||
"isodate": convert_mactime_to_iso(row[0]),
|
||||
"duration": row[1],
|
||||
"location": row[2],
|
||||
"number": row[3].decode("utf-8")
|
||||
if row[3] and row[3] is bytes
|
||||
else row[3],
|
||||
"number": (
|
||||
row[3].decode("utf-8") if row[3] and row[3] is bytes else row[3]
|
||||
),
|
||||
"provider": row[4],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
import sqlite3
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import convert_chrometime_to_datetime, convert_datetime_to_iso
|
||||
@@ -66,7 +65,7 @@ class ChromeFavicon(IOSExtraction):
|
||||
)
|
||||
self.log.info("Found Chrome favicon cache database at path: %s", self.file_path)
|
||||
|
||||
conn = sqlite3.connect(self.file_path)
|
||||
conn = self._open_sqlite_db(self.file_path)
|
||||
|
||||
# Fetch icon cache
|
||||
cur = conn.cursor()
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
import sqlite3
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import convert_chrometime_to_datetime, convert_datetime_to_iso
|
||||
@@ -67,7 +66,7 @@ class ChromeHistory(IOSExtraction):
|
||||
)
|
||||
self.log.info("Found Chrome history database at path: %s", self.file_path)
|
||||
|
||||
conn = sqlite3.connect(self.file_path)
|
||||
conn = self._open_sqlite_db(self.file_path)
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
|
||||
@@ -44,7 +44,7 @@ class Contacts(IOSExtraction):
|
||||
)
|
||||
self.log.info("Found Contacts database at path: %s", self.file_path)
|
||||
|
||||
conn = sqlite3.connect(self.file_path)
|
||||
conn = self._open_sqlite_db(self.file_path)
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
cur.execute(
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
import sqlite3
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import convert_unix_to_iso
|
||||
@@ -68,7 +67,7 @@ class FirefoxFavicon(IOSExtraction):
|
||||
)
|
||||
self.log.info("Found Firefox favicon database at path: %s", self.file_path)
|
||||
|
||||
conn = sqlite3.connect(self.file_path)
|
||||
conn = self._open_sqlite_db(self.file_path)
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
import sqlite3
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import convert_unix_to_iso
|
||||
@@ -68,7 +67,7 @@ class FirefoxHistory(IOSExtraction):
|
||||
)
|
||||
self.log.info("Found Firefox history database at path: %s", self.file_path)
|
||||
|
||||
conn = sqlite3.connect(self.file_path)
|
||||
conn = self._open_sqlite_db(self.file_path)
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
|
||||
@@ -280,7 +280,7 @@ class InteractionC(IOSExtraction):
|
||||
)
|
||||
self.log.info("Found InteractionC database at path: %s", self.file_path)
|
||||
|
||||
conn = sqlite3.connect(self.file_path)
|
||||
conn = self._open_sqlite_db(self.file_path)
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
|
||||
@@ -76,7 +76,7 @@ class SafariBrowserState(IOSExtraction):
|
||||
|
||||
def _process_browser_state_db(self, db_path):
|
||||
self._recover_sqlite_db_if_needed(db_path)
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn = self._open_sqlite_db(db_path)
|
||||
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sqlite3
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.url import URL
|
||||
@@ -115,7 +114,7 @@ class SafariHistory(IOSExtraction):
|
||||
|
||||
def _process_history_db(self, history_path):
|
||||
self._recover_sqlite_db_if_needed(history_path)
|
||||
conn = sqlite3.connect(history_path)
|
||||
conn = self._open_sqlite_db(history_path)
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
|
||||
@@ -83,7 +83,7 @@ class Shortcuts(IOSExtraction):
|
||||
)
|
||||
self.log.info("Found Shortcuts database at path: %s", self.file_path)
|
||||
|
||||
conn = sqlite3.connect(self.file_path)
|
||||
conn = self._open_sqlite_db(self.file_path)
|
||||
conn.text_factory = bytes
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
|
||||
@@ -44,25 +44,33 @@ class SMS(IOSExtraction):
|
||||
def serialize(self, record: dict) -> Union[dict, list]:
|
||||
text = record["text"].replace("\n", "\\n")
|
||||
sms_data = f"{record['service']}: {record['guid']} \"{text}\" from {record['phone_number']} ({record['account']})"
|
||||
return [
|
||||
records = [
|
||||
{
|
||||
"timestamp": record["isodate"],
|
||||
"module": self.__class__.__name__,
|
||||
"event": "sms_received",
|
||||
"data": sms_data,
|
||||
},
|
||||
{
|
||||
"timestamp": record["isodate_read"],
|
||||
"module": self.__class__.__name__,
|
||||
"event": "sms_read",
|
||||
"data": sms_data,
|
||||
},
|
||||
]
|
||||
# If the message was read, we add an extra event.
|
||||
if record["isodate_read"]:
|
||||
records.append(
|
||||
{
|
||||
"timestamp": record["isodate_read"],
|
||||
"module": self.__class__.__name__,
|
||||
"event": "sms_read",
|
||||
"data": sms_data,
|
||||
}
|
||||
)
|
||||
return records
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
for message in self.results:
|
||||
alert = "ALERT: State-sponsored attackers may be targeting your iPhone"
|
||||
if message.get("text", "").startswith(alert):
|
||||
alert_old = "ALERT: State-sponsored attackers may be targeting your iPhone"
|
||||
alert_new = "ALERT: Apple detected a targeted mercenary spyware attack against your iPhone"
|
||||
if message.get("text", "").startswith(alert_old) or message.get(
|
||||
"text", ""
|
||||
).startswith(alert_new):
|
||||
self.log.warning(
|
||||
"Apple warning about state-sponsored attack received on the %s",
|
||||
message["isodate"],
|
||||
@@ -86,7 +94,7 @@ class SMS(IOSExtraction):
|
||||
self.log.info("Found SMS database at path: %s", self.file_path)
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(self.file_path)
|
||||
conn = self._open_sqlite_db(self.file_path)
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
@@ -103,7 +111,7 @@ class SMS(IOSExtraction):
|
||||
conn.close()
|
||||
if "database disk image is malformed" in str(exc):
|
||||
self._recover_sqlite_db_if_needed(self.file_path, forced=True)
|
||||
conn = sqlite3.connect(self.file_path)
|
||||
conn = self._open_sqlite_db(self.file_path)
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
import sqlite3
|
||||
from base64 import b64encode
|
||||
from typing import Optional, Union
|
||||
|
||||
@@ -56,6 +55,12 @@ class SMSAttachments(IOSExtraction):
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
for attachment in self.results:
|
||||
# Check for known malicious filenames.
|
||||
if self.indicators and self.indicators.check_file_path(
|
||||
attachment["filename"]
|
||||
):
|
||||
self.detected.append(attachment)
|
||||
|
||||
if (
|
||||
attachment["filename"].startswith("/var/tmp/")
|
||||
and attachment["filename"].endswith("-1")
|
||||
@@ -72,7 +77,7 @@ class SMSAttachments(IOSExtraction):
|
||||
self._find_ios_database(backup_ids=SMS_BACKUP_IDS, root_paths=SMS_ROOT_PATHS)
|
||||
self.log.info("Found SMS database at path: %s", self.file_path)
|
||||
|
||||
conn = sqlite3.connect(self.file_path)
|
||||
conn = self._open_sqlite_db(self.file_path)
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
|
||||
@@ -95,7 +95,7 @@ class TCC(IOSExtraction):
|
||||
self.detected.append(result)
|
||||
|
||||
def process_db(self, file_path):
|
||||
conn = sqlite3.connect(file_path)
|
||||
conn = self._open_sqlite_db(file_path)
|
||||
cur = conn.cursor()
|
||||
db_version = "v3"
|
||||
try:
|
||||
|
||||
@@ -73,7 +73,7 @@ class WebkitResourceLoadStatistics(IOSExtraction):
|
||||
|
||||
self._recover_sqlite_db_if_needed(db_path)
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn = self._open_sqlite_db(db_path)
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
import sqlite3
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import check_for_links, convert_mactime_to_iso
|
||||
@@ -69,7 +68,7 @@ class Whatsapp(IOSExtraction):
|
||||
)
|
||||
self.log.info("Found WhatsApp database at path: %s", self.file_path)
|
||||
|
||||
conn = sqlite3.connect(self.file_path)
|
||||
conn = self._open_sqlite_db(self.file_path)
|
||||
cur = conn.cursor()
|
||||
|
||||
# Query all messages and join tables which can contain media attachments
|
||||
|
||||
@@ -31,7 +31,7 @@ install_requires =
|
||||
iOSbackup >=0.9.923
|
||||
adb-shell >=0.4.3
|
||||
libusb1 >=3.0.0
|
||||
cryptography >=38.0.1
|
||||
cryptography >=42.0.5
|
||||
pyyaml >=6.0
|
||||
pyahocorasick >= 2.0.0
|
||||
|
||||
|
||||
45
tests/android/test_artifact_dumpstate.py
Normal file
45
tests/android/test_artifact_dumpstate.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2023 The MVT Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
from mvt.android.artifacts.dumpstate_artifact import DumpStateArtifact
|
||||
|
||||
from ..utils import get_artifact
|
||||
|
||||
|
||||
class TestAndroidArtifactDumpState:
|
||||
def _parse_dump_state(self):
|
||||
"""
|
||||
Load the test artifact
|
||||
"""
|
||||
file = get_artifact("android_data/bugreport/dumpstate.txt")
|
||||
with open(file, "rb") as f:
|
||||
data = f.read()
|
||||
dumpstate = DumpStateArtifact()
|
||||
dumpstate.parse_dumpstate(data)
|
||||
return dumpstate
|
||||
|
||||
def test_extract_dumpstate_sections(self):
|
||||
"""
|
||||
Test parsing of dumpstate sections
|
||||
"""
|
||||
dumpstate = self._parse_dump_state()
|
||||
assert len(dumpstate.dumpstate_sections) == 4
|
||||
|
||||
assert len(dumpstate.dumpstate_header) == 4
|
||||
assert dumpstate.dumpstate_header.get(b"Bugreport format version") == b"2.0"
|
||||
|
||||
for section in dumpstate.dumpstate_sections:
|
||||
if section["section_name"] == b"SYSTEM LOG":
|
||||
assert len(section["lines"]) == 5
|
||||
assert section["lines"][0].startswith(b"--------- beginning of system")
|
||||
|
||||
elif section["section_name"] == b"MODEM CRASH HISTORY":
|
||||
# Test parsing where section only has an error message
|
||||
assert len(section["lines"]) == 1
|
||||
assert (
|
||||
section["lines"][0]
|
||||
== b"*** /data/tombstones//modem/mcrash_history: No such file or directory"
|
||||
)
|
||||
|
||||
assert len(dumpstate.unparsed_lines) == 11
|
||||
@@ -26,6 +26,18 @@ class TestDumpsysAccessibilityArtifact:
|
||||
== "com.android.settings/com.samsung.android.settings.development.gpuwatch.GPUWatchInterceptor"
|
||||
)
|
||||
|
||||
def test_parsing_v14_aosp_format(self):
|
||||
da = DumpsysAccessibilityArtifact()
|
||||
file = get_artifact("android_data/dumpsys_accessibility_v14_or_later.txt")
|
||||
with open(file) as f:
|
||||
data = f.read()
|
||||
|
||||
assert len(da.results) == 0
|
||||
da.parse(data)
|
||||
assert len(da.results) == 1
|
||||
assert da.results[0]["package_name"] == "com.malware.accessibility"
|
||||
assert da.results[0]["service"] == "com.malware.service.malwareservice"
|
||||
|
||||
def test_ioc_check(self, indicator_file):
|
||||
da = DumpsysAccessibilityArtifact()
|
||||
file = get_artifact("android_data/dumpsys_accessibility.txt")
|
||||
|
||||
42
tests/android/test_artifact_dumpsys_packages.py
Normal file
42
tests/android/test_artifact_dumpsys_packages.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2023 The MVT Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
import logging
|
||||
|
||||
from mvt.android.artifacts.dumpsys_packages import DumpsysPackagesArtifact
|
||||
from mvt.common.indicators import Indicators
|
||||
|
||||
from ..utils import get_artifact
|
||||
|
||||
|
||||
class TestDumpsysPackagesArtifact:
|
||||
def test_parsing(self):
|
||||
dpa = DumpsysPackagesArtifact()
|
||||
file = get_artifact("android_data/dumpsys_packages.txt")
|
||||
with open(file) as f:
|
||||
data = f.read()
|
||||
|
||||
assert len(dpa.results) == 0
|
||||
dpa.parse(data)
|
||||
assert len(dpa.results) == 2
|
||||
assert (
|
||||
dpa.results[0]["package_name"]
|
||||
== "com.samsung.android.provider.filterprovider"
|
||||
)
|
||||
assert dpa.results[0]["version_name"] == "5.0.07"
|
||||
|
||||
def test_ioc_check(self, indicator_file):
|
||||
dpa = DumpsysPackagesArtifact()
|
||||
file = get_artifact("android_data/dumpsys_packages.txt")
|
||||
with open(file) as f:
|
||||
data = f.read()
|
||||
dpa.parse(data)
|
||||
|
||||
ind = Indicators(log=logging.getLogger())
|
||||
ind.parse_stix2(indicator_file)
|
||||
ind.ioc_collections[0]["app_ids"].append("com.sec.android.app.DataCreate")
|
||||
dpa.indicators = ind
|
||||
assert len(dpa.detected) == 0
|
||||
dpa.check_indicators()
|
||||
assert len(dpa.detected) == 1
|
||||
87
tests/android_androidqf/test_packages.py
Normal file
87
tests/android_androidqf/test_packages.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2023 The MVT Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
from mvt.android.modules.androidqf.packages import Packages
|
||||
from mvt.common.module import run_module
|
||||
|
||||
from ..utils import get_android_androidqf, list_files
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def data_path():
|
||||
return get_android_androidqf()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def parent_data_path(data_path):
|
||||
return Path(data_path).absolute().parent.as_posix()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def file_list(data_path):
|
||||
return list_files(data_path)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def module(parent_data_path, file_list):
|
||||
m = Packages(target_path=parent_data_path, log=logging)
|
||||
m.from_folder(parent_data_path, file_list)
|
||||
return m
|
||||
|
||||
|
||||
class TestAndroidqfPackages:
|
||||
def test_packages_list(self, module):
|
||||
run_module(module)
|
||||
|
||||
# There should just be 7 packages listed, no detections
|
||||
assert len(module.results) == 7
|
||||
assert len(module.timeline) == 0
|
||||
assert len(module.detected) == 0
|
||||
|
||||
def test_non_appstore_warnings(self, caplog, module):
|
||||
run_module(module)
|
||||
|
||||
# Not a super test to be searching logs for this but heuristic detections not yet formalised
|
||||
assert (
|
||||
'Found a non-system package installed via adb or another method: "com.whatsapp"'
|
||||
in caplog.text
|
||||
)
|
||||
assert (
|
||||
'Found a package installed via a browser (installer="com.google.android.packageinstaller"): '
|
||||
'"app.revanced.manager.flutter"' in caplog.text
|
||||
)
|
||||
assert (
|
||||
'Found a package installed via a third party store (installer="org.fdroid.fdroid"): "org.nuclearfog.apollo"'
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
def test_packages_ioc_package_names(self, module, indicators_factory):
|
||||
module.indicators = indicators_factory(app_ids=["com.malware.blah"])
|
||||
|
||||
run_module(module)
|
||||
|
||||
assert len(module.detected) == 1
|
||||
assert module.detected[0]["name"] == "com.malware.blah"
|
||||
assert module.detected[0]["matched_indicator"]["value"] == "com.malware.blah"
|
||||
|
||||
def test_packages_ioc_sha256(self, module, indicators_factory):
|
||||
module.indicators = indicators_factory(
|
||||
files_sha256=[
|
||||
"31037a27af59d4914906c01ad14a318eee2f3e31d48da8954dca62a99174e3fa"
|
||||
]
|
||||
)
|
||||
|
||||
run_module(module)
|
||||
|
||||
assert len(module.detected) == 1
|
||||
assert module.detected[0]["name"] == "com.malware.muahaha"
|
||||
assert (
|
||||
module.detected[0]["matched_indicator"]["value"]
|
||||
== "31037a27af59d4914906c01ad14a318eee2f3e31d48da8954dca62a99174e3fa"
|
||||
)
|
||||
@@ -1,3 +1,25 @@
|
||||
========================================================
|
||||
== dumpstate: 2024-04-21 10:00:11
|
||||
========================================================
|
||||
|
||||
Build: TP1A.220624.014
|
||||
Uptime: up 0 weeks, 0 days, 0 hours, 20 minutes, load average: 20.00, 19.92, 15.46
|
||||
Bugreport format version: 2.0
|
||||
Dumpstate info: id=1 pid=21015 dry_run=0 parallel_run=1 args=/system/bin/dumpstate -S bugreport_mode=
|
||||
|
||||
------ DUMPSYS CRITICAL (/system/bin/dumpsys) ------
|
||||
-------------------------------------------------------------------------------
|
||||
DUMP OF SERVICE CRITICAL SurfaceFlinger:
|
||||
now = 1202781815070
|
||||
Build configuration: [sf PRESENT_TIME_OFFSET=0 FORCE_HWC_FOR_RBG_TO_YUV=1 MAX_VIRT_DISPLAY_DIM=0 RUNNING_WITHOUT_SYNC_FRAMEWORK=0 NUM_FRAMEBUFFER_SURFACE_BUFFERS=3]
|
||||
|
||||
Display identification data:
|
||||
Display 0 (HWC display 0): no identification data
|
||||
|
||||
Wide-Color information:
|
||||
Device has wide color built-in display: 0
|
||||
Device uses color management: 1
|
||||
|
||||
Currently running services:
|
||||
AAS
|
||||
AODManagerService
|
||||
@@ -246,6 +268,16 @@ Packages:
|
||||
com.instagram.direct.share.handler.DirectMultipleExternalMediaShareActivity
|
||||
com.instagram.share.handleractivity.ClipsShareHandlerActivity
|
||||
com.instagram.direct.share.handler.DirectMultipleExternalMediaShareActivityInterop
|
||||
|
||||
|
||||
|
||||
------ 0.557s was the duration of 'DUMPSYS CRITICAL' ------
|
||||
------ 0.023s was the duration of 'DUMPSYS CRITICAL PROTO' ------
|
||||
------ SERIALIZE PERFETTO TRACE (perfetto --save-for-bugreport) ------
|
||||
------ 0.036s was the duration of 'SERIALIZE PERFETTO TRACE' ------
|
||||
------ End of SERIALIZE PERFETTO TRACE (perfetto --save-for-bugreport) ------
|
||||
------ MODEM CRASH HISTORY (/data/tombstones//modem/mcrash_history) ------
|
||||
*** /data/tombstones//modem/mcrash_history: No such file or directory
|
||||
------ SYSTEM LOG (logcat -v threadtime -v printable -v uid -d *:v) ------
|
||||
--------- beginning of system
|
||||
05-28 09:44:19.845 root 578 578 I vold : Vold 3.0 (the awakening) firing up
|
||||
05-28 09:44:19.845 root 578 578 D vold : Detected support for: exfat ext4 f2fs ntfs vfat
|
||||
05-28 09:44:19.849 root 578 578 W vold : [libfs_mgr]Warning: unknown flag: resize
|
||||
------ 0.417s was the duration of 'SYSTEM LOG' ------
|
||||
@@ -0,0 +1,46 @@
|
||||
DUMP OF SERVICE accessibility:
|
||||
Service host process PID: 633
|
||||
Threads in use: 0/14
|
||||
Client PIDs: 2445, 2428, 2405, 2235, 2215, 2164, 2148, 2130, 2104, 2062, 1879, 1824, 1736, 1748, 1746, 1617, 1605, 1606, 1582, 1538, 1523, 1474, 1307, 1237, 1118, 1081, 1076, 1023, 997, 978, 882, 187
|
||||
ACCESSIBILITY MANAGER (dumpsys accessibility)
|
||||
|
||||
currentUserId=0
|
||||
hasWindowMagnificationConnection=false
|
||||
Magnifier on display#0
|
||||
MagnificationConfig[mode: 1, activated: false, scale: 1.0, centerX: 360.0, centerY: 640.0]
|
||||
Magnification region=SkRegion((3,3,717,1277))
|
||||
IdOfLastServiceToMagnify=-1
|
||||
SupportWindowMagnification=true
|
||||
WindowMagnificationConnectionState=DISCONNECTED
|
||||
User state[
|
||||
attributes:{id=0, touchExplorationEnabled=false, serviceHandlesDoubleTap=false, requestMultiFingerGestures=false, requestTwoFingerPassthrough=false, sendMotionEventsEnabledfalse, displayMagnificationEnabled=false, autoclickEnabled=false, nonInteractiveUiTimeout=0, interactiveUiTimeout=0, installedServiceCount=2, magnificationModes={0=1}, magnificationCapabilities=3, audioDescriptionByDefaultEnabled=false, magnificationFollowTypingEnabled=true, alwaysOnMagnificationEnabled=true}
|
||||
shortcut key:{}
|
||||
button:{}
|
||||
button target:{null}
|
||||
Bound services:{Service[label=Accessibility service, feedbackType[FEEDBACK_SPOKEN, FEEDBACK_HAPTIC, FEEDBACK_AUDIBLE, FEEDBACK_VISUAL, FEEDBACK_GENERIC, FEEDBACK_BRAILLE], capabilities=11, eventTypes=TYPES_ALL_MASK, notificationTimeout=1000, requestA11yBtn=false]}
|
||||
Enabled services:{{com.malware.accessibility/com.malware.service.malwareservice}}
|
||||
Binding services:{}
|
||||
Crashed services:{}
|
||||
Client list info:{
|
||||
Client list callbacks: 6
|
||||
Client list killed: false
|
||||
Client list broadcasts count: -1
|
||||
Registered clients:{
|
||||
[com.malware.accessibility][com.android.launcher3][com.android.systemui][com.android.launcher3][com.android.settings.intelligence][com.android.inputmethod.latin]}]
|
||||
|
||||
Window attributes:[{1=AccessibilityWindowAttributes{mAccessibilityWindowTitle=nullmLocales=[en_US]}, 2=AccessibilityWindowAttributes{mAccessibilityWindowTitle=nullmLocales=[en_US]}, 3=AccessibilityWindowAttributes{mAccessibilityWindowTitle=nullmLocales=[en_US]}, 4=AccessibilityWindowAttributes{mAccessibilityWindowTitle=nullmLocales=[en_US]}, 5=AccessibilityWindowAttributes{mAccessibilityWindowTitle=nullmLocales=[en_US]}, 6=AccessibilityWindowAttributes{mAccessibilityWindowTitle=nullmLocales=[en_US]}, 7=AccessibilityWindowAttributes{mAccessibilityWindowTitle=HomemLocales=[en_US]}, 9=AccessibilityWindowAttributes{mAccessibilityWindowTitle=SettingsmLocales=[en_US]}, 10=AccessibilityWindowAttributes{mAccessibilityWindowTitle=Settings SuggestionsmLocales=[en_US]}}]
|
||||
A11yInputFilter Info :
|
||||
Enabled features of Display [0] = [KeyboardInterceptor]
|
||||
Global client list info:{
|
||||
Client list callbacks: 4
|
||||
Client list killed: false
|
||||
Client list broadcasts count: -1
|
||||
Registered clients:{
|
||||
[com.android.permissioncontroller][com.android.dynsystem, com.android.server.telecom, com.android.keychain, com.android.settings, com.android.localtransport, com.android.wallpaperbackup, com.android.inputdevices, com.android.providers.settings, android, com.android.emulator.multidisplay, com.android.location.fused][com.android.emulator.multidisplay, com.android.wallpaperbackup, com.android.settings, com.android.keychain, com.android.dynsystem, com.android.inputdevices, com.android.providers.settings, com.android.localtransport, com.android.server.telecom, android, com.android.location.fused][com.android.systemui]
|
||||
|
||||
Proxy manager state:
|
||||
Number of proxy connections: 0
|
||||
Registered proxy connections:
|
||||
Accessibility Display Listener:
|
||||
SystemUI uid: 10076
|
||||
1 valid display: 0
|
||||
233
tests/artifacts/androidqf/packages.json
Normal file
233
tests/artifacts/androidqf/packages.json
Normal file
@@ -0,0 +1,233 @@
|
||||
[
|
||||
{
|
||||
"name": "com.whatsapp",
|
||||
"files": [
|
||||
{
|
||||
"path": "/data/app/~~/com.whatsapp-~~/base.apk",
|
||||
"local_name": "",
|
||||
"md5": "5870bd06e642de410c54705226ecfa9a",
|
||||
"sha1": "6cb06e9ab5619345f930c2b2096b4dd013a10ec9",
|
||||
"sha256": "744ed47f8176ec423840344c33e88bd2c96e8988cda0797f3415bb5229efc12b",
|
||||
"sha512": "f222f742b0bd302c82e202bc78f7ff8b2de4acfc8d606994245ffa80998b003e215cad82cae023abe4f65c0da0a56fa9890e9bb3a753af6dac848a753ac07aee",
|
||||
"error": "",
|
||||
"verified_certificate": true,
|
||||
"certificate": {
|
||||
"Md5": "556c6019249bbc0cab70495178d3a9d1",
|
||||
"Sha1": "38a0f7d505fe18fec64fbf343ecaaaf310dbd799",
|
||||
"Sha256": "3987d043d10aefaf5a8710b3671418fe57e0e19b653c9df82558feb5ffce5d44",
|
||||
"ValidFrom": "2010-06-25T23:07:16Z",
|
||||
"ValidTo": "2044-02-15T23:07:16Z",
|
||||
"Issuer": "C=US, ST=California, L=Santa Clara, O=WhatsApp Inc., OU=Engineering, CN=Brian Acton",
|
||||
"Subject": "C=US, ST=California, L=Santa Clara, O=WhatsApp Inc., OU=Engineering, CN=Brian Acton",
|
||||
"SignatureAlgorithm": "DSA-SHA1",
|
||||
"SerialNumber": 1277507236
|
||||
},
|
||||
"certificate_error": "",
|
||||
"trusted_certificate": true
|
||||
}
|
||||
],
|
||||
"installer": "null",
|
||||
"uid": 10271,
|
||||
"disabled": false,
|
||||
"system": false,
|
||||
"third_party": true
|
||||
},
|
||||
{
|
||||
"name": "app.revanced.manager.flutter",
|
||||
"files": [
|
||||
{
|
||||
"path": "/data/app/~~==/app.revanced.manager.flutter-==/base.apk",
|
||||
"local_name": "",
|
||||
"md5": "aae9b55c6f2592233518bb5a173e8505",
|
||||
"sha1": "9185a83dae0fc8a0ba79f89f3c84fe8a038f93af",
|
||||
"sha256": "6ddb76f6180ca8bc0a11d5b343ac9ad8f137a351f20c080e989ca4310973d319",
|
||||
"sha512": "923a57d4cdf2e7d48539307abbd12f982d61f393a1d058ceef0f6109301d21fedf0fe73c667f8add37fb35da570ac35c6b911360d9bf0389aa0bbbd53103ff46",
|
||||
"error": "",
|
||||
"verified_certificate": true,
|
||||
"certificate": {
|
||||
"Md5": "f822d70f449d798f0688e2c7358a429c",
|
||||
"Sha1": "93adc8e2bd1687644f1143e184bcbfd57912ff2c",
|
||||
"Sha256": "b6362c6ea7888efd15c0800f480786ad0f5b133b4f84e12d46afba5f9eac1223",
|
||||
"ValidFrom": "2022-09-14T11:45:44Z",
|
||||
"ValidTo": "2050-01-30T11:45:44Z",
|
||||
"Issuer": "C=Unknown, ST=Unknown, L=Unknown, O=ReVanced, OU=ReVanced, CN=ReVanced Manager",
|
||||
"Subject": "C=Unknown, ST=Unknown, L=Unknown, O=ReVanced, OU=ReVanced, CN=ReVanced Manager",
|
||||
"SignatureAlgorithm": "SHA256-RSA",
|
||||
"SerialNumber": 710526530
|
||||
},
|
||||
"certificate_error": "",
|
||||
"trusted_certificate": false
|
||||
}
|
||||
],
|
||||
"installer": "com.google.android.packageinstaller",
|
||||
"uid": 10266,
|
||||
"disabled": false,
|
||||
"system": false,
|
||||
"third_party": true
|
||||
},
|
||||
{
|
||||
"name": "com.google.android.youtube",
|
||||
"files": [
|
||||
{
|
||||
"path": "/data/app/~~==/com.google.android.youtube-==/base.apk",
|
||||
"local_name": "",
|
||||
"md5": "3ec11b187ec6195e9ca4b5be671eba34",
|
||||
"sha1": "33a9a89836690966498ba106283e76eff430365b",
|
||||
"sha256": "a81b6392ab855905763272cf1a248b0d09fc675a91eabe7ef4ed589356a35241",
|
||||
"sha512": "c736fbd07fe52539d8e96f6489a49c915c2bac472f0203f6187d167e2e3623f07db9e70b0fbc0494f6eeffb66a4cf71da56ad70503dc8138512faa3c1e847174",
|
||||
"error": "",
|
||||
"verified_certificate": true,
|
||||
"certificate": {
|
||||
"Md5": "d046fc5d1fc3cd0e57c5444097cd5449",
|
||||
"Sha1": "24bb24c05e47e0aefa68a58a766179d9b613a600",
|
||||
"Sha256": "3d7a1223019aa39d9ea0e3436ab7c0896bfb4fb679f4de5fe7c23f326c8f994a",
|
||||
"ValidFrom": "2008-12-02T02:07:58Z",
|
||||
"ValidTo": "2036-04-19T02:07:58Z",
|
||||
"Issuer": "C=US, ST=CA, L=Mountain View, O=Google, Inc, OU=Google, Inc, CN=Unknown",
|
||||
"Subject": "C=US, ST=CA, L=Mountain View, O=Google, Inc, OU=Google, Inc, CN=Unknown",
|
||||
"SignatureAlgorithm": "MD5-RSA",
|
||||
"SerialNumber": 1228183678
|
||||
},
|
||||
"certificate_error": "",
|
||||
"trusted_certificate": true
|
||||
}
|
||||
],
|
||||
"installer": "com.google.android.packageinstaller",
|
||||
"uid": 10194,
|
||||
"disabled": false,
|
||||
"system": true,
|
||||
"third_party": false
|
||||
},
|
||||
{
|
||||
"name": "org.fdroid.fdroid",
|
||||
"files": [
|
||||
{
|
||||
"path": "/data/app/~~-==/org.fdroid.fdroid-==/base.apk",
|
||||
"local_name": "",
|
||||
"md5": "1f7524d15b3d229e5e89af609551e640",
|
||||
"sha1": "4ce271a8ac2afb9f584f1deb165f1ab4768c50b0",
|
||||
"sha256": "dc3bb88f6419ee7dde7d1547a41569aa03282fe00e0dc43ce035efd7c9d27d75",
|
||||
"sha512": "40e9bfaf6c2833078e370c85001adcb7493851a5146d2b4067a9909266a0d7904f80825f040c8c6e0cb59ec6e8c0825d522ff963f6db780b049a24d47f81b289",
|
||||
"error": "",
|
||||
"verified_certificate": true,
|
||||
"certificate": {
|
||||
"Md5": "17c55c628056e193e95644e989792786",
|
||||
"Sha1": "05f2e65928088981b317fc9a6dbfe04b0fa13b4e",
|
||||
"Sha256": "43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab",
|
||||
"ValidFrom": "2010-07-23T17:10:24Z",
|
||||
"ValidTo": "2037-12-08T17:10:24Z",
|
||||
"Issuer": "C=UK, ST=Unknown, L=Wetherby, O=Unknown, OU=Unknown, CN=Ciaran Gultnieks",
|
||||
"Subject": "C=UK, ST=Unknown, L=Wetherby, O=Unknown, OU=Unknown, CN=Ciaran Gultnieks",
|
||||
"SignatureAlgorithm": "SHA1-RSA",
|
||||
"SerialNumber": 1279905024
|
||||
},
|
||||
"certificate_error": "",
|
||||
"trusted_certificate": false
|
||||
}
|
||||
],
|
||||
"installer": "com.google.android.packageinstaller",
|
||||
"uid": 10267,
|
||||
"disabled": false,
|
||||
"system": false,
|
||||
"third_party": true
|
||||
},
|
||||
{
|
||||
"name": "org.nuclearfog.apollo",
|
||||
"files": [
|
||||
{
|
||||
"path": "/data/app/~~==/org.nuclearfog.apollo-==/base.apk",
|
||||
"local_name": "",
|
||||
"md5": "69f611758cc911f472fcabad6151684a",
|
||||
"sha1": "1f5e450ef1901e245d4828735e0e93f0f94fb4da",
|
||||
"sha256": "00bdfc80a397b449bef89dd2051ddd3c9d2a64e954176420b40c90a2af956799",
|
||||
"sha512": "2af8037e0e226cba9f32227f709afc32fd8871c0077f73d00d59353d67ab843cb6641a5e0101d494699aeb91dcd136767fe9d76b30df65e1a1153f3c5b51a837",
|
||||
"error": "",
|
||||
"verified_certificate": true,
|
||||
"certificate": {
|
||||
"Md5": "2bef3d492d62fad190a8b6b7d71d42a4",
|
||||
"Sha1": "cad23563b5be0c33611d827ee0da6ad5ef3be39a",
|
||||
"Sha256": "e1a418c51baa829917daa2e86d7509a8a10470e44280c20146b70ea550bfe1ab",
|
||||
"ValidFrom": "2022-01-15T20:17:10Z",
|
||||
"ValidTo": "2047-01-09T20:17:10Z",
|
||||
"Issuer": "C=DE, ST=Saarland, CN=nuclearfog",
|
||||
"Subject": "C=DE, ST=Saarland, CN=nuclearfog",
|
||||
"SignatureAlgorithm": "SHA256-RSA",
|
||||
"SerialNumber": 75365821
|
||||
},
|
||||
"certificate_error": "",
|
||||
"trusted_certificate": false
|
||||
}
|
||||
],
|
||||
"installer": "org.fdroid.fdroid",
|
||||
"uid": 10272,
|
||||
"disabled": false,
|
||||
"system": false,
|
||||
"third_party": true
|
||||
},
|
||||
{
|
||||
"name": "com.malware.blah",
|
||||
"files": [
|
||||
{
|
||||
"path": "/data/app/~~-==/com.malware.blah-==/base.apk",
|
||||
"local_name": "",
|
||||
"md5": "349ba2de140fccaf2ed2ac20f66e711f",
|
||||
"sha1": "2cc5b4a70ada9229fb50d30f525392f2d66f58d6",
|
||||
"sha256": "79a3569fbb63a9167ad8a2dad963616bb01474c87d769c7640f6d6810c448eae",
|
||||
"sha512": "df1bbbfa6e895054b36093548558ee0d9fbf61ef09e617d3b3b158ba9f9c11825dbbf7e84711331afb80fc24ea0e5aa07a9db1919932c109c34fefec3c02d184",
|
||||
"error": "",
|
||||
"verified_certificate": true,
|
||||
"certificate": {
|
||||
"Md5": "54d5b5aca1e7e76bb1a26c61a9381b93",
|
||||
"Sha1": "4ba9d1f82adb7be841bcf53b03ddae857747199a",
|
||||
"Sha256": "c3e8cafdcd10e7cd9b2ec67f7abd4447b840431126066f6b16ed42151d2b4d64",
|
||||
"ValidFrom": "2021-01-15T22:03:53Z",
|
||||
"ValidTo": "2051-01-15T22:03:53Z",
|
||||
"Issuer": "C=US, ST=California, L=Mountain View, O=Google Inc., OU=Android, CN=Android",
|
||||
"Subject": "C=US, ST=California, L=Mountain View, O=Google Inc., OU=Android, CN=Android",
|
||||
"SignatureAlgorithm": "SHA256-RSA",
|
||||
"SerialNumber": 955466096586930338769951715633687128507538251257
|
||||
},
|
||||
"certificate_error": "",
|
||||
"trusted_certificate": false
|
||||
}
|
||||
],
|
||||
"installer": "null",
|
||||
"uid": 10058,
|
||||
"disabled": false,
|
||||
"system": true,
|
||||
"third_party": false
|
||||
},
|
||||
{
|
||||
"name": "com.malware.muahaha",
|
||||
"files": [
|
||||
{
|
||||
"path": "/data/app/~~-==/com.malware.meh-==/base.apk",
|
||||
"local_name": "",
|
||||
"md5": "349ba2de140fccaf2ed2ac20f66e711f",
|
||||
"sha1": "2cc5b4a70ada9229fb50d30f525392f2d66f58d6",
|
||||
"sha256": "31037a27af59d4914906c01ad14a318eee2f3e31d48da8954dca62a99174e3fa",
|
||||
"sha512": "df1bbbfa6e895054b36093548558ee0d9fbf61ef09e617d3b3b158ba9f9c11825dbbf7e84711331afb80fc24ea0e5aa07a9db1919932c109c34fefec3c02d184",
|
||||
"error": "",
|
||||
"verified_certificate": true,
|
||||
"certificate": {
|
||||
"Md5": "54d5b5aca1e7e76bb1a26c61a9381b93",
|
||||
"Sha1": "4ba9d1f82adb7be841bcf53b03ddae857747199a",
|
||||
"Sha256": "31037a27af59d4914906c01ad14a318eee2f3e31d48da8954dca62a99174e3fa",
|
||||
"ValidFrom": "2021-01-15T22:03:53Z",
|
||||
"ValidTo": "2051-01-15T22:03:53Z",
|
||||
"Issuer": "C=US, ST=California, L=Mountain View, O=Google Inc., OU=Android, CN=Android",
|
||||
"Subject": "C=US, ST=California, L=Mountain View, O=Google Inc., OU=Android, CN=Android",
|
||||
"SignatureAlgorithm": "SHA256-RSA",
|
||||
"SerialNumber": 955466096586930338769951715633687128507538251257
|
||||
},
|
||||
"certificate_error": "",
|
||||
"trusted_certificate": false
|
||||
}
|
||||
],
|
||||
"installer": "null",
|
||||
"uid": 10058,
|
||||
"disabled": false,
|
||||
"system": true,
|
||||
"third_party": false
|
||||
}
|
||||
]
|
||||
Binary file not shown.
@@ -42,6 +42,14 @@ class TestDateConversions:
|
||||
converted = convert_unix_to_utc_datetime(TEST_DATE_EPOCH)
|
||||
assert convert_datetime_to_iso(converted) == TEST_DATE_ISO
|
||||
|
||||
def test_convert_timezone_aware_to_iso(self):
|
||||
assert (
|
||||
convert_datetime_to_iso(
|
||||
datetime.strptime("2024-09-30 11:21:20+0200", "%Y-%m-%d %H:%M:%S%z")
|
||||
)
|
||||
== "2024-09-30 09:21:20.000000"
|
||||
)
|
||||
|
||||
|
||||
class TestHashes:
|
||||
def test_hash_from_file(self):
|
||||
@@ -54,7 +62,7 @@ class TestHashes:
|
||||
def test_hash_from_folder(self):
|
||||
path = os.path.join(get_artifact_folder(), "androidqf")
|
||||
hashes = list(generate_hashes_from_path(path, logging))
|
||||
assert len(hashes) == 5
|
||||
assert len(hashes) == 6
|
||||
# Sort the files to have reliable order for tests.
|
||||
hashes = sorted(hashes, key=lambda x: x["file_path"])
|
||||
assert hashes[0]["file_path"] == os.path.join(path, "backup.ab")
|
||||
|
||||
@@ -8,6 +8,8 @@ import os
|
||||
import pytest
|
||||
|
||||
from .artifacts.generate_stix import generate_test_stix_file
|
||||
import logging
|
||||
from mvt.common.indicators import Indicators
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
@@ -24,3 +26,31 @@ def clean_test_env(request, tmp_path_factory):
|
||||
del os.environ["MVT_STIX2"]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def indicators_factory(indicator_file):
|
||||
def f(
|
||||
domains=[],
|
||||
emails=[],
|
||||
file_names=[],
|
||||
processes=[],
|
||||
app_ids=[],
|
||||
android_property_names=[],
|
||||
files_sha256=[],
|
||||
):
|
||||
|
||||
ind = Indicators(log=logging.getLogger())
|
||||
ind.parse_stix2(indicator_file)
|
||||
|
||||
ind.ioc_collections[0]["domains"].extend(domains)
|
||||
ind.ioc_collections[0]["emails"].extend(emails)
|
||||
ind.ioc_collections[0]["file_names"].extend(file_names)
|
||||
ind.ioc_collections[0]["processes"].extend(processes)
|
||||
ind.ioc_collections[0]["app_ids"].extend(app_ids)
|
||||
ind.ioc_collections[0]["android_property_names"].extend(android_property_names)
|
||||
ind.ioc_collections[0]["files_sha256"].extend(files_sha256)
|
||||
|
||||
return ind
|
||||
|
||||
return f
|
||||
|
||||
@@ -17,7 +17,7 @@ class TestSMSModule:
|
||||
m = SMS(target_path=get_ios_backup_folder())
|
||||
run_module(m)
|
||||
assert len(m.results) == 1
|
||||
assert len(m.timeline) == 2 # SMS received and read events.
|
||||
assert len(m.timeline) == 2
|
||||
assert len(m.detected) == 0
|
||||
|
||||
def test_detection(self, indicator_file):
|
||||
|
||||
@@ -4,31 +4,22 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from mvt.common.indicators import Indicators
|
||||
from mvt.common.module import run_module
|
||||
from mvt.ios.modules.fs.filesystem import Filesystem
|
||||
|
||||
from ..utils import delete_tmp_db_files, get_ios_backup_folder
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def cleanup_tmp_artifacts():
|
||||
ios_backup_folder = get_ios_backup_folder()
|
||||
delete_tmp_db_files(ios_backup_folder)
|
||||
return
|
||||
from ..utils import get_ios_backup_folder
|
||||
|
||||
|
||||
class TestFilesystem:
|
||||
def test_filesystem(self, cleanup_tmp_artifacts):
|
||||
def test_filesystem(self):
|
||||
m = Filesystem(target_path=get_ios_backup_folder())
|
||||
run_module(m)
|
||||
assert len(m.results) == 15
|
||||
assert len(m.timeline) == 15
|
||||
assert len(m.detected) == 0
|
||||
|
||||
def test_detection(self, indicator_file, cleanup_tmp_artifacts):
|
||||
def test_detection(self, indicator_file):
|
||||
m = Filesystem(target_path=get_ios_backup_folder())
|
||||
ind = Indicators(log=logging.getLogger())
|
||||
ind.parse_stix2(indicator_file)
|
||||
|
||||
Reference in New Issue
Block a user