Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
215 changes: 215 additions & 0 deletions dissect/target/helpers/reg_export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
#!/usr/bin/env python3
"""
reg-export.py - Export Windows registry hives to .reg format using dissect.target
"""

import argparse
import sys
import time
from pathlib import Path

from dissect.target import Target
from dissect.target.helpers.regutil import VirtualHive, VirtualKey

# Define SHORTNAMES locally to avoid modifying dissect code
# Taken from registry.py in dissect.target.plugins.os.windows.registry
SHORTNAMES = {
"HKLM": "HKEY_LOCAL_MACHINE",
"HKCC": "HKEY_CURRENT_CONFIG",
"HKCU": "HKEY_CURRENT_USER",
"HKCR": "HKEY_CLASSES_ROOT",
"HKU": "HKEY_USERS",
}

REG_FILE_HEADER = "Windows Registry Editor Version "
REG_FILE_VERSION = "5.00"


def _escape_reg_string(value: str) -> str:
"""Escape special characters for .reg file format"""
return str(value).replace("\\", "\\\\").replace('"', '\\"')


def export_registry(target: Target, paths: list) -> str:
"""Export registry keys and values recursively"""
# Write header and comments about exported paths
lines = []
lines.append(REG_FILE_HEADER + REG_FILE_VERSION)
lines.append("")
lines.append("; This .reg file was generated using reg-export.py (part of Dissect Target)")
lines.append("; Registery paths exported:")
lines.extend([f"; - {path}" for path in paths])
lines.append("")

# Process each specified path
for path in paths:
print("Exporting path:", path)

start_key = target.registry.key(path)
_export_key(start_key, path, lines, path)

return "\n".join(lines)


def _export_key(key: VirtualKey, path: str, lines: list, current_path: str = "") -> None:
"""Recursively export registry key"""
try:
if current_path:
lines.append(f"[{current_path}]")
lines.extend(_format_value(value) for value in key.values())
lines.append("")

for subkey in key.subkeys():
subkey_path = f"{current_path}\\{subkey.name}".lstrip("\\")

if path is None or subkey_path.startswith(path):
lines.append(f"[{subkey_path}]")

lines.extend(_format_value(value) for value in subkey.values())

lines.append("")
_export_key(subkey, path, lines, subkey_path)
except Exception as e:
print(f"Error processing key: {e}", file=sys.stderr)


def _format_value(value: VirtualKey) -> str:
"""Format registry value for .reg file"""
name = _escape_reg_string(value.name)
data = value.value

if isinstance(data, bytes):
hex_data = " ".join(f"{b:02x}" for b in data)
return f'"{name}"=hex:{hex_data}'
if isinstance(data, int):
return f'"{name}"=dword:{data:08x}'
return f'"{name}"="{_escape_reg_string(data)}"'


def _expand_shortname(key_path: str) -> str:
"""Expand registry shortnames to full names."""
for short, full in SHORTNAMES.items():
if key_path.startswith(short + "\\"):
return full + key_path[len(short) :]
if key_path == short:
return full
return key_path


def _parse_value(value_str: str) -> object:
"""Parse a value string from .reg format into appropriate Python type.

Args:
value_str (str): The value string from the .reg file.

Returns:
The parsed value (str, int, bytes, etc.).
"""
value_str = value_str.strip()
if value_str.startswith('"') and value_str.endswith('"'):
# String value
inner = value_str[1:-1]
return inner.replace('\\"', '"').replace("\\\\", "\\")
if value_str.startswith("dword:"):
# DWORD value
return int(value_str[6:], 16)
if value_str.startswith("hex:"):
# Binary data
hex_part = value_str[4:]
hex_part = hex_part.replace(",", "")
return bytes.fromhex(hex_part)
# Default to string
return value_str


class RegHive(VirtualHive):
"""VirtualHive wrapper that expands registry shortnames in key lookups."""

def key(self, key_path: str | None) -> VirtualKey:
if key_path:
key_path = _expand_shortname(key_path)
return super().key(key_path)


def _load_reg(reg_content: str) -> RegHive:
"""Load a .reg file content into a RegHive (VirtualHive with shortname support)

Args:
reg_content (str): The content of the .reg file

Returns:
RegHive: The loaded virtual registry hive with shortname expansion
"""
hive = RegHive()

# validate reg header
lines = reg_content.splitlines()
if not lines[0].startswith(REG_FILE_HEADER):
raise ValueError("Invalid .reg file format, unexpected header")

current_key = None
for line in lines:
line = line.strip()
# Skip empty and comment ";" lines)
if not line or line.startswith(";"):
continue
if line.startswith("[") and line.endswith("]"):
key_path = line[1:-1]
# Expand shortnames
key_path = _expand_shortname(key_path)
current_key = VirtualKey(hive, key_path)
hive.map_key(key_path, current_key)
elif "=" in line and current_key:
name, value_str = line.split("=", 1)
name = name.strip('"')
value = _parse_value(value_str)
current_key.add_value(name, value)
return hive


def load_reg_from_file(filepath: str) -> RegHive:
"""Load a .reg file from the specified file path.

Args:
filepath (str): The path to the .reg file.

Returns:
RegHive: The loaded virtual registry hive.
"""
with Path(filepath).open() as file:
reg_content = file.read()
return _load_reg(reg_content)


def main() -> None:
parser = argparse.ArgumentParser(description="Export Windows registry to .reg format")
parser.add_argument("--target", help="Target to open with dissect.target")
parser.add_argument("path", nargs="*", help="Registry paths to export (optional, exports all if none)")
parser.add_argument("--output", default=None, help="Output file (default: reg-save-<epoch>.reg)")

args = parser.parse_args()
print("Processing paths:", args.path)
if not args.target:
parser.print_help()
sys.exit(1)

if args.output is None:
args.output = f"reg-save-{int(time.time())}.reg"

# try:
if True:
target = Target.open(args.target)
output = export_registry(target, args.path)

print(output)
# if output:
# with Path.open(args.output, "w", encoding="utf-8") as f:
# f.write(output)
# print(f"Registry exported to {args.output}")
# #except Exception as e:
# print(f"Error: {e}", file=sys.stderr)
# sys.exit(1)


if __name__ == "__main__":
main()
63 changes: 59 additions & 4 deletions dissect/target/plugins/os/windows/_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@
from dissect.target.exceptions import RegistryError, RegistryValueNotFoundError
from dissect.target.helpers.record import WindowsUserRecord
from dissect.target.plugin import OperatingSystem, OSPlugin, export, internal
from dissect.target.plugins.os.windows.credential import sam

if TYPE_CHECKING:
from collections.abc import Iterator

from typing_extensions import Self

from dissect.target.filesystem import Filesystem
from dissect.target.plugins.os.windows.credential.sam import SamRecord
from dissect.target.plugins.os.windows.credential.sam import SamUserRecord
from dissect.target.target import Target

ARCH_MAP = {
Expand Down Expand Up @@ -287,13 +288,13 @@ def architecture(self) -> str | None:
pass

@cached_property
def _sam_by_sid(self) -> dict[str, SamRecord]:
def _sam_by_sid(self) -> dict[str, SamUserRecord]:
if not (machine_sid := next(self.target.machine_sid(), None)):
return {}

sam_users: dict[str, SamRecord] = {}
sam_users: dict[str, SamUserRecord] = {}
try:
for sam_record in self.target.sam():
for sam_record in self.target.sam.users():
# Compose SID from domain_sid and RID
sam_users[f"{machine_sid.sid}-{sam_record.rid}"] = sam_record
except Exception as e:
Expand All @@ -302,8 +303,62 @@ def _sam_by_sid(self) -> dict[str, SamRecord]:

return sam_users

def _get_home_for_user(self, sid: str) -> str | None:
"""Get the profile path for a user SID from the ProfileList registry key."""
key = "HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList"
try:
profile_key = self.target.registry.key(f"{key}\\{sid}")
return profile_key.value("ProfileImagePath").value
except RegistryError as e:
self.target.log.debug("Could not read ProfileImagePath for SID %s", sid, exc_info=e)
return None

@export(record=WindowsUserRecord)
def users(self) -> Iterator[WindowsUserRecord]:
"""Return Windows users found on the system.

This method yields the unique users found in the SAM hive and the ProfileList registry key.
"""
seen_sids = set()

# user_from_sam can cause tests to fail when not all required registry keys are present.
try:
users = self.users_from_sam()
for user in users:
seen_sids.add(user.sid)
yield user
except Exception as e:
# continue with users_from_ProfileList
self.target.log.warning("Could not read users from SAM hive")
self.target.log.debug("", exc_info=e)

for user in self.users_from_ProfileList():
if user.sid in seen_sids:
continue

seen_sids.add(user.sid)
yield user

@export(record=WindowsUserRecord)
def users_from_sam(self) -> Iterator[WindowsUserRecord]:
"""Return Windows users found in the SAM hive"""
for sam_user in sam.SamPlugin(self.target).users():
home = None
name = None
if sam_user.sid in self._sam_by_sid:
sam_record = self._sam_by_sid[sam_user.sid]
name = sam_record.username
home = self._get_home_for_user(sam_user.sid)

yield WindowsUserRecord(
sid=sam_user.sid,
name=name,
home=self.target.resolve(home) if home else None,
_target=self.target,
)

@export(record=WindowsUserRecord)
def users_from_ProfileList(self) -> Iterator[WindowsUserRecord]:
# Be aware that this function can never do anything which needs user
# registry hives. Initializing those hives will need this function,
# which will then cause a recursion.
Expand Down
Loading