Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds checks for IntelCET Indirect Branch Tracking (IBT) and Shadow Stack (SHSTK) #43

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
110 changes: 106 additions & 4 deletions src/elf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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::<u32>());
*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 {
Expand Down Expand Up @@ -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**
Expand Down Expand Up @@ -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<String>,
}
Expand All @@ -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()
Expand All @@ -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,
Expand All @@ -232,15 +288,16 @@ impl fmt::Display for CheckSecResults {
self.pie,
self.relro,
self.rpath,
self.runpath
self.runpath,
self.intelcet
)
}
#[cfg(feature = "color")]
/// Colorized human readable format output
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} {} {} {} {} {} {} {} {} {} {} {:2} {} {:2} {} {} {} {} {} {} {} {} {} {}",
"{} {} {} {} {} {} {} {} {} {} {} {:2} {} {:2} {} {} {} {} {} {} {} {} {} {} {} {}",
"Canary:".bold(),
colorize_bool!(self.canary),
"CFI:".bold(),
Expand All @@ -264,7 +321,9 @@ impl fmt::Display for CheckSecResults {
"RPATH:".bold(),
self.rpath,
"RUNPATH:".bold(),
self.runpath
self.runpath,
"IntelCET:".bold(),
self.intelcet
)
}
}
Expand Down Expand Up @@ -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>;
}
Expand Down Expand Up @@ -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 {
Expand Down
70 changes: 65 additions & 5 deletions src/pe.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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)]
Expand Down Expand Up @@ -229,6 +236,35 @@ fn get_load_config_val(
}
}

trait AdditionalFunctions<'a> {
fn get_image_debug_directory(&self,bytes: &'a[u8]) -> Option<Vec<DebugData<'a>>>;
}
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<Vec<DebugData<'a>>> {
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 {
Expand Down Expand Up @@ -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]
Expand All @@ -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),
}
}
}
Expand All @@ -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,
Expand All @@ -365,7 +404,8 @@ impl fmt::Display for CheckSecResults {
self.isolation,
self.rfg,
self.safeseh,
self.seh
self.seh,
self.cetcompat
)
}
#[cfg(feature = "color")]
Expand All @@ -374,7 +414,7 @@ impl fmt::Display for CheckSecResults {
write!(
f,
"{} {} {} {} {} {} {} {} {} {} {} {} {} {} \
{} {} {} {} {} {} {} {} {} {} {} {}",
{} {} {} {} {} {} {} {} {} {} {} {} {} {}",
"ASLR:".bold(),
self.aslr,
"Authenticode:".bold(),
Expand All @@ -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)
)
}
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
}
}