Table of Contents

About

The Pre-boot eXecution Environment (PXE) is useful in case you need to perform many operating system installs and allows you to boot up a system, automatically acquire an IP address via DHCP and start booting a kernel over the network. It is also useful, in cases where you need to administer systems and you would like to start some recovery ISO on a machine without having to haul your recovery tools around.

PXE works by:

and may have to be enabled in the BIOS.

Setting up TFTP

On Debian, the tftpd-hpa package must be installed:

aptitude install tftpd-hpa

and the /etc/default/tftpd-hpa file can be checked for the TFTP_DIRECTORY which is conventionally placed in /srv/tftp. It is a good idea to edit /etc/default/tftpd-hpa and modify the listening address to the address of the TFTP server:

# /etc/default/tftpd-hpa

TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/srv/tftp"
TFTP_ADDRESS="192.168.1.1:69"
TFTP_OPTIONS="--secure"

which listens on 192.168.1.1 port 69.

Next, we install syslinux-common to grab memdisk and the rest of the required modules:

aptitude install syslinux-common

Now we can set-up the TFTP tree in /srv/tftp. The structure of /srv/tftp is the following:

+
|
+- boot.txt
+- iso
|   +
|   |
|   +- debian.iso
+- memdisk
+- ldlinux.c32
+- libutil.c32
+- libcom32.c32
+- pxelinux.0
+- pxelinux.cfg
       +
       |
       +- default

with the following description of the files:

cp /usr/lib/syslinux/modules/bios/{ldlinux,libutil,libcom32}.c32 /srv/tftp/
cp /usr/lib/syslinux/memdisk /srv/tftp/
cp /usr/lib/PXELINUX/pxelinux.0 /srv/tftp/

Once these files are in-place, we can proceed to configuring the /srv/tftp/pxelinux.cfg/default and the /srv/tftp/boot.txt files in order to build a menu of options that a client can boot over the network.

Configuring the Boot Menu and Images to Boot

We edit /srv/tftp/pxelinux.cfg/default in order to include the ISO files that can be booted:

# The boot text
DISPLAY boot.txt

# Prompt for 10s
PROMPT 10
# Don't timeout
TIMEOUT 0

# By default boot the Debian ISO
DEFAULT debian

# The Debian ISO configuration
LABEL debian
  KERNEL memdisk
  INITRD iso/debian.iso
  APPEND iso
  
# Boot from Local Hard-Drive
LABEL local
   LOCALBOOT 0

As well as the /srv/tftp/boot.txt file to list the labels:

###########################################################################
##                      Wizardry and Steamworks                          ##
##                           PXE Boot Menu                               ##
###########################################################################

debian
local

which lists debian since it is our only label / image so far.

Setting up DHCP

The following lines have to added to the subnet declaration:

  filename "pxelinux.0";
  next-server 192.168.1.1;

where:

Troubleshooting

Make sure that the ISO images in /srv/tftp/iso/ have read permissions for everybody.

Support Open Source Boot Firmware (iPXE)

In order to support iPXE as well seamlessly the option is to chain-load iPXE. In order to do this, download the iPXE chain-load boot file and save it to /srv/tftp/ in the same directory as pxelinux.0 (note the new undionly.kpxe):

+
|
+- boot.txt
+- iso
|   +
|   |
|   +- debian.iso
+- memdisk
+- ldlinux.c32
+- libutil.c32
+- libcom32.c32
+- pxelinux.0
+- undionly.kpxe (new)
+- pxelinux.cfg
       +
       |
       +- default

After that, remove the DHCP lines:

  filename "pxelinux.0";
  next-server 192.168.1.1;

and instead define a global class:

class "PC-NetBoot" {
    match if ( substring(option vendor-class-identifier, 0, 9) = "PXEClient" );

    # After iPXE, load PXE.
    if exists user-class and option user-class = "iPXE" {
        filename "/srv/tftp/pxe/PC/pxelinux.0";
        next-server 172.16.1.1;
    # If this is a regular PXE client, load PXE.
    } elsif substring(option vendor-class-identifier, 0, 9) = "PXEClient" {
        filename "/srv/tftp/pxe/PC/pxelinux.0";
    # Otherwise, chainload iPXE.
    } else {
        filename "/srv/tftp/pxe/PC/undionly.kpxe";
    }
}

This class will match all the PXE booting clients. In case the client is using iPXE, the undionly.kpxe boot-file will be used and the pxelinux.0 will be loaded next. In case it is a regular PXE client, then the pxelinux.0 boot file will be loaded directly - which is what the second clause takes care of.

Installing Windows

PXE can be used to boot any ISO image file via the memdisk kernel over the network but in case the ISO file is too large, the transfer would take too long or the machine transferring the ISO might run out of RAM. Such is the case for Windows ISO install DVDs that are officially distributed with a filesize over $3GiB$, sometimes reaching up to $6GiB$, all of which would have to be transferred over the network and loaded into the RAM of the machine wishing to install Windows.

Fortunately, there is a solution by creating a Windows Pre-Execution ISO that has a fair size of around $100MiB$ and can be loaded into RAM in order to stage the real Windows install via a network share.

Workflow Diagram

Create a WinPE Image

Download the AIK image file, in this case, for Windows 7 KB3AIK_EN.iso and then create a WinPE image from the ISO. This can be accomplished under Linux as well as Windows.

For Linux and on Debian:

mkdir -p /mnt/aik
mount KB3AIK_EN.iso /mnt/aik
mkwinpeimg -a amd64 --iso --waik-dir=/mnt/aik winpe.iso
mkdir -p /mnt/winpe
mount winpe.iso /mnt/winpe
cp /mnt/winpe/sources/boot.wim /tmp/boot.wim
mkdir -p /mnt/boot.wim
wimmountrw /tmp/boot.wim /mnt/boot.wim
@echo off
wpeinit

rem -- The server with the Samba share containing the Windows install.
set INSTALL_SERVER=pxe
rem -- The name of the Samba share containg the Windows install.
set INSTALL_SHARE=pxe-windows7
rem -- The account credentials for the Samba share.
set INSTALL_ACCOUNT=pxe
set INSTALL_PASSWORD=pxe

rem -- Get the default gateway.
for /f "tokens=*" %%s in ('ipconfig ^| find /n "Default Gateway"') do (
  set NETCONFIG=%%s
)
 
for /f "tokens=2 delims=:" %%s in ("%NETCONFIG%") do (
  set GATEWAY=%%s
)

rem -- Remove spaces around the gateway.
set GATEWAY=%GATEWAY: =%
 
ping %GATEWAY% -n 3 -w 1000 > NUL
ping %INSTALL_SERVER% > NUL
 
net use W: \\%INSTALL_SERVER%\%INSTALL_SHARE% /user:%INSTALL_ACCOUNT% %INSTALL_PASSWORD%
W:setup.exe
wimunmount /mnt/boot.wim --commit
mkwinpeimg -a amd64 --iso -w /tmp/boot.wim --waik-dir=/mnt/aik winpe-updated.iso
umount /mnt/winpe
umount /mnt/aik
cp winpe-updated.iso winpe.iso

winpe.iso is now the final WinPE ISO that has to be booted via PXE. The next step would be to slipstream some required drivers into the WinPE image.

Slipstream Drivers onto WinPE Image

Drivers can be loaded in the pre-execution envionment via the drivload command so, for this example, we are going to slipstream libvirt drivers into the WinPE ISO.

wget -c https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/latest-virtio/virtio-win.iso
mkdir -p /mnt/virtio-win
mount virtio-win.iso /mnt/virtio-win

Now the same procedure as per the previous section is repeated in order to gain read-write access to boot.wim.

mkdir -p /mnt/boot.wim/Windows/System32/drivers/virtio
cp /mnt/virtio-win/{viostor,NetKVM}/w7/amd64/* /mnt/boot.wim/Windows/System32/drivers/virtio
@echo off

rem ------------------------------------------------------------------------------------------------
rem --                                  CONFIGURATION                                             --
rem ------------------------------------------------------------------------------------------------

rem -- The server with the Samba share containing the Windows install.
set INSTALL_SERVER=pxe
rem -- The name of the Samba share containg the Windows install.
set INSTALL_SHARE=pxe-windows7
rem -- The account credentials for the Samba share.
set INSTALL_DOMAIN=WORKGROUP
set INSTALL_ACCOUNT=pxe
set INSTALL_PASSWORD=pxe

rem ------------------------------------------------------------------------------------------------
rem --                                    INTERNALS                                               --
rem ------------------------------------------------------------------------------------------------

rem -- Load drivers.
echo Loading drivers...
for /f %%f in ('dir /b drivers\virtio\*.inf') do echo drvload %%f > nul

rem -- Start WinPE initialization.
echo Windows PE initializing...
wpeinit
rem -- Not supported on WinPE 7:
rem -- wpeutil WaitForNetwork > nul

rem -- Get the default gateway.
echo Retrieving the default gateway...
for /f "tokens=*" %%s in ('ipconfig ^| find /n "Default Gateway"') do (
  set NETCONFIG=%%s
)
 
for /f "tokens=2 delims=:" %%s in ("%NETCONFIG%") do (
  set GATEWAY=%%s
)

rem -- Remove spaces around the gateway.
set GATEWAY=%GATEWAY: =%

rem -- Set network parameters.
echo Optimizing network...
rem -- Disable firewall.
wpeutil DisableFirewall > nul
rem -- Workaround for TCP autotuning bugs.
netsh interface tcp set global autotuning=disabled > nul
rem -- Start the DNS cache.
net start dnscache > nul

rem -- Sleep for 10 seconds.
echo Waiting 10 seconds for network to settle...
ping %GATEWAY% -n 5 > nul
ping %INSTALL_SERVER% -n 5 > null

rem -- Mount network share.
echo Mouting setup files...
net use * /delete > nul
net use W: \\%INSTALL_SERVER%\%INSTALL_SHARE% /user:%INSTALL_DOMAIN%\%INSTALL_ACCOUNT% %INSTALL_PASSWORD% > nul

rem -- Sleep for 5 seconds.
echo Waiting 5 seconds for mount to settle...
ping %GATEWAY% -n 5 > nul

rem -- Start Windows setup.
echo Setup starting in 5 seconds...
W:setup.exe

rem -- Trap and reboot if user aborts setup.
echo Rebooting...
wpeutil Reboot > nul

Now proceed with the rest of the steps in the previous section in order to commit the changes and generate the new WinPE ISO winpe.iso.

Create Samba Share

A Samba share must be set up, corresponding to the settings in the startnet.cmd script, namely, INSTALL_SERVER, INSTALL_SHARE, INSTALL_ACCOUNT and INSTALL_PASSWORD. Following the settings in the scripts provided in the previous sections, a corresponding Samba share configuration would be:

[pxe-windows7]
     comment = PXE Windows 7 Install
     path = /srv/tftp/pxe/PC/iso/pxe-windows7
     valid users = pxe
     force user = pxe
     force group = pxe
     create mask = 0664
     force create mode = 0664
     force directory mode = 0755
     read only = No

Setting up PXE

Finally, there last requirement is to add a menu entry to PXE to load the WinPE ISO file winpe.iso that was generated in the previous sections.

The corresponding pxelinux.cfg/default menu entry would be:

LABEL Windows 7 Install (AMD64)
    KERNEL memdisk
    INITRD iso/pxe-windows7.iso
    APPEND iso raw

where pxe-windows7.iso is a copy of winpe.iso - the reason therefore is that the ISO generated in the previous sections has now been fully customized to install Windows 7 and is not a generic WinPE image anymore.

All-in-One Script for Re-Creating the WinPE Image File

The following script can be used to re-create the WinPE image by downloading all the necessary components, editing the netstart.cmd file and then bundling everything back together into a new WinPE ISO image.

#!/bin/bash
###########################################################################
##  Copyright (C) Wizardry and Steamworks 2020 - License: PD             ##
###########################################################################
###########################################################################
## All-in-one dirty script to change the startnet.cmd of a WinPE image   ##
## and generate the modified result as a WinPE ISO file bootable via PXE ##
## in order to install Windows from a network share.                     ##
##                                                                       ##
## Bonus: the script will slipstream virtio network and storage drivers. ##
## Requirements: wimtools, wget and loopback mount kernel support.       ##
###########################################################################
 
wget -c https://download.microsoft.com/download/8/E/9/8E9BBC64-E6F8-457C-9B8D-F6C9A16E6D6A/KB3AIK_EN.iso
 
mkdir -p /mnt/aik
mount KB3AIK_EN.iso /mnt/aik
 
mkwinpeimg -a amd64 --iso --waik-dir=/mnt/aik winpe.iso
 
mkdir -p /mnt/winpe
mount winpe.iso /mnt/winpe
 
cp /mnt/winpe/sources/boot.wim /tmp/boot.wim
 
mkdir -p /mnt/boot.wim
wimmountrw /tmp/boot.wim /mnt/boot.wim
 
# Edit the startnet.cmd script.
editor /mnt/boot.wim/Windows/System32/startnet.cmd
 
wget -c https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/latest-virtio/virtio-win.iso
 
#################### BEGIN INJECT VIRTIO DRIVERS ##########################
# This section can be removed if not needed.
 
mkdir -p /mnt/virtio-win
mount virtio-win.iso /mnt/virtio-win
 
mkdir -p /mnt/boot.wim/Windows/System32/drivers/virtio
 
cp /mnt/virtio-win/{viostor,NetKVM}/w7/amd64/* /mnt/boot.wim/Windows/System32/drivers/virtio
 
##################### END INJECT VIRTIO DRIVERS ###########################
 
wimunmount /mnt/boot.wim --commit
 
mkwinpeimg -a amd64 --iso -w /tmp/boot.wim --waik-dir=/mnt/aik winpe-updated.iso
 
umount /mnt/winpe
umount /mnt/aik
 
cp winpe-updated.iso pxe-windows7.iso

Security

The modified WinPE ISO file transferred over the network contains the necessary credentials to access the Windows install Samba share. In turn, the Samba share that contains the Windows setup files has been created in this section by granting read-write permissions to the pxe user. Any machine that transfers the WinPE ISO file could peek inside the ISO, extract the startnet.cmd script and find out the username and password used to access the Samba share.

Even if an attacker gains access just to the Windows setup files, a trojan could be injected into the setup files thereby infecting all machines that install Windows.

There are several mitigations that could be established in order to prevent the misuse of the resources:

read only = Yes

in order to make sure that the Windows setup files cannot be tampered with.

Alternatively, the same type of privilege separation can be established when using a Windows domain controller instead of Samba.

Unattended (Automatic) Windows Install via PXE Menu

In corporate scenarios where the hardware is well-known in advance and installation keys are available, an unattended Windows setup is possible and perhaps preferable via PXE. This would require the following additional steps to set up:

W:setup.exe Autounattend.xml

instead of just W:setup.exe in case the Autounattend.xml file is placed into C:\Windows\System32 directory (the default current working directory when booting the WinPE ISO) of the WinPE ISO file.

Workaround for Various Issues

In case the Samba share does not seem to mount properly, it may be due to DFS and inspecting the Samba logs will reveal some errors concerning DFS as well as authentication issues. To work around the problem, include the following setting under the [global] section in the Samba configuration:

host msdfs = No

Final Notes

The extraction and modification of the WinPE image can also be perfromed on Windows and this guide was the equivalent for Linux.

Using the same method, it is possible to install any Windows version - different WinPE images would have to be generated, install DVDs copied to local folders (or bind-mounted), Samba shares created to match the Windows version and PXE entries made to boot the WinPE images to stage the real Windows install.

With all the configuration in place, any machine on the local network will be able to install various operating systems just by connecting the machine to the network and then picking the desired operating system to be installed.

Given that the PXE, Samba and files do not have to be on different servers, perhaps a Raspberry Pi could be used to provide the means to install a bunch of operating systems whilst sill being energy-efficient and a low-cost solution.