Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions dissect/target/helpers/localeutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,16 @@ def normalize_timezone(input: str) -> str:
def normalize_language(input: str) -> str:
"""Returns normalized locales per ISO-3166. Takes Unix LANG locales and Windows registry languages as input.

Output will be in the format ``ISO-3166-1_ISO-3166-2``, e.g.: ``en_US``, ``nl_NL`` or ``en_GB``.
Output will be in the format ``ISO-3166-1-alpha-2-code_ISO-3166-2``, e.g.: ``en_US``, ``nl_NL`` or ``en_GB``.

Resources:
- https://en.wikipedia.org/wiki/ISO_3166
- https://en.wikipedia.org/wiki/ISO_3166-1
- https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
- https://en.wikipedia.org/wiki/ISO_3166-2
"""

return normalize(input).split(".")[0]
return normalize(input.replace("-", "_", 1)).split(".")[0]


def get_resource_string(path: str) -> str:
Expand Down
44 changes: 23 additions & 21 deletions dissect/target/helpers/record.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,18 +124,30 @@ def DynamicDescriptor(types): # noqa
],
)

COMMON_UNIX_FIELDS = [
("string", "name"),
("string", "passwd"),
("varint", "uid"),
("varint", "gid"),
("string", "gecos"),
("path", "home"),
("string", "shell"),
("string", "source"),
]

UnixUserRecord = TargetRecordDescriptor(
"unix/user",
[
("string", "name"),
("string", "passwd"),
("varint", "uid"),
("varint", "gid"),
("string", "gecos"),
("path", "home"),
("string", "shell"),
("string", "source"),
],
COMMON_UNIX_FIELDS,
)

MacOSUserRecord = TargetRecordDescriptor(
"macos/user",
COMMON_UNIX_FIELDS,
)

IOSUserRecord = TargetRecordDescriptor(
"ios/user",
COMMON_UNIX_FIELDS,
)

EmptyRecord = RecordDescriptor(
Expand Down Expand Up @@ -180,7 +192,7 @@ def DynamicDescriptor(types): # noqa
],
)

MacInterfaceRecord = TargetRecordDescriptor(
MacOSInterfaceRecord = TargetRecordDescriptor(
"macos/network/interface",
[
*COMMON_INTERFACE_ELEMENTS,
Expand All @@ -200,13 +212,3 @@ def DynamicDescriptor(types): # noqa
("string", "type"),
("path", "path"),
]

UnixApplicationRecord = TargetRecordDescriptor(
"unix/application",
COMMON_APPLICATION_FIELDS,
)

WindowsApplicationRecord = TargetRecordDescriptor(
"windows/application",
COMMON_APPLICATION_FIELDS,
)
2 changes: 1 addition & 1 deletion dissect/target/loaders/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def map(self, target):
map_solaris_drives(target)
elif os_name == "vmkernel":
map_esxi_drives(target)
elif os_name in ["darwin", "osx"]:
elif os_name in ["darwin", "osx", "macos"]:
# There is currently no way to access raw disk devices in OS-X,
# so we always do a simple DirectoryFilesystem fallback.
target.filesystems.add(DirectoryFilesystem(Path("/")))
Expand Down
3 changes: 2 additions & 1 deletion dissect/target/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ class OperatingSystem(StrEnum):
FORTIOS = "fortios"
IOS = "ios"
LINUX = "linux"
OSX = "osx"
MACOS = "macos"
OSX = "osx" # legacy
PROXMOX = "proxmox"
UNIX = "unix"
VYOS = "vyos"
Expand Down
9 changes: 7 additions & 2 deletions dissect/target/plugins/general/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@
from dissect.target import Target
from dissect.target.exceptions import UnsupportedPluginError
from dissect.target.helpers.fsutil import TargetPath
from dissect.target.helpers.record import UnixUserRecord, WindowsUserRecord
from dissect.target.helpers.record import (
IOSUserRecord,
MacOSUserRecord,
UnixUserRecord,
WindowsUserRecord,
)
from dissect.target.plugin import InternalPlugin

UserRecord = Union[UnixUserRecord, WindowsUserRecord]
UserRecord = Union[UnixUserRecord, WindowsUserRecord, MacOSUserRecord, IOSUserRecord]


class UserDetails(NamedTuple):
Expand Down
4 changes: 2 additions & 2 deletions dissect/target/plugins/os/default/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from flow.record.fieldtypes.net import IPAddress, IPNetwork

from dissect.target.helpers.record import (
MacInterfaceRecord,
MacOSInterfaceRecord,
UnixInterfaceRecord,
WindowsInterfaceRecord,
)
Expand All @@ -16,7 +16,7 @@
from collections.abc import Iterator

from dissect.target.target import Target
InterfaceRecord = Union[UnixInterfaceRecord, WindowsInterfaceRecord, MacInterfaceRecord]
InterfaceRecord = Union[UnixInterfaceRecord, WindowsInterfaceRecord, MacOSInterfaceRecord]


class NetworkPlugin(Plugin):
Expand Down
9 changes: 6 additions & 3 deletions dissect/target/plugins/os/unix/_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@


class UnixPlugin(OSPlugin):
"""UNIX plugin."""

# Files to parse for user details
PASSWD_FILES = ["/etc/passwd", "/etc/passwd-", "/etc/master.passwd"]

def __init__(self, target: Target):
super().__init__(target)
self._add_mounts()
Expand Down Expand Up @@ -76,12 +81,10 @@ def users(self, sessions: bool = False) -> Iterator[UnixUserRecord]:
- https://manpages.ubuntu.com/manpages/oracular/en/man5/passwd.5.html
"""

PASSWD_FILES = ["/etc/passwd", "/etc/passwd-", "/etc/master.passwd"]

seen_users = set()

# Yield users found in passwd files.
for passwd_file in PASSWD_FILES:
for passwd_file in self.PASSWD_FILES:
if (path := self.target.fs.path(passwd_file)).exists():
for line in path.open("rt", errors="surrogateescape"):
line = line.strip()
Expand Down
10 changes: 9 additions & 1 deletion dissect/target/plugins/os/unix/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,18 @@
from dissect.target.exceptions import UnsupportedPluginError
from dissect.target.helpers import configutil
from dissect.target.helpers.fsutil import TargetPath
from dissect.target.helpers.record import UnixApplicationRecord
from dissect.target.helpers.record import (
COMMON_APPLICATION_FIELDS,
TargetRecordDescriptor,
)
from dissect.target.plugin import Plugin, export
from dissect.target.target import Target

UnixApplicationRecord = TargetRecordDescriptor(
"unix/application",
COMMON_APPLICATION_FIELDS,
)


class UnixApplicationsPlugin(Plugin):
"""Unix Applications plugin."""
Expand Down
8 changes: 3 additions & 5 deletions dissect/target/plugins/os/unix/bsd/_os.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from __future__ import annotations

from typing import List, Optional

from dissect.target.filesystem import Filesystem
from dissect.target.plugin import OperatingSystem, export
from dissect.target.plugins.os.unix._os import UnixPlugin
Expand All @@ -13,7 +11,7 @@ def __init__(self, target: Target):
super().__init__(target)

@classmethod
def detect(cls, target: Target) -> Optional[Filesystem]:
def detect(cls, target: Target) -> Filesystem | None:
for fs in target.filesystems:
# checking the existence of /var/authpf for free- and openbsd
# checking the existence of /var/at for net- and freebsd
Expand All @@ -27,7 +25,7 @@ def os(self) -> str:
return OperatingSystem.BSD.value

@export(property=True)
def hostname(self) -> Optional[str]:
def hostname(self) -> str | None:
fh = self.target.fs.path("/etc/rc.conf")

for line in fh.open("rt").readlines():
Expand All @@ -36,6 +34,6 @@ def hostname(self) -> Optional[str]:
return hostname

@export(property=True)
def ips(self) -> Optional[List[str]]:
def ips(self) -> list[str] | None:
self.target.log.error(f"ips plugin not implemented for {self.__class__}")
return None
61 changes: 61 additions & 0 deletions dissect/target/plugins/os/unix/bsd/darwin/_os.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from __future__ import annotations

from pathlib import Path

from dissect.target.filesystem import Filesystem
from dissect.target.plugins.os.unix.bsd._os import BsdPlugin
from dissect.target.target import Target

# https://en.wikipedia.org/wiki/Mach-O
ARCH_MAP = {
b"\x0c\x00\x00\x01": "arm64", # big endian, x64
b"\x01\x00\x00\x0c": "arm64", # little endian, x64
b"\x0c\x00\x00\x00": "arm32", # big endian, x32
b"\x00\x00\x00\x0c": "arm32", # little endian, x32
}


class DarwinPlugin(BsdPlugin):
"""Darwin plugin."""

def __init__(self, target: Target):
super().__init__(target)

@classmethod
def detect(cls, target: Target) -> Filesystem | None:
for fs in target.filesystems:
if (fs.exists("/Library") and fs.exists("/Applications")) or fs.exists("/private/var/mobile"):
return fs


def detect_macho_arch(paths: list[str | Path], fs: Filesystem | None = None) -> str | None:
"""Detect the architecture of the system by reading the Mach-O headers of the provided binaries.

We could use the mach-o magic headers (feedface, feedfacf, cafebabe), but the mach-o cpu type
also contains bitness.

Args:
paths: List of strings or ``Path`` objects.
fs: Optional filesystem to search the provided paths in. Required if ``paths`` is a list of strings.

Returns:
Detected architecture (e.g. ``arm64``) or ``None``.

Resources:
- https://github.com/opensource-apple/cctools/blob/master/include/mach/machine.h
"""
for path in paths:
if isinstance(path, str):
if not fs:
raise ValueError("Provided string paths but no filesystem!")
path = fs.path(path)

if not path.is_file():
continue

try:
with path.open("rb") as fh:
fh.seek(4)
return ARCH_MAP.get(fh.read(4)) # mach-o cpu type
except Exception:
pass
98 changes: 98 additions & 0 deletions dissect/target/plugins/os/unix/bsd/darwin/ios/_os.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from __future__ import annotations

import plistlib
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterator

from dissect.target.filesystem import Filesystem, VirtualFilesystem
from dissect.target.helpers.record import IOSUserRecord
from dissect.target.plugin import OperatingSystem, export
from dissect.target.plugins.os.unix.bsd.darwin._os import (
DarwinPlugin,
detect_macho_arch,
)
from dissect.target.target import Target


class IOSPlugin(DarwinPlugin):
"""Apple iOS plugin.

Resources:
- https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html
- https://corp.digitalcorpora.org/corpora/mobile/iOS17/
""" # noqa: E501

SYSTEM = "/private/var/preferences/SystemConfiguration/preferences.plist"
GLOBAL = "/private/var/mobile/Library/Preferences/.GlobalPreferences.plist"
VERSION = "/System/Library/CoreServices/SystemVersion.plist"

# /private/etc/master.passwd is a copy of /private/etc/passwd
PASSWD_FILES = ["/private/etc/passwd"]

def __init__(self, target: Target):
super().__init__(target)

self._config = Config.load(
target.fs.path(self.SYSTEM),
target.fs.path(self.GLOBAL),
target.fs.path(self.VERSION),
)

@classmethod
def detect(cls, target: Target) -> Filesystem | None:
for fs in target.filesystems:
if fs.exists("/private/var/preferences") and fs.exists("/private/var/mobile"):
return fs

@classmethod
def create(cls, target: Target, sysvol: VirtualFilesystem) -> None:
target.fs.mount("/", sysvol)
return cls(target)

@export(property=True)
def hostname(self) -> str | None:
try:
# ComputerName can contain invalid utf characters, so we use HostName instead.
return self._config.SYSTEM["System"]["System"]["HostName"]
except KeyError:
pass

@export(property=True)
def ips(self) -> list:
return []

@export(property=True)
def version(self) -> str:
return f'{self._config.VERSION["ProductName"]} {self._config.VERSION["ProductVersion"]} ({self._config.VERSION["ProductBuildVersion"]})' # noqa: E501

@export(record=IOSUserRecord)
def users(self) -> Iterator[IOSUserRecord]:
for user in super().users():
yield IOSUserRecord(**user._asdict())

@export(property=True)
def os(self) -> str:
return OperatingSystem.IOS.value

@export(property=True)
def architecture(self) -> str | None:
if arch := detect_macho_arch(["/bin/df", "/bin/ps", "/sbin/fsck", "/sbin/mount"], fs=self.target.fs):
return f"{arch}-ios"


@dataclass
class Config:
SYSTEM: dict[str, Any]
GLOBAL: dict[str, Any]
VERSION: dict[str, Any]

@classmethod
def load(cls, *args: list[Path]) -> Config:
plists = []
for path in args:
if path.is_file():
plists.append(plistlib.load(path.open("rb")))
else:
plists.append({})
return cls(*plists)
Loading
Loading