use crate::Encoding;
const AUTOIT_SIGNATURE: &[u8] = &[
0xa3, 0x48, 0x4b, 0xbe, 0x98, 0x6c, 0x4a, 0xa9, 0x99, 0x4c, 0x53, 0x0a, 0x86, 0xd6, 0x48, 0x7d,
0x41, 0x55, 0x33, 0x21,
];
const AUTOIT_SIGNATURE_ALT_GUID: &[u8] = &[
0xa3, 0x48, 0x4b, 0xbe, 0x98, 0x6c, 0xa9, 0x4a, 0x99, 0x4c, 0x53, 0x0a, 0x86, 0xd6, 0x48, 0x7d,
0x41, 0x55, 0x33, 0x21,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RawScan {
encoding: Encoding,
signature_offset: usize,
version_offset: usize,
payload_offset: usize,
}
impl RawScan {
#[must_use]
pub const fn encoding(self) -> Encoding {
self.encoding
}
#[must_use]
pub const fn signature_offset(self) -> usize {
self.signature_offset
}
#[must_use]
pub const fn version_offset(self) -> usize {
self.version_offset
}
#[must_use]
pub const fn payload_offset(self) -> usize {
self.payload_offset
}
}
#[must_use]
pub fn scan(data: &[u8]) -> Option<RawScan> {
let signature_offset = find_signature(data)?;
let version_offset = signature_offset.checked_add(AUTOIT_SIGNATURE.len())?;
let version = data.get(version_offset..)?;
let encoding = if version.starts_with(b"EA05") {
Encoding::Ea05
} else if version.starts_with(b"EA06") {
Encoding::Ea06
} else if version.starts_with(b"JB01") {
Encoding::Jb01
} else {
return None;
};
let payload_offset = payload_offset_after_version(data, version_offset, encoding)?;
Some(RawScan {
encoding,
signature_offset,
version_offset,
payload_offset,
})
}
pub fn payload_offset_after_version(
data: &[u8],
version_offset: usize,
encoding: Encoding,
) -> Option<usize> {
let direct_offset = version_offset.checked_add(4)?;
match encoding {
Encoding::Ea05 | Encoding::Ea06 => salted_payload_offset(data, direct_offset, encoding),
Encoding::Ea04 | Encoding::Jb01 => Some(direct_offset),
}
}
fn salted_payload_offset(data: &[u8], direct_offset: usize, encoding: Encoding) -> Option<usize> {
if first_file_record_matches(data, direct_offset, encoding) {
return Some(direct_offset);
}
let hashed_offset = direct_offset.checked_add(16)?;
if first_file_record_matches(data, hashed_offset, encoding) {
Some(hashed_offset)
} else {
Some(direct_offset)
}
}
pub fn first_file_record_matches(data: &[u8], offset: usize, encoding: Encoding) -> bool {
let Some(end) = offset.checked_add(4) else {
return false;
};
let Some(marker) = data.get(offset..end) else {
return false;
};
let decrypted = match encoding {
Encoding::Ea04 | Encoding::Ea05 | Encoding::Jb01 => {
crate::crypto::mt::decrypt(marker, 0x16fa)
}
Encoding::Ea06 => crate::crypto::lame::decrypt(marker, 0x18ee),
};
decrypted.is_some_and(|bytes| bytes == b"FILE")
}
fn find_signature(data: &[u8]) -> Option<usize> {
data.windows(AUTOIT_SIGNATURE.len()).position(|candidate| {
candidate == AUTOIT_SIGNATURE || candidate == AUTOIT_SIGNATURE_ALT_GUID
})
}
const ENCRYPTED_FILE_MARKER: &[u8; 4] = &[0xff, 0x6d, 0xb0, 0xce];
#[must_use]
pub fn scan_ea04_stream(data: &[u8]) -> Option<usize> {
let mut from = 0usize;
while let Some(slice) = data.get(from..) {
let rel = slice
.windows(ENCRYPTED_FILE_MARKER.len())
.position(|candidate| candidate == ENCRYPTED_FILE_MARKER)?;
let offset = from.checked_add(rel)?;
if ea04_first_record_is_plausible(data, offset) {
return Some(offset);
}
from = offset.checked_add(1)?;
}
None
}
fn ea04_first_record_is_plausible(data: &[u8], offset: usize) -> bool {
let Some(subtype_len_offset) = offset.checked_add(4) else {
return false;
};
let Some(len_end) = subtype_len_offset.checked_add(4) else {
return false;
};
let Some(len_bytes) = data.get(subtype_len_offset..len_end) else {
return false;
};
let Ok(len_array) = <[u8; 4]>::try_from(len_bytes) else {
return false;
};
let subtype_len = u32::from_le_bytes(len_array) ^ 0x29bc;
if !(1..=64).contains(&subtype_len) {
return false;
}
let Ok(len) = usize::try_from(subtype_len) else {
return false;
};
let subtype_start = len_end;
let Some(subtype_end) = subtype_start.checked_add(len) else {
return false;
};
let Some(encrypted) = data.get(subtype_start..subtype_end) else {
return false;
};
let key = 0xa25e_u32.wrapping_add(subtype_len);
crate::crypto::mt::decrypt(encrypted, key).is_some_and(|subtype| is_known_subtype(&subtype))
}
fn is_known_subtype(subtype: &[u8]) -> bool {
matches!(
subtype,
b">AUTOIT SCRIPT<"
| b">AUTOIT UNICODE SCRIPT<"
| b">>>AUTOIT SCRIPT<<<"
| b">>>AUTOIT NO CMDEXECUTE<<<"
| b">AUTOHOTKEY SCRIPT<"
| b">AHK WITH ICON<"
)
}
#[must_use]
pub fn scan_jb01_stream(data: &[u8]) -> Option<usize> {
let guid = AUTOIT_SIGNATURE.get(0..16)?;
let mut from = 0usize;
while let Some(slice) = data.get(from..) {
let rel = slice.windows(16).position(|candidate| candidate == guid)?;
let guid_offset = from.checked_add(rel)?;
if let Some(stream_offset) = jb01_stream_after_guid(data, guid_offset)
&& ea04_first_record_is_plausible(data, stream_offset)
{
return Some(stream_offset);
}
from = guid_offset.checked_add(1)?;
}
None
}
fn jb01_stream_after_guid(data: &[u8], guid_offset: usize) -> Option<usize> {
let marker_offset = guid_offset.checked_add(16)?;
if *data.get(marker_offset)? != 3 {
return None;
}
let len_offset = marker_offset.checked_add(1)?;
let len_bytes = data.get(len_offset..len_offset.checked_add(4)?)?;
let passphrase_len = u32::from_le_bytes(<[u8; 4]>::try_from(len_bytes).ok()?) ^ 0xfac1;
let passphrase_len = usize::try_from(passphrase_len).ok()?;
if passphrase_len > 1024 {
return None;
}
len_offset.checked_add(4)?.checked_add(passphrase_len)
}