Skip to content

Commit

Permalink
lots of refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
ex0dus-0x committed May 27, 2024
1 parent 926e25b commit 5290959
Show file tree
Hide file tree
Showing 10 changed files with 3,427 additions and 508 deletions.
2,939 changes: 2,792 additions & 147 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ goblin = "0.8.1"
byte-unit = "4.0.10"
chrono = "0.4"
thiserror = "1.0.59"
regex = "1.10.4"

serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }

yara-x = { version = "0.3.0", optional = true }

[features]
advanced_scan = ["dep:yara-x"]
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright 2020 Alan Cao <alan [at] codemuch.tech>
Copyright 2024 <ex0dus [at] codemuch.tech>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

Expand Down
297 changes: 209 additions & 88 deletions src/check/elf.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
//! ### ELF-Specific Compilation Checks:
//!
//! * Binary Type
//! * Static Compilation
//! * Stripped Executable
//! * Compiler Runtime
//! * Static compilation
//! * Linker Path
//! * Minimum glibc Version
//!
//! ### Exploit Mitigations:
//! ### ELF-Specific Exploit Mitigations:
//!
//! * NX (Non-eXecutable bit) stack
//! * Stack Canaries
Expand All @@ -17,44 +14,123 @@
use goblin::elf::dynamic::{tag_to_str, Dyn};
use goblin::elf::{header, program_header, Elf};
use regex::Regex;
use serde_json::json;
use std::collections::HashSet;
use std::fmt::{self, Display};

use crate::check::{Analyze, GenericMap};
use crate::BinResult;

use super::UniversalCompilationProperties;

const GLIBC: &str = "GLIBC_2.";

impl Analyze for Elf<'_> {
fn compilation(&self, bytes: &[u8]) -> BinResult<GenericMap> {
let mut comp_map: GenericMap = GenericMap::new();
enum LinuxCompiler {
Gcc(Option<String>),
Clang(Option<String>),
Unknown,
}

// supported: shared object (pie exec or .so) or executable
comp_map.insert(
"Binary Type".to_string(),
json!(header::et_to_str(self.header.e_type)),
);
impl Display for LinuxCompiler {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Gcc(Some(ver)) => write!(f, "GCC version {}", ver),
Self::Gcc(None) => write!(f, "GCC version <unknown>"),
Self::Clang(Some(ver)) => write!(f, "Clang/LLVM version {}", ver),
Self::Clang(None) => write!(f, "Clang/LLVM version <unknown>"),
_ => write!(f, "<unknown>"),
}
}
}

impl LinuxCompiler {
/// Given data from `.comment`, deduce a compiler + version triplet.
/// TODO(alan): need more binary artifacts to test and make this better.
fn parse(compiler_string: &str) -> LinuxCompiler {
// parse for unique version triplets in the string
let ver_triplet_re = Regex::new(r"\b\d+\.\d+(\.\d+)?\b").unwrap();
let ver_set: Vec<&str> = ver_triplet_re
.find_iter(compiler_string)
.map(|mat| mat.as_str())
.collect();

let unique_vers: HashSet<&str> = ver_set.into_iter().collect();
let versions: Vec<&str> = unique_vers.into_iter().collect();
let min_ver = versions.first().map(|s| s.to_string());

// pattern match for compilers
for test in self.section_headers.clone().into_iter() {
let _symbol = self.shdr_strtab.get_at(test.sh_name);
if let Some(sym) = _symbol {}
// parse for the actual compiler
if compiler_string.contains("clang") {
LinuxCompiler::Clang(min_ver)
} else if compiler_string.contains("GCC:") {
LinuxCompiler::Gcc(min_ver)
} else {
LinuxCompiler::Unknown
}
//let runtime = self.detect_compiler_runtime(rules::ELF_COMPILER_RULES, bytes)?;
//comp_map.insert("Compiler Runtime".to_string(), json!(runtime));
}
}

// static executable: check if PT_INTERP segment exists
let static_exec: bool = !self
.program_headers
.iter()
.any(|ph| program_header::pt_to_str(ph.p_type) == "PT_INTERP");
comp_map.insert("Statically Compiled".to_string(), json!(static_exec));
#[derive(serde::Deserialize, serde::Serialize, Debug)]
pub enum Relro {
Partial,
Full,
None,
}

// path to linker if dynamic linking enabled
if let Some(linker) = self.interpreter {
comp_map.insert("Linker Path".to_string(), json!(linker));
impl UniversalCompilationProperties for Elf<'_> {
// common: shared object (pie exec or .so) or non-pie executable
fn binary_type(&self) -> &str {
header::et_to_str(self.header.e_type)
}

fn is_stripped(&self) -> bool {
self.syms.is_empty()
}

fn compiler_runtime(&self, bytes: &[u8]) -> Option<String> {
// most simple: `.comment` section annotating the compiler version
let mut compilation_string: Option<&str> = None;
for section in self.section_headers.clone().into_iter() {
let _symbol = self.shdr_strtab.get_at(section.sh_name);
if let Some(sym) = _symbol {
if sym == ".comment" {
let comment_section = &bytes[section.sh_offset as usize
..(section.sh_offset + section.sh_size) as usize];
if let Ok(comment_str) = std::str::from_utf8(comment_section) {
compilation_string = Some(comment_str);
}
}
}
}

if let Some(comp_string) = compilation_string {
let comp_value = LinuxCompiler::parse(comp_string);
return Some(comp_value.to_string());
}
None
}
}

trait ElfCompilationProperties {
fn is_statically_compiled(&self) -> bool;
fn linker_path(&self) -> Option<&str>;
fn libc(&self) -> f64;
}

impl ElfCompilationProperties for Elf<'_> {
// elf static executable: check if PT_INTERP segment exists
fn is_statically_compiled(&self) -> bool {
self.program_headers
.iter()
.any(|ph| program_header::pt_to_str(ph.p_type) == "PT_INTERP")
}

fn linker_path(&self) -> Option<&str> {
self.interpreter
}

// parse minimum glibc version needed
// TODO(alan): match on other stdlib runtimes
fn libc(&self) -> f64 {
let mut glibcs: Vec<f64> = vec![];
for sym in self.dynstrtab.to_vec().unwrap() {
if sym.starts_with(GLIBC) {
Expand All @@ -63,75 +139,120 @@ impl Analyze for Elf<'_> {
glibcs.push(version);
}
}
let min_ver = glibcs.iter().fold(f64::INFINITY, |a, &b| a.min(b));
comp_map.insert(
"Minimum Libc Version".to_string(),
json!(format!("2.{:?}", min_ver)),
);
comp_map.insert(
"Stripped Executable".to_string(),
json!(self.syms.is_empty()),
);
Ok(comp_map)
if !glibcs.is_empty() {
glibcs.iter().fold(f64::INFINITY, |a, &b| a.min(b))
} else {
0.0
}
}
}

fn mitigations(&self) -> GenericMap {
let mut mitigate_map: GenericMap = GenericMap::new();
pub trait ElfMitigations {
fn executable_stack(&self) -> bool;
fn stack_canary(&self) -> bool;
fn fortify_source(&self) -> bool;
fn position_independent(&self) -> bool;
fn relro(&self) -> Relro;
}

// features we are checking for
let mut nx_bit: bool = false;
let mut relro: String = "NONE".to_string();
let mut stack_canary: bool = false;
let mut fortify_source: bool = false;

// iterate over program headers for exploit mitigation fingerprints
for ph in self.program_headers.iter() {
// check for NX bit
if program_header::pt_to_str(ph.p_type) == "PT_GNU_STACK" && ph.p_flags == 6 {
nx_bit = true;
}
impl ElfMitigations for Elf<'_> {
fn executable_stack(&self) -> bool {
self.program_headers
.iter()
.any(|ph| program_header::pt_to_str(ph.p_type) == "PT_GNU_STACK" && ph.p_flags == 6)
}

// check for RELRO
if program_header::pt_to_str(ph.p_type) == "PT_GNU_RELRO" {
// check for full/partial RELRO support by checking dynamic section for DT_BIND_NOW flag.
// DT_BIND_NOW takes precedence over lazy binding and processes relocations before actual execution.
if let Some(segs) = &self.dynamic {
let dyn_seg: Option<Dyn> = segs
.dyns
.iter()
.find(|tag| tag_to_str(tag.d_tag) == "DT_BIND_NOW")
.cloned();

if dyn_seg.is_some() {
relro = "FULL".to_string();
} else {
relro = "PARTIAL".to_string();
}
}
fn stack_canary(&self) -> bool {
self.syms
.iter()
.filter_map(|sym| self.strtab.get_at(sym.st_name))
.any(|symstr| symstr == "__stack_chk_fail" || symstr == "__stack_chk_guard")
}

fn fortify_source(&self) -> bool {
// TODO: list fortified symbols
self.syms
.iter()
.filter_map(|sym| self.strtab.get_at(sym.st_name))
.any(|symstr| symstr.ends_with("_chk"))
}

fn position_independent(&self) -> bool {
matches!(self.header.e_type, 3)
}

fn relro(&self) -> Relro {
if !self
.program_headers
.iter()
.any(|ph| program_header::pt_to_str(ph.p_type) == "PT_GNU_RELRO")
{
return Relro::None;
}

// check for full/partial RELRO support by checking dynamic section for DT_BIND_NOW flag.
// DT_BIND_NOW takes precedence over lazy binding and processes relocations before actual execution.
if let Some(segs) = &self.dynamic {
let dyn_seg: Option<Dyn> = segs
.dyns
.iter()
.find(|tag| tag_to_str(tag.d_tag) == "DT_BIND_NOW")
.cloned();

if dyn_seg.is_some() {
return Relro::Full;
} else {
return Relro::Partial;
}
}
mitigate_map.insert("Executable Stack (NX Bit)".to_string(), json!(nx_bit));
mitigate_map.insert("Read-Only Relocatable (RELRO)".to_string(), json!(relro));
mitigate_map.insert(
"Position Independent Executable (PIE)".to_string(),
json!(matches!(self.header.e_type, 3)),
Relro::None
}
}

impl Analyze for Elf<'_> {
fn compilation(&self, bytes: &[u8]) -> BinResult<GenericMap> {
let mut comp_map: GenericMap = GenericMap::new();
comp_map.insert("Binary Type".to_string(), json!(self.binary_type()));
comp_map.insert("Stripped Executable".to_string(), json!(self.is_stripped()));
comp_map.insert(
"Statically Compiled".to_string(),
json!(self.is_statically_compiled()),
);

// find symbols for stack canary and FORTIFY_SOURCE
for _sym in self.syms.iter() {
let _symbol = self.strtab.get_at(_sym.st_name);
if let Some(symbol) = _symbol {
if symbol == "__stack_chk_fail" {
stack_canary = true;
if let Some(comp) = self.compiler_runtime(bytes) {
comp_map.insert("Compiler Runtime".to_string(), json!(comp));
}

// TODO: make tighter
} else if symbol.ends_with("_chk") {
fortify_source = true;
}
}
// path to linker if dynamic linking enabled
if let Some(linker) = self.linker_path() {
comp_map.insert("Linker Path".to_string(), json!(linker));
}

if self.libc() != 0.0 {
comp_map.insert(
"Minimum Libc Version".to_string(),
json!(format!("2.{:?}", self.libc())),
);
}
mitigate_map.insert("Stack Canary".to_string(), json!(stack_canary));
mitigate_map.insert("FORTIFY_SOURCE".to_string(), json!(fortify_source));
Ok(comp_map)
}

fn mitigations(&self) -> GenericMap {
let mut mitigate_map: GenericMap = GenericMap::new();
mitigate_map.insert(
"Executable Stack (NX Bit)".to_string(),
json!(self.executable_stack()),
);
mitigate_map.insert(
"Read-Only Relocatable (RELRO)".to_string(),
json!(self.relro()),
);
mitigate_map.insert(
"Position Independent Executable (PIE)".to_string(),
json!(self.position_independent()),
);
mitigate_map.insert("Stack Canary".to_string(), json!(self.stack_canary()));
mitigate_map.insert("FORTIFY_SOURCE".to_string(), json!(self.fortify_source()));
mitigate_map
}

Expand Down
Loading

0 comments on commit 5290959

Please sign in to comment.