Two Ways to Wipe a Windows Host with Powershell

Recently I had to come up with a solution to remote wipe a large number of windows workstations. The hosts had an EDR agent running on them which allowed me to remotely execute powershell, and that’s what I had to work with. There were two scenarios I had to deal with: one where the OS needed to be unusable and the data inaccessible by the user, and another scenario where the device needed to be usable but essentially factory reset in a clean state.
Scenario 1 – Using BitLocker
Luckily all the hosts had BitLocker enabled to encrypt their drives. After some googling I decided on a course of action. I would use Powershell to remove the recovery key from the TPM, reboot the hosts, and force them into BitLocker recovery mode. The devices would essentially be bricked without the recovery password which the users didn’t have access to, and the command to do this was simple:
manage-bde -forcerecovery C:

Another added benefit of using BitLocker was that it was reversable if we accidently wiped a device that wasn’t supposed to be wiped. We could just give the user the recovery key over the phone and they would be back in business in no time. But in order to do this we wanted to be absolutely sure we had the correct recovery key. The following code allows you to dump the current BitLocker recovery key from memory:
manage-bde -protectors -get "C:" -type recoverypassword

I now had all pieces I need to create a script. Eureka! I created ransomeware. The next order of business was to do testing. I decided to use EC2 since that was readily available in my work environment. The problem was most AMIs don’t support TPM and therefore don’t support BitLocker. So after a bit more research I came across a few community AMIs that do support TPM. So I did what any good security professional would do and performed copious amounts of testing until I was absolutely sure everything was working perfect.

For good measure I decided to run through some last minute tests on a few physical devices. I ran my script on the first device, the tech sitting in front of the laptop confirmed it rebooted, and then the device went to the bitlocker recovery screen. Success! I ran the script on the second laptop, the tech confirmed it rebooted, but then it went to the login screen…. This can’t be right? I asked the tech to reboot the device again manually and this time it went to the BitLocker recovery screen. We ran through a few more tests and about 50% of the devices needed a second reboot. It didn’t make sense but sometimes reality is a bit messy. So I had to add some messy code to my script to schedule a task to execute a second reboot at now+ 10 minutes.

The Final Script
Here’s the final version of the Powershell script that the EDR agent would execute. It stores the output in a single string using comma separated values so that the python script I was using to kick things off could parse the output.
# Obtain the bitlocker recoverypassword
try {
$pass = manage-bde -protectors -get "C:" -type recoverypassword
$pass = $pass.split([Environment]::NewLine)[9].trim()
} catch {
$output = ",Error obtaining RecoveryKey"
Write-Output $output
exit
}
# forces bitlocker into recovery mode
try {
$result = manage-bde -forcerecovery C:
# get rid of commas and linebreaks
$result = $($result.replace(',', ' ') -join ' ')
$output = "${pass},${result}"
} catch {
$output = "${pass},Error forcing recovery"
Write-Output $output
exit
}
# create a scheduled task to force a second reboot (just in case)
try {
$restartTask = 'ForceSecondRestart'
$action = New-ScheduledTaskAction -Execute "C:\WINDOWS\system32\shutdown.exe" -Argument "/r /t 0 /f"
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM'
$trigger = New-ScheduledTaskTrigger -Once -At ([DateTime]::Now.AddMinutes(10))
$trash = Register-ScheduledTask -Action $action -TaskName $restartTask -Trigger $trigger -Principal $principal
} catch {
Write-Output $output
Start-Sleep 30
C:\WINDOWS\system32\shutdown.exe /r /t 0 /f
exit
}
# sleep before restarting
Write-Output $output
Start-Sleep 30
C:\WINDOWS\system32\shutdown.exe /r /t 0 /f
How to Readd the Recovery Key to the TPM
When you enter the recovery password into the BitLocker recovery screen you’ll be able to get back into the OS, but the key is stored in memory so the minute you reboot, you’ll be prompted for the recovery password all over again. This took a surprising amount of googling to figure out how to fix, but the command to readd the in memory recovery key to the TPM is:
manage-bde -protectors -add C: -tpm

Scenario 2 – Using MDM_RemoteWipe
So for this scenario I needed to come up with a way to remote wipe a device so that it would still be usable at the end, but all the user/company data would be removed. For most devices we could use Microsoft Intune to do the wipe, but there might be a few edge cases where Intune fails for whatever reason and the device needs to be wiped through the EDR agent and powershell, or by a support technician with remote access to the host. So how does Intune perform the remote wipe and is it possible to trigger that mechanism outside of Intune? The answer is ‘yes’ as it turns out and you don’t even need Intune to be installed; the required libraries should be present on just about any Windows 10 desktop device.
The Intune Method
Intune uses a Win32 class named MDM_RemoteWipe to perform the wipe. Microsoft is helpful enough to provide example code for how to call the doWipeMethod within the class:
$namespaceName = "root\cimv2\mdm\dmmap"
$className = "MDM_RemoteWipe"
$methodName = "doWipeMethod"
$session = New-CimSession
$params = New-Object Microsoft.Management.Infrastructure.CimMethodParametersCollection
$param = [Microsoft.Management.Infrastructure.CimMethodParameter]::Create("param", "", "String", "In")
$params.Add($param)
try
{
$instance = Get-CimInstance -Namespace $namespaceName -ClassName $className -Filter "ParentID='./Vendor/MSFT' and InstanceID='RemoteWipe'"
$session.InvokeMethod($namespaceName, $instance, $methodName, $params)
}
catch [Exception]
{
write-host $_ | out-string
}
This code works well as is, but I did make one minor modification in the final script. Instead of ‘doWipeMethod’ which can be interrupted by a user if they power off the device while its “resetting”, so I used ‘doWipeProtectedMethod’ which can’t be interrupted by the user but you also risk leaving the OS unbootable if the process gets interrupted before it’s finished. The only catch is it has to be run from a SYSTEM level context. This isn’t a problem if it’s run through an EDR agent as most EDR agents run at the SYSTEM level anyway. Microsoft recommends running it through psexec, but you might be hesitant to have your support technicians throwing psexec all around the network. So the alternative is to run it through a scheduled task which run at the SYSTEM level by default.

The Final Script
The final script creates a scheduled task and executes the wipe script. The actual wipe script is embedded in the script as a base64 encoded blob. I did this because it’s easier to preserve the formatting and because I needed the the script to be in a predictable location so that the scheduled task could find it (since I wouldn’t be the one running this). But it looks a whole lot like malware. I mean it’s writing an encoded blob to a Powershell file in Temp and then executing it as SYSTEM. But hey it works (at least with Defender).
$base64 = 'JG5hbWVzcGFjZU5hbWUgPSAicm9vdFxjaW12MlxtZG1cZG1tYXAiDQokY2xhc3NOYW1lID0gIk1ETV9SZW1vdGVXaXBlIg0KJG1ldGhvZE5hbWUgPSAiZG9XaXBlUHJvdGVjdGVkTWV0aG9kIg0KDQokc2Vzc2lvbiA9IE5ldy1DaW1TZXNzaW9uDQoNCiRwYXJhbXMgPSBOZXctT2JqZWN0IE1pY3Jvc29mdC5NYW5hZ2VtZW50LkluZnJhc3RydWN0dXJlLkNpbU1ldGhvZFBhcmFtZXRlcnNDb2xsZWN0aW9uDQokcGFyYW0gPSBbTWljcm9zb2Z0Lk1hbmFnZW1lbnQuSW5mcmFzdHJ1Y3R1cmUuQ2ltTWV0aG9kUGFyYW1ldGVyXTo6Q3JlYXRlKCJwYXJhbSIsICIiLCAiU3RyaW5nIiwgIkluIikNCiRwYXJhbXMuQWRkKCRwYXJhbSkNCg0KdHJ5DQp7DQogICAgJGluc3RhbmNlID0gR2V0LUNpbUluc3RhbmNlIC1OYW1lc3BhY2UgJG5hbWVzcGFjZU5hbWUgLUNsYXNzTmFtZSAkY2xhc3NOYW1lIC1GaWx0ZXIgIlBhcmVudElEPScuL1ZlbmRvci9NU0ZUJyBhbmQgSW5zdGFuY2VJRD0nUmVtb3RlV2lwZSciDQogICAgJHNlc3Npb24uSW52b2tlTWV0aG9kKCRuYW1lc3BhY2VOYW1lLCAkaW5zdGFuY2UsICRtZXRob2ROYW1lLCAkcGFyYW1zKQ0KfQ0KY2F0Y2ggW0V4Y2VwdGlvbl0NCnsNCiAgICB3cml0ZS1ob3N0ICRfIHwgb3V0LXN0cmluZw0KfQ=='
$bytes = [System.Convert]::FromBase64String($base64)
$taskScript = [System.Text.Encoding]::UTF8.GetString($bytes)
$taskScript | Out-File -FilePath 'C:\Windows\Temp\wipe.ps1'
$taskName = 'Start Remote Wipe'
$action = New-ScheduledTaskAction -Execute 'Powershell.exe' -Argument '-WindowStyle Hidden -ExecutionPolicy Bypass -File "C:\Windows\Temp\wipe.ps1"'
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount
Register-ScheduledTask -Action $action -TaskName $taskName -Principal $principal
Start-ScheduledTask -TaskName $taskName