1use 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 fn parse(compiler_string: &str) -> LinuxCompiler {
51 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 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 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 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 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 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 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 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 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 if symbol.starts_with("__ubsan") {
269 instr_map.insert(
270 "Undefined Behavior Sanitizer (UBSAN)".to_string(),
271 json!(true),
272 );
273
274 } else if symbol.starts_with("__asan") {
276 instr_map.insert("Address Sanitizer (ASAN)".to_string(), json!(true));
277
278 } else if symbol.starts_with("__afl") {
280 instr_map.insert("AFL Instrumentation".to_string(), json!(true));
281
282 } 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}