diff --git a/README.md b/README.md index ba46819..47ae045 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/config.txt b/config.txt index 3ef3a3d..a2e64fd 100644 --- a/config.txt +++ b/config.txt @@ -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. diff --git a/src/Efi.cs b/src/Efi.cs index 5be3b1e..0987786 100644 --- a/src/Efi.cs +++ b/src/Efi.cs @@ -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 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(); + 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 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(); + 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(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); + } + } } diff --git a/src/Setup.cs b/src/Setup.cs index cf74d50..481846b 100644 --- a/src/Setup.cs +++ b/src/Setup.cs @@ -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 { diff --git a/src/main.c b/src/main.c index 049e1ea..84b1ad8 100644 --- a/src/main.c +++ b/src/main.c @@ -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); - goto fail; + 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");