There are times that you may need to push out a change to all existing user profiles and to new profiles that are created on a system. I’ve seen a few PowerShell scripts floating around out there, but they didn’t seem to work for Windows 7 SP1. You may or may not be surprised, but there are many organizations that still run Windows 7. The script is actually pretty simple.

Here is the breakdown of the script:

  • Enumerate all the existing user profiles
  • Add the .DEFAULT user profile to the list of existing user profiles
  • Iterate through all the profiles
    • If the profile hive is not loaded, load it
    • Manipulate the users’ registry
    • If the profile hive was loaded by the script, unload it
  • Finished

Enumerate all the existing user profiles

Using the registry path below, we can find a list of all the user profiles on the system and where the profile path exists. Every user profile has the file NTuser.dat which contains the registry hive that is loaded into the HKEY_USERS and HKCU when a user logs on to the system. This NTuser.dat can for example also be loaded when using RunAs.exe. It will then only show up in HKEY_USERS\<users’ SID>

# Get each user profile SID and Path to the profile
$UserProfiles = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | Where {$_.PSChildName -match "S-1-5-21-(\d+-?){4}$" } | Select-Object @{Name="SID"; Expression={$_.PSChildName}}, @{Name="UserHive";Expression={"$($_.ProfileImagePath)\NTuser.dat"}}

Add the .DEFAULT user profile to the list of existing user profiles

If you need to manipulate the registry of all new profiles, then you’ll need to add the following code. The .DEFAULT user information does not exist in the registry key information above.

# Add in the .DEFAULT User Profile
$DefaultProfile = "" | Select-Object SID, UserHive
$DefaultProfile.SID = ".DEFAULT"
$DefaultProfile.Userhive = "C:\Users\Public\NTuser.dat"
$UserProfiles += $DefaultProfile

Iterate through all the profiles

This is the main code where we will determine if we need to load or unload any user registry hives. It is also where the registry changes will be made.

# Loop through each profile on the machine</p>
Foreach ($UserProfile in $UserProfiles) {
    # Load User ntuser.dat if it's not already loaded
    If (($ProfileWasLoaded = Test-Path Registry::HKEY_USERS\$($UserProfile.SID)) -eq $false) {
        Start-Process -FilePath "CMD.EXE" -ArgumentList "/C REG.EXE LOAD HKU\$($UserProfile.SID) $($UserProfile.UserHive)" -Wait -WindowStyle Hidden
    }

Manipulate the users’ registry

This is the area where you can create, delete or modify the registry. After the changes are made, the profile will be unloaded. Upon the next logon, the changes will come into effect.

    # Manipulate the registry
    $key = "Registry::HKEY_USERS\$($UserProfile.SID)\Software\SomeArchaicSoftware\Configuration"
    New-Item -Path $key -Force | Out-Null
    New-ItemProperty -Path $key -Name "LoginURL" -Value "https://www.myCompany.local" -PropertyType STRING -Force | Out-Null
    New-ItemProperty -Path $key -Name "DisplayWelcome" -Value 0x00000001 -PropertyType DWORD -Force | Out-Null

    $key = "$key\UserInfo"
    New-Item -Path $key -Force | Out-Null
    New-ItemProperty -Path $key -Name "LoginName" -Value "$($ENV:USERDOMAIN)\$($ENV:USERNAME)" -PropertyType STRING -Force | Out-Null

If the profile hive was loaded by the script, unload it

This is another area that is easier to just call out to REG.EXE again to unload the registry. One issue to keep in mind is that if any handles are open to the registry, they need to be closed. If they’re not closed, you’ll get “Access Denied” when trying to unload the registry hive. This is why I’ve added the Garbage Collector. This cleans up all open handles [gc]::Collector. I also noticed that if I was opening and closing registry hives too fast, they all weren’t being closed. I’m guessing this is due to a race condition. I added a Start-Sleep 1 and this fixed the problem for me.

    # Unload NTuser.dat        
    If ($ProfileWasLoaded -eq $false) {
        [gc]::Collect()
        Start-Sleep 1
        Start-Process -FilePath "CMD.EXE" -ArgumentList "/C REG.EXE UNLOAD HKU\$($UserProfile.SID)" -Wait -WindowStyle Hidden| Out-Null
    }

Happy coding and I hope this helps you solve whatever your problem was!