python / cpython

The Python programming language
https://www.python.org
Other
63.17k stars 30.25k forks source link

Winreg makes persistent changes somewhere, but not to the Windows registry #121022

Closed Armingleggings closed 3 months ago

Armingleggings commented 4 months ago

Bug report

Bug description:

Below is a simplified version of my script to demonstrate the issue. The registry changes in question remove some of the more annoying "features" of Windows, but it's advised to make a copy of the registry before running the code regardless.

Anyway, the code is supposed to test open registry keys and will create them if not found. It also creates values with given data. The thing is that it all seems to work and even has persistence between system restarts. That suggests that SOMETHING is changing, but it's not the Windows registry:

import winreg

def hive_name(hive):
    if hive == winreg.HKEY_CURRENT_USER:
        return "HKEY_CURRENT_USER"
    elif hive == winreg.HKEY_LOCAL_MACHINE:
        return "HKEY_LOCAL_MACHINE"
    elif hive == winreg.HKEY_CLASSES_ROOT:
        return "HKEY_CLASSES_ROOT"
    elif hive == winreg.HKEY_USERS:
        return "HKEY_USERS"
    elif hive == winreg.HKEY_PERFORMANCE_DATA:
        return "HKEY_PERFORMANCE_DATA"
    elif hive == winreg.HKEY_CURRENT_CONFIG:
        return "HKEY_CURRENT_CONFIG"
    else:
        return "UNKNOWN_HIVE"

def open_or_create_key(hive, path):
    try:
        # Open the registry key for reading and writing in 64-bit view
        key = winreg.OpenKey(hive, path, 0, winreg.KEY_READ | winreg.KEY_WRITE | winreg.KEY_WOW64_64KEY)
        print(f"Key opened: {hive_name(hive)}\\{path}")
    except FileNotFoundError:
        # Handle if the key doesn't exist
        print(f"Creating key: {hive_name(hive)}\\{path}")
        key = winreg.CreateKeyEx(hive, path, 0, winreg.KEY_READ | winreg.KEY_WRITE | winreg.KEY_WOW64_64KEY)
    except PermissionError:
        # Handle if there are permission issues
        print(f"Permission denied while accessing the key: {hive_name(hive)}\\{path}")
        key = None
    except Exception as e:
        # Handle any other exceptions
        print(f"An error occurred: {e}")
        key = None
    return key

def get_value(key,which):
    try:
        value, _ = winreg.QueryValueEx(key, which)
        print(f"Current value: {value}")
    except FileNotFoundError:
        print("Current value: <not set>")
    except Exception as e:
        print(f"An error occurred while querying the value: {e}")

def set_value(key,which,what):
    try:
        winreg.SetValueEx(key, which, 0, winreg.REG_DWORD, what)
        print (which, "was set to", what)
    except FileNotFoundError:
        print (which, "could not be set to", what)

def close_key(key):
    if key:
        winreg.CloseKey(key)
        print("Key closed.")

# Test the open_or_create_key function
if __name__ == "__main__":

    print("# This key does exist on my system and has tons of values")
    print("# Expected Output: Key opened: HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced")
    key = open_or_create_key(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced")
    print("# Value name DisallowShaking is NOT in my registry.")
    print("# Expected Output: Current value: <not set> ")
    # Prevents the windows "feature" of minimizing everything if you "shake" a window while dragging. Does nothing if the value isn't actually set.
    get_value(key,"DisallowShaking")
    set_value(key,"DisallowShaking",1)
    get_value(key,"DisallowShaking")    print("# Value name HideFileExt IS in my registry.")
    print("# Expected Output: HideFileExt set to X (where X is the value set in the code) - needs to be checked in the registry to see if it changed between runs")
    # enables or disables hiding of file extensions. 0 to not hide it.
    set_value(key,"HideFileExt",1)
    close_key(key)

    # This restores the Windows 10 right-click context menu to your system in Windows 11 if the key is present. It has no effect when left
    print("# Neither {86ca1aa0-34aa-4e8b-a509-50c905bae2a2} nor InprocServer32 exist in my registry.")
    print("# Expected Output: Creating Key: Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32")
    key = open_or_create_key(winreg.HKEY_CURRENT_USER, r"Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32")
    close_key(key)

    # Setting this key and vlue restores the Windows 10 Windows Explorer ribbon. 
    print("# The Blocked key does not exist in my registry.")
    print("# Expected Output: Creating Key: SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions\Blocked")
    key = open_or_create_key(winreg.HKEY_CURRENT_USER, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions\Blocked")
    print("# If the key were created, then I can test for value {e2bf9676-5f8f-435c-97eb-11607a5bedf7} which should not exist yet.")
    print("# Expected Output: An error occurred while querying the value:  ")
    get_value(key,"{e2bf9676-5f8f-435c-97eb-11607a5bedf7}")
    set_value(key,"{e2bf9676-5f8f-435c-97eb-11607a5bedf7}", 1)
    close_key(key)

It's not a problem with 32bit vs 64 and isn't related to permissions (the code is running as admin and so is my account). It's not the "virtualstore" and even if it was, I could have found the unique value set ("disallowshaking" for example) somewhere in the registry - even if in an unexpected place. However, this value is not ANYWHERE in the registry. I did a full search to be sure.

To test, I'm using Regedit, though I did also check with Powershell a few times just to be sure they were both seeing the same thing. I've posted this problem to StackOverflow, Reddit, and the discourse forums here with no luck.

Having exhausted all suggestions and guides possible, I'm filing this as a bug.

CPython versions tested on:

3.10

Operating systems tested on:

Windows

zware commented 4 months ago

I cannot reproduce the failure. Your script ran as expected (after fixing the SyntaxError), and all changes were immediately visible in regedit.exe.

eryksun commented 4 months ago

I'm curious about the actual opened paths of those 3 keys. Please reply with the output of the following script:

import ctypes
import ctypes.wintypes as wintypes
import winreg

ntdll = ctypes.WinDLL('ntdll')

ObjectNameInformation = 1
OBJECT_INFORMATION_CLASS = wintypes.DWORD

class UNICODE_STRING(ctypes.Structure):
    _fields_ = (('Length',        wintypes.USHORT),
                ('MaximumLength', wintypes.USHORT),
                ('Buffer',        ctypes.POINTER(wintypes.WCHAR)))
    @property
    def value(self):
        length = self.Length // ctypes.sizeof(wintypes.WCHAR)
        if length and self.Buffer:
            if self.Buffer[length - 1] == '\x00':
                length -= 1
            return self.Buffer[:length]
        return ''

def _check_status(status, func, args):
    if status < 0:
        raise ctypes.WinError(ntdll.RtlNtStatusToDosError(status))
    return args

ntdll.NtQueryObject.errcheck = _check_status
ntdll.NtQueryObject.argtypes = (
    wintypes.HANDLE,          # Handle
    OBJECT_INFORMATION_CLASS, # ObjectInformationClass
    wintypes.LPVOID,          # ObjectInformation
    wintypes.ULONG,           # ObjectInformationLength
    wintypes.PULONG)          # ReturnLength

def get_object_name(handle):
    buf = (wintypes.CHAR * 65536)()
    ntdll.NtQueryObject(handle, ObjectNameInformation, buf,
                        ctypes.sizeof(buf), None)
    return UNICODE_STRING.from_buffer(buf).value

def open_or_create_key(hkey, subkey, access=winreg.KEY_READ):
    try:
        return winreg.OpenKey(hkey, subkey, 0, access)
    except FileNotFoundError:
        return winreg.CreateKeyEx(hkey, subkey, 0, access)

subkey_list = [
    r'Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}'
        r'\InprocServer32',
    r'Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced',
    r'Software\Microsoft\Windows\CurrentVersion\Shell Extensions\Blocked',
]

hkey = winreg.HKEY_CURRENT_USER
access = winreg.KEY_READ | winreg.KEY_WRITE | winreg.KEY_WOW64_64KEY
for subkey in subkey_list:
    with open_or_create_key(hkey, subkey, access) as hsubkey:
        print(get_object_name(hsubkey.handle))
Armingleggings commented 3 months ago

I'm curious about the actual opened paths of those 3 keys. Please reply with the output of the following script:

import ctypes
import ctypes.wintypes as wintypes
import winreg

ntdll = ctypes.WinDLL('ntdll')

ObjectNameInformation = 1
OBJECT_INFORMATION_CLASS = wintypes.DWORD

class UNICODE_STRING(ctypes.Structure):
    _fields_ = (('Length',        wintypes.USHORT),
                ('MaximumLength', wintypes.USHORT),
                ('Buffer',        ctypes.POINTER(wintypes.WCHAR)))
    @property
    def value(self):
        length = self.Length // ctypes.sizeof(wintypes.WCHAR)
        if length and self.Buffer:
            if self.Buffer[length - 1] == '\x00':
                length -= 1
            return self.Buffer[:length]
        return ''

def _check_status(status, func, args):
    if status < 0:
        raise ctypes.WinError(ntdll.RtlNtStatusToDosError(status))
    return args

ntdll.NtQueryObject.errcheck = _check_status
ntdll.NtQueryObject.argtypes = (
    wintypes.HANDLE,          # Handle
    OBJECT_INFORMATION_CLASS, # ObjectInformationClass
    wintypes.LPVOID,          # ObjectInformation
    wintypes.ULONG,           # ObjectInformationLength
    wintypes.PULONG)          # ReturnLength

def get_object_name(handle):
    buf = (wintypes.CHAR * 65536)()
    ntdll.NtQueryObject(handle, ObjectNameInformation, buf,
                        ctypes.sizeof(buf), None)
    return UNICODE_STRING.from_buffer(buf).value

def open_or_create_key(hkey, subkey, access=winreg.KEY_READ):
    try:
        return winreg.OpenKey(hkey, subkey, 0, access)
    except FileNotFoundError:
        return winreg.CreateKeyEx(hkey, subkey, 0, access)

subkey_list = [
    r'Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}'
        r'\InprocServer32',
    r'Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced',
    r'Software\Microsoft\Windows\CurrentVersion\Shell Extensions\Blocked',
]

hkey = winreg.HKEY_CURRENT_USER
access = winreg.KEY_READ | winreg.KEY_WRITE | winreg.KEY_WOW64_64KEY
for subkey in subkey_list:
    with open_or_create_key(hkey, subkey, access) as hsubkey:
        print(get_object_name(hsubkey.handle))

Output is:

\Registry\User\S-1-5-21-2848259066-2618722447-3046218285-1006_Classes\CLSID{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32 \REGISTRY\USER\S-1-5-21-2848259066-2618722447-3046218285-1006\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced \REGISTRY\USER\S-1-5-21-2848259066-2618722447-3046218285-1006\Software\Microsoft\Windows\CurrentVersion\Shell Extensions\Blocked

eryksun commented 3 months ago

Given your account's SID is "S-1-5-21-2848259066-2618722447-3046218285-1006", then those are the expected paths. There's no redirection occurring, such as to the lookaside key in "HKLM\Software\Microsoft\AppModel\Lookaside\user".

Are you using a standard account or an administrator account? I use a standard account and the system "Administrator" account, so elevating a shell or regedit runs in the context of the "Administrator" account. I have to keep in mind that HKCU when elevated refers to that profile instead of my normal profile.

Armingleggings commented 3 months ago

My account is an admin account. I run VScode as admin. Regedit is run as admin. If I close VScode and run as normal user, the ouput of the code you provided is the same as above. If I don't run VScode as admin, when I try to do "flask run" in the dedicated terminal, it returns to prompt without loading the server. I get the same behavior in Windows Terminal outside of VScode.

Just in case, I hardcoded my user id into the command like so:

open_or_create_key(winreg.HKEY_USERS, r"S-1-5-21-2848259066-2618722447-3046218285-1006\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced")

It made no difference.

Armingleggings commented 3 months ago

If it helps, here is my most updated code - the whole project: https://github.com/Armingleggings/Paneless/blob/main/paneless.py

Armingleggings commented 3 months ago

Someone suggested reinstalling python so I did. I upgraded from 3.10 to 3.12 and rebuilt my venv with new flask install and it works!

eryksun commented 3 months ago

I was going to ask you to run Sysinternals procmon to record all registry operations for the Python process. This would test an hypothesis that something is hooking registry access in the kernel to make operations seem transparently successful but silently redirecting to another hive. As is, can you confirm that the new Python installation is still accessing the profile for user SID "S-1-5-21-2848259066-2618722447-3046218285-1006"?

I'll have to write this one off to the work of a ghost in the machine.