Compare commits

..

189 Commits

Author SHA1 Message Date
Donncha Ó Cearbhaill
3d924e22ec Merge branch 'release/v2.4.0' 2023-07-21 12:17:32 +02:00
Donncha Ó Cearbhaill
ca3c1bade4 Bump version to v2.4.0
Bumping the minor version as we introduce some backwards-incompatible
API changes to module definition in #367.
2023-07-21 12:14:31 +02:00
Donncha Ó Cearbhaill
85877fd3eb Merge pull request #369 from mvt-project/move-indicator-checking
Move detection and alerts from run() to check_indicators()
2023-07-21 12:12:36 +02:00
Donncha Ó Cearbhaill
8015ff78e8 Fix black error 2023-07-21 12:10:45 +02:00
Donncha Ó Cearbhaill
1a07b9a78f Move syntax checking before unit tests 2023-07-21 11:30:59 +02:00
Donncha Ó Cearbhaill
0b88de9867 Move detection and alerts from run() to check_indicators() 2023-07-21 11:29:12 +02:00
Donncha Ó Cearbhaill
76d7534b05 Fix bug recording detections in WebkitResourceLoadStatistics module 2023-07-18 18:02:42 +02:00
Donncha Ó Cearbhaill
ae2ab02347 Merge pull request #367 from mvt-project/refactor-module-options
Add a module_options parameter to pass data from CLI to modules
2023-07-17 19:07:41 +02:00
Donncha Ó Cearbhaill
e2c623c40f Move --fast flag from being a top-level MVT module parameter to an option in a new module_options parameter 2023-07-17 18:52:35 +02:00
Christian Clauss
a6e1a3de12 Add GitHub Annotions to ruff output (#364)
* Add GitHub Annotions to ruff output
* Upgrade GitHub Actions
* No Py3.11
2023-07-15 14:42:13 +02:00
tek
e7270d6a07 Fixes import and adds test for PR 361 2023-07-10 22:55:22 +02:00
Niclas Schwarzlose
1968a0fca2 Improve appops parsing in dumpsys (#361)
Without this change the package doesn't get properly reset when a new
user starts.

See for example in this excerpt:

```
 1 |    Package com.android.bluetooth:
 2 |      READ_CONTACTS (allow):
 3 |        null=[
 4 |          Access: [pers-s] 2022-04-22 13:24:17.577 (-277d5h22m53s447ms)
 5 |        ]
 6 |      WAKE_LOCK (allow):
 7 |        null=[
 8 |          Access: [pers-s] 2023-01-24 17:45:49.712 (-1m21s312ms) duration=+3ms
 9 |        ]
10 |      GET_USAGE_STATS (default):
11 |        null=[
12 |          Reject: [pers-s]2022-04-22 13:23:53.964 (-277d5h23m17s60ms)
13 |        ]
14 |      BLUETOOTH_CONNECT (allow):
15 |        null=[
16 |          Access: [pers-s] 2022-04-22 13:23:53.988 (-277d5h23m17s36ms)
17 |        ]
18 |  Uid 1027:
19 |    state=pers
20 |    capability=LCMN
21 |    appWidgetVisible=false
22 |      LEGACY_STORAGE: mode=ignore
23 |    Package com.android.nfc:
24 |      WAKE_LOCK (allow):
25 |        null=[
26 |          Access: [pers-s] 2022-04-22 13:23:54.633 (-277d5h23m16s391ms) duration=+1s73ms
27 |        ]
```

Here the package "com.android.bluetooth" is not reset when in line 18,
so when "LEGACY_STORAGE:" in line 22 is encountered, it's added as
another permission to "com.android.bluetooth" with "access" set to
"ode=igno".

This PR fixes that by resetting the package whenever a new Uid is
encountered.

Co-authored-by: Niclas Schwarzlose <niclas.schwarzlose@reporter-ohne-grenzen.de>
2023-07-10 22:53:58 +02:00
Donncha Ó Cearbhaill
46cc54df74 Add information about public indicators and support avenues to documentation 2023-06-30 19:43:30 +02:00
Donncha Ó Cearbhaill
7046ff80d1 Add SMS read time in the MVT logs 2023-06-30 19:30:50 +02:00
Donncha Ó Cearbhaill
e2516f284b Bump version number 2023-06-29 17:03:26 +02:00
Donncha Ó Cearbhaill
17963f83d6 Fix URL to indicator repo in docs 2023-06-29 16:49:20 +02:00
Donncha Ó Cearbhaill
4f0c9c6077 Update README with information on indicators of compromise and path ways for forensic support 2023-06-29 16:48:56 +02:00
Donncha Ó Cearbhaill
27bd5f03a8 Merge pull request #359 from mvt-project/optimise-domain-checking
Optimise domain checking performance
2023-06-29 14:56:50 +02:00
Donncha Ó Cearbhaill
3babbadc1d Add docs for the profiling feature 2023-06-29 14:55:09 +02:00
Donncha Ó Cearbhaill
41db117168 Improve performance when checking URLs and domains
Some MVT modules such as the WhatsApp module can be very slow as it was taking a naive approach to look for IOCs. The code was checking URLs (potentially more than 100k) against
1000's of IOC domains resulting in a quadratic run-time with hundreds of millions of comparisons as the number of IOCs increases.

This commit add an Aho-Corasick library which allows the efficient search in a string (the URL in this case) for all matches in set of keys (the IOCs). This data structure is perfect for this use case.

A quick measurement shows a 80% performance improvement for a WhatsApp database with 100k entries. The slow path is now the time spent fetching and expanding short URLs found in the database. This
can also be sped up significantly by fetching each URL asynchronously. This would require reworking modules to split the URL expansion from the IOC check so I will implement in a separate PR.
2023-06-29 14:14:44 +02:00
Donncha Ó Cearbhaill
2b01ed7179 Add optional profiling for MVT modules 2023-06-29 13:31:13 +02:00
Donncha Ó Cearbhaill
78d493b17e Merge pull request #356 from mvt-project/auto/add-new-ios-releases
[auto] Update iOS releases and versions
2023-06-22 11:06:45 +02:00
DonnchaC
473c80009b Add new iOS versions and build numbers 2023-06-22 00:17:52 +00:00
tek
a1481683e3 Adapts linter workflow to black 2023-06-14 01:05:14 +02:00
Nex
bdd36a9179 Merge pull request #349 from mvt-project/code-cleanup
Linted code using isort + autoflake + black
2023-06-08 21:12:34 +02:00
Nex
e1677639c4 Linted code using isort + autoflake + black, fixed wrong use of Optional[bool] 2023-06-01 23:40:26 +02:00
tek
c2d740ed36 Handle better some empty database issues in iOS backups 2023-05-25 00:24:34 +02:00
tek
d0e24c6369 Fixes a bug in the applications module 2023-05-24 12:04:03 +02:00
tek
a1994079b1 Sort imports 2023-05-24 12:03:49 +02:00
Donncha Ó Cearbhaill
289b7efdeb Add missing iOS build numbers 2023-05-21 17:11:07 +01:00
Donncha Ó Cearbhaill
166a63e14c Merge pull request #347 from mvt-project/auto/add-new-ios-releases
[auto] Update iOS releases and versions
2023-05-21 17:54:25 +02:00
DonnchaC
1b933fdb12 Add new iOS versions and build numbers 2023-05-21 15:53:45 +00:00
Donncha Ó Cearbhaill
0c0ff7012b Set branch number for auto-generated pull request 2023-05-21 16:52:47 +01:00
Donncha Ó Cearbhaill
f9b0d07a81 Don't include information beta's in the version JSON 2023-05-21 16:49:14 +01:00
Donncha Ó Cearbhaill
d14bcdd05f Update title used in auto PR for new iOS versions 2023-05-21 16:47:56 +01:00
Donncha Ó Cearbhaill
e026bb0a76 Fix path to script in workflow 2023-05-21 16:44:17 +01:00
Donncha Ó Cearbhaill
253b4f031a Allow workflow to be triggered manually 2023-05-21 16:42:54 +01:00
Donncha Ó Cearbhaill
ec14297643 Merge pull request #345 from mvt-project/feature/auto-update-version-info
Add workflow to auto-update iOS builds and version numbers
2023-05-21 17:38:46 +02:00
Donncha Ó Cearbhaill
3142d86edd Fix path to include version JSON files in built package 2023-05-21 16:37:36 +01:00
Donncha Ó Cearbhaill
c18998d771 Add version 16.5 to resolve merge conflict from main 2023-05-21 16:26:12 +01:00
Donncha Ó Cearbhaill
22fd794fb8 Fix python style and setup.cfg syntax 2023-05-21 16:15:49 +01:00
Donncha Ó Cearbhaill
27c5c76dc2 Add script and worker to auto-update build and version info 2023-05-21 16:09:50 +01:00
Donncha Ó Cearbhaill
fafbac3545 Fix sorting of version numbers 2023-05-20 21:49:27 +01:00
Donncha Ó Cearbhaill
bbfaadd297 Load iOS device and build information from a JSON file. 2023-05-20 21:24:14 +01:00
tek
85abed55b6 Merge branch 'main' of github.com:mvt-project/mvt 2023-05-20 00:14:01 +02:00
tek
2fbd7607ef Adds latest iOS version 2023-05-20 00:11:16 +02:00
Donncha Ó Cearbhaill
3787dc48cd Fix bug where getprop sections where missing due to non-standard section header 2023-05-18 11:28:10 +02:00
tek
f814244ff8 Fixes bug in bugreport getprop module 2023-05-06 11:20:10 -04:00
tek
11730f164f Fixes an issue in androidqf SMS module 2023-05-06 11:04:42 -04:00
Sebastian Pederiva
912fb060cb Fix error when creating report: csv.Error (#341) 2023-05-02 17:09:16 +02:00
tek
a9edf4a9fe Bumps version 2023-04-25 12:20:45 +02:00
tek
ea7b9066ba Improves iOS app detection 2023-04-25 11:21:55 +02:00
tek
fd81e3aa13 Adds verbose mode 2023-04-25 11:13:46 +02:00
tek
15477cc187 Bumps version 2023-04-13 17:59:05 +02:00
tek
551b95b38b Improves documentation 2023-04-13 16:11:55 +02:00
tek
d767abb912 Fixes a bug in the calendar plugin 2023-04-13 13:21:33 +02:00
tek
8a507b0a0b Fixes a bug in WhatsApp iOS module 2023-04-13 09:26:52 +02:00
tek
63b95ee6a5 Bumps version 2023-04-12 12:52:57 +02:00
Tek
c8ae495971 Extract all messages from SMS and WhatsApp (#337) 2023-04-12 12:39:25 +02:00
tek
33d092692e Adds calendar iOS plugin 2023-04-12 10:21:17 +02:00
tek
b1e5dc715f Adds latest iOS version 2023-04-07 22:22:44 +02:00
tek
1dc1ee2238 Improves Indicator object 2023-04-07 15:07:45 +02:00
tek
a2cbaacfce Fixes hashing issue 2023-04-07 14:51:54 +02:00
tek
801fe367ac Improves WebkitResourceLoadStatistics module 2023-04-07 14:43:20 +02:00
tek
0d653be4dd Adds Applications iOS module 2023-04-07 14:10:24 +02:00
tek
179b6976fa Improves interactionc module 2023-04-07 12:25:30 +02:00
tek
577fcf752d Fixes issues in analytics module 2023-04-07 12:25:17 +02:00
tek
2942209f62 Improves module handling 2023-04-07 12:25:01 +02:00
tek
06bf7b9cb1 Bumps version 2023-03-29 14:44:59 +02:00
tek
b5d7e528de Adds indicators for android properties 2023-03-29 12:57:41 +02:00
tek
70c6f0c153 Adds latest iOS version 2023-03-27 23:10:25 +02:00
tek
49491800fb Improves typing 2023-03-24 19:02:02 +01:00
tek
1ad176788b Updates install instructions from sources 2023-03-24 15:11:21 +01:00
Donncha Ó Cearbhaill
11d58022cf Change checksum log message to info instead of warning 2023-03-03 21:21:32 +00:00
tek
cc205bfab0 Adds missing iOS versions 2023-03-02 15:47:37 -05:00
tek
671cd07200 Fixes a bug with YAML parsing of github workflow 2023-03-01 17:34:35 -05:00
tek
7581f81464 removes duplicated flake8 workflow 2023-03-01 16:50:33 -05:00
tek
4ed8ff51ff Improves code PEP8 compliance and adds ruff check 2023-03-01 16:43:08 -05:00
tek
fc4e2a9029 Improves logcat logging in mvt-android check-adb 2023-03-01 16:34:28 -05:00
tek
383d9b16de Bumps version 2023-02-21 15:34:48 -05:00
tek
55f6a4ae54 Fixes mypy typing issues 2023-02-21 15:18:36 -05:00
tek
89c6a35c26 Update documentation on making backups with Finder 2023-02-21 14:31:44 -05:00
Huy Ta
25614922d7 Added Documentation for Creating Encrypted iPhone Backup with Finder on macOS (#332)
* Added Documentation for Encrypted Iphone Backup with Finder on MacOS

* Added details on where to check the backups after completion and added screenshots for the process

* Added location of backups
2023-02-21 20:22:45 +01:00
Tek
7d79844749 Improves generation of hashes (#327)
* Improves generation of hashes

* Adds generation of reference info.json hash
2023-02-21 20:16:32 +01:00
tek
83447411ff Adds additional iOS versions 2023-02-14 16:05:11 -05:00
tek
ce177978cd Sort imports 2023-02-14 11:51:55 -05:00
tek
95842ac449 Fixes #329 outdated iOS version error 2023-02-14 11:51:38 -05:00
tek
8ce6b31299 Adding latest iOS version 16.3.1 2023-02-13 19:21:41 -05:00
tek
704ea39569 Removes empty lines to be PEP8 compliant 2023-02-08 20:20:13 +01:00
tek
81ed0b0c19 Update copyright information 2023-02-08 20:18:16 +01:00
tek
318c908dd8 Fixes bug in adb File module. Fixes #268 2023-02-08 20:03:45 +01:00
tek
a5cf5271fa Allows -h argument for --help 2023-02-08 19:09:47 +01:00
tek
716909b528 Temporary fix of setuptools issue 2023-01-24 16:42:50 +01:00
William Budington
cbd9158daf Fixes bug where su binary is present but privilege is not granted to com.android.shell (#326) 2023-01-24 16:22:52 +01:00
tek
013e3421c8 Adding iOS 16.3 2023-01-24 16:22:02 +01:00
tek
1042354be5 Adds serializing to iOS module webkit_resource_load_statistics 2023-01-13 12:58:26 +01:00
tek
96bc02d344 Adds new iOS versions 2022-12-14 17:18:42 +01:00
tek
d05e6fac00 Attempt to fix #268 bug in android files module 2022-12-08 12:04:15 +01:00
tek
200e26d906 Fixes a bug in shortcut parsing #296 2022-12-08 11:57:08 +01:00
tek
27fbdd2fd4 Merge branch 'main' of github.com:mvt-project/mvt 2022-12-08 11:12:43 +01:00
tek
4bbaa20e22 Adds iOS 16.1.2 build number 2022-12-08 11:05:26 +01:00
Nex
99e14ad8b0 Bumped version 2022-11-13 01:11:52 +01:00
tek
deaa68a2e0 Adds iOS 16.1.1 in iOS versions 2022-11-11 12:11:46 +01:00
tek
07f819bf5f Adds new iPhone hardware 2022-11-02 10:41:33 +01:00
tek
51fdfce7f4 Adds iOS 16.1 to iOS versions 2022-10-31 11:17:25 +01:00
Nex
41e05a107e Merge branch 'main' of github.com:mvt-project/mvt 2022-10-15 11:26:55 +02:00
Nex
e559fb223b Upgraded dependencies 2022-10-15 11:26:40 +02:00
Nex
b69bb92f3d Merge pull request #279 from Niek/main
Dockerfile improvements, support arm64 builds
2022-10-15 11:14:40 +02:00
Nex
42e8e41b7d Merge branch 'besendorf-patch-1' 2022-10-15 11:11:57 +02:00
Nex
00b7314395 Added quotes 2022-10-15 11:11:47 +02:00
Nex
39a8bf236d Merge branch 'patch-1' of github.com:besendorf/mvt into besendorf-patch-1 2022-10-15 11:11:29 +02:00
tek
d268b17284 Adds missing module in androidqf module list 2022-10-14 15:01:08 +02:00
tek
66c015bc23 Improves check-androidqf tests 2022-10-11 13:07:24 +02:00
tek
ba0106c476 Adds SMS androidqf module and improves tests 2022-10-11 12:41:42 +02:00
tek
41826d7951 Fixes PEP8 syntax issue 2022-10-05 15:30:39 +02:00
Nex
4e0a393a02 Bumped version 2022-10-01 12:40:04 +02:00
Tek
c3dc4174fc Adds detection for disabled security packages in Android (#306)
* Adds detection for disabled security packages in Android

* Update detection of disabled security packages
2022-09-26 12:17:09 +02:00
tek
e1d1b6c5de Fixes a minor issue in the iOS manifest module 2022-09-26 12:07:52 +02:00
tek
d0a893841b Adds new iOS versions 2022-09-12 23:49:33 +02:00
Nex
d4e99661c7 Merge pull request #300 from andefined/fix-idstatuscache-error
Fixed missing root_paths check for ios/idstatuscache module
2022-09-07 09:29:09 +02:00
Nex
6a00d3a14d Closing handle to ZipFile 2022-09-05 12:21:11 +02:00
Nex
a863209abb Added check-androidqf command 2022-09-05 12:12:36 +02:00
Nex
4c7db02da4 Bumped version 2022-09-01 09:42:03 +02:00
Nex
92dfefbdeb Added some support for patterns in backups' relative paths 2022-08-31 19:34:59 +02:00
Nex
8988adcf77 Warnings should be reserved for detections ideally 2022-08-25 17:22:24 +02:00
andefined
91667b0ded Fixed missing root_paths check for ios/idstatuscache module 2022-08-24 18:54:45 +03:00
tek
2365175dbd Adds check of process name in paths in indicators 2022-08-23 13:18:42 +02:00
Nex
528d43b914 Merge branch 'main' of github.com:mvt-project/mvt 2022-08-22 21:13:22 +02:00
Nex
f952ba5119 Removed comment with odd char 2022-08-22 21:12:59 +02:00
besendorf
d61b2751f1 Add pip command for update
Adds the pip comman for updating mvt. I think this would be helpfull for novice users as it already has been asked here: https://github.com/mvt-project/mvt/discussions/261
Also I sometimes forget the command too ;)
2022-08-22 12:20:56 +02:00
Nex
b4ed2c6ed4 Added commented backup ID 2022-08-22 10:40:36 +02:00
Nex
3eed1d6edf Sorted imports 2022-08-22 10:30:58 +02:00
Nex
83ef545cd1 Merge pull request #298 from jons44/patch-1
Fixed idevicebackup2 syntax
2022-08-20 16:29:57 +02:00
jons44
5d4fbec62b Fixed idevicebackup2 syntax 2022-08-19 19:34:12 +02:00
Nex
fa7d6166f4 Removed legacy print 2022-08-19 15:19:46 +02:00
Nex
429b223555 Bumped version 2022-08-18 18:31:32 +02:00
tek
e4b9a9652a Adds ios 15.6.1 version 2022-08-18 14:42:26 +02:00
Nex
134581c000 Merge pull request #297 from mvt-project/feature/dumpsys-packages-parsing
Improves Android dumpsys package parsing
2022-08-18 13:58:59 +02:00
tek
5356a399c9 Moves dumpsys parsing to android parsers and use the same parser for adb and bugreport modules 2022-08-17 18:24:51 +02:00
Nex
e0f563596d Setting a default value for list of ioc files in case none was specified 2022-08-17 15:58:53 +02:00
Nex
ea5de0203a Changed default for Optional[str] 2022-08-17 15:52:17 +02:00
Nex
ace965ee8a Changed default value for optional lists to None 2022-08-17 15:37:12 +02:00
Nex
ad8f455209 Sorted imports 2022-08-17 11:34:58 +02:00
tek
ae67b41374 Merge branch 'main' of github.com:mvt-project/mvt 2022-08-16 18:57:37 +02:00
tek
5fe88098b9 Improves dumpsys battery history parsing 2022-08-16 18:57:18 +02:00
Nex
d578c240f9 Added additional missing space in inline comment 2022-08-16 18:26:34 +02:00
Nex
427a29c2b6 Pylint notes to ignore some lines too long 2022-08-16 16:09:59 +02:00
Nex
5e6f6faa9c Sorted imports 2022-08-16 16:02:32 +02:00
Nex
74a3ecaa4e Linted code 2022-08-16 16:02:17 +02:00
Nex
f536af1124 Not using bare except and removed unused var 2022-08-16 15:55:29 +02:00
Nex
631354c131 Properly checking any potential domains in Manifest.db records (fixes: #293) 2022-08-16 15:40:28 +02:00
Nex
7ad7782b51 Merge branch 'main' of github.com:mvt-project/mvt 2022-08-16 13:40:14 +02:00
Nex
f04f91e1e3 Improved type hints and code style enforcement 2022-08-16 13:39:55 +02:00
Nex
6936908f86 Bumped version 2022-08-15 10:27:36 +02:00
Nex
f3e5763c6a Added SECURITY.md 2022-08-14 19:28:30 +02:00
Nex
f438f7b1fb Fixing unix epoch timestamps conversion to float 2022-08-13 23:37:35 +02:00
Nex
66a157868f Ensuring all adb connect/disconnect are happening in modules only 2022-08-13 23:12:43 +02:00
Nex
a966b694ea More line length enforcement 2022-08-13 18:27:54 +02:00
Nex
c9dd3af278 More line length enforcing 2022-08-13 18:24:11 +02:00
Nex
82a60ee07c Enforcing line length 2022-08-13 17:52:56 +02:00
Nex
8bc5113bd2 Enforcing line length 2022-08-13 17:51:06 +02:00
Nex
00d82f7f00 Enforcing line lenght 2022-08-13 17:50:00 +02:00
Nex
2781f33fb5 Added more date conversion wrappers 2022-08-13 14:04:10 +02:00
Nex
271fe5fbee Continuing enforcement of line length and simplifying date conversions 2022-08-13 02:14:24 +02:00
Nex
0f503f72b5 Starting to enforce line lengths on mvt-ios 2022-08-12 19:38:57 +02:00
Nex
424b86a261 Fixed typos 2022-08-12 19:25:56 +02:00
Nex
1fe595f4cc Added CONTRIBUTING.md file 2022-08-12 19:25:11 +02:00
Nex
b8c59f1183 Removed public_indicators.json legacy file 2022-08-12 19:15:17 +02:00
Nex
a935347aed Trying to enforce line lengths at 80/100 2022-08-12 19:14:05 +02:00
Nex
661d0a8669 Using Union type hints in order to support older versions of Python 2022-08-12 16:29:43 +02:00
Nex
63ff5fd334 Started linting the code 2022-08-12 16:20:16 +02:00
Nex
146b9245ab Sorted imports 2022-08-11 16:57:08 +02:00
Nex
99d33922be Conformed ways modules logger is initialized 2022-08-11 16:42:04 +02:00
Nex
c42634af3f Fixed logging in accessibility module 2022-08-11 14:50:25 +02:00
Nex
6cb59cc3ab Trying to tidy up ConfigurationProfiles module 2022-08-10 16:44:43 +02:00
Nex
e0481686b7 Fixed test file 2022-08-08 16:47:01 +02:00
Nex
804ade3a40 Conformed browerstate plugin to others with similar structure 2022-08-08 16:44:54 +02:00
tek
c5ccaef0c4 Fixes a bug in Safari Browser State module 2022-08-08 11:20:05 +02:00
Nex
c4416d406a Avoiding duplicate entries for stix2 files with multiple malware definitions 2022-08-06 14:49:05 +02:00
Nex
6b8a23ae10 Added an attribute list to keep track of executed modules 2022-08-05 13:52:51 +02:00
tek
872d5d766e Adds product name in iOS backup info module 2022-08-03 16:34:39 +02:00
Nex
f5abd0719c Bumped version 2022-08-02 18:26:29 +02:00
Nex
6462ffc15d Added iOS 15.6 2022-08-02 18:26:23 +02:00
Nex
6333cafd38 Bumped version 2022-07-25 17:43:37 +02:00
Nex
03c59811a3 Ordered imports 2022-07-25 17:43:27 +02:00
Nex
cfd3b5bbcb Merge branch 'main' of github.com:mvt-project/mvt 2022-07-25 17:43:08 +02:00
Nex
97ab67240f Creating MVT data folder when missing 2022-07-25 17:42:51 +02:00
Nex
7fc664185c Flake8 fixes 2022-07-20 15:49:51 +02:00
Niek van der Maas
067402831a Dockerfile improvements, support arm64 builds 2022-06-02 09:22:07 +02:00
202 changed files with 9493 additions and 3166 deletions

11
.github/workflows/black.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
name: Black
on: [push]
jobs:
black:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: psf/black@stable
with:
options: "--check"

View File

@@ -1,26 +0,0 @@
name: Flake8
on:
push:
paths:
- '*.py'
jobs:
flake8_py3:
runs-on: ubuntu-latest
steps:
- name: Setup Python
uses: actions/setup-python@v1
with:
python-version: 3.9
architecture: x64
- name: Checkout
uses: actions/checkout@master
- name: Install flake8
run: pip install flake8
- name: Run flake8
uses: suo/flake8-github-action@releases/v1
with:
checkName: 'flake8_py3' # NOTE: this needs to be the same as the job name
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -16,17 +16,17 @@ jobs:
strategy:
fail-fast: false
matrix:
# python-version: [3.7, 3.8, 3.9]
python-version: [3.8, 3.9]
python-version: ['3.8', '3.9', '3.10'] # , '3.11']
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade setuptools
python -m pip install --upgrade pip
python -m pip install flake8 pytest safety stix2 pytest-mock
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi

19
.github/workflows/ruff.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: Ruff
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
ruff_py3:
name: Ruff syntax check
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@master
- name: Install Dependencies
run: |
pip install --user ruff
- name: ruff
run: |
ruff --format=github .

View File

@@ -0,0 +1,89 @@
"""
Python script to download the Apple RSS feed and parse it.
"""
import json
import os
import urllib.request
from xml.dom.minidom import parseString
from packaging import version
def download_apple_rss(feed_url):
with urllib.request.urlopen(feed_url) as f:
rss_feed = f.read().decode("utf-8")
print("Downloaded RSS feed from Apple.")
return rss_feed
def parse_latest_ios_versions(rss_feed_text):
latest_ios_versions = []
parsed_feed = parseString(rss_feed_text)
for item in parsed_feed.getElementsByTagName("item"):
title = item.getElementsByTagName("title")[0].firstChild.data
if not title.startswith("iOS"):
continue
import re
build_match = re.match(
r"iOS (?P<version>[\d\.]+) (?P<beta>beta )?(\S*)?\((?P<build>.*)\)", title
)
if not build_match:
print("Could not parse iOS build:", title)
continue
release_info = build_match.groupdict()
if release_info["beta"]:
print("Skipping beta release:", title)
continue
release_info.pop("beta")
latest_ios_versions.append(release_info)
return latest_ios_versions
def update_mvt(mvt_checkout_path, latest_ios_versions):
version_path = os.path.join(mvt_checkout_path, "mvt/ios/data/ios_versions.json")
with open(version_path, "r") as version_file:
current_versions = json.load(version_file)
new_entry_count = 0
for new_version in latest_ios_versions:
for current_version in current_versions:
if new_version["build"] == current_version["build"]:
break
else:
# New version that does not exist in current data
current_versions.append(new_version)
new_entry_count += 1
if not new_entry_count:
print("No new iOS versions found.")
else:
print("Found {} new iOS versions.".format(new_entry_count))
new_version_list = sorted(
current_versions, key=lambda x: version.Version(x["version"])
)
with open(version_path, "w") as version_file:
json.dump(new_version_list, version_file, indent=4)
def main():
print("Downloading RSS feed...")
mvt_checkout_path = os.path.abspath(
os.path.join(os.path.dirname(__file__), "../../../")
)
rss_feed = download_apple_rss(
"https://developer.apple.com/news/releases/rss/releases.rss"
)
latest_ios_version = parse_latest_ios_versions(rss_feed)
update_mvt(mvt_checkout_path, latest_ios_version)
if __name__ == "__main__":
main()

29
.github/workflows/update-ios-data.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: Update iOS releases and version numbers
run-name: ${{ github.actor }} is finding the latest iOS release version and build numbers
on:
workflow_dispatch:
schedule:
# * is a special character in YAML so you have to quote this string
- cron: '0 */6 * * *'
jobs:
update-ios-version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
- name: Run script to fetch latest iOS releases from Apple RSS feed.
run: python3 .github/workflows/scripts/update-ios-releases.py
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
with:
title: '[auto] Update iOS releases and versions'
commit-message: Add new iOS versions and build numbers
branch: auto/add-new-ios-releases
body: |
This is an automated pull request to update the iOS releases and version numbers.
add-paths: |
*.json
labels: |
automated pr

19
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,19 @@
# Contributing
Thank you for your interest in contributing to Mobile Verification Toolkit (MVT)! Your help is very much appreciated.
## Where to start
Starting to contribute to a somewhat complex project like MVT might seem intimidating. Unless you have specific ideas of new functionality you would like to submit, some good starting points are searching for `TODO:` and `FIXME:` comments throughout the code. Alternatively you can check if any GitHub issues existed marked with the ["help wanted"](https://github.com/mvt-project/mvt/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) tag.
## Code style
When contributing code to
- **Indentation**: we use 4-spaces tabs.
- **Quotes**: we use double quotes (`"`) as a default. Single quotes (`'`) can be favored with nested strings instead of escaping (`\"`), or when using f-formatting.
- **Maximum line length**: we strongly encourage to respect a 80 characters long lines and to follow [PEP8 indentation guidelines](https://peps.python.org/pep-0008/#indentation) when having to wrap. However, if breaking at 80 is not possible or is detrimental to the readability of the code, exceptions are tolerated. For example, long log lines, or long strings can be extended to 100 characters long. Please hard wrap anything beyond 100 characters.

View File

@@ -1,4 +1,4 @@
FROM ubuntu:20.04
FROM ubuntu:22.04
# Ref. https://github.com/mvt-project/mvt
@@ -7,13 +7,12 @@ LABEL vcs-url="https://github.com/mvt-project/mvt"
LABEL description="MVT is a forensic tool to look for signs of infection in smartphone devices."
ENV PIP_NO_CACHE_DIR=1
ENV DEBIAN_FRONTEND=noninteractive
# Fixing major OS dependencies
# ----------------------------
RUN apt update \
&& apt install -y python3 python3-pip libusb-1.0-0-dev \
&& apt install -y wget unzip\
&& DEBIAN_FRONTEND=noninteractive apt-get -y install default-jre-headless \
&& apt install -y python3 python3-pip libusb-1.0-0-dev wget unzip default-jre-headless adb \
# Install build tools for libimobiledevice
# ----------------------------------------
@@ -67,18 +66,9 @@ RUN mkdir /opt/abe \
# Create alias for abe
&& echo 'alias abe="java -jar /opt/abe/abe.jar"' >> ~/.bashrc
# Install Android Platform Tools
# ------------------------------
RUN mkdir /opt/android \
&& wget -q https://dl.google.com/android/repository/platform-tools-latest-linux.zip \
&& unzip platform-tools-latest-linux.zip -d /opt/android \
# Create alias for adb
&& echo 'alias adb="/opt/android/platform-tools/adb"' >> ~/.bashrc
# Generate adb key folder
# ------------------------------
RUN mkdir /root/.android && /opt/android/platform-tools/adb keygen /root/.android/adbkey
RUN mkdir /root/.android && adb keygen /root/.android/adbkey
# Setup investigations environment
# --------------------------------

View File

@@ -1,5 +1,12 @@
PWD = $(shell pwd)
check:
flake8
ruff check -q .
black --check .
pytest -q
clean:
rm -rf $(PWD)/build $(PWD)/dist $(PWD)/mvt.egg-info
@@ -11,3 +18,6 @@ upload:
test-upload:
python3 -m twine upload --repository testpypi dist/*
pylint:
pylint --rcfile=setup.cfg mvt

View File

@@ -11,10 +11,24 @@
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.
It has been developed and released by the [Amnesty International Security Lab](https://www.amnesty.org/en/tech/) in July 2021 in the context of the [Pegasus project](https://forbiddenstories.org/about-the-pegasus-project/) along with [a technical forensic methodology and forensic evidence](https://www.amnesty.org/en/latest/research/2021/07/forensic-methodology-report-how-to-catch-nso-groups-pegasus/).
It has been developed and released by the [Amnesty International Security Lab](https://www.amnesty.org/en/tech/) in July 2021 in the context of the [Pegasus Project](https://forbiddenstories.org/about-the-pegasus-project/) along with [a technical forensic methodology](https://www.amnesty.org/en/latest/research/2021/07/forensic-methodology-report-how-to-catch-nso-groups-pegasus/). It continues to be maintained by Amnesty International and other contributors.
*Warning*: MVT is a forensic research tool intended for technologists and investigators. Using it requires understanding the basics of forensic analysis and using command-line tools. This is not intended for end-user self-assessment. If you are concerned with the security of your device please seek expert assistance.
> **Note**
> MVT is a forensic research tool intended for technologists and investigators. It requires understanding digital forensics and using command-line tools. This is not intended for end-user self-assessment. If you are concerned with the security of your device please seek reputable expert assistance.
>
### Indicators of Compromise
MVT supports using public [indicators of compromise (IOCs)](https://github.com/mvt-project/mvt-indicators) to scan mobile devices for potential traces of targeting or infection by known spyware campaigns. This includes IOCs published by [Amnesty International](https://github.com/AmnestyTech/investigations/) and other research groups.
> **Warning**
> Public indicators of compromise are insufficient to determine that a device is "clean", and not targeted with a particular spyware tool. Reliance on public indicators alone can miss recent forensic traces and give a false sense of security.
>
> Reliable and comprehensive digital forensic support and triage requires access to non-public indicators, research and threat intelligence.
>
>Such support is available to civil society through [Amnesty International's Security Lab](https://www.amnesty.org/en/tech/) or through our forensic partnership with [Access Nows Digital Security Helpline](https://www.accessnow.org/help/).
More information about using indicators of compromise with MVT is available in the [documentation](https://docs.mvt.re/en/latest/iocs/).
## Installation

5
SECURITY.md Normal file
View File

@@ -0,0 +1,5 @@
# Reporting security issues
Thank you for your interest in reporting security issues and vulnerabilities! Security research is of utmost importance and we take all reports seriously. If you discover an issue please report it to us right away!
Please DO NOT file a public issue, instead send your report privately to *nex [at] nex [dot] sx*. You can also write PGP-encrypted emails to [this key](https://keybase.io/nex/pgp_keys.asc?fingerprint=05216f3b86848a303c2fe37dd166f1667359d880).

View File

@@ -24,7 +24,7 @@ Some recent phones will enforce the utilisation of a password to encrypt the bac
## 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:
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 from the backup directly with MVT:
```bash
$ mvt-android check-backup --output /path/to/results/ /path/to/backup.ab
@@ -32,7 +32,7 @@ $ mvt-android check-backup --output /path/to/results/ /path/to/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
INFO [mvt.android.modules.backup.sms] Extracted a total of 64 SMS messages
```
If the backup is encrypted, MVT will prompt you to enter the password.
@@ -52,4 +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.
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/` (the path to backup given should be the folder containing the `apps` folder).
You can then extract SMSs 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/` (the path to backup given should be the folder containing the `apps` folder).

27
docs/development.md Normal file
View File

@@ -0,0 +1,27 @@
# Development
The Mobile Verification Toolkit team welcomes contributions of new forensic modules or other contributions which help improve the software.
## Testing
MVT uses `pytest` for unit and integration tests. Code style consistency is maintained with `flake8`, `ruff` and `black`. All can
be run automatically with:
```bash
make check
```
Run these tests before making new commits or opening pull requests.
## Profiling
Some MVT modules extract and process significant amounts of data during the analysis process or while checking results against known indicators. Care must be
take to avoid inefficient code paths as we add new modules.
MVT modules can be profiled with Python built-in `cProfile` by setting the `MVT_PROFILE` environment variable.
```bash
MVT_PROFILE=1 dev/mvt-ios check-backup test_backup
```
Open an issue or PR if you are encountering significant performance issues when analyzing a device with MVT.

View File

@@ -1,4 +1,4 @@
Using Docker simplifies having all the required dependencies and tools (including most recent versions of [libimobiledevice](https://libimobiledevice.org)) readily installed.
Using Docker simplifies having all the required dependencies and tools (including most recent versions of [libimobiledevice](https://libimobiledevice.org)) readily installed. Note that this requires a Linux host, as Docker for Windows and Mac [doesn't support passing through USB devices](https://docs.docker.com/desktop/faqs/#can-i-pass-through-a-usb-device-to-a-container).
Install Docker following the [official documentation](https://docs.docker.com/get-docker/).
@@ -10,11 +10,6 @@ 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

BIN
docs/img/macos-backup2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

BIN
docs/img/macos-backups.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

View File

@@ -6,6 +6,9 @@
Mobile Verification Toolkit (MVT) is a tool to facilitate the [consensual forensic analysis](introduction.md#consensual-forensics) of Android and iOS devices, for the purpose of identifying traces of compromise.
It has been developed and released by the [Amnesty International Security Lab](https://www.amnesty.org/en/tech/) in July 2021 in the context of the [Pegasus Project](https://forbiddenstories.org/about-the-pegasus-project/) along with [a technical forensic methodology](https://www.amnesty.org/en/latest/research/2021/07/forensic-methodology-report-how-to-catch-nso-groups-pegasus/). It continues to be maintained by Amnesty International and other contributors.
In this documentation you will find instructions on how to install and run the `mvt-ios` and `mvt-android` commands, and guidance on how to interpret the extracted results.
## Resources

View File

@@ -54,7 +54,7 @@ Then you can install MVT directly from [pypi](https://pypi.org/project/mvt/)
pip3 install mvt
```
Or from the source code:
If you want to have the latest features in development, you can install MVT directly from the source code. If you installed MVT previously from pypi, you should first uninstall it using `pip3 uninstall mvt` and then install from the source code:
```bash
git clone https://github.com/mvt-project/mvt.git

View File

@@ -12,6 +12,20 @@ Mobile Verification Toolkit (MVT) is a collection of utilities designed to facil
MVT is a forensic research tool intended for technologists and investigators. Using it requires understanding the basics of forensic analysis and using command-line tools. MVT is not intended for end-user self-assessment. If you are concerned with the security of your device please seek expert assistance.
## Indicators of Compromise
MVT supports using [indicators of compromise (IOCs)](https://github.com/mvt-project/mvt-indicators) to scan mobile devices for potential traces of targeting or infection by known spyware campaigns. This includes IOCs published by [Amnesty International](https://github.com/AmnestyTech/investigations/) and other research groups.
!!! warning
Public indicators of compromise are insufficient to determine that a device is "clean", and not targeted with a particular spyware tool. Reliance on public indicators alone can miss recent forensic traces and give a false sense of security.
Reliable and comprehensive digital forensic support and triage requires access to non-public indicators, research and threat intelligence.
Such support is available to civil society through [Amnesty International's Security Lab](https://www.amnesty.org/en/tech/) or [Access Nows Digital Security Helpline](https://www.accessnow.org/help/).
More information about using indicators of compromise with MVT is available in the [documentation](iocs.md).
## Consensual Forensics
While MVT is capable of extracting and processing various types of very personal records typically found on a mobile phone (such as calls history, SMS and WhatsApp messages, etc.), this is intended to help identify potential attack vectors such as malicious SMS messages leading to exploitation.

View File

@@ -39,8 +39,10 @@ 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://raw.githubusercontent.com/AmnestyTech/investigations/master/2021-12-16_cytrox/cytrox.stix2))
- [An Android Spyware Campaign Linked to a Mercenary Company](https://github.com/AmnestyTech/investigations/tree/master/2023-03-29_android_campaign) ([STIX2](https://github.com/AmnestyTech/investigations/blob/master/2023-03-29_android_campaign/malware.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/generated/stalkerware.stix2).
- We are also maintaining [a list of IOCs](https://github.com/mvt-project/mvt-indicators) in STIX format from public spyware campaigns.
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.
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 from the [mvt-indicators](https://github.com/mvt-project/mvt-indicators/blob/main/indicators.yaml) repository 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.

View File

@@ -1,16 +1,41 @@
# Backup with iTunes app
It is possible to do an iPhone backup by using iTunes on Windows or macOS computers (in most recent versions of macOS, this feature is included in Finder).
It is possible to do an iPhone backup by using iTunes on Windows or macOS computers (in most recent versions of macOS, this feature is included in Finder, see below).
To do that:
* Make sure iTunes is installed.
* Connect your iPhone to your computer using a Lightning/USB cable.
* Open the device in iTunes (or Finder on macOS).
* If you want to have a more accurate detection, ensure that the encrypted backup option is activated and choose a secure password for the backup.
* Start the backup and wait for it to finish (this may take up to 30 minutes).
1. Make sure iTunes is installed.
2. Connect your iPhone to your computer using a Lightning/USB cable.
3. Open the device in iTunes (or Finder on macOS).
4. If you want to have a more accurate detection, ensure that the encrypted backup option is activated and choose a secure password for the backup.
5. Start the backup and wait for it to finish (this may take up to 30 minutes).
![](../../../img/macos-backup.jpg)
_Source: [Apple Support](https://support.apple.com/en-us/HT211229)_
* Once the backup is done, find its location and copy it to a place where it can be analyzed by MVT. On Windows, the backup can be stored either in `%USERPROFILE%\Apple\MobileSync\` or `%USERPROFILE%\AppData\Roaming\Apple Computer\MobileSync\`. On macOS, the backup is stored in `~/Library/Application Support/MobileSync/`.
Once the backup is done, find its location and copy it to a place where it can be analyzed by MVT. On Windows, the backup can be stored either in `%USERPROFILE%\Apple\MobileSync\` or `%USERPROFILE%\AppData\Roaming\Apple Computer\MobileSync\`. On macOS, the backup is stored in `~/Library/Application Support/MobileSync/`.
# Backup with Finder
On more recent MacOS versions, this feature is included in Finder. To do a backup:
1. Launch Finder on your Mac.
2. Connect your iPhone to your Mac using a Lightning/USB cable.
3. Select your device from the list of devices located at the bottom of the left side bar labeled "locations".
4. In the General tab, select `Back up all the data on your iPhone to this Mac` from the options under the Backups section.
5. Check the box that says `Encrypt local backup`. If it is your first time selecting this option, you may need to enter a password to encrypt the backup.
![](../../../img/macos-backup2.png)
_Source: [Apple Support](https://support.apple.com/en-us/HT211229)_
6. Click `Back Up Now` to start the back-up process.
7. The encrypted backup for your iPhone should now start. Once the process finishes, you can check the backup by opening `Finder`, clicking on the `General` tab, then click on `Manage Backups`. Now you should see a list of your backups like the image below:
![](../../../img/macos-backups.png)
_Source: [Apple Support](https://support.apple.com/en-us/HT211229)_
If your backup has a lock next to it like in the image above, then the backup is encrypted. You should also see the date and time when the encrypted backup was created. The backup files are stored in `~/Library/Application Support/MobileSync/`.
## Notes:
- Remember to keep the backup encryption password that you created safe, since without it you will not be able to access/modify/decrypt the backup file.

View File

@@ -3,10 +3,10 @@
If you have correctly [installed libimobiledevice](../install.md) you can easily generate an iTunes backup using the `idevicebackup2` tool included in the suite. First, you might want to ensure that backup encryption is enabled (**note: encrypted backup contain more data than unencrypted backups**):
```bash
idevicebackup2 -i backup encryption on
idevicebackup2 -i encryption on
```
Note that if a backup password was previously set on this device, you might need to use the same or change it. You can try changing password using `idevicebackup2 -i backup changepw`, or by turning off encryption (`idevicebackup2 -i backup encryption off`) and turning it back on again.
Note that if a backup password was previously set on this device, you might need to use the same or change it. You can try changing password using `idevicebackup2 -i changepw`, or by turning off encryption (`idevicebackup2 -i encryption off`) and turning it back on again.
If you are not able to recover or change the password, you should try to disable encryption and obtain an unencrypted backup.

View File

@@ -7,7 +7,7 @@ In this page you can find a (reasonably) up-to-date breakdown of the files creat
### `analytics.json`
!!! info "Availability"
Backup (if encrypted): :material-close:
Backup (if encrypted): :material-close:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `Analytics` module. The module extracts records from the plists inside the SQLite databases located at *private/var/Keychains/Analytics/\*.db*, which contain various analytics information regarding networking, certificate-pinning, TLS, etc. failures.
@@ -16,10 +16,22 @@ If indicators are provided through the command-line, processes and domains are c
---
### `applications.json`
!!! info "Availability"
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `Applications` module. The module extracts the list of applications installed on the device from the `Info.plist` file in backup, or from the `iTunesMetadata.plist` files in a file system dump. These records contains detailed information on the source and installation of the app.
If indicators are provided through the command-line, processes and application ids are checked against the app name of each application. It also flags any applications not installed from the AppStore. Any matches are stored in *applications_detected.json*.
---
### `backup_info.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-close:
This JSON file is created by mvt-ios' `BackupInfo` module. The module extracts some details about the backup and the device, such as name, phone number, IMEI, product type and version.
@@ -29,7 +41,7 @@ This JSON file is created by mvt-ios' `BackupInfo` module. The module extracts s
### `cache_files.json`
!!! info "Availability"
Backup: :material-close:
Backup: :material-close:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `CacheFiles` module. The module extracts records from all SQLite database files stored on disk with the name *Cache.db*. These databases typically contain data from iOS' [internal URL caching](https://developer.apple.com/documentation/foundation/nsurlcache). Through this module you might be able to recover records of HTTP requests and responses performed my applications as well as system services, that would otherwise be unavailable. For example, you might see HTTP requests part of an exploitation chain performed by an iOS service attempting to download a first stage malicious payload.
@@ -38,10 +50,22 @@ If indicators are provided through the command-line, they are checked against th
---
### `calendar.json`
!!! info "Availability"
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `Calendar` module. This module extracts all CalendarItems from the `Calendar.sqlitedb` database. This database contains all calendar entries from the different calendars installed on the phone.
If indicators are provided through the command-line, email addresses are checked against the inviter's email of the different events. Any matches are stored in *calendar_detected.json*.
---
### `calls.json`
!!! info "Availability"
Backup (if encrypted): :material-check:
Backup (if encrypted): :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `Calls` module. The module extracts records from a SQLite database located at */private/var/mobile/Library/CallHistoryDB/CallHistory.storedata*, which contains records of incoming and outgoing calls, including from messaging apps such as WhatsApp or Skype.
@@ -51,7 +75,7 @@ This JSON file is created by mvt-ios' `Calls` module. The module extracts record
### `chrome_favicon.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `ChromeFavicon` module. The module extracts records from a SQLite database located at */private/var/mobile/Containers/Data/Application/\*/Library/Application Support/Google/Chrome/Default/Favicons*, which contains a mapping of favicons' URLs and the visited URLs which loaded them.
@@ -63,7 +87,7 @@ If indicators are provided through the command-line, they are checked against bo
### `chrome_history.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `ChromeHistory` module. The module extracts records from a SQLite database located at */private/var/mobile/Containers/Data/Application/\*/Library/Application Support/Google/Chrome/Default/History*, which contains a history of URL visits.
@@ -75,7 +99,7 @@ If indicators are provided through the command-line, they are checked against th
### `configuration_profiles.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-close:
This JSON file is created by mvt-ios' `ConfigurationProfiles` module. The module extracts details about iOS configuration profiles that have been installed on the device. These should include both default iOS as well as third-party profiles.
@@ -87,7 +111,7 @@ If indicators are provided through the command-line, they are checked against th
### `contacts.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `Contacts` module. The module extracts records from a SQLite database located at */private/var/mobile/Library/AddressBook/AddressBook.sqlitedb*, which contains records from the phone's address book. While this database obviously would not contain any malicious indicators per se, you might want to use it to compare records from other apps (such as iMessage, SMS, etc.) to filter those originating from unknown origins.
@@ -97,7 +121,7 @@ This JSON file is created by mvt-ios' `Contacts` module. The module extracts rec
### `firefox_favicon.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `FirefoxFavicon` module. The module extracts records from a SQLite database located at */private/var/mobile/profile.profile/browser.db*, which contains a mapping of favicons' URLs and the visited URLs which loaded them.
@@ -109,7 +133,7 @@ If indicators are provided through the command-line, they are checked against bo
### `firefox_history.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `FirefoxHistory` module. The module extracts records from a SQLite database located at */private/var/mobile/profile.profile/browser.db*, which contains a history of URL visits.
@@ -121,7 +145,7 @@ If indicators are provided through the command-line, they are checked against th
### `id_status_cache.json`
!!! info "Availability"
Backup (before iOS 14.7): :material-check:
Backup (before iOS 14.7): :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `IDStatusCache` module. The module extracts records from a plist file located at */private/var/mobile/Library/Preferences/com.apple.identityservices.idstatuscache.plist*, which contains a cache of Apple user ID authentication. This chance will indicate when apps like Facetime and iMessage first established contacts with other registered Apple IDs. This is significant because it might contain traces of malicious accounts involved in exploitation of those apps.
@@ -133,7 +157,7 @@ Starting from iOS 14.7.0, this file is empty or absent.
### `shortcuts.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `Shortcuts` module. The module extracts records from an SQLite database located at */private/var/mobile/Library/Shortcuts/Shortcuts.sqlite*, which contains records about the Shortcuts application. Shortcuts are a built-in iOS feature which allows users to automation certain actions on their device. In some cases the legitimate Shortcuts app may be abused by spyware to maintain persistence on an infected devices.
@@ -143,7 +167,7 @@ This JSON file is created by mvt-ios' `Shortcuts` module. The module extracts re
### `interaction_c.json`
!!! info "Availability"
Backup (if encrypted): :material-check:
Backup (if encrypted): :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `InteractionC` module. The module extracts records from a SQLite database located at */private/var/mobile/Library/CoreDuet/People/interactionC.db*, which contains details about user interactions with installed apps.
@@ -153,7 +177,7 @@ This JSON file is created by mvt-ios' `InteractionC` module. The module extracts
### `locationd_clients.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `LocationdClients` module. The module extracts records from a plist file located at */private/var/mobile/Library/Caches/locationd/clients.plist*, which contains a cache of apps which requested access to location services.
@@ -163,7 +187,7 @@ This JSON file is created by mvt-ios' `LocationdClients` module. The module extr
### `manifest.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-close:
This JSON file is created by mvt-ios' `Manifest` module. The module extracts records from the SQLite database *Manifest.db* contained in iTunes backups, and which indexes the locally backed-up files to the original paths on the iOS device.
@@ -175,7 +199,7 @@ If indicators are provided through the command-line, they are checked against th
### `os_analytics_ad_daily.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `OSAnalyticsADDaily` module. The module extracts records from a plist located *private/var/mobile/Library/Preferences/com.apple.osanalytics.addaily.plist*, 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.
@@ -187,7 +211,7 @@ If indicators are provided through the command-line, they are checked against th
### `datausage.json`
!!! info "Availability"
Backup: :material-check:
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 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.
@@ -199,7 +223,7 @@ If indicators are provided through the command-line, they are checked against th
### `netusage.json`
!!! info "Availability"
Backup: :material-close:
Backup: :material-close:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `Netusage` module. The module extracts records from a SQLite database located */private/var/networkd/netusage.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.
@@ -211,7 +235,7 @@ If indicators are provided through the command-line, they are checked against th
### `profile_events.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-close:
This JSON file is created by mvt-ios' `ProfileEvents` module. The module extracts a timeline of configuration profile operations. For example, it should indicate when a new profile was installed from the Settings app, or when one was removed.
@@ -221,7 +245,7 @@ This JSON file is created by mvt-ios' `ProfileEvents` module. The module extract
### `safari_browser_state.json`
!!! info "Availability"
Backup (if encrypted): :material-check:
Backup (if encrypted): :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `SafariBrowserState` module. The module extracts records from the SQLite databases located at */private/var/mobile/Library/Safari/BrowserState.db* or */private/var/mobile/Containers/Data/Application/\*/Library/Safari/BrowserState.db*, which contain records of opened tabs.
@@ -233,7 +257,7 @@ If indicators are provided through the command-line, they are checked against th
### `safari_favicon.json`
!!! info "Availability"
Backup: :material-close:
Backup: :material-close:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `SafariFavicon` module. The module extracts records from the SQLite databases located at */private/var/mobile/Library/Image Cache/Favicons/Favicons.db* or */private/var/mobile/Containers/Data/Application/\*/Library/Image Cache/Favicons/Favicons.db*, which contain mappings of favicons' URLs and the visited URLs which loaded them.
@@ -245,7 +269,7 @@ If indicators are provided through the command-line, they are checked against bo
### `safari_history.json`
!!! info "Availability"
Backup (if encrypted): :material-check:
Backup (if encrypted): :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `SafariHistory` module. The module extracts records from the SQLite databases located at */private/var/mobile/Library/Safari/History.db* or */private/var/mobile/Containers/Data/Application/\*/Library/Safari/History.db*, which contain a history of URL visits.
@@ -257,7 +281,7 @@ If indicators are provided through the command-line, they are checked against th
### `shutdown_log.json`
!!! info "Availability"
Backup (if encrypted): :material-close:
Backup (if encrypted): :material-close:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `ShutdownLog` module. The module extracts records from the shutdown log located at *private/var/db/diagnostics/shutdown.log*. When shutting down an iPhone, a SIGTERM will be sent to all processes runnning. The `shutdown.log` file will log any process (with its pid and path) that did not shut down after the SIGTERM was sent.
@@ -269,10 +293,10 @@ If indicators are provided through the command-line, they are checked against th
### `sms.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `SMS` module. The module extracts a list of SMS messages containing HTTP links from the SQLite database located at */private/var/mobile/Library/SMS/sms.db*.
This JSON file is created by mvt-ios' `SMS` module. The module extracts a list of SMS messages from the SQLite database located at */private/var/mobile/Library/SMS/sms.db*.
If indicators are provided through the command-line, they are checked against the extracted HTTP links. Any matches are stored in *sms_detected.json*.
@@ -281,7 +305,7 @@ If indicators are provided through the command-line, they are checked against th
### `sms_attachments.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `SMSAttachments` module. The module extracts details about attachments sent via SMS or iMessage from the same database used by the `SMS` module. These records might be useful to indicate unique patterns that might be indicative of exploitation attempts leveraging potential vulnerabilities in file format parsers or other forms of file handling by the Messages app.
@@ -291,7 +315,7 @@ This JSON file is created by mvt-ios' `SMSAttachments` module. The module extrac
### `tcc.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `TCC` module. The module extracts records from a SQLite database located at */private/var/mobile/Library/TCC/TCC.db*, which contains a list of which services such as microphone, camera, or location, apps have been granted or denied access to.
@@ -301,7 +325,7 @@ This JSON file is created by mvt-ios' `TCC` module. The module extracts records
### `version_history.json`
!!! info "Availability"
Backup: :material-close:
Backup: :material-close:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `IOSVersionHistory` module. The module extracts records of iOS software updates from analytics plist files located at */private/var/db/analyticsd/Analytics-Journal-\*.ips*.
@@ -311,7 +335,7 @@ This JSON file is created by mvt-ios' `IOSVersionHistory` module. The module ext
### `webkit_indexeddb.json`
!!! info "Availability"
Backup: :material-close:
Backup: :material-close:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `WebkitIndexedDB` module. The module extracts a list of file and folder names located at the following path */private/var/mobile/Containers/Data/Application/\*/Library/WebKit/WebsiteData/IndexedDB*, which contains IndexedDB files created by any app installed on the device.
@@ -323,7 +347,7 @@ If indicators are provided through the command-line, they are checked against th
### `webkit_local_storage.json`
!!! info "Availability"
Backup: :material-close:
Backup: :material-close:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `WebkitLocalStorage` module. The module extracts a list of file and folder names located at the following path */private/var/mobile/Containers/Data/Application/\*/Library/WebKit/WebsiteData/LocalStorage/*, which contains local storage files created by any app installed on the device.
@@ -335,7 +359,7 @@ If indicators are provided through the command-line, they are checked against th
### `webkit_resource_load_statistics.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios `WebkitResourceLoadStatistics` module. The module extracts records from available WebKit ResourceLoadStatistics *observations.db* SQLite3 databases. These records should indicate domain names contacted by apps, including a timestamp.
@@ -347,7 +371,7 @@ If indicators are provided through the command-line, they are checked against th
### `webkit_safari_view_service.json`
!!! info "Availability"
Backup: :material-close:
Backup: :material-close:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `WebkitSafariViewService` module. The module extracts a list of file and folder names located at the following path */private/var/mobile/Containers/Data/Application/\*/SystemData/com.apple.SafariViewService/Library/WebKit/WebsiteData/*, which contains files cached by SafariVewService.
@@ -359,7 +383,7 @@ If indicators are provided through the command-line, they are checked against th
### `webkit_session_resource_log.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `WebkitSessionResourceLog` module. The module extracts records from plist files with the name *full_browsing_session_resourceLog.plist*, which contain records of resources loaded by different domains visited.
@@ -371,10 +395,10 @@ If indicators are provided through the command-line, they are checked against th
### `whatsapp.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `WhatsApp` module. The module extracts a list of WhatsApp messages containing HTTP links from the SQLite database located at *private/var/mobile/Containers/Shared/AppGroup/\*/ChatStorage.sqlite*.
This JSON file is created by mvt-ios' `WhatsApp` module. The module extracts a list of WhatsApp messages from the SQLite database located at *private/var/mobile/Containers/Shared/AppGroup/\*/ChatStorage.sqlite*.
If indicators are provided through the command-line, they are checked against the extracted HTTP links. Any matches are stored in *whatsapp_detected.json*.

View File

@@ -1,7 +1,7 @@
site_name: Mobile Verification Toolkit
repo_url: https://github.com/mvt-project/mvt
edit_uri: edit/main/docs/
copyright: Copyright &copy; 2021-2022 MVT Project Developers
copyright: Copyright &copy; 2021-2023 MVT Project Developers
site_description: Mobile Verification Toolkit Documentation
markdown_extensions:
- attr_list
@@ -46,4 +46,5 @@ nav:
- Check an Android Backup (SMS messages): "android/backup.md"
- Download APKs: "android/download_apks.md"
- Indicators of Compromise: "iocs.md"
- Development: "development.md"
- License: "license.md"

View File

@@ -1,4 +1,4 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/

View File

@@ -1,21 +1,29 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# 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 click
from rich.logging import RichHandler
from mvt.common.cmd_check_iocs import CmdCheckIOCS
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.help import (
HELP_MSG_FAST,
HELP_MSG_HASHES,
HELP_MSG_IOC,
HELP_MSG_LIST_MODULES,
HELP_MSG_MODULE,
HELP_MSG_OUTPUT,
HELP_MSG_SERIAL,
HELP_MSG_VERBOSE,
)
from mvt.common.logo import logo
from mvt.common.updates import IndicatorsUpdates
from mvt.common.utils import init_logging, set_verbose_logging
from .cmd_check_adb import CmdAndroidCheckADB
from .cmd_check_androidqf import CmdAndroidCheckAndroidQF
from .cmd_check_backup import CmdAndroidCheckBackup
from .cmd_check_bugreport import CmdAndroidCheckBugreport
from .cmd_download_apks import DownloadAPKs
@@ -24,48 +32,66 @@ from .modules.adb.packages import Packages
from .modules.backup import BACKUP_MODULES
from .modules.bugreport import BUGREPORT_MODULES
# Setup logging using Rich.
LOG_FORMAT = "[%(name)s] %(message)s"
logging.basicConfig(level="INFO", format=LOG_FORMAT, handlers=[
RichHandler(show_path=False, log_time_format="%X")])
log = logging.getLogger(__name__)
init_logging()
log = logging.getLogger("mvt")
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
#==============================================================================
# ==============================================================================
# Main
#==============================================================================
# ==============================================================================
@click.group(invoke_without_command=False)
def cli():
logo()
#==============================================================================
# ==============================================================================
# Command: version
#==============================================================================
# ==============================================================================
@cli.command("version", help="Show the currently installed version of MVT")
def version():
return
#==============================================================================
# ==============================================================================
# Command: download-apks
#==============================================================================
@cli.command("download-apks", help="Download all or only non-system installed APKs")
# ==============================================================================
@cli.command(
"download-apks",
help="Download all or only non-system installed APKs",
context_settings=CONTEXT_SETTINGS,
)
@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")
@click.option(
"--all-apks",
"-a",
is_flag=True,
help="Extract all packages installed on the phone, including system packages",
)
@click.option("--virustotal", "-v", is_flag=True, help="Check packages on VirusTotal")
@click.option("--output", "-o", type=click.Path(exists=False),
help="Specify a path to a folder where you want to store the APKs")
@click.option("--from-file", "-f", type=click.Path(exists=True),
help="Instead of acquiring from phone, load an existing packages.json file for lookups (mainly for debug purposes)")
@click.option(
"--output",
"-o",
type=click.Path(exists=False),
help="Specify a path to a folder where you want to store the APKs",
)
@click.option(
"--from-file",
"-f",
type=click.Path(exists=True),
help="Instead of acquiring from phone, load an existing packages.json file for "
"lookups (mainly for debug purposes)",
)
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
@click.pass_context
def download_apks(ctx, all_apks, virustotal, output, from_file, serial):
def download_apks(ctx, all_apks, virustotal, output, from_file, serial, verbose):
set_verbose_logging(verbose)
try:
if from_file:
download = DownloadAPKs.from_json(from_file)
else:
# TODO: Do we actually want to be able to run without storing any file?
# TODO: Do we actually want to be able to run without storing any
# file?
if not output:
log.critical("You need to specify an output folder with --output!")
ctx.exit(1)
@@ -94,22 +120,40 @@ def download_apks(ctx, all_apks, virustotal, output, from_file, serial):
ctx.exit(1)
#==============================================================================
# ==============================================================================
# Command: check-adb
#==============================================================================
@cli.command("check-adb", help="Check an Android device over adb")
# ==============================================================================
@cli.command(
"check-adb",
help="Check an Android device over adb",
context_settings=CONTEXT_SETTINGS,
)
@click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL)
@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(
"--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("--fast", "-f", is_flag=True, help=HELP_MSG_FAST)
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
@click.option("--module", "-m", help=HELP_MSG_MODULE)
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
@click.pass_context
def check_adb(ctx, serial, iocs, output, fast, list_modules, module):
cmd = CmdAndroidCheckADB(results_path=output, ioc_files=iocs,
module_name=module, serial=serial, fast_mode=fast)
def check_adb(ctx, serial, iocs, output, fast, list_modules, module, verbose):
set_verbose_logging(verbose)
module_options = {"fast_mode": fast}
cmd = CmdAndroidCheckADB(
results_path=output,
ioc_files=iocs,
module_name=module,
serial=serial,
module_options=module_options,
)
if list_modules:
cmd.list_modules()
@@ -119,25 +163,45 @@ def check_adb(ctx, serial, iocs, output, fast, list_modules, module):
cmd.run()
if len(cmd.timeline_detected) > 0:
log.warning("The analysis of the Android device produced %d detections!",
len(cmd.timeline_detected))
if cmd.detected_count > 0:
log.warning(
"The analysis of the Android device produced %d detections!",
cmd.detected_count,
)
#==============================================================================
# ==============================================================================
# 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)
# ==============================================================================
@cli.command(
"check-bugreport",
help="Check an Android Bug Report",
context_settings=CONTEXT_SETTINGS,
)
@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.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
@click.argument("BUGREPORT_PATH", type=click.Path(exists=True))
@click.pass_context
def check_bugreport(ctx, iocs, output, list_modules, module, bugreport_path):
cmd = CmdAndroidCheckBugreport(target_path=bugreport_path, results_path=output,
ioc_files=iocs, module_name=module)
def check_bugreport(ctx, iocs, output, list_modules, module, verbose, bugreport_path):
set_verbose_logging(verbose)
# Always generate hashes as bug reports are small.
cmd = CmdAndroidCheckBugreport(
target_path=bugreport_path,
results_path=output,
ioc_files=iocs,
module_name=module,
hashes=True,
)
if list_modules:
cmd.list_modules()
@@ -147,25 +211,38 @@ def check_bugreport(ctx, iocs, output, list_modules, module, bugreport_path):
cmd.run()
if len(cmd.timeline_detected) > 0:
log.warning("The analysis of the Android bug report produced %d detections!",
len(cmd.timeline_detected))
if cmd.detected_count > 0:
log.warning(
"The analysis of the Android bug report produced %d detections!",
cmd.detected_count,
)
#==============================================================================
# ==============================================================================
# Command: check-backup
#==============================================================================
@cli.command("check-backup", help="Check an Android Backup")
@click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL)
@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True,
default=[], help=HELP_MSG_IOC)
# ==============================================================================
@cli.command(
"check-backup", help="Check an Android Backup", context_settings=CONTEXT_SETTINGS
)
@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("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
@click.pass_context
def check_backup(ctx, serial, iocs, output, list_modules, backup_path):
cmd = CmdAndroidCheckBackup(target_path=backup_path, results_path=output,
ioc_files=iocs)
def check_backup(ctx, iocs, output, list_modules, verbose, backup_path):
set_verbose_logging(verbose)
# Always generate hashes as backups are generally small.
cmd = CmdAndroidCheckBackup(
target_path=backup_path, results_path=output, ioc_files=iocs, hashes=True
)
if list_modules:
cmd.list_modules()
@@ -175,17 +252,79 @@ def check_backup(ctx, serial, iocs, output, list_modules, backup_path):
cmd.run()
if len(cmd.timeline_detected) > 0:
log.warning("The analysis of the Android backup produced %d detections!",
len(cmd.timeline_detected))
if cmd.detected_count > 0:
log.warning(
"The analysis of the Android backup produced %d detections!",
cmd.detected_count,
)
#==============================================================================
# ==============================================================================
# Command: check-androidqf
# ==============================================================================
@cli.command(
"check-androidqf",
help="Check data collected with AndroidQF",
context_settings=CONTEXT_SETTINGS,
)
@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.option("--hashes", "-H", is_flag=True, help=HELP_MSG_HASHES)
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
@click.argument("ANDROIDQF_PATH", type=click.Path(exists=True))
@click.pass_context
def check_androidqf(
ctx, iocs, output, list_modules, module, hashes, verbose, androidqf_path
):
set_verbose_logging(verbose)
cmd = CmdAndroidCheckAndroidQF(
target_path=androidqf_path,
results_path=output,
ioc_files=iocs,
module_name=module,
hashes=hashes,
)
if list_modules:
cmd.list_modules()
return
log.info("Checking AndroidQF acquisition at path: %s", androidqf_path)
cmd.run()
if cmd.detected_count > 0:
log.warning(
"The analysis of the AndroidQF acquisition produced %d detections!",
cmd.detected_count,
)
# ==============================================================================
# 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)
# ==============================================================================
@cli.command(
"check-iocs",
help="Compare stored JSON results to provided indicators",
context_settings=CONTEXT_SETTINGS,
)
@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))
@@ -201,10 +340,14 @@ def check_iocs(ctx, iocs, list_modules, module, folder):
cmd.run()
#==============================================================================
# ==============================================================================
# Command: download-iocs
#==============================================================================
@cli.command("download-iocs", help="Download public STIX2 indicators")
# ==============================================================================
@cli.command(
"download-iocs",
help="Download public STIX2 indicators",
context_settings=CONTEXT_SETTINGS,
)
def download_indicators():
ioc_updates = IndicatorsUpdates()
ioc_updates.update()

View File

@@ -1,9 +1,10 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
from mvt.common.command import Command
@@ -13,13 +14,24 @@ log = logging.getLogger(__name__)
class CmdAndroidCheckADB(Command):
def __init__(
self,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
ioc_files: Optional[list] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
module_options: Optional[dict] = None,
) -> None:
super().__init__(
target_path=target_path,
results_path=results_path,
ioc_files=ioc_files,
module_name=module_name,
serial=serial,
module_options=module_options,
log=log,
)
name = "check-adb"
modules = ADB_MODULES
def __init__(self, target_path: str = None, results_path: str = None,
ioc_files: list = [], module_name: str = None, serial: str = None,
fast_mode: bool = False):
super().__init__(target_path=target_path, results_path=results_path,
ioc_files=ioc_files, module_name=module_name,
serial=serial, fast_mode=fast_mode, log=log)
self.name = "check-adb"
self.modules = ADB_MODULES

View File

@@ -0,0 +1,39 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
from mvt.common.command import Command
from .modules.androidqf import ANDROIDQF_MODULES
log = logging.getLogger(__name__)
class CmdAndroidCheckAndroidQF(Command):
def __init__(
self,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
ioc_files: Optional[list] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
module_options: Optional[dict] = None,
hashes: bool = False,
) -> None:
super().__init__(
target_path=target_path,
results_path=results_path,
ioc_files=ioc_files,
module_name=module_name,
serial=serial,
module_options=module_options,
hashes=hashes,
log=log,
)
self.name = "check-androidqf"
self.modules = ANDROIDQF_MODULES

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
@@ -9,13 +9,17 @@ import os
import sys
import tarfile
from pathlib import Path
from typing import Callable
from typing import List, Optional
from rich.prompt import Prompt
from mvt.android.parsers.backup import (AndroidBackupParsingError,
InvalidBackupPassword, parse_ab_header,
parse_backup_file)
from mvt.android.modules.backup.base import BackupExtraction
from mvt.android.parsers.backup import (
AndroidBackupParsingError,
InvalidBackupPassword,
parse_ab_header,
parse_backup_file,
)
from mvt.common.command import Command
from .modules.backup import BACKUP_MODULES
@@ -24,22 +28,38 @@ log = logging.getLogger(__name__)
class CmdAndroidCheckBackup(Command):
def __init__(
self,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
ioc_files: Optional[list] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
module_options: Optional[dict] = None,
hashes: bool = False,
) -> None:
super().__init__(
target_path=target_path,
results_path=results_path,
ioc_files=ioc_files,
module_name=module_name,
serial=serial,
module_options=module_options,
hashes=hashes,
log=log,
)
name = "check-backup"
modules = BACKUP_MODULES
self.name = "check-backup"
self.modules = BACKUP_MODULES
def __init__(self, target_path: str = None, results_path: str = None,
ioc_files: list = [], module_name: str = None, serial: str = None,
fast_mode: bool = False):
super().__init__(target_path=target_path, results_path=results_path,
ioc_files=ioc_files, module_name=module_name,
serial=serial, fast_mode=fast_mode, log=log)
self.backup_type = None
self.backup_archive = None
self.backup_files = []
self.backup_type: str = ""
self.backup_archive: Optional[tarfile.TarFile] = None
self.backup_files: List[str] = []
def init(self) -> None:
if not self.target_path:
return
if os.path.isfile(self.target_path):
self.backup_type = "ab"
with open(self.target_path, "rb") as handle:
@@ -58,8 +78,8 @@ class CmdAndroidCheckBackup(Command):
except InvalidBackupPassword:
log.critical("Invalid backup password")
sys.exit(1)
except AndroidBackupParsingError as e:
log.critical("Impossible to parse this backup file: %s", e)
except AndroidBackupParsingError as exc:
log.critical("Impossible to parse this backup file: %s", exc)
log.critical("Please use Android Backup Extractor (ABE) instead")
sys.exit(1)
@@ -73,12 +93,17 @@ class CmdAndroidCheckBackup(Command):
self.target_path = Path(self.target_path).absolute().as_posix()
for root, subdirs, subfiles in os.walk(os.path.abspath(self.target_path)):
for fname in subfiles:
self.backup_files.append(os.path.relpath(os.path.join(root, fname), self.target_path))
self.backup_files.append(
os.path.relpath(os.path.join(root, fname), self.target_path)
)
else:
log.critical("Invalid backup path, path should be a folder or an Android Backup (.ab) file")
log.critical(
"Invalid backup path, path should be a folder or an "
"Android Backup (.ab) file"
)
sys.exit(1)
def module_init(self, module: Callable) -> None:
def module_init(self, module: BackupExtraction) -> None: # type: ignore[override]
if self.backup_type == "folder":
module.from_folder(self.target_path, self.backup_files)
else:

View File

@@ -1,14 +1,15 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# 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 pathlib import Path
from typing import Callable
from typing import List, Optional
from zipfile import ZipFile
from mvt.android.modules.bugreport.base import BugReportModule
from mvt.common.command import Command
from .modules.bugreport import BUGREPORT_MODULES
@@ -17,22 +18,38 @@ log = logging.getLogger(__name__)
class CmdAndroidCheckBugreport(Command):
def __init__(
self,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
ioc_files: Optional[list] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
module_options: Optional[dict] = None,
hashes: bool = False,
) -> None:
super().__init__(
target_path=target_path,
results_path=results_path,
ioc_files=ioc_files,
module_name=module_name,
serial=serial,
module_options=module_options,
hashes=hashes,
log=log,
)
name = "check-bugreport"
modules = BUGREPORT_MODULES
self.name = "check-bugreport"
self.modules = BUGREPORT_MODULES
def __init__(self, target_path: str = None, results_path: str = None,
ioc_files: list = [], module_name: str = None, serial: str = None,
fast_mode: bool = False):
super().__init__(target_path=target_path, results_path=results_path,
ioc_files=ioc_files, module_name=module_name,
serial=serial, fast_mode=fast_mode, log=log)
self.bugreport_format = None
self.bugreport_archive = None
self.bugreport_files = []
self.bugreport_format: str = ""
self.bugreport_archive: Optional[ZipFile] = None
self.bugreport_files: List[str] = []
def init(self) -> None:
if not self.target_path:
return
if os.path.isfile(self.target_path):
self.bugreport_format = "zip"
self.bugreport_archive = ZipFile(self.target_path)
@@ -41,12 +58,19 @@ class CmdAndroidCheckBugreport(Command):
elif os.path.isdir(self.target_path):
self.bugreport_format = "dir"
parent_path = Path(self.target_path).absolute().as_posix()
for root, subdirs, subfiles in os.walk(os.path.abspath(self.target_path)):
for root, _, subfiles in os.walk(os.path.abspath(self.target_path)):
for file_name in subfiles:
self.bugreport_files.append(os.path.relpath(os.path.join(root, file_name), parent_path))
file_path = os.path.relpath(
os.path.join(root, file_name), parent_path
)
self.bugreport_files.append(file_path)
def module_init(self, module: Callable) -> None:
def module_init(self, module: BugReportModule) -> None: # type: ignore[override]
if self.bugreport_format == "zip":
module.from_zip(self.bugreport_archive, self.bugreport_files)
else:
module.from_folder(self.target_path, self.bugreport_files)
def finish(self) -> None:
if self.bugreport_archive:
self.bugreport_archive.close()

View File

@@ -1,12 +1,12 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# 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 logging
import os
from typing import Callable
from typing import Callable, Optional
from rich.progress import track
@@ -21,12 +21,14 @@ log = logging.getLogger(__name__)
class DownloadAPKs(AndroidExtraction):
"""DownloadAPKs is the main class operating the download of APKs
from the device.
"""
def __init__(self, results_path: str = "", all_apks: bool = False,
packages: list = []):
def __init__(
self,
results_path: Optional[str] = None,
all_apks: bool = False,
packages: Optional[list] = None,
) -> None:
"""Initialize module.
:param results_path: Path to the folder where data should be stored
:param all_apks: Boolean indicating whether to download all packages
@@ -64,27 +66,31 @@ class DownloadAPKs(AndroidExtraction):
if "==/" in remote_path:
file_name = "_" + remote_path.split("==/")[1].replace(".apk", "")
local_path = os.path.join(self.results_path_apks,
f"{package_name}{file_name}.apk")
local_path = os.path.join(
self.results_path_apks, f"{package_name}{file_name}.apk"
)
name_counter = 0
while True:
if not os.path.exists(local_path):
break
name_counter += 1
local_path = os.path.join(self.results_path_apks,
f"{package_name}{file_name}_{name_counter}.apk")
local_path = os.path.join(
self.results_path_apks, f"{package_name}{file_name}_{name_counter}.apk"
)
try:
self._adb_download(remote_path, local_path)
except InsufficientPrivileges:
log.error("Unable to pull package file from %s: insufficient privileges, it might be a system app",
remote_path)
log.error(
"Unable to pull package file from %s: insufficient privileges, "
"it might be a system app",
remote_path,
)
self._adb_reconnect()
return None
except Exception as e:
log.exception("Failed to pull package file from %s: %s",
remote_path, e)
except Exception as exc:
log.exception("Failed to pull package file from %s: %s", remote_path, exc)
self._adb_reconnect()
return None
@@ -104,10 +110,10 @@ class DownloadAPKs(AndroidExtraction):
self.packages = m.results
def pull_packages(self) -> None:
"""Download all files of all selected packages from the device.
"""
log.info("Starting extraction of installed APKs at folder %s",
self.results_path)
"""Download all files of all selected packages from the device."""
log.info(
"Starting extraction of installed APKs at folder %s", self.results_path
)
# If the user provided the flag --all-apks we select all packages.
packages_selection = []
@@ -121,8 +127,10 @@ class DownloadAPKs(AndroidExtraction):
if not package.get("system", False):
packages_selection.append(package)
log.info("Selected only %d packages which are not marked as \"system\"",
len(packages_selection))
log.info(
'Selected only %d packages which are not marked as "system"',
len(packages_selection),
)
if len(packages_selection) == 0:
log.info("No packages were selected for download")
@@ -134,19 +142,26 @@ class DownloadAPKs(AndroidExtraction):
if not os.path.exists(self.results_path_apks):
os.makedirs(self.results_path_apks, exist_ok=True)
for i in track(range(len(packages_selection)),
description=f"Downloading {len(packages_selection)} packages..."):
for i in track(
range(len(packages_selection)),
description=f"Downloading {len(packages_selection)} packages...",
):
package = packages_selection[i]
log.info("[%d/%d] Package: %s", i, len(packages_selection),
package["package_name"])
log.info(
"[%d/%d] Package: %s",
i,
len(packages_selection),
package["package_name"],
)
# Sometimes the package path contains multiple lines for multiple apks.
# We loop through each line and download each file.
# Sometimes the package path contains multiple lines for multiple
# apks. We loop through each line and download each file.
for package_file in package["files"]:
device_path = package_file["path"]
local_path = self.pull_package_file(package["package_name"],
device_path)
local_path = self.pull_package_file(
package["package_name"], device_path
)
if not local_path:
continue

View File

@@ -1,4 +1,4 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
@@ -23,8 +23,24 @@ from .settings import Settings
from .sms import SMS
from .whatsapp import Whatsapp
ADB_MODULES = [ChromeHistory, SMS, Whatsapp, Processes, Getprop, Settings,
SELinuxStatus, DumpsysBatteryHistory, DumpsysBatteryDaily,
DumpsysReceivers, DumpsysActivities, DumpsysAccessibility,
DumpsysDBInfo, DumpsysFull, DumpsysAppOps, Packages, Logcat,
RootBinaries, Files]
ADB_MODULES = [
ChromeHistory,
SMS,
Whatsapp,
Processes,
Getprop,
Settings,
SELinuxStatus,
DumpsysBatteryHistory,
DumpsysBatteryDaily,
DumpsysReceivers,
DumpsysActivities,
DumpsysAccessibility,
DumpsysDBInfo,
DumpsysFull,
DumpsysAppOps,
Packages,
Logcat,
RootBinaries,
Files,
]

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
@@ -11,22 +11,27 @@ import string
import sys
import tempfile
import time
from typing import Callable
from typing import Callable, Optional
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,
UsbDeviceNotFoundError, UsbReadFailedError)
from adb_shell.exceptions import (
AdbCommandFailureException,
DeviceAuthError,
UsbDeviceNotFoundError,
UsbReadFailedError,
)
from rich.prompt import Prompt
from usb1 import USBErrorAccess, USBErrorBusy
from mvt.android.parsers.backup import (InvalidBackupPassword, parse_ab_header,
parse_backup_file)
from mvt.android.parsers.backup import (
InvalidBackupPassword,
parse_ab_header,
parse_backup_file,
)
from mvt.common.module import InsufficientPrivileges, MVTModule
log = logging.getLogger(__name__)
ADB_KEY_PATH = os.path.expanduser("~/.android/adbkey")
ADB_PUB_KEY_PATH = os.path.expanduser("~/.android/adbkey.pub")
@@ -34,12 +39,23 @@ ADB_PUB_KEY_PATH = os.path.expanduser("~/.android/adbkey.pub")
class AndroidExtraction(MVTModule):
"""This class provides a base for all Android extraction modules."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self.device = None
self.serial = None
@@ -74,33 +90,49 @@ class AndroidExtraction(MVTModule):
try:
self.device = AdbDeviceUsb(serial=self.serial)
except UsbDeviceNotFoundError:
log.critical("No device found. Make sure it is connected and unlocked.")
self.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(":")
if len(addr) < 2:
raise ValueError("TCP serial number must follow the format: `address:port`")
raise ValueError(
"TCP serial number must follow the format: `address:port`"
)
self.device = AdbDeviceTcp(addr[0], int(addr[1]),
default_transport_timeout_s=30.)
self.device = AdbDeviceTcp(
addr[0], int(addr[1]), default_transport_timeout_s=30.0
)
while True:
try:
self.device.connect(rsa_keys=[signer], auth_timeout_s=5)
except (USBErrorBusy, USBErrorAccess):
log.critical("Device is busy, maybe run `adb kill-server` and try again.")
self.log.critical(
"Device is busy, maybe run `adb kill-server` and try again."
)
sys.exit(-1)
except DeviceAuthError:
log.error("You need to authorize this computer on the Android device. Retrying in 5 seconds...")
self.log.error(
"You need to authorize this computer on the Android device. "
"Retrying in 5 seconds..."
)
time.sleep(5)
except UsbReadFailedError:
log.error("Unable to connect to the device over USB. Try to unplug, plug the device and start again.")
self.log.error(
"Unable to connect to the device over USB. "
"Try to unplug, plug the device and start again."
)
sys.exit(-1)
except OSError as e:
if e.errno == 113 and self.serial:
log.critical("Unable to connect to the device %s: did you specify the correct IP addres?",
self.serial)
except OSError as exc:
if exc.errno == 113 and self.serial:
self.log.critical(
"Unable to connect to the device %s: "
"did you specify the correct IP address?",
self.serial,
)
sys.exit(-1)
else:
break
@@ -111,7 +143,7 @@ class AndroidExtraction(MVTModule):
def _adb_reconnect(self) -> None:
"""Reconnect to device using adb."""
log.info("Reconnecting ...")
self.log.info("Reconnecting ...")
self._adb_disconnect()
self._adb_connect()
@@ -131,12 +163,17 @@ class AndroidExtraction(MVTModule):
:returns: Boolean indicating whether a `su` binary is present or not
"""
return bool(self._adb_command("command -v su"))
result = self._adb_command("command -v su && su -c true")
return bool(result) and "Permission denied" not in result
def _adb_root_or_die(self) -> None:
"""Check if we have a `su` binary, otherwise raise an Exception."""
if not self._adb_check_if_root():
raise InsufficientPrivileges("This module is optionally available in case the device is already rooted. Do NOT root your own device!")
raise InsufficientPrivileges(
"This module is optionally available "
"in case the device is already rooted."
" Do NOT root your own device!"
)
def _adb_command_as_root(self, command):
"""Execute an adb shell command.
@@ -157,63 +194,81 @@ class AndroidExtraction(MVTModule):
# TODO: Need to support checking files without root privileges as well.
# Connect to the device over adb.
self._adb_connect()
# Check if we have root, if not raise an Exception.
self._adb_root_or_die()
return bool(self._adb_command_as_root(f"[ ! -f {file} ] || echo 1"))
def _adb_download(self, remote_path: str, local_path: str,
progress_callback: Callable = None,
retry_root: bool = True) -> None:
def _adb_download(
self,
remote_path: str,
local_path: str,
progress_callback: Optional[Callable] = None,
retry_root: Optional[bool] = True,
) -> None:
"""Download a file form the device.
:param remote_path: Path to download from the device
:param local_path: Path to where to locally store the copy of the file
:param progress_callback: Callback for download progress bar (Default value = None)
:param progress_callback: Callback for download progress bar
(Default value = None)
:param retry_root: Default value = True)
"""
try:
self.device.pull(remote_path, local_path, progress_callback)
except AdbCommandFailureException as e:
except AdbCommandFailureException as exc:
if retry_root:
self._adb_download_root(remote_path, local_path, progress_callback)
else:
raise Exception(f"Unable to download file {remote_path}: {e}")
raise Exception(
f"Unable to download file {remote_path}: {exc}"
) from exc
def _adb_download_root(self, remote_path: str, local_path: str,
progress_callback: Callable = None) -> None:
def _adb_download_root(
self,
remote_path: str,
local_path: str,
progress_callback: Optional[Callable] = None,
) -> None:
try:
# Check if we have root, if not raise an Exception.
self._adb_root_or_die()
# We generate a random temporary filename.
tmp_filename = "tmp_" + ''.join(random.choices(string.ascii_uppercase + string.ascii_lowercase + string.digits, k=10))
allowed_chars = (
string.ascii_uppercase + string.ascii_lowercase + string.digits
)
tmp_filename = "tmp_" + "".join(random.choices(allowed_chars, k=10))
# We create a temporary local file.
new_remote_path = f"/sdcard/{tmp_filename}"
# We copy the file from the data folder to /sdcard/.
cp = self._adb_command_as_root(f"cp {remote_path} {new_remote_path}")
if cp.startswith("cp: ") and "No such file or directory" in cp:
cp_output = self._adb_command_as_root(f"cp {remote_path} {new_remote_path}")
if (
cp_output.startswith("cp: ")
and "No such file or directory" in cp_output
):
raise Exception(f"Unable to process file {remote_path}: File not found")
elif cp.startswith("cp: ") and "Permission denied" in cp:
raise Exception(f"Unable to process file {remote_path}: Permission denied")
if cp_output.startswith("cp: ") and "Permission denied" in cp_output:
raise Exception(
f"Unable to process file {remote_path}: Permission denied"
)
# We download from /sdcard/ to the local temporary file.
# If it doesn't work now, don't try again (retry_root=False)
self._adb_download(new_remote_path, local_path, retry_root=False)
self._adb_download(
new_remote_path, local_path, progress_callback, retry_root=False
)
# Delete the copy on /sdcard/.
self._adb_command(f"rm -rf {new_remote_path}")
except AdbCommandFailureException as e:
raise Exception(f"Unable to download file {remote_path}: {e}")
except AdbCommandFailureException as exc:
raise Exception(f"Unable to download file {remote_path}: {exc}") from exc
def _adb_process_file(self, remote_path: str,
process_routine: Callable) -> None:
def _adb_process_file(self, remote_path: str, process_routine: Callable) -> None:
"""Download a local copy of a file which is only accessible as root.
This is a wrapper around process_routine.
@@ -223,7 +278,6 @@ class AndroidExtraction(MVTModule):
"""
# Connect to the device over adb.
self._adb_connect()
# Check if we have root, if not raise an Exception.
self._adb_root_or_die()
@@ -234,10 +288,10 @@ class AndroidExtraction(MVTModule):
new_remote_path = f"/sdcard/Download/{local_name}"
# We copy the file from the data folder to /sdcard/.
cp = self._adb_command_as_root(f"cp {remote_path} {new_remote_path}")
if cp.startswith("cp: ") and "No such file or directory" in cp:
cp_output = self._adb_command_as_root(f"cp {remote_path} {new_remote_path}")
if cp_output.startswith("cp: ") and "No such file or directory" in cp_output:
raise Exception(f"Unable to process file {remote_path}: File not found")
elif cp.startswith("cp: ") and "Permission denied" in cp:
if cp_output.startswith("cp: ") and "Permission denied" in cp_output:
raise Exception(f"Unable to process file {remote_path}: Permission denied")
# We download from /sdcard/ to the local temporary file.
@@ -250,26 +304,31 @@ class AndroidExtraction(MVTModule):
tmp.close()
# Delete the copy on /sdcard/.
self._adb_command(f"rm -f {new_remote_path}")
# Disconnect from the device.
self._adb_disconnect()
def _generate_backup(self, package_name: str) -> bytes:
self.log.warning("Please check phone and accept Android backup prompt. You may need to set a backup password. \a")
self.log.info(
"Please check phone and accept Android backup prompt. "
"You may need to set a backup password. \a"
)
# 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))
# TODO: Base64 encoding as temporary fix to avoid byte-mangling over
# the shell transport...
cmd = f"/system/bin/bu backup -nocompress '{package_name}' | base64"
backup_output_b64 = self._adb_command(cmd)
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
self.log.error(
"Extracting SMS via Android backup failed. "
"No valid backup data found."
)
return None
if header["encryption"] == "none":
return parse_backup_file(backup_output, password=None)
for password_retry in range(0, 3):
for _ in range(0, 3):
backup_password = Prompt.ask("Enter backup password", password=True)
try:
decrypted_backup_tar = parse_backup_file(backup_output, backup_password)
@@ -277,7 +336,9 @@ class AndroidExtraction(MVTModule):
except InvalidBackupPassword:
self.log.error("You provided the wrong password! Please try again...")
self.log.warn("All attempts to decrypt backup with password failed!")
self.log.error("All attempts to decrypt backup with password failed!")
return None
def run(self) -> None:
"""Run the main procedure."""

View File

@@ -1,38 +1,49 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# 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 sqlite3
from typing import Optional, Union
from mvt.common.utils import (convert_chrometime_to_unix,
convert_timestamp_to_iso)
from mvt.common.utils import convert_chrometime_to_datetime, convert_datetime_to_iso
from .base import AndroidExtraction
log = logging.getLogger(__name__)
CHROME_HISTORY_PATH = "data/data/com.android.chrome/app_chrome/Default/History"
class ChromeHistory(AndroidExtraction):
"""This module extracts records from Android's Chrome browsing history."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self.results = []
def serialize(self, record: dict) -> None:
def serialize(self, record: dict) -> Union[dict, list]:
return {
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": "visit",
"data": f"{record['id']} - {record['url']} (visit ID: {record['visit_id']}, redirect source: {record['redirect_source']})"
"data": f"{record['id']} - {record['url']} (visit ID: {record['visit_id']}, "
f"redirect source: {record['redirect_source']})",
}
def check_indicators(self) -> None:
@@ -49,9 +60,11 @@ class ChromeHistory(AndroidExtraction):
:param db_path: Path to the History database to process.
"""
assert isinstance(self.results, list) # assert results type for mypy
conn = sqlite3.connect(db_path)
cur = conn.cursor()
cur.execute("""
cur.execute(
"""
SELECT
urls.id,
urls.url,
@@ -61,26 +74,36 @@ class ChromeHistory(AndroidExtraction):
FROM urls
JOIN visits ON visits.url = urls.id
ORDER BY visits.visit_time;
""")
"""
)
for item in cur:
self.results.append({
"id": item[0],
"url": item[1],
"visit_id": item[2],
"timestamp": item[3],
"isodate": convert_timestamp_to_iso(convert_chrometime_to_unix(item[3])),
"redirect_source": item[4],
})
self.results.append(
{
"id": item[0],
"url": item[1],
"visit_id": item[2],
"timestamp": item[3],
"isodate": convert_datetime_to_iso(
convert_chrometime_to_datetime(item[3])
),
"redirect_source": item[4],
}
)
cur.close()
conn.close()
log.info("Extracted a total of %d history items", len(self.results))
self.log.info("Extracted a total of %d history items", len(self.results))
def run(self) -> None:
self._adb_connect()
try:
self._adb_process_file(os.path.join("/", CHROME_HISTORY_PATH),
self._parse_db)
except Exception as e:
self.log.error(e)
self._adb_process_file(
os.path.join("/", CHROME_HISTORY_PATH), self._parse_db
)
except Exception as exc:
self.log.error(exc)
self._adb_disconnect()

View File

@@ -1,26 +1,36 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
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: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def check_indicators(self) -> None:
if not self.indicators:
@@ -41,6 +51,10 @@ class DumpsysAccessibility(AndroidExtraction):
self.results = parse_dumpsys_accessibility(output)
for result in self.results:
log.info("Found installed accessibility service \"%s\"", result.get("service"))
self.log.info(
'Found installed accessibility service "%s"', result.get("service")
)
self.log.info("Identified a total of %d accessibility services", len(self.results))
self.log.info(
"Identified a total of %d accessibility services", len(self.results)
)

View File

@@ -1,26 +1,36 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
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: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self.results = results if results else {}

View File

@@ -1,30 +1,40 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional, Union
from mvt.android.parsers.dumpsys import parse_dumpsys_appops
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysAppOps(AndroidExtraction):
"""This module extracts records from App-op Manager."""
slug = "dumpsys_appops"
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def serialize(self, record: dict) -> None:
def serialize(self, record: dict) -> Union[dict, list]:
records = []
for perm in record["permissions"]:
if "entries" not in perm:
@@ -32,12 +42,15 @@ class DumpsysAppOps(AndroidExtraction):
for entry in perm["entries"]:
if "timestamp" in entry:
records.append({
"timestamp": entry["timestamp"],
"module": self.__class__.__name__,
"event": entry["access"],
"data": f"{record['package_name']} access to {perm['name']}: {entry['access']}",
})
records.append(
{
"timestamp": entry["timestamp"],
"module": self.__class__.__name__,
"event": entry["access"],
"data": f"{record['package_name']} access to "
f"{perm['name']}: {entry['access']}",
}
)
return records
@@ -51,9 +64,14 @@ class DumpsysAppOps(AndroidExtraction):
continue
for perm in result["permissions"]:
if perm["name"] == "REQUEST_INSTALL_PACKAGES" and perm["access"] == "allow":
self.log.info("Package %s with REQUEST_INSTALL_PACKAGES permission",
result["package_name"])
if (
perm["name"] == "REQUEST_INSTALL_PACKAGES"
and perm["access"] == "allow"
):
self.log.info(
"Package %s with REQUEST_INSTALL_PACKAGES " "permission",
result["package_name"],
)
def run(self) -> None:
self._adb_connect()
@@ -62,5 +80,6 @@ class DumpsysAppOps(AndroidExtraction):
self.results = parse_dumpsys_appops(output)
self.log.info("Extracted a total of %d records from app-ops manager",
len(self.results))
self.log.info(
"Extracted a total of %d records from app-ops manager", len(self.results)
)

View File

@@ -1,33 +1,44 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional, Union
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: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def serialize(self, record: dict) -> None:
def serialize(self, record: dict) -> Union[dict, list]:
return {
"timestamp": record["from"],
"module": self.__class__.__name__,
"event": "battery_daily",
"data": f"Recorded update of package {record['package_name']} with vers {record['vers']}"
"data": f"Recorded update of package {record['package_name']} "
f"with vers {record['vers']}",
}
def check_indicators(self) -> None:
@@ -48,4 +59,6 @@ class DumpsysBatteryDaily(AndroidExtraction):
self.results = parse_dumpsys_battery_daily(output)
self.log.info("Extracted %d records from battery daily stats", len(self.results))
self.log.info(
"Extracted %d records from battery daily stats", len(self.results)
)

View File

@@ -1,26 +1,36 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
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: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def check_indicators(self) -> None:
if not self.indicators:

View File

@@ -1,28 +1,38 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
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: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def check_indicators(self) -> None:
if not self.indicators:
@@ -44,5 +54,7 @@ class DumpsysDBInfo(AndroidExtraction):
self.results = parse_dumpsys_dbinfo(output)
self.log.info("Extracted a total of %d records from database information",
len(self.results))
self.log.info(
"Extracted a total of %d records from database information",
len(self.results),
)

View File

@@ -1,25 +1,35 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# 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 typing import Optional
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysFull(AndroidExtraction):
"""This module extracts stats on battery consumption by processes."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def run(self) -> None:
self._adb_connect()
@@ -30,6 +40,6 @@ class DumpsysFull(AndroidExtraction):
with open(output_path, "w", encoding="utf-8") as handle:
handle.write(output)
log.info("Full dumpsys output stored at %s", output_path)
self.log.info("Full dumpsys output stored at %s", output_path)
self._adb_disconnect()

View File

@@ -1,16 +1,15 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
from mvt.android.parsers import parse_dumpsys_receiver_resolver_table
from .base import AndroidExtraction
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"
@@ -21,12 +20,23 @@ INTENT_NEW_OUTGOING_CALL = "android.intent.action.NEW_OUTGOING_CALL"
class DumpsysReceivers(AndroidExtraction):
"""This module extracts details on receivers for risky activities."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self.results = results if results else {}
@@ -37,26 +47,37 @@ class DumpsysReceivers(AndroidExtraction):
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"])
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"])
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"])
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"])
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"])
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
ioc = self.indicators.check_app_id(receiver["package_name"])
if ioc:
receiver["matched_indicator"] = ioc
self.detected.append({intent: receiver})
continue
def run(self) -> None:
self._adb_connect()

View File

@@ -1,19 +1,17 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# 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 logging
import os
import stat
from typing import Optional, Union
from mvt.common.utils import convert_timestamp_to_iso
from mvt.common.utils import convert_unix_to_iso
from .base import AndroidExtraction
log = logging.getLogger(__name__)
ANDROID_TMP_FOLDERS = [
"/tmp/",
"/data/local/tmp/",
@@ -27,15 +25,26 @@ ANDROID_MEDIA_FOLDERS = [
class Files(AndroidExtraction):
"""This module extracts the list of files on the device."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self.full_find = False
def serialize(self, record: dict) -> None:
def serialize(self, record: dict) -> Union[dict, list, None]:
if "modified_time" in record:
return {
"timestamp": record["modified_time"],
@@ -44,17 +53,26 @@ class Files(AndroidExtraction):
"data": record["path"],
}
return None
def check_indicators(self) -> None:
for result in self.results:
if result.get("is_suid"):
self.log.warning("Found an SUID file in a non-standard directory \"%s\".",
result["path"])
self.log.warning(
'Found an SUID file in a non-standard directory "%s".',
result["path"],
)
if self.indicators and self.indicators.check_file_path(result["path"]):
self.log.warning("Found a known suspicous file at path: \"%s\"", result["path"])
self.log.warning(
'Found a known suspicous file at path: "%s"', result["path"]
)
self.detected.append(result)
def backup_file(self, file_path: str) -> None:
if not self.results_path:
return
local_file_name = file_path.replace("/", "_").replace(" ", "-")
local_files_folder = os.path.join(self.results_path, "files")
if not os.path.exists(local_files_folder):
@@ -63,32 +81,40 @@ class Files(AndroidExtraction):
local_file_path = os.path.join(local_files_folder, local_file_name)
try:
self._adb_download(remote_path=file_path,
local_path=local_file_path)
self._adb_download(remote_path=file_path, local_path=local_file_path)
except Exception:
pass
else:
self.log.info("Downloaded file %s to local copy at %s",
file_path, local_file_path)
self.log.info(
"Downloaded file %s to local copy at %s", file_path, local_file_path
)
def find_files(self, folder: str) -> None:
assert isinstance(self.results, list)
if self.full_find:
output = self._adb_command(f"find '{folder}' -type f -printf '%T@ %m %s %u %g %p\n' 2> /dev/null")
cmd = f"find '{folder}' -type f -printf '%T@ %m %s %u %g %p\n' 2> /dev/null"
output = self._adb_command(cmd)
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))))
file_info = file_line.rstrip().split(" ", 5)
if len(file_line) < 6:
self.log.info("Skipping invalid file info - %s", file_line.rstrip())
continue
[unix_timestamp, mode, size, owner, group, full_path] = file_info
mod_time = convert_unix_to_iso(unix_timestamp)
self.results.append({
"path": full_path,
"modified_time": mod_time,
"mode": mode,
"is_suid": (int(mode, 8) & stat.S_ISUID) == 2048,
"is_sgid": (int(mode, 8) & stat.S_ISGID) == 1024,
"size": size,
"owner": owner,
"group": group,
})
self.results.append(
{
"path": full_path,
"modified_time": mod_time,
"mode": mode,
"is_suid": (int(mode, 8) & stat.S_ISUID) == 2048,
"is_sgid": (int(mode, 8) & stat.S_ISGID) == 1024,
"size": size,
"owner": owner,
"group": group,
}
)
else:
output = self._adb_command(f"find '{folder}' -type f 2> /dev/null")
for file_line in output.splitlines():
@@ -97,7 +123,8 @@ class Files(AndroidExtraction):
def run(self) -> None:
self._adb_connect()
output = self._adb_command("find '/' -maxdepth 1 -printf '%T@ %m %s %u %g %p\n' 2> /dev/null")
cmd = "find '/' -maxdepth 1 -printf '%T@ %m %s %u %g %p\n' 2> /dev/null"
output = self._adb_command(cmd)
if output or output.strip().splitlines():
self.full_find = True
@@ -105,19 +132,20 @@ class Files(AndroidExtraction):
self.find_files(tmp_folder)
for entry in self.results:
self.log.info("Found file in tmp folder at path %s",
entry.get("path"))
if self.results_path:
self.backup_file(entry.get("path"))
self.log.info("Found file in tmp folder at path %s", entry.get("path"))
self.backup_file(entry.get("path"))
for media_folder in ANDROID_MEDIA_FOLDERS:
self.find_files(media_folder)
self.log.info("Found %s files in primary Android tmp and media folders",
len(self.results))
self.log.info(
"Found %s files in primary Android tmp and media folders", len(self.results)
)
if self.fast_mode:
self.log.info("Flag --fast was enabled: skipping full file listing")
if self.module_options.get("fast_mode", None):
self.log.info(
"The `fast_mode` option was enabled: skipping full file listing"
)
else:
self.log.info("Processing full file listing. This may take a while...")
self.find_files("/")

View File

@@ -1,30 +1,50 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# 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 datetime import datetime, timedelta
from typing import Optional
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: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self.results = {} if not results else results
def check_indicators(self) -> None:
if not self.indicators:
return
for result in self.results:
ioc = self.indicators.check_android_property_name(result.get("name", ""))
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
def run(self) -> None:
self._adb_connect()
output = self._adb_command("getprop")
@@ -33,11 +53,15 @@ class Getprop(AndroidExtraction):
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)
for entry in self.results:
if entry.get("name", "") != "ro.build.version.security_patch":
continue
patch_date = datetime.strptime(entry["value"], "%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)",
entry["value"],
)
self.log.info("Extracted %d Android system properties", len(self.results))

View File

@@ -1,49 +1,57 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# 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 typing import Optional
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class Logcat(AndroidExtraction):
"""This module extracts details on installed packages."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def run(self) -> None:
self._adb_connect()
# Get the current logcat.
output = self._adb_command("logcat -d")
output = self._adb_command('logcat -d -b all "*:V"')
# Get the locat prior to last reboot.
last_output = self._adb_command("logcat -L")
last_output = self._adb_command('logcat -L -b all "*:V"')
if self.results_path:
logcat_path = os.path.join(self.results_path,
"logcat.txt")
logcat_path = os.path.join(self.results_path, "logcat.txt")
with open(logcat_path, "w", encoding="utf-8") as handle:
handle.write(output)
log.info("Current logcat logs stored at %s",
logcat_path)
self.log.info("Current logcat logs stored at %s", logcat_path)
logcat_last_path = os.path.join(self.results_path,
"logcat_last.txt")
logcat_last_path = os.path.join(self.results_path, "logcat_last.txt")
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",
logcat_last_path)
self.log.info(
"Logcat logs prior to last reboot stored at %s", logcat_last_path
)
self._adb_disconnect()

View File

@@ -1,21 +1,21 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import List, Optional, Union
from rich.console import Console
from rich.progress import track
from rich.table import Table
from rich.text import Text
from mvt.android.parsers.dumpsys import parse_dumpsys_package_for_details
from mvt.common.virustotal import VTNoKey, VTQuotaExceeded, virustotal_lookup
from .base import AndroidExtraction
log = logging.getLogger(__name__)
DANGEROUS_PERMISSIONS_THRESHOLD = 10
DANGEROUS_PERMISSIONS = [
"android.permission.ACCESS_COARSE_LOCATION",
@@ -39,8 +39,7 @@ DANGEROUS_PERMISSIONS = [
"android.permission.USE_SIP",
"com.android.browser.permission.READ_HISTORY_BOOKMARKS",
]
ROOT_PACKAGES = [
ROOT_PACKAGES: List[str] = [
"com.noshufou.android.su",
"com.noshufou.android.su.elite",
"eu.chainfire.supersu",
@@ -67,45 +66,93 @@ ROOT_PACKAGES = [
"com.kingouser.com",
"com.topjohnwu.magisk",
]
SECURITY_PACKAGES = [
"com.policydm",
"com.samsung.android.app.omcagent",
"com.samsung.android.securitylogagent",
"com.sec.android.soagent",
]
SYSTEM_UPDATE_PACKAGES = [
"com.android.updater",
"com.google.android.gms",
"com.huawei.android.hwouc",
"com.lge.lgdmsclient",
"com.motorola.ccc.ota",
"com.oneplus.opbackup",
"com.oppo.ota",
"com.transsion.systemupdate",
"com.wssyncmldm",
]
class Packages(AndroidExtraction):
"""This module extracts the list of installed packages."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def serialize(self, record: dict) -> None:
def serialize(self, record: dict) -> Union[dict, list]:
records = []
timestamps = [
{"event": "package_install", "timestamp": record["timestamp"]},
{"event": "package_first_install", "timestamp": record["first_install_time"]},
{
"event": "package_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"{record['package_name']} (system: {record['system']}, third party: {record['third_party']})",
})
for timestamp in timestamps:
records.append(
{
"timestamp": timestamp["timestamp"],
"module": self.__class__.__name__,
"event": timestamp["event"],
"data": f"{record['package_name']} (system: {record['system']},"
f" third party: {record['third_party']})",
}
)
return records
def check_indicators(self) -> None:
for result in self.results:
if result["package_name"] in ROOT_PACKAGES:
self.log.warning("Found an installed package related to rooting/jailbreaking: \"%s\"",
result["package_name"])
self.log.warning(
"Found an installed package related to "
'rooting/jailbreaking: "%s"',
result["package_name"],
)
self.detected.append(result)
continue
if result["package_name"] in SECURITY_PACKAGES and result["disabled"]:
self.log.warning(
'Found a security package disabled: "%s"', result["package_name"]
)
if result["package_name"] in SYSTEM_UPDATE_PACKAGES and result["disabled"]:
self.log.warning(
'System OTA update package "%s" disabled on the phone',
result["package_name"],
)
if not self.indicators:
continue
@@ -132,14 +179,14 @@ class Packages(AndroidExtraction):
total_hashes = len(hashes)
detections = {}
for i in track(range(total_hashes), description=f"Looking up {total_hashes} files..."):
progress_desc = f"Looking up {total_hashes} files..."
for i in track(range(total_hashes), description=progress_desc):
try:
results = virustotal_lookup(hashes[i])
except VTNoKey as e:
log.info(e)
except VTNoKey:
return
except VTQuotaExceeded as e:
log.error("Unable to continue: %s", e)
except VTQuotaExceeded as exc:
print("Unable to continue: %s", exc)
break
if not results:
@@ -176,43 +223,17 @@ class Packages(AndroidExtraction):
@staticmethod
def parse_package_for_details(output: str) -> dict:
details = {
"uid": "",
"version_name": "",
"version_code": "",
"timestamp": "",
"first_install_time": "",
"last_update_time": "",
"requested_permissions": [],
}
in_permissions = False
lines = []
in_packages = False
for line in output.splitlines():
if in_permissions:
if line.startswith(" " * 4) and not line.startswith(" " * 6):
in_permissions = False
continue
if in_packages:
if line.strip() == "":
break
lines.append(line)
if line.strip() == "Packages:":
in_packages = True
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
return parse_dumpsys_package_for_details("\n".join(lines))
def _get_files_for_package(self, package_name: str) -> list:
output = self._adb_command(f"pm path {package_name}")
@@ -224,18 +245,24 @@ class Packages(AndroidExtraction):
for file_path in output.splitlines():
file_path = file_path.strip()
md5 = self._adb_command(f"md5sum {file_path}").split(" ")[0]
sha1 = self._adb_command(f"sha1sum {file_path}").split(" ")[0]
sha256 = self._adb_command(f"sha256sum {file_path}").split(" ")[0]
sha512 = self._adb_command(f"sha512sum {file_path}").split(" ")[0]
md5 = self._adb_command(f"md5sum {file_path}").split(" ", maxsplit=1)[0]
sha1 = self._adb_command(f"sha1sum {file_path}").split(" ", maxsplit=1)[0]
sha256 = self._adb_command(f"sha256sum {file_path}").split(" ", maxsplit=1)[
0
]
sha512 = self._adb_command(f"sha512sum {file_path}").split(" ", maxsplit=1)[
0
]
package_files.append({
"path": file_path,
"md5": md5,
"sha1": sha1,
"sha256": sha256,
"sha512": sha512,
})
package_files.append(
{
"path": file_path,
"md5": md5,
"sha1": sha1,
"sha256": sha256,
"sha512": sha512,
}
)
return package_files
@@ -304,8 +331,12 @@ class Packages(AndroidExtraction):
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)
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:
@@ -313,13 +344,18 @@ class Packages(AndroidExtraction):
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"])
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:
if not self.module_options.get("fast_mode", None):
self.check_virustotal(packages_to_lookup)
self.log.info("Extracted at total of %d installed package names",
len(self.results))
self.log.info(
"Extracted at total of %d installed package names", len(self.results)
)
self._adb_disconnect()

View File

@@ -1,31 +1,55 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class Processes(AndroidExtraction):
"""This module extracts details on running processes."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def check_indicators(self) -> None:
if not self.indicators:
return
for result in self.results:
ioc = self.indicators.check_app_id(result.get("name", ""))
proc_name = result.get("proc_name", "")
if not proc_name:
continue
# Skipping this process because of false positives.
if result["proc_name"] == "gatekeeperd":
continue
ioc = self.indicators.check_app_id(proc_name)
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
ioc = self.indicators.check_process(proc_name)
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
@@ -33,7 +57,7 @@ class Processes(AndroidExtraction):
def run(self) -> None:
self._adb_connect()
output = self._adb_command("ps -e")
output = self._adb_command("ps -A")
for line in output.splitlines()[1:]:
line = line.strip()
@@ -63,4 +87,4 @@ class Processes(AndroidExtraction):
self._adb_disconnect()
log.info("Extracted records on a total of %d processes", len(self.results))
self.log.info("Extracted records on a total of %d processes", len(self.results))

View File

@@ -1,24 +1,39 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class RootBinaries(AndroidExtraction):
"""This module extracts the list of installed packages."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def check_indicators(self) -> None:
for root_binary in self.results:
self.detected.append(root_binary)
self.log.warning('Found root binary "%s"', root_binary)
def run(self) -> None:
root_binaries = [
@@ -50,7 +65,6 @@ class RootBinaries(AndroidExtraction):
if "which: not found" in output:
continue
self.detected.append(root_binary)
self.log.warning("Found root binary \"%s\"", root_binary)
self.results.append(root_binary)
self._adb_disconnect()

View File

@@ -1,26 +1,36 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class SELinuxStatus(AndroidExtraction):
"""This module checks if SELinux is being enforced."""
slug = "selinux_status"
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self.results = {} if not results else results
@@ -35,4 +45,4 @@ class SELinuxStatus(AndroidExtraction):
if status == "enforcing":
self.log.info("SELinux is being regularly enforced")
else:
self.log.warning("SELinux status is \"%s\"!", status)
self.log.warning('SELinux status is "%s"!', status)

View File

@@ -1,15 +1,13 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
from .base import AndroidExtraction
log = logging.getLogger(__name__)
ANDROID_DANGEROUS_SETTINGS = [
{
"description": "disabled Google Play Services apps verification",
@@ -55,31 +53,46 @@ ANDROID_DANGEROUS_SETTINGS = [
"description": "enabled installation of non Google Play apps",
"key": "install_non_market_apps",
"safe_value": "0",
}
},
]
class Settings(AndroidExtraction):
"""This module extracts Android system settings."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self.results = {} if not results else results
def check_indicators(self) -> None:
for namespace, settings in self.results.items():
for _, 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"])
self.log.warning(
'Found suspicious setting "%s = %s" (%s)',
key,
value,
danger["description"],
)
break
def run(self) -> None:

View File

@@ -1,21 +1,19 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# 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 sqlite3
from typing import Optional, Union
from mvt.android.parsers.backup import (AndroidBackupParsingError,
parse_tar_for_sms)
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 mvt.common.utils import check_for_links, convert_unix_to_iso
from .base import AndroidExtraction
log = logging.getLogger(__name__)
SMS_BUGLE_PATH = "data/data/com.google.android.apps.messaging/databases/bugle_db"
SMS_BUGLE_QUERY = """
SELECT
@@ -44,22 +42,35 @@ FROM sms;
class SMS(AndroidExtraction):
"""This module extracts all SMS messages containing links."""
"""This module extracts all SMS messages."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def serialize(self, record: dict) -> None:
self.sms_db_type = 0
def serialize(self, record: dict) -> Union[dict, list]:
body = record["body"].replace("\n", "\\n")
return {
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": f"sms_{record['direction']}",
"data": f"{record.get('address', 'unknown source')}: \"{body}\""
"data": f"{record.get('address', 'unknown source')}: \"{body}\"",
}
def check_indicators(self) -> None:
@@ -70,8 +81,10 @@ class SMS(AndroidExtraction):
if "body" not in message:
continue
# TODO: check links exported from the body previously.
message_links = check_for_links(message["body"])
message_links = message.get("links", [])
if message_links == []:
message_links = check_for_links(message["body"])
if self.indicators.check_domains(message_links):
self.detected.append(message)
@@ -84,9 +97,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,22 +109,23 @@ class SMS(AndroidExtraction):
for index, value in enumerate(item):
message[names[index]] = value
message["direction"] = ("received" if message["incoming"] == 1 else "sent")
message["isodate"] = convert_timestamp_to_iso(message["timestamp"])
message["direction"] = "received" if message["incoming"] == 1 else "sent"
message["isodate"] = convert_unix_to_iso(message["timestamp"])
# 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["body"]) or message["body"].strip() == "":
self.results.append(message)
# Extract links in the message body
links = check_for_links(message["body"])
message["links"] = links
self.results.append(message)
cur.close()
conn.close()
log.info("Extracted a total of %d SMS messages containing links", len(self.results))
self.log.info("Extracted a total of %d SMS messages", len(self.results))
def _extract_sms_adb(self) -> None:
"""Use the Android backup command to extract SMS data from the native SMS
app.
"""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 algorithm. This module only supports
@@ -124,24 +138,39 @@ class SMS(AndroidExtraction):
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")
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))
self.log.info("Extracted a total of %d SMS messages", len(self.results))
def run(self) -> None:
self._adb_connect()
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)
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
)
self._adb_disconnect()
return
except InsufficientPrivileges:
pass
self.log.warn("No SMS database found. Trying extraction of SMS data using " \
"Android backup feature.")
self.log.info(
"No SMS database found. Trying extraction of SMS data "
"using Android backup feature."
)
self._extract_sms_adb()
self._adb_disconnect()

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
@@ -7,33 +7,43 @@ import base64
import logging
import os
import sqlite3
from typing import Optional, Union
from mvt.common.utils import check_for_links, convert_timestamp_to_iso
from mvt.common.utils import check_for_links, convert_unix_to_iso
from .base import AndroidExtraction
log = logging.getLogger(__name__)
WHATSAPP_PATH = "data/data/com.whatsapp/databases/msgstore.db"
class Whatsapp(AndroidExtraction):
"""This module extracts all WhatsApp messages containing links."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def serialize(self, record: dict) -> None:
def serialize(self, record: dict) -> Union[dict, list]:
text = record["data"].replace("\n", "\\n")
return {
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": f"whatsapp_msg_{record['direction']}",
"data": f"\"{text}\""
"data": f'"{text}"',
}
def check_indicators(self) -> None:
@@ -56,9 +66,11 @@ class Whatsapp(AndroidExtraction):
"""
conn = sqlite3.connect(db_path)
cur = conn.cursor()
cur.execute("""
cur.execute(
"""
SELECT * FROM messages;
""")
"""
)
names = [description[0] for description in cur.description]
messages = []
@@ -70,23 +82,31 @@ class Whatsapp(AndroidExtraction):
if not message["data"]:
continue
message["direction"] = ("send" if message["key_from_me"] == 1 else "received")
message["isodate"] = convert_timestamp_to_iso(message["timestamp"])
message["direction"] = "send" if message["key_from_me"] == 1 else "received"
message["isodate"] = convert_unix_to_iso(message["timestamp"])
# If we find links in the messages or if they are empty we add them to the list.
# If we find links in the messages or if they are empty we add them
# to the list.
if check_for_links(message["data"]) or message["data"].strip() == "":
if (message.get('thumb_image') is not None):
message['thumb_image'] = base64.b64encode(message['thumb_image'])
if message.get("thumb_image"):
message["thumb_image"] = base64.b64encode(message["thumb_image"])
messages.append(message)
cur.close()
conn.close()
log.info("Extracted a total of %d WhatsApp messages containing links", len(messages))
self.log.info(
"Extracted a total of %d WhatsApp messages containing links", len(messages)
)
self.results = messages
def run(self) -> None:
self._adb_connect()
try:
self._adb_process_file(os.path.join("/", WHATSAPP_PATH), self._parse_db)
except Exception as e:
self.log.error(e)
except Exception as exc:
self.log.error(exc)
self._adb_disconnect()

View File

@@ -0,0 +1,26 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 Claudio Guarnieri.
# 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_accessibility import DumpsysAccessibility
from .dumpsys_activities import DumpsysActivities
from .dumpsys_appops import DumpsysAppops
from .dumpsys_packages import DumpsysPackages
from .dumpsys_receivers import DumpsysReceivers
from .getprop import Getprop
from .processes import Processes
from .settings import Settings
from .sms import SMS
ANDROIDQF_MODULES = [
DumpsysActivities,
DumpsysReceivers,
DumpsysAccessibility,
DumpsysAppops,
Processes,
Getprop,
Settings,
SMS,
DumpsysPackages,
]

View File

@@ -0,0 +1,43 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 Claudio Guarnieri.
# 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 logging
import os
from typing import Any, Dict, List, Optional, Union
from mvt.common.module import MVTModule
class AndroidQFModule(MVTModule):
"""This class provides a base for all Android Data analysis modules."""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Union[List[Dict[str, Any]], Dict[str, Any], None] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self._path = target_path
self._files = []
for root, dirs, files in os.walk(target_path):
for name in files:
self._files.append(os.path.join(root, name))
def _get_files_by_pattern(self, pattern):
return fnmatch.filter(self._files, pattern)

View File

@@ -0,0 +1,77 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
from mvt.android.parsers import parse_dumpsys_accessibility
from .base import AndroidQFModule
class DumpsysAccessibility(AndroidQFModule):
"""This module analyse dumpsys accessbility"""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def check_indicators(self) -> None:
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)
def run(self) -> None:
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
if not dumpsys_file:
return
lines = []
in_accessibility = False
with open(dumpsys_file[0]) as handle:
for line in handle:
if line.strip().startswith("DUMP OF SERVICE accessibility:"):
in_accessibility = True
continue
if not in_accessibility:
continue
if line.strip().startswith(
"-------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
break
lines.append(line.rstrip())
self.results = parse_dumpsys_accessibility("\n".join(lines))
for result in self.results:
self.log.info(
'Found installed accessibility service "%s"', result.get("service")
)
self.log.info(
"Identified a total of %d accessibility services", len(self.results)
)

View File

@@ -0,0 +1,73 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
from mvt.android.parsers import parse_dumpsys_activity_resolver_table
from .base import AndroidQFModule
class DumpsysActivities(AndroidQFModule):
"""This module extracts details on receivers for risky activities."""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self.results = results if results else {}
def check_indicators(self) -> None:
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})
def run(self) -> None:
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
if not dumpsys_file:
return
lines = []
in_package = False
with open(dumpsys_file[0]) as handle:
for line in handle:
if line.strip() == "DUMP OF SERVICE package:":
in_package = True
continue
if not in_package:
continue
if line.strip().startswith(
"------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
break
lines.append(line.rstrip())
self.results = parse_dumpsys_activity_resolver_table("\n".join(lines))
self.log.info("Extracted activities for %d intents", len(self.results))

View File

@@ -0,0 +1,94 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional, Union
from mvt.android.parsers import parse_dumpsys_appops
from .base import AndroidQFModule
class DumpsysAppops(AndroidQFModule):
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def serialize(self, record: dict) -> Union[dict, list]:
records = []
for perm in record["permissions"]:
if "entries" not in perm:
continue
for entry in perm["entries"]:
if "timestamp" in entry:
records.append(
{
"timestamp": entry["timestamp"],
"module": self.__class__.__name__,
"event": entry["access"],
"data": f"{record['package_name']} access to "
f"{perm['name']} : {entry['access']}",
}
)
return records
def check_indicators(self) -> None:
for result in self.results:
if self.indicators:
ioc = self.indicators.check_app_id(result.get("package_name"))
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
for perm in result["permissions"]:
if (
perm["name"] == "REQUEST_INSTALL_PACKAGES"
and perm["access"] == "allow"
):
self.log.info(
"Package %s with REQUEST_INSTALL_PACKAGES permission",
result["package_name"],
)
def run(self) -> None:
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
if not dumpsys_file:
return
lines = []
in_package = False
with open(dumpsys_file[0]) as handle:
for line in handle:
if line.startswith("DUMP OF SERVICE appops:"):
in_package = True
continue
if in_package:
if line.startswith(
"-------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
break
lines.append(line.rstrip())
self.results = parse_dumpsys_appops("\n".join(lines))
self.log.info("Identified %d applications in AppOps Manager", len(self.results))

View File

@@ -0,0 +1,119 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Any, Dict, List, Optional, Union
from mvt.android.modules.adb.packages import (
DANGEROUS_PERMISSIONS,
DANGEROUS_PERMISSIONS_THRESHOLD,
ROOT_PACKAGES,
)
from mvt.android.parsers.dumpsys import parse_dumpsys_packages
from .base import AndroidQFModule
class DumpsysPackages(AndroidQFModule):
"""This module analyse dumpsys packages"""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[List[Dict[str, Any]]] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def serialize(self, record: dict) -> Union[dict, list]:
entries = []
for entry in ["timestamp", "first_install_time", "last_update_time"]:
if entry in record:
entries.append(
{
"timestamp": record[entry],
"module": self.__class__.__name__,
"event": entry,
"data": f"Package {record['package_name']} "
f"({record['uid']})",
}
)
return entries
def check_indicators(self) -> None:
for result in self.results:
if result["package_name"] in ROOT_PACKAGES:
self.log.warning(
"Found an installed package related to "
'rooting/jailbreaking: "%s"',
result["package_name"],
)
self.detected.append(result)
continue
if not self.indicators:
continue
ioc = self.indicators.check_app_id(result.get("package_name", ""))
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
def run(self) -> None:
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
if len(dumpsys_file) != 1:
self.log.info("Dumpsys file not found")
return
with open(dumpsys_file[0]) as handle:
data = handle.read().split("\n")
package = []
in_service = False
in_package_list = False
for line in data:
if line.strip().startswith("DUMP OF SERVICE package:"):
in_service = True
continue
if in_service and line.startswith("Packages:"):
in_package_list = True
continue
if not in_service or not in_package_list:
continue
if line.strip() == "":
break
package.append(line)
self.results = parse_dumpsys_packages("\n".join(package))
for result in self.results:
dangerous_permissions_count = 0
for perm in result["permissions"]:
if perm["name"] in DANGEROUS_PERMISSIONS:
dangerous_permissions_count += 1
if dangerous_permissions_count >= DANGEROUS_PERMISSIONS_THRESHOLD:
self.log.info(
'Found package "%s" requested %d potentially dangerous permissions',
result["package_name"],
dangerous_permissions_count,
)
self.log.info("Extracted details on %d packages", len(self.results))

View File

@@ -0,0 +1,107 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Any, Dict, List, Optional, Union
from mvt.android.modules.adb.dumpsys_receivers import (
INTENT_DATA_SMS_RECEIVED,
INTENT_NEW_OUTGOING_CALL,
INTENT_NEW_OUTGOING_SMS,
INTENT_PHONE_STATE,
INTENT_SMS_RECEIVED,
)
from mvt.android.parsers import parse_dumpsys_receiver_resolver_table
from .base import AndroidQFModule
class DumpsysReceivers(AndroidQFModule):
"""This module analyse dumpsys receivers"""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Union[List[Any], Dict[str, Any], None] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self.results = results if results else {}
def check_indicators(self) -> None:
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})
def run(self) -> None:
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
if not dumpsys_file:
return
in_receivers = False
lines = []
with open(dumpsys_file[0]) as handle:
for line in handle:
if line.strip() == "DUMP OF SERVICE package:":
in_receivers = True
continue
if not in_receivers:
continue
if line.strip().startswith(
"------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
break
lines.append(line.rstrip())
self.results = parse_dumpsys_receiver_resolver_table("\n".join(lines))
self.log.info("Extracted receivers for %d intents", len(self.results))

View File

@@ -0,0 +1,84 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 Claudio Guarnieri.
# 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 datetime import datetime, timedelta
from typing import Optional
from mvt.android.parsers.getprop import parse_getprop
from .base import AndroidQFModule
INTERESTING_PROPERTIES = [
"gsm.sim.operator.alpha",
"gsm.sim.operator.iso-country",
"persist.sys.timezone",
"ro.boot.serialno",
"ro.build.version.sdk",
"ro.build.version.security_patch",
"ro.product.cpu.abi",
"ro.product.locale",
"ro.product.vendor.manufacturer",
"ro.product.vendor.model",
"ro.product.vendor.name",
]
class Getprop(AndroidQFModule):
"""This module extracts data from get properties."""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self.results = []
def check_indicators(self) -> None:
if not self.indicators:
return
for result in self.results:
ioc = self.indicators.check_android_property_name(result.get("name", ""))
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
def run(self) -> None:
getprop_files = self._get_files_by_pattern("*/getprop.txt")
if not getprop_files:
self.log.info("getprop.txt file not found")
return
with open(getprop_files[0]) as f:
data = f.read()
self.results = parse_getprop(data)
for entry in self.results:
if entry["name"] in INTERESTING_PROPERTIES:
self.log.info("%s: %s", entry["name"], entry["value"])
if entry["name"] == "ro.build.version.security_patch":
last_patch = datetime.strptime(entry["value"], "%Y-%m-%d")
if (datetime.now() - last_patch) > timedelta(days=6 * 31):
self.log.warning(
"This phone has not received security "
"updates for more than six months "
"(last update: %s)",
entry["value"],
)
self.log.info("Extracted a total of %d properties", len(self.results))

View File

@@ -0,0 +1,99 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
from .base import AndroidQFModule
class Processes(AndroidQFModule):
"""This module analyse running processes"""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def check_indicators(self) -> None:
if not self.indicators:
return
for result in self.results:
proc_name = result.get("proc_name", "")
if not proc_name:
continue
# Skipping this process because of false positives.
if result["proc_name"] == "gatekeeperd":
continue
ioc = self.indicators.check_app_id(proc_name)
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
ioc = self.indicators.check_process(proc_name)
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
def _parse_ps(self, data):
for line in data.split("\n")[1:]:
proc = line.split()
# Sometimes WCHAN is empty.
if len(proc) == 8:
proc = proc[:5] + [""] + proc[5:]
# Sometimes there is the security label.
if proc[0].startswith("u:r"):
label = proc[0]
proc = proc[1:]
else:
label = ""
# Sometimes there is no WCHAN.
if len(proc) < 9:
proc = proc[:5] + [""] + proc[5:]
self.results.append(
{
"user": proc[0],
"pid": int(proc[1]),
"ppid": int(proc[2]),
"virtual_memory_size": int(proc[3]),
"resident_set_size": int(proc[4]),
"wchan": proc[5],
"aprocress": proc[6],
"stat": proc[7],
"proc_name": proc[8].strip("[]"),
"label": label,
}
)
def run(self) -> None:
ps_files = self._get_files_by_pattern("*/ps.txt")
if not ps_files:
return
with open(ps_files[0]) as handle:
self._parse_ps(handle.read())
self.log.info("Identified %d running processes", len(self.results))

View File

@@ -0,0 +1,67 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
from mvt.android.modules.adb.settings import ANDROID_DANGEROUS_SETTINGS
from .base import AndroidQFModule
class Settings(AndroidQFModule):
"""This module analyse setting files"""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self.results = {}
def run(self) -> None:
for setting_file in self._get_files_by_pattern("*/settings_*.txt"):
namespace = setting_file[setting_file.rfind("_") + 1 : -4]
self.results[namespace] = {}
with open(setting_file) as handle:
for line in handle:
line = line.strip()
try:
key, value = line.split("=", 1)
except ValueError:
continue
try:
self.results[namespace][key] = value
except IndexError:
continue
for danger in ANDROID_DANGEROUS_SETTINGS:
if danger["key"] == key and danger["safe_value"] != value:
self.log.warning(
'Found suspicious setting "%s = %s" (%s)',
key,
value,
danger["description"],
)
break
self.log.info(
"Identified %d settings", sum([len(val) for val in self.results.values()])
)

View File

@@ -0,0 +1,97 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 Claudio Guarnieri.
# 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 logging
from typing import Optional
from mvt.android.parsers.backup import (
AndroidBackupParsingError,
InvalidBackupPassword,
parse_ab_header,
parse_backup_file,
parse_tar_for_sms,
)
from .base import AndroidQFModule
class SMS(AndroidQFModule):
"""This module analyse SMS file in backup"""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def check_indicators(self) -> None:
if not self.indicators:
return
for message in self.results:
if "body" not in message:
continue
if self.indicators.check_domains(message.get("links", [])):
self.detected.append(message)
def parse_backup(self, data):
header = parse_ab_header(data)
if not header["backup"]:
self.log.critical("Invalid backup format, backup.ab was not analysed")
return
password = None
if header["encryption"] != "none":
password = getpass.getpass(prompt="Backup Password: ", stream=None)
try:
tardata = parse_backup_file(data, password=password)
except InvalidBackupPassword:
self.log.critical("Invalid backup password")
return
except AndroidBackupParsingError:
self.log.critical(
"Impossible to parse this backup file, please use"
" Android Backup Extractor instead"
)
return
if not tardata:
return
try:
self.results = parse_tar_for_sms(tardata)
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
def run(self) -> None:
files = self._get_files_by_pattern("*/backup.ab")
if not files:
self.log.info("No backup data found")
return
with open(files[0], "rb") as handle:
data = handle.read()
self.parse_backup(data)
self.log.info("Identified %d SMS in backup data", len(self.results))

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/

View File

@@ -1,27 +1,52 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# 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 logging
import os
from tarfile import TarFile
from typing import List, Optional
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: str, files: list) -> None:
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self.ab = None
self.backup_path = None
self.tar = None
self.files = []
def from_folder(self, backup_path: Optional[str], files: List[str]) -> None:
"""
Get all the files and list them
"""
self.backup_path = backup_path
self.files = files
def from_ab(self, file_path: str, tar: TarFile, files: list) -> None:
def from_ab(
self, file_path: Optional[str], tar: Optional[TarFile], files: List[str]
) -> None:
"""
Extract the files
"""

View File

@@ -1,21 +1,34 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
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(BackupExtraction):
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self.results = []
def check_indicators(self) -> None:
@@ -26,19 +39,24 @@ class SMS(BackupExtraction):
if "body" not in message:
continue
if self.indicators.check_domains(message["links"]):
message_links = message.get("links", [])
if message_links == []:
message_links = check_for_links(message.get("text", ""))
if self.indicators.check_domains(message_links):
self.detected.append(message)
def run(self) -> None:
for file in self._get_files_by_pattern("apps/com.android.providers.telephony/d_f/*_sms_backup"):
sms_path = "apps/com.android.providers.telephony/d_f/*_sms_backup"
for file in self._get_files_by_pattern(sms_path):
self.log.info("Processing SMS backup file at %s", file)
data = self._get_file_content(file)
self.results.extend(parse_sms_file(data))
for file in self._get_files_by_pattern("apps/com.android.providers.telephony/d_f/*_mms_backup"):
mms_path = "apps/com.android.providers.telephony/d_f/*_mms_backup"
for file in self._get_files_by_pattern(mms_path):
self.log.info("Processing MMS 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 & MMS messages containing links",
len(self.results))
self.log.info("Extracted a total of %d SMS & MMS messages", len(self.results))

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
@@ -13,5 +13,14 @@ from .getprop import Getprop
from .packages import Packages
from .receivers import Receivers
BUGREPORT_MODULES = [Accessibility, Activities, Appops, BatteryDaily,
BatteryHistory, DBInfo, Getprop, Packages, Receivers]
BUGREPORT_MODULES = [
Accessibility,
Activities,
Appops,
BatteryDaily,
BatteryHistory,
DBInfo,
Getprop,
Packages,
Receivers,
]

View File

@@ -1,26 +1,36 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
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: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def check_indicators(self) -> None:
if not self.indicators:
@@ -36,7 +46,10 @@ class Accessibility(BugReportModule):
def run(self) -> None:
content = self._get_dumpstate_file()
if not content:
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
self.log.error(
"Unable to find dumpstate file. "
"Did you provide a valid bug report archive?"
)
return
lines = []
@@ -49,13 +62,19 @@ class Accessibility(BugReportModule):
if not in_accessibility:
continue
if line.strip().startswith("------------------------------------------------------------------------------"):
if line.strip().startswith(
"------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
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(
'Found installed accessibility service "%s"', result.get("service")
)
self.log.info("Identified a total of %d accessibility services", len(self.results))
self.log.info(
"Identified a total of %d accessibility services", len(self.results)
)

View File

@@ -1,26 +1,36 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
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: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self.results = results if results else {}
@@ -39,7 +49,10 @@ class Activities(BugReportModule):
def run(self) -> None:
content = self._get_dumpstate_file()
if not content:
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
self.log.error(
"Unable to find dumpstate file. "
"Did you provide a valid bug report archive?"
)
return
lines = []
@@ -52,7 +65,9 @@ class Activities(BugReportModule):
if not in_package:
continue
if line.strip().startswith("------------------------------------------------------------------------------"):
if line.strip().startswith(
"------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
break
lines.append(line)

View File

@@ -1,28 +1,38 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional, Union
from mvt.android.parsers import parse_dumpsys_appops
from .base import BugReportModule
log = logging.getLogger(__name__)
class Appops(BugReportModule):
"""This module extracts information on package from App-Ops Manager."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def serialize(self, record: dict) -> None:
def serialize(self, record: dict) -> Union[dict, list]:
records = []
for perm in record["permissions"]:
if "entries" not in perm:
@@ -30,12 +40,15 @@ class Appops(BugReportModule):
for entry in perm["entries"]:
if "timestamp" in entry:
records.append({
"timestamp": entry["timestamp"],
"module": self.__class__.__name__,
"event": entry["access"],
"data": f"{record['package_name']} access to {perm['name']}: {entry['access']}",
})
records.append(
{
"timestamp": entry["timestamp"],
"module": self.__class__.__name__,
"event": entry["access"],
"data": f"{record['package_name']} access to "
f"{perm['name']}: {entry['access']}",
}
)
return records
@@ -49,13 +62,22 @@ class Appops(BugReportModule):
continue
for perm in result["permissions"]:
if perm["name"] == "REQUEST_INSTALL_PACKAGES" and perm["access"] == "allow":
self.log.info("Package %s with REQUEST_INSTALL_PACKAGES permission", result["package_name"])
if (
perm["name"] == "REQUEST_INSTALL_PACKAGES"
and perm["access"] == "allow"
):
self.log.info(
"Package %s with REQUEST_INSTALL_PACKAGES permission",
result["package_name"],
)
def run(self) -> None:
content = self._get_dumpstate_file()
if not content:
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
self.log.error(
"Unable to find dumpstate file. "
"Did you provide a valid bug report archive?"
)
return
lines = []
@@ -68,12 +90,15 @@ class Appops(BugReportModule):
if not in_appops:
continue
if line.strip().startswith("------------------------------------------------------------------------------"):
if line.strip().startswith(
"------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
break
lines.append(line)
self.results = parse_dumpsys_appops("\n".join(lines))
self.log.info("Identified a total of %d packages in App-Ops Manager",
len(self.results))
self.log.info(
"Identified a total of %d packages in App-Ops Manager", len(self.results)
)

View File

@@ -1,28 +1,50 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 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 typing import List, Optional
from zipfile import ZipFile
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 __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def from_folder(self, extract_path: str, extract_files: str) -> None:
self.zip_archive: Optional[ZipFile] = None
self.extract_path: Optional[str] = None
self.extract_files: List[str] = []
self.zip_files: List[str] = []
def from_folder(
self, extract_path: Optional[str], extract_files: List[str]
) -> None:
self.extract_path = extract_path
self.extract_files = extract_files
def from_zip(self, zip_archive: ZipFile, zip_files: list) -> None:
def from_zip(self, zip_archive: Optional[ZipFile], zip_files: List[str]) -> None:
self.zip_archive = zip_archive
self.zip_files = zip_files
@@ -42,6 +64,8 @@ class BugReportModule(MVTModule):
if matches:
return matches
return []
def _get_file_content(self, file_path: str) -> bytes:
if self.zip_archive:
handle = self.zip_archive.open(file_path)

View File

@@ -1,33 +1,44 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional, Union
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: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def serialize(self, record: dict) -> None:
def serialize(self, record: dict) -> Union[dict, list]:
return {
"timestamp": record["from"],
"module": self.__class__.__name__,
"event": "battery_daily",
"data": f"Recorded update of package {record['package_name']} with vers {record['vers']}"
"data": f"Recorded update of package {record['package_name']} "
f"with vers {record['vers']}",
}
def check_indicators(self) -> None:
@@ -44,7 +55,10 @@ class BatteryDaily(BugReportModule):
def run(self) -> None:
content = self._get_dumpstate_file()
if not content:
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
self.log.error(
"Unable to find dumpstate file. "
"Did you provide a valid bug report archive?"
)
return
lines = []
@@ -73,5 +87,4 @@ class BatteryDaily(BugReportModule):
self.results = parse_dumpsys_battery_daily("\n".join(lines))
self.log.info("Extracted a total of %d battery daily stats",
len(self.results))
self.log.info("Extracted a total of %d battery daily stats", len(self.results))

View File

@@ -1,26 +1,36 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
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: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def check_indicators(self) -> None:
if not self.indicators:
@@ -36,7 +46,10 @@ class BatteryHistory(BugReportModule):
def run(self) -> None:
content = self._get_dumpstate_file()
if not content:
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
self.log.error(
"Unable to find dumpstate file. "
"Did you provide a valid bug report archive?"
)
return
lines = []
@@ -57,5 +70,6 @@ class BatteryHistory(BugReportModule):
self.results = parse_dumpsys_battery_history("\n".join(lines))
self.log.info("Extracted a total of %d battery history records",
len(self.results))
self.log.info(
"Extracted a total of %d battery history records", len(self.results)
)

View File

@@ -1,28 +1,38 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
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: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def check_indicators(self) -> None:
if not self.indicators:
@@ -40,7 +50,10 @@ class DBInfo(BugReportModule):
def run(self) -> None:
content = self._get_dumpstate_file()
if not content:
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
self.log.error(
"Unable to find dumpstate file. "
"Did you provide a valid bug report archive?"
)
return
in_dbinfo = False
@@ -53,12 +66,16 @@ class DBInfo(BugReportModule):
if not in_dbinfo:
continue
if line.strip().startswith("------------------------------------------------------------------------------"):
if line.strip().startswith(
"------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
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))
self.log.info(
"Extracted a total of %d database connection pool records",
len(self.results),
)

View File

@@ -1,40 +1,54 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# 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 datetime import datetime, timedelta
from typing import Optional
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: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self.results = {} if not results else results
def run(self) -> None:
content = self._get_dumpstate_file()
if not content:
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
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) ------":
if line.strip().startswith("------ SYSTEM PROPERTIES"):
in_getprop = True
continue
@@ -49,11 +63,15 @@ class Getprop(BugReportModule):
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)
for entry in self.results:
if entry["name"] == "ro.build.version.security_patch":
security_patch = entry["value"]
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))

View File

@@ -1,54 +1,74 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# 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 typing import Optional, Union
from mvt.android.modules.adb.packages import (DANGEROUS_PERMISSIONS,
DANGEROUS_PERMISSIONS_THRESHOLD,
ROOT_PACKAGES)
from mvt.android.modules.adb.packages import (
DANGEROUS_PERMISSIONS,
DANGEROUS_PERMISSIONS_THRESHOLD,
ROOT_PACKAGES,
)
from mvt.android.parsers.dumpsys import parse_dumpsys_packages
from .base import BugReportModule
log = logging.getLogger(__name__)
class Packages(BugReportModule):
"""This module extracts details on receivers for risky activities."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def serialize(self, record: dict) -> None:
def serialize(self, record: dict) -> Union[dict, list]:
records = []
timestamps = [
{"event": "package_install", "timestamp": record["timestamp"]},
{"event": "package_first_install", "timestamp": record["first_install_time"]},
{
"event": "package_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']}",
})
for timestamp in timestamps:
records.append(
{
"timestamp": timestamp["timestamp"],
"module": self.__class__.__name__,
"event": timestamp["event"],
"data": f"Install or update of package {record['package_name']}",
}
)
return records
def check_indicators(self) -> None:
for result in self.results:
if result["package_name"] in ROOT_PACKAGES:
self.log.warning("Found an installed package related to rooting/jailbreaking: \"%s\"",
result["package_name"])
self.log.warning(
"Found an installed package related to "
'rooting/jailbreaking: "%s"',
result["package_name"],
)
self.detected.append(result)
continue
@@ -61,93 +81,13 @@ class Packages(BugReportModule):
self.detected.append(result)
continue
@staticmethod
def parse_package_for_details(output: str) -> dict:
details = {
"uid": "",
"version_name": "",
"version_code": "",
"timestamp": "",
"first_install_time": "",
"last_update_time": "",
"requested_permissions": [],
}
in_install_permissions = False
in_runtime_permissions = False
for line in output.splitlines():
if in_install_permissions:
if line.startswith(" " * 4) and not line.startswith(" " * 6):
in_install_permissions = False
continue
permission = line.strip().split(":")[0]
if permission not in details["requested_permissions"]:
details["requested_permissions"].append(permission)
if in_runtime_permissions:
if not line.startswith(" " * 8):
in_runtime_permissions = False
continue
permission = line.strip().split(":")[0]
if permission not in details["requested_permissions"]:
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() == "install permissions:":
in_install_permissions = True
elif line.strip() == "runtime permissions:":
in_runtime_permissions = True
return details
def parse_packages_list(self, output: str) -> list:
pkg_rxp = re.compile(r" Package \[(.+?)\].*")
results = []
package_name = None
package = {}
lines = []
for line in output.splitlines():
if line.startswith(" Package ["):
if len(lines) > 0:
details = self.parse_package_for_details("\n".join(lines))
package.update(details)
results.append(package)
lines = []
package = {}
matches = pkg_rxp.findall(line)
if not matches:
continue
package_name = matches[0]
package["package_name"] = package_name
continue
if not package_name:
continue
lines.append(line)
return results
def run(self) -> None:
content = self._get_dumpstate_file()
if not content:
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
self.log.error(
"Unable to find dumpstate file. "
"Did you provide a valid bug report archive?"
)
return
in_package = False
@@ -173,16 +113,19 @@ class Packages(BugReportModule):
lines.append(line)
self.results = self.parse_packages_list("\n".join(lines))
self.results = parse_dumpsys_packages("\n".join(lines))
for result in self.results:
dangerous_permissions_count = 0
for perm in result["requested_permissions"]:
if perm in DANGEROUS_PERMISSIONS:
for perm in result["permissions"]:
if perm["name"] in DANGEROUS_PERMISSIONS:
dangerous_permissions_count += 1
if dangerous_permissions_count >= DANGEROUS_PERMISSIONS_THRESHOLD:
self.log.info("Found package \"%s\" requested %d potentially dangerous permissions",
result["package_name"], dangerous_permissions_count)
self.log.info(
'Found package "%s" requested %d potentially dangerous permissions',
result["package_name"],
dangerous_permissions_count,
)
self.log.info("Extracted details on %d packages", len(self.results))

View File

@@ -1,16 +1,15 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
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"
@@ -21,12 +20,23 @@ 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: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self.results = results if results else {}
@@ -37,31 +47,45 @@ class Receivers(BugReportModule):
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"])
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"])
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"])
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"])
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"])
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
ioc = self.indicators.check_app_id(receiver["package_name"])
if ioc:
receiver["matched_indicator"] = ioc
self.detected.append({intent: receiver})
continue
def run(self) -> None:
content = self._get_dumpstate_file()
if not content:
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
self.log.error(
"Unable to find dumpstate file. "
"Did you provide a valid bug report archive?"
)
return
in_receivers = False
@@ -74,7 +98,9 @@ class Receivers(BugReportModule):
if not in_receivers:
continue
if line.strip().startswith("------------------------------------------------------------------------------"):
if line.strip().startswith(
"------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
break
lines.append(line)

View File

@@ -1,11 +1,15 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# 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_appops, parse_dumpsys_battery_daily,
parse_dumpsys_battery_history, parse_dumpsys_dbinfo,
parse_dumpsys_receiver_resolver_table)
from .dumpsys import (
parse_dumpsys_accessibility,
parse_dumpsys_activity_resolver_table,
parse_dumpsys_appops,
parse_dumpsys_battery_daily,
parse_dumpsys_battery_history,
parse_dumpsys_dbinfo,
parse_dumpsys_receiver_resolver_table,
)
from .getprop import parse_getprop

View File

@@ -1,9 +1,8 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# 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
@@ -13,7 +12,7 @@ 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
from mvt.common.utils import check_for_links, convert_unix_to_iso
PBKDF2_KEY_SIZE = 32
@@ -30,15 +29,18 @@ class InvalidBackupPassword(AndroidBackupParsingError):
pass
# TODO: Need to clean all the following code and conform it to the coding style.
def to_utf8_bytes(input_bytes):
output = []
for byte in input_bytes:
if byte < ord(b'\x80'):
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')))
output.append(ord("\xef") | (byte >> 12))
output.append(ord("\xbc") | ((byte >> 6) & ord("\x3f")))
output.append(ord("\x80") | (byte & ord("\x3f")))
return bytes(output)
@@ -49,36 +51,43 @@ def parse_ab_header(data):
'encryption': "none", 'version': 4}
"""
if data.startswith(b"ANDROID BACKUP"):
[magic_header, version, is_compressed, encryption, tar_data] = data.split(b"\n", 4)
[_, version, is_compressed, encryption, _] = data.split(b"\n", 4)
return {
"backup": True,
"compression": (is_compressed == b"1"),
"version": int(version),
"encryption": encryption.decode("utf-8")
"encryption": encryption.decode("utf-8"),
}
return {
"backup": False,
"compression": None,
"version": None,
"encryption": None
}
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):
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)
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()
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)
@@ -90,8 +99,8 @@ def decrypt_master_key(password, user_salt, user_iv, pbkdf2_rounds, master_key_b
master_key_checksum_length = ord(key_blob.read(1))
master_key_checksum = key_blob.read(master_key_checksum_length)
except TypeError:
raise InvalidBackupPassword()
except TypeError as exc:
raise InvalidBackupPassword() from exc
# Handle quirky encoding of master key bytes in Android original Java crypto code.
if format_version > 1:
@@ -100,7 +109,9 @@ def decrypt_master_key(password, user_salt, user_iv, pbkdf2_rounds, master_key_b
hmac_mk = master_key
# Derive checksum to confirm successful backup decryption.
kdf = PBKDF2HMAC(algorithm=hashes.SHA1(), length=32, salt=checksum_salt, iterations=pbkdf2_rounds)
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:
@@ -120,7 +131,15 @@ def decrypt_backup_data(encrypted_backup, password, encryption_algo, format_vers
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,
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)
@@ -128,9 +147,15 @@ def decrypt_backup_data(encrypted_backup, password, encryption_algo, format_vers
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)
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))
@@ -149,18 +174,23 @@ def parse_backup_file(data, password=None):
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, 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)
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")
except zlib.error as exc:
raise AndroidBackupParsingError(
"Impossible to decompress the backup file"
) from exc
return tar_data
@@ -171,13 +201,16 @@ def parse_tar_for_sms(data):
Returns an array of SMS
"""
dbytes = io.BytesIO(data)
tar = tarfile.open(fileobj=dbytes)
res = []
for member in tar.getmembers():
if member.name.startswith("apps/com.android.providers.telephony/d_f/") and \
(member.name.endswith("_sms_backup") or member.name.endswith("_mms_backup")):
dhandler = tar.extractfile(member)
res.extend(parse_sms_file(dhandler.read()))
with tarfile.open(fileobj=dbytes) as tar:
for member in tar.getmembers():
if member.name.startswith("apps/com.android.providers.telephony/d_f/") and (
member.name.endswith("_sms_backup")
or member.name.endswith("_mms_backup")
):
dhandler = tar.extractfile(member)
res.extend(parse_sms_file(dhandler.read()))
return res
@@ -192,20 +225,19 @@ def parse_sms_file(data):
json_data = json.loads(data)
for entry in json_data:
# Adapt MMS format to SMS format
# Adapt MMS format to SMS format.
if "mms_body" in entry:
entry["body"] = entry["mms_body"]
entry.pop("mms_body")
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")
entry["isodate"] = convert_unix_to_iso(int(entry["date"]) / 1000)
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.
# Extract links from the body
if message_links or entry["body"].strip() == "":
entry["links"] = message_links
res.append(entry)
res.append(entry)
return res

View File

@@ -1,15 +1,16 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import re
from datetime import datetime
from typing import Any, Dict, List
from mvt.common.utils import convert_timestamp_to_iso
from mvt.common.utils import convert_datetime_to_iso
def parse_dumpsys_accessibility(output: str) -> list:
def parse_dumpsys_accessibility(output: str) -> List[Dict[str, str]]:
results = []
in_services = False
@@ -26,15 +27,17 @@ def parse_dumpsys_accessibility(output: str) -> list:
service = line.split(":")[1].strip()
results.append({
"package_name": service.split("/")[0],
"service": service,
})
results.append(
{
"package_name": service.split("/")[0],
"service": service,
}
)
return results
def parse_dumpsys_activity_resolver_table(output: str) -> dict:
def parse_dumpsys_activity_resolver_table(output: str) -> Dict[str, Any]:
results = {}
in_activity_resolver_table = False
@@ -82,10 +85,12 @@ def parse_dumpsys_activity_resolver_table(output: str) -> dict:
activity = line.strip().split(" ")[1]
package_name = activity.split("/")[0]
results[intent].append({
"package_name": package_name,
"activity": activity,
})
results[intent].append(
{
"package_name": package_name,
"activity": activity,
}
)
return results
@@ -122,13 +127,15 @@ def parse_dumpsys_battery_daily(output: str) -> list:
break
if not already_seen:
daily_updates.append({
"action": "update",
"from": daily["from"],
"to": daily["to"],
"package_name": package_name,
"vers": vers_nr,
})
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)
@@ -136,7 +143,7 @@ def parse_dumpsys_battery_daily(output: str) -> list:
return results
def parse_dumpsys_battery_history(output: str) -> list:
def parse_dumpsys_battery_history(output: str) -> List[Dict[str, Any]]:
results = []
for line in output.splitlines():
@@ -151,41 +158,60 @@ def parse_dumpsys_battery_history(output: str) -> list:
event = ""
if line.find("+job") > 0:
event = "start_job"
uid = line[line.find("+job")+5:line.find(":")]
service = line[line.find(":")+1:].strip('"')
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('"')
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(":")]
uid = line[line.find("+running +wake_lock=") + 21 : line.find(":")]
event = "wake"
service = line[line.find("*walarm*:")+9:].split(" ")[0].strip('"').strip()
service = (
line[line.find("*walarm*:") + 9 :].split(" ")[0].strip('"').strip()
)
if service == "" or "/" not in service:
continue
package_name = service.split("/")[0]
elif (line.find("+top=") > 0) or (line.find("-top") > 0):
if line.find("+top=") > 0:
event = "start_top"
top_pos = line.find("+top=")
else:
event = "end_top"
top_pos = line.find("-top=")
colon_pos = top_pos + line[top_pos:].find(":")
uid = line[top_pos + 5 : colon_pos]
service = ""
package_name = line[colon_pos + 1 :].strip('"')
else:
continue
results.append({
"time_elapsed": time_elapsed,
"event": event,
"uid": uid,
"package_name": package_name,
"service": service,
})
results.append(
{
"time_elapsed": time_elapsed,
"event": event,
"uid": uid,
"package_name": package_name,
"service": service,
}
)
return results
def parse_dumpsys_dbinfo(output: str) -> list:
def parse_dumpsys_dbinfo(output: str) -> List[Dict[str, Any]]:
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\=\"(.+?)\"')
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\=\"(.+?)\""
) # pylint: disable=line-too-long
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\=\"(.+?)\""
) # pylint: disable=line-too-long
pool = None
in_operations = False
@@ -213,28 +239,32 @@ def parse_dumpsys_dbinfo(output: str) -> list:
matches = rxp_no_pid.findall(line)
if not matches:
continue
else:
match = matches[0]
results.append({
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,
})
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: str) -> dict:
def parse_dumpsys_receiver_resolver_table(output: str) -> Dict[str, Any]:
results = {}
in_receiver_resolver_table = False
@@ -282,15 +312,17 @@ def parse_dumpsys_receiver_resolver_table(output: str) -> dict:
receiver = line.strip().split(" ")[1]
package_name = receiver.split("/")[0]
results[intent].append({
"package_name": package_name,
"receiver": receiver,
})
results[intent].append(
{
"package_name": package_name,
"receiver": receiver,
}
)
return results
def parse_dumpsys_appops(output: str) -> list:
def parse_dumpsys_appops(output: str) -> List[Dict[str, Any]]:
results = []
perm = {}
package = {}
@@ -307,6 +339,17 @@ def parse_dumpsys_appops(output: str) -> list:
if line.startswith(" Uid "):
uid = line[6:-1]
if entry:
perm["entries"].append(entry)
entry = {}
if package:
if perm:
package["permissions"].append(perm)
perm = {}
results.append(package)
package = {}
continue
if line.startswith(" Package "):
@@ -328,7 +371,7 @@ def parse_dumpsys_appops(output: str) -> list:
}
continue
if line.startswith(" ") and line[6] != " ":
if package and line.startswith(" ") and line[6] != " ":
if entry:
perm["entries"].append(entry)
entry = {}
@@ -351,13 +394,15 @@ def parse_dumpsys_appops(output: str) -> list:
entry = {}
entry["access"] = line.split(":")[0].strip()
entry["type"] = line[line.find("[")+1:line.find("]")]
entry["type"] = line[line.find("[") + 1 : line.find("]")]
try:
entry["timestamp"] = convert_timestamp_to_iso(
entry["timestamp"] = convert_datetime_to_iso(
datetime.strptime(
line[line.find("]")+1:line.find("(")].strip(),
"%Y-%m-%d %H:%M:%S.%f"))
line[line.find("]") + 1 : line.find("(")].strip(),
"%Y-%m-%d %H:%M:%S.%f",
)
)
except ValueError:
# Invalid date format
pass
@@ -373,3 +418,127 @@ def parse_dumpsys_appops(output: str) -> list:
results.append(package)
return results
def parse_dumpsys_package_for_details(output: str) -> Dict[str, Any]:
"""
Parse one entry of a dumpsys package information
"""
details = {
"uid": "",
"version_name": "",
"version_code": "",
"timestamp": "",
"first_install_time": "",
"last_update_time": "",
"permissions": [],
"requested_permissions": [],
}
in_install_permissions = False
in_runtime_permissions = False
in_declared_permissions = False
in_requested_permissions = True
for line in output.splitlines():
if in_install_permissions:
if line.startswith(" " * 4) and not line.startswith(" " * 6):
in_install_permissions = False
else:
lineinfo = line.strip().split(":")
permission = lineinfo[0]
granted = None
if "granted=" in lineinfo[1]:
granted = "granted=true" in lineinfo[1]
details["permissions"].append(
{"name": permission, "granted": granted, "type": "install"}
)
if in_runtime_permissions:
if not line.startswith(" " * 8):
in_runtime_permissions = False
else:
lineinfo = line.strip().split(":")
permission = lineinfo[0]
granted = None
if "granted=" in lineinfo[1]:
granted = "granted=true" in lineinfo[1]
details["permissions"].append(
{"name": permission, "granted": granted, "type": "runtime"}
)
if in_declared_permissions:
if not line.startswith(" " * 6):
in_declared_permissions = False
else:
permission = line.strip().split(":")[0]
details["permissions"].append({"name": permission, "type": "declared"})
if in_requested_permissions:
if not line.startswith(" " * 6):
in_requested_permissions = False
else:
details["requested_permissions"].append(line.strip())
if line.strip().startswith("userId="):
details["uid"] = line.split("=")[1].strip()
elif line.strip().startswith("versionName="):
details["version_name"] = line.split("=")[1].strip()
elif line.strip().startswith("versionCode="):
details["version_code"] = line.split("=", 1)[1].strip()
elif line.strip().startswith("timeStamp="):
details["timestamp"] = line.split("=")[1].strip()
elif line.strip().startswith("firstInstallTime="):
details["first_install_time"] = line.split("=")[1].strip()
elif line.strip().startswith("lastUpdateTime="):
details["last_update_time"] = line.split("=")[1].strip()
elif line.strip() == "install permissions:":
in_install_permissions = True
elif line.strip() == "runtime permissions:":
in_runtime_permissions = True
elif line.strip() == "declared permissions:":
in_declared_permissions = True
elif line.strip() == "requested permissions:":
in_requested_permissions = True
return details
def parse_dumpsys_packages(output: str) -> List[Dict[str, Any]]:
"""
Parse the dumpsys package service data
"""
pkg_rxp = re.compile(r" Package \[(.+?)\].*")
results = []
package_name = None
package = {}
lines = []
for line in output.splitlines():
if line.startswith(" Package ["):
if len(lines) > 0:
details = parse_dumpsys_package_for_details("\n".join(lines))
package.update(details)
results.append(package)
lines = []
package = {}
matches = pkg_rxp.findall(line)
if not matches:
continue
package_name = matches[0]
package["package_name"] = package_name
continue
if not package_name:
continue
lines.append(line)
if len(lines) > 0:
details = parse_dumpsys_package_for_details("\n".join(lines))
package.update(details)
results.append(package)
return results

View File

@@ -1,13 +1,14 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import re
from typing import Dict, List
def parse_getprop(output: str) -> dict:
results = {}
def parse_getprop(output: str) -> List[Dict[str, str]]:
results = []
rxp = re.compile(r"\[(.+?)\]: \[(.+?)\]")
for line in output.splitlines():
@@ -19,8 +20,7 @@ def parse_getprop(output: str) -> dict:
if not matches or len(matches[0]) != 2:
continue
key = matches[0][0]
value = matches[0][1]
results[key] = value
entry = {"name": matches[0][0], "value": matches[0][1]}
results.append(entry)
return results

View File

@@ -1,4 +1,4 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/

View File

@@ -1,29 +1,42 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# 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 typing import Optional
from mvt.common.command import Command
from mvt.common.utils import exec_or_profile
log = logging.getLogger(__name__)
class CmdCheckIOCS(Command):
def __init__(
self,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
ioc_files: Optional[list] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
module_options: Optional[dict] = None,
) -> None:
super().__init__(
target_path=target_path,
results_path=results_path,
ioc_files=ioc_files,
module_name=module_name,
serial=serial,
module_options=module_options,
log=log,
)
name = "check-iocs"
modules = []
def __init__(self, target_path: str = None, results_path: str = None,
ioc_files: list = [], module_name: str = None, serial: str = None,
fast_mode: bool = False):
super().__init__(target_path=target_path, results_path=results_path,
ioc_files=ioc_files, module_name=module_name,
serial=serial, fast_mode=fast_mode, log=log)
self.name = "check-iocs"
def run(self) -> None:
assert self.target_path is not None
all_modules = []
for entry in self.modules:
if entry not in all_modules:
@@ -33,7 +46,7 @@ class CmdCheckIOCS(Command):
total_detections = 0
for file_name in os.listdir(self.target_path):
name_only, ext = os.path.splitext(file_name)
name_only, _ = os.path.splitext(file_name)
file_path = os.path.join(self.target_path, file_name)
for iocs_module in all_modules:
@@ -43,22 +56,27 @@ class CmdCheckIOCS(Command):
if iocs_module().get_slug() != name_only:
continue
log.info("Loading results from \"%s\" with module %s", file_name,
iocs_module.__name__)
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__))
m = iocs_module.from_json(
file_path, log=logging.getLogger(iocs_module.__module__)
)
if self.iocs.total_ioc_count > 0:
m.indicators = self.iocs
m.indicators.log = m.log
try:
m.check_indicators()
exec_or_profile("m.check_indicators()", globals(), locals())
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)
log.warning(
"The check of the results produced %d detections!", total_detections
)

View File

@@ -1,74 +1,106 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import hashlib
import json
import logging
import os
import sys
from datetime import datetime
from typing import Callable
from typing import Optional
from mvt.common.indicators import Indicators
from mvt.common.module import run_module, save_timeline
from mvt.common.utils import convert_timestamp_to_iso
from mvt.common.module import MVTModule, run_module, save_timeline
from mvt.common.utils import (
convert_datetime_to_iso,
generate_hashes_from_path,
get_sha256_from_file_path,
)
from mvt.common.version import MVT_VERSION
class Command(object):
def __init__(self, target_path: str = None, results_path: str = None,
ioc_files: list = [], module_name: str = None, serial: str = None,
fast_mode: bool = False,
log: logging.Logger = logging.getLogger(__name__)):
class Command:
def __init__(
self,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
ioc_files: Optional[list] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
module_options: Optional[dict] = None,
hashes: bool = False,
log: logging.Logger = logging.getLogger(__name__),
) -> None:
self.name = ""
self.modules = []
self.target_path = target_path
self.results_path = results_path
self.ioc_files = ioc_files
self.ioc_files = ioc_files if ioc_files else []
self.module_name = module_name
self.serial = serial
self.fast_mode = fast_mode
self.log = log
self.iocs = Indicators(log=log)
self.iocs.load_indicators_files(ioc_files)
# This dictionary can contain options that will be passed down from
# the Command to all modules. This can for example be used to pass
# down a password to decrypt a backup or flags which are need by some modules.
self.module_options = module_options if module_options else {}
# This list will contain all executed modules.
# We can use this to reference e.g. self.executed[0].results.
self.executed = []
self.detected_count = 0
self.hashes = hashes
self.hash_values = []
self.timeline = []
self.timeline_detected = []
# Load IOCs
self._create_storage()
self._setup_logging()
self.iocs = Indicators(log=log)
self.iocs.load_indicators_files(self.ioc_files)
def _create_storage(self) -> None:
if self.results_path and not os.path.exists(self.results_path):
try:
os.makedirs(self.results_path)
except Exception as e:
self.log.critical("Unable to create output folder %s: %s",
self.results_path, e)
except Exception as exc:
self.log.critical(
"Unable to create output folder %s: %s", self.results_path, exc
)
sys.exit(1)
def _add_log_file_handler(self, logger: logging.Logger) -> None:
def _setup_logging(self):
if not self.results_path:
return
fh = logging.FileHandler(os.path.join(self.results_path, "command.log"))
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
fh.setLevel(logging.DEBUG)
fh.setFormatter(formatter)
logger.addHandler(fh)
logger = logging.getLogger("mvt")
file_handler = logging.FileHandler(
os.path.join(self.results_path, "command.log")
)
formatter = logging.Formatter(
"%(asctime)s - %(name)s - " "%(levelname)s - %(message)s"
)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
def _store_timeline(self) -> None:
if not self.results_path:
return
if len(self.timeline) > 0:
save_timeline(self.timeline,
os.path.join(self.results_path, "timeline.csv"))
save_timeline(
self.timeline, os.path.join(self.results_path, "timeline.csv")
)
if len(self.timeline_detected) > 0:
save_timeline(self.timeline_detected,
os.path.join(self.results_path, "timeline_detected.csv"))
save_timeline(
self.timeline_detected,
os.path.join(self.results_path, "timeline_detected.csv"),
)
def _store_info(self) -> None:
if not self.results_path:
@@ -81,50 +113,39 @@ class Command(object):
info = {
"target_path": target_path,
"mvt_version": MVT_VERSION,
"date": convert_timestamp_to_iso(datetime.now()),
"date": convert_datetime_to_iso(datetime.now()),
"ioc_files": [],
"hashes": [],
}
for coll in self.iocs.ioc_collections:
info["ioc_files"].append(coll.get("stix2_file_path", ""))
ioc_file_path = coll.get("stix2_file_path", "")
if ioc_file_path and ioc_file_path not in info["ioc_files"]:
info["ioc_files"].append(ioc_file_path)
# TODO: Revisit if setting this from environment variable is good
# enough.
if self.target_path and os.environ.get("MVT_HASH_FILES"):
if os.path.isfile(self.target_path):
h = hashlib.sha256()
with open(self.target_path, "rb") as handle:
h.update(handle.read())
if self.target_path and (os.environ.get("MVT_HASH_FILES") or self.hashes):
self.generate_hashes()
info["hashes"].append({
"file_path": self.target_path,
"sha256": h.hexdigest(),
})
elif os.path.isdir(self.target_path):
for (root, dirs, files) in os.walk(self.target_path):
for file in files:
file_path = os.path.join(root, file)
h = hashlib.sha256()
info["hashes"] = self.hash_values
try:
with open(file_path, "rb") as handle:
h.update(handle.read())
except FileNotFoundError:
self.log.error("Failed to hash the file %s: might be a symlink", file_path)
continue
except PermissionError:
self.log.error("Failed to hash the file %s: permission denied", file_path)
continue
info["hashes"].append({
"file_path": file_path,
"sha256": h.hexdigest(),
})
with open(os.path.join(self.results_path, "info.json"), "w+") as handle:
info_path = os.path.join(self.results_path, "info.json")
with open(info_path, "w+", encoding="utf-8") as handle:
json.dump(info, handle, indent=4)
if self.target_path and (os.environ.get("MVT_HASH_FILES") or self.hashes):
info_hash = get_sha256_from_file_path(info_path)
self.log.info('Reference hash of the info.json file: "%s"', info_hash)
def generate_hashes(self) -> None:
"""
Compute hashes for files in the target_path
"""
if not self.target_path:
return
for file in generate_hashes_from_path(self.target_path, self.log):
self.hash_values.append(file)
def list_modules(self) -> None:
self.log.info("Following is the list of available %s modules:", self.name)
for module in self.modules:
@@ -133,16 +154,13 @@ class Command(object):
def init(self) -> None:
raise NotImplementedError
def module_init(self, module: Callable) -> None:
def module_init(self, module: MVTModule) -> None:
raise NotImplementedError
def finish(self) -> None:
raise NotImplementedError
def run(self) -> None:
self._create_storage()
self._add_log_file_handler(self.log)
try:
self.init()
except NotImplementedError:
@@ -152,13 +170,15 @@ class Command(object):
if self.module_name and module.__name__ != self.module_name:
continue
# FIXME: do we need the logger here
module_logger = logging.getLogger(module.__module__)
self._add_log_file_handler(module_logger)
m = module(target_path=self.target_path,
results_path=self.results_path,
fast_mode=self.fast_mode,
log=module_logger)
m = module(
target_path=self.target_path,
results_path=self.results_path,
module_options=self.module_options,
log=module_logger,
)
if self.iocs.total_ioc_count:
m.indicators = self.iocs
@@ -174,13 +194,17 @@ class Command(object):
run_module(m)
self.executed.append(m)
self.detected_count += len(m.detected)
self.timeline.extend(m.timeline)
self.timeline_detected.extend(m.timeline_detected)
self._store_timeline()
self._store_info()
try:
self.finish()
except NotImplementedError:
pass
self._store_timeline()
self._store_info()

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
@@ -9,6 +9,8 @@ HELP_MSG_IOC = "Path to indicators file (can be invoked multiple time)"
HELP_MSG_FAST = "Avoid running time/resource consuming features"
HELP_MSG_LIST_MODULES = "Print list of available modules and exit"
HELP_MSG_MODULE = "Name of a single module you would like to run instead of all"
HELP_MSG_HASHES = "Generate hashes of all the files analyzed"
HELP_MSG_VERBOSE = "Verbose mode"
# Android-specific.
HELP_MSG_SERIAL = "Specify a device serial number or HOST:PORT connection string"

View File

@@ -1,12 +1,15 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# 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 logging
import os
from functools import lru_cache
from typing import Any, Dict, Iterator, List, Optional, Union
import ahocorasick
from appdirs import user_data_dir
from .url import URL
@@ -14,15 +17,17 @@ from .url import URL
MVT_DATA_FOLDER = user_data_dir("mvt")
MVT_INDICATORS_FOLDER = os.path.join(MVT_DATA_FOLDER, "indicators")
logger = logging.getLogger(__name__)
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=logging.Logger) -> None:
def __init__(self, log=logger) -> None:
self.log = log
self.ioc_collections = []
self.ioc_collections: List[Dict[str, Any]] = []
self.total_ioc_count = 0
def _load_downloaded_indicators(self) -> None:
@@ -45,11 +50,18 @@ class Indicators:
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)
self.log.error(
"Path specified with env MVT_STIX2 is not a valid file: %s", path
)
def _new_collection(self, cid: str = "", name: str = "", description: str = "",
file_name: str = "", file_path: str = "") -> dict:
def _new_collection(
self,
cid: Optional[str] = None,
name: Optional[str] = None,
description: Optional[str] = None,
file_name: Optional[str] = None,
file_path: Optional[str] = None,
) -> dict:
return {
"id": cid,
"name": name,
@@ -64,6 +76,7 @@ class Indicators:
"files_sha256": [],
"app_ids": [],
"ios_profile_ids": [],
"android_property_names": [],
"count": 0,
}
@@ -74,6 +87,57 @@ class Indicators:
ioc_coll["count"] += 1
self.total_ioc_count += 1
def _process_indicator(self, indicator: dict, collection: dict) -> None:
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"],
)
elif key == "android-property:name":
self._add_indicator(
ioc=value,
ioc_coll=collection,
ioc_coll_list=collection["android_property_names"],
)
def parse_stix2(self, file_path: str) -> None:
"""Extract indicators from a STIX2 file.
@@ -87,8 +151,10 @@ class Indicators:
try:
data = json.load(handle)
except json.decoder.JSONDecodeError:
self.log.critical("Unable to parse STIX2 indicator file. "
"The file is corrupted 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 = {}
@@ -108,10 +174,13 @@ class Indicators:
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)
collection = self._new_collection(
mal_id,
mal_values.get("name"),
mal_values.get("description"),
os.path.basename(file_path),
file_path,
)
collections.append(collection)
# We loop through all indicators.
@@ -132,55 +201,22 @@ class Indicators:
# 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
if collection["id"] == malware_id:
self._process_indicator(indicator, collection)
break
for coll in collections:
self.log.info("Extracted %d indicators for collection with name \"%s\"",
coll["count"], coll["name"])
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: list, load_default: bool = True) -> None:
def load_indicators_files(
self, files: list, load_default: Optional[bool] = True
) -> None:
"""
Load a list of indicators files.
"""
@@ -188,8 +224,7 @@ class Indicators:
if os.path.isfile(file_path):
self.parse_stix2(file_path)
else:
self.log.warning("No indicators file exists at path %s",
file_path)
self.log.warning("No indicators file exists at path %s", file_path)
# Load downloaded indicators and any indicators from env variable.
if load_default:
@@ -198,7 +233,7 @@ class 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: str) -> dict:
def get_iocs(self, ioc_type: str) -> Iterator[Dict[str, Any]]:
for ioc_collection in self.ioc_collections:
for ioc in ioc_collection.get(ioc_type, []):
yield {
@@ -208,7 +243,41 @@ class Indicators:
"stix2_file_name": ioc_collection["stix2_file_name"],
}
def check_domain(self, url: str) -> dict:
@lru_cache()
def get_ioc_matcher(
self, ioc_type: Optional[str] = None, ioc_list: Optional[list] = None
) -> ahocorasick.Automaton:
"""
Build an Aho-Corasick automaton from a list of iocs (i.e indicators)
Returns an Aho-Corasick automaton
This data-structue and algorithim allows for fast matching of a large number
of match strings (i.e IOCs) against a large body of text. This will also
match strings containing the IOC, so it is important to confirm the
match is a valid IOC before using it.
for _, ioc in domains_automaton.iter(url.domain.lower()):
if ioc.value == url.domain.lower():
print(ioc)
We use an LRU cache to avoid rebuilding the automaton every time we call a
function such as check_domain().
"""
automaton = ahocorasick.Automaton()
if ioc_type:
iocs = self.get_iocs(ioc_type)
elif ioc_list:
iocs = ioc_list
else:
raise ValueError("Must provide either ioc_tyxpe or ioc_list")
for ioc in iocs:
automaton.add_word(ioc["value"], ioc)
automaton.make_automaton()
return automaton
@lru_cache()
def check_domain(self, url: str) -> Union[dict, None]:
"""Check if a given URL matches any of the provided domain indicators.
:param url: URL to match against domain indicators
@@ -216,10 +285,13 @@ class Indicators:
:returns: Indicator details if matched, otherwise None
"""
# TODO: If the IOC domain contains a subdomain, it is not currently
# being matched.
if not url:
return None
if not isinstance(url, str):
return None
# Create an Aho-Corasick automaton from the list of domains
domain_matcher = self.get_ioc_matcher("domains")
try:
# First we use the provided URL.
@@ -230,14 +302,20 @@ class Indicators:
# HTTP HEAD request.
unshortened = orig_url.unshorten()
# self.log.info("Found a shortened URL %s -> %s",
# url, unshortened)
self.log.debug("Found a shortened URL %s -> %s", url, unshortened)
if unshortened is None:
self.log.warning("Unable to unshorten URL %s", url)
return None
# Now we check for any nested URL shorteners.
dest_url = URL(unshortened)
if dest_url.check_if_shortened():
# self.log.info("Original URL %s appears to shorten another shortened URL %s ... checking!",
# orig_url.url, dest_url.url)
self.log.debug(
"Original URL %s appears to shorten another "
"shortened URL %s ... checking!",
orig_url.url,
dest_url.url,
)
return self.check_domain(dest_url.url)
final_url = dest_url
@@ -247,40 +325,71 @@ class Indicators:
except Exception:
# If URL parsing failed, we just try to do a simple substring
# match.
for ioc in self.get_iocs("domains"):
for idx, ioc in domain_matcher.iter(url):
if ioc["value"].lower() in url:
self.log.warning("Maybe found a known suspicious domain %s matching indicators from \"%s\"",
url, ioc["name"])
self.log.warning(
"Maybe found a known suspicious domain %s "
'matching indicator "%s" from "%s"',
url,
ioc["value"],
ioc["name"],
)
return ioc
# If nothing matched, we can quit here.
return None
# If all parsing worked, we start walking through available domain indicators.
for ioc in self.get_iocs("domains"):
# If all parsing worked, we start walking through available domain
# indicators.
for idx, ioc in domain_matcher.iter(final_url.domain.lower()):
# First we check the full domain.
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 matching indicators from \"%s\"",
final_url.url, orig_url.url, ioc["name"])
self.log.warning(
"Found a known suspicious domain %s "
'shortened as %s matching indicator "%s" from "%s"',
final_url.url,
orig_url.url,
ioc["value"],
ioc["name"],
)
else:
self.log.warning("Found a known suspicious domain %s matching indicators from \"%s\"",
final_url.url, ioc["name"])
self.log.warning(
"Found a known suspicious domain %s "
'matching indicator "%s" from "%s"',
final_url.url,
ioc["value"],
ioc["name"],
)
return ioc
# Then we just check the top level domain.
# Then we just check the top level domain.
for idx, ioc in domain_matcher.iter(final_url.top_level.lower()):
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 with suspicious top level %s shortened as %s matching indicators from \"%s\"",
final_url.url, orig_url.url, ioc["name"])
self.log.warning(
"Found a sub-domain with suspicious top "
"level %s shortened as %s matching "
'indicator "%s" from "%s"',
final_url.url,
orig_url.url,
ioc["value"],
ioc["name"],
)
else:
self.log.warning("Found a sub-domain with a suspicious top level %s matching indicators from \"%s\"",
final_url.url, ioc["name"])
self.log.warning(
"Found a sub-domain with a suspicious top "
'level %s matching indicator "%s" from "%s"',
final_url.url,
ioc["value"],
ioc["name"],
)
return ioc
def check_domains(self, urls: list) -> dict:
return None
def check_domains(self, urls: list) -> Union[dict, None]:
"""Check a list of URLs against the provided list of domain indicators.
:param urls: List of URLs to check against domain indicators
@@ -296,7 +405,9 @@ class Indicators:
if check:
return check
def check_process(self, process: str) -> dict:
return None
def check_process(self, process: str) -> Union[dict, None]:
"""Check the provided process name against the list of process
indicators.
@@ -311,17 +422,27 @@ class Indicators:
proc_name = os.path.basename(process)
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"])
self.log.warning(
'Found a known suspicious process name "%s" '
'matching indicators from "%s"',
process,
ioc["name"],
)
return ioc
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"])
self.log.warning(
"Found a truncated known suspicious "
'process name "%s" matching indicators from "%s"',
process,
ioc["name"],
)
return ioc
def check_processes(self, processes: list) -> dict:
return None
def check_processes(self, processes: list) -> Union[dict, None]:
"""Check the provided list of processes against the list of
process indicators.
@@ -338,7 +459,9 @@ class Indicators:
if check:
return check
def check_email(self, email: str) -> dict:
return None
def check_email(self, email: str) -> Union[dict, None]:
"""Check the provided email against the list of email indicators.
:param email: Email address to check against email indicators
@@ -351,11 +474,17 @@ class Indicators:
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"])
self.log.warning(
'Found a known suspicious email address "%s" '
'matching indicators from "%s"',
email,
ioc["name"],
)
return ioc
def check_file_name(self, file_name: str) -> dict:
return None
def check_file_name(self, file_name: str) -> Union[dict, None]:
"""Check the provided file name against the list of file indicators.
:param file_name: File name to check against file
@@ -369,12 +498,19 @@ class Indicators:
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"])
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: str) -> dict:
"""Check the provided file path against the list of file indicators (both path and name).
return None
def check_file_path(self, file_path: str) -> Union[dict, None]:
"""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
@@ -390,16 +526,51 @@ class Indicators:
return ioc
for ioc in self.get_iocs("file_paths"):
# Strip any trailing slash from indicator paths to match directories.
# Strip any trailing slash from indicator paths to match
# directories.
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"])
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: str) -> dict:
"""Check the provided configuration profile UUID against the list of indicators.
return None
:param profile_uuid: Profile UUID to check against configuration profile indicators
def check_file_path_process(self, file_path: str) -> Optional[Dict[str, Any]]:
"""Check the provided file path contains a process name from the
list of indicators
:param file_path: File path or file name to check against file
indicators
:type file_path: str
:returns: Indicator details if matched, otherwise None
"""
if not file_path:
return None
for ioc in self.get_iocs("processes"):
parts = file_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"',
file_path,
ioc["name"],
)
return ioc
return None
def check_profile(self, profile_uuid: str) -> Union[dict, None]:
"""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: Indicator details if matched, otherwise None
@@ -409,11 +580,17 @@ class Indicators:
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"])
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: str) -> dict:
return None
def check_file_hash(self, file_hash: str) -> Union[dict, None]:
"""Check the provided SHA256 file hash against the list of indicators.
:param file_hash: SHA256 hash to check
@@ -426,11 +603,17 @@ class Indicators:
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"])
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: str) -> dict:
return None
def check_app_id(self, app_id: str) -> Union[dict, None]:
"""Check the provided app identifier (typically an Android package name)
against the list of indicators.
@@ -444,6 +627,35 @@ class Indicators:
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"])
self.log.warning(
'Found a known suspicious app with ID "%s" '
'matching indicators from "%s"',
app_id,
ioc["name"],
)
return ioc
return None
def check_android_property_name(self, property_name: str) -> Optional[dict]:
"""Check the android property name against the list of indicators.
:param property_name: Name of the Android property
:type property_name: str
:returns: Indicator details if matched, otherwise None
"""
if property_name is None:
return None
for ioc in self.get_iocs("android_property_names"):
if property_name.lower() == ioc["value"].lower():
self.log.warning(
'Found a known suspicious Android property "%s" '
'matching indicators from "%s"',
property_name,
ioc["name"],
)
return ioc
return None

View File

@@ -1,9 +1,9 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from rich import print
from rich import print as rich_print
from .updates import IndicatorsUpdates, MVTUpdates
from .version import MVT_VERSION
@@ -18,7 +18,10 @@ def check_updates() -> None:
pass
else:
if latest_version:
print(f"\t\t[bold]Version {latest_version} is available! Upgrade mvt![/bold]")
rich_print(
f"\t\t[bold]Version {latest_version} is available! "
"Upgrade mvt with `pip3 install -U mvt`[/bold]"
)
# Then we check for indicators files updates.
ioc_updates = IndicatorsUpdates()
@@ -26,7 +29,10 @@ def check_updates() -> None:
# Before proceeding, we check if we have downloaded an indicators index.
# If not, there's no point in proceeding with the updates check.
if ioc_updates.get_latest_update() == 0:
print("\t\t[bold]You have not yet downloaded any indicators, check the `download-iocs` command![/bold]")
rich_print(
"\t\t[bold]You have not yet downloaded any indicators, check "
"the `download-iocs` command![/bold]"
)
return
# We only perform this check at a fixed frequency, in order to not
@@ -34,7 +40,10 @@ def check_updates() -> None:
# multiple times.
should_check, hours = ioc_updates.should_check()
if not should_check:
print(f"\t\tIndicators updates checked recently, next automatic check in {int(hours)} hours")
rich_print(
f"\t\tIndicators updates checked recently, next automatic check "
f"in {int(hours)} hours"
)
return
try:
@@ -43,17 +52,20 @@ def check_updates() -> None:
pass
else:
if ioc_to_update:
print("\t\t[bold]There are updates to your indicators files! Run the `download-iocs` command to update![/bold]")
rich_print(
"\t\t[bold]There are updates to your indicators files! "
"Run the `download-iocs` command to update![/bold]"
)
else:
print("\t\tYour indicators files seem to be up to date.")
rich_print("\t\tYour indicators files seem to be up to date.")
def logo() -> None:
print("\n")
print("\t[bold]MVT[/bold] - Mobile Verification Toolkit")
print("\t\thttps://mvt.re")
print(f"\t\tVersion: {MVT_VERSION}")
rich_print("\n")
rich_print("\t[bold]MVT[/bold] - Mobile Verification Toolkit")
rich_print("\t\thttps://mvt.re")
rich_print(f"\t\tVersion: {MVT_VERSION}")
check_updates()
print("\n")
rich_print("\n")

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
@@ -7,10 +7,12 @@ import csv
import logging
import os
import re
from typing import Callable
from typing import Any, Dict, List, Optional, Union
import simplejson as json
from .utils import exec_or_profile
class DatabaseNotFoundError(Exception):
pass
@@ -24,20 +26,27 @@ class InsufficientPrivileges(Exception):
pass
class MVTModule(object):
class MVTModule:
"""This class provides a base for all extraction modules."""
enabled = True
slug = None
slug: Optional[str] = None
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = None):
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[Dict[str, Any]] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Union[List[Dict[str, Any]], Dict[str, Any], None] = None,
) -> None:
"""Initialize module.
:param file_path: Path to the module's database file, if there is any
:type file_path: str
:param target_path: Path to the target folder (backup or filesystem dump)
:param target_path: Path to the target folder (backup or filesystem
dump)
:type file_path: str
:param results_path: Folder where results will be stored
:type results_path: str
@@ -50,21 +59,20 @@ class MVTModule(object):
self.file_path = file_path
self.target_path = target_path
self.results_path = results_path
self.fast_mode = fast_mode
self.module_options = module_options if module_options else {}
self.log = log
self.indicators = None
self.results = results if results else []
self.detected = []
self.timeline = []
self.timeline_detected = []
self.detected: List[Dict[str, Any]] = []
self.timeline: List[Dict[str, str]] = []
self.timeline_detected: List[Dict[str, str]] = []
@classmethod
def from_json(cls, json_path: str, log: logging.Logger = None):
def from_json(cls, json_path: str, log: logging.Logger):
with open(json_path, "r", encoding="utf-8") as handle:
results = json.load(handle)
if log:
log.info("Loaded %d results from \"%s\"",
len(results), json_path)
log.info('Loaded %d results from "%s"', len(results), json_path)
return cls(results=results, log=log)
def get_slug(self) -> str:
@@ -96,9 +104,13 @@ class MVTModule(object):
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:
self.log.error("Unable to store results of module %s to file %s: %s",
self.__class__.__name__, results_file_name, e)
except Exception as exc:
self.log.error(
"Unable to store results of module %s to file %s: %s",
self.__class__.__name__,
results_file_name,
exc,
)
if self.detected:
detected_file_name = f"{name}_detected.json"
@@ -106,7 +118,7 @@ class MVTModule(object):
with open(detected_json_path, "w", encoding="utf-8") as handle:
json.dump(self.detected, handle, indent=4, default=str)
def serialize(self, record: dict) -> None:
def serialize(self, record: dict) -> Union[dict, list, None]:
raise NotImplementedError
@staticmethod
@@ -126,7 +138,7 @@ class MVTModule(object):
for result in self.results:
record = self.serialize(result)
if record:
if type(record) == list:
if isinstance(record, list):
self.timeline.extend(record)
else:
self.timeline.append(record)
@@ -134,7 +146,7 @@ class MVTModule(object):
for detected in self.detected:
record = self.serialize(detected)
if record:
if type(record) == list:
if isinstance(record, list):
self.timeline_detected.extend(record)
else:
self.timeline_detected.append(record)
@@ -148,41 +160,69 @@ class MVTModule(object):
raise NotImplementedError
def run_module(module: Callable) -> None:
def run_module(module: MVTModule) -> None:
module.log.info("Running module %s...", module.__class__.__name__)
try:
module.run()
exec_or_profile("module.run()", globals(), locals())
except NotImplementedError:
module.log.exception("The run() procedure of module %s was not implemented yet!",
module.__class__.__name__)
except InsufficientPrivileges as e:
module.log.info("Insufficient privileges for module %s: %s", module.__class__.__name__, e)
except DatabaseNotFoundError as e:
module.log.info("There might be no data to extract by module %s: %s",
module.__class__.__name__, e)
except DatabaseCorruptedError as e:
module.log.error("The %s module database seems to be corrupted: %s",
module.__class__.__name__, e)
except Exception as e:
module.log.exception("Error in running extraction from module %s: %s",
module.__class__.__name__, e)
module.log.exception(
"The run() procedure of module %s was not implemented yet!",
module.__class__.__name__,
)
except InsufficientPrivileges as exc:
module.log.info(
"Insufficient privileges for module %s: %s", module.__class__.__name__, exc
)
except DatabaseNotFoundError as exc:
module.log.info(
"There might be no data to extract by module %s: %s",
module.__class__.__name__,
exc,
)
except DatabaseCorruptedError as exc:
module.log.error(
"The %s module database seems to be corrupted: %s",
module.__class__.__name__,
exc,
)
except Exception as exc:
module.log.exception(
"Error in running extraction from module %s: %s",
module.__class__.__name__,
exc,
)
else:
try:
module.check_indicators()
exec_or_profile("module.check_indicators()", globals(), locals())
except NotImplementedError:
module.log.info("The %s module does not support checking for indicators",
module.__class__.__name__)
pass
module.log.info(
"The %s module does not support checking for indicators",
module.__class__.__name__,
)
except Exception as exc:
module.log.exception(
"Error when checking indicators from module %s: %s",
module.__class__.__name__,
exc,
)
else:
if module.indicators and not module.detected:
module.log.info("The %s module produced no detections!",
module.__class__.__name__)
module.log.info(
"The %s module produced no detections!", module.__class__.__name__
)
try:
module.to_timeline()
except NotImplementedError:
pass
except Exception as exc:
module.log.exception(
"Error when serializing data from module %s: %s",
module.__class__.__name__,
exc,
)
module.save_to_json()
@@ -195,13 +235,19 @@ def save_timeline(timeline: list, timeline_path: str) -> None:
"""
with open(timeline_path, "a+", encoding="utf-8") as handle:
csvoutput = csv.writer(handle, delimiter=",", quotechar="\"",
quoting=csv.QUOTE_ALL)
csvoutput = csv.writer(
handle, delimiter=",", quotechar='"', quoting=csv.QUOTE_ALL, escapechar="\\"
)
csvoutput.writerow(["UTC Timestamp", "Plugin", "Event", "Description"])
for event in sorted(timeline, key=lambda x: x["timestamp"] if x["timestamp"] is not None else ""):
csvoutput.writerow([
event.get("timestamp"),
event.get("module"),
event.get("event"),
event.get("data"),
])
for event in sorted(
timeline, key=lambda x: x["timestamp"] if x["timestamp"] is not None else ""
):
csvoutput.writerow(
[
event.get("timestamp"),
event.get("module"),
event.get("event"),
event.get("data"),
]
)

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
@@ -13,27 +13,21 @@ class MutuallyExclusiveOption(Option):
def __init__(self, *args, **kwargs):
self.mutually_exclusive = set(kwargs.pop("mutually_exclusive", []))
help = kwargs.get("help", "")
help_msg = kwargs.get("help", "")
if self.mutually_exclusive:
ex_str = ", ".join(self.mutually_exclusive)
kwargs["help"] = help + (
" NOTE: This argument is mutually exclusive with "
"arguments: [" + ex_str + "]."
kwargs["help"] = (
f"{help_msg} NOTE: This argument is mutually exclusive with arguments"
f"[{ex_str}]."
)
super(MutuallyExclusiveOption, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
def handle_parse_result(self, ctx, opts, args):
if self.mutually_exclusive.intersection(opts) and self.name in opts:
raise UsageError(
"Illegal usage: `{}` is mutually exclusive with "
"arguments `{}`.".format(
self.name,
", ".join(self.mutually_exclusive)
)
f"Illegal usage: `{self.name}` is mutually exclusive "
f"with arguments `{', '.join(self.mutually_exclusive)}`."
)
return super(MutuallyExclusiveOption, self).handle_parse_result(
ctx,
opts,
args
)
return super().handle_parse_result(ctx, opts, args)

View File

@@ -1,11 +1,12 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# 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 datetime import datetime
from typing import Optional, Tuple
import requests
import yaml
@@ -21,7 +22,6 @@ INDICATORS_CHECK_FREQUENCY = 12
class MVTUpdates:
def check(self) -> str:
res = requests.get("https://pypi.org/pypi/mvt/json")
data = res.json()
@@ -34,7 +34,6 @@ class MVTUpdates:
class IndicatorsUpdates:
def __init__(self) -> None:
self.github_raw_url = "https://raw.githubusercontent.com/{}/{}/{}/{}"
@@ -43,16 +42,21 @@ class IndicatorsUpdates:
self.index_branch = "main"
self.index_path = "indicators.yaml"
self.latest_update_path = os.path.join(MVT_DATA_FOLDER,
"latest_indicators_update")
self.latest_check_path = os.path.join(MVT_DATA_FOLDER,
"latest_indicators_check")
if not os.path.exists(MVT_DATA_FOLDER):
os.makedirs(MVT_DATA_FOLDER)
self.latest_update_path = os.path.join(
MVT_DATA_FOLDER, "latest_indicators_update"
)
self.latest_check_path = os.path.join(
MVT_DATA_FOLDER, "latest_indicators_check"
)
def get_latest_check(self) -> int:
if not os.path.exists(self.latest_check_path):
return 0
with open(self.latest_check_path, "r") as handle:
with open(self.latest_check_path, "r", encoding="utf-8") as handle:
data = handle.read().strip()
if data:
return int(data)
@@ -61,14 +65,14 @@ class IndicatorsUpdates:
def set_latest_check(self) -> None:
timestamp = int(datetime.utcnow().timestamp())
with open(self.latest_check_path, "w") as handle:
with open(self.latest_check_path, "w", encoding="utf-8") as handle:
handle.write(str(timestamp))
def get_latest_update(self) -> int:
if not os.path.exists(self.latest_update_path):
return 0
with open(self.latest_update_path, "r") as handle:
with open(self.latest_update_path, "r", encoding="utf-8") as handle:
data = handle.read().strip()
if data:
return int(data)
@@ -77,25 +81,32 @@ class IndicatorsUpdates:
def set_latest_update(self) -> None:
timestamp = int(datetime.utcnow().timestamp())
with open(self.latest_update_path, "w") as handle:
with open(self.latest_update_path, "w", encoding="utf-8") as handle:
handle.write(str(timestamp))
def get_remote_index(self) -> dict:
url = self.github_raw_url.format(self.index_owner, self.index_repo,
self.index_branch, self.index_path)
def get_remote_index(self) -> Optional[dict]:
url = self.github_raw_url.format(
self.index_owner, self.index_repo, self.index_branch, self.index_path
)
res = requests.get(url)
if res.status_code != 200:
log.error("Failed to retrieve indicators index located at %s (error %d)",
url, res.status_code)
log.error(
"Failed to retrieve indicators index located at %s (error %d)",
url,
res.status_code,
)
return None
return yaml.safe_load(res.content)
def download_remote_ioc(self, ioc_url: str) -> str:
def download_remote_ioc(self, ioc_url: str) -> Optional[str]:
res = requests.get(ioc_url)
if res.status_code != 200:
log.error("Failed to download indicators file from %s (error %d)",
ioc_url, res.status_code)
log.error(
"Failed to download indicators file from %s (error %d)",
ioc_url,
res.status_code,
)
return None
clean_file_name = ioc_url.lstrip("https://").replace("/", "_")
@@ -113,6 +124,9 @@ class IndicatorsUpdates:
os.makedirs(MVT_INDICATORS_FOLDER)
index = self.get_remote_index()
if not index:
return
for ioc in index.get("indicators", []):
ioc_type = ioc.get("type", "")
@@ -128,44 +142,59 @@ class IndicatorsUpdates:
ioc_url = ioc.get("download_url", "")
if not ioc_url:
log.error("Could not find a way to download indicator file for %s",
ioc.get("name"))
log.error(
"Could not find a way to download indicator file for %s",
ioc.get("name"),
)
continue
ioc_local_path = self.download_remote_ioc(ioc_url)
if not ioc_local_path:
continue
log.info("Downloaded indicators \"%s\" to %s",
ioc.get("name"), ioc_local_path)
log.info(
'Downloaded indicators "%s" to %s', ioc.get("name"), ioc_local_path
)
self.set_latest_update()
def _get_remote_file_latest_commit(self, owner: str, repo: str,
branch: str, path: str) -> bool:
file_commit_url = f"https://api.github.com/repos/{self.index_owner}/{self.index_repo}/commits?path={self.index_path}"
def _get_remote_file_latest_commit(
self, owner: str, repo: str, branch: str, path: str
) -> int:
# TODO: The branch is currently not taken into consideration.
# How do we specify which branch to look up to the API?
file_commit_url = (
f"https://api.github.com/repos/{owner}/{repo}/commits?path={path}"
)
res = requests.get(file_commit_url)
if res.status_code != 200:
log.error("Failed to get details about file %s (error %d)",
file_commit_url, res.status_code)
return False
log.error(
"Failed to get details about file %s (error %d)",
file_commit_url,
res.status_code,
)
return -1
details = res.json()
if len(details) == 0:
return False
return -1
latest_commit = details[0]
latest_commit_date = latest_commit.get("commit", {}).get("author", {}).get("date", None)
latest_commit_date = (
latest_commit.get("commit", {}).get("author", {}).get("date", None)
)
if not latest_commit_date:
log.error("Failed to retrieve date of latest update to indicators index file")
return False
log.error(
"Failed to retrieve date of latest update to indicators index file"
)
return -1
latest_commit_dt = datetime.strptime(latest_commit_date, '%Y-%m-%dT%H:%M:%SZ')
latest_commit_dt = datetime.strptime(latest_commit_date, "%Y-%m-%dT%H:%M:%SZ")
latest_commit_ts = int(latest_commit_dt.timestamp())
return latest_commit_ts
def should_check(self) -> (bool, int):
def should_check(self) -> Tuple[bool, int]:
now = datetime.utcnow()
latest_check_ts = self.get_latest_check()
latest_check_dt = datetime.fromtimestamp(latest_check_ts)
@@ -176,21 +205,23 @@ class IndicatorsUpdates:
if diff_hours >= INDICATORS_CHECK_FREQUENCY:
return True, 0
return False, INDICATORS_CHECK_FREQUENCY - diff_hours
return False, int(INDICATORS_CHECK_FREQUENCY - diff_hours)
def check(self) -> bool:
self.set_latest_check()
latest_update = self.get_latest_update()
latest_commit_ts = self._get_remote_file_latest_commit(self.index_owner,
self.index_repo,
self.index_branch,
self.index_path)
latest_commit_ts = self._get_remote_file_latest_commit(
self.index_owner, self.index_repo, self.index_branch, self.index_path
)
if latest_update < latest_commit_ts:
return True
index = self.get_remote_index()
if not index:
return False
for ioc in index.get("indicators", []):
if ioc.get("type", "") != "github":
continue
@@ -201,10 +232,9 @@ class IndicatorsUpdates:
branch = github.get("branch", "main")
path = github.get("path", "")
file_latest_commit_ts = self._get_remote_file_latest_commit(owner,
repo,
branch,
path)
file_latest_commit_ts = self._get_remote_file_latest_commit(
owner, repo, branch, path
)
if latest_update < file_latest_commit_ts:
return True

View File

@@ -1,10 +1,11 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# 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 typing import Optional
import requests
from tld import get_tld
SHORTENER_DOMAINS = [
@@ -253,9 +254,8 @@ SHORTENER_DOMAINS = [
class URL:
def __init__(self, url: str) -> None:
if type(url) == bytes:
if isinstance(url, bytes):
url = url.decode()
self.url = url
@@ -263,7 +263,7 @@ class URL:
self.top_level = self.get_top_level()
self.is_shortened = False
def get_domain(self) -> None:
def get_domain(self) -> str:
"""Get the domain from a URL.
:param url: URL to parse
@@ -272,15 +272,13 @@ class URL:
:rtype: str
"""
# TODO: Properly handle exception.
try:
return get_tld(self.url,
as_object=True,
fix_protocol=True).parsed_url.netloc.lower().lstrip("www.")
except Exception:
return None
return (
get_tld(self.url, as_object=True, fix_protocol=True)
.parsed_url.netloc.lower()
.lstrip("www.")
)
def get_top_level(self) -> None:
def get_top_level(self) -> str:
"""Get only the top-level domain from a URL.
:param url: URL to parse
@@ -289,11 +287,7 @@ class URL:
:rtype: str
"""
# TODO: Properly handle exception.
try:
return get_tld(self.url, as_object=True, fix_protocol=True).fld.lower()
except Exception:
return None
return get_tld(self.url, as_object=True, fix_protocol=True).fld.lower()
def check_if_shortened(self) -> bool:
"""Check if the URL is among list of shortener services.
@@ -314,3 +308,5 @@ class URL:
res = requests.head(self.url)
if str(res.status_code).startswith("30"):
return res.headers["Location"]
return ""

View File

@@ -1,21 +1,83 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import cProfile
import datetime
import hashlib
import logging
import os
import re
from typing import Any, Iterator, Union
from rich.logging import RichHandler
def convert_mactime_to_unix(timestamp, from_2001: bool = True):
"""Converts Mac Standard Time to a Unix timestamp.
def convert_chrometime_to_datetime(timestamp: int) -> datetime.datetime:
"""Converts Chrome timestamp to a datetime.
:param timestamp: Chrome timestamp as int.
:type timestamp: int
:returns: datetime.
"""
epoch_start = datetime.datetime(1601, 1, 1)
delta = datetime.timedelta(microseconds=timestamp)
return epoch_start + delta
def convert_datetime_to_iso(date_time: datetime.datetime) -> str:
"""Converts datetime to ISO string.
:param datetime: datetime.
:type datetime: datetime.datetime
:returns: ISO datetime string in YYYY-mm-dd HH:MM:SS.ms format.
:rtype: str
"""
try:
return date_time.strftime("%Y-%m-%d %H:%M:%S.%f")
except Exception:
return ""
def convert_unix_to_utc_datetime(
timestamp: Union[int, float, str]
) -> datetime.datetime:
"""Converts a unix epoch timestamp to UTC datetime.
:param timestamp: Epoc timestamp to convert.
:type timestamp: int
:returns: datetime.
"""
return datetime.datetime.utcfromtimestamp(float(timestamp))
def convert_unix_to_iso(timestamp: Union[int, float, str]) -> str:
"""Converts a unix epoch to ISO string.
:param timestamp: Epoc timestamp to convert.
:type timestamp: int
:returns: ISO datetime string in YYYY-mm-dd HH:MM:SS.ms format.
:rtype: str
"""
try:
return convert_datetime_to_iso(convert_unix_to_utc_datetime(timestamp))
except Exception:
return ""
def convert_mactime_to_datetime(timestamp: Union[int, float], from_2001: bool = True):
"""Converts Mac Standard Time to a datetime.
:param timestamp: MacTime timestamp (either int or float).
:type timestamp: int
:param from_2001: bool: Whether to (Default value = True)
:param from_2001: Default value = True)
:returns: Unix epoch timestamp.
:returns: datetime.
"""
if not timestamp:
@@ -23,7 +85,7 @@ def convert_mactime_to_unix(timestamp, from_2001: bool = True):
# This is to fix formats in case of, for example, SMS messages database
# timestamp format.
if type(timestamp) == int and len(str(timestamp)) == 18:
if isinstance(timestamp, int) and len(str(timestamp)) == 18:
timestamp = int(str(timestamp)[:9])
# MacTime counts from 2001-01-01.
@@ -32,37 +94,24 @@ def convert_mactime_to_unix(timestamp, from_2001: bool = True):
# TODO: This is rather ugly. Happens sometimes with invalid timestamps.
try:
return datetime.datetime.utcfromtimestamp(timestamp)
return convert_unix_to_utc_datetime(timestamp)
except Exception:
return None
def convert_chrometime_to_unix(timestamp: int) -> int:
"""Converts Chrome timestamp to a Unix timestamp.
def convert_mactime_to_iso(timestamp: int, from_2001: bool = True):
"""Wraps two conversions from mactime to iso date.
:param timestamp: Chrome timestamp as int.
:type timestamp: int
:returns: Unix epoch timestamp.
"""
epoch_start = datetime.datetime(1601, 1, 1)
delta = datetime.timedelta(microseconds=timestamp)
return epoch_start + delta
def convert_timestamp_to_iso(timestamp: str) -> str:
"""Converts Unix timestamp to ISO string.
:param timestamp: Unix timestamp.
:param timestamp: MacTime timestamp (either int or float).
:type timestamp: int
:param from_2001: bool: Whether to (Default value = True)
:param from_2001: Default value = True)
:returns: ISO timestamp string in YYYY-mm-dd HH:MM:SS.ms format.
:rtype: str
"""
try:
return timestamp.strftime("%Y-%m-%d %H:%M:%S.%f")
except Exception:
return None
return convert_datetime_to_iso(convert_mactime_to_datetime(timestamp, from_2001))
def check_for_links(text: str) -> list:
@@ -76,24 +125,9 @@ def check_for_links(text: str) -> list:
return re.findall(r"(?P<url>https?://[^\s]+)", text, re.IGNORECASE)
def get_sha256_from_file_path(file_path: str) -> str:
"""Calculate the SHA256 hash of a file from a file path.
:param file_path: Path to the file to hash
:returns: The SHA256 hash string
"""
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as handle:
for byte_block in iter(lambda: handle.read(4096), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
# Note: taken from here:
# https://stackoverflow.com/questions/57014259/json-dumps-on-dictionary-with-bytes-for-keys
def keys_bytes_to_string(obj) -> str:
def keys_bytes_to_string(obj: Any) -> Any:
"""Convert object keys from bytes to string.
:param obj: Object to convert from bytes to string.
@@ -106,8 +140,8 @@ def keys_bytes_to_string(obj) -> str:
if isinstance(obj, (tuple, list, set)):
value = [keys_bytes_to_string(x) for x in obj]
return value
else:
return obj
return obj
for key, value in obj.items():
if isinstance(key, bytes):
@@ -119,3 +153,84 @@ def keys_bytes_to_string(obj) -> str:
new_obj[key] = value
return new_obj
def get_sha256_from_file_path(file_path: str) -> str:
"""Calculate the SHA256 hash of a file from a file path.
:param file_path: Path to the file to hash
:returns: The SHA256 hash string
"""
sha256_hash = hashlib.sha256()
try:
with open(file_path, "rb") as handle:
for byte_block in iter(lambda: handle.read(4096), b""):
sha256_hash.update(byte_block)
except OSError:
return ""
return sha256_hash.hexdigest()
def generate_hashes_from_path(path: str, log) -> Iterator[dict]:
"""
Generates hashes of all files at the given path.
:params path: Path of the given folder or file
:returns: generator of dict {"file_path", "hash"}
"""
if os.path.isfile(path):
hash_value = get_sha256_from_file_path(path)
yield {"file_path": path, "sha256": hash_value}
elif os.path.isdir(path):
for root, _, files in os.walk(path):
for file in files:
file_path = os.path.join(root, file)
try:
sha256 = get_sha256_from_file_path(file_path)
except FileNotFoundError:
log.error(
"Failed to hash the file %s: might be a symlink", file_path
)
continue
except PermissionError:
log.error(
"Failed to hash the file %s: permission denied", file_path
)
continue
yield {"file_path": file_path, "sha256": sha256}
def init_logging(verbose: bool = False):
"""
Initialise logging for the MVT module
"""
# Setup logging using Rich.
log = logging.getLogger("mvt")
log.setLevel(logging.DEBUG)
consoleHandler = RichHandler(show_path=False, log_time_format="%X")
consoleHandler.setFormatter(logging.Formatter("[%(name)s] %(message)s"))
if verbose:
consoleHandler.setLevel(logging.DEBUG)
else:
consoleHandler.setLevel(logging.INFO)
log.addHandler(consoleHandler)
def set_verbose_logging(verbose: bool = False):
log = logging.getLogger("mvt")
handler = log.handlers[0]
if verbose:
handler.setLevel(logging.DEBUG)
else:
handler.setLevel(logging.INFO)
def exec_or_profile(module, globals, locals):
"""Hook for profiling MVT modules"""
if int(os.environ.get("MVT_PROFILE", False)):
cProfile.runctx(module, globals, locals)
else:
exec(module, globals, locals)

View File

@@ -1,6 +1,6 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
MVT_VERSION = "2.1.1"
MVT_VERSION = "2.4.0"

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
@@ -23,23 +23,30 @@ class VTQuotaExceeded(Exception):
def virustotal_lookup(file_hash: str):
if MVT_VT_API_KEY not in os.environ:
raise VTNoKey("No VirusTotal API key provided: to use VirusTotal lookups please provide your API key with `export MVT_VT_API_KEY=<key>`")
raise VTNoKey(
"No VirusTotal API key provided: to use VirusTotal "
"lookups please provide your API key with "
"`export MVT_VT_API_KEY=<key>`"
)
headers = {
"User-Agent": "VirusTotal",
"Content-Type": "application/json",
"x-apikey": os.environ[MVT_VT_API_KEY],
}
res = requests.get(f"https://www.virustotal.com/api/v3/files/{file_hash}", headers=headers)
res = requests.get(
f"https://www.virustotal.com/api/v3/files/{file_hash}", headers=headers
)
if res.status_code == 200:
report = res.json()
return report["data"]
elif res.status_code == 404:
if res.status_code == 404:
log.info("Could not find results for file with hash %s", file_hash)
elif res.status_code == 429:
raise VTQuotaExceeded("You have exceeded the quota for your VirusTotal API key")
else:
raise Exception("Unexpected response from VirusTotal: %s", res.status_code)
raise Exception(f"Unexpected response from VirusTotal: {res.status_code}")
return None

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/

View File

@@ -1,22 +1,33 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# 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 logging
import os
import click
from rich.logging import RichHandler
from rich.prompt import Prompt
from mvt.common.cmd_check_iocs import CmdCheckIOCS
from mvt.common.help import (HELP_MSG_FAST, HELP_MSG_IOC,
HELP_MSG_LIST_MODULES, HELP_MSG_MODULE,
HELP_MSG_OUTPUT)
from mvt.common.help import (
HELP_MSG_FAST,
HELP_MSG_HASHES,
HELP_MSG_IOC,
HELP_MSG_LIST_MODULES,
HELP_MSG_MODULE,
HELP_MSG_OUTPUT,
HELP_MSG_VERBOSE,
)
from mvt.common.logo import logo
from mvt.common.options import MutuallyExclusiveOption
from mvt.common.updates import IndicatorsUpdates
from mvt.common.utils import (
generate_hashes_from_path,
init_logging,
set_verbose_logging,
)
from .cmd_check_backup import CmdIOSCheckBackup
from .cmd_check_fs import CmdIOSCheckFS
@@ -25,62 +36,86 @@ from .modules.backup import BACKUP_MODULES
from .modules.fs import FS_MODULES
from .modules.mixed import MIXED_MODULES
# Setup logging using Rich.
LOG_FORMAT = "[%(name)s] %(message)s"
logging.basicConfig(level="INFO", format=LOG_FORMAT, handlers=[
RichHandler(show_path=False, log_time_format="%X")])
log = logging.getLogger(__name__)
init_logging()
log = logging.getLogger("mvt")
# Set this environment variable to a password if needed.
MVT_IOS_BACKUP_PASSWORD = "MVT_IOS_BACKUP_PASSWORD"
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
#==============================================================================
# ==============================================================================
# Main
#==============================================================================
# ==============================================================================
@click.group(invoke_without_command=False)
def cli():
logo()
#==============================================================================
# ==============================================================================
# Command: version
#==============================================================================
# ==============================================================================
@cli.command("version", help="Show the currently installed version of MVT")
def version():
return
#==============================================================================
# ==============================================================================
# Command: decrypt-backup
#==============================================================================
@cli.command("decrypt-backup", help="Decrypt an encrypted iTunes backup")
@click.option("--destination", "-d", required=True,
help="Path to the folder where to store the decrypted backup")
@click.option("--password", "-p", cls=MutuallyExclusiveOption,
help=f"Password to use to decrypt the backup (or, set {MVT_IOS_BACKUP_PASSWORD} environment variable)",
mutually_exclusive=["key_file"])
@click.option("--key-file", "-k", cls=MutuallyExclusiveOption,
type=click.Path(exists=True),
help="File containing raw encryption key to use to decrypt the backup",
mutually_exclusive=["password"])
# ==============================================================================
@cli.command(
"decrypt-backup",
help="Decrypt an encrypted iTunes backup",
context_settings=CONTEXT_SETTINGS,
)
@click.option(
"--destination",
"-d",
required=True,
help="Path to the folder where to store the decrypted backup",
)
@click.option(
"--password",
"-p",
cls=MutuallyExclusiveOption,
help="Password to use to decrypt the backup (or, set "
f"{MVT_IOS_BACKUP_PASSWORD} environment variable)",
mutually_exclusive=["key_file"],
)
@click.option(
"--key-file",
"-k",
cls=MutuallyExclusiveOption,
type=click.Path(exists=True),
help="File containing raw encryption key to use to decrypt " "the backup",
mutually_exclusive=["password"],
)
@click.option("--hashes", "-H", is_flag=True, help=HELP_MSG_HASHES)
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
@click.pass_context
def decrypt_backup(ctx, destination, password, key_file, backup_path):
def decrypt_backup(ctx, destination, password, key_file, hashes, backup_path):
backup = DecryptBackup(backup_path, destination)
if key_file:
if MVT_IOS_BACKUP_PASSWORD in os.environ:
log.info("Ignoring environment variable, using --key-file '%s' instead",
MVT_IOS_BACKUP_PASSWORD, key_file)
log.info(
"Ignoring %s environment variable, using --key-file" "'%s' instead",
MVT_IOS_BACKUP_PASSWORD,
key_file,
)
backup.decrypt_with_key_file(key_file)
elif password:
log.info("Your password may be visible in the process table because it was supplied on the command line!")
log.info(
"Your password may be visible in the process table because it "
"was supplied on the command line!"
)
if MVT_IOS_BACKUP_PASSWORD in os.environ:
log.info("Ignoring %s environment variable, using --password argument instead",
MVT_IOS_BACKUP_PASSWORD)
log.info(
"Ignoring %s environment variable, using --password" "argument instead",
MVT_IOS_BACKUP_PASSWORD,
)
backup.decrypt_with_password(password)
elif MVT_IOS_BACKUP_PASSWORD in os.environ:
@@ -95,27 +130,54 @@ def decrypt_backup(ctx, destination, password, key_file, backup_path):
backup.process_backup()
if hashes:
info = {"encrypted": [], "decrypted": []}
for file in generate_hashes_from_path(backup_path, log):
info["encrypted"].append(file)
for file in generate_hashes_from_path(destination, log):
info["decrypted"].append(file)
info_path = os.path.join(destination, "info.json")
with open(info_path, "w+", encoding="utf-8") as handle:
json.dump(info, handle, indent=4)
#==============================================================================
# ==============================================================================
# Command: extract-key
#==============================================================================
@cli.command("extract-key", help="Extract decryption key from an iTunes backup")
@click.option("--password", "-p",
help=f"Password to use to decrypt the backup (or, set {MVT_IOS_BACKUP_PASSWORD} environment variable)")
@click.option("--key-file", "-k",
help="Key file to be written (if unset, will print to STDOUT)",
required=False,
type=click.Path(exists=False, file_okay=True, dir_okay=False, writable=True))
# ==============================================================================
@cli.command(
"extract-key",
help="Extract decryption key from an iTunes backup",
context_settings=CONTEXT_SETTINGS,
)
@click.option(
"--password",
"-p",
help="Password to use to decrypt the backup (or, set "
f"{MVT_IOS_BACKUP_PASSWORD} environment variable)",
)
@click.option(
"--key-file",
"-k",
help="Key file to be written (if unset, will print to STDOUT)",
required=False,
type=click.Path(exists=False, file_okay=True, dir_okay=False, writable=True),
)
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
def extract_key(password, key_file, backup_path):
backup = DecryptBackup(backup_path)
if password:
log.info("Your password may be visible in the process table because it was supplied on the command line!")
log.info(
"Your password may be visible in the process table because it "
"was supplied on the command line!"
)
if MVT_IOS_BACKUP_PASSWORD in os.environ:
log.info("Ignoring %s environment variable, using --password argument instead",
MVT_IOS_BACKUP_PASSWORD)
log.info(
"Ignoring %s environment variable, using --password "
"argument instead",
MVT_IOS_BACKUP_PASSWORD,
)
elif MVT_IOS_BACKUP_PASSWORD in os.environ:
log.info("Using password from %s environment variable", MVT_IOS_BACKUP_PASSWORD)
password = os.environ[MVT_IOS_BACKUP_PASSWORD]
@@ -129,21 +191,44 @@ def extract_key(password, key_file, backup_path):
backup.write_key(key_file)
#==============================================================================
# ==============================================================================
# Command: check-backup
#==============================================================================
@cli.command("check-backup", help="Extract artifacts from an iTunes backup")
@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True,
default=[], help=HELP_MSG_IOC)
# ==============================================================================
@cli.command(
"check-backup",
help="Extract artifacts from an iTunes backup",
context_settings=CONTEXT_SETTINGS,
)
@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("--fast", "-f", is_flag=True, help=HELP_MSG_FAST)
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
@click.option("--module", "-m", help=HELP_MSG_MODULE)
@click.option("--hashes", "-H", is_flag=True, help=HELP_MSG_HASHES)
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
@click.pass_context
def check_backup(ctx, iocs, output, fast, list_modules, module, backup_path):
cmd = CmdIOSCheckBackup(target_path=backup_path, results_path=output,
ioc_files=iocs, module_name=module, fast_mode=fast)
def check_backup(
ctx, iocs, output, fast, list_modules, module, hashes, verbose, backup_path
):
set_verbose_logging(verbose)
module_options = {"fast_mode": fast}
cmd = CmdIOSCheckBackup(
target_path=backup_path,
results_path=output,
ioc_files=iocs,
module_name=module,
module_options=module_options,
hashes=hashes,
)
if list_modules:
cmd.list_modules()
@@ -153,26 +238,48 @@ def check_backup(ctx, iocs, output, fast, list_modules, module, backup_path):
cmd.run()
if len(cmd.timeline_detected) > 0:
log.warning("The analysis of the backup produced %d detections!",
len(cmd.timeline_detected))
if cmd.detected_count > 0:
log.warning(
"The analysis of the backup produced %d detections!", cmd.detected_count
)
#==============================================================================
# ==============================================================================
# Command: check-fs
#==============================================================================
@cli.command("check-fs", help="Extract artifacts from a full filesystem dump")
@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True,
default=[], help=HELP_MSG_IOC)
# ==============================================================================
@cli.command(
"check-fs",
help="Extract artifacts from a full filesystem dump",
context_settings=CONTEXT_SETTINGS,
)
@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("--fast", "-f", is_flag=True, help=HELP_MSG_FAST)
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
@click.option("--module", "-m", help=HELP_MSG_MODULE)
@click.option("--hashes", "-H", is_flag=True, help=HELP_MSG_HASHES)
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
@click.argument("DUMP_PATH", type=click.Path(exists=True))
@click.pass_context
def check_fs(ctx, iocs, output, fast, list_modules, module, dump_path):
cmd = CmdIOSCheckFS(target_path=dump_path, results_path=output,
ioc_files=iocs, module_name=module, fast_mode=fast)
def check_fs(ctx, iocs, output, fast, list_modules, module, hashes, verbose, dump_path):
set_verbose_logging(verbose)
module_options = {"fast_mode": fast}
cmd = CmdIOSCheckFS(
target_path=dump_path,
results_path=output,
ioc_files=iocs,
module_name=module,
module_options=module_options,
hashes=hashes,
)
if list_modules:
cmd.list_modules()
@@ -182,17 +289,29 @@ def check_fs(ctx, iocs, output, fast, list_modules, module, dump_path):
cmd.run()
if len(cmd.timeline_detected) > 0:
log.warning("The analysis of the iOS filesystem produced %d detections!",
len(cmd.timeline_detected))
if cmd.detected_count > 0:
log.warning(
"The analysis of the iOS filesystem produced %d detections!",
cmd.detected_count,
)
#==============================================================================
# ==============================================================================
# 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)
# ==============================================================================
@cli.command(
"check-iocs",
help="Compare stored JSON results to provided indicators",
context_settings=CONTEXT_SETTINGS,
)
@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))
@@ -208,10 +327,14 @@ def check_iocs(ctx, iocs, list_modules, module, folder):
cmd.run()
#==============================================================================
# ==============================================================================
# Command: download-iocs
#==============================================================================
@cli.command("download-iocs", help="Download public STIX2 indicators")
# ==============================================================================
@cli.command(
"download-iocs",
help="Download public STIX2 indicators",
context_settings=CONTEXT_SETTINGS,
)
def download_iocs():
ioc_updates = IndicatorsUpdates()
ioc_updates.update()

View File

@@ -1,9 +1,10 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
from mvt.common.command import Command
@@ -14,16 +15,29 @@ log = logging.getLogger(__name__)
class CmdIOSCheckBackup(Command):
def __init__(
self,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
ioc_files: Optional[list] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
module_options: Optional[dict] = None,
hashes: bool = False,
) -> None:
super().__init__(
target_path=target_path,
results_path=results_path,
ioc_files=ioc_files,
module_name=module_name,
serial=serial,
module_options=module_options,
hashes=hashes,
log=log,
)
name = "check-backup"
modules = BACKUP_MODULES + MIXED_MODULES
def __init__(self, target_path: str = None, results_path: str = None,
ioc_files: list = [], module_name: str = None, serial: str = None,
fast_mode: bool = False):
super().__init__(target_path=target_path, results_path=results_path,
ioc_files=ioc_files, module_name=module_name,
serial=serial, fast_mode=fast_mode, log=log)
self.name = "check-backup"
self.modules = BACKUP_MODULES + MIXED_MODULES
def module_init(self, module):
module.is_backup = True

View File

@@ -1,9 +1,10 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
from mvt.common.command import Command
@@ -14,16 +15,29 @@ log = logging.getLogger(__name__)
class CmdIOSCheckFS(Command):
def __init__(
self,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
ioc_files: Optional[list] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
module_options: Optional[dict] = None,
hashes: bool = False,
) -> None:
super().__init__(
target_path=target_path,
results_path=results_path,
ioc_files=ioc_files,
module_name=module_name,
serial=serial,
module_options=module_options,
hashes=hashes,
log=log,
)
name = "check-fs"
modules = FS_MODULES + MIXED_MODULES
def __init__(self, target_path: str = None, results_path: str = None,
ioc_files: list = [], module_name: str = None, serial: str = None,
fast_mode: bool = False):
super().__init__(target_path=target_path, results_path=results_path,
ioc_files=ioc_files, module_name=module_name,
serial=serial, fast_mode=fast_mode, log=log)
self.name = "check-fs"
self.modules = FS_MODULES + MIXED_MODULES
def module_init(self, module):
module.is_fs_dump = True

Some files were not shown because too many files have changed in this diff Show More