Skip to content

Commit a1e6090

Browse files
committed
Changes
1 parent 7039c64 commit a1e6090

4 files changed

Lines changed: 197 additions & 257 deletions

File tree

dissect/target/helpers/regutil.py

Lines changed: 133 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,16 @@
3030
RE_GLOB_MAGIC = re.compile(r"[*?[]")
3131
RE_REGFLEX_NAME_VALUE = re.compile(r'^"(?P<name>(?:[^"\\]|\\.)*?)"=(?P<value>.*)')
3232

33-
REG_FLEX_HEADER = "Windows Registry Editor Version 5.00"
34-
_REG_LINE_LIMIT = 80
33+
REGFLEX_HEADER = "Windows Registry Editor Version 5.00"
34+
_REGFLEX_LINE_LIMIT = 80
35+
36+
SHORTNAMES = {
37+
"HKLM": "HKEY_LOCAL_MACHINE",
38+
"HKCC": "HKEY_CURRENT_CONFIG",
39+
"HKCU": "HKEY_CURRENT_USER",
40+
"HKCR": "HKEY_CLASSES_ROOT",
41+
"HKU": "HKEY_USERS",
42+
}
3543

3644

3745
KeyType = regf.IndexLeaf | regf.FastLeaf | regf.HashLeaf | regf.IndexRoot | regf.KeyNode
@@ -798,110 +806,11 @@ def map_definition(self, fh: TextIO) -> None:
798806

799807
vhive.map_key(vkey.path, vkey)
800808

801-
def dump_definition(self, fh: TextIO, paths: list[str] | None = None) -> None:
802-
"""Dump the parsed registry hives to a .reg file format.
803-
804-
This is the inverse of :meth:`map_definition`. When ``paths`` is
805-
given, only the listed registry subtrees are written; otherwise every
806-
hive stored in :attr:`hives` is exported in full.
807-
808-
The header always contains a comment line indicating that the file was
809-
generated by RegFlex. When ``paths`` is provided each path is also
810-
listed as an individual comment line so that the exported file is
811-
self-documenting.
812-
813-
Args:
814-
fh: A file-like object opened in text mode to write the registry export to.
815-
paths: Optional list of full registry paths to export
816-
(e.g. ``["HKEY_LOCAL_MACHINE\\\\SOFTWARE\\\\Test"]``).
817-
When ``None`` all hives are exported in full.
818-
"""
819-
fh.write(f"{REG_FLEX_HEADER}\n\n")
820-
fh.write("; This .reg file was generated by RegFlex (part of Dissect Target)\n")
821-
if paths:
822-
fh.write("; Registry paths exported:\n")
823-
fh.writelines(f"; {path}\n" for path in paths)
824-
fh.write("\n")
825-
826-
if not paths:
827-
for hive_name, hive in self.hives.items():
828-
for subkey in hive.root().subkeys():
829-
write_reg_key(fh, subkey, f"{hive_name}\\{subkey.name}")
830-
return
831-
832-
for path in paths:
833-
hive_name, _, key_path = path.partition("\\")
834-
hive_name = hive_name.upper()
835-
hive = self.hives.get(hive_name)
836-
if hive is None:
837-
log.warning("RegFlex: hive %r not found, skipping path %r", hive_name, path)
838-
continue
839-
try:
840-
key = hive.key(key_path) if key_path else hive.root()
841-
except RegistryKeyNotFoundError:
842-
log.warning("RegFlex: key %r not found in hive %r, skipping", key_path, hive_name)
843-
continue
844-
write_reg_key(fh, key, path)
845-
846809

847810
class RegFlexHive(VirtualHive):
848811
pass
849812

850813

851-
def _escape_reg_str(value: str) -> str:
852-
"""Escape special characters in a string for .reg file format.
853-
854-
Backslashes are doubled and double-quote characters are backslash-escaped,
855-
matching the encoding used by the Windows Registry Editor.
856-
857-
Args:
858-
value: The string to escape.
859-
860-
Returns:
861-
The escaped string, safe for use inside a quoted .reg value.
862-
"""
863-
return value.replace("\\", "\\\\").replace('"', '\\"')
864-
865-
866-
def _format_hex_data(data: bytes, prefix: str) -> str:
867-
"""Format binary data as hex for .reg format, wrapping long lines with continuation.
868-
869-
Produces comma-separated lowercase hex byte pairs. Lines longer than
870-
:data:`_REG_LINE_LIMIT` characters are split with a trailing backslash and
871-
the continuation line is indented by two spaces, matching the layout
872-
produced by the Windows Registry Editor.
873-
874-
Args:
875-
data: The bytes to encode.
876-
prefix: The line prefix (e.g. ``'"name"=hex:'``). Included verbatim at
877-
the start of the first line and used when computing line length.
878-
879-
Returns:
880-
The formatted string, potentially spanning multiple lines.
881-
"""
882-
if not data:
883-
return prefix
884-
885-
hex_pairs = [f"{b:02x}" for b in data]
886-
lines = []
887-
current = prefix
888-
889-
for i, pair in enumerate(hex_pairs):
890-
is_last = i == len(hex_pairs) - 1
891-
entry = pair if is_last else f"{pair},"
892-
# Reserve one column for the backslash continuation character on non-final segments
893-
fits = len(current) + len(entry) + (0 if is_last else 1) <= _REG_LINE_LIMIT
894-
895-
if not fits and i > 0:
896-
lines.append(f"{current}\\")
897-
current = f" {entry}"
898-
else:
899-
current += entry
900-
901-
lines.append(current)
902-
return "\n".join(lines)
903-
904-
905814
class RegFlexKey(VirtualKey):
906815
pass
907816

@@ -977,12 +886,66 @@ def parse_flex_value(value: str) -> tuple[RegistryValueType, ValueType]:
977886
raise ValueError(f"Unsupported registry flex value type: {value!r}")
978887

979888

980-
def write_flex_value(value: RegistryValue) -> str:
889+
def _escape_flex_str(value: str) -> str:
890+
"""Escape special characters in a string for ``.reg`` file format.
891+
892+
Backslashes are doubled and double-quote characters are backslash-escaped,
893+
matching the encoding used by the Windows Registry Editor.
894+
895+
Args:
896+
value: The string to escape.
897+
898+
Returns:
899+
The escaped string, safe for use inside a quoted ``.reg`` value.
900+
"""
901+
return value.replace("\\", "\\\\").replace('"', '\\"')
902+
903+
904+
def _format_flex_hex(data: bytes, prefix: str) -> str:
905+
"""Format binary data as hex for .reg format, wrapping long lines with continuation.
906+
907+
Produces comma-separated lowercase hex byte pairs. Lines longer than
908+
:data:`_REGFLEX_LINE_LIMIT` characters are split with a trailing backslash and
909+
the continuation line is indented by two spaces, matching the layout
910+
produced by the Windows Registry Editor.
911+
912+
Args:
913+
data: The bytes to encode.
914+
prefix: The line prefix (e.g. ``'"name"=hex:'``). Included verbatim at
915+
the start of the first line and used when computing line length.
916+
917+
Returns:
918+
The formatted string, potentially spanning multiple lines.
919+
"""
920+
if not data:
921+
return prefix
922+
923+
hex_pairs = [f"{b:02x}" for b in data]
924+
lines = []
925+
current = prefix
926+
927+
for i, pair in enumerate(hex_pairs):
928+
is_last = i == len(hex_pairs) - 1
929+
entry = pair if is_last else f"{pair},"
930+
# Reserve one column for the backslash continuation character on non-final segments
931+
fits = len(current) + len(entry) + (0 if is_last else 1) <= _REGFLEX_LINE_LIMIT
932+
933+
if not fits and i > 0:
934+
lines.append(f"{current}\\")
935+
current = f" {entry}"
936+
else:
937+
current += entry
938+
939+
lines.append(current)
940+
return "\n".join(lines)
941+
942+
943+
def format_flex_value(value: RegistryValue) -> str:
981944
"""Format a :class:`RegistryValue` as a ``.reg`` file value line.
982945
983946
This is the inverse of :func:`parse_flex_value`.
984947
985-
When the registry type is :attr:`RegistryValueType.NONE` the Python type
948+
When the value type is :attr:`RegistryValueType.NONE` the Python type
986949
of the value data is used to infer an appropriate registry type:
987950
988951
- :class:`str` → :attr:`RegistryValueType.SZ`
@@ -1001,7 +964,7 @@ def write_flex_value(value: RegistryValue) -> str:
1001964
data = value.value
1002965
vtype = RegistryValueType(value.type)
1003966

1004-
name_str = "@" if name == "" else f'"{_escape_reg_str(name)}"'
967+
name_str = "@" if name == "" else f'"{_escape_flex_str(name)}"'
1005968

1006969
# Infer a sensible registry type from the Python value when the type is NONE.
1007970
# This covers VirtualValues which always report NONE.
@@ -1014,58 +977,79 @@ def write_flex_value(value: RegistryValue) -> str:
1014977
vtype = RegistryValueType.MULTI_SZ
1015978
elif isinstance(data, bytes):
1016979
vtype = RegistryValueType.BINARY
980+
1017981
if vtype == RegistryValueType.SZ:
1018-
return f'{name_str}="{_escape_reg_str(str(data))}"'
982+
return f'{name_str}="{_escape_flex_str(str(data))}"'
1019983

1020984
if vtype == RegistryValueType.DWORD:
1021985
return f"{name_str}=dword:{int(data):08x}"
1022986

1023987
if vtype == RegistryValueType.BINARY:
1024-
return _format_hex_data(data if isinstance(data, bytes) else b"", f"{name_str}=hex:")
988+
return _format_flex_hex(data if isinstance(data, bytes) else b"", f"{name_str}=hex:")
1025989

1026990
if vtype == RegistryValueType.EXPAND_SZ:
1027991
raw = data.encode("utf-16-le") + b"\x00\x00" if isinstance(data, str) else (data or b"")
1028-
return _format_hex_data(raw, f"{name_str}=hex(2):")
992+
return _format_flex_hex(raw, f"{name_str}=hex(2):")
1029993

1030994
if vtype == RegistryValueType.MULTI_SZ:
1031995
if isinstance(data, list):
1032996
joined = "".join(f"{s}\x00" for s in data) + "\x00"
1033997
raw = joined.encode("utf-16-le")
1034998
else:
1035999
raw = data if isinstance(data, bytes) else b""
1036-
return _format_hex_data(raw, f"{name_str}=hex(7):")
1000+
return _format_flex_hex(raw, f"{name_str}=hex(7):")
10371001

10381002
if vtype == RegistryValueType.DWORD_BIG_ENDIAN:
10391003
raw = int(data).to_bytes(4, "big") if isinstance(data, int) else (data or b"")
1040-
return _format_hex_data(raw, f"{name_str}=hex(5):")
1004+
return _format_flex_hex(raw, f"{name_str}=hex(5):")
10411005

10421006
if vtype == RegistryValueType.QWORD:
10431007
raw = int(data).to_bytes(8, "little") if isinstance(data, int) else (data or b"")
1044-
return _format_hex_data(raw, f"{name_str}=hex(b):")
1008+
return _format_flex_hex(raw, f"{name_str}=hex(b):")
10451009

10461010
if vtype == RegistryValueType.NONE:
10471011
raw = data if isinstance(data, bytes) else b""
1048-
return _format_hex_data(raw, f"{name_str}=hex(0):")
1012+
return _format_flex_hex(raw, f"{name_str}=hex(0):")
10491013

10501014
# Fallback for LINK, RESOURCE_LIST, FULL_RESOURCE_DESCRIPTOR, RESOURCE_REQUIREMENTS_LIST, etc.
10511015
raw = data if isinstance(data, bytes) else b""
1052-
return _format_hex_data(raw, f"{name_str}=hex({int(vtype):x}):")
1016+
return _format_flex_hex(raw, f"{name_str}=hex({int(vtype):x}):")
10531017

10541018

1055-
def write_reg_key(fh: TextIO, key: RegistryKey, path: str) -> None:
1056-
"""Recursively write a registry key and its subkeys in .reg format.
1019+
def format_flex_key(
1020+
key: RegistryKey,
1021+
path: str | None = None,
1022+
) -> Iterator[str]:
1023+
"""Recursively format a registry key and its subkeys in ``.reg`` format.
10571024
10581025
Args:
1059-
fh: A file-like object opened in text mode.
1060-
key: The registry key to write.
1061-
path: The full registry path for this key
1062-
(e.g. ``HKEY_LOCAL_MACHINE\\SOFTWARE\\Test``).
1026+
key: The registry key to format.
1027+
path: The full registry path for this key (e.g. ``HKEY_LOCAL_MACHINE\\SOFTWARE\\Test``).
1028+
If ``None``, ``key.path`` is used.
10631029
"""
1064-
fh.write(f"[{path}]\n")
1065-
fh.writelines(f"{write_flex_value(reg_value)}\n" for reg_value in key.values())
1066-
fh.write("\n")
1030+
yield f"[{path or key.path}]"
1031+
for value in key.values():
1032+
yield f"{format_flex_value(value)}"
1033+
yield ""
10671034
for subkey in key.subkeys():
1068-
write_reg_key(fh, subkey, f"{path}\\{subkey.name}")
1035+
yield from format_flex_key(subkey, f"{path or key.path}\\{subkey.name}")
1036+
1037+
1038+
def format_flex_header(keys: list[str] | None = None) -> Iterator[str]:
1039+
"""Format the header for a ``.reg`` file.
1040+
1041+
Args:
1042+
keys: Optional list of registry paths that will be exported in the file.
1043+
If provided, each path is included as a comment line in the header.
1044+
"""
1045+
yield f"{REGFLEX_HEADER}"
1046+
yield ""
1047+
yield "; Generated by dissect.target"
1048+
if keys:
1049+
yield "; Registry paths exported:"
1050+
for key in keys:
1051+
yield f"; {key}"
1052+
yield ""
10691053

10701054

10711055
def has_glob_magic(pattern: str) -> bool:
@@ -1080,6 +1064,33 @@ def has_glob_magic(pattern: str) -> bool:
10801064
return RE_GLOB_MAGIC.search(pattern) is not None
10811065

10821066

1067+
def split_key_path(path: str) -> tuple[str, str]:
1068+
"""Split a registry key path into the canonical ``HKEY_*`` hive name and the rest of the path.
1069+
1070+
Args:
1071+
path: A registry path starting with a shortname such as ``HKLM`` or ``HKCU``.
1072+
1073+
Returns:
1074+
A tuple containing the canonical ``HKEY_*`` hive name and the rest of the path.
1075+
"""
1076+
hive, _, rest = path.partition("\\")
1077+
expanded = SHORTNAMES.get(hive.upper(), hive.upper())
1078+
return expanded, rest
1079+
1080+
1081+
def expand_key_path(path: str) -> str:
1082+
"""Expand a registry shortname prefix to the canonical ``HKEY_*`` name.
1083+
1084+
Args:
1085+
path: A registry path starting with a shortname such as ``HKLM`` or ``HKCU``.
1086+
1087+
Returns:
1088+
The path with the leading component expanded to its full name.
1089+
"""
1090+
hive, rest = split_key_path(path)
1091+
return f"{hive}\\{rest}"
1092+
1093+
10831094
def glob_split(pattern: str) -> tuple[str]:
10841095
"""Split a key path with glob patterns on the first key path part with glob patterns.
10851096

0 commit comments

Comments
 (0)