Merge pull request #75 from alois31/unsigned-kernel
Prevent loading of untrusted initrds
This commit is contained in:
commit
eb3b4703fd
|
@ -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,
|
||||||
|
@ -23,7 +25,7 @@ use uefi::{
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
linux_loader::InitrdLoader,
|
linux_loader::InitrdLoader,
|
||||||
uefi_helpers::{booted_image_cmdline, booted_image_file, read_all},
|
uefi_helpers::{booted_image_file, read_all},
|
||||||
};
|
};
|
||||||
|
|
||||||
type Hash = sha2::digest::Output<Sha256>;
|
type Hash = sha2::digest::Output<Sha256>;
|
||||||
|
@ -65,13 +67,16 @@ struct EmbeddedConfiguration {
|
||||||
/// The cryptographic hash of the initrd. This hash is computed
|
/// The cryptographic hash of the initrd. This hash is computed
|
||||||
/// over the whole PE binary, not only the embedded initrd.
|
/// over the whole PE binary, not only the embedded initrd.
|
||||||
initrd_hash: Hash,
|
initrd_hash: Hash,
|
||||||
|
|
||||||
|
/// The kernel command-line.
|
||||||
|
cmdline: CString16,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract a filename from a PE section. The filename is stored as UTF-8.
|
/// Extract a string, stored as UTF-8, from a PE section.
|
||||||
fn extract_filename(file_data: &[u8], section: &str) -> Result<CString16> {
|
fn extract_string(file_data: &[u8], section: &str) -> Result<CString16> {
|
||||||
let filename = pe_section_as_string(file_data, section).ok_or(Status::INVALID_PARAMETER)?;
|
let string = pe_section_as_string(file_data, section).ok_or(Status::INVALID_PARAMETER)?;
|
||||||
|
|
||||||
Ok(CString16::try_from(filename.as_str()).map_err(|_| Status::INVALID_PARAMETER)?)
|
Ok(CString16::try_from(string.as_str()).map_err(|_| Status::INVALID_PARAMETER)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract a Blake3 hash from a PE section.
|
/// Extract a Blake3 hash from a PE section.
|
||||||
|
@ -90,11 +95,13 @@ impl EmbeddedConfiguration {
|
||||||
let file_data = read_all(file)?;
|
let file_data = read_all(file)?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
kernel_filename: extract_filename(&file_data, ".kernelp")?,
|
kernel_filename: extract_string(&file_data, ".kernelp")?,
|
||||||
kernel_hash: extract_hash(&file_data, ".kernelh")?,
|
kernel_hash: extract_hash(&file_data, ".kernelh")?,
|
||||||
|
|
||||||
initrd_filename: extract_filename(&file_data, ".initrdp")?,
|
initrd_filename: extract_string(&file_data, ".initrdp")?,
|
||||||
initrd_hash: extract_hash(&file_data, ".initrdh")?,
|
initrd_hash: extract_hash(&file_data, ".initrdh")?,
|
||||||
|
|
||||||
|
cmdline: extract_string(&file_data, ".cmdline")?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -162,40 +169,13 @@ fn main(handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
|
||||||
return Status::SECURITY_VIOLATION;
|
return Status::SECURITY_VIOLATION;
|
||||||
}
|
}
|
||||||
|
|
||||||
let kernel_cmdline =
|
let kernel =
|
||||||
booted_image_cmdline(system_table.boot_services()).expect("Failed to fetch command line");
|
Image::load(system_table.boot_services(), &kernel_data).expect("Failed to load the kernel");
|
||||||
|
|
||||||
let kernel_handle = {
|
|
||||||
system_table
|
|
||||||
.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, &config.cmdline) };
|
||||||
.start_image(kernel_handle)
|
|
||||||
.status();
|
|
||||||
|
|
||||||
initrd_loader
|
initrd_loader
|
||||||
.uninstall(system_table.boot_services())
|
.uninstall(system_table.boot_services())
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
CStr16, 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: &CStr16,
|
||||||
|
) -> 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.num_bytes()).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
|
||||||
|
}
|
||||||
|
}
|
|
@ -56,15 +56,3 @@ pub fn booted_image_file(boot_services: &BootServices) -> Result<RegularFile> {
|
||||||
.into_regular_file()
|
.into_regular_file()
|
||||||
.ok_or(Status::INVALID_PARAMETER)?)
|
.ok_or(Status::INVALID_PARAMETER)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the command line of the currently executing image.
|
|
||||||
pub fn booted_image_cmdline(boot_services: &BootServices) -> Result<Vec<u8>> {
|
|
||||||
let loaded_image =
|
|
||||||
boot_services.open_protocol_exclusive::<LoadedImage>(boot_services.image_handle())?;
|
|
||||||
|
|
||||||
Ok(loaded_image
|
|
||||||
// If this fails, we have no load options and we return an empty string.
|
|
||||||
.load_options_as_bytes()
|
|
||||||
.map(|b| b.to_vec())
|
|
||||||
.unwrap_or_default())
|
|
||||||
}
|
|
||||||
|
|
|
@ -157,8 +157,8 @@ impl Installer {
|
||||||
append_initrd_secrets(initrd_secrets_script, &initrd_location)?;
|
append_initrd_secrets(initrd_secrets_script, &initrd_location)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The initrd doesn't need to be signed. The stub has its hash embedded and will refuse
|
// The initrd and kernel don't need to be signed.
|
||||||
// loading it when the hash mismatches.
|
// The stub has their hashes embedded and will refuse loading on hash mismatches.
|
||||||
//
|
//
|
||||||
// The initrd and kernel are not forcibly installed because they are not built
|
// The initrd and kernel are not forcibly installed because they are not built
|
||||||
// reproducibly. Forcibly installing (i.e. overwriting) them is likely to break older
|
// reproducibly. Forcibly installing (i.e. overwriting) them is likely to break older
|
||||||
|
@ -166,7 +166,9 @@ impl Installer {
|
||||||
// will not match anymore.
|
// will not match anymore.
|
||||||
install(&initrd_location, &esp_gen_paths.initrd)
|
install(&initrd_location, &esp_gen_paths.initrd)
|
||||||
.context("Failed to install initrd to ESP")?;
|
.context("Failed to install initrd to ESP")?;
|
||||||
install_signed(&self.key_pair, &bootspec.kernel, &esp_gen_paths.kernel)
|
// Do not sign the kernel.
|
||||||
|
// Boot loader specification could be used to make a signed kernel load an unprotected initrd.
|
||||||
|
install(&bootspec.kernel, &esp_gen_paths.kernel)
|
||||||
.context("Failed to install kernel to ESP.")?;
|
.context("Failed to install kernel to ESP.")?;
|
||||||
|
|
||||||
let lanzaboote_image = pe::lanzaboote_image(
|
let lanzaboote_image = pe::lanzaboote_image(
|
||||||
|
|
Loading…
Reference in New Issue