pub mod error;
pub mod format;
pub mod au3;
pub mod decompress;
pub mod script;
pub mod strings;
pub mod token;
mod container;
mod crypto;
mod util;
pub use au3::{
CompressionProfile, DecompressionStatus, EncryptionProfile, Record, RecordParseDiagnostic,
RecordParseReport, RecordProfile,
};
pub use error::{Error, RecognitionFailure};
pub use format::{
ContainerInfo, KnownSubtype, Observation, ObservationLog, PackedMarkerInfo,
PeScriptResourceInfo, VersionMarkerInfo,
};
pub use script::{Script, ScriptDecodeError, ScriptKind, ScriptText, ScriptTextEncoding};
pub use strings::{StringEncoding, StringFinding};
pub use token::{Token, TokenError, TokenStream};
#[derive(Debug, Clone)]
pub struct AutoItBinary {
input_kind: InputKind,
encoding: Option<Encoding>,
container: ContainerInfo,
observations: ObservationLog,
records: Vec<Record>,
record_diagnostics: Vec<RecordParseDiagnostic>,
scripts: Vec<Script>,
artifacts: Vec<Artifact>,
strings: Vec<StringFinding>,
}
impl AutoItBinary {
pub fn try_parse(data: &[u8]) -> Result<Self, Error> {
let raw = container::raw::scan(data);
let pe_resource = container::pe::find_script_resource(data);
let loose_version = loose_autoit_version_marker(data);
let packed_marker = packed_marker(data);
let (ea04_stream_offset, jb01_stream_offset) = if raw.is_none() && pe_resource.is_none() {
(
container::raw::scan_ea04_stream(data),
container::raw::scan_jb01_stream(data),
)
} else {
(None, None)
};
if raw.is_none()
&& pe_resource.is_none()
&& ea04_stream_offset.is_none()
&& jb01_stream_offset.is_none()
&& !(util::has_mz_header(data) && packed_marker.is_some() && loose_version.is_some())
{
return Err(Error::not_recognized());
};
let input_kind = if util::has_mz_header(data) {
InputKind::Pe
} else if raw.is_some_and(|candidate| candidate.signature_offset() == 0) {
InputKind::A3x
} else {
InputKind::RawStream
};
let mut observations = ObservationLog::new();
if input_kind == InputKind::Pe {
observations.push(Observation::OuterPeHeaderFound);
}
if let Some(marker) = packed_marker {
observations.push(Observation::PackedMarkerFound {
name: marker.name,
offset: marker.offset,
});
}
if let Some(resource) = pe_resource {
observations.push(Observation::PeScriptResourceFound {
type_id: resource.resource_type_id(),
type_name: resource.resource_type_name(),
name: resource.resource_name(),
language_id: resource.language_id(),
rva: resource.rva(),
offset: resource.file_offset(),
size: resource.size(),
});
if let Some(offset) = resource.stream_offset() {
observations.push(Observation::PayloadStreamStartFound { offset });
if first_file_record_matches(data, offset, Encoding::Ea06) {
observations.push(Observation::FirstFileRecordDecrypted {
encoding: Encoding::Ea06,
offset,
});
}
}
}
if let Some(raw) = raw {
observations.push(Observation::AutoItSignatureFound {
offset: raw.signature_offset(),
});
observations.push(Observation::VersionMarkerFound {
encoding: raw.encoding(),
offset: raw.version_offset(),
});
observations.push(Observation::PayloadStreamStartFound {
offset: raw.payload_offset(),
});
if first_file_record_matches(data, raw.payload_offset(), raw.encoding()) {
observations.push(Observation::FirstFileRecordDecrypted {
encoding: raw.encoding(),
offset: raw.payload_offset(),
});
}
} else if let Some(offset) = jb01_stream_offset {
observations.push(Observation::PayloadStreamStartFound { offset });
observations.push(Observation::FirstFileRecordDecrypted {
encoding: Encoding::Jb01,
offset,
});
} else if let Some(offset) = ea04_stream_offset {
observations.push(Observation::PayloadStreamStartFound { offset });
observations.push(Observation::FirstFileRecordDecrypted {
encoding: Encoding::Ea04,
offset,
});
} else if let Some(version) = loose_version {
observations.push(Observation::VersionMarkerFound {
encoding: version.encoding,
offset: version.offset,
});
}
let encoding = raw
.map(container::raw::RawScan::encoding)
.or_else(|| first_file_encoding(observations.entries()))
.or_else(|| loose_version.map(|version| version.encoding));
let (records, record_diagnostics) = encoding
.zip(first_stream_offset(observations.entries()))
.map_or_else(
|| Ok::<(Vec<Record>, Vec<RecordParseDiagnostic>), Error>((Vec::new(), Vec::new())),
|(record_encoding, offset)| {
let record_report = au3::parse_records_partial(
data,
offset,
record_encoding,
au3::Limits::default(),
)?;
let diagnostics = match record_report.diagnostic() {
Some(diagnostic) => vec![diagnostic],
None => Vec::new(),
};
Ok((record_report.into_records(), diagnostics))
},
)?;
if let Some(record) = records.first()
&& let Some(subtype) = known_subtype(record.subtype())
{
observations.push(Observation::KnownSubtypeDecrypted {
encoding: record.profile().encoding,
offset: record.offset(),
subtype,
});
}
let scripts = script::recover_scripts(records.as_slice());
let artifacts = recover_artifacts(records.as_slice(), scripts.as_slice());
let strings = strings::extract_from_records(records.as_slice(), strings::Limits::default());
Ok(Self {
input_kind,
encoding,
container: ContainerInfo::from_observations(
input_kind,
encoding,
observations.entries(),
),
scripts,
artifacts,
strings,
observations,
records,
record_diagnostics,
})
}
#[must_use]
pub const fn input_kind(&self) -> InputKind {
self.input_kind
}
#[must_use]
pub const fn encoding(&self) -> Option<Encoding> {
self.encoding
}
#[must_use]
pub const fn container(&self) -> &ContainerInfo {
&self.container
}
#[must_use]
pub const fn observations(&self) -> &ObservationLog {
&self.observations
}
#[must_use]
pub fn records(&self) -> &[Record] {
self.records.as_slice()
}
#[must_use]
pub fn record_diagnostics(&self) -> &[RecordParseDiagnostic] {
self.record_diagnostics.as_slice()
}
#[must_use]
pub fn scripts(&self) -> &[Script] {
self.scripts.as_slice()
}
#[must_use]
pub fn artifacts(&self) -> &[Artifact] {
self.artifacts.as_slice()
}
#[must_use]
pub fn strings(&self) -> &[StringFinding] {
self.strings.as_slice()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Artifact {
record_index: usize,
subtype: String,
name: String,
bytes: Vec<u8>,
creation_time: u64,
last_write_time: u64,
checksum_valid: bool,
decompression_status: DecompressionStatus,
}
impl Artifact {
#[must_use]
pub const fn record_index(&self) -> usize {
self.record_index
}
#[must_use]
pub fn subtype(&self) -> &str {
self.subtype.as_str()
}
#[must_use]
pub fn name(&self) -> &str {
self.name.as_str()
}
#[must_use]
pub fn bytes(&self) -> &[u8] {
self.bytes.as_slice()
}
#[must_use]
pub const fn creation_time(&self) -> u64 {
self.creation_time
}
#[must_use]
pub const fn last_write_time(&self) -> u64 {
self.last_write_time
}
#[must_use]
pub const fn checksum_valid(&self) -> bool {
self.checksum_valid
}
#[must_use]
pub const fn decompression_status(&self) -> DecompressionStatus {
self.decompression_status
}
}
fn recover_artifacts(records: &[Record], scripts: &[Script]) -> Vec<Artifact> {
records
.iter()
.filter(|record| {
!scripts
.iter()
.any(|script| script.record_index() == record.index())
})
.map(|record| Artifact {
record_index: record.index(),
subtype: record.subtype().to_string(),
name: record.name().to_string(),
bytes: record.payload_data().to_vec(),
creation_time: record.creation_time(),
last_write_time: record.last_write_time(),
checksum_valid: record.checksum_valid(),
decompression_status: record.decompression_status(),
})
.collect()
}
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 => crypto::mt::decrypt(marker, 0x16fa),
Encoding::Ea06 => crypto::lame::decrypt(marker, 0x18ee),
};
decrypted.is_some_and(|bytes| bytes == b"FILE")
}
fn first_stream_offset(observations: &[Observation]) -> Option<usize> {
observations
.iter()
.find_map(|observation| match observation {
Observation::PayloadStreamStartFound { offset } => Some(*offset),
_ => None,
})
}
fn first_file_encoding(observations: &[Observation]) -> Option<Encoding> {
observations
.iter()
.find_map(|observation| match observation {
Observation::FirstFileRecordDecrypted { encoding, .. } => Some(*encoding),
_ => None,
})
}
fn known_subtype(subtype: &str) -> Option<KnownSubtype> {
match subtype {
">>>AUTOIT SCRIPT<<<" => Some(KnownSubtype::TokenizedScript),
">AUTOIT UNICODE SCRIPT<" => Some(KnownSubtype::UnicodeScript),
">AUTOIT SCRIPT<" => Some(KnownSubtype::PlainScript),
_ => None,
}
}
#[derive(Debug, Clone, Copy)]
struct LooseVersionMarker {
encoding: Encoding,
offset: usize,
}
#[derive(Debug, Clone, Copy)]
struct PackedMarker {
name: &'static str,
offset: usize,
}
fn loose_autoit_version_marker(data: &[u8]) -> Option<LooseVersionMarker> {
if find_bytes(data, b"AU3!").is_none()
&& find_bytes(data, b"AutoIt").is_none()
&& find_bytes(data, b"SCRIPT").is_none()
{
return None;
}
[
(Encoding::Ea06, b"EA06".as_slice()),
(Encoding::Ea05, b"EA05".as_slice()),
(Encoding::Jb01, b"JB01".as_slice()),
]
.into_iter()
.find_map(|(encoding, marker)| {
find_bytes(data, marker).map(|offset| LooseVersionMarker { encoding, offset })
})
}
fn packed_marker(data: &[u8]) -> Option<PackedMarker> {
find_bytes(data, b"UPX").map(|offset| PackedMarker {
name: "UPX",
offset,
})
}
fn find_bytes(data: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() || needle.len() > data.len() {
return None;
}
data.windows(needle.len())
.position(|candidate| candidate == needle)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InputKind {
Pe,
A3x,
RawStream,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Encoding {
Ea04,
Ea05,
Ea06,
Jb01,
}
#[cfg(test)]
mod tests {
use super::*;
const PREFIX: [u8; 20] = [
0xa3, 0x48, 0x4b, 0xbe, 0x98, 0x6c, 0x4a, 0xa9, 0x99, 0x4c, 0x53, 0x0a, 0x86, 0xd6, 0x48,
0x7d, 0x41, 0x55, 0x33, 0x21,
];
#[test]
fn scans_raw_ea06_signature() -> Result<(), String> {
let mut data = Vec::from(PREFIX);
data.extend_from_slice(b"EA06");
let binary = AutoItBinary::try_parse(&data).map_err(|err| err.to_string())?;
check_eq(binary.input_kind(), InputKind::A3x, "input kind")?;
check_eq(binary.encoding(), Some(Encoding::Ea06), "encoding")?;
check_eq(
binary.container().autoit_signature_offset(),
Some(0),
"signature offset",
)?;
check_eq(
binary.container().version_marker(),
Some(VersionMarkerInfo {
encoding: Encoding::Ea06,
offset: 20,
}),
"version marker",
)?;
check_eq(
binary.container().payload_stream_offsets(),
[24usize].as_slice(),
"payload offsets",
)?;
check_eq(
binary.observations().entries().len(),
3,
"observation count",
)
}
#[test]
fn validates_raw_ea06_first_file_record() -> Result<(), String> {
let mut data = Vec::from(PREFIX);
data.extend_from_slice(b"EA06");
append_ea06_record(&mut data, ">>>AUTOIT SCRIPT<<<", "main.au3", b"payload")?;
let binary = AutoItBinary::try_parse(&data).map_err(|err| err.to_string())?;
check_eq(binary.encoding(), Some(Encoding::Ea06), "encoding")?;
check_eq(binary.records().len(), 1, "record count")?;
let found = binary.observations().entries().iter().any(|observation| {
matches!(
observation,
Observation::FirstFileRecordDecrypted {
encoding: Encoding::Ea06,
offset: 24
}
)
});
if found {
let subtype_found = binary.observations().entries().iter().any(|observation| {
matches!(
observation,
Observation::KnownSubtypeDecrypted {
encoding: Encoding::Ea06,
offset: 24,
subtype: KnownSubtype::TokenizedScript
}
)
});
if subtype_found {
Ok(())
} else {
Err("missing known subtype observation".to_string())
}
} else {
Err("missing first FILE record observation".to_string())
}
}
#[test]
fn preserves_records_when_later_record_is_truncated() -> Result<(), String> {
let mut data = Vec::from(PREFIX);
data.extend_from_slice(b"EA06");
append_ea06_record(&mut data, ">AUTOIT SCRIPT<", "main.au3", b"payload")?;
let truncated_offset = data.len();
append_encrypted(&mut data, b"FILE", 0x18ee)?;
let binary = AutoItBinary::try_parse(&data).map_err(|err| err.to_string())?;
check_eq(binary.records().len(), 1, "record count")?;
check_eq(binary.scripts().len(), 1, "script count")?;
check_eq(
binary.record_diagnostics(),
[RecordParseDiagnostic {
record_index: 1,
offset: truncated_offset,
reason: RecognitionFailure::Truncated,
}]
.as_slice(),
"diagnostics",
)
}
#[test]
fn scans_pe_wrapped_signature() -> Result<(), String> {
let mut data = Vec::from(b"MZ");
data.extend_from_slice(&[0; 8]);
data.extend_from_slice(&PREFIX);
data.extend_from_slice(b"EA05");
let binary = AutoItBinary::try_parse(&data).map_err(|err| err.to_string())?;
check_eq(binary.input_kind(), InputKind::Pe, "input kind")?;
check_eq(binary.encoding(), Some(Encoding::Ea05), "encoding")?;
check_eq(
binary.observations().entries().len(),
4,
"observation count",
)
}
#[test]
fn rejects_unknown_input() -> Result<(), String> {
let Err(err) = AutoItBinary::try_parse(b"not autoit") else {
return Err("unexpected parse success".to_string());
};
check_eq(
err.recognition_failure(),
Some(RecognitionFailure::NotRecognized),
"recognition failure",
)
}
fn check_eq<T>(actual: T, expected: T, context: &str) -> Result<(), String>
where
T: core::fmt::Debug + PartialEq,
{
if actual == expected {
Ok(())
} else {
Err(format!("{context}: got {actual:?}, expected {expected:?}"))
}
}
fn append_ea06_record(
out: &mut Vec<u8>,
subtype: &str,
name: &str,
data: &[u8],
) -> Result<(), String> {
append_encrypted(out, b"FILE", 0x18ee)?;
append_xored_u32(out, utf16_len(subtype)?, 0xadbc);
append_encrypted_utf16(out, subtype, 0xb33f)?;
append_xored_u32(out, utf16_len(name)?, 0xf820);
append_encrypted_utf16(out, name, 0xf479)?;
out.push(0);
let data_len = u32::try_from(data.len()).map_err(|err| err.to_string())?;
append_xored_u32(out, data_len, 0x87bc);
append_xored_u32(out, data_len, 0x87bc);
append_xored_u32(out, 0, 0xa685);
append_u64(out, 0);
append_u64(out, 0);
append_encrypted(out, data, 0x2477)
}
fn append_encrypted_utf16(out: &mut Vec<u8>, value: &str, key_base: u32) -> Result<(), String> {
let char_len = utf16_len(value)?;
let key = key_base.wrapping_add(char_len);
let mut bytes = Vec::new();
for unit in value.encode_utf16() {
bytes.extend_from_slice(&unit.to_le_bytes());
}
append_encrypted(out, bytes.as_slice(), key)
}
fn append_encrypted(out: &mut Vec<u8>, plain: &[u8], key: u32) -> Result<(), String> {
let encrypted = crypto::lame::decrypt(plain, key)
.ok_or_else(|| "EA06 encryption failed".to_string())?;
out.extend_from_slice(encrypted.as_slice());
Ok(())
}
fn append_xored_u32(out: &mut Vec<u8>, value: u32, mask: u32) {
append_u32(out, value ^ mask);
}
fn append_u32(out: &mut Vec<u8>, value: u32) {
out.extend_from_slice(&value.to_le_bytes());
}
fn append_u64(out: &mut Vec<u8>, value: u64) {
out.extend_from_slice(&value.to_le_bytes());
}
fn utf16_len(value: &str) -> Result<u32, String> {
u32::try_from(value.encode_utf16().count()).map_err(|err| err.to_string())
}
}