Load the kernel image ourselves

When loading something with UEFI LoadImage, signature validation is
performed. However, we verify the kernel by its hash already, and don't
want to sign it. Hence, we have to load it on our own.
This commit is contained in:
Alois Wohlschlager 2023-01-22 15:22:03 +01:00
parent 60e28106e9
commit 7387c6708d
No known key found for this signature in database
GPG Key ID: E0F59EA5E5216914
2 changed files with 183 additions and 29 deletions

View File

@ -2,20 +2,22 @@
#![no_std] #![no_std]
#![feature(abi_efiapi)] #![feature(abi_efiapi)]
#![feature(negative_impls)] #![feature(negative_impls)]
#![deny(unsafe_op_in_unsafe_fn)]
extern crate alloc; extern crate alloc;
mod linux_loader; mod linux_loader;
mod pe_loader;
mod pe_section; mod pe_section;
mod uefi_helpers; mod uefi_helpers;
use pe_loader::Image;
use pe_section::{pe_section, pe_section_as_string}; use pe_section::{pe_section, pe_section_as_string};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use uefi::{ use uefi::{
prelude::*, prelude::*,
proto::{ proto::{
console::text::Output, console::text::Output,
loaded_image::LoadedImage,
media::file::{File, FileAttribute, FileMode, RegularFile}, media::file::{File, FileAttribute, FileMode, RegularFile},
}, },
CString16, Result, CString16, Result,
@ -165,37 +167,13 @@ fn main(handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
let kernel_cmdline = let kernel_cmdline =
booted_image_cmdline(system_table.boot_services()).expect("Failed to fetch command line"); booted_image_cmdline(system_table.boot_services()).expect("Failed to fetch command line");
let kernel_handle = { let kernel =
system_table Image::load(system_table.boot_services(), &kernel_data).expect("Failed to load the kernel");
.boot_services()
.load_image(
handle,
uefi::table::boot::LoadImageSource::FromBuffer {
buffer: &kernel_data,
file_path: None,
},
)
.expect("UEFI refused to load the kernel image. It may not be signed or it may not have an EFI stub.")
};
let mut kernel_image = system_table
.boot_services()
.open_protocol_exclusive::<LoadedImage>(kernel_handle)
.expect("Failed to open the LoadedImage protocol");
unsafe {
kernel_image.set_load_options(
kernel_cmdline.as_ptr() as *const u8,
u32::try_from(kernel_cmdline.len()).unwrap(),
);
}
let mut initrd_loader = InitrdLoader::new(system_table.boot_services(), handle, initrd_data) let mut initrd_loader = InitrdLoader::new(system_table.boot_services(), handle, initrd_data)
.expect("Failed to load the initrd. It may not be there or it is not signed"); .expect("Failed to load the initrd. It may not be there or it is not signed");
let status = system_table
.boot_services() let status = unsafe { kernel.start(handle, &system_table, &kernel_cmdline) };
.start_image(kernel_handle)
.status();
initrd_loader initrd_loader
.uninstall(system_table.boot_services()) .uninstall(system_table.boot_services())

176
rust/stub/src/pe_loader.rs Normal file
View File

@ -0,0 +1,176 @@
use core::ffi::c_void;
use alloc::vec::Vec;
use goblin::pe::PE;
use uefi::{
prelude::BootServices,
proto::loaded_image::LoadedImage,
table::{
boot::{AllocateType, MemoryType},
Boot, SystemTable,
},
Handle, Status,
};
/// UEFI mandates 4 KiB pages.
const UEFI_PAGE_BITS: usize = 12;
const UEFI_PAGE_MASK: usize = (1 << UEFI_PAGE_BITS) - 1;
#[cfg(target_arch = "x86_64")]
fn flush_instruction_cache(_start: *const u8, _length: usize) {
// x86_64 mandates coherent instruction cache
}
pub struct Image {
image: &'static mut [u8],
entry: extern "efiapi" fn(Handle, SystemTable<Boot>) -> Status,
}
/// Converts a length in bytes to the number of required pages.
fn bytes_to_pages(bytes: usize) -> usize {
bytes
.checked_add(UEFI_PAGE_MASK)
.map(|rounded_up| rounded_up >> UEFI_PAGE_BITS)
.unwrap_or(1 << (usize::try_from(usize::BITS).unwrap() - UEFI_PAGE_BITS))
}
impl Image {
/// Loads and relocates a PE file.
///
/// The image must be handed to [`start`] later. If this does not
/// happen, the memory allocated for the unpacked PE binary will
/// leak.
pub fn load(boot_services: &BootServices, file_data: &[u8]) -> uefi::Result<Image> {
let pe = PE::parse(file_data).map_err(|_| Status::LOAD_ERROR)?;
// Allocate all memory the image will need in virtual memory.
// We follow shim here and allocate as EfiLoaderCode.
let image = {
let section_lengths = pe
.sections
.iter()
.map(|section| {
section
.virtual_address
.checked_add(section.virtual_size)
.ok_or(Status::LOAD_ERROR)
})
.collect::<Result<Vec<u32>, uefi::Status>>()?;
let length = usize::try_from(section_lengths.into_iter().max().unwrap_or(0)).unwrap();
let base = boot_services.allocate_pages(
AllocateType::AnyPages,
MemoryType::LOADER_CODE,
bytes_to_pages(length),
)? as *mut u8;
unsafe {
core::ptr::write_bytes(base, 0, length);
core::slice::from_raw_parts_mut(base, length)
}
};
// Populate all sections in virtual memory.
for section in &pe.sections {
let copy_size =
usize::try_from(u32::min(section.virtual_size, section.size_of_raw_data)).unwrap();
let raw_start = usize::try_from(section.pointer_to_raw_data).unwrap();
let raw_end = raw_start.checked_add(copy_size).ok_or(Status::LOAD_ERROR)?;
let virt_start = usize::try_from(section.virtual_address).unwrap();
let virt_end = virt_start
.checked_add(copy_size)
.ok_or(Status::LOAD_ERROR)?;
if virt_end > image.len() || raw_end > file_data.len() {
return Err(Status::LOAD_ERROR.into());
}
image[virt_start..virt_end].copy_from_slice(&file_data[raw_start..raw_end]);
}
// Image base relocations are not supported.
if pe
.header
.optional_header
.and_then(|h| *h.data_directories.get_base_relocation_table())
.is_some()
{
return Err(Status::INCOMPATIBLE_VERSION.into());
}
flush_instruction_cache(image.as_ptr(), image.len());
if pe.entry >= image.len() {
return Err(Status::LOAD_ERROR.into());
}
let entry = unsafe { core::mem::transmute(&image[pe.entry]) };
Ok(Image { image, entry })
}
/// Starts a trusted loaded PE file.
/// The caller is responsible for verifying that it trusts the PE file to uphold the invariants detailed below.
/// If the entry point returns, the image memory is subsequently deallocated.
///
/// # Safety
/// The image is assumed to be trusted. This means:
/// * The PE file it was loaded from must have been a completely valid EFI application of the correct architecture.
/// * If the entry point returns, it must leave the system in a state that allows our stub to continue.
/// In particular:
/// * Only memory it either has allocated, or that belongs to the image, should have been altered.
/// * Memory it has not allocated should not have been freed.
/// * Boot services must not have been exited.
pub unsafe fn start(
self,
handle: Handle,
system_table: &SystemTable<Boot>,
load_options: &[u8],
) -> Status {
let mut loaded_image = system_table
.boot_services()
.open_protocol_exclusive::<LoadedImage>(handle)
.expect("Failed to open the LoadedImage protocol");
let (our_data, our_size) = loaded_image.info();
let our_load_options = loaded_image
.load_options_as_bytes()
.map(|options| options.as_ptr_range());
// It seems to be impossible to allocate custom image handles.
// Hence, we reuse our own for the kernel.
// The shim does the same thing.
unsafe {
loaded_image.set_image(
self.image.as_ptr() as *const c_void,
self.image.len().try_into().unwrap(),
);
loaded_image.set_load_options(
load_options.as_ptr() as *const u8,
u32::try_from(load_options.len()).unwrap(),
);
}
let status = (self.entry)(handle, unsafe { system_table.unsafe_clone() });
// If the kernel has exited boot services, it must not return any more, and has full control over the entire machine.
// If the kernel entry point returned, deallocate its image, and restore our loaded image handle.
// If it calls Exit(), that call returns directly to systemd-boot. This unfortunately causes a resource leak.
system_table
.boot_services()
.free_pages(self.image.as_ptr() as u64, bytes_to_pages(self.image.len()))
.expect("Double free attempted");
unsafe {
loaded_image.set_image(our_data, our_size);
match our_load_options {
Some(options) => loaded_image.set_load_options(
options.start,
options.end.offset_from(options.start).try_into().unwrap(),
),
None => loaded_image.set_load_options(core::ptr::null(), 0),
}
}
status
}
}