From 49c40c88e21a49b14c069a0b33fd58c855e839b5 Mon Sep 17 00:00:00 2001 From: Schamper <1254028+Schamper@users.noreply.github.com> Date: Mon, 16 Oct 2023 20:32:51 +0200 Subject: [PATCH 1/6] Add support for Android Backup --- dissect/target/loader.py | 1 + dissect/target/loaders/ab.py | 356 ++++++++++++++++++ .../plugins/os/unix/linux/android/_os.py | 43 ++- 3 files changed, 380 insertions(+), 20 deletions(-) create mode 100644 dissect/target/loaders/ab.py diff --git a/dissect/target/loader.py b/dissect/target/loader.py index a7beef45c..591b8d56c 100644 --- a/dissect/target/loader.py +++ b/dissect/target/loader.py @@ -195,6 +195,7 @@ def open(item: Union[str, Path], *args, **kwargs) -> Loader: register("kape", "KapeLoader") register("tanium", "TaniumLoader") register("itunes", "ITunesLoader") +register("ab", "AndroidBackupLoader") register("target", "TargetLoader") register("log", "LogLoader") # Disabling ResLoader because of DIS-536 diff --git a/dissect/target/loaders/ab.py b/dissect/target/loaders/ab.py new file mode 100644 index 000000000..ff4851615 --- /dev/null +++ b/dissect/target/loaders/ab.py @@ -0,0 +1,356 @@ +import hashlib +import io +import posixpath +import shutil +import struct +import sys +import zlib +from pathlib import Path +from typing import BinaryIO, Optional + +try: + from Crypto.Cipher import AES + + HAS_PYCRYPTODOME = True +except ImportError: + HAS_PYCRYPTODOME = False + +from dissect.util.stream import AlignedStream, RelativeStream + +from dissect.target.exceptions import LoaderError +from dissect.target.filesystem import VirtualFilesystem +from dissect.target.filesystems.tar import TarFilesystem +from dissect.target.helpers import keychain +from dissect.target.loader import Loader +from dissect.target.plugins.os.unix.linux.android._os import AndroidPlugin +from dissect.target.target import Target + +DIRECTORY_MAPPING = { + "a": "/data/app/{id}", + "f": "/data/data/{id}/files", + "db": "/data/data/{id}/databases", + "ef": "/storage/emulated/0/Android/data/{id}", + "sp": "/data/data/{id}/shared_preferences", + "r": "/data/data/{id}", + "obb": "/storage/emulated/0/Android/obb/{id}", +} + + +class AndroidBackupLoader(Loader): + """Load Android backup files. + + References: + - http://fileformats.archiveteam.org/wiki/Android_ADB_Backup + """ + + def __init__(self, path: Path, **kwargs): + super().__init__(path) + self.ab = AndroidBackup(path.open("rb")) + + if self.ab.encrypted: + for key in keychain.get_keys_for_provider("ab") + keychain.get_keys_without_provider(): + if key.key_type == keychain.KeyType.PASSPHRASE: + try: + self.ab.unlock(key.value) + break + except ValueError: + continue + else: + raise LoaderError(f"Missing password for encrypted Android Backup: {self.path}") + + @staticmethod + def detect(path: Path) -> bool: + return path.suffix.lower() == ".ab" + + def map(self, target: Target) -> None: + if self.ab.compressed or self.ab.encrypted: + if self.ab.compressed and not self.ab.encrypted: + word = "compressed" + elif self.ab.encrypted and not self.ab.compressed: + word = "encrypted" + else: + word = "compressed and encrypted" + + target.log.warning( + f"Backup file is {word}, consider unwrapping with " + "`python -m dissect.target.loaders.ab `" + ) + + vfs = VirtualFilesystem(case_sensitive=False) + + fs = TarFilesystem(self.ab.open()) + for app in fs.path("/apps").iterdir(): + for subdir in app.iterdir(): + if subdir.name not in DIRECTORY_MAPPING: + continue + + path = DIRECTORY_MAPPING[subdir.name].format(id=app.name) + + # TODO: Remove once we move towards "directory entries" + entry = subdir.get() + entry.name = posixpath.basename(path) + + vfs.map_file_entry(path, entry) + + target.filesystems.add(vfs) + + target.fs.mount("/", vfs) + target._os_plugin = AndroidPlugin(target) + + +class AndroidBackup: + def __init__(self, fh: BinaryIO): + self.fh = fh + + size = fh.seek(0, io.SEEK_END) + fh.seek(0) + + # Don't readline() straight away as we may be reading something other than a backup file + magic = fh.read(15) + if magic != b"ANDROID BACKUP\n": + raise ValueError("Not a valid Android Backup file") + + self.version = int(fh.read(2)[:1]) + self.compressed = bool(int(fh.read(2)[:1])) + + self.encrypted = False + self.unlocked = True + self.encryption = fh.readline().strip().decode() + + if self.encryption != "none": + self.encrypted = True + self.unlocked = False + self._user_salt = bytes.fromhex(fh.readline().strip().decode()) + self._ck_salt = bytes.fromhex(fh.readline().strip().decode()) + self._rounds = int(fh.readline().strip()) + self._user_iv = bytes.fromhex(fh.readline().strip().decode()) + self._master_key = bytes.fromhex(fh.readline().strip().decode()) + + self._mk = None + self._iv = None + + self._data_offset = fh.tell() + self.size = size - self._data_offset + + def unlock(self, password: str) -> None: + if not self.encrypted: + raise ValueError("Android Backup is not encrypted") + + self._mk, self._iv = self._decrypt_mk(password) + self.unlocked = True + + def _decrypt_mk(self, password: str) -> tuple[bytes, bytes]: + user_key = hashlib.pbkdf2_hmac("sha1", password.encode(), self._user_salt, self._rounds, 32) + + blob = AES.new(user_key, AES.MODE_CBC, iv=self._user_iv).decrypt(self._master_key) + blob = blob[: -blob[-1]] + + offset = 0 + iv_len = blob[offset] + offset += 1 + iv = blob[offset : offset + iv_len] + + offset += iv_len + mk_len = blob[offset] + offset += 1 + mk = blob[offset : offset + mk_len] + + offset += mk_len + checksum_len = blob[offset] + offset += 1 + checksum = blob[offset : offset + checksum_len] + + ck_mk = _encode_bytes(mk) if self.version >= 2 else mk + our_checksum = hashlib.pbkdf2_hmac("sha1", ck_mk, self._ck_salt, self._rounds, 32) + if our_checksum != checksum: + # Try reverse encoding for good measure + ck_mk = mk if self.version >= 2 else _encode_bytes(mk) + our_checksum = hashlib.pbkdf2_hmac("sha1", ck_mk, self._ck_salt, self._rounds, 32) + + if our_checksum != checksum: + raise ValueError("Invalid password: master key checksum does not match") + + return mk, iv + + def open(self) -> BinaryIO: + fh = RelativeStream(self.fh, self._data_offset) + + if self.encrypted: + if not self.unlocked: + raise ValueError("Missing password for encrypted Android Backup") + fh = CipherStream(fh, self._mk, self._iv, self.size) + + if self.compressed: + fh = ZlibStream(fh) + + return fh + + +class ZlibStream(AlignedStream): + def __init__(self, fh: BinaryIO, size: Optional[int] = None, **kwargs): + self._fh = fh + + self._zlib = None + self._zlib_args = kwargs + self._zlib_offset = 0 + self._zlib_prepend = b"" + self._zlib_prepend_offset = None + self._rewind() + + super().__init__(size) + + def _rewind(self) -> None: + self._fh.seek(0) + self._zlib = zlib.decompressobj(**self._zlib_args) + self._zlib_offset = 0 + self._zlib_prepend = b"" + self._zlib_prepend_offset = None + + def _seek_zlib(self, offset: int) -> None: + if offset < self._zlib_offset: + self._rewind() + + while self._zlib_offset < offset: + read_size = min(offset - self._zlib_offset, self.align) + if self._read_zlib(read_size) == b"": + break + + def _read_fh(self, length: int) -> bytes: + if self._zlib_prepend_offset is None: + return self._fh.read(length) + + if self._zlib_prepend_offset + length <= len(self._zlib_prepend): + offset = self._zlib_prepend_offset + self._zlib_prepend_offset += length + return self._zlib_prepend_offset[offset : self._zlib_prepend_offset] + else: + offset = self._zlib_prepend_offset + self._zlib_prepend_offset = None + return self._zlib_prepend[offset:] + self._fh.read(length - len(self._zlib_prepend) + offset) + + def _read_zlib(self, length: int) -> bytes: + if length < 0: + return self.readall() + + result = [] + while length > 0: + buf = self._read_fh(io.DEFAULT_BUFFER_SIZE) + decompressed = self._zlib.decompress(buf, length) + + if self._zlib.unconsumed_tail != b"": + self._zlib_prepend = self._zlib.unconsumed_tail + self._zlib_prepend_offset = 0 + + if buf == b"": + break + + result.append(decompressed) + length -= len(decompressed) + + buf = b"".join(result) + self._zlib_offset += len(buf) + return buf + + def _read(self, offset: int, length: int) -> bytes: + self._seek_zlib(offset) + return self._read_zlib(length) + + def readall(self) -> bytes: + chunks = [] + # sys.maxsize means the max length of output buffer is unlimited, + # so that the whole input buffer can be decompressed within one + # .decompress() call. + while data := self._read_zlib(sys.maxsize): + chunks.append(data) + + return b"".join(chunks) + + +class CipherStream(AlignedStream): + """Transparently AES-CBC decrypted stream.""" + + def __init__(self, fh: BinaryIO, key: bytes, iv: bytes, size: int): + self._fh = fh + + self._key = key + self._iv = iv + self._cipher = None + self._cipher_offset = 0 + self._reset_cipher() + + super().__init__(size) + + def _reset_cipher(self) -> None: + self._cipher = AES.new(self._key, AES.MODE_CBC, iv=self._iv) + self._cipher_offset = 0 + + def _seek_cipher(self, offset: int) -> None: + """CBC is dependent on previous blocks so to seek the cipher, decrypt and discard to the wanted offset.""" + if offset < self._cipher_offset: + self._reset_cipher() + self._fh.seek(0) + + while self._cipher_offset < offset: + read_size = min(offset - self._cipher_offset, self.align) + self._cipher.decrypt(self._fh.read(read_size)) + self._cipher_offset += read_size + + def _read(self, offset: int, length: int) -> bytes: + self._seek_cipher(offset) + + data = self._cipher.decrypt(self._fh.read(length)) + if offset + length >= self.size: + # Remove padding + data = data[: -data[-1]] + + self._cipher_offset += len(data) + + return data + + +def _encode_bytes(buf: bytes) -> bytes: + # Emulate byte[] -> char[] -> utf8 byte[] casting + return struct.pack(">32h", *struct.unpack(">32b", buf)).decode("utf-16-be").encode("utf-8") + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("path", type=Path, help="source path") + parser.add_argument("-p", "--password", help="encryption password") + parser.add_argument("-t", "--tar", action="store_true", help="write a tar file instead of a plain Android Backup") + parser.add_argument("-o", "--output", type=Path, help="output path") + args = parser.parse_args() + + if not args.path.is_file(): + parser.exit("source path does not exist or is not a file") + + ext = ".tar" if args.tar else ".plain.ab" + if args.output is None: + output = args.path.with_suffix(ext) + elif args.output.is_dir(): + output = args.output.joinpath(args.path.name).with_suffix(ext) + else: + output = args.output + + if output.exists(): + parser.exit(f"output path already exists: {output}") + + print(f"unwrapping {args.path} -> {output}") + with args.path.open("rb") as fh: + ab = AndroidBackup(fh) + + if ab.encrypted: + if not args.password: + parser.exit("missing password for encrypted Android Backup") + ab.unlock(args.password) + + with ab.open() as fhab, output.open("wb") as fhout: + if not args.tar: + fhout.write(b"ANDROID BACKUP\n") # header + fhout.write(b"5\n") # version + fhout.write(b"0\n") # compressed + fhout.write(b"none\n") # encryption + + shutil.copyfileobj(fhab, fhout, 1024 * 1024 * 64) diff --git a/dissect/target/plugins/os/unix/linux/android/_os.py b/dissect/target/plugins/os/unix/linux/android/_os.py index 6d376b7d8..e26a19063 100644 --- a/dissect/target/plugins/os/unix/linux/android/_os.py +++ b/dissect/target/plugins/os/unix/linux/android/_os.py @@ -3,31 +3,20 @@ from typing import Iterator, Optional, TextIO from dissect.target.filesystem import Filesystem -from dissect.target.helpers.record import UnixUserRecord +from dissect.target.helpers.record import EmptyRecord from dissect.target.plugin import OperatingSystem, export from dissect.target.plugins.os.unix.linux._os import LinuxPlugin from dissect.target.target import Target -class BuildProp: - def __init__(self, fh: TextIO): - self.props = {} - - for line in fh: - line = line.strip() - - if not line or line.startswith("#"): - continue - - k, v = line.split("=") - self.props[k] = v - - class AndroidPlugin(LinuxPlugin): def __init__(self, target: Target): super().__init__(target) self.target = target - self.props = BuildProp(self.target.fs.path("/build.prop").open("rt")) + + self.props = {} + if (build_prop := self.target.fs.path("/build.prop")).exists(): + self.props = _read_props(build_prop.open("rt")) @classmethod def detect(cls, target: Target) -> Optional[Filesystem]: @@ -42,8 +31,8 @@ def create(cls, target: Target, sysvol: Filesystem) -> AndroidPlugin: return cls(target) @export(property=True) - def hostname(self) -> str: - return self.props.props["ro.build.host"] + def hostname(self) -> Optional[str]: + return self.props.get("ro.build.host") @export(property=True) def ips(self) -> list[str]: @@ -66,5 +55,19 @@ def version(self) -> str: def os(self) -> str: return OperatingSystem.ANDROID.value - def users(self) -> Iterator[UnixUserRecord]: - raise NotImplementedError() + @export(record=EmptyRecord) + def users(self) -> Iterator[EmptyRecord]: + yield from () + + +def _read_props(fh: TextIO) -> dict[str, str]: + result = {} + + for line in fh: + line = line.strip() + + if not line or line.startswith("#"): + continue + + k, v = line.split("=") + result[k] = v From 222d6cb2a8ea37eb6e953a9af0d34fe884ecdabc Mon Sep 17 00:00:00 2001 From: Schamper <1254028+Schamper@users.noreply.github.com> Date: Sat, 21 Oct 2023 22:43:30 +0200 Subject: [PATCH 2/6] Add unit tests --- dissect/target/loaders/ab.py | 19 +++++++++++--- tests/_data/loaders/ab/test.ab | 3 +++ tests/loaders/test_ab.py | 48 ++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 tests/_data/loaders/ab/test.ab create mode 100644 tests/loaders/test_ab.py diff --git a/dissect/target/loaders/ab.py b/dissect/target/loaders/ab.py index ff4851615..160f7d976 100644 --- a/dissect/target/loaders/ab.py +++ b/dissect/target/loaders/ab.py @@ -60,7 +60,16 @@ def __init__(self, path: Path, **kwargs): @staticmethod def detect(path: Path) -> bool: - return path.suffix.lower() == ".ab" + if path.suffix.lower() == ".ab": + return True + + if path.is_file(): + # The file extension can be chosen freely so let's also test for the file magic + with path.open("rb") as fh: + if fh.read(15) == b"ANDROID BACKUP\n": + return True + + return False def map(self, target: Target) -> None: if self.ab.compressed or self.ab.encrypted: @@ -313,10 +322,10 @@ def _encode_bytes(buf: bytes) -> bytes: return struct.pack(">32h", *struct.unpack(">32b", buf)).decode("utf-16-be").encode("utf-8") -if __name__ == "__main__": +def main() -> None: import argparse - parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser(description="Android Backup file unwrapper") parser.add_argument("path", type=Path, help="source path") parser.add_argument("-p", "--password", help="encryption password") parser.add_argument("-t", "--tar", action="store_true", help="write a tar file instead of a plain Android Backup") @@ -354,3 +363,7 @@ def _encode_bytes(buf: bytes) -> bytes: fhout.write(b"none\n") # encryption shutil.copyfileobj(fhab, fhout, 1024 * 1024 * 64) + + +if __name__ == "__main__": + main() diff --git a/tests/_data/loaders/ab/test.ab b/tests/_data/loaders/ab/test.ab new file mode 100644 index 000000000..2009785aa --- /dev/null +++ b/tests/_data/loaders/ab/test.ab @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c49cf6ae4f8bd91b02fb3689d552416e3ed64605a25011cb204004e5b1f9aac +size 2181 diff --git a/tests/loaders/test_ab.py b/tests/loaders/test_ab.py new file mode 100644 index 000000000..a53a8b84d --- /dev/null +++ b/tests/loaders/test_ab.py @@ -0,0 +1,48 @@ +import hashlib +from pathlib import Path + +import pytest + +from dissect.target import Target +from dissect.target.helpers import keychain +from dissect.target.loaders.ab import AndroidBackupLoader +from dissect.target.loaders.ab import main as ab_main +from tests._utils import absolute_path + + +def test_ab_loader(target_bare: Target) -> None: + ab_path = Path(absolute_path("_data/loaders/ab/test.ab")) + keychain.register_wildcard_value("password") + + assert AndroidBackupLoader.detect(ab_path) + + loader = AndroidBackupLoader(ab_path) + loader.map(target_bare) + + assert len(target_bare.filesystems) == 1 + + assert list(map(str, target_bare.fs.path("/").rglob("*"))) == [ + "/data", + "/data/data", + "/data/data/org.fedorahosted.freeotp", + "/data/data/org.fedorahosted.freeotp/shared_preferences", + "/data/data/org.fedorahosted.freeotp/shared_preferences/tokenBackup.xml", + ] + + buf = target_bare.fs.path("/data/data/org.fedorahosted.freeotp/shared_preferences/tokenBackup.xml").read_bytes() + assert hashlib.sha1(buf).hexdigest() == "7177d340414d5ca2a835603c347e64e3d2625e47" + + +def test_ab_unwrapper(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture) -> None: + ab_path = Path(absolute_path("_data/loaders/ab/test.ab")) + + with monkeypatch.context() as m: + m.setattr("sys.argv", ["python", str(ab_path), "-p", "password", "-o", str(tmp_path)]) + ab_main() + + assert [entry.name for entry in tmp_path.iterdir()] == ["test.plain.ab"] + + m.setattr("sys.argv", ["python", str(ab_path), "-p", "password", "-o", str(tmp_path), "-t"]) + ab_main() + + assert sorted([entry.name for entry in tmp_path.iterdir()]) == ["test.plain.ab", "test.tar"] From a28e7ac98487b6de629b891119b41bc80bd331b6 Mon Sep 17 00:00:00 2001 From: Schamper <1254028+Schamper@users.noreply.github.com> Date: Tue, 7 Nov 2023 18:11:08 +0100 Subject: [PATCH 3/6] Fix AndroidPlugin --- dissect/target/plugins/os/unix/linux/android/_os.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dissect/target/plugins/os/unix/linux/android/_os.py b/dissect/target/plugins/os/unix/linux/android/_os.py index e26a19063..9db951472 100644 --- a/dissect/target/plugins/os/unix/linux/android/_os.py +++ b/dissect/target/plugins/os/unix/linux/android/_os.py @@ -42,11 +42,11 @@ def ips(self) -> list[str]: def version(self) -> str: full_version = "Android" - release_version = self.props.props.get("ro.build.version.release") - if release_version := self.props.props.get("ro.build.version.release"): + release_version = self.props.get("ro.build.version.release") + if release_version := self.props.get("ro.build.version.release"): full_version += f" {release_version}" - if security_patch_version := self.props.props.get("ro.build.version.security_patch"): + if security_patch_version := self.props.get("ro.build.version.security_patch"): full_version += f" ({security_patch_version})" return full_version @@ -71,3 +71,5 @@ def _read_props(fh: TextIO) -> dict[str, str]: k, v = line.split("=") result[k] = v + + return result From 9ebc67bfdf16d426a93ce7e7902f869aea0fa4a5 Mon Sep 17 00:00:00 2001 From: Schamper <1254028+Schamper@users.noreply.github.com> Date: Mon, 25 Dec 2023 00:15:35 +0100 Subject: [PATCH 4/6] Move ZlibStream to dissect.util.stream --- dissect/target/loaders/ab.py | 86 +----------------------------------- 1 file changed, 2 insertions(+), 84 deletions(-) diff --git a/dissect/target/loaders/ab.py b/dissect/target/loaders/ab.py index 160f7d976..80275b2e0 100644 --- a/dissect/target/loaders/ab.py +++ b/dissect/target/loaders/ab.py @@ -3,10 +3,8 @@ import posixpath import shutil import struct -import sys -import zlib from pathlib import Path -from typing import BinaryIO, Optional +from typing import BinaryIO try: from Crypto.Cipher import AES @@ -15,7 +13,7 @@ except ImportError: HAS_PYCRYPTODOME = False -from dissect.util.stream import AlignedStream, RelativeStream +from dissect.util.stream import AlignedStream, RelativeStream, ZlibStream from dissect.target.exceptions import LoaderError from dissect.target.filesystem import VirtualFilesystem @@ -195,86 +193,6 @@ def open(self) -> BinaryIO: return fh -class ZlibStream(AlignedStream): - def __init__(self, fh: BinaryIO, size: Optional[int] = None, **kwargs): - self._fh = fh - - self._zlib = None - self._zlib_args = kwargs - self._zlib_offset = 0 - self._zlib_prepend = b"" - self._zlib_prepend_offset = None - self._rewind() - - super().__init__(size) - - def _rewind(self) -> None: - self._fh.seek(0) - self._zlib = zlib.decompressobj(**self._zlib_args) - self._zlib_offset = 0 - self._zlib_prepend = b"" - self._zlib_prepend_offset = None - - def _seek_zlib(self, offset: int) -> None: - if offset < self._zlib_offset: - self._rewind() - - while self._zlib_offset < offset: - read_size = min(offset - self._zlib_offset, self.align) - if self._read_zlib(read_size) == b"": - break - - def _read_fh(self, length: int) -> bytes: - if self._zlib_prepend_offset is None: - return self._fh.read(length) - - if self._zlib_prepend_offset + length <= len(self._zlib_prepend): - offset = self._zlib_prepend_offset - self._zlib_prepend_offset += length - return self._zlib_prepend_offset[offset : self._zlib_prepend_offset] - else: - offset = self._zlib_prepend_offset - self._zlib_prepend_offset = None - return self._zlib_prepend[offset:] + self._fh.read(length - len(self._zlib_prepend) + offset) - - def _read_zlib(self, length: int) -> bytes: - if length < 0: - return self.readall() - - result = [] - while length > 0: - buf = self._read_fh(io.DEFAULT_BUFFER_SIZE) - decompressed = self._zlib.decompress(buf, length) - - if self._zlib.unconsumed_tail != b"": - self._zlib_prepend = self._zlib.unconsumed_tail - self._zlib_prepend_offset = 0 - - if buf == b"": - break - - result.append(decompressed) - length -= len(decompressed) - - buf = b"".join(result) - self._zlib_offset += len(buf) - return buf - - def _read(self, offset: int, length: int) -> bytes: - self._seek_zlib(offset) - return self._read_zlib(length) - - def readall(self) -> bytes: - chunks = [] - # sys.maxsize means the max length of output buffer is unlimited, - # so that the whole input buffer can be decompressed within one - # .decompress() call. - while data := self._read_zlib(sys.maxsize): - chunks.append(data) - - return b"".join(chunks) - - class CipherStream(AlignedStream): """Transparently AES-CBC decrypted stream.""" From 045c563953ab2561ec4c7f4b96b46ee5295634c5 Mon Sep 17 00:00:00 2001 From: Schamper <1254028+Schamper@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:56:00 +0200 Subject: [PATCH 5/6] Use configutil --- .../plugins/os/unix/linux/android/_os.py | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/dissect/target/plugins/os/unix/linux/android/_os.py b/dissect/target/plugins/os/unix/linux/android/_os.py index 9db951472..62c7dc61e 100644 --- a/dissect/target/plugins/os/unix/linux/android/_os.py +++ b/dissect/target/plugins/os/unix/linux/android/_os.py @@ -1,8 +1,9 @@ from __future__ import annotations -from typing import Iterator, Optional, TextIO +from typing import Iterator, Optional from dissect.target.filesystem import Filesystem +from dissect.target.helpers import configutil from dissect.target.helpers.record import EmptyRecord from dissect.target.plugin import OperatingSystem, export from dissect.target.plugins.os.unix.linux._os import LinuxPlugin @@ -16,7 +17,7 @@ def __init__(self, target: Target): self.props = {} if (build_prop := self.target.fs.path("/build.prop")).exists(): - self.props = _read_props(build_prop.open("rt")) + self.props = configutil.parse(build_prop, separator=("=",), comment_prefixes=("#",)).parsed_data @classmethod def detect(cls, target: Target) -> Optional[Filesystem]: @@ -58,18 +59,3 @@ def os(self) -> str: @export(record=EmptyRecord) def users(self) -> Iterator[EmptyRecord]: yield from () - - -def _read_props(fh: TextIO) -> dict[str, str]: - result = {} - - for line in fh: - line = line.strip() - - if not line or line.startswith("#"): - continue - - k, v = line.split("=") - result[k] = v - - return result From 1bc16742de2d049ae91a54386157dc2a248ad234 Mon Sep 17 00:00:00 2001 From: Schamper <1254028+Schamper@users.noreply.github.com> Date: Mon, 29 Jul 2024 13:29:26 +0200 Subject: [PATCH 6/6] Adjust OS plugin initialization --- dissect/target/loaders/ab.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dissect/target/loaders/ab.py b/dissect/target/loaders/ab.py index 80275b2e0..003ae101e 100644 --- a/dissect/target/loaders/ab.py +++ b/dissect/target/loaders/ab.py @@ -100,9 +100,7 @@ def map(self, target: Target) -> None: vfs.map_file_entry(path, entry) target.filesystems.add(vfs) - - target.fs.mount("/", vfs) - target._os_plugin = AndroidPlugin(target) + target._os_plugin = AndroidPlugin.create(target, vfs) class AndroidBackup: