From 56ff00b020c3d28f7b3222359b98e8ba116df1f8 Mon Sep 17 00:00:00 2001
From: titison
Date: Sun, 29 Oct 2023 15:04:27 +0100
Subject: [PATCH 1/3] Update to goblin 0.7.1
---
Cargo.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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"
From 74845c9132dc26481728316cb9d8ddb0515782de Mon Sep 17 00:00:00 2001
From: titison
Date: Sun, 29 Oct 2023 15:05:47 +0100
Subject: [PATCH 2/3] Add check for IntelCET features IBT & SHSTK
---
src/elf.rs | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 106 insertions(+), 4 deletions(-)
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 {
From a216207a9ae49f3b4f881c0b464614968eefdd35 Mon Sep 17 00:00:00 2001
From: titison
Date: Mon, 30 Oct 2023 17:10:19 +0100
Subject: [PATCH 3/3] Add check for CETCOMPAT in PE files
---
src/pe.rs | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 65 insertions(+), 5 deletions(-)
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
+ }
+ }
}