Software Uninstalls – How I Do It

As a follow-up to my previous post on installing software via MSI, I thought it would be good to dive into uninstalling software.

There are a multitude of methods to install and uninstall software, and just as many (if not more) methods of automating this task. Going into “Programs and Features” or “Add/Remove Programs”, selecting the software, then clicking the uninstall button is very easy. But having the user do this can require permissions that they probably should not have, and the wrong item can be selected and removed. Automating the removal of software can have many pitfalls and unforeseen consequences. Sometimes, when you install a piece of software, it doesn’t always update a previous version of itself that may be on the device, then you’re left with two versions that can often conflict with each other. In this case, administrators will uninstall the old version during the installation process of the new version. It is important that the correct software be uninstalled the correct way.

Generally, each of the items listed in “Programs and Features” or “Add/Remove Programs” has a registry key entry in either:

Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\

-or-

Computer\HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\

In that entry, you will find a key named “UninstallString” or something similar. This is the command that is run by Windows when the uninstall button is clicked in “Programs and Features” or “Add/Remove Programs”.  I will be using the “Cisco AnyConnect Secure Mobility Client” program again for this post.  The UninstallString value for this program is:

1
MsiExec.exe /X{511F072A-BBE3-4BE8-92BF-6C497DB76179}

Using this string in a script will uninstall the program just as if you ran it from “Programs and Features” or “Add/Remove Programs”.

Digging in the registry to find information can be a slow, dangerous, pain in the ass. I developed the following script to go through the registry for me and return a list of installed programs and their uninstall strings:

1
2
3
4
5
6
$InstalledSoftware = Get-ChildItem HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall -ErrorAction SilentlyContinue | Get-ItemProperty
$InstalledSoftware += Get-ChildItem HKCU:Software\Microsoft\Windows\CurrentVersion\Uninstall -ErrorAction SilentlyContinue | Get-ItemProperty
If (Test-Path HKLM:SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall) {
$InstalledSoftware += Get-ChildItem HKLM:SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall -ErrorAction SilentlyContinue | Get-ItemProperty
}
$InstalledSoftware | Where {$_.DisplayName -ne $null} | Select-Object DisplayName, UninstallString | Sort-Object DisplayName

This loops through the registry keys I mentioned earlier, plus one more for the current user:

Computer\HKEY_Current_User\Software\Microsoft\Windows\CurrentVersion\Uninstall\

All of the installed program information is stored in $InstalledSoftware which can be queried in whatever way is needed. In this case, it sorts and displays all of the program names and their uninstall commands.

So, just run the script, find out what the uninstall string is, use it in your script, done. Right?

Sort of… but no.

There is a metric crap-ton of information to look at. I’m using “Cisco AnyConnect Secure Mobility Client” as an example because it doesn’t have just one uninstall entry, IT HAS TWO.

I want to uninstall the program using the MsiExec with the GUID rather than the EXE. In my experience, the EXE uninstallers just call the MsiExec, so it’s better to run the MsiExec. We know that MsiExec is a standard Windows utility and it will accept command line switches for logging, to make the execution silent, etc.

So, now that I have the uninstall string using the MsiExec with the GUID, I can dump that into my script and be done. Right?

Sort of… but no.

The problem with hard-coding an uninstall command into a script is that the program will get updated after you install it. This means the GUID can change, meaning the uninstall command can change, meaning your script no longer works. I want to uninstall the specifically installed version of the program, regardless of how many times it has been updated since the initial program install, and I want to use the MsiExec command in my script with specific arguments that aren’t included in the basic “UninstallString” property. How do I solve it? Some additional filters to the script.

Looking at the info for both registry keys, the one with the MsiExec has a property “PSChildName” that is the same as the GUID.

I can filter the script to get just the GUID to a variable which I can use in my script. The program info I don’t want has a “PSChildName” that is the same as the “DisplayName”, so I will make sure that they don’t equal each other with another filter using “-AND”.

1
2
3
4
5
6
7
$ProgramName = "Cisco AnyConnect Secure Mobility Client"
$InstalledSoftware = Get-ChildItem HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall -ErrorAction SilentlyContinue | Get-ItemProperty
$InstalledSoftware += Get-ChildItem HKCU:Software\Microsoft\Windows\CurrentVersion\Uninstall -ErrorAction SilentlyContinue | Get-ItemProperty
If (Test-Path HKLM:SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall) {
$InstalledSoftware += Get-ChildItem HKLM:SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall -ErrorAction SilentlyContinue | Get-ItemProperty
}
$WantedGUID = $InstalledSoftware | Where-Object { $_.DisplayName -match "$ProgramName" -AND $_.PSChildName -ne "$ProgramName" } | Select-Object PSChildName

With the specific GUID in the variable “$WantedGUID” it can be put into my script to silently uninstall the program and log the uninstall actions to a file for reference:

1
2
3
4
5
6
7
8
9
$ProgramName = "Cisco AnyConnect Secure Mobility Client"
$LogFile = "C:\Temp\$ProgramName.log"
$InstalledSoftware = Get-ChildItem HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall -ErrorAction SilentlyContinue | Get-ItemProperty
$InstalledSoftware += Get-ChildItem HKCU:Software\Microsoft\Windows\CurrentVersion\Uninstall -ErrorAction SilentlyContinue | Get-ItemProperty
If (Test-Path HKLM:SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall) {
$InstalledSoftware += Get-ChildItem HKLM:SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall -ErrorAction SilentlyContinue | Get-ItemProperty
}
$WantedGUID = $InstalledSoftware | Where-Object { $_.DisplayName -match "$ProgramName" -AND $_.PSChildName -ne "$ProgramName" } | Select-Object PSChildName
Start-Process 'MsiExec.exe' -ArgumentList ("/X $WantedGUID /qn /l*v $LogFile") -Wait -PassThru -NoNewWindow

Using this script, with a little bit of legwork (lots less than digging through the registry myself), I can dynamically uninstall any program at any time, regardless of when it was deployed.

Some may ask why I don’t just check WMI for the GUID:

1
Get-WmiObject -Class Win32_Product | Where-Object -FilterScript {$_.Name -eq $ProgramName} | Select-Object -Property IdentifyingNumber

I used to. Then I found out that it’s a bad idea for a few reasons. First, it’s slow… very, very slow. The reason it is slow is reason two, “Get-WmiObject” kicks off a consistency check of every WMI object on the computer. Third, a consistency check verifies and repairs the WMI object, therefore every WMI object goes through this even though you don’t want it to and this can sometimes cause problems with programs that were just fine before query was run. YIKES!

So, if you are running “Get-WmiObject” on devices in your fleet, I suggest changing that over to some other method as soon as possible.

I am not a PowerShell expert. Always test any scripts you find online on a test system before using them in production. If you blow your shit up, it’s not my fault.