mirror of
https://github.com/mvt-project/mvt.git
synced 2026-02-14 17:42:46 +00:00
Compare commits
433 Commits
v1.1.0
...
Te-k-featu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7222bc82e1 | ||
|
|
4a568835d2 | ||
|
|
f98282d6c5 | ||
|
|
f864adf97e | ||
|
|
8f6882b0ff | ||
|
|
b6531e3e70 | ||
|
|
ef662c1145 | ||
|
|
b8e5346660 | ||
|
|
aedef123c9 | ||
|
|
8ff8e599d8 | ||
|
|
815cdc0a88 | ||
|
|
b420d828ee | ||
|
|
7b92903536 | ||
|
|
2bde693c35 | ||
|
|
7daea737c6 | ||
|
|
0d75dc3ba0 | ||
|
|
0622357a64 | ||
|
|
c4f91ba28b | ||
|
|
5ade0657ac | ||
|
|
cca9083dff | ||
|
|
3f4ddaaa0c | ||
|
|
7024909e05 | ||
|
|
3899dce353 | ||
|
|
4830aa5a6c | ||
|
|
3608576417 | ||
|
|
043c234401 | ||
|
|
8663c78b63 | ||
|
|
b847683717 | ||
|
|
09400a2847 | ||
|
|
2bc6fbef2f | ||
|
|
b77749e6ba | ||
|
|
1643454190 | ||
|
|
c2f1fe718d | ||
|
|
444ecf032d | ||
|
|
dd230c2407 | ||
|
|
cd87b6ed31 | ||
|
|
6f50af479d | ||
|
|
36a67911b3 | ||
|
|
2dbfef322a | ||
|
|
fba4e27757 | ||
|
|
abc0f2768b | ||
|
|
e7fe30e201 | ||
|
|
c54a01ca59 | ||
|
|
a12c4e6b93 | ||
|
|
a9be771f79 | ||
|
|
a7d35dba4a | ||
|
|
3a6e4a7001 | ||
|
|
bb0e41e949 | ||
|
|
6844f0b90b | ||
|
|
fb2a0ba668 | ||
|
|
e34f8f3660 | ||
|
|
fd3f9dba8f | ||
|
|
27f0364c1d | ||
|
|
8dac714214 | ||
|
|
732a712e3d | ||
|
|
6d278d4bec | ||
|
|
c39b4d2179 | ||
|
|
a653fd5253 | ||
|
|
f754bf274d | ||
|
|
fcac8a8c7d | ||
|
|
d82c788a18 | ||
|
|
946a9ef02b | ||
|
|
c343eed5a0 | ||
|
|
6162a1e1f2 | ||
|
|
f61729deed | ||
|
|
7a00e88f1f | ||
|
|
ff41efba72 | ||
|
|
26e6a00bf5 | ||
|
|
9d61b9048c | ||
|
|
9950b3d6c2 | ||
|
|
e0d30ea990 | ||
|
|
293752f90a | ||
|
|
ac1e5c29d3 | ||
|
|
d868e6d9f0 | ||
|
|
f5cb7f06e1 | ||
|
|
5ce8035820 | ||
|
|
e3a8bde150 | ||
|
|
d6af7c8cca | ||
|
|
6584d8232c | ||
|
|
3487078c03 | ||
|
|
bc5d386be7 | ||
|
|
03efc8494b | ||
|
|
0b3f529cfa | ||
|
|
9bdef6ede4 | ||
|
|
fc9a27d030 | ||
|
|
f5f3660d82 | ||
|
|
712f5bcb9b | ||
|
|
ac26aa964a | ||
|
|
be511dcb51 | ||
|
|
b44c67e699 | ||
|
|
a4d08f8f35 | ||
|
|
6cc67f3c1d | ||
|
|
0d5377597f | ||
|
|
86c79075ff | ||
|
|
9940b1d145 | ||
|
|
b07fb092aa | ||
|
|
639c163297 | ||
|
|
8eb30e3a02 | ||
|
|
cd0e7d9879 | ||
|
|
bdaaf15434 | ||
|
|
699824d9ff | ||
|
|
8cca78d222 | ||
|
|
57cbb0ed56 | ||
|
|
e9cc6b3928 | ||
|
|
6d47d4d416 | ||
|
|
ed54761747 | ||
|
|
71c4ba799f | ||
|
|
09a6f291c0 | ||
|
|
b50be69dd4 | ||
|
|
6fc6102b73 | ||
|
|
3fe5d8dc8d | ||
|
|
fec6210d1b | ||
|
|
6a723e533f | ||
|
|
ed8a5a3845 | ||
|
|
04225a4455 | ||
|
|
5987f218be | ||
|
|
748780476e | ||
|
|
c522b54326 | ||
|
|
0e0e346916 | ||
|
|
69daf3c3cd | ||
|
|
998d87900d | ||
|
|
230f81879a | ||
|
|
df42efb7cb | ||
|
|
0922e569b0 | ||
|
|
03092cf3b7 | ||
|
|
ab63a02c9f | ||
|
|
a833dda581 | ||
|
|
189b1d7fc6 | ||
|
|
b1b282ac20 | ||
|
|
512c349c2c | ||
|
|
b94ba28873 | ||
|
|
564efc3629 | ||
|
|
9c62e6e4d6 | ||
|
|
153f6cce02 | ||
|
|
47f9a0104c | ||
|
|
bdad23feee | ||
|
|
5416b66915 | ||
|
|
e2936c3d33 | ||
|
|
3483ca1584 | ||
|
|
7b107edf1f | ||
|
|
b97ce7651a | ||
|
|
52a204cab6 | ||
|
|
1b335fda1d | ||
|
|
2ad175eae2 | ||
|
|
2d00dca5bd | ||
|
|
c8e50eb958 | ||
|
|
1f049fc8ba | ||
|
|
434738a306 | ||
|
|
06cd640c5e | ||
|
|
fb8a7ca104 | ||
|
|
8d15ff58dd | ||
|
|
eb5f07a75d | ||
|
|
ececf1a6b2 | ||
|
|
851cd52602 | ||
|
|
8db04fc991 | ||
|
|
3d0ba56e1f | ||
|
|
c48a4e8f50 | ||
|
|
001c2998a5 | ||
|
|
5e7c5727af | ||
|
|
883fbaeb88 | ||
|
|
6f0012cede | ||
|
|
458e80ccbb | ||
|
|
c8185fdbd8 | ||
|
|
67eea3edec | ||
|
|
bc86d159b8 | ||
|
|
43b1612dfe | ||
|
|
156f1084f1 | ||
|
|
49e34f6299 | ||
|
|
d88a66dd54 | ||
|
|
d3ed778ae4 | ||
|
|
4c3306c272 | ||
|
|
1c912f68fe | ||
|
|
10a640d3f7 | ||
|
|
c3acc95e9e | ||
|
|
90d05336da | ||
|
|
5513e6e9e3 | ||
|
|
38116f8405 | ||
|
|
59b069f006 | ||
|
|
28e1348aa7 | ||
|
|
034338d1f4 | ||
|
|
09d5eabf2f | ||
|
|
a425d6c511 | ||
|
|
f8897a4f8c | ||
|
|
86eae68bdb | ||
|
|
d2bf348b03 | ||
|
|
25c6c03075 | ||
|
|
cf88740f6a | ||
|
|
eb4810b0ad | ||
|
|
cce9159eda | ||
|
|
e1211991aa | ||
|
|
8ae9ca328c | ||
|
|
0e2eb51732 | ||
|
|
b35cd4bc73 | ||
|
|
1b4f99a31d | ||
|
|
e4e1716729 | ||
|
|
083bc12351 | ||
|
|
cf6d392460 | ||
|
|
95205d8e17 | ||
|
|
1460828c30 | ||
|
|
fa84b3f296 | ||
|
|
e1efaa5467 | ||
|
|
696d42fc6e | ||
|
|
a0e1662726 | ||
|
|
51645bdbc0 | ||
|
|
bb1b108fd7 | ||
|
|
92f9dcb8a5 | ||
|
|
a6fd5fe1f3 | ||
|
|
3e0ef20fcd | ||
|
|
01f3acde2e | ||
|
|
b697874f56 | ||
|
|
41d699f457 | ||
|
|
6fcd40f6b6 | ||
|
|
38bb583a9e | ||
|
|
48ec2d8fa8 | ||
|
|
798805c583 | ||
|
|
24be9e9570 | ||
|
|
adbd95c559 | ||
|
|
8a707c288a | ||
|
|
4c906ad52e | ||
|
|
a2f8030cce | ||
|
|
737007afdb | ||
|
|
33efeda90a | ||
|
|
146f2ae57d | ||
|
|
11bc916854 | ||
|
|
3084876f31 | ||
|
|
f63cb585b2 | ||
|
|
637aebcd89 | ||
|
|
16a0de3af4 | ||
|
|
15fbedccc9 | ||
|
|
e0514b20dd | ||
|
|
b2e9f0361b | ||
|
|
e85c70c603 | ||
|
|
3f8dade610 | ||
|
|
54963b0b59 | ||
|
|
513e2cc704 | ||
|
|
28d57e7178 | ||
|
|
dc8eeb618e | ||
|
|
c282d4341d | ||
|
|
681bae2f66 | ||
|
|
b079246c8a | ||
|
|
82b57f1997 | ||
|
|
8f88f872df | ||
|
|
2d16218489 | ||
|
|
3215e797ec | ||
|
|
e65a598903 | ||
|
|
e80c02451c | ||
|
|
5df50f864c | ||
|
|
45b31bb718 | ||
|
|
e10f1767e6 | ||
|
|
d64277c0bf | ||
|
|
3f3261511a | ||
|
|
4cfe75e2d4 | ||
|
|
cdd90332f7 | ||
|
|
d9b29b3739 | ||
|
|
79bb7d1d4b | ||
|
|
a653cb3cfc | ||
|
|
b25cc48be0 | ||
|
|
40bd9ddc1d | ||
|
|
deb95297da | ||
|
|
02014b414b | ||
|
|
7dd5fe7831 | ||
|
|
11d1a3dcee | ||
|
|
74f9db2bf2 | ||
|
|
356bddc3af | ||
|
|
512f40dcb4 | ||
|
|
b3a464ba58 | ||
|
|
529df85f0f | ||
|
|
19a6da8fe7 | ||
|
|
34c997f923 | ||
|
|
02bf903411 | ||
|
|
7019375767 | ||
|
|
34dd27c5d2 | ||
|
|
a4d6a08a8b | ||
|
|
635d3a392d | ||
|
|
2d78bddbba | ||
|
|
c1938d2ead | ||
|
|
104b01e5cd | ||
|
|
7087e8adb2 | ||
|
|
67608ac02b | ||
|
|
6d8de5b461 | ||
|
|
b0177d6104 | ||
|
|
e0c9a44b10 | ||
|
|
ef8c1ae895 | ||
|
|
3165801e2b | ||
|
|
1aa371a398 | ||
|
|
f8e380baa1 | ||
|
|
35559b09a8 | ||
|
|
daf5c1f3de | ||
|
|
f601db2174 | ||
|
|
3ce9641c23 | ||
|
|
9be393e3f6 | ||
|
|
5f125974b8 | ||
|
|
aa0f152ba1 | ||
|
|
169f5fbc26 | ||
|
|
5ea3460c09 | ||
|
|
c38df37967 | ||
|
|
7f29b522fa | ||
|
|
40b0da9885 | ||
|
|
94a8d9dd91 | ||
|
|
963d3db51a | ||
|
|
660e208473 | ||
|
|
01e68ccc6a | ||
|
|
fba0fa1f2c | ||
|
|
1cbf55e50e | ||
|
|
8fcc79ebfa | ||
|
|
423462395a | ||
|
|
1f08572a6a | ||
|
|
94e3c0ce7b | ||
|
|
904daad935 | ||
|
|
eb2a8b8b41 | ||
|
|
60a17381a2 | ||
|
|
ef2bb93dc4 | ||
|
|
f68b7e7089 | ||
|
|
a22241ec32 | ||
|
|
8ad1bc7a2b | ||
|
|
c6b3509ed4 | ||
|
|
75b5b296a5 | ||
|
|
2d62e31eaa | ||
|
|
1bfc683e4b | ||
|
|
7ab09669b5 | ||
|
|
757bd8618e | ||
|
|
f1d039346d | ||
|
|
ccdfd92d4a | ||
|
|
032b229eb8 | ||
|
|
93936976c7 | ||
|
|
f3a4e9d108 | ||
|
|
93a9735b5e | ||
|
|
7b0e2d4564 | ||
|
|
725a99bcd5 | ||
|
|
35a6f6ec9a | ||
|
|
f4ba29f1ef | ||
|
|
3f9809f36c | ||
|
|
6da6595108 | ||
|
|
35dfeaccee | ||
|
|
e5f2aa3c3d | ||
|
|
3236c1b390 | ||
|
|
80a670273d | ||
|
|
969b5cc506 | ||
|
|
ef8622d4c3 | ||
|
|
e39e9e6f92 | ||
|
|
7b32ed3179 | ||
|
|
315317863e | ||
|
|
08d35b056a | ||
|
|
3e679312d1 | ||
|
|
be4f1afed6 | ||
|
|
0dea25d86e | ||
|
|
505d3c7e60 | ||
|
|
8f04c09b75 | ||
|
|
595b7e2066 | ||
|
|
d3941bb5d3 | ||
|
|
194c8a0ac1 | ||
|
|
bef190fe50 | ||
|
|
cacf027051 | ||
|
|
da97f5ca30 | ||
|
|
a774577940 | ||
|
|
7252cc82a7 | ||
|
|
b34d80fd11 | ||
|
|
0347dfa3c9 | ||
|
|
28647b8493 | ||
|
|
c2ec26fd75 | ||
|
|
856a6fb895 | ||
|
|
62f3c535df | ||
|
|
34c64af815 | ||
|
|
ea4da71277 | ||
|
|
94fe3c90e0 | ||
|
|
f78332aa71 | ||
|
|
0c4eb0bb34 | ||
|
|
e70054d0c2 | ||
|
|
a75cf58f72 | ||
|
|
c859b43220 | ||
|
|
75ee2db02e | ||
|
|
f6efb3c89a | ||
|
|
b27047ed27 | ||
|
|
d43c8109d1 | ||
|
|
79f313827f | ||
|
|
67d8820cc9 | ||
|
|
9297e06cc4 | ||
|
|
faf44b0d4d | ||
|
|
4ebe0b6971 | ||
|
|
3cbeb4befa | ||
|
|
0005ad2abd | ||
|
|
a16b0c12d2 | ||
|
|
e0a6608b9d | ||
|
|
80a91bb2ad | ||
|
|
9a7970e8a0 | ||
|
|
05a82075cf | ||
|
|
d99a8be632 | ||
|
|
4882ce9c88 | ||
|
|
2d277d2d14 | ||
|
|
1fc6c49d4f | ||
|
|
6a3b2dde81 | ||
|
|
51a71bceb3 | ||
|
|
ee5ac2a502 | ||
|
|
b74d7719ea | ||
|
|
7887ad6ee4 | ||
|
|
012a6ead77 | ||
|
|
803dd2ff3a | ||
|
|
817aaab258 | ||
|
|
4d8d91846c | ||
|
|
e31e08e710 | ||
|
|
27847bf16c | ||
|
|
f2b1311ff7 | ||
|
|
48810af83d | ||
|
|
6a63256b5c | ||
|
|
07cf14a921 | ||
|
|
e30f6d9134 | ||
|
|
d77809060f | ||
|
|
d61d40ee5a | ||
|
|
99d539b040 | ||
|
|
7edf147112 | ||
|
|
39b81214c2 | ||
|
|
94fd6b5208 | ||
|
|
71e270fdf8 | ||
|
|
8125f1ba14 | ||
|
|
96e4a9a4a4 | ||
|
|
24d7187303 | ||
|
|
6af6c52f60 | ||
|
|
fdaf2fc760 | ||
|
|
fda621672d | ||
|
|
ce6cc771b4 | ||
|
|
e1e4476bee | ||
|
|
9582778adf | ||
|
|
5e6e4fa8d0 | ||
|
|
9e5a412fe2 | ||
|
|
763cb6e06c | ||
|
|
cbdbf41e1e | ||
|
|
cf630f7c2b | ||
|
|
3d6e01179a | ||
|
|
8260bda308 | ||
|
|
30e00e0707 | ||
|
|
88e2576334 | ||
|
|
076930c2c9 | ||
|
|
8a91e64bb9 |
26
.github/workflows/flake8.yml
vendored
Normal file
26
.github/workflows/flake8.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
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 }}
|
||||
14
.github/workflows/python-package.yml
vendored
14
.github/workflows/python-package.yml
vendored
@@ -1,7 +1,7 @@
|
||||
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||
|
||||
name: Python package
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -16,7 +16,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: [3.7, 3.8, 3.9]
|
||||
# python-version: [3.7, 3.8, 3.9]
|
||||
python-version: [3.8, 3.9]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -27,8 +28,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install flake8 pytest safety
|
||||
python -m pip install flake8 pytest safety stix2 pytest-mock
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
python -m pip install .
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
@@ -37,7 +39,5 @@ jobs:
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
- name: Safety checks
|
||||
run: safety check
|
||||
|
||||
# - name: Test with pytest
|
||||
# run: |
|
||||
# pytest
|
||||
- name: Test with pytest
|
||||
run: pytest
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -131,3 +131,9 @@ dmypy.json
|
||||
|
||||
# Temporal files
|
||||
*~
|
||||
|
||||
# IDEA Dev Environment
|
||||
.idea
|
||||
|
||||
# Sublime Text project files
|
||||
*.sublime*
|
||||
7
AUTHORS
7
AUTHORS
@@ -1,7 +0,0 @@
|
||||
MVT was originally authored by Claudio Guarnieri <nex@nex.sx>.
|
||||
|
||||
For an up-to-date list of all contributors visit:
|
||||
https://github.com/mvt-project/mvt/graphs/contributors
|
||||
|
||||
Or run:
|
||||
git shortlog -s -n
|
||||
82
Dockerfile
82
Dockerfile
@@ -2,48 +2,59 @@ FROM ubuntu:20.04
|
||||
|
||||
# Ref. https://github.com/mvt-project/mvt
|
||||
|
||||
LABEL url="https://mvt.re"
|
||||
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
|
||||
|
||||
# Fixing major OS dependencies
|
||||
# ----------------------------
|
||||
RUN apt update \
|
||||
&& apt install -y python3 python3-pip libusb-1.0-0-dev \
|
||||
&& apt install -y wget \
|
||||
&& apt install -y adb \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get -y install default-jre-headless
|
||||
&& apt install -y wget unzip\
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get -y install default-jre-headless \
|
||||
|
||||
# Install build tools for libimobiledevice
|
||||
# ----------------------------------------
|
||||
RUN apt install -y build-essential \
|
||||
checkinstall \
|
||||
git \
|
||||
autoconf \
|
||||
automake \
|
||||
libtool-bin \
|
||||
libplist-dev \
|
||||
libusbmuxd-dev \
|
||||
libssl-dev \
|
||||
sqlite3 \
|
||||
pkg-config
|
||||
build-essential \
|
||||
checkinstall \
|
||||
git \
|
||||
autoconf \
|
||||
automake \
|
||||
libtool-bin \
|
||||
libplist-dev \
|
||||
libusbmuxd-dev \
|
||||
libssl-dev \
|
||||
sqlite3 \
|
||||
pkg-config \
|
||||
|
||||
# Clean up
|
||||
# --------
|
||||
RUN apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* /var/cache/apt
|
||||
|
||||
|
||||
# Build libimobiledevice
|
||||
# ----------------------
|
||||
RUN git clone https://github.com/libimobiledevice/libplist
|
||||
RUN git clone https://github.com/libimobiledevice/libusbmuxd
|
||||
RUN git clone https://github.com/libimobiledevice/libimobiledevice
|
||||
RUN git clone https://github.com/libimobiledevice/usbmuxd
|
||||
RUN git clone https://github.com/libimobiledevice/libplist \
|
||||
&& git clone https://github.com/libimobiledevice/libimobiledevice-glue \
|
||||
&& git clone https://github.com/libimobiledevice/libusbmuxd \
|
||||
&& git clone https://github.com/libimobiledevice/libimobiledevice \
|
||||
&& git clone https://github.com/libimobiledevice/usbmuxd \
|
||||
|
||||
RUN cd libplist && ./autogen.sh && make && make install && ldconfig
|
||||
&& cd libplist && ./autogen.sh && make && make install && ldconfig \
|
||||
|
||||
RUN cd libusbmuxd && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./autogen.sh && make && make install && ldconfig
|
||||
&& cd ../libimobiledevice-glue && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./autogen.sh --prefix=/usr && make && make install && ldconfig \
|
||||
|
||||
RUN cd libimobiledevice && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./autogen.sh --enable-debug && make && make install && ldconfig
|
||||
&& cd ../libusbmuxd && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./autogen.sh && make && make install && ldconfig \
|
||||
|
||||
RUN cd usbmuxd && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./autogen.sh --prefix=/usr --sysconfdir=/etc --localstatedir=/var --runstatedir=/run && make && make install
|
||||
&& cd ../libimobiledevice && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./autogen.sh --enable-debug && make && make install && ldconfig \
|
||||
|
||||
&& cd ../usbmuxd && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./autogen.sh --prefix=/usr --sysconfdir=/etc --localstatedir=/var --runstatedir=/run && make && make install \
|
||||
|
||||
# Clean up.
|
||||
&& cd .. && rm -rf libplist libimobiledevice-glue libusbmuxd libimobiledevice usbmuxd
|
||||
|
||||
# Installing MVT
|
||||
# --------------
|
||||
@@ -51,16 +62,29 @@ RUN pip3 install mvt
|
||||
|
||||
# Installing ABE
|
||||
# --------------
|
||||
RUN mkdir /opt/abe
|
||||
RUN wget https://github.com/nelenkov/android-backup-extractor/releases/download/20210709062403-4c55371/abe.jar -O /opt/abe/abe.jar
|
||||
RUN mkdir /opt/abe \
|
||||
&& wget https://github.com/nelenkov/android-backup-extractor/releases/download/20210709062403-4c55371/abe.jar -O /opt/abe/abe.jar \
|
||||
# Create alias for abe
|
||||
RUN echo 'alias abe="java -jar /opt/abe/abe.jar"' >> ~/.bashrc
|
||||
&& 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
|
||||
|
||||
# Setup investigations environment
|
||||
# --------------------------------
|
||||
RUN mkdir /home/cases
|
||||
WORKDIR /home/cases
|
||||
RUN echo 'echo "Mobile Verification Toolkit @ Docker\n------------------------------------\n\nYou can find information about how to use this image for Android (https://github.com/mvt-project/mvt/tree/master/docs/android) and iOS (https://github.com/mvt-project/mvt/tree/master/docs/ios) in the official docs of the project.\n"' >> ~/.bashrc
|
||||
RUN echo 'echo "Note that to perform the debug via USB you might need to give the Docker image access to the USB using \"docker run -it --privileged -v /dev/bus/usb:/dev/bus/usb mvt\" or, preferably, the \"--device=\" parameter.\n"' >> ~/.bashrc
|
||||
RUN echo 'echo "Mobile Verification Toolkit @ Docker\n------------------------------------\n\nYou can find information about how to use this image for Android (https://github.com/mvt-project/mvt/tree/master/docs/android) and iOS (https://github.com/mvt-project/mvt/tree/master/docs/ios) in the official docs of the project.\n"' >> ~/.bashrc \
|
||||
&& echo 'echo "Note that to perform the debug via USB you might need to give the Docker image access to the USB using \"docker run -it --privileged -v /dev/bus/usb:/dev/bus/usb mvt\" or, preferably, the \"--device=\" parameter.\n"' >> ~/.bashrc
|
||||
|
||||
CMD /bin/bash
|
||||
|
||||
15
README.md
15
README.md
@@ -1,11 +1,13 @@
|
||||
<p align="center">
|
||||
<img src="./docs/mvt.png" width="300" />
|
||||
<img src="https://docs.mvt.re/en/latest/mvt.png" width="200" />
|
||||
</p>
|
||||
|
||||
# Mobile Verification Toolkit
|
||||
|
||||
[](https://pypi.org/project/mvt/)
|
||||
[](https://docs.mvt.re/en/latest/?badge=latest)
|
||||
[](https://github.com/mvt-project/mvt/actions/workflows/python-package.yml)
|
||||
[](https://pepy.tech/project/mvt)
|
||||
|
||||
Mobile Verification Toolkit (MVT) is a collection of utilities to simplify and automate the process of gathering forensic traces helpful to identify a potential compromise of Android and iOS devices.
|
||||
|
||||
@@ -13,22 +15,23 @@ It has been developed and released by the [Amnesty International Security Lab](h
|
||||
|
||||
*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.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
MVT can be installed from sources or from [PyPi](https://pypi.org/project/mvt/) (you will need some dependencies, check the [documentation](https://docs.mvt.re/en/latest/install.html)):
|
||||
MVT can be installed from sources or from [PyPi](https://pypi.org/project/mvt/) (you will need some dependencies, check the [documentation](https://docs.mvt.re/en/latest/install/)):
|
||||
|
||||
```
|
||||
pip3 install mvt
|
||||
```
|
||||
|
||||
Alternatively, you can decide to run MVT and all relevant tools through a [Docker container](https://docs.mvt.re/en/latest/docker.html).
|
||||
For alternative installation options and known issues, please refer to the [documentation](https://docs.mvt.re/en/latest/install/) as well as [GitHub Issues](https://github.com/mvt-project/mvt/issues).
|
||||
|
||||
**Please note:** MVT is best run on Linux or Mac systems. [It does not currently support running natively on Windows.](https://docs.mvt.re/en/latest/install.html#mvt-on-windows)
|
||||
|
||||
## Usage
|
||||
|
||||
MVT provides two commands `mvt-ios` and `mvt-android`. [Check out the documentation to learn how to use them!](https://docs.mvt.re/).
|
||||
MVT provides two commands `mvt-ios` and `mvt-android`. [Check out the documentation to learn how to use them!](https://docs.mvt.re/)
|
||||
|
||||
|
||||
## License
|
||||
|
||||
The purpose of MVT is to facilitate the ***consensual forensic analysis*** of devices of those who might be targets of sophisticated mobile spyware attacks, especially members of civil society and marginalized communities. We do not want MVT to enable privacy violations of non-consenting individuals. In order to achieve this, MVT is released under its own license. [Read more here.](https://docs.mvt.re/en/latest/license.html)
|
||||
The purpose of MVT is to facilitate the ***consensual forensic analysis*** of devices of those who might be targets of sophisticated mobile spyware attacks, especially members of civil society and marginalized communities. We do not want MVT to enable privacy violations of non-consenting individuals. In order to achieve this, MVT is released under its own license. [Read more here.](https://docs.mvt.re/en/latest/license/)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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/
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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/
|
||||
|
||||
|
||||
42
docs/android/adb.md
Normal file
42
docs/android/adb.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Check over ADB
|
||||
|
||||
In order to check an Android device over the [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb) you will first need to install [Android SDK Platform Tools](https://developer.android.com/studio/releases/platform-tools). If you have installed [Android Studio](https://developer.android.com/studio/) you should already have access to `adb` and other utilities.
|
||||
|
||||
While many Linux distributions already package Android Platform Tools (for example `android-platform-tools-base` on Debian), it is preferable to install the most recent version from the official website. Packaged versions might be outdated and incompatible with most recent Android handsets.
|
||||
|
||||
Next you will need to enable debugging on the Android device you are testing. [Please follow the official instructions on how to do so.](https://developer.android.com/studio/command-line/adb)
|
||||
|
||||
## Connecting over USB
|
||||
|
||||
The easiest way to check the device is over a USB transport. You will need to have USB debugging enabled and the device plugged into your computer. If everything is configured appropriately you should see your device when launching the command `adb devices`.
|
||||
|
||||
Now you can try launching MVT with:
|
||||
|
||||
```bash
|
||||
mvt-android check-adb --output /path/to/results
|
||||
```
|
||||
|
||||
If you have previously started an adb daemon MVT will alert you and require you to kill it with `adb kill-server` and relaunch the command.
|
||||
|
||||
!!! warning
|
||||
MVT relies on the Python library [adb-shell](https://pypi.org/project/adb-shell/) to connect to an Android device, which relies on libusb for the USB transport. Because of known driver issues, Windows users [are recommended](https://github.com/JeffLIrion/adb_shell/issues/118) to install appropriate drivers using [Zadig](https://zadig.akeo.ie/). Alternatively, an easier option might be to use the TCP transport and connect over Wi-Fi as describe next.
|
||||
|
||||
## Connecting over Wi-FI
|
||||
|
||||
When connecting to the device over USB is not possible or not working properly, an alternative option is to connect over the network. In order to do so, first launch an adb daemon at a fixed port number:
|
||||
|
||||
```bash
|
||||
adb tcpip 5555
|
||||
```
|
||||
|
||||
Then you can specify the IP address of the phone with the adb port number to MVT like so:
|
||||
|
||||
```bash
|
||||
mvt-android check-adb --serial 192.168.1.20:5555 --output /path/to/results
|
||||
```
|
||||
|
||||
Where `192.168.1.20` is the correct IP address of your device.
|
||||
|
||||
## MVT modules requiring root privileges
|
||||
|
||||
Of the currently available `mvt-android check-adb` modules a handful require root privileges to function correctly. This is because certain files, such as browser history and SMS messages databases are not accessible with user privileges through adb. These modules are to be considered OPTIONALLY available in case the device was already jailbroken. **Do NOT jailbreak your own device unless you are sure of what you are doing!** Jailbreaking your phone exposes it to considerable security risks!
|
||||
@@ -1,38 +1,55 @@
|
||||
# Checking SMSs from Android backup
|
||||
# Check an Android Backup (SMS messages)
|
||||
|
||||
Some attacks against Android phones are done by sending malicious links by SMS. The Android backup feature does not allow to gather much information that can be interesting for a forensic analysis, but it can be used to extract SMSs and check them with MVT.
|
||||
Android supports generating a backup archive of all the installed applications which supports it. However, over the years this functionality has been increasingly abandoned in favor of enabling users to remotely backup their personal data over the cloud. App developers can therefore decide to opt out from allowing the apps' data from being exported locally.
|
||||
|
||||
To do so, you need to connect your Android device to your computer. You will then need to [enable USB debugging](https://developer.android.com/studio/debug/dev-options#enable>) on the Android device.
|
||||
At the time of writing, the Android Debug Bridge (adb) command to generate backups is still available but marked as deprecated.
|
||||
|
||||
If this is the first time you connect to this device, you will need to approve the authentication keys through a prompt that will appear on your Android device.
|
||||
That said, most versions of Android should still allow to locally backup SMS messages, and since messages are still a prime vehicle for phishing and malware attacks, you might still want to take advantage of this functionality while it is supported.
|
||||
|
||||
Then you can use adb to extract the backup for SMS only with the following command:
|
||||
## Generate a backup
|
||||
|
||||
Because `mvt-android check-backup` currently only supports checking SMS messages, you can indicate to backup only those:
|
||||
|
||||
```bash
|
||||
adb backup com.android.providers.telephony
|
||||
adb backup -nocompress com.android.providers.telephony
|
||||
```
|
||||
|
||||
You will need to approve the backup on the phone and potentially enter a password to encrypt the backup. The backup will then be stored in a file named `backup.ab`.
|
||||
In case you nonetheless wish to take a full backup, you can do so with
|
||||
|
||||
You will need to use [Android Backup Extractor](https://github.com/nelenkov/android-backup-extractor) to convert it to a readable file format. Make sure that java is installed on your system and use the following command:
|
||||
```bash
|
||||
java -jar ~/Download/abe.jar unpack backup.ab backup.tar
|
||||
adb backup -nocompress -all
|
||||
```
|
||||
|
||||
Some recent phones will enforce the utilisation of a password to encrypt the backup archive. In that case, the password will obviously be needed to extract and analyse the data later on.
|
||||
|
||||
## Unpack and check the backup
|
||||
|
||||
MVT includes a partial implementation of the Android Backup parsing, because of the implementation difference in the compression algorithm between Java and Python. The `-nocompress` option passed to adb in the section above allows to avoid this issue. You can analyse and extract SMSs containing links from the backup directly with MVT:
|
||||
|
||||
```bash
|
||||
$ mvt-android check-backup --output /path/to/results/ /path/to/backup.ab
|
||||
14:09:45 INFO [mvt.android.cli] Checking ADB backup located at: backup.ab
|
||||
INFO [mvt.android.modules.backup.sms] Running module SMS...
|
||||
INFO [mvt.android.modules.backup.sms] Processing SMS backup file at
|
||||
apps/com.android.providers.telephony/d_f/000000_sms_backup
|
||||
INFO [mvt.android.modules.backup.sms] Extracted a total of 64 SMS messages containing links
|
||||
```
|
||||
|
||||
If the backup is encrypted, MVT will prompt you to enter the password.
|
||||
|
||||
Through the `--iocs` argument you can specify a [STIX2](https://oasis-open.github.io/cti-documentation/stix/intro) file defining a list of malicious indicators to check against the records extracted from the backup by MVT. Any matches will be highlighted in the terminal output.
|
||||
|
||||
## Alternative ways to unpack and check the backup
|
||||
|
||||
If you encounter an issue during the analysis of the backup, you can alternatively use [Android Backup Extractor (ABE)](https://github.com/nelenkov/android-backup-extractor) to convert it to a readable file format. Make sure that java is installed on your system and use the following command:
|
||||
|
||||
```bash
|
||||
java -jar ~/path/to/abe.jar unpack backup.ab backup.tar
|
||||
tar xvf backup.tar
|
||||
```
|
||||
|
||||
(If the backup is encrypted, the password will be asked by Android Backup Extractor).
|
||||
If the backup is encrypted, ABE will prompt you to enter the password.
|
||||
|
||||
You can then extract SMSs containing links with MVT:
|
||||
Alternatively, [ab-decrypt](https://github.com/joernheissler/ab-decrypt) can be used for that purpose.
|
||||
|
||||
```bash
|
||||
$ mvt-android check-backup --output . .
|
||||
16:18:38 INFO [mvt.android.cli] Checking ADB backup located at: .
|
||||
INFO [mvt.android.modules.backup.sms] Running module SMS...
|
||||
INFO [mvt.android.modules.backup.sms] Processing SMS backup
|
||||
file at ./apps/com.android.providers.telephony/d_f/000
|
||||
000_sms_backup
|
||||
16:18:39 INFO [mvt.android.modules.backup.sms] Extracted a total of
|
||||
64 SMS messages containing links
|
||||
```
|
||||
|
||||
Through the `--iocs` argument you can specify a [STIX2](https://oasis-open.github.io/cti-documentation/stix/intro) file defining a list of malicious indicators to check against the records extracted from the backup by mvt. Any matches will be highlighted in the terminal output.
|
||||
You can then extract SMSs containing links with MVT by passing the folder path as parameter instead of the `.ab` file: `mvt-android check-backup --output /path/to/results/ /path/to/backup/` (the path to backup given should be the folder containing the `apps` folder).
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
# Downloading APKs from an Android phone
|
||||
|
||||
In order to use `mvt-android` you need to connect your Android device to your computer. You will then need to [enable USB debugging](https://developer.android.com/studio/debug/dev-options#enable>) on the Android device.
|
||||
MVT allows to attempt to download all available installed packages (APKs) in order to further inspect them and potentially identify any which might be malicious in nature.
|
||||
|
||||
If this is the first time you connect to this device, you will need to approve the authentication keys through a prompt that will appear on your Android device.
|
||||
|
||||
Now you can launch `mvt-android` and specify the `download-apks` command and the path to the folder where you want to store the extracted data:
|
||||
You can do so by launching the following command:
|
||||
|
||||
```bash
|
||||
mvt-android download-apks --output /path/to/folder
|
||||
```
|
||||
|
||||
Optionally, you can decide to enable lookups of the SHA256 hash of all the extracted APKs on [VirusTotal](https://www.virustotal.com) and/or [Koodous](https://koodous.com). While these lookups do not provide any conclusive assessment on all of the extracted APKs, they might highlight any known malicious ones:
|
||||
It might take several minutes to complete.
|
||||
|
||||
!!! info
|
||||
MVT will likely warn you it was unable to download certain installed packages. There is no reason to be alarmed: this is typically expected behavior when MVT attempts to download a system package it has no privileges to access.
|
||||
|
||||
Optionally, you can decide to enable lookups of the SHA256 hash of all the extracted APKs on [VirusTotal](https://www.virustotal.com). While these lookups do not provide any conclusive assessment on all of the extracted APKs, they might highlight any known malicious ones:
|
||||
|
||||
```bash
|
||||
mvt-android download-apks --output /path/to/folder --virustotal
|
||||
mvt-android download-apks --output /path/to/folder --koodous
|
||||
MVT_VT_API_KEY=<key> mvt-android download-apks --output /path/to/folder --virustotal
|
||||
```
|
||||
|
||||
Or, to launch all available lookups::
|
||||
Please note that in order to use VirusTotal lookups you are required to provide your own API key through the `MVT_VT_API_KEY` environment variable. You should also note that VirusTotal enforces strict API usage. Be mindful that MVT might consume your hourly search quota.
|
||||
|
||||
In case you have a previous extraction of APKs you want to later check against VirusTotal, you can do so with the following arguments:
|
||||
|
||||
```bash
|
||||
mvt-android download-apks --output /path/to/folder --all-checks
|
||||
MVT_VT_API_KEY=<key> mvt-android download-apks --from-file /path/to/folder/apks.json --virustotal
|
||||
```
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
# Methodology for Android forensic
|
||||
|
||||
TODO
|
||||
Unfortunately Android devices provide much less observability than their iOS cousins. Android stores very little diagnostic information useful to triage potential compromises, and because of this `mvt-android` capabilities are limited as well.
|
||||
|
||||
However, not all is lost.
|
||||
|
||||
## Check installed Apps
|
||||
|
||||
Because malware attacks over Android typically take the form of malicious or backdoored apps, the very first thing you might want to do is to extract and verify all installed Android packages and triage quickly if there are any which stand out as malicious or which might be atypical.
|
||||
|
||||
While it is out of the scope of this documentation to dwell into details on how to analyze Android apps, MVT does allow to easily and automatically extract information about installed apps, download copies of them, and quickly look them up on services such as [VirusTotal](https://www.virustotal.com).
|
||||
|
||||
!!! info "Using VirusTotal"
|
||||
Please note that in order to use VirusTotal lookups you are required to provide your own API key through the `MVT_VT_API_KEY` environment variable. You should also note that VirusTotal enforces strict API usage. Be mindful that MVT might consume your hourly search quota.
|
||||
|
||||
## Check the device over Android Debug Bridge
|
||||
|
||||
Some additional diagnostic information can be extracted from the phone using the [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb). `mvt-android` allows to automatically extract information including [dumpsys](https://developer.android.com/studio/command-line/dumpsys) results, details on installed packages (without download), running processes, presence of root binaries and packages, and more.
|
||||
|
||||
|
||||
## Check an Android Backup (SMS messages)
|
||||
|
||||
Although Android backups are becoming deprecated, it is still possible to generate one. Unfortunately, because apps these days typically favor backup over the cloud, the amount of data available is limited. Currently, `mvt-android check-backup` only supports checking SMS messages containing links.
|
||||
|
||||
@@ -10,6 +10,11 @@ cd mvt
|
||||
docker build -t mvt .
|
||||
```
|
||||
|
||||
Optionally, you may need to specify your platform to Docker in order to build successfully (Apple M1)
|
||||
```bash
|
||||
docker build --platform amd64 -t mvt .
|
||||
```
|
||||
|
||||
Test if the image was created successfully:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -10,4 +10,4 @@ In this documentation you will find instructions on how to install and run the `
|
||||
|
||||
## Resources
|
||||
|
||||
[:fontawesome-brands-python: Python Package](https://pypi.org/project/mvt){: .md-button .md-button--primary } [:fontawesome-brands-github: GitHub](https://github.com/mvt-project/mvt){: .md-button }
|
||||
[:fontawesome-brands-github: GitHub](https://github.com/mvt-project/mvt){: .md-button .md-button--primary } [:fontawesome-brands-python: Python Package](https://pypi.org/project/mvt){: .md-button }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Installation
|
||||
|
||||
Before proceeding, please note that mvt requires Python 3.6+ to run. While it should be available on most operating systems, please make sure of that before proceeding.
|
||||
Before proceeding, please note that MVT requires Python 3.6+ to run. While it should be available on most operating systems, please make sure of that before proceeding.
|
||||
|
||||
## Dependencies on Linux
|
||||
|
||||
@@ -14,9 +14,9 @@ sudo apt install python3 python3-pip libusb-1.0-0 sqlite3
|
||||
|
||||
When working with Android devices you should additionally install [Android SDK Platform Tools](https://developer.android.com/studio/releases/platform-tools). If you prefer to install a package made available by your distribution of choice, please make sure the version is recent to ensure compatibility with modern Android devices.
|
||||
|
||||
## Dependencies on Mac
|
||||
## Dependencies on macOS
|
||||
|
||||
Running MVT on Mac requires Xcode and [homebrew](https://brew.sh) to be installed.
|
||||
Running MVT on macOS requires Xcode and [homebrew](https://brew.sh) to be installed.
|
||||
|
||||
In order to install dependencies use:
|
||||
|
||||
@@ -26,7 +26,7 @@ brew install python3 libusb sqlite3
|
||||
|
||||
*libusb* is not required if you intend to only use `mvt-ios` and not `mvt-android`.
|
||||
|
||||
When working with Android devices you should additionally install Android SDK Platform Tools:
|
||||
When working with Android devices you should additionally install [Android SDK Platform Tools](https://developer.android.com/studio/releases/platform-tools):
|
||||
|
||||
```bash
|
||||
brew install --cask android-platform-tools
|
||||
|
||||
@@ -10,6 +10,8 @@ Mobile Verification Toolkit (MVT) is a collection of utilities designed to facil
|
||||
- Generate JSON logs of extracted records, and separate JSON logs of all detected malicious traces.
|
||||
- Generate a unified chronological timeline of extracted records, along with a timeline all detected malicious traces.
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
16
docs/iocs.md
16
docs/iocs.md
@@ -22,11 +22,25 @@ After extracting forensics data from a device, you are also able to compare it w
|
||||
mvt-ios check-iocs --iocs ~/iocs/malware.stix2 /path/to/iphone/output/
|
||||
```
|
||||
|
||||
If you're looking for indicators of compromise for a specific piece of malware or adversary, please ask investigators or anti-malware researchers who have the relevant expertise for a STIX file.
|
||||
The `--iocs` option can be invoked multiple times to let MVT import multiple STIX2 files at once. For example:
|
||||
|
||||
```bash
|
||||
mvt-ios check-backup --iocs ~/iocs/malware1.stix --iocs ~/iocs/malware2.stix2 /path/to/backup
|
||||
```
|
||||
|
||||
It is also possible to load STIX2 files automatically from the environment variable `MVT_STIX2`:
|
||||
|
||||
```bash
|
||||
export MVT_STIX2="/home/user/IOC1.stix2:/home/user/IOC2.stix2"
|
||||
```
|
||||
|
||||
## Known repositories of STIX2 IOCs
|
||||
|
||||
- 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))
|
||||
- [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).
|
||||
|
||||
You can automaticallly download the latest public indicator files with the command `mvt-ios download-iocs` or `mvt-android download-iocs`. These commands download the list of indicators listed [here](https://github.com/mvt-project/mvt/blob/main/public_indicators.json) and store them in the [appdir](https://pypi.org/project/appdirs/) folder. They are then loaded automatically by MVT.
|
||||
|
||||
Please [open an issue](https://github.com/mvt-project/mvt/issues/) to suggest new sources of STIX-formatted IOCs.
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# Backup with iTunes app
|
||||
|
||||
It is possible to do an iPhone backup by using iTunes on Windows or Mac computers (in most recent versions of Mac OS, 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).
|
||||
|
||||
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 Mac OS).
|
||||
* 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).
|
||||
|
||||

|
||||
_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 Mac OS, 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/`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Install libimobiledevice
|
||||
|
||||
Before proceeding with doing any acquisition of iOS devices we recommend installing [libimobiledevice](https://www.libimobiledevice.org/) utilities. These utilities will become useful when extracting crash logs and generating iTunes backups. Because the utilities and its libraries are subject to frequent changes in response to new versions of iOS, you might want to consider compiling libimobiledevice utilities from sources. Otherwise, if available, you can try installing packages available in your distribution:
|
||||
Before proceeding with doing any acquisition of iOS devices we recommend installing [libimobiledevice](https://libimobiledevice.org/) utilities. These utilities will become useful when extracting crash logs and generating iTunes backups. Because the utilities and its libraries are subject to frequent changes in response to new versions of iOS, you might want to consider compiling libimobiledevice utilities from sources. Otherwise, if available, you can try installing packages available in your distribution:
|
||||
|
||||
```bash
|
||||
sudo apt install libimobiledevice-utils
|
||||
|
||||
@@ -12,4 +12,4 @@ If you are not expected to return the phone, you might want to consider to attem
|
||||
|
||||
#### iTunes Backup
|
||||
|
||||
An alternative option is to generate an iTunes backup (in most recent version of mac OS, they are no longer launched from iTunes, but directly from Finder). While backups only provide a subset of the files stored on the device, in many cases it might be sufficient to at least detect some suspicious artifacts. Backups encrypted with a password will have some additional interesting records not available in unencrypted ones, such as Safari history, Safari state, etc.
|
||||
An alternative option is to generate an iTunes backup (in most recent version of macOS, they are no longer launched from iTunes, but directly from Finder). While backups only provide a subset of the files stored on the device, in many cases it might be sufficient to at least detect some suspicious artifacts. Backups encrypted with a password will have some additional interesting records not available in unencrypted ones, such as Safari history, Safari state, etc.
|
||||
|
||||
@@ -4,10 +4,32 @@ In this page you can find a (reasonably) up-to-date breakdown of the files creat
|
||||
|
||||
## Records extracted by `check-fs` or `check-backup`
|
||||
|
||||
### `analytics.json`
|
||||
|
||||
!!! info "Availability"
|
||||
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.
|
||||
|
||||
If indicators are provided through the command-line, processes and domains are checked against all fields of the plist. Any matches are stored in *analytics_detected.json*.
|
||||
|
||||
---
|
||||
|
||||
### `backup_info.json`
|
||||
|
||||
!!! info "Availability"
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
### `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.
|
||||
@@ -19,7 +41,7 @@ If indicators are provided through the command-line, they are checked against th
|
||||
### `calls.json`
|
||||
|
||||
!!! info "Availability"
|
||||
Backup: :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.
|
||||
@@ -29,7 +51,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.
|
||||
@@ -41,19 +63,31 @@ 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.
|
||||
|
||||
If indicators a provided through the command-line, they are checked against the visited URL. Any matches are stored in *chrome_history_detected.json*.
|
||||
If indicators are provided through the command-line, they are checked against the visited URL. Any matches are stored in *chrome_history_detected.json*.
|
||||
|
||||
---
|
||||
|
||||
### `configuration_profiles.json`
|
||||
|
||||
!!! info "Availability"
|
||||
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.
|
||||
|
||||
If indicators are provided through the command-line, they are checked against the configuration profile UUID to identify any known malicious profiles. Any matches are stored in *configuration_profiles_detected.json*.
|
||||
|
||||
---
|
||||
|
||||
### `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.
|
||||
@@ -63,7 +97,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.
|
||||
@@ -75,29 +109,41 @@ 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.
|
||||
|
||||
If indicators a provided through the command-line, they are checked against the visited URL. Any matches are stored in *firefox_history_detected.json*.
|
||||
If indicators are provided through the command-line, they are checked against the visited URL. Any matches are stored in *firefox_history_detected.json*.
|
||||
|
||||
---
|
||||
|
||||
### `id_status_cache.json`
|
||||
|
||||
!!! info "Availability"
|
||||
Backup: :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.
|
||||
|
||||
Starting from iOS 14.7.0, this file is empty or absent.
|
||||
|
||||
---
|
||||
|
||||
### `shortcuts.json`
|
||||
|
||||
!!! info "Availability"
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
### `interaction_c.json`
|
||||
|
||||
!!! info "Availability"
|
||||
Backup: :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.
|
||||
@@ -107,7 +153,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.
|
||||
@@ -117,7 +163,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.
|
||||
@@ -126,13 +172,25 @@ If indicators are provided through the command-line, they are checked against th
|
||||
|
||||
---
|
||||
|
||||
### `os_analytics_ad_daily.json`
|
||||
|
||||
!!! info "Availability"
|
||||
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.
|
||||
|
||||
If indicators are provided through the command-line, they are checked against the process names. Any matches are stored in *os_analytics_ad_daily_detected.json*.
|
||||
|
||||
---
|
||||
|
||||
### `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 data usage by processes running on the system. Besides the network statistics, these records are particularly important because they might show traces of malicious process executions and the relevant timeframe. In particular, processes which do not have a valid bundle ID might require particular attention.
|
||||
This JSON file is created by mvt-ios' `Datausage` module. The module extracts records from a SQLite database located */private/var/wireless/Library/Databases/DataUsage.sqlite*, which contains a history of network data usage by processes running on the system. It does not log network traffic through WiFi (the fields `WIFI_IN` and `WIFI_OUT` are always empty), and the `WWAN_IN` and `WWAN_OUT` fields are stored in bytes. Besides the network statistics, these records are particularly important because they might show traces of malicious process executions and the relevant timeframe. In particular, processes which do not have a valid bundle ID might require particular attention.
|
||||
|
||||
If indicators are provided through the command-line, they are checked against the process names. Any matches are stored in *datausage_detected.json*. If running on a full filesystem dump and if the `--fast` flag was not enabled by command-line, mvt-ios will highlight processes which look suspicious and check the presence of a binary file of the same name in the dump.
|
||||
|
||||
@@ -141,7 +199,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.
|
||||
@@ -150,22 +208,32 @@ If indicators are provided through the command-line, they are checked against th
|
||||
|
||||
---
|
||||
|
||||
### `profile_events.json`
|
||||
|
||||
!!! info "Availability"
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
### `safari_browser_state.json`
|
||||
|
||||
!!! info "Availability"
|
||||
Backup: :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.
|
||||
|
||||
If indicators a provided through the command-line, they are checked against the visited URL. Any matches are stored in *safari_browser_state_detected.json*.
|
||||
If indicators are provided through the command-line, they are checked against the visited URL. Any matches are stored in *safari_browser_state_detected.json*.
|
||||
|
||||
---
|
||||
|
||||
### `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.
|
||||
@@ -177,7 +245,7 @@ If indicators are provided through the command-line, they are checked against bo
|
||||
### `safari_history.json`
|
||||
|
||||
!!! info "Availability"
|
||||
Backup: :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.
|
||||
@@ -186,10 +254,22 @@ If indicators are provided through the command-line, they are checked against th
|
||||
|
||||
---
|
||||
|
||||
### `shutdown_log.json`
|
||||
|
||||
!!! info "Availability"
|
||||
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.
|
||||
|
||||
If indicators are provided through the command-line, they are checked against the paths. Any matches are stored in *shutdown_log_detected.json*.
|
||||
|
||||
---
|
||||
|
||||
### `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*.
|
||||
@@ -201,17 +281,27 @@ 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.
|
||||
|
||||
---
|
||||
|
||||
### `tcc.json`
|
||||
|
||||
!!! info "Availability"
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
### `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*.
|
||||
@@ -221,7 +311,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.
|
||||
@@ -233,7 +323,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.
|
||||
@@ -242,10 +332,22 @@ If indicators are provided through the command-line, they are checked against th
|
||||
|
||||
---
|
||||
|
||||
### `webkit_resource_load_statistics.json`
|
||||
|
||||
!!! info "Availability"
|
||||
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.
|
||||
|
||||
If indicators are provided through the command-line, they are checked against the extracted domain names. Any matches are stored in *webkit_resource_load_statistics_detected.json*.
|
||||
|
||||
---
|
||||
|
||||
### `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.
|
||||
@@ -257,7 +359,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.
|
||||
@@ -269,7 +371,7 @@ 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*.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
mkdocs==1.2.1
|
||||
mkdocs==1.2.3
|
||||
mkdocs-autorefs
|
||||
mkdocs-material
|
||||
mkdocs-material-extensions
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
site_name: Mobile Verification Toolkit
|
||||
repo_url: https://github.com/mvt-project/mvt
|
||||
edit_uri: edit/main/docs/
|
||||
copyright: Copyright © 2021 MVT Project Developers
|
||||
copyright: Copyright © 2021-2022 MVT Project Developers
|
||||
site_description: Mobile Verification Toolkit Documentation
|
||||
markdown_extensions:
|
||||
- attr_list
|
||||
@@ -42,7 +42,8 @@ nav:
|
||||
- Records extracted by mvt-ios: "ios/records.md"
|
||||
- MVT for Android:
|
||||
- Android Forensic Methodology: "android/methodology.md"
|
||||
- Check APKs: "android/download_apks.md"
|
||||
- Check an Android Backup: "android/backup.md"
|
||||
- Check over ADB: "android/adb.md"
|
||||
- Check an Android Backup (SMS messages): "android/backup.md"
|
||||
- Download APKs: "android/download_apks.md"
|
||||
- Indicators of Compromise: "iocs.md"
|
||||
- License: "license.md"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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/
|
||||
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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 argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import click
|
||||
from rich.logging import RichHandler
|
||||
|
||||
from mvt.common.indicators import Indicators
|
||||
from mvt.common.module import run_module, save_timeline
|
||||
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.logo import logo
|
||||
from mvt.common.updates import IndicatorsUpdates
|
||||
|
||||
from .download_apks import DownloadAPKs
|
||||
from .lookups.koodous import koodous_lookup
|
||||
from .lookups.virustotal import virustotal_lookup
|
||||
from .cmd_check_adb import CmdAndroidCheckADB
|
||||
from .cmd_check_backup import CmdAndroidCheckBackup
|
||||
from .cmd_check_bugreport import CmdAndroidCheckBugreport
|
||||
from .cmd_download_apks import DownloadAPKs
|
||||
from .modules.adb import ADB_MODULES
|
||||
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"
|
||||
@@ -26,160 +31,189 @@ logging.basicConfig(level="INFO", format=LOG_FORMAT, handlers=[
|
||||
RichHandler(show_path=False, log_time_format="%X")])
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Help messages of repeating options.
|
||||
OUTPUT_HELP_MESSAGE = "Specify a path to a folder where you want to store JSON results"
|
||||
SERIAL_HELP_MESSAGE = "Specify a device serial number or HOST:PORT connection string"
|
||||
|
||||
#==============================================================================
|
||||
# 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
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# Download APKs
|
||||
# Command: download-apks
|
||||
#==============================================================================
|
||||
@cli.command("download-apks", help="Download all or non-safelisted installed APKs installed on the device")
|
||||
@click.option("--serial", "-s", type=str, help=SERIAL_HELP_MESSAGE)
|
||||
@cli.command("download-apks", help="Download all or only non-system installed APKs")
|
||||
@click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL)
|
||||
@click.option("--all-apks", "-a", is_flag=True,
|
||||
help="Extract all packages installed on the phone, even those marked as safe")
|
||||
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("--koodous", "-k", is_flag=True, help="Check packages on Koodous")
|
||||
@click.option("--all-checks", "-A", is_flag=True, help="Run all available checks")
|
||||
@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)")
|
||||
def download_apks(all_apks, virustotal, koodous, all_checks, output, from_file, serial):
|
||||
@click.pass_context
|
||||
def download_apks(ctx, all_apks, virustotal, output, from_file, serial):
|
||||
try:
|
||||
if from_file:
|
||||
download = DownloadAPKs.from_json(from_file)
|
||||
else:
|
||||
if output and not os.path.exists(output):
|
||||
# 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)
|
||||
|
||||
if not os.path.exists(output):
|
||||
try:
|
||||
os.makedirs(output)
|
||||
except Exception as e:
|
||||
log.critical("Unable to create output folder %s: %s", output, e)
|
||||
sys.exit(-1)
|
||||
ctx.exit(1)
|
||||
|
||||
download = DownloadAPKs(output_folder=output, all_apks=all_apks)
|
||||
download = DownloadAPKs(output_folder=output, all_apks=all_apks,
|
||||
log=logging.getLogger(DownloadAPKs.__module__))
|
||||
if serial:
|
||||
download.serial = serial
|
||||
download.run()
|
||||
|
||||
packages = download.packages
|
||||
packages_to_lookup = []
|
||||
if all_apks:
|
||||
packages_to_lookup = download.packages
|
||||
else:
|
||||
for package in download.packages:
|
||||
if not package.get("system", False):
|
||||
packages_to_lookup.append(package)
|
||||
|
||||
if len(packages) == 0:
|
||||
return
|
||||
if len(packages_to_lookup) == 0:
|
||||
return
|
||||
|
||||
if virustotal or all_checks:
|
||||
virustotal_lookup(packages)
|
||||
|
||||
if koodous or all_checks:
|
||||
koodous_lookup(packages)
|
||||
if virustotal:
|
||||
m = Packages()
|
||||
m.check_virustotal(packages_to_lookup)
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
sys.exit(-1)
|
||||
ctx.exit(1)
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# Checks through ADB
|
||||
# Command: check-adb
|
||||
#==============================================================================
|
||||
@cli.command("check-adb", help="Check an Android device over adb")
|
||||
@click.option("--serial", "-s", type=str, help=SERIAL_HELP_MESSAGE)
|
||||
@click.option("--iocs", "-i", type=click.Path(exists=True), help="Path to indicators file")
|
||||
@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="Specify a path to a folder where you want to store JSON results")
|
||||
@click.option("--list-modules", "-l", is_flag=True, help="Print list of available modules and exit")
|
||||
@click.option("--module", "-m", help="Name of a single module you would like to run instead of all")
|
||||
def check_adb(iocs, output, list_modules, module, serial):
|
||||
if list_modules:
|
||||
log.info("Following is the list of available check-adb modules:")
|
||||
for adb_module in ADB_MODULES:
|
||||
log.info(" - %s", adb_module.__name__)
|
||||
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.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)
|
||||
|
||||
if list_modules:
|
||||
cmd.list_modules()
|
||||
return
|
||||
|
||||
log.info("Checking Android through adb bridge")
|
||||
log.info("Checking Android device over debug bridge")
|
||||
|
||||
if output and not os.path.exists(output):
|
||||
try:
|
||||
os.makedirs(output)
|
||||
except Exception as e:
|
||||
log.critical("Unable to create output folder %s: %s", output, e)
|
||||
sys.exit(-1)
|
||||
cmd.run()
|
||||
|
||||
if iocs:
|
||||
# Pre-load indicators for performance reasons.
|
||||
log.info("Loading indicators from provided file at %s", iocs)
|
||||
indicators = Indicators(iocs)
|
||||
if len(cmd.timeline_detected) > 0:
|
||||
log.warning("The analysis of the Android device produced %d detections!",
|
||||
len(cmd.timeline_detected))
|
||||
|
||||
timeline = []
|
||||
timeline_detected = []
|
||||
for adb_module in ADB_MODULES:
|
||||
if module and adb_module.__name__ != module:
|
||||
continue
|
||||
|
||||
m = adb_module(output_folder=output, log=logging.getLogger(adb_module.__module__))
|
||||
if serial:
|
||||
m.serial = serial
|
||||
|
||||
if iocs:
|
||||
indicators.log = m.log
|
||||
m.indicators = indicators
|
||||
|
||||
run_module(m)
|
||||
timeline.extend(m.timeline)
|
||||
timeline_detected.extend(m.timeline_detected)
|
||||
|
||||
if output:
|
||||
if len(timeline) > 0:
|
||||
save_timeline(timeline, os.path.join(output, "timeline.csv"))
|
||||
if len(timeline_detected) > 0:
|
||||
save_timeline(timeline_detected, os.path.join(output, "timeline_detected.csv"))
|
||||
|
||||
#==============================================================================
|
||||
# Check ADB backup
|
||||
# Command: check-bugreport
|
||||
#==============================================================================
|
||||
@cli.command("check-bugreport", help="Check an Android Bug Report")
|
||||
@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True,
|
||||
default=[], help=HELP_MSG_IOC)
|
||||
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
|
||||
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
||||
@click.option("--module", "-m", help=HELP_MSG_MODULE)
|
||||
@click.argument("BUGREPORT_PATH", type=click.Path(exists=True))
|
||||
@click.pass_context
|
||||
def check_bugreport(ctx, iocs, output, list_modules, module, bugreport_path):
|
||||
cmd = CmdAndroidCheckBugreport(target_path=bugreport_path, results_path=output,
|
||||
ioc_files=iocs, module_name=module)
|
||||
|
||||
if list_modules:
|
||||
cmd.list_modules()
|
||||
return
|
||||
|
||||
log.info("Checking Android bug report at path: %s", 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))
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# Command: check-backup
|
||||
#==============================================================================
|
||||
@cli.command("check-backup", help="Check an Android Backup")
|
||||
@click.option("--serial", "-s", type=str, help=SERIAL_HELP_MESSAGE)
|
||||
@click.option("--iocs", "-i", type=click.Path(exists=True), help="Path to indicators file")
|
||||
@click.option("--output", "-o", type=click.Path(exists=False), help=OUTPUT_HELP_MESSAGE)
|
||||
@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("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
||||
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
|
||||
def check_backup(iocs, output, backup_path, serial):
|
||||
log.info("Checking ADB backup located at: %s", backup_path)
|
||||
@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)
|
||||
|
||||
if output and not os.path.exists(output):
|
||||
try:
|
||||
os.makedirs(output)
|
||||
except Exception as e:
|
||||
log.critical("Unable to create output folder %s: %s", output, e)
|
||||
sys.exit(-1)
|
||||
if list_modules:
|
||||
cmd.list_modules()
|
||||
return
|
||||
|
||||
if iocs:
|
||||
# Pre-load indicators for performance reasons.
|
||||
log.info("Loading indicators from provided file at %s", iocs)
|
||||
indicators = Indicators(iocs)
|
||||
log.info("Checking Android backup at path: %s", backup_path)
|
||||
|
||||
if os.path.isfile(backup_path):
|
||||
log.critical("The path you specified is a not a folder!")
|
||||
cmd.run()
|
||||
|
||||
if os.path.basename(backup_path) == "backup.ab":
|
||||
log.info("You can use ABE (https://github.com/nelenkov/android-backup-extractor) " \
|
||||
"to extract 'backup.ab' files!")
|
||||
sys.exit(-1)
|
||||
if len(cmd.timeline_detected) > 0:
|
||||
log.warning("The analysis of the Android backup produced %d detections!",
|
||||
len(cmd.timeline_detected))
|
||||
|
||||
for module in BACKUP_MODULES:
|
||||
m = module(base_folder=backup_path, output_folder=output,
|
||||
log=logging.getLogger(module.__module__))
|
||||
|
||||
if serial:
|
||||
m.serial = serial
|
||||
#==============================================================================
|
||||
# Command: check-iocs
|
||||
#==============================================================================
|
||||
@cli.command("check-iocs", help="Compare stored JSON results to provided indicators")
|
||||
@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True,
|
||||
default=[], help=HELP_MSG_IOC)
|
||||
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
||||
@click.option("--module", "-m", help=HELP_MSG_MODULE)
|
||||
@click.argument("FOLDER", type=click.Path(exists=True))
|
||||
@click.pass_context
|
||||
def check_iocs(ctx, iocs, list_modules, module, folder):
|
||||
cmd = CmdCheckIOCS(target_path=folder, ioc_files=iocs, module_name=module)
|
||||
cmd.modules = BACKUP_MODULES + ADB_MODULES + BUGREPORT_MODULES
|
||||
|
||||
if iocs:
|
||||
indicators.log = m.log
|
||||
m.indicators = indicators
|
||||
if list_modules:
|
||||
cmd.list_modules()
|
||||
return
|
||||
|
||||
run_module(m)
|
||||
cmd.run()
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# Command: download-iocs
|
||||
#==============================================================================
|
||||
@cli.command("download-iocs", help="Download public STIX2 indicators")
|
||||
def download_indicators():
|
||||
ioc_updates = IndicatorsUpdates()
|
||||
ioc_updates.update()
|
||||
|
||||
25
mvt/android/cmd_check_adb.py
Normal file
25
mvt/android/cmd_check_adb.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 mvt.common.command import Command
|
||||
|
||||
from .modules.adb import ADB_MODULES
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdAndroidCheckADB(Command):
|
||||
|
||||
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)
|
||||
83
mvt/android/cmd_check_backup.py
Normal file
83
mvt/android/cmd_check_backup.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 io
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
||||
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
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdAndroidCheckBackup(Command):
|
||||
|
||||
name = "check-backup"
|
||||
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 = []
|
||||
|
||||
def init(self):
|
||||
if os.path.isfile(self.target_path):
|
||||
self.backup_type = "ab"
|
||||
with open(self.target_path, "rb") as handle:
|
||||
data = handle.read()
|
||||
|
||||
header = parse_ab_header(data)
|
||||
if not header["backup"]:
|
||||
log.critical("Invalid backup format, file should be in .ab format")
|
||||
sys.exit(1)
|
||||
|
||||
password = None
|
||||
if header["encryption"] != "none":
|
||||
password = getpass.getpass(prompt="Backup Password: ", stream=None)
|
||||
try:
|
||||
tardata = parse_backup_file(data, password=password)
|
||||
except InvalidBackupPassword:
|
||||
log.critical("Invalid backup password")
|
||||
sys.exit(1)
|
||||
except AndroidBackupParsingError as e:
|
||||
log.critical("Impossible to parse this backup file: %s", e)
|
||||
log.critical("Please use Android Backup Extractor (ABE) instead")
|
||||
sys.exit(1)
|
||||
|
||||
dbytes = io.BytesIO(tardata)
|
||||
self.backup_archive = tarfile.open(fileobj=dbytes)
|
||||
for member in self.backup_archive:
|
||||
self.backup_files.append(member.name)
|
||||
|
||||
elif os.path.isdir(self.target_path):
|
||||
self.backup_type = "folder"
|
||||
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))
|
||||
else:
|
||||
log.critical("Invalid backup path, path should be a folder or an Android Backup (.ab) file")
|
||||
sys.exit(1)
|
||||
|
||||
def module_init(self, module):
|
||||
if self.backup_type == "folder":
|
||||
module.from_folder(self.target_path, self.backup_files)
|
||||
else:
|
||||
module.from_ab(self.target_path, self.backup_archive, self.backup_files)
|
||||
51
mvt/android/cmd_check_bugreport.py
Normal file
51
mvt/android/cmd_check_bugreport.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 zipfile import ZipFile
|
||||
|
||||
from mvt.common.command import Command
|
||||
|
||||
from .modules.bugreport import BUGREPORT_MODULES
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdAndroidCheckBugreport(Command):
|
||||
|
||||
name = "check-bugreport"
|
||||
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 = []
|
||||
|
||||
def init(self):
|
||||
if os.path.isfile(self.target_path):
|
||||
self.bugreport_format = "zip"
|
||||
self.bugreport_archive = ZipFile(self.target_path)
|
||||
for file_name in self.bugreport_archive.namelist():
|
||||
self.bugreport_files.append(file_name)
|
||||
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 file_name in subfiles:
|
||||
self.bugreport_files.append(os.path.relpath(os.path.join(root, file_name), parent_path))
|
||||
|
||||
def module_init(self, module):
|
||||
if self.bugreport_format == "zip":
|
||||
module.from_zip(self.bugreport_archive, self.bugreport_files)
|
||||
else:
|
||||
module.from_folder(self.target_path, self.bugreport_files)
|
||||
185
mvt/android/cmd_download_apks.py
Normal file
185
mvt/android/cmd_download_apks.py
Normal file
@@ -0,0 +1,185 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 tqdm import tqdm
|
||||
|
||||
from mvt.common.module import InsufficientPrivileges
|
||||
|
||||
from .modules.adb.base import AndroidExtraction
|
||||
from .modules.adb.packages import Packages
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO: Would be better to replace tqdm with rich.progress to reduce
|
||||
# the number of dependencies. Need to investigate whether
|
||||
# it's possible to have a similar callback system.
|
||||
class PullProgress(tqdm):
|
||||
"""PullProgress is a tqdm update system for APK downloads."""
|
||||
|
||||
def update_to(self, file_name, current, total):
|
||||
if total is not None:
|
||||
self.total = total
|
||||
self.update(current - self.n)
|
||||
|
||||
|
||||
class DownloadAPKs(AndroidExtraction):
|
||||
"""DownloadAPKs is the main class operating the download of APKs
|
||||
from the device.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, output_folder=None, all_apks=False, log=None,
|
||||
packages=None):
|
||||
"""Initialize module.
|
||||
:param output_folder: Path to the folder where data should be stored
|
||||
:param all_apks: Boolean indicating whether to download all packages
|
||||
or filter known-goods
|
||||
:param packages: Provided list of packages, typically for JSON checks
|
||||
"""
|
||||
super().__init__(output_folder=output_folder, log=log)
|
||||
|
||||
self.packages = packages
|
||||
self.all_apks = all_apks
|
||||
self.output_folder_apk = None
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_path):
|
||||
"""Initialize this class from an existing apks.json file.
|
||||
|
||||
:param json_path: Path to the apks.json file to parse.
|
||||
|
||||
"""
|
||||
with open(json_path, "r", encoding="utf-8") as handle:
|
||||
packages = json.load(handle)
|
||||
return cls(packages=packages)
|
||||
|
||||
def pull_package_file(self, package_name, remote_path):
|
||||
"""Pull files related to specific package from the device.
|
||||
|
||||
:param package_name: Name of the package to download
|
||||
:param remote_path: Path to the file to download
|
||||
:returns: Path to the local copy
|
||||
|
||||
"""
|
||||
log.info("Downloading %s ...", remote_path)
|
||||
|
||||
file_name = ""
|
||||
if "==/" in remote_path:
|
||||
file_name = "_" + remote_path.split("==/")[1].replace(".apk", "")
|
||||
|
||||
local_path = os.path.join(self.output_folder_apk,
|
||||
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.output_folder_apk,
|
||||
f"{package_name}{file_name}_{name_counter}.apk")
|
||||
|
||||
try:
|
||||
with PullProgress(unit='B', unit_divisor=1024, unit_scale=True,
|
||||
miniters=1) as pp:
|
||||
self._adb_download(remote_path, local_path,
|
||||
progress_callback=pp.update_to)
|
||||
except InsufficientPrivileges:
|
||||
log.warn("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)
|
||||
self._adb_reconnect()
|
||||
return None
|
||||
|
||||
return local_path
|
||||
|
||||
def get_packages(self):
|
||||
"""Use the Packages adb module to retrieve the list of packages.
|
||||
We reuse the same extraction logic to then download the APKs.
|
||||
|
||||
|
||||
"""
|
||||
self.log.info("Retrieving list of installed packages...")
|
||||
|
||||
m = Packages()
|
||||
m.log = self.log
|
||||
m.run()
|
||||
|
||||
self.packages = m.results
|
||||
|
||||
def pull_packages(self):
|
||||
"""Download all files of all selected packages from the device."""
|
||||
log.info("Starting extraction of installed APKs at folder %s", self.output_folder)
|
||||
|
||||
if not os.path.exists(self.output_folder):
|
||||
os.mkdir(self.output_folder)
|
||||
|
||||
# If the user provided the flag --all-apks we select all packages.
|
||||
packages_selection = []
|
||||
if self.all_apks:
|
||||
log.info("Selected all %d available packages", len(self.packages))
|
||||
packages_selection = self.packages
|
||||
else:
|
||||
# Otherwise we loop through the packages and get only those that
|
||||
# are not marked as system.
|
||||
for package in self.packages:
|
||||
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))
|
||||
|
||||
if len(packages_selection) == 0:
|
||||
log.info("No packages were selected for download")
|
||||
return
|
||||
|
||||
log.info("Downloading packages from device. This might take some time ...")
|
||||
|
||||
self.output_folder_apk = os.path.join(self.output_folder, "apks")
|
||||
if not os.path.exists(self.output_folder_apk):
|
||||
os.mkdir(self.output_folder_apk)
|
||||
|
||||
counter = 0
|
||||
for package in packages_selection:
|
||||
counter += 1
|
||||
|
||||
log.info("[%d/%d] Package: %s", counter, 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.
|
||||
for package_file in package["files"]:
|
||||
device_path = package_file["path"]
|
||||
local_path = self.pull_package_file(package["package_name"],
|
||||
device_path)
|
||||
if not local_path:
|
||||
continue
|
||||
|
||||
package_file["local_path"] = local_path
|
||||
|
||||
log.info("Download of selected packages completed")
|
||||
|
||||
def save_json(self):
|
||||
"""Save the results to the package.json file."""
|
||||
json_path = os.path.join(self.output_folder, "apks.json")
|
||||
with open(json_path, "w", encoding="utf-8") as handle:
|
||||
json.dump(self.packages, handle, indent=4)
|
||||
|
||||
def run(self) -> None:
|
||||
"""Run all steps of fetch-apk."""
|
||||
self.get_packages()
|
||||
self._adb_connect()
|
||||
self.pull_packages()
|
||||
self.save_json()
|
||||
self._adb_disconnect()
|
||||
@@ -1,10 +0,0 @@
|
||||
su
|
||||
busybox
|
||||
supersu
|
||||
Superuser.apk
|
||||
KingoUser.apk
|
||||
SuperSu.apk
|
||||
magisk
|
||||
magiskhide
|
||||
magiskinit
|
||||
magiskpolicy
|
||||
@@ -1,25 +0,0 @@
|
||||
com.noshufou.android.su
|
||||
com.noshufou.android.su.elite
|
||||
eu.chainfire.supersu
|
||||
com.koushikdutta.superuser
|
||||
com.thirdparty.superuser
|
||||
com.yellowes.su
|
||||
com.koushikdutta.rommanager
|
||||
com.koushikdutta.rommanager.license
|
||||
com.dimonvideo.luckypatcher
|
||||
com.chelpus.lackypatch
|
||||
com.ramdroid.appquarantine
|
||||
com.ramdroid.appquarantinepro
|
||||
com.devadvance.rootcloak
|
||||
com.devadvance.rootcloakplus
|
||||
de.robv.android.xposed.installer
|
||||
com.saurik.substrate
|
||||
com.zachspong.temprootremovejb
|
||||
com.amphoras.hidemyroot
|
||||
com.amphoras.hidemyrootadfree
|
||||
com.formyhm.hiderootPremium
|
||||
com.formyhm.hideroot
|
||||
me.phh.superuser
|
||||
eu.chainfire.supersu.pro
|
||||
com.kingouser.com
|
||||
com.topjohnwu.magisk
|
||||
@@ -1,182 +0,0 @@
|
||||
android
|
||||
android.auto_generated_rro__
|
||||
android.autoinstalls.config.google.nexus
|
||||
com.android.backupconfirm
|
||||
com.android.bips
|
||||
com.android.bluetooth
|
||||
com.android.bluetoothmidiservice
|
||||
com.android.bookmarkprovider
|
||||
com.android.calllogbackup
|
||||
com.android.captiveportallogin
|
||||
com.android.carrierconfig
|
||||
com.android.carrierdefaultapp
|
||||
com.android.cellbroadcastreceiver
|
||||
com.android.certinstaller
|
||||
com.android.chrome
|
||||
com.android.companiondevicemanager
|
||||
com.android.connectivity.metrics
|
||||
com.android.cts.ctsshim
|
||||
com.android.cts.priv.ctsshim
|
||||
com.android.defcontainer
|
||||
com.android.documentsui
|
||||
com.android.dreams.basic
|
||||
com.android.egg
|
||||
com.android.emergency
|
||||
com.android.externalstorage
|
||||
com.android.facelock
|
||||
com.android.hotwordenrollment
|
||||
com.android.hotwordenrollment.okgoogle
|
||||
com.android.hotwordenrollment.tgoogle
|
||||
com.android.hotwordenrollment.xgoogle
|
||||
com.android.htmlviewer
|
||||
com.android.inputdevices
|
||||
com.android.keychain
|
||||
com.android.location.fused
|
||||
com.android.managedprovisioning
|
||||
com.android.mms.service
|
||||
com.android.mtp
|
||||
com.android.musicfx
|
||||
com.android.nfc
|
||||
com.android.omadm.service
|
||||
com.android.pacprocessor
|
||||
com.android.phone
|
||||
com.android.printspooler
|
||||
com.android.providers.blockednumber
|
||||
com.android.providers.calendar
|
||||
com.android.providers.contacts
|
||||
com.android.providers.downloads
|
||||
com.android.providers.downloads.ui
|
||||
com.android.providers.media
|
||||
com.android.providers.partnerbookmarks
|
||||
com.android.providers.settings
|
||||
com.android.providers.telephony
|
||||
com.android.providers.userdictionary
|
||||
com.android.proxyhandler
|
||||
com.android.retaildemo
|
||||
com.android.safetyregulatoryinfo
|
||||
com.android.sdm.plugins.connmo
|
||||
com.android.sdm.plugins.dcmo
|
||||
com.android.sdm.plugins.diagmon
|
||||
com.android.sdm.plugins.sprintdm
|
||||
com.android.server.telecom
|
||||
com.android.service.ims
|
||||
com.android.service.ims.presence
|
||||
com.android.settings
|
||||
com.android.sharedstoragebackup
|
||||
com.android.shell
|
||||
com.android.statementservice
|
||||
com.android.stk
|
||||
com.android.systemui
|
||||
com.android.systemui.theme.dark
|
||||
com.android.vending
|
||||
com.android.vpndialogs
|
||||
com.android.vzwomatrigger
|
||||
com.android.wallpaperbackup
|
||||
com.android.wallpaper.livepicker
|
||||
com.breel.wallpapers
|
||||
com.customermobile.preload.vzw
|
||||
com.google.android.apps.cloudprint
|
||||
com.google.android.apps.docs
|
||||
com.google.android.apps.docs.editors.docs
|
||||
com.google.android.apps.enterprise.dmagent
|
||||
com.google.android.apps.gcs
|
||||
com.google.android.apps.helprtc
|
||||
com.google.android.apps.inputmethod.hindi
|
||||
com.google.android.apps.maps
|
||||
com.google.android.apps.messaging
|
||||
com.google.android.apps.nexuslauncher
|
||||
com.google.android.apps.photos
|
||||
com.google.android.apps.pixelmigrate
|
||||
com.google.android.apps.tachyon
|
||||
com.google.android.apps.turbo
|
||||
com.google.android.apps.tycho
|
||||
com.google.android.apps.wallpaper
|
||||
com.google.android.apps.wallpaper.nexus
|
||||
com.google.android.apps.work.oobconfig
|
||||
com.google.android.apps.youtube.vr
|
||||
com.google.android.asdiv
|
||||
com.google.android.backuptransport
|
||||
com.google.android.calculator
|
||||
com.google.android.calendar
|
||||
com.google.android.carrier
|
||||
com.google.android.carrier.authdialog
|
||||
com.google.android.carrierentitlement
|
||||
com.google.android.carriersetup
|
||||
com.google.android.configupdater
|
||||
com.google.android.contacts
|
||||
com.google.android.deskclock
|
||||
com.google.android.dialer
|
||||
com.google.android.euicc
|
||||
com.google.android.ext.services
|
||||
com.google.android.ext.shared
|
||||
com.google.android.feedback
|
||||
com.google.android.gm
|
||||
com.google.android.gms
|
||||
com.google.android.gms.policy_auth
|
||||
com.google.android.gms.policy_sidecar_o
|
||||
com.google.android.gms.setup
|
||||
com.google.android.GoogleCamera
|
||||
com.google.android.googlequicksearchbox
|
||||
com.google.android.gsf
|
||||
com.google.android.gsf.login
|
||||
com.google.android.hardwareinfo
|
||||
com.google.android.hiddenmenu
|
||||
com.google.android.ims
|
||||
com.google.android.inputmethod.japanese
|
||||
com.google.android.inputmethod.korean
|
||||
com.google.android.inputmethod.latin
|
||||
com.google.android.inputmethod.pinyin
|
||||
com.google.android.instantapps.supervisor
|
||||
com.google.android.keep
|
||||
com.google.android.marvin.talkback
|
||||
com.google.android.music
|
||||
com.google.android.nexusicons
|
||||
com.google.android.onetimeinitializer
|
||||
com.google.android.packageinstaller
|
||||
com.google.android.partnersetup
|
||||
com.google.android.printservice.recommendation
|
||||
com.google.android.setupwizard
|
||||
com.google.android.soundpicker
|
||||
com.google.android.storagemanager
|
||||
com.google.android.syncadapters.contacts
|
||||
com.google.android.tag
|
||||
com.google.android.talk
|
||||
com.google.android.tetheringentitlement
|
||||
com.google.android.theme.pixel
|
||||
com.google.android.tts
|
||||
com.google.android.videos
|
||||
com.google.android.vr.home
|
||||
com.google.android.vr.inputmethod
|
||||
com.google.android.webview
|
||||
com.google.android.wfcactivation
|
||||
com.google.android.youtube
|
||||
com.google.ar.core
|
||||
com.google.intelligence.sense
|
||||
com.google.modemservice
|
||||
com.google.pixel.wahoo.gfxdrv
|
||||
com.google.SSRestartDetector
|
||||
com.google.tango
|
||||
com.google.vr.apps.ornament
|
||||
com.google.vr.vrcore
|
||||
com.htc.omadm.trigger
|
||||
com.qti.qualcomm.datastatusnotification
|
||||
com.qualcomm.atfwd
|
||||
com.qualcomm.embms
|
||||
com.qualcomm.fastdormancy
|
||||
com.qualcomm.ltebc_vzw
|
||||
com.qualcomm.qcrilmsgtunnel
|
||||
com.qualcomm.qti.ims
|
||||
com.qualcomm.qti.networksetting
|
||||
com.qualcomm.qti.telephonyservice
|
||||
com.qualcomm.qti.uceShimService
|
||||
com.qualcomm.shutdownlistner
|
||||
com.qualcomm.timeservice
|
||||
com.qualcomm.vzw_api
|
||||
com.quicinc.cne.CNEService
|
||||
com.verizon.llkagent
|
||||
com.verizon.mips.services
|
||||
com.verizon.obdm
|
||||
com.verizon.obdm_permissions
|
||||
com.verizon.services
|
||||
com.vzw.apnlib
|
||||
qualcomm.com.vzw_msdc_api
|
||||
@@ -1,214 +0,0 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
import pkg_resources
|
||||
from tqdm import tqdm
|
||||
|
||||
from mvt.common.utils import get_sha256_from_file_path
|
||||
|
||||
from .modules.adb.base import AndroidExtraction
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# TODO: Would be better to replace tqdm with rich.progress to reduce
|
||||
# the number of dependencies. Need to investigate whether
|
||||
# it's possible to have a similar callback system.
|
||||
class PullProgress(tqdm):
|
||||
"""PullProgress is a tqdm update system for APK downloads."""
|
||||
|
||||
def update_to(self, file_name, current, total):
|
||||
if total is not None:
|
||||
self.total = total
|
||||
self.update(current - self.n)
|
||||
|
||||
|
||||
class Package:
|
||||
"""Package indicates a package name and all the files associated with it."""
|
||||
|
||||
def __init__(self, name, files=None):
|
||||
self.name = name
|
||||
self.files = files or []
|
||||
|
||||
|
||||
class DownloadAPKs(AndroidExtraction):
|
||||
"""DownloadAPKs is the main class operating the download of APKs
|
||||
from the device."""
|
||||
|
||||
def __init__(self, output_folder=None, all_apks=False, packages=None):
|
||||
"""Initialize module.
|
||||
:param output_folder: Path to the folder where data should be stored
|
||||
:param all_apks: Boolean indicating whether to download all packages
|
||||
or filter known-goods
|
||||
:param packages: Provided list of packages, typically for JSON checks
|
||||
"""
|
||||
super().__init__(file_path=None, base_folder=None,
|
||||
output_folder=output_folder)
|
||||
|
||||
self.output_folder_apk = None
|
||||
self.packages = packages or []
|
||||
self.all_apks = all_apks
|
||||
|
||||
self._safe_packages = []
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_path):
|
||||
"""Initialize this class from an existing packages.json file.
|
||||
:param json_path: Path to the packages.json file to parse.
|
||||
"""
|
||||
with open(json_path, "r") as handle:
|
||||
data = json.load(handle)
|
||||
|
||||
packages = []
|
||||
for entry in data:
|
||||
package = Package(entry["name"], entry["files"])
|
||||
packages.append(package)
|
||||
|
||||
return cls(packages=packages)
|
||||
|
||||
def _load_safe_packages(self):
|
||||
"""Load known-good package names.
|
||||
"""
|
||||
safe_packages_path = os.path.join("data", "safe_packages.txt")
|
||||
safe_packages_string = pkg_resources.resource_string(__name__, safe_packages_path)
|
||||
safe_packages_list = safe_packages_string.decode("utf-8").split("\n")
|
||||
self._safe_packages.extend(safe_packages_list)
|
||||
|
||||
def _clean_output(self, output):
|
||||
"""Clean adb shell command output.
|
||||
:param output: Command output to clean.
|
||||
"""
|
||||
return output.strip().replace("package:", "")
|
||||
|
||||
def get_packages(self):
|
||||
"""Retrieve package names from the device using adb.
|
||||
"""
|
||||
log.info("Retrieving package names ...")
|
||||
|
||||
if not self.all_apks:
|
||||
self._load_safe_packages()
|
||||
|
||||
output = self._adb_command("pm list packages")
|
||||
total = 0
|
||||
for line in output.split("\n"):
|
||||
package_name = self._clean_output(line)
|
||||
if package_name == "":
|
||||
continue
|
||||
|
||||
total += 1
|
||||
|
||||
if not self.all_apks and package_name in self._safe_packages:
|
||||
continue
|
||||
|
||||
if package_name not in self.packages:
|
||||
self.packages.append(Package(package_name))
|
||||
|
||||
log.info("There are %d packages installed on the device. I selected %d for inspection.",
|
||||
total, len(self.packages))
|
||||
|
||||
def pull_package_file(self, package_name, remote_path):
|
||||
"""Pull files related to specific package from the device.
|
||||
:param package_name: Name of the package to download
|
||||
:param remote_path: Path to the file to download
|
||||
:returns: Path to the local copy
|
||||
"""
|
||||
log.info("Downloading %s ...", remote_path)
|
||||
|
||||
file_name = ""
|
||||
if "==/" in remote_path:
|
||||
file_name = "_" + remote_path.split("==/")[1].replace(".apk", "")
|
||||
|
||||
local_path = os.path.join(self.output_folder_apk,
|
||||
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.output_folder_apk,
|
||||
f"{package_name}{file_name}_{name_counter}.apk")
|
||||
|
||||
try:
|
||||
with PullProgress(unit='B', unit_divisor=1024, unit_scale=True,
|
||||
miniters=1) as pp:
|
||||
self._adb_download(remote_path, local_path,
|
||||
progress_callback=pp.update_to)
|
||||
except Exception as e:
|
||||
log.exception("Failed to pull package file from %s: %s",
|
||||
remote_path, e)
|
||||
self._adb_reconnect()
|
||||
return None
|
||||
|
||||
return local_path
|
||||
|
||||
def pull_packages(self):
|
||||
"""Download all files of all selected packages from the device.
|
||||
"""
|
||||
log.info("Starting extraction of installed APKs at folder %s", self.output_folder)
|
||||
|
||||
if not os.path.exists(self.output_folder):
|
||||
os.mkdir(self.output_folder)
|
||||
|
||||
log.info("Downloading packages from device. This might take some time ...")
|
||||
|
||||
self.output_folder_apk = os.path.join(self.output_folder, "apks")
|
||||
if not os.path.exists(self.output_folder_apk):
|
||||
os.mkdir(self.output_folder_apk)
|
||||
|
||||
total_packages = len(self.packages)
|
||||
counter = 0
|
||||
for package in self.packages:
|
||||
counter += 1
|
||||
|
||||
log.info("[%d/%d] Package: %s", counter, total_packages, package.name)
|
||||
|
||||
try:
|
||||
output = self._adb_command(f"pm path {package.name}")
|
||||
output = self._clean_output(output)
|
||||
if not output:
|
||||
continue
|
||||
except Exception as e:
|
||||
log.exception("Failed to get path of package %s: %s", package.name, e)
|
||||
self._adb_reconnect()
|
||||
continue
|
||||
|
||||
# Sometimes the package path contains multiple lines for multiple apks.
|
||||
# We loop through each line and download each file.
|
||||
for path in output.split("\n"):
|
||||
device_path = path.strip()
|
||||
file_path = self.pull_package_file(package.name, device_path)
|
||||
if not file_path:
|
||||
continue
|
||||
|
||||
# We add the apk metadata to the package object.
|
||||
package.files.append({
|
||||
"path": device_path,
|
||||
"local_name": file_path,
|
||||
"sha256": get_sha256_from_file_path(file_path),
|
||||
})
|
||||
|
||||
def save_json(self):
|
||||
"""Save the results to the package.json file.
|
||||
"""
|
||||
json_path = os.path.join(self.output_folder, "packages.json")
|
||||
packages = []
|
||||
for package in self.packages:
|
||||
packages.append(package.__dict__)
|
||||
|
||||
with open(json_path, "w") as handle:
|
||||
json.dump(packages, handle, indent=4)
|
||||
|
||||
def run(self):
|
||||
"""Run all steps of fetch-apk.
|
||||
"""
|
||||
self._adb_connect()
|
||||
self.get_packages()
|
||||
self.pull_packages()
|
||||
self.save_json()
|
||||
self._adb_disconnect()
|
||||
@@ -1,57 +0,0 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from rich.console import Console
|
||||
from rich.progress import track
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def koodous_lookup(packages):
|
||||
log.info("Looking up all extracted files on Koodous (www.koodous.com)")
|
||||
log.info("This might take a while...")
|
||||
|
||||
table = Table(title="Koodous Packages Detections")
|
||||
table.add_column("Package name")
|
||||
table.add_column("File name")
|
||||
table.add_column("Trusted")
|
||||
table.add_column("Detected")
|
||||
table.add_column("Rating")
|
||||
|
||||
total_packages = len(packages)
|
||||
for i in track(range(total_packages), description=f"Looking up {total_packages} packages..."):
|
||||
package = packages[i]
|
||||
for file in package.files:
|
||||
url = f"https://api.koodous.com/apks/{file['sha256']}"
|
||||
res = requests.get(url)
|
||||
report = res.json()
|
||||
|
||||
row = [package.name, file["local_name"]]
|
||||
|
||||
if "package_name" in report:
|
||||
trusted = "no"
|
||||
if report["trusted"]:
|
||||
trusted = Text("yes", "green bold")
|
||||
|
||||
detected = "no"
|
||||
if report["detected"]:
|
||||
detected = Text("yes", "red bold")
|
||||
|
||||
rating = "0"
|
||||
if int(report["rating"]) < 0:
|
||||
rating = Text(str(report["rating"]), "red bold")
|
||||
|
||||
row.extend([trusted, detected, rating])
|
||||
else:
|
||||
row.extend(["n/a", "n/a", "n/a"])
|
||||
|
||||
table.add_row(*row)
|
||||
|
||||
console = Console()
|
||||
console.print(table)
|
||||
@@ -1,93 +0,0 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from rich.console import Console
|
||||
from rich.progress import track
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def get_virustotal_report(hashes):
|
||||
apikey = "233f22e200ca5822bd91103043ccac138b910db79f29af5616a9afe8b6f215ad"
|
||||
url = f"https://www.virustotal.com/partners/sysinternals/file-reports?apikey={apikey}"
|
||||
|
||||
items = []
|
||||
for sha256 in hashes:
|
||||
items.append({
|
||||
"autostart_location": "",
|
||||
"autostart_entry": "",
|
||||
"hash": sha256,
|
||||
"local_name": "",
|
||||
"creation_datetime": "",
|
||||
})
|
||||
headers = {"User-Agent": "VirusTotal", "Content-Type": "application/json"}
|
||||
res = requests.post(url, headers=headers, json=items)
|
||||
|
||||
if res.status_code == 200:
|
||||
report = res.json()
|
||||
return report["data"]
|
||||
else:
|
||||
log.error("Unexpected response from VirusTotal: %s", res.status_code)
|
||||
return None
|
||||
|
||||
def virustotal_lookup(packages):
|
||||
log.info("Looking up all extracted files on VirusTotal (www.virustotal.com)")
|
||||
|
||||
unique_hashes = []
|
||||
for package in packages:
|
||||
for file in package.files:
|
||||
if file["sha256"] not in unique_hashes:
|
||||
unique_hashes.append(file["sha256"])
|
||||
|
||||
total_unique_hashes = len(unique_hashes)
|
||||
|
||||
detections = {}
|
||||
def virustotal_query(batch):
|
||||
report = get_virustotal_report(batch)
|
||||
if not report:
|
||||
return
|
||||
|
||||
for entry in report:
|
||||
if entry["hash"] not in detections and entry["found"] is True:
|
||||
detections[entry["hash"]] = entry["detection_ratio"]
|
||||
|
||||
batch = []
|
||||
for i in track(range(total_unique_hashes), description=f"Looking up {total_unique_hashes} files..."):
|
||||
file_hash = unique_hashes[i]
|
||||
batch.append(file_hash)
|
||||
if len(batch) == 25:
|
||||
virustotal_query(batch)
|
||||
batch = []
|
||||
|
||||
if batch:
|
||||
virustotal_query(batch)
|
||||
|
||||
table = Table(title="VirusTotal Packages Detections")
|
||||
table.add_column("Package name")
|
||||
table.add_column("File path")
|
||||
table.add_column("Detections")
|
||||
|
||||
for package in packages:
|
||||
for file in package.files:
|
||||
row = [package.name, file["local_name"]]
|
||||
|
||||
if file["sha256"] in detections:
|
||||
detection = detections[file["sha256"]]
|
||||
positives = detection.split("/")[0]
|
||||
if int(positives) > 0:
|
||||
row.append(Text(detection, "red bold"))
|
||||
else:
|
||||
row.append(detection)
|
||||
else:
|
||||
row.append("not found")
|
||||
|
||||
table.add_row(*row)
|
||||
|
||||
console = Console()
|
||||
console.print(table)
|
||||
@@ -1,4 +1,4 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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/
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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 .chrome_history import ChromeHistory
|
||||
from .dumpsys_batterystats import DumpsysBatterystats
|
||||
from .dumpsys_packages import DumpsysPackages
|
||||
from .dumpsys_procstats import DumpsysProcstats
|
||||
from .dumpsys_accessibility import DumpsysAccessibility
|
||||
from .dumpsys_activities import DumpsysActivities
|
||||
from .dumpsys_appops import DumpsysAppOps
|
||||
from .dumpsys_battery_daily import DumpsysBatteryDaily
|
||||
from .dumpsys_battery_history import DumpsysBatteryHistory
|
||||
from .dumpsys_dbinfo import DumpsysDBInfo
|
||||
from .dumpsys_full import DumpsysFull
|
||||
from .dumpsys_receivers import DumpsysReceivers
|
||||
from .files import Files
|
||||
from .getprop import Getprop
|
||||
from .logcat import Logcat
|
||||
from .packages import Packages
|
||||
from .processes import Processes
|
||||
from .rootbinaries import RootBinaries
|
||||
from .root_binaries import RootBinaries
|
||||
from .selinux_status import SELinuxStatus
|
||||
from .settings import Settings
|
||||
from .sms import SMS
|
||||
from .whatsapp import Whatsapp
|
||||
|
||||
ADB_MODULES = [ChromeHistory, SMS, Whatsapp, Processes,
|
||||
DumpsysBatterystats, DumpsysProcstats,
|
||||
DumpsysPackages, Packages, RootBinaries]
|
||||
ADB_MODULES = [ChromeHistory, SMS, Whatsapp, Processes, Getprop, Settings,
|
||||
SELinuxStatus, DumpsysBatteryHistory, DumpsysBatteryDaily,
|
||||
DumpsysReceivers, DumpsysActivities, DumpsysAccessibility,
|
||||
DumpsysDBInfo, DumpsysFull, DumpsysAppOps, Packages, Logcat,
|
||||
RootBinaries, Files]
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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 base64
|
||||
import getpass
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
@@ -10,13 +12,17 @@ import string
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from typing import Callable
|
||||
|
||||
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
|
||||
from adb_shell.exceptions import (AdbCommandFailureException, DeviceAuthError,
|
||||
UsbDeviceNotFoundError, UsbReadFailedError)
|
||||
from usb1 import USBErrorAccess, USBErrorBusy
|
||||
|
||||
from mvt.android.parsers.backup import (InvalidBackupPassword, parse_ab_header,
|
||||
parse_backup_file)
|
||||
from mvt.common.module import InsufficientPrivileges, MVTModule
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -24,41 +30,52 @@ log = logging.getLogger(__name__)
|
||||
ADB_KEY_PATH = os.path.expanduser("~/.android/adbkey")
|
||||
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=None, base_folder=None, output_folder=None,
|
||||
fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
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)
|
||||
|
||||
self.device = None
|
||||
self.serial = None
|
||||
|
||||
def _adb_check_keys(self):
|
||||
"""Make sure Android adb keys exist.
|
||||
"""
|
||||
@staticmethod
|
||||
def _adb_check_keys() -> None:
|
||||
"""Make sure Android adb keys exist."""
|
||||
if not os.path.isdir(os.path.dirname(ADB_KEY_PATH)):
|
||||
os.makedirs(os.path.dirname(ADB_KEY_PATH))
|
||||
|
||||
if not os.path.exists(ADB_KEY_PATH):
|
||||
keygen(ADB_KEY_PATH)
|
||||
|
||||
if not os.path.exists(ADB_PUB_KEY_PATH):
|
||||
write_public_keyfile(ADB_KEY_PATH, ADB_PUB_KEY_PATH)
|
||||
|
||||
def _adb_connect(self):
|
||||
"""Connect to the device over adb.
|
||||
"""
|
||||
def _adb_connect(self) -> None:
|
||||
"""Connect to the device over adb."""
|
||||
self._adb_check_keys()
|
||||
|
||||
with open(ADB_KEY_PATH, "rb") as handle:
|
||||
priv_key = handle.read()
|
||||
|
||||
signer = PythonRSASigner("", priv_key)
|
||||
with open(ADB_PUB_KEY_PATH, "rb") as handle:
|
||||
pub_key = handle.read()
|
||||
|
||||
signer = PythonRSASigner(pub_key, priv_key)
|
||||
|
||||
# If no serial was specified or if the serial does not seem to be
|
||||
# a HOST:PORT definition, we use the USB transport.
|
||||
if not self.serial or ":" not in self.serial:
|
||||
self.device = AdbDeviceUsb(serial=self.serial)
|
||||
try:
|
||||
self.device = AdbDeviceUsb(serial=self.serial)
|
||||
except UsbDeviceNotFoundError:
|
||||
log.critical("No device found. Make sure it is connected and unlocked.")
|
||||
sys.exit(-1)
|
||||
# Otherwise we try to use the TCP transport.
|
||||
else:
|
||||
addr = self.serial.split(":")
|
||||
@@ -77,54 +94,65 @@ class AndroidExtraction(MVTModule):
|
||||
except DeviceAuthError:
|
||||
log.error("You need to authorize this computer on the Android device. Retrying in 5 seconds...")
|
||||
time.sleep(5)
|
||||
except Exception as e:
|
||||
log.critical(e)
|
||||
except UsbReadFailedError:
|
||||
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)
|
||||
sys.exit(-1)
|
||||
else:
|
||||
break
|
||||
|
||||
def _adb_disconnect(self):
|
||||
"""Close adb connection to the device.
|
||||
"""
|
||||
def _adb_disconnect(self) -> None:
|
||||
"""Close adb connection to the device."""
|
||||
self.device.close()
|
||||
|
||||
def _adb_reconnect(self):
|
||||
"""Reconnect to device using adb.
|
||||
"""
|
||||
def _adb_reconnect(self) -> None:
|
||||
"""Reconnect to device using adb."""
|
||||
log.info("Reconnecting ...")
|
||||
self._adb_disconnect()
|
||||
self._adb_connect()
|
||||
|
||||
def _adb_command(self, command):
|
||||
def _adb_command(self, command: str) -> str:
|
||||
"""Execute an adb shell command.
|
||||
|
||||
:param command: Shell command to execute
|
||||
:returns: Output of command
|
||||
"""
|
||||
return self.device.shell(command)
|
||||
|
||||
def _adb_check_if_root(self):
|
||||
"""
|
||||
return self.device.shell(command, read_timeout_s=200.0)
|
||||
|
||||
def _adb_check_if_root(self) -> bool:
|
||||
"""Check if we have a `su` binary on the Android device.
|
||||
|
||||
|
||||
:returns: Boolean indicating whether a `su` binary is present or not
|
||||
|
||||
"""
|
||||
return bool(self._adb_command("command -v su"))
|
||||
|
||||
def _adb_root_or_die(self):
|
||||
"""Check if we have a `su` binary, otherwise raise an Exception.
|
||||
"""
|
||||
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!")
|
||||
|
||||
def _adb_command_as_root(self, command):
|
||||
"""Execute an adb shell command.
|
||||
|
||||
:param command: Shell command to execute as root
|
||||
:returns: Output of command
|
||||
|
||||
"""
|
||||
return self._adb_command(f"su -c {command}")
|
||||
|
||||
def _adb_check_file_exists(self, file):
|
||||
|
||||
def _adb_check_file_exists(self, file: str) -> bool:
|
||||
"""Verify that a file exists.
|
||||
|
||||
:param file: Path of the file
|
||||
:returns: Boolean indicating whether the file exists or not
|
||||
|
||||
"""
|
||||
|
||||
# TODO: Need to support checking files without root privileges as well.
|
||||
@@ -136,11 +164,16 @@ class AndroidExtraction(MVTModule):
|
||||
|
||||
return bool(self._adb_command_as_root(f"[ ! -f {file} ] || echo 1"))
|
||||
|
||||
def _adb_download(self, remote_path, local_path, progress_callback=None, retry_root=True):
|
||||
def _adb_download(self, remote_path: str, local_path: str,
|
||||
progress_callback: Callable = None,
|
||||
retry_root: 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
|
||||
: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)
|
||||
@@ -149,8 +182,9 @@ class AndroidExtraction(MVTModule):
|
||||
self._adb_download_root(remote_path, local_path, progress_callback)
|
||||
else:
|
||||
raise Exception(f"Unable to download file {remote_path}: {e}")
|
||||
|
||||
def _adb_download_root(self, remote_path, local_path, progress_callback=None):
|
||||
|
||||
def _adb_download_root(self, remote_path: str, local_path: str,
|
||||
progress_callback: Callable = None) -> None:
|
||||
try:
|
||||
# Check if we have root, if not raise an Exception.
|
||||
self._adb_root_or_die()
|
||||
@@ -174,16 +208,19 @@ class AndroidExtraction(MVTModule):
|
||||
|
||||
# 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}")
|
||||
|
||||
def _adb_process_file(self, remote_path, process_routine):
|
||||
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.
|
||||
|
||||
:param remote_path: Path of the file on the device to process
|
||||
:param process_routine: Function to be called on the local copy of the
|
||||
downloaded file
|
||||
|
||||
"""
|
||||
# Connect to the device over adb.
|
||||
self._adb_connect()
|
||||
@@ -216,7 +253,32 @@ class AndroidExtraction(MVTModule):
|
||||
# Disconnect from the device.
|
||||
self._adb_disconnect()
|
||||
|
||||
def run(self):
|
||||
"""Run the main procedure.
|
||||
"""
|
||||
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")
|
||||
|
||||
# TODO: Base64 encoding as temporary fix to avoid byte-mangling over the shell transport...
|
||||
backup_output_b64 = self._adb_command("/system/bin/bu backup -nocompress '{}' | base64".format(
|
||||
package_name))
|
||||
backup_output = base64.b64decode(backup_output_b64)
|
||||
header = parse_ab_header(backup_output)
|
||||
|
||||
if not header["backup"]:
|
||||
self.log.error("Extracting SMS via Android backup failed. No valid backup data found.")
|
||||
return
|
||||
|
||||
if header["encryption"] == "none":
|
||||
return parse_backup_file(backup_output, password=None)
|
||||
|
||||
for password_retry in range(0, 3):
|
||||
backup_password = getpass.getpass(prompt="Backup password: ", stream=None)
|
||||
try:
|
||||
decrypted_backup_tar = parse_backup_file(backup_output, backup_password)
|
||||
return decrypted_backup_tar
|
||||
except InvalidBackupPassword:
|
||||
self.log.error("You provided the wrong password! Please try again...")
|
||||
|
||||
self.log.warn("All attempts to decrypt backup with password failed!")
|
||||
|
||||
def run(self) -> None:
|
||||
"""Run the main procedure."""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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/
|
||||
|
||||
@@ -16,16 +16,18 @@ 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=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
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 serialize(self, record):
|
||||
def serialize(self, record: dict) -> None:
|
||||
return {
|
||||
"timestamp": record["isodate"],
|
||||
"module": self.__class__.__name__,
|
||||
@@ -33,9 +35,19 @@ class ChromeHistory(AndroidExtraction):
|
||||
"data": f"{record['id']} - {record['url']} (visit ID: {record['visit_id']}, redirect source: {record['redirect_source']})"
|
||||
}
|
||||
|
||||
def _parse_db(self, db_path):
|
||||
def check_indicators(self) -> None:
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
if self.indicators.check_domain(result["url"]):
|
||||
self.detected.append(result)
|
||||
|
||||
def _parse_db(self, db_path: str) -> None:
|
||||
"""Parse a Chrome History database file.
|
||||
|
||||
:param db_path: Path to the History database to process.
|
||||
|
||||
"""
|
||||
conn = sqlite3.connect(db_path)
|
||||
cur = conn.cursor()
|
||||
@@ -52,20 +64,23 @@ class ChromeHistory(AndroidExtraction):
|
||||
""")
|
||||
|
||||
for item in cur:
|
||||
self.results.append(dict(
|
||||
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_timestamp_to_iso(convert_chrometime_to_unix(item[3])),
|
||||
"redirect_source": item[4],
|
||||
})
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
log.info("Extracted a total of %d history items", len(self.results))
|
||||
|
||||
def run(self):
|
||||
self._adb_process_file(os.path.join("/", CHROME_HISTORY_PATH),
|
||||
self._parse_db)
|
||||
def run(self) -> None:
|
||||
try:
|
||||
self._adb_process_file(os.path.join("/", CHROME_HISTORY_PATH),
|
||||
self._parse_db)
|
||||
except Exception as e:
|
||||
self.log.error(e)
|
||||
|
||||
46
mvt/android/modules/adb/dumpsys_accessibility.py
Normal file
46
mvt/android/modules/adb/dumpsys_accessibility.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 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 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)
|
||||
continue
|
||||
|
||||
def run(self) -> None:
|
||||
self._adb_connect()
|
||||
output = self._adb_command("dumpsys accessibility")
|
||||
self._adb_disconnect()
|
||||
|
||||
self.results = parse_dumpsys_accessibility(output)
|
||||
|
||||
for result in self.results:
|
||||
log.info("Found installed accessibility service \"%s\"", result.get("service"))
|
||||
|
||||
self.log.info("Identified a total of %d accessibility services", len(self.results))
|
||||
46
mvt/android/modules/adb/dumpsys_activities.py
Normal file
46
mvt/android/modules/adb/dumpsys_activities.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 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)
|
||||
|
||||
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})
|
||||
continue
|
||||
|
||||
def run(self) -> None:
|
||||
self._adb_connect()
|
||||
output = self._adb_command("dumpsys package")
|
||||
self._adb_disconnect()
|
||||
|
||||
self.results = parse_dumpsys_activity_resolver_table(output)
|
||||
|
||||
self.log.info("Extracted activities for %d intents", len(self.results))
|
||||
66
mvt/android/modules/adb/dumpsys_appops.py
Normal file
66
mvt/android/modules/adb/dumpsys_appops.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 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 serialize(self, record: dict) -> None:
|
||||
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 {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:
|
||||
self._adb_connect()
|
||||
output = self._adb_command("dumpsys appops")
|
||||
self._adb_disconnect()
|
||||
|
||||
self.results = parse_dumpsys_appops(output)
|
||||
|
||||
self.log.info("Extracted a total of %d records from app-ops manager",
|
||||
len(self.results))
|
||||
51
mvt/android/modules/adb/dumpsys_battery_daily.py
Normal file
51
mvt/android/modules/adb/dumpsys_battery_daily.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 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 serialize(self, record: dict) -> None:
|
||||
return {
|
||||
"timestamp": record["from"],
|
||||
"module": self.__class__.__name__,
|
||||
"event": "battery_daily",
|
||||
"data": f"Recorded update of package {record['package_name']} with vers {record['vers']}"
|
||||
}
|
||||
|
||||
def check_indicators(self) -> 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)
|
||||
continue
|
||||
|
||||
def run(self) -> None:
|
||||
self._adb_connect()
|
||||
output = self._adb_command("dumpsys batterystats --daily")
|
||||
self._adb_disconnect()
|
||||
|
||||
self.results = parse_dumpsys_battery_daily(output)
|
||||
|
||||
self.log.info("Extracted %d records from battery daily stats", len(self.results))
|
||||
43
mvt/android/modules/adb/dumpsys_battery_history.py
Normal file
43
mvt/android/modules/adb/dumpsys_battery_history.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 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 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)
|
||||
continue
|
||||
|
||||
def run(self) -> None:
|
||||
self._adb_connect()
|
||||
output = self._adb_command("dumpsys batterystats --history")
|
||||
self._adb_disconnect()
|
||||
|
||||
self.results = parse_dumpsys_battery_history(output)
|
||||
|
||||
self.log.info("Extracted %d records from battery history", len(self.results))
|
||||
@@ -1,45 +0,0 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class DumpsysBatterystats(AndroidExtraction):
|
||||
"""This module extracts stats on battery consumption by processes."""
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
def run(self):
|
||||
self._adb_connect()
|
||||
|
||||
stats = self._adb_command("dumpsys batterystats")
|
||||
if self.output_folder:
|
||||
stats_path = os.path.join(self.output_folder,
|
||||
"dumpsys_batterystats.txt")
|
||||
with open(stats_path, "w") as handle:
|
||||
handle.write(stats)
|
||||
|
||||
log.info("Records from dumpsys batterystats stored at %s",
|
||||
stats_path)
|
||||
|
||||
history = self._adb_command("dumpsys batterystats --history")
|
||||
if self.output_folder:
|
||||
history_path = os.path.join(self.output_folder,
|
||||
"dumpsys_batterystats_history.txt")
|
||||
with open(history_path, "w") as handle:
|
||||
handle.write(history)
|
||||
|
||||
log.info("History records from dumpsys batterystats stored at %s",
|
||||
history_path)
|
||||
|
||||
self._adb_disconnect()
|
||||
48
mvt/android/modules/adb/dumpsys_dbinfo.py
Normal file
48
mvt/android/modules/adb/dumpsys_dbinfo.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 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 check_indicators(self) -> None:
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
path = result.get("path", "")
|
||||
for part in path.split("/"):
|
||||
ioc = self.indicators.check_app_id(part)
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
def run(self) -> None:
|
||||
self._adb_connect()
|
||||
output = self._adb_command("dumpsys dbinfo")
|
||||
self._adb_disconnect()
|
||||
|
||||
self.results = parse_dumpsys_dbinfo(output)
|
||||
|
||||
self.log.info("Extracted a total of %d records from database information",
|
||||
len(self.results))
|
||||
35
mvt/android/modules/adb/dumpsys_full.py
Normal file
35
mvt/android/modules/adb/dumpsys_full.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 .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 run(self) -> None:
|
||||
self._adb_connect()
|
||||
|
||||
output = self._adb_command("dumpsys")
|
||||
if self.results_path:
|
||||
output_path = os.path.join(self.results_path, "dumpsys.txt")
|
||||
with open(output_path, "w", encoding="utf-8") as handle:
|
||||
handle.write(output)
|
||||
|
||||
log.info("Full dumpsys output stored at %s", output_path)
|
||||
|
||||
self._adb_disconnect()
|
||||
@@ -1,35 +0,0 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class DumpsysPackages(AndroidExtraction):
|
||||
"""This module extracts stats on installed packages."""
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
def run(self):
|
||||
self._adb_connect()
|
||||
|
||||
output = self._adb_command("dumpsys package")
|
||||
if self.output_folder:
|
||||
packages_path = os.path.join(self.output_folder,
|
||||
"dumpsys_packages.txt")
|
||||
with open(packages_path, "w") as handle:
|
||||
handle.write(output)
|
||||
|
||||
log.info("Records from dumpsys package stored at %s",
|
||||
packages_path)
|
||||
|
||||
self._adb_disconnect()
|
||||
@@ -1,35 +0,0 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class DumpsysProcstats(AndroidExtraction):
|
||||
"""This module extracts stats on memory consumption by processes."""
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
def run(self):
|
||||
self._adb_connect()
|
||||
|
||||
output = self._adb_command("dumpsys procstats")
|
||||
if self.output_folder:
|
||||
procstats_path = os.path.join(self.output_folder,
|
||||
"dumpsys_procstats.txt")
|
||||
with open(procstats_path, "w") as handle:
|
||||
handle.write(output)
|
||||
|
||||
log.info("Records from dumpsys procstats stored at %s",
|
||||
procstats_path)
|
||||
|
||||
self._adb_disconnect()
|
||||
67
mvt/android/modules/adb/dumpsys_receivers.py
Normal file
67
mvt/android/modules/adb/dumpsys_receivers.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 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"
|
||||
INTENT_PHONE_STATE = "android.intent.action.PHONE_STATE"
|
||||
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)
|
||||
|
||||
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})
|
||||
continue
|
||||
|
||||
def run(self) -> None:
|
||||
self._adb_connect()
|
||||
|
||||
output = self._adb_command("dumpsys package")
|
||||
self.results = parse_dumpsys_receiver_resolver_table(output)
|
||||
|
||||
self._adb_disconnect()
|
||||
98
mvt/android/modules/adb/files.py
Normal file
98
mvt/android/modules/adb/files.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 stat
|
||||
|
||||
from mvt.common.utils import convert_timestamp_to_iso
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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)
|
||||
self.full_find = False
|
||||
|
||||
def find_files(self, folder: str) -> None:
|
||||
if self.full_find:
|
||||
output = self._adb_command(f"find '{folder}' -printf '%T@ %m %s %u %g %p\n' 2> /dev/null")
|
||||
|
||||
for file_line in output.splitlines():
|
||||
[unix_timestamp, mode, size, owner, group, full_path] = file_line.rstrip().split(" ", 5)
|
||||
mod_time = convert_timestamp_to_iso(datetime.datetime.utcfromtimestamp(int(float(unix_timestamp))))
|
||||
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}' 2> /dev/null")
|
||||
for file_line in output.splitlines():
|
||||
self.results.append({"path": file_line.rstrip()})
|
||||
|
||||
def serialize(self, record: dict) -> None:
|
||||
if "modified_time" in record:
|
||||
return {
|
||||
"timestamp": record["modified_time"],
|
||||
"module": self.__class__.__name__,
|
||||
"event": "file_modified",
|
||||
"data": record["path"],
|
||||
}
|
||||
|
||||
def check_suspicious(self) -> None:
|
||||
"""Check for files with suspicious permissions"""
|
||||
for result in sorted(self.results, key=lambda item: item["path"]):
|
||||
if result.get("is_suid"):
|
||||
self.log.warning("Found an SUID file in a non-standard directory \"%s\".",
|
||||
result["path"])
|
||||
self.detected.append(result)
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
"""Check file list for known suspicious files or suspicious properties"""
|
||||
self.check_suspicious()
|
||||
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
if self.indicators.check_file_path(result["path"]):
|
||||
self.log.warning("Found a known suspicous file at path: \"%s\"", result["path"])
|
||||
self.detected.append(result)
|
||||
|
||||
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")
|
||||
if output or output.strip().splitlines():
|
||||
self.full_find = True
|
||||
|
||||
for data_path in ["/data/local/tmp/", "/sdcard/", "/tmp/"]:
|
||||
self.find_files(data_path)
|
||||
|
||||
self.log.info("Found %s files in primary Android data directories", len(self.results))
|
||||
|
||||
if self.fast_mode:
|
||||
self.log.info("Flag --fast was enabled: skipping full file listing")
|
||||
else:
|
||||
self.log.info("Processing full file listing. This may take a while...")
|
||||
self.find_files("/")
|
||||
self.log.info("Found %s total files", len(self.results))
|
||||
|
||||
self._adb_disconnect()
|
||||
43
mvt/android/modules/adb/getprop.py
Normal file
43
mvt/android/modules/adb/getprop.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 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)
|
||||
|
||||
self.results = {} if not results else results
|
||||
|
||||
def run(self) -> None:
|
||||
self._adb_connect()
|
||||
output = self._adb_command("getprop")
|
||||
self._adb_disconnect()
|
||||
|
||||
self.results = parse_getprop(output)
|
||||
|
||||
# Alert if phone is outdated.
|
||||
security_patch = self.results.get("ro.build.version.security_patch", "")
|
||||
if security_patch:
|
||||
patch_date = datetime.strptime(security_patch, "%Y-%m-%d")
|
||||
if (datetime.now() - patch_date) > timedelta(days=6*30):
|
||||
self.log.warning("This phone has not received security updates for more than "
|
||||
"six months (last update: %s)", security_patch)
|
||||
|
||||
self.log.info("Extracted %d Android system properties", len(self.results))
|
||||
49
mvt/android/modules/adb/logcat.py
Normal file
49
mvt/android/modules/adb/logcat.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 .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 run(self) -> None:
|
||||
self._adb_connect()
|
||||
|
||||
# Get the current logcat.
|
||||
output = self._adb_command("logcat -d")
|
||||
# Get the locat prior to last reboot.
|
||||
last_output = self._adb_command("logcat -L")
|
||||
|
||||
if self.results_path:
|
||||
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)
|
||||
|
||||
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._adb_disconnect()
|
||||
@@ -1,27 +1,85 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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 pkg_resources
|
||||
from rich.console import Console
|
||||
from rich.progress import track
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
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",
|
||||
"android.permission.ACCESS_FINE_LOCATION",
|
||||
"android.permission.AUTHENTICATE_ACCOUNTS",
|
||||
"android.permission.CAMERA",
|
||||
"android.permission.DISABLE_KEYGUARD",
|
||||
"android.permission.PROCESS_OUTGOING_CALLS",
|
||||
"android.permission.READ_CALENDAR",
|
||||
"android.permission.READ_CALL_LOG",
|
||||
"android.permission.READ_CONTACTS",
|
||||
"android.permission.READ_PHONE_STATE",
|
||||
"android.permission.READ_SMS",
|
||||
"android.permission.RECEIVE_MMS",
|
||||
"android.permission.RECEIVE_SMS",
|
||||
"android.permission.RECEIVE_WAP_PUSH",
|
||||
"android.permission.RECORD_AUDIO",
|
||||
"android.permission.SEND_SMS",
|
||||
"android.permission.SYSTEM_ALERT_WINDOW",
|
||||
"android.permission.USE_CREDENTIALS",
|
||||
"android.permission.USE_SIP",
|
||||
"com.android.browser.permission.READ_HISTORY_BOOKMARKS",
|
||||
]
|
||||
|
||||
ROOT_PACKAGES = [
|
||||
"com.noshufou.android.su",
|
||||
"com.noshufou.android.su.elite",
|
||||
"eu.chainfire.supersu",
|
||||
"com.koushikdutta.superuser",
|
||||
"com.thirdparty.superuser",
|
||||
"com.yellowes.su",
|
||||
"com.koushikdutta.rommanager",
|
||||
"com.koushikdutta.rommanager.license",
|
||||
"com.dimonvideo.luckypatcher",
|
||||
"com.chelpus.lackypatch",
|
||||
"com.ramdroid.appquarantine",
|
||||
"com.ramdroid.appquarantinepro",
|
||||
"com.devadvance.rootcloak",
|
||||
"com.devadvance.rootcloakplus",
|
||||
"de.robv.android.xposed.installer",
|
||||
"com.saurik.substrate",
|
||||
"com.zachspong.temprootremovejb",
|
||||
"com.amphoras.hidemyroot",
|
||||
"com.amphoras.hidemyrootadfree",
|
||||
"com.formyhm.hiderootPremium",
|
||||
"com.formyhm.hideroot",
|
||||
"me.phh.superuser",
|
||||
"eu.chainfire.supersu.pro",
|
||||
"com.kingouser.com",
|
||||
"com.topjohnwu.magisk",
|
||||
]
|
||||
|
||||
|
||||
class Packages(AndroidExtraction):
|
||||
"""This module extracts the list of installed packages."""
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
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 serialize(self, record):
|
||||
def serialize(self, record: dict) -> None:
|
||||
records = []
|
||||
|
||||
timestamps = [
|
||||
@@ -40,54 +98,184 @@ class Packages(AndroidExtraction):
|
||||
|
||||
return records
|
||||
|
||||
def check_indicators(self):
|
||||
root_packages_path = os.path.join("..", "..", "data", "root_packages.txt")
|
||||
root_packages_string = pkg_resources.resource_string(__name__, root_packages_path)
|
||||
root_packages = root_packages_string.decode("utf-8").split("\n")
|
||||
|
||||
for root_package in root_packages:
|
||||
root_package = root_package.strip()
|
||||
if not root_package:
|
||||
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 root_package in self.results:
|
||||
self.log.warning("Found an installed package related to rooting/jailbreaking: \"%s\"",
|
||||
root_package)
|
||||
self.detected.append(root_package)
|
||||
if not self.indicators:
|
||||
continue
|
||||
|
||||
def run(self):
|
||||
ioc = self.indicators.check_app_id(result.get("package_name"))
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
for package_file in result.get("files", []):
|
||||
ioc = self.indicators.check_file_hash(package_file["sha256"])
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
@staticmethod
|
||||
def check_virustotal(packages: list) -> None:
|
||||
hashes = []
|
||||
for package in packages:
|
||||
for file in package.get("files", []):
|
||||
if file["sha256"] not in hashes:
|
||||
hashes.append(file["sha256"])
|
||||
|
||||
total_hashes = len(hashes)
|
||||
detections = {}
|
||||
|
||||
for i in track(range(total_hashes), description=f"Looking up {total_hashes} files..."):
|
||||
try:
|
||||
results = virustotal_lookup(hashes[i])
|
||||
except VTNoKey as e:
|
||||
log.info(e)
|
||||
return
|
||||
except VTQuotaExceeded as e:
|
||||
log.error("Unable to continue: %s", e)
|
||||
break
|
||||
|
||||
if not results:
|
||||
continue
|
||||
|
||||
positives = results["attributes"]["last_analysis_stats"]["malicious"]
|
||||
total = len(results["attributes"]["last_analysis_results"])
|
||||
|
||||
detections[hashes[i]] = f"{positives}/{total}"
|
||||
|
||||
table = Table(title="VirusTotal Packages Detections")
|
||||
table.add_column("Package name")
|
||||
table.add_column("File path")
|
||||
table.add_column("Detections")
|
||||
|
||||
for package in packages:
|
||||
for file in package.get("files", []):
|
||||
row = [package["package_name"], file["path"]]
|
||||
|
||||
if file["sha256"] in detections:
|
||||
detection = detections[file["sha256"]]
|
||||
positives = detection.split("/")[0]
|
||||
if int(positives) > 0:
|
||||
row.append(Text(detection, "red bold"))
|
||||
else:
|
||||
row.append(detection)
|
||||
else:
|
||||
row.append("not found")
|
||||
|
||||
table.add_row(*row)
|
||||
|
||||
console = Console()
|
||||
console.print(table)
|
||||
|
||||
@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
|
||||
for line in output.splitlines():
|
||||
if in_permissions:
|
||||
if line.startswith(" " * 4) and not line.startswith(" " * 6):
|
||||
in_permissions = False
|
||||
continue
|
||||
|
||||
permission = line.strip().split(":")[0]
|
||||
details["requested_permissions"].append(permission)
|
||||
|
||||
if line.strip().startswith("userId="):
|
||||
details["uid"] = line.split("=")[1].strip()
|
||||
elif line.strip().startswith("versionName="):
|
||||
details["version_name"] = line.split("=")[1].strip()
|
||||
elif line.strip().startswith("versionCode="):
|
||||
details["version_code"] = line.split("=", 1)[1].strip()
|
||||
elif line.strip().startswith("timeStamp="):
|
||||
details["timestamp"] = line.split("=")[1].strip()
|
||||
elif line.strip().startswith("firstInstallTime="):
|
||||
details["first_install_time"] = line.split("=")[1].strip()
|
||||
elif line.strip().startswith("lastUpdateTime="):
|
||||
details["last_update_time"] = line.split("=")[1].strip()
|
||||
elif line.strip() == "requested permissions:":
|
||||
in_permissions = True
|
||||
continue
|
||||
|
||||
return details
|
||||
|
||||
def _get_files_for_package(self, package_name: str) -> list:
|
||||
output = self._adb_command(f"pm path {package_name}")
|
||||
output = output.strip().replace("package:", "")
|
||||
if not output:
|
||||
return []
|
||||
|
||||
package_files = []
|
||||
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]
|
||||
|
||||
package_files.append({
|
||||
"path": file_path,
|
||||
"md5": md5,
|
||||
"sha1": sha1,
|
||||
"sha256": sha256,
|
||||
"sha512": sha512,
|
||||
})
|
||||
|
||||
return package_files
|
||||
|
||||
def run(self) -> None:
|
||||
self._adb_connect()
|
||||
|
||||
packages = self._adb_command("pm list packages -U -u -i -f")
|
||||
for line in packages.split("\n"):
|
||||
packages = self._adb_command("pm list packages -u -i -f")
|
||||
|
||||
for line in packages.splitlines():
|
||||
line = line.strip()
|
||||
if not line.startswith("package:"):
|
||||
continue
|
||||
|
||||
fields = line.split()
|
||||
file_name, package_name = fields[0].split(":")[1].rsplit("=", 1)
|
||||
installer = fields[1].split("=")[1].strip()
|
||||
if installer == "null":
|
||||
|
||||
try:
|
||||
installer = fields[1].split("=")[1].strip()
|
||||
except IndexError:
|
||||
installer = None
|
||||
uid = fields[2].split(":")[1].strip()
|
||||
else:
|
||||
if installer == "null":
|
||||
installer = None
|
||||
|
||||
dumpsys = self._adb_command(f"dumpsys package {package_name} | grep -A2 timeStamp").split("\n")
|
||||
timestamp = dumpsys[0].split("=")[1].strip()
|
||||
first_install = dumpsys[1].split("=")[1].strip()
|
||||
last_update = dumpsys[2].split("=")[1].strip()
|
||||
package_files = self._get_files_for_package(package_name)
|
||||
new_package = {
|
||||
"package_name": package_name,
|
||||
"file_name": file_name,
|
||||
"installer": installer,
|
||||
"disabled": False,
|
||||
"system": False,
|
||||
"third_party": False,
|
||||
"files": package_files,
|
||||
}
|
||||
|
||||
self.results.append(dict(
|
||||
package_name=package_name,
|
||||
file_name=file_name,
|
||||
installer=installer,
|
||||
timestamp=timestamp,
|
||||
first_install_time=first_install,
|
||||
last_update_time=last_update,
|
||||
uid=uid,
|
||||
disabled=False,
|
||||
system=False,
|
||||
third_party=False,
|
||||
))
|
||||
dumpsys_package = self._adb_command(f"dumpsys package {package_name}")
|
||||
package_details = self.parse_package_for_details(dumpsys_package)
|
||||
new_package.update(package_details)
|
||||
|
||||
self.results.append(new_package)
|
||||
|
||||
cmds = [
|
||||
{"field": "disabled", "arg": "-d"},
|
||||
@@ -96,7 +284,7 @@ class Packages(AndroidExtraction):
|
||||
]
|
||||
for cmd in cmds:
|
||||
output = self._adb_command(f"pm list packages {cmd['arg']}")
|
||||
for line in output.split("\n"):
|
||||
for line in output.splitlines():
|
||||
line = line.strip()
|
||||
if not line.startswith("package:"):
|
||||
continue
|
||||
@@ -106,6 +294,31 @@ class Packages(AndroidExtraction):
|
||||
if result["package_name"] == package_name:
|
||||
self.results[i][cmd["field"]] = True
|
||||
|
||||
for result in self.results:
|
||||
if not result["third_party"]:
|
||||
continue
|
||||
|
||||
dangerous_permissions_count = 0
|
||||
for perm in result["requested_permissions"]:
|
||||
if perm in DANGEROUS_PERMISSIONS:
|
||||
dangerous_permissions_count += 1
|
||||
|
||||
if dangerous_permissions_count >= DANGEROUS_PERMISSIONS_THRESHOLD:
|
||||
self.log.info("Third-party package \"%s\" requested %d potentially dangerous permissions",
|
||||
result["package_name"], dangerous_permissions_count)
|
||||
|
||||
packages_to_lookup = []
|
||||
for result in self.results:
|
||||
if result["system"]:
|
||||
continue
|
||||
|
||||
packages_to_lookup.append(result)
|
||||
self.log.info("Found non-system package with name \"%s\" installed by \"%s\" on %s",
|
||||
result["package_name"], result["installer"], result["timestamp"])
|
||||
|
||||
if not self.fast_mode:
|
||||
self.check_virustotal(packages_to_lookup)
|
||||
|
||||
self.log.info("Extracted at total of %d installed package names",
|
||||
len(self.results))
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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,33 +9,45 @@ from .base import AndroidExtraction
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Processes(AndroidExtraction):
|
||||
"""This module extracts details on running processes."""
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
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 run(self):
|
||||
def check_indicators(self) -> None:
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
ioc = self.indicators.check_app_id(result.get("name", ""))
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
def run(self) -> None:
|
||||
self._adb_connect()
|
||||
|
||||
output = self._adb_command("ps -e")
|
||||
|
||||
for line in output.split("\n")[1:]:
|
||||
for line in output.splitlines()[1:]:
|
||||
line = line.strip()
|
||||
if line == "":
|
||||
continue
|
||||
|
||||
fields = line.split()
|
||||
proc = dict(
|
||||
user=fields[0],
|
||||
pid=fields[1],
|
||||
parent_pid=fields[2],
|
||||
vsize=fields[3],
|
||||
rss=fields[4],
|
||||
)
|
||||
proc = {
|
||||
"user": fields[0],
|
||||
"pid": fields[1],
|
||||
"parent_pid": fields[2],
|
||||
"vsize": fields[3],
|
||||
"rss": fields[4],
|
||||
}
|
||||
|
||||
# Sometimes WCHAN is empty, so we need to re-align output fields.
|
||||
if len(fields) == 8:
|
||||
|
||||
@@ -1,30 +1,38 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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 pkg_resources
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RootBinaries(AndroidExtraction):
|
||||
"""This module extracts the list of installed packages."""
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
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 run(self):
|
||||
root_binaries_path = os.path.join("..", "..", "data", "root_binaries.txt")
|
||||
root_binaries_string = pkg_resources.resource_string(__name__, root_binaries_path)
|
||||
root_binaries = root_binaries_string.decode("utf-8").split("\n")
|
||||
def run(self) -> None:
|
||||
root_binaries = [
|
||||
"su",
|
||||
"busybox",
|
||||
"supersu",
|
||||
"Superuser.apk",
|
||||
"KingoUser.apk",
|
||||
"SuperSu.apk",
|
||||
"magisk",
|
||||
"magiskhide",
|
||||
"magiskinit",
|
||||
"magiskpolicy",
|
||||
]
|
||||
|
||||
self._adb_connect()
|
||||
|
||||
38
mvt/android/modules/adb/selinux_status.py
Normal file
38
mvt/android/modules/adb/selinux_status.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 .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)
|
||||
|
||||
self.results = {} if not results else results
|
||||
|
||||
def run(self) -> None:
|
||||
self._adb_connect()
|
||||
output = self._adb_command("getenforce")
|
||||
self._adb_disconnect()
|
||||
|
||||
status = output.lower().strip()
|
||||
self.results["status"] = status
|
||||
|
||||
if status == "enforcing":
|
||||
self.log.info("SELinux is being regularly enforced")
|
||||
else:
|
||||
self.log.warning("SELinux status is \"%s\"!", status)
|
||||
101
mvt/android/modules/adb/settings.py
Normal file
101
mvt/android/modules/adb/settings.py
Normal file
@@ -0,0 +1,101 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 .base import AndroidExtraction
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
ANDROID_DANGEROUS_SETTINGS = [
|
||||
{
|
||||
"description": "disabled Google Play Services apps verification",
|
||||
"key": "verifier_verify_adb_installs",
|
||||
"safe_value": "1",
|
||||
},
|
||||
{
|
||||
"description": "disabled Google Play Protect",
|
||||
"key": "package_verifier_enable",
|
||||
"safe_value": "1",
|
||||
},
|
||||
{
|
||||
"description": "disabled Google Play Protect",
|
||||
"key": "package_verifier_user_consent",
|
||||
"safe_value": "1",
|
||||
},
|
||||
{
|
||||
"description": "disabled Google Play Protect",
|
||||
"key": "upload_apk_enable",
|
||||
"safe_value": "1",
|
||||
},
|
||||
{
|
||||
"description": "disabled confirmation of adb apps installation",
|
||||
"key": "adb_install_need_confirm",
|
||||
"safe_value": "1",
|
||||
},
|
||||
{
|
||||
"description": "disabled sharing of security reports",
|
||||
"key": "send_security_reports",
|
||||
"safe_value": "1",
|
||||
},
|
||||
{
|
||||
"description": "disabled sharing of crash logs with manufacturer",
|
||||
"key": "samsung_errorlog_agree",
|
||||
"safe_value": "1",
|
||||
},
|
||||
{
|
||||
"description": "disabled applications errors reports",
|
||||
"key": "send_action_app_error",
|
||||
"safe_value": "1",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class Settings(AndroidExtraction):
|
||||
"""This module extracts Android system settings."""
|
||||
|
||||
def __init__(self, file_path: 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)
|
||||
|
||||
self.results = {} if not results else results
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
for namespace, settings in self.results.items():
|
||||
for key, value in settings.items():
|
||||
for danger in ANDROID_DANGEROUS_SETTINGS:
|
||||
# Check if one of the dangerous settings is using an unsafe
|
||||
# value (different than the one specified).
|
||||
if danger["key"] == key and danger["safe_value"] != value:
|
||||
self.log.warning("Found suspicious setting \"%s = %s\" (%s)",
|
||||
key, value, danger["description"])
|
||||
break
|
||||
|
||||
def run(self) -> None:
|
||||
self._adb_connect()
|
||||
|
||||
for namespace in ["system", "secure", "global"]:
|
||||
out = self._adb_command(f"cmd settings list {namespace}")
|
||||
if not out:
|
||||
continue
|
||||
|
||||
self.results[namespace] = {}
|
||||
|
||||
for line in out.splitlines():
|
||||
line = line.strip()
|
||||
if line == "":
|
||||
continue
|
||||
|
||||
fields = line.split("=", 1)
|
||||
try:
|
||||
self.results[namespace][fields[0]] = fields[1]
|
||||
except IndexError:
|
||||
continue
|
||||
|
||||
self._adb_disconnect()
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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,6 +7,9 @@ import logging
|
||||
import os
|
||||
import sqlite3
|
||||
|
||||
from mvt.android.parsers.backup import (AndroidBackupParsingError,
|
||||
parse_tar_for_sms)
|
||||
from mvt.common.module import InsufficientPrivileges
|
||||
from mvt.common.utils import check_for_links, convert_timestamp_to_iso
|
||||
|
||||
from .base import AndroidExtraction
|
||||
@@ -15,12 +18,12 @@ log = logging.getLogger(__name__)
|
||||
|
||||
SMS_BUGLE_PATH = "data/data/com.google.android.apps.messaging/databases/bugle_db"
|
||||
SMS_BUGLE_QUERY = """
|
||||
SELECT
|
||||
ppl.normalized_destination AS number,
|
||||
SELECT
|
||||
ppl.normalized_destination AS address,
|
||||
p.timestamp AS timestamp,
|
||||
CASE WHEN m.sender_id IN
|
||||
CASE WHEN m.sender_id IN
|
||||
(SELECT _id FROM participants WHERE contact_id=-1)
|
||||
THEN 2 ELSE 1 END incoming, p.text AS text
|
||||
THEN 2 ELSE 1 END incoming, p.text AS body
|
||||
FROM messages m, conversations c, parts p,
|
||||
participants ppl, conversation_participants cp
|
||||
WHERE (m.conversation_id = c._id)
|
||||
@@ -31,60 +34,65 @@ WHERE (m.conversation_id = c._id)
|
||||
|
||||
SMS_MMSSMS_PATH = "data/data/com.android.providers.telephony/databases/mmssms.db"
|
||||
SMS_MMSMS_QUERY = """
|
||||
SELECT
|
||||
address AS number,
|
||||
SELECT
|
||||
address AS address,
|
||||
date_sent AS timestamp,
|
||||
type as incoming,
|
||||
body AS text
|
||||
body AS body
|
||||
FROM sms;
|
||||
"""
|
||||
|
||||
|
||||
class SMS(AndroidExtraction):
|
||||
"""This module extracts all SMS messages containing links."""
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
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 serialize(self, record):
|
||||
text = record["text"].replace("\n", "\\n")
|
||||
def serialize(self, record: dict) -> None:
|
||||
body = record["body"].replace("\n", "\\n")
|
||||
return {
|
||||
"timestamp": record["isodate"],
|
||||
"module": self.__class__.__name__,
|
||||
"event": f"sms_{record['direction']}",
|
||||
"data": f"{record['number']}: \"{text}\""
|
||||
"data": f"{record['address']}: \"{body}\""
|
||||
}
|
||||
|
||||
def check_indicators(self):
|
||||
def check_indicators(self) -> None:
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for message in self.results:
|
||||
if not "text" in message:
|
||||
if "body" not in message:
|
||||
continue
|
||||
|
||||
message_links = check_for_links(message["text"])
|
||||
# FIXME: check links exported from the body previously
|
||||
message_links = check_for_links(message["body"])
|
||||
if self.indicators.check_domains(message_links):
|
||||
self.detected.append(message)
|
||||
|
||||
def _parse_db(self, db_path):
|
||||
def _parse_db(self, db_path: str) -> None:
|
||||
"""Parse an Android bugle_db SMS database file.
|
||||
|
||||
:param db_path: Path to the Android SMS database file to process
|
||||
|
||||
"""
|
||||
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]
|
||||
|
||||
for item in cur:
|
||||
message = dict()
|
||||
message = {}
|
||||
for index, value in enumerate(item):
|
||||
message[names[index]] = value
|
||||
|
||||
@@ -93,7 +101,7 @@ class SMS(AndroidExtraction):
|
||||
|
||||
# If we find links in the messages or if they are empty we add
|
||||
# them to the list of results.
|
||||
if check_for_links(message["text"]) or message["text"].strip() == "":
|
||||
if check_for_links(message["body"]) or message["body"].strip() == "":
|
||||
self.results.append(message)
|
||||
|
||||
cur.close()
|
||||
@@ -101,12 +109,35 @@ class SMS(AndroidExtraction):
|
||||
|
||||
log.info("Extracted a total of %d SMS messages containing links", len(self.results))
|
||||
|
||||
def run(self):
|
||||
if (self._adb_check_file_exists(os.path.join("/", SMS_BUGLE_PATH))):
|
||||
self.SMS_DB_TYPE = 1
|
||||
self._adb_process_file(os.path.join("/", SMS_BUGLE_PATH), self._parse_db)
|
||||
elif (self._adb_check_file_exists(os.path.join("/", SMS_MMSSMS_PATH))):
|
||||
self.SMS_DB_TYPE = 2
|
||||
self._adb_process_file(os.path.join("/", SMS_MMSSMS_PATH), self._parse_db)
|
||||
else:
|
||||
self.log.error("No SMS database found")
|
||||
def _extract_sms_adb(self) -> None:
|
||||
"""Use the Android backup command to extract SMS data from the native SMS app
|
||||
|
||||
It is crucial to use the under-documented "-nocompress" flag to disable the non-standard Java compression
|
||||
algorithim. This module only supports an unencrypted ADB backup.
|
||||
"""
|
||||
backup_tar = self._generate_backup("com.android.providers.telephony")
|
||||
if not backup_tar:
|
||||
return
|
||||
|
||||
try:
|
||||
self.results = parse_tar_for_sms(backup_tar)
|
||||
except AndroidBackupParsingError:
|
||||
self.log.info("Impossible to read SMS from the Android Backup, please extract the SMS and try extracting it with Android Backup Extractor")
|
||||
return
|
||||
|
||||
log.info("Extracted a total of %d SMS messages containing links", len(self.results))
|
||||
|
||||
def run(self) -> None:
|
||||
try:
|
||||
if (self._adb_check_file_exists(os.path.join("/", SMS_BUGLE_PATH))):
|
||||
self.SMS_DB_TYPE = 1
|
||||
self._adb_process_file(os.path.join("/", SMS_BUGLE_PATH), self._parse_db)
|
||||
elif (self._adb_check_file_exists(os.path.join("/", SMS_MMSSMS_PATH))):
|
||||
self.SMS_DB_TYPE = 2
|
||||
self._adb_process_file(os.path.join("/", SMS_MMSSMS_PATH), self._parse_db)
|
||||
return
|
||||
except InsufficientPrivileges:
|
||||
pass
|
||||
|
||||
self.log.warn("No SMS database found. Trying extraction of SMS data using Android backup feature.")
|
||||
self._extract_sms_adb()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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/
|
||||
|
||||
@@ -16,16 +16,18 @@ 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=None, base_folder=None, output_folder=None,
|
||||
serial=None, fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
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 serialize(self, record):
|
||||
def serialize(self, record: dict) -> None:
|
||||
text = record["data"].replace("\n", "\\n")
|
||||
return {
|
||||
"timestamp": record["isodate"],
|
||||
@@ -34,21 +36,23 @@ class Whatsapp(AndroidExtraction):
|
||||
"data": f"\"{text}\""
|
||||
}
|
||||
|
||||
def check_indicators(self):
|
||||
def check_indicators(self) -> None:
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for message in self.results:
|
||||
if not "data" in message:
|
||||
if "data" not in message:
|
||||
continue
|
||||
|
||||
message_links = check_for_links(message["data"])
|
||||
if self.indicators.check_domains(message_links):
|
||||
self.detected.append(message)
|
||||
|
||||
def _parse_db(self, db_path):
|
||||
def _parse_db(self, db_path: str) -> None:
|
||||
"""Parse an Android msgstore.db WhatsApp database file.
|
||||
|
||||
:param db_path: Path to the Android WhatsApp database file to process
|
||||
|
||||
"""
|
||||
conn = sqlite3.connect(db_path)
|
||||
cur = conn.cursor()
|
||||
@@ -59,7 +63,7 @@ class Whatsapp(AndroidExtraction):
|
||||
|
||||
messages = []
|
||||
for item in cur:
|
||||
message = dict()
|
||||
message = {}
|
||||
for index, value in enumerate(item):
|
||||
message[names[index]] = value
|
||||
|
||||
@@ -81,5 +85,8 @@ class Whatsapp(AndroidExtraction):
|
||||
log.info("Extracted a total of %d WhatsApp messages containing links", len(messages))
|
||||
self.results = messages
|
||||
|
||||
def run(self):
|
||||
self._adb_process_file(os.path.join("/", WHATSAPP_PATH), self._parse_db)
|
||||
def run(self) -> None:
|
||||
try:
|
||||
self._adb_process_file(os.path.join("/", WHATSAPP_PATH), self._parse_db)
|
||||
except Exception as e:
|
||||
self.log.error(e)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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 .sms import SMS
|
||||
|
||||
BACKUP_MODULES = [SMS,]
|
||||
BACKUP_MODULES = [SMS]
|
||||
|
||||
48
mvt/android/modules/backup/base.py
Normal file
48
mvt/android/modules/backup/base.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 os
|
||||
from tarfile import TarFile
|
||||
|
||||
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:
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
Extract the files
|
||||
"""
|
||||
self.ab = file_path
|
||||
self.tar = tar
|
||||
self.files = files
|
||||
|
||||
def _get_files_by_pattern(self, pattern: str) -> list:
|
||||
return fnmatch.filter(self.files, pattern)
|
||||
|
||||
def _get_file_content(self, file_path: str) -> bytes:
|
||||
if self.ab:
|
||||
try:
|
||||
member = self.tar.getmember(file_path)
|
||||
except KeyError:
|
||||
return None
|
||||
handle = self.tar.extractfile(member)
|
||||
else:
|
||||
handle = open(os.path.join(self.backup_path, file_path), "rb")
|
||||
|
||||
data = handle.read()
|
||||
handle.close()
|
||||
|
||||
return data
|
||||
@@ -1,64 +1,44 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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 os
|
||||
import zlib
|
||||
import logging
|
||||
|
||||
from mvt.common.module import MVTModule
|
||||
from mvt.common.utils import check_for_links, convert_timestamp_to_iso
|
||||
from mvt.android.modules.backup.base import BackupExtraction
|
||||
from mvt.android.parsers.backup import parse_sms_file
|
||||
|
||||
|
||||
class SMS(MVTModule):
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
fast_mode=False, log=None, results=[]):
|
||||
super().__init__(file_path=file_path, base_folder=base_folder,
|
||||
output_folder=output_folder, fast_mode=fast_mode,
|
||||
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)
|
||||
self.results = []
|
||||
|
||||
def check_indicators(self):
|
||||
def check_indicators(self) -> None:
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for message in self.results:
|
||||
if not "body" in message:
|
||||
if "body" not in message:
|
||||
continue
|
||||
|
||||
message_links = check_for_links(message["body"])
|
||||
if self.indicators.check_domains(message_links):
|
||||
if self.indicators.check_domains(message["links"]):
|
||||
self.detected.append(message)
|
||||
|
||||
def _process_sms_file(self, file_path):
|
||||
self.log.info("Processing SMS backup file at %s", file_path)
|
||||
def run(self) -> None:
|
||||
for file in self._get_files_by_pattern("apps/com.android.providers.telephony/d_f/*_sms_backup"):
|
||||
self.log.info("Processing SMS backup file at %s", file)
|
||||
data = self._get_file_content(file)
|
||||
self.results.extend(parse_sms_file(data))
|
||||
|
||||
with open(file_path, "rb") as handle:
|
||||
data = zlib.decompress(handle.read())
|
||||
json_data = json.loads(data)
|
||||
for file in self._get_files_by_pattern("apps/com.android.providers.telephony/d_f/*_mms_backup"):
|
||||
self.log.info("Processing MMS backup file at %s", file)
|
||||
data = self._get_file_content(file)
|
||||
self.results.extend(parse_sms_file(data))
|
||||
|
||||
for entry in json_data:
|
||||
message_links = check_for_links(entry["body"])
|
||||
|
||||
# If we find links in the messages or if they are empty we add them to the list.
|
||||
if message_links or entry["body"].strip() == "":
|
||||
self.results.append(entry)
|
||||
|
||||
def run(self):
|
||||
app_folder = os.path.join(self.base_folder,
|
||||
"apps",
|
||||
"com.android.providers.telephony",
|
||||
"d_f")
|
||||
if not os.path.exists(app_folder):
|
||||
raise FileNotFoundError("Unable to find the SMS backup folder")
|
||||
|
||||
for file_name in os.listdir(app_folder):
|
||||
if not file_name.endswith("_sms_backup"):
|
||||
continue
|
||||
|
||||
file_path = os.path.join(app_folder, file_name)
|
||||
self._process_sms_file(file_path)
|
||||
|
||||
self.log.info("Extracted a total of %d SMS messages containing links",
|
||||
self.log.info("Extracted a total of %d SMS & MMS messages containing links",
|
||||
len(self.results))
|
||||
|
||||
17
mvt/android/modules/bugreport/__init__.py
Normal file
17
mvt/android/modules/bugreport/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 .accessibility import Accessibility
|
||||
from .activities import Activities
|
||||
from .appops import Appops
|
||||
from .battery_daily import BatteryDaily
|
||||
from .battery_history import BatteryHistory
|
||||
from .dbinfo import DBInfo
|
||||
from .getprop import Getprop
|
||||
from .packages import Packages
|
||||
from .receivers import Receivers
|
||||
|
||||
BUGREPORT_MODULES = [Accessibility, Activities, Appops, BatteryDaily,
|
||||
BatteryHistory, DBInfo, Getprop, Packages, Receivers]
|
||||
61
mvt/android/modules/bugreport/accessibility.py
Normal file
61
mvt/android/modules/bugreport/accessibility.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 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 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)
|
||||
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?")
|
||||
return
|
||||
|
||||
lines = []
|
||||
in_accessibility = False
|
||||
for line in content.decode(errors="ignore").splitlines():
|
||||
if line.strip() == "DUMP OF SERVICE accessibility:":
|
||||
in_accessibility = True
|
||||
continue
|
||||
|
||||
if not in_accessibility:
|
||||
continue
|
||||
|
||||
if line.strip().startswith("------------------------------------------------------------------------------"):
|
||||
break
|
||||
|
||||
lines.append(line)
|
||||
|
||||
self.results = parse_dumpsys_accessibility("\n".join(lines))
|
||||
for result in self.results:
|
||||
log.info("Found installed accessibility service \"%s\"", result.get("service"))
|
||||
|
||||
self.log.info("Identified a total of %d accessibility services", len(self.results))
|
||||
62
mvt/android/modules/bugreport/activities.py
Normal file
62
mvt/android/modules/bugreport/activities.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 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)
|
||||
|
||||
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})
|
||||
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?")
|
||||
return
|
||||
|
||||
lines = []
|
||||
in_package = False
|
||||
for line in content.decode(errors="ignore").splitlines():
|
||||
if line.strip() == "DUMP OF SERVICE package:":
|
||||
in_package = True
|
||||
continue
|
||||
|
||||
if not in_package:
|
||||
continue
|
||||
|
||||
if line.strip().startswith("------------------------------------------------------------------------------"):
|
||||
break
|
||||
|
||||
lines.append(line)
|
||||
|
||||
self.results = parse_dumpsys_activity_resolver_table("\n".join(lines))
|
||||
|
||||
self.log.info("Extracted activities for %d intents", len(self.results))
|
||||
79
mvt/android/modules/bugreport/appops.py
Normal file
79
mvt/android/modules/bugreport/appops.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 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 serialize(self, record: dict) -> None:
|
||||
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 {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:
|
||||
content = self._get_dumpstate_file()
|
||||
if not content:
|
||||
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
|
||||
return
|
||||
|
||||
lines = []
|
||||
in_appops = False
|
||||
for line in content.decode(errors="ignore").splitlines():
|
||||
if line.strip() == "DUMP OF SERVICE appops:":
|
||||
in_appops = True
|
||||
continue
|
||||
|
||||
if not in_appops:
|
||||
continue
|
||||
|
||||
if line.strip().startswith("------------------------------------------------------------------------------"):
|
||||
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))
|
||||
71
mvt/android/modules/bugreport/base.py
Normal file
71
mvt/android/modules/bugreport/base.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 Claudio Guarnieri.
|
||||
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
|
||||
# https://github.com/mvt-project/mvt/blob/main/LICENSE
|
||||
|
||||
import fnmatch
|
||||
import logging
|
||||
import os
|
||||
from 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 from_folder(self, extract_path: str, extract_files: str) -> None:
|
||||
self.extract_path = extract_path
|
||||
self.extract_files = extract_files
|
||||
|
||||
def from_zip(self, zip_archive: ZipFile, zip_files: list) -> None:
|
||||
self.zip_archive = zip_archive
|
||||
self.zip_files = zip_files
|
||||
|
||||
def _get_files_by_pattern(self, pattern: str) -> list:
|
||||
file_names = []
|
||||
if self.zip_archive:
|
||||
for zip_file in self.zip_files:
|
||||
file_names.append(zip_file)
|
||||
else:
|
||||
file_names = self.extract_files
|
||||
|
||||
return fnmatch.filter(file_names, pattern)
|
||||
|
||||
def _get_files_by_patterns(self, patterns: list) -> list:
|
||||
for pattern in patterns:
|
||||
matches = self._get_files_by_pattern(pattern)
|
||||
if matches:
|
||||
return matches
|
||||
|
||||
def _get_file_content(self, file_path: str) -> bytes:
|
||||
if self.zip_archive:
|
||||
handle = self.zip_archive.open(file_path)
|
||||
else:
|
||||
handle = open(os.path.join(self.extract_path, file_path), "rb")
|
||||
|
||||
data = handle.read()
|
||||
handle.close()
|
||||
|
||||
return data
|
||||
|
||||
def _get_dumpstate_file(self) -> bytes:
|
||||
main = self._get_files_by_pattern("main_entry.txt")
|
||||
if main:
|
||||
main_content = self._get_file_content(main[0])
|
||||
try:
|
||||
return self._get_file_content(main_content.decode().strip())
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
dumpstate_logs = self._get_files_by_pattern("dumpState_*.log")
|
||||
if not dumpstate_logs:
|
||||
return None
|
||||
|
||||
return self._get_file_content(dumpstate_logs[0])
|
||||
|
||||
return None
|
||||
77
mvt/android/modules/bugreport/battery_daily.py
Normal file
77
mvt/android/modules/bugreport/battery_daily.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 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 serialize(self, record: dict) -> None:
|
||||
return {
|
||||
"timestamp": record["from"],
|
||||
"module": self.__class__.__name__,
|
||||
"event": "battery_daily",
|
||||
"data": f"Recorded update of package {record['package_name']} with vers {record['vers']}"
|
||||
}
|
||||
|
||||
def check_indicators(self) -> 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)
|
||||
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?")
|
||||
return
|
||||
|
||||
lines = []
|
||||
in_batterystats = False
|
||||
in_daily = False
|
||||
for line in content.decode(errors="ignore").splitlines():
|
||||
if line.strip() == "DUMP OF SERVICE batterystats:":
|
||||
in_batterystats = True
|
||||
continue
|
||||
|
||||
if not in_batterystats:
|
||||
continue
|
||||
|
||||
if line.strip() == "Daily stats:":
|
||||
lines.append(line)
|
||||
in_daily = True
|
||||
continue
|
||||
|
||||
if not in_daily:
|
||||
continue
|
||||
|
||||
if line.strip() == "":
|
||||
break
|
||||
|
||||
lines.append(line)
|
||||
|
||||
self.results = parse_dumpsys_battery_daily("\n".join(lines))
|
||||
|
||||
self.log.info("Extracted a total of %d battery daily stats",
|
||||
len(self.results))
|
||||
61
mvt/android/modules/bugreport/battery_history.py
Normal file
61
mvt/android/modules/bugreport/battery_history.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 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 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)
|
||||
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?")
|
||||
return
|
||||
|
||||
lines = []
|
||||
in_history = False
|
||||
for line in content.decode(errors="ignore").splitlines():
|
||||
if line.strip().startswith("Battery History "):
|
||||
lines.append(line)
|
||||
in_history = True
|
||||
continue
|
||||
|
||||
if not in_history:
|
||||
continue
|
||||
|
||||
if line.strip() == "":
|
||||
break
|
||||
|
||||
lines.append(line)
|
||||
|
||||
self.results = parse_dumpsys_battery_history("\n".join(lines))
|
||||
|
||||
self.log.info("Extracted a total of %d battery history records",
|
||||
len(self.results))
|
||||
64
mvt/android/modules/bugreport/dbinfo.py
Normal file
64
mvt/android/modules/bugreport/dbinfo.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 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 check_indicators(self) -> None:
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
path = result.get("path", "")
|
||||
for part in path.split("/"):
|
||||
ioc = self.indicators.check_app_id(part)
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
def run(self) -> 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?")
|
||||
return
|
||||
|
||||
in_dbinfo = False
|
||||
lines = []
|
||||
for line in content.decode(errors="ignore").splitlines():
|
||||
if line.strip() == "DUMP OF SERVICE dbinfo:":
|
||||
in_dbinfo = True
|
||||
continue
|
||||
|
||||
if not in_dbinfo:
|
||||
continue
|
||||
|
||||
if line.strip().startswith("------------------------------------------------------------------------------"):
|
||||
break
|
||||
|
||||
lines.append(line)
|
||||
|
||||
self.results = parse_dumpsys_dbinfo("\n".join(lines))
|
||||
|
||||
self.log.info("Extracted a total of %d database connection pool records",
|
||||
len(self.results))
|
||||
59
mvt/android/modules/bugreport/getprop.py
Normal file
59
mvt/android/modules/bugreport/getprop.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 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)
|
||||
|
||||
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?")
|
||||
return
|
||||
|
||||
lines = []
|
||||
in_getprop = False
|
||||
for line in content.decode(errors="ignore").splitlines():
|
||||
if line.strip() == "------ SYSTEM PROPERTIES (getprop) ------":
|
||||
in_getprop = True
|
||||
continue
|
||||
|
||||
if not in_getprop:
|
||||
continue
|
||||
|
||||
if line.strip() == "------":
|
||||
break
|
||||
|
||||
lines.append(line)
|
||||
|
||||
self.results = parse_getprop("\n".join(lines))
|
||||
|
||||
# Alert if phone is outdated.
|
||||
security_patch = self.results.get("ro.build.version.security_patch", "")
|
||||
if security_patch:
|
||||
patch_date = datetime.strptime(security_patch, "%Y-%m-%d")
|
||||
if (datetime.now() - patch_date) > timedelta(days=6*30):
|
||||
self.log.warning("This phone has not received security updates for more than "
|
||||
"six months (last update: %s)", security_patch)
|
||||
|
||||
self.log.info("Extracted %d Android system properties", len(self.results))
|
||||
168
mvt/android/modules/bugreport/packages.py
Normal file
168
mvt/android/modules/bugreport/packages.py
Normal file
@@ -0,0 +1,168 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 .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 serialize(self, record: dict) -> None:
|
||||
records = []
|
||||
|
||||
timestamps = [
|
||||
{"event": "package_install", "timestamp": record["timestamp"]},
|
||||
{"event": "package_first_install", "timestamp": record["first_install_time"]},
|
||||
{"event": "package_last_update", "timestamp": record["last_update_time"]},
|
||||
]
|
||||
|
||||
for ts in timestamps:
|
||||
records.append({
|
||||
"timestamp": ts["timestamp"],
|
||||
"module": self.__class__.__name__,
|
||||
"event": ts["event"],
|
||||
"data": f"Install or update of package {record['package_name']}",
|
||||
})
|
||||
|
||||
return records
|
||||
|
||||
def check_indicators(self) -> 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)
|
||||
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?")
|
||||
return
|
||||
|
||||
in_package = False
|
||||
in_packages_list = False
|
||||
lines = []
|
||||
for line in content.decode(errors="ignore").splitlines():
|
||||
if line.strip() == "DUMP OF SERVICE package:":
|
||||
in_package = True
|
||||
continue
|
||||
|
||||
if not in_package:
|
||||
continue
|
||||
|
||||
if line.strip() == "Packages:":
|
||||
in_packages_list = True
|
||||
continue
|
||||
|
||||
if not in_packages_list:
|
||||
continue
|
||||
|
||||
if line.strip() == "":
|
||||
break
|
||||
|
||||
lines.append(line)
|
||||
|
||||
self.results = self.parse_packages_list("\n".join(lines))
|
||||
|
||||
self.log.info("Extracted details on %d packages", len(self.results))
|
||||
84
mvt/android/modules/bugreport/receivers.py
Normal file
84
mvt/android/modules/bugreport/receivers.py
Normal file
@@ -0,0 +1,84 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 mvt.android.parsers import parse_dumpsys_receiver_resolver_table
|
||||
|
||||
from .base import BugReportModule
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
INTENT_NEW_OUTGOING_SMS = "android.provider.Telephony.NEW_OUTGOING_SMS"
|
||||
INTENT_SMS_RECEIVED = "android.provider.Telephony.SMS_RECEIVED"
|
||||
INTENT_DATA_SMS_RECEIVED = "android.intent.action.DATA_SMS_RECEIVED"
|
||||
INTENT_PHONE_STATE = "android.intent.action.PHONE_STATE"
|
||||
INTENT_NEW_OUTGOING_CALL = "android.intent.action.NEW_OUTGOING_CALL"
|
||||
|
||||
|
||||
class Receivers(BugReportModule):
|
||||
"""This module extracts details on receivers for risky activities."""
|
||||
|
||||
def __init__(self, file_path: 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)
|
||||
|
||||
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})
|
||||
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?")
|
||||
return
|
||||
|
||||
in_receivers = False
|
||||
lines = []
|
||||
for line in content.decode(errors="ignore").splitlines():
|
||||
if line.strip() == "DUMP OF SERVICE package:":
|
||||
in_receivers = True
|
||||
continue
|
||||
|
||||
if not in_receivers:
|
||||
continue
|
||||
|
||||
if line.strip().startswith("------------------------------------------------------------------------------"):
|
||||
break
|
||||
|
||||
lines.append(line)
|
||||
|
||||
self.results = parse_dumpsys_receiver_resolver_table("\n".join(lines))
|
||||
|
||||
self.log.info("Extracted receivers for %d intents", len(self.results))
|
||||
11
mvt/android/parsers/__init__.py
Normal file
11
mvt/android/parsers/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 .getprop import parse_getprop
|
||||
211
mvt/android/parsers/backup.py
Normal file
211
mvt/android/parsers/backup.py
Normal file
@@ -0,0 +1,211 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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
|
||||
import zlib
|
||||
|
||||
from cryptography.hazmat.primitives import hashes, padding
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
|
||||
from mvt.common.utils import check_for_links, convert_timestamp_to_iso
|
||||
|
||||
PBKDF2_KEY_SIZE = 32
|
||||
|
||||
|
||||
class AndroidBackupParsingError(Exception):
|
||||
"""Exception raised file parsing an android backup file"""
|
||||
|
||||
|
||||
class AndroidBackupNotImplemented(AndroidBackupParsingError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidBackupPassword(AndroidBackupParsingError):
|
||||
pass
|
||||
|
||||
|
||||
def to_utf8_bytes(input_bytes):
|
||||
output = []
|
||||
for byte in input_bytes:
|
||||
if byte < ord(b'\x80'):
|
||||
output.append(byte)
|
||||
else:
|
||||
output.append(ord('\xef') | (byte >> 12))
|
||||
output.append(ord('\xbc') | ((byte >> 6) & ord('\x3f')))
|
||||
output.append(ord('\x80') | (byte & ord('\x3f')))
|
||||
return bytes(output)
|
||||
|
||||
|
||||
def parse_ab_header(data):
|
||||
"""
|
||||
Parse the header of an Android Backup file
|
||||
Returns a dict {'backup': True, 'compression': False,
|
||||
'encryption': "none", 'version': 4}
|
||||
"""
|
||||
if data.startswith(b"ANDROID BACKUP"):
|
||||
[magic_header, version, is_compressed, encryption, tar_data] = data.split(b"\n", 4)
|
||||
return {
|
||||
"backup": True,
|
||||
"compression": (is_compressed == b"1"),
|
||||
"version": int(version),
|
||||
"encryption": encryption.decode("utf-8")
|
||||
}
|
||||
|
||||
return {
|
||||
"backup": False,
|
||||
"compression": None,
|
||||
"version": None,
|
||||
"encryption": None
|
||||
}
|
||||
|
||||
|
||||
def decrypt_master_key(password, user_salt, user_iv, pbkdf2_rounds, master_key_blob, format_version, checksum_salt):
|
||||
"""Generate AES key from user password uisng PBKDF2
|
||||
|
||||
The backup master key is extracted from the master key blog after decryption.
|
||||
"""
|
||||
# Derive key from password using PBKDF2.
|
||||
kdf = PBKDF2HMAC(algorithm=hashes.SHA1(), length=32, salt=user_salt, iterations=pbkdf2_rounds)
|
||||
key = kdf.derive(password.encode("utf-8"))
|
||||
|
||||
# Decrypt master key blob.
|
||||
cipher = Cipher(algorithms.AES(key), modes.CBC(user_iv))
|
||||
decryptor = cipher.decryptor()
|
||||
try:
|
||||
decryted_master_key_blob = decryptor.update(master_key_blob) + decryptor.finalize()
|
||||
|
||||
# Extract key and IV from decrypted blob.
|
||||
key_blob = io.BytesIO(decryted_master_key_blob)
|
||||
master_iv_length = ord(key_blob.read(1))
|
||||
master_iv = key_blob.read(master_iv_length)
|
||||
|
||||
master_key_length = ord(key_blob.read(1))
|
||||
master_key = key_blob.read(master_key_length)
|
||||
|
||||
master_key_checksum_length = ord(key_blob.read(1))
|
||||
master_key_checksum = key_blob.read(master_key_checksum_length)
|
||||
except TypeError:
|
||||
raise InvalidBackupPassword()
|
||||
|
||||
# Handle quirky encoding of master key bytes in Android original Java crypto code.
|
||||
if format_version > 1:
|
||||
hmac_mk = to_utf8_bytes(master_key)
|
||||
else:
|
||||
hmac_mk = master_key
|
||||
|
||||
# Derive checksum to confirm successful backup decryption.
|
||||
kdf = PBKDF2HMAC(algorithm=hashes.SHA1(), length=32, salt=checksum_salt, iterations=pbkdf2_rounds)
|
||||
calculated_checksum = kdf.derive(hmac_mk)
|
||||
|
||||
if master_key_checksum != calculated_checksum:
|
||||
raise InvalidBackupPassword()
|
||||
|
||||
return master_key, master_iv
|
||||
|
||||
|
||||
def decrypt_backup_data(encrypted_backup, password, encryption_algo, format_version):
|
||||
"""
|
||||
Generate encryption keyffrom password and do decryption
|
||||
|
||||
"""
|
||||
if encryption_algo != b"AES-256":
|
||||
raise AndroidBackupNotImplemented("Encryption Algorithm not implemented")
|
||||
|
||||
if password is None:
|
||||
raise InvalidBackupPassword()
|
||||
|
||||
[user_salt, checksum_salt, pbkdf2_rounds, user_iv, master_key_blob, encrypted_data] = encrypted_backup.split(b"\n", 5)
|
||||
user_salt = bytes.fromhex(user_salt.decode("utf-8"))
|
||||
checksum_salt = bytes.fromhex(checksum_salt.decode("utf-8"))
|
||||
pbkdf2_rounds = int(pbkdf2_rounds)
|
||||
user_iv = bytes.fromhex(user_iv.decode("utf-8"))
|
||||
master_key_blob = bytes.fromhex(master_key_blob.decode("utf-8"))
|
||||
|
||||
# Derive decryption master key from password.
|
||||
master_key, master_iv = decrypt_master_key(password=password, user_salt=user_salt, user_iv=user_iv,
|
||||
pbkdf2_rounds=pbkdf2_rounds, master_key_blob=master_key_blob,
|
||||
format_version=format_version, checksum_salt=checksum_salt)
|
||||
|
||||
# Decrypt and unpad backup data using derivied key.
|
||||
cipher = Cipher(algorithms.AES(master_key), modes.CBC(master_iv))
|
||||
decryptor = cipher.decryptor()
|
||||
decrypted_tar = decryptor.update(encrypted_data) + decryptor.finalize()
|
||||
|
||||
unpadder = padding.PKCS7(128).unpadder()
|
||||
return unpadder.update(decrypted_tar)
|
||||
|
||||
|
||||
def parse_backup_file(data, password=None):
|
||||
"""
|
||||
Parse an ab file, returns a tar file
|
||||
|
||||
"""
|
||||
if not data.startswith(b"ANDROID BACKUP"):
|
||||
raise AndroidBackupParsingError("Invalid file header")
|
||||
|
||||
[magic_header, version, is_compressed, encryption_algo, tar_data] = data.split(b"\n", 4)
|
||||
version = int(version)
|
||||
is_compressed = int(is_compressed)
|
||||
|
||||
if encryption_algo != b"none":
|
||||
tar_data = decrypt_backup_data(tar_data, password, encryption_algo, format_version=version)
|
||||
|
||||
if is_compressed:
|
||||
try:
|
||||
tar_data = zlib.decompress(tar_data)
|
||||
except zlib.error:
|
||||
raise AndroidBackupParsingError("Impossible to decompress the backup file")
|
||||
|
||||
return tar_data
|
||||
|
||||
|
||||
def parse_tar_for_sms(data):
|
||||
"""
|
||||
Extract SMS from a tar backup archive
|
||||
Returns an array of SMS
|
||||
"""
|
||||
dbytes = io.BytesIO(data)
|
||||
tar = tarfile.open(fileobj=dbytes)
|
||||
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()))
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def parse_sms_file(data):
|
||||
"""
|
||||
Parse an SMS or MMS file extracted from a backup
|
||||
Returns a list of SMS or MMS entries
|
||||
"""
|
||||
res = []
|
||||
data = zlib.decompress(data)
|
||||
json_data = json.loads(data)
|
||||
|
||||
for entry in json_data:
|
||||
# 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")
|
||||
|
||||
# If we find links in the messages or if they are empty we add them to the list.
|
||||
if message_links or entry["body"].strip() == "":
|
||||
entry["links"] = message_links
|
||||
res.append(entry)
|
||||
|
||||
return res
|
||||
375
mvt/android/parsers/dumpsys.py
Normal file
375
mvt/android/parsers/dumpsys.py
Normal file
@@ -0,0 +1,375 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 mvt.common.utils import convert_timestamp_to_iso
|
||||
|
||||
|
||||
def parse_dumpsys_accessibility(output: str) -> list:
|
||||
results = []
|
||||
|
||||
in_services = False
|
||||
for line in output.splitlines():
|
||||
if line.strip().startswith("installed services:"):
|
||||
in_services = True
|
||||
continue
|
||||
|
||||
if not in_services:
|
||||
continue
|
||||
|
||||
if line.strip() == "}":
|
||||
break
|
||||
|
||||
service = line.split(":")[1].strip()
|
||||
|
||||
results.append({
|
||||
"package_name": service.split("/")[0],
|
||||
"service": service,
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def parse_dumpsys_activity_resolver_table(output: str) -> dict:
|
||||
results = {}
|
||||
|
||||
in_activity_resolver_table = False
|
||||
in_non_data_actions = False
|
||||
intent = None
|
||||
for line in output.splitlines():
|
||||
if line.startswith("Activity Resolver Table:"):
|
||||
in_activity_resolver_table = True
|
||||
continue
|
||||
|
||||
if not in_activity_resolver_table:
|
||||
continue
|
||||
|
||||
if line.startswith(" Non-Data Actions:"):
|
||||
in_non_data_actions = True
|
||||
continue
|
||||
|
||||
if not in_non_data_actions:
|
||||
continue
|
||||
|
||||
# If we hit an empty line, the Non-Data Actions section should be
|
||||
# finished.
|
||||
if line.strip() == "":
|
||||
break
|
||||
|
||||
# We detect the action name.
|
||||
if line.startswith(" " * 6) and not line.startswith(" " * 8) and ":" in line:
|
||||
intent = line.strip().replace(":", "")
|
||||
results[intent] = []
|
||||
continue
|
||||
|
||||
# If we are not in an intent block yet, skip.
|
||||
if not intent:
|
||||
continue
|
||||
|
||||
# If we are in a block but the line does not start with 8 spaces
|
||||
# it means the block ended a new one started, so we reset and
|
||||
# continue.
|
||||
if not line.startswith(" " * 8):
|
||||
intent = None
|
||||
continue
|
||||
|
||||
# If we got this far, we are processing receivers for the
|
||||
# activities we are interested in.
|
||||
activity = line.strip().split(" ")[1]
|
||||
package_name = activity.split("/")[0]
|
||||
|
||||
results[intent].append({
|
||||
"package_name": package_name,
|
||||
"activity": activity,
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def parse_dumpsys_battery_daily(output: str) -> list:
|
||||
results = []
|
||||
daily = None
|
||||
daily_updates = []
|
||||
for line in output.splitlines():
|
||||
if line.startswith(" Daily from "):
|
||||
if len(daily_updates) > 0:
|
||||
results.extend(daily_updates)
|
||||
daily_updates = []
|
||||
|
||||
timeframe = line[13:].strip()
|
||||
date_from, date_to = timeframe.strip(":").split(" to ", 1)
|
||||
daily = {"from": date_from[0:10], "to": date_to[0:10]}
|
||||
continue
|
||||
|
||||
if not daily:
|
||||
continue
|
||||
|
||||
if not line.strip().startswith("Update "):
|
||||
continue
|
||||
|
||||
line = line.strip().replace("Update ", "")
|
||||
package_name, vers = line.split(" ", 1)
|
||||
vers_nr = vers.split("=", 1)[1]
|
||||
|
||||
already_seen = False
|
||||
for update in daily_updates:
|
||||
if package_name == update["package_name"] and vers_nr == update["vers"]:
|
||||
already_seen = True
|
||||
break
|
||||
|
||||
if not already_seen:
|
||||
daily_updates.append({
|
||||
"action": "update",
|
||||
"from": daily["from"],
|
||||
"to": daily["to"],
|
||||
"package_name": package_name,
|
||||
"vers": vers_nr,
|
||||
})
|
||||
|
||||
if len(daily_updates) > 0:
|
||||
results.extend(daily_updates)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def parse_dumpsys_battery_history(output: str) -> list:
|
||||
results = []
|
||||
|
||||
for line in output.splitlines():
|
||||
if line.startswith("Battery History "):
|
||||
continue
|
||||
|
||||
if line.strip() == "":
|
||||
break
|
||||
|
||||
time_elapsed = line.strip().split(" ", 1)[0]
|
||||
|
||||
event = ""
|
||||
if line.find("+job") > 0:
|
||||
event = "start_job"
|
||||
uid = line[line.find("+job")+5:line.find(":")]
|
||||
service = line[line.find(":")+1:].strip('"')
|
||||
package_name = service.split("/")[0]
|
||||
elif line.find("-job") > 0:
|
||||
event = "end_job"
|
||||
uid = line[line.find("-job")+5:line.find(":")]
|
||||
service = line[line.find(":")+1:].strip('"')
|
||||
package_name = service.split("/")[0]
|
||||
elif line.find("+running +wake_lock=") > 0:
|
||||
uid = line[line.find("+running +wake_lock=")+21:line.find(":")]
|
||||
event = "wake"
|
||||
service = line[line.find("*walarm*:")+9:].split(" ")[0].strip('"').strip()
|
||||
if service == "" or "/" not in service:
|
||||
continue
|
||||
|
||||
package_name = service.split("/")[0]
|
||||
else:
|
||||
continue
|
||||
|
||||
results.append({
|
||||
"time_elapsed": time_elapsed,
|
||||
"event": event,
|
||||
"uid": uid,
|
||||
"package_name": package_name,
|
||||
"service": service,
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def parse_dumpsys_dbinfo(output: str) -> list:
|
||||
results = []
|
||||
|
||||
rxp = re.compile(r'.*\[([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3})\].*\[Pid:\((\d+)\)\](\w+).*sql\=\"(.+?)\"')
|
||||
rxp_no_pid = re.compile(r'.*\[([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3})\][ ]{1}(\w+).*sql\=\"(.+?)\"')
|
||||
|
||||
pool = None
|
||||
in_operations = False
|
||||
for line in output.splitlines():
|
||||
if line.startswith("Connection pool for "):
|
||||
pool = line.replace("Connection pool for ", "").rstrip(":")
|
||||
|
||||
if not pool:
|
||||
continue
|
||||
|
||||
if line.strip() == "Most recently executed operations:":
|
||||
in_operations = True
|
||||
continue
|
||||
|
||||
if not in_operations:
|
||||
continue
|
||||
|
||||
if not line.startswith(" "):
|
||||
in_operations = False
|
||||
pool = None
|
||||
continue
|
||||
|
||||
matches = rxp.findall(line)
|
||||
if not matches:
|
||||
matches = rxp_no_pid.findall(line)
|
||||
if not matches:
|
||||
continue
|
||||
else:
|
||||
match = matches[0]
|
||||
results.append({
|
||||
"isodate": match[0],
|
||||
"action": match[1],
|
||||
"sql": match[2],
|
||||
"path": pool,
|
||||
})
|
||||
else:
|
||||
match = matches[0]
|
||||
results.append({
|
||||
"isodate": match[0],
|
||||
"pid": match[1],
|
||||
"action": match[2],
|
||||
"sql": match[3],
|
||||
"path": pool,
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def parse_dumpsys_receiver_resolver_table(output: str) -> dict:
|
||||
results = {}
|
||||
|
||||
in_receiver_resolver_table = False
|
||||
in_non_data_actions = False
|
||||
intent = None
|
||||
for line in output.splitlines():
|
||||
if line.startswith("Receiver Resolver Table:"):
|
||||
in_receiver_resolver_table = True
|
||||
continue
|
||||
|
||||
if not in_receiver_resolver_table:
|
||||
continue
|
||||
|
||||
if line.startswith(" Non-Data Actions:"):
|
||||
in_non_data_actions = True
|
||||
continue
|
||||
|
||||
if not in_non_data_actions:
|
||||
continue
|
||||
|
||||
# If we hit an empty line, the Non-Data Actions section should be
|
||||
# finished.
|
||||
if line.strip() == "":
|
||||
break
|
||||
|
||||
# We detect the action name.
|
||||
if line.startswith(" " * 6) and not line.startswith(" " * 8) and ":" in line:
|
||||
intent = line.strip().replace(":", "")
|
||||
results[intent] = []
|
||||
continue
|
||||
|
||||
# If we are not in an intent block yet, skip.
|
||||
if not intent:
|
||||
continue
|
||||
|
||||
# If we are in a block but the line does not start with 8 spaces
|
||||
# it means the block ended a new one started, so we reset and
|
||||
# continue.
|
||||
if not line.startswith(" " * 8):
|
||||
intent = None
|
||||
continue
|
||||
|
||||
# If we got this far, we are processing receivers for the
|
||||
# activities we are interested in.
|
||||
receiver = line.strip().split(" ")[1]
|
||||
package_name = receiver.split("/")[0]
|
||||
|
||||
results[intent].append({
|
||||
"package_name": package_name,
|
||||
"receiver": receiver,
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def parse_dumpsys_appops(output: str) -> list:
|
||||
results = []
|
||||
perm = {}
|
||||
package = {}
|
||||
entry = {}
|
||||
uid = None
|
||||
in_packages = False
|
||||
|
||||
for line in output.splitlines():
|
||||
if line.startswith(" Uid 0:"):
|
||||
in_packages = True
|
||||
|
||||
if not in_packages:
|
||||
continue
|
||||
|
||||
if line.startswith(" Uid "):
|
||||
uid = line[6:-1]
|
||||
continue
|
||||
|
||||
if line.startswith(" Package "):
|
||||
if entry:
|
||||
perm["entries"].append(entry)
|
||||
entry = {}
|
||||
|
||||
if package:
|
||||
if perm:
|
||||
package["permissions"].append(perm)
|
||||
|
||||
perm = {}
|
||||
results.append(package)
|
||||
|
||||
package = {
|
||||
"package_name": line[12:-1],
|
||||
"permissions": [],
|
||||
"uid": uid,
|
||||
}
|
||||
continue
|
||||
|
||||
if line.startswith(" ") and line[6] != " ":
|
||||
if entry:
|
||||
perm["entries"].append(entry)
|
||||
entry = {}
|
||||
if perm:
|
||||
package["permissions"].append(perm)
|
||||
perm = {}
|
||||
|
||||
perm["name"] = line.split()[0]
|
||||
perm["entries"] = []
|
||||
if len(line.split()) > 1:
|
||||
perm["access"] = line.split()[1][1:-2]
|
||||
|
||||
continue
|
||||
|
||||
if line.startswith(" "):
|
||||
# Permission entry like:
|
||||
# Reject: [fg-s]2021-05-19 22:02:52.054 (-314d1h25m2s33ms)
|
||||
if entry:
|
||||
perm["entries"].append(entry)
|
||||
entry = {}
|
||||
|
||||
entry["access"] = line.split(":")[0].strip()
|
||||
entry["type"] = line[line.find("[")+1:line.find("]")]
|
||||
|
||||
try:
|
||||
entry["timestamp"] = convert_timestamp_to_iso(
|
||||
datetime.strptime(
|
||||
line[line.find("]")+1:line.find("(")].strip(),
|
||||
"%Y-%m-%d %H:%M:%S.%f"))
|
||||
except ValueError:
|
||||
# Invalid date format
|
||||
pass
|
||||
|
||||
if line.strip() == "":
|
||||
break
|
||||
|
||||
if entry:
|
||||
perm["entries"].append(entry)
|
||||
if perm:
|
||||
package["permissions"].append(perm)
|
||||
if package:
|
||||
results.append(package)
|
||||
|
||||
return results
|
||||
26
mvt/android/parsers/getprop.py
Normal file
26
mvt/android/parsers/getprop.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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
|
||||
|
||||
|
||||
def parse_getprop(output: str) -> dict:
|
||||
results = {}
|
||||
rxp = re.compile(r"\[(.+?)\]: \[(.+?)\]")
|
||||
|
||||
for line in output.splitlines():
|
||||
line = line.strip()
|
||||
if line == "":
|
||||
continue
|
||||
|
||||
matches = re.findall(rxp, line)
|
||||
if not matches or len(matches[0]) != 2:
|
||||
continue
|
||||
|
||||
key = matches[0][0]
|
||||
value = matches[0][1]
|
||||
results[key] = value
|
||||
|
||||
return results
|
||||
@@ -1,4 +1,4 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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/
|
||||
|
||||
64
mvt/common/cmd_check_iocs.py
Normal file
64
mvt/common/cmd_check_iocs.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 mvt.common.command import Command
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdCheckIOCS(Command):
|
||||
|
||||
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)
|
||||
|
||||
def run(self) -> None:
|
||||
all_modules = []
|
||||
for entry in self.modules:
|
||||
if entry not in all_modules:
|
||||
all_modules.append(entry)
|
||||
|
||||
log.info("Checking stored results against provided indicators...")
|
||||
|
||||
total_detections = 0
|
||||
for file_name in os.listdir(self.target_path):
|
||||
name_only, ext = os.path.splitext(file_name)
|
||||
file_path = os.path.join(self.target_path, file_name)
|
||||
|
||||
for iocs_module in all_modules:
|
||||
if self.module_name and iocs_module.__name__ != self.module_name:
|
||||
continue
|
||||
|
||||
if iocs_module().get_slug() != name_only:
|
||||
continue
|
||||
|
||||
log.info("Loading results from \"%s\" with module %s", file_name,
|
||||
iocs_module.__name__)
|
||||
|
||||
m = iocs_module.from_json(file_path,
|
||||
log=logging.getLogger(iocs_module.__module__))
|
||||
if self.iocs.total_ioc_count > 0:
|
||||
m.indicators = self.iocs
|
||||
m.indicators.log = m.log
|
||||
|
||||
try:
|
||||
m.check_indicators()
|
||||
except NotImplementedError:
|
||||
continue
|
||||
else:
|
||||
total_detections += len(m.detected)
|
||||
|
||||
if total_detections > 0:
|
||||
log.warning("The check of the results produced %d detections!",
|
||||
total_detections)
|
||||
178
mvt/common/command.py
Normal file
178
mvt/common/command.py
Normal file
@@ -0,0 +1,178 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 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.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__)):
|
||||
self.name = ""
|
||||
|
||||
self.target_path = target_path
|
||||
self.results_path = results_path
|
||||
self.ioc_files = ioc_files
|
||||
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)
|
||||
|
||||
self.timeline = []
|
||||
self.timeline_detected = []
|
||||
|
||||
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)
|
||||
sys.exit(1)
|
||||
|
||||
def _add_log_file_handler(self, logger: logging.Logger) -> None:
|
||||
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)
|
||||
|
||||
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"))
|
||||
|
||||
if len(self.timeline_detected) > 0:
|
||||
save_timeline(self.timeline_detected,
|
||||
os.path.join(self.results_path, "timeline_detected.csv"))
|
||||
|
||||
def _store_info(self) -> None:
|
||||
if not self.results_path:
|
||||
return
|
||||
|
||||
target_path = None
|
||||
if self.target_path:
|
||||
target_path = os.path.abspath(self.target_path)
|
||||
|
||||
info = {
|
||||
"target_path": target_path,
|
||||
"mvt_version": MVT_VERSION,
|
||||
"date": convert_timestamp_to_iso(datetime.now()),
|
||||
"ioc_files": [],
|
||||
"hashes": [],
|
||||
}
|
||||
|
||||
for coll in self.iocs.ioc_collections:
|
||||
info["ioc_files"].append(coll.get("stix2_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())
|
||||
|
||||
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()
|
||||
|
||||
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:
|
||||
json.dump(info, handle, indent=4)
|
||||
|
||||
def list_modules(self) -> None:
|
||||
self.log.info("Following is the list of available %s modules:", self.name)
|
||||
for module in self.modules:
|
||||
self.log.info(" - %s", module.__name__)
|
||||
|
||||
def init(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def module_init(self, module: Callable) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def run(self) -> None:
|
||||
self._create_storage()
|
||||
self._add_log_file_handler(self.log)
|
||||
|
||||
try:
|
||||
self.init()
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
for module in self.modules:
|
||||
if self.module_name and module.__name__ != self.module_name:
|
||||
continue
|
||||
|
||||
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)
|
||||
|
||||
if self.iocs.total_ioc_count:
|
||||
m.indicators = self.iocs
|
||||
m.indicators.log = m.log
|
||||
|
||||
if self.serial:
|
||||
m.serial = self.serial
|
||||
|
||||
try:
|
||||
self.module_init(m)
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
run_module(m)
|
||||
|
||||
self.timeline.extend(m.timeline)
|
||||
self.timeline_detected.extend(m.timeline_detected)
|
||||
|
||||
self._store_timeline()
|
||||
self._store_info()
|
||||
14
mvt/common/help.py
Normal file
14
mvt/common/help.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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/
|
||||
|
||||
# Help messages of repeating options.
|
||||
HELP_MSG_OUTPUT = "Specify a path to a folder where you want to store JSON results"
|
||||
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"
|
||||
|
||||
# Android-specific.
|
||||
HELP_MSG_SERIAL = "Specify a device serial number or HOST:PORT connection string"
|
||||
@@ -1,64 +1,225 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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 appdirs import user_data_dir
|
||||
|
||||
from .url import URL
|
||||
|
||||
MVT_DATA_FOLDER = user_data_dir("mvt")
|
||||
MVT_INDICATORS_FOLDER = os.path.join(MVT_DATA_FOLDER, "indicators")
|
||||
|
||||
|
||||
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, file_path, log=None):
|
||||
self.file_path = file_path
|
||||
with open(self.file_path, "r") as handle:
|
||||
self.data = json.load(handle)
|
||||
|
||||
def __init__(self, log=logging.Logger) -> None:
|
||||
self.log = log
|
||||
self.ioc_domains = []
|
||||
self.ioc_processes = []
|
||||
self.ioc_emails = []
|
||||
self.ioc_files = []
|
||||
self._parse_stix_file()
|
||||
self.ioc_collections = []
|
||||
self.total_ioc_count = 0
|
||||
|
||||
def _parse_stix_file(self):
|
||||
"""Extract IOCs of given type from STIX2 definitions.
|
||||
def _load_downloaded_indicators(self) -> None:
|
||||
if not os.path.isdir(MVT_INDICATORS_FOLDER):
|
||||
return
|
||||
|
||||
for ioc_file_name in os.listdir(MVT_INDICATORS_FOLDER):
|
||||
if ioc_file_name.lower().endswith(".stix2"):
|
||||
self.parse_stix2(os.path.join(MVT_INDICATORS_FOLDER, ioc_file_name))
|
||||
|
||||
def _check_stix2_env_variable(self) -> None:
|
||||
"""
|
||||
for entry in self.data["objects"]:
|
||||
Checks if a variable MVT_STIX2 contains path to a STIX files.
|
||||
"""
|
||||
if "MVT_STIX2" not in os.environ:
|
||||
return
|
||||
|
||||
paths = os.environ["MVT_STIX2"].split(":")
|
||||
for path in paths:
|
||||
if os.path.isfile(path):
|
||||
self.parse_stix2(path)
|
||||
else:
|
||||
self.log.error("Path specified with env MVT_STIX2 is not a valid file: %s",
|
||||
path)
|
||||
|
||||
def _new_collection(self, cid: str = "", name: str = "", description: str = "",
|
||||
file_name: str = "", file_path: str = "") -> dict:
|
||||
return {
|
||||
"id": cid,
|
||||
"name": name,
|
||||
"description": description,
|
||||
"stix2_file_name": file_name,
|
||||
"stix2_file_path": file_path,
|
||||
"domains": [],
|
||||
"processes": [],
|
||||
"emails": [],
|
||||
"file_names": [],
|
||||
"file_paths": [],
|
||||
"files_sha256": [],
|
||||
"app_ids": [],
|
||||
"ios_profile_ids": [],
|
||||
"count": 0,
|
||||
}
|
||||
|
||||
def _add_indicator(self, ioc: str, ioc_coll: dict, ioc_coll_list: list) -> None:
|
||||
ioc = ioc.strip("'")
|
||||
if ioc not in ioc_coll_list:
|
||||
ioc_coll_list.append(ioc)
|
||||
ioc_coll["count"] += 1
|
||||
self.total_ioc_count += 1
|
||||
|
||||
def parse_stix2(self, file_path: str) -> None:
|
||||
"""Extract indicators from a STIX2 file.
|
||||
|
||||
:param file_path: Path to the STIX2 file to parse
|
||||
:type file_path: str
|
||||
|
||||
"""
|
||||
self.log.info("Parsing STIX2 indicators file at path %s", file_path)
|
||||
|
||||
with open(file_path, "r", encoding="utf-8") as handle:
|
||||
try:
|
||||
if entry["type"] != "indicator":
|
||||
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!")
|
||||
return
|
||||
|
||||
malware = {}
|
||||
indicators = []
|
||||
relationships = []
|
||||
for entry in data.get("objects", []):
|
||||
entry_type = entry.get("type", "")
|
||||
if entry_type == "malware":
|
||||
malware[entry["id"]] = {
|
||||
"name": entry["name"],
|
||||
"description": entry.get("description", ""),
|
||||
}
|
||||
elif entry_type == "indicator":
|
||||
indicators.append(entry)
|
||||
elif entry_type == "relationship":
|
||||
relationships.append(entry)
|
||||
|
||||
collections = []
|
||||
for mal_id, mal_values in malware.items():
|
||||
collection = self._new_collection(mal_id, mal_values.get("name"),
|
||||
mal_values.get("description"),
|
||||
os.path.basename(file_path),
|
||||
file_path)
|
||||
collections.append(collection)
|
||||
|
||||
# We loop through all indicators.
|
||||
for indicator in indicators:
|
||||
malware_id = None
|
||||
|
||||
# We loop through all relationships and find the one pertinent to
|
||||
# the current indicator.
|
||||
for relationship in relationships:
|
||||
if relationship["source_ref"] != indicator["id"]:
|
||||
continue
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
key, value = entry["pattern"].strip("[]").split("=")
|
||||
value = value.strip("'")
|
||||
# Look for a malware definition with the correct identifier.
|
||||
if relationship["target_ref"] in malware.keys():
|
||||
malware_id = relationship["target_ref"]
|
||||
break
|
||||
|
||||
if key == "domain-name:value":
|
||||
# We force domain names to lower case.
|
||||
value = value.lower()
|
||||
if value not in self.ioc_domains:
|
||||
self.ioc_domains.append(value)
|
||||
elif key == "process:name":
|
||||
if value not in self.ioc_processes:
|
||||
self.ioc_processes.append(value)
|
||||
elif key == "email-addr:value":
|
||||
# We force email addresses to lower case.
|
||||
value = value.lower()
|
||||
if value not in self.ioc_emails:
|
||||
self.ioc_emails.append(value)
|
||||
elif key == "file:name":
|
||||
if value not in self.ioc_files:
|
||||
self.ioc_files.append(value)
|
||||
# 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
|
||||
|
||||
def check_domain(self, url):
|
||||
key, value = indicator.get("pattern", "").strip("[]").split("=")
|
||||
|
||||
if key == "domain-name:value":
|
||||
# We force domain names to lower case.
|
||||
self._add_indicator(ioc=value.lower(),
|
||||
ioc_coll=collection,
|
||||
ioc_coll_list=collection["domains"])
|
||||
elif key == "process:name":
|
||||
self._add_indicator(ioc=value,
|
||||
ioc_coll=collection,
|
||||
ioc_coll_list=collection["processes"])
|
||||
elif key == "email-addr:value":
|
||||
# We force email addresses to lower case.
|
||||
self._add_indicator(ioc=value.lower(),
|
||||
ioc_coll=collection,
|
||||
ioc_coll_list=collection["emails"])
|
||||
elif key == "file:name":
|
||||
self._add_indicator(ioc=value,
|
||||
ioc_coll=collection,
|
||||
ioc_coll_list=collection["file_names"])
|
||||
elif key == "file:path":
|
||||
self._add_indicator(ioc=value,
|
||||
ioc_coll=collection,
|
||||
ioc_coll_list=collection["file_paths"])
|
||||
elif key == "file:hashes.sha256":
|
||||
self._add_indicator(ioc=value,
|
||||
ioc_coll=collection,
|
||||
ioc_coll_list=collection["files_sha256"])
|
||||
elif key == "app:id":
|
||||
self._add_indicator(ioc=value,
|
||||
ioc_coll=collection,
|
||||
ioc_coll_list=collection["app_ids"])
|
||||
elif key == "configuration-profile:id":
|
||||
self._add_indicator(ioc=value,
|
||||
ioc_coll=collection,
|
||||
ioc_coll_list=collection["ios_profile_ids"])
|
||||
|
||||
break
|
||||
|
||||
for coll in collections:
|
||||
self.log.info("Extracted %d indicators for collection with name \"%s\"",
|
||||
coll["count"], coll["name"])
|
||||
|
||||
self.ioc_collections.extend(collections)
|
||||
|
||||
def load_indicators_files(self, files: list, load_default: bool = True) -> None:
|
||||
"""
|
||||
Load a list of indicators files.
|
||||
"""
|
||||
for file_path in files:
|
||||
if os.path.isfile(file_path):
|
||||
self.parse_stix2(file_path)
|
||||
else:
|
||||
self.log.warning("No indicators file exists at path %s",
|
||||
file_path)
|
||||
|
||||
# Load downloaded indicators and any indicators from env variable.
|
||||
if load_default:
|
||||
self._load_downloaded_indicators()
|
||||
|
||||
self._check_stix2_env_variable()
|
||||
self.log.info("Loaded a total of %d unique indicators", self.total_ioc_count)
|
||||
|
||||
def get_iocs(self, ioc_type: str) -> dict:
|
||||
for ioc_collection in self.ioc_collections:
|
||||
for ioc in ioc_collection.get(ioc_type, []):
|
||||
yield {
|
||||
"value": ioc,
|
||||
"type": ioc_type,
|
||||
"name": ioc_collection["name"],
|
||||
"stix2_file_name": ioc_collection["stix2_file_name"],
|
||||
}
|
||||
|
||||
def check_domain(self, url: str) -> dict:
|
||||
"""Check if a given URL matches any of the provided domain indicators.
|
||||
|
||||
:param url: URL to match against domain indicators
|
||||
:type url: str
|
||||
:returns: Indicator details if matched, otherwise None
|
||||
|
||||
"""
|
||||
# TODO: If the IOC domain contains a subdomain, it is not currently
|
||||
# being matched.
|
||||
# being matched.
|
||||
if not url:
|
||||
return None
|
||||
|
||||
try:
|
||||
# First we use the provided URL.
|
||||
@@ -83,94 +244,206 @@ class Indicators:
|
||||
else:
|
||||
# If it's not shortened, we just use the original URL object.
|
||||
final_url = orig_url
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
# If URL parsing failed, we just try to do a simple substring
|
||||
# match.
|
||||
for ioc in self.ioc_domains:
|
||||
if ioc.lower() in url:
|
||||
self.log.warning("Maybe found a known suspicious domain: %s", url)
|
||||
return True
|
||||
for ioc in self.get_iocs("domains"):
|
||||
if ioc["value"].lower() in url:
|
||||
self.log.warning("Maybe found a known suspicious domain %s matching indicators from \"%s\"",
|
||||
url, ioc["name"])
|
||||
return ioc
|
||||
|
||||
# If nothing matched, we can quit here.
|
||||
return False
|
||||
return None
|
||||
|
||||
# If all parsing worked, we start walking through available domain indicators.
|
||||
for ioc in self.ioc_domains:
|
||||
for ioc in self.get_iocs("domains"):
|
||||
# First we check the full domain.
|
||||
if final_url.domain.lower() == ioc:
|
||||
if final_url.domain.lower() == ioc["value"]:
|
||||
if orig_url.is_shortened and orig_url.url != final_url.url:
|
||||
self.log.warning("Found a known suspicious domain %s shortened as %s",
|
||||
final_url.url, orig_url.url)
|
||||
self.log.warning("Found a known suspicious domain %s shortened as %s matching indicators from \"%s\"",
|
||||
final_url.url, orig_url.url, ioc["name"])
|
||||
else:
|
||||
self.log.warning("Found a known suspicious domain: %s", final_url.url)
|
||||
self.log.warning("Found a known suspicious domain %s matching indicators from \"%s\"",
|
||||
final_url.url, ioc["name"])
|
||||
|
||||
return True
|
||||
return ioc
|
||||
|
||||
# Then we just check the top level domain.
|
||||
if final_url.top_level.lower() == ioc:
|
||||
if final_url.top_level.lower() == ioc["value"]:
|
||||
if orig_url.is_shortened and orig_url.url != final_url.url:
|
||||
self.log.warning("Found a sub-domain matching a suspicious top level %s shortened as %s",
|
||||
final_url.url, orig_url.url)
|
||||
self.log.warning("Found a sub-domain with suspicious top level %s shortened as %s matching indicators from \"%s\"",
|
||||
final_url.url, orig_url.url, ioc["name"])
|
||||
else:
|
||||
self.log.warning("Found a sub-domain matching a suspicious top level: %s", final_url.url)
|
||||
self.log.warning("Found a sub-domain with a suspicious top level %s matching indicators from \"%s\"",
|
||||
final_url.url, ioc["name"])
|
||||
|
||||
return True
|
||||
return ioc
|
||||
|
||||
def check_domains(self, urls: list) -> dict:
|
||||
"""Check a list of URLs against the provided list of domain indicators.
|
||||
|
||||
:param urls: List of URLs to check against domain indicators
|
||||
:type urls: list
|
||||
:returns: Indicator details if matched, otherwise None
|
||||
|
||||
def check_domains(self, urls):
|
||||
"""Check the provided list of (suspicious) domains against a list of URLs.
|
||||
:param urls: List of URLs to check
|
||||
"""
|
||||
for url in urls:
|
||||
if self.check_domain(url):
|
||||
return True
|
||||
if not urls:
|
||||
return None
|
||||
|
||||
def check_process(self, process):
|
||||
for url in urls:
|
||||
check = self.check_domain(url)
|
||||
if check:
|
||||
return check
|
||||
|
||||
def check_process(self, process: str) -> dict:
|
||||
"""Check the provided process name against the list of process
|
||||
indicators.
|
||||
:param process: Process name to check
|
||||
|
||||
:param process: Process name to check against process indicators
|
||||
:type process: str
|
||||
:returns: Indicator details if matched, otherwise None
|
||||
|
||||
"""
|
||||
if not process:
|
||||
return False
|
||||
return None
|
||||
|
||||
proc_name = os.path.basename(process)
|
||||
if proc_name in self.ioc_processes:
|
||||
self.log.warning("Found a known suspicious process name \"%s\"", process)
|
||||
return True
|
||||
for ioc in self.get_iocs("processes"):
|
||||
if proc_name == ioc["value"]:
|
||||
self.log.warning("Found a known suspicious process name \"%s\" matching indicators from \"%s\"",
|
||||
process, ioc["name"])
|
||||
return ioc
|
||||
|
||||
if len(proc_name) == 16:
|
||||
for bad_proc in self.ioc_processes:
|
||||
if bad_proc.startswith(proc_name):
|
||||
self.log.warning("Found a truncated known suspicious process name \"%s\"", process)
|
||||
return True
|
||||
if len(proc_name) == 16:
|
||||
if ioc["value"].startswith(proc_name):
|
||||
self.log.warning("Found a truncated known suspicious process name \"%s\" matching indicators from \"%s\"",
|
||||
process, ioc["name"])
|
||||
return ioc
|
||||
|
||||
def check_processes(self, processes):
|
||||
def check_processes(self, processes: list) -> dict:
|
||||
"""Check the provided list of processes against the list of
|
||||
process indicators.
|
||||
:param processes: List of processes to check
|
||||
"""
|
||||
for process in processes:
|
||||
if self.check_process(process):
|
||||
return True
|
||||
|
||||
def check_email(self, email):
|
||||
:param processes: List of processes to check against process indicators
|
||||
:type processes: list
|
||||
:returns: Indicator details if matched, otherwise None
|
||||
|
||||
"""
|
||||
if not processes:
|
||||
return None
|
||||
|
||||
for process in processes:
|
||||
check = self.check_process(process)
|
||||
if check:
|
||||
return check
|
||||
|
||||
def check_email(self, email: str) -> dict:
|
||||
"""Check the provided email against the list of email indicators.
|
||||
:param email: Suspicious email to check
|
||||
|
||||
:param email: Email address to check against email indicators
|
||||
:type email: str
|
||||
:returns: Indicator details if matched, otherwise None
|
||||
|
||||
"""
|
||||
if not email:
|
||||
return False
|
||||
return None
|
||||
|
||||
if email.lower() in self.ioc_emails:
|
||||
self.log.warning("Found a known suspicious email address: \"%s\"", email)
|
||||
return True
|
||||
for ioc in self.get_iocs("emails"):
|
||||
if email.lower() == ioc["value"].lower():
|
||||
self.log.warning("Found a known suspicious email address \"%s\" matching indicators from \"%s\"",
|
||||
email, ioc["name"])
|
||||
return ioc
|
||||
|
||||
def check_file_name(self, file_name: str) -> dict:
|
||||
"""Check the provided file name against the list of file indicators.
|
||||
|
||||
:param file_name: File name to check against file
|
||||
indicators
|
||||
:type file_name: str
|
||||
:returns: Indicator details if matched, otherwise None
|
||||
|
||||
"""
|
||||
if not file_name:
|
||||
return None
|
||||
|
||||
for ioc in self.get_iocs("file_names"):
|
||||
if ioc["value"] == file_name:
|
||||
self.log.warning("Found a known suspicious file name \"%s\" matching indicators from \"%s\"",
|
||||
file_name, ioc["name"])
|
||||
return ioc
|
||||
|
||||
def check_file_path(self, file_path: str) -> dict:
|
||||
"""Check the provided file path against the list of file indicators (both path and name).
|
||||
|
||||
:param file_path: File path or file name to check against file
|
||||
indicators
|
||||
:type file_path: str
|
||||
:returns: Indicator details if matched, otherwise None
|
||||
|
||||
def check_file(self, file_path):
|
||||
"""Check the provided file path against the list of file indicators.
|
||||
:param file_path: Path or name of the file to check
|
||||
"""
|
||||
if not file_path:
|
||||
return False
|
||||
return None
|
||||
|
||||
file_name = os.path.basename(file_path)
|
||||
if file_name in self.ioc_files:
|
||||
self.log.warning("Found a known suspicious file: \"%s\"", file_path)
|
||||
return True
|
||||
ioc = self.check_file_name(os.path.basename(file_path))
|
||||
if ioc:
|
||||
return ioc
|
||||
|
||||
for ioc in self.get_iocs("file_paths"):
|
||||
# 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"])
|
||||
return ioc
|
||||
|
||||
def check_profile(self, profile_uuid: str) -> dict:
|
||||
"""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
|
||||
|
||||
"""
|
||||
if not profile_uuid:
|
||||
return None
|
||||
|
||||
for ioc in self.get_iocs("ios_profile_ids"):
|
||||
if profile_uuid in ioc["value"]:
|
||||
self.log.warning("Found a known suspicious profile ID \"%s\" matching indicators from \"%s\"",
|
||||
profile_uuid, ioc["name"])
|
||||
return ioc
|
||||
|
||||
def check_file_hash(self, file_hash: str) -> dict:
|
||||
"""Check the provided SHA256 file hash against the list of indicators.
|
||||
|
||||
:param file_hash: SHA256 hash to check
|
||||
:type file_hash: str
|
||||
:returns: Indicator details if matched, otherwise None
|
||||
|
||||
"""
|
||||
if not file_hash:
|
||||
return None
|
||||
|
||||
for ioc in self.get_iocs("files_sha256"):
|
||||
if file_hash.lower() == ioc["value"].lower():
|
||||
self.log.warning("Found a known suspicious file with hash \"%s\" matching indicators from \"%s\"",
|
||||
file_hash, ioc["name"])
|
||||
return ioc
|
||||
|
||||
def check_app_id(self, app_id: str) -> dict:
|
||||
"""Check the provided app identifier (typically an Android package name)
|
||||
against the list of indicators.
|
||||
|
||||
:param app_id: App ID to check against the list of indicators
|
||||
:type app_id: str
|
||||
:returns: Indicator details if matched, otherwise None
|
||||
|
||||
"""
|
||||
if not app_id:
|
||||
return None
|
||||
|
||||
for ioc in self.get_iocs("app_ids"):
|
||||
if app_id.lower() == ioc["value"].lower():
|
||||
self.log.warning("Found a known suspicious app with ID \"%s\" matching indicators from \"%s\"",
|
||||
app_id, ioc["name"])
|
||||
return ioc
|
||||
|
||||
59
mvt/common/logo.py
Normal file
59
mvt/common/logo.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 .updates import IndicatorsUpdates, MVTUpdates
|
||||
from .version import MVT_VERSION
|
||||
|
||||
|
||||
def check_updates() -> None:
|
||||
# First we check for MVT version udpates.
|
||||
mvt_updates = MVTUpdates()
|
||||
try:
|
||||
latest_version = mvt_updates.check()
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
if latest_version:
|
||||
print(f"\t\t[bold]Version {latest_version} is available! Upgrade mvt![/bold]")
|
||||
|
||||
# Then we check for indicators files updates.
|
||||
ioc_updates = IndicatorsUpdates()
|
||||
|
||||
# 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]")
|
||||
return
|
||||
|
||||
# We only perform this check at a fixed frequency, in order to not
|
||||
# overburden the user with too many lookups if the command is being run
|
||||
# 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")
|
||||
return
|
||||
|
||||
try:
|
||||
ioc_to_update = ioc_updates.check()
|
||||
except Exception:
|
||||
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]")
|
||||
else:
|
||||
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}")
|
||||
|
||||
check_updates()
|
||||
|
||||
print("\n")
|
||||
@@ -1,117 +1,128 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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 csv
|
||||
import glob
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import Callable
|
||||
|
||||
import simplejson as json
|
||||
|
||||
from .indicators import Indicators
|
||||
|
||||
|
||||
class DatabaseNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DatabaseCorruptedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InsufficientPrivileges(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MVTModule(object):
|
||||
"""This class provides a base for all extraction modules."""
|
||||
|
||||
enabled = True
|
||||
slug = None
|
||||
|
||||
def __init__(self, file_path=None, base_folder=None, output_folder=None,
|
||||
fast_mode=False, log=None, results=[]):
|
||||
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):
|
||||
"""Initialize module.
|
||||
:param file_path: Path to the module's database file, if there is any.
|
||||
:param base_folder: Path to the base folder (backup or filesystem dump)
|
||||
:param output_folder: Folder where results will be stored
|
||||
|
||||
: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)
|
||||
:type file_path: str
|
||||
:param results_path: Folder where results will be stored
|
||||
:type results_path: str
|
||||
:param fast_mode: Flag to enable or disable slow modules
|
||||
:type fast_mode: bool
|
||||
:param log: Handle to logger
|
||||
:param results: Provided list of results entries
|
||||
:type results: list
|
||||
"""
|
||||
self.file_path = file_path
|
||||
self.base_folder = base_folder
|
||||
self.output_folder = output_folder
|
||||
self.target_path = target_path
|
||||
self.results_path = results_path
|
||||
self.fast_mode = fast_mode
|
||||
self.log = log
|
||||
self.indicators = None
|
||||
self.results = results
|
||||
self.results = results if results else []
|
||||
self.detected = []
|
||||
self.timeline = []
|
||||
self.timeline_detected = []
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_path, log=None):
|
||||
with open(json_path, "r") as handle:
|
||||
def from_json(cls, json_path: str, log: logging.Logger = None):
|
||||
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)
|
||||
return cls(results=results, log=log)
|
||||
|
||||
def get_slug(self):
|
||||
def get_slug(self) -> str:
|
||||
"""Use the module's class name to retrieve a slug"""
|
||||
if self.slug:
|
||||
return self.slug
|
||||
|
||||
sub = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", self.__class__.__name__)
|
||||
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", sub).lower()
|
||||
|
||||
def _find_paths(self, root_paths):
|
||||
for root_path in root_paths:
|
||||
for found_path in glob.glob(os.path.join(self.base_folder, root_path)):
|
||||
if not os.path.exists(found_path):
|
||||
continue
|
||||
yield found_path
|
||||
|
||||
def load_indicators(self, file_path):
|
||||
self.indicators = Indicators(file_path, self.log)
|
||||
|
||||
def check_indicators(self):
|
||||
def check_indicators(self) -> None:
|
||||
"""Check the results of this module against a provided list of
|
||||
indicators."""
|
||||
indicators.
|
||||
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def save_to_json(self):
|
||||
"""Save the collected results to a json file.
|
||||
"""
|
||||
if not self.output_folder:
|
||||
def save_to_json(self) -> None:
|
||||
"""Save the collected results to a json file."""
|
||||
if not self.results_path:
|
||||
return
|
||||
|
||||
name = self.get_slug()
|
||||
|
||||
if self.results:
|
||||
results_file_name = f"{name}.json"
|
||||
results_json_path = os.path.join(self.output_folder, results_file_name)
|
||||
with open(results_json_path, "w") as handle:
|
||||
results_json_path = os.path.join(self.results_path, results_file_name)
|
||||
with open(results_json_path, "w", encoding="utf-8") as handle:
|
||||
try:
|
||||
json.dump(self.results, handle, indent=4)
|
||||
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)
|
||||
|
||||
if self.detected:
|
||||
detected_file_name = f"{name}_detected.json"
|
||||
detected_json_path = os.path.join(self.output_folder, detected_file_name)
|
||||
with open(detected_json_path, "w") as handle:
|
||||
json.dump(self.detected, handle, indent=4)
|
||||
detected_json_path = os.path.join(self.results_path, detected_file_name)
|
||||
with open(detected_json_path, "w", encoding="utf-8") as handle:
|
||||
json.dump(self.detected, handle, indent=4, default=str)
|
||||
|
||||
def serialize(self, record):
|
||||
def serialize(self, record: dict) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def to_timeline(self):
|
||||
"""Convert results into a timeline.
|
||||
@staticmethod
|
||||
def _deduplicate_timeline(timeline: list) -> list:
|
||||
"""Serialize entry as JSON to deduplicate repeated entries
|
||||
|
||||
:param timeline: List of entries from timeline to deduplicate
|
||||
|
||||
"""
|
||||
timeline_set = set()
|
||||
for record in timeline:
|
||||
timeline_set.add(json.dumps(record, sort_keys=True))
|
||||
return [json.loads(record) for record in timeline_set]
|
||||
|
||||
def to_timeline(self) -> None:
|
||||
"""Convert results into a timeline."""
|
||||
for result in self.results:
|
||||
record = self.serialize(result)
|
||||
if record:
|
||||
@@ -129,23 +140,15 @@ class MVTModule(object):
|
||||
self.timeline_detected.append(record)
|
||||
|
||||
# De-duplicate timeline entries.
|
||||
self.timeline = self.timeline_deduplicate(self.timeline)
|
||||
self.timeline_detected = self.timeline_deduplicate(self.timeline_detected)
|
||||
self.timeline = self._deduplicate_timeline(self.timeline)
|
||||
self.timeline_detected = self._deduplicate_timeline(self.timeline_detected)
|
||||
|
||||
def timeline_deduplicate(self, timeline):
|
||||
"""Serialize entry as JSON to deduplicate repeated entries"""
|
||||
timeline_set = set()
|
||||
for record in timeline:
|
||||
timeline_set.add(json.dumps(record, sort_keys=True))
|
||||
return [json.loads(record) for record in timeline_set]
|
||||
|
||||
def run(self):
|
||||
"""Run the main module procedure.
|
||||
"""
|
||||
def run(self) -> None:
|
||||
"""Run the main module procedure."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def run_module(module):
|
||||
def run_module(module: Callable) -> None:
|
||||
module.log.info("Running module %s...", module.__class__.__name__)
|
||||
|
||||
try:
|
||||
@@ -159,7 +162,7 @@ def run_module(module):
|
||||
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 and recovery failed: %s",
|
||||
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",
|
||||
@@ -184,18 +187,20 @@ def run_module(module):
|
||||
module.save_to_json()
|
||||
|
||||
|
||||
def save_timeline(timeline, timeline_path):
|
||||
def save_timeline(timeline: list, timeline_path: str) -> None:
|
||||
"""Save the timeline in a csv file.
|
||||
:param timeline: List of records to order and store.
|
||||
:param timeline_path: Path to the csv file to store the timeline to.
|
||||
|
||||
:param timeline: List of records to order and store
|
||||
:param timeline_path: Path to the csv file to store the timeline to
|
||||
|
||||
"""
|
||||
with io.open(timeline_path, "a+", encoding="utf-8") as handle:
|
||||
with open(timeline_path, "a+", encoding="utf-8") as handle:
|
||||
csvoutput = csv.writer(handle, delimiter=",", quotechar="\"")
|
||||
csvoutput.writerow(["UTC Timestamp", "Plugin", "Event", "Description"])
|
||||
for event in sorted(timeline, key=lambda x: x["timestamp"] if x["timestamp"] is not None else ""):
|
||||
csvoutput.writerow([
|
||||
event["timestamp"],
|
||||
event["module"],
|
||||
event["event"],
|
||||
event["data"],
|
||||
event.get("timestamp"),
|
||||
event.get("module"),
|
||||
event.get("event"),
|
||||
event.get("data"),
|
||||
])
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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: https://gist.github.com/stanchan/bce1c2d030c76fe9223b5ff6ad0f03db
|
||||
|
||||
from click import Option, UsageError, command, option
|
||||
from click import Option, UsageError
|
||||
|
||||
|
||||
class MutuallyExclusiveOption(Option):
|
||||
"""This class extends click to support mutually exclusive options.
|
||||
"""
|
||||
"""This class extends click to support mutually exclusive options."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.mutually_exclusive = set(kwargs.pop("mutually_exclusive", []))
|
||||
|
||||
211
mvt/common/updates.py
Normal file
211
mvt/common/updates.py
Normal file
@@ -0,0 +1,211 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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
|
||||
|
||||
import requests
|
||||
import yaml
|
||||
from packaging import version
|
||||
|
||||
from .indicators import MVT_DATA_FOLDER, MVT_INDICATORS_FOLDER
|
||||
from .version import MVT_VERSION
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# In hours.
|
||||
INDICATORS_CHECK_FREQUENCY = 12
|
||||
|
||||
|
||||
class MVTUpdates:
|
||||
|
||||
def check(self) -> str:
|
||||
res = requests.get("https://pypi.org/pypi/mvt/json")
|
||||
data = res.json()
|
||||
latest_version = data.get("info", {}).get("version", "")
|
||||
|
||||
if version.parse(latest_version) > version.parse(MVT_VERSION):
|
||||
return latest_version
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
class IndicatorsUpdates:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.github_raw_url = "https://raw.githubusercontent.com/{}/{}/{}/{}"
|
||||
|
||||
self.index_owner = "mvt-project"
|
||||
self.index_repo = "mvt-indicators"
|
||||
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")
|
||||
|
||||
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:
|
||||
data = handle.read().strip()
|
||||
if data:
|
||||
return int(data)
|
||||
|
||||
return 0
|
||||
|
||||
def set_latest_check(self) -> None:
|
||||
timestamp = int(datetime.utcnow().timestamp())
|
||||
with open(self.latest_check_path, "w") 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:
|
||||
data = handle.read().strip()
|
||||
if data:
|
||||
return int(data)
|
||||
|
||||
return 0
|
||||
|
||||
def set_latest_update(self) -> None:
|
||||
timestamp = int(datetime.utcnow().timestamp())
|
||||
with open(self.latest_update_path, "w") 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)
|
||||
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)
|
||||
return None
|
||||
|
||||
return yaml.safe_load(res.content)
|
||||
|
||||
def download_remote_ioc(self, ioc_url: str) -> 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)
|
||||
return None
|
||||
|
||||
clean_file_name = ioc_url.lstrip("https://").replace("/", "_")
|
||||
ioc_path = os.path.join(MVT_INDICATORS_FOLDER, clean_file_name)
|
||||
|
||||
with open(ioc_path, "w", encoding="utf-8") as handle:
|
||||
handle.write(res.text)
|
||||
|
||||
return ioc_path
|
||||
|
||||
def update(self) -> None:
|
||||
self.set_latest_check()
|
||||
|
||||
if not os.path.exists(MVT_INDICATORS_FOLDER):
|
||||
os.makedirs(MVT_INDICATORS_FOLDER)
|
||||
|
||||
index = self.get_remote_index()
|
||||
for ioc in index.get("indicators", []):
|
||||
ioc_type = ioc.get("type", "")
|
||||
|
||||
if ioc_type == "github":
|
||||
github = ioc.get("github", {})
|
||||
owner = github.get("owner", "")
|
||||
repo = github.get("repo", "")
|
||||
branch = github.get("branch", "main")
|
||||
path = github.get("path", "")
|
||||
|
||||
ioc_url = self.github_raw_url.format(owner, repo, branch, path)
|
||||
else:
|
||||
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"))
|
||||
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)
|
||||
|
||||
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}"
|
||||
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
|
||||
|
||||
details = res.json()
|
||||
if len(details) == 0:
|
||||
return False
|
||||
|
||||
latest_commit = details[0]
|
||||
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
|
||||
|
||||
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):
|
||||
now = datetime.utcnow()
|
||||
latest_check_ts = self.get_latest_check()
|
||||
latest_check_dt = datetime.fromtimestamp(latest_check_ts)
|
||||
|
||||
diff = now - latest_check_dt
|
||||
diff_hours = divmod(diff.total_seconds(), 3600)[0]
|
||||
|
||||
if diff_hours >= INDICATORS_CHECK_FREQUENCY:
|
||||
return True, 0
|
||||
|
||||
return False, 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)
|
||||
|
||||
if latest_update < latest_commit_ts:
|
||||
return True
|
||||
|
||||
index = self.get_remote_index()
|
||||
for ioc in index.get("indicators", []):
|
||||
if ioc.get("type", "") != "github":
|
||||
continue
|
||||
|
||||
github = ioc.get("github", {})
|
||||
owner = github.get("owner", "")
|
||||
repo = github.get("repo", "")
|
||||
branch = github.get("branch", "main")
|
||||
path = github.get("path", "")
|
||||
|
||||
file_latest_commit_ts = self._get_remote_file_latest_commit(owner,
|
||||
repo,
|
||||
branch,
|
||||
path)
|
||||
if latest_update < file_latest_commit_ts:
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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,6 +7,7 @@ import requests
|
||||
from tld import get_tld
|
||||
|
||||
SHORTENER_DOMAINS = [
|
||||
"1drv.ms",
|
||||
"1link.in",
|
||||
"1url.com",
|
||||
"2big.at",
|
||||
@@ -15,29 +16,29 @@ SHORTENER_DOMAINS = [
|
||||
"2ya.com",
|
||||
"4url.cc",
|
||||
"6url.com",
|
||||
"a.gg",
|
||||
"a.nf",
|
||||
"a2a.me",
|
||||
"abbrr.com",
|
||||
"adf.ly",
|
||||
"adjix.com",
|
||||
"a.gg",
|
||||
"alturl.com",
|
||||
"a.nf",
|
||||
"atu.ca",
|
||||
"b23.ru",
|
||||
"bacn.me",
|
||||
"bit.ly",
|
||||
"bit.do",
|
||||
"bit.ly",
|
||||
"bkite.com",
|
||||
"bloat.me",
|
||||
"budurl.com",
|
||||
"buff.ly",
|
||||
"buk.me",
|
||||
"burnurl.com",
|
||||
"c-o.in",
|
||||
"chilp.it",
|
||||
"clck.ru",
|
||||
"clickmeter.com",
|
||||
"cli.gs",
|
||||
"c-o.in",
|
||||
"clickmeter.com",
|
||||
"cort.as",
|
||||
"cut.ly",
|
||||
"cuturl.com",
|
||||
@@ -55,19 +56,20 @@ SHORTENER_DOMAINS = [
|
||||
"esyurl.com",
|
||||
"ewerl.com",
|
||||
"fa.b",
|
||||
"fff.to",
|
||||
"ff.im",
|
||||
"fff.to",
|
||||
"fhurl.com",
|
||||
"fire.to",
|
||||
"firsturl.de",
|
||||
"flic.kr",
|
||||
"fly2.ws",
|
||||
"fon.gs",
|
||||
"forms.gle",
|
||||
"fwd4.me",
|
||||
"gl.am",
|
||||
"go2cut.com",
|
||||
"go2.me",
|
||||
"go.9nl.com",
|
||||
"go2.me",
|
||||
"go2cut.com",
|
||||
"goo.gl",
|
||||
"goshrink.com",
|
||||
"gowat.ch",
|
||||
@@ -77,6 +79,7 @@ SHORTENER_DOMAINS = [
|
||||
"hex.io",
|
||||
"hover.com",
|
||||
"href.in",
|
||||
"ht.ly",
|
||||
"htxt.it",
|
||||
"hugeurl.com",
|
||||
"hurl.it",
|
||||
@@ -85,8 +88,8 @@ SHORTENER_DOMAINS = [
|
||||
"icanhaz.com",
|
||||
"idek.net",
|
||||
"inreply.to",
|
||||
"iscool.net",
|
||||
"is.gd",
|
||||
"iscool.net",
|
||||
"iterasi.net",
|
||||
"jijr.com",
|
||||
"jmp2.net",
|
||||
@@ -102,10 +105,11 @@ SHORTENER_DOMAINS = [
|
||||
"linkbee.com",
|
||||
"linkbun.ch",
|
||||
"liurl.cn",
|
||||
"lnk.gd",
|
||||
"lnk.in",
|
||||
"ln-s.net",
|
||||
"ln-s.ru",
|
||||
"lnk.gd",
|
||||
"lnk.in",
|
||||
"lnkd.in",
|
||||
"loopt.us",
|
||||
"lru.jp",
|
||||
"lt.tl",
|
||||
@@ -122,44 +126,44 @@ SHORTENER_DOMAINS = [
|
||||
"nn.nf",
|
||||
"notlong.com",
|
||||
"nsfw.in",
|
||||
"o-x.fr",
|
||||
"om.ly",
|
||||
"ow.ly",
|
||||
"o-x.fr",
|
||||
"pd.am",
|
||||
"pic.gd",
|
||||
"ping.fm",
|
||||
"piurl.com",
|
||||
"pnt.me",
|
||||
"poprl.com",
|
||||
"posted.at",
|
||||
"post.ly",
|
||||
"posted.at",
|
||||
"profile.to",
|
||||
"qicute.com",
|
||||
"qlnk.net",
|
||||
"quip-art.com",
|
||||
"rb6.me",
|
||||
"redirx.com",
|
||||
"rickroll.it",
|
||||
"ri.ms",
|
||||
"rickroll.it",
|
||||
"riz.gd",
|
||||
"rsmonkey.com",
|
||||
"rubyurl.com",
|
||||
"ru.ly",
|
||||
"rubyurl.com",
|
||||
"s7y.us",
|
||||
"safe.mn",
|
||||
"sharein.com",
|
||||
"sharetabs.com",
|
||||
"shorl.com",
|
||||
"short.ie",
|
||||
"short.to",
|
||||
"shortlinks.co.uk",
|
||||
"shortna.me",
|
||||
"short.to",
|
||||
"shorturl.com",
|
||||
"shoturl.us",
|
||||
"shrinkify.com",
|
||||
"shrinkster.com",
|
||||
"shrten.com",
|
||||
"shrt.st",
|
||||
"shrten.com",
|
||||
"shrunkin.com",
|
||||
"shw.me",
|
||||
"simurl.com",
|
||||
@@ -177,20 +181,20 @@ SHORTENER_DOMAINS = [
|
||||
"tcrn.ch",
|
||||
"thrdl.es",
|
||||
"tighturl.com",
|
||||
"tiny123.com",
|
||||
"tinyarro.ws",
|
||||
"tiny.cc",
|
||||
"tiny.pl",
|
||||
"tiny123.com",
|
||||
"tinyarro.ws",
|
||||
"tinytw.it",
|
||||
"tinyuri.ca",
|
||||
"tinyurl.com",
|
||||
"tinyvid.io",
|
||||
"tnij.org",
|
||||
"togoto.us",
|
||||
"to.ly",
|
||||
"traceurl.com",
|
||||
"togoto.us",
|
||||
"tr.im",
|
||||
"tr.my",
|
||||
"traceurl.com",
|
||||
"turo.us",
|
||||
"tweetburner.com",
|
||||
"twirl.at",
|
||||
@@ -200,21 +204,21 @@ SHORTENER_DOMAINS = [
|
||||
"twiturl.de",
|
||||
"twurl.cc",
|
||||
"twurl.nl",
|
||||
"u6e.de",
|
||||
"ub0.cc",
|
||||
"u.mavrev.com",
|
||||
"u.nu",
|
||||
"u6e.de",
|
||||
"ub0.cc",
|
||||
"updating.me",
|
||||
"ur1.ca",
|
||||
"url.co.uk",
|
||||
"url.ie",
|
||||
"url4.eu",
|
||||
"urlao.com",
|
||||
"urlbrief.com",
|
||||
"url.co.uk",
|
||||
"urlcover.com",
|
||||
"urlcut.com",
|
||||
"urlenco.de",
|
||||
"urlhawk.com",
|
||||
"url.ie",
|
||||
"urlkiss.com",
|
||||
"urlot.com",
|
||||
"urlpire.com",
|
||||
@@ -227,32 +231,29 @@ SHORTENER_DOMAINS = [
|
||||
"wapurl.co.uk",
|
||||
"wipi.es",
|
||||
"wp.me",
|
||||
"xaddr.com",
|
||||
"x.co",
|
||||
"x.se",
|
||||
"xaddr.com",
|
||||
"xeeurl.com",
|
||||
"xr.com",
|
||||
"xrl.in",
|
||||
"xrl.us",
|
||||
"x.se",
|
||||
"xurl.jp",
|
||||
"xzb.cc",
|
||||
"yep.it",
|
||||
"yfrog.com",
|
||||
"ymlp.com",
|
||||
"yweb.com",
|
||||
"zi.ma",
|
||||
"zi.pe",
|
||||
"zipmyurl.com",
|
||||
"zz.gd",
|
||||
"ymlp.com",
|
||||
"forms.gle",
|
||||
"ht.ly",
|
||||
"lnkd.in",
|
||||
"1drv.ms",
|
||||
]
|
||||
|
||||
|
||||
class URL:
|
||||
|
||||
def __init__(self, url):
|
||||
def __init__(self, url: str) -> None:
|
||||
if type(url) == bytes:
|
||||
url = url.decode()
|
||||
|
||||
@@ -261,35 +262,54 @@ class URL:
|
||||
self.top_level = self.get_top_level()
|
||||
self.is_shortened = False
|
||||
|
||||
def get_domain(self):
|
||||
def get_domain(self) -> None:
|
||||
"""Get the domain from a URL.
|
||||
|
||||
:param url: URL to parse
|
||||
:returns: Just the domain name extracted from the URL
|
||||
:type url: str
|
||||
:returns: Domain name extracted from 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:
|
||||
return get_tld(self.url,
|
||||
as_object=True,
|
||||
fix_protocol=True).parsed_url.netloc.lower().lstrip("www.")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_top_level(self):
|
||||
"""Get only the top level domain from a URL.
|
||||
def get_top_level(self) -> None:
|
||||
"""Get only the top-level domain from a URL.
|
||||
|
||||
:param url: URL to parse
|
||||
:returns: The top level domain extracted from the URL
|
||||
:type url: str
|
||||
:returns: Top-level domain name extracted from URL
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
# TODO: Properly handle exception.
|
||||
try:
|
||||
return get_tld(self.url, as_object=True, fix_protocol=True).fld.lower()
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def check_if_shortened(self):
|
||||
def check_if_shortened(self) -> bool:
|
||||
"""Check if the URL is among list of shortener services.
|
||||
|
||||
|
||||
:returns: True if the URL is shortened, otherwise False
|
||||
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
if self.domain.lower() in SHORTENER_DOMAINS:
|
||||
self.is_shortened = True
|
||||
|
||||
return self.is_shortened
|
||||
|
||||
def unshorten(self):
|
||||
def unshorten(self) -> None:
|
||||
"""Unshorten the URL by requesting an HTTP HEAD response."""
|
||||
res = requests.head(self.url)
|
||||
if str(res.status_code).startswith("30"):
|
||||
return res.headers["Location"]
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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 hashlib
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
def convert_mactime_to_unix(timestamp, from_2001=True):
|
||||
def convert_mactime_to_unix(timestamp, from_2001: bool = True):
|
||||
"""Converts Mac Standard Time to a Unix timestamp.
|
||||
:param timestamp: MacTime timestamp (either int or float)
|
||||
:returns: Unix epoch 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: Unix epoch timestamp.
|
||||
|
||||
"""
|
||||
if not timestamp:
|
||||
return None
|
||||
@@ -33,37 +37,51 @@ def convert_mactime_to_unix(timestamp, from_2001=True):
|
||||
return None
|
||||
|
||||
|
||||
def convert_chrometime_to_unix(timestamp):
|
||||
def convert_chrometime_to_unix(timestamp: int) -> int:
|
||||
"""Converts Chrome timestamp to a Unix timestamp.
|
||||
:param timestamp: Chrome timestamp as int
|
||||
:returns: Unix epoch timestamp
|
||||
|
||||
:param timestamp: Chrome timestamp as int.
|
||||
:type timestamp: int
|
||||
:returns: Unix epoch timestamp.
|
||||
|
||||
"""
|
||||
epoch_start = datetime.datetime(1601, 1 , 1)
|
||||
epoch_start = datetime.datetime(1601, 1, 1)
|
||||
delta = datetime.timedelta(microseconds=timestamp)
|
||||
return epoch_start + delta
|
||||
|
||||
|
||||
def convert_timestamp_to_iso(timestamp):
|
||||
def convert_timestamp_to_iso(timestamp: str) -> str:
|
||||
"""Converts Unix timestamp to ISO string.
|
||||
:param timestamp: Unix timestamp
|
||||
:returns: ISO timestamp string in YYYY-mm-dd HH:MM:SS.ms format
|
||||
|
||||
:param timestamp: Unix timestamp.
|
||||
:type timestamp: int
|
||||
: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
|
||||
|
||||
def check_for_links(text):
|
||||
"""Checks if a given text contains HTTP links.
|
||||
:param text: Any provided text
|
||||
:returns: Search results
|
||||
"""
|
||||
return re.findall("(?P<url>https?://[^\s]+)", text, re.IGNORECASE)
|
||||
|
||||
def get_sha256_from_file_path(file_path):
|
||||
def check_for_links(text: str) -> list:
|
||||
"""Checks if a given text contains HTTP links.
|
||||
|
||||
:param text: Any provided text.
|
||||
:type text: str
|
||||
:returns: Search results.
|
||||
|
||||
"""
|
||||
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:
|
||||
@@ -72,12 +90,16 @@ def get_sha256_from_file_path(file_path):
|
||||
|
||||
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):
|
||||
def keys_bytes_to_string(obj) -> str:
|
||||
"""Convert object keys from bytes to string.
|
||||
|
||||
:param obj: Object to convert from bytes to string.
|
||||
:returns: Converted object.
|
||||
:returns: Object converted to string.
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
new_obj = {}
|
||||
if not isinstance(obj, dict):
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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 = "1.6"
|
||||
45
mvt/common/virustotal.py
Normal file
45
mvt/common/virustotal.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 requests
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
MVT_VT_API_KEY = "MVT_VT_API_KEY"
|
||||
|
||||
|
||||
class VTNoKey(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class VTQuotaExceeded(Exception):
|
||||
pass
|
||||
|
||||
|
||||
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>`")
|
||||
|
||||
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)
|
||||
|
||||
if res.status_code == 200:
|
||||
report = res.json()
|
||||
return report["data"]
|
||||
elif 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)
|
||||
|
||||
return None
|
||||
@@ -1,5 +1,5 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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/
|
||||
|
||||
|
||||
263
mvt/ios/cli.py
263
mvt/ios/cli.py
@@ -1,22 +1,30 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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 tarfile
|
||||
|
||||
import click
|
||||
from rich.logging import RichHandler
|
||||
from rich.prompt import Prompt
|
||||
|
||||
from mvt.common.indicators import Indicators
|
||||
from mvt.common.module import run_module, save_timeline
|
||||
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.logo import logo
|
||||
from mvt.common.options import MutuallyExclusiveOption
|
||||
from mvt.common.updates import IndicatorsUpdates
|
||||
|
||||
from .cmd_check_backup import CmdIOSCheckBackup
|
||||
from .cmd_check_fs import CmdIOSCheckFS
|
||||
from .cmd_check_usb import CmdIOSCheckUSB
|
||||
from .decrypt import DecryptBackup
|
||||
from .modules.fs import BACKUP_MODULES, FS_MODULES
|
||||
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"
|
||||
@@ -24,17 +32,23 @@ logging.basicConfig(level="INFO", format=LOG_FORMAT, handlers=[
|
||||
RichHandler(show_path=False, log_time_format="%X")])
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Help messages of repeating options.
|
||||
OUTPUT_HELP_MESSAGE = "Specify a path to a folder where you want to store JSON results"
|
||||
|
||||
# Set this environment variable to a password if needed.
|
||||
PASSWD_ENV = "MVT_IOS_BACKUP_PASSWORD"
|
||||
MVT_IOS_BACKUP_PASSWORD = "MVT_IOS_BACKUP_PASSWORD"
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# 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
|
||||
|
||||
|
||||
@@ -45,7 +59,7 @@ def cli():
|
||||
@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 {PASSWD_ENV} environment variable)",
|
||||
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),
|
||||
@@ -57,28 +71,29 @@ def decrypt_backup(ctx, destination, password, key_file, backup_path):
|
||||
backup = DecryptBackup(backup_path, destination)
|
||||
|
||||
if key_file:
|
||||
if PASSWD_ENV in os.environ:
|
||||
if MVT_IOS_BACKUP_PASSWORD in os.environ:
|
||||
log.info("Ignoring environment variable, using --key-file '%s' instead",
|
||||
PASSWD_ENV, key_file)
|
||||
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!")
|
||||
|
||||
if PASSWD_ENV in os.environ:
|
||||
if MVT_IOS_BACKUP_PASSWORD in os.environ:
|
||||
log.info("Ignoring %s environment variable, using --password argument instead",
|
||||
PASSWD_ENV)
|
||||
MVT_IOS_BACKUP_PASSWORD)
|
||||
|
||||
backup.decrypt_with_password(password)
|
||||
elif PASSWD_ENV in os.environ:
|
||||
log.info("Using password from %s environment variable", PASSWD_ENV)
|
||||
backup.decrypt_with_password(os.environ[PASSWD_ENV])
|
||||
elif MVT_IOS_BACKUP_PASSWORD in os.environ:
|
||||
log.info("Using password from %s environment variable", MVT_IOS_BACKUP_PASSWORD)
|
||||
backup.decrypt_with_password(os.environ[MVT_IOS_BACKUP_PASSWORD])
|
||||
else:
|
||||
sekrit = Prompt.ask("Enter backup password", password=True)
|
||||
backup.decrypt_with_password(sekrit)
|
||||
|
||||
if not backup.can_process():
|
||||
ctx.exit(1)
|
||||
|
||||
backup.process_backup()
|
||||
|
||||
|
||||
@@ -87,24 +102,24 @@ def decrypt_backup(ctx, destination, password, key_file, backup_path):
|
||||
#==============================================================================
|
||||
@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 {PASSWD_ENV} environment variable)")
|
||||
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))
|
||||
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
|
||||
def extract_key(password, backup_path, key_file):
|
||||
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!")
|
||||
|
||||
if PASSWD_ENV in os.environ:
|
||||
if MVT_IOS_BACKUP_PASSWORD in os.environ:
|
||||
log.info("Ignoring %s environment variable, using --password argument instead",
|
||||
PASSWD_ENV)
|
||||
elif PASSWD_ENV in os.environ:
|
||||
log.info("Using password from %s environment variable", PASSWD_ENV)
|
||||
password = os.environ[PASSWD_ENV]
|
||||
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]
|
||||
else:
|
||||
password = Prompt.ask("Enter backup password", password=True)
|
||||
|
||||
@@ -119,168 +134,116 @@ def extract_key(password, backup_path, 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), help="Path to indicators file")
|
||||
@click.option("--output", "-o", type=click.Path(exists=False), help=OUTPUT_HELP_MESSAGE)
|
||||
@click.option("--fast", "-f", is_flag=True, help="Avoid running time/resource consuming features")
|
||||
@click.option("--list-modules", "-l", is_flag=True, help="Print list of available modules and exit")
|
||||
@click.option("--module", "-m", help="Name of a single module you would like to run instead of all")
|
||||
@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.argument("BACKUP_PATH", type=click.Path(exists=True))
|
||||
@click.pass_context
|
||||
def check_backup(ctx, iocs, output, fast, backup_path, list_modules, module):
|
||||
if list_modules:
|
||||
log.info("Following is the list of available check-backup modules:")
|
||||
for backup_module in BACKUP_MODULES:
|
||||
log.info(" - %s", backup_module.__name__)
|
||||
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)
|
||||
|
||||
if list_modules:
|
||||
cmd.list_modules()
|
||||
return
|
||||
|
||||
log.info("Checking iTunes backup located at: %s", backup_path)
|
||||
|
||||
if output and not os.path.exists(output):
|
||||
try:
|
||||
os.makedirs(output)
|
||||
except Exception as e:
|
||||
log.critical("Unable to create output folder %s: %s", output, e)
|
||||
ctx.exit(1)
|
||||
cmd.run()
|
||||
|
||||
if iocs:
|
||||
# Pre-load indicators for performance reasons.
|
||||
log.info("Loading indicators from provided file at: %s", iocs)
|
||||
indicators = Indicators(iocs)
|
||||
|
||||
timeline = []
|
||||
timeline_detected = []
|
||||
for backup_module in BACKUP_MODULES:
|
||||
if module and backup_module.__name__ != module:
|
||||
continue
|
||||
|
||||
m = backup_module(base_folder=backup_path, output_folder=output, fast_mode=fast,
|
||||
log=logging.getLogger(backup_module.__module__))
|
||||
m.is_backup = True
|
||||
|
||||
if iocs:
|
||||
indicators.log = m.log
|
||||
m.indicators = indicators
|
||||
|
||||
run_module(m)
|
||||
timeline.extend(m.timeline)
|
||||
timeline_detected.extend(m.timeline_detected)
|
||||
|
||||
if output:
|
||||
if len(timeline) > 0:
|
||||
save_timeline(timeline, os.path.join(output, "timeline.csv"))
|
||||
if len(timeline_detected) > 0:
|
||||
save_timeline(timeline_detected, os.path.join(output, "timeline_detected.csv"))
|
||||
if len(cmd.timeline_detected) > 0:
|
||||
log.warning("The analysis of the backup produced %d detections!",
|
||||
len(cmd.timeline_detected))
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# Command: check-fs
|
||||
#==============================================================================
|
||||
@cli.command("check-fs", help="Extract artifacts from a full filesystem dump")
|
||||
@click.option("--iocs", "-i", type=click.Path(exists=True), help="Path to indicators file")
|
||||
@click.option("--output", "-o", type=click.Path(exists=False), help=OUTPUT_HELP_MESSAGE)
|
||||
@click.option("--fast", "-f", is_flag=True, help="Avoid running time/resource consuming features")
|
||||
@click.option("--list-modules", "-l", is_flag=True, help="Print list of available modules and exit")
|
||||
@click.option("--module", "-m", help="Name of a single module you would like to run instead of all")
|
||||
@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.argument("DUMP_PATH", type=click.Path(exists=True))
|
||||
@click.pass_context
|
||||
def check_fs(ctx, iocs, output, fast, dump_path, list_modules, module):
|
||||
if list_modules:
|
||||
log.info("Following is the list of available check-fs modules:")
|
||||
for fs_module in FS_MODULES:
|
||||
log.info(" - %s", fs_module.__name__)
|
||||
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)
|
||||
|
||||
if list_modules:
|
||||
cmd.list_modules()
|
||||
return
|
||||
|
||||
log.info("Checking filesystem dump located at: %s", dump_path)
|
||||
log.info("Checking iOS filesystem located at: %s", dump_path)
|
||||
|
||||
if output and not os.path.exists(output):
|
||||
try:
|
||||
os.makedirs(output)
|
||||
except Exception as e:
|
||||
log.critical("Unable to create output folder %s: %s", output, e)
|
||||
ctx.exit(1)
|
||||
cmd.run()
|
||||
|
||||
if iocs:
|
||||
# Pre-load indicators for performance reasons.
|
||||
log.info("Loading indicators from provided file at: %s", iocs)
|
||||
indicators = Indicators(iocs)
|
||||
|
||||
timeline = []
|
||||
timeline_detected = []
|
||||
for fs_module in FS_MODULES:
|
||||
if module and fs_module.__name__ != module:
|
||||
continue
|
||||
|
||||
m = fs_module(base_folder=dump_path, output_folder=output, fast_mode=fast,
|
||||
log=logging.getLogger(fs_module.__module__))
|
||||
|
||||
m.is_fs_dump = True
|
||||
|
||||
if iocs:
|
||||
indicators.log = m.log
|
||||
m.indicators = indicators
|
||||
|
||||
run_module(m)
|
||||
timeline.extend(m.timeline)
|
||||
timeline_detected.extend(m.timeline_detected)
|
||||
|
||||
if output:
|
||||
if len(timeline) > 0:
|
||||
save_timeline(timeline, os.path.join(output, "timeline.csv"))
|
||||
if len(timeline_detected) > 0:
|
||||
save_timeline(timeline_detected, os.path.join(output, "timeline_detected.csv"))
|
||||
if len(cmd.timeline_detected) > 0:
|
||||
log.warning("The analysis of the iOS filesystem produced %d detections!",
|
||||
len(cmd.timeline_detected))
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# Command: check-iocs
|
||||
#==============================================================================
|
||||
@cli.command("check-iocs", help="Compare stored JSON results to provided indicators")
|
||||
@click.option("--iocs", "-i", required=True, type=click.Path(exists=True),
|
||||
help="Path to indicators file")
|
||||
@click.option("--list-modules", "-l", is_flag=True, help="Print list of available modules and exit")
|
||||
@click.option("--module", "-m", help="Name of a single module you would like to run instead of all")
|
||||
@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))
|
||||
def check_iocs(iocs, list_modules, module, folder):
|
||||
all_modules = []
|
||||
for entry in BACKUP_MODULES + FS_MODULES:
|
||||
if entry not in all_modules:
|
||||
all_modules.append(entry)
|
||||
@click.pass_context
|
||||
def check_iocs(ctx, iocs, list_modules, module, folder):
|
||||
cmd = CmdCheckIOCS(target_path=folder, ioc_files=iocs, module_name=module)
|
||||
cmd.modules = BACKUP_MODULES + FS_MODULES + MIXED_MODULES
|
||||
|
||||
if list_modules:
|
||||
log.info("Following is the list of available check-iocs modules:")
|
||||
for iocs_module in all_modules:
|
||||
log.info(" - %s", iocs_module.__name__)
|
||||
|
||||
cmd.list_modules()
|
||||
return
|
||||
|
||||
log.info("Checking stored results against provided indicators...")
|
||||
cmd.run()
|
||||
|
||||
# Pre-load indicators for performance reasons.
|
||||
log.info("Loading indicators from provided file at: %s", iocs)
|
||||
indicators = Indicators(iocs)
|
||||
|
||||
for file_name in os.listdir(folder):
|
||||
name_only, ext = os.path.splitext(file_name)
|
||||
file_path = os.path.join(folder, file_name)
|
||||
#==============================================================================
|
||||
# Command: download-iocs
|
||||
#==============================================================================
|
||||
@cli.command("download-iocs", help="Download public STIX2 indicators")
|
||||
def download_iocs():
|
||||
ioc_updates = IndicatorsUpdates()
|
||||
ioc_updates.update()
|
||||
|
||||
for iocs_module in all_modules:
|
||||
if module and iocs_module.__name__ != module:
|
||||
continue
|
||||
|
||||
if iocs_module().get_slug() != name_only:
|
||||
continue
|
||||
#==============================================================================
|
||||
# Command: check-usb
|
||||
#==============================================================================
|
||||
@cli.command("check-usb", help="Extract artifacts from a live iPhone through USB / lockdown")
|
||||
@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("--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)
|
||||
# TODO: serial
|
||||
# @click.argument("BACKUP_PATH", type=click.Path(exists=True))
|
||||
@click.pass_context
|
||||
def check_usb(ctx, serial, iocs, output, fast, list_modules, module):
|
||||
cmd = CmdIOSCheckUSB(results_path=output, ioc_files=iocs,
|
||||
module_name=module, fast_mode=fast,
|
||||
serial=serial)
|
||||
|
||||
log.info("Loading results from \"%s\" with module %s", file_name,
|
||||
iocs_module.__name__)
|
||||
if list_modules:
|
||||
cmd.list_modules()
|
||||
return
|
||||
|
||||
m = iocs_module.from_json(file_path,
|
||||
log=logging.getLogger(iocs_module.__module__))
|
||||
log.info("Checking iPhone through USB, this may take a while")
|
||||
cmd.run()
|
||||
|
||||
indicators.log = m.log
|
||||
m.indicators = indicators
|
||||
|
||||
try:
|
||||
m.check_indicators()
|
||||
except NotImplementedError:
|
||||
continue
|
||||
if len(cmd.timeline_detected) > 0:
|
||||
log.warning("The analysis of the data produced %d detections!",
|
||||
len(cmd.timeline_detected))
|
||||
|
||||
29
mvt/ios/cmd_check_backup.py
Normal file
29
mvt/ios/cmd_check_backup.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 mvt.common.command import Command
|
||||
|
||||
from .modules.backup import BACKUP_MODULES
|
||||
from .modules.mixed import MIXED_MODULES
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdIOSCheckBackup(Command):
|
||||
|
||||
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)
|
||||
|
||||
def module_init(self, module):
|
||||
module.is_backup = True
|
||||
29
mvt/ios/cmd_check_fs.py
Normal file
29
mvt/ios/cmd_check_fs.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 mvt.common.command import Command
|
||||
|
||||
from .modules.fs import FS_MODULES
|
||||
from .modules.mixed import MIXED_MODULES
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdIOSCheckFS(Command):
|
||||
|
||||
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)
|
||||
|
||||
def module_init(self, module):
|
||||
module.is_fs_dump = True
|
||||
46
mvt/ios/cmd_check_usb.py
Normal file
46
mvt/ios/cmd_check_usb.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 sys
|
||||
|
||||
from pymobiledevice3.exceptions import ConnectionFailedError
|
||||
from pymobiledevice3.lockdown import LockdownClient
|
||||
|
||||
from mvt.common.command import Command
|
||||
|
||||
from .modules.usb import USB_MODULES
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdIOSCheckUSB(Command):
|
||||
|
||||
name = "check-usb"
|
||||
modules = USB_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.lockdown = None
|
||||
|
||||
def init(self):
|
||||
try:
|
||||
if self.serial:
|
||||
self.lockdown = LockdownClient(udid=self.serial)
|
||||
else:
|
||||
self.lockdown = LockdownClient()
|
||||
except ConnectionRefusedError:
|
||||
log.error("Unable to connect to the device over USB. Try to unplug, plug the device and start again.")
|
||||
sys.exit(-1)
|
||||
except ConnectionFailedError:
|
||||
log.error("Unable to connect to the device %s", self.serial)
|
||||
sys.exit(-1)
|
||||
|
||||
def module_init(self, module):
|
||||
module.lockdown = self.lockdown
|
||||
@@ -1,12 +1,14 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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 binascii
|
||||
import glob
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
import os.path
|
||||
import shutil
|
||||
import sqlite3
|
||||
|
||||
@@ -14,25 +16,52 @@ from iOSbackup import iOSbackup
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DecryptBackup:
|
||||
"""This class provides functions to decrypt an encrypted iTunes backup
|
||||
using either a password or a key file.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, backup_path, dest_path=None):
|
||||
def __init__(self, backup_path: str, dest_path: str = None) -> None:
|
||||
"""Decrypts an encrypted iOS backup.
|
||||
:param backup_path: Path to the encrypted backup folder
|
||||
:param dest_path: Path to the folder where to store the decrypted backup
|
||||
"""
|
||||
self.backup_path = backup_path
|
||||
self.backup_path = os.path.abspath(backup_path)
|
||||
self.dest_path = dest_path
|
||||
self._backup = None
|
||||
self._decryption_key = None
|
||||
|
||||
def can_process(self) -> bool:
|
||||
return self._backup is not None
|
||||
|
||||
def process_backup(self):
|
||||
|
||||
@staticmethod
|
||||
def is_encrypted(backup_path: str) -> bool:
|
||||
"""Query Manifest.db file to see if it's encrypted or not.
|
||||
|
||||
:param backup_path: Path to the backup to decrypt
|
||||
|
||||
"""
|
||||
conn = sqlite3.connect(os.path.join(backup_path, "Manifest.db"))
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
cur.execute("SELECT fileID FROM Files LIMIT 1;")
|
||||
except sqlite3.DatabaseError:
|
||||
return True
|
||||
else:
|
||||
log.critical("The backup does not seem encrypted!")
|
||||
return False
|
||||
|
||||
def _process_file(self, relative_path: str, domain: str, item,
|
||||
file_id: str, item_folder: str) -> None:
|
||||
self._backup.getFileDecryptedCopy(manifestEntry=item,
|
||||
targetName=file_id,
|
||||
targetFolder=item_folder)
|
||||
log.info("Decrypted file %s [%s] to %s/%s", relative_path, domain, item_folder, file_id)
|
||||
|
||||
def process_backup(self) -> None:
|
||||
if not os.path.exists(self.dest_path):
|
||||
os.makedirs(self.dest_path)
|
||||
|
||||
@@ -42,6 +71,8 @@ class DecryptBackup:
|
||||
# We store it to the destination folder.
|
||||
shutil.copy(self._backup.manifestDB, manifest_path)
|
||||
|
||||
pool = multiprocessing.Pool(multiprocessing.cpu_count())
|
||||
|
||||
for item in self._backup.getBackupFilesList():
|
||||
try:
|
||||
file_id = item["backupFile"]
|
||||
@@ -64,16 +95,28 @@ class DecryptBackup:
|
||||
# Add manifest plist to both keys to handle this.
|
||||
item["manifest"] = item["file"]
|
||||
|
||||
self._backup.getFileDecryptedCopy(manifestEntry=item,
|
||||
targetName=file_id,
|
||||
targetFolder=item_folder)
|
||||
log.info("Decrypted file %s [%s] to %s/%s", relative_path, domain, item_folder, file_id)
|
||||
pool.apply_async(self._process_file, args=(relative_path,
|
||||
domain, item,
|
||||
file_id,
|
||||
item_folder))
|
||||
except Exception as e:
|
||||
log.error("Failed to decrypt file %s: %s", relative_path, e)
|
||||
|
||||
def decrypt_with_password(self, password):
|
||||
pool.close()
|
||||
pool.join()
|
||||
|
||||
# Copying over the root plist files as well.
|
||||
for file_name in os.listdir(self.backup_path):
|
||||
if file_name.endswith(".plist"):
|
||||
log.info("Copied plist file %s to %s", file_name, self.dest_path)
|
||||
shutil.copy(os.path.join(self.backup_path, file_name),
|
||||
self.dest_path)
|
||||
|
||||
def decrypt_with_password(self, password: str) -> None:
|
||||
"""Decrypts an encrypted iOS backup.
|
||||
|
||||
:param password: Password to use to decrypt the original backup
|
||||
|
||||
"""
|
||||
log.info("Decrypting iOS backup at path %s with password", self.backup_path)
|
||||
|
||||
@@ -88,6 +131,11 @@ class DecryptBackup:
|
||||
log.critical("No Manifest.plist in %s, and %d Manifest.plist files in subdirs. Please choose one!",
|
||||
self.backup_path, len(possible))
|
||||
return
|
||||
|
||||
# Before proceeding, we check whether the backup is indeed encrypted.
|
||||
if not self.is_encrypted(self.backup_path):
|
||||
return
|
||||
|
||||
try:
|
||||
self._backup = iOSbackup(udid=os.path.basename(self.backup_path),
|
||||
cleartextpassword=password,
|
||||
@@ -102,13 +150,19 @@ class DecryptBackup:
|
||||
log.exception(e)
|
||||
log.critical("Failed to decrypt backup. Did you provide the correct password? Did you point to the right backup path?")
|
||||
|
||||
def decrypt_with_key_file(self, key_file):
|
||||
def decrypt_with_key_file(self, key_file: str) -> None:
|
||||
"""Decrypts an encrypted iOS backup using a key file.
|
||||
|
||||
:param key_file: File to read the key bytes to decrypt the backup
|
||||
|
||||
"""
|
||||
log.info("Decrypting iOS backup at path %s with key file %s",
|
||||
self.backup_path, key_file)
|
||||
|
||||
# Before proceeding, we check whether the backup is indeed encrypted.
|
||||
if not self.is_encrypted(self.backup_path):
|
||||
return
|
||||
|
||||
with open(key_file, "rb") as handle:
|
||||
key_bytes = handle.read()
|
||||
|
||||
@@ -126,9 +180,8 @@ class DecryptBackup:
|
||||
log.exception(e)
|
||||
log.critical("Failed to decrypt backup. Did you provide the correct key file?")
|
||||
|
||||
def get_key(self):
|
||||
"""Retrieve and prints the encryption key.
|
||||
"""
|
||||
def get_key(self) -> None:
|
||||
"""Retrieve and prints the encryption key."""
|
||||
if not self._backup:
|
||||
return
|
||||
|
||||
@@ -136,15 +189,17 @@ class DecryptBackup:
|
||||
log.info("Derived decryption key for backup at path %s is: \"%s\"",
|
||||
self.backup_path, self._decryption_key)
|
||||
|
||||
def write_key(self, key_path):
|
||||
def write_key(self, key_path: str) -> None:
|
||||
"""Save extracted key to file.
|
||||
|
||||
:param key_path: Path to the file where to write the derived decryption key.
|
||||
|
||||
"""
|
||||
if not self._decryption_key:
|
||||
return
|
||||
|
||||
try:
|
||||
with open(key_path, 'w') as handle:
|
||||
with open(key_path, 'w', encoding="utf-8") as handle:
|
||||
handle.write(self._decryption_key)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021 The MVT Project Authors.
|
||||
# Copyright (c) 2021-2022 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
mvt/ios/modules/backup/__init__.py
Normal file
11
mvt/ios/modules/backup/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 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 .backup_info import BackupInfo
|
||||
from .configuration_profiles import ConfigurationProfiles
|
||||
from .manifest import Manifest
|
||||
from .profile_events import ProfileEvents
|
||||
|
||||
BACKUP_MODULES = [BackupInfo, ConfigurationProfiles, Manifest, ProfileEvents]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user