Skip to main content

threatflux_binary_analysis/analysis/
entropy.rs

1//! Entropy analysis for binary files
2
3use crate::{
4    BinaryFile, Result,
5    types::{EntropyAnalysis, EntropyRegion, ObfuscationLevel, PackingIndicators, Section},
6};
7use std::collections::HashMap;
8
9/// Analyze entropy of a binary file
10pub fn analyze_binary(binary: &BinaryFile) -> Result<EntropyAnalysis> {
11    let data = binary.data();
12
13    // Calculate overall entropy
14    let overall_entropy = calculate_entropy(data);
15
16    // Calculate section-wise entropy
17    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    // Find high entropy regions
26    let high_entropy_regions = find_high_entropy_regions(data)?;
27
28    // Analyze packing indicators
29    let packing_indicators = analyze_packing_indicators(data, &section_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
51/// Calculate Shannon entropy for data
52fn calculate_entropy(data: &[u8]) -> f64 {
53    if data.is_empty() {
54        return 0.0;
55    }
56
57    // Count byte frequencies
58    let mut freq = [0_u64; 256];
59    for &byte in data {
60        freq[byte as usize] += 1;
61    }
62
63    // Calculate entropy
64    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
77/// Find regions with high entropy
78fn find_high_entropy_regions(data: &[u8]) -> Result<Vec<EntropyRegion>> {
79    let mut regions = Vec::new();
80    let chunk_size = 1024; // Analyze in 1KB chunks
81    let high_entropy_threshold = 7.5; // Threshold for high entropy
82
83    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
104/// Classify what might cause high entropy in a region
105fn 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        // Check for patterns that might indicate specific types
110        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
122/// Check for cryptographic constants
123fn has_crypto_constants(data: &[u8]) -> bool {
124    // Look for common crypto constants (simplified)
125    #[allow(clippy::type_complexity)]
126    const CRYPTO_CONSTANTS: &[&[u8]] = &[
127        b"\x67\x45\x23\x01", // MD5 constant
128        b"\x01\x23\x45\x67", // Another common constant
129        b"\x89\xab\xcd\xef", // Another common constant
130    ];
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
141/// Check for compression signatures
142fn has_compression_signature(data: &[u8]) -> bool {
143    data.starts_with(b"\x1f\x8b") // GZIP
144        || data.starts_with(b"PK\x03\x04") // ZIP
145        || data.starts_with(b"BZh") // BZIP2
146        || data.starts_with(b"\xfd7zXZ\0") // XZ
147}
148
149/// Analyze indicators of packing/obfuscation
150fn analyze_packing_indicators(
151    data: &[u8],
152    section_entropy: &HashMap<String, f64>,
153) -> PackingIndicators {
154    let mut indicators = PackingIndicators::default();
155
156    // Check overall entropy
157    let overall_entropy = calculate_entropy(data);
158
159    // High overall entropy suggests packing
160    if overall_entropy > 7.5 {
161        indicators.is_packed = true;
162    }
163
164    // Check for high entropy in code sections
165    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    // Entropy alone cannot determine a compression ratio, so leave the estimate unknown.
178    indicators.compression_ratio = None;
179
180    // Determine obfuscation level
181    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    // Try to identify specific packers (simplified)
192    indicators.packer_name = detect_packer(data);
193
194    indicators
195}
196
197/// Attempt to detect specific packers
198fn detect_packer(data: &[u8]) -> Option<String> {
199    // This is a very simplified packer detection
200    // In practice, this would use a database of packer signatures
201
202    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, &section(32, 7, 7)),
244            Some(&b"PAYLOAD"[..])
245        );
246        assert_eq!(section_bytes(data, &section(32, 0, 0)), None);
247    }
248
249    #[test]
250    fn test_entropy_calculation() {
251        // Test with uniform data (low entropy)
252        let uniform_data = vec![0u8; 1024];
253        let entropy = calculate_entropy(&uniform_data);
254        assert!(entropy < 1.0);
255
256        // Test with random-like data (high entropy)
257        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        // Test GZIP signature
274        let gzip_data = b"\x1f\x8b\x08\x00";
275        assert!(has_compression_signature(gzip_data));
276
277        // Test ZIP signature
278        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        // Test no compression
285        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}