From 8e82280f0ea9e25f579f2562911ead973f90236e Mon Sep 17 00:00:00 2001 From: Ebrix Date: Thu, 12 Jun 2025 09:05:06 +0200 Subject: [PATCH 01/27] Adding basic winreg, with limited actions (ls, cd, use) --- scapyred/winreg.py | 429 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 429 insertions(+) create mode 100644 scapyred/winreg.py diff --git a/scapyred/winreg.py b/scapyred/winreg.py new file mode 100644 index 0000000..9becd63 --- /dev/null +++ b/scapyred/winreg.py @@ -0,0 +1,429 @@ +""" +DCE/RPC client +""" + +import socket +import os +import pathlib +from typing import Optional, NoReturn + +from scapy.layers.msrpce.all import * +from scapy.layers.msrpce.raw.ms_samr import * +from scapy.layers.msrpce.raw.ms_rrp import * +from scapy.layers.dcerpc import RPC_C_AUTHN_LEVEL +from scapy.utils import ( + CLIUtil, + pretty_list, + human_size, + valid_ip, + valid_ip6, +) +from scapy.layers.kerberos import ( + KerberosSSP, + krb_as_and_tgs, + _parse_upn, +) +from scapy.config import conf +from scapy.themes import DefaultTheme +from scapy.base_classes import Net +from scapy.utils6 import Net6 + +from scapy.layers.msrpce.rpcclient import DCERPC_Client +from scapy.layers.dcerpc import find_dcerpc_interface, DCERPC_Transport +from scapy.layers.ntlm import MD4le, NTLMSSP +from scapy.layers.spnego import SPNEGOSSP +from scapy.layers.kerberos import KerberosSSP + +from pathlib import PureWindowsPath + +conf.color_theme = DefaultTheme() + + +KEY_QUERY_VALUE = 0x00000001 +KEY_ENUMERATE_SUB_KEYS = 0x00000008 +MAX_ALLOWED = 0x02000000 +ERROR_NO_MORE_ITEMS = 0x00000103 +ERROR_SUBKEY_NOT_FOUND = 0x000006F7 + +# Predefined keys +HKEY_CLASSES_ROOT = "HKCROOT" # Registry entries subordinate to this key define types (or classes) of documents and the properties associated with those types. The subkeys of the HKEY_CLASSES_ROOT key are a merged view of the following two subkeys: +HKEY_CURRENT_USER = "HKCU" # Registry entries subordinate to this key define the preferences of the current user. These preferences include the settings of environment variables, data on program groups, colors, printers, network connections, and application preferences. The HKEY_CURRENT_USER root key is a subkey of the HKEY_USERS root key, as described in section 3.1.1.8. +HKEY_LOCAL_MACHINE = "HKLM" # Registry entries subordinate to this key define the physical state of the computer, including data on the bus type, system memory, and installed hardware and software. +HKEY_CURRENT_CONFIG = "" # This key contains information on the current hardware profile of the local computer. +HKEY_USERS = "HKU" +HKEY_PERFORMANCE_DATA = "HKPERFORMANCE" # Registry entries subordinate to this key allow access to performance data. +HKEY_PERFORMANCE_TEXT = "" # Registry entries subordinate to this key reference the text strings that describe counters in U.S. English. +HKEY_PERFORMANCE_NLSTEXT = "" # Registry entries subordinate to this key reference the text strings that describe counters in the local language of the area in which the computer is running. + + +@conf.commands.register +class regclient(CLIUtil): + r""" + A simple registry CLI + + :param target: can be a hostname, the IPv4 or the IPv6 to connect to + :param UPN: the upn to use (DOMAIN/USER, DOMAIN\USER, USER@DOMAIN or USER) + :param guest: use guest mode (over NTLM) + :param ssp: if provided, use this SSP for auth. + :param kerberos: if available, whether to use Kerberos or not + :param kerberos_required: require kerberos + :param port: the TCP port. default 445 + :param password: (string) if provided, used for auth + :param HashNt: (bytes) if provided, used for auth (NTLM) + :param ST: if provided, the service ticket to use (Kerberos) + :param KEY: if provided, the session key associated to the ticket (Kerberos) + :param cli: CLI mode (default True). False to use for scripting + + Some additional SMB parameters are available under help(SMB_Client). Some of + them include the following: + + :param REQUIRE_ENCRYPTION: requires encryption. + """ + + def __init__( + self, + target: str, + UPN: str = None, + password: str = None, + guest: bool = False, + kerberos: bool = True, + kerberos_required: bool = False, + HashNt: str = None, + port: int = 445, + timeout: int = 2, + debug: int = 0, + ssp=None, + ST=None, + KEY=None, + cli=True, + # SMB arguments + **kwargs, + ): + if cli: + self._depcheck() + hostname = None + # Check if target is a hostname / Check IP + if ":" in target: + family = socket.AF_INET6 + if not valid_ip6(target): + hostname = target + target = str(Net6(target)) + else: + family = socket.AF_INET + if not valid_ip(target): + hostname = target + target = str(Net(target)) + assert UPN or ssp or guest, "Either UPN, ssp or guest must be provided !" + # Do we need to build a SSP? + if ssp is None: + # Create the SSP (only if not guest mode) + if not guest: + # Check UPN + try: + _, realm = _parse_upn(UPN) + if realm == ".": + # Local + kerberos = False + except ValueError: + # not a UPN: NTLM + kerberos = False + # Do we need to ask the password? + if HashNt is None and password is None and ST is None: + # yes. + from prompt_toolkit import prompt + + password = prompt("Password: ", is_password=True) + ssps = [] + # Kerberos + if kerberos and hostname: + if ST is None: + resp = krb_as_and_tgs( + upn=UPN, + spn="cifs/%s" % hostname, + password=password, + debug=debug, + ) + if resp is not None: + ST, KEY = resp.tgsrep.ticket, resp.sessionkey + if ST: + ssps.append(KerberosSSP(UPN=UPN, ST=ST, KEY=KEY, debug=debug)) + elif kerberos_required: + raise ValueError( + "Kerberos required but target isn't a hostname !" + ) + elif kerberos_required: + raise ValueError( + "Kerberos required but domain not specified in the UPN, " + "or target isn't a hostname !" + ) + # NTLM + if not kerberos_required: + if HashNt is None and password is not None: + HashNt = MD4le(password) + ssps.append(NTLMSSP(UPN=UPN, HASHNT=HashNt)) + # Build the SSP + ssp = SPNEGOSSP(ssps) + else: + # Guest mode + ssp = None + + # Interface WINREG + self.interface = find_dcerpc_interface("winreg") + + # Connexion NCACN_NP: SMB + self.client = DCERPC_Client( + DCERPC_Transport.NCACN_NP, + auth_level=RPC_C_AUTHN_LEVEL.PKT_PRIVACY, + ssp=ssp, + ndr64=False, + ) + + self.client.verb = False + self.client.connect(target) + self.client.open_smbpipe("winreg") + self.client.bind(self.interface) + self.root_handle = {} + self.current_root_handle = None + self.current_root_path = "CHOOSE ROOT KEY" + self.current_subkey_handle = None + self.current_subkey_path: PureWindowsPath = pathlib.PureWindowsPath("") + self.ls_cache: dict[str:list] = dict() + + if cli: + self.loop(debug=debug) + + def ps1(self) -> str: + return f"[reg] {self.current_root_path}\\{self.current_subkey_path} > " + + @CLIUtil.addcommand() + def close(self) -> NoReturn: + """ + Close all connections + """ + + print("Connection closed") + self.client.close() + + @CLIUtil.addcommand() + def use(self, root_path): + """ + Use base key (HKLM, HKCU, etc.) + """ + if root_path.upper().startswith(HKEY_CLASSES_ROOT): + # Change to HKLM root + self.current_root_handle = self.root_handle.setdefault( + HKEY_CLASSES_ROOT, + self.client.sr1_req( + OpenClassesRoot_Request( + ServerName=None, + samDesired=KEY_QUERY_VALUE | KEY_ENUMERATE_SUB_KEYS, + ), + timeout=10, + ).phKey, + ) + self.current_root_path = HKEY_CLASSES_ROOT + + if root_path.upper().startswith(HKEY_LOCAL_MACHINE): + # Change to HKLM root + self.current_root_handle = self.root_handle.setdefault( + HKEY_LOCAL_MACHINE, + self.client.sr1_req( + OpenLocalMachine_Request( + ServerName=None, + samDesired=KEY_QUERY_VALUE | KEY_ENUMERATE_SUB_KEYS, + ), + timeout=6, + ).phKey, + ) + self.current_root_path = HKEY_LOCAL_MACHINE + + if root_path.upper().startswith(HKEY_CURRENT_USER): + # Change to HKLM root + self.current_root_handle = self.root_handle.setdefault( + HKEY_CURRENT_USER, + self.client.sr1_req( + OpenCurrentUser_Request( + ServerName=None, + samDesired=KEY_QUERY_VALUE | KEY_ENUMERATE_SUB_KEYS, + ), + timeout=4, + ).phKey, + ) + self.current_root_path = HKEY_CURRENT_USER + + self.ls_cache.clear() + self.cd("") + + @CLIUtil.addcommand() + def version(self): + """ + Get remote registry server version + """ + version = self.client.sr1_req( + BaseRegGetVersion_Request(hKey=self.current_root_handle) + ).lpdwVersion + print(f"Remote registry server version: {version}") + + @CLIUtil.addcommand(spaces=True) + def ls(self, folder: Optional[str] = None) -> list[str]: + """ + EnumKeys of the current subkey path + """ + # If no specific folder was specified + # we use our current subkey path + if folder is None or folder == "": + cache = self.ls_cache.get(self.current_subkey_path) + subkey_path = self.current_subkey_path + + # if the resolution was already performed, + # no need to query again the RPC + if cache: + return cache + + # The first time we do an ls we need to get + # a proper handle + if self.current_subkey_handle is None: + self.current_subkey_handle = self.get_handle_on_subkey( + PureWindowsPath("") + ) + + handle = self.current_subkey_handle + + # Otherwise we use the folder path, + # the calling parent shall make sure that this path was properly sanitized + else: + subkey_path = self._join_path(self.current_subkey_path, folder) + handle = self.get_handle_on_subkey(subkey_path) + if handle is None: + return [] + + self.ls_cache[subkey_path] = list() + idx = 0 + while True: + req = BaseRegEnumKey_Request( + hKey=handle, + dwIndex=idx, + lpNameIn=RPC_UNICODE_STRING(MaximumLength=1024), + lpClassIn=RPC_UNICODE_STRING(), + lpftLastWriteTime=None, + ) + + resp = self.client.sr1_req(req) + if resp.status == ERROR_NO_MORE_ITEMS: + break + elif resp.status: + print( + f"[-] Error : got status {hex(resp.status)} while enumerating keys" + ) + self.ls_cache.clear() + return [] + + self.ls_cache[subkey_path].append( + resp.lpNameOut.valueof("Buffer").decode("utf-8").strip("\x00") + ) + idx += 1 + + return self.ls_cache[subkey_path] + + @CLIUtil.addoutput(ls) + def ls_output(self, results: list[str]) -> NoReturn: + """ + Print the output of 'ls' + """ + for subkey in results: + print(subkey) + + @CLIUtil.addcomplete(ls) + def ls_complete(self, folder: str) -> list[str]: + """ + Auto-complete ls + """ + if self._require_root_handles(silent=True): + return [] + return [ + str(subk) + for subk in self.ls() + if str(subk).lower().startswith(folder.lower()) + ] + + def _require_root_handles(self, silent: bool = False) -> bool: + if self.current_root_handle is None: + if not silent: + print("No root key selected ! Use 'use' to use one.") + return True + + @CLIUtil.addcommand() + def dev(self) -> NoReturn: + """ + Joker function to jump into the python code for dev purpose + """ + breakpoint() + + @CLIUtil.addcommand(spaces=True) + def cd(self, subkey: str) -> NoReturn: + """ + Change current subkey path + """ + self.current_subkey_path = self._join_path(self.current_subkey_path, subkey) + self.current_subkey_handle = self.get_handle_on_subkey(self.current_subkey_path) + self.ls_cache.clear() + + @CLIUtil.addcomplete(cd) + def cd_complete(self, folder: str) -> list[str]: + """ + Auto-complete cd + """ + if self._require_root_handles(silent=True): + return [] + return [ + str(subk) + for subk in self.ls() + if str(subk).lower().startswith(folder.lower()) + ] + + def get_handle_on_subkey(self, subkey_path: PureWindowsPath) -> NDRContextHandle: + """ + Ask the remote server to return an handle on a given subkey + """ + if str(subkey_path) == ".": + subkey_path = "\x00" + else: + subkey_path = str(subkey_path) + "\x00" + + # print(f"getting handle on: {subkey_path}") + req = BaseRegOpenKey_Request( + hKey=self.current_root_handle, + lpSubKey=RPC_UNICODE_STRING(Buffer=subkey_path), + samDesired=KEY_QUERY_VALUE | KEY_ENUMERATE_SUB_KEYS, + ) + resp = self.client.sr1_req(req) + if resp.status == ERROR_SUBKEY_NOT_FOUND: + print(f"[-] Error : got status {hex(resp.status)} while enumerating keys") + return None + + return resp.phkResult + + def _join_path(self, first_path: str, second_path: str) -> PureWindowsPath: + return PureWindowsPath( + os.path.normpath( + os.path.join( + PureWindowsPath(first_path).as_posix(), + PureWindowsPath(second_path).as_posix(), + ) + ) + ) + + +def main(): + """ + Main entry point + """ + from scapy.utils import AutoArgparse + + AutoArgparse(regclient) + + +if __name__ == "__main__": + from scapy.utils import AutoArgparse + + AutoArgparse(regclient) From f8d475a70c37dc2e7481635cfd960b103e40795a Mon Sep 17 00:00:00 2001 From: Ebrix Date: Thu, 12 Jun 2025 09:06:20 +0200 Subject: [PATCH 02/27] register scapy-winreg as a cli pointing on winreg.py --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 477471b..84c1ccc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,3 +49,4 @@ scapy-listips = "scapyred.listips:main" scapy-ldaphero = "scapyred.ldaphero:main" scapy-smbclient = "scapyred.smbclient:main" scapy-smbscan = "scapyred.smbscan:main" +scapy-winreg = "scapyred.winreg:main" From 6b83e051532f74ed5dd6024085ec15207b5cc808 Mon Sep 17 00:00:00 2001 From: Ebrix Date: Thu, 3 Jul 2025 08:08:47 +0200 Subject: [PATCH 03/27] buggy cat and use autocomplete --- scapyred/winreg.py | 96 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/scapyred/winreg.py b/scapyred/winreg.py index 9becd63..1c5644d 100644 --- a/scapyred/winreg.py +++ b/scapyred/winreg.py @@ -49,12 +49,23 @@ HKEY_CLASSES_ROOT = "HKCROOT" # Registry entries subordinate to this key define types (or classes) of documents and the properties associated with those types. The subkeys of the HKEY_CLASSES_ROOT key are a merged view of the following two subkeys: HKEY_CURRENT_USER = "HKCU" # Registry entries subordinate to this key define the preferences of the current user. These preferences include the settings of environment variables, data on program groups, colors, printers, network connections, and application preferences. The HKEY_CURRENT_USER root key is a subkey of the HKEY_USERS root key, as described in section 3.1.1.8. HKEY_LOCAL_MACHINE = "HKLM" # Registry entries subordinate to this key define the physical state of the computer, including data on the bus type, system memory, and installed hardware and software. -HKEY_CURRENT_CONFIG = "" # This key contains information on the current hardware profile of the local computer. +HKEY_CURRENT_CONFIG = "HKC" # This key contains information on the current hardware profile of the local computer. HKEY_CURRENT_CONFIG is an alias for HKEY_LOCAL_MACHINE\System\CurrentControlSet\Hardware Profiles\Current HKEY_USERS = "HKU" HKEY_PERFORMANCE_DATA = "HKPERFORMANCE" # Registry entries subordinate to this key allow access to performance data. HKEY_PERFORMANCE_TEXT = "" # Registry entries subordinate to this key reference the text strings that describe counters in U.S. English. HKEY_PERFORMANCE_NLSTEXT = "" # Registry entries subordinate to this key reference the text strings that describe counters in the local language of the area in which the computer is running. +AVAILABLE_ROOT_KEYS: list[str] = [ + HKEY_LOCAL_MACHINE, + HKEY_CURRENT_USER, + HKEY_USERS, + HKEY_CLASSES_ROOT, + HKEY_CURRENT_CONFIG, + HKEY_PERFORMANCE_DATA, + HKEY_PERFORMANCE_TEXT, + HKEY_PERFORMANCE_NLSTEXT, +] + @conf.commands.register class regclient(CLIUtil): @@ -73,6 +84,8 @@ class regclient(CLIUtil): :param ST: if provided, the service ticket to use (Kerberos) :param KEY: if provided, the session key associated to the ticket (Kerberos) :param cli: CLI mode (default True). False to use for scripting + :param rootKey: the root key to get a handle to (HKLM, HKCU, etc.), in CLI mode you can chose it later + Some additional SMB parameters are available under help(SMB_Client). Some of them include the following: @@ -96,6 +109,7 @@ def __init__( ST=None, KEY=None, cli=True, + rootKey: str = None, # SMB arguments **kwargs, ): @@ -182,13 +196,17 @@ def __init__( self.client.connect(target) self.client.open_smbpipe("winreg") self.client.bind(self.interface) + self.ls_cache: dict[str:list] = dict() + self.cat_cache: dict[str:list] = dict() self.root_handle = {} self.current_root_handle = None - self.current_root_path = "CHOOSE ROOT KEY" self.current_subkey_handle = None self.current_subkey_path: PureWindowsPath = pathlib.PureWindowsPath("") - self.ls_cache: dict[str:list] = dict() - + if rootKey in AVAILABLE_ROOT_KEYS: + self.current_root_path = rootKey.strip() + self.use(self.current_root_path) + else: + self.current_root_path = "CHOOSE ROOT KEY" if cli: self.loop(debug=debug) @@ -209,6 +227,7 @@ def use(self, root_path): """ Use base key (HKLM, HKCU, etc.) """ + print(f">{root_path}<") if root_path.upper().startswith(HKEY_CLASSES_ROOT): # Change to HKLM root self.current_root_handle = self.root_handle.setdefault( @@ -254,6 +273,17 @@ def use(self, root_path): self.ls_cache.clear() self.cd("") + @CLIUtil.addcomplete(use) + def use_complete(self, root_key: str) -> list[str]: + """ + Auto complete root key for `use` + """ + return [ + str(rkey) + for rkey in AVAILABLE_ROOT_KEYS + if str(rkey).lower().startswith(root_key.lower()) + ] + @CLIUtil.addcommand() def version(self): """ @@ -346,6 +376,64 @@ def ls_complete(self, folder: str) -> list[str]: if str(subk).lower().startswith(folder.lower()) ] + @CLIUtil.addcommand(spaces=True) + def cat(self, folder: Optional[str] = None) -> list[str]: + # If no specific folder was specified + # we use our current subkey path + if folder is None or folder == "": + cache = self.cat_cache.get(self.current_subkey_path) + subkey_path = self.current_subkey_path + + # if the resolution was already performed, + # no need to query again the RPC + if cache: + return cache + + # The first time we do a cat we need to get + # a proper handle + if self.current_subkey_handle is None: + self.current_subkey_handle = self.get_handle_on_subkey( + PureWindowsPath("") + ) + + handle = self.current_subkey_handle + + # Otherwise we use the folder path, + # the calling parent shall make sure that this path was properly sanitized + else: + subkey_path = self._join_path(self.current_subkey_path, folder) + handle = self.get_handle_on_subkey(subkey_path) + if handle is None: + return [] + + idx = 0 + while True: + req = BaseRegEnumValue_Request( + hKey=handle, + dwIndex=idx, + lpValueNameIn=RPC_UNICODE_STRING(MaximumLength=1024), + lpData=b" " * 1024, + lpcbData=1024, + lpcbLen=1024, + ) + + resp = self.client.sr1_req(req) + if resp.status == ERROR_NO_MORE_ITEMS: + break + elif resp.status: + print( + f"[-] Error : got status {hex(resp.status)} while enumerating values" + ) + breakpoint() + self.cat_cache.clear() + return [] + + breakpoint() + self.ls_cache[subkey_path].append( + resp.lpNameOut.valueof("Buffer").decode("utf-8").strip("\x00") + ) + idx += 1 + def _require_root_handles(self, silent: bool = False) -> bool: if self.current_root_handle is None: if not silent: From bc59277fb025da76d8a589d4898887eaa91b4a4d Mon Sep 17 00:00:00 2001 From: Ebrix Date: Sun, 6 Jul 2025 18:03:34 +0200 Subject: [PATCH 04/27] Get info and get key security... almost --- scapyred/winreg.py | 164 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 157 insertions(+), 7 deletions(-) diff --git a/scapyred/winreg.py b/scapyred/winreg.py index 1c5644d..75730e5 100644 --- a/scapyred/winreg.py +++ b/scapyred/winreg.py @@ -2,6 +2,8 @@ DCE/RPC client """ +from ctypes.wintypes import PFILETIME +import re import socket import os import pathlib @@ -33,6 +35,7 @@ from scapy.layers.ntlm import MD4le, NTLMSSP from scapy.layers.spnego import SPNEGOSSP from scapy.layers.kerberos import KerberosSSP +from scapy.layers.smb2 import SECURITY_DESCRIPTOR from pathlib import PureWindowsPath @@ -41,9 +44,11 @@ KEY_QUERY_VALUE = 0x00000001 KEY_ENUMERATE_SUB_KEYS = 0x00000008 +STANDARD_RIGHTS_READ = 0x00020000 # Standard rights for read access MAX_ALLOWED = 0x02000000 ERROR_NO_MORE_ITEMS = 0x00000103 ERROR_SUBKEY_NOT_FOUND = 0x000006F7 +ERROR_INSUFFICIENT_BUFFER = 0x0000007A # Predefined keys HKEY_CLASSES_ROOT = "HKCROOT" # Registry entries subordinate to this key define types (or classes) of documents and the properties associated with those types. The subkeys of the HKEY_CLASSES_ROOT key are a merged view of the following two subkeys: @@ -67,6 +72,21 @@ ] +def from_filetime_to_datetime(lp_filetime: PFILETIME) -> str: + """ + Convert a filetime to a human readable date + """ + from datetime import datetime, timezone + + filetime = lp_filetime.dwLowDateTime + (lp_filetime.dwHighDateTime << 32) + # Filetime is in 100ns intervals since 1601-01-01 + # Convert to seconds since epoch + seconds = (filetime - 116444736000000000) // 10000000 + return datetime.fromtimestamp(seconds, tz=timezone.utc).strftime( + "%Y-%m-%d %H:%M:%S" + ) + + @conf.commands.register class regclient(CLIUtil): r""" @@ -225,9 +245,21 @@ def close(self) -> NoReturn: @CLIUtil.addcommand() def use(self, root_path): """ - Use base key (HKLM, HKCU, etc.) + Selects and sets the base registry key (root) to use for subsequent operations. + + Parameters: + root_path (str): The root registry path to use. Should start with one of the following: + - HKEY_CLASSES_ROOT + - HKEY_LOCAL_MACHINE + - HKEY_CURRENT_USER + + Behavior: + - Determines which registry root to use based on the prefix of `root_path`. + - Opens the corresponding registry root handle if not already opened, using the appropriate request. + - Sets `self.current_root_handle` and `self.current_root_path` to the selected root. + - Clears the local subkey cache (`self.ls_cache`). + - Changes the current directory to the root of the selected registry hive. """ - print(f">{root_path}<") if root_path.upper().startswith(HKEY_CLASSES_ROOT): # Change to HKLM root self.current_root_handle = self.root_handle.setdefault( @@ -235,7 +267,9 @@ def use(self, root_path): self.client.sr1_req( OpenClassesRoot_Request( ServerName=None, - samDesired=KEY_QUERY_VALUE | KEY_ENUMERATE_SUB_KEYS, + samDesired=KEY_QUERY_VALUE + | KEY_ENUMERATE_SUB_KEYS + | MAX_ALLOWED, ), timeout=10, ).phKey, @@ -412,9 +446,10 @@ def cat(self, folder: Optional[str] = None) -> list[str]: hKey=handle, dwIndex=idx, lpValueNameIn=RPC_UNICODE_STRING(MaximumLength=1024), - lpData=b" " * 1024, - lpcbData=1024, - lpcbLen=1024, + lpType=0, # pointer to type, set to 0 for query + lpData=b" " * 1024, # pointer to buffer + lpcbData=1024, # pointer to buffer size + lpcbLen=1024, # pointer to length ) resp = self.client.sr1_req(req) @@ -447,6 +482,119 @@ def dev(self) -> NoReturn: """ breakpoint() + @CLIUtil.addcommand() + def get_key_security(self, folder: Optional[str] = None) -> NoReturn: + """ + Get the security descriptor of the current subkey. SACL are not retrieve at this point (TODO). + + """ + if self._require_root_handles(silent=True): + return + + # If no specific folder was specified + # we use our current subkey path + if folder is None or folder == "": + subkey_path = self.current_subkey_path + handle = self.current_subkey_handle + + # Otherwise we use the folder path, + # the calling parent shall make sure that this path was properly sanitized + else: + subkey_path = self._join_path(self.current_subkey_path, folder) + handle = self.get_handle_on_subkey(subkey_path) + if handle is None: + return [] + + req = BaseRegGetKeySecurity_Request( + hKey=handle, + SecurityInformation=0x00000001 # OWNER_SECURITY_INFORMATION + | 0x00000002 # GROUP_SECURITY_INFORMATION + | 0x00000004, # DACL_SECURITY_INFORMATION + pRpcSecurityDescriptorIn=PRPC_SECURITY_DESCRIPTOR( + cbInSecurityDescriptor=512, # Initial size of the buffer + ), + ) + + resp = self.client.sr1_req(req) + if resp.status == ERROR_INSUFFICIENT_BUFFER: + # The buffer was too small, we need to retry with a larger one + req.pRpcSecurityDescriptorIn.cbInSecurityDescriptor = ( + resp.pRpcSecurityDescriptorOut.cbInSecurityDescriptor + ) + resp = self.client.sr1_req(req) + + if resp.status: + print(f"[-] Error : got status {hex(resp.status)} while getting security") + return + + results = resp.pRpcSecurityDescriptorOut.valueof("lpSecurityDescriptor") + sd = SECURITY_DESCRIPTOR(results) + print("Owner:", sd.OwnerSid.summary()) + print("Group:", sd.GroupSid.summary()) + if getattr(sd, "DACL", None): + print("DACL:") + for ace in sd.DACL.Aces: + print(" - ", ace.toSDDL()) + return sd + + @CLIUtil.addcommand() + def query_info(self, folder: Optional[str] = None) -> NoReturn: + """ + Query information on the current subkey + """ + if self._require_root_handles(silent=True): + return + + # If no specific folder was specified + # we use our current subkey path + if folder is None or folder == "": + cache = self.ls_cache.get(self.current_subkey_path) + subkey_path = self.current_subkey_path + + # if the resolution was already performed, + # no need to query again the RPC + if cache: + return cache + + # The first time we do an ls we need to get + # a proper handle + if self.current_subkey_handle is None: + self.current_subkey_handle = self.get_handle_on_subkey( + PureWindowsPath("") + ) + + handle = self.current_subkey_handle + + # Otherwise we use the folder path, + # the calling parent shall make sure that this path was properly sanitized + else: + subkey_path = self._join_path(self.current_subkey_path, folder) + handle = self.get_handle_on_subkey(subkey_path) + if handle is None: + return [] + + req = BaseRegQueryInfoKey_Request( + hKey=handle, + lpClassIn=RPC_UNICODE_STRING(), # pointer to class name + ) + + resp = self.client.sr1_req(req) + if resp.status: + print(f"[-] Error : got status {hex(resp.status)} while querying info") + return + + print(f"Info on key: {self.current_subkey_path}") + print(f"- Number of subkeys: {resp.lpcSubKeys}") + print( + f"- Length of the longuest subkey name (in bytes): {resp.lpcbMaxSubKeyLen}" + ) + print(f"- Number of values: {resp.lpcValues}") + print( + f"- Length of the longest value name (in bytes): {resp.lpcbMaxValueNameLen}" + ) + print(f"- Last write time: {from_filetime_to_datetime(resp.lpftLastWriteTime)}") + resp.show() + @CLIUtil.addcommand(spaces=True) def cd(self, subkey: str) -> NoReturn: """ @@ -482,7 +630,9 @@ def get_handle_on_subkey(self, subkey_path: PureWindowsPath) -> NDRContextHandle req = BaseRegOpenKey_Request( hKey=self.current_root_handle, lpSubKey=RPC_UNICODE_STRING(Buffer=subkey_path), - samDesired=KEY_QUERY_VALUE | KEY_ENUMERATE_SUB_KEYS, + samDesired=KEY_QUERY_VALUE + | KEY_ENUMERATE_SUB_KEYS + | STANDARD_RIGHTS_READ, # | MAX_ALLOWED, ) resp = self.client.sr1_req(req) if resp.status == ERROR_SUBKEY_NOT_FOUND: From 966ad537b6d060aad4f4bd8edd41882edb25d252 Mon Sep 17 00:00:00 2001 From: Ebrix Date: Tue, 8 Jul 2025 08:07:33 +0200 Subject: [PATCH 05/27] working cat... almost --- scapyred/winreg.py | 167 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 149 insertions(+), 18 deletions(-) diff --git a/scapyred/winreg.py b/scapyred/winreg.py index 75730e5..a49bbc2 100644 --- a/scapyred/winreg.py +++ b/scapyred/winreg.py @@ -2,17 +2,23 @@ DCE/RPC client """ -from ctypes.wintypes import PFILETIME -import re +from asyncio import timeout import socket import os -import pathlib + +from ctypes.wintypes import PFILETIME from typing import Optional, NoReturn +from pathlib import PureWindowsPath from scapy.layers.msrpce.all import * from scapy.layers.msrpce.raw.ms_samr import * from scapy.layers.msrpce.raw.ms_rrp import * -from scapy.layers.dcerpc import RPC_C_AUTHN_LEVEL +from scapy.layers.dcerpc import ( + RPC_C_AUTHN_LEVEL, + NDRConformantArray, + NDRPointer, + NDRVaryingArray, +) from scapy.utils import ( CLIUtil, pretty_list, @@ -29,15 +35,12 @@ from scapy.themes import DefaultTheme from scapy.base_classes import Net from scapy.utils6 import Net6 - from scapy.layers.msrpce.rpcclient import DCERPC_Client from scapy.layers.dcerpc import find_dcerpc_interface, DCERPC_Transport from scapy.layers.ntlm import MD4le, NTLMSSP from scapy.layers.spnego import SPNEGOSSP -from scapy.layers.kerberos import KerberosSSP from scapy.layers.smb2 import SECURITY_DESCRIPTOR -from pathlib import PureWindowsPath conf.color_theme = DefaultTheme() @@ -49,6 +52,7 @@ ERROR_NO_MORE_ITEMS = 0x00000103 ERROR_SUBKEY_NOT_FOUND = 0x000006F7 ERROR_INSUFFICIENT_BUFFER = 0x0000007A +ERROR_MORE_DATA = 0x000000EA # Predefined keys HKEY_CLASSES_ROOT = "HKCROOT" # Registry entries subordinate to this key define types (or classes) of documents and the properties associated with those types. The subkeys of the HKEY_CLASSES_ROOT key are a merged view of the following two subkeys: @@ -71,6 +75,17 @@ HKEY_PERFORMANCE_NLSTEXT, ] +REG_TYPE = { + 1: "REG_SZ", # Unicode string + 2: "REG_EXPAND_SZ", # Unicode string with environment variable expansion + 3: "REG_BINARY", # Binary data + 4: "REG_DWORD", # 32-bit unsigned integer + 5: "REG_DWORD_BIG_ENDIAN", # 32-bit unsigned integer in big-endian format + 6: "REG_LINK", # Symbolic link + 7: "REG_MULTI_SZ", # Multiple Unicode strings + 11: "REG_QWORD", # 64-bit unsigned integer +} + def from_filetime_to_datetime(lp_filetime: PFILETIME) -> str: """ @@ -87,6 +102,62 @@ def from_filetime_to_datetime(lp_filetime: PFILETIME) -> str: ) +class RegEntry: + """ + A registry entry + """ + + def __init__(self, reg_value: str, reg_type: int, reg_data: bytes): + """ + Initialize the RegEntry + :param reg_value: the name of the registry value (str) + :param reg_type: the type of the registry value (int) + :param reg_data: the data of the registry value (str) + """ + self.reg_value = reg_value + self.reg_type = reg_type + + if self.reg_type == 7 or self.reg_type == 1 or self.reg_type == 2: + if self.reg_type == 7: + # decode multiple null terminated strings + self.reg_data = reg_data.decode("utf-16le")[:-2].replace("\x00", "\n") + else: + # REG_MULTI_SZ, REG_SZ, REG_EXPAND_SZ + # Decode the data as a string + self.reg_data = reg_data.decode("utf-16le") + + elif self.reg_type == 3: + # REG_BINARY + # Decode the data as a bytes object + self.reg_data = reg_data + + elif self.reg_type == 4 or self.reg_type == 11: + # REG_DWORD, REG_QWORD + # Decode the data as an integer + self.reg_data = int.from_bytes(reg_data, byteorder="little") + + elif self.reg_type == 5: + # REG_DWORD_BIG_ENDIAN + # Decode the data as an integer in big-endian format + self.reg_data = int.from_bytes(reg_data, byteorder="big") + + elif self.reg_type == 6: + # REG_LINK + # Decode the data as a string (symbolic link) + self.reg_data = reg_data.decode("utf-16le") + + else: + self.reg_data = reg_data + + def __str__(self) -> str: + return ( + f"{self.reg_value} ({REG_TYPE.get(self.reg_type, "UNK")}) {self.reg_data} " + ) + + def __repr__(self) -> str: + return f"RegEntry(reg_value={self.reg_value}, reg_type={self.reg_type}, reg_data={self.reg_data})" + + @conf.commands.register class regclient(CLIUtil): r""" @@ -221,7 +292,7 @@ def __init__( self.root_handle = {} self.current_root_handle = None self.current_subkey_handle = None - self.current_subkey_path: PureWindowsPath = pathlib.PureWindowsPath("") + self.current_subkey_path: PureWindowsPath = PureWindowsPath("") if rootKey in AVAILABLE_ROOT_KEYS: self.current_root_path = rootKey.strip() self.use(self.current_root_path) @@ -411,7 +482,7 @@ def ls_complete(self, folder: str) -> list[str]: ] @CLIUtil.addcommand(spaces=True) - def cat(self, folder: Optional[str] = None) -> list[str]: + def cat(self, folder: Optional[str] = None) -> list[tuple[str, str]]: # If no specific folder was specified # we use our current subkey path if folder is None or folder == "": @@ -441,15 +512,23 @@ def cat(self, folder: Optional[str] = None) -> list[str]: return [] idx = 0 + results = [] while True: req = BaseRegEnumValue_Request( hKey=handle, dwIndex=idx, - lpValueNameIn=RPC_UNICODE_STRING(MaximumLength=1024), + lpValueNameIn=RPC_UNICODE_STRING( + MaximumLength=2048, + Buffer=NDRPointer( + value=NDRConformantArray( + max_count=1024, value=NDRVaryingArray(value=b"") + ) + ), + ), lpType=0, # pointer to type, set to 0 for query - lpData=b" " * 1024, # pointer to buffer - lpcbData=1024, # pointer to buffer size - lpcbLen=1024, # pointer to length + lpData=None, # pointer to buffer + lpcbData=0, # pointer to buffer size + lpcbLen=0, # pointer to length ) resp = self.client.sr1_req(req) @@ -459,16 +538,69 @@ def cat(self, folder: Optional[str] = None) -> list[str]: print( f"[-] Error : got status {hex(resp.status)} while enumerating values" ) - breakpoint() self.cat_cache.clear() return [] + # resp.show() - breakpoint() - self.ls_cache[subkey_path].append( - resp.lpNameOut.valueof("Buffer").decode("utf-8").strip("\x00") + req = BaseRegQueryValue_Request( + hKey=handle, + lpValueName=resp.valueof("lpValueNameOut"), + lpType=0, + lpcbData=1024, + lpcbLen=0, + lpData=NDRPointer( + value=NDRConformantArray( + max_count=1024, value=NDRVaryingArray(actual_count=0, value=b"") + ) + ), + ) + # req.show() + resp2 = self.client.sr1_req(req) + if resp2.status == ERROR_MORE_DATA: + # The buffer was too small, we need to retry with a larger one + req.lpcbData = resp2.lpcbData + req.lpData.value.max_count = resp2.lpcbData.value + return results + resp2 = self.client.sr1_req(req, timeout=1) + + if resp2.status: + print( + f"[-] Error : got status {hex(resp2.status)} while querying value" + ) + return [] + + value = ( + resp.valueof("lpValueNameOut").valueof("Buffer").decode("utf-8").strip() ) + results.append( + RegEntry( + reg_value=value, + reg_type=resp2.valueof("lpType"), + reg_data=resp2.valueof("lpData"), + ) + ) + + # self.cat_cache[subkey_path].append( + # resp.valueof("lpValueNameOut").valueof("Buffer").decode("utf-8").strip() + # ) idx += 1 + return results + + @CLIUtil.addoutput(cat) + def cat_output(self, results: list[RegEntry]) -> NoReturn: + """ + Print the output of 'cat' + """ + if not results or len(results) == 0: + print("No values found.") + return + + for entry in results: + print( + f" - {entry.reg_value:<20} {'(' + REG_TYPE.get(entry.reg_type, "UNK") + ')':<15} {entry.reg_data}" + ) + def _require_root_handles(self, silent: bool = False) -> bool: if self.current_root_handle is None: if not silent: @@ -593,7 +725,6 @@ def query_info(self, folder: Optional[str] = None) -> NoReturn: f"- Length of the longest value name (in bytes): {resp.lpcbMaxValueNameLen}" ) print(f"- Last write time: {from_filetime_to_datetime(resp.lpftLastWriteTime)}") - resp.show() @CLIUtil.addcommand(spaces=True) def cd(self, subkey: str) -> NoReturn: From a166df67e047daab66ab5582c30edf68b97d5912 Mon Sep 17 00:00:00 2001 From: Ebrix Date: Thu, 17 Jul 2025 09:22:24 +0200 Subject: [PATCH 06/27] Clean up and some reorganisation --- .gitignore | 1 + scapyred/winreg.py | 730 +++++++++++++++++++++++++++++++-------------- 2 files changed, 513 insertions(+), 218 deletions(-) diff --git a/.gitignore b/.gitignore index 8d65e4c..6447680 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.pyc *.pyo +*.log dist/ build/ MANIFEST diff --git a/scapyred/winreg.py b/scapyred/winreg.py index a49bbc2..db7a03c 100644 --- a/scapyred/winreg.py +++ b/scapyred/winreg.py @@ -2,17 +2,37 @@ DCE/RPC client """ -from asyncio import timeout import socket import os +import logging + +from enum import IntEnum, IntFlag, StrEnum from ctypes.wintypes import PFILETIME from typing import Optional, NoReturn from pathlib import PureWindowsPath -from scapy.layers.msrpce.all import * -from scapy.layers.msrpce.raw.ms_samr import * -from scapy.layers.msrpce.raw.ms_rrp import * +# pylint: disable-next=import-error, no-name-in-module +from scapy.layers.msrpce.raw.ms_rrp import ( + OpenClassesRoot_Request, + OpenLocalMachine_Request, + OpenCurrentUser_Request, + OpenUsers_Request, + OpenCurrentConfig_Request, + OpenPerformanceData_Request, + OpenPerformanceText_Request, + OpenPerformanceNlsText_Request, + BaseRegOpenKey_Request, + BaseRegEnumKey_Request, + BaseRegEnumValue_Request, + BaseRegQueryValue_Request, + BaseRegGetVersion_Request, + BaseRegQueryInfoKey_Request, + BaseRegGetKeySecurity_Request, + PRPC_SECURITY_DESCRIPTOR, + NDRContextHandle, + RPC_UNICODE_STRING, +) from scapy.layers.dcerpc import ( RPC_C_AUTHN_LEVEL, NDRConformantArray, @@ -21,8 +41,6 @@ ) from scapy.utils import ( CLIUtil, - pretty_list, - human_size, valid_ip, valid_ip6, ) @@ -43,48 +61,131 @@ conf.color_theme = DefaultTheme() +logging.basicConfig( + level=logging.INFO, + format="[%(levelname)s][%(funcName)s] %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + filename="winreg.log", # write logs here + filemode="w", +) +logger = logging.getLogger(__name__) + +class AccessRights(IntFlag): + """ + Access rights for registry keys + """ -KEY_QUERY_VALUE = 0x00000001 -KEY_ENUMERATE_SUB_KEYS = 0x00000008 -STANDARD_RIGHTS_READ = 0x00020000 # Standard rights for read access -MAX_ALLOWED = 0x02000000 -ERROR_NO_MORE_ITEMS = 0x00000103 -ERROR_SUBKEY_NOT_FOUND = 0x000006F7 -ERROR_INSUFFICIENT_BUFFER = 0x0000007A -ERROR_MORE_DATA = 0x000000EA - -# Predefined keys -HKEY_CLASSES_ROOT = "HKCROOT" # Registry entries subordinate to this key define types (or classes) of documents and the properties associated with those types. The subkeys of the HKEY_CLASSES_ROOT key are a merged view of the following two subkeys: -HKEY_CURRENT_USER = "HKCU" # Registry entries subordinate to this key define the preferences of the current user. These preferences include the settings of environment variables, data on program groups, colors, printers, network connections, and application preferences. The HKEY_CURRENT_USER root key is a subkey of the HKEY_USERS root key, as described in section 3.1.1.8. -HKEY_LOCAL_MACHINE = "HKLM" # Registry entries subordinate to this key define the physical state of the computer, including data on the bus type, system memory, and installed hardware and software. -HKEY_CURRENT_CONFIG = "HKC" # This key contains information on the current hardware profile of the local computer. HKEY_CURRENT_CONFIG is an alias for HKEY_LOCAL_MACHINE\System\CurrentControlSet\Hardware Profiles\Current -HKEY_USERS = "HKU" -HKEY_PERFORMANCE_DATA = "HKPERFORMANCE" # Registry entries subordinate to this key allow access to performance data. -HKEY_PERFORMANCE_TEXT = "" # Registry entries subordinate to this key reference the text strings that describe counters in U.S. English. -HKEY_PERFORMANCE_NLSTEXT = "" # Registry entries subordinate to this key reference the text strings that describe counters in the local language of the area in which the computer is running. + # Access rights for registry keys + # These constants are used to specify the access rights when opening or creating registry keys. + KEY_QUERY_VALUE = 0x00000001 + KEY_ENUMERATE_SUB_KEYS = 0x00000008 + STANDARD_RIGHTS_READ = 0x00020000 + MAX_ALLOWED = 0x02000000 -AVAILABLE_ROOT_KEYS: list[str] = [ - HKEY_LOCAL_MACHINE, - HKEY_CURRENT_USER, - HKEY_USERS, - HKEY_CLASSES_ROOT, - HKEY_CURRENT_CONFIG, - HKEY_PERFORMANCE_DATA, - HKEY_PERFORMANCE_TEXT, - HKEY_PERFORMANCE_NLSTEXT, -] -REG_TYPE = { - 1: "REG_SZ", # Unicode string - 2: "REG_EXPAND_SZ", # Unicode string with environment variable expansion - 3: "REG_BINARY", # Binary data - 4: "REG_DWORD", # 32-bit unsigned integer - 5: "REG_DWORD_BIG_ENDIAN", # 32-bit unsigned integer in big-endian format - 6: "REG_LINK", # Symbolic link - 7: "REG_MULTI_SZ", # Multiple Unicode strings - 11: "REG_QWORD", # 64-bit unsigned integer -} +class ErrorCodes(IntEnum): + """ + Error codes for registry operations + """ + + ERROR_SUCCESS = 0x00000000 + ERROR_ACCESS_DENIED = 0x00000005 + ERROR_FILE_NOT_FOUND = 0x00000002 + ERROR_INVALID_HANDLE = 0x00000006 + ERROR_NOT_SAME_DEVICE = 0x00000011 + ERROR_WRITE_PROTECT = 0x00000013 + ERROR_INVALID_PARAMETER = 0x00000057 + ERROR_CALL_NOT_IMPLEMENTED = 0x00000057 + ERROR_NO_MORE_ITEMS = 0x00000103 + ERROR_NOACCESS = 0x000003E6 + ERROR_SUBKEY_NOT_FOUND = 0x000006F7 + ERROR_INSUFFICIENT_BUFFER = 0x0000007A + ERROR_MORE_DATA = 0x000000EA + + def __str__(self) -> str: + """ + Return the string representation of the error code. + :return: The string representation of the error code. + """ + return self.name + + +class RootKeys(StrEnum): + """ + Root keys for the Windows registry + """ + + # Registry root keys + # These constants are used to specify the root keys of the Windows registry. + # The root keys are the top-level keys in the registry hierarchy. + + # Registry entries subordinate to this key define types (or classes) of documents and the + # properties associated with those types. + # The subkeys of the HKEY_CLASSES_ROOT key are a merged view of the following two subkeys: + HKEY_CLASSES_ROOT = "HKCROOT" + + # Registry entries subordinate to this key define the preferences of the current user. + # These preferences include the settings of environment variables, data on program groups, + # colors, printers, network connections, and application preferences. + # The HKEY_CURRENT_USER root key is a subkey of the HKEY_USERS root key, as described in + # section 3.1.1.8. + HKEY_CURRENT_USER = "HKCU" + + # Registry entries subordinate to this key define the physical state of the computer, + # including data on the bus type, system memory, and installed hardware and software. + HKEY_LOCAL_MACHINE = "HKLM" + + # This key contains information on the current hardware profile of the local computer. + # HKEY_CURRENT_CONFIG is an alias for + # HKEY_LOCAL_MACHINE\System\CurrentControlSet\Hardware Profiles\Current + HKEY_CURRENT_CONFIG = "HKC" + + # This key define the default user configuration for new users on the local computer and the + # user configuration for the current user. + HKEY_USERS = "HKU" + + # Registry entries subordinate to this key allow access to performance data. + HKEY_PERFORMANCE_DATA = "HKPERFDATA" + + # Registry entries subordinate to this key reference the text strings that describe counters + # in U.S. English. + HKEY_PERFORMANCE_TEXT = "HKPERFTXT" + + # Registry entries subordinate to this key reference the text strings that describe + # counters in the local language of the area in which the computer is running. + HKEY_PERFORMANCE_NLSTEXT = "HKPERFNLSTXT" + + def __new__(cls, value): + # 1. Strip and uppercase the raw input + normalized = value.strip().upper() + # 2. Create the enum member with the normalized value + obj = str.__new__(cls, normalized) + obj._value_ = normalized + return obj + + +class RegType(IntEnum): + """ + Registry value types + """ + + # Registry value types + # These constants are used to specify the type of a registry value. + REG_SZ = 1 # Unicode string + REG_EXPAND_SZ = 2 # Unicode string with environment variable expansion + REG_BINARY = 3 # Binary data + REG_DWORD = 4 # 32-bit unsigned integer + REG_DWORD_BIG_ENDIAN = 5 # 32-bit unsigned integer in big-endian format + REG_LINK = 6 # Symbolic link + REG_MULTI_SZ = 7 # Multiple Unicode strings + REG_QWORD = 11 # 64-bit unsigned integer + UNK = 99999 # fallback default + + @classmethod + def _missing_(cls, value): + print(f"Unknown registry type: {value}, using UNK") + return cls.UNK def from_filetime_to_datetime(lp_filetime: PFILETIME) -> str: @@ -102,64 +203,91 @@ def from_filetime_to_datetime(lp_filetime: PFILETIME) -> str: ) -class RegEntry: +def is_status_ok(status: int) -> bool: """ - A registry entry + Check the error code and raise an exception if it is not successful. + :param status: The error code to check. """ + try: + err = ErrorCodes(status) + if err not in [ + ErrorCodes.ERROR_SUCCESS, + ErrorCodes.ERROR_NO_MORE_ITEMS, + ErrorCodes.ERROR_MORE_DATA, + ]: + print(f"[!] Error: {hex(err.value)} - {ErrorCodes(status).name}") + breakpoint() + return False + return True + except ValueError as exc: + print(f"[!] Error: {hex(status)} - Unknown error code") + breakpoint() + raise ValueError(f"Error: {hex(status)} - Unknown error code") from exc + + +AVAILABLE_ROOT_KEYS: list[str] = [ + RootKeys.HKEY_LOCAL_MACHINE, + RootKeys.HKEY_CURRENT_USER, + RootKeys.HKEY_USERS, + RootKeys.HKEY_CLASSES_ROOT, + RootKeys.HKEY_CURRENT_CONFIG, + RootKeys.HKEY_PERFORMANCE_DATA, + RootKeys.HKEY_PERFORMANCE_TEXT, + RootKeys.HKEY_PERFORMANCE_NLSTEXT, +] + + +class RegEntry: + """ + RegEntry to properly parse the data based on the type. - def __init__(self, reg_value: str, reg_type: int, reg_data: bytes): - """ - Initialize the RegEntry :param reg_value: the name of the registry value (str) :param reg_type: the type of the registry value (int) :param reg_data: the data of the registry value (str) - """ - self.reg_value = reg_value - self.reg_type = reg_type + """ - if self.reg_type == 7 or self.reg_type == 1 or self.reg_type == 2: - if self.reg_type == 7: + def __init__(self, reg_value: str, reg_type: int, reg_data: bytes): + self.reg_value = reg_value + try: + self.reg_type = RegType(reg_type) + except ValueError: + self.reg_type = RegType.UNK + + if ( + self.reg_type == RegType.REG_MULTI_SZ + or self.reg_type == RegType.REG_SZ + or self.reg_type == RegType.REG_EXPAND_SZ + ): + if self.reg_type == RegType.REG_MULTI_SZ: # decode multiple null terminated strings self.reg_data = reg_data.decode("utf-16le")[:-2].replace("\x00", "\n") else: - # REG_MULTI_SZ, REG_SZ, REG_EXPAND_SZ - # Decode the data as a string self.reg_data = reg_data.decode("utf-16le") - elif self.reg_type == 3: - # REG_BINARY - # Decode the data as a bytes object + elif self.reg_type == RegType.REG_BINARY: self.reg_data = reg_data - elif self.reg_type == 4 or self.reg_type == 11: - # REG_DWORD, REG_QWORD - # Decode the data as an integer + elif self.reg_type == RegType.REG_DWORD or self.reg_type == RegType.REG_QWORD: self.reg_data = int.from_bytes(reg_data, byteorder="little") - elif self.reg_type == 5: - # REG_DWORD_BIG_ENDIAN - # Decode the data as an integer in big-endian format + elif self.reg_type == RegType.REG_DWORD_BIG_ENDIAN: self.reg_data = int.from_bytes(reg_data, byteorder="big") - elif self.reg_type == 6: - # REG_LINK - # Decode the data as a string (symbolic link) + elif self.reg_type == RegType.REG_LINK: self.reg_data = reg_data.decode("utf-16le") else: self.reg_data = reg_data def __str__(self) -> str: - return ( - f"{self.reg_value} ({REG_TYPE.get(self.reg_type, "UNK")}) {self.reg_data} " - ) + return f"{self.reg_value} ({self.reg_type.name}) {self.reg_data}" def __repr__(self) -> str: - return f"RegEntry(reg_value={self.reg_value}, reg_type={self.reg_type}, reg_data={self.reg_data})" + return f"RegEntry({self.reg_value}, {self.reg_type}, {self.reg_data})" @conf.commands.register -class regclient(CLIUtil): +class RegClient(CLIUtil): r""" A simple registry CLI @@ -282,7 +410,19 @@ def __init__( ssp=ssp, ndr64=False, ) + if debug: + logger.setLevel(logging.DEBUG) + logger.debug( + "Connecting to %s:%d with UPN=%s, guest=%s, kerberos=%s, kerberos_required=%s", + target, + port, + UPN, + guest, + kerberos, + kerberos_required, + ) + self.timeout = timeout self.client.verb = False self.client.connect(target) self.client.open_smbpipe("winreg") @@ -313,6 +453,10 @@ def close(self) -> NoReturn: print("Connection closed") self.client.close() + # --------------------------------------------- # + # Use Root Key + # --------------------------------------------- # + @CLIUtil.addcommand() def use(self, root_path): """ @@ -331,50 +475,124 @@ def use(self, root_path): - Clears the local subkey cache (`self.ls_cache`). - Changes the current directory to the root of the selected registry hive. """ - if root_path.upper().startswith(HKEY_CLASSES_ROOT): - # Change to HKLM root - self.current_root_handle = self.root_handle.setdefault( - HKEY_CLASSES_ROOT, - self.client.sr1_req( - OpenClassesRoot_Request( - ServerName=None, - samDesired=KEY_QUERY_VALUE - | KEY_ENUMERATE_SUB_KEYS - | MAX_ALLOWED, - ), - timeout=10, - ).phKey, - ) - self.current_root_path = HKEY_CLASSES_ROOT - - if root_path.upper().startswith(HKEY_LOCAL_MACHINE): - # Change to HKLM root - self.current_root_handle = self.root_handle.setdefault( - HKEY_LOCAL_MACHINE, - self.client.sr1_req( - OpenLocalMachine_Request( - ServerName=None, - samDesired=KEY_QUERY_VALUE | KEY_ENUMERATE_SUB_KEYS, - ), - timeout=6, - ).phKey, - ) - self.current_root_path = HKEY_LOCAL_MACHINE - - if root_path.upper().startswith(HKEY_CURRENT_USER): - # Change to HKLM root - self.current_root_handle = self.root_handle.setdefault( - HKEY_CURRENT_USER, - self.client.sr1_req( - OpenCurrentUser_Request( - ServerName=None, - samDesired=KEY_QUERY_VALUE | KEY_ENUMERATE_SUB_KEYS, - ), - timeout=4, - ).phKey, - ) - self.current_root_path = HKEY_CURRENT_USER + default_read_access_rights = ( + AccessRights.KEY_QUERY_VALUE + | AccessRights.KEY_ENUMERATE_SUB_KEYS + | AccessRights.STANDARD_RIGHTS_READ + ) + root_path = RootKeys(root_path) + + match root_path: + case RootKeys.HKEY_CLASSES_ROOT: + # Change to HKCR root + self.current_root_handle = self.root_handle.setdefault( + RootKeys.HKEY_CLASSES_ROOT.value, + self.client.sr1_req( + OpenClassesRoot_Request( + ServerName=None, samDesired=default_read_access_rights + ), + timeout=self.timeout, + ).phKey, + ) + + case RootKeys.HKEY_CURRENT_USER: + # Change to HKCU root + self.current_root_handle = self.root_handle.setdefault( + RootKeys.HKEY_CURRENT_USER.value, + self.client.sr1_req( + OpenCurrentUser_Request( + ServerName=None, + samDesired=default_read_access_rights, + ), + timeout=self.timeout, + ).phKey, + ) + + case RootKeys.HKEY_LOCAL_MACHINE: + # Change to HKLM root + self.current_root_handle = self.root_handle.setdefault( + RootKeys.HKEY_LOCAL_MACHINE.value, + self.client.sr1_req( + OpenLocalMachine_Request( + ServerName=None, + samDesired=default_read_access_rights, + ), + timeout=self.timeout, + ).phKey, + ) + + case RootKeys.HKEY_CURRENT_CONFIG: + # Change to HKCU root + self.current_root_handle = self.root_handle.setdefault( + RootKeys.HKEY_CURRENT_CONFIG.value, + self.client.sr1_req( + OpenCurrentConfig_Request( + ServerName=None, + samDesired=default_read_access_rights, + ), + timeout=self.timeout, + ).phKey, + ) + + case RootKeys.HKEY_USERS: + self.current_root_handle = self.root_handle.setdefault( + RootKeys.HKEY_USERS.value, + self.client.sr1_req( + OpenUsers_Request( + ServerName=None, + samDesired=default_read_access_rights, + ), + timeout=self.timeout, + ).phKey, + ) + + case RootKeys.HKEY_PERFORMANCE_DATA: + self.current_root_handle = self.root_handle.setdefault( + RootKeys.HKEY_PERFORMANCE_DATA.value, + self.client.sr1_req( + OpenPerformanceData_Request( + ServerName=None, + samDesired=default_read_access_rights, + ), + timeout=self.timeout, + ).phKey, + ) + + case RootKeys.HKEY_PERFORMANCE_TEXT: + self.current_root_handle = self.root_handle.setdefault( + RootKeys.HKEY_PERFORMANCE_TEXT.value, + self.client.sr1_req( + OpenPerformanceText_Request( + ServerName=None, + samDesired=default_read_access_rights, + ), + timeout=self.timeout, + ).phKey, + ) + + case RootKeys.HKEY_PERFORMANCE_NLSTEXT: + self.current_root_handle = self.root_handle.setdefault( + RootKeys.HKEY_PERFORMANCE_NLSTEXT.value, + self.client.sr1_req( + OpenPerformanceNlsText_Request( + ServerName=None, + samDesired=default_read_access_rights, + ), + timeout=self.timeout, + ).phKey, + ) + + case _: + # If the root key is not recognized, raise an error + print(f"Unknown root key: {root_path}") + self.ls_cache.clear() + self.current_root_handle = None + self.current_root_path = "CHOOSE ROOT KEY" + self.cd("") + return + + self.current_root_path = root_path.value self.ls_cache.clear() self.cd("") @@ -389,16 +607,9 @@ def use_complete(self, root_key: str) -> list[str]: if str(rkey).lower().startswith(root_key.lower()) ] - @CLIUtil.addcommand() - def version(self): - """ - Get remote registry server version - """ - version = self.client.sr1_req( - BaseRegGetVersion_Request(hKey=self.current_root_handle) - ).lpdwVersion - print(f"Remote registry server version: {version}") - + # --------------------------------------------- # + # List and Cat + # --------------------------------------------- # @CLIUtil.addcommand(spaces=True) def ls(self, folder: Optional[str] = None) -> list[str]: """ @@ -444,9 +655,9 @@ def ls(self, folder: Optional[str] = None) -> list[str]: ) resp = self.client.sr1_req(req) - if resp.status == ERROR_NO_MORE_ITEMS: + if resp.status == ErrorCodes.ERROR_NO_MORE_ITEMS: break - elif resp.status: + elif not is_status_ok(resp.status): print( f"[-] Error : got status {hex(resp.status)} while enumerating keys" ) @@ -483,6 +694,23 @@ def ls_complete(self, folder: str) -> list[str]: @CLIUtil.addcommand(spaces=True) def cat(self, folder: Optional[str] = None) -> list[tuple[str, str]]: + """ + Enumerates and retrieves registry values for a given subkey path. + + If no folder is specified, uses the current subkey path and caches results to avoid redundant RPC queries. + Otherwise, enumerates values under the specified folder path. + + Args: + folder (Optional[str]): The subkey path to enumerate. If None or empty, uses the current subkey path. + + Returns: + list[tuple[str, str]]: A list of registry entries (as RegEntry objects) for the specified subkey path. + Returns an empty list if the handle is invalid or an error occurs during enumeration. + + Side Effects: + - May print error messages to standard output if RPC queries fail. + - Updates internal cache for previously enumerated subkey paths. + """ # If no specific folder was specified # we use our current subkey path if folder is None or folder == "": @@ -532,15 +760,14 @@ def cat(self, folder: Optional[str] = None) -> list[tuple[str, str]]: ) resp = self.client.sr1_req(req) - if resp.status == ERROR_NO_MORE_ITEMS: + if resp.status == ErrorCodes.ERROR_NO_MORE_ITEMS: break - elif resp.status: + elif not is_status_ok(resp.status): print( f"[-] Error : got status {hex(resp.status)} while enumerating values" ) self.cat_cache.clear() return [] - # resp.show() req = BaseRegQueryValue_Request( hKey=handle, @@ -554,14 +781,22 @@ def cat(self, folder: Optional[str] = None) -> list[tuple[str, str]]: ) ), ) - # req.show() + resp2 = self.client.sr1_req(req) - if resp2.status == ERROR_MORE_DATA: + if resp2.status == ErrorCodes.ERROR_MORE_DATA: # The buffer was too small, we need to retry with a larger one req.lpcbData = resp2.lpcbData req.lpData.value.max_count = resp2.lpcbData.value +<<<<<<< HEAD return results resp2 = self.client.sr1_req(req, timeout=1) +======= +<<<<<<< HEAD + resp2 = self.client.sr1_req(req) +======= + return results +>>>>>>> 886efa0 (Clean up and some reorganisation) +>>>>>>> 1e91a3c (Clean up and some reorganisation) if resp2.status: print( @@ -588,7 +823,7 @@ def cat(self, folder: Optional[str] = None) -> list[tuple[str, str]]: return results @CLIUtil.addoutput(cat) - def cat_output(self, results: list[RegEntry]) -> NoReturn: + def cat_output(self, results: list[RegEntry]) -> None: """ Print the output of 'cat' """ @@ -598,24 +833,51 @@ def cat_output(self, results: list[RegEntry]) -> NoReturn: for entry in results: print( - f" - {entry.reg_value:<20} {'(' + REG_TYPE.get(entry.reg_type, "UNK") + ')':<15} {entry.reg_data}" + f" - {entry.reg_value:<20} {'(' + entry.reg_type.name + ')':<15} {entry.reg_data}" ) - def _require_root_handles(self, silent: bool = False) -> bool: - if self.current_root_handle is None: - if not silent: - print("No root key selected ! Use 'use' to use one.") - return True + # --------------------------------------------- # + # Change Directory + # --------------------------------------------- # - @CLIUtil.addcommand() - def dev(self) -> NoReturn: + @CLIUtil.addcommand(spaces=True) + def cd(self, subkey: str) -> None: """ - Joker function to jump into the python code for dev purpose + Change current subkey path """ - breakpoint() + if subkey.strip() == "": + # If the subkey is ".", we do not change the current subkey path + tmp_path = PureWindowsPath() + else: + tmp_path = self._join_path(self.current_subkey_path, subkey) + + tmp_handle = self.get_handle_on_subkey(tmp_path) + + if tmp_handle is not None: + # If the handle was successfully retrieved, + # we update the current subkey path and handle + self.current_subkey_path = tmp_path + self.current_subkey_handle = tmp_handle + + @CLIUtil.addcomplete(cd) + def cd_complete(self, folder: str) -> list[str]: + """ + Auto-complete cd + """ + if self._require_root_handles(silent=True): + return [] + return [ + str(subk) + for subk in self.ls() + if str(subk).lower().startswith(folder.lower()) + ] + + # --------------------------------------------- # + # Get Information + # --------------------------------------------- # @CLIUtil.addcommand() - def get_key_security(self, folder: Optional[str] = None) -> NoReturn: + def get_sd(self, folder: Optional[str] = None) -> Optional[SECURITY_DESCRIPTOR]: """ Get the security descriptor of the current subkey. SACL are not retrieve at this point (TODO). @@ -635,7 +897,7 @@ def get_key_security(self, folder: Optional[str] = None) -> NoReturn: subkey_path = self._join_path(self.current_subkey_path, folder) handle = self.get_handle_on_subkey(subkey_path) if handle is None: - return [] + return None req = BaseRegGetKeySecurity_Request( hKey=handle, @@ -648,16 +910,16 @@ def get_key_security(self, folder: Optional[str] = None) -> NoReturn: ) resp = self.client.sr1_req(req) - if resp.status == ERROR_INSUFFICIENT_BUFFER: + if resp.status == ErrorCodes.ERROR_INSUFFICIENT_BUFFER: # The buffer was too small, we need to retry with a larger one req.pRpcSecurityDescriptorIn.cbInSecurityDescriptor = ( resp.pRpcSecurityDescriptorOut.cbInSecurityDescriptor ) resp = self.client.sr1_req(req) - if resp.status: + if not is_status_ok(resp.status): print(f"[-] Error : got status {hex(resp.status)} while getting security") - return + return None results = resp.pRpcSecurityDescriptorOut.valueof("lpSecurityDescriptor") sd = SECURITY_DESCRIPTOR(results) @@ -670,40 +932,14 @@ def get_key_security(self, folder: Optional[str] = None) -> NoReturn: return sd @CLIUtil.addcommand() - def query_info(self, folder: Optional[str] = None) -> NoReturn: + def query_info(self, folder: Optional[str] = None) -> None: """ Query information on the current subkey """ - if self._require_root_handles(silent=True): - return - - # If no specific folder was specified - # we use our current subkey path - if folder is None or folder == "": - cache = self.ls_cache.get(self.current_subkey_path) - subkey_path = self.current_subkey_path - - # if the resolution was already performed, - # no need to query again the RPC - if cache: - return cache - - # The first time we do an ls we need to get - # a proper handle - if self.current_subkey_handle is None: - self.current_subkey_handle = self.get_handle_on_subkey( - PureWindowsPath("") - ) - - handle = self.current_subkey_handle - - # Otherwise we use the folder path, - # the calling parent shall make sure that this path was properly sanitized - else: - subkey_path = self._join_path(self.current_subkey_path, folder) - handle = self.get_handle_on_subkey(subkey_path) - if handle is None: - return [] + handle = self._get_handle(folder) + if handle is None: + logger.error("Could not get handle on the specified subkey.") + return None req = BaseRegQueryInfoKey_Request( hKey=handle, @@ -711,67 +947,109 @@ def query_info(self, folder: Optional[str] = None) -> NoReturn: ) resp = self.client.sr1_req(req) - if resp.status: - print(f"[-] Error : got status {hex(resp.status)} while querying info") - return + if not is_status_ok(resp.status): + logger.error("Got status %s while querying info", hex(resp.status)) + return None - print(f"Info on key: {self.current_subkey_path}") - print(f"- Number of subkeys: {resp.lpcSubKeys}") print( - f"- Length of the longuest subkey name (in bytes): {resp.lpcbMaxSubKeyLen}" - ) - print(f"- Number of values: {resp.lpcValues}") - print( - f"- Length of the longest value name (in bytes): {resp.lpcbMaxValueNameLen}" + f""" +Info on key: {self.current_subkey_path} + - Number of subkeys: {resp.lpcSubKeys} + - Length of the longuest subkey name (in bytes): {resp.lpcbMaxSubKeyLen} + - Number of values: {resp.lpcValues} + - Length of the longest value name (in bytes): {resp.lpcbMaxValueNameLen} + - Last write time: {from_filetime_to_datetime(resp.lpftLastWriteTime)} +""" ) - print(f"- Last write time: {from_filetime_to_datetime(resp.lpftLastWriteTime)}") - @CLIUtil.addcommand(spaces=True) - def cd(self, subkey: str) -> NoReturn: + @CLIUtil.addcommand() + def version(self): """ - Change current subkey path + Get remote registry server version """ - self.current_subkey_path = self._join_path(self.current_subkey_path, subkey) - self.current_subkey_handle = self.get_handle_on_subkey(self.current_subkey_path) - self.ls_cache.clear() + version = self.client.sr1_req( + BaseRegGetVersion_Request(hKey=self.current_root_handle) + ).lpdwVersion + print(f"Remote registry server version: {version}") - @CLIUtil.addcomplete(cd) - def cd_complete(self, folder: str) -> list[str]: - """ - Auto-complete cd - """ - if self._require_root_handles(silent=True): - return [] - return [ - str(subk) - for subk in self.ls() - if str(subk).lower().startswith(folder.lower()) - ] + # --------------------------------------------- # + # Utils + # --------------------------------------------- # - def get_handle_on_subkey(self, subkey_path: PureWindowsPath) -> NDRContextHandle: + def get_handle_on_subkey( + self, + subkey_path: PureWindowsPath, + desired_access_rights: Optional[IntFlag] = None, + ) -> Optional[NDRContextHandle]: """ Ask the remote server to return an handle on a given subkey """ + # If we don't have a root handle, we cannot get a subkey handle + # This is a safety check, as we should not be able to call this function + # without having a root handle already set. + if self._require_root_handles(silent=True): + return None if str(subkey_path) == ".": subkey_path = "\x00" else: subkey_path = str(subkey_path) + "\x00" - # print(f"getting handle on: {subkey_path}") + # If no access rights were specified, we use the default read access rights + if desired_access_rights is None: + # Default to read access rights + desired_access_rights = ( + AccessRights.KEY_QUERY_VALUE + | AccessRights.KEY_ENUMERATE_SUB_KEYS + | AccessRights.STANDARD_RIGHTS_READ + ) + + logger.debug( + "Getting handle on subkey: %s with access rights: %s", + subkey_path, + desired_access_rights, + ) req = BaseRegOpenKey_Request( hKey=self.current_root_handle, lpSubKey=RPC_UNICODE_STRING(Buffer=subkey_path), - samDesired=KEY_QUERY_VALUE - | KEY_ENUMERATE_SUB_KEYS - | STANDARD_RIGHTS_READ, # | MAX_ALLOWED, + samDesired=desired_access_rights, ) + resp = self.client.sr1_req(req) - if resp.status == ERROR_SUBKEY_NOT_FOUND: - print(f"[-] Error : got status {hex(resp.status)} while enumerating keys") + if not is_status_ok(resp.status): + logger.error( + "[-] Error : got status %s while enumerating keys", hex(resp.status) + ) return None return resp.phkResult + def _get_handle( + self, folder: Optional[str] = None, desired_access: Optional[IntFlag] = None + ) -> NDRContextHandle: + """ + Get the handle on the current subkey or the specified folder. + If no folder is specified, it uses the current subkey path. + """ + if self._require_root_handles(silent=True): + return None + + # If no specific folder was specified + # we use our current subkey path + if folder is None or folder == "" or folder == ".": + subkey_path = self.current_subkey_path + handle = self.current_subkey_handle + + # Otherwise we use the folder path, + # the calling parent shall make sure that this path was properly sanitized + else: + subkey_path = self._join_path(self.current_subkey_path, folder) + handle = self.get_handle_on_subkey(subkey_path, desired_access) + if handle is None: + logger.error("Could not get handle on %s", subkey_path) + return None + + return handle + def _join_path(self, first_path: str, second_path: str) -> PureWindowsPath: return PureWindowsPath( os.path.normpath( @@ -782,6 +1060,22 @@ def _join_path(self, first_path: str, second_path: str) -> PureWindowsPath: ) ) + def _require_root_handles(self, silent: bool = False) -> bool: + if self.current_root_handle is None: + if not silent: + print("No root key selected ! Use 'use' to use one.") + return True + return False + + @CLIUtil.addcommand() + def dev(self) -> NoReturn: + """ + Joker function to jump into the python code for dev purpose + """ + logger.info("Jumping into the code for dev purpose...") + # pylint: disable=forgotten-debug-statement, pointless-statement + breakpoint() + def main(): """ @@ -789,10 +1083,10 @@ def main(): """ from scapy.utils import AutoArgparse - AutoArgparse(regclient) + AutoArgparse(RegClient) if __name__ == "__main__": from scapy.utils import AutoArgparse - AutoArgparse(regclient) + AutoArgparse(RegClient) From ede5dd8b331bda2223ed319430212c1f7a7bf4c8 Mon Sep 17 00:00:00 2001 From: Ebrix Date: Thu, 17 Jul 2025 09:28:38 +0200 Subject: [PATCH 07/27] rebase --- scapyred/winreg.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/scapyred/winreg.py b/scapyred/winreg.py index db7a03c..5f12f78 100644 --- a/scapyred/winreg.py +++ b/scapyred/winreg.py @@ -787,16 +787,8 @@ def cat(self, folder: Optional[str] = None) -> list[tuple[str, str]]: # The buffer was too small, we need to retry with a larger one req.lpcbData = resp2.lpcbData req.lpData.value.max_count = resp2.lpcbData.value -<<<<<<< HEAD - return results - resp2 = self.client.sr1_req(req, timeout=1) -======= -<<<<<<< HEAD resp2 = self.client.sr1_req(req) -======= return results ->>>>>>> 886efa0 (Clean up and some reorganisation) ->>>>>>> 1e91a3c (Clean up and some reorganisation) if resp2.status: print( From f8f07dca959b974c6776d5d243ed554b259c8822 Mon Sep 17 00:00:00 2001 From: Ebrix Date: Tue, 22 Jul 2025 08:19:03 +0200 Subject: [PATCH 08/27] Fix #1 - adding function to handle cache and cache for cd func --- scapyred/winreg.py | 224 ++++++++++++++++++++++++--------------------- 1 file changed, 118 insertions(+), 106 deletions(-) diff --git a/scapyred/winreg.py b/scapyred/winreg.py index 5f12f78..e690994 100644 --- a/scapyred/winreg.py +++ b/scapyred/winreg.py @@ -2,6 +2,7 @@ DCE/RPC client """ +from dataclasses import dataclass import socket import os import logging @@ -216,12 +217,10 @@ def is_status_ok(status: int) -> bool: ErrorCodes.ERROR_MORE_DATA, ]: print(f"[!] Error: {hex(err.value)} - {ErrorCodes(status).name}") - breakpoint() return False return True except ValueError as exc: print(f"[!] Error: {hex(status)} - Unknown error code") - breakpoint() raise ValueError(f"Error: {hex(status)} - Unknown error code") from exc @@ -236,6 +235,12 @@ def is_status_ok(status: int) -> bool: RootKeys.HKEY_PERFORMANCE_NLSTEXT, ] +READ_ACCESS_RIGHTS = ( + AccessRights.KEY_QUERY_VALUE + | AccessRights.KEY_ENUMERATE_SUB_KEYS + | AccessRights.STANDARD_RIGHTS_READ +) + class RegEntry: """ @@ -286,6 +291,17 @@ def __repr__(self) -> str: return f"RegEntry({self.reg_value}, {self.reg_type}, {self.reg_data})" +@dataclass +class CacheElt: + """ + Cache element to store the handle and the subkey path + """ + + handle: NDRContextHandle + access: AccessRights + values: list + + @conf.commands.register class RegClient(CLIUtil): r""" @@ -427,8 +443,11 @@ def __init__( self.client.connect(target) self.client.open_smbpipe("winreg") self.client.bind(self.interface) - self.ls_cache: dict[str:list] = dict() - self.cat_cache: dict[str:list] = dict() + self.cache: dict[str : dict[str, CacheElt]] = { + "ls": dict(), + "cat": dict(), + "cd": dict(), + } self.root_handle = {} self.current_root_handle = None self.current_subkey_handle = None @@ -472,16 +491,12 @@ def use(self, root_path): - Determines which registry root to use based on the prefix of `root_path`. - Opens the corresponding registry root handle if not already opened, using the appropriate request. - Sets `self.current_root_handle` and `self.current_root_path` to the selected root. - - Clears the local subkey cache (`self.ls_cache`). + - Clears the local subkey cache - Changes the current directory to the root of the selected registry hive. """ - default_read_access_rights = ( - AccessRights.KEY_QUERY_VALUE - | AccessRights.KEY_ENUMERATE_SUB_KEYS - | AccessRights.STANDARD_RIGHTS_READ - ) - root_path = RootKeys(root_path) + default_read_access_rights = READ_ACCESS_RIGHTS + root_path = RootKeys(root_path.upper().strip()) match root_path: case RootKeys.HKEY_CLASSES_ROOT: @@ -586,14 +601,14 @@ def use(self, root_path): case _: # If the root key is not recognized, raise an error print(f"Unknown root key: {root_path}") - self.ls_cache.clear() + self._clear_all_caches() self.current_root_handle = None self.current_root_path = "CHOOSE ROOT KEY" self.cd("") return self.current_root_path = root_path.value - self.ls_cache.clear() + self._clear_all_caches() self.cd("") @CLIUtil.addcomplete(use) @@ -615,39 +630,24 @@ def ls(self, folder: Optional[str] = None) -> list[str]: """ EnumKeys of the current subkey path """ - # If no specific folder was specified - # we use our current subkey path - if folder is None or folder == "": - cache = self.ls_cache.get(self.current_subkey_path) - subkey_path = self.current_subkey_path - # if the resolution was already performed, + res = self._get_cached_elt(folder=folder, cache_name="ls") + if res is None: + return [] + elif len(res.values) != 0: + # If the resolution was already performed, # no need to query again the RPC - if cache: - return cache - - # The first time we do an ls we need to get - # a proper handle - if self.current_subkey_handle is None: - self.current_subkey_handle = self.get_handle_on_subkey( - PureWindowsPath("") - ) + return res.values - handle = self.current_subkey_handle + if folder is None: + folder = "" - # Otherwise we use the folder path, - # the calling parent shall make sure that this path was properly sanitized - else: - subkey_path = self._join_path(self.current_subkey_path, folder) - handle = self.get_handle_on_subkey(subkey_path) - if handle is None: - return [] + subkey_path = self._join_path(self.current_subkey_path, folder) - self.ls_cache[subkey_path] = list() idx = 0 while True: req = BaseRegEnumKey_Request( - hKey=handle, + hKey=res.handle, dwIndex=idx, lpNameIn=RPC_UNICODE_STRING(MaximumLength=1024), lpClassIn=RPC_UNICODE_STRING(), @@ -661,15 +661,15 @@ def ls(self, folder: Optional[str] = None) -> list[str]: print( f"[-] Error : got status {hex(resp.status)} while enumerating keys" ) - self.ls_cache.clear() + self.cache["ls"].pop(subkey_path, None) return [] - self.ls_cache[subkey_path].append( + self.cache["ls"][subkey_path].values.append( resp.lpNameOut.valueof("Buffer").decode("utf-8").strip("\x00") ) idx += 1 - return self.ls_cache[subkey_path] + return self.cache["ls"][subkey_path].values @CLIUtil.addoutput(ls) def ls_output(self, results: list[str]) -> NoReturn: @@ -711,39 +711,21 @@ def cat(self, folder: Optional[str] = None) -> list[tuple[str, str]]: - May print error messages to standard output if RPC queries fail. - Updates internal cache for previously enumerated subkey paths. """ - # If no specific folder was specified - # we use our current subkey path - if folder is None or folder == "": - cache = self.cat_cache.get(self.current_subkey_path) - subkey_path = self.current_subkey_path - - # if the resolution was already performed, + res = self._get_cached_elt(folder=folder, cache_name="cat") + if res is None: + return [] + elif len(res.values) != 0: + # If the resolution was already performed, # no need to query again the RPC - if cache: - return cache - - # The first time we do a cat we need to get - # a proper handle - if self.current_subkey_handle is None: - self.current_subkey_handle = self.get_handle_on_subkey( - PureWindowsPath("") - ) + return res.values - handle = self.current_subkey_handle - - # Otherwise we use the folder path, - # the calling parent shall make sure that this path was properly sanitized - else: - subkey_path = self._join_path(self.current_subkey_path, folder) - handle = self.get_handle_on_subkey(subkey_path) - if handle is None: - return [] + subkey_path = self._join_path(self.current_subkey_path, folder) idx = 0 - results = [] while True: + # Get the name of the value at index idx req = BaseRegEnumValue_Request( - hKey=handle, + hKey=res.handle, dwIndex=idx, lpValueNameIn=RPC_UNICODE_STRING( MaximumLength=2048, @@ -766,11 +748,13 @@ def cat(self, folder: Optional[str] = None) -> list[tuple[str, str]]: print( f"[-] Error : got status {hex(resp.status)} while enumerating values" ) - self.cat_cache.clear() + self.cache["cat"].pop(subkey_path, None) return [] + # Get the value name and type + # for the name we got earlier req = BaseRegQueryValue_Request( - hKey=handle, + hKey=res.handle, lpValueName=resp.valueof("lpValueNameOut"), lpType=0, lpcbData=1024, @@ -788,18 +772,18 @@ def cat(self, folder: Optional[str] = None) -> list[tuple[str, str]]: req.lpcbData = resp2.lpcbData req.lpData.value.max_count = resp2.lpcbData.value resp2 = self.client.sr1_req(req) - return results - if resp2.status: + if not is_status_ok(resp2.status): print( f"[-] Error : got status {hex(resp2.status)} while querying value" ) + self.cache["cat"].pop(subkey_path, None) return [] value = ( resp.valueof("lpValueNameOut").valueof("Buffer").decode("utf-8").strip() ) - results.append( + self.cache["cat"][subkey_path].values.append( RegEntry( reg_value=value, reg_type=resp2.valueof("lpType"), @@ -807,12 +791,9 @@ def cat(self, folder: Optional[str] = None) -> list[tuple[str, str]]: ) ) - # self.cat_cache[subkey_path].append( - # resp.valueof("lpValueNameOut").valueof("Buffer").decode("utf-8").strip() - # ) idx += 1 - return results + return self.cache["cat"][subkey_path].values @CLIUtil.addoutput(cat) def cat_output(self, results: list[RegEntry]) -> None: @@ -828,6 +809,19 @@ def cat_output(self, results: list[RegEntry]) -> None: f" - {entry.reg_value:<20} {'(' + entry.reg_type.name + ')':<15} {entry.reg_data}" ) + @CLIUtil.addcomplete(cat) + def cat_complete(self, folder: str) -> list[str]: + """ + Auto-complete cat + """ + if self._require_root_handles(silent=True): + return [] + return [ + str(subk) + for subk in self.ls() + if str(subk).lower().startswith(folder.lower()) + ] + # --------------------------------------------- # # Change Directory # --------------------------------------------- # @@ -840,11 +834,16 @@ def cd(self, subkey: str) -> None: if subkey.strip() == "": # If the subkey is ".", we do not change the current subkey path tmp_path = PureWindowsPath() + tmp_handle = self.get_handle_on_subkey(tmp_path) + else: + res = self._get_cached_elt( + folder=subkey, + cache_name="cd", + ) + tmp_handle = res.handle if res else None tmp_path = self._join_path(self.current_subkey_path, subkey) - tmp_handle = self.get_handle_on_subkey(tmp_path) - if tmp_handle is not None: # If the handle was successfully retrieved, # we update the current subkey path and handle @@ -874,22 +873,9 @@ def get_sd(self, folder: Optional[str] = None) -> Optional[SECURITY_DESCRIPTOR]: Get the security descriptor of the current subkey. SACL are not retrieve at this point (TODO). """ - if self._require_root_handles(silent=True): - return - - # If no specific folder was specified - # we use our current subkey path - if folder is None or folder == "": - subkey_path = self.current_subkey_path - handle = self.current_subkey_handle - - # Otherwise we use the folder path, - # the calling parent shall make sure that this path was properly sanitized - else: - subkey_path = self._join_path(self.current_subkey_path, folder) - handle = self.get_handle_on_subkey(subkey_path) - if handle is None: - return None + handle = self._get_cached_elt(folder=folder) + if handle is None: + return None req = BaseRegGetKeySecurity_Request( hKey=handle, @@ -928,7 +914,7 @@ def query_info(self, folder: Optional[str] = None) -> None: """ Query information on the current subkey """ - handle = self._get_handle(folder) + handle = self._get_cached_elt(folder) if handle is None: logger.error("Could not get handle on the specified subkey.") return None @@ -1015,9 +1001,12 @@ def get_handle_on_subkey( return resp.phkResult - def _get_handle( - self, folder: Optional[str] = None, desired_access: Optional[IntFlag] = None - ) -> NDRContextHandle: + def _get_cached_elt( + self, + folder: Optional[str] = None, + cache_name: str = None, + desired_access: Optional[IntFlag] = None, + ) -> Optional[NDRContextHandle | CacheElt]: """ Get the handle on the current subkey or the specified folder. If no folder is specified, it uses the current subkey path. @@ -1025,22 +1014,38 @@ def _get_handle( if self._require_root_handles(silent=True): return None + if desired_access is None: + # Default to read access rights + desired_access = READ_ACCESS_RIGHTS + # If no specific folder was specified # we use our current subkey path if folder is None or folder == "" or folder == ".": subkey_path = self.current_subkey_path - handle = self.current_subkey_handle - # Otherwise we use the folder path, # the calling parent shall make sure that this path was properly sanitized else: subkey_path = self._join_path(self.current_subkey_path, folder) - handle = self.get_handle_on_subkey(subkey_path, desired_access) - if handle is None: - logger.error("Could not get handle on %s", subkey_path) - return None - return handle + if ( + self.cache.get(cache_name, None) is not None + and self.cache[cache_name].get(subkey_path, None) is not None + and self.cache[cache_name][subkey_path].access == desired_access + ): + # If we have a cache, we check if the handle is already cached + # If the access rights are the same, we return the cached elt + return self.cache[cache_name][subkey_path] + + handle = self.get_handle_on_subkey(subkey_path, desired_access) + if handle is None: + logger.error("Could not get handle on %s", subkey_path) + return None + + cache_elt = CacheElt(handle, desired_access, []) + if cache_name is not None: + self.cache[cache_name][subkey_path] = cache_elt + + return cache_elt if cache_name is not None else handle def _join_path(self, first_path: str, second_path: str) -> PureWindowsPath: return PureWindowsPath( @@ -1059,6 +1064,13 @@ def _require_root_handles(self, silent: bool = False) -> bool: return True return False + def _clear_all_caches(self) -> None: + """ + Clear all caches + """ + for key in self.cache.keys(): + self.cache[key].clear() + @CLIUtil.addcommand() def dev(self) -> NoReturn: """ From 88611c961ee14b1b20840b31aa883fbe7c4728b4 Mon Sep 17 00:00:00 2001 From: Ebrix Date: Wed, 23 Jul 2025 08:13:38 +0200 Subject: [PATCH 09/27] dwOptions manipulation backup & volatile --- scapyred/winreg.py | 63 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/scapyred/winreg.py b/scapyred/winreg.py index e690994..0e10c14 100644 --- a/scapyred/winreg.py +++ b/scapyred/winreg.py @@ -85,6 +85,19 @@ class AccessRights(IntFlag): MAX_ALLOWED = 0x02000000 +class RegOptions(IntFlag): + """ + Registry options for registry keys + """ + + REG_OPTION_NON_VOLATILE = 0x00000000 + REG_OPTION_VOLATILE = 0x00000001 + REG_OPTION_CREATE_LINK = 0x00000002 + REG_OPTION_BACKUP_RESTORE = 0x00000004 + REG_OPTION_OPEN_LINK = 0x00000008 + REG_OPTION_DONT_VIRTUALIZE = 0x00000010 + + class ErrorCodes(IntEnum): """ Error codes for registry operations @@ -448,6 +461,9 @@ def __init__( "cat": dict(), "cd": dict(), } + # Options for registry operations default to non-volatile + # This means that the registry key will not be deleted when the system is restarted. + self.extra_options = RegOptions.REG_OPTION_NON_VOLATILE self.root_handle = {} self.current_root_handle = None self.current_subkey_handle = None @@ -950,6 +966,52 @@ def version(self): ).lpdwVersion print(f"Remote registry server version: {version}") + # --------------------------------------------- # + # Operation options + # --------------------------------------------- # + + @CLIUtil.addcommand() + def activate_backup(self) -> None: + """ + Activate the backup option for the registry operations (enable your backup privilege). + This enable the backup privilege for the current session. + """ + self.extra_options |= RegOptions.REG_OPTION_BACKUP_RESTORE + print("Backup option activated.") + # Clear the local cache, as the backup option will change the behavior of the registry + self._clear_all_caches() + + @CLIUtil.addcommand() + def disable_backup(self) -> None: + """ + Disable the backup option for the registry operations (disable your backup privilege). + This disable the backup privilege for the current session. + """ + self.extra_options &= ~RegOptions.REG_OPTION_BACKUP_RESTORE + print("Backup option deactivated.") + self._clear_all_caches() + + def switch_volatile(self) -> None: + """ + Set the registry operations to be volatile. + This means that the registry key will be deleted when the system is restarted. + """ + self.extra_options |= RegOptions.REG_OPTION_VOLATILE + self.extra_options &= ~RegOptions.REG_OPTION_NON_VOLATILE + print("Volatile option activated.") + + self._clear_all_caches() + + def disable_volatile(self) -> None: + """ + Disable the volatile option for the registry operations. + This means that the registry key will not be deleted when the system is restarted. + """ + self.extra_options &= ~RegOptions.REG_OPTION_VOLATILE + self.extra_options |= RegOptions.REG_OPTION_NON_VOLATILE + print("Volatile option deactivated.") + self._clear_all_caches() + # --------------------------------------------- # # Utils # --------------------------------------------- # @@ -990,6 +1052,7 @@ def get_handle_on_subkey( hKey=self.current_root_handle, lpSubKey=RPC_UNICODE_STRING(Buffer=subkey_path), samDesired=desired_access_rights, + dwOptions=self.extra_options, ) resp = self.client.sr1_req(req) From d8ab0d860c019d616a4ae9959c34ca86ebf57d8b Mon Sep 17 00:00:00 2001 From: Ebrix Date: Wed, 23 Jul 2025 08:38:52 +0200 Subject: [PATCH 10/27] buggy save --- scapyred/winreg.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/scapyred/winreg.py b/scapyred/winreg.py index 0e10c14..4f6657f 100644 --- a/scapyred/winreg.py +++ b/scapyred/winreg.py @@ -30,6 +30,7 @@ BaseRegGetVersion_Request, BaseRegQueryInfoKey_Request, BaseRegGetKeySecurity_Request, + BaseRegSaveKey_Request, PRPC_SECURITY_DESCRIPTOR, NDRContextHandle, RPC_UNICODE_STRING, @@ -931,6 +932,7 @@ def query_info(self, folder: Optional[str] = None) -> None: Query information on the current subkey """ handle = self._get_cached_elt(folder) + breakpoint() if handle is None: logger.error("Could not get handle on the specified subkey.") return None @@ -966,6 +968,39 @@ def version(self): ).lpdwVersion print(f"Remote registry server version: {version}") + # --------------------------------------------- # + # Backup and Restore + # --------------------------------------------- # + + @CLIUtil.addcommand() + def backup( + self, folder: Optional[str] = None, output_path: Optional[str] = None + ) -> None: + """ + Backup the current subkey to a file. + If no folder is specified, it uses the current subkey path. + """ + self.activate_backup() + handle = self._get_cached_elt(folder=folder) + if handle is None: + logger.error("Could not get handle on the specified subkey.") + return None + + req = BaseRegSaveKey_Request( + hKey=handle, + lpFile=RPC_UNICODE_STRING(Buffer=f"C:\\AAAAAAAAAAAAAAAA.reg\x00"), + pSecurityAttributes=None, # No security attributes - Tout le monde peut lire OKLM + ) + + resp = self.client.sr1_req(req) + if not is_status_ok(resp.status): + logger.error("Got status %s while backing up", hex(resp.status)) + return None + + print( + f"Backup of {self.current_subkey_path} saved to {self.current_subkey_path}.reg" + ) + # --------------------------------------------- # # Operation options # --------------------------------------------- # From 3e750cb5c02b46d1cf6a04265768ebcca1f8fb99 Mon Sep 17 00:00:00 2001 From: Ebrix Date: Wed, 23 Jul 2025 08:42:06 +0200 Subject: [PATCH 11/27] prevent cache clear if backup option was already enabled/disabled --- scapyred/winreg.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scapyred/winreg.py b/scapyred/winreg.py index 4f6657f..467bfb3 100644 --- a/scapyred/winreg.py +++ b/scapyred/winreg.py @@ -1011,6 +1011,10 @@ def activate_backup(self) -> None: Activate the backup option for the registry operations (enable your backup privilege). This enable the backup privilege for the current session. """ + # check if backup privilege is already enabled + if self.extra_options & RegOptions.REG_OPTION_BACKUP_RESTORE: + print("Backup option is already activated. Didn't do anything.") + return self.extra_options |= RegOptions.REG_OPTION_BACKUP_RESTORE print("Backup option activated.") # Clear the local cache, as the backup option will change the behavior of the registry @@ -1022,6 +1026,10 @@ def disable_backup(self) -> None: Disable the backup option for the registry operations (disable your backup privilege). This disable the backup privilege for the current session. """ + # check if backup privilege is already disabled + if not self.extra_options & RegOptions.REG_OPTION_BACKUP_RESTORE: + print("Backup option is already disabled. Didn't do anything.") + return self.extra_options &= ~RegOptions.REG_OPTION_BACKUP_RESTORE print("Backup option deactivated.") self._clear_all_caches() From 43df0850c6c7d84c6376244d2177dd5af32ce64e Mon Sep 17 00:00:00 2001 From: Ebrix Date: Thu, 31 Jul 2025 13:07:36 +0200 Subject: [PATCH 12/27] Buggy save with sd --- scapyred/winreg.py | 210 +++++++++++++++++++++++++++++++++------------ 1 file changed, 153 insertions(+), 57 deletions(-) diff --git a/scapyred/winreg.py b/scapyred/winreg.py index 467bfb3..2bd3692 100644 --- a/scapyred/winreg.py +++ b/scapyred/winreg.py @@ -7,13 +7,48 @@ import os import logging -from enum import IntEnum, IntFlag, StrEnum - +from enum import IntEnum, IntFlag, StrEnum, Enum from ctypes.wintypes import PFILETIME from typing import Optional, NoReturn from pathlib import PureWindowsPath -# pylint: disable-next=import-error, no-name-in-module +from scapy.themes import DefaultTheme +from scapy.base_classes import Net +from scapy.utils import ( + CLIUtil, + valid_ip, + valid_ip6, +) +from scapy.utils6 import Net6 +from scapy.layers.kerberos import ( + KerberosSSP, + krb_as_and_tgs, + _parse_upn, +) +from scapy.layers.msrpce.rpcclient import DCERPC_Client +from scapy.layers.dcerpc import find_dcerpc_interface, DCERPC_Transport +from scapy.layers.ntlm import MD4le, NTLMSSP +from scapy.layers.spnego import SPNEGOSSP +from scapy.layers.smb2 import ( + SECURITY_DESCRIPTOR, + WINNT_SID, + WINNT_ACL, + WINNT_ACE_HEADER, + WINNT_ACCESS_ALLOWED_ACE, +) +from scapy.layers.dcerpc import ( + RPC_C_AUTHN_LEVEL, + NDRConformantArray, + NDRPointer, + NDRVaryingArray, +) +from scapy.config import conf + +# pylint: disable-next=too-many-function-args +conf.exts.load("scapy-rpc") +conf.color_theme = DefaultTheme() + +# pylint: disable-next=import-error, no-name-in-module, wrong-import-position from scapy.layers.msrpce.raw.ms_rrp import ( OpenClassesRoot_Request, OpenLocalMachine_Request, @@ -32,37 +67,13 @@ BaseRegGetKeySecurity_Request, BaseRegSaveKey_Request, PRPC_SECURITY_DESCRIPTOR, + PRPC_SECURITY_ATTRIBUTES, + RPC_SECURITY_DESCRIPTOR, NDRContextHandle, RPC_UNICODE_STRING, ) -from scapy.layers.dcerpc import ( - RPC_C_AUTHN_LEVEL, - NDRConformantArray, - NDRPointer, - NDRVaryingArray, -) -from scapy.utils import ( - CLIUtil, - valid_ip, - valid_ip6, -) -from scapy.layers.kerberos import ( - KerberosSSP, - krb_as_and_tgs, - _parse_upn, -) -from scapy.config import conf -from scapy.themes import DefaultTheme -from scapy.base_classes import Net -from scapy.utils6 import Net6 -from scapy.layers.msrpce.rpcclient import DCERPC_Client -from scapy.layers.dcerpc import find_dcerpc_interface, DCERPC_Transport -from scapy.layers.ntlm import MD4le, NTLMSSP -from scapy.layers.spnego import SPNEGOSSP -from scapy.layers.smb2 import SECURITY_DESCRIPTOR -conf.color_theme = DefaultTheme() logging.basicConfig( level=logging.INFO, format="[%(levelname)s][%(funcName)s] %(message)s", @@ -84,6 +95,7 @@ class AccessRights(IntFlag): KEY_ENUMERATE_SUB_KEYS = 0x00000008 STANDARD_RIGHTS_READ = 0x00020000 MAX_ALLOWED = 0x02000000 + FILE_ALL_ACCESS = 0x001F01FF class RegOptions(IntFlag): @@ -256,6 +268,35 @@ def is_status_ok(status: int) -> bool: ) +class WellKnownSIDs(Enum): + """ + Well-known SIDs. + """ + + SY = WINNT_SID.fromstr("S-1-5-18") # Local System + BA = WINNT_SID.fromstr("S-1-5-32-544") # Built-in Administrators + + +DEFAULT_SECURITY_DESCRIPTOR = SECURITY_DESCRIPTOR( + Control=0x1000 | 0x8000 | 0x4, + OwnerSid=WellKnownSIDs.SY.value, # Local System SID + GroupSid=WellKnownSIDs.SY.value, # Local System SID + DACL=WINNT_ACL( + Aces=[ + WINNT_ACE_HEADER( + AceType=0x0, # ACCESS_ALLOWED_ACE_TYPE + AceFlags=0x0, # No flags + ) + / WINNT_ACCESS_ALLOWED_ACE( + Mask=0x00020000, # Read access rights + Sid=WellKnownSIDs.SY.value, # Built-in Administrators SID + ), + ], + ), + ndr64=True, +) + + class RegEntry: """ RegEntry to properly parse the data based on the type. @@ -272,31 +313,30 @@ def __init__(self, reg_value: str, reg_type: int, reg_data: bytes): except ValueError: self.reg_type = RegType.UNK - if ( - self.reg_type == RegType.REG_MULTI_SZ - or self.reg_type == RegType.REG_SZ - or self.reg_type == RegType.REG_EXPAND_SZ - ): - if self.reg_type == RegType.REG_MULTI_SZ: - # decode multiple null terminated strings - self.reg_data = reg_data.decode("utf-16le")[:-2].replace("\x00", "\n") - else: - self.reg_data = reg_data.decode("utf-16le") + match self.reg_type: + case RegType.REG_MULTI_SZ | RegType.REG_SZ | RegType.REG_EXPAND_SZ: + if self.reg_type == RegType.REG_MULTI_SZ: + # decode multiple null terminated strings + self.reg_data = reg_data.decode("utf-16le")[:-2].replace( + "\x00", "\n" + ) + else: + self.reg_data = reg_data.decode("utf-16le") - elif self.reg_type == RegType.REG_BINARY: - self.reg_data = reg_data + case RegType.REG_BINARY: + self.reg_data = reg_data - elif self.reg_type == RegType.REG_DWORD or self.reg_type == RegType.REG_QWORD: - self.reg_data = int.from_bytes(reg_data, byteorder="little") + case RegType.REG_DWORD | RegType.REG_QWORD: + self.reg_data = int.from_bytes(reg_data, byteorder="little") - elif self.reg_type == RegType.REG_DWORD_BIG_ENDIAN: - self.reg_data = int.from_bytes(reg_data, byteorder="big") + case RegType.REG_DWORD_BIG_ENDIAN: + self.reg_data = int.from_bytes(reg_data, byteorder="big") - elif self.reg_type == RegType.REG_LINK: - self.reg_data = reg_data.decode("utf-16le") + case RegType.REG_LINK: + self.reg_data = reg_data.decode("utf-16le") - else: - self.reg_data = reg_data + case _: + self.reg_data = reg_data def __str__(self) -> str: return f"{self.reg_value} ({self.reg_type.name}) {self.reg_data}" @@ -334,6 +374,7 @@ class RegClient(CLIUtil): :param KEY: if provided, the session key associated to the ticket (Kerberos) :param cli: CLI mode (default True). False to use for scripting :param rootKey: the root key to get a handle to (HKLM, HKCU, etc.), in CLI mode you can chose it later + :param subKey: the subkey to use (default None, in CLI mode you can chose it later) Some additional SMB parameters are available under help(SMB_Client). Some of @@ -359,6 +400,7 @@ def __init__( KEY=None, cli=True, rootKey: str = None, + subKey: str = None, # SMB arguments **kwargs, ): @@ -438,7 +480,7 @@ def __init__( DCERPC_Transport.NCACN_NP, auth_level=RPC_C_AUTHN_LEVEL.PKT_PRIVACY, ssp=ssp, - ndr64=False, + ndr64=True, ) if debug: logger.setLevel(logging.DEBUG) @@ -454,8 +496,17 @@ def __init__( self.timeout = timeout self.client.verb = False - self.client.connect(target) - self.client.open_smbpipe("winreg") + try: + self.client.connect(target) + self.client.open_smbpipe("winreg") + except ValueError as exc: + if "3221225644" in str(exc): + print( + "[!] Warn: Remote service didn't seem to be running. Let's try again now that we should have trigger it." + ) + self.client.open_smbpipe("winreg") + else: + raise exc self.client.bind(self.interface) self.cache: dict[str : dict[str, CacheElt]] = { "ls": dict(), @@ -474,6 +525,8 @@ def __init__( self.use(self.current_root_path) else: self.current_root_path = "CHOOSE ROOT KEY" + if subKey: + self.cd(subKey.strip()) if cli: self.loop(debug=debug) @@ -522,7 +575,9 @@ def use(self, root_path): RootKeys.HKEY_CLASSES_ROOT.value, self.client.sr1_req( OpenClassesRoot_Request( - ServerName=None, samDesired=default_read_access_rights + ServerName=None, + samDesired=default_read_access_rights, + ndr64=True, ), timeout=self.timeout, ).phKey, @@ -536,6 +591,7 @@ def use(self, root_path): OpenCurrentUser_Request( ServerName=None, samDesired=default_read_access_rights, + ndr64=True, ), timeout=self.timeout, ).phKey, @@ -549,6 +605,7 @@ def use(self, root_path): OpenLocalMachine_Request( ServerName=None, samDesired=default_read_access_rights, + ndr64=True, ), timeout=self.timeout, ).phKey, @@ -562,6 +619,7 @@ def use(self, root_path): OpenCurrentConfig_Request( ServerName=None, samDesired=default_read_access_rights, + ndr64=True, ), timeout=self.timeout, ).phKey, @@ -574,6 +632,7 @@ def use(self, root_path): OpenUsers_Request( ServerName=None, samDesired=default_read_access_rights, + ndr64=True, ), timeout=self.timeout, ).phKey, @@ -586,6 +645,7 @@ def use(self, root_path): OpenPerformanceData_Request( ServerName=None, samDesired=default_read_access_rights, + ndr64=True, ), timeout=self.timeout, ).phKey, @@ -598,6 +658,7 @@ def use(self, root_path): OpenPerformanceText_Request( ServerName=None, samDesired=default_read_access_rights, + ndr64=True, ), timeout=self.timeout, ).phKey, @@ -610,6 +671,7 @@ def use(self, root_path): OpenPerformanceNlsText_Request( ServerName=None, samDesired=default_read_access_rights, + ndr64=True, ), timeout=self.timeout, ).phKey, @@ -669,6 +731,7 @@ def ls(self, folder: Optional[str] = None) -> list[str]: lpNameIn=RPC_UNICODE_STRING(MaximumLength=1024), lpClassIn=RPC_UNICODE_STRING(), lpftLastWriteTime=None, + ndr64=True, ) resp = self.client.sr1_req(req) @@ -756,6 +819,7 @@ def cat(self, folder: Optional[str] = None) -> list[tuple[str, str]]: lpData=None, # pointer to buffer lpcbData=0, # pointer to buffer size lpcbLen=0, # pointer to length + ndr64=True, ) resp = self.client.sr1_req(req) @@ -781,6 +845,7 @@ def cat(self, folder: Optional[str] = None) -> list[tuple[str, str]]: max_count=1024, value=NDRVaryingArray(actual_count=0, value=b"") ) ), + ndr64=True, ) resp2 = self.client.sr1_req(req) @@ -902,6 +967,7 @@ def get_sd(self, folder: Optional[str] = None) -> Optional[SECURITY_DESCRIPTOR]: pRpcSecurityDescriptorIn=PRPC_SECURITY_DESCRIPTOR( cbInSecurityDescriptor=512, # Initial size of the buffer ), + ndr64=True, ) resp = self.client.sr1_req(req) @@ -932,7 +998,6 @@ def query_info(self, folder: Optional[str] = None) -> None: Query information on the current subkey """ handle = self._get_cached_elt(folder) - breakpoint() if handle is None: logger.error("Could not get handle on the specified subkey.") return None @@ -940,6 +1005,7 @@ def query_info(self, folder: Optional[str] = None) -> None: req = BaseRegQueryInfoKey_Request( hKey=handle, lpClassIn=RPC_UNICODE_STRING(), # pointer to class name + ndr64=True, ) resp = self.client.sr1_req(req) @@ -973,7 +1039,7 @@ def version(self): # --------------------------------------------- # @CLIUtil.addcommand() - def backup( + def save( self, folder: Optional[str] = None, output_path: Optional[str] = None ) -> None: """ @@ -982,17 +1048,46 @@ def backup( """ self.activate_backup() handle = self._get_cached_elt(folder=folder) + key_to_save = ( + folder.split("\\")[-1] if folder else self.current_subkey_path.name + ) + if handle is None: logger.error("Could not get handle on the specified subkey.") return None + # Default path is %WINDIR%\System32 + if output_path is None: + output_path = str(key_to_save) + ".reg" + + elif output_path.endswith(".reg"): + # If the output path ends with .reg, we use it as is + output_path = output_path.strip() + + else: + # Otherwise, we use the current subkey path as the output path + output_path = str(self._join_path(output_path, str(key_to_save) + ".reg")) + + print(f"Backing up {key_to_save} to {output_path}") + + sa = PRPC_SECURITY_ATTRIBUTES( + RpcSecurityDescriptor=RPC_SECURITY_DESCRIPTOR( + lpSecurityDescriptor=DEFAULT_SECURITY_DESCRIPTOR, + ), + ndr64=True, + ) req = BaseRegSaveKey_Request( hKey=handle, - lpFile=RPC_UNICODE_STRING(Buffer=f"C:\\AAAAAAAAAAAAAAAA.reg\x00"), - pSecurityAttributes=None, # No security attributes - Tout le monde peut lire OKLM + lpFile=RPC_UNICODE_STRING(Buffer=output_path), + pSecurityAttributes=sa, + ndr64=True, ) + # If the security attributes are not provided, the default security descriptor is used. + # Meanning the file will inherite the access rights of the its parent directory. + req.show2() resp = self.client.sr1_req(req) + resp.show() if not is_status_ok(resp.status): logger.error("Got status %s while backing up", hex(resp.status)) return None @@ -1096,6 +1191,7 @@ def get_handle_on_subkey( lpSubKey=RPC_UNICODE_STRING(Buffer=subkey_path), samDesired=desired_access_rights, dwOptions=self.extra_options, + ndr64=True, ) resp = self.client.sr1_req(req) From a46e54a02593ea60bd371a10d0cc56832b72974d Mon Sep 17 00:00:00 2001 From: Ebrix Date: Sat, 16 Aug 2025 16:32:17 +0200 Subject: [PATCH 13/27] Reg save --- scapyred/winreg.py | 568 +++++++++++++++++++++++++++++---------------- 1 file changed, 368 insertions(+), 200 deletions(-) diff --git a/scapyred/winreg.py b/scapyred/winreg.py index 2bd3692..77e794f 100644 --- a/scapyred/winreg.py +++ b/scapyred/winreg.py @@ -3,31 +3,21 @@ """ from dataclasses import dataclass -import socket import os import logging from enum import IntEnum, IntFlag, StrEnum, Enum from ctypes.wintypes import PFILETIME -from typing import Optional, NoReturn +from typing import NoReturn from pathlib import PureWindowsPath from scapy.themes import DefaultTheme -from scapy.base_classes import Net from scapy.utils import ( CLIUtil, - valid_ip, - valid_ip6, -) -from scapy.utils6 import Net6 -from scapy.layers.kerberos import ( - KerberosSSP, - krb_as_and_tgs, - _parse_upn, ) + from scapy.layers.msrpce.rpcclient import DCERPC_Client from scapy.layers.dcerpc import find_dcerpc_interface, DCERPC_Transport -from scapy.layers.ntlm import MD4le, NTLMSSP from scapy.layers.spnego import SPNEGOSSP from scapy.layers.smb2 import ( SECURITY_DESCRIPTOR, @@ -43,6 +33,7 @@ NDRVaryingArray, ) from scapy.config import conf +from scapy.error import Scapy_Exception # pylint: disable-next=too-many-function-args conf.exts.load("scapy-rpc") @@ -64,13 +55,16 @@ BaseRegQueryValue_Request, BaseRegGetVersion_Request, BaseRegQueryInfoKey_Request, + BaseRegQueryInfoKey_Response, BaseRegGetKeySecurity_Request, BaseRegSaveKey_Request, + BaseRegSetValue_Request, PRPC_SECURITY_DESCRIPTOR, PRPC_SECURITY_ATTRIBUTES, RPC_SECURITY_DESCRIPTOR, NDRContextHandle, RPC_UNICODE_STRING, + NDRIntField, ) @@ -93,9 +87,23 @@ class AccessRights(IntFlag): # These constants are used to specify the access rights when opening or creating registry keys. KEY_QUERY_VALUE = 0x00000001 KEY_ENUMERATE_SUB_KEYS = 0x00000008 + KEY_ALL_ACCESS = 0xF003F # Combines the STANDARD_RIGHTS_REQUIRED, KEY_QUERY_VALUE, KEY_SET_VALUE, KEY_CREATE_SUB_KEY, KEY_ENUMERATE_SUB_KEYS, KEY_NOTIFY, and KEY_CREATE_LINK access rights. STANDARD_RIGHTS_READ = 0x00020000 - MAX_ALLOWED = 0x02000000 - FILE_ALL_ACCESS = 0x001F01FF + KEY_CREATE_LINK = 0x0020 + KEY_NOTIFY = 0x0010 + KEY_READ = 0x20019 # Combines the STANDARD_RIGHTS_READ, KEY_QUERY_VALUE, KEY_ENUMERATE_SUB_KEYS, and KEY_NOTIFY values. + KEY_EXECUTE = 0x20019 # Equivalent to KEY_READ. + KEY_CREATE_SUB_KEY = 0x0004 # Required to create a subkey of a registry key. + KEY_SET_VALUE = 0x0002 # Required to create, delete, or set a registry value. + KEY_WOW64_32KEY = 0x0200 # Indicates that an application on 64-bit Windows should operate on the 32-bit registry view. + # This flag is ignored by 32-bit Windows. For more information, see Accessing an Alternate Registry View. + # This flag must be combined using the OR operator with the other flags in this table that either query or access registry values. + # Windows 2000: This flag is not supported. + KEY_WOW64_64KEY = 0x0100 # Indicates that an application on 64-bit Windows should operate on the 64-bit registry view. + # This flag is ignored by 32-bit Windows. For more information, see Accessing an Alternate Registry View. + # This flag must be combined using the OR operator with the other flags in this table that either query or access registry values. + # Windows 2000: This flag is not supported. + KEY_WRITE = 0x20006 # Combines the STANDARD_RIGHTS_WRITE, KEY_SET_VALUE, and KEY_CREATE_SUB_KEY access rights. class RegOptions(IntFlag): @@ -117,13 +125,16 @@ class ErrorCodes(IntEnum): """ ERROR_SUCCESS = 0x00000000 - ERROR_ACCESS_DENIED = 0x00000005 ERROR_FILE_NOT_FOUND = 0x00000002 + ERROR_PATH_NOT_FOUND = 0x00000003 + ERROR_ACCESS_DENIED = 0x00000005 ERROR_INVALID_HANDLE = 0x00000006 ERROR_NOT_SAME_DEVICE = 0x00000011 ERROR_WRITE_PROTECT = 0x00000013 ERROR_INVALID_PARAMETER = 0x00000057 ERROR_CALL_NOT_IMPLEMENTED = 0x00000057 + ERROR_INVALID_NAME = 0x0000007B + ERROR_ALREADY_EXISTS = 0x000000B7 ERROR_NO_MORE_ITEMS = 0x00000103 ERROR_NOACCESS = 0x000003E6 ERROR_SUBKEY_NOT_FOUND = 0x000006F7 @@ -214,6 +225,26 @@ def _missing_(cls, value): print(f"Unknown registry type: {value}, using UNK") return cls.UNK + @classmethod + def fromvalue(cls, value: str | int) -> "RegType": + """Convert a string to a RegType enum member. + :param value: The string representation of the registry type. + :return: The corresponding RegType enum member. + """ + if isinstance(value, int): + breakpoint() + try: + return cls(value) + except ValueError: + print(f"Unknown registry type: {value}, using UNK") + + value = value.strip().upper() + try: + return cls[int(value)] + except (ValueError, KeyError): + print(f"Unknown registry type: {value}, using UNK") + return cls.UNK + def from_filetime_to_datetime(lp_filetime: PFILETIME) -> str: """ @@ -261,12 +292,6 @@ def is_status_ok(status: int) -> bool: RootKeys.HKEY_PERFORMANCE_NLSTEXT, ] -READ_ACCESS_RIGHTS = ( - AccessRights.KEY_QUERY_VALUE - | AccessRights.KEY_ENUMERATE_SUB_KEYS - | AccessRights.STANDARD_RIGHTS_READ -) - class WellKnownSIDs(Enum): """ @@ -279,23 +304,31 @@ class WellKnownSIDs(Enum): DEFAULT_SECURITY_DESCRIPTOR = SECURITY_DESCRIPTOR( Control=0x1000 | 0x8000 | 0x4, - OwnerSid=WellKnownSIDs.SY.value, # Local System SID - GroupSid=WellKnownSIDs.SY.value, # Local System SID + # OwnerSid=WellKnownSIDs.SY.value, # Local System SID + # GroupSid=WellKnownSIDs.SY.value, # Local System SID DACL=WINNT_ACL( + AclRevision=2, + Sbz1=0, + AclSize=0xFF, Aces=[ WINNT_ACE_HEADER( AceType=0x0, # ACCESS_ALLOWED_ACE_TYPE AceFlags=0x0, # No flags ) / WINNT_ACCESS_ALLOWED_ACE( - Mask=0x00020000, # Read access rights - Sid=WellKnownSIDs.SY.value, # Built-in Administrators SID + Mask=0x10000000, # GA + Sid=WellKnownSIDs.BA.value, # Built-in Administrators SID ), ], ), ndr64=True, ) +# For now we force the AclSize to the length of the Acl +DEFAULT_SECURITY_DESCRIPTOR.Data[0][1][WINNT_ACL].AclSize = len( + DEFAULT_SECURITY_DESCRIPTOR.Data[0][1][WINNT_ACL] +) + class RegEntry: """ @@ -338,6 +371,36 @@ def __init__(self, reg_value: str, reg_type: int, reg_data: bytes): case _: self.reg_data = reg_data + @staticmethod + def encode_data(reg_type: RegType, data: str) -> bytes: + """ + Encode data based on the type. + """ + match reg_type: + case RegType.REG_MULTI_SZ | RegType.REG_SZ | RegType.REG_EXPAND_SZ: + if reg_type == RegType.REG_MULTI_SZ: + # decode multiple null terminated strings + return data.replace("\n", "\x00").encode("utf-16le") + b"\x00\x00" + else: + return data.decode("utf-16le") + + case RegType.REG_BINARY: + return data + + case RegType.REG_DWORD | RegType.REG_QWORD: + bit_length = (int(data).bit_length() + 7) // 8 + return int(data).to_bytes(bit_length, byteorder="little") + + case RegType.REG_DWORD_BIG_ENDIAN: + bit_length = (int(data).bit_length() + 7) // 8 + return int(data).to_bytes(bit_length, byteorder="big") + + case RegType.REG_LINK: + return data.encode("utf-16le") + + case _: + return data.encode("utf-8") + def __str__(self) -> str: return f"{self.reg_value} ({self.reg_type.name}) {self.reg_data}" @@ -363,12 +426,12 @@ class RegClient(CLIUtil): :param target: can be a hostname, the IPv4 or the IPv6 to connect to :param UPN: the upn to use (DOMAIN/USER, DOMAIN\USER, USER@DOMAIN or USER) + :param password: (string) if provided, used for auth :param guest: use guest mode (over NTLM) :param ssp: if provided, use this SSP for auth. :param kerberos: if available, whether to use Kerberos or not :param kerberos_required: require kerberos :param port: the TCP port. default 445 - :param password: (string) if provided, used for auth :param HashNt: (bytes) if provided, used for auth (NTLM) :param ST: if provided, the service ticket to use (Kerberos) :param KEY: if provided, the session key associated to the ticket (Kerberos) @@ -376,7 +439,6 @@ class RegClient(CLIUtil): :param rootKey: the root key to get a handle to (HKLM, HKCU, etc.), in CLI mode you can chose it later :param subKey: the subkey to use (default None, in CLI mode you can chose it later) - Some additional SMB parameters are available under help(SMB_Client). Some of them include the following: @@ -392,6 +454,8 @@ def __init__( kerberos: bool = True, kerberos_required: bool = False, HashNt: str = None, + HashAes128Sha96: str = None, + HashAes256Sha96: str = None, port: int = 445, timeout: int = 2, debug: int = 0, @@ -404,70 +468,25 @@ def __init__( # SMB arguments **kwargs, ): + if cli: self._depcheck() - hostname = None - # Check if target is a hostname / Check IP - if ":" in target: - family = socket.AF_INET6 - if not valid_ip6(target): - hostname = target - target = str(Net6(target)) - else: - family = socket.AF_INET - if not valid_ip(target): - hostname = target - target = str(Net(target)) assert UPN or ssp or guest, "Either UPN, ssp or guest must be provided !" # Do we need to build a SSP? if ssp is None: # Create the SSP (only if not guest mode) if not guest: - # Check UPN - try: - _, realm = _parse_upn(UPN) - if realm == ".": - # Local - kerberos = False - except ValueError: - # not a UPN: NTLM - kerberos = False - # Do we need to ask the password? - if HashNt is None and password is None and ST is None: - # yes. - from prompt_toolkit import prompt - - password = prompt("Password: ", is_password=True) - ssps = [] - # Kerberos - if kerberos and hostname: - if ST is None: - resp = krb_as_and_tgs( - upn=UPN, - spn="cifs/%s" % hostname, - password=password, - debug=debug, - ) - if resp is not None: - ST, KEY = resp.tgsrep.ticket, resp.sessionkey - if ST: - ssps.append(KerberosSSP(UPN=UPN, ST=ST, KEY=KEY, debug=debug)) - elif kerberos_required: - raise ValueError( - "Kerberos required but target isn't a hostname !" - ) - elif kerberos_required: - raise ValueError( - "Kerberos required but domain not specified in the UPN, " - "or target isn't a hostname !" - ) - # NTLM - if not kerberos_required: - if HashNt is None and password is not None: - HashNt = MD4le(password) - ssps.append(NTLMSSP(UPN=UPN, HASHNT=HashNt)) - # Build the SSP - ssp = SPNEGOSSP(ssps) + ssp = SPNEGOSSP.from_cli_arguments( + UPN=UPN, + target=target, + password=password, + HashNt=HashNt, + HashAes256Sha96=HashAes256Sha96, + HashAes128Sha96=HashAes128Sha96, + ST=ST, + KEY=KEY, + kerberos_required=kerberos_required, + ) else: # Guest mode ssp = None @@ -497,17 +516,48 @@ def __init__( self.timeout = timeout self.client.verb = False try: - self.client.connect(target) + self.client.connect(target, timeout=self.timeout) + from time import sleep + + sleep(1.5) self.client.open_smbpipe("winreg") + self.client.bind(self.interface) + except ValueError as exc: - if "3221225644" in str(exc): + print( + f"[!] Warn: Remote service didn't seem to be running. Let's try again now that we should have trigger it. ({exc})" + ) + from time import sleep + + sleep(1.5) + self.client.open_smbpipe("winreg") + self.client.bind(self.interface) + except Scapy_Exception as e: + if str(3221225566) in str(e): print( - "[!] Warn: Remote service didn't seem to be running. Let's try again now that we should have trigger it." + f""" + [!] STATUS_LOGON_FAILURE - {e} You used: + - UPN {UPN}, + - password {password}, + - target {target}, + - guest {guest}, + - kerberos {kerberos}, + - kerberos_required {kerberos_required}, + - HashNt {HashNt}, + - HashAes128Sha96 {HashAes128Sha96}, + - HashAes256Sha96 {HashAes256Sha96}, + - ST {ST}, + - KEY {KEY} + + [💡 TIPS] If you want to use a local account you may use something like: UPN = "WORKGROUP\\\\Administrator" or UPN = "Administrator@WORKGROUP" or "Administrator@192.168.1.2" +""" ) - self.client.open_smbpipe("winreg") - else: - raise exc - self.client.bind(self.interface) + exit() + except TimeoutError as exc: + print( + f"[!] Timeout while connecting to {target}:{port}. Check service status. {exc}" + ) + self.cache: dict[str : dict[str, CacheElt]] = { "ls": dict(), "cat": dict(), @@ -551,21 +601,24 @@ def use(self, root_path): """ Selects and sets the base registry key (root) to use for subsequent operations. - Parameters: - root_path (str): The root registry path to use. Should start with one of the following: - - HKEY_CLASSES_ROOT - - HKEY_LOCAL_MACHINE - - HKEY_CURRENT_USER - Behavior: - Determines which registry root to use based on the prefix of `root_path`. - Opens the corresponding registry root handle if not already opened, using the appropriate request. - - Sets `self.current_root_handle` and `self.current_root_path` to the selected root. - Clears the local subkey cache - Changes the current directory to the root of the selected registry hive. + + :param root_path: The root registry path to use. Should start with one of the following: + - HKCROOT + - HKLM + - HKCU + - HKC + - HKU + - HKPERFDATA + - HKPERFTXT + - HKPERFNLSTXT """ - default_read_access_rights = READ_ACCESS_RIGHTS + default_access_rights = AccessRights.KEY_READ root_path = RootKeys(root_path.upper().strip()) match root_path: @@ -576,7 +629,7 @@ def use(self, root_path): self.client.sr1_req( OpenClassesRoot_Request( ServerName=None, - samDesired=default_read_access_rights, + samDesired=default_access_rights, ndr64=True, ), timeout=self.timeout, @@ -590,7 +643,7 @@ def use(self, root_path): self.client.sr1_req( OpenCurrentUser_Request( ServerName=None, - samDesired=default_read_access_rights, + samDesired=default_access_rights, ndr64=True, ), timeout=self.timeout, @@ -604,7 +657,7 @@ def use(self, root_path): self.client.sr1_req( OpenLocalMachine_Request( ServerName=None, - samDesired=default_read_access_rights, + samDesired=default_access_rights, ndr64=True, ), timeout=self.timeout, @@ -618,7 +671,7 @@ def use(self, root_path): self.client.sr1_req( OpenCurrentConfig_Request( ServerName=None, - samDesired=default_read_access_rights, + samDesired=default_access_rights, ndr64=True, ), timeout=self.timeout, @@ -631,7 +684,7 @@ def use(self, root_path): self.client.sr1_req( OpenUsers_Request( ServerName=None, - samDesired=default_read_access_rights, + samDesired=default_access_rights, ndr64=True, ), timeout=self.timeout, @@ -644,7 +697,7 @@ def use(self, root_path): self.client.sr1_req( OpenPerformanceData_Request( ServerName=None, - samDesired=default_read_access_rights, + samDesired=default_access_rights, ndr64=True, ), timeout=self.timeout, @@ -657,7 +710,7 @@ def use(self, root_path): self.client.sr1_req( OpenPerformanceText_Request( ServerName=None, - samDesired=default_read_access_rights, + samDesired=default_access_rights, ndr64=True, ), timeout=self.timeout, @@ -670,7 +723,7 @@ def use(self, root_path): self.client.sr1_req( OpenPerformanceNlsText_Request( ServerName=None, - samDesired=default_read_access_rights, + samDesired=default_access_rights, ndr64=True, ), timeout=self.timeout, @@ -705,12 +758,12 @@ def use_complete(self, root_key: str) -> list[str]: # List and Cat # --------------------------------------------- # @CLIUtil.addcommand(spaces=True) - def ls(self, folder: Optional[str] = None) -> list[str]: + def ls(self, subkey: str | None = None) -> list[str]: """ EnumKeys of the current subkey path """ - res = self._get_cached_elt(folder=folder, cache_name="ls") + res = self._get_cached_elt(subkey=subkey, cache_name="ls") if res is None: return [] elif len(res.values) != 0: @@ -718,10 +771,10 @@ def ls(self, folder: Optional[str] = None) -> list[str]: # no need to query again the RPC return res.values - if folder is None: - folder = "" + if subkey is None: + subkey = "" - subkey_path = self._join_path(self.current_subkey_path, folder) + subkey_path = self._join_path(self.current_subkey_path, subkey) idx = 0 while True: @@ -760,28 +813,36 @@ def ls_output(self, results: list[str]) -> NoReturn: print(subkey) @CLIUtil.addcomplete(ls) - def ls_complete(self, folder: str) -> list[str]: + def ls_complete(self, subkey: str) -> list[str]: """ Auto-complete ls """ if self._require_root_handles(silent=True): return [] + + subkey = subkey.strip().replace("/", "\\") + if "\\" in subkey: + parent = "\\".join(subkey.split("\\")[:-1]) + subkey = subkey.split("\\")[-1] + else: + parent = "" + return [ - str(subk) - for subk in self.ls() - if str(subk).lower().startswith(folder.lower()) + str(self._join_path(parent, str(subk))) + for subk in self.ls(parent) + if str(subk).lower().startswith(subkey.lower()) ] @CLIUtil.addcommand(spaces=True) - def cat(self, folder: Optional[str] = None) -> list[tuple[str, str]]: + def cat(self, subkey: str | None = None) -> list[tuple[str, str]]: """ Enumerates and retrieves registry values for a given subkey path. - If no folder is specified, uses the current subkey path and caches results to avoid redundant RPC queries. - Otherwise, enumerates values under the specified folder path. + If no subkey is specified, uses the current subkey path and caches results to avoid redundant RPC queries. + Otherwise, enumerates values under the specified subkey path. Args: - folder (Optional[str]): The subkey path to enumerate. If None or empty, uses the current subkey path. + subkey (str | None): The subkey path to enumerate. If None or empty, uses the current subkey path. Returns: list[tuple[str, str]]: A list of registry entries (as RegEntry objects) for the specified subkey path. @@ -791,7 +852,7 @@ def cat(self, folder: Optional[str] = None) -> list[tuple[str, str]]: - May print error messages to standard output if RPC queries fail. - Updates internal cache for previously enumerated subkey paths. """ - res = self._get_cached_elt(folder=folder, cache_name="cat") + res = self._get_cached_elt(subkey=subkey, cache_name="cat") if res is None: return [] elif len(res.values) != 0: @@ -799,7 +860,7 @@ def cat(self, folder: Optional[str] = None) -> list[tuple[str, str]]: # no need to query again the RPC return res.values - subkey_path = self._join_path(self.current_subkey_path, folder) + subkey_path = self._join_path(self.current_subkey_path, subkey) idx = 0 while True: @@ -892,17 +953,11 @@ def cat_output(self, results: list[RegEntry]) -> None: ) @CLIUtil.addcomplete(cat) - def cat_complete(self, folder: str) -> list[str]: + def cat_complete(self, subkey: str) -> list[str]: """ Auto-complete cat """ - if self._require_root_handles(silent=True): - return [] - return [ - str(subk) - for subk in self.ls() - if str(subk).lower().startswith(folder.lower()) - ] + return self.ls_complete(subkey) # --------------------------------------------- # # Change Directory @@ -920,7 +975,7 @@ def cd(self, subkey: str) -> None: else: res = self._get_cached_elt( - folder=subkey, + subkey=subkey, cache_name="cd", ) tmp_handle = res.handle if res else None @@ -933,29 +988,23 @@ def cd(self, subkey: str) -> None: self.current_subkey_handle = tmp_handle @CLIUtil.addcomplete(cd) - def cd_complete(self, folder: str) -> list[str]: + def cd_complete(self, subkey: str) -> list[str]: """ Auto-complete cd """ - if self._require_root_handles(silent=True): - return [] - return [ - str(subk) - for subk in self.ls() - if str(subk).lower().startswith(folder.lower()) - ] + return self.ls_complete(subkey) # --------------------------------------------- # # Get Information # --------------------------------------------- # @CLIUtil.addcommand() - def get_sd(self, folder: Optional[str] = None) -> Optional[SECURITY_DESCRIPTOR]: + def get_sd(self, subkey: str | None = None) -> SECURITY_DESCRIPTOR | None: """ Get the security descriptor of the current subkey. SACL are not retrieve at this point (TODO). """ - handle = self._get_cached_elt(folder=folder) + handle = self._get_cached_elt(subkey=subkey) if handle is None: return None @@ -984,20 +1033,36 @@ def get_sd(self, folder: Optional[str] = None) -> Optional[SECURITY_DESCRIPTOR]: results = resp.pRpcSecurityDescriptorOut.valueof("lpSecurityDescriptor") sd = SECURITY_DESCRIPTOR(results) - print("Owner:", sd.OwnerSid.summary()) - print("Group:", sd.GroupSid.summary()) - if getattr(sd, "DACL", None): - print("DACL:") - for ace in sd.DACL.Aces: - print(" - ", ace.toSDDL()) return sd + @CLIUtil.addoutput(get_sd) + def get_sd_output(self, sd: SECURITY_DESCRIPTOR | None) -> None: + """ + Print the output of 'get_sd' + """ + if sd is None: + print("No security descriptor found.") + return + else: + print("Owner:", sd.OwnerSid.summary()) + print("Group:", sd.GroupSid.summary()) + if getattr(sd, "DACL", None): + print("DACL:") + for ace in sd.DACL.Aces: + print(" - ", ace.toSDDL()) + @CLIUtil.addcommand() - def query_info(self, folder: Optional[str] = None) -> None: + def query_info( + self, subkey: str | None = None + ) -> BaseRegQueryInfoKey_Response | None: """ Query information on the current subkey + + :param subkey: The subkey to query. If None, it uses the current subkey path. + :return: BaseRegQueryInfoKey_Response object containing information about the subkey. + Returns None if the handle is invalid or an error occurs during the query. """ - handle = self._get_cached_elt(folder) + handle = self._get_cached_elt(subkey) if handle is None: logger.error("Could not get handle on the specified subkey.") return None @@ -1012,44 +1077,116 @@ def query_info(self, folder: Optional[str] = None) -> None: if not is_status_ok(resp.status): logger.error("Got status %s while querying info", hex(resp.status)) return None + return resp + + @CLIUtil.addoutput(query_info) + def query_info_output(self, info: None) -> None: + """ + Print the output of 'query_info' + """ + if info is None: + print("No information found.") + return print( f""" -Info on key: {self.current_subkey_path} - - Number of subkeys: {resp.lpcSubKeys} - - Length of the longuest subkey name (in bytes): {resp.lpcbMaxSubKeyLen} - - Number of values: {resp.lpcValues} - - Length of the longest value name (in bytes): {resp.lpcbMaxValueNameLen} - - Last write time: {from_filetime_to_datetime(resp.lpftLastWriteTime)} +Info on key: + - Number of subkeys: {info.lpcSubKeys} + - Length of the longest subkey name (in bytes): {info.lpcbMaxSubKeyLen} + - Number of values: {info.lpcValues} + - Length of the longest value name (in bytes): {info.lpcbMaxValueNameLen} + - Last write time: {from_filetime_to_datetime(info.lpftLastWriteTime)} """ ) @CLIUtil.addcommand() - def version(self): + def version(self) -> NDRIntField: """ Get remote registry server version """ - version = self.client.sr1_req( + return self.client.sr1_req( BaseRegGetVersion_Request(hKey=self.current_root_handle) ).lpdwVersion + + @CLIUtil.addoutput(version) + def version_output(self, version: int) -> None: + """ + Print the output of 'version' + """ print(f"Remote registry server version: {version}") + # --------------------------------------------- # + # Modify # + # --------------------------------------------- # + + @CLIUtil.addcommand() + def set_value( + self, + value_name: str, + value_type: RegType | str, + value_data: str, + subkey: str | None = None, + ) -> None: + """ + Set a registry value in the current subkey. + If no subkey is specified, it uses the current subkey path. + """ + try: + value_type = RegType.fromvalue(value_type) + except ValueError: + logger.error("Unknown registry type: %s", value_type) + return None + + data = RegEntry.encode_data(value_type, value_data) + + handle = self._get_cached_elt( + subkey=subkey, desired_access=AccessRights.KEY_WRITE + ) + if handle is None: + logger.error("Could not get handle on the specified subkey.") + return None + + req = BaseRegSetValue_Request( + hKey=handle, + lpValueName=RPC_UNICODE_STRING(Buffer=value_name), + dwType=value_type.value, + lpData=NDRPointer( + value=NDRConformantArray(value=NDRVaryingArray(value=data)) + ), + ndr64=True, + ) + + resp = self.client.sr1_req(req) + if not is_status_ok(resp.status): + logger.error("Got status %s while setting value", hex(resp.status)) + return None + + breakpoint() + # --------------------------------------------- # # Backup and Restore # --------------------------------------------- # @CLIUtil.addcommand() def save( - self, folder: Optional[str] = None, output_path: Optional[str] = None + self, + output_path: str | None = None, + subkey: str | None = None, + fsecurity: bool = False, ) -> None: """ - Backup the current subkey to a file. - If no folder is specified, it uses the current subkey path. + Backup the current subkey to a file. If no subkey is specified, it uses the current subkey path. If no output_path is specified, + it will be saved in the `%WINDIR%\\System32` directory with the name of the subkey and .reg extension. + + :param output_path: The path to save the backup file. If None, it defaults to the current subkey name with .reg extension. + If the output path ends with .reg, it uses it as is, otherwise it appends .reg to the output path. + :param subkey: The subkey to backup. If None, it uses the current subkey path. + :return: None, by default it saves the backup to a file protected so that only BA can read it. """ self.activate_backup() - handle = self._get_cached_elt(folder=folder) + handle = self._get_cached_elt(subkey=subkey) key_to_save = ( - folder.split("\\")[-1] if folder else self.current_subkey_path.name + subkey.split("\\")[-1] if subkey else self.current_subkey_path.name ) if handle is None: @@ -1062,20 +1199,27 @@ def save( elif output_path.endswith(".reg"): # If the output path ends with .reg, we use it as is - output_path = output_path.strip() + output_path = str(self._join_path("", output_path)) else: # Otherwise, we use the current subkey path as the output path output_path = str(self._join_path(output_path, str(key_to_save) + ".reg")) - print(f"Backing up {key_to_save} to {output_path}") + if fsecurity: + print( + "Looks like you don't like security so much. Hope you know what you are doing." + ) + sa = None + else: + sa = PRPC_SECURITY_ATTRIBUTES( + RpcSecurityDescriptor=RPC_SECURITY_DESCRIPTOR( + lpSecurityDescriptor=DEFAULT_SECURITY_DESCRIPTOR, + ), + bInheritHandle=False, + ndr64=True, + ) - sa = PRPC_SECURITY_ATTRIBUTES( - RpcSecurityDescriptor=RPC_SECURITY_DESCRIPTOR( - lpSecurityDescriptor=DEFAULT_SECURITY_DESCRIPTOR, - ), - ndr64=True, - ) + sa.nLength = len(sa) req = BaseRegSaveKey_Request( hKey=handle, lpFile=RPC_UNICODE_STRING(Buffer=output_path), @@ -1083,18 +1227,16 @@ def save( ndr64=True, ) - # If the security attributes are not provided, the default security descriptor is used. - # Meanning the file will inherite the access rights of the its parent directory. - req.show2() resp = self.client.sr1_req(req) - resp.show() if not is_status_ok(resp.status): logger.error("Got status %s while backing up", hex(resp.status)) - return None - - print( - f"Backup of {self.current_subkey_path} saved to {self.current_subkey_path}.reg" - ) + else: + logger.info( + "Backup of %s saved to %s.reg successful ", + self.current_subkey_path, + output_path, + ) + print(f"Backup of {self.current_subkey_path} saved to {output_path}") # --------------------------------------------- # # Operation options @@ -1129,7 +1271,8 @@ def disable_backup(self) -> None: print("Backup option deactivated.") self._clear_all_caches() - def switch_volatile(self) -> None: + @CLIUtil.addcommand() + def activate_volatile(self) -> None: """ Set the registry operations to be volatile. This means that the registry key will be deleted when the system is restarted. @@ -1140,6 +1283,7 @@ def switch_volatile(self) -> None: self._clear_all_caches() + @CLIUtil.addcommand() def disable_volatile(self) -> None: """ Disable the volatile option for the registry operations. @@ -1157,10 +1301,15 @@ def disable_volatile(self) -> None: def get_handle_on_subkey( self, subkey_path: PureWindowsPath, - desired_access_rights: Optional[IntFlag] = None, - ) -> Optional[NDRContextHandle]: + desired_access_rights: IntFlag | None = None, + ) -> NDRContextHandle | None: """ - Ask the remote server to return an handle on a given subkey + Ask the remote server to return an handle on a given subkey. + If no access rights are specified, it defaults to read access rights. + + :param subkey_path: The subkey path to get a handle on. + :param desired_access_rights: The desired access rights for the subkey. If None, defaults to read access rights. + :return: An NDRContextHandle on success, None on failure. """ # If we don't have a root handle, we cannot get a subkey handle # This is a safety check, as we should not be able to call this function @@ -1175,11 +1324,7 @@ def get_handle_on_subkey( # If no access rights were specified, we use the default read access rights if desired_access_rights is None: # Default to read access rights - desired_access_rights = ( - AccessRights.KEY_QUERY_VALUE - | AccessRights.KEY_ENUMERATE_SUB_KEYS - | AccessRights.STANDARD_RIGHTS_READ - ) + desired_access_rights = AccessRights.KEY_READ logger.debug( "Getting handle on subkey: %s with access rights: %s", @@ -1205,29 +1350,36 @@ def get_handle_on_subkey( def _get_cached_elt( self, - folder: Optional[str] = None, + subkey: str | None = None, cache_name: str = None, - desired_access: Optional[IntFlag] = None, - ) -> Optional[NDRContextHandle | CacheElt]: + desired_access: IntFlag | None = None, + ) -> NDRContextHandle | CacheElt | None: """ - Get the handle on the current subkey or the specified folder. - If no folder is specified, it uses the current subkey path. + Get a cached element for the specified subkey. + + If the element is not cached, it retrieves the handle on the subkey + and caches it for future use. + + :param subkey: The subkey path to retrieve. If None, uses the current subkey path. + :param cache_name: The name of the cache to use. If None, does not use cache. + :param desired_access: The desired access rights for the subkey. If None, defaults to read access rights. + :return: A CacheElt object if cache_name is provided, otherwise an NDRContextHandle. """ if self._require_root_handles(silent=True): return None if desired_access is None: # Default to read access rights - desired_access = READ_ACCESS_RIGHTS + desired_access = AccessRights.KEY_READ - # If no specific folder was specified + # If no specific subkey was specified # we use our current subkey path - if folder is None or folder == "" or folder == ".": + if subkey is None or subkey == "" or subkey == ".": subkey_path = self.current_subkey_path - # Otherwise we use the folder path, + # Otherwise we use the subkey path, # the calling parent shall make sure that this path was properly sanitized else: - subkey_path = self._join_path(self.current_subkey_path, folder) + subkey_path = self._join_path(self.current_subkey_path, subkey) if ( self.cache.get(cache_name, None) is not None @@ -1238,6 +1390,7 @@ def _get_cached_elt( # If the access rights are the same, we return the cached elt return self.cache[cache_name][subkey_path] + # Otherwise, we need to get a new handle on the subkey handle = self.get_handle_on_subkey(subkey_path, desired_access) if handle is None: logger.error("Could not get handle on %s", subkey_path) @@ -1250,6 +1403,15 @@ def _get_cached_elt( return cache_elt if cache_name is not None else handle def _join_path(self, first_path: str, second_path: str) -> PureWindowsPath: + """ + Join two paths in a way that is compatible with Windows paths. + This ensures that the paths are normalized and combined correctly, + even if they are provided as strings or PureWindowsPath objects. + + :param first_path: The first path to join. + :param second_path: The second path to join. + :return: A PureWindowsPath object representing the combined path. + """ return PureWindowsPath( os.path.normpath( os.path.join( @@ -1260,6 +1422,12 @@ def _join_path(self, first_path: str, second_path: str) -> PureWindowsPath: ) def _require_root_handles(self, silent: bool = False) -> bool: + """ + Check if we have a root handle set. + + :param silent: If True, do not print any message if no root handle is set. + :return: True if no root handle is set, False otherwise. + """ if self.current_root_handle is None: if not silent: print("No root key selected ! Use 'use' to use one.") @@ -1270,8 +1438,8 @@ def _clear_all_caches(self) -> None: """ Clear all caches """ - for key in self.cache.keys(): - self.cache[key].clear() + for _, c in self.cache.items(): + c.clear() @CLIUtil.addcommand() def dev(self) -> NoReturn: From d4474b9cdbe00a45e3168925b2a944a869d6fe8a Mon Sep 17 00:00:00 2001 From: Ebrix Date: Sun, 17 Aug 2025 10:37:19 +0200 Subject: [PATCH 14/27] Add set_value --- scapyred/winreg.py | 70 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/scapyred/winreg.py b/scapyred/winreg.py index 77e794f..5e28bff 100644 --- a/scapyred/winreg.py +++ b/scapyred/winreg.py @@ -8,6 +8,7 @@ from enum import IntEnum, IntFlag, StrEnum, Enum from ctypes.wintypes import PFILETIME +from re import sub from typing import NoReturn from pathlib import PureWindowsPath @@ -17,7 +18,11 @@ ) from scapy.layers.msrpce.rpcclient import DCERPC_Client -from scapy.layers.dcerpc import find_dcerpc_interface, DCERPC_Transport +from scapy.layers.dcerpc import ( + find_dcerpc_interface, + DCERPC_Transport, + NDRConformantString, +) from scapy.layers.spnego import SPNEGOSSP from scapy.layers.smb2 import ( SECURITY_DESCRIPTOR, @@ -62,8 +67,8 @@ PRPC_SECURITY_DESCRIPTOR, PRPC_SECURITY_ATTRIBUTES, RPC_SECURITY_DESCRIPTOR, - NDRContextHandle, RPC_UNICODE_STRING, + NDRContextHandle, NDRIntField, ) @@ -202,6 +207,33 @@ def __new__(cls, value): obj._value_ = normalized return obj + @classmethod + def from_value(cls, value: str): + """Convert a string to a RootKeys enum member.""" + value = value.strip().upper() + match value: + case "HKEY_CLASSES_ROOT": + value = RootKeys.HKEY_CLASSES_ROOT.value + case "HKEY_CURRENT_USER": + value = RootKeys.HKEY_CURRENT_USER.value + case "HKEY_LOCAL_MACHINE": + value = RootKeys.HKEY_LOCAL_MACHINE.value + case "HKEY_CURRENT_CONFIG": + value = RootKeys.HKEY_CURRENT_CONFIG.value + case "HKEY_USERS": + value = RootKeys.HKEY_USERS.value + case "HKEY_PERFORMANCE_DATA": + value = RootKeys.HKEY_PERFORMANCE_DATA.value + case "HKEY_PERFORMANCE_TEXT": + value = RootKeys.HKEY_PERFORMANCE_TEXT.value + case "HKEY_PERFORMANCE_NLSTEXT": + value = RootKeys.HKEY_PERFORMANCE_NLSTEXT.value + + try: + return cls(value) + except ValueError: + print(f"Unknown root key: {value}.") + class RegType(IntEnum): """ @@ -232,15 +264,15 @@ def fromvalue(cls, value: str | int) -> "RegType": :return: The corresponding RegType enum member. """ if isinstance(value, int): - breakpoint() try: return cls(value) except ValueError: print(f"Unknown registry type: {value}, using UNK") + return cls.UNK value = value.strip().upper() try: - return cls[int(value)] + return cls(int(value)) except (ValueError, KeyError): print(f"Unknown registry type: {value}, using UNK") return cls.UNK @@ -350,7 +382,7 @@ def __init__(self, reg_value: str, reg_type: int, reg_data: bytes): case RegType.REG_MULTI_SZ | RegType.REG_SZ | RegType.REG_EXPAND_SZ: if self.reg_type == RegType.REG_MULTI_SZ: # decode multiple null terminated strings - self.reg_data = reg_data.decode("utf-16le")[:-2].replace( + self.reg_data = reg_data.decode("utf-16le")[:-1].replace( "\x00", "\n" ) else: @@ -380,12 +412,12 @@ def encode_data(reg_type: RegType, data: str) -> bytes: case RegType.REG_MULTI_SZ | RegType.REG_SZ | RegType.REG_EXPAND_SZ: if reg_type == RegType.REG_MULTI_SZ: # decode multiple null terminated strings - return data.replace("\n", "\x00").encode("utf-16le") + b"\x00\x00" + return data.replace("\\n", "\x00").encode("utf-16le") + b"\x00\x00" else: - return data.decode("utf-16le") + return data.encode("utf-16le") case RegType.REG_BINARY: - return data + return data.encode("utf-8").decode("unicode_escape").encode("latin1") case RegType.REG_DWORD | RegType.REG_QWORD: bit_length = (int(data).bit_length() + 7) // 8 @@ -399,7 +431,7 @@ def encode_data(reg_type: RegType, data: str) -> bytes: return data.encode("utf-16le") case _: - return data.encode("utf-8") + return data.encode("utf-8").decode("unicode_escape").encode("latin1") def __str__(self) -> str: return f"{self.reg_value} ({self.reg_type.name}) {self.reg_data}" @@ -949,7 +981,7 @@ def cat_output(self, results: list[RegEntry]) -> None: for entry in results: print( - f" - {entry.reg_value:<20} {'(' + entry.reg_type.name + ')':<15} {entry.reg_data}" + f" - {entry.reg_value:<20} {'(' + entry.reg_type.name + " - " + str(entry.reg_type.value) + ')':<15} {entry.reg_data}" ) @CLIUtil.addcomplete(cat) @@ -1145,24 +1177,28 @@ def set_value( if handle is None: logger.error("Could not get handle on the specified subkey.") return None - req = BaseRegSetValue_Request( hKey=handle, - lpValueName=RPC_UNICODE_STRING(Buffer=value_name), + lpValueName=RPC_UNICODE_STRING(Buffer=value_name + "\x00"), dwType=value_type.value, - lpData=NDRPointer( - value=NDRConformantArray(value=NDRVaryingArray(value=data)) - ), + lpData=data, ndr64=True, ) resp = self.client.sr1_req(req) + # We remove the entry from the cache if it exists + # Even if the response status is not OK, we want to remove it + if subkey is None: + subkey_path = self.current_subkey_path + else: + subkey_path = self._join_path(self.current_subkey_path, subkey) + if subkey_path in self.cache["cat"]: + self.cache["cat"].pop(subkey_path, None) + if not is_status_ok(resp.status): logger.error("Got status %s while setting value", hex(resp.status)) return None - breakpoint() - # --------------------------------------------- # # Backup and Restore # --------------------------------------------- # From 9dc8de1efd474a3f18ba154e96deab4e6da7c02a Mon Sep 17 00:00:00 2001 From: Ebrix Date: Mon, 25 Aug 2025 07:26:08 +0200 Subject: [PATCH 15/27] delete key and value and exploration mode --- scapyred/winreg.py | 344 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 305 insertions(+), 39 deletions(-) diff --git a/scapyred/winreg.py b/scapyred/winreg.py index 5e28bff..7579449 100644 --- a/scapyred/winreg.py +++ b/scapyred/winreg.py @@ -1,16 +1,24 @@ """ -DCE/RPC client +Wrapper for the Windows Registry (winreg) using DCERPC +This module provides a client for interacting with the Windows Registry over DCERPC. +It allows for operations such as opening keys, enumerating subkeys and values, querying values, +setting values, and deleting keys or values. + +The client supports authentication via NTLM or Kerberos, and can operate in a CLI mode. +It also provides utility functions for handling registry data types and error codes. +It is designed to be used with Scapy's DCERPC framework. """ from dataclasses import dataclass import os import logging +import sys from enum import IntEnum, IntFlag, StrEnum, Enum from ctypes.wintypes import PFILETIME -from re import sub from typing import NoReturn from pathlib import PureWindowsPath +from time import sleep from scapy.themes import DefaultTheme from scapy.utils import ( @@ -21,7 +29,6 @@ from scapy.layers.dcerpc import ( find_dcerpc_interface, DCERPC_Transport, - NDRConformantString, ) from scapy.layers.spnego import SPNEGOSSP from scapy.layers.smb2 import ( @@ -64,6 +71,9 @@ BaseRegGetKeySecurity_Request, BaseRegSaveKey_Request, BaseRegSetValue_Request, + BaseRegCreateKey_Request, + BaseRegDeleteKey_Request, + BaseRegDeleteValue_Request, PRPC_SECURITY_DESCRIPTOR, PRPC_SECURITY_ATTRIBUTES, RPC_SECURITY_DESCRIPTOR, @@ -83,32 +93,132 @@ logger = logging.getLogger(__name__) -class AccessRights(IntFlag): +class GenericAccessRights(IntFlag): + """ + Generic access rights: + https://learn.microsoft.com/en-us/windows/win32/secauthz/generic-access-rights + """ + + GENERIC_READ = 0x80000000 + GENERIC_WRITE = 0x40000000 + GENERIC_EXECUTE = 0x20000000 + GENERIC_ALL = 0x10000000 + MAXIMUM_ALLOWED = 0x02000000 + ACCESS_SACL = 0x01000000 + + +class StandardAccessRights(IntFlag): + """ + Standard access rights: + https://learn.microsoft.com/en-us/windows/win32/secauthz/standard-access-rights """ - Access rights for registry keys + + DELETE = 0x00010000 + READ_CONTROL = 0x00020000 + WRITE_DAC = 0x00040000 + WRITE_OWNER = 0x00080000 + SYNCHRONIZE = 0x00100000 + + STANDARD_RIGHTS_REQUIRED = DELETE | READ_CONTROL | WRITE_DAC | WRITE_OWNER + STANDARD_RIGHTS_ALL = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE + + STANDARD_RIGHTS_READ = READ_CONTROL + STANDARD_RIGHTS_WRITE = READ_CONTROL + STANDARD_RIGHTS_EXECUTE = READ_CONTROL + SPECIFIC_RIGHTS_ALL = 0x0000FFFF + + +class SpecificAccessRights(IntFlag): + """ + Access rights for registry keys: + https://learn.microsoft.com/en-us/windows/win32/sysinfo/registry-key-security-and-access-rights """ - # Access rights for registry keys - # These constants are used to specify the access rights when opening or creating registry keys. KEY_QUERY_VALUE = 0x00000001 + KEY_SET_VALUE = 0x00000002 + KEY_CREATE_SUB_KEY = 0x00000004 KEY_ENUMERATE_SUB_KEYS = 0x00000008 - KEY_ALL_ACCESS = 0xF003F # Combines the STANDARD_RIGHTS_REQUIRED, KEY_QUERY_VALUE, KEY_SET_VALUE, KEY_CREATE_SUB_KEY, KEY_ENUMERATE_SUB_KEYS, KEY_NOTIFY, and KEY_CREATE_LINK access rights. - STANDARD_RIGHTS_READ = 0x00020000 - KEY_CREATE_LINK = 0x0020 - KEY_NOTIFY = 0x0010 - KEY_READ = 0x20019 # Combines the STANDARD_RIGHTS_READ, KEY_QUERY_VALUE, KEY_ENUMERATE_SUB_KEYS, and KEY_NOTIFY values. - KEY_EXECUTE = 0x20019 # Equivalent to KEY_READ. - KEY_CREATE_SUB_KEY = 0x0004 # Required to create a subkey of a registry key. - KEY_SET_VALUE = 0x0002 # Required to create, delete, or set a registry value. - KEY_WOW64_32KEY = 0x0200 # Indicates that an application on 64-bit Windows should operate on the 32-bit registry view. - # This flag is ignored by 32-bit Windows. For more information, see Accessing an Alternate Registry View. - # This flag must be combined using the OR operator with the other flags in this table that either query or access registry values. - # Windows 2000: This flag is not supported. - KEY_WOW64_64KEY = 0x0100 # Indicates that an application on 64-bit Windows should operate on the 64-bit registry view. - # This flag is ignored by 32-bit Windows. For more information, see Accessing an Alternate Registry View. - # This flag must be combined using the OR operator with the other flags in this table that either query or access registry values. - # Windows 2000: This flag is not supported. - KEY_WRITE = 0x20006 # Combines the STANDARD_RIGHTS_WRITE, KEY_SET_VALUE, and KEY_CREATE_SUB_KEY access rights. + KEY_NOTIFY = 0x00000010 + KEY_CREATE_LINK = 0x00000020 + KEY_WOW64_64KEY = 0x0100 + KEY_WOW64_32KEY = 0x0200 + KEY_READ = ( + StandardAccessRights.STANDARD_RIGHTS_READ + | KEY_QUERY_VALUE + | KEY_ENUMERATE_SUB_KEYS + | KEY_NOTIFY + ) + KEY_EXECUTE = KEY_READ + + +class AccessRights(IntFlag): + """ + Combines generic, standard, and specific access rights for registry keys. + """ + + # Generic + GENERIC_READ = GenericAccessRights.GENERIC_READ + GENERIC_WRITE = GenericAccessRights.GENERIC_WRITE + GENERIC_EXECUTE = GenericAccessRights.GENERIC_EXECUTE + GENERIC_ALL = GenericAccessRights.GENERIC_ALL + MAXIMUM_ALLOWED = GenericAccessRights.MAXIMUM_ALLOWED + ACCESS_SACL = GenericAccessRights.ACCESS_SACL + + # Standard + DELETE = StandardAccessRights.DELETE + READ_CONTROL = StandardAccessRights.READ_CONTROL + WRITE_DAC = StandardAccessRights.WRITE_DAC + WRITE_OWNER = StandardAccessRights.WRITE_OWNER + SYNCHRONIZE = StandardAccessRights.SYNCHRONIZE + STANDARD_RIGHTS_REQUIRED = ( + StandardAccessRights.DELETE + | StandardAccessRights.READ_CONTROL + | StandardAccessRights.WRITE_DAC + | StandardAccessRights.WRITE_OWNER + ) + STANDARD_RIGHTS_READ = StandardAccessRights.READ_CONTROL + STANDARD_RIGHTS_WRITE = StandardAccessRights.READ_CONTROL + STANDARD_RIGHTS_EXECUTE = StandardAccessRights.READ_CONTROL + STANDARD_RIGHTS_ALL = ( + StandardAccessRights.DELETE + | StandardAccessRights.READ_CONTROL + | StandardAccessRights.WRITE_DAC + | StandardAccessRights.WRITE_OWNER + | StandardAccessRights.SYNCHRONIZE + ) + SPECIFIC_RIGHTS_ALL = StandardAccessRights.SPECIFIC_RIGHTS_ALL + + # Specific + KEY_QUERY_VALUE = SpecificAccessRights.KEY_QUERY_VALUE + KEY_SET_VALUE = SpecificAccessRights.KEY_SET_VALUE + KEY_CREATE_SUB_KEY = SpecificAccessRights.KEY_CREATE_SUB_KEY + KEY_ENUMERATE_SUB_KEYS = SpecificAccessRights.KEY_ENUMERATE_SUB_KEYS + KEY_NOTIFY = SpecificAccessRights.KEY_NOTIFY + KEY_CREATE_LINK = SpecificAccessRights.KEY_CREATE_LINK + KEY_WOW64_64KEY = SpecificAccessRights.KEY_WOW64_64KEY + KEY_WOW64_32KEY = SpecificAccessRights.KEY_WOW64_32KEY + + KEY_READ = ( + StandardAccessRights.READ_CONTROL + | SpecificAccessRights.KEY_QUERY_VALUE + | SpecificAccessRights.KEY_ENUMERATE_SUB_KEYS + | SpecificAccessRights.KEY_NOTIFY + ) + KEY_EXECUTE = KEY_READ + KEY_WRITE = ( + STANDARD_RIGHTS_ALL + | SpecificAccessRights.KEY_SET_VALUE + | SpecificAccessRights.KEY_CREATE_SUB_KEY + ) + KEY_ALL_ACCESS = ( + STANDARD_RIGHTS_REQUIRED + | SpecificAccessRights.KEY_QUERY_VALUE + | SpecificAccessRights.KEY_SET_VALUE + | SpecificAccessRights.KEY_CREATE_SUB_KEY + | SpecificAccessRights.KEY_ENUMERATE_SUB_KEYS + | SpecificAccessRights.KEY_NOTIFY + | SpecificAccessRights.KEY_CREATE_LINK + ) class RegOptions(IntFlag): @@ -139,6 +249,7 @@ class ErrorCodes(IntEnum): ERROR_INVALID_PARAMETER = 0x00000057 ERROR_CALL_NOT_IMPLEMENTED = 0x00000057 ERROR_INVALID_NAME = 0x0000007B + ERROR_BAD_PATHNAME = 0x000000A1 ERROR_ALREADY_EXISTS = 0x000000B7 ERROR_NO_MORE_ITEMS = 0x00000103 ERROR_NOACCESS = 0x000003E6 @@ -255,7 +366,17 @@ class RegType(IntEnum): @classmethod def _missing_(cls, value): print(f"Unknown registry type: {value}, using UNK") - return cls.UNK + unk = cls.UNK + unk.real_value = value + return unk + + def __new__(cls, value, real_value=None): + obj = int.__new__(cls, value) + obj._value_ = value + if real_value is None: + real_value = value + obj.real_value = real_value + return obj @classmethod def fromvalue(cls, value: str | int) -> "RegType": @@ -549,7 +670,6 @@ def __init__( self.client.verb = False try: self.client.connect(target, timeout=self.timeout) - from time import sleep sleep(1.5) self.client.open_smbpipe("winreg") @@ -559,7 +679,6 @@ def __init__( print( f"[!] Warn: Remote service didn't seem to be running. Let's try again now that we should have trigger it. ({exc})" ) - from time import sleep sleep(1.5) self.client.open_smbpipe("winreg") @@ -589,6 +708,7 @@ def __init__( print( f"[!] Timeout while connecting to {target}:{port}. Check service status. {exc}" ) + sys.exit(-1) self.cache: dict[str : dict[str, CacheElt]] = { "ls": dict(), @@ -601,7 +721,9 @@ def __init__( self.root_handle = {} self.current_root_handle = None self.current_subkey_handle = None + self.exploration_mode = False self.current_subkey_path: PureWindowsPath = PureWindowsPath("") + self.sam_requested_access_rights = AccessRights.MAXIMUM_ALLOWED if rootKey in AVAILABLE_ROOT_KEYS: self.current_root_path = rootKey.strip() self.use(self.current_root_path) @@ -616,7 +738,7 @@ def ps1(self) -> str: return f"[reg] {self.current_root_path}\\{self.current_subkey_path} > " @CLIUtil.addcommand() - def close(self) -> NoReturn: + def close(self) -> None: """ Close all connections """ @@ -650,7 +772,6 @@ def use(self, root_path): - HKPERFNLSTXT """ - default_access_rights = AccessRights.KEY_READ root_path = RootKeys(root_path.upper().strip()) match root_path: @@ -661,7 +782,7 @@ def use(self, root_path): self.client.sr1_req( OpenClassesRoot_Request( ServerName=None, - samDesired=default_access_rights, + samDesired=self.sam_requested_access_rights, ndr64=True, ), timeout=self.timeout, @@ -675,7 +796,7 @@ def use(self, root_path): self.client.sr1_req( OpenCurrentUser_Request( ServerName=None, - samDesired=default_access_rights, + samDesired=self.sam_requested_access_rights, ndr64=True, ), timeout=self.timeout, @@ -689,7 +810,7 @@ def use(self, root_path): self.client.sr1_req( OpenLocalMachine_Request( ServerName=None, - samDesired=default_access_rights, + samDesired=self.sam_requested_access_rights, ndr64=True, ), timeout=self.timeout, @@ -703,7 +824,7 @@ def use(self, root_path): self.client.sr1_req( OpenCurrentConfig_Request( ServerName=None, - samDesired=default_access_rights, + samDesired=self.sam_requested_access_rights, ndr64=True, ), timeout=self.timeout, @@ -716,7 +837,7 @@ def use(self, root_path): self.client.sr1_req( OpenUsers_Request( ServerName=None, - samDesired=default_access_rights, + samDesired=self.sam_requested_access_rights, ndr64=True, ), timeout=self.timeout, @@ -729,7 +850,7 @@ def use(self, root_path): self.client.sr1_req( OpenPerformanceData_Request( ServerName=None, - samDesired=default_access_rights, + samDesired=self.sam_requested_access_rights, ndr64=True, ), timeout=self.timeout, @@ -742,7 +863,7 @@ def use(self, root_path): self.client.sr1_req( OpenPerformanceText_Request( ServerName=None, - samDesired=default_access_rights, + samDesired=self.sam_requested_access_rights, ndr64=True, ), timeout=self.timeout, @@ -755,7 +876,7 @@ def use(self, root_path): self.client.sr1_req( OpenPerformanceNlsText_Request( ServerName=None, - samDesired=default_access_rights, + samDesired=self.sam_requested_access_rights, ndr64=True, ), timeout=self.timeout, @@ -789,6 +910,7 @@ def use_complete(self, root_key: str) -> list[str]: # --------------------------------------------- # # List and Cat # --------------------------------------------- # + @CLIUtil.addcommand(spaces=True) def ls(self, subkey: str | None = None) -> list[str]: """ @@ -837,7 +959,7 @@ def ls(self, subkey: str | None = None) -> list[str]: return self.cache["ls"][subkey_path].values @CLIUtil.addoutput(ls) - def ls_output(self, results: list[str]) -> NoReturn: + def ls_output(self, results: list[str]) -> None: """ Print the output of 'ls' """ @@ -980,6 +1102,10 @@ def cat_output(self, results: list[RegEntry]) -> None: return for entry in results: + if entry.reg_type == RegType.UNK: + print( + f" - {entry.reg_value:<20} {'(' + entry.reg_type.name + " - " + str(entry.reg_type.real_value) + ')':<15} {entry.reg_data}" + ) print( f" - {entry.reg_value:<20} {'(' + entry.reg_type.name + " - " + str(entry.reg_type.value) + ')':<15} {entry.reg_data}" ) @@ -1019,6 +1145,10 @@ def cd(self, subkey: str) -> None: self.current_subkey_path = tmp_path self.current_subkey_handle = tmp_handle + if self.exploration_mode: + # force the trigger of the UTILS.OUTPUT command (cd_output) + return f"[{self.current_root_path}:\\{self.current_subkey_path}]" + @CLIUtil.addcomplete(cd) def cd_complete(self, subkey: str) -> list[str]: """ @@ -1026,6 +1156,34 @@ def cd_complete(self, subkey: str) -> list[str]: """ return self.ls_complete(subkey) + @CLIUtil.addoutput(cd) + def cd_output(self, pwd) -> None: + """ + Print the output of 'cd' + """ + if self.exploration_mode: + print(pwd) + print("-" * 10 + " SubKeys" + "-" * 10) + self.ls_output(self.ls()) + print("-" * 10 + " Values" + "-" * 10) + self.cat_output(self.cat()) + + @CLIUtil.addcommand() + def activate_exploration_mode(self) -> None: + """ + Activate exploration mode: perform ls and cat automatically when changing directory + """ + self.exploration_mode = True + print("Exploration mode activated") + + @CLIUtil.addcommand() + def disable_exploration_mode(self) -> None: + """ + Disable exploration mode + """ + self.exploration_mode = False + print("Exploration mode disabled") + # --------------------------------------------- # # Get Information # --------------------------------------------- # @@ -1199,6 +1357,101 @@ def set_value( logger.error("Got status %s while setting value", hex(resp.status)) return None + @CLIUtil.addcommand() + def create_key( + self, new_key: str, root_key: str | None, subkey: str | None = None + ) -> None: + req = BaseRegCreateKey_Request( + hKey=self.current_subkey_handle, + lpSubKey=RPC_UNICODE_STRING(Buffer=new_key + "\x00"), + samDesired=self.sam_requested_access_rights, + dwOptions=self.extra_options, + lpClass=None, + lpSecurityAttributes=None, + ) + resp = self.client.sr1_req(req) + if not is_status_ok(resp.status): + logger.error("Got status %s while creating key", hex(resp.status)) + return None + print(f"Key {new_key} created successfully.") + + @CLIUtil.addcommand() + def delete_key(self, subkey: str | None = None) -> None: + """ + Delete the specified subkey. If no subkey is specified, it uses the current subkey path. + Proper same access rights are required to delete a key. By default we request MAXIMUM_ALLOWED. + So no issue. + + :param subkey: The subkey to delete. If None, it uses the current subkey path. + """ + self.activate_backup() + if subkey is None: + subkey_path = self.current_subkey_path + else: + subkey_path = self._join_path(self.current_subkey_path, subkey) + + req = BaseRegDeleteKey_Request( + hKey=self.current_root_handle, + lpSubKey=RPC_UNICODE_STRING(Buffer=str(subkey_path) + "\x00"), + ndr64=True, + ) + + resp = self.client.sr1_req(req) + # We remove the entry from the cache if it exists + # Even if the response status is not OK, we want to remove it + + if subkey_path in self.cache["ls"]: + self.cache["ls"].pop(subkey_path, None) + if subkey_path in self.cache["cat"]: + self.cache["cat"].pop(subkey_path, None) + + if not is_status_ok(resp.status): + logger.error("Got status %s while deleting key", hex(resp.status)) + return None + + print(f"Key {subkey} deleted successfully.") + + @CLIUtil.addcommand() + def delete_value(self, value: str = "", subkey: str | None = None) -> None: + """ + Delete the specified value. + If no subkey is specified, it uses the current subkey path. + If no value is specified, it will delete the default value of the subkey, but subkey cannot be specified. + + :param subkey: The subkey to delete. If None, it uses the current subkey path. + """ + self.activate_backup() + handle = self._get_cached_elt( + subkey=subkey, desired_access=AccessRights.KEY_WRITE + ) + if handle is None: + logger.error("Could not get handle on the specified subkey.") + return None + + req = BaseRegDeleteValue_Request( + hKey=handle, + lpValueName=RPC_UNICODE_STRING(Buffer=value + "\x00"), + ndr64=True, + ) + + resp = self.client.sr1_req(req) + # We remove the entry from the cache if it exists + # Even if the response status is not OK, we want to remove it + # We remove the entry from the cache if it exists + # Even if the response status is not OK, we want to remove it + if subkey is None: + subkey_path = self.current_subkey_path + else: + subkey_path = self._join_path(self.current_subkey_path, subkey) + if subkey_path in self.cache["cat"]: + self.cache["cat"].pop(subkey_path, None) + + if not is_status_ok(resp.status): + logger.error("Got status %s while setting value", hex(resp.status)) + return None + + print(f"Key {subkey} deleted successfully.") + # --------------------------------------------- # # Backup and Restore # --------------------------------------------- # @@ -1315,6 +1568,7 @@ def activate_volatile(self) -> None: """ self.extra_options |= RegOptions.REG_OPTION_VOLATILE self.extra_options &= ~RegOptions.REG_OPTION_NON_VOLATILE + self.use(self.current_root_path) print("Volatile option activated.") self._clear_all_caches() @@ -1327,6 +1581,7 @@ def disable_volatile(self) -> None: """ self.extra_options &= ~RegOptions.REG_OPTION_VOLATILE self.extra_options |= RegOptions.REG_OPTION_NON_VOLATILE + self.use(self.current_root_path) print("Volatile option deactivated.") self._clear_all_caches() @@ -1438,7 +1693,9 @@ def _get_cached_elt( return cache_elt if cache_name is not None else handle - def _join_path(self, first_path: str, second_path: str) -> PureWindowsPath: + def _join_path( + self, first_path: str | None, second_path: str | None + ) -> PureWindowsPath: """ Join two paths in a way that is compatible with Windows paths. This ensures that the paths are normalized and combined correctly, @@ -1448,6 +1705,15 @@ def _join_path(self, first_path: str, second_path: str) -> PureWindowsPath: :param second_path: The second path to join. :return: A PureWindowsPath object representing the combined path. """ + if first_path is None: + first_path = "" + if second_path is None: + second_path = "" + if str(PureWindowsPath(second_path).as_posix()).startswith("/"): + # If the second path is an absolute path, we return it as is + return PureWindowsPath( + os.path.normpath(PureWindowsPath(second_path).as_posix()).lstrip("/") + ) return PureWindowsPath( os.path.normpath( os.path.join( From c81749382a299531cce79ff800b145b479776d97 Mon Sep 17 00:00:00 2001 From: Ebrix Date: Wed, 27 Aug 2025 09:32:57 +0200 Subject: [PATCH 16/27] delete and create key/value --- scapyred/winreg.py | 155 +++++++++++++++++++++++++++++++++------------ 1 file changed, 114 insertions(+), 41 deletions(-) diff --git a/scapyred/winreg.py b/scapyred/winreg.py index 7579449..319d16c 100644 --- a/scapyred/winreg.py +++ b/scapyred/winreg.py @@ -83,14 +83,23 @@ ) -logging.basicConfig( - level=logging.INFO, - format="[%(levelname)s][%(funcName)s] %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", - filename="winreg.log", # write logs here - filemode="w", -) -logger = logging.getLogger(__name__) +# pylint: disable=logging-fstring-interpolation +# Set log level to benefit from Scapy warnings +logger = logging.getLogger("scapy") +logger.setLevel(logging.INFO) +# Create a file handler +file_handler = logging.FileHandler("winreg.log") +file_handler.setLevel(logging.DEBUG) + +# Create a formatter and attach it +formatter = logging.Formatter("[%(levelname)s][%(funcName)s] %(message)s") +file_handler.setFormatter(formatter) + +# Add the file handler +logger.addHandler(file_handler) + + +logger.info("Starting scapy-windows-registry module") class GenericAccessRights(IntFlag): @@ -277,7 +286,7 @@ class RootKeys(StrEnum): # Registry entries subordinate to this key define types (or classes) of documents and the # properties associated with those types. # The subkeys of the HKEY_CLASSES_ROOT key are a merged view of the following two subkeys: - HKEY_CLASSES_ROOT = "HKCROOT" + HKEY_CLASSES_ROOT = "HKCR" # Registry entries subordinate to this key define the preferences of the current user. # These preferences include the settings of environment variables, data on program groups, @@ -293,22 +302,22 @@ class RootKeys(StrEnum): # This key contains information on the current hardware profile of the local computer. # HKEY_CURRENT_CONFIG is an alias for # HKEY_LOCAL_MACHINE\System\CurrentControlSet\Hardware Profiles\Current - HKEY_CURRENT_CONFIG = "HKC" + HKEY_CURRENT_CONFIG = "HKCC" # This key define the default user configuration for new users on the local computer and the # user configuration for the current user. HKEY_USERS = "HKU" # Registry entries subordinate to this key allow access to performance data. - HKEY_PERFORMANCE_DATA = "HKPERFDATA" + HKEY_PERFORMANCE_DATA = "HKPD" # Registry entries subordinate to this key reference the text strings that describe counters # in U.S. English. - HKEY_PERFORMANCE_TEXT = "HKPERFTXT" + HKEY_PERFORMANCE_TEXT = "HKPT" # Registry entries subordinate to this key reference the text strings that describe # counters in the local language of the area in which the computer is running. - HKEY_PERFORMANCE_NLSTEXT = "HKPERFNLSTXT" + HKEY_PERFORMANCE_NLSTEXT = "HKPN" def __new__(cls, value): # 1. Strip and uppercase the raw input @@ -351,8 +360,8 @@ class RegType(IntEnum): Registry value types """ - # Registry value types # These constants are used to specify the type of a registry value. + REG_SZ = 1 # Unicode string REG_EXPAND_SZ = 2 # Unicode string with environment variable expansion REG_BINARY = 3 # Binary data @@ -365,7 +374,7 @@ class RegType(IntEnum): @classmethod def _missing_(cls, value): - print(f"Unknown registry type: {value}, using UNK") + logger.info(f"Unknown registry type: {value}, using UNK") unk = cls.UNK unk.real_value = value return unk @@ -388,14 +397,14 @@ def fromvalue(cls, value: str | int) -> "RegType": try: return cls(value) except ValueError: - print(f"Unknown registry type: {value}, using UNK") + logger.info(f"Unknown registry type: {value}, using UNK") return cls.UNK value = value.strip().upper() try: return cls(int(value)) except (ValueError, KeyError): - print(f"Unknown registry type: {value}, using UNK") + logger.info(f"Unknown registry type: {value}, using UNK") return cls.UNK @@ -427,13 +436,17 @@ def is_status_ok(status: int) -> bool: ErrorCodes.ERROR_MORE_DATA, ]: print(f"[!] Error: {hex(err.value)} - {ErrorCodes(status).name}") + logger.error("Error: %s - %s", hex(err.value), ErrorCodes(status).name) return False return True except ValueError as exc: print(f"[!] Error: {hex(status)} - Unknown error code") + logger.error("Error: %s - %s", hex(err.value), ErrorCodes(status).name) raise ValueError(f"Error: {hex(status)} - Unknown error code") from exc +# Global constant used to easily record +# the root keys available and prevent typos AVAILABLE_ROOT_KEYS: list[str] = [ RootKeys.HKEY_LOCAL_MACHINE, RootKeys.HKEY_CURRENT_USER, @@ -449,6 +462,9 @@ def is_status_ok(status: int) -> bool: class WellKnownSIDs(Enum): """ Well-known SIDs. + + .. notes:: + This class should be filled with more values as needs arise """ SY = WINNT_SID.fromstr("S-1-5-18") # Local System @@ -469,7 +485,7 @@ class WellKnownSIDs(Enum): AceFlags=0x0, # No flags ) / WINNT_ACCESS_ALLOWED_ACE( - Mask=0x10000000, # GA + Mask=AccessRights.GENERIC_ALL, # GA Sid=WellKnownSIDs.BA.value, # Built-in Administrators SID ), ], @@ -555,7 +571,9 @@ def encode_data(reg_type: RegType, data: str) -> bytes: return data.encode("utf-8").decode("unicode_escape").encode("latin1") def __str__(self) -> str: - return f"{self.reg_value} ({self.reg_type.name}) {self.reg_data}" + if self.reg_type == RegType.UNK: + return f"{self.reg_value} ({self.reg_type.name}:{self.reg_type.real_value}) {self.reg_data}" + return f"{self.reg_value} ({self.reg_type.name}:{self.reg_type.value}) {self.reg_data}" def __repr__(self) -> str: return f"RegEntry({self.reg_value}, {self.reg_type}, {self.reg_data})" @@ -567,8 +585,14 @@ class CacheElt: Cache element to store the handle and the subkey path """ + # Handle on a remote object handle: NDRContextHandle + + # Requested AccessRights for this handle access: AccessRights + + # List of elements returned by the server + # using this handle. For example a list of subkeys or values. values: list @@ -676,8 +700,8 @@ def __init__( self.client.bind(self.interface) except ValueError as exc: - print( - f"[!] Warn: Remote service didn't seem to be running. Let's try again now that we should have trigger it. ({exc})" + logger.warning( + f"Remote service didn't seem to be running. Let's try again now that we should have trigger it. ({exc})" ) sleep(1.5) @@ -685,7 +709,7 @@ def __init__( self.client.bind(self.interface) except Scapy_Exception as e: if str(3221225566) in str(e): - print( + logger.error( f""" [!] STATUS_LOGON_FAILURE - {e} You used: - UPN {UPN}, @@ -705,7 +729,7 @@ def __init__( ) exit() except TimeoutError as exc: - print( + logger.error( f"[!] Timeout while connecting to {target}:{port}. Check service status. {exc}" ) sys.exit(-1) @@ -762,14 +786,14 @@ def use(self, root_path): - Changes the current directory to the root of the selected registry hive. :param root_path: The root registry path to use. Should start with one of the following: - - HKCROOT + - HKR - HKLM - HKCU - - HKC + - HKCC - HKU - - HKPERFDATA - - HKPERFTXT - - HKPERFNLSTXT + - HKPD + - HKPT + - HKPN """ root_path = RootKeys(root_path.upper().strip()) @@ -863,7 +887,7 @@ def use(self, root_path): self.client.sr1_req( OpenPerformanceText_Request( ServerName=None, - samDesired=self.sam_requested_access_rights, + samDesired=0, # self.sam_requested_access_rights, ndr64=True, ), timeout=self.timeout, @@ -885,7 +909,7 @@ def use(self, root_path): case _: # If the root key is not recognized, raise an error - print(f"Unknown root key: {root_path}") + logger.error(f"Unknown root key: {root_path}") self._clear_all_caches() self.current_root_handle = None self.current_root_path = "CHOOSE ROOT KEY" @@ -988,7 +1012,7 @@ def ls_complete(self, subkey: str) -> list[str]: ] @CLIUtil.addcommand(spaces=True) - def cat(self, subkey: str | None = None) -> list[tuple[str, str]]: + def cat(self, subkey: str | None = None) -> list[RegEntry]: """ Enumerates and retrieves registry values for a given subkey path. @@ -999,8 +1023,8 @@ def cat(self, subkey: str | None = None) -> list[tuple[str, str]]: subkey (str | None): The subkey path to enumerate. If None or empty, uses the current subkey path. Returns: - list[tuple[str, str]]: A list of registry entries (as RegEntry objects) for the specified subkey path. - Returns an empty list if the handle is invalid or an error occurs during enumeration. + list[RegEntry]: A list of registry entries (as RegEntry objects) for the specified subkey path. + Returns an empty list if the handle is invalid or an error occurs during enumeration. Side Effects: - May print error messages to standard output if RPC queries fail. @@ -1358,24 +1382,50 @@ def set_value( return None @CLIUtil.addcommand() - def create_key( - self, new_key: str, root_key: str | None, subkey: str | None = None - ) -> None: + def create_key(self, new_key: str, subkey: str | None = None) -> None: + """ + Create a new key named as the specified `new_key` under the `subkey`. + If no subkey is specified, it uses the current subkey path. + + :param new_key: name a the new key to create + :param subkey: relative subkey to create the the new key + """ + + handle = self._get_cached_elt( + subkey=subkey, + desired_access=AccessRights.KEY_CREATE_SUB_KEY, + ) + if handle is None: + logger.error("Could not get handle on the specified subkey.") + return None req = BaseRegCreateKey_Request( - hKey=self.current_subkey_handle, + hKey=handle, lpSubKey=RPC_UNICODE_STRING(Buffer=new_key + "\x00"), samDesired=self.sam_requested_access_rights, dwOptions=self.extra_options, - lpClass=None, lpSecurityAttributes=None, + ndr64=True, ) + resp = self.client.sr1_req(req) + # We remove the entry from the cache if it exists + # Even if the response status is not OK, we want to remove it + if subkey is None: + subkey_path = self.current_subkey_path + else: + subkey_path = self._join_path(self.current_subkey_path, subkey) + + if subkey_path in self.cache["ls"]: + self.cache["ls"].pop(subkey_path, None) + if subkey_path in self.cache["cat"]: + self.cache["cat"].pop(subkey_path, None) + if not is_status_ok(resp.status): logger.error("Got status %s while creating key", hex(resp.status)) return None print(f"Key {new_key} created successfully.") - @CLIUtil.addcommand() + @CLIUtil.addcommand(spaces=True) def delete_key(self, subkey: str | None = None) -> None: """ Delete the specified subkey. If no subkey is specified, it uses the current subkey path. @@ -1411,6 +1461,13 @@ def delete_key(self, subkey: str | None = None) -> None: print(f"Key {subkey} deleted successfully.") + @CLIUtil.addcomplete(delete_key) + def delete_key_complete(self, subkey: str) -> list[str]: + """ + Auto-complete delete_key + """ + return self.ls_complete(subkey) + @CLIUtil.addcommand() def delete_value(self, value: str = "", subkey: str | None = None) -> None: """ @@ -1450,7 +1507,22 @@ def delete_value(self, value: str = "", subkey: str | None = None) -> None: logger.error("Got status %s while setting value", hex(resp.status)) return None - print(f"Key {subkey} deleted successfully.") + print(f"Value {value} deleted successfully.") + + @CLIUtil.addcomplete(delete_value) + def delete_value_complete(self, value: str) -> list[str]: + """ + Auto-complete delete_value + """ + if self._require_root_handles(silent=True): + return [] + + value = value.strip() + return [ + subval.reg_value.strip("\x00") + for subval in self.cat() + if str(subval.reg_value).lower().startswith(value.lower()) + ] # --------------------------------------------- # # Backup and Restore @@ -1633,7 +1705,8 @@ def get_handle_on_subkey( resp = self.client.sr1_req(req) if not is_status_ok(resp.status): logger.error( - "[-] Error : got status %s while enumerating keys", hex(resp.status) + "[-] Error : got status %s while getting handle on key", + hex(resp.status), ) return None From 3baa67764daa780ae76daeeec155583f2b2e4112 Mon Sep 17 00:00:00 2001 From: Ebrix Date: Wed, 27 Aug 2025 13:54:11 +0200 Subject: [PATCH 17/27] =?UTF-8?q?Coh=C3=A9rence=20commentaire=20et=20logs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scapyred/winreg.py | 227 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 181 insertions(+), 46 deletions(-) diff --git a/scapyred/winreg.py b/scapyred/winreg.py index 319d16c..0f2e4f4 100644 --- a/scapyred/winreg.py +++ b/scapyred/winreg.py @@ -9,11 +9,11 @@ It is designed to be used with Scapy's DCERPC framework. """ -from dataclasses import dataclass import os import logging import sys +from dataclasses import dataclass from enum import IntEnum, IntFlag, StrEnum, Enum from ctypes.wintypes import PFILETIME from typing import NoReturn @@ -86,7 +86,14 @@ # pylint: disable=logging-fstring-interpolation # Set log level to benefit from Scapy warnings logger = logging.getLogger("scapy") -logger.setLevel(logging.INFO) +logger.setLevel(logging.DEBUG) + +# nique ta mere logger +for handler in logger.handlers: + if isinstance(handler, logging.StreamHandler): + handler.setLevel(logging.DEBUG) # Set desired level + break + # Create a file handler file_handler = logging.FileHandler("winreg.log") file_handler.setLevel(logging.DEBUG) @@ -99,7 +106,7 @@ logger.addHandler(file_handler) -logger.info("Starting scapy-windows-registry module") +logger.debug("Starting scapy-windows-registry module") class GenericAccessRights(IntFlag): @@ -271,6 +278,7 @@ def __str__(self) -> str: Return the string representation of the error code. :return: The string representation of the error code. """ + return self.name @@ -393,6 +401,7 @@ def fromvalue(cls, value: str | int) -> "RegType": :param value: The string representation of the registry type. :return: The corresponding RegType enum member. """ + if isinstance(value, int): try: return cls(value) @@ -412,6 +421,7 @@ def from_filetime_to_datetime(lp_filetime: PFILETIME) -> str: """ Convert a filetime to a human readable date """ + from datetime import datetime, timezone filetime = lp_filetime.dwLowDateTime + (lp_filetime.dwHighDateTime << 32) @@ -428,6 +438,7 @@ def is_status_ok(status: int) -> bool: Check the error code and raise an exception if it is not successful. :param status: The error code to check. """ + try: err = ErrorCodes(status) if err not in [ @@ -435,13 +446,11 @@ def is_status_ok(status: int) -> bool: ErrorCodes.ERROR_NO_MORE_ITEMS, ErrorCodes.ERROR_MORE_DATA, ]: - print(f"[!] Error: {hex(err.value)} - {ErrorCodes(status).name}") logger.error("Error: %s - %s", hex(err.value), ErrorCodes(status).name) return False return True except ValueError as exc: - print(f"[!] Error: {hex(status)} - Unknown error code") - logger.error("Error: %s - %s", hex(err.value), ErrorCodes(status).name) + logger.error("Error: %s - Unknown error code", hex(status)) raise ValueError(f"Error: {hex(status)} - Unknown error code") from exc @@ -545,6 +554,7 @@ def encode_data(reg_type: RegType, data: str) -> bytes: """ Encode data based on the type. """ + match reg_type: case RegType.REG_MULTI_SZ | RegType.REG_SZ | RegType.REG_EXPAND_SZ: if reg_type == RegType.REG_MULTI_SZ: @@ -801,6 +811,7 @@ def use(self, root_path): match root_path: case RootKeys.HKEY_CLASSES_ROOT: # Change to HKCR root + logger.debug("Changing to HKCR root") self.current_root_handle = self.root_handle.setdefault( RootKeys.HKEY_CLASSES_ROOT.value, self.client.sr1_req( @@ -815,6 +826,7 @@ def use(self, root_path): case RootKeys.HKEY_CURRENT_USER: # Change to HKCU root + logger.debug("Changing to HKCU root") self.current_root_handle = self.root_handle.setdefault( RootKeys.HKEY_CURRENT_USER.value, self.client.sr1_req( @@ -829,6 +841,7 @@ def use(self, root_path): case RootKeys.HKEY_LOCAL_MACHINE: # Change to HKLM root + logger.debug("Changing to HKLM root") self.current_root_handle = self.root_handle.setdefault( RootKeys.HKEY_LOCAL_MACHINE.value, self.client.sr1_req( @@ -842,7 +855,8 @@ def use(self, root_path): ) case RootKeys.HKEY_CURRENT_CONFIG: - # Change to HKCU root + # Change to HKCC root + logger.debug("Changing to HKCC root") self.current_root_handle = self.root_handle.setdefault( RootKeys.HKEY_CURRENT_CONFIG.value, self.client.sr1_req( @@ -856,6 +870,8 @@ def use(self, root_path): ) case RootKeys.HKEY_USERS: + # Cange to HKU root + logger.debug("Changing to HKU root") self.current_root_handle = self.root_handle.setdefault( RootKeys.HKEY_USERS.value, self.client.sr1_req( @@ -869,12 +885,14 @@ def use(self, root_path): ) case RootKeys.HKEY_PERFORMANCE_DATA: + # Change to HKPD root + logger.debug("Changing to HKPD root") self.current_root_handle = self.root_handle.setdefault( RootKeys.HKEY_PERFORMANCE_DATA.value, self.client.sr1_req( OpenPerformanceData_Request( ServerName=None, - samDesired=self.sam_requested_access_rights, + samDesired=0, ndr64=True, ), timeout=self.timeout, @@ -882,12 +900,14 @@ def use(self, root_path): ) case RootKeys.HKEY_PERFORMANCE_TEXT: + # Change to HKPT root + logger.debug("Changing to HKPT root") self.current_root_handle = self.root_handle.setdefault( RootKeys.HKEY_PERFORMANCE_TEXT.value, self.client.sr1_req( OpenPerformanceText_Request( ServerName=None, - samDesired=0, # self.sam_requested_access_rights, + samDesired=0, ndr64=True, ), timeout=self.timeout, @@ -895,12 +915,14 @@ def use(self, root_path): ) case RootKeys.HKEY_PERFORMANCE_NLSTEXT: + # Change to HKPN root + logger.debug("Changing to HKPN root") self.current_root_handle = self.root_handle.setdefault( RootKeys.HKEY_PERFORMANCE_NLSTEXT.value, self.client.sr1_req( OpenPerformanceNlsText_Request( ServerName=None, - samDesired=self.sam_requested_access_rights, + samDesired=0, ndr64=True, ), timeout=self.timeout, @@ -941,6 +963,7 @@ def ls(self, subkey: str | None = None) -> list[str]: EnumKeys of the current subkey path """ + # Try to use the cache res = self._get_cached_elt(subkey=subkey, cache_name="ls") if res is None: return [] @@ -955,6 +978,7 @@ def ls(self, subkey: str | None = None) -> list[str]: subkey_path = self._join_path(self.current_subkey_path, subkey) idx = 0 + logger.debug("Enumerating keys in %s", subkey_path) while True: req = BaseRegEnumKey_Request( hKey=res.handle, @@ -965,13 +989,14 @@ def ls(self, subkey: str | None = None) -> list[str]: ndr64=True, ) + # Send request resp = self.client.sr1_req(req) if resp.status == ErrorCodes.ERROR_NO_MORE_ITEMS: break + + # Check the response status elif not is_status_ok(resp.status): - print( - f"[-] Error : got status {hex(resp.status)} while enumerating keys" - ) + logger.error("Got status %s while enumerating keys", hex(resp.status)) self.cache["ls"].pop(subkey_path, None) return [] @@ -1030,6 +1055,8 @@ def cat(self, subkey: str | None = None) -> list[RegEntry]: - May print error messages to standard output if RPC queries fail. - Updates internal cache for previously enumerated subkey paths. """ + + # Try to use the cache res = self._get_cached_elt(subkey=subkey, cache_name="cat") if res is None: return [] @@ -1041,6 +1068,7 @@ def cat(self, subkey: str | None = None) -> list[RegEntry]: subkey_path = self._join_path(self.current_subkey_path, subkey) idx = 0 + logger.debug("Enumerating values in %s", subkey_path) while True: # Get the name of the value at index idx req = BaseRegEnumValue_Request( @@ -1061,13 +1089,14 @@ def cat(self, subkey: str | None = None) -> list[RegEntry]: ndr64=True, ) + # Send request resp = self.client.sr1_req(req) if resp.status == ErrorCodes.ERROR_NO_MORE_ITEMS: break + + # Check the response status elif not is_status_ok(resp.status): - print( - f"[-] Error : got status {hex(resp.status)} while enumerating values" - ) + logger.error("got status %s while enumerating values", hex(resp.status)) self.cache["cat"].pop(subkey_path, None) return [] @@ -1087,6 +1116,7 @@ def cat(self, subkey: str | None = None) -> list[RegEntry]: ndr64=True, ) + # Send request resp2 = self.client.sr1_req(req) if resp2.status == ErrorCodes.ERROR_MORE_DATA: # The buffer was too small, we need to retry with a larger one @@ -1094,10 +1124,9 @@ def cat(self, subkey: str | None = None) -> list[RegEntry]: req.lpData.value.max_count = resp2.lpcbData.value resp2 = self.client.sr1_req(req) + # Check the response status if not is_status_ok(resp2.status): - print( - f"[-] Error : got status {hex(resp2.status)} while querying value" - ) + logger.error("got status %s while querying value", hex(resp2.status)) self.cache["cat"].pop(subkey_path, None) return [] @@ -1121,6 +1150,7 @@ def cat_output(self, results: list[RegEntry]) -> None: """ Print the output of 'cat' """ + if not results or len(results) == 0: print("No values found.") return @@ -1150,12 +1180,14 @@ def cd(self, subkey: str) -> None: """ Change current subkey path """ + if subkey.strip() == "": # If the subkey is ".", we do not change the current subkey path tmp_path = PureWindowsPath() tmp_handle = self.get_handle_on_subkey(tmp_path) else: + # Try to use the cache res = self._get_cached_elt( subkey=subkey, cache_name="cd", @@ -1178,6 +1210,7 @@ def cd_complete(self, subkey: str) -> list[str]: """ Auto-complete cd """ + return self.ls_complete(subkey) @CLIUtil.addoutput(cd) @@ -1185,6 +1218,7 @@ def cd_output(self, pwd) -> None: """ Print the output of 'cd' """ + if self.exploration_mode: print(pwd) print("-" * 10 + " SubKeys" + "-" * 10) @@ -1197,6 +1231,7 @@ def activate_exploration_mode(self) -> None: """ Activate exploration mode: perform ls and cat automatically when changing directory """ + self.exploration_mode = True print("Exploration mode activated") @@ -1205,6 +1240,7 @@ def disable_exploration_mode(self) -> None: """ Disable exploration mode """ + self.exploration_mode = False print("Exploration mode disabled") @@ -1216,12 +1252,15 @@ def disable_exploration_mode(self) -> None: def get_sd(self, subkey: str | None = None) -> SECURITY_DESCRIPTOR | None: """ Get the security descriptor of the current subkey. SACL are not retrieve at this point (TODO). - """ + + # Try to use the cache handle = self._get_cached_elt(subkey=subkey) if handle is None: return None + # Log and execute + logger.debug("Getting security descriptor for %s", subkey) req = BaseRegGetKeySecurity_Request( hKey=handle, SecurityInformation=0x00000001 # OWNER_SECURITY_INFORMATION @@ -1233,6 +1272,7 @@ def get_sd(self, subkey: str | None = None) -> SECURITY_DESCRIPTOR | None: ndr64=True, ) + # Send request resp = self.client.sr1_req(req) if resp.status == ErrorCodes.ERROR_INSUFFICIENT_BUFFER: # The buffer was too small, we need to retry with a larger one @@ -1241,12 +1281,14 @@ def get_sd(self, subkey: str | None = None) -> SECURITY_DESCRIPTOR | None: ) resp = self.client.sr1_req(req) + # Check the response status if not is_status_ok(resp.status): - print(f"[-] Error : got status {hex(resp.status)} while getting security") + logger.error("Got status %s while getting security", hex(resp.status)) return None - results = resp.pRpcSecurityDescriptorOut.valueof("lpSecurityDescriptor") - sd = SECURITY_DESCRIPTOR(results) + sd = SECURITY_DESCRIPTOR( + resp.pRpcSecurityDescriptorOut.valueof("lpSecurityDescriptor") + ) return sd @CLIUtil.addoutput(get_sd) @@ -1254,6 +1296,7 @@ def get_sd_output(self, sd: SECURITY_DESCRIPTOR | None) -> None: """ Print the output of 'get_sd' """ + if sd is None: print("No security descriptor found.") return @@ -1276,18 +1319,25 @@ def query_info( :return: BaseRegQueryInfoKey_Response object containing information about the subkey. Returns None if the handle is invalid or an error occurs during the query. """ + + # Try to use the cache handle = self._get_cached_elt(subkey) if handle is None: logger.error("Could not get handle on the specified subkey.") return None + # Log and execute + logger.debug("Querying info for %s", subkey) req = BaseRegQueryInfoKey_Request( hKey=handle, lpClassIn=RPC_UNICODE_STRING(), # pointer to class name ndr64=True, ) + # Send request resp = self.client.sr1_req(req) + + # Check the response status if not is_status_ok(resp.status): logger.error("Got status %s while querying info", hex(resp.status)) return None @@ -1298,6 +1348,7 @@ def query_info_output(self, info: None) -> None: """ Print the output of 'query_info' """ + if info is None: print("No information found.") return @@ -1316,10 +1367,12 @@ def query_info_output(self, info: None) -> None: @CLIUtil.addcommand() def version(self) -> NDRIntField: """ - Get remote registry server version + Get remote registry server version of the current subkey """ + + logger.debug("Getting remote registry server version") return self.client.sr1_req( - BaseRegGetVersion_Request(hKey=self.current_root_handle) + BaseRegGetVersion_Request(hKey=self.current_subkey_handle, ndr64=True) ).lpdwVersion @CLIUtil.addoutput(version) @@ -1327,6 +1380,7 @@ def version_output(self, version: int) -> None: """ Print the output of 'version' """ + print(f"Remote registry server version: {version}") # --------------------------------------------- # @@ -1345,6 +1399,8 @@ def set_value( Set a registry value in the current subkey. If no subkey is specified, it uses the current subkey path. """ + + # Validate the value type try: value_type = RegType.fromvalue(value_type) except ValueError: @@ -1353,12 +1409,26 @@ def set_value( data = RegEntry.encode_data(value_type, value_data) + # Try to use the cache handle = self._get_cached_elt( subkey=subkey, desired_access=AccessRights.KEY_WRITE ) if handle is None: logger.error("Could not get handle on the specified subkey.") return None + + if subkey is None: + subkey_path = self.current_subkey_path + else: + subkey_path = self._join_path(self.current_subkey_path, subkey) + + # Log and execute + logger.debug( + "Setting value %s of type %s in %s", + value_name, + value_type.name, + subkey_path, + ) req = BaseRegSetValue_Request( hKey=handle, lpValueName=RPC_UNICODE_STRING(Buffer=value_name + "\x00"), @@ -1367,16 +1437,15 @@ def set_value( ndr64=True, ) + # Send request resp = self.client.sr1_req(req) + # We remove the entry from the cache if it exists # Even if the response status is not OK, we want to remove it - if subkey is None: - subkey_path = self.current_subkey_path - else: - subkey_path = self._join_path(self.current_subkey_path, subkey) if subkey_path in self.cache["cat"]: self.cache["cat"].pop(subkey_path, None) + # Check the response status if not is_status_ok(resp.status): logger.error("Got status %s while setting value", hex(resp.status)) return None @@ -1391,6 +1460,7 @@ def create_key(self, new_key: str, subkey: str | None = None) -> None: :param subkey: relative subkey to create the the new key """ + # Try to use the cache handle = self._get_cached_elt( subkey=subkey, desired_access=AccessRights.KEY_CREATE_SUB_KEY, @@ -1398,6 +1468,14 @@ def create_key(self, new_key: str, subkey: str | None = None) -> None: if handle is None: logger.error("Could not get handle on the specified subkey.") return None + + if subkey is None: + subkey_path = self.current_subkey_path + else: + subkey_path = self._join_path(self.current_subkey_path, subkey) + + # Log and execute + logger.debug("Creating key %s under %s", new_key, subkey_path) req = BaseRegCreateKey_Request( hKey=handle, lpSubKey=RPC_UNICODE_STRING(Buffer=new_key + "\x00"), @@ -1407,19 +1485,17 @@ def create_key(self, new_key: str, subkey: str | None = None) -> None: ndr64=True, ) + # Send request resp = self.client.sr1_req(req) + # We remove the entry from the cache if it exists # Even if the response status is not OK, we want to remove it - if subkey is None: - subkey_path = self.current_subkey_path - else: - subkey_path = self._join_path(self.current_subkey_path, subkey) - if subkey_path in self.cache["ls"]: self.cache["ls"].pop(subkey_path, None) if subkey_path in self.cache["cat"]: self.cache["cat"].pop(subkey_path, None) + # Check the response status if not is_status_ok(resp.status): logger.error("Got status %s while creating key", hex(resp.status)) return None @@ -1434,27 +1510,35 @@ def delete_key(self, subkey: str | None = None) -> None: :param subkey: The subkey to delete. If None, it uses the current subkey path. """ + + # Make sure that we have a backup activated self.activate_backup() + + # Determine the subkey path for logging and cache purposes if subkey is None: subkey_path = self.current_subkey_path else: subkey_path = self._join_path(self.current_subkey_path, subkey) + # Log and execute + logger.debug("Deleting key %s", subkey_path) req = BaseRegDeleteKey_Request( hKey=self.current_root_handle, lpSubKey=RPC_UNICODE_STRING(Buffer=str(subkey_path) + "\x00"), ndr64=True, ) + # Send request resp = self.client.sr1_req(req) + # We remove the entry from the cache if it exists # Even if the response status is not OK, we want to remove it - if subkey_path in self.cache["ls"]: self.cache["ls"].pop(subkey_path, None) if subkey_path in self.cache["cat"]: self.cache["cat"].pop(subkey_path, None) + # Check the response status if not is_status_ok(resp.status): logger.error("Got status %s while deleting key", hex(resp.status)) return None @@ -1466,6 +1550,7 @@ def delete_key_complete(self, subkey: str) -> list[str]: """ Auto-complete delete_key """ + return self.ls_complete(subkey) @CLIUtil.addcommand() @@ -1477,7 +1562,11 @@ def delete_value(self, value: str = "", subkey: str | None = None) -> None: :param subkey: The subkey to delete. If None, it uses the current subkey path. """ + + # Make sure that we have a backup activated self.activate_backup() + + # Try to use the cache handle = self._get_cached_elt( subkey=subkey, desired_access=AccessRights.KEY_WRITE ) @@ -1485,24 +1574,29 @@ def delete_value(self, value: str = "", subkey: str | None = None) -> None: logger.error("Could not get handle on the specified subkey.") return None + # Determine the subkey path for logging and cache purposes + if subkey is None: + subkey_path = self.current_subkey_path + else: + subkey_path = self._join_path(self.current_subkey_path, subkey) + + # Log and execute + logger.debug("Deleting value %s in %s", value, subkey_path) req = BaseRegDeleteValue_Request( hKey=handle, lpValueName=RPC_UNICODE_STRING(Buffer=value + "\x00"), ndr64=True, ) + # Send request resp = self.client.sr1_req(req) + # We remove the entry from the cache if it exists # Even if the response status is not OK, we want to remove it - # We remove the entry from the cache if it exists - # Even if the response status is not OK, we want to remove it - if subkey is None: - subkey_path = self.current_subkey_path - else: - subkey_path = self._join_path(self.current_subkey_path, subkey) if subkey_path in self.cache["cat"]: self.cache["cat"].pop(subkey_path, None) + # Check the response status if not is_status_ok(resp.status): logger.error("Got status %s while setting value", hex(resp.status)) return None @@ -1514,6 +1608,7 @@ def delete_value_complete(self, value: str) -> list[str]: """ Auto-complete delete_value """ + if self._require_root_handles(silent=True): return [] @@ -1544,7 +1639,11 @@ def save( :param subkey: The subkey to backup. If None, it uses the current subkey path. :return: None, by default it saves the backup to a file protected so that only BA can read it. """ + + # Make sure that we have a backup activated self.activate_backup() + + # Try to use the cache handle = self._get_cached_elt(subkey=subkey) key_to_save = ( subkey.split("\\")[-1] if subkey else self.current_subkey_path.name @@ -1570,6 +1669,7 @@ def save( print( "Looks like you don't like security so much. Hope you know what you are doing." ) + logger.warning("Disabling security built-in protections while saving.") sa = None else: sa = PRPC_SECURITY_ATTRIBUTES( @@ -1579,8 +1679,10 @@ def save( bInheritHandle=False, ndr64=True, ) - sa.nLength = len(sa) + + # Log and execute + logger.debug("Backing up %s to %s", key_to_save, output_path) req = BaseRegSaveKey_Request( hKey=handle, lpFile=RPC_UNICODE_STRING(Buffer=output_path), @@ -1588,7 +1690,10 @@ def save( ndr64=True, ) + # Send request resp = self.client.sr1_req(req) + + # Check the response status if not is_status_ok(resp.status): logger.error("Got status %s while backing up", hex(resp.status)) else: @@ -1609,12 +1714,17 @@ def activate_backup(self) -> None: Activate the backup option for the registry operations (enable your backup privilege). This enable the backup privilege for the current session. """ + # check if backup privilege is already enabled if self.extra_options & RegOptions.REG_OPTION_BACKUP_RESTORE: - print("Backup option is already activated. Didn't do anything.") + logger.info("Backup option is already activated. Didn't do anything.") return self.extra_options |= RegOptions.REG_OPTION_BACKUP_RESTORE + + # Log and print print("Backup option activated.") + logger.debug("Backup option activated.") + # Clear the local cache, as the backup option will change the behavior of the registry self._clear_all_caches() @@ -1624,12 +1734,18 @@ def disable_backup(self) -> None: Disable the backup option for the registry operations (disable your backup privilege). This disable the backup privilege for the current session. """ + # check if backup privilege is already disabled if not self.extra_options & RegOptions.REG_OPTION_BACKUP_RESTORE: print("Backup option is already disabled. Didn't do anything.") return self.extra_options &= ~RegOptions.REG_OPTION_BACKUP_RESTORE + + # Log and print print("Backup option deactivated.") + logger.debug("Backup option deactivated.") + + # Clear the local cache, as the backup option will change the behavior of the registry self._clear_all_caches() @CLIUtil.addcommand() @@ -1638,6 +1754,7 @@ def activate_volatile(self) -> None: Set the registry operations to be volatile. This means that the registry key will be deleted when the system is restarted. """ + self.extra_options |= RegOptions.REG_OPTION_VOLATILE self.extra_options &= ~RegOptions.REG_OPTION_NON_VOLATILE self.use(self.current_root_path) @@ -1651,6 +1768,7 @@ def disable_volatile(self) -> None: Disable the volatile option for the registry operations. This means that the registry key will not be deleted when the system is restarted. """ + self.extra_options &= ~RegOptions.REG_OPTION_VOLATILE self.extra_options |= RegOptions.REG_OPTION_NON_VOLATILE self.use(self.current_root_path) @@ -1674,11 +1792,14 @@ def get_handle_on_subkey( :param desired_access_rights: The desired access rights for the subkey. If None, defaults to read access rights. :return: An NDRContextHandle on success, None on failure. """ + # If we don't have a root handle, we cannot get a subkey handle # This is a safety check, as we should not be able to call this function # without having a root handle already set. if self._require_root_handles(silent=True): return None + + # Convert subkey_path to string and ensure it is null-terminated if str(subkey_path) == ".": subkey_path = "\x00" else: @@ -1687,8 +1808,11 @@ def get_handle_on_subkey( # If no access rights were specified, we use the default read access rights if desired_access_rights is None: # Default to read access rights - desired_access_rights = AccessRights.KEY_READ + desired_access_rights = ( + AccessRights.KEY_READ | AccessRights.STANDARD_RIGHTS_READ + ) + # Log and execute logger.debug( "Getting handle on subkey: %s with access rights: %s", subkey_path, @@ -1702,10 +1826,13 @@ def get_handle_on_subkey( ndr64=True, ) + # Send request resp = self.client.sr1_req(req) + + # Check the response status if not is_status_ok(resp.status): logger.error( - "[-] Error : got status %s while getting handle on key", + "Got status %s while getting handle on key", hex(resp.status), ) return None @@ -1729,22 +1856,25 @@ def _get_cached_elt( :param desired_access: The desired access rights for the subkey. If None, defaults to read access rights. :return: A CacheElt object if cache_name is provided, otherwise an NDRContextHandle. """ + if self._require_root_handles(silent=True): return None if desired_access is None: # Default to read access rights - desired_access = AccessRights.KEY_READ + desired_access = AccessRights.KEY_READ | AccessRights.STANDARD_RIGHTS_READ # If no specific subkey was specified # we use our current subkey path if subkey is None or subkey == "" or subkey == ".": subkey_path = self.current_subkey_path + # Otherwise we use the subkey path, # the calling parent shall make sure that this path was properly sanitized else: subkey_path = self._join_path(self.current_subkey_path, subkey) + # If cache name is specified, we try to use it if ( self.cache.get(cache_name, None) is not None and self.cache[cache_name].get(subkey_path, None) is not None @@ -1760,6 +1890,7 @@ def _get_cached_elt( logger.error("Could not get handle on %s", subkey_path) return None + # If we have a cache name, we store the handle in the cache cache_elt = CacheElt(handle, desired_access, []) if cache_name is not None: self.cache[cache_name][subkey_path] = cache_elt @@ -1778,6 +1909,7 @@ def _join_path( :param second_path: The second path to join. :return: A PureWindowsPath object representing the combined path. """ + if first_path is None: first_path = "" if second_path is None: @@ -1803,6 +1935,7 @@ def _require_root_handles(self, silent: bool = False) -> bool: :param silent: If True, do not print any message if no root handle is set. :return: True if no root handle is set, False otherwise. """ + if self.current_root_handle is None: if not silent: print("No root key selected ! Use 'use' to use one.") @@ -1813,6 +1946,7 @@ def _clear_all_caches(self) -> None: """ Clear all caches """ + for _, c in self.cache.items(): c.clear() @@ -1821,6 +1955,7 @@ def dev(self) -> NoReturn: """ Joker function to jump into the python code for dev purpose """ + logger.info("Jumping into the code for dev purpose...") # pylint: disable=forgotten-debug-statement, pointless-statement breakpoint() From b126ab0c97d02185e7b78dd445ce574aeaafe5e8 Mon Sep 17 00:00:00 2001 From: Ebrix Date: Wed, 27 Aug 2025 17:34:36 +0200 Subject: [PATCH 18/27] some doc and log shit --- doc/winreg.rst | 185 +++++++++++++++++++++++++++++++++++++++++++++ scapyred/winreg.py | 33 ++++---- 2 files changed, 204 insertions(+), 14 deletions(-) create mode 100644 doc/winreg.rst diff --git a/doc/winreg.rst b/doc/winreg.rst new file mode 100644 index 0000000..98dcaed --- /dev/null +++ b/doc/winreg.rst @@ -0,0 +1,185 @@ +############# +WinReg +############# + +The `scapy-winreg` module allows interaction with the Windows Registry over SMB using the MS-RRP protocol. +It supports various operations such as listing subkeys, reading and writing values, creating and deleting keys, and more. + +******************** +Some vocabulary +******************** + +In the context of the Windows Registry, it's important to understand the following terms: + +* **Root key**: A root key, also called hive, is a container object in the registry that can hold subkeys and values. It's a key of the highest hierarchical level. +* **Subkey**: A subkey is a key that is nested within another key. It can also contain its own subkeys and values. +* **Key**: A key is a container object in the registry that can hold subkeys and values. It can designate both a root key and a subkey. +* **Value**: A value is a named item containing data and attached to a key. Each value has a name, a data type, and the actual data. Common data types include strings (REG_SZ), binary data (REG_BINARY), and DWORDs (REG_DWORD). + +******************** +Key functionnalities +******************** + +=================================== +``use``: Select a root registry key +=================================== + +The ``use`` function allows you to select a root registry key to work with. +The available root keys are: + +* HKEY_CLASSES_ROOT (**HKCR**) +* HKEY_LOCAL_MACHINE (**HKLM**) +* HKEY_CURRENT_USER (**HKCU**) +* HKEY_USERS (**HKU**) +* HKEY_CURRENT_CONFIG (**HKCC**) +* HKEY_PERFORMANCE_DATA (**HKPD**) +* HKEY_PERFORMANCE_NLSTEXT (**HKPN**) +* HKEY_PERFORMANCE_TEXT (**HKPT**) + +.. code-block:: python + :caption: CLI usage example + + >>> [reg] CHOOSE ROOT KEY\. > use HKLM + >>> [reg] HKLM\. > + +.. code-block:: bash + :caption: Direct request from the command line + + >>> scapy-winreg --UPN Administrator@DOM.LOCAL --password Passw0rd 10.0.0.10 --rootKey HKLM + >>> [reg] HKLM\. > + +==================== +``ls``: List subkeys +==================== + +The ``ls`` function lists the subkeys of the current key or a specified relative key. + +.. code-block:: python + :caption: CLI usage example + + >>> [reg] HKLM\. > ls + Subkeys: + SOFTWARE + SYSTEM + SAM + SECURITY + HARDWARE + BCD00000000 + ... + >>> [reg] HKLM\. > ls SYSTEM\CurrentControlSet\Services + Subkeys: + AdobeARMservice + AFD + ALG + AppIDSvc + Appinfo + AppMgmt + ... + +============================= +``cd``: Change current subkey +============================= + +The ``cd`` function changes the current subkey to a specified relative key or to the root of the current root key. + +.. code-block:: python + :caption: CLI usage example + + >>> [reg] HKLM\. > cd SYSTEM\CurrentControlSet\Services + >>> [reg] HKLM\SYSTEM\CurrentControlSet\Services > cd .. + >>> [reg] HKLM\SYSTEM\CurrentControlSet > cd \ + >>> [reg] HKLM\. > cd /SOFTWARE/Microsoft/Windows + >>> [reg] HKLM\SOFTWARE\Microsoft\Windows > cd / + >>> [reg] HKLM\. > + +.. code-block:: bash + :caption: Direct request from the command line + + >>> scapy-winreg --UPN Administrator@DOM.LOCAL --password Passw0rd 10.0.0.10 --rootKey HKLM --subKey SYSTEM/CurrentControlSet/Services/winmgmt + >>> [reg] HKLM\SYSTEM\CurrentControlSet\Services\winmgmt > + +================================ +``cat``: Display values of a key +================================ + +The ``cat`` function displays the values of the current key or a specified relative key. + +.. code-block:: python + :caption: CLI usage example + + >>> [reg] HKLM\. > cat + Values: + (Default) REG_SZ (value not set) + Class REG_SZ (value not set) + LastWriteTime REG_QWORD 132537600000000000 + ... + >>> [reg] HKLM\SYSTEM\CurrentControlSet\Services\winmgmt > cat + - DependOnService (REG_MULTI_SZ - 7) RPCSS + + - Description (REG_SZ - 1) @%Systemroot%\system32\wbem\wmisvc.dll,-204 + - DisplayName (REG_SZ - 1) @%Systemroot%\system32\wbem\wmisvc.dll,-205 + - ErrorControl (REG_DWORD - 4) 0 + - FailureActions (REG_BINARY - 3) b'\x80Q\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x14\x00\x00\x00\x01\x00\x00\x00\xc0\xd4\x01\x00\x01\x00\x00\x00\xe0\x93\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00' + - ImagePath (REG_EXPAND_SZ - 2) %systemroot%\system32\svchost.exe -k netsvcs -p + - ObjectName (REG_SZ - 1) localSystem + - ServiceSidType (REG_DWORD - 4) 1 + - Start (REG_DWORD - 4) 2 + - SvcMemHardLimitInMB (REG_DWORD - 4) 28 + - SvcMemMidLimitInMB (REG_DWORD - 4) 20 + - SvcMemSoftLimitInMB (REG_DWORD - 4) 11 + - Type (REG_DWORD - 4) 32 + - (REG_SZ - 1) This is the default value + +Notice how the default value is represented with an empty name, when regedit shows it as "(Default)". +This is a design choice to avoid confusion with a value that would actually be named "(Default)". +Future development may include an option to display it as "(Default)" for better user experience. + + +======================================= +``query_info``: Get subkey information +======================================= + +The ``query_info`` function retrieves information about the current key or a specified relative key, including the number of subkeys, number of values, and last write time. + +.. code-block:: python + :caption: CLI usage example + + >>> [reg] HKLM\SYSTEM\CurrentControlSet\Services\winmgmt > query_info + Info on key: + - Number of subkeys: 1 + - Length of the longest subkey name (in bytes): 20 + - Number of values: 14 + - Length of the longest value name (in bytes): 38 + - Last write time: 2025-08-27 15:20:54 + +============================================= +``version``: Get the remote registry version +============================================= + +.. code-block:: python + :caption: CLI usage example + + >>> [reg] HKLM\SYSTEM\CurrentControlSet\Services\winmgmt > version + Remote registry server version: 6 + +======================================== +``get_sd``: Get security descriptor +======================================== + +The ``get_sd`` function retrieves the security descriptor of the current key or a specified relative key. +The information is displayed in a kindof human-readable format. Yet, information displayed is currently incomplete. +Upcoming versions will provide a more complete and user-friendly output. + +.. code-block:: python + :caption: CLI usage example + + >>> [reg] HKLM\. > get_sd SAM + Owner: S-1-5-32-544 + Group: S-1-5-18 + DACL: + - (A;CI;;;;S-1-5-32-545) + - (A;CI;;;;S-1-5-32-544) + - (A;CI;;;;S-1-5-18) + - (A;CI;;;;S-1-3-0) + - (A;CI;;;;S-1-15-2-1) + - (A;CI;;;;S-1-15-3-1024-1065365936-1281604716-3511738428-1654721687-432734479-3232135806-4053264122-3456934681) \ No newline at end of file diff --git a/scapyred/winreg.py b/scapyred/winreg.py index 0f2e4f4..697c31e 100644 --- a/scapyred/winreg.py +++ b/scapyred/winreg.py @@ -85,22 +85,27 @@ # pylint: disable=logging-fstring-interpolation # Set log level to benefit from Scapy warnings -logger = logging.getLogger("scapy") +logger = logging.getLogger() logger.setLevel(logging.DEBUG) -# nique ta mere logger -for handler in logger.handlers: - if isinstance(handler, logging.StreamHandler): - handler.setLevel(logging.DEBUG) # Set desired level - break +# Create a stream handler +stream_handler = logging.StreamHandler(sys.stdout) +stream_handler.setLevel(logging.INFO) + +# Create a formatter and attach it +formatter_sh = logging.Formatter("[%(levelname)s] %(message)s") +stream_handler.setFormatter(formatter_sh) + +# Add the stream handler +logger.addHandler(stream_handler) # Create a file handler file_handler = logging.FileHandler("winreg.log") file_handler.setLevel(logging.DEBUG) # Create a formatter and attach it -formatter = logging.Formatter("[%(levelname)s][%(funcName)s] %(message)s") -file_handler.setFormatter(formatter) +formatter_fh = logging.Formatter("[%(levelname)s][%(funcName)s] %(message)s") +file_handler.setFormatter(formatter_fh) # Add the file handler logger.addHandler(file_handler) @@ -796,7 +801,7 @@ def use(self, root_path): - Changes the current directory to the root of the selected registry hive. :param root_path: The root registry path to use. Should start with one of the following: - - HKR + - HKCR - HKLM - HKCU - HKCC @@ -1490,8 +1495,8 @@ def create_key(self, new_key: str, subkey: str | None = None) -> None: # We remove the entry from the cache if it exists # Even if the response status is not OK, we want to remove it - if subkey_path in self.cache["ls"]: - self.cache["ls"].pop(subkey_path, None) + if subkey_path.parent in self.cache["ls"]: + self.cache["ls"].pop(subkey_path.parent, None) if subkey_path in self.cache["cat"]: self.cache["cat"].pop(subkey_path, None) @@ -1533,8 +1538,8 @@ def delete_key(self, subkey: str | None = None) -> None: # We remove the entry from the cache if it exists # Even if the response status is not OK, we want to remove it - if subkey_path in self.cache["ls"]: - self.cache["ls"].pop(subkey_path, None) + if subkey_path.parent in self.cache["ls"]: + self.cache["ls"].pop(subkey_path.parent, None) if subkey_path in self.cache["cat"]: self.cache["cat"].pop(subkey_path, None) @@ -1717,7 +1722,7 @@ def activate_backup(self) -> None: # check if backup privilege is already enabled if self.extra_options & RegOptions.REG_OPTION_BACKUP_RESTORE: - logger.info("Backup option is already activated. Didn't do anything.") + logger.debug("Backup option is already activated. Didn't do anything.") return self.extra_options |= RegOptions.REG_OPTION_BACKUP_RESTORE From 9f996430bd5d215bdf4b470879b7ff55d6b75ee6 Mon Sep 17 00:00:00 2001 From: Ebrix Date: Wed, 27 Aug 2025 17:36:55 +0200 Subject: [PATCH 19/27] rst stuff --- doc/winreg.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/winreg.rst b/doc/winreg.rst index 98dcaed..84574f3 100644 --- a/doc/winreg.rst +++ b/doc/winreg.rst @@ -36,7 +36,7 @@ The available root keys are: * HKEY_PERFORMANCE_NLSTEXT (**HKPN**) * HKEY_PERFORMANCE_TEXT (**HKPT**) -.. code-block:: python +.. code-block:: bash :caption: CLI usage example >>> [reg] CHOOSE ROOT KEY\. > use HKLM @@ -54,7 +54,7 @@ The available root keys are: The ``ls`` function lists the subkeys of the current key or a specified relative key. -.. code-block:: python +.. code-block:: bash :caption: CLI usage example >>> [reg] HKLM\. > ls @@ -82,7 +82,7 @@ The ``ls`` function lists the subkeys of the current key or a specified relative The ``cd`` function changes the current subkey to a specified relative key or to the root of the current root key. -.. code-block:: python +.. code-block:: bash :caption: CLI usage example >>> [reg] HKLM\. > cd SYSTEM\CurrentControlSet\Services @@ -104,7 +104,7 @@ The ``cd`` function changes the current subkey to a specified relative key or to The ``cat`` function displays the values of the current key or a specified relative key. -.. code-block:: python +.. code-block:: bash :caption: CLI usage example >>> [reg] HKLM\. > cat @@ -141,7 +141,7 @@ Future development may include an option to display it as "(Default)" for better The ``query_info`` function retrieves information about the current key or a specified relative key, including the number of subkeys, number of values, and last write time. -.. code-block:: python +.. code-block:: bash :caption: CLI usage example >>> [reg] HKLM\SYSTEM\CurrentControlSet\Services\winmgmt > query_info @@ -156,7 +156,7 @@ The ``query_info`` function retrieves information about the current key or a spe ``version``: Get the remote registry version ============================================= -.. code-block:: python +.. code-block:: bash :caption: CLI usage example >>> [reg] HKLM\SYSTEM\CurrentControlSet\Services\winmgmt > version @@ -170,7 +170,7 @@ The ``get_sd`` function retrieves the security descriptor of the current key or The information is displayed in a kindof human-readable format. Yet, information displayed is currently incomplete. Upcoming versions will provide a more complete and user-friendly output. -.. code-block:: python +.. code-block:: bash :caption: CLI usage example >>> [reg] HKLM\. > get_sd SAM From 37c9ea5ff7c3b8b248854d2f1921997bc4817d4f Mon Sep 17 00:00:00 2001 From: Ebrix Date: Wed, 27 Aug 2025 17:38:12 +0200 Subject: [PATCH 20/27] rst stuff --- doc/winreg.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/winreg.rst b/doc/winreg.rst index 84574f3..cc98107 100644 --- a/doc/winreg.rst +++ b/doc/winreg.rst @@ -130,6 +130,7 @@ The ``cat`` function displays the values of the current key or a specified relat - Type (REG_DWORD - 4) 32 - (REG_SZ - 1) This is the default value + Notice how the default value is represented with an empty name, when regedit shows it as "(Default)". This is a design choice to avoid confusion with a value that would actually be named "(Default)". Future development may include an option to display it as "(Default)" for better user experience. From f53316e03c9ed0fdca70c6248ffd1fb345a86aa7 Mon Sep 17 00:00:00 2001 From: Ebrix Date: Wed, 27 Aug 2025 17:39:17 +0200 Subject: [PATCH 21/27] rst stuff --- doc/winreg.rst | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/doc/winreg.rst b/doc/winreg.rst index cc98107..fac2101 100644 --- a/doc/winreg.rst +++ b/doc/winreg.rst @@ -108,27 +108,27 @@ The ``cat`` function displays the values of the current key or a specified relat :caption: CLI usage example >>> [reg] HKLM\. > cat - Values: - (Default) REG_SZ (value not set) - Class REG_SZ (value not set) - LastWriteTime REG_QWORD 132537600000000000 - ... - >>> [reg] HKLM\SYSTEM\CurrentControlSet\Services\winmgmt > cat - - DependOnService (REG_MULTI_SZ - 7) RPCSS - - - Description (REG_SZ - 1) @%Systemroot%\system32\wbem\wmisvc.dll,-204 - - DisplayName (REG_SZ - 1) @%Systemroot%\system32\wbem\wmisvc.dll,-205 - - ErrorControl (REG_DWORD - 4) 0 - - FailureActions (REG_BINARY - 3) b'\x80Q\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x14\x00\x00\x00\x01\x00\x00\x00\xc0\xd4\x01\x00\x01\x00\x00\x00\xe0\x93\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00' - - ImagePath (REG_EXPAND_SZ - 2) %systemroot%\system32\svchost.exe -k netsvcs -p - - ObjectName (REG_SZ - 1) localSystem - - ServiceSidType (REG_DWORD - 4) 1 - - Start (REG_DWORD - 4) 2 - - SvcMemHardLimitInMB (REG_DWORD - 4) 28 - - SvcMemMidLimitInMB (REG_DWORD - 4) 20 - - SvcMemSoftLimitInMB (REG_DWORD - 4) 11 - - Type (REG_DWORD - 4) 32 - - (REG_SZ - 1) This is the default value + Values: + (Default) REG_SZ (value not set) + Class REG_SZ (value not set) + LastWriteTime REG_QWORD 132537600000000000 + ... + >>> [reg] HKLM\SYSTEM\CurrentControlSet\Services\winmgmt > cat + - DependOnService (REG_MULTI_SZ - 7) RPCSS + + - Description (REG_SZ - 1) @%Systemroot%\system32\wbem\wmisvc.dll,-204 + - DisplayName (REG_SZ - 1) @%Systemroot%\system32\wbem\wmisvc.dll,-205 + - ErrorControl (REG_DWORD - 4) 0 + - FailureActions (REG_BINARY - 3) b'\x80Q\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x14\x00\x00\x00\x01\x00\x00\x00\xc0\xd4\x01\x00\x01\x00\x00\x00\xe0\x93\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00' + - ImagePath (REG_EXPAND_SZ - 2) %systemroot%\system32\svchost.exe -k netsvcs -p + - ObjectName (REG_SZ - 1) localSystem + - ServiceSidType (REG_DWORD - 4) 1 + - Start (REG_DWORD - 4) 2 + - SvcMemHardLimitInMB (REG_DWORD - 4) 28 + - SvcMemMidLimitInMB (REG_DWORD - 4) 20 + - SvcMemSoftLimitInMB (REG_DWORD - 4) 11 + - Type (REG_DWORD - 4) 32 + - (REG_SZ - 1) This is the default value Notice how the default value is represented with an empty name, when regedit shows it as "(Default)". From 00daba6ea01dced3e2649e36571471b0f83a4874 Mon Sep 17 00:00:00 2001 From: Ebrix Date: Wed, 27 Aug 2025 18:11:47 +0200 Subject: [PATCH 22/27] some doc --- doc/winreg.rst | 168 ++++++++++++++++++++++++++++++++++++++++++--- scapyred/winreg.py | 3 +- 2 files changed, 162 insertions(+), 9 deletions(-) diff --git a/doc/winreg.rst b/doc/winreg.rst index fac2101..68c62a5 100644 --- a/doc/winreg.rst +++ b/doc/winreg.rst @@ -107,13 +107,7 @@ The ``cat`` function displays the values of the current key or a specified relat .. code-block:: bash :caption: CLI usage example - >>> [reg] HKLM\. > cat - Values: - (Default) REG_SZ (value not set) - Class REG_SZ (value not set) - LastWriteTime REG_QWORD 132537600000000000 - ... - >>> [reg] HKLM\SYSTEM\CurrentControlSet\Services\winmgmt > cat + >>> [reg] HKLM\SYSTEM\CurrentControlSet\Services\winmgmt > cat - DependOnService (REG_MULTI_SZ - 7) RPCSS - Description (REG_SZ - 1) @%Systemroot%\system32\wbem\wmisvc.dll,-204 @@ -183,4 +177,162 @@ Upcoming versions will provide a more complete and user-friendly output. - (A;CI;;;;S-1-5-18) - (A;CI;;;;S-1-3-0) - (A;CI;;;;S-1-15-2-1) - - (A;CI;;;;S-1-15-3-1024-1065365936-1281604716-3511738428-1654721687-432734479-3232135806-4053264122-3456934681) \ No newline at end of file + - (A;CI;;;;S-1-15-3-1024-1065365936-1281604716-3511738428-1654721687-432734479-3232135806-4053264122-3456934681) + +======================================== +``save``: Save the registry to a file +======================================== + +The ``save`` function saves the entire registry or a specified root key to a file in a format similar to that of regedit export files. + +.. code-block:: bash + :caption: CLI usage example + + >>> [reg] HKLM\. > save C:\\my_SAM_backup.reg SAM + Backup option activated. + [INFO] Backup of SAM saved to C:\\my_SAM_backup.reg successful + Backup of SAM saved to C:\\my_SAM_backup + + +Notice that by default the access to the saved file is restricted to the Administrators group. +This is hardcoded in the current implementation. Future versions may include an option to customize the file permissions. +If you want to remove this hardcoded behavior, you can use the additional ``fsecurity`` option of the ``save`` function. +This option will not request any specific permissions when creating the file, and the +file **will inherit the default permissions of the parent directory**. Should you put a sensitive backup in +a directory with weak permissions, you may expose it to unauthorized access. + +.. code-block:: powershell + :caption: CLI usage example + + >>> PS C:\> Get-Acl .\my_SAM_backup.reg | fl + Path : Microsoft.PowerShell.Core\FileSystem::C:\my_SAM_backup.reg + Owner : BUILTIN\Administrators + Group : + Access : BUILTIN\Administrators Allow FullControl + Audit : + Sddl : O:BAG:DUD:P(A;;FA;;;BA) + + +======================================== +``create_key``: Create a new subkey +======================================== + +The ``create_key`` function creates a new subkey under the current key or a specified relative key. + +.. code-block:: bash + :caption: CLI usage example + + >>> [reg] HKLM\SOFTWARE\examples > ls + [reg] HKLM\SOFTWARE\examples > create_key MySubKey + Key MySubKey created successfully. + [reg] HKLM\SOFTWARE\examples > ls + MySubKey + [reg] HKLM\SOFTWARE\examples > + +======================================== +``delete_key``: Delete a subkey +======================================== + +The ``delete_key`` function deletes a specified subkey under the current key or a specified relative key. +Note that the subkey to be deleted must not have any subkeys. If it does, you need to delete them first. + +.. code-block:: bash + :caption: CLI usage example + + >>> [reg] HKLM\SOFTWARE\examples > ls + MySubKey + [reg] HKLM\SOFTWARE\examples > cd .. + [reg] HKLM\SOFTWARE > ls + Classes + Clients + DefaultUserEnvironment + examples + Google + Microsoft + ODBC + OEM + OpenSSH + Partner + Policies + RegisteredApplications + Setup + WOW6432Node + [reg] HKLM\SOFTWARE > delete_key examples + [ERROR] Error: 0x5 - ERROR_ACCESS_DENIED + [ERROR] Got status 0x5 while deleting key + [reg] HKLM\SOFTWARE > delete_key examples\\MySubKey + Key examples\MySubKey deleted successfully. + [reg] HKLM\SOFTWARE > delete_key examples + Key examples deleted successfully. + [reg] HKLM\SOFTWARE > ls + Classes + Clients + DefaultUserEnvironment + Google + Microsoft + ODBC + OEM + OpenSSH + Partner + Policies + RegisteredApplications + Setup + WOW6432Node + [reg] HKLM\SOFTWARE > + + +======================================== +``set_value``: Set or create a value +======================================== + +The ``set_value`` function sets the data of an existing value or creates a new value under the current key or a specified relative key. + +.. code-block:: bash + :caption: CLI usage example + + >>> [reg] HKLM\SOFTWARE\examples > set_value string 1 MyUnicodeString + [reg] HKLM\SOFTWARE\examples > cat + - string (REG_SZ - 1) MyUnicodeString + [reg] HKLM\SOFTWARE\examples > set_value string 2 %APPDATA%UnicodeString + [reg] HKLM\SOFTWARE\examples > cat + - string (REG_EXPAND_SZ - 2) %APPDATA%UnicodeString + [reg] HKLM\SOFTWARE\examples > set_value bin 3 01044923afebc000 + [reg] HKLM\SOFTWARE\examples > set_value mydword 4 012345 + [reg] HKLM\SOFTWARE\examples > set_value myBEdword 5 0123451238412304 + [reg] HKLM\SOFTWARE\examples > cat + - string (REG_EXPAND_SZ - 2) %APPDATA%UnicodeString + - bin (REG_BINARY - 3) b'01044923afebc000' + - mydword (REG_DWORD - 4) 12345 + - myBEdword (REG_DWORD_BIG_ENDIAN - 5) 123451238412304 + +Notice that: + +* the data for REG_BINARY values must be provided as a hexadecimal string. +* the data for REG_DWORD and REG_DWORD_BIG_ENDIAN values must be provided as a base-10 integer. +* it's not currently possible via the CLI to set value with spaces in their names or in the data. This is a limitation of the current CLI parser. + You can still use the Python API to set values with spaces in their names. Yet I agree this is not very user-friendly. + Future versions may include a more advanced CLI parser to handle this case. +* when setting a value that already exists, its data type is updated to the new type provided. +* it's not currently possible to set the default value of a key via the CLI. + +======================================== +``delete_value``: Delete a value +======================================== + +The ``delete_value`` function deletes a specified value under the current key or a specified relative key. + +.. code-block:: bash + :caption: CLI usage example + + >>> [reg] HKLM\SOFTWARE\examples > cat + - string (REG_EXPAND_SZ - 2) %APPDATA%UnicodeString + - bin (REG_BINARY - 3) b'01044923afebc000' + - mydword (REG_DWORD - 4) 12345 + - myBEdword (REG_DWORD_BIG_ENDIAN - 5) 123451238412304 + [reg] HKLM\SOFTWARE\examples > delete_value bin + Backup option activated. + Value bin deleted successfully. + [reg] HKLM\SOFTWARE\examples > cat + - string (REG_EXPAND_SZ - 2) %APPDATA%UnicodeString + - mydword (REG_DWORD - 4) 12345 + - myBEdword (REG_DWORD_BIG_ENDIAN - 5) 123451238412304 \ No newline at end of file diff --git a/scapyred/winreg.py b/scapyred/winreg.py index 697c31e..d5e2d62 100644 --- a/scapyred/winreg.py +++ b/scapyred/winreg.py @@ -1475,9 +1475,10 @@ def create_key(self, new_key: str, subkey: str | None = None) -> None: return None if subkey is None: - subkey_path = self.current_subkey_path + subkey_path = self._join_path(self.current_subkey_path, new_key) else: subkey_path = self._join_path(self.current_subkey_path, subkey) + subkey_path = self._join_path(subkey_path, new_key) # Log and execute logger.debug("Creating key %s under %s", new_key, subkey_path) From a572dec85d3eb08297c058740a7143492c3965f4 Mon Sep 17 00:00:00 2001 From: Ebrix Date: Wed, 27 Aug 2025 18:17:22 +0200 Subject: [PATCH 23/27] some doc --- doc/winreg.rst | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/doc/winreg.rst b/doc/winreg.rst index 68c62a5..69c661d 100644 --- a/doc/winreg.rst +++ b/doc/winreg.rst @@ -335,4 +335,26 @@ The ``delete_value`` function deletes a specified value under the current key or [reg] HKLM\SOFTWARE\examples > cat - string (REG_EXPAND_SZ - 2) %APPDATA%UnicodeString - mydword (REG_DWORD - 4) 12345 - - myBEdword (REG_DWORD_BIG_ENDIAN - 5) 123451238412304 \ No newline at end of file + - myBEdword (REG_DWORD_BIG_ENDIAN - 5) 123451238412304 + + +================================================ +``activate_backup``: Activate backup privilege +================================================ + +The ``activate_backup`` function activates the SeBackupPrivilege on the current session. +This privilege is required to perform certain operations, such as saving the registry to a file or most operations which modify the registry. +If you get an "Access Denied" error while performing such operations, try activating the backup privilege first. + +You can disable it via ``disable_backup`` function. + +======================================================== +``activate_exploration_mode``: Activate exploration mode +======================================================== + +The ``activate_exploration_mode`` function activates the exploration mode on the current session. +This mode is usefull when you want to explore the registry not knowing precisely what you are looking for. +It just do an ```ls`` and a ``cat`` when you ``cd`` into a new subkey. +This way you can quickly explore the registry without having to manually ``ls`` and ``cat`` each time. + +You can disable it via ``disable_exploration_mode`` function. \ No newline at end of file From bed19941860fa4f407516a4c02be0b2e27d2ae76 Mon Sep 17 00:00:00 2001 From: Ebrix Date: Wed, 27 Aug 2025 18:18:54 +0200 Subject: [PATCH 24/27] some doc --- doc/winreg.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/winreg.rst b/doc/winreg.rst index 69c661d..8ffae8e 100644 --- a/doc/winreg.rst +++ b/doc/winreg.rst @@ -188,10 +188,10 @@ The ``save`` function saves the entire registry or a specified root key to a fil .. code-block:: bash :caption: CLI usage example - >>> [reg] HKLM\. > save C:\\my_SAM_backup.reg SAM + >>> [reg] HKLM\. > save C:\my_SAM_backup.reg SAM Backup option activated. - [INFO] Backup of SAM saved to C:\\my_SAM_backup.reg successful - Backup of SAM saved to C:\\my_SAM_backup + [INFO] Backup of SAM saved to C:\my_SAM_backup.reg successful + Backup of SAM saved to C:\my_SAM_backup Notice that by default the access to the saved file is restricted to the Administrators group. From 49d95d0d9505f5ea76ea563ae673b1a44893e861 Mon Sep 17 00:00:00 2001 From: Ebrix Date: Thu, 28 Aug 2025 08:29:21 +0200 Subject: [PATCH 25/27] some docs --- doc/basic_windows_regedit.png | Bin 0 -> 58018 bytes doc/winreg.rst | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 doc/basic_windows_regedit.png diff --git a/doc/basic_windows_regedit.png b/doc/basic_windows_regedit.png new file mode 100644 index 0000000000000000000000000000000000000000..8bb8fda4ad41435ffce6190cd365086f89c9ae68 GIT binary patch literal 58018 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 Z8n!EQ+kQY;E&&GkD9ETvS4ckd`#(^#+ Date: Tue, 2 Sep 2025 08:26:41 +0200 Subject: [PATCH 26/27] some comments and close - dont know if it works tho --- doc/winreg.rst | 13 ++-- scapyred/winreg.py | 175 +++++++++++++++++++++++++++++++++------------ 2 files changed, 138 insertions(+), 50 deletions(-) diff --git a/doc/winreg.rst b/doc/winreg.rst index d0f8980..620ae86 100644 --- a/doc/winreg.rst +++ b/doc/winreg.rst @@ -125,12 +125,11 @@ The ``cat`` function displays the values of the current key or a specified relat - SvcMemMidLimitInMB (REG_DWORD - 4) 20 - SvcMemSoftLimitInMB (REG_DWORD - 4) 11 - Type (REG_DWORD - 4) 32 - - (REG_SZ - 1) This is the default value + - (Default) (REG_SZ - 1) This is the default value -Notice how the default value is represented with an empty name, when regedit shows it as "(Default)". -This is a design choice to avoid confusion with a value that would actually be named "(Default)". -Future development may include an option to display it as "(Default)" for better user experience. +The default value is represented with a bold blue ``(Default)`` in a similar fashion as regedit. +This is a design choice to avoid confusion with a value that would actually be named ``(Default)``. ======================================= @@ -316,7 +315,8 @@ Notice that: You can still use the Python API to set values with spaces in their names. Yet I agree this is not very user-friendly. Future versions may include a more advanced CLI parser to handle this case. * when setting a value that already exists, its data type is updated to the new type provided. -* it's not currently possible to set the default value of a key via the CLI. +* if you want to set the default value, set the value ``(Default)``. +* if you want to set the value ``(Default)`` but not the default value, use the parameter ``is_not_default`` ======================================== ``delete_value``: Delete a value @@ -341,6 +341,9 @@ The ``delete_value`` function deletes a specified value under the current key or - myBEdword (REG_DWORD_BIG_ENDIAN - 5) 123451238412304 +If you want to delete the default value: do not specify anyvalue. + + ================================================ ``activate_backup``: Activate backup privilege ================================================ diff --git a/scapyred/winreg.py b/scapyred/winreg.py index d5e2d62..3de5c8e 100644 --- a/scapyred/winreg.py +++ b/scapyred/winreg.py @@ -64,6 +64,7 @@ BaseRegOpenKey_Request, BaseRegEnumKey_Request, BaseRegEnumValue_Request, + BaseRegCloseKey_Request, BaseRegQueryValue_Request, BaseRegGetVersion_Request, BaseRegQueryInfoKey_Request, @@ -790,7 +791,7 @@ def close(self) -> None: # --------------------------------------------- # @CLIUtil.addcommand() - def use(self, root_path): + def use(self, root_path: str) -> None: """ Selects and sets the base registry key (root) to use for subsequent operations. @@ -965,7 +966,11 @@ def use_complete(self, root_key: str) -> list[str]: @CLIUtil.addcommand(spaces=True) def ls(self, subkey: str | None = None) -> list[str]: """ - EnumKeys of the current subkey path + Enumerate the subkeys of the given relative `subkey` + + :param subkey: the relative subkey to enumerate the subkey from. If None, uses the current subkey path. + + :return: the list of the subkeys. """ # Try to use the cache @@ -1049,16 +1054,10 @@ def cat(self, subkey: str | None = None) -> list[RegEntry]: If no subkey is specified, uses the current subkey path and caches results to avoid redundant RPC queries. Otherwise, enumerates values under the specified subkey path. - Args: - subkey (str | None): The subkey path to enumerate. If None or empty, uses the current subkey path. + :param subkey: the relative subkey path to enumerate. If None, uses the current subkey path. - Returns: - list[RegEntry]: A list of registry entries (as RegEntry objects) for the specified subkey path. + :return: a list of registry entries (as RegEntry objects) for the specified subkey path. Returns an empty list if the handle is invalid or an error occurs during enumeration. - - Side Effects: - - May print error messages to standard output if RPC queries fail. - - Updates internal cache for previously enumerated subkey paths. """ # Try to use the cache @@ -1162,12 +1161,25 @@ def cat_output(self, results: list[RegEntry]) -> None: for entry in results: if entry.reg_type == RegType.UNK: - print( - f" - {entry.reg_value:<20} {'(' + entry.reg_type.name + " - " + str(entry.reg_type.real_value) + ')':<15} {entry.reg_data}" - ) - print( - f" - {entry.reg_value:<20} {'(' + entry.reg_type.name + " - " + str(entry.reg_type.value) + ')':<15} {entry.reg_data}" - ) + if entry.reg_value == "\x00": + # Default value + print( + f" - {'\033[94;1m(Default)\033[0m':<28} {'(' + entry.reg_type.name + " - " + str(entry.reg_type.real_value) + ')':<15} {entry.reg_data}" + ) + else: + print( + f" - {entry.reg_value:<20} {'(' + entry.reg_type.name + " - " + str(entry.reg_type.real_value) + ')':<15} {entry.reg_data}" + ) + else: + if entry.reg_value == "\x00": + # Default value + print( + f" - {'\033[94;1m(Default)\033[0m':<28} {'(' + entry.reg_type.name + " - " + str(entry.reg_type.real_value) + ')':<15} {entry.reg_data}" + ) + else: + print( + f" - {entry.reg_value:<20} {'(' + entry.reg_type.name + " - " + str(entry.reg_type.value) + ')':<15} {entry.reg_data}" + ) @CLIUtil.addcomplete(cat) def cat_complete(self, subkey: str) -> list[str]: @@ -1184,6 +1196,8 @@ def cat_complete(self, subkey: str) -> list[str]: def cd(self, subkey: str) -> None: """ Change current subkey path + + :param subkey: the relative subkey to go to. Root keys shall not be provided here. """ if subkey.strip() == "": @@ -1257,6 +1271,10 @@ def disable_exploration_mode(self) -> None: def get_sd(self, subkey: str | None = None) -> SECURITY_DESCRIPTOR | None: """ Get the security descriptor of the current subkey. SACL are not retrieve at this point (TODO). + + :param: the relative subkey to get the security descriptor from. If None, it uses the current subkey path. + + :return: the SECURITY_DESCRIPTOR object if all went well. None otherwise. """ # Try to use the cache @@ -1264,7 +1282,7 @@ def get_sd(self, subkey: str | None = None) -> SECURITY_DESCRIPTOR | None: if handle is None: return None - # Log and execute + # Log and prepare request logger.debug("Getting security descriptor for %s", subkey) req = BaseRegGetKeySecurity_Request( hKey=handle, @@ -1320,7 +1338,8 @@ def query_info( """ Query information on the current subkey - :param subkey: The subkey to query. If None, it uses the current subkey path. + :param subkey: the relative subkey to query info from. If None, it uses the current subkey path. + :return: BaseRegQueryInfoKey_Response object containing information about the subkey. Returns None if the handle is invalid or an error occurs during the query. """ @@ -1331,7 +1350,7 @@ def query_info( logger.error("Could not get handle on the specified subkey.") return None - # Log and execute + # Log and prepare request logger.debug("Querying info for %s", subkey) req = BaseRegQueryInfoKey_Request( hKey=handle, @@ -1399,10 +1418,19 @@ def set_value( value_type: RegType | str, value_data: str, subkey: str | None = None, - ) -> None: + is_not_default: bool = False, + ) -> bool | None: """ Set a registry value in the current subkey. If no subkey is specified, it uses the current subkey path. + + :param value_name: name of the value to set. Use "(Default)" for the default value. + :param value_type: type of the value to set. Can be a RegType or a string representing the type. + :param value_data: data of the value to set. The input will be encoded based on the type. + :param subkey: relative subkey to set the value in + :param is_not_default: if set, the value_name will not be converted to the default value in the case were it equals "(Default)". + + :return: returns True if all went well, None otherwise. """ # Validate the value type @@ -1427,7 +1455,11 @@ def set_value( else: subkey_path = self._join_path(self.current_subkey_path, subkey) - # Log and execute + # look for default value + if value_name == "(Default)" and not is_not_default: + value_name = "" + + # Log and prepare request logger.debug( "Setting value %s of type %s in %s", value_name, @@ -1455,14 +1487,18 @@ def set_value( logger.error("Got status %s while setting value", hex(resp.status)) return None + return True + @CLIUtil.addcommand() - def create_key(self, new_key: str, subkey: str | None = None) -> None: + def create_key(self, new_key: str, subkey: str | None = None) -> bool | None: """ Create a new key named as the specified `new_key` under the `subkey`. If no subkey is specified, it uses the current subkey path. :param new_key: name a the new key to create :param subkey: relative subkey to create the the new key + + :return: returns True if all went well, None otherwise. """ # Try to use the cache @@ -1480,7 +1516,7 @@ def create_key(self, new_key: str, subkey: str | None = None) -> None: subkey_path = self._join_path(self.current_subkey_path, subkey) subkey_path = self._join_path(subkey_path, new_key) - # Log and execute + # Log and prepare request logger.debug("Creating key %s under %s", new_key, subkey_path) req = BaseRegCreateKey_Request( hKey=handle, @@ -1505,16 +1541,20 @@ def create_key(self, new_key: str, subkey: str | None = None) -> None: if not is_status_ok(resp.status): logger.error("Got status %s while creating key", hex(resp.status)) return None + print(f"Key {new_key} created successfully.") + return True @CLIUtil.addcommand(spaces=True) - def delete_key(self, subkey: str | None = None) -> None: + def delete_key(self, subkey: str | None = None) -> bool | None: """ Delete the specified subkey. If no subkey is specified, it uses the current subkey path. Proper same access rights are required to delete a key. By default we request MAXIMUM_ALLOWED. So no issue. - :param subkey: The subkey to delete. If None, it uses the current subkey path. + :param subkey: the relative subkey to delete. If None, it uses the current subkey path. + + :return: returns True if all went well, None otherwise. """ # Make sure that we have a backup activated @@ -1526,7 +1566,7 @@ def delete_key(self, subkey: str | None = None) -> None: else: subkey_path = self._join_path(self.current_subkey_path, subkey) - # Log and execute + # Log and prepare request logger.debug("Deleting key %s", subkey_path) req = BaseRegDeleteKey_Request( hKey=self.current_root_handle, @@ -1550,6 +1590,7 @@ def delete_key(self, subkey: str | None = None) -> None: return None print(f"Key {subkey} deleted successfully.") + return True @CLIUtil.addcomplete(delete_key) def delete_key_complete(self, subkey: str) -> list[str]: @@ -1560,13 +1601,16 @@ def delete_key_complete(self, subkey: str) -> list[str]: return self.ls_complete(subkey) @CLIUtil.addcommand() - def delete_value(self, value: str = "", subkey: str | None = None) -> None: + def delete_value(self, value: str = "", subkey: str | None = None) -> bool | None: """ Delete the specified value. If no subkey is specified, it uses the current subkey path. - If no value is specified, it will delete the default value of the subkey, but subkey cannot be specified. + If no value is specified, it will delete the default value of the subkey, but subkey cannot be specified in CLI mode. - :param subkey: The subkey to delete. If None, it uses the current subkey path. + :param value: the value to delete. + :param subkey: the relative subkey which holds the value to delete. If None, it uses the current subkey path. + + :return: returns True if all went well, None otherwise. """ # Make sure that we have a backup activated @@ -1586,7 +1630,7 @@ def delete_value(self, value: str = "", subkey: str | None = None) -> None: else: subkey_path = self._join_path(self.current_subkey_path, subkey) - # Log and execute + # Log and prepare request logger.debug("Deleting value %s in %s", value, subkey_path) req = BaseRegDeleteValue_Request( hKey=handle, @@ -1608,6 +1652,7 @@ def delete_value(self, value: str = "", subkey: str | None = None) -> None: return None print(f"Value {value} deleted successfully.") + return True @CLIUtil.addcomplete(delete_value) def delete_value_complete(self, value: str) -> list[str]: @@ -1635,15 +1680,18 @@ def save( output_path: str | None = None, subkey: str | None = None, fsecurity: bool = False, - ) -> None: + ) -> bool | None: """ Backup the current subkey to a file. If no subkey is specified, it uses the current subkey path. If no output_path is specified, it will be saved in the `%WINDIR%\\System32` directory with the name of the subkey and .reg extension. + By default it saves the backup to a file protected so that only BA can read it. :param output_path: The path to save the backup file. If None, it defaults to the current subkey name with .reg extension. If the output path ends with .reg, it uses it as is, otherwise it appends .reg to the output path. - :param subkey: The subkey to backup. If None, it uses the current subkey path. - :return: None, by default it saves the backup to a file protected so that only BA can read it. + :param subkey: the relative subkey to backup. If None, it uses the current subkey path. + :param fsecurity: do not set security descriptor of the backup. Let it be inherited from its parent folder. + + :return: returns True if all went well, None otherwise. """ # Make sure that we have a backup activated @@ -1687,7 +1735,7 @@ def save( ) sa.nLength = len(sa) - # Log and execute + # Log and prepare request logger.debug("Backing up %s to %s", key_to_save, output_path) req = BaseRegSaveKey_Request( hKey=handle, @@ -1702,13 +1750,15 @@ def save( # Check the response status if not is_status_ok(resp.status): logger.error("Got status %s while backing up", hex(resp.status)) - else: - logger.info( - "Backup of %s saved to %s.reg successful ", - self.current_subkey_path, - output_path, - ) - print(f"Backup of {self.current_subkey_path} saved to {output_path}") + return None + + logger.info( + "Backup of %s saved to %s.reg successful ", + self.current_subkey_path, + output_path, + ) + print(f"Backup of {self.current_subkey_path} saved to {output_path}") + return True # --------------------------------------------- # # Operation options @@ -1728,8 +1778,8 @@ def activate_backup(self) -> None: self.extra_options |= RegOptions.REG_OPTION_BACKUP_RESTORE # Log and print - print("Backup option activated.") logger.debug("Backup option activated.") + print("Backup option activated.") # Clear the local cache, as the backup option will change the behavior of the registry self._clear_all_caches() @@ -1748,8 +1798,8 @@ def disable_backup(self) -> None: self.extra_options &= ~RegOptions.REG_OPTION_BACKUP_RESTORE # Log and print - print("Backup option deactivated.") logger.debug("Backup option deactivated.") + print("Backup option deactivated.") # Clear the local cache, as the backup option will change the behavior of the registry self._clear_all_caches() @@ -1764,6 +1814,9 @@ def activate_volatile(self) -> None: self.extra_options |= RegOptions.REG_OPTION_VOLATILE self.extra_options &= ~RegOptions.REG_OPTION_NON_VOLATILE self.use(self.current_root_path) + + # Log and print + logger.debug("Volatile option activated.") print("Volatile option activated.") self._clear_all_caches() @@ -1778,7 +1831,11 @@ def disable_volatile(self) -> None: self.extra_options &= ~RegOptions.REG_OPTION_VOLATILE self.extra_options |= RegOptions.REG_OPTION_NON_VOLATILE self.use(self.current_root_path) + + # Log and print + logger.debug("Volatile option deactivated.") print("Volatile option deactivated.") + self._clear_all_caches() # --------------------------------------------- # @@ -1796,6 +1853,7 @@ def get_handle_on_subkey( :param subkey_path: The subkey path to get a handle on. :param desired_access_rights: The desired access rights for the subkey. If None, defaults to read access rights. + :return: An NDRContextHandle on success, None on failure. """ @@ -1818,7 +1876,7 @@ def get_handle_on_subkey( AccessRights.KEY_READ | AccessRights.STANDARD_RIGHTS_READ ) - # Log and execute + # Log and prepare request logger.debug( "Getting handle on subkey: %s with access rights: %s", subkey_path, @@ -1860,6 +1918,7 @@ def _get_cached_elt( :param subkey: The subkey path to retrieve. If None, uses the current subkey path. :param cache_name: The name of the cache to use. If None, does not use cache. :param desired_access: The desired access rights for the subkey. If None, defaults to read access rights. + :return: A CacheElt object if cache_name is provided, otherwise an NDRContextHandle. """ @@ -1913,6 +1972,7 @@ def _join_path( :param first_path: The first path to join. :param second_path: The second path to join. + :return: A PureWindowsPath object representing the combined path. """ @@ -1939,6 +1999,7 @@ def _require_root_handles(self, silent: bool = False) -> bool: Check if we have a root handle set. :param silent: If True, do not print any message if no root handle is set. + :return: True if no root handle is set, False otherwise. """ @@ -1948,6 +2009,27 @@ def _require_root_handles(self, silent: bool = False) -> bool: return True return False + def _close_key(self, handle: NDRContextHandle) -> bool | None: + + # Log and prepare request + logger.debug("Closing hKey %d", handle) + req = BaseRegCloseKey_Request( + hKe=handle, + ) + + # Send request + resp = self.client.sr1_req(req) + + # Check the response status + if not is_status_ok(resp.status): + logger.error( + "Got status %s while getting handle on key", + hex(resp.status), + ) + return None + + return True + def _clear_all_caches(self) -> None: """ Clear all caches @@ -1963,7 +2045,10 @@ def dev(self) -> NoReturn: """ logger.info("Jumping into the code for dev purpose...") - # pylint: disable=forgotten-debug-statement, pointless-statement + # pylint: disable=forgotten-debug-statement, pointless-statement, import-outside-toplevel, unused-import + from IPython import embed + + print("type: embed()") breakpoint() From f51e027e0adc7d8498908a0db7bb63d72175f95d Mon Sep 17 00:00:00 2001 From: Ebrix Date: Wed, 3 Sep 2025 08:15:52 +0200 Subject: [PATCH 27/27] Added close function --- doc/winreg.rst | 4 ++++ scapyred/winreg.py | 43 ++++++++++++++++++++++++++++++++----------- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/doc/winreg.rst b/doc/winreg.rst index 620ae86..544bc6b 100644 --- a/doc/winreg.rst +++ b/doc/winreg.rst @@ -51,6 +51,8 @@ The available root keys are: >>> scapy-winreg --UPN Administrator@DOM.LOCAL --password Passw0rd 10.0.0.10 --rootKey HKLM >>> [reg] HKLM\. > +This activation empties the cache of the handles. + ==================== ``ls``: List subkeys ==================== @@ -352,6 +354,8 @@ The ``activate_backup`` function activates the SeBackupPrivilege on the current This privilege is required to perform certain operations, such as saving the registry to a file or most operations which modify the registry. If you get an "Access Denied" error while performing such operations, try activating the backup privilege first. +This activation empties the cache of the handles to avoid any conflict with the new privilege. + You can disable it via ``disable_backup`` function. ======================================================== diff --git a/scapyred/winreg.py b/scapyred/winreg.py index 3de5c8e..2b039a3 100644 --- a/scapyred/winreg.py +++ b/scapyred/winreg.py @@ -1007,7 +1007,9 @@ def ls(self, subkey: str | None = None) -> list[str]: # Check the response status elif not is_status_ok(resp.status): logger.error("Got status %s while enumerating keys", hex(resp.status)) - self.cache["ls"].pop(subkey_path, None) + c_elt = self.cache["ls"].pop(subkey_path, None) + if c_elt is not None: + self._close_key(c_elt.handle) return [] self.cache["ls"][subkey_path].values.append( @@ -1101,7 +1103,9 @@ def cat(self, subkey: str | None = None) -> list[RegEntry]: # Check the response status elif not is_status_ok(resp.status): logger.error("got status %s while enumerating values", hex(resp.status)) - self.cache["cat"].pop(subkey_path, None) + c_elt = self.cache["cat"].pop(subkey_path, None) + if c_elt is not None: + self._close_key(c_elt.handle) return [] # Get the value name and type @@ -1131,7 +1135,9 @@ def cat(self, subkey: str | None = None) -> list[RegEntry]: # Check the response status if not is_status_ok(resp2.status): logger.error("got status %s while querying value", hex(resp2.status)) - self.cache["cat"].pop(subkey_path, None) + c_elt = self.cache["cat"].pop(subkey_path, None) + if c_elt is not None: + self._close_key(c_elt.handle) return [] value = ( @@ -1480,7 +1486,9 @@ def set_value( # We remove the entry from the cache if it exists # Even if the response status is not OK, we want to remove it if subkey_path in self.cache["cat"]: - self.cache["cat"].pop(subkey_path, None) + c_elt = self.cache["cat"].pop(subkey_path, None) + if c_elt is not None: + self._close_key(c_elt.handle) # Check the response status if not is_status_ok(resp.status): @@ -1533,9 +1541,13 @@ def create_key(self, new_key: str, subkey: str | None = None) -> bool | None: # We remove the entry from the cache if it exists # Even if the response status is not OK, we want to remove it if subkey_path.parent in self.cache["ls"]: - self.cache["ls"].pop(subkey_path.parent, None) + c_elt = self.cache["ls"].pop(subkey_path.parent, None) + if c_elt is not None: + self._close_key(c_elt.handle) if subkey_path in self.cache["cat"]: - self.cache["cat"].pop(subkey_path, None) + c_elt = self.cache["cat"].pop(subkey_path, None) + if c_elt is not None: + self._close_key(c_elt.handle) # Check the response status if not is_status_ok(resp.status): @@ -1580,9 +1592,13 @@ def delete_key(self, subkey: str | None = None) -> bool | None: # We remove the entry from the cache if it exists # Even if the response status is not OK, we want to remove it if subkey_path.parent in self.cache["ls"]: - self.cache["ls"].pop(subkey_path.parent, None) + c_elt = self.cache["ls"].pop(subkey_path.parent, None) + if c_elt is not None: + self._close_key(c_elt.handle) if subkey_path in self.cache["cat"]: - self.cache["cat"].pop(subkey_path, None) + c_elt = self.cache["cat"].pop(subkey_path, None) + if c_elt is not None: + self._close_key(c_elt.handle) # Check the response status if not is_status_ok(resp.status): @@ -1644,7 +1660,9 @@ def delete_value(self, value: str = "", subkey: str | None = None) -> bool | Non # We remove the entry from the cache if it exists # Even if the response status is not OK, we want to remove it if subkey_path in self.cache["cat"]: - self.cache["cat"].pop(subkey_path, None) + c_elt = self.cache["cat"].pop(subkey_path, None) + if c_elt is not None: + self._close_key(c_elt.handle) # Check the response status if not is_status_ok(resp.status): @@ -2012,9 +2030,10 @@ def _require_root_handles(self, silent: bool = False) -> bool: def _close_key(self, handle: NDRContextHandle) -> bool | None: # Log and prepare request - logger.debug("Closing hKey %d", handle) + logger.debug("Closing hKey %s - %s", handle.uuid, handle.uuid.hex()) req = BaseRegCloseKey_Request( - hKe=handle, + hKey=handle, + ndr64=True, ) # Send request @@ -2036,6 +2055,8 @@ def _clear_all_caches(self) -> None: """ for _, c in self.cache.items(): + for c_elt in c.values(): + self._close_key(c_elt.handle) c.clear() @CLIUtil.addcommand()