Add support for analysing Setup Factory installers#26
Add support for analysing Setup Factory installers#26Tom Plant (pl4nty) wants to merge 50 commits into
Conversation
Signed-off-by: Tom Plant <tom@tplant.com.au>
7zip, SFXCab, InstallShield, Squirrel, UPX, WExtract Signed-off-by: Tom Plant <tom@tplant.com.au>
Signed-off-by: Tom Plant <tom@tplant.com.au>
Signed-off-by: Tom Plant <tom@tplant.com.au>
Signed-off-by: Tom Plant <tom.plant@devicie.com>
… and vanity URLs Signed-off-by: Tom Plant <tom.plant@devicie.com>
Signed-off-by: Tom Plant <tom.plant@devicie.com>
Signed-off-by: Tom Plant <tom.plant@devicie.com>
Signed-off-by: Tom Plant <tom.plant@devicie.com>
Signed-off-by: Tom Plant <tom.plant@devicie.com>
Edge cases like version not matching github, or multiple channels Signed-off-by: Tom Plant <tom.plant@devicie.com>
useful when a package ID changes. could copy versions to the new ID, but that's a lot of validation requests Signed-off-by: Tom Plant <tom.plant@devicie.com>
Signed-off-by: Tom Plant <tom.plant@devicie.com>
…tegies to file Signed-off-by: Tom Plant <tom.plant@devicie.com>
Signed-off-by: Tom Plant <tom.plant@devicie.com>
Signed-off-by: Tom Plant <tom.plant@devicie.com>
Signed-off-by: Tom Plant <tom.plant@devicie.com>
Signed-off-by: Tom Plant <tom.plant@devicie.com>
Signed-off-by: Tom Plant <tom.plant@devicie.com>
Signed-off-by: Tom Plant <tom.plant@devicie.com>
Workspace is set to use Rust v1.93 even though the module is 1.89 Signed-off-by: Tom Plant <tom.plant@devicie.com>
Inno Setup 6.2 introduced a new PrivilegesRequired setting 'requireadmin' (value 4), which requires administrative privileges and will refuse to run if they cannot be obtained, unlike 'admin' which will prompt for credentials. Previously, reading this value with TryFromBytes failed since the enum only contained values 0-3. The failure was silently swallowed by unwrap_or_default(), returning None instead of the actual value.
…flag In Inno Setup 6.4.x, the decrypt_dll was removed from the wizard section as encryption is now handled via the inline EncryptionHeader. However, the ENCRYPTION_USED flag can still be set in the header flags bitfield. Previously, when ENCRYPTION_USED was set, the parser tried to read decrypt_dll from the wizard section regardless of version, causing UnexpectedEof for Inno 6.4.x installers that do not store decrypt_dll. Also revert the error suppression that converted UnexpectedEof to NotInnoFile, which was masking the root cause.
The non-solid NSIS header detection checked for an exact byte value of 0x80 in the high byte of compressed_header_size (signature.get(3) == Some(&0x80)). This missed valid non-solid installers where the high byte has other bits set in addition to the IS_COMPRESSED_MASK bit (e.g. 0x8D for timi.exe). The correct check is to test the IS_COMPRESSED_MASK bit directly: (compressed_header_size & IS_COMPRESSED_MASK) != 0 Additionally, installers with the BI_EXTERNAL_FILE flag set store their data in an external file and cannot be analysed inline. These are now rejected early with NotNsisFile instead of failing later in header decompression. The map_err suppression on Header::decompress is also removed; genuine decompression errors are now propagated as-is.
The NSIS header's language_table_size field is unreliable for old installers: it can be 0 (typefaster.exe) or -1 (quickpar.exe) even when valid language tables are present in the LangTables block. Fix this by computing the size from the block data, matching the approach already used for sections: lang_table_size = lang_block_data.len() / num_tables This uses the actual block data size divided by the number of language table entries reported in the block header, which is always correct. Also removes the now-unnecessary header parameter from primary_language().
… values The FileOpen entry (type 55) stores a file creation disposition value that NSIS passes directly to CreateFile(). NSIS uses values like 0 and 700 that are not valid Win32 OPEN_EXISTING (3) / CREATE_ALWAYS (2) / OPEN_ALWAYS (4) values. Previously, the create_mode field used CreationDisposition which only accepts values 1-5 via TryFromBytes, causing FileOpen entries with non-standard modes to silently become Entry::Invalid. Fix by changing create_mode to I32<LE> (accepts any value) and comparing by integer in the execution logic.
Signed-off-by: Tom Plant <tom.plant@devicie.com>
The vendored `inno` and `msi` git submodules pointed at commits that no longer exist on their remotes, so `git submodule update --init` failed and broke CI checkout. Both crates are available on crates.io at the exact versions the manifest already declares (inno 0.4.2, msi 0.10.0), so the `[patch.crates-io]` overrides and submodules are removed in favour of the published crates. The healthy `winget-types` submodule is left untouched. Reconciling the lockfile to the published crates surfaced several pre-existing issues that the broken submodule checkout had been masking: - inno 0.4.2 has no `PrivilegeLevel::RequireAdmin` variant (that was a fork-only addition); drop the arm so it falls through as before. - msi analysis set the `Installer::switches` field twice (a botched merge of the upstream install-location switch and the custom-switch feature); merge both into a single `InstallerSwitches` builder. - `token.rs` matched a `keyring_core::Entry` as if it were an `Option`. - Remove unused NSIS header imports. - Add `hide_env_values` to the three `GITHUB_TOKEN` args that were missing it so the help-output secrecy test passes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmPAQgcCW15dCnnmKSyWWc
With the submodule checkout fixed, CI's clippy job (which runs with `RUSTFLAGS=-D warnings`) could finally run and flagged seven pre-existing lints. None are behavioural changes: - `collapsible_if` in `prompts` and `html_page` -> let-chains (already used elsewhere in the crate; MSRV 1.95 supports them). - `redundant_closure` in `sourceforge` -> pass the fn directly. - `unnecessary_map_or` in `vanity_url` -> `Option::is_none_or`. - `ptr_arg` in `submit_option` -> take `&mut [_]` (callers coerce, and `Editor::new` already takes a slice). - `large_enum_variant` in `ExeType` -> box the 464-byte `InstallShield` variant. - `too_many_arguments` on `AutoUpdate::run_entry` -> `#[allow]`. Verified with `RUSTFLAGS=-D warnings cargo clippy` on rust 1.96 (matching CI) and `cargo check` on the 1.95 MSRV; both clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmPAQgcCW15dCnnmKSyWWc
Detect Indigo Rose Setup Factory installers (e.g. DIALux evo). The setup stub embeds the `irsetup.exe` run-time, whose assembly manifest carries `<description>Setup Factory Run-time</description>`, and the builder stamps `Created with Setup Factory <version>` into the `Comments` version-info field. Either signal identifies the installer. Emits an `exe` installer with the `/S` silent switch and populates the ARP entry from the version info (product name, publisher, version). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012BX4kXe76SvBKY3BySbzRN
The setup stub's version info describes the Setup Factory run-time (and is often left at the builder's defaults, e.g. ProductName "Setup Factory Runtime" published by Indigo Rose Corporation), not the packaged application. The real Apps & Features / uninstall entry (DisplayName, DisplayVersion, ProductCode, ...) is written by the compiled setup script inside the payload, which isn't recoverable from the PE. Emitting DisplayName/Publisher/DisplayVersion from the version info therefore produced ARP entries that don't match the registry - the real winget manifests for these packages (e.g. DIAL.DIALux-evo) carry no such entries. Drop the extraction and emit only the reliably-known fields: the exe installer type and the /S silent switch. Version info is still read solely to detect the "Created with Setup Factory" Comments marker. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012BX4kXe76SvBKY3BySbzRN
Test resultsRan
Example output (DIALux evo), which matches the shape of the published InstallerType: exe
InstallModes:
- interactive
- silent
- silentWithProgress
InstallerSwitches:
Silent: /S
SilentWithProgress: /SNotes¹ Architecture is read from the setup stub's PE machine type. DIALux ships a 32-bit stub that installs a 64-bit app, so it reports ² The exact ³ The OutCALL asset is a GitHub release download. This session's egress policy scopes GitHub to On
|
Rather than reading the setup stub's PE version info - which describes the Setup Factory run-time and is frequently left at the builder's defaults (e.g. gloCOM/Communicator reported "Setup Factory Runtime" published by "Indigo Rose Corporation") - walk the Setup Factory 8/9 archive in the PE overlay to recover the compiled setup script (irsetup.dat) and its session variable table. That table holds the values the author entered in the designer: %ProductName%, %CompanyName% and %ProductVer%. The archive layout and XOR/compression handling follow the format documented by CybercentreCanada/sfextract. The script is LZMA- or LZMA2-compressed (decoded via the already-vendored liblzma); large embedded files are seeked past so only the small script is buffered. Detection now keys off the 16-byte overlay signature, with the run-time manifest and version-info comments kept as fallbacks. The recovered product name and publisher populate AppsAndFeaturesEntries. The version variable is author-defined and sometimes a bare major number, so it is only used when it looks like a real dotted version. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012BX4kXe76SvBKY3BySbzRN
884e97d to
aca026b
Compare
Updated results — metadata now parsed from the overlay archiveFollowing the feedback to parse the binary rather than the stub's PE version info, the detector now walks the Setup Factory archive, decompresses All emit Notes¹ The exact ² OutCALL is a GitHub release asset; this session's egress policy scopes GitHub to On the empty/omitted fields
Known limitationArchitecture still comes from the setup stub's PE machine type, so DIALux reports Generated by Claude Code |
| } | ||
| } | ||
|
|
||
| fn has_archive_signature<R: Read + Seek>(reader: &mut R, overlay_start: u64) -> bool { |
There was a problem hiding this comment.
if this is only called once, we should inline it
There was a problem hiding this comment.
Done in 484018c — inlined into new(). (The manifest/comments fallback that also lived in this file is gone too, so detection is now just the inlined signature check.)
Generated by Claude Code
| // The overlay signature is the definitive marker, but fall back to the run-time manifest and | ||
| // version-info comments so older (Setup Factory 7) or repacked stubs are still recognised. | ||
| if !has_archive | ||
| && !is_runtime_manifest(pe, &mut reader) | ||
| && !has_setup_factory_comments(pe, &mut reader) | ||
| { | ||
| return Err(SetupFactoryError::NotSetupFactoryFile); | ||
| } |
There was a problem hiding this comment.
can we remove this? which of your tests actually needs it? if we're going to keep it, we should document the tests + Factory versions that trigger it
There was a problem hiding this comment.
Removed in 484018c. No test needed it — all seven test installers (DIALux evo, LDraw AIOI, gloCOM, Communicator, and the three Locklizard Safeguard installers) carry the 16-byte overlay signature, so detection now keys off that alone. The fallback was speculative (Setup Factory 7 / repacked stubs) with no coverage.
Generated by Claude Code
| //! ..\x0d%ProductName%\x06gloCOM\x01\x00\x00\x00..\x0d%CompanyName%\x0dBicom Systems.. | ||
| //! ``` | ||
| //! | ||
| //! Entries are separated by a short, version-dependent run of bytes, so rather than decode that |
There was a problem hiding this comment.
is there any documentation or code that could decode these bytes? I wonder if they have useful data
There was a problem hiding this comment.
I dug into these. Between entries the bytes are 01 00 00 00 <XX XX 01> 00 00 00: the leading 01 is a constant type/flag, and the middle three bytes are constant within a given installer but differ between installers (6e 81 01 for Locklizard PDF Writer, b4 83 01 for gloCOM) — i.e. a per-build marker/checksum, not per-variable data. No product metadata is hidden there; the useful content is the name/value pairs we already extract. Neither sfextract nor SFUnpacker decode this table (they only handle the CSetupFileData file table), so there's no reference decoder to borrow from.
Generated by Claude Code
|
|
||
| /// A file count above this is treated as an embedded `lua5.1.dll` special file rather than a real | ||
| /// count, matching `sfextract`'s heuristic. | ||
| const MAX_PLAUSIBLE_FILE_COUNT: u32 = 1000; |
There was a problem hiding this comment.
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.
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. The u32 read 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
|
|
||
| /// Upper bound on the decompressed script size we are willing to buffer (scripts are well under a | ||
| /// megabyte; this guards against a malformed size field). | ||
| const MAX_SCRIPT_SIZE: i64 = 64 << 20; |
There was a problem hiding this comment.
I don't think this is necessary
There was a problem hiding this comment.
Agreed — removed in 484018c. The script is now read with a bounded take(size).read_to_end(), which grows with the data instead of pre-allocating from the size field, so the cap isn't needed.
Generated by Claude Code
Integrate 31 upstream commits from unpn-org/Komac (anthelion branch) alongside the existing devicie changes. Key conflict resolutions: - CI workflows (audit/build/release): kept devicie's versions (anthelion had deleted them). - serde: kept devicie's serde_yaml + serini stack; reverted anthelion's serde-saphyr usage in the shared files while preserving their other changes (retry middleware, font support, changed signatures). - winget-types: switched the [patch.crates-io] source to anthelion's fork (UnownPlain/winget-types, anthelion branch) with features [serde, std, chrono] and no serde-saphyr, because anthelion's code requires API only that fork provides (Velopack/Squirrel installer types, LanguageTag accessors, ValidFileExtensions). komac still uses serde_yaml. devicie's own winget-types fork carried no meaningful custom code (a one-line Cargo.toml tweak plus two upstream PRs). - github owner/repo: adopted anthelion's configurable EnvStr system but kept devicie's pl4nty/winget-extras as the defaults. - get_versions/get_manifests/get_existing_pull_request/PackagePath: threaded anthelion's new font parameters through devicie call sites. - update_version: preserved devicie's optional package_version + PE ProductVersion inference flow; grafted in anthelion's font handling and locale normalization. - Adapted devicie's installer analyzers (installshield/qt return codes, autoupdate strategies) to anthelion fork's winget-types API. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LAsaSyfFuR5pcV43kApw5x
- Detect solely from the 16-byte overlay signature and inline the check; drop the run-time manifest / version-info comment fallback, which no test exercised (all samples carry the signature). - Read the script with a bounded take().read_to_end() instead of pre-allocating from the size field, removing the size-cap guard. - Document that the embedded lua5.1.dll branch is taken by every tested installer (it is the normal Setup Factory 9 layout, not a version mis-parse): the leading u32 is the low half of the DLL's 64-bit size. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012BX4kXe76SvBKY3BySbzRN
…on-gijrjb Merge anthelion changes into devicie fork
…analysis-2yb6iz # Conflicts: # src/analysis/installers/exe.rs
The stable clippy used in CI (1.97) flags the redundant `&` on the format! arguments in `remove_version`, which fails the `-D warnings` job after merging the latest main. Drop the borrows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012BX4kXe76SvBKY3BySbzRN
Summary
Adds detection and analysis of Indigo Rose Setup Factory installers, such as DIALux evo.
Setup Factory setup executables are a self-extracting stub wrapping the
irsetup.exerun-time, with a compressed archive in the PE overlay. This detector:Setup Factory Run-timeand theCreated with Setup Factoryversion-info comment kept as fallbacks for older/repacked stubs).irsetup.dat) and reads its session variable table — the real values the author entered in the designer (%ProductName%,%CompanyName%,%ProductVer%).exeinstaller with the documented/Ssilent switch and anAppsAndFeaturesEntriesbuilt from the recovered metadata.Why parse the binary instead of the PE version info
The setup stub's PE version info describes the run-time, not the packaged app — and authors frequently leave it at the Setup Factory defaults (gloCOM/Communicator would otherwise report
DisplayName: Setup Factory Runtime, publisherIndigo Rose Corporation). The real product name/publisher/version live in the compiled script inside the overlay archive, so that's what we read. The archive format, XOR obfuscation and compression handling follow CybercentreCanada/sfextract.Notes:
liblzma. Large embedded files are seeked past so only the small script (well under 1 MB) is buffered — no full-payload extraction.%ProductVer%is author-defined and is sometimes a bare major number (gloCOM ships"4"; the real version lives in a custom variable), so it's only used when it looks like a real dotted version.Changes
src/analysis/installers/setup_factory/module:mod.rs— detection, best-effort metadata extraction, installer emission.archive.rs— walks the overlay archive and decompressesirsetup.dat.script.rs— parses the session variable table (with unit tests).src/analysis/installers/mod.rsand wiredSetupFactoryinto theExedetection chain insrc/analysis/installers/exe.rs.Verification
Tested end-to-end with
komac analyzeagainst real Setup Factory installers — see the results comment below.cargo build,cargo clippy(-D warnings), andcargo test(session-variable parser) all pass.🤖 Generated with Claude Code
https://claude.ai/code/session_012BX4kXe76SvBKY3BySbzRN