HappySCCM

/

/

Emergency repair disk to disable UWF (Unified Write Filter)


To remove the UWF filter uwfvol, the only way outside of Windows is to edit the registry by loading the system hive and removing it from
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class{71a27cdd-812a-11d0-bec7-08002be2092f} – LowerFilters

Below is a script that will create a winpe iso that modifies the value, note: your value may have different filters to keep. It also injects a CSV for bitlocker codes in the format ComputerName, Password.

If machines are stuck in a loop this will get them out.

# Build-UWF-WinPE.ps1
# Creates C:\UWF_WinPE.iso
# Copies D:\Temp\bitlocker.csv into WinPE
# On boot: auto-unlocks C: from CSV, prints Bitlocker Unlocked or Computer Not In Sheet, then runs UWF LowerFilters fix using ControlSet001

#requires -RunAsAdministrator

$ErrorActionPreference = "Stop"

$Arch  = "amd64"
$Work  = "C:\UWF_WinPE"
$Iso   = "C:\UWF_WinPE.iso"
$Mount = "$Work\mount"

$BitLockerCsvSource = "D:\Temp\bitlocker.csv"
$BitLockerCsvName   = "bitlocker.csv"

$AdkRoot   = "${env:ProgramFiles(x86)}\Windows Kits\10\Assessment and Deployment Kit"
$Deploy    = Join-Path $AdkRoot "Deployment Tools"
$WinPeRoot = Join-Path $AdkRoot "Windows Preinstallation Environment"
$OcRoot    = Join-Path $WinPeRoot "$Arch\WinPE_OCs"
$DandI     = Join-Path $Deploy "DandISetEnv.bat"

if (-not (Test-Path $BitLockerCsvSource)) {
    throw "BitLocker CSV not found: $BitLockerCsvSource"
}

if (-not (Test-Path $AdkRoot)) {
    throw "ADK root not found: $AdkRoot"
}

if (-not (Test-Path $DandI)) {
    throw "DandISetEnv.bat not found. Install ADK with Deployment Tools."
}

if (-not (Test-Path $WinPeRoot)) {
    throw "WinPE folder not found. Install the Windows PE Add-on for the ADK."
}

if (-not (Test-Path $OcRoot)) {
    throw "WinPE optional components not found: $OcRoot"
}

$Copype = Get-ChildItem $AdkRoot -Recurse -Filter "copype.cmd" -ErrorAction SilentlyContinue | Select-Object -First 1
$MakePE = Get-ChildItem $AdkRoot -Recurse -Filter "MakeWinPEMedia.cmd" -ErrorAction SilentlyContinue | Select-Object -First 1

if (-not $Copype) {
    throw "copype.cmd not found. This usually means the WinPE Add-on is missing or broken."
}

if (-not $MakePE) {
    throw "MakeWinPEMedia.cmd not found. This usually means the WinPE Add-on is missing or broken."
}

$Dism = Join-Path $Deploy "$Arch\DISM\dism.exe"
if (-not (Test-Path $Dism)) {
    $Dism = "dism.exe"
}

function Invoke-CmdChecked {
    param(
        [Parameter(Mandatory)]
        [string]$Command,

        [Parameter(Mandatory)]
        [string]$FailMessage
    )

    Write-Host ""
    Write-Host "Running: $Command"

    $OutFile = Join-Path $env:TEMP "uwf_winpe_stdout.txt"
    $ErrFile = Join-Path $env:TEMP "uwf_winpe_stderr.txt"

    Remove-Item $OutFile, $ErrFile -Force -ErrorAction SilentlyContinue

    $p = Start-Process `
        -FilePath "cmd.exe" `
        -ArgumentList "/d", "/c", $Command `
        -Wait `
        -PassThru `
        -NoNewWindow `
        -RedirectStandardOutput $OutFile `
        -RedirectStandardError $ErrFile

    if (Test-Path $OutFile) {
        Get-Content $OutFile | ForEach-Object { Write-Host $_ }
    }

    if (Test-Path $ErrFile) {
        Get-Content $ErrFile | ForEach-Object { Write-Host $_ }
    }

    if ($p.ExitCode -ne 0) {
        throw "$FailMessage Exit code: $($p.ExitCode)"
    }
}

function Invoke-DismChecked {
    param(
        [Parameter(Mandatory)]
        [string[]]$Args,

        [Parameter(Mandatory)]
        [string]$FailMessage
    )

    Write-Host ""
    Write-Host "Running: $Dism $($Args -join ' ')"
    & $Dism @Args

    if ($LASTEXITCODE -ne 0) {
        throw "$FailMessage Exit code: $LASTEXITCODE"
    }
}

function Add-WinPePackageChecked {
    param(
        [Parameter(Mandatory)]
        [string]$PackageRelativePath
    )

    $PkgPath = Join-Path $OcRoot $PackageRelativePath
    if (-not (Test-Path $PkgPath)) {
        throw "Required WinPE package missing: $PkgPath"
    }

    Invoke-DismChecked `
        -Args @("/Image:$Mount", "/Add-Package", "/PackagePath:$PkgPath") `
        -FailMessage "Failed adding package: $PkgPath"
}

if (Test-Path $Mount) {
    Write-Host "Cleaning up any previous mount..."
    & $Dism /Unmount-Image /MountDir:"$Mount" /Discard 2>$null | Out-Null
}

if (Test-Path $Work) {
    Write-Host "Removing old working folder: $Work"
    Remove-Item $Work -Recurse -Force
}

if (Test-Path $Iso) {
    Write-Host "Removing old ISO: $Iso"
    Remove-Item $Iso -Force
}

Write-Host ""
Write-Host "Creating WinPE working files..."
Invoke-CmdChecked `
    -Command "call `"$DandI`" && call `"$($Copype.FullName)`" $Arch `"$Work`"" `
    -FailMessage "copype failed."

if (-not (Test-Path "$Work\media\sources\boot.wim")) {
    throw "boot.wim was not created at $Work\media\sources\boot.wim"
}

Write-Host ""
Write-Host "Mounting boot.wim..."
Invoke-DismChecked `
    -Args @("/Mount-Image", "/ImageFile:$Work\media\sources\boot.wim", "/Index:1", "/MountDir:$Mount") `
    -FailMessage "DISM mount failed."

try {
    Write-Host ""
    Write-Host "Adding WinPE packages..."

    $Packages = @(
        "WinPE-WMI.cab",
        "en-us\WinPE-WMI_en-us.cab",
        "WinPE-NetFX.cab",
        "en-us\WinPE-NetFX_en-us.cab",
        "WinPE-Scripting.cab",
        "en-us\WinPE-Scripting_en-us.cab",
        "WinPE-PowerShell.cab",
        "en-us\WinPE-PowerShell_en-us.cab",
        "WinPE-StorageWMI.cab",
        "en-us\WinPE-StorageWMI_en-us.cab",
        "WinPE-SecureStartup.cab",
        "en-us\WinPE-SecureStartup_en-us.cab"
    )

    foreach ($Pkg in $Packages) {
        Add-WinPePackageChecked -PackageRelativePath $Pkg
    }

    Write-Host ""
    Write-Host "Copying BitLocker CSV into WinPE..."
    Copy-Item -Path $BitLockerCsvSource -Destination "$Mount\Windows\System32\$BitLockerCsvName" -Force
    Copy-Item -Path $BitLockerCsvSource -Destination "$Mount\$BitLockerCsvName" -Force

    Write-Host ""
    Write-Host "Writing UWF automatic startup script..."

    $StartNet = @'
wpeinit
powershell.exe -NoProfile -ExecutionPolicy Bypass -File %SystemRoot%\System32\UWFAuto.ps1
cmd.exe
'@

    Set-Content -Path "$Mount\Windows\System32\startnet.cmd" -Value $StartNet -Encoding ASCII -Force

    $UwfAuto = @'
$ErrorActionPreference = "Continue"

$Drive = "C:"
$Hive = "C:\Windows\System32\Config\SYSTEM"
$MountName = "HKLM\OFFSYS"
$ClassKey = "HKLM\OFFSYS\ControlSet001\Control\Class\{71a27cdd-812a-11d0-bec7-08002be2092f}"
$LogPath = "X:\UWF-Auto.log"

function Log {
    param([string]$Message)
    $line = "{0} {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Message
    Write-Host $Message
    try { Add-Content -Path $LogPath -Value $line -Encoding ASCII -ErrorAction SilentlyContinue } catch {}
}

function Get-CsvPath {
    $paths = @(
        "X:\bitlocker.csv",
        "X:\Windows\System32\bitlocker.csv",
        (Join-Path $env:WINDIR "System32\bitlocker.csv")
    ) | Select-Object -Unique

    foreach ($p in $paths) {
        if (Test-Path $p) { return $p }
    }
    return $null
}

function Test-OfflineWindowsAvailable {
    return (Test-Path $Hive)
}

function Test-BitLockerUnlocked {
    if (Test-OfflineWindowsAvailable) { return $true }

    $status = & manage-bde.exe -status $Drive 2>$null | Out-String
    if ($status -match "Lock Status:\s+Unlocked") { return $true }

    return $false
}

function Normalize-Name {
    param([string]$Name)
    if ($null -eq $Name) { return "" }
    return (($Name -replace '[^a-zA-Z0-9]', '').ToLowerInvariant())
}

function Get-ColumnName {
    param(
        [string[]]$Headers,
        [string[]]$Wanted
    )

    foreach ($h in $Headers) {
        $n = Normalize-Name $h
        foreach ($w in $Wanted) {
            if ($n -eq $w -or $n -like "*$w*") { return $h }
        }
    }
    return $null
}

function Get-RecoveryPasswordsFromText {
    param([string]$Text)
    if ([string]::IsNullOrWhiteSpace($Text)) { return @() }

    $matches = [regex]::Matches($Text, '(?<!\d)(?:\d{6}-){7}\d{6}(?!\d)')
    return @($matches | ForEach-Object { $_.Value } | Select-Object -Unique)
}

function Try-UnlockWithPassword {
    param([string]$RecoveryPassword)

    if ([string]::IsNullOrWhiteSpace($RecoveryPassword)) { return $false }

    $RecoveryPassword = $RecoveryPassword.Trim()
    if ($RecoveryPassword -notmatch '^(?:\d{6}-){7}\d{6}$') { return $false }

    & manage-bde.exe -unlock $Drive -RecoveryPassword $RecoveryPassword | Out-Null
    Start-Sleep -Milliseconds 300
    return (Test-BitLockerUnlocked)
}

function Unlock-BitLockerFromCsv {
    if (Test-BitLockerUnlocked) {
        Log "Bitlocker Unlocked"
        return $true
    }

    $csvPath = Get-CsvPath
    if (-not $csvPath) {
        Log "Computer Not In Sheet"
        Log "CSV not found. Expected X:\bitlocker.csv or X:\Windows\System32\bitlocker.csv"
        return $false
    }

    Log "Using CSV: $csvPath"
    Log "WinPE computer name: $env:COMPUTERNAME"

    $raw = ""
    try { $raw = Get-Content -Path $csvPath -Raw -ErrorAction Stop } catch {}

    $rows = @()
    try { $rows = @(Import-Csv -Path $csvPath -ErrorAction Stop) } catch {}

    $passwordsToTry = New-Object System.Collections.Generic.List[string]

    if ($rows.Count -gt 0) {
        $first = $rows | Select-Object -First 1
        $headers = @($first.PSObject.Properties.Name)

        $computerColumn = Get-ColumnName -Headers $headers -Wanted @("computer", "computername", "devicename", "hostname", "name")
        $passwordColumn = Get-ColumnName -Headers $headers -Wanted @("password", "recoverypassword", "bitlockerrecoverypassword", "recoverykey", "key")

        if ($passwordColumn) {
            if ($computerColumn) {
                $matchedRows = @($rows | Where-Object {
                    $value = [string]($_.$computerColumn)
                    $value.Trim() -ieq $env:COMPUTERNAME
                })

                foreach ($row in $matchedRows) {
                    $pw = [string]($row.$passwordColumn)
                    if ($pw) { [void]$passwordsToTry.Add($pw.Trim()) }
                }
            }

            foreach ($row in $rows) {
                $pw = [string]($row.$passwordColumn)
                if ($pw) { [void]$passwordsToTry.Add($pw.Trim()) }
            }
        }
    }

    foreach ($pw in (Get-RecoveryPasswordsFromText -Text $raw)) {
        [void]$passwordsToTry.Add($pw)
    }

    $passwords = @($passwordsToTry | Where-Object { $_ -match '^(?:\d{6}-){7}\d{6}$' } | Select-Object -Unique)

    if ($passwords.Count -eq 0) {
        Log "Computer Not In Sheet"
        Log "No valid 48-digit recovery passwords found in CSV. Check the CSV format."
        return $false
    }

    Log "Trying $($passwords.Count) recovery password candidate(s)."

    foreach ($pw in $passwords) {
        if (Try-UnlockWithPassword -RecoveryPassword $pw) {
            Log "Bitlocker Unlocked"
            return $true
        }
    }

    Log "Computer Not In Sheet"
    Log "No CSV recovery password unlocked $Drive."
    return $false
}

function Run-UwfFix {
    Log ""
    Log "Running UWF LowerFilters fix..."

    if (-not (Test-Path $Hive)) {
        Log "ERROR: $Hive not found. C: is still locked, or Windows is not C:."
        return $false
    }

    try {
        Copy-Item -Path $Hive -Destination "$Hive.bak" -Force
        Log "Backed up SYSTEM hive to $Hive.bak"
    }
    catch {
        Log "ERROR: Backup failed: $($_.Exception.Message)"
        return $false
    }

    & reg.exe unload $MountName 1>$null 2>$null

    & reg.exe load $MountName $Hive 1>$null
    if ($LASTEXITCODE -ne 0) {
        Log "ERROR: Failed to load offline SYSTEM hive."
        return $false
    }

    & reg.exe add $ClassKey /v LowerFilters /t REG_MULTI_SZ /d "fvevol\0iorate\0rdyboost" /f 1>$null
    if ($LASTEXITCODE -ne 0) {
        Log "ERROR: Failed to set LowerFilters."
        & reg.exe unload $MountName | Out-Null
        return $false
    }

    & reg.exe query $ClassKey /v LowerFilters

    & reg.exe unload $MountName 1>$null
    if ($LASTEXITCODE -ne 0) {
        Log "WARNING: Failed to unload hive. Run manually: reg unload $MountName"
        return $false
    }

    Log "UWF Fix Complete"
    return $true
}

Clear-Host
Log "========================================"
Log "          UWF WinPE Auto Repair"
Log "========================================"
Log ""

Unlock-BitLockerFromCsv | Out-Null
Run-UwfFix | Out-Null

Log ""
Log "Finished. Log: $LogPath"
Log "Command prompt will remain open."
'@

    Set-Content -Path "$Mount\Windows\System32\UWFAuto.ps1" -Value $UwfAuto -Encoding ASCII -Force

    Write-Host ""
    Write-Host "Committing boot.wim..."
    Invoke-DismChecked `
        -Args @("/Unmount-Image", "/MountDir:$Mount", "/Commit") `
        -FailMessage "DISM commit failed."
}
catch {
    Write-Warning "Build failed. Discarding mounted image..."
    & $Dism /Unmount-Image /MountDir:"$Mount" /Discard 2>$null | Out-Null
    throw
}

Write-Host ""
Write-Host "Creating ISO..."
$DandI = 'C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\DandISetEnv.bat'
$Make  = 'C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Windows Preinstallation Environment\MakeWinPEMedia.cmd'
$Out   = "$env:TEMP\UWF_WinPE_out.txt"
$Err   = "$env:TEMP\UWF_WinPE_err.txt"

Remove-Item $Iso -Force -ErrorAction SilentlyContinue
Remove-Item $Out,$Err -Force -ErrorAction SilentlyContinue

$p = Start-Process `
  -FilePath "cmd.exe" `
  -ArgumentList "/d", "/c", "call `"$DandI`" && call `"$Make`" /ISO /F `"C:\UWF_WinPE`" `"C:\UWF_WinPE.iso`"" `
  -Wait `
  -PassThru `
  -NoNewWindow `
  -RedirectStandardOutput $Out `
  -RedirectStandardError $Err

Get-Content $Out -ErrorAction SilentlyContinue
Get-Content $Err -ErrorAction SilentlyContinue

if ($p.ExitCode -ne 0) {
    throw "MakeWinPEMedia ISO creation failed. Exit code: $($p.ExitCode)"
}

if (-not (Test-Path $Iso)) {
    throw "ISO was not created: $Iso"
}

Write-Host ""
Write-Host "DONE"
Write-Host "ISO: $Iso"
Write-Host ""
Write-Host "To write to USB later:"
Write-Host "MakeWinPEMedia /UFD /F $Work E:"
Write-Host ""
Write-Host "Replace E: with the USB drive letter. It will wipe the USB."



Leave a Reply

Your email address will not be published. Required fields are marked *