For some Friday the 13th is just another day, but for others…
TL;DR: Just want the script?
What Happened?
On Friday, January 13th, 2023, Microsoft updated the Windows Security and Microsoft Defender for Endpoint service that resulted in a series of false positive detections for the Attack Surface Reduction (ASR) rule “Block Win32 API calls from Office macro”. This updated caused many Windows shortcut (.lnk) files to be match a detection pattern which caused them to be deleted. Not tossed in the Recycle Bin but permanently hard deleted. It was a pretty incredible thing to have a mostly blank Start Menu and find other shortcuts missing outside of that location including but not limited to:
- C:\Users\%username%\\AppData\Roaming\Microsoft\Windows\Recent
- C:\Users\%username%\\AppData\Roaming\Microsoft\Office\Recent
- C:\Users\%username%\AppData\Roaming\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar
- C:\Users\%username%\AppData\Roaming\Microsoft\Internet Explorer\Quick Launch\User Pinned\ImplicitAppShortcuts
You can read all about it #ASRmageddon here Service health – Microsoft 365 admin center (https://admin.microsoft.com/Adminportal/Home?#/servicehealth/:/alerts/MO497128) and follow the saga on Twitter
Microsoft’s Fix
Microsoft posted a script to recreate shortcuts on GitHub here:
MDE-PowerBI-Templates/AddShortcutsV1.ps1 at master · microsoft/MDE-PowerBI-Templates · GitHub (https://github.com/microsoft/MDE-PowerBI-Templates/blob/master/ASR_scripts/AddShortcutsV1.ps1)
But it only recreates select shortcuts and it’s not 100% complete. In fact, I think every solution I’ve seen online follows the same basic process:
- GitHub – InsideTechnologiesSrl/DefenderBug: This repository is to save the different solution to restore links after KB2267602 (https://github.com/InsideTechnologiesSrl/DefenderBug)
- Powershellisfun/ASRmageddon Create Common Shortcuts Start Menu at main · HarmVeenstra/Powershellisfun · GitHub (https://github.com/HarmVeenstra/Powershellisfun/tree/main/ASRmageddon%20Create%20Common%20Shortcuts%20Start%20Menu)
- PowerShell is fun 🙂 Recreate Start Menu shortcuts #ASRmageddon (https://powershellisfun.com/2023/01/13/recreate-start-menu-shortcuts-asrmageddon/)
- Recreate deleted Office 365 shortcuts – Microsoft 365 consultancy (cloudshark.nl) (https://www.cloudshark.nl/blog/2023/01/13/recreate-deleted-office-365-shortcuts/)
- There are probably another thousand posts out there covering this 🙂
While I like the above strategies in general – lets be fair, their net results are WAY better than nothing! – none of them were complete, and in some cases shortcuts we re-created in the root of the Programs directory (C:\ProgramData\Microsoft\Windows\Start Menu\Programs) versus where they belong (e.g.: C:\ProgramData\Microsoft\Windows\Start Menu\Programs\7-Zip). Not a big deal but I had a handful of folders under \Start Menu\Programs that were empty even after running these solutions which meant me and my team to fill in the gaps.
The Obvious Fix: Restore from Backup
If you have some sort of backup solution in place, then you could probably whip something up from that with ease. In my new environment, we don’t have that since virtually everything is in the cloud with very little stored locally. That said, we needed a solution in-lieu of something like Druva.
Volume Shadow Copy Service to the Rescue!
In Microsoft’s Tech Community site, they posted a page titled “Recovering from Attack Surface Reduction rule shortcut deletions” which, to be candid, was not very helpful, even with a link to their script on GitHub. However Matt Rouse posted a gem of comment about leveraging the Volume Shadow Copy Service to restore the deleted files! I was immediately reminded of the work I used to do with Citrix-based VDI’s running on VMware where manipulating the VSS was commonplace! I quickly put this to the test, on my machine then ran it past the rest of my team who confirmed it was very promising. Provided there was a VSS copy, it was hands down the absolute best solution we had seen since it allowed us to fully restore the Start Menu!
We packaged up our VSS-based restoration script into an application so that we could easily measure execution, success etc. and deployed it to our 6k+ devices. After letting it bake for a while I checked a handful random machines, including those of users who had previously called to complain of empty Start Menu’s, and I was pleased to see it was working well!
Again, ultra hat tip to Matt Rouse.
Confirm you’ve got a vss copy to potentially extract data from
Please note I am NOT an expert on the subject of the Volume Shadow Copy Service. This is ‘just-in-time’ learning & education to solve a crazy issue that took everyone globally by surprise overnight..
This should work for virtually everyone since VSS should be enabled as Microsoft does not recommend disabling it. So assuming its functional, fire up an elevated command prompt to issue the following command to see available shadow copies:
vssadmin list shadows
If you have multiple drives add /for=%SystemDrive%
or /for=C:
or whatever drive stores your ProgramData directory:
I prefer this method since it makes the Creation Time
(in PowerShell land it’s the InstallDate
) easy to read.
You can do the same from an elevated PowerShell session, by running either of these commands:
Get-CIMInstance -Class Win32_ShadowCopy
Get-WMIObject -Class Win32_ShadowCopy
Look for a DeviceObject
that has an InstallDate
from before the security intelligence update build (1.381.2140.0) went out, which I think is 9am UTC on January 13, 2023.
In my case I’ll be using DeviceObject : \?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1
because the restore InstallDate
is just before the window when the update was released.
Now we need to “mount” the Shadow Copy, and to do that we’ll be making a symbolic link (aka symlink) to it accessible via what looks like a directory in the root of C:\. So from that same elevated command prompt run this command
mklink /d c:\ShadowCopy1 \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1\
Note: Make sure you replace the path to the DeviceObject with yours of course! it could be Copy2, or Copy3 or CopyN
Also, it is possible to create a symlink in PowerShell but I was struggling to sort it out, and with over 6k machines/users to sort out I went down the path of least resistance. If anyone can set me straight on that, I would very much appreciate it!
Now when I browse C:\ShadowCopy1, I’ll actually be looking at the contents of my Shadow Copy in \?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1\.
From here it was as simple as copying from the “mounted” VSS copy to %ProgramData%\Microsoft\Windows\Start Menu\Programs. My gut was to initially do a mirrored robocopy:
robocopy /E /MIR "C:\ShadowCopy1\ProgramData\Microsoft\Windows\Start Menu\Programs" "C:\ProgramData\Microsoft\Windows\Start Menu\Programs"
To fix any issues that may have arisen with manual fixes. But my colleagues convinced me of the possibility that someone may have installed something between the last VSS copy and the date the recovery script executed so I erred on the side of caution and decided not to do a mirror copy.
The Script
Our script ended up being longer than what’s below to account for situations where there wasn’t a usable Shadow Copy available, but this should be a good starting point to send you down the right path.
The normal warnings & Standard Disclaimers apply:
- Use at your own risk
- I’m not liable if this breaks your machine or environment
- Your Mileage May Vary (YMMV)
- Etc.
Everyone signed? Yes? Good. On we go!
try { [System.Management.Automation.ActionPreference]$OriginalEAP = $ErrorActionPreference $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop # The last 'good' date in UTC time $DateTime = [datetime]'01/13/2023 09:00:00' # Get the machines' current time zone for conversion from UTC to local time $ToTimeZone = [System.TimeZoneInfo]::FindSystemTimeZoneById((Get-Timezone).Id) # Convert the DateTime above from UTC to local time $ConvertedTime = ([System.TimeZoneInfo]::ConvertTimeFromUtc($DateTime ,$ToTimeZone)) Write-Host "$(Get-Date -Format s) ConvertedTime [$ConvertedTime]" # Query to see what Shadow Copies are available $Win32_ShadowCopy = Get-WMIObject -Class Win32_ShadowCopy # Loop through each Copy to see if one has an installdate prior to $DateTime above :ShadowCopyCheck foreach($ShadowCopy in $Win32_ShadowCopy) { if($ShadowCopy.InstallDate) { # This worked once or twice for me but then failed #[datetime]$ShadowCopy_Date = $Win32_ShadowCopy.ConvertToDateTime($ShadowCopy.InstallDate) # So I'm now having to do this to ensure it's successful $ShadowCopy_Date = $Win32_ShadowCopy.ConvertToDateTime($ShadowCopy.InstallDate) if($ShadowCopy_Date.Count -gt 1) { [datetime]$ShadowCopy_Date = $ShadowCopy_Date[0] } else { [datetime]$ShadowCopy_Date = $ShadowCopy_Date } if($ShadowCopy_Date -lt $ConvertedTime) { [bool]$ShadowCopy_Usable = $true Write-Host "$(Get-Date -Format s) Valid [$ShadowCopy_Usable] ShadowCopy from before [$ConvertedTime]: [$ShadowCopy_Date]" break :ShadowCopyCheck } else { [bool]$ShadowCopy_Usable = $false Write-Host "$(Get-Date -Format s) Invalid [$ShadowCopy_Usable] ShadowCopy is newer/after [$ConvertedTime]: [$ShadowCopy_Date]" } } else { [bool]$ShadowCopy_Usable = $false Write-Host "$(Get-Date -Format s) No Shadow Copy InstallDate [$($ShadowCopy.InstallDate)]" } } Write-Host "$(Get-Date -Format s) ShadowCopy_Usable [$ShadowCopy_Usable]" if(($ShadowCopy_Usable -eq $true) -and ($ShadowCopy)) { $ShadowCopy_DirName = $ShadowCopy.DeviceObject.Substring($ShadowCopy.DeviceObject.IndexOf('ShadowCopy')) Write-Host "$(Get-Date -Format s) ShadowCopy_DirName [$ShadowCopy_DirName]" $ShadowCopy_LocalPath = Join-Path $env:SystemDrive $ShadowCopy_DirName Write-Host "$(Get-Date -Format s) ShadowCopy_LocalPath [$ShadowCopy_LocalPath]" # Check to see if the symlink has already been created if(!(Test-Path -Path "$ShadowCopy_LocalPath" -PathType Container)) { Write-Host "$(Get-Date -Format s) Creating symlink [$ShadowCopy_LocalPath] $($ShadowCopy.DeviceObject)\" # I couldnt figure out how to create a symlink with PowerShell so relying on good-ol cmd & cmd.exe /c mklink /d "$ShadowCopy_LocalPath" "$($ShadowCopy.DeviceObject)\" # Give it a sec to complete Start-Sleep -Seconds 3 } else { Write-Host "$(Get-Date -Format s) WARN Path already exists [$ShadowCopy_LocalPath] suggesting a symlink has already been created" } # Verify we have something to co back to if(Test-Path -Path "$ShadowCopy_LocalPath\ProgramData\Microsoft\Windows\Start Menu\Programs" -PathType Container) { Write-Host "$(Get-Date -Format s) Begin restore from Shadow Copy via Robocopy" & robocopy "$ShadowCopy_LocalPath\ProgramData\Microsoft\Windows\Start Menu\Programs" "$env:ProgramData\Microsoft\Windows\Start Menu\Programs" /E /R:10 /W:10 /X /V /TS /FP } else { Write-Host "$(Get-Date -Format s) ERROR CAN'T FIND [$ShadowCopy_LocalPath\ProgramData\Microsoft\Windows\Start Menu\Programs]" } if(Test-Path -Path $ShadowCopy_LocalPath -PathType Container) { Write-Host "$(Get-Date -Format s) Removing Shadow Copy symlnk" & cmd.exe /c RD "$ShadowCopy_LocalPath" # Give it a sec to complete Start-Sleep -Seconds 3 } } else { Write-Host "$(Get-Date -Format s) No usable Shadow Copies available." } Write-Host "$(Get-Date -Format s) Main script completed. ^^^ See above for details ^^^" } catch { throw $_ } finally { $ErrorActionPreference = $OriginalEAP Remove-Variable OriginalEAP,DateTime,ToTimeZone,ConvertedTime,Win32_ShadowCopy,shadow* -ErrorAction SilentlyContinue }
I’ll say that was one heck of a Friday [the 13th] and hopefully the last crazy IT-related thing for this year.
Thanks for hanging with me on this. I hope this helps and if you have any questions, let me know in the comments.
Good Providence to you!
Thanks for linking my post and your approach is a nice one too! Good job!
LikeLiked by 1 person
Hey there and thanks for taking the time to comment! Thank YOU for your script too as it served as a good base for the fallback solution. Many thanks 🙂
LikeLiked by 1 person
Thanks for the explanation and the script!
I’d request one fix prior to manually performing the robocopy steps: “my gut was to initially do a mirrored robocopy” you’ve added a space in “Program Data” path – it should be “ProgramData”
robocopy /E /MIR “C:\ShadowCopy1\ProgramData\Microsoft\Windows\Start Menu\Programs” “C:\ProgramXData\Microsoft\Windows\Start Menu\Programs”
LikeLike
Appreciate you highlighting that – I’ve updated the post.
LikeLike