diff --git a/Cargo.toml b/Cargo.toml index c569df9..a5d91aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ colored = {version = "2.0.0", optional = true} colored_json = {version = "3.0.1", optional = true} either = "1.8.1" glob = "0.3.0" -goblin = "0.6.0" +goblin = "0.7.1" iced-x86 = {version = "1.18.0", optional = true} ignore = "0.4.18" itertools = "0.10.5" diff --git a/src/elf.rs b/src/elf.rs index 831217a..69199a9 100644 --- a/src/elf.rs +++ b/src/elf.rs @@ -7,6 +7,7 @@ use goblin::elf::dynamic::{ DF_1_NOW, DF_1_PIE, DF_BIND_NOW, DT_RPATH, DT_RUNPATH, }; use goblin::elf::header::ET_DYN; +use goblin::elf::note::NT_GNU_PROPERTY_TYPE_0; use goblin::elf::program_header::{PF_X, PT_GNU_RELRO, PT_GNU_STACK}; #[cfg(feature = "disassembly")] use goblin::elf::section_header::{SHF_ALLOC, SHF_EXECINSTR, SHT_PROGBITS}; @@ -26,6 +27,20 @@ use crate::disassembly::{has_stack_clash_protection, Bitness}; use crate::ldso::{LdSoError, LdSoLookup}; use crate::shared::{Rpath, VecRpath}; +/* X86 processor-specific features */ +pub const GNU_PROPERTY_X86_FEATURE_1_AND:u32 = 0xc0000002; +/* Executable sections are compatible with IBT. */ +pub const GNU_PROPERTY_X86_FEATURE_1_IBT:u32 = (1 as u32) << 0; +/* Executable sections are compatible with SHSTK. */ +pub const GNU_PROPERTY_X86_FEATURE_1_SHSTK:u32 = (1 as u32) << 1; + + +fn read_le_u32(input: &mut &[u8]) -> u32 { + let (int_bytes, rest) = input.split_at(std::mem::size_of::()); + *input = rest; + u32::from_le_bytes(int_bytes.try_into().unwrap()) +} + /// Relocation Read-Only mode: `None`, `Partial`, or `Full` #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] pub enum Relro { @@ -134,6 +149,44 @@ impl fmt::Display for Fortify { } } +/// IntelCET Features: `None`, `SHSTK`, `IBT` or `IBTSHSTK` (IBT and SHSTK) +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum IntelCET { + None, + SHSTK, + IBT, + IBTSHSTK +} + +impl fmt::Display for IntelCET { + #[cfg(not(feature = "color"))] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{:<4}", + match *self { + Self::None => "None", + Self::SHSTK => "SHSTK", + Self::IBT => "IBT", + Self::IBTSHSTK => "IBT & SHSTK" + } + ) + } + #[cfg(feature = "color")] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{:<4}", + match *self { + Self::None => "None".red(), + Self::SHSTK => "SHSTK".yellow(), + Self::IBT => "IBT".yellow(), + Self::IBTSHSTK => "IBT & SHSTK".green() + } + ) + } +} + /// Checksec result struct for ELF32/64 binaries /// /// **Example** @@ -178,6 +231,8 @@ pub struct CheckSecResults { pub rpath: VecRpath, /// Run-time search path (`DT_RUNTIME`) pub runpath: VecRpath, + /// Intel CET Features (*CFLAGS=*`-fcf-protection=[full|branch|return|none]`) + pub intelcet: IntelCET, /// Linked dynamic libraries pub dynlibs: Vec, } @@ -204,6 +259,7 @@ impl CheckSecResults { relro: elf.has_relro(), rpath: elf.has_rpath(), runpath: elf.has_runpath(), + intelcet: elf.has_intel_cet(bytes), dynlibs: elf .libraries .iter() @@ -220,7 +276,7 @@ impl fmt::Display for CheckSecResults { write!( f, "Canary: {} CFI: {} SafeStack: {} StackClash: {} Fortify: {} Fortified: {:2} \ - Fortifiable: {:2} NX: {} PIE: {} Relro: {} RPATH: {} RUNPATH: {}", + Fortifiable: {:2} NX: {} PIE: {} Relro: {} RPATH: {} RUNPATH: {} IntelCET: {}", self.canary, self.clang_cfi, self.clang_safestack, @@ -232,7 +288,8 @@ impl fmt::Display for CheckSecResults { self.pie, self.relro, self.rpath, - self.runpath + self.runpath, + self.intelcet ) } #[cfg(feature = "color")] @@ -240,7 +297,7 @@ impl fmt::Display for CheckSecResults { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "{} {} {} {} {} {} {} {} {} {} {} {:2} {} {:2} {} {} {} {} {} {} {} {} {} {}", + "{} {} {} {} {} {} {} {} {} {} {} {:2} {} {:2} {} {} {} {} {} {} {} {} {} {} {} {}", "Canary:".bold(), colorize_bool!(self.canary), "CFI:".bold(), @@ -264,7 +321,9 @@ impl fmt::Display for CheckSecResults { "RPATH:".bold(), self.rpath, "RUNPATH:".bold(), - self.runpath + self.runpath, + "IntelCET:".bold(), + self.intelcet ) } } @@ -313,6 +372,9 @@ pub trait Properties { /// check the `.dynamic` section for `DT_RPATH` and return results in a /// `VecRpath` fn has_runpath(&self) -> VecRpath; + /// check the notes in `.note.gnu.property` for + /// GNU_PROPERTY_X86_FEATURE_1_IBT and GNU_PROPERTY_X86_FEATURE_1_SHSTK + fn has_intel_cet(&self, bytes: &[u8]) -> IntelCET; /// return the corresponding string from dynstrtab for a given `d_tag` fn get_dynstr_by_tag(&self, tag: u64) -> Option<&str>; } @@ -583,6 +645,46 @@ impl Properties for Elf<'_> { } VecRpath::new(vec![Rpath::None]) } + fn has_intel_cet(&self, bytes: &[u8]) -> IntelCET { + let Some(note_iterator) = self.iter_note_sections(bytes, Some(".note.gnu.property")) else {return IntelCET::None;}; + for note_result in note_iterator { + match note_result { + Ok(note) => { + if note.n_type == NT_GNU_PROPERTY_TYPE_0 { + let mut desc_buffer = note.desc; + let size = if self.is_64 {8} else {4}; + while desc_buffer.len()>=8 { + let property_type: u32 = read_le_u32(&mut desc_buffer); + let property_datasz: u32 = read_le_u32(&mut desc_buffer); + if property_type == GNU_PROPERTY_X86_FEATURE_1_AND { + let property_bitmask: u32 = read_le_u32(&mut desc_buffer); + if (property_bitmask & GNU_PROPERTY_X86_FEATURE_1_IBT) != 0 && (property_bitmask & GNU_PROPERTY_X86_FEATURE_1_SHSTK)!= 0 { + return IntelCET::IBTSHSTK; + } + if (property_bitmask & GNU_PROPERTY_X86_FEATURE_1_IBT) != 0 { + return IntelCET::IBT; + } + if (property_bitmask & GNU_PROPERTY_X86_FEATURE_1_SHSTK)!= 0 { + return IntelCET::SHSTK; + } + // Align word size manually here + if size == 8 { + read_le_u32(&mut desc_buffer); + } + } else { + // Align word size + // skip_bytes > datasz with skip_bytes = x*size + let skip_bytes:u32 = (property_datasz + (size-1)) & !(size-1); + (_, desc_buffer) = desc_buffer.split_at(skip_bytes as usize); + } + } + } + } + Err(_) => {} + } + } + return IntelCET::None + } fn get_dynstr_by_tag(&self, tag: u64) -> Option<&str> { if let Some(dynamic) = &self.dynamic { for dynamic in &dynamic.dyns { diff --git a/src/pe.rs b/src/pe.rs index 9063f7e..56d2741 100644 --- a/src/pe.rs +++ b/src/pe.rs @@ -1,8 +1,9 @@ //! Implements checksec for PE32/32+ binaries #[cfg(feature = "color")] use colored::Colorize; +use goblin::pe::debug::DebugData; use goblin::pe::utils::find_offset; -use goblin::pe::PE; +use goblin::pe::{PE, debug}; use goblin::pe::{ data_directories::DataDirectory, options::ParseOptions, section_table::SectionTable, @@ -28,6 +29,12 @@ const IMAGE_GUARD_RF_INSTRUMENTED: u32 = 0x0002_0000; const IMAGE_GUARD_RF_ENABLE: u32 = 0x0004_0000; const IMAGE_GUARD_RF_STRICT: u32 = 0x0008_0000; +const SIZE_OF_IMAGE_DEBUG_DIR_ENTRY: u32 = 28; +const IMAGE_DEBUG_TYPE_EX_DLLCHARACTERISTICS: u32 = 20; +// stored in `IMAGE_DEBUG_TYPE_EX_DLLCHARACTERISTICS` +const IMAGE_DLLCHARACTERISTICS_EX_CET_COMPAT: u32 = 0x01; +const IMAGE_DLLCHARACTERISTICS_EX_CET_COMPAT_STRICT_MODE :u32 = 0x02; + /// `IMAGE_LOAD_CONFIG_CODE_INTEGRITY` #[repr(C)] #[derive(Debug, Copy, Clone, Default, Pread)] @@ -229,6 +236,35 @@ fn get_load_config_val( } } +trait AdditionalFunctions<'a> { + fn get_image_debug_directory(&self,bytes: &'a[u8]) -> Option>>; +} +impl<'a> AdditionalFunctions<'a> for PE<'a> { + /// Workaround function for https://github.com/m4b/goblin/issues/314 + /// Goblin only provides the first entry from the ImageDebugDirectory + /// instead of all entries + /// + /// Returns a vector containing all DebugData from the PE File + fn get_image_debug_directory(&self,bytes: &'a[u8]) -> Option>> { + let mut image_debug_dir = Vec::new(); + + let Some(optional_header) = self.header.optional_header else {return None}; + + let file_alignment = optional_header.windows_fields.file_alignment; + let Some(mut dd) = optional_header.data_directories.get_debug_table() else {return None}; + for _ in 0..dd.size/SIZE_OF_IMAGE_DEBUG_DIR_ENTRY { + // this parse should return a vector but doesnt. + // instead it only returns the first entry + let debug_data_entry_result = debug::DebugData::parse(bytes, dd, &self.sections, file_alignment); + let Ok(debug_data_entry) = debug_data_entry_result else {return None}; + image_debug_dir.push(debug_data_entry); + // shift the address to receive the next entry when calling parse + dd.virtual_address += SIZE_OF_IMAGE_DEBUG_DIR_ENTRY; + } + return Some(image_debug_dir) + } +} + /// Address Space Layout Randomization: `None`, `DYNBASE`, or `HIGHENTROPYVA` #[derive(Clone, Deserialize, Serialize, Debug)] pub enum ASLR { @@ -323,6 +359,8 @@ pub struct CheckSecResults { pub safeseh: bool, /// Structured Exception Handler pub seh: bool, + /// IntelCET Shadow Stack (`/CETCOMPAT`) + pub cetcompat: bool, } impl CheckSecResults { #[must_use] @@ -341,6 +379,7 @@ impl CheckSecResults { rfg: pe.has_rfg(buffer), safeseh: pe.has_safe_seh(buffer), seh: pe.has_seh(), + cetcompat: pe.has_cetcompat(buffer), } } } @@ -352,7 +391,7 @@ impl fmt::Display for CheckSecResults { f, "ASLR: {} Authenticode: {} CFG: {} CLR: {} DEP: {} \ Dynamic Base: {} Force Integrity: {} GS: {} \ - High Entropy VA: {} Isolation: {} RFG: {} SafeSEH: {} SEH: {}", + High Entropy VA: {} Isolation: {} RFG: {} SafeSEH: {} SEH: {} CETCOMPAT: {}", self.aslr, self.authenticode, self.cfg, @@ -365,7 +404,8 @@ impl fmt::Display for CheckSecResults { self.isolation, self.rfg, self.safeseh, - self.seh + self.seh, + self.cetcompat ) } #[cfg(feature = "color")] @@ -374,7 +414,7 @@ impl fmt::Display for CheckSecResults { write!( f, "{} {} {} {} {} {} {} {} {} {} {} {} {} {} \ - {} {} {} {} {} {} {} {} {} {} {} {}", + {} {} {} {} {} {} {} {} {} {} {} {} {} {}", "ASLR:".bold(), self.aslr, "Authenticode:".bold(), @@ -400,7 +440,9 @@ impl fmt::Display for CheckSecResults { "SafeSEH:".bold(), colorize_bool!(self.safeseh), "SEH:".bold(), - colorize_bool!(self.seh) + colorize_bool!(self.seh), + "CETCOMPAT:".bold(), + colorize_bool!(self.cetcompat) ) } } @@ -497,6 +539,9 @@ pub trait Properties { /// check `IMAGE_DLLCHARACTERISTICS_NO_SEH` from the /// `IMAGE_OPTIONAL_HEADER32/64` fn has_seh(&self) -> bool; + /// check `IMAGE_DLLCHARACTERISTICS_EX_CET_COMPAT` from the debug_data + /// `IMAGE_DEBUG_TYPE_EX_DLLCHARACTERISTICS` + fn has_cetcompat(&self, bytes: &[u8]) -> bool; } impl Properties for PE<'_> { fn has_aslr(&self) -> ASLR { @@ -689,4 +734,19 @@ impl Properties for PE<'_> { _ => false, } } + fn has_cetcompat(&self, bytes: &[u8]) -> bool { + match self.get_image_debug_directory(bytes) { + Some(dd_vec) => { + for dd in dd_vec { + if dd.image_debug_directory.data_type == IMAGE_DEBUG_TYPE_EX_DLLCHARACTERISTICS { + let i = dd.image_debug_directory.pointer_to_raw_data as usize; + let properties = u32::from_le_bytes(bytes[i..i+4].try_into().unwrap()); + return (properties & IMAGE_DLLCHARACTERISTICS_EX_CET_COMPAT) != 0 || (properties & IMAGE_DLLCHARACTERISTICS_EX_CET_COMPAT_STRICT_MODE) != 0; + } + } + return false + } + None => return false + } + } }