xkln.net Open in urlscan Pro
104.198.14.52  Public Scan

URL: https://xkln.net/blog/please-stop-using-win32product-to-find-installed-software-alternatives-inside/
Submission: On August 22 via manual from US — Scanned from DE

Form analysis 0 forms found in the DOM

Text Content

This app works best with JavaScript enabled.


XKLN.NET

 * Blog
 * Projects
 * Contact
 * Tools
 * IP
 * Type7

 * Links
 * Twitter
 * GitHub


PLEASE STOP USING WIN32_PRODUCT TO FIND INSTALLED SOFTWARE. ALTERNATIVES INSIDE!

Posted on April 04, 2020

   and tagged as
 * powershell



For years I’ve seen blog posts, scripts, forum messages, you name it - all
referencing the Win32_Product WMI class when someone is looking for a way to
list installed applications on a Windows system. This method seems to be
exceptionally prevalent and can be dangerous. Let’s find out why.


REASONS NOT TO QUERY WIN32_PRODUCT


IT’S SLOW

The least important reason is that it’s not very fast. On my admittedly ancient
i7 with an SSD for the OS volume it takes over a minute.

PS C:\> (Measure-Command {Get-CimInstance Win32_Product}).TotalSeconds
68.4418397


IT POTENTIALLY RETURNS INCOMPLETE DATA

Win32_Product will only return applications installed via Windows Installer.
There are many products used to assemble installers that don’t build Windows
Installer packages. Any applications that use these non-Windows Installer
packages for deployment won’t be returned when Win32_Product is queried.


IT RUNS A CONSISTENCY CHECK ON ALL APPLICATIONS AND PERFORMS AUTOMATIC AND
SILENT REPAIRS

Yes, you read that right.

This is the big one, and is the reason for the poor performance. When you run a
command such as Get-CimInstance Win32_Product it causes every single application
installed via Windows Installer to perform a consistency check, and if any
problems are found, it runs an automated and silent repair.

Have a look at the Application Event Log after running the above command
(preferably on a test system).



Here is the message inside the entries, obviously with a different product named
in each event.



There is some good documentation from Microsoft on this in KB974524.

> Win32_product Class is not query optimized. Queries such as “select * from
> Win32_Product where (name like ‘Sniffer%’)” require WMI to use the MSI
> provider to enumerate all of the installed products and then parse the full
> list sequentially to handle the “where” clause. This process also initiates a
> consistency check of packages installed, verifying and repairing the install.

I’ll also note that the WMI class Win32reg_AddRemovePrograms referenced in the
above KB only exists on systems where the SCCM agent is installed, it is not
included in the standard Windows WMI namespace.


ANECDOTAL EVIDENCE

Like many others, I too first reached for Win32_Product before I knew better.
One one occasion it caused a BSOD on an Exchange server. This was a long time
ago, and I wouldn’t expect the same thing to happen today (and to be fair the
server was superbly under-specced and in a poor state to begin with), but it did
leave me scarred.

Now that we know what not to do, how do we pull installed applications?


QUERYING THE REGISTRY

The simplest and fastest alternative has been to query the registry. There are
paths (one for 32bit, and one for 64bit applications) that are used to populate
the Add/Remove Programs table, and we can query those instead.

$Apps = @()
$Apps += Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" # 32 Bit
$Apps += Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"             # 64 Bit

To compare run times, the above takes 2.159 seconds on my PC.

And we get much the same information as the WMI command

PS C:\> $Apps[84]


DisplayName     : WinPcap 4.1.3
UninstallString : C:\Program Files (x86)\WinPcap\uninstall.exe
Publisher       : Riverbed Technology, Inc.
URLInfoAbout    : http://www.riverbed.com/
URLUpdateInfo   : http://www.winpcap.org
VersionMajor    : 4
VersionMinor    : 1
DisplayVersion  : 4.1.0.2980
DisplayIcon     : C:\Program Files (x86)\WinPcap\uninstall.exe
PSPath          : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\WinPcapInst
PSParentPath    : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall
PSChildName     : WinPcapInst
PSDrive         : HKLM
PSProvider      : Microsoft.PowerShell.Core\Registry

One caveat is that this method will return many more elements than
Win32_Product, it will include things such as service packs, Office updates,
language packs, etc. You will most likely need to invest a little time in
filtering out things you aren’t interested in.

On my PC Win32_Product returned 493 items, whereas the registry method returned
862.

However, this does not find applications installed into a users profile, which
is where things can get a little more complicated.


FINDING APPS THAT INSTALL INTO APPDATA

There has been a growing trend of application vendors making installers that
deploy to a user’s profile (%userprofile%\AppData). This is commonly done (much
to the dismay of the IT departments) to allow users to install programs without
needing administrative privileges.

These applications will also have their installation documented in the registry,
but under HKEY_CURRENT_USER instead of HKEY_LOCAL_MACHINE.

This poses a few challenges. Each user’s registry hive is located in their
profile as %userprofile%\NTUSER.DAT.

 * If a user is logged in, this can be accessed by other (administrative) users
   on the system via the HKEY_USERS\$ACCOUNT_SID key
 * If a user is not logged in, the hive can be manually mounted using REG LOAD.

One catch is that if a user’s registry hive is already loaded (i.e., they are
logged in) it cannot be loaded again as we will get a The process cannot access
the file because it is being used by another process. error.

So we’ll need to enumerate a list of profiles in the system, determine whether
we need to load their registry hive, mount it if we need to, pull the
application install data, and finally unload the hive. The last part is
important, failing to do so will leave the user unable to log in due to the same
error we encountered above.

The first part of finding a list of profiles and determining whether they’re
currently loaded is made easy by quering Win32_UserProfile

localpath                                 sid                                                              loaded special
---------                                 ---                                                              ------ -------
C:\Users\.NET v4.5 Classic                S-1-5-82-3876422241-1344743610-1729199087-774402673-2621913236   False  False
C:\Users\.NET v4.5                        S-1-5-82-271721585-897601226-2024613209-625570482-296978595      False  False
C:\Users\DefaultAppPool                   S-1-5-82-3006700770-424185619-1745488364-794895919-4004696415    False  False
C:\Users\MSSQL$MICROSOFT##WID             S-1-5-80-1184457765-4068085190-3456807688-2200952327-3769537534  True   False
C:\Users\test2                            S-1-5-21-1543284909-1794992621-2893585182-1019                   False  False
C:\Users\test1                            S-1-5-21-1543284909-1794992621-2893585182-1017                   False  False
C:\Users\md                               S-1-5-21-1543284909-1794992621-2893585182-1000                   True   False
C:\WINDOWS\ServiceProfiles\NetworkService S-1-5-20                                                         True   True
C:\WINDOWS\ServiceProfiles\LocalService   S-1-5-19                                                         True   True
C:\WINDOWS\system32\config\systemprofile  S-1-5-18                                                         True   True

There are some key pieces of information we need to extract from this output

 * The loaded parameter tells us if the profile (and therefore hive) is loaded
 * The special parameter tells us whether this is a system account - we can
   ignore those
 * The sid parameter gives us some clues on what type of account we’re dealing
   with.

Normal user accounts are prefixed with S-1-5-21, which matches the Microsoft
documentation on Well known security identifiers.

Using this info we can put together a smarter function that pulls system wide
installed applications, as well as those deployed across all user profiles.
Using parameter sets we can allow the user to pull various combinations of data,
though some will require administrative privileges:

Parameter Requires Admin Data Returned GlobalNoGlobally installed applications
GlobalAndAllUsersYesGlobally installed applications and all user installed
applications. Default. GlobalAndCurrentUserNoGlobally installed applications and
applications installed under the profile of the user executing the function
CurrentUserNoApplications installed under the profile of the user executing the
function AllUsersYesAll user installed applications

And here is the function:

function Get-InstalledApplications() {
    [cmdletbinding(DefaultParameterSetName = 'GlobalAndAllUsers')]

    Param (
        [Parameter(ParameterSetName="Global")]
        [switch]$Global,
        [Parameter(ParameterSetName="GlobalAndCurrentUser")]
        [switch]$GlobalAndCurrentUser,
        [Parameter(ParameterSetName="GlobalAndAllUsers")]
        [switch]$GlobalAndAllUsers,
        [Parameter(ParameterSetName="CurrentUser")]
        [switch]$CurrentUser,
        [Parameter(ParameterSetName="AllUsers")]
        [switch]$AllUsers
    )

    # Excplicitly set default param to True if used to allow conditionals to work
    if ($PSCmdlet.ParameterSetName -eq "GlobalAndAllUsers") {
        $GlobalAndAllUsers = $true
    }

    # Check if running with Administrative privileges if required
    if ($GlobalAndAllUsers -or $AllUsers) {
        $RunningAsAdmin = (New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
        if ($RunningAsAdmin -eq $false) {
            Write-Error "Finding all user applications requires administrative privileges"
            break
        }
    }

    # Empty array to store applications
    $Apps = @()
    $32BitPath = "SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
    $64BitPath = "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"

    # Retreive globally insatlled applications
    if ($Global -or $GlobalAndAllUsers -or $GlobalAndCurrentUser) {
        Write-Host "Processing global hive"
        $Apps += Get-ItemProperty "HKLM:\$32BitPath"
        $Apps += Get-ItemProperty "HKLM:\$64BitPath"
    }

    if ($CurrentUser -or $GlobalAndCurrentUser) {
        Write-Host "Processing current user hive"
        $Apps += Get-ItemProperty "Registry::\HKEY_CURRENT_USER\$32BitPath"
        $Apps += Get-ItemProperty "Registry::\HKEY_CURRENT_USER\$64BitPath"
    }

    if ($AllUsers -or $GlobalAndAllUsers) {
        Write-Host "Collecting hive data for all users"
        $AllProfiles = Get-CimInstance Win32_UserProfile | Select LocalPath, SID, Loaded, Special | Where {$_.SID -like "S-1-5-21-*"}
        $MountedProfiles = $AllProfiles | Where {$_.Loaded -eq $true}
        $UnmountedProfiles = $AllProfiles | Where {$_.Loaded -eq $false}

        Write-Host "Processing mounted hives"
        $MountedProfiles | % {
            $Apps += Get-ItemProperty -Path "Registry::\HKEY_USERS\$($_.SID)\$32BitPath"
            $Apps += Get-ItemProperty -Path "Registry::\HKEY_USERS\$($_.SID)\$64BitPath"
        }

        Write-Host "Processing unmounted hives"
        $UnmountedProfiles | % {

            $Hive = "$($_.LocalPath)\NTUSER.DAT"
            Write-Host " -> Mounting hive at $Hive"

            if (Test-Path $Hive) {
            
                REG LOAD HKU\temp $Hive

                $Apps += Get-ItemProperty -Path "Registry::\HKEY_USERS\temp\$32BitPath"
                $Apps += Get-ItemProperty -Path "Registry::\HKEY_USERS\temp\$64BitPath"

                # Run manual GC to allow hive to be unmounted
                [GC]::Collect()
                [GC]::WaitForPendingFinalizers()
            
                REG UNLOAD HKU\temp

            } else {
                Write-Warning "Unable to access registry hive at $Hive"
            }
        }
    }

    Write-Output $Apps
}


EXAMPLE USAGE

# Find installed applications installed globally and inside all user profiles (default behavior) and export to a CSV
Get-InstalledApplications | Select DisplayName, InstallLocation | Export-Csv AllApps.csv -NoTypeInformation

# Find installed applications within user profiles
Get-InstalledApplications -AllUsers | Select DisplayName, InstallLocation 

# Find installed applications within the current user profile
Get-InstalledApplications -CurrentUser | Select DisplayName, InstallLocation 


GET-PACKAGE

The last and probably most convenient option is Get-Package, but as is the way,
there are a few caveats.

 1. You must be running PowerShell 5.1 or newer
 2. It won’t pull applications installed into user profiles that are not the
    user running the command

That is to say, Get-Package will detect:

 1. Globally installed applications
 2. Applications installed into the user profile of the user running the command

Get-Package returned 1345 items, but the vast majority of the extra rows were
various updates (Windows Defender Security and Intelligence updates, Windows
Malicious Software Removal Tool updates, monthly Cumulative Updates - you get
the picture ).

Interestingly, there were a few NVIDIA applications under the HKLM path that my
function above pulled but were not present in the Get-Package output (NVIDIA
Display Session Container, NVIDIA Display Session Container, NVIDIA Control
Panel, and a bunch more).

Get-Package also returned PowerShell modules installed via the PowerShell
Gallery.



Lastly, it also returned a few applications that had previously been
uninstalled, though evidently they still left some traces behind.

Hopefully this provides everyone with faster and safer ways to query for
installed applications. Remember, friends don’t let friends query Win32_Product.

--------------------------------------------------------------------------------

If you enjoyed this post consider sharing it on Twitter, Reddit, Facebook, or
LinkedIn, and following me on Twitter.


 * ← Putting The PowerShell Window Title To Better Use
 * Expanding Shortened URLs With PowerShell →

© 2008-2024, xkln.net