#![forbid(unsafe_code)]
use forensicnomicon::shlink;
const FILETIME_UNIX_DELTA_100NS: i64 = 116_444_736_000_000_000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShellLink {
pub header: ShellLinkHeader,
pub link_target_idlist: Option<LinkTargetIdList>,
pub link_info: Option<LinkInfo>,
pub string_data: StringData,
pub tracker: Option<TrackerDataBlock>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShellLinkHeader {
pub link_flags: u32,
pub file_attributes: u32,
pub creation_time: i64,
pub access_time: i64,
pub write_time: i64,
pub file_size: u32,
pub icon_index: i32,
pub show_command: u32,
pub hotkey: u16,
}
impl ShellLinkHeader {
#[must_use]
pub fn has_flag(&self, flag: u32) -> bool {
self.link_flags & flag != 0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LinkTargetIdList {
pub raw: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LinkInfo {
pub volume_id: Option<VolumeId>,
pub local_base_path: Option<String>,
pub common_network_relative_link: Option<CommonNetworkRelativeLink>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VolumeId {
pub drive_type: u32,
pub drive_serial_number: u32,
pub volume_label: Option<String>,
}
pub mod drive_type {
pub const UNKNOWN: u32 = 0;
pub const NO_ROOT_DIR: u32 = 1;
pub const REMOVABLE: u32 = 2;
pub const FIXED: u32 = 3;
pub const REMOTE: u32 = 4;
pub const CDROM: u32 = 5;
pub const RAMDISK: u32 = 6;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommonNetworkRelativeLink {
pub net_name: Option<String>,
pub device_name: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct StringData {
pub name: Option<String>,
pub relative_path: Option<String>,
pub working_dir: Option<String>,
pub arguments: Option<String>,
pub icon_location: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TrackerDataBlock {
pub machine_id: String,
pub droid: DroidGuids,
pub birth_droid: DroidGuids,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DroidGuids {
pub volume: String,
pub object: String,
}
fn le_u16(data: &[u8], off: usize) -> u16 {
let mut b = [0u8; 2];
if let Some(s) = data.get(off..off + 2) {
b.copy_from_slice(s);
}
u16::from_le_bytes(b)
}
fn le_u32(data: &[u8], off: usize) -> u32 {
let mut b = [0u8; 4];
if let Some(s) = data.get(off..off + 4) {
b.copy_from_slice(s);
}
u32::from_le_bytes(b)
}
fn le_i32(data: &[u8], off: usize) -> i32 {
le_u32(data, off) as i32
}
fn le_u64(data: &[u8], off: usize) -> u64 {
let mut b = [0u8; 8];
if let Some(s) = data.get(off..off + 8) {
b.copy_from_slice(s);
}
u64::from_le_bytes(b)
}
fn filetime_to_unix(ft: u64) -> i64 {
if ft == 0 {
return 0;
}
((ft as i64) - FILETIME_UNIX_DELTA_100NS) / 10_000_000
}
fn guid_string(b: &[u8]) -> Option<String> {
let g = b.get(0..16)?;
Some(format!(
"{:08X}-{:04X}-{:04X}-{:02X}{:02X}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}",
u32::from_le_bytes([g[0], g[1], g[2], g[3]]),
u16::from_le_bytes([g[4], g[5]]),
u16::from_le_bytes([g[6], g[7]]),
g[8],
g[9],
g[10],
g[11],
g[12],
g[13],
g[14],
g[15],
))
}
fn ansi_z(data: &[u8], off: usize) -> Option<String> {
let slice = data.get(off..)?;
let end = slice.iter().position(|&c| c == 0).unwrap_or(slice.len());
Some(String::from_utf8_lossy(&slice[..end]).into_owned())
}
fn unicode_z(data: &[u8], off: usize) -> Option<String> {
let slice = data.get(off..)?;
let mut units = Vec::new();
let mut i = 0;
while i + 1 < slice.len() {
let u = u16::from_le_bytes([slice[i], slice[i + 1]]);
if u == 0 {
break;
}
units.push(u);
i += 2;
}
Some(String::from_utf16_lossy(&units))
}
#[must_use]
pub fn parse_shell_link(data: &[u8]) -> Option<ShellLink> {
if le_u32(data, 0) != shlink::HEADER_SIZE {
return None;
}
let clsid = guid_string(data.get(4..20)?)?;
if clsid != shlink::LINK_CLSID {
return None;
}
let link_flags = le_u32(data, 20);
let file_attributes = le_u32(data, 24);
let creation_time = filetime_to_unix(le_u64(data, 28));
let access_time = filetime_to_unix(le_u64(data, 36));
let write_time = filetime_to_unix(le_u64(data, 44));
let file_size = le_u32(data, 52);
let icon_index = le_i32(data, 56);
let show_command = le_u32(data, 60);
let hotkey = le_u16(data, 64);
let header = ShellLinkHeader {
link_flags,
file_attributes,
creation_time,
access_time,
write_time,
file_size,
icon_index,
show_command,
hotkey,
};
let mut off = shlink::HEADER_SIZE as usize;
let link_target_idlist = if header.has_flag(shlink::LINK_FLAG_HAS_LINK_TARGET_ID_LIST) {
let id_list_size = le_u16(data, off) as usize;
let blob_start = off + 2;
let raw = data
.get(blob_start..blob_start + id_list_size)
.map(<[u8]>::to_vec)
.unwrap_or_default();
off = blob_start + id_list_size;
Some(LinkTargetIdList { raw })
} else {
None
};
let link_info = if header.has_flag(shlink::LINK_FLAG_HAS_LINK_INFO) {
let info = parse_link_info(data, off);
let size = le_u32(data, off) as usize;
off += size.max(4);
info
} else {
None
};
let is_unicode = header.has_flag(shlink::LINK_FLAG_IS_UNICODE);
let mut string_data = StringData::default();
for (flag, slot) in [
(
shlink::LINK_FLAG_HAS_NAME,
&mut string_data.name as &mut Option<String>,
),
(
shlink::LINK_FLAG_HAS_RELATIVE_PATH,
&mut string_data.relative_path,
),
(
shlink::LINK_FLAG_HAS_WORKING_DIR,
&mut string_data.working_dir,
),
(shlink::LINK_FLAG_HAS_ARGUMENTS, &mut string_data.arguments),
(
shlink::LINK_FLAG_HAS_ICON_LOCATION,
&mut string_data.icon_location,
),
] {
if header.has_flag(flag) {
let (value, next) = read_sized_string(data, off, is_unicode);
*slot = value;
off = next;
}
}
let tracker = parse_extra_data_tracker(data, off);
Some(ShellLink {
header,
link_target_idlist,
link_info,
string_data,
tracker,
})
}
fn parse_link_info(data: &[u8], base: usize) -> Option<LinkInfo> {
let size = le_u32(data, base) as usize;
if size < 0x1C {
return None;
}
let header_size = le_u32(data, base + 4) as usize;
let flags = le_u32(data, base + 8);
let volume_id_offset = le_u32(data, base + 12) as usize;
let local_base_path_offset = le_u32(data, base + 16) as usize;
let cnrl_offset = le_u32(data, base + 20) as usize;
let local_base_path_offset_unicode = if header_size >= 0x24 {
le_u32(data, base + 28) as usize
} else {
0
};
const VOLUME_ID_AND_LOCAL_BASE_PATH: u32 = 0x1;
const CNRL_AND_PATH_SUFFIX: u32 = 0x2;
let volume_id = if flags & VOLUME_ID_AND_LOCAL_BASE_PATH != 0 && volume_id_offset != 0 {
parse_volume_id(data, base + volume_id_offset)
} else {
None
};
let local_base_path = if flags & VOLUME_ID_AND_LOCAL_BASE_PATH != 0 {
if local_base_path_offset_unicode != 0 {
unicode_z(data, base + local_base_path_offset_unicode)
} else if local_base_path_offset != 0 {
ansi_z(data, base + local_base_path_offset)
} else {
None
}
} else {
None
};
let common_network_relative_link = if flags & CNRL_AND_PATH_SUFFIX != 0 && cnrl_offset != 0 {
parse_cnrl(data, base + cnrl_offset)
} else {
None
};
Some(LinkInfo {
volume_id,
local_base_path,
common_network_relative_link,
})
}
fn parse_volume_id(data: &[u8], base: usize) -> Option<VolumeId> {
let size = le_u32(data, base) as usize;
if size < 0x10 {
return None;
}
let drive_type = le_u32(data, base + 4);
let drive_serial_number = le_u32(data, base + 8);
let label_offset = le_u32(data, base + 12) as usize;
let volume_label = if label_offset == 0x14 {
let uni_off = le_u32(data, base + 16) as usize;
unicode_z(data, base + uni_off)
} else if label_offset != 0 {
ansi_z(data, base + label_offset)
} else {
None
}
.filter(|s| !s.is_empty());
Some(VolumeId {
drive_type,
drive_serial_number,
volume_label,
})
}
fn parse_cnrl(data: &[u8], base: usize) -> Option<CommonNetworkRelativeLink> {
let size = le_u32(data, base) as usize;
if size < 0x14 {
return None;
}
let flags = le_u32(data, base + 4);
let net_name_offset = le_u32(data, base + 8) as usize;
let device_name_offset = le_u32(data, base + 12) as usize;
const VALID_DEVICE: u32 = 0x1;
let net_name = if net_name_offset != 0 {
ansi_z(data, base + net_name_offset)
} else {
None
};
let device_name = if flags & VALID_DEVICE != 0 && device_name_offset != 0 {
ansi_z(data, base + device_name_offset)
} else {
None
};
Some(CommonNetworkRelativeLink {
net_name,
device_name,
})
}
fn read_sized_string(data: &[u8], off: usize, is_unicode: bool) -> (Option<String>, usize) {
let count = le_u16(data, off) as usize;
let body = off + 2;
if is_unicode {
let byte_len = count * 2;
let value = data
.get(body..body + byte_len)
.map(decode_utf16le)
.filter(|s| !s.is_empty());
(value, body + byte_len)
} else {
let value = data
.get(body..body + count)
.map(|s| String::from_utf8_lossy(s).into_owned())
.filter(|s| !s.is_empty());
(value, body + count)
}
}
fn decode_utf16le(bytes: &[u8]) -> String {
let units: Vec<u16> = bytes
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
String::from_utf16_lossy(&units)
}
fn parse_extra_data_tracker(data: &[u8], start: usize) -> Option<TrackerDataBlock> {
let mut off = start;
while off + 8 <= data.len() {
let block_size = le_u32(data, off) as usize;
if (block_size as u32) < shlink::EXTRA_DATA_TERMINAL_BLOCK_SIZE {
break;
}
let signature = le_u32(data, off + 4);
if signature == shlink::EXTRA_TRACKER_DATA_BLOCK {
return parse_tracker_block(data, off);
}
if block_size < 4 {
break; }
off += block_size;
}
None
}
fn parse_tracker_block(data: &[u8], base: usize) -> Option<TrackerDataBlock> {
let machine_id = ansi_z(data, base + 16)?;
let droid = DroidGuids {
volume: guid_string(data.get(base + 32..base + 48)?)?,
object: guid_string(data.get(base + 48..base + 64)?)?,
};
let birth_droid = DroidGuids {
volume: guid_string(data.get(base + 64..base + 80)?)?,
object: guid_string(data.get(base + 80..base + 96)?)?,
};
Some(TrackerDataBlock {
machine_id,
droid,
birth_droid,
})
}
#[cfg(test)]
mod tests {
include!("tests.rs");
}