forked from russellbanks/Komac
-
Notifications
You must be signed in to change notification settings - Fork 0
Add support for analysing Setup Factory installers #26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
Tom Plant (pl4nty)
wants to merge
4
commits into
main
Choose a base branch
from
claude/setup-factory-analysis-2yb6iz
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
27bc842
Add support for analysing Setup Factory installers
claude 1186850
Don't emit ARP data guessed from Setup Factory version info
claude aca026b
Parse Setup Factory archive for real installer metadata
claude 484018c
Address review: signature-only detection, simpler script read
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| //! Walks the Setup Factory 8/9 archive stored in the PE overlay to recover the compiled setup | ||
| //! script (`irsetup.dat`). | ||
| //! | ||
| //! Layout (little-endian), based on the format documented by | ||
| //! [`sfextract`](https://github.com/CybercentreCanada/sfextract): | ||
| //! | ||
| //! ```text | ||
| //! [16] signature (SIGNATURE) | ||
| //! [10] unknown | ||
| //! [ 8] irsetup.exe size, then that many XOR-obfuscated bytes | ||
| //! [ 4] file count (if > 1000 it is really the size of an embedded lua5.1.dll) | ||
| //! per file: | ||
| //! [264] NUL-terminated name | ||
| //! [ 8] size | ||
| //! [ 4] CRC-32 | ||
| //! [ 4] unknown | ||
| //! [ ..] compressed data (LZMA / LZMA2 / PKWARE, auto-detected from the first bytes) | ||
| //! ``` | ||
|
|
||
| use std::io::{self, Cursor, Read, Seek, SeekFrom}; | ||
|
|
||
| use liblzma::{ | ||
| read::XzDecoder, | ||
| stream::{Filters, Stream}, | ||
| }; | ||
| use zerocopy::LittleEndian; | ||
|
|
||
| use crate::read::ReadBytesExt; | ||
|
|
||
| /// Marks a Setup Factory 8/9 archive at the start of the PE overlay. | ||
| pub const SIGNATURE: [u8; 16] = [ | ||
| 0xe0, 0xe0, 0xe1, 0xe1, 0xe2, 0xe2, 0xe3, 0xe3, 0xe4, 0xe4, 0xe5, 0xe5, 0xe6, 0xe6, 0xe7, 0xe7, | ||
| ]; | ||
|
|
||
| /// Bytes to skip from the overlay start to reach the first embedded file (signature + 10 unknown). | ||
| const HEADER_LENGTH: i64 = SIGNATURE.len() as i64 + 10; | ||
|
|
||
| /// Fixed width of a file name entry in the file table. | ||
| const FILENAME_LENGTH: usize = 264; | ||
|
|
||
| /// Name of the compiled setup script within the archive. | ||
| const SCRIPT_NAME: &[u8] = b"irsetup.dat"; | ||
|
|
||
| /// After `irsetup.exe`, Setup Factory 9 stubs embed a second special file (`lua5.1.dll`) before the | ||
| /// file table, but older layouts do not. The two are told apart by reading a `u32`: a real file | ||
| /// count is small, whereas the low half of `lua5.1.dll`'s ~350 KB `u64` size is far larger. A value | ||
| /// above this threshold is therefore the DLL's size rather than a count. Every installer tested so | ||
| /// far (DIALux evo, LDraw AIOI, gloCOM, Communicator and the Locklizard Safeguard family, all Setup | ||
| /// Factory 9.5) embeds the DLL and takes this path. | ||
| const MAX_PLAUSIBLE_FILE_COUNT: u32 = 1000; | ||
|
|
||
| /// Reads and decompresses the `irsetup.dat` script from the archive at `overlay_start`. | ||
| /// | ||
| /// Returns `Ok(None)` when the script is absent or uses an unsupported compression. Best-effort: | ||
| /// callers treat any error as "no metadata". | ||
| pub fn read_script<R: Read + Seek>( | ||
| reader: &mut R, | ||
| overlay_start: u64, | ||
| ) -> io::Result<Option<Vec<u8>>> { | ||
| reader.seek(SeekFrom::Start(overlay_start))?; | ||
| reader.seek(SeekFrom::Current(HEADER_LENGTH))?; | ||
|
|
||
| // Embedded irsetup.exe special file: an 8-byte size followed by that many bytes. | ||
| skip_special_file(reader)?; | ||
|
|
||
| let mut file_count = reader.read_u32::<LittleEndian>()?; | ||
| if file_count > MAX_PLAUSIBLE_FILE_COUNT { | ||
| // What we read was actually the size of an embedded lua5.1.dll special file. | ||
| reader.seek(SeekFrom::Current(-(size_of::<u32>() as i64)))?; | ||
| skip_special_file(reader)?; | ||
| file_count = reader.read_u32::<LittleEndian>()?; | ||
| } | ||
|
|
||
| for _ in 0..file_count { | ||
| let mut name = [0; FILENAME_LENGTH]; | ||
| reader.read_exact(&mut name)?; | ||
| let name = &name[..name | ||
| .iter() | ||
| .position(|&byte| byte == 0) | ||
| .unwrap_or(name.len())]; | ||
|
|
||
| let size = read_size(reader)?; | ||
| reader.seek(SeekFrom::Current(size_of::<u32>() as i64))?; // CRC-32 | ||
| reader.seek(SeekFrom::Current(size_of::<u32>() as i64))?; // unknown | ||
|
|
||
| if name.eq_ignore_ascii_case(SCRIPT_NAME) { | ||
| // Bounded read that grows with the data rather than pre-allocating from the size field. | ||
| let mut data = Vec::new(); | ||
| reader.take(size.unsigned_abs()).read_to_end(&mut data)?; | ||
| return Ok(decompress(&data)); | ||
| } | ||
|
|
||
| reader.seek(SeekFrom::Current(size))?; | ||
| } | ||
|
|
||
| Ok(None) | ||
| } | ||
|
|
||
| /// Skips an 8-byte size field and the bytes it describes. | ||
| fn skip_special_file<R: Read + Seek>(reader: &mut R) -> io::Result<()> { | ||
| let size = read_size(reader)?; | ||
| reader.seek(SeekFrom::Current(size))?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Reads an 8-byte little-endian size, clamping negatives to zero. | ||
| fn read_size<R: Read>(reader: &mut R) -> io::Result<i64> { | ||
| let mut bytes = [0; size_of::<i64>()]; | ||
| reader.read_exact(&mut bytes)?; | ||
| Ok(i64::from_le_bytes(bytes).max(0)) | ||
| } | ||
|
|
||
| /// Decompresses a file whose compression is auto-detected from its leading bytes. | ||
| /// | ||
| /// Handles the LZMA ("alone") and LZMA2 streams used by the setup script; returns `None` for the | ||
| /// PKWARE-compressed scripts of older Setup Factory versions and anything unrecognised. | ||
| fn decompress(data: &[u8]) -> Option<Vec<u8>> { | ||
| let mut output = Vec::new(); | ||
| match (data.first()?, data.get(1)?) { | ||
| // Classic LZMA "alone" stream (properties byte + dictionary size + uncompressed size). | ||
| (0x5D, 0x00) => { | ||
| let stream = Stream::new_lzma_decoder(u64::MAX).ok()?; | ||
| XzDecoder::new_stream(Cursor::new(data), stream) | ||
| .read_to_end(&mut output) | ||
| .ok()?; | ||
| } | ||
| // Raw LZMA2 stream: one property byte, an 8-byte size, then the raw payload. | ||
| (0x18, _) => { | ||
| let mut filters = Filters::new(); | ||
| filters.lzma2_properties(&data[..1]).ok()?; | ||
| let stream = Stream::new_raw_decoder(&filters).ok()?; | ||
| XzDecoder::new_stream(Cursor::new(data.get(9..)?), stream) | ||
| .read_to_end(&mut output) | ||
| .ok()?; | ||
| } | ||
| _ => return None, | ||
| } | ||
| Some(output) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| mod archive; | ||
| mod script; | ||
|
|
||
| use std::io::{Read, Seek, SeekFrom}; | ||
|
|
||
| use script::ScriptMetadata; | ||
| use thiserror::Error; | ||
| use tracing::debug; | ||
| use winget_types::{ | ||
| Version, | ||
| installer::{ | ||
| AppsAndFeaturesEntries, AppsAndFeaturesEntry, Architecture, InstallModes, Installer, | ||
| InstallerSwitches, InstallerType, | ||
| }, | ||
| }; | ||
|
|
||
| use crate::{ | ||
| analysis::{Installers, installers::pe::PE}, | ||
| traits::FromMachine, | ||
| }; | ||
|
|
||
| #[derive(Error, Debug)] | ||
| pub enum SetupFactoryError { | ||
| #[error("File is not a Setup Factory installer")] | ||
| NotSetupFactoryFile, | ||
| #[error(transparent)] | ||
| Io(#[from] std::io::Error), | ||
| } | ||
|
|
||
| pub struct SetupFactory { | ||
| architecture: Architecture, | ||
| metadata: ScriptMetadata, | ||
| } | ||
|
|
||
| impl SetupFactory { | ||
| // Detects Indigo Rose Setup Factory installers from the Setup Factory 8/9 signature at the start | ||
| // of the PE overlay, then walks the overlay archive to recover the compiled setup script and its | ||
| // session variable table - the real product name, publisher and version the author entered. | ||
| // (The stub's own version info describes the run-time, not the packaged application, and is often | ||
| // left at Setup Factory's defaults, so it is not used.) | ||
| pub fn new<R: Read + Seek>(mut reader: R, pe: &PE) -> Result<Self, SetupFactoryError> { | ||
| let overlay_start = pe | ||
| .overlay_offset() | ||
| .ok_or(SetupFactoryError::NotSetupFactoryFile)?; | ||
|
|
||
| let mut signature = [0; archive::SIGNATURE.len()]; | ||
| if reader.seek(SeekFrom::Start(overlay_start)).is_err() | ||
| || reader.read_exact(&mut signature).is_err() | ||
| || signature != archive::SIGNATURE | ||
| { | ||
| return Err(SetupFactoryError::NotSetupFactoryFile); | ||
| } | ||
|
|
||
| let metadata = archive::read_script(&mut reader, overlay_start) | ||
| .ok() | ||
| .flatten() | ||
| .map(|script| ScriptMetadata::parse(&script)) | ||
| .unwrap_or_default(); | ||
|
|
||
| debug!(?metadata, "Setup Factory"); | ||
|
|
||
| Ok(Self { | ||
| architecture: Architecture::from_machine(pe.machine()), | ||
| metadata, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| impl Installers for SetupFactory { | ||
| fn installers(&self) -> Vec<Installer> { | ||
| let apps_and_features_entry = AppsAndFeaturesEntry::builder() | ||
| .maybe_display_name(self.metadata.display_name.clone()) | ||
| .maybe_publisher(self.metadata.publisher.clone()) | ||
| .maybe_display_version( | ||
| self.metadata | ||
| .display_version | ||
| .as_deref() | ||
| .and_then(|version| version.parse::<Version>().ok()), | ||
| ) | ||
| .build(); | ||
|
|
||
| vec![Installer { | ||
| architecture: self.architecture, | ||
| r#type: Some(InstallerType::Exe), | ||
| install_modes: InstallModes::all(), | ||
| // https://www.indigorose.com/webhelp/suf9/Program_Reference/Command_Line_Options.htm | ||
| switches: InstallerSwitches::builder() | ||
| .silent("/S".parse().unwrap()) | ||
| .silent_with_progress("/S".parse().unwrap()) | ||
| .build(), | ||
| apps_and_features_entries: if apps_and_features_entry.is_empty() { | ||
| AppsAndFeaturesEntries::default() | ||
| } else { | ||
| apps_and_features_entry.into() | ||
| }, | ||
| ..Installer::default() | ||
| }] | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
which test cases have this? is the real issue that we're not parsing a specific Factory version properly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All seven test installers take this path — it's the normal Setup Factory 9 layout, not a version mis-parse. After
irsetup.exe, SF9 embeds a second special file (lua5.1.dll, ~350 KB) before the file table. Theu32read here to disambiguate is the low half of that DLL's 64-bit size (~350000), hence>1000; a genuine file count is small. Expanded the doc comment in 484018c to spell this out.Generated by Claude Code