From 24cf6b5e732076a4fe466a0d53f8d72aa01113d3 Mon Sep 17 00:00:00 2001 From: Lennart Haagsma <6630974+lhaagsma@users.noreply.github.com> Date: Wed, 6 May 2026 16:12:00 +0200 Subject: [PATCH 1/2] Add sam groups parsing and regflex for registry testing --- dissect/target/plugins/os/windows/sam.py | 249 ++++++++++++++++-- .../plugins/os/windows/sam/sam_groups.reg | 3 + tests/plugins/os/windows/test_sam.py | 40 ++- tests/tools/test_query.py | 19 +- 4 files changed, 287 insertions(+), 24 deletions(-) create mode 100644 tests/_data/plugins/os/windows/sam/sam_groups.reg diff --git a/dissect/target/plugins/os/windows/sam.py b/dissect/target/plugins/os/windows/sam.py index fec9243bc4..19da8fac28 100644 --- a/dissect/target/plugins/os/windows/sam.py +++ b/dissect/target/plugins/os/windows/sam.py @@ -211,15 +211,38 @@ char salt[16]; /* 0x08 */ /* char data[]; 0x18, variable size */ }; + +typedef struct _ALIAS_C_HDR { + uint32 rid; // 0x00 + uint32 unk04; // 0x04 + uint32 unk08; // 0x08 + uint32 unk0C; // 0x0C + uint32 name_ofs; // 0x10 relative to 0x34 + uint32 name_len; // 0x14 bytes, UTF-16LE + uint32 unk18; // 0x18 + uint32 desc_ofs; // 0x1C relative to 0x34 + uint32 desc_len; // 0x20 bytes, UTF-16LE + uint32 unk24; // 0x24 + uint32 sid_ofs; // 0x28 relative to 0x34 + uint32 sid_len; // 0x2C bytes + uint32 sid_cnt; // 0x30 +} ALIAS_C_HDR; + +typedef struct _SID_PREFIX { + uint8 revision; // 1 + uint8 subcnt; // 1 + uint8 ident_auth[6]; // 6 (big-endian integer) +} SID_PREFIX; """ c_sam = cstruct().load(sam_def) -SamRecord = TargetRecordDescriptor( - "windows/credential/sam", +SamUserRecord = TargetRecordDescriptor( + "windows/credential/sam/user", [ ("datetime", "ts"), ("uint32", "rid"), + ("string", "sid"), ("string", "fullname"), ("string", "username"), ("string", "admincomment"), @@ -236,6 +259,18 @@ ], ) +SamGroupRecord = TargetRecordDescriptor( + "windows/sam/group/member", + [ + ("uint32", "group_rid"), + ("string", "group_sid"), + ("string", "group_name"), + ("string", "group_description"), + ("string", "member_sid"), + ("string", "member_name"), + ], +) + def expand_des_key(key: bytes) -> bytes: # Expand the key from a 7-byte password key into an 8-byte DES key @@ -255,6 +290,85 @@ def expand_des_key(key: bytes) -> bytes: return bytes(s) +def parse_sid_cstruct(buf: bytes, offset: int = 0) -> tuple[str, int]: + """Parse a SID using cstruct for the fixed prefix, then loop for the variable subauthorities. + + Layout: + 1 byte revision + 1 byte subcnt + 6 bytes identifier authority (big-endian) + N x 4-byte subauthorities (little-endian) + """ + SID_PREFIX_LEN = 8 # 1(revision)+1(subcnt)+6(identifier authority) + + if len(buf) - offset < SID_PREFIX_LEN: + raise ValueError("Buffer too small for SID prefix") + + sp = c_sam.SID_PREFIX(buf[offset : offset + SID_PREFIX_LEN]) + rev = sp.revision + subcnt = sp.subcnt + ident_auth = int.from_bytes(bytes(sp.ident_auth), "big") + + cur = offset + SID_PREFIX_LEN + need = subcnt * 4 + if len(buf) - cur < need: + raise ValueError("Buffer too small for SID subauthorities") + + subs: list[int] = [] + for _ in range(subcnt): + # subauthority is little-endian uint32 + val = int.from_bytes(buf[cur : cur + 4], "little") + subs.append(val) + cur += 4 + + sid_str = f"S-{rev}-{ident_auth}" + "".join(f"-{s}" for s in subs) + return sid_str, (cur - offset) + + +def parse_sam_group_c_value(cbytes: bytes) -> tuple[int, str, str, list[str]]: + """Parse an Aliases RID 'C' value using cstruct for the fixed header. + Returns: (rid, name, description, members). + """ + ALIAS_C_HDR_LEN = 0x34 # 13 DWORDs = 52 bytes + + if len(cbytes) < ALIAS_C_HDR_LEN: + raise ValueError("C value too small") + + hdr = c_sam.ALIAS_C_HDR(cbytes[:ALIAS_C_HDR_LEN]) + base = ALIAS_C_HDR_LEN + + def read_utf16_rel(ofs: int, ln: int) -> str: + if ln <= 0: + return "" + start = base + ofs + end = start + ln + if start < 0 or end > len(cbytes): + return "" + try: + return cbytes[start:end].decode("utf-16le", errors="replace") + except Exception: + return "" + + name = read_utf16_rel(hdr.name_ofs, hdr.name_len) + desc = read_utf16_rel(hdr.desc_ofs, hdr.desc_len) + + members: list[str] = [] + if hdr.sid_len > 0 and hdr.sid_cnt > 0: + arr_start = base + hdr.sid_ofs + arr_end = arr_start + hdr.sid_len + if 0 <= arr_start < len(cbytes) and arr_end <= len(cbytes): + arr = cbytes[arr_start:arr_end] + cur = 0 + for _ in range(hdr.sid_cnt): + if cur >= len(arr): + break + sid, used = parse_sid_cstruct(arr, cur) + members.append(sid) + cur += used + + return hdr.rid, name, desc, members + + def rid_to_key(rid: int) -> tuple[bytes, bytes]: s = rid.to_bytes(4, "little", signed=False) k1 = expand_des_key(bytes([s[0], s[1], s[2], s[3], s[0], s[1], s[2]])) @@ -325,23 +439,30 @@ class SamPlugin(Plugin): - https://web.archive.org/web/20190717124313/http://www.beginningtoseethelight.org/ntsecurity/index.htm """ - SAM_KEY = "HKEY_LOCAL_MACHINE\\SAM\\SAM\\Domains\\Account" + __namespace__ = "sam" + + SAM_USER_KEY = "HKEY_LOCAL_MACHINE\\SAM\\SAM\\Domains\\Account" + SAM_GROUP_KEYS = ( + "HKEY_LOCAL_MACHINE\\SAM\\SAM\\Domains\\Builtin\\Aliases", + "HKEY_LOCAL_MACHINE\\SAM\\SAM\\Domains\\Account\\Aliases", + ) + DEFAULT_ADMIN_GROUP_PATH = "HKEY_LOCAL_MACHINE\\SAM\\SAM\\Domains\\Builtin\\Aliases\\00000220" def check_compatible(self) -> None: if not HAS_CRYPTO: raise UnsupportedPluginError("Missing pycryptodome dependency") - if not self.target.has_function("lsa"): - raise UnsupportedPluginError("LSA plugin is required for SAM plugin") + # if not self.target.has_function("lsa"): + # raise UnsupportedPluginError("LSA plugin is required for SAM plugin") - if not len(list(self.target.registry.keys(self.SAM_KEY))) > 0: - raise UnsupportedPluginError(f"Registry key not found: {self.SAM_KEY}") + if not len(list(self.target.registry.keys(self.SAM_USER_KEY))) > 0: + raise UnsupportedPluginError(f"Registry key not found: {self.SAM_USER_KEY}") def calculate_samkey(self, syskey: bytes) -> bytes: aqwerty = b"!@#$%^&*()qwertyUIOPAzxcvbnmQQQQQQQQQQQQ)(*@&%\0" anum = b"0123456789012345678901234567890123456789\0" - f_reg = self.target.registry.key(self.SAM_KEY).value("F").value + f_reg = self.target.registry.key(self.SAM_USER_KEY).value("F").value f = c_sam.DOMAIN_ACCOUNT_F(f_reg) f_key = f_reg[len(c_sam.DOMAIN_ACCOUNT_F) :] fk = c_sam.SAM_KEY(f_key) @@ -375,9 +496,85 @@ def calculate_samkey(self, syskey: bytes) -> bytes: raise ValueError("SAM key checksum validation failed!") return samkey - @export(record=SamRecord) - def sam(self) -> Iterator[SamRecord]: - """Dump SAM entries. + def get_local_admins(self) -> set[str]: + """Retrieve the SIDs of local administrators from the SAM hive.""" + local_admins = set() + try: + admin_group_key = self.target.registry.key(self.DEFAULT_ADMIN_GROUP_PATH) + except Exception: + return local_admins + + c_bytes = admin_group_key.value("C").value + _, _, _, members = parse_sam_group_c_value(c_bytes) + for sid in members: + local_admins.add(sid) + + return local_admins + + @export(record=SamGroupRecord) + def groups(self) -> Iterator[SamGroupRecord]: + """Dump SAM group memberships. + + The Security Account Manager (SAM) registry hive contains registry keys that stores group membership. + + Yields SamGroupRecords with fields: + + .. code-block:: text + + group_rid (uint32): The RID. + group_sid (string): The group SID. + group_name (string): The parsed group name. + group_description (string): The parsed description of the group. + member_sid (string): SID of the user that is member of the group. + member_name (string): Name of the user that is member of the group. + """ + # Windows stores built-in groups and local groups in different locations. + # Local groups have SIDs based on the machine SID, while built-in groups use a well-known SID prefix. + if not (machine_sid := next(self.target.machine_sid(), None)): + # use a placeholder if machine SID is not available + machine_sid = "S-1-5-21-0000000000-0000000000-0000000000" + else: + machine_sid = machine_sid.sid + builtin_prefix = "S-1-5-32" # Built-in group SID prefix + + for group_path in self.SAM_GROUP_KEYS: + # Determine the correct SID prefix based on group type + sid_prefix = builtin_prefix if "Builtin" in group_path else machine_sid + + users = list(self.target.users()) + + for key in self.target.registry.key(group_path).subkeys(): + if key.name in ["Members", "Names"]: + continue + + c_bytes = key.value("C").value + group_rid, group_name, group_desc, group_members = parse_sam_group_c_value(c_bytes) + + group_sid = f"{sid_prefix}-{group_rid}" + + # By yielding only members of groups, we skip empty groups entirely. + for member_sid in group_members: + # Check if the member SID corresponds to a user and get the username + # I had issues using UserPLugin().find(sid=member_sid), probably recursion, so doing it manually. + user_details = None + for user in users: + if user.sid == member_sid: + user_details = user + break + + yield SamGroupRecord( + group_rid=group_rid, + group_sid=group_sid, + group_name=group_name, + group_description=group_desc, + member_sid=member_sid, + member_name=user_details.name if user_details else "", + _target=self.target, + ) + + @export(record=SamUserRecord) + def users(self) -> Iterator[SamUserRecord]: + """Dump SAM user entries. The Security Account Manager (SAM) registry hive contains registry keys that store usernames, full names and passwords in a hashed format, either an LM or NT hash. @@ -385,7 +582,7 @@ def sam(self) -> Iterator[SamRecord]: References: - https://en.wikipedia.org/wiki/Security_Account_Manager - Yields SamRecords with fields: + Yields SamUserRecords with fields: .. code-block:: text @@ -405,13 +602,27 @@ def sam(self) -> Iterator[SamRecord]: lm (string): Parsed LM-hash. nt (string): Parsed NT-hash. """ - syskey = self.target.lsa.syskey # aka. bootkey - samkey = self.calculate_samkey(syskey) # aka. hashed bootkey or hbootkey + # syskey = self.target.lsa.syskey # aka. bootkey + # samkey = self.calculate_samkey(syskey) # aka. hashed bootkey or hbootkey + + try: + syskey = self.target.lsa.syskey # aka. bootkey + samkey = self.calculate_samkey(syskey) # aka. hashed bootkey or hbootkey + except Exception as e: + self.target.log.warning("Could not calculate SAM key") + self.target.log.debug("", exc_info=e) + samkey = None + + # Get machine SID or placeholder SID for constructing user SIDs + if not (machine_sid := next(self.target.machine_sid(), None)): + machine_sid = "S-1-5-21-0000000000-0000000000-0000000000" + else: + machine_sid = machine_sid.sid almpassword = b"LMPASSWORD\0" antpassword = b"NTPASSWORD\0" - for users_key in self.target.registry.keys(f"{self.SAM_KEY}\\Users"): + for users_key in self.target.registry.keys(f"{self.SAM_USER_KEY}\\Users"): for user_key in users_key.subkeys(): if user_key.name == "Names": continue @@ -436,11 +647,15 @@ def sam(self) -> Iterator[SamRecord]: lm_hash = decrypt_single_hash(f.rid, samkey, u_lmpw, almpassword).hex() nt_hash = decrypt_single_hash(f.rid, samkey, u_ntpw, antpassword).hex() - names_key = self.target.registry.key(f"{self.SAM_KEY}\\Users\\Names\\{u_username}") + names_key = self.target.registry.key(f"{self.SAM_USER_KEY}\\Users\\Names\\{u_username}") + + # Construct the SID as - + sid = f"{machine_sid}-{f.rid}" - yield SamRecord( + yield SamUserRecord( ts=names_key.ts, rid=f.rid, + sid=sid, fullname=u_fullname, username=u_username, admincomment=u_admin_comment, diff --git a/tests/_data/plugins/os/windows/sam/sam_groups.reg b/tests/_data/plugins/os/windows/sam/sam_groups.reg new file mode 100644 index 0000000000..440b52c626 --- /dev/null +++ b/tests/_data/plugins/os/windows/sam/sam_groups.reg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae11a5eebe41c748a61b0d9bdd3678adade5589e38de342f719057375ccc8859 +size 224468 diff --git a/tests/plugins/os/windows/test_sam.py b/tests/plugins/os/windows/test_sam.py index 8d945a98fc..e3f2695597 100644 --- a/tests/plugins/os/windows/test_sam.py +++ b/tests/plugins/os/windows/test_sam.py @@ -5,7 +5,9 @@ import pytest from flow.record.fieldtypes import datetime as dt -from dissect.target.helpers.regutil import VirtualKey +from dissect.target.helpers.regutil import RegFlex, VirtualKey +from dissect.target.plugins.os.windows.registry import RegistryPlugin +from tests._utils import absolute_path from tests.plugins.os.windows.test_lsa import map_lsa_system_keys if TYPE_CHECKING: @@ -141,7 +143,7 @@ def test_sam_plugin_rev1(target_win_users: Target, hive_hklm: VirtualHive) -> No ) target_win_users.add_plugin(SamPlugin) - results = list(target_win_users.sam()) + results = list(target_win_users.sam.users()) assert len(results) == 3 assert results[2].ts == dt("2023-01-05 15:56:51.654921+00:00") @@ -344,7 +346,7 @@ def test_sam_plugin_rev2(target_win_users: Target, hive_hklm: VirtualHive) -> No ) target_win_users.add_plugin(SamPlugin) - results = list(target_win_users.sam()) + results = list(target_win_users.sam.users()) assert len(results) == 5 @@ -382,3 +384,35 @@ def test_sam_plugin_rev2(target_win_users: Target, hive_hklm: VirtualHive) -> No assert results[4].logins == 4 assert results[4].lm == "" assert results[4].nt == MD4.new("MD4St1llGo1ngStr0ng!".encode("utf-16-le")).digest().hex() + + +@pytest.mark.skipif(not HAS_CRYPTO, reason="requires pycryptodome") +def test_sam_plugin_groups(target_win_users: Target) -> None: + """Test SAM groups loading from a .reg file using RegFlex.""" + # Load test data registry hive from .reg file using RegFlex + regflex = RegFlex() + with absolute_path("_data/plugins/os/windows/sam/sam_groups.reg").open() as fh: + regflex.map_definition(fh) + + # Map hives from regflex into target + for name, hive in regflex.hives.items(): + if name in RegistryPlugin.SHORTNAMES: + name = RegistryPlugin.SHORTNAMES[name] + target_win_users.registry._map_hive(name, hive) + + # Add the SAM plugin and get groups + target_win_users.add_plugin(SamPlugin) + results = list(target_win_users.sam.groups()) + + # Test for the default administrator group from \\SAM\\Domains\\Builtin\\Aliases + assert len(results) == 10 + assert results[0].group_rid == 544 + assert results[0].group_name == "Administrators" + assert results[0].group_description == "Administrators have complete and unrestricted access to the computer/domain" + assert results[0].member_sid == "S-1-5-21-3713105778-3002763963-2454762811-500" + + # Test for a custom group from \\SAM\\Domains\\Account\\Aliases + assert results[9].group_rid == 1002 + assert results[9].group_name == "custom_local_admin_group" + assert results[9].group_description == "I'm a custom local admin group" + assert results[9].member_sid == "S-1-5-21-3713105778-3002763963-2454762811-1003" diff --git a/tests/tools/test_query.py b/tests/tools/test_query.py index 908c0f5f9b..cba4a42e81 100644 --- a/tests/tools/test_query.py +++ b/tests/tools/test_query.py @@ -322,14 +322,25 @@ def get_plugin(plugins: list[dict], needle: str) -> dict | bool: } # regular plugin - sam_plugin = get_plugin(output, "sam") + sam_plugin = get_plugin(output, "sam.users") assert sam_plugin == { - "name": "sam", - "description": "Dump SAM entries.", + "name": "sam.users", + "description": "Dump SAM user entries.", "output": "record", "arguments": [], "alias": False, - "path": "os.windows.sam.sam", + "path": "os.windows.sam.users", + } + + # regular plugin + sam_plugin = get_plugin(output, "sam.groups") + assert sam_plugin == { + "name": "sam.groups", + "description": "Dump SAM group memberships.", + "output": "record", + "arguments": [], + "alias": False, + "path": "os.windows.sam.groups", } # plugin with arguments From 803ae148792638c4ab20a7025f9cf7ef65a71ba3 Mon Sep 17 00:00:00 2001 From: Lennart Haagsma <6630974+lhaagsma@users.noreply.github.com> Date: Fri, 8 May 2026 16:52:15 +0200 Subject: [PATCH 2/2] Remove some duplicate code and further improvements --- dissect/target/helpers/regutil.py | 16 +++++++- dissect/target/plugins/os/windows/_os.py | 8 ++-- dissect/target/plugins/os/windows/sam.py | 52 ++++++------------------ tests/plugins/os/windows/test__os.py | 5 ++- tests/plugins/os/windows/test_sam.py | 6 +-- 5 files changed, 37 insertions(+), 50 deletions(-) diff --git a/dissect/target/helpers/regutil.py b/dissect/target/helpers/regutil.py index 16b581329a..c733081373 100644 --- a/dissect/target/helpers/regutil.py +++ b/dissect/target/helpers/regutil.py @@ -7,7 +7,7 @@ from collections import defaultdict from functools import cached_property from io import BytesIO -from typing import TYPE_CHECKING, BinaryIO, NewType, TextIO +from typing import TYPE_CHECKING, BinaryIO, Final, NewType, TextIO from dissect.regf import c_regf, regf @@ -30,6 +30,15 @@ RE_GLOB_MAGIC = re.compile(r"[*?[]") RE_REGFLEX_NAME_VALUE = re.compile(r'^"(?P(?:[^"\\]|\\.)*?)"=(?P.*)') +# Define Registry Hive short-to-longname conversion locally. +SHORTNAMES: Final[dict[str, str]] = { + "HKLM": "HKEY_LOCAL_MACHINE", + "HKCC": "HKEY_CURRENT_CONFIG", + "HKCU": "HKEY_CURRENT_USER", + "HKCR": "HKEY_CLASSES_ROOT", + "HKU": "HKEY_USERS", +} + KeyType = regf.IndexLeaf | regf.FastLeaf | regf.HashLeaf | regf.IndexRoot | regf.KeyNode """The possible key types that can be returned from the registry.""" @@ -764,7 +773,12 @@ def map_definition(self, fh: TextIO) -> None: vhive.map_key(vkey.path, vkey) hive, _, path = line[1:-1].partition("\\") + + # Always expand shortname to longname hive = hive.upper() + if hive in SHORTNAMES: + hive = SHORTNAMES[hive] + if hive not in self.hives: self.hives[hive] = RegFlexHive() diff --git a/dissect/target/plugins/os/windows/_os.py b/dissect/target/plugins/os/windows/_os.py index 14c349cf20..b6c55c75d3 100644 --- a/dissect/target/plugins/os/windows/_os.py +++ b/dissect/target/plugins/os/windows/_os.py @@ -17,7 +17,7 @@ from typing_extensions import Self from dissect.target.filesystem import Filesystem - from dissect.target.plugins.os.windows.sam import SamRecord + from dissect.target.plugins.os.windows.sam import SamUserRecord from dissect.target.target import Target @@ -282,13 +282,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: diff --git a/dissect/target/plugins/os/windows/sam.py b/dissect/target/plugins/os/windows/sam.py index 19da8fac28..5cd5b45c13 100644 --- a/dissect/target/plugins/os/windows/sam.py +++ b/dissect/target/plugins/os/windows/sam.py @@ -14,6 +14,7 @@ from dissect.cstruct import cstruct from dissect.util import ts +from dissect.util.sid import read_sid, write_sid from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import TargetRecordDescriptor @@ -290,41 +291,6 @@ def expand_des_key(key: bytes) -> bytes: return bytes(s) -def parse_sid_cstruct(buf: bytes, offset: int = 0) -> tuple[str, int]: - """Parse a SID using cstruct for the fixed prefix, then loop for the variable subauthorities. - - Layout: - 1 byte revision - 1 byte subcnt - 6 bytes identifier authority (big-endian) - N x 4-byte subauthorities (little-endian) - """ - SID_PREFIX_LEN = 8 # 1(revision)+1(subcnt)+6(identifier authority) - - if len(buf) - offset < SID_PREFIX_LEN: - raise ValueError("Buffer too small for SID prefix") - - sp = c_sam.SID_PREFIX(buf[offset : offset + SID_PREFIX_LEN]) - rev = sp.revision - subcnt = sp.subcnt - ident_auth = int.from_bytes(bytes(sp.ident_auth), "big") - - cur = offset + SID_PREFIX_LEN - need = subcnt * 4 - if len(buf) - cur < need: - raise ValueError("Buffer too small for SID subauthorities") - - subs: list[int] = [] - for _ in range(subcnt): - # subauthority is little-endian uint32 - val = int.from_bytes(buf[cur : cur + 4], "little") - subs.append(val) - cur += 4 - - sid_str = f"S-{rev}-{ident_auth}" + "".join(f"-{s}" for s in subs) - return sid_str, (cur - offset) - - def parse_sam_group_c_value(cbytes: bytes) -> tuple[int, str, str, list[str]]: """Parse an Aliases RID 'C' value using cstruct for the fixed header. Returns: (rid, name, description, members). @@ -362,7 +328,10 @@ def read_utf16_rel(ofs: int, ln: int) -> str: for _ in range(hdr.sid_cnt): if cur >= len(arr): break - sid, used = parse_sid_cstruct(arr, cur) + + sid = read_sid(arr[cur:]) + used = len(write_sid(sid)) + members.append(sid) cur += used @@ -452,8 +421,8 @@ def check_compatible(self) -> None: if not HAS_CRYPTO: raise UnsupportedPluginError("Missing pycryptodome dependency") - # if not self.target.has_function("lsa"): - # raise UnsupportedPluginError("LSA plugin is required for SAM plugin") + if not self.target.has_function("lsa"): + raise UnsupportedPluginError("LSA plugin is required for SAM plugin") if not len(list(self.target.registry.keys(self.SAM_USER_KEY))) > 0: raise UnsupportedPluginError(f"Registry key not found: {self.SAM_USER_KEY}") @@ -644,8 +613,11 @@ def users(self) -> Iterator[SamUserRecord]: u_lmpw = v_data[v.lmpw_ofs : v.lmpw_ofs + v.lmpw_len] u_ntpw = v_data[v.ntpw_ofs : v.ntpw_ofs + v.ntpw_len] - lm_hash = decrypt_single_hash(f.rid, samkey, u_lmpw, almpassword).hex() - nt_hash = decrypt_single_hash(f.rid, samkey, u_ntpw, antpassword).hex() + lm_hash = "" + nt_hash = "" + if samkey: + lm_hash = decrypt_single_hash(f.rid, samkey, u_lmpw, almpassword).hex() + nt_hash = decrypt_single_hash(f.rid, samkey, u_ntpw, antpassword).hex() names_key = self.target.registry.key(f"{self.SAM_USER_KEY}\\Users\\Names\\{u_username}") diff --git a/tests/plugins/os/windows/test__os.py b/tests/plugins/os/windows/test__os.py index 64b90c1e99..c67145bbb7 100644 --- a/tests/plugins/os/windows/test__os.py +++ b/tests/plugins/os/windows/test__os.py @@ -290,7 +290,10 @@ def test_windows_user_from_sam(target_win_users: Target) -> None: target_win_users.machine_sid = Mock( return_value=iter([ComputerSidRecord(sid="S-1-5-21-3263113198-3007035898-945866154")]) ) - target_win_users.sam = Mock(return_value=[fake_sam_user]) + + # Need to define sam, then sam.users + target_win_users.sam = Mock() + target_win_users.sam.users = Mock(return_value=[fake_sam_user]) users = list(target_win_users.users()) diff --git a/tests/plugins/os/windows/test_sam.py b/tests/plugins/os/windows/test_sam.py index e3f2695597..0afc5c366b 100644 --- a/tests/plugins/os/windows/test_sam.py +++ b/tests/plugins/os/windows/test_sam.py @@ -6,7 +6,6 @@ from flow.record.fieldtypes import datetime as dt from dissect.target.helpers.regutil import RegFlex, VirtualKey -from dissect.target.plugins.os.windows.registry import RegistryPlugin from tests._utils import absolute_path from tests.plugins.os.windows.test_lsa import map_lsa_system_keys @@ -391,13 +390,12 @@ def test_sam_plugin_groups(target_win_users: Target) -> None: """Test SAM groups loading from a .reg file using RegFlex.""" # Load test data registry hive from .reg file using RegFlex regflex = RegFlex() - with absolute_path("_data/plugins/os/windows/sam/sam_groups.reg").open() as fh: + reg_file_path = "_data/plugins/os/windows/sam/sam_groups.reg" + with absolute_path(reg_file_path).open() as fh: regflex.map_definition(fh) # Map hives from regflex into target for name, hive in regflex.hives.items(): - if name in RegistryPlugin.SHORTNAMES: - name = RegistryPlugin.SHORTNAMES[name] target_win_users.registry._map_hive(name, hive) # Add the SAM plugin and get groups