Skip to main content

exec_pe_core/
strings.rs

1//! String extraction from PE binary data.
2//!
3//! Scans raw bytes for runs of printable ASCII characters and UTF-16LE text.
4//! Used to find embedded C2 URLs, file paths, registry keys, and IOC strings.
5
6/// Minimum string length (in characters) to include in the output.
7pub const MIN_STRING_LEN: usize = 6;
8
9/// Shannon entropy of a byte slice (0.0 – 8.0).
10///
11/// Returns 0.0 for empty slices.
12pub fn compute_entropy(data: &[u8]) -> f32 {
13    if data.is_empty() {
14        return 0.0;
15    }
16    let mut freq = [0u32; 256];
17    for &b in data {
18        freq[b as usize] += 1;
19    }
20    let len = data.len() as f32;
21    let mut entropy = 0.0_f32;
22    for &count in &freq {
23        if count > 0 {
24            let p = count as f32 / len;
25            entropy -= p * p.log2();
26        }
27    }
28    entropy
29}
30
31/// Extract ASCII strings of at least `min_len` consecutive printable chars from `bytes`.
32///
33/// "Printable" means bytes 0x20 – 0x7E (space through tilde), matching the
34/// behaviour of the classic `strings(1)` utility.
35pub fn extract_ascii(bytes: &[u8], min_len: usize) -> Vec<String> {
36    let mut results = Vec::new();
37    let mut current = String::new();
38    for &b in bytes {
39        if (0x20..=0x7E).contains(&b) {
40            current.push(b as char);
41        } else {
42            if current.len() >= min_len {
43                results.push(current.clone());
44            }
45            current.clear();
46        }
47    }
48    if current.len() >= min_len {
49        results.push(current);
50    }
51    results
52}
53
54/// Extract UTF-16LE strings of at least `min_len` printable chars from `bytes`.
55///
56/// Detects runs where every second byte is 0x00 and the preceding byte is a
57/// printable ASCII character (0x20 – 0x7E).  This is a fast heuristic; it will
58/// not decode arbitrary Unicode code points outside the ASCII range.
59pub fn extract_utf16le(bytes: &[u8], min_len: usize) -> Vec<String> {
60    let mut results = Vec::new();
61    let mut current = String::new();
62    let mut i = 0;
63    while i + 1 < bytes.len() {
64        let lo = bytes[i];
65        let hi = bytes[i + 1];
66        if hi == 0x00 && (0x20..=0x7E).contains(&lo) {
67            current.push(lo as char);
68            i += 2;
69        } else {
70            if current.len() >= min_len {
71                results.push(current.clone());
72            }
73            current.clear();
74            i += 1;
75        }
76    }
77    if current.len() >= min_len {
78        results.push(current);
79    }
80    results
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    // ── compute_entropy ───────────────────────────────────────────────────────
88
89    #[test]
90    fn entropy_of_empty_is_zero() {
91        assert!(compute_entropy(&[]).abs() < 1e-6);
92    }
93
94    #[test]
95    fn entropy_of_uniform_bytes_is_zero() {
96        // A single repeated symbol carries no information -> 0 bits.
97        assert!(compute_entropy(&[0x41; 64]).abs() < 1e-6);
98    }
99
100    #[test]
101    fn entropy_of_two_equiprobable_symbols_is_one_bit() {
102        // Two symbols at p=0.5 each -> exactly 1.0 bit/byte.
103        let data: Vec<u8> = (0..256u32).map(|i| (i % 2) as u8).collect();
104        assert!((compute_entropy(&data) - 1.0).abs() < 1e-6);
105    }
106
107    #[test]
108    fn entropy_of_all_256_values_is_eight_bits() {
109        // A uniform distribution over all 256 byte values -> maximal 8.0 bits.
110        let data: Vec<u8> = (0..=255u8).collect();
111        assert!((compute_entropy(&data) - 8.0).abs() < 1e-6);
112    }
113
114    // ── extract_ascii ─────────────────────────────────────────────────────────
115
116    #[test]
117    fn ascii_extracts_simple_string() {
118        let input = b"Hello, World!";
119        let strings = extract_ascii(input, 6);
120        assert_eq!(strings, vec!["Hello, World!"]);
121    }
122
123    #[test]
124    fn ascii_skips_short_runs() {
125        let input = b"AB\x00CDEFGH";
126        let strings = extract_ascii(input, 6);
127        assert!(
128            strings.iter().all(|s| s.len() >= 6),
129            "all returned strings must be >= min_len chars"
130        );
131        assert!(
132            !strings.iter().any(|s| s == "AB"),
133            "two-char run must be filtered"
134        );
135    }
136
137    #[test]
138    fn ascii_empty_input_returns_empty() {
139        assert!(extract_ascii(&[], 6).is_empty());
140    }
141
142    #[test]
143    fn ascii_extracts_multiple_strings() {
144        let mut buf = Vec::new();
145        buf.extend_from_slice(b"VirtualAlloc");
146        buf.push(0x00);
147        buf.extend_from_slice(b"CreateRemoteThread");
148        let strings = extract_ascii(&buf, 6);
149        assert!(strings.contains(&"VirtualAlloc".to_string()));
150        assert!(strings.contains(&"CreateRemoteThread".to_string()));
151    }
152
153    #[test]
154    fn ascii_handles_all_non_printable() {
155        let input = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06];
156        assert!(extract_ascii(&input, 6).is_empty());
157    }
158
159    #[test]
160    fn ascii_returns_exact_min_len_string() {
161        let input = b"ABCDEF"; // exactly 6 chars
162        let strings = extract_ascii(input, 6);
163        assert!(strings.contains(&"ABCDEF".to_string()));
164    }
165
166    // ── extract_utf16le ───────────────────────────────────────────────────────
167
168    #[test]
169    fn utf16le_extracts_simple_string() {
170        // "Hello" as UTF-16LE
171        let input: Vec<u8> = "Hello!".encode_utf16().flat_map(u16::to_le_bytes).collect();
172        let strings = extract_utf16le(&input, 6);
173        assert!(
174            strings.contains(&"Hello!".to_string()),
175            "UTF-16LE 'Hello!' must be extracted"
176        );
177    }
178
179    #[test]
180    fn utf16le_empty_input_returns_empty() {
181        assert!(extract_utf16le(&[], 6).is_empty());
182    }
183
184    #[test]
185    fn utf16le_skips_short_runs() {
186        // "AB" as UTF-16LE — only 2 chars, below min_len
187        let input: Vec<u8> = "AB".encode_utf16().flat_map(u16::to_le_bytes).collect();
188        let strings = extract_utf16le(&input, 6);
189        assert!(
190            strings.iter().all(|s| s.len() >= 6),
191            "two-char UTF-16LE run must be filtered"
192        );
193    }
194
195    #[test]
196    fn utf16le_mixed_with_binary_extracts_only_strings() {
197        let mut buf: Vec<u8> = vec![0xDE, 0xAD, 0xBE, 0xEF];
198        buf.extend("VirtualAlloc".encode_utf16().flat_map(u16::to_le_bytes));
199        buf.extend_from_slice(&[0xFF, 0xFE]);
200        let strings = extract_utf16le(&buf, 6);
201        assert!(strings.contains(&"VirtualAlloc".to_string()));
202    }
203}