dissect.target 3.19.dev18__py3-none-any.whl → 3.19.dev19__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dissect/target/loader.py CHANGED
@@ -195,6 +195,7 @@ register("vma", "VmaLoader")
195
195
  register("kape", "KapeLoader")
196
196
  register("tanium", "TaniumLoader")
197
197
  register("itunes", "ITunesLoader")
198
+ register("ab", "AndroidBackupLoader")
198
199
  register("target", "TargetLoader")
199
200
  register("log", "LogLoader")
200
201
  # Disabling ResLoader because of DIS-536
@@ -0,0 +1,285 @@
1
+ import hashlib
2
+ import io
3
+ import posixpath
4
+ import shutil
5
+ import struct
6
+ from pathlib import Path
7
+ from typing import BinaryIO
8
+
9
+ try:
10
+ from Crypto.Cipher import AES
11
+
12
+ HAS_PYCRYPTODOME = True
13
+ except ImportError:
14
+ HAS_PYCRYPTODOME = False
15
+
16
+ from dissect.util.stream import AlignedStream, RelativeStream, ZlibStream
17
+
18
+ from dissect.target.exceptions import LoaderError
19
+ from dissect.target.filesystem import VirtualFilesystem
20
+ from dissect.target.filesystems.tar import TarFilesystem
21
+ from dissect.target.helpers import keychain
22
+ from dissect.target.loader import Loader
23
+ from dissect.target.plugins.os.unix.linux.android._os import AndroidPlugin
24
+ from dissect.target.target import Target
25
+
26
+ DIRECTORY_MAPPING = {
27
+ "a": "/data/app/{id}",
28
+ "f": "/data/data/{id}/files",
29
+ "db": "/data/data/{id}/databases",
30
+ "ef": "/storage/emulated/0/Android/data/{id}",
31
+ "sp": "/data/data/{id}/shared_preferences",
32
+ "r": "/data/data/{id}",
33
+ "obb": "/storage/emulated/0/Android/obb/{id}",
34
+ }
35
+
36
+
37
+ class AndroidBackupLoader(Loader):
38
+ """Load Android backup files.
39
+
40
+ References:
41
+ - http://fileformats.archiveteam.org/wiki/Android_ADB_Backup
42
+ """
43
+
44
+ def __init__(self, path: Path, **kwargs):
45
+ super().__init__(path)
46
+ self.ab = AndroidBackup(path.open("rb"))
47
+
48
+ if self.ab.encrypted:
49
+ for key in keychain.get_keys_for_provider("ab") + keychain.get_keys_without_provider():
50
+ if key.key_type == keychain.KeyType.PASSPHRASE:
51
+ try:
52
+ self.ab.unlock(key.value)
53
+ break
54
+ except ValueError:
55
+ continue
56
+ else:
57
+ raise LoaderError(f"Missing password for encrypted Android Backup: {self.path}")
58
+
59
+ @staticmethod
60
+ def detect(path: Path) -> bool:
61
+ if path.suffix.lower() == ".ab":
62
+ return True
63
+
64
+ if path.is_file():
65
+ # The file extension can be chosen freely so let's also test for the file magic
66
+ with path.open("rb") as fh:
67
+ if fh.read(15) == b"ANDROID BACKUP\n":
68
+ return True
69
+
70
+ return False
71
+
72
+ def map(self, target: Target) -> None:
73
+ if self.ab.compressed or self.ab.encrypted:
74
+ if self.ab.compressed and not self.ab.encrypted:
75
+ word = "compressed"
76
+ elif self.ab.encrypted and not self.ab.compressed:
77
+ word = "encrypted"
78
+ else:
79
+ word = "compressed and encrypted"
80
+
81
+ target.log.warning(
82
+ f"Backup file is {word}, consider unwrapping with "
83
+ "`python -m dissect.target.loaders.ab <path/to/backup.ab>`"
84
+ )
85
+
86
+ vfs = VirtualFilesystem(case_sensitive=False)
87
+
88
+ fs = TarFilesystem(self.ab.open())
89
+ for app in fs.path("/apps").iterdir():
90
+ for subdir in app.iterdir():
91
+ if subdir.name not in DIRECTORY_MAPPING:
92
+ continue
93
+
94
+ path = DIRECTORY_MAPPING[subdir.name].format(id=app.name)
95
+
96
+ # TODO: Remove once we move towards "directory entries"
97
+ entry = subdir.get()
98
+ entry.name = posixpath.basename(path)
99
+
100
+ vfs.map_file_entry(path, entry)
101
+
102
+ target.filesystems.add(vfs)
103
+ target._os_plugin = AndroidPlugin.create(target, vfs)
104
+
105
+
106
+ class AndroidBackup:
107
+ def __init__(self, fh: BinaryIO):
108
+ self.fh = fh
109
+
110
+ size = fh.seek(0, io.SEEK_END)
111
+ fh.seek(0)
112
+
113
+ # Don't readline() straight away as we may be reading something other than a backup file
114
+ magic = fh.read(15)
115
+ if magic != b"ANDROID BACKUP\n":
116
+ raise ValueError("Not a valid Android Backup file")
117
+
118
+ self.version = int(fh.read(2)[:1])
119
+ self.compressed = bool(int(fh.read(2)[:1]))
120
+
121
+ self.encrypted = False
122
+ self.unlocked = True
123
+ self.encryption = fh.readline().strip().decode()
124
+
125
+ if self.encryption != "none":
126
+ self.encrypted = True
127
+ self.unlocked = False
128
+ self._user_salt = bytes.fromhex(fh.readline().strip().decode())
129
+ self._ck_salt = bytes.fromhex(fh.readline().strip().decode())
130
+ self._rounds = int(fh.readline().strip())
131
+ self._user_iv = bytes.fromhex(fh.readline().strip().decode())
132
+ self._master_key = bytes.fromhex(fh.readline().strip().decode())
133
+
134
+ self._mk = None
135
+ self._iv = None
136
+
137
+ self._data_offset = fh.tell()
138
+ self.size = size - self._data_offset
139
+
140
+ def unlock(self, password: str) -> None:
141
+ if not self.encrypted:
142
+ raise ValueError("Android Backup is not encrypted")
143
+
144
+ self._mk, self._iv = self._decrypt_mk(password)
145
+ self.unlocked = True
146
+
147
+ def _decrypt_mk(self, password: str) -> tuple[bytes, bytes]:
148
+ user_key = hashlib.pbkdf2_hmac("sha1", password.encode(), self._user_salt, self._rounds, 32)
149
+
150
+ blob = AES.new(user_key, AES.MODE_CBC, iv=self._user_iv).decrypt(self._master_key)
151
+ blob = blob[: -blob[-1]]
152
+
153
+ offset = 0
154
+ iv_len = blob[offset]
155
+ offset += 1
156
+ iv = blob[offset : offset + iv_len]
157
+
158
+ offset += iv_len
159
+ mk_len = blob[offset]
160
+ offset += 1
161
+ mk = blob[offset : offset + mk_len]
162
+
163
+ offset += mk_len
164
+ checksum_len = blob[offset]
165
+ offset += 1
166
+ checksum = blob[offset : offset + checksum_len]
167
+
168
+ ck_mk = _encode_bytes(mk) if self.version >= 2 else mk
169
+ our_checksum = hashlib.pbkdf2_hmac("sha1", ck_mk, self._ck_salt, self._rounds, 32)
170
+ if our_checksum != checksum:
171
+ # Try reverse encoding for good measure
172
+ ck_mk = mk if self.version >= 2 else _encode_bytes(mk)
173
+ our_checksum = hashlib.pbkdf2_hmac("sha1", ck_mk, self._ck_salt, self._rounds, 32)
174
+
175
+ if our_checksum != checksum:
176
+ raise ValueError("Invalid password: master key checksum does not match")
177
+
178
+ return mk, iv
179
+
180
+ def open(self) -> BinaryIO:
181
+ fh = RelativeStream(self.fh, self._data_offset)
182
+
183
+ if self.encrypted:
184
+ if not self.unlocked:
185
+ raise ValueError("Missing password for encrypted Android Backup")
186
+ fh = CipherStream(fh, self._mk, self._iv, self.size)
187
+
188
+ if self.compressed:
189
+ fh = ZlibStream(fh)
190
+
191
+ return fh
192
+
193
+
194
+ class CipherStream(AlignedStream):
195
+ """Transparently AES-CBC decrypted stream."""
196
+
197
+ def __init__(self, fh: BinaryIO, key: bytes, iv: bytes, size: int):
198
+ self._fh = fh
199
+
200
+ self._key = key
201
+ self._iv = iv
202
+ self._cipher = None
203
+ self._cipher_offset = 0
204
+ self._reset_cipher()
205
+
206
+ super().__init__(size)
207
+
208
+ def _reset_cipher(self) -> None:
209
+ self._cipher = AES.new(self._key, AES.MODE_CBC, iv=self._iv)
210
+ self._cipher_offset = 0
211
+
212
+ def _seek_cipher(self, offset: int) -> None:
213
+ """CBC is dependent on previous blocks so to seek the cipher, decrypt and discard to the wanted offset."""
214
+ if offset < self._cipher_offset:
215
+ self._reset_cipher()
216
+ self._fh.seek(0)
217
+
218
+ while self._cipher_offset < offset:
219
+ read_size = min(offset - self._cipher_offset, self.align)
220
+ self._cipher.decrypt(self._fh.read(read_size))
221
+ self._cipher_offset += read_size
222
+
223
+ def _read(self, offset: int, length: int) -> bytes:
224
+ self._seek_cipher(offset)
225
+
226
+ data = self._cipher.decrypt(self._fh.read(length))
227
+ if offset + length >= self.size:
228
+ # Remove padding
229
+ data = data[: -data[-1]]
230
+
231
+ self._cipher_offset += len(data)
232
+
233
+ return data
234
+
235
+
236
+ def _encode_bytes(buf: bytes) -> bytes:
237
+ # Emulate byte[] -> char[] -> utf8 byte[] casting
238
+ return struct.pack(">32h", *struct.unpack(">32b", buf)).decode("utf-16-be").encode("utf-8")
239
+
240
+
241
+ def main() -> None:
242
+ import argparse
243
+
244
+ parser = argparse.ArgumentParser(description="Android Backup file unwrapper")
245
+ parser.add_argument("path", type=Path, help="source path")
246
+ parser.add_argument("-p", "--password", help="encryption password")
247
+ parser.add_argument("-t", "--tar", action="store_true", help="write a tar file instead of a plain Android Backup")
248
+ parser.add_argument("-o", "--output", type=Path, help="output path")
249
+ args = parser.parse_args()
250
+
251
+ if not args.path.is_file():
252
+ parser.exit("source path does not exist or is not a file")
253
+
254
+ ext = ".tar" if args.tar else ".plain.ab"
255
+ if args.output is None:
256
+ output = args.path.with_suffix(ext)
257
+ elif args.output.is_dir():
258
+ output = args.output.joinpath(args.path.name).with_suffix(ext)
259
+ else:
260
+ output = args.output
261
+
262
+ if output.exists():
263
+ parser.exit(f"output path already exists: {output}")
264
+
265
+ print(f"unwrapping {args.path} -> {output}")
266
+ with args.path.open("rb") as fh:
267
+ ab = AndroidBackup(fh)
268
+
269
+ if ab.encrypted:
270
+ if not args.password:
271
+ parser.exit("missing password for encrypted Android Backup")
272
+ ab.unlock(args.password)
273
+
274
+ with ab.open() as fhab, output.open("wb") as fhout:
275
+ if not args.tar:
276
+ fhout.write(b"ANDROID BACKUP\n") # header
277
+ fhout.write(b"5\n") # version
278
+ fhout.write(b"0\n") # compressed
279
+ fhout.write(b"none\n") # encryption
280
+
281
+ shutil.copyfileobj(fhab, fhout, 1024 * 1024 * 64)
282
+
283
+
284
+ if __name__ == "__main__":
285
+ main()
@@ -1,33 +1,23 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Iterator, Optional, TextIO
3
+ from typing import Iterator, Optional
4
4
 
5
5
  from dissect.target.filesystem import Filesystem
6
- from dissect.target.helpers.record import UnixUserRecord
6
+ from dissect.target.helpers import configutil
7
+ from dissect.target.helpers.record import EmptyRecord
7
8
  from dissect.target.plugin import OperatingSystem, export
8
9
  from dissect.target.plugins.os.unix.linux._os import LinuxPlugin
9
10
  from dissect.target.target import Target
10
11
 
11
12
 
12
- class BuildProp:
13
- def __init__(self, fh: TextIO):
14
- self.props = {}
15
-
16
- for line in fh:
17
- line = line.strip()
18
-
19
- if not line or line.startswith("#"):
20
- continue
21
-
22
- k, v = line.split("=")
23
- self.props[k] = v
24
-
25
-
26
13
  class AndroidPlugin(LinuxPlugin):
27
14
  def __init__(self, target: Target):
28
15
  super().__init__(target)
29
16
  self.target = target
30
- self.props = BuildProp(self.target.fs.path("/build.prop").open("rt"))
17
+
18
+ self.props = {}
19
+ if (build_prop := self.target.fs.path("/build.prop")).exists():
20
+ self.props = configutil.parse(build_prop, separator=("=",), comment_prefixes=("#",)).parsed_data
31
21
 
32
22
  @classmethod
33
23
  def detect(cls, target: Target) -> Optional[Filesystem]:
@@ -42,8 +32,8 @@ class AndroidPlugin(LinuxPlugin):
42
32
  return cls(target)
43
33
 
44
34
  @export(property=True)
45
- def hostname(self) -> str:
46
- return self.props.props["ro.build.host"]
35
+ def hostname(self) -> Optional[str]:
36
+ return self.props.get("ro.build.host")
47
37
 
48
38
  @export(property=True)
49
39
  def ips(self) -> list[str]:
@@ -53,11 +43,11 @@ class AndroidPlugin(LinuxPlugin):
53
43
  def version(self) -> str:
54
44
  full_version = "Android"
55
45
 
56
- release_version = self.props.props.get("ro.build.version.release")
57
- if release_version := self.props.props.get("ro.build.version.release"):
46
+ release_version = self.props.get("ro.build.version.release")
47
+ if release_version := self.props.get("ro.build.version.release"):
58
48
  full_version += f" {release_version}"
59
49
 
60
- if security_patch_version := self.props.props.get("ro.build.version.security_patch"):
50
+ if security_patch_version := self.props.get("ro.build.version.security_patch"):
61
51
  full_version += f" ({security_patch_version})"
62
52
 
63
53
  return full_version
@@ -66,5 +56,6 @@ class AndroidPlugin(LinuxPlugin):
66
56
  def os(self) -> str:
67
57
  return OperatingSystem.ANDROID.value
68
58
 
69
- def users(self) -> Iterator[UnixUserRecord]:
70
- raise NotImplementedError()
59
+ @export(record=EmptyRecord)
60
+ def users(self) -> Iterator[EmptyRecord]:
61
+ yield from ()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.target
3
- Version: 3.19.dev18
3
+ Version: 3.19.dev19
4
4
  Summary: This module ties all other Dissect modules together, it provides a programming API and command line tools which allow easy access to various data sources inside disk images or file collections (a.k.a. targets)
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License: Affero General Public License v3
@@ -2,7 +2,7 @@ dissect/target/__init__.py,sha256=Oc7ounTgq2hE4nR6YcNabetc7SQA40ldSa35VEdZcQU,63
2
2
  dissect/target/container.py,sha256=0YcwcGmfJjhPXUB6DEcjWEoSuAtTDxMDpoTviMrLsxM,9353
3
3
  dissect/target/exceptions.py,sha256=ULi7NXlqju_d8KENEL3aimmfKTFfbNssfeWhAnOB654,2972
4
4
  dissect/target/filesystem.py,sha256=G1gbOUpnQZyovubYGEUKgaDV0eHH5vE83-0gTc5PZAM,59793
5
- dissect/target/loader.py,sha256=hjKInZAEcv43RiqxZJ0yBI4Y2YZ2-nrsKWu_BKrgba4,7336
5
+ dissect/target/loader.py,sha256=I8WNzDA0SMy42F7zfyBcSKj_VKNv64213WUvtGZ77qE,7374
6
6
  dissect/target/plugin.py,sha256=HAN8maaDt-Rlqt8Rr1IW7gXQpzNQZjCVz-i4aSPphSw,48677
7
7
  dissect/target/report.py,sha256=06uiP4MbNI8cWMVrC1SasNS-Yg6ptjVjckwj8Yhe0Js,7958
8
8
  dissect/target/target.py,sha256=8vg0VdEQuy5Ih5ewlm0b64o3HcJq_Nley4Ygyp2fLI4,32362
@@ -75,6 +75,7 @@ dissect/target/helpers/compat/path_39.py,sha256=FIyZ3sb-XQhJnm02jVdOc6ncjCWa9OVx
75
75
  dissect/target/helpers/compat/path_common.py,sha256=X9mAPoP6E5e_1idiZz7-FPRsOwcAjQ5FP70k30s_yMA,7739
76
76
  dissect/target/helpers/data/windowsZones.xml,sha256=4OijeR7oxI0ZwPTSwCkmtcofOsUCjSnbZ4dQxVOM_4o,50005
77
77
  dissect/target/loaders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
+ dissect/target/loaders/ab.py,sha256=iwj1LHe_-VaBmj6d-nKrvim1UvrJ-nzp2LlgCFlOuUk,9484
78
79
  dissect/target/loaders/ad1.py,sha256=1_VmPZckDzXVvNF-HNtoUZqabnhCKBLUD3vVaitHQ00,571
79
80
  dissect/target/loaders/asdf.py,sha256=dvPPDBrnz2JPXpCbqsu-NgQWIdVGMOit2KAdhIO1iiQ,972
80
81
  dissect/target/loaders/cb.py,sha256=EGhdytBKBdofTd89juavDZZbmupEZmMBadeUXvVIK20,6612
@@ -222,7 +223,7 @@ dissect/target/plugins/os/unix/linux/processes.py,sha256=rvDJWAp16WAJZ91A8_GJJIj
222
223
  dissect/target/plugins/os/unix/linux/services.py,sha256=-d2y073mOXUM3XCzRgDVCRFR9eTLoVuN8FsZVewHzRg,4075
223
224
  dissect/target/plugins/os/unix/linux/sockets.py,sha256=CXstlQt0tLcVSpvi0xOXJu580O6BGUBW3lJQt20aMUw,9920
224
225
  dissect/target/plugins/os/unix/linux/android/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
225
- dissect/target/plugins/os/unix/linux/android/_os.py,sha256=trmESlpHdwVu7wV18RevEhh_TsVyfKPFCd5Usb5-fSU,2056
226
+ dissect/target/plugins/os/unix/linux/android/_os.py,sha256=-VWLkL3mFAr-ZxwH1qdSPNLvOgbYN92d4HyM2LSty5w,1947
226
227
  dissect/target/plugins/os/unix/linux/debian/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
227
228
  dissect/target/plugins/os/unix/linux/debian/_os.py,sha256=GI19ZqcyfZ1mUYg2NCx93HkZje9MWMU8FYNKQv-G6go,498
228
229
  dissect/target/plugins/os/unix/linux/debian/apt.py,sha256=dkTfLrS-MS8wfrXILFLHDoLqBkM_w16KTRQ7ysiZkZY,4316
@@ -344,10 +345,10 @@ dissect/target/volumes/luks.py,sha256=OmCMsw6rCUXG1_plnLVLTpsvE1n_6WtoRUGQbpmu1z
344
345
  dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
345
346
  dissect/target/volumes/md.py,sha256=j1K1iKmspl0C_OJFc7-Q1BMWN2OCC5EVANIgVlJ_fIE,1673
346
347
  dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
347
- dissect.target-3.19.dev18.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
348
- dissect.target-3.19.dev18.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
349
- dissect.target-3.19.dev18.dist-info/METADATA,sha256=5gfwL4PP7a_vTLYBnbyfy1keo11MDR4aYuyMlPMv5Vs,12719
350
- dissect.target-3.19.dev18.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
351
- dissect.target-3.19.dev18.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
352
- dissect.target-3.19.dev18.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
353
- dissect.target-3.19.dev18.dist-info/RECORD,,
348
+ dissect.target-3.19.dev19.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
349
+ dissect.target-3.19.dev19.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
350
+ dissect.target-3.19.dev19.dist-info/METADATA,sha256=1_oiRc_YPQzlraRQsEAAfx7MTXUAjB505xvN06L7us0,12719
351
+ dissect.target-3.19.dev19.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
352
+ dissect.target-3.19.dev19.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
353
+ dissect.target-3.19.dev19.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
354
+ dissect.target-3.19.dev19.dist-info/RECORD,,