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 b >= 0x20 && b <= 0x7E {
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 && lo >= 0x20 && lo <= 0x7E {
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    // ── extract_ascii ─────────────────────────────────────────────────────────
88
89    #[test]
90    fn ascii_extracts_simple_string() {
91        let input = b"Hello, World!";
92        let strings = extract_ascii(input, 6);
93        assert_eq!(strings, vec!["Hello, World!"]);
94    }
95
96    #[test]
97    fn ascii_skips_short_runs() {
98        let input = b"AB\x00CDEFGH";
99        let strings = extract_ascii(input, 6);
100        assert!(
101            strings.iter().all(|s| s.len() >= 6),
102            "all returned strings must be >= min_len chars"
103        );
104        assert!(
105            !strings.iter().any(|s| s == "AB"),
106            "two-char run must be filtered"
107        );
108    }
109
110    #[test]
111    fn ascii_empty_input_returns_empty() {
112        assert!(extract_ascii(&[], 6).is_empty());
113    }
114
115    #[test]
116    fn ascii_extracts_multiple_strings() {
117        let mut buf = Vec::new();
118        buf.extend_from_slice(b"VirtualAlloc");
119        buf.push(0x00);
120        buf.extend_from_slice(b"CreateRemoteThread");
121        let strings = extract_ascii(&buf, 6);
122        assert!(strings.contains(&"VirtualAlloc".to_string()));
123        assert!(strings.contains(&"CreateRemoteThread".to_string()));
124    }
125
126    #[test]
127    fn ascii_handles_all_non_printable() {
128        let input = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06];
129        assert!(extract_ascii(&input, 6).is_empty());
130    }
131
132    #[test]
133    fn ascii_returns_exact_min_len_string() {
134        let input = b"ABCDEF"; // exactly 6 chars
135        let strings = extract_ascii(input, 6);
136        assert!(strings.contains(&"ABCDEF".to_string()));
137    }
138
139    // ── extract_utf16le ───────────────────────────────────────────────────────
140
141    #[test]
142    fn utf16le_extracts_simple_string() {
143        // "Hello" as UTF-16LE
144        let input: Vec<u8> = "Hello!"
145            .encode_utf16()
146            .flat_map(|c| c.to_le_bytes())
147            .collect();
148        let strings = extract_utf16le(&input, 6);
149        assert!(
150            strings.contains(&"Hello!".to_string()),
151            "UTF-16LE 'Hello!' must be extracted"
152        );
153    }
154
155    #[test]
156    fn utf16le_empty_input_returns_empty() {
157        assert!(extract_utf16le(&[], 6).is_empty());
158    }
159
160    #[test]
161    fn utf16le_skips_short_runs() {
162        // "AB" as UTF-16LE — only 2 chars, below min_len
163        let input: Vec<u8> = "AB".encode_utf16().flat_map(|c| c.to_le_bytes()).collect();
164        let strings = extract_utf16le(&input, 6);
165        assert!(
166            strings.iter().all(|s| s.len() >= 6),
167            "two-char UTF-16LE run must be filtered"
168        );
169    }
170
171    #[test]
172    fn utf16le_mixed_with_binary_extracts_only_strings() {
173        let mut buf: Vec<u8> = vec![0xDE, 0xAD, 0xBE, 0xEF];
174        buf.extend("VirtualAlloc".encode_utf16().flat_map(|c| c.to_le_bytes()));
175        buf.extend_from_slice(&[0xFF, 0xFE]);
176        let strings = extract_utf16le(&buf, 6);
177        assert!(strings.contains(&"VirtualAlloc".to_string()));
178    }
179}