Create an own entry, don't replace MS boot loader

This commit is contained in:
Lauri Kenttä
2023-09-02 10:52:53 +03:00
parent dfb5b916ed
commit 7dbdf33ea8
5 changed files with 311 additions and 31 deletions

View File

@@ -28,6 +28,8 @@ When booting on a UEFI-based computer, Windows may show a vendor-defined logo wh
* Edit the `config.txt` and `splash.bmp` (or any other images) to your needs.
* Run `setup.exe batch COMMANDS` as administrator, with some of the following commands:
* `install` copy the files but don't enable.
* `enable-entry` create a new EFI boot entry.
* `disable-entry` disable the EFI boot entry.
* `enable-overwrite` overwrite the MS boot loader.
* `disable-overwrite` restore the MS boot loader.
* `allow-secure-boot` ignore Secure Boot in subsequent commands.
@@ -45,8 +47,7 @@ If you only need HackBGRT for Windows:
If you need it for other systems as well:
* Configure HackBGRT to start your boot loader (such as systemd-boot): `boot=\EFI\systemd\systemd-bootx64.efi`.
* Run `setup.exe`, install files.
* Set `\EFI\HackBGRT\loader.efi` as your default boot loader with `efibootmgr` or some other EFI boot manager tool.
* Run `setup.exe`, install as a new EFI boot entry.
To install purely on Linux, you can install with `setup.exe dry-run` and then manually copy files from `dry-run/EFI` to your `[EFI System Partition]/EFI`. For further instructions, consult the documentation of your own Linux system.
@@ -64,10 +65,7 @@ Advanced users may edit the `config.txt` to define multiple images, in which cas
## Recovery
If something breaks and you can't boot to Windows, you have the following options:
* Windows installation (or recovery) media can fix boot issues.
* You can copy `[EFI System Partition]\EFI\HackBGRT\bootmgfw-original.efi` into `[EFI System Partition]\EFI\Microsoft\Boot\bootmgfw.efi` by some other means such as Linux or Windows command prompt.
If something breaks and you can't boot to Windows, you need to use the Windows installation disk (or recovery disk) to fix boot issues.
## Building

View File

@@ -1,8 +1,8 @@
# vim: set fileencoding=utf-8
# The same options may be given also as command line parameters in the EFI Shell, which is useful for debugging.
# Boot loader path. Default: backup of the Windows boot loader.
boot=\EFI\HackBGRT\bootmgfw-original.efi
# Boot loader path. MS = either backup or original Windows boot loader.
boot=MS
# The image is specified with an image line.
# Multiple image lines may be present, in which case one will be picked by random.

View File

@@ -1,4 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Linq;
using System.Runtime.InteropServices;
using Microsoft.Win32;
@@ -42,6 +45,92 @@ public class Efi {
public string Name, Guid;
public UInt32 Attributes;
public byte[] Data;
/**
* Convert to string.
*
* @return String representation of this object.
*/
public override string ToString() {
var hex = BitConverter.ToString(Data).Replace("-", " ");
var text = new string(Data.Select(c => 0x20 <= c && c <= 0x7f ? (char) c : ' ').ToArray());
return $"{Name} Guid={Guid} Attributes={Attributes} Text='{text}' Bytes='{hex}'";
}
}
/**
* Information about an EFI boot entry.
*/
public class BootEntryData {
public UInt32 Attributes;
public string Label;
public class DevicePathNode {
public byte Type, SubType;
public byte[] Data;
public DevicePathNode(byte[] data) {
Type = data[0];
SubType = data[1];
Data = data.Skip(4).ToArray();
}
public byte[] ToBytes() {
var len = Data.Length + 4;
return new byte[] { Type, SubType, (byte)(len & 0xff), (byte)(len >> 8) }.Concat(Data).ToArray();
}
}
public List<DevicePathNode> DevicePathNodes;
public byte[] Arguments;
public BootEntryData(byte[] data) {
Attributes = BitConverter.ToUInt32(data, 0);
var pathNodesLength = BitConverter.ToUInt16(data, 4);
Label = new string(BytesToUInt16s(data).Skip(3).TakeWhile(i => i != 0).Select(i => (char)i).ToArray());
var pos = 6 + 2 * (Label.Length + 1);
var pathNodesEnd = pos + pathNodesLength;
DevicePathNodes = new List<DevicePathNode>();
while (pos + 4 <= pathNodesEnd) {
var len = BitConverter.ToUInt16(data, pos + 2);
if (len < 4 || pos + len > pathNodesEnd) {
return; // throw new Exception("Bad entry.");
}
DevicePathNodes.Add(new DevicePathNode(data.Skip(pos).Take(len).ToArray()));
pos += len;
}
Arguments = data.Skip(pathNodesEnd).ToArray();
}
public byte[] ToBytes() {
return new byte[0]
.Concat(BitConverter.GetBytes((UInt32) Attributes))
.Concat(BitConverter.GetBytes((UInt16) DevicePathNodes.Sum(n => n.Data.Length + 4)))
.Concat(Encoding.Unicode.GetBytes(Label + "\0"))
.Concat(DevicePathNodes.SelectMany(n => n.ToBytes()))
.Concat(Arguments)
.ToArray();
}
public DevicePathNode FileNameNode {
get {
var d = DevicePathNodes;
return d.Count > 1 && d[d.Count - 1].Type == 0x7F && d[d.Count - 2].Type == 0x04 ? d[d.Count - 2] : null;
}
}
public bool HasFileName {
get {
return FileNameNode != null;
}
}
public string FileName {
get {
if (!HasFileName) {
return "";
}
return new string(Encoding.Unicode.GetChars(FileNameNode.Data).TakeWhile(c => c != '\0').ToArray());
}
set {
if (!HasFileName) {
throw new Exception("Logic error: Setting FileName on a bad boot entry.");
}
FileNameNode.Data = Encoding.Unicode.GetBytes(value + "\0");
}
}
}
public const string EFI_GLOBAL_GUID = "{8be4df61-93ca-11d2-aa0d-00e098032b8c}";
@@ -117,17 +206,23 @@ public class Efi {
* Set an EFI variable.
*
* @param v Information of the variable.
* @param dryRun Don't actually set the variable.
*/
private static void SetVariable(Variable v) {
private static void SetVariable(Variable v, bool dryRun = false) {
Console.WriteLine($"Writing EFI variable {v}");
if (dryRun) {
return;
}
UInt32 r = SetFirmwareEnvironmentVariableEx(v.Name, v.Guid, v.Data, (UInt32) v.Data.Length, v.Attributes);
if (r == 0) {
switch (Marshal.GetLastWin32Error()) {
case 87:
throw new Exception("GetVariable: Invalid parameter");
throw new Exception("SetVariable: Invalid parameter");
case 1314:
throw new Exception("GetVariable: Privilege not held");
throw new Exception("SetVariable: Privilege not held");
default:
throw new Exception("GetVariable: error " + Marshal.GetLastWin32Error());
throw new Exception("SetVariable: error " + Marshal.GetLastWin32Error());
}
}
}
@@ -177,4 +272,136 @@ public class Efi {
tmp.Data[0] |= 1;
SetVariable(tmp);
}
/**
* Convert bytes into UInt16 values.
*
* @param bytes The byte array.
* @return An enumeration of UInt16 values.
*/
public static IEnumerable<UInt16> BytesToUInt16s(byte[] bytes) {
// TODO: return bytes.Chunk(2).Select(b => (UInt16) (b[0] + 0x100 * b[1])).ToArray();
return Enumerable.Range(0, bytes.Length / 2).Select(i => (UInt16) (bytes[2 * i] + 0x100 * bytes[2 * i + 1]));
}
/**
* Disable the said boot entry from BootOrder.
*
* @param label Label of the boot entry.
* @param fileName File name of the boot entry.
* @param dryRun Don't actually disable the entry.
* @return True, if the entry was found in BootOrder.
*/
public static bool DisableBootEntry(string label, string fileName, bool dryRun = false) {
Variable bootOrder;
try {
bootOrder = GetVariable("BootOrder");
} catch {
return false;
}
if (bootOrder.Data == null) {
return false;
}
var found = false;
var bootOrderInts = new List<UInt16>();
foreach (var num in BytesToUInt16s(bootOrder.Data)) {
var entry = GetVariable(String.Format("Boot{0:X04}", num));
if (entry.Data != null) {
var entryData = new BootEntryData(entry.Data);
if (entryData.Label == label && entryData.FileName == fileName) {
found = true;
continue;
}
}
bootOrderInts.Add(num);
}
if (found) {
bootOrder.Data = bootOrderInts.SelectMany(num => new byte[] { (byte)(num & 0xff), (byte)(num >> 8) }).ToArray();
SetVariable(bootOrder, dryRun);
}
return found;
}
/**
* Create and enable the said boot entry from BootOrder.
*
* @param label Label of the boot entry.
* @param fileName File name of the boot entry.
* @param dryRun Don't actually create the entry.
*/
public static void MakeAndEnableBootEntry(string label, string fileName, bool dryRun = false) {
Variable msEntry = null, ownEntry = null;
UInt16 msNum = 0, ownNum = 0;
// Find a free entry and the MS bootloader entry.
Variable bootOrder = null;
try {
bootOrder = GetVariable("BootOrder");
} catch {
if (dryRun) {
return;
}
}
if (bootOrder == null || bootOrder.Data == null) {
throw new Exception("MakeBootEntry: Could not read BootOrder. Maybe your computer is defective.");
}
var bootCurrent = GetVariable("BootCurrent");
if (bootCurrent.Data == null) {
throw new Exception("MakeBootEntry: Could not read BootCurrent. Maybe your computer is defective.");
}
var bootOrderInts = new List<UInt16>(BytesToUInt16s(bootOrder.Data));
foreach (var num in BytesToUInt16s(bootCurrent.Data).Concat(bootOrderInts).Concat(Enumerable.Range(0, 0xffff).Select(i => (UInt16) i))) {
var entry = GetVariable(String.Format("Boot{0:X04}", num));
if (entry.Data == null) {
if (ownEntry == null) {
ownNum = num;
ownEntry = entry;
}
} else {
var entryData = new BootEntryData(entry.Data);
if (!entryData.HasFileName) {
continue;
}
if (entryData.Label == label && entryData.FileName == fileName) {
ownNum = num;
ownEntry = entry;
}
if (msEntry == null && entryData.FileName.StartsWith("\\EFI\\Microsoft\\Boot\\bootmgfw.efi", StringComparison.OrdinalIgnoreCase)) {
msNum = num;
msEntry = entry;
}
}
if (ownEntry != null && msEntry != null) {
break;
}
}
if (ownEntry == null) {
throw new Exception("MakeBootEntry: Boot entry list is full.");
} else if (msEntry == null) {
throw new Exception("MakeBootEntry: Windows Boot Manager not found.");
} else {
// Make a new boot entry using the MS entry as a starting point.
var entryData = new BootEntryData(msEntry.Data);
entryData.Arguments = Encoding.UTF8.GetBytes(label + "\0");
entryData.Attributes = 1; // LOAD_OPTION_ACTIVE
entryData.Label = label;
entryData.FileName = fileName;
ownEntry.Attributes = 7; // EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS
ownEntry.Data = entryData.ToBytes();
SetVariable(ownEntry, dryRun);
}
var msPos = bootOrderInts.IndexOf(msNum);
var ownPos = bootOrderInts.IndexOf(ownNum);
var mustAdd = ownPos == -1;
var mustMove = 0 <= msPos && msPos <= ownPos;
if (mustAdd || mustMove) {
if (mustMove) {
bootOrderInts.RemoveAt(ownPos);
}
bootOrderInts.Insert(msPos < 0 ? 0 : msPos, ownNum);
bootOrder.Data = bootOrderInts.SelectMany(num => new byte[] { (byte)(num & 0xff), (byte)(num >> 8) }).ToArray();
SetVariable(bootOrder, dryRun);
}
}
}

View File

@@ -41,6 +41,7 @@ public class Setup: SetupHelper {
protected static readonly string[] privilegedActions = new string[] {
"install",
"allow-secure-boot",
"enable-entry", "disable-entry",
"enable-overwrite", "disable-overwrite",
"disable",
"uninstall",
@@ -219,6 +220,22 @@ public class Setup: SetupHelper {
Console.WriteLine($"HackBGRT has been copied to {InstallPath}.");
}
/**
* Enable HackBGRT boot entry.
*/
protected void EnableEntry() {
Efi.MakeAndEnableBootEntry("HackBGRT", "\\EFI\\HackBGRT\\loader.efi", DryRun);
Console.WriteLine("Enabled NVRAM entry for HackBGRT.");
}
/**
* Disable HackBGRT boot entry.
*/
protected void DisableEntry() {
Efi.DisableBootEntry("HackBGRT", "\\EFI\\HackBGRT\\loader.efi", DryRun);
Console.WriteLine("Disabled NVRAM entry for HackBGRT.");
}
/**
* Enable HackBGRT by overwriting the MS boot loader.
*/
@@ -248,6 +265,7 @@ public class Setup: SetupHelper {
protected void RestoreMsLoader() {
var MsLoader = new BootLoaderInfo(Esp.MsLoaderPath);
if (MsLoader.Type == BootLoaderType.Own) {
Console.WriteLine("Disabling an old version of HackBGRT.");
var MsLoaderBackup = new BootLoaderInfo(BackupLoaderPath);
if (!MsLoader.ReplaceWith(MsLoaderBackup)) {
throw new SetupException("Couldn't restore the old MS loader.");
@@ -281,6 +299,7 @@ public class Setup: SetupHelper {
*/
protected void Uninstall() {
RestoreMsLoader();
DisableEntry();
try {
Directory.Delete(InstallPath, true);
Console.WriteLine($"HackBGRT has been removed from {InstallPath}.");
@@ -359,18 +378,30 @@ public class Setup: SetupHelper {
protected void ShowMenu() {
Console.WriteLine();
Console.WriteLine("Choose action (press a key):");
Console.WriteLine(" I = install, enable, upgrade, repair, modify");
Console.WriteLine(" F = install files only, don't enable");
Console.WriteLine(" D = disable, restore the original boot loader");
Console.WriteLine(" R = remove completely; delete all HackBGRT files and images");
Console.WriteLine(" I = install");
Console.WriteLine(" - creates a new EFI boot entry for HackBGRT");
Console.WriteLine(" - sometimes needs to be enabled in firmware settings");
Console.WriteLine(" - should fall back to MS boot loader if HackBGRT fails");
Console.WriteLine(" O = install (legacy)");
Console.WriteLine(" - overwrites the MS boot loader");
Console.WriteLine(" - may require re-install after Windows updates");
Console.WriteLine(" - could brick your system if configured incorrectly");
Console.WriteLine(" F = install files only");
Console.WriteLine(" - needs to be enabled somehow");
Console.WriteLine(" D = disable");
Console.WriteLine(" - removes created entries, restores MS boot loader");
Console.WriteLine(" R = remove completely");
Console.WriteLine(" - disables, then deletes all files and images");
Console.WriteLine(" C = cancel");
var k = Console.ReadKey().Key;
Console.WriteLine();
if (k == ConsoleKey.I || k == ConsoleKey.F) {
if (k == ConsoleKey.I || k == ConsoleKey.O || k == ConsoleKey.F) {
Configure();
}
if (k == ConsoleKey.I) {
RunPrivilegedActions(new string[] { "install", "enable-entry" });
} else if (k == ConsoleKey.O) {
RunPrivilegedActions(new string[] { "install", "enable-overwrite" });
} else if (k == ConsoleKey.F) {
RunPrivilegedActions(new string[] { "install" });
@@ -417,6 +448,11 @@ public class Setup: SetupHelper {
InstallFiles();
} else if (arg == "allow-secure-boot") {
allowSecureBoot = true;
} else if (arg == "enable-entry") {
HandleSecureBoot(allowSecureBoot);
EnableEntry();
} else if (arg == "disable-entry") {
DisableEntry();
} else if (arg == "enable-overwrite") {
HandleSecureBoot(allowSecureBoot);
OverwriteMsLoader();
@@ -424,6 +460,7 @@ public class Setup: SetupHelper {
RestoreMsLoader();
} else if (arg == "disable") {
RestoreMsLoader();
DisableEntry();
} else if (arg == "uninstall") {
Uninstall();
} else {

View File

@@ -323,6 +323,19 @@ void HackBgrt(EFI_FILE_HANDLE root_dir) {
HandleAcpiTables(HackBGRT_REPLACE, bgrt);
}
/**
* Load an application.
*/
static EFI_HANDLE LoadApp(print_t* print_failure, EFI_HANDLE image_handle, EFI_LOADED_IMAGE* image, const CHAR16* path) {
EFI_DEVICE_PATH* boot_dp = FileDevicePath(image->DeviceHandle, (CHAR16*) path);
EFI_HANDLE result = 0;
Debug(L"HackBGRT: Loading application %s.\n", path);
if (EFI_ERROR(BS->LoadImage(0, image_handle, boot_dp, 0, 0, &result))) {
print_failure(L"HackBGRT: Failed to load application %s.\n", path);
}
return result;
}
/**
* The main program.
*/
@@ -356,29 +369,34 @@ EFI_STATUS EFIAPI EfiMain(EFI_HANDLE image_handle, EFI_SYSTEM_TABLE *ST_) {
HackBgrt(root_dir);
EFI_HANDLE next_image_handle = 0;
if (!config.boot_path) {
Print(L"HackBGRT: Boot path not specified.\n");
static CHAR16 backup_boot_path[] = L"\\EFI\\HackBGRT\\bootmgfw-original.efi";
static CHAR16 ms_boot_path[] = L"\\EFI\\Microsoft\\Boot\\bootmgfw.efi";
if (config.boot_path && StriCmp(config.boot_path, L"MS") != 0) {
next_image_handle = LoadApp(Print, image_handle, image, config.boot_path);
} else {
Debug(L"HackBGRT: Loading application %s.\n", config.boot_path);
EFI_DEVICE_PATH* boot_dp = FileDevicePath(image->DeviceHandle, (CHAR16*) config.boot_path);
if (EFI_ERROR(BS->LoadImage(0, image_handle, boot_dp, 0, 0, &next_image_handle))) {
Print(L"HackBGRT: Failed to load application %s.\n", config.boot_path);
config.boot_path = backup_boot_path;
next_image_handle = LoadApp(Debug, image_handle, image, config.boot_path);
if (!next_image_handle) {
config.boot_path = ms_boot_path;
next_image_handle = LoadApp(Debug, image_handle, image, config.boot_path);
}
}
if (!next_image_handle) {
static CHAR16 default_boot_path[] = L"\\EFI\\HackBGRT\\bootmgfw-original.efi";
Debug(L"HackBGRT: Loading application %s.\n", default_boot_path);
EFI_DEVICE_PATH* boot_dp = FileDevicePath(image->DeviceHandle, default_boot_path);
if (EFI_ERROR(BS->LoadImage(0, image_handle, boot_dp, 0, 0, &next_image_handle))) {
Print(L"HackBGRT: Also failed to load application %s.\n", default_boot_path);
config.boot_path = backup_boot_path;
next_image_handle = LoadApp(Print, image_handle, image, config.boot_path);
if (!next_image_handle) {
config.boot_path = ms_boot_path;
next_image_handle = LoadApp(Print, image_handle, image, config.boot_path);
if (!next_image_handle) {
goto fail;
}
Print(L"HackBGRT: Reverting to %s.\n", default_boot_path);
}
Print(L"HackBGRT: Reverting to %s.\n", config.boot_path);
Print(L"Press escape to cancel, any other key to boot.\n");
if (ReadKey().ScanCode == SCAN_ESC) {
goto fail;
}
config.boot_path = default_boot_path;
}
if (config.debug) {
Print(L"HackBGRT: Ready to boot.\nPress escape to cancel, any other key to boot.\n");