diff --git a/windows/installer/build.ps1 b/windows/installer/build.ps1 index 7872397..7a6e78d 100644 --- a/windows/installer/build.ps1 +++ b/windows/installer/build.ps1 @@ -51,8 +51,9 @@ $oscdimg = Resolve-Tool 'oscdimg.exe' @( 'C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\*\Oscdimg\oscdimg.exe') $m = Get-Content $Manifest -Raw | ConvertFrom-Json $isoRoot = Join-Path $WorkDir 'iso' # writable copy of ISO contents -$mount = Join-Path $WorkDir 'mount' # WIM mount point -$null = New-Item -ItemType Directory -Force -Path $WorkDir,$mount,(Split-Path $OutputIso) +$mount = Join-Path $WorkDir 'mount' # install.wim mount point +$bootmnt = Join-Path $WorkDir 'bootmnt' # boot.wim mount point +$null = New-Item -ItemType Directory -Force -Path $WorkDir,$mount,$bootmnt,(Split-Path $OutputIso) # --- 1. Verify input ------------------------------------------------------- function Invoke-VerifyInput { @@ -82,6 +83,36 @@ function Invoke-Extract { attrib -r "$isoRoot\*" /s /d 2>$null # clear read-only carried from the ISO } +# --- 2b. Force legacy Setup (bypass the 24H2 "ConX" front-end) ------------- +function Invoke-ForceLegacySetup { + Write-Stage 'Stage 2b: force legacy Setup (patch sources\boot.wim)' + # Win11 24H2/25H2 ship a redesigned WinPE Setup front-end (setup.exe -> + # SetupHost.exe -> SetupPrep.exe) that IGNORES the windowsPE pass of + # autounattend.xml (manual language/keyboard/region prompts). No answer-file + # element toggles it; the verified fix is to point WinPE's shell at the + # LEGACY installer via HKLM\SYSTEM\Setup\CmdLine in boot.wim index 2 (the + # Setup image). The legacy engine consumes all four passes -> hands-off. + $bootwim = Join-Path $isoRoot 'sources\boot.wim' + if (-not (Test-Path $bootwim)) { throw "boot.wim not found: $bootwim" } + Mount-WindowsImage -ImagePath $bootwim -Index 2 -Path $bootmnt | Out-Null + try { + $cmdline = 'X:\sources\setup.exe' + if (-not (Test-Path (Join-Path $bootmnt 'sources\setup.exe')) -and + (Test-Path (Join-Path $bootmnt 'setup.exe'))) { $cmdline = 'X:\setup.exe' } + $hive = Join-Path $bootmnt 'Windows\System32\config\SYSTEM' + & reg load 'HKLM\SM_BOOT' $hive | Out-Null + try { + & reg add 'HKLM\SM_BOOT\Setup' /v CmdLine /t REG_SZ /d $cmdline /f | Out-Null + Write-Host " WinPE Setup\CmdLine = $cmdline (legacy Setup forced)" + } finally { + [gc]::Collect(); Start-Sleep -Seconds 2 + & reg unload 'HKLM\SM_BOOT' | Out-Null + } + } finally { + Dismount-WindowsImage -Path $bootmnt -Save | Out-Null + } +} + # --- 3. Service the WIM offline (DISM) ------------------------------------- function Invoke-ServiceWim { Write-Stage 'Stage 3: offline-service install.wim' @@ -131,8 +162,11 @@ function Invoke-ServiceWim { # --- 4. Inject answer file ------------------------------------------------- function Invoke-InjectUnattend { - Write-Stage 'Stage 4: inject autounattend.xml at ISO root' - Copy-Item (Join-Path $PSScriptRoot 'autounattend\autounattend.xml') (Join-Path $isoRoot 'autounattend.xml') -Force + Write-Stage 'Stage 4: inject autounattend.xml (ISO root + \sources)' + $src = Join-Path $PSScriptRoot 'autounattend\autounattend.xml' + Copy-Item $src (Join-Path $isoRoot 'autounattend.xml') -Force + # Also place in \sources (the implicit search location the windowsPE pass uses). + Copy-Item $src (Join-Path $isoRoot 'sources\autounattend.xml') -Force } # --- 5. Brand -------------------------------------------------------------- @@ -175,6 +209,7 @@ function Invoke-Attest { # --- orchestrate ----------------------------------------------------------- Invoke-VerifyInput Invoke-Extract +Invoke-ForceLegacySetup Invoke-ServiceWim Invoke-InjectUnattend Invoke-Brand diff --git a/windows/iso-builder.md b/windows/iso-builder.md index c4de73c..47294e2 100644 --- a/windows/iso-builder.md +++ b/windows/iso-builder.md @@ -36,6 +36,7 @@ Orchestrated by [`installer/build.ps1`](installer/build.ps1). Runs on a **Window 1. **Verify input** — assert the source ISO SHA-256 matches the pinned manifest (supply-chain integrity). 2. **Extract** — expand the ISO to a working directory. +2b. **Force legacy Setup** — patch `sources\boot.wim` (index 2) so WinPE launches the legacy installer (see note below). *Required on 24H2/25H2 for a hands-off install.* 3. **Service the WIM offline (DISM)**: - Select the IoT Enterprise LTSC image index. - `/Add-Driver` the GPD Pocket 4 driver pack (Strix Point GPU, sensors/auto-rotate, Wi-Fi, fingerprint). @@ -51,6 +52,26 @@ Orchestrated by [`installer/build.ps1`](installer/build.ps1). Runs on a **Window 6. **Repack** — `oscdimg` produces a UEFI-bootable ISO (efisys boot image, El Torito). 7. **Attest** — emit output ISO SHA-256, an **SBOM** (every component + version), and a signed build attestation (design-principle #4). +### 3a. Windows 11 24H2/25H2: forcing legacy Setup (critical) + +Windows 11 **24H2 (build 26100) and 25H2** ship a **redesigned "ConX" Setup front-end** (`setup.exe → SetupHost.exe → SetupPrep.exe`, launched in WinPE) that **does not honor the `windowsPE` pass** of a root `autounattend.xml`. Symptom (confirmed on our VM test): the new *Select language/keyboard* pages and the OOBE *region* prompt require key presses despite the answer file. **No answer-file element toggles legacy vs new Setup.** + +The verified, scriptable fix — applied by `build.ps1` stage 2b — is to point WinPE's shell at the **legacy** installer in `sources\boot.wim` **index 2** (the Setup image; index 1 is WinPE): + +``` +DISM /Mount-Image sources\boot.wim index 2 → mount +reg load HKLM\SM_BOOT \Windows\System32\config\SYSTEM +reg add HKLM\SM_BOOT\Setup /v CmdLine /t REG_SZ /d X:\sources\setup.exe /f +reg unload HKLM\SM_BOOT +DISM /Unmount-Image /commit +``` + +WinPE's winlogon launches whatever `HKLM\SYSTEM\Setup\CmdLine` holds; overriding it to `X:\sources\setup.exe` runs the legacy engine, which consumes all four passes (windowsPE/offlineServicing/specialize/oobeSystem) → fully hands-off. The answer file is placed at **both** the ISO root and `\sources\`. + +**Rejected alternatives** (community-tested, unreliable): `winpeshl.ini` `[LaunchApp]` variants and the answer-file-embedded `RunSynchronous` reg trick (the latter only works if the answer file contains *nothing else*, else it reboot-loops WinPE). + +**Caveat:** this is ConX-specific behaviour that Microsoft may change in a future cumulative update / ADK refresh — re-verify against the exact base media before each batch. Sources: ElevenForum/NTLite 24H2–25H2 threads + MS Learn setup-automation docs (the Learn pages predate the redesign and describe only the legacy mechanism). Open question: whether MS later ships a supported way to pre-answer the ConX front-end. + ## 4. Where each hardening control is applied The control domains in [`hardening-spec.md`](hardening-spec.md) split across three layers: