How do I stop Windows 10 Install from modifying BIOS boot settings?
Learned the following:
- On Linux, this would be fairly straightforward, via efibootmgr
- EasyUEFI would let me do what I want too - command line support requires a fairly cheap license; but I don't feel great depending on a niche tool like it, especially if there are other options.
- bcdedit on a UEFI machine modifies UEFI settings. I think it would work.
- The UEFI spec for boot order isn't too complicated. The API is really just GetVariable/SetVariable with variables named BootOrder (to get/set the list of boot options in the order they'll be tried) and Boot#### (to get/set info about each boot option).
- I have no idea how I'd write a windows app against the UEFI API on windows (anyone?)
- Windows provides an API that, among other things, wraps UEFI's GetVariable/SetVariable.
Once I understood the UEFI spec for boot order and the windows API, the code (C++, built for 64-bit as that is all we are using) wasn't too bad. This needs to be built into an exe that requires administrative privileges and statically links the windows runtime, and then I run it in MDT after the OS is installed before restart.
First, you have to claim a privilege to call the API. Use a little helper:
struct CloseHandleHelper
{
void operator()(void *p) const
{
CloseHandle(p);
}
};
BOOL SetPrivilege(HANDLE process, LPCWSTR name, BOOL on)
{
HANDLE token;
if (!OpenProcessToken(process, TOKEN_ADJUST_PRIVILEGES, &token))
return FALSE;
std::unique_ptr<void, CloseHandleHelper> tokenLifetime(token);
TOKEN_PRIVILEGES tp;
tp.PrivilegeCount = 1;
if (!LookupPrivilegeValueW(NULL, name, &tp.Privileges[0].Luid))
return FALSE;
tp.Privileges[0].Attributes = on ? SE_PRIVILEGE_ENABLED : 0;
return AdjustTokenPrivileges(token, FALSE, &tp, sizeof(tp), NULL, NULL);
}
then call
SetPrivilege(GetCurrentProcess(), SE_SYSTEM_ENVIRONMENT_NAME, TRUE));
Next, get the list of boot options (a concatenation of uint16_t values):
const int BUFFER_SIZE = 4096;
BYTE bootOrderBuffer[BUFFER_SIZE];
DWORD bootOrderLength = 0;
const TCHAR bootOrderName[] = TEXT("BootOrder");
const TCHAR globalGuid[] = TEXT("{8BE4DF61-93CA-11D2-AA0D-00E098032B8C}");
DWORD bootOrderAttributes;
bootOrderLength = GetFirmwareEnvironmentVariableEx(bootOrderName, globalGuid, bootOrderBuffer, BUFFER_SIZE, &bootOrderAttributes);
if (bootOrderLength == 0)
{
std::cout << "Failed getting BootOrder with error " << GetLastError() << std::endl;
return 1;
}
You then can iterate over each boot option, form the Boot#### variable name for it, and then use that to get a struct with info about the option. You'll want to see if the first active option has "Description" equal to "Windows Boot Manager". Description is a null-terminated wide character string at offset 6 in the struct.
for (DWORD i = 0; i < bootOrderLength; i += 2)
{
std::wstringstream bootOptionNameBuilder;
bootOptionNameBuilder << "Boot" << std::uppercase << std::setfill(L'0') << std::setw(4) << std::hex << *reinterpret_cast<uint16_t*>(bootOrderBuffer + i);
std::wstring bootOptionName(bootOptionNameBuilder.str());
BYTE bootOptionInfoBuffer[BUFFER_SIZE];
DWORD bootOptionInfoLength = GetFirmwareEnvironmentVariableEx(bootOptionName.c_str(), globalGuid, bootOptionInfoBuffer, BUFFER_SIZE, nullptr);
if (bootOptionInfoLength == 0)
{
std::cout << "Failed getting option info for option at offset " << i << std::endl;
return 1;
}
uint32_t* bootOptionInfoAttributes = reinterpret_cast<uint32_t*>(bootOptionInfoBuffer);
//First 4 bytes make a uint32_t comprised of flags. 0x1 means the boot option is active (not disabled)
if (((*bootOptionInfoAttributes) & 0x1) != 0)
{
std::wstring description(reinterpret_cast<wchar_t*>(bootOptionInfoBuffer + sizeof(uint32_t) + sizeof(uint16_t)));
bool isWBM = boost::algorithm::to_upper_copy<std::wstring>(description) == L"WINDOWS BOOT MANAGER";
// details - keep track of the value of i for the first WBM and non-WBM options you find, and the fact that you found them
}
}
Now if you found active WBM and non-WBM boot options and the first WBM option is at wbmOffset, and the first non-WBM option is at nonWBMOffset, with wbmOffset < nonWBMOffset, swap the entries in the BootOrder variable with the following:
uint16_t *wbmBootOrderEntry = reinterpret_cast<uint16_t*>(bootOrderBuffer + wbmOffset);
uint16_t *nonWBMBootOrderEntry = reinterpret_cast<uint16_t*>(bootOrderBuffer + nonWBMOffset);
std::swap(*wbmBootOrderEntry, *nonWBMBootOrderEntry);
if (SetFirmwareEnvironmentVariableEx(bootOrderName, globalGuid, bootOrderBuffer, bootOrderLength, bootOrderAttributes))
{
std::cout << "Swapped WBM boot entry at offset " << wbmOffset << " with non-WBM boot entry at offset " << nonWBMOffset << std::endl;
}
else
{
std::cout << "Failed to swap WBM boot entry with non-WBM boot entry, error " << GetLastError() << std::endl;
return 1;
}
I came up with this powershell script that works for me. It's not perfect because it just "stupidly" moves the first non-Windows boot entry to the top. That works for my purposes and there may be a way to make it smarter that I just didn't find.
It looks long, but it's mostly comments and formatted for comprehension. It could be re-written to 5 or 6 lines.
https://github.com/mmseng/bcdedit-revert-uefi-gpt-boot-order
# This script looks for the first non-Windows Boot Manager entry in the UEFI/GPT boot order and moves it to the top
# For preventing newly installed Windows from hijacking the top boot order spot on my UEFI/GPT image testing VMs
# by mmseng
# https://github.com/mmseng/bcdedit-revert-uefi-gpt-boot-order
# Notes:
# - There's very little point in using this on regular production machines being deployed. Its main use is for machines being repeatedly imaged, or might be useful for lab machines.
# - AFAICT bcdedit provideds no way to pull the friendly names of the devices in the overall UEFI boot order list. Therefore, this script only moves the first entry it identifies in the list which is NOT "{bootmgr}" (a.k.a. "Windows Boot Manager"). It's up to the user to make sure the boot order will exist in a state where the desired result is achieved.
# - In my case, my test UEFI VMs initially have the boot order of 1) "EFI Network", 2) whatever else. When Windows is installed with GPT partitioning, it changes the boot order to 1) "Windows Boot Manager", 2) "EFI Network", 3) whatever else. In that state, this script can be used to change the boot order to 1) "EFI Network", 2) "Windows Boot Manager", 3) whatever else.
# - This functionality relies on the completely undocumented feature of bcdedit to modify the "{fwbootmgr}" GPT entry, which contains the overall list of UEFI boot devices.
# - AFAICT bcdedit is really only designed to edit Windows' own "{bootmgr}" entry which represents one of the "boot devices" in the overall UEFI list.
# - Here are some sources:
# - https://www.cnet.com/forums/discussions/bugged-bcdedit-349276/
# - https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/bcd-system-store-settings-for-uefi
# - https://www.boyans.net/DownloadVisualBCD.html
# - https://serverfault.com/questions/813695/how-do-i-stop-windows-10-install-from-modifying-bios-boot-settings
# - https://serverfault.com/questions/714337/changing-uefi-boot-order-from-windows
# Read current boot order
echo "Reading current boot order..."
$bcdOutput = cmd /c bcdedit /enum "{fwbootmgr}"
echo $bcdOutput
# Kill as many of the stupid characters as possible
echo "Removing extraneous characters from boot order output..."
$bcdOutput = $bcdOutput -replace '\s+',''
$bcdOutput = $bcdOutput -replace '`t',''
$bcdOutput = $bcdOutput -replace '`n',''
$bcdOutput = $bcdOutput -replace '`r',''
$bcdOutput = $bcdOutput.trim()
$bcdOutput = $bcdOutput.trimEnd()
$bcdOutput = $bcdOutput.trimStart()
$bcdOutput = $bcdOutput -replace ' ',''
echo $bcdOutput
# Define a reliable regex to capture the UUIDs of non-Windows Boot Manager devices in the boot order list
# This is difficult because apparently Powershell interprets regex is a fairly non-standard way (.NET regex flavor)
# https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-expressions
# Even then, .NET regex testers I used didn't match the behavior of what I got out of various Powershell commands that accept regex strings
# However this seems to work, even though I can't replicate the results in any regex testers
$regex = [regex]'^{([\-a-z0-9]+)+}'
echo "Defined regex as: $regex"
# Save matches
echo "Save strings matching regex..."
$foundMatches = $bcdOutput -match $regex
# Grab first match
# If Windows Boot Manager (a.k.a. "{bootmgr}" was the first in the list, this should be the second
# Which means it was probably the first before Windows hijacked the first spot
# Which means it was probably my "EFI Network" boot device
$secondBootEntry = $foundMatches[0]
echo "First match: $secondBootEntry"
# Move it to the first spot
echo "Running this command:"
echo "cmd /c bcdedit $bcdParams /set `"{fwbootmgr}`" displayorder $secondBootEntry /addfirst"
cmd /c bcdedit $bcdParams /set "{fwbootmgr}" displayorder $secondBootEntry /addfirst