1#![forbid(unsafe_code)]
23
24use forensicnomicon::shlink;
25
26const FILETIME_UNIX_DELTA_100NS: i64 = 116_444_736_000_000_000;
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct ShellLink {
33 pub header: ShellLinkHeader,
35 pub link_target_idlist: Option<LinkTargetIdList>,
40 pub link_info: Option<LinkInfo>,
42 pub string_data: StringData,
44 pub tracker: Option<TrackerDataBlock>,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct ShellLinkHeader {
51 pub link_flags: u32,
53 pub file_attributes: u32,
55 pub creation_time: i64,
57 pub access_time: i64,
59 pub write_time: i64,
61 pub file_size: u32,
63 pub icon_index: i32,
65 pub show_command: u32,
67 pub hotkey: u16,
69}
70
71impl ShellLinkHeader {
72 #[must_use]
74 pub fn has_flag(&self, flag: u32) -> bool {
75 self.link_flags & flag != 0
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct LinkTargetIdList {
82 pub raw: Vec<u8>,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct LinkInfo {
89 pub volume_id: Option<VolumeId>,
91 pub local_base_path: Option<String>,
93 pub common_network_relative_link: Option<CommonNetworkRelativeLink>,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct VolumeId {
100 pub drive_type: u32,
102 pub drive_serial_number: u32,
105 pub volume_label: Option<String>,
107}
108
109pub mod drive_type {
111 pub const UNKNOWN: u32 = 0;
113 pub const NO_ROOT_DIR: u32 = 1;
115 pub const REMOVABLE: u32 = 2;
117 pub const FIXED: u32 = 3;
119 pub const REMOTE: u32 = 4;
121 pub const CDROM: u32 = 5;
123 pub const RAMDISK: u32 = 6;
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct CommonNetworkRelativeLink {
130 pub net_name: Option<String>,
132 pub device_name: Option<String>,
134}
135
136#[derive(Debug, Clone, Default, PartialEq, Eq)]
141pub struct StringData {
142 pub name: Option<String>,
144 pub relative_path: Option<String>,
146 pub working_dir: Option<String>,
148 pub arguments: Option<String>,
150 pub icon_location: Option<String>,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct TrackerDataBlock {
157 pub machine_id: String,
159 pub droid: DroidGuids,
161 pub birth_droid: DroidGuids,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq)]
167pub struct DroidGuids {
168 pub volume: String,
170 pub object: String,
172}
173
174fn le_u16(data: &[u8], off: usize) -> u16 {
177 let mut b = [0u8; 2];
178 if let Some(s) = data.get(off..off + 2) {
179 b.copy_from_slice(s);
180 }
181 u16::from_le_bytes(b)
182}
183
184fn le_u32(data: &[u8], off: usize) -> u32 {
185 let mut b = [0u8; 4];
186 if let Some(s) = data.get(off..off + 4) {
187 b.copy_from_slice(s);
188 }
189 u32::from_le_bytes(b)
190}
191
192fn le_i32(data: &[u8], off: usize) -> i32 {
193 le_u32(data, off) as i32
194}
195
196fn le_u64(data: &[u8], off: usize) -> u64 {
197 let mut b = [0u8; 8];
198 if let Some(s) = data.get(off..off + 8) {
199 b.copy_from_slice(s);
200 }
201 u64::from_le_bytes(b)
202}
203
204fn filetime_to_unix(ft: u64) -> i64 {
207 if ft == 0 {
208 return 0;
209 }
210 ((ft as i64) - FILETIME_UNIX_DELTA_100NS) / 10_000_000
211}
212
213fn guid_string(b: &[u8]) -> Option<String> {
218 let g = b.get(0..16)?;
219 Some(format!(
220 "{:08X}-{:04X}-{:04X}-{:02X}{:02X}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}",
221 u32::from_le_bytes([g[0], g[1], g[2], g[3]]),
222 u16::from_le_bytes([g[4], g[5]]),
223 u16::from_le_bytes([g[6], g[7]]),
224 g[8],
225 g[9],
226 g[10],
227 g[11],
228 g[12],
229 g[13],
230 g[14],
231 g[15],
232 ))
233}
234
235fn ansi_z(data: &[u8], off: usize) -> Option<String> {
237 let slice = data.get(off..)?;
238 let end = slice.iter().position(|&c| c == 0).unwrap_or(slice.len());
239 Some(String::from_utf8_lossy(&slice[..end]).into_owned())
240}
241
242fn unicode_z(data: &[u8], off: usize) -> Option<String> {
244 let slice = data.get(off..)?;
245 let mut units = Vec::new();
246 let mut i = 0;
247 while i + 1 < slice.len() {
248 let u = u16::from_le_bytes([slice[i], slice[i + 1]]);
249 if u == 0 {
250 break;
251 }
252 units.push(u);
253 i += 2;
254 }
255 Some(String::from_utf16_lossy(&units))
256}
257
258#[must_use]
265pub fn parse_shell_link(data: &[u8]) -> Option<ShellLink> {
266 if le_u32(data, 0) != shlink::HEADER_SIZE {
268 return None;
269 }
270 let clsid = guid_string(data.get(4..20)?)?;
271 if clsid != shlink::LINK_CLSID {
272 return None;
273 }
274
275 let link_flags = le_u32(data, 20);
276 let file_attributes = le_u32(data, 24);
277 let creation_time = filetime_to_unix(le_u64(data, 28));
278 let access_time = filetime_to_unix(le_u64(data, 36));
279 let write_time = filetime_to_unix(le_u64(data, 44));
280 let file_size = le_u32(data, 52);
281 let icon_index = le_i32(data, 56);
282 let show_command = le_u32(data, 60);
283 let hotkey = le_u16(data, 64);
284
285 let header = ShellLinkHeader {
286 link_flags,
287 file_attributes,
288 creation_time,
289 access_time,
290 write_time,
291 file_size,
292 icon_index,
293 show_command,
294 hotkey,
295 };
296
297 let mut off = shlink::HEADER_SIZE as usize;
299
300 let link_target_idlist = if header.has_flag(shlink::LINK_FLAG_HAS_LINK_TARGET_ID_LIST) {
302 let id_list_size = le_u16(data, off) as usize;
303 let blob_start = off + 2;
304 let raw = data
305 .get(blob_start..blob_start + id_list_size)
306 .map(<[u8]>::to_vec)
307 .unwrap_or_default();
308 off = blob_start + id_list_size;
309 Some(LinkTargetIdList { raw })
310 } else {
311 None
312 };
313
314 let link_info = if header.has_flag(shlink::LINK_FLAG_HAS_LINK_INFO) {
316 let info = parse_link_info(data, off);
317 let size = le_u32(data, off) as usize;
319 off += size.max(4);
320 info
321 } else {
322 None
323 };
324
325 let is_unicode = header.has_flag(shlink::LINK_FLAG_IS_UNICODE);
327 let mut string_data = StringData::default();
328 for (flag, slot) in [
329 (
330 shlink::LINK_FLAG_HAS_NAME,
331 &mut string_data.name as &mut Option<String>,
332 ),
333 (
334 shlink::LINK_FLAG_HAS_RELATIVE_PATH,
335 &mut string_data.relative_path,
336 ),
337 (
338 shlink::LINK_FLAG_HAS_WORKING_DIR,
339 &mut string_data.working_dir,
340 ),
341 (shlink::LINK_FLAG_HAS_ARGUMENTS, &mut string_data.arguments),
342 (
343 shlink::LINK_FLAG_HAS_ICON_LOCATION,
344 &mut string_data.icon_location,
345 ),
346 ] {
347 if header.has_flag(flag) {
348 let (value, next) = read_sized_string(data, off, is_unicode);
349 *slot = value;
350 off = next;
351 }
352 }
353
354 let tracker = parse_extra_data_tracker(data, off);
357
358 Some(ShellLink {
359 header,
360 link_target_idlist,
361 link_info,
362 string_data,
363 tracker,
364 })
365}
366
367fn parse_link_info(data: &[u8], base: usize) -> Option<LinkInfo> {
369 let size = le_u32(data, base) as usize;
370 if size < 0x1C {
371 return None;
372 }
373 let header_size = le_u32(data, base + 4) as usize;
374 let flags = le_u32(data, base + 8);
375 let volume_id_offset = le_u32(data, base + 12) as usize;
376 let local_base_path_offset = le_u32(data, base + 16) as usize;
377 let cnrl_offset = le_u32(data, base + 20) as usize;
378 let local_base_path_offset_unicode = if header_size >= 0x24 {
380 le_u32(data, base + 28) as usize
381 } else {
382 0
383 };
384
385 const VOLUME_ID_AND_LOCAL_BASE_PATH: u32 = 0x1;
386 const CNRL_AND_PATH_SUFFIX: u32 = 0x2;
387
388 let volume_id = if flags & VOLUME_ID_AND_LOCAL_BASE_PATH != 0 && volume_id_offset != 0 {
389 parse_volume_id(data, base + volume_id_offset)
390 } else {
391 None
392 };
393
394 let local_base_path = if flags & VOLUME_ID_AND_LOCAL_BASE_PATH != 0 {
395 if local_base_path_offset_unicode != 0 {
396 unicode_z(data, base + local_base_path_offset_unicode)
397 } else if local_base_path_offset != 0 {
398 ansi_z(data, base + local_base_path_offset)
399 } else {
400 None
401 }
402 } else {
403 None
404 };
405
406 let common_network_relative_link = if flags & CNRL_AND_PATH_SUFFIX != 0 && cnrl_offset != 0 {
407 parse_cnrl(data, base + cnrl_offset)
408 } else {
409 None
410 };
411
412 Some(LinkInfo {
413 volume_id,
414 local_base_path,
415 common_network_relative_link,
416 })
417}
418
419fn parse_volume_id(data: &[u8], base: usize) -> Option<VolumeId> {
421 let size = le_u32(data, base) as usize;
422 if size < 0x10 {
423 return None;
424 }
425 let drive_type = le_u32(data, base + 4);
426 let drive_serial_number = le_u32(data, base + 8);
427 let label_offset = le_u32(data, base + 12) as usize;
428
429 let volume_label = if label_offset == 0x14 {
431 let uni_off = le_u32(data, base + 16) as usize;
432 unicode_z(data, base + uni_off)
433 } else if label_offset != 0 {
434 ansi_z(data, base + label_offset)
435 } else {
436 None
437 }
438 .filter(|s| !s.is_empty());
439
440 Some(VolumeId {
441 drive_type,
442 drive_serial_number,
443 volume_label,
444 })
445}
446
447fn parse_cnrl(data: &[u8], base: usize) -> Option<CommonNetworkRelativeLink> {
449 let size = le_u32(data, base) as usize;
450 if size < 0x14 {
451 return None;
452 }
453 let flags = le_u32(data, base + 4);
454 let net_name_offset = le_u32(data, base + 8) as usize;
455 let device_name_offset = le_u32(data, base + 12) as usize;
456
457 const VALID_DEVICE: u32 = 0x1;
458
459 let net_name = if net_name_offset != 0 {
460 ansi_z(data, base + net_name_offset)
461 } else {
462 None
463 };
464 let device_name = if flags & VALID_DEVICE != 0 && device_name_offset != 0 {
465 ansi_z(data, base + device_name_offset)
466 } else {
467 None
468 };
469
470 Some(CommonNetworkRelativeLink {
471 net_name,
472 device_name,
473 })
474}
475
476fn read_sized_string(data: &[u8], off: usize, is_unicode: bool) -> (Option<String>, usize) {
479 let count = le_u16(data, off) as usize;
480 let body = off + 2;
481 if is_unicode {
482 let byte_len = count * 2;
483 let value = data
484 .get(body..body + byte_len)
485 .map(decode_utf16le)
486 .filter(|s| !s.is_empty());
487 (value, body + byte_len)
488 } else {
489 let value = data
490 .get(body..body + count)
491 .map(|s| String::from_utf8_lossy(s).into_owned())
492 .filter(|s| !s.is_empty());
493 (value, body + count)
494 }
495}
496
497fn decode_utf16le(bytes: &[u8]) -> String {
498 let units: Vec<u16> = bytes
499 .chunks_exact(2)
500 .map(|c| u16::from_le_bytes([c[0], c[1]]))
501 .collect();
502 String::from_utf16_lossy(&units)
503}
504
505fn parse_extra_data_tracker(data: &[u8], start: usize) -> Option<TrackerDataBlock> {
507 let mut off = start;
508 while off + 8 <= data.len() {
510 let block_size = le_u32(data, off) as usize;
511 if (block_size as u32) < shlink::EXTRA_DATA_TERMINAL_BLOCK_SIZE {
512 break;
513 }
514 let signature = le_u32(data, off + 4);
515 if signature == shlink::EXTRA_TRACKER_DATA_BLOCK {
516 return parse_tracker_block(data, off);
517 }
518 if block_size < 4 {
520 break; }
522 off += block_size;
523 }
524 None
525}
526
527fn parse_tracker_block(data: &[u8], base: usize) -> Option<TrackerDataBlock> {
529 let machine_id = ansi_z(data, base + 16)?;
533 let droid = DroidGuids {
534 volume: guid_string(data.get(base + 32..base + 48)?)?,
535 object: guid_string(data.get(base + 48..base + 64)?)?,
536 };
537 let birth_droid = DroidGuids {
538 volume: guid_string(data.get(base + 64..base + 80)?)?,
539 object: guid_string(data.get(base + 80..base + 96)?)?,
540 };
541 Some(TrackerDataBlock {
542 machine_id,
543 droid,
544 birth_droid,
545 })
546}
547
548#[cfg(test)]
549mod tests {
550 include!("tests.rs");
551}