|
#Requires -RunAsAdministrator |
|
# Forcefully remove a stuck Bitdefender install when the uninstall tool fails. |
|
# 1. Kill installer.exe running from under the Bitdefender folder. |
|
# 2. Abort if any Bitdefender service is still running. |
|
# 3. Delete the stopped Bitdefender services. |
|
# 4. Rename C:\Program Files\Bitdefender to .old (takes ownership if ACL-locked). |
|
# 5. Delete Bitdefender's registry footprint (uninstall entry + config hive), |
|
# seizing key ownership if protected by deny ACLs. |
|
# Requires admin. Reboot afterward to fully unload any remaining kernel driver. |
|
|
|
$ErrorActionPreference = 'Stop' |
|
|
|
$bdPath = Join-Path $env:ProgramFiles 'Bitdefender' |
|
$services = 'epag','EPIntegrationService','EPMaintenanceService','EPSecurityService','EPUpdateService' |
|
|
|
# 1. Kill any installer.exe running from under the Bitdefender folder. |
|
$installers = Get-CimInstance Win32_Process -Filter "Name = 'installer.exe'" | |
|
Where-Object { $_.ExecutablePath -like "$bdPath\*" } |
|
foreach ($p in $installers) { |
|
Write-Host "Stopping installer.exe (PID $($p.ProcessId)) -> $($p.ExecutablePath)" |
|
Stop-Process -Id $p.ProcessId -Force |
|
} |
|
|
|
# 2. Any of our services still running is a hard stop. |
|
$running = Get-Service -Name $services -ErrorAction SilentlyContinue | |
|
Where-Object { $_.Status -ne 'Stopped' } |
|
if ($running) { |
|
Write-Error "Service(s) not stopped: $($running.Name -join ', '). Aborting." |
|
exit 1 |
|
} |
|
|
|
# 3. Delete the (stopped) services. sc.exe: Remove-Service is PS 6+ only. |
|
foreach ($name in $services) { |
|
if (Get-Service -Name $name -ErrorAction SilentlyContinue) { |
|
Write-Host "Deleting service $name" |
|
sc.exe delete $name | Out-Null |
|
if ($LASTEXITCODE -ne 0) { Write-Error "sc.exe delete $name failed ($LASTEXITCODE)"; exit 1 } |
|
} |
|
} |
|
|
|
# Force-delete a Bitdefender-protected folder: take ownership + grant rights first, |
|
# because its files carry deny ACLs that block Remove-Item even as admin. |
|
function Remove-Protected($path) { |
|
takeown.exe /F $path /R /D Y | Out-Null |
|
icacls.exe $path /grant "*S-1-5-32-544:F" /T /C | Out-Null # BUILTIN\Administrators |
|
Remove-Item $path -Recurse -Force |
|
} |
|
|
|
# 4. Rename the install folder out of the way. |
|
if (Test-Path $bdPath) { |
|
$old = "$bdPath.old" |
|
if (Test-Path $old) { Remove-Protected $old } |
|
Rename-Item $bdPath $old |
|
Write-Host "Renamed $bdPath -> $old" |
|
} |
|
|
|
# --- Registry teardown ----------------------------------------------------- |
|
# Enable the privileges needed to seize ownership of protected registry keys: |
|
# the registry counterpart of the takeown/icacls trick used on the folder. |
|
Add-Type @' |
|
using System; |
|
using System.Runtime.InteropServices; |
|
public static class Priv { |
|
[DllImport("advapi32.dll", SetLastError=true)] |
|
static extern bool OpenProcessToken(IntPtr h, int access, out IntPtr token); |
|
[DllImport("advapi32.dll", SetLastError=true)] |
|
static extern bool LookupPrivilegeValue(string host, string name, ref long luid); |
|
[DllImport("advapi32.dll", SetLastError=true)] |
|
static extern bool AdjustTokenPrivileges(IntPtr token, bool disableAll, ref TokPriv tp, int len, IntPtr prev, IntPtr relen); |
|
[DllImport("kernel32.dll")] static extern IntPtr GetCurrentProcess(); |
|
[StructLayout(LayoutKind.Sequential, Pack=1)] |
|
struct TokPriv { public int Count; public long Luid; public int Attr; } |
|
public static void Enable(string name) { |
|
IntPtr token; |
|
OpenProcessToken(GetCurrentProcess(), 0x28, out token); // ADJUST|QUERY |
|
TokPriv tp; tp.Count = 1; tp.Luid = 0; tp.Attr = 0x2; // SE_PRIVILEGE_ENABLED |
|
LookupPrivilegeValue(null, name, ref tp.Luid); |
|
AdjustTokenPrivileges(token, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero); |
|
} |
|
} |
|
'@ |
|
[Priv]::Enable('SeTakeOwnershipPrivilege') |
|
[Priv]::Enable('SeRestorePrivilege') |
|
|
|
$admins = New-Object System.Security.Principal.SecurityIdentifier 'S-1-5-32-544' # BUILTIN\Administrators |
|
|
|
# Recursively take ownership of an HKLM key tree and grant Administrators full control. |
|
function Reset-RegTreeOwnership([string]$sub) { |
|
$root = [Microsoft.Win32.Registry]::LocalMachine |
|
# Owning requires only the privilege, not prior access. |
|
$k = $root.OpenSubKey($sub, 'ReadWriteSubTree', 'TakeOwnership') |
|
if (-not $k) { return } |
|
$acl = $k.GetAccessControl(); $acl.SetOwner($admins); $k.SetAccessControl($acl); $k.Close() |
|
# As owner, grant ourselves full control. |
|
$k = $root.OpenSubKey($sub, 'ReadWriteSubTree', 'ChangePermissions') |
|
$acl = $k.GetAccessControl() |
|
$acl.ResetAccessRule((New-Object System.Security.AccessControl.RegistryAccessRule( |
|
$admins, 'FullControl', 'ContainerInherit', 'None', 'Allow'))) |
|
$k.SetAccessControl($acl); $k.Close() |
|
# Now readable: recurse into children. |
|
$k = $root.OpenSubKey($sub) |
|
foreach ($child in $k.GetSubKeyNames()) { Reset-RegTreeOwnership "$sub\$child" } |
|
$k.Close() |
|
} |
|
|
|
function Remove-RegKey([string]$sub) { |
|
$psPath = "HKLM:\$sub" |
|
if (-not (Test-Path $psPath)) { return } |
|
try { |
|
Remove-Item $psPath -Recurse -Force |
|
} catch { |
|
Write-Host "ACL-protected, seizing ownership: HKLM\$sub" |
|
Reset-RegTreeOwnership $sub |
|
Remove-Item $psPath -Recurse -Force |
|
} |
|
Write-Host "Deleted HKLM\$sub" |
|
} |
|
|
|
# 5. Remove Bitdefender's registry footprint (orphaned once the folder is gone). |
|
Remove-RegKey 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Endpoint Security' |
|
Remove-RegKey 'SOFTWARE\Bitdefender' |
|
|
|
Write-Host "Done." |