Modifying the Registry for All Users

We’re going to look at modifying the registry for all users whether or not a user is logged into a machine. This is a continuation of my last blog post – Modifying the Registry of Another User.

As a quick refresher, we learned how to modify a user’s registry (HKEY_CURRENT USER or HKEY_USERS) without having that user logged onto a machine. We had to load and unload their NTUSER.DAT file separately in the HKEY_USERS registry hive.

It was pretty exciting.

Now, we’re going to add to that excitement by learning how to do it for all users instead of only specific users.

Modifying the Registry for All Users

Before we can modify the registry for all users, we need to be able to go out and grab all the ntuser.dat files so that we can load them as we did in the last blog post.

I know what you’re thinking. You’re thinking that’s easy! We know that the ntuser.dat file is in the C:\Users\<Username>\ directory, so that should be as simple as searching through C:\Users for any ntuser.dat file, right?!

This will only work if nobody is logged into a machine. We have to take into consideration any currently-logged on users. Any currently-logged on users will already have their ntuser.dat files loaded into the registry. This includes users who forget to log off. Even though their session is disconnected and somebody else has logged on, their registry is still loaded in the registry.

Here’s an example of this. I’m currently logged into my test machine. There is also a disconnected user Reg who forgot to log off:

PS Blog - Registry - HKU example

If I try loading Reg’s ntuser.dat, I encounter an error telling me that ntuser.dat is already being used by something else.

PS Blog - Registry - Cannot load ntuser.dat

So, what do we do?

We need to find all users on a machine and compare it with all currently-logged on user security identifiers (SIDs).

Find all users and their SIDs

Fortunately for us, there is a convenient location in the registry that stores the users on a machine and their SIDs.

HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*

This location will have a list of all the SIDs for a machine as well as some other properties. We’re interested in the SIDs that start with S-1-5-21. Notice that you see the two SIDs from an earlier screenshot:

PS Blog - Registry - ProfileList example modifying the registry

From this, we are able to use regular expressions and some calculated properties to select some great information with PowerShell. We’ll use the Get-ItemProperty cmdlet to get that information from the registry.

$PatternSID = 'S-1-5-21-\d+-\d+\-\d+\-\d+$'
 Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*' | Where-Object {$_.PSChildName -match $PatternSID} |
     select  @{name="SID";expression={$_.PSChildName}},
             @{name="UserHive";expression={"$($_.ProfileImagePath)\ntuser.dat"}},
             @{name="Username";expression={$_.ProfileImagePath -replace '^(.*[\\\/])', ''}}

Now we have a list of the usernames and their associated SIDs.

Getting SID of users in HKEY_USERS

Next, we’ll need to compare those SIDs with the SIDs of the users that are currently logged on and have their registry’s loaded to HKEY_USERS:

Get-ChildItem Registry::HKEY_USERS | Where-Object {$_.PSChildName -match $PatternSID} | select PSChildName

Easy peasy.

Putting it all together

Now, we just need to compare the two lists of SIDs and we’ll be able to modify the registry at will. I’ve compiled it all into a template that somebody could use to read or modify the registry of each user on a machine. In my example, I load each registry (if not loaded) and attempt to read the Uninstall key at HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*

This will show me which users have per-user installs of software as well as the software name:

 

# Regex pattern for SIDs
$PatternSID = 'S-1-5-21-\d+-\d+\-\d+\-\d+$'
 
# Get Username, SID, and location of ntuser.dat for all users
$ProfileList = gp 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*' | Where-Object {$_.PSChildName -match $PatternSID} | 
    Select  @{name="SID";expression={$_.PSChildName}}, 
            @{name="UserHive";expression={"$($_.ProfileImagePath)\ntuser.dat"}}, 
            @{name="Username";expression={$_.ProfileImagePath -replace '^(.*[\\\/])', ''}}
 
# Get all user SIDs found in HKEY_USERS (ntuder.dat files that are loaded)
$LoadedHives = gci Registry::HKEY_USERS | ? {$_.PSChildname -match $PatternSID} | Select @{name="SID";expression={$_.PSChildName}}
 
# Get all users that are not currently logged
$UnloadedHives = Compare-Object $ProfileList.SID $LoadedHives.SID | Select @{name="SID";expression={$_.InputObject}}, UserHive, Username
 
# Loop through each profile on the machine
Foreach ($item in $ProfileList) {
    # Load User ntuser.dat if it's not already loaded
    IF ($item.SID -in $UnloadedHives.SID) {
        reg load HKU\$($Item.SID) $($Item.UserHive) | Out-Null
    }
 
    #####################################################################
    # This is where you can read/modify a users portion of the registry 
 
    # This example lists the Uninstall keys for each user registry hive
    "{0}" -f $($item.Username) | Write-Output
    Get-ItemProperty registry::HKEY_USERS\$($Item.SID)\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | 
        Foreach {"{0} {1}" -f "   Program:", $($_.DisplayName) | Write-Output}
    Get-ItemProperty registry::HKEY_USERS\$($Item.SID)\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | 
        Foreach {"{0} {1}" -f "   Program:", $($_.DisplayName) | Write-Output}
    
    #####################################################################
 
    # Unload ntuser.dat        
    IF ($item.SID -in $UnloadedHives.SID) {
        ### Garbage collection and closing of ntuser.dat ###
        [gc]::Collect()
        reg unload HKU\$($Item.SID) | Out-Null
    }
}

Final Notes

Use this information with a healthy dose of caution. It is never wise to modify the registry without a good reason, and even some good reasons aren’t always great justification. In other words, be responsible and test your scripts before using on production systems. We cannot be held responsible for any issues that you may encounter.

Happy PowerShelling!

Buy PDQ Deploy

16 responses

  • Great script, Thanks for sharing! I used your script to clear out some Remote desktop connections on Windows 7 machines.

  • Great script thank you. We are trying to get it to run from a Computer Group Policy to clean out Citrix Receiver Keys. Script runs but doesn’t return any values in the logging and makes no changes.
    If run manually once a local user is logged in it succeeds and deletes the HKCU keys.
    Is it unable to reach ntuser.dat \ HKCU before a user is physically logged in?
    Thanks!

    • Thanks for the comment and I’m sorry to hear you’re running into some issues!

      It sounds like you’re running this as a Computer Startup script via GPO. Are you setting it as a regular Script or a PowerShell script? When creating a Startup script, in the Startup Properties window there should be two tabs: Scripts and PowerShell Scripts. Be certain to use the PowerShell Scripts tab to configure your script. I’ve had this trip me up before since the default tab is “Scripts” and I become blind to the other tab since the window is familiar to me.

      You may also consider adding additional logging to the script to verify that it is actually working and returning data. Outputting all to a separate file should do the trick.

      I tested this by creating a GPO that configured a PowerShell Script (from the PowerShell Scripts tab) to run via the Computer Startup policy. I also added some additional logging so that it would create a file with the expected output.

      Additionally, startup scripts are run under the Local System admin according to here: (https://technet.microsoft.com/en-us/library/cc770556.aspx).

  • What is the $item variable supposed to be set as? My process is always to match the variables before running a script. Thanks!

    • Thanks for the comment!

      The $item variable represents one item as it goes through $ProfileList. The $ProfileList variable should contain a list of SIDs, ntuser.dat Locations (user hives), and Usernames, so the $item variable should have 1 SID, 1 User Hive, and 1 Username.

  • Also, it seems powershell may not like the -in expression. In this case swap -in for -contains and reverse the $item with the $UnloadedHives

    • The -in operator was introduced in PowerShell 3, so it will not work if you’re using PowerShell 1 or 2. In those cases, the workaround that you’ve described should work well.

      On that note, however, now that PowerShell 5.1 (WMF 5.1) has recently been released for all supported operating systems, I do recommended updating to PowerShell 5.1 where possible. There are a ton of new and awesome things that have happened since PowerShell 2!

  • Great script – exactly what I needed. Thanks. Have used it to clear out the HKEY_USERS\$Profile\Printers\Connections subkeys in a Remote Desktop environment.
    Needed to do it for all users before re-deploying printers.

    • Awesome! Glad to hear that it worked out for you. This is definitely one of those useful techniques if nothing else can seem to work.

  • I appreciate the snippet. I turned this into a function for my personal utility module, with an argument for what to do in the template section.

    # start of function
    Function Invoke-ForeachUserRegistry {
    Param($ScriptBlock)
    if ($ScriptBlock -is [string]) {
    $scriptBlock = [System.Management.Automation.ScriptBlock]::Create($ScriptBlock)
    }

    # Replace Template section with
    $ScriptBlock.Invoke($item)

    and called like:
    Invoke-ForeachUserRegistry {
    Param($Hive)
    $hive
    }

    • Thanks for reading the blog! Glad to hear that you found some usage for it! I love hearing what people do with snippets that I post. 🙂

      Cheers!

  • This line of code is wrong as it doesn’t output all three values you are looking for:

    $UnloadedHives = Compare-Object $ProfileList.SID $LoadedHives.SID | Select @{name=”SID”;expression={$_.InputObject}}, UserHive, Username
    :
    Need to replace this code for your line:

    $UnloadedHives = Compare-Object $ProfileList $LoadedHives -Property SID -PassThru | Select SID, UserHive, Username

    • Good eye and thanks for reading!

      Interestingly enough, we don’t end up using the UserHive and Username values of $UnloadedHives, so adding the -Passthru won’t actually matter for this example script on the blog. Originally, we ended up modifying the script to use the values in $ProfileList and apparently didn’t clean up our script well enough. 🙂

      This is great information for anybody who modifies the script and requires those values in $UnloadedHives.

      Thanks again!

Your email address will not be published.

Your Name