Search code examples
powershellexportsccmbcdbcdedit

BCD Edit Export fails when user not logged on and run via SCCM as System account in Powershell


I am having an issue where running a powershell script as the system account via SCCM when trying to deploy a VHD to a system.

The script tries to perform a bcdedit /export - however when I run it as myself or use psexec /i /s cmd and then run the powershell via that whilst I am logged on to the system with my own account it works fine.

When running the script via SCCM it stops the script and throws the custom error seen below in the log output comment, whether I am logged on or not, which leads me to believe that System doesn't have the ability to export the bcd to a file.

The script is provided below:

#Set Package Details
$PackageName = "Certiport-Office2013_01.30_86a"

# Set Logging Location
$Global:LogFile = "C:\Logs\$PackageName.log"
$Global:Returnval = 0

##Set Diskpart Commands
$DiskpartAttachVDisk = "SELECT VDISK FILE=D:\Certiport\$PackageName.vhd
ATTACH VDISK"

$DiskpartChangeLetter ="SELECT VDISK FILE=D:\Certiport\$PackageName.vhd
SELECT PARTITION 1
ASSIGN LETTER=V
EXIT"

$DiskpartDetachVDisk = "SELECT VDISK FILE=D:\Certiport\$PackageName.vhd
DETACH VDISK
EXIT"

# Set PKGLocation
$Global:hostinvocation = (Get-Variable MyInvocation).Value
$Global:PkgLocation = if($hostinvocation.MyCommand.path -ne $null) { Split-Path $hostinvocation.MyCommand.path }
       else { (Get-Location).Path  }


######################
## LOGGING FUNCTION ##

##Writes to the log file in C:\Logs
$OSName = (Get-WmiObject -class Win32_OperatingSystem).Caption.trim()
Function Log {
    Param ([string]$LogEntry)
    $TimeStamp = Get-Date -Format "HH:mm:ss"
    Write-Host $TimeStamp - $LogEntry
    Add-Content "$LogFile" -value "$TimeStamp - $LogEntry"
}

##Creates the C:\Logs\PackageName.log or renames it to .lo_ if it already exists
##The .lo_ format is still readable by some live log file readers such as CMTrace and Trace32 and doesn't require being renamed before reading.
$Date = Get-Date
If (test-path $LogFile) {
    Copy-Item -Path $LogFile ($LogFile.TrimEnd("log") + "lo_") -Force
    New-Item -Path "C:\Logs\$PackageName.log" -Force -ItemType File -Value "---Log File---`r`nInstalling = $PackageName`r`nDate = $Date`r`nOperating System = $OSName`r`nOS Architecture = $env:PROCESSOR_ARCHITECTURE`r`n`r`n`r`n---Start Logging---`r`n`r`n"
} else {
    New-Item -Path "C:\Logs\$PackageName.log" -Force -ItemType File -Value "---Log File---`r`nInstalling = $PackageName`r`nDate = $Date`r`nOperating System = $OSName`r`nOS Architecture = $env:PROCESSOR_ARCHITECTURE`r`n`r`n`r`n---Start Logging---`r`n`r`n"
}

######################

Function Check-CriticalError(){
    If($Global:CriticalError -eq $true){
        Log("Critical Error detected! - Script will now Exit")
        $returnval = 1
        Exit($returnval)
    }
}
$Global:CriticalError = $False

Log("######################")
Log("Starting Certiport 2013 VHD Installation")
Log("Source Directory is: $PKGLocation")

# Check that D Drive Exists
If(-Not (Test-Path D:\)){
    Log("ERROR: D Drive does not exist - Exiting")
    $Global:CriticalError = $true
}
Check-CriticalError

# Check Disk Space requirement (40 GB) for D Drive
If(((Get-WmiObject Win32_Volume -Namespace "root\CIMV2" -Filter {DriveLetter = "D:"}).FreeSpace) -lt "21474836480"){
 Log("ERROR: Insufficient Disk Space - 40GB Required - $("{0:N2}" -f (((Get-WmiObject Win32_Volume -Namespace "root\CIMV2" -Filter {DriveLetter = "D:"}).FreeSpace)/1024/1024)) Mb Free - Exiting")
 $Global:CriticalError = $true
}
Check-CriticalError

# Check the Certiport Install Directory exists and create it if not.
If(-Not (Test-Path D:\CertiPort)){
    New-Item -ItemType Directory D:\Certiport | Out-Null
}

# Extract the VHD to the correct directory and perform an MD5 Check OR Verify and validate the state of the currently existing VHD.
$Global:VHDFile = "D:\Certiport\$PackageName.vhd"
$Global:MasterHash = Get-Content $PkgLocation\MD5.txt
If(-Not (Test-Path D:\Certiport\$PackageName.vhd)){
    Log("VHD Does not exist in D:\Certiport - Extracting from Compressed File")
    $ScriptBlock = [Scriptblock]::Create("$PkgLocation\7za.exe x `"$PkgLocation\$PackageName.7z`" -oD:\ -r -aoa")
    Log("Running - `'$ScriptBlock`'")
    $7ZipExtract = Invoke-Command -ScriptBlock $ScriptBlock
    Log("Verifying MD5 Hash")
    $hash = [Security.Cryptography.HashAlgorithm]::Create( "MD5" )
    $stream = ([IO.StreamReader]"$VHDFile").BaseStream 
    $HashCode =  -join ($hash.ComputeHash($stream) | ForEach { "{0:x2}" -f $_ })
    $stream.Close()
    If($MasterHash -ne $HashCode){
        Log("ERROR: Hash Check Failed - `"$MasterHash`" is not `"$HashCode`"")
        Log("ERROR: Source appears corrupted")
        $Global:CriticalError = $true
    }Else{
        Log("Hash Check Successful")
    }
}Else{
    Log("VHD already exists in D:\Certiport - Verifying MD5 Hash")
    $hash = [Security.Cryptography.HashAlgorithm]::Create( "MD5" )
    $stream = ([IO.StreamReader]"$VHDFile").BaseStream 
    $HashCode =  -join ($hash.ComputeHash($stream) | ForEach { "{0:x2}" -f $_ })
    $stream.Close()
    If($MasterHash -ne $HashCode){
        Log("WARNING: Hash Check Failed - `"$MasterHash`" is not `"$HashCode`"")
        Log("Extracting file from source...")
        $ScriptBlock = [Scriptblock]::Create("$PkgLocation\7za.exe x `"$PkgLocation\$PackageName.7z`" -oD:\ -r -aoa")
        Log("Running - `'$ScriptBlock`'")
        $7ZipExtract = Invoke-Command -ScriptBlock $ScriptBlock
        Log("Verifying MD5 Hash")
        $hash = [Security.Cryptography.HashAlgorithm]::Create( "MD5" )
        $stream = ([IO.StreamReader]"$VHDFile").BaseStream 
        $HashCode =  -join ($hash.ComputeHash($stream) | ForEach { "{0:x2}" -f $_ })
        $stream.Close()
        If($MasterHash -ne $HashCode){
            Log("ERROR: Hash Check Failed - `"$MasterHash`" is not `"$HashCode`"")
            Log("ERROR: Source appears corrupted")
            $Global:CriticalError = $true
        }Else{
            Log("VHD Hash Check Successful")
        }
    }Else{
        Log("VHD Hash Check Successful")
    }
}
Check-CriticalError

# Check BCD For any Previous Entry and remove it
$ScriptBlock = [Scriptblock]::Create("bcdedit /v")
$BCDConfig = Invoke-Command -ScriptBlock $ScriptBlock
$BCDItem = (0..($BCDConfig.Count - 1) | Where { $BCDConfig[$_] -like "description*Certiport 2013 Certification*" }) - 3
If($BCDConfig[$BCDItem] -ne $BCDConfig[-3]){
    $BCDIdentifier = (((($BCDConfig[$BCDItem]).Split("{"))[1]).Split("}"))[0]
    Log("Previous entry found - $BCDIdentifier")
    $ScriptBlock = [Scriptblock]::Create("bcdedit /delete ``{$BCDIdentifier``}")
    $cmdReturn = Invoke-Command -ScriptBlock $ScriptBlock
    If($cmdReturn -eq "The operation completed successfully."){
        Log("Successfully Removed previous Certiport Entry")
    }Else{
        Log("ERROR: Could not remove previous Entry - $cmdReturn")
        $Global:CriticalError = $true
    }
}
Check-CriticalError

# Update Boot Files for UEFI devices and Windows 7

$ScriptBlock = [Scriptblock]::Create("bcdedit /export C:\CertiportVHD-2013-BCD-Backup")
$cmdReturn = Invoke-Command -ScriptBlock $ScriptBlock
If($cmdReturn -eq "The operation completed successfully."){
    Set-Content $Env:Temp\CertiportVHD.txt $DiskpartAttachVDisk
    $ScriptBlock = [Scriptblock]::Create("diskpart /s $($Env:Temp)\CertiportVHD.txt")
    $cmdReturn = Invoke-Command -ScriptBlock $ScriptBlock
    If($cmdReturn -like "*DiskPart successfully attached the virtual disk file.*"){
        Sleep 10
        Set-Content $Env:Temp\CertiportVHD.txt $DiskpartChangeLetter
        $ScriptBlock = [Scriptblock]::Create("diskpart /s $($Env:Temp)\CertiportVHD.txt")
        $cmdReturn = Invoke-Command -ScriptBlock $ScriptBlock
        If($cmdReturn -like "*DiskPart successfully assigned the drive letter or mount point.*"){
            Log("VHD Successfully Mounted")
            $ScriptBlock = [Scriptblock]::Create("bcdboot.exe V:\Windows")
            $cmdReturn = Invoke-Command -ScriptBlock $ScriptBlock
            If($cmdReturn -eq "Boot files successfully created."){
                Log("Boot files successfully created")
                Set-Content $Env:Temp\CertiportVHD.txt $DiskpartDetachVDisk
                $ScriptBlock = [Scriptblock]::Create("diskpart /s $($Env:Temp)\CertiportVHD.txt")
                $cmdReturn = Invoke-Command -ScriptBlock $ScriptBlock
                If($cmdReturn -like "DiskPart successfully detached the virtual disk file."){
                    Log("Successfully Detached the VHD")
                    sleep 10
                    $ScriptBlock = [Scriptblock]::Create("bcdedit /import C:\CertiportVHD-2013-BCD-Backup")
                    $cmdReturn = Invoke-Command -ScriptBlock $ScriptBlock
                    If($cmdReturn -eq "The operation completed successfully."){
                        Log("Successfully Imported the BCD Backup")
                    }Else{
                        Log("ERROR: Could not restore the BCD Backup - $cmdReturn")
                    }
                }Else{
                    Log("ERROR: Could not detach the VHD - $cmdReturn")
                }
            }Else{
                Log("ERROR: Could not create the boot files - $cmdReturn")
            }
        }Else{
            Log("ERROR: Could not assign the VHD to `"V:`" drive - $cmdReturn")
            $Global:CriticalError = $true
            Check-CriticalError
        }
    }Else{
        Log("ERROR: Could not mount the VHD - $cmdReturn")
        $Global:CriticalError = $true
        Check-CriticalError
    }
}Else{
    Log("ERROR: Could not back up BCD - Quitting immediately to avoid destroying boot order - $cmdReturn")
    $Global:CriticalError = $true
    Check-CriticalError
}

# Configure new BCD Entry For Certiport 2013
Log("Creating BCD Entry")
$ScriptBlock = [Scriptblock]::Create("bcdedit.exe /copy ``{default``} /d `"Certiport 2013 Certification v1.3`"")
$cmdReturn = Invoke-Command -ScriptBlock $ScriptBlock
    If($cmdReturn -like "The entry was successfully copied to*"){
        $CertiportGuid = ((($cmdReturn.Split(" "))[6]) -replace ".$") -replace '[{}]',''
        Log("Created new BCD entry - $CertiportGuid")
        $ScriptBlock = [Scriptblock]::Create("bcdedit.exe /set ``{$CertiportGuid``} osdevice vhd=[d:]\Certiport\$PackageName.vhd")
        $cmdReturn = Invoke-Command -ScriptBlock $ScriptBlock
        If($cmdReturn -like "The operation completed successfully."){
            Log("Successfully changed BCD osdevice")
            $ScriptBlock = [Scriptblock]::Create("bcdedit.exe /set ``{$CertiportGuid``} device vhd=[d:]\Certiport\$PackageName.vhd")
            $cmdReturn = Invoke-Command -ScriptBlock $ScriptBlock
            If($cmdReturn -like "The operation completed successfully."){
                Log("Successfully changed BCD device")
                $ScriptBlock = [Scriptblock]::Create("bcdedit.exe /timeout 5")
                $cmdReturn = Invoke-Command -ScriptBlock $ScriptBlock
                If($cmdReturn -like "The operation completed successfully."){
                    Log("Successfully changed boot timeout")
                }Else{
                    Log("ERROR: Could not change boot timeout - $cmdReturn")
                }
            }Else{
               Log("ERROR: Could not change BCD device - $cmdReturn")
               $Global:CriticalError = $true 
            }
        }Else{
        Log("ERROR: Could not change BCD osdevice - $cmdReturn")
        $Global:CriticalError = $true
        }
    }Else{
    Log("ERROR: Could not copy BCD Entry for Certiport - $cmdReturn")
    $Global:CriticalError = $true
    }
Check-CriticalError

Log("Certiport Exam VHD Deployed")
exit $returnval

Log Output:

10:28:29 - ######################

10:28:29 - Starting Certiport 2013 VHD Installation

10:28:29 - Source Directory is: C:\Windows\SysWOW64\CCM\Cache\00000379.2.System

10:28:29 - VHD already exists in D:\Certiport - Verifying MD5 Hash

10:36:41 - VHD Hash Check Successful

10:36:44 - ERROR: Could not back up BCD - Quitting immediately to avoid destroying boot order -

10:36:44 - Critical Error detected! - Script will now Exit

It appears to me that the issue lies within this script block:

$ScriptBlock = [Scriptblock]::Create("bcdedit /export C:\CertiportVHD-2013-BCD-Backup")
$cmdReturn = Invoke-Command -ScriptBlock $ScriptBlock

I'm running the code on a mix of devices, with the same results. Windows 10 x64, Windows 8.1 x64, and Windows 7 x86/x64.

Running powershell v4

I tried changing the part that I thought was the issue above to:

$App = "bcdedit"
$Arguments = "/export C:\CertiportVHD-2013-BCD-Backup"
$cmdReturn = Start-Process -FilePath $App -ArgumentList $Arguments -Wait -PassThru

However that didn't work either, and gave the same result.

I really appreciate any help provided, thank you in advance.


Solution

  • It turns out the fix for this issue was to take a copy of bcdedit and put it inside the package rather than simply specifying "bcdedit".

    I updated the references to the file 'bcdedit.exe' to ".\bcdedit.exe" through out the script as SCCM changes directory to the root of the package folder that it copies to the local cache.

    I'm not sure why you can specify bcdedit when running as a user logged onto the device, but SCCM when running as system, can't use the same syntax, but this fix worked.

    Thanks to @LievenKeersmaekers for his help in helping me to figure out that the file couldn't be found by the system, when he said that $cmdReturn was empty, which got me thinking about why this was, when he tested it and it returned a value, whether it errored or not.

    Edit: I also noticed the same thing happened for bcdboot.exe and had to copy the file to the package source files and reference .\bcdboot.exe