use std::io::Cursor;
use winreg_core::hive::Hive;
use winreg_core::key::filetime_to_datetime;
use forensicnomicon::appcompatcache as fmt;
#[derive(Debug, Clone, serde::Serialize)]
pub struct ShimcacheEntry {
pub path: String,
pub last_modified: Option<String>,
pub raw_size: usize,
pub entry_index: usize,
}
const APPCOMPAT_SUFFIX: &str = "Control\\Session Manager\\AppCompatCache";
const APPCOMPAT_VALUE: &str = "AppCompatCache";
const WIN8_HEADER_SIG: u8 = 0x80;
#[derive(Clone, Copy)]
enum EntryBodyLayout {
Win10,
Win8x,
}
pub fn parse(hive: &Hive<Cursor<Vec<u8>>>) -> Vec<ShimcacheEntry> {
let current = hive
.open_key("Select")
.ok()
.flatten()
.and_then(|sel| sel.value("Current").ok().flatten())
.and_then(|v| v.raw_data().ok())
.filter(|d| d.len() >= 4)
.map_or(1u32, |d| u32::from_le_bytes([d[0], d[1], d[2], d[3]]));
let candidates = [
format!("CurrentControlSet\\{APPCOMPAT_SUFFIX}"),
format!("ControlSet{current:03}\\{APPCOMPAT_SUFFIX}"),
format!("ControlSet001\\{APPCOMPAT_SUFFIX}"),
];
let key = match candidates
.iter()
.find_map(|p| hive.open_key(p).ok().flatten())
{
Some(k) => k,
None => return Vec::new(),
};
let blob: Vec<u8> = match key.value(APPCOMPAT_VALUE) {
Ok(Some(v)) => match v.raw_data() {
Ok(d) => d,
Err(_) => return Vec::new(),
},
_ => return Vec::new(),
};
let raw_size = blob.len();
if raw_size < 4 {
return Vec::new();
}
let sig = u32::from_le_bytes([blob[0], blob[1], blob[2], blob[3]]);
if sig == fmt::WIN10_1507_HEADER_LEN || sig == fmt::WIN10_1607_HEADER_LEN {
return parse_win10_entries(&blob, sig as usize, raw_size, b"10ts", EntryBodyLayout::Win10);
}
if sig == fmt::ENTRY_MARKER_WIN81_WIN10_U32 {
return parse_win10_entries(&blob, 0, raw_size, b"10ts", EntryBodyLayout::Win10);
}
if blob.len() >= fmt::WIN8X_ENTRY_STREAM_OFFSET + 4 {
let marker = &blob[fmt::WIN8X_ENTRY_STREAM_OFFSET..fmt::WIN8X_ENTRY_STREAM_OFFSET + 4];
if marker == fmt::ENTRY_MARKER_WIN80 || marker == fmt::ENTRY_MARKER_WIN81_WIN10 {
return parse_win10_entries(
&blob,
fmt::WIN8X_ENTRY_STREAM_OFFSET,
raw_size,
marker,
EntryBodyLayout::Win8x,
);
}
}
if blob[0] == WIN8_HEADER_SIG {
return parse_win10(&blob, raw_size);
}
if let Some(pos) = blob.windows(4).position(|w| w == fmt::ENTRY_MARKER_WIN81_WIN10) {
return parse_win10_entries(&blob, pos, raw_size, b"10ts", EntryBodyLayout::Win10);
}
vec![ShimcacheEntry {
path: String::new(),
last_modified: None,
raw_size,
entry_index: 0,
}]
}
fn parse_win10_entries(
blob: &[u8],
start: usize,
raw_size: usize,
entry_sig: &[u8],
layout: EntryBodyLayout,
) -> Vec<ShimcacheEntry> {
let mut entries = Vec::new();
let mut offset = start;
let mut entry_index = 0;
while offset + fmt::ENTRY_FRAMING_LEN <= blob.len() {
if &blob[offset..offset + 4] != entry_sig {
break;
}
let ce_data_size =
u32::from_le_bytes([blob[offset + 8], blob[offset + 9], blob[offset + 10], blob[offset + 11]])
as usize;
let body_start = offset + fmt::ENTRY_FRAMING_LEN;
let body_end = match body_start.checked_add(ce_data_size) {
Some(e) if e <= blob.len() => e,
_ => break,
};
let (path, last_modified) = decode_win10_entry_body(&blob[body_start..body_end], layout);
entries.push(ShimcacheEntry {
path,
last_modified,
raw_size,
entry_index,
});
offset = body_end;
entry_index += 1;
}
entries
}
fn decode_win10_entry_body(body: &[u8], layout: EntryBodyLayout) -> (String, Option<String>) {
if body.len() < 2 {
return (String::new(), None);
}
let path_size = u16::from_le_bytes([body[0], body[1]]) as usize;
let path_end = 2 + path_size;
let path = if path_size > 0 && path_end <= body.len() {
decode_utf16le(&body[2..path_end])
} else {
String::new()
};
let ft_offset = match layout {
EntryBodyLayout::Win10 => path_end.checked_add(fmt::WIN10_PATH_TO_FILETIME),
EntryBodyLayout::Win8x => {
if path_end + 2 <= body.len() {
let package_len = u16::from_le_bytes([body[path_end], body[path_end + 1]]) as usize;
path_end.checked_add(2 + package_len + fmt::WIN8X_PATH_TO_FILETIME_FIXED)
} else {
None
}
}
};
let last_modified = ft_offset
.filter(|&o| o.checked_add(8).is_some_and(|end| end <= body.len()))
.and_then(|o| {
let ft = winreg_core::bytes::le_u64(body, o);
filetime_to_datetime(ft).map(|dt| dt.format("%Y-%m-%dT%H:%M:%SZ").to_string())
});
(path, last_modified)
}
fn parse_win10(blob: &[u8], raw_size: usize) -> Vec<ShimcacheEntry> {
const HEADER_SIZE: usize = 128;
if blob.len() < HEADER_SIZE {
return Vec::new();
}
let entry_count = u32::from_le_bytes([blob[4], blob[5], blob[6], blob[7]]) as usize;
if entry_count == 0 {
return Vec::new();
}
let mut entries = Vec::with_capacity(entry_count);
let mut offset = HEADER_SIZE;
let mut entry_index = 0;
while offset + 8 <= blob.len() && entry_index < entry_count {
let entry_sig = u32::from_le_bytes([
blob[offset],
blob[offset + 1],
blob[offset + 2],
blob[offset + 3],
]);
if entry_sig != fmt::ENTRY_MARKER_WIN81_WIN10_U32 {
break;
}
let entry_data_len = u32::from_le_bytes([
blob[offset + 4],
blob[offset + 5],
blob[offset + 6],
blob[offset + 7],
]) as usize;
let body_start = offset + 8;
let body_end = body_start + entry_data_len;
if body_end > blob.len() {
break;
}
let body = &blob[body_start..body_end];
let (path, last_modified) = decode_entry_body(body);
entries.push(ShimcacheEntry {
path,
last_modified,
raw_size,
entry_index,
});
offset = body_end;
entry_index += 1;
}
entries
}
fn decode_entry_body(body: &[u8]) -> (String, Option<String>) {
if body.len() < 2 {
return (String::new(), None);
}
let path_len = u16::from_le_bytes([body[0], body[1]]) as usize;
let last_modified: Option<String> = if body.len() >= 16 {
let ft = winreg_core::bytes::le_u64(&body[..], 8);
filetime_to_datetime(ft).map(|dt| dt.format("%Y-%m-%dT%H:%M:%SZ").to_string())
} else {
None
};
let path: String = if path_len == 0 || body.len() < 18 {
String::new()
} else {
let path_offset = u16::from_le_bytes([body[16], body[17]]) as usize;
let path_end = path_offset + path_len;
if path_offset < body.len() && path_end <= body.len() {
decode_utf16le(&body[path_offset..path_end])
} else {
String::new()
}
};
(path, last_modified)
}
fn decode_utf16le(data: &[u8]) -> String {
let u16s: Vec<u16> = data
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
let trimmed: &[u16] = match u16s.iter().position(|&c| c == 0) {
Some(pos) => &u16s[..pos],
None => &u16s,
};
String::from_utf16_lossy(trimmed).to_owned()
}