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.";
enum LinuxCompiler {
Gcc(Option<String>),
Clang(Option<String>),
Unknown,
}
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 {
fn parse(compiler_string: &str) -> LinuxCompiler {
let Ok(ver_triplet_re) = Regex::new(r"\b\d+\.\d+(\.\d+)?\b") else {
return LinuxCompiler::Unknown;
};
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());
if compiler_string.contains("clang") {
LinuxCompiler::Clang(min_ver)
} else if compiler_string.contains("GCC:") {
LinuxCompiler::Gcc(min_ver)
} else {
LinuxCompiler::Unknown
}
}
}
#[derive(serde::Deserialize, serde::Serialize, Debug)]
pub enum Relro {
Partial,
Full,
None,
}
impl UniversalCompilationProperties for Elf<'_> {
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> {
let mut compilation_string: Option<&str> = None;
for section in self.section_headers.clone().into_iter() {
if let Some(sym) = self.shdr_strtab.get_at(section.sh_name) {
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<'_> {
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
}
fn libc(&self) -> f64 {
let mut glibcs: Vec<f64> = vec![];
let Ok(dynsyms) = self.dynstrtab.to_vec() else {
return f64::INFINITY;
};
for sym in dynsyms {
if let Some(ver_str) = sym.strip_prefix(GLIBC) {
if let Ok(version) = ver_str.parse::<f64>() {
glibcs.push(version);
}
}
}
if !glibcs.is_empty() {
glibcs.iter().fold(f64::INFINITY, |a, &b| a.min(b))
} else {
f64::INFINITY
}
}
}
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;
}
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)
}
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 {
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;
}
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;
}
}
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()),
);
if let Some(comp) = self.compiler_runtime(bytes) {
comp_map.insert("Compiler Runtime".to_string(), json!(comp));
}
if let Some(linker) = self.linker_path() {
comp_map.insert("Linker Path".to_string(), json!(linker));
}
if self.libc() != f64::INFINITY {
comp_map.insert(
"Minimum Libc Version".to_string(),
json!(format!("2.{:?}", self.libc())),
);
}
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
}
fn instrumentation(&self) -> GenericMap {
let mut instr_map: GenericMap = GenericMap::new();
for sym in self.syms.iter() {
if let Some(symbol) = self.strtab.get_at(sym.st_name) {
if symbol.starts_with("__ubsan") {
instr_map.insert(
"Undefined Behavior Sanitizer (UBSAN)".to_string(),
json!(true),
);
} else if symbol.starts_with("__asan") {
instr_map.insert("Address Sanitizer (ASAN)".to_string(), json!(true));
} else if symbol.starts_with("__afl") {
instr_map.insert("AFL Instrumentation".to_string(), json!(true));
} else if symbol.starts_with("__llvm") {
instr_map.insert("LLVM Code Coverage".to_string(), json!(true));
}
}
}
instr_map
}
}