diff options
| -rw-r--r-- | README | 133 | ||||
| -rwxr-xr-x | win11-qemu-install | 232 |
2 files changed, 365 insertions, 0 deletions
@@ -0,0 +1,133 @@ +win11-qemu-install +================== +Mostly-unattended install of Windows 11 onto a disk image under QEMU/KVM. +The script generates an Autounattend.xml, hands it to Windows Setup on a +virtual floppy, and runs QEMU against the install ISO and a target disk +created beforehand with qemu-img. The answer file presets the en-US locale, +bypasses the TPM 2.0, Secure Boot, and minimum RAM checks via LabConfig, +creates one local administrator, and auto-logs in once. The pages it leaves +alone (EULA, edition, target disk) are clicked once over VNC. + +Whatever disk is picked on Setup's disk page is wiped. + + +Requirements +------------ + - qemu-system-x86_64 with KVM and seccomp sandbox support + - a POSIX shell, sed, id, and mktemp + - a Windows 11 install ISO and a disk image made with qemu-img + - a VNC client such as remote-viewer for the Setup pages + - optionally the VirtIO driver ISO + +Run as an unprivileged user with access to /dev/kvm. The script refuses to +run as root. + + +Usage +----- +Create a disk, then run the install: + + qemu-img create -f qcow2 win11.qcow2 64G + ./win11-qemu-install win11.iso win11.qcow2 + +QEMU runs in the foreground. Connect a VNC client and click through the +pages the answer file leaves alone: + + remote-viewer vnc://127.0.0.1:5900 + +A third argument names the VirtIO driver ISO. The install disk is then +attached over VirtIO, which is faster: + + ./win11-qemu-install win11.iso win11.qcow2 virtio-win.iso + +Setup shows an empty disk list until viostor is loaded by hand: + + Load driver -> virtio CD -> viostor -> w11 -> amd64 + +Without it the disk is attached over SATA/AHCI and Setup sees it with no +extra drivers. The same driver ISO covers network and balloon after the +install. + + +Configuration +------------- +Environment variables, all optional. + + - RAM_MB guest memory in MB during install (default 8192) + - SMP guest CPU count during install (default 8) + - IMAGE_FORMAT disk image format: qcow2 or raw (default qcow2) + - LOCAL_USER administrator account name (default user) + - LOCAL_PASS administrator account password (default 123456) + +Example: + + RAM_MB=16384 SMP=4 LOCAL_USER=admin LOCAL_PASS=hunter2 \ + ./win11-qemu-install win11.iso win11.qcow2 + + +Networking +---------- +The installer runs with -nic none. With no way to reach Microsoft, OOBE +settles for a local account instead of forcing an online one, and restarting +inside Setup changes nothing. Networking arrives when the installed image is +booted afterwards, with a NIC model Windows has an in-box driver for: + + qemu-system-x86_64 -enable-kvm -machine q35 -cpu host -m 8192 \ + -drive file=win11.qcow2,if=ide \ + -nic user,model=e1000e \ + -vnc 127.0.0.1:0 -usb -device usb-tablet + +Use if=virtio for the drive if the install ran in VirtIO mode. virtio-net +stays dead until NetKVM is installed from the driver ISO; e1000e works out +of the box. + + +Quirks +------ +The windowsPE pass is minimal on purpose. Automating the EULA, edition, and +disk pages with UserData, DiskConfiguration, and DriverPaths makes the +Windows 11 24H2 setup engine abort right after boot ("Windows installation +encountered an unexpected error", extend code 0x40031), so those pages are +left to the VNC session. + +The answer file is written to a per-run mktemp directory (mode 0700, under +TMPDIR), reaches the guest as fat:floppy:, and is removed by an EXIT trap. +SIGKILL and host failure cannot run the trap; remove stale +win11-qemu-install.* directories from TMPDIR after either. + +QEMU runs as a background command so HUP, INT, and TERM stop it and then +clean up, exiting 129, 130, or 143. In the foreground the trap would not run +until QEMU had already exited. + +LOCAL_USER and LOCAL_PASS are XML-escaped, so values containing & < > " ' +are safe. + + +Debugging +--------- +Watch the install over VNC. While QEMU runs the generated answer file can be +read at win11-qemu-install.*/Autounattend.xml under TMPDIR. + + - "must not contain a comma": QEMU separates drive options with commas; + use a path without one. + - Black VNC screen: Setup is still booting. + - Empty disk list in Setup: load the VirtIO driver as shown above, or + rerun without the VirtIO ISO for the SATA/AHCI path. + - "installation encountered an unexpected error" right after boot: the + answer file automates more of windowsPE than the setup engine accepts. + - KVM errors on start: check that /dev/kvm exists and is accessible. + + +Security +-------- +The default password is 123456 and the account is a local administrator, so +this is for throwaway lab VMs. Set LOCAL_PASS for anything kept. VNC has no +authentication and any local host user can reach the guest console; it is +bound to 127.0.0.1, and the QEMU monitor, serial port, and guest network are +not exposed. + +The answer file holds the password in plain text in the 0700 temp dir. QEMU +refuses to run as root and runs under its seccomp sandbox with privilege +elevation, process spawning, obsolete system calls, and resource-control +changes denied, but it still has the invoking user's file access. Use a +dedicated unprivileged account when installing untrusted media. diff --git a/win11-qemu-install b/win11-qemu-install new file mode 100755 index 0000000..8426f09 --- /dev/null +++ b/win11-qemu-install @@ -0,0 +1,232 @@ +#!/bin/sh +set -eu + +# win11-qemu-install WINDOWS_ISO VM_DISK [VIRTIO_WIN_ISO] +# +# Mostly-unattended install of Windows 11 onto VM_DISK, a disk image created +# beforehand with qemu-img. The answer file presets the language, bypasses the +# TPM, Secure Boot, and RAM checks, and automates OOBE. Setup's remaining pages +# (EULA, edition, target disk) are clicked once over VNC on 127.0.0.1:5900. + +[ "$#" -eq 2 ] || [ "$#" -eq 3 ] || { echo "usage: $0 WINDOWS_ISO VM_DISK [VIRTIO_WIN_ISO]" >&2; exit 64; } + +[ "$(id -u)" -ne 0 ] || { echo "error: refusing to run QEMU as root" >&2; exit 1; } + +WIN_ISO=$1 +VM_DISK=$2 +VIRTIO_ISO=${3:-} +[ -r "$WIN_ISO" ] || { echo "error: WINDOWS_ISO not readable: $WIN_ISO" >&2; exit 1; } +[ -r "$VM_DISK" ] && [ -w "$VM_DISK" ] || { echo "error: VM_DISK must be readable and writable: $VM_DISK" >&2; exit 1; } +[ -z "$VIRTIO_ISO" ] || [ -r "$VIRTIO_ISO" ] || { echo "error: VIRTIO_WIN_ISO not readable: $VIRTIO_ISO" >&2; exit 1; } + +RAM_MB=${RAM_MB:-8192} +SMP=${SMP:-8} +IMAGE_FORMAT=${IMAGE_FORMAT:-qcow2} +LOCAL_USER=${LOCAL_USER:-user} +LOCAL_PASS=${LOCAL_PASS:-123456} + +validate_positive_integer() { + case $2 in + ''|*[!0-9]*|0) echo "error: $1 must be a positive integer: $2" >&2; exit 1 ;; + esac +} + +reject_comma() { + case $2 in + *,*) echo "error: $1 must not contain a comma: $2" >&2; exit 1 ;; + esac +} + +validate_positive_integer RAM_MB "$RAM_MB" +validate_positive_integer SMP "$SMP" +case $IMAGE_FORMAT in + qcow2|raw) ;; + *) echo "error: IMAGE_FORMAT must be qcow2 or raw: $IMAGE_FORMAT" >&2; exit 1 ;; +esac +reject_comma WINDOWS_ISO "$WIN_ISO" +reject_comma VM_DISK "$VM_DISK" +reject_comma VIRTIO_WIN_ISO "$VIRTIO_ISO" +reject_comma TMPDIR "${TMPDIR:-/tmp}" + +command -v qemu-system-x86_64 >/dev/null || { echo "error: need qemu-system-x86_64" >&2; exit 1; } + +xml_escape() { + sed \ + -e 's/&/\&/g' \ + -e 's/</\</g' \ + -e 's/>/\>/g' \ + -e 's/"/\"/g' \ + -e "s/'/\'/g" +} + +LOCAL_USER_XML=$(printf '%s' "$LOCAL_USER" | xml_escape) +LOCAL_PASS_XML=$(printf '%s' "$LOCAL_PASS" | xml_escape) + +# The answer file carries LOCAL_PASS in plain text; mktemp gives a 0700 dir. +WORK=$(mktemp -d "${TMPDIR:-/tmp}/win11-qemu-install.XXXXXX") +QEMU_PID= +trap 'rm -rf "$WORK"' EXIT + +# QEMU runs in the background so a trapped signal is handled while it is +# still alive; in the foreground the trap only runs once QEMU has exited. +# QEMU shuts down on TERM, so that is what gets sent on whatever arrived. +stop() { + trap - HUP INT TERM + if [ -n "$QEMU_PID" ]; then + kill "$QEMU_PID" 2>/dev/null || : + wait "$QEMU_PID" 2>/dev/null || : + fi + exit "$1" +} +trap 'stop 129' HUP +trap 'stop 130' INT +trap 'stop 143' TERM + +# The windowsPE pass stays minimal on purpose: LabConfig bypasses and +# language only. Automating the EULA, edition, and disk pages with UserData, +# DiskConfiguration, and DriverPaths makes the Windows 11 24H2 setup engine +# abort right after boot (extend code 0x40031), so those pages are clicked +# once over VNC instead. +cat <<EOF >"$WORK/Autounattend.xml" +<?xml version="1.0" encoding="utf-8"?> +<unattend xmlns="urn:schemas-microsoft-com:unattend" + xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"> + <settings pass="windowsPE"> + <component name="Microsoft-Windows-International-Core-WinPE" + processorArchitecture="amd64" + publicKeyToken="31bf3856ad364e35" + language="neutral" + versionScope="nonSxS"> + <SetupUILanguage> + <UILanguage>en-US</UILanguage> + </SetupUILanguage> + <InputLocale>0409:00000409</InputLocale> + <SystemLocale>en-US</SystemLocale> + <UILanguage>en-US</UILanguage> + <UserLocale>en-US</UserLocale> + </component> + + <component name="Microsoft-Windows-Setup" + processorArchitecture="amd64" + publicKeyToken="31bf3856ad364e35" + language="neutral" + versionScope="nonSxS"> + <RunSynchronous> + <RunSynchronousCommand wcm:action="add"> + <Order>1</Order> + <Description>Create LabConfig</Description> + <Path>cmd /c reg add HKLM\\SYSTEM\\Setup\\LabConfig /f</Path> + </RunSynchronousCommand> + <RunSynchronousCommand wcm:action="add"> + <Order>2</Order> + <Description>Bypass TPM</Description> + <Path>cmd /c reg add HKLM\\SYSTEM\\Setup\\LabConfig /v BypassTPMCheck /t REG_DWORD /d 1 /f</Path> + </RunSynchronousCommand> + <RunSynchronousCommand wcm:action="add"> + <Order>3</Order> + <Description>Bypass Secure Boot</Description> + <Path>cmd /c reg add HKLM\\SYSTEM\\Setup\\LabConfig /v BypassSecureBootCheck /t REG_DWORD /d 1 /f</Path> + </RunSynchronousCommand> + <RunSynchronousCommand wcm:action="add"> + <Order>4</Order> + <Description>Bypass RAM check</Description> + <Path>cmd /c reg add HKLM\\SYSTEM\\Setup\\LabConfig /v BypassRAMCheck /t REG_DWORD /d 1 /f</Path> + </RunSynchronousCommand> + </RunSynchronous> + </component> + </settings> + + <settings pass="oobeSystem"> + <component name="Microsoft-Windows-International-Core" + processorArchitecture="amd64" + publicKeyToken="31bf3856ad364e35" + language="neutral" + versionScope="nonSxS"> + <InputLocale>en-US</InputLocale> + <SystemLocale>en-US</SystemLocale> + <UILanguage>en-US</UILanguage> + <UserLocale>en-US</UserLocale> + </component> + + <component name="Microsoft-Windows-Shell-Setup" + processorArchitecture="amd64" + publicKeyToken="31bf3856ad364e35" + language="neutral" + versionScope="nonSxS"> + <OOBE> + <HideOnlineAccountScreens>true</HideOnlineAccountScreens> + <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> + <HideEULAPage>true</HideEULAPage> + <ProtectYourPC>3</ProtectYourPC> + </OOBE> + + <UserAccounts> + <LocalAccounts> + <LocalAccount wcm:action="add"> + <Name>${LOCAL_USER_XML}</Name> + <DisplayName>${LOCAL_USER_XML}</DisplayName> + <Group>Administrators</Group> + <Password> + <Value>${LOCAL_PASS_XML}</Value> + <PlainText>true</PlainText> + </Password> + </LocalAccount> + </LocalAccounts> + </UserAccounts> + + <AutoLogon> + <Enabled>true</Enabled> + <Username>${LOCAL_USER_XML}</Username> + <LogonCount>1</LogonCount> + <Password> + <Value>${LOCAL_PASS_XML}</Value> + <PlainText>true</PlainText> + </Password> + </AutoLogon> + </component> + </settings> +</unattend> +EOF + +echo 'use: remote-viewer vnc://127.0.0.1:5900' + +# WinPE scans floppy drives for Autounattend.xml on its own; a vvfat floppy +# holding the single file is the simplest way to hand it over. usb-tablet +# gives the VNC pointer absolute positioning. No NIC: with no way to reach +# Microsoft, OOBE settles for a local account instead of forcing an online +# one. +set -- qemu-system-x86_64 \ + -enable-kvm \ + -machine q35 \ + -cpu host \ + -smp "$SMP" \ + -m "$RAM_MB" \ + -boot order=d \ + -sandbox on,obsolete=deny,elevateprivileges=deny,spawn=deny,resourcecontrol=deny \ + -drive file="$WIN_ISO",format=raw,media=cdrom,if=ide,index=1 \ + -drive if=floppy,file="fat:floppy:$WORK",format=raw,readonly=on \ + -vnc 127.0.0.1:0 \ + -monitor none \ + -serial none \ + -parallel none \ + -usb \ + -device usb-tablet \ + -nic none + +if [ -n "$VIRTIO_ISO" ]; then + echo "virtio mode: enabled; when Setup's disk list is empty:" + echo " Load driver -> virtio CD -> viostor -> w11 -> amd64" + set -- "$@" \ + -drive file="$VM_DISK",format="$IMAGE_FORMAT",if=virtio \ + -drive file="$VIRTIO_ISO",format=raw,media=cdrom,if=ide,index=2 +else + echo "virtio mode: disabled; disk attached over SATA/AHCI, no extra drivers" + set -- "$@" \ + -device ich9-ahci,id=sata \ + -drive file="$VM_DISK",format="$IMAGE_FORMAT",if=none,id=systemdisk \ + -device ide-hd,drive=systemdisk,bus=sata.0 +fi + +"$@" & +QEMU_PID=$! +wait "$QEMU_PID" |