exec_pe_core/
rich_header.rs1#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
24pub struct RichEntry {
25 pub product_id: u16,
27 pub build_id: u16,
29 pub use_count: u32,
31}
32
33#[derive(Debug, Clone, serde::Serialize)]
35pub struct RichHeader {
36 pub entries: Vec<RichEntry>,
38 pub xor_key: u32,
40}
41
42pub fn parse_rich_header(bytes: &[u8]) -> Option<RichHeader> {
47 if bytes.len() < 0x40 {
48 return None;
49 }
50
51 let e_lfanew = read_u32_le(bytes, 0x3C)? as usize;
53 if e_lfanew < 0x44 || e_lfanew > bytes.len() {
54 return None;
55 }
56
57 let stub_area = bytes.get(0x40..e_lfanew)?;
59
60 let rich_rel = find_pattern(stub_area, b"Rich")?;
62 let rich_abs = 0x40 + rich_rel;
63
64 let xor_key = read_u32_le(bytes, rich_abs + 4)?;
66
67 const DANS: u32 = 0x536E_6144;
70 let mut dans_rel: Option<usize> = None;
71 let mut i = 0usize;
72 while i + 4 <= rich_rel {
73 if let Some(raw) = read_u32_le(stub_area, i) {
74 if raw ^ xor_key == DANS {
75 dans_rel = Some(i);
76 break;
77 }
78 } i += 4;
80 }
81 let dans_rel = dans_rel?;
82
83 let entries_start = dans_rel + 16;
85 if entries_start > rich_rel {
86 return Some(RichHeader {
87 entries: vec![],
88 xor_key,
89 });
90 }
91
92 let mut entries = Vec::new();
93 let mut pos = entries_start;
94 while pos + 8 <= rich_rel {
95 let comp_id = read_u32_le(stub_area, pos)? ^ xor_key;
96 let use_count = read_u32_le(stub_area, pos + 4)? ^ xor_key;
97 entries.push(RichEntry {
98 product_id: (comp_id >> 16) as u16,
99 build_id: (comp_id & 0xFFFF) as u16,
100 use_count,
101 });
102 pos += 8;
103 }
104
105 Some(RichHeader { entries, xor_key })
106}
107
108pub(crate) fn read_u32_le(bytes: &[u8], offset: usize) -> Option<u32> {
111 bytes
112 .get(offset..offset + 4)
113 .and_then(|s| <[u8; 4]>::try_from(s).ok())
114 .map(u32::from_le_bytes)
115}
116
117pub(crate) fn find_pattern(haystack: &[u8], needle: &[u8]) -> Option<usize> {
118 haystack.windows(needle.len()).position(|w| w == needle)
119}
120
121#[cfg(test)]
122pub(crate) mod test_helpers {
123
124 pub fn make_pe_with_rich(entries: &[(u16, u16, u32)], xor_key: u32) -> Vec<u8> {
130 const DANS: u32 = 0x536E_6144; let mut stub: Vec<u8> = Vec::new();
133 stub.extend_from_slice(&(DANS ^ xor_key).to_le_bytes());
135 for _ in 0..3 {
136 stub.extend_from_slice(&xor_key.to_le_bytes()); }
138 for &(prod, build, count) in entries {
140 let comp_id = (u32::from(prod) << 16) | u32::from(build);
141 stub.extend_from_slice(&(comp_id ^ xor_key).to_le_bytes());
142 stub.extend_from_slice(&(count ^ xor_key).to_le_bytes());
143 }
144 stub.extend_from_slice(b"Rich");
146 stub.extend_from_slice(&xor_key.to_le_bytes());
147
148 let e_lfanew: u32 = 0x40 + stub.len() as u32;
149 let mut buf = vec![0u8; e_lfanew as usize + 4];
150 buf[0] = b'M';
151 buf[1] = b'Z';
152 buf[0x3C..0x40].copy_from_slice(&e_lfanew.to_le_bytes());
153 buf[0x40..e_lfanew as usize].copy_from_slice(&stub);
154 buf[e_lfanew as usize..].copy_from_slice(b"PE\0\0");
155 buf
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::{test_helpers::make_pe_with_rich, *};
162
163 #[test]
164 fn returns_none_for_empty_bytes() {
165 assert!(parse_rich_header(&[]).is_none());
166 }
167
168 #[test]
169 fn returns_none_when_no_rich_marker() {
170 let mut pe = vec![0u8; 68];
172 pe[0] = b'M';
173 pe[1] = b'Z';
174 let e_lfanew: u32 = 0x40;
175 pe[0x3C..0x40].copy_from_slice(&e_lfanew.to_le_bytes());
176 pe[0x40..0x44].copy_from_slice(b"PE\0\0");
177 assert!(parse_rich_header(&pe).is_none());
178 }
179
180 #[test]
181 fn returns_none_for_truncated_input() {
182 let buf = [b'M', b'Z'];
183 assert!(parse_rich_header(&buf).is_none());
184 }
185
186 #[test]
187 fn parses_single_entry_correctly() {
188 let key = 0xDEAD_BEEF_u32;
189 let buf = make_pe_with_rich(&[(0x0103, 0x6B6B, 5)], key);
190 let rh = parse_rich_header(&buf).expect("Rich header must be found");
191 assert_eq!(rh.xor_key, key);
192 assert_eq!(rh.entries.len(), 1);
193 assert_eq!(rh.entries[0].product_id, 0x0103);
194 assert_eq!(rh.entries[0].build_id, 0x6B6B);
195 assert_eq!(rh.entries[0].use_count, 5);
196 }
197
198 #[test]
199 fn parses_multiple_entries_in_order() {
200 let key = 0x1234_5678_u32;
201 let expected = [
202 (0x0001, 0x6B00, 1),
203 (0x010C, 0x6B1A, 12),
204 (0x0103, 0x6B6B, 42),
205 ];
206 let buf = make_pe_with_rich(&expected, key);
207 let rh = parse_rich_header(&buf).expect("Rich header must be found");
208 assert_eq!(rh.entries.len(), 3);
209 assert_eq!(rh.entries[0].product_id, 0x0001);
210 assert_eq!(rh.entries[1].use_count, 12);
211 assert_eq!(rh.entries[2].product_id, 0x0103);
212 assert_eq!(rh.entries[2].build_id, 0x6B6B);
213 }
214
215 #[test]
216 fn zero_entry_rich_header_parses_successfully() {
217 let key = 0xCAFE_BABE_u32;
218 let buf = make_pe_with_rich(&[], key);
219 let rh = parse_rich_header(&buf).expect("zero-entry Rich header");
220 assert!(rh.entries.is_empty());
221 assert_eq!(rh.xor_key, key);
222 }
223
224 #[test]
225 fn xor_key_is_correct() {
226 let expected_key = 0x0101_0202_u32;
227 let buf = make_pe_with_rich(&[(1, 2, 3)], expected_key);
228 let rh = parse_rich_header(&buf).unwrap();
229 assert_eq!(rh.xor_key, expected_key);
230 }
231
232 const DANS: u32 = 0x536E_6144;
233
234 fn wrap_stub(stub: &[u8]) -> Vec<u8> {
237 let e_lfanew = 0x40 + stub.len() as u32;
238 let mut buf = vec![0u8; e_lfanew as usize + 4];
239 buf[0] = b'M';
240 buf[1] = b'Z';
241 buf[0x3C..0x40].copy_from_slice(&e_lfanew.to_le_bytes());
242 buf[0x40..e_lfanew as usize].copy_from_slice(stub);
243 buf[e_lfanew as usize..].copy_from_slice(b"PE\0\0");
244 buf
245 }
246
247 #[test]
248 fn dans_marker_found_after_leading_junk_dword() {
249 let key = 0x1122_3344_u32;
252 let mut stub = Vec::new();
253 stub.extend_from_slice(&0u32.to_le_bytes()); stub.extend_from_slice(&(DANS ^ key).to_le_bytes()); for _ in 0..3 {
256 stub.extend_from_slice(&key.to_le_bytes()); }
258 let comp_id = (0x0103u32 << 16) | 0x6B6B;
259 stub.extend_from_slice(&(comp_id ^ key).to_le_bytes());
260 stub.extend_from_slice(&(7u32 ^ key).to_le_bytes());
261 stub.extend_from_slice(b"Rich");
262 stub.extend_from_slice(&key.to_le_bytes());
263
264 let rh = parse_rich_header(&wrap_stub(&stub)).expect("DanS found after junk");
265 assert_eq!(rh.xor_key, key);
266 assert_eq!(rh.entries.len(), 1);
267 assert_eq!(rh.entries[0].product_id, 0x0103);
268 assert_eq!(rh.entries[0].use_count, 7);
269 }
270
271 #[test]
272 fn dans_immediately_before_rich_yields_empty_entries() {
273 let key = 0xAABB_CCDD_u32;
277 let mut stub = Vec::new();
278 stub.extend_from_slice(&(DANS ^ key).to_le_bytes()); stub.extend_from_slice(b"Rich"); stub.extend_from_slice(&key.to_le_bytes());
281
282 let rh = parse_rich_header(&wrap_stub(&stub)).expect("empty-but-valid Rich header");
283 assert!(rh.entries.is_empty());
284 assert_eq!(rh.xor_key, key);
285 }
286}