1pub mod error;
64pub mod format;
65
66pub mod au3;
67pub mod decompress;
68pub mod script;
69pub mod strings;
70pub mod token;
71
72mod container;
73mod crypto;
74mod util;
75
76pub use au3::{
77 CompressionProfile, DecompressionStatus, EncryptionProfile, Record, RecordParseDiagnostic,
78 RecordParseReport, RecordProfile,
79};
80pub use error::{Error, RecognitionFailure};
81pub use format::{
82 ContainerInfo, KnownSubtype, Observation, ObservationLog, PackedMarkerInfo,
83 PeScriptResourceInfo, VersionMarkerInfo,
84};
85pub use script::{Script, ScriptDecodeError, ScriptKind, ScriptText, ScriptTextEncoding};
86pub use strings::{StringEncoding, StringFinding};
87pub use token::{Token, TokenError, TokenStream};
88
89#[derive(Debug, Clone)]
98pub struct AutoItBinary {
99 input_kind: InputKind,
100 encoding: Option<Encoding>,
101 container: ContainerInfo,
102 observations: ObservationLog,
103 records: Vec<Record>,
104 record_diagnostics: Vec<RecordParseDiagnostic>,
105 scripts: Vec<Script>,
106 artifacts: Vec<Artifact>,
107 strings: Vec<StringFinding>,
108}
109
110impl AutoItBinary {
111 pub fn try_parse(data: &[u8]) -> Result<Self, Error> {
127 let raw = container::raw::scan(data);
128 let pe_resource = container::pe::find_script_resource(data);
129 let loose_version = loose_autoit_version_marker(data);
130 let packed_marker = packed_marker(data);
131 let (ea04_stream_offset, jb01_stream_offset) = if raw.is_none() && pe_resource.is_none() {
135 (
136 container::raw::scan_ea04_stream(data),
137 container::raw::scan_jb01_stream(data),
138 )
139 } else {
140 (None, None)
141 };
142 if raw.is_none()
143 && pe_resource.is_none()
144 && ea04_stream_offset.is_none()
145 && jb01_stream_offset.is_none()
146 && !(util::has_mz_header(data) && packed_marker.is_some() && loose_version.is_some())
147 {
148 return Err(Error::not_recognized());
149 };
150
151 let input_kind = if util::has_mz_header(data) {
152 InputKind::Pe
153 } else if raw.is_some_and(|candidate| candidate.signature_offset() == 0) {
154 InputKind::A3x
155 } else {
156 InputKind::RawStream
157 };
158
159 let mut observations = ObservationLog::new();
160 if input_kind == InputKind::Pe {
161 observations.push(Observation::OuterPeHeaderFound);
162 }
163 if let Some(marker) = packed_marker {
164 observations.push(Observation::PackedMarkerFound {
165 name: marker.name,
166 offset: marker.offset,
167 });
168 }
169 if let Some(resource) = pe_resource {
170 observations.push(Observation::PeScriptResourceFound {
171 type_id: resource.resource_type_id(),
172 type_name: resource.resource_type_name(),
173 name: resource.resource_name(),
174 language_id: resource.language_id(),
175 rva: resource.rva(),
176 offset: resource.file_offset(),
177 size: resource.size(),
178 });
179 if let Some(offset) = resource.stream_offset() {
180 observations.push(Observation::PayloadStreamStartFound { offset });
181 if first_file_record_matches(data, offset, Encoding::Ea06) {
182 observations.push(Observation::FirstFileRecordDecrypted {
183 encoding: Encoding::Ea06,
184 offset,
185 });
186 }
187 }
188 }
189 if let Some(raw) = raw {
190 observations.push(Observation::AutoItSignatureFound {
191 offset: raw.signature_offset(),
192 });
193 observations.push(Observation::VersionMarkerFound {
194 encoding: raw.encoding(),
195 offset: raw.version_offset(),
196 });
197 observations.push(Observation::PayloadStreamStartFound {
198 offset: raw.payload_offset(),
199 });
200 if first_file_record_matches(data, raw.payload_offset(), raw.encoding()) {
201 observations.push(Observation::FirstFileRecordDecrypted {
202 encoding: raw.encoding(),
203 offset: raw.payload_offset(),
204 });
205 }
206 } else if let Some(offset) = jb01_stream_offset {
207 observations.push(Observation::PayloadStreamStartFound { offset });
211 observations.push(Observation::FirstFileRecordDecrypted {
212 encoding: Encoding::Jb01,
213 offset,
214 });
215 } else if let Some(offset) = ea04_stream_offset {
216 observations.push(Observation::PayloadStreamStartFound { offset });
217 observations.push(Observation::FirstFileRecordDecrypted {
218 encoding: Encoding::Ea04,
219 offset,
220 });
221 } else if let Some(version) = loose_version {
222 observations.push(Observation::VersionMarkerFound {
223 encoding: version.encoding,
224 offset: version.offset,
225 });
226 }
227
228 let encoding = raw
229 .map(container::raw::RawScan::encoding)
230 .or_else(|| first_file_encoding(observations.entries()))
231 .or_else(|| loose_version.map(|version| version.encoding));
232
233 let (records, record_diagnostics) = encoding
234 .zip(first_stream_offset(observations.entries()))
235 .map_or_else(
236 || Ok::<(Vec<Record>, Vec<RecordParseDiagnostic>), Error>((Vec::new(), Vec::new())),
237 |(record_encoding, offset)| {
238 let record_report = au3::parse_records_partial(
239 data,
240 offset,
241 record_encoding,
242 au3::Limits::default(),
243 )?;
244 let diagnostics = match record_report.diagnostic() {
245 Some(diagnostic) => vec![diagnostic],
246 None => Vec::new(),
247 };
248 Ok((record_report.into_records(), diagnostics))
249 },
250 )?;
251 if let Some(record) = records.first()
252 && let Some(subtype) = known_subtype(record.subtype())
253 {
254 observations.push(Observation::KnownSubtypeDecrypted {
255 encoding: record.profile().encoding,
256 offset: record.offset(),
257 subtype,
258 });
259 }
260
261 let scripts = script::recover_scripts(records.as_slice());
262 let artifacts = recover_artifacts(records.as_slice(), scripts.as_slice());
263 let strings = strings::extract_from_records(records.as_slice(), strings::Limits::default());
264
265 Ok(Self {
266 input_kind,
267 encoding,
268 container: ContainerInfo::from_observations(
269 input_kind,
270 encoding,
271 observations.entries(),
272 ),
273 scripts,
274 artifacts,
275 strings,
276 observations,
277 records,
278 record_diagnostics,
279 })
280 }
281
282 #[must_use]
289 pub const fn input_kind(&self) -> InputKind {
290 self.input_kind
291 }
292
293 #[must_use]
300 pub const fn encoding(&self) -> Option<Encoding> {
301 self.encoding
302 }
303
304 #[must_use]
310 pub const fn container(&self) -> &ContainerInfo {
311 &self.container
312 }
313
314 #[must_use]
321 pub const fn observations(&self) -> &ObservationLog {
322 &self.observations
323 }
324
325 #[must_use]
335 pub fn records(&self) -> &[Record] {
336 self.records.as_slice()
337 }
338
339 #[must_use]
346 pub fn record_diagnostics(&self) -> &[RecordParseDiagnostic] {
347 self.record_diagnostics.as_slice()
348 }
349
350 #[must_use]
357 pub fn scripts(&self) -> &[Script] {
358 self.scripts.as_slice()
359 }
360
361 #[must_use]
368 pub fn artifacts(&self) -> &[Artifact] {
369 self.artifacts.as_slice()
370 }
371
372 #[must_use]
379 pub fn strings(&self) -> &[StringFinding] {
380 self.strings.as_slice()
381 }
382}
383
384#[derive(Debug, Clone, PartialEq, Eq)]
386pub struct Artifact {
387 record_index: usize,
388 subtype: String,
389 name: String,
390 bytes: Vec<u8>,
391 creation_time: u64,
392 last_write_time: u64,
393 checksum_valid: bool,
394 decompression_status: DecompressionStatus,
395}
396
397impl Artifact {
398 #[must_use]
404 pub const fn record_index(&self) -> usize {
405 self.record_index
406 }
407
408 #[must_use]
414 pub fn subtype(&self) -> &str {
415 self.subtype.as_str()
416 }
417
418 #[must_use]
424 pub fn name(&self) -> &str {
425 self.name.as_str()
426 }
427
428 #[must_use]
434 pub fn bytes(&self) -> &[u8] {
435 self.bytes.as_slice()
436 }
437
438 #[must_use]
444 pub const fn creation_time(&self) -> u64 {
445 self.creation_time
446 }
447
448 #[must_use]
455 pub const fn last_write_time(&self) -> u64 {
456 self.last_write_time
457 }
458
459 #[must_use]
465 pub const fn checksum_valid(&self) -> bool {
466 self.checksum_valid
467 }
468
469 #[must_use]
475 pub const fn decompression_status(&self) -> DecompressionStatus {
476 self.decompression_status
477 }
478}
479
480fn recover_artifacts(records: &[Record], scripts: &[Script]) -> Vec<Artifact> {
492 records
493 .iter()
494 .filter(|record| {
495 !scripts
496 .iter()
497 .any(|script| script.record_index() == record.index())
498 })
499 .map(|record| Artifact {
500 record_index: record.index(),
501 subtype: record.subtype().to_string(),
502 name: record.name().to_string(),
503 bytes: record.payload_data().to_vec(),
504 creation_time: record.creation_time(),
505 last_write_time: record.last_write_time(),
506 checksum_valid: record.checksum_valid(),
507 decompression_status: record.decompression_status(),
508 })
509 .collect()
510}
511
512fn first_file_record_matches(data: &[u8], offset: usize, encoding: Encoding) -> bool {
529 let Some(end) = offset.checked_add(4) else {
530 return false;
531 };
532 let Some(marker) = data.get(offset..end) else {
533 return false;
534 };
535 let decrypted = match encoding {
536 Encoding::Ea04 | Encoding::Ea05 | Encoding::Jb01 => crypto::mt::decrypt(marker, 0x16fa),
537 Encoding::Ea06 => crypto::lame::decrypt(marker, 0x18ee),
538 };
539 decrypted.is_some_and(|bytes| bytes == b"FILE")
540}
541
542fn first_stream_offset(observations: &[Observation]) -> Option<usize> {
554 observations
555 .iter()
556 .find_map(|observation| match observation {
557 Observation::PayloadStreamStartFound { offset } => Some(*offset),
558 _ => None,
559 })
560}
561
562fn first_file_encoding(observations: &[Observation]) -> Option<Encoding> {
574 observations
575 .iter()
576 .find_map(|observation| match observation {
577 Observation::FirstFileRecordDecrypted { encoding, .. } => Some(*encoding),
578 _ => None,
579 })
580}
581
582fn known_subtype(subtype: &str) -> Option<KnownSubtype> {
593 match subtype {
594 ">>>AUTOIT SCRIPT<<<" => Some(KnownSubtype::TokenizedScript),
595 ">AUTOIT UNICODE SCRIPT<" => Some(KnownSubtype::UnicodeScript),
596 ">AUTOIT SCRIPT<" => Some(KnownSubtype::PlainScript),
597 _ => None,
598 }
599}
600
601#[derive(Debug, Clone, Copy)]
603struct LooseVersionMarker {
604 encoding: Encoding,
606 offset: usize,
608}
609
610#[derive(Debug, Clone, Copy)]
612struct PackedMarker {
613 name: &'static str,
615 offset: usize,
617}
618
619fn loose_autoit_version_marker(data: &[u8]) -> Option<LooseVersionMarker> {
634 if find_bytes(data, b"AU3!").is_none()
635 && find_bytes(data, b"AutoIt").is_none()
636 && find_bytes(data, b"SCRIPT").is_none()
637 {
638 return None;
639 }
640
641 [
642 (Encoding::Ea06, b"EA06".as_slice()),
643 (Encoding::Ea05, b"EA05".as_slice()),
644 (Encoding::Jb01, b"JB01".as_slice()),
645 ]
646 .into_iter()
647 .find_map(|(encoding, marker)| {
648 find_bytes(data, marker).map(|offset| LooseVersionMarker { encoding, offset })
649 })
650}
651
652fn packed_marker(data: &[u8]) -> Option<PackedMarker> {
663 find_bytes(data, b"UPX").map(|offset| PackedMarker {
664 name: "UPX",
665 offset,
666 })
667}
668
669fn find_bytes(data: &[u8], needle: &[u8]) -> Option<usize> {
681 if needle.is_empty() || needle.len() > data.len() {
682 return None;
683 }
684 data.windows(needle.len())
685 .position(|candidate| candidate == needle)
686}
687
688#[derive(Debug, Clone, Copy, PartialEq, Eq)]
690pub enum InputKind {
691 Pe,
693 A3x,
695 RawStream,
697}
698
699#[derive(Debug, Clone, Copy, PartialEq, Eq)]
701pub enum Encoding {
702 Ea04,
706 Ea05,
708 Ea06,
710 Jb01,
712}
713
714#[cfg(test)]
715mod tests {
716 use super::*;
717
718 const PREFIX: [u8; 20] = [
719 0xa3, 0x48, 0x4b, 0xbe, 0x98, 0x6c, 0x4a, 0xa9, 0x99, 0x4c, 0x53, 0x0a, 0x86, 0xd6, 0x48,
720 0x7d, 0x41, 0x55, 0x33, 0x21,
721 ];
722
723 #[test]
724 fn scans_raw_ea06_signature() -> Result<(), String> {
725 let mut data = Vec::from(PREFIX);
726 data.extend_from_slice(b"EA06");
727
728 let binary = AutoItBinary::try_parse(&data).map_err(|err| err.to_string())?;
729
730 check_eq(binary.input_kind(), InputKind::A3x, "input kind")?;
731 check_eq(binary.encoding(), Some(Encoding::Ea06), "encoding")?;
732 check_eq(
733 binary.container().autoit_signature_offset(),
734 Some(0),
735 "signature offset",
736 )?;
737 check_eq(
738 binary.container().version_marker(),
739 Some(VersionMarkerInfo {
740 encoding: Encoding::Ea06,
741 offset: 20,
742 }),
743 "version marker",
744 )?;
745 check_eq(
746 binary.container().payload_stream_offsets(),
747 [24usize].as_slice(),
748 "payload offsets",
749 )?;
750 check_eq(
751 binary.observations().entries().len(),
752 3,
753 "observation count",
754 )
755 }
756
757 #[test]
758 fn validates_raw_ea06_first_file_record() -> Result<(), String> {
759 let mut data = Vec::from(PREFIX);
760 data.extend_from_slice(b"EA06");
761 append_ea06_record(&mut data, ">>>AUTOIT SCRIPT<<<", "main.au3", b"payload")?;
762
763 let binary = AutoItBinary::try_parse(&data).map_err(|err| err.to_string())?;
764
765 check_eq(binary.encoding(), Some(Encoding::Ea06), "encoding")?;
766 check_eq(binary.records().len(), 1, "record count")?;
767 let found = binary.observations().entries().iter().any(|observation| {
768 matches!(
769 observation,
770 Observation::FirstFileRecordDecrypted {
771 encoding: Encoding::Ea06,
772 offset: 24
773 }
774 )
775 });
776 if found {
777 let subtype_found = binary.observations().entries().iter().any(|observation| {
778 matches!(
779 observation,
780 Observation::KnownSubtypeDecrypted {
781 encoding: Encoding::Ea06,
782 offset: 24,
783 subtype: KnownSubtype::TokenizedScript
784 }
785 )
786 });
787 if subtype_found {
788 Ok(())
789 } else {
790 Err("missing known subtype observation".to_string())
791 }
792 } else {
793 Err("missing first FILE record observation".to_string())
794 }
795 }
796
797 #[test]
798 fn preserves_records_when_later_record_is_truncated() -> Result<(), String> {
799 let mut data = Vec::from(PREFIX);
800 data.extend_from_slice(b"EA06");
801 append_ea06_record(&mut data, ">AUTOIT SCRIPT<", "main.au3", b"payload")?;
802 let truncated_offset = data.len();
803 append_encrypted(&mut data, b"FILE", 0x18ee)?;
804
805 let binary = AutoItBinary::try_parse(&data).map_err(|err| err.to_string())?;
806
807 check_eq(binary.records().len(), 1, "record count")?;
808 check_eq(binary.scripts().len(), 1, "script count")?;
809 check_eq(
810 binary.record_diagnostics(),
811 [RecordParseDiagnostic {
812 record_index: 1,
813 offset: truncated_offset,
814 reason: RecognitionFailure::Truncated,
815 }]
816 .as_slice(),
817 "diagnostics",
818 )
819 }
820
821 #[test]
822 fn scans_pe_wrapped_signature() -> Result<(), String> {
823 let mut data = Vec::from(b"MZ");
824 data.extend_from_slice(&[0; 8]);
825 data.extend_from_slice(&PREFIX);
826 data.extend_from_slice(b"EA05");
827
828 let binary = AutoItBinary::try_parse(&data).map_err(|err| err.to_string())?;
829
830 check_eq(binary.input_kind(), InputKind::Pe, "input kind")?;
831 check_eq(binary.encoding(), Some(Encoding::Ea05), "encoding")?;
832 check_eq(
833 binary.observations().entries().len(),
834 4,
835 "observation count",
836 )
837 }
838
839 #[test]
840 fn rejects_unknown_input() -> Result<(), String> {
841 let Err(err) = AutoItBinary::try_parse(b"not autoit") else {
842 return Err("unexpected parse success".to_string());
843 };
844
845 check_eq(
846 err.recognition_failure(),
847 Some(RecognitionFailure::NotRecognized),
848 "recognition failure",
849 )
850 }
851
852 fn check_eq<T>(actual: T, expected: T, context: &str) -> Result<(), String>
853 where
854 T: core::fmt::Debug + PartialEq,
855 {
856 if actual == expected {
857 Ok(())
858 } else {
859 Err(format!("{context}: got {actual:?}, expected {expected:?}"))
860 }
861 }
862
863 fn append_ea06_record(
864 out: &mut Vec<u8>,
865 subtype: &str,
866 name: &str,
867 data: &[u8],
868 ) -> Result<(), String> {
869 append_encrypted(out, b"FILE", 0x18ee)?;
870 append_xored_u32(out, utf16_len(subtype)?, 0xadbc);
871 append_encrypted_utf16(out, subtype, 0xb33f)?;
872 append_xored_u32(out, utf16_len(name)?, 0xf820);
873 append_encrypted_utf16(out, name, 0xf479)?;
874 out.push(0);
875 let data_len = u32::try_from(data.len()).map_err(|err| err.to_string())?;
876 append_xored_u32(out, data_len, 0x87bc);
877 append_xored_u32(out, data_len, 0x87bc);
878 append_xored_u32(out, 0, 0xa685);
879 append_u64(out, 0);
880 append_u64(out, 0);
881 append_encrypted(out, data, 0x2477)
882 }
883
884 fn append_encrypted_utf16(out: &mut Vec<u8>, value: &str, key_base: u32) -> Result<(), String> {
885 let char_len = utf16_len(value)?;
886 let key = key_base.wrapping_add(char_len);
887 let mut bytes = Vec::new();
888 for unit in value.encode_utf16() {
889 bytes.extend_from_slice(&unit.to_le_bytes());
890 }
891 append_encrypted(out, bytes.as_slice(), key)
892 }
893
894 fn append_encrypted(out: &mut Vec<u8>, plain: &[u8], key: u32) -> Result<(), String> {
895 let encrypted = crypto::lame::decrypt(plain, key)
896 .ok_or_else(|| "EA06 encryption failed".to_string())?;
897 out.extend_from_slice(encrypted.as_slice());
898 Ok(())
899 }
900
901 fn append_xored_u32(out: &mut Vec<u8>, value: u32, mask: u32) {
902 append_u32(out, value ^ mask);
903 }
904
905 fn append_u32(out: &mut Vec<u8>, value: u32) {
906 out.extend_from_slice(&value.to_le_bytes());
907 }
908
909 fn append_u64(out: &mut Vec<u8>, value: u64) {
910 out.extend_from_slice(&value.to_le_bytes());
911 }
912
913 fn utf16_len(value: &str) -> Result<u32, String> {
914 u32::try_from(value.encode_utf16().count()).map_err(|err| err.to_string())
915 }
916}