mirror of
https://github.com/mvt-project/mvt.git
synced 2026-02-14 17:42:46 +00:00
Compare commits
161 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc5d386be7 | ||
|
|
03efc8494b | ||
|
|
0b3f529cfa | ||
|
|
9bdef6ede4 | ||
|
|
fc9a27d030 | ||
|
|
f5f3660d82 | ||
|
|
712f5bcb9b | ||
|
|
ac26aa964a | ||
|
|
be511dcb51 | ||
|
|
b44c67e699 | ||
|
|
a4d08f8f35 | ||
|
|
6cc67f3c1d | ||
|
|
0d5377597f | ||
|
|
86c79075ff | ||
|
|
9940b1d145 | ||
|
|
b07fb092aa | ||
|
|
639c163297 | ||
|
|
8eb30e3a02 | ||
|
|
cd0e7d9879 | ||
|
|
bdaaf15434 | ||
|
|
699824d9ff | ||
|
|
8cca78d222 | ||
|
|
57cbb0ed56 | ||
|
|
e9cc6b3928 | ||
|
|
6d47d4d416 | ||
|
|
ed54761747 | ||
|
|
71c4ba799f | ||
|
|
09a6f291c0 | ||
|
|
b50be69dd4 | ||
|
|
6fc6102b73 | ||
|
|
3fe5d8dc8d | ||
|
|
fec6210d1b | ||
|
|
6a723e533f | ||
|
|
ed8a5a3845 | ||
|
|
04225a4455 | ||
|
|
5987f218be | ||
|
|
748780476e | ||
|
|
c522b54326 | ||
|
|
0e0e346916 | ||
|
|
69daf3c3cd | ||
|
|
998d87900d | ||
|
|
230f81879a | ||
|
|
df42efb7cb | ||
|
|
0922e569b0 | ||
|
|
03092cf3b7 | ||
|
|
ab63a02c9f | ||
|
|
a833dda581 | ||
|
|
189b1d7fc6 | ||
|
|
b1b282ac20 | ||
|
|
512c349c2c | ||
|
|
b94ba28873 | ||
|
|
564efc3629 | ||
|
|
9c62e6e4d6 | ||
|
|
153f6cce02 | ||
|
|
47f9a0104c | ||
|
|
bdad23feee | ||
|
|
5416b66915 | ||
|
|
e2936c3d33 | ||
|
|
3483ca1584 | ||
|
|
7b107edf1f | ||
|
|
b97ce7651a | ||
|
|
52a204cab6 | ||
|
|
1b335fda1d | ||
|
|
2ad175eae2 | ||
|
|
2d00dca5bd | ||
|
|
c8e50eb958 | ||
|
|
1f049fc8ba | ||
|
|
434738a306 | ||
|
|
06cd640c5e | ||
|
|
fb8a7ca104 | ||
|
|
8d15ff58dd | ||
|
|
eb5f07a75d | ||
|
|
ececf1a6b2 | ||
|
|
851cd52602 | ||
|
|
8db04fc991 | ||
|
|
3d0ba56e1f | ||
|
|
c48a4e8f50 | ||
|
|
001c2998a5 | ||
|
|
5e7c5727af | ||
|
|
883fbaeb88 | ||
|
|
6f0012cede | ||
|
|
458e80ccbb | ||
|
|
c8185fdbd8 | ||
|
|
67eea3edec | ||
|
|
bc86d159b8 | ||
|
|
43b1612dfe | ||
|
|
156f1084f1 | ||
|
|
49e34f6299 | ||
|
|
d88a66dd54 | ||
|
|
d3ed778ae4 | ||
|
|
4c3306c272 | ||
|
|
1c912f68fe | ||
|
|
10a640d3f7 | ||
|
|
c3acc95e9e | ||
|
|
90d05336da | ||
|
|
5513e6e9e3 | ||
|
|
38116f8405 | ||
|
|
59b069f006 | ||
|
|
28e1348aa7 | ||
|
|
034338d1f4 | ||
|
|
09d5eabf2f | ||
|
|
a425d6c511 | ||
|
|
f8897a4f8c | ||
|
|
86eae68bdb | ||
|
|
d2bf348b03 | ||
|
|
25c6c03075 | ||
|
|
cf88740f6a | ||
|
|
eb4810b0ad | ||
|
|
cce9159eda | ||
|
|
e1211991aa | ||
|
|
8ae9ca328c | ||
|
|
0e2eb51732 | ||
|
|
b35cd4bc73 | ||
|
|
1b4f99a31d | ||
|
|
e4e1716729 | ||
|
|
083bc12351 | ||
|
|
cf6d392460 | ||
|
|
95205d8e17 | ||
|
|
1460828c30 | ||
|
|
fa84b3f296 | ||
|
|
e1efaa5467 | ||
|
|
696d42fc6e | ||
|
|
a0e1662726 | ||
|
|
51645bdbc0 | ||
|
|
bb1b108fd7 | ||
|
|
92f9dcb8a5 | ||
|
|
a6fd5fe1f3 | ||
|
|
3e0ef20fcd | ||
|
|
01f3acde2e | ||
|
|
b697874f56 | ||
|
|
41d699f457 | ||
|
|
6fcd40f6b6 | ||
|
|
38bb583a9e | ||
|
|
48ec2d8fa8 | ||
|
|
798805c583 | ||
|
|
24be9e9570 | ||
|
|
adbd95c559 | ||
|
|
8a707c288a | ||
|
|
4c906ad52e | ||
|
|
a2f8030cce | ||
|
|
737007afdb | ||
|
|
33efeda90a | ||
|
|
146f2ae57d | ||
|
|
11bc916854 | ||
|
|
3084876f31 | ||
|
|
f63cb585b2 | ||
|
|
637aebcd89 | ||
|
|
16a0de3af4 | ||
|
|
15fbedccc9 | ||
|
|
e0514b20dd | ||
|
|
b2e9f0361b | ||
|
|
e85c70c603 | ||
|
|
3f8dade610 | ||
|
|
54963b0b59 | ||
|
|
513e2cc704 | ||
|
|
28d57e7178 | ||
|
|
dc8eeb618e | ||
|
|
c282d4341d | ||
|
|
681bae2f66 | ||
|
|
b079246c8a | ||
|
|
82b57f1997 |
14
.github/workflows/python-package.yml
vendored
14
.github/workflows/python-package.yml
vendored
@@ -1,7 +1,7 @@
|
||||
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||
|
||||
name: Python package
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -16,7 +16,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: [3.7, 3.8, 3.9]
|
||||
# python-version: [3.7, 3.8, 3.9]
|
||||
python-version: [3.8, 3.9]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -27,8 +28,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install flake8 pytest safety
|
||||
python -m pip install flake8 pytest safety stix2
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
python -m pip install .
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
@@ -37,7 +39,5 @@ jobs:
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
- name: Safety checks
|
||||
run: safety check
|
||||
|
||||
# - name: Test with pytest
|
||||
# run: |
|
||||
# pytest
|
||||
- name: Test with pytest
|
||||
run: pytest
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -131,3 +131,6 @@ dmypy.json
|
||||
|
||||
# Temporal files
|
||||
*~
|
||||
|
||||
# IDEA Dev Environment
|
||||
.idea
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
[](https://pypi.org/project/mvt/)
|
||||
[](https://docs.mvt.re/en/latest/?badge=latest)
|
||||
[](https://github.com/mvt-project/mvt/actions/workflows/python-package.yml)
|
||||
[](https://pepy.tech/project/mvt)
|
||||
|
||||
Mobile Verification Toolkit (MVT) is a collection of utilities to simplify and automate the process of gathering forensic traces helpful to identify a potential compromise of Android and iOS devices.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
|
||||
@@ -11,18 +11,37 @@ That said, most versions of Android should still allow to locally backup SMS mes
|
||||
Because `mvt-android check-backup` currently only supports checking SMS messages, you can indicate to backup only those:
|
||||
|
||||
```bash
|
||||
adb backup com.android.providers.telephony
|
||||
adb backup -nocompress com.android.providers.telephony
|
||||
```
|
||||
|
||||
In case you nonetheless wish to take a full backup, you can do so with
|
||||
|
||||
```bash
|
||||
adb backup -all
|
||||
adb backup -nocompress -all
|
||||
```
|
||||
|
||||
## Unpack the backup
|
||||
Some recent phones will enforce the utilisation of a password to encrypt the backup archive. In that case, the password will obviously be needed to extract and analyse the data later on.
|
||||
|
||||
In order to unpack the backup, use [Android Backup Extractor (ABE)](https://github.com/nelenkov/android-backup-extractor) to convert it to a readable file format. Make sure that java is installed on your system and use the following command:
|
||||
## Unpack and check the backup
|
||||
|
||||
MVT includes a partial implementation of the Android Backup parsing, because of the implementation difference in the compression algorithm between Java and Python. The `-nocompress` option passed to adb in the section above allows to avoid this issue. You can analyse and extract SMSs containing links from the backup directly with MVT:
|
||||
|
||||
```bash
|
||||
$ mvt-android check-backup --output /path/to/results/ /path/to/backup.ab
|
||||
14:09:45 INFO [mvt.android.cli] Checking ADB backup located at: backup.ab
|
||||
INFO [mvt.android.modules.backup.sms] Running module SMS...
|
||||
INFO [mvt.android.modules.backup.sms] Processing SMS backup file at
|
||||
apps/com.android.providers.telephony/d_f/000000_sms_backup
|
||||
INFO [mvt.android.modules.backup.sms] Extracted a total of 64 SMS messages containing links
|
||||
```
|
||||
|
||||
If the backup is encrypted, MVT will prompt you to enter the password.
|
||||
|
||||
Through the `--iocs` argument you can specify a [STIX2](https://oasis-open.github.io/cti-documentation/stix/intro) file defining a list of malicious indicators to check against the records extracted from the backup by MVT. Any matches will be highlighted in the terminal output.
|
||||
|
||||
## Alternative ways to unpack and check the backup
|
||||
|
||||
If you encounter an issue during the analysis of the backup, you can alternatively use [Android Backup Extractor (ABE)](https://github.com/nelenkov/android-backup-extractor) to convert it to a readable file format. Make sure that java is installed on your system and use the following command:
|
||||
|
||||
```bash
|
||||
java -jar ~/path/to/abe.jar unpack backup.ab backup.tar
|
||||
@@ -33,17 +52,4 @@ If the backup is encrypted, ABE will prompt you to enter the password.
|
||||
|
||||
Alternatively, [ab-decrypt](https://github.com/joernheissler/ab-decrypt) can be used for that purpose.
|
||||
|
||||
## Check the backup
|
||||
|
||||
You can then extract SMSs containing links with MVT:
|
||||
|
||||
```bash
|
||||
$ mvt-android check-backup --output /path/to/results/ /path/to/backup/
|
||||
16:18:38 INFO [mvt.android.cli] Checking ADB backup located at: .
|
||||
INFO [mvt.android.modules.backup.sms] Running module SMS...
|
||||
INFO [mvt.android.modules.backup.sms] Processing SMS backup file at /path/to/backup/apps/com.android.providers.telephony/d_f/000000_sms_backup
|
||||
16:18:39 INFO [mvt.android.modules.backup.sms] Extracted a total of
|
||||
64 SMS messages containing links
|
||||
```
|
||||
|
||||
Through the `--iocs` argument you can specify a [STIX2](https://oasis-open.github.io/cti-documentation/stix/intro) file defining a list of malicious indicators to check against the records extracted from the backup by MVT. Any matches will be highlighted in the terminal output.
|
||||
You can then extract SMSs containing links with MVT by passing the folder path as parameter instead of the `.ab` file: `mvt-android check-backup --output /path/to/results/ /path/to/backup/`.
|
||||
|
||||
@@ -10,6 +10,11 @@ cd mvt
|
||||
docker build -t mvt .
|
||||
```
|
||||
|
||||
Optionally, you may need to specify your platform to Docker in order to build successfully (Apple M1)
|
||||
```bash
|
||||
docker build --platform amd64 -t mvt .
|
||||
```
|
||||
|
||||
Test if the image was created successfully:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -38,7 +38,9 @@ export MVT_STIX2="/home/user/IOC1.stix2:/home/user/IOC2.stix2"
|
||||
|
||||
- The [Amnesty International investigations repository](https://github.com/AmnestyTech/investigations) contains STIX-formatted IOCs for:
|
||||
- [Pegasus](https://en.wikipedia.org/wiki/Pegasus_(spyware)) ([STIX2](https://raw.githubusercontent.com/AmnestyTech/investigations/master/2021-07-18_nso/pegasus.stix2))
|
||||
- [Predator from Cytrox](https://citizenlab.ca/2021/12/pegasus-vs-predator-dissidents-doubly-infected-iphone-reveals-cytrox-mercenary-spyware/) ([STIX2](https://github.com/AmnestyTech/investigations/tree/master/2021-12-16_cytrox/cytrox.stix2))
|
||||
- [This repository](https://github.com/Te-k/stalkerware-indicators) contains IOCs for Android stalkerware including [a STIX MVT-compatible file](https://github.com/Te-k/stalkerware-indicators/blob/master/stalkerware.stix2).
|
||||
- [Predator from Cytrox](https://citizenlab.ca/2021/12/pegasus-vs-predator-dissidents-doubly-infected-iphone-reveals-cytrox-mercenary-spyware/) ([STIX2](https://raw.githubusercontent.com/AmnestyTech/investigations/master/2021-12-16_cytrox/cytrox.stix2))
|
||||
- [This repository](https://github.com/Te-k/stalkerware-indicators) contains IOCs for Android stalkerware including [a STIX MVT-compatible file](https://raw.githubusercontent.com/Te-k/stalkerware-indicators/master/stalkerware.stix2).
|
||||
|
||||
You can automaticallly download the latest public indicator files with the command `mvt-ios download-iocs` or `mvt-android download-iocs`. These commands download the list of indicators listed [here](https://github.com/mvt-project/mvt/blob/main/public_indicators.json) and store them in the [appdir](https://pypi.org/project/appdirs/) folder. They are then loaded automatically by mvt.
|
||||
|
||||
Please [open an issue](https://github.com/mvt-project/mvt/issues/) to suggest new sources of STIX-formatted IOCs.
|
||||
|
||||
@@ -190,7 +190,7 @@ If indicators are provided through the command-line, they are checked against th
|
||||
Backup: :material-check:
|
||||
Full filesystem dump: :material-check:
|
||||
|
||||
This JSON file is created by mvt-ios' `Datausage` module. The module extracts records from a SQLite database located */private/var/wireless/Library/Databases/DataUsage.sqlite*, which contains a history of data usage by processes running on the system. Besides the network statistics, these records are particularly important because they might show traces of malicious process executions and the relevant timeframe. In particular, processes which do not have a valid bundle ID might require particular attention.
|
||||
This JSON file is created by mvt-ios' `Datausage` module. The module extracts records from a SQLite database located */private/var/wireless/Library/Databases/DataUsage.sqlite*, which contains a history of network data usage by processes running on the system. It does not log network traffic through WiFi (the fields `WIFI_IN` and `WIFI_OUT` are always empty), and the `WWAN_IN` and `WWAN_OUT` fields are stored in bytes. Besides the network statistics, these records are particularly important because they might show traces of malicious process executions and the relevant timeframe. In particular, processes which do not have a valid bundle ID might require particular attention.
|
||||
|
||||
If indicators are provided through the command-line, they are checked against the process names. Any matches are stored in *datausage_detected.json*. If running on a full filesystem dump and if the `--fast` flag was not enabled by command-line, mvt-ios will highlight processes which look suspicious and check the presence of a binary file of the same name in the dump.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project 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 getpass
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
from zipfile import ZipFile
|
||||
|
||||
import click
|
||||
from rich.logging import RichHandler
|
||||
|
||||
from mvt.common.help import HELP_MSG_MODULE, HELP_MSG_IOC
|
||||
from mvt.common.help import HELP_MSG_FAST, HELP_MSG_OUTPUT, HELP_MSG_LIST_MODULES
|
||||
from mvt.common.help import HELP_MSG_SERIAL
|
||||
from mvt.common.indicators import Indicators, IndicatorsFileBadFormat
|
||||
from mvt.android.parsers.backup import (AndroidBackupParsingError,
|
||||
InvalidBackupPassword, parse_ab_header,
|
||||
parse_backup_file)
|
||||
from mvt.common.help import (HELP_MSG_FAST, HELP_MSG_IOC,
|
||||
HELP_MSG_LIST_MODULES, HELP_MSG_MODULE,
|
||||
HELP_MSG_OUTPUT, HELP_MSG_SERIAL)
|
||||
from mvt.common.indicators import Indicators, download_indicators_files
|
||||
from mvt.common.logo import logo
|
||||
from mvt.common.module import run_module, save_timeline
|
||||
|
||||
@@ -21,6 +29,7 @@ from .lookups.koodous import koodous_lookup
|
||||
from .lookups.virustotal import virustotal_lookup
|
||||
from .modules.adb import ADB_MODULES
|
||||
from .modules.backup import BACKUP_MODULES
|
||||
from .modules.bugreport import BUGREPORT_MODULES
|
||||
|
||||
# Setup logging using Rich.
|
||||
LOG_FORMAT = "[%(name)s] %(message)s"
|
||||
@@ -46,9 +55,9 @@ def version():
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# Download APKs
|
||||
# Command: download-apks
|
||||
#==============================================================================
|
||||
@cli.command("download-apks", help="Download all or non-safelisted installed APKs installed on the device")
|
||||
@cli.command("download-apks", help="Download all or only non-system installed APKs")
|
||||
@click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL)
|
||||
@click.option("--all-apks", "-a", is_flag=True,
|
||||
help="Extract all packages installed on the phone, including system packages")
|
||||
@@ -99,7 +108,7 @@ def download_apks(ctx, all_apks, virustotal, koodous, all_checks, output, from_f
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# Checks through ADB
|
||||
# Command: check-adb
|
||||
#==============================================================================
|
||||
@cli.command("check-adb", help="Check an Android device over adb")
|
||||
@click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL)
|
||||
@@ -129,13 +138,7 @@ def check_adb(ctx, iocs, output, fast, list_modules, module, serial):
|
||||
ctx.exit(1)
|
||||
|
||||
indicators = Indicators(log=log)
|
||||
for ioc_path in iocs:
|
||||
try:
|
||||
indicators.parse_stix2(ioc_path)
|
||||
except IndicatorsFileBadFormat as e:
|
||||
log.critical(e)
|
||||
ctx.exit(1)
|
||||
log.info("Loaded a total of %d indicators", indicators.ioc_count)
|
||||
indicators.load_indicators_files(iocs)
|
||||
|
||||
timeline = []
|
||||
timeline_detected = []
|
||||
@@ -145,13 +148,12 @@ def check_adb(ctx, iocs, output, fast, list_modules, module, serial):
|
||||
|
||||
m = adb_module(output_folder=output, fast_mode=fast,
|
||||
log=logging.getLogger(adb_module.__module__))
|
||||
if indicators.total_ioc_count:
|
||||
m.indicators = indicators
|
||||
m.indicators.log = m.log
|
||||
if serial:
|
||||
m.serial = serial
|
||||
|
||||
if iocs:
|
||||
indicators.log = m.log
|
||||
m.indicators = indicators
|
||||
|
||||
run_module(m)
|
||||
timeline.extend(m.timeline)
|
||||
timeline_detected.extend(m.timeline_detected)
|
||||
@@ -164,7 +166,81 @@ def check_adb(ctx, iocs, output, fast, list_modules, module, serial):
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# Check ADB backup
|
||||
# Command: check-bugreport
|
||||
#==============================================================================
|
||||
@cli.command("check-bugreport", help="Check an Android Bug Report")
|
||||
@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True,
|
||||
default=[], help=HELP_MSG_IOC)
|
||||
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
|
||||
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
||||
@click.option("--module", "-m", help=HELP_MSG_MODULE)
|
||||
@click.argument("BUGREPORT_PATH", type=click.Path(exists=True))
|
||||
@click.pass_context
|
||||
def check_bugreport(ctx, iocs, output, list_modules, module, bugreport_path):
|
||||
if list_modules:
|
||||
log.info("Following is the list of available check-bugreport modules:")
|
||||
for adb_module in BUGREPORT_MODULES:
|
||||
log.info(" - %s", adb_module.__name__)
|
||||
|
||||
return
|
||||
|
||||
log.info("Checking an Android Bug Report located at: %s", bugreport_path)
|
||||
|
||||
if output and not os.path.exists(output):
|
||||
try:
|
||||
os.makedirs(output)
|
||||
except Exception as e:
|
||||
log.critical("Unable to create output folder %s: %s", output, e)
|
||||
ctx.exit(1)
|
||||
|
||||
indicators = Indicators(log=log)
|
||||
indicators.load_indicators_files(iocs)
|
||||
|
||||
if os.path.isfile(bugreport_path):
|
||||
bugreport_format = "zip"
|
||||
zip_archive = ZipFile(bugreport_path)
|
||||
zip_files = []
|
||||
for file_name in zip_archive.namelist():
|
||||
zip_files.append(file_name)
|
||||
elif os.path.isdir(bugreport_path):
|
||||
bugreport_format = "dir"
|
||||
folder_files = []
|
||||
parent_path = Path(bugreport_path).absolute().as_posix()
|
||||
for root, subdirs, subfiles in os.walk(os.path.abspath(bugreport_path)):
|
||||
for file_name in subfiles:
|
||||
folder_files.append(os.path.relpath(os.path.join(root, file_name), parent_path))
|
||||
|
||||
timeline = []
|
||||
timeline_detected = []
|
||||
for bugreport_module in BUGREPORT_MODULES:
|
||||
if module and bugreport_module.__name__ != module:
|
||||
continue
|
||||
|
||||
m = bugreport_module(base_folder=bugreport_path, output_folder=output,
|
||||
log=logging.getLogger(bugreport_module.__module__))
|
||||
|
||||
if bugreport_format == "zip":
|
||||
m.from_zip(zip_archive, zip_files)
|
||||
else:
|
||||
m.from_folder(bugreport_path, folder_files)
|
||||
|
||||
if indicators.total_ioc_count:
|
||||
m.indicators = indicators
|
||||
m.indicators.log = m.log
|
||||
|
||||
run_module(m)
|
||||
timeline.extend(m.timeline)
|
||||
timeline_detected.extend(m.timeline_detected)
|
||||
|
||||
if output:
|
||||
if len(timeline) > 0:
|
||||
save_timeline(timeline, os.path.join(output, "timeline.csv"))
|
||||
if len(timeline_detected) > 0:
|
||||
save_timeline(timeline_detected, os.path.join(output, "timeline_detected.csv"))
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# Command: check-backup
|
||||
#==============================================================================
|
||||
@cli.command("check-backup", help="Check an Android Backup")
|
||||
@click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL)
|
||||
@@ -176,6 +252,44 @@ def check_adb(ctx, iocs, output, fast, list_modules, module, serial):
|
||||
def check_backup(ctx, iocs, output, backup_path, serial):
|
||||
log.info("Checking ADB backup located at: %s", backup_path)
|
||||
|
||||
if os.path.isfile(backup_path):
|
||||
# AB File
|
||||
backup_type = "ab"
|
||||
with open(backup_path, "rb") as handle:
|
||||
data = handle.read()
|
||||
header = parse_ab_header(data)
|
||||
if not header["backup"]:
|
||||
log.critical("Invalid backup format, file should be in .ab format")
|
||||
ctx.exit(1)
|
||||
password = None
|
||||
if header["encryption"] != "none":
|
||||
password = getpass.getpass(prompt="Backup Password: ", stream=None)
|
||||
try:
|
||||
tardata = parse_backup_file(data, password=password)
|
||||
except InvalidBackupPassword:
|
||||
log.critical("Invalid backup password")
|
||||
ctx.exit(1)
|
||||
except AndroidBackupParsingError:
|
||||
log.critical("Impossible to parse this backup file, please use Android Backup Extractor instead")
|
||||
ctx.exit(1)
|
||||
|
||||
dbytes = io.BytesIO(tardata)
|
||||
tar = tarfile.open(fileobj=dbytes)
|
||||
files = []
|
||||
for member in tar:
|
||||
files.append(member.name)
|
||||
|
||||
elif os.path.isdir(backup_path):
|
||||
backup_type = "folder"
|
||||
backup_path = Path(backup_path).absolute().as_posix()
|
||||
files = []
|
||||
for root, subdirs, subfiles in os.walk(os.path.abspath(backup_path)):
|
||||
for fname in subfiles:
|
||||
files.append(os.path.relpath(os.path.join(root, fname), backup_path))
|
||||
else:
|
||||
log.critical("Invalid backup path, path should be a folder or an Android Backup (.ab) file")
|
||||
ctx.exit(1)
|
||||
|
||||
if output and not os.path.exists(output):
|
||||
try:
|
||||
os.makedirs(output)
|
||||
@@ -184,31 +298,94 @@ def check_backup(ctx, iocs, output, backup_path, serial):
|
||||
ctx.exit(1)
|
||||
|
||||
indicators = Indicators(log=log)
|
||||
for ioc_path in iocs:
|
||||
try:
|
||||
indicators.parse_stix2(ioc_path)
|
||||
except IndicatorsFileBadFormat as e:
|
||||
log.critical(e)
|
||||
ctx.exit(1)
|
||||
log.info("Loaded a total of %d indicators", indicators.ioc_count)
|
||||
|
||||
if os.path.isfile(backup_path):
|
||||
log.critical("The path you specified is a not a folder!")
|
||||
|
||||
if os.path.basename(backup_path) == "backup.ab":
|
||||
log.info("You can use ABE (https://github.com/nelenkov/android-backup-extractor) "
|
||||
"to extract 'backup.ab' files!")
|
||||
ctx.exit(1)
|
||||
indicators.load_indicators_files(iocs)
|
||||
|
||||
for module in BACKUP_MODULES:
|
||||
m = module(base_folder=backup_path, output_folder=output,
|
||||
log=logging.getLogger(module.__module__))
|
||||
|
||||
if indicators.total_ioc_count:
|
||||
m.indicators = indicators
|
||||
m.indicators.log = m.log
|
||||
if serial:
|
||||
m.serial = serial
|
||||
|
||||
if len(indicators.ioc_count) > 0:
|
||||
indicators.log = m.log
|
||||
m.indicators = indicators
|
||||
if backup_type == "folder":
|
||||
m.from_folder(backup_path, files)
|
||||
else:
|
||||
m.from_ab(backup_path, tar, files)
|
||||
|
||||
run_module(m)
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# Command: check-iocs
|
||||
#==============================================================================
|
||||
@cli.command("check-iocs", help="Compare stored JSON results to provided indicators")
|
||||
@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True,
|
||||
default=[], help=HELP_MSG_IOC)
|
||||
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
||||
@click.option("--module", "-m", help=HELP_MSG_MODULE)
|
||||
@click.argument("FOLDER", type=click.Path(exists=True))
|
||||
@click.pass_context
|
||||
def check_iocs(ctx, iocs, list_modules, module, folder):
|
||||
all_modules = []
|
||||
for entry in BACKUP_MODULES + ADB_MODULES:
|
||||
if entry not in all_modules:
|
||||
all_modules.append(entry)
|
||||
|
||||
if list_modules:
|
||||
log.info("Following is the list of available check-iocs modules:")
|
||||
for iocs_module in all_modules:
|
||||
log.info(" - %s", iocs_module.__name__)
|
||||
|
||||
return
|
||||
|
||||
log.info("Checking stored results against provided indicators...")
|
||||
|
||||
indicators = Indicators(log=log)
|
||||
indicators.load_indicators_files(iocs)
|
||||
|
||||
total_detections = 0
|
||||
for file_name in os.listdir(folder):
|
||||
name_only, ext = os.path.splitext(file_name)
|
||||
file_path = os.path.join(folder, file_name)
|
||||
|
||||
# TODO: Skipping processing of result files that are not json.
|
||||
# We might want to revisit this eventually.
|
||||
if ext != ".json":
|
||||
continue
|
||||
|
||||
for iocs_module in all_modules:
|
||||
if module and iocs_module.__name__ != module:
|
||||
continue
|
||||
|
||||
if iocs_module().get_slug() != name_only:
|
||||
continue
|
||||
|
||||
log.info("Loading results from \"%s\" with module %s", file_name,
|
||||
iocs_module.__name__)
|
||||
|
||||
m = iocs_module.from_json(file_path,
|
||||
log=logging.getLogger(iocs_module.__module__))
|
||||
if indicators.total_ioc_count > 0:
|
||||
m.indicators = indicators
|
||||
m.indicators.log = m.log
|
||||
|
||||
try:
|
||||
m.check_indicators()
|
||||
except NotImplementedError:
|
||||
continue
|
||||
else:
|
||||
total_detections += len(m.detected)
|
||||
|
||||
if total_detections > 0:
|
||||
log.warning("The check of the results produced %d detections!",
|
||||
total_detections)
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# Command: download-iocs
|
||||
#==============================================================================
|
||||
@cli.command("download-iocs", help="Download public STIX2 indicators")
|
||||
def download_indicators():
|
||||
download_indicators_files(log)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -57,7 +57,7 @@ class DownloadAPKs(AndroidExtraction):
|
||||
:param json_path: Path to the apks.json file to parse.
|
||||
|
||||
"""
|
||||
with open(json_path, "r") as handle:
|
||||
with open(json_path, "r", encoding="utf-8") as handle:
|
||||
packages = json.load(handle)
|
||||
return cls(packages=packages)
|
||||
|
||||
@@ -173,7 +173,7 @@ class DownloadAPKs(AndroidExtraction):
|
||||
def save_json(self):
|
||||
"""Save the results to the package.json file."""
|
||||
json_path = os.path.join(self.output_folder, "apks.json")
|
||||
with open(json_path, "w") as handle:
|
||||
with open(json_path, "w", encoding="utf-8") as handle:
|
||||
json.dump(self.packages, handle, indent=4)
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -39,6 +39,10 @@ def get_virustotal_report(hashes):
|
||||
|
||||
|
||||
def virustotal_lookup(packages):
|
||||
# NOTE: This is temporary, until we resolved the issue.
|
||||
log.error("Unfortunately VirusTotal lookup is disabled until further notice, due to unresolved issues with the API service.")
|
||||
return
|
||||
|
||||
log.info("Looking up all extracted files on VirusTotal (www.virustotal.com)")
|
||||
|
||||
unique_hashes = []
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project 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 .chrome_history import ChromeHistory
|
||||
from .dumpsys_batterystats import DumpsysBatterystats
|
||||
from .dumpsys_accessibility import DumpsysAccessibility
|
||||
from .dumpsys_activities import DumpsysActivities
|
||||
from .dumpsys_battery_daily import DumpsysBatteryDaily
|
||||
from .dumpsys_battery_history import DumpsysBatteryHistory
|
||||
from .dumpsys_dbinfo import DumpsysDBInfo
|
||||
from .dumpsys_full import DumpsysFull
|
||||
from .dumpsys_packages import DumpsysPackages
|
||||
from .dumpsys_procstats import DumpsysProcstats
|
||||
from .dumpsys_receivers import DumpsysReceivers
|
||||
from .files import Files
|
||||
from .getprop import Getprop
|
||||
from .logcat import Logcat
|
||||
from .packages import Packages
|
||||
from .processes import Processes
|
||||
from .rootbinaries import RootBinaries
|
||||
from .root_binaries import RootBinaries
|
||||
from .selinux_status import SELinuxStatus
|
||||
from .settings import Settings
|
||||
from .sms import SMS
|
||||
from .whatsapp import Whatsapp
|
||||
|
||||
ADB_MODULES = [ChromeHistory, SMS, Whatsapp, Processes,
|
||||
DumpsysBatterystats, DumpsysProcstats,
|
||||
DumpsysPackages, DumpsysReceivers, DumpsysFull,
|
||||
Packages, RootBinaries, Logcat, Files]
|
||||
ADB_MODULES = [ChromeHistory, SMS, Whatsapp, Processes, Getprop, Settings,
|
||||
SELinuxStatus, DumpsysBatteryHistory, DumpsysBatteryDaily,
|
||||
DumpsysReceivers, DumpsysActivities, DumpsysAccessibility,
|
||||
DumpsysDBInfo, DumpsysFull, Packages, Logcat, RootBinaries,
|
||||
Files]
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project 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 base64
|
||||
import getpass
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
@@ -15,9 +17,11 @@ from adb_shell.adb_device import AdbDeviceTcp, AdbDeviceUsb
|
||||
from adb_shell.auth.keygen import keygen, write_public_keyfile
|
||||
from adb_shell.auth.sign_pythonrsa import PythonRSASigner
|
||||
from adb_shell.exceptions import (AdbCommandFailureException, DeviceAuthError,
|
||||
UsbReadFailedError)
|
||||
UsbDeviceNotFoundError, UsbReadFailedError)
|
||||
from usb1 import USBErrorAccess, USBErrorBusy
|
||||
|
||||
from mvt.android.parsers.backup import (InvalidBackupPassword, parse_ab_header,
|
||||
parse_backup_file)
|
||||
from mvt.common.module import InsufficientPrivileges, MVTModule
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -65,7 +69,11 @@ class AndroidExtraction(MVTModule):
|
||||
# If no serial was specified or if the serial does not seem to be
|
||||
# a HOST:PORT definition, we use the USB transport.
|
||||
if not self.serial or ":" not in self.serial:
|
||||
self.device = AdbDeviceUsb(serial=self.serial)
|
||||
try:
|
||||
self.device = AdbDeviceUsb(serial=self.serial)
|
||||
except UsbDeviceNotFoundError:
|
||||
log.critical("No device found. Make sure it is connected and unlocked.")
|
||||
sys.exit(-1)
|
||||
# Otherwise we try to use the TCP transport.
|
||||
else:
|
||||
addr = self.serial.split(":")
|
||||
@@ -112,7 +120,7 @@ class AndroidExtraction(MVTModule):
|
||||
:returns: Output of command
|
||||
|
||||
"""
|
||||
return self.device.shell(command)
|
||||
return self.device.shell(command, read_timeout_s=200.0)
|
||||
|
||||
def _adb_check_if_root(self):
|
||||
"""Check if we have a `su` binary on the Android device.
|
||||
@@ -239,6 +247,34 @@ class AndroidExtraction(MVTModule):
|
||||
# Disconnect from the device.
|
||||
self._adb_disconnect()
|
||||
|
||||
def _generate_backup(self, package_name):
|
||||
# Run ADB command to create a backup of SMS app
|
||||
self.log.warning("Please check phone and accept Android backup prompt. You may need to set a backup password. \a")
|
||||
|
||||
# Run ADB command to create a backup of SMS app
|
||||
# TODO: Base64 encoding as temporary fix to avoid byte-mangling over the shell transport...
|
||||
backup_output_b64 = self._adb_command("/system/bin/bu backup -nocompress '{}' | base64".format(package_name))
|
||||
backup_output = base64.b64decode(backup_output_b64)
|
||||
header = parse_ab_header(backup_output)
|
||||
|
||||
if not header["backup"]:
|
||||
self.log.error("Extracting SMS via Android backup failed. No valid backup data found.")
|
||||
return
|
||||
|
||||
if header["encryption"] == "none":
|
||||
return parse_backup_file(backup_output, password=None)
|
||||
|
||||
# Backup encrypted. Request password from user.
|
||||
for password_retry in range(0, 3):
|
||||
backup_password = getpass.getpass(prompt="Backup Password: ", stream=None)
|
||||
try:
|
||||
decrypted_backup_tar = parse_backup_file(backup_output, backup_password)
|
||||
return decrypted_backup_tar
|
||||
except InvalidBackupPassword:
|
||||
self.log.info("Invalid backup password")
|
||||
|
||||
self.log.warn("All attempts to decrypt backup with password failed!")
|
||||
|
||||
def run(self):
|
||||
"""Run the main procedure."""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -68,7 +68,7 @@ class ChromeHistory(AndroidExtraction):
|
||||
"url": item[1],
|
||||
"visit_id": item[2],
|
||||
"timestamp": item[3],
|
||||
"isodate": convert_timestamp_to_iso(convert_chrometime_to_unix[item[3]]),
|
||||
"isodate": convert_timestamp_to_iso(convert_chrometime_to_unix(item[3])),
|
||||
"redirect_source": item[4],
|
||||
})
|
||||
|
||||
@@ -78,5 +78,8 @@ class ChromeHistory(AndroidExtraction):
|
||||
log.info("Extracted a total of %d history items", len(self.results))
|
||||
|
||||
def run(self):
|
||||
self._adb_process_file(os.path.join("/", CHROME_HISTORY_PATH),
|
||||
self._parse_db)
|
||||
try:
|
||||
self._adb_process_file(os.path.join("/", CHROME_HISTORY_PATH),
|
||||
self._parse_db)
|
||||
except Exception as e:
|
||||
self.log.error(e)
|
||||
|
||||
45
mvt/android/modules/adb/dumpsys_accessibility.py
Normal file
45
mvt/android/modules/adb/dumpsys_accessibility.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 The MVT Project 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.parsers import parse_dumpsys_accessibility
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DumpsysAccessibility(AndroidExtraction):
|
||||
"""This module extracts stats on accessibility."""
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
def check_indicators(self):
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
ioc = self.indicators.check_app_id(result["package_name"])
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
def run(self):
|
||||
self._adb_connect()
|
||||
output = self._adb_command("dumpsys accessibility")
|
||||
self._adb_disconnect()
|
||||
|
||||
self.results = parse_dumpsys_accessibility(output)
|
||||
|
||||
for result in self.results:
|
||||
log.info("Found installed accessibility service \"%s\"", result.get("service"))
|
||||
|
||||
self.log.info("Identified a total of %d accessibility services", len(self.results))
|
||||
45
mvt/android/modules/adb/dumpsys_activities.py
Normal file
45
mvt/android/modules/adb/dumpsys_activities.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 The MVT Project 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.parsers import parse_dumpsys_activity_resolver_table
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DumpsysActivities(AndroidExtraction):
|
||||
"""This module extracts details on receivers for risky activities."""
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
self.results = results if results else {}
|
||||
|
||||
def check_indicators(self):
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for intent, activities in self.results.items():
|
||||
for activity in activities:
|
||||
ioc = self.indicators.check_app_id(activity["package_name"])
|
||||
if ioc:
|
||||
activity["matched_indicator"] = ioc
|
||||
self.detected.append({intent: activity})
|
||||
continue
|
||||
|
||||
def run(self):
|
||||
self._adb_connect()
|
||||
output = self._adb_command("dumpsys package")
|
||||
self._adb_disconnect()
|
||||
|
||||
self.results = parse_dumpsys_activity_resolver_table(output)
|
||||
|
||||
self.log.info("Extracted activities for %d intents", len(self.results))
|
||||
50
mvt/android/modules/adb/dumpsys_battery_daily.py
Normal file
50
mvt/android/modules/adb/dumpsys_battery_daily.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 The MVT Project 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.parsers import parse_dumpsys_battery_daily
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DumpsysBatteryDaily(AndroidExtraction):
|
||||
"""This module extracts records from battery daily updates."""
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
def serialize(self, record):
|
||||
return {
|
||||
"timestamp": record["from"],
|
||||
"module": self.__class__.__name__,
|
||||
"event": "battery_daily",
|
||||
"data": f"Recorded update of package {record['package_name']} with vers {record['vers']}"
|
||||
}
|
||||
|
||||
def check_indicators(self):
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
ioc = self.indicators.check_app_id(result["package_name"])
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
def run(self):
|
||||
self._adb_connect()
|
||||
output = self._adb_command("dumpsys batterystats --daily")
|
||||
self._adb_disconnect()
|
||||
|
||||
self.results = parse_dumpsys_battery_daily(output)
|
||||
|
||||
self.log.info("Extracted %d records from battery daily stats", len(self.results))
|
||||
42
mvt/android/modules/adb/dumpsys_battery_history.py
Normal file
42
mvt/android/modules/adb/dumpsys_battery_history.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 The MVT Project 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.parsers import parse_dumpsys_battery_history
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DumpsysBatteryHistory(AndroidExtraction):
|
||||
"""This module extracts records from battery history events."""
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
def check_indicators(self):
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
ioc = self.indicators.check_app_id(result["package_name"])
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
def run(self):
|
||||
self._adb_connect()
|
||||
output = self._adb_command("dumpsys batterystats --history")
|
||||
self._adb_disconnect()
|
||||
|
||||
self.results = parse_dumpsys_battery_history(output)
|
||||
|
||||
self.log.info("Extracted %d records from battery history", len(self.results))
|
||||
@@ -1,46 +0,0 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project 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 os
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DumpsysBatterystats(AndroidExtraction):
|
||||
"""This module extracts stats on battery consumption by processes."""
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
def run(self):
|
||||
self._adb_connect()
|
||||
|
||||
stats = self._adb_command("dumpsys batterystats")
|
||||
if self.output_folder:
|
||||
stats_path = os.path.join(self.output_folder,
|
||||
"dumpsys_batterystats.txt")
|
||||
with open(stats_path, "w") as handle:
|
||||
handle.write(stats)
|
||||
|
||||
log.info("Records from dumpsys batterystats stored at %s",
|
||||
stats_path)
|
||||
|
||||
history = self._adb_command("dumpsys batterystats --history")
|
||||
if self.output_folder:
|
||||
history_path = os.path.join(self.output_folder,
|
||||
"dumpsys_batterystats_history.txt")
|
||||
with open(history_path, "w") as handle:
|
||||
handle.write(history)
|
||||
|
||||
log.info("History records from dumpsys batterystats stored at %s",
|
||||
history_path)
|
||||
|
||||
self._adb_disconnect()
|
||||
48
mvt/android/modules/adb/dumpsys_dbinfo.py
Normal file
48
mvt/android/modules/adb/dumpsys_dbinfo.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 The MVT Project 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 re
|
||||
|
||||
from mvt.android.parsers import parse_dumpsys_dbinfo
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DumpsysDBInfo(AndroidExtraction):
|
||||
"""This module extracts records from battery daily updates."""
|
||||
|
||||
slug = "dumpsys_dbinfo"
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
def check_indicators(self):
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
path = result.get("path", "")
|
||||
for part in path.split("/"):
|
||||
ioc = self.indicators.check_app_id(part)
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
def run(self):
|
||||
self._adb_connect()
|
||||
output = self._adb_command("dumpsys dbinfo")
|
||||
self._adb_disconnect()
|
||||
|
||||
self.results = parse_dumpsys_dbinfo(output)
|
||||
|
||||
self.log.info("Extracted a total of %d records from database information",
|
||||
len(self.results))
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -23,14 +23,12 @@ class DumpsysFull(AndroidExtraction):
|
||||
def run(self):
|
||||
self._adb_connect()
|
||||
|
||||
stats = self._adb_command("dumpsys")
|
||||
output = self._adb_command("dumpsys")
|
||||
if self.output_folder:
|
||||
stats_path = os.path.join(self.output_folder,
|
||||
"dumpsys.txt")
|
||||
with open(stats_path, "w") as handle:
|
||||
handle.write(stats)
|
||||
output_path = os.path.join(self.output_folder, "dumpsys.txt")
|
||||
with open(output_path, "w", encoding="utf-8") as handle:
|
||||
handle.write(output)
|
||||
|
||||
log.info("Full dumpsys output stored at %s",
|
||||
stats_path)
|
||||
log.info("Full dumpsys output stored at %s", output_path)
|
||||
|
||||
self._adb_disconnect()
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project 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 os
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DumpsysProcstats(AndroidExtraction):
|
||||
"""This module extracts stats on memory consumption by processes."""
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
def run(self):
|
||||
self._adb_connect()
|
||||
|
||||
output = self._adb_command("dumpsys procstats")
|
||||
if self.output_folder:
|
||||
procstats_path = os.path.join(self.output_folder,
|
||||
"dumpsys_procstats.txt")
|
||||
with open(procstats_path, "w") as handle:
|
||||
handle.write(output)
|
||||
|
||||
log.info("Records from dumpsys procstats stored at %s",
|
||||
procstats_path)
|
||||
|
||||
self._adb_disconnect()
|
||||
@@ -1,18 +1,21 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project 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.parsers import parse_dumpsys_receiver_resolver_table
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
ACTION_NEW_OUTGOING_SMS = "android.provider.Telephony.NEW_OUTGOING_SMS"
|
||||
ACTION_SMS_RECEIVED = "android.provider.Telephony.SMS_RECEIVED"
|
||||
ACTION_DATA_SMS_RECEIVED = "android.intent.action.DATA_SMS_RECEIVED"
|
||||
ACTION_PHONE_STATE = "android.intent.action.PHONE_STATE"
|
||||
INTENT_NEW_OUTGOING_SMS = "android.provider.Telephony.NEW_OUTGOING_SMS"
|
||||
INTENT_SMS_RECEIVED = "android.provider.Telephony.SMS_RECEIVED"
|
||||
INTENT_DATA_SMS_RECEIVED = "android.intent.action.DATA_SMS_RECEIVED"
|
||||
INTENT_PHONE_STATE = "android.intent.action.PHONE_STATE"
|
||||
INTENT_NEW_OUTGOING_CALL = "android.intent.action.NEW_OUTGOING_CALL"
|
||||
|
||||
|
||||
class DumpsysReceivers(AndroidExtraction):
|
||||
@@ -24,64 +27,40 @@ class DumpsysReceivers(AndroidExtraction):
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
self.results = results if results else {}
|
||||
|
||||
def check_indicators(self):
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for intent, receivers in self.results.items():
|
||||
for receiver in receivers:
|
||||
if intent == INTENT_NEW_OUTGOING_SMS:
|
||||
self.log.info("Found a receiver to intercept outgoing SMS messages: \"%s\"",
|
||||
receiver["receiver"])
|
||||
elif intent == INTENT_SMS_RECEIVED:
|
||||
self.log.info("Found a receiver to intercept incoming SMS messages: \"%s\"",
|
||||
receiver["receiver"])
|
||||
elif intent == INTENT_DATA_SMS_RECEIVED:
|
||||
self.log.info("Found a receiver to intercept incoming data SMS message: \"%s\"",
|
||||
receiver["receiver"])
|
||||
elif intent == INTENT_PHONE_STATE:
|
||||
self.log.info("Found a receiver monitoring telephony state/incoming calls: \"%s\"",
|
||||
receiver["receiver"])
|
||||
elif intent == INTENT_NEW_OUTGOING_CALL:
|
||||
self.log.info("Found a receiver monitoring outgoing calls: \"%s\"",
|
||||
receiver["receiver"])
|
||||
|
||||
ioc = self.indicators.check_app_id(receiver["package_name"])
|
||||
if ioc:
|
||||
receiver["matched_indicator"] = ioc
|
||||
self.detected.append({intent: receiver})
|
||||
continue
|
||||
|
||||
def run(self):
|
||||
self._adb_connect()
|
||||
|
||||
output = self._adb_command("dumpsys package")
|
||||
if not output:
|
||||
return
|
||||
|
||||
activity = None
|
||||
for line in output.split("\n"):
|
||||
# Find activity block markers.
|
||||
if line.strip().startswith(ACTION_NEW_OUTGOING_SMS):
|
||||
activity = ACTION_NEW_OUTGOING_SMS
|
||||
continue
|
||||
elif line.strip().startswith(ACTION_SMS_RECEIVED):
|
||||
activity = ACTION_SMS_RECEIVED
|
||||
continue
|
||||
elif line.strip().startswith(ACTION_PHONE_STATE):
|
||||
activity = ACTION_PHONE_STATE
|
||||
continue
|
||||
elif line.strip().startswith(ACTION_DATA_SMS_RECEIVED):
|
||||
activity = ACTION_DATA_SMS_RECEIVED
|
||||
continue
|
||||
|
||||
# If we are not in an activity block yet, skip.
|
||||
if not activity:
|
||||
continue
|
||||
|
||||
# If we are in a block but the line does not start with 8 spaces
|
||||
# it means the block ended a new one started, so we reset and
|
||||
# continue.
|
||||
if not line.startswith(" " * 8):
|
||||
activity = None
|
||||
continue
|
||||
|
||||
# If we got this far, we are processing receivers for the
|
||||
# activities we are interested in.
|
||||
receiver = line.strip().split(" ")[1]
|
||||
package_name = receiver.split("/")[0]
|
||||
if package_name == "com.google.android.gms":
|
||||
continue
|
||||
|
||||
if activity == ACTION_NEW_OUTGOING_SMS:
|
||||
self.log.info("Found a receiver to intercept outgoing SMS messages: \"%s\"",
|
||||
receiver)
|
||||
elif activity == ACTION_SMS_RECEIVED:
|
||||
self.log.info("Found a receiver to intercept incoming SMS messages: \"%s\"",
|
||||
receiver)
|
||||
elif activity == ACTION_DATA_SMS_RECEIVED:
|
||||
self.log.info("Found a receiver to intercept incoming data SMS message: \"%s\"",
|
||||
receiver)
|
||||
elif activity == ACTION_PHONE_STATE:
|
||||
self.log.info("Found a receiver monitoring telephony state: \"%s\"",
|
||||
receiver)
|
||||
|
||||
self.results.append({
|
||||
"activity": activity,
|
||||
"package_name": package_name,
|
||||
"receiver": receiver,
|
||||
})
|
||||
self.results = parse_dumpsys_receiver_resolver_table(output)
|
||||
|
||||
self._adb_disconnect()
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project 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 os
|
||||
import stat
|
||||
import datetime
|
||||
import logging
|
||||
import stat
|
||||
|
||||
from mvt.common.utils import check_for_links, convert_timestamp_to_iso
|
||||
from mvt.common.utils import convert_timestamp_to_iso
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
@@ -23,30 +22,16 @@ class Files(AndroidExtraction):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
self.full_find = None
|
||||
self.full_find = False
|
||||
|
||||
def find_path(self, file_path):
|
||||
"""Checks if Android system supports full find command output"""
|
||||
# Check find command params on first run
|
||||
# Run find command with correct args and parse results.
|
||||
def find_files(self, folder):
|
||||
if self.full_find:
|
||||
output = self._adb_command(f"find '{folder}' -printf '%T@ %m %s %u %g %p\n' 2> /dev/null")
|
||||
|
||||
# Check that full file printf options are suppported on first run.
|
||||
if self.full_find == None:
|
||||
output = self._adb_command(f"find '/' -maxdepth 1 -printf '%T@ %m %s %u %g %p\n' 2> /dev/null")
|
||||
if not (output or output.strip().splitlines()):
|
||||
# Full find command failed to generate output, fallback to basic file arguments
|
||||
self.full_find = False
|
||||
else:
|
||||
self.full_find = True
|
||||
|
||||
found_files = []
|
||||
if self.full_find == True:
|
||||
# Run full file command and collect additonal file information.
|
||||
output = self._adb_command(f"find '{file_path}' -printf '%T@ %m %s %u %g %p\n' 2> /dev/null")
|
||||
for file_line in output.splitlines():
|
||||
[unix_timestamp, mode, size, owner, group, full_path] = file_line.rstrip().split(" ", 5)
|
||||
mod_time = convert_timestamp_to_iso(datetime.datetime.utcfromtimestamp(int(float(unix_timestamp))))
|
||||
found_files.append({
|
||||
self.results.append({
|
||||
"path": full_path,
|
||||
"modified_time": mod_time,
|
||||
"mode": mode,
|
||||
@@ -57,14 +42,9 @@ class Files(AndroidExtraction):
|
||||
"group": group,
|
||||
})
|
||||
else:
|
||||
# Run a basic listing of file paths.
|
||||
output = self._adb_command(f"find '{file_path}' 2> /dev/null")
|
||||
output = self._adb_command(f"find '{folder}' 2> /dev/null")
|
||||
for file_line in output.splitlines():
|
||||
found_files.append({
|
||||
"path": file_line.rstrip()
|
||||
})
|
||||
|
||||
return found_files
|
||||
self.results.append({"path": file_line.rstrip()})
|
||||
|
||||
def serialize(self, record):
|
||||
if "modified_time" in record:
|
||||
@@ -86,39 +66,32 @@ class Files(AndroidExtraction):
|
||||
def check_indicators(self):
|
||||
"""Check file list for known suspicious files or suspicious properties"""
|
||||
self.check_suspicious()
|
||||
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
if self.indicators.check_filename(result["path"]):
|
||||
self.log.warning("Found a known suspicous filename at path: \"%s\"", result["path"])
|
||||
self.detected.append(result)
|
||||
|
||||
if self.indicators.check_file_path(result["path"]):
|
||||
self.log.warning("Found a known suspicous file at path: \"%s\"", result["path"])
|
||||
self.detected.append(result)
|
||||
|
||||
def run(self):
|
||||
self._adb_connect()
|
||||
found_file_paths = []
|
||||
|
||||
DATA_PATHS = ["/data/local/tmp/", "/sdcard/", "/tmp/"]
|
||||
for path in DATA_PATHS:
|
||||
file_info = self.find_path(path)
|
||||
found_file_paths.extend(file_info)
|
||||
output = self._adb_command("find '/' -maxdepth 1 -printf '%T@ %m %s %u %g %p\n' 2> /dev/null")
|
||||
if output or output.strip().splitlines():
|
||||
self.full_find = True
|
||||
|
||||
# Store results
|
||||
self.results.extend(found_file_paths)
|
||||
self.log.info("Found %s files in primary Android data directories.", len(found_file_paths))
|
||||
for data_path in ["/data/local/tmp/", "/sdcard/", "/tmp/"]:
|
||||
self.find_files(data_path)
|
||||
|
||||
self.log.info("Found %s files in primary Android data directories", len(self.results))
|
||||
|
||||
if self.fast_mode:
|
||||
self.log.info("Flag --fast was enabled: skipping full file listing")
|
||||
else:
|
||||
self.log.info("Flag --fast was not enabled: processing full file listing. "
|
||||
"This may take a while...")
|
||||
output = self.find_path("/")
|
||||
if output and self.output_folder:
|
||||
self.results.extend(output)
|
||||
log.info("List of visible files stored in files.json")
|
||||
self.log.info("Processing full file listing. This may take a while...")
|
||||
self.find_files("/")
|
||||
self.log.info("Found %s total files", len(self.results))
|
||||
|
||||
self._adb_disconnect()
|
||||
|
||||
43
mvt/android/modules/adb/getprop.py
Normal file
43
mvt/android/modules/adb/getprop.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 The MVT Project 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 re
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from mvt.android.parsers import parse_getprop
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Getprop(AndroidExtraction):
|
||||
"""This module extracts device properties from getprop command."""
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
self.results = {} if not results else results
|
||||
|
||||
def run(self):
|
||||
self._adb_connect()
|
||||
output = self._adb_command("getprop")
|
||||
self._adb_disconnect()
|
||||
|
||||
self.results = parse_getprop(output)
|
||||
|
||||
# Alert if phone is outdated.
|
||||
security_patch = self.results.get("ro.build.version.security_patch", "")
|
||||
if security_patch:
|
||||
patch_date = datetime.strptime(security_patch, "%Y-%m-%d")
|
||||
if (datetime.now() - patch_date) > timedelta(days=6*30):
|
||||
self.log.warning("This phone has not received security updates for more than "
|
||||
"six months (last update: %s)", security_patch)
|
||||
|
||||
self.log.info("Extracted %d Android system properties", len(self.results))
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -31,7 +31,7 @@ class Logcat(AndroidExtraction):
|
||||
if self.output_folder:
|
||||
logcat_path = os.path.join(self.output_folder,
|
||||
"logcat.txt")
|
||||
with open(logcat_path, "w") as handle:
|
||||
with open(logcat_path, "w", encoding="utf-8") as handle:
|
||||
handle.write(output)
|
||||
|
||||
log.info("Current logcat logs stored at %s",
|
||||
@@ -39,7 +39,7 @@ class Logcat(AndroidExtraction):
|
||||
|
||||
logcat_last_path = os.path.join(self.output_folder,
|
||||
"logcat_last.txt")
|
||||
with open(logcat_last_path, "w") as handle:
|
||||
with open(logcat_last_path, "w", encoding="utf-8") as handle:
|
||||
handle.write(last_output)
|
||||
|
||||
log.info("Logcat logs prior to last reboot stored at %s",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -8,10 +8,37 @@ import os
|
||||
|
||||
import pkg_resources
|
||||
|
||||
from mvt.android.lookups.koodous import koodous_lookup
|
||||
from mvt.android.lookups.virustotal import virustotal_lookup
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
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",
|
||||
]
|
||||
|
||||
|
||||
class Packages(AndroidExtraction):
|
||||
"""This module extracts the list of installed packages."""
|
||||
@@ -42,12 +69,9 @@ class Packages(AndroidExtraction):
|
||||
return records
|
||||
|
||||
def check_indicators(self):
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
root_packages_path = os.path.join("..", "..", "data", "root_packages.txt")
|
||||
root_packages_string = pkg_resources.resource_string(__name__, root_packages_path)
|
||||
root_packages = root_packages_string.decode("utf-8").split("\n")
|
||||
root_packages = root_packages_string.decode("utf-8").splitlines()
|
||||
root_packages = [rp.strip() for rp in root_packages]
|
||||
|
||||
for result in self.results:
|
||||
@@ -55,16 +79,62 @@ class Packages(AndroidExtraction):
|
||||
self.log.warning("Found an installed package related to rooting/jailbreaking: \"%s\"",
|
||||
result["package_name"])
|
||||
self.detected.append(result)
|
||||
if result["package_name"] in self.indicators.ioc_app_ids:
|
||||
self.log.warning("Found a malicious package name: \"%s\"",
|
||||
result["package_name"])
|
||||
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)
|
||||
for file in result["files"]:
|
||||
if file["sha256"] in self.indicators.ioc_files_sha256:
|
||||
self.log.warning("Found a malicious APK: \"%s\" %s",
|
||||
result["package_name"],
|
||||
file["sha256"])
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
for package_file in result["files"]:
|
||||
ioc = self.indicators.check_file_hash(package_file["sha256"])
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
@staticmethod
|
||||
def parse_package_for_details(output):
|
||||
details = {
|
||||
"uid": "",
|
||||
"version_name": "",
|
||||
"version_code": "",
|
||||
"timestamp": "",
|
||||
"first_install_time": "",
|
||||
"last_update_time": "",
|
||||
"requested_permissions": [],
|
||||
}
|
||||
|
||||
in_permissions = False
|
||||
for line in output.splitlines():
|
||||
if in_permissions:
|
||||
if line.startswith(" " * 4) and not line.startswith(" " * 6):
|
||||
in_permissions = False
|
||||
continue
|
||||
|
||||
permission = line.strip().split(":")[0]
|
||||
details["requested_permissions"].append(permission)
|
||||
|
||||
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() == "requested permissions:":
|
||||
in_permissions = True
|
||||
continue
|
||||
|
||||
return details
|
||||
|
||||
def _get_files_for_package(self, package_name):
|
||||
output = self._adb_command(f"pm path {package_name}")
|
||||
@@ -73,7 +143,7 @@ class Packages(AndroidExtraction):
|
||||
return []
|
||||
|
||||
package_files = []
|
||||
for file_path in output.split("\n"):
|
||||
for file_path in output.splitlines():
|
||||
file_path = file_path.strip()
|
||||
|
||||
md5 = self._adb_command(f"md5sum {file_path}").split(" ")[0]
|
||||
@@ -94,8 +164,9 @@ class Packages(AndroidExtraction):
|
||||
def run(self):
|
||||
self._adb_connect()
|
||||
|
||||
packages = self._adb_command("pm list packages -U -u -i -f")
|
||||
for line in packages.split("\n"):
|
||||
packages = self._adb_command("pm list packages -u -i -f")
|
||||
|
||||
for line in packages.splitlines():
|
||||
line = line.strip()
|
||||
if not line.startswith("package:"):
|
||||
continue
|
||||
@@ -111,31 +182,22 @@ class Packages(AndroidExtraction):
|
||||
if installer == "null":
|
||||
installer = None
|
||||
|
||||
try:
|
||||
uid = fields[2].split(":")[1].strip()
|
||||
except IndexError:
|
||||
uid = None
|
||||
|
||||
dumpsys = self._adb_command(f"dumpsys package {package_name} | grep -A2 timeStamp").split("\n")
|
||||
timestamp = dumpsys[0].split("=")[1].strip()
|
||||
first_install = dumpsys[1].split("=")[1].strip()
|
||||
last_update = dumpsys[2].split("=")[1].strip()
|
||||
|
||||
package_files = self._get_files_for_package(package_name)
|
||||
|
||||
self.results.append({
|
||||
new_package = {
|
||||
"package_name": package_name,
|
||||
"file_name": file_name,
|
||||
"installer": installer,
|
||||
"timestamp": timestamp,
|
||||
"first_install_time": first_install,
|
||||
"last_update_time": last_update,
|
||||
"uid": uid,
|
||||
"disabled": False,
|
||||
"system": False,
|
||||
"third_party": False,
|
||||
"files": package_files,
|
||||
})
|
||||
}
|
||||
|
||||
dumpsys_package = self._adb_command(f"dumpsys package {package_name}")
|
||||
package_details = self.parse_package_for_details(dumpsys_package)
|
||||
new_package.update(package_details)
|
||||
|
||||
self.results.append(new_package)
|
||||
|
||||
cmds = [
|
||||
{"field": "disabled", "arg": "-d"},
|
||||
@@ -144,7 +206,7 @@ class Packages(AndroidExtraction):
|
||||
]
|
||||
for cmd in cmds:
|
||||
output = self._adb_command(f"pm list packages {cmd['arg']}")
|
||||
for line in output.split("\n"):
|
||||
for line in output.splitlines():
|
||||
line = line.strip()
|
||||
if not line.startswith("package:"):
|
||||
continue
|
||||
@@ -154,13 +216,32 @@ class Packages(AndroidExtraction):
|
||||
if result["package_name"] == package_name:
|
||||
self.results[i][cmd["field"]] = True
|
||||
|
||||
for result in self.results:
|
||||
if not result["third_party"]:
|
||||
continue
|
||||
|
||||
dangerous_permissions_count = 0
|
||||
for perm in result["requested_permissions"]:
|
||||
if perm in DANGEROUS_PERMISSIONS:
|
||||
dangerous_permissions_count += 1
|
||||
|
||||
if dangerous_permissions_count >= DANGEROUS_PERMISSIONS_THRESHOLD:
|
||||
self.log.info("Third-party package \"%s\" requested %d potentially dangerous permissions",
|
||||
result["package_name"], dangerous_permissions_count)
|
||||
|
||||
packages_to_lookup = []
|
||||
for result in self.results:
|
||||
if result["system"]:
|
||||
continue
|
||||
|
||||
packages_to_lookup.append(result)
|
||||
self.log.info("Found non-system package with name \"%s\" installed by \"%s\" on %s",
|
||||
result["package_name"], result["installer"], result["timestamp"])
|
||||
|
||||
if not self.fast_mode:
|
||||
virustotal_lookup(packages_to_lookup)
|
||||
koodous_lookup(packages_to_lookup)
|
||||
|
||||
self.log.info("Extracted at total of %d installed package names",
|
||||
len(self.results))
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -19,12 +19,22 @@ class Processes(AndroidExtraction):
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
def check_indicators(self):
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
ioc = self.indicators.check_app_id(result.get("name", ""))
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
def run(self):
|
||||
self._adb_connect()
|
||||
|
||||
output = self._adb_command("ps -e")
|
||||
|
||||
for line in output.split("\n")[1:]:
|
||||
for line in output.splitlines()[1:]:
|
||||
line = line.strip()
|
||||
if line == "":
|
||||
continue
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -25,7 +25,7 @@ class RootBinaries(AndroidExtraction):
|
||||
def run(self):
|
||||
root_binaries_path = os.path.join("..", "..", "data", "root_binaries.txt")
|
||||
root_binaries_string = pkg_resources.resource_string(__name__, root_binaries_path)
|
||||
root_binaries = root_binaries_string.decode("utf-8").split("\n")
|
||||
root_binaries = root_binaries_string.decode("utf-8").splitlines()
|
||||
|
||||
self._adb_connect()
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project 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 os
|
||||
|
||||
import pkg_resources
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DumpsysPackages(AndroidExtraction):
|
||||
"""This module extracts details on installed packages."""
|
||||
class SELinuxStatus(AndroidExtraction):
|
||||
"""This module checks if SELinux is being enforced."""
|
||||
|
||||
slug = "selinux_status"
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
@@ -20,18 +24,17 @@ class DumpsysPackages(AndroidExtraction):
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
self.results = {} if not results else results
|
||||
|
||||
def run(self):
|
||||
self._adb_connect()
|
||||
|
||||
output = self._adb_command("dumpsys package")
|
||||
|
||||
if self.output_folder:
|
||||
packages_path = os.path.join(self.output_folder,
|
||||
"dumpsys_packages.txt")
|
||||
with open(packages_path, "w") as handle:
|
||||
handle.write(output)
|
||||
|
||||
log.info("Records from dumpsys package stored at %s",
|
||||
packages_path)
|
||||
|
||||
output = self._adb_command("getenforce")
|
||||
self._adb_disconnect()
|
||||
|
||||
status = output.lower().strip()
|
||||
self.results["status"] = status
|
||||
|
||||
if status == "enforcing":
|
||||
self.log.info("SELinux is being regularly enforced")
|
||||
else:
|
||||
self.log.warning("SELinux status is \"%s\"!", status)
|
||||
105
mvt/android/modules/adb/settings.py
Normal file
105
mvt/android/modules/adb/settings.py
Normal file
@@ -0,0 +1,105 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 The MVT Project 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 .base import AndroidExtraction
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
ANDROID_DANGEROUS_SETTINGS = [
|
||||
{
|
||||
"description": "disabled Google Play Services apps verification",
|
||||
"key": "verifier_verify_adb_installs",
|
||||
"safe_value": "1",
|
||||
},
|
||||
{
|
||||
"description": "disabled Google Play Protect",
|
||||
"key": "package_verifier_enable",
|
||||
"safe_value": "1",
|
||||
},
|
||||
{
|
||||
"description": "disabled Google Play Protect",
|
||||
"key": "package_verifier_user_consent",
|
||||
"safe_value": "1",
|
||||
},
|
||||
{
|
||||
"description": "disabled Google Play Protect",
|
||||
"key": "upload_apk_enable",
|
||||
"safe_value": "1",
|
||||
},
|
||||
{
|
||||
"description": "enabled installation of non-market apps",
|
||||
"key": "install_non_market_apps",
|
||||
"safe_value": "0",
|
||||
},
|
||||
{
|
||||
"description": "disabled confirmation of adb apps installation",
|
||||
"key": "adb_install_need_confirm",
|
||||
"safe_value": "1",
|
||||
},
|
||||
{
|
||||
"description": "disabled sharing of security reports",
|
||||
"key": "send_security_reports",
|
||||
"safe_value": "1",
|
||||
},
|
||||
{
|
||||
"description": "disabled sharing of crash logs with manufacturer",
|
||||
"key": "samsung_errorlog_agree",
|
||||
"safe_value": "1",
|
||||
},
|
||||
{
|
||||
"description": "disabled applications errors reports",
|
||||
"key": "send_action_app_error",
|
||||
"safe_value": "1",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class Settings(AndroidExtraction):
|
||||
"""This module extracts Android system settings."""
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
self.results = {} if not results else results
|
||||
|
||||
def check_indicators(self):
|
||||
for namespace, settings in self.results.items():
|
||||
for key, value in settings.items():
|
||||
for danger in ANDROID_DANGEROUS_SETTINGS:
|
||||
# Check if one of the dangerous settings is using an unsafe
|
||||
# value (different than the one specified).
|
||||
if danger["key"] == key and danger["safe_value"] != value:
|
||||
self.log.warning("Found suspicious setting \"%s = %s\" (%s)",
|
||||
key, value, danger["description"])
|
||||
break
|
||||
|
||||
def run(self):
|
||||
self._adb_connect()
|
||||
|
||||
for namespace in ["system", "secure", "global"]:
|
||||
out = self._adb_command(f"cmd settings list {namespace}")
|
||||
if not out:
|
||||
continue
|
||||
|
||||
self.results[namespace] = {}
|
||||
|
||||
for line in out.splitlines():
|
||||
line = line.strip()
|
||||
if line == "":
|
||||
continue
|
||||
|
||||
fields = line.split("=", 1)
|
||||
try:
|
||||
self.results[namespace][fields[0]] = fields[1]
|
||||
except IndexError:
|
||||
continue
|
||||
|
||||
self._adb_disconnect()
|
||||
@@ -1,12 +1,17 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project 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 base64
|
||||
import getpass
|
||||
import logging
|
||||
import os
|
||||
import sqlite3
|
||||
|
||||
from mvt.android.parsers.backup import (AndroidBackupParsingError,
|
||||
parse_tar_for_sms)
|
||||
from mvt.common.module import InsufficientPrivileges
|
||||
from mvt.common.utils import check_for_links, convert_timestamp_to_iso
|
||||
|
||||
from .base import AndroidExtraction
|
||||
@@ -16,11 +21,11 @@ log = logging.getLogger(__name__)
|
||||
SMS_BUGLE_PATH = "data/data/com.google.android.apps.messaging/databases/bugle_db"
|
||||
SMS_BUGLE_QUERY = """
|
||||
SELECT
|
||||
ppl.normalized_destination AS number,
|
||||
ppl.normalized_destination AS address,
|
||||
p.timestamp AS timestamp,
|
||||
CASE WHEN m.sender_id IN
|
||||
(SELECT _id FROM participants WHERE contact_id=-1)
|
||||
THEN 2 ELSE 1 END incoming, p.text AS text
|
||||
THEN 2 ELSE 1 END incoming, p.text AS body
|
||||
FROM messages m, conversations c, parts p,
|
||||
participants ppl, conversation_participants cp
|
||||
WHERE (m.conversation_id = c._id)
|
||||
@@ -32,10 +37,10 @@ WHERE (m.conversation_id = c._id)
|
||||
SMS_MMSSMS_PATH = "data/data/com.android.providers.telephony/databases/mmssms.db"
|
||||
SMS_MMSMS_QUERY = """
|
||||
SELECT
|
||||
address AS number,
|
||||
address AS address,
|
||||
date_sent AS timestamp,
|
||||
type as incoming,
|
||||
body AS text
|
||||
body AS body
|
||||
FROM sms;
|
||||
"""
|
||||
|
||||
@@ -50,12 +55,12 @@ class SMS(AndroidExtraction):
|
||||
log=log, results=results)
|
||||
|
||||
def serialize(self, record):
|
||||
text = record["text"].replace("\n", "\\n")
|
||||
body = record["body"].replace("\n", "\\n")
|
||||
return {
|
||||
"timestamp": record["isodate"],
|
||||
"module": self.__class__.__name__,
|
||||
"event": f"sms_{record['direction']}",
|
||||
"data": f"{record['number']}: \"{text}\""
|
||||
"data": f"{record['address']}: \"{body}\""
|
||||
}
|
||||
|
||||
def check_indicators(self):
|
||||
@@ -63,10 +68,11 @@ class SMS(AndroidExtraction):
|
||||
return
|
||||
|
||||
for message in self.results:
|
||||
if "text" not in message:
|
||||
if "body" not in message:
|
||||
continue
|
||||
|
||||
message_links = check_for_links(message["text"])
|
||||
# FIXME: check links exported from the body previously
|
||||
message_links = check_for_links(message["body"])
|
||||
if self.indicators.check_domains(message_links):
|
||||
self.detected.append(message)
|
||||
|
||||
@@ -79,9 +85,9 @@ class SMS(AndroidExtraction):
|
||||
conn = sqlite3.connect(db_path)
|
||||
cur = conn.cursor()
|
||||
|
||||
if (self.SMS_DB_TYPE == 1):
|
||||
if self.SMS_DB_TYPE == 1:
|
||||
cur.execute(SMS_BUGLE_QUERY)
|
||||
elif (self.SMS_DB_TYPE == 2):
|
||||
elif self.SMS_DB_TYPE == 2:
|
||||
cur.execute(SMS_MMSMS_QUERY)
|
||||
|
||||
names = [description[0] for description in cur.description]
|
||||
@@ -96,7 +102,7 @@ class SMS(AndroidExtraction):
|
||||
|
||||
# If we find links in the messages or if they are empty we add
|
||||
# them to the list of results.
|
||||
if check_for_links(message["text"]) or message["text"].strip() == "":
|
||||
if check_for_links(message["body"]) or message["body"].strip() == "":
|
||||
self.results.append(message)
|
||||
|
||||
cur.close()
|
||||
@@ -104,12 +110,35 @@ class SMS(AndroidExtraction):
|
||||
|
||||
log.info("Extracted a total of %d SMS messages containing links", len(self.results))
|
||||
|
||||
def _extract_sms_adb(self):
|
||||
"""Use the Android backup command to extract SMS data from the native SMS app
|
||||
|
||||
It is crucial to use the under-documented "-nocompress" flag to disable the non-standard Java compression
|
||||
algorithim. This module only supports an unencrypted ADB backup.
|
||||
"""
|
||||
backup_tar = self._generate_backup("com.android.providers.telephony")
|
||||
if not backup_tar:
|
||||
return
|
||||
|
||||
try:
|
||||
self.results = parse_tar_for_sms(backup_tar)
|
||||
except AndroidBackupParsingError:
|
||||
self.log.info("Impossible to read SMS from the Android Backup, please extract the SMS and try extracting it with Android Backup Extractor")
|
||||
return
|
||||
|
||||
log.info("Extracted a total of %d SMS messages containing links", len(self.results))
|
||||
|
||||
def run(self):
|
||||
if (self._adb_check_file_exists(os.path.join("/", SMS_BUGLE_PATH))):
|
||||
self.SMS_DB_TYPE = 1
|
||||
self._adb_process_file(os.path.join("/", SMS_BUGLE_PATH), self._parse_db)
|
||||
elif (self._adb_check_file_exists(os.path.join("/", SMS_MMSSMS_PATH))):
|
||||
self.SMS_DB_TYPE = 2
|
||||
self._adb_process_file(os.path.join("/", SMS_MMSSMS_PATH), self._parse_db)
|
||||
else:
|
||||
self.log.error("No SMS database found")
|
||||
try:
|
||||
if (self._adb_check_file_exists(os.path.join("/", SMS_BUGLE_PATH))):
|
||||
self.SMS_DB_TYPE = 1
|
||||
self._adb_process_file(os.path.join("/", SMS_BUGLE_PATH), self._parse_db)
|
||||
elif (self._adb_check_file_exists(os.path.join("/", SMS_MMSSMS_PATH))):
|
||||
self.SMS_DB_TYPE = 2
|
||||
self._adb_process_file(os.path.join("/", SMS_MMSSMS_PATH), self._parse_db)
|
||||
return
|
||||
except InsufficientPrivileges:
|
||||
pass
|
||||
|
||||
self.log.warn("No SMS database found. Trying extraction of SMS data using Android backup feature.")
|
||||
self._extract_sms_adb()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -85,4 +85,7 @@ class Whatsapp(AndroidExtraction):
|
||||
self.results = messages
|
||||
|
||||
def run(self):
|
||||
self._adb_process_file(os.path.join("/", WHATSAPP_PATH), self._parse_db)
|
||||
try:
|
||||
self._adb_process_file(os.path.join("/", WHATSAPP_PATH), self._parse_db)
|
||||
except Exception as e:
|
||||
self.log.error(e)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
|
||||
46
mvt/android/modules/backup/base.py
Normal file
46
mvt/android/modules/backup/base.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 The MVT Project 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 fnmatch
|
||||
import os
|
||||
|
||||
from mvt.common.module import MVTModule
|
||||
|
||||
|
||||
class BackupExtraction(MVTModule):
|
||||
"""This class provides a base for all backup extractios modules"""
|
||||
ab = None
|
||||
|
||||
def from_folder(self, backup_path, files):
|
||||
"""
|
||||
Get all the files and list them
|
||||
"""
|
||||
self.backup_path = backup_path
|
||||
self.files = files
|
||||
|
||||
def from_ab(self, file_path, tar, files):
|
||||
"""
|
||||
Extract the files
|
||||
"""
|
||||
self.ab = file_path
|
||||
self.tar = tar
|
||||
self.files = files
|
||||
|
||||
def _get_files_by_pattern(self, pattern):
|
||||
return fnmatch.filter(self.files, pattern)
|
||||
|
||||
def _get_file_content(self, file_path):
|
||||
if self.ab:
|
||||
try:
|
||||
member = self.tar.getmember(file_path)
|
||||
except KeyError:
|
||||
return None
|
||||
handle = self.tar.extractfile(member)
|
||||
else:
|
||||
handle = open(os.path.join(self.backup_path, file_path), "rb")
|
||||
|
||||
data = handle.read()
|
||||
handle.close()
|
||||
return data
|
||||
@@ -1,23 +1,20 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project 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 json
|
||||
import os
|
||||
import zlib
|
||||
|
||||
from mvt.common.module import MVTModule
|
||||
from mvt.android.modules.backup.base import BackupExtraction
|
||||
from mvt.android.parsers.backup import parse_sms_file
|
||||
from mvt.common.utils import check_for_links
|
||||
|
||||
|
||||
class SMS(MVTModule):
|
||||
|
||||
class SMS(BackupExtraction):
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
self.results = []
|
||||
|
||||
def check_indicators(self):
|
||||
if not self.indicators:
|
||||
@@ -27,38 +24,13 @@ class SMS(MVTModule):
|
||||
if "body" not in message:
|
||||
continue
|
||||
|
||||
message_links = check_for_links(message["body"])
|
||||
if self.indicators.check_domains(message_links):
|
||||
if self.indicators.check_domains(message["links"]):
|
||||
self.detected.append(message)
|
||||
|
||||
def _process_sms_file(self, file_path):
|
||||
self.log.info("Processing SMS backup file at %s", file_path)
|
||||
|
||||
with open(file_path, "rb") as handle:
|
||||
data = zlib.decompress(handle.read())
|
||||
json_data = json.loads(data)
|
||||
|
||||
for entry in json_data:
|
||||
message_links = check_for_links(entry["body"])
|
||||
|
||||
# If we find links in the messages or if they are empty we add them to the list.
|
||||
if message_links or entry["body"].strip() == "":
|
||||
self.results.append(entry)
|
||||
|
||||
def run(self):
|
||||
app_folder = os.path.join(self.base_folder,
|
||||
"apps",
|
||||
"com.android.providers.telephony",
|
||||
"d_f")
|
||||
if not os.path.exists(app_folder):
|
||||
raise FileNotFoundError("Unable to find the SMS backup folder")
|
||||
|
||||
for file_name in os.listdir(app_folder):
|
||||
if not file_name.endswith("_sms_backup"):
|
||||
continue
|
||||
|
||||
file_path = os.path.join(app_folder, file_name)
|
||||
self._process_sms_file(file_path)
|
||||
|
||||
for file in self._get_files_by_pattern("apps/com.android.providers.telephony/d_f/*_sms_backup"):
|
||||
self.log.info("Processing SMS backup file at %s", file)
|
||||
data = self._get_file_content(file)
|
||||
self.results.extend(parse_sms_file(data))
|
||||
self.log.info("Extracted a total of %d SMS messages containing links",
|
||||
len(self.results))
|
||||
|
||||
16
mvt/android/modules/bugreport/__init__.py
Normal file
16
mvt/android/modules/bugreport/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 The MVT Project 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 .accessibility import Accessibility
|
||||
from .activities import Activities
|
||||
from .battery_daily import BatteryDaily
|
||||
from .battery_history import BatteryHistory
|
||||
from .dbinfo import DBInfo
|
||||
from .getprop import Getprop
|
||||
from .packages import Packages
|
||||
from .receivers import Receivers
|
||||
|
||||
BUGREPORT_MODULES = [Accessibility, Activities, BatteryDaily, BatteryHistory,
|
||||
DBInfo, Getprop, Packages, Receivers]
|
||||
60
mvt/android/modules/bugreport/accessibility.py
Normal file
60
mvt/android/modules/bugreport/accessibility.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 The MVT Project 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.parsers import parse_dumpsys_accessibility
|
||||
|
||||
from .base import BugReportModule
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Accessibility(BugReportModule):
|
||||
"""This module extracts stats on accessibility."""
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
def check_indicators(self):
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
ioc = self.indicators.check_app_id(result["package_name"])
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
def run(self):
|
||||
content = self._get_dumpstate_file()
|
||||
if not content:
|
||||
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
|
||||
return
|
||||
|
||||
lines = []
|
||||
in_accessibility = False
|
||||
for line in content.decode(errors="ignore").splitlines():
|
||||
if line.strip() == "DUMP OF SERVICE accessibility:":
|
||||
in_accessibility = True
|
||||
continue
|
||||
|
||||
if not in_accessibility:
|
||||
continue
|
||||
|
||||
if line.strip().startswith("------------------------------------------------------------------------------"):
|
||||
break
|
||||
|
||||
lines.append(line)
|
||||
|
||||
self.results = parse_dumpsys_accessibility("\n".join(lines))
|
||||
for result in self.results:
|
||||
log.info("Found installed accessibility service \"%s\"", result.get("service"))
|
||||
|
||||
self.log.info("Identified a total of %d accessibility services", len(self.results))
|
||||
61
mvt/android/modules/bugreport/activities.py
Normal file
61
mvt/android/modules/bugreport/activities.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 The MVT Project 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.parsers import parse_dumpsys_activity_resolver_table
|
||||
|
||||
from .base import BugReportModule
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Activities(BugReportModule):
|
||||
"""This module extracts details on receivers for risky activities."""
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
self.results = results if results else {}
|
||||
|
||||
def check_indicators(self):
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for intent, activities in self.results.items():
|
||||
for activity in activities:
|
||||
ioc = self.indicators.check_app_id(activity["package_name"])
|
||||
if ioc:
|
||||
activity["matched_indicator"] = ioc
|
||||
self.detected.append({intent: activity})
|
||||
continue
|
||||
|
||||
def run(self):
|
||||
content = self._get_dumpstate_file()
|
||||
if not content:
|
||||
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
|
||||
return
|
||||
|
||||
lines = []
|
||||
in_package = False
|
||||
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().startswith("------------------------------------------------------------------------------"):
|
||||
break
|
||||
|
||||
lines.append(line)
|
||||
|
||||
self.results = parse_dumpsys_activity_resolver_table("\n".join(lines))
|
||||
|
||||
self.log.info("Extracted activities for %d intents", len(self.results))
|
||||
70
mvt/android/modules/bugreport/base.py
Normal file
70
mvt/android/modules/bugreport/base.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 Claudio Guarnieri.
|
||||
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
|
||||
# https://github.com/mvt-project/mvt/blob/main/LICENSE
|
||||
|
||||
import fnmatch
|
||||
import logging
|
||||
import os
|
||||
|
||||
from mvt.common.module import MVTModule
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BugReportModule(MVTModule):
|
||||
"""This class provides a base for all Android Bug Report modules."""
|
||||
|
||||
zip_archive = None
|
||||
|
||||
def from_folder(self, extract_path, extract_files):
|
||||
self.extract_path = extract_path
|
||||
self.extract_files = extract_files
|
||||
|
||||
def from_zip(self, zip_archive, zip_files):
|
||||
self.zip_archive = zip_archive
|
||||
self.zip_files = zip_files
|
||||
|
||||
def _get_files_by_pattern(self, pattern):
|
||||
file_names = []
|
||||
if self.zip_archive:
|
||||
for zip_file in self.zip_files:
|
||||
file_names.append(zip_file)
|
||||
else:
|
||||
file_names = self.extract_files
|
||||
|
||||
return fnmatch.filter(file_names, pattern)
|
||||
|
||||
def _get_files_by_patterns(self, patterns):
|
||||
for pattern in patterns:
|
||||
matches = self._get_files_by_pattern(pattern)
|
||||
if matches:
|
||||
return matches
|
||||
|
||||
def _get_file_content(self, file_path):
|
||||
if self.zip_archive:
|
||||
handle = self.zip_archive.open(file_path)
|
||||
else:
|
||||
handle = open(os.path.join(self.extract_path, file_path), "rb")
|
||||
|
||||
data = handle.read()
|
||||
handle.close()
|
||||
|
||||
return data
|
||||
|
||||
def _get_dumpstate_file(self):
|
||||
main = self._get_files_by_pattern("main_entry.txt")
|
||||
if main:
|
||||
main_content = self._get_file_content(main[0])
|
||||
try:
|
||||
return self._get_file_content(main_content.decode().strip())
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
dumpstate_logs = self._get_files_by_pattern("dumpState_*.log")
|
||||
if not dumpstate_logs:
|
||||
return None
|
||||
|
||||
return self._get_file_content(dumpstate_logs[0])
|
||||
|
||||
return None
|
||||
76
mvt/android/modules/bugreport/battery_daily.py
Normal file
76
mvt/android/modules/bugreport/battery_daily.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 The MVT Project 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.parsers import parse_dumpsys_battery_daily
|
||||
|
||||
from .base import BugReportModule
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BatteryDaily(BugReportModule):
|
||||
"""This module extracts records from battery daily updates."""
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
def serialize(self, record):
|
||||
return {
|
||||
"timestamp": record["from"],
|
||||
"module": self.__class__.__name__,
|
||||
"event": "battery_daily",
|
||||
"data": f"Recorded update of package {record['package_name']} with vers {record['vers']}"
|
||||
}
|
||||
|
||||
def check_indicators(self):
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
ioc = self.indicators.check_app_id(result["package_name"])
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
def run(self):
|
||||
content = self._get_dumpstate_file()
|
||||
if not content:
|
||||
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
|
||||
return
|
||||
|
||||
lines = []
|
||||
in_batterystats = False
|
||||
in_daily = False
|
||||
for line in content.decode(errors="ignore").splitlines():
|
||||
if line.strip() == "DUMP OF SERVICE batterystats:":
|
||||
in_batterystats = True
|
||||
continue
|
||||
|
||||
if not in_batterystats:
|
||||
continue
|
||||
|
||||
if line.strip() == "Daily stats:":
|
||||
lines.append(line)
|
||||
in_daily = True
|
||||
continue
|
||||
|
||||
if not in_daily:
|
||||
continue
|
||||
|
||||
if line.strip() == "":
|
||||
break
|
||||
|
||||
lines.append(line)
|
||||
|
||||
self.results = parse_dumpsys_battery_daily("\n".join(lines))
|
||||
|
||||
self.log.info("Extracted a total of %d battery daily stats",
|
||||
len(self.results))
|
||||
60
mvt/android/modules/bugreport/battery_history.py
Normal file
60
mvt/android/modules/bugreport/battery_history.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 The MVT Project 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.parsers import parse_dumpsys_battery_history
|
||||
|
||||
from .base import BugReportModule
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BatteryHistory(BugReportModule):
|
||||
"""This module extracts records from battery daily updates."""
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
def check_indicators(self):
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
ioc = self.indicators.check_app_id(result["package_name"])
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
def run(self):
|
||||
content = self._get_dumpstate_file()
|
||||
if not content:
|
||||
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
|
||||
return
|
||||
|
||||
lines = []
|
||||
in_history = False
|
||||
for line in content.decode(errors="ignore").splitlines():
|
||||
if line.strip().startswith("Battery History "):
|
||||
lines.append(line)
|
||||
in_history = True
|
||||
continue
|
||||
|
||||
if not in_history:
|
||||
continue
|
||||
|
||||
if line.strip() == "":
|
||||
break
|
||||
|
||||
lines.append(line)
|
||||
|
||||
self.results = parse_dumpsys_battery_history("\n".join(lines))
|
||||
|
||||
self.log.info("Extracted a total of %d battery history records",
|
||||
len(self.results))
|
||||
63
mvt/android/modules/bugreport/dbinfo.py
Normal file
63
mvt/android/modules/bugreport/dbinfo.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 The MVT Project 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.parsers import parse_dumpsys_dbinfo
|
||||
|
||||
from .base import BugReportModule
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DBInfo(BugReportModule):
|
||||
"""This module extracts records from battery daily updates."""
|
||||
|
||||
slug = "dbinfo"
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
def check_indicators(self):
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
path = result.get("path", "")
|
||||
for part in path.split("/"):
|
||||
ioc = self.indicators.check_app_id(part)
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
def run(self):
|
||||
content = self._get_dumpstate_file()
|
||||
if not content:
|
||||
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
|
||||
return
|
||||
|
||||
in_dbinfo = False
|
||||
lines = []
|
||||
for line in content.decode(errors="ignore").splitlines():
|
||||
if line.strip() == "DUMP OF SERVICE dbinfo:":
|
||||
in_dbinfo = True
|
||||
continue
|
||||
|
||||
if not in_dbinfo:
|
||||
continue
|
||||
|
||||
if line.strip().startswith("------------------------------------------------------------------------------"):
|
||||
break
|
||||
|
||||
lines.append(line)
|
||||
|
||||
self.results = parse_dumpsys_dbinfo("\n".join(lines))
|
||||
|
||||
self.log.info("Extracted a total of %d database connection pool records",
|
||||
len(self.results))
|
||||
59
mvt/android/modules/bugreport/getprop.py
Normal file
59
mvt/android/modules/bugreport/getprop.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 The MVT Project 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 re
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from mvt.android.parsers import parse_getprop
|
||||
|
||||
from .base import BugReportModule
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Getprop(BugReportModule):
|
||||
"""This module extracts device properties from getprop command."""
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
self.results = {} if not results else results
|
||||
|
||||
def run(self):
|
||||
content = self._get_dumpstate_file()
|
||||
if not content:
|
||||
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
|
||||
return
|
||||
|
||||
lines = []
|
||||
in_getprop = False
|
||||
for line in content.decode(errors="ignore").splitlines():
|
||||
if line.strip() == "------ SYSTEM PROPERTIES (getprop) ------":
|
||||
in_getprop = True
|
||||
continue
|
||||
|
||||
if not in_getprop:
|
||||
continue
|
||||
|
||||
if line.strip() == "------":
|
||||
break
|
||||
|
||||
lines.append(line)
|
||||
|
||||
self.results = parse_getprop("\n".join(lines))
|
||||
|
||||
# Alert if phone is outdated.
|
||||
security_patch = self.results.get("ro.build.version.security_patch", "")
|
||||
if security_patch:
|
||||
patch_date = datetime.strptime(security_patch, "%Y-%m-%d")
|
||||
if (datetime.now() - patch_date) > timedelta(days=6*30):
|
||||
self.log.warning("This phone has not received security updates for more than "
|
||||
"six months (last update: %s)", security_patch)
|
||||
|
||||
self.log.info("Extracted %d Android system properties", len(self.results))
|
||||
117
mvt/android/modules/bugreport/packages.py
Normal file
117
mvt/android/modules/bugreport/packages.py
Normal file
@@ -0,0 +1,117 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 The MVT Project 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 re
|
||||
|
||||
from mvt.android.modules.adb.packages import Packages as PCK
|
||||
|
||||
from .base import BugReportModule
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Packages(BugReportModule):
|
||||
"""This module extracts details on receivers for risky activities."""
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
def serialize(self, record):
|
||||
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 ts in timestamps:
|
||||
records.append({
|
||||
"timestamp": ts["timestamp"],
|
||||
"module": self.__class__.__name__,
|
||||
"event": ts["event"],
|
||||
"data": f"Install or update of package {record['package_name']}",
|
||||
})
|
||||
|
||||
return records
|
||||
|
||||
def check_indicators(self):
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
ioc = self.indicators.check_app_id(result["package_name"])
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
@staticmethod
|
||||
def parse_packages_list(output):
|
||||
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 = PCK.parse_package_for_details("\n".join(lines))
|
||||
package.update(details)
|
||||
results.append(package)
|
||||
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)
|
||||
|
||||
return results
|
||||
|
||||
def run(self):
|
||||
content = self._get_dumpstate_file()
|
||||
if not content:
|
||||
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 = self.parse_packages_list("\n".join(lines))
|
||||
|
||||
self.log.info("Extracted details on %d packages", len(self.results))
|
||||
83
mvt/android/modules/bugreport/receivers.py
Normal file
83
mvt/android/modules/bugreport/receivers.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 The MVT Project 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.parsers import parse_dumpsys_receiver_resolver_table
|
||||
|
||||
from .base import BugReportModule
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
INTENT_NEW_OUTGOING_SMS = "android.provider.Telephony.NEW_OUTGOING_SMS"
|
||||
INTENT_SMS_RECEIVED = "android.provider.Telephony.SMS_RECEIVED"
|
||||
INTENT_DATA_SMS_RECEIVED = "android.intent.action.DATA_SMS_RECEIVED"
|
||||
INTENT_PHONE_STATE = "android.intent.action.PHONE_STATE"
|
||||
INTENT_NEW_OUTGOING_CALL = "android.intent.action.NEW_OUTGOING_CALL"
|
||||
|
||||
|
||||
class Receivers(BugReportModule):
|
||||
"""This module extracts details on receivers for risky activities."""
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
self.results = results if results else {}
|
||||
|
||||
def check_indicators(self):
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for intent, receivers in self.results.items():
|
||||
for receiver in receivers:
|
||||
if intent == INTENT_NEW_OUTGOING_SMS:
|
||||
self.log.info("Found a receiver to intercept outgoing SMS messages: \"%s\"",
|
||||
receiver["receiver"])
|
||||
elif intent == INTENT_SMS_RECEIVED:
|
||||
self.log.info("Found a receiver to intercept incoming SMS messages: \"%s\"",
|
||||
receiver["receiver"])
|
||||
elif intent == INTENT_DATA_SMS_RECEIVED:
|
||||
self.log.info("Found a receiver to intercept incoming data SMS message: \"%s\"",
|
||||
receiver["receiver"])
|
||||
elif intent == INTENT_PHONE_STATE:
|
||||
self.log.info("Found a receiver monitoring telephony state/incoming calls: \"%s\"",
|
||||
receiver["receiver"])
|
||||
elif intent == INTENT_NEW_OUTGOING_CALL:
|
||||
self.log.info("Found a receiver monitoring outgoing calls: \"%s\"",
|
||||
receiver["receiver"])
|
||||
|
||||
ioc = self.indicators.check_app_id(receiver["package_name"])
|
||||
if ioc:
|
||||
receiver["matched_indicator"] = ioc
|
||||
self.detected.append({intent: receiver})
|
||||
continue
|
||||
|
||||
def run(self):
|
||||
content = self._get_dumpstate_file()
|
||||
if not content:
|
||||
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
|
||||
return
|
||||
|
||||
in_receivers = False
|
||||
lines = []
|
||||
for line in content.decode(errors="ignore").splitlines():
|
||||
if line.strip() == "DUMP OF SERVICE package:":
|
||||
in_receivers = True
|
||||
continue
|
||||
|
||||
if not in_receivers:
|
||||
continue
|
||||
|
||||
if line.strip().startswith("------------------------------------------------------------------------------"):
|
||||
break
|
||||
|
||||
lines.append(line)
|
||||
|
||||
self.results = parse_dumpsys_receiver_resolver_table("\n".join(lines))
|
||||
|
||||
self.log.info("Extracted receivers for %d intents", len(self.results))
|
||||
11
mvt/android/parsers/__init__.py
Normal file
11
mvt/android/parsers/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 The MVT Project 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 .dumpsys import (parse_dumpsys_accessibility,
|
||||
parse_dumpsys_activity_resolver_table,
|
||||
parse_dumpsys_battery_daily,
|
||||
parse_dumpsys_battery_history, parse_dumpsys_dbinfo,
|
||||
parse_dumpsys_receiver_resolver_table)
|
||||
from .getprop import parse_getprop
|
||||
202
mvt/android/parsers/backup.py
Normal file
202
mvt/android/parsers/backup.py
Normal file
@@ -0,0 +1,202 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 The MVT Project 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 datetime
|
||||
import io
|
||||
import json
|
||||
import tarfile
|
||||
import zlib
|
||||
|
||||
from cryptography.hazmat.primitives import hashes, padding
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
|
||||
from mvt.common.utils import check_for_links, convert_timestamp_to_iso
|
||||
|
||||
PBKDF2_KEY_SIZE = 32
|
||||
|
||||
|
||||
class AndroidBackupParsingError(Exception):
|
||||
"""Exception raised file parsing an android backup file"""
|
||||
|
||||
|
||||
class AndroidBackupNotImplemented(AndroidBackupParsingError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidBackupPassword(AndroidBackupParsingError):
|
||||
pass
|
||||
|
||||
|
||||
def to_utf8_bytes(input_bytes):
|
||||
output = []
|
||||
for byte in input_bytes:
|
||||
if byte < ord(b'\x80'):
|
||||
output.append(byte)
|
||||
else:
|
||||
output.append(ord('\xef') | (byte >> 12))
|
||||
output.append(ord('\xbc') | ((byte >> 6) & ord('\x3f')))
|
||||
output.append(ord('\x80') | (byte & ord('\x3f')))
|
||||
return bytes(output)
|
||||
|
||||
|
||||
def parse_ab_header(data):
|
||||
"""
|
||||
Parse the header of an Android Backup file
|
||||
Returns a dict {'backup': True, 'compression': False,
|
||||
'encryption': "none", 'version': 4}
|
||||
"""
|
||||
if data.startswith(b"ANDROID BACKUP"):
|
||||
[magic_header, version, is_compressed, encryption, tar_data] = data.split(b"\n", 4)
|
||||
return {
|
||||
"backup": True,
|
||||
"compression": (is_compressed == b"1"),
|
||||
"version": int(version),
|
||||
"encryption": encryption.decode("utf-8")
|
||||
}
|
||||
|
||||
return {
|
||||
"backup": False,
|
||||
"compression": None,
|
||||
"version": None,
|
||||
"encryption": None
|
||||
}
|
||||
|
||||
|
||||
def decrypt_master_key(password, user_salt, user_iv, pbkdf2_rounds, master_key_blob, format_version, checksum_salt):
|
||||
"""Generate AES key from user password uisng PBKDF2
|
||||
|
||||
The backup master key is extracted from the master key blog after decryption.
|
||||
"""
|
||||
# Derive key from password using PBKDF2
|
||||
kdf = PBKDF2HMAC(algorithm=hashes.SHA1(), length=32, salt=user_salt, iterations=pbkdf2_rounds)
|
||||
key = kdf.derive(password.encode("utf-8"))
|
||||
|
||||
# Decrypt master key blob
|
||||
cipher = Cipher(algorithms.AES(key), modes.CBC(user_iv))
|
||||
decryptor = cipher.decryptor()
|
||||
try:
|
||||
decryted_master_key_blob = decryptor.update(master_key_blob) + decryptor.finalize()
|
||||
|
||||
# Extract key and IV from decrypted blob.
|
||||
key_blob = io.BytesIO(decryted_master_key_blob)
|
||||
master_iv_length = ord(key_blob.read(1))
|
||||
master_iv = key_blob.read(master_iv_length)
|
||||
|
||||
master_key_length = ord(key_blob.read(1))
|
||||
master_key = key_blob.read(master_key_length)
|
||||
|
||||
master_key_checksum_length = ord(key_blob.read(1))
|
||||
master_key_checksum = key_blob.read(master_key_checksum_length)
|
||||
except TypeError:
|
||||
raise InvalidBackupPassword()
|
||||
|
||||
# Handle quirky encoding of master key bytes in Android original Java crypto code
|
||||
if format_version > 1:
|
||||
hmac_mk = to_utf8_bytes(master_key)
|
||||
else:
|
||||
hmac_mk = master_key
|
||||
|
||||
# Derive checksum to confirm successful backup decryption.
|
||||
kdf = PBKDF2HMAC(algorithm=hashes.SHA1(), length=32, salt=checksum_salt, iterations=pbkdf2_rounds)
|
||||
calculated_checksum = kdf.derive(hmac_mk)
|
||||
|
||||
if master_key_checksum != calculated_checksum:
|
||||
raise InvalidBackupPassword()
|
||||
|
||||
return master_key, master_iv
|
||||
|
||||
|
||||
def decrypt_backup_data(encrypted_backup, password, encryption_algo, format_version):
|
||||
"""
|
||||
Generate encryption keyffrom password and do decryption
|
||||
"""
|
||||
if encryption_algo != b"AES-256":
|
||||
raise AndroidBackupNotImplemented("Encryption Algorithm not implemented")
|
||||
|
||||
if password is None:
|
||||
raise InvalidBackupPassword()
|
||||
|
||||
[user_salt, checksum_salt, pbkdf2_rounds, user_iv, master_key_blob, encrypted_data] = encrypted_backup.split(b"\n", 5)
|
||||
user_salt = bytes.fromhex(user_salt.decode("utf-8"))
|
||||
checksum_salt = bytes.fromhex(checksum_salt.decode("utf-8"))
|
||||
pbkdf2_rounds = int(pbkdf2_rounds)
|
||||
user_iv = bytes.fromhex(user_iv.decode("utf-8"))
|
||||
master_key_blob = bytes.fromhex(master_key_blob.decode("utf-8"))
|
||||
|
||||
# Derive decryption master key from password
|
||||
master_key, master_iv = decrypt_master_key(password=password, user_salt=user_salt, user_iv=user_iv,
|
||||
pbkdf2_rounds=pbkdf2_rounds, master_key_blob=master_key_blob,
|
||||
format_version=format_version, checksum_salt=checksum_salt)
|
||||
|
||||
# Decrypt and unpad backup data using derivied key
|
||||
cipher = Cipher(algorithms.AES(master_key), modes.CBC(master_iv))
|
||||
decryptor = cipher.decryptor()
|
||||
decrypted_tar = decryptor.update(encrypted_data) + decryptor.finalize()
|
||||
|
||||
unpadder = padding.PKCS7(128).unpadder()
|
||||
return unpadder.update(decrypted_tar)
|
||||
|
||||
|
||||
def parse_backup_file(data, password=None):
|
||||
"""
|
||||
Parse an ab file, returns a tar file
|
||||
"""
|
||||
if not data.startswith(b"ANDROID BACKUP"):
|
||||
raise AndroidBackupParsingError("Invalid file header")
|
||||
|
||||
[magic_header, version, is_compressed, encryption_algo, tar_data] = data.split(b"\n", 4)
|
||||
version = int(version)
|
||||
is_compressed = int(is_compressed)
|
||||
|
||||
if encryption_algo != b"none":
|
||||
tar_data = decrypt_backup_data(tar_data, password, encryption_algo, format_version=version)
|
||||
|
||||
if is_compressed:
|
||||
try:
|
||||
tar_data = zlib.decompress(tar_data)
|
||||
except zlib.error:
|
||||
raise AndroidBackupParsingError("Impossible to decompress the backup file")
|
||||
|
||||
return tar_data
|
||||
|
||||
|
||||
def parse_tar_for_sms(data):
|
||||
"""
|
||||
Extract SMS from a tar backup archive
|
||||
Returns an array of SMS
|
||||
"""
|
||||
dbytes = io.BytesIO(data)
|
||||
tar = tarfile.open(fileobj=dbytes)
|
||||
try:
|
||||
member = tar.getmember("apps/com.android.providers.telephony/d_f/000000_sms_backup")
|
||||
except KeyError:
|
||||
return []
|
||||
|
||||
dhandler = tar.extractfile(member)
|
||||
return parse_sms_file(dhandler.read())
|
||||
|
||||
|
||||
def parse_sms_file(data):
|
||||
"""
|
||||
Parse an SMS file extracted from a folder
|
||||
Returns a list of SMS entries
|
||||
"""
|
||||
res = []
|
||||
data = zlib.decompress(data)
|
||||
json_data = json.loads(data)
|
||||
|
||||
for entry in json_data:
|
||||
message_links = check_for_links(entry["body"])
|
||||
utc_timestamp = datetime.datetime.utcfromtimestamp(int(entry["date"]) / 1000)
|
||||
entry["isodate"] = convert_timestamp_to_iso(utc_timestamp)
|
||||
entry["direction"] = ("sent" if int(entry["date_sent"]) else "received")
|
||||
|
||||
# If we find links in the messages or if they are empty we add them to the list.
|
||||
if message_links or entry["body"].strip() == "":
|
||||
entry["links"] = message_links
|
||||
res.append(entry)
|
||||
|
||||
return res
|
||||
287
mvt/android/parsers/dumpsys.py
Normal file
287
mvt/android/parsers/dumpsys.py
Normal file
@@ -0,0 +1,287 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 The MVT Project 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
|
||||
|
||||
|
||||
def parse_dumpsys_accessibility(output):
|
||||
results = []
|
||||
|
||||
in_services = False
|
||||
for line in output.splitlines():
|
||||
if line.strip().startswith("installed services:"):
|
||||
in_services = True
|
||||
continue
|
||||
|
||||
if not in_services:
|
||||
continue
|
||||
|
||||
if line.strip() == "}":
|
||||
break
|
||||
|
||||
service = line.split(":")[1].strip()
|
||||
|
||||
results.append({
|
||||
"package_name": service.split("/")[0],
|
||||
"service": service,
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def parse_dumpsys_activity_resolver_table(output):
|
||||
results = {}
|
||||
|
||||
in_activity_resolver_table = False
|
||||
in_non_data_actions = False
|
||||
intent = None
|
||||
for line in output.splitlines():
|
||||
if line.startswith("Activity Resolver Table:"):
|
||||
in_activity_resolver_table = True
|
||||
continue
|
||||
|
||||
if not in_activity_resolver_table:
|
||||
continue
|
||||
|
||||
if line.startswith(" Non-Data Actions:"):
|
||||
in_non_data_actions = True
|
||||
continue
|
||||
|
||||
if not in_non_data_actions:
|
||||
continue
|
||||
|
||||
# If we hit an empty line, the Non-Data Actions section should be
|
||||
# finished.
|
||||
if line.strip() == "":
|
||||
break
|
||||
|
||||
# We detect the action name.
|
||||
if line.startswith(" " * 6) and not line.startswith(" " * 8) and ":" in line:
|
||||
intent = line.strip().replace(":", "")
|
||||
results[intent] = []
|
||||
continue
|
||||
|
||||
# If we are not in an intent block yet, skip.
|
||||
if not intent:
|
||||
continue
|
||||
|
||||
# If we are in a block but the line does not start with 8 spaces
|
||||
# it means the block ended a new one started, so we reset and
|
||||
# continue.
|
||||
if not line.startswith(" " * 8):
|
||||
intent = None
|
||||
continue
|
||||
|
||||
# If we got this far, we are processing receivers for the
|
||||
# activities we are interested in.
|
||||
activity = line.strip().split(" ")[1]
|
||||
package_name = activity.split("/")[0]
|
||||
|
||||
results[intent].append({
|
||||
"package_name": package_name,
|
||||
"activity": activity,
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def parse_dumpsys_battery_daily(output):
|
||||
results = []
|
||||
daily = None
|
||||
daily_updates = []
|
||||
for line in output.splitlines():
|
||||
if line.startswith(" Daily from "):
|
||||
if len(daily_updates) > 0:
|
||||
results.extend(daily_updates)
|
||||
daily_updates = []
|
||||
|
||||
timeframe = line[13:].strip()
|
||||
date_from, date_to = timeframe.strip(":").split(" to ", 1)
|
||||
daily = {"from": date_from[0:10], "to": date_to[0:10]}
|
||||
continue
|
||||
|
||||
if not daily:
|
||||
continue
|
||||
|
||||
if not line.strip().startswith("Update "):
|
||||
continue
|
||||
|
||||
line = line.strip().replace("Update ", "")
|
||||
package_name, vers = line.split(" ", 1)
|
||||
vers_nr = vers.split("=", 1)[1]
|
||||
|
||||
already_seen = False
|
||||
for update in daily_updates:
|
||||
if package_name == update["package_name"] and vers_nr == update["vers"]:
|
||||
already_seen = True
|
||||
break
|
||||
|
||||
if not already_seen:
|
||||
daily_updates.append({
|
||||
"action": "update",
|
||||
"from": daily["from"],
|
||||
"to": daily["to"],
|
||||
"package_name": package_name,
|
||||
"vers": vers_nr,
|
||||
})
|
||||
|
||||
if len(daily_updates) > 0:
|
||||
results.extend(daily_updates)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def parse_dumpsys_battery_history(output):
|
||||
results = []
|
||||
|
||||
for line in output.splitlines():
|
||||
if line.startswith("Battery History "):
|
||||
continue
|
||||
|
||||
if line.strip() == "":
|
||||
break
|
||||
|
||||
time_elapsed = line.strip().split(" ", 1)[0]
|
||||
|
||||
event = ""
|
||||
if line.find("+job") > 0:
|
||||
event = "start_job"
|
||||
uid = line[line.find("+job")+5:line.find(":")]
|
||||
service = line[line.find(":")+1:].strip('"')
|
||||
package_name = service.split("/")[0]
|
||||
elif line.find("-job") > 0:
|
||||
event = "end_job"
|
||||
uid = line[line.find("-job")+5:line.find(":")]
|
||||
service = line[line.find(":")+1:].strip('"')
|
||||
package_name = service.split("/")[0]
|
||||
elif line.find("+running +wake_lock=") > 0:
|
||||
uid = line[line.find("+running +wake_lock=")+21:line.find(":")]
|
||||
event = "wake"
|
||||
service = line[line.find("*walarm*:")+9:].split(" ")[0].strip('"').strip()
|
||||
if service == "" or "/" not in service:
|
||||
continue
|
||||
|
||||
package_name = service.split("/")[0]
|
||||
else:
|
||||
continue
|
||||
|
||||
results.append({
|
||||
"time_elapsed": time_elapsed,
|
||||
"event": event,
|
||||
"uid": uid,
|
||||
"package_name": package_name,
|
||||
"service": service,
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def parse_dumpsys_dbinfo(output):
|
||||
results = []
|
||||
|
||||
rxp = re.compile(r'.*\[([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3})\].*\[Pid:\((\d+)\)\](\w+).*sql\=\"(.+?)\"')
|
||||
rxp_no_pid = re.compile(r'.*\[([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3})\][ ]{1}(\w+).*sql\=\"(.+?)\"')
|
||||
|
||||
pool = None
|
||||
in_operations = False
|
||||
for line in output.splitlines():
|
||||
if line.startswith("Connection pool for "):
|
||||
pool = line.replace("Connection pool for ", "").rstrip(":")
|
||||
|
||||
if not pool:
|
||||
continue
|
||||
|
||||
if line.strip() == "Most recently executed operations:":
|
||||
in_operations = True
|
||||
continue
|
||||
|
||||
if not in_operations:
|
||||
continue
|
||||
|
||||
if not line.startswith(" "):
|
||||
in_operations = False
|
||||
pool = None
|
||||
continue
|
||||
|
||||
matches = rxp.findall(line)
|
||||
if not matches:
|
||||
matches = rxp_no_pid.findall(line)
|
||||
if not matches:
|
||||
continue
|
||||
else:
|
||||
match = matches[0]
|
||||
results.append({
|
||||
"isodate": match[0],
|
||||
"action": match[1],
|
||||
"sql": match[2],
|
||||
"path": pool,
|
||||
})
|
||||
else:
|
||||
match = matches[0]
|
||||
results.append({
|
||||
"isodate": match[0],
|
||||
"pid": match[1],
|
||||
"action": match[2],
|
||||
"sql": match[3],
|
||||
"path": pool,
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def parse_dumpsys_receiver_resolver_table(output):
|
||||
results = {}
|
||||
|
||||
in_receiver_resolver_table = False
|
||||
in_non_data_actions = False
|
||||
intent = None
|
||||
for line in output.splitlines():
|
||||
if line.startswith("Receiver Resolver Table:"):
|
||||
in_receiver_resolver_table = True
|
||||
continue
|
||||
|
||||
if not in_receiver_resolver_table:
|
||||
continue
|
||||
|
||||
if line.startswith(" Non-Data Actions:"):
|
||||
in_non_data_actions = True
|
||||
continue
|
||||
|
||||
if not in_non_data_actions:
|
||||
continue
|
||||
|
||||
# If we hit an empty line, the Non-Data Actions section should be
|
||||
# finished.
|
||||
if line.strip() == "":
|
||||
break
|
||||
|
||||
# We detect the action name.
|
||||
if line.startswith(" " * 6) and not line.startswith(" " * 8) and ":" in line:
|
||||
intent = line.strip().replace(":", "")
|
||||
results[intent] = []
|
||||
continue
|
||||
|
||||
# If we are not in an intent block yet, skip.
|
||||
if not intent:
|
||||
continue
|
||||
|
||||
# If we are in a block but the line does not start with 8 spaces
|
||||
# it means the block ended a new one started, so we reset and
|
||||
# continue.
|
||||
if not line.startswith(" " * 8):
|
||||
intent = None
|
||||
continue
|
||||
|
||||
# If we got this far, we are processing receivers for the
|
||||
# activities we are interested in.
|
||||
receiver = line.strip().split(" ")[1]
|
||||
package_name = receiver.split("/")[0]
|
||||
|
||||
results[intent].append({
|
||||
"package_name": package_name,
|
||||
"receiver": receiver,
|
||||
})
|
||||
|
||||
return results
|
||||
26
mvt/android/parsers/getprop.py
Normal file
26
mvt/android/parsers/getprop.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 The MVT Project 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
|
||||
|
||||
|
||||
def parse_getprop(output):
|
||||
results = {}
|
||||
rxp = re.compile(r"\[(.+?)\]: \[(.+?)\]")
|
||||
|
||||
for line in output.splitlines():
|
||||
line = line.strip()
|
||||
if line == "":
|
||||
continue
|
||||
|
||||
matches = re.findall(rxp, line)
|
||||
if not matches or len(matches[0]) != 2:
|
||||
continue
|
||||
|
||||
key = matches[0][0]
|
||||
value = matches[0][1]
|
||||
results[key] = value
|
||||
|
||||
return results
|
||||
@@ -1,4 +1,4 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
|
||||
@@ -1,53 +1,76 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project 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 json
|
||||
import os
|
||||
|
||||
import requests
|
||||
from appdirs import user_data_dir
|
||||
|
||||
from .url import URL
|
||||
|
||||
|
||||
class IndicatorsFileBadFormat(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Indicators:
|
||||
"""This class is used to parse indicators from a STIX2 file and provide
|
||||
functions to compare extracted artifacts to the indicators.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, log=None):
|
||||
self.data_dir = user_data_dir("mvt")
|
||||
self.log = log
|
||||
self.ioc_domains = []
|
||||
self.ioc_processes = []
|
||||
self.ioc_emails = []
|
||||
self.ioc_files = []
|
||||
self.ioc_files_sha256 = []
|
||||
self.ioc_app_ids = []
|
||||
self.ios_profile_ids = []
|
||||
self.ioc_count = 0
|
||||
self._check_env_variable()
|
||||
self.ioc_collections = []
|
||||
self.total_ioc_count = 0
|
||||
|
||||
def _add_indicator(self, ioc, iocs_list):
|
||||
if ioc not in iocs_list:
|
||||
iocs_list.append(ioc)
|
||||
self.ioc_count += 1
|
||||
def _load_downloaded_indicators(self):
|
||||
if not os.path.isdir(self.data_dir):
|
||||
return
|
||||
|
||||
def _check_env_variable(self):
|
||||
for f in os.listdir(self.data_dir):
|
||||
if f.lower().endswith(".stix2"):
|
||||
self.parse_stix2(os.path.join(self.data_dir, f))
|
||||
|
||||
def _check_stix2_env_variable(self):
|
||||
"""
|
||||
Checks if a variable MVT_STIX2 contains path to STIX Files
|
||||
Checks if a variable MVT_STIX2 contains path to a STIX files.
|
||||
"""
|
||||
if "MVT_STIX2" in os.environ:
|
||||
paths = os.environ["MVT_STIX2"].split(":")
|
||||
for path in paths:
|
||||
if os.path.isfile(path):
|
||||
self.parse_stix2(path)
|
||||
else:
|
||||
self.log.info("Invalid STIX2 path %s in MVT_STIX2 environment variable", path)
|
||||
if "MVT_STIX2" not in os.environ:
|
||||
return
|
||||
|
||||
paths = os.environ["MVT_STIX2"].split(":")
|
||||
for path in paths:
|
||||
if os.path.isfile(path):
|
||||
self.parse_stix2(path)
|
||||
else:
|
||||
self.log.error("Path specified with env MVT_STIX2 is not a valid file: %s",
|
||||
path)
|
||||
|
||||
def _new_collection(self, cid="", name="", description="", file_name="",
|
||||
file_path=""):
|
||||
return {
|
||||
"id": cid,
|
||||
"name": name,
|
||||
"description": description,
|
||||
"stix2_file_name": file_name,
|
||||
"stix2_file_path": file_path,
|
||||
"domains": [],
|
||||
"processes": [],
|
||||
"emails": [],
|
||||
"file_names": [],
|
||||
"file_paths": [],
|
||||
"files_sha256": [],
|
||||
"app_ids": [],
|
||||
"ios_profile_ids": [],
|
||||
"count": 0,
|
||||
}
|
||||
|
||||
def _add_indicator(self, ioc, ioc_coll, ioc_coll_list):
|
||||
ioc = ioc.strip("'")
|
||||
if ioc not in ioc_coll_list:
|
||||
ioc_coll_list.append(ioc)
|
||||
ioc_coll["count"] += 1
|
||||
self.total_ioc_count += 1
|
||||
|
||||
def parse_stix2(self, file_path):
|
||||
"""Extract indicators from a STIX2 file.
|
||||
@@ -56,59 +79,145 @@ class Indicators:
|
||||
:type file_path: str
|
||||
|
||||
"""
|
||||
self.log.info("Parsing STIX2 indicators file at path %s",
|
||||
file_path)
|
||||
self.log.info("Parsing STIX2 indicators file at path %s", file_path)
|
||||
|
||||
with open(file_path, "r") as handle:
|
||||
with open(file_path, "r", encoding="utf-8") as handle:
|
||||
try:
|
||||
data = json.load(handle)
|
||||
except json.decoder.JSONDecodeError:
|
||||
raise IndicatorsFileBadFormat("Unable to parse STIX2 indicators file, the file seems malformed or in the wrong format")
|
||||
self.log.critical("Unable to parse STIX2 indicator file. "
|
||||
"The file is corrupted or in the wrong format!")
|
||||
return
|
||||
|
||||
malware = {}
|
||||
indicators = []
|
||||
relationships = []
|
||||
for entry in data.get("objects", []):
|
||||
if entry.get("type", "") != "indicator":
|
||||
continue
|
||||
entry_type = entry.get("type", "")
|
||||
if entry_type == "malware":
|
||||
malware[entry["id"]] = {
|
||||
"name": entry["name"],
|
||||
"description": entry["description"],
|
||||
}
|
||||
elif entry_type == "indicator":
|
||||
indicators.append(entry)
|
||||
elif entry_type == "relationship":
|
||||
relationships.append(entry)
|
||||
|
||||
key, value = entry.get("pattern", "").strip("[]").split("=")
|
||||
value = value.strip("'")
|
||||
collections = []
|
||||
for mal_id, mal_values in malware.items():
|
||||
collection = self._new_collection(mal_id, mal_values.get("name"),
|
||||
mal_values.get("description"),
|
||||
os.path.basename(file_path),
|
||||
file_path)
|
||||
collections.append(collection)
|
||||
|
||||
if key == "domain-name:value":
|
||||
# We force domain names to lower case.
|
||||
self._add_indicator(ioc=value.lower(),
|
||||
iocs_list=self.ioc_domains)
|
||||
elif key == "process:name":
|
||||
self._add_indicator(ioc=value,
|
||||
iocs_list=self.ioc_processes)
|
||||
elif key == "email-addr:value":
|
||||
# We force email addresses to lower case.
|
||||
self._add_indicator(ioc=value.lower(),
|
||||
iocs_list=self.ioc_emails)
|
||||
elif key == "file:name":
|
||||
self._add_indicator(ioc=value,
|
||||
iocs_list=self.ioc_files)
|
||||
elif key == "app:id":
|
||||
self._add_indicator(ioc=value,
|
||||
iocs_list=self.ioc_app_ids)
|
||||
elif key == "configuration-profile:id":
|
||||
self._add_indicator(ioc=value,
|
||||
iocs_list=self.ios_profile_ids)
|
||||
elif key == "file:hashes.sha256":
|
||||
self._add_indicator(ioc=value,
|
||||
iocs_list=self.ioc_files_sha256)
|
||||
# We loop through all indicators.
|
||||
for indicator in indicators:
|
||||
malware_id = None
|
||||
|
||||
def check_domain(self, url) -> bool:
|
||||
# We loop through all relationships and find the one pertinent to
|
||||
# the current indicator.
|
||||
for relationship in relationships:
|
||||
if relationship["source_ref"] != indicator["id"]:
|
||||
continue
|
||||
|
||||
# Look for a malware definition with the correct identifier.
|
||||
if relationship["target_ref"] in malware.keys():
|
||||
malware_id = relationship["target_ref"]
|
||||
break
|
||||
|
||||
# Now we look for the correct collection matching the malware ID we
|
||||
# got from the relationship.
|
||||
for collection in collections:
|
||||
if collection["id"] != malware_id:
|
||||
continue
|
||||
|
||||
key, value = indicator.get("pattern", "").strip("[]").split("=")
|
||||
|
||||
if key == "domain-name:value":
|
||||
# We force domain names to lower case.
|
||||
self._add_indicator(ioc=value.lower(),
|
||||
ioc_coll=collection,
|
||||
ioc_coll_list=collection["domains"])
|
||||
elif key == "process:name":
|
||||
self._add_indicator(ioc=value,
|
||||
ioc_coll=collection,
|
||||
ioc_coll_list=collection["processes"])
|
||||
elif key == "email-addr:value":
|
||||
# We force email addresses to lower case.
|
||||
self._add_indicator(ioc=value.lower(),
|
||||
ioc_coll=collection,
|
||||
ioc_coll_list=collection["emails"])
|
||||
elif key == "file:name":
|
||||
self._add_indicator(ioc=value,
|
||||
ioc_coll=collection,
|
||||
ioc_coll_list=collection["file_names"])
|
||||
elif key == "file:path":
|
||||
self._add_indicator(ioc=value,
|
||||
ioc_coll=collection,
|
||||
ioc_coll_list=collection["file_paths"])
|
||||
elif key == "file:hashes.sha256":
|
||||
self._add_indicator(ioc=value,
|
||||
ioc_coll=collection,
|
||||
ioc_coll_list=collection["files_sha256"])
|
||||
elif key == "app:id":
|
||||
self._add_indicator(ioc=value,
|
||||
ioc_coll=collection,
|
||||
ioc_coll_list=collection["app_ids"])
|
||||
elif key == "configuration-profile:id":
|
||||
self._add_indicator(ioc=value,
|
||||
ioc_coll=collection,
|
||||
ioc_coll_list=collection["ios_profile_ids"])
|
||||
|
||||
break
|
||||
|
||||
for coll in collections:
|
||||
self.log.info("Extracted %d indicators for collection with name \"%s\"",
|
||||
coll["count"], coll["name"])
|
||||
|
||||
self.ioc_collections.extend(collections)
|
||||
|
||||
def load_indicators_files(self, files, load_default=True):
|
||||
"""
|
||||
Load a list of indicators files.
|
||||
"""
|
||||
for file_path in files:
|
||||
if os.path.isfile(file_path):
|
||||
self.parse_stix2(file_path)
|
||||
else:
|
||||
self.log.warning("No indicators file exists at path %s",
|
||||
file_path)
|
||||
|
||||
# Load downloaded indicators and any indicators from env variable.
|
||||
if load_default:
|
||||
self._load_downloaded_indicators()
|
||||
|
||||
self._check_stix2_env_variable()
|
||||
self.log.info("Loaded a total of %d unique indicators", self.total_ioc_count)
|
||||
|
||||
def get_iocs(self, ioc_type):
|
||||
for ioc_collection in self.ioc_collections:
|
||||
for ioc in ioc_collection.get(ioc_type, []):
|
||||
yield {
|
||||
"value": ioc,
|
||||
"type": ioc_type,
|
||||
"name": ioc_collection["name"],
|
||||
"stix2_file_name": ioc_collection["stix2_file_name"],
|
||||
}
|
||||
|
||||
def check_domain(self, url):
|
||||
"""Check if a given URL matches any of the provided domain indicators.
|
||||
|
||||
:param url: URL to match against domain indicators
|
||||
:type url: str
|
||||
:returns: True if the URL matched an indicator, otherwise False
|
||||
:rtype: bool
|
||||
:returns: Indicator details if matched, otherwise None
|
||||
|
||||
"""
|
||||
# TODO: If the IOC domain contains a subdomain, it is not currently
|
||||
# being matched.
|
||||
# being matched.
|
||||
if not url:
|
||||
return False
|
||||
return None
|
||||
|
||||
try:
|
||||
# First we use the provided URL.
|
||||
@@ -136,167 +245,236 @@ class Indicators:
|
||||
except Exception:
|
||||
# If URL parsing failed, we just try to do a simple substring
|
||||
# match.
|
||||
for ioc in self.ioc_domains:
|
||||
if ioc.lower() in url:
|
||||
self.log.warning("Maybe found a known suspicious domain: %s", url)
|
||||
return True
|
||||
for ioc in self.get_iocs("domains"):
|
||||
if ioc["value"].lower() in url:
|
||||
self.log.warning("Maybe found a known suspicious domain %s matching indicators from \"%s\"",
|
||||
url, ioc["name"])
|
||||
return ioc
|
||||
|
||||
# If nothing matched, we can quit here.
|
||||
return False
|
||||
return None
|
||||
|
||||
# If all parsing worked, we start walking through available domain indicators.
|
||||
for ioc in self.ioc_domains:
|
||||
for ioc in self.get_iocs("domains"):
|
||||
# First we check the full domain.
|
||||
if final_url.domain.lower() == ioc:
|
||||
if final_url.domain.lower() == ioc["value"]:
|
||||
if orig_url.is_shortened and orig_url.url != final_url.url:
|
||||
self.log.warning("Found a known suspicious domain %s shortened as %s",
|
||||
final_url.url, orig_url.url)
|
||||
self.log.warning("Found a known suspicious domain %s shortened as %s matching indicators from \"%s\"",
|
||||
final_url.url, orig_url.url, ioc["name"])
|
||||
else:
|
||||
self.log.warning("Found a known suspicious domain: %s", final_url.url)
|
||||
self.log.warning("Found a known suspicious domain %s matching indicators from \"%s\"",
|
||||
final_url.url, ioc["name"])
|
||||
|
||||
return True
|
||||
return ioc
|
||||
|
||||
# Then we just check the top level domain.
|
||||
if final_url.top_level.lower() == ioc:
|
||||
if final_url.top_level.lower() == ioc["value"]:
|
||||
if orig_url.is_shortened and orig_url.url != final_url.url:
|
||||
self.log.warning("Found a sub-domain matching a known suspicious top level %s shortened as %s",
|
||||
final_url.url, orig_url.url)
|
||||
self.log.warning("Found a sub-domain with suspicious top level %s shortened as %s matching indicators from \"%s\"",
|
||||
final_url.url, orig_url.url, ioc["name"])
|
||||
else:
|
||||
self.log.warning("Found a sub-domain matching a known suspicious top level: %s", final_url.url)
|
||||
self.log.warning("Found a sub-domain with a suspicious top level %s matching indicators from \"%s\"",
|
||||
final_url.url, ioc["name"])
|
||||
|
||||
return True
|
||||
return ioc
|
||||
|
||||
return False
|
||||
|
||||
def check_domains(self, urls) -> bool:
|
||||
def check_domains(self, urls):
|
||||
"""Check a list of URLs against the provided list of domain indicators.
|
||||
|
||||
:param urls: List of URLs to check against domain indicators
|
||||
:type urls: list
|
||||
:returns: True if any URL matched an indicator, otherwise False
|
||||
:rtype: bool
|
||||
:returns: Indicator details if matched, otherwise None
|
||||
|
||||
"""
|
||||
if not urls:
|
||||
return False
|
||||
return None
|
||||
|
||||
for url in urls:
|
||||
if self.check_domain(url):
|
||||
return True
|
||||
check = self.check_domain(url)
|
||||
if check:
|
||||
return check
|
||||
|
||||
return False
|
||||
|
||||
def check_process(self, process) -> bool:
|
||||
def check_process(self, process):
|
||||
"""Check the provided process name against the list of process
|
||||
indicators.
|
||||
|
||||
:param process: Process name to check against process indicators
|
||||
:type process: str
|
||||
:returns: True if process matched an indicator, otherwise False
|
||||
:rtype: bool
|
||||
:returns: Indicator details if matched, otherwise None
|
||||
|
||||
"""
|
||||
if not process:
|
||||
return False
|
||||
return None
|
||||
|
||||
proc_name = os.path.basename(process)
|
||||
if proc_name in self.ioc_processes:
|
||||
self.log.warning("Found a known suspicious process name \"%s\"", process)
|
||||
return True
|
||||
for ioc in self.get_iocs("processes"):
|
||||
if proc_name == ioc["value"]:
|
||||
self.log.warning("Found a known suspicious process name \"%s\" matching indicators from \"%s\"",
|
||||
process, ioc["name"])
|
||||
return ioc
|
||||
|
||||
if len(proc_name) == 16:
|
||||
for bad_proc in self.ioc_processes:
|
||||
if bad_proc.startswith(proc_name):
|
||||
self.log.warning("Found a truncated known suspicious process name \"%s\"", process)
|
||||
return True
|
||||
if len(proc_name) == 16:
|
||||
if ioc["value"].startswith(proc_name):
|
||||
self.log.warning("Found a truncated known suspicious process name \"%s\" matching indicators from \"%s\"",
|
||||
process, ioc["name"])
|
||||
return ioc
|
||||
|
||||
return False
|
||||
|
||||
def check_processes(self, processes) -> bool:
|
||||
def check_processes(self, processes):
|
||||
"""Check the provided list of processes against the list of
|
||||
process indicators.
|
||||
|
||||
:param processes: List of processes to check against process indicators
|
||||
:type processes: list
|
||||
:returns: True if process matched an indicator, otherwise False
|
||||
:rtype: bool
|
||||
:returns: Indicator details if matched, otherwise None
|
||||
|
||||
"""
|
||||
if not processes:
|
||||
return False
|
||||
return None
|
||||
|
||||
for process in processes:
|
||||
if self.check_process(process):
|
||||
return True
|
||||
check = self.check_process(process)
|
||||
if check:
|
||||
return check
|
||||
|
||||
return False
|
||||
|
||||
def check_email(self, email) -> bool:
|
||||
def check_email(self, email):
|
||||
"""Check the provided email against the list of email indicators.
|
||||
|
||||
:param email: Email address to check against email indicators
|
||||
:type email: str
|
||||
:returns: True if email address matched an indicator, otherwise False
|
||||
:rtype: bool
|
||||
:returns: Indicator details if matched, otherwise None
|
||||
|
||||
"""
|
||||
if not email:
|
||||
return False
|
||||
return None
|
||||
|
||||
if email.lower() in self.ioc_emails:
|
||||
self.log.warning("Found a known suspicious email address: \"%s\"", email)
|
||||
return True
|
||||
for ioc in self.get_iocs("emails"):
|
||||
if email.lower() == ioc["value"].lower():
|
||||
self.log.warning("Found a known suspicious email address \"%s\" matching indicators from \"%s\"",
|
||||
email, ioc["name"])
|
||||
return ioc
|
||||
|
||||
return False
|
||||
def check_file_name(self, file_name):
|
||||
"""Check the provided file name against the list of file indicators.
|
||||
|
||||
def check_filename(self, file_path) -> bool:
|
||||
"""Check the provided file path against the list of file indicators.
|
||||
:param file_name: File name to check against file
|
||||
indicators
|
||||
:type file_name: str
|
||||
:returns: Indicator details if matched, otherwise None
|
||||
|
||||
"""
|
||||
if not file_name:
|
||||
return None
|
||||
|
||||
for ioc in self.get_iocs("file_names"):
|
||||
if ioc["value"] == file_name:
|
||||
self.log.warning("Found a known suspicious file name \"%s\" matching indicators from \"%s\"",
|
||||
file_name, ioc["name"])
|
||||
return ioc
|
||||
|
||||
def check_file_path(self, file_path):
|
||||
"""Check the provided file path against the list of file indicators (both path and name).
|
||||
|
||||
:param file_path: File path or file name to check against file
|
||||
indicators
|
||||
:type file_path: str
|
||||
:returns: True if the file path matched an indicator, otherwise False
|
||||
:rtype: bool
|
||||
:returns: Indicator details if matched, otherwise None
|
||||
|
||||
"""
|
||||
if not file_path:
|
||||
return False
|
||||
return None
|
||||
|
||||
file_name = os.path.basename(file_path)
|
||||
if file_name in self.ioc_files:
|
||||
return True
|
||||
ioc = self.check_file_name(os.path.basename(file_path))
|
||||
if ioc:
|
||||
return ioc
|
||||
|
||||
return False
|
||||
|
||||
def check_file_path(self, file_path) -> bool:
|
||||
"""Check the provided file path against the list of file indicators.
|
||||
|
||||
:param file_path: File path or file name to check against file
|
||||
indicators
|
||||
:type file_path: str
|
||||
:returns: True if the file path matched an indicator, otherwise False
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
if not file_path:
|
||||
return False
|
||||
|
||||
for ioc_file in self.ioc_files:
|
||||
for ioc in self.get_iocs("file_paths"):
|
||||
# Strip any trailing slash from indicator paths to match directories.
|
||||
if file_path.startswith(ioc_file.rstrip("/")):
|
||||
return True
|
||||
return False
|
||||
if file_path.startswith(ioc["value"].rstrip("/")):
|
||||
self.log.warning("Found a known suspicious file path \"%s\" matching indicators form \"%s\"",
|
||||
file_path, ioc["name"])
|
||||
return ioc
|
||||
|
||||
def check_profile(self, profile_uuid) -> bool:
|
||||
def check_profile(self, profile_uuid):
|
||||
"""Check the provided configuration profile UUID against the list of indicators.
|
||||
|
||||
:param profile_uuid: Profile UUID to check against configuration profile indicators
|
||||
:type profile_uuid: str
|
||||
:returns: True if the UUID in indicator list, otherwise False
|
||||
:rtype: bool
|
||||
:returns: Indicator details if matched, otherwise None
|
||||
|
||||
"""
|
||||
if profile_uuid in self.ios_profile_ids:
|
||||
return True
|
||||
if not profile_uuid:
|
||||
return None
|
||||
|
||||
return False
|
||||
for ioc in self.get_iocs("ios_profile_ids"):
|
||||
if profile_uuid in ioc["value"]:
|
||||
self.log.warning("Found a known suspicious profile ID \"%s\" matching indicators from \"%s\"",
|
||||
profile_uuid, ioc["name"])
|
||||
return ioc
|
||||
|
||||
def check_file_hash(self, file_hash):
|
||||
"""Check the provided SHA256 file hash against the list of indicators.
|
||||
|
||||
:param file_hash: SHA256 hash to check
|
||||
:type file_hash: str
|
||||
:returns: Indicator details if matched, otherwise None
|
||||
|
||||
"""
|
||||
if not file_hash:
|
||||
return None
|
||||
|
||||
for ioc in self.get_iocs("files_sha256"):
|
||||
if file_hash.lower() == ioc["value"].lower():
|
||||
self.log.warning("Found a known suspicious file with hash \"%s\" matching indicators from \"%s\"",
|
||||
file_hash, ioc["name"])
|
||||
return ioc
|
||||
|
||||
def check_app_id(self, app_id):
|
||||
"""Check the provided app identifier (typically an Android package name)
|
||||
against the list of indicators.
|
||||
|
||||
:param app_id: App ID to check against the list of indicators
|
||||
:type app_id: str
|
||||
:returns: Indicator details if matched, otherwise None
|
||||
|
||||
"""
|
||||
if not app_id:
|
||||
return None
|
||||
|
||||
for ioc in self.get_iocs("app_ids"):
|
||||
if app_id.lower() == ioc["value"].lower():
|
||||
self.log.warning("Found a known suspicious app with ID \"%s\" matching indicators from \"%s\"",
|
||||
app_id, ioc["name"])
|
||||
return ioc
|
||||
|
||||
|
||||
def download_indicators_files(log):
|
||||
"""
|
||||
Download indicators from repo into MVT app data directory.
|
||||
"""
|
||||
data_dir = user_data_dir("mvt")
|
||||
if not os.path.isdir(data_dir):
|
||||
os.makedirs(data_dir, exist_ok=True)
|
||||
|
||||
# Download latest list of indicators from the MVT repo.
|
||||
res = requests.get("https://github.com/mvt-project/mvt/raw/main/public_indicators.json")
|
||||
if res.status_code != 200:
|
||||
log.warning("Unable to find retrieve list of indicators from the MVT repository.")
|
||||
return
|
||||
|
||||
for ioc_entry in res.json():
|
||||
ioc_url = ioc_entry["stix2_url"]
|
||||
log.info("Downloading indicator file %s from %s", ioc_entry["name"], ioc_url)
|
||||
|
||||
res = requests.get(ioc_url)
|
||||
if res.status_code != 200:
|
||||
log.warning("Could not find indicator file %s", ioc_url)
|
||||
continue
|
||||
|
||||
clean_file_name = ioc_url.lstrip("https://").replace("/", "_")
|
||||
ioc_path = os.path.join(data_dir, clean_file_name)
|
||||
|
||||
# Write file to disk. This will overwrite any older version of the STIX2 file.
|
||||
with open(ioc_path, "w", encoding="utf-8") as handle:
|
||||
handle.write(res.text)
|
||||
|
||||
log.info("Saved indicator file to %s", os.path.basename(ioc_path))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project 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 csv
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
|
||||
@@ -30,7 +29,7 @@ class MVTModule(object):
|
||||
slug = None
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
fast_mode=False, log=None, results=[]):
|
||||
fast_mode=False, log=None, results=None):
|
||||
"""Initialize module.
|
||||
|
||||
:param file_path: Path to the module's database file, if there is any
|
||||
@@ -51,14 +50,14 @@ class MVTModule(object):
|
||||
self.fast_mode = fast_mode
|
||||
self.log = log
|
||||
self.indicators = None
|
||||
self.results = results
|
||||
self.results = results if results else []
|
||||
self.detected = []
|
||||
self.timeline = []
|
||||
self.timeline_detected = []
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_path, log=None):
|
||||
with open(json_path, "r") as handle:
|
||||
with open(json_path, "r", encoding="utf-8") as handle:
|
||||
results = json.load(handle)
|
||||
if log:
|
||||
log.info("Loaded %d results from \"%s\"",
|
||||
@@ -91,7 +90,7 @@ class MVTModule(object):
|
||||
if self.results:
|
||||
results_file_name = f"{name}.json"
|
||||
results_json_path = os.path.join(self.output_folder, results_file_name)
|
||||
with io.open(results_json_path, "w", encoding="utf-8") as handle:
|
||||
with open(results_json_path, "w", encoding="utf-8") as handle:
|
||||
try:
|
||||
json.dump(self.results, handle, indent=4, default=str)
|
||||
except Exception as e:
|
||||
@@ -101,7 +100,7 @@ class MVTModule(object):
|
||||
if self.detected:
|
||||
detected_file_name = f"{name}_detected.json"
|
||||
detected_json_path = os.path.join(self.output_folder, detected_file_name)
|
||||
with io.open(detected_json_path, "w", encoding="utf-8") as handle:
|
||||
with open(detected_json_path, "w", encoding="utf-8") as handle:
|
||||
json.dump(self.detected, handle, indent=4, default=str)
|
||||
|
||||
def serialize(self, record):
|
||||
@@ -192,7 +191,7 @@ def save_timeline(timeline, timeline_path):
|
||||
:param timeline_path: Path to the csv file to store the timeline to
|
||||
|
||||
"""
|
||||
with io.open(timeline_path, "a+", encoding="utf-8") as handle:
|
||||
with open(timeline_path, "a+", encoding="utf-8") as handle:
|
||||
csvoutput = csv.writer(handle, delimiter=",", quotechar="\"")
|
||||
csvoutput.writerow(["UTC Timestamp", "Plugin", "Event", "Description"])
|
||||
for event in sorted(timeline, key=lambda x: x["timestamp"] if x["timestamp"] is not None else ""):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -73,7 +73,7 @@ def check_for_links(text):
|
||||
:returns: Search results.
|
||||
|
||||
"""
|
||||
return re.findall("(?P<url>https?://[^\s]+)", text, re.IGNORECASE)
|
||||
return re.findall(r"(?P<url>https?://[^\s]+)", text, re.IGNORECASE)
|
||||
|
||||
|
||||
def get_sha256_from_file_path(file_path):
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project 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 requests
|
||||
from packaging import version
|
||||
|
||||
MVT_VERSION = "1.4.0"
|
||||
MVT_VERSION = "1.5.3"
|
||||
|
||||
|
||||
def check_for_updates():
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -10,10 +10,10 @@ import click
|
||||
from rich.logging import RichHandler
|
||||
from rich.prompt import Prompt
|
||||
|
||||
from mvt.common.help import HELP_MSG_MODULE, HELP_MSG_IOC
|
||||
from mvt.common.help import HELP_MSG_FAST, HELP_MSG_OUTPUT
|
||||
from mvt.common.help import HELP_MSG_LIST_MODULES
|
||||
from mvt.common.indicators import Indicators, IndicatorsFileBadFormat
|
||||
from mvt.common.help import (HELP_MSG_FAST, HELP_MSG_IOC,
|
||||
HELP_MSG_LIST_MODULES, HELP_MSG_MODULE,
|
||||
HELP_MSG_OUTPUT)
|
||||
from mvt.common.indicators import Indicators, download_indicators_files
|
||||
from mvt.common.logo import logo
|
||||
from mvt.common.module import run_module, save_timeline
|
||||
from mvt.common.options import MutuallyExclusiveOption
|
||||
@@ -157,13 +157,7 @@ def check_backup(ctx, iocs, output, fast, backup_path, list_modules, module):
|
||||
ctx.exit(1)
|
||||
|
||||
indicators = Indicators(log=log)
|
||||
for ioc_path in iocs:
|
||||
try:
|
||||
indicators.parse_stix2(ioc_path)
|
||||
except IndicatorsFileBadFormat as e:
|
||||
log.critical(e)
|
||||
ctx.exit(1)
|
||||
log.info("Loaded a total of %d indicators", indicators.ioc_count)
|
||||
indicators.load_indicators_files(iocs)
|
||||
|
||||
timeline = []
|
||||
timeline_detected = []
|
||||
@@ -174,8 +168,7 @@ def check_backup(ctx, iocs, output, fast, backup_path, list_modules, module):
|
||||
m = backup_module(base_folder=backup_path, output_folder=output, fast_mode=fast,
|
||||
log=logging.getLogger(backup_module.__module__))
|
||||
m.is_backup = True
|
||||
|
||||
if indicators.ioc_count > 0:
|
||||
if indicators.total_ioc_count > 0:
|
||||
m.indicators = indicators
|
||||
m.indicators.log = m.log
|
||||
|
||||
@@ -189,6 +182,10 @@ def check_backup(ctx, iocs, output, fast, backup_path, list_modules, module):
|
||||
if len(timeline_detected) > 0:
|
||||
save_timeline(timeline_detected, os.path.join(output, "timeline_detected.csv"))
|
||||
|
||||
if len(timeline_detected) > 0:
|
||||
log.warning("The analysis of the backup produced %d detections!",
|
||||
len(timeline_detected))
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# Command: check-fs
|
||||
@@ -220,13 +217,7 @@ def check_fs(ctx, iocs, output, fast, dump_path, list_modules, module):
|
||||
ctx.exit(1)
|
||||
|
||||
indicators = Indicators(log=log)
|
||||
for ioc_path in iocs:
|
||||
try:
|
||||
indicators.parse_stix2(ioc_path)
|
||||
except IndicatorsFileBadFormat as e:
|
||||
log.critical(e)
|
||||
ctx.exit(1)
|
||||
log.info("Loaded a total of %d indicators", indicators.ioc_count)
|
||||
indicators.load_indicators_files(iocs)
|
||||
|
||||
timeline = []
|
||||
timeline_detected = []
|
||||
@@ -238,8 +229,7 @@ def check_fs(ctx, iocs, output, fast, dump_path, list_modules, module):
|
||||
log=logging.getLogger(fs_module.__module__))
|
||||
|
||||
m.is_fs_dump = True
|
||||
|
||||
if iocs:
|
||||
if indicators.total_ioc_count > 0:
|
||||
m.indicators = indicators
|
||||
m.indicators.log = m.log
|
||||
|
||||
@@ -253,20 +243,24 @@ def check_fs(ctx, iocs, output, fast, dump_path, list_modules, module):
|
||||
if len(timeline_detected) > 0:
|
||||
save_timeline(timeline_detected, os.path.join(output, "timeline_detected.csv"))
|
||||
|
||||
if len(timeline_detected) > 0:
|
||||
log.warning("The analysis of the filesystem produced %d detections!",
|
||||
len(timeline_detected))
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# Command: check-iocs
|
||||
#==============================================================================
|
||||
@cli.command("check-iocs", help="Compare stored JSON results to provided indicators")
|
||||
@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True,
|
||||
default=[], required=True, help=HELP_MSG_IOC)
|
||||
default=[], help=HELP_MSG_IOC)
|
||||
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
||||
@click.option("--module", "-m", help=HELP_MSG_MODULE)
|
||||
@click.argument("FOLDER", type=click.Path(exists=True))
|
||||
@click.pass_context
|
||||
def check_iocs(ctx, iocs, list_modules, module, folder):
|
||||
all_modules = []
|
||||
for entry in BACKUP_MODULES + FS_MODULES:
|
||||
for entry in BACKUP_MODULES + FS_MODULES + MIXED_MODULES:
|
||||
if entry not in all_modules:
|
||||
all_modules.append(entry)
|
||||
|
||||
@@ -280,14 +274,9 @@ def check_iocs(ctx, iocs, list_modules, module, folder):
|
||||
log.info("Checking stored results against provided indicators...")
|
||||
|
||||
indicators = Indicators(log=log)
|
||||
for ioc_path in iocs:
|
||||
try:
|
||||
indicators.parse_stix2(ioc_path)
|
||||
except IndicatorsFileBadFormat as e:
|
||||
log.critical(e)
|
||||
ctx.exit(1)
|
||||
log.info("Loaded a total of %d indicators", indicators.ioc_count)
|
||||
indicators.load_indicators_files(iocs)
|
||||
|
||||
total_detections = 0
|
||||
for file_name in os.listdir(folder):
|
||||
name_only, ext = os.path.splitext(file_name)
|
||||
file_path = os.path.join(folder, file_name)
|
||||
@@ -304,11 +293,25 @@ def check_iocs(ctx, iocs, list_modules, module, folder):
|
||||
|
||||
m = iocs_module.from_json(file_path,
|
||||
log=logging.getLogger(iocs_module.__module__))
|
||||
|
||||
m.indicators = indicators
|
||||
m.indicators.log = m.log
|
||||
if indicators.total_ioc_count > 0:
|
||||
m.indicators = indicators
|
||||
m.indicators.log = m.log
|
||||
|
||||
try:
|
||||
m.check_indicators()
|
||||
except NotImplementedError:
|
||||
continue
|
||||
else:
|
||||
total_detections += len(m.detected)
|
||||
|
||||
if total_detections > 0:
|
||||
log.warning("The check of the results produced %d detections!",
|
||||
total_detections)
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# Command: download-iocs
|
||||
#==============================================================================
|
||||
@cli.command("download-iocs", help="Download public STIX2 indicators")
|
||||
def download_iocs():
|
||||
download_indicators_files(log)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -185,7 +185,7 @@ class DecryptBackup:
|
||||
return
|
||||
|
||||
try:
|
||||
with open(key_path, 'w') as handle:
|
||||
with open(key_path, 'w', encoding="utf-8") as handle:
|
||||
handle.write(self._decryption_key)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -7,6 +7,7 @@ import os
|
||||
import plistlib
|
||||
|
||||
from mvt.common.module import DatabaseNotFoundError
|
||||
from mvt.ios.versions import latest_ios_version
|
||||
|
||||
from ..base import IOSExtraction
|
||||
|
||||
@@ -41,3 +42,9 @@ class BackupInfo(IOSExtraction):
|
||||
value = info.get(field, None)
|
||||
self.log.info("%s: %s", field, value)
|
||||
self.results[field] = value
|
||||
|
||||
if "Product Version" in info:
|
||||
latest = latest_ios_version()
|
||||
if info["Product Version"] != latest["version"]:
|
||||
self.log.warning("This phone is running an outdated iOS version: %s (latest is %s)",
|
||||
info["Product Version"], latest['version'])
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project 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 os
|
||||
import plistlib
|
||||
from base64 import b64encode
|
||||
|
||||
from mvt.common.utils import convert_timestamp_to_iso
|
||||
|
||||
from ..base import IOSExtraction
|
||||
@@ -45,8 +45,10 @@ class ConfigurationProfiles(IOSExtraction):
|
||||
payload_content = result["plist"]["PayloadContent"][0]
|
||||
|
||||
# Alert on any known malicious configuration profiles in the indicator list.
|
||||
if self.indicators.check_profile(result["plist"]["PayloadUUID"]):
|
||||
ioc = self.indicators.check_profile(result["plist"]["PayloadUUID"])
|
||||
if ioc:
|
||||
self.log.warning(f"Found a known malicious configuration profile \"{result['plist']['PayloadDisplayName']}\" with UUID '{result['plist']['PayloadUUID']}'.")
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
@@ -70,11 +72,14 @@ class ConfigurationProfiles(IOSExtraction):
|
||||
with open(conf_file_path, "rb") as handle:
|
||||
try:
|
||||
conf_plist = plistlib.load(handle)
|
||||
except:
|
||||
except Exception:
|
||||
conf_plist = {}
|
||||
|
||||
if "SignerCerts" in conf_plist:
|
||||
conf_plist["SignerCerts"] = [b64encode(x) for x in conf_plist["SignerCerts"]]
|
||||
if "OTAProfileStub" in conf_plist:
|
||||
if "SignerCerts" in conf_plist["OTAProfileStub"]:
|
||||
conf_plist["OTAProfileStub"]["SignerCerts"] = [b64encode(x) for x in conf_plist["OTAProfileStub"]["SignerCerts"]]
|
||||
if "PushTokenDataSentToServerKey" in conf_plist:
|
||||
conf_plist["PushTokenDataSentToServerKey"] = b64encode(conf_plist["PushTokenDataSentToServerKey"])
|
||||
if "LastPushTokenHash" in conf_plist:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -72,9 +72,7 @@ class Manifest(IOSExtraction):
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
if "relative_path" not in result:
|
||||
continue
|
||||
if not result["relative_path"]:
|
||||
if not result.get("relative_path"):
|
||||
continue
|
||||
|
||||
if result["domain"]:
|
||||
@@ -83,16 +81,15 @@ class Manifest(IOSExtraction):
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
if self.indicators.check_filename(result["relative_path"]):
|
||||
self.log.warning("Found a known malicious file at path: %s", result["relative_path"])
|
||||
if self.indicators.check_file_path("/" + result["relative_path"]):
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
relPath = result["relative_path"].lower()
|
||||
for ioc in self.indicators.ioc_domains:
|
||||
if ioc.lower() in relPath:
|
||||
rel_path = result["relative_path"].lower()
|
||||
for ioc in self.indicators.get_iocs("domains"):
|
||||
if ioc["value"].lower() in rel_path:
|
||||
self.log.warning("Found mention of domain \"%s\" in a backup file with path: %s",
|
||||
ioc, relPath)
|
||||
ioc["value"], rel_path)
|
||||
self.detected.append(result)
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -37,20 +37,24 @@ class Analytics(IOSExtraction):
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
for ioc in self.indicators.ioc_processes:
|
||||
for key in result.keys():
|
||||
if ioc == result[key]:
|
||||
self.log.warning("Found mention of a malicious process \"%s\" in %s file at %s",
|
||||
ioc, result["artifact"], result["timestamp"])
|
||||
self.detected.append(result)
|
||||
break
|
||||
for ioc in self.indicators.ioc_domains:
|
||||
for key in result.keys():
|
||||
if ioc in str(result[key]):
|
||||
self.log.warning("Found mention of a malicious domain \"%s\" in %s file at %s",
|
||||
ioc, result["artifact"], result["timestamp"])
|
||||
self.detected.append(result)
|
||||
break
|
||||
for value in result.values():
|
||||
if not isinstance(value, str):
|
||||
continue
|
||||
|
||||
ioc = self.indicators.check_process(value)
|
||||
if ioc:
|
||||
self.log.warning("Found mention of a malicious process \"%s\" in %s file at %s",
|
||||
value, result["artifact"], result["timestamp"])
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
ioc = self.indicators.check_domain(value)
|
||||
if ioc:
|
||||
self.log.warning("Found mention of a malicious domain \"%s\" in %s file at %s",
|
||||
value, result["artifact"], result["timestamp"])
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
def _extract_analytics_data(self):
|
||||
artifact = self.file_path.split("/")[-1]
|
||||
@@ -101,6 +105,7 @@ class Analytics(IOSExtraction):
|
||||
timestamp = ""
|
||||
data = plistlib.loads(row[1])
|
||||
data["timestamp"] = timestamp
|
||||
|
||||
data["artifact"] = artifact
|
||||
|
||||
self.results.append(data)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -34,13 +34,15 @@ class CacheFiles(IOSExtraction):
|
||||
return
|
||||
|
||||
self.detected = {}
|
||||
for key, items in self.results.items():
|
||||
for item in items:
|
||||
if self.indicators.check_domain(item["url"]):
|
||||
for key, values in self.results.items():
|
||||
for value in values:
|
||||
ioc = self.indicators.check_domain(value["url"])
|
||||
if ioc:
|
||||
value["matched_indicator"] = ioc
|
||||
if key not in self.detected:
|
||||
self.detected[key] = [item, ]
|
||||
self.detected[key] = [value, ]
|
||||
else:
|
||||
self.detected[key].append(item)
|
||||
self.detected[key].append(value)
|
||||
|
||||
def _process_cache_file(self, file_path):
|
||||
self.log.info("Processing cache file at path: %s", file_path)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -37,23 +37,25 @@ class Filesystem(IOSExtraction):
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
if self.indicators.check_file(result["path"]):
|
||||
self.log.warning("Found a known malicious file name at path: %s", result["path"])
|
||||
if "path" not in result:
|
||||
continue
|
||||
|
||||
ioc = self.indicators.check_file_path(result["path"])
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
if self.indicators.check_file_path(result["path"]):
|
||||
self.log.warning("Found a known malicious file path at path: %s", result["path"])
|
||||
self.detected.append(result)
|
||||
|
||||
# If we are instructed to run fast, we skip this.
|
||||
# If we are instructed to run fast, we skip the rest.
|
||||
if self.fast_mode:
|
||||
self.log.info("Flag --fast was enabled: skipping extended search for suspicious files/processes")
|
||||
else:
|
||||
for ioc in self.indicators.ioc_processes:
|
||||
parts = result["path"].split("/")
|
||||
if ioc in parts:
|
||||
self.log.warning("Found a known malicious file/process at path: %s", result["path"])
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
for ioc in self.indicators.get_iocs("processes"):
|
||||
parts = result["path"].split("/")
|
||||
if ioc["value"] in parts:
|
||||
self.log.warning("Found known suspicious process name mentioned in file at path \"%s\" matching indicators from \"%s\"",
|
||||
result["path"], ioc["name"])
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
def run(self):
|
||||
for root, dirs, files in os.walk(self.base_folder):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -37,7 +37,12 @@ class SafariFavicon(IOSExtraction):
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
if self.indicators.check_domain(result["url"]) or self.indicators.check_domain(result["icon_url"]):
|
||||
ioc = self.indicators.check_domain(result["url"])
|
||||
if not ioc:
|
||||
ioc = self.indicators.check_domain(result["icon_url"])
|
||||
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
def _process_favicon_db(self, file_path):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -34,12 +34,20 @@ class ShutdownLog(IOSExtraction):
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
for ioc in self.indicators.ioc_processes:
|
||||
ioc = self.indicators.check_file_path(result["client"])
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
for ioc in self.indicators.get_iocs("processes"):
|
||||
parts = result["client"].split("/")
|
||||
if ioc in parts:
|
||||
self.log.warning("Found mention of a known malicious process \"%s\" in shutdown.log",
|
||||
ioc)
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
def process_shutdownlog(self, content):
|
||||
current_processes = []
|
||||
@@ -78,5 +86,5 @@ class ShutdownLog(IOSExtraction):
|
||||
def run(self):
|
||||
self._find_ios_database(root_paths=SHUTDOWN_LOG_PATH)
|
||||
self.log.info("Found shutdown log at path: %s", self.file_path)
|
||||
with open(self.file_path, "r") as handle:
|
||||
with open(self.file_path, "r", encoding="utf-8") as handle:
|
||||
self.process_shutdownlog(handle.read())
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -34,7 +34,7 @@ class IOSVersionHistory(IOSExtraction):
|
||||
|
||||
def run(self):
|
||||
for found_path in self._get_fs_files_from_patterns(IOS_ANALYTICS_JOURNAL_PATHS):
|
||||
with open(found_path, "r") as analytics_log:
|
||||
with open(found_path, "r", encoding="utf-8") as analytics_log:
|
||||
log_line = json.loads(analytics_log.readline().strip())
|
||||
|
||||
timestamp = datetime.datetime.strptime(log_line["timestamp"],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -18,9 +18,11 @@ class WebkitBase(IOSExtraction):
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for item in self.results:
|
||||
if self.indicators.check_domain(item["url"]):
|
||||
self.detected.append(item)
|
||||
for result in self.results:
|
||||
ioc = self.indicators.check_domain(result["url"])
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
def _process_webkit_folder(self, root_paths):
|
||||
for found_path in self._get_fs_files_from_patterns(root_paths):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -16,13 +16,13 @@ from .net_datausage import Datausage
|
||||
from .osanalytics_addaily import OSAnalyticsADDaily
|
||||
from .safari_browserstate import SafariBrowserState
|
||||
from .safari_history import SafariHistory
|
||||
from .shortcuts import Shortcuts
|
||||
from .sms import SMS
|
||||
from .sms_attachments import SMSAttachments
|
||||
from .tcc import TCC
|
||||
from .webkit_resource_load_statistics import WebkitResourceLoadStatistics
|
||||
from .webkit_session_resource_log import WebkitSessionResourceLog
|
||||
from .whatsapp import Whatsapp
|
||||
from .shortcuts import Shortcuts
|
||||
|
||||
MIXED_MODULES = [Calls, ChromeFavicon, ChromeHistory, Contacts, FirefoxFavicon,
|
||||
FirefoxHistory, IDStatusCache, InteractionC, LocationdClients,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -42,7 +42,12 @@ class ChromeFavicon(IOSExtraction):
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
if self.indicators.check_domain(result["url"]) or self.indicators.check_domain(result["icon_url"]):
|
||||
ioc = self.indicators.check_domain(result["url"])
|
||||
if not ioc:
|
||||
ioc = self.indicators.check_domain(result["icon_url"])
|
||||
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -41,7 +41,9 @@ class ChromeHistory(IOSExtraction):
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
if self.indicators.check_domain(result["url"]):
|
||||
ioc = self.indicators.check_domain(result["url"])
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -40,8 +40,12 @@ class FirefoxFavicon(IOSExtraction):
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
if (self.indicators.check_domain(result.get("url", "")) or
|
||||
self.indicators.check_domain(result.get("history_url", ""))):
|
||||
ioc = self.indicators.check_domain(result.get("url", ""))
|
||||
if not ioc:
|
||||
ioc = self.indicators.check_domain(result.get("history_url", ""))
|
||||
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -44,7 +44,9 @@ class FirefoxHistory(IOSExtraction):
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
if self.indicators.check_domain(result["url"]):
|
||||
ioc = self.indicators.check_domain(result["url"])
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -43,7 +43,9 @@ class IDStatusCache(IOSExtraction):
|
||||
for result in self.results:
|
||||
if result.get("user", "").startswith("mailto:"):
|
||||
email = result["user"][7:].strip("'")
|
||||
if self.indicators.check_email(email):
|
||||
ioc = self.indicators.check_email(email)
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -41,13 +41,13 @@ class LocationdClients(IOSExtraction):
|
||||
|
||||
def serialize(self, record):
|
||||
records = []
|
||||
for ts in self.timestamps:
|
||||
if ts in record.keys():
|
||||
for timestamp in self.timestamps:
|
||||
if timestamp in record.keys():
|
||||
records.append({
|
||||
"timestamp": record[ts],
|
||||
"timestamp": record[timestamp],
|
||||
"module": self.__class__.__name__,
|
||||
"event": ts,
|
||||
"data": f"{ts} from {record['package']}"
|
||||
"event": timestamp,
|
||||
"data": f"{timestamp} from {record['package']}"
|
||||
})
|
||||
|
||||
return records
|
||||
@@ -60,8 +60,40 @@ class LocationdClients(IOSExtraction):
|
||||
parts = result["package"].split("/")
|
||||
proc_name = parts[len(parts)-1]
|
||||
|
||||
if self.indicators.check_process(proc_name):
|
||||
ioc = self.indicators.check_process(proc_name)
|
||||
if ioc:
|
||||
self.log.warning("Found a suspicious process name in LocationD entry %s",
|
||||
result["package"])
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
if "BundlePath" in result:
|
||||
ioc = self.indicators.check_file_path(result["BundlePath"])
|
||||
if ioc:
|
||||
self.log.warning("Found a suspicious file path in Location D: %s",
|
||||
result["BundlePath"])
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
if "Executable" in result:
|
||||
ioc = self.indicators.check_file_path(result["Executable"])
|
||||
if ioc:
|
||||
self.log.warning("Found a suspicious file path in Location D: %s",
|
||||
result["Executable"])
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
if "Registered" in result:
|
||||
ioc = self.indicators.check_file_path(result["Registered"])
|
||||
if ioc:
|
||||
self.log.warning("Found a suspicious file path in Location D: %s",
|
||||
result["Registered"])
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
def _extract_locationd_entries(self, file_path):
|
||||
with open(file_path, "rb") as handle:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -41,7 +41,9 @@ class OSAnalyticsADDaily(IOSExtraction):
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
if self.indicators.check_process(result["package"]):
|
||||
ioc = self.indicators.check_process(result["package"])
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
@@ -44,18 +44,25 @@ class SafariBrowserState(IOSExtraction):
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
if "tab_url" in result and self.indicators.check_domain(result["tab_url"]):
|
||||
self.detected.append(result)
|
||||
continue
|
||||
if "tab_url" in result:
|
||||
ioc = self.indicators.check_domain(result["tab_url"])
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
if "session_data" not in result:
|
||||
continue
|
||||
|
||||
for session_entry in result["session_data"]:
|
||||
if "entry_url" in session_entry and self.indicators.check_domain(session_entry["entry_url"]):
|
||||
self.detected.append(result)
|
||||
if "entry_url" in session_entry:
|
||||
ioc = self.indicators.check_domain(session_entry["entry_url"])
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
def _process_browser_state_db(self, db_path):
|
||||
self._recover_sqlite_db_if_needed(db_path)
|
||||
conn = sqlite3.connect(db_path)
|
||||
|
||||
cur = conn.cursor()
|
||||
@@ -86,8 +93,12 @@ class SafariBrowserState(IOSExtraction):
|
||||
if row[4]:
|
||||
# Skip a 4 byte header before the plist content.
|
||||
session_plist = row[4][4:]
|
||||
session_data = plistlib.load(io.BytesIO(session_plist))
|
||||
session_data = keys_bytes_to_string(session_data)
|
||||
session_data = {}
|
||||
try:
|
||||
session_data = plistlib.load(io.BytesIO(session_plist))
|
||||
session_data = keys_bytes_to_string(session_data)
|
||||
except plistlib.InvalidFileException:
|
||||
pass
|
||||
|
||||
if "SessionHistoryEntries" in session_data.get("SessionHistory", {}):
|
||||
for session_entry in session_data["SessionHistory"].get("SessionHistoryEntries"):
|
||||
@@ -108,7 +119,6 @@ class SafariBrowserState(IOSExtraction):
|
||||
})
|
||||
|
||||
def run(self):
|
||||
|
||||
if self.is_backup:
|
||||
for backup_file in self._get_backup_files_from_manifest(relative_path=SAFARI_BROWSER_STATE_BACKUP_RELPATH):
|
||||
self.file_path = self._get_backup_file_from_id(backup_file["file_id"])
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user