# Wizard Kit: Windows PE Build Tool ## Init ## #Requires -Version 3.0 #Requires -RunAsAdministrator if (Test-Path Env:\DEBUG) { Set-PSDebug -Trace 1 } try { Import-Module -Name $Env:DISMRoot -ErrorAction "stop" } catch { Write-Host -ForegroundColor "Red" "ERROR: Failed to load DISM CmdLet" Abort } # Dirs $WorkingDir = $(Split-Path $MyInvocation.MyCommand.Path) $SetupDir = (Get-Item $WorkingDir -Force).Parent.FullName $RootDir = (Get-Item $SetupDir -Force).Parent.FullName $BuildDir = "$SetupDir\BUILD_PE" $OutDir = "$SetupDir\OUT_PE" $TempDir = "$BuildDir\temp" $MountDir = "$BuildDir\mount" $TargetDir = "$BuildDir\pe_files" # Misc $Arch = "amd64" $Date = Get-Date -UFormat "%Y-%m-%d" $DISM = "{0}\DISM.exe" -f $Env:DISMRoot $HostSystem32 = "{0}\System32" -f $Env:SystemRoot $HostSysWOW64 = "{0}\SysWOW64" -f $Env:SystemRoot $KitNameFull = (Get-Content "$RootDir\scripts\wk\cfg\main.py" | Where-Object {$_ -imatch '^KIT_NAME_FULL'}) -ireplace '.*=.(.*).$', '$1' $KitNameShort = (Get-Content "$RootDir\scripts\wk\cfg\main.py" | Where-Object {$_ -imatch '^KIT_NAME_SHORT'}) -ireplace '.*=.(.*).$', '$1' # Set up UI $Host.UI.RawUI.WindowTitle = "${KitNameFull}: Windows PE Build Tool" $Host.UI.RawUI.BackgroundColor = "Black" $Host.UI.RawUI.ForegroundColor = "White" # Enable TLS 1.2 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ## Functions ## function Abort { Write-Host -ForegroundColor "Red" "`nAborted." WKPause "Press Enter to exit... " exit } function Ask-User ($text = "Kotaero") { $text += " [Y/N]" while ($true) { $answer = read-host $text if ($answer -imatch "^(y|yes)$") { $answer = $true break } elseif ($answer -imatch "^(n|no|nope)$") { $answer = $false break } } $answer } function Clean-BuildDir { $Folders = @( "$BuildDir\additions", "$BuildDir\mount", "$BuildDir\pe_files", "$BuildDir\temp") # WIM cleanup if (Test-Path "$MountDir") { try { Dismount-WindowsImage -Path "$MountDir" -Discard } catch { # Ignore } Start-Process -FilePath $DISM -ArgumentList @("/Cleanup-Mountpoints") -NoNewWindow -Wait } # Folders foreach ($f in $Folders) { if (Test-Path $f) { Write-Host -ForegroundColor "Yellow" ("Removing: {0}" -f $f) Remove-Item -Path $f -Recurse -Force } } } function Download-File ($Path, $Name, $Url) { $OutFile = "{0}\{1}" -f $Path, $Name Write-Host ("Downloading: $Name") New-Item -Type Directory $Path 2>&1 | Out-Null try { Invoke-WebRequest -Uri $Url -OutFile $OutFile } catch { Write-Host (" ERROR: Failed to download file." ) -ForegroundColor "Red" Abort } } function Download-SourceFiles { $Sources = Get-Content -Path "$WorkingDir\sources.json" | ConvertFrom-JSON foreach ($s in $Sources) { $Dest = "$BuildDir\downloads\{0}" -f $s.Name if (Test-Path -PathType Leaf -Path "$Dest") { if (Test-Hash -File $Dest -Hash $s.Hash) { continue } # Hash didn't match Remove-Item $Dest -Force } # File needs downloaded (again) Download-File -Path "$BuildDir\downloads" -Name $s.Name -Url $s.Url # Verify download if ( -Not (Test-Hash -File $Dest -Hash $s.Hash)) { Write-Host (" ERROR: Download failed hash check." ) -ForegroundColor "Red" Abort } } } function Extract-SourceFiles { $ProgramFilesPE = "$BuildDir\additions\Program Files" New-Item -Type Directory $ProgramFilesPE -Force # 7-Zip Write-Host "Extracting: 7-Zip" try { $ArgumentList = @("/a", "$BuildDir\downloads\7z.msi", "TARGETDIR=$TempDir\7zi", "/qn") Start-Process -FilePath "$HostSystem32\msiexec.exe" -ArgumentList $ArgumentList -Wait New-Item -Type Directory "$ProgramFilesPE\7-Zip" 2>&1 | Out-Null Move-Item "$TempDir\7zi\Files\7-Zip\7z.dll" "$ProgramFilesPE\7-Zip\7z.dll" Move-Item "$TempDir\7zi\Files\7-Zip\7z.exe" "$ProgramFilesPE\7-Zip\7z.exe" Move-Item "$TempDir\7zi\Files\7-Zip\License.txt" "$ProgramFilesPE\7-Zip\License.txt" } catch { Write-Host (" ERROR: Failed to extract files." ) -ForegroundColor "Red" } $SevenZip = "$ProgramFilesPE\7-Zip\7z.exe" # ConEmu Write-Host "Extracting: ConEmu" try { $ArgumentList = @( "x", "$BuildDir\downloads\ConEmu.7z", "-o`"$ProgramFilesPE`"\ConEmu", "-aoa", "-bso0", "-bse0", "-bsp0", "ConEmu.exe", "ConEmu.map", "ConEmu64.exe", "ConEmu64.map", "ConEmu\CmdInit.cmd", "ConEmu\ConEmuC.exe", "ConEmu\ConEmuC64.exe", "ConEmu\ConEmuCD.dll", "ConEmu\ConEmuCD64.dll", "ConEmu\ConEmuHk.dll", "ConEmu\ConEmuHk64.dll" ) Start-Process -FilePath $SevenZip -ArgumentList $ArgumentList -NoNewWindow -Wait } catch { Write-Host (" ERROR: Failed to extract files." ) -ForegroundColor "Red" } # Notepad++ Write-Host "Extracting: Notepad++" try { $ArgumentList = @( "x", "$BuildDir\downloads\npp.7z", "-o`"$ProgramFilesPE`"\NotepadPlusPlus", "-aoa", "-bso0", "-bse0", "-bsp0") Start-Process -FilePath $SevenZip -ArgumentList $ArgumentList -NoNewWindow -Wait } catch { Write-Host (" ERROR: Failed to extract files." ) -ForegroundColor "Red" } # NTPWEdit Write-Host "Extracting: NTPWEdit" try { $ArgumentList = @( "e", "$BuildDir\downloads\ntpwedit.zip", "-o`"$ProgramFilesPE`"\NTPWEdit", "-aoa", "-bso0", "-bse0", "-bsp0", "COPYING.txt", "GPL.txt", "ntpwedit64.exe") Start-Process -FilePath $SevenZip -ArgumentList $ArgumentList -NoNewWindow -Wait Move-Item "$ProgramFilesPE\NTPWEdit\ntpwedit64.exe" "$ProgramFilesPE\NTPWEdit\ntpwedit.exe" } catch { Write-Host (" ERROR: Failed to extract files." ) -ForegroundColor "Red" } # wimlib Write-Host "Extracting: wimlib" try { $ArgumentList = @( "x", "$BuildDir\downloads\wimlib.zip", "-o`"$ProgramFilesPE`"\wimlib", "-aoa", "-bso0", "-bse0", "-bsp0") Start-Process -FilePath $SevenZip -ArgumentList $ArgumentList -NoNewWindow -Wait } catch { Write-Host (" ERROR: Failed to extract files." ) -ForegroundColor "Red" } } function Find-DynamicUrl ($SourcePage, $RegEx) { # Get source page Invoke-Webrequest -Uri $SourcePage -OutFile "tmp_page" # Search for real url $Url = Get-Content "tmp_page" | Where-Object {$_ -imatch $RegEx} $Url = $Url -ireplace '.*(a |)href="([^"]+)".*', '$2' $Url = $Url -ireplace ".*(a |)href='([^']+)'.*", '$2' # Remove tmp_page Remove-Item "tmp_page" $Url | Select-Object -First 1 } function Test-Hash ($File, $Hash) { Write-Host -BackgroundColor Black -ForegroundColor Cyan "Verifying ${File}..." $FileHash = (Get-FileHash -Path $File -Algorithm SHA256 -ErrorAction Stop).Hash return ($FileHash.ToLower() -eq $Hash) } function WKPause ($Message = "Press Enter to continue... ") { Write-Host $Message -NoNewLine Read-Host } ## Safety Check ## if ($PSVersionTable.PSVersion.Major -eq 6 -and $PSVersionTable.OS -imatch "Windows 6.1") { Write-Host "`nThis script doesn't support PowerShell 6.0 on Windows 7." Write-Host "Press Enter to exit... " -NoNewLine Abort } ## PowerShell equivalent of Python's "if __name__ == '__main__'" # Code based on StackOverflow comments # Question: https://stackoverflow.com/q/4693947 # Using answer: https://stackoverflow.com/a/5582692 # Asked by: https://stackoverflow.com/users/65164/mark-mascolino # Answer by: https://stackoverflow.com/users/696808/bacon-bits if ($MyInvocation.InvocationName -ne ".") { Write-Host "Wizard Kit: Windows PE Build Tool`n`n`n`n`n" Push-Location "$WorkingDir" New-Item -Type Directory $BuildDir 2>&1 | Out-Null Clean-BuildDir Copy-Item -Path "$SetupDir\pe\additions" -Destination "$BuildDir\additions" -Recurse -Force ## Download Sources ## Download-SourceFiles Extract-SourceFiles ## Build ## # Copy WinPE files Write-Host "Copying files..." $Cmd = ("{0}\copype.cmd" -f $Env:WinPERoot) Start-Process -FilePath $Cmd -ArgumentList @($Arch, $TargetDir) -NoNewWindow -Wait # Remove unwanted items foreach ($SubDir in @("media", "media\Boot", "media\EFI\Microsoft\Boot")) { foreach ($Item in Get-ChildItem "$TargetDir\$SubDir") { if ($Item.Name -inotmatch "^(boot|efi|en-us|sources|fonts|resources|bcd|memtest)") { Remove-Item -Path $Item.FullName -Recurse -Force } } } # Mount image Write-Host "Mounting image..." New-Item -Path $MountDir -ItemType "directory" -Force | Out-Null Mount-WindowsImage -Path $MountDir -ImagePath "$TargetDir\media\sources\boot.wim" -Index 1 # Add drivers Write-Host "Adding drivers..." Add-WindowsDriver -Path $MountDir -Driver "$SetupDir\pe\drivers" -ForceUnsigned -Recurse # Add packages Write-Host "Adding packages..." $WinPEPackages = @( "WinPE-EnhancedStorage", "WinPE-FMAPI", "WinPE-WMI", "WinPE-SecureStartup" ) foreach ($Package in $WinPEPackages) { $PackagePath = ("{0}\{1}\WinPE_OCs\{2}.cab" -f $Env:WinPERoot, $Arch, $Package) Write-Host " $Package..." Add-WindowsPackage –PackagePath $PackagePath –Path $MountDir $LangPackagePath = ("{0}\{1}\WinPE_OCs\en-us\{2}_en-us.cab" -f $Env:WinPERoot, $Arch, $Package) if (Test-Path $LangPackagePath) { Add-WindowsPackage –PackagePath $LangPackagePath –Path $MountDir } } # Set RamDisk size $ArgumentList = @( ('/Image:"{0}"' -f $MountDir), "/Set-ScratchSpace:512" ) Start-Process -FilePath $DISM -ArgumentList $ArgumentList -NoNewWindow -Wait # Add tools Write-Host "Copying tools..." Copy-Item -Path "$BuildDir\additions\*" -Destination $MountDir -Recurse -Force Copy-Item -Path "$RootDir\Images\WinPE.jpg" -Destination "$MountDir\Program Files\ConEmu\ConEmu.jpg" -Recurse -Force # Add System32 items $ArgumentList = @("/f", "$MountDir\Windows\System32\winpe.jpg", "/a") Start-Process -FilePath "$HostSystem32\takeown.exe" -ArgumentList $ArgumentList -NoNewWindow -Wait $ArgumentList = @("$MountDir\Windows\System32\winpe.jpg", "/grant", "Administrators:F") Start-Process -FilePath "$HostSystem32\icacls.exe" -ArgumentList $ArgumentList -NoNewWindow -Wait Copy-Item -Path "$RootDir\Images\WinPE.jpg" -Destination "$MountDir\Windows\System32\winpe.jpg" -Force # Load registry hives Write-Host "Updating Registry..." $Reg = "$HostSystem32\reg.exe" $ArgumentList = @("load", "HKLM\WinPE-SW", "$MountDir\Windows\System32\config\SOFTWARE") Start-Process -FilePath $Reg -ArgumentList $ArgumentList -NoNewWindow -Wait $ArgumentList = @("load", "HKLM\WinPE-SYS", "$MountDir\Windows\System32\config\SYSTEM") Start-Process -FilePath $Reg -ArgumentList $ArgumentList -NoNewWindow -Wait # Configure CMD (command aliases "DOSKEY Macros" and tab completion) $RegPath = "HKLM:\WinPE-SW\Microsoft\Command Processor" $NewValue = "doskey /macrofile=X:\Windows\System32\custom.doskey" New-Item -Path $RegPath -Force | Out-Null New-ItemProperty -Path $RegPath -Name "AutoRun" -Value $NewValue -Force | Out-Null New-ItemProperty -Path $RegPath -Name "CompletionChar" -Value 9 -Type DWord -Force | Out-Null New-ItemProperty -Path $RegPath -Name "PathCompletionChar" -Value 9 -Type DWord -Force | Out-Null # Add tools to path ## .NET code to properly handle REG_EXPAND_SZ values ## Credit: https://www.sepago.com/blog/2013/08/22/reading-and-writing-regexpandsz-data-with-powershell ## By: Marius Gawenda $Hive = [Microsoft.Win32.Registry]::LocalMachine $RegPath = "WinPE-SYS\ControlSet001\Control\Session Manager\Environment" $RegKey = $Hive.OpenSubKey($RegPath) $CurValue = $RegKey.GetValue( "Path", $false, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) $NewValue = "$CurValue;%ProgramFiles%\7-Zip;%ProgramFiles%\wimlib" Set-ItemProperty -Path "HKLM:\$RegPath" -Name "Path" -Value $NewValue -Force | Out-Null $Hive.close() $RegKey.close() # Replace Notepad $RegPath = "HKLM:\WinPE-SW\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\notepad.exe" $NewValue = 'cmd /c "%ProgramFiles%\NotepadPlusPlus\npp.cmd"' New-Item -Path $RegPath -Force | Out-Null New-ItemProperty -Path $RegPath -Name "Debugger" -Value $NewValue -Force | Out-Null # Run garbage collection to release potential stale handles ## Credit: https://jrich523.wordpress.com/2012/03/06/powershell-loading-and-unloading-registry-hives/ Start-Sleep -Seconds 2 [gc]::collect() # Unload registry hives Start-Sleep -Seconds 2 Start-Process -FilePath $Reg -ArgumentList @("unload", "HKLM\WinPE-SW") -NoNewWindow -Wait Start-Process -FilePath $Reg -ArgumentList @("unload", "HKLM\WinPE-SYS") -NoNewWindow -Wait # Unmount image Write-Host "Dismounting image..." Dismount-WindowsImage -Path $MountDir -Save # Rebuild image Write-Host "Recompressing image..." Move-Item "$TargetDir\media\sources\boot.wim" "$TargetDir\media\sources\boot-edited.wim" Export-WindowsImage -DestinationImagePath "$TargetDir\media\sources\boot.wim" -SourceImagePath "$TargetDir\media\sources\boot-edited.wim" -SourceIndex 1 -CompressionType Max # Create ISO New-Item -Type Directory "$SetupDir\OUT_PE" 2>&1 | Out-Null $ArgumentList = @("/iso", $TargetDir, "$SetupDir\OUT_PE\$KitNameShort-WinPE-$Date-$Arch.iso", "/f") $Cmd = "{0}\MakeWinPEMedia.cmd" -f $Env:WinPERoot Start-Process -FilePath $Cmd -ArgumentList $ArgumentList -NoNewWindow -Wait ## Cleanup ## Remove-Item -Path "$MountDir" -Recurse -Force ## Done ## Pop-Location Write-Host "`nDone." WKPause "Press Enter to exit... " }