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. * 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: * Run `setup.exe batch COMMANDS` as administrator, with some of the following commands:
* `install` copy the files but don't enable. * `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. * `enable-overwrite` overwrite the MS boot loader.
* `disable-overwrite` restore the MS boot loader. * `disable-overwrite` restore the MS boot loader.
* `allow-secure-boot` ignore Secure Boot in subsequent commands. * `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: 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`. * Configure HackBGRT to start your boot loader (such as systemd-boot): `boot=\EFI\systemd\systemd-bootx64.efi`.
* Run `setup.exe`, install files. * Run `setup.exe`, install as a new EFI boot entry.
* Set `\EFI\HackBGRT\loader.efi` as your default boot loader with `efibootmgr` or some other EFI boot manager tool.
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. 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 ## Recovery
If something breaks and you can't boot to Windows, you have the following options: 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.
* 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.
## Building ## Building

View File

@@ -1,8 +1,8 @@
# vim: set fileencoding=utf-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. # 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 loader path. MS = either backup or original Windows boot loader.
boot=\EFI\HackBGRT\bootmgfw-original.efi boot=MS
# The image is specified with an image line. # The image is specified with an image line.
# Multiple image lines may be present, in which case one will be picked by random. # 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;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Microsoft.Win32; using Microsoft.Win32;
@@ -42,6 +45,92 @@ public class Efi {
public string Name, Guid; public string Name, Guid;
public UInt32 Attributes; public UInt32 Attributes;
public byte[] Data; 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}"; public const string EFI_GLOBAL_GUID = "{8be4df61-93ca-11d2-aa0d-00e098032b8c}";
@@ -117,17 +206,23 @@ public class Efi {
* Set an EFI variable. * Set an EFI variable.
* *
* @param v Information of the 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); UInt32 r = SetFirmwareEnvironmentVariableEx(v.Name, v.Guid, v.Data, (UInt32) v.Data.Length, v.Attributes);
if (r == 0) { if (r == 0) {
switch (Marshal.GetLastWin32Error()) { switch (Marshal.GetLastWin32Error()) {
case 87: case 87:
throw new Exception("GetVariable: Invalid parameter"); throw new Exception("SetVariable: Invalid parameter");
case 1314: case 1314:
throw new Exception("GetVariable: Privilege not held"); throw new Exception("SetVariable: Privilege not held");
default: 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; tmp.Data[0] |= 1;
SetVariable(tmp); 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[] { protected static readonly string[] privilegedActions = new string[] {
"install", "install",
"allow-secure-boot", "allow-secure-boot",
"enable-entry", "disable-entry",
"enable-overwrite", "disable-overwrite", "enable-overwrite", "disable-overwrite",
"disable", "disable",
"uninstall", "uninstall",
@@ -219,6 +220,22 @@ public class Setup: SetupHelper {
Console.WriteLine($"HackBGRT has been copied to {InstallPath}."); 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. * Enable HackBGRT by overwriting the MS boot loader.
*/ */
@@ -248,6 +265,7 @@ public class Setup: SetupHelper {
protected void RestoreMsLoader() { protected void RestoreMsLoader() {
var MsLoader = new BootLoaderInfo(Esp.MsLoaderPath); var MsLoader = new BootLoaderInfo(Esp.MsLoaderPath);
if (MsLoader.Type == BootLoaderType.Own) { if (MsLoader.Type == BootLoaderType.Own) {
Console.WriteLine("Disabling an old version of HackBGRT.");
var MsLoaderBackup = new BootLoaderInfo(BackupLoaderPath); var MsLoaderBackup = new BootLoaderInfo(BackupLoaderPath);
if (!MsLoader.ReplaceWith(MsLoaderBackup)) { if (!MsLoader.ReplaceWith(MsLoaderBackup)) {
throw new SetupException("Couldn't restore the old MS loader."); throw new SetupException("Couldn't restore the old MS loader.");
@@ -281,6 +299,7 @@ public class Setup: SetupHelper {
*/ */
protected void Uninstall() { protected void Uninstall() {
RestoreMsLoader(); RestoreMsLoader();
DisableEntry();
try { try {
Directory.Delete(InstallPath, true); Directory.Delete(InstallPath, true);
Console.WriteLine($"HackBGRT has been removed from {InstallPath}."); Console.WriteLine($"HackBGRT has been removed from {InstallPath}.");
@@ -359,18 +378,30 @@ public class Setup: SetupHelper {
protected void ShowMenu() { protected void ShowMenu() {
Console.WriteLine(); Console.WriteLine();
Console.WriteLine("Choose action (press a key):"); Console.WriteLine("Choose action (press a key):");
Console.WriteLine(" I = install, enable, upgrade, repair, modify"); Console.WriteLine(" I = install");
Console.WriteLine(" F = install files only, don't enable"); Console.WriteLine(" - creates a new EFI boot entry for HackBGRT");
Console.WriteLine(" D = disable, restore the original boot loader"); Console.WriteLine(" - sometimes needs to be enabled in firmware settings");
Console.WriteLine(" R = remove completely; delete all HackBGRT files and images"); 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"); Console.WriteLine(" C = cancel");
var k = Console.ReadKey().Key; var k = Console.ReadKey().Key;
Console.WriteLine(); Console.WriteLine();
if (k == ConsoleKey.I || k == ConsoleKey.F) { if (k == ConsoleKey.I || k == ConsoleKey.O || k == ConsoleKey.F) {
Configure(); Configure();
} }
if (k == ConsoleKey.I) { if (k == ConsoleKey.I) {
RunPrivilegedActions(new string[] { "install", "enable-entry" });
} else if (k == ConsoleKey.O) {
RunPrivilegedActions(new string[] { "install", "enable-overwrite" }); RunPrivilegedActions(new string[] { "install", "enable-overwrite" });
} else if (k == ConsoleKey.F) { } else if (k == ConsoleKey.F) {
RunPrivilegedActions(new string[] { "install" }); RunPrivilegedActions(new string[] { "install" });
@@ -417,6 +448,11 @@ public class Setup: SetupHelper {
InstallFiles(); InstallFiles();
} else if (arg == "allow-secure-boot") { } else if (arg == "allow-secure-boot") {
allowSecureBoot = true; allowSecureBoot = true;
} else if (arg == "enable-entry") {
HandleSecureBoot(allowSecureBoot);
EnableEntry();
} else if (arg == "disable-entry") {
DisableEntry();
} else if (arg == "enable-overwrite") { } else if (arg == "enable-overwrite") {
HandleSecureBoot(allowSecureBoot); HandleSecureBoot(allowSecureBoot);
OverwriteMsLoader(); OverwriteMsLoader();
@@ -424,6 +460,7 @@ public class Setup: SetupHelper {
RestoreMsLoader(); RestoreMsLoader();
} else if (arg == "disable") { } else if (arg == "disable") {
RestoreMsLoader(); RestoreMsLoader();
DisableEntry();
} else if (arg == "uninstall") { } else if (arg == "uninstall") {
Uninstall(); Uninstall();
} else { } else {

View File

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