threatflux_binary_analysis/analysis/
entropy.rs1use crate::{
4 BinaryFile, Result,
5 types::{EntropyAnalysis, EntropyRegion, ObfuscationLevel, PackingIndicators, Section},
6};
7use std::collections::HashMap;
8
9pub fn analyze_binary(binary: &BinaryFile) -> Result<EntropyAnalysis> {
11 let data = binary.data();
12
13 let overall_entropy = calculate_entropy(data);
15
16 let mut section_entropy = HashMap::new();
18 for section in binary.sections() {
19 if let Some(section_data) = section_bytes(data, section) {
20 let entropy = calculate_entropy(section_data);
21 section_entropy.insert(section.name.clone(), entropy);
22 }
23 }
24
25 let high_entropy_regions = find_high_entropy_regions(data)?;
27
28 let packing_indicators = analyze_packing_indicators(data, §ion_entropy);
30
31 Ok(EntropyAnalysis {
32 overall_entropy,
33 section_entropy,
34 high_entropy_regions,
35 packing_indicators,
36 })
37}
38
39fn section_bytes<'a>(data: &'a [u8], section: &Section) -> Option<&'a [u8]> {
40 let length = section.file_size.min(section.size);
41 if length == 0 {
42 return None;
43 }
44
45 let end_offset = section.offset.checked_add(length)?;
46 let start = usize::try_from(section.offset).ok()?;
47 let end = usize::try_from(end_offset).ok()?;
48 data.get(start..end)
49}
50
51fn calculate_entropy(data: &[u8]) -> f64 {
53 if data.is_empty() {
54 return 0.0;
55 }
56
57 let mut freq = [0_u64; 256];
59 for &byte in data {
60 freq[byte as usize] += 1;
61 }
62
63 let len = data.len() as f64;
65 let mut entropy = 0.0;
66
67 for &count in &freq {
68 if count > 0 {
69 let p = count as f64 / len;
70 entropy -= p * p.log2();
71 }
72 }
73
74 entropy
75}
76
77fn find_high_entropy_regions(data: &[u8]) -> Result<Vec<EntropyRegion>> {
79 let mut regions = Vec::new();
80 let chunk_size = 1024; let high_entropy_threshold = 7.5; for (i, chunk) in data.chunks(chunk_size).enumerate() {
84 let entropy = calculate_entropy(chunk);
85
86 if entropy > high_entropy_threshold {
87 let start = i * chunk_size;
88 let end = std::cmp::min(start + chunk_size, data.len());
89
90 let description = classify_high_entropy_region(chunk, entropy);
91
92 regions.push(EntropyRegion {
93 start: start as u64,
94 end: end as u64,
95 entropy,
96 description,
97 });
98 }
99 }
100
101 Ok(regions)
102}
103
104fn classify_high_entropy_region(data: &[u8], entropy: f64) -> String {
106 if entropy > 7.9 {
107 "Likely encrypted or compressed data".to_string()
108 } else if entropy > 7.5 {
109 if has_crypto_constants(data) {
111 "Possible cryptographic constants".to_string()
112 } else if has_compression_signature(data) {
113 "Possible compressed data".to_string()
114 } else {
115 "High entropy region - possible obfuscation".to_string()
116 }
117 } else {
118 "Moderately high entropy".to_string()
119 }
120}
121
122fn has_crypto_constants(data: &[u8]) -> bool {
124 #[allow(clippy::type_complexity)]
126 const CRYPTO_CONSTANTS: &[&[u8]] = &[
127 b"\x67\x45\x23\x01", b"\x01\x23\x45\x67", b"\x89\xab\xcd\xef", ];
131
132 for &constant in CRYPTO_CONSTANTS {
133 if data.windows(constant.len()).any(|w| w == constant) {
134 return true;
135 }
136 }
137
138 false
139}
140
141fn has_compression_signature(data: &[u8]) -> bool {
143 data.starts_with(b"\x1f\x8b") || data.starts_with(b"PK\x03\x04") || data.starts_with(b"BZh") || data.starts_with(b"\xfd7zXZ\0") }
148
149fn analyze_packing_indicators(
151 data: &[u8],
152 section_entropy: &HashMap<String, f64>,
153) -> PackingIndicators {
154 let mut indicators = PackingIndicators::default();
155
156 let overall_entropy = calculate_entropy(data);
158
159 if overall_entropy > 7.5 {
161 indicators.is_packed = true;
162 }
163
164 let mut high_entropy_code = false;
166 for (name, &entropy) in section_entropy {
167 if (name.contains("text") || name.contains("code")) && entropy > 7.0 {
168 high_entropy_code = true;
169 break;
170 }
171 }
172
173 if high_entropy_code {
174 indicators.is_packed = true;
175 }
176
177 indicators.compression_ratio = None;
179
180 indicators.obfuscation_level = if overall_entropy > 7.8 {
182 ObfuscationLevel::High
183 } else if overall_entropy > 7.5 {
184 ObfuscationLevel::Medium
185 } else if overall_entropy > 7.0 {
186 ObfuscationLevel::Low
187 } else {
188 ObfuscationLevel::None
189 };
190
191 indicators.packer_name = detect_packer(data);
193
194 indicators
195}
196
197fn detect_packer(data: &[u8]) -> Option<String> {
199 let contains = |signature: &[u8]| {
203 data.windows(signature.len())
204 .any(|window| window == signature)
205 };
206
207 if contains(b"UPX") {
208 Some("UPX".to_string())
209 } else if contains(b"VMProtect") {
210 Some("VMProtect".to_string())
211 } else if contains(b"Themida") {
212 Some("Themida".to_string())
213 } else if contains(b"ASPack") {
214 Some("ASPack".to_string())
215 } else {
216 None
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use crate::types::{SectionPermissions, SectionType};
224
225 fn section(size: u64, offset: u64, file_size: u64) -> Section {
226 Section {
227 name: ".test".to_string(),
228 address: 0x1000,
229 size,
230 offset,
231 file_size,
232 permissions: SectionPermissions::default(),
233 section_type: SectionType::Data,
234 data: None,
235 }
236 }
237
238 #[test]
239 fn section_entropy_uses_only_file_backed_bytes() {
240 let data = b"headersPAYLOADnext-section";
241
242 assert_eq!(
243 section_bytes(data, §ion(32, 7, 7)),
244 Some(&b"PAYLOAD"[..])
245 );
246 assert_eq!(section_bytes(data, §ion(32, 0, 0)), None);
247 }
248
249 #[test]
250 fn test_entropy_calculation() {
251 let uniform_data = vec![0u8; 1024];
253 let entropy = calculate_entropy(&uniform_data);
254 assert!(entropy < 1.0);
255
256 let random_data: Vec<u8> = (0..1024).map(|i| (i * 7 + 13) as u8).collect();
258 let entropy = calculate_entropy(&random_data);
259 assert!(entropy > 7.0);
260 }
261
262 #[test]
263 fn test_crypto_constants_detection() {
264 let data = b"\x67\x45\x23\x01some other data";
265 assert!(has_crypto_constants(data));
266
267 let data = b"no crypto constants here";
268 assert!(!has_crypto_constants(data));
269 }
270
271 #[test]
272 fn test_compression_signature_detection() {
273 let gzip_data = b"\x1f\x8b\x08\x00";
275 assert!(has_compression_signature(gzip_data));
276
277 let zip_data = b"PK\x03\x04";
279 assert!(has_compression_signature(zip_data));
280
281 let xz_data = b"\xfd7zXZ\0payload";
282 assert!(has_compression_signature(xz_data));
283
284 let normal_data = b"normal data";
286 assert!(!has_compression_signature(normal_data));
287 }
288
289 #[test]
290 fn detects_packer_marker_beyond_the_file_prefix() {
291 let mut data = vec![0_u8; 4096];
292 data[3000..3003].copy_from_slice(b"UPX");
293
294 assert_eq!(detect_packer(&data).as_deref(), Some("UPX"));
295 }
296
297 #[test]
298 fn entropy_does_not_claim_a_compression_ratio() {
299 let data: Vec<u8> = (0..8192).map(|index| index as u8).collect();
300 let indicators = analyze_packing_indicators(&data, &HashMap::new());
301
302 assert!(indicators.is_packed);
303 assert!(indicators.compression_ratio.is_none());
304 }
305}