Skip to main content

binsec/check/
elf.rs

1//! ### ELF-Specific Compilation Checks:
2//!
3//! * Static compilation
4//! * Linker Path
5//! * Minimum glibc Version
6//!
7//! ### ELF-Specific Exploit Mitigations:
8//!
9//! * NX (Non-eXecutable bit) stack
10//! * Stack Canaries
11//! * FORTIFY_SOURCE
12//! * Position-Independent Executable / ASLR
13//! * Full/Partial RELRO
14
15use goblin::elf::dynamic::{tag_to_str, Dyn};
16use goblin::elf::{header, program_header, Elf};
17use regex::Regex;
18use serde_json::json;
19use std::collections::HashSet;
20use std::fmt::{self, Display};
21
22use crate::check::{Analyze, GenericMap};
23use crate::BinResult;
24
25use super::UniversalCompilationProperties;
26
27const GLIBC: &str = "GLIBC_2.";
28
29enum LinuxCompiler {
30    Gcc(Option<String>),
31    Clang(Option<String>),
32    Unknown,
33}
34
35impl Display for LinuxCompiler {
36    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
37        match self {
38            Self::Gcc(Some(ver)) => write!(f, "GCC version {}", ver),
39            Self::Gcc(None) => write!(f, "GCC version <unknown>"),
40            Self::Clang(Some(ver)) => write!(f, "Clang/LLVM version {}", ver),
41            Self::Clang(None) => write!(f, "Clang/LLVM version <unknown>"),
42            _ => write!(f, "<unknown>"),
43        }
44    }
45}
46
47impl LinuxCompiler {
48    /// Given data from `.comment`, deduce a compiler + version triplet.
49    /// TODO(alan): need more binary artifacts to test and make this better.
50    fn parse(compiler_string: &str) -> LinuxCompiler {
51        // parse for unique version triplets in the string
52        let Ok(ver_triplet_re) = Regex::new(r"\b\d+\.\d+(\.\d+)?\b") else {
53            return LinuxCompiler::Unknown;
54        };
55        let ver_set: Vec<&str> = ver_triplet_re
56            .find_iter(compiler_string)
57            .map(|mat| mat.as_str())
58            .collect();
59
60        let unique_vers: HashSet<&str> = ver_set.into_iter().collect();
61        let versions: Vec<&str> = unique_vers.into_iter().collect();
62        let min_ver = versions.first().map(|s| s.to_string());
63
64        // parse for the actual compiler
65        if compiler_string.contains("clang") {
66            LinuxCompiler::Clang(min_ver)
67        } else if compiler_string.contains("GCC:") {
68            LinuxCompiler::Gcc(min_ver)
69        } else {
70            LinuxCompiler::Unknown
71        }
72    }
73}
74
75#[derive(serde::Deserialize, serde::Serialize, Debug)]
76pub enum Relro {
77    Partial,
78    Full,
79    None,
80}
81
82impl UniversalCompilationProperties for Elf<'_> {
83    // common: shared object (pie exec or .so) or non-pie executable
84    fn binary_type(&self) -> &str {
85        header::et_to_str(self.header.e_type)
86    }
87
88    fn is_stripped(&self) -> bool {
89        self.syms.is_empty()
90    }
91
92    fn compiler_runtime(&self, bytes: &[u8]) -> Option<String> {
93        // most simple: `.comment` section annotating the compiler version
94        let mut compilation_string: Option<&str> = None;
95        for section in self.section_headers.clone().into_iter() {
96            if let Some(sym) = self.shdr_strtab.get_at(section.sh_name) {
97                if sym == ".comment" {
98                    let comment_section = &bytes[section.sh_offset as usize
99                        ..(section.sh_offset + section.sh_size) as usize];
100                    if let Ok(comment_str) = std::str::from_utf8(comment_section) {
101                        compilation_string = Some(comment_str);
102                    }
103                }
104            }
105        }
106
107        if let Some(comp_string) = compilation_string {
108            let comp_value = LinuxCompiler::parse(comp_string);
109            return Some(comp_value.to_string());
110        }
111        None
112    }
113}
114
115trait ElfCompilationProperties {
116    fn is_statically_compiled(&self) -> bool;
117    fn linker_path(&self) -> Option<&str>;
118    fn libc(&self) -> f64;
119}
120
121impl ElfCompilationProperties for Elf<'_> {
122    // elf static executable: check if PT_INTERP segment exists
123    fn is_statically_compiled(&self) -> bool {
124        self.program_headers
125            .iter()
126            .any(|ph| program_header::pt_to_str(ph.p_type) == "PT_INTERP")
127    }
128
129    fn linker_path(&self) -> Option<&str> {
130        self.interpreter
131    }
132
133    // TODO(alan): match on other stdlib runtimes, right now only glibc support
134    fn libc(&self) -> f64 {
135        let mut glibcs: Vec<f64> = vec![];
136        let Ok(dynsyms) = self.dynstrtab.to_vec() else {
137            return f64::INFINITY;
138        };
139        for sym in dynsyms {
140            if let Some(ver_str) = sym.strip_prefix(GLIBC) {
141                if let Ok(version) = ver_str.parse::<f64>() {
142                    glibcs.push(version);
143                }
144            }
145        }
146        if !glibcs.is_empty() {
147            glibcs.iter().fold(f64::INFINITY, |a, &b| a.min(b))
148        } else {
149            f64::INFINITY
150        }
151    }
152}
153
154pub trait ElfMitigations {
155    fn executable_stack(&self) -> bool;
156    fn stack_canary(&self) -> bool;
157    fn fortify_source(&self) -> bool;
158    fn position_independent(&self) -> bool;
159    fn relro(&self) -> Relro;
160}
161
162impl ElfMitigations for Elf<'_> {
163    fn executable_stack(&self) -> bool {
164        self.program_headers
165            .iter()
166            .any(|ph| program_header::pt_to_str(ph.p_type) == "PT_GNU_STACK" && ph.p_flags == 6)
167    }
168
169    fn stack_canary(&self) -> bool {
170        self.syms
171            .iter()
172            .filter_map(|sym| self.strtab.get_at(sym.st_name))
173            .any(|symstr| symstr == "__stack_chk_fail" || symstr == "__stack_chk_guard")
174    }
175
176    fn fortify_source(&self) -> bool {
177        // TODO: list fortified symbols
178        self.syms
179            .iter()
180            .filter_map(|sym| self.strtab.get_at(sym.st_name))
181            .any(|symstr| symstr.ends_with("_chk"))
182    }
183
184    fn position_independent(&self) -> bool {
185        matches!(self.header.e_type, 3)
186    }
187
188    fn relro(&self) -> Relro {
189        if !self
190            .program_headers
191            .iter()
192            .any(|ph| program_header::pt_to_str(ph.p_type) == "PT_GNU_RELRO")
193        {
194            return Relro::None;
195        }
196
197        // check for full/partial RELRO support by checking dynamic section for DT_BIND_NOW flag.
198        // DT_BIND_NOW takes precedence over lazy binding and processes relocations before actual execution.
199        if let Some(segs) = &self.dynamic {
200            let dyn_seg: Option<Dyn> = segs
201                .dyns
202                .iter()
203                .find(|tag| tag_to_str(tag.d_tag) == "DT_BIND_NOW")
204                .cloned();
205
206            if dyn_seg.is_some() {
207                return Relro::Full;
208            } else {
209                return Relro::Partial;
210            }
211        }
212        Relro::None
213    }
214}
215
216impl Analyze for Elf<'_> {
217    fn compilation(&self, bytes: &[u8]) -> BinResult<GenericMap> {
218        let mut comp_map: GenericMap = GenericMap::new();
219        comp_map.insert("Binary Type".to_string(), json!(self.binary_type()));
220        comp_map.insert("Stripped Executable".to_string(), json!(self.is_stripped()));
221        comp_map.insert(
222            "Statically Compiled".to_string(),
223            json!(self.is_statically_compiled()),
224        );
225
226        if let Some(comp) = self.compiler_runtime(bytes) {
227            comp_map.insert("Compiler Runtime".to_string(), json!(comp));
228        }
229
230        // path to linker if dynamic linking enabled
231        if let Some(linker) = self.linker_path() {
232            comp_map.insert("Linker Path".to_string(), json!(linker));
233        }
234
235        if self.libc() != f64::INFINITY {
236            comp_map.insert(
237                "Minimum Libc Version".to_string(),
238                json!(format!("2.{:?}", self.libc())),
239            );
240        }
241        Ok(comp_map)
242    }
243
244    fn mitigations(&self) -> GenericMap {
245        let mut mitigate_map: GenericMap = GenericMap::new();
246        mitigate_map.insert(
247            "Executable Stack (NX Bit)".to_string(),
248            json!(self.executable_stack()),
249        );
250        mitigate_map.insert(
251            "Read-Only Relocatable (RELRO)".to_string(),
252            json!(self.relro()),
253        );
254        mitigate_map.insert(
255            "Position Independent Executable (PIE)".to_string(),
256            json!(self.position_independent()),
257        );
258        mitigate_map.insert("Stack Canary".to_string(), json!(self.stack_canary()));
259        mitigate_map.insert("FORTIFY_SOURCE".to_string(), json!(self.fortify_source()));
260        mitigate_map
261    }
262
263    fn instrumentation(&self) -> GenericMap {
264        let mut instr_map: GenericMap = GenericMap::new();
265        for sym in self.syms.iter() {
266            if let Some(symbol) = self.strtab.get_at(sym.st_name) {
267                // /__ubsan\w+\d+/
268                if symbol.starts_with("__ubsan") {
269                    instr_map.insert(
270                        "Undefined Behavior Sanitizer (UBSAN)".to_string(),
271                        json!(true),
272                    );
273
274                // /_ZN\w+__asan\w+\d+/
275                } else if symbol.starts_with("__asan") {
276                    instr_map.insert("Address Sanitizer (ASAN)".to_string(), json!(true));
277
278                // /__afl\w+\d+/
279                } else if symbol.starts_with("__afl") {
280                    instr_map.insert("AFL Instrumentation".to_string(), json!(true));
281
282                // /__llvm\w+\d+/
283                } else if symbol.starts_with("__llvm") {
284                    instr_map.insert("LLVM Code Coverage".to_string(), json!(true));
285                }
286            }
287        }
288        instr_map
289    }
290}