From 4ebbfdb455f4fb9481f7c494b2bbecad3e35b20a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Fri, 14 Apr 2023 09:27:57 -0700 Subject: [PATCH] Add logic for reading KASLR offset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In order to (eventually...) support normalization of kernel addresses, we need to take into account whether the running kernel has address space layout randomization enabled. If that is the case, we will need to incorporate the randomization offset in the normalization process. This change introduces the necessary logic for reading said offset, so that it can be used down the line. This change builds on all the infrastructure we added for making the ELF parser optionally work with using regular I/O APIs instead of relying on memory mapping. Refs: #950 Signed-off-by: Daniel Müller --- src/elf/mod.rs | 2 + src/elf/parser.rs | 21 +++- src/elf/types.rs | 35 +++++- src/normalize/kernel.rs | 238 ++++++++++++++++++++++++++++++++++++++++ src/normalize/mod.rs | 4 + src/util.rs | 2 + 6 files changed, 299 insertions(+), 3 deletions(-) create mode 100644 src/normalize/kernel.rs diff --git a/src/elf/mod.rs b/src/elf/mod.rs index c16e443f..d82d464e 100644 --- a/src/elf/mod.rs +++ b/src/elf/mod.rs @@ -11,6 +11,8 @@ pub(crate) mod types; // of concerns that is not a workable location. pub(crate) static DEFAULT_DEBUG_DIRS: &[&str] = &["/usr/lib/debug", "/lib/debug/"]; +#[cfg(test)] +pub(crate) use parser::BackendImpl; pub(crate) use parser::ElfParser; pub(crate) use resolver::ElfResolverData; diff --git a/src/elf/parser.rs b/src/elf/parser.rs index cb9d6a84..e1f4752a 100644 --- a/src/elf/parser.rs +++ b/src/elf/parser.rs @@ -887,9 +887,9 @@ where _backend: B::ObjTy, } +#[cfg(test)] impl ElfParser { - #[cfg(test)] - pub(crate) fn open_file_io

(file: File, path: P) -> Self + fn open_file_io

(file: File, path: P) -> Self where P: Into, { @@ -903,6 +903,23 @@ impl ElfParser { }; parser } + + /// Create an `ElfParser` from an open file. + pub(crate) fn open_non_mmap

(path: P) -> Result + where + P: Into, + { + let path = path.into(); + let file = + File::open(&path).with_context(|| format!("failed to open `{}`", path.display()))?; + let slf = Self::open_file_io(file, path); + Ok(slf) + } + + /// Retrieve a reference to the backend in use. + pub(crate) fn backend(&self) -> &File { + &self._backend + } } impl ElfParser { diff --git a/src/elf/types.rs b/src/elf/types.rs index 90c5c866..8021149a 100644 --- a/src/elf/types.rs +++ b/src/elf/types.rs @@ -216,7 +216,8 @@ impl ElfN_Ehdr<'_> { } -pub(crate) const PT_LOAD: u32 = 1; +pub(crate) const PT_LOAD: u32 = 1; /* Loadable program segment */ +pub(crate) const PT_NOTE: u32 = 4; /* Auxiliary information */ #[derive(Copy, Clone, Debug, Default)] @@ -274,6 +275,32 @@ impl Has32BitTy for Elf64_Phdr { pub(crate) type ElfN_Phdr<'elf> = ElfN<'elf, Elf64_Phdr>; pub(crate) type ElfN_Phdrs<'elf> = ElfNSlice<'elf, Elf64_Phdr>; +impl ElfN_Phdr<'_> { + #[inline] + pub fn type_(&self) -> Elf64_Word { + match self { + ElfN::B32(phdr) => phdr.p_type, + ElfN::B64(phdr) => phdr.p_type, + } + } + + #[inline] + pub fn offset(&self) -> Elf64_Off { + match self { + ElfN::B32(phdr) => phdr.p_offset.into(), + ElfN::B64(phdr) => phdr.p_offset, + } + } + + #[inline] + pub fn file_size(&self) -> Elf64_Xword { + match self { + ElfN::B32(phdr) => phdr.p_filesz.into(), + ElfN::B64(phdr) => phdr.p_filesz, + } + } +} + pub(crate) const PF_X: Elf64_Word = 1; @@ -703,6 +730,12 @@ mod tests { let _val = shdr.addr(); let _val = shdr.link(); + let phdr32 = Elf32_Phdr::default(); + let phdr = ElfN_Phdr::B32(Cow::Borrowed(&phdr32)); + let _val = phdr.type_(); + let _val = phdr.offset(); + let _val = phdr.file_size(); + let sym32 = Elf32_Sym::default(); let sym = ElfN_Sym::B32(Cow::Borrowed(&sym32)); let _val = sym.value(); diff --git a/src/normalize/kernel.rs b/src/normalize/kernel.rs new file mode 100644 index 00000000..e49766f2 --- /dev/null +++ b/src/normalize/kernel.rs @@ -0,0 +1,238 @@ +use std::error::Error as StdError; +use std::fs::File; +use std::io; +use std::io::Read as _; +use std::path::Path; +use std::str; +use std::str::FromStr; + +use crate::elf; +use crate::elf::types::Elf64_Nhdr; +use crate::elf::BackendImpl; +use crate::elf::ElfParser; +use crate::util::align_up_u32; +use crate::util::from_radix_16; +use crate::util::split_bytes; +use crate::Addr; +use crate::Error; +use crate::ErrorExt as _; +use crate::IntoError as _; +use crate::Result; + +use super::normalizer::Output; + + +/// The absolute path of the `randomize_va_space` `proc` node. +const PROC_RANDOMIZE_VA_SPACE: &str = "/proc/sys/kernel/randomize_va_space"; +/// The absolute path to the `kcore` `proc` node. +const PROC_KCORE: &str = "/proc/kcore"; +/// The name of the `VMCOREINFO` ELF note. +/// +/// See https://www.kernel.org/doc/html/latest/admin-guide/kdump/vmcoreinfo.html +const VMCOREINFO_NAME: &[u8] = b"VMCOREINFO\0"; + + +/// The kernel address space layout randomization (KASLR) state of the +/// system. +#[derive(Debug)] +enum KaslrState { + /// KASLR is known to be disabled. + Disabled, + /// KASLR is known to be enabled. + Enabled, + /// The state of KASLR on the system could not be determined. + Unknown, +} + +impl FromStr for KaslrState { + type Err = Error; + + fn from_str(s: &str) -> Result { + let value = usize::from_str(s.trim()).map_err(Error::with_invalid_data)?; + match value { + 0 => Ok(KaslrState::Disabled), + 1 | 2 => Ok(KaslrState::Enabled), + // It's unclear whether we should error out here or map anything + // "unknown" to `Unknown`. + x => Err(Error::with_invalid_data(format!( + "{PROC_RANDOMIZE_VA_SPACE} node value {x} is not understood" + ))), + } + } +} + + +/// # Notes +/// Right now this function imposes an arbitrary limit on the maximum +/// node value content size. +fn read_proc_node_value(path: &Path) -> Result> +where + T: FromStr, + T::Err: StdError + Send + Sync + 'static, +{ + let result = File::open(path); + let mut file = match result { + Ok(file) => file, + Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None), + Err(err) => return Err(err.into()), + }; + + // We don't want to blindly use Read::read_to_end or something like + // that if we can avoid it. + let mut buffer = [0; u8::MAX as usize]; + let count = file.read(&mut buffer)?; + if count >= size_of_val(&buffer) { + return Err(Error::with_invalid_data(format!( + "file content is larger than {} bytes", + size_of_val(&buffer) + ))) + } + + let s = str::from_utf8(&buffer[0..count]).map_err(Error::with_invalid_data)?; + let value = T::from_str(s).map_err(Error::with_invalid_data)?; + Ok(Some(value)) +} + + +/// Try to determine the KASLR state of the system. +fn determine_kaslr_state() -> Result { + // https://www.kernel.org/doc/html/latest/admin-guide/sysctl/kernel.html#randomize-va-space + let kaslr = read_proc_node_value::(Path::new(PROC_RANDOMIZE_VA_SPACE)) + .with_context(|| { + format!( + "failed to determine KASLR state from {}", + PROC_RANDOMIZE_VA_SPACE + ) + })? + .unwrap_or(KaslrState::Unknown); + Ok(kaslr) +} + +/// "Parse" the VMCOREINFO descriptor. +/// +/// This underspecified blob roughly has the following format: +/// ``` +/// OSRELEASE=6.2.15-100.fc36.x86_64 +/// BUILD-ID=d3d01c80278f8927486b7f01d0ab6be77784dceb +/// PAGESIZE=4096 +/// SYMBOL(init_uts_ns)=ffffffffb72b8160 +/// OFFSET(uts_namespace.name)=0 +/// [...] +/// ``` +fn parse_vmcoreinfo_desc(desc: &[u8]) -> impl Iterator { + desc.split(|&b| b == b'\n') + .filter_map(|line| split_bytes(line, |b| b == b'=')) +} + +/// Find and read the `KERNELOFFSET` note in a "kcore" file represented by +/// `parser` (i.e., already opened as an ELF). +fn find_kaslr_offset(parser: &ElfParser) -> Result> { + let phdrs = parser.program_headers()?; + for phdr in phdrs.iter(0) { + if phdr.type_() != elf::types::PT_NOTE { + continue + } + + let file = parser.backend(); + let mut offset = phdr.offset(); + + // Iterate through all available notes. See `elf(5)` for + // details. + while offset + (size_of::() as u64) <= phdr.file_size() { + let nhdr = file + .read_pod_obj::(offset) + .context("failed to read kcore note header")?; + offset += size_of::() as u64; + + let name = if nhdr.n_namesz > 0 { + let name = file.read_pod_slice::(offset, nhdr.n_namesz as _)?; + offset += u64::from(align_up_u32(nhdr.n_namesz, 4)); + Some(name) + } else { + None + }; + + // We are looking for the note named `VMCOREINFO`. + if name.as_deref() == Some(VMCOREINFO_NAME) { + if nhdr.n_descsz > 0 { + let desc = file.read_pod_slice::(offset, nhdr.n_descsz as _)?; + let offset = parse_vmcoreinfo_desc(&desc) + .find(|(key, _value)| key == b"KERNELOFFSET") + // The value is in hexadecimal format. Go figure. + .map(|(_key, value)| { + from_radix_16(value).ok_or_invalid_data(|| { + format!("failed to parse KERNELOFFSET value `{value:x?}`") + }) + }) + .transpose(); + return offset + } + + // There shouldn't be multiple notes with that name, + // but I suppose it can't hurt to keep checking...? + } + + offset += u64::from(align_up_u32(nhdr.n_descsz, 4)); + } + } + Ok(None) +} + + +#[cfg(test)] +mod tests { + use super::*; + + use test_log::test; + + use crate::ErrorKind; + + + /// Check that we can parse a dummy VMCOREINFO descriptor. + #[test] + fn vmcoreinfo_desc_parsing() { + let desc = b"OSRELEASE=6.2.15-100.fc36.x86_64 +BUILD-ID=d3d01c80278f8927486b7f01d0ab6be77784dceb +SYMBOL(init_uts_ns)=ffffffffb72b8160 +OFFSET(uts_namespace.name)=0 +PAGESIZE=4096 +"; + + let page_size = parse_vmcoreinfo_desc(desc) + .find(|(key, _value)| key == b"PAGESIZE") + .map(|(_key, value)| value) + .unwrap(); + assert_eq!(page_size, b"4096"); + } + + /// Check that we can determine the system's KASLR state. + #[test] + fn kaslr_detection() { + let state = determine_kaslr_state().unwrap(); + + // Always attempt reading the KASLR to exercise the VMCOREINFO + // parsing path. + // Note that we cannot use the regular mmap based ELF parser + // backend for this file, as it cannot be mmap'ed. We have to + // fall back to using regular I/O instead. + let parser = match ElfParser::open_non_mmap(PROC_KCORE) { + Ok(parser) => parser, + Err(err) if err.kind() == ErrorKind::NotFound => return, + Err(err) => panic!("{err}"), + }; + let offset = find_kaslr_offset(&parser).unwrap(); + + match state { + KaslrState::Enabled => assert_ne!(offset, None), + KaslrState::Disabled => { + assert!( + offset.is_none() || matches!(offset, Some(0)), + "{offset:#x?}" + ); + } + KaslrState::Unknown => { + // Anything is game. + } + } + } +} diff --git a/src/normalize/mod.rs b/src/normalize/mod.rs index 9838ebf9..5dde3000 100644 --- a/src/normalize/mod.rs +++ b/src/normalize/mod.rs @@ -44,6 +44,10 @@ pub(crate) mod buildid; pub(crate) mod ioctl; +// Still work in progress. +#[allow(unused)] +#[cfg(test)] +mod kernel; mod meta; mod normalizer; mod user; diff --git a/src/util.rs b/src/util.rs index fb1017ce..a3a7dde6 100644 --- a/src/util.rs +++ b/src/util.rs @@ -96,6 +96,8 @@ macro_rules! def_align_up { }; } +#[cfg(test)] +def_align_up!(align_up_u32, u32); def_align_up!(align_up_usize, usize);