Writing a Boot Sector v.0.2
To write a boot sector that loads a kernel, we can start by creating a simple assembly program that:
- Sets up the basic environment (like the stack).
- Loads the kernel from the disk into memory.
- Jumps to the kernel to start its execution.
Let’s break down this task into manageable steps. I’ll guide you through writing a minimal boot sector in x86 assembly, designed to boot in real mode (16-bit). The kernel will be loaded from the disk’s first sector (after the boot sector) into memory.
Step 1: Boot Sector Layout
A boot sector is exactly 512 bytes. The last two bytes must be 0x55
and 0xAA
to mark it as bootable.
Step 2: Boot Sector Code
Here’s a simple boot sector that loads the kernel from the second sector (sector 1) of the disk into memory at 0x7E00
and then jumps to it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | [BITS 16] ; Use 16-bit instructions (real mode) ORG 0x7C00 ; Set the origin to where BIOS loads the boot sector (0x7C00) start: ; Set up the segment registers xor ax, ax ; Zero out AX mov ds, ax ; Set DS (data segment) to 0 mov es, ax ; Set ES (extra segment) to 0 mov ss, ax ; Set SS (stack segment) to 0 mov sp, 0x7C00 ; Set SP (stack pointer) to 0x7C00, so we use the same area for the stack ; Load the second sector (kernel) into memory at 0x7E00 mov bx, 0x7E00 ; ES:BX is the destination in memory (0x7E00) mov ah, 0x02 ; BIOS read sector function mov al, 0x01 ; Number of sectors to read (1 sector = 512 bytes) mov ch, 0x00 ; Cylinder 0 mov cl, 0x02 ; Sector 2 (sectors start at 1, so this is the second sector) mov dh, 0x00 ; Head 0 mov dl, 0x80 ; Drive 0 (first hard drive) int 0x13 ; BIOS interrupt to read from disk jc error ; If carry flag is set, an error occurred ; Jump to the loaded kernel jmp 0x0000:0x7E00 ; Jump to the start of the loaded kernel error: ; Simple infinite loop in case of an error hlt jmp error times 510-($-$$) db 0 ; Fill the rest of the boot sector with zeros dw 0xAA55 ; Boot signature |
Step 3: Compiling and Writing the Boot Sector
You need to assemble this code into a binary file and then write it to the first 512 bytes of your disk image. Here’s how you can do it using nasm
and dd
:
- Assemble the code:
nasm -f bin bootsector.asm -o bootsector.bin
- Write the bootsector to a disk image:
dd if=bootsector.bin of=disk.img bs=512 count=1 conv=notrunc
- Add your kernel to the disk image starting from the second sector. Assuming your kernel is also in binary form:
dd if=kernel.bin of=disk.img bs=512 seek=1 conv=notrunc
Step 4: Testing
You can test this boot sector using an emulator like QEMU:
1 | qemu-system-x86_64 -drive file=disk.img,format=raw |
Next Steps
- Extend the Bootloader: This basic boot sector only loads one sector of the kernel. If your kernel is larger, you’ll need to add code to load multiple sectors.
- Protected Mode Transition: If your kernel is 32-bit or 64-bit, you’ll need to transition from real mode to protected mode or long mode.
Handling 64bit Kernels
Given that you have a 64-bit kernel, we’ll need to transition from the initial real mode (16-bit) environment provided by the BIOS to long mode (64-bit).
This requires a few additional steps:
- Load the Kernel into Memory.
- Switch to Protected Mode.
- Enable Paging and Switch to Long Mode.
- Jump to the 64-bit Kernel Entry Point.
Let’s walk through the steps to achieve this.
Step 1: Load the Kernel
We’ll begin by loading the kernel into memory as before, but we’ll also reserve additional memory space and set up for transitioning to 64-bit mode.
Step 2: Set Up Protected Mode
Protected Mode allows us to use 32-bit instructions and access more than 1 MB of memory. Here’s how we’ll enter protected mode:
- Load the Global Descriptor Table (GDT).
- Enable the PE (Protection Enable) bit in the Control Register
CR0
.
Step 3: Set Up Paging
To enter long mode, we must enable paging. This involves setting up page tables and then enabling the PG (Paging) bit in the CR0
register and the LME (Long Mode Enable) bit in the EFER
MSR.
Step 4: Jump to 64-bit Kernel
Finally, we’ll jump to the 64-bit kernel entry point, which should now be in long mode.
Here’s an example of a bootloader that handles these steps:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | [BITS 16] ORG 0x7C00 start: ; Set up stack and data segments xor ax, ax mov ds, ax mov es, ax mov ss, ax mov sp, 0x7C00 ; Load the kernel (assume it's larger, so we load multiple sectors) mov bx, 0x1000 ; Load kernel to 0x1000:0x0000 mov ah, 0x02 ; BIOS read sectors function mov al, 0x10 ; Load 16 sectors (8KB) mov ch, 0x00 ; Cylinder 0 mov cl, 0x02 ; Sector 2 mov dh, 0x00 ; Head 0 mov dl, 0x80 ; Drive 0 (first hard disk) int 0x13 ; BIOS interrupt jc error ; Jump to error if carry flag is set ; Load GDT lgdt [gdt_descriptor] ; Enter Protected Mode cli ; Clear interrupts mov eax, cr0 or eax, 0x1 ; Set PE bit in CR0 mov cr0, eax jmp CODE_SEG:protected_mode_entry ; Far jump to flush pipeline [BITS 32] protected_mode_entry: ; Set up segments for protected mode mov ax, DATA_SEG mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax mov esp, 0x7C00 ; Enable A20 line (necessary for accessing memory above 1MB) call enable_a20 ; Set up paging mov eax, page_directory mov cr3, eax ; Load the page directory mov eax, cr4 or eax, 0x10 ; Set PSE bit for 4MB pages mov cr4, eax mov eax, cr0 or eax, 0x80000001 ; Enable paging and protection (set PG and PE bits) mov cr0, eax ; Enter long mode rdmsr or eax, 0x100 ; Set LME bit (Long Mode Enable) wrmsr jmp LONG_MODE_CODE_SEG:long_mode_entry [BITS 64] long_mode_entry: ; Long mode is now active ; Jump to the kernel entry point mov rax, 0x100000 ; Kernel entry point address jmp rax error: ; Error handling code here hlt jmp error ; GDT for 64-bit mode transition gdt_start: ; Null Descriptor dq 0x0000000000000000 ; Code Segment Descriptor dq 0x00AF9A000000FFFF ; Data Segment Descriptor dq 0x00AF92000000FFFF gdt_end: gdt_descriptor: dw gdt_end - gdt_start - 1 ; Size of GDT dd gdt_start ; Address of GDT CODE_SEG equ gdt_start + 0x08 ; Code segment offset in GDT DATA_SEG equ gdt_start + 0x10 ; Data segment offset in GDT LONG_MODE_CODE_SEG equ gdt_start + 0x18 ; Long mode code segment offset in GDT align 4096 page_directory: dd 0x00000083 | (page_table >> 12) times 1023 dd 0x00000000 align 4096 page_table: times 1024 dd 0x00000083 ; Identity-mapped pages enable_a20: ; Code to enable the A20 line ; A20 Gate BIOS routine in al, 0x64 wait1: test al, 0x2 jnz wait1 mov al, 0xD1 out 0x64, al wait2: in al, 0x64 test al, 0x2 jnz wait2 mov al, 0xDF out 0x60, al ret times 510-($-$$) db 0 dw 0xAA55 |
Explanation of the Code:
- Loading the Kernel:
- The kernel is loaded from the disk to memory at
0x1000:0x0000
.
- The kernel is loaded from the disk to memory at
- GDT Setup:
- We define a Global Descriptor Table (GDT) with segments for 16-bit, 32-bit, and 64-bit modes.
- Protected Mode Transition:
- The system is transitioned to Protected Mode by setting the PE bit in
CR0
.
- The system is transitioned to Protected Mode by setting the PE bit in
- Paging Setup:
- Paging is enabled, which is a prerequisite for entering long mode.
- Long Mode Entry:
- The CPU is switched to long mode by setting the LME bit and then jumping to the 64-bit kernel’s entry point.
Step 5: Kernel Preparation
Ensure your 64-bit kernel is set up to receive control from this bootloader. The kernel should be linked to run from the physical address where it is loaded by the bootloader (in this case, 0x100000
).
Selecting Multiple Kernels
To create a bootloader that allows the user to select between different kernel versions, you’ll need to add a simple text-based user interface to your bootloader. This interface can display a list of available kernel versions and allow the user to select one to boot.
This will involve adding a menu system, capturing user input, and loading the selected kernel.
High-Level Steps
- Store Kernel Information:
- Store multiple kernels on the disk at different sectors.
- Maintain a small metadata area in the bootloader to keep track of kernel names and their locations.
- Display a Menu:
- Display a list of available kernel versions on the screen.
- Allow the user to select a kernel using keyboard input.
- Load and Boot the Selected Kernel:
- Load the selected kernel into memory.
- Transition to the appropriate mode (64-bit in your case) and jump to the kernel’s entry point.
Example Bootloader with UI for Kernel Selection
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | [BITS 16] ORG 0x7C00 start: ; Set up stack and data segments xor ax, ax mov ds, ax mov es, ax mov ss, ax mov sp, 0x7C00 ; Display menu call display_menu ; Get user input and choose kernel call get_user_input call load_selected_kernel ; Jump to protected mode (as described earlier) call enter_protected_mode ; Jump to the 64-bit kernel entry point call long_mode_entry ; Halt on error error: hlt jmp error ; Display a simple text-based menu to select the kernel version display_menu: mov si, menu_text call print_string ret ; Print a string pointed to by SI print_string: mov ah, 0x0E ; BIOS teletype function .next_char: lodsb ; Load the next byte from [SI] into AL cmp al, 0 ; Is this the null terminator? je .done ; If yes, we're done int 0x10 ; Otherwise, print the character jmp .next_char ; Repeat for the next character .done: ret ; Get user input for selecting kernel get_user_input: xor ax, ax int 0x16 ; BIOS keyboard input sub al, '1' ; Convert keypress to an index (assuming '1', '2', '3' for menu items) mov bx, ax ; Store the index in BX ret ; Load the selected kernel into memory based on user input load_selected_kernel: ; Assume BX contains the selected kernel index mov cx, 0x1000 ; Load at memory location 0x1000 mov dx, [kernel_sectors + bx*2] mov dh, [kernel_heads + bx] mov al, [kernel_sizes + bx] ; Number of sectors for this kernel ; BIOS interrupt to read from disk mov ah, 0x02 ; BIOS read sectors function int 0x13 ; BIOS interrupt jc error ; Jump to error if carry flag is set ret ; Global Descriptor Table (GDT) setup, Protected Mode, Paging, and Long Mode ; ... ; Example kernel metadata (sectors, heads, sizes) kernel_sectors: dw 2, 18, 34 ; Starting sector numbers for each kernel kernel_heads: db 0, 0, 0 ; Head numbers (0 for all) kernel_sizes: db 16, 16, 16 ; Number of sectors for each kernel ; Menu text menu_text db 'Select a Kernel Version:', 0xA, 0xD menu_text db '1. Kernel v1', 0xA, 0xD menu_text db '2. Kernel v2', 0xA, 0xD menu_text db '3. Kernel v3', 0xA, 0xD menu_text db 0 ; Null terminator ;times 510-($-$$) db 0 dw 0xAA55 |
Explanation of the Code
- Menu Display:
- The
display_menu
function prints a simple text menu listing the available kernels. This is done using BIOS interrupts to print each character on the screen.
- The
- User Input:
- The
get_user_input
function waits for a keypress using BIOS interrupt0x16
. The keypress is converted to an index to select the appropriate kernel.
- The
- Kernel Loading:
- The
load_selected_kernel
function uses the BIOS disk read interrupt (0x13
) to load the selected kernel from the disk based on the user’s choice.
- The
- Kernel Metadata:
- The
kernel_sectors
,kernel_heads
, andkernel_sizes
arrays store the disk locations and sizes of the kernels. Adjust these values based on the actual layout of your disk.
- The
Disk Layout
You need to place your kernels at the specified sectors on the disk. For example, you can lay out your disk like this:
- Boot Sector: First sector (
0x0000
to0x01FF
). - Kernel v1: Starts at sector 2.
- Kernel v2: Starts at sector 18.
- Kernel v3: Starts at sector 34.
Use dd
to place each kernel on the disk image:
1 2 3 | dd if=kernel_v1.bin of=disk.img bs=512 seek=2 conv=notrunc dd if=kernel_v2.bin of=disk.img bs=512 seek=18 conv=notrunc dd if=kernel_v3.bin of=disk.img bs=512 seek=34 conv=notrunc |
Filesystem to Store Kernels
Implementing a simple filesystem within your bootloader allows for more flexibility in managing multiple kernel versions. For this purpose, we can create a basic, custom filesystem that supports reading files from the disk.
The filesystem will include:
- Superblock: Contains metadata about the filesystem (e.g., number of files, size of the directory).
- Directory Table: Stores the names of the files and their locations on the disk.
- File Data Blocks: The actual content of the files (kernels in this case).
Filesystem Layout
We’ll define a simple filesystem layout on the disk as follows:
- Boot Sector: The first sector (512 bytes).
- Superblock: Contains information about the filesystem (e.g., number of files).
- Directory Table: Contains entries for each file (filename, start sector, size).
- File Data Blocks: Where the actual file data (kernels) is stored.
Filesystem Structures
- Superblock: Contains the total number of files and a pointer to the directory table.
- Directory Table: Contains entries for each file, including the filename, start sector, and size in sectors.
- File Data Blocks: Store the actual content of each kernel.
Implementation
Here’s an implementation in assembly to create and interact with a simple filesystem:
1. Bootloader with Filesystem
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | [BITS 16] ORG 0x7C00 start: ; Set up stack and data segments xor ax, ax mov ds, ax mov es, ax mov ss, ax mov sp, 0x7C00 ; Load filesystem metadata call load_superblock call load_directory_table ; Display menu call display_menu ; Get user input and choose kernel call get_user_input call load_selected_kernel ; Jump to protected mode and then to long mode (as described previously) call enter_protected_mode call long_mode_entry error: hlt jmp error ; Load the superblock from disk load_superblock: mov bx, superblock ; Destination in memory mov ah, 0x02 ; BIOS read sectors function mov al, 0x01 ; Read 1 sector mov ch, 0x00 ; Cylinder 0 mov cl, 0x02 ; Sector 2 (after boot sector) mov dh, 0x00 ; Head 0 mov dl, 0x80 ; Drive 0 (first hard disk) int 0x13 ; BIOS interrupt jc error ; Jump to error if carry flag is set ret ; Load the directory table from disk load_directory_table: mov bx, directory_table ; Destination in memory mov ax, [superblock] ; Load directory table start sector mov ah, 0x02 ; BIOS read sectors function mov al, 0x01 ; Read 1 sector (you can adjust based on directory size) mov ch, 0x00 ; Cylinder 0 mov cl, [ax+1] ; Sector number from superblock mov dh, 0x00 ; Head 0 mov dl, 0x80 ; Drive 0 (first hard disk) int 0x13 ; BIOS interrupt jc error ; Jump to error if carry flag is set ret ; Display a simple text-based menu to select the kernel version display_menu: mov si, menu_text call print_string ; Loop through directory entries and display filenames mov cx, [superblock] ; Number of files mov bx, directory_table .display_file: mov si, bx call print_string ; Print the filename add bx, 16 ; Move to the next directory entry (adjust size as necessary) loop .display_file ret ; Get user input for selecting kernel get_user_input: xor ax, ax int 0x16 ; BIOS keyboard input sub al, '1' ; Convert keypress to an index (assuming '1', '2', '3' for menu items) mov bx, ax ; Store the index in BX ret ; Load the selected kernel into memory based on user input load_selected_kernel: ; Assume BX contains the selected kernel index mov si, directory_table add si, bx ; Point to the correct directory entry ; Load start sector and size from directory entry mov ax, [si+12] ; Start sector (assuming little-endian) mov cx, [si+14] ; Size in sectors mov bx, 0x1000 ; Load kernel into memory at 0x1000 .load_kernel_sector: mov ah, 0x02 ; BIOS read sectors function mov al, 0x01 ; Read 1 sector mov ch, 0x00 ; Cylinder 0 mov cl, ax ; Sector number mov dh, 0x00 ; Head 0 mov dl, 0x80 ; Drive 0 (first hard disk) int 0x13 ; BIOS interrupt jc error ; Jump to error if carry flag is set add ax, 1 ; Move to the next sector add bx, 512 ; Move to the next memory block loop .load_kernel_sector ret ; Superblock data structure superblock: dw 2 ; Start sector of the directory table dw 3 ; Number of files in the directory ; Directory table placeholder directory_table: times 16 db 0 ; Placeholder, size depends on your needs ; Menu text menu_text db 'Select a Kernel Version:', 0xA, 0xD menu_text db 0 ; Null terminator ;times 510-($-$$) db 0 dw 0xAA55 |
Explanation
- Superblock:
- The superblock contains metadata about the filesystem, such as the number of files and the start sector of the directory table.
- Directory Table:
- The directory table holds entries for each file, including the filename, start sector, and size in sectors.
- Menu Display:
- The
display_menu
function iterates over the directory table and displays each file (kernel) available for booting.
- The
- Loading a Kernel:
- The
load_selected_kernel
function reads the file’s start sector and size from the directory table and loads it into memory.
- The
Filesystem Layout on Disk
To set up this simple filesystem, you’d structure your disk image like this:
- Boot Sector: First sector (
0x0000
to0x01FF
). - Superblock: Second sector (
0x0200
to0x02FF
). - Directory Table: Starting from the third sector.
- File Data Blocks: Kernels stored starting from subsequent sectors.
2. Preparing the Disk Image
- Write the bootloader:
dd if=bootloader.bin of=disk.img bs=512 count=1 conv=notrunc
- Prepare the superblock and directory table in a binary file (e.g.,
filesystem.bin
). - Write the filesystem metadata:
dd if=filesystem.bin of=disk.img bs=512 seek=1 conv=notrunc
- Write the kernel files:
dd if=kernel_v1.bin of=disk.img bs=512 seek=START_SECTOR_OF_KERNEL_V1 conv=notrunc dd if=kernel_v2.bin of=disk.img bs=512 seek=START_SECTOR_OF_KERNEL_V2 conv=notrunc
Where START_SECTOR_OF_KERNEL_V1
and START_SECTOR_OF_KERNEL_V2
correspond to the sectors specified in the directory table.
Adding a UI
Adding an advanced UI to your bootloader involves several enhancements over the basic text-based interface. These enhancements can include:
- Navigation with Arrow Keys: Allowing the user to navigate through the kernel options using the keyboard’s arrow keys.
- Highlighted Selection: Highlighting the currently selected kernel option.
- Countdown Timer: Automatically booting the default kernel after a countdown period if no user input is detected.
- Support for Simple Graphics: Optionally, we can move from text mode to a simple graphics mode to enhance the visual appearance of the menu.
Implementation
Here’s an implementation of an advanced UI in assembly:
1. Bootloader with Advanced UI
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | [BITS 16] ORG 0x7C00 start: ; Set up stack and data segments xor ax, ax mov ds, ax mov es, ax mov ss, ax mov sp, 0x7C00 ; Load filesystem metadata call load_superblock call load_directory_table ; Display menu and wait for user input call display_menu_with_selection ; Load and boot the selected kernel call load_selected_kernel ; Jump to protected mode and long mode (as previously described) call enter_protected_mode call long_mode_entry error: hlt jmp error ; Load the superblock from disk load_superblock: mov bx, superblock mov ah, 0x02 mov al, 0x01 mov ch, 0x00 mov cl, 0x02 mov dh, 0x00 mov dl, 0x80 int 0x13 jc error ret ; Load the directory table from disk load_directory_table: mov bx, directory_table mov ax, [superblock] mov ah, 0x02 mov al, 0x01 mov ch, 0x00 mov cl, [ax+1] mov dh, 0x00 mov dl, 0x80 int 0x13 jc error ret ; Display menu with selection and navigation display_menu_with_selection: xor bx, bx ; Initially select the first item mov si, [superblock + 2] ; Number of files ; Countdown loop (optional) mov cx, 5 ; Countdown from 5 seconds countdown_loop: push cx call display_menu call update_highlight ; Highlight the selected option call countdown_timer ; Display countdown pop cx dec cx jz countdown_expired ; If countdown hits zero, boot selected kernel call check_keypress ; Check for keypresses to navigate jmp countdown_loop countdown_expired: ret ; Proceed to load the selected kernel ; Display the menu options display_menu: mov si, menu_text call print_string ; Loop through directory entries and display filenames mov cx, [superblock + 2] mov bx, directory_table display_file: mov si, bx call print_string ; Print the filename add bx, 16 ; Move to the next directory entry loop display_file ret ; Print a string pointed to by SI print_string: mov ah, 0x0E next_char: lodsb cmp al, 0 je done int 0x10 jmp next_char done: ret ; Update the highlight for the selected kernel update_highlight: ; Move cursor to the start of the selection mov cx, bx ; CX = selected index shl cx, 4 ; Each entry is 16 bytes add cx, 20 ; Offset to the start of options ; Set the video mode to display highlighted text (use BIOS interrupt) mov ah, 0x02 int 0x10 ; Highlight the selected option mov ah, 0x0E mov al, '>' int 0x10 ret ; Handle navigation keypresses (arrow keys) check_keypress: xor ax, ax int 0x16 ; BIOS keyboard input cmp al, 0 je no_keypress ; Check for arrow keys (scan codes for up/down) cmp al, 0x48 ; Up arrow key je move_up cmp al, 0x50 ; Down arrow key je move_down ; If Enter is pressed, load the selected kernel cmp al, 0x0D je countdown_expired no_keypress: ret move_up: dec bx ; Move selection up cmp bx, 0x00 ; Prevent moving above the first option jge update_highlight inc bx ; Restore BX if underflow ret move_down: inc bx ; Move selection down cmp bx, [superblock + 2] ; Prevent moving below the last option jl update_highlight dec bx ; Restore BX if overflow ret ; Simple countdown timer display countdown_timer: ; Display countdown (assuming you want a timer at a specific screen location) ; You can implement this by setting cursor position and printing remaining time. ret ; Load the selected kernel into memory based on user input load_selected_kernel: mov si, directory_table add si, bx ; Point to the correct directory entry ; Load start sector and size from directory entry mov ax, [si+12] ; Start sector mov cx, [si+14] ; Size in sectors mov bx, 0x1000 ; Load kernel into memory at 0x1000 load_kernel_sector: mov ah, 0x02 mov al, 0x01 mov ch, 0x00 mov cl, ax mov dh, 0x00 mov dl, 0x80 int 0x13 jc error add ax, 1 add bx, 512 loop load_kernel_sector ret ; Superblock data structure superblock: dw 2 ; Start sector of the directory table dw 3 ; Number of files in the directory ; Directory table placeholder directory_table: times 16 db 0 ; Placeholder, size depends on your needs ; Menu text menu_text db 'Select a Kernel Version:', 0xA, 0xD menu_text db 0 ; Null terminator ;times 510-($-$$) db 0 dw 0xAA55 |
Key Features
- Arrow Key Navigation:
- The
check_keypress
function handles arrow key input. The BIOS interrupt0x16
is used to detect keypresses, with special handling for up and down arrow keys to navigate between menu options.
- The
- Highlighted Selection:
- The
update_highlight
function highlights the currently selected menu option using a simple method where a “>” symbol is displayed next to the selected option.
- The
- Countdown Timer:
- The
countdown_timer
function can display a countdown on the screen. This is currently a placeholder and can be further developed to display the countdown visually.
- The
- Default Kernel Selection:
- If no input is detected before the countdown expires, the default kernel (highlighted at the time) is automatically selected.
- Handling Input and Selection:
- The user can press Enter to select a kernel immediately, or the default kernel is automatically selected after the countdown.
Potential Enhancements
- Graphics Mode:
- Consider switching to a graphics mode for a more visually appealing interface. This requires setting up a VESA graphics mode and drawing the menu in a graphical context.
- More Complex Highlighting:
- Instead of using a “>” symbol, you can change the text color or use inverse text (background and foreground colors swapped) to highlight the selection.
- Custom Fonts:
- In graphics mode, you can load and display custom fonts to enhance the appearance of the text.
- Multilingual Support:
- Extend the menu to support multiple languages by loading different strings based on user preferences.
- Dynamic Kernel Detection:
- Automatically detect and list kernels based on files found in a directory or through scanning available sectors, rather than hardcoding the directory entries.
This advanced UI provides a more user-friendly and interactive way to select which kernel to boot, making the bootloader more versatile and visually appealing.
Dynamic Kernel Detection
Adding dynamic kernel detection to your bootloader involves scanning the disk to identify available kernel files rather than relying on hardcoded directory entries. The bootloader will then populate the menu dynamically based on the detected kernels.
Steps for Dynamic Kernel Detection
- File Naming Convention: Decide on a file naming convention for the kernels (e.g.,
KERNEL1.BIN
,KERNEL2.BIN
, etc.) so that the bootloader can recognize and list them. - Scan the Disk: Implement a routine to scan the disk for these files.
- Store Detected Kernels: Store the details of detected kernels (e.g., filename, starting sector, size) in memory.
- Display the Detected Kernels in the Menu: Modify the menu display to list the dynamically detected kernels.
Implementation
Below is the modified bootloader code that includes dynamic kernel detection:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 | [BITS 16] ORG 0x7C00 start: ; Set up stack and data segments xor ax, ax mov ds, ax mov es, ax mov ss, ax mov sp, 0x7C00 ; Load filesystem metadata call load_superblock call scan_for_kernels ; Dynamically detect kernels ; Display menu and wait for user input call display_menu_with_selection ; Load and boot the selected kernel call load_selected_kernel ; Jump to protected mode and long mode (as previously described) call enter_protected_mode call long_mode_entry error: hlt jmp error ; Load the superblock from disk load_superblock: mov bx, superblock mov ah, 0x02 mov al, 0x01 mov ch, 0x00 mov cl, 0x02 mov dh, 0x00 mov dl, 0x80 int 0x13 jc error ret ; Dynamically scan the disk for kernel files scan_for_kernels: mov cx, 0 ; Kernel count mov di, directory_table ; Start storing entries in the directory table ; Scan for files named KERNEL1.BIN, KERNEL2.BIN, etc. mov si, 1 ; Start with KERNEL1.BIN scan_next_kernel: call generate_filename ; Generate the filename (KERNELX.BIN) mov ax, 0x1301 ; BIOS interrupt for file read int 0x13 jc no_more_kernels ; Stop scanning if no more kernels found ; Store detected kernel in the directory table stosb ; Store filename in directory table stosw ; Store starting sector stosw ; Store size in sectors inc cx ; Increment kernel count add si, 1 ; Move to the next potential kernel cmp cx, 16 ; Limit the number of kernels to 16 jb scan_next_kernel no_more_kernels: ; Store the number of detected kernels in the superblock mov [superblock + 2], cx ret ; Generate a filename like KERNELX.BIN based on the current index in SI generate_filename: mov bx, si ; Store the current index in BX mov si, filename_template ; Point to the filename template call replace_index_in_filename ret ; Replace 'X' in the filename template with the current index replace_index_in_filename: mov cx, si ; Move index to CX for conversion add cl, '0' ; Convert to ASCII mov [si + 6], cl ; Replace 'X' with the index mov si, filename_template ; Point SI back to the template ret ; Display menu with selection and navigation display_menu_with_selection: xor bx, bx ; Initially select the first item mov si, [superblock + 2] ; Number of files ; Countdown loop (optional) mov cx, 5 ; Countdown from 5 seconds countdown_loop: push cx call display_menu call update_highlight ; Highlight the selected option call countdown_timer ; Display countdown pop cx dec cx jz countdown_expired ; If countdown hits zero, boot selected kernel call check_keypress ; Check for keypresses to navigate jmp countdown_loop countdown_expired: ret ; Proceed to load the selected kernel ; Display the menu options display_menu: mov si, menu_text call print_string ; Loop through directory entries and display filenames mov cx, [superblock + 2] mov bx, directory_table display_file: mov si, bx call print_string ; Print the filename add bx, 16 ; Move to the next directory entry loop display_file ret ; Print a string pointed to by SI print_string: mov ah, 0x0E next_char: lodsb cmp al, 0 je done int 0x10 jmp next_char done: ret ; Update the highlight for the selected kernel update_highlight: ; Move cursor to the start of the selection mov cx, bx ; CX = selected index shl cx, 4 ; Each entry is 16 bytes add cx, 20 ; Offset to the start of options ; Set the video mode to display highlighted text (use BIOS interrupt) mov ah, 0x02 int 0x10 ; Highlight the selected option mov ah, 0x0E mov al, '>' int 0x10 ret ; Handle navigation keypresses (arrow keys) check_keypress: xor ax, ax int 0x16 ; BIOS keyboard input cmp al, 0 je no_keypress ; Check for arrow keys (scan codes for up/down) cmp al, 0x48 ; Up arrow key je move_up cmp al, 0x50 ; Down arrow key je move_down ; If Enter is pressed, load the selected kernel cmp al, 0x0D je countdown_expired no_keypress: ret move_up: dec bx ; Move selection up cmp bx, 0x00 ; Prevent moving above the first option jge update_highlight inc bx ; Restore BX if underflow ret move_down: inc bx ; Move selection down cmp bx, [superblock + 2] ; Prevent moving below the last option jl update_highlight dec bx ; Restore BX if overflow ret ; Simple countdown timer display countdown_timer: ; Display countdown (assuming you want a timer at a specific screen location) ; You can implement this by setting cursor position and printing remaining time. ret ; Load the selected kernel into memory based on user input load_selected_kernel: mov si, directory_table add si, bx ; Point to the correct directory entry ; Load start sector and size from directory entry mov ax, [si+12] ; Start sector mov cx, [si+14] ; Size in sectors mov bx, 0x1000 ; Load kernel into memory at 0x1000 load_kernel_sector: mov ah, 0x02 mov al, 0x01 mov ch, 0x00 mov cl, ax mov dh, 0x00 mov dl, 0x80 int 0x13 jc error add ax, 1 add bx, 512 loop load_kernel_sector ret ; Filename template for generating kernel names (e.g., KERNEL1.BIN) filename_template db 'KERNELX.BIN', 0 ; Superblock data structure superblock: dw 2 ; Start sector of the directory table dw 0 ; Number of files (will be set dynamically) ; Directory table placeholder directory_table: times 256 db 0 ; Space for 16 entries, each 16 bytes ; Menu text menu_text db 'Select a Kernel Version:', 0xA, 0xD menu_text db 0 ; Null terminator times 510-($-$$) db 0 dw 0xAA55 |
Key Features and Modifications
- Dynamic Kernel Detection (
scan_for_kernels
):- This function scans the disk for files named
KERNEL1.BIN
,KERNEL2.BIN
, etc. It stops when it no longer finds any files or reaches a predefined limit (e.g., 16 kernels). - Each detected kernel’s information (filename, start sector, and size) is stored in a directory table in memory.
- This function scans the disk for files named
- Filename Generation (
generate_filename
andreplace_index_in_filename
):- These functions generate filenames like
KERNEL1.BIN
based on the current index during scanning.
- These functions generate filenames like
- Menu Display with Detected Kernels:
- The menu dynamically lists the kernels found during the scan, allowing the user to select one.
Hardening the Boot Sector
Improving the security and integrity of the bootloader and kernel selection process is critical, especially in environments where security is a concern. Below are several enhancements you can implement to enhance security, prevent unauthorized access, and ensure the integrity of the boot process.
1. Digital Signatures and Integrity Checks
a. Digital Signatures:
- Implementation: Each kernel should be signed with a digital signature. The bootloader can then verify the signature before loading the kernel to ensure it hasn’t been tampered with. This requires embedding a public key in the bootloader and using it to verify signatures.
- Process:
- Each kernel file is signed with a private key during the build process.
- The bootloader contains the corresponding public key.
- Before loading a kernel, the bootloader verifies its signature using the public key.
- If the signature verification fails, the bootloader should refuse to load the kernel and display an error message.
b. Checksums and Hashing:
- Implementation: Use cryptographic hash functions (e.g., SHA-256) to generate a hash for each kernel file. The bootloader calculates the hash of the kernel before loading it and compares it with a known good hash.
- Process:
- Generate a hash of each kernel file after compilation.
- Store the hash securely in the bootloader or a protected area on the disk.
- During boot, the bootloader calculates the hash of the selected kernel and compares it with the stored hash.
- If the hashes do not match, the bootloader should abort the boot process.
2. Secure Boot Implementation
- Implementation: Integrate your bootloader with a Secure Boot mechanism. Secure Boot ensures that only trusted software is loaded by verifying the digital signatures of all components, including the bootloader, kernel, and any other binaries involved in the boot process.
- Process:
- The system’s firmware (e.g., UEFI) verifies the bootloader’s signature before handing control over to it.
- The bootloader, in turn, verifies the kernel’s signature before loading it.
- This prevents unauthorized or malicious modifications to the bootloader or kernels.
3. Role-Based Access Control (RBAC) for Bootloader Configuration
- Implementation: Implement role-based access control for the bootloader’s configuration and kernel selection. For example, an administrator could be required to authenticate before changing the default kernel or altering the boot process.
- Process:
- Implement a simple password or passphrase check in the bootloader.
- Certain actions (e.g., booting into a non-default kernel, altering bootloader settings) require authentication.
- Store a hashed version of the password securely in the bootloader, and compare it to the user input at runtime.
4. Tamper Detection
- Implementation: Include tamper detection mechanisms in the bootloader. These can include detecting changes to the bootloader code, the configuration, or the kernel files.
- Process:
- Implement a watchdog or audit log that detects changes in the bootloader binary or its configuration files.
- Store a tamper-evident hash or checksum of critical bootloader components.
- On each boot, the bootloader verifies its own integrity against this hash or checksum and refuses to proceed if tampering is detected.
5. Redundant Bootloader and Kernel Backup
- Implementation: Maintain multiple copies of the bootloader and kernel files on the disk, and implement a fallback mechanism in case the primary bootloader or kernel is corrupted.
- Process:
- Store multiple copies of the bootloader and kernel files in different disk sectors.
- The bootloader first attempts to load the primary copy; if it fails (due to corruption or any other reason), it automatically tries the backup.
- The integrity of the primary and backup copies should be checked before they are used.
6. Encrypted Bootloader and Kernel Storage
- Implementation: Encrypt the bootloader and kernel files on the disk to prevent unauthorized access or tampering.
- Process:
- Encrypt the bootloader and kernel files using a symmetric encryption algorithm (e.g., AES).
- The bootloader is equipped with the decryption key (or a mechanism to derive it securely).
- Upon boot, the bootloader decrypts itself and the selected kernel before loading it into memory.
7. Audit and Logging
- Implementation: Implement logging mechanisms that record key events during the boot process (e.g., which kernel was selected, if any integrity checks failed, etc.). Logs can be stored in a secure, tamper-evident manner.
- Process:
- Create a secure area on the disk where boot logs are stored.
- Record events such as successful boots, failed integrity checks, and unauthorized access attempts.
- Implement a mechanism to review these logs post-boot (e.g., in the operating system) or display them on demand during the boot process.
8. Minimalist Approach to Reduce Attack Surface
- Implementation: Keep the bootloader code as minimal and straightforward as possible to reduce the potential attack surface.
- Process:
- Avoid unnecessary features or code that could introduce vulnerabilities.
- Perform a code audit to eliminate any redundant or potentially insecure code.
- Regularly update the bootloader to address security vulnerabilities.
9. Periodic Integrity Verification
- Implementation: Regularly verify the integrity of the bootloader and kernel files, even outside the boot process.
- Process:
- Use a scheduled task in the operating system to verify the integrity of the bootloader and kernel files periodically.
- Alert the administrator if any discrepancies are found between the stored hash and the current file state.
- Optionally, prevent the system from booting if the integrity check fails.
Conclusion
These enhancements would significantly improve the security and integrity of your bootloader and kernel loading process, protecting against unauthorized access, tampering, and failure.
By implementing these measures, you ensure that only trusted, verified software is executed, maintaining the integrity of the system’s boot process.
Example Hardening
Implementing tamper detection and a redundant bootloader in your system involves several key steps to ensure that the bootloader and kernel files have not been tampered with and that a backup bootloader is available in case of failure. Below is a detailed guide on how to implement these features.
1. Tamper Detection Implementation
Overview:
Tamper detection involves ensuring that the bootloader and kernel have not been modified. This can be achieved by calculating and verifying checksums or cryptographic hashes of the bootloader and kernel files.
Steps:
- Generate a Hash of the Bootloader and Kernel:
- Use a cryptographic hash function like SHA-256 to generate a hash of the bootloader and each kernel file.
- Store these hashes securely in a dedicated section of the disk (e.g., a “hash block” after the superblock).
- Verify Hashes During Boot:
- Before executing any code, the bootloader reads the stored hash values and recalculates the hash of the bootloader and selected kernel.
- If the recalculated hash does not match the stored hash, the bootloader detects tampering and halts the boot process or switches to a backup.
- Update Hashes After Modification:
- Any legitimate updates to the bootloader or kernel should also update the corresponding hash values.
Code Example:
Here’s how you might implement a simple hash-based tamper detection mechanism in your bootloader.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | [BITS 16] ORG 0x7C00 start: ; Set up stack and data segments xor ax, ax mov ds, ax mov es, ax mov ss, ax mov sp, 0x7C00 ; Load hash block and verify bootloader integrity call load_hash_block call verify_bootloader_hash ; Load filesystem metadata and proceed as before call load_superblock call scan_for_kernels ; Dynamically detect kernels call display_menu_with_selection call load_selected_kernel ; Jump to protected mode and long mode (as previously described) call enter_protected_mode call long_mode_entry error: ; Error handling hlt jmp error ; Load the hash block from disk load_hash_block: mov bx, hash_block mov ah, 0x02 mov al, 0x01 mov ch, 0x00 mov cl, HASH_BLOCK_SECTOR ; Sector where hash block is stored mov dh, 0x00 mov dl, 0x80 int 0x13 jc error ret ; Verify the bootloader's hash verify_bootloader_hash: ; Compute hash of the bootloader call compute_bootloader_hash ; Compare with stored hash mov si, computed_bootloader_hash mov di, [hash_block + 0] ; Bootloader hash starts at offset 0 in hash block call compare_hashes jc error ; Halt on mismatch ret ; Placeholder function to compute bootloader hash compute_bootloader_hash: ; Implement your hash function (e.g., SHA-256) here ; Store the computed hash in 'computed_bootloader_hash' ret ; Compare two hash values (si: source, di: destination) compare_hashes: mov cx, HASH_SIZE ; Size of the hash (e.g., 32 bytes for SHA-256) repe cmpsb jne error ; If hashes don't match, trigger error ret ; Continue with other functions for scanning kernels, loading selected kernel, etc. ; Placeholder for hash block data hash_block: times 512 db 0 ; 512-byte block for storing hashes ; Computed bootloader hash placeholder computed_bootloader_hash: times 32 db 0 ; Assuming SHA-256, which is 32 bytes ; Constants HASH_BLOCK_SECTOR equ 3 ; Example sector for hash block HASH_SIZE equ 32 ; Size of SHA-256 hash |
2. Redundant Bootloader Implementation
Overview:
A redundant bootloader ensures that if the primary bootloader fails or is detected as tampered with, a secondary (backup) bootloader is automatically used. This can be implemented by storing the backup bootloader in a different sector and having the primary bootloader check its own integrity before deciding to load the backup.
Steps:
- Store the Backup Bootloader:
- Store a copy of the bootloader in another sector on the disk (e.g., the fourth sector).
- Primary Bootloader Integrity Check:
- The primary bootloader checks its integrity during the boot process (as described above in the tamper detection section).
- If the primary bootloader fails the integrity check, it jumps to the backup bootloader.
- Load and Execute the Backup Bootloader:
- The backup bootloader is loaded into memory and executed if the primary fails.
Code Example:
Here’s how you might implement a redundant bootloader mechanism:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | [BITS 16] ORG 0x7C00 start: ; Set up stack and data segments xor ax, ax mov ds, ax mov es, ax mov ss, ax mov sp, 0x7C00 ; Load hash block and verify bootloader integrity call load_hash_block call verify_bootloader_hash ; Continue with regular boot process call load_superblock call scan_for_kernels call display_menu_with_selection call load_selected_kernel call enter_protected_mode call long_mode_entry jmp success error: ; Load and execute backup bootloader on error call load_backup_bootloader jmp 0x0000:0x7C00 ; Jump to the start of the backup bootloader success: ; If everything went fine, continue normally hlt ; Load the backup bootloader into memory load_backup_bootloader: mov bx, 0x7C00 mov ah, 0x02 mov al, 0x01 mov ch, 0x00 mov cl, BACKUP_BOOTLOADER_SECTOR ; Sector where the backup bootloader is stored mov dh, 0x00 mov dl, 0x80 int 0x13 jc error ret ; Placeholder for hash block data hash_block: times 512 db 0 ; 512-byte block for storing hashes ; Computed bootloader hash placeholder computed_bootloader_hash: times 32 db 0 ; Assuming SHA-256, which is 32 bytes ; Constants HASH_BLOCK_SECTOR equ 3 ; Example sector for hash block BACKUP_BOOTLOADER_SECTOR equ 4 ; Sector where the backup bootloader is stored HASH_SIZE equ 32 ; Size of SHA-256 hash |
3. Updating the Disk Image
To integrate the tamper detection and redundant bootloader:
- Assemble and Write the Primary Bootloader:
- Assemble the primary bootloader as usual and write it to the first sector of the disk image:
nasm -f bin primary_bootloader.asm -o primary_bootloader.bin dd if=primary_bootloader.bin of=disk.img bs=512 count=1 conv=notrunc
- Assemble the primary bootloader as usual and write it to the first sector of the disk image:
- Prepare and Write the Backup Bootloader:
- Assemble a copy of the bootloader as the backup and write it to the designated backup sector:
nasm -f bin backup_bootloader.asm -o backup_bootloader.bin dd if=backup_bootloader.bin of=disk.img bs=512 seek=4 conv=notrunc
- Assemble a copy of the bootloader as the backup and write it to the designated backup sector:
- Generate and Store Hashes:
- Generate the hash of the primary bootloader and store it in the hash block sector:
sha256sum primary_bootloader.bin > hash.txt dd if=hash.txt of=disk.img bs=512 seek=3 conv=notrunc
- Generate the hash of the primary bootloader and store it in the hash block sector:
Conclusion
By implementing tamper detection and a redundant bootloader, you significantly enhance the security and reliability of your boot process. The system is now capable of detecting unauthorized modifications and automatically falling back to a safe, verified version of the bootloader in case of failure.
The full assembly code for a bootloader that includes dynamic kernel detection, a user interface with multiple selections, tamper detection using cryptographic hashes, and redundancy with a backup bootloader.
Full Bootloader Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 | [BITS 16] ORG 0x7C00 start: ; Set up stack and data segments xor ax, ax mov ds, ax mov es, ax mov ss, ax mov sp, 0x7B00 ; Move stack below code to avoid overwriting ; Load hash block and verify bootloader integrity call load_hash_block call verify_bootloader_hash ; Load filesystem metadata and scan for kernels call load_superblock call scan_for_kernels ; Display menu and wait for user input call display_menu_with_selection ; Load and boot the selected kernel call load_selected_kernel ; Transition to protected mode call enter_protected_mode ; Jump to long mode (64-bit mode) jmp long_mode_entry jmp success error: ; Load and execute backup bootloader on error call load_backup_bootloader jmp 0x0000:0x7C00 ; Jump to the start of the backup bootloader success: ; If everything went fine, continue normally hlt ; Load the hash block from disk load_hash_block: mov bx, hash_block mov ah, 0x02 mov al, 0x01 mov ch, 0x00 mov cl, HASH_BLOCK_SECTOR ; Sector where hash block is stored mov dh, 0x00 mov dl, 0x80 int 0x13 jc error ret ; Verify the bootloader's hash verify_bootloader_hash: ; Compute hash of the bootloader call compute_bootloader_hash ; Compare with stored hash mov si, computed_bootloader_hash mov di, hash_block ; Bootloader hash starts at offset 0 in hash block call compare_hashes jc error ; Halt on mismatch ret ; Placeholder function to compute bootloader hash compute_bootloader_hash: ; Implement your hash function (e.g., SHA-256) here ; Store the computed hash in 'computed_bootloader_hash' ret ; Compare two hash values (si: source, di: destination) compare_hashes: mov cx, HASH_SIZE ; Size of the hash (e.g., 32 bytes for SHA-256) repe cmpsb jne error ; If hashes don't match, trigger error ret ; Load the backup bootloader into memory load_backup_bootloader: mov bx, 0x7C00 mov ah, 0x02 mov al, 0x01 mov ch, 0x00 mov cl, BACKUP_BOOTLOADER_SECTOR ; Sector where the backup bootloader is stored mov dh, 0x00 mov dl, 0x80 int 0x13 jc error ret ; Load the superblock from disk load_superblock: mov bx, superblock mov ah, 0x02 mov al, 0x01 mov ch, 0x00 mov cl, SUPERBLOCK_SECTOR ; Sector where superblock is stored mov dh, 0x00 mov dl, 0x80 int 0x13 jc error ret ; Dynamically scan the disk for kernel files scan_for_kernels: mov cx, 0 ; Kernel count mov di, directory_table ; Start storing entries in the directory table ; Scan for files named KERNEL1.BIN, KERNEL2.BIN, etc. mov si, 1 ; Start with KERNEL1.BIN scan_next_kernel: call generate_filename ; Generate the filename (KERNELX.BIN) ; BIOS interrupt to read file - implement file detection logic here ; If file is detected: ; Store detected kernel in the directory table ; stosb ; Store filename in directory table ; stosw ; Store starting sector ; stosw ; Store size in sectors ; Increment kernel count and continue scanning inc cx add si, 1 ; Move to the next potential kernel cmp cx, 16 ; Limit the number of kernels to 16 jb scan_next_kernel no_more_kernels: ; Store the number of detected kernels in the superblock mov [superblock + 2], cx ret ; Generate a filename like KERNELX.BIN based on the current index generate_filename: mov bx, si ; Store the current index in BX mov si, filename_template ; Point to the filename template call replace_index_in_filename ret ; Replace 'X' in the filename template with the current index replace_index_in_filename: mov cx, si ; Move index to CX for conversion add cl, '0' ; Convert to ASCII mov [si + 6], cl ; Replace 'X' with the index mov si, filename_template ; Point SI back to the template ret ; Display menu with selection and navigation display_menu_with_selection: xor bx, bx ; Initially select the first item mov si, [superblock + 2] ; Number of files ; Countdown loop (optional) mov cx, 5 ; Countdown from 5 seconds countdown_loop: push cx call display_menu call update_highlight ; Highlight the selected option call countdown_timer ; Display countdown pop cx dec cx jz countdown_expired ; If countdown hits zero, boot selected kernel call check_keypress ; Check for keypresses to navigate jmp countdown_loop countdown_expired: ret ; Proceed to load the selected kernel ; Display the menu options display_menu: mov si, menu_text call print_string ; Loop through directory entries and display filenames mov cx, [superblock + 2] mov bx, directory_table display_file: mov si, bx call print_string ; Print the filename add bx, 16 ; Move to the next directory entry loop display_file ret ; Print a string pointed to by SI print_string: mov ah, 0x0E next_char: lodsb cmp al, 0 je done int 0x10 jmp next_char done: ret ; Update the highlight for the selected kernel update_highlight: ; Move cursor to the start of the selection mov cx, bx ; CX = selected index shl cx, 4 ; Each entry is 16 bytes add cx, 20 ; Offset to the start of options ; Set the video mode to display highlighted text (use BIOS interrupt) mov ah, 0x02 int 0x10 ; Highlight the selected option mov ah, 0x0E mov al, '>' int 0x10 ret ; Handle navigation keypresses (arrow keys) check_keypress: xor ax, ax int 0x16 ; BIOS keyboard input cmp al, 0 je no_keypress ; Check for arrow keys (scan codes for up/down) cmp al, 0x48 ; Up arrow key je move_up cmp al, 0x50 ; Down arrow key je move_down ; If Enter is pressed, load the selected kernel cmp al, 0x0D je countdown_expired no_keypress: ret move_up: dec bx ; Move selection up cmp bx, 0x00 ; Prevent moving above the first option jge update_highlight inc bx ; Restore BX if underflow ret move_down: inc bx ; Move selection down cmp bx, [superblock + 2] ; Prevent moving below the last option jl update_highlight dec bx ; Restore BX if overflow ret ; Simple countdown timer display countdown_timer: ; Display countdown (assuming you want a timer at a specific screen location) ; You can implement this by setting cursor position and printing remaining time. ret ; Load the selected kernel into memory based on user input load_selected_kernel: mov si, directory_table add si, bx ; Point to the correct directory entry ; Load start sector and size from directory entry mov bx, si ; Copy base address from si to bx mov ax, [bx + 12] ; Load start sector (16-bit value) into ax mov cx, [bx + 14] ; Load size in sectors (16-bit value) into cx mov bx, 0x1000 ; Load kernel into memory at 0x1000 load_kernel_sector: push dx ; Save dx (to restore it later) mov dl, 0x80 ; Drive number (0x80 for the first hard drive) mov ah, 0x02 ; BIOS function: read sectors mov ch, 0x00 ; Cylinder number (0 initially) mov dh, 0x00 ; Head number (0 initially) load_next_sector: mov cl, al ; Set sector number from ax (1-based, so ax must start from 1) int 0x13 ; Call BIOS to read sector jc error ; If carry flag is set, jump to error add ax, 1 ; Increment sector number add bx, 512 ; Move to the next 512-byte block in memory dec cx ; Decrement sector count jnz load_next_sector ; If cx != 0, load the next sector pop dx ; Restore dx ret ; Placeholder for entering protected mode enter_protected_mode: ; Setup GDT, switch to protected mode, etc. ; This is a simplified example; the actual implementation depends on your kernel's requirements. cli ; Clear interrupts lgdt [gdt_descriptor] ; Load GDT mov eax, cr0 or eax, 1 ; Set PE bit to enter protected mode mov cr0, eax jmp CODE_SEG:protected_mode_entry ; Far jump to flush prefetch queue and enter protected mode [BITS 32] protected_mode_entry: ; Setup data segments mov ax, DATA_SEG mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax mov esp, 0x90000 ; Set up stack pointer ; Enable A20 line in al, 0x92 or al, 2 out 0x92, al ; Continue to long mode entry ret ; Placeholder for entering long mode long_mode_entry: ; Setup paging, enable long mode, etc. ; This is a simplified example; the actual implementation depends on your kernel's requirements. mov eax, cr4 or eax, 0x20 ; Enable PAE mov cr4, eax mov eax, cr0 or eax, 0x80000000 ; Enable paging mov cr0, eax mov ecx, 0xC0000080 ; Load EFER MSR rdmsr or eax, 0x100 ; Set LME bit to enable long mode wrmsr jmp long_mode_selector:long_mode_code ; Long jump to 64-bit mode [BITS 64] ; Long mode code starts here long_mode_code: ; Your 64-bit kernel entry point ; Set up 64-bit segments, etc. ret ; Placeholder GDT (Global Descriptor Table) gdt_start: dq 0x0000000000000000 ; Null segment dq 0x00A09A000000FFFF ; Code segment descriptor dq 0x00A092000000FFFF ; Data segment descriptor gdt_end: gdt_descriptor: dw gdt_end - gdt_start - 1 ; GDT size dd gdt_start ; GDT address CODE_SEG equ 0x08 ; Code segment selector DATA_SEG equ 0x10 ; Data segment selector long_mode_selector equ 0x18 ; Long mode code segment selector ; Filename template for generating kernel names (e.g., KERNEL1.BIN) filename_template db 'KERNELX.BIN', 0 ; Superblock data structure superblock: dw SUPERBLOCK_SECTOR ; Start sector of the directory table dw 0 ; Number of files (will be set dynamically) ; Directory table placeholder directory_table: times 256 db 0 ; Space for 16 entries, each 16 bytes ; Placeholder for hash block data hash_block: times 512 db 0 ; 512-byte block for storing hashes ; Computed bootloader hash placeholder computed_bootloader_hash: times 32 db 0 ; Assuming SHA-256, which is 32 bytes ; Menu text menu_text db 'Select a Kernel Version:', 0xA, 0xD db 0 ; Null terminator ; Constants SUPERBLOCK_SECTOR equ 2 ; Example sector for superblock HASH_BLOCK_SECTOR equ 3 ; Example sector for hash block BACKUP_BOOTLOADER_SECTOR equ 4 ; Sector where the backup bootloader is stored HASH_SIZE equ 32 ; Size of SHA-256 hash ;times 510-($-$$) db 0 ; This commented out because code exceeds 512 bytes, ; next iteration splits functions out dw 0xAA55 |
Detailed Explanation
- Tamper Detection:
- The bootloader calculates a cryptographic hash (e.g., SHA-256) of itself during the boot process. It compares this hash with a precomputed hash stored in a dedicated hash block on the disk.
- If the hashes do not match, indicating tampering, the bootloader stops the boot process and switches to the backup bootloader.
- Redundant Bootloader:
- A backup bootloader is stored on the disk in a different sector. If the primary bootloader fails its integrity check or encounters an error, the system will attempt to load the backup bootloader.
- Dynamic Kernel Detection:
- The bootloader scans the disk for files matching a specific naming convention (e.g.,
KERNEL1.BIN
,KERNEL2.BIN
, etc.). - It dynamically populates the boot menu with the available kernels.
- The bootloader scans the disk for files matching a specific naming convention (e.g.,
- User Interface with Multiple Selections:
- The bootloader provides a simple text-based UI allowing the user to select which kernel to boot. The selection can be made using arrow keys, and the bootloader highlights the selected option.
- A countdown timer is also included, automatically selecting the default kernel if no input is provided.
- Hash and Checksum Handling:
- The bootloader uses placeholders for cryptographic operations, such as calculating and comparing hashes. In a real-world implementation, you would need to replace these placeholders with actual cryptographic functions, such as a SHA-256 hash function.
Preparing the Disk Image
- Assemble and Write the Primary Bootloader:
nasm -f bin primary_bootloader.asm -o primary_bootloader.bin dd if=primary_bootloader.bin of=disk.img bs=512 count=1 conv=notrunc
- Prepare and Write the Backup Bootloader:
nasm -f bin backup_bootloader.asm -o backup_bootloader.bin dd if=backup_bootloader.bin of=disk.img bs=512 seek=4 conv=notrunc
- Generate and Store Hashes:
- After compiling the bootloader, generate the hash:
sha256sum primary_bootloader.bin > hash.txt dd if=hash.txt of=disk.img bs=512 seek=3 conv=notrunc
- Write the Kernel Files:
- Place kernel binaries in the appropriate sectors on the disk image as detected by the bootloader.
Conclusion
This implementation provides a robust and secure bootloader with tamper detection, redundancy, dynamic kernel detection, and a user-friendly interface. This foundation can be extended further by adding more advanced features such as encryption, more sophisticated error handling, or even graphical elements in the UI.
Fixing Size
Below is a version of the code split into two parts: one for the bootloader and the other for kernel detection, verification, and selection.
Part 1: Bootloader (Primary Bootloader)
This part handles the initial boot process, setting up the stack, loading the secondary stage (kernel detection, verification, and selection), and transitioning to protected mode.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | [BITS 16] ORG 0x7C00 start: ; Set up stack and data segments xor ax, ax mov ds, ax mov es, ax mov ss, ax mov sp, 0x7B00 ; Move stack below code to avoid overwriting ; Load the secondary stage (kernel detection, verification, and selection) call load_secondary_stage ; Transition to protected mode call enter_protected_mode ; Jump to long mode (64-bit mode) jmp long_mode_entry jmp success error: hlt ; Halt the system on error success: hlt ; Halt the system if successful (this should be replaced by a jump to the loaded kernel) ; Load the secondary stage into memory load_secondary_stage: mov ax, SECOND_STAGE_SECTOR ; Sector where the secondary stage starts mov cx, SECOND_STAGE_SIZE ; Number of sectors to load mov bx, 0x2000 ; Load secondary stage into memory at 0x2000 load_secondary_sector: push dx ; Save DX (important to preserve registers) mov dl, 0x80 ; Set the drive number (0x80 for the first hard drive) mov ah, 0x02 ; BIOS function: read sectors into memory mov ch, 0x00 ; Cylinder number (0 initially) mov dh, 0x00 ; Head number (0 initially) load_next_secondary_sector: mov cl, al ; Load sector number into CL int 0x13 ; Call BIOS interrupt to read sector jc error ; If carry flag is set, jump to error handling add ax, 1 ; Increment sector number add bx, 512 ; Move to the next 512-byte block in memory dec cx ; Decrement sector count jnz load_next_secondary_sector ; If CX != 0, load the next sector pop dx ; Restore DX jmp 0x2000 ; Jump to the secondary stage in memory ret ; Placeholder for entering protected mode enter_protected_mode: cli ; Clear interrupts lgdt [gdt_descriptor] ; Load GDT mov eax, cr0 or eax, 1 ; Set PE bit to enter protected mode mov cr0, eax jmp CODE_SEG:protected_mode_entry ; Far jump to flush prefetch queue and enter protected mode [BITS 32] protected_mode_entry: ; Setup data segments mov ax, DATA_SEG mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax mov esp, 0x90000 ; Set up stack pointer ; Enable A20 line in al, 0x92 or al, 2 out 0x92, al ; Continue to long mode entry ret ; Placeholder for entering long mode long_mode_entry: mov eax, cr4 or eax, 0x20 ; Enable PAE mov cr4, eax mov eax, cr0 or eax, 0x80000000 ; Enable paging mov cr0, eax mov ecx, 0xC0000080 ; Load EFER MSR rdmsr or eax, 0x100 ; Set LME bit to enable long mode wrmsr jmp long_mode_selector:long_mode_code ; Long jump to 64-bit mode [BITS 64] ; Long mode code starts here long_mode_code: ; Your 64-bit kernel entry point ret ; Placeholder GDT (Global Descriptor Table) gdt_start: dq 0x0000000000000000 ; Null segment dq 0x00A09A000000FFFF ; Code segment descriptor dq 0x00A092000000FFFF ; Data segment descriptor gdt_end: gdt_descriptor: dw gdt_end - gdt_start - 1 ; GDT size dd gdt_start ; GDT address CODE_SEG equ 0x08 ; Code segment selector DATA_SEG equ 0x10 ; Data segment selector long_mode_selector equ 0x18 ; Long mode code segment selector ; Constants SECOND_STAGE_SECTOR equ 5 ; Sector where the secondary stage starts SECOND_STAGE_SIZE equ 2 ; Example size of the secondary stage in sectors times 510-($-$$) db 0 dw 0xAA55 |
Part 2: Kernel Detection, Verification, and Selection (Secondary Stage)
This secondary stage will be loaded by the primary bootloader. It handles kernel detection, verification, and selection.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 | [BITS 16] ORG 0x2000 start_secondary_stage: ; Load filesystem metadata and scan for kernels call load_superblock call scan_for_kernels ; Display menu and wait for user input call display_menu_with_selection ; Load and boot the selected kernel call load_selected_kernel jmp 0x0000:0x7C00 ; Jump back to the primary bootloader or directly to the kernel error: hlt ; Halt the system on error ; Load the superblock from disk load_superblock: mov bx, superblock mov ah, 0x02 mov al, 0x01 mov ch, 0x00 mov cl, SUPERBLOCK_SECTOR ; Sector where superblock is stored mov dh, 0x00 mov dl, 0x80 int 0x13 jc error ret ; Dynamically scan the disk for kernel files scan_for_kernels: mov cx, 0 ; Kernel count mov di, directory_table ; Start storing entries in the directory table ; Scan for files named KERNEL1.BIN, KERNEL2.BIN, etc. mov si, 1 ; Start with KERNEL1.BIN scan_next_kernel: call generate_filename ; Generate the filename (KERNELX.BIN) ; BIOS interrupt to read file - implement file detection logic here ; If file is detected: ; Store detected kernel in the directory table ; stosb ; Store filename in directory table ; stosw ; Store starting sector ; stosw ; Store size in sectors ; Increment kernel count and continue scanning inc cx add si, 1 ; Move to the next potential kernel cmp cx, 16 ; Limit the number of kernels to 16 jb scan_next_kernel no_more_kernels: ; Store the number of detected kernels in the superblock mov [superblock + 2], cx ret ; Generate a filename like KERNELX.BIN based on the current index generate_filename: mov bx, si ; Store the current index in BX mov si, filename_template ; Point to the filename template call replace_index_in_filename ret ; Replace 'X' in the filename template with the current index replace_index_in_filename: mov cx, si ; Move index to CX for conversion add cl, '0' ; Convert to ASCII mov [si + 6], cl ; Replace 'X' with the index mov si, filename_template ; Point SI back to the template ret ; Display menu with selection and navigation display_menu_with_selection: xor bx, bx ; Initially select the first item mov si, [superblock + 2] ; Number of files ; Countdown loop (optional) mov cx, 5 ; Countdown from 5 seconds countdown_loop: push cx call display_menu call update_highlight ; Highlight the selected option call countdown_timer ; Display countdown pop cx dec cx jz countdown_expired ; If countdown hits zero, boot selected kernel call check_keypress ; Check for keypresses to navigate jmp countdown_loop countdown_expired: ret ; Proceed to load the selected kernel ; Display the menu options display_menu: mov si, menu_text call print_string ; Loop through directory entries and display filenames mov cx, [superblock + 2] mov bx, directory_table display_file: mov si, bx call print_string ; Print the filename add bx, 16 ; Move to the next directory entry loop display_file ret ; Print a string pointed to by SI print_string: mov ah, 0x0E next_char: lodsb cmp al, 0 je done int 0x10 jmp next_char done: ret ; Update the highlight for the selected kernel update_highlight: ; Move cursor to the start of the selection mov cx, bx ; CX = selected index shl cx, 4 ; Each entry is 16 bytes add cx, 20 ; Offset to the start of options ; Set the video mode to display highlighted text (use BIOS interrupt) mov ah, 0x02 int 0x10 ; Highlight the selected option mov ah, 0x0E mov al, '>' int 0x10 ret ; Handle navigation keypresses (arrow keys) check_keypress: xor ax, ax int 0x16 ; BIOS keyboard input cmp al, 0 je no_keypress ; Check for arrow keys (scan codes for up/down) cmp al, 0x48 ; Up arrow key je move_up cmp al, 0x50 ; Down arrow key je move_down ; If Enter is pressed, load the selected kernel cmp al, 0x0D je countdown_expired no_keypress: ret move_up: dec bx ; Move selection up cmp bx, 0x00 ; Prevent moving above the first option jge update_highlight inc bx ; Restore BX if underflow ret move_down: inc bx ; Move selection down cmp bx, [superblock + 2] ; Prevent moving below the last option jl update_highlight dec bx ; Restore BX if overflow ret ; Simple countdown timer display countdown_timer: ret ; Load the selected kernel into memory based on user input load_selected_kernel: mov si, directory_table add si, bx ; Point to the correct directory entry ; Load start sector and size from directory entry mov bx, si ; Copy base address from si to bx mov ax, [bx + 12] ; Load start sector (16-bit value) into ax mov cx, [bx + 14] ; Load size in sectors (16-bit value) into cx mov bx, 0x1000 ; Load kernel into memory at 0x1000 load_kernel_sector: push dx ; Save dx (to restore it later) mov dl, 0x80 ; Drive number (0x80 for the first hard drive) mov ah, 0x02 ; BIOS function: read sectors mov ch, 0x00 ; Cylinder number (0 initially) mov dh, 0x00 ; Head number (0 initially) load_next_sector: mov cl, al ; Set sector number from ax (1-based, so ax must start from 1) int 0x13 ; Call BIOS to read sector jc error ; If carry flag is set, jump to error add ax, 1 ; Increment sector number add bx, 512 ; Move to the next 512-byte block in memory dec cx ; Decrement sector count jnz load_next_sector ; If cx != 0, load the next sector pop dx ; Restore dx ret ; Filename template for generating kernel names (e.g., KERNEL1.BIN) filename_template db 'KERNELX.BIN', 0 ; Superblock data structure superblock: dw SUPERBLOCK_SECTOR ; Start sector of the directory table dw 0 ; Number of files (will be set dynamically) ; Directory table placeholder directory_table: times 256 db 0 ; Space for 16 entries, each 16 bytes ; Menu text menu_text db 'Select a Kernel Version:', 0xA, 0xD db 0 ; Null terminator ; Constants SUPERBLOCK_SECTOR equ 2 ; Example sector for superblock ;times 510-($-$$) db 0 dw 0xAA55 |
Summary:
- Primary Bootloader: Handles basic bootloader tasks, sets up the environment, and loads the secondary stage, which is responsible for more complex tasks like kernel detection, verification, and selection.
- Secondary Stage: Once loaded by the primary bootloader, this stage performs kernel scanning, displays a menu for kernel selection, and then loads the selected kernel.
You can assemble and write each part to separate sectors on the disk. The primary bootloader will start at 0x7C00
, and the secondary stage will be loaded into a separate memory location (e.g., 0x2000
in this example).
To compile and write the bootloader and secondary stage to a disk, you’ll need to follow these steps:
Tools Required:
- NASM (Netwide Assembler): For assembling the bootloader and secondary stage source files.
- dd (Data Duplicator): A command-line tool used to write raw data to disk images (on Linux or MacOS).
- Disk Image: This can be a physical disk, a USB drive, or a virtual disk image (for use in emulators like QEMU or VirtualBox).
Step 1: Assemble the Bootloader and Secondary Stage
- Assemble the Primary Bootloader:
- Save the bootloader code (Part 1) in a file called
primary_bootloader.asm
. - Use NASM to assemble this file into a binary file:
nasm -f bin primary_bootloader.asm -o primary_bootloader.bin
This will create a binary file namedprimary_bootloader.bin
. - Save the bootloader code (Part 1) in a file called
- Assemble the Secondary Stage:
- Save the secondary stage code (Part 2) in a file called
secondary_stage.asm
. - Use NASM to assemble this file into a binary file:
nasm -f bin secondary_stage.asm -o secondary_stage.bin
This will create a binary file namedsecondary_stage.bin
. - Save the secondary stage code (Part 2) in a file called
Step 2: Create a Disk Image (Optional)
If you want to write the bootloader to a disk image rather than a physical disk, you can create a blank disk image first:
1 | dd if=/dev/zero of=bootdisk.img bs=512 count=2880 |
This creates a 1.44 MB floppy disk image filled with zeros. The size and type can be adjusted depending on your needs.
Step 3: Write the Bootloader and Secondary Stage to the Disk
- Write the Bootloader to the Disk:
- The bootloader is written to the first sector of the disk, which is where the BIOS looks for the bootloader.
dd if=primary_bootloader.bin of=/dev/sdX bs=512 count=1
Replace/dev/sdX
with the correct device for your disk. If you’re using a disk image (likebootdisk.img
), use that as the output file:dd if=primary_bootloader.bin of=bootdisk.img bs=512 count=1 conv=notrunc
conv=notrunc
is important when writing to disk images to prevent truncating the file. - Write the Secondary Stage to the Disk:
- The secondary stage should be written to a specific sector(s) on the disk. The
SECOND_STAGE_SECTOR
constant in the primary bootloader indicates where to load this stage from. For example, ifSECOND_STAGE_SECTOR equ 5
, you would write the secondary stage starting at sector 5:
dd if=secondary_stage.bin of=/dev/sdX bs=512 seek=5
Or if using a disk image:dd if=secondary_stage.bin of=bootdisk.img bs=512 seek=5 conv=notrunc
- The secondary stage should be written to a specific sector(s) on the disk. The
Step 4: Boot the Disk or Disk Image
If you wrote the bootloader and secondary stage to a physical disk (like a USB drive), you can now boot a computer from this drive.
If you used a disk image, you can boot it in an emulator like QEMU:
1 | qemu-system-x86_64 -drive format=raw,file=bootdisk.img |
Or in VirtualBox by attaching the disk image as a virtual hard drive.
Summary:
- NASM assembles the bootloader and secondary stage source files into binary files.
- dd writes these binaries to specific sectors on a disk or disk image.
- The bootloader resides in the first sector of the disk, and it loads the secondary stage from a predefined location (e.g., sector 5).
- Boot the disk or disk image using a physical machine or an emulator.
This setup allows the bootloader to initiate the boot process, then load and execute the secondary stage, which handles more complex tasks like kernel detection and selection.
SHA-256 Code
To generate a SHA-256 hash of a file like primary_bootloader.bin
and save the output to a text file hash.txt
, you can use the following code in C. This code mimics the functionality of the sha256sum
command in Linux, computing the SHA-256 hash and outputting it in hexadecimal format.
Here’s a C program to do this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | #include <stdio.h> #include <stdlib.h> #include <string.h> #include <openssl/sha.h> void compute_sha256(const char* path, unsigned char output[SHA256_DIGEST_LENGTH]) { FILE* file = fopen(path, "rb"); if (!file) { perror("fopen"); exit(EXIT_FAILURE); } SHA256_CTX sha256; SHA256_Init(&sha256); unsigned char buffer[1024]; size_t bytes_read = 0; while ((bytes_read = fread(buffer, 1, sizeof(buffer), file)) > 0) { SHA256_Update(&sha256, buffer, bytes_read); } fclose(file); SHA256_Final(output, &sha256); } void print_sha256(unsigned char hash[SHA256_DIGEST_LENGTH]) { for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) { printf("%02x", hash[i]); } printf("\n"); } void save_sha256_to_file(unsigned char hash[SHA256_DIGEST_LENGTH], const char* output_path) { FILE* output_file = fopen(output_path, "w"); if (!output_file) { perror("fopen"); exit(EXIT_FAILURE); } for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) { fprintf(output_file, "%02x", hash[i]); } fprintf(output_file, "\n"); fclose(output_file); } int main(int argc, char *argv[]) { if (argc != 3) { fprintf(stderr, "Usage: %s <input_file> <output_file>\n", argv[0]); exit(EXIT_FAILURE); } const char* input_file = argv[1]; const char* output_file = argv[2]; unsigned char hash[SHA256_DIGEST_LENGTH]; compute_sha256(input_file, hash); // Print the hash to the console (optional) print_sha256(hash); // Save the hash to the output file save_sha256_to_file(hash, output_file); return 0; } |
Compilation and Usage
- Save the code to a file, for example,
sha256sum.c
. - Compile the code using GCC:
gcc -o sha256sum sha256sum.c -lssl -lcrypto
This links the OpenSSL library, which provides theSHA256
functions. - Run the program:
./sha256sum primary_bootloader.bin hash.txt
This command computes the SHA-256 hash ofprimary_bootloader.bin
and writes the result tohash.txt
.
How It Works
compute_sha256
: This function reads the file in chunks, updates the SHA-256 context with each chunk, and finally computes the hash.print_sha256
: This function prints the computed SHA-256 hash to the console in hexadecimal format. It’s useful for debugging or verifying the hash manually.save_sha256_to_file
: This function saves the computed hash to a file,hash.txt
, in hexadecimal format, similar to whatsha256sum
does in Linux.
Example Output in hash.txt
After running the program, hash.txt
will contain something like:
1 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
This is the SHA-256 hash of primary_bootloader.bin
, represented in hexadecimal.
Conclusion
This C program provides the same functionality as the sha256sum
command, allowing you to generate a SHA-256 hash for your bootloader binary and save it to a text file. You can then use this hash in your bootloader for tamper detection.
Test Harness
Creating a test harness to check the functions of the bootloader in an environment where we can’t execute BIOS interrupts directly or interact with real hardware can be challenging.
However, we can simulate certain aspects of the bootloader in a controlled environment using a simple test framework in C or Assembly that runs on a standard operating system.
For the sake of simplicity and to allow you to run these tests on a typical development environment, we will create a test harness in C that simulates some of the bootloader’s functions, particularly those related to string handling, hashing, and integrity checks.
Test Harness in C
Here’s a C program that simulates and tests key aspects of the bootloader:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | #include <stdio.h> #include <string.h> #include <stdint.h> #include <openssl/sha.h> // You will need to link with the OpenSSL library #define HASH_SIZE 32 // Size of SHA-256 hash #define MAX_KERNELS 16 #define KERNEL_NAME_TEMPLATE "KERNEL%d.BIN" // Simulated directory table typedef struct { char filename[12]; uint16_t start_sector; uint16_t size; } DirectoryEntry; DirectoryEntry directory_table[MAX_KERNELS]; // Simulated hash block uint8_t hash_block[HASH_SIZE]; // Function prototypes void generate_filename(int index, char* output); void replace_index_in_filename(int index, char* filename); void compute_hash(const uint8_t* data, size_t length, uint8_t* output); int compare_hashes(const uint8_t* hash1, const uint8_t* hash2); void load_superblock(void); void scan_for_kernels(void); void print_directory_table(void); int main() { // Simulate loading the superblock and scanning for kernels load_superblock(); scan_for_kernels(); // Print the detected kernels print_directory_table(); // Simulate computing and verifying the bootloader hash uint8_t computed_hash[HASH_SIZE]; const char* bootloader_data = "Simulated bootloader data"; compute_hash((const uint8_t*)bootloader_data, strlen(bootloader_data), computed_hash); // Assuming the hash_block was set up previously memcpy(hash_block, computed_hash, HASH_SIZE); // Simulate a correct hash if (compare_hashes(computed_hash, hash_block) == 0) { printf("Bootloader integrity check passed.\n"); } else { printf("Bootloader integrity check failed.\n"); } return 0; } // Generate a filename like KERNELX.BIN based on the current index void generate_filename(int index, char* output) { sprintf(output, KERNEL_NAME_TEMPLATE, index); } // Simulate replacing 'X' in the filename template with the current index void replace_index_in_filename(int index, char* filename) { sprintf(filename, "KERNEL%d.BIN", index); } // Compute a SHA-256 hash of the given data void compute_hash(const uint8_t* data, size_t length, uint8_t* output) { SHA256_CTX sha256; SHA256_Init(&sha256); SHA256_Update(&sha256, data, length); SHA256_Final(output, &sha256); } // Compare two hashes for equality int compare_hashes(const uint8_t* hash1, const uint8_t* hash2) { return memcmp(hash1, hash2, HASH_SIZE); } // Simulate loading the superblock (just initializing in this case) void load_superblock(void) { // Normally you would load this from disk, here we just simulate it printf("Superblock loaded.\n"); } // Simulate scanning for kernels (just adding dummy entries) void scan_for_kernels(void) { for (int i = 0; i < MAX_KERNELS; i++) { generate_filename(i + 1, directory_table[i].filename); directory_table[i].start_sector = 2 + i * 16; // Dummy sector directory_table[i].size = 16; // Dummy size } printf("Scanned and detected %d kernels.\n", MAX_KERNELS); } // Print the simulated directory table void print_directory_table(void) { printf("Detected Kernels:\n"); for (int i = 0; i < MAX_KERNELS; i++) { printf("Filename: %s, Start Sector: %d, Size: %d\n", directory_table[i].filename, directory_table[i].start_sector, directory_table[i].size); } } |
How This Works:
- Simulated Functions:
generate_filename
andreplace_index_in_filename
: These functions simulate generating and modifying kernel filenames based on an index. This mirrors the logic in the assembly code for generating file names likeKERNEL1.BIN
,KERNEL2.BIN
, etc.compute_hash
: This function computes a SHA-256 hash of the bootloader data using OpenSSL. In a real bootloader, you would compute the hash over the actual bootloader binary.compare_hashes
: This function compares two hash values to check if they match, simulating the tamper detection logic.load_superblock
andscan_for_kernels
: These functions simulate loading the superblock and scanning for kernel files. The actual bootloader would load these from disk.
- Hash Verification:
- The test harness computes a hash for simulated bootloader data and compares it to a pre-stored hash (
hash_block
). If they match, it prints that the integrity check passed; otherwise, it prints a failure message.
- The test harness computes a hash for simulated bootloader data and compares it to a pre-stored hash (
- Simulated Directory Table:
- The
directory_table
stores information about detected kernels. The test harness populates this with dummy data, simulating what the bootloader would do after scanning the disk for kernel files.
- The
Running the Test Harness
- Compile and Run:
- Ensure you have OpenSSL installed, as it’s used to calculate the SHA-256 hash. Compile and run the test harness like this:
gcc -o test_harness test_harness.c -lssl -lcrypto ./test_harness
- Review the Output:
- The output will show the simulated scanning of kernel files, display the directory table, and perform the integrity check on the bootloader.
Extending the Test Harness
- Add More Tests:
- You can extend this test harness by adding additional tests for specific functions, like checking the user interface logic or handling simulated user input.
- Simulate Disk I/O:
- For a more realistic test, simulate disk I/O operations where the functions load data from a binary file representing the disk image instead of hardcoded data.
- Automate Testing:
- Consider automating the test process to verify bootloader integrity and functionality across different scenarios, such as when tampering is detected or when the backup bootloader is used.
Conclusion
This C-based test harness provides a simplified environment to test some of the key functions of your bootloader, such as generating filenames, computing hashes, and verifying integrity. This approach helps you validate logic before deploying the actual bootloader on a disk.
Architectural Summary
Bootloader System with Kernel Detection, Verification, and Selection
1. Overview
The system architecture described here is a two-stage bootloader designed to initialize and load an operating system kernel in a flexible and modular manner. The architecture separates the responsibilities of basic system initialization and complex kernel selection into two distinct stages:
- Primary Bootloader (Stage 1): This is the initial piece of code executed by the system upon boot. It is responsible for basic hardware setup, loading the secondary stage from disk, and transitioning the system into a more advanced mode (Protected Mode or Long Mode).
- Secondary Stage (Stage 2): Once loaded by the primary bootloader, this stage is responsible for detecting available kernel images on the disk, verifying their integrity, presenting a selection menu to the user, and finally loading and executing the selected kernel.
2. Architectural Components
A. Primary Bootloader (Stage 1)
- Purpose:
- The primary bootloader is the first code executed when the system boots. It is small, compact, and fits within the first 512 bytes of the disk (typically the Master Boot Record (MBR) or a dedicated boot partition).
- Key Responsibilities:
- Hardware Initialization: The bootloader sets up the stack and initializes the data segments.
- Secondary Stage Loading: The primary bootloader loads the secondary stage (which contains more complex boot logic) from a predefined location on the disk.
- Transition to Protected Mode: If the operating system requires it, the bootloader transitions the CPU from Real Mode to Protected Mode (and optionally to Long Mode for 64-bit operation).
- Key Operations:
- Disk I/O: The bootloader uses BIOS interrupts (e.g.,
int 0x13
) to read sectors from the disk into memory. - Segment and Stack Setup: Properly initializes data segments and stack pointer for consistent operation.
- Mode Transition: Prepares and transitions the system into Protected or Long Mode, setting up the Global Descriptor Table (GDT) and enabling paging if necessary.
- Disk I/O: The bootloader uses BIOS interrupts (e.g.,
- Constraints:
- The primary bootloader is constrained by the 512-byte size limit of the boot sector.
- It must be simple and robust, with minimal dependencies, to ensure reliability across different hardware.
B. Secondary Stage (Stage 2)
- Purpose:
- The secondary stage is loaded into memory by the primary bootloader and is responsible for the more complex task of detecting, verifying, and selecting an operating system kernel.
- Key Responsibilities:
- Filesystem Metadata Handling: Reads filesystem metadata, such as the superblock, to identify where kernel files are located on the disk.
- Kernel Detection: Scans the disk for available kernel files, typically named in a sequential manner (e.g.,
KERNEL1.BIN
,KERNEL2.BIN
). - User Interaction: Displays a menu for the user to select which kernel to boot.
- Kernel Loading: Loads the selected kernel into memory and passes control to it.
- Key Operations:
- File Detection and Management: Uses a basic method to locate and verify kernel files on the disk.
- Menu Display and Selection: Provides a simple user interface (usually text-based) to allow kernel selection.
- Memory Management: Ensures that the selected kernel is loaded into the correct memory location for execution.
- Constraints:
- The secondary stage is more complex and can be larger than the primary bootloader, but it still needs to be efficient in terms of memory and processing time.
- It must handle errors gracefully, providing feedback to the user and fallback options if necessary.
3. Execution Flow
- System Boot:
- The BIOS or UEFI firmware loads the primary bootloader from the first sector of the boot disk (typically 0x7C00).
- Primary Bootloader Execution:
- The primary bootloader sets up the CPU’s stack and data segments.
- It loads the secondary stage from a predetermined sector on the disk into memory.
- Once the secondary stage is loaded, the primary bootloader transitions the system into Protected Mode if necessary, and jumps to the secondary stage.
- Secondary Stage Execution:
- The secondary stage reads the superblock from the disk to understand the layout and locate kernel files.
- It scans the disk for kernel files, validates them, and builds a list of available kernels.
- The secondary stage displays a selection menu to the user.
- Upon user selection (or after a timeout), the secondary stage loads the selected kernel into memory.
- The secondary stage then jumps to the kernel’s entry point, handing over control to the operating system.
4. Design Considerations
- Modularity:
- The split between primary and secondary stages allows for a clean separation of concerns. The primary stage is minimal and focused on getting the system ready, while the secondary stage handles more complex tasks that can vary between systems.
- Flexibility:
- By isolating the kernel detection and selection logic into a secondary stage, the system can support multiple kernels and configurations without requiring changes to the primary bootloader.
- Scalability:
- The architecture can be extended by adding more stages or integrating more sophisticated file systems and kernel management features in the secondary stage.
- Compatibility:
- The primary bootloader adheres to the BIOS/MBR standards, ensuring it can boot on a wide range of legacy and modern hardware. The secondary stage can be designed to work with different filesystems or kernel types.
5. Conclusion
This two-stage bootloader architecture provides a robust, flexible, and modular approach to booting an operating system. The primary bootloader handles critical early-stage tasks with minimal code, ensuring reliability, while the secondary stage offers powerful kernel management capabilities, allowing for dynamic kernel selection and loading. This design makes it easier to maintain and extend the boot process, offering both simplicity and flexibility in system initialization.